Zettelstore

Check-in Differences
Login

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

Difference From v0.5.1 To v0.4

2022-08-02
10:06
Merge changes from release 0.5.1 ... (check-in: ad7c201f7e user: stern tags: trunk)
09:27
Version 0.5.1 ... (Leaf check-in: e10f2a270d user: stern tags: release, release-0.5, v0.5.1)
09:09
Add IP address when logging about some auth problems ... (check-in: b6687c0706 user: stern tags: release-0.5)
2022-03-10
13:20
Increase version to 0.5-dev to begin next development cycle ... (check-in: 73f6c7eb13 user: stern tags: trunk)
2022-03-09
14:06
Version 0.4 ... (check-in: 54ed47f372 user: stern tags: trunk, release, v0.4)
13:54
Update release checklist ... (check-in: 711a670d7a user: stern tags: trunk)

Changes to VERSION.

1


1
-
+
0.5.1
0.4

Changes to ast/ast.go.

60
61
62
63
64
65
66







67
68
69
70
71
72
73
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80







+
+
+
+
+
+
+







type DescriptionSlice []DescriptionNode

// InlineNode is the interface that all inline nodes must implement.
type InlineNode interface {
	Node
	inlineNode()
}

// InlineEmbedNode is a node that specifies some embeddings in inline mode.
// It is abstract, b/c there are different concrete type implementations.
type InlineEmbedNode interface {
	InlineNode
	inlineEmbedNode()
}

// Reference is a reference to external or internal material.
type Reference struct {
	URL   *url.URL
	Value string
	State RefState
}

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












-
+







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

import "zettelstore.de/c/attrs"
import "zettelstore.de/c/zjson"

// Definition of Block nodes.

// BlockSlice is a slice of BlockNodes.
type BlockSlice []BlockNode

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







+
+
+

-
-
+
+
+
+

-
+
+
+






-
+




-
+






-


-








-
-
-
-
-





-
+





-
+







	Inlines InlineSlice
}

func (*ParaNode) blockNode()       { /* Just a marker */ }
func (*ParaNode) itemNode()        { /* Just a marker */ }
func (*ParaNode) descriptionNode() { /* Just a marker */ }

// NewParaNode creates an empty ParaNode.
func NewParaNode() *ParaNode { return &ParaNode{} }

// CreateParaNode creates a parameter block from inline nodes.
func CreateParaNode(nodes ...InlineNode) *ParaNode { return &ParaNode{Inlines: nodes} }

func CreateParaNode(nodes ...InlineNode) *ParaNode {
	return &ParaNode{Inlines: nodes}
}

// WalkChildren walks down the inline elements.
func (pn *ParaNode) WalkChildren(v Visitor) { Walk(v, &pn.Inlines) }
func (pn *ParaNode) WalkChildren(v Visitor) {
	Walk(v, &pn.Inlines)
}

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

// VerbatimNode contains uninterpreted text
type VerbatimNode struct {
	Kind    VerbatimKind
	Attrs   attrs.Attributes
	Attrs   zjson.Attributes
	Content []byte
}

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

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

func (*VerbatimNode) blockNode() { /* Just a marker */ }
func (*VerbatimNode) itemNode()  { /* Just a marker */ }

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

// Supported syntax values for VerbatimEval.
const (
	VerbatimEvalSyntaxDraw = "draw"
)

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

// RegionNode encapsulates a region of block nodes.
type RegionNode struct {
	Kind    RegionKind
	Attrs   attrs.Attributes
	Attrs   zjson.Attributes
	Blocks  BlockSlice
	Inlines InlineSlice // Optional text at the end of the region
}

// RegionKind specifies the actual region type.
type RegionKind int
type RegionKind uint8

// Values for RegionCode
const (
	_           RegionKind = iota
	RegionSpan             // Just a span of blocks
	RegionQuote            // A longer quotation
	RegionVerse            // Line breaks matter
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
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







-
+


-
+






-
+
+
+





-
+














-
+







}

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

// HeadingNode stores the heading text and level.
type HeadingNode struct {
	Level    int
	Attrs    attrs.Attributes
	Inlines  InlineSlice // Heading text, possibly formatted
	Slug     string      // Heading text, normalized
	Fragment string      // Heading text, suitable to be used as an unique URL fragment
	Inlines  InlineSlice // Heading text, possibly formatted
	Attrs    zjson.Attributes
}

func (*HeadingNode) blockNode() { /* Just a marker */ }
func (*HeadingNode) itemNode()  { /* Just a marker */ }

// WalkChildren walks the heading text.
func (hn *HeadingNode) WalkChildren(v Visitor) { Walk(v, &hn.Inlines) }
func (hn *HeadingNode) WalkChildren(v Visitor) {
	Walk(v, &hn.Inlines)
}

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

// HRuleNode specifies a horizontal rule.
type HRuleNode struct {
	Attrs attrs.Attributes
	Attrs zjson.Attributes
}

func (*HRuleNode) blockNode() { /* Just a marker */ }
func (*HRuleNode) itemNode()  { /* Just a marker */ }

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

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

// NestedListNode specifies a nestable list, either ordered or unordered.
type NestedListNode struct {
	Kind  NestedListKind
	Items []ItemSlice
	Attrs attrs.Attributes
	Attrs zjson.Attributes
}

// NestedListKind specifies the actual list type.
type NestedListKind uint8

// Values for ListCode
const (

Changes to ast/inline.go.

9
10
11
12
13
14
15
16

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

16
17
18
19
20
21
22
23







-
+







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

package ast

import (
	"unicode/utf8"

	"zettelstore.de/c/attrs"
	"zettelstore.de/c/zjson"
)

// Definitions of inline nodes.

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

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







-

-
+
+















-

-

+
+


-
+
+


-
+
+
+





-
+

-

+


-
+
+


-
+
+
+





-


+





-
+
+
+







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

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

// LinkNode contains the specified link.
type LinkNode struct {
	Attrs   attrs.Attributes // Optional attributes
	Ref     *Reference
	Inlines InlineSlice // The text associated with the link.
	Inlines InlineSlice      // The text associated with the link.
	Attrs   zjson.Attributes // Optional attributes
}

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

// WalkChildren walks to the link text.
func (ln *LinkNode) WalkChildren(v Visitor) {
	if len(ln.Inlines) > 0 {
		Walk(v, &ln.Inlines)
	}
}

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

// EmbedRefNode contains the specified embedded reference material.
type EmbedRefNode struct {
	Attrs   attrs.Attributes // Optional attributes
	Ref     *Reference       // The reference to be embedded.
	Syntax  string           // Syntax of referenced material, if known
	Inlines InlineSlice      // Optional text associated with the image.
	Attrs   zjson.Attributes // Optional attributes
	Syntax  string           // Syntax of referenced material, if known
}

func (*EmbedRefNode) inlineNode() { /* Just a marker */ }
func (*EmbedRefNode) inlineNode()      { /* Just a marker */ }
func (*EmbedRefNode) inlineEmbedNode() { /* Just a marker */ }

// WalkChildren walks to the text that describes the embedded material.
func (en *EmbedRefNode) WalkChildren(v Visitor) { Walk(v, &en.Inlines) }
func (en *EmbedRefNode) WalkChildren(v Visitor) {
	Walk(v, &en.Inlines)
}

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

// EmbedBLOBNode contains the specified embedded BLOB material.
type EmbedBLOBNode struct {
	Attrs   attrs.Attributes // Optional attributes
	Blob    []byte           // BLOB data itself.
	Syntax  string           // Syntax of Blob
	Blob    []byte           // BLOB data itself.
	Inlines InlineSlice      // Optional text associated with the image.
	Attrs   zjson.Attributes // Optional attributes
}

func (*EmbedBLOBNode) inlineNode() { /* Just a marker */ }
func (*EmbedBLOBNode) inlineNode()      { /* Just a marker */ }
func (*EmbedBLOBNode) inlineEmbedNode() { /* Just a marker */ }

// WalkChildren walks to the text that describes the embedded material.
func (en *EmbedBLOBNode) WalkChildren(v Visitor) { Walk(v, &en.Inlines) }
func (en *EmbedBLOBNode) WalkChildren(v Visitor) {
	Walk(v, &en.Inlines)
}

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

// CiteNode contains the specified citation.
type CiteNode struct {
	Attrs   attrs.Attributes // Optional attributes
	Key     string           // The citation key
	Inlines InlineSlice      // Optional text associated with the citation.
	Attrs   zjson.Attributes // Optional attributes
}

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

// WalkChildren walks to the cite text.
func (cn *CiteNode) WalkChildren(v Visitor) { Walk(v, &cn.Inlines) }
func (cn *CiteNode) WalkChildren(v Visitor) {
	Walk(v, &cn.Inlines)
}

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

// MarkNode contains the specified merked position.
// It is a BlockNode too, because although it is typically parsed during inline
// mode, it is moved into block mode afterwards.
type MarkNode struct {
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
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







-

+





-
+
+
+






-
+




-
+

















-
+
+
+






-
+




-
+










-






	}
}

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

// FootnoteNode contains the specified footnote.
type FootnoteNode struct {
	Attrs   attrs.Attributes // Optional attributes
	Inlines InlineSlice      // The footnote text.
	Attrs   zjson.Attributes // Optional attributes
}

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

// WalkChildren walks to the footnote text.
func (fn *FootnoteNode) WalkChildren(v Visitor) { Walk(v, &fn.Inlines) }
func (fn *FootnoteNode) WalkChildren(v Visitor) {
	Walk(v, &fn.Inlines)
}

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

// FormatNode specifies some inline formatting.
type FormatNode struct {
	Kind    FormatKind
	Attrs   attrs.Attributes // Optional attributes.
	Attrs   zjson.Attributes // Optional attributes.
	Inlines InlineSlice
}

// FormatKind specifies the format that is applied to the inline nodes.
type FormatKind int
type FormatKind uint8

// Constants for FormatCode
const (
	_            FormatKind = iota
	FormatEmph              // Emphasized text.
	FormatStrong            // Strongly emphasized text.
	FormatInsert            // Inserted text.
	FormatDelete            // Deleted text.
	FormatSuper             // Superscripted text.
	FormatSub               // SubscriptedText.
	FormatQuote             // Quoted text.
	FormatSpan              // Generic inline container.
)

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

// WalkChildren walks to the formatted text.
func (fn *FormatNode) WalkChildren(v Visitor) { Walk(v, &fn.Inlines) }
func (fn *FormatNode) WalkChildren(v Visitor) {
	Walk(v, &fn.Inlines)
}

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

// LiteralNode specifies some uninterpreted text.
type LiteralNode struct {
	Kind    LiteralKind
	Attrs   attrs.Attributes // Optional attributes.
	Attrs   zjson.Attributes // Optional attributes.
	Content []byte
}

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

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

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

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

Changes to ast/ref.go.

1
2

3
4

5
6
7
8
9
10
11
1

2
3

4
5
6
7
8
9
10
11

-
+

-
+







//-----------------------------------------------------------------------------
// Copyright (c) 2020-2022 Detlef Stern
// Copyright (c) 2020-2021 Detlef Stern
//
// This file is part of Zettelstore.
// This file is part of zettelstore.
//
// Zettelstore is licensed 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 ast

Changes to ast/ref_test.go.

1
2

3
4

5
6
7
8
9
10
11
1

2
3

4
5
6
7
8
9
10
11

-
+

-
+







//-----------------------------------------------------------------------------
// Copyright (c) 2020-2022 Detlef Stern
// Copyright (c) 2020-2021 Detlef Stern
//
// This file is part of Zettelstore.
// This file is part of zettelstore.
//
// Zettelstore is licensed 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 ast_test

Changes to ast/walk.go.

1
2

3
4

5
6
7
8
9
10
11
1

2
3

4
5
6
7
8
9
10
11

-
+

-
+







//-----------------------------------------------------------------------------
// Copyright (c) 2021-2022 Detlef Stern
// Copyright (c) 2021 Detlef Stern
//
// This file is part of Zettelstore.
// This file is part of zettelstore.
//
// Zettelstore is licensed 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 ast

Changes to ast/walk_test.go.

9
10
11
12
13
14
15
16

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

16
17
18
19
20
21
22
23







-
+







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

package ast_test

import (
	"testing"

	"zettelstore.de/c/attrs"
	"zettelstore.de/c/zjson"
	"zettelstore.de/z/ast"
)

func BenchmarkWalk(b *testing.B) {
	root := ast.BlockSlice{
		&ast.HeadingNode{
			Inlines: ast.CreateInlineSliceFromWords("A", "Simple", "Heading"),
42
43
44
45
46
47
48
49

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

49
50
51
52
53
54
55
56







-
+







		},
		&ast.ParaNode{
			Inlines: ast.CreateInlineSliceFromWords("This", "is", "some", "intermediate", "text."),
		},
		ast.CreateParaNode(
			&ast.FormatNode{
				Kind: ast.FormatEmph,
				Attrs: attrs.Attributes(map[string]string{
				Attrs: zjson.Attributes(map[string]string{
					"":      "class",
					"color": "green",
				}),
				Inlines: ast.CreateInlineSliceFromWords("This", "is", "some", "emphasized", "text."),
			},
			&ast.SpaceNode{Lexeme: " "},
			&ast.LinkNode{

Changes to auth/impl/impl.go.

1
2

3
4

5
6
7
8
9
10
11
1

2
3

4
5
6
7
8
9
10
11

-
+

-
+







//-----------------------------------------------------------------------------
// Copyright (c) 2021-2022 Detlef Stern
// Copyright (c) 2021 Detlef Stern
//
// This file is part of Zettelstore.
// This file is part of zettelstore.
//
// Zettelstore is licensed 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 impl provides services for authentification / authorization.

Changes to box/constbox/base.css.

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







+












+
-
+
-
+
+




-
+







    clear: both;
    visibility: hidden;
  }
  main form div { margin: .5em 0 0 0 }
  input { font-family: monospace }
  input[type="submit"],button,select { font: inherit }
  label { font-family: sans-serif; font-size:.9rem }
  label::after { content:":" }
  textarea {
    font-family: monospace;
    resize: vertical;
    width: 100%;
  }
  .zs-input {
    padding: .5em;
    display:block;
    border:none;
    border-bottom:1px solid #ccc;
    width:100%;
  }
  .zs-button {
  input.zs-primary { float:right }
    float:right;
  input.zs-secondary { float:left }
    margin: .5em 0 .5em 1em;
  }
  a:not([class]) { text-decoration-skip-ink: auto }
  a.broken { text-decoration: line-through }
  img { max-width: 100% }
  img.right { float: right }
  ol.zs-endnotes {
  ol.endnotes {
    padding-top: .5rem;
    border-top: 1px solid;
  }
  kbd { font-family:monospace }
  code,pre {
    font-family: monospace;
    font-size: 85%;

Changes to box/constbox/base.mustache.

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







-







<!DOCTYPE html>
<html{{#Lang}} lang="{{Lang}}"{{/Lang}}>
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<meta name="generator" content="Zettelstore">
<meta name="format-detection" content="telephone=no">
{{{MetaHeader}}}
<link rel="stylesheet" href="{{{CSSBaseURL}}}">
<link rel="stylesheet" href="{{{CSSUserURL}}}">
{{#CSSRoleURL}}<link rel="stylesheet" href="{{{CSSRoleURL}}}">{{/CSSRoleURL}}
<title>{{Title}}</title>
</head>
<body>
<nav class="zs-menu">
<a href="{{{HomeURL}}}">Home</a>
{{#WithUser}}
<div class="zs-dropdown">
56
57
58
59
60
61
62
63

64
65
66




52
53
54
55
56
57
58

59



60
61
62
63







-
+
-
-
-
+
+
+
+
<form action="{{{SearchURL}}}">
<input type="text" placeholder="Search.." name="{{QueryKeySearch}}">
</form>
</nav>
<main class="content">
{{{Content}}}
</main>
{{#FooterHTML}}<footer>{{{FooterHTML}}}</footer>{{/FooterHTML}}
{{#FooterHTML}}
{{#DebugMode}}<div><b>WARNING: Debug mode is enabled. DO NOT USE IN PRODUCTION!</b></div>{{/DebugMode}}
</body>
</html>
<footer>
{{{FooterHTML}}}
</footer>
{{/FooterHTML}}

Changes to box/constbox/constbox.go.

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
291
292
293
294
295
296
297








298
299

300
301
302
303
304
305
306
307







-
-
-
-
-
-
-
-


-
+







		constHeader{
			api.KeyTitle:      "Zettelstore User CSS",
			api.KeyRole:       api.ValueRoleConfiguration,
			api.KeySyntax:     "css",
			api.KeyVisibility: api.ValueVisibilityPublic,
		},
		domain.NewContent([]byte("/* User-defined CSS */"))},
	id.RoleCSSMapZid: {
		constHeader{
			api.KeyTitle:      "Zettelstore Role to CSS Map",
			api.KeyRole:       api.ValueRoleConfiguration,
			api.KeySyntax:     api.ValueSyntaxNone,
			api.KeyVisibility: api.ValueVisibilityExpert,
		},
		domain.NewContent(nil)},
	id.EmojiZid: {
		constHeader{
			api.KeyTitle:      "Zettelstore Generic Emoji",
			api.KeyTitle:      "Generic Emoji",
			api.KeyRole:       api.ValueRoleConfiguration,
			api.KeySyntax:     api.ValueSyntaxGif,
			api.KeyReadOnly:   api.ValueTrue,
			api.KeyVisibility: api.ValueVisibilityPublic,
		},
		domain.NewContent(contentEmoji)},
	id.TOCNewTemplateZid: {

Changes to box/constbox/delete.mustache.

33
34
35
36
37
38
39
40

41
42
43
33
34
35
36
37
38
39

40
41
42
43







-
+



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

Changes to box/constbox/form.mustache.

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






-
-
+
+



-
-
+
+
-
-
-
-
-
-
-

-
-
+
+


-
-
+
+






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


-
-
+
+


-
-
+
-
-


<article>
<header>
<h1>{{Heading}}</h1>
</header>
<form method="POST">
<div>
<label for="zs-title">Title <a title="Main heading of this zettel. You can use inline zettelmarkup.">&#9432;</a></label>
<input class="zs-input" type="text" id="zs-title" name="title" placeholder="Title.." value="{{MetaTitle}}" autofocus>
<label for="title">Title</label>
<input class="zs-input" type="text" id="title" name="title" placeholder="Title.." value="{{MetaTitle}}" autofocus>
</div>
<div>
<div>
<label for="zs-role">Role <a title="One word, without spaces, to set the main role of this zettel.">&#9432;</a></label>
<input class="zs-input" type="text" id="zs-role" {{#HasRoleData}}list="zs-role-data"{{/HasRoleData}} name="role" placeholder="role.." value="{{MetaRole}}">
<label for="role">Role</label>
<input class="zs-input" type="text" id="role" name="role" placeholder="role.." value="{{MetaRole}}">
{{#HasRoleData}}
<datalist id="zs-role-data">
{{#RoleData}}
<option value="{{.}}">
{{/RoleData}}
</datalist>
{{/HasRoleData}}
</div>
<label for="zs-tags">Tags <a title="Tags must begin with an '#' sign. They are separated by spaces.">&#9432;</a></label>
<input class="zs-input" type="text" id="zs-tags" name="tags" placeholder="#tag" value="{{MetaTags}}">
<label for="tags">Tags</label>
<input class="zs-input" type="text" id="tags" name="tags" placeholder="#tag" value="{{MetaTags}}">
</div>
<div>
<label for="zs-meta">Metadata <a title="Other metadata for this zettel. Each line contains a key/value pair, separated by a colon ':'.">&#9432;</a></label>
<textarea class="zs-input" id="zs-meta" name="meta" rows="4" placeholder="metakey: metavalue">
<label for="meta">Metadata</label>
<textarea class="zs-input" id="meta" name="meta" rows="4" placeholder="metakey: metavalue">
{{#MetaPairsRest}}
{{Key}}: {{Value}}
{{/MetaPairsRest}}
</textarea>
</div>
<div>
<label for="zs-syntax">Syntax <a title="Syntax of zettel content below, one word. Typically 'zmk' (for zettelmarkup).">&#9432;</a></label>
<input class="zs-input" type="text" id="zs-syntax" {{#HasSyntaxData}}list="zs-syntax-data"{{/HasSyntaxData}} name="syntax" placeholder="syntax.." value="{{MetaSyntax}}">
<label for="syntax">Syntax</label>
<input class="zs-input" type="text" id="syntax" name="syntax" placeholder="syntax.." value="{{MetaSyntax}}">
{{#HasSyntaxData}}
<datalist id="zs-syntax-data">
{{#SyntaxData}}
<option value="{{.}}">
{{/SyntaxData}}
</datalist>
{{/HasSyntaxData}}</div>
</div>
<div>
{{#IsTextContent}}
<label for="zs-content">Content <a title="Content for this zettel, according to above syntax.">&#9432;</a></label>
<textarea class="zs-input zs-content" id="zs-content" name="content" rows="20" placeholder="Your content..">{{Content}}</textarea>
<label for="content">Content</label>
<textarea class="zs-input zs-content" id="meta" name="content" rows="20" placeholder="Your content..">{{Content}}</textarea>
{{/IsTextContent}}
</div>
<div>
<input class="zs-primary" type="submit" value="Submit">
<input class="zs-button" type="submit" value="Submit">
<input class="zs-secondary" type="submit" value="Save" formaction="?save">
</div>
</form>
</article>

Changes to box/constbox/login.mustache.

1
2
3
4
5
6
7
8
9
10

11
12
13
14

15
16
17

18
19
1
2
3
4
5
6
7
8
9

10
11
12
13

14
15
16

17
18
19









-
+



-
+


-
+


<article>
<header>
<h1>{{Title}}</h1>
</header>
{{#Retry}}
<div class="zs-indication zs-error">Wrong user name / password. Try again.</div>
{{/Retry}}
<form method="POST" action="">
<div>
<label for="username">User name:</label>
<label for="username">User name</label>
<input class="zs-input" type="text" id="username" name="username" placeholder="Your user name.." autofocus>
</div>
<div>
<label for="password">Password:</label>
<label for="password">Password</label>
<input class="zs-input" type="password" id="password" name="password" placeholder="Your password..">
</div>
<div><input class="zs-primary" type="submit" value="Login"></div>
<input class="zs-button" type="submit" value="Login">
</form>
</article>

Changes to box/constbox/rename.mustache.

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







-
+







{{/HasUselessFiles}}
<form method="POST">
<div>
<label for="newid">New zettel id</label>
<input class="zs-input" type="text" id="newzid" name="newzid" placeholder="ZID.." value="{{Zid}}" autofocus>
</div>
<input type="hidden" id="curzid" name="curzid" value="{{Zid}}">
<div><input class="zs-primary" type="submit" value="Rename"></div>
<input class="zs-button" type="submit" value="Rename">
</form>
<dl>
{{#MetaPairs}}
<dt>{{Key}}:</dt><dd>{{Value}}</dd>
{{/MetaPairs}}
</dl>
</article>

Changes to box/dirbox/dirbox.go.

15
16
17
18
19
20
21

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







+







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

	"zettelstore.de/c/api"
	"zettelstore.de/z/box"
	"zettelstore.de/z/box/manager"
	"zettelstore.de/z/box/notify"
	"zettelstore.de/z/domain"
	"zettelstore.de/z/domain/id"
	"zettelstore.de/z/domain/meta"
	"zettelstore.de/z/kernel"
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
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







+



















+







	if !entry.IsValid() {
		return domain.Zettel{}, box.ErrNotFound
	}
	m, c, err := dp.srvGetMetaContent(ctx, entry, zid)
	if err != nil {
		return domain.Zettel{}, err
	}
	dp.cleanupMeta(m)
	zettel := domain.Zettel{Meta: m, Content: domain.NewContent(c)}
	dp.log.Trace().Zid(zid).Msg("GetZettel")
	return zettel, nil
}

func (dp *dirBox) GetMeta(ctx context.Context, zid id.Zid) (*meta.Meta, error) {
	m, err := dp.doGetMeta(ctx, zid)
	dp.log.Trace().Zid(zid).Err(err).Msg("GetMeta")
	return m, err
}
func (dp *dirBox) doGetMeta(ctx context.Context, zid id.Zid) (*meta.Meta, error) {
	entry := dp.dirSrv.GetDirEntry(zid)
	if !entry.IsValid() {
		return nil, box.ErrNotFound
	}
	m, err := dp.srvGetMeta(ctx, entry, zid)
	if err != nil {
		return nil, err
	}
	dp.cleanupMeta(m)
	return m, nil
}

func (dp *dirBox) ApplyZid(_ context.Context, handle box.ZidFunc, constraint search.RetrievePredicate) error {
	entries := dp.dirSrv.GetDirEntries(constraint)
	dp.log.Trace().Int("entries", int64(len(entries))).Msg("ApplyZid")
	for _, entry := range entries {
274
275
276
277
278
279
280

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







+







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

func (dp *dirBox) CanUpdateZettel(context.Context, domain.Zettel) bool {
393
394
395
396
397
398
399









397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412







+
+
+
+
+
+
+
+
+
}

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

func (dp *dirBox) cleanupMeta(m *meta.Meta) {
	if role, ok := m.Get(api.KeyRole); !ok || role == "" {
		m.Set(api.KeyRole, dp.cdata.Config.GetDefaultRole())
	}
	if syntax, ok := m.Get(api.KeySyntax); !ok || syntax == "" {
		m.Set(api.KeySyntax, dp.cdata.Config.GetDefaultSyntax())
	}
}

Changes to box/dirbox/service.go.

1
2
3
4

5
6
7
8
9
10
11
1
2
3

4
5
6
7
8
9
10
11



-
+







//-----------------------------------------------------------------------------
// Copyright (c) 2020-2022 Detlef Stern
//
// This file is part of Zettelstore.
// This file is part of zettelstore.
//
// Zettelstore is licensed 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 dirbox

Changes to box/filebox/filebox.go.

1
2
3
4

5
6
7
8
9
10
11
1
2
3

4
5
6
7
8
9
10
11



-
+







//-----------------------------------------------------------------------------
// Copyright (c) 2021-2022 Detlef Stern
//
// This file is part of Zettelstore.
// This file is part of zettelstore.
//
// Zettelstore is licensed 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 filebox provides boxes that are stored in a file.
67
68
69
70
71
72
73

74
75
76
77
78
79




80
81
82
83
84
85
86
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







+






+
+
+
+







	}
	return ext
}

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

// CleanupMeta enhances the given metadata.
func CleanupMeta(m *meta.Meta, zid id.Zid, ext string, inMeta bool, uselessFiles []string) {
	if title, ok := m.Get(api.KeyTitle); !ok || title == "" {
		m.Set(api.KeyTitle, zid.String())
	}

	if inMeta {
		if syntax, ok := m.Get(api.KeySyntax); !ok || syntax == "" {
			dm := CalcDefaultMeta(zid, ext)
			syntax, ok = dm.Get(api.KeySyntax)
			if !ok {
				panic("Default meta must contain syntax")
			}

Changes to box/filebox/zipbox.go.

1
2
3
4

5
6
7
8
9
10
11
1
2
3

4
5
6
7
8
9
10
11



-
+







//-----------------------------------------------------------------------------
// Copyright (c) 2021-2022 Detlef Stern
//
// This file is part of Zettelstore.
// This file is part of zettelstore.
//
// Zettelstore is licensed 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 filebox

Changes to box/manager/manager.go.

11
12
13
14
15
16
17

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

22
23
24
25
26
27
28







+



-







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

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

	"zettelstore.de/c/maps"
	"zettelstore.de/z/auth"
	"zettelstore.de/z/box"
	"zettelstore.de/z/box/manager/memstore"
	"zettelstore.de/z/box/manager/store"
	"zettelstore.de/z/config"
	"zettelstore.de/z/domain/id"
	"zettelstore.de/z/domain/meta"
76
77
78
79
80
81
82
83








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

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







-
+
+
+
+
+
+
+
+







	if _, ok := registry[scheme]; ok {
		panic(scheme)
	}
	registry[scheme] = create
}

// GetSchemes returns all registered scheme, ordered by scheme string.
func GetSchemes() []string { return maps.Keys(registry) }
func GetSchemes() []string {
	result := make([]string, 0, len(registry))
	for scheme := range registry {
		result = append(result, scheme)
	}
	sort.Strings(result)
	return result
}

// Manager is a coordinating box.
type Manager struct {
	mgrLog       *logger.Logger
	mgrMx        sync.RWMutex
	started      bool
	rtConfig     config.Config

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

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

23
24
25
26
27
28
29







-







	"fmt"
	"io"
	"sort"
	"strings"
	"sync"

	"zettelstore.de/c/api"
	"zettelstore.de/c/maps"
	"zettelstore.de/z/box/manager/store"
	"zettelstore.de/z/domain/id"
	"zettelstore.de/z/domain/meta"
)

type metaRefs struct {
	forward  id.Slice
587
588
589
590
591
592
593





594

595
596
597
598
586
587
588
589
590
591
592
593
594
595
596
597

598
599
600
601
602







+
+
+
+
+
-
+




}

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

Changes to box/notify/directory_test.go.

12
13
14
15
16
17
18

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







+








import (
	"testing"

	"zettelstore.de/c/api"
	"zettelstore.de/z/domain/id"
	_ "zettelstore.de/z/parser/blob"       // Allow to use BLOB parser.
	_ "zettelstore.de/z/parser/draw"       // Allow to use draw parser.
	_ "zettelstore.de/z/parser/markdown"   // Allow to use markdown parser.
	_ "zettelstore.de/z/parser/none"       // Allow to use none parser.
	_ "zettelstore.de/z/parser/plain"      // Allow to use plain parser.
	_ "zettelstore.de/z/parser/zettelmark" // Allow to use zettelmark parser.
)

func TestSeekZid(t *testing.T) {
44
45
46
47
48
49
50
51

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

52
53
54
55
56
57
58
59







-
+







		}
	}
}

func TestNewExtIsBetter(t *testing.T) {
	extVals := []string{
		// Main Formats
		api.ValueSyntaxZmk, "markdown", "md",
		api.ValueSyntaxZmk, api.ValueSyntaxDraw, "markdown", "md",
		// Other supported text formats
		"css", "txt", api.ValueSyntaxHTML, api.ValueSyntaxNone, "mustache", api.ValueSyntaxText, "plain",
		// Supported graphics formats
		api.ValueSyntaxGif, "png", api.ValueSyntaxSVG, "jpeg", "jpg",
		// Unsupported syntax values
		"gz", "cpp", "tar", "cppc",
	}

Changes to box/notify/entry.go.

1
2
3
4

5
6
7
8
9
10
11
1
2
3

4
5
6
7
8
9
10
11



-
+







//-----------------------------------------------------------------------------
// Copyright (c) 2020-2022 Detlef Stern
//
// This file is part of Zettelstore.
// This file is part of zettelstore.
//
// Zettelstore is licensed 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 notify
95
96
97
98
99
100
101
102

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

102
103
104
105
106
107
108
109







-
+







}

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

Changes to cmd/cmd_file.go.

37
38
39
40
41
42
43
44



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

44
45
46
47
48
49
50
51
52
53







-
+
+
+







		domain.Zettel{
			Meta:    m,
			Content: domain.NewContent(inp.Src[inp.Pos:]),
		},
		m.GetDefault(api.KeySyntax, api.ValueSyntaxZmk),
		nil,
	)
	encdr := encoder.Create(api.Encoder(enc))
	encdr := encoder.Create(api.Encoder(enc), &encoder.Environment{
		Lang: m.GetDefault(api.KeyLang, api.ValueLangEN),
	})
	if encdr == nil {
		fmt.Fprintf(os.Stderr, "Unknown format %q\n", enc)
		return 2, nil
	}
	_, err = encdr.WriteZettel(os.Stdout, z, parser.ParseMetadata)
	if err != nil {
		return 2, err

Changes to cmd/cmd_run.go.

22
23
24
25
26
27
28
29

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

29
30
31
32
33
34
35
36







-
+







	"zettelstore.de/z/web/adapter/webui"
	"zettelstore.de/z/web/server"
)

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

func flgRun(fs *flag.FlagSet) {
	fs.String("c", "", "configuration file")
	fs.String("c", defConfigfile, "configuration file")
	fs.Uint("a", 0, "port number kernel service (0=disable)")
	fs.Uint("p", 23123, "port number web service")
	fs.String("d", "", "zettel directory")
	fs.Bool("r", false, "system-wide read-only mode")
	fs.Bool("v", false, "verbose mode")
	fs.Bool("debug", false, "debug mode")
}
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
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







-
-
+

















-
+




-
+



















-




+
+







	ucCreateZettel := usecase.NewCreateZettel(ucLog, rtConfig, protectedBoxManager)
	ucGetMeta := usecase.NewGetMeta(protectedBoxManager)
	ucGetAllMeta := usecase.NewGetAllMeta(protectedBoxManager)
	ucGetZettel := usecase.NewGetZettel(protectedBoxManager)
	ucParseZettel := usecase.NewParseZettel(rtConfig, ucGetZettel)
	ucEvaluate := usecase.NewEvaluate(rtConfig, ucGetZettel, ucGetMeta)
	ucListMeta := usecase.NewListMeta(protectedBoxManager)
	ucListSyntax := usecase.NewListSyntax(protectedBoxManager)
	ucListRoles := usecase.NewListRoles(protectedBoxManager)
	ucListRoles := usecase.NewListRole(protectedBoxManager)
	ucListTags := usecase.NewListTags(protectedBoxManager)
	ucZettelContext := usecase.NewZettelContext(protectedBoxManager, rtConfig)
	ucDelete := usecase.NewDeleteZettel(ucLog, protectedBoxManager)
	ucUpdate := usecase.NewUpdateZettel(ucLog, protectedBoxManager)
	ucRename := usecase.NewRenameZettel(ucLog, protectedBoxManager)
	ucUnlinkedRefs := usecase.NewUnlinkedReferences(protectedBoxManager, rtConfig)
	ucRefresh := usecase.NewRefresh(ucLog, protectedBoxManager)
	ucVersion := usecase.NewVersion(kernel.Main.GetConfig(kernel.CoreService, kernel.CoreVersion).(string))

	webSrv.Handle("/", wui.MakeGetRootHandler(protectedBoxManager))

	// Web user interface
	if !authManager.IsReadonly() {
		webSrv.AddZettelRoute('b', server.MethodGet, wui.MakeGetRenameZettelHandler(
			ucGetMeta, &ucEvaluate))
		webSrv.AddZettelRoute('b', server.MethodPost, wui.MakePostRenameZettelHandler(&ucRename))
		webSrv.AddZettelRoute('c', server.MethodGet, wui.MakeGetCreateZettelHandler(
			ucGetZettel, &ucCreateZettel, ucListRoles, ucListSyntax))
			ucGetZettel, &ucCreateZettel))
		webSrv.AddZettelRoute('c', server.MethodPost, wui.MakePostCreateZettelHandler(&ucCreateZettel))
		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.MethodGet, wui.MakeEditGetZettelHandler(ucGetZettel))
		webSrv.AddZettelRoute('e', server.MethodPost, wui.MakeEditSetZettelHandler(&ucUpdate))
	}
	webSrv.AddListRoute('g', server.MethodGet, wui.MakeGetGoActionHandler(&ucRefresh))
	webSrv.AddListRoute('h', server.MethodGet, wui.MakeListHTMLMetaHandler(
		ucListMeta, ucListRoles, ucListTags, &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.AddListRoute('j', server.MethodGet, a.MakeListMetaHandler(ucListMeta))
	webSrv.AddZettelRoute('j', server.MethodGet, a.MakeGetZettelHandler(ucGetZettel))
	webSrv.AddListRoute('m', server.MethodGet, a.MakeListMapMetaHandler(ucListRoles, ucListTags))
	webSrv.AddZettelRoute('m', server.MethodGet, a.MakeGetMetaHandler(ucGetMeta))
	webSrv.AddZettelRoute('o', server.MethodGet, a.MakeGetOrderHandler(
		usecase.NewZettelOrder(protectedBoxManager, ucEvaluate)))
	webSrv.AddZettelRoute('p', server.MethodGet, a.MakeGetParsedZettelHandler(ucParseZettel))
	webSrv.AddListRoute('r', server.MethodGet, a.MakeListRoleHandler(ucListRoles))
	webSrv.AddListRoute('t', server.MethodGet, a.MakeListTagsHandler(ucListTags))
	webSrv.AddZettelRoute('u', server.MethodGet, a.MakeListUnlinkedMetaHandler(
		ucGetMeta, ucUnlinkedRefs, &ucEvaluate))
	webSrv.AddZettelRoute('v', server.MethodGet, a.MakeGetEvalZettelHandler(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.MakeListPlainHandler(ucListMeta))

Changes to cmd/command.go.

1
2

3
4

5
6
7
8
9
10
11
12
13
14

15
16
17
18
19
20
21
22
23
1

2
3

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

17
18
19
20
21
22
23

-
+

-
+










+

-







//-----------------------------------------------------------------------------
// Copyright (c) 2020-2022 Detlef Stern
// Copyright (c) 2020-2021 Detlef Stern
//
// This file is part of Zettelstore.
// This file is part of zettelstore.
//
// Zettelstore is licensed 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 cmd

import (
	"flag"
	"sort"

	"zettelstore.de/c/maps"
	"zettelstore.de/z/logger"
)

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








59
60
61
62
63
64
65

66
67
68
69
70
71
72
73







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

// List returns a sorted list of all registered command names.
func List() []string { return maps.Keys(commands) }
func List() []string {
	result := make([]string, 0, len(commands))
	for name := range commands {
		result = append(result, name)
	}
	sort.Strings(result)
	return result
}

Changes to cmd/main.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
1
2
3

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

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

38
39
40
41
42
43
44
45
46
47



-
+















-


















-
+
+
+







//-----------------------------------------------------------------------------
// Copyright (c) 2020-2022 Detlef Stern
//
// This file is part of Zettelstore.
// This file is part of zettelstore.
//
// Zettelstore is licensed 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 cmd

import (
	"errors"
	"flag"
	"fmt"
	"net"
	"net/url"
	"os"
	"runtime/debug"
	"strconv"
	"strings"

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

const strRunSimple = "run-simple"
const (
	defConfigfile = ".zscfg"
)

func init() {
	RegisterCommand(Command{
		Name: "help",
		Func: func(*flag.FlagSet) (int, error) {
			fmt.Println("Available commands:")
			for _, name := range List() {
59
60
61
62
63
64
65
66

67
68
69
70
71
72
73
60
61
62
63
64
65
66

67
68
69
70
71
72
73
74







-
+







		Func:       runFunc,
		Boxes:      true,
		Header:     true,
		LineServer: true,
		SetFlags:   flgRun,
	})
	RegisterCommand(Command{
		Name:   strRunSimple,
		Name:   "run-simple",
		Func:   runFunc,
		Simple: true,
		Boxes:  true,
		Header: true,
		// LineServer: true,
		SetFlags: func(fs *flag.FlagSet) {
			// fs.Uint("a", 0, "port number kernel service (0=disable)")
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
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







-
+
+

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






-
-
-
-
-
-
-
-
-
-
-

-
+







	})
	RegisterCommand(Command{
		Name: "password",
		Func: cmdPassword,
	})
}

func fetchStartupConfiguration(fs *flag.FlagSet) (cfg *meta.Meta) {
func readConfig(fs *flag.FlagSet) (cfg *meta.Meta) {
	var configFile string
	if configFlag := fs.Lookup("c"); configFlag != nil {
		if filename := configFlag.Value.String(); filename != "" {
		configFile = configFlag.Value.String()
			content, err := readConfiguration(filename)
			return createConfiguration(content, err)
		}
	} else {
	}
	content, err := searchAndReadConfiguration()
	return createConfiguration(content, err)
}

		configFile = defConfigfile
	}
	content, err := os.ReadFile(configFile)
func createConfiguration(content []byte, err error) *meta.Meta {
	if err != nil {
		return meta.New(id.Invalid)
	}
	return meta.NewFromInput(id.Invalid, input.NewInput(content))
}

func readConfiguration(filename string) ([]byte, error) { return os.ReadFile(filename) }

func searchAndReadConfiguration() ([]byte, error) {
	for _, filename := range []string{"zettelstore.cfg", "zsconfig.txt", "zscfg.txt", "_zscfg"} {
		if content, err := readConfiguration(filename); err == nil {
			return content, nil
		}
	}
	return readConfiguration(".zscfg")
}

func getConfig(fs *flag.FlagSet) *meta.Meta {
	cfg := fetchStartupConfiguration(fs)
	cfg := readConfig(fs)
	fs.Visit(func(flg *flag.Flag) {
		switch flg.Name {
		case "p":
			if portStr, err := parsePort(flg.Value.String()); err == nil {
				cfg.Set(keyListenAddr, net.JoinHostPort("127.0.0.1", portStr))
			}
		case "a":
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
156
157
158
159
160
161
162

163
164
165
166
167
168
169







-







const (
	keyAdminPort         = "admin-port"
	keyDebug             = "debug-mode"
	keyDefaultDirBoxType = "default-dir-box-type"
	keyInsecureCookie    = "insecure-cookie"
	keyListenAddr        = "listen-addr"
	keyLogLevel          = "log-level"
	keyMaxRequestSize    = "max-request-size"
	keyOwner             = "owner"
	keyPersistentCookie  = "persistent-cookie"
	keyBoxOneURI         = kernel.BoxURIs + "1"
	keyReadOnly          = "read-only-mode"
	keyTokenLifetimeHTML = "token-lifetime-html"
	keyTokenLifetimeAPI  = "token-lifetime-api"
	keyURLPrefix         = "url-prefix"
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
205
206
207
208
209
210
211



212
213
214
215
216
217
218







-
-
-








	ok = setConfigValue(
		ok, kernel.WebService, kernel.WebListenAddress,
		cfg.GetDefault(keyListenAddr, "127.0.0.1:23123"))
	ok = setConfigValue(ok, kernel.WebService, kernel.WebURLPrefix, cfg.GetDefault(keyURLPrefix, "/"))
	ok = setConfigValue(ok, kernel.WebService, kernel.WebSecureCookie, !cfg.GetBool(keyInsecureCookie))
	ok = setConfigValue(ok, kernel.WebService, kernel.WebPersistentCookie, cfg.GetBool(keyPersistentCookie))
	if val, found := cfg.Get(keyMaxRequestSize); found {
		ok = setConfigValue(ok, kernel.WebService, kernel.WebMaxRequestSize, val)
	}
	ok = setConfigValue(
		ok, kernel.WebService, kernel.WebTokenLifetimeAPI, cfg.GetDefault(keyTokenLifetimeAPI, ""))
	ok = setConfigValue(
		ok, kernel.WebService, kernel.WebTokenLifetimeHTML, cfg.GetDefault(keyTokenLifetimeHTML, ""))

	if !ok {
		return errors.New("unable to set configuration")
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292

293
294
295
296
297
298
299
259
260
261
262
263
264
265






266
267

268
269
270
271
272
273
274
275







-
-
-
-
-
-


-
+







			compbox.Setup(cfg)
			return manager.New(boxURIs, authManager, rtConfig)
		}
	} else {
		createManager = func([]*url.URL, auth.Manager, config.Config) (box.Manager, error) { return nil, nil }
	}

	secret := cfg.GetDefault("secret", "")
	if len(secret) < 16 && cfg.GetDefault(keyOwner, "") != "" {
		fmt.Fprintf(os.Stderr, "secret must have at least length 16 when authentication is enabled, but is %q\n", secret)
		return 2
	}

	kern.SetCreators(
		func(readonly bool, owner id.Zid) (auth.Manager, error) {
			return impl.New(readonly, owner, secret), nil
			return impl.New(readonly, owner, cfg.GetDefault("secret", "")), nil
		},
		createManager,
		func(srv server.Server, plMgr box.Manager, authMgr auth.Manager, rtConfig config.Config) error {
			setupRouting(srv, plMgr, authMgr, rtConfig)
			return nil
		},
	)
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
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





























-
-
-





-
+







-

-
+















-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
	kern.Shutdown(true)
	return exitCode
}

// runSimple is called, when the user just starts the software via a double click
// or via a simple call ``./zettelstore`` on the command line.
func runSimple() int {
	if _, err := searchAndReadConfiguration(); err == nil {
		return executeCommand(strRunSimple)
	}
	dir := "./zettel"
	if err := os.MkdirAll(dir, 0750); err != nil {
		fmt.Fprintf(os.Stderr, "Unable to create zettel directory %q (%s)\n", dir, err)
		return 1
	}
	return executeCommand(strRunSimple, "-d", dir)
	return executeCommand("run-simple", "-d", dir)
}

var cpuprofile = flag.String("cpuprofile", "", "write cpu profile to `file`")
var memprofile = flag.String("memprofile", "", "write memory profile to `file`")

// Main is the real entrypoint of the zettelstore.
func Main(progName, buildVersion string) int {
	fullVersion := retrieveFullVersion(buildVersion)
	kernel.Main.SetConfig(kernel.CoreService, kernel.CoreProgname, progName)
	kernel.Main.SetConfig(kernel.CoreService, kernel.CoreVersion, fullVersion)
	kernel.Main.SetConfig(kernel.CoreService, kernel.CoreVersion, buildVersion)
	flag.Parse()
	if *cpuprofile != "" || *memprofile != "" {
		if *cpuprofile != "" {
			kernel.Main.StartProfiling(kernel.ProfileCPU, *cpuprofile)
		} else {
			kernel.Main.StartProfiling(kernel.ProfileHead, *memprofile)
		}
		defer kernel.Main.StopProfiling()
	}
	args := flag.Args()
	if len(args) == 0 {
		return runSimple()
	}
	return executeCommand(args[0], args[1:]...)
}

func retrieveFullVersion(version string) string {
	info, ok := debug.ReadBuildInfo()
	if !ok {
		return version
	}
	var revision, dirty string
	for _, kv := range info.Settings {
		switch kv.Key {
		case "vcs.revision":
			revision = "+" + kv.Value
			if len(revision) > 11 {
				revision = revision[:11]
			}
		case "vcs.modified":
			if kv.Value == "true" {
				dirty = "-dirty"
			}
		}
	}
	return version + revision + dirty
}

Changes to cmd/register.go.

15
16
17
18
19
20
21
22

23
24
25
26
27

28
29
30
31
32
15
16
17
18
19
20
21

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







-
+





+





import (
	_ "zettelstore.de/z/box/compbox"       // Allow to use computed box.
	_ "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/sexprenc"  // Allow to use sexpr encoder.
	_ "zettelstore.de/z/encoder/nativeenc" // Allow to use native 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 config/config.go.

1
2

3
4

5
6
7
8
9
10
11
1

2
3

4
5
6
7
8
9
10
11

-
+

-
+







//-----------------------------------------------------------------------------
// Copyright (c) 2020-2022 Detlef Stern
// Copyright (c) 2020-2021 Detlef Stern
//
// This file is part of Zettelstore.
// This file is part of zettelstore.
//
// Zettelstore is licensed 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 config provides functions to retrieve runtime configuration data.
19
20
21
22
23
24
25









26
27
28
29
30
31
32
33
34



35
36
37
38
39
40
41
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







+
+
+
+
+
+
+
+
+









+
+
+








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

	// AddDefaultValues enriches the given meta data with its default values.
	AddDefaultValues(m *meta.Meta) *meta.Meta

	// GetDefaultTitle returns the current value of the "default-title" key.
	GetDefaultTitle() string

	// GetDefaultRole returns the current value of the "default-role" key.
	GetDefaultRole() string

	// GetDefaultSyntax returns the current value of the "default-syntax" key.
	GetDefaultSyntax() string

	// GetDefaultLang returns the current value of the "default-lang" key.
	GetDefaultLang() string

	// GetSiteName returns the current value of the "site-name" key.
	GetSiteName() string

	// GetHomeZettel returns the value of the "home-zettel" key.
	GetHomeZettel() id.Zid

	// GetDefaultVisibility returns the default value for zettel visibility.
	GetDefaultVisibility() meta.Visibility

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

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

57
58
59
60
61
62
63






























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







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










	// GetExpertMode returns the current value of the "expert-mode" key.
	GetExpertMode() bool

	// GetVisibility returns the visibility value of the metadata.
	GetVisibility(m *meta.Meta) meta.Visibility
}

// GetTitle returns the value of the "title" key of the given meta. If there
// is no such value, GetDefaultTitle is returned.
func GetTitle(m *meta.Meta, cfg Config) string {
	if val, ok := m.Get(api.KeyTitle); ok {
		return val
	}
	if cfg != nil {
		return cfg.GetDefaultTitle()
	}
	return "Untitled"
}

// GetRole returns the value of the "role" key of the given meta. If there
// is no such value, GetDefaultRole is returned.
func GetRole(m *meta.Meta, cfg Config) string {
	if val, ok := m.Get(api.KeyRole); ok {
		return val
	}
	return cfg.GetDefaultRole()
}

// GetSyntax returns the value of the "syntax" key of the given meta. If there
// is no such value, GetDefaultSyntax is returned.
func GetSyntax(m *meta.Meta, cfg Config) string {
	if val, ok := m.Get(api.KeySyntax); ok {
		return val
	}
	return cfg.GetDefaultSyntax()
}

// GetLang returns the value of the "lang" key of the given meta. If there is
// no such value, GetDefaultLang is returned.
func GetLang(m *meta.Meta, cfg Config) string {
	if val, ok := m.Get(api.KeyLang); ok {
		return val
	}
	return cfg.GetDefaultLang()
}

Changes to docs/manual/00001003305000.zettel.

38
39
40
41
42
43
44
45

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

45
46
47
48
49
50
51
52







-
+







All you have to do is to open your web browser, enter the appropriate URL, and there you go.

On the negative side, you will not be notified when you enter the wrong data in the Task scheduler and Zettelstore fails to start.
This can be mitigated by first using the command line prompt to start Zettelstore with the appropriate options.
Once everything works, you can register Zettelstore to be automatically started by the task scheduler.
There you should make sure that you have followed the first steps as described on the [[parent page|00001003300000]].

To start the Task scheduler management console, press the Windows logo key and the key ''R'', type ''taskschd.msc''.
To sart the Task scheduler management console, press the Windows logo key and the key ''R'', type ''taskschd.msc''.
Select the OK button.

{{00001003305102}}

This will start the ""Task Scheduler"".

Now, create a new task with ""Create Task ...""

Changes to docs/manual/00001003310000.zettel.

38
39
40
41
42
43
44
45

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

45
46
47
48
49
50
51
52







-
+








=== Launch Agent

If you want to execute Zettelstore automatically and less visible, and if you know a little bit about working in the terminal application, then you could try to run Zettelstore under the control of the [[Launchd system|https://developer.apple.com/library/archive/documentation/MacOSX/Conceptual/BPSystemStartup/Chapters/Introduction.html]].

First, you have to create a description for ""Launchd"".
This is a text file named ''zettelstore.plist'' with the following content.
It assumes that you have copied the Zettelstore executable in a local folder called ''~/bin'' and have created a file for [[startup configuration|00001004010000]] called ''zettelstore.cfg'', which is placed in the same folder[^If you are not using a configuration file, just remove the lines ``<string>-c</string>`` and ``<string>/Users/USERNAME/bin/zettelstore.cfg</string>``.]:
It assumes that you have copied the Zettelstore executable in a local folder called ''~/bin'' and have created a file for [[startup configuration|00001004010000]] called ''zettelstore.cfg'', which is placed in the same folder[^If you are not using a confguration file, just remove the lines ``<string>-c</string>`` and ``<string>/Users/USERNAME/bin/zettelstore.cfg</string>``.]:

```
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
	<dict>
		<key>Label</key>

Changes to docs/manual/00001004010000.zettel.

1
2
3
4
5
6

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

6
7
8
9
10
11
12
13





-
+







id: 00001004010000
title: Zettelstore startup configuration
role: manual
tags: #configuration #manual #zettelstore
syntax: zmk
modified: 20220419193611
modified: 20220304115353

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

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







-
-
-
-
-




-
-
-















-
-
-
-
-







; [!log-level|''log-level'']
: Specify the global [[logging level|00001004059700]] for the whole application, overwriting the level ""debug"" set by configuration [[''debug-mode''|#debug-mode]].
  Can be changed at runtime, even for specific internal services, with the ''log-level'' command of the [[administrator console|00001004101000#log-level]].
  
  Default: ""info"".

  When you are familiar to operate the Zettelstore, you might set the level to ""warn"" or ""error"" to receive less noisy messages from the Zettelstore.
; [!max-request-size|''max-request-size'']
: Limits the maximum byte size of a web request body to prevent clients from accidentally or maliciously sending a large request and wasting server resources.
  The minimum value is 1024.

  Default: 16777216 (16 MiB). 
; [!owner|''owner'']
: [[Identifier|00001006050000]] of a zettel that contains data about the owner of the Zettelstore.
  The owner has full authorization for the Zettelstore.
  Only if owner is set to some value, user [[authentication|00001010000000]] is enabled.

  Ensure that key [[''secret''|#secret]] is set to a value of at least 16 bytes.
  Otherwise the Zettelstore will not start for security reasons.
; [!persistent-cookie|''persistent-cookie'']
: A [[boolean value|00001006030500]] to make the access cookie persistent.
  This is helpful if you access the Zettelstore via a mobile device.
  On these devices, the operating system is free to stop the web browser and to remove temporary cookies.
  Therefore, an authenticated user will be logged off.

  If ""true"", a persistent cookie is used.
  Its lifetime exceeds the lifetime of the authentication token (see option ''token-lifetime-html'') by 30 seconds.

  Default: ""false""
; [!read-only-mode|''read-only-mode'']
: Puts the Zettelstore service into a read-only mode, if set to a [[true value|00001006030500]].
  No changes are possible.

  Default: ""false"".
; [!secret|''secret'']
: A string value to make the communication with external clients strong enough so that sessions of the [[web user interface|00001014000000]] or [[API access token|00001010040700]] cannot be modified by some external unfriendly party.
  The string must have a length of at least 16 bytes.

  It is only needed to set this value, if [[authentication is enabled|00001010040100]] by setting key [[''owner''|#owner]] to some user identification.
; [!token-lifetime-api|''token-lifetime-api''], [!token-lifetime-html|''token-lifetime-html'']
: Define lifetime of access tokens in minutes.
  Values are only valid if authentication is enabled, i.e. key ''owner'' is set.

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

Changes to docs/manual/00001004011400.zettel.

1
2
3
4
5
6

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

22
23
24
25
26
27
28
1
2
3
4
5

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

21
22
23
24
25
26
27
28





-
+














-
+







id: 00001004011400
title: Configure file directory boxes
role: manual
tags: #configuration #manual #zettelstore
syntax: zmk
modified: 20220724200512
modified: 20220307121244

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

The following parameters are supported:

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

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

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

; simple
: Is not able to detect external changes.

Changes to docs/manual/00001004020000.zettel.

1
2
3
4
5
6

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

6
7
8
9
10
11
12
13





-
+







id: 00001004020000
title: Configure the running Zettelstore
role: manual
tags: #configuration #manual #zettelstore
syntax: zmk
modified: 20220628110920
modified: 20220304114412

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:

; [!default-copyright|''default-copyright'']
: Copyright value to be used when rendering content.
21
22
23
24
25
26
27











28
29
30
31
32
33
34
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







+
+
+
+
+
+
+
+
+
+
+







  This value 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]].
; [!default-license|''default-license'']
: License value to be used when rendering content.
  Can be overwritten in a zettel with [[meta key|00001006020000]] ''license''.
  Default: (the empty string).
; [!default-role|''default-role'']
: Role to be used, if a zettel specifies no ''role'' [[meta key|00001006020000]].
  Default: ""zettel"".
; [!default-syntax|''default-syntax'']
: Syntax to be used, if a zettel specifies no ''syntax'' [[meta key|00001006020000]].
  Default: ""zmk"" (""[[Zettelmarkup|00001007000000]]"").
; [!default-title|''default-title'']
: Title to be used, if a zettel specifies no ''title'' [[meta key|00001006020000]].
  Default: ""Untitled"".

  You can use all [[inline-structured elements|00001007040000]] of Zettelmarkup.
; [!default-visibility|''default-visibility'']
: Visibility to be used, if zettel does not specify a value for the [[''visibility''|00001006020000#visibility]] metadata key.
  Default: ""login"".
; [!expert-mode|''expert-mode'']
: If set to a [[boolean true value|00001006030500]], all zettel with [[visibility ""expert""|00001010070200]] will be shown (to the owner, if [[authentication is enabled|00001010040100]]; to all, otherwise).
  This affects most computed zettel.
  Default: ""False"".

Changes to docs/manual/00001004051000.zettel.

1
2
3
4
5
6

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

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

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

22
23
24
25
26
27
28
29





-
+















-
+







id: 00001004051000
title: The ''run'' sub-command
role: manual
tags: #command #configuration #manual #zettelstore
syntax: zmk
modified: 20220724162050
modified: 20220214175947

=== ``zettelstore run``
This starts the web service.

```
zettelstore run [-a PORT] [-c CONFIGFILE] [-d DIR] [-debug] [-p PORT] [-r] [-v]
```

; [!a|''-a PORT'']
: Specifies the TCP port through which you can reach the [[administrator console|00001004100000]].
  See the explanation of [[''admin-port''|00001004010000#admin-port]] for more details.
; [!c|''-c CONFIGFILE'']
: Specifies ''CONFIGFILE'' as a file, where [[startup configuration data|00001004010000]] is read.
  It is ignored, when the given file is not available, nor readable.

  Default: tries to read the following files in the ""current directory"": ''zettelstore.cfg'', ''zsconfig.txt'', ''zscfg.txt'', ''_zscfg'', and ''.zscfg''. 
  Default: ''./.zscfg''. (''.\\.zscfg'' on Windows)), where ''.'' denotes the ""current directory"".
; [!d|''-d DIR'']
: Specifies ''DIR'' as the directory that contains all zettel.

  Default is ''./zettel'' (''.\\zettel'' on Windows), where ''.'' denotes the ""current directory"".
; [!debug|''-debug'']
: Allows better debugging of the internal web server by disabling any timeout values.
  You should specify this only as a developer.

Changes to docs/manual/00001004051100.zettel.

1
2
3
4
5
6

7
8
9
10
11
12
13
14
15
16

17
18
19
20
21
22
23
24
25
26
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





-
+





-
-
-
-
-
+












id: 00001004051100
title: The ''run-simple'' sub-command
role: manual
tags: #command #configuration #manual #zettelstore
syntax: zmk
modified: 20220724162843
modified: 20220214180253

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

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


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

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

; [!d|''-d DIR'']
: Specifies ''DIR'' as the directory that contains all zettel.

  Default is ''./zettel'' (''.\\zettel'' on Windows), where ''.'' denotes the ""current directory"".

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

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

18
19
20
21
22
23
24
25
26
27
28





-
+











-
+










id: 00001004051200
title: The ''file'' sub-command
role: manual
tags: #command #configuration #manual #zettelstore
syntax: zmk
modified: 20220423131738
modified: 20220209114650

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),
  [[''sexpr''|00001012920516]],
  [[''native''|00001012920513]],
  [[''text''|00001012920519]],
  [[''zjson''|00001012920503]],
  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/00001004100000.zettel.

14
15
16
17
18
19
20
21

22
23
24
25
14
15
16
17
18
19
20

21
22
23
24
25







-
+




In fact, the administrator console is __not__ a full telnet service.
It is merely a simple line-oriented service where each input line is interpreted separately.
Therefore, you can also use tools like [[netcat|https://nc110.sourceforge.io/]], [[socat|http://www.dest-unreach.org/socat/]], etc.

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

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

Changes to docs/manual/00001005000000.zettel.

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







-
+


















-
+







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

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

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

=== Predefined zettel

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

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

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

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

Therefore, the file store ""shadows"" the internal zettel store.

Changes to docs/manual/00001005090000.zettel.

1
2
3
4
5
6

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

28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
1
2
3
4
5

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

27
28
29
30
31
32
33
34

35
36
37
38
39
40
41
42
43





-
+




















-
+







-









id: 00001005090000
title: List of predefined zettel
role: manual
tags: #manual #reference #zettelstore
syntax: zmk
modified: 20220321192401
modified: 20211229000646

The following table lists all predefined zettel with their purpose.

|= Identifier :|= Title | Purpose
| [[00000000000001]] | Zettelstore Version | Contains the version string of the running Zettelstore
| [[00000000000002]] | Zettelstore Host | Contains the name of the computer running the Zettelstore
| [[00000000000003]] | Zettelstore Operating System | Contains the operating system and CPU architecture of the computer running the Zettelstore
| [[00000000000004]] | Zettelstore License | Lists the license of Zettelstore
| [[00000000000005]] | Zettelstore Contributors | Lists all contributors of Zettelstore
| [[00000000000006]] | Zettelstore Dependencies | Lists all licensed content
| [[00000000000007]] | Zettelstore Log | Lists the last 8192 log messages
| [[00000000000020]] | Zettelstore Box Manager | Contains some statistics about zettel boxes and the the index process
| [[00000000000090]] | Zettelstore Supported Metadata Keys | Contains all supported metadata keys, their [[types|00001006030000]], and more
| [[00000000000092]] | Zettelstore Supported Parser | Lists all supported values for metadata [[syntax|00001006020000#syntax]] that are recognized by Zettelstore
| [[00000000000096]] | Zettelstore Startup Configuration | Contains the effective values of the [[startup configuration|00001004010000]]
| [[00000000000100]] | Zettelstore Runtime Configuration | Allows to [[configure Zettelstore at runtime|00001004020000]]
| [[00000000010100]] | Zettelstore Base HTML Template | Contains the general layout of the HTML view
| [[00000000010200]] | Zettelstore Login Form HTML Template | Layout of the login form, when authentication is [[enabled|00001010040100]]
| [[00000000010300]] | Zettelstore List Meta HTML Template | Used when displaying a list of zettel
| [[00000000010401]] | Zettelstore Detail HTML Template | Layout for the HTML detail view of one zettel
| [[00000000010402]] | Zettelstore Info HTML Template | Layout for the information view of a specific zettel
| [[00000000010402]] | Zettelstore Info HTML Templรถate | Layout for the information view of a specific zettel
| [[00000000010403]] | Zettelstore Form HTML Template | Form that is used to create a new or to change an existing zettel that contains text
| [[00000000010404]] | Zettelstore Rename Form HTML Template | View that is displayed to change the [[zettel identifier|00001006050000]]
| [[00000000010405]] | Zettelstore Delete HTML Template | View to confirm the deletion of a zettel
| [[00000000010500]] | Zettelstore List Roles HTML Template | Layout for listing all roles
| [[00000000010600]] | Zettelstore List Tags HTML Template | Layout of tags lists
| [[00000000020001]] | Zettelstore Base CSS | System-defined CSS file that is included by the [[Base HTML Template|00000000010100]]
| [[00000000025001]] | Zettelstore User CSS | User-defined CSS file that is included by the [[Base HTML Template|00000000010100]]
| [[00000000029000]] | Zettelstore Role to CSS Map | Maps [[role|00001006020000#role]] to a zettel identifier that is included by the [[Base HTML Template|00000000010100]] as an CSS file 
| [[00000000040001]] | Generic Emoji | Image that is shown if [[original image reference|00001007040322]] is invalid
| [[00000000090000]] | New Menu | Contains items that should contain in the zettel template menu
| [[00000000090001]] | New Zettel | Template for a new zettel with role ""[[zettel|00001006020100]]""
| [[00000000090002]] | New User | Template for a new [[user zettel|00001010040200]]
| [[00010000000000]] | Home | Default home zettel, contains some welcome information

If a zettel is not linked, it is not accessible for the current user.

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

Changes to docs/manual/00001006000000.zettel.

1
2
3
4
5
6

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

6
7
8
9
10
11
12
13





-
+







id: 00001006000000
title: Layout of a Zettel
role: manual
tags: #design #manual #zettelstore
syntax: zmk
modified: 20220724165931
modified: 20220113185522

A zettel consists of two parts: the metadata and the zettel content.
Metadata gives some information mostly about the zettel content, how it should be interpreted, how it is sorted within Zettelstore.
The zettel content is, well, the actual content.
In many cases, the content is in plain text form.
Plain text is long-lasting.
However, content in binary format is also possible.
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
21
22
23
24
25
26
27

28
29


























-
+

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

The zettel content is your valuable content.
Zettelstore contains some predefined parsers that interpret the zettel content to the syntax of the zettel.
This includes markup languages, like [[Zettelmarkup|00001007000000]] and [[CommonMark|00001008010500]].
Other text formats are also supported, like CSS and HTML templates.
Plain text content is always Unicode, encoded as UTF-8.
Other character encodings are not supported and will never be[^This is not a real problem, since every modern software should support UTF-8 as an encoding.].
There is support for a graphical format with a text representation: SVG.
There is support for a graphical format with a text represenation: SVG.
And there is support for some binary image formats, like GIF, PNG, and JPEG.

=== Plain, parsed, and evaluated zettel
Zettelstore may present your zettel in various forms.
One way is to present the zettel as it was read by Zettelstore.
This is called ""[[plain zettel|00001003000000#plain]]"", typically retrieved with the [[endpoint|00001012920000]] ''/z/{ID}''.

The second way is to present the zettel as it was recognized by Zettelstore.
This is called ""[[parsed zettel|00001012053600]]"", typically retrieved with the [[endpoint|00001012920000]] ''/p/{ID}''.
Such a zettel was read and analyzed.
It can be presented in various [[encodings|00001012920500]].[^The [[zmk encoding|00001012920522]] allows you to compare the plain, the parsed, and the evaluated form of a zettel.]

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

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

Changes to docs/manual/00001006020000.zettel.

1
2
3
4
5
6

7
8
9
10
11
12
13
14
15
16
17

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

6
7
8
9
10
11
12
13
14
15
16

17
18
19
20
21
22
23
24





-
+










-
+







id: 00001006020000
title: Supported Metadata Keys
role: manual
tags: #manual #meta #reference #zettel #zettelstore
syntax: zmk
modified: 20220628111132
modified: 20220218130146

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

; [!all-tags|''all-tags'']
: A property (a computed values that is not stored) that contains both the value of [[''tags''|#tags]] together with all [[tags|00001007040000#tag]] that are specified within the content.
; [!back|''back'']
: Is a property that contains the identifier of all zettel that reference the zettel of this metadata, that are not referenced by this zettel.
  Basically, it is the value of [[''backward''|#backward]], but without any zettel identifier that is contained in [[''forward''|#forward]].
  Basically, it is the value of [[''backward''|#bachward]], but without any zettel identifier that is contained in [[''forward''|#forward]].
; [!backward|''backward'']
: Is a property that contains the identifier of all zettel that reference the zettel of this metadata.
  References within invertible values are not included here, e.g. [[''precursor''|#precursor]].
; [!box-number|''box-number'']
: Is a computed value and contains the number of the box where the zettel was found.
  For all but the [[predefined zettel|00001005090000]], this number is equal to the number __X__ specified in startup configuration key [[''box-uri-__X__''|00001004010000#box-uri-x]].
; [!copyright|''copyright'']
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
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







-
+



-
+





-
+







; [!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.
  If not given, the value ''default-role'' from the [[configuration zettel|00001004020000#default-role]] will be used.
; [!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''.
  If not given, the value ''default-syntax'' from the [[configuration zettel|00001004020000#default-syntax]] will be used.
; [!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.
  If not given, the value ''default-title'' from the [[configuration zettel|00001004020000#default-title]] 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'']

Changes to docs/manual/00001006020100.zettel.

1
2
3
4
5
6

7
8
9
10


11
12
13
14
15
16
17
1
2
3
4
5

6
7
8
9

10
11
12
13
14
15
16
17
18





-
+



-
+
+







id: 00001006020100
title: Supported Zettel Roles
role: manual
tags: #manual #meta #reference #zettel #zettelstore
syntax: zmk
modified: 20220623183234
modified: 20220214174553

The [[''role'' key|00001006020000#role]] defines what kind of zettel you are writing.
You are free to define your own roles.
It is allowed to set an empty value or to omit the role.

The role ''zettel'' is predefined as the default role, but you can [[change this|00001004020000#default-role]].

Some roles are defined for technical reasons:

; [!configuration|''configuration'']
: A zettel that contains some configuration data for the Zettelstore.
  Most prominent is [[00000000000100]], as described in [[00001004020000]].
; [!manual|''manual'']

Changes to docs/manual/00001006036000.zettel.

1
2
3
4
5
6

7
8
9
10
11
12
13
14
15
16

17
18
19
1
2
3
4
5

6
7
8
9
10
11
12
13
14
15

16
17
18
19





-
+









-
+



id: 00001006036000
title: WordSet Key Type
role: manual
tags: #manual #meta #reference #zettel #zettelstore
syntax: zmk
modified: 20220724201056
modified: 20220111103714

Values of this type denote a (sorted) set of [[words|00001006035500]].

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

=== Allowed values
Must be a sequence of at least one word, separated by space characters.

=== Match operator
A value matches a WordSet value, if the first value is equal to one of the word values in the word set.
A value matches an wordset value, if the first value is equal to one of the word values in the word set.

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

Changes to docs/manual/00001006055000.zettel.

1
2
3
4
5
6

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

6
7
8
9
10
11
12
13





-
+







id: 00001006055000
title: Reserved zettel identifier
role: manual
tags: #design #manual #zettelstore
syntax: zmk
modified: 20220311111751
modified: 20211124182600

[[Zettel identifier|00001006050000]] are typically created by examine the current date and time.
By renaming a zettel, you are able to provide any sequence of 14 digits.
If no other zettel has the same identifier, you are allowed to rename a zettel.

To make things easier, you normally should not use zettel identifier that begin with four zeroes (''0000'').

37
38
39
40
41
42
43
44
37
38
39
40
41
42
43








-
| 00009000000000 | 0000999999999 | Reserved for applications

This list may change in the future.

==== External Applications
|= From | To | Description
| 00009000001000 | 00009000001999 | [[Zettel Presenter|https://zettelstore.de/contrib]], an application to display zettel as a HTML-based slideshow
| 00009000002000 | 00009000002999 | [[Zettel Blog|https://zettelstore.de/contrib]], an application to collect and transform zettel into a blog

Changes to docs/manual/00001007000000.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: 00001007000000
title: Zettelmarkup
role: manual
tags: #manual #zettelmarkup #zettelstore
syntax: zmk
modified: 20220113185501

Zettelmarkup is a rich plain-text based markup language for writing zettel content.
Besides the zettel content, Zettelmarkup is also used for specifying the title of a zettel, regardless of the syntax of a zettel.

Zettelmarkup supports the longevity of stored notes by providing a syntax that any person can easily read, as well as a computer.
Zettelmarkup can be much easier parsed / consumed by a software compared to other markup languages.
Zettelmark supports the longevity of stored notes by providing a syntax that any person can easily read, as well as a computer.
Zettelmark can be much easier parsed / consumed by a software compared to other markup languages.
Writing a parser for [[Markdown|https://daringfireball.net/projects/markdown/syntax]] is quite challenging.
[[CommonMark|00001008010500]] is an attempt to make it simpler by providing a comprehensive specification, combined with an extra chapter to give hints for the implementation.
Zettelmarkup follows some simple principles that anybody who knows to ho write software should be able understand to create an implementation.
Zettelmark follows some simple principles that anybody who knows to ho write software should be able understand to create an implementation.

Zettelmarkup is a markup language on its own.
This is in contrast to Markdown, which is basically a super-set of HTML.
This is in contrast to Markdown, which is basically a superset of HTML.
While HTML is a markup language that will probably last for a long time, it cannot be easily translated to other formats, such as PDF, JSON, or LaTeX.
Additionally, it is allowed to embed other languages into HTML, such as CSS or even JavaScript.
This could create problems with longevity as well as security problems.

Zettelmarkup is a rich markup language, but it focuses on relatively short zettel content.
Zettelmarkup is a rich markup language, but it focusses on relatively short zettel content.
It allows embedding other content, simple tables, quotations, description lists, and images.
It provides a broad range of inline formatting, including __emphasized__, **strong**, ~~deleted~~{-} and >>inserted>> text.
Footnotes[^like this] are supported, links to other zettel and to external material, as well as citation keys.

Zettelmarkup might be seen as a proprietary markup language.
But if you want to use [[Markdown/CommonMark|00001008010000]] and you need support for footnotes or tables, you'll end up with proprietary extensions.
However, the Zettelstore supports CommonMark as a zettel syntax, so you can mix both Zettelmarkup zettel and CommonMark zettel in one store to get the best of both worlds.

Changes to docs/manual/00001007010000.zettel.

29
30
31
32
33
34
35
36

37
38
39
40
41
42
43
29
30
31
32
33
34
35

36
37
38
39
40
41
42
43







-
+








Some inline elements do not follow the rule of two identical character, especially to specify [[footnotes|00001007040330]], [[citation keys|00001007040340]], and local marks.
These elements begin with one opening square bracket (""``[``""), use a character for specifying the kind of the inline, typically allow to specify some content, and end with one closing square bracket (""``]``"").

One inline element that does not begin with two characters is the ""entity"".
It allows to specify any Unicode character.
The specification of that character is put between an ampersand character and a semicolon: ``&...;``{=zmk}.
For example, an ""n-dash"" could also be specified as ``&ndash;``{==zmk}.
For exmple, an ""n-dash"" could also be specified as ``&ndash;``{==zmk}.

The backslash character (""``\\``"") possibly gives the next character a special meaning.
This allows to resolve some left ambiguities.
For example, a list of depth 2 will begin a line with ``** Item 2.2``{=zmk}.
An inline element to strongly emphasize some text begin with a space will be specified as ``** Text**``{=zmk}.
To force the inline element formatting at the beginning of a line, ``**\\ Text**``{=zmk} should better be specified.

Changes to docs/manual/00001007030000.zettel.

1
2
3
4
5
6

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

6
7
8
9
10
11
12
13





-
+







id: 00001007030000
title: Zettelmarkup: Block-Structured Elements
role: manual
tags: #manual #zettelmarkup #zettelstore
syntax: zmk
modified: 20220311181036
modified: 20220201133655

Every markup for blocks-structured elements (""blocks"") begins at the very first position of a line.

There are five kinds of blocks: lists, one-line blocks, line-range blocks, tables, and paragraphs.

=== Lists

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


42
43
44
45
46
47
48







-
-







Additionally, all other blocks elements are allowed in line-range blocks.

* [[Verbatim blocks|00001007030500]] do not interpret their content,
* [[Quotation blocks|00001007030600]] specify a block-length quotations,
* [[Verse blocks|00001007030700]] allow to enter poetry, lyrics and similar text, where line endings are important,
* [[Region blocks|00001007030800]] just mark regions, e.g. for common formatting,
* [[Comment blocks|00001007030900]] allow to enter text that will be ignored when rendered.
* [[Evaluation blocks|00001007031300]] specify some content to be evaluated by either Zettelstore or external software.
* [[Math-mode blocks|00001007031400]] can be used to enter mathematical formulas / equations.
* [[Inline-Zettel blocks|00001007031200]] provide a mechanism to specify zettel content with a new syntax without creating a new zettel.

=== Tables

Similar to lists are tables not specified explicitly.
A sequence of table rows is considered a [[table|00001007031000]].
A table row itself is a sequence of table cells.

Changes to docs/manual/00001007030200.zettel.

87
88
89
90
91
92
93
94

95
96
97
98
99
100
101
87
88
89
90
91
92
93

94
95
96
97
98
99
100
101







-
+







*# A.3
* B

* C
:::

Please note that two lists cannot be separated by an empty line.
Instead you should put a horizontal rule (""thematic break"") between them.
Instead you should put a horizonal rule (""thematic break"") between them.
You could also use a [[mark element|00001007040350]] or a hard line break to separate the two lists:
```zmk
# One
# Two
[!sep]
# Uno
# Due

Changes to docs/manual/00001007030600.zettel.

1
2
3
4
5
6
7
8
9
10

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

10
11
12
13
14
15
16
17









-
+







id: 00001007030600
title: Zettelmarkup: Quotation Blocks
role: manual
tags: #manual #zettelmarkup #zettelstore
syntax: zmk
modified: 20220218131806

A simple way to enter a quotation is to use the [[quotation list|00001007030200]].
A quotation list loosely follows the convention of quoting text within emails.
However, if you want to attribute the quotation to someone, a quotation block is more appropriately.
However, if you want to attribute the quotation to seomeone, a quotation block is more appropriately.

This kind of line-range block begins with at least three less-than characters (""''<''"", U+003C) at the first position of a line.
You can add some [[attributes|00001007050000]] on the beginning line of a quotation block, following the initiating characters.
The quotation does not support the default attribute, nor the generic attribute.
Attributes are interpreted on HTML rendering.
Any other character in this line will be ignored

Changes to docs/manual/00001007030800.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: 00001007030800
title: Zettelmarkup: Region Blocks
role: manual
tags: #manual #zettelmarkup #zettelstore
syntax: zmk
modified: 20220323190829
modified: 20220218131131

Region blocks does not directly have a visual representation.
They just group a range of lines.
You can use region blocks to enter [[attributes|00001007050000]] that apply only to this range of lines.
One example is to enter a multi-line warning that should be visible.

This kind of line-range block begins with at least three colon characters (""'':''"", U+003A) at the first position of a line[^Since a [[description text|00001007030100]] only use exactly one colon character at the first position of a line, there is no possible ambiguity between these elements.].
You can add some [[attributes|00001007050000]] on the beginning line of a region block, following the initiating characters.
The region block does not support the default attribute, but it supports the generic attribute.
Some generic attributes, like ``=note``, ``=warning`` will be rendered special.
All other generic attributes are used as a CSS class definition.
Attributes are interpreted on HTML rendering.
Any other character in this line will be ignored.

Text following the beginning line will be interpreted, until a line begins with at least the same number of the same characters given at the beginning line.
This allows to enter a region block within a region block.
At the ending line, you can enter some [[inline elements|00001007040000]] after the colon characters.
These will interpreted as some attribution text.
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
64
65
66
67
68
69
70




















-
-
-
-
-
-
-
-
-
-
-
-
-
Generic attributes that are result in a special HTML rendering are:
* example
* note
* tip
* important
* caution
* warning

All other generic attribute values are rendered as a CSS class:
```zmk
:::abc
def
:::
```
is rendered as
::::example
:::abc
def
:::
::::

Changes to docs/manual/00001007030900.zettel.

9
10
11
12
13
14
15
16

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

16
17
18
19
20
21
22
23







-
+







While the text entered inside a verbatim block will be processed somehow, text inside a comment block will be ignored[^Well, not completely ignored: text is read, but it will typically not rendered visible.].
Comment blocks are typically used to give some internal comments, e.g. the license of a text or some internal remarks.

Comment blocks begin with at least three percent sign characters (""''%''"", U+0025) at the first position of a line.
You can add some [[attributes|00001007050000]] on the beginning line of a comment block, following the initiating characters.
The comment block supports the default attribute: when given, the text will be rendered, e.g. as an HTML comment.
When rendered to JSON, the comment block will not be ignored but it will output some JSON text.
Same for other renderer.
Same for other renderers.

Any other character in this line will be ignored

Text following the beginning line will not be interpreted, until a line begins with at least the same number of the same characters given at the beginning line.
This allows to enter some percent sign characters in the text that should not be interpreted.

For example:

Changes to docs/manual/00001007031000.zettel.

1
2
3
4
5
6
7
8

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

8
9
10
11
12
13
14
15







-
+







id: 00001007031000
title: Zettelmarkup: Tables
role: manual
tags: #manual #zettelmarkup #zettelstore
syntax: zmk
modified: 20220218131107

Tables are used to show some data in a two-dimensional fashion.
Tables are used to show some data in a two-dimenensional fashion.
In zettelmarkup, table are not specified explicitly, but by entering __table rows__.
Therefore, a table can be seen as a sequence of table rows.
A table row is nothing as a sequence of __table cells__.
The length of a table is the number of table rows, the width of a table is the maximum length of its rows.

The first cell of a row must begin with the vertical bar character (""''|''"", U+007C) at the first position of a line.
The other cells of a row begin with the same vertical bar character at later positions in that line.

Changes to docs/manual/00001007031200.zettel.

1
2
3
4
5
6

7
8

9
10


11
12
13
14
15
16
17
18

19
20
21
22
23
24
25
26

















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

6
7
8
9


10
11
12
13
14
15
16
17
18

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





-
+


+
-
-
+
+







-
+








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







id: 00001007031200
title: Zettelmarkup: Inline-Zettel Block
role: manual
tags: #manual #zettelmarkup #zettelstore
syntax: zmk
modified: 20220311112247
modified: 20220218172121

An inline-zettel block allows to specify some content with another syntax without creating a new zettel.
This is useful, for example, if you want to specify a [[simple drawing|00001008050000]] within your zettel and you are sure that you do not need the drawing in another context.
This is useful, for example, if you want to embed some [[Markdown|00001008010500]] content, because you are too lazy to translate Markdown into Zettelmarkup.
Another example is to specify HTML code to use it for some kind of web front-end framework.
Another example is to embed some [[Markdown|00001008010500]] content, because you are too lazy to translate Markdown into Zettelmarkup.[^However, translating into Zettelmarkup is quite easy with the [[zmk encoder|00001012920522]].]
A last example is to specify HTML code to use it for some kind of web frontend framework.

As all other [[line-range blocks|00001007030000#line-range-blocks]], an inline-zettel block begins with at least three identical characters, starting at the first position of a line.
For inline-zettel blocks, the at-sign character (""''@''"", U+0040) is used.

You can add some [[attributes|00001007050000]] on the beginning line of a verbatim block, following the initiating characters.
The inline-zettel block uses the attribute key ""syntax"" to specify the [[syntax|00001008000000]] of the inline-zettel.
Alternatively, you can use the generic attribute to specify the syntax value.
If no value is provided, ""[[text|00001008000000#text]]"" is assumed.
If no value is provided, ""draw"" is assumed.

Any other character in this first line will be ignored.

Text following the beginning line will not be interpreted, until a line begins with at least the same number of the same at-sign characters given at the beginning line.
This allows to enter some at-sign characters in the text that should not be interpreted at this level.

Some examples:
```zmk
@@@draw
+-------+      +-------+
| Box 1 | ---> | Box 2 |
+-------+      +-------+
@@@
```
This will be rendered as:
:::example
@@@draw
+-------+      +-------+
| Box 1 | ---> | Box 2 |
+-------+      +-------+
@@@
:::

Using Markdown syntax:
```zmk
@@@markdown
A link to [this](00001007031200) zettel.
@@@
```
will be rendered as:
:::example
@@@markdown

Deleted docs/manual/00001007031300.zettel.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66


































































-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
id: 00001007031300
title: Zettelmarkup: Evaluation Blocks
role: manual
tags: #manual #zettelmarkup #zettelstore
syntax: zmk
modified: 20220311120658

Evaluation blocks are used to enter text that could be evaluated by either Zettelstore or external software.
They begin with at least three tilde characters (""''~''"", U+007E) at the first position of a line.

You can add some [[attributes|00001007050000]] on the beginning line of a verbatim block, following the initiating characters.
The evaluation block supports the default attribute[^Depending on the syntax value.]: when given, all spaces in the text are rendered in HTML as open box characters (U+2423).
If you want to give only one attribute and this attribute is the generic attribute, you can omit the most of the attribute syntax and just specify the value.
It will be interpreted as a [[syntax|00001008000000]] value to evaluate its content.
Not all syntax values are supported by Zettelstore.[^Currently just ""[[draw|00001008050000]]"".]
The main reason for an evaluation block is to be used with external software via the [[ZJSON encoding|00001012920503]].

Any other character in this line will be ignored

Text following the beginning line will not be interpreted, until a line begins with at least the same number of the same characters given at the beginning line.
This allows to enter some tilde characters in the text that should not be interpreted.

For example:
`````zmk
~~~~
~~~
~~~~
`````
will be rendered in HTML as:
:::example
~~~~
~~~
~~~~
:::

`````zmk
~~~{-}
This is  some
text with no 
  real sense.
~~~~
`````
will be rendered as:
:::example
~~~{-}
This is  some
text with no 
  real sense.
~~~~
:::

`````zmk
~~~draw
+---+     +---+
| A | --> | B |
+---+     +---+
~~~
`````
will be rendered as:
:::example
~~~draw
+---+     +---+
| A | --> | B |
+---+     +---+
~~~
:::

Deleted docs/manual/00001007031400.zettel.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74










































































-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
id: 00001007031400
title: Zettelmarkup: Math-mode Blocks
role: manual
tags: #manual #zettelmarkup #zettelstore
syntax: zmk
modified: 20220311182505

Math-mode blocks are used to enter mathematical formulas / equations in a display style mode.
Similar to a [[evaluation blocks|00001007031300]], the block content will be interpreted by either Zettelstore or an external software.
They begin with at least three dollar sign characters (""''$''"", U+0024) at the first position of a line.

You can add some [[attributes|00001007050000]] on the beginning line of a verbatim block, following the initiating characters.
A math-mode block supports the default attribute[^Depending on the syntax value.]: when given, all spaces in the text are rendered in HTML as open box characters (U+2423).
If you want to give only one attribute and this attribute is the generic attribute, you can omit the most of the attribute syntax and just specify the value.
It will be interpreted as a [[syntax|00001008000000]] value to evaluate its content.
Alternatively, you could provide an attribute with the key ""syntax"" and use the value to specify the syntax.
Not all syntax values are supported by Zettelstore.[^Currently: none.]
External software might support several values via the [[ZJSON encoding|00001012920503]].

Any other character in this line will be ignored

Text following the beginning line will not be interpreted, until a line begins with at least the same number of the same characters given at the beginning line.
This allows to enter some dollar-sign characters in the text that should not be interpreted.

For example:
`````zmk
$$$$
$$$
$$$$
`````
will be rendered in HTML as:
:::example
$$$$
$$$
$$$$
:::

`````zmk
$$${-}
This is  some
text with no 
  real sense.
$$$$
`````
will be rendered as:
:::example
$$${-}
This is  some
text with no 
  real sense.
$$$$
:::

In the future, Zettelstore might somehow support mathematical formulae with a $$\TeX$$-like syntax.
Until then,
`````zmk
$$$
\begin{align*}
  f(x) &= x^2\\
  g(x) &= \frac{1}{x}\\
  F(x) &= \int^a_b \frac{1}{3}x^3
\end{align*}
$$$
`````
is rendered as:
:::example
$$$
\begin{align*}
  f(x) &= x^2\\
  g(x) &= \frac{1}{x}\\
  F(x) &= \int^a_b \frac{1}{3}x^3
\end{align*}
$$$
:::

Changes to docs/manual/00001007040100.zettel.

18
19
20
21
22
23
24
25

26
27

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

25
26

27
28
29
30
31
32
33







-
+

-
+






** Example: ``abc __def__ ghi`` is rendered in HTML as: ::abc __def__ ghi::{=example}.
* The asterisk character (""''*''"", U+002A) strongly emphasized its enclosed text.
** Example: ``abc **def** ghi`` is rendered in HTML as: ::abc **def** ghi::{=example}.
* The greater-than sign character (""''>''"", U+003E) marks text as inserted.
** Example: ``abc >>def>> ghi`` is rendered in HTML as: ::abc >>def>> ghi::{=example}.
* Similar, the tilde character (""''~''"", U+007E) marks deleted text.
** Example: ``abc ~~def~~ ghi`` is rendered in HTML as: ::abc ~~def~~ ghi::{=example}.
* The circumflex accent character (""''^''"", U+005E) allows to enter super-scripted text.
* The circumflex accent character (""''^''"", U+005E) allows to enter superscripted text.
** Example: ``e=mc^^2^^`` is rendered in HTML as: ::e=mc^^2^^::{=example}.
* The comma character (""'',''"", U+002C) produces sub-scripted text.
* The comma character (""'',''"", U+002C) produces subscripted text.
** Example: ``H,,2,,O`` is rendered in HTML as: ::H,,2,,O::{=example}.
* The quotation mark character (""''"''"", U+0022) marks an inline quotation, according to the [[specified language|00001007050100]].
** Example: ``""To be or not""`` is rendered in HTML as: ::""To be or not""::{=example}.
** Example: ``""Sein oder nicht""{lang=de}`` is rendered in HTML as: ::""Sein oder nicht""{lang=de}::{=example}.
* The colon character (""'':''"", U+003A) mark some text that should belong together. It fills a similar role as [[region blocks|00001007030800]], but just for inline elements.
** Example: ``abc ::def::{=example} ghi`` is rendered in HTML as: abc ::def::{=example} ghi.

Changes to docs/manual/00001007040200.zettel.

1
2
3
4
5
6

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

6
7
8
9
10
11
12
13





-
+







id: 00001007040200
title: Zettelmarkup: Literal-like formatting
role: manual
tags: #manual #zettelmarkup #zettelstore
syntax: zmk
modified: 20220311185110
modified: 20220218131420

There are some reasons to mark text that should be rendered as uninterpreted:
* Mark text as literal, sometimes as part of a program.
* Mark text as input you give into a computer via a keyboard.
* Mark text as output from some computer, e.g. shown at the command line.

=== Literal text
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
47
48
49
50
51
52
53

54
55
56
57
58
59
60
61


























-
+







-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
Attributes can be specified, the default attribute has the same semantic as for literal text.

=== Inline-zettel snippet
To specify an inline snippet in a different [[syntax|00001008000000]], delimit your text with two at-sign characters (""''@''"", U+0040) on each side.

You can add some [[attributes|00001007050000]] immediate after the two closing at-sign characters to specify the syntax to use.
Either use the attribute key ""syntax"" or use the generic attribute to specify the syntax value.
If no value is provided, ""[[text|00001008000000#text]]"" is assumed.
If no value is provided, ""draw"" is assumed.

Examples:
* ``A @@-->@@ B`` renders in HTML as ::A @@-->@@ B::{=example}.
* ``@@<small>@@{=html}Small@@</small>@@{=html}`` renders in HTML as ::@@<small>@@{=html}Small@@</small>@@{=html}::{=example}.

To some degree, an inline-zettel snippet is the @@<small>@@{=html}smaller@@</small>@@{=html} sibling of the [[inline-zettel block|00001007031200]].
For HTML syntax, the same rules apply.

=== Math mode / $$\TeX$$ input
This allows to enter text, that is typically interpreted by $$\TeX$$ or similar software.
The main difference to all other literal-like formatting above is that the backslash character (""''\\''"", U+005C) has no special meaning.
Therefore it is well suited the enter text with a lot of backslash characters.

Math mode text is delimited with two dollar signs (""''$''"", U+0024) on each side.

You can add some [[attributes|00001007050000]] immediate after the two closing at-sign characters to specify the syntax to use.
Either use the attribute key ""syntax"" or use the generic attribute to specify the syntax value.
If no syntax value is provided, math mode text roughly corresponds to literal text.

Currently, Zettelstore does not support any syntax.
This will probably change.
However, external software might support specific syntax value, like ""tex"", ""latex"", ""mathjax"", ""itex"", or ""webtex"".
Even an empty syntax value might be supported.

Example:
* ``Happy $$\\TeX$$!``is rendered as ::Happy $$\TeX$$!::{=example}

Changes to docs/manual/00001007040324.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
17





-
+



+







id: 00001007040324
title: Zettelmarkup: Inline-mode Transclusion
role: manual
tags: #manual #zettelmarkup #zettelstore
syntax: zmk
modified: 20220311110814
modified: 20220131155955

Inline-mode transclusion applies to all zettel that are parsed in a non-trivial way, e.g. as structured textual content.
For example, textual content is assumed if the [[syntax|00001006020000#syntax]] of a zettel is ""zmk"" ([[Zettelmarkup|00001007000000]]), or ""markdown"" / ""md"" ([[Markdown|00001008010000]]).
If the syntax is ""[[draw|00001008050000]]"", it is also a non-trivial way.

Since this type of transclusion is at the level of [[inline-structured elements|00001007040000]], the transclude specification must be replaced with some inline-structured elements.

First, the referenced zettel is read.
If it contains other transclusions, these will be expanded, recursively.
When an endless recursion is detected, expansion does not take place.
Instead an error message replaces the transclude specification.

Changes to docs/manual/00001007040340.zettel.

1
2
3
4
5
6
7
8

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

8
9
10
11
12
13
14
15







-
+







id: 00001007040340
title: Zettelmarkup: Citation Key
role: manual
tags: #manual #zettelmarkup #zettelstore
syntax: zmk
modified: 20220218133447

A citation key references some external material that is part of a bibliographical collection.
A citation key references some external material that is part of a bibliografical collection.

Currently, Zettelstore implements this only partially, it is ""work in progress"".

However, the syntax is: beginning with a left square bracket and followed by an at sign character (""''@''"", U+0040), a the citation key is given.
The key is typically a sequence of letters and digits.
If a comma character (""'',''"", U+002C) or a vertical bar character is given, the following is interpreted as [[inline elements|00001007040000]].
A right square bracket ends the text and the citation key element.

Changes to docs/manual/00001007050000.zettel.

1
2
3
4
5
6

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

6
7
8
9
10
11
12
13





-
+







id: 00001007050000
title: Zettelmarkup: Attributes
role: manual
tags: #manual #zettelmarkup #zettelstore
syntax: zmk
modified: 20220630194106
modified: 20220218132935

Attributes allows to modify the way how material is presented.
Alternatively, they provide additional information to markup elements.
To some degree, attributes are similar to [[HTML attributes|https://html.spec.whatwg.org/multipage/dom.html#global-attributes]].

Typical use cases for attributes are to specify the (natural) [[language|00001007050100]] for a text region, to specify the [[programming language|00001007050200]] for highlighting program code, or to make white space visible in plain text.

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







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

-
-
+
+
+
+
+

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

+

















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




The key ""''-''"" (just hyphen-minus) is special.
It is called __default attribute__ and has a markup specific meaning.
For example, when used for plain text, it replaces the non-visible space with a visible representation:

* ''``Hello, world``{-}'' produces ==Hello, world=={-}.
* ''``Hello, world``'' produces ==Hello, world==.

For some [[block-structured elements|00001007030000]], there is a syntax variant if you only want to specify a generic attribute.
For all line-range blocks you can specify the generic attributes directly in the first line, after the three (or more) block characters.
Attributes may be continued on the next line when a space or line ending character is possible.
In case of a quoted attribute value, the line ending character will be part of the attribute value.
For example:

```
:::attr
...
:::
```
is equivalent to
```
:::{=attr}
...
:::
```.

For other blocks, the closing curly bracket must be on the same line where the block element begins.
However, spaces are allowed between the blocks characters and the attributes.
```
=== Heading {example}
```
{key="quoted
value"}
is allowed and equivalent to
```
=== Heading{example}
```.
But
```
=== Heading {class=example
background=grey}
```
is not allowed. Same for
```
=== Heading {background=color:"
green"}
```.
will produce a value ''quoted\\nvalue'' (where \\n denotes a line ending character).

For [[inline-structued elements|00001007040000]], the attributes must immediately follow the inline markup.
However, the attributes may be continued on the next line when a space or line ending character is possible.

``::GREEN::{example}`` is allowed, but not ``::GREEN:: {example}``.

```
::GREEN::{class=example
background=grey}
```
is allowed, but not
```
::GREEN::{background=color:
green}
```.

However,
```
::GREEN::{background=color:"
green"}
```
is allowed, because line endings are allowed within quotes.

For [[block-structured elements|00001007030000]], there is a syntax variant if you only want to specify a generic attribute.
For all line-range blocks you can specify the generic attributes directly in the first line, after the three (or more) characters starting the block.

```
:::attr
...
:::
```
is equivalent to
```
:::{=attr}
...
:::
```.

For block-structured elements, spaces are allowed between the blocks characters and the attributes.
```
=== Heading {example}
```
is allowed and equivalent to
```
=== Heading{example}
```.

For [[inline-structured elements|00001007040000]], the attributes must immediately follow the inline markup.

``::GREEN::{example}`` is allowed, but not ``::GREEN:: {example}``.


=== Reference material
* [[Supported attribute values for natural languages|00001007050100]]
* [[Supported attribute values for programming languages|00001007050200]]

Changes to docs/manual/00001007060000.zettel.

1
2
3
4
5
6

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


24
25
26
27
28
29
30
31
32

33
34
35
36

37
38
39
40
41
42

1
2
3
4
5

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


22
23
24
25
26
27
28
29
30
31

32
33
34
35

36
37
38
39
40
41

42





-
+















-
-
+
+








-
+



-
+





-
+
id: 00001007060000
title: Zettelmarkup: Summary of Formatting Characters
role: manual
tags: #manual #reference #zettelmarkup #zettelstore
syntax: zmk
modified: 20220311120759
modified: 20220218124943

The following table gives an overview about the use of all characters that begin a markup element.

|= Character :|= [[Blocks|00001007030000]] <|= [[Inlines|00001007040000]] <
| ''!''  | (free) | (free)
| ''"''  | [[Verse block|00001007030700]] | [[Short inline quote|00001007040100]]
| ''#''  | [[Ordered list|00001007030200]] | [[Tag|00001007040000]]
| ''$''  | (reserved) | (reserved)
| ''%''  | [[Comment block|00001007030900]] | [[Comment|00001007040000]]
| ''&''  | (free) | [[Entity|00001007040000]]
| ''\'''  | (free)  | [[Computer input|00001007040200]]
| ''(''  | (free) | (free)
| '')''  | (free) | (free)
| ''*''  | [[Unordered list|00001007030200]] | [[strongly emphasized text|00001007040100]]
| ''+''  | (free) | (free)
| '',''  | (free) | [[Sub-scripted text|00001007040100]]
| ''-''  | [[Horizontal rule|00001007030400]] | ""[[en-dash|00001007040000]]""
| '',''  | (free) | [[Subscripted text|00001007040100]]
| ''-''  | [[Horizonal rule|00001007030400]] | ""[[en-dash|00001007040000]]""
| ''.''  | (free) | [[Horizontal ellipsis|00001007040000]]
| ''/''  | (free) | (free)
| '':''  | [[Region block|00001007030800]] / [[description text|00001007030100]] | [[Inline region|00001007040100]]
| '';''  | [[Description term|00001007030100]] | [[Small text|00001007040100]]
| ''<''  | [[Quotation block|00001007030600]] | (free)
| ''=''  | [[Headings|00001007030300]] | [[Computer output|00001007040200]]
| ''>''  | [[Quotation lists|00001007030200]] | [[Inserted text|00001007040100]]
| ''?''  | (free) | (free)
| ''@''  | [[Inline-Zettel block|00001007031200]] | [[Inline-zettel snippet|00001007040200#inline-zettel-snippet]]
| ''@''  | [[Inline-Zettel blocks|00001007031200]] | [[Inline-zettel snippets|00001007040200#inline-zettel-snippet]]
| ''[''  | (reserved)  | [[Linked material|00001007040300]], [[citation key|00001007040300]], [[footnote|00001007040300]], [[mark|00001007040300]]
| ''\\'' | (blocked by inline meaning) | [[Escape character|00001007040000]]
| '']''  | (reserved) | End of [[link|00001007040300]], [[citation key|00001007040300]], [[footnote|00001007040300]], [[mark|00001007040300]]
| ''^''  | (free) | [[Super-scripted text|00001007040100]]
| ''^''  | (free) | [[Superscripted text|00001007040100]]
| ''_''  | (free) | [[Emphasized text|00001007040100]]
| ''`''  | [[Verbatim block|00001007030500]] | [[Literal text|00001007040200]]
| ''{''  | [[Transclusion|00001007031100]] | [[Embedded material|00001007040300]], [[Attribute|00001007050000]]
| ''|''  | [[Table row / table cell|00001007031000]] | Separator within link and [[embed|00001007040320]] formatting
| ''}''  | End of [[Transclusion|00001007031100]] | End of embedded material, End of Attribute
| ''~''  | [[Evaluation block|00001007031300]] | [[Deleted text|00001007040100]]
| ''~''  | (free) | [[Deleted text|00001007040100]]

Changes to docs/manual/00001008000000.zettel.

1
2
3
4
5
6

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

21
22
23
24


25
26
27

28
29
30
31
32

33
34
35
36
37
38
39
40
1
2
3
4
5

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

20
21
22
23
24
25
26
27
28

29
30
31
32
33

34

35
36
37
38
39
40
41





-
+













-
+




+
+


-
+




-
+
-







id: 00001008000000
title: Other Markup Languages
role: manual
tags: #manual #zettelstore
syntax: zmk
modified: 20220627192329
modified: 20220214180202

[[Zettelmarkup|00001007000000]] is not the only markup language you can use to define your content.
Zettelstore is quite agnostic with respect to markup languages.
Of course, Zettelmarkup plays an important role.
However, with the exception of zettel titles, you can use any (markup) language that is supported:

* CSS
* HTML template data
* Image formats: GIF, PNG, JPEG, SVG
* Markdown
* Plain text, not further interpreted

The [[metadata key|00001006020000#syntax]] ""''syntax''"" specifies which language should be used.
If it is not given, it defaults to ''plain''.
If it is not given, the key ""''default-syntax''"" will be used (specified in the [[configuration zettel|00001004020000#default-syntax]]).
The following syntax values are supported:

; [!css|''css'']
: A [[Cascading Style Sheet|https://www.w3.org/Style/CSS/]], to be used when rendering a zettel as HTML.
; [!draw|''draw'']
: A simple [[language|00001008050000]] to ""draw"" a graphic by using some simple Unicode characters.
; [!gif|''gif'']; [!jpeg|''jpeg'']; [!jpg|''jpg'']; [!png|''png'']
: The formats for pixel graphics.
  Typically the data is stored in a separate file and the syntax is given in the meta-file, which has the same name as the zettel identifier and has no file extension.[^Before version 0.2, the meta-file had the file extension ''.meta'']
  Typically the data is stored in a separate file and the syntax is given in the metafile, which has the same name as the zettel identifier and has no file extension.[^Before version 0.2.0, the metafile had the file extension ''.meta'']
; [!html|''html'']
: Hypertext Markup Language, will not be parsed further.
  Instead, it is treated as [[text|#text]], but will be encoded differently for [[HTML format|00001012920510]] (same for the [[web user interface|00001014000000]]).

  For security reasons, equivocal elements will not be encoded in the HTML format / web user interface.
  For security reasons, equivocal elements will not be encoded in the HTML format / web user interface, e.g. the ``<script ...`` tag.
  The ``< script ...>`` tag is an example.
  See [[security aspects of Markdown|00001008010000#security-aspects]] for some details.
; [!markdown|''markdown''], [!md|''md'']
: For those who desperately need [[Markdown|https://daringfireball.net/projects/markdown/]].
  Since the world of Markdown is so diverse, a [[CommonMark|00001008010500]] parser is used.
  See [[Use Markdown within Zettelstore|00001008010000]].
; [!mustache|''mustache'']
: A [[Mustache template|https://mustache.github.io/]], used when rendering a zettel as HTML for the [[web user interface|00001014000000]].
49
50
51
52
53
54
55
56
57
58
59
60
61
62
50
51
52
53
54
55
56














-
-
-
-
-
-
-
: Just plain text that must not be interpreted further.
; [!zmk|''zmk'']
: [[Zettelmarkup|00001007000000]].

The actual values are also listed in a zettel named [[Zettelstore Supported Parser|00000000000092]].

If you specify something else, your content will be interpreted as plain text.

=== Language for other elements of a zettel
[[Zettelmarkup|00001007000000]] allows to specify [[evaluation blocks|00001007031300]], which also receive a syntax value.
An evaluation blocks is typically interpreted by external software, for example [[Zettel Presenter|00001006055000#external-applications]].
However, some values are interpreted by Zettelstore during evaluation of a zettel:
; [!draw|''draw'']
: A [[language|00001008050000]] to ""draw"" a graphic by using some simple Unicode characters.

Changes to docs/manual/00001008010000.zettel.

1
2
3
4
5
6

7
8
9
10
11
12
13

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

34
35
36
37
38
39
40
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





-
+






-
+



















-
+







id: 00001008010000
title: Use Markdown within Zettelstore
role: manual
tags: #manual #markdown #zettelstore
syntax: zmk
modified: 20220627192014
modified: 20220113185400

If you are customized to use Markdown as your markup language, you can configure Zettelstore to support your decision.
Zettelstore supports the [[CommonMark|00001008010500]] dialect of Markdown.

=== Use Markdown as the default markup language of Zettelstore

Update the [[New Zettel|00000000090001]] template (and other relevant template zettel) by setting the syntax value to ''md'' or ''markdown''.
Add the key ''default-syntax'' with a value of ''md'' or ''markdown'' to the [[configuration zettel|00000000000100]].
Whether to use ''md'' or ''markdown'' is not just a matter to taste.
It also depends on the value of [[''zettel-file-syntax''|00001004020000#zettel-file-syntax]] and, to some degree, on the value of [[''yaml-header''|00001004020000#yaml-header]].

If you set ''yaml-header'' to true, then new content is always stored in a file with the extension ''.zettel''.

Otherwise ''zettel-file-syntax'' lists all syntax values, where its content should be stored in a file with the extension ''.zettel''.

If neither ''yaml-header'' nor ''zettel-file-syntax'' is set, new content is stored in a file where its file name extension is the same as the syntax value of that zettel.
In this case it makes a difference, whether you specify ''md'' or ''markdown''.
If you specify the syntax ''md'', your content will be stored in a file with the ''.md'' extension.
Similar for the syntax ''markdown''.

If you want to process the files that store the zettel content, e.g. with some other Markdown tools, this may be important.
Not every Markdown tool allows both file extensions.

BTW, metadata is stored in a file without a file extension, if neither ''yaml-header'' nor ''zettel-file-syntax'' is set.

=== Security aspects

You should be aware that Markdown is a super-set of HTML.
You should be aware that Markdown is a superset of HTML.
Any HTML code is valid Markdown code.
If you write your own zettel, this is probably not a problem.

However, if you receive zettel from others, you should be careful.
An attacker might include malicious HTML code in your zettel.
For example, HTML allows to embed JavaScript, a full-sized programming language that drives many web sites.
When a zettel is displayed, JavaScript code might be executed, sometimes with harmful results.

Changes to docs/manual/00001008010500.zettel.

15
16
17
18
19
20
21
22
23


24
25
26
15
16
17
18
19
20
21


22
23
24
25
26







-
-
+
+



But they provide proprietary extensions, which makes it harder to change to another CommonMark implementation if needed.
Plus, they sometimes build on an older specification of CommonMark.

Zettelstore supports the latest CommonMark [[specification version 0.30 (2021-06-19)|https://spec.commonmark.org/0.30/]].
If possible, Zettelstore will adapt to newer versions when they are available.

To provide CommonMark support, Zettelstore uses currently the [[Goldmark|https://github.com/yuin/goldmark]] implementation, which passes all validation tests of CommonMark.
Internally, CommonMark is translated into some kind of super-set of [[Zettelmarkup|00001007000000]], which additionally allows to use HTML code.[^Effectively, Markdown and CommonMark are itself super-sets of HTML.]
This Zettelmarkup super-set is later [[encoded|00001012920500]], often into [[HTML|00001012920510]].
Internally, CommonMark is translated into some kind of superset of [[Zettelmarkup|00001007000000]], which additionally allows to use HTML code.[^Effectively, Markdown and CommonMark are itself supersets of HTML.]
This Zettelmarkup superset is later [[encoded|00001012920500]], often into [[HTML|00001012920510]].
Because Zettelstore HTML encoding philosophy differs a little bit to that of CommonMark, Zettelstore itself will not pass the CommonMark test suite fully.
However, no CommonMark language element will fail to be encoded as HTML.
In most cases, the differences are not visible for an user, but only by comparing the generated HTML code.

Changes to docs/manual/00001008050000.zettel.

1
2
3
4
5
6

7
8
9

10
11
12
13
14

15
16

17
18
19
20
21


22
23

24

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

6
7
8

9
10
11
12


13
14

15

16
17


18
19
20

21

22
23
24
25
26
27
28
29





-
+


-
+



-
-
+

-
+
-


-
-
+
+

-
+
-
+







id: 00001008050000
title: The ""draw"" language
role: manual
tags: #graphic #manual #zettelstore
syntax: zmk
modified: 20220311120439
modified: 20220217180713

Sometimes, ""a picture is worth a thousand words"".
To create some graphical representations, Zettelmarkup provides a simple mechanism.
To create some graphical representations, Zettelstore provides a simple mechanism.
Characters like ""''|''"" or ""''-''"" already provide some visual feedback.
For example, to create a picture containing two boxes that are connected via an arrow, the following representation is possible:
```
~~~draw
+-------+       .-------.
+-------+       +-------+
| Box 1 | ----> | Box 2 |
+-------+       '-------'
+-------+       +-------+
~~~
```
Zettelstore translates this to:
~~~draw
+-------+       .-------.
@@@draw
+-------+       +-------+
| Box 1 | ----> | Box 2 |
+-------+       '-------'
+-------+       +-------+
~~~
@@@

Technically spoken, the drawing is translated to a [[SVG|00001008000000#svg]] element.

The following characters are interpreted to create a graphical representation.
Some of them will start a path that results in a recognized object.

|=Character:|Meaning|Path Start:

Changes to docs/manual/00001010000000.zettel.

38
39
40
41
42
43
44
45

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

45
46
47
48
49
50
51
52







-
+







Or you want to allow them to create new zettel, or to change them.
It is up to you.

If someone is authenticated as the owner of the Zettelstore (hopefully you), no restrictions apply.
But as an owner, you can create ""user zettel"" to allow others to access your Zettelstore in various ways.
Even if you do not want to share your Zettelstore with other persons, creating user zettel can be useful if you plan to access your Zettelstore via the [[API|00001012000000]].

Additionally, you can specify that a zettel is publicly visible.
Additionally, you can specify that a zettel is publicily visible.
In this case no one has to authenticate itself to see the content of the zettel.
Or you can specify that a zettel is visible only to the owner.
In this case, no authenticated user will be able to read and change that protected zettel.

* [[Visibility rules for zettel|00001010070200]]
* [[User roles|00001010070300]] define basic rights of an user
* [[Authorization and read-only mode|00001010070400]]

Changes to docs/manual/00001010040100.zettel.

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

6
7
8
9







-




-
-
id: 00001010040100
title: Enable authentication
role: manual
tags: #authentication #configuration #manual #security #zettelstore
syntax: zmk
modified: 20220419192817

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

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

Changes to docs/manual/00001012000000.zettel.

1
2
3
4
5
6

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

6
7
8
9
10
11
12
13





-
+







id: 00001012000000
title: API
role: manual
tags: #api #manual #zettelstore
syntax: zmk
modified: 20220627183444
modified: 20220304173249

The API (short for ""**A**pplication **P**rogramming **I**nterface"") is the primary way to communicate with a running Zettelstore.
Most integration with other systems and services is done through the API.
The [[web user interface|00001014000000]] is just an alternative, secondary way of interacting with a Zettelstore.

=== Background
The API is HTTP-based and uses plain text and JSON as its main encoding format for exchanging messages between a Zettelstore and its client software.
23
24
25
26
27
28
29
30


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

30
31
32
33
34
35
36
37
38







-
+
+







=== Zettel lists
* [[List metadata of all zettel|00001012051200]]
* [[Shape the list of zettel metadata|00001012051800]]
** [[Selection of zettel|00001012051810]]
** [[Limit the list length|00001012051830]]
** [[Content search|00001012051840]]
** [[Sort the list of zettel metadata|00001012052000]]
* [[Map metadata values to lists of zettel identifier|00001012052400]]
* [[List all tags|00001012052400]]
* [[List all roles|00001012052600]]

=== Working with zettel
* [[Create a new zettel|00001012053200]]
* [[Retrieve metadata and content of an existing zettel|00001012053300]]
* [[Retrieve metadata of an existing zettel|00001012053400]]
* [[Retrieve evaluated metadata and content of an existing zettel in various encodings|00001012053500]]
* [[Retrieve parsed metadata and content of an existing zettel in various encodings|00001012053600]]

Changes to docs/manual/00001012052400.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
1

2
3
4
5

6


7





8
9
10

11












12
13
14

15
16
17
18

-
+



-

-
-
+
-
-
-
-
-



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


-
+



id: 00001012052400
title: API: Map metadata values to list of zettel identifier
title: API: List all tags
role: manual
tags: #api #manual #zettelstore
syntax: zmk
modified: 20220627183323

The [[endpoint|00001012920000]] ''/m'' allows to retrieve a map of metadata values (of a specific key) to the list of zettel identifier, which reference zettel containing this value under the given metadata key.

To list all [[tags|00001006020000#tags]] used in the Zettelstore just send a HTTP GET request to the [[endpoint|00001012920000]] ''/t''.
Currently, two keys are supported:
* [[''role''|00001006020100]]
* [[''tags''|00001006020000#tags]]

To list all roles used in the Zettelstore, send a HTTP GET request to the endpoint ''/m?_key=role''.
If successful, the output is a JSON object:

```sh
# curl http://127.0.0.1:23123/m?_key=role
# curl http://127.0.0.1:23123/t
{"map":{"configuration":["00000000090002","00000000090000", ... ,"00000000000001"],"manual":["00001014000000", ... ,"00001000000000"],"zettel":["00010000000000", ... ,"00001012070500","00000000090001"]}}
```

The JSON object only contains the key ''"map"'' with the value of another object.
This second object contains all role names as keys and the list of identifier of those zettel with this specific role as a value.

Similar, to list all tags used in the Zettelstore, send a HTTP GET request to the endpoint ''/m?_key=tags''.
If successful, the output is a JSON object:

```sh
# curl http://127.0.0.1:23123/m?_key=tags
{"map":{"#api":[:["00001012921000","00001012920800","00001012920522",...],"#authorization":["00001010040700","00001010040400",...],...,"#zettelstore":["00010000000000","00001014000000",...,"00001001000000"]}}
{"tags":{"#api":[:["00001012921000","00001012920800","00001012920522",...],"#authorization":["00001010040700","00001010040400",...],...,"#zettelstore":["00010000000000","00001014000000",...,"00001001000000"]}}
```

The JSON object only contains the key ''"map"'' with the value of another object.
The JSON object only contains the key ''"tags"'' with the value of another object.
This second object contains all tags as keys and the list of identifier of those zettel with this tag as a value.

Please note that this structure will likely change in the future to be more compliant with other API calls.

Added docs/manual/00001012052600.zettel.



















1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
id: 00001012052600
title: API: List all roles
role: manual
tags: #api #manual #zettelstore
syntax: zmk

To list all [[roles|00001006020100]] used in the Zettelstore just send a HTTP GET request to the [[endpoint|00001012920000]] ''/r''.
If successful, the output is a JSON object:

```sh
# curl http://127.0.0.1:23123/r
{"role-list":["configuration","manual","user","zettel"]}
```

The JSON object only contains the key ''"role-list"'' with the value of a sorted string list.
Each string names one role.

Please note that this structure will likely change in the future to be more compliant with other API calls.

Changes to docs/manual/00001012053200.zettel.

1
2
3
4
5
6

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

6
7
8
9
10
11
12
13





-
+







id: 00001012053200
title: API: Create a new zettel
role: manual
tags: #api #manual #zettelstore
syntax: zmk
modified: 20220628111320
modified: 20211124180030

A zettel is created by adding it to the [[list of zettel|00001012000000#zettel-lists]].
Therefore, the [[endpoint|00001012920000]] to create a new zettel is also ''/j'', but you must send the data of the new zettel via a HTTP POST request.

The body of the POST request must contain a JSON object that specifies metadata and content of the zettel to be created.
The following keys of the JSON object are used:
; ''"meta"''
23
24
25
26
27
28
29
30

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

30
31
32
33
34
35
36
37







-
+







  Typically, text content is not encoded, and binary content is encoded via Base64.

Other keys will be ignored.
Even these three keys are just optional.
The body of the HTTP POST request must not be empty and it must contain a JSON object.

Therefore, a body containing just ''{}'' is perfectly valid.
The new zettel will have no content, and only an identifier as [[metadata|00001006020000]]:
The new zettel will have no content, its title will be set to the value of [[''default-title''|00001004020000#default-title]] (default: ""Untitled""), its role is set to the value of [[''default-role''|00001004020000#default-role]] (default: ""zettel""), and its syntax is set to the value of [[''default-syntax''|00001004020000#default-syntax]] (default: ""zmk"").

```
# curl -X POST --data '{}' http://127.0.0.1:23123/j
{"id":"20210713161000"}
```
If creating the zettel was successful, the HTTP response will contain a JSON object with one key:
; ''"id"''

Changes to docs/manual/00001012053300.zettel.

1
2
3
4
5
6

7
8
9
10
11
12
13
14

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

6
7
8
9
10
11
12
13

14
15
16
17
18
19
20
21





-
+







-
+







id: 00001012053300
title: API: Retrieve metadata and content of an existing zettel
role: manual
tags: #api #manual #zettelstore
syntax: zmk
modified: 20220724163741
modified: 20220201180121

The [[endpoint|00001012920000]] to work with metadata and content of a specific zettel is ''/j/{ID}'', where ''{ID}'' is a placeholder for the [[zettel identifier|00001006050000]].

For example, to retrieve some data about this zettel you are currently viewing, just send a HTTP GET request to the endpoint ''/j/00001012053300''[^If [[authentication is enabled|00001010040100]], you must include the a valid [[access token|00001012050200]] in the ''Authorization'' header].
If successful, the output is a JSON object:
```sh
# curl http://127.0.0.1:23123/j/00001012053300
{"id":"00001012053300","meta":{"title":"API: Retrieve data for an existing zettel","tags":"#api #manual #zettelstore","syntax":"zmk","role":"manual","copyright":"(c) 2020 by Detlef Stern <ds@zettelstore.de>","lang":"en","license":"CC BY-SA 4.0"},"content":"The endpoint to work with a specific zettel is ''/j/{ID}'', where ''{ID}'' is a placeholder for the [[zettel identifier|00001006050000}}.\n\nFor example, ...
{"id":"00001012053300","meta":{"title":"API: Retrieve data for an exisiting zettel","tags":"#api #manual #zettelstore","syntax":"zmk","role":"manual","copyright":"(c) 2020 by Detlef Stern <ds@zettelstore.de>","lang":"en","license":"CC BY-SA 4.0"},"content":"The endpoint to work with a specific zettel is ''/j/{ID}'', where ''{ID}'' is a placeholder for the [[zettel identifier|00001006050000}}.\n\nFor example, ...
```

Pretty-printed, this results in:
```
{
  "id": "00001012053300",
  "meta": {
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
49
50
51
52
53
54
55

56
57
58
59
60
61
62







-







  Other values will result in a HTTP response status code ''400''.
; ''"content"''
: Is a string value that contains the content of the zettel to be created.
  Typically, text content is not encoded, and binary content is encoded via Base64.
; ''"rights"''
: An integer number that describes the [[access rights|00001012921200]] for the zettel.

=== Plain zettel
[!plain]Additionally, you can retrieve the plain zettel, without using JSON.
Just change the [[endpoint|00001012920000]] to ''/z/{ID}''
Optionally, you may provide which parts of the zettel you are requesting.
In this case, add an additional query parameter ''_part=[[PART|00001012920800]]''.
Valid values are ""zettel"", ""[[meta|00001012053400]]"", and ""content"" (the default value).

````sh

Changes to docs/manual/00001012053500.zettel.

1
2
3
4
5
6

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

6
7
8
9
10
11
12
13





-
+







id: 00001012053500
title: API: Retrieve evaluated metadata and content of an existing zettel in various encodings
role: manual
tags: #api #manual #zettelstore
syntax: zmk
modified: 20220410153546
modified: 20220301174012

The [[endpoint|00001012920000]] to work with evaluated metadata and content of a specific zettel is ''/v/{ID}'', where ''{ID}'' is a placeholder for the [[zettel identifier|00001006050000]].

For example, to retrieve some evaluated data about this zettel you are currently viewing, just send a HTTP GET request to the endpoint ''/v/00001012053500''[^If [[authentication is enabled|00001010040100]], you must include the a valid [[access token|00001012050200]] in the ''Authorization'' header].
If successful, the output is a JSON object:
```sh
# curl http://127.0.0.1:23123/v/00001012053500
51
52
53
54
55
56
57


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







+
+













<meta name="zs-backward" content="00001012000000">
<meta name="zs-box-number" content="1">
<meta name="copyright" content="(c) 2020-2021 by Detlef Stern <ds@zettelstore.de>">
<meta name="zs-forward" content="00001010040100 00001012050200 00001012920000 00001012920800">
<meta name="zs-lang" content="en">
<meta name="zs-published" content="00001012053500">
```

The optional query parameter ''_embed'' will embed all images into the returned encoding.

=== HTTP Status codes
; ''200''
: Retrieval was successful, the body contains an appropriate JSON object.
; ''400''
: Request was not valid. 
  There are several reasons for this.
  Maybe the zettel identifier did not consist of exactly 14 digits or ''_enc'' / ''_part'' contained illegal values.
; ''403''
: You are not allowed to retrieve data of the given zettel.
; ''404''
: Zettel not found.
  You probably used a zettel identifier that is not used in the Zettelstore.

Changes to docs/manual/00001012920000.zettel.

1
2
3
4
5
6

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

26
27


28
29
30
31
32
33
34
35
36
37
38
39
40
1
2
3
4
5

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

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





-
+


















-
+


+
+













id: 00001012920000
title: Endpoints used by the API
role: manual
tags: #api #manual #reference #zettelstore
syntax: zmk
modified: 20220627183408
modified: 20220304173423

All API endpoints conform to the pattern ''[PREFIX]LETTER[/ZETTEL-ID]'', where:
; ''PREFIX''
: is the URL prefix (default: ""/""), configured via the ''url-prefix'' [[startup configuration|00001004010000]],
; ''LETTER''
: is a single letter that specifies the resource type,
; ''ZETTEL-ID''
: is an optional 14 digits string that uniquely [[identify a zettel|00001006050000]].

The following letters are currently in use:

|= Letter:| Without zettel identifier | With [[zettel identifier|00001006050000]] | Mnemonic
| ''a'' | POST: [[client authentication|00001012050200]] | | **A**uthenticate
|       | PUT: [[renew access token|00001012050400]] |
| ''j'' | GET: [[list zettel AS JSON|00001012051200]] | GET: [[retrieve zettel AS JSON|00001012053300]] | **J**SON
|       | POST: [[create new zettel|00001012053200]] | PUT: [[update a zettel|00001012054200]]
|       |  | DELETE: [[delete the zettel|00001012054600]]
|       |  | MOVE: [[rename the zettel|00001012054400]]
| ''m'' | GET: [[map metadata values|00001012052400]] | GET: [[retrieve metadata|00001012053400]] | **M**etadata
| ''m'' |  | GET: [[retrieve metadata|00001012053400]] | **M**etadata
| ''o'' |  | GET: [[list zettel order|00001012054000]] | **O**rder
| ''p'' |  | GET: [[retrieve parsed zettel|00001012053600]]| **P**arsed
| ''r'' | GET: [[list roles|00001012052600]] | | **R**oles
| ''t'' | GET: [[list tags|00001012052400]] || **T**ags
| ''u'' |  | GET [[unlinked references|00001012053900]] | **U**nlinked
| ''v'' |  | GET: [[retrieve evaluated zettel|00001012053500]] | E**v**aluated
| ''x'' | GET: [[retrieve administrative data|00001012070500]] | GET: [[list zettel context|00001012053800]] | Conte**x**t
|       | POST: [[execute command|00001012080100]]
| ''z'' | GET: [[list zettel|00001012051200#plain]] | GET: [[retrieve zettel|00001012053300#plain]] | **Z**ettel
|       | POST: [[create new zettel|00001012053200#plain]] | PUT: [[update a zettel|00001012054200#plain]]
|       |  | DELETE: [[delete zettel|00001012054600#plain]]
|       |  | MOVE: [[rename zettel|00001012054400#plain]]

The full URL will contain either the ""http"" oder ""https"" scheme, a host name, and an optional port number.

The API examples will assume the ""http"" schema, the local host ""127.0.0.1"", the default port ""23123"", and the default empty ''PREFIX'' ""/"".
Therefore, all URLs in the API documentation will begin with ""http://127.0.0.1:23123/"".

Changes to docs/manual/00001012920500.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: 00001012920500
title: Encodings available via the [[API|00001012000000]]
role: manual
tags: #api #manual #reference #zettelstore
syntax: zmk
modified: 20220423131535
modified: 20220209114459

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

* [[zjson|00001012920503]] (default)
* [[html|00001012920510]]
* [[sexpr|00001012920516]]
* [[native|00001012920513]]
* [[text|00001012920519]]
* [[zjson|00001012920503]] (default)
* [[zmk|00001012920522]]

Changes to docs/manual/00001012920503.zettel.

1
2
3
4
5
6

7
8
9
10
11

12
13
14
15
16
17
18
1
2
3
4
5

6
7
8
9
10

11
12
13
14
15
16
17
18





-
+




-
+







id: 00001012920503
title: ZJSON Encoding
role: manual
tags: #api #manual #reference #zettelstore
syntax: zmk
modified: 20220422191748
modified: 20220223185826

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 [[endpoint|00001012920000]] ''/j/{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: 
For an example, take a look at the JSON encoding of this page, which is available via the ""Info"" sub-page of this zettel: 

* [[//v/00001012920503?_enc=zjson&_part=zettel]],
* [[//v/00001012920503?_enc=zjson&_part=meta]],
* [[//v/00001012920503?_enc=zjson&_part=content]].

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

Added docs/manual/00001012920513.zettel.














1
2
3
4
5
6
7
8
9
10
11
12
13
+
+
+
+
+
+
+
+
+
+
+
+
+
id: 00001012920513
title: Native Encoding
role: manual
tags: #api #manual #reference #zettelstore
syntax: zmk
modified: 20210726193049

A zettel representation shows the structure of a zettel in a more user-friendly way.
Mostly used for debugging.

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

TODO: formal description

Deleted 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
34
35
36
37
38
39
40
41
42
43
44












































-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
id: 00001012920516
title: Sexpr Encoding
role: manual
tags: #api #manual #reference #zettelstore
syntax: zmk
modified: 20220724170637

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

* [[//v/00001012920516?_enc=sexpr&_part=zettel]],
* [[//v/00001012920516?_enc=sexpr&_part=meta]],
* [[//v/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.

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.
Unicode characters of the following categories contains printable characters in the above sense: letter (L), number (N), punctuation (P), symbol (S).
Symbols are case-insensitive, i.e. ""''ZETTEL''"" and ""''zettel''"" denote the same symbol.

A string starts with a quotation mark (""''"''"", U+0022), contains a possibly empty sequence of Unicode characters, and ends with a quotation mark.
To allow a string to contain a quotations mark, it must be prefixed by one backslash (""''\\''"", U+005C).
To allow a string to contain a backslash, it also must be prefixed by one backslash.
Unicode characters with a code less than U+FF are encoded by by the sequence ""''\\xNM''"", where ''NM'' is the hex encoding of the character.
Unicode characters with a code less than U+FFFF are encoded by by the sequence ""''\\uNMOP''"", where ''NMOP'' is the hex encoding of the character.
Unicode characters with a code less than U+FFFFFF are encoded by by the sequence ""''\\UNMOPQR''"", where ''NMOPQR'' is the hex encoding of the character.
In addition, the sequence ""''\\t''"" encodes a horizontal tab (U+0009), the sequence ""''\\n''"" encodes a line feed (U+000A).

Atoms are separated by Unicode characters of category separator (Z).

Changes to docs/manual/00001012920519.zettel.

1
2
3
4
5
6
7
8
9
10
11

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

11
12
13










-
+


id: 00001012920519
title: Text Encoding
role: manual
tags: #api #manual #reference #zettelstore
syntax: zmk
modified: 20210726193119

A zettel representation contains just all textual data of a zettel.
Could be used for creating a search index.

Every line may contain zero, one, or more words, separated by space character.
Every line may contain zero, one, or more words, spearated by space character.

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

Changes to docs/manual/00001012921000.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: 00001012921000
title: API: JSON structure of an access token
tags: #api #manual #reference #zettelstore
syntax: zmk
role: manual

If the [[authentication process|00001012050200]] was successful, an access token with some additional data is returned.
If the [[authentiction process|00001012050200]] was successful, an access token with some additional data is returned.
The same is true, if the access token was [[renewed|00001012050400]].
The response is structured as an JSON object, with the following named values:

|=Name|Description
|''access_token''|The access token itself, as string value, which is a [[JSON Web Token|https://tools.ietf.org/html/rfc7519]] (JWT, RFC 7915)
|''token_type''|The type of the token, always set to ''"Bearer"'', as described in [[RFC 6750|https://tools.ietf.org/html/rfc6750]]
|''expires_in''|An integer that gives a hint about the lifetime / endurance of the token, measured in seconds

Changes to domain/id/id.go.

1
2

3
4

5
6
7
8
9
10
11
1

2
3

4
5
6
7
8
9
10
11

-
+

-
+







//-----------------------------------------------------------------------------
// Copyright (c) 2020-2022 Detlef Stern
// Copyright (c) 2020-2021 Detlef Stern
//
// This file is part of Zettelstore.
// This file is part of zettelstore.
//
// Zettelstore is licensed 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 provides domain specific types, constants, and functions about
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
44
45
46
47
48
49
50

51
52
53
54
55
56
57







-







	FormTemplateZid    = MustParse(api.ZidFormTemplate)
	RenameTemplateZid  = MustParse(api.ZidRenameTemplate)
	DeleteTemplateZid  = MustParse(api.ZidDeleteTemplate)
	ContextTemplateZid = MustParse(api.ZidContextTemplate)
	RolesTemplateZid   = MustParse(api.ZidRolesTemplate)
	TagsTemplateZid    = MustParse(api.ZidTagsTemplate)
	ErrorTemplateZid   = MustParse(api.ZidErrorTemplate)
	RoleCSSMapZid      = MustParse(api.ZidRoleCSSMap)
	EmojiZid           = MustParse(api.ZidEmoji)
	TOCNewTemplateZid  = MustParse(api.ZidTOCNewTemplate)
	DefaultHomeZid     = MustParse(api.ZidDefaultHome)
)

const maxZid = 99999999999999

Deleted domain/meta/collection.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





































































































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

import "sort"

// Arrangement stores metadata within its categories.
// Typecally a category might be a tag name, a role name, a syntax value.
type Arrangement map[string][]*Meta

// CreateArrangement by inspecting a given key and use the found
// value as a category.
func CreateArrangement(metaList []*Meta, key string) Arrangement {
	if len(metaList) == 0 {
		return nil
	}
	descr := Type(key)
	if descr == nil {
		return nil
	}
	a := make(Arrangement)
	if descr.IsSet {
		for _, m := range metaList {
			if vals, ok := m.GetList(key); ok {
				for _, val := range vals {
					a[val] = append(a[val], m)
				}
			}
		}
	} else {
		for _, m := range metaList {
			if val, ok := m.Get(key); ok && val != "" {
				a[val] = append(a[val], m)
			}
		}
	}
	return a
}

// Counted returns the list of categories, together with the number of
// metadata for each category.
func (a Arrangement) Counted() CountedCategories {
	if len(a) == 0 {
		return nil
	}
	result := make(CountedCategories, 0, len(a))
	for cat, metas := range a {
		result = append(result, CountedCategory{Name: cat, Count: len(metas)})
	}
	return result
}

// CountedCategory contains of a name and the number how much this name occured
// somewhere.
type CountedCategory struct {
	Name  string
	Count int
}

// CountedCategories is the list of CountedCategories.
// Every name must occur only once.
type CountedCategories []CountedCategory

// SortByName sorts the list by the name attribute.
// Since each name must occur only once, two CountedCategories cannot have
// the same name.
func (ccs CountedCategories) SortByName() {
	sort.Slice(ccs, func(i, j int) bool { return ccs[i].Name < ccs[j].Name })
}

// SortByCount sorts the list by the count attribute, descending.
// If two counts are equal, elements are sorted by name.
func (ccs CountedCategories) SortByCount() {
	sort.Slice(ccs, func(i, j int) bool {
		iCount, jCount := ccs[i].Count, ccs[j].Count
		if iCount > jCount {
			return true
		}
		if iCount == jCount {
			return ccs[i].Name < ccs[j].Name
		}
		return false
	})
}

// Categories returns just the category names.
func (ccs CountedCategories) Categories() []string {
	result := make([]string, len(ccs))
	for i, cc := range ccs {
		result[i] = cc.Name
	}
	return result
}

Changes to domain/meta/meta.go.

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

23
24
25
26
27
28
29







-







	"regexp"
	"sort"
	"strings"
	"unicode"
	"unicode/utf8"

	"zettelstore.de/c/api"
	"zettelstore.de/c/maps"
	"zettelstore.de/z/domain/id"
	"zettelstore.de/z/input"
	"zettelstore.de/z/strfun"
)

type keyUsage int

103
104
105
106
107
108
109

110
111
112






113
114
115
116
117
118
119
102
103
104
105
106
107
108
109



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







+
-
-
-
+
+
+
+
+
+







		return *d
	}
	return DescriptionKey{Type: Type(name)}
}

// GetSortedKeyDescriptions delivers all metadata key descriptions as a slice, sorted by name.
func GetSortedKeyDescriptions() []*DescriptionKey {
	names := make([]string, 0, len(registeredKeys))
	keys := maps.Keys(registeredKeys)
	result := make([]*DescriptionKey, 0, len(keys))
	for _, n := range keys {
	for n := range registeredKeys {
		names = append(names, n)
	}
	sort.Strings(names)
	result := make([]*DescriptionKey, 0, len(names))
	for _, n := range names {
		result = append(result, registeredKeys[n])
	}
	return result
}

// Supported keys.
func init() {
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
227
228
229
230
231
232
233










234
235
236
237
238
239
240







-
-
-
-
-
-
-
-
-
-







// Set stores the given string value under the given key.
func (m *Meta) Set(key, value string) {
	if key != api.KeyID {
		m.pairs[key] = trimValue(value)
	}
}

// SetNonEmpty stores the given value under the given key, if the value is non-empty.
// An empty value will delete the previous association.
func (m *Meta) SetNonEmpty(key, value string) {
	if value == "" {
		delete(m.pairs, key)
	} else if key != api.KeyID {
		m.pairs[key] = trimValue(value)
	}
}

func trimValue(value string) string {
	return strings.TrimFunc(value, input.IsSpace)
}

// Get retrieves the string value of a given key. The bool value signals,
// whether there was a value stored or not.
func (m *Meta) Get(key string) (string, bool) {
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
250
251
252
253
254
255
256









257
258
259
260
261
262
263







-
-
-
-
-
-
-
-
-







func (m *Meta) GetDefault(key, def string) string {
	if value, ok := m.Get(key); ok {
		return value
	}
	return def
}

// GetTitle returns the title of the metadata. It is the only key that has a
// defined default value: the string representation of the zettel identifier.
func (m *Meta) GetTitle() string {
	if title, found := m.Get(api.KeyTitle); found {
		return title
	}
	return m.Zid.String()
}

// Pairs returns not computed key/values pairs stored, in a specific order.
// First come the pairs with predefined keys: MetaTitleKey, MetaTagsKey, MetaSyntaxKey,
// MetaContextKey. Then all other pairs are append to the list, ordered by key.
func (m *Meta) Pairs() []Pair {
	return m.doPairs(m.getFirstKeys(), notComputedKey)
}

Changes to domain/meta/meta_test.go.

1
2
3
4

5
6
7
8
9
10
11
1
2
3

4
5
6
7
8
9
10
11



-
+







//-----------------------------------------------------------------------------
// Copyright (c) 2020-2022 Detlef Stern
//
// This file is part of Zettelstore.
// This file is part of zettelstore.
//
// Zettelstore is licensed 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'.
36
37
38
39
40
41
42
43

44
45
46
47

48
49
50
51
52
53
54
36
37
38
39
40
41
42

43
44
45
46

47
48
49
50
51
52
53
54







-
+



-
+







		}
	}
}

func TestTitleHeader(t *testing.T) {
	t.Parallel()
	m := New(testID)
	if got, ok := m.Get(api.KeyTitle); ok && got != "" {
	if got, ok := m.Get(api.KeyTitle); ok || got != "" {
		t.Errorf("Title is not empty, but %q", got)
	}
	addToMeta(m, api.KeyTitle, " ")
	if got, ok := m.Get(api.KeyTitle); ok && got != "" {
	if got, ok := m.Get(api.KeyTitle); ok || got != "" {
		t.Errorf("Title is not empty, but %q", got)
	}
	const st = "A simple text"
	addToMeta(m, api.KeyTitle, " "+st+"  ")
	if got, ok := m.Get(api.KeyTitle); !ok || got != st {
		t.Errorf("Title is not %q, but %q", st, got)
	}

Changes to domain/meta/parse.go.

8
9
10
11
12
13
14

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

19
20
21
22
23
24
25







+



-







// under this license.
//-----------------------------------------------------------------------------

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

import (
	"sort"
	"strings"

	"zettelstore.de/c/api"
	"zettelstore.de/c/maps"
	"zettelstore.de/z/domain/id"
	"zettelstore.de/z/input"
	"zettelstore.de/z/strfun"
)

// NewFromInput parses the meta data of a zettel.
func NewFromInput(zid id.Zid, inp *input.Input) *Meta {
125
126
127
128
129
130
131






132

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

138
139
140
141
142
143
144
145







+
+
+
+
+
+
-
+







	set := make(strfun.Set, len(newElems)+len(oldElems))
	addToSet(set, newElems, useElem)
	if len(set) == 0 {
		// Nothing to add. Maybe because of rejected elements.
		return
	}
	addToSet(set, oldElems, useElem)

	resultList := make([]string, 0, len(set))
	for tag := range set {
		resultList = append(resultList, tag)
	}
	sort.Strings(resultList)
	m.SetList(key, maps.Keys(set))
	m.SetList(key, resultList)
}

func addData(m *Meta, k, v string) {
	if o, ok := m.Get(k); !ok || o == "" {
		m.Set(k, v)
	} else if v != "" {
		m.Set(k, o+" "+v)
149
150
151
152
153
154
155




156
157
158
159
160
161
162
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172







+
+
+
+







	switch key {
	case "", api.KeyID:
		// Empty key and 'id' key will be ignored
		return
	}

	switch Type(key) {
	case TypeString, TypeZettelmarkup:
		if v != "" {
			addData(m, key, v)
		}
	case TypeTagSet:
		addSet(m, key, strings.ToLower(v), func(s string) bool { return s[0] == '#' })
	case TypeWord:
		m.Set(key, strings.ToLower(v))
	case TypeWordSet:
		addSet(m, key, strings.ToLower(v), func(s string) bool { return true })
	case TypeID:

Changes to domain/meta/parse_test.go.

1
2
3
4

5
6
7
8
9
10
11
1
2
3

4
5
6
7
8
9
10
11



-
+







//-----------------------------------------------------------------------------
// Copyright (c) 2020-2022 Detlef Stern
//
// This file is part of Zettelstore.
// This file is part of zettelstore.
//
// Zettelstore is licensed 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 provides tests for the domain specific type 'meta'.
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
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







+
+
+
+
+


















-







	for i, tc := range td {
		m := parseMetaStr(tc.s)
		if got, ok := m.Get(api.KeyTitle); !ok || got != tc.e {
			t.Log(m)
			t.Errorf("TC=%d: expected %q, got %q", i, tc.e, got)
		}
	}

	m := parseMetaStr(api.KeyTitle + ": ")
	if title, ok := m.Get(api.KeyTitle); ok {
		t.Errorf("Expected a missing title key, but got %q (meta=%v)", title, m)
	}
}

func TestNewFromInput(t *testing.T) {
	t.Parallel()
	testcases := []struct {
		input string
		exp   []meta.Pair
	}{
		{"", []meta.Pair{}},
		{" a:b", []meta.Pair{{"a", "b"}}},
		{"%a:b", []meta.Pair{}},
		{"a:b\r\n\r\nc:d", []meta.Pair{{"a", "b"}}},
		{"a:b\r\n%c:d", []meta.Pair{{"a", "b"}}},
		{"% a:b\r\n c:d", []meta.Pair{{"c", "d"}}},
		{"---\r\na:b\r\n", []meta.Pair{{"a", "b"}}},
		{"---\r\na:b\r\n--\r\nc:d", []meta.Pair{{"a", "b"}, {"c", "d"}}},
		{"---\r\na:b\r\n---\r\nc:d", []meta.Pair{{"a", "b"}}},
		{"---\r\na:b\r\n----\r\nc:d", []meta.Pair{{"a", "b"}}},
		{"new-title:\nnew-url:", []meta.Pair{{"new-title", ""}, {"new-url", ""}}},
	}
	for i, tc := range testcases {
		meta := parseMetaStr(tc.input)
		if got := meta.Pairs(); !equalPairs(tc.exp, got) {
			t.Errorf("TC=%d: expected=%v, got=%v", i, tc.exp, got)
		}
	}

Changes to domain/meta/type.go.

14
15
16
17
18
19
20

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







+







import (
	"strconv"
	"strings"
	"sync"
	"time"

	"zettelstore.de/c/api"
	"zettelstore.de/c/zjson"
)

// DescriptionType is a description of a specific key type.
type DescriptionType struct {
	Name  string
	IsSet bool
}
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
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







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







	t := &DescriptionType{name, isSet}
	registeredTypes[name] = t
	return t
}

// Supported key types.
var (
	TypeCredential   = registerType(api.MetaCredential, false)
	TypeEmpty        = registerType(api.MetaEmpty, false)
	TypeID           = registerType(api.MetaID, false)
	TypeIDSet        = registerType(api.MetaIDSet, true)
	TypeNumber       = registerType(api.MetaNumber, false)
	TypeString       = registerType(api.MetaString, false)
	TypeTagSet       = registerType(api.MetaTagSet, true)
	TypeTimestamp    = registerType(api.MetaTimestamp, false)
	TypeURL          = registerType(api.MetaURL, false)
	TypeWord         = registerType(api.MetaWord, false)
	TypeWordSet      = registerType(api.MetaWordSet, true)
	TypeZettelmarkup = registerType(api.MetaZettelmarkup, false)
	TypeCredential   = registerType(zjson.MetaCredential, false)
	TypeEmpty        = registerType(zjson.MetaEmpty, false)
	TypeID           = registerType(zjson.MetaID, false)
	TypeIDSet        = registerType(zjson.MetaIDSet, true)
	TypeNumber       = registerType(zjson.MetaNumber, false)
	TypeString       = registerType(zjson.MetaString, false)
	TypeTagSet       = registerType(zjson.MetaTagSet, true)
	TypeTimestamp    = registerType(zjson.MetaTimestamp, false)
	TypeURL          = registerType(zjson.MetaURL, false)
	TypeWord         = registerType(zjson.MetaWord, false)
	TypeWordSet      = registerType(zjson.MetaWordSet, true)
	TypeZettelmarkup = registerType(zjson.MetaZettelmarkup, false)
)

// Type returns a type hint for the given key. If no type hint is specified,
// TypeUnknown is returned.
func (*Meta) Type(key string) *DescriptionType {
	return Type(key)
}

Changes to encoder/encoder.go.

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







-
-
-
+
+
+




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


-
+



+
+
+
+
+
+
-
+













+
+
+



-
+

	ErrNoWriteMeta    = errors.New("method WriteMeta is not implemented")
	ErrNoWriteContent = errors.New("method WriteContent is not implemented")
	ErrNoWriteBlocks  = errors.New("method WriteBlocks is not implemented")
	ErrNoWriteInlines = errors.New("method WriteInlines is not implemented")
)

// Create builds a new encoder with the given options.
func Create(enc api.EncodingEnum) Encoder {
	if create, ok := registry[enc]; ok {
		return create()
func Create(enc api.EncodingEnum, env *Environment) Encoder {
	if info, ok := registry[enc]; ok {
		return info.Create(env)
	}
	return nil
}

// CreateFunc produces a new encoder.
type CreateFunc func() Encoder

var registry = map[api.EncodingEnum]CreateFunc{}
// Info stores some data about an encoder.
type Info struct {
	Create  func(*Environment) Encoder
	Default bool
}

var registry = map[api.EncodingEnum]Info{}
var defEncoding api.EncodingEnum

// Register the encoder for later retrieval.
func Register(enc api.EncodingEnum, create CreateFunc) {
func Register(enc api.EncodingEnum, info Info) {
	if _, ok := registry[enc]; ok {
		panic(fmt.Sprintf("Encoder %q already registered", enc))
	}
	if info.Default {
		if defEncoding != api.EncoderUnknown && defEncoding != enc {
			panic(fmt.Sprintf("Default encoder already set: %q, new encoding: %q", defEncoding, enc))
		}
		defEncoding = enc
	}
	registry[enc] = create
	registry[enc] = info
}

// GetEncodings returns all registered encodings, ordered by encoding value.
func GetEncodings() []api.EncodingEnum {
	result := make([]api.EncodingEnum, 0, len(registry))
	for enc := range registry {
		result = append(result, enc)
	}
	return result
}

// GetDefaultEncoding returns the encoding that should be used as default.
func GetDefaultEncoding() api.EncodingEnum {
	if defEncoding != api.EncoderUnknown {
		return defEncoding
	}
	if _, ok := registry[api.EncoderZJSON]; ok {
		return api.EncoderZJSON
	}
	panic("No ZJSON encoding registered")
	panic("No default encoding given")
}

Changes to encoder/encoder_blob_test.go.

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







-
-
-
-
-
+
+
+
+
+













			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":"PNG","s":"png","o":"iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAAAAAA6fptVAAAACklEQVR4nGNiAAAABgADNjd8qAAAAABJRU5ErkJggg=="}]`,
			encoderHTML:  `<p><img src="data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAAAAAA6fptVAAAACklEQVR4nGNiAAAABgADNjd8qAAAAABJRU5ErkJggg==" title="PNG"></p>`,
			encoderSexpr: `((BLOB "PNG" "png" "iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAAAAAA6fptVAAAACklEQVR4nGNiAAAABgADNjd8qAAAAABJRU5ErkJggg=="))`,
			encoderText:  "",
			encoderZmk:   `%% Unable to display BLOB with title 'PNG' and syntax 'png'.`,
			encoderZJSON:  `[{"":"BLOB","q":"PNG","s":"png","o":"iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAAAAAA6fptVAAAACklEQVR4nGNiAAAABgADNjd8qAAAAABJRU5ErkJggg=="}]`,
			encoderHTML:   `<img src="data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAAAAAA6fptVAAAACklEQVR4nGNiAAAABgADNjd8qAAAAABJRU5ErkJggg==" title="PNG">`,
			encoderNative: `[BLOB "PNG" "png" "iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAAAAAA6fptVAAAACklEQVR4nGNiAAAABgADNjd8qAAAAABJRU5ErkJggg=="]`,
			encoderText:   "",
			encoderZmk:    `%% Unable to display BLOB with title 'PNG' and syntax 'png'.`,
		},
	},
}

func TestBlob(t *testing.T) {
	m := meta.New(id.Invalid)
	m.Set(api.KeyTitle, "PNG")
	for testNum, tc := range pngTestCases {
		inp := input.NewInput(tc.blob)
		pe := &peBlocks{bs: parser.ParseBlocks(inp, m, "png")}
		checkEncodings(t, testNum, pe, tc.descr, tc.expect, "???")
	}
}

Changes to encoder/encoder_block_test.go.

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





23
24
25
26
27
28
29
30
31
32
33





34
35
36
37
38
39
40
41
42
43
44





45
46
47
48
49
50
51
52
53
54
55





56
57
58
59
60
61
62
63
64
65
66





67
68
69
70

71
72
73
74
75
76
77







78
79
80
81
82


83
84
85


86
87
88


























89
90
91
92

93
94
95
96
97
98
99









100
101
102
103
104
105
106
107
108
109
110





111
112
113
114
115
116
117
118
119
120
121





122
123
124
125
126
127
128
129
130
131
132






133
134
135
136
137
138
139
140
141
142
143






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







-
-
-
-
-
+
+
+
+
+






-
-
-
-
-
+
+
+
+
+






-
-
-
-
-
+
+
+
+
+






-
-
-
-
-
+
+
+
+
+






-
-
-
-
-
+
+
+
+
+



-
+



-
-
-
-
+
+
+
+
+
+
+



-
-
+
+

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



-
+



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






-
-
-
-
-
+
+
+
+
+






-
-
-
-
-
+
+
+
+
+







-
-
-
-
+
+
+
+
+
+







-
-
-
-
+
+
+
+
+
+







-
-
-
-
+
+
+
+
+
+
+














-
-
-
-
-
+
+
+
+
+











-
-
-
-
+
+
+
+
+



-
+


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







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







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










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






-
-
-
-
-
-
-
-
-
-
-



-
-
-
-
-
+
+
+
+
+







package encoder_test

var tcsBlock = []zmkTestCase{
	{
		descr: "Empty Zettelmarkup should produce near nothing",
		zmk:   "",
		expect: expectMap{
			encoderZJSON: `[]`,
			encoderHTML:  "",
			encoderSexpr: `()`,
			encoderText:  "",
			encoderZmk:   useZmk,
			encoderZJSON:  `[]`,
			encoderHTML:   "",
			encoderNative: ``,
			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>",
			encoderSexpr: `((PARA (TEXT "Hello,") (SPACE) (TEXT "world")))`,
			encoderText:  "Hello, world",
			encoderZmk:   useZmk,
			encoderZJSON:  `[{"":"Para","i":[{"":"Text","s":"Hello,"},{"":"Space"},{"":"Text","s":"world"}]}]`,
			encoderHTML:   "<p>Hello, world</p>",
			encoderNative: `[Para Text "Hello,",Space,Text "world"]`,
			encoderText:   "Hello, world",
			encoderZmk:    useZmk,
		},
	},
	{
		descr: "Simple block comment",
		zmk:   "%%%\nNo\nrender\n%%%",
		expect: expectMap{
			encoderZJSON: `[{"":"CommentBlock","s":"No\nrender"}]`,
			encoderHTML:  ``,
			encoderSexpr: `((VERBATIM-COMMENT () "No\nrender"))`,
			encoderText:  ``,
			encoderZmk:   useZmk,
			encoderZJSON:  `[{"":"CommentBlock","s":"No\nrender"}]`,
			encoderHTML:   ``,
			encoderNative: `[CommentBlock "No\nrender"]`,
			encoderText:   ``,
			encoderZmk:    useZmk,
		},
	},
	{
		descr: "Rendered block comment",
		zmk:   "%%%{-}\nRender\n%%%",
		expect: expectMap{
			encoderZJSON: `[{"":"CommentBlock","a":{"-":""},"s":"Render"}]`,
			encoderHTML:  "<!--\nRender\n-->",
			encoderSexpr: `((VERBATIM-COMMENT (("-" "")) "Render"))`,
			encoderText:  ``,
			encoderZmk:   useZmk,
			encoderZJSON:  `[{"":"CommentBlock","a":{"-":""},"s":"Render"}]`,
			encoderHTML:   "<!--\nRender\n-->",
			encoderNative: `[CommentBlock ("",[-]) "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>",
			encoderSexpr: `((HEADING 1 () "top" "top" (TEXT "Top")))`,
			encoderText:  `Top`,
			encoderZmk:   useZmk,
			encoderZJSON:  `[{"":"Heading","n":1,"s":"top","i":[{"":"Text","s":"Top"}]}]`,
			encoderHTML:   "<h2 id=\"top\">Top</h2>",
			encoderNative: `[Heading 1 #top Text "Top"]`,
			encoderText:   `Top`,
			encoderZmk:    useZmk,
		},
	},
	{
		descr: "Simple List",
		descr: "Einfache Liste",
		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>",
			encoderSexpr: `((UNORDERED ((TEXT "A")) ((TEXT "B")) ((TEXT "C"))))`,
			encoderText:  "A\nB\nC",
			encoderZmk:   useZmk,
			encoderHTML:  "<ul>\n<li>A</li>\n<li>B</li>\n<li>C</li>\n</ul>",
			encoderNative: `[BulletList
 [[Para Text "A"]],
 [[Para Text "B"]],
 [[Para Text "C"]]]`,
			encoderText: "A\nB\nC",
			encoderZmk:  useZmk,
		},
	},
	{
		descr: "Nested List",
		zmk:   "* T1\n** T2\n* T3\n** T4\n** T5\n* T6",
		descr: "Schachtelliste",
		zmk:   "* T1\n** T2\n* T3\n** T4\n* T5",
		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>`,
			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"}]}]]}]`,
			encoderHTML: `<ul>
			encoderSexpr: `((UNORDERED ((PARA (TEXT "T1")) (UNORDERED ((TEXT "T2")))) ((PARA (TEXT "T3")) (UNORDERED ((TEXT "T4")) ((TEXT "T5")))) ((PARA (TEXT "T6")))))`,
			encoderText:  "T1\nT2\nT3\nT4\nT5\nT6",
			encoderZmk:   useZmk,
<li>
<p>T1</p>
<ul>
<li>T2</li>
</ul>
</li>
<li>
<p>T3</p>
<ul>
<li>T4</li>
</ul>
</li>
<li>
<p>T5</p>
</li>
</ul>`,
			encoderNative: `[BulletList
 [[Para Text "T1"],
  [BulletList
   [[Para Text "T2"]]]],
 [[Para Text "T3"],
  [BulletList
   [[Para Text "T4"]]]],
 [[Para Text "T5"]]]`,
			encoderText: "T1\nT2\nT3\nT4\nT5",
			encoderZmk:  useZmk,
		},
	},
	{
		descr: "Sequence of two lists",
		descr: "Zwei Listen hintereinander",
		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>",
			encoderSexpr: `((UNORDERED ((TEXT "Item1.1")) ((TEXT "Item1.2")) ((TEXT "Item1.3")) ((TEXT "Item2.1")) ((TEXT "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",
			encoderHTML:  "<ul>\n<li>Item1.1</li>\n<li>Item1.2</li>\n<li>Item1.3</li>\n<li>Item2.1</li>\n<li>Item2.2</li>\n</ul>",
			encoderNative: `[BulletList
 [[Para Text "Item1.1"]],
 [[Para Text "Item1.2"]],
 [[Para Text "Item1.3"]],
 [[Para Text "Item2.1"]],
 [[Para Text "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>",
			encoderSexpr: `((THEMATIC ()))`,
			encoderText:  ``,
			encoderZmk:   useZmk,
			encoderZJSON:  `[{"":"Thematic"}]`,
			encoderHTML:   "<hr>",
			encoderNative: `[Hrule]`,
			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>",
			encoderSexpr: `((PARA (TEXT "Text") (SOFT) (TEXT "*abc")))`,
			encoderText:  `Text *abc`,
			encoderZmk:   useZmk,
			encoderZJSON:  `[{"":"Para","i":[{"":"Text","s":"Text"},{"":"Soft"},{"":"Text","s":"*abc"}]}]`,
			encoderHTML:   "<p>Text\n*abc</p>",
			encoderNative: `[Para Text "Text",Space,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>",
			encoderSexpr: `((PARA (TEXT "Text")) (ORDERED ((TEXT "abc"))))`,
			encoderText:  "Text\nabc",
			encoderZmk:   useZmk,
			encoderHTML:  "<p>Text</p>\n<ol>\n<li>abc</li>\n</ol>",
			encoderNative: `[Para Text "Text"],
[OrderedList
 [[Para Text "abc"]]]`,
			encoderText: "Text\nabc",
			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>",
			encoderSexpr: `((REGION-QUOTE () ((PARA (TEXT "ToBeOrNotToBe"))) ((TEXT "Romeo"))))`,
			encoderText:  "ToBeOrNotToBe\nRomeo",
			encoderZmk:   useZmk,
			encoderHTML:  "<blockquote>\n<p>ToBeOrNotToBe</p>\n<cite>Romeo</cite>\n</blockquote>",
			encoderNative: `[QuoteBlock
 [[Para Text "ToBeOrNotToBe"]],
 [Cite Text "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>",
			encoderSexpr: `((REGION-QUOTE () ((PARA (TEXT "ToBeOr")) (PARA (TEXT "NotToBe"))) ((TEXT "Romeo"))))`,
			encoderText:  "ToBeOr\nNotToBe\nRomeo",
			encoderZmk:   useZmk,
			encoderHTML:  "<blockquote>\n<p>ToBeOr</p>\n<p>NotToBe</p>\n<cite>Romeo</cite>\n</blockquote>",
			encoderNative: `[QuoteBlock
 [[Para Text "ToBeOr"],
  [Para Text "NotToBe"]],
 [Cite Text "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>",
			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\"))))",
			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",
			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>\n<p>A\u00a0line<br>\n\u00a0\u00a0another\u00a0line<br>\nBack</p>\n<p>Paragraph</p>\n<p>\u00a0\u00a0\u00a0\u00a0Spacy\u00a0\u00a0Para</p>\n<cite>Author</cite>\n</div>",
			encoderNative: "[VerseBlock\n [[Para Text \"A\",Space,Text \"line\",Break,Space 2,Text \"another\",Space,Text \"line\",Break,Text \"Back\"],\n  [Para Text \"Paragraph\"],\n  [Para Space 4,Text \"Spacy\",Space 2,Text \"Para\"]],\n [Cite Text \"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>",
			encoderSexpr: `((REGION-BLOCK () ((PARA (TEXT "A") (SPACE) (TEXT "simple") (SOFT) (SPACE) (TEXT "span") (SOFT) (TEXT "and") (SPACE) (TEXT "much") (SPACE) (TEXT "more"))) ()))`,
			encoderText:  `A simple  span and much more`,
			encoderZmk:   useZmk,
			encoderHTML:  "<div>\n<p>A simple\n span\nand much more</p>\n</div>",
			encoderNative: `[SpanBlock
 [[Para Text "A",Space,Text "simple",Space,Space 3,Text "span",Space,Text "and",Space,Text "much",Space,Text "more"]]]`,
			encoderText: `A simple  span and much more`,
			encoderZmk:  useZmk,
		},
	},
	{
		descr: "Simple Verbatim Code",
		descr: "Simple Verbatim",
		zmk:   "```\nHello\nWorld\n```",
		expect: expectMap{
			encoderZJSON: `[{"":"CodeBlock","s":"Hello\nWorld"}]`,
			encoderHTML:  "<pre><code>Hello\nWorld</code></pre>",
			encoderZJSON:  `[{"":"CodeBlock","s":"Hello\nWorld"}]`,
			encoderHTML:   "<pre><code>Hello\nWorld</code></pre>",
			encoderSexpr: `((VERBATIM-CODE () "Hello\nWorld"))`,
			encoderText:  "Hello\nWorld",
			encoderNative: `[CodeBlock "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>",
			encoderSexpr: `((VERBATIM-CODE (("-" "")) "Hello World"))`,
			encoderText:  "Hello World",
			encoderZmk:   useZmk,
			encoderText:   "Hello\nWorld",
			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>",
			encoderSexpr: `((VERBATIM-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>",
			encoderSexpr: `((VERBATIM-MATH () "Hello\n\\LaTeX"))`,
			encoderText:  "Hello\n\\LaTeX",
			encoderZmk:   useZmk,
		},
	},
	{
		descr: "Simple Description List",
		zmk:   "; Zettel\n: Paper\n: Note\n; Zettelkasten\n: Slip box",
		expect: expectMap{
			encoderZJSON: `[{"":"Description","d":[{"i":[{"":"Text","s":"Zettel"}],"e":[[{"":"Para","i":[{"":"Text","s":"Paper"}]}],[{"":"Para","i":[{"":"Text","s":"Note"}]}]]},{"i":[{"":"Text","s":"Zettelkasten"}],"e":[[{"":"Para","i":[{"":"Text","s":"Slip"},{"":"Space"},{"":"Text","s":"box"}]}]]}]}]`,
			encoderHTML:  "<dl><dt>Zettel</dt><dd>Paper</dd><dd>Note</dd><dt>Zettelkasten</dt><dd>Slip box</dd></dl>",
			encoderSexpr: `((DESCRIPTION ((TEXT "Zettel")) (((TEXT "Paper")) ((TEXT "Note"))) ((TEXT "Zettelkasten")) (((TEXT "Slip") (SPACE) (TEXT "box")))))`,
			encoderText:  "Zettel\nPaper\nNote\nZettelkasten\nSlip box",
			encoderZmk:   useZmk,
			encoderHTML:  "<dl>\n<dt>Zettel</dt>\n<dd>Paper</dd>\n<dd>Note</dd>\n<dt>Zettelkasten</dt>\n<dd>Slip box</dd>\n</dl>",
			encoderNative: `[DescriptionList
 [Term [Text "Zettel"],
  [Description
   [Para Text "Paper"]],
  [Description
   [Para Text "Note"]]],
 [Term [Text "Zettelkasten"],
  [Description
   [Para Text "Slip",Space,Text "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>
			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>`,
<tr><td>c1</td><td>c2</td><td>c3</td></tr>
			encoderSexpr: `((TABLE () ((CELL (TEXT "c1")) (CELL (TEXT "c2")) (CELL (TEXT "c3"))) ((CELL (TEXT "d1")) (CELL) (CELL (TEXT "d3")))))`,
			encoderText:  "c1 c2 c3\nd1  d3",
			encoderZmk:   useZmk,
<tr><td>d1</td><td></td><td>d3</td></tr>
</tbody>
</table>`,
			encoderNative: `[Table
 [Row [Cell Default Text "c1"],[Cell Default Text "c2"],[Cell Default Text "c3"]],
 [Row [Cell Default Text "d1"],[Cell Default],[Cell Default Text "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><th class="right">h1</th><th>h2</th><th class="center">h3</th></tr>
</thead>
<tbody>
			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>`,
<tr><td class="left">c1</td><td>c2</td><td class="center">c3</td></tr>
			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")))))`,
			encoderText:  "h1 h2 h3\nc1 c2 c3\nf1 f2 =f3",
<tr><td class="right">f1</td><td>f2</td><td class="center">=f3</td></tr>
</tbody>
</table>`,
			encoderNative: `[Table
 [Header [Cell Right Text "h1"],[Cell Default Text "h2"],[Cell Center Text "h3"]],
 [Row [Cell Left Text "c1"],[Cell Default Text "c2"],[Cell Center Text "c3"]],
 [Row [Cell Right Text "f1"],[Cell Default Text "f2"],[Cell Center Text "=f3"]]]`,
			encoderText: "h1 h2 h3\nc1 c2 c3\nf1 f2 =f3",
			encoderZmk: `|=h1>|=h2|=h3:
|<c1|c2|c3
|f1|f2|=f3`,
		},
	},
	{
		descr: "Simple Endnotes",
		zmk:   `Text[^Footnote]`,
		expect: expectMap{
			encoderZJSON: `[{"":"Para","i":[{"":"Text","s":"Text"},{"":"Footnote","i":[{"":"Text","s":"Footnote"}]}]}]`,
			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>`,
			encoderSexpr: `((PARA (TEXT "Text") (FOOTNOTE () (TEXT "Footnote"))))`,
			encoderText:  "Text Footnote",
			encoderZmk:   useZmk,
		},
	},
	{
		descr: "",
		zmk:   ``,
		expect: expectMap{
			encoderZJSON: `[]`,
			encoderHTML:  ``,
			encoderSexpr: `()`,
			encoderText:  "",
			encoderZmk:   useZmk,
			encoderZJSON:  `[]`,
			encoderHTML:   ``,
			encoderNative: ``,
			encoderText:   "",
			encoderZmk:    useZmk,
		},
	},
}

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

Changes to encoder/encoder_inline_test.go.

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





23
24
25
26
27
28
29
30
31
32
33





34
35
36
37
38
39
40
41
42
43
44





45
46
47
48
49
50
51
52
53
54
55





56
57
58
59
60
61
62
63
64
65
66





67
68
69
70
71
72
73
74
75
76
77





78
79
80
81
82
83
84
85
86
87
88





89
90
91
92
93
94
95
96
97
98
99





100
101
102
103
104
105
106
107
108
109
110





111
112
113
114
115
116
117
118
119
120
121





122
123
124
125
126
127
128
129
130
131
132





133
134
135
136
137
138
139
140
141
142
143





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







-
-
-
-
-
+
+
+
+
+






-
-
-
-
-
+
+
+
+
+






-
-
-
-
-
+
+
+
+
+






-
-
-
-
-
+
+
+
+
+






-
-
-
-
-
+
+
+
+
+






-
-
-
-
-
+
+
+
+
+






-
-
-
-
-
+
+
+
+
+






-
-
-
-
-
+
+
+
+
+






-
-
-
-
-
+
+
+
+
+






-
-
-
-
-
+
+
+
+
+






-
-
-
-
-
+
+
+
+
+






-
-
-
-
-
+
+
+
+
+






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






-
-
-
-
-
+
+
+
+
+






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






-
-
-
-
-
+
+
+
+
+






-
-
-
-
-
+
+
+
+
+






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




-
+

-
-
-
-
-
+
+
+
+
+




-
+

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






-
-
-
-
-
+
+
+
+
+






-
-
-
-
-
+
+
+
+
+






-
-
-
-
-
+
+
+
+
+






-
-
-
-
-
+
+
+
+
+






-
-
-
-
-
+
+
+
+
+






-
-
-
-
-
+
+
+
+
+






-
-
-
-
-
+
+
+
+
+






-
-
-
-
-
+
+
+
+
+






-
-
-
-
-
+
+
+
+
+






-
-
-
-
-
+
+
+
+
+






-
-
-
-
-
+
+
+
+
+






-
-
-
-
-
+
+
+
+
+






-
-
-
-
-
+
+
+
+
+






-
-
-
-
-
+
+
+
+
+






-
-
-
-
-
+
+
+
+
+






-
-
-
-
-
+
+
+
+
+



package encoder_test

var tcsInline = []zmkTestCase{
	{
		descr: "Empty Zettelmarkup should produce near nothing (inline)",
		zmk:   "",
		expect: expectMap{
			encoderZJSON: `[]`,
			encoderHTML:  "",
			encoderSexpr: `()`,
			encoderText:  "",
			encoderZmk:   useZmk,
			encoderZJSON:  `[]`,
			encoderHTML:   "",
			encoderNative: ``,
			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",
			encoderSexpr: `((TEXT "Hello,") (SPACE) (TEXT "world"))`,
			encoderText:  "Hello, world",
			encoderZmk:   useZmk,
			encoderZJSON:  `[{"":"Text","s":"Hello,"},{"":"Space"},{"":"Text","s":"world"}]`,
			encoderHTML:   "Hello, world",
			encoderNative: `Text "Hello,",Space,Text "world"`,
			encoderText:   "Hello, world",
			encoderZmk:    useZmk,
		},
	},
	{
		descr: "Emphasized formatting",
		zmk:   "__emph__",
		expect: expectMap{
			encoderZJSON: `[{"":"Emph","i":[{"":"Text","s":"emph"}]}]`,
			encoderHTML:  "<em>emph</em>",
			encoderSexpr: `((FORMAT-EMPH () (TEXT "emph")))`,
			encoderText:  "emph",
			encoderZmk:   useZmk,
			encoderZJSON:  `[{"":"Emph","i":[{"":"Text","s":"emph"}]}]`,
			encoderHTML:   "<em>emph</em>",
			encoderNative: `Emph [Text "emph"]`,
			encoderText:   "emph",
			encoderZmk:    useZmk,
		},
	},
	{
		descr: "Strong formatting",
		zmk:   "**strong**",
		expect: expectMap{
			encoderZJSON: `[{"":"Strong","i":[{"":"Text","s":"strong"}]}]`,
			encoderHTML:  "<strong>strong</strong>",
			encoderSexpr: `((FORMAT-STRONG () (TEXT "strong")))`,
			encoderText:  "strong",
			encoderZmk:   useZmk,
			encoderZJSON:  `[{"":"Strong","i":[{"":"Text","s":"strong"}]}]`,
			encoderHTML:   "<strong>strong</strong>",
			encoderNative: `Strong [Text "strong"]`,
			encoderText:   "strong",
			encoderZmk:    useZmk,
		},
	},
	{
		descr: "Insert formatting",
		zmk:   ">>insert>>",
		expect: expectMap{
			encoderZJSON: `[{"":"Insert","i":[{"":"Text","s":"insert"}]}]`,
			encoderHTML:  "<ins>insert</ins>",
			encoderSexpr: `((FORMAT-INSERT () (TEXT "insert")))`,
			encoderText:  "insert",
			encoderZmk:   useZmk,
			encoderZJSON:  `[{"":"Insert","i":[{"":"Text","s":"insert"}]}]`,
			encoderHTML:   "<ins>insert</ins>",
			encoderNative: `Insert [Text "insert"]`,
			encoderText:   "insert",
			encoderZmk:    useZmk,
		},
	},
	{
		descr: "Delete formatting",
		zmk:   "~~delete~~",
		expect: expectMap{
			encoderZJSON: `[{"":"Delete","i":[{"":"Text","s":"delete"}]}]`,
			encoderHTML:  "<del>delete</del>",
			encoderSexpr: `((FORMAT-DELETE () (TEXT "delete")))`,
			encoderText:  "delete",
			encoderZmk:   useZmk,
			encoderZJSON:  `[{"":"Delete","i":[{"":"Text","s":"delete"}]}]`,
			encoderHTML:   "<del>delete</del>",
			encoderNative: `Delete [Text "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>",
			encoderSexpr: `((FORMAT-DELETE () (TEXT "old")) (FORMAT-INSERT () (TEXT "new")))`,
			encoderText:  "oldnew",
			encoderZmk:   useZmk,
			encoderZJSON:  `[{"":"Delete","i":[{"":"Text","s":"old"}]},{"":"Insert","i":[{"":"Text","s":"new"}]}]`,
			encoderHTML:   "<del>old</del><ins>new</ins>",
			encoderNative: `Delete [Text "old"],Insert [Text "new"]`,
			encoderText:   "oldnew",
			encoderZmk:    useZmk,
		},
	},
	{
		descr: "Superscript formatting",
		zmk:   "^^superscript^^",
		expect: expectMap{
			encoderZJSON: `[{"":"Super","i":[{"":"Text","s":"superscript"}]}]`,
			encoderHTML:  `<sup>superscript</sup>`,
			encoderSexpr: `((FORMAT-SUPER () (TEXT "superscript")))`,
			encoderText:  `superscript`,
			encoderZmk:   useZmk,
			encoderZJSON:  `[{"":"Super","i":[{"":"Text","s":"superscript"}]}]`,
			encoderHTML:   `<sup>superscript</sup>`,
			encoderNative: `Super [Text "superscript"]`,
			encoderText:   `superscript`,
			encoderZmk:    useZmk,
		},
	},
	{
		descr: "Subscript formatting",
		zmk:   ",,subscript,,",
		expect: expectMap{
			encoderZJSON: `[{"":"Sub","i":[{"":"Text","s":"subscript"}]}]`,
			encoderHTML:  `<sub>subscript</sub>`,
			encoderSexpr: `((FORMAT-SUB () (TEXT "subscript")))`,
			encoderText:  `subscript`,
			encoderZmk:   useZmk,
			encoderZJSON:  `[{"":"Sub","i":[{"":"Text","s":"subscript"}]}]`,
			encoderHTML:   `<sub>subscript</sub>`,
			encoderNative: `Sub [Text "subscript"]`,
			encoderText:   `subscript`,
			encoderZmk:    useZmk,
		},
	},
	{
		descr: "Quotes formatting",
		zmk:   `""quotes""`,
		expect: expectMap{
			encoderZJSON: `[{"":"Quote","i":[{"":"Text","s":"quotes"}]}]`,
			encoderHTML:  "<q>quotes</q>",
			encoderSexpr: `((FORMAT-QUOTE () (TEXT "quotes")))`,
			encoderText:  `quotes`,
			encoderZmk:   useZmk,
			encoderZJSON:  `[{"":"Quote","i":[{"":"Text","s":"quotes"}]}]`,
			encoderHTML:   "<q>quotes</q>",
			encoderNative: `Quote [Text "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:  `<q lang="de">quotes</q>`,
			encoderSexpr: `((FORMAT-QUOTE (("lang" "de")) (TEXT "quotes")))`,
			encoderText:  `quotes`,
			encoderZmk:   `""quotes""{lang="de"}`,
			encoderZJSON:  `[{"":"Quote","a":{"lang":"de"},"i":[{"":"Text","s":"quotes"}]}]`,
			encoderHTML:   `<q lang="de">quotes</q>`,
			encoderNative: `Quote ("",[lang="de"]) [Text "quotes"]`,
			encoderText:   `quotes`,
			encoderZmk:    `""quotes""{lang="de"}`,
		},
	},
	{
		descr: "Span formatting",
		zmk:   `::span::`,
		expect: expectMap{
			encoderZJSON: `[{"":"Span","i":[{"":"Text","s":"span"}]}]`,
			encoderHTML:  `<span>span</span>`,
			encoderSexpr: `((FORMAT-SPAN () (TEXT "span")))`,
			encoderText:  `span`,
			encoderZmk:   useZmk,
			encoderZJSON:  `[{"":"Span","i":[{"":"Text","s":"span"}]}]`,
			encoderHTML:   `<span>span</span>`,
			encoderNative: `Span [Text "span"]`,
			encoderText:   `span`,
			encoderZmk:    useZmk,
		},
	},
	{
		descr: "Code formatting",
		zmk:   "``code``",
		expect: expectMap{
			encoderZJSON: `[{"":"Code","s":"code"}]`,
			encoderHTML:  `<code>code</code>`,
			encoderZJSON:  `[{"":"Code","s":"code"}]`,
			encoderHTML:   `<code>code</code>`,
			encoderSexpr: `((LITERAL-CODE () "code"))`,
			encoderText:  `code`,
			encoderNative: `Code "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>",
			encoderSexpr: `((LITERAL-CODE (("-" "")) "x y"))`,
			encoderText:  `x y`,
			encoderZmk:   useZmk,
			encoderText:   `code`,
			encoderZmk:    useZmk,
		},
	},
	{
		descr: "HTML in Code formatting",
		zmk:   "``<script `` abc",
		expect: expectMap{
			encoderZJSON: `[{"":"Code","s":"<script "},{"":"Space"},{"":"Text","s":"abc"}]`,
			encoderHTML:  "<code>&lt;script\u00a0</code> abc",
			encoderSexpr: `((LITERAL-CODE () "<script ") (SPACE) (TEXT "abc"))`,
			encoderText:  `<script  abc`,
			encoderZmk:   useZmk,
		},
	},
	{
		descr: "Input formatting",
		zmk:   `''input''`,
		expect: expectMap{
			encoderZJSON: `[{"":"Input","s":"input"}]`,
			encoderHTML:  `<kbd>input</kbd>`,
			encoderSexpr: `((LITERAL-INPUT () "input"))`,
			encoderText:  `input`,
			encoderZmk:   useZmk,
			encoderZJSON:  `[{"":"Input","s":"input"}]`,
			encoderHTML:   `<kbd>input</kbd>`,
			encoderNative: `Input "input"`,
			encoderText:   `input`,
			encoderZmk:    useZmk,
		},
	},
	{
		descr: "Output formatting",
		zmk:   `==output==`,
		expect: expectMap{
			encoderZJSON: `[{"":"Output","s":"output"}]`,
			encoderHTML:  `<samp>output</samp>`,
			encoderZJSON:  `[{"":"Output","s":"output"}]`,
			encoderHTML:   `<samp>output</samp>`,
			encoderSexpr: `((LITERAL-OUTPUT () "output"))`,
			encoderText:  `output`,
			encoderZmk:   useZmk,
			encoderNative: `Output "output"`,
		},
	},
	{
		descr: "Math formatting",
		zmk:   `$$\TeX$$`,
		expect: expectMap{
			encoderZJSON: `[{"":"Math","s":"\\TeX"}]`,
			encoderHTML:  `<code class="zs-math">\TeX</code>`,
			encoderSexpr: `((LITERAL-MATH () "\\TeX"))`,
			encoderText:  `\TeX`,
			encoderZmk:   useZmk,
			encoderText:   `output`,
			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>`,
			encoderSexpr: `((FORMAT-SPAN (("lang" "fr")) (FORMAT-QUOTE () (TEXT "abc"))))`,
			encoderText:  `abc`,
			encoderZmk:   `::""abc""::{lang="fr"}`,
			encoderZJSON:  `[{"":"Span","a":{"lang":"fr"},"i":[{"":"Quote","i":[{"":"Text","s":"abc"}]}]}]`,
			encoderHTML:   `<span lang="fr"><q>abc</q></span>`,
			encoderNative: `Span ("",[lang="fr"]) [Quote [Text "abc"]]`,
			encoderText:   `abc`,
			encoderZmk:    `::""abc""::{lang="fr"}`,
		},
	},
	{
		descr: "Simple Citation",
		zmk:   `[@Stern18]`,
		expect: expectMap{
			encoderZJSON: `[{"":"Cite","s":"Stern18"}]`,
			encoderHTML:  `<span>Stern18</span>`, // TODO
			encoderSexpr: `((CITE () "Stern18"))`,
			encoderText:  ``,
			encoderZmk:   useZmk,
			encoderZJSON:  `[{"":"Cite","s":"Stern18"}]`,
			encoderHTML:   `Stern18`, // TODO
			encoderNative: `Cite "Stern18"`,
			encoderText:   ``,
			encoderZmk:    useZmk,
		},
	},
	{
		descr: "No comment",
		zmk:   `% comment`,
		expect: expectMap{
			encoderZJSON: `[{"":"Text","s":"%"},{"":"Space"},{"":"Text","s":"comment"}]`,
			encoderZJSON:  `[{"":"Text","s":"%"},{"":"Space"},{"":"Text","s":"comment"}]`,
			encoderHTML:  `% comment`,
			encoderSexpr: `((TEXT "%") (SPACE) (TEXT "comment"))`,
			encoderText:  `% comment`,
			encoderZmk:   useZmk,
		},
	},
	{
		descr: "Line comment (nogen HTML)",
		zmk:   `%% line comment`,
		expect: expectMap{
			encoderZJSON: `[{"":"Comment","s":"line comment"}]`,
			encoderHTML:  ``,
			encoderSexpr: `((LITERAL-COMMENT () "line comment"))`,
			encoderText:  ``,
			encoderZmk:   useZmk,
			encoderHTML:   `% comment`,
			encoderNative: `Text "%",Space,Text "comment"`,
			encoderText:   `% comment`,
			encoderZmk:    useZmk,
		},
	},
	{
		descr: "Line comment",
		zmk:   `%%{-} line comment`,
		zmk:   `%% line comment`,
		expect: expectMap{
			encoderZJSON: `[{"":"Comment","a":{"-":""},"s":"line comment"}]`,
			encoderHTML:  `<!-- line comment -->`,
			encoderSexpr: `((LITERAL-COMMENT (("-" "")) "line comment"))`,
			encoderText:  ``,
			encoderZmk:   useZmk,
			encoderZJSON:  `[{"":"Comment","s":"line comment"}]`,
			encoderHTML:   `<!-- line comment -->`,
			encoderNative: `Comment "line comment"`,
			encoderText:   ``,
			encoderZmk:    useZmk,
		},
	},
	{
		descr: "Comment after text",
		zmk:   `Text %%{-} comment`,
		zmk:   `Text %% comment`,
		expect: expectMap{
			encoderZJSON: `[{"":"Text","s":"Text"},{"":"Comment","a":{"-":""},"s":"comment"}]`,
			encoderHTML:  `Text<!-- comment -->`,
			encoderZJSON:  `[{"":"Text","s":"Text"},{"":"Comment","s":"comment"}]`,
			encoderHTML:   `Text <!-- comment -->`,
			encoderSexpr: `((TEXT "Text") (LITERAL-COMMENT (("-" "")) "comment"))`,
			encoderText:  `Text`,
			encoderNative: `Text "Text",Comment "comment"`,
			encoderZmk:   useZmk,
		},
	},
	{
		descr: "Comment after text and with -->",
		zmk:   `Text %%{-} comment --> end`,
		expect: expectMap{
			encoderZJSON: `[{"":"Text","s":"Text"},{"":"Comment","a":{"-":""},"s":"comment --> end"}]`,
			encoderHTML:  `Text<!-- comment --&gt; end -->`,
			encoderSexpr: `((TEXT "Text") (LITERAL-COMMENT (("-" "")) "comment --> end"))`,
			encoderText:  `Text`,
			encoderZmk:   useZmk,
			encoderText:   `Text`,
			encoderZmk:    useZmk,
		},
	},
	{
		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>`,
			encoderSexpr: `((FOOTNOTE () (TEXT "footnote")))`,
			encoderText:  `footnote`,
			encoderZmk:   useZmk,
			encoderZJSON:  `[{"":"Footnote","i":[{"":"Text","s":"footnote"}]}]`,
			encoderHTML:   `<sup id="fnref:0"><a href="#fn:0" class="footnote-ref" role="doc-noteref">0</a></sup>`,
			encoderNative: `Footnote [Text "footnote"]`,
			encoderText:   `footnote`,
			encoderZmk:    useZmk,
		},
	},
	{
		descr: "Simple mark",
		zmk:   `[!mark]`,
		expect: expectMap{
			encoderZJSON: `[{"":"Mark","s":"mark","q":"mark"}]`,
			encoderHTML:  `<a id="mark"></a>`,
			encoderSexpr: `((MARK "mark" "mark" "mark"))`,
			encoderText:  ``,
			encoderZmk:   useZmk,
			encoderZJSON:  `[{"":"Mark","s":"mark","q":"mark"}]`,
			encoderHTML:   `<a id="mark"></a>`,
			encoderNative: `Mark "mark" #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>`,
			encoderSexpr: `((MARK "mark" "mark" "mark" (TEXT "with") (SPACE) (TEXT "text")))`,
			encoderText:  `with text`,
			encoderZmk:   useZmk,
			encoderZJSON:  `[{"":"Mark","s":"mark","q":"mark","i":[{"":"Text","s":"with"},{"":"Space"},{"":"Text","s":"text"}]}]`,
			encoderHTML:   `<a id="mark">with text</a>`,
			encoderNative: `Mark "mark" #mark [Text "with",Space,Text "text"]`,
			encoderText:   `with text`,
			encoderZmk:    useZmk,
		},
	},
	{
		descr: "Dummy Link",
		zmk:   `[[abc]]`,
		expect: expectMap{
			encoderZJSON: `[{"":"Link","q":"external","s":"abc"}]`,
			encoderHTML:  `<a class="external" href="abc">abc</a>`,
			encoderSexpr: `((LINK-EXTERNAL () "abc"))`,
			encoderText:  ``,
			encoderZmk:   useZmk,
			encoderZJSON:  `[{"":"Link","q":"external","s":"abc"}]`,
			encoderHTML:   `<a href="abc" class="external">abc</a>`,
			encoderNative: `Link EXTERNAL "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>`,
			encoderSexpr: `((LINK-EXTERNAL () "https://zettelstore.de"))`,
			encoderText:  ``,
			encoderZmk:   useZmk,
			encoderZJSON:  `[{"":"Link","q":"external","s":"https://zettelstore.de"}]`,
			encoderHTML:   `<a href="https://zettelstore.de" class="external">https://zettelstore.de</a>`,
			encoderNative: `Link EXTERNAL "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>`,
			encoderSexpr: `((LINK-EXTERNAL () "https://zettelstore.de" (TEXT "Home")))`,
			encoderText:  `Home`,
			encoderZmk:   useZmk,
			encoderZJSON:  `[{"":"Link","q":"external","s":"https://zettelstore.de","i":[{"":"Text","s":"Home"}]}]`,
			encoderHTML:   `<a href="https://zettelstore.de" class="external">Home</a>`,
			encoderNative: `Link EXTERNAL "https://zettelstore.de" [Text "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>`,
			encoderSexpr: `((LINK-ZETTEL () "00000000000100"))`,
			encoderText:  ``,
			encoderZmk:   useZmk,
			encoderZJSON:  `[{"":"Link","q":"zettel","s":"00000000000100"}]`,
			encoderHTML:   `<a href="00000000000100">00000000000100</a>`,
			encoderNative: `Link ZETTEL "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>`,
			encoderSexpr: `((LINK-ZETTEL () "00000000000100" (TEXT "Config")))`,
			encoderText:  `Config`,
			encoderZmk:   useZmk,
			encoderZJSON:  `[{"":"Link","q":"zettel","s":"00000000000100","i":[{"":"Text","s":"Config"}]}]`,
			encoderHTML:   `<a href="00000000000100">Config</a>`,
			encoderNative: `Link ZETTEL "00000000000100" [Text "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>`,
			encoderSexpr: `((LINK-ZETTEL () "00000000000100#frag"))`,
			encoderText:  ``,
			encoderZmk:   useZmk,
			encoderZJSON:  `[{"":"Link","q":"zettel","s":"00000000000100#frag"}]`,
			encoderHTML:   `<a href="00000000000100#frag">00000000000100#frag</a>`,
			encoderNative: `Link ZETTEL "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>`,
			encoderSexpr: `((LINK-ZETTEL () "00000000000100#frag" (TEXT "Config")))`,
			encoderText:  `Config`,
			encoderZmk:   useZmk,
			encoderZJSON:  `[{"":"Link","q":"zettel","s":"00000000000100#frag","i":[{"":"Text","s":"Config"}]}]`,
			encoderHTML:   `<a href="00000000000100#frag">Config</a>`,
			encoderNative: `Link ZETTEL "00000000000100#frag" [Text "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>`,
			encoderSexpr: `((LINK-SELF () "#frag"))`,
			encoderText:  ``,
			encoderZmk:   useZmk,
			encoderZJSON:  `[{"":"Link","q":"self","s":"#frag"}]`,
			encoderHTML:   `<a href="#frag">#frag</a>`,
			encoderNative: `Link SELF "#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>`,
			encoderSexpr: `((LINK-HOSTED () "/hosted" (TEXT "H")))`,
			encoderText:  `H`,
			encoderZmk:   useZmk,
			encoderZJSON:  `[{"":"Link","q":"local","s":"/hosted","i":[{"":"Text","s":"H"}]}]`,
			encoderHTML:   `<a href="/hosted">H</a>`,
			encoderNative: `Link LOCAL "/hosted" [Text "H"]`,
			encoderText:   `H`,
			encoderZmk:    useZmk,
		},
	},
	{
		descr: "Based link",
		zmk:   `[[B|/based]]`,
		expect: expectMap{
			encoderZJSON: `[{"":"Link","q":"local","s":"/based","i":[{"":"Text","s":"B"}]}]`,
			encoderHTML:  `<a href="/based">B</a>`,
			encoderSexpr: `((LINK-HOSTED () "/based" (TEXT "B")))`,
			encoderText:  `B`,
			encoderZmk:   useZmk,
			encoderZJSON:  `[{"":"Link","q":"local","s":"/based","i":[{"":"Text","s":"B"}]}]`,
			encoderHTML:   `<a href="/based">B</a>`,
			encoderNative: `Link LOCAL "/based" [Text "B"]`,
			encoderText:   `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>`,
			encoderSexpr: `((LINK-HOSTED () "../relative" (TEXT "R")))`,
			encoderText:  `R`,
			encoderZmk:   useZmk,
			encoderZJSON:  `[{"":"Link","q":"local","s":"../relative","i":[{"":"Text","s":"R"}]}]`,
			encoderHTML:   `<a href="../relative">R</a>`,
			encoderNative: `Link LOCAL "../relative" [Text "R"]`,
			encoderText:   `R`,
			encoderZmk:    useZmk,
		},
	},
	{
		descr: "Dummy Embed",
		zmk:   `{{abc}}`,
		expect: expectMap{
			encoderZJSON: `[{"":"Embed","s":"abc"}]`,
			encoderHTML:  `<img src="abc">`,
			encoderSexpr: `((EMBED () (EXTERNAL "abc") ""))`,
			encoderText:  ``,
			encoderZmk:   useZmk,
			encoderZJSON:  `[{"":"Embed","s":"abc"}]`,
			encoderHTML:   `<img src="abc" alt="">`,
			encoderNative: `Embed EXTERNAL "abc"`,
			encoderText:   ``,
			encoderZmk:    useZmk,
		},
	},
	{
		descr: "",
		zmk:   ``,
		expect: expectMap{
			encoderZJSON: `[]`,
			encoderHTML:  ``,
			encoderSexpr: `()`,
			encoderText:  ``,
			encoderZmk:   useZmk,
			encoderZJSON:  `[]`,
			encoderHTML:   ``,
			encoderNative: ``,
			encoderText:   ``,
			encoderZmk:    useZmk,
		},
	},
}

Changes to encoder/encoder_test.go.

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





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

45
46
47
48
49
50





51
52
53
54
55
56
57
58
59
60

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

77
78

79
80
81

82
83
84

85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
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







-

-





-
-
-
-
-
+
+
+
+
+














+

-
-
-
-
-
+
+
+
+
+










+











-




+

-
+


-
+


-
+













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







package encoder_test

import (
	"bytes"
	"fmt"
	"testing"

	"codeberg.org/t73fde/sxpf"
	"zettelstore.de/c/api"
	"zettelstore.de/c/sexpr"
	"zettelstore.de/z/ast"
	"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/sexprenc" // Allow to use sexpr 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/encoder/htmlenc"   // Allow to use HTML encoder.
	_ "zettelstore.de/z/encoder/nativeenc" // Allow to use native 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
	encoderSexpr = api.EncoderSexpr
	encoderText  = api.EncoderText
	encoderZmk   = api.EncoderZmk
	encoderZJSON  = api.EncoderZJSON
	encoderHTML   = api.EncoderHTML
	encoderNative = api.EncoderNative
	encoderText   = api.EncoderText
	encoderZmk    = api.EncoderZmk
)

func TestEncoder(t *testing.T) {
	for i := range tcsInline {
		tcsInline[i].inline = true
	}
	executeTestCases(t, append(tcsBlock, tcsInline...))
}

func executeTestCases(t *testing.T, testCases []zmkTestCase) {
	t.Helper()
	for testNum, tc := range testCases {
		inp := input.NewInput([]byte(tc.zmk))
		var pe parserEncoder
		if tc.inline {
			is := parser.ParseInlines(inp, api.ValueSyntaxZmk)
			cleaner.CleanInlineSlice(&is)
			pe = &peInlines{is: is}
		} else {
			pe = &peBlocks{bs: parser.ParseBlocks(inp, nil, api.ValueSyntaxZmk)}
		}
		checkEncodings(t, testNum, pe, tc.descr, tc.expect, tc.zmk)
		checkSexpr(t, testNum, pe, tc.descr)
	}
}

func checkEncodings(t *testing.T, testNum int, pe parserEncoder, descr string, expected expectMap, zmkDefault string) {
	t.Helper()
	for enc, exp := range expected {
		encdr := encoder.Create(enc)
		encdr := encoder.Create(enc, nil)
		got, err := pe.encode(encdr)
		if err != nil {
			t.Error("pe.encode:", err)
			t.Error(err)
			continue
		}
		if enc == api.EncoderZmk && exp == useZmk {
		if enc == api.EncoderZmk && exp == "\000" {
			exp = zmkDefault
		}
		if got != exp {
			prefix := fmt.Sprintf("Test #%d", testNum)
			if d := descr; d != "" {
				prefix += "\nReason:   " + d
			}
			prefix += "\nMode:     " + pe.mode()
			t.Errorf("%s\nEncoder:  %s\nExpected: %q\nGot:      %q", prefix, enc, exp, got)
		}
	}
}

func checkSexpr(t *testing.T, testNum int, pe parserEncoder, descr string) {
	t.Helper()
	encdr := encoder.Create(encoderSexpr)
	exp, err := pe.encode(encdr)
	if err != nil {
		t.Error(err)
		return
	}
	val, err := sxpf.ParseString(sexpr.Smk, exp)
	if err != nil {
		t.Error(err)
		return
	}
	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

Added encoder/env.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
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
//-----------------------------------------------------------------------------
// 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

import (
	"zettelstore.de/z/ast"
	"zettelstore.de/z/strfun"
)

// Environment specifies all data and functions that affects encoding.
type Environment struct {
	// Important for HTML encoder
	Lang           string // default language
	Interactive    bool   // Encoded data will be placed in interactive content
	Xhtml          bool   // use XHTML syntax instead of HTML syntax
	MarkerExternal string // Marker after link to (external) material.
	NewWindow      bool   // open link in new window
	IgnoreMeta     strfun.Set
	footnotes      []footnoteInfo // Stores footnotes detected while encoding
	footnoteNum    int
}

// IsInteractive returns true, if Interactive is enabled and currently embedded
// interactive encoding will take place.
func (env *Environment) IsInteractive(inInteractive bool) bool {
	return inInteractive && env != nil && env.Interactive
}

// IsXHTML return true, if XHTML is enabled.
func (env *Environment) IsXHTML() bool {
	return env != nil && env.Xhtml
}

// HasNewWindow retruns true, if a new browser windows should be opened.
func (env *Environment) HasNewWindow() bool {
	return env != nil && env.NewWindow
}

type footnoteInfo struct {
	fn  *ast.FootnoteNode
	num int
}

// AddFootnote adds a footnote node to the environment and returns the number of that footnote.
func (env *Environment) AddFootnote(fn *ast.FootnoteNode) int {
	if env == nil {
		return 0
	}
	env.footnoteNum++
	env.footnotes = append(env.footnotes, footnoteInfo{fn: fn, num: env.footnoteNum})
	return env.footnoteNum
}

// PopFootnote returns the next footnote and removes it from the list.
func (env *Environment) PopFootnote() (*ast.FootnoteNode, int) {
	if env == nil {
		return nil, -1
	}
	if len(env.footnotes) == 0 {
		env.footnotes = nil
		env.footnoteNum = 0
		return nil, -1
	}
	fni := env.footnotes[0]
	env.footnotes = env.footnotes[1:]
	return fni.fn, fni.num
}

Added encoder/htmlenc/block.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
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
//-----------------------------------------------------------------------------
// 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 htmlenc encodes the abstract syntax tree into HTML5.
package htmlenc

import (
	"fmt"
	"strconv"
	"strings"

	"zettelstore.de/c/api"
	"zettelstore.de/c/html"
	"zettelstore.de/c/zjson"
	"zettelstore.de/z/ast"
	"zettelstore.de/z/strfun"
)

func (v *visitor) visitVerbatim(vn *ast.VerbatimNode) {
	switch vn.Kind {
	case ast.VerbatimZettel:
		v.b.WriteString("<!--\n")
		v.visitAttributes(vn.Attrs)
		v.writeHTMLEscaped(string(vn.Content))
		v.b.WriteString("\n-->")
	case ast.VerbatimProg:
		oldVisible := v.visibleSpace
		if vn.Attrs != nil {
			v.visibleSpace = vn.Attrs.HasDefault()
		}
		v.b.WriteString("<pre><code")
		v.visitAttributes(vn.Attrs)
		v.b.WriteByte('>')
		v.writeHTMLEscaped(string(vn.Content))
		v.b.WriteString("</code></pre>")
		v.visibleSpace = oldVisible

	case ast.VerbatimComment:
		if vn.Attrs.HasDefault() {
			v.b.WriteString("<!--\n")
			v.writeHTMLEscaped(string(vn.Content))
			v.b.WriteString("\n-->")
		}

	case ast.VerbatimHTML:
		if html.IsSave(string(vn.Content)) {
			v.b.Write(vn.Content)
		}
	default:
		panic(fmt.Sprintf("Unknown verbatim kind %v", vn.Kind))
	}
}

var specialSpanAttr = strfun.NewSet("example", "note", "tip", "important", "caution", "warning")

func processSpanAttributes(attrs zjson.Attributes) zjson.Attributes {
	if attrVal, ok := attrs.Get(""); ok {
		attrVal = strings.ToLower(attrVal)
		if specialSpanAttr.Has(attrVal) {
			attrs = attrs.Clone().Remove("").AddClass("zs-indication").AddClass("zs-" + attrVal)
		}
	}
	return attrs
}

func (v *visitor) visitRegion(rn *ast.RegionNode) {
	var code string
	attrs := rn.Attrs
	oldVerse := v.inVerse
	switch rn.Kind {
	case ast.RegionSpan:
		code = "div"
		attrs = processSpanAttributes(attrs)
	case ast.RegionVerse:
		v.inVerse = true
		code = "div"
	case ast.RegionQuote:
		code = "blockquote"
	default:
		panic(fmt.Sprintf("Unknown region kind %v", rn.Kind))
	}

	v.b.WriteStrings("<", code)
	v.visitAttributes(attrs)
	v.b.WriteString(">\n")
	ast.Walk(v, &rn.Blocks)
	if len(rn.Inlines) > 0 {
		v.b.WriteString("\n<cite>")
		ast.Walk(v, &rn.Inlines)
		v.b.WriteString("</cite>")
	}
	v.b.WriteStrings("\n</", code, ">")
	v.inVerse = oldVerse
}

func (v *visitor) visitHeading(hn *ast.HeadingNode) {
	lvl := hn.Level + 1
	if lvl > 6 {
		lvl = 6 // HTML has H1..H6
	}
	strLvl := strconv.Itoa(lvl)
	v.b.WriteStrings("<h", strLvl)
	v.visitAttributes(hn.Attrs)
	if _, ok := hn.Attrs.Get("id"); !ok {
		if fragment := hn.Fragment; fragment != "" {
			v.b.WriteStrings(" id=\"", fragment, "\"")
		}
	}
	v.b.WriteByte('>')
	ast.Walk(v, &hn.Inlines)
	v.b.WriteStrings("</h", strLvl, ">")
}

var mapNestedListKind = map[ast.NestedListKind]string{
	ast.NestedListOrdered:   "ol",
	ast.NestedListUnordered: "ul",
}

func (v *visitor) visitNestedList(ln *ast.NestedListNode) {
	if ln.Kind == ast.NestedListQuote {
		// NestedListQuote -> HTML <blockquote> doesn't use <li>...</li>
		v.writeQuotationList(ln)
		return
	}

	code, ok := mapNestedListKind[ln.Kind]
	if !ok {
		panic(fmt.Sprintf("Invalid list kind %v", ln.Kind))
	}

	compact := isCompactList(ln.Items)
	v.b.WriteStrings("<", code)
	v.visitAttributes(ln.Attrs)
	v.b.WriteString(">\n")
	for _, item := range ln.Items {
		v.b.WriteString("<li>")
		v.writeItemSliceOrPara(item, compact)
		v.b.WriteString("</li>\n")
	}
	v.b.WriteStrings("</", code, ">")
}

func (v *visitor) writeQuotationList(ln *ast.NestedListNode) {
	v.b.WriteString("<blockquote>\n")
	inPara := false
	for _, item := range ln.Items {
		if pn := getParaItem(item); pn != nil {
			if inPara {
				v.b.WriteByte('\n')
			} else {
				v.b.WriteString("<p>")
				inPara = true
			}
			ast.Walk(v, &pn.Inlines)
		} else {
			if inPara {
				v.writeEndPara()
				inPara = false
			}
			ast.WalkItemSlice(v, item)
		}
	}
	if inPara {
		v.writeEndPara()
	}
	v.b.WriteString("</blockquote>\n")
}

func getParaItem(its ast.ItemSlice) *ast.ParaNode {
	if len(its) != 1 {
		return nil
	}
	if pn, ok := its[0].(*ast.ParaNode); ok {
		return pn
	}
	return nil
}

func isCompactList(insl []ast.ItemSlice) bool {
	for _, ins := range insl {
		if !isCompactSlice(ins) {
			return false
		}
	}
	return true
}

func isCompactSlice(ins ast.ItemSlice) bool {
	if len(ins) < 1 {
		return true
	}
	if len(ins) == 1 {
		switch ins[0].(type) {
		case *ast.ParaNode, *ast.VerbatimNode, *ast.HRuleNode:
			return true
		case *ast.NestedListNode:
			return false
		}
	}
	return false
}

// writeItemSliceOrPara emits the content of a paragraph if the paragraph is
// the only element of the block slice and if compact mode is true. Otherwise,
// the item slice is emitted normally.
func (v *visitor) writeItemSliceOrPara(ins ast.ItemSlice, compact bool) {
	if compact && len(ins) == 1 {
		if para, ok := ins[0].(*ast.ParaNode); ok {
			ast.Walk(v, &para.Inlines)
			return
		}
	}
	for i, in := range ins {
		if i >= 0 {
			v.b.WriteByte('\n')
		}
		ast.Walk(v, in)
	}
	v.b.WriteByte('\n')
}

func (v *visitor) writeDescriptionsSlice(ds ast.DescriptionSlice) {
	if len(ds) == 1 {
		if para, ok := ds[0].(*ast.ParaNode); ok {
			ast.Walk(v, &para.Inlines)
			return
		}
	}
	ast.WalkDescriptionSlice(v, ds)
}

func (v *visitor) visitDescriptionList(dn *ast.DescriptionListNode) {
	v.b.WriteString("<dl>\n")
	for _, descr := range dn.Descriptions {
		v.b.WriteString("<dt>")
		ast.Walk(v, &descr.Term)
		v.b.WriteString("</dt>\n")

		for _, b := range descr.Descriptions {
			v.b.WriteString("<dd>")
			v.writeDescriptionsSlice(b)
			v.b.WriteString("</dd>\n")
		}
	}
	v.b.WriteString("</dl>")
}

func (v *visitor) visitTable(tn *ast.TableNode) {
	v.b.WriteString("<table>\n")
	if len(tn.Header) > 0 {
		v.b.WriteString("<thead>\n")
		v.writeRow(tn.Header, "<th", "</th>")
		v.b.WriteString("</thead>\n")
	}
	if len(tn.Rows) > 0 {
		v.b.WriteString("<tbody>\n")
		for _, row := range tn.Rows {
			v.writeRow(row, "<td", "</td>")
		}
		v.b.WriteString("</tbody>\n")
	}
	v.b.WriteString("</table>")
}

var alignStyle = map[ast.Alignment]string{
	ast.AlignDefault: ">",
	ast.AlignLeft:    " class=\"left\">",
	ast.AlignCenter:  " class=\"center\">",
	ast.AlignRight:   " class=\"right\">",
}

func (v *visitor) writeRow(row ast.TableRow, cellStart, cellEnd string) {
	v.b.WriteString("<tr>")
	for _, cell := range row {
		v.b.WriteString(cellStart)
		if len(cell.Inlines) == 0 {
			v.b.WriteByte('>')
		} else {
			v.b.WriteString(alignStyle[cell.Align])
			ast.Walk(v, &cell.Inlines)
		}
		v.b.WriteString(cellEnd)
	}
	v.b.WriteString("</tr>\n")
}

func (v *visitor) visitBLOB(bn *ast.BLOBNode) {
	switch bn.Syntax {
	case api.ValueSyntaxSVG:
		v.b.Write(bn.Blob)
	case api.ValueSyntaxGif, "jpeg", "png":
		v.b.WriteStrings("<img src=\"data:image/", bn.Syntax, ";base64,")
		v.b.WriteBase64(bn.Blob)
		v.b.WriteString("\" title=\"")
		v.writeQuotedEscaped(bn.Title)
		v.b.WriteString("\">")
	default:
		v.b.WriteStrings("<p class=\"zs-error\">Unable to display BLOB with syntax '", bn.Syntax, "'.</p>")
	}
}

func (v *visitor) writeEndPara() {
	v.b.WriteString("</p>")
}

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






-
+








-
+




-

-

-



-
-



-
+
+
+



-
-
-
+
-
-




+
+
+
+
+
+
+
+
+
-
+


-
+
-
-
-

-
-
-
+
+
-


-
-
+
+
-
-
-
+


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




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







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



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

-
-
+
-
-
+
-
-
+

-
-
-
-
-
//-----------------------------------------------------------------------------
// Copyright (c) 2022 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 htmlenc encodes the abstract syntax tree into HTML5 via zettelstore-client.
// Package htmlenc encodes the abstract syntax tree into HTML5.
package htmlenc

import (
	"io"
	"strings"

	"codeberg.org/t73fde/sxpf"
	"zettelstore.de/c/api"
	"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/textenc"
)

func init() {
	encoder.Register(api.EncoderHTML, func() encoder.Encoder { return &mySHE })
	encoder.Register(api.EncoderHTML, encoder.Info{
		Create: func(env *encoder.Environment) encoder.Encoder { return &htmlEncoder{env: env} },
	})
}

type htmlEncoder struct {
	textEnc *textenc.Encoder
}

	env *encoder.Environment
var mySHE = htmlEncoder{
	textEnc: textenc.Create(),
}

// WriteZettel encodes a full zettel as HTML5.
func (he *htmlEncoder) WriteZettel(w io.Writer, zn *ast.ZettelNode, evalMeta encoder.EvalMetaFunc) (int, error) {
	v := newVisitor(he, w)
	if !he.env.IsXHTML() {
		v.b.WriteString("<!DOCTYPE html>\n")
	}
	if env := he.env; env != nil && env.Lang == "" {
		v.b.WriteStrings("<html>\n<head>")
	} else {
		v.b.WriteStrings("<html lang=\"", env.Lang, "\">")
	}
	io.WriteString(w, "<html>\n<head>\n<meta charset=\"utf-8\">\n")
	v.b.WriteString("\n<head>\n<meta charset=\"utf-8\">\n")
	plainTitle, hasTitle := zn.InhMeta.Get(api.KeyTitle)
	if hasTitle {
		io.WriteString(w, "<title>")
		v.b.WriteStrings("<title>", v.evalValue(plainTitle, evalMeta), "</title>")
		is := evalMeta(plainTitle)
		he.textEnc.WriteInlines(w, &is)
		io.WriteString(w, "</title>\n")
	}

	acceptMeta(w, he.textEnc, zn.InhMeta, evalMeta)
	io.WriteString(w, "</head>\n<body>\n")
	v.acceptMeta(zn.InhMeta, evalMeta)
	v.b.WriteString("\n</head>\n<body>\n")
	env := html.NewEncEnvironment(w, 1)
	if hasTitle {
		if isTitle := evalMeta(plainTitle); len(isTitle) > 0 {
			io.WriteString(w, "<h1>")
			if l, err := acceptInlines(env, &isTitle); err != nil {
			v.b.WriteString("<h1>")
			ast.Walk(v, &isTitle)
				return l, err
			}
			io.WriteString(w, "</h1>\n")
			v.b.WriteString("</h1>\n")
		}
	}

	_, err := acceptBlocks(env, &zn.Ast)
	ast.Walk(v, &zn.Ast)
	if err == nil {
		// env.WriteEndnotes()
		io.WriteString(w, "</body>\n</html>")
	v.writeEndnotes()
	v.b.WriteString("</body>\n</html>")
	}
	return 0, err
	length, err := v.b.Flush()
	return length, err
}

// WriteMeta encodes meta data as HTML5.
func (he *htmlEncoder) WriteMeta(w io.Writer, m *meta.Meta, evalMeta encoder.EvalMetaFunc) (int, error) {
	v := newVisitor(he, w)

	// Write title
	if title, ok := m.Get(api.KeyTitle); ok {
		v.b.WriteStrings("<meta name=\"zs-", api.KeyTitle, "\" content=\"")
		v.writeQuotedEscaped(v.evalValue(title, evalMeta))
		v.b.WriteString("\">")
	}

	// Write other metadata
	acceptMeta(w, he.textEnc, m, evalMeta)
	return 0, nil
	v.acceptMeta(m, evalMeta)
	length, err := v.b.Flush()
	return length, err
}

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

// WriteBlocks encodes a block slice.
func (*htmlEncoder) WriteBlocks(w io.Writer, bs *ast.BlockSlice) (int, error) {
	env := html.NewEncEnvironment(w, 1)
	_, err := acceptBlocks(env, bs)
func (he *htmlEncoder) WriteBlocks(w io.Writer, bs *ast.BlockSlice) (int, error) {
	v := newVisitor(he, w)
	ast.Walk(v, bs)
	if err == nil {
		env.WriteEndnotes()
		err = env.GetError()
	v.writeEndnotes()
	length, err := v.b.Flush()
	}
	return 0, err
	return length, err
}

// WriteInlines writes an inline slice to the writer
func (*htmlEncoder) WriteInlines(w io.Writer, is *ast.InlineSlice) (int, error) {
	env := html.NewEncEnvironment(w, 1)
func (he *htmlEncoder) WriteInlines(w io.Writer, is *ast.InlineSlice) (int, error) {
	v := newVisitor(he, w)
	return acceptInlines(env, is)
}

	if env := he.env; env != nil {
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)
		v.inInteractive = env.Interactive
		var sb strings.Builder
		textEnc.WriteInlines(&sb, &is)
		html.AttributeEscape(w, sb.String())
		io.WriteString(w, "\">\n")
	}
}

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

Added encoder/htmlenc/inline.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
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
//-----------------------------------------------------------------------------
// 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 htmlenc encodes the abstract syntax tree into HTML5.
package htmlenc

import (
	"fmt"
	"strconv"

	"zettelstore.de/c/api"
	"zettelstore.de/c/html"
	"zettelstore.de/c/zjson"
	"zettelstore.de/z/ast"
)

func (v *visitor) visitBreak(bn *ast.BreakNode) {
	if bn.Hard {
		if v.env.IsXHTML() {
			v.b.WriteString("<br />\n")
		} else {
			v.b.WriteString("<br>\n")
		}
	} else {
		v.b.WriteByte('\n')
	}
}

func (v *visitor) visitLink(ln *ast.LinkNode) {
	switch ln.Ref.State {
	case ast.RefStateSelf, ast.RefStateFound, ast.RefStateHosted, ast.RefStateBased:
		v.writeAHref(ln.Ref, ln.Attrs, &ln.Inlines)
	case ast.RefStateBroken:
		attrs := ln.Attrs.Clone()
		attrs = attrs.Set("class", "broken")
		attrs = attrs.Set("title", "Zettel not found") // l10n
		v.writeAHref(ln.Ref, attrs, &ln.Inlines)
	case ast.RefStateExternal:
		attrs := ln.Attrs.Clone()
		attrs = attrs.Set("class", "external")
		if v.env.HasNewWindow() {
			attrs = attrs.Set("target", "_blank").Set("rel", "noopener noreferrer")
		}
		v.writeAHref(ln.Ref, attrs, &ln.Inlines)
		if v.env != nil {
			v.b.WriteString(v.env.MarkerExternal)
		}
	default:
		if v.env.IsInteractive(v.inInteractive) {
			v.writeSpan(&ln.Inlines, ln.Attrs)
			return
		}
		v.b.WriteString("<a href=\"")
		v.writeQuotedEscaped(ln.Ref.Value)
		v.b.WriteByte('"')
		v.visitAttributes(ln.Attrs)
		v.b.WriteByte('>')
		v.writeLinkInlines(&ln.Inlines, ln.Ref)
		v.inInteractive = false
		v.b.WriteString("</a>")
	}
}

func (v *visitor) writeAHref(ref *ast.Reference, attrs zjson.Attributes, is *ast.InlineSlice) {
	if v.env.IsInteractive(v.inInteractive) {
		v.writeSpan(is, attrs)
		return
	}
	v.b.WriteString("<a href=\"")
	v.writeReference(ref)
	v.b.WriteByte('"')
	v.visitAttributes(attrs)
	v.b.WriteByte('>')
	v.writeLinkInlines(is, ref)
	v.b.WriteString("</a>")
}
func (v *visitor) writeLinkInlines(is *ast.InlineSlice, ref *ast.Reference) {
	saveInteractive := v.inInteractive
	v.inInteractive = true
	if len(*is) == 0 {
		v.writeHTMLEscaped(ref.Value)
	} else {
		ast.Walk(v, is)
	}
	v.inInteractive = saveInteractive
}

func (v *visitor) visitEmbedRef(en *ast.EmbedRefNode) {
	v.b.WriteString("<img src=\"")
	v.writeReference(en.Ref)
	v.b.WriteString("\" alt=\"")
	ast.Walk(v, &en.Inlines) // TODO: wrong, must use textenc
	v.b.WriteByte('"')
	v.visitAttributes(en.Attrs)
	if v.env.IsXHTML() {
		v.b.WriteString(" />")
	} else {
		v.b.WriteByte('>')
	}
}

func (v *visitor) visitEmbedBLOB(en *ast.EmbedBLOBNode) {
	if en.Syntax == api.ValueSyntaxSVG {
		v.b.Write(en.Blob)
		return
	}

	v.b.WriteString("<img src=\"data:image/")
	v.b.WriteStrings(en.Syntax, ";base64,")
	v.b.WriteBase64(en.Blob)
	v.b.WriteString("\" alt=\"")
	ast.Walk(v, &en.Inlines)
	v.b.WriteByte('"')
	v.visitAttributes(en.Attrs)
	if v.env.IsXHTML() {
		v.b.WriteString(" />")
	} else {
		v.b.WriteByte('>')
	}
}

func (v *visitor) visitCite(cn *ast.CiteNode) {
	v.b.WriteString(cn.Key)
	if len(cn.Inlines) > 0 {
		v.b.WriteString(", ")
		ast.Walk(v, &cn.Inlines)
	}
}

func (v *visitor) visitFootnote(fn *ast.FootnoteNode) {
	if v.env.IsInteractive(v.inInteractive) {
		return
	}

	n := strconv.Itoa(v.env.AddFootnote(fn))
	v.b.WriteStrings("<sup id=\"fnref:", n, "\"><a href=\"#fn:", n, "\" class=\"footnote-ref\" role=\"doc-noteref\">", n, "</a></sup>")
	// TODO: what to do with Attrs?
}

func (v *visitor) visitMark(mn *ast.MarkNode) {
	if v.env.IsInteractive(v.inInteractive) {
		return
	}
	if fragment := mn.Fragment; fragment != "" {
		v.b.WriteStrings(`<a id="`, fragment, `">`)
		if len(mn.Inlines) > 0 {
			ast.Walk(v, &mn.Inlines)
		}
		v.b.WriteString("</a>")
	}
}

func (v *visitor) visitFormat(fn *ast.FormatNode) {
	var code string
	switch fn.Kind {
	case ast.FormatEmph:
		code = "em"
	case ast.FormatStrong:
		code = "strong"
	case ast.FormatInsert:
		code = "ins"
	case ast.FormatDelete:
		code = "del"
	case ast.FormatSuper:
		code = "sup"
	case ast.FormatSub:
		code = "sub"
	case ast.FormatQuote:
		code = "q"
	case ast.FormatSpan:
		v.writeSpan(&fn.Inlines, processSpanAttributes(fn.Attrs))
		return
	default:
		panic(fmt.Sprintf("Unknown format kind %v", fn.Kind))
	}
	v.b.WriteStrings("<", code)
	v.visitAttributes(fn.Attrs)
	v.b.WriteByte('>')
	ast.Walk(v, &fn.Inlines)
	v.b.WriteStrings("</", code, ">")
}

func (v *visitor) writeSpan(is *ast.InlineSlice, attrs zjson.Attributes) {
	v.b.WriteString("<span")
	v.visitAttributes(attrs)
	v.b.WriteByte('>')
	ast.Walk(v, is)
	v.b.WriteString("</span>")

}

func (v *visitor) visitLiteral(ln *ast.LiteralNode) {
	switch ln.Kind {
	case ast.LiteralProg:
		v.writeLiteral("<code", "</code>", ln.Attrs, ln.Content)
	case ast.LiteralInput:
		v.writeLiteral("<kbd", "</kbd>", ln.Attrs, ln.Content)
	case ast.LiteralOutput:
		v.writeLiteral("<samp", "</samp>", ln.Attrs, ln.Content)
	case ast.LiteralZettel, ast.LiteralComment:
		if v.inlinePos > 0 {
			v.b.WriteByte(' ')
		}
		v.b.WriteString("<!-- ")
		v.writeHTMLEscaped(string(ln.Content)) // writeCommentEscaped
		v.b.WriteString(" -->")
	case ast.LiteralHTML:
		if html.IsSave(string(ln.Content)) {
			v.b.Write(ln.Content)
		}
	default:
		panic(fmt.Sprintf("Unknown literal kind %v", ln.Kind))
	}
}

func (v *visitor) writeLiteral(codeS, codeE string, attrs zjson.Attributes, content []byte) {
	oldVisible := v.visibleSpace
	if attrs != nil {
		v.visibleSpace = attrs.HasDefault()
	}
	v.b.WriteString(codeS)
	v.visitAttributes(attrs)
	v.b.WriteByte('>')
	v.writeHTMLEscaped(string(content))
	v.b.WriteString(codeE)
	v.visibleSpace = oldVisible
}

Added encoder/htmlenc/visitor.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
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
//-----------------------------------------------------------------------------
// 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 htmlenc

import (
	"bytes"
	"io"
	"strconv"
	"strings"

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

// visitor writes the abstract syntax tree to an io.Writer.
type visitor struct {
	env           *encoder.Environment
	b             encoder.EncWriter
	visibleSpace  bool // Show space character in plain text
	inVerse       bool // In verse block
	inInteractive bool // Rendered interactive HTML code
	textEnc       encoder.Encoder
	inlinePos     int // Element position in inline list node
}

func newVisitor(he *htmlEncoder, w io.Writer) *visitor {
	return &visitor{
		env:     he.env,
		b:       encoder.NewEncWriter(w),
		textEnc: encoder.Create(api.EncoderText, nil),
	}
}

func (v *visitor) Visit(node ast.Node) ast.Visitor {
	switch n := node.(type) {
	case *ast.BlockSlice:
		for i, bn := range *n {
			if i > 0 {
				v.b.WriteByte('\n')
			}
			ast.Walk(v, bn)
		}
	case *ast.InlineSlice:
		for i, in := range *n {
			v.inlinePos = i
			ast.Walk(v, in)
		}
		v.inlinePos = 0
	case *ast.ParaNode:
		v.b.WriteString("<p>")
		ast.Walk(v, &n.Inlines)
		v.writeEndPara()
	case *ast.VerbatimNode:
		v.visitVerbatim(n)
	case *ast.RegionNode:
		v.visitRegion(n)
	case *ast.HeadingNode:
		v.visitHeading(n)
	case *ast.HRuleNode:
		v.b.WriteString("<hr")
		v.visitAttributes(n.Attrs)
		if v.env.IsXHTML() {
			v.b.WriteString(" />")
		} else {
			v.b.WriteBytes('>')
		}
	case *ast.NestedListNode:
		v.visitNestedList(n)
	case *ast.DescriptionListNode:
		v.visitDescriptionList(n)
	case *ast.TableNode:
		v.visitTable(n)
	case *ast.TranscludeNode:
		return nil // Nothing to write. Or: an iFrame?
	case *ast.BLOBNode:
		v.visitBLOB(n)
	case *ast.TextNode:
		v.writeHTMLEscaped(n.Text)
	case *ast.TagNode:
		v.b.WriteString("<span class=\"zettel-tag\">#")
		v.writeHTMLEscaped(n.Tag)
		v.b.WriteString("</span>")
	case *ast.SpaceNode:
		if v.inVerse || v.env.IsXHTML() {
			v.b.WriteString(n.Lexeme)
		} else {
			v.b.WriteByte(' ')
		}
	case *ast.BreakNode:
		v.visitBreak(n)
	case *ast.LinkNode:
		v.visitLink(n)
	case *ast.EmbedRefNode:
		v.visitEmbedRef(n)
	case *ast.EmbedBLOBNode:
		v.visitEmbedBLOB(n)
	case *ast.CiteNode:
		v.visitCite(n)
	case *ast.FootnoteNode:
		v.visitFootnote(n)
	case *ast.MarkNode:
		v.visitMark(n)
	case *ast.FormatNode:
		v.visitFormat(n)
	case *ast.LiteralNode:
		v.visitLiteral(n)
	default:
		return v
	}
	return nil
}

var mapMetaKey = map[string]string{
	api.KeyCopyright: "copyright",
	api.KeyLicense:   "license",
}

func (v *visitor) acceptMeta(m *meta.Meta, evalMeta encoder.EvalMetaFunc) {
	ignore := v.setupIgnoreSet()
	ignore.Set(api.KeyTitle)
	if tags, ok := m.Get(api.KeyAllTags); ok {
		v.writeTags(tags)
		ignore.Set(api.KeyAllTags)
		ignore.Set(api.KeyTags)
	} else if tags, ok = m.Get(api.KeyTags); ok {
		v.writeTags(tags)
		ignore.Set(api.KeyTags)
	}

	for _, p := range m.ComputedPairs() {
		key := p.Key
		if ignore.Has(key) {
			continue
		}
		value := p.Value
		if m.Type(key) == meta.TypeZettelmarkup {
			if v := v.evalValue(value, evalMeta); v != "" {
				value = v
			}
		}
		if mKey, ok := mapMetaKey[key]; ok {
			v.writeMeta("", mKey, value)
		} else {
			v.writeMeta("zs-", key, value)
		}
	}
}

func (v *visitor) evalValue(value string, evalMeta encoder.EvalMetaFunc) string {
	var buf bytes.Buffer
	is := evalMeta(value)
	_, err := v.textEnc.WriteInlines(&buf, &is)
	if err == nil {
		return buf.String()
	}
	return ""
}

func (v *visitor) setupIgnoreSet() strfun.Set {
	if v.env == nil || v.env.IgnoreMeta == nil {
		return make(strfun.Set)
	}
	result := make(strfun.Set, len(v.env.IgnoreMeta))
	for k := range v.env.IgnoreMeta {
		result.Set(k)
	}
	return result
}

func (v *visitor) writeTags(tags string) {
	v.b.WriteString("\n<meta name=\"keywords\" content=\"")
	for i, val := range meta.ListFromValue(tags) {
		if i > 0 {
			v.b.WriteString(", ")
		}
		v.writeQuotedEscaped(strings.TrimPrefix(val, "#"))
	}
	v.b.WriteString("\">")
}

func (v *visitor) writeMeta(prefix, key, value string) {
	v.b.WriteStrings("\n<meta name=\"", prefix, key, "\" content=\"")
	v.writeQuotedEscaped(value)
	v.b.WriteString("\">")
}

func (v *visitor) writeEndnotes() {
	fn, fnNum := v.env.PopFootnote()
	if fn == nil {
		return
	}
	v.b.WriteString("\n<ol class=\"endnotes\">\n")
	for fn != nil {
		n := strconv.Itoa(fnNum)
		v.b.WriteStrings("<li value=\"", n, "\" id=\"fn:", n, "\" role=\"doc-endnote\">")
		ast.Walk(v, &fn.Inlines)
		v.b.WriteStrings(
			" <a href=\"#fnref:",
			n,
			"\" class=\"footnote-backref\" role=\"doc-backlink\">&#x21a9;&#xfe0e;</a></li>\n")
		fn, fnNum = v.env.PopFootnote()
	}
	v.b.WriteString("</ol>\n")
}

// visitAttributes write HTML attributes
func (v *visitor) visitAttributes(a zjson.Attributes) {
	if a.IsEmpty() {
		return
	}
	a = a.Clone().RemoveDefault()

	for _, k := range a.Keys() {
		if k == "" || k == "-" {
			continue
		}
		v.b.WriteStrings(" ", k)
		vl := a[k]
		if len(vl) > 0 {
			v.b.WriteString("=\"")
			v.writeQuotedEscaped(vl)
			v.b.WriteByte('"')
		}
	}
}

func (v *visitor) writeHTMLEscaped(s string) {
	if v.visibleSpace {
		html.EscapeVisible(&v.b, s)
	} else {
		html.Escape(&v.b, s)
	}
}

func (v *visitor) writeQuotedEscaped(s string) {
	html.AttributeEscape(&v.b, s)
}

func (v *visitor) writeReference(ref *ast.Reference) {
	if ref.URL == nil {
		v.writeHTMLEscaped(ref.Value)
		return
	}
	v.b.WriteString(ref.URL.String())
}

Added encoder/nativeenc/nativeenc.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
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
//-----------------------------------------------------------------------------
// 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 nativeenc encodes the abstract syntax tree into native format.
package nativeenc

import (
	"fmt"
	"io"
	"strconv"

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

func init() {
	encoder.Register(api.EncoderNative, encoder.Info{
		Create: func(env *encoder.Environment) encoder.Encoder { return &nativeEncoder{env: env} },
	})
}

type nativeEncoder struct {
	env *encoder.Environment
}

// WriteZettel encodes the zettel to the writer.
func (ne *nativeEncoder) WriteZettel(w io.Writer, zn *ast.ZettelNode, evalMeta encoder.EvalMetaFunc) (int, error) {
	v := newVisitor(w, ne)
	v.acceptMeta(zn.InhMeta, evalMeta)
	v.b.WriteByte('\n')
	ast.Walk(v, &zn.Ast)
	length, err := v.b.Flush()
	return length, err
}

// WriteMeta encodes meta data in native format.
func (ne *nativeEncoder) WriteMeta(w io.Writer, m *meta.Meta, evalMeta encoder.EvalMetaFunc) (int, error) {
	v := newVisitor(w, ne)
	v.acceptMeta(m, evalMeta)
	length, err := v.b.Flush()
	return length, err
}

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

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

// WriteInlines writes an inline slice to the writer
func (ne *nativeEncoder) WriteInlines(w io.Writer, is *ast.InlineSlice) (int, error) {
	v := newVisitor(w, ne)
	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
	level int
	env   *encoder.Environment
}

func newVisitor(w io.Writer, enc *nativeEncoder) *visitor {
	return &visitor{b: encoder.NewEncWriter(w), env: enc.env}
}

func (v *visitor) Visit(node ast.Node) ast.Visitor {
	switch n := node.(type) {
	case *ast.BlockSlice:
		v.visitBlockSlice(n)
	case *ast.InlineSlice:
		v.walkInlineSlice(n)
	case *ast.ParaNode:
		v.b.WriteString("[Para ")
		ast.Walk(v, &n.Inlines)
		v.b.WriteByte(']')
	case *ast.VerbatimNode:
		v.visitVerbatim(n)
	case *ast.RegionNode:
		v.visitRegion(n)
	case *ast.HeadingNode:
		v.visitHeading(n)
	case *ast.HRuleNode:
		v.b.WriteString("[Hrule")
		v.visitAttributes(n.Attrs)
		v.b.WriteByte(']')
	case *ast.NestedListNode:
		v.visitNestedList(n)
	case *ast.DescriptionListNode:
		v.visitDescriptionList(n)
	case *ast.TableNode:
		v.visitTable(n)
	case *ast.TranscludeNode:
		v.b.WriteString("[Transclude ")
		v.b.WriteString(mapRefState[n.Ref.State])
		v.b.WriteString(" \"")
		v.writeEscaped(n.Ref.String())
		v.b.WriteString("\"]")
	case *ast.BLOBNode:
		v.visitBLOB(n)
	case *ast.TextNode:
		v.b.WriteString("Text \"")
		v.writeEscaped(n.Text)
		v.b.WriteByte('"')
	case *ast.TagNode:
		v.b.WriteString("Tag \"")
		v.writeEscaped(n.Tag)
		v.b.WriteByte('"')
	case *ast.SpaceNode:
		v.b.WriteString("Space")
		if l := n.Count(); l > 1 {
			v.b.WriteByte(' ')
			v.b.WriteString(strconv.Itoa(l))
		}
	case *ast.BreakNode:
		if n.Hard {
			v.b.WriteString("Break")
		} else {
			v.b.WriteString("Space")
		}
	case *ast.LinkNode:
		v.visitLink(n)
	case *ast.EmbedRefNode:
		v.visitEmbedRef(n)
	case *ast.EmbedBLOBNode:
		v.visitEmbedBLOB(n)
	case *ast.CiteNode:
		v.b.WriteString("Cite")
		v.visitAttributes(n.Attrs)
		v.b.WriteString(" \"")
		v.writeEscaped(n.Key)
		v.b.WriteByte('"')
		if len(n.Inlines) > 0 {
			v.b.WriteString(" [")
			ast.Walk(v, &n.Inlines)
			v.b.WriteByte(']')
		}
	case *ast.FootnoteNode:
		v.b.WriteString("Footnote")
		v.visitAttributes(n.Attrs)
		v.b.WriteString(" [")
		ast.Walk(v, &n.Inlines)
		v.b.WriteByte(']')
	case *ast.MarkNode:
		v.visitMark(n)
	case *ast.FormatNode:
		v.b.Write(mapFormatKind[n.Kind])
		v.visitAttributes(n.Attrs)
		v.b.WriteString(" [")
		ast.Walk(v, &n.Inlines)
		v.b.WriteByte(']')
	case *ast.LiteralNode:
		kind, ok := mapLiteralKind[n.Kind]
		if !ok {
			panic(fmt.Sprintf("Unknown literal kind %v", n.Kind))
		}
		v.b.Write(kind)
		v.visitAttributes(n.Attrs)
		v.b.WriteString(" \"")
		v.writeEscaped(string(n.Content))
		v.b.WriteByte('"')
	default:
		return v
	}
	return nil
}

var (
	rawBackslash   = []byte{'\\', '\\'}
	rawDoubleQuote = []byte{'\\', '"'}
	rawNewline     = []byte{'\\', 'n'}
)

func (v *visitor) acceptMeta(m *meta.Meta, evalMeta encoder.EvalMetaFunc) {
	v.writeZettelmarkup("Title", m.GetDefault(api.KeyTitle, ""), evalMeta)
	v.writeMetaString(m, api.KeyRole, "Role")
	v.writeMetaList(m, api.KeyTags, "Tags")
	v.writeMetaString(m, api.KeySyntax, "Syntax")
	pairs := m.ComputedPairsRest()
	if len(pairs) == 0 {
		return
	}
	v.b.WriteString("\n[Header")
	v.level++
	for i, p := range pairs {
		v.writeComma(i)
		v.writeNewLine()
		key, value := p.Key, p.Value
		if meta.Type(key) == meta.TypeZettelmarkup {
			v.writeZettelmarkup(key, value, evalMeta)
		} else {
			v.b.WriteByte('[')
			v.b.WriteStrings(key, " \"")
			v.writeEscaped(value)
			v.b.WriteString("\"]")
		}
	}
	v.level--
	v.b.WriteByte(']')
}

func (v *visitor) writeZettelmarkup(key, value string, evalMeta encoder.EvalMetaFunc) {
	v.b.WriteByte('[')
	v.b.WriteString(key)
	v.b.WriteByte(' ')
	is := evalMeta(value)
	ast.Walk(v, &is)
	v.b.WriteByte(']')
}

func (v *visitor) writeMetaString(m *meta.Meta, key, native string) {
	if val, ok := m.Get(key); ok && len(val) > 0 {
		v.b.WriteStrings("\n[", native, " \"", val, "\"]")
	}
}

func (v *visitor) writeMetaList(m *meta.Meta, key, native string) {
	if vals, ok := m.GetList(key); ok && len(vals) > 0 {
		v.b.WriteStrings("\n[", native)
		for _, val := range vals {
			v.b.WriteByte(' ')
			v.b.WriteString(val)
		}
		v.b.WriteByte(']')
	}
}

var mapVerbatimKind = map[ast.VerbatimKind][]byte{
	ast.VerbatimZettel:  []byte("[ZettelBlock"),
	ast.VerbatimProg:    []byte("[CodeBlock"),
	ast.VerbatimComment: []byte("[CommentBlock"),
	ast.VerbatimHTML:    []byte("[HTMLBlock"),
}

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

var mapRegionKind = map[ast.RegionKind][]byte{
	ast.RegionSpan:  []byte("[SpanBlock"),
	ast.RegionQuote: []byte("[QuoteBlock"),
	ast.RegionVerse: []byte("[VerseBlock"),
}

func (v *visitor) visitRegion(rn *ast.RegionNode) {
	kind, ok := mapRegionKind[rn.Kind]
	if !ok {
		panic(fmt.Sprintf("Unknown region kind %v", rn.Kind))
	}
	v.b.Write(kind)
	v.visitAttributes(rn.Attrs)
	v.level++
	v.writeNewLine()
	v.b.WriteByte('[')
	v.level++
	ast.Walk(v, &rn.Blocks)
	v.level--
	v.b.WriteByte(']')
	if len(rn.Inlines) > 0 {
		v.b.WriteByte(',')
		v.writeNewLine()
		v.b.WriteString("[Cite ")
		ast.Walk(v, &rn.Inlines)
		v.b.WriteByte(']')
	}
	v.level--
	v.b.WriteByte(']')
}

func (v *visitor) visitHeading(hn *ast.HeadingNode) {
	v.b.WriteStrings("[Heading ", strconv.Itoa(hn.Level))
	if fragment := hn.Fragment; fragment != "" {
		v.b.WriteStrings(" #", fragment)
	}
	v.visitAttributes(hn.Attrs)
	v.b.WriteByte(' ')
	ast.Walk(v, &hn.Inlines)
	v.b.WriteByte(']')
}

var mapNestedListKind = map[ast.NestedListKind][]byte{
	ast.NestedListOrdered:   []byte("[OrderedList"),
	ast.NestedListUnordered: []byte("[BulletList"),
	ast.NestedListQuote:     []byte("[QuoteList"),
}

func (v *visitor) visitNestedList(ln *ast.NestedListNode) {
	v.b.Write(mapNestedListKind[ln.Kind])
	v.level++
	for i, item := range ln.Items {
		v.writeComma(i)
		v.writeNewLine()
		v.level++
		v.b.WriteByte('[')
		for i, in := range item {
			if i > 0 {
				v.b.WriteByte(',')
				v.writeNewLine()
			}
			ast.Walk(v, in)
		}
		v.b.WriteByte(']')
		v.level--
	}
	v.level--
	v.b.WriteByte(']')
}

func (v *visitor) visitDescriptionList(dn *ast.DescriptionListNode) {
	v.b.WriteString("[DescriptionList")
	v.level++
	for i, descr := range dn.Descriptions {
		v.writeComma(i)
		v.writeNewLine()
		v.b.WriteString("[Term [")
		ast.Walk(v, &descr.Term)
		v.b.WriteByte(']')

		if len(descr.Descriptions) > 0 {
			v.level++
			for _, b := range descr.Descriptions {
				v.b.WriteByte(',')
				v.writeNewLine()
				v.b.WriteString("[Description")
				v.level++
				v.writeNewLine()
				for i, dn := range b {
					if i > 0 {
						v.b.WriteByte(',')
						v.writeNewLine()
					}
					ast.Walk(v, dn)
				}
				v.b.WriteByte(']')
				v.level--
			}
			v.level--
		}
		v.b.WriteByte(']')
	}
	v.level--
	v.b.WriteByte(']')
}

func (v *visitor) visitTable(tn *ast.TableNode) {
	v.b.WriteString("[Table")
	v.level++
	if len(tn.Header) > 0 {
		v.writeNewLine()
		v.b.WriteString("[Header ")
		for i, cell := range tn.Header {
			v.writeComma(i)
			v.writeCell(cell)
		}
		v.b.WriteString("],")
	}
	for i, row := range tn.Rows {
		v.writeComma(i)
		v.writeNewLine()
		v.b.WriteString("[Row ")
		for j, cell := range row {
			v.writeComma(j)
			v.writeCell(cell)
		}
		v.b.WriteByte(']')
	}
	v.level--
	v.b.WriteByte(']')
}

var alignString = map[ast.Alignment]string{
	ast.AlignDefault: " Default",
	ast.AlignLeft:    " Left",
	ast.AlignCenter:  " Center",
	ast.AlignRight:   " Right",
}

func (v *visitor) writeCell(cell *ast.TableCell) {
	v.b.WriteStrings("[Cell", alignString[cell.Align])
	if len(cell.Inlines) > 0 {
		v.b.WriteByte(' ')
		ast.Walk(v, &cell.Inlines)
	}
	v.b.WriteByte(']')
}

func (v *visitor) visitBLOB(bn *ast.BLOBNode) {
	v.b.WriteString("[BLOB \"")
	v.writeEscaped(bn.Title)
	v.b.WriteString("\" \"")
	v.writeEscaped(bn.Syntax)
	v.b.WriteString("\" \"")
	if bn.Syntax == api.ValueSyntaxSVG {
		v.writeEscaped(string(bn.Blob))
	} else {
		v.b.WriteBase64(bn.Blob)
	}
	v.b.WriteString("\"]")
}

var mapRefState = map[ast.RefState]string{
	ast.RefStateInvalid:  "INVALID",
	ast.RefStateZettel:   "ZETTEL",
	ast.RefStateSelf:     "SELF",
	ast.RefStateFound:    "ZETTEL",
	ast.RefStateBroken:   "BROKEN",
	ast.RefStateHosted:   "LOCAL",
	ast.RefStateBased:    "BASED",
	ast.RefStateExternal: "EXTERNAL",
}

func (v *visitor) visitLink(ln *ast.LinkNode) {
	v.b.WriteString("Link")
	v.visitAttributes(ln.Attrs)
	v.b.WriteByte(' ')
	v.b.WriteString(mapRefState[ln.Ref.State])
	v.b.WriteString(" \"")
	v.writeEscaped(ln.Ref.String())
	v.b.WriteByte('"')
	if len(ln.Inlines) > 0 {
		v.b.WriteString(" [")
		ast.Walk(v, &ln.Inlines)
		v.b.WriteByte(']')
	}
}

func (v *visitor) visitEmbedRef(en *ast.EmbedRefNode) {
	v.b.WriteString("Embed")
	v.visitAttributes(en.Attrs)
	v.b.WriteByte(' ')
	v.b.WriteString(mapRefState[en.Ref.State])
	v.b.WriteString(" \"")
	v.writeEscaped(en.Ref.String())
	v.b.WriteByte('"')

	if len(en.Inlines) > 0 {
		v.b.WriteString(" [")
		ast.Walk(v, &en.Inlines)
		v.b.WriteByte(']')
	}
}

func (v *visitor) visitEmbedBLOB(en *ast.EmbedBLOBNode) {
	v.b.WriteString("EmbedBLOB")
	v.visitAttributes(en.Attrs)
	v.b.WriteStrings(" {\"", en.Syntax, "\" \"")
	if en.Syntax == api.ValueSyntaxSVG {
		v.writeEscaped(string(en.Blob))
	} else {
		v.b.WriteString("\" \"")
		v.b.WriteBase64(en.Blob)
	}
	v.b.WriteString("\"}")

	if len(en.Inlines) > 0 {
		v.b.WriteString(" [")
		ast.Walk(v, &en.Inlines)
		v.b.WriteByte(']')
	}
}

func (v *visitor) visitMark(mn *ast.MarkNode) {
	v.b.WriteString("Mark")
	if text := mn.Mark; text != "" {
		v.b.WriteString(" \"")
		v.writeEscaped(text)
		v.b.WriteByte('"')
	}
	if fragment := mn.Fragment; fragment != "" {
		v.b.WriteString(" #")
		v.writeEscaped(fragment)
	}
	if len(mn.Inlines) > 0 {
		v.b.WriteString(" [")
		ast.Walk(v, &mn.Inlines)
		v.b.WriteByte(']')
	}
}

var mapFormatKind = map[ast.FormatKind][]byte{
	ast.FormatEmph:   []byte("Emph"),
	ast.FormatStrong: []byte("Strong"),
	ast.FormatInsert: []byte("Insert"),
	ast.FormatDelete: []byte("Delete"),
	ast.FormatSuper:  []byte("Super"),
	ast.FormatSub:    []byte("Sub"),
	ast.FormatQuote:  []byte("Quote"),
	ast.FormatSpan:   []byte("Span"),
}

var mapLiteralKind = map[ast.LiteralKind][]byte{
	ast.LiteralZettel:  []byte("Zettel"),
	ast.LiteralProg:    []byte("Code"),
	ast.LiteralInput:   []byte("Input"),
	ast.LiteralOutput:  []byte("Output"),
	ast.LiteralComment: []byte("Comment"),
	ast.LiteralHTML:    []byte("HTML"),
}

func (v *visitor) visitBlockSlice(bs *ast.BlockSlice) {
	for i, bn := range *bs {
		if i > 0 {
			v.b.WriteByte(',')
			v.writeNewLine()
		}
		ast.Walk(v, bn)
	}
}
func (v *visitor) walkInlineSlice(is *ast.InlineSlice) {
	for i, in := range *is {
		v.writeComma(i)
		ast.Walk(v, in)
	}
}

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

	v.b.WriteString(" (\"")
	if val, ok := a[""]; ok {
		v.writeEscaped(val)
	}
	v.b.WriteString("\",[")
	for i, k := range a.Keys() {
		if k == "" {
			continue
		}
		v.writeComma(i)
		v.b.WriteString(k)
		val := a[k]
		if len(val) > 0 {
			v.b.WriteString("=\"")
			v.writeEscaped(val)
			v.b.WriteByte('"')
		}
	}
	v.b.WriteString("])")
}

func (v *visitor) writeNewLine() {
	v.b.WriteByte('\n')
	for i := 0; i < v.level; i++ {
		v.b.WriteByte(' ')
	}
}

func (v *visitor) writeEscaped(s string) {
	last := 0
	for i, ch := range s {
		var b []byte
		switch ch {
		case '\n':
			b = rawNewline
		case '"':
			b = rawDoubleQuote
		case '\\':
			b = rawBackslash
		default:
			continue
		}
		v.b.WriteString(s[last:i])
		v.b.Write(b)
		last = i + 1
	}
	v.b.WriteString(s[last:])
}

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

Deleted encoder/sexprenc/sexprenc.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



























































-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
//-----------------------------------------------------------------------------
// 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 sexprenc encodes the abstract syntax tree into a s-expr.
package sexprenc

import (
	"io"

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

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

// Create a S-expr 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) {
	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 JSON.
func (*Encoder) WriteMeta(w io.Writer, m *meta.Meta, evalMeta encoder.EvalMetaFunc) (int, error) {
	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) {
	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) {
	return io.WriteString(w, GetSexpr(is).String())
}

Deleted encoder/sexprenc/transform.go.

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


























































































































































































































































































































































































































































































-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
//-----------------------------------------------------------------------------
// 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 sexprenc encodes the abstract syntax tree into a s-expr.
package sexprenc

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

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

// GetSexpr returns the given node as a s-expression.
func GetSexpr(node ast.Node) *sxpf.Pair {
	t := transformer{}
	return t.getSexpr(node)
}

type transformer struct {
	inVerse bool
}

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 sxpf.NewPair(sexpr.SymPara, t.getInlineSlice(n.Inlines))
	case *ast.VerbatimNode:
		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.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.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.NewPairFromValues(sexpr.SymTransclude, getReference(n.Ref))
	case *ast.BLOBNode:
		return getBLOB(n)
	case *ast.TextNode:
		return sxpf.NewPairFromValues(sexpr.SymText, sxpf.NewString(n.Text))
	case *ast.TagNode:
		return sxpf.NewPairFromValues(sexpr.SymTag, sxpf.NewString(n.Tag))
	case *ast.SpaceNode:
		if t.inVerse {
			return sxpf.NewPairFromValues(sexpr.SymSpace, sxpf.NewString(n.Lexeme))
		}
		return sxpf.NewPairFromValues(sexpr.SymSpace)
	case *ast.BreakNode:
		if n.Hard {
			return sxpf.NewPairFromValues(sexpr.SymHard)
		} else {
			return sxpf.NewPairFromValues(sexpr.SymSoft)
		}
	case *ast.LinkNode:
		return t.getLink(n)
	case *ast.EmbedRefNode:
		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),
					),
				),
			),
		)
	case *ast.EmbedBLOBNode:
		return t.getEmbedBLOB(n)
	case *ast.CiteNode:
		return sxpf.NewPair(
			sexpr.SymCite,
			sxpf.NewPair(
				getAttributes(n.Attrs),
				sxpf.NewPair(
					sxpf.NewString(n.Key),
					t.getInlineSlice(n.Inlines),
				),
			),
		)
	case *ast.FootnoteNode:
		return sxpf.NewPair(
			sexpr.SymFootnote,
			sxpf.NewPair(
				getAttributes(n.Attrs),
				t.getInlineSlice(n.Inlines),
			),
		)
	case *ast.MarkNode:
		return sxpf.NewPair(
			sexpr.SymMark,
			sxpf.NewPair(
				sxpf.NewString(n.Mark),
				sxpf.NewPair(
					sxpf.NewString(n.Slug),
					sxpf.NewPair(
						sxpf.NewString(n.Fragment),
						t.getInlineSlice(n.Inlines),
					),
				),
			),
		)
	case *ast.FormatNode:
		return sxpf.NewPair(
			mapGetS(mapFormatKindS, n.Kind),
			sxpf.NewPair(
				getAttributes(n.Attrs),
				t.getInlineSlice(n.Inlines),
			),
		)
	case *ast.LiteralNode:
		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.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.Pair {
	saveInVerse := t.inVerse
	if rn.Kind == ast.RegionVerse {
		t.inVerse = true
	}
	symBlocks := t.getSexpr(&rn.Blocks)
	t.inVerse = saveInVerse
	return sxpf.NewPairFromValues(
		mapGetS(mapRegionKindS, rn.Kind),
		getAttributes(rn.Attrs),
		symBlocks,
		t.getSexpr(&rn.Inlines),
	)
}

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

func (t *transformer) getNestedList(ln *ast.NestedListNode) *sxpf.Pair {
	nlistVals := make([]sxpf.Value, len(ln.Items)+1)
	nlistVals[0] = mapGetS(mapNestedListKindS, ln.Kind)
	isCompact := isCompactList(ln.Items)
	for i, item := range ln.Items {
		if isCompact && len(item) > 0 {
			paragraph := t.getSexpr(item[0])
			nlistVals[i+1] = paragraph.GetTail()
			continue
		}
		itemVals := make([]sxpf.Value, len(item))
		for j, in := range item {
			itemVals[j] = t.getSexpr(in)
		}
		nlistVals[i+1] = sxpf.NewPairFromValues(itemVals...)
	}
	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.Pair {
	dlVals := make([]sxpf.Value, 2*len(dn.Descriptions)+1)
	dlVals[0] = sexpr.SymDescription
	for i, def := range dn.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.Value, len(b))
			for k, dn := range b {
				dVal[k] = t.getSexpr(dn)
			}
			descVals[j] = sxpf.NewPairFromValues(dVal...)
		}
		dlVals[2*i+2] = sxpf.NewPairFromValues(descVals...)
	}
	return sxpf.NewPairFromValues(dlVals...)
}

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

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.Pair {
	return sxpf.NewPair(mapGetS(alignmentSymbolS, cell.Align), t.getInlineSlice(cell.Inlines))
}

func getBLOB(bn *ast.BLOBNode) *sxpf.Pair {
	var lastValue sxpf.Value
	if bn.Syntax == api.ValueSyntaxSVG {
		lastValue = sxpf.NewString(string(bn.Blob))
	} else {
		lastValue = getBase64String(bn.Blob)
	}
	return sxpf.NewPairFromValues(
		sexpr.SymBLOB,
		sxpf.NewString(bn.Title),
		sxpf.NewString(bn.Syntax),
		lastValue,
	)
}

var mapRefStateLink = map[ast.RefState]*sxpf.Symbol{
	ast.RefStateInvalid:  sexpr.SymLinkInvalid,
	ast.RefStateZettel:   sexpr.SymLinkZettel,
	ast.RefStateSelf:     sexpr.SymLinkSelf,
	ast.RefStateFound:    sexpr.SymLinkFound,
	ast.RefStateBroken:   sexpr.SymLinkBroken,
	ast.RefStateHosted:   sexpr.SymLinkHosted,
	ast.RefStateBased:    sexpr.SymLinkBased,
	ast.RefStateExternal: sexpr.SymLinkExternal,
}

func (t *transformer) getLink(ln *ast.LinkNode) *sxpf.Pair {
	return sxpf.NewPair(
		mapGetS(mapRefStateLink, ln.Ref.State),
		sxpf.NewPair(
			getAttributes(ln.Attrs),
			sxpf.NewPair(
				sxpf.NewString(ln.Ref.Value),
				t.getInlineSlice(ln.Inlines),
			),
		),
	)
}

func (t *transformer) getEmbedBLOB(en *ast.EmbedBLOBNode) *sxpf.Pair {
	tail := t.getInlineSlice(en.Inlines)
	if en.Syntax == api.ValueSyntaxSVG {
		tail = sxpf.NewPair(sxpf.NewString(string(en.Blob)), tail)
	} else {
		tail = sxpf.NewPair(getBase64String(en.Blob), tail)
	}
	return sxpf.NewPair(
		sexpr.SymEmbedBLOB,
		sxpf.NewPair(
			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.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 GetMeta(m *meta.Meta, evalMeta encoder.EvalMetaFunc) *sxpf.Pair {
	pairs := m.ComputedPairs()
	lstVals := make([]sxpf.Value, 0, len(pairs))
	for _, p := range pairs {
		key := p.Key
		ty := m.Type(key)
		symType := mapGetS(mapMetaTypeS, ty)
		strKey := sxpf.NewString(key)
		var val sxpf.Value
		if ty.IsSet {
			setList := meta.ListFromValue(p.Value)
			setVals := make([]sxpf.Value, len(setList))
			for i, val := range setList {
				setVals[i] = sxpf.NewString(val)
			}
			val = sxpf.NewPairFromSlice(setVals)
		} else if ty == meta.TypeZettelmarkup {
			is := evalMeta(p.Value)
			t := transformer{}
			val = t.getSexpr(&is)
		} else {
			val = sxpf.NewString(p.Value)
		}
		lstVals = append(lstVals, sxpf.NewPair(symType, sxpf.NewPair(strKey, sxpf.NewPair(val, nil))))
	}
	return sxpf.NewPairFromSlice(lstVals)
}

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 sexpr.Smk.MakeSymbol(fmt.Sprintf("**%v:not-found**", k))
}

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.NewString(buf.String())
	}
	return nil
}

Changes to encoder/textenc/textenc.go.

17
18
19
20
21
22
23
24
25
26




27
28
29
30

31
32
33
34
35

36
37
38
39
40
41
42
43
44

45
46
47
48
49
50
51
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







-
-
-
+
+
+
+
-
-

-
+

-
-

-
+








-
+







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

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

	encoder.Register(api.EncoderText, encoder.Info{
		Create: func(*encoder.Environment) encoder.Encoder { return &textEncoder{} },
	})
}
// Create an encoder.
func Create() *Encoder { return &myTE }

type Encoder struct{}
type textEncoder struct{}

var myTE Encoder // Only a singleton is required.

// WriteZettel writes metadata and content.
func (te *Encoder) WriteZettel(w io.Writer, zn *ast.ZettelNode, evalMeta encoder.EvalMetaFunc) (int, error) {
func (te *textEncoder) WriteZettel(w io.Writer, zn *ast.ZettelNode, evalMeta encoder.EvalMetaFunc) (int, error) {
	v := newVisitor(w)
	te.WriteMeta(&v.b, zn.InhMeta, evalMeta)
	v.visitBlockSlice(&zn.Ast)
	length, err := v.b.Flush()
	return length, err
}

// WriteMeta encodes metadata as text.
func (te *Encoder) WriteMeta(w io.Writer, m *meta.Meta, evalMeta encoder.EvalMetaFunc) (int, error) {
func (te *textEncoder) WriteMeta(w io.Writer, m *meta.Meta, evalMeta encoder.EvalMetaFunc) (int, error) {
	buf := encoder.NewEncWriter(w)
	for _, pair := range m.ComputedPairs() {
		switch meta.Type(pair.Key) {
		case meta.TypeTagSet:
			writeTagSet(&buf, meta.ListFromValue(pair.Value))
		case meta.TypeZettelmarkup:
			is := evalMeta(pair.Value)
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
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







-
+




-
+







-
+




















-

+
-
+
+
+
+







			buf.WriteByte(' ')
		}
		buf.WriteString(meta.CleanTag(tag))
	}

}

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

// WriteBlocks writes the content of a block slice to the writer.
func (*Encoder) WriteBlocks(w io.Writer, bs *ast.BlockSlice) (int, error) {
func (*textEncoder) WriteBlocks(w io.Writer, bs *ast.BlockSlice) (int, error) {
	v := newVisitor(w)
	v.visitBlockSlice(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) {
func (*textEncoder) WriteInlines(w io.Writer, is *ast.InlineSlice) (int, error) {
	v := newVisitor(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
	inlinePos int
}

func newVisitor(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:
		for i, in := range *n {
		v.visitInlineSlice(n)
			v.inlinePos = i
			ast.Walk(v, in)
		}
		v.inlinePos = 0
		return nil
	case *ast.VerbatimNode:
		v.visitVerbatim(n)
		return nil
	case *ast.RegionNode:
		v.visitBlockSlice(&n.Blocks)
		if len(n.Inlines) > 0 {
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
219
220
221
222
223
224
225








226
227
228
229
230







-
-
-
-
-
-
-
-





func (v *visitor) visitBlockSlice(bs *ast.BlockSlice) {
	for i, bn := range *bs {
		v.writePosChar(i, '\n')
		ast.Walk(v, bn)
	}
}

func (v *visitor) visitInlineSlice(is *ast.InlineSlice) {
	for i, in := range *is {
		v.inlinePos = i
		ast.Walk(v, in)
	}
	v.inlinePos = 0
}

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

Changes to encoder/zjsonenc/zjsonenc.go.

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




32
33
34
35
36



37
38
39
40
41


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


54
55
56
57
58
59

60
61
62
63
64
65


66
67
68
69
70
71
72
73


74
75
76
77
78
79
80
81

82
83
84

85


86
87
88
89
90
91
92
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







-








-
-
-
+
+
+
+
-
-

-
-
+
+
+
-


-
-
+
+










-
-
+
+





-
+




-
-
+
+






-
-
+
+








+



+
-
+
+








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

	encoder.Register(api.EncoderZJSON, encoder.Info{
		Create: func(env *encoder.Environment) encoder.Encoder { return &jsonDetailEncoder{env: env} },
	})
}
// Create a ZJSON encoder
func Create() *Encoder { return &myJE }

type Encoder struct{}

type jsonDetailEncoder struct {
	env *encoder.Environment
}
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)
func (je *jsonDetailEncoder) WriteZettel(w io.Writer, zn *ast.ZettelNode, evalMeta encoder.EvalMetaFunc) (int, error) {
	v := newDetailVisitor(w, je)
	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)
func (je *jsonDetailEncoder) WriteMeta(w io.Writer, m *meta.Meta, evalMeta encoder.EvalMetaFunc) (int, error) {
	v := newDetailVisitor(w, je)
	v.writeMeta(m, evalMeta)
	length, err := v.b.Flush()
	return length, err
}

func (je *Encoder) WriteContent(w io.Writer, zn *ast.ZettelNode) (int, error) {
func (je *jsonDetailEncoder) 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)
func (je *jsonDetailEncoder) WriteBlocks(w io.Writer, bs *ast.BlockSlice) (int, error) {
	v := newDetailVisitor(w, je)
	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)
func (je *jsonDetailEncoder) WriteInlines(w io.Writer, is *ast.InlineSlice) (int, error) {
	v := newDetailVisitor(w, je)
	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
	env     *encoder.Environment
	inVerse bool // Visiting a verse block: save spaces in ZJSON object
}

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

func (v *visitor) Visit(node ast.Node) ast.Visitor {
	switch n := node.(type) {
	case *ast.BlockSlice:
		v.visitBlockSlice(n)
		return nil
	case *ast.InlineSlice:
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
191
192
193
194
195
196
197


198
199
200
201
202
203
204







-
-







	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 {
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
435
436
437
438
439
440
441

442
443
444
445
446
447
448







-







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)
458
459
460
461
462
463
464
465

466
467
468
469
470
471
472
456
457
458
459
460
461
462

463
464
465
466
467
468
469
470







-
+







		v.writeComma(i)
		ast.Walk(v, in)
	}
	v.b.WriteByte(']')
}

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

	v.writeContentStart(zjson.NameAttribute)
	for i, k := range a.Keys() {
		if i > 0 {

Changes to encoder/zmkenc/zmkenc.go.

12
13
14
15
16
17
18
19

20
21
22
23
24
25
26
27



28
29
30
31
32
33
34
35
36


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


51
52
53
54
55
56
57
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







-
+







-
+
+
+




-
-

-
-
+
+












-
-
+
+







package zmkenc

import (
	"fmt"
	"io"

	"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.EncoderZmk, func() encoder.Encoder { return &myZE })
	encoder.Register(api.EncoderZmk, encoder.Info{
		Create: func(*encoder.Environment) encoder.Encoder { return &zmkEncoder{} },
	})
}

type zmkEncoder struct{}

var myZE zmkEncoder

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

// WriteMeta encodes meta data as zmk.
func (*zmkEncoder) WriteMeta(w io.Writer, m *meta.Meta, evalMeta encoder.EvalMetaFunc) (int, error) {
	v := newVisitor(w)
func (ze *zmkEncoder) WriteMeta(w io.Writer, m *meta.Meta, evalMeta encoder.EvalMetaFunc) (int, error) {
	v := newVisitor(w, ze)
	v.acceptMeta(m, evalMeta)
	length, err := v.b.Flush()
	return length, err
}

func (v *visitor) acceptMeta(m *meta.Meta, evalMeta encoder.EvalMetaFunc) {
	for _, p := range m.ComputedPairs() {
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
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







-
-
+
+






-
-
+
+









+




-
-
+
+
+
+
+







}

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

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

// WriteInlines writes an inline slice to the writer
func (*zmkEncoder) WriteInlines(w io.Writer, is *ast.InlineSlice) (int, error) {
	v := newVisitor(w)
func (ze *zmkEncoder) WriteInlines(w io.Writer, is *ast.InlineSlice) (int, error) {
	v := newVisitor(w, ze)
	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
	prefix    []byte
	enc       *zmkEncoder
	inVerse   bool
	inlinePos int
}

func newVisitor(w io.Writer) *visitor {
	return &visitor{b: encoder.NewEncWriter(w)}
func newVisitor(w io.Writer, enc *zmkEncoder) *visitor {
	return &visitor{
		b:   encoder.NewEncWriter(w),
		enc: enc,
	}
}

func (v *visitor) Visit(node ast.Node) ast.Visitor {
	switch n := node.(type) {
	case *ast.BlockSlice:
		v.visitBlockSlice(n)
	case *ast.InlineSlice:
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
182
183
184
185
186
187
188


189
190
191
192
193
194
195







-
-







}

var mapVerbatimKind = map[ast.VerbatimKind]string{
	ast.VerbatimZettel:  "@@@",
	ast.VerbatimComment: "%%%",
	ast.VerbatimHTML:    "@@@", // Attribute is set to {="html"}
	ast.VerbatimProg:    "```",
	ast.VerbatimEval:    "~~~",
	ast.VerbatimMath:    "$$$",
}

func (v *visitor) visitVerbatim(vn *ast.VerbatimNode) {
	kind, ok := mapVerbatimKind[vn.Kind]
	if !ok {
		panic(fmt.Sprintf("Unknown verbatim kind %d", vn.Kind))
	}
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
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







-
-
-








-
+
-
-








-
+



-
+



-
+








func (v *visitor) visitLiteral(ln *ast.LiteralNode) {
	switch ln.Kind {
	case ast.LiteralZettel:
		v.writeLiteral('@', ln.Attrs, ln.Content)
	case ast.LiteralProg:
		v.writeLiteral('`', ln.Attrs, ln.Content)
	case ast.LiteralMath:
		v.b.WriteStrings("$$", string(ln.Content), "$$")
		v.visitAttributes(ln.Attrs)
	case ast.LiteralInput:
		v.writeLiteral('\'', ln.Attrs, ln.Content)
	case ast.LiteralOutput:
		v.writeLiteral('=', ln.Attrs, ln.Content)
	case ast.LiteralComment:
		if v.inlinePos > 0 {
			v.b.WriteByte(' ')
		}
		v.b.WriteString("%%")
		v.b.WriteString("%% ")
		v.visitAttributes(ln.Attrs)
		v.b.WriteByte(' ')
		v.b.Write(ln.Content)
	case ast.LiteralHTML:
		v.writeLiteral('x', syntaxToHTML(ln.Attrs), ln.Content)
	default:
		panic(fmt.Sprintf("Unknown literal kind %v", ln.Kind))
	}
}

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

// visitAttributes write HTML attributes
func (v *visitor) visitAttributes(a attrs.Attributes) {
func (v *visitor) visitAttributes(a zjson.Attributes) {
	if a.IsEmpty() {
		return
	}
	v.b.WriteByte('{')
	for i, k := range a.Keys() {
		if i > 0 {
			v.b.WriteByte(' ')
520
521
522
523
524
525
526
527

528
529
517
518
519
520
521
522
523

524
525
526







-
+


			v.b.WriteBytes('\\', b)
			last = i + 1
		}
	}
	v.b.WriteString(s[last:])
}

func syntaxToHTML(a attrs.Attributes) attrs.Attributes {
func syntaxToHTML(a zjson.Attributes) zjson.Attributes {
	return a.Clone().Set("", api.ValueSyntaxHTML).Remove(api.KeySyntax)
}

Changes to evaluator/evaluator.go.

15
16
17
18
19
20
21
22

23
24
25
26
27
28
29
30
31
32
33








34
35
36
37
38
39


40
41
42
43

44
45
46
47
48
49

50
51
52
53
54
55
56


57
58
59
60




61
62
63

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

77
78
79
80
81
82
83
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







-
+









-

+
+
+
+
+
+
+
+






+
+



-
+





-
+





-
-
+
+



-
+
+
+
+



+













+







	"context"
	"errors"
	"fmt"
	"strconv"
	"strings"

	"zettelstore.de/c/api"
	"zettelstore.de/c/attrs"
	"zettelstore.de/c/zjson"
	"zettelstore.de/z/ast"
	"zettelstore.de/z/box"
	"zettelstore.de/z/config"
	"zettelstore.de/z/domain"
	"zettelstore.de/z/domain/id"
	"zettelstore.de/z/domain/meta"
	"zettelstore.de/z/input"
	"zettelstore.de/z/parser"
	"zettelstore.de/z/parser/cleaner"
	"zettelstore.de/z/parser/draw"
)

// Environment contains values to control the evaluation.
type Environment struct {
	GetTagRef        func(string) *ast.Reference
	GetHostedRef     func(string) *ast.Reference
	GetFoundRef      func(zid id.Zid, fragment string) *ast.Reference
	GetImageMaterial func(zettel domain.Zettel, syntax string) ast.InlineEmbedNode
}

// Port contains all methods to retrieve zettel (or part of it) to evaluate a zettel.
type Port interface {
	GetMeta(context.Context, id.Zid) (*meta.Meta, error)
	GetZettel(context.Context, id.Zid) (domain.Zettel, error)
}

var emptyEnv Environment

// EvaluateZettel evaluates the given zettel in the given context, with the
// given ports, and the given environment.
func EvaluateZettel(ctx context.Context, port Port, rtConfig config.Config, zn *ast.ZettelNode) {
func EvaluateZettel(ctx context.Context, port Port, env *Environment, rtConfig config.Config, zn *ast.ZettelNode) {
	if zn.Syntax == api.ValueSyntaxNone {
		// AST is empty, evaluate to a description list of metadata.
		zn.Ast = evaluateMetadata(zn.Meta)
		return
	}
	evaluateNode(ctx, port, rtConfig, &zn.Ast)
	evaluateNode(ctx, port, env, rtConfig, &zn.Ast)
	cleaner.CleanBlockSlice(&zn.Ast)
}

// EvaluateInline evaluates the given inline list in the given context, with
// the given ports, and the given environment.
func EvaluateInline(ctx context.Context, port Port, rtConfig config.Config, is *ast.InlineSlice) {
	evaluateNode(ctx, port, rtConfig, is)
func EvaluateInline(ctx context.Context, port Port, env *Environment, rtConfig config.Config, is *ast.InlineSlice) {
	evaluateNode(ctx, port, env, rtConfig, is)
	cleaner.CleanInlineSlice(is)
}

func evaluateNode(ctx context.Context, port Port, rtConfig config.Config, n ast.Node) {
func evaluateNode(ctx context.Context, port Port, env *Environment, rtConfig config.Config, n ast.Node) {
	if env == nil {
		env = &emptyEnv
	}
	e := evaluator{
		ctx:             ctx,
		port:            port,
		env:             env,
		rtConfig:        rtConfig,
		transcludeMax:   rtConfig.GetMaxTransclusions(),
		transcludeCount: 0,
		costMap:         map[id.Zid]transcludeCost{},
		embedMap:        map[string]ast.InlineSlice{},
		marker:          &ast.ZettelNode{},
	}
	ast.Walk(&e, n)
}

type evaluator struct {
	ctx             context.Context
	port            Port
	env             *Environment
	rtConfig        config.Config
	transcludeMax   int
	transcludeCount int
	costMap         map[id.Zid]transcludeCost
	marker          *ast.ZettelNode
	embedMap        map[string]ast.InlineSlice
}
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
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







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

-
+









-
+







	if i+1 < len(bns) {
		newIns = append(newIns, bns[i+1:]...)
	}
	return newIns
}

func (e *evaluator) evalVerbatimNode(vn *ast.VerbatimNode) ast.BlockNode {
	switch vn.Kind {
	case ast.VerbatimZettel:
	if vn.Kind != ast.VerbatimZettel {
		return e.evalVerbatimZettel(vn)
	case ast.VerbatimEval:
		if syntax, found := vn.Attrs.Get(""); found && syntax == ast.VerbatimEvalSyntaxDraw {
			return draw.ParseDrawBlock(vn)
		}
	}
	return vn
}
		return vn
	}

func (e *evaluator) evalVerbatimZettel(vn *ast.VerbatimNode) ast.BlockNode {
	m := meta.New(id.Invalid)
	m.Set(api.KeySyntax, getSyntax(vn.Attrs, api.ValueSyntaxText))
	m.Set(api.KeySyntax, getSyntax(vn.Attrs, api.ValueSyntaxDraw))
	zettel := domain.Zettel{
		Meta:    m,
		Content: domain.NewContent(vn.Content),
	}
	e.transcludeCount++
	zn := e.evaluateEmbeddedZettel(zettel)
	return &zn.Ast
}

func getSyntax(a attrs.Attributes, defSyntax string) string {
func getSyntax(a zjson.Attributes, defSyntax string) string {
	if a != nil {
		if val, ok := a.Get(api.KeySyntax); ok {
			return val
		}
		if val, ok := a.Get(""); ok {
			return val
		}
240
241
242
243
244
245
246


247
248
249
250
251
252
253
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260







+
+







func makeBlockNode(in ast.InlineNode) ast.BlockNode { return ast.CreateParaNode(in) }

func (e *evaluator) visitInlineSlice(is *ast.InlineSlice) {
	for i := 0; i < len(*is); i++ {
		in := (*is)[i]
		ast.Walk(e, in)
		switch n := in.(type) {
		case *ast.TagNode:
			(*is)[i] = e.visitTag(n)
		case *ast.LinkNode:
			(*is)[i] = e.evalLinkNode(n)
		case *ast.EmbedRefNode:
			i += embedNode(is, i, e.evalEmbedRefNode(n))
		case *ast.LiteralNode:
			i += embedNode(is, i, e.evalLiteralNode(n))
		}
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
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







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

+
+
+
+
+
+
+
+
+
-
+
















+
+
-
+
+







	}
	if i+1 < len(ins) {
		newIns = append(newIns, ins[i+1:]...)
	}
	return newIns
}

func (e *evaluator) evalLinkNode(ln *ast.LinkNode) ast.InlineNode {
	if len(ln.Inlines) == 0 {
		ln.Inlines = ast.InlineSlice{&ast.TextNode{Text: ln.Ref.Value}}
	}
func (e *evaluator) visitTag(tn *ast.TagNode) ast.InlineNode {
	if gtr := e.env.GetTagRef; gtr != nil {
		fullTag := "#" + tn.Tag
		return &ast.LinkNode{
			Ref:     e.env.GetTagRef(fullTag),
			Inlines: ast.CreateInlineSliceFromWords(fullTag),
		}
	}
	return tn
}

func (e *evaluator) evalLinkNode(ln *ast.LinkNode) ast.InlineNode {
	ref := ln.Ref
	if ref == nil {
		return ln
	}
	if ref.State == ast.RefStateBased {
		if ghr := e.env.GetHostedRef; ghr != nil {
			ln.Ref = ghr(ref.Value[1:])
		}
		return ln
	}
	if ref == nil || ref.State != ast.RefStateZettel {
	if ref.State != ast.RefStateZettel {
		return ln
	}

	zid := mustParseZid(ref)
	_, err := e.port.GetMeta(box.NoEnrichContext(e.ctx), zid)
	if errors.Is(err, &box.ErrNotAllowed{}) {
		return &ast.FormatNode{
			Kind:    ast.FormatSpan,
			Attrs:   ln.Attrs,
			Inlines: getLinkInline(ln),
		}
	} else if err != nil {
		ln.Ref.State = ast.RefStateBroken
		return ln
	}

	if gfr := e.env.GetFoundRef; gfr != nil {
		ln.Inlines = getLinkInline(ln)
	ln.Ref.State = ast.RefStateZettel
		ln.Ref = gfr(zid, ref.URL.EscapedFragment())
	}
	return ln
}

func getLinkInline(ln *ast.LinkNode) ast.InlineSlice {
	if ln.Inlines != nil {
		return ln.Inlines
	}
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
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







-
+













-
+


-
+
-
-
+







	}

	switch ref.State {
	case ast.RefStateZettel:
		// Only zettel references will be evaluated.
	case ast.RefStateInvalid, ast.RefStateBroken:
		e.transcludeCount++
		return createInlineErrorImage(en)
		return e.createInlineErrorImage(en)
	case ast.RefStateSelf:
		e.transcludeCount++
		return createInlineErrorText(ref, "Self", "embed", "reference:")
	case ast.RefStateFound, ast.RefStateHosted, ast.RefStateBased, ast.RefStateExternal:
		return en
	default:
		panic(fmt.Sprintf("Unknown state %v for reference %v", ref.State, ref))
	}

	zid := mustParseZid(ref)
	zettel, err := e.port.GetZettel(box.NoEnrichContext(e.ctx), zid)
	if err != nil {
		e.transcludeCount++
		return createInlineErrorImage(en)
		return e.createInlineErrorImage(en)
	}

	if syntax := zettel.Meta.GetDefault(api.KeySyntax, ""); parser.IsImageFormat(syntax) {
	if syntax := e.getSyntax(zettel.Meta); parser.IsImageFormat(syntax) {
		en.Syntax = syntax
		return en
		return e.embedImage(en, zettel)
	} else if !parser.IsTextParser(syntax) {
		// Not embeddable.
		e.transcludeCount++
		return createInlineErrorText(ref, "Not", "embeddable (syntax="+syntax+"):")
	}

	cost, ok := e.costMap[zid]
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
399
400
401
402
403
404
405

406
407
408
409
410
411
412







-







		// Search for text to be embedded.
		result = findInlineSlice(&zn.Ast, ref.URL.Fragment)
		e.embedMap[ref.Value] = result
	}
	if len(result) == 0 {
		return &ast.LiteralNode{
			Kind:    ast.LiteralComment,
			Attrs:   map[string]string{"-": ""},
			Content: append([]byte("Nothing to transclude: "), ref.String()...),
		}
	}

	if ec := cost.ec; ec > 0 {
		e.transcludeCount += cost.ec
	}
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
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







-
+



-






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

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






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







}

func (e *evaluator) evalLiteralNode(ln *ast.LiteralNode) ast.InlineNode {
	if ln.Kind != ast.LiteralZettel {
		return ln
	}
	e.transcludeCount++
	result := e.evaluateEmbeddedInline(ln.Content, getSyntax(ln.Attrs, api.ValueSyntaxText))
	result := e.evaluateEmbeddedInline(ln.Content, getSyntax(ln.Attrs, api.ValueSyntaxDraw))
	if len(result) == 0 {
		return &ast.LiteralNode{
			Kind:    ast.LiteralComment,
			Attrs:   map[string]string{"-": ""},
			Content: []byte("Nothing to transclude"),
		}
	}
	return &result
}

func (e *evaluator) getSyntax(m *meta.Meta) string {
	if cfg := e.rtConfig; cfg != nil {
		return config.GetSyntax(m, cfg)
	}
	return m.GetDefault(api.KeySyntax, "")
}

func (e *evaluator) getTitle(m *meta.Meta) string {
	if cfg := e.rtConfig; cfg != nil {
		return config.GetTitle(m, cfg)
	}
	return m.GetDefault(api.KeyTitle, "")
}

func createInlineErrorImage(en *ast.EmbedRefNode) *ast.EmbedRefNode {
func (e *evaluator) createInlineErrorImage(en *ast.EmbedRefNode) ast.InlineEmbedNode {
	errorZid := id.EmojiZid
	if gim := e.env.GetImageMaterial; gim != nil {
		zettel, err := e.port.GetZettel(box.NoEnrichContext(e.ctx), errorZid)
		if err != nil {
			panic(err)
		}
		inlines := en.Inlines
		if len(inlines) == 0 {
			if title := e.getTitle(zettel.Meta); title != "" {
				inlines = parser.ParseMetadata(title)
			}
		}
		syntax := e.getSyntax(zettel.Meta)
		return enrichImageNode(gim(zettel, syntax), inlines, en.Attrs, syntax)
	}
	en.Ref = ast.ParseReference(errorZid.String())
	if len(en.Inlines) == 0 {
		en.Inlines = parser.ParseMetadata("Error placeholder")
	}
	return en
}

func (e *evaluator) embedImage(en *ast.EmbedRefNode, zettel domain.Zettel) ast.InlineEmbedNode {
	syntax := e.getSyntax(zettel.Meta)
	if gim := e.env.GetImageMaterial; gim != nil {
		return enrichImageNode(gim(zettel, syntax), en.Inlines, en.Attrs, syntax)
	}
	en.Syntax = syntax
	return en
}

func enrichImageNode(result ast.InlineEmbedNode, in ast.InlineSlice, a zjson.Attributes, syntax string) ast.InlineEmbedNode {
	switch er := result.(type) {
	case *ast.EmbedRefNode:
		er.Inlines = in
		er.Attrs = a
		er.Syntax = syntax
	case *ast.EmbedBLOBNode:
		er.Inlines = in
		er.Attrs = a
	}
	return result
}

func createInlineErrorText(ref *ast.Reference, msgWords ...string) ast.InlineNode {
	text := strings.Join(msgWords, " ")
	if ref != nil {
		text += ": " + ref.String() + "."
	}
	ln := &ast.LiteralNode{
441
442
443
444
445
446
447
448

449
450
451
452
453
454
455
515
516
517
518
519
520
521

522
523
524
525
526
527
528
529







-
+







func (e *evaluator) evaluateEmbeddedInline(content []byte, syntax string) ast.InlineSlice {
	is := parser.ParseInlines(input.NewInput(content), syntax)
	ast.Walk(e, &is)
	return is
}

func (e *evaluator) evaluateEmbeddedZettel(zettel domain.Zettel) *ast.ZettelNode {
	zn := parser.ParseZettel(zettel, zettel.Meta.GetDefault(api.KeySyntax, ""), e.rtConfig)
	zn := parser.ParseZettel(zettel, e.getSyntax(zettel.Meta), e.rtConfig)
	ast.Walk(e, &zn.Ast)
	return zn
}

func findInlineSlice(bs *ast.BlockSlice, fragment string) ast.InlineSlice {
	if fragment == "" {
		return firstInlinesToEmbed(*bs)
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
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







+
+
+
+
+
-
+
+

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






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












func (fs *fragmentSearcher) Visit(node ast.Node) ast.Visitor {
	if len(fs.result) > 0 {
		return nil
	}
	switch n := node.(type) {
	case *ast.BlockSlice:
		for i, bn := range *n {
			if hn, ok := bn.(*ast.HeadingNode); ok && hn.Fragment == fs.fragment {
				fs.result = (*n)[i+1:].FirstParagraphInlines()
				return nil
			}
		fs.visitBlockSlice(n)
			ast.Walk(fs, bn)
		}
	case *ast.InlineSlice:
		for i, in := range *n {
			if mn, ok := in.(*ast.MarkNode); ok && mn.Fragment == fs.fragment {
				ris := skipSpaceNodes((*n)[i+1:])
				if len(mn.Inlines) > 0 {
		fs.visitInlineSlice(n)
					fs.result = append(ast.InlineSlice{}, mn.Inlines...)
					fs.result = append(fs.result, &ast.SpaceNode{Lexeme: " "})
					fs.result = append(fs.result, ris...)
				} else {
					fs.result = ris
				}
				return nil
			}
			ast.Walk(fs, in)
		}
	default:
		return fs
	}
	return nil
}

func (fs *fragmentSearcher) visitBlockSlice(bs *ast.BlockSlice) {
	for i, bn := range *bs {
		if hn, ok := bn.(*ast.HeadingNode); ok && hn.Fragment == fs.fragment {
			fs.result = (*bs)[i+1:].FirstParagraphInlines()
			return
		}
		ast.Walk(fs, bn)
	}
}

func (fs *fragmentSearcher) visitInlineSlice(is *ast.InlineSlice) {
	for i, in := range *is {
		if mn, ok := in.(*ast.MarkNode); ok && mn.Fragment == fs.fragment {
			ris := skipSpaceNodes((*is)[i+1:])
			if len(mn.Inlines) > 0 {
				fs.result = append(ast.InlineSlice{}, mn.Inlines...)
				fs.result = append(fs.result, &ast.SpaceNode{Lexeme: " "})
				fs.result = append(fs.result, ris...)
			} else {
				fs.result = ris
			}
			return
		}
		ast.Walk(fs, in)
	}
}

func skipSpaceNodes(ins ast.InlineSlice) ast.InlineSlice {
	for i, in := range ins {
		switch in.(type) {
		case *ast.SpaceNode:
		case *ast.BreakNode:
		default:
			return ins[i:]
		}
	}
	return nil
}

Changes to go.mod.

1
2
3

4
5
6
7
8
9
10
11





12
13

14
15
16

1
2

3
4
5






6
7
8
9
10
11

12
13
14

15


-
+


-
-
-
-
-
-
+
+
+
+
+

-
+


-
+
module zettelstore.de/z

go 1.18
go 1.17

require (
	codeberg.org/t73fde/sxpf v0.0.0-20220719090054-749a39d0a7a0
	github.com/fsnotify/fsnotify v1.5.4
	github.com/pascaldekloe/jwt v1.12.0
	github.com/yuin/goldmark v1.4.13
	golang.org/x/crypto v0.0.0-20220722155217-630584e8d5aa
	golang.org/x/term v0.0.0-20220722155259-a9ba230a4035
	github.com/fsnotify/fsnotify v1.5.1
	github.com/pascaldekloe/jwt v1.10.0
	github.com/yuin/goldmark v1.4.8
	golang.org/x/crypto v0.0.0-20220307211146-efcb8507fb70
	golang.org/x/term v0.0.0-20210927222741-03fcf44c2211
	golang.org/x/text v0.3.7
	zettelstore.de/c v0.0.0-20220729135959-532261810eac
	zettelstore.de/c v0.0.0-20220308145137-122c412c3a99
)

require golang.org/x/sys v0.0.0-20220608164250-635b8c9b7f68 // indirect
require golang.org/x/sys v0.0.0-20210630005230-0f9fa26af87c // indirect

Changes to go.sum.

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


















16
17

18
19

















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


22
23
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+


+
-
-
+
+
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.5.4 h1:jRbGcIw6P2Meqdwuo0H1p6JVLbL5DHKAKlYndzMwVZI=
github.com/fsnotify/fsnotify v1.5.4/go.mod h1:OVB6XrOHzAwXMpEM7uPOzcehqUV2UqJxmVXmkdnm1bU=
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.4.13 h1:fVcFKWvrslecOb/tg+Cc05dkeYx540o0FuFt3nUVDoE=
github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY=
golang.org/x/crypto v0.0.0-20220722155217-630584e8d5aa h1:zuSxTR4o9y82ebqCUJYNGJbGPo6sKVl54f/TVDObg1c=
golang.org/x/crypto v0.0.0-20220722155217-630584e8d5aa/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4=
golang.org/x/sys v0.0.0-20220412211240-33da011f77ad/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220608164250-635b8c9b7f68 h1:z8Hj/bl9cOV2grsOpEaQFUaly0JWN3i97mo3jXKJNp0=
golang.org/x/sys v0.0.0-20220608164250-635b8c9b7f68/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/term v0.0.0-20220722155259-a9ba230a4035 h1:Q5284mrmYTpACcm+eAKjKJH48BBwSyfJqmmGDTtT8Vc=
golang.org/x/term v0.0.0-20220722155259-a9ba230a4035/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
github.com/fsnotify/fsnotify v1.5.1 h1:mZcQUHVQUQWoPXXtuf9yuEXKudkV2sx1E06UadKWpgI=
github.com/fsnotify/fsnotify v1.5.1/go.mod h1:T3375wBYaZdLLcVNkcVbzGHY7f1l/uK5T5Ai1i3InKU=
github.com/pascaldekloe/jwt v1.10.0 h1:ktcIUV4TPvh404R5dIBEnPCsSwj0sqi3/0+XafE5gJs=
github.com/pascaldekloe/jwt v1.10.0/go.mod h1:TKhllgThT7TOP5rGr2zMLKEDZRAgJfBbtKyVeRsNB9A=
github.com/yuin/goldmark v1.4.8 h1:zHPiabbIRssZOI0MAzJDHsyvG4MXCGqVaMOwR+HeoQQ=
github.com/yuin/goldmark v1.4.8/go.mod h1:rmuwmfZ0+bvzB24eSC//bk1R1Zp3hM0OXYv/G2LIilg=
golang.org/x/crypto v0.0.0-20220307211146-efcb8507fb70 h1:syTAU9FwmvzEoIYMqcPHOcVm4H3U5u90WsvuYgwpETU=
golang.org/x/crypto v0.0.0-20220307211146-efcb8507fb70/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4=
golang.org/x/net v0.0.0-20211112202133-69e39bad7dc2/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20210630005230-0f9fa26af87c h1:F1jZWGFhYfh0Ci55sIpILtKKK8p3i2/krTr0H1rg74I=
golang.org/x/sys v0.0.0-20210630005230-0f9fa26af87c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211 h1:JGgROgKl9N8DuW20oFS5gxc+lE67/N3FcwmBPMe7ArY=
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/text v0.3.7 h1:olpwvP2KacW1ZWvsR7uQhoyTYvKAupfQrRGBFM352Gk=
golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
zettelstore.de/c v0.0.0-20220729135959-532261810eac h1:T8GP4P8pHNfjQ2lXtpRXBmNF685xI0kraoou3tKqH2Y=
zettelstore.de/c v0.0.0-20220729135959-532261810eac/go.mod h1:8wPlP6rYiXkfoCNE84K3bmhSAKHz15+xWSHMH26JAU0=
zettelstore.de/c v0.0.0-20220308145137-122c412c3a99 h1:0WknFoNBwtwD1pqUq4XPGtvkqyE0nN8tJJAPTCjHt/8=
zettelstore.de/c v0.0.0-20220308145137-122c412c3a99/go.mod h1:Hx/qzHCaQ8zzXEzBglBj/2aGkQpBQG81/4XztCIGJ84=

Changes to input/input.go.

1
2

3
4

5
6
7
8
9
10
11
1

2
3

4
5
6
7
8
9
10
11

-
+

-
+







//-----------------------------------------------------------------------------
// Copyright (c) 2020-2022 Detlef Stern
// Copyright (c) 2020-2021 Detlef Stern
//
// This file is part of Zettelstore.
// This file is part of zettelstore.
//
// Zettelstore is licensed 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 input provides an abstraction for data to be read.
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
187
188
189
190
191
192
193

194
195
196
197
198
199
200
201
202
203


204








205




206




207










-
+









-
-

-
-
-
-
-
-
-
-
+
-
-
-
-
+
-
-
-
-
+
-
-
-
		}
		inp.Next()
	}
}
func (inp *Input) scanEntityNamed(pos int) (string, bool) {
	for {
		switch inp.Ch {
		case EOS, '\n', '\r', '&':
		case EOS, '\n', '\r':
			return "", false
		case ';':
			inp.Next()
			es := string(inp.Src[pos:inp.Pos])
			ues := html.UnescapeString(es)
			if es == ues {
				return "", false
			}
			return ues, true
		default:
			inp.Next()
		}
	}
}

// ScanLineContent reads the reaining input stream and interprets it as lines of text.
func (inp *Input) ScanLineContent() []byte {
	result := make([]byte, 0, len(inp.Src)-inp.Pos+1)
	for {
		inp.EatEOL()
		inp.Next()
		posL := inp.Pos
		if inp.Ch == EOS {
			return result
		}
	}
		inp.SkipToEOL()
		if len(result) > 0 {
			result = append(result, '\n')
		}
}
		result = append(result, inp.Src[posL:inp.Pos]...)
	}
}

Changes to input/input_test.go.

1
2

3
4

5
6
7
8
9
10
11
1

2
3

4
5
6
7
8
9
10
11

-
+

-
+







//-----------------------------------------------------------------------------
// Copyright (c) 2020-2022 Detlef Stern
// Copyright (c) 2020-2021 Detlef Stern
//
// This file is part of Zettelstore.
// This file is part of zettelstore.
//
// Zettelstore is licensed 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 input_test provides some unit-tests for reading data.
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
63
64
65
66
67
68
69




















-
-
-
-
-
-
-
-
-
-
-
-
-
			continue
		}
		if tc.exp != got {
			t.Errorf("ID=%d, text=%q: expected %q, but got %q", id, tc.text, tc.exp, got)
		}
	}
}

func TestScanIllegalEntity(t *testing.T) {
	t.Parallel()
	testcases := []string{"", "a", "& Input &rarr;"}
	for i, tc := range testcases {
		inp := input.NewInput([]byte(tc))
		got, ok := inp.ScanEntity()
		if ok {
			t.Errorf("%d: scanning %q was unexpected successful, got %q", i, tc, got)
			continue
		}
	}
}

Changes to input/runes.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-2022 Detlef Stern
// Copyright (c) 2020-2021 Detlef Stern
//
// This file is part of Zettelstore.
// This file is part of zettelstore.
//
// Zettelstore is licensed 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 input provides an abstraction for data to be read.
package input

// IsSpace returns true if rune is a whitespace.
func IsSpace(ch rune) bool {
	switch ch {
	case ' ', '\t':
		return true
	}
	return false
}

Changes to kernel/impl/cfg.go.

32
33
34
35
36
37
38



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



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







+
+
+

















+
+
+















-
+













+
+
+





-
+








// Predefined Metadata keys for runtime configuration
// See: https://zettelstore.de/manual/h/00001004020000
const (
	keyDefaultCopyright  = "default-copyright"
	keyDefaultLang       = "default-lang"
	keyDefaultLicense    = "default-license"
	keyDefaultRole       = "default-role"
	keyDefaultSyntax     = "default-syntax"
	keyDefaultTitle      = "default-title"
	keyDefaultVisibility = "default-visibility"
	keyExpertMode        = "expert-mode"
	keyFooterHTML        = "footer-html"
	keyHomeZettel        = "home-zettel"
	keyMarkerExternal    = "marker-external"
	keyMaxTransclusions  = "max-transclusions"
	keySiteName          = "site-name"
	keyYAMLHeader        = "yaml-header"
	keyZettelFileSyntax  = "zettel-file-syntax"
)

func (cs *configService) Initialize(logger *logger.Logger) {
	cs.logger = logger
	cs.descr = descriptionMap{
		keyDefaultCopyright: {"Default copyright", parseString, true},
		keyDefaultLang:      {"Default language", parseString, true},
		keyDefaultLicense:   {"Default license", parseString, true},
		keyDefaultRole:      {"Default role", parseString, true},
		keyDefaultSyntax:    {"Default syntax", parseString, true},
		keyDefaultTitle:     {"Default title", parseString, true},
		keyDefaultVisibility: {
			"Default zettel visibility",
			func(val string) interface{} {
				vis := meta.GetVisibility(val)
				if vis == meta.VisibilityUnknown {
					return nil
				}
				return vis
			},
			true,
		},
		keyExpertMode:       {"Expert mode", parseBool, true},
		keyFooterHTML:       {"Footer HTML", parseString, true},
		keyHomeZettel:       {"Home zettel", parseZid, true},
		keyMarkerExternal:   {"Marker external URL", parseString, true},
		keyMaxTransclusions: {"Maximum transclusions", parseInt64, true},
		keyMaxTransclusions: {"Maximum transclusions", parseInt, true},
		keySiteName:         {"Site name", parseString, true},
		keyYAMLHeader:       {"YAML header", parseBool, true},
		keyZettelFileSyntax: {
			"Zettel file syntax",
			func(val string) interface{} { return strings.Fields(val) },
			true,
		},
		kernel.ConfigSimpleMode: {"Simple mode", cs.noFrozen(parseBool), true},
	}
	cs.next = interfaceMap{
		keyDefaultCopyright:     "",
		keyDefaultLang:          api.ValueLangEN,
		keyDefaultLicense:       "",
		keyDefaultRole:          api.ValueRoleZettel,
		keyDefaultSyntax:        api.ValueSyntaxZmk,
		keyDefaultTitle:         "Untitled",
		keyDefaultVisibility:    meta.VisibilityLogin,
		keyExpertMode:           false,
		keyFooterHTML:           "",
		keyHomeZettel:           id.DefaultHomeZid,
		keyMarkerExternal:       "&#10138;",
		keyMaxTransclusions:     int64(1024),
		keyMaxTransclusions:     1024,
		keySiteName:             "Zettelstore",
		keyYAMLHeader:           false,
		keyZettelFileSyntax:     nil,
		kernel.ConfigSimpleMode: false,
	}
}
func (cs *configService) GetLogger() *logger.Logger { return cs.logger }
176
177
178
179
180
181
182



183
184
185
186
187
188
189
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201







+
+
+







	}
}

var defaultKeys = map[string]string{
	api.KeyCopyright:  keyDefaultCopyright,
	api.KeyLang:       keyDefaultLang,
	api.KeyLicense:    keyDefaultLicense,
	api.KeyRole:       keyDefaultRole,
	api.KeySyntax:     keyDefaultSyntax,
	api.KeyTitle:      keyDefaultTitle,
	api.KeyVisibility: keyDefaultVisibility,
}

// AddDefaultValues enriches the given meta data with its default values.
func (cfg *myConfig) AddDefaultValues(m *meta.Meta) *meta.Meta {
	if cfg == nil {
		return m
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
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







+
+
+
+
+
+
+
+
+


















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







func (cfg *myConfig) getBool(key string) bool {
	cfg.mx.RLock()
	val := cfg.data.GetBool(key)
	cfg.mx.RUnlock()
	return val
}

// GetDefaultTitle returns the current value of the "default-title" key.
func (cfg *myConfig) GetDefaultTitle() string { return cfg.getString(keyDefaultTitle) }

// GetDefaultRole returns the current value of the "default-role" key.
func (cfg *myConfig) GetDefaultRole() string { return cfg.getString(keyDefaultRole) }

// GetDefaultSyntax returns the current value of the "default-syntax" key.
func (cfg *myConfig) GetDefaultSyntax() string { return cfg.getString(keyDefaultSyntax) }

// GetDefaultLang returns the current value of the "default-lang" key.
func (cfg *myConfig) GetDefaultLang() string { return cfg.getString(keyDefaultLang) }

// GetSiteName returns the current value of the "site-name" key.
func (cfg *myConfig) GetSiteName() string { return cfg.getString(keySiteName) }

// GetHomeZettel returns the value of the "home-zettel" key.
func (cfg *myConfig) GetHomeZettel() id.Zid {
	val := cfg.getString(keyHomeZettel)
	if homeZid, err := id.Parse(val); err == nil {
		return homeZid
	}
	cfg.mx.RLock()
	val, _ = cfg.orig.Get(keyHomeZettel)
	homeZid, _ := id.Parse(val)
	cfg.mx.RUnlock()
	return homeZid
}

// GetDefaultVisibility returns the default value for zettel visibility.
func (cfg *myConfig) GetDefaultVisibility() meta.Visibility {
	val := cfg.getString(keyDefaultVisibility)
	if vis := meta.GetVisibility(val); vis != meta.VisibilityUnknown {
		return vis
	}
	cfg.mx.RLock()
	val, _ = cfg.orig.Get(keyDefaultVisibility)
	vis := meta.GetVisibility(val)
	cfg.mx.RUnlock()
	return vis
}

// GetMaxTransclusions return the maximum number of indirect transclusions.
func (cfg *myConfig) GetMaxTransclusions() int {
	const defaultValue = 1024
	cfg.mx.RLock()
	val := cfg.data.GetNumber(keyMaxTransclusions, defaultValue)
	cfg.mx.RUnlock()
278
279
280
281
282
283
284
285
286
287
288
289


290
291
292
293
294
295
312
313
314
315
316
317
318





319
320













-
-
-
-
-
+
+
-
-
-
-
-
-
// GetVisibility returns the visibility value, or "login" if none is given.
func (cfg *myConfig) GetVisibility(m *meta.Meta) meta.Visibility {
	if val, ok := m.Get(api.KeyVisibility); ok {
		if vis := meta.GetVisibility(val); vis != meta.VisibilityUnknown {
			return vis
		}
	}

	val := cfg.getString(keyDefaultVisibility)
	if vis := meta.GetVisibility(val); vis != meta.VisibilityUnknown {
		return vis
	}
	return cfg.GetDefaultVisibility()
}
	cfg.mx.RLock()
	val, _ = cfg.orig.Get(keyDefaultVisibility)
	vis := meta.GetVisibility(val)
	cfg.mx.RUnlock()
	return vis
}

Changes to kernel/impl/cmd.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
1

2
3

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

22
23
24
25
26
27
28

-
+

-
+

















-







//-----------------------------------------------------------------------------
// Copyright (c) 2021-2022 Detlef Stern
// Copyright (c) 2021 Detlef Stern
//
// This file is part of Zettelstore.
// This file is part of zettelstore.
//
// Zettelstore is licensed 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 impl

import (
	"fmt"
	"io"
	"os"
	"runtime/metrics"
	"sort"
	"strconv"
	"strings"

	"zettelstore.de/c/maps"
	"zettelstore.de/z/kernel"
	"zettelstore.de/z/logger"
	"zettelstore.de/z/strfun"
)

type cmdSession struct {
	w        io.Writer
195
196
197
198
199
200
201
202








203
204
205
206
207
208
209
194
195
196
197
198
199
200

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







-
+
+
+
+
+
+
+
+







	},
	"start": {"start service", cmdStart},
	"stat":  {"show service statistics", cmdStat},
	"stop":  {"stop service", cmdStop},
}

func cmdHelp(sess *cmdSession, _ string, _ []string) bool {
	cmds := maps.Keys(commands)
	cmds := make([]string, 0, len(commands))
	for key := range commands {
		if key == "" {
			continue
		}
		cmds = append(cmds, key)
	}
	sort.Strings(cmds)
	table := [][]string{{"Command", "Description"}}
	for _, cmd := range cmds {
		table = append(table, []string{cmd, commands[cmd].Text})
	}
	sess.printTable(table)
	return true
}
554
555
556
557
558
559
560
561








562
563
564
565
566
567
568
569
560
561
562
563
564
565
566

567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582







-
+
+
+
+
+
+
+
+








			table = append(table, []string{env[:pos], env[pos+1:]})
		}
	}
	sess.printTable(table)
	return true
}

func sortedServiceNames(sess *cmdSession) []string { return maps.Keys(sess.kern.srvNames) }
func sortedServiceNames(sess *cmdSession) []string {
	names := make([]string, 0, len(sess.kern.srvNames))
	for name := range sess.kern.srvNames {
		names = append(names, name)
	}
	sort.Strings(names)
	return names
}

func getService(sess *cmdSession, name string) (serviceData, bool) {
	srvD, found := sess.kern.srvNames[name]
	if !found {
		sess.println("Unknown service", name)
	}
	return srvD, found
}

Changes to kernel/impl/config.go.

1
2

3
4

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

2
3

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

20
21
22
23
24
25
26

-
+

-
+















-







//-----------------------------------------------------------------------------
// Copyright (c) 2021-2022 Detlef Stern
// Copyright (c) 2021 Detlef Stern
//
// This file is part of Zettelstore.
// This file is part of zettelstore.
//
// Zettelstore is licensed 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 impl

import (
	"fmt"
	"sort"
	"strconv"
	"strings"
	"sync"

	"zettelstore.de/c/maps"
	"zettelstore.de/z/domain/id"
	"zettelstore.de/z/kernel"
	"zettelstore.de/z/logger"
)

type parseFunc func(string) interface{}
type configDescription struct {
51
52
53
54
55
56
57

58




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

58
59
60
61
62
63
64
65
66
67
68







+
-
+
+
+
+







	cur      interfaceMap
	next     interfaceMap
}

func (cfg *srvConfig) ConfigDescriptions() []serviceConfigDescription {
	cfg.mxConfig.RLock()
	defer cfg.mxConfig.RUnlock()
	keys := make([]string, 0, len(cfg.descr))
	keys := maps.Keys(cfg.descr)
	for k := range cfg.descr {
		keys = append(keys, k)
	}
	sort.Strings(keys)
	result := make([]serviceConfigDescription, 0, len(keys))
	for _, k := range keys {
		text := cfg.descr[k].text
		if strings.HasSuffix(k, "-") {
			text = text + " (list)"
		}
		result = append(result, serviceConfigDescription{Key: k, Descr: text})
220
221
222
223
224
225
226
227
228
229




230
231

232
233
234
235
236
237
238
239
223
224
225
226
227
228
229



230
231
232
233
234

235
236
237
238
239
240
241
242
243







-
-
-
+
+
+
+

-
+








	switch val[0] {
	case '0', 'f', 'F', 'n', 'N':
		return false
	}
	return true
}

func parseInt64(val string) any {
	if u64, err := strconv.ParseInt(val, 10, 64); err == nil {
		return u64
func parseInt(val string) interface{} {
	i, err := strconv.Atoi(val)
	if err == nil {
		return i
	}
	return nil
	return 0
}

func parseZid(val string) interface{} {
	if zid, err := id.Parse(val); err == nil {
		return zid
	}
	return id.Invalid
}

Changes to kernel/impl/core.go.

11
12
13
14
15
16
17

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

22
23
24
25
26
27
28







+



-







package impl

import (
	"fmt"
	"net"
	"os"
	"runtime"
	"sort"
	"sync"
	"time"

	"zettelstore.de/c/maps"
	"zettelstore.de/z/kernel"
	"zettelstore.de/z/logger"
	"zettelstore.de/z/strfun"
)

type coreService struct {
	srvConfig
95
96
97
98
99
100
101

102




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

103
104
105
106
107
108
109
110
111
112
113







+
-
+
+
+
+







func (cs *coreService) Stop(*myKernel) {
	cs.started = false
}

func (cs *coreService) GetStatistics() []kernel.KeyValue {
	cs.mxRecover.RLock()
	defer cs.mxRecover.RUnlock()
	names := make([]string, 0, len(cs.mapRecover))
	names := maps.Keys(cs.mapRecover)
	for n := range cs.mapRecover {
		names = append(names, n)
	}
	sort.Strings(names)
	result := make([]kernel.KeyValue, 0, 3*len(names))
	for _, n := range names {
		ri := cs.mapRecover[n]
		result = append(
			result,
			kernel.KeyValue{
				Key:   fmt.Sprintf("Recover %q / Count", n),

Changes to kernel/impl/web.go.

1
2

3
4

5
6
7
8
9
10
11
1

2
3

4
5
6
7
8
9
10
11

-
+

-
+







//-----------------------------------------------------------------------------
// Copyright (c) 2021-2022 Detlef Stern
// Copyright (c) 2021 Detlef Stern
//
// This file is part of Zettelstore.
// This file is part of zettelstore.
//
// Zettelstore is licensed 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 impl
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
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







+
+
+
+
+
+
+
+
+
+

















-








type webService struct {
	srvConfig
	mxService   sync.RWMutex
	srvw        server.Server
	setupServer kernel.SetupWebServerFunc
}

// Constants for web service keys.
const (
	WebSecureCookie      = "secure"
	WebListenAddress     = "listen"
	WebPersistentCookie  = "persistent"
	WebTokenLifetimeAPI  = "api-lifetime"
	WebTokenLifetimeHTML = "html-lifetime"
	WebURLPrefix         = "prefix"
)

func (ws *webService) Initialize(logger *logger.Logger) {
	ws.logger = logger
	ws.descr = descriptionMap{
		kernel.WebListenAddress: {
			"Listen address",
			func(val string) interface{} {
				host, port, err := net.SplitHostPort(val)
				if err != nil {
					return nil
				}
				if _, err = net.LookupPort("tcp", port); err != nil {
					return nil
				}
				return net.JoinHostPort(host, port)
			},
			true},
		kernel.WebMaxRequestSize:   {"Max Request Size", parseInt64, true},
		kernel.WebPersistentCookie: {"Persistent cookie", parseBool, true},
		kernel.WebSecureCookie:     {"Secure cookie", parseBool, true},
		kernel.WebTokenLifetimeAPI: {
			"Token lifetime API",
			makeDurationParser(10*time.Minute, 0, 1*time.Hour),
			true,
		},
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
78
79
80
81
82
83
84

85
86
87
88
89
90
91







-







				return nil
			},
			true,
		},
	}
	ws.next = interfaceMap{
		kernel.WebListenAddress:     "127.0.0.1:23123",
		kernel.WebMaxRequestSize:    int64(16 * 1024 * 1024),
		kernel.WebPersistentCookie:  false,
		kernel.WebSecureCookie:      true,
		kernel.WebTokenLifetimeAPI:  1 * time.Hour,
		kernel.WebTokenLifetimeHTML: 10 * time.Minute,
		kernel.WebURLPrefix:         "/",
	}
}
101
102
103
104
105
106
107
108
109
110
111

112
113

114
115
116
117
118
119
120
109
110
111
112
113
114
115




116


117
118
119
120
121
122
123
124







-
-
-
-
+
-
-
+







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

func (ws *webService) Start(kern *myKernel) error {
	listenAddr := ws.GetNextConfig(kernel.WebListenAddress).(string)
	urlPrefix := ws.GetNextConfig(kernel.WebURLPrefix).(string)
	persistentCookie := ws.GetNextConfig(kernel.WebPersistentCookie).(bool)
	secureCookie := ws.GetNextConfig(kernel.WebSecureCookie).(bool)
	maxRequestSize := ws.GetNextConfig(kernel.WebMaxRequestSize).(int64)
	if maxRequestSize < 1024 {
		maxRequestSize = 1024
	}


	srvw := impl.New(ws.logger, listenAddr, urlPrefix, persistentCookie, secureCookie, maxRequestSize, kern.auth.manager)
	srvw := impl.New(ws.logger, listenAddr, urlPrefix, persistentCookie, secureCookie, kern.auth.manager)
	err := kern.web.setupServer(srvw, kern.box.manager, kern.auth.manager, kern.cfg.rtConfig)
	if err != nil {
		ws.logger.Fatal().Err(err).Msg("Unable to create")
		return err
	}
	if kern.core.GetNextConfig(kernel.CoreDebug).(bool) {
		srvw.SetDebug()

Changes to kernel/kernel.go.

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

170
171
172
173
174
175
176







-







	BoxDirTypeSimple = "simple"
)

// Constants for web service keys.
const (
	WebListenAddress     = "listen"
	WebPersistentCookie  = "persistent"
	WebMaxRequestSize    = "max-request-size"
	WebSecureCookie      = "secure"
	WebTokenLifetimeAPI  = "api-lifetime"
	WebTokenLifetimeHTML = "html-lifetime"
	WebURLPrefix         = "prefix"
)

// KeyDescrValue is a triple of config data.

Changes to logger/message.go.

1
2

3
4

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

2
3

4
5
6
7
8
9
10
11
12
13
14

15
16
17
18
19
20
21

-
+

-
+










-







//-----------------------------------------------------------------------------
// Copyright (c) 2022 Detlef Stern
// Copyright (c) 2021 Detlef Stern
//
// This file is part of Zettelstore.
// This file is part of zettelstore.
//
// Zettelstore is licensed 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 logger

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

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

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











125
126
127
128
129
130
131







-
-
-
-
-
-
-
-
-
-
-







				}
			}
		}
	}
	return m
}

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

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

// Msg add the given text to the message and writes it to the log.
func (m *Message) Msg(text string) {

Changes to parser/blob/blob.go.

46
47
48
49
50
51
52
53

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

53
54
55
56
57
58
59
60
61







-
+








	})
}

func parseBlocks(inp *input.Input, m *meta.Meta, syntax string) ast.BlockSlice {
	if p := parser.Get(syntax); p != nil {
		syntax = p.Name
	}
	title := m.GetDefault(api.KeyTitle, "")
	title, _ := m.Get(api.KeyTitle)
	return ast.BlockSlice{&ast.BLOBNode{
		Title:  title,
		Syntax: syntax,
		Blob:   []byte(inp.Src),
	}}
}

func parseInlines(*input.Input, string) ast.InlineSlice { return nil }

Changes to parser/cleaner/cleaner.go.

11
12
13
14
15
16
17

18
19

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

32
33
34
35
36
37
38
39
40
41
42
43

44
45
46
47
48
49
50
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







+

-
+











-
+











-
+







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

import (
	"bytes"
	"strconv"

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

// CleanBlockSlice cleans the given block list.
func CleanBlockSlice(bs *ast.BlockSlice) { cleanNode(bs) }

// CleanInlineSlice cleans the given inline list.
func CleanInlineSlice(is *ast.InlineSlice) { cleanNode(is) }

func cleanNode(n ast.Node) {
	cv := cleanVisitor{
		textEnc: textenc.Create(),
		textEnc: encoder.Create(api.EncoderText, nil),
		hasMark: false,
		doMark:  false,
	}
	ast.Walk(&cv, n)
	if cv.hasMark {
		cv.doMark = true
		ast.Walk(&cv, n)
	}
}

type cleanVisitor struct {
	textEnc *textenc.Encoder
	textEnc encoder.Encoder
	ids     map[string]ast.Node
	hasMark bool
	doMark  bool
}

func (cv *cleanVisitor) Visit(node ast.Node) ast.Visitor {
	switch n := node.(type) {

Changes to parser/draw/canvas.go.

26
27
28
29
30
31
32
33

34
35
36
37
38
39
40
41
42
43
44








45

46
47
48
49
50
51
52
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







-
+











+
+
+
+
+
+
+
+
-
+







	"sort"
	"unicode/utf8"
)

// newCanvas returns a new Canvas, initialized from the provided data. If tabWidth is set to a non-negative
// value, that value will be used to convert tabs to spaces within the grid. Creation of the Canvas
// can fail if the diagram contains invalid UTF-8 sequences.
func newCanvas(data []byte) (*canvas, error) {
func newCanvas(data []byte, tabWidth int) (*canvas, error) {
	c := &canvas{}

	lines := bytes.Split(data, []byte("\n"))
	c.siz.Y = len(lines)

	// Diagrams will often not be padded to a uniform width. To overcome this, we scan over
	// each line and figure out which is the longest. This becomes the width of the canvas.
	for i, line := range lines {
		if ok := utf8.Valid(line); !ok {
			return nil, fmt.Errorf("invalid UTF-8 encoding on line %d", i)
		}

		l, err := expandTabs(line, tabWidth)
		if err != nil {
			return nil, err
		}

		lines[i] = l

		if i1 := utf8.RuneCount(line); i1 > c.siz.X {
		if i1 := utf8.RuneCount(lines[i]); i1 > c.siz.X {
			c.siz.X = i1
		}
	}

	c.grid = make([]char, c.siz.X*c.siz.Y)
	c.visited = make([]bool, c.siz.X*c.siz.Y)
	for y, line := range lines {
62
63
64
65
66
67
68














































69
70
71
72
73
74
75
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







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







			c.grid[y*c.siz.X+x] = ' '
		}
	}

	c.findObjects()
	return c, nil
}

// The expandTabs function pads tab characters to the specified width of spaces for the provided
// line of input. We cannot simply pad based on byte-offset since our input is UTF-8 encoded.
// Fortunately, we can assume that this function is called that the line contains only valid
// UTF-8 sequences. We first decode the line rune-wise, and use individual runes to figure out
// where we are within the line. When we encounter a tab character, we expand based on our rune
// index.
func expandTabs(line []byte, tabWidth int) ([]byte, error) {
	// Initial sizing of our output slice assumes no UTF-8 bytes or tabs, since this is often
	// the common case.
	out := make([]byte, 0, len(line))

	// pos tracks our position in the input byte slice, while index tracks our position in the
	// resulting output slice.
	pos := 0
	index := 0
	for _, c := range line {
		if c == '\t' {
			// Loop over the remaining space count for this particular tabstop until
			// the next, replacing each position with a space.
			for s := tabWidth - (pos % tabWidth); s > 0; s-- {
				out = append(out, ' ')
				index++
			}
			pos++
		} else {
			// We need to know the byte length of the rune at this position so that we
			// can account for our tab expansion properly. So we first decode the rune
			// at this position to get its length in bytes, plop that rune back into our
			// output slice, and account accordingly.
			r, l := utf8.DecodeRune(line[pos:])
			if r == utf8.RuneError {
				return nil, fmt.Errorf("invalid rune at byte offset %d; rune offset %d", pos, index)
			}

			enc := make([]byte, l)
			utf8.EncodeRune(enc, r)
			out = append(out, enc...)

			pos += l
			index++
		}
	}

	return out, nil
}

// canvas is the parsed source data.
type canvas struct {
	// (0,0) is top left.
	grid           []char
	visited        []bool
	objs           objects
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
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







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

-
+





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

-
+
-

-






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







func (c *canvas) objects() objects { return c.objs }

// size returns the visual dimensions of the Canvas.
func (c *canvas) size() image.Point { return c.siz }

// findObjects finds all objects (lines, polygons, and text) within the underlying grid.
func (c *canvas) findObjects() {
	c.findPaths()
	p := point{}
	c.findTexts()
	sort.Sort(c.objs)
}


// findPaths by starting with a point that wasn't yet visited, beginning at the top
// left of the grid.
	// Find any new paths by starting with a point that wasn't yet visited, beginning at the top
	// left of the grid.
func (c *canvas) findPaths() {
	for y := 0; y < c.siz.Y; y++ {
		p := point{y: y}
		p.y = y
		for x := 0; x < c.siz.X; x++ {
			p.x = x
			if c.isVisited(p) {
				continue
			}
			ch := c.at(p)
			if ch := c.at(p); ch.isPathStart() {
			if !ch.isPathStart() {
				continue
			}

			// Found the start of a one or multiple connected paths. Traverse all
			// connecting points. This will generate multiple objects if multiple
			// paths (either open or closed) are found.
			c.visit(p)
			objs := c.scanPath([]point{p})
			for _, obj := range objs {
				// For all points in all objects found, mark the points as visited.
				for _, p := range obj.Points() {
					c.visit(p)
				}
			}
			c.objs = append(c.objs, objs...)
		}
	}
}
				// Found the start of a one or multiple connected paths. Traverse all
				// connecting points. This will generate multiple objects if multiple
				// paths (either open or closed) are found.
				c.visit(p)
				objs := c.scanPath([]point{p})
				for _, obj := range objs {
					// For all points in all objects found, mark the points as visited.
					for _, p := range obj.Points() {
						c.visit(p)
					}
				}
				c.objs = append(c.objs, objs...)
			}
		}
	}

// findTexts with a second pass through the grid attempts to identify any text within the grid.
	// A second pass through the grid attempts to identify any text within the grid.
func (c *canvas) findTexts() {
	for y := 0; y < c.siz.Y; y++ {
		p := point{}
		p.y = y
		for x := 0; x < c.siz.X; x++ {
			p.x = x
			if c.isVisited(p) {
				continue
			}
			ch := c.at(p)
			if !ch.isTextStart() {
			if ch := c.at(p); ch.isTextStart() {
				obj := c.scanText(p)
				continue
			}


			// scanText will return nil if the text at this area is simply
			// setting options on a container object.
				// scanText will return nil if the text at this area is simply
				// setting options on a container object.
			obj := c.scanText(p)
			if obj == nil {
				continue
			}
			for _, p := range obj.Points() {
				c.visit(p)
			}
			c.objs = append(c.objs, obj)
		}
	}
				if obj == nil {
					continue
				}
				for _, p := range obj.Points() {
					c.visit(p)
				}
				c.objs = append(c.objs, obj)
			}
		}
	}

	sort.Sort(c.objs)
}

// scanPath tries to complete a total path (for lines or polygons) starting with some partial path.
// It recurses when it finds multiple unvisited outgoing paths.
func (c *canvas) scanPath(points []point) objects {
	cur := points[len(points)-1]
	next := c.next(cur)
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
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







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












+
+
+
+
+












+
+
+
+
+







	// Our caller must have called c.visit prior to calling this function.
	if !c.isVisited(pos) {
		panic(fmt.Errorf("internal error; revisiting %s", pos))
	}

	var out []point

	nextHorizontal := func(p point) {
		if !c.isVisited(p) && c.at(p).canHorizontal() {
	ch := c.at(pos)
	if ch.canHorizontal() {
			out = append(out, p)
		}
	}
	nextVertical := func(p point) {
		if !c.isVisited(p) && c.at(p).canVertical() {
			out = append(out, p)
		}
	}
		nextHorizontal := func(p point) {
			if !c.isVisited(p) && c.at(p).canHorizontal() {
				out = append(out, p)
			}
		}
	nextDiagonal := func(from, to point) {
		if !c.isVisited(to) && c.at(to).canDiagonalFrom(c.at(from)) {
			out = append(out, to)
		}
	}

	ch := c.at(pos)
	if ch.canHorizontal() {
		if c.canLeft(pos) {
			n := pos
			n.x--
			nextHorizontal(n)
		}
		if c.canRight(pos) {
			n := pos
			n.x++
			nextHorizontal(n)
		}
	}
	if ch.canVertical() {
		nextVertical := func(p point) {
			if !c.isVisited(p) && c.at(p).canVertical() {
				out = append(out, p)
			}
		}
		if c.canUp(pos) {
			n := pos
			n.y--
			nextVertical(n)
		}
		if c.canDown(pos) {
			n := pos
			n.y++
			nextVertical(n)
		}
	}
	if c.canDiagonal(pos) {
		nextDiagonal := func(from, to point) {
			if !c.isVisited(to) && c.at(to).canDiagonalFrom(c.at(from)) {
				out = append(out, to)
			}
		}
		if c.canUp(pos) {
			if c.canLeft(pos) {
				n := pos
				n.x--
				n.y--
				nextDiagonal(pos, n)
			}

Changes to parser/draw/canvas_test.go.

297
298
299
300
301
302
303
304
305
306



307
308

309
310

311
312
313
314
315
316
317
297
298
299
300
301
302
303



304
305
306
307

308
309

310
311
312
313
314
315
316
317







-
-
-
+
+
+

-
+

-
+







			false,
		},

		// 9 Indented box
		{
			[]string{
				"",
				" +-+",
				" | |",
				" +-+",
				"\t+-+",
				"\t| |",
				"\t+-+",
			},
			[]string{"Path{[(1,1) (2,1) (3,1) (3,2) (3,3) (2,3) (1,3) (1,2)]}"},
			[]string{"Path{[(9,1) (10,1) (11,1) (11,2) (11,3) (10,3) (9,3) (9,2)]}"},
			[]string{""},
			[][]point{{{x: 1, y: 1}, {x: 3, y: 1}, {x: 3, y: 3}, {x: 1, y: 3}}},
			[][]point{{{x: 9, y: 1}, {x: 11, y: 1}, {x: 11, y: 3}, {x: 9, y: 3}}},
			false,
		},

		// 10 Diagonal lines with arrows
		{
			[]string{
				"^          ^",
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
442
443
444
445
446
447
448

449

























450

451
452
453
454
455
456
457
458







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

-
+







					{x: 11, y: 2, hint: 0},
					{x: 12, y: 2, hint: 0},
					{x: 13, y: 2, hint: 0},
				},
			},
			true,
		},

	}
		// 14 Multiple closed path on one object
		{
			[]string{
				"+-+-+",
				"| | |",
				"+-+-+",
			},
			[]string{
				"Path{[(0,0) (1,0) (2,0) (3,0) (4,0) (4,1) (4,2) (3,2) (2,2) (1,2) (0,2) (0,1)]}",
				"Path{[(0,0) (1,0) (2,0) (3,0) (4,0) (4,1) (4,2) (3,2) (2,2) (2,1)]}", // TODO (2,0)
			},
			[]string{"", ""},
			[][]point{
				{
					{0, 0, 0}, {1, 0, 0}, {2, 0, 0}, {3, 0, 0}, {4, 0, 0}, {4, 1, 0}, {4, 2, 0},
					{3, 2, 0}, {2, 2, 0}, {1, 2, 0}, {0, 2, 0}, {0, 1, 0},
				},
				{
					{0, 0, 0}, {1, 0, 0}, {2, 0, 0}, {3, 0, 0}, {4, 0, 0}, {4, 1, 0}, {4, 2, 0},
					{3, 2, 0}, {2, 2, 0}, {2, 1, 0}, // TODO: {2, 0, 0}
				},
			},
			true,
		},
	}
	for i, line := range data {
		c, err := newCanvas([]byte(strings.Join(line.input, "\n")))
		c, err := newCanvas([]byte(strings.Join(line.input, "\n")), 9)
		if err != nil {
			t.Fatalf("Test %d: error creating canvas: %s", i, err)
		}
		objs := c.objects()
		if line.strings != nil {
			if got := getStrings(objs); !reflect.DeepEqual(line.strings, got) {
				t.Errorf("%d: expected %q, but got %q", i, line.strings, got)
556
557
558
559
560
561
562
563

564
565
566
567
568
569
570
531
532
533
534
535
536
537

538
539
540
541
542
543
544
545







-
+







				{{x: 0, y: 0}, {x: 5, y: 0}, {x: 5, y: 2}, {x: 0, y: 2}},
				{{x: 0, y: 0}, {x: 5, y: 0}, {x: 5, y: 2}, {x: 2, y: 2}, {x: 2, y: 1}},
				{{x: 0, y: 0}, {x: 5, y: 0}, {x: 5, y: 2}, {x: 3, y: 2}, {x: 3, y: 1}},
			},
		},
	}
	for i, line := range data {
		c, err := newCanvas([]byte(strings.Join(line.input, "\n")))
		c, err := newCanvas([]byte(strings.Join(line.input, "\n")), 9)
		if err != nil {
			t.Fatalf("Test %d: error creating canvas: %s", i, err)
		}
		objs := c.objects()
		if line.strings != nil {
			if got := getStrings(objs); !reflect.DeepEqual(line.strings, got) {
				t.Errorf("%d: expected %q, but got %q", i, line.strings, got)
659
660
661
662
663
664
665
666

667
668
669
670
671
672
673
634
635
636
637
638
639
640

641
642
643
644
645
646
647
648







-
+







	chunk := []byte(strings.Join(data, "\n"))
	input := make([]byte, 0, len(chunk)*b.N)
	for i := 0; i < b.N; i++ {
		input = append(input, chunk...)
	}
	expected := 30 * b.N
	b.ResetTimer()
	c, err := newCanvas(input)
	c, err := newCanvas(input, 8)
	if err != nil {
		b.Fatalf("Error creating canvas: %s", err)
	}

	objs := c.objects()
	if len(objs) != expected {
		b.Fatalf("%d != %d", len(objs), expected)

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










-
+
-
-
-



-
-
-
-
-
+
+
+
+
+


+
+
+
+
+
+
+
+
+
+
+

+
-
-
-
+
+
+


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

-
+









-
+

-
+



-
+


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

-
+
+
+
+









//-----------------------------------------------------------------------------
// 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 draw provides a parser to create SVG from ASCII drawing.
// Package draw provides a parser to create SVG from ASCII drawing
//
// It is not a parser registered by the general parser framework (directed by
// metadata "syntax" of a zettel). It will be used when a zettel is evaluated.
package draw

import (
	"strconv"

	"zettelstore.de/c/api"
	"zettelstore.de/c/attrs"
	"zettelstore.de/z/ast"
	"zettelstore.de/c/api"
	"zettelstore.de/z/ast"
	"zettelstore.de/z/domain/meta"
	"zettelstore.de/z/input"
	"zettelstore.de/z/parser"
)

func init() {
	parser.Register(&parser.Info{
		Name:          api.ValueSyntaxDraw,
		AltNames:      []string{},
		IsTextParser:  true,
		IsImageFormat: false,
		ParseBlocks:   parseBlocks,
		ParseInlines:  parseInlines,
	})
}

const (
	defaultTabSize = 8
	defaultFont   = ""
	defaultScaleX = 10
	defaultScaleY = 20
	defaultFont    = ""
	defaultScaleX  = 10
	defaultScaleY  = 20
)

// ParseDrawBlock parses the content of an eval verbatim node into an SVG image BLOB.
func ParseDrawBlock(vn *ast.VerbatimNode) ast.BlockNode {
	font := defaultFont
func parseBlocks(inp *input.Input, m *meta.Meta, _ string) ast.BlockSlice {
	font := m.GetDefault("font", defaultFont)
	if val, found := vn.Attrs.Get("font"); found {
		font = val
	}
	scaleX := getScale(vn.Attrs, "x-scale", defaultScaleX)
	scaleY := getScale(vn.Attrs, "y-scale", defaultScaleY)
	scaleX := m.GetNumber("x-scale", defaultScaleX)
	scaleY := m.GetNumber("y-scale", defaultScaleY)

	canvas, err := newCanvas(vn.Content)
	canvas, err := newCanvas(inp.Src[inp.Pos:], defaultTabSize)
	if err != nil {
		return ast.CreateParaNode(canvasErrMsg(err)...)
		return ast.BlockSlice{ast.CreateParaNode(canvasErrMsg(err)...)}
	}
	if scaleX < 1 || 1000000 < scaleX {
		scaleX = defaultScaleX
	}
	if scaleY < 1 || 1000000 < scaleY {
		scaleY = defaultScaleY
	}
	svg := canvasToSVG(canvas, font, int(scaleX), int(scaleY))
	if len(svg) == 0 {
		return ast.CreateParaNode(noSVGErrMsg()...)
		return ast.BlockSlice{ast.CreateParaNode(noSVGErrMsg()...)}
	}
	return &ast.BLOBNode{
	return ast.BlockSlice{&ast.BLOBNode{
		Title:  "",
		Syntax: api.ValueSyntaxSVG,
		Blob:   svg,
	}
	}}
}

func getScale(a attrs.Attributes, key string, defVal int) int {
	if val, found := a.Get(key); found {
		if n, err := strconv.Atoi(val); err == nil && 0 < n && n < 100000 {
			return n
		}
func parseInlines(inp *input.Input, _ string) ast.InlineSlice {
	canvas, err := newCanvas(inp.Src[inp.Pos:], defaultTabSize)
	if err != nil {
		return canvasErrMsg(err)
	}
	svg := canvasToSVG(canvas, defaultFont, defaultScaleX, defaultScaleY)
	if len(svg) == 0 {
		return noSVGErrMsg()
	}
	return defVal
	return ast.InlineSlice{&ast.EmbedBLOBNode{
		Blob:   svg,
		Syntax: api.ValueSyntaxSVG,
	}}
}

func canvasErrMsg(err error) ast.InlineSlice {
	return ast.CreateInlineSliceFromWords("Error:", err.Error())
}

func noSVGErrMsg() ast.InlineSlice {
	return ast.CreateInlineSliceFromWords("NO", "IMAGE")
}

Changes to parser/draw/object.go.

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







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

















-

-
+

-
+


+
-
+









-
+








func (o *object) String() string {
	if o.isJustText() {
		return fmt.Sprintf("Text{%s %q}", o.points[0], string(o.text))
	}
	return fmt.Sprintf("Path{%v}", o.points)
}

// HasPoint determines whether the supplied point lives inside the object. Since we support complex
// convex and concave polygons, we need to do a full point-in-polygon test. The algorithm implemented
// comes from the more efficient, less-clever version at http://alienryderflex.com/polygon/.
func (o *object) HasPoint(p point) bool {
	hasPoint := false
	ncorners := len(o.corners)
	j := ncorners - 1
	for i := 0; i < ncorners; i++ {
		if (o.corners[i].y < p.y && o.corners[j].y >= p.y || o.corners[j].y < p.y && o.corners[i].y >= p.y) && (o.corners[i].x <= p.x || o.corners[j].x <= p.x) {
			if o.corners[i].x+(p.y-o.corners[i].y)/(o.corners[j].y-o.corners[i].y)*(o.corners[j].x-o.corners[i].x) < p.x {
				hasPoint = !hasPoint
			}
		}
		j = i
	}

	return hasPoint
}

// seal finalizes the object, setting its text, its corners, and its various rendering hints.
func (o *object) seal(c *canvas) {
	if c.at(o.points[0]).isArrow() {
		o.points[0].hint = startMarker
		c.hasStartMarker = true
	}

	if c.at(o.points[len(o.points)-1]).isArrow() {
		o.points[len(o.points)-1].hint = endMarker
		c.hasEndMarker = true
	}

	o.corners, o.isClosed = pointsToCorners(o.points)
	o.text = make([]rune, len(o.points))

	for i, p := range o.points {
		ch := c.at(p)
		if !o.isJustText() {
			if ch.isTick() {
			if c.at(p).isTick() {
				o.points[i].hint = tick
			} else if ch.isDot() {
			} else if c.at(p).isDot() {
				o.points[i].hint = dot
			}

			if ch.isDashed() {
			if c.at(p).isDashed() {
				o.isDashed = true
			}

			for _, corner := range o.corners {
				if corner.x == p.x && corner.y == p.y && c.at(p).isRoundedCorner() {
					o.points[i].hint = roundedCorner
				}
			}
		}
		o.text[i] = rune(ch)
		o.text[i] = rune(c.at(p))
	}
}

// objects implements a sortable collection of Object interfaces.
type objects []*object

func (o objects) Len() int      { return len(o) }
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173






174
175
176
177
178
179
180
176
177
178
179
180
181
182







183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198







-
-
-
-
-
-
-



+
+
+
+
+
+







		dir = dirNW
	} else if isDiagonalNE(points[0], points[1]) {
		dir = dirNE
	} else {
		panic(fmt.Errorf("discontiguous points: %+v", points))
	}

	cornerFunc := func(idx, newDir int) {
		if dir != newDir {
			out = append(out, points[idx-1])
			dir = newDir
		}
	}

	// Starting from the third point, check to see if the directionality between points P and
	// P-1 has changed.
	for i := 2; i < l; i++ {
		cornerFunc := func(idx, newDir int) {
			if dir != newDir {
				out = append(out, points[idx-1])
				dir = newDir
			}
		}
		if isHorizontal(points[i-1], points[i]) {
			cornerFunc(i, dirH)
		} else if isVertical(points[i-1], points[i]) {
			cornerFunc(i, dirV)
		} else if isDiagonalSE(points[i-1], points[i]) {
			cornerFunc(i, dirSE)
		} else if isDiagonalSW(points[i-1], points[i]) {

Changes to parser/draw/svg_test.go.

55
56
57
58
59
60
61
62

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

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







-
+











			[]string{
				" foo",
			},
			265,
		},
	}
	for i, line := range data {
		canvas, err := newCanvas([]byte(strings.Join(line.input, "\n")))
		canvas, err := newCanvas([]byte(strings.Join(line.input, "\n")), 9)
		if err != nil {
			t.Fatalf("Error creating canvas: %s", err)
		}
		actual := string(canvasToSVG(canvas, "", 9, 16))
		// TODO(dhobsd): Use golden file? Worth postponing once output is actually
		// nice.
		if line.length != len(actual) {
			t.Errorf("%d: expected length %d, but got %d\n%q", i, line.length, len(actual), actual)
		}
	}
}

Changes to parser/markdown/markdown.go.

15
16
17
18
19
20
21

22

23
24
25

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

23
24
25

26
27
28
29
30
31
32
33







+
-
+


-
+







	"bytes"
	"fmt"

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

	"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/textenc"
	"zettelstore.de/z/encoder"
	"zettelstore.de/z/input"
	"zettelstore.de/z/parser"
)

func init() {
	parser.Register(&parser.Info{
		Name:          "markdown",
48
49
50
51
52
53
54
55

56
57
58
59
60
61
62

63
64
65
66
67
68
69
49
50
51
52
53
54
55

56
57
58
59
60
61
62

63
64
65
66
67
68
69
70







-
+






-
+







	return bs.FirstParagraphInlines()
}

func parseMarkdown(inp *input.Input) *mdP {
	source := []byte(inp.Src[inp.Pos:])
	parser := gm.DefaultParser()
	node := parser.Parse(gmText.NewReader(source))
	textEnc := textenc.Create()
	textEnc := encoder.Create(api.EncoderText, nil)
	return &mdP{source: source, docNode: node, textEnc: textEnc}
}

type mdP struct {
	source  []byte
	docNode gmAst.Node
	textEnc *textenc.Encoder
	textEnc encoder.Encoder
}

func (p *mdP) acceptBlockChildren(docNode gmAst.Node) ast.BlockSlice {
	if docNode.Type() != gmAst.TypeDocument {
		panic(fmt.Sprintf("Expected document, but got node type %v", docNode.Type()))
	}
	result := make(ast.BlockSlice, 0, docNode.ChildCount())
128
129
130
131
132
133
134
135

136
137

138
139
140
141

142
143
144
145
146
147
148
129
130
131
132
133
134
135

136
137

138
139
140
141

142
143
144
145
146
147
148
149







-
+

-
+



-
+







		Kind:    ast.VerbatimProg,
		Attrs:   nil, //TODO
		Content: p.acceptRawText(node),
	}
}

func (p *mdP) acceptFencedCodeBlock(node *gmAst.FencedCodeBlock) *ast.VerbatimNode {
	var a attrs.Attributes
	var attrs zjson.Attributes
	if language := node.Language(p.source); len(language) > 0 {
		a = a.Set("class", "language-"+cleanText(language, true))
		attrs = attrs.Set("class", "language-"+cleanText(language, true))
	}
	return &ast.VerbatimNode{
		Kind:    ast.VerbatimProg,
		Attrs:   a,
		Attrs:   attrs,
		Content: p.acceptRawText(node),
	}
}

func (p *mdP) acceptRawText(node gmAst.Node) []byte {
	lines := node.Lines()
	result := make([]byte, 0, 512)
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
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







-
+



-
+













-
+







			p.acceptItemSlice(node),
		},
	}
}

func (p *mdP) acceptList(node *gmAst.List) ast.ItemNode {
	kind := ast.NestedListUnordered
	var a attrs.Attributes
	var attrs zjson.Attributes
	if node.IsOrdered() {
		kind = ast.NestedListOrdered
		if node.Start != 1 {
			a = a.Set("start", fmt.Sprintf("%d", node.Start))
			attrs = attrs.Set("start", fmt.Sprintf("%d", node.Start))
		}
	}
	items := make([]ast.ItemSlice, 0, node.ChildCount())
	for child := node.FirstChild(); child != nil; child = child.NextSibling() {
		item, ok := child.(*gmAst.ListItem)
		if !ok {
			panic(fmt.Sprintf("Expected list item node, but got %v", child.Kind()))
		}
		items = append(items, p.acceptItemSlice(item))
	}
	return &ast.NestedListNode{
		Kind:  kind,
		Items: items,
		Attrs: a,
		Attrs: attrs,
	}
}

func (p *mdP) acceptItemSlice(node gmAst.Node) ast.ItemSlice {
	result := make(ast.ItemSlice, 0, node.ChildCount())
	for elem := node.FirstChild(); elem != nil; elem = elem.NextSibling() {
		if item := p.acceptBlock(elem); item != nil {
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
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







-
+

-
+





-
+






-
+

-
+





-
+







			Inlines: p.acceptInlineChildren(node),
		},
	}
}

func (p *mdP) acceptLink(node *gmAst.Link) ast.InlineSlice {
	ref := ast.ParseReference(cleanText(node.Destination, true))
	var a attrs.Attributes
	var attrs zjson.Attributes
	if title := node.Title; len(title) > 0 {
		a = a.Set("title", cleanText(title, true))
		attrs = attrs.Set("title", cleanText(title, true))
	}
	return ast.InlineSlice{
		&ast.LinkNode{
			Ref:     ref,
			Inlines: p.acceptInlineChildren(node),
			Attrs:   a,
			Attrs:   attrs,
		},
	}
}

func (p *mdP) acceptImage(node *gmAst.Image) ast.InlineSlice {
	ref := ast.ParseReference(cleanText(node.Destination, true))
	var a attrs.Attributes
	var attrs zjson.Attributes
	if title := node.Title; len(title) > 0 {
		a = a.Set("title", cleanText(title, true))
		attrs = attrs.Set("title", cleanText(title, true))
	}
	return ast.InlineSlice{
		&ast.EmbedRefNode{
			Ref:     ref,
			Inlines: p.flattenInlineSlice(node),
			Attrs:   a,
			Attrs:   attrs,
		},
	}
}

func (p *mdP) flattenInlineSlice(node gmAst.Node) ast.InlineSlice {
	is := p.acceptInlineChildren(node)
	var buf bytes.Buffer

Changes to parser/parser_test.go.

15
16
17
18
19
20
21

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

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







+















+







	"testing"

	"zettelstore.de/c/api"
	"zettelstore.de/z/parser"
	"zettelstore.de/z/strfun"

	_ "zettelstore.de/z/parser/blob"       // Allow to use BLOB parser.
	_ "zettelstore.de/z/parser/draw"       // Allow to use draw parser.
	_ "zettelstore.de/z/parser/markdown"   // Allow to use markdown parser.
	_ "zettelstore.de/z/parser/none"       // Allow to use none parser.
	_ "zettelstore.de/z/parser/plain"      // Allow to use plain parser.
	_ "zettelstore.de/z/parser/zettelmark" // Allow to use zettelmark parser.
)

func TestParserType(t *testing.T) {
	syntaxSet := strfun.NewSet(parser.GetSyntaxes()...)
	testCases := []struct {
		syntax string
		text   bool
		image  bool
	}{
		{api.ValueSyntaxHTML, false, false},
		{"css", false, false},
		{api.ValueSyntaxDraw, true, false},
		{api.ValueSyntaxGif, false, true},
		{"jpeg", false, true},
		{"jpg", false, true},
		{"markdown", true, false},
		{"md", true, false},
		{"mustache", false, false},
		{api.ValueSyntaxNone, false, false},

Changes to parser/plain/plain.go.

11
12
13
14
15
16
17
18

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

18
19
20
21
22
23
24
25







-
+







// Package plain provides a parser for plain text data.
package plain

import (
	"strings"

	"zettelstore.de/c/api"
	"zettelstore.de/c/attrs"
	"zettelstore.de/c/zjson"
	"zettelstore.de/z/ast"
	"zettelstore.de/z/domain/meta"
	"zettelstore.de/z/input"
	"zettelstore.de/z/parser"
)

func init() {
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
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







-
-
+
+



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











-
+







func parseBlocksHTML(inp *input.Input, _ *meta.Meta, syntax string) ast.BlockSlice {
	return doParseBlocks(inp, syntax, ast.VerbatimHTML)
}
func doParseBlocks(inp *input.Input, syntax string, kind ast.VerbatimKind) ast.BlockSlice {
	return ast.BlockSlice{
		&ast.VerbatimNode{
			Kind:    kind,
			Attrs:   attrs.Attributes{"": syntax},
			Content: inp.ScanLineContent(),
			Attrs:   zjson.Attributes{"": syntax},
			Content: readContent(inp),
		},
	}
}

func readContent(inp *input.Input) []byte {
	result := make([]byte, 0, len(inp.Src)-inp.Pos+1)
	for {
		inp.EatEOL()
		posL := inp.Pos
		if inp.Ch == input.EOS {
			return result
		}
		inp.SkipToEOL()
		if len(result) > 0 {
			result = append(result, '\n')
		}
		result = append(result, inp.Src[posL:inp.Pos]...)
	}
}

func parseInlines(inp *input.Input, syntax string) ast.InlineSlice {
	return doParseInlines(inp, syntax, ast.LiteralProg)
}
func parseInlinesHTML(inp *input.Input, syntax string) ast.InlineSlice {
	return doParseInlines(inp, syntax, ast.LiteralHTML)
}
func doParseInlines(inp *input.Input, syntax string, kind ast.LiteralKind) ast.InlineSlice {
	inp.SkipToEOL()
	return ast.InlineSlice{&ast.LiteralNode{
		Kind:    kind,
		Attrs:   attrs.Attributes{"": syntax},
		Attrs:   zjson.Attributes{"": syntax},
		Content: append([]byte(nil), inp.Src[0:inp.Pos]...),
	}}
}

func parseSVGBlocks(inp *input.Input, _ *meta.Meta, syntax string) ast.BlockSlice {
	is := parseSVGInlines(inp, syntax)
	if len(is) == 0 {

Changes to parser/zettelmark/block.go.

17
18
19
20
21
22
23
24

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

24

25
26
27
28
29
30
31







-
+
-







	"zettelstore.de/z/input"
)

// parseBlockSlice parses a sequence of blocks.
func (cp *zmkP) parseBlockSlice() ast.BlockSlice {
	inp := cp.inp
	var lastPara *ast.ParaNode
	bs := ast.BlockSlice{}
	bs := make(ast.BlockSlice, 0, 2)

	for inp.Ch != input.EOS {
		bn, cont := cp.parseBlock(lastPara)
		if bn != nil {
			bs = append(bs, bn)
		}
		if !cont {
			lastPara, _ = bn.(*ast.ParaNode)
54
55
56
57
58
59
60
61

62
63
64
65
66
67
68
53
54
55
56
57
58
59

60
61
62
63
64
65
66
67







-
+







			return nil, false
		case '\n', '\r':
			inp.EatEOL()
			cp.cleanupListsAfterEOL()
			return nil, false
		case ':':
			bn, success = cp.parseColon()
		case '@', '`', runeModGrave, '%', '~', '$':
		case '@', '`', runeModGrave, '%':
			cp.clearStacked()
			bn, success = cp.parseVerbatim()
		case '"', '<':
			cp.clearStacked()
			bn, success = cp.parseRegion()
		case '=':
			cp.clearStacked()
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
92
93
94
95
96
97
98



99
100
101
102
103
104
105










106
107
108
109
110
111
112







-
-
-
+






-
-
-
-
-
-
-
-
-
-







		if success {
			return bn, false
		}
	}
	inp.SetPos(pos)
	cp.clearStacked()
	pn := cp.parsePara()
	if startsWithSpaceSoftBreak(pn) {
		pn.Inlines = pn.Inlines[2:]
	} else if lastPara != nil {
	if lastPara != nil {
		lastPara.Inlines = append(lastPara.Inlines, pn.Inlines...)
		return nil, true
	}
	return pn, false
}

func startsWithSpaceSoftBreak(pn *ast.ParaNode) bool {
	ins := pn.Inlines
	if len(ins) < 2 {
		return false
	}
	_, isSpace := ins[0].(*ast.SpaceNode)
	_, isBreak := ins[1].(*ast.BreakNode)
	return isSpace && isBreak
}

func (cp *zmkP) cleanupListsAfterEOL() {
	for _, l := range cp.lists {
		if lits := len(l.Items); lits > 0 {
			l.Items[lits-1] = append(l.Items[lits-1], &nullItemNode{})
		}
	}
	if cp.descrl != nil {
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
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







-
+



-
+

-
+




-
-
+
+







		return cp.parseRegion()
	}
	return cp.parseDefDescr()
}

// parsePara parses paragraphed inline material.
func (cp *zmkP) parsePara() *ast.ParaNode {
	ins := ast.InlineSlice{}
	pn := ast.NewParaNode()
	for {
		in := cp.parseInline()
		if in == nil {
			return &ast.ParaNode{Inlines: ins}
			return pn
		}
		ins = append(ins, in)
		pn.Inlines = append(pn.Inlines, in)
		if _, ok := in.(*ast.BreakNode); ok {
			ch := cp.inp.Ch
			switch ch {
			// Must contain all cases from above switch in parseBlock.
			case input.EOS, '\n', '\r', '@', '`', runeModGrave, '%', '~', '$', '"', '<', '=', '-', '*', '#', '>', ';', ':', ' ', '|', '{':
				return &ast.ParaNode{Inlines: ins}
			case input.EOS, '\n', '\r', '@', '`', runeModGrave, '%', '"', '<', '=', '-', '*', '#', '>', ';', ':', ' ', '|', '{':
				return pn
			}
		}
	}
}

// countDelim read from input until a non-delimiter is found and returns number of delimiter chars.
func (cp *zmkP) countDelim(delim rune) int {
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
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







-
+












-
-
-
-







func (cp *zmkP) parseVerbatim() (rn *ast.VerbatimNode, success bool) {
	inp := cp.inp
	fch := inp.Ch
	cnt := cp.countDelim(fch)
	if cnt < 3 {
		return nil, false
	}
	attrs := cp.parseBlockAttributes()
	attrs := cp.parseAttributes(true)
	inp.SkipToEOL()
	if inp.Ch == input.EOS {
		return nil, false
	}
	var kind ast.VerbatimKind
	switch fch {
	case '@':
		kind = ast.VerbatimZettel
	case '`', runeModGrave:
		kind = ast.VerbatimProg
	case '%':
		kind = ast.VerbatimComment
	case '~':
		kind = ast.VerbatimEval
	case '$':
		kind = ast.VerbatimMath
	default:
		panic(fmt.Sprintf("%q is not a verbatim char", fch))
	}
	rn = &ast.VerbatimNode{Kind: kind, Attrs: attrs, Content: make([]byte, 0, 512)}
	for {
		inp.EatEOL()
		posL := inp.Pos
235
236
237
238
239
240
241
242

243
244
245
246
247
248
249
218
219
220
221
222
223
224

225
226
227
228
229
230
231
232







-
+







	if !ok {
		panic(fmt.Sprintf("%q is not a region char", fch))
	}
	cnt := cp.countDelim(fch)
	if cnt < 3 {
		return nil, false
	}
	attrs := cp.parseBlockAttributes()
	attrs := cp.parseAttributes(true)
	inp.SkipToEOL()
	if inp.Ch == input.EOS {
		return nil, false
	}
	rn = &ast.RegionNode{
		Kind:    kind,
		Attrs:   attrs,
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
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







-
+













-
+







		}
		in := cp.parseInline()
		if in == nil {
			return hn, true
		}
		hn.Inlines = append(hn.Inlines, in)
		if inp.Ch == '{' && inp.Peek() != '{' {
			attrs := cp.parseBlockAttributes()
			attrs := cp.parseAttributes(true)
			hn.Attrs = attrs
			inp.SkipToEOL()
			return hn, true
		}
	}
}

// parseHRule parses a horizontal rule.
func (cp *zmkP) parseHRule() (hn *ast.HRuleNode, success bool) {
	inp := cp.inp
	if cp.countDelim(inp.Ch) < 3 {
		return nil, false
	}
	attrs := cp.parseBlockAttributes()
	attrs := cp.parseAttributes(true)
	inp.SkipToEOL()
	return &ast.HRuleNode{Attrs: attrs}, true
}

var mapRuneNestedList = map[rune]ast.NestedListKind{
	'*': ast.NestedListUnordered,
	'#': ast.NestedListOrdered,
355
356
357
358
359
360
361
362

363
364
365
366
367
368
369
338
339
340
341
342
343
344

345
346
347
348
349
350
351
352







-
+








	if len(kinds) < len(cp.lists) {
		cp.lists = cp.lists[:len(kinds)]
	}
	ln, newLnCount := cp.buildNestedList(kinds)
	pn := cp.parseLinePara()
	if pn == nil {
		pn = &ast.ParaNode{}
		pn = ast.NewParaNode()
	}
	ln.Items = append(ln.Items, ast.ItemSlice{pn})
	return cp.cleanupParsedNestedList(newLnCount)
}

func (cp *zmkP) parseNestedListKinds() []ast.NestedListKind {
	inp := cp.inp
513
514
515
516
517
518
519
520

521
522
523
524
525
526
527
496
497
498
499
500
501
502

503
504
505
506
507
508
509
510







-
+







	cp.lists = cp.lists[:cnt]
	if cnt == 0 {
		return false
	}
	ln := cp.lists[cnt-1]
	pn := cp.parseLinePara()
	if pn == nil {
		pn = &ast.ParaNode{}
		pn = ast.NewParaNode()
	}
	lbn := ln.Items[len(ln.Items)-1]
	if lpn, ok := lbn[len(lbn)-1].(*ast.ParaNode); ok {
		lpn.Inlines = append(lpn.Inlines, pn.Inlines...)
	} else {
		ln.Items[len(ln.Items)-1] = append(ln.Items[len(ln.Items)-1], pn)
	}
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
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







-
+



-
+


-
+

-
+

-
+







		cp.descrl.Descriptions[defPos].Descriptions[descrPos] = append(cp.descrl.Descriptions[defPos].Descriptions[descrPos], pn)
	}
	return true
}

// parseLinePara parses one line of inline material.
func (cp *zmkP) parseLinePara() *ast.ParaNode {
	ins := ast.InlineSlice{}
	pn := ast.NewParaNode()
	for {
		in := cp.parseInline()
		if in == nil {
			if len(ins) == 0 {
			if len(pn.Inlines) == 0 {
				return nil
			}
			return &ast.ParaNode{Inlines: ins}
			return pn
		}
		ins = append(ins, in)
		pn.Inlines = append(pn.Inlines, in)
		if _, ok := in.(*ast.BreakNode); ok {
			return &ast.ParaNode{Inlines: ins}
			return pn
		}
	}
}

// parseRow parse one table row.
func (cp *zmkP) parseRow() ast.BlockNode {
	inp := cp.inp

Changes to parser/zettelmark/inline.go.

1
2
3
4
5
6
7
8
9
10

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

23

24
25
26
27
28
29

30
31

32
33
34
35
36
37
38
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22

23
24
25
26
27
28
29
30

31
32

33
34
35
36
37
38
39
40










+











-
+

+





-
+

-
+







//-----------------------------------------------------------------------------
// 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 provides a parser for zettelmarkup.
package zettelmark

import (
	"bytes"
	"fmt"

	"zettelstore.de/z/ast"
	"zettelstore.de/z/input"
)

// parseInlineSlice parses a sequence of Inlines until EOS.
func (cp *zmkP) parseInlineSlice() (ins ast.InlineSlice) {
func (cp *zmkP) parseInlineSlice() ast.InlineSlice {
	inp := cp.inp
	var is ast.InlineSlice
	for inp.Ch != input.EOS {
		in := cp.parseInline()
		if in == nil {
			break
		}
		ins = append(ins, in)
		is = append(is, in)
	}
	return ins
	return is
}

func (cp *zmkP) parseInline() ast.InlineNode {
	inp := cp.inp
	pos := inp.Pos
	if cp.nestingLevel <= maxNestingLevel {
		cp.nestingLevel++
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
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







-
-



















-
-
+






-
+




+
+
+
+
+







			return cp.parseTag()
		case '%':
			in, success = cp.parseComment()
		case '_', '*', '>', '~', '^', ',', '"', ':':
			in, success = cp.parseFormat()
		case '@', '\'', '`', '=', runeModGrave:
			in, success = cp.parseLiteral()
		case '$':
			in, success = cp.parseLiteralMath()
		case '\\':
			return cp.parseBackslash()
		case '-':
			in, success = cp.parseNdash()
		case '&':
			in, success = cp.parseEntity()
		}
		if success {
			return in
		}
	}
	inp.SetPos(pos)
	return cp.parseText()
}

func (cp *zmkP) parseText() *ast.TextNode {
	inp := cp.inp
	pos := inp.Pos
	if inp.Ch == '\\' {
		cp.inp.Next()
		return cp.parseBackslashRest()
		return cp.parseTextBackslash()
	}
	for {
		inp.Next()
		switch inp.Ch {
		// The following case must contain all runes that occur in parseInline!
		// Plus the closing brackets ] and } and ) and the middle |
		case input.EOS, '\n', '\r', ' ', '\t', '[', ']', '{', '}', '(', ')', '|', '#', '%', '_', '*', '>', '~', '^', ',', '"', ':', '\'', '@', '`', runeModGrave, '$', '=', '\\', '-', '&':
		case input.EOS, '\n', '\r', ' ', '\t', '[', ']', '{', '}', '(', ')', '|', '#', '%', '_', '*', '>', '~', '^', ',', '"', ':', '\'', '@', '`', runeModGrave, '=', '\\', '-', '&':
			return &ast.TextNode{Text: string(inp.Src[pos:inp.Pos])}
		}
	}
}

func (cp *zmkP) parseTextBackslash() *ast.TextNode {
	cp.inp.Next()
	return cp.parseBackslashRest()
}

func (cp *zmkP) parseBackslash() ast.InlineNode {
	inp := cp.inp
	inp.Next()
	switch inp.Ch {
	case '\n', '\r':
		inp.EatEOL()
149
150
151
152
153
154
155
156

157
158
159
160
161
162
163
153
154
155
156
157
158
159

160
161
162
163
164
165
166
167







-
+







func (cp *zmkP) parseSoftBreak() *ast.BreakNode {
	cp.inp.EatEOL()
	return &ast.BreakNode{}
}

func (cp *zmkP) parseLink() (*ast.LinkNode, bool) {
	if ref, is, ok := cp.parseReference(']'); ok {
		attrs := cp.parseInlineAttributes()
		attrs := cp.parseAttributes(false)
		if len(ref) > 0 {
			return &ast.LinkNode{
				Ref:     ast.ParseReference(ref),
				Inlines: is,
				Attrs:   attrs,
			}, true
		}
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
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







-
+





-
+



-
-
+
+




-
+






-
+





-
+


-
+



-
-
+
+




-
+


















-
+



-
+






-
+







	case ' ', ',', '|':
		inp.Next()
	}
	ins, ok := cp.parseLinkLikeRest()
	if !ok {
		return nil, false
	}
	attrs := cp.parseInlineAttributes()
	attrs := cp.parseAttributes(false)
	return &ast.CiteNode{Key: string(inp.Src[pos:posL]), Inlines: ins, Attrs: attrs}, true
}

func (cp *zmkP) parseFootnote() (*ast.FootnoteNode, bool) {
	cp.inp.Next()
	ins, ok := cp.parseLinkLikeRest()
	is, ok := cp.parseLinkLikeRest()
	if !ok {
		return nil, false
	}
	attrs := cp.parseInlineAttributes()
	return &ast.FootnoteNode{Inlines: ins, Attrs: attrs}, true
	attrs := cp.parseAttributes(false)
	return &ast.FootnoteNode{Inlines: is, Attrs: attrs}, true
}

func (cp *zmkP) parseLinkLikeRest() (ast.InlineSlice, bool) {
	cp.skipSpace()
	ins := ast.InlineSlice{}
	var is ast.InlineSlice
	inp := cp.inp
	for inp.Ch != ']' {
		in := cp.parseInline()
		if in == nil {
			return nil, false
		}
		ins = append(ins, in)
		is = append(is, in)
		if _, ok := in.(*ast.BreakNode); ok && input.IsEOLEOS(inp.Ch) {
			return nil, false
		}
	}
	inp.Next()
	if len(ins) == 0 {
	if len(is) == 0 {
		return nil, true
	}
	return ins, true
	return is, true
}

func (cp *zmkP) parseEmbed() (ast.InlineNode, bool) {
	if ref, ins, ok := cp.parseReference('}'); ok {
		attrs := cp.parseInlineAttributes()
	if ref, is, ok := cp.parseReference('}'); ok {
		attrs := cp.parseAttributes(false)
		if len(ref) > 0 {
			r := ast.ParseReference(ref)
			return &ast.EmbedRefNode{
				Ref:     r,
				Inlines: ins,
				Inlines: is,
				Attrs:   attrs,
			}, true
		}
	}
	return nil, false
}

func (cp *zmkP) parseMark() (*ast.MarkNode, bool) {
	inp := cp.inp
	inp.Next()
	pos := inp.Pos
	for inp.Ch != '|' && inp.Ch != ']' {
		if !isNameRune(inp.Ch) {
			return nil, false
		}
		inp.Next()
	}
	mark := inp.Src[pos:inp.Pos]
	ins := ast.InlineSlice{}
	var is ast.InlineSlice
	if inp.Ch == '|' {
		inp.Next()
		var ok bool
		ins, ok = cp.parseLinkLikeRest()
		is, ok = cp.parseLinkLikeRest()
		if !ok {
			return nil, false
		}
	} else {
		inp.Next()
	}
	mn := &ast.MarkNode{Mark: string(mark), Inlines: ins}
	mn := &ast.MarkNode{Mark: string(mark), Inlines: is}
	return mn, true
}

func (cp *zmkP) parseTag() ast.InlineNode {
	inp := cp.inp
	posH := inp.Pos
	inp.Next()
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
392
393
394
395
396
397
398

399
400
401
402
403
404

405
406
407
408
409
410
411







-






-







	inp.Next()
	if inp.Ch != '%' {
		return nil, false
	}
	for inp.Ch == '%' {
		inp.Next()
	}
	attrs := cp.parseInlineAttributes()
	cp.skipSpace()
	pos := inp.Pos
	for {
		if input.IsEOLEOS(inp.Ch) {
			return &ast.LiteralNode{
				Kind:    ast.LiteralComment,
				Attrs:   attrs,
				Content: append([]byte(nil), inp.Src[pos:inp.Pos]...),
			}, true
		}
		inp.Next()
	}
}

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







-
+


















-













-
+










-
-
-
+
+
+










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







		if inp.Ch == input.EOS {
			return nil, false
		}
		if inp.Ch == fch {
			inp.Next()
			if inp.Ch == fch {
				inp.Next()
				fn.Attrs = cp.parseInlineAttributes()
				fn.Attrs = cp.parseAttributes(false)
				return fn, true
			}
			fn.Inlines = append(fn.Inlines, &ast.TextNode{Text: string(fch)})
		} else if in := cp.parseInline(); in != nil {
			if _, ok = in.(*ast.BreakNode); ok && input.IsEOLEOS(inp.Ch) {
				return nil, false
			}
			fn.Inlines = append(fn.Inlines, in)
		}
	}
}

var mapRuneLiteral = map[rune]ast.LiteralKind{
	'@':          ast.LiteralZettel,
	'`':          ast.LiteralProg,
	runeModGrave: ast.LiteralProg,
	'\'':         ast.LiteralInput,
	'=':          ast.LiteralOutput,
	// No '$': ast.LiteralMath, because paring literal math is a little different
}

func (cp *zmkP) parseLiteral() (res ast.InlineNode, success bool) {
	inp := cp.inp
	fch := inp.Ch
	kind, ok := mapRuneLiteral[fch]
	if !ok {
		panic(fmt.Sprintf("%q is not a formatting char", fch))
	}
	inp.Next() // read 2nd formatting character
	if inp.Ch != fch {
		return nil, false
	}
	litn := &ast.LiteralNode{Kind: kind}
	fn := &ast.LiteralNode{Kind: kind}
	inp.Next()
	var buf bytes.Buffer
	for {
		if inp.Ch == input.EOS {
			return nil, false
		}
		if inp.Ch == fch {
			if inp.Peek() == fch {
				inp.Next()
				inp.Next()
				litn.Attrs = cp.parseInlineAttributes()
				litn.Content = buf.Bytes()
				return litn, true
				fn.Attrs = cp.parseAttributes(false)
				fn.Content = buf.Bytes()
				return fn, true
			}
			buf.WriteRune(fch)
			inp.Next()
		} else {
			tn := cp.parseText()
			buf.WriteString(tn.Text)
		}
	}
}

func (cp *zmkP) parseLiteralMath() (res ast.InlineNode, success bool) {
	inp := cp.inp
	inp.Next() // read 2nd formatting character
	if inp.Ch != '$' {
		return nil, false
	}
	inp.Next()
	pos := inp.Pos
	for {
		if inp.Ch == input.EOS {
			return nil, false
		}
		if inp.Ch == '$' && inp.Peek() == '$' {
			content := append([]byte{}, inp.Src[pos:inp.Pos]...)
			inp.Next()
			inp.Next()
			fn := &ast.LiteralNode{
				Kind:    ast.LiteralMath,
				Attrs:   cp.parseInlineAttributes(),
				Content: content,
			}
			return fn, true
		}
		inp.Next()
	}
}

func (cp *zmkP) parseNdash() (res *ast.TextNode, success bool) {
	inp := cp.inp
	if inp.Peek() != inp.Ch {
		return nil, false
	}
	inp.Next()
	inp.Next()

Changes to parser/zettelmark/node.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) 2020-2022 Detlef Stern
// Copyright (c) 2020-2021 Detlef Stern
//
// This file is part of Zettelstore.
// This file is part of zettelstore.
//
// Zettelstore is licensed 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 provides a parser for zettelmarkup.
package zettelmark

import "zettelstore.de/z/ast"

// Internal nodes for parsing zettelmark. These will be removed in
// post-processing.

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

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
79
80
81
82
83
84
85
































86
87
88
89
90
91
92







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







	pp.inVerse = oldVerse
}

func (pp *postProcessor) visitNestedList(ln *ast.NestedListNode) {
	for i, item := range ln.Items {
		ln.Items[i] = pp.processItemSlice(item)
	}
	if ln.Kind != ast.NestedListQuote {
		return
	}
	items := []ast.ItemSlice{}
	collectedInlines := ast.InlineSlice{}

	addCollectedParagraph := func() {
		if len(collectedInlines) > 1 {
			items = append(items, []ast.ItemNode{&ast.ParaNode{Inlines: collectedInlines[1:]}})
			collectedInlines = ast.InlineSlice{}
		}
	}

	for _, item := range ln.Items {
		if len(item) != 1 { // i.e. 0 or > 1
			addCollectedParagraph()
			items = append(items, item)
			continue
		}

		// len(item) == 1
		if pn, ok := item[0].(*ast.ParaNode); ok {
			collectedInlines = append(collectedInlines, &ast.BreakNode{})
			collectedInlines = append(collectedInlines, pn.Inlines...)
			continue
		}

		addCollectedParagraph()
		items = append(items, item)
	}
	addCollectedParagraph()
	ln.Items = items
}

func (pp *postProcessor) visitDescriptionList(dn *ast.DescriptionListNode) {
	for i, def := range dn.Descriptions {
		if len(def.Term) > 0 {
			ast.Walk(pp, &dn.Descriptions[i].Term)
		}

Changes to parser/zettelmark/zettelmark.go.

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

20
21
22
23
24
25
26
8
9
10
11
12
13
14

15
16
17

18
19
20
21
22
23
24
25







-



-
+







// under this license.
//-----------------------------------------------------------------------------

// Package zettelmark provides a parser for zettelmarkup.
package zettelmark

import (
	"strings"
	"unicode"

	"zettelstore.de/c/api"
	"zettelstore.de/c/attrs"
	"zettelstore.de/c/zjson"
	"zettelstore.de/z/ast"
	"zettelstore.de/z/domain/meta"
	"zettelstore.de/z/input"
	"zettelstore.de/z/parser"
)

func init() {
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
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







-
+













+
+
+
-
+


-
+
+



-
+






-
+
+
+
+
+
+







-
+


-
+





-
+


+
+
+
+
+
+








-
+














+
+
+
+
+
+
-
+

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

-
-
+
+
-
-
+

-
-

-
+
+






-
-
+





-
-
+
+



-
+


-
+


-
+














-
+

-
-
+
+



-
+








+
+
+








-
+
+
+
+
+







// clearStacked removes all multi-line nodes from parser.
func (cp *zmkP) clearStacked() {
	cp.lists = nil
	cp.table = nil
	cp.descrl = nil
}

func (cp *zmkP) parseNormalAttribute(attrs map[string]string) bool {
func (cp *zmkP) parseNormalAttribute(attrs map[string]string, sameLine bool) bool {
	inp := cp.inp
	posK := inp.Pos
	for isNameRune(inp.Ch) {
		inp.Next()
	}
	if posK == inp.Pos {
		return false
	}
	key := string(inp.Src[posK:inp.Pos])
	if inp.Ch != '=' {
		attrs[key] = ""
		return true
	}
	if sameLine && input.IsEOLEOS(inp.Ch) {
		return false
	}
	return cp.parseAttributeValue(key, attrs)
	return cp.parseAttributeValue(key, attrs, sameLine)
}

func (cp *zmkP) parseAttributeValue(key string, attrs map[string]string) bool {
func (cp *zmkP) parseAttributeValue(
	key string, attrs map[string]string, sameLine bool) bool {
	inp := cp.inp
	inp.Next()
	if inp.Ch == '"' {
		return cp.parseQuotedAttributeValue(key, attrs)
		return cp.parseQuotedAttributeValue(key, attrs, sameLine)
	}
	posV := inp.Pos
	for {
		switch inp.Ch {
		case input.EOS:
			return false
		case '\n', '\r', ' ', '}':
		case '\n', '\r':
			if sameLine {
				return false
			}
			fallthrough
		case ' ', '}':
			updateAttrs(attrs, key, string(inp.Src[posV:inp.Pos]))
			return true
		}
		inp.Next()
	}
}

func (cp *zmkP) parseQuotedAttributeValue(key string, attrs map[string]string) bool {
func (cp *zmkP) parseQuotedAttributeValue(key string, attrs map[string]string, sameLine bool) bool {
	inp := cp.inp
	inp.Next()
	var sb strings.Builder
	var val string
	for {
		switch inp.Ch {
		case input.EOS:
			return false
		case '"':
			updateAttrs(attrs, key, sb.String())
			updateAttrs(attrs, key, val)
			inp.Next()
			return true
		case '\n', '\r':
			if sameLine {
				return false
			}
			inp.EatEOL()
			val += " "
		case '\\':
			inp.Next()
			switch inp.Ch {
			case input.EOS, '\n', '\r':
				return false
			}
			fallthrough
		default:
			sb.WriteRune(inp.Ch)
			val += string(inp.Ch)
			inp.Next()
		}
	}

}

func updateAttrs(attrs map[string]string, key, val string) {
	if prevVal := attrs[key]; len(prevVal) > 0 {
		attrs[key] = prevVal + " " + val
	} else {
		attrs[key] = val
	}
}

// parseAttributes reads optional attributes.
// If sameLine is True, it is called from block nodes. In this case, a single
// name is allowed. It will parse as {name}. Attributes are not allowed to be
// continued on next line.
// If sameLine is False, it is called from inline nodes. In this case, the next
// rune must be '{'. A continuation on next lines is allowed.
func (cp *zmkP) parseBlockAttributes() attrs.Attributes {
func (cp *zmkP) parseAttributes(sameLine bool) zjson.Attributes {
	inp := cp.inp
	if sameLine {
	pos := inp.Pos
	for isNameRune(inp.Ch) {
		inp.Next()
	}
	if pos < inp.Pos {
		return attrs.Attributes{"": string(inp.Src[pos:inp.Pos])}
	}
		pos := inp.Pos
		for isNameRune(inp.Ch) {
			inp.Next()
		}
		if pos < inp.Pos {
			return zjson.Attributes{"": string(inp.Src[pos:inp.Pos])}
		}

	// No immediate name: skip spaces
	cp.skipSpace()
		// No immediate name: skip spaces
		cp.skipSpace()
	return cp.parseInlineAttributes()
}
	}

func (cp *zmkP) parseInlineAttributes() attrs.Attributes {
	inp := cp.inp
	pos := inp.Pos
	if attrs, success := cp.doParseAttributes(); success {
	attrs, success := cp.doParseAttributes(sameLine)
	if sameLine || success {
		return attrs
	}
	inp.SetPos(pos)
	return nil
}

// doParseAttributes reads attributes.
func (cp *zmkP) doParseAttributes() (res attrs.Attributes, success bool) {
func (cp *zmkP) doParseAttributes(sameLine bool) (res zjson.Attributes, success bool) {
	inp := cp.inp
	if inp.Ch != '{' {
		return nil, false
	}
	inp.Next()
	a := attrs.Attributes{}
	if !cp.parseAttributeValues(a) {
	attrs := zjson.Attributes{}
	if !cp.parseAttributeValues(sameLine, attrs) {
		return nil, false
	}
	inp.Next()
	return a, true
	return attrs, true
}

func (cp *zmkP) parseAttributeValues(a attrs.Attributes) bool {
func (cp *zmkP) parseAttributeValues(sameLine bool, attrs zjson.Attributes) bool {
	inp := cp.inp
	for {
		cp.skipSpaceLine()
		cp.skipSpaceLine(sameLine)
		switch inp.Ch {
		case input.EOS:
			return false
		case '}':
			return true
		case '.':
			inp.Next()
			posC := inp.Pos
			for isNameRune(inp.Ch) {
				inp.Next()
			}
			if posC == inp.Pos {
				return false
			}
			updateAttrs(a, "class", string(inp.Src[posC:inp.Pos]))
			updateAttrs(attrs, "class", string(inp.Src[posC:inp.Pos]))
		case '=':
			delete(a, "")
			if !cp.parseAttributeValue("", a) {
			delete(attrs, "")
			if !cp.parseAttributeValue("", attrs, sameLine) {
				return false
			}
		default:
			if !cp.parseNormalAttribute(a) {
			if !cp.parseNormalAttribute(attrs, sameLine) {
				return false
			}
		}

		switch inp.Ch {
		case '}':
			return true
		case '\n', '\r':
			if sameLine {
				return false
			}
		case ' ', ',':
			inp.Next()
		default:
			return false
		}
	}
}

func (cp *zmkP) skipSpaceLine() {
func (cp *zmkP) skipSpaceLine(sameLine bool) {
	if sameLine {
		cp.skipSpace()
		return
	}
	for inp := cp.inp; ; {
		switch inp.Ch {
		case ' ':
			inp.Next()
		case '\n', '\r':
			inp.EatEOL()
		default:

Changes to parser/zettelmark/zettelmark_test.go.

10
11
12
13
14
15
16

17
18
19
20
21

22
23
24




25
26
27
28
29
30
31
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







+




-
+



+
+
+
+








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

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

	"zettelstore.de/c/api"
	"zettelstore.de/c/attrs"
	"zettelstore.de/c/zjson"
	"zettelstore.de/z/ast"
	"zettelstore.de/z/input"
	"zettelstore.de/z/parser"

	// Ensure that the text encoder is available.
	// Needed by parser/cleanup.go
	_ "zettelstore.de/z/encoder/textenc"
)

type TestCase struct{ source, want string }
type TestCases []TestCase

func replace(s string, tcs TestCases) TestCases {
	var testCases TestCases
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
370
371
372
373
374
375
376





















377
378
379
380
381
382
383







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







		{"''````''", "(PARA {' ````})"},
		{"''``a``''", "(PARA {' ``a``})"},
		{"''``''``", "(PARA {' ``} ``)"},
		{"''\\'''", "(PARA {' '})"},
	})
}

func TestLiteralMath(t *testing.T) {
	t.Parallel()
	checkTcs(t, TestCases{
		{"$", "(PARA $)"},
		{"$$", "(PARA $$)"},
		{"$$$", "(PARA $$$)"},
		{"$$$$", "(PARA {$})"},
		{"$$a$$", "(PARA {$ a})"},
		{"$$a$$$", "(PARA {$ a} $)"},
		{"$$$a$$", "(PARA {$ $a})"},
		{"$$$a$$$", "(PARA {$ $a} $)"},
		{`$\$`, "(PARA $$)"},
		{`$\$$`, "(PARA $$$)"},
		{`$$\$`, "(PARA $$$)"},
		{`$$a\$$`, `(PARA {$ a\})`},
		{`$$a$\$`, "(PARA $$a$$)"},
		{`$$a\$$$`, `(PARA {$ a\} $)`},
		{"$$a$${go}", "(PARA {$ a}[ATTR go])"},
	})
}

func TestMixFormatCode(t *testing.T) {
	t.Parallel()
	checkTcs(t, TestCases{
		{"__abc__\n**def**", "(PARA {_ abc} SB {* def})"},
		{"''abc''\n==def==", "(PARA {' abc} SB {= def})"},
		{"__abc__\n==def==", "(PARA {_ abc} SB {= def})"},
		{"__abc__\n``def``", "(PARA {_ abc} SB {` def})"},
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
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







-
+















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









-
-
-
-
-
-
-
-







}

func TestVerbatimZettel(t *testing.T) {
	t.Parallel()
	checkTcs(t, TestCases{
		{"@@@\n@@@", "(ZETTEL)"},
		{"@@@\nabc\n@@@", "(ZETTEL\nabc)"},
		{"@@@@def\nabc\n@@@@", "(ZETTEL\nabc)[ATTR =def]"},
		{"@@@@draw\nabc\n@@@@", "(ZETTEL\nabc)[ATTR =draw]"},
	})
}

func TestVerbatimCode(t *testing.T) {
	t.Parallel()
	checkTcs(t, TestCases{
		{"```\n```", "(PROG)"},
		{"```\nabc\n```", "(PROG\nabc)"},
		{"```\nabc\n````", "(PROG\nabc)"},
		{"````\nabc\n````", "(PROG\nabc)"},
		{"````\nabc\n```\n````", "(PROG\nabc\n```)"},
		{"````go\nabc\n````", "(PROG\nabc)[ATTR =go]"},
	})
}

func TestVerbatimEval(t *testing.T) {
	t.Parallel()
	checkTcs(t, TestCases{
		{"~~~\n~~~", "(EVAL)"},
		{"~~~\nabc\n~~~", "(EVAL\nabc)"},
		{"~~~\nabc\n~~~~", "(EVAL\nabc)"},
		{"~~~~\nabc\n~~~~", "(EVAL\nabc)"},
		{"~~~~\nabc\n~~~\n~~~~", "(EVAL\nabc\n~~~)"},
		{"~~~~go\nabc\n~~~~", "(EVAL\nabc)[ATTR =go]"},
	})
}

func TestVerbatimMath(t *testing.T) {
	t.Parallel()
	checkTcs(t, TestCases{
		{"$$$\n$$$", "(MATH)"},
		{"$$$\nabc\n$$$", "(MATH\nabc)"},
		{"$$$\nabc\n$$$$", "(MATH\nabc)"},
		{"$$$$\nabc\n$$$$", "(MATH\nabc)"},
		{"$$$$\nabc\n$$$\n$$$$", "(MATH\nabc\n$$$)"},
		{"$$$$go\nabc\n$$$$", "(MATH\nabc)[ATTR =go]"},
	})
}

func TestVerbatimComment(t *testing.T) {
	t.Parallel()
	checkTcs(t, TestCases{
		{"%%%\n%%%", "(COMMENT)"},
		{"%%%\nabc\n%%%", "(COMMENT\nabc)"},
		{"%%%%go\nabc\n%%%%", "(COMMENT\nabc)[ATTR =go]"},
	})
}

func TestPara(t *testing.T) {
	t.Parallel()
	checkTcs(t, TestCases{
		{"a\n\nb", "(PARA a)(PARA b)"},
		{"a\n \nb", "(PARA a)(PARA b)"},
	})
}

func TestSpanRegion(t *testing.T) {
	t.Parallel()
	checkTcs(t, TestCases{
		{":::\n:::", "(SPAN)"},
		{":::\nabc\n:::", "(SPAN (PARA abc))"},
		{":::\nabc\n::::", "(SPAN (PARA abc))"},
		{"::::\nabc\n::::", "(SPAN (PARA abc))"},
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
574
575
576
577
578
579
580









581
582
583
584
585
586
587







-
-
-
-
-
-
-
-
-







		{"* abc\n# def", "(UL {(PARA abc)})(OL {(PARA def)})"},

		// Quotation lists may have empty items
		{">", "(QL {})"},
	})
}

func TestQuoteList(t *testing.T) {
	t.Parallel()
	checkTcs(t, TestCases{
		{"> w1 w2", "(QL {(PARA w1 SP w2)})"},
		{"> w1\n> w2", "(QL {(PARA w1 SB w2)})"},
		{"> w1\n>\n>w2", "(QL {(PARA w1)} {})(PARA >w2)"},
	})
}

func TestEnumAfterPara(t *testing.T) {
	t.Parallel()
	checkTcs(t, TestCases{
		{"abc\n* def", "(PARA abc)(UL {(PARA def)})"},
		{"abc\n*def", "(PARA abc SB *def)"},
	})
}
706
707
708
709
710
711
712
713

714
715
716
717
718
719
720
721
722
723
724
725

726
727
728
729
730
731
732
649
650
651
652
653
654
655

656
657
658
659
660
661
662
663
664
665
666
667

668
669
670
671
672
673
674
675







-
+











-
+







		{":::{=go}\n:::", "(SPAN)[ATTR =go]"},
		{":::{go}\n:::", "(SPAN)[ATTR go]"},
		{":::{go=py}\n:::", "(SPAN)[ATTR go=py]"},
		{":::{.go=py}\n:::", "(SPAN)"},
		{":::{go=}\n:::", "(SPAN)[ATTR go]"},
		{":::{.go=}\n:::", "(SPAN)"},
		{":::{go py}\n:::", "(SPAN)[ATTR go py]"},
		{":::{go\npy}\n:::", "(SPAN)[ATTR go py]"},
		{":::{go\npy}\n:::", "(SPAN (PARA py}))"},
		{":::{.go py}\n:::", "(SPAN)[ATTR class=go py]"},
		{":::{go .py}\n:::", "(SPAN)[ATTR class=py go]"},
		{":::{.go py=3}\n:::", "(SPAN)[ATTR class=go py=3]"},
		{":::  {  go  }  \n:::", "(SPAN)[ATTR go]"},
		{":::  {  .go  }  \n:::", "(SPAN)[ATTR class=go]"},
	})
	checkTcs(t, replace("\"", TestCases{
		{":::{py=3}\n:::", "(SPAN)[ATTR py=3]"},
		{":::{py=$2 3$}\n:::", "(SPAN)[ATTR py=$2 3$]"},
		{":::{py=$2\\$3$}\n:::", "(SPAN)[ATTR py=2$3]"},
		{":::{py=2$3}\n:::", "(SPAN)[ATTR py=2$3]"},
		{":::{py=$2\n3$}\n:::", "(SPAN)[ATTR py=$2\n3$]"},
		{":::{py=$2\n3$}\n:::", "(SPAN (PARA 3$}))"},
		{":::{py=$2 3}\n:::", "(SPAN)"},
		{":::{py=2 py=3}\n:::", "(SPAN)[ATTR py=$2 3$]"},
		{":::{.go .py}\n:::", "(SPAN)[ATTR class=$go py$]"},
		{":::{go go}\n:::", "(SPAN)[ATTR go]"},
		{":::{=py =go}\n:::", "(SPAN)[ATTR =go]"},
	}))
}
752
753
754
755
756
757
758
759

760
761
762
763
764
765
766
695
696
697
698
699
700
701

702
703
704
705
706
707
708
709







-
+







		{"::a::{\ngo\n}", "(PARA {: a}[ATTR go])"},
	})
	checkTcs(t, replace("\"", TestCases{
		{"::a::{py=3}", "(PARA {: a}[ATTR py=3])"},
		{"::a::{py=$2 3$}", "(PARA {: a}[ATTR py=$2 3$])"},
		{"::a::{py=$2\\$3$}", "(PARA {: a}[ATTR py=2$3])"},
		{"::a::{py=2$3}", "(PARA {: a}[ATTR py=2$3])"},
		{"::a::{py=$2\n3$}", "(PARA {: a}[ATTR py=$2\n3$])"},
		{"::a::{py=$2\n3$}", "(PARA {: a}[ATTR py=$2 3$])"},
		{"::a::{py=$2 3}", "(PARA {: a} {py=$2 SP 3})"},

		{"::a::{py=2 py=3}", "(PARA {: a}[ATTR py=$2 3$])"},
		{"::a::{.go .py}", "(PARA {: a}[ATTR class=$go py$])"},
	}))
}

967
968
969
970
971
972
973
974
975
976
977
978
979
980
981
982
910
911
912
913
914
915
916


917
918
919
920
921
922
923







-
-







	}
	return nil
}

var mapVerbatimKind = map[ast.VerbatimKind]string{
	ast.VerbatimZettel:  "(ZETTEL",
	ast.VerbatimProg:    "(PROG",
	ast.VerbatimEval:    "(EVAL",
	ast.VerbatimMath:    "(MATH",
	ast.VerbatimComment: "(COMMENT",
}

var mapRegionKind = map[ast.RegionKind]string{
	ast.RegionSpan:  "(SPAN",
	ast.RegionQuote: "(QUOTE",
	ast.RegionVerse: "(VERSE",
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
1046
1047
1048
1049
1050
1051
1052
1053
1054
1055
1056
1057
949
950
951
952
953
954
955

956
957
958
959
960
961
962
963
964

965
966
967
968
969
970
971
972
973
974
975
976

977
978
979
980
981
982

983
984
985
986
987
988
989
990
991
992
993
994
















-









-
+





+
+
+
+
+
+
-
+





-
+











-
-
-
-
-
-
-
-
-

var mapLiteralKind = map[ast.LiteralKind]rune{
	ast.LiteralZettel:  '@',
	ast.LiteralProg:    '`',
	ast.LiteralInput:   '\'',
	ast.LiteralOutput:  '=',
	ast.LiteralComment: '%',
	ast.LiteralMath:    '$',
}

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

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

	keys := make([]string, 0, len(a))
	for k := range a {
		keys = append(keys, k)
	}
	sort.Strings(keys)

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

	tv.buf.WriteByte(']')
}

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

Changes to search/print.go.

9
10
11
12
13
14
15

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

21
22
23
24
25
26
27







+




-







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

// Package search provides a zettel search.
package search

import (
	"io"
	"sort"
	"strconv"
	"strings"

	"zettelstore.de/c/api"
	"zettelstore.de/c/maps"
)

func (s *Search) String() string {
	var sb strings.Builder
	s.Print(&sb)
	return sb.String()
}
36
37
38
39
40
41
42





43

44
45
46
47
48

49
50
51
52
53
54
55
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







+
+
+
+
+
-
+




-
+







	}
	space := false
	if len(s.search) > 0 {
		io.WriteString(w, "ANY")
		printSelectExprValues(w, s.search)
		space = true
	}
	names := make([]string, 0, len(s.tags))
	for name := range s.tags {
		names = append(names, name)
	}
	sort.Strings(names)
	for _, name := range maps.Keys(s.mvals) {
	for _, name := range names {
		if space {
			io.WriteString(w, " AND ")
		}
		io.WriteString(w, name)
		printSelectExprValues(w, s.mvals[name])
		printSelectExprValues(w, s.tags[name])
		space = true
	}
	if s.negate {
		io.WriteString(w, ")")
		space = true
	}

Changes to search/search.go.

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







-
+










-
+








-
-
-
+
+
+








// Search specifies a mechanism for selecting zettel.
type Search struct {
	mx sync.RWMutex // Protects other attributes

	// Fields to be used for selecting
	preMatch MetaMatchFunc // Match that must be true
	mvals    expMetaValues // Expected values for a meta datum
	tags     expTagValues  // Expected values for a tag
	search   []expValue    // Search string
	negate   bool          // Negate the result of the whole selecting process

	// Fields to be used for sorting
	order      string // Name of meta key. None given: use "id"
	descending bool   // Sort by order, but descending
	offset     int    // <= 0: no offset
	limit      int    // <= 0: no limit
}

type expMetaValues map[string][]expValue
type expTagValues map[string][]expValue

// Clone the search value.
func (s *Search) Clone() *Search {
	if s == nil {
		return nil
	}
	c := new(Search)
	c.preMatch = s.preMatch
	c.mvals = make(expMetaValues, len(s.mvals))
	for k, v := range s.mvals {
		c.mvals[k] = v
	c.tags = make(expTagValues, len(s.tags))
	for k, v := range s.tags {
		c.tags[k] = v
	}
	c.search = append([]expValue{}, s.search...)
	c.negate = s.negate
	c.order = s.order
	c.descending = s.descending
	c.offset = s.offset
	c.limit = s.limit
138
139
140
141
142
143
144
145
146


147
148

149
150
151
152
153
154
155
138
139
140
141
142
143
144


145
146
147

148
149
150
151
152
153
154
155







-
-
+
+

-
+







	if s == nil {
		s = new(Search)
	}
	s.mx.Lock()
	defer s.mx.Unlock()
	if key == "" {
		s.addSearch(val)
	} else if s.mvals == nil {
		s.mvals = expMetaValues{key: {val}}
	} else if s.tags == nil {
		s.tags = expTagValues{key: {val}}
	} else {
		s.mvals[key] = append(s.mvals[key], val)
		s.tags[key] = append(s.tags[key], val)
	}
	return s
}

func (s *Search) addSearch(val expValue) {
	if val.negate {
		val.op = val.op.negate()
296
297
298
299
300
301
302
303

304
305
306
307
308
309
310
296
297
298
299
300
301
302

303
304
305
306
307
308
309
310







-
+







// is calculated via metadata enrichments.
func (s *Search) EnrichNeeded() bool {
	if s == nil {
		return false
	}
	s.mx.RLock()
	defer s.mx.RUnlock()
	for key := range s.mvals {
	for key := range s.tags {
		if meta.IsComputed(key) {
			return true
		}
	}
	return meta.IsComputed(s.order)
}

Changes to search/select.go.

33
34
35
36
37
38
39
40
41
42



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



40
41
42
43
44
45
46
47
48
49







-
-
-
+
+
+







	if len(posSpecs) > 0 || len(negSpecs) > 0 || len(nomatch) > 0 {
		return makeSearchMetaMatchFunc(posSpecs, negSpecs, nomatch)
	}
	return nil
}

func (s *Search) createSelectSpecs() (posSpecs, negSpecs []matchSpec, nomatch []string) {
	posSpecs = make([]matchSpec, 0, len(s.mvals))
	negSpecs = make([]matchSpec, 0, len(s.mvals))
	for key, values := range s.mvals {
	posSpecs = make([]matchSpec, 0, len(s.tags))
	negSpecs = make([]matchSpec, 0, len(s.tags))
	for key, values := range s.tags {
		if !meta.KeyIsValid(key) {
			continue
		}
		if always, never := countEmptyValues(values); always+never > 0 {
			if never == 0 {
				posSpecs = append(posSpecs, matchSpec{key, matchValueAlways})
				continue
389
390
391
392
393
394
395
396

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

396
397
398
399
400
401
402
403
404







-
+








		}
	}
	return true
}
func matchMetaNegSpecs(m *meta.Meta, negSpecs []matchSpec) bool {
	for _, s := range negSpecs {
		if s.match == nil {
			if value, ok := m.Get(s.key); ok && matchValueAlways(value) {
			if _, ok := m.Get(s.key); ok {
				return false
			}
		} else if value, ok := m.Get(s.key); !ok || !s.match(value) {
			return false
		}
	}
	return true
}

Changes to strfun/escape.go.

61
62
63
64
65
66
67
68

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

68

69
70
71
72
73
74
75







-
+
-







	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) {
func JSONEscape(w io.Writer, s string) {
	length := 0
	last := 0
	for i, ch := range s {
		var b []byte
		switch ch {
		case '\t':
			b = jsTab
		case '\r':
88
89
90
91
92
93
94
95

96
97
98
99

100
101
102
103
104
105
106

107
108
109

110
111
87
88
89
90
91
92
93

94




95




96
97

98



99









-
+
-
-
-
-
+
-
-
-
-


-
+
-
-
-
+
-
-
				b[3] = '0'
				b[4] = jsHex[ch>>4]
				b[5] = jsHex[ch&0xF]
			} else {
				continue
			}
		}
		l1, err := io.WriteString(w, s[last:i])
		io.WriteString(w, s[last:i])
		if err != nil {
			return 0, err
		}
		l2, err := w.Write(b)
		w.Write(b)
		if err != nil {
			return 0, err
		}
		length += l1 + l2
		last = i + 1
	}
	l, err := io.WriteString(w, s[last:])
	io.WriteString(w, s[last:])
	if err != nil {
		return 0, err
	}
}
	return length + l, nil
}

Changes to testdata/testbox/19700101000000.zettel.

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

10
11









-


id: 19700101000000
title: Startup Configuration
role: configuration
tags: #invisible
syntax: none
box-uri-1: mem:
box-uri-2: dir:testdata/testbox?readonly
modified: 20210629174022
owner: 20210629163300
secret: 1234567890123456
token-lifetime-api: 1
visibility: owner

Changes to tests/client/client_test.go.

45
46
47
48
49
50
51
52
53
54
55




56
57
58
59
60
61
62
45
46
47
48
49
50
51




52
53
54
55
56
57
58
59
60
61
62







-
-
-
-
+
+
+
+







		}

	}
}

func TestListZettel(t *testing.T) {
	const (
		ownerZettel      = 49
		configRoleZettel = 31
		writerZettel     = ownerZettel - 25
		readerZettel     = ownerZettel - 25
		ownerZettel      = 48
		configRoleZettel = 30
		writerZettel     = ownerZettel - 24
		readerZettel     = ownerZettel - 24
		creatorZettel    = 7
		publicZettel     = 4
	)

	testdata := []struct {
		user string
		exp  int
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
163
164
165
166
167
168
169

170
171
172
173
174
175
176
177
178
179
180
181

182
183
184
185
186
187
188
189







-
+











-
+







func TestGetParsedEvaluatedZettel(t *testing.T) {
	t.Parallel()
	c := getClient()
	c.SetAuth("owner", "owner")
	encodings := []api.EncodingEnum{
		api.EncoderZJSON,
		api.EncoderHTML,
		api.EncoderSexpr,
		api.EncoderNative,
		api.EncoderText,
	}
	for _, enc := range encodings {
		content, err := c.GetParsedZettel(context.Background(), api.ZidDefaultHome, enc)
		if err != nil {
			t.Error(err)
			continue
		}
		if len(content) == 0 {
			t.Errorf("Empty content for parsed encoding %v", enc)
		}
		content, err = c.GetEvaluatedZettel(context.Background(), api.ZidDefaultHome, enc)
		content, err = c.GetEvaluatedZettel(context.Background(), api.ZidDefaultHome, enc, true)
		if err != nil {
			t.Error(err)
			continue
		}
		if len(content) == 0 {
			t.Errorf("Empty content for evaluated encoding %v", enc)
		}
329
330
331
332
333
334
335
336

337
338
339
340
341
342
343
329
330
331
332
333
334
335

336
337
338
339
340
341
342
343







-
+







	}
}

func TestListTags(t *testing.T) {
	t.Parallel()
	c := getClient()
	c.SetAuth("owner", "owner")
	tm, err := c.ListMapMeta(context.Background(), api.KeyTags)
	tm, err := c.ListTags(context.Background())
	if err != nil {
		t.Error(err)
		return
	}
	tags := []struct {
		key  string
		size int
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
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







-
+








-
-
-
+
+
+







	}
}

func TestListRoles(t *testing.T) {
	t.Parallel()
	c := getClient()
	c.SetAuth("owner", "owner")
	rl, err := c.ListMapMeta(context.Background(), api.KeyRole)
	rl, err := c.ListRoles(context.Background())
	if err != nil {
		t.Error(err)
		return
	}
	exp := []string{"configuration", "user", "zettel"}
	if len(rl) != len(exp) {
		t.Errorf("Expected %d different tags, but got only %d (%v)", len(exp), len(rl), rl)
	}
	for _, id := range exp {
		if _, found := rl[id]; !found {
			t.Errorf("Role map expected key %q", id)
	for i, id := range exp {
		if id != rl[i] {
			t.Errorf("Role list pos %d: expected %q, got %q", i, id, rl[i])
		}
	}
}

func TestVersion(t *testing.T) {
	t.Parallel()
	c := getClient()

Changes to tests/client/crud_test.go.

1
2

3
4

5
6
7
8
9
10
11
1

2
3

4
5
6
7
8
9
10
11

-
+

-
+







//-----------------------------------------------------------------------------
// Copyright (c) 2021-2022 Detlef Stern
// Copyright (c) 2021 Detlef Stern
//
// This file is part of Zettelstore.
// This file is part of zettelstore-client.
//
// 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.
//-----------------------------------------------------------------------------

package client_test
40
41
42
43
44
45
46


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







+
+







	}
	data, err := c.GetZettel(context.Background(), zid, api.PartZettel)
	if err != nil {
		t.Error("Cannot read zettel", zid, err)
		return
	}
	exp := `title: A Test
role: zettel
syntax: zmk

Example content.`
	if string(data) != exp {
		t.Errorf("Expected zettel data: %q, but got %q", exp, data)
	}
	newZid := nextZid(zid)
	err = c.RenameZettel(context.Background(), zid, newZid)

Changes to tests/client/embed_test.go.

1
2

3
4

5
6
7
8



9
10
11
12
13
14
15
1

2
3

4
5



6
7
8
9
10
11
12
13
14
15

-
+

-
+

-
-
-
+
+
+







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

package client_test

import (
	"context"
	"strings"
38
39
40
41
42
43
44
45

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

45
46
47
48
49
50
51
52







-
+







	content, err := c.GetZettel(context.Background(), abcZid, api.PartContent)
	if err != nil {
		t.Error(err)
		return
	}
	baseContent := string(content)
	for zid, siz := range contentMap {
		content, err = c.GetEvaluatedZettel(context.Background(), zid, api.EncoderHTML)
		content, err = c.GetEvaluatedZettel(context.Background(), zid, api.EncoderHTML, true)
		if err != nil {
			t.Error(err)
			continue
		}
		sContent := string(content)
		prefix := "<p>"
		if !strings.HasPrefix(sContent, prefix) {
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
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







-
+




-
+

















-
+




+
-
+







		}
		got := sContent[len(prefix) : len(content)-len(suffix)]
		if expect := strings.Repeat(baseContent, siz); expect != got {
			t.Errorf("Unexpected content for zettel %q\nExpect: %q\nGot:    %q", zid, expect, got)
		}
	}

	content, err = c.GetEvaluatedZettel(context.Background(), abc10000Zid, api.EncoderHTML)
	content, err = c.GetEvaluatedZettel(context.Background(), abc10000Zid, api.EncoderHTML, true)
	if err != nil {
		t.Error(err)
		return
	}
	checkContentContains(t, abc10000Zid, string(content), "Too\u00a0many\u00a0transclusions")
	checkContentContains(t, abc10000Zid, string(content), "Too many transclusions")
}

func TestZettelTransclusionNoPrivilegeEscalation(t *testing.T) {
	t.Parallel()
	c := getClient()
	c.SetAuth("reader", "reader")

	zettelData, err := c.GetZettelJSON(context.Background(), api.ZidEmoji)
	if err != nil {
		t.Error(err)
		return
	}
	expectedEnc := "base64"
	if got := zettelData.Encoding; expectedEnc != got {
		t.Errorf("Zettel %q: encoding %q expected, but got %q", abcZid, expectedEnc, got)
	}

	content, err := c.GetEvaluatedZettel(context.Background(), abc10Zid, api.EncoderHTML)
	content, err := c.GetEvaluatedZettel(context.Background(), abc10Zid, api.EncoderHTML, true)
	if err != nil {
		t.Error(err)
		return
	}
	expectedContent := "<img src=\"data:image/gif;" + expectedEnc + "," + zettelData.Content
	checkContentContains(t, abc10Zid, string(content), "Error placeholder")
	checkContentContains(t, abc10Zid, string(content), expectedContent)
}

func stringHead(s string) string {
	const maxLen = 40
	if len(s) <= maxLen {
		return s
	}
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
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







-
+





-
+












-
+















-
+




-
+








	)
	recursiveZettel := map[api.ZettelID]api.ZettelID{
		selfRecursiveZid:      selfRecursiveZid,
		indirectRecursive1Zid: indirectRecursive2Zid,
		indirectRecursive2Zid: indirectRecursive1Zid,
	}
	for zid, errZid := range recursiveZettel {
		content, err := c.GetEvaluatedZettel(context.Background(), zid, api.EncoderHTML)
		content, err := c.GetEvaluatedZettel(context.Background(), zid, api.EncoderHTML, true)
		if err != nil {
			t.Error(err)
			continue
		}
		sContent := string(content)
		checkContentContains(t, zid, sContent, "Recursive\u00a0transclusion")
		checkContentContains(t, zid, sContent, "Recursive transclusion")
		checkContentContains(t, zid, sContent, string(errZid))
	}
}
func TestNothingToTransclude(t *testing.T) {
	t.Parallel()
	c := getClient()
	c.SetAuth("owner", "owner")

	const (
		transZid = api.ZettelID("20211020184342")
		emptyZid = api.ZettelID("20211020184300")
	)
	content, err := c.GetEvaluatedZettel(context.Background(), transZid, api.EncoderHTML)
	content, err := c.GetEvaluatedZettel(context.Background(), transZid, api.EncoderHTML, true)
	if err != nil {
		t.Error(err)
		return
	}
	sContent := string(content)
	checkContentContains(t, transZid, sContent, "<!-- Nothing to transclude")
	checkContentContains(t, transZid, sContent, string(emptyZid))
}

func TestSelfEmbedRef(t *testing.T) {
	t.Parallel()
	c := getClient()
	c.SetAuth("owner", "owner")

	const selfEmbedZid = api.ZettelID("20211020185400")
	content, err := c.GetEvaluatedZettel(context.Background(), selfEmbedZid, api.EncoderHTML)
	content, err := c.GetEvaluatedZettel(context.Background(), selfEmbedZid, api.EncoderHTML, true)
	if err != nil {
		t.Error(err)
		return
	}
	checkContentContains(t, selfEmbedZid, string(content), "Self\u00a0embed\u00a0reference")
	checkContentContains(t, selfEmbedZid, string(content), "Self embed reference")
}

func checkContentContains(t *testing.T, zid api.ZettelID, content, expected string) {
	if !strings.Contains(content, expected) {
		t.Helper()
		t.Errorf("Zettel %q should contain %q, but does not: %q", zid, expected, content)
	}
}

Changes to tests/markdown_test.go.

18
19
20
21
22
23
24
25

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

25
26
27
28
29
30
31
32







-
+







	"os"
	"testing"

	"zettelstore.de/c/api"
	"zettelstore.de/z/ast"
	"zettelstore.de/z/encoder"
	_ "zettelstore.de/z/encoder/htmlenc"
	_ "zettelstore.de/z/encoder/sexprenc"
	_ "zettelstore.de/z/encoder/nativeenc"
	_ "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"
41
42
43
44
45
46
47
48

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

48
49
50
51
52
53
54
55







-
+







	Section   string `json:"section"`
}

func TestEncoderAvailability(t *testing.T) {
	t.Parallel()
	encoderMissing := false
	for _, enc := range encodings {
		enc := encoder.Create(enc)
		enc := encoder.Create(enc, nil)
		if enc == nil {
			t.Errorf("No encoder for %q found", enc)
			encoderMissing = true
		}
	}
	if encoderMissing {
		panic("At least one encoder is missing. See test log")
75
76
77
78
79
80
81
82

83
84
85
86
87
88
89

90
91
92
93
94
95
96
75
76
77
78
79
80
81

82
83
84
85
86
87
88

89
90
91
92
93
94
95
96







-
+






-
+







}

func testAllEncodings(t *testing.T, tc markdownTestCase, ast *ast.BlockSlice) {
	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(&buf, ast)
			encoder.Create(enc, nil).WriteBlocks(&buf, ast)
			buf.Reset()
		})
	}
}

func testZmkEncoding(t *testing.T, tc markdownTestCase, ast *ast.BlockSlice) {
	zmkEncoder := encoder.Create(api.EncoderZmk)
	zmkEncoder := encoder.Create(api.EncoderZmk, nil)
	var buf bytes.Buffer
	testID := tc.Example*100 + 1
	t.Run(fmt.Sprintf("Encode zmk %14d", testID), func(st *testing.T) {
		buf.Reset()
		zmkEncoder.WriteBlocks(&buf, ast)
		// gotFirst := buf.String()

Changes to tests/regression_test.go.

1
2

3
4

5
6
7
8
9
10
11
1

2
3

4
5
6
7
8
9
10
11

-
+

-
+







//-----------------------------------------------------------------------------
// Copyright (c) 2020-2022 Detlef Stern
// Copyright (c) 2020-2021 Detlef Stern
//
// This file is part of Zettelstore.
// This file is part of zettelstore.
//
// Zettelstore is licensed 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.
32
33
34
35
36
37
38
39
40
41



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



39
40
41
42
43
44
45
46
47
48







-
-
-
+
+
+







	"zettelstore.de/z/parser"

	_ "zettelstore.de/z/box/dirbox"
)

var encodings = []api.EncodingEnum{
	api.EncoderHTML,
	api.EncoderSexpr,
	api.EncoderText,
	api.EncoderZJSON,
	api.EncoderZJSON,
	api.EncoderNative,
	api.EncoderText,
}

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
117
118
119
120
121
122
123

124
125
126
127
128
129
130
131







-
+







	}
	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 {
	if enc := encoder.Create(enc, nil); enc != nil {
		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))
}
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
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







+
+
+

+








-
-
-
-
+
+
+
+







	}
	ss.Stop(context.Background())
}

type myConfig struct{}

func (*myConfig) AddDefaultValues(m *meta.Meta) *meta.Meta { return m }
func (*myConfig) GetDefaultTitle() string                  { return "" }
func (*myConfig) GetDefaultRole() string                   { return api.ValueRoleZettel }
func (*myConfig) GetDefaultSyntax() string                 { return api.ValueSyntaxZmk }
func (*myConfig) GetDefaultLang() string                   { return "" }
func (*myConfig) GetDefaultVisibility() meta.Visibility    { return meta.VisibilityPublic }
func (*myConfig) GetFooterHTML() string                    { return "" }
func (*myConfig) GetHomeZettel() id.Zid                    { return id.Invalid }
func (*myConfig) GetListPageSize() int                     { return 0 }
func (*myConfig) GetMarkerExternal() string                { return "" }
func (*myConfig) GetSiteName() string                      { return "" }
func (*myConfig) GetYAMLHeader() bool                      { return false }
func (*myConfig) GetZettelFileSyntax() []string            { return nil }

func (*myConfig) GetSimpleMode() bool                      { return false }
func (*myConfig) GetExpertMode() bool                      { return false }
func (*myConfig) GetVisibility(*meta.Meta) meta.Visibility { return meta.VisibilityPublic }
func (*myConfig) GetMaxTransclusions() int                 { return 1024 }
func (*myConfig) GetSimpleMode() bool                          { return false }
func (*myConfig) GetExpertMode() bool                          { return false }
func (cfg *myConfig) GetVisibility(*meta.Meta) meta.Visibility { return cfg.GetDefaultVisibility() }
func (*myConfig) GetMaxTransclusions() int                     { return 1024 }

var testConfig = &myConfig{}

func TestMetaRegression(t *testing.T) {
	t.Parallel()
	wd, err := os.Getwd()
	if err != nil {

Changes to tools/build.go.

19
20
21
22
23
24
25

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







+







	"fmt"
	"io"
	"io/fs"
	"net"
	"os"
	"os/exec"
	"path/filepath"
	"regexp"
	"strings"
	"time"

	"zettelstore.de/z/strfun"
)

var directProxy = []string{"GOPROXY=direct"}
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
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







-
-
-
-
-
-
-
-
+





-
+




+

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


-
-
+
+

+
+
+
-
+

-
+
+
+
+
+
+
+
+







		return "", err
	}
	return strings.TrimFunc(string(content), func(r rune) bool {
		return r <= ' '
	}), nil
}

func getVersion() string {
	base, err := readVersionFile()
	if err != nil {
		base = "dev"
	}
	return base
}

var fossilCheckout = regexp.MustCompile(`^checkout:\s+([0-9a-f]+)\s`)
var dirtyPrefixes = []string{
	"DELETED ", "ADDED ", "UPDATED ", "CONFLICT ", "EDITED ", "RENAMED ", "EXTRA "}

const dirtySuffix = "-dirty"

func readFossilDirty() (string, error) {
func readFossilVersion() (string, error) {
	s, err := executeCommand(nil, "fossil", "status", "--differ")
	if err != nil {
		return "", err
	}
	var hash, suffix string
	for _, line := range strfun.SplitLines(s) {
		if hash == "" {
			if m := fossilCheckout.FindStringSubmatch(line); len(m) > 0 {
				hash = m[1][:10]
				if suffix != "" {
					return hash + suffix, nil
				}
				continue
			}
		}
		if suffix == "" {
		for _, prefix := range dirtyPrefixes {
			if strings.HasPrefix(line, prefix) {
				return dirtySuffix, nil
			}
		}
	}
	return "", nil
			for _, prefix := range dirtyPrefixes {
				if strings.HasPrefix(line, prefix) {
					suffix = dirtySuffix
					if hash != "" {
						return hash + suffix, nil
					}
					break
				}
			}
		}
	}
	return hash + suffix, nil
}

func getFossilDirty() string {
	fossil, err := readFossilDirty()
func getVersionData() (string, string) {
	base, err := readVersionFile()
	if err != nil {
		base = "dev"
	}
	if fossil, err2 := readFossilVersion(); err2 == nil {
		return ""
		return base, fossil
	}
	return fossil
	return base, ""
}

func calcVersion(base, vcs string) string { return base + "+" + vcs }

func getVersion() string {
	base, vcs := getVersionData()
	return calcVersion(base, vcs)
}

func findExec(cmd string) string {
	if path, err := executeCommand(nil, "which", cmd); err == nil && path != "" {
		return strings.TrimSpace(path)
	}
	return ""
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
207
208
209
210
211
212
213





214
215
216
217
218







219
220
221
222
223
224
225



226
227
228
229
230
231
232
233
234
235







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







}

func checkUnparam(forRelease bool) error {
	path, err := findExecStrict("unparam", forRelease)
	if path == "" {
		return err
	}
	// out, err := executeCommand(nil, path, "./...")
	// if err != nil {
	// 	fmt.Fprintln(os.Stderr, "Some unparam problems found")
	// 	if len(out) > 0 {
	// 		fmt.Fprintln(os.Stderr, out)
	out, err := executeCommand(nil, path, "./...")
	if err != nil {
		fmt.Fprintln(os.Stderr, "Some unparam problems found")
		if len(out) > 0 {
			fmt.Fprintln(os.Stderr, out)
	// 	}
	// }
	// if forRelease {
	// 	if out2, err2 := executeCommand(nil, path, "-exported", "-tests", "./..."); err2 != nil {
	// 		fmt.Fprintln(os.Stderr, "Some optional unparam problems found")
	// 		if len(out2) > 0 {
	// 			fmt.Fprintln(os.Stderr, out2)
		}
	}
	if forRelease {
		if out2, err2 := executeCommand(nil, path, "-exported", "-tests", "./..."); err2 != nil {
			fmt.Fprintln(os.Stderr, "Some optional unparam problems found")
			if len(out2) > 0 {
				fmt.Fprintln(os.Stderr, out2)
	// 		}
	// 	}
	// }
			}
		}
	}
	return err
}

func findExecStrict(cmd string, forRelease bool) (string, error) {
	path := findExec(cmd)
	if path != "" || !forRelease {
		return path, nil
324
325
326
327
328
329
330
331

332
333
334
335
336
337
338
344
345
346
347
348
349
350

351
352
353
354
355
356
357
358







-
+







	if len(out) > 0 {
		fmt.Println(out)
	}
	return nil
}

func cmdManual() error {
	base := getReleaseVersionData()
	base, _ := getReleaseVersionData()
	return createManualZip(".", base)
}

func createManualZip(path, base string) error {
	manualPath := filepath.Join("docs", "manual")
	entries, err := os.ReadDir(manualPath)
	if err != nil {
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
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







-
+
-
-
-
-
+

-
+

+
+
+
-
+






-
+

















-
+







		return err
	}
	defer manualFile.Close()
	_, err = io.Copy(w, manualFile)
	return err
}

func getReleaseVersionData() string {
func getReleaseVersionData() (string, string) {
	if fossil := getFossilDirty(); fossil != "" {
		fmt.Fprintln(os.Stderr, "Warning: releasing a dirty version")
	}
	base := getVersion()
	base, fossil := getVersionData()
	if strings.HasSuffix(base, "dev") {
		return base[:len(base)-3] + "preview-" + time.Now().Format("20060102")
		base = base[:len(base)-3] + "preview-" + time.Now().Format("20060102")
	}
	if strings.HasSuffix(fossil, dirtySuffix) {
		fmt.Fprintf(os.Stderr, "Warning: releasing a dirty version %v\n", fossil)
	}
	return base
	return base, fossil
}

func cmdRelease() error {
	if err := cmdCheck(true); err != nil {
		return err
	}
	base := getReleaseVersionData()
	base, fossil := getReleaseVersionData()
	releases := []struct {
		arch string
		os   string
		env  []string
		name string
	}{
		{"amd64", "linux", nil, "zettelstore"},
		{"arm", "linux", []string{"GOARM=6"}, "zettelstore"},
		{"amd64", "darwin", nil, "zettelstore"},
		{"arm64", "darwin", nil, "zettelstore"},
		{"amd64", "windows", nil, "zettelstore.exe"},
	}
	for _, rel := range releases {
		env := append([]string{}, rel.env...)
		env = append(env, "GOARCH="+rel.arch, "GOOS="+rel.os)
		env = append(env, directProxy...)
		zsName := filepath.Join("releases", rel.name)
		if err := doBuild(env, base, zsName); err != nil {
		if err := doBuild(env, calcVersion(base, fossil), zsName); err != nil {
			return err
		}
		zipName := fmt.Sprintf("zettelstore-%v-%v-%v.zip", base, rel.os, rel.arch)
		if err := createReleaseZip(zsName, zipName, rel.name); err != nil {
			return err
		}
		if err := os.Remove(zsName); err != nil {

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



-
+











-







//-----------------------------------------------------------------------------
// Copyright (c) 2020-2022 Detlef Stern
//
// This file is part of Zettelstore.
// This file is part of zettelstore.
//
// Zettelstore is licensed 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"
	"math/rand"
	"net/http"
	"time"

	"zettelstore.de/c/api"
	"zettelstore.de/z/auth"
	"zettelstore.de/z/auth/cred"
	"zettelstore.de/z/domain/id"
	"zettelstore.de/z/domain/meta"
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
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







-
-
-
-
+




-
+







-
+











-
+







		token:     token,
		port:      port,
		ucGetUser: NewGetUser(authz, port),
	}
}

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

	if identMeta == nil || err != nil {
		uc.log.Info().Str("ident", ident).Err(err).HTTPIP(r).Msg("No user with given ident found")
		uc.log.Info().Str("ident", ident).Err(err).Msg("No user with given ident found")
		compensateCompare()
		return nil, err
	}

	if hashCred, ok := identMeta.Get(api.KeyCredential); ok {
		ok, err = cred.CompareHashAndCredential(hashCred, identMeta.Zid, ident, credential)
		if err != nil {
			uc.log.Info().Str("ident", ident).Err(err).HTTPIP(r).Msg("Error while comparing credentials")
			uc.log.Info().Str("ident", ident).Err(err).Msg("Error while comparing credentials")
			return nil, err
		}
		if ok {
			token, err2 := uc.token.GetToken(identMeta, d, k)
			if err2 != nil {
				uc.log.Info().Str("ident", ident).Err(err).Msg("Unable to produce authentication token")
				return nil, err2
			}
			uc.log.Info().Str("user", ident).Msg("Successful")
			return token, nil
		}
		uc.log.Info().Str("ident", ident).HTTPIP(r).Msg("Credentials don't match")
		uc.log.Info().Str("ident", ident).Msg("Credentials don't match")
		return nil, nil
	}
	uc.log.Info().Str("ident", ident).Msg("No credential stored")
	compensateCompare()
	return nil, nil
}

Changes to usecase/create_zettel.go.

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







-
+


-
+








-
+


-
+


-
-
-
+
+
+








-
-
-
-
+
+
+
+







		port:     port,
	}
}

// PrepareCopy the zettel for further modification.
func (*CreateZettel) PrepareCopy(origZettel domain.Zettel) domain.Zettel {
	m := origZettel.Meta.Clone()
	if title, found := m.Get(api.KeyTitle); found {
	if title, ok := m.Get(api.KeyTitle); ok {
		m.Set(api.KeyTitle, prependTitle(title, "Copy", "Copy of "))
	}
	if readonly, found := m.Get(api.KeyReadOnly); found {
	if readonly, ok := m.Get(api.KeyReadOnly); ok {
		m.Set(api.KeyReadOnly, copyReadonly(readonly))
	}
	content := origZettel.Content
	content.TrimSpace()
	return domain.Zettel{Meta: m, Content: content}
}

// PrepareFolge the zettel for further modification.
func (*CreateZettel) PrepareFolge(origZettel domain.Zettel) domain.Zettel {
func (uc *CreateZettel) PrepareFolge(origZettel domain.Zettel) domain.Zettel {
	origMeta := origZettel.Meta
	m := meta.New(id.Invalid)
	if title, found := origMeta.Get(api.KeyTitle); found {
	if title, ok := origMeta.Get(api.KeyTitle); ok {
		m.Set(api.KeyTitle, prependTitle(title, "Folge", "Folge of "))
	}
	m.SetNonEmpty(api.KeyRole, origMeta.GetDefault(api.KeyRole, ""))
	m.SetNonEmpty(api.KeyTags, origMeta.GetDefault(api.KeyTags, ""))
	m.SetNonEmpty(api.KeySyntax, origMeta.GetDefault(api.KeySyntax, ""))
	m.Set(api.KeyRole, config.GetRole(origMeta, uc.rtConfig))
	m.Set(api.KeyTags, origMeta.GetDefault(api.KeyTags, ""))
	m.Set(api.KeySyntax, uc.rtConfig.GetDefaultSyntax())
	m.Set(api.KeyPrecursor, origMeta.Zid.String())
	return domain.Zettel{Meta: m, Content: domain.NewContent(nil)}
}

// PrepareNew the zettel for further modification.
func (*CreateZettel) PrepareNew(origZettel domain.Zettel) domain.Zettel {
	m := meta.New(id.Invalid)
	om := origZettel.Meta
	m.SetNonEmpty(api.KeyTitle, om.GetDefault(api.KeyTitle, ""))
	m.SetNonEmpty(api.KeyRole, om.GetDefault(api.KeyRole, ""))
	m.SetNonEmpty(api.KeyTags, om.GetDefault(api.KeyTags, ""))
	m.SetNonEmpty(api.KeySyntax, om.GetDefault(api.KeySyntax, ""))
	m.Set(api.KeyTitle, om.GetDefault(api.KeyTitle, ""))
	m.Set(api.KeyRole, om.GetDefault(api.KeyRole, ""))
	m.Set(api.KeyTags, om.GetDefault(api.KeyTags, ""))
	m.Set(api.KeySyntax, om.GetDefault(api.KeySyntax, ""))

	const prefixLen = len(meta.NewPrefix)
	for _, pair := range om.PairsRest() {
		if key := pair.Key; len(key) > prefixLen && key[0:prefixLen] == meta.NewPrefix {
			m.Set(key[prefixLen:], pair.Value)
		}
	}
109
110
111
112
113
114
115


116







117
118
119
120
121
122
123
124
109
110
111
112
113
114
115
116
117

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







+
+
-
+
+
+
+
+
+
+









// Run executes the use case.
func (uc *CreateZettel) Run(ctx context.Context, zettel domain.Zettel) (id.Zid, error) {
	m := zettel.Meta
	if m.Zid.IsValid() {
		return m.Zid, nil // TODO: new error: already exists
	}
	if title, ok := m.Get(api.KeyTitle); !ok || title == "" {
		m.Set(api.KeyTitle, uc.rtConfig.GetDefaultTitle())

	}
	if role, ok := m.Get(api.KeyRole); !ok || role == "" {
		m.Set(api.KeyRole, uc.rtConfig.GetDefaultRole())
	}
	if syntax, ok := m.Get(api.KeySyntax); !ok || syntax == "" {
		m.Set(api.KeySyntax, uc.rtConfig.GetDefaultSyntax())
	}
	m.Delete(api.KeyModified)
	m.YamlSep = uc.rtConfig.GetYAMLHeader()

	zettel.Content.TrimSpace()
	zid, err := uc.port.CreateZettel(ctx, zettel)
	uc.log.Info().User(ctx).Zid(zid).Err(err).Msg("Create zettel")
	return zid, err
}

Changes to usecase/evaluate.go.

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







-
+









-
+




-
+

-
+












		rtConfig:  rtConfig,
		getZettel: getZettel,
		getMeta:   getMeta,
	}
}

// Run executes the use case.
func (uc *Evaluate) Run(ctx context.Context, zid id.Zid, syntax string) (*ast.ZettelNode, error) {
func (uc *Evaluate) Run(ctx context.Context, zid id.Zid, syntax string, env *evaluator.Environment) (*ast.ZettelNode, error) {
	zettel, err := uc.getZettel.Run(ctx, zid)
	if err != nil {
		return nil, err
	}
	zn, err := parser.ParseZettel(zettel, syntax, uc.rtConfig), nil
	if err != nil {
		return nil, err
	}

	evaluator.EvaluateZettel(ctx, uc, uc.rtConfig, zn)
	evaluator.EvaluateZettel(ctx, uc, env, uc.rtConfig, zn)
	return zn, nil
}

// RunMetadata executes the use case for a metadata value.
func (uc *Evaluate) RunMetadata(ctx context.Context, value string) ast.InlineSlice {
func (uc *Evaluate) RunMetadata(ctx context.Context, value string, env *evaluator.Environment) ast.InlineSlice {
	is := parser.ParseMetadata(value)
	evaluator.EvaluateInline(ctx, uc, uc.rtConfig, &is)
	evaluator.EvaluateInline(ctx, uc, env, 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.
func (uc *Evaluate) GetZettel(ctx context.Context, zid id.Zid) (domain.Zettel, error) {
	return uc.getZettel.Run(ctx, zid)
}

Added usecase/list_meta.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
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
//-----------------------------------------------------------------------------
// Copyright (c) 2020-2021 Detlef Stern
//
// This file is part of zettelstore.
//
// Zettelstore is licensed 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/z/domain/meta"
	"zettelstore.de/z/search"
)

// ListMetaPort is the interface used by this use case.
type ListMetaPort interface {
	// SelectMeta returns all zettel meta data that match the selection criteria.
	SelectMeta(ctx context.Context, s *search.Search) ([]*meta.Meta, error)
}

// ListMeta is the data for this use case.
type ListMeta struct {
	port ListMetaPort
}

// NewListMeta creates a new use case.
func NewListMeta(port ListMetaPort) ListMeta {
	return ListMeta{port: port}
}

// Run executes the use case.
func (uc ListMeta) Run(ctx context.Context, s *search.Search) ([]*meta.Meta, error) {
	return uc.port.SelectMeta(ctx, s)
}

Added usecase/list_role.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
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
//-----------------------------------------------------------------------------
// 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 usecase

import (
	"context"
	"sort"

	"zettelstore.de/c/api"
	"zettelstore.de/z/box"
	"zettelstore.de/z/domain/meta"
	"zettelstore.de/z/search"
	"zettelstore.de/z/strfun"
)

// ListRolePort is the interface used by this use case.
type ListRolePort interface {
	// SelectMeta returns all zettel meta data that match the selection criteria.
	SelectMeta(ctx context.Context, s *search.Search) ([]*meta.Meta, error)
}

// ListRole is the data for this use case.
type ListRole struct {
	port ListRolePort
}

// NewListRole creates a new use case.
func NewListRole(port ListRolePort) ListRole {
	return ListRole{port: port}
}

// Run executes the use case.
func (uc ListRole) Run(ctx context.Context) ([]string, error) {
	metas, err := uc.port.SelectMeta(box.NoEnrichContext(ctx), nil)
	if err != nil {
		return nil, err
	}
	roles := make(strfun.Set, 256)
	for _, m := range metas {
		if role, ok := m.Get(api.KeyRole); ok && role != "" {
			roles.Set(role)
		}
	}
	result := make([]string, 0, len(roles))
	for role := range roles {
		result = append(result, role)
	}
	sort.Strings(result)
	return result, nil
}

Added usecase/list_tags.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
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
//-----------------------------------------------------------------------------
// Copyright (c) 2020-2021 Detlef Stern
//
// This file is part of zettelstore.
//
// Zettelstore is licensed 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/meta"
	"zettelstore.de/z/search"
)

// ListTagsPort is the interface used by this use case.
type ListTagsPort interface {
	// SelectMeta returns all zettel meta data that match the selection criteria.
	SelectMeta(ctx context.Context, s *search.Search) ([]*meta.Meta, error)
}

// ListTags is the data for this use case.
type ListTags struct {
	port ListTagsPort
}

// NewListTags creates a new use case.
func NewListTags(port ListTagsPort) ListTags {
	return ListTags{port: port}
}

// TagData associates tags with a list of all zettel meta that use this tag
type TagData map[string][]*meta.Meta

// Run executes the use case.
func (uc ListTags) Run(ctx context.Context, minCount int) (TagData, error) {
	metas, err := uc.port.SelectMeta(ctx, nil)
	if err != nil {
		return nil, err
	}
	result := make(TagData)
	for _, m := range metas {
		if tl, ok := m.GetList(api.KeyAllTags); ok && len(tl) > 0 {
			for _, t := range tl {
				result[t] = append(result[t], m)
			}
		}
	}
	if minCount > 1 {
		for t, ms := range result {
			if len(ms) < minCount {
				delete(result, t)
			}
		}
	}
	return result, nil
}

Deleted usecase/lists.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















































































































































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

import (
	"context"

	"zettelstore.de/c/api"
	"zettelstore.de/z/box"
	"zettelstore.de/z/domain/meta"
	"zettelstore.de/z/parser"
	"zettelstore.de/z/search"
)

// ListMetaPort is the interface used by this use case.
type ListMetaPort interface {
	// SelectMeta returns all zettel metadata that match the selection criteria.
	SelectMeta(ctx context.Context, s *search.Search) ([]*meta.Meta, error)
}

// ListMeta is the data for this use case.
type ListMeta struct {
	port ListMetaPort
}

// NewListMeta creates a new use case.
func NewListMeta(port ListMetaPort) ListMeta {
	return ListMeta{port: port}
}

// Run executes the use case.
func (uc ListMeta) Run(ctx context.Context, s *search.Search) ([]*meta.Meta, error) {
	return uc.port.SelectMeta(ctx, s)
}

// -------- List roles -------------------------------------------------------

// ListSyntaxPort is the interface used by this use case.
type ListSyntaxPort interface {
	// SelectMeta returns all zettel metadata that match the selection criteria.
	SelectMeta(ctx context.Context, s *search.Search) ([]*meta.Meta, error)
}

// ListSyntax is the data for this use case.
type ListSyntax struct {
	port ListSyntaxPort
}

// NewListSyntax creates a new use case.
func NewListSyntax(port ListSyntaxPort) ListSyntax {
	return ListSyntax{port: port}
}

// Run executes the use case.
func (uc ListSyntax) Run(ctx context.Context) (meta.Arrangement, error) {
	var s *search.Search
	s = s.AddExpr(api.KeySyntax, "") // We look for all metadata with a syntax key
	metas, err := uc.port.SelectMeta(box.NoEnrichContext(ctx), s)
	if err != nil {
		return nil, err
	}
	result := meta.CreateArrangement(metas, api.KeySyntax)
	for _, syn := range parser.GetSyntaxes() {
		if _, found := result[syn]; !found {
			result[syn] = nil
		}
	}
	return result, nil
}

// -------- List roles -------------------------------------------------------

// ListRolesPort is the interface used by this use case.
type ListRolesPort interface {
	// SelectMeta returns all zettel metadata that match the selection criteria.
	SelectMeta(ctx context.Context, s *search.Search) ([]*meta.Meta, error)
}

// ListRoles is the data for this use case.
type ListRoles struct {
	port ListRolesPort
}

// NewListRoles creates a new use case.
func NewListRoles(port ListRolesPort) ListRoles {
	return ListRoles{port: port}
}

// Run executes the use case.
func (uc ListRoles) Run(ctx context.Context) (meta.Arrangement, error) {
	var s *search.Search
	s = s.AddExpr(api.KeyRole, "") // We look for all metadata with a role key
	metas, err := uc.port.SelectMeta(box.NoEnrichContext(ctx), s)
	if err != nil {
		return nil, err
	}
	return meta.CreateArrangement(metas, api.KeyRole), nil
}

// -------- List tags --------------------------------------------------------

// ListTagsPort is the interface used by this use case.
type ListTagsPort interface {
	// SelectMeta returns all zettel metadata that match the selection criteria.
	SelectMeta(ctx context.Context, s *search.Search) ([]*meta.Meta, error)
}

// ListTags is the data for this use case.
type ListTags struct {
	port ListTagsPort
}

// NewListTags creates a new use case.
func NewListTags(port ListTagsPort) ListTags {
	return ListTags{port: port}
}

// Run executes the use case.
func (uc ListTags) Run(ctx context.Context, minCount int) (meta.Arrangement, error) {
	var s *search.Search
	s = s.AddExpr(api.KeyTags, "") // We look for all metadata with a tag
	metas, err := uc.port.SelectMeta(ctx, s)
	if err != nil {
		return nil, err
	}
	result := meta.CreateArrangement(metas, api.KeyAllTags)
	if minCount > 1 {
		for t, ms := range result {
			if len(ms) < minCount {
				delete(result, t)
			}
		}
	}
	return result, nil
}

Changes to usecase/order.go.

1
2

3
4

5
6
7
8
9
10
11
1

2
3

4
5
6
7
8
9
10
11

-
+

-
+







//-----------------------------------------------------------------------------
// Copyright (c) 2021-2022 Detlef Stern
// Copyright (c) 2021 Detlef Stern
//
// This file is part of Zettelstore.
// This file is part of zettelstore.
//
// Zettelstore is licensed 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
35
36
37
38
39
40
41
42

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

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







-
+












	return ZettelOrder{port: port, evaluate: evaluate}
}

// Run executes the use case.
func (uc ZettelOrder) Run(ctx context.Context, zid id.Zid, syntax string) (
	start *meta.Meta, result []*meta.Meta, err error,
) {
	zn, err := uc.evaluate.Run(ctx, zid, syntax)
	zn, err := uc.evaluate.Run(ctx, zid, syntax, nil)
	if err != nil {
		return nil, nil, err
	}
	for _, ref := range collect.Order(zn) {
		if collectedZid, err2 := id.Parse(ref.URL.Path); err2 == nil {
			if m, err3 := uc.port.GetMeta(ctx, collectedZid); err3 == nil {
				result = append(result, m)
			}
		}
	}
	return zn.Meta, result, nil
}

Changes to usecase/unlinked_refs.go.

17
18
19
20
21
22
23
24

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

42
43
44
45
46
47
48
49

50
51
52
53
54
55
56
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







-
+
















-
+







-
+








	"zettelstore.de/c/api"
	"zettelstore.de/z/ast"
	"zettelstore.de/z/config"
	"zettelstore.de/z/domain"
	"zettelstore.de/z/domain/id"
	"zettelstore.de/z/domain/meta"
	"zettelstore.de/z/encoder/textenc"
	"zettelstore.de/z/encoder"
	"zettelstore.de/z/evaluator"
	"zettelstore.de/z/parser"
	"zettelstore.de/z/search"
)

// UnlinkedReferencesPort is the interface used by this use case.
type UnlinkedReferencesPort interface {
	GetMeta(ctx context.Context, zid id.Zid) (*meta.Meta, error)
	GetZettel(ctx context.Context, zid id.Zid) (domain.Zettel, error)
	SelectMeta(ctx context.Context, s *search.Search) ([]*meta.Meta, error)
}

// UnlinkedReferences is the data for this use case.
type UnlinkedReferences struct {
	port     UnlinkedReferencesPort
	rtConfig config.Config
	encText  *textenc.Encoder
	encText  encoder.Encoder
}

// NewUnlinkedReferences creates a new use case.
func NewUnlinkedReferences(port UnlinkedReferencesPort, rtConfig config.Config) UnlinkedReferences {
	return UnlinkedReferences{
		port:     port,
		rtConfig: rtConfig,
		encText:  textenc.Create(),
		encText:  encoder.Create(api.EncoderText, nil),
	}
}

// Run executes the usecase with already evaluated title value.
func (uc *UnlinkedReferences) Run(ctx context.Context, title string, s *search.Search) ([]*meta.Meta, error) {
	words := makeWords(title)
	if len(words) == 0 {
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
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







-
+















-
+







		v.text = v.joinWords(words)

		for _, pair := range zettel.Meta.Pairs() {
			if meta.Type(pair.Key) != meta.TypeZettelmarkup {
				continue
			}
			is := parser.ParseMetadata(pair.Value)
			evaluator.EvaluateInline(ctx, uc.port, uc.rtConfig, &is)
			evaluator.EvaluateInline(ctx, uc.port, nil, uc.rtConfig, &is)
			ast.Walk(&v, &is)
			if v.found {
				result = append(result, cand)
				continue candLoop
			}
		}

		syntax := zettel.Meta.GetDefault(api.KeySyntax, "")
		if !parser.IsTextParser(syntax) {
			continue
		}
		zn, err := parser.ParseZettel(zettel, syntax, nil), nil
		if err != nil {
			continue
		}
		evaluator.EvaluateZettel(ctx, uc.port, uc.rtConfig, zn)
		evaluator.EvaluateZettel(ctx, uc.port, nil, uc.rtConfig, zn)
		ast.Walk(&v, &zn.Ast)
		if v.found {
			result = append(result, cand)
		}
	}
	return result
}

Changes to web/adapter/api/content_type.go.

17
18
19
20
21
22
23
24
25


26
27
28



29
30
31
32
33
34
35
36
37
38
39

40
41
42
43
44
45
46
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







-
-
+
+
-
-
-
+
+
+











+







	ctHTML      = "text/html; charset=utf-8"
	ctJSON      = "application/json"
	ctPlainText = "text/plain; charset=utf-8"
	ctSVG       = "image/svg+xml"
)

var mapEncoding2CT = map[api.EncodingEnum]string{
	api.EncoderHTML:  ctHTML,
	api.EncoderSexpr: ctPlainText,
	api.EncoderHTML:   ctHTML,
	api.EncoderNative: ctPlainText,
	api.EncoderText:  ctPlainText,
	api.EncoderZJSON: ctJSON,
	api.EncoderZmk:   ctPlainText,
	api.EncoderZJSON:  ctJSON,
	api.EncoderText:   ctPlainText,
	api.EncoderZmk:    ctPlainText,
}

func encoding2ContentType(enc api.EncodingEnum) string {
	if ct, ok := mapEncoding2CT[enc]; ok {
		return ct
	}
	return "application/octet-stream"
}

var mapSyntax2CT = map[string]string{
	"css":               "text/css; charset=utf-8",
	api.ValueSyntaxDraw: ctSVG,
	api.ValueSyntaxGif:  "image/gif",
	api.ValueSyntaxHTML: "text/html; charset=utf-8",
	"jpeg":              "image/jpeg",
	"jpg":               "image/jpeg",
	"js":                "text/javascript; charset=utf-8",
	"pdf":               "application/pdf",
	"png":               "image/png",

Changes to web/adapter/api/get_eval_zettel.go.

11
12
13
14
15
16
17

18
19

20

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

35









36

37
38
39
40
41
42

43
44
45
46
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







+


+

+













-
+

+
+
+
+
+
+
+
+
+
-
+





-
+




package api

import (
	"net/http"

	"zettelstore.de/c/api"
	"zettelstore.de/z/ast"
	"zettelstore.de/z/domain"
	"zettelstore.de/z/domain/id"
	"zettelstore.de/z/encoder"
	"zettelstore.de/z/evaluator"
	"zettelstore.de/z/usecase"
	"zettelstore.de/z/web/adapter"
)

// MakeGetEvalZettelHandler creates a new HTTP handler to return a evaluated zettel.
func (a *API) MakeGetEvalZettelHandler(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()
		q := r.URL.Query()
		enc, encStr := getEncoding(r, q, encoder.GetDefaultEncoding())
		enc, encStr := adapter.GetEncoding(r, q, encoder.GetDefaultEncoding())
		part := getPart(q, partContent)
		var env evaluator.Environment
		if q.Has(api.QueryKeyEmbed) {
			env.GetImageMaterial = func(zettel domain.Zettel, syntax string) ast.InlineEmbedNode {
				return &ast.EmbedBLOBNode{
					Blob:   zettel.Content.AsBytes(),
					Syntax: syntax,
				}
			}
		}
		zn, err := evaluate.Run(ctx, zid, q.Get(api.KeySyntax))
		zn, err := evaluate.Run(ctx, zid, q.Get(api.KeySyntax), &env)
		if err != nil {
			a.reportUsecaseError(w, err)
			return
		}
		evalMeta := func(value string) ast.InlineSlice {
			return evaluate.RunMetadata(ctx, value)
			return evaluate.RunMetadata(ctx, value, &env)
		}
		a.writeEncodedZettelPart(w, zn, evalMeta, enc, encStr, part)
	}
}

Deleted web/adapter/api/get_lists.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








































































-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
//-----------------------------------------------------------------------------
// 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 api provides api handlers for web requests.
package api

import (
	"bytes"
	"net/http"
	"strconv"

	"zettelstore.de/c/api"
	"zettelstore.de/z/domain/meta"
	"zettelstore.de/z/usecase"
)

// MakeListMapMetaHandler creates a new HTTP handler to retrieve mappings of
// metadata values of a specific key to the list of zettel IDs, which contain
// this value.
func (a *API) MakeListMapMetaHandler(listRole usecase.ListRoles, listTags usecase.ListTags) http.HandlerFunc {
	return func(w http.ResponseWriter, r *http.Request) {
		var ar meta.Arrangement
		query := r.URL.Query()
		iMinCount, err := strconv.Atoi(query.Get(api.QueryKeyMin))
		if err != nil || iMinCount < 0 {
			iMinCount = 0
		}
		ctx := r.Context()
		key := query.Get(api.QueryKeyKey)
		switch key {
		case api.KeyRole:
			ar, err = listRole.Run(ctx)
		case api.KeyTags:
			ar, err = listTags.Run(ctx, iMinCount)
		default:
			a.log.Info().Str("key", key).Msg("illegal key for retrieving meta map")
			http.Error(w, http.StatusText(http.StatusBadRequest), http.StatusBadRequest)
			return
		}
		if err != nil {
			a.reportUsecaseError(w, err)
			return
		}

		mm := make(api.MapMeta, len(ar))
		for tag, metaList := range ar {
			zidList := make([]api.ZettelID, 0, len(metaList))
			for _, m := range metaList {
				zidList = append(zidList, api.ZettelID(m.Zid.String()))
			}
			mm[tag] = zidList
		}

		var buf bytes.Buffer
		err = encodeJSONData(&buf, api.MapListJSON{Map: mm})
		if err != nil {
			a.log.Fatal().Err(err).Msg("Unable to store map list in buffer")
			http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError)
			return
		}

		err = writeBuffer(w, &buf, ctJSON)
		a.log.IfErr(err).Str("key", key).Msg("write meta map")
	}
}

Changes to web/adapter/api/get_parsed_zettel.go.

14
15
16
17
18
19
20

21
22
23

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

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







54

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







+



+














-
+















+
+
+
+
+
+
+
-
+







import (
	"bytes"
	"fmt"
	"net/http"

	"zettelstore.de/c/api"
	"zettelstore.de/z/ast"
	"zettelstore.de/z/config"
	"zettelstore.de/z/domain/id"
	"zettelstore.de/z/encoder"
	"zettelstore.de/z/parser"
	"zettelstore.de/z/strfun"
	"zettelstore.de/z/usecase"
	"zettelstore.de/z/web/adapter"
)

// MakeGetParsedZettelHandler creates a new HTTP handler to return a parsed zettel.
func (a *API) MakeGetParsedZettelHandler(parseZettel usecase.ParseZettel) 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
		}

		q := r.URL.Query()
		enc, encStr := getEncoding(r, q, encoder.GetDefaultEncoding())
		enc, encStr := adapter.GetEncoding(r, q, encoder.GetDefaultEncoding())
		part := getPart(q, partContent)
		zn, err := parseZettel.Run(r.Context(), zid, q.Get(api.KeySyntax))
		if err != nil {
			a.reportUsecaseError(w, err)
			return
		}
		a.writeEncodedZettelPart(w, zn, parser.ParseMetadata, enc, encStr, part)
	}
}

func (a *API) writeEncodedZettelPart(
	w http.ResponseWriter, zn *ast.ZettelNode,
	evalMeta encoder.EvalMetaFunc,
	enc api.EncodingEnum, encStr string, part partType,
) {
	env := encoder.Environment{
		Lang:           config.GetLang(zn.InhMeta, a.rtConfig),
		Xhtml:          false,
		MarkerExternal: "",
		NewWindow:      false,
		IgnoreMeta:     strfun.NewSet(api.KeyLang),
	}
	encdr := encoder.Create(enc)
	encdr := encoder.Create(enc, &env)
	if encdr == nil {
		adapter.BadRequest(w, fmt.Sprintf("Zettel %q not available in encoding %q", zn.Meta.Zid.String(), encStr))
		return
	}
	var err error
	var buf bytes.Buffer
	switch part {

Added web/adapter/api/get_role_list.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
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
//-----------------------------------------------------------------------------
// Copyright (c) 2020-2021 Detlef Stern
//
// This file is part of zettelstore.
//
// Zettelstore is licensed 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 (
	"bytes"
	"net/http"

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

// MakeListRoleHandler creates a new HTTP handler for the use case "list roles".
func (a *API) MakeListRoleHandler(listRole usecase.ListRole) http.HandlerFunc {
	return func(w http.ResponseWriter, r *http.Request) {
		roleList, err := listRole.Run(r.Context())
		if err != nil {
			a.reportUsecaseError(w, err)
			return
		}

		var buf bytes.Buffer
		err = encodeJSONData(&buf, api.RoleListJSON{Roles: roleList})
		if err != nil {
			a.log.Fatal().Err(err).Msg("Unable to store role list in buffer")
			http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError)
			return
		}

		err = writeBuffer(w, &buf, ctJSON)
		a.log.IfErr(err).Msg("Write Roles")
	}
}

Added web/adapter/api/get_tags_list.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
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
//-----------------------------------------------------------------------------
// 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 api provides api handlers for web requests.
package api

import (
	"bytes"
	"net/http"
	"strconv"

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

// MakeListTagsHandler creates a new HTTP handler for the use case "list some zettel".
func (a *API) MakeListTagsHandler(listTags usecase.ListTags) http.HandlerFunc {
	return func(w http.ResponseWriter, r *http.Request) {
		iMinCount, err := strconv.Atoi(r.URL.Query().Get("min"))
		if err != nil || iMinCount < 0 {
			iMinCount = 0
		}
		tagData, err := listTags.Run(r.Context(), iMinCount)
		if err != nil {
			a.reportUsecaseError(w, err)
			return
		}

		tagMap := make(map[string][]api.ZettelID, len(tagData))
		for tag, metaList := range tagData {
			zidList := make([]api.ZettelID, 0, len(metaList))
			for _, m := range metaList {
				zidList = append(zidList, api.ZettelID(m.Zid.String()))
			}
			tagMap[tag] = zidList
		}

		var buf bytes.Buffer
		err = encodeJSONData(&buf, api.TagListJSON{Tags: tagMap})
		if err != nil {
			a.log.Fatal().Err(err).Msg("Unable to store tag list in buffer")
			http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError)
			return
		}

		err = writeBuffer(w, &buf, ctJSON)
		a.log.IfErr(err).Msg("Write Tags")
	}
}

Changes to web/adapter/api/get_unlinked_refs.go.

13
14
15
16
17
18
19
20

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

20
21
22
23
24
25
26
27







-
+







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

	"zettelstore.de/c/api"
	"zettelstore.de/z/domain/id"
	"zettelstore.de/z/encoder/textenc"
	"zettelstore.de/z/encoder"
	"zettelstore.de/z/usecase"
	"zettelstore.de/z/web/adapter"
)

// MakeListUnlinkedMetaHandler creates a new HTTP handler for the use case "list unlinked references".
func (a *API) MakeListUnlinkedMetaHandler(
	getMeta usecase.GetMeta,
40
41
42
43
44
45
46
47
48
49
50
51
52
53







54
55
56
57
58
59
60
61
40
41
42
43
44
45
46







47
48
49
50
51
52
53

54
55
56
57
58
59
60







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







			a.reportUsecaseError(w, err)
			return
		}

		q := r.URL.Query()
		phrase := q.Get(api.QueryKeyPhrase)
		if phrase == "" {
			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()
			zmkTitle := zm.GetDefault(api.KeyTitle, "")
			isTitle := evaluate.RunMetadata(ctx, zmkTitle, nil)
			encdr := encoder.Create(api.EncoderText, nil)
			var b strings.Builder
			_, err = encdr.WriteInlines(&b, &isTitle)
			if err == nil {
				phrase = b.String()
				}
			}
		}

		metaList, err := unlinkedRefs.Run(
			ctx, phrase, adapter.AddUnlinkedRefsToSearch(adapter.GetSearch(q), zm))
		if err != nil {
			a.reportUsecaseError(w, err)

Changes to web/adapter/api/get_zettel.go.

14
15
16
17
18
19
20

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







+







import (
	"bytes"
	"context"
	"net/http"

	"zettelstore.de/c/api"
	"zettelstore.de/z/box"
	"zettelstore.de/z/config"
	"zettelstore.de/z/domain"
	"zettelstore.de/z/domain/id"
	"zettelstore.de/z/usecase"
)

// MakeGetZettelHandler creates a new HTTP handler to return a zettel.
func (a *API) MakeGetZettelHandler(getZettel usecase.GetZettel) http.HandlerFunc {
72
73
74
75
76
77
78
79

80
81
82
83
84
85
86
73
74
75
76
77
78
79

80
81
82
83
84
85
86
87







-
+







			if err == nil {
				_, err = z.Content.Write(&buf)
			}
		case partMeta:
			contentType = ctPlainText
			_, err = z.Meta.Write(&buf)
		case partContent:
			if ct, ok := syntax2contentType(z.Meta.GetDefault(api.KeySyntax, "")); ok {
			if ct, ok := syntax2contentType(config.GetSyntax(z.Meta, a.rtConfig)); ok {
				contentType = ct
			}
			_, err = z.Content.Write(&buf)
		}
		if err != nil {
			a.log.Fatal().Err(err).Zid(z.Meta.Zid).Msg("Unable to store plain zettel/part in buffer")
			http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError)

Changes to web/adapter/api/get_zettel_list.go.

13
14
15
16
17
18
19

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







+








import (
	"bytes"
	"fmt"
	"net/http"

	"zettelstore.de/c/api"
	"zettelstore.de/z/config"
	"zettelstore.de/z/usecase"
	"zettelstore.de/z/web/adapter"
)

// MakeListMetaHandler creates a new HTTP handler for the use case "list some zettel".
func (a *API) MakeListMetaHandler(listMeta usecase.ListMeta) http.HandlerFunc {
	return func(w http.ResponseWriter, r *http.Request) {
68
69
70
71
72
73
74
75

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

76
77
78
79
80
81
82
83
84
85
86
87







-
+











		if err != nil {
			a.reportUsecaseError(w, err)
			return
		}

		var buf bytes.Buffer
		for _, m := range metaList {
			_, err = fmt.Fprintln(&buf, m.Zid.String(), m.GetTitle())
			_, err = fmt.Fprintln(&buf, m.Zid.String(), config.GetTitle(m, a.rtConfig))
			if err != nil {
				a.log.Fatal().Err(err).Msg("Unable to store plain list in buffer")
				http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError)
				return
			}
		}

		err = writeBuffer(w, &buf, ctPlainText)
		a.log.IfErr(err).Msg("Write Plain List")
	}
}

Changes to web/adapter/api/login.go.

1
2
3
4

5
6
7
8
9
10
11
1
2
3

4
5
6
7
8
9
10
11



-
+







//-----------------------------------------------------------------------------
// Copyright (c) 2020-2022 Detlef Stern
//
// This file is part of Zettelstore.
// This file is part of zettelstore.
//
// Zettelstore is licensed 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
29
30
31
32
33
34
35
36

37
38
39
40
41
42
43
29
30
31
32
33
34
35

36
37
38
39
40
41
42
43







-
+







			err := a.writeJSONToken(w, "freeaccess", 24*366*10*time.Hour)
			a.log.IfErr(err).Msg("Login/free")
			return
		}
		var token []byte
		if ident, cred := retrieveIdentCred(r); ident != "" {
			var err error
			token, err = ucAuth.Run(r.Context(), r, ident, cred, a.tokenLifetime, auth.KindJSON)
			token, err = ucAuth.Run(r.Context(), ident, cred, a.tokenLifetime, auth.KindJSON)
			if err != nil {
				a.reportUsecaseError(w, err)
				return
			}
		}
		if len(token) == 0 {
			w.Header().Set("WWW-Authenticate", `Bearer realm="Default"`)

Changes to web/adapter/api/request.go.

1
2

3
4

5
6
7
8
9
10
11
1

2
3

4
5
6
7
8
9
10
11

-
+

-
+







//-----------------------------------------------------------------------------
// Copyright (c) 2020-2022 Detlef Stern
// Copyright (c) 2020-2021 Detlef Stern
//
// This file is part of Zettelstore.
// This file is part of zettelstore.
//
// Zettelstore is licensed 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.
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
19
20
21
22
23
24
25





































26
27
28
29
30
31
32







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







	"zettelstore.de/c/api"
	"zettelstore.de/z/domain"
	"zettelstore.de/z/domain/id"
	"zettelstore.de/z/domain/meta"
	"zettelstore.de/z/input"
)

// getEncoding returns the data encoding selected by the caller.
func getEncoding(r *http.Request, q url.Values, defEncoding api.EncodingEnum) (api.EncodingEnum, string) {
	encoding := q.Get(api.QueryKeyEncoding)
	if len(encoding) > 0 {
		return api.Encoder(encoding), encoding
	}
	if enc, ok := getOneEncoding(r, api.HeaderAccept); ok {
		return api.Encoder(enc), enc
	}
	if enc, ok := getOneEncoding(r, api.HeaderContentType); ok {
		return api.Encoder(enc), enc
	}
	return defEncoding, defEncoding.String()
}

func getOneEncoding(r *http.Request, key string) (string, bool) {
	if values, ok := r.Header[key]; ok {
		for _, value := range values {
			if enc, ok2 := contentType2encoding(value); ok2 {
				return enc, true
			}
		}
	}
	return "", false
}

var mapCT2encoding = map[string]string{
	"application/json": "json",
	"text/html":        api.EncodingHTML,
}

func contentType2encoding(contentType string) (string, bool) {
	// TODO: only check before first ';'
	enc, ok := mapCT2encoding[contentType]
	return enc, ok
}

type partType int

const (
	_ partType = iota
	partMeta
	partContent
	partZettel

Changes to web/adapter/request.go.

45
46
47
48
49
50
51





































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







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







	if s != "" {
		if val, err := strconv.Atoi(s); err == nil {
			return val, true
		}
	}
	return 0, false
}

// GetEncoding returns the data encoding selected by the caller.
func GetEncoding(r *http.Request, q url.Values, defEncoding api.EncodingEnum) (api.EncodingEnum, string) {
	encoding := q.Get(api.QueryKeyEncoding)
	if len(encoding) > 0 {
		return api.Encoder(encoding), encoding
	}
	if enc, ok := getOneEncoding(r, api.HeaderAccept); ok {
		return api.Encoder(enc), enc
	}
	if enc, ok := getOneEncoding(r, api.HeaderContentType); ok {
		return api.Encoder(enc), enc
	}
	return defEncoding, defEncoding.String()
}

func getOneEncoding(r *http.Request, key string) (string, bool) {
	if values, ok := r.Header[key]; ok {
		for _, value := range values {
			if enc, ok2 := contentType2encoding(value); ok2 {
				return enc, true
			}
		}
	}
	return "", false
}

var mapCT2encoding = map[string]string{
	"application/json": "json",
	"text/html":        api.EncodingHTML,
}

func contentType2encoding(contentType string) (string, bool) {
	// TODO: only check before first ';'
	enc, ok := mapCT2encoding[contentType]
	return enc, ok
}

// GetSearch retrieves the specified search and sorting options from a query.
func GetSearch(q url.Values) (s *search.Search) {
	for key, values := range q {
		switch key {
		case api.QueryKeySort, api.QueryKeyOrder:
			s = extractOrderFromQuery(values, s)

Changes to web/adapter/response.go.

13
14
15
16
17
18
19

20

21

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







+

+

+








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

	"zettelstore.de/c/api"
	"zettelstore.de/z/ast"
	"zettelstore.de/z/box"
	"zettelstore.de/z/domain/id"
	"zettelstore.de/z/usecase"
	"zettelstore.de/z/web/server"
)

// PrepareHeader sets the HTTP header to defined values.
func PrepareHeader(w http.ResponseWriter, contentType string) http.Header {
	h := w.Header()
	if contentType != "" {
		h.Set(api.HeaderContentType, contentType)
64
65
66
67
68
69
70


































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







+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
		return http.StatusConflict, "Zettelstore operations conflicted"
	}
	if errors.Is(err, box.ErrCapacity) {
		return http.StatusInsufficientStorage, "Zettelstore reached one of its storage limits"
	}
	return http.StatusInternalServerError, err.Error()
}

// CreateTagReference builds a reference to list all tags.
func CreateTagReference(b server.Builder, key byte, enc, s string) *ast.Reference {
	u := b.NewURLBuilder(key).AppendQuery(api.QueryKeyEncoding, enc).AppendQuery(api.KeyAllTags, s)
	ref := ast.ParseReference(u.String())
	ref.State = ast.RefStateHosted
	return ref
}

// CreateHostedReference builds a reference with state "hosted".
func CreateHostedReference(b server.Builder, s string) *ast.Reference {
	urlPrefix := b.GetURLPrefix()
	ref := ast.ParseReference(urlPrefix + s)
	ref.State = ast.RefStateHosted
	return ref
}

// CreateFoundReference builds a reference for a found zettel.
func CreateFoundReference(b server.Builder, key byte, part, enc string, zid id.Zid, fragment string) *ast.Reference {
	ub := b.NewURLBuilder(key).SetZid(api.ZettelID(zid.String()))
	if part != "" {
		ub.AppendQuery(api.QueryKeyPart, part)
	}
	if enc != "" {
		ub.AppendQuery(api.QueryKeyEncoding, enc)
	}
	if fragment != "" {
		ub.SetFragment(fragment)
	}

	ref := ast.ParseReference(ub.String())
	ref.State = ast.RefStateFound
	return ref
}

Changes to web/adapter/webui/create_zettel.go.

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

15
16
17
18
19
20
21
22

23
24
25
26
27
28
29
30

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














-
+







-
+







-
+
-
-




+
+
+
+
+











-


-
+

-
+


-
-
+
+




+
-
+




-
+



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



-

+


-
-

+



-
+




-
+
-
-
-
-
-
+











-
+














-
-
-
-
-
-
+
+
+
-
//-----------------------------------------------------------------------------
// 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 (
	"context"
	"fmt"
	"net/http"

	"zettelstore.de/c/api"
	"zettelstore.de/z/box"
	"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/parser"
	"zettelstore.de/z/usecase"
	"zettelstore.de/z/web/adapter"
)

// MakeGetCreateZettelHandler creates a new HTTP handler to display the
// HTML edit view for the various zettel creation methods.
func (wui *WebUI) MakeGetCreateZettelHandler(
func (wui *WebUI) MakeGetCreateZettelHandler(getZettel usecase.GetZettel, createZettel *usecase.CreateZettel) http.HandlerFunc {
	getZettel usecase.GetZettel, createZettel *usecase.CreateZettel,
	ucListRoles usecase.ListRoles, ucListSyntax usecase.ListSyntax) http.HandlerFunc {
	return func(w http.ResponseWriter, r *http.Request) {
		ctx := r.Context()
		q := r.URL.Query()
		op := getCreateAction(q.Get(queryKeyAction))
		if enc, encText := adapter.GetEncoding(r, q, api.EncoderHTML); enc != api.EncoderHTML {
			wui.reportError(ctx, w, adapter.NewErrBadRequest(
				fmt.Sprintf("%v zettel not possible in encoding %q", mapActionOp[op], encText)))
			return
		}
		zid, err := id.Parse(r.URL.Path[1:])
		if err != nil {
			wui.reportError(ctx, w, box.ErrNotFound)
			return
		}
		origZettel, err := getZettel.Run(box.NoEnrichContext(ctx), zid)
		if err != nil {
			wui.reportError(ctx, w, box.ErrNotFound)
			return
		}

		roleData, syntaxData := retrieveDataLists(ctx, ucListRoles, ucListSyntax)
		switch op {
		case actionCopy:
			wui.renderZettelForm(ctx, w, createZettel.PrepareCopy(origZettel), "Copy Zettel", "Copy Zettel", roleData, syntaxData)
			wui.renderZettelForm(w, r, createZettel.PrepareCopy(origZettel), "Copy Zettel", "Copy Zettel")
		case actionFolge:
			wui.renderZettelForm(ctx, w, createZettel.PrepareFolge(origZettel), "Folge Zettel", "Folgezettel", roleData, syntaxData)
			wui.renderZettelForm(w, r, createZettel.PrepareFolge(origZettel), "Folge Zettel", "Folgezettel")
		case actionNew:
			m := origZettel.Meta
			title := parser.ParseMetadata(m.GetTitle())
			textTitle, err2 := encodeInlinesText(&title, wui.gentext)
			title := parser.ParseMetadata(config.GetTitle(m, wui.rtConfig))
			textTitle, err2 := encodeInlines(&title, api.EncoderText, nil)
			if err2 != nil {
				wui.reportError(ctx, w, err2)
				return
			}
			env := encoder.Environment{Lang: config.GetLang(m, wui.rtConfig)}
			htmlTitle, err2 := wui.getSimpleHTMLEncoder().InlinesString(&title, false)
			htmlTitle, err2 := encodeInlines(&title, api.EncoderHTML, &env)
			if err2 != nil {
				wui.reportError(ctx, w, err2)
				return
			}
			wui.renderZettelForm(ctx, w, createZettel.PrepareNew(origZettel), textTitle, htmlTitle, roleData, syntaxData)
			wui.renderZettelForm(w, r, createZettel.PrepareNew(origZettel), textTitle, htmlTitle)
		}
	}
}

func retrieveDataLists(ctx context.Context, ucListRoles usecase.ListRoles, ucListSyntax usecase.ListSyntax) ([]string, []string) {
	roleData := dataListFromArrangement(ucListRoles.Run(ctx))
	syntaxData := dataListFromArrangement(ucListSyntax.Run(ctx))
	return roleData, syntaxData
}

func dataListFromArrangement(ar meta.Arrangement, err error) []string {
	if err == nil {
		l := ar.Counted()
		l.SortByCount()
		return l.Categories()
	}

	return nil
var mapActionOp = map[createAction]string{
	actionCopy:  "Copy",
	actionFolge: "Folge",
	actionNew:   "New",
}

func (wui *WebUI) renderZettelForm(
	ctx context.Context,
	w http.ResponseWriter,
	r *http.Request,
	zettel domain.Zettel,
	title, heading string,
	roleData []string,
	syntaxData []string,
) {
	ctx := r.Context()
	user := wui.getUser(ctx)
	m := zettel.Meta
	var base baseData
	wui.makeBaseData(ctx, config.GetLang(m, wui.rtConfig), title, "", user, &base)
	wui.makeBaseData(ctx, config.GetLang(m, wui.rtConfig), title, user, &base)
	wui.renderTemplate(ctx, w, id.FormTemplateZid, &base, formZettelData{
		Heading:       heading,
		MetaTitle:     m.GetDefault(api.KeyTitle, ""),
		MetaTags:      m.GetDefault(api.KeyTags, ""),
		MetaRole:      m.GetDefault(api.KeyRole, ""),
		MetaRole:      config.GetRole(m, wui.rtConfig),
		HasRoleData:   len(roleData) > 0,
		RoleData:      roleData,
		HasSyntaxData: len(syntaxData) > 0,
		SyntaxData:    syntaxData,
		MetaSyntax:    m.GetDefault(api.KeySyntax, ""),
		MetaSyntax:    config.GetSyntax(m, wui.rtConfig),
		MetaPairsRest: m.PairsRest(),
		IsTextContent: !zettel.Content.IsBinary(),
		Content:       zettel.Content.AsString(),
	})
}

// MakePostCreateZettelHandler creates a new HTTP handler to store content of
// an existing zettel.
func (wui *WebUI) MakePostCreateZettelHandler(createZettel *usecase.CreateZettel) http.HandlerFunc {
	return func(w http.ResponseWriter, r *http.Request) {
		ctx := r.Context()
		reEdit, zettel, hasContent, err := parseZettelForm(r, id.Invalid)
		zettel, hasContent, err := parseZettelForm(r, id.Invalid)
		if err != nil {
			wui.reportError(ctx, w, adapter.NewErrBadRequest("Unable to read form data"))
			return
		}
		if !hasContent {
			wui.reportError(ctx, w, adapter.NewErrBadRequest("Content is missing"))
			return
		}

		newZid, err := createZettel.Run(ctx, zettel)
		if err != nil {
			wui.reportError(ctx, w, err)
			return
		}
		if reEdit {
			wui.redirectFound(w, r, wui.NewURLBuilder('e').SetZid(api.ZettelID(newZid.String())))
		} else {
			wui.redirectFound(w, r, wui.NewURLBuilder('h').SetZid(api.ZettelID(newZid.String())))
		}
	}
		wui.redirectFound(w, r, wui.NewURLBuilder('h').SetZid(api.ZettelID(newZid.String())))
	}
}
}

Changes to web/adapter/webui/delete_zettel.go.

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

14

15
16
17
18
19
20
21
22
23

24
25
26
27
28
29
30
31
32
33
34






35
36
37
38
39
40
41
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18

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













+

+


-






+











+
+
+
+
+
+







//-----------------------------------------------------------------------------
// 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 (
	"fmt"
	"net/http"
	"sort"

	"zettelstore.de/c/api"
	"zettelstore.de/c/maps"
	"zettelstore.de/z/box"
	"zettelstore.de/z/config"
	"zettelstore.de/z/domain/id"
	"zettelstore.de/z/domain/meta"
	"zettelstore.de/z/strfun"
	"zettelstore.de/z/usecase"
	"zettelstore.de/z/web/adapter"
)

// 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,
	evaluate *usecase.Evaluate,
) http.HandlerFunc {
	return func(w http.ResponseWriter, r *http.Request) {
		ctx := r.Context()
		if enc, encText := adapter.GetEncoding(r, r.URL.Query(), api.EncoderHTML); enc != api.EncoderHTML {
			wui.reportError(ctx, w, adapter.NewErrBadRequest(
				fmt.Sprintf("Delete zettel not possible in encoding %q", encText)))
			return
		}

		zid, err := id.Parse(r.URL.Path[1:])
		if err != nil {
			wui.reportError(ctx, w, box.ErrNotFound)
			return
		}

		ms, err := getAllMeta.Run(ctx, zid)
53
54
55
56
57
58
59
60

61
62
63
64
65
66
67
61
62
63
64
65
66
67

68
69
70
71
72
73
74
75







-
+







			getTextTitle := wui.makeGetTextTitle(ctx, getMeta, evaluate)
			incomingLinks = wui.encodeIncoming(m, getTextTitle)
		}
		uselessFiles := retrieveUselessFiles(m)

		user := wui.getUser(ctx)
		var base baseData
		wui.makeBaseData(ctx, config.GetLang(m, wui.rtConfig), "Delete Zettel "+m.Zid.String(), "", user, &base)
		wui.makeBaseData(ctx, config.GetLang(m, wui.rtConfig), "Delete Zettel "+m.Zid.String(), user, &base)
		wui.renderTemplate(ctx, w, id.DeleteTemplateZid, &base, struct {
			Zid             string
			MetaPairs       []meta.Pair
			HasShadows      bool
			ShadowedBox     string
			HasIncoming     bool
			Incoming        []simpleLink
119
120
121
122
123
124
125





126

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

139
140
141
142
143
144
145
146
147
148







+
+
+
+
+
-
+









			if val, ok := m.Get(inverseKey); ok {
				zidMap.Set(val)
			}
		case meta.TypeIDSet:
			addListValues(zidMap, m, inverseKey)
		}
	}
	values := make([]string, 0, len(zidMap))
	for val := range zidMap {
		values = append(values, val)
	}
	sort.Strings(values)
	return wui.encodeZidLinks(maps.Keys(zidMap), getTextTitle)
	return wui.encodeZidLinks(values, getTextTitle)
}

func addListValues(zidMap strfun.Set, m *meta.Meta, key string) {
	if values, ok := m.GetList(key); ok {
		for _, val := range values {
			zidMap.Set(val)
		}
	}
}

Changes to web/adapter/webui/edit_zettel.go.

1
2
3
4

5
6
7
8
9
10
11
12
13

14
15
16
17

18
19
20
21
22
23
24
25

26
27
28
29
30
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
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




-
+









+




+







-
+














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














-
+









-
-
-
-
-
-
-
+
+
+
-
//-----------------------------------------------------------------------------
// Copyright (c) 2020-2022 Detlef Stern
//
// This file is part of Zettelstore.
// This file is part of zettelstore.
//
// Zettelstore is licensed 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 (
	"fmt"
	"net/http"

	"zettelstore.de/c/api"
	"zettelstore.de/z/box"
	"zettelstore.de/z/config"
	"zettelstore.de/z/domain/id"
	"zettelstore.de/z/usecase"
	"zettelstore.de/z/web/adapter"
)

// MakeEditGetZettelHandler creates a new HTTP handler to display the
// HTML edit view of a zettel.
func (wui *WebUI) MakeEditGetZettelHandler(getZettel usecase.GetZettel, ucListRoles usecase.ListRoles, ucListSyntax usecase.ListSyntax) http.HandlerFunc {
func (wui *WebUI) MakeEditGetZettelHandler(getZettel usecase.GetZettel) 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
		}

		zettel, err := getZettel.Run(box.NoEnrichContext(ctx), zid)
		if err != nil {
			wui.reportError(ctx, w, err)
			return
		}

		if enc, encText := adapter.GetEncoding(r, r.URL.Query(), api.EncoderHTML); enc != api.EncoderHTML {
			wui.reportError(ctx, w, adapter.NewErrBadRequest(
				fmt.Sprintf("Edit zettel %q not possible in encoding %q", zid, encText)))
			return
		}
		roleData, syntaxData := retrieveDataLists(ctx, ucListRoles, ucListSyntax)
		wui.renderZettelForm(ctx, w, zettel, "Edit Zettel", "Edit Zettel", roleData, syntaxData)

		user := wui.getUser(ctx)
		m := zettel.Meta
		var base baseData
		wui.makeBaseData(ctx, config.GetLang(m, wui.rtConfig), "Edit Zettel", user, &base)
		wui.renderTemplate(ctx, w, id.FormTemplateZid, &base, formZettelData{
			Heading:       base.Title,
			MetaTitle:     m.GetDefault(api.KeyTitle, ""),
			MetaRole:      m.GetDefault(api.KeyRole, ""),
			MetaTags:      m.GetDefault(api.KeyTags, ""),
			MetaSyntax:    m.GetDefault(api.KeySyntax, ""),
			MetaPairsRest: m.PairsRest(),
			IsTextContent: !zettel.Content.IsBinary(),
			Content:       zettel.Content.AsString(),
		})
	}
}

// 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) {
		ctx := r.Context()
		zid, err := id.Parse(r.URL.Path[1:])
		if err != nil {
			wui.reportError(ctx, w, box.ErrNotFound)
			return
		}

		reEdit, zettel, hasContent, err := parseZettelForm(r, zid)
		zettel, hasContent, err := parseZettelForm(r, zid)
		if err != nil {
			wui.reportError(ctx, w, adapter.NewErrBadRequest("Unable to read zettel form"))
			return
		}

		if err = updateZettel.Run(r.Context(), zettel, hasContent); err != nil {
			wui.reportError(ctx, w, err)
			return
		}

		if reEdit {
			wui.redirectFound(w, r, wui.NewURLBuilder('e').SetZid(api.ZettelID(zid.String())))
		} else {
			wui.redirectFound(w, r, wui.NewURLBuilder('h').SetZid(api.ZettelID(zid.String())))
		}
	}
		wui.redirectFound(w, r, wui.NewURLBuilder('h').SetZid(api.ZettelID(zid.String())))
	}
}
}

Changes to web/adapter/webui/forms.go.

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

47
48
49

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







-
-
-
-












-
+


-
+

-







	"zettelstore.de/z/input"
)

type formZettelData struct {
	Heading       string
	MetaTitle     string
	MetaRole      string
	HasRoleData   bool
	RoleData      []string
	HasSyntaxData bool
	SyntaxData    []string
	MetaTags      string
	MetaSyntax    string
	MetaPairsRest []meta.Pair
	IsTextContent bool
	Content       string
}

var (
	bsCRLF = []byte{'\r', '\n'}
	bsLF   = []byte{'\n'}
)

func parseZettelForm(r *http.Request, zid id.Zid) (bool, domain.Zettel, bool, error) {
func parseZettelForm(r *http.Request, zid id.Zid) (domain.Zettel, bool, error) {
	err := r.ParseForm()
	if err != nil {
		return false, domain.Zettel{}, false, err
		return domain.Zettel{}, false, err
	}
	_, doSave := r.Form["save"]

	var m *meta.Meta
	if postMeta, ok := trimmedFormValue(r, "meta"); ok {
		m = meta.NewFromInput(zid, input.NewInput(removeEmptyLines([]byte(postMeta))))
		m.Sanitize()
	} else {
		m = meta.New(zid)
68
69
70
71
72
73
74
75

76
77
78
79
80

81
82
83
84
85
86
87
63
64
65
66
67
68
69

70
71
72
73
74

75
76
77
78
79
80
81
82







-
+




-
+







	if postRole, ok := trimmedFormValue(r, "role"); ok {
		m.Set(api.KeyRole, meta.RemoveNonGraphic(postRole))
	}
	if postSyntax, ok := trimmedFormValue(r, "syntax"); ok {
		m.Set(api.KeySyntax, meta.RemoveNonGraphic(postSyntax))
	}
	if values, ok := r.PostForm["content"]; ok && len(values) > 0 {
		return doSave, domain.Zettel{
		return domain.Zettel{
			Meta:    m,
			Content: domain.NewContent(bytes.ReplaceAll([]byte(values[0]), bsCRLF, bsLF)),
		}, true, nil
	}
	return doSave, domain.Zettel{
	return domain.Zettel{
		Meta:    m,
		Content: domain.NewContent(nil),
	}, false, nil
}

func trimmedFormValue(r *http.Request, key string) (string, bool) {
	if values, ok := r.PostForm[key]; ok && len(values) > 0 {

Changes to web/adapter/webui/get_info.go.

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







+









+


+







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

package webui

import (
	"bytes"
	"context"
	"fmt"
	"net/http"
	"sort"
	"strings"

	"zettelstore.de/c/api"
	"zettelstore.de/z/ast"
	"zettelstore.de/z/box"
	"zettelstore.de/z/collect"
	"zettelstore.de/z/config"
	"zettelstore.de/z/domain"
	"zettelstore.de/z/domain/id"
	"zettelstore.de/z/encoder"
	"zettelstore.de/z/evaluator"
	"zettelstore.de/z/usecase"
	"zettelstore.de/z/web/adapter"
)

type metaDataInfo struct {
	Key   string
	Value string
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
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







+
+
+
+
+













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









-
+

-
+




















-
+








-
+







	getMeta usecase.GetMeta,
	getAllMeta usecase.GetAllMeta,
	unlinkedRefs usecase.UnlinkedReferences,
) http.HandlerFunc {
	return func(w http.ResponseWriter, r *http.Request) {
		ctx := r.Context()
		q := r.URL.Query()
		if enc, encText := adapter.GetEncoding(r, q, api.EncoderHTML); enc != api.EncoderHTML {
			wui.reportError(ctx, w, adapter.NewErrBadRequest(
				fmt.Sprintf("Zettel info not available in encoding %q", encText)))
			return
		}

		zid, err := id.Parse(r.URL.Path[1:])
		if err != nil {
			wui.reportError(ctx, w, box.ErrNotFound)
			return
		}

		zn, err := parseZettel.Run(ctx, zid, q.Get(api.KeySyntax))
		if err != nil {
			wui.reportError(ctx, w, err)
			return
		}

		envEval := evaluator.Environment{
			GetTagRef: func(s string) *ast.Reference {
				return adapter.CreateTagReference(wui, 'h', api.EncodingHTML, s)
			},
			GetHostedRef: func(s string) *ast.Reference {
				return adapter.CreateHostedReference(wui, s)
			},
			GetFoundRef: func(zid id.Zid, fragment string) *ast.Reference {
				return adapter.CreateFoundReference(wui, 'h', "", "", zid, fragment)
			},
			GetImageMaterial: func(zettel domain.Zettel, _ string) ast.InlineEmbedNode {
				return wui.createImageMaterial(zettel.Meta.Zid)
			},
		}
		lang := config.GetLang(zn.InhMeta, wui.rtConfig)
		enc := wui.getSimpleHTMLEncoder()
		envHTML := encoder.Environment{Lang: lang}
		pairs := zn.Meta.ComputedPairs()
		metaData := make([]metaDataInfo, len(pairs))
		getTextTitle := wui.makeGetTextTitle(ctx, getMeta, evaluate)
		for i, p := range pairs {
			var buf bytes.Buffer
			wui.writeHTMLMetaValue(
				&buf, p.Key, p.Value,
				getTextTitle,
				func(val string) ast.InlineSlice {
					return evaluate.RunMetadata(ctx, val)
					return evaluate.RunMetadata(ctx, val, &envEval)
				},
				enc)
				&envHTML)
			metaData[i] = metaDataInfo{p.Key, buf.String()}
		}
		summary := collect.References(zn)
		locLinks, extLinks := splitLocExtLinks(append(summary.Links, summary.Embeds...))

		textTitle := wui.encodeTitleAsText(ctx, zn.InhMeta, evaluate)
		phrase := q.Get(api.QueryKeyPhrase)
		if phrase == "" {
			phrase = textTitle
		}
		phrase = strings.TrimSpace(phrase)
		unlinkedMeta, err := unlinkedRefs.Run(
			ctx, phrase, adapter.AddUnlinkedRefsToSearch(nil, zn.InhMeta))
		if err != nil {
			wui.reportError(ctx, w, err)
			return
		}
		unLinks := wui.buildHTMLMetaList(ctx, unlinkedMeta, evaluate)

		shadowLinks := getShadowLinks(ctx, zid, getAllMeta)
		endnotes, err := enc.BlocksString(&ast.BlockSlice{})
		endnotes, err := encodeBlocks(&ast.BlockSlice{}, api.EncoderHTML, &envHTML)
		if err != nil {
			endnotes = ""
		}

		user := wui.getUser(ctx)
		canCreate := wui.canCreate(ctx, user)
		apiZid := api.ZettelID(zid.String())
		var base baseData
		wui.makeBaseData(ctx, config.GetLang(zn.InhMeta, wui.rtConfig), textTitle, "", user, &base)
		wui.makeBaseData(ctx, lang, textTitle, user, &base)
		wui.renderTemplate(ctx, w, id.InfoTemplateZid, &base, struct {
			Zid            string
			WebURL         string
			ContextURL     string
			CanWrite       bool
			EditURL        string
			CanFolge       bool

Changes to web/adapter/webui/get_zettel.go.

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







+






+


-
+
+
+

+













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






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




-
-
+
+
+




-
-
-










-
+



-







// under this license.
//-----------------------------------------------------------------------------

package webui

import (
	"bytes"
	"errors"
	"net/http"

	"zettelstore.de/c/api"
	"zettelstore.de/z/ast"
	"zettelstore.de/z/box"
	"zettelstore.de/z/config"
	"zettelstore.de/z/domain"
	"zettelstore.de/z/domain/id"
	"zettelstore.de/z/domain/meta"
	"zettelstore.de/z/encoder/textenc"
	"zettelstore.de/z/encoder"
	"zettelstore.de/z/evaluator"
	"zettelstore.de/z/strfun"
	"zettelstore.de/z/usecase"
	"zettelstore.de/z/web/adapter"
)

// 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) {
		ctx := r.Context()
		zid, err := id.Parse(r.URL.Path[1:])
		if err != nil {
			wui.reportError(ctx, w, box.ErrNotFound)
			return
		}

		q := r.URL.Query()
		env := evaluator.Environment{
			GetTagRef: func(s string) *ast.Reference {
				return adapter.CreateTagReference(wui, 'h', api.EncodingHTML, s)
			},
			GetHostedRef: func(s string) *ast.Reference {
				return adapter.CreateHostedReference(wui, s)
			},
			GetFoundRef: func(zid id.Zid, fragment string) *ast.Reference {
				return adapter.CreateFoundReference(wui, 'h', "", "", zid, fragment)
			},
			GetImageMaterial: func(zettel domain.Zettel, _ string) ast.InlineEmbedNode {
				return wui.createImageMaterial(zettel.Meta.Zid)
			},
		}
		zn, err := evaluate.Run(ctx, zid, q.Get(api.KeySyntax))
		zn, err := evaluate.Run(ctx, zid, q.Get(api.KeySyntax), &env)

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

		enc := wui.createZettelEncoder()
		metaHeader := enc.MetaString(zn.InhMeta, func(value string) ast.InlineSlice {
			return evaluate.RunMetadata(ctx, value)
		evalMeta := func(value string) ast.InlineSlice {
			return evaluate.RunMetadata(ctx, value, &env)
		})
		textTitle := wui.encodeTitleAsText(ctx, zn.InhMeta, evaluate)
		htmlTitle := encodeTitleAsHTML(ctx, zn.InhMeta, evaluate, enc, false)
		htmlContent, err := enc.BlocksString(&zn.Ast)
		}
		lang := config.GetLang(zn.InhMeta, wui.rtConfig)
		envHTML := encoder.Environment{
			Lang:           lang,
			Xhtml:          false,
			MarkerExternal: wui.rtConfig.GetMarkerExternal(),
			NewWindow:      true,
			IgnoreMeta:     strfun.NewSet(api.KeyTitle, api.KeyLang),
		}
		metaHeader, err := encodeMeta(zn.InhMeta, evalMeta, api.EncoderHTML, &envHTML)
		if err != nil {
			wui.reportError(ctx, w, err)
			return
		}
		var roleCSSURL string
		cssZid, err := wui.retrieveCSSZidFromRole(ctx, *zn.InhMeta)
		textTitle := wui.encodeTitleAsText(ctx, zn.InhMeta, evaluate)
		htmlTitle := wui.encodeTitleAsHTML(ctx, zn.InhMeta, evaluate, &env, &envHTML)
		htmlContent, err := encodeBlocks(&zn.Ast, api.EncoderHTML, &envHTML)
		if err != nil {
			wui.reportError(ctx, w, err)
			return
		}
		if cssZid != id.Invalid {
			roleCSSURL = wui.NewURLBuilder('z').SetZid(api.ZettelID(cssZid.String())).String()
		}
		user := wui.getUser(ctx)
		roleText := zn.Meta.GetDefault(api.KeyRole, "*")
		tags := wui.buildTagInfos(zn.Meta)
		canCreate := wui.canCreate(ctx, user)
		getTextTitle := wui.makeGetTextTitle(ctx, getMeta, evaluate)
		extURL, hasExtURL := zn.Meta.Get(api.KeyURL)
		folgeLinks := wui.encodeZettelLinks(zn.InhMeta, api.KeyFolge, getTextTitle)
		backLinks := wui.encodeZettelLinks(zn.InhMeta, api.KeyBack, getTextTitle)
		apiZid := api.ZettelID(zid.String())
		var base baseData
		wui.makeBaseData(ctx, config.GetLang(zn.InhMeta, wui.rtConfig), textTitle, roleCSSURL, user, &base)
		wui.makeBaseData(ctx, lang, textTitle, user, &base)
		base.MetaHeader = metaHeader
		wui.renderTemplate(ctx, w, id.ZettelTemplateZid, &base, struct {
			HTMLTitle     string
			RoleCSS       string
			CanWrite      bool
			EditURL       string
			Zid           string
			InfoURL       string
			RoleText      string
			RoleURL       string
			HasTags       bool
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
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







-















-
+









+
+
-
-
+
+
+
+


+
+
+
-
+
+

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







			Content       string
			HasFolgeLinks bool
			FolgeLinks    []simpleLink
			HasBackLinks  bool
			BackLinks     []simpleLink
		}{
			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("role", roleText).String(),
			HasTags:       len(tags) > 0,
			Tags:          tags,
			CanCopy:       canCreate && !zn.Content.IsBinary(),
			CopyURL:       wui.NewURLBuilder('c').SetZid(apiZid).AppendQuery(queryKeyAction, valueActionCopy).String(),
			CanFolge:      canCreate,
			FolgeURL:      wui.NewURLBuilder('c').SetZid(apiZid).AppendQuery(queryKeyAction, valueActionFolge).String(),
			PrecursorRefs: wui.encodeIdentifierSet(zn.InhMeta, api.KeyPrecursor, getTextTitle),
			ExtURL:        extURL,
			HasExtURL:     hasExtURL,
			ExtNewWindow:  htmlAttrNewWindow(hasExtURL),
			ExtNewWindow:  htmlAttrNewWindow(envHTML.NewWindow && hasExtURL),
			Content:       htmlContent,
			HasFolgeLinks: len(folgeLinks) > 0,
			FolgeLinks:    folgeLinks,
			HasBackLinks:  len(backLinks) > 0,
			BackLinks:     backLinks,
		})
	}
}

// errNoSuchEncoding signals an unsupported encoding encoding
var errNoSuchEncoding = errors.New("no such encoding")
func encodeInlinesText(is *ast.InlineSlice, enc *textenc.Encoder) (string, error) {
	if is == nil || len(*is) == 0 {

// encodeInlines returns a string representation of the inline slice.
func encodeInlines(is *ast.InlineSlice, enc api.EncodingEnum, env *encoder.Environment) (string, error) {
	if is == nil {
		return "", nil
	}
	encdr := encoder.Create(enc, env)
	if encdr == nil {
		return "", errNoSuchEncoding

	}

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

func encodeBlocks(bs *ast.BlockSlice, enc api.EncodingEnum, env *encoder.Environment) (string, error) {
	encdr := encoder.Create(enc, env)
	if encdr == nil {
		return "", errNoSuchEncoding
	}

	var buf bytes.Buffer
	_, err := encdr.WriteBlocks(&buf, bs)
	if err != nil {
		return "", err
	}
	return buf.String(), nil
}

func encodeMeta(
	m *meta.Meta, evalMeta encoder.EvalMetaFunc,
	enc api.EncodingEnum, env *encoder.Environment,
) (string, error) {
	encdr := encoder.Create(enc, env)
	if encdr == nil {
		return "", errNoSuchEncoding
	}

	var buf bytes.Buffer
	_, err := encdr.WriteMeta(&buf, m, evalMeta)
	if err != nil {
		return "", err
	}
	return buf.String(), nil
}

func (wui *WebUI) buildTagInfos(m *meta.Meta) []simpleLink {

Deleted 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













































































































































































































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

import (
	"bytes"
	"strings"

	"codeberg.org/t73fde/sxpf"
	"zettelstore.de/c/api"
	"zettelstore.de/c/html"
	"zettelstore.de/c/sexpr"
	"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/strfun"
)

// Builder allows to build new URLs for the web service.
type urlBuilder interface {
	GetURLPrefix() string
	NewURLBuilder(key byte) *api.URLBuilder
}

type htmlGenerator struct {
	builder   urlBuilder
	textEnc   *textenc.Encoder
	extMarker string
	newWindow bool
	env       *html.EncEnvironment
}

func createGenerator(builder urlBuilder, extMarker string, newWindow bool) *htmlGenerator {
	env := html.NewEncEnvironment(nil, 1)
	gen := &htmlGenerator{
		builder:   builder,
		textEnc:   textenc.Create(),
		extMarker: extMarker,
		newWindow: newWindow,
		env:       env,
	}

	env.Builtins.Set(sexpr.SymTag, sxpf.NewBuiltin("tag", true, 0, -1, gen.generateTag))
	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.SymLinkExternal, sxpf.NewBuiltin("linkE", true, 2, -1, gen.generateLinkExternal))

	f, err := env.Builtins.LookupForm(sexpr.SymEmbed)
	if err != nil {
		panic(err)
	}
	b := f.(*sxpf.Builtin)
	env.Builtins.Set(sexpr.SymEmbed, sxpf.NewBuiltin(b.Name(), true, 3, -1, gen.makeGenerateEmbed(b.GetValue())))
	return gen
}

var mapMetaKey = map[string]string{
	api.KeyCopyright: "copyright",
	api.KeyLicense:   "license",
}

func (g *htmlGenerator) MetaString(m *meta.Meta, evalMeta encoder.EvalMetaFunc) string {
	ignore := strfun.NewSet(api.KeyTitle, api.KeyLang)
	var buf bytes.Buffer

	if tags, ok := m.Get(api.KeyAllTags); ok {
		writeMetaTags(&buf, tags)
		ignore.Set(api.KeyAllTags)
		ignore.Set(api.KeyTags)
	} else if tags, ok = m.Get(api.KeyTags); ok {
		writeMetaTags(&buf, tags)
		ignore.Set(api.KeyTags)
	}

	for _, p := range m.ComputedPairs() {
		key := p.Key
		if ignore.Has(key) {
			continue
		}
		if altKey, found := mapMetaKey[key]; found {
			buf.WriteString(`<meta name="`)
			buf.WriteString(altKey)
		} else {
			buf.WriteString(`<meta name="zs-`)
			buf.WriteString(key)
		}
		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 writeMetaTags(buf *bytes.Buffer, tags string) {
	buf.WriteString(`<meta name="keywords" content="`)
	for i, val := range meta.ListFromValue(tags) {
		if i > 0 {
			buf.WriteString(", ")
		}
		html.AttributeEscape(buf, strings.TrimPrefix(val, "#"))
	}
	buf.WriteString("\">\n")
}

// BlocksString encodes a block slice.
func (g *htmlGenerator) BlocksString(bs *ast.BlockSlice) (string, error) {
	if bs == nil || len(*bs) == 0 {
		return "", nil
	}
	lst := sexprenc.GetSexpr(bs)
	var buf bytes.Buffer
	g.env.ReplaceWriter(&buf)
	sxpf.Eval(g.env, lst)
	if g.env.GetError() == nil {
		g.env.WriteEndnotes()
	}
	g.env.ReplaceWriter(nil)
	return buf.String(), g.env.GetError()
}

// InlinesString writes an inline slice to the writer
func (g *htmlGenerator) InlinesString(is *ast.InlineSlice, noLink bool) (string, error) {
	if is == nil || len(*is) == 0 {
		return "", nil
	}
	return html.EvaluateInline(g.env, sexprenc.GetSexpr(is), !noLink, noLink), nil
}

func (g *htmlGenerator) generateTag(senv sxpf.Environment, args *sxpf.Pair, _ int) (sxpf.Value, error) {
	if !sxpf.IsNil(args) {
		env := senv.(*html.EncEnvironment)
		s := env.GetString(args)
		if env.IgnoreLinks() {
			env.WriteEscaped(s)
		} else {
			u := g.builder.NewURLBuilder('h').AppendQuery(api.KeyAllTags, "#"+strings.ToLower(s))
			env.WriteStrings(`<a href="`, u.String(), `">#`)
			env.WriteEscaped(s)
			env.WriteString("</a>")
		}
	}
	return nil, nil
}

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

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.

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







+


+
+












-
+







	"net/url"
	"time"

	"zettelstore.de/c/api"
	"zettelstore.de/c/html"
	"zettelstore.de/z/ast"
	"zettelstore.de/z/box"
	"zettelstore.de/z/config"
	"zettelstore.de/z/domain/id"
	"zettelstore.de/z/domain/meta"
	"zettelstore.de/z/encoder"
	"zettelstore.de/z/evaluator"
	"zettelstore.de/z/usecase"
)

var space = []byte{' '}

type evalMetadataFunc = func(string) ast.InlineSlice

func (wui *WebUI) writeHTMLMetaValue(
	w io.Writer,
	key, value string,
	getTextTitle getTextTitleFunc,
	evalMetadata evalMetadataFunc,
	gen *htmlGenerator,
	envEnc *encoder.Environment,
) {
	switch kt := meta.Type(key); kt {
	case meta.TypeCredential:
		writeCredential(w, value)
	case meta.TypeEmpty:
		writeEmpty(w, value)
	case meta.TypeID:
60
61
62
63
64
65
66
67

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

70
71
72
73
74
75
76
77







-
+







	case meta.TypeURL:
		writeURL(w, value)
	case meta.TypeWord:
		wui.writeWord(w, key, value)
	case meta.TypeWordSet:
		wui.writeWordSet(w, key, meta.ListFromValue(value))
	case meta.TypeZettelmarkup:
		io.WriteString(w, encodeZmkMetadata(value, evalMetadata, gen, false))
		io.WriteString(w, encodeZmkMetadata(value, evalMetadata, api.EncoderHTML, envEnc))
	default:
		html.Escape(w, value)
		fmt.Fprintf(w, " <b>(Unhandled type: %v, key: %v)</b>", kt, key)
	}
}

func writeCredential(w io.Writer, val string) { html.Escape(w, val) }
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
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







-
+

-
-
+
+

-
+


-
-
+
+
+
+


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


+
-
+
+
+

+
+
+
-
+





			}
			return "", 0
		}
		return wui.encodeTitleAsText(ctx, m, evaluate), 1
	}
}

func encodeTitleAsHTML(
func (wui *WebUI) encodeTitleAsHTML(
	ctx context.Context, m *meta.Meta,
	evaluate *usecase.Evaluate,
	gen *htmlGenerator, noLink bool,
	evaluate *usecase.Evaluate, envEval *evaluator.Environment,
	envHTML *encoder.Environment,
) string {
	plainTitle := m.GetTitle()
	plainTitle := config.GetTitle(m, wui.rtConfig)
	return encodeZmkMetadata(
		plainTitle,
		func(val string) ast.InlineSlice { return evaluate.RunMetadata(ctx, val) },
		gen, noLink)
		func(val string) ast.InlineSlice {
			return evaluate.RunMetadata(ctx, plainTitle, envEval)
		},
		api.EncoderHTML, envHTML)
}

func (wui *WebUI) encodeTitleAsText(ctx context.Context, m *meta.Meta, evaluate *usecase.Evaluate) string {
	plainTitle := m.GetTitle()
	is := evaluate.RunMetadata(ctx, plainTitle)
func (wui *WebUI) encodeTitleAsText(
	ctx context.Context, m *meta.Meta, evaluate *usecase.Evaluate,
) string {
	plainTitle := config.GetTitle(m, wui.rtConfig)
	return encodeZmkMetadata(
		plainTitle,
		func(val string) ast.InlineSlice {
			return evaluate.RunMetadata(ctx, plainTitle, nil)
	result, err := encodeInlinesText(&is, wui.gentext)
	if err != nil {
		return err.Error()
	}
		},
	return result
		api.EncoderText, nil)
}

func encodeZmkMetadata(
func encodeZmkMetadata(value string, evalMetadata evalMetadataFunc, gen *htmlGenerator, noLink bool) string {
	value string, evalMetadata evalMetadataFunc,
	enc api.EncodingEnum, envHTML *encoder.Environment,
) string {
	is := evalMetadata(value)
	if len(is) == 0 {
		return ""
	}
	result, err := gen.InlinesString(&is, noLink)
	result, err := encodeInlines(&is, enc, envHTML)
	if err != nil {
		return err.Error()
	}
	return result
}

Changes to web/adapter/webui/lists.go.

18
19
20
21
22
23
24

25
26
27
28
29
30
31
32
33
34

35
36
37
38
39
40
41
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







+









-
+







	"sort"
	"strconv"

	"zettelstore.de/c/api"
	"zettelstore.de/z/box"
	"zettelstore.de/z/domain/id"
	"zettelstore.de/z/domain/meta"
	"zettelstore.de/z/encoder"
	"zettelstore.de/z/search"
	"zettelstore.de/z/usecase"
	"zettelstore.de/z/web/adapter"
)

// MakeListHTMLMetaHandler creates a HTTP handler for rendering the list of
// zettel as HTML.
func (wui *WebUI) MakeListHTMLMetaHandler(
	listMeta usecase.ListMeta,
	listRole usecase.ListRoles,
	listRole usecase.ListRole,
	listTags usecase.ListTags,
	evaluate *usecase.Evaluate,
) http.HandlerFunc {
	return func(w http.ResponseWriter, r *http.Request) {
		query := r.URL.Query()
		switch query.Get("_l") {
		case "r":
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
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







-
+














-
+

-
+




-
-



-
+




-
+







	if err != nil {
		wui.reportError(ctx, w, err)
		return
	}
	user := wui.getUser(ctx)
	metas := wui.buildHTMLMetaList(ctx, metaList, evaluate)
	var base baseData
	wui.makeBaseData(ctx, wui.rtConfig.GetDefaultLang(), wui.rtConfig.GetSiteName(), "", user, &base)
	wui.makeBaseData(ctx, wui.rtConfig.GetDefaultLang(), wui.rtConfig.GetSiteName(), user, &base)
	wui.renderTemplate(ctx, w, id.ListTemplateZid, &base, struct {
		Title string
		Metas []simpleLink
	}{
		Title: title,
		Metas: metas,
	})
}

type roleInfo struct {
	Text string
	URL  string
}

func (wui *WebUI) renderRolesList(w http.ResponseWriter, r *http.Request, listRole usecase.ListRoles) {
func (wui *WebUI) renderRolesList(w http.ResponseWriter, r *http.Request, listRole usecase.ListRole) {
	ctx := r.Context()
	roleArrangement, err := listRole.Run(ctx)
	roleList, err := listRole.Run(ctx)
	if err != nil {
		wui.reportError(ctx, w, err)
		return
	}
	roleList := roleArrangement.Counted()
	roleList.SortByName()

	roleInfos := make([]roleInfo, len(roleList))
	for i, role := range roleList {
		roleInfos[i] = roleInfo{role.Name, wui.NewURLBuilder('h').AppendQuery("role", role.Name).String()}
		roleInfos[i] = roleInfo{role, wui.NewURLBuilder('h').AppendQuery("role", role).String()}
	}

	user := wui.getUser(ctx)
	var base baseData
	wui.makeBaseData(ctx, wui.rtConfig.GetDefaultLang(), wui.rtConfig.GetSiteName(), "", user, &base)
	wui.makeBaseData(ctx, wui.rtConfig.GetDefaultLang(), wui.rtConfig.GetSiteName(), user, &base)
	wui.renderTemplate(ctx, w, id.RolesTemplateZid, &base, struct {
		Roles []roleInfo
	}{
		Roles: roleInfos,
	})
}

160
161
162
163
164
165
166
167

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

166
167
168
169
170
171
172
173







-
+







	for i := 0; i < len(tagsList); i++ {
		count := tagsList[i].iCount
		tagsList[i].Count = strconv.Itoa(count)
		tagsList[i].Size = strconv.Itoa(countMap[count])
	}

	var base baseData
	wui.makeBaseData(ctx, wui.rtConfig.GetDefaultLang(), wui.rtConfig.GetSiteName(), "", user, &base)
	wui.makeBaseData(ctx, wui.rtConfig.GetDefaultLang(), wui.rtConfig.GetSiteName(), user, &base)
	minCounts := make([]countInfo, 0, len(countList))
	for _, c := range countList {
		sCount := strconv.Itoa(c)
		minCounts = append(minCounts, countInfo{sCount, base.ListTagsURL + "&min=" + sCount})
	}

	wui.renderTemplate(ctx, w, id.TagsTemplateZid, &base, struct {
215
216
217
218
219
220
221
222

223
224
225
226
227
228
229
214
215
216
217
218
219
220

221
222
223
224
225
226
227
228







-
+







			}
			depthURL.AppendQuery(api.QueryKeyDepth, depth)
			depthLinks[i].Text = depth
			depthLinks[i].URL = depthURL.String()
		}
		var base baseData
		user := wui.getUser(ctx)
		wui.makeBaseData(ctx, wui.rtConfig.GetDefaultLang(), wui.rtConfig.GetSiteName(), "", user, &base)
		wui.makeBaseData(ctx, wui.rtConfig.GetDefaultLang(), wui.rtConfig.GetSiteName(), user, &base)
		wui.renderTemplate(ctx, w, id.ContextTemplateZid, &base, struct {
			Title   string
			InfoURL string
			Depths  []simpleLink
			Start   simpleLink
			Metas   []simpleLink
		}{
250
251
252
253
254
255
256

257



258
259
260







261
262

263
264
265
266
267
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







+
-
+
+
+

-

+
+
+
+
+
+
+

-
+





	}
	var buf bytes.Buffer
	s.Print(&buf)
	return buf.String()
}

// buildHTMLMetaList builds a zettel list based on a meta list for HTML rendering.
func (wui *WebUI) buildHTMLMetaList(
func (wui *WebUI) buildHTMLMetaList(ctx context.Context, metaList []*meta.Meta, evaluate *usecase.Evaluate) []simpleLink {
	ctx context.Context, metaList []*meta.Meta, evaluate *usecase.Evaluate,
) []simpleLink {
	defaultLang := wui.rtConfig.GetDefaultLang()
	metas := make([]simpleLink, 0, len(metaList))
	encHTML := wui.getSimpleHTMLEncoder()
	for _, m := range metaList {
		var lang string
		if val, ok := m.Get(api.KeyLang); ok {
			lang = val
		} else {
			lang = defaultLang
		}
		env := encoder.Environment{Lang: lang, Interactive: true}
		metas = append(metas, simpleLink{
			Text: encodeTitleAsHTML(ctx, m, evaluate, encHTML, true),
			Text: wui.encodeTitleAsHTML(ctx, m, evaluate, nil, &env),
			URL:  wui.NewURLBuilder('h').SetZid(api.ZettelID(m.Zid.String())).String(),
		})
	}
	return metas
}

Changes to web/adapter/webui/login.go.

1
2

3
4

5
6
7
8
9
10
11
1

2
3

4
5
6
7
8
9
10
11

-
+

-
+







//-----------------------------------------------------------------------------
// Copyright (c) 2020-2022 Detlef Stern
// Copyright (c) 2020-2021 Detlef Stern
//
// This file is part of Zettelstore.
// This file is part of zettelstore.
//
// Zettelstore is licensed 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
32
33
34
35
36
37
38
39

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

39
40
41
42
43
44
45
46







-
+







		}
		wui.renderLoginForm(wui.clearToken(r.Context(), w), w, false)
	}
}

func (wui *WebUI) renderLoginForm(ctx context.Context, w http.ResponseWriter, retry bool) {
	var base baseData
	wui.makeBaseData(ctx, wui.rtConfig.GetDefaultLang(), "Login", "", nil, &base)
	wui.makeBaseData(ctx, wui.rtConfig.GetDefaultLang(), "Login", nil, &base)
	wui.renderTemplate(ctx, w, id.LoginTemplateZid, &base, struct {
		Title string
		Retry bool
	}{
		Title: base.Title,
		Retry: retry,
	})
55
56
57
58
59
60
61
62

63
64
65
66
67
68
69
70
71
72
73
74
75
55
56
57
58
59
60
61

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







-
+













		}
		ctx := r.Context()
		ident, cred, ok := adapter.GetCredentialsViaForm(r)
		if !ok {
			wui.reportError(ctx, w, adapter.NewErrBadRequest("Unable to read login form"))
			return
		}
		token, err := ucAuth.Run(ctx, r, ident, cred, wui.tokenLifetime, auth.KindHTML)
		token, err := ucAuth.Run(ctx, ident, cred, wui.tokenLifetime, auth.KindHTML)
		if err != nil {
			wui.reportError(ctx, w, err)
			return
		}
		if token == nil {
			wui.renderLoginForm(wui.clearToken(ctx, w), w, true)
			return
		}

		wui.setToken(w, token)
		wui.redirectFound(w, r, wui.NewURLBuilder('/'))
	}
}

Changes to web/adapter/webui/rename_zettel.go.

1
2
3
4

5
6
7
8
9
10
11
1
2
3

4
5
6
7
8
9
10
11



-
+







//-----------------------------------------------------------------------------
// Copyright (c) 2020-2022 Detlef Stern
//
// This file is part of Zettelstore.
// This file is part of zettelstore.
//
// Zettelstore is licensed 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
36
37
38
39
40
41
42






43
44
45
46
47
48
49
50

51
52
53
54
55
56
57
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







+
+
+
+
+
+







-
+







		}

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

		if enc, encText := adapter.GetEncoding(r, r.URL.Query(), api.EncoderHTML); enc != api.EncoderHTML {
			wui.reportError(ctx, w, adapter.NewErrBadRequest(
				fmt.Sprintf("Rename zettel %q not possible in encoding %q", zid.String(), encText)))
			return
		}

		getTextTitle := wui.makeGetTextTitle(ctx, getMeta, evaluate)
		incomingLinks := wui.encodeIncoming(m, getTextTitle)
		uselessFiles := retrieveUselessFiles(m)

		user := wui.getUser(ctx)
		var base baseData
		wui.makeBaseData(ctx, config.GetLang(m, wui.rtConfig), "Rename Zettel "+zid.String(), "", user, &base)
		wui.makeBaseData(ctx, config.GetLang(m, wui.rtConfig), "Rename Zettel "+zid.String(), user, &base)
		wui.renderTemplate(ctx, w, id.RenameTemplateZid, &base, struct {
			Zid             string
			MetaPairs       []meta.Pair
			HasIncoming     bool
			Incoming        []simpleLink
			HasUselessFiles bool
			UselessFiles    []string

Changes to web/adapter/webui/response.go.

10
11
12
13
14
15
16


17
18
19
20
21
22
23







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







+
+







+
+
+
+
+
+
+

package webui

import (
	"net/http"

	"zettelstore.de/c/api"
	"zettelstore.de/z/ast"
	"zettelstore.de/z/domain/id"
)

func (wui *WebUI) redirectFound(w http.ResponseWriter, r *http.Request, ub *api.URLBuilder) {
	us := ub.String()
	wui.log.Debug().Str("uri", us).Msg("redirect")
	http.Redirect(w, r, us, http.StatusFound)
}

func (wui *WebUI) createImageMaterial(zid id.Zid) ast.InlineEmbedNode {
	ub := wui.NewURLBuilder('z').SetZid(api.ZettelID(zid.String()))
	ref := ast.ParseReference(ub.String())
	ref.State = ast.RefStateFound
	return &ast.EmbedRefNode{Ref: ref}
}

Changes to web/adapter/webui/webui.go.

10
11
12
13
14
15
16

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

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

56
57
58
59
60
61
62
63
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







+

-











-
+



















-
-
-

-
-
+
-








// Package webui provides web-UI handlers for web requests.
package webui

import (
	"bytes"
	"context"
	"io"
	"net/http"
	"strings"
	"sync"
	"time"

	"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/textenc"
	"zettelstore.de/z/encoder"
	"zettelstore.de/z/kernel"
	"zettelstore.de/z/logger"
	"zettelstore.de/z/parser"
	"zettelstore.de/z/template"
	"zettelstore.de/z/web/adapter"
	"zettelstore.de/z/web/server"
)

// WebUI holds all data for delivering the web ui.
type WebUI struct {
	log      *logger.Logger
	debug    bool
	ab       server.AuthBuilder
	authz    auth.AuthzManager
	rtConfig config.Config
	token    auth.TokenManager
	box      webuiBox
	policy   auth.Policy

	gentext *textenc.Encoder

	mxCache       sync.RWMutex
	templateCache map[id.Zid]*template.Template

	mxRoleCSSMap sync.RWMutex
	mxCache       sync.RWMutex
	roleCSSMap   map[string]id.Zid

	tokenLifetime time.Duration
	cssBaseURL    string
	cssUserURL    string
	homeURL       string
	listZettelURL string
	listRolesURL  string
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
83
84
85
86
87
88
89


90
91
92
93
94
95
96







-
-







		ab:       ab,
		rtConfig: rtConfig,
		authz:    authz,
		token:    token,
		box:      mgr,
		policy:   pol,

		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("_l", "r").String(),
		listTagsURL:   ab.NewURLBuilder('h').AppendQuery("_l", "t").String(),
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
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







-
-
-
-
-















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







	wui.mxCache.Lock()
	if ci.Reason == box.OnReload || ci.Zid == id.BaseTemplateZid {
		wui.templateCache = make(map[id.Zid]*template.Template, len(wui.templateCache))
	} else {
		delete(wui.templateCache, ci.Zid)
	}
	wui.mxCache.Unlock()
	wui.mxRoleCSSMap.Lock()
	if ci.Reason == box.OnReload || ci.Zid == id.RoleCSSMapZid {
		wui.roleCSSMap = nil
	}
	wui.mxRoleCSSMap.Unlock()
}

func (wui *WebUI) cacheSetTemplate(zid id.Zid, t *template.Template) {
	wui.mxCache.Lock()
	wui.templateCache[zid] = t
	wui.mxCache.Unlock()
}

func (wui *WebUI) cacheGetTemplate(zid id.Zid) (*template.Template, bool) {
	wui.mxCache.RLock()
	t, ok := wui.templateCache[zid]
	wui.mxCache.RUnlock()
	return t, ok
}

func (wui *WebUI) retrieveCSSZidFromRole(ctx context.Context, m meta.Meta) (id.Zid, error) {
	wui.mxRoleCSSMap.RLock()
	if wui.roleCSSMap == nil {
		wui.mxRoleCSSMap.RUnlock()
		wui.mxRoleCSSMap.Lock()
		mMap, err := wui.box.GetMeta(ctx, id.RoleCSSMapZid)
		if err == nil {
			wui.roleCSSMap = createRoleCSSMap(mMap)
		}
		wui.mxRoleCSSMap.Unlock()
		if err != nil {
			return id.Invalid, err
		}
		wui.mxRoleCSSMap.RLock()
	}

	defer wui.mxRoleCSSMap.RUnlock()
	if role, found := m.Get("css-role"); found {
		if result, found2 := wui.roleCSSMap[role]; found2 {
			return result, nil
		}
	}
	if role, found := m.Get(api.KeyRole); found {
		if result, found2 := wui.roleCSSMap[role]; found2 {
			return result, nil
		}
	}
	return id.Invalid, nil
}

func createRoleCSSMap(mMap *meta.Meta) map[string]id.Zid {
	result := make(map[string]id.Zid)
	for _, p := range mMap.PairsRest() {
		key := p.Key
		if len(key) < 9 || !strings.HasPrefix(key, "css-") || !strings.HasSuffix(key, "-zid") {
			continue
		}
		zid, err2 := id.Parse(p.Value)
		if err2 != nil {
			continue
		}
		result[key[4:len(key)-4]] = zid
	}
	return result
}

func (wui *WebUI) canCreate(ctx context.Context, user *meta.Meta) bool {
	m := meta.New(id.Invalid)
	return wui.policy.CanCreate(user, m) && wui.box.CanCreateZettel(ctx)
}

func (wui *WebUI) canWrite(
	ctx context.Context, user, meta *meta.Meta, content domain.Content) bool {
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
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







-




















-


-
+













-



















-
-
-
-
-
-
-
-







}

type baseData struct {
	Lang              string
	MetaHeader        string
	CSSBaseURL        string
	CSSUserURL        string
	CSSRoleURL        string
	Title             string
	HomeURL           string
	WithUser          bool
	WithAuth          bool
	UserIsValid       bool
	UserZettelURL     string
	UserIdent         string
	LoginURL          string
	LogoutURL         string
	ListZettelURL     string
	ListRolesURL      string
	ListTagsURL       string
	CanRefresh        bool
	RefreshURL        string
	HasNewZettelLinks bool
	NewZettelLinks    []simpleLink
	SearchURL         string
	QueryKeySearch    string
	Content           string
	FooterHTML        string
	DebugMode         bool
}

func (wui *WebUI) makeBaseData(ctx context.Context, lang, title, roleCSSURL string, user *meta.Meta, data *baseData) {
func (wui *WebUI) makeBaseData(ctx context.Context, lang, title string, user *meta.Meta, data *baseData) {
	var userZettelURL string
	var userIdent string

	userIsValid := user != nil
	if userIsValid {
		userZettelURL = wui.NewURLBuilder('h').SetZid(api.ZettelID(user.Zid.String())).String()
		userIdent = user.GetDefault(api.KeyUserID, "")
	}
	newZettelLinks := wui.fetchNewTemplates(ctx, user)

	data.Lang = lang
	data.CSSBaseURL = wui.cssBaseURL
	data.CSSUserURL = wui.cssUserURL
	data.CSSRoleURL = roleCSSURL
	data.Title = title
	data.HomeURL = wui.homeURL
	data.WithAuth = wui.withAuth
	data.WithUser = data.WithAuth
	data.UserIsValid = userIsValid
	data.UserZettelURL = userZettelURL
	data.UserIdent = userIdent
	data.LoginURL = wui.loginURL
	data.LogoutURL = wui.logoutURL
	data.ListZettelURL = wui.listZettelURL
	data.ListRolesURL = wui.listRolesURL
	data.ListTagsURL = wui.listTagsURL
	data.CanRefresh = wui.canRefresh(user)
	data.RefreshURL = wui.refreshURL
	data.HasNewZettelLinks = len(newZettelLinks) > 0
	data.NewZettelLinks = newZettelLinks
	data.SearchURL = wui.searchURL
	data.QueryKeySearch = api.QueryKeySearch
	data.FooterHTML = wui.rtConfig.GetFooterHTML()
	data.DebugMode = wui.debug
}

func (wui *WebUI) getSimpleHTMLEncoder() *htmlGenerator {
	return createGenerator(wui, "", false)
}
func (wui *WebUI) createZettelEncoder() *htmlGenerator {
	return createGenerator(wui, wui.rtConfig.GetMarkerExternal(), true)
}

// 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\" ref=\"noopener noreferrer\""
331
332
333
334
335
336
337
338

339

340

341
342

343
344
345
346
347
348
349
262
263
264
265
266
267
268

269
270
271

272
273

274
275
276
277
278
279
280
281







-
+

+
-
+

-
+







		m, err2 := wui.box.GetMeta(ctx, zid)
		if err2 != nil {
			continue
		}
		if !wui.policy.CanRead(user, m) {
			continue
		}
		title := m.GetTitle()
		title := config.GetTitle(m, wui.rtConfig)
		astTitle := parser.ParseMetadata(title)
		env := encoder.Environment{Lang: config.GetLang(m, wui.rtConfig)}
		menuTitle, err2 := wui.getSimpleHTMLEncoder().InlinesString(&astTitle, false)
		menuTitle, err2 := encodeInlines(&astTitle, api.EncoderHTML, &env)
		if err2 != nil {
			menuTitle, err2 = encodeInlinesText(&astTitle, wui.gentext)
			menuTitle, err2 = encodeInlines(&astTitle, api.EncoderText, nil)
			if err2 != nil {
				menuTitle = title
			}
		}
		result = append(result, simpleLink{
			Text: menuTitle,
			URL: wui.NewURLBuilder('c').SetZid(api.ZettelID(m.Zid.String())).
365
366
367
368
369
370
371
372

373
374
375
376
377
378
379
297
298
299
300
301
302
303

304
305
306
307
308
309
310
311







-
+







func (wui *WebUI) reportError(ctx context.Context, w http.ResponseWriter, err error) {
	code, text := adapter.CodeMessageFromError(err)
	if code == http.StatusInternalServerError {
		wui.log.Error().Msg(err.Error())
	}
	user := wui.getUser(ctx)
	var base baseData
	wui.makeBaseData(ctx, api.ValueLangEN, "Error", "", user, &base)
	wui.makeBaseData(ctx, api.ValueLangEN, "Error", user, &base)
	wui.renderTemplateStatus(ctx, w, code, id.ErrorTemplateZid, &base, struct {
		ErrorTitle string
		ErrorText  string
	}{
		ErrorTitle: http.StatusText(code),
		ErrorText:  text,
	})
404
405
406
407
408
409
410


411
412
413







414
415
416
417






























418
419
420
421
422
423
424
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







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




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







			wui.setToken(w, tok)
		}
	}
	var content bytes.Buffer
	err = t.Render(&content, data)
	if err == nil {
		wui.prepareAndWriteHeader(w, code)
		err = writeHTMLStart(w, base.Lang)
		if err == nil {
		base.Content = content.String()
		err = bt.Render(w, base)
	}
			base.Content = content.String()
			err = bt.Render(w, base)
			if err == nil {
				err = wui.writeHTMLEnd(w)
			}
		}
	}
	if err != nil {
		wui.log.IfErr(err).Msg("Unable to write HTML via template")
	}
}

func writeHTMLStart(w http.ResponseWriter, lang string) error {
	_, err := io.WriteString(w, "<!DOCTYPE html>\n<html")
	if err != nil {
		return err
	}
	if lang != "" {
		_, err = io.WriteString(w, " lang=\"")
		if err == nil {
			_, err = io.WriteString(w, lang)
		}
		if err == nil {
			_, err = io.WriteString(w, "\">\n<head>\n")
		}
	} else {
		_, err = io.WriteString(w, ">\n<head>\n")
	}
	return err
}

func (wui *WebUI) writeHTMLEnd(w http.ResponseWriter) error {
	if wui.debug {
		_, err := io.WriteString(w, "<div><b>WARNING: Debug mode is enabled. DO NOT USE IN PRODUCTION!</b></div>\n")
		if err != nil {
			return err
		}
	}
	_, err := io.WriteString(w, "</body>\n</html>")
	return err
}

func (wui *WebUI) getUser(ctx context.Context) *meta.Meta { return wui.ab.GetUser(ctx) }

// GetURLPrefix returns the configured URL prefix of the web server.
func (wui *WebUI) GetURLPrefix() string { return wui.ab.GetURLPrefix() }

// NewURLBuilder creates a new URL builder object with the given key.

Changes to web/server/impl/impl.go.

28
29
30
31
32
33
34
35

36
37
38
39
40
41

42
43
44
45
46
47
48
28
29
30
31
32
33
34

35
36
37
38
39
40

41
42
43
44
45
46
47
48







-
+





-
+







	server           httpServer
	router           httpRouter
	persistentCookie bool
	secureCookie     bool
}

// New creates a new web server.
func New(log *logger.Logger, listenAddr, urlPrefix string, persistentCookie, secureCookie bool, maxRequestSize int64, auth auth.TokenManager) server.Server {
func New(log *logger.Logger, listenAddr, urlPrefix string, persistentCookie, secureCookie bool, auth auth.TokenManager) server.Server {
	srv := myServer{
		log:              log,
		persistentCookie: persistentCookie,
		secureCookie:     secureCookie,
	}
	srv.router.initializeRouter(log, urlPrefix, maxRequestSize, auth)
	srv.router.initializeRouter(log, urlPrefix, auth)
	srv.server.initializeHTTPServer(listenAddr, &srv.router)
	return &srv
}

func (srv *myServer) Handle(pattern string, handler http.Handler) {
	srv.router.Handle(pattern, handler)
}

Changes to web/server/impl/router.go.

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







-



-
+







-







	minKey      byte
	maxKey      byte
	reURL       *regexp.Regexp
	listTable   routingTable
	zettelTable routingTable
	ur          server.UserRetriever
	mux         *http.ServeMux
	maxReqSize  int64
}

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

func (rt *httpRouter) addRoute(key byte, method server.Method, handler http.Handler, table *routingTable) {
	// Set minKey and maxKey; re-calculate regexp.
	if key < rt.minKey || rt.maxKey < key {
		if key < rt.minKey {
			rt.minKey = key
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
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







-
+








-
+












-







	rt.mux.Handle(pattern, handler)
}

func (rt *httpRouter) ServeHTTP(w http.ResponseWriter, r *http.Request) {
	// Something may panic. Ensure a kernel log.
	defer func() {
		if reco := recover(); reco != nil {
			rt.log.Error().Str("Method", r.Method).Str("URL", r.URL.String()).HTTPIP(r).Msg("Recover context")
			rt.log.Error().Str("Method", r.Method).Str("URL", r.URL.String()).Msg("Recover context")
			kernel.Main.LogRecover("Web", reco)
		}
	}()

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

	if prefixLen := len(rt.urlPrefix); prefixLen > 1 {
		if len(r.URL.Path) < prefixLen || r.URL.Path[:prefixLen] != rt.urlPrefix {
			http.Error(w, http.StatusText(http.StatusNotFound), http.StatusNotFound)
			if withDebug {
				rt.log.Debug().Int("sc", int64(w.(*traceResponseWriter).statusCode)).Msg("/ServeHTTP/prefix")
			}
			return
		}
		r.URL.Path = r.URL.Path[prefixLen-1:]
	}
	r.Body = http.MaxBytesReader(w, r.Body, rt.maxReqSize)
	match := rt.reURL.FindStringSubmatch(r.URL.Path)
	if len(match) != 3 {
		rt.mux.ServeHTTP(w, rt.addUserContext(r))
		if withDebug {
			rt.log.Debug().Int("sc", int64(w.(*traceResponseWriter).statusCode)).Msg("match other")
		}
		return
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
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







-





-




-




-





-







	if withDebug {
		rt.log.Debug().Int("sc", int64(w.(*traceResponseWriter).statusCode)).Msg("no match")
	}
}

func (rt *httpRouter) addUserContext(r *http.Request) *http.Request {
	if rt.ur == nil {
		// No auth needed
		return r
	}
	k := auth.KindJSON
	t := getHeaderToken(r)
	if len(t) == 0 {
		rt.log.Debug().Msg("no jwt token found") // IP already logged: ServeHTTP
		k = auth.KindHTML
		t = getSessionToken(r)
	}
	if len(t) == 0 {
		rt.log.Debug().Msg("no auth token found in request") // IP already logged: ServeHTTP
		return r
	}
	tokenData, err := rt.auth.CheckToken(t, k)
	if err != nil {
		rt.log.Sense().Err(err).HTTPIP(r).Msg("invalid auth token")
		return r
	}
	ctx := r.Context()
	user, err := rt.ur.GetUser(ctx, tokenData.Zid, tokenData.Ident)
	if err != nil {
		rt.log.Sense().Zid(tokenData.Zid).Str("ident", tokenData.Ident).Err(err).HTTPIP(r).Msg("auth user not found")
		return r
	}
	return r.WithContext(updateContext(ctx, user, &tokenData))
}

func getSessionToken(r *http.Request) []byte {
	cookie, err := r.Cookie(sessionName)

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


3
4

































































5
6
7
8
9
10
11


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







<title>Change Log</title>

<a name="0_6"></a>
<h2>Changes for Version 0.6.0 (pending)</h2>
<a name="0_5"></a>
<h2>Changes for Version 0.5 (pending)</h2>

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

<h2>Changes for Version 0.5.0 (2022-07-29)</h2>
  *  Removed zettel syntax &ldquo;draw&rdquo;. The new default syntax for
     inline zettel is now &ldquo;text&rdquo;. A drawing can now be made by
     using the &ldquo;evaluation block&rdquo; syntax (see below) by setting
     the generic attribute to &ldquo;draw&rdquo;.
     (breaking: zettelmarkup, api, webui)
  *  If authentication is enabled, a secret of at least 16 bytes must be set in
     the startup configuration.
     (breaking)
  *  &ldquo;Sexpr&rdquo; encoding replaces &ldquo;Native&rdquo; encoding. Sexpr
     encoding is much easier to parse, compared with native and ZJSON encoding.
     In most cases it is smaller than ZJSON.
     (breaking: api)
  *  Endpoint <tt>/r</tt> is changed to <tt>/m?_key=role</tt> and returns now
     a map of role names to the list of zettel having this role. Endpoint
     <tt>/t</tt> is changed to <tt>/m?_key=tags</tt>. It already returned
     mapping described before.
     (breaking: api)
  *  Remove support for a default value for metadata key title, role, and
     syntax. Title and role are now allowed to be empty, an empty syntax value
     defaults to &ldquo;plain&rdquo;.
     (breaking)
  *  Add support for an &ldquo;evaluation block&rdquo; syntax in Zettelmarkup
     to allow interpretation of content by external software.
     (minor: zettelmarkup)
  *  Add initial support for a TeX-like math-mode to Zettelmarkup (both block-
     and inline-structured elements). Currently, support only the syntax,
     but WebUI does not render these elements in a special way.
     (minor: zettelmarkup)
  *  For block-structured elements, attributes may now span more than one line.
     If a line ending occurs within a quoted attribute value, the line ending
     characters are part of the attributes value.
     (minor: zettelmarkup)
  *  Zettel 00000000029000 acts as a map of zettel roles to identifier of
     zettel that are included as additional CSS. Allows to display zettel
     differently depending on their role. Use case: slides that are processed
     by Zettel Presenter will use the same CSS if they are rendered by
     Zettelstore WebUI.
     (minor: webui)
  *  A zettel can be saved while creating / editing it. There is no need to
     manually re-edit it by using the 'e' endpoint.
     (minor: webui)
  *  Zettel role and zettel syntax are backed by a HTML5 data list element
     which lists supported and used values to help to enter a valid value.
     (mirnor: webui)
  *  Allow to use startup configuration, even if started in simple mode.
     (minor)
  *  Log authentication issues in level "sense"; add caller IP address to some
     web server log messages.
     (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 inprovements, to the software and to the
     documentation.

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

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.5.1</code> (2022-08-02).
Build: <code>v0.4</code> (2022-03-08).

  *  [/uv/zettelstore-0.5.1-linux-amd64.zip|Linux] (amd64)
  *  [/uv/zettelstore-0.5.1-linux-arm.zip|Linux] (arm6, e.g. Raspberry Pi)
  *  [/uv/zettelstore-0.5.1-windows-amd64.zip|Windows] (amd64)
  *  [/uv/zettelstore-0.5.1-darwin-amd64.zip|macOS] (amd64)
  *  [/uv/zettelstore-0.5.1-darwin-arm64.zip|macOS] (arm64, aka Apple silicon)
  *  [/uv/zettelstore-0.4-linux-amd64.zip|Linux] (amd64)
  *  [/uv/zettelstore-0.4-linux-arm.zip|Linux] (arm6, e.g. Raspberry Pi)
  *  [/uv/zettelstore-0.4-windows-amd64.zip|Windows] (amd64)
  *  [/uv/zettelstore-0.4-darwin-amd64.zip|macOS] (amd64)
  *  [/uv/zettelstore-0.4-darwin-arm64.zip|macOS] (arm64, aka Apple silicon)

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.5.0.zip|here].
[/uv/manual-0.4.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.

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







-
+

-
-
-
-
-
+
+
+
+
+











access Zettelstore via its API more easily,
[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 tuned]&hellip;
<hr>
<h3>Latest Release: 0.5.1 (2022-08-02)</h3>
<h3>Latest Release: 0.4 (2022-03-08)</h3>
  *  [./download.wiki|Download]
  *  [./changes.wiki#0_5_0|Change summary]
  *  [/timeline?p=v0.5.1&bt=v0.4&y=ci|Check-ins for version 0.5.1],
     [/vdiff?to=v0.5.1&from=v0.4|content diff]
  *  [/timeline?df=v0.5.0&y=ci|Check-ins derived from the 0.5.0 release],
     [/vdiff?from=v0.5.0&to=trunk|content diff]
  *  [./changes.wiki#0_4|Change summary]
  *  [/timeline?p=v0.4&bt=v0.3&y=ci|Check-ins for version 0.4],
     [/vdiff?to=v0.4&from=v0.3|content diff]
  *  [/timeline?df=v0.4&y=ci|Check-ins derived from the 0.4 release],
     [/vdiff?from=v0.4&to=trunk|content diff]
  *  [./plan.wiki|Limitations and planned improvements]
  *  [/timeline?t=release|Timeline of all past releases]

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

Changes to www/plan.wiki.

22
23
24
25
26
27
28





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







+
+
+
+
+
     always preserved.
  *  Some file systems differentiate filenames with different cases (e.g. some
     on Linux, sometimes on macOS), others do not (default on macOS, most on
     Windows). Zettelstore is not able to detect these differences. Do not put
     files in your directory boxes and in files boxes that differ only by upper
     / lower case letters.
  *  &hellip;

<h3>Planned improvements</h3>
  *  Support for mathematical content is missing, e.g. <code>$$F(x) &=
     \\int^a_b \\frac{1}{3}x^3$$</code>.
  *  &hellip;