Zettelstore Client

Check-in Differences
Login

Check-in Differences

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

Difference From v0.19.0 To v0.20.0

2025-03-07
16:02
Fix date in home page ... (Leaf check-in: 9c43b4d2b2 user: stern tags: trunk)
15:01
Version 0.20.0 ... (check-in: 16b4168715 user: stern tags: release, trunk, v0.20.0)
2025-03-04
21:11
sz: allow optional inlines in Transclude list ... (check-in: d73c989323 user: stern tags: trunk)
2024-12-13
14:14
Remove query action constants for rss/atom feed ... (check-in: 7902e0d349 user: stern tags: trunk)
12:10
Version 0.19.0 ... (check-in: fd15ae683e user: stern tags: release, trunk, v0.19.0)
2024-12-02
17:00
Remove some linter errors (revive) ... (check-in: 2b332aee9c user: stern tags: trunk)

Changes to api/api.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
10
11
12
13
14
15
16



17
















18
19
20
21
22
23
24







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







// SPDX-License-Identifier: EUPL-1.2
// SPDX-FileCopyrightText: 2021-present Detlef Stern
//-----------------------------------------------------------------------------

// Package api contains common definitions used for client and server.
package api

// ZettelID contains the identifier of a zettel. It is a string with 14 digits.
type ZettelID string

import "t73f.de/r/zsc/domain/id"
// InvalidZID is an invalid zettel identifier
const InvalidZID = ""

// IsValid returns true, if the idenfifier contains 14 digits.
func (zid ZettelID) IsValid() bool {
	if len(zid) != 14 {
		return false
	}
	for i := range 14 {
		ch := zid[i]
		if ch < '0' || '9' < ch {
			return false
		}
	}
	return true
}

// ZettelMeta is a map containg the normalized metadata of a zettel.
type ZettelMeta map[string]string

// ZettelRights is an integer that encode access rights for a zettel.
type ZettelRights uint8

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

37
38
39
40
41
42
43

44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62

63







-
+


















-
+
type MetaRights struct {
	Meta   ZettelMeta
	Rights ZettelRights
}

// ZidMetaRights contains the identifier, the metadata of a zettel, and its rights.
type ZidMetaRights struct {
	ID     ZettelID
	ID     id.Zid
	Meta   ZettelMeta
	Rights ZettelRights
}

// ZettelData contains all data for a zettel.
//
//   - Meta is a map containing the metadata of the zettel.
//   - Rights is an integer specifying the access rights.
//   - Encoding is a string specifying the encoding of the zettel content.
//   - Content is the zettel content itself.
type ZettelData struct {
	Meta     ZettelMeta
	Rights   ZettelRights
	Encoding string
	Content  string // raw, uninterpreted zettel content
}

// Aggregate maps metadata keys to list of zettel identifier.
type Aggregate map[string][]ZettelID
type Aggregate map[string][]id.Zid

Changes to api/const.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
11
12
13
14
15
16
17













































































































































































18
19
20
21
22
23
24







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







// SPDX-FileCopyrightText: 2021-present Detlef Stern
//-----------------------------------------------------------------------------

package api

import "fmt"

// Predefined zettel identifier.
//
// See [List of predefined zettel].
//
// [List of predefined zettel]: https://zettelstore.de/manual/h/00001005090000
const (
	// System zettel
	ZidVersion              = ZettelID("00000000000001")
	ZidHost                 = ZettelID("00000000000002")
	ZidOperatingSystem      = ZettelID("00000000000003")
	ZidLicense              = ZettelID("00000000000004")
	ZidAuthors              = ZettelID("00000000000005")
	ZidDependencies         = ZettelID("00000000000006")
	ZidLog                  = ZettelID("00000000000007")
	ZidMemory               = ZettelID("00000000000008")
	ZidSx                   = ZettelID("00000000000009")
	ZidHTTP                 = ZettelID("00000000000010")
	ZidAPI                  = ZettelID("00000000000011")
	ZidWebUI                = ZettelID("00000000000012")
	ZidConsole              = ZettelID("00000000000013")
	ZidBoxManager           = ZettelID("00000000000020")
	ZidZettel               = ZettelID("00000000000021")
	ZidIndex                = ZettelID("00000000000022")
	ZidQuery                = ZettelID("00000000000023")
	ZidMetadataKey          = ZettelID("00000000000090")
	ZidParser               = ZettelID("00000000000092")
	ZidStartupConfiguration = ZettelID("00000000000096")
	ZidConfiguration        = ZettelID("00000000000100")
	ZidDirectory            = ZettelID("00000000000101")

	// WebUI HTML templates are in the range 10000..19999
	ZidBaseTemplate   = ZettelID("00000000010100")
	ZidLoginTemplate  = ZettelID("00000000010200")
	ZidListTemplate   = ZettelID("00000000010300")
	ZidZettelTemplate = ZettelID("00000000010401")
	ZidInfoTemplate   = ZettelID("00000000010402")
	ZidFormTemplate   = ZettelID("00000000010403")
	ZidDeleteTemplate = ZettelID("00000000010405")
	ZidErrorTemplate  = ZettelID("00000000010700")

	// WebUI sxn code zettel are in the range 19000..19999
	ZidSxnStart = ZettelID("00000000019000")
	ZidSxnBase  = ZettelID("00000000019990")

	// CSS-related zettel are in the range 20000..29999
	ZidBaseCSS = ZettelID("00000000020001")
	ZidUserCSS = ZettelID("00000000025001")

	// WebUI JS zettel are in the range 30000..39999

	// WebUI image zettel are in the range 40000..49999
	ZidEmoji = ZettelID("00000000040001")

	// Other sxn code zettel are in the range 50000..59999
	ZidSxnPrelude = ZettelID("00000000059900")

	// Predefined Zettelmarkup zettel are in the range 60000..69999
	ZidRoleZettelZettel        = ZettelID("00000000060010")
	ZidRoleConfigurationZettel = ZettelID("00000000060020")
	ZidRoleRoleZettel          = ZettelID("00000000060030")
	ZidRoleTagZettel           = ZettelID("00000000060040")

	// Range 90000...99999 is reserved for zettel templates
	ZidTOCNewTemplate    = ZettelID("00000000090000")
	ZidTemplateNewZettel = ZettelID("00000000090001")
	ZidTemplateNewRole   = ZettelID("00000000090004")
	ZidTemplateNewTag    = ZettelID("00000000090003")
	ZidTemplateNewUser   = ZettelID("00000000090002")

	// Range 00000999999900...00000999999999 are predefined zettel to be searched by content.
	ZidAppDirectory = ZettelID("00000999999999")

	// Default Home Zettel
	ZidDefaultHome = ZettelID("00010000000000")
)

// LengthZid factors the constant length of a zettel identifier
const LengthZid = len(ZidDefaultHome)

// Values of the metadata key/value types.
//
// See [Supported Key Types].
//
// [Supported Key Types]: https://zettelstore.de/manual/h/00001006030000
const (
	MetaCredential   = "Credential"
	MetaEmpty        = "EString"
	MetaID           = "Identifier"
	MetaIDSet        = "IdentifierSet"
	MetaNumber       = "Number"
	MetaString       = "String"
	MetaTagSet       = "TagSet"
	MetaTimestamp    = "Timestamp"
	MetaURL          = "URL"
	MetaWord         = "Word"
	MetaZettelmarkup = "Zettelmarkup"
)

// Predefined / supported metadata keys.
//
// See [Supported Metadata Keys].
//
// [Supported Metadata Keys]: https://zettelstore.de/manual/h/00001006020000
const (
	KeyID           = "id"
	KeyTitle        = "title"
	KeyRole         = "role"
	KeyTags         = "tags"
	KeySyntax       = "syntax"
	KeyAuthor       = "author"
	KeyBack         = "back"
	KeyBackward     = "backward"
	KeyBoxNumber    = "box-number"
	KeyCopyright    = "copyright"
	KeyCreated      = "created"
	KeyCredential   = "credential"
	KeyDead         = "dead"
	KeyExpire       = "expire"
	KeyFolge        = "folge"
	KeyFolgeRole    = "folge-role"
	KeyForward      = "forward"
	KeyLang         = "lang"
	KeyLicense      = "license"
	KeyModified     = "modified"
	KeyPrecursor    = "precursor"
	KeyPredecessor  = "predecessor"
	KeyPrequel      = "prequel"
	KeyPublished    = "published"
	KeyQuery        = "query"
	KeyReadOnly     = "read-only"
	KeySequel       = "sequel"
	KeySubordinates = "subordinates"
	KeySuccessors   = "successors"
	KeySummary      = "summary"
	KeySuperior     = "superior"
	KeyURL          = "url"
	KeyUselessFiles = "useless-files"
	KeyUserID       = "user-id"
	KeyUserRole     = "user-role"
	KeyVisibility   = "visibility"
)

// Predefined metadata values.
const (
	ValueFalse             = "false"
	ValueTrue              = "true"
	ValueLangEN            = "en"            // Default for "lang"
	ValueRoleConfiguration = "configuration" // A role for internal zettel
	ValueRoleTag           = "tag"           // A role for tag zettel
	ValueRoleRole          = "role"          // A role for role zettel
	ValueRoleZettel        = "zettel"        // A role for zettel
	ValueSyntaxCSS         = "css"           // Syntax: CSS
	ValueSyntaxDraw        = "draw"          // Syntax: Drawing
	ValueSyntaxGif         = "gif"           // Syntax GIF image
	ValueSyntaxHTML        = "html"          // Syntax: HTML
	ValueSyntaxMarkdown    = "markdown"      // Syntax: Markdown / CommonMark
	ValueSyntaxMD          = "md"            // Syntax: Markdown / CommonMark
	ValueSyntaxNone        = "none"          // Syntax: no syntax / content, just metadata
	ValueSyntaxSVG         = "svg"           // Syntax: SVG
	ValueSyntaxSxn         = "sxn"           // Syntax: S-Expression
	ValueSyntaxText        = "text"          // Syntax: plain text
	ValueSyntaxZmk         = "zmk"           // Syntax: Zettelmarkup
	ValueUserRoleCreator   = "creator"
	ValueUserRoleOwner     = "owner"
	ValueUserRoleReader    = "reader"
	ValueUserRoleWriter    = "writer"
	ValueVisibilityCreator = "creator"
	ValueVisibilityExpert  = "expert"
	ValueVisibilityLogin   = "login"
	ValueVisibilityOwner   = "owner"
	ValueVisibilityPublic  = "public"
)

// Additional HTTP constants.
const (
	HeaderAccept      = "Accept"
	HeaderContentType = "Content-Type"
	HeaderDestination = "Destination"
	HeaderLocation    = "Location"
)
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
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







+












-






-
-







	ContextDirective  = "CONTEXT"  // Context directive
	CostDirective     = "COST"     // Maximum cost of a context operation
	ForwardDirective  = "FORWARD"  // Forward-only context
	FullDirective     = "FULL"     // Include tags in context
	IdentDirective    = "IDENT"    // Use only specified zettel
	ItemsDirective    = "ITEMS"    // Select list elements in a zettel
	MaxDirective      = "MAX"      // Maximum number of context results
	MinDirective      = "MIN"      // Minimum number of context results
	LimitDirective    = "LIMIT"    // Maximum number of zettel
	OffsetDirective   = "OFFSET"   // Offset to start returned zettel list
	OrDirective       = "OR"       // Combine several search expression with an "or"
	OrderDirective    = "ORDER"    // Specify metadata keys for the order of returned list
	PhraseDirective   = "PHRASE"   // Only unlinked zettel with given phrase
	PickDirective     = "PICK"     // Pick some random zettel
	RandomDirective   = "RANDOM"   // Order zettel list randomly
	ReverseDirective  = "REVERSE"  // Reverse the order of a zettel list
	UnlinkedDirective = "UNLINKED" // Search for zettel that contain a phase(s) but do not link

	ActionSeparator = "|" // Separates action list of previous elements of query expression

	AtomAction     = "ATOM"     // Return an Atom web feed
	KeysAction     = "KEYS"     // Provide metadata key used
	MinAction      = "MIN"      // Return only those values with a minimum amount of zettel
	MaxAction      = "MAX"      // Return only those values with a maximum amount of zettel
	NumberedAction = "NUMBERED" // Return a numbered list
	RedirectAction = "REDIRECT" // Return the first zettel in list
	ReIndexAction  = "REINDEX"  // Ensure that zettel is/are indexed.
	RSSAction      = "RSS"      // Return a RSS web feed
	TitleAction    = "TITLE"    // Set a title for Atom or RSS web feed

	ExistOperator    = "?"  // Does zettel have metadata with given key?
	ExistNotOperator = "!?" // True id zettel does not have metadata with given key.

	SearchOperatorNot        = "!"
	SearchOperatorEqual      = "="  // True if values are equal
	SearchOperatorNotEqual   = "!=" // False if values are equal

Changes to api/urlbuilder.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
24
25
26







+
-
+
+
+







//
// SPDX-License-Identifier: EUPL-1.2
// SPDX-FileCopyrightText: 2020-present Detlef Stern
//-----------------------------------------------------------------------------

package api

import (
import "t73f.de/r/webs/urlbuilder"
	"t73f.de/r/webs/urlbuilder"
	"t73f.de/r/zsc/domain/id"
)

// URLBuilder should be used to create zettelstore URLs.
type URLBuilder struct {
	base   urlbuilder.URLBuilder
	prefix string
}

38
39
40
41
42
43
44
45
46


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


48
49
50
51
52
53
54
55
56







-
-
+
+







	cpy := new(URLBuilder)
	ub.base.Copy(&cpy.base)
	cpy.prefix = ub.prefix
	return cpy
}

// SetZid sets the zettel identifier.
func (ub *URLBuilder) SetZid(zid ZettelID) *URLBuilder {
	ub.base.AddPath(string(zid))
func (ub *URLBuilder) SetZid(zid id.Zid) *URLBuilder {
	ub.base.AddPath(zid.String())
	return ub
}

// AppendPath adds a new path element.
func (ub *URLBuilder) AppendPath(p string) *URLBuilder {
	ub.base.AddPath(p)
	return ub

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







+


-
-







// SPDX-FileCopyrightText: 2020-present Detlef Stern
//-----------------------------------------------------------------------------

// Package attrs stores attributes of zettel parts.
package attrs

import (
	"maps"
	"slices"
	"strings"

	"t73f.de/r/zsc/maps"
)

// Attributes store additional information about some node types.
type Attributes map[string]string

// IsEmpty returns true if there are no attributes.
func (a Attributes) IsEmpty() bool { return len(a) == 0 }
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
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







-
+











-
+
-
-
-
-
-
-
-
-
-







	if a != nil {
		a.Remove(DefaultAttribute)
	}
	return a
}

// Keys returns the sorted list of keys.
func (a Attributes) Keys() []string { return maps.Keys(a) }
func (a Attributes) Keys() []string { return slices.Sorted(maps.Keys(a)) }

// Get returns the attribute value of the given key and a succes value.
func (a Attributes) Get(key string) (string, bool) {
	if a != nil {
		value, ok := a[key]
		return value, ok
	}
	return "", false
}

// Clone returns a duplicate of the attribute.
func (a Attributes) Clone() Attributes {
func (a Attributes) Clone() Attributes { return maps.Clone(a) }
	if a == nil {
		return nil
	}
	attrs := make(map[string]string, len(a))
	for k, v := range a {
		attrs[k] = v
	}
	return attrs
}

// Set changes the attribute that a given key has now a given value.
func (a Attributes) Set(key, value string) Attributes {
	if a == nil {
		return map[string]string{key: value}
	}
	a[key] = value

Changes to client/client.go.

25
26
27
28
29
30
31

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







+







	"strconv"
	"strings"
	"time"

	"t73f.de/r/sx"
	"t73f.de/r/sx/sxreader"
	"t73f.de/r/zsc/api"
	"t73f.de/r/zsc/domain/id"
	"t73f.de/r/zsc/sexp"
)

// Client contains all data to execute requests.
type Client struct {
	base      string
	username  string
185
186
187
188
189
190
191
192

193
194
195
196
197
198
199
186
187
188
189
190
191
192

193
194
195
196
197
198
199
200







-
+







}

func (c *Client) executeAuthRequest(req *http.Request) error {
	resp, err := c.executeRequest(req)
	if err != nil {
		return err
	}
	defer resp.Body.Close()
	defer func() { _ = resp.Body.Close() }()
	if resp.StatusCode != http.StatusOK {
		return statusToError(resp)
	}
	rd := sxreader.MakeReader(resp.Body)
	obj, err := rd.Read()
	if err != nil {
		return err
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
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







-
+



-
+

-
+

-
+



-
+

-
-
+
-
-





-
+


-
+




-
+

-
+



-
+


-
+




-
+


-
+





-
-
-
+
+
+









-
+





-
+







-
+









-
+







-
+





-
+

















-
+





}

// CreateZettel creates a new zettel and returns its URL.
//
// data contains the zettel metadata and content, as it is stored in a file in a zettel box,
// or as returned by [Client.GetZettel].
// Metadata is separated from zettel content by an empty line.
func (c *Client) CreateZettel(ctx context.Context, data []byte) (api.ZettelID, error) {
func (c *Client) CreateZettel(ctx context.Context, data []byte) (id.Zid, error) {
	ub := c.NewURLBuilder('z')
	resp, err := c.buildAndExecuteRequest(ctx, http.MethodPost, ub, bytes.NewBuffer(data))
	if err != nil {
		return api.InvalidZID, err
		return id.Invalid, err
	}
	defer resp.Body.Close()
	defer func() { _ = resp.Body.Close() }()
	if resp.StatusCode != http.StatusCreated {
		return api.InvalidZID, statusToError(resp)
		return id.Invalid, statusToError(resp)
	}
	b, err := io.ReadAll(resp.Body)
	if err != nil {
		return api.InvalidZID, err
		return id.Invalid, err
	}
	if zid := api.ZettelID(b); zid.IsValid() {
		return zid, nil
	return id.Parse(string(b))
	}
	return api.InvalidZID, err
}

// CreateZettelData creates a new zettel and returns its URL.
//
// data contains the zettel date, encoded as explicit struct.
func (c *Client) CreateZettelData(ctx context.Context, data api.ZettelData) (api.ZettelID, error) {
func (c *Client) CreateZettelData(ctx context.Context, data api.ZettelData) (id.Zid, error) {
	var buf bytes.Buffer
	if _, err := sx.Print(&buf, sexp.EncodeZettel(data)); err != nil {
		return api.InvalidZID, err
		return id.Invalid, err
	}
	ub := c.NewURLBuilder('z').AppendKVQuery(api.QueryKeyEncoding, api.EncodingData)
	resp, err := c.buildAndExecuteRequest(ctx, http.MethodPost, ub, &buf)
	if err != nil {
		return api.InvalidZID, err
		return id.Invalid, err
	}
	defer resp.Body.Close()
	defer func() { _ = resp.Body.Close() }()
	rdr := sxreader.MakeReader(resp.Body)
	obj, err := rdr.Read()
	if resp.StatusCode != http.StatusCreated {
		return api.InvalidZID, statusToError(resp)
		return id.Invalid, statusToError(resp)
	}
	if err != nil {
		return api.InvalidZID, err
		return id.Invalid, err
	}
	return makeZettelID(obj)
}

func makeZettelID(obj sx.Object) (api.ZettelID, error) {
func makeZettelID(obj sx.Object) (id.Zid, error) {
	val, isInt64 := obj.(sx.Int64)
	if !isInt64 || val <= 0 {
		return api.InvalidZID, fmt.Errorf("invalid zettel ID: %v", val)
		return id.Invalid, fmt.Errorf("invalid zettel ID: %v", val)
	}
	sVal := strconv.FormatInt(int64(val), 10)
	if len(sVal) < 14 {
		sVal = "00000000000000"[0:14-len(sVal)] + sVal
	}
	zid := api.ZettelID(sVal)
	if !zid.IsValid() {
		return api.InvalidZID, fmt.Errorf("invalid zettel ID: %v", val)
	zid, err := id.Parse(sVal)
	if err != nil {
		return id.Invalid, fmt.Errorf("invalid zettel ID %v: %w", val, err)
	}
	return zid, nil
}

// UpdateZettel updates an existing zettel, specified by its zettel identifier.
//
// data contains the zettel metadata and content, as it is stored in a file in a zettel box,
// or as returned by [Client.GetZettel].
// Metadata is separated from zettel content by an empty line.
func (c *Client) UpdateZettel(ctx context.Context, zid api.ZettelID, data []byte) error {
func (c *Client) UpdateZettel(ctx context.Context, zid id.Zid, data []byte) error {
	ub := c.NewURLBuilder('z').SetZid(zid)
	resp, err := c.buildAndExecuteRequest(ctx, http.MethodPut, ub, bytes.NewBuffer(data))
	if err != nil {
		return err
	}
	defer resp.Body.Close()
	defer func() { _ = resp.Body.Close() }()
	if resp.StatusCode != http.StatusNoContent {
		return statusToError(resp)
	}
	return nil
}

// UpdateZettelData updates an existing zettel, specified by its zettel identifier.
func (c *Client) UpdateZettelData(ctx context.Context, zid api.ZettelID, data api.ZettelData) error {
func (c *Client) UpdateZettelData(ctx context.Context, zid id.Zid, data api.ZettelData) error {
	var buf bytes.Buffer
	if _, err := sx.Print(&buf, sexp.EncodeZettel(data)); err != nil {
		return err
	}
	ub := c.NewURLBuilder('z').SetZid(zid).AppendKVQuery(api.QueryKeyEncoding, api.EncodingData)
	resp, err := c.buildAndExecuteRequest(ctx, http.MethodPut, ub, &buf)
	if err != nil {
		return err
	}
	defer resp.Body.Close()
	defer func() { _ = resp.Body.Close() }()
	if resp.StatusCode != http.StatusNoContent {
		return statusToError(resp)
	}
	return nil
}

// DeleteZettel deletes a zettel with the given identifier.
func (c *Client) DeleteZettel(ctx context.Context, zid api.ZettelID) error {
func (c *Client) DeleteZettel(ctx context.Context, zid id.Zid) error {
	ub := c.NewURLBuilder('z').SetZid(zid)
	resp, err := c.buildAndExecuteRequest(ctx, http.MethodDelete, ub, nil)
	if err != nil {
		return err
	}
	defer resp.Body.Close()
	defer func() { _ = resp.Body.Close() }()
	if resp.StatusCode != http.StatusNoContent {
		return statusToError(resp)
	}
	return nil
}

// ExecuteCommand will execute a given command at the Zettelstore.
//
// See [API commands] for a list of valid commands.
//
// [API commands]: https://zettelstore.de/manual/h/00001012080100
func (c *Client) ExecuteCommand(ctx context.Context, command api.Command) error {
	ub := c.NewURLBuilder('x').AppendKVQuery(api.QueryKeyCommand, string(command))
	resp, err := c.buildAndExecuteRequest(ctx, http.MethodPost, ub, nil)
	if err != nil {
		return err
	}
	defer resp.Body.Close()
	defer func() { _ = resp.Body.Close() }()
	if resp.StatusCode != http.StatusNoContent {
		return statusToError(resp)
	}
	return nil
}

Changes to client/client_test.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
47
48
49
50
51

52
53
54
55
56
57
58
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







+













-
+












-
+







	"flag"
	"net/http"
	"net/url"
	"testing"

	"t73f.de/r/zsc/api"
	"t73f.de/r/zsc/client"
	"t73f.de/r/zsc/domain/id"
)

func TestZettelList(t *testing.T) {
	c := getClient()
	_, err := c.QueryZettel(context.Background(), "")
	if err != nil {
		t.Error(err)
		return
	}
}

func TestGetProtectedZettel(t *testing.T) {
	c := getClient()
	_, err := c.GetZettel(context.Background(), api.ZidStartupConfiguration, api.PartZettel)
	_, err := c.GetZettel(context.Background(), id.ZidStartupConfiguration, api.PartZettel)
	if err != nil {
		if cErr, ok := err.(*client.Error); ok && cErr.StatusCode == http.StatusForbidden {
			return
		} else {
			t.Error(err)
		}
		return
	}
}

func TestGetSzZettel(t *testing.T) {
	c := getClient()
	value, err := c.GetEvaluatedSz(context.Background(), api.ZidDefaultHome, api.PartContent)
	value, err := c.GetEvaluatedSz(context.Background(), id.ZidDefaultHome, api.PartContent)
	if err != nil {
		t.Error(err)
		return
	}
	if value.IsNil() {
		t.Error("No data")
	}

Changes to client/retrieve.go.

20
21
22
23
24
25
26

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







+







	"fmt"
	"io"
	"net/http"

	"t73f.de/r/sx"
	"t73f.de/r/sx/sxreader"
	"t73f.de/r/zsc/api"
	"t73f.de/r/zsc/domain/id"
	"t73f.de/r/zsc/sexp"
	"t73f.de/r/zsc/sz"
)

var bsLF = []byte{'\n'}

// QueryZettel returns a list of all Zettel based on the given query.
41
42
43
44
45
46
47
48

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

49
50
51
52
53
54
55
56







-
+







// [Query the list of all zettel]: https://zettelstore.de/manual/h/00001012051400
func (c *Client) QueryZettel(ctx context.Context, query string) ([][]byte, error) {
	ub := c.NewURLBuilder('z').AppendQuery(query)
	resp, err := c.buildAndExecuteRequest(ctx, http.MethodGet, ub, nil)
	if err != nil {
		return nil, err
	}
	defer resp.Body.Close()
	defer func() { _ = resp.Body.Close() }()
	data, err := io.ReadAll(resp.Body)
	switch resp.StatusCode {
	case http.StatusOK:
	case http.StatusNoContent:
		return nil, nil
	default:
		return nil, statusToError(resp)
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
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







-
-
+
+











-
+
















-
-
-
-
-
-

-
+




-
+







// [Query the list of all zettel]: https://zettelstore.de/manual/h/00001012051400
func (c *Client) QueryZettelData(ctx context.Context, query string) (string, string, []api.ZidMetaRights, error) {
	ub := c.NewURLBuilder('z').AppendKVQuery(api.QueryKeyEncoding, api.EncodingData).AppendQuery(query)
	resp, err := c.buildAndExecuteRequest(ctx, http.MethodGet, ub, nil)
	if err != nil {
		return "", "", nil, err
	}
	defer resp.Body.Close()
	rdr := sxreader.MakeReader(resp.Body)
	defer func() { _ = resp.Body.Close() }()
	rdr := sxreader.MakeReader(resp.Body).SetListLimit(0) // No limit b/c number of zettel may be more than 100000. We must trust the server
	obj, err := rdr.Read()
	switch resp.StatusCode {
	case http.StatusOK:
	case http.StatusNoContent:
		return "", "", nil, nil
	default:
		return "", "", nil, statusToError(resp)
	}
	if err != nil {
		return "", "", nil, err
	}
	vals, err := sexp.ParseList(obj, "yppp")
	vals, err := sexp.ParseList(obj, "yppr")
	if err != nil {
		return "", "", nil, err
	}
	qVals, err := sexp.ParseList(vals[1], "ys")
	if err != nil {
		return "", "", nil, err
	}
	hVals, err := sexp.ParseList(vals[2], "ys")
	if err != nil {
		return "", "", nil, err
	}
	metaList, err := parseMetaList(vals[3].(*sx.Pair))
	return sz.GoValue(qVals[1]), sz.GoValue(hVals[1]), metaList, err
}

func parseMetaList(metaPair *sx.Pair) ([]api.ZidMetaRights, error) {
	if metaPair == nil {
		return nil, fmt.Errorf("no zettel list")
	}
	if errSym := sexp.CheckSymbol(metaPair.Car(), "list"); errSym != nil {
		return nil, errSym
	}
	var result []api.ZidMetaRights
	for node := metaPair.Cdr(); !sx.IsNil(node); {
	for node := metaPair; !sx.IsNil(node); {
		elem, isPair := sx.GetPair(node)
		if !isPair {
			return nil, fmt.Errorf("meta-list not a proper list: %v", metaPair.String())
		}
		node = elem.Cdr()
		node = elem.Tail()
		vals, err := sexp.ParseList(elem.Car(), "yppp")
		if err != nil {
			return nil, err
		}

		if errSym := sexp.CheckSymbol(vals[0], "zettel"); errSym != nil {
			return nil, errSym
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
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







-
+











-
+






-
+



-
+






-
+

-
+


-
+




-
+

-
-
-
+
-
-

-
+










-
+








-
+












-
+





-
+




















-
+












-
+



-
+










-
+
















-
+











-
+



-
+












-
+







-
+







-
+







		return nil, nil
	}
	agg := make(api.Aggregate, len(lines))
	for _, line := range lines {
		if fields := bytes.Fields(line); len(fields) > 1 {
			key := string(fields[0])
			for _, field := range fields[1:] {
				if zid := api.ZettelID(string(field)); zid.IsValid() {
				if zid, zidErr := id.Parse(string(field)); zidErr == nil {
					agg[key] = append(agg[key], zid)
				}
			}
		}
	}
	return agg, nil
}

// TagZettel returns the identifier of the tag zettel for a given tag.
//
// This method only works if c.AllowRedirect(true) was called.
func (c *Client) TagZettel(ctx context.Context, tag string) (api.ZettelID, error) {
func (c *Client) TagZettel(ctx context.Context, tag string) (id.Zid, error) {
	return c.fetchTagOrRoleZettel(ctx, api.QueryKeyTag, tag)
}

// RoleZettel returns the identifier of the tag zettel for a given role.
//
// This method only works if c.AllowRedirect(true) was called.
func (c *Client) RoleZettel(ctx context.Context, role string) (api.ZettelID, error) {
func (c *Client) RoleZettel(ctx context.Context, role string) (id.Zid, error) {
	return c.fetchTagOrRoleZettel(ctx, api.QueryKeyRole, role)
}

func (c *Client) fetchTagOrRoleZettel(ctx context.Context, key, val string) (api.ZettelID, error) {
func (c *Client) fetchTagOrRoleZettel(ctx context.Context, key, val string) (id.Zid, error) {
	if c.client.CheckRedirect == nil {
		panic("client does not allow to track redirect")
	}
	ub := c.NewURLBuilder('z').AppendKVQuery(key, val)
	resp, err := c.buildAndExecuteRequest(ctx, http.MethodGet, ub, nil)
	if err != nil {
		return api.InvalidZID, err
		return id.Invalid, err
	}
	defer resp.Body.Close()
	defer func() { _ = resp.Body.Close() }()
	data, err := io.ReadAll(resp.Body)
	if err != nil {
		return api.InvalidZID, err
		return id.Invalid, err
	}

	switch resp.StatusCode {
	case http.StatusNotFound:
		return "", nil
		return id.Invalid, nil
	case http.StatusFound:
		zid := api.ZettelID(data)
		if zid.IsValid() {
			return zid, nil
		return id.Parse(string(data))
		}
		return api.InvalidZID, nil
	default:
		return api.InvalidZID, statusToError(resp)
		return id.Invalid, statusToError(resp)
	}
}

// GetZettel returns a zettel as a byte slice.
//
// part must be one of "meta", "content", or "zettel".
//
// The format of the byte slice is described in [Layout of a zettel].
//
// [Layout of a zettel]: https://zettelstore.de/manual/h/00001006000000
func (c *Client) GetZettel(ctx context.Context, zid api.ZettelID, part string) ([]byte, error) {
func (c *Client) GetZettel(ctx context.Context, zid id.Zid, part string) ([]byte, error) {
	ub := c.NewURLBuilder('z').SetZid(zid)
	if part != "" && part != api.PartContent {
		ub.AppendKVQuery(api.QueryKeyPart, part)
	}
	resp, err := c.buildAndExecuteRequest(ctx, http.MethodGet, ub, nil)
	if err != nil {
		return nil, err
	}
	defer resp.Body.Close()
	defer func() { _ = resp.Body.Close() }()
	data, err := io.ReadAll(resp.Body)
	switch resp.StatusCode {
	case http.StatusOK:
	case http.StatusNoContent:
		return nil, nil
	default:
		return nil, statusToError(resp)
	}
	return data, err
}

// GetZettelData returns a zettel as a struct of its parts.
func (c *Client) GetZettelData(ctx context.Context, zid api.ZettelID) (api.ZettelData, error) {
func (c *Client) GetZettelData(ctx context.Context, zid id.Zid) (api.ZettelData, error) {
	ub := c.NewURLBuilder('z').SetZid(zid)
	ub.AppendKVQuery(api.QueryKeyEncoding, api.EncodingData)
	ub.AppendKVQuery(api.QueryKeyPart, api.PartZettel)
	resp, err := c.buildAndExecuteRequest(ctx, http.MethodGet, ub, nil)
	if err == nil {
		defer resp.Body.Close()
		defer func() { _ = resp.Body.Close() }()
		if resp.StatusCode != http.StatusOK {
			return api.ZettelData{}, statusToError(resp)
		}
		rdr := sxreader.MakeReader(resp.Body)
		obj, err2 := rdr.Read()
		if err2 == nil {
			return sexp.ParseZettel(obj)
		}
	}
	return api.ZettelData{}, err
}

// GetParsedZettel return a parsed zettel in a specified text-based encoding.
//
// A parsed zettel is just read from its box and is not processed any further.
//
// Valid encoding values are given as constants. They are described in more
// detail in [Encodings available via the API].
//
// [Encodings available via the API]: https://zettelstore.de/manual/h/00001012920500
func (c *Client) GetParsedZettel(ctx context.Context, zid api.ZettelID, enc api.EncodingEnum) ([]byte, error) {
func (c *Client) GetParsedZettel(ctx context.Context, zid id.Zid, enc api.EncodingEnum) ([]byte, error) {
	return c.getZettelString(ctx, zid, enc, true)
}

// GetEvaluatedZettel return an evaluated zettel in a specified text-based encoding.
//
// An evaluated zettel was parsed, and any transclusions etc. are resolved.
// This is the zettel representation you typically see on the Web UI.
//
// Valid encoding values are given as constants. They are described in more
// detail in [Encodings available via the API].
//
// [Encodings available via the API]: https://zettelstore.de/manual/h/00001012920500
func (c *Client) GetEvaluatedZettel(ctx context.Context, zid api.ZettelID, enc api.EncodingEnum) ([]byte, error) {
func (c *Client) GetEvaluatedZettel(ctx context.Context, zid id.Zid, enc api.EncodingEnum) ([]byte, error) {
	return c.getZettelString(ctx, zid, enc, false)
}

func (c *Client) getZettelString(ctx context.Context, zid api.ZettelID, enc api.EncodingEnum, parseOnly bool) ([]byte, error) {
func (c *Client) getZettelString(ctx context.Context, zid id.Zid, enc api.EncodingEnum, parseOnly bool) ([]byte, error) {
	ub := c.NewURLBuilder('z').SetZid(zid)
	ub.AppendKVQuery(api.QueryKeyEncoding, enc.String())
	ub.AppendKVQuery(api.QueryKeyPart, api.PartContent)
	if parseOnly {
		ub.AppendKVQuery(api.QueryKeyParseOnly, "")
	}
	resp, err := c.buildAndExecuteRequest(ctx, http.MethodGet, ub, nil)
	if err != nil {
		return nil, err
	}
	defer resp.Body.Close()
	defer func() { _ = resp.Body.Close() }()
	switch resp.StatusCode {
	case http.StatusOK:
	case http.StatusNoContent:
	default:
		return nil, statusToError(resp)
	}
	return io.ReadAll(resp.Body)
}

// GetParsedSz returns a part of an parsed zettel as a Sexpr-decoded data structure.
//
// A parsed zettel is just read from its box and is not processed any further.
//
// part must be one of "meta", "content", or "zettel".
//
// Basically, this function returns the sz encoding of a part of a zettel.
func (c *Client) GetParsedSz(ctx context.Context, zid api.ZettelID, part string) (sx.Object, error) {
func (c *Client) GetParsedSz(ctx context.Context, zid id.Zid, part string) (sx.Object, error) {
	return c.getSz(ctx, zid, part, true)
}

// GetEvaluatedSz returns an evaluated zettel as a Sexpr-decoded data structure.
//
// An evaluated zettel was parsed, and any transclusions etc. are resolved.
// This is the zettel representation you typically see on the Web UI.
//
// part must be one of "meta", "content", or "zettel".
//
// Basically, this function returns the sz encoding of a part of a zettel.
func (c *Client) GetEvaluatedSz(ctx context.Context, zid api.ZettelID, part string) (sx.Object, error) {
func (c *Client) GetEvaluatedSz(ctx context.Context, zid id.Zid, part string) (sx.Object, error) {
	return c.getSz(ctx, zid, part, false)
}

func (c *Client) getSz(ctx context.Context, zid api.ZettelID, part string, parseOnly bool) (sx.Object, error) {
func (c *Client) getSz(ctx context.Context, zid id.Zid, part string, parseOnly bool) (sx.Object, error) {
	ub := c.NewURLBuilder('z').SetZid(zid)
	ub.AppendKVQuery(api.QueryKeyEncoding, api.EncodingSz)
	if part != "" {
		ub.AppendKVQuery(api.QueryKeyPart, part)
	}
	if parseOnly {
		ub.AppendKVQuery(api.QueryKeyParseOnly, "")
	}
	resp, err := c.buildAndExecuteRequest(ctx, http.MethodGet, ub, nil)
	if err != nil {
		return nil, err
	}
	defer resp.Body.Close()
	defer func() { _ = resp.Body.Close() }()
	if resp.StatusCode != http.StatusOK {
		return nil, statusToError(resp)
	}
	return sxreader.MakeReader(bufio.NewReaderSize(resp.Body, 8)).Read()
}

// GetMetaData returns the metadata of a zettel.
func (c *Client) GetMetaData(ctx context.Context, zid api.ZettelID) (api.MetaRights, error) {
func (c *Client) GetMetaData(ctx context.Context, zid id.Zid) (api.MetaRights, error) {
	ub := c.NewURLBuilder('z').SetZid(zid)
	ub.AppendKVQuery(api.QueryKeyEncoding, api.EncodingData)
	ub.AppendKVQuery(api.QueryKeyPart, api.PartMeta)
	resp, err := c.buildAndExecuteRequest(ctx, http.MethodGet, ub, nil)
	if err != nil {
		return api.MetaRights{}, err
	}
	defer resp.Body.Close()
	defer func() { _ = resp.Body.Close() }()
	rdr := sxreader.MakeReader(resp.Body)
	obj, err := rdr.Read()
	if resp.StatusCode != http.StatusOK {
		return api.MetaRights{}, statusToError(resp)
	}
	if err != nil {
		return api.MetaRights{}, err
415
416
417
418
419
420
421
422

423
424
425
426
427
428
429
406
407
408
409
410
411
412

413
414
415
416
417
418
419
420







-
+








// GetVersionInfo returns version information of the Zettelstore that is used.
func (c *Client) GetVersionInfo(ctx context.Context) (VersionInfo, error) {
	resp, err := c.buildAndExecuteRequest(ctx, http.MethodGet, c.NewURLBuilder('x'), nil)
	if err != nil {
		return VersionInfo{}, err
	}
	defer resp.Body.Close()
	defer func() { _ = resp.Body.Close() }()
	if resp.StatusCode != http.StatusOK {
		return VersionInfo{}, statusToError(resp)
	}
	rdr := sxreader.MakeReader(resp.Body)
	obj, err := rdr.Read()
	if err == nil {
		if vals, errVals := sexp.ParseList(obj, "iiiss"); errVals == nil {
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
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







-
-
+
+

-
+




-
+

-
+
+


-
+








-
+










	Patch int
	Info  string
	Hash  string
}

// GetApplicationZid returns the zettel identifier used to configure a client
// application with the given name.
func (c *Client) GetApplicationZid(ctx context.Context, appname string) (api.ZettelID, error) {
	mr, err := c.GetMetaData(ctx, api.ZidAppDirectory)
func (c *Client) GetApplicationZid(ctx context.Context, appname string) (id.Zid, error) {
	mr, err := c.GetMetaData(ctx, id.ZidAppDirectory)
	if err != nil {
		return api.InvalidZID, err
		return id.Invalid, err
	}
	key := appname + "-zid"
	val, found := mr.Meta[key]
	if !found {
		return api.InvalidZID, fmt.Errorf("no application registered: %v", appname)
		return id.Invalid, fmt.Errorf("no application registered: %v", appname)
	}
	if zid := api.ZettelID(val); zid.IsValid() {
	zid, err := id.Parse(val)
	if err == nil {
		return zid, nil
	}
	return api.InvalidZID, fmt.Errorf("invalid identifier for application %v: %v", appname, val)
	return id.Invalid, fmt.Errorf("invalid identifier for application %v: %v", appname, val)
}

// Get executes a GET request to the given URL and returns the read data.
func (c *Client) Get(ctx context.Context, ub *api.URLBuilder) ([]byte, error) {
	resp, err := c.buildAndExecuteRequest(ctx, http.MethodGet, ub, nil)
	if err != nil {
		return nil, err
	}
	defer resp.Body.Close()
	defer func() { _ = resp.Body.Close() }()
	switch resp.StatusCode {
	case http.StatusOK:
	case http.StatusNoContent:
		return nil, nil
	default:
		return nil, statusToError(resp)
	}
	data, err := io.ReadAll(resp.Body)
	return data, err
}

Added domain/id/id.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
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
//-----------------------------------------------------------------------------
// Copyright (c) 2020-present Detlef Stern
//
// 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.
//
// SPDX-License-Identifier: EUPL-1.2
// SPDX-FileCopyrightText: 2020-present Detlef Stern
//-----------------------------------------------------------------------------

// Package id provides zettel specific types, constants, and functions about
// zettel identifier.
package id

import (
	"strconv"
	"time"
)

// Zid is the internal identifier of a zettel. Typically, it is a time stamp
// of the form "YYYYMMDDHHmmSS" converted to an unsigned integer.
type Zid uint64

// LengthZid factors the constant length of a zettel identifier
const LengthZid = 14

// Some important ZettelIDs.
const (
	Invalid = Zid(0) // Invalid is a Zid that will never be valid

	maxZid = 99999999999999
)

// Predefined zettel identifier.
//
// See [List of predefined zettel].
//
// [List of predefined zettel]: https://zettelstore.de/manual/h/00001005090000
const (
	// System zettel
	ZidVersion              = Zid(1)
	ZidHost                 = Zid(2)
	ZidOperatingSystem      = Zid(3)
	ZidLicense              = Zid(4)
	ZidAuthors              = Zid(5)
	ZidDependencies         = Zid(6)
	ZidLog                  = Zid(7)
	ZidMemory               = Zid(8)
	ZidSx                   = Zid(9)
	ZidHTTP                 = Zid(10)
	ZidAPI                  = Zid(11)
	ZidWebUI                = Zid(12)
	ZidConsole              = Zid(13)
	ZidBoxManager           = Zid(20)
	ZidZettel               = Zid(21)
	ZidIndex                = Zid(22)
	ZidQuery                = Zid(23)
	ZidMetadataKey          = Zid(90)
	ZidParser               = Zid(92)
	ZidStartupConfiguration = Zid(96)
	ZidConfiguration        = Zid(100)
	ZidDirectory            = Zid(101)

	// WebUI HTML templates are in the range 10000..19999
	ZidBaseTemplate   = Zid(10100)
	ZidLoginTemplate  = Zid(10200)
	ZidListTemplate   = Zid(10300)
	ZidZettelTemplate = Zid(10401)
	ZidInfoTemplate   = Zid(10402)
	ZidFormTemplate   = Zid(10403)
	ZidDeleteTemplate = Zid(10405)
	ZidErrorTemplate  = Zid(10700)

	// WebUI sxn code zettel are in the range 19000..19999
	ZidSxnStart = Zid(19000)
	ZidSxnBase  = Zid(19990)

	// CSS-related zettel are in the range 20000..29999
	ZidBaseCSS = Zid(20001)
	ZidUserCSS = Zid(25001)

	// WebUI JS zettel are in the range 30000..39999

	// WebUI image zettel are in the range 40000..49999
	ZidEmoji = Zid(40001)

	// Other sxn code zettel are in the range 50000..59999
	ZidSxnPrelude = Zid(59900)

	// Predefined Zettelmarkup zettel are in the range 60000..69999
	ZidRoleZettelZettel        = Zid(60010)
	ZidRoleConfigurationZettel = Zid(60020)
	ZidRoleRoleZettel          = Zid(60030)
	ZidRoleTagZettel           = Zid(60040)

	// Range 80000...89999 is reserved for web ui menus
	ZidTOCListsMenu = Zid(80001) // "Lists" menu

	// Range 90000...99999 is reserved for zettel templates
	ZidTOCNewTemplate    = Zid(90000)
	ZidTemplateNewZettel = Zid(90001)
	ZidTemplateNewRole   = Zid(90004)
	ZidTemplateNewTag    = Zid(90003)
	ZidTemplateNewUser   = Zid(90002)

	// Range 00000999999900...00000999999999 are predefined zettel to be searched by content.
	ZidAppDirectory = Zid(999999999)

	// Default Home Zettel
	ZidDefaultHome = Zid(10000000000)
)

// ParseUint interprets a string as a possible zettel identifier
// and returns its integer value.
func ParseUint(s string) (uint64, error) {
	res, err := strconv.ParseUint(s, 10, 47)
	if err != nil {
		return 0, err
	}
	if res == 0 || res > maxZid {
		return res, strconv.ErrRange
	}
	return res, nil
}

// Parse interprets a string as a zettel identification and
// returns its value.
func Parse(s string) (Zid, error) {
	if len(s) != LengthZid {
		return Invalid, strconv.ErrSyntax
	}
	res, err := ParseUint(s)
	if err != nil {
		return Invalid, err
	}
	return Zid(res), nil
}

// MustParse tries to interpret a string as a zettel identifier and returns
// its value or panics otherwise.
func MustParse(s string) Zid {
	zid, err := Parse(string(s))
	if err == nil {
		return zid
	}
	panic(err)
}

// String converts the zettel identification to a string of 14 digits.
// Only defined for valid ids.
func (zid Zid) String() string {
	var result [LengthZid]byte
	zid.toByteArray(&result)
	return string(result[:])
}

// Bytes converts the zettel identification to a byte slice of 14 digits.
// Only defined for valid ids.
func (zid Zid) Bytes() []byte {
	var result [LengthZid]byte
	zid.toByteArray(&result)
	return result[:]
}

// toByteArray converts the Zid into a fixed byte array, usable for printing.
//
// Based on idea by Daniel Lemire: "Converting integers to fix-digit representations quickly"
// https://lemire.me/blog/2021/11/18/converting-integers-to-fix-digit-representations-quickly/
func (zid Zid) toByteArray(result *[LengthZid]byte) {
	date := uint64(zid) / 1000000
	fullyear := date / 10000
	century, year := fullyear/100, fullyear%100
	monthday := date % 10000
	month, day := monthday/100, monthday%100
	time := uint64(zid) % 1000000
	hmtime, second := time/100, time%100
	hour, minute := hmtime/100, hmtime%100

	result[0] = byte(century/10) + '0'
	result[1] = byte(century%10) + '0'
	result[2] = byte(year/10) + '0'
	result[3] = byte(year%10) + '0'
	result[4] = byte(month/10) + '0'
	result[5] = byte(month%10) + '0'
	result[6] = byte(day/10) + '0'
	result[7] = byte(day%10) + '0'
	result[8] = byte(hour/10) + '0'
	result[9] = byte(hour%10) + '0'
	result[10] = byte(minute/10) + '0'
	result[11] = byte(minute%10) + '0'
	result[12] = byte(second/10) + '0'
	result[13] = byte(second%10) + '0'
}

// IsValid determines if zettel id is a valid one, e.g. consists of max. 14 digits.
func (zid Zid) IsValid() bool { return 0 < zid && zid <= maxZid }

// TimestampLayout to transform a date into a Zid and into other internal dates.
const TimestampLayout = "20060102150405"

// New returns a new zettel id based on the current time.
func New(withSeconds bool) Zid {
	now := time.Now().Local()
	var s string
	if withSeconds {
		s = now.Format(TimestampLayout)
	} else {
		s = now.Format("20060102150400")
	}
	res, err := Parse(s)
	if err != nil {
		panic(err)
	}
	return res
}

Added domain/id/id_test.go.





























































































1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
//-----------------------------------------------------------------------------
// Copyright (c) 2020-present Detlef Stern
//
// 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.
//
// SPDX-License-Identifier: EUPL-1.2
// SPDX-FileCopyrightText: 2020-present Detlef Stern
//-----------------------------------------------------------------------------

// Package id_test provides unit tests for testing zettel id specific functions.
package id_test

import (
	"testing"

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

func TestIsValid(t *testing.T) {
	t.Parallel()
	validIDs := []string{
		"00000000000001",
		"00000000000020",
		"00000000000300",
		"00000000004000",
		"00000000050000",
		"00000000600000",
		"00000007000000",
		"00000080000000",
		"00000900000000",
		"00001000000000",
		"00020000000000",
		"00300000000000",
		"04000000000000",
		"50000000000000",
		"99999999999999",
		"00001007030200",
		"20200310195100",
		"12345678901234",
	}

	for i, sid := range validIDs {
		zid, err := id.Parse(sid)
		if err != nil {
			t.Errorf("i=%d: sid=%q is not valid, but should be. err=%v", i, sid, err)
		}
		s := zid.String()
		if s != sid {
			t.Errorf(
				"i=%d: zid=%v does not format to %q, but to %q", i, zid, sid, s)
		}
	}

	invalidIDs := []string{
		"", "0", "a",
		"00000000000000",
		"0000000000000a",
		"000000000000000",
		"20200310T195100",
		"+1234567890123",
	}

	for i, sid := range invalidIDs {
		if zid, err := id.Parse(sid); err == nil {
			t.Errorf("i=%d: sid=%q is valid (zid=%s), but should not be", i, sid, zid)
		}
	}
}

var sResult string // to disable compiler optimization in loop below

func BenchmarkString(b *testing.B) {
	var s string
	for b.Loop() {
		s = id.Zid(12345678901200).String()
	}
	sResult = s
}

var bResult []byte // to disable compiler optimization in loop below

func BenchmarkBytes(b *testing.B) {
	var bs []byte
	for b.Loop() {
		bs = id.Zid(12345678901200).Bytes()
	}
	bResult = bs
}

Added domain/id/idgraph/digraph.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
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
//-----------------------------------------------------------------------------
// Copyright (c) 2023-present Detlef Stern
//
// 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.
//
// SPDX-License-Identifier: EUPL-1.2
// SPDX-FileCopyrightText: 2023-present Detlef Stern
//-----------------------------------------------------------------------------

// Package idgraph implements a graph of zettel identifier.
package idgraph

import (
	"maps"
	"slices"

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

// Digraph relates zettel identifier in a directional way.
type Digraph map[id.Zid]*idset.Set

// AddVertex adds an edge / vertex to the digraph.
func (dg Digraph) AddVertex(zid id.Zid) Digraph {
	if dg == nil {
		return Digraph{zid: nil}
	}
	if _, found := dg[zid]; !found {
		dg[zid] = nil
	}
	return dg
}

// RemoveVertex removes a vertex and all its edges from the digraph.
func (dg Digraph) RemoveVertex(zid id.Zid) {
	if len(dg) > 0 {
		delete(dg, zid)
		for vertex, closure := range dg {
			dg[vertex] = closure.Remove(zid)
		}
	}
}

// AddEdge adds a connection from `zid1` to `zid2`.
// Both vertices must be added before. Otherwise the function may panic.
func (dg Digraph) AddEdge(fromZid, toZid id.Zid) Digraph {
	if dg == nil {
		return Digraph{fromZid: (*idset.Set)(nil).Add(toZid), toZid: nil}
	}
	dg[fromZid] = dg[fromZid].Add(toZid)
	return dg
}

// AddEgdes adds all given `Edge`s to the digraph.
//
// In contrast to `AddEdge` the vertices must not exist before.
func (dg Digraph) AddEgdes(edges EdgeSlice) Digraph {
	if dg == nil {
		if len(edges) == 0 {
			return nil
		}
		dg = make(Digraph, len(edges))
	}
	for _, edge := range edges {
		dg = dg.AddVertex(edge.From)
		dg = dg.AddVertex(edge.To)
		dg = dg.AddEdge(edge.From, edge.To)
	}
	return dg
}

// Equal returns true if both digraphs have the same vertices and edges.
func (dg Digraph) Equal(other Digraph) bool {
	return maps.EqualFunc(dg, other, func(cg, co *idset.Set) bool { return cg.Equal(co) })
}

// Clone a digraph.
func (dg Digraph) Clone() Digraph {
	if len(dg) == 0 {
		return nil
	}
	copyDG := make(Digraph, len(dg))
	for vertex, closure := range dg {
		copyDG[vertex] = closure.Clone()
	}
	return copyDG
}

// HasVertex returns true, if `zid` is a vertex of the digraph.
func (dg Digraph) HasVertex(zid id.Zid) bool {
	if len(dg) == 0 {
		return false
	}
	_, found := dg[zid]
	return found
}

// Vertices returns the set of all vertices.
func (dg Digraph) Vertices() *idset.Set {
	if len(dg) == 0 {
		return nil
	}
	verts := idset.NewCap(len(dg))
	for vert := range dg {
		verts.Add(vert)
	}
	return verts
}

// Edges returns an unsorted slice of the edges of the digraph.
func (dg Digraph) Edges() (es EdgeSlice) {
	for vert, closure := range dg {
		closure.ForEach(func(next id.Zid) {
			es = append(es, Edge{From: vert, To: next})
		})
	}
	return es
}

// Originators will return the set of all vertices that are not referenced
// a the to-part of an edge.
func (dg Digraph) Originators() *idset.Set {
	if len(dg) == 0 {
		return nil
	}
	origs := dg.Vertices()
	for _, closure := range dg {
		origs.ISubstract(closure)
	}
	return origs
}

// Terminators returns the set of all vertices that does not reference
// other vertices.
func (dg Digraph) Terminators() (terms *idset.Set) {
	for vert, closure := range dg {
		if closure.IsEmpty() {
			terms = terms.Add(vert)
		}
	}
	return terms
}

// TransitiveClosure calculates the sub-graph that is reachable from `zid`.
func (dg Digraph) TransitiveClosure(zid id.Zid) (tc Digraph) {
	if len(dg) == 0 {
		return nil
	}
	var marked *idset.Set
	stack := []id.Zid{zid}
	for pos := len(stack) - 1; pos >= 0; pos = len(stack) - 1 {
		curr := stack[pos]
		stack = stack[:pos]
		if marked.Contains(curr) {
			continue
		}
		tc = tc.AddVertex(curr)
		dg[curr].ForEach(func(next id.Zid) {
			tc = tc.AddVertex(next)
			tc = tc.AddEdge(curr, next)
			stack = append(stack, next)
		})
		marked = marked.Add(curr)
	}
	return tc
}

// ReachableVertices calculates the set of all vertices that are reachable
// from the given `zid`.
func (dg Digraph) ReachableVertices(zid id.Zid) (tc *idset.Set) {
	if len(dg) == 0 {
		return nil
	}
	stack := dg[zid].SafeSorted()
	for last := len(stack) - 1; last >= 0; last = len(stack) - 1 {
		curr := stack[last]
		stack = stack[:last]
		if tc.Contains(curr) {
			continue
		}
		closure, found := dg[curr]
		if !found {
			continue
		}
		tc = tc.Add(curr)
		closure.ForEach(func(next id.Zid) {
			stack = append(stack, next)
		})
	}
	return tc
}

// IsDAG returns a vertex and false, if the graph has a cycle containing the vertex.
func (dg Digraph) IsDAG() (id.Zid, bool) {
	for vertex := range dg {
		if dg.ReachableVertices(vertex).Contains(vertex) {
			return vertex, false
		}
	}
	return id.Invalid, true
}

// Reverse returns a graph with reversed edges.
func (dg Digraph) Reverse() (revDg Digraph) {
	for vertex, closure := range dg {
		revDg = revDg.AddVertex(vertex)
		closure.ForEach(func(next id.Zid) {
			revDg = revDg.AddVertex(next)
			revDg = revDg.AddEdge(next, vertex)
		})
	}
	return revDg
}

// SortReverse returns a deterministic, topological, reverse sort of the
// digraph.
//
// Works only if digraph is a DAG. Otherwise the algorithm will not terminate
// or returns an arbitrary value.
func (dg Digraph) SortReverse() (sl []id.Zid) {
	if len(dg) == 0 {
		return nil
	}
	tempDg := dg.Clone()
	for len(tempDg) > 0 {
		terms := tempDg.Terminators()
		if terms.IsEmpty() {
			break
		}
		termSlice := terms.SafeSorted()
		slices.Reverse(termSlice)
		sl = append(sl, termSlice...)
		terms.ForEach(func(t id.Zid) {
			tempDg.RemoveVertex(t)
		})
	}
	return sl
}

Added domain/id/idgraph/digraph_test.go.






















































































































































































1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
//-----------------------------------------------------------------------------
// Copyright (c) 2023-present Detlef Stern
//
// 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.
//
// SPDX-License-Identifier: EUPL-1.2
// SPDX-FileCopyrightText: 2023-present Detlef Stern
//-----------------------------------------------------------------------------

package idgraph_test

import (
	"slices"
	"testing"

	"t73f.de/r/zsc/domain/id"
	"t73f.de/r/zsc/domain/id/idgraph"
	"t73f.de/r/zsc/domain/id/idset"
)

type zps = idgraph.EdgeSlice

func createDigraph(pairs zps) (dg idgraph.Digraph) {
	return dg.AddEgdes(pairs)
}

func TestDigraphOriginators(t *testing.T) {
	t.Parallel()
	testcases := []struct {
		name string
		dg   idgraph.EdgeSlice
		orig *idset.Set
		term *idset.Set
	}{
		{"empty", nil, nil, nil},
		{"single", zps{{0, 1}}, idset.New(0), idset.New(1)},
		{"chain", zps{{0, 1}, {1, 2}, {2, 3}}, idset.New(0), idset.New(3)},
	}
	for _, tc := range testcases {
		t.Run(tc.name, func(t *testing.T) {
			dg := createDigraph(tc.dg)
			if got := dg.Originators(); !tc.orig.Equal(got) {
				t.Errorf("Originators: expected:\n%v, but got:\n%v", tc.orig, got)
			}
			if got := dg.Terminators(); !tc.term.Equal(got) {
				t.Errorf("Termintors: expected:\n%v, but got:\n%v", tc.orig, got)
			}
		})
	}
}

func TestDigraphReachableVertices(t *testing.T) {
	t.Parallel()
	testcases := []struct {
		name  string
		pairs idgraph.EdgeSlice
		start id.Zid
		exp   *idset.Set
	}{
		{"nil", nil, 0, nil},
		{"0-2", zps{{1, 2}, {2, 3}}, 1, idset.New(2, 3)},
		{"1,2", zps{{1, 2}, {2, 3}}, 2, idset.New(3)},
		{"0-2,1-2", zps{{1, 2}, {2, 3}, {1, 3}}, 1, idset.New(2, 3)},
		{"0-2,1-2/1", zps{{1, 2}, {2, 3}, {1, 3}}, 2, idset.New(3)},
		{"0-2,1-2/2", zps{{1, 2}, {2, 3}, {1, 3}}, 3, nil},
		{"0-2,1-2,3*", zps{{1, 2}, {2, 3}, {1, 3}, {4, 4}}, 1, idset.New(2, 3)},
	}

	for _, tc := range testcases {
		t.Run(tc.name, func(t *testing.T) {
			dg := createDigraph(tc.pairs)
			if got := dg.ReachableVertices(tc.start); !got.Equal(tc.exp) {
				t.Errorf("\n%v, but got:\n%v", tc.exp, got)
			}

		})
	}
}

func TestDigraphTransitiveClosure(t *testing.T) {
	t.Parallel()
	testcases := []struct {
		name  string
		pairs idgraph.EdgeSlice
		start id.Zid
		exp   idgraph.EdgeSlice
	}{
		{"nil", nil, 0, nil},
		{"1-3", zps{{1, 2}, {2, 3}}, 1, zps{{1, 2}, {2, 3}}},
		{"1,2", zps{{1, 1}, {2, 3}}, 2, zps{{2, 3}}},
		{"0-2,1-2", zps{{1, 2}, {2, 3}, {1, 3}}, 1, zps{{1, 2}, {1, 3}, {2, 3}}},
		{"0-2,1-2/1", zps{{1, 2}, {2, 3}, {1, 3}}, 1, zps{{1, 2}, {1, 3}, {2, 3}}},
		{"0-2,1-2/2", zps{{1, 2}, {2, 3}, {1, 3}}, 2, zps{{2, 3}}},
		{"0-2,1-2,3*", zps{{1, 2}, {2, 3}, {1, 3}, {4, 4}}, 1, zps{{1, 2}, {1, 3}, {2, 3}}},
	}

	for _, tc := range testcases {
		t.Run(tc.name, func(t *testing.T) {
			dg := createDigraph(tc.pairs)
			if got := dg.TransitiveClosure(tc.start).Edges().Sort(); !got.Equal(tc.exp) {
				t.Errorf("\n%v, but got:\n%v", tc.exp, got)
			}
		})
	}
}

func TestIsDAG(t *testing.T) {
	t.Parallel()
	testcases := []struct {
		name string
		dg   idgraph.EdgeSlice
		exp  bool
	}{
		{"empty", nil, true},
		{"single-edge", zps{{1, 2}}, true},
		{"single-loop", zps{{1, 1}}, false},
		{"long-loop", zps{{1, 2}, {2, 3}, {3, 4}, {4, 5}, {5, 2}}, false},
	}
	for _, tc := range testcases {
		t.Run(tc.name, func(t *testing.T) {
			if zid, got := createDigraph(tc.dg).IsDAG(); got != tc.exp {
				t.Errorf("expected %v, but got %v (%v)", tc.exp, got, zid)
			}
		})
	}
}

func TestDigraphReverse(t *testing.T) {
	t.Parallel()
	testcases := []struct {
		name string
		dg   idgraph.EdgeSlice
		exp  idgraph.EdgeSlice
	}{
		{"empty", nil, nil},
		{"single-edge", zps{{1, 2}}, zps{{2, 1}}},
		{"single-loop", zps{{1, 1}}, zps{{1, 1}}},
		{"end-loop", zps{{1, 2}, {2, 2}}, zps{{2, 1}, {2, 2}}},
		{"long-loop", zps{{1, 2}, {2, 3}, {3, 4}, {4, 5}, {5, 2}}, zps{{2, 1}, {2, 5}, {3, 2}, {4, 3}, {5, 4}}},
		{"sect-loop", zps{{1, 2}, {2, 3}, {3, 4}, {4, 5}, {4, 2}}, zps{{2, 1}, {2, 4}, {3, 2}, {4, 3}, {5, 4}}},
		{"two-islands", zps{{1, 2}, {2, 3}, {4, 5}}, zps{{2, 1}, {3, 2}, {5, 4}}},
		{"direct-indirect", zps{{1, 2}, {1, 3}, {3, 2}}, zps{{2, 1}, {2, 3}, {3, 1}}},
	}
	for _, tc := range testcases {
		t.Run(tc.name, func(t *testing.T) {
			dg := createDigraph(tc.dg)
			if got := dg.Reverse().Edges().Sort(); !got.Equal(tc.exp) {
				t.Errorf("\n%v, but got:\n%v", tc.exp, got)
			}
		})
	}
}

func TestDigraphSortReverse(t *testing.T) {
	t.Parallel()
	testcases := []struct {
		name string
		dg   idgraph.EdgeSlice
		exp  []id.Zid
	}{
		{"empty", nil, nil},
		{"single-edge", zps{{1, 2}}, []id.Zid{2, 1}},
		{"single-loop", zps{{1, 1}}, nil},
		{"end-loop", zps{{1, 2}, {2, 2}}, []id.Zid{}},
		{"long-loop", zps{{1, 2}, {2, 3}, {3, 4}, {4, 5}, {5, 2}}, []id.Zid{}},
		{"sect-loop", zps{{1, 2}, {2, 3}, {3, 4}, {4, 5}, {4, 2}}, []id.Zid{5}},
		{"two-islands", zps{{1, 2}, {2, 3}, {4, 5}}, []id.Zid{5, 3, 4, 2, 1}},
		{"direct-indirect", zps{{1, 2}, {1, 3}, {3, 2}}, []id.Zid{2, 3, 1}},
	}
	for _, tc := range testcases {
		t.Run(tc.name, func(t *testing.T) {
			if got := createDigraph(tc.dg).SortReverse(); !slices.Equal(got, tc.exp) {
				t.Errorf("expected:\n%v, but got:\n%v", tc.exp, got)
			}
		})
	}
}

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

package idgraph

import (
	"slices"

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

// Edge is a pair of to vertices.
type Edge struct {
	From, To id.Zid
}

// EdgeSlice is a slice of Edges
type EdgeSlice []Edge

// Equal return true if both slices are the same.
func (es EdgeSlice) Equal(other EdgeSlice) bool {
	return slices.Equal(es, other)
}

// Sort the slice.
func (es EdgeSlice) Sort() EdgeSlice {
	slices.SortFunc(es, func(e1, e2 Edge) int {
		if e1.From < e2.From {
			return -1
		}
		if e1.From > e2.From {
			return 1
		}
		if e1.To < e2.To {
			return -1
		}
		if e1.To > e2.To {
			return 1
		}
		return 0
	})
	return es
}

Added domain/id/idset/idset.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
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
//-----------------------------------------------------------------------------
// Copyright (c) 2021-present Detlef Stern
//
// 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.
//
// SPDX-License-Identifier: EUPL-1.2
// SPDX-FileCopyrightText: 2021-present Detlef Stern
//-----------------------------------------------------------------------------

// Package idset implements sets of zettel identifier.
package idset

import (
	"slices"
	"strings"

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

// Set is a set of zettel identifier
type Set struct {
	seq []id.Zid
}

// String returns a string representation of the set.
func (s *Set) String() string {
	return "{" + s.MetaString() + "}"
}

// MetaString returns a string representation of the set to be stored as metadata.
func (s *Set) MetaString() string {
	if s == nil || len(s.seq) == 0 {
		return ""
	}
	var sb strings.Builder
	for i, zid := range s.seq {
		if i > 0 {
			sb.WriteByte(' ')
		}
		sb.Write(zid.Bytes())
	}
	return sb.String()
}

// MetaValue returns a metadata value representation of the set.
func (s *Set) MetaValue() meta.Value { return meta.Value(s.MetaString()) }

// New returns a new set of identifier with the given initial values.
func New(zids ...id.Zid) *Set {
	switch l := len(zids); l {
	case 0:
		return &Set{seq: nil}
	case 1:
		return &Set{seq: []id.Zid{zids[0]}}
	default:
		result := Set{seq: make([]id.Zid, 0, l)}
		result.AddSlice(zids)
		return &result
	}
}

// NewCap returns a new set of identifier with the given capacity and initial values.
func NewCap(c int, zids ...id.Zid) *Set {
	result := Set{seq: make([]id.Zid, 0, max(c, len(zids)))}
	result.AddSlice(zids)
	return &result
}

// IsEmpty returns true, if the set conains no element.
func (s *Set) IsEmpty() bool {
	return s == nil || len(s.seq) == 0
}

// Length returns the number of elements in this set.
func (s *Set) Length() int {
	if s == nil {
		return 0
	}
	return len(s.seq)
}

// Clone returns a copy of the given set.
func (s *Set) Clone() *Set {
	if s == nil || len(s.seq) == 0 {
		return nil
	}
	return &Set{seq: slices.Clone(s.seq)}
}

// Add adds a Add to the set.
func (s *Set) Add(zid id.Zid) *Set {
	if s == nil {
		return New(zid)
	}
	s.add(zid)
	return s
}

// Contains return true if the set is non-nil and the set contains the given Zettel identifier.
func (s *Set) Contains(zid id.Zid) bool { return s != nil && s.contains(zid) }

// ContainsOrNil return true if the set is nil or if the set contains the given Zettel identifier.
func (s *Set) ContainsOrNil(zid id.Zid) bool { return s == nil || s.contains(zid) }

// AddSlice adds all identifier of the given slice to the set.
func (s *Set) AddSlice(sl []id.Zid) *Set {
	if s == nil {
		return New(sl...)
	}
	s.seq = slices.Grow(s.seq, len(sl))
	for _, zid := range sl {
		s.add(zid)
	}
	return s
}

// SafeSorted returns the set as a new sorted slice of zettel identifier.
func (s *Set) SafeSorted() []id.Zid {
	if s == nil {
		return nil
	}
	return slices.Clone(s.seq)
}

// IntersectOrSet removes all zettel identifier that are not in the other set.
// Both sets can be modified by this method. One of them is the set returned.
// It contains the intersection of both, if s is not nil.
//
// If s == nil, then the other set is always returned.
func (s *Set) IntersectOrSet(other *Set) *Set {
	if s == nil || other == nil {
		return other.Clone()
	}
	topos, spos, opos := 0, 0, 0
	for spos < len(s.seq) && opos < len(other.seq) {
		sz, oz := s.seq[spos], other.seq[opos]
		if sz < oz {
			spos++
			continue
		}
		if sz > oz {
			opos++
			continue
		}
		s.seq[topos] = sz
		topos++
		spos++
		opos++
	}
	s.seq = s.seq[:topos]
	return s
}

// IUnion adds the elements of set other to s.
func (s *Set) IUnion(other *Set) *Set {
	if other == nil || len(other.seq) == 0 {
		return s
	}
	// TODO: if other is large enough (and s is not too small) -> optimize by swapping and/or loop through both
	return s.AddSlice(other.seq)
}

// ISubstract removes all zettel identifier from 's' that are in the set 'other'.
func (s *Set) ISubstract(other *Set) {
	if s == nil || len(s.seq) == 0 || other == nil || len(other.seq) == 0 {
		return
	}
	topos, spos, opos := 0, 0, 0
	for spos < len(s.seq) && opos < len(other.seq) {
		sz, oz := s.seq[spos], other.seq[opos]
		if sz < oz {
			s.seq[topos] = sz
			topos++
			spos++
			continue
		}
		if sz == oz {
			spos++
		}
		opos++
	}
	for spos < len(s.seq) {
		s.seq[topos] = s.seq[spos]
		topos++
		spos++
	}
	s.seq = s.seq[:topos]
}

// Diff returns the difference sets between the two sets: the first difference
// set is the set of elements that are in other, but not in s; the second
// difference set is the set of element that are in s but not in other.
//
// in other words: the first result is the set of elements from other that must
// be added to s; the second result is the set of elements that must be removed
// from s, so that s would have the same elemest as other.
func (s *Set) Diff(other *Set) (newS, remS *Set) {
	if s == nil || len(s.seq) == 0 {
		return other.Clone(), nil
	}
	if other == nil || len(other.seq) == 0 {
		return nil, s.Clone()
	}
	seqS, seqO := s.seq, other.seq
	var newRefs, remRefs []id.Zid
	npos, opos := 0, 0
	for npos < len(seqO) && opos < len(seqS) {
		rn, ro := seqO[npos], seqS[opos]
		if rn == ro {
			npos++
			opos++
			continue
		}
		if rn < ro {
			newRefs = append(newRefs, rn)
			npos++
			continue
		}
		remRefs = append(remRefs, ro)
		opos++
	}
	if npos < len(seqO) {
		newRefs = append(newRefs, seqO[npos:]...)
	}
	if opos < len(seqS) {
		remRefs = append(remRefs, seqS[opos:]...)
	}
	return newFromSlice(newRefs), newFromSlice(remRefs)
}

// Remove the identifier from the set.
func (s *Set) Remove(zid id.Zid) *Set {
	if s == nil || len(s.seq) == 0 {
		return nil
	}
	if pos, found := s.find(zid); found {
		copy(s.seq[pos:], s.seq[pos+1:])
		s.seq = s.seq[:len(s.seq)-1]
	}
	if len(s.seq) == 0 {
		return nil
	}
	return s
}

// Equal returns true if the other set is equal to the given set.
func (s *Set) Equal(other *Set) bool {
	if s == nil {
		return other == nil
	}
	if other == nil {
		return false
	}
	return slices.Equal(s.seq, other.seq)
}

// ForEach calls the given function for each element of the set.
//
// Every element is bigger than the previous one.
func (s *Set) ForEach(fn func(zid id.Zid)) {
	if s != nil {
		for _, zid := range s.seq {
			fn(zid)
		}
	}
}

// Pop return one arbitrary element of the set.
func (s *Set) Pop() (id.Zid, bool) {
	if s != nil {
		if l := len(s.seq); l > 0 {
			zid := s.seq[l-1]
			s.seq = s.seq[:l-1]
			return zid, true
		}
	}
	return id.Invalid, false
}

// Optimize the amount of memory to store the set.
func (s *Set) Optimize() {
	if s != nil {
		s.seq = slices.Clip(s.seq)
	}
}

// ----- unchecked base operations

func newFromSlice(seq []id.Zid) *Set {
	if l := len(seq); l == 0 {
		return nil
	}
	return &Set{seq: seq}
}

func (s *Set) add(zid id.Zid) {
	if pos, found := s.find(zid); !found {
		s.seq = slices.Insert(s.seq, pos, zid)
	}
}

func (s *Set) contains(zid id.Zid) bool {
	_, found := s.find(zid)
	return found
}

func (s *Set) find(zid id.Zid) (int, bool) {
	hi := len(s.seq)
	for lo := 0; lo < hi; {
		m := lo + (hi-lo)/2
		if z := s.seq[m]; z == zid {
			return m, true
		} else if z < zid {
			lo = m + 1
		} else {
			hi = m
		}
	}
	return hi, false
}

Added domain/id/idset/idset_test.go.


















































































































































































































































1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
//-----------------------------------------------------------------------------
// Copyright (c) 2021-present Detlef Stern
//
// 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.
//
// SPDX-License-Identifier: EUPL-1.2
// SPDX-FileCopyrightText: 2021-present Detlef Stern
//-----------------------------------------------------------------------------

package idset_test

import (
	"slices"
	"testing"

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

func TestSetContainsOrNil(t *testing.T) {
	t.Parallel()
	testcases := []struct {
		s   *idset.Set
		zid id.Zid
		exp bool
	}{
		{nil, id.Invalid, true},
		{nil, 14, true},
		{idset.New(), id.Invalid, false},
		{idset.New(), 1, false},
		{idset.New(), id.Invalid, false},
		{idset.New(1), 1, true},
	}
	for i, tc := range testcases {
		got := tc.s.ContainsOrNil(tc.zid)
		if got != tc.exp {
			t.Errorf("%d: %v.ContainsOrNil(%v) == %v, but got %v", i, tc.s, tc.zid, tc.exp, got)
		}
	}
}

func TestSetAdd(t *testing.T) {
	t.Parallel()
	testcases := []struct {
		s1, s2 *idset.Set
		exp    []id.Zid
	}{
		{nil, nil, nil},
		{idset.New(), nil, nil},
		{idset.New(), idset.New(), nil},
		{nil, idset.New(1), []id.Zid{1}},
		{idset.New(1), nil, []id.Zid{1}},
		{idset.New(1), idset.New(), []id.Zid{1}},
		{idset.New(1), idset.New(2), []id.Zid{1, 2}},
		{idset.New(1), idset.New(1), []id.Zid{1}},
	}
	for i, tc := range testcases {
		sl1 := tc.s1.SafeSorted()
		sl2 := tc.s2.SafeSorted()
		got := tc.s1.IUnion(tc.s2).SafeSorted()
		if !slices.Equal(got, tc.exp) {
			t.Errorf("%d: %v.Add(%v) should be %v, but got %v", i, sl1, sl2, tc.exp, got)
		}
	}
}

func TestSetSafeSorted(t *testing.T) {
	t.Parallel()
	testcases := []struct {
		set *idset.Set
		exp []id.Zid
	}{
		{nil, nil},
		{idset.New(), nil},
		{idset.New(9, 4, 6, 1, 7), []id.Zid{1, 4, 6, 7, 9}},
	}
	for i, tc := range testcases {
		got := tc.set.SafeSorted()
		if !slices.Equal(got, tc.exp) {
			t.Errorf("%d: %v.SafeSorted() should be %v, but got %v", i, tc.set, tc.exp, got)
		}
	}
}

func TestSetIntersectOrSet(t *testing.T) {
	t.Parallel()
	testcases := []struct {
		s1, s2 *idset.Set
		exp    []id.Zid
	}{
		{nil, nil, nil},
		{idset.New(), nil, nil},
		{nil, idset.New(), nil},
		{idset.New(), idset.New(), nil},
		{idset.New(1), nil, nil},
		{nil, idset.New(1), []id.Zid{1}},
		{idset.New(1), idset.New(), nil},
		{idset.New(), idset.New(1), nil},
		{idset.New(1), idset.New(2), nil},
		{idset.New(2), idset.New(1), nil},
		{idset.New(1), idset.New(1), []id.Zid{1}},
	}
	for i, tc := range testcases {
		sl1 := tc.s1.SafeSorted()
		sl2 := tc.s2.SafeSorted()
		got := tc.s1.IntersectOrSet(tc.s2).SafeSorted()
		if !slices.Equal(got, tc.exp) {
			t.Errorf("%d: %v.IntersectOrSet(%v) should be %v, but got %v", i, sl1, sl2, tc.exp, got)
		}
	}
}

func TestSetIUnion(t *testing.T) {
	t.Parallel()
	testcases := []struct {
		s1, s2 *idset.Set
		exp    *idset.Set
	}{
		{nil, nil, nil},
		{idset.New(), nil, nil},
		{nil, idset.New(), nil},
		{idset.New(), idset.New(), nil},
		{idset.New(1), nil, idset.New(1)},
		{nil, idset.New(1), idset.New(1)},
		{idset.New(1), idset.New(), idset.New(1)},
		{idset.New(), idset.New(1), idset.New(1)},
		{idset.New(1), idset.New(2), idset.New(1, 2)},
		{idset.New(2), idset.New(1), idset.New(2, 1)},
		{idset.New(1), idset.New(1), idset.New(1)},
		{idset.New(1, 2, 3), idset.New(2, 3, 4), idset.New(1, 2, 3, 4)},
	}
	for i, tc := range testcases {
		s1 := tc.s1.Clone()
		sl1 := s1.SafeSorted()
		sl2 := tc.s2.SafeSorted()
		got := s1.IUnion(tc.s2)
		if !got.Equal(tc.exp) {
			t.Errorf("%d: %v.IUnion(%v) should be %v, but got %v", i, sl1, sl2, tc.exp, got)
		}
	}
}

func TestSetISubtract(t *testing.T) {
	t.Parallel()
	testcases := []struct {
		s1, s2 *idset.Set
		exp    []id.Zid
	}{
		{nil, nil, nil},
		{idset.New(), nil, nil},
		{nil, idset.New(), nil},
		{idset.New(), idset.New(), nil},
		{idset.New(1), nil, []id.Zid{1}},
		{nil, idset.New(1), nil},
		{idset.New(1), idset.New(), []id.Zid{1}},
		{idset.New(), idset.New(1), nil},
		{idset.New(1), idset.New(2), []id.Zid{1}},
		{idset.New(2), idset.New(1), []id.Zid{2}},
		{idset.New(1), idset.New(1), nil},
		{idset.New(1, 2, 3), idset.New(1), []id.Zid{2, 3}},
		{idset.New(1, 2, 3), idset.New(2), []id.Zid{1, 3}},
		{idset.New(1, 2, 3), idset.New(3), []id.Zid{1, 2}},
		{idset.New(1, 2, 3), idset.New(1, 2), []id.Zid{3}},
		{idset.New(1, 2, 3), idset.New(1, 3), []id.Zid{2}},
		{idset.New(1, 2, 3), idset.New(2, 3), []id.Zid{1}},
	}
	for i, tc := range testcases {
		s1 := tc.s1.Clone()
		sl1 := s1.SafeSorted()
		sl2 := tc.s2.SafeSorted()
		s1.ISubstract(tc.s2)
		got := s1.SafeSorted()
		if !slices.Equal(got, tc.exp) {
			t.Errorf("%d: %v.ISubstract(%v) should be %v, but got %v", i, sl1, sl2, tc.exp, got)
		}
	}
}

func TestSetDiff(t *testing.T) {
	t.Parallel()
	testcases := []struct {
		in1, in2   *idset.Set
		exp1, exp2 *idset.Set
	}{
		{nil, nil, nil, nil},
		{idset.New(1), nil, nil, idset.New(1)},
		{nil, idset.New(1), idset.New(1), nil},
		{idset.New(1), idset.New(1), nil, nil},
		{idset.New(1, 2), idset.New(1), nil, idset.New(2)},
		{idset.New(1), idset.New(1, 2), idset.New(2), nil},
		{idset.New(1, 2), idset.New(1, 3), idset.New(3), idset.New(2)},
		{idset.New(1, 2, 3), idset.New(2, 3, 4), idset.New(4), idset.New(1)},
		{idset.New(2, 3, 4), idset.New(1, 2, 3), idset.New(1), idset.New(4)},
	}
	for i, tc := range testcases {
		gotN, gotO := tc.in1.Diff(tc.in2)
		if !tc.exp1.Equal(gotN) {
			t.Errorf("%d: expected %v, but got: %v", i, tc.exp1, gotN)
		}
		if !tc.exp2.Equal(gotO) {
			t.Errorf("%d: expected %v, but got: %v", i, tc.exp2, gotO)
		}
	}
}

func TestSetRemove(t *testing.T) {
	t.Parallel()
	testcases := []struct {
		s1, s2 *idset.Set
		exp    []id.Zid
	}{
		{nil, nil, nil},
		{idset.New(), nil, nil},
		{idset.New(), idset.New(), nil},
		{idset.New(1), nil, []id.Zid{1}},
		{idset.New(1), idset.New(), []id.Zid{1}},
		{idset.New(1), idset.New(2), []id.Zid{1}},
		{idset.New(1), idset.New(1), []id.Zid{}},
	}
	for i, tc := range testcases {
		sl1 := tc.s1.SafeSorted()
		sl2 := tc.s2.SafeSorted()
		newS1 := idset.New(sl1...)
		newS1.ISubstract(tc.s2)
		got := newS1.SafeSorted()
		if !slices.Equal(got, tc.exp) {
			t.Errorf("%d: %v.Remove(%v) should be %v, but got %v", i, sl1, sl2, tc.exp, got)
		}
	}
}

func BenchmarkSet(b *testing.B) {
	s := idset.NewCap(b.N)
	for i := range b.N {
		s.Add(id.Zid(i))
	}
}

Added 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
102
103
104
105
106
107
108
109
110
111
112
113
114
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
//-----------------------------------------------------------------------------
// Copyright (c) 2022-present Detlef Stern
//
// 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.
//
// SPDX-License-Identifier: EUPL-1.2
// SPDX-FileCopyrightText: 2022-present Detlef Stern
//-----------------------------------------------------------------------------

package meta

import (
	"slices"
	"strings"
)

// 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
	}
	if descr.IsSet {
		return createSetArrangement(metaList, key)
	}
	return createSimplearrangement(metaList, key)
}

func createSetArrangement(metaList []*Meta, key string) Arrangement {
	a := make(Arrangement)
	for _, m := range metaList {
		for val := range m.GetFields(key) {
			a[val] = append(a[val], m)
		}
	}
	return a
}

func createSimplearrangement(metaList []*Meta, key string) Arrangement {
	a := make(Arrangement)
	for _, m := range metaList {
		if val, ok := m.Get(key); ok && val != "" {
			a[string(val)] = append(a[string(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() {
	slices.SortFunc(ccs, func(i, j CountedCategory) int { return strings.Compare(i.Name, 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() {
	slices.SortFunc(ccs, func(i, j CountedCategory) int {
		iCount, jCount := i.Count, j.Count
		if iCount > jCount {
			return -1
		}
		if iCount == jCount {
			return strings.Compare(i.Name, j.Name)
		}
		return 1
	})
}

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

Added domain/meta/meta.go.











































































































































































































































































































































































































































































1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
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
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
//-----------------------------------------------------------------------------
// Copyright (c) 2020-present Detlef Stern
//
// 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.
//
// SPDX-License-Identifier: EUPL-1.2
// SPDX-FileCopyrightText: 2020-present Detlef Stern
//-----------------------------------------------------------------------------

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

import (
	"iter"
	"maps"
	"regexp"
	"slices"
	"strings"
	"unicode"
	"unicode/utf8"

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

type keyUsage int

const (
	_             keyUsage = iota
	usageUser              // Key will be manipulated by the user
	usageComputed          // Key is computed by zettelstore
	usageProperty          // Key is computed and not stored by zettelstore
)

// DescriptionKey formally describes each supported metadata key.
type DescriptionKey struct {
	Name    string
	Type    *DescriptionType
	usage   keyUsage
	Inverse string
}

// IsComputed returns true, if metadata is computed and not set by the user.
func (kd *DescriptionKey) IsComputed() bool { return kd.usage >= usageComputed }

// IsProperty returns true, if metadata is a computed property.
func (kd *DescriptionKey) IsProperty() bool { return kd.usage >= usageProperty }

var registeredKeys = make(map[string]*DescriptionKey)

func registerKey(name string, t *DescriptionType, usage keyUsage, inverse string) {
	if _, ok := registeredKeys[name]; ok {
		panic("Key '" + name + "' already defined")
	}
	if inverse != "" {
		if t != TypeID && t != TypeIDSet {
			panic("Inversable key '" + name + "' is not identifier type, but " + t.String())
		}
		inv, ok := registeredKeys[inverse]
		if !ok {
			panic("Inverse Key '" + inverse + "' not found")
		}
		if !inv.IsComputed() {
			panic("Inverse Key '" + inverse + "' is not computed.")
		}
		if inv.Type != TypeIDSet {
			panic("Inverse Key '" + inverse + "' is not an identifier set, but " + inv.Type.String())
		}
	}
	registeredKeys[name] = &DescriptionKey{name, t, usage, inverse}
}

// IsComputed returns true, if key denotes a computed metadata key.
func IsComputed(name string) bool {
	if kd, ok := registeredKeys[name]; ok {
		return kd.IsComputed()
	}
	return false
}

// IsProperty returns true, if key denotes a property metadata value.
func IsProperty(name string) bool {
	if kd, ok := registeredKeys[name]; ok {
		return kd.IsProperty()
	}
	return false
}

// Inverse returns the name of the inverse key.
func Inverse(name string) string {
	if kd, ok := registeredKeys[name]; ok {
		return kd.Inverse
	}
	return ""
}

// GetDescription returns the key description object of the given key name.
func GetDescription(name string) DescriptionKey {
	if d, ok := registeredKeys[name]; ok {
		return *d
	}
	return DescriptionKey{Type: Type(name)}
}

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

// Key is the type of metadata keys.
type Key = string

// Predefined / supported metadata keys.
//
// See [Supported Metadata Keys].
//
// [Supported Metadata Keys]: https://zettelstore.de/manual/h/00001006020000
const (
	KeyID           = "id"
	KeyTitle        = "title"
	KeyRole         = "role"
	KeyTags         = "tags"
	KeySyntax       = "syntax"
	KeyAuthor       = "author"
	KeyBack         = "back"
	KeyBackward     = "backward"
	KeyBoxNumber    = "box-number"
	KeyCopyright    = "copyright"
	KeyCreated      = "created"
	KeyCredential   = "credential"
	KeyDead         = "dead"
	KeyExpire       = "expire"
	KeyFolge        = "folge"
	KeyFolgeRole    = "folge-role"
	KeyForward      = "forward"
	KeyLang         = "lang"
	KeyLicense      = "license"
	KeyModified     = "modified"
	KeyPrecursor    = "precursor"
	KeyPredecessor  = "predecessor"
	KeyPrequel      = "prequel"
	KeyPublished    = "published"
	KeyQuery        = "query"
	KeyReadOnly     = "read-only"
	KeySequel       = "sequel"
	KeySubordinates = "subordinates"
	KeySuccessors   = "successors"
	KeySummary      = "summary"
	KeySuperior     = "superior"
	KeyURL          = "url"
	KeyUselessFiles = "useless-files"
	KeyUserID       = "user-id"
	KeyUserRole     = "user-role"
	KeyVisibility   = "visibility"
)

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

	// Properties that are inverse keys
	registerKey(KeyFolge, TypeIDSet, usageProperty, "")
	registerKey(KeySequel, TypeIDSet, usageProperty, "")
	registerKey(KeySuccessors, TypeIDSet, usageProperty, "")
	registerKey(KeySubordinates, TypeIDSet, usageProperty, "")

	// Non-inverse keys
	registerKey(KeyAuthor, TypeString, usageUser, "")
	registerKey(KeyBack, TypeIDSet, usageProperty, "")
	registerKey(KeyBackward, TypeIDSet, usageProperty, "")
	registerKey(KeyBoxNumber, TypeNumber, usageProperty, "")
	registerKey(KeyCopyright, TypeString, usageUser, "")
	registerKey(KeyCreated, TypeTimestamp, usageComputed, "")
	registerKey(KeyCredential, TypeCredential, usageUser, "")
	registerKey(KeyDead, TypeIDSet, usageProperty, "")
	registerKey(KeyExpire, TypeTimestamp, usageUser, "")
	registerKey(KeyFolgeRole, TypeWord, usageUser, "")
	registerKey(KeyForward, TypeIDSet, usageProperty, "")
	registerKey(KeyLang, TypeWord, usageUser, "")
	registerKey(KeyLicense, TypeEmpty, usageUser, "")
	registerKey(KeyModified, TypeTimestamp, usageComputed, "")
	registerKey(KeyPrecursor, TypeIDSet, usageUser, KeyFolge)
	registerKey(KeyPredecessor, TypeID, usageUser, KeySuccessors)
	registerKey(KeyPrequel, TypeIDSet, usageUser, KeySequel)
	registerKey(KeyPublished, TypeTimestamp, usageProperty, "")
	registerKey(KeyQuery, TypeEmpty, usageUser, "")
	registerKey(KeyReadOnly, TypeWord, usageUser, "")
	registerKey(KeySummary, TypeString, usageUser, "")
	registerKey(KeySuperior, TypeIDSet, usageUser, KeySubordinates)
	registerKey(KeyURL, TypeURL, usageUser, "")
	registerKey(KeyUselessFiles, TypeString, usageProperty, "")
	registerKey(KeyUserID, TypeWord, usageUser, "")
	registerKey(KeyUserRole, TypeWord, usageUser, "")
	registerKey(KeyVisibility, TypeWord, usageUser, "")
}

// NewPrefix is the prefix for metadata keys in template zettel for creating new zettel.
const NewPrefix = "new-"

// Meta contains all meta-data of a zettel.
type Meta struct {
	Zid     id.Zid
	pairs   map[Key]Value
	YamlSep bool
}

// New creates a new chunk for storing metadata.
func New(zid id.Zid) *Meta {
	return &Meta{Zid: zid, pairs: make(map[Key]Value, 5)}
}

// NewWithData creates metadata object with given data.
func NewWithData(zid id.Zid, data map[string]string) *Meta {
	pairs := make(map[Key]Value, len(data))
	for k, v := range data {
		pairs[k] = Value(v)
	}
	return &Meta{Zid: zid, pairs: pairs}
}

// ByteSize returns the number of bytes stored for the metadata.
func (m *Meta) ByteSize() int {
	if m == nil {
		return 0
	}
	result := 6 // storage needed for Zid
	for k, v := range m.pairs {
		result += len(k) + len(v) + 1 // 1 because separator
	}
	return result
}

// Clone returns a new copy of the metadata.
func (m *Meta) Clone() *Meta {
	return &Meta{
		Zid:     m.Zid,
		pairs:   maps.Clone(m.pairs),
		YamlSep: m.YamlSep,
	}
}

// Map returns a copy of the meta data as a string map.
func (m *Meta) Map() map[string]string {
	pairs := make(map[string]string, len(m.pairs))
	for k, v := range m.pairs {
		pairs[k] = string(v)
	}
	return pairs
}

var reKey = regexp.MustCompile("^[0-9a-z][-0-9a-z]{0,254}$")

// KeyIsValid returns true, if the string is a valid metadata key.
func KeyIsValid(s string) bool { return reKey.MatchString(s) }

var firstKeys = []string{KeyTitle, KeyRole, KeyTags, KeySyntax}

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

// 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 string, value Value) {
	if value == "" {
		delete(m.pairs, key) // TODO: key != KeyID
	} else {
		m.Set(key, value.TrimSpace())
	}
}

// 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) (Value, bool) {
	if m == nil {
		return "", false
	}
	if key == KeyID {
		return Value(m.Zid.String()), true
	}
	value, ok := m.pairs[key]
	return value, ok
}

// GetDefault retrieves the string value of the given key. If no value was
// stored, the given default value is returned.
func (m *Meta) GetDefault(key string, def Value) Value {
	if value, found := m.Get(key); found {
		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(KeyTitle); found {
		return string(title)
	}
	return m.Zid.String()
}

// All returns an iterator over all key/value pairs, except the zettel identifier
// and computed values.
func (m *Meta) All() iter.Seq2[Key, Value] {
	return func(yield func(Key, Value) bool) {
		m.firstKeys()(yield)
		m.restKeys(notComputedKey)(yield)
	}
}

// Computed returns an iterator over all key/value pairs, except the zettel identifier.
func (m *Meta) Computed() iter.Seq2[Key, Value] {
	return func(yield func(Key, Value) bool) {
		m.firstKeys()(yield)
		m.restKeys(anyKey)(yield)
	}
}

// Rest returns an iterator over all key/value pairs, except the zettel identifier,
// the main keys, and computed values.
func (m *Meta) Rest() iter.Seq2[Key, Value] {
	return func(yield func(Key, Value) bool) {
		m.restKeys(notComputedKey)(yield)
	}
}

// ComputedRest returns an iterator over all key/value pairs, except the zettel identifier,
// and the main keys.
func (m *Meta) ComputedRest() iter.Seq2[Key, Value] {
	return func(yield func(Key, Value) bool) {
		m.restKeys(anyKey)(yield)
	}
}

func (m *Meta) firstKeys() iter.Seq2[Key, Value] {
	return func(yield func(Key, Value) bool) {
		for _, key := range firstKeys {
			if val, ok := m.pairs[key]; ok {
				if !yield(key, val) {
					return
				}
			}
		}
	}
}

func (m *Meta) restKeys(addKeyPred func(Key) bool) iter.Seq2[Key, Value] {
	return func(yield func(Key, Value) bool) {
		keys := slices.Sorted(maps.Keys(m.pairs))
		for _, key := range keys {
			if !slices.Contains(firstKeys, key) && addKeyPred(key) {
				if !yield(key, m.pairs[key]) {
					return
				}
			}
		}
	}
}

func notComputedKey(key string) bool { return !IsComputed(key) }
func anyKey(string) bool             { return true }

// Delete removes a key from the data.
func (m *Meta) Delete(key string) {
	if key != KeyID {
		delete(m.pairs, key)
	}
}

// Equal compares to metas for equality.
func (m *Meta) Equal(o *Meta, allowComputed bool) bool {
	if m == nil && o == nil {
		return true
	}
	if m == nil || o == nil || m.Zid != o.Zid {
		return false
	}
	tested := set.New[string]()
	for k, v := range m.pairs {
		tested.Add(k)
		if !equalValue(k, v, o, allowComputed) {
			return false
		}
	}
	for k, v := range o.pairs {
		if !tested.Contains(k) && !equalValue(k, v, m, allowComputed) {
			return false
		}
	}
	return true
}

func equalValue(key string, val Value, other *Meta, allowComputed bool) bool {
	if allowComputed || !IsComputed(key) {
		if valO, found := other.pairs[key]; !found || val != valO {
			return false
		}
	}
	return true
}

// Sanitize all metadata keys and values, so that they can be written safely into a file.
func (m *Meta) Sanitize() {
	if m == nil {
		return
	}
	for key, val := range m.pairs {
		newKey := RemoveNonGraphic(key)
		if key == newKey {
			m.pairs[key] = Value(RemoveNonGraphic(string(val)))
		} else {
			delete(m.pairs, key)
			m.pairs[newKey] = Value(RemoveNonGraphic(string(val)))
		}
	}
}

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

Added domain/meta/meta_test.go.











































































































































































































































































1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
//-----------------------------------------------------------------------------
// Copyright (c) 2020-present Detlef Stern
//
// 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.
//
// SPDX-License-Identifier: EUPL-1.2
// SPDX-FileCopyrightText: 2020-present Detlef Stern
//-----------------------------------------------------------------------------

package meta

import (
	"iter"
	"slices"
	"strings"
	"testing"

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

const testID = id.Zid(98765432101234)

func TestKeyIsValid(t *testing.T) {
	t.Parallel()
	validKeys := []string{"0", "a", "0-", "title", "title-----", strings.Repeat("r", 255)}
	for _, key := range validKeys {
		if !KeyIsValid(key) {
			t.Errorf("Key %q wrongly identified as invalid key", key)
		}
	}
	invalidKeys := []string{"", "-", "-a", "Title", "a_b", strings.Repeat("e", 256)}
	for _, key := range invalidKeys {
		if KeyIsValid(key) {
			t.Errorf("Key %q wrongly identified as valid key", key)
		}
	}
}

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

	m = New(testID)
	const at = "A Title"
	addToMeta(m, KeyTitle, at)
	addToMeta(m, KeyTitle, " ")
	if got, ok := m.Get(KeyTitle); !ok || got != at {
		t.Errorf("Title is not %q, but %q", at, got)
	}
}

func checkTags(t *testing.T, exp []string, m *Meta) {
	t.Helper()
	got := slices.Collect(m.GetFields(KeyTags))
	for i, tag := range exp {
		if i < len(got) {
			if tag != got[i] {
				t.Errorf("Pos=%d, expected %q, got %q", i, exp[i], got[i])
			}
		} else {
			t.Errorf("Expected %q, but is missing", exp[i])
		}
	}
	if len(exp) < len(got) {
		t.Errorf("Extra tags: %q", got[len(exp):])
	}
}

func TestTagsHeader(t *testing.T) {
	t.Parallel()
	m := New(testID)
	checkTags(t, []string{}, m)

	addToMeta(m, KeyTags, "")
	checkTags(t, []string{}, m)

	addToMeta(m, KeyTags, "  #t1 #t2  #t3 #t4  ")
	checkTags(t, []string{"#t1", "#t2", "#t3", "#t4"}, m)

	addToMeta(m, KeyTags, "#t5")
	checkTags(t, []string{"#t1", "#t2", "#t3", "#t4", "#t5"}, m)

	addToMeta(m, KeyTags, "t6")
	checkTags(t, []string{"#t1", "#t2", "#t3", "#t4", "#t5"}, m)
}

func TestSyntax(t *testing.T) {
	t.Parallel()
	m := New(testID)
	if got, ok := m.Get(KeySyntax); ok || got != "" {
		t.Errorf("Syntax is not %q, but %q", "", got)
	}
	addToMeta(m, KeySyntax, " ")
	if got, _ := m.Get(KeySyntax); got != "" {
		t.Errorf("Syntax is not %q, but %q", "", got)
	}
	addToMeta(m, KeySyntax, "MarkDown")
	const exp = "markdown"
	if got, ok := m.Get(KeySyntax); !ok || got != exp {
		t.Errorf("Syntax is not %q, but %q", exp, got)
	}
	addToMeta(m, KeySyntax, " ")
	if got, _ := m.Get(KeySyntax); got != "" {
		t.Errorf("Syntax is not %q, but %q", "", got)
	}
}

func checkHeader(t *testing.T, exp map[string]string, gotI iter.Seq2[Key, Value]) {
	t.Helper()
	got := make(map[string]string)
	gotI(func(key Key, val Value) bool {
		got[key] = string(val)
		if _, ok := exp[key]; !ok {
			t.Errorf("Key %q is not expected, but has value %q", key, val)
		}
		return true
	})
	for k, v := range exp {
		if gv, ok := got[k]; !ok || v != gv {
			if ok {
				t.Errorf("Key %q is not %q, but %q", k, v, got[k])
			} else {
				t.Errorf("Key %q missing, should have value %q", k, v)
			}
		}
	}
}

func TestDefaultHeader(t *testing.T) {
	t.Parallel()
	m := New(testID)
	addToMeta(m, "h1", "d1")
	addToMeta(m, "H2", "D2")
	addToMeta(m, "H1", "D1.1")
	exp := map[string]string{"h1": "d1 D1.1", "h2": "D2"}
	checkHeader(t, exp, m.All())
	addToMeta(m, "", "d0")
	checkHeader(t, exp, m.All())
	addToMeta(m, "h3", "")
	exp["h3"] = ""
	checkHeader(t, exp, m.All())
	addToMeta(m, "h3", "  ")
	checkHeader(t, exp, m.All())
	addToMeta(m, "h4", " ")
	exp["h4"] = ""
	checkHeader(t, exp, m.All())
}

func TestDelete(t *testing.T) {
	t.Parallel()
	m := New(testID)
	m.Set("key", "val")
	if got, ok := m.Get("key"); !ok || got != "val" {
		t.Errorf("Value != %q, got: %v/%q", "val", ok, got)
	}
	m.Set("key", "")
	if got, ok := m.Get("key"); !ok || got != "" {
		t.Errorf("Value != %q, got: %v/%q", "", ok, got)
	}
	m.Delete("key")
	if got, ok := m.Get("key"); ok || got != "" {
		t.Errorf("Value != %q, got: %v/%q", "", ok, got)
	}
}

func TestEqual(t *testing.T) {
	t.Parallel()
	testcases := []struct {
		pairs1, pairs2 []string
		allowComputed  bool
		exp            bool
	}{
		{nil, nil, true, true},
		{nil, nil, false, true},
		{[]string{"a", "a"}, nil, false, false},
		{[]string{"a", "a"}, nil, true, false},
		{[]string{KeyFolge, "0"}, nil, true, false},
		{[]string{KeyFolge, "0"}, nil, false, true},
		{[]string{KeyFolge, "0"}, []string{KeyFolge, "0"}, true, true},
		{[]string{KeyFolge, "0"}, []string{KeyFolge, "0"}, false, true},
	}
	for i, tc := range testcases {
		m1 := pairs2meta(tc.pairs1)
		m2 := pairs2meta(tc.pairs2)
		got := m1.Equal(m2, tc.allowComputed)
		if tc.exp != got {
			t.Errorf("%d: %v =?= %v: expected=%v, but got=%v", i, tc.pairs1, tc.pairs2, tc.exp, got)
		}
		got = m2.Equal(m1, tc.allowComputed)
		if tc.exp != got {
			t.Errorf("%d: %v =!= %v: expected=%v, but got=%v", i, tc.pairs1, tc.pairs2, tc.exp, got)
		}
	}

	// Pathologic cases
	var m1, m2 *Meta
	if !m1.Equal(m2, true) {
		t.Error("Nil metas should be treated equal")
	}
	m1 = New(testID)
	if m1.Equal(m2, true) {
		t.Error("Empty meta should not be equal to nil")
	}
	if m2.Equal(m1, true) {
		t.Error("Nil meta should should not be equal to empty")
	}
	m2 = New(testID + 1)
	if m1.Equal(m2, true) {
		t.Error("Different ID should differentiate")
	}
	if m2.Equal(m1, true) {
		t.Error("Different ID should differentiate")
	}
}

func pairs2meta(pairs []string) *Meta {
	m := New(testID)
	for i := 0; i < len(pairs); i += 2 {
		m.Set(pairs[i], Value(pairs[i+1]))
	}
	return m
}

func TestRemoveNonGraphic(t *testing.T) {
	testCases := []struct {
		inp string
		exp string
	}{
		{"", ""},
		{" ", ""},
		{"a", "a"},
		{"a ", "a"},
		{"a b", "a b"},
		{"\n", ""},
		{"a\n", "a"},
		{"a\nb", "a b"},
		{"a\tb", "a b"},
	}
	for i, tc := range testCases {
		got := RemoveNonGraphic(tc.inp)
		if tc.exp != got {
			t.Errorf("%q/%d: expected %q, but got %q", tc.inp, i, tc.exp, got)
		}
	}
}

Added domain/meta/parse.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
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
//-----------------------------------------------------------------------------
// Copyright (c) 2020-present Detlef Stern
//
// 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.
//
// SPDX-License-Identifier: EUPL-1.2
// SPDX-FileCopyrightText: 2020-present Detlef Stern
//-----------------------------------------------------------------------------

package meta

import (
	"iter"
	"slices"
	"strings"

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

// NewFromInput parses the meta data of a zettel.
func NewFromInput(zid id.Zid, inp *input.Input) *Meta {
	if inp.Ch == '-' && inp.PeekN(0) == '-' && inp.PeekN(1) == '-' {
		skipToEOL(inp)
		inp.EatEOL()
	}
	meta := New(zid)
	for {
		inp.SkipSpace()
		switch inp.Ch {
		case '\r':
			if inp.Peek() == '\n' {
				inp.Next()
			}
			fallthrough
		case '\n':
			inp.Next()
			return meta
		case input.EOS:
			return meta
		case '%':
			skipToEOL(inp)
			inp.EatEOL()
			continue
		}
		parseHeader(meta, inp)
		if inp.Ch == '-' && inp.PeekN(0) == '-' && inp.PeekN(1) == '-' {
			skipToEOL(inp)
			inp.EatEOL()
			meta.YamlSep = true
			return meta
		}
	}
}

func parseHeader(m *Meta, inp *input.Input) {
	pos := inp.Pos
	for isHeader(inp.Ch) {
		inp.Next()
	}
	key := inp.Src[pos:inp.Pos]
	inp.SkipSpace()
	if inp.Ch == ':' {
		inp.Next()
	}
	var val []byte
	for {
		inp.SkipSpace()
		pos = inp.Pos
		skipToEOL(inp)
		val = append(val, inp.Src[pos:inp.Pos]...)
		inp.EatEOL()
		if !inp.IsSpace() {
			break
		}
		val = append(val, ' ')
	}
	addToMeta(m, string(key), Value(val))
}

func skipToEOL(inp *input.Input) {
	for {
		switch inp.Ch {
		case '\n', '\r', input.EOS:
			return
		}
		inp.Next()
	}
}

// Return true iff rune is valid for header key.
func isHeader(ch rune) bool {
	return ('a' <= ch && ch <= 'z') ||
		('0' <= ch && ch <= '9') ||
		ch == '-' ||
		('A' <= ch && ch <= 'Z')
}

type predValidElem func(string) bool

func addToSet(set *set.Set[string], it iter.Seq[string], useElem predValidElem) {
	for e := range it {
		if len(e) > 0 && useElem(e) {
			set.Add(e)
		}
	}
}

func addSet(m *Meta, key string, val Value, useElem predValidElem) {
	newElems := val.Fields()
	oldElems := m.GetFields(key)

	s := set.New[string]()
	addToSet(s, newElems, useElem)
	if s.Length() == 0 {
		// Nothing to add. Maybe because of rejected elements.
		return
	}
	addToSet(s, oldElems, useElem)
	m.SetList(key, slices.Sorted(s.Values()))
}

func addData(m *Meta, k string, v Value) {
	if o, ok := m.Get(k); !ok || o == "" {
		m.Set(k, v)
	} else if v != "" {
		m.Set(k, o+" "+v)
	}
}

func addToMeta(m *Meta, key string, val Value) {
	v := val.TrimSpace()
	key = strings.ToLower(key)
	if !KeyIsValid(key) {
		return
	}
	switch key {
	case "", KeyID:
		// Empty key and 'id' key will be ignored
		return
	}

	switch Type(key) {
	case TypeTagSet:
		addSet(m, key, v.ToLower(), func(s string) bool { return s[0] == '#' && len(s) > 1 })
	case TypeWord:
		m.Set(key, v.ToLower())
	case TypeID:
		if _, err := id.Parse(string(v)); err == nil {
			m.Set(key, v)
		}
	case TypeIDSet:
		addSet(m, key, v, func(s string) bool {
			_, err := id.Parse(s)
			return err == nil
		})
	case TypeTimestamp:
		if _, ok := v.AsTime(); ok {
			m.Set(key, v)
		}
	default:
		addData(m, key, v)
	}
}

Added domain/meta/parse_test.go.





























































































































































































1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
//-----------------------------------------------------------------------------
// Copyright (c) 2020-present Detlef Stern
//
// 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.
//
// SPDX-License-Identifier: EUPL-1.2
// SPDX-FileCopyrightText: 2020-present Detlef Stern
//-----------------------------------------------------------------------------

package meta_test

import (
	"iter"
	"slices"
	"strings"
	"testing"

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

func parseMetaStr(src string) *meta.Meta {
	return meta.NewFromInput(testID, input.NewInput([]byte(src)))
}

func TestEmpty(t *testing.T) {
	t.Parallel()
	m := parseMetaStr("")
	if got, ok := m.Get(meta.KeySyntax); ok || got != "" {
		t.Errorf("Syntax is not %q, but %q", "", got)
	}
	if got := slices.Collect(m.GetDefault(meta.KeyTags, "").Fields()); len(got) > 0 {
		t.Errorf("Tags are not nil, but %v", got)
	}
}

func TestTitle(t *testing.T) {
	t.Parallel()
	td := []struct {
		s string
		e meta.Value
	}{
		{meta.KeyTitle + ": a title", "a title"},
		{meta.KeyTitle + ": a\n\t title", "a title"},
		{meta.KeyTitle + ": a\n\t title\r\n  x", "a title x"},
		{meta.KeyTitle + " AbC", "AbC"},
		{meta.KeyTitle + " AbC\n ded", "AbC ded"},
		{meta.KeyTitle + ": o\ntitle: p", "o p"},
		{meta.KeyTitle + ": O\n\ntitle: P", "O"},
		{meta.KeyTitle + ": b\r\ntitle: c", "b c"},
		{meta.KeyTitle + ": B\r\n\r\ntitle: C", "B"},
		{meta.KeyTitle + ": r\rtitle: q", "r q"},
		{meta.KeyTitle + ": R\r\rtitle: Q", "R"},
	}
	for i, tc := range td {
		m := parseMetaStr(tc.s)
		if got, ok := m.Get(meta.KeyTitle); !ok || got != tc.e {
			t.Log(m)
			t.Errorf("TC=%d: expected %q, got %q", i, tc.e, got)
		}
	}
}

func TestTags(t *testing.T) {
	t.Parallel()
	testcases := []struct {
		src string
		exp string
	}{
		{"", ""},
		{meta.KeyTags + ":", ""},
		{meta.KeyTags + ": c", ""},
		{meta.KeyTags + ": #", ""},
		{meta.KeyTags + ": #c", "c"},
		{meta.KeyTags + ": #c #", "c"},
		{meta.KeyTags + ": #c #b", "b c"},
		{meta.KeyTags + ": #c # #", "c"},
		{meta.KeyTags + ": #c # #b", "b c"},
	}
	for i, tc := range testcases {
		m := parseMetaStr(tc.src)
		tagsString, found := m.Get(meta.KeyTags)
		if !found {
			if tc.exp != "" {
				t.Errorf("%d / %q: no %s found", i, tc.src, meta.KeyTags)
			}
			continue
		}
		tags := tagsString.AsTags()
		if tc.exp == "" && len(tags) > 0 {
			t.Errorf("%d / %q: expected no %s, but got %v", i, tc.src, meta.KeyTags, tags)
			continue
		}
		got := strings.Join(tags, " ")
		if tc.exp != got {
			t.Errorf("%d / %q: expected %q, got: %q", i, tc.src, tc.exp, got)
		}
	}
}

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

	// Test, whether input position is correct.
	inp := input.NewInput([]byte("---\na:b\n---\nX"))
	m := meta.NewFromInput(testID, inp)
	exp := []pair{{"a", "b"}}
	if got := iter2pairs(m.All()); !equalPairs(exp, got) {
		t.Errorf("Expected=%v, got=%v", exp, got)
	}
	expCh := 'X'
	if gotCh := inp.Ch; gotCh != expCh {
		t.Errorf("Expected=%v, got=%v", expCh, gotCh)
	}
}

type pair struct {
	key meta.Key
	val meta.Value
}

func iter2pairs(it iter.Seq2[meta.Key, meta.Value]) (result []pair) {
	it(func(key meta.Key, val meta.Value) bool {
		result = append(result, pair{key, val})
		return true
	})
	return result
}

func equalPairs(one, two []pair) bool {
	if len(one) != len(two) {
		return false
	}
	for i := range len(one) {
		if one[i].key != two[i].key || one[i].val != two[i].val {
			return false
		}
	}
	return true
}

func TestPrecursorIDSet(t *testing.T) {
	t.Parallel()
	var testdata = []struct {
		inp string
		exp meta.Value
	}{
		{"", ""},
		{"123", ""},
		{"12345678901234", "12345678901234"},
		{"123 12345678901234", "12345678901234"},
		{"12345678901234 123", "12345678901234"},
		{"01234567890123 123 12345678901234", "01234567890123 12345678901234"},
		{"12345678901234 01234567890123", "01234567890123 12345678901234"},
	}
	for i, tc := range testdata {
		m := parseMetaStr(meta.KeyPrecursor + ": " + tc.inp)
		if got, ok := m.Get(meta.KeyPrecursor); (!ok && tc.exp != "") || tc.exp != got {
			t.Errorf("TC=%d: expected %q, but got %q when parsing %q", i, tc.exp, got, tc.inp)
		}
	}
}

Added domain/meta/type.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
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
//-----------------------------------------------------------------------------
// Copyright (c) 2020-present Detlef Stern
//
// 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.
//
// SPDX-License-Identifier: EUPL-1.2
// SPDX-FileCopyrightText: 2020-present Detlef Stern
//-----------------------------------------------------------------------------

package meta

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

	zeroiter "t73f.de/r/zero/iter"
	"t73f.de/r/zsc/domain/id"
)

// DescriptionType is a description of a specific key type.
type DescriptionType struct {
	Name  string
	IsSet bool
}

// String returns the string representation of the given type
func (t DescriptionType) String() string { return t.Name }

var registeredTypes = make(map[string]*DescriptionType)

func registerType(name string, isSet bool) *DescriptionType {
	if _, ok := registeredTypes[name]; ok {
		panic("Type '" + name + "' already registered")
	}
	t := &DescriptionType{name, isSet}
	registeredTypes[name] = t
	return t
}

// Values of the metadata key/value types.
//
// See [Supported Key Types].
//
// [Supported Key Types]: https://zettelstore.de/manual/h/00001006030000
const (
	MetaCredential = "Credential"
	MetaEmpty      = "EString"
	MetaID         = "Identifier"
	MetaIDSet      = "IdentifierSet"
	MetaNumber     = "Number"
	MetaString     = "String"
	MetaTagSet     = "TagSet"
	MetaTimestamp  = "Timestamp"
	MetaURL        = "URL"
	MetaWord       = "Word"
)

// Supported key types.
var (
	TypeCredential = registerType(MetaCredential, false)
	TypeEmpty      = registerType(MetaEmpty, false)
	TypeID         = registerType(MetaID, false)
	TypeIDSet      = registerType(MetaIDSet, true)
	TypeNumber     = registerType(MetaNumber, false)
	TypeString     = registerType(MetaString, false)
	TypeTagSet     = registerType(MetaTagSet, true)
	TypeTimestamp  = registerType(MetaTimestamp, false)
	TypeURL        = registerType(MetaURL, false)
	TypeWord       = registerType(MetaWord, 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)
}

// Some constants for key suffixes that determine a type.
const (
	SuffixKeyRole = "-role"
	SuffixKeyURL  = "-url"
)

var (
	cachedTypedKeys = make(map[string]*DescriptionType)
	mxTypedKey      sync.RWMutex
	suffixTypes     = map[string]*DescriptionType{
		"-date":       TypeTimestamp,
		"-number":     TypeNumber,
		SuffixKeyRole: TypeWord,
		"-time":       TypeTimestamp,
		SuffixKeyURL:  TypeURL,
		"-zettel":     TypeID,
		"-zid":        TypeID,
		"-zids":       TypeIDSet,
	}
)

// Type returns a type hint for the given key. If no type hint is specified,
// TypeEmpty is returned.
func Type(key string) *DescriptionType {
	if k, ok := registeredKeys[key]; ok {
		return k.Type
	}
	mxTypedKey.RLock()
	k, found := cachedTypedKeys[key]
	mxTypedKey.RUnlock()
	if found {
		return k
	}

	for suffix, t := range suffixTypes {
		if strings.HasSuffix(key, suffix) {
			mxTypedKey.Lock()
			defer mxTypedKey.Unlock()
			// Double check to avoid races
			if _, found = cachedTypedKeys[key]; !found {
				cachedTypedKeys[key] = t
			}
			return t
		}
	}
	return TypeEmpty
}

// SetList stores the given string list value under the given key.
func (m *Meta) SetList(key string, values []string) {
	if key != KeyID {
		for i, val := range values {
			values[i] = string(Value(val).TrimSpace())
		}
		m.pairs[key] = Value(strings.Join(values, " "))
	}
}

// SetWord stores the given word under the given key.
func (m *Meta) SetWord(key, word string) {
	for val := range Value(word).Elems() {
		m.Set(key, val)
		return
	}
}

// SetNow stores the current timestamp under the given key.
func (m *Meta) SetNow(key string) {
	m.Set(key, Value(time.Now().Local().Format(id.TimestampLayout)))
}

// GetBool returns the boolean value of the given key.
func (m *Meta) GetBool(key string) bool {
	if val, ok := m.Get(key); ok {
		return val.AsBool()
	}
	return false
}

// GetFields returns the metadata value as a sequence of string. The bool value
// signals, whether there was a value stored or not.
func (m *Meta) GetFields(key Key) iter.Seq[string] {
	if val, ok := m.Get(key); ok {
		return val.Fields()
	}
	return zeroiter.EmptySeq[string]()
}

// GetNumber retrieves the numeric value of a given key.
func (m *Meta) GetNumber(key string, def int64) int64 {
	if value, ok := m.Get(key); ok {
		if num, err := strconv.ParseInt(string(value), 10, 64); err == nil {
			return num
		}
	}
	return def
}

Added domain/meta/type_test.go.
















































































1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
//-----------------------------------------------------------------------------
// Copyright (c) 2020-present Detlef Stern
//
// 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.
//
// SPDX-License-Identifier: EUPL-1.2
// SPDX-FileCopyrightText: 2020-present Detlef Stern
//-----------------------------------------------------------------------------

package meta_test

import (
	"strconv"
	"testing"
	"time"

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

func TestNow(t *testing.T) {
	t.Parallel()
	m := meta.New(id.Invalid)
	m.SetNow("key")
	val, ok := m.Get("key")
	if !ok {
		t.Error("Unable to get value of key")
	}
	if len(val) != 14 {
		t.Errorf("Value is not 14 digits long: %q", val)
	}
	if _, err := strconv.ParseInt(string(val), 10, 64); err != nil {
		t.Errorf("Unable to parse %q as an int64: %v", val, err)
	}
	if _, ok = val.AsTime(); !ok {
		t.Errorf("Unable to get time from value %q", val)
	}
}

func TestTimeValue(t *testing.T) {
	t.Parallel()
	testCases := []struct {
		value meta.Value
		valid bool
		exp   time.Time
	}{
		{"", false, time.Time{}},
		{"1", false, time.Time{}},
		{"00000000000000", false, time.Time{}},
		{"98765432109876", false, time.Time{}},
		{"20201221111905", true, time.Date(2020, time.December, 21, 11, 19, 5, 0, time.UTC)},
		{"2023", true, time.Date(2023, time.January, 1, 0, 0, 0, 0, time.UTC)},
		{"20231", false, time.Time{}},
		{"202310", true, time.Date(2023, time.October, 1, 0, 0, 0, 0, time.UTC)},
		{"2023103", false, time.Time{}},
		{"20231030", true, time.Date(2023, time.October, 30, 0, 0, 0, 0, time.UTC)},
		{"202310301", false, time.Time{}},
		{"2023103016", true, time.Date(2023, time.October, 30, 16, 0, 0, 0, time.UTC)},
		{"20231030165", false, time.Time{}},
		{"202310301654", true, time.Date(2023, time.October, 30, 16, 54, 0, 0, time.UTC)},
		{"2023103016541", false, time.Time{}},
		{"20231030165417", true, time.Date(2023, time.October, 30, 16, 54, 17, 0, time.UTC)},
		{"2023103916541700", false, time.Time{}},
	}
	for i, tc := range testCases {
		got, ok := tc.value.AsTime()
		if ok != tc.valid {
			t.Errorf("%d: parsing of %q should be %v, but got %v", i, tc.value, tc.valid, ok)
			continue
		}
		if got != tc.exp {
			t.Errorf("%d: parsing of %q should return %v, but got %v", i, tc.value, tc.exp, got)
		}
	}
}

Added domain/meta/values.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
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
//-----------------------------------------------------------------------------
// Copyright (c) 2020-present Detlef Stern
//
// 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.
//
// SPDX-License-Identifier: EUPL-1.2
// SPDX-FileCopyrightText: 2020-present Detlef Stern
//-----------------------------------------------------------------------------

package meta

import (
	"fmt"
	"iter"
	"slices"
	"strings"
	"time"

	zeroiter "t73f.de/r/zero/iter"
	"t73f.de/r/zsc/domain/id"
	"t73f.de/r/zsc/input"
)

// Value ist a single metadata value.
type Value string

// AsBool returns the value interpreted as a bool.
func (val Value) AsBool() bool {
	if len(val) > 0 {
		switch val[0] {
		case '0', 'f', 'F', 'n', 'N':
			return false
		}
	}
	return true
}

// AsTime returns the time value of the given value.
func (val Value) AsTime() (time.Time, bool) {
	if t, err := time.Parse(id.TimestampLayout, ExpandTimestamp(val)); err == nil {
		return t, true
	}
	return time.Time{}, false
}

// ExpandTimestamp makes a short-form timestamp larger.
func ExpandTimestamp(val Value) string {
	switch l := len(val); l {
	case 4: // YYYY
		return string(val) + "0101000000"
	case 6: // YYYYMM
		return string(val) + "01000000"
	case 8, 10, 12: // YYYYMMDD, YYYYMMDDhh, YYYYMMDDhhmm
		return string(val) + "000000"[:14-l]
	case 14: // YYYYMMDDhhmmss
		return string(val)
	default:
		if l > 14 {
			return string(val[:14])
		}
		return string(val)
	}
}

// Fields iterates over the value as a list/set of strings.
func (val Value) Fields() iter.Seq[string] {
	return strings.FieldsSeq(string(val))
}

// Elems iterates over the value as a list/set of values.
func (val Value) Elems() iter.Seq[Value] {
	return zeroiter.MapSeq(val.Fields(), func(s string) Value { return Value(s) })
}

// AsSlice transforms a value into a slice of strings.
func (val Value) AsSlice() []string {
	return strings.Fields(string(val))
}

// ToLower maps the value to lowercase runes.
func (val Value) ToLower() Value { return Value(strings.ToLower(string(val))) }

// TrimSpace removes all leading and remaining space from value
func (val Value) TrimSpace() Value {
	return Value(strings.TrimFunc(string(val), input.IsSpace))
}

// AsTags returns the value as a sequence of normalized tags.
func (val Value) AsTags() []string {
	return slices.Collect(zeroiter.MapSeq(
		val.Fields(),
		func(e string) string { return string(Value(e).ToLower().CleanTag()) }))
}

// CleanTag removes the number character ('#') from a tag value.
func (val Value) CleanTag() Value {
	if len(val) > 1 && val[0] == '#' {
		return val[1:]
	}
	return val
}

// NormalizeTag adds a missing prefix "#" to the tag
func (val Value) NormalizeTag() Value {
	if len(val) > 0 && val[0] == '#' {
		return val
	}
	return "#" + val
}

// Predefined metadata values.
const (
	ValueFalse             = "false"
	ValueTrue              = "true"
	ValueLangEN            = "en"            // Default for "lang"
	ValueRoleConfiguration = "configuration" // A role for internal zettel
	ValueRoleTag           = "tag"           // A role for tag zettel
	ValueRoleRole          = "role"          // A role for role zettel
	ValueRoleZettel        = "zettel"        // A role for zettel
	ValueSyntaxCSS         = "css"           // Syntax: CSS
	ValueSyntaxDraw        = "draw"          // Syntax: Drawing
	ValueSyntaxGif         = "gif"           // Syntax: GIF image
	ValueSyntaxHTML        = "html"          // Syntax: HTML
	ValueSyntaxJPEG        = "jpeg"          // Syntax: JPEG image
	ValueSyntaxJPG         = "jpg"           // Syntax: PEG image
	ValueSyntaxMarkdown    = "markdown"      // Syntax: Markdown / CommonMark
	ValueSyntaxMD          = "md"            // Syntax: Markdown / CommonMark
	ValueSyntaxNone        = "none"          // Syntax: no syntax / content, just metadata
	ValueSyntaxPlain       = "plain"         // Syntax: plain text
	ValueSyntaxPNG         = "png"           // Syntax: PNG image
	ValueSyntaxSVG         = "svg"           // Syntax: SVG
	ValueSyntaxSxn         = "sxn"           // Syntax: S-Expression
	ValueSyntaxText        = "text"          // Syntax: plain text
	ValueSyntaxTxt         = "txt"           // Syntax: plain text
	ValueSyntaxWebp        = "webp"          // Syntax: WEBP image
	ValueSyntaxZmk         = "zmk"           // Syntax: Zettelmarkup
	ValueUserRoleCreator   = "creator"
	ValueUserRoleOwner     = "owner"
	ValueUserRoleReader    = "reader"
	ValueUserRoleWriter    = "writer"
	ValueVisibilityCreator = "creator"
	ValueVisibilityExpert  = "expert"
	ValueVisibilityLogin   = "login"
	ValueVisibilityOwner   = "owner"
	ValueVisibilityPublic  = "public"
)

// DefaultSyntax is the default value for metadata 'syntax'.
const DefaultSyntax = ValueSyntaxPlain

// Visibility enumerates the variations of the 'visibility' meta key.
type Visibility int

// Supported values for visibility.
const (
	_ Visibility = iota
	VisibilityUnknown
	VisibilityPublic
	VisibilityCreator
	VisibilityLogin
	VisibilityOwner
	VisibilityExpert
)

var visMap = map[Value]Visibility{
	ValueVisibilityPublic:  VisibilityPublic,
	ValueVisibilityCreator: VisibilityCreator,
	ValueVisibilityLogin:   VisibilityLogin,
	ValueVisibilityOwner:   VisibilityOwner,
	ValueVisibilityExpert:  VisibilityExpert,
}
var revVisMap = map[Visibility]Value{}

func init() {
	for k, v := range visMap {
		revVisMap[v] = k
	}
}

// AsVisibility returns the visibility value of the given value string
func (val Value) AsVisibility() Visibility {
	if vis, ok := visMap[val]; ok {
		return vis
	}
	return VisibilityUnknown
}

func (v Visibility) String() string {
	if s, ok := revVisMap[v]; ok {
		return string(s)
	}
	return fmt.Sprintf("Unknown (%d)", v)
}

// UserRole enumerates the supported values of meta key 'user-role'.
type UserRole int

// Supported values for user roles.
const (
	_ UserRole = iota
	UserRoleUnknown
	UserRoleCreator
	UserRoleReader
	UserRoleWriter
	UserRoleOwner
)

var urMap = map[Value]UserRole{
	ValueUserRoleCreator: UserRoleCreator,
	ValueUserRoleReader:  UserRoleReader,
	ValueUserRoleWriter:  UserRoleWriter,
	ValueUserRoleOwner:   UserRoleOwner,
}

// AsUserRole role returns the user role of the given string.
func (val Value) AsUserRole() UserRole {
	if ur, ok := urMap[val]; ok {
		return ur
	}
	return UserRoleUnknown
}

Added domain/meta/write.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) 2020-present Detlef Stern
//
// 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.
//
// SPDX-License-Identifier: EUPL-1.2
// SPDX-FileCopyrightText: 2020-present Detlef Stern
//-----------------------------------------------------------------------------

package meta

import "io"

// Write writes metadata to a writer, excluding computed and propery values.
func (m *Meta) Write(w io.Writer) (int, error) {
	return m.doWrite(w, IsComputed)
}

// WriteComputed writes metadata to a writer, including computed values,
// but excluding property values.
func (m *Meta) WriteComputed(w io.Writer) (int, error) {
	return m.doWrite(w, IsProperty)
}

func (m *Meta) doWrite(w io.Writer, ignoreKeyPred func(string) bool) (length int, err error) {
	for key, val := range m.Computed() {
		if ignoreKeyPred(key) {
			continue
		}
		if err != nil {
			break
		}
		var l int
		l, err = io.WriteString(w, key)
		length += l
		if err == nil {
			l, err = w.Write(colonSpace)
			length += l
		}
		if err == nil {
			l, err = io.WriteString(w, string(val))
			length += l
		}
		if err == nil {
			l, err = w.Write(newline)
			length += l
		}
	}
	return length, err
}

var (
	colonSpace = []byte{':', ' '}
	newline    = []byte{'\n'}
)

Added domain/meta/write_test.go.




























































1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
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) 2020-present Detlef Stern
//
// 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.
//
// SPDX-License-Identifier: EUPL-1.2
// SPDX-FileCopyrightText: 2020-present Detlef Stern
//-----------------------------------------------------------------------------

package meta_test

import (
	"strings"
	"testing"

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

const testID = id.Zid(98765432101234)

func newMeta(title string, tags []string, syntax string) *meta.Meta {
	m := meta.New(testID)
	if title != "" {
		m.Set(meta.KeyTitle, meta.Value(title))
	}
	if tags != nil {
		m.Set(meta.KeyTags, meta.Value(strings.Join(tags, " ")))
	}
	if syntax != "" {
		m.Set(meta.KeySyntax, meta.Value(syntax))
	}
	return m
}
func assertWriteMeta(t *testing.T, m *meta.Meta, expected string) {
	t.Helper()
	var sb strings.Builder
	_, _ = m.Write(&sb)
	if got := sb.String(); got != expected {
		t.Errorf("\nExp: %q\ngot: %q", expected, got)
	}
}

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

	m := newMeta("TITLE", []string{"#t1", "#t2"}, "syntax")
	assertWriteMeta(t, m, "title: TITLE\ntags: #t1 #t2\nsyntax: syntax\n")

	m = newMeta("TITLE", nil, "")
	m.Set("user", "zettel")
	m.Set("auth", "basic")
	assertWriteMeta(t, m, "title: TITLE\nauth: basic\nuser: zettel\n")
}

Changes to go.mod.

1
2
3

4
5
6
7
8




9
1
2

3
4
5



6
7
8
9
10


-
+


-
-
-
+
+
+
+

module t73f.de/r/zsc

go 1.23
go 1.24

require (
	t73f.de/r/sx v0.0.0-20240814083626-4df0ec6454b5
	t73f.de/r/sxwebs v0.0.0-20241031144449-53c3b2ed1a6f
	t73f.de/r/webs v0.0.0-20241031141359-cd4f76a622cd
	t73f.de/r/sx v0.0.0-20250226205800-c12af029b6d3
	t73f.de/r/sxwebs v0.0.0-20250226210617-7bc3145c269b
	t73f.de/r/webs v0.0.0-20250226210341-4a531b8bfb18
	t73f.de/r/zero v0.0.0-20250226205915-c4194684acb7
)

Changes to go.sum.

1
2
3
4
5
6














1
2
3
4
5
6
7
8
-
-
-
-
-
-
+
+
+
+
+
+
+
+
t73f.de/r/sx v0.0.0-20240814083626-4df0ec6454b5 h1:ug4hohM6pK28M8Uo0o3+XvjBure2wfEtuCnHVIdqBZY=
t73f.de/r/sx v0.0.0-20240814083626-4df0ec6454b5/go.mod h1:VRvsWoBErPKvMieDMMk1hsh1tb9sA4ijEQWGw/TbtQ0=
t73f.de/r/sxwebs v0.0.0-20241031144449-53c3b2ed1a6f h1:VJ4S7YWy9tCJuFz5MckqUjjktPaf0kpnTkNBVRVXpo4=
t73f.de/r/sxwebs v0.0.0-20241031144449-53c3b2ed1a6f/go.mod h1:IaM+U+LvYTYeuiIS5cwZW6kcEpdwoKBYVCU7LZr4Sgk=
t73f.de/r/webs v0.0.0-20241031141359-cd4f76a622cd h1:+7cqJonXKDso+uPvsvOPl7BiLkhj8VQT/Has8qC5VIQ=
t73f.de/r/webs v0.0.0-20241031141359-cd4f76a622cd/go.mod h1:NSoOON8be62MfQZzlCApK27Jt2zhIa6Vrmo9RJ4tOnQ=
t73f.de/r/sx v0.0.0-20250226205800-c12af029b6d3 h1:Jek4x1Qp59SWXI1enWVTeP1wxcVO96FuBpJBnnwOY98=
t73f.de/r/sx v0.0.0-20250226205800-c12af029b6d3/go.mod h1:hzg05uSCMk3D/DWaL0pdlowfL2aWQeGIfD1S04vV+Xg=
t73f.de/r/sxwebs v0.0.0-20250226210617-7bc3145c269b h1:X+9mMDd3fKML5SPcQk4n28oDGFUwqjDiSmQrH2LHZwI=
t73f.de/r/sxwebs v0.0.0-20250226210617-7bc3145c269b/go.mod h1:p+3JCSzNm9e+Yyub0ODRiLDeKaGVYWvBKYANZaAWYIA=
t73f.de/r/webs v0.0.0-20250226210341-4a531b8bfb18 h1:p7rOFBzP6FE/aYN5MUfmGDrKP1H1IFs6v19T7hm7rXI=
t73f.de/r/webs v0.0.0-20250226210341-4a531b8bfb18/go.mod h1:zk92hSKB4iWyT290+163seNzu350TA9XLATC9kOldqo=
t73f.de/r/zero v0.0.0-20250226205915-c4194684acb7 h1:OuzHSfniY8UzLmo5zp1w23Kd9h7x9CSXP2jQ+kppeqU=
t73f.de/r/zero v0.0.0-20250226205915-c4194684acb7/go.mod h1:T1vFcHoymUQcr7+vENBkS1yryZRZ3YB8uRtnMy8yRBA=

Deleted maps/maps.go.

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






























-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
//-----------------------------------------------------------------------------
// Copyright (c) 2022-present Detlef Stern
//
// 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.
//
// SPDX-License-Identifier: EUPL-1.2
// SPDX-FileCopyrightText: 2022-present Detlef Stern
//-----------------------------------------------------------------------------

// Package maps provides utility functions to work with maps.
package maps

import "sort"

// Keys returns the sorted list of string keys of the given map.
func Keys[T any](m map[string]T) []string {
	if len(m) == 0 {
		return nil
	}
	result := make([]string, 0, len(m))
	for k := range m {
		result = append(result, k)
	}
	sort.Strings(result)
	return result
}

Deleted maps/maps_test.go.

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

















































-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
//-----------------------------------------------------------------------------
// Copyright (c) 2022-present Detlef Stern
//
// 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.
//
// SPDX-License-Identifier: EUPL-1.2
// SPDX-FileCopyrightText: 2022-present Detlef Stern
//-----------------------------------------------------------------------------

package maps_test

import (
	"testing"

	"t73f.de/r/zsc/maps"
)

func isSorted(seq []string) bool {
	for i := 1; i < len(seq); i++ {
		if seq[i] < seq[i-1] {
			return false
		}
	}
	return true
}

func TestKeys(t *testing.T) {
	testcases := []struct{ keys []string }{
		{nil}, {[]string{""}},
		{[]string{"z", "y", "a"}},
	}
	for i, tc := range testcases {
		m := make(map[string]struct{})
		for _, k := range tc.keys {
			m[k] = struct{}{}
		}
		got := maps.Keys(m)
		if len(got) != len(tc.keys) {
			t.Errorf("%d: wrong number of keys: exp %d, got %d", i, len(tc.keys), len(got))
		}
		if !isSorted(got) {
			t.Errorf("%d: keys not sorted: %v", i, got)
		}
	}
}

Changes to sexp/sexp.go.

105
106
107
108
109
110
111
112
113


114
115
116
117
118
119
120
105
106
107
108
109
110
111


112
113
114
115
116
117
118
119
120







-
-
+
+








// ParseMeta translates the given list to metadata.
func ParseMeta(pair *sx.Pair) (api.ZettelMeta, error) {
	if err := CheckSymbol(pair.Car(), "meta"); err != nil {
		return nil, err
	}
	res := api.ZettelMeta{}
	for node := pair.Tail(); node != nil; node = node.Tail() {
		mVals, err := ParseList(node.Car(), "ys")
	for obj := range pair.Tail().Values() {
		mVals, err := ParseList(obj, "ys")
		if err != nil {
			return nil, err
		}
		res[(mVals[0].(*sx.Symbol)).GetValue()] = mVals[1].(sx.String).GetValue()
	}
	return res, nil
}
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
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







+
+
+
+






+
+
+






+
-
+

+

-
+














+
+
+
+
+
+
+

















-
-
+
+
+
+
+







	if i64 < 0 && i64 >= int64(api.ZettelMaxRight) {
		return api.ZettelMaxRight, fmt.Errorf("invalid zettel right value: %v", i64)
	}
	return api.ZettelRights(i64), nil
}

// ParseList parses the given object as a proper list, based on a type specification.
//
// 'b' expects a boolean, 'i' an int64, 'o' any object, 'p' a pair, 's' a string,
// and 'y' expects a symbol. A 'r' as the last type spracification matches all
// remaining values, including a non existent object.
func ParseList(obj sx.Object, spec string) (sx.Vector, error) {
	pair, isPair := sx.GetPair(obj)
	if !isPair {
		return nil, fmt.Errorf("not a list: %T/%v", obj, obj)
	}
	if pair == nil {
		if spec == "r" {
			return sx.Vector{sx.Nil()}, nil
		}
		if spec == "" {
			return nil, nil
		}
		return nil, ErrElementsMissing
	}

	specLen := len(spec)
	result := make(sx.Vector, 0, len(spec))
	result := make(sx.Vector, 0, specLen)
	node, i := pair, 0
loop:
	for ; node != nil; i++ {
		if i >= len(spec) {
		if i >= specLen {
			return nil, ErrNoSpec
		}
		var val sx.Object
		var ok bool
		car := node.Car()
		switch spec[i] {
		case 'b':
			val, ok = sx.MakeBoolean(!sx.IsNil(car)), true
		case 'i':
			val, ok = car.(sx.Int64)
		case 'o':
			val, ok = car, true
		case 'p':
			val, ok = sx.GetPair(car)
		case 'r':
			if i < specLen-1 {
				return nil, fmt.Errorf("spec 'r' must be the last: %v", spec)
			}
			result = append(result, node)
			i++
			break loop
		case 's':
			val, ok = sx.GetString(car)
		case 'y':
			val, ok = sx.GetSymbol(car)
		default:
			return nil, fmt.Errorf("unknown spec '%c'", spec[i])
		}
		if !ok {
			return nil, fmt.Errorf("does not match spec '%v': %v", spec[i], car)
		}
		result = append(result, val)
		next, isNextPair := sx.GetPair(node.Cdr())
		if !isNextPair {
			return nil, sx.ErrImproper{Pair: pair}
		}
		node = next
	}
	if i < len(spec) {
		return nil, ErrElementsMissing
	if i < specLen {
		if lastSpec := specLen - 1; i < lastSpec || spec[lastSpec] != 'r' {
			return nil, ErrElementsMissing
		}
		result = append(result, sx.Nil())
	}
	return result, nil
}

// ErrElementsMissing is returned,
// if ParseList is called with a list smaller than the number of type specifications.
var ErrElementsMissing = errors.New("spec contains more data")

Changes to sexp/sexp_test.go.

50
51
52
53
54
55
56
































57
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







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

		t.Error(err)
	} else if len(elems) != 1 {
		t.Error("length == 1, but got: ", elems)
	} else {
		_ = elems[0].(sx.String)
	}

	if elems, err := sexp.ParseList(sx.Nil(), "r"); err != nil {
		t.Error(err)
	} else if len(elems) != 1 {
		t.Error("length == 1, but got: ", elems)
	} else if !sx.IsNil(elems[0]) {
		t.Error("must be nil, but got:", elems[0])
	}
	if elems, err := sexp.ParseList(sx.MakeList(sx.MakeString("a")), "r"); err != nil {
		t.Error(err)
	} else if len(elems) != 1 {
		t.Error("length == 1, but got: ", elems)
	} else if !elems[0].IsEqual(sx.MakeList(sx.MakeString("a"))) {
		t.Error("must be (\"a\"), but got:", elems[0])
	}
	if elems, err := sexp.ParseList(sx.MakeList(sx.MakeString("a")), "sr"); err != nil {
		t.Error(err)
	} else if len(elems) != 2 {
		t.Error("length == 2, but got: ", elems)
	} else if !elems[0].IsEqual(sx.MakeString("a")) {
		t.Error("0-th must be \"a\", but got:", elems[0])
	} else if !sx.IsNil(elems[1]) {
		t.Error("must be nil, but got:", elems[1])
	}
	if elems, err := sexp.ParseList(sx.MakeList(sx.MakeString("a"), sx.MakeString("b"), sx.MakeString("c")), "sr"); err != nil {
		t.Error(err)
	} else if len(elems) != 2 {
		t.Error("length == 2, but got: ", elems)
	} else if !elems[0].IsEqual(sx.MakeString("a")) {
		t.Error("0-th must be \"a\", but got:", elems[0])
	} else if !elems[1].IsEqual(sx.MakeList(sx.MakeString("b"), sx.MakeString("c"))) {
		t.Error("must be nil, but got:", elems[1])
	}
}

Changes to shtml/const.go.

15
16
17
18
19
20
21
22

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

22
23
24
25
26
27
28
29







-
+








import "t73f.de/r/sx"

// Symbols for HTML header tags
var (
	SymBody   = sx.MakeSymbol("body")
	SymHead   = sx.MakeSymbol("head")
	SymHtml   = sx.MakeSymbol("html")
	SymHTML   = sx.MakeSymbol("html")
	SymMeta   = sx.MakeSymbol("meta")
	SymScript = sx.MakeSymbol("script")
	SymTitle  = sx.MakeSymbol("title")
)

// Symbols for HTML body tags
var (
67
68
69
70
71
72
73
74

75
76
77
78
79
80
81
82
83
84
67
68
69
70
71
72
73

74
75
76
77
78
79
80
81
82
83
84







-
+










)

// Symbols for HTML attribute keys
var (
	symAttrAlt    = sx.MakeSymbol("alt")
	SymAttrClass  = sx.MakeSymbol("class")
	SymAttrHref   = sx.MakeSymbol("href")
	SymAttrId     = sx.MakeSymbol("id")
	SymAttrID     = sx.MakeSymbol("id")
	SymAttrLang   = sx.MakeSymbol("lang")
	SymAttrOpen   = sx.MakeSymbol("open")
	SymAttrRel    = sx.MakeSymbol("rel")
	SymAttrRole   = sx.MakeSymbol("role")
	SymAttrSrc    = sx.MakeSymbol("src")
	SymAttrTarget = sx.MakeSymbol("target")
	SymAttrTitle  = sx.MakeSymbol("title")
	SymAttrType   = sx.MakeSymbol("type")
	SymAttrValue  = sx.MakeSymbol("value")
)

Changes to shtml/lang.go.

12
13
14
15
16
17
18
19

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

19
20
21
22
23
24
25
26







-
+







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

package shtml

import (
	"strings"

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

// LangStack is a stack to store the nesting of "lang" attribute values.
// It is used to generate typographically correct quotes.
type LangStack []string

// NewLangStack creates a new language stack.
81
82
83
84
85
86
87
88
89
90
91




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




88
89
90
91
92
93
94
95
96
97
98







-
-
-
-
+
+
+
+







}

// GetNBSp returns true, if there must be a non-breaking space between the
// quote entities and the quoted text.
func (qi *QuoteInfo) GetNBSp() bool { return qi.nbsp }

var langQuotes = map[string]*QuoteInfo{
	"":              {"&quot;", "&quot;", "&quot;", "&quot;", false},
	api.ValueLangEN: {"&ldquo;", "&rdquo;", "&lsquo;", "&rsquo;", false},
	"de":            {"&bdquo;", "&ldquo;", "&sbquo;", "&lsquo;", false},
	"fr":            {"&laquo;", "&raquo;", "&lsaquo;", "&rsaquo;", true},
	"":               {"&quot;", "&quot;", "&quot;", "&quot;", false},
	meta.ValueLangEN: {"&ldquo;", "&rdquo;", "&lsquo;", "&rsquo;", false},
	"de":             {"&bdquo;", "&ldquo;", "&sbquo;", "&lsquo;", false},
	"fr":             {"&laquo;", "&raquo;", "&lsaquo;", "&rsaquo;", true},
}

// GetQuoteInfo returns language specific data about quotes.
func GetQuoteInfo(lang string) *QuoteInfo {
	langFields := strings.FieldsFunc(lang, func(r rune) bool { return r == '-' || r == '_' })
	for len(langFields) > 0 {
		langSup := strings.Join(langFields, "-")

Changes to shtml/shtml.go.

20
21
22
23
24
25
26
27
28


29
30
31
32
33
34
35
20
21
22
23
24
25
26


27
28
29
30
31
32
33
34
35







-
-
+
+







	"strconv"
	"strings"

	"t73f.de/r/sx"
	"t73f.de/r/sxwebs/sxhtml"
	"t73f.de/r/zsc/api"
	"t73f.de/r/zsc/attrs"
	"t73f.de/r/zsc/sz"
	"t73f.de/r/zsc/text"
	"t73f.de/r/zsc/domain/meta"
	"t73f.de/r/zsc/sz"
)

// Evaluator will transform a s-expression that encodes the zettel AST into an s-expression
// that represents HTML.
type Evaluator struct {
	headingOffset int64
	unique        string
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
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







-
-
+
+
+
+




-
+












-
+
-

-
+
-







// Endnotes returns a SHTML object with all collected endnotes.
func Endnotes(env *Environment) *sx.Pair {
	if env.err != nil || len(env.endnotes) == 0 {
		return nil
	}

	var result sx.ListBuilder
	result.Add(SymOL)
	result.Add(sx.Nil().Cons(sx.Cons(SymAttrClass, sx.MakeString("zs-endnotes"))).Cons(sxhtml.SymAttr))
	result.AddN(
		SymOL,
		sx.Nil().Cons(sx.Cons(SymAttrClass, sx.MakeString("zs-endnotes"))).Cons(sxhtml.SymAttr),
	)
	for i, fni := range env.endnotes {
		noteNum := strconv.Itoa(i + 1)
		attrs := fni.attrs.Cons(sx.Cons(SymAttrClass, sx.MakeString("zs-endnote"))).
			Cons(sx.Cons(SymAttrValue, sx.MakeString(noteNum))).
			Cons(sx.Cons(SymAttrId, sx.MakeString("fn:"+fni.noteID))).
			Cons(sx.Cons(SymAttrID, sx.MakeString("fn:"+fni.noteID))).
			Cons(sx.Cons(SymAttrRole, sx.MakeString("doc-endnote"))).
			Cons(sxhtml.SymAttr)

		backref := sx.Nil().Cons(sx.MakeString("\u21a9\ufe0e")).
			Cons(sx.Nil().
				Cons(sx.Cons(SymAttrClass, sx.MakeString("zs-endnote-backref"))).
				Cons(sx.Cons(SymAttrHref, sx.MakeString("#fnref:"+fni.noteID))).
				Cons(sx.Cons(SymAttrRole, sx.MakeString("doc-backlink"))).
				Cons(sxhtml.SymAttr)).
			Cons(SymA)

		var li sx.ListBuilder
		li.Add(SymLI)
		li.AddN(SymLI, attrs)
		li.Add(attrs)
		li.ExtendBang(fni.noteHx)
		li.Add(sx.MakeString(" "))
		li.AddN(sx.MakeString(" "), backref)
		li.Add(backref)
		result.Add(li.List())
	}
	return result.List()
}

// Environment where sz objects are evaluated to shtml objects
type Environment struct {
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
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







-
+

-
+












-
-
-
-
-
-







	ev.bind(sz.SymTypeString, 2, evalMetaString)
	ev.bind(sz.SymTypeTimestamp, 2, evalMetaString)
	ev.bind(sz.SymTypeURL, 2, evalMetaString)
	ev.bind(sz.SymTypeWord, 2, evalMetaString)

	evalMetaSet := func(args sx.Vector, env *Environment) sx.Object {
		var sb strings.Builder
		for elem := getList(args[1], env); elem != nil; elem = elem.Tail() {
		for obj := range getList(args[1], env).Values() {
			sb.WriteByte(' ')
			sb.WriteString(getString(elem.Car(), env).GetValue())
			sb.WriteString(getString(obj, env).GetValue())
		}
		s := sb.String()
		if len(s) > 0 {
			s = s[1:]
		}
		a := make(attrs.Attributes, 2).
			Set("name", getSymbol(args[0], env).GetValue()).
			Set("content", s)
		return ev.EvaluateMeta(a)
	}
	ev.bind(sz.SymTypeIDSet, 2, evalMetaSet)
	ev.bind(sz.SymTypeTagSet, 2, evalMetaSet)
	ev.bind(sz.SymTypeZettelmarkup, 2, func(args sx.Vector, env *Environment) sx.Object {
		a := make(attrs.Attributes, 2).
			Set("name", getSymbol(args[0], env).GetValue()).
			Set("content", text.EvaluateInlineString(getList(args[1], env)))
		return ev.EvaluateMeta(a)
	})
}

// EvaluateMeta returns HTML meta object for an attribute.
func (ev *Evaluator) EvaluateMeta(a attrs.Attributes) *sx.Pair {
	return sx.Nil().Cons(EvaluateAttrbute(a)).Cons(SymMeta)
}

336
337
338
339
340
341
342
343
344


345
346
347
348
349
350
351
330
331
332
333
334
335
336


337
338
339
340
341
342
343
344
345







-
-
+
+







			if pos >= len(args) {
				break
			}
			ddBlock := getList(ev.Eval(args[pos], env), env)
			if ddBlock == nil {
				continue
			}
			for ddlst := ddBlock; ddlst != nil; ddlst = ddlst.Tail() {
				dditem := getList(ddlst.Car(), env)
			for ddlst := range ddBlock.Values() {
				dditem := getList(ddlst, env)
				items.Add(dditem.Cons(symDD))
			}
		}
		return items.List()
	})
	ev.bind(sz.SymListQuote, 0, func(args sx.Vector, env *Environment) sx.Object {
		if args == nil {
409
410
411
412
413
414
415
416

417
418
419
420
421
422
423
403
404
405
406
407
408
409

410
411
412
413
414
415
416
417







-
+







	ev.bind(sz.SymVerbatimEval, 2, func(args sx.Vector, env *Environment) sx.Object {
		return evalVerbatim(GetAttributes(args[0], env).AddClass("zs-eval"), getString(args[1], env))
	})
	ev.bind(sz.SymVerbatimHTML, 2, ev.evalHTML)
	ev.bind(sz.SymVerbatimMath, 2, func(args sx.Vector, env *Environment) sx.Object {
		return evalVerbatim(GetAttributes(args[0], env).AddClass("zs-math"), getString(args[1], env))
	})
	ev.bind(sz.SymVerbatimProg, 2, func(args sx.Vector, env *Environment) sx.Object {
	ev.bind(sz.SymVerbatimCode, 2, func(args sx.Vector, env *Environment) sx.Object {
		a := GetAttributes(args[0], env)
		content := getString(args[1], env)
		if a.HasDefault() {
			content = sx.MakeString(visibleReplacer.Replace(content.GetValue()))
		}
		return evalVerbatim(a, content)
	})
433
434
435
436
437
438
439

440
441
442
443
444
445
446
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441







+







		refKind := ref.Car()
		if sx.IsNil(refKind) {
			return sx.Nil()
		}
		if refValue := getString(ref.Tail().Car(), env); refValue.GetValue() != "" {
			if refSym, isRefSym := sx.GetSymbol(refKind); isRefSym && refSym.IsEqualSymbol(sz.SymRefStateExternal) {
				a := GetAttributes(args[0], env).Set("src", refValue.GetValue()).AddClass("external")
				// TODO: if len(args) > 2, add "alt" attr based on args[2:], as in SymEmbed
				return sx.Nil().Cons(sx.Nil().Cons(EvaluateAttrbute(a)).Cons(SymIMG)).Cons(SymP)
			}
			return sx.MakeList(
				sxhtml.SymInlineComment,
				sx.MakeString("transclude"),
				refKind,
				sx.MakeString("->"),
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
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







-
-
+
+











-
-
+
+







		}
		return result.List()
	}
}

func (ev *Evaluator) evalDescriptionTerm(term *sx.Pair, env *Environment) *sx.Pair {
	var result sx.ListBuilder
	for node := term; node != nil; node = node.Tail() {
		elem := ev.Eval(node.Car(), env)
	for obj := range term.Values() {
		elem := ev.Eval(obj, env)
		result.Add(elem)
	}
	return result.List()
}

func (ev *Evaluator) evalTableRow(sym *sx.Symbol, pairs *sx.Pair, env *Environment) *sx.Pair {
	if pairs == nil {
		return nil
	}
	var row sx.ListBuilder
	row.Add(symTR)
	for pair := pairs; pair != nil; pair = pair.Tail() {
		row.Add(sx.Cons(sym, ev.Eval(pair.Car(), env)))
	for obj := range pairs.Values() {
		row.Add(sx.Cons(sym, ev.Eval(obj, env)))
	}
	return row.List()
}
func (ev *Evaluator) makeCellFn(align string) EvalFn {
	return func(args sx.Vector, env *Environment) sx.Object {
		tdata := ev.evalSlice(args, env)
		if align != "" {
600
601
602
603
604
605
606
607

608
609
610
611
612
613
614
595
596
597
598
599
600
601

602
603
604
605
606
607
608
609







-
+







				a = a.Set("alt", d)
			}
		}
		return sx.MakeList(SymIMG, EvaluateAttrbute(a))
	})
	ev.bind(sz.SymEmbedBLOB, 3, func(args sx.Vector, env *Environment) sx.Object {
		a, syntax, data := GetAttributes(args[0], env), getString(args[1], env), getString(args[2], env)
		summary, hasSummary := a.Get(api.KeySummary)
		summary, hasSummary := a.Get(meta.KeySummary)
		if !hasSummary {
			summary = ""
		}
		return evalBLOB(
			sx.MakeList(sxhtml.SymListSplice, sx.MakeString(summary)),
			syntax,
			data,
660
661
662
663
664
665
666
667

668
669
670
671
672
673
674
655
656
657
658
659
660
661

662
663
664
665
666
667
668
669







-
+







		env.endnotes = append(env.endnotes, endnoteInfo{
			noteID: noteID, noteAST: args[1:], noteHx: nil, attrs: attrPlist})
		hrefAttr := sx.Nil().Cons(sx.Cons(SymAttrRole, sx.MakeString("doc-noteref"))).
			Cons(sx.Cons(SymAttrHref, sx.MakeString("#fn:"+noteID))).
			Cons(sx.Cons(SymAttrClass, sx.MakeString("zs-noteref"))).
			Cons(sxhtml.SymAttr)
		href := sx.Nil().Cons(sx.MakeString(noteNum)).Cons(hrefAttr).Cons(SymA)
		supAttr := sx.Nil().Cons(sx.Cons(SymAttrId, sx.MakeString("fnref:"+noteID))).Cons(sxhtml.SymAttr)
		supAttr := sx.Nil().Cons(sx.Cons(SymAttrID, sx.MakeString("fnref:"+noteID))).Cons(sxhtml.SymAttr)
		return sx.Nil().Cons(href).Cons(supAttr).Cons(symSUP)
	})

	ev.bind(sz.SymFormatDelete, 1, ev.makeFormatFn(symDEL))
	ev.bind(sz.SymFormatEmph, 1, ev.makeFormatFn(symEM))
	ev.bind(sz.SymFormatInsert, 1, ev.makeFormatFn(symINS))
	ev.bind(sz.SymFormatMark, 1, ev.makeFormatFn(symMARK))
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702

703
704
705
706
707
708
709
710
711
712
713
679
680
681
682
683
684
685

686
687
688
689
690
691
692
693
694
695

696
697
698


699
700
701
702
703
704
705







-










-
+


-
-







				if s := getString(ev.Eval(args[1], env), env); s.GetValue() != "" {
					return sx.Nil().Cons(s).Cons(sxhtml.SymInlineComment)
				}
			}
		}
		return sx.Nil()
	})
	ev.bind(sz.SymLiteralHTML, 2, ev.evalHTML)
	ev.bind(sz.SymLiteralInput, 2, func(args sx.Vector, env *Environment) sx.Object {
		return evalLiteral(args, nil, symKBD, env)
	})
	ev.bind(sz.SymLiteralMath, 2, func(args sx.Vector, env *Environment) sx.Object {
		a := GetAttributes(args[0], env).AddClass("zs-math")
		return evalLiteral(args, a, symCODE, env)
	})
	ev.bind(sz.SymLiteralOutput, 2, func(args sx.Vector, env *Environment) sx.Object {
		return evalLiteral(args, nil, symSAMP, env)
	})
	ev.bind(sz.SymLiteralProg, 2, func(args sx.Vector, env *Environment) sx.Object {
	ev.bind(sz.SymLiteralCode, 2, func(args sx.Vector, env *Environment) sx.Object {
		return evalLiteral(args, nil, symCODE, env)
	})

	ev.bind(sz.SymLiteralZettel, 0, nilFn)
}

func (ev *Evaluator) makeFormatFn(sym *sx.Symbol) EvalFn {
	return func(args sx.Vector, env *Environment) sx.Object {
		a := GetAttributes(args[0], env)
		env.pushAttributes(a)
		defer env.popAttributes()
790
791
792
793
794
795
796
797

798
799
800
801
802
803
804
805
806
807
808
809
810
811
812


813
814
815
816
817
818
819
782
783
784
785
786
787
788

789
790
791
792
793
794
795
796
797
798
799
800
801
802


803
804
805
806
807
808
809
810
811







-
+













-
-
+
+







func evalBLOB(description *sx.Pair, syntax, data sx.String) sx.Object {
	if data.GetValue() == "" {
		return sx.Nil()
	}
	switch syntax.GetValue() {
	case "":
		return sx.Nil()
	case api.ValueSyntaxSVG:
	case meta.ValueSyntaxSVG:
		return sx.Nil().Cons(sx.Nil().Cons(data).Cons(sxhtml.SymNoEscape)).Cons(SymP)
	default:
		imgAttr := sx.Nil().Cons(sx.Cons(SymAttrSrc, sx.MakeString("data:image/"+syntax.GetValue()+";base64,"+data.GetValue())))
		var sb strings.Builder
		flattenText(&sb, description)
		if d := sb.String(); d != "" {
			imgAttr = imgAttr.Cons(sx.Cons(symAttrAlt, sx.MakeString(d)))
		}
		return sx.Nil().Cons(sx.Nil().Cons(imgAttr.Cons(sxhtml.SymAttr)).Cons(SymIMG)).Cons(SymP)
	}
}

func flattenText(sb *strings.Builder, lst *sx.Pair) {
	for elem := lst; elem != nil; elem = elem.Tail() {
		switch obj := elem.Car().(type) {
	for elem := range lst.Values() {
		switch obj := elem.(type) {
		case sx.String:
			sb.WriteString(obj.GetValue())
		case *sx.Pair:
			flattenText(sb, obj)
		}
	}
}
879
880
881
882
883
884
885
886
887


888
889
890
891
892
893
894
871
872
873
874
875
876
877


878
879
880
881
882
883
884
885
886







-
-
+
+







	}
	return nil
}

// EvalPairList evaluates a list of lists.
func (ev *Evaluator) EvalPairList(pair *sx.Pair, env *Environment) *sx.Pair {
	var result sx.ListBuilder
	for node := pair; node != nil; node = node.Tail() {
		elem := ev.Eval(node.Car(), env)
	for obj := range pair.Values() {
		elem := ev.Eval(obj, env)
		result.Add(elem)
	}
	if env.err == nil {
		return result.List()
	}
	return nil
}

Added sz/build.go.


































































































































1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
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
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
//-----------------------------------------------------------------------------
// Copyright (c) 2025-present Detlef Stern
//
// 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.
//
// SPDX-License-Identifier: EUPL-1.2
// SPDX-FileCopyrightText: 2025-present Detlef Stern
//-----------------------------------------------------------------------------

package sz

import "t73f.de/r/sx"

// MakeBlock builds a block node.
func MakeBlock(blocks ...*sx.Pair) *sx.Pair {
	var lb sx.ListBuilder
	lb.Add(SymBlock)
	for _, block := range blocks {
		lb.Add(block)
	}
	return lb.List()
}

// MakeBlockList builds a block node from a list of blocks.
func MakeBlockList(blocks *sx.Pair) *sx.Pair { return blocks.Cons(SymBlock) }

// MakeInlineList builds an inline node from a list of inlines.
func MakeInlineList(inlines *sx.Pair) *sx.Pair { return inlines.Cons(SymInline) }

// MakePara builds a paragraph node.
func MakePara(inlines *sx.Pair) *sx.Pair { return inlines.Cons(SymPara) }

// MakeVerbatim builds a node for verbatim text.
func MakeVerbatim(sym *sx.Symbol, attrs *sx.Pair, content string) *sx.Pair {
	return sx.MakeList(sym, attrs, sx.MakeString(content))
}

// MakeRegion builds a region node.
func MakeRegion(sym *sx.Symbol, attrs *sx.Pair, blocks *sx.Pair, inlines *sx.Pair) *sx.Pair {
	return inlines.Cons(blocks).Cons(attrs).Cons(sym)
}

// MakeHeading builds a heading node.
func MakeHeading(level int, attrs, text *sx.Pair, slug, fragment string) *sx.Pair {
	return text.
		Cons(sx.MakeString(fragment)).
		Cons(sx.MakeString(slug)).
		Cons(attrs).
		Cons(sx.Int64(level)).
		Cons(SymHeading)
}

// MakeThematic builds a node to implement a thematic break.
func MakeThematic(attrs *sx.Pair) *sx.Pair {
	return sx.Cons(SymThematic, sx.Cons(attrs, sx.Nil()))
}

// MakeCell builds a table cell node.
func MakeCell(sym *sx.Symbol, inlines *sx.Pair) *sx.Pair {
	return inlines.Cons(sym)
}

// MakeTransclusion builds a transclusion node.
func MakeTransclusion(attrs *sx.Pair, ref *sx.Pair, text *sx.Pair) *sx.Pair {
	return text.Cons(ref).Cons(attrs).Cons(SymTransclude)
}

// MakeBLOB builds a block BLOB node.
func MakeBLOB(description *sx.Pair, syntax, content string) *sx.Pair {
	return sx.Cons(SymBLOB,
		sx.Cons(description,
			sx.Cons(sx.MakeString(syntax),
				sx.Cons(sx.MakeString(content), sx.Nil()))))
}

// MakeText builds a text node.
func MakeText(text string) *sx.Pair {
	return sx.Cons(SymText, sx.Cons(sx.MakeString(text), sx.Nil()))
}

// MakeSoft builds a node for a soft line break.
func MakeSoft() *sx.Pair { return sx.Cons(SymSoft, sx.Nil()) }

// MakeHard builds a node for a hard line break.
func MakeHard() *sx.Pair { return sx.Cons(SymHard, sx.Nil()) }

// MakeLink builds a link node.
func MakeLink(sym *sx.Symbol, attrs *sx.Pair, ref string, text *sx.Pair) *sx.Pair {
	return text.Cons(sx.MakeString(ref)).Cons(attrs).Cons(sym)
}

// MakeEmbed builds a embed node.
func MakeEmbed(attrs *sx.Pair, ref sx.Object, syntax string, text *sx.Pair) *sx.Pair {
	return text.Cons(sx.MakeString(syntax)).Cons(ref).Cons(attrs).Cons(SymEmbed)
}

// MakeEmbedBLOB builds an embedded inline BLOB node.
func MakeEmbedBLOB(attrs *sx.Pair, syntax string, content string, inlines *sx.Pair) *sx.Pair {
	return inlines.Cons(sx.MakeString(content)).Cons(sx.MakeString(syntax)).Cons(attrs).Cons(SymEmbedBLOB)
}

// MakeCite builds a node that specifies a citation.
func MakeCite(attrs *sx.Pair, text string, inlines *sx.Pair) *sx.Pair {
	return inlines.Cons(sx.MakeString(text)).Cons(attrs).Cons(SymCite)
}

// MakeEndnote builds an endnote node.
func MakeEndnote(attrs, inlines *sx.Pair) *sx.Pair {
	return inlines.Cons(attrs).Cons(SymEndnote)
}

// MakeMark builds a mark note.
func MakeMark(mark string, slug, fragment string, inlines *sx.Pair) *sx.Pair {
	return inlines.Cons(sx.MakeString(fragment)).Cons(sx.MakeString(slug)).Cons(sx.MakeString(mark)).Cons(SymMark)
}

// MakeFormat builds an inline formatting node.
func MakeFormat(sym *sx.Symbol, attrs, inlines *sx.Pair) *sx.Pair {
	return inlines.Cons(attrs).Cons(sym)
}

// MakeLiteral builds a inline node with literal text.
func MakeLiteral(sym *sx.Symbol, attrs *sx.Pair, text string) *sx.Pair {
	return sx.Cons(sym, sx.Cons(attrs, sx.Cons(sx.MakeString(text), sx.Nil())))
}

Changes to sz/const.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
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105










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







-
+

-



-











+




-














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

	SymLinkHosted      = sx.MakeSymbol("LINK-HOSTED")
	SymLinkBased       = sx.MakeSymbol("LINK-BASED")
	SymLinkQuery       = sx.MakeSymbol("LINK-QUERY")
	SymLinkExternal    = sx.MakeSymbol("LINK-EXTERNAL")
	SymListOrdered     = sx.MakeSymbol("ORDERED")
	SymListUnordered   = sx.MakeSymbol("UNORDERED")
	SymListQuote       = sx.MakeSymbol("QUOTATION")
	SymLiteralProg     = sx.MakeSymbol("LITERAL-CODE")
	SymLiteralCode     = sx.MakeSymbol("LITERAL-CODE")
	SymLiteralComment  = sx.MakeSymbol("LITERAL-COMMENT")
	SymLiteralHTML     = sx.MakeSymbol("LITERAL-HTML")
	SymLiteralInput    = sx.MakeSymbol("LITERAL-INPUT")
	SymLiteralMath     = sx.MakeSymbol("LITERAL-MATH")
	SymLiteralOutput   = sx.MakeSymbol("LITERAL-OUTPUT")
	SymLiteralZettel   = sx.MakeSymbol("LITERAL-ZETTEL")
	SymMark            = sx.MakeSymbol("MARK")
	SymPara            = sx.MakeSymbol("PARA")
	SymRegionBlock     = sx.MakeSymbol("REGION-BLOCK")
	SymRegionQuote     = sx.MakeSymbol("REGION-QUOTE")
	SymRegionVerse     = sx.MakeSymbol("REGION-VERSE")
	SymSoft            = sx.MakeSymbol("SOFT")
	SymTable           = sx.MakeSymbol("TABLE")
	SymText            = sx.MakeSymbol("TEXT")
	SymThematic        = sx.MakeSymbol("THEMATIC")
	SymTransclude      = sx.MakeSymbol("TRANSCLUDE")
	SymUnknown         = sx.MakeSymbol("UNKNOWN-NODE")
	SymVerbatimCode    = sx.MakeSymbol("VERBATIM-CODE")
	SymVerbatimComment = sx.MakeSymbol("VERBATIM-COMMENT")
	SymVerbatimEval    = sx.MakeSymbol("VERBATIM-EVAL")
	SymVerbatimHTML    = sx.MakeSymbol("VERBATIM-HTML")
	SymVerbatimMath    = sx.MakeSymbol("VERBATIM-MATH")
	SymVerbatimProg    = sx.MakeSymbol("VERBATIM-CODE")
	SymVerbatimZettel  = sx.MakeSymbol("VERBATIM-ZETTEL")

	// Constant symbols for reference states.
	SymRefStateInvalid  = sx.MakeSymbol("INVALID")
	SymRefStateZettel   = sx.MakeSymbol("ZETTEL")
	SymRefStateSelf     = sx.MakeSymbol("SELF")
	SymRefStateFound    = sx.MakeSymbol("FOUND")
	SymRefStateBroken   = sx.MakeSymbol("BROKEN")
	SymRefStateHosted   = sx.MakeSymbol("HOSTED")
	SymRefStateBased    = sx.MakeSymbol("BASED")
	SymRefStateQuery    = sx.MakeSymbol("QUERY")
	SymRefStateExternal = sx.MakeSymbol("EXTERNAL")

	// Symbols for metadata types.
	SymTypeCredential   = sx.MakeSymbol("CREDENTIAL")
	SymTypeEmpty        = sx.MakeSymbol("EMPTY-STRING")
	SymTypeID           = sx.MakeSymbol("ZID")
	SymTypeIDSet        = sx.MakeSymbol("ZID-SET")
	SymTypeNumber       = sx.MakeSymbol("NUMBER")
	SymTypeString       = sx.MakeSymbol("STRING")
	SymTypeTagSet       = sx.MakeSymbol("TAG-SET")
	SymTypeTimestamp    = sx.MakeSymbol("TIMESTAMP")
	SymTypeURL          = sx.MakeSymbol("URL")
	SymTypeWord         = sx.MakeSymbol("WORD")
	SymTypeCredential = sx.MakeSymbol("CREDENTIAL")
	SymTypeEmpty      = sx.MakeSymbol("EMPTY-STRING")
	SymTypeID         = sx.MakeSymbol("ZID")
	SymTypeIDSet      = sx.MakeSymbol("ZID-SET")
	SymTypeNumber     = sx.MakeSymbol("NUMBER")
	SymTypeString     = sx.MakeSymbol("STRING")
	SymTypeTagSet     = sx.MakeSymbol("TAG-SET")
	SymTypeTimestamp  = sx.MakeSymbol("TIMESTAMP")
	SymTypeURL        = sx.MakeSymbol("URL")
	SymTypeWord       = sx.MakeSymbol("WORD")
	SymTypeZettelmarkup = sx.MakeSymbol("ZETTELMARKUP")
)

Changes to sz/parser.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
11
12
13
14
15
16
17

18
19
20
21
22
23
24
25
26
27
28






29
30
31
32
33

34
35
36

37
38
39
40
41
42
43
44
























-
+










-
-
-
-
-
-





-
+


-
+







-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
// SPDX-FileCopyrightText: 2024-present Detlef Stern
//-----------------------------------------------------------------------------

package sz

import (
	"t73f.de/r/sx"
	"t73f.de/r/zsc/api"
	"t73f.de/r/zsc/domain/meta"
	"t73f.de/r/zsc/input"
)

// --- Contains some simple parsers

// ---- Syntax: none

// ParseNoneBlocks parses no block.
func ParseNoneBlocks(*input.Input) *sx.Pair { return nil }

// ParseNoneInlines skips to the end of line and parses no inline.
func ParseNoneInlines(inp *input.Input) *sx.Pair {
	inp.SkipToEOL()
	return nil
}

// ---- Some plain text syntaxes

// ParsePlainBlocks parses the block as plain text with the given syntax.
func ParsePlainBlocks(inp *input.Input, syntax string) *sx.Pair {
	var sym *sx.Symbol
	if syntax == api.ValueSyntaxHTML {
	if syntax == meta.ValueSyntaxHTML {
		sym = SymVerbatimHTML
	} else {
		sym = SymVerbatimProg
		sym = SymVerbatimCode
	}
	return sx.MakeList(
		sym,
		sx.MakeList(sx.Cons(sx.MakeString(""), sx.MakeString(syntax))),
		sx.MakeString(string(inp.ScanLineContent())),
	)
}

// ParsePlainInlines parses the inline as plain text with the given syntax.
func ParsePlainInlines(inp *input.Input, syntax string) *sx.Pair {
	var sym *sx.Symbol
	if syntax == api.ValueSyntaxHTML {
		sym = SymLiteralHTML
	} else {
		sym = SymLiteralProg
	}
	pos := inp.Pos
	inp.SkipToEOL()
	return sx.MakeList(
		sym,
		sx.MakeList(sx.Cons(sx.MakeString(""), sx.MakeString(syntax))),
		sx.MakeString(string(inp.Src[pos:inp.Pos])),
	)
}

Changes to sz/parser_test.go.

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







-
+


-
-
-
-
-
-




-
-
-
+
+
+
-

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







-
-
-
-




func TestParseNone(t *testing.T) {
	if got := sz.ParseNoneBlocks(nil); got != nil {
		t.Error("GOTB", got)
	}

	inp := input.NewInput([]byte("1234\n6789"))
	if got := sz.ParseNoneInlines(inp); got != nil {
	if got := sz.ParseNoneBlocks(inp); got != nil {
		t.Error("GOTI", got)
	}
	if got := inp.Pos; got != 4 {
		t.Errorf("input should be on position 4, but is %d", got)
	}
	if got := inp.Ch; got != '\n' {
		t.Errorf("input character should be 10, but is %d", got)
	}
}

func TestParsePlani(t *testing.T) {
	testcases := []struct {
		src        string
		syntax     string
		expBlocks  string
		src       string
		syntax    string
		expBlocks string
		expInlines string
	}{
		{"abc", "html",
			"(VERBATIM-HTML ((\"\" . \"html\")) \"abc\")",
		{"abc", "html", "(VERBATIM-HTML ((\"\" . \"html\")) \"abc\")"},
			"(LITERAL-HTML ((\"\" . \"html\")) \"abc\")"},
		{"abc\ndef", "html",
			"(VERBATIM-HTML ((\"\" . \"html\")) \"abc\\ndef\")",
		{"abc\ndef", "html", "(VERBATIM-HTML ((\"\" . \"html\")) \"abc\\ndef\")"},
			"(LITERAL-HTML ((\"\" . \"html\")) \"abc\")"},
		{"abc", "text",
			"(VERBATIM-CODE ((\"\" . \"text\")) \"abc\")",
		{"abc", "text", "(VERBATIM-CODE ((\"\" . \"text\")) \"abc\")"},
			"(LITERAL-CODE ((\"\" . \"text\")) \"abc\")"},
		{"abc\nDEF", "text",
			"(VERBATIM-CODE ((\"\" . \"text\")) \"abc\\nDEF\")",
		{"abc\nDEF", "text", "(VERBATIM-CODE ((\"\" . \"text\")) \"abc\\nDEF\")"},
			"(LITERAL-CODE ((\"\" . \"text\")) \"abc\")"},
	}
	for i, tc := range testcases {
		t.Run(tc.syntax+":"+tc.src, func(t *testing.T) {
			inp := input.NewInput([]byte(tc.src))
			if got := sz.ParsePlainBlocks(inp, tc.syntax).String(); tc.expBlocks != got {
				t.Errorf("%d: %q/%v\nexpected: %q\ngot     : %q", i, tc.src, tc.syntax, tc.expBlocks, got)
			}
			inp.SetPos(0)
			if got := sz.ParsePlainInlines(inp, tc.syntax).String(); tc.expInlines != got {
				t.Errorf("%d: %q/%v\nexpected: %q\ngot     : %q", i, tc.src, tc.syntax, tc.expInlines, got)
			}
		})
	}
}

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







-
-
+
+







import (
	"t73f.de/r/sx"
	"t73f.de/r/zsc/attrs"
)

// GetAttributes traverses a s-expression list and returns an attribute structure.
func GetAttributes(seq *sx.Pair) (result attrs.Attributes) {
	for elem := seq; elem != nil; elem = elem.Tail() {
		pair, isPair := sx.GetPair(elem.Car())
	for obj := range seq.Values() {
		pair, isPair := sx.GetPair(obj)
		if !isPair || pair == nil {
			continue
		}
		key := pair.Car()
		if !key.IsAtom() {
			continue
		}
90
91
92
93
94
95
96
97

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

97
98
99
100
101
102
103
104







-
+







}
func doMakeMeta(obj sx.Object) Meta {
	lst, isList := sx.GetPair(obj)
	if !isList || !lst.Car().IsEqual(SymMeta) {
		return nil
	}
	result := make(map[string]MetaValue)
	for node := lst.Tail(); node != nil; node = node.Tail() {
	for node := range lst.Tail().Pairs() {
		if mv, found := makeMetaValue(node.Head()); found {
			result[mv.Key] = mv
		}
	}
	return result
}
func makeMetaValue(mnode *sx.Pair) (MetaValue, bool) {
134
135
136
137
138
139
140
141

142
143

144
145
146
147
148
149
150
151
152
153
134
135
136
137
138
139
140

141


142



143
144
145
146
147
148
149







-
+
-
-
+
-
-
-







		if pair, isPair := sx.GetPair(mv.Value); isPair {
			return pair
		}
	}
	return nil
}

// MapRefStateToLinkEmbed maps a reference state symbol to a link symbol or to
// MapRefStateToLink maps a reference state symbol to a link symbol.
// an embed symbol, depending on 'forLink'.
func MapRefStateToLinkEmbed(symRefState *sx.Symbol, forLink bool) *sx.Symbol {
func MapRefStateToLink(symRefState *sx.Symbol) *sx.Symbol {
	if !forLink {
		return SymEmbed
	}
	if sym, found := mapRefStateLink[symRefState]; found {
		return sym
	}
	return SymLinkInvalid
}

var mapRefStateLink = map[*sx.Symbol]*sx.Symbol{

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







+
-
+
+



+
-
+



-
+



-
+





-
+

-

-
+

















+
+
+
+
+







//
// SPDX-License-Identifier: EUPL-1.2
// SPDX-FileCopyrightText: 2024-present Detlef Stern
//-----------------------------------------------------------------------------

package sz

import (
import "t73f.de/r/sx"
	"t73f.de/r/sx"
)

// Visitor is walking the sx-based AST.
type Visitor interface {
	VisitBefore(node *sx.Pair, env *sx.Pair) (sx.Object, bool)
	Visit(node *sx.Pair, env *sx.Pair) sx.Object
	VisitAfter(node *sx.Pair, env *sx.Pair) sx.Object
}

// Walk a sx-based AST through a Visitor.
func Walk(v Visitor, node *sx.Pair, env *sx.Pair) *sx.Pair {
func Walk(v Visitor, node *sx.Pair, env *sx.Pair) sx.Object {
	if node == nil {
		return nil
	}
	if result, isPair := sx.GetPair(v.Visit(node, env)); isPair {
	if result, ok := v.VisitBefore(node, env); ok {
		return result
	}

	if sym, isSymbol := sx.GetSymbol(node.Car()); isSymbol {
		if fn, found := mapChildrenWalk[sym]; found {
			return fn(v, node, env)
			node = fn(v, node, env)
		}
		return node
	}
	panic(node)
	return v.VisitAfter(node, env)
}

var mapChildrenWalk map[*sx.Symbol]func(Visitor, *sx.Pair, *sx.Pair) *sx.Pair

func init() {
	mapChildrenWalk = map[*sx.Symbol]func(Visitor, *sx.Pair, *sx.Pair) *sx.Pair{
		SymBlock:         walkChildrenTail,
		SymPara:          walkChildrenTail,
		SymRegionBlock:   walkChildrenRegion,
		SymRegionQuote:   walkChildrenRegion,
		SymRegionVerse:   walkChildrenRegion,
		SymHeading:       walkChildrenHeading,
		SymListOrdered:   walkChildrenTail,
		SymListUnordered: walkChildrenTail,
		SymListQuote:     walkChildrenTail,
		SymDescription:   walkChildrenDescription,
		SymTable:         walkChildrenTable,
		SymCell:          walkChildrenTail,
		SymCellCenter:    walkChildrenTail,
		SymCellLeft:      walkChildrenTail,
		SymCellRight:     walkChildrenTail,
		SymTransclude:    walkChildrenInlines4,

		SymInline:       walkChildrenTail,
		SymEndnote:      walkChildrenInlines3,
		SymMark:         walkChildrenMark,
		SymLinkBased:    walkChildrenInlines4,
		SymLinkBroken:   walkChildrenInlines4,
		SymLinkExternal: walkChildrenInlines4,
78
79
80
81
82
83
84
85

86
87
88
89
90
91
92
85
86
87
88
89
90
91

92
93
94
95
96
97
98
99







-
+







		SymFormatSub:    walkChildrenInlines3,
		SymFormatSuper:  walkChildrenInlines3,
	}
}

func walkChildrenTail(v Visitor, node *sx.Pair, env *sx.Pair) *sx.Pair {
	hasNil := false
	for n := node.Tail(); n != nil; n = n.Tail() {
	for n := range node.Tail().Pairs() {
		obj := Walk(v, n.Head(), env)
		if sx.IsNil(obj) {
			hasNil = true
		}
		n.SetCar(obj)
	}
	if !hasNil {
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
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







-
+










-
-
+







		n = next
	}
	return node
}

func walkChildrenList(v Visitor, lst *sx.Pair, env *sx.Pair) *sx.Pair {
	hasNil := false
	for n := lst; n != nil; n = n.Tail() {
	for n := range lst.Pairs() {
		obj := Walk(v, n.Head(), env)
		if sx.IsNil(obj) {
			hasNil = true
		}
		n.SetCar(obj)
	}
	if !hasNil {
		return lst
	}
	var result sx.ListBuilder
	for n := lst; n != nil; n = n.Tail() {
		obj := n.Car()
	for obj := range lst.Values() {
		if !sx.IsNil(obj) {
			result.Add(obj)
		}
	}
	return result.List()
}

161
162
163
164
165
166
167
168

169
170
171
172
173
174
175
167
168
169
170
171
172
173

174
175
176
177
178
179
180
181







-
+







		}
		n.SetCar(Walk(v, n.Head(), env))
	}
	return dn
}

func walkChildrenTable(v Visitor, tn *sx.Pair, env *sx.Pair) *sx.Pair {
	for row := tn.Tail(); row != nil; row = row.Tail() {
	for row := range tn.Tail().Pairs() {
		row.SetCar(walkChildrenList(v, row.Head(), env))
	}
	return tn
}

func walkChildrenMark(v Visitor, mn *sx.Pair, env *sx.Pair) *sx.Pair {
	// sym := mn.Car()
187
188
189
190
191
192
193
194
195
196
197
198



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





200
201
202
203
204
205
206
207
208
209







-
-
-
-
-
+
+
+







	// sym := en.Car()
	next := en.Tail()
	// attr := next.Car()
	next = next.Tail()
	// ref := next.Car()
	next = next.Tail()
	// syntax := next.Car()
	next = next.Tail()
	if next != nil {
		// text := next.Car()
		next.SetCar(Walk(v, next.Head(), env))
	}

	// text-list := next.Tail()
	next.SetCdr(walkChildrenList(v, next.Tail(), env))
	return en
}

func walkChildrenInlines4(v Visitor, ln *sx.Pair, env *sx.Pair) *sx.Pair {
	// sym := ln.Car()
	next := ln.Tail()
	// attrs := next.Car()

Changes to sz/zmk/block.go.

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







-
+








-
+

















-
+










-
+


-
+


-
+


-
-
+
+


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


+







			inp.EatEOL()
			cp.cleanupListsAfterEOL()
			return nil, false
		case ':':
			bn, success = cp.parseColon()
		case '@', '`', runeModGrave, '%', '~', '$':
			cp.clearStacked()
			bn, success = cp.parseVerbatim()
			bn, success = parseVerbatim(inp)
		case '"', '<':
			cp.clearStacked()
			bn, success = cp.parseRegion()
		case '=':
			cp.clearStacked()
			bn, success = cp.parseHeading()
		case '-':
			cp.clearStacked()
			bn, success = cp.parseHRule()
			bn, success = parseHRule(inp)
		case '*', '#', '>':
			cp.lastRow = nil
			cp.descrl = nil
			bn, success = cp.parseNestedList()
		case ';':
			cp.lists = nil
			cp.lastRow = nil
			bn, success = cp.parseDefTerm()
		case ' ':
			cp.lastRow = nil
			bn, success = nil, cp.parseIndent()
		case '|':
			cp.lists = nil
			cp.descrl = nil
			bn, success = cp.parseRow(), true
		case '{':
			cp.clearStacked()
			bn, success = cp.parseTransclusion()
			bn, success = parseTransclusion(inp)
		}

		if success {
			return bn, false
		}
	}
	inp.SetPos(pos)
	cp.clearStacked()
	ins := cp.parsePara()
	if startsWithSpaceSoftBreak(ins) {
		ins = ins[2:]
		ins = ins.Tail().Tail()
	} else if lastPara != nil {
		lastPair := lastPara.LastPair()
		lastPair.ExtendBang(sx.MakeList(ins...))
		lastPair.ExtendBang(ins)
		return nil, true
	}
	return sx.MakeList(ins...).Cons(sz.SymPara), false
	return sz.MakePara(ins), false
}

func startsWithSpaceSoftBreak(ins sx.Vector) bool {
	if len(ins) < 2 {
func startsWithSpaceSoftBreak(ins *sx.Pair) bool {
	if ins == nil {
		return false
	}
	pair0, isPair0 := sx.GetPair(ins[0])
	pair1, isPair1 := sx.GetPair(ins[0])
	if !isPair0 || !isPair1 {
	pair0, isPair0 := sx.GetPair(ins.Car())
	if pair0 == nil || !isPair0 {
		return false
	}
	next := ins.Tail()
	if next == nil {
		return false
	}
	pair1, isPair1 := sx.GetPair(next.Car())
	if pair1 == nil || !isPair1 {
		return false
	}

	if pair0.Car().IsEqual(sz.SymText) && sz.IsBreakSym(pair1.Car()) {
		if args := pair0.Tail(); args != nil {
			if val, isString := sx.GetString(args.Car()); isString {
				for _, ch := range val.GetValue() {
					if !input.IsSpace(ch) {
						return false
					}
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
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







-
-
+
+
+



-
+

-
+





-
+






-
+
-









-
+
-

-
+



-
+









-
+















-
+

-
+
-














-
+


-
+





-
-







-



-
+











-
+

-
+
-







	if inp.PeekN(1) == ':' {
		cp.clearStacked()
		return cp.parseRegion()
	}
	return cp.parseDefDescr()
}

// parsePara parses paragraphed inline material as a sx.Vector.
func (cp *zmkP) parsePara() (result sx.Vector) {
// parsePara parses paragraphed inline material as a sx List.
func (cp *zmkP) parsePara() *sx.Pair {
	var lb sx.ListBuilder
	for {
		in := cp.parseInline()
		if in == nil {
			return result
			return lb.List()
		}
		result = append(result, in)
		lb.Add(in)
		if sz.IsBreakSym(in.Car()) {
			ch := cp.inp.Ch
			switch ch {
			// Must contain all cases from above switch in parseBlock.
			case input.EOS, '\n', '\r', '@', '`', runeModGrave, '%', '~', '$', '"', '<', '=', '-', '*', '#', '>', ';', ':', ' ', '|', '{':
				return result
				return lb.List()
			}
		}
	}
}

// countDelim read from input until a non-delimiter is found and returns number of delimiter chars.
func (cp *zmkP) countDelim(delim rune) int {
func countDelim(inp *input.Input, delim rune) int {
	inp := cp.inp
	cnt := 0
	for inp.Ch == delim {
		cnt++
		inp.Next()
	}
	return cnt
}

// parseVerbatim parses a verbatim block.
func (cp *zmkP) parseVerbatim() (rn *sx.Pair, success bool) {
func parseVerbatim(inp *input.Input) (*sx.Pair, bool) {
	inp := cp.inp
	fch := inp.Ch
	cnt := cp.countDelim(fch)
	cnt := countDelim(inp, fch)
	if cnt < 3 {
		return nil, false
	}
	attrs := cp.parseBlockAttributes()
	attrs := parseBlockAttributes(inp)
	inp.SkipToEOL()
	if inp.Ch == input.EOS {
		return nil, false
	}
	var sym *sx.Symbol
	switch fch {
	case '@':
		sym = sz.SymVerbatimZettel
	case '`', runeModGrave:
		sym = sz.SymVerbatimProg
		sym = sz.SymVerbatimCode
	case '%':
		sym = sz.SymVerbatimComment
	case '~':
		sym = sz.SymVerbatimEval
	case '$':
		sym = sz.SymVerbatimMath
	default:
		panic(fmt.Sprintf("%q is not a verbatim char", fch))
	}
	content := make([]byte, 0, 512)
	for {
		inp.EatEOL()
		posL := inp.Pos
		switch inp.Ch {
		case fch:
			if cp.countDelim(fch) >= cnt {
			if countDelim(inp, fch) >= cnt {
				inp.SkipToEOL()
				rn = sx.MakeList(sym, attrs, sx.MakeString(string(content)))
				return sz.MakeVerbatim(sym, attrs, string(content)), true
				return rn, true
			}
			inp.SetPos(posL)
		case input.EOS:
			return nil, false
		}
		inp.SkipToEOL()
		if len(content) > 0 {
			content = append(content, '\n')
		}
		content = append(content, inp.Src[posL:inp.Pos]...)
	}
}

// parseRegion parses a block region.
func (cp *zmkP) parseRegion() (rn *sx.Pair, success bool) {
func (cp *zmkP) parseRegion() (*sx.Pair, bool) {
	inp := cp.inp
	fch := inp.Ch
	cnt := cp.countDelim(fch)
	cnt := countDelim(inp, fch)
	if cnt < 3 {
		return nil, false
	}

	var sym *sx.Symbol
	oldInVerse := cp.inVerse
	defer func() { cp.inVerse = oldInVerse }()
	switch fch {
	case ':':
		sym = sz.SymRegionBlock
	case '<':
		sym = sz.SymRegionQuote
	case '"':
		sym = sz.SymRegionVerse
		cp.inVerse = true
	default:
		panic(fmt.Sprintf("%q is not a region char", fch))
	}
	attrs := cp.parseBlockAttributes()
	attrs := parseBlockAttributes(inp)
	inp.SkipToEOL()
	if inp.Ch == input.EOS {
		return nil, false
	}
	var blocksBuilder sx.ListBuilder
	var lastPara *sx.Pair
	inp.EatEOL()
	for {
		posL := inp.Pos
		switch inp.Ch {
		case fch:
			if cp.countDelim(fch) >= cnt {
			if countDelim(inp, fch) >= cnt {
				ins := cp.parseRegionLastLine()
				rn = ins.Cons(blocksBuilder.List()).Cons(attrs).Cons(sym)
				return sz.MakeRegion(sym, attrs, blocksBuilder.List(), ins), true
				return rn, true
			}
			inp.SetPos(posL)
		case input.EOS:
			return nil, false
		}
		bn, cont := cp.parseBlock(lastPara)
		if bn != nil {
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
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







-
+

-
+











-
+




-
+



-
+



-
+

-
+



-
-
-
-
-
-
-
-
+
-

-
+
-
-
+



-
+

-
+



-
-
+
+
+



-










-
-
-
+
+
+






-
+
-







			return region.List()
		}
		region.Add(in)
	}
}

// parseHeading parses a head line.
func (cp *zmkP) parseHeading() (hn *sx.Pair, success bool) {
func (cp *zmkP) parseHeading() (*sx.Pair, bool) {
	inp := cp.inp
	delims := cp.countDelim(inp.Ch)
	delims := countDelim(inp, inp.Ch)
	if delims < 3 {
		return nil, false
	}
	if inp.Ch != ' ' {
		return nil, false
	}
	inp.Next()
	inp.SkipSpace()
	if delims > 7 {
		delims = 7
	}
	level := int64(delims - 2)
	level := delims - 2
	var attrs *sx.Pair
	var text sx.ListBuilder
	for {
		if input.IsEOLEOS(inp.Ch) {
			return createHeading(level, attrs, text.List()), true
			return sz.MakeHeading(level, attrs, text.List(), "", ""), true
		}
		in := cp.parseInline()
		if in == nil {
			return createHeading(level, attrs, text.List()), true
			return sz.MakeHeading(level, attrs, text.List(), "", ""), true
		}
		text.Add(in)
		if inp.Ch == '{' && inp.Peek() != '{' {
			attrs = cp.parseBlockAttributes()
			attrs = parseBlockAttributes(inp)
			inp.SkipToEOL()
			return createHeading(level, attrs, text.List()), true
			return sz.MakeHeading(level, attrs, text.List(), "", ""), true
		}
	}
}
func createHeading(level int64, attrs, text *sx.Pair) *sx.Pair {
	return text.
		Cons(sx.MakeString("")). // Fragment
		Cons(sx.MakeString("")). // Slug
		Cons(attrs).
		Cons(sx.Int64(level)).
		Cons(sz.SymHeading)
}


// parseHRule parses a horizontal rule.
func (cp *zmkP) parseHRule() (hn *sx.Pair, success bool) {
func parseHRule(inp *input.Input) (*sx.Pair, bool) {
	inp := cp.inp
	if cp.countDelim(inp.Ch) < 3 {
	if countDelim(inp, inp.Ch) < 3 {
		return nil, false
	}

	attrs := cp.parseBlockAttributes()
	attrs := parseBlockAttributes(inp)
	inp.SkipToEOL()
	return sx.MakeList(sz.SymThematic, attrs), true
	return sz.MakeThematic(attrs), true
}

// parseNestedList parses a list.
func (cp *zmkP) parseNestedList() (res *sx.Pair, success bool) {
	kinds := cp.parseNestedListKinds()
func (cp *zmkP) parseNestedList() (*sx.Pair, bool) {
	inp := cp.inp
	kinds := parseNestedListKinds(inp)
	if len(kinds) == 0 {
		return nil, false
	}
	inp := cp.inp
	inp.SkipSpace()
	if !kinds[len(kinds)-1].IsEqual(sz.SymListQuote) && input.IsEOLEOS(inp.Ch) {
		return nil, false
	}

	if len(kinds) < len(cp.lists) {
		cp.lists = cp.lists[:len(kinds)]
	}
	ln, newLnCount := cp.buildNestedList(kinds)
	pv := cp.parseLinePara()
	bn := sx.Cons(sz.SymBlock, nil)
	if len(pv) != 0 {
		bn.AppendBang(pv.MakeList().Cons(sz.SymPara))
	bn := sz.MakeBlock()
	if pv != nil {
		bn.AppendBang(sz.MakePara(pv))
	}
	lastItemPair := ln.LastPair()
	lastItemPair.AppendBang(bn)
	return cp.cleanupParsedNestedList(newLnCount)
}

func (cp *zmkP) parseNestedListKinds() []*sx.Symbol {
func parseNestedListKinds(inp *input.Input) []*sx.Symbol {
	inp := cp.inp
	result := make([]*sx.Symbol, 0, 8)
	for {
		var sym *sx.Symbol
		switch inp.Ch {
		case '*':
			sym = sz.SymListUnordered
		case '#':
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
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







-
+


-
+











-
-
+







			newLnCount++
			cp.lists = append(cp.lists, ln)
		}
	}
	return ln, newLnCount
}

func (cp *zmkP) cleanupParsedNestedList(newLnCount int) (res *sx.Pair, success bool) {
func (cp *zmkP) cleanupParsedNestedList(newLnCount int) (*sx.Pair, bool) {
	childPos := len(cp.lists) - 1
	parentPos := childPos - 1
	for i := 0; i < newLnCount; i++ {
	for range newLnCount {
		if parentPos < 0 {
			return cp.lists[0], true
		}
		parentLn := cp.lists[parentPos]
		childLn := cp.lists[childPos]
		if firstParent := parentLn.Tail(); firstParent != nil {
			// Add list to last item of the parent list
			lastParent := firstParent.LastPair()
			lastParent.Head().LastPair().AppendBang(childLn)
		} else {
			// Set list to first child of parent.
			childBlock := sx.MakeList(sz.SymBlock, cp.lists[childPos])
			parentLn.LastPair().AppendBang(childBlock)
			parentLn.LastPair().AppendBang(sz.MakeBlock(cp.lists[childPos]))
		}
		childPos--
		parentPos--
	}
	return nil, true
}

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







-
+













+
+
+
+
+
+



















-
-
+
+





-
+



-
-
+
+

-
+







	descrl := cp.descrl
	if descrl == nil {
		descrl = sx.Cons(sz.SymDescription, nil)
		cp.descrl = descrl
		res = descrl
	}
	lastPair, pos := lastPairPos(descrl)
	for {
	for first := true; ; first = false {
		in := cp.parseInline()
		if in == nil {
			if pos%2 == 0 {
				// lastPair is either the empty description list or the last block of definitions
				return nil, false
			}
			// lastPair is the definition term
			return res, true
		}
		if pos%2 == 0 {
			// lastPair is either the empty description list or the last block of definitions
			lastPair = lastPair.AppendBang(sx.Cons(in, nil))
			pos++
		} else if first {
			// Previous term had no description
			lastPair = lastPair.
				AppendBang(sz.MakeBlock()).
				AppendBang(sx.Cons(in, nil))
			pos += 2
		} else {
			// lastPair is the term part and we need to append the inline list just read
			lastPair.Head().LastPair().AppendBang(in)
		}
		if sz.IsBreakSym(in.Car()) {
			return res, true
		}
	}
}

// parseDefDescr parses a description of a definition list.
func (cp *zmkP) parseDefDescr() (res *sx.Pair, success bool) {
	inp := cp.inp
	if inp.Next() != ' ' {
		return nil, false
	}
	inp.Next()
	inp.SkipSpace()
	descrl := cp.descrl
	lastPair, pos := lastPairPos(descrl)
	if descrl == nil || pos <= 0 {
	lastPair, lpPos := lastPairPos(descrl)
	if descrl == nil || lpPos < 0 {
		// No term given
		return nil, false
	}

	pn := cp.parseLinePara()
	if len(pn) == 0 {
	if pn == nil {
		return nil, false
	}

	newDef := sx.MakeList(sz.SymBlock, pn.MakeList().Cons(sz.SymPara))
	if pos%2 == 1 {
	newDef := sz.MakeBlock(sz.MakePara(pn))
	if lpPos%2 == 1 {
		// Just a term, but no definitions
		lastPair.AppendBang(sx.MakeList(sz.SymBlock, newDef))
		lastPair.AppendBang(sz.MakeBlock(newDef))
	} else {
		// lastPara points a the last definition
		lastPair.Head().LastPair().AppendBang(newDef)
	}
	return nil, true
}

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
546
547
548
549
550
551
552

553
554
555
556
557
558

559

560
561

562
563
564
565
566
567
568
569







-
+





-

-
+

-
+







		cnt = len(cp.lists)
	}
	cp.lists = cp.lists[:cnt]
	if cnt == 0 {
		return false
	}
	pv := cp.parseLinePara()
	if len(pv) == 0 {
	if pv == nil {
		return false
	}
	ln := cp.lists[cnt-1]
	lbn := ln.LastPair().Head()
	lpn := lbn.LastPair().Head()
	pvList := pv.MakeList()
	if lpn.Car().IsEqual(sz.SymPara) {
		lpn.LastPair().SetCdr(pvList)
		lpn.LastPair().SetCdr(pv)
	} else {
		lbn.LastPair().AppendBang(pvList.Cons(sz.SymPara))
		lbn.LastPair().AppendBang(sz.MakePara(pv))
	}
	return true
}

func (cp *zmkP) parseIndentForDescription(cnt int) bool {
	descrl := cp.descrl
	lastPair, pos := lastPairPos(descrl)
584
585
586
587
588
589
590
591


592
593

594
595

596




















597
598
599
600

601
602

603
604
605
606
607
608
609


610
611
612
613

614
615

616
617

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

630
631
632
633
634
635
636
580
581
582
583
584
585
586

587
588
589

590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615

616

617
618

619
620
621
622
623
624


625
626
627
628
629

630
631

632
633

634
635
636
637
638
639
640
641
642
643
644
645

646
647
648
649
650
651
652
653







-
+
+

-
+


+

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

-

-
+

-
+





-
-
+
+



-
+

-
+

-
+











-
+







			lastPair.Head().LastPair().AppendBang(in)
			if sz.IsBreakSym(in.Car()) {
				return true
			}
		}
	}

	// Continuation of a definition description
	// Continuation of a definition description.
	// Either it is a continuation of a definition paragraph, or it is a new paragraph.
	pn := cp.parseLinePara()
	if len(pn) == 0 {
	if pn == nil {
		return false
	}

	bn := lastPair.Head()

	// Check for new paragraph
	for curr := bn.Tail(); curr != nil; {
		obj := curr.Head()
		if obj == nil {
			break
		}
		next := curr.Tail()
		if next == nil {
			break
		}
		if symSeparator.IsEqual(next.Head().Car()) {
			// It is a new paragraph!
			obj.LastPair().AppendBang(sz.MakePara(pn))
			return true
		}
		curr = next
	}

	// Continuation of existing paragraph
	para := bn.LastPair().Head().LastPair().Head()
	pnList := pn.MakeList()
	if para.Car().IsEqual(sz.SymPara) {
		para.LastPair().SetCdr(pnList)
		para.LastPair().SetCdr(pn)
	} else {
		bn.LastPair().AppendBang(pnList.Cons(sz.SymPara))
		bn.LastPair().AppendBang(sz.MakePara(pn))
	}
	return true
}

// parseLinePara parses one paragraph of inline material.
func (cp *zmkP) parseLinePara() sx.Vector {
	var ins sx.Vector
func (cp *zmkP) parseLinePara() *sx.Pair {
	var lb sx.ListBuilder
	for {
		in := cp.parseInline()
		if in == nil {
			return ins
			return lb.List()
		}
		ins = append(ins, in)
		lb.Add(in)
		if sz.IsBreakSym(in.Car()) {
			return ins
			return lb.List()
		}
	}
}

// parseRow parse one table row.
func (cp *zmkP) parseRow() *sx.Pair {
	inp := cp.inp
	if inp.Peek() == '%' {
		inp.SkipToEOL()
		return nil
	}
	//var row, curr *sx.Pair

	var row sx.ListBuilder
	for {
		inp.Next()
		cell := cp.parseCell()
		if cell != nil {
			row.Add(cell)
		}
659
660
661
662
663
664
665
666

667
668
669

670
671
672
673
674
675
676
677
678
679


680
681
682
683
684
685
686
687
688
689
676
677
678
679
680
681
682

683
684
685

686
687
688
689
690
691
692
693
694


695
696
697
698

699
700
701
702
703
704
705







-
+


-
+








-
-
+
+


-







	inp := cp.inp
	var cell sx.ListBuilder
	for {
		if input.IsEOLEOS(inp.Ch) {
			if cell.IsEmpty() {
				return nil
			}
			return cell.List().Cons(sz.SymCell)
			return sz.MakeCell(sz.SymCell, cell.List())
		}
		if inp.Ch == '|' {
			return cell.List().Cons(sz.SymCell)
			return sz.MakeCell(sz.SymCell, cell.List())
		}

		in := cp.parseInline()
		cell.Add(in)
	}
}

// parseTransclusion parses '{' '{' '{' ZID '}' '}' '}'
func (cp *zmkP) parseTransclusion() (*sx.Pair, bool) {
	if cp.countDelim('{') != 3 {
func parseTransclusion(inp *input.Input) (*sx.Pair, bool) {
	if countDelim(inp, '{') != 3 {
		return nil, false
	}
	inp := cp.inp
	posA, posE := inp.Pos, 0

loop:

	for {
		switch inp.Ch {
		case input.EOS:
709
710
711
712
713
714
715
716

717
718
719
720

721
725
726
727
728
729
730
731

732
733
734
735

736
737







-
+



-
+

				continue
			}
			break loop
		}
		inp.Next()
	}
	inp.Next() // consume last '}'
	a := cp.parseBlockAttributes()
	attrs := parseBlockAttributes(inp)
	inp.SkipToEOL()
	refText := string(inp.Src[posA:posE])
	ref := ParseReference(refText)
	return sx.MakeList(sz.SymTransclude, a, ref), true
	return sz.MakeTransclusion(attrs, ref, sx.Nil()), true
}

Changes to sz/zmk/inline.go.

11
12
13
14
15
16
17

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







+







// SPDX-FileCopyrightText: 2020-present Detlef Stern
//-----------------------------------------------------------------------------

package zmk

import (
	"fmt"
	"slices"
	"strings"

	"t73f.de/r/sx"
	"t73f.de/r/zsc/api"
	"t73f.de/r/zsc/input"
	"t73f.de/r/zsc/sz"
)
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
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







-
+



-
+









-
+


-
+


-
-
+
+

-
+

-
+

-
+

-
+






-
+


-
+
-
-
-
+
+
-
-


-
-
+
+





-
-
+
+




-
+
-



-
+

-
+



-
+
-

-
+



-
+



-
+


-
-
-
+
+
+


-
+

-
+



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









-
+







-
+


-
+













-
+













-
+


-
+




-
-
-
+
+
-
-
+
-
-
+

-








		var in *sx.Pair
		success := false
		switch inp.Ch {
		case input.EOS:
			return nil
		case '\n', '\r':
			return cp.parseSoftBreak()
			return parseSoftBreak(inp)
		case '[':
			switch inp.Next() {
			case '[':
				in, success = cp.parseLinkEmbed('[', ']', true)
				in, success = cp.parseLink('[', ']')
			case '@':
				in, success = cp.parseCite()
			case '^':
				in, success = cp.parseEndnote()
			case '!':
				in, success = cp.parseMark()
			}
		case '{':
			if inp.Next() == '{' {
				in, success = cp.parseLinkEmbed('{', '}', false)
				in, success = cp.parseEmbed('{', '}')
			}
		case '%':
			in, success = cp.parseComment()
			in, success = parseComment(inp)
		case '_', '*', '>', '~', '^', ',', '"', '#', ':':
			in, success = cp.parseFormat()
		case '@', '\'', '`', '=', runeModGrave:
			in, success = cp.parseLiteral()
		case '\'', '`', '=', runeModGrave:
			in, success = parseLiteral(inp)
		case '$':
			in, success = cp.parseLiteralMath()
			in, success = parseLiteralMath(inp)
		case '\\':
			return cp.parseBackslash()
			return parseBackslash(inp)
		case '-':
			in, success = cp.parseNdash()
			in, success = parseNdash(inp)
		case '&':
			in, success = cp.parseEntity()
			in, success = parseEntity(inp)
		}
		if success {
			return in
		}
	}
	inp.SetPos(pos)
	return cp.parseText()
	return parseText(inp)
}

func (cp *zmkP) parseText() *sx.Pair {
func parseText(inp *input.Input) *sx.Pair { return sz.MakeText(parseString(inp)) }
	return sx.MakeList(sz.SymText, cp.parseString())
}


func parseString(inp *input.Input) string {
func (cp *zmkP) parseString() sx.String {
	inp := cp.inp
	pos := inp.Pos
	if inp.Ch == '\\' {
		cp.inp.Next()
		return cp.parseBackslashRest()
		inp.Next()
		return parseBackslashRest(inp)
	}
	for {
		switch inp.Next() {
		// 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', '[', ']', '{', '}', '(', ')', '|', '%', '_', '*', '>', '~', '^', ',', '"', '#', ':', '\'', '@', '`', runeModGrave, '$', '=', '\\', '-', '&':
			return sx.MakeString(string(inp.Src[pos:inp.Pos]))
		case input.EOS, '\n', '\r', '[', ']', '{', '}', '(', ')', '|', '%', '_', '*', '>', '~', '^', ',', '"', '#', ':', '\'', '`', runeModGrave, '$', '=', '\\', '-', '&':
			return string(inp.Src[pos:inp.Pos])
		}
	}
}

func (cp *zmkP) parseBackslash() *sx.Pair {
func parseBackslash(inp *input.Input) *sx.Pair {
	inp := cp.inp
	switch inp.Next() {
	case '\n', '\r':
		inp.EatEOL()
		return sx.MakeList(sz.SymHard)
		return sz.MakeHard()
	default:
		return sx.MakeList(sz.SymText, cp.parseBackslashRest())
		return sz.MakeText(parseBackslashRest(inp))
	}
}

func (cp *zmkP) parseBackslashRest() sx.String {
func parseBackslashRest(inp *input.Input) string {
	inp := cp.inp
	if input.IsEOLEOS(inp.Ch) {
		return sx.MakeString("\\")
		return "\\"
	}
	if inp.Ch == ' ' {
		inp.Next()
		return sx.MakeString("\u00a0")
		return "\u00a0"
	}
	pos := inp.Pos
	inp.Next()
	return sx.MakeString(string(inp.Src[pos:inp.Pos]))
	return string(inp.Src[pos:inp.Pos])
}

func (cp *zmkP) parseSoftBreak() *sx.Pair {
	cp.inp.EatEOL()
	return sx.MakeList(sz.SymSoft)
func parseSoftBreak(inp *input.Input) *sx.Pair {
	inp.EatEOL()
	return sz.MakeSoft()
}

func (cp *zmkP) parseLinkEmbed(openCh, closeCh rune, forLink bool) (*sx.Pair, bool) {
func (cp *zmkP) parseLink(openCh, closeCh rune) (*sx.Pair, bool) {
	if refString, text, ok := cp.parseReference(openCh, closeCh); ok {
		attrs := cp.parseInlineAttributes()
		attrs := parseInlineAttributes(cp.inp)
		if len(refString) > 0 {
			ref := ParseReference(refString)
			refSym, _ := sx.GetSymbol(ref.Car())
			sym := sz.MapRefStateToLinkEmbed(refSym, forLink)
			sym := sz.MapRefStateToLink(refSym)
			ln := text.
				Cons(ref.Tail().Car()). // reference value
			return sz.MakeLink(sym, attrs, ref.Tail().Car().(sx.String).GetValue(), text), true
				Cons(attrs).
				Cons(sym)
			return ln, true
		}
	}
	return nil, false
}
func (cp *zmkP) parseEmbed(openCh, closeCh rune) (*sx.Pair, bool) {
	if refString, text, ok := cp.parseReference(openCh, closeCh); ok {
		attrs := parseInlineAttributes(cp.inp)
		if len(refString) > 0 {
			return sz.MakeEmbed(attrs, ParseReference(refString), "", text), true
		}
	}
	return nil, false
}

func hasQueryPrefix(src []byte) bool {
	return len(src) > len(api.QueryPrefix) && string(src[:len(api.QueryPrefix)]) == api.QueryPrefix
}

func (cp *zmkP) parseReference(openCh, closeCh rune) (ref string, text *sx.Pair, _ bool) {
func (cp *zmkP) parseReference(openCh, closeCh rune) (string, *sx.Pair, bool) {
	inp := cp.inp
	inp.Next()
	inp.SkipSpace()
	if inp.Ch == openCh {
		// Additional opening chars result in a fail
		return "", nil, false
	}
	var is sx.Vector
	var lb sx.ListBuilder
	pos := inp.Pos
	if !hasQueryPrefix(inp.Src[pos:]) {
		hasSpace, ok := cp.readReferenceToSep(closeCh)
		hasSpace, ok := readReferenceToSep(inp, closeCh)
		if !ok {
			return "", nil, false
		}
		if inp.Ch == '|' { // First part must be inline text
			if pos == inp.Pos { // [[| or {{|
				return "", nil, false
			}
			cp.inp = input.NewInput(inp.Src[pos:inp.Pos])
			for {
				in := cp.parseInline()
				if in == nil {
					break
				}
				is = append(is, in)
				lb.Add(in)
			}
			cp.inp = inp
			inp.Next()
		} else {
			if hasSpace {
				return "", nil, false
			}
			inp.SetPos(pos)
		}
	}

	inp.SkipSpace()
	pos = inp.Pos
	if !cp.readReferenceToClose(closeCh) {
	if !readReferenceToClose(inp, closeCh) {
		return "", nil, false
	}
	ref = strings.TrimSpace(string(inp.Src[pos:inp.Pos]))
	ref := strings.TrimSpace(string(inp.Src[pos:inp.Pos]))
	if inp.Next() != closeCh {
		return "", nil, false
	}
	inp.Next()
	if len(is) == 0 {
		return ref, nil, true
	}
	return ref, lb.List(), true
}
	return ref, sx.MakeList(is...), true
}


func (cp *zmkP) readReferenceToSep(closeCh rune) (bool, bool) {
func readReferenceToSep(inp *input.Input, closeCh rune) (bool, bool) {
	hasSpace := false
	inp := cp.inp
	for {
		switch inp.Ch {
		case input.EOS:
			return false, false
		case '\n', '\r', ' ':
			hasSpace = true
		case '|':
229
230
231
232
233
234
235
236

237
238
239
240
241
242
243
244
226
227
228
229
230
231
232

233

234
235
236
237
238
239
240







-
+
-







			}
			continue
		}
		inp.Next()
	}
}

func (cp *zmkP) readReferenceToClose(closeCh rune) bool {
func readReferenceToClose(inp *input.Input, closeCh rune) bool {
	inp := cp.inp
	pos := inp.Pos
	for {
		switch inp.Ch {
		case input.EOS:
			return false
		case '\t', '\r', '\n', ' ':
			if !hasQueryPrefix(inp.Src[pos:]) {
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
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







-
-
+
+
-








-
-
+
+












-
+











-
-
-
-
-
-
+





-
+







-
+





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






-
+




-
-
+
-
-
-

















-
+











-
+







-
+
-
-
+

-
+




-
+





-
-
-
+
+





-
+
-



















-
+




-
-
+
+




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











-
+


-
+
-





-
+
-





-
+


-
-
-
+
+
+



	case ' ', ',', '|':
		inp.Next()
	}
	ins, ok := cp.parseLinkLikeRest()
	if !ok {
		return nil, false
	}
	attrs := cp.parseInlineAttributes()
	cn := ins.Cons(sx.MakeString(string(inp.Src[pos:posL]))).Cons(attrs).Cons(sz.SymCite)
	attrs := parseInlineAttributes(inp)
	return sz.MakeCite(attrs, string(inp.Src[pos:posL]), ins), true
	return cn, true
}

func (cp *zmkP) parseEndnote() (*sx.Pair, bool) {
	cp.inp.Next()
	ins, ok := cp.parseLinkLikeRest()
	if !ok {
		return nil, false
	}
	attrs := cp.parseInlineAttributes()
	return ins.Cons(attrs).Cons(sz.SymEndnote), true
	attrs := parseInlineAttributes(cp.inp)
	return sz.MakeEndnote(attrs, ins), true
}

func (cp *zmkP) parseMark() (*sx.Pair, 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]
	mark := string(inp.Src[pos:inp.Pos])
	var ins *sx.Pair
	if inp.Ch == '|' {
		inp.Next()
		var ok bool
		ins, ok = cp.parseLinkLikeRest()
		if !ok {
			return nil, false
		}
	} else {
		inp.Next()
	}
	mn := ins.
		Cons(sx.MakeString("")). // Fragment
		Cons(sx.MakeString("")). // Slug
		Cons(sx.MakeString(string(mark))).
		Cons(sz.SymMark)
	return mn, true
	return sz.MakeMark(mark, "", "", ins), true
	// Problematisch ist, dass hier noch nicht mn.Fragment und mn.Slug gesetzt werden.
	// Evtl. muss es ein PreMark-Symbol geben
}

func (cp *zmkP) parseLinkLikeRest() (*sx.Pair, bool) {
	var ins sx.Vector
	var ins sx.ListBuilder
	inp := cp.inp
	inp.SkipSpace()
	for inp.Ch != ']' {
		in := cp.parseInline()
		if in == nil {
			return nil, false
		}
		ins = append(ins, in)
		ins.Add(in)
		if input.IsEOLEOS(inp.Ch) && sz.IsBreakSym(in.Car()) {
			return nil, false
		}
	}
	inp.Next()
	if len(ins) == 0 {
		return nil, true
	}
	return ins.List(), true
}
	return sx.MakeList(ins...), true
}


func parseComment(inp *input.Input) (*sx.Pair, bool) {
func (cp *zmkP) parseComment() (res *sx.Pair, success bool) {
	inp := cp.inp
	if inp.Next() != '%' {
		return nil, false
	}
	for inp.Ch == '%' {
		inp.Next()
	}
	attrs := cp.parseInlineAttributes()
	attrs := parseInlineAttributes(inp)
	inp.SkipSpace()
	pos := inp.Pos
	for {
		if input.IsEOLEOS(inp.Ch) {
			return sx.MakeList(
				sz.SymLiteralComment,
			return sz.MakeLiteral(sz.SymLiteralComment, attrs, string(inp.Src[pos:inp.Pos])), true
				attrs,
				sx.MakeString(string(inp.Src[pos:inp.Pos])),
			), true
		}
		inp.Next()
	}
}

var mapRuneFormat = map[rune]*sx.Symbol{
	'_': sz.SymFormatEmph,
	'*': sz.SymFormatStrong,
	'>': sz.SymFormatInsert,
	'~': sz.SymFormatDelete,
	'^': sz.SymFormatSuper,
	',': sz.SymFormatSub,
	'"': sz.SymFormatQuote,
	'#': sz.SymFormatMark,
	':': sz.SymFormatSpan,
}

func (cp *zmkP) parseFormat() (res *sx.Pair, success bool) {
func (cp *zmkP) parseFormat() (*sx.Pair, bool) {
	inp := cp.inp
	fch := inp.Ch
	symFormat, ok := mapRuneFormat[fch]
	if !ok {
		panic(fmt.Sprintf("%q is not a formatting char", fch))
	}
	// read 2nd formatting character
	if inp.Next() != fch {
		return nil, false
	}
	inp.Next()
	var inlines sx.Vector
	var inlines sx.ListBuilder
	for {
		if inp.Ch == input.EOS {
			return nil, false
		}
		if inp.Ch == fch {
			if inp.Next() == fch {
				inp.Next()
				attrs := cp.parseInlineAttributes()
				attrs := parseInlineAttributes(inp)
				fn := sx.MakeList(inlines...).Cons(attrs).Cons(symFormat)
				return fn, true
				return sz.MakeFormat(symFormat, attrs, inlines.List()), true
			}
			inlines = append(inlines, sx.MakeList(sz.SymText, sx.MakeString(string(fch))))
			inlines.Add(sz.MakeText(string(fch)))
		} else if in := cp.parseInline(); in != nil {
			if input.IsEOLEOS(inp.Ch) && sz.IsBreakSym(in.Car()) {
				return nil, false
			}
			inlines = append(inlines, in)
			inlines.Add(in)
		}
	}
}

var mapRuneLiteral = map[rune]*sx.Symbol{
	'@':          sz.SymLiteralZettel,
	'`':          sz.SymLiteralProg,
	runeModGrave: sz.SymLiteralProg,
	'`':          sz.SymLiteralCode,
	runeModGrave: sz.SymLiteralCode,
	'\'':         sz.SymLiteralInput,
	'=':          sz.SymLiteralOutput,
	// No '$': sz.SymLiteralMath, because pairing literal math is a little different
}

func (cp *zmkP) parseLiteral() (res *sx.Pair, success bool) {
func parseLiteral(inp *input.Input) (*sx.Pair, bool) {
	inp := cp.inp
	fch := inp.Ch
	symLiteral, ok := mapRuneLiteral[fch]
	if !ok {
		panic(fmt.Sprintf("%q is not a formatting char", fch))
	}
	// read 2nd formatting character
	if inp.Next() != fch {
		return nil, false
	}
	inp.Next()
	var sb strings.Builder
	for {
		if inp.Ch == input.EOS {
			return nil, false
		}
		if inp.Ch == fch {
			if inp.Peek() == fch {
				inp.Next()
				inp.Next()
				return createLiteralNode(symLiteral, cp.parseInlineAttributes(), sb.String()), true
				return sz.MakeLiteral(symLiteral, parseInlineAttributes(inp), sb.String()), true
			}
			sb.WriteRune(fch)
			inp.Next()
		} else {
			s := cp.parseString()
			sb.WriteString(s.GetValue())
			s := parseString(inp)
			sb.WriteString(s)
		}
	}
}

func createLiteralNode(sym *sx.Symbol, attrs *sx.Pair, content string) *sx.Pair {
	if sym.IsEqualSymbol(sz.SymLiteralZettel) {
		if p := attrs.Assoc(sx.MakeString("")); p != nil {
			if val, isString := sx.GetString(p.Cdr()); isString && val.GetValue() == api.ValueSyntaxHTML {
				sym = sz.SymLiteralHTML
				attrs = attrs.RemoveAssoc(sx.MakeString(""))
			}
		}
	}
	return sx.MakeList(sym, attrs, sx.MakeString(content))
}

func (cp *zmkP) parseLiteralMath() (res *sx.Pair, success bool) {
func parseLiteralMath(inp *input.Input) (res *sx.Pair, success bool) {
	inp := cp.inp
	// read 2nd formatting character
	if inp.Next() != '$' {
		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]...)
			content := slices.Clone(inp.Src[pos:inp.Pos])
			inp.Next()
			inp.Next()
			fn := sx.MakeList(sz.SymLiteralMath, cp.parseInlineAttributes(), sx.MakeString(string(content)))
			return sz.MakeLiteral(sz.SymLiteralMath, parseInlineAttributes(inp), string(content)), true
			return fn, true
		}
		inp.Next()
	}
}

func (cp *zmkP) parseNdash() (res *sx.Pair, success bool) {
func parseNdash(inp *input.Input) (*sx.Pair, bool) {
	inp := cp.inp
	if inp.Peek() != inp.Ch {
		return nil, false
	}
	inp.Next()
	inp.Next()
	return sx.MakeList(sz.SymText, sx.MakeString("\u2013")), true
	return sz.MakeText("\u2013"), true
}

func (cp *zmkP) parseEntity() (res *sx.Pair, success bool) {
	if text, ok := cp.inp.ScanEntity(); ok {
		return sx.MakeList(sz.SymText, sx.MakeString(text)), true
func parseEntity(inp *input.Input) (*sx.Pair, bool) {
	if text, ok := inp.ScanEntity(); ok {
		return sz.MakeText(text), true
	}
	return nil, false
}

Changes to sz/zmk/post-processor.go.

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







-
+

-
+






-
+

-
+

+
+



-
-
+
+


















-
+







)

var symInVerse = sx.MakeSymbol("in-verse")
var symNoBlock = sx.MakeSymbol("no-block")

type postProcessor struct{}

func (pp *postProcessor) Visit(lst *sx.Pair, env *sx.Pair) sx.Object {
func (pp *postProcessor) VisitBefore(lst *sx.Pair, env *sx.Pair) (sx.Object, bool) {
	if lst == nil {
		return nil
		return nil, true
	}
	sym, isSym := sx.GetSymbol(lst.Car())
	if !isSym {
		panic(lst)
	}
	if fn, found := symMap[sym]; found {
		return fn(pp, lst, env)
		return fn(pp, lst, env), true
	}
	return sx.Int64(0)
	return nil, false
}

func (pp *postProcessor) VisitAfter(lst *sx.Pair, _ *sx.Pair) sx.Object { return lst }

func (pp *postProcessor) visitPairList(lst *sx.Pair, env *sx.Pair) *sx.Pair {
	var pList sx.ListBuilder
	for node := lst; node != nil; node = node.Tail() {
		if elem := sz.Walk(pp, node.Head(), env); elem != nil {
	for node := range lst.Pairs() {
		if elem, isPair := sx.GetPair(sz.Walk(pp, node.Head(), env)); isPair && elem != nil {
			pList.Add(elem)
		}
	}
	return pList.List()
}

var symMap map[*sx.Symbol]func(*postProcessor, *sx.Pair, *sx.Pair) *sx.Pair

func init() {
	symMap = map[*sx.Symbol]func(*postProcessor, *sx.Pair, *sx.Pair) *sx.Pair{
		sz.SymBlock:           postProcessBlockList,
		sz.SymPara:            postProcessInlineList,
		sz.SymRegionBlock:     postProcessRegion,
		sz.SymRegionQuote:     postProcessRegion,
		sz.SymRegionVerse:     postProcessRegionVerse,
		sz.SymVerbatimComment: postProcessVerbatim,
		sz.SymVerbatimEval:    postProcessVerbatim,
		sz.SymVerbatimMath:    postProcessVerbatim,
		sz.SymVerbatimProg:    postProcessVerbatim,
		sz.SymVerbatimCode:    postProcessVerbatim,
		sz.SymVerbatimZettel:  postProcessVerbatim,
		sz.SymHeading:         postProcessHeading,
		sz.SymListOrdered:     postProcessItemList,
		sz.SymListUnordered:   postProcessItemList,
		sz.SymListQuote:       postProcessQuoteList,
		sz.SymDescription:     postProcessDescription,
		sz.SymTable:           postProcessTable,
80
81
82
83
84
85
86
87

88
89
90
91
92
93
94
82
83
84
85
86
87
88

89
90
91
92
93
94
95
96







-
+







		sz.SymLinkExternal: postProcessInlines4,
		sz.SymLinkFound:    postProcessInlines4,
		sz.SymLinkHosted:   postProcessInlines4,
		sz.SymLinkInvalid:  postProcessInlines4,
		sz.SymLinkQuery:    postProcessInlines4,
		sz.SymLinkSelf:     postProcessInlines4,
		sz.SymLinkZettel:   postProcessInlines4,
		sz.SymEmbed:        postProcessInlines4,
		sz.SymEmbed:        postProcessEmbed,
		sz.SymCite:         postProcessInlines4,
		sz.SymFormatDelete: postProcessFormat,
		sz.SymFormatEmph:   postProcessFormat,
		sz.SymFormatInsert: postProcessFormat,
		sz.SymFormatMark:   postProcessFormat,
		sz.SymFormatQuote:  postProcessFormat,
		sz.SymFormatStrong: postProcessFormat,
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
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







-
-
+

-
+






-
+










-

-
+

-
+

-
+

-
+

-
+







}

func postProcessRegionVerse(pp *postProcessor, rn *sx.Pair, env *sx.Pair) *sx.Pair {
	return doPostProcessRegion(pp, rn, env.Cons(sx.Cons(symInVerse, nil)), env)
}

func doPostProcessRegion(pp *postProcessor, rn *sx.Pair, envBlock, envInline *sx.Pair) *sx.Pair {

	sym := rn.Car()
	sym := rn.Car().(*sx.Symbol)
	next := rn.Tail()
	attrs := next.Car()
	attrs := next.Car().(*sx.Pair)
	next = next.Tail()
	blocks := pp.visitPairList(next.Head(), envBlock)
	text := pp.visitInlines(next.Tail(), envInline)
	if blocks == nil && text == nil {
		return nil
	}
	return text.Cons(blocks).Cons(attrs).Cons(sym)
	return sz.MakeRegion(sym, attrs, blocks, text)
}

func postProcessVerbatim(_ *postProcessor, verb *sx.Pair, _ *sx.Pair) *sx.Pair {
	if content, isString := sx.GetString(verb.Tail().Tail().Car()); isString && content.GetValue() != "" {
		return verb
	}
	return nil
}

func postProcessHeading(pp *postProcessor, hn *sx.Pair, env *sx.Pair) *sx.Pair {
	sym := hn.Car()
	next := hn.Tail()
	level := next.Car()
	level := next.Car().(sx.Int64)
	next = next.Tail()
	attrs := next.Car()
	attrs := next.Car().(*sx.Pair)
	next = next.Tail()
	slug := next.Car()
	slug := next.Car().(sx.String)
	next = next.Tail()
	fragment := next.Car()
	fragment := next.Car().(sx.String)
	if text := pp.visitInlines(next.Tail(), env); text != nil {
		return text.Cons(fragment).Cons(slug).Cons(attrs).Cons(level).Cons(sym)
		return sz.MakeHeading(int(level), attrs, text, slug.GetValue(), fragment.GetValue())
	}
	return nil
}

func postProcessItemList(pp *postProcessor, ln *sx.Pair, env *sx.Pair) *sx.Pair {
	elems := pp.visitListElems(ln, env)
	if elems == nil {
180
181
182
183
184
185
186

187
188
189

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

190
191
192
193
194
195
196
197







+


-
+








	var newElems sx.ListBuilder
	var newPara sx.ListBuilder

	addtoParagraph := func() {
		if !newPara.IsEmpty() {
			newElems.Add(sx.MakeList(sz.SymBlock, newPara.List().Cons(sz.SymPara)))
			newPara.Reset()
		}
	}
	for node := elems; node != nil; node = node.Tail() {
	for node := range elems.Pairs() {
		item := node.Head()
		if !item.Car().IsEqual(sz.SymBlock) {
			continue
		}
		itemTail := item.Tail()
		if itemTail == nil || itemTail.Tail() != nil {
			addtoParagraph()
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
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







-
+










-
+







	}
	addtoParagraph()
	return newElems.List().Cons(ln.Car())
}

func (pp *postProcessor) visitListElems(ln *sx.Pair, env *sx.Pair) *sx.Pair {
	var pList sx.ListBuilder
	for node := ln.Tail(); node != nil; node = node.Tail() {
	for node := range ln.Tail().Pairs() {
		if elem := sz.Walk(pp, node.Head(), env); elem != nil {
			pList.Add(elem)
		}
	}
	return pList.List()
}

func postProcessDescription(pp *postProcessor, dl *sx.Pair, env *sx.Pair) *sx.Pair {
	var dList sx.ListBuilder
	isTerm := false
	for node := dl.Tail(); node != nil; node = node.Tail() {
	for node := range dl.Tail().Pairs() {
		isTerm = !isTerm
		if isTerm {
			dList.Add(pp.visitInlines(node.Head(), env))
		} else {
			dList.Add(sz.Walk(pp, node.Head(), env))
		}
	}
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
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







-
+








-
+













-
+
















-
+
-

+







	rows, width := pp.visitRows(next.Tail(), env)
	if rows == nil {
		// Header and row are nil -> no table
		return nil
	}
	header, rows, align := splitTableHeader(rows, width)
	alignRow(header, align)
	for node := rows; node != nil; node = node.Tail() {
	for node := range rows.Pairs() {
		alignRow(node.Head(), align)
	}
	return rows.Cons(header).Cons(sym)
}

func (pp *postProcessor) visitRows(rows *sx.Pair, env *sx.Pair) (*sx.Pair, int) {
	maxWidth := 0
	var pRows sx.ListBuilder
	for node := rows; node != nil; node = node.Tail() {
	for node := range rows.Pairs() {
		row := node.Head()
		row, width := pp.visitCells(row, env)
		if maxWidth < width {
			maxWidth = width
		}
		pRows.Add(row)
	}
	return pRows.List(), maxWidth
}

func (pp *postProcessor) visitCells(cells *sx.Pair, env *sx.Pair) (*sx.Pair, int) {
	width := 0
	var pCells sx.ListBuilder
	for node := cells; node != nil; node = node.Tail() {
	for node := range cells.Pairs() {
		cell := node.Head()
		ins := pp.visitInlines(cell.Tail(), env)
		newCell := ins.Cons(cell.Car())
		pCells.Add(newCell)
		width++
	}
	return pCells.List(), width
}

func splitTableHeader(rows *sx.Pair, width int) (header, realRows *sx.Pair, align []*sx.Symbol) {
	align = make([]*sx.Symbol, width)

	foundHeader := false
	cellCount := 0

	// assert: rows != nil (checked in postProcessTable)
	for node := rows.Head(); node != nil; node = node.Tail() {
	for node := range rows.Head().Pairs() {
		cellCount++
		cell := node.Head()
		cellCount++
		cellTail := cell.Tail()
		if cellTail == nil {
			continue
		}

		// elem is first cell inline element
		elem := cellTail.Head()
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
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







-
+





-
+













-
+







				align[cellCount-1] = cellAlign
				cell.SetCar(cellAlign)
			}
		}
	}

	if !foundHeader {
		for i := 0; i < width; i++ {
		for i := range width {
			align[i] = sz.SymCell // Default alignment
		}
		return nil, rows, align
	}

	for i := 0; i < width; i++ {
	for i := range width {
		if align[i] == nil {
			align[i] = sz.SymCell // Default alignment
		}
	}
	return rows.Head(), rows.Tail(), align
}

func alignRow(row *sx.Pair, align []*sx.Symbol) {
	if row == nil {
		return
	}
	var lastCellNode *sx.Pair
	cellCount := 0
	for node := row; node != nil; node = node.Tail() {
	for node := range row.Pairs() {
		lastCellNode = node
		cell := node.Head()
		cell.SetCar(align[cellCount])
		cellCount++
		cellTail := cell.Tail()
		if cellTail == nil {
			continue
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
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







-
-
-
+
+
+



+
+
+
+
+
+
+
+
+
+


-
+




-







	length := lst.Length()
	if length <= 0 {
		return nil
	}
	inVerse := env.Assoc(symInVerse) != nil
	vector := make([]*sx.Pair, 0, length)
	// 1st phase: process all childs, ignore ' ' / '\t' at start, and merge some elements
	for node := lst; node != nil; node = node.Tail() {
		elem := sz.Walk(pp, node.Head(), env)
		if elem == nil {
	for node := range lst.Pairs() {
		elem, isPair := sx.GetPair(sz.Walk(pp, node.Head(), env))
		if !isPair || elem == nil {
			continue
		}
		elemSym := elem.Car()
		elemTail := elem.Tail()

		if inVerse && elemSym.IsEqual(sz.SymText) {
			if s, isString := sx.GetString(elemTail.Car()); isString {
				verseText := s.GetValue()
				verseText = strings.ReplaceAll(verseText, " ", "\u00a0")
				elemTail.SetCar(sx.MakeString(verseText))
			}
		}

		if len(vector) == 0 {
			// If the 1st element is a TEXT, remove all ' ', '\t' at the beginning, if outside a verse block.
			if inVerse || !elemSym.IsEqual(sz.SymText) {
			if !elemSym.IsEqual(sz.SymText) {
				vector = append(vector, elem)
				continue
			}

			elemTail := elem.Tail()
			elemText := elemTail.Car().(sx.String).GetValue()
			if elemText != "" && (elemText[0] == ' ' || elemText[0] == '\t') {
				for elemText != "" {
					if ch := elemText[0]; ch != ' ' && ch != '\t' {
						break
					}
					elemText = elemText[1:]
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
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







-

-
+

-
+

-
+



-

-
+

-
+

-
+

-
+












+
+
+
+
+
+
+
+
+
+
+

-
+

-
+





-
+

	if env.Assoc(symInVerse) == nil {
		return sn
	}
	return sx.Cons(sz.SymHard, nil)
}

func postProcessEndnote(pp *postProcessor, en *sx.Pair, env *sx.Pair) *sx.Pair {
	sym := en.Car()
	next := en.Tail()
	attrs := next.Car()
	attrs := next.Car().(*sx.Pair)
	if text := pp.visitInlines(next.Tail(), env); text != nil {
		return text.Cons(attrs).Cons(sym)
		return sz.MakeEndnote(attrs, text)
	}
	return sx.MakeList(sym, attrs)
	return sz.MakeEndnote(attrs, sx.Nil())
}

func postProcessMark(pp *postProcessor, en *sx.Pair, env *sx.Pair) *sx.Pair {
	sym := en.Car()
	next := en.Tail()
	mark := next.Car()
	mark := next.Car().(sx.String)
	next = next.Tail()
	slug := next.Car()
	slug := next.Car().(sx.String)
	next = next.Tail()
	fragment := next.Car()
	fragment := next.Car().(sx.String)
	text := pp.visitInlines(next.Tail(), env)
	return text.Cons(fragment).Cons(slug).Cons(mark).Cons(sym)
	return sz.MakeMark(mark.GetValue(), slug.GetValue(), fragment.GetValue(), text)
}

func postProcessInlines4(pp *postProcessor, ln *sx.Pair, env *sx.Pair) *sx.Pair {
	sym := ln.Car()
	next := ln.Tail()
	attrs := next.Car()
	next = next.Tail()
	val3 := next.Car()
	text := pp.visitInlines(next.Tail(), env)
	return text.Cons(val3).Cons(attrs).Cons(sym)
}

func postProcessEmbed(pp *postProcessor, ln *sx.Pair, env *sx.Pair) *sx.Pair {
	next := ln.Tail()
	attrs := next.Car().(*sx.Pair)
	next = next.Tail()
	ref := next.Car()
	next = next.Tail()
	syntax := next.Car().(sx.String)
	text := pp.visitInlines(next.Tail(), env)
	return sz.MakeEmbed(attrs, ref, syntax.GetValue(), text)
}

func postProcessFormat(pp *postProcessor, fn *sx.Pair, env *sx.Pair) *sx.Pair {
	symFormat := fn.Car()
	symFormat := fn.Car().(*sx.Symbol)
	next := fn.Tail() // Attrs
	attrs := next.Car()
	attrs := next.Car().(*sx.Pair)
	next = next.Tail() // Possible inlines
	if next == nil {
		return fn
	}
	inlines := pp.visitInlines(next, env)
	return inlines.Cons(attrs).Cons(symFormat)
	return sz.MakeFormat(symFormat, attrs, inlines)
}

Changes to sz/zmk/ref.go.

15
16
17
18
19
20
21

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







+








import (
	"net/url"
	"strings"

	"t73f.de/r/sx"
	"t73f.de/r/zsc/api"
	"t73f.de/r/zsc/domain/id"
	"t73f.de/r/zsc/sz"
)

// ParseReference parses a string and returns a reference.
func ParseReference(s string) *sx.Pair {
	if invalidReference(s) {
		return makePairRef(sz.SymRefStateInvalid, s)
40
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







-
+
-







		}
	}
	u, err := url.Parse(s)
	if err != nil {
		return makePairRef(sz.SymRefStateInvalid, s)
	}
	if !externalURL(u) {
		zid := api.ZettelID(u.Path)
		if _, err = id.Parse(u.Path); err == nil {
		if zid.IsValid() {
			return makePairRef(sz.SymRefStateZettel, s)
		}
		if u.Path == "" && u.Fragment != "" {
			return makePairRef(sz.SymRefStateSelf, s)
		}
	}
	return makePairRef(sz.SymRefStateExternal, s)

Changes to sz/zmk/zmk.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
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27


28
29
30
31
32
33
34
35
36







+









-
-
+
+







// SPDX-FileCopyrightText: 2020-present Detlef Stern
//-----------------------------------------------------------------------------

// Package zmk provides a parser for zettelmarkup.
package zmk

import (
	"maps"
	"slices"
	"strings"
	"unicode"

	"t73f.de/r/sx"
	"t73f.de/r/zsc/input"
	"t73f.de/r/zsc/sz"
)

// ParseBlocks tries to parse the input as a block element.
func ParseBlocks(inp *input.Input) *sx.Pair {
// Parse tries to parse the input as a block element.
func Parse(inp *input.Input) *sx.Pair {
	parser := zmkP{inp: inp}

	var lastPara *sx.Pair
	var blkBuild sx.ListBuilder
	for inp.Ch != input.EOS {
		bn, cont := parser.parseBlock(lastPara)
		if bn != nil {
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
52
53
54
55
56
57
58

















59
60
61
62
63
64


65
66
67
68
69
70
71







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






-
-







	var pp postProcessor
	if bs := pp.visitPairList(bnl, nil); bs != nil {
		return bs.Cons(sz.SymBlock)
	}
	return nil
}

// ParseInlines tries to parse the input as an inline element.
func ParseInlines(inp *input.Input) *sx.Pair {
	parser := zmkP{inp: inp}
	var ins sx.Vector
	for inp.Ch != input.EOS {
		in := parser.parseInline()
		if in == nil {
			break
		}
		ins = append(ins, in)
	}

	inl := ins.MakeList().Cons(sz.SymInline)
	var pp postProcessor
	return sz.Walk(&pp, inl, nil)
}

type zmkP struct {
	inp          *input.Input // Input stream
	lists        []*sx.Pair   // Stack of lists
	lastRow      *sx.Pair     // Last row of table, or nil if not in table.
	descrl       *sx.Pair     // Current description list
	nestingLevel int          // Count nesting of block and inline elements

	inVerse bool // Currently in a vers region?
}

// runeModGrave is Unicode code point U+02CB (715) called "MODIFIER LETTER
// GRAVE ACCENT". On the iPad it is much more easier to type in this code point
// than U+0060 (96) "Grave accent" (aka backtick). Therefore, U+02CB will be
// considered equivalent to U+0060.
const runeModGrave = 'Ë‹' // This is NOT '`'!
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
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







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

-
+


-
+
-












-
+


-
+
-

-
+














-
+
-







		attrs[key] = prevVal + " " + val
	} else {
		attrs[key] = val
	}
}

func (attrs attrMap) asPairAssoc() *sx.Pair {
	names := make([]string, 0, len(attrs))
	for n := range attrs {
	var lb sx.ListBuilder
		names = append(names, n)
	}
	slices.Sort(names)
	for _, key := range slices.Sorted(maps.Keys(attrs)) {
	var assoc *sx.Pair
	for i := len(names) - 1; i >= 0; i-- {
		n := names[i]
		assoc = assoc.Cons(sx.Cons(sx.MakeString(n), sx.MakeString(attrs[n])))
		lb.Add(sx.Cons(sx.MakeString(key), sx.MakeString(attrs[key])))
	}
	return assoc
	return lb.List()
}

func (cp *zmkP) parseNormalAttribute(attrs attrMap) bool {
func parseNormalAttribute(inp *input.Input, attrs attrMap) 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
	}
	return cp.parseAttributeValue(key, attrs)
	return parseAttributeValue(inp, key, attrs)
}

func (cp *zmkP) parseAttributeValue(key string, attrs attrMap) bool {
func parseAttributeValue(inp *input.Input, key string, attrs attrMap) bool {
	inp := cp.inp
	if inp.Next() == '"' {
		return cp.parseQuotedAttributeValue(key, attrs)
		return parseQuotedAttributeValue(inp, key, attrs)
	}
	posV := inp.Pos
	for {
		switch inp.Ch {
		case input.EOS:
			return false
		case '\n', '\r', ' ', ',', '}':
			attrs.updateAttrs(key, string(inp.Src[posV:inp.Pos]))
			return true
		}
		inp.Next()
	}
}

func (cp *zmkP) parseQuotedAttributeValue(key string, attrs attrMap) bool {
func parseQuotedAttributeValue(inp *input.Input, key string, attrs attrMap) bool {
	inp := cp.inp
	inp.Next()
	var sb strings.Builder
	for {
		switch inp.Ch {
		case input.EOS:
			return false
		case '"':
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
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







-
+
-










-
+


-
+
-

-
+







-
+
-





-
+






-
+
-

-
+

















-
+



-
+
















-
-
+
+







			sb.WriteRune(inp.Ch)
			inp.Next()
		}
	}

}

func (cp *zmkP) parseBlockAttributes() *sx.Pair {
func parseBlockAttributes(inp *input.Input) *sx.Pair {
	inp := cp.inp
	pos := inp.Pos
	for isNameRune(inp.Ch) {
		inp.Next()
	}
	if pos < inp.Pos {
		return attrMap{"": string(inp.Src[pos:inp.Pos])}.asPairAssoc()
	}

	// No immediate name: skip spaces
	inp.SkipSpace()
	return cp.parseInlineAttributes()
	return parseInlineAttributes(inp)
}

func (cp *zmkP) parseInlineAttributes() *sx.Pair {
func parseInlineAttributes(inp *input.Input) *sx.Pair {
	inp := cp.inp
	pos := inp.Pos
	if attrs, success := cp.doParseAttributes(); success {
	if attrs, success := doParseAttributes(inp); success {
		return attrs
	}
	inp.SetPos(pos)
	return nil
}

// doParseAttributes reads attributes.
func (cp *zmkP) doParseAttributes() (res *sx.Pair, success bool) {
func doParseAttributes(inp *input.Input) (*sx.Pair, bool) {
	inp := cp.inp
	if inp.Ch != '{' {
		return nil, false
	}
	inp.Next()
	a := attrMap{}
	if !cp.parseAttributeValues(a) {
	if !parseAttributeValues(inp, a) {
		return nil, false
	}
	inp.Next()
	return a.asPairAssoc(), true
}

func (cp *zmkP) parseAttributeValues(a attrMap) bool {
func parseAttributeValues(inp *input.Input, a attrMap) bool {
	inp := cp.inp
	for {
		cp.skipSpaceLine()
		skipSpaceLine(inp)
		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
			}
			a.updateAttrs("class", string(inp.Src[posC:inp.Pos]))
		case '=':
			delete(a, "")
			if !cp.parseAttributeValue("", a) {
			if !parseAttributeValue(inp, "", a) {
				return false
			}
		default:
			if !cp.parseNormalAttribute(a) {
			if !parseNormalAttribute(inp, a) {
				return false
			}
		}

		switch inp.Ch {
		case '}':
			return true
		case '\n', '\r':
		case ' ', ',':
			inp.Next()
		default:
			return false
		}
	}
}

func (cp *zmkP) skipSpaceLine() {
	for inp := cp.inp; ; {
func skipSpaceLine(inp *input.Input) {
	for {
		switch inp.Ch {
		case ' ':
			inp.Next()
		case '\n', '\r':
			inp.EatEOL()
		default:
			return

Changes to sz/zmk/zmk_fuzz_test.go.

20
21
22
23
24
25
26
27

28
29
20
21
22
23
24
25
26

27
28
29







-
+


	"t73f.de/r/zsc/sz/zmk"
)

func FuzzParseBlocks(f *testing.F) {
	f.Fuzz(func(t *testing.T, src []byte) {
		t.Parallel()
		inp := input.NewInput(src)
		zmk.ParseBlocks(inp)
		zmk.Parse(inp)
	})
}

Changes to sz/zmk/zmk_test.go.

43
44
45
46
47
48
49
50

51
52
53
54
55

56

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

77

78
79
80
81
82
83
84
85
86
87
88







89
90
91
92
93
94
95
96
97
98
99






100
101

102
103

104
105
106
107
108
109
110
111







112
113
114
115
116
117
118
119
120
121
122





123
124
125
126
127
128
129
130



131
132
133
134
135
136
137
138
139
140



141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196

















































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




































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















258
259

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









275
276

277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299

















300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321

















322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344





345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362














363
364

365
366
367
368
369
370
371
372





373
374
375
376
377
378
379
380
381
382
383
384
385
386













387
388
389
390
391




392
393
394
395
396
397
398

399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419

















420
421
422
423
424
425
426






427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
















453
454
455
456
457
458
459
460
461
462





463
464
465

466
467
468
469
470
471
472
473
474



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










490
491
492
493
494
495
496
497
498







499
500
501
502
503
504

505
506
507
508
509
510
511
512
513

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

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

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

550
551
552
553
554
555
556
557
558

559
560

561
562
563
564
565
566

567
568
569
570
571
572
573
574
575
576
577
578
579

580
581
582
583
584
585
586
587
588
589
590
591
592

593
594
595
596
597
598
599
600
601
602
603




604
605
606
607
608
609

610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634





635
636
637
638
639
640
641
642
643

644
645
646
647
648
649
650
651
652


653
654
655
656
657
658
659
660

661
662
663
664
665
666
667
668
669

670
671
672
673
674
675
676
43
44
45
46
47
48
49

50
51
52
53
54
55
56

57
58
59
60
61
62
63
64
65









66
67
68
69

70
71
72
73








74
75
76
77
78
79
80

81
82
83
84






85
86
87
88
89
90
91

92
93

94
95







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






108
109
110
111
112

113
114
115
116



117
118
119
120
121
122
123
124
125
126



127
128
129
130
131
132
133
134
135
136

















































137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190




































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















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

248
249
250
251
252
253
254









255
256
257
258
259
260
261
262
263
264

265
266
267
268
269
270
271

















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

















294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328





329
330
331
332
333
334
335
336
337














338
339
340
341
342
343
344
345
346
347
348
349
350
351
352

353
354
355
356





357
358
359
360
361
362













363
364
365
366
367
368
369
370
371
372
373
374
375
376




377
378
379
380
381
382
383
384
385


386
387
388
389
390

















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





410
411
412
413
414
415





416
417
418
419
420
















421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441





442
443
444
445
446
447
448

449
450
451
452
453
454
455



456
457
458
459
460
461
462
463










464
465
466
467
468
469
470
471
472
473
474
475







476
477
478
479
480
481
482
483
484
485
486
487

488
489
490
491
492
493
494
495
496

497
498
499
500
501
502
503
504
505
506
507
508

509
510
511
512
513
514
515
516
517
518
519
520

521
522
523
524
525
526
527
528
529
530
531
532

533
534
535
536
537
538
539
540
541

542
543

544
545
546
547
548
549

550
551
552
553
554
555
556
557
558
559
560
561
562

563
564
565
566
567
568
569
570
571
572
573
574
575

576
577
578
579
580
581
582
583
584



585
586
587
588
589
590
591
592
593

594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614





615
616
617
618
619
620
621
622
623
624
625
626
627

628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646

647
648
649
650
651
652
653
654
655

656
657
658
659
660
661
662
663







-
+





+
-
+








-
-
-
-
-
-
-
-
-



+
-
+



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




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

-
+

-
+

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





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




-
-
-
+
+
+







-
-
-
+
+
+







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





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





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

-
+






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

-
+






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





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


















-
-
-
-
-
+
+
+
+
+




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

-
+



-
-
-
-
-
+
+
+
+
+

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

-
-
-
-
+
+
+
+





-
-
+




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


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





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





-
-
-
-
-
+
+
+
+
+


-
+






-
-
-
+
+
+





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


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





-
+








-
+











-
+











-
+











-
+








-
+

-
+





-
+












-
+












-
+








-
-
-
+
+
+
+





-
+




















-
-
-
-
-
+
+
+
+
+








-
+









+
+







-
+








-
+







		}
		want = strings.ReplaceAll(want, "$", s)
		testCases = append(testCases, TestCase{source, want})
	}
	return testCases
}

func checkTcs(t *testing.T, isBlock bool, tcs TestCases) {
func checkTcs(t *testing.T, tcs TestCases) {
	t.Helper()

	for tcn, tc := range tcs {
		t.Run(fmt.Sprintf("TC=%02d,src=%q", tcn, tc.source), func(st *testing.T) {
			st.Helper()
			inp := input.NewInput([]byte(tc.source))
			ast := parseInput(tc.source, isBlock)
			ast := zmk.Parse(inp)
			sz.Walk(astWalker{}, ast, nil)
			got := ast.String()
			if tc.want != got {
				st.Errorf("\nwant=%q\n got=%q", tc.want, got)
			}
		})
	}
}
func parseInput(src string, asBlock bool) *sx.Pair {
	inp := input.NewInput([]byte(src))
	if asBlock {
		bl := zmk.ParseBlocks(inp)
		return bl
	}
	il := zmk.ParseInlines(inp)
	return il
}

type astWalker struct{}

func (astWalker) VisitBefore(node *sx.Pair, env *sx.Pair) (sx.Object, bool) { return sx.Nil(), false }
func (astWalker) Visit(node *sx.Pair, env *sx.Pair) sx.Object { return sx.MakeBoolean(true) }
func (astWalker) VisitAfter(node *sx.Pair, env *sx.Pair) sx.Object          { return node }

func TestEOL(t *testing.T) {
	t.Parallel()
	for _, isBlock := range []bool{true, false} {
		checkTcs(t, isBlock, TestCases{
			{"", "()"},
			{"\n", "()"},
			{"\r", "()"},
			{"\r\n", "()"},
			{"\n\n", "()"},
		})
	checkTcs(t, TestCases{
		{"", "()"},
		{"\n", "()"},
		{"\r", "()"},
		{"\r\n", "()"},
		{"\n\n", "()"},
	})
	}
}

func TestText(t *testing.T) {
	t.Parallel()
	checkTcs(t, false, TestCases{
		{"abcd", "(INLINE (TEXT \"abcd\"))"},
		{"ab cd", "(INLINE (TEXT \"ab cd\"))"},
		{"abcd ", "(INLINE (TEXT \"abcd\"))"},
		{" abcd", "(INLINE (TEXT \"abcd\"))"},
		{"\\", "(INLINE (TEXT \"\\\\\"))"},
	checkTcs(t, TestCases{
		{"abcd", "(BLOCK (PARA (TEXT \"abcd\")))"},
		{"ab cd", "(BLOCK (PARA (TEXT \"ab cd\")))"},
		{"abcd ", "(BLOCK (PARA (TEXT \"abcd\")))"},
		{" abcd", "(BLOCK (PARA (TEXT \"abcd\")))"},
		{"\\", "(BLOCK (PARA (TEXT \"\\\\\")))"},
		{"\\\n", "()"},
		{"\\\ndef", "(INLINE (HARD) (TEXT \"def\"))"},
		{"\\\ndef", "(BLOCK (PARA (HARD) (TEXT \"def\")))"},
		{"\\\r", "()"},
		{"\\\rdef", "(INLINE (HARD) (TEXT \"def\"))"},
		{"\\\rdef", "(BLOCK (PARA (HARD) (TEXT \"def\")))"},
		{"\\\r\n", "()"},
		{"\\\r\ndef", "(INLINE (HARD) (TEXT \"def\"))"},
		{"\\a", "(INLINE (TEXT \"a\"))"},
		{"\\aa", "(INLINE (TEXT \"aa\"))"},
		{"a\\a", "(INLINE (TEXT \"aa\"))"},
		{"\\+", "(INLINE (TEXT \"+\"))"},
		{"\\ ", "(INLINE (TEXT \"\u00a0\"))"},
		{"http://a, http://b", "(INLINE (TEXT \"http://a, http://b\"))"},
		{"\\\r\ndef", "(BLOCK (PARA (HARD) (TEXT \"def\")))"},
		{"\\a", "(BLOCK (PARA (TEXT \"a\")))"},
		{"\\aa", "(BLOCK (PARA (TEXT \"aa\")))"},
		{"a\\a", "(BLOCK (PARA (TEXT \"aa\")))"},
		{"\\+", "(BLOCK (PARA (TEXT \"+\")))"},
		{"\\ ", "(BLOCK (PARA (TEXT \"\u00a0\")))"},
		{"http://a, http://b", "(BLOCK (PARA (TEXT \"http://a, http://b\")))"},
	})
}

func TestSpace(t *testing.T) {
	t.Parallel()
	for _, isBlock := range []bool{true, false} {
		checkTcs(t, isBlock, TestCases{
			{" ", "()"},
			{"\t", "()"},
			{"  ", "()"},
		})
	checkTcs(t, TestCases{
		{" ", "()"},
		{"\t", "()"},
		{"  ", "()"},
	})
	}
}

func TestSoftBreak(t *testing.T) {
	t.Parallel()
	checkTcs(t, false, TestCases{
		{"x\ny", "(INLINE (TEXT \"x\") (SOFT) (TEXT \"y\"))"},
		{"z\n", "(INLINE (TEXT \"z\"))"},
	checkTcs(t, TestCases{
		{"x\ny", "(BLOCK (PARA (TEXT \"x\") (SOFT) (TEXT \"y\")))"},
		{"z\n", "(BLOCK (PARA (TEXT \"z\")))"},
		{" \n ", "()"},
		{" \n", "()"},
	})
}

func TestHardBreak(t *testing.T) {
	t.Parallel()
	checkTcs(t, false, TestCases{
		{"x  \ny", "(INLINE (TEXT \"x\") (HARD) (TEXT \"y\"))"},
		{"z  \n", "(INLINE (TEXT \"z\"))"},
	checkTcs(t, TestCases{
		{"x  \ny", "(BLOCK (PARA (TEXT \"x\") (HARD) (TEXT \"y\")))"},
		{"z  \n", "(BLOCK (PARA (TEXT \"z\")))"},
		{"   \n ", "()"},
		{"   \n", "()"},
	})
}

func TestLink(t *testing.T) {
	t.Parallel()
	checkTcs(t, false, TestCases{
		{"[", "(INLINE (TEXT \"[\"))"},
		{"[[", "(INLINE (TEXT \"[[\"))"},
		{"[[|", "(INLINE (TEXT \"[[|\"))"},
		{"[[]", "(INLINE (TEXT \"[[]\"))"},
		{"[[|]", "(INLINE (TEXT \"[[|]\"))"},
		{"[[]]", "(INLINE (TEXT \"[[]]\"))"},
		{"[[|]]", "(INLINE (TEXT \"[[|]]\"))"},
		{"[[ ]]", "(INLINE (TEXT \"[[ ]]\"))"},
		{"[[\n]]", "(INLINE (TEXT \"[[\") (SOFT) (TEXT \"]]\"))"},
		{"[[ a]]", "(INLINE (LINK-EXTERNAL () \"a\"))"},
		{"[[a ]]", "(INLINE (TEXT \"[[a ]]\"))"},
		{"[[a\n]]", "(INLINE (TEXT \"[[a\") (SOFT) (TEXT \"]]\"))"},
		{"[[a]]", "(INLINE (LINK-EXTERNAL () \"a\"))"},
		{"[[12345678901234]]", "(INLINE (LINK-ZETTEL () \"12345678901234\"))"},
		{"[[a]", "(INLINE (TEXT \"[[a]\"))"},
		{"[[|a]]", "(INLINE (TEXT \"[[|a]]\"))"},
		{"[[b|]]", "(INLINE (TEXT \"[[b|]]\"))"},
		{"[[b|a]]", "(INLINE (LINK-EXTERNAL () \"a\" (TEXT \"b\")))"},
		{"[[b| a]]", "(INLINE (LINK-EXTERNAL () \"a\" (TEXT \"b\")))"},
		{"[[b%c|a]]", "(INLINE (LINK-EXTERNAL () \"a\" (TEXT \"b%c\")))"},
		{"[[b%%c|a]]", "(INLINE (TEXT \"[[b\") (LITERAL-COMMENT () \"c|a]]\"))"},
		{"[[b|a]", "(INLINE (TEXT \"[[b|a]\"))"},
		{"[[b\nc|a]]", "(INLINE (LINK-EXTERNAL () \"a\" (TEXT \"b\") (SOFT) (TEXT \"c\")))"},
		{"[[b c|a#n]]", "(INLINE (LINK-EXTERNAL () \"a#n\" (TEXT \"b c\")))"},
		{"[[a]]go", "(INLINE (LINK-EXTERNAL () \"a\") (TEXT \"go\"))"},
		{"[[b|a]]{go}", "(INLINE (LINK-EXTERNAL ((\"go\" . \"\")) \"a\" (TEXT \"b\")))"},
		{"[[[[a]]|b]]", "(INLINE (TEXT \"[[\") (LINK-EXTERNAL () \"a\") (TEXT \"|b]]\"))"},
		{"[[a[b]c|d]]", "(INLINE (LINK-EXTERNAL () \"d\" (TEXT \"a[b]c\")))"},
		{"[[[b]c|d]]", "(INLINE (TEXT \"[\") (LINK-EXTERNAL () \"d\" (TEXT \"b]c\")))"},
		{"[[a[]c|d]]", "(INLINE (LINK-EXTERNAL () \"d\" (TEXT \"a[]c\")))"},
		{"[[a[b]|d]]", "(INLINE (LINK-EXTERNAL () \"d\" (TEXT \"a[b]\")))"},
		{"[[\\|]]", "(INLINE (LINK-EXTERNAL () \"\\\\|\"))"},
		{"[[\\||a]]", "(INLINE (LINK-EXTERNAL () \"a\" (TEXT \"|\")))"},
		{"[[b\\||a]]", "(INLINE (LINK-EXTERNAL () \"a\" (TEXT \"b|\")))"},
		{"[[b\\|c|a]]", "(INLINE (LINK-EXTERNAL () \"a\" (TEXT \"b|c\")))"},
		{"[[\\]]]", "(INLINE (LINK-EXTERNAL () \"\\\\]\"))"},
		{"[[\\]|a]]", "(INLINE (LINK-EXTERNAL () \"a\" (TEXT \"]\")))"},
		{"[[b\\]|a]]", "(INLINE (LINK-EXTERNAL () \"a\" (TEXT \"b]\")))"},
		{"[[\\]\\||a]]", "(INLINE (LINK-EXTERNAL () \"a\" (TEXT \"]|\")))"},
		{"[[http://a]]", "(INLINE (LINK-EXTERNAL () \"http://a\"))"},
		{"[[http://a|http://a]]", "(INLINE (LINK-EXTERNAL () \"http://a\" (TEXT \"http://a\")))"},
		{"[[[[a]]]]", "(INLINE (TEXT \"[[\") (LINK-EXTERNAL () \"a\") (TEXT \"]]\"))"},
		{"[[query:title]]", "(INLINE (LINK-QUERY () \"title\"))"},
		{"[[query:title syntax]]", "(INLINE (LINK-QUERY () \"title syntax\"))"},
		{"[[query:title | action]]", "(INLINE (LINK-QUERY () \"title | action\"))"},
		{"[[Text|query:title]]", "(INLINE (LINK-QUERY () \"title\" (TEXT \"Text\")))"},
		{"[[Text|query:title syntax]]", "(INLINE (LINK-QUERY () \"title syntax\" (TEXT \"Text\")))"},
		{"[[Text|query:title | action]]", "(INLINE (LINK-QUERY () \"title | action\" (TEXT \"Text\")))"},
	checkTcs(t, TestCases{
		{"[", "(BLOCK (PARA (TEXT \"[\")))"},
		{"[[", "(BLOCK (PARA (TEXT \"[[\")))"},
		{"[[|", "(BLOCK (PARA (TEXT \"[[|\")))"},
		{"[[]", "(BLOCK (PARA (TEXT \"[[]\")))"},
		{"[[|]", "(BLOCK (PARA (TEXT \"[[|]\")))"},
		{"[[]]", "(BLOCK (PARA (TEXT \"[[]]\")))"},
		{"[[|]]", "(BLOCK (PARA (TEXT \"[[|]]\")))"},
		{"[[ ]]", "(BLOCK (PARA (TEXT \"[[ ]]\")))"},
		{"[[\n]]", "(BLOCK (PARA (TEXT \"[[\") (SOFT) (TEXT \"]]\")))"},
		{"[[ a]]", "(BLOCK (PARA (LINK-EXTERNAL () \"a\")))"},
		{"[[a ]]", "(BLOCK (PARA (TEXT \"[[a ]]\")))"},
		{"[[a\n]]", "(BLOCK (PARA (TEXT \"[[a\") (SOFT) (TEXT \"]]\")))"},
		{"[[a]]", "(BLOCK (PARA (LINK-EXTERNAL () \"a\")))"},
		{"[[12345678901234]]", "(BLOCK (PARA (LINK-ZETTEL () \"12345678901234\")))"},
		{"[[a]", "(BLOCK (PARA (TEXT \"[[a]\")))"},
		{"[[|a]]", "(BLOCK (PARA (TEXT \"[[|a]]\")))"},
		{"[[b|]]", "(BLOCK (PARA (TEXT \"[[b|]]\")))"},
		{"[[b|a]]", "(BLOCK (PARA (LINK-EXTERNAL () \"a\" (TEXT \"b\"))))"},
		{"[[b| a]]", "(BLOCK (PARA (LINK-EXTERNAL () \"a\" (TEXT \"b\"))))"},
		{"[[b%c|a]]", "(BLOCK (PARA (LINK-EXTERNAL () \"a\" (TEXT \"b%c\"))))"},
		{"[[b%%c|a]]", "(BLOCK (PARA (TEXT \"[[b\") (LITERAL-COMMENT () \"c|a]]\")))"},
		{"[[b|a]", "(BLOCK (PARA (TEXT \"[[b|a]\")))"},
		{"[[b\nc|a]]", "(BLOCK (PARA (LINK-EXTERNAL () \"a\" (TEXT \"b\") (SOFT) (TEXT \"c\"))))"},
		{"[[b c|a#n]]", "(BLOCK (PARA (LINK-EXTERNAL () \"a#n\" (TEXT \"b c\"))))"},
		{"[[a]]go", "(BLOCK (PARA (LINK-EXTERNAL () \"a\") (TEXT \"go\")))"},
		{"[[b|a]]{go}", "(BLOCK (PARA (LINK-EXTERNAL ((\"go\" . \"\")) \"a\" (TEXT \"b\"))))"},
		{"[[[[a]]|b]]", "(BLOCK (PARA (TEXT \"[[\") (LINK-EXTERNAL () \"a\") (TEXT \"|b]]\")))"},
		{"[[a[b]c|d]]", "(BLOCK (PARA (LINK-EXTERNAL () \"d\" (TEXT \"a[b]c\"))))"},
		{"[[[b]c|d]]", "(BLOCK (PARA (TEXT \"[\") (LINK-EXTERNAL () \"d\" (TEXT \"b]c\"))))"},
		{"[[a[]c|d]]", "(BLOCK (PARA (LINK-EXTERNAL () \"d\" (TEXT \"a[]c\"))))"},
		{"[[a[b]|d]]", "(BLOCK (PARA (LINK-EXTERNAL () \"d\" (TEXT \"a[b]\"))))"},
		{"[[\\|]]", "(BLOCK (PARA (LINK-EXTERNAL () \"\\\\|\")))"},
		{"[[\\||a]]", "(BLOCK (PARA (LINK-EXTERNAL () \"a\" (TEXT \"|\"))))"},
		{"[[b\\||a]]", "(BLOCK (PARA (LINK-EXTERNAL () \"a\" (TEXT \"b|\"))))"},
		{"[[b\\|c|a]]", "(BLOCK (PARA (LINK-EXTERNAL () \"a\" (TEXT \"b|c\"))))"},
		{"[[\\]]]", "(BLOCK (PARA (LINK-EXTERNAL () \"\\\\]\")))"},
		{"[[\\]|a]]", "(BLOCK (PARA (LINK-EXTERNAL () \"a\" (TEXT \"]\"))))"},
		{"[[b\\]|a]]", "(BLOCK (PARA (LINK-EXTERNAL () \"a\" (TEXT \"b]\"))))"},
		{"[[\\]\\||a]]", "(BLOCK (PARA (LINK-EXTERNAL () \"a\" (TEXT \"]|\"))))"},
		{"[[http://a]]", "(BLOCK (PARA (LINK-EXTERNAL () \"http://a\")))"},
		{"[[http://a|http://a]]", "(BLOCK (PARA (LINK-EXTERNAL () \"http://a\" (TEXT \"http://a\"))))"},
		{"[[[[a]]]]", "(BLOCK (PARA (TEXT \"[[\") (LINK-EXTERNAL () \"a\") (TEXT \"]]\")))"},
		{"[[query:title]]", "(BLOCK (PARA (LINK-QUERY () \"title\")))"},
		{"[[query:title syntax]]", "(BLOCK (PARA (LINK-QUERY () \"title syntax\")))"},
		{"[[query:title | action]]", "(BLOCK (PARA (LINK-QUERY () \"title | action\")))"},
		{"[[Text|query:title]]", "(BLOCK (PARA (LINK-QUERY () \"title\" (TEXT \"Text\"))))"},
		{"[[Text|query:title syntax]]", "(BLOCK (PARA (LINK-QUERY () \"title syntax\" (TEXT \"Text\"))))"},
		{"[[Text|query:title | action]]", "(BLOCK (PARA (LINK-QUERY () \"title | action\" (TEXT \"Text\"))))"},
	})
}

func TestEmbed(t *testing.T) {
	t.Parallel()
	checkTcs(t, false, TestCases{
		{"{", "(INLINE (TEXT \"{\"))"},
		{"{{", "(INLINE (TEXT \"{{\"))"},
		{"{{|", "(INLINE (TEXT \"{{|\"))"},
		{"{{}", "(INLINE (TEXT \"{{}\"))"},
		{"{{|}", "(INLINE (TEXT \"{{|}\"))"},
		{"{{}}", "(INLINE (TEXT \"{{}}\"))"},
		{"{{|}}", "(INLINE (TEXT \"{{|}}\"))"},
		{"{{ }}", "(INLINE (TEXT \"{{ }}\"))"},
		{"{{\n}}", "(INLINE (TEXT \"{{\") (SOFT) (TEXT \"}}\"))"},
		{"{{a }}", "(INLINE (TEXT \"{{a }}\"))"},
		{"{{a\n}}", "(INLINE (TEXT \"{{a\") (SOFT) (TEXT \"}}\"))"},
		{"{{a}}", "(INLINE (EMBED () \"a\"))"},
		{"{{12345678901234}}", "(INLINE (EMBED () \"12345678901234\"))"},
		{"{{ a}}", "(INLINE (EMBED () \"a\"))"},
		{"{{a}", "(INLINE (TEXT \"{{a}\"))"},
		{"{{|a}}", "(INLINE (TEXT \"{{|a}}\"))"},
		{"{{b|}}", "(INLINE (TEXT \"{{b|}}\"))"},
		{"{{b|a}}", "(INLINE (EMBED () \"a\" (TEXT \"b\")))"},
		{"{{b| a}}", "(INLINE (EMBED () \"a\" (TEXT \"b\")))"},
		{"{{b|a}", "(INLINE (TEXT \"{{b|a}\"))"},
		{"{{b\nc|a}}", "(INLINE (EMBED () \"a\" (TEXT \"b\") (SOFT) (TEXT \"c\")))"},
		{"{{b c|a#n}}", "(INLINE (EMBED () \"a#n\" (TEXT \"b c\")))"},
		{"{{a}}{go}", "(INLINE (EMBED ((\"go\" . \"\")) \"a\"))"},
		{"{{{{a}}|b}}", "(INLINE (TEXT \"{{\") (EMBED () \"a\") (TEXT \"|b}}\"))"},
		{"{{\\|}}", "(INLINE (EMBED () \"\\\\|\"))"},
		{"{{\\||a}}", "(INLINE (EMBED () \"a\" (TEXT \"|\")))"},
		{"{{b\\||a}}", "(INLINE (EMBED () \"a\" (TEXT \"b|\")))"},
		{"{{b\\|c|a}}", "(INLINE (EMBED () \"a\" (TEXT \"b|c\")))"},
		{"{{\\}}}", "(INLINE (EMBED () \"\\\\}\"))"},
		{"{{\\}|a}}", "(INLINE (EMBED () \"a\" (TEXT \"}\")))"},
		{"{{b\\}|a}}", "(INLINE (EMBED () \"a\" (TEXT \"b}\")))"},
		{"{{\\}\\||a}}", "(INLINE (EMBED () \"a\" (TEXT \"}|\")))"},
		{"{{http://a}}", "(INLINE (EMBED () \"http://a\"))"},
		{"{{http://a|http://a}}", "(INLINE (EMBED () \"http://a\" (TEXT \"http://a\")))"},
		{"{{{{a}}}}", "(INLINE (TEXT \"{{\") (EMBED () \"a\") (TEXT \"}}\"))"},
	checkTcs(t, TestCases{
		{"{", "(BLOCK (PARA (TEXT \"{\")))"},
		{"{{", "(BLOCK (PARA (TEXT \"{{\")))"},
		{"{{|", "(BLOCK (PARA (TEXT \"{{|\")))"},
		{"{{}", "(BLOCK (PARA (TEXT \"{{}\")))"},
		{"{{|}", "(BLOCK (PARA (TEXT \"{{|}\")))"},
		{"{{}}", "(BLOCK (PARA (TEXT \"{{}}\")))"},
		{"{{|}}", "(BLOCK (PARA (TEXT \"{{|}}\")))"},
		{"{{ }}", "(BLOCK (PARA (TEXT \"{{ }}\")))"},
		{"{{\n}}", "(BLOCK (PARA (TEXT \"{{\") (SOFT) (TEXT \"}}\")))"},
		{"{{a }}", "(BLOCK (PARA (TEXT \"{{a }}\")))"},
		{"{{a\n}}", "(BLOCK (PARA (TEXT \"{{a\") (SOFT) (TEXT \"}}\")))"},
		{"{{a}}", "(BLOCK (PARA (EMBED () (EXTERNAL \"a\") \"\")))"},
		{"{{12345678901234}}", "(BLOCK (PARA (EMBED () (ZETTEL \"12345678901234\") \"\")))"},
		{"{{ a}}", "(BLOCK (PARA (EMBED () (EXTERNAL \"a\") \"\")))"},
		{"{{a}", "(BLOCK (PARA (TEXT \"{{a}\")))"},
		{"{{|a}}", "(BLOCK (PARA (TEXT \"{{|a}}\")))"},
		{"{{b|}}", "(BLOCK (PARA (TEXT \"{{b|}}\")))"},
		{"{{b|a}}", "(BLOCK (PARA (EMBED () (EXTERNAL \"a\") \"\" (TEXT \"b\"))))"},
		{"{{b| a}}", "(BLOCK (PARA (EMBED () (EXTERNAL \"a\") \"\" (TEXT \"b\"))))"},
		{"{{b|a}", "(BLOCK (PARA (TEXT \"{{b|a}\")))"},
		{"{{b\nc|a}}", "(BLOCK (PARA (EMBED () (EXTERNAL \"a\") \"\" (TEXT \"b\") (SOFT) (TEXT \"c\"))))"},
		{"{{b c|a#n}}", "(BLOCK (PARA (EMBED () (EXTERNAL \"a#n\") \"\" (TEXT \"b c\"))))"},
		{"{{a}}{go}", "(BLOCK (PARA (EMBED ((\"go\" . \"\")) (EXTERNAL \"a\") \"\")))"},
		{"{{{{a}}|b}}", "(BLOCK (PARA (TEXT \"{{\") (EMBED () (EXTERNAL \"a\") \"\") (TEXT \"|b}}\")))"},
		{"{{\\|}}", "(BLOCK (PARA (EMBED () (EXTERNAL \"\\\\|\") \"\")))"},
		{"{{\\||a}}", "(BLOCK (PARA (EMBED () (EXTERNAL \"a\") \"\" (TEXT \"|\"))))"},
		{"{{b\\||a}}", "(BLOCK (PARA (EMBED () (EXTERNAL \"a\") \"\" (TEXT \"b|\"))))"},
		{"{{b\\|c|a}}", "(BLOCK (PARA (EMBED () (EXTERNAL \"a\") \"\" (TEXT \"b|c\"))))"},
		{"{{\\}}}", "(BLOCK (PARA (EMBED () (EXTERNAL \"\\\\}\") \"\")))"},
		{"{{\\}|a}}", "(BLOCK (PARA (EMBED () (EXTERNAL \"a\") \"\" (TEXT \"}\"))))"},
		{"{{b\\}|a}}", "(BLOCK (PARA (EMBED () (EXTERNAL \"a\") \"\" (TEXT \"b}\"))))"},
		{"{{\\}\\||a}}", "(BLOCK (PARA (EMBED () (EXTERNAL \"a\") \"\" (TEXT \"}|\"))))"},
		{"{{http://a}}", "(BLOCK (PARA (EMBED () (EXTERNAL \"http://a\") \"\")))"},
		{"{{http://a|http://a}}", "(BLOCK (PARA (EMBED () (EXTERNAL \"http://a\") \"\" (TEXT \"http://a\"))))"},
		{"{{{{a}}}}", "(BLOCK (PARA (TEXT \"{{\") (EMBED () (EXTERNAL \"a\") \"\") (TEXT \"}}\")))"},
	})
}

func TestCite(t *testing.T) {
	t.Parallel()
	checkTcs(t, false, TestCases{
		{"[@", "(INLINE (TEXT \"[@\"))"},
		{"[@]", "(INLINE (TEXT \"[@]\"))"},
		{"[@a]", "(INLINE (CITE () \"a\"))"},
		{"[@ a]", "(INLINE (TEXT \"[@ a]\"))"},
		{"[@a ]", "(INLINE (CITE () \"a\"))"},
		{"[@a\n]", "(INLINE (CITE () \"a\"))"},
		{"[@a\nx]", "(INLINE (CITE () \"a\" (SOFT) (TEXT \"x\")))"},
		{"[@a\n\n]", "(INLINE (TEXT \"[@a\") (SOFT) (SOFT) (TEXT \"]\"))"},
		{"[@a,\n]", "(INLINE (CITE () \"a\"))"},
		{"[@a,n]", "(INLINE (CITE () \"a\" (TEXT \"n\")))"},
		{"[@a| n]", "(INLINE (CITE () \"a\" (TEXT \"n\")))"},
		{"[@a|n ]", "(INLINE (CITE () \"a\" (TEXT \"n\")))"},
		{"[@a,[@b]]", "(INLINE (CITE () \"a\" (CITE () \"b\")))"},
		{"[@a]{color=green}", "(INLINE (CITE ((\"color\" . \"green\")) \"a\"))"},
	checkTcs(t, TestCases{
		{"[@", "(BLOCK (PARA (TEXT \"[@\")))"},
		{"[@]", "(BLOCK (PARA (TEXT \"[@]\")))"},
		{"[@a]", "(BLOCK (PARA (CITE () \"a\")))"},
		{"[@ a]", "(BLOCK (PARA (TEXT \"[@ a]\")))"},
		{"[@a ]", "(BLOCK (PARA (CITE () \"a\")))"},
		{"[@a\n]", "(BLOCK (PARA (CITE () \"a\")))"},
		{"[@a\nx]", "(BLOCK (PARA (CITE () \"a\" (SOFT) (TEXT \"x\"))))"},
		{"[@a\n\n]", "(BLOCK (PARA (TEXT \"[@a\")) (PARA (TEXT \"]\")))"},
		{"[@a,\n]", "(BLOCK (PARA (CITE () \"a\")))"},
		{"[@a,n]", "(BLOCK (PARA (CITE () \"a\" (TEXT \"n\"))))"},
		{"[@a| n]", "(BLOCK (PARA (CITE () \"a\" (TEXT \"n\"))))"},
		{"[@a|n ]", "(BLOCK (PARA (CITE () \"a\" (TEXT \"n\"))))"},
		{"[@a,[@b]]", "(BLOCK (PARA (CITE () \"a\" (CITE () \"b\"))))"},
		{"[@a]{color=green}", "(BLOCK (PARA (CITE ((\"color\" . \"green\")) \"a\")))"},
	})
	checkTcs(t, true, TestCases{
	checkTcs(t, TestCases{
		{"[@a\n\n]", "(BLOCK (PARA (TEXT \"[@a\")) (PARA (TEXT \"]\")))"},
	})
}

func TestEndnote(t *testing.T) {
	t.Parallel()
	checkTcs(t, false, TestCases{
		{"[^", "(INLINE (TEXT \"[^\"))"},
		{"[^]", "(INLINE (ENDNOTE ()))"},
		{"[^abc]", "(INLINE (ENDNOTE () (TEXT \"abc\")))"},
		{"[^abc ]", "(INLINE (ENDNOTE () (TEXT \"abc\")))"},
		{"[^abc\ndef]", "(INLINE (ENDNOTE () (TEXT \"abc\") (SOFT) (TEXT \"def\")))"},
		{"[^abc\n\ndef]", "(INLINE (TEXT \"[^abc\") (SOFT) (SOFT) (TEXT \"def]\"))"},
		{"[^abc[^def]]", "(INLINE (ENDNOTE () (TEXT \"abc\") (ENDNOTE () (TEXT \"def\"))))"},
		{"[^abc]{-}", "(INLINE (ENDNOTE ((\"-\" . \"\")) (TEXT \"abc\")))"},
	checkTcs(t, TestCases{
		{"[^", "(BLOCK (PARA (TEXT \"[^\")))"},
		{"[^]", "(BLOCK (PARA (ENDNOTE ())))"},
		{"[^abc]", "(BLOCK (PARA (ENDNOTE () (TEXT \"abc\"))))"},
		{"[^abc ]", "(BLOCK (PARA (ENDNOTE () (TEXT \"abc\"))))"},
		{"[^abc\ndef]", "(BLOCK (PARA (ENDNOTE () (TEXT \"abc\") (SOFT) (TEXT \"def\"))))"},
		{"[^abc\n\ndef]", "(BLOCK (PARA (TEXT \"[^abc\")) (PARA (TEXT \"def]\")))"},
		{"[^abc[^def]]", "(BLOCK (PARA (ENDNOTE () (TEXT \"abc\") (ENDNOTE () (TEXT \"def\")))))"},
		{"[^abc]{-}", "(BLOCK (PARA (ENDNOTE ((\"-\" . \"\")) (TEXT \"abc\"))))"},
	})
	checkTcs(t, true, TestCases{
	checkTcs(t, TestCases{
		{"[^abc\n\ndef]", "(BLOCK (PARA (TEXT \"[^abc\")) (PARA (TEXT \"def]\")))"},
	})
}

func TestMark(t *testing.T) {
	t.Parallel()
	checkTcs(t, false, TestCases{
		{"[!", "(INLINE (TEXT \"[!\"))"},
		{"[!\n", "(INLINE (TEXT \"[!\"))"},
		{"[!]", "(INLINE (MARK \"\" \"\" \"\"))"},
		{"[!][!]", "(INLINE (MARK \"\" \"\" \"\") (MARK \"\" \"\" \"\"))"},
		{"[! ]", "(INLINE (TEXT \"[! ]\"))"},
		{"[!a]", "(INLINE (MARK \"a\" \"\" \"\"))"},
		{"[!a][!a]", "(INLINE (MARK \"a\" \"\" \"\") (MARK \"a\" \"\" \"\"))"},
		{"[!a ]", "(INLINE (TEXT \"[!a ]\"))"},
		{"[!a_]", "(INLINE (MARK \"a_\" \"\" \"\"))"},
		{"[!a_][!a]", "(INLINE (MARK \"a_\" \"\" \"\") (MARK \"a\" \"\" \"\"))"},
		{"[!a-b]", "(INLINE (MARK \"a-b\" \"\" \"\"))"},
		{"[!a|b]", "(INLINE (MARK \"a\" \"\" \"\" (TEXT \"b\")))"},
		{"[!a|]", "(INLINE (MARK \"a\" \"\" \"\"))"},
		{"[!|b]", "(INLINE (MARK \"\" \"\" \"\" (TEXT \"b\")))"},
		{"[!|b ]", "(INLINE (MARK \"\" \"\" \"\" (TEXT \"b\")))"},
		{"[!|b c]", "(INLINE (MARK \"\" \"\" \"\" (TEXT \"b c\")))"},
	checkTcs(t, TestCases{
		{"[!", "(BLOCK (PARA (TEXT \"[!\")))"},
		{"[!\n", "(BLOCK (PARA (TEXT \"[!\")))"},
		{"[!]", "(BLOCK (PARA (MARK \"\" \"\" \"\")))"},
		{"[!][!]", "(BLOCK (PARA (MARK \"\" \"\" \"\") (MARK \"\" \"\" \"\")))"},
		{"[! ]", "(BLOCK (PARA (TEXT \"[! ]\")))"},
		{"[!a]", "(BLOCK (PARA (MARK \"a\" \"\" \"\")))"},
		{"[!a][!a]", "(BLOCK (PARA (MARK \"a\" \"\" \"\") (MARK \"a\" \"\" \"\")))"},
		{"[!a ]", "(BLOCK (PARA (TEXT \"[!a ]\")))"},
		{"[!a_]", "(BLOCK (PARA (MARK \"a_\" \"\" \"\")))"},
		{"[!a_][!a]", "(BLOCK (PARA (MARK \"a_\" \"\" \"\") (MARK \"a\" \"\" \"\")))"},
		{"[!a-b]", "(BLOCK (PARA (MARK \"a-b\" \"\" \"\")))"},
		{"[!a|b]", "(BLOCK (PARA (MARK \"a\" \"\" \"\" (TEXT \"b\"))))"},
		{"[!a|]", "(BLOCK (PARA (MARK \"a\" \"\" \"\")))"},
		{"[!|b]", "(BLOCK (PARA (MARK \"\" \"\" \"\" (TEXT \"b\"))))"},
		{"[!|b ]", "(BLOCK (PARA (MARK \"\" \"\" \"\" (TEXT \"b\"))))"},
		{"[!|b c]", "(BLOCK (PARA (MARK \"\" \"\" \"\" (TEXT \"b c\"))))"},
	})
}

func TestComment(t *testing.T) {
	t.Parallel()
	checkTcs(t, false, TestCases{
		{"%", "(INLINE (TEXT \"%\"))"},
		{"%%", "(INLINE (LITERAL-COMMENT () \"\"))"},
		{"%\n", "(INLINE (TEXT \"%\"))"},
		{"%%\n", "(INLINE (LITERAL-COMMENT () \"\"))"},
		{"%%a", "(INLINE (LITERAL-COMMENT () \"a\"))"},
		{"%%%a", "(INLINE (LITERAL-COMMENT () \"a\"))"},
		{"%% a", "(INLINE (LITERAL-COMMENT () \"a\"))"},
		{"%%%  a", "(INLINE (LITERAL-COMMENT () \"a\"))"},
		{"%% % a", "(INLINE (LITERAL-COMMENT () \"% a\"))"},
		{"%%a", "(INLINE (LITERAL-COMMENT () \"a\"))"},
		{"a%%b", "(INLINE (TEXT \"a\") (LITERAL-COMMENT () \"b\"))"},
		{"a %%b", "(INLINE (TEXT \"a \") (LITERAL-COMMENT () \"b\"))"},
		{" %%b", "(INLINE (LITERAL-COMMENT () \"b\"))"},
		{"%%b ", "(INLINE (LITERAL-COMMENT () \"b \"))"},
		{"100%", "(INLINE (TEXT \"100%\"))"},
		{"%%{=}a", "(INLINE (LITERAL-COMMENT ((\"\" . \"\")) \"a\"))"},
	checkTcs(t, TestCases{
		{"%", "(BLOCK (PARA (TEXT \"%\")))"},
		{"%%", "(BLOCK (PARA (LITERAL-COMMENT () \"\")))"},
		{"%\n", "(BLOCK (PARA (TEXT \"%\")))"},
		{"%%\n", "(BLOCK (PARA (LITERAL-COMMENT () \"\")))"},
		{"%%a", "(BLOCK (PARA (LITERAL-COMMENT () \"a\")))"},
		{"%%%a", "(BLOCK (PARA (LITERAL-COMMENT () \"a\")))"},
		{"%% a", "(BLOCK (PARA (LITERAL-COMMENT () \"a\")))"},
		{"%%%  a", "(BLOCK (PARA (LITERAL-COMMENT () \"a\")))"},
		{"%% % a", "(BLOCK (PARA (LITERAL-COMMENT () \"% a\")))"},
		{"%%a", "(BLOCK (PARA (LITERAL-COMMENT () \"a\")))"},
		{"a%%b", "(BLOCK (PARA (TEXT \"a\") (LITERAL-COMMENT () \"b\")))"},
		{"a %%b", "(BLOCK (PARA (TEXT \"a \") (LITERAL-COMMENT () \"b\")))"},
		{" %%b", "(BLOCK (PARA (LITERAL-COMMENT () \"b\")))"},
		{"%%b ", "(BLOCK (PARA (LITERAL-COMMENT () \"b \")))"},
		{"100%", "(BLOCK (PARA (TEXT \"100%\")))"},
		{"%%{=}a", "(BLOCK (PARA (LITERAL-COMMENT ((\"\" . \"\")) \"a\")))"},
	})
}

func TestFormat(t *testing.T) {
	symMap := symbolMap{
		"_": sz.SymFormatEmph,
		"*": sz.SymFormatStrong,
		">": sz.SymFormatInsert,
		"~": sz.SymFormatDelete,
		"^": sz.SymFormatSuper,
		",": sz.SymFormatSub,
		"#": sz.SymFormatMark,
		":": sz.SymFormatSpan,
	}
	t.Parallel()
	// Not for Insert / '>', because collision with quoted list
	// Not for Quote / '"', because escaped representation.
	for _, ch := range []string{"_", "*", "~", "^", ",", "#", ":"} {
		checkTcs(t, false, replace(ch, symMap, TestCases{
			{"$", "(INLINE (TEXT \"$\"))"},
			{"$$", "(INLINE (TEXT \"$$\"))"},
			{"$$$", "(INLINE (TEXT \"$$$\"))"},
			{"$$$$", "(INLINE ($% ()))"},
		checkTcs(t, replace(ch, symMap, TestCases{
			{"$", "(BLOCK (PARA (TEXT \"$\")))"},
			{"$$", "(BLOCK (PARA (TEXT \"$$\")))"},
			{"$$$", "(BLOCK (PARA (TEXT \"$$$\")))"},
			{"$$$$", "(BLOCK (PARA ($% ())))"},
		}))
	}
	// Not for Quote / '"', because escaped representation.
	for _, ch := range []string{"_", "*", ">", "~", "^", ",", "#", ":"} {
		checkTcs(t, false, replace(ch, symMap, TestCases{
			{"$$a$$", "(INLINE ($% () (TEXT \"a\")))"},
			{"$$a$$$", "(INLINE ($% () (TEXT \"a\")) (TEXT \"$\"))"},
			{"$$$a$$", "(INLINE ($% () (TEXT \"$a\")))"},
			{"$$$a$$$", "(INLINE ($% () (TEXT \"$a\")) (TEXT \"$\"))"},
			{"$\\$", "(INLINE (TEXT \"$$\"))"},
			{"$\\$$", "(INLINE (TEXT \"$$$\"))"},
			{"$$\\$", "(INLINE (TEXT \"$$$\"))"},
			{"$$a\\$$", "(INLINE (TEXT \"$$a$$\"))"},
			{"$$a$\\$", "(INLINE (TEXT \"$$a$$\"))"},
			{"$$a\\$$$", "(INLINE ($% () (TEXT \"a$\")))"},
			{"$$a\na$$", "(INLINE ($% () (TEXT \"a\") (SOFT) (TEXT \"a\")))"},
			{"$$a\n\na$$", "(INLINE (TEXT \"$$a\") (SOFT) (SOFT) (TEXT \"a$$\"))"},
			{"$$a$${go}", "(INLINE ($% ((\"go\" . \"\")) (TEXT \"a\")))"},
		checkTcs(t, replace(ch, symMap, TestCases{
			{"$$a$$", "(BLOCK (PARA ($% () (TEXT \"a\"))))"},
			{"$$a$$$", "(BLOCK (PARA ($% () (TEXT \"a\")) (TEXT \"$\")))"},
			{"$$$a$$", "(BLOCK (PARA ($% () (TEXT \"$a\"))))"},
			{"$$$a$$$", "(BLOCK (PARA ($% () (TEXT \"$a\")) (TEXT \"$\")))"},
			{"$\\$", "(BLOCK (PARA (TEXT \"$$\")))"},
			{"$\\$$", "(BLOCK (PARA (TEXT \"$$$\")))"},
			{"$$\\$", "(BLOCK (PARA (TEXT \"$$$\")))"},
			{"$$a\\$$", "(BLOCK (PARA (TEXT \"$$a$$\")))"},
			{"$$a$\\$", "(BLOCK (PARA (TEXT \"$$a$$\")))"},
			{"$$a\\$$$", "(BLOCK (PARA ($% () (TEXT \"a$\"))))"},
			{"$$a\na$$", "(BLOCK (PARA ($% () (TEXT \"a\") (SOFT) (TEXT \"a\"))))"},
			{"$$a\n\na$$", "(BLOCK (PARA (TEXT \"$$a\")) (PARA (TEXT \"a$$\")))"},
			{"$$a$${go}", "(BLOCK (PARA ($% ((\"go\" . \"\")) (TEXT \"a\"))))"},
		}))
		checkTcs(t, true, replace(ch, symMap, TestCases{
		checkTcs(t, replace(ch, symMap, TestCases{
			{"$$a\n\na$$", "(BLOCK (PARA (TEXT \"$$a\")) (PARA (TEXT \"a$$\")))"},
		}))
	}
	checkTcs(t, false, replace(`"`, symbolMap{`"`: sz.SymFormatQuote}, TestCases{
		{"$", "(INLINE (TEXT \"\\\"\"))"},
		{"$$", "(INLINE (TEXT \"\\\"\\\"\"))"},
		{"$$$", "(INLINE (TEXT \"\\\"\\\"\\\"\"))"},
		{"$$$$", "(INLINE ($% ()))"},
	checkTcs(t, replace(`"`, symbolMap{`"`: sz.SymFormatQuote}, TestCases{
		{"$", "(BLOCK (PARA (TEXT \"\\\"\")))"},
		{"$$", "(BLOCK (PARA (TEXT \"\\\"\\\"\")))"},
		{"$$$", "(BLOCK (PARA (TEXT \"\\\"\\\"\\\"\")))"},
		{"$$$$", "(BLOCK (PARA ($% ())))"},

		{"$$a$$", "(INLINE ($% () (TEXT \"a\")))"},
		{"$$a$$$", "(INLINE ($% () (TEXT \"a\")) (TEXT \"\\\"\"))"},
		{"$$$a$$", "(INLINE ($% () (TEXT \"\\\"a\")))"},
		{"$$$a$$$", "(INLINE ($% () (TEXT \"\\\"a\")) (TEXT \"\\\"\"))"},
		{"$\\$", "(INLINE (TEXT \"\\\"\\\"\"))"},
		{"$\\$$", "(INLINE (TEXT \"\\\"\\\"\\\"\"))"},
		{"$$\\$", "(INLINE (TEXT \"\\\"\\\"\\\"\"))"},
		{"$$a\\$$", "(INLINE (TEXT \"\\\"\\\"a\\\"\\\"\"))"},
		{"$$a$\\$", "(INLINE (TEXT \"\\\"\\\"a\\\"\\\"\"))"},
		{"$$a\\$$$", "(INLINE ($% () (TEXT \"a\\\"\")))"},
		{"$$a\na$$", "(INLINE ($% () (TEXT \"a\") (SOFT) (TEXT \"a\")))"},
		{"$$a\n\na$$", "(INLINE (TEXT \"\\\"\\\"a\") (SOFT) (SOFT) (TEXT \"a\\\"\\\"\"))"},
		{"$$a$${go}", "(INLINE ($% ((\"go\" . \"\")) (TEXT \"a\")))"},
		{"$$a$$", "(BLOCK (PARA ($% () (TEXT \"a\"))))"},
		{"$$a$$$", "(BLOCK (PARA ($% () (TEXT \"a\")) (TEXT \"\\\"\")))"},
		{"$$$a$$", "(BLOCK (PARA ($% () (TEXT \"\\\"a\"))))"},
		{"$$$a$$$", "(BLOCK (PARA ($% () (TEXT \"\\\"a\")) (TEXT \"\\\"\")))"},
		{"$\\$", "(BLOCK (PARA (TEXT \"\\\"\\\"\")))"},
		{"$\\$$", "(BLOCK (PARA (TEXT \"\\\"\\\"\\\"\")))"},
		{"$$\\$", "(BLOCK (PARA (TEXT \"\\\"\\\"\\\"\")))"},
		{"$$a\\$$", "(BLOCK (PARA (TEXT \"\\\"\\\"a\\\"\\\"\")))"},
		{"$$a$\\$", "(BLOCK (PARA (TEXT \"\\\"\\\"a\\\"\\\"\")))"},
		{"$$a\\$$$", "(BLOCK (PARA ($% () (TEXT \"a\\\"\"))))"},
		{"$$a\na$$", "(BLOCK (PARA ($% () (TEXT \"a\") (SOFT) (TEXT \"a\"))))"},
		{"$$a\n\na$$", "(BLOCK (PARA (TEXT \"\\\"\\\"a\")) (PARA (TEXT \"a\\\"\\\"\")))"},
		{"$$a$${go}", "(BLOCK (PARA ($% ((\"go\" . \"\")) (TEXT \"a\"))))"},
	}))
	checkTcs(t, false, TestCases{
		{"__****__", "(INLINE (FORMAT-EMPH () (FORMAT-STRONG ())))"},
		{"__**a**__", "(INLINE (FORMAT-EMPH () (FORMAT-STRONG () (TEXT \"a\"))))"},
		{"__**__**", "(INLINE (TEXT \"__\") (FORMAT-STRONG () (TEXT \"__\")))"},
	checkTcs(t, TestCases{
		{"__****__", "(BLOCK (PARA (FORMAT-EMPH () (FORMAT-STRONG ()))))"},
		{"__**a**__", "(BLOCK (PARA (FORMAT-EMPH () (FORMAT-STRONG () (TEXT \"a\")))))"},
		{"__**__**", "(BLOCK (PARA (TEXT \"__\") (FORMAT-STRONG () (TEXT \"__\"))))"},
	})
}

func TestLiteral(t *testing.T) {
	symMap := symbolMap{
		"@": sz.SymLiteralZettel,
		"`": sz.SymLiteralProg,
		"`": sz.SymLiteralCode,
		"'": sz.SymLiteralInput,
		"=": sz.SymLiteralOutput,
	}
	t.Parallel()
	for _, ch := range []string{"@", "`", "'", "="} {
		checkTcs(t, false, replace(ch, symMap, TestCases{
			{"$", "(INLINE (TEXT \"$\"))"},
			{"$$", "(INLINE (TEXT \"$$\"))"},
			{"$$$", "(INLINE (TEXT \"$$$\"))"},
			{"$$$$", "(INLINE ($% () \"\"))"},
			{"$$a$$", "(INLINE ($% () \"a\"))"},
			{"$$a$$$", "(INLINE ($% () \"a\") (TEXT \"$\"))"},
			{"$$$a$$", "(INLINE ($% () \"$a\"))"},
			{"$$$a$$$", "(INLINE ($% () \"$a\") (TEXT \"$\"))"},
			{"$\\$", "(INLINE (TEXT \"$$\"))"},
			{"$\\$$", "(INLINE (TEXT \"$$$\"))"},
			{"$$\\$", "(INLINE (TEXT \"$$$\"))"},
			{"$$a\\$$", "(INLINE (TEXT \"$$a$$\"))"},
			{"$$a$\\$", "(INLINE (TEXT \"$$a$$\"))"},
			{"$$a\\$$$", "(INLINE ($% () \"a$\"))"},
			{"$$a$${go}", "(INLINE ($% ((\"go\" . \"\")) \"a\"))"},
	for _, ch := range []string{"`", "'", "="} {
		checkTcs(t, replace(ch, symMap, TestCases{
			{"$", "(BLOCK (PARA (TEXT \"$\")))"},
			{"$$", "(BLOCK (PARA (TEXT \"$$\")))"},
			{"$$$", "(BLOCK (PARA (TEXT \"$$$\")))"},
			{"$$$$", "(BLOCK (PARA ($% () \"\")))"},
			{"$$a$$", "(BLOCK (PARA ($% () \"a\")))"},
			{"$$a$$$", "(BLOCK (PARA ($% () \"a\") (TEXT \"$\")))"},
			{"$$$a$$", "(BLOCK (PARA ($% () \"$a\")))"},
			{"$$$a$$$", "(BLOCK (PARA ($% () \"$a\") (TEXT \"$\")))"},
			{"$\\$", "(BLOCK (PARA (TEXT \"$$\")))"},
			{"$\\$$", "(BLOCK (PARA (TEXT \"$$$\")))"},
			{"$$\\$", "(BLOCK (PARA (TEXT \"$$$\")))"},
			{"$$a\\$$", "(BLOCK (PARA (TEXT \"$$a$$\")))"},
			{"$$a$\\$", "(BLOCK (PARA (TEXT \"$$a$$\")))"},
			{"$$a\\$$$", "(BLOCK (PARA ($% () \"a$\")))"},
			{"$$a$${go}", "(BLOCK (PARA ($% ((\"go\" . \"\")) \"a\")))"},
		}))
	}
	checkTcs(t, false, TestCases{
		{"''````''", "(INLINE (LITERAL-INPUT () \"````\"))"},
		{"''``a``''", "(INLINE (LITERAL-INPUT () \"``a``\"))"},
		{"''``''``", "(INLINE (LITERAL-INPUT () \"``\") (TEXT \"``\"))"},
		{"''\\'''", "(INLINE (LITERAL-INPUT () \"'\"))"},
	checkTcs(t, TestCases{
		{"``<script `` abc", "(BLOCK (PARA (LITERAL-CODE () \"<script \") (TEXT \" abc\")))"},
		{"''````''", "(BLOCK (PARA (LITERAL-INPUT () \"````\")))"},
		{"''``a``''", "(BLOCK (PARA (LITERAL-INPUT () \"``a``\")))"},
		{"''``''``", "(BLOCK (PARA (LITERAL-INPUT () \"``\") (TEXT \"``\")))"},
		{"''\\'''", "(BLOCK (PARA (LITERAL-INPUT () \"'\")))"},
	})
	checkTcs(t, false, TestCases{
		{"@@HTML@@{=html}", "(INLINE (LITERAL-HTML () \"HTML\"))"},
		{"@@HTML@@{=html lang=en}", "(INLINE (LITERAL-HTML ((\"lang\" . \"en\")) \"HTML\"))"},
		{"@@HTML@@{=html,lang=en}", "(INLINE (LITERAL-HTML ((\"lang\" . \"en\")) \"HTML\"))"},
	})
}

func TestLiteralMath(t *testing.T) {
	t.Parallel()
	checkTcs(t, false, TestCases{
		{"$", "(INLINE (TEXT \"$\"))"},
		{"$$", "(INLINE (TEXT \"$$\"))"},
		{"$$$", "(INLINE (TEXT \"$$$\"))"},
		{"$$$$", "(INLINE (LITERAL-MATH () \"\"))"},
		{"$$a$$", "(INLINE (LITERAL-MATH () \"a\"))"},
		{"$$a$$$", "(INLINE (LITERAL-MATH () \"a\") (TEXT \"$\"))"},
		{"$$$a$$", "(INLINE (LITERAL-MATH () \"$a\"))"},
		{"$$$a$$$", "(INLINE (LITERAL-MATH () \"$a\") (TEXT \"$\"))"},
		{`$\$`, "(INLINE (TEXT \"$$\"))"},
		{`$\$$`, "(INLINE (TEXT \"$$$\"))"},
		{`$$\$`, "(INLINE (TEXT \"$$$\"))"},
		{`$$a\$$`, "(INLINE (LITERAL-MATH () \"a\\\\\"))"},
		{`$$a$\$`, "(INLINE (TEXT \"$$a$$\"))"},
		{`$$a\$$$`, "(INLINE (LITERAL-MATH () \"a\\\\\") (TEXT \"$\"))"},
		{"$$a$${go}", "(INLINE (LITERAL-MATH ((\"go\" . \"\")) \"a\"))"},
	checkTcs(t, TestCases{
		{"$", "(BLOCK (PARA (TEXT \"$\")))"},
		{"$$", "(BLOCK (PARA (TEXT \"$$\")))"},
		{"$$$", "(BLOCK (PARA (TEXT \"$$$\")))"},
		{"$$$$", "(BLOCK (PARA (LITERAL-MATH () \"\")))"},
		{"$$a$$", "(BLOCK (PARA (LITERAL-MATH () \"a\")))"},
		{"$$a$$$", "(BLOCK (PARA (LITERAL-MATH () \"a\") (TEXT \"$\")))"},
		{"$$$a$$", "(BLOCK (PARA (LITERAL-MATH () \"$a\")))"},
		{"$$$a$$$", "(BLOCK (PARA (LITERAL-MATH () \"$a\") (TEXT \"$\")))"},
		{`$\$`, "(BLOCK (PARA (TEXT \"$$\")))"},
		{`$\$$`, "(BLOCK (PARA (TEXT \"$$$\")))"},
		{`$$\$`, "(BLOCK (PARA (TEXT \"$$$\")))"},
		{`$$a\$$`, "(BLOCK (PARA (LITERAL-MATH () \"a\\\\\")))"},
		{`$$a$\$`, "(BLOCK (PARA (TEXT \"$$a$$\")))"},
		{`$$a\$$$`, "(BLOCK (PARA (LITERAL-MATH () \"a\\\\\") (TEXT \"$\")))"},
		{"$$a$${go}", "(BLOCK (PARA (LITERAL-MATH ((\"go\" . \"\")) \"a\")))"},
	})
}

func TestMixFormatCode(t *testing.T) {
	t.Parallel()
	checkTcs(t, false, TestCases{
		{"__abc__\n**def**", "(INLINE (FORMAT-EMPH () (TEXT \"abc\")) (SOFT) (FORMAT-STRONG () (TEXT \"def\")))"},
		{"''abc''\n==def==", "(INLINE (LITERAL-INPUT () \"abc\") (SOFT) (LITERAL-OUTPUT () \"def\"))"},
		{"__abc__\n==def==", "(INLINE (FORMAT-EMPH () (TEXT \"abc\")) (SOFT) (LITERAL-OUTPUT () \"def\"))"},
		{"__abc__\n``def``", "(INLINE (FORMAT-EMPH () (TEXT \"abc\")) (SOFT) (LITERAL-CODE () \"def\"))"},
	checkTcs(t, TestCases{
		{"__abc__\n**def**", "(BLOCK (PARA (FORMAT-EMPH () (TEXT \"abc\")) (SOFT) (FORMAT-STRONG () (TEXT \"def\"))))"},
		{"''abc''\n==def==", "(BLOCK (PARA (LITERAL-INPUT () \"abc\") (SOFT) (LITERAL-OUTPUT () \"def\")))"},
		{"__abc__\n==def==", "(BLOCK (PARA (FORMAT-EMPH () (TEXT \"abc\")) (SOFT) (LITERAL-OUTPUT () \"def\")))"},
		{"__abc__\n``def``", "(BLOCK (PARA (FORMAT-EMPH () (TEXT \"abc\")) (SOFT) (LITERAL-CODE () \"def\")))"},
		{
			"\"\"ghi\"\"\n::abc::\n``def``\n",
			"(INLINE (FORMAT-QUOTE () (TEXT \"ghi\")) (SOFT) (FORMAT-SPAN () (TEXT \"abc\")) (SOFT) (LITERAL-CODE () \"def\"))",
			"(BLOCK (PARA (FORMAT-QUOTE () (TEXT \"ghi\")) (SOFT) (FORMAT-SPAN () (TEXT \"abc\")) (SOFT) (LITERAL-CODE () \"def\")))",
		},
	})
}

func TestNDash(t *testing.T) {
	t.Parallel()
	checkTcs(t, false, TestCases{
		{"--", "(INLINE (TEXT \"\u2013\"))"},
		{"a--b", "(INLINE (TEXT \"a\u2013b\"))"},
	checkTcs(t, TestCases{
		{"--", "(BLOCK (PARA (TEXT \"\u2013\")))"},
		{"a--b", "(BLOCK (PARA (TEXT \"a\u2013b\")))"},
	})
}

func TestEntity(t *testing.T) {
	t.Parallel()
	checkTcs(t, false, TestCases{
		{"&", "(INLINE (TEXT \"&\"))"},
		{"&;", "(INLINE (TEXT \"&;\"))"},
		{"&#;", "(INLINE (TEXT \"&#;\"))"},
		{"&#1a;", "(INLINE (TEXT \"&#1a;\"))"},
		{"&#x;", "(INLINE (TEXT \"&#x;\"))"},
		{"&#x0z;", "(INLINE (TEXT \"&#x0z;\"))"},
		{"&1;", "(INLINE (TEXT \"&1;\"))"},
		{"&#9;", "(INLINE (TEXT \"&#9;\"))"}, // Numeric entities below space are not allowed.
		{"&#x1f;", "(INLINE (TEXT \"&#x1f;\"))"},
	checkTcs(t, TestCases{
		{"&", "(BLOCK (PARA (TEXT \"&\")))"},
		{"&;", "(BLOCK (PARA (TEXT \"&;\")))"},
		{"&#;", "(BLOCK (PARA (TEXT \"&#;\")))"},
		{"&#1a;", "(BLOCK (PARA (TEXT \"&#1a;\")))"},
		{"&#x;", "(BLOCK (PARA (TEXT \"&#x;\")))"},
		{"&#x0z;", "(BLOCK (PARA (TEXT \"&#x0z;\")))"},
		{"&1;", "(BLOCK (PARA (TEXT \"&1;\")))"},
		{"&#9;", "(BLOCK (PARA (TEXT \"&#9;\")))"}, // Numeric entities below space are not allowed.
		{"&#x1f;", "(BLOCK (PARA (TEXT \"&#x1f;\")))"},

		// Good cases
		{"&lt;", "(INLINE (TEXT \"<\"))"},
		{"&#48;", "(INLINE (TEXT \"0\"))"},
		{"&#x4A;", "(INLINE (TEXT \"J\"))"},
		{"&#X4a;", "(INLINE (TEXT \"J\"))"},
		{"&hellip;", "(INLINE (TEXT \"\u2026\"))"},
		{"&nbsp;", "(INLINE (TEXT \"\u00a0\"))"},
		{"E: &amp;,&#63;;&#x63;.", "(INLINE (TEXT \"E: &,?;c.\"))"},
		{"&lt;", "(BLOCK (PARA (TEXT \"<\")))"},
		{"&#48;", "(BLOCK (PARA (TEXT \"0\")))"},
		{"&#x4A;", "(BLOCK (PARA (TEXT \"J\")))"},
		{"&#X4a;", "(BLOCK (PARA (TEXT \"J\")))"},
		{"&hellip;", "(BLOCK (PARA (TEXT \"\u2026\")))"},
		{"&nbsp;", "(BLOCK (PARA (TEXT \"\u00a0\")))"},
		{"E: &amp;,&#63;;&#x63;.", "(BLOCK (PARA (TEXT \"E: &,?;c.\")))"},
	})
}

func TestVerbatimZettel(t *testing.T) {
	t.Parallel()
	checkTcs(t, true, TestCases{
	checkTcs(t, TestCases{
		{"@@@\n@@@", "()"},
		{"@@@\nabc\n@@@", "(BLOCK (VERBATIM-ZETTEL () \"abc\"))"},
		{"@@@@def\nabc\n@@@@", "(BLOCK (VERBATIM-ZETTEL ((\"\" . \"def\")) \"abc\"))"},
	})
}

func TestVerbatimCode(t *testing.T) {
	t.Parallel()
	checkTcs(t, true, TestCases{
	checkTcs(t, TestCases{
		{"```\n```", "()"},
		{"```\nabc\n```", "(BLOCK (VERBATIM-CODE () \"abc\"))"},
		{"```\nabc\n````", "(BLOCK (VERBATIM-CODE () \"abc\"))"},
		{"````\nabc\n````", "(BLOCK (VERBATIM-CODE () \"abc\"))"},
		{"````\nabc\n```\n````", "(BLOCK (VERBATIM-CODE () \"abc\\n```\"))"},
		{"````go\nabc\n````", "(BLOCK (VERBATIM-CODE ((\"\" . \"go\")) \"abc\"))"},
	})
}

func TestVerbatimEval(t *testing.T) {
	t.Parallel()
	checkTcs(t, true, TestCases{
	checkTcs(t, TestCases{
		{"~~~\n~~~", "()"},
		{"~~~\nabc\n~~~", "(BLOCK (VERBATIM-EVAL () \"abc\"))"},
		{"~~~\nabc\n~~~~", "(BLOCK (VERBATIM-EVAL () \"abc\"))"},
		{"~~~~\nabc\n~~~~", "(BLOCK (VERBATIM-EVAL () \"abc\"))"},
		{"~~~~\nabc\n~~~\n~~~~", "(BLOCK (VERBATIM-EVAL () \"abc\\n~~~\"))"},
		{"~~~~go\nabc\n~~~~", "(BLOCK (VERBATIM-EVAL ((\"\" . \"go\")) \"abc\"))"},
	})
}

func TestVerbatimMath(t *testing.T) {
	t.Parallel()
	checkTcs(t, true, TestCases{
	checkTcs(t, TestCases{
		{"$$$\n$$$", "()"},
		{"$$$\nabc\n$$$", "(BLOCK (VERBATIM-MATH () \"abc\"))"},
		{"$$$\nabc\n$$$$", "(BLOCK (VERBATIM-MATH () \"abc\"))"},
		{"$$$$\nabc\n$$$$", "(BLOCK (VERBATIM-MATH () \"abc\"))"},
		{"$$$$\nabc\n$$$\n$$$$", "(BLOCK (VERBATIM-MATH () \"abc\\n$$$\"))"},
		{"$$$$go\nabc\n$$$$", "(BLOCK (VERBATIM-MATH ((\"\" . \"go\")) \"abc\"))"},
	})
}

func TestVerbatimComment(t *testing.T) {
	t.Parallel()
	checkTcs(t, true, TestCases{
	checkTcs(t, TestCases{
		{"%%%\n%%%", "()"},
		{"%%%\nabc\n%%%", "(BLOCK (VERBATIM-COMMENT () \"abc\"))"},
		{"%%%%go\nabc\n%%%%", "(BLOCK (VERBATIM-COMMENT ((\"\" . \"go\")) \"abc\"))"},
	})
}

func TestPara(t *testing.T) {
	t.Parallel()
	checkTcs(t, true, TestCases{
	checkTcs(t, TestCases{
		{"a\n\nb", "(BLOCK (PARA (TEXT \"a\")) (PARA (TEXT \"b\")))"},
		{"a\n \nb", "(BLOCK (PARA (TEXT \"a\") (SOFT) (HARD) (TEXT \"b\")))"},
		{"a\n \nb", "(BLOCK (PARA (TEXT \"a\")) (PARA (TEXT \"b\")))"},
	})
}

func TestSpanRegion(t *testing.T) {
	t.Parallel()
	checkTcs(t, true, TestCases{
	checkTcs(t, TestCases{
		{":::\n:::", "()"},
		{":::\nabc\n:::", "(BLOCK (REGION-BLOCK () ((PARA (TEXT \"abc\")))))"},
		{":::\nabc\n::::", "(BLOCK (REGION-BLOCK () ((PARA (TEXT \"abc\")))))"},
		{"::::\nabc\n::::", "(BLOCK (REGION-BLOCK () ((PARA (TEXT \"abc\")))))"},
		{"::::\nabc\n:::\ndef\n:::\n::::", "(BLOCK (REGION-BLOCK () ((PARA (TEXT \"abc\")) (REGION-BLOCK () ((PARA (TEXT \"def\")))))))"},
		{":::{go}\n:::a", "(BLOCK (REGION-BLOCK ((\"go\" . \"\")) () (TEXT \"a\")))"},
		{":::\nabc\n::: def ", "(BLOCK (REGION-BLOCK () ((PARA (TEXT \"abc\"))) (TEXT \"def\")))"},
	})
}

func TestQuoteRegion(t *testing.T) {
	t.Parallel()
	checkTcs(t, true, TestCases{
	checkTcs(t, TestCases{
		{"<<<\n<<<", "()"},
		{"<<<\nabc\n<<<", "(BLOCK (REGION-QUOTE () ((PARA (TEXT \"abc\")))))"},
		{"<<<\nabc\n<<<<", "(BLOCK (REGION-QUOTE () ((PARA (TEXT \"abc\")))))"},
		{"<<<<\nabc\n<<<<", "(BLOCK (REGION-QUOTE () ((PARA (TEXT \"abc\")))))"},
		{"<<<<\nabc\n<<<\ndef\n<<<\n<<<<", "(BLOCK (REGION-QUOTE () ((PARA (TEXT \"abc\")) (REGION-QUOTE () ((PARA (TEXT \"def\")))))))"},
		{"<<<go\n<<< a", "(BLOCK (REGION-QUOTE ((\"\" . \"go\")) () (TEXT \"a\")))"},
		{"<<<\nabc\n<<< def ", "(BLOCK (REGION-QUOTE () ((PARA (TEXT \"abc\"))) (TEXT \"def\")))"},
	})
}

func TestVerseRegion(t *testing.T) {
	t.Parallel()
	checkTcs(t, true, replace("\"", nil, TestCases{
	checkTcs(t, replace("\"", nil, TestCases{
		{"$$$\n$$$", "()"},
		{"$$$\nabc\n$$$", "(BLOCK (REGION-VERSE () ((PARA (TEXT \"abc\")))))"},
		{"$$$\nabc\n$$$$", "(BLOCK (REGION-VERSE () ((PARA (TEXT \"abc\")))))"},
		{"$$$$\nabc\n$$$$", "(BLOCK (REGION-VERSE () ((PARA (TEXT \"abc\")))))"},
		{"$$$\nabc\ndef\n$$$", "(BLOCK (REGION-VERSE () ((PARA (TEXT \"abc\") (HARD) (TEXT \"def\")))))"},
		{"$$$$\nabc\n$$$\ndef\n$$$\n$$$$", "(BLOCK (REGION-VERSE () ((PARA (TEXT \"abc\")) (REGION-VERSE () ((PARA (TEXT \"def\")))))))"},
		{"$$$go\n$$$x", "(BLOCK (REGION-VERSE ((\"\" . \"go\")) () (TEXT \"x\")))"},
		{"$$$\nabc\n$$$ def ", "(BLOCK (REGION-VERSE () ((PARA (TEXT \"abc\"))) (TEXT \"def\")))"},
		{"$$$\n space \n$$$", "(BLOCK (REGION-VERSE () ((PARA (TEXT \" space\")))))"},
		{"$$$\n  spaces  \n$$$", "(BLOCK (REGION-VERSE () ((PARA (TEXT \"  spaces\")))))"},
		{"$$$\n  spaces  \n space  \n$$$", "(BLOCK (REGION-VERSE () ((PARA (TEXT \"  spaces  \") (HARD) (TEXT \" space\")))))"},
		{"$$$\n space \n$$$", "(BLOCK (REGION-VERSE () ((PARA (TEXT \"\u00a0space\u00a0\")))))"},
		{"$$$\n  spaces  \n$$$", "(BLOCK (REGION-VERSE () ((PARA (TEXT \"\u00a0\u00a0spaces\u00a0\u00a0\")))))"},
		{"$$$\n  spaces  \n space  \n$$$", "(BLOCK (REGION-VERSE () ((PARA (TEXT \"\u00a0\u00a0spaces\u00a0\u00a0\") (HARD) (TEXT \"\u00a0space\u00a0\u00a0\")))))"},
		{"$$$\n space space \n$$$", "(BLOCK (REGION-VERSE () ((PARA (TEXT \"\u00a0space\u00a0space\u00a0\")))))"},
	}))
}

func TestHeading(t *testing.T) {
	t.Parallel()
	checkTcs(t, true, TestCases{
	checkTcs(t, TestCases{
		{"=h", "(BLOCK (PARA (TEXT \"=h\")))"},
		{"= h", "(BLOCK (PARA (TEXT \"= h\")))"},
		{"==h", "(BLOCK (PARA (TEXT \"==h\")))"},
		{"== h", "(BLOCK (PARA (TEXT \"== h\")))"},
		{"===h", "(BLOCK (PARA (TEXT \"===h\")))"},
		{"===", "(BLOCK (PARA (TEXT \"===\")))"},
		{"=== h", "(BLOCK (HEADING 1 () \"\" \"\" (TEXT \"h\")))"},
		{"===  h", "(BLOCK (HEADING 1 () \"\" \"\" (TEXT \"h\")))"},
		{"==== h", "(BLOCK (HEADING 2 () \"\" \"\" (TEXT \"h\")))"},
		{"===== h", "(BLOCK (HEADING 3 () \"\" \"\" (TEXT \"h\")))"},
		{"====== h", "(BLOCK (HEADING 4 () \"\" \"\" (TEXT \"h\")))"},
		{"======= h", "(BLOCK (HEADING 5 () \"\" \"\" (TEXT \"h\")))"},
		{"======== h", "(BLOCK (HEADING 5 () \"\" \"\" (TEXT \"h\")))"},
		{"=", "(BLOCK (PARA (TEXT \"=\")))"},
		{"=== h=__=a__", "(BLOCK (HEADING 1 () \"\" \"\" (TEXT \"h=\") (FORMAT-EMPH () (TEXT \"=a\"))))"},
		{"=\n", "(BLOCK (PARA (TEXT \"=\")))"},
		{"a=", "(BLOCK (PARA (TEXT \"a=\")))"},
		{" =", "(BLOCK (PARA (TEXT \"=\")))"},
		{"=== h\na", "(BLOCK (HEADING 1 () \"\" \"\" (TEXT \"h\")) (PARA (TEXT \"a\")))"},
		{"=== h i {-}", "(BLOCK (HEADING 1 ((\"-\" . \"\")) \"\" \"\" (TEXT \"h i\")))"},
		{"=== h {{a}}", "(BLOCK (HEADING 1 () \"\" \"\" (TEXT \"h \") (EMBED () \"a\")))"},
		{"=== h{{a}}", "(BLOCK (HEADING 1 () \"\" \"\" (TEXT \"h\") (EMBED () \"a\")))"},
		{"=== {{a}}", "(BLOCK (HEADING 1 () \"\" \"\" (EMBED () \"a\")))"},
		{"=== h {{a}}{-}", "(BLOCK (HEADING 1 () \"\" \"\" (TEXT \"h \") (EMBED ((\"-\" . \"\")) \"a\")))"},
		{"=== h {{a}} {-}", "(BLOCK (HEADING 1 ((\"-\" . \"\")) \"\" \"\" (TEXT \"h \") (EMBED () \"a\")))"},
		{"=== h {{a}}", "(BLOCK (HEADING 1 () \"\" \"\" (TEXT \"h \") (EMBED () (EXTERNAL \"a\") \"\")))"},
		{"=== h{{a}}", "(BLOCK (HEADING 1 () \"\" \"\" (TEXT \"h\") (EMBED () (EXTERNAL \"a\") \"\")))"},
		{"=== {{a}}", "(BLOCK (HEADING 1 () \"\" \"\" (EMBED () (EXTERNAL \"a\") \"\")))"},
		{"=== h {{a}}{-}", "(BLOCK (HEADING 1 () \"\" \"\" (TEXT \"h \") (EMBED ((\"-\" . \"\")) (EXTERNAL \"a\") \"\")))"},
		{"=== h {{a}} {-}", "(BLOCK (HEADING 1 ((\"-\" . \"\")) \"\" \"\" (TEXT \"h \") (EMBED () (EXTERNAL \"a\") \"\")))"},
		{"=== h {-}{{a}}", "(BLOCK (HEADING 1 ((\"-\" . \"\")) \"\" \"\" (TEXT \"h\")))"},
		{"=== h{id=abc}", "(BLOCK (HEADING 1 ((\"id\" . \"abc\")) \"\" \"\" (TEXT \"h\")))"},
		{"=== h\n=== h", "(BLOCK (HEADING 1 () \"\" \"\" (TEXT \"h\")) (HEADING 1 () \"\" \"\" (TEXT \"h\")))"},
	})
}

func TestHRule(t *testing.T) {
	t.Parallel()
	checkTcs(t, true, TestCases{
	checkTcs(t, TestCases{
		{"-", "(BLOCK (PARA (TEXT \"-\")))"},
		{"---", "(BLOCK (THEMATIC ()))"},
		{"----", "(BLOCK (THEMATIC ()))"},
		{"---A", "(BLOCK (THEMATIC ((\"\" . \"A\"))))"},
		{"---A-", "(BLOCK (THEMATIC ((\"\" . \"A-\"))))"},
		{"-1", "(BLOCK (PARA (TEXT \"-1\")))"},
		{"2-1", "(BLOCK (PARA (TEXT \"2-1\")))"},
		{"---  {  go  }  ", "(BLOCK (THEMATIC ((\"go\" . \"\"))))"},
		{"---  {  .go  }  ", "(BLOCK (THEMATIC ((\"class\" . \"go\"))))"},
		{`---{lang=zmk}`, "(BLOCK (THEMATIC ((\"lang\" . \"zmk\"))))"},
		{`---{lang="zmk"}`, "(BLOCK (THEMATIC ((\"lang\" . \"zmk\"))))"},
	})
}

func TestList(t *testing.T) {
	t.Parallel()
	// No ">" in the following, because quotation lists may have empty items.
	for _, ch := range []string{"*", "#"} {
		checkTcs(t, true, replace(ch, nil, TestCases{
		checkTcs(t, replace(ch, nil, TestCases{
			{"$", "(BLOCK (PARA (TEXT \"$\")))"},
			{"$$", "(BLOCK (PARA (TEXT \"$$\")))"},
			{"$$$", "(BLOCK (PARA (TEXT \"$$$\")))"},
			{"$ ", "(BLOCK (PARA (TEXT \"$\")))"},
			{"$$ ", "(BLOCK (PARA (TEXT \"$$\")))"},
			{"$$$ ", "(BLOCK (PARA (TEXT \"$$$\")))"},
		}))
	}
	checkTcs(t, true, TestCases{
	checkTcs(t, TestCases{
		{"* abc", "(BLOCK (UNORDERED (BLOCK (PARA (TEXT \"abc\")))))"},
		{"** abc", "(BLOCK (UNORDERED (BLOCK (UNORDERED (BLOCK (PARA (TEXT \"abc\")))))))"},
		{"*** abc", "(BLOCK (UNORDERED (BLOCK (UNORDERED (BLOCK (UNORDERED (BLOCK (PARA (TEXT \"abc\")))))))))"},
		{"**** abc", "(BLOCK (UNORDERED (BLOCK (UNORDERED (BLOCK (UNORDERED (BLOCK (UNORDERED (BLOCK (PARA (TEXT \"abc\")))))))))))"},
		{"** abc\n**** def", "(BLOCK (UNORDERED (BLOCK (UNORDERED (BLOCK (PARA (TEXT \"abc\")) (UNORDERED (BLOCK (UNORDERED (BLOCK (PARA (TEXT \"def\")))))))))))"},
		{"* abc\ndef", "(BLOCK (UNORDERED (BLOCK (PARA (TEXT \"abc\")))) (PARA (TEXT \"def\")))"},
		{"* abc\n def", "(BLOCK (UNORDERED (BLOCK (PARA (TEXT \"abc\")))) (PARA (TEXT \"def\")))"},
701
702
703
704
705
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
733
734
735
736
737
738
739
740
741
742


743
744
745
746
747
748
749
750
751
752
753
754
755
756

757
758
759
760
761
762
763
688
689
690
691
692
693
694

695
696
697
698
699
700
701
702
703
704
705

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
733
734
735
736
737
738
739
740
741
742
743

744
745
746
747
748
749
750
751







-
+










-
+







-
+














-
+
+













-
+







		// Empty continuation
		{"* abc\n  ", "(BLOCK (UNORDERED (BLOCK (PARA (TEXT \"abc\")))))"},
	})
}

func TestQuoteList(t *testing.T) {
	t.Parallel()
	checkTcs(t, true, TestCases{
	checkTcs(t, TestCases{
		{"> w1 w2", "(BLOCK (QUOTATION (BLOCK (PARA (TEXT \"w1 w2\")))))"},
		{"> w1\n> w2", "(BLOCK (QUOTATION (BLOCK (PARA (TEXT \"w1\") (SOFT) (TEXT \"w2\")))))"},
		{"> w1\n>w2", "(BLOCK (QUOTATION (BLOCK (PARA (TEXT \"w1\")))) (PARA (TEXT \">w2\")))"},
		{"> w1\n>\n>w2", "(BLOCK (QUOTATION (BLOCK (PARA (TEXT \"w1\"))) (BLOCK)) (PARA (TEXT \">w2\")))"},
		{"> w1\n> \n> w2", "(BLOCK (QUOTATION (BLOCK (PARA (TEXT \"w1\"))) (BLOCK) (BLOCK (PARA (TEXT \"w2\")))))"},
	})
}

func TestEnumAfterPara(t *testing.T) {
	t.Parallel()
	checkTcs(t, true, TestCases{
	checkTcs(t, TestCases{
		{"abc\n* def", "(BLOCK (PARA (TEXT \"abc\")) (UNORDERED (BLOCK (PARA (TEXT \"def\")))))"},
		{"abc\n*def", "(BLOCK (PARA (TEXT \"abc\") (SOFT) (TEXT \"*def\")))"},
	})
}

func TestDefinition(t *testing.T) {
	t.Parallel()
	checkTcs(t, true, TestCases{
	checkTcs(t, TestCases{
		{";", "(BLOCK (PARA (TEXT \";\")))"},
		{"; ", "(BLOCK (PARA (TEXT \";\")))"},
		{"; abc", "(BLOCK (DESCRIPTION ((TEXT \"abc\"))))"},
		{"; abc\ndef", "(BLOCK (DESCRIPTION ((TEXT \"abc\"))) (PARA (TEXT \"def\")))"},
		{"; abc\n def", "(BLOCK (DESCRIPTION ((TEXT \"abc\"))) (PARA (TEXT \"def\")))"},
		{"; abc\n  def", "(BLOCK (DESCRIPTION ((TEXT \"abc\") (SOFT) (TEXT \"def\"))))"},
		{"; abc\n  def\n  ghi", "(BLOCK (DESCRIPTION ((TEXT \"abc\") (SOFT) (TEXT \"def\") (SOFT) (TEXT \"ghi\"))))"},
		{":", "(BLOCK (PARA (TEXT \":\")))"},
		{": ", "(BLOCK (PARA (TEXT \":\")))"},
		{": abc", "(BLOCK (PARA (TEXT \": abc\")))"},
		{"; abc\n: def", "(BLOCK (DESCRIPTION ((TEXT \"abc\")) (BLOCK (BLOCK (PARA (TEXT \"def\"))))))"},
		{"; abc\n: def\nghi", "(BLOCK (DESCRIPTION ((TEXT \"abc\")) (BLOCK (BLOCK (PARA (TEXT \"def\"))))) (PARA (TEXT \"ghi\")))"},
		{"; abc\n: def\n ghi", "(BLOCK (DESCRIPTION ((TEXT \"abc\")) (BLOCK (BLOCK (PARA (TEXT \"def\"))))) (PARA (TEXT \"ghi\")))"},
		{"; abc\n: def\n  ghi", "(BLOCK (DESCRIPTION ((TEXT \"abc\")) (BLOCK (BLOCK (PARA (TEXT \"def\") (SOFT) (TEXT \"ghi\"))))))"},
		{"; abc\n: def\n\n  ghi", "(BLOCK (DESCRIPTION ((TEXT \"abc\")) (BLOCK (BLOCK (PARA (TEXT \"def\"))) (PARA (TEXT \"ghi\")))))"},
		{"; abc\n: def\n\n  ghi", "(BLOCK (DESCRIPTION ((TEXT \"abc\")) (BLOCK (BLOCK (PARA (TEXT \"def\")) (PARA (TEXT \"ghi\"))))))"},
		{"; abc\n: def\n\n  ghi\n\n  jkl", "(BLOCK (DESCRIPTION ((TEXT \"abc\")) (BLOCK (BLOCK (PARA (TEXT \"def\")) (PARA (TEXT \"ghi\")) (PARA (TEXT \"jkl\"))))))"},
		{"; abc\n:", "(BLOCK (DESCRIPTION ((TEXT \"abc\"))) (PARA (TEXT \":\")))"},
		{"; abc\n: def\n: ghi", "(BLOCK (DESCRIPTION ((TEXT \"abc\")) (BLOCK (BLOCK (PARA (TEXT \"def\"))) (BLOCK (PARA (TEXT \"ghi\"))))))"},
		{"; abc\n: def\n; ghi\n: jkl", "(BLOCK (DESCRIPTION ((TEXT \"abc\")) (BLOCK (BLOCK (PARA (TEXT \"def\")))) ((TEXT \"ghi\")) (BLOCK (BLOCK (PARA (TEXT \"jkl\"))))))"},

		// Empty description
		{"; abc\n: ", "(BLOCK (DESCRIPTION ((TEXT \"abc\"))) (PARA (TEXT \":\")))"},
		// Empty continuation of definition
		{"; abc\n: def\n  ", "(BLOCK (DESCRIPTION ((TEXT \"abc\")) (BLOCK (BLOCK (PARA (TEXT \"def\"))))))"},
	})
}

func TestTable(t *testing.T) {
	t.Parallel()
	checkTcs(t, true, TestCases{
	checkTcs(t, TestCases{
		{"|", "()"},
		{"||", "(BLOCK (TABLE () ((CELL))))"},
		{"| |", "(BLOCK (TABLE () ((CELL))))"},
		{"|a", "(BLOCK (TABLE () ((CELL (TEXT \"a\")))))"},
		{"|a|", "(BLOCK (TABLE () ((CELL (TEXT \"a\")))))"},
		{"|a| ", "(BLOCK (TABLE () ((CELL (TEXT \"a\")) (CELL))))"},
		{"|a|b", "(BLOCK (TABLE () ((CELL (TEXT \"a\")) (CELL (TEXT \"b\")))))"},
771
772
773
774
775
776
777
778

779
780
781
782
783
784

785
786
787
788
789
790
791

792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811

812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827

828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845

846
847
848
849
850
851
852
853

854
855
856
857
858
859
860
861

862
863
864
759
760
761
762
763
764
765

766
767
768
769
770
771

772
773
774
775
776
777
778

779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798

799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814

815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832

833
834
835
836
837
838
839
840

841
842
843
844
845
846
847
848

849
850
851
852







-
+





-
+






-
+



















-
+















-
+

















-
+







-
+







-
+



		{"|=<a>\n|b|c", "(BLOCK (TABLE ((CELL-LEFT (TEXT \"a\")) (CELL)) ((CELL-RIGHT (TEXT \"b\")) (CELL (TEXT \"c\")))))"},
		{"|=<a|=b>\n||", "(BLOCK (TABLE ((CELL-LEFT (TEXT \"a\")) (CELL-RIGHT (TEXT \"b\"))) ((CELL) (CELL-RIGHT))))"},
	})
}

func TestTransclude(t *testing.T) {
	t.Parallel()
	checkTcs(t, true, TestCases{
	checkTcs(t, TestCases{
		{"{{{a}}}", "(BLOCK (TRANSCLUDE () (EXTERNAL \"a\")))"},
		{"{{{a}}}b", "(BLOCK (TRANSCLUDE ((\"\" . \"b\")) (EXTERNAL \"a\")))"},
		{"{{{a}}}}", "(BLOCK (TRANSCLUDE () (EXTERNAL \"a\")))"},
		{"{{{a\\}}}}", "(BLOCK (TRANSCLUDE () (EXTERNAL \"a\\\\}\")))"},
		{"{{{a\\}}}}b", "(BLOCK (TRANSCLUDE ((\"\" . \"b\")) (EXTERNAL \"a\\\\}\")))"},
		{"{{{a}}", "(BLOCK (PARA (TEXT \"{\") (EMBED () \"a\")))"},
		{"{{{a}}", "(BLOCK (PARA (TEXT \"{\") (EMBED () (EXTERNAL \"a\") \"\")))"},
		{"{{{a}}}{go=b}", "(BLOCK (TRANSCLUDE ((\"go\" . \"b\")) (EXTERNAL \"a\")))"},
	})
}

func TestBlockAttr(t *testing.T) {
	t.Parallel()
	checkTcs(t, true, TestCases{
	checkTcs(t, TestCases{
		{":::go\na\n:::", "(BLOCK (REGION-BLOCK ((\"\" . \"go\")) ((PARA (TEXT \"a\")))))"},
		{":::go=\na\n:::", "(BLOCK (REGION-BLOCK ((\"\" . \"go\")) ((PARA (TEXT \"a\")))))"},
		{":::{}\na\n:::", "(BLOCK (REGION-BLOCK () ((PARA (TEXT \"a\")))))"},
		{":::{ }\na\n:::", "(BLOCK (REGION-BLOCK () ((PARA (TEXT \"a\")))))"},
		{":::{.go}\na\n:::", "(BLOCK (REGION-BLOCK ((\"class\" . \"go\")) ((PARA (TEXT \"a\")))))"},
		{":::{=go}\na\n:::", "(BLOCK (REGION-BLOCK ((\"\" . \"go\")) ((PARA (TEXT \"a\")))))"},
		{":::{go}\na\n:::", "(BLOCK (REGION-BLOCK ((\"go\" . \"\")) ((PARA (TEXT \"a\")))))"},
		{":::{go=py}\na\n:::", "(BLOCK (REGION-BLOCK ((\"go\" . \"py\")) ((PARA (TEXT \"a\")))))"},
		{":::{.go=py}\na\n:::", "(BLOCK (REGION-BLOCK () ((PARA (TEXT \"a\")))))"},
		{":::{go=}\na\n:::", "(BLOCK (REGION-BLOCK ((\"go\" . \"\")) ((PARA (TEXT \"a\")))))"},
		{":::{.go=}\na\n:::", "(BLOCK (REGION-BLOCK () ((PARA (TEXT \"a\")))))"},
		{":::{go py}\na\n:::", "(BLOCK (REGION-BLOCK ((\"go\" . \"\") (\"py\" . \"\")) ((PARA (TEXT \"a\")))))"},
		{":::{go\npy}\na\n:::", "(BLOCK (REGION-BLOCK ((\"go\" . \"\") (\"py\" . \"\")) ((PARA (TEXT \"a\")))))"},
		{":::{.go py}\na\n:::", "(BLOCK (REGION-BLOCK ((\"class\" . \"go\") (\"py\" . \"\")) ((PARA (TEXT \"a\")))))"},
		{":::{go .py}\na\n:::", "(BLOCK (REGION-BLOCK ((\"class\" . \"py\") (\"go\" . \"\")) ((PARA (TEXT \"a\")))))"},
		{":::{.go py=3}\na\n:::", "(BLOCK (REGION-BLOCK ((\"class\" . \"go\") (\"py\" . \"3\")) ((PARA (TEXT \"a\")))))"},
		{":::  {  go  }  \na\n:::", "(BLOCK (REGION-BLOCK ((\"go\" . \"\")) ((PARA (TEXT \"a\")))))"},
		{":::  {  .go  }  \na\n:::", "(BLOCK (REGION-BLOCK ((\"class\" . \"go\")) ((PARA (TEXT \"a\")))))"},
	})
	checkTcs(t, true, replace("\"", nil, TestCases{
	checkTcs(t, replace("\"", nil, TestCases{
		{":::{py=3}\na\n:::", "(BLOCK (REGION-BLOCK ((\"py\" . \"3\")) ((PARA (TEXT \"a\")))))"},
		{":::{py=$2 3$}\na\n:::", "(BLOCK (REGION-BLOCK ((\"py\" . \"2 3\")) ((PARA (TEXT \"a\")))))"},
		{":::{py=$2\\$3$}\na\n:::", "(BLOCK (REGION-BLOCK ((\"py\" . \"2\\\"3\")) ((PARA (TEXT \"a\")))))"},
		{":::{py=2$3}\na\n:::", "(BLOCK (REGION-BLOCK ((\"py\" . \"2\\\"3\")) ((PARA (TEXT \"a\")))))"},
		{":::{py=$2\n3$}\na\n:::", "(BLOCK (REGION-BLOCK ((\"py\" . \"2\\n3\")) ((PARA (TEXT \"a\")))))"},
		{":::{py=$2 3}\na\n:::", "(BLOCK (REGION-BLOCK () ((PARA (TEXT \"a\")))))"},
		{":::{py=2 py=3}\na\n:::", "(BLOCK (REGION-BLOCK ((\"py\" . \"2 3\")) ((PARA (TEXT \"a\")))))"},
		{":::{.go .py}\na\n:::", "(BLOCK (REGION-BLOCK ((\"class\" . \"go py\")) ((PARA (TEXT \"a\")))))"},
		{":::{go go}\na\n:::", "(BLOCK (REGION-BLOCK ((\"go\" . \"\")) ((PARA (TEXT \"a\")))))"},
		{":::{=py =go}\na\n:::", "(BLOCK (REGION-BLOCK ((\"\" . \"go\")) ((PARA (TEXT \"a\")))))"},
	}))
}

func TestInlineAttr(t *testing.T) {
	t.Parallel()
	checkTcs(t, true, TestCases{
	checkTcs(t, TestCases{
		{"::a::{}", "(BLOCK (PARA (FORMAT-SPAN () (TEXT \"a\"))))"},
		{"::a::{ }", "(BLOCK (PARA (FORMAT-SPAN () (TEXT \"a\"))))"},
		{"::a::{.go}", "(BLOCK (PARA (FORMAT-SPAN ((\"class\" . \"go\")) (TEXT \"a\"))))"},
		{"::a::{=go}", "(BLOCK (PARA (FORMAT-SPAN ((\"\" . \"go\")) (TEXT \"a\"))))"},
		{"::a::{go}", "(BLOCK (PARA (FORMAT-SPAN ((\"go\" . \"\")) (TEXT \"a\"))))"},
		{"::a::{go=py}", "(BLOCK (PARA (FORMAT-SPAN ((\"go\" . \"py\")) (TEXT \"a\"))))"},
		{"::a::{.go=py}", "(BLOCK (PARA (FORMAT-SPAN () (TEXT \"a\")) (TEXT \"{.go=py}\")))"},
		{"::a::{go=}", "(BLOCK (PARA (FORMAT-SPAN ((\"go\" . \"\")) (TEXT \"a\"))))"},
		{"::a::{.go=}", "(BLOCK (PARA (FORMAT-SPAN () (TEXT \"a\")) (TEXT \"{.go=}\")))"},
		{"::a::{go py}", "(BLOCK (PARA (FORMAT-SPAN ((\"go\" . \"\") (\"py\" . \"\")) (TEXT \"a\"))))"},
		{"::a::{go\npy}", "(BLOCK (PARA (FORMAT-SPAN ((\"go\" . \"\") (\"py\" . \"\")) (TEXT \"a\"))))"},
		{"::a::{.go py}", "(BLOCK (PARA (FORMAT-SPAN ((\"class\" . \"go\") (\"py\" . \"\")) (TEXT \"a\"))))"},
		{"::a::{go .py}", "(BLOCK (PARA (FORMAT-SPAN ((\"class\" . \"py\") (\"go\" . \"\")) (TEXT \"a\"))))"},
		{"::a::{  \n go \n .py\n  \n}", "(BLOCK (PARA (FORMAT-SPAN ((\"class\" . \"py\") (\"go\" . \"\")) (TEXT \"a\"))))"},
		{"::a::{  \n go \n .py\n\n}", "(BLOCK (PARA (FORMAT-SPAN ((\"class\" . \"py\") (\"go\" . \"\")) (TEXT \"a\"))))"},
		{"::a::{\ngo\n}", "(BLOCK (PARA (FORMAT-SPAN ((\"go\" . \"\")) (TEXT \"a\"))))"},
	})
	checkTcs(t, true, TestCases{
	checkTcs(t, TestCases{
		{"::a::{py=3}", "(BLOCK (PARA (FORMAT-SPAN ((\"py\" . \"3\")) (TEXT \"a\"))))"},
		{"::a::{py=\"2 3\"}", "(BLOCK (PARA (FORMAT-SPAN ((\"py\" . \"2 3\")) (TEXT \"a\"))))"},
		{"::a::{py=\"2\\\"3\"}", "(BLOCK (PARA (FORMAT-SPAN ((\"py\" . \"2\\\"3\")) (TEXT \"a\"))))"},
		{"::a::{py=2\"3}", "(BLOCK (PARA (FORMAT-SPAN ((\"py\" . \"2\\\"3\")) (TEXT \"a\"))))"},
		{"::a::{py=\"2\n3\"}", "(BLOCK (PARA (FORMAT-SPAN ((\"py\" . \"2\\n3\")) (TEXT \"a\"))))"},
		{"::a::{py=\"2 3}", "(BLOCK (PARA (FORMAT-SPAN () (TEXT \"a\")) (TEXT \"{py=\\\"2 3}\")))"},
	})
	checkTcs(t, true, TestCases{
	checkTcs(t, TestCases{
		{"::a::{py=2 py=3}", "(BLOCK (PARA (FORMAT-SPAN ((\"py\" . \"2 3\")) (TEXT \"a\"))))"},
		{"::a::{.go .py}", "(BLOCK (PARA (FORMAT-SPAN ((\"class\" . \"go py\")) (TEXT \"a\"))))"},
	})
}

func TestTemp(t *testing.T) {
	t.Parallel()
	checkTcs(t, true, TestCases{
	checkTcs(t, TestCases{
		{"", "()"},
	})
}

Changes to text/text.go.

45
46
47
48
49
50
51
52
53


54
55
56
57
58
59
60
45
46
47
48
49
50
51


52
53
54
55
56
57
58
59
60







-
-
+
+








// EvaluateInlineString returns the text content of the given inline list as a string.
func EvaluateInlineString(lst *sx.Pair) string {
	return NewEncoder().Encode(lst)
}

func (enc *Encoder) executeList(lst *sx.Pair) {
	for elem := lst; elem != nil; elem = elem.Tail() {
		enc.execute(elem.Car())
	for obj := range lst.Values() {
		enc.execute(obj)
	}
}
func (enc *Encoder) execute(obj sx.Object) {
	cmd, isPair := sx.GetPair(obj)
	if !isPair {
		return
	}

Changes to www/changes.wiki.

1
2
3
4




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




+
+
+
+







<title>Change Log</title>

<a name="0_20"></a>
<h2>Changes for Version 0.20.0 (pending)</h2>
  *  Add Zettelmarkup-Parser that translates to sz expressions
  *  Add domain specific data structure from main zettelstore
  *  <code>client.QueryZettelData</code> will support the new encoding of
     result lists. This make the client incompatible to version 0.19.

<a name="0_19"></a>
<h2>Changes for Version 0.19.0 (2024-12-13)</h2>
  *  Remove support for rename operation; removed all associated constants
  *  Make quote handling in shtml public, to be used by other encoders
  *  shtml generates external links with rel attribute
  *  Add some input handling methods

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



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





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


-
+

-
-
-
+
+
+
-

-
-
-
-
+
+
+
+

-
+


-
+

-
+
+

-
-
-
+
+
+
<title>Home</title>

This repository contains Go client software to
access [https://zettelstore.de|Zettelstore] via its API.

<h3>Latest Release: 0.19.0 (2024-12-13)</h3>
  *  [./changes.wiki#0_19|Change summary]
  *  [/timeline?p=v0.19.0&bt=v0.18.0&y=ci|Check-ins for version 0.19],
     [/vdiff?to=v0.19.0&from=v0.18.0|content diff]
  *  [/timeline?df=v0.19.0&y=ci|Check-ins derived from the 0.19 release],
     [/vdiff?from=v0.19.0&to=trunk|content diff]
<h3>Latest Release: 0.20.0 (2024-12-13)</h3>
  *  [./changes.wiki#0_20|Change summary]
  *  [/timeline?p=v0.20.0&bt=v0.19.0&y=ci|Check-ins for version 0.20],
     [/vdiff?to=v0.20.0&from=v0.19.0|content diff]
  *  [/timeline?df=v0.20.0&y=ci|Check-ins derived from the 0.20 release],
     [/vdiff?from=v0.20.0&to=trunk|content diff]
  *  [/timeline?t=release|Timeline of all past releases]

<h3>Use instructions</h3>
<h2>Usage instructions</h2>

If you want to import this library into your own [https://go.dev/|Go] software,
you must execute a <code>go get</code> command. Since Go treats non-standard
software and non-standard platforms quite badly, you must use some non-standard
To import this library into your own [https://go.dev/|Go] software, you need to
run the <code>go get</code> command. Since Go does not handle non-standard
software and platforms well, some additional steps are required.
commands.

First, you must install the [https://fossil-scm.org|Fossil] version control
system. In too many use cases it is a superior solution compared to Git, It is
just a single executable, nothing more. Make sure, it is in your search path
for commands.
First, install the version control system [https://fossil-scm.org|Fossil],
which is a superior alternative to Git in many use cases. Fossil is just a
single executable, nothing more. Make sure it is included in your system's
command search path.

How you can execute the following Go command to retrieve a given version of
Then, run the following Go command to retrieve a specific version of
this library:

<code>GOVCS=zettelstore.de:fossil go get -x t73f.de/r/zsc@HASH</code>
<code>GOVCS=t73f.de:fossil go get t73f.de/r/zsc@HASH</code>

where <code>HASH</code> is the hash value of the commit you want to use.
Here, <code>HASH</code> represents the commit hash of the version you want to
use.

Go currently seems not to support software versions when the software is
managed by Fossil. This explains the need for the hash value. However, this
methods works and you can use the client software to access a Zettelstore.
Go currently does not seem to support software versioning for projects managed
by Fossil. This is why the hash value is required. However, this method works
reliably.