Zettelstore Client

Check-in Differences
Login

Check-in Differences

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

Difference From v0.20.0 To v0.21.0

2025-04-17
14:58
Version 0.21.0 ... (Leaf check-in: ef331b4f0c user: stern tags: release, trunk, v0.21.0)
09:21
Update changelog ... (check-in: 6f32e74255 user: stern tags: trunk)
2025-03-07
16:02
Fix date in home page ... (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)

Deleted attrs/attrs.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




















































































































-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
//-----------------------------------------------------------------------------
// 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 attrs stores attributes of zettel parts.
package attrs

import (
	"maps"
	"slices"
	"strings"
)

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

// DefaultAttribute is the value of the key of the default attribute
const DefaultAttribute = "-"

// HasDefault returns true, if the default attribute "-" has been set.
func (a Attributes) HasDefault() bool {
	if a != nil {
		_, ok := a[DefaultAttribute]
		return ok
	}
	return false
}

// RemoveDefault removes the default attribute
func (a Attributes) RemoveDefault() Attributes {
	if a != nil {
		a.Remove(DefaultAttribute)
	}
	return a
}

// Keys returns the sorted list of keys.
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 { return maps.Clone(a) }

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

// Remove the key from the attributes.
func (a Attributes) Remove(key string) Attributes {
	if a != nil {
		delete(a, key)
	}
	return a
}

// Add a value to an attribute key.
func (a Attributes) Add(key, value string) Attributes {
	if a == nil {
		return map[string]string{key: value}
	}
	values := a.Values(key)
	if !slices.Contains(values, value) {
		values = append(values, value)
		a[key] = strings.Join(values, " ")
	}
	return a
}

// Values are the space separated values of an attribute.
func (a Attributes) Values(key string) []string {
	if a != nil {
		if value, ok := a[key]; ok {
			return strings.Fields(value)
		}
	}
	return nil
}

// Has the attribute key a value?
func (a Attributes) Has(key, value string) bool {
	return slices.Contains(a.Values(key), value)
}

// AddClass adds a value to the class attribute.
func (a Attributes) AddClass(class string) Attributes { return a.Add("class", class) }

// GetClasses returns the class values as a string slice
func (a Attributes) GetClasses() []string { return a.Values("class") }

// HasClass returns true, if attributes contains the given class.
func (a Attributes) HasClass(s string) bool { return a.Has("class", s) }

Deleted attrs/attrs_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














































































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

import (
	"testing"

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

func TestHasDefault(t *testing.T) {
	t.Parallel()
	attr := attrs.Attributes{}
	if attr.HasDefault() {
		t.Error("Should not have default attr")
	}
	attr = attrs.Attributes(map[string]string{"-": "value"})
	if !attr.HasDefault() {
		t.Error("Should have default attr")
	}
}

func TestAttrClone(t *testing.T) {
	t.Parallel()
	orig := attrs.Attributes{}
	clone := orig.Clone()
	if !clone.IsEmpty() {
		t.Error("Attrs must be empty")
	}

	orig = attrs.Attributes(map[string]string{"": "0", "-": "1", "a": "b"})
	clone = orig.Clone()
	if clone[""] != "0" || clone["-"] != "1" || clone["a"] != "b" || len(clone) != len(orig) {
		t.Error("Wrong cloned map")
	}
	clone["a"] = "c"
	if orig["a"] != "b" {
		t.Error("Aliased map")
	}
}

func TestHasClass(t *testing.T) {
	t.Parallel()
	testcases := []struct {
		classes string
		class   string
		exp     bool
	}{
		{"", "", false},
		{"x", "", false},
		{"x", "x", true},
		{"x", "y", false},
		{"abc def ghi", "abc", true},
		{"abc def ghi", "def", true},
		{"abc def ghi", "ghi", true},
		{"ab de gi", "b", false},
		{"ab de gi", "d", false},
	}
	for _, tc := range testcases {
		var a attrs.Attributes
		a = a.Set("class", tc.classes)
		got := a.HasClass(tc.class)
		if tc.exp != got {
			t.Errorf("%q.HasClass(%q)=%v, but got %v", tc.classes, tc.class, tc.exp, got)
		}
	}
}

Changes to client/retrieve.go.

22
23
24
25
26
27
28
29

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

29
30
31
32
33
34
35
36







-
+







	"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"
	"t73f.de/r/zsx"
)

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

// QueryZettel returns a list of all Zettel based on the given query.
//
// query is a search expression, as described in [Query the list of all zettel].
101
102
103
104
105
106
107
108

109
110
111
112
113
114
115
101
102
103
104
105
106
107

108
109
110
111
112
113
114
115







-
+







		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
	return zsx.GoValue(qVals[1]), zsx.GoValue(hVals[1]), metaList, err
}

func parseMetaList(metaPair *sx.Pair) ([]api.ZidMetaRights, error) {
	var result []api.ZidMetaRights
	for node := metaPair; !sx.IsNil(node); {
		elem, isPair := sx.GetPair(node)
		if !isPair {
399
400
401
402
403
404
405


































406
407
408
409
410
411
412
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







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







	}

	return api.MetaRights{
		Meta:   meta,
		Rights: rights,
	}, nil
}

// GetReferences returns all references / URIs of a given zettel.
//
// part must be one of "meta", "content", or "zettel".
func (c *Client) GetReferences(ctx context.Context, zid id.Zid, part string) (urls []string, err error) {
	ub := c.NewURLBuilder('r').SetZid(zid)
	if part != "" {
		ub.AppendKVQuery(api.QueryKeyPart, part)
	}
	ub.AppendKVQuery(api.QueryKeyEncoding, api.EncodingData) // data encoding is more robust.
	resp, err := c.buildAndExecuteRequest(ctx, http.MethodGet, ub, nil)
	if err != nil {
		return nil, err
	}
	defer func() { _ = resp.Body.Close() }()
	rdr := sxreader.MakeReader(resp.Body)
	obj, err := rdr.Read()
	if resp.StatusCode != http.StatusOK {
		return nil, statusToError(resp)
	}
	if err != nil {
		return nil, err
	}
	seq, isSeq := sx.GetSequence(obj)
	if !isSeq {
		return nil, fmt.Errorf("not a sequence: %T/%v", obj, obj)
	}
	for val := range seq.Values() {
		if s, isString := sx.GetString(val); isString {
			urls = append(urls, s.GetValue())
		}
	}
	return urls, nil
}

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

Added docs/fuzz.txt.





1
2
3
4
+
+
+
+
The source code contains some simple fuzzing tests. You should call them
regulary to make sure the the software will cope with unusual input.

go test -fuzz=FuzzParseBlocks t73f.de/r/zsc/sz/zmk

Changes to domain/id/id.go.

43
44
45
46
47
48
49

50
51
52
53
54
55
56







57
58
59
60
61
62
63
43
44
45
46
47
48
49
50







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







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







	// System zettel
	ZidVersion              = Zid(1)
	ZidHost                 = Zid(2)
	ZidOperatingSystem      = Zid(3)
	ZidLicense              = Zid(4)
	ZidAuthors              = Zid(5)
	ZidDependencies         = Zid(6)
	ZidModules              = Zid(7)
	ZidLog                  = Zid(7)
	ZidMemory               = Zid(8)
	ZidSx                   = Zid(9)
	ZidHTTP                 = Zid(10)
	ZidAPI                  = Zid(11)
	ZidWebUI                = Zid(12)
	ZidConsole              = Zid(13)
	ZidLog                  = Zid(9)
	ZidMemory               = Zid(10)
	ZidSx                   = Zid(11)
	ZidHTTP                 = Zid(12)
	ZidAPI                  = Zid(13)
	ZidWebUI                = Zid(14)
	ZidConsole              = Zid(15)
	ZidBoxManager           = Zid(20)
	ZidZettel               = Zid(21)
	ZidIndex                = Zid(22)
	ZidQuery                = Zid(23)
	ZidMetadataKey          = Zid(90)
	ZidParser               = Zid(92)
	ZidStartupConfiguration = Zid(96)

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

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

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

Changes to domain/meta/parse.go.

16
17
18
19
20
21
22
23

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

23
24
25
26
27
28
29
30







-
+







import (
	"iter"
	"slices"
	"strings"

	"t73f.de/r/zero/set"
	"t73f.de/r/zsc/domain/id"
	"t73f.de/r/zsc/input"
	"t73f.de/r/zsx/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()

Changes to domain/meta/parse_test.go.

16
17
18
19
20
21
22
23

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

23
24
25
26
27
28
29
30







-
+







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

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

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

func TestEmpty(t *testing.T) {

Changes to domain/meta/values.go.

18
19
20
21
22
23
24
25

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

25
26
27
28
29
30
31
32







-
+







	"iter"
	"slices"
	"strings"
	"time"

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

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

// AsBool returns the value interpreted as a bool.
func (val Value) AsBool() bool {

Changes to go.mod.

1
2
3
4
5
6
7
8



9

10
1
2
3
4
5



6
7
8
9
10
11





-
-
-
+
+
+

+

module t73f.de/r/zsc

go 1.24

require (
	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/sx v0.0.0-20250415161954-42ed9c4d6abc
	t73f.de/r/sxwebs v0.0.0-20250415162443-110f49c5a1ae
	t73f.de/r/webs v0.0.0-20250311182734-f263a38b32d5
	t73f.de/r/zero v0.0.0-20250226205915-c4194684acb7
	t73f.de/r/zsx v0.0.0-20250415162540-fc13b286b6ce
)

Changes to go.sum.

1
2
3
4
5
6






7
8








1
2
3
4
5
6
7
8
9
10
-
-
-
-
-
-
+
+
+
+
+
+


+
+
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/sx v0.0.0-20250415161954-42ed9c4d6abc h1:tlsP+47Rf8i9Zv1TqRnwfbQx3nN/F/92RkT6iCA6SVA=
t73f.de/r/sx v0.0.0-20250415161954-42ed9c4d6abc/go.mod h1:hzg05uSCMk3D/DWaL0pdlowfL2aWQeGIfD1S04vV+Xg=
t73f.de/r/sxwebs v0.0.0-20250415162443-110f49c5a1ae h1:K6nxN/bb0BCSiDffwNPGTF2uf5WcTdxcQXzByXNuJ7M=
t73f.de/r/sxwebs v0.0.0-20250415162443-110f49c5a1ae/go.mod h1:0LQ9T1svSg9ADY/6vQLKNUu6LqpPi8FGr7fd2qDT5H8=
t73f.de/r/webs v0.0.0-20250311182734-f263a38b32d5 h1:nnKfs/2i9n3S5VjbSj98odcwZKGcL96qPSIUATT/2P8=
t73f.de/r/webs v0.0.0-20250311182734-f263a38b32d5/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=
t73f.de/r/zsx v0.0.0-20250415162540-fc13b286b6ce h1:R9rtg4ecx4YYixsMmsh+wdcqLdY9GxoC5HZ9mMS33to=
t73f.de/r/zsx v0.0.0-20250415162540-fc13b286b6ce/go.mod h1:tXOlmsQBoY4mY7Plu0LCCMZNSJZJbng98fFarZXAWvM=

Deleted input/entity.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


































































































































































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

import (
	"html"
	"unicode"
)

// ScanEntity scans either a named or a numbered entity and returns it as a string.
//
// For numbered entities (like &#123; or &#x123;) html.UnescapeString returns
// sometimes other values as expected, if the number is not well-formed. This
// may happen because of some strange HTML parsing rules. But these do not
// apply to Zettelmarkup. Therefore, I parse the number here in the code.
func (inp *Input) ScanEntity() (res string, success bool) {
	if inp.Ch != '&' {
		return "", false
	}
	pos := inp.Pos
	inp.Next()
	if inp.Ch == '#' {
		inp.Next()
		if inp.Ch == 'x' || inp.Ch == 'X' {
			return inp.scanEntityBase16()
		}
		return inp.scanEntityBase10()
	}
	return inp.scanEntityNamed(pos)
}

func (inp *Input) scanEntityBase16() (string, bool) {
	inp.Next()
	if inp.Ch == ';' {
		return "", false
	}
	code := 0
	for {
		switch ch := inp.Ch; ch {
		case ';':
			inp.Next()
			if r := rune(code); isValidEntity(r) {
				return string(r), true
			}
			return "", false
		case '0', '1', '2', '3', '4', '5', '6', '7', '8', '9':
			code = 16*code + int(ch-'0')
		case 'a', 'b', 'c', 'd', 'e', 'f':
			code = 16*code + int(ch-'a'+10)
		case 'A', 'B', 'C', 'D', 'E', 'F':
			code = 16*code + int(ch-'A'+10)
		default:
			return "", false
		}
		if code > unicode.MaxRune {
			return "", false
		}
		inp.Next()
	}
}

func (inp *Input) scanEntityBase10() (string, bool) {
	// Base 10 code
	if inp.Ch == ';' {
		return "", false
	}
	code := 0
	for {
		switch ch := inp.Ch; ch {
		case ';':
			inp.Next()
			if r := rune(code); isValidEntity(r) {
				return string(r), true
			}
			return "", false
		case '0', '1', '2', '3', '4', '5', '6', '7', '8', '9':
			code = 10*code + int(ch-'0')
		default:
			return "", false
		}
		if code > unicode.MaxRune {
			return "", false
		}
		inp.Next()
	}
}
func (inp *Input) scanEntityNamed(pos int) (string, bool) {
	for {
		switch inp.Ch {
		case EOS, '\n', '\r', '&':
			return "", false
		case ';':
			inp.Next()
			es := string(inp.Src[pos:inp.Pos])
			ues := html.UnescapeString(es)
			if es == ues {
				return "", false
			}
			return ues, true
		default:
			inp.Next()
		}
	}
}

// isValidEntity checks if the given code is valid for an entity.
//
// According to https://html.spec.whatwg.org/multipage/syntax.html#character-references
// ""The numeric character reference forms described above are allowed to reference any code point
// excluding U+000D CR, noncharacters, and controls other than ASCII whitespace.""
func isValidEntity(r rune) bool {
	// No C0 control and no "code point in the range U+007F DELETE to U+009F APPLICATION PROGRAM COMMAND, inclusive."
	if r < ' ' || ('\u007f' <= r && r <= '\u009f') {
		return false
	}

	// If below any noncharacter code point, return true
	//
	// See: https://infra.spec.whatwg.org/#noncharacter
	if r < '\ufdd0' {
		return true
	}

	// First range of noncharacter code points: "(...) in the range U+FDD0 to U+FDEF, inclusive"
	if r <= '\ufdef' {
		return false
	}

	// Other noncharacter code points:
	switch r {
	case '\uFFFE', '\uFFFF',
		'\U0001FFFE', '\U0001FFFF',
		'\U0002FFFE', '\U0002FFFF',
		'\U0003FFFE', '\U0003FFFF',
		'\U0004FFFE', '\U0004FFFF',
		'\U0005FFFE', '\U0005FFFF',
		'\U0006FFFE', '\U0006FFFF',
		'\U0007FFFE', '\U0007FFFF',
		'\U0008FFFE', '\U0008FFFF',
		'\U0009FFFE', '\U0009FFFF',
		'\U000AFFFE', '\U000AFFFF',
		'\U000BFFFE', '\U000BFFFF',
		'\U000CFFFE', '\U000CFFFF',
		'\U000DFFFE', '\U000DFFFF',
		'\U000EFFFE', '\U000EFFFF',
		'\U000FFFFE', '\U000FFFFF',
		'\U0010FFFE', '\U0010FFFF':
		return false
	}
	return true
}

Deleted input/entity_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
































































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

import (
	"testing"

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

func TestScanEntity(t *testing.T) {
	t.Parallel()
	var testcases = []struct {
		text string
		exp  string
	}{
		{"", ""},
		{"a", ""},
		{"&amp;", "&"},
		{"&#33;", "!"},
		{"&#x33;", "3"},
		{"&quot;", "\""},
	}
	for id, tc := range testcases {
		inp := input.NewInput([]byte(tc.text))
		got, ok := inp.ScanEntity()
		if !ok {
			if tc.exp != "" {
				t.Errorf("ID=%d, text=%q: expected error, but got %q", id, tc.text, got)
			}
			if inp.Pos != 0 {
				t.Errorf("ID=%d, text=%q: input position advances to %d", id, tc.text, inp.Pos)
			}
			continue
		}
		if tc.exp != got {
			t.Errorf("ID=%d, text=%q: expected %q, but got %q", id, tc.text, tc.exp, got)
		}
	}
}

func TestScanIllegalEntity(t *testing.T) {
	t.Parallel()
	testcases := []string{"", "a", "& Input &rarr;", "&#9;", "&#x1f;"}
	for i, tc := range testcases {
		inp := input.NewInput([]byte(tc))
		got, ok := inp.ScanEntity()
		if ok {
			t.Errorf("%d: scanning %q was unexpected successful, got %q", i, tc, got)
			continue
		}
	}
}

Deleted input/input.go.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157





























































































































































-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
//-----------------------------------------------------------------------------
// Copyright (c) 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 input provides an abstraction for data to be read.
package input

import "unicode/utf8"

// Input is an abstract input source
type Input struct {
	// Read-only, will never change
	Src []byte // The source string

	// Read-only, will change
	Ch      rune // current character
	Pos     int  // character position in src
	readPos int  // reading position (position after current character)
}

// NewInput creates a new input source.
func NewInput(src []byte) *Input {
	inp := &Input{Src: src}
	inp.Next()
	return inp
}

// EOS = End of source
const EOS = rune(-1)

// Next reads the next rune into inp.Ch and returns it too.
func (inp *Input) Next() rune {
	if inp.readPos >= len(inp.Src) {
		inp.Pos = len(inp.Src)
		inp.Ch = EOS
		return EOS
	}
	inp.Pos = inp.readPos
	r, w := rune(inp.Src[inp.readPos]), 1
	if r >= utf8.RuneSelf {
		r, w = utf8.DecodeRune(inp.Src[inp.readPos:])
	}
	inp.readPos += w
	inp.Ch = r
	return r
}

// Peek returns the rune following the most recently read rune without
// advancing. If end-of-source was already found peek returns EOS.
func (inp *Input) Peek() rune {
	return inp.PeekN(0)
}

// PeekN returns the n-th rune after the most recently read rune without
// advancing. If end-of-source was already found peek returns EOS.
func (inp *Input) PeekN(n int) rune {
	pos := inp.readPos + n
	if pos < len(inp.Src) {
		r := rune(inp.Src[pos])
		if r >= utf8.RuneSelf {
			r, _ = utf8.DecodeRune(inp.Src[pos:])
		}
		if r == '\t' {
			return ' '
		}
		return r
	}
	return EOS
}

// Accept checks if the given string is a prefix of the text to be parsed.
// If successful, advance position and current character.
// String must only contain bytes < 128.
// If not successful, everything remains as it is.
func (inp *Input) Accept(s string) bool {
	pos := inp.Pos
	remaining := len(inp.Src) - pos
	if s == "" || len(s) > remaining {
		return false
	}
	// According to internal documentation of bytes.Equal, the string() will not allocate any memory.
	if readPos := pos + len(s); s == string(inp.Src[pos:readPos]) {
		inp.readPos = readPos
		inp.Next()
		return true
	}
	return false
}

// IsEOLEOS returns true if char is either EOS or EOL.
func IsEOLEOS(ch rune) bool { return ch == EOS || ch == '\n' || ch == '\r' }

// EatEOL transforms both "\r" and "\r\n" into "\n".
func (inp *Input) EatEOL() {
	switch inp.Ch {
	case '\r':
		if inp.Peek() == '\n' {
			inp.Next()
		}
		inp.Ch = '\n'
		inp.Next()
	case '\n':
		inp.Next()
	}
}

// SetPos allows to reset the read position.
func (inp *Input) SetPos(pos int) {
	if inp.Pos != pos {
		inp.readPos = pos
		inp.Next()
	}
}

// SkipSpace reads while the current character is not a space character.
func (inp *Input) SkipSpace() {
	for ch := inp.Ch; IsSpace(ch); {
		ch = inp.Next()
	}
}

// SkipToEOL reads until the next end-of-line.
func (inp *Input) SkipToEOL() {
	for {
		switch inp.Ch {
		case EOS, '\n', '\r':
			return
		}
		inp.Next()
	}
}

// ScanLineContent reads the reaining input stream and interprets it as lines of text.
func (inp *Input) ScanLineContent() []byte {
	result := make([]byte, 0, len(inp.Src)-inp.Pos+1)
	for {
		inp.EatEOL()
		posL := inp.Pos
		if inp.Ch == EOS {
			return result
		}
		inp.SkipToEOL()
		if len(result) > 0 {
			result = append(result, '\n')
		}
		result = append(result, inp.Src[posL:inp.Pos]...)
	}
}

Deleted input/input_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




































































-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
//-----------------------------------------------------------------------------
// 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 input_test provides some unit-tests for reading data.
package input_test

import (
	"testing"

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

func TestEatEOL(t *testing.T) {
	t.Parallel()
	inp := input.NewInput(nil)
	inp.EatEOL()
	if inp.Ch != input.EOS {
		t.Errorf("No EOS found: %q", inp.Ch)
	}
	if inp.Pos != 0 {
		t.Errorf("Pos != 0: %d", inp.Pos)
	}

	inp = input.NewInput([]byte("ABC"))
	if inp.Ch != 'A' {
		t.Errorf("First ch != 'A', got %q", inp.Ch)
	}
	inp.EatEOL()
	if inp.Ch != 'A' {
		t.Errorf("First ch != 'A', got %q", inp.Ch)
	}
}

func TestAccept(t *testing.T) {
	t.Parallel()
	testcases := []struct {
		accept string
		src    string
		acc    bool
		exp    rune
	}{
		{"", "", false, input.EOS},
		{"AB", "abc", false, 'a'},
		{"AB", "ABC", true, 'C'},
		{"AB", "AB", true, input.EOS},
		{"AB", "A", false, 'A'},
	}
	for i, tc := range testcases {
		inp := input.NewInput([]byte(tc.src))
		acc := inp.Accept(tc.accept)
		if acc != tc.acc {
			t.Errorf("%d: %q.Accept(%q) == %v, but got %v", i, tc.src, tc.accept, tc.acc, acc)
		}
		if got := inp.Ch; tc.exp != got {
			t.Errorf("%d: %q.Accept(%q) should result in run %v, but got %v", i, tc.src, tc.accept, tc.exp, got)
		}
	}
}

Deleted input/runes.go.

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






























-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
//-----------------------------------------------------------------------------
// Copyright (c) 2020-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 input

import "unicode"

// IsSpace returns true if rune is a whitespace.
func IsSpace(ch rune) bool {
	switch ch {
	case ' ', '\t':
		return true
	case '\n', '\r', EOS:
		return false
	}
	return unicode.IsSpace(ch)
}

// IsSpace returns true if current character is a whitespace.
func (inp *Input) IsSpace() bool { return IsSpace(inp.Ch) }

Changes to shtml/const.go.

64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
64
65
66
67
68
69
70

71
72
73
74
75
76
77







-







	symTH         = sx.MakeSymbol("th")
	symTR         = sx.MakeSymbol("tr")
	SymUL         = sx.MakeSymbol("ul")
)

// Symbols for HTML attribute keys
var (
	symAttrAlt    = sx.MakeSymbol("alt")
	SymAttrClass  = sx.MakeSymbol("class")
	SymAttrHref   = sx.MakeSymbol("href")
	SymAttrID     = sx.MakeSymbol("id")
	SymAttrLang   = sx.MakeSymbol("lang")
	SymAttrOpen   = sx.MakeSymbol("open")
	SymAttrRel    = sx.MakeSymbol("rel")
	SymAttrRole   = sx.MakeSymbol("role")

Changes to shtml/shtml.go.

19
20
21
22
23
24
25
26
27
28



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



26
27
28
29
30
31
32
33
34
35







-
-
-
+
+
+







	"net/url"
	"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/domain/meta"
	"t73f.de/r/zsc/sz"
	"t73f.de/r/zsc/domain/meta"
	"t73f.de/r/zsc/sz"
	"t73f.de/r/zsx"
)

// 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
55
56
57
58
59
60
61
62
63


64
65
66
67
68
69
70
71

72
73
74
75
76
77
78
55
56
57
58
59
60
61


62
63
64
65
66
67
68
69
70

71
72
73
74
75
76
77
78







-
-
+
+







-
+








// SetUnique sets a prefix to make several HTML ids unique.
func (ev *Evaluator) SetUnique(s string) { ev.unique = s }

// IsValidName returns true, if name is a valid symbol name.
func isValidName(s string) bool { return s != "" }

// EvaluateAttrbute transforms the given attributes into a HTML s-expression.
func EvaluateAttrbute(a attrs.Attributes) *sx.Pair {
// EvaluateAttributes transforms the given attributes into a HTML s-expression.
func EvaluateAttributes(a zsx.Attributes) *sx.Pair {
	if len(a) == 0 {
		return nil
	}
	plist := sx.Nil()
	keys := a.Keys()
	for i := len(keys) - 1; i >= 0; i-- {
		key := keys[i]
		if key != attrs.DefaultAttribute && isValidName(key) {
		if key != zsx.DefaultAttribute && isValidName(key) {
			plist = plist.Cons(sx.Cons(sx.MakeSymbol(key), sx.MakeString(a[key])))
		}
	}
	if plist == nil {
		return nil
	}
	return plist.Cons(sxhtml.SymAttr)
183
184
185
186
187
188
189
190

191
192
193
194
195
196
197
183
184
185
186
187
188
189

190
191
192
193
194
195
196
197







-
+







func (env *Environment) Reset() {
	env.langStack.Reset()
	env.endnotes = nil
	env.quoteNesting = 0
}

// pushAttribute adds the current attributes to the environment.
func (env *Environment) pushAttributes(a attrs.Attributes) {
func (env *Environment) pushAttributes(a zsx.Attributes) {
	if value, ok := a.Get("lang"); ok {
		env.langStack.Push(value)
	} else {
		env.langStack.Dup()
	}
}

234
235
236
237
238
239
240
241

242
243
244
245
246
247
248
234
235
236
237
238
239
240

241
242
243
244
245
246
247
248







-
+







	}
	ev.fns[symVal] = fn
}

func (ev *Evaluator) bindMetadata() {
	ev.bind(sz.SymMeta, 0, ev.evalList)
	evalMetaString := func(args sx.Vector, env *Environment) sx.Object {
		a := make(attrs.Attributes, 2).
		a := make(zsx.Attributes, 2).
			Set("name", getSymbol(args[0], env).GetValue()).
			Set("content", getString(args[1], env).GetValue())
		return ev.EvaluateMeta(a)
	}
	ev.bind(sz.SymTypeCredential, 2, evalMetaString)
	ev.bind(sz.SymTypeEmpty, 2, evalMetaString)
	ev.bind(sz.SymTypeID, 2, evalMetaString)
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
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







-
+









-
-
+
+



-
-
+
+


-
+

















-
+





-
+



-
+





-
-
-
-
+
+
+
+


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

-
+










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





-
+







			sb.WriteByte(' ')
			sb.WriteString(getString(obj, env).GetValue())
		}
		s := sb.String()
		if len(s) > 0 {
			s = s[1:]
		}
		a := make(attrs.Attributes, 2).
		a := make(zsx.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)
}

// 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)
func (ev *Evaluator) EvaluateMeta(a zsx.Attributes) *sx.Pair {
	return sx.Nil().Cons(EvaluateAttributes(a)).Cons(SymMeta)
}

func (ev *Evaluator) bindBlocks() {
	ev.bind(sz.SymBlock, 0, ev.evalList)
	ev.bind(sz.SymPara, 0, func(args sx.Vector, env *Environment) sx.Object {
	ev.bind(zsx.SymBlock, 0, ev.evalList)
	ev.bind(zsx.SymPara, 0, func(args sx.Vector, env *Environment) sx.Object {
		return ev.evalSlice(args, env).Cons(SymP)
	})
	ev.bind(sz.SymHeading, 5, func(args sx.Vector, env *Environment) sx.Object {
	ev.bind(zsx.SymHeading, 5, func(args sx.Vector, env *Environment) sx.Object {
		nLevel := getInt64(args[0], env)
		if nLevel <= 0 {
			env.err = fmt.Errorf("%v is a negative heading level", nLevel)
			return sx.Nil()
		}
		level := strconv.FormatInt(nLevel+ev.headingOffset, 10)
		headingSymbol := sx.MakeSymbol("h" + level)

		a := GetAttributes(args[1], env)
		env.pushAttributes(a)
		defer env.popAttributes()
		if fragment := getString(args[3], env).GetValue(); fragment != "" {
			a = a.Set("id", ev.unique+fragment)
		}

		if result, _ := ev.EvaluateList(args[4:], env); result != nil {
			if len(a) > 0 {
				result = result.Cons(EvaluateAttrbute(a))
				result = result.Cons(EvaluateAttributes(a))
			}
			return result.Cons(headingSymbol)
		}
		return sx.MakeList(headingSymbol, sx.MakeString("<MISSING TEXT>"))
	})
	ev.bind(sz.SymThematic, 0, func(args sx.Vector, env *Environment) sx.Object {
	ev.bind(zsx.SymThematic, 0, func(args sx.Vector, env *Environment) sx.Object {
		result := sx.Nil()
		if len(args) > 0 {
			if attrList := getList(args[0], env); attrList != nil {
				result = result.Cons(EvaluateAttrbute(sz.GetAttributes(attrList)))
				result = result.Cons(EvaluateAttributes(zsx.GetAttributes(attrList)))
			}
		}
		return result.Cons(SymHR)
	})

	ev.bind(sz.SymListOrdered, 0, ev.makeListFn(SymOL))
	ev.bind(sz.SymListUnordered, 0, ev.makeListFn(SymUL))
	ev.bind(sz.SymDescription, 0, func(args sx.Vector, env *Environment) sx.Object {
		if len(args) == 0 {
	ev.bind(zsx.SymListOrdered, 1, ev.makeListFn(SymOL))
	ev.bind(zsx.SymListUnordered, 1, ev.makeListFn(SymUL))
	ev.bind(zsx.SymListQuote, 1, func(args sx.Vector, env *Environment) sx.Object {
		if len(args) == 1 {
			return sx.Nil()
		}
		var items sx.ListBuilder
		items.Add(symDL)
		for pos := 0; pos < len(args); pos++ {
		var result sx.ListBuilder
		result.Add(symBLOCKQUOTE)
		if attrs := EvaluateAttributes(GetAttributes(args[0], env)); attrs != nil {
			result.Add(attrs)
		}
		for _, elem := range args[1:] {
			if quote, isPair := sx.GetPair(ev.Eval(elem, env)); isPair {
				result.Add(quote.Cons(sxhtml.SymListSplice))
			}
		}
		return result.List()
	})

	ev.bind(zsx.SymDescription, 1, func(args sx.Vector, env *Environment) sx.Object {
		if len(args) == 1 {
			return sx.Nil()
		}
		var result sx.ListBuilder
		result.Add(symDL)
		if attrs := EvaluateAttributes(GetAttributes(args[0], env)); attrs != nil {
			result.Add(attrs)
		}
		for pos := 1; pos < len(args); pos++ {
			term := ev.evalDescriptionTerm(getList(args[pos], env), env)
			items.Add(term.Cons(symDT))
			result.Add(term.Cons(symDT))
			pos++
			if pos >= len(args) {
				break
			}
			ddBlock := getList(ev.Eval(args[pos], env), env)
			if ddBlock == nil {
				continue
			}
			for ddlst := range ddBlock.Values() {
				dditem := getList(ddlst, env)
				items.Add(dditem.Cons(symDD))
				result.Add(dditem.Cons(symDD))
			}
		}
		return items.List()
	})
	ev.bind(sz.SymListQuote, 0, func(args sx.Vector, env *Environment) sx.Object {
		if args == nil {
			return sx.Nil()
		}
		var result sx.ListBuilder
		result.Add(symBLOCKQUOTE)
		for _, elem := range args {
			if quote, isPair := sx.GetPair(ev.Eval(elem, env)); isPair {
				result.Add(quote.Cons(sxhtml.SymListSplice))
			}
		}
		return result.List()
	})

	ev.bind(sz.SymTable, 1, func(args sx.Vector, env *Environment) sx.Object {
	ev.bind(zsx.SymTable, 1, func(args sx.Vector, env *Environment) sx.Object {
		thead := sx.Nil()
		if header := getList(args[0], env); !sx.IsNil(header) {
			thead = sx.Nil().Cons(ev.evalTableRow(symTH, header, env)).Cons(symTHEAD)
		}

		var tbody sx.ListBuilder
		if len(args) > 1 {
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
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







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

-
+









-
+


-
-
+
+


-
+







-
-
-
+
+
+
+

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

-
+




-
+

-
+










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







			table = table.Cons(thead)
		}
		if table == nil {
			return sx.Nil()
		}
		return table.Cons(symTABLE)
	})
	ev.bind(sz.SymCell, 0, ev.makeCellFn(""))
	ev.bind(sz.SymCellCenter, 0, ev.makeCellFn("center"))
	ev.bind(sz.SymCellLeft, 0, ev.makeCellFn("left"))
	ev.bind(sz.SymCellRight, 0, ev.makeCellFn("right"))

	ev.bind(sz.SymRegionBlock, 2, ev.makeRegionFn(SymDIV, true))
	ev.bind(sz.SymRegionQuote, 2, ev.makeRegionFn(symBLOCKQUOTE, false))
	ev.bind(sz.SymRegionVerse, 2, ev.makeRegionFn(SymDIV, false))
	ev.bind(zsx.SymCell, 1, func(args sx.Vector, env *Environment) sx.Object {
		tdata := ev.evalSlice(args[1:], env)
		pattrs := getList(args[0], env)
		if alignPairs := pattrs.Assoc(zsx.SymAttrAlign); alignPairs != nil {
			if salign, isString := sx.GetString(alignPairs.Cdr()); isString {
				a := zsx.GetAttributes(pattrs.RemoveAssoc(zsx.SymAttrAlign))
				// Since in Sz there are attributes of align:center|left|right, we can reuse the values.
				a = a.AddClass(salign.GetValue())
				tdata = tdata.Cons(EvaluateAttributes(a))
			}
		}
		return tdata
	})

	ev.bind(zsx.SymRegionBlock, 2, ev.makeRegionFn(SymDIV, true))
	ev.bind(zsx.SymRegionQuote, 2, ev.makeRegionFn(symBLOCKQUOTE, false))
	ev.bind(zsx.SymRegionVerse, 2, ev.makeRegionFn(SymDIV, false))

	ev.bind(sz.SymVerbatimComment, 1, func(args sx.Vector, env *Environment) sx.Object {
	ev.bind(zsx.SymVerbatimComment, 1, func(args sx.Vector, env *Environment) sx.Object {
		if GetAttributes(args[0], env).HasDefault() {
			if len(args) > 1 {
				if s := getString(args[1], env); s.GetValue() != "" {
					return sx.Nil().Cons(s).Cons(sxhtml.SymBlockComment)
				}
			}
		}
		return nil
	})
	ev.bind(sz.SymVerbatimEval, 2, func(args sx.Vector, env *Environment) sx.Object {
	ev.bind(zsx.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 {
	ev.bind(zsx.SymVerbatimHTML, 2, ev.evalHTML)
	ev.bind(zsx.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.SymVerbatimCode, 2, func(args sx.Vector, env *Environment) sx.Object {
	ev.bind(zsx.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)
	})
	ev.bind(sz.SymVerbatimZettel, 0, nilFn)
	ev.bind(sz.SymBLOB, 3, func(args sx.Vector, env *Environment) sx.Object {
		return evalBLOB(getList(args[0], env), getString(args[1], env), getString(args[2], env))
	ev.bind(zsx.SymVerbatimZettel, 0, nilFn)
	ev.bind(zsx.SymBLOB, 4, func(args sx.Vector, env *Environment) sx.Object {
		a := GetAttributes(args[0], env)
		return evalBLOB(a, getList(args[1], env), getString(args[2], env), getString(args[3], env))
	})
	ev.bind(sz.SymTransclude, 2, func(args sx.Vector, env *Environment) sx.Object {
		ref, isPair := sx.GetPair(args[1])
	ev.bind(zsx.SymTransclude, 2, func(args sx.Vector, env *Environment) sx.Object {
		if refSym, refValue := GetReference(args[1], env); refSym != nil {
		if !isPair {
			return sx.Nil()
		}
		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")
			if refSym.IsEqualSymbol(zsx.SymRefStateExternal) {
				a := GetAttributes(args[0], env).Set("src", refValue).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.Nil().Cons(sx.Nil().Cons(EvaluateAttributes(a)).Cons(SymIMG)).Cons(SymP)
			}
			return sx.MakeList(
				sxhtml.SymInlineComment,
				sx.MakeString("transclude"),
				refKind,
				refSym,
				sx.MakeString("->"),
				refValue,
				sx.MakeString(refValue),
			)
		}
		return ev.evalSlice(args, env)
	})
}

func (ev *Evaluator) makeListFn(sym *sx.Symbol) EvalFn {
	return func(args sx.Vector, env *Environment) sx.Object {
		var result sx.ListBuilder
		result.Add(sym)
		if attrs := EvaluateAttributes(GetAttributes(args[0], env)); attrs != nil {
			result.Add(attrs)
		}
		if len(args) > 1 {
		for _, elem := range args {
			item := sx.Nil().Cons(SymLI)
			if res, isPair := sx.GetPair(ev.Eval(elem, env)); isPair {
				item.ExtendBang(res)
			}
			result.Add(item)
			for _, elem := range args[1:] {
				item := sx.Nil().Cons(SymLI)
				if res, isPair := sx.GetPair(ev.Eval(elem, env)); isPair {
					item.ExtendBang(res)
				}
				result.Add(item)
			}
		}
		return result.List()
	}
}

func (ev *Evaluator) evalDescriptionTerm(term *sx.Pair, env *Environment) *sx.Pair {
	var result sx.ListBuilder
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

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
706
707
708
709
710
711

712
713
714
715
716
717
718
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

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
706







-
-
-
-
-
-
-
-
-














-
+















-
+


-
+







-
-
-
-
+
+
+
+

-
+



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





-
+



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







-
+

-
+






+






-
+











-
+






-
+



-
-
+
+




-
+





-
+

















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

-
+









-
+


-
+



-
+


-
+














-
+







	var row sx.ListBuilder
	row.Add(symTR)
	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 != "" {
			tdata = tdata.Cons(EvaluateAttrbute(attrs.Attributes{"class": align}))
		}
		return tdata
	}
}

func (ev *Evaluator) makeRegionFn(sym *sx.Symbol, genericToClass bool) EvalFn {
	return func(args sx.Vector, env *Environment) sx.Object {
		a := GetAttributes(args[0], env)
		env.pushAttributes(a)
		defer env.popAttributes()
		if genericToClass {
			if val, found := a.Get(""); found {
				a = a.Remove("").AddClass(val)
			}
		}
		var result sx.ListBuilder
		result.Add(sym)
		if len(a) > 0 {
			result.Add(EvaluateAttrbute(a))
			result.Add(EvaluateAttributes(a))
		}
		if region, isPair := sx.GetPair(args[1]); isPair {
			if evalRegion := ev.EvalPairList(region, env); evalRegion != nil {
				result.ExtendBang(evalRegion)
			}
		}
		if len(args) > 2 {
			if cite, _ := ev.EvaluateList(args[2:], env); cite != nil {
				result.Add(cite.Cons(symCITE))
			}
		}
		return result.List()
	}
}

func evalVerbatim(a attrs.Attributes, s sx.String) sx.Object {
func evalVerbatim(a zsx.Attributes, s sx.String) sx.Object {
	a = setProgLang(a)
	code := sx.Nil().Cons(s)
	if al := EvaluateAttrbute(a); al != nil {
	if al := EvaluateAttributes(a); al != nil {
		code = code.Cons(al)
	}
	code = code.Cons(symCODE)
	return sx.Nil().Cons(code).Cons(symPRE)
}

func (ev *Evaluator) bindInlines() {
	ev.bind(sz.SymInline, 0, ev.evalList)
	ev.bind(sz.SymText, 1, func(args sx.Vector, env *Environment) sx.Object { return getString(args[0], env) })
	ev.bind(sz.SymSoft, 0, func(sx.Vector, *Environment) sx.Object { return sx.MakeString(" ") })
	ev.bind(sz.SymHard, 0, func(sx.Vector, *Environment) sx.Object { return sx.Nil().Cons(symBR) })
	ev.bind(zsx.SymInline, 0, ev.evalList)
	ev.bind(zsx.SymText, 1, func(args sx.Vector, env *Environment) sx.Object { return getString(args[0], env) })
	ev.bind(zsx.SymSoft, 0, func(sx.Vector, *Environment) sx.Object { return sx.MakeString(" ") })
	ev.bind(zsx.SymHard, 0, func(sx.Vector, *Environment) sx.Object { return sx.Nil().Cons(symBR) })

	ev.bind(sz.SymLinkInvalid, 2, func(args sx.Vector, env *Environment) sx.Object {
	ev.bind(zsx.SymLink, 2, func(args sx.Vector, env *Environment) sx.Object {
		a := GetAttributes(args[0], env)
		env.pushAttributes(a)
		defer env.popAttributes()
		refSym, refValue := GetReference(args[1], env)
		switch refSym {
		case sz.SymRefStateZettel, zsx.SymRefStateSelf, sz.SymRefStateFound, zsx.SymRefStateHosted, sz.SymRefStateBased:
			return ev.evalLink(a.Set("href", refValue), refValue, args[2:], env)

		case zsx.SymRefStateExternal:
			return ev.evalLink(a.Set("href", refValue).Add("rel", "external"), refValue, args[2:], env)

		case sz.SymRefStateQuery:
			query := "?" + api.QueryKeyQuery + "=" + url.QueryEscape(refValue)
			return ev.evalLink(a.Set("href", query), refValue, args[2:], env)

		case sz.SymRefStateBroken:
			return ev.evalLink(a.AddClass("broken"), refValue, args[2:], env)
		}

		// sz.SymRefStateInvalid or unknown
		var inline *sx.Pair
		if len(args) > 2 {
			inline = ev.evalSlice(args[2:], env)
		}
		if inline == nil {
			inline = sx.Nil().Cons(ev.Eval(args[1], env))
			inline = sx.Nil().Cons(sx.MakeString(refValue))
		}
		return inline.Cons(SymSPAN)
	})
	evalHREF := func(args sx.Vector, env *Environment) sx.Object {
		a := GetAttributes(args[0], env)
		env.pushAttributes(a)
		defer env.popAttributes()
		refValue := getString(args[1], env)
		return ev.evalLink(a.Set("href", refValue.GetValue()), refValue, args[2:], env)
	}

	ev.bind(sz.SymLinkZettel, 2, evalHREF)
	ev.bind(sz.SymLinkSelf, 2, evalHREF)
	ev.bind(sz.SymLinkFound, 2, evalHREF)
	ev.bind(sz.SymLinkBroken, 2, func(args sx.Vector, env *Environment) sx.Object {
		a := GetAttributes(args[0], env)
		env.pushAttributes(a)
		defer env.popAttributes()
		refValue := getString(args[1], env)
		return ev.evalLink(a.AddClass("broken"), refValue, args[2:], env)
	})
	ev.bind(sz.SymLinkHosted, 2, evalHREF)
	ev.bind(sz.SymLinkBased, 2, evalHREF)
	ev.bind(sz.SymLinkQuery, 2, func(args sx.Vector, env *Environment) sx.Object {
		a := GetAttributes(args[0], env)
		env.pushAttributes(a)
		defer env.popAttributes()
		refValue := getString(args[1], env)
		query := "?" + api.QueryKeyQuery + "=" + url.QueryEscape(refValue.GetValue())
		return ev.evalLink(a.Set("href", query), refValue, args[2:], env)
	})
	ev.bind(sz.SymLinkExternal, 2, func(args sx.Vector, env *Environment) sx.Object {
		a := GetAttributes(args[0], env)
		env.pushAttributes(a)
		defer env.popAttributes()
		refValue := getString(args[1], env)
		return ev.evalLink(a.Set("href", refValue.GetValue()).Add("rel", "external"), refValue, args[2:], env)
	})

	ev.bind(sz.SymEmbed, 3, func(args sx.Vector, env *Environment) sx.Object {
		ref := getList(args[1], env)
		a := GetAttributes(args[0], env)
	ev.bind(zsx.SymEmbed, 3, func(args sx.Vector, env *Environment) sx.Object {
		_, refValue := GetReference(args[1], env)
		a := GetAttributes(args[0], env).Set("src", refValue)
		a = a.Set("src", getString(ref.Tail().Car(), env).GetValue())
		if len(args) > 3 {
			var sb strings.Builder
			flattenText(&sb, sx.MakeList(args[3:]...))
			if d := sb.String(); d != "" {
				a = a.Set("alt", d)
			}
		}
		return sx.MakeList(SymIMG, EvaluateAttrbute(a))
		return sx.MakeList(SymIMG, EvaluateAttributes(a))
	})
	ev.bind(sz.SymEmbedBLOB, 3, func(args sx.Vector, env *Environment) sx.Object {
	ev.bind(zsx.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(meta.KeySummary)
		if !hasSummary {
			summary = ""
		}
		return evalBLOB(
			a,
			sx.MakeList(sxhtml.SymListSplice, sx.MakeString(summary)),
			syntax,
			data,
		)
	})

	ev.bind(sz.SymCite, 2, func(args sx.Vector, env *Environment) sx.Object {
	ev.bind(zsx.SymCite, 2, func(args sx.Vector, env *Environment) sx.Object {
		a := GetAttributes(args[0], env)
		env.pushAttributes(a)
		defer env.popAttributes()
		result := sx.Nil()
		if key := getString(args[1], env); key.GetValue() != "" {
			if len(args) > 2 {
				result = ev.evalSlice(args[2:], env).Cons(sx.MakeString(", "))
			}
			result = result.Cons(key)
		}
		if len(a) > 0 {
			result = result.Cons(EvaluateAttrbute(a))
			result = result.Cons(EvaluateAttributes(a))
		}
		if result == nil {
			return nil
		}
		return result.Cons(SymSPAN)
	})
	ev.bind(sz.SymMark, 3, func(args sx.Vector, env *Environment) sx.Object {
	ev.bind(zsx.SymMark, 3, func(args sx.Vector, env *Environment) sx.Object {
		result := ev.evalSlice(args[3:], env)
		if !ev.noLinks {
			if fragment := getString(args[2], env).GetValue(); fragment != "" {
				a := attrs.Attributes{"id": fragment + ev.unique}
				return result.Cons(EvaluateAttrbute(a)).Cons(SymA)
				a := zsx.Attributes{"id": fragment + ev.unique}
				return result.Cons(EvaluateAttributes(a)).Cons(SymA)
			}
		}
		return result.Cons(SymSPAN)
	})
	ev.bind(sz.SymEndnote, 1, func(args sx.Vector, env *Environment) sx.Object {
	ev.bind(zsx.SymEndnote, 1, func(args sx.Vector, env *Environment) sx.Object {
		a := GetAttributes(args[0], env)
		env.pushAttributes(a)
		defer env.popAttributes()
		attrPlist := sx.Nil()
		if len(a) > 0 {
			if attrs := EvaluateAttrbute(a); attrs != nil {
			if attrs := EvaluateAttributes(a); attrs != nil {
				attrPlist = attrs.Tail()
			}
		}

		noteNum := strconv.Itoa(len(env.endnotes) + 1)
		noteID := ev.unique + noteNum
		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)
		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))
	ev.bind(sz.SymFormatQuote, 1, ev.evalQuote)
	ev.bind(sz.SymFormatSpan, 1, ev.makeFormatFn(SymSPAN))
	ev.bind(sz.SymFormatStrong, 1, ev.makeFormatFn(SymSTRONG))
	ev.bind(sz.SymFormatSub, 1, ev.makeFormatFn(symSUB))
	ev.bind(sz.SymFormatSuper, 1, ev.makeFormatFn(symSUP))
	ev.bind(zsx.SymFormatDelete, 1, ev.makeFormatFn(symDEL))
	ev.bind(zsx.SymFormatEmph, 1, ev.makeFormatFn(symEM))
	ev.bind(zsx.SymFormatInsert, 1, ev.makeFormatFn(symINS))
	ev.bind(zsx.SymFormatMark, 1, ev.makeFormatFn(symMARK))
	ev.bind(zsx.SymFormatQuote, 1, ev.evalQuote)
	ev.bind(zsx.SymFormatSpan, 1, ev.makeFormatFn(SymSPAN))
	ev.bind(zsx.SymFormatStrong, 1, ev.makeFormatFn(SymSTRONG))
	ev.bind(zsx.SymFormatSub, 1, ev.makeFormatFn(symSUB))
	ev.bind(zsx.SymFormatSuper, 1, ev.makeFormatFn(symSUP))

	ev.bind(sz.SymLiteralComment, 1, func(args sx.Vector, env *Environment) sx.Object {
	ev.bind(zsx.SymLiteralComment, 1, func(args sx.Vector, env *Environment) sx.Object {
		if GetAttributes(args[0], env).HasDefault() {
			if len(args) > 1 {
				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.SymLiteralInput, 2, func(args sx.Vector, env *Environment) sx.Object {
	ev.bind(zsx.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 {
	ev.bind(zsx.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 {
	ev.bind(zsx.SymLiteralOutput, 2, func(args sx.Vector, env *Environment) sx.Object {
		return evalLiteral(args, nil, symSAMP, env)
	})
	ev.bind(sz.SymLiteralCode, 2, func(args sx.Vector, env *Environment) sx.Object {
	ev.bind(zsx.SymLiteralCode, 2, func(args sx.Vector, env *Environment) sx.Object {
		return evalLiteral(args, nil, symCODE, env)
	})
}

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()
		if val, hasClass := a.Get(""); hasClass {
			a = a.Remove("").AddClass(val)
		}
		res := ev.evalSlice(args[1:], env)
		if len(a) > 0 {
			res = res.Cons(EvaluateAttrbute(a))
			res = res.Cons(EvaluateAttributes(a))
		}
		return res.Cons(sym)
	}
}

func (ev *Evaluator) evalQuote(args sx.Vector, env *Environment) sx.Object {
	a := GetAttributes(args[0], env)
737
738
739
740
741
742
743
744

745
746
747
748
749
750
751
752

753
754
755
756
757
758
759
760
761
762
763
764

765
766
767
768

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







-
+







-
+











-
+



-
+













-
+









-
+



-
+

-
+







			res = res.Cons(sx.MakeList(sxhtml.SymNoEscape, sx.MakeString(leftQ), sx.MakeString("&nbsp;")))
		} else {
			lastPair.AppendBang(sx.MakeList(sxhtml.SymNoEscape, sx.MakeString(rightQ)))
			res = res.Cons(sx.MakeList(sxhtml.SymNoEscape, sx.MakeString(leftQ)))
		}
	}
	if len(a) > 0 {
		res = res.Cons(EvaluateAttrbute(a))
		res = res.Cons(EvaluateAttributes(a))
		return res.Cons(SymSPAN)
	}
	return res.Cons(sxhtml.SymListSplice)
}

var visibleReplacer = strings.NewReplacer(" ", "\u2423")

func evalLiteral(args sx.Vector, a attrs.Attributes, sym *sx.Symbol, env *Environment) sx.Object {
func evalLiteral(args sx.Vector, a zsx.Attributes, sym *sx.Symbol, env *Environment) sx.Object {
	if a == nil {
		a = GetAttributes(args[0], env)
	}
	a = setProgLang(a)
	literal := getString(args[1], env).GetValue()
	if a.HasDefault() {
		a = a.RemoveDefault()
		literal = visibleReplacer.Replace(literal)
	}
	res := sx.Nil().Cons(sx.MakeString(literal))
	if len(a) > 0 {
		res = res.Cons(EvaluateAttrbute(a))
		res = res.Cons(EvaluateAttributes(a))
	}
	return res.Cons(sym)
}
func setProgLang(a attrs.Attributes) attrs.Attributes {
func setProgLang(a zsx.Attributes) zsx.Attributes {
	if val, found := a.Get(""); found {
		a = a.AddClass("language-" + val).Remove("")
	}
	return a
}

func (ev *Evaluator) evalHTML(args sx.Vector, env *Environment) sx.Object {
	if s := getString(ev.Eval(args[1], env), env); s.GetValue() != "" && IsSafe(s.GetValue()) {
		return sx.Nil().Cons(s).Cons(sxhtml.SymNoEscape)
	}
	return nil
}

func evalBLOB(description *sx.Pair, syntax, data sx.String) sx.Object {
func evalBLOB(a zsx.Attributes, description *sx.Pair, syntax, data sx.String) sx.Object {
	if data.GetValue() == "" {
		return sx.Nil()
	}
	switch syntax.GetValue() {
	case "":
		return sx.Nil()
	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())))
		a = a.Add("src", "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)))
			a = a.Add("alt", d)
		}
		return sx.Nil().Cons(sx.Nil().Cons(imgAttr.Cons(sxhtml.SymAttr)).Cons(SymIMG)).Cons(SymP)
		return sx.Nil().Cons(sx.Nil().Cons(EvaluateAttributes(a)).Cons(SymIMG)).Cons(SymP)
	}
}

func flattenText(sb *strings.Builder, lst *sx.Pair) {
	for elem := range lst.Values() {
		switch obj := elem.(type) {
		case sx.String:
881
882
883
884
885
886
887
888

889
890
891

892
893
894
895
896

897
898
899
900
901
902
903
904
905
906
907
908
909

910
911
912
913
914
915
916






917
918
919
920
921
922
923
869
870
871
872
873
874
875

876
877
878

879
880
881
882
883

884
885
886
887
888
889
890
891
892
893
894
895
896

897







898
899
900
901
902
903
904
905
906
907
908
909
910







-
+


-
+




-
+












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







	}
	if env.err == nil {
		return result.List()
	}
	return nil
}

func (ev *Evaluator) evalLink(a attrs.Attributes, refValue sx.String, inline sx.Vector, env *Environment) sx.Object {
func (ev *Evaluator) evalLink(a zsx.Attributes, refValue string, inline sx.Vector, env *Environment) sx.Object {
	result := ev.evalSlice(inline, env)
	if len(inline) == 0 {
		result = sx.Nil().Cons(refValue)
		result = sx.Nil().Cons(sx.MakeString(refValue))
	}
	if ev.noLinks {
		return result.Cons(SymSPAN)
	}
	return result.Cons(EvaluateAttrbute(a)).Cons(SymA)
	return result.Cons(EvaluateAttributes(a)).Cons(SymA)
}

func getSymbol(obj sx.Object, env *Environment) *sx.Symbol {
	if env.err == nil {
		if sym, ok := sx.GetSymbol(obj); ok {
			return sym
		}
		env.err = fmt.Errorf("%v/%T is not a symbol", obj, obj)
	}
	return sx.MakeSymbol("???")
}
func getString(val sx.Object, env *Environment) sx.String {
	if env.err != nil {
	if env.err == nil {
		return sx.String{}
	}
	if s, ok := sx.GetString(val); ok {
		return s
	}
	env.err = fmt.Errorf("%v/%T is not a string", val, val)
	return sx.String{}
		if s, ok := sx.GetString(val); ok {
			return s
		}
		env.err = fmt.Errorf("%v/%T is not a string", val, val)
	}
	return sx.MakeString("")
}
func getList(val sx.Object, env *Environment) *sx.Pair {
	if env.err == nil {
		if res, isPair := sx.GetPair(val); isPair {
			return res
		}
		env.err = fmt.Errorf("%v/%T is not a list", val, val)
933
934
935
936
937
938
939
940
941
















942
943
944
945
946
947
948
920
921
922
923
924
925
926


927
928
929
930
931
932
933
934
935
936
937
938
939
940
941
942
943
944
945
946
947
948
949







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







	}
	env.err = fmt.Errorf("%v/%T is not a number", val, val)
	return -1017
}

// GetAttributes evaluates the given arg in the given environment and returns
// the contained attributes.
func GetAttributes(arg sx.Object, env *Environment) attrs.Attributes {
	return sz.GetAttributes(getList(arg, env))
func GetAttributes(arg sx.Object, env *Environment) zsx.Attributes {
	return zsx.GetAttributes(getList(arg, env))
}

// GetReference returns the reference symbol and the reference value of a reference pair.
func GetReference(val sx.Object, env *Environment) (*sx.Symbol, string) {
	if env.err == nil {
		if p := getList(val, env); env.err == nil {
			sym, val := sz.GetReference(p)
			if sym != nil {
				return sym, val
			}
			env.err = fmt.Errorf("%v/%T is not a reference", val, val)
		}
	}
	return nil, ""
}

var unsafeSnippets = []string{
	"<script", "</script",
	"<iframe", "</iframe",
}

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

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







+
+


-
-
-
+

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

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







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

package sz

import "t73f.de/r/sx"

// Various constants for Zettel data. They are technically variables.
// These are only Zettelstore-specific symbols. The more general symbols are
// defined in t73f.de/r/zsx
var (
	// Symbols for Metanodes
	SymBlock  = sx.MakeSymbol("BLOCK")
	SymInline = sx.MakeSymbol("INLINE")
	SymMeta   = sx.MakeSymbol("META")
	SymMeta = sx.MakeSymbol("META")

	// Symbols for Zettel noMakede types.
	SymBLOB            = sx.MakeSymbol("BLOB")
	SymCell            = sx.MakeSymbol("CELL")
	SymCellCenter      = sx.MakeSymbol("CELL-CENTER")
	SymCellLeft        = sx.MakeSymbol("CELL-LEFT")
	SymCellRight       = sx.MakeSymbol("CELL-RIGHT")
	SymCite            = sx.MakeSymbol("CITE")
	SymDescription     = sx.MakeSymbol("DESCRIPTION")
	SymEmbed           = sx.MakeSymbol("EMBED")
	SymEmbedBLOB       = sx.MakeSymbol("EMBED-BLOB")
	SymEndnote         = sx.MakeSymbol("ENDNOTE")
	SymFormatEmph      = sx.MakeSymbol("FORMAT-EMPH")
	SymFormatDelete    = sx.MakeSymbol("FORMAT-DELETE")
	SymFormatInsert    = sx.MakeSymbol("FORMAT-INSERT")
	SymFormatMark      = sx.MakeSymbol("FORMAT-MARK")
	SymFormatQuote     = sx.MakeSymbol("FORMAT-QUOTE")
	SymFormatSpan      = sx.MakeSymbol("FORMAT-SPAN")
	SymFormatSub       = sx.MakeSymbol("FORMAT-SUB")
	SymFormatSuper     = sx.MakeSymbol("FORMAT-SUPER")
	SymFormatStrong    = sx.MakeSymbol("FORMAT-STRONG")
	SymHard            = sx.MakeSymbol("HARD")
	SymHeading         = sx.MakeSymbol("HEADING")
	SymLinkInvalid     = sx.MakeSymbol("LINK-INVALID")
	SymLinkZettel      = sx.MakeSymbol("LINK-ZETTEL")
	SymLinkSelf        = sx.MakeSymbol("LINK-SELF")
	SymLinkFound       = sx.MakeSymbol("LINK-FOUND")
	SymLinkBroken      = sx.MakeSymbol("LINK-BROKEN")
	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")
	SymLiteralCode     = sx.MakeSymbol("LITERAL-CODE")
	SymLiteralComment  = sx.MakeSymbol("LITERAL-COMMENT")
	SymLiteralInput    = sx.MakeSymbol("LITERAL-INPUT")
	SymLiteralMath     = sx.MakeSymbol("LITERAL-MATH")
	SymLiteralOutput   = sx.MakeSymbol("LITERAL-OUTPUT")
	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")
	SymVerbatimZettel  = sx.MakeSymbol("VERBATIM-ZETTEL")

	// Constant symbols for reference states.
	SymRefStateInvalid  = sx.MakeSymbol("INVALID")
	SymRefStateZettel   = sx.MakeSymbol("ZETTEL")
	SymRefStateZettel = sx.MakeSymbol("ZETTEL")
	SymRefStateSelf     = sx.MakeSymbol("SELF")
	SymRefStateFound    = sx.MakeSymbol("FOUND")
	SymRefStateBroken   = sx.MakeSymbol("BROKEN")
	SymRefStateFound  = sx.MakeSymbol("FOUND")
	SymRefStateBroken = sx.MakeSymbol("BROKEN")
	SymRefStateHosted   = sx.MakeSymbol("HOSTED")
	SymRefStateBased    = sx.MakeSymbol("BASED")
	SymRefStateQuery    = sx.MakeSymbol("QUERY")
	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")

Changes to sz/parser.go.

12
13
14
15
16
17
18

19

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

36
37

38
39
40
41
42
43
44
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







+
-
+















-
+

-
+







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

package sz

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

// --- Contains some simple parsers

// ---- Syntax: none

// ParseNoneBlocks parses no block.
func ParseNoneBlocks(*input.Input) *sx.Pair { 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 == meta.ValueSyntaxHTML {
		sym = SymVerbatimHTML
		sym = zsx.SymVerbatimHTML
	} else {
		sym = SymVerbatimCode
		sym = zsx.SymVerbatimCode
	}
	return sx.MakeList(
		sym,
		sx.MakeList(sx.Cons(sx.MakeString(""), sx.MakeString(syntax))),
		sx.MakeString(string(inp.ScanLineContent())),
	)
}

Changes to sz/parser_test.go.

12
13
14
15
16
17
18
19
20


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


19
20
21
22
23
24
25
26
27







-
-
+
+







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

package sz_test

import (
	"testing"

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

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

Added sz/ref.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
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
//-----------------------------------------------------------------------------
// 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 sz

import (
	"net/url"
	"strings"

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

// MakeReference builds a reference node.
func MakeReference(sym *sx.Symbol, val string) *sx.Pair {
	return sx.Cons(sym, sx.Cons(sx.MakeString(val), sx.Nil()))
}

// GetReference returns the reference symbol and value.
func GetReference(ref *sx.Pair) (*sx.Symbol, string) {
	if ref != nil {
		if sym, isSymbol := sx.GetSymbol(ref.Car()); isSymbol {
			val, isString := sx.GetString(ref.Cdr())
			if !isString {
				val, isString = sx.GetString(ref.Tail().Car())
			}
			if isString {
				return sym, val.GetValue()
			}
		}
	}
	return nil, ""
}

// ScanReference scans a string and returns a reference.
//
// This function is very specific for Zettelstore.
func ScanReference(s string) *sx.Pair {
	if len(s) == id.LengthZid {
		if _, err := id.Parse(s); err == nil {
			return MakeReference(SymRefStateZettel, s)
		}
		if s == "00000000000000" {
			return MakeReference(zsx.SymRefStateInvalid, s)
		}
	} else if len(s) > id.LengthZid && s[id.LengthZid] == '#' {
		zidPart := s[:id.LengthZid]
		if _, err := id.Parse(zidPart); err == nil {
			if u, err2 := url.Parse(s); err2 != nil || u.String() != s {
				return MakeReference(zsx.SymRefStateInvalid, s)
			}
			return MakeReference(SymRefStateZettel, s)
		}
		if zidPart == "00000000000000" {
			return MakeReference(zsx.SymRefStateInvalid, s)
		}
	}
	if strings.HasPrefix(s, api.QueryPrefix) {
		return MakeReference(SymRefStateQuery, s[len(api.QueryPrefix):])
	}
	if strings.HasPrefix(s, "//") {
		if u, err := url.Parse(s[1:]); err == nil {
			if u.Scheme == "" && u.Opaque == "" && u.Host == "" && u.User == nil {
				if u.String() == s[1:] {
					return MakeReference(SymRefStateBased, s[1:])
				}
				return MakeReference(zsx.SymRefStateInvalid, s)
			}
		}
	}

	if s == "" {
		return MakeReference(zsx.SymRefStateInvalid, s)
	}
	u, err := url.Parse(s)
	if err != nil || u.String() != s {
		return MakeReference(zsx.SymRefStateInvalid, s)
	}
	sym := zsx.SymRefStateExternal
	if u.Scheme == "" && u.Opaque == "" && u.Host == "" && u.User == nil {
		if s[0] == '#' {
			sym = zsx.SymRefStateSelf
		} else {
			sym = zsx.SymRefStateHosted
		}
	}
	return MakeReference(sym, s)
}

Added sz/ref_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
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
// -----------------------------------------------------------------------------
// 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 sz_test

import (
	"testing"

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

func TestParseReference(t *testing.T) {
	t.Parallel()
	testcases := []struct {
		s   string
		exp string
	}{
		{"", `(INVALID "")`},
		{"abc", `(HOSTED "abc")`},
		{"abc def", `(INVALID "abc def")`},
		{"/hosted", `(HOSTED "/hosted")`},
		{"/hosted ref", `(INVALID "/hosted ref")`},
		{"./", `(HOSTED "./")`},
		{"./12345678901234", `(HOSTED "./12345678901234")`},
		{"../", `(HOSTED "../")`},
		{"../12345678901234", `(HOSTED "../12345678901234")`},
		{"abc#frag", `(HOSTED "abc#frag")`},
		{"abc#frag space", `(INVALID "abc#frag space")`},
		{"abc#", `(INVALID "abc#")`},
		{"abc# ", `(INVALID "abc# ")`},
		{"/hosted#frag", `(HOSTED "/hosted#frag")`},
		{"./#frag", `(HOSTED "./#frag")`},
		{"./12345678901234#frag", `(HOSTED "./12345678901234#frag")`},
		{"../#frag", `(HOSTED "../#frag")`},
		{"../12345678901234#frag", `(HOSTED "../12345678901234#frag")`},
		{"#frag", `(SELF "#frag")`},
		{"#", `(INVALID "#")`},
		{"# ", `(INVALID "# ")`},
		{"https://t73f.de", `(EXTERNAL "https://t73f.de")`},
		{"https://t73f.de/12345678901234", `(EXTERNAL "https://t73f.de/12345678901234")`},
		{"http://t73f.de/1234567890", `(EXTERNAL "http://t73f.de/1234567890")`},
		{"mailto:ds@zettelstore.de", `(EXTERNAL "mailto:ds@zettelstore.de")`},
		{",://", `(INVALID ",://")`},

		// ZS specific
		{"00000000000000", `(INVALID "00000000000000")`},
		{"00000000000000#frag", `(INVALID "00000000000000#frag")`},
		{"12345678901234", `(ZETTEL "12345678901234")`},
		{"12345678901234#frag", `(ZETTEL "12345678901234#frag")`},
		{"12345678901234#", `(INVALID "12345678901234#")`},
		{"12345678901234# space", `(INVALID "12345678901234# space")`},
		{"12345678901234#frag ", `(INVALID "12345678901234#frag ")`},
		{"12345678901234#frag space", `(INVALID "12345678901234#frag space")`},
		{"query:role:zettel LIMIT 13", `(QUERY "role:zettel LIMIT 13")`},
		{"//based", `(BASED "/based")`},
		{"//based#frag", `(BASED "/based#frag")`},
		{"//based#", `(INVALID "//based#")`},
	}
	for _, tc := range testcases {
		t.Run(tc.s, func(t *testing.T) {
			if got := sz.ScanReference(tc.s); got.String() != tc.exp {
				t.Errorf("%q should be %q, but got %q", tc.s, tc.exp, got)
			}
		})
	}
}

Changes to sz/sz.go.

12
13
14
15
16
17
18
19

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

19
20


































21
22
23
24
25
26
27







-
+

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







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

// Package sz contains zettel data handling as sx expressions.
package sz

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

// GetAttributes traverses a s-expression list and returns an attribute structure.
func GetAttributes(seq *sx.Pair) (result attrs.Attributes) {
	for obj := range seq.Values() {
		pair, isPair := sx.GetPair(obj)
		if !isPair || pair == nil {
			continue
		}
		key := pair.Car()
		if !key.IsAtom() {
			continue
		}
		val := pair.Cdr()
		if tail, isTailPair := sx.GetPair(val); isTailPair {
			val = tail.Car()
		}
		if !val.IsAtom() {
			continue
		}
		result = result.Set(GoValue(key), GoValue(val))
	}
	return result
}

// GoValue returns the string value of the sx.Object suitable for Go processing.
func GoValue(obj sx.Object) string {
	switch o := obj.(type) {
	case sx.String:
		return o.GetValue()
	case *sx.Symbol:
		return o.GetValue()
	}
	return obj.String()
}

// GetMetaContent returns the metadata and the content of a sz encoded zettel.
func GetMetaContent(zettel sx.Object) (Meta, *sx.Pair) {
	if pair, isPair := sx.GetPair(zettel); isPair {
		m := pair.Car()
		if s := pair.Tail(); s != nil {
			if content, isContentPair := sx.GetPair(s.Car()); isContentPair {
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
84
85
86
87
88
89
90

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
































-
+














-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
	result.Value = next.Car()
	return result, true
}

// GetString return the metadata string value associated with the given key.
func (m Meta) GetString(key string) string {
	if v, found := m[key]; found {
		return GoValue(v.Value)
		return zsx.GoValue(v.Value)
	}
	return ""
}

// GetPair return the metadata value associated with the given key,
// as a list of objects.
func (m Meta) GetPair(key string) *sx.Pair {
	if mv, found := m[key]; found {
		if pair, isPair := sx.GetPair(mv.Value); isPair {
			return pair
		}
	}
	return nil
}

// MapRefStateToLink maps a reference state symbol to a link symbol.
func MapRefStateToLink(symRefState *sx.Symbol) *sx.Symbol {
	if sym, found := mapRefStateLink[symRefState]; found {
		return sym
	}
	return SymLinkInvalid
}

var mapRefStateLink = map[*sx.Symbol]*sx.Symbol{
	SymRefStateInvalid:  SymLinkInvalid,
	SymRefStateZettel:   SymLinkZettel,
	SymRefStateSelf:     SymLinkSelf,
	SymRefStateFound:    SymLinkFound,
	SymRefStateBroken:   SymLinkBroken,
	SymRefStateHosted:   SymLinkHosted,
	SymRefStateBased:    SymLinkBased,
	SymRefStateQuery:    SymLinkQuery,
	SymRefStateExternal: SymLinkExternal,
}

// IsBreakSym return true if the object is either a soft or a hard break symbol.
func IsBreakSym(obj sx.Object) bool {
	return SymSoft.IsEqual(obj) || SymHard.IsEqual(obj)
}

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






























































































































































































































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

package sz

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

// Visitor is walking the sx-based AST.
type Visitor interface {
	VisitBefore(node *sx.Pair, env *sx.Pair) (sx.Object, bool)
	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.Object {
	if node == nil {
		return nil
	}
	if result, ok := v.VisitBefore(node, env); ok {
		return result
	}

	if sym, isSymbol := sx.GetSymbol(node.Car()); isSymbol {
		if fn, found := mapChildrenWalk[sym]; found {
			node = fn(v, node, env)
		}
	}
	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,
		SymLinkFound:    walkChildrenInlines4,
		SymLinkHosted:   walkChildrenInlines4,
		SymLinkInvalid:  walkChildrenInlines4,
		SymLinkQuery:    walkChildrenInlines4,
		SymLinkSelf:     walkChildrenInlines4,
		SymLinkZettel:   walkChildrenInlines4,
		SymEmbed:        walkChildrenEmbed,
		SymCite:         walkChildrenInlines4,
		SymFormatDelete: walkChildrenInlines3,
		SymFormatEmph:   walkChildrenInlines3,
		SymFormatInsert: walkChildrenInlines3,
		SymFormatMark:   walkChildrenInlines3,
		SymFormatQuote:  walkChildrenInlines3,
		SymFormatStrong: walkChildrenInlines3,
		SymFormatSpan:   walkChildrenInlines3,
		SymFormatSub:    walkChildrenInlines3,
		SymFormatSuper:  walkChildrenInlines3,
	}
}

func walkChildrenTail(v Visitor, node *sx.Pair, env *sx.Pair) *sx.Pair {
	hasNil := false
	for n := range node.Tail().Pairs() {
		obj := Walk(v, n.Head(), env)
		if sx.IsNil(obj) {
			hasNil = true
		}
		n.SetCar(obj)
	}
	if !hasNil {
		return node
	}
	for n := node; ; {
		next := n.Tail()
		if next == nil {
			break
		}
		if sx.IsNil(next.Car()) {
			n.SetCdr(next.Cdr())
			continue
		}
		n = next
	}
	return node
}

func walkChildrenList(v Visitor, lst *sx.Pair, env *sx.Pair) *sx.Pair {
	hasNil := false
	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 obj := range lst.Values() {
		if !sx.IsNil(obj) {
			result.Add(obj)
		}
	}
	return result.List()
}

func walkChildrenRegion(v Visitor, node *sx.Pair, env *sx.Pair) *sx.Pair {
	// sym := node.Car()
	next := node.Tail()
	// attrs := next.Car()
	next = next.Tail()
	next.SetCar(walkChildrenList(v, next.Head(), env))
	next.SetCdr(walkChildrenList(v, next.Tail(), env))
	return node
}

func walkChildrenHeading(v Visitor, node *sx.Pair, env *sx.Pair) *sx.Pair {
	// sym := node.Car()
	next := node.Tail()
	// level := next.Car()
	next = next.Tail()
	// attrs := next.Car()
	next = next.Tail()
	// slug := next.Car()
	next = next.Tail()
	// fragment := next.Car()
	next.SetCdr(walkChildrenList(v, next.Tail(), env))
	return node
}

func walkChildrenDescription(v Visitor, dn *sx.Pair, env *sx.Pair) *sx.Pair {
	for n := dn.Tail(); n != nil; n = n.Tail() {
		n.SetCar(walkChildrenList(v, n.Head(), env))
		n = n.Tail()
		if n == nil {
			break
		}
		n.SetCar(Walk(v, n.Head(), env))
	}
	return dn
}

func walkChildrenTable(v Visitor, tn *sx.Pair, env *sx.Pair) *sx.Pair {
	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()
	next := mn.Tail()
	// mark := next.Car()
	next = next.Tail()
	// slug := next.Car()
	next = next.Tail()
	// fragment := next.Car()
	next.SetCdr(walkChildrenList(v, next.Tail(), env))
	return mn
}

func walkChildrenEmbed(v Visitor, en *sx.Pair, env *sx.Pair) *sx.Pair {
	// sym := en.Car()
	next := en.Tail()
	// attr := next.Car()
	next = next.Tail()
	// ref := next.Car()
	next = next.Tail()
	// syntax := next.Car()

	// 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()
	next = next.Tail()
	// val3 := next.Car()
	next.SetCdr(walkChildrenList(v, next.Tail(), env))
	return ln
}

func walkChildrenInlines3(v Visitor, node *sx.Pair, env *sx.Pair) *sx.Pair {
	// sym := node.Car()
	next := node.Tail() // Attrs
	// attrs := next.Car()
	next.SetCdr(walkChildrenList(v, next.Tail(), env))
	return node
}

Changes to sz/zmk/block.go.

13
14
15
16
17
18
19
20
21


22
23
24














25

26
27
28
29
30
31
32
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







-
-
+
+



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








package zmk

import (
	"fmt"

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

// parseBlock parses one block.
func (cp *Parser) parseBlock(blocksBuilder *sx.ListBuilder, lastPara *sx.Pair) *sx.Pair {
	bn, cont := cp.parseBlock0(lastPara)
	if bn != nil {
		blocksBuilder.Add(bn)
	}
	if cont {
		return lastPara
	}
	if bn.Car().IsEqual(zsx.SymPara) {
		return bn
	}
	return nil
}

func (cp *zmkP) parseBlock(lastPara *sx.Pair) (res *sx.Pair, cont bool) {
func (cp *Parser) parseBlock0(lastPara *sx.Pair) (res *sx.Pair, cont bool) {
	inp := cp.inp
	pos := inp.Pos
	if cp.nestingLevel <= maxNestingLevel {
		cp.nestingLevel++
		defer func() { cp.nestingLevel-- }()

		var bn *sx.Pair
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
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







-
+
















-
+



















-
+
















-
+




-
+






-
+









-
+







-
+







			bn, success = nil, cp.parseIndent()
		case '|':
			cp.lists = nil
			cp.descrl = nil
			bn, success = cp.parseRow(), true
		case '{':
			cp.clearStacked()
			bn, success = parseTransclusion(inp)
			bn, success = cp.parseTransclusion()
		}

		if success {
			return bn, false
		}
	}
	inp.SetPos(pos)
	cp.clearStacked()
	ins := cp.parsePara()
	if startsWithSpaceSoftBreak(ins) {
		ins = ins.Tail().Tail()
	} else if lastPara != nil {
		lastPair := lastPara.LastPair()
		lastPair.ExtendBang(ins)
		return nil, true
	}
	return sz.MakePara(ins), false
	return zsx.MakeParaList(ins), false
}

func startsWithSpaceSoftBreak(ins *sx.Pair) bool {
	if ins == nil {
		return false
	}
	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 pair0.Car().IsEqual(zsx.SymText) && 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
					}
				}
				return true
			}
		}
	}
	return false
}

var symSeparator = sx.MakeSymbol("sEpArAtOr")

func (cp *zmkP) cleanupListsAfterEOL() {
func (cp *Parser) cleanupListsAfterEOL() {
	for _, l := range cp.lists {
		l.LastPair().Head().LastPair().AppendBang(sx.Cons(symSeparator, nil))
	}
	if descrl := cp.descrl; descrl != nil {
		if lastPair, pos := lastPairPos(descrl); pos > 1 && pos%2 == 0 {
		if lastPair, pos := lastPairPos(descrl); pos > 2 && pos%2 != 0 {
			lastPair.Head().LastPair().AppendBang(sx.Cons(symSeparator, nil))
		}
	}
}

// parseColon determines which element should be parsed.
func (cp *zmkP) parseColon() (*sx.Pair, bool) {
func (cp *Parser) parseColon() (*sx.Pair, bool) {
	inp := cp.inp
	if inp.PeekN(1) == ':' {
		cp.clearStacked()
		return cp.parseRegion()
	}
	return cp.parseDefDescr()
}

// parsePara parses paragraphed inline material as a sx List.
func (cp *zmkP) parsePara() *sx.Pair {
func (cp *Parser) parsePara() *sx.Pair {
	var lb sx.ListBuilder
	for {
		in := cp.parseInline()
		if in == nil {
			return lb.List()
		}
		lb.Add(in)
		if sz.IsBreakSym(in.Car()) {
		if 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 lb.List()
			}
		}
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
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







-
+

-
+

-
+

-
+

-
+











-
+














-
+










-
+

-
+

-
+

















-
+





-
-
-
-
+
-
-
+
-




-
+


















-
+


















-
+



-
+





-
+












-
+



-
+






-
+








-
+

-
+












-
+

-
+

-
+














-
+



-
+







-
+







-
+








-
+





-
+








-
+








-
+







-
+






-
+






-
+






-
+






-
+


















-
-
+
+

-
+







	inp.SkipToEOL()
	if inp.Ch == input.EOS {
		return nil, false
	}
	var sym *sx.Symbol
	switch fch {
	case '@':
		sym = sz.SymVerbatimZettel
		sym = zsx.SymVerbatimZettel
	case '`', runeModGrave:
		sym = sz.SymVerbatimCode
		sym = zsx.SymVerbatimCode
	case '%':
		sym = sz.SymVerbatimComment
		sym = zsx.SymVerbatimComment
	case '~':
		sym = sz.SymVerbatimEval
		sym = zsx.SymVerbatimEval
	case '$':
		sym = sz.SymVerbatimMath
		sym = zsx.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 countDelim(inp, fch) >= cnt {
				inp.SkipToEOL()
				return sz.MakeVerbatim(sym, attrs, string(content)), true
				return zsx.MakeVerbatim(sym, attrs, string(content)), 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() (*sx.Pair, bool) {
func (cp *Parser) parseRegion() (*sx.Pair, bool) {
	inp := cp.inp
	fch := inp.Ch
	cnt := countDelim(inp, fch)
	if cnt < 3 {
		return nil, false
	}

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

		if !cont {
			lastPara = bn
		lastPara = cp.parseBlock(&blocksBuilder, lastPara)
		}
	}
}

// parseRegionLastLine parses the last line of a region and returns its inline text.
func (cp *zmkP) parseRegionLastLine() *sx.Pair {
func (cp *Parser) parseRegionLastLine() *sx.Pair {
	inp := cp.inp
	cp.clearStacked() // remove any lists defined in the region
	inp.SkipSpace()
	var region sx.ListBuilder
	for {
		switch inp.Ch {
		case input.EOS, '\n', '\r':
			return region.List()
		}
		in := cp.parseInline()
		if in == nil {
			return region.List()
		}
		region.Add(in)
	}
}

// parseHeading parses a head line.
func (cp *zmkP) parseHeading() (*sx.Pair, bool) {
func (cp *Parser) parseHeading() (*sx.Pair, bool) {
	inp := cp.inp
	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 := delims - 2
	var attrs *sx.Pair
	var text sx.ListBuilder
	for {
		if input.IsEOLEOS(inp.Ch) {
			return sz.MakeHeading(level, attrs, text.List(), "", ""), true
			return zsx.MakeHeading(level, attrs, text.List(), "", ""), true
		}
		in := cp.parseInline()
		if in == nil {
			return sz.MakeHeading(level, attrs, text.List(), "", ""), true
			return zsx.MakeHeading(level, attrs, text.List(), "", ""), true
		}
		text.Add(in)
		if inp.Ch == '{' && inp.Peek() != '{' {
			attrs = parseBlockAttributes(inp)
			inp.SkipToEOL()
			return sz.MakeHeading(level, attrs, text.List(), "", ""), true
			return zsx.MakeHeading(level, attrs, text.List(), "", ""), true
		}
	}
}

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

	attrs := parseBlockAttributes(inp)
	inp.SkipToEOL()
	return sz.MakeThematic(attrs), true
	return zsx.MakeThematic(attrs), true
}

// parseNestedList parses a list.
func (cp *zmkP) parseNestedList() (*sx.Pair, bool) {
func (cp *Parser) parseNestedList() (*sx.Pair, bool) {
	inp := cp.inp
	kinds := parseNestedListKinds(inp)
	if len(kinds) == 0 {
		return nil, false
	}
	inp.SkipSpace()
	if !kinds[len(kinds)-1].IsEqual(sz.SymListQuote) && input.IsEOLEOS(inp.Ch) {
	if !kinds[len(kinds)-1].IsEqual(zsx.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 := sz.MakeBlock()
	bn := zsx.MakeBlock()
	if pv != nil {
		bn.AppendBang(sz.MakePara(pv))
		bn.AppendBang(zsx.MakeParaList(pv))
	}
	lastItemPair := ln.LastPair()
	lastItemPair.AppendBang(bn)
	return cp.cleanupParsedNestedList(newLnCount)
}

func parseNestedListKinds(inp *input.Input) []*sx.Symbol {
	result := make([]*sx.Symbol, 0, 8)
	for {
		var sym *sx.Symbol
		switch inp.Ch {
		case '*':
			sym = sz.SymListUnordered
			sym = zsx.SymListUnordered
		case '#':
			sym = sz.SymListOrdered
			sym = zsx.SymListOrdered
		case '>':
			sym = sz.SymListQuote
			sym = zsx.SymListQuote
		default:
			panic(fmt.Sprintf("%q is not a region char", inp.Ch))
		}
		result = append(result, sym)
		switch inp.Next() {
		case '*', '#', '>':
		case ' ', input.EOS, '\n', '\r':
			return result
		default:
			return nil
		}
	}
}

func (cp *zmkP) buildNestedList(kinds []*sx.Symbol) (ln *sx.Pair, newLnCount int) {
func (cp *Parser) buildNestedList(kinds []*sx.Symbol) (ln *sx.Pair, newLnCount int) {
	for i, kind := range kinds {
		if i < len(cp.lists) {
			if !cp.lists[i].Car().IsEqual(kind) {
				ln = sx.Cons(kind, nil)
				ln = sx.Cons(kind, sx.Cons(sx.Nil(), sx.Nil()))
				newLnCount++
				cp.lists[i] = ln
				cp.lists = cp.lists[:i+1]
			} else {
				ln = cp.lists[i]
			}
		} else {
			ln = sx.Cons(kind, nil)
			ln = sx.Cons(kind, sx.Cons(sx.Nil(), sx.Nil()))
			newLnCount++
			cp.lists = append(cp.lists, ln)
		}
	}
	return ln, newLnCount
}

func (cp *zmkP) cleanupParsedNestedList(newLnCount int) (*sx.Pair, bool) {
func (cp *Parser) cleanupParsedNestedList(newLnCount int) (*sx.Pair, bool) {
	childPos := len(cp.lists) - 1
	parentPos := childPos - 1
	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 {
		if firstParent := parentLn.Tail().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.
			parentLn.LastPair().AppendBang(sz.MakeBlock(cp.lists[childPos]))
			parentLn.LastPair().AppendBang(zsx.MakeBlock(cp.lists[childPos]))
		}
		childPos--
		parentPos--
	}
	return nil, true
}

// parseDefTerm parses a term of a definition list.
func (cp *zmkP) parseDefTerm() (res *sx.Pair, success bool) {
func (cp *Parser) parseDefTerm() (res *sx.Pair, success bool) {
	inp := cp.inp
	if inp.Next() != ' ' {
		return nil, false
	}
	inp.Next()
	inp.SkipSpace()
	descrl := cp.descrl
	if descrl == nil {
		descrl = sx.Cons(sz.SymDescription, nil)
		descrl = sx.Cons(zsx.SymDescription, sx.Cons(sx.Nil(), sx.Nil()))
		cp.descrl = descrl
		res = descrl
	}
	lastPair, pos := lastPairPos(descrl)
	for first := true; ; first = false {
		in := cp.parseInline()
		if in == nil {
			if pos%2 == 0 {
			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 {
		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(zsx.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()) {
		if isBreakSym(in.Car()) {
			return res, true
		}
	}
}

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

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

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

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







-
+

















-
+














-
+


-
+




-
+


-
+


-
+







-
+







		node = next
		cnt++
	}
	return nil, -1
}

// parseIndent parses initial spaces to continue a list.
func (cp *zmkP) parseIndent() bool {
func (cp *Parser) parseIndent() bool {
	inp := cp.inp
	cnt := 0
	for {
		if inp.Next() != ' ' {
			break
		}
		cnt++
	}
	if cp.lists != nil {
		return cp.parseIndentForList(cnt)
	}
	if cp.descrl != nil {
		return cp.parseIndentForDescription(cnt)
	}
	return false
}

func (cp *zmkP) parseIndentForList(cnt int) bool {
func (cp *Parser) parseIndentForList(cnt int) bool {
	if len(cp.lists) < cnt {
		cnt = len(cp.lists)
	}
	cp.lists = cp.lists[:cnt]
	if cnt == 0 {
		return false
	}
	pv := cp.parseLinePara()
	if pv == nil {
		return false
	}
	ln := cp.lists[cnt-1]
	lbn := ln.LastPair().Head()
	lpn := lbn.LastPair().Head()
	if lpn.Car().IsEqual(sz.SymPara) {
	if lpn.Car().IsEqual(zsx.SymPara) {
		lpn.LastPair().SetCdr(pv)
	} else {
		lbn.LastPair().AppendBang(sz.MakePara(pv))
		lbn.LastPair().AppendBang(zsx.MakeParaList(pv))
	}
	return true
}

func (cp *zmkP) parseIndentForDescription(cnt int) bool {
func (cp *Parser) parseIndentForDescription(cnt int) bool {
	descrl := cp.descrl
	lastPair, pos := lastPairPos(descrl)
	if cnt < 1 || pos < 1 {
	if cnt < 1 || pos < 2 {
		return false
	}
	if pos%2 == 1 {
	if pos%2 == 0 {
		// Continuation of a definition term
		for {
			in := cp.parseInline()
			if in == nil {
				return true
			}
			lastPair.Head().LastPair().AppendBang(in)
			if sz.IsBreakSym(in.Car()) {
			if isBreakSym(in.Car()) {
				return true
			}
		}
	}

	// Continuation of a definition description.
	// Either it is a continuation of a definition paragraph, or it is a new paragraph.
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
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







-
+







-
+


-
+





-
+







-
+






-
+







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

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

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

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

	var row sx.ListBuilder
658
659
660
661
662
663
664
665

666
667
668
669
670
671
672
673
674
675

676
677
678
679
680
681
682
683

684
685
686

687
688
689
690
691
692
693
694
695


696
697
698
699
700
701
702
703
704
705
706
707
708

709
710
711
712
713
714
715
667
668
669
670
671
672
673

674
675
676
677
678
679
680
681
682
683

684
685
686
687
688
689
690
691

692
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







-
+









-
+







-
+


-
+








-
+
+












-
+







		case input.EOS:
			// add to table
			if cp.lastRow == nil {
				if row.IsEmpty() {
					return nil
				}
				cp.lastRow = sx.Cons(row.List(), nil)
				return cp.lastRow.Cons(nil).Cons(sz.SymTable)
				return cp.lastRow.Cons(nil).Cons(zsx.SymTable)
			}
			cp.lastRow = cp.lastRow.AppendBang(row.List())
			return nil
		}
		// inp.Ch must be '|'
	}
}

// parseCell parses one single cell of a table row.
func (cp *zmkP) parseCell() *sx.Pair {
func (cp *Parser) parseCell() *sx.Pair {
	inp := cp.inp
	var cell sx.ListBuilder
	for {
		if input.IsEOLEOS(inp.Ch) {
			if cell.IsEmpty() {
				return nil
			}
			return sz.MakeCell(sz.SymCell, cell.List())
			return zsx.MakeCell(nil, cell.List())
		}
		if inp.Ch == '|' {
			return sz.MakeCell(sz.SymCell, cell.List())
			return zsx.MakeCell(nil, cell.List())
		}

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

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

loop:

	for {
		switch inp.Ch {
		case input.EOS:
			return nil, false
		case '\n', '\r', ' ', '\t':
			if !hasQueryPrefix(inp.Src[posA:]) {
			if !cp.isSpaceReference(inp.Src[posA:]) {
				return nil, false
			}
		case '\\':
			switch inp.Next() {
			case input.EOS, '\n', '\r':
				return nil, false
			}
728
729
730
731
732
733
734
735
736


737
738
739
740
741
742
743
744


745
746
747







-
-
+
+

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

Changes to sz/zmk/inline.go.

15
16
17
18
19
20
21
22
23


24
25
26
27

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


22
23

24
25

26
27
28
29
30
31
32
33







-
-
+
+
-


-
+








import (
	"fmt"
	"slices"
	"strings"

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

func (cp *zmkP) parseInline() *sx.Pair {
func (cp *Parser) parseInline() *sx.Pair {
	inp := cp.inp
	pos := inp.Pos
	if cp.nestingLevel <= maxNestingLevel {
		cp.nestingLevel++
		defer func() { cp.nestingLevel-- }()

		var in *sx.Pair
72
73
74
75
76
77
78
79

80
81
82
83
84
85
86
71
72
73
74
75
76
77

78
79
80
81
82
83
84
85







-
+







			return in
		}
	}
	inp.SetPos(pos)
	return parseText(inp)
}

func parseText(inp *input.Input) *sx.Pair { return sz.MakeText(parseString(inp)) }
func parseText(inp *input.Input) *sx.Pair { return zsx.MakeText(parseString(inp)) }

func parseString(inp *input.Input) string {
	pos := inp.Pos
	if inp.Ch == '\\' {
		inp.Next()
		return parseBackslashRest(inp)
	}
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
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







-
+

-
+


















-
+


-
+



-
+
-
-
-
+




-
+



-
+





-
-
-
-
-
+









-
+







	}
}

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

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

func parseSoftBreak(inp *input.Input) *sx.Pair {
	inp.EatEOL()
	return sz.MakeSoft()
	return zsx.MakeSoft()
}

func (cp *zmkP) parseLink(openCh, closeCh rune) (*sx.Pair, bool) {
func (cp *Parser) parseLink(openCh, closeCh rune) (*sx.Pair, bool) {
	if refString, text, ok := cp.parseReference(openCh, closeCh); ok {
		attrs := parseInlineAttributes(cp.inp)
		if len(refString) > 0 {
			ref := ParseReference(refString)
			ref := cp.scanReference(refString)
			refSym, _ := sx.GetSymbol(ref.Car())
			sym := sz.MapRefStateToLink(refSym)
			return sz.MakeLink(sym, attrs, ref.Tail().Car().(sx.String).GetValue(), text), true
			return zsx.MakeLink(attrs, ref, text), true
		}
	}
	return nil, false
}
func (cp *zmkP) parseEmbed(openCh, closeCh rune) (*sx.Pair, bool) {
func (cp *Parser) 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 zsx.MakeEmbed(attrs, cp.scanReference(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) (string, *sx.Pair, bool) {
func (cp *Parser) 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 lb sx.ListBuilder
	pos := inp.Pos
	if !hasQueryPrefix(inp.Src[pos:]) {
	if !cp.isSpaceReference(inp.Src[pos:]) {
		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
183
184
185
186
187
188
189
190

191
192
193
194
195
196
197
176
177
178
179
180
181
182

183
184
185
186
187
188
189
190







-
+







			}
			inp.SetPos(pos)
		}
	}

	inp.SkipSpace()
	pos = inp.Pos
	if !readReferenceToClose(inp, closeCh) {
	if !cp.readReferenceToClose(closeCh) {
		return "", nil, false
	}
	ref := strings.TrimSpace(string(inp.Src[pos:inp.Pos]))
	if inp.Next() != closeCh {
		return "", nil, false
	}
	inp.Next()
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
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







-
+
+






-
+














-
+







			}
			continue
		}
		inp.Next()
	}
}

func readReferenceToClose(inp *input.Input, closeCh rune) bool {
func (cp *Parser) readReferenceToClose(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:]) {
			if !cp.isSpaceReference(inp.Src[pos:]) {
				return false
			}
		case '\\':
			switch inp.Next() {
			case input.EOS, '\n', '\r':
				return false
			}
		case closeCh:
			return true
		}
		inp.Next()
	}
}

func (cp *zmkP) parseCite() (*sx.Pair, bool) {
func (cp *Parser) parseCite() (*sx.Pair, bool) {
	inp := cp.inp
	switch inp.Next() {
	case ' ', ',', '|', ']', '\n', '\r':
		return nil, false
	}
	pos := inp.Pos
loop:
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
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







-
+


-
+






-
+


-
+







		inp.Next()
	}
	ins, ok := cp.parseLinkLikeRest()
	if !ok {
		return nil, false
	}
	attrs := parseInlineAttributes(inp)
	return sz.MakeCite(attrs, string(inp.Src[pos:posL]), ins), true
	return zsx.MakeCite(attrs, string(inp.Src[pos:posL]), ins), true
}

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

func (cp *zmkP) parseMark() (*sx.Pair, bool) {
func (cp *Parser) parseMark() (*sx.Pair, bool) {
	inp := cp.inp
	inp.Next()
	pos := inp.Pos
	for inp.Ch != '|' && inp.Ch != ']' {
		if !isNameRune(inp.Ch) {
			return nil, false
		}
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
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







-
+




-
+









-
+



















-
+






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


-
+




















-
+

-
+

-
+








-
-
-
-
+
+
+
+







		ins, ok = cp.parseLinkLikeRest()
		if !ok {
			return nil, false
		}
	} else {
		inp.Next()
	}
	return sz.MakeMark(mark, "", "", ins), true
	return zsx.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) {
func (cp *Parser) parseLinkLikeRest() (*sx.Pair, bool) {
	var ins sx.ListBuilder
	inp := cp.inp
	inp.SkipSpace()
	for inp.Ch != ']' {
		in := cp.parseInline()
		if in == nil {
			return nil, false
		}
		ins.Add(in)
		if input.IsEOLEOS(inp.Ch) && sz.IsBreakSym(in.Car()) {
		if input.IsEOLEOS(inp.Ch) && isBreakSym(in.Car()) {
			return nil, false
		}
	}
	inp.Next()
	return ins.List(), true
}

func parseComment(inp *input.Input) (*sx.Pair, bool) {
	if inp.Next() != '%' {
		return nil, false
	}
	for inp.Ch == '%' {
		inp.Next()
	}
	attrs := parseInlineAttributes(inp)
	inp.SkipSpace()
	pos := inp.Pos
	for {
		if input.IsEOLEOS(inp.Ch) {
			return sz.MakeLiteral(sz.SymLiteralComment, attrs, string(inp.Src[pos:inp.Pos])), true
			return zsx.MakeLiteral(zsx.SymLiteralComment, attrs, 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,
	'_': zsx.SymFormatEmph,
	'*': zsx.SymFormatStrong,
	'>': zsx.SymFormatInsert,
	'~': zsx.SymFormatDelete,
	'^': zsx.SymFormatSuper,
	',': zsx.SymFormatSub,
	'"': zsx.SymFormatQuote,
	'#': zsx.SymFormatMark,
	':': zsx.SymFormatSpan,
}

func (cp *zmkP) parseFormat() (*sx.Pair, bool) {
func (cp *Parser) 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.ListBuilder
	for {
		if inp.Ch == input.EOS {
			return nil, false
		}
		if inp.Ch == fch {
			if inp.Next() == fch {
				inp.Next()
				attrs := parseInlineAttributes(inp)
				return sz.MakeFormat(symFormat, attrs, inlines.List()), true
				return zsx.MakeFormat(symFormat, attrs, inlines.List()), true
			}
			inlines.Add(sz.MakeText(string(fch)))
			inlines.Add(zsx.MakeText(string(fch)))
		} else if in := cp.parseInline(); in != nil {
			if input.IsEOLEOS(inp.Ch) && sz.IsBreakSym(in.Car()) {
			if input.IsEOLEOS(inp.Ch) && isBreakSym(in.Car()) {
				return nil, false
			}
			inlines.Add(in)
		}
	}
}

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

func parseLiteral(inp *input.Input) (*sx.Pair, bool) {
	fch := inp.Ch
	symLiteral, ok := mapRuneLiteral[fch]
	if !ok {
424
425
426
427
428
429
430
431

432
433
434
435
436
437
438
418
419
420
421
422
423
424

425
426
427
428
429
430
431
432







-
+







		if inp.Ch == input.EOS {
			return nil, false
		}
		if inp.Ch == fch {
			if inp.Peek() == fch {
				inp.Next()
				inp.Next()
				return sz.MakeLiteral(symLiteral, parseInlineAttributes(inp), sb.String()), true
				return zsx.MakeLiteral(symLiteral, parseInlineAttributes(inp), sb.String()), true
			}
			sb.WriteRune(fch)
			inp.Next()
		} else {
			s := parseString(inp)
			sb.WriteString(s)
		}
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
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







-
+











-
+



-
-
+
+



		if inp.Ch == input.EOS {
			return nil, false
		}
		if inp.Ch == '$' && inp.Peek() == '$' {
			content := slices.Clone(inp.Src[pos:inp.Pos])
			inp.Next()
			inp.Next()
			return sz.MakeLiteral(sz.SymLiteralMath, parseInlineAttributes(inp), string(content)), true
			return zsx.MakeLiteral(zsx.SymLiteralMath, parseInlineAttributes(inp), string(content)), true
		}
		inp.Next()
	}
}

func parseNdash(inp *input.Input) (*sx.Pair, bool) {
	if inp.Peek() != inp.Ch {
		return nil, false
	}
	inp.Next()
	inp.Next()
	return sz.MakeText("\u2013"), true
	return zsx.MakeText("\u2013"), true
}

func parseEntity(inp *input.Input) (*sx.Pair, bool) {
	if text, ok := inp.ScanEntity(); ok {
		return sz.MakeText(text), true
	if text, ok := zsx.ScanEntity(inp); ok {
		return zsx.MakeText(text), true
	}
	return nil, false
}

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

13
14
15
16
17
18
19
20

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

20
21
22
23
24
25
26
27







-
+








package zmk

import (
	"strings"

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

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

type postProcessor struct{}

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







-
+










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

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







}

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 := range lst.Pairs() {
		if elem, isPair := sx.GetPair(sz.Walk(pp, node.Head(), env)); isPair && elem != nil {
		if elem, isPair := sx.GetPair(zsx.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.SymVerbatimCode:    postProcessVerbatim,
		sz.SymVerbatimZettel:  postProcessVerbatim,
		sz.SymHeading:         postProcessHeading,
		sz.SymListOrdered:     postProcessItemList,
		sz.SymListUnordered:   postProcessItemList,
		sz.SymListQuote:       postProcessQuoteList,
		sz.SymDescription:     postProcessDescription,
		sz.SymTable:           postProcessTable,
		zsx.SymBlock:           postProcessBlockList,
		zsx.SymPara:            postProcessInlineList,
		zsx.SymRegionBlock:     postProcessRegion,
		zsx.SymRegionQuote:     postProcessRegion,
		zsx.SymRegionVerse:     postProcessRegionVerse,
		zsx.SymVerbatimComment: postProcessVerbatim,
		zsx.SymVerbatimEval:    postProcessVerbatim,
		zsx.SymVerbatimMath:    postProcessVerbatim,
		zsx.SymVerbatimCode:    postProcessVerbatim,
		zsx.SymVerbatimZettel:  postProcessVerbatim,
		zsx.SymHeading:         postProcessHeading,
		zsx.SymListOrdered:     postProcessItemList,
		zsx.SymListUnordered:   postProcessItemList,
		zsx.SymListQuote:       postProcessQuoteList,
		zsx.SymDescription:     postProcessDescription,
		zsx.SymTable:           postProcessTable,

		sz.SymInline:       postProcessInlineList,
		sz.SymText:         postProcessText,
		sz.SymSoft:         postProcessSoft,
		sz.SymEndnote:      postProcessEndnote,
		sz.SymMark:         postProcessMark,
		sz.SymLinkBased:    postProcessInlines4,
		zsx.SymInline:       postProcessInlineList,
		zsx.SymText:         postProcessText,
		zsx.SymSoft:         postProcessSoft,
		zsx.SymEndnote:      postProcessEndnote,
		zsx.SymMark:         postProcessMark,
		zsx.SymLink:         postProcessInlines4,
		sz.SymLinkBroken:   postProcessInlines4,
		sz.SymLinkExternal: postProcessInlines4,
		sz.SymLinkFound:    postProcessInlines4,
		sz.SymLinkHosted:   postProcessInlines4,
		sz.SymLinkInvalid:  postProcessInlines4,
		sz.SymLinkQuery:    postProcessInlines4,
		sz.SymLinkSelf:     postProcessInlines4,
		sz.SymLinkZettel:   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,
		sz.SymFormatSpan:   postProcessFormat,
		sz.SymFormatSub:    postProcessFormat,
		sz.SymFormatSuper:  postProcessFormat,
		zsx.SymEmbed:        postProcessEmbed,
		zsx.SymCite:         postProcessInlines4,
		zsx.SymFormatDelete: postProcessFormat,
		zsx.SymFormatEmph:   postProcessFormat,
		zsx.SymFormatInsert: postProcessFormat,
		zsx.SymFormatMark:   postProcessFormat,
		zsx.SymFormatQuote:  postProcessFormat,
		zsx.SymFormatStrong: postProcessFormat,
		zsx.SymFormatSpan:   postProcessFormat,
		zsx.SymFormatSub:    postProcessFormat,
		zsx.SymFormatSuper:  postProcessFormat,

		symSeparator: ignoreProcess,
	}
}

func ignoreProcess(*postProcessor, *sx.Pair, *sx.Pair) *sx.Pair { return nil }

136
137
138
139
140
141
142
143

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

164
165
166
167
168

169

170
171
172
173

174
175
176

177

178
179
180
181
182
183
184
185
186

187
188
189
190
191
192

193
194
195
196
197
198
199
200
201

202
203

204
205
206
207
208
209
210
211
212

213
214
215
216
217
218

219
220
221
222
223
224
225

226
227
228

229
230
231
232
233

234
235
236

237
238
239
240
241
242
243
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







-
+



















-
+





+
-
+



-
+



+
-
+








-
+





-
+








-
+

-
+








-
+





-
+







+


-
+




-
+


-
+







	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 sz.MakeRegion(sym, attrs, blocks, text)
	return zsx.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 {
	next := hn.Tail()
	level := next.Car().(sx.Int64)
	next = next.Tail()
	attrs := next.Car().(*sx.Pair)
	next = next.Tail()
	slug := next.Car().(sx.String)
	next = next.Tail()
	fragment := next.Car().(sx.String)
	if text := pp.visitInlines(next.Tail(), env); text != nil {
		return sz.MakeHeading(int(level), attrs, text, slug.GetValue(), fragment.GetValue())
		return zsx.MakeHeading(int(level), attrs, text, slug.GetValue(), fragment.GetValue())
	}
	return nil
}

func postProcessItemList(pp *postProcessor, ln *sx.Pair, env *sx.Pair) *sx.Pair {
	attrs := ln.Tail().Head()
	elems := pp.visitListElems(ln, env)
	elems := pp.visitListElems(ln.Tail(), env)
	if elems == nil {
		return nil
	}
	return elems.Cons(ln.Car())
	return zsx.MakeList(ln.Car().(*sx.Symbol), attrs, elems)
}

func postProcessQuoteList(pp *postProcessor, ln *sx.Pair, env *sx.Pair) *sx.Pair {
	attrs := ln.Tail().Head()
	elems := pp.visitListElems(ln, env.Cons(sx.Cons(symNoBlock, nil)))
	elems := pp.visitListElems(ln.Tail(), env.Cons(sx.Cons(symNoBlock, nil)))

	// Collect multiple paragraph items into one item.

	var newElems sx.ListBuilder
	var newPara sx.ListBuilder

	addtoParagraph := func() {
		if !newPara.IsEmpty() {
			newElems.Add(sx.MakeList(sz.SymBlock, newPara.List().Cons(sz.SymPara)))
			newElems.Add(sx.MakeList(zsx.SymBlock, newPara.List().Cons(zsx.SymPara)))
			newPara.Reset()
		}
	}
	for node := range elems.Pairs() {
		item := node.Head()
		if !item.Car().IsEqual(sz.SymBlock) {
		if !item.Car().IsEqual(zsx.SymBlock) {
			continue
		}
		itemTail := item.Tail()
		if itemTail == nil || itemTail.Tail() != nil {
			addtoParagraph()
			newElems.Add(item)
			continue
		}
		if pn := itemTail.Head(); pn.Car().IsEqual(sz.SymPara) {
		if pn := itemTail.Head(); pn.Car().IsEqual(zsx.SymPara) {
			if !newPara.IsEmpty() {
				newPara.Add(sx.Cons(sz.SymSoft, nil))
				newPara.Add(sx.Cons(zsx.SymSoft, nil))
			}
			newPara.ExtendBang(pn.Tail())
			continue
		}
		addtoParagraph()
		newElems.Add(item)
	}
	addtoParagraph()
	return newElems.List().Cons(ln.Car())
	return zsx.MakeList(ln.Car().(*sx.Symbol), attrs, newElems.List())
}

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

func postProcessTable(pp *postProcessor, tbl *sx.Pair, env *sx.Pair) *sx.Pair {
	sym := tbl.Car()
	next := tbl.Tail()
	header := next.Head()
	if header != nil {
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
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







+
+
-
+
-
-
+





-
-
+
+








-
-
+
+
+




-
-
+
+











-
+



-
+


-
-
+
+


-
-
+
+

+

-
+
-





-
-
-



-
-
-
-
-



-
+




-
+



-
-
-
-
+
+
+
+
+
+
+


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

-
+





-
-
-
+
+
+
+
+
+
+



+
+
+
+
-
+


-
+

-
+

-
+

-
+












-
+






-
+









-
+







}

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

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

	foundHeader := false
	cellCount := 0

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

		// elem is first cell inline element
		elem := cellTail.Head()
		if elem.Car().IsEqual(sz.SymText) {
		elem := cellInlines.Head()
		if elem.Car().IsEqual(zsx.SymText) {
			if s, isString := sx.GetString(elem.Tail().Car()); isString && s.GetValue() != "" {
				str := s.GetValue()
				if str[0] == '=' {
					foundHeader = true
					elem.SetCdr(sx.Cons(sx.MakeString(str[1:]), nil))
				}
			}
		}

		// move to the last cell inline element
		for {
			next := cellTail.Tail()
			next := cellInlines.Tail()
			if next == nil {
				break
			}
			cellTail = next
			cellInlines = next
		}

		elem = cellTail.Head()
		if elem.Car().IsEqual(sz.SymText) {
		elem = cellInlines.Head()
		if elem.Car().IsEqual(zsx.SymText) {
			if s, isString := sx.GetString(elem.Tail().Car()); isString && s.GetValue() != "" {
				str := s.GetValue()
				cellAlign := getCellAlignment(str[len(str)-1])
				if !cellAlign.IsEqualSymbol(sz.SymCell) {
				lastByte := str[len(str)-1]
				if cellAlign, isValid := getCellAlignment(lastByte); isValid {
					elem.SetCdr(sx.Cons(sx.MakeString(str[0:len(str)-1]), nil))
					rest.SetCar(makeCellAttrs(cellAlign))
				}
				align[cellCount-1] = cellAlign
				align[cellCount-1] = lastByte
				cell.SetCar(cellAlign)
			}
		}
	}

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

	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) {
func alignRow(row *sx.Pair, defaultAlign []byte) {
	if row == nil {
		return
	}
	var lastCellNode *sx.Pair
	cellCount := 0
	cellColumnNo := 0
	for node := range row.Pairs() {
		lastCellNode = node
		cell := node.Head()
		cell.SetCar(align[cellCount])
		cellCount++
		cellTail := cell.Tail()
		if cellTail == nil {
		cellColumnNo++
		rest := cell.Tail() // attrs := rest.Head()
		if cellAlign, isValid := getCellAlignment(defaultAlign[cellColumnNo-1]); isValid {
			rest.SetCar(makeCellAttrs(cellAlign))
		}
		cellInlines := rest.Tail()
		if cellInlines == nil {
			continue
		}

		// elem is first cell inline element
		elem := cellInlines.Head()
		if elem.Car().IsEqual(zsx.SymText) {
			if s, isString := sx.GetString(elem.Tail().Car()); isString && s.GetValue() != "" {
				str := s.GetValue()
				cellAlign, isValid := getCellAlignment(str[0])

				if isValid {
		// elem is first cell inline element
		elem := cellTail.Head()
		if elem.Car().IsEqual(sz.SymText) {
			if s, isString := sx.GetString(elem.Tail().Car()); isString && s.GetValue() != "" {
				str := s.GetValue()
				cellAlign := getCellAlignment(str[0])
				if !cellAlign.IsEqualSymbol(sz.SymCell) {
					elem.SetCdr(sx.Cons(sx.MakeString(str[1:]), nil))
					cell.SetCar(cellAlign)
					rest.SetCar(makeCellAttrs(cellAlign))
				}
			}
		}
	}

	for cellCount < len(align) {
		lastCellNode = lastCellNode.AppendBang(sx.Cons(align[cellCount], nil))
		cellCount++
	for cellColumnNo < len(defaultAlign) {
		var attrs *sx.Pair
		if cellAlign, isValid := getCellAlignment(defaultAlign[cellColumnNo]); isValid {
			attrs = makeCellAttrs(cellAlign)
		}
		lastCellNode = lastCellNode.AppendBang(zsx.MakeCell(attrs, nil))
		cellColumnNo++
	}
}

func makeCellAttrs(align sx.String) *sx.Pair {
	return sx.Cons(sx.Cons(zsx.SymAttrAlign, align), sx.Nil())
}

func getCellAlignment(ch byte) *sx.Symbol {
func getCellAlignment(ch byte) (sx.String, bool) {
	switch ch {
	case ':':
		return sz.SymCellCenter
		return zsx.AttrAlignCenter, true
	case '<':
		return sz.SymCellLeft
		return zsx.AttrAlignLeft, true
	case '>':
		return sz.SymCellRight
		return zsx.AttrAlignRight, true
	default:
		return sz.SymCell
		return sx.MakeString(""), false
	}
}

func (pp *postProcessor) visitInlines(lst *sx.Pair, env *sx.Pair) *sx.Pair {
	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 := range lst.Pairs() {
		elem, isPair := sx.GetPair(sz.Walk(pp, node.Head(), env))
		elem, isPair := sx.GetPair(zsx.Walk(pp, node.Head(), env))
		if !isPair || elem == nil {
			continue
		}
		elemSym := elem.Car()
		elemTail := elem.Tail()

		if inVerse && elemSym.IsEqual(sz.SymText) {
		if inVerse && elemSym.IsEqual(zsx.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 !elemSym.IsEqual(sz.SymText) {
			if !elemSym.IsEqual(zsx.SymText) {
				vector = append(vector, elem)
				continue
			}

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







-
+







-
+





-
+



-
+















-
+








-
+







				vector = append(vector, elem)
			}
			continue
		}
		last := vector[len(vector)-1]
		lastSym := last.Car()

		if lastSym.IsEqual(sz.SymText) && elemSym.IsEqual(sz.SymText) {
		if lastSym.IsEqual(zsx.SymText) && elemSym.IsEqual(zsx.SymText) {
			// Merge two TEXT elements into one
			lastText := last.Tail().Car().(sx.String).GetValue()
			elemText := elem.Tail().Car().(sx.String).GetValue()
			last.SetCdr(sx.Cons(sx.MakeString(lastText+elemText), sx.Nil()))
			continue
		}

		if lastSym.IsEqual(sz.SymText) && elemSym.IsEqual(sz.SymSoft) {
		if lastSym.IsEqual(zsx.SymText) && elemSym.IsEqual(zsx.SymSoft) {
			// Merge (TEXT "... ") (SOFT) to (TEXT "...") (HARD)
			lastTail := last.Tail()
			if lastText := lastTail.Car().(sx.String).GetValue(); strings.HasSuffix(lastText, " ") {
				newText := removeTrailingSpaces(lastText)
				if newText == "" {
					vector[len(vector)-1] = sx.Cons(sz.SymHard, sx.Nil())
					vector[len(vector)-1] = sx.Cons(zsx.SymHard, sx.Nil())
					continue
				}
				lastTail.SetCar(sx.MakeString(newText))
				elemSym = sz.SymHard
				elemSym = zsx.SymHard
				elem.SetCar(elemSym)
			}
		}

		vector = append(vector, elem)
	}
	if len(vector) == 0 {
		return nil
	}

	// 2nd phase: remove (SOFT), (HARD) at the end, remove trailing spaces in (TEXT "...")
	lastPos := len(vector) - 1
	for lastPos >= 0 {
		elem := vector[lastPos]
		elemSym := elem.Car()
		if elemSym.IsEqual(sz.SymText) {
		if elemSym.IsEqual(zsx.SymText) {
			elemTail := elem.Tail()
			elemText := elemTail.Car().(sx.String).GetValue()
			newText := removeTrailingSpaces(elemText)
			if newText != "" {
				elemTail.SetCar(sx.MakeString(newText))
				break
			}
			lastPos--
		} else if sz.IsBreakSym(elemSym) {
		} else if isBreakSym(elemSym) {
			lastPos--
		} else {
			break
		}
	}
	if lastPos < 0 {
		return nil
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
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







-
+






-
+

-
+










-
+




















-
+











-
+

	return nil
}

func postProcessSoft(_ *postProcessor, sn *sx.Pair, env *sx.Pair) *sx.Pair {
	if env.Assoc(symInVerse) == nil {
		return sn
	}
	return sx.Cons(sz.SymHard, nil)
	return sx.Cons(zsx.SymHard, nil)
}

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

func postProcessMark(pp *postProcessor, en *sx.Pair, env *sx.Pair) *sx.Pair {
	next := en.Tail()
	mark := next.Car().(sx.String)
	next = next.Tail()
	slug := next.Car().(sx.String)
	next = next.Tail()
	fragment := next.Car().(sx.String)
	text := pp.visitInlines(next.Tail(), env)
	return sz.MakeMark(mark.GetValue(), slug.GetValue(), fragment.GetValue(), text)
	return zsx.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)
	return zsx.MakeEmbed(attrs, ref, syntax.GetValue(), text)
}

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

Deleted sz/zmk/ref.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











































































































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

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)
	}
	if strings.HasPrefix(s, api.QueryPrefix) {
		return makePairRef(sz.SymRefStateQuery, s[len(api.QueryPrefix):])
	}
	if state, ok := localState(s); ok {
		if state.IsEqualSymbol(sz.SymRefStateBased) {
			s = s[1:]
		}
		_, err := url.Parse(s)
		if err == nil {
			return makePairRef(state, s)
		}
	}
	u, err := url.Parse(s)
	if err != nil {
		return makePairRef(sz.SymRefStateInvalid, s)
	}
	if !externalURL(u) {
		if _, err = id.Parse(u.Path); err == nil {
			return makePairRef(sz.SymRefStateZettel, s)
		}
		if u.Path == "" && u.Fragment != "" {
			return makePairRef(sz.SymRefStateSelf, s)
		}
	}
	return makePairRef(sz.SymRefStateExternal, s)
}
func makePairRef(sym *sx.Symbol, val string) *sx.Pair {
	return sx.MakeList(sym, sx.MakeString(val))
}

func invalidReference(s string) bool { return s == "" || s == "00000000000000" }

func externalURL(u *url.URL) bool {
	return u.Scheme != "" || u.Opaque != "" || u.Host != "" || u.User != nil
}

func localState(path string) (*sx.Symbol, bool) {
	if len(path) > 0 && path[0] == '/' {
		if len(path) > 1 && path[1] == '/' {
			return sz.SymRefStateBased, true
		}
		return sz.SymRefStateHosted, true
	}
	if len(path) > 1 && path[0] == '.' {
		if len(path) > 2 && path[1] == '.' && path[2] == '/' {
			return sz.SymRefStateHosted, true
		}
		return sz.SymRefStateHosted, path[1] == '/'
	}
	return sz.SymRefStateInvalid, false
}

// ReferenceIsValid returns true if reference is valid
func ReferenceIsValid(ref *sx.Pair) bool {
	return !ref.Car().IsEqual(sz.SymRefStateInvalid)
}

// ReferenceIsZettel returns true if it is a reference to a local zettel.
func ReferenceIsZettel(ref *sx.Pair) bool {
	state := ref.Car()
	return state.IsEqual(sz.SymRefStateZettel) ||
		state.IsEqual(sz.SymRefStateSelf) ||
		state.IsEqual(sz.SymRefStateFound) ||
		state.IsEqual(sz.SymRefStateBroken)
}

// ReferenceIsLocal returns true if reference is local
func ReferenceIsLocal(ref *sx.Pair) bool {
	state := ref.Car()
	return state.IsEqual(sz.SymRefStateHosted) ||
		state.IsEqual(sz.SymRefStateBased)
}

// ReferenceIsExternal returns true if it is a reference to external material.
func ReferenceIsExternal(ref *sx.Pair) bool {
	return ref.Car().IsEqual(sz.SymRefStateExternal)
}

Deleted sz/zmk/ref_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



































































































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

import (
	"testing"

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

func TestParseReference(t *testing.T) {
	t.Parallel()
	testcases := []struct {
		link string
		err  bool
		exp  string
	}{
		{"", true, ""},
		{"12345678901234", false, "(ZETTEL \"12345678901234\")"},
		{"123", false, "(EXTERNAL \"123\")"},
		{",://", true, ""},
	}

	for i, tc := range testcases {
		got := zmk.ParseReference(tc.link)
		gotIsValid := zmk.ReferenceIsValid(got)
		if gotIsValid == tc.err {
			t.Errorf(
				"TC=%d, expected parse error of %q: %v, but got %q", i, tc.link, tc.err, got)
		}
		if gotIsValid && got.String() != tc.exp {
			t.Errorf("TC=%d, Reference of %q is %q, but got %q", i, tc.link, tc.exp, got)
		}
	}
}

func TestReferenceIsZettelMaterial(t *testing.T) {
	t.Parallel()
	testcases := []struct {
		link       string
		isZettel   bool
		isExternal bool
		isLocal    bool
	}{
		{"", false, false, false},
		{"00000000000000", false, false, false},
		{"http://zettelstore.de/z/ast", false, true, false},
		{"12345678901234", true, false, false},
		{"12345678901234#local", true, false, false},
		{"http://12345678901234", false, true, false},
		{"http://zettelstore.de/z/12345678901234", false, true, false},
		{"http://zettelstore.de/12345678901234", false, true, false},
		{"/12345678901234", false, false, true},
		{"//12345678901234", false, false, true},
		{"./12345678901234", false, false, true},
		{"../12345678901234", false, false, true},
		{".../12345678901234", false, true, false},
	}

	for i, tc := range testcases {
		ref := zmk.ParseReference(tc.link)
		isZettel := zmk.ReferenceIsZettel(ref)
		if isZettel != tc.isZettel {
			t.Errorf(
				"TC=%d, Reference %q isZettel=%v expected, but got %v",
				i,
				tc.link,
				tc.isZettel,
				isZettel)
		}
		isLocal := zmk.ReferenceIsLocal(ref)
		if isLocal != tc.isLocal {
			t.Errorf(
				"TC=%d, Reference %q isLocal=%v expected, but got %v",
				i,
				tc.link,
				tc.isLocal, isLocal)
		}
		isExternal := zmk.ReferenceIsExternal(ref)
		if isExternal != tc.isExternal {
			t.Errorf(
				"TC=%d, Reference %q isExternal=%v expected, but got %v",
				i,
				tc.link,
				tc.isExternal,
				isExternal)
		}
	}
}

Changes to sz/zmk/zmk.go.

17
18
19
20
21
22
23
24

25
26
27

28
29
30
31

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

51
52
53
54
55

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







-
+

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





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











-
+







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

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

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

	"t73f.de/r/zsx/input"
	var lastPara *sx.Pair
	var blkBuild sx.ListBuilder
	for inp.Ch != input.EOS {
		bn, cont := parser.parseBlock(lastPara)
		if bn != nil {
			blkBuild.Add(bn)
		}
		if !cont {
			if bn.Car().IsEqual(sz.SymPara) {
				lastPara = bn
			} else {
				lastPara = nil
			}
		}
	}
	if parser.nestingLevel != 0 {
		panic("Nesting level was not decremented")
	}

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

	return nil
}

type zmkP struct {
// Parser allows to parse its plain text input into Zettelmarkup.
type Parser 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

	scanReference    func(string) *sx.Pair // Builds a reference node from a given string reference
	isSpaceReference func([]byte) bool     // Returns true, if src starts with a reference that allows white space
}

// Initialize the parser with the input stream and a reference scanner.
func (cp *Parser) Initialize(inp *input.Input) {
	var zeroParser Parser
	*cp = zeroParser
	cp.inp = inp
	cp.scanReference = sz.ScanReference
	cp.isSpaceReference = withQueryPrefix
}

// Parse tries to parse the input as a block element.
func (cp *Parser) Parse() *sx.Pair {

	var lastPara *sx.Pair
	var blkBuild sx.ListBuilder
	for cp.inp.Ch != input.EOS {
		lastPara = cp.parseBlock(&blkBuild, lastPara)
	}
	if cp.nestingLevel != 0 {
		panic("Nesting level was not decremented")
	}

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

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

// 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 '`'!

const maxNestingLevel = 50

// clearStacked removes all multi-line nodes from parser.
func (cp *zmkP) clearStacked() {
func (cp *Parser) clearStacked() {
	cp.lists = nil
	cp.lastRow = nil
	cp.descrl = nil
}

type attrMap map[string]string

245
246
247
248
249
250
251





252
253
254
255
256
257
258
259
260
261
262
263







+
+
+
+
+
		}
	}
}

func isNameRune(ch rune) bool {
	return unicode.IsLetter(ch) || unicode.IsDigit(ch) || ch == '-' || ch == '_'
}

// isBreakSym return true if the object is either a soft or a hard break symbol.
func isBreakSym(obj sx.Object) bool {
	return zsx.SymSoft.IsEqual(obj) || zsx.SymHard.IsEqual(obj)
}

Changes to sz/zmk/zmk_fuzz_test.go.

12
13
14
15
16
17
18
19
20


21
22
23

24
25
26

27

28
29
12
13
14
15
16
17
18


19
20
21
22
23
24
25
26
27
28

29
30
31







-
-
+
+



+



+
-
+


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

package zmk_test

import (
	"testing"

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

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

Changes to sz/zmk/zmk_test.go.

16
17
18
19
20
21
22
23
24
25



26
27
28
29
30
31
32
16
17
18
19
20
21
22



23
24
25
26
27
28
29
30
31
32







-
-
-
+
+
+








import (
	"fmt"
	"strings"
	"testing"

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

type TestCase struct{ source, want string }
type TestCases []TestCase
type symbolMap map[string]*sx.Symbol

func replace(s string, sm symbolMap, tcs TestCases) TestCases {
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
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







+




+
-
-
+
+












+
+
+
+
+
+
+







	}
	return testCases
}

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

	var parser zmk.Parser
	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))
			parser.Initialize(inp)
			ast := zmk.Parse(inp)
			sz.Walk(astWalker{}, ast, nil)
			ast := parser.Parse()
			zsx.Walk(astWalker{}, ast, nil)
			got := ast.String()
			if tc.want != got {
				st.Errorf("\nwant=%q\n got=%q", tc.want, got)
			}
		})
	}
}

type astWalker struct{}

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

func TestEdges(t *testing.T) {
	t.Parallel()
	checkTcs(t, TestCases{
		{"\"\"\"\n; \n0{{0}}{0}\n\"\"\"", "(BLOCK (REGION-VERSE () ((DESCRIPTION () ()) (PARA (TEXT \"0\") (EMBED ((\"0\" . \"\")) (HOSTED \"0\") \"\")))))"},
	})
}

func TestEOL(t *testing.T) {
	t.Parallel()
	checkTcs(t, TestCases{
		{"", "()"},
		{"\n", "()"},
		{"\r", "()"},
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
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







-
+


-
-
+
+



-
-
-
+
+
+


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

















-
+

-
+



-
-
+
+

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


-
+







		{"[[|", "(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 (LINK () (HOSTED \"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 (LINK () (HOSTED \"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|a]]", "(BLOCK (PARA (LINK () (HOSTED \"a\") (TEXT \"b\"))))"},
		{"[[b| a]]", "(BLOCK (PARA (LINK () (HOSTED \"a\") (TEXT \"b\"))))"},
		{"[[b%c|a]]", "(BLOCK (PARA (LINK () (HOSTED \"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\"))))"},
		{"[[b\nc|a]]", "(BLOCK (PARA (LINK () (HOSTED \"a\") (TEXT \"b\") (SOFT) (TEXT \"c\"))))"},
		{"[[b c|a#n]]", "(BLOCK (PARA (LINK () (HOSTED \"a#n\") (TEXT \"b c\"))))"},
		{"[[a]]go", "(BLOCK (PARA (LINK () (HOSTED \"a\")) (TEXT \"go\")))"},
		{"[[b|a]]{go}", "(BLOCK (PARA (LINK ((\"go\" . \"\")) (HOSTED \"a\") (TEXT \"b\"))))"},
		{"[[[[a]]|b]]", "(BLOCK (PARA (TEXT \"[[\") (LINK () (HOSTED \"a\")) (TEXT \"|b]]\")))"},
		{"[[a[b]c|d]]", "(BLOCK (PARA (LINK () (HOSTED \"d\") (TEXT \"a[b]c\"))))"},
		{"[[[b]c|d]]", "(BLOCK (PARA (TEXT \"[\") (LINK () (HOSTED \"d\") (TEXT \"b]c\"))))"},
		{"[[a[]c|d]]", "(BLOCK (PARA (LINK () (HOSTED \"d\") (TEXT \"a[]c\"))))"},
		{"[[a[b]|d]]", "(BLOCK (PARA (LINK () (HOSTED \"d\") (TEXT \"a[b]\"))))"},
		{"[[\\|]]", "(BLOCK (PARA (LINK () (INVALID \"\\\\|\"))))"},
		{"[[\\||a]]", "(BLOCK (PARA (LINK () (HOSTED \"a\") (TEXT \"|\"))))"},
		{"[[b\\||a]]", "(BLOCK (PARA (LINK () (HOSTED \"a\") (TEXT \"b|\"))))"},
		{"[[b\\|c|a]]", "(BLOCK (PARA (LINK () (HOSTED \"a\") (TEXT \"b|c\"))))"},
		{"[[\\]]]", "(BLOCK (PARA (LINK () (INVALID \"\\\\]\"))))"},
		{"[[\\]|a]]", "(BLOCK (PARA (LINK () (HOSTED \"a\") (TEXT \"]\"))))"},
		{"[[b\\]|a]]", "(BLOCK (PARA (LINK () (HOSTED \"a\") (TEXT \"b]\"))))"},
		{"[[\\]\\||a]]", "(BLOCK (PARA (LINK () (HOSTED \"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 () (HOSTED \"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, 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\") \"\")))"},
		{"{{a}}", "(BLOCK (PARA (EMBED () (HOSTED \"a\") \"\")))"},
		{"{{12345678901234}}", "(BLOCK (PARA (EMBED () (ZETTEL \"12345678901234\") \"\")))"},
		{"{{ a}}", "(BLOCK (PARA (EMBED () (EXTERNAL \"a\") \"\")))"},
		{"{{ a}}", "(BLOCK (PARA (EMBED () (HOSTED \"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 (EMBED () (HOSTED \"a\") \"\" (TEXT \"b\"))))"},
		{"{{b| a}}", "(BLOCK (PARA (EMBED () (HOSTED \"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 \"}|\"))))"},
		{"{{b\nc|a}}", "(BLOCK (PARA (EMBED () (HOSTED \"a\") \"\" (TEXT \"b\") (SOFT) (TEXT \"c\"))))"},
		{"{{b c|a#n}}", "(BLOCK (PARA (EMBED () (HOSTED \"a#n\") \"\" (TEXT \"b c\"))))"},
		{"{{a}}{go}", "(BLOCK (PARA (EMBED ((\"go\" . \"\")) (HOSTED \"a\") \"\")))"},
		{"{{{{a}}|b}}", "(BLOCK (PARA (TEXT \"{{\") (EMBED () (HOSTED \"a\") \"\") (TEXT \"|b}}\")))"},
		{"{{\\|}}", "(BLOCK (PARA (EMBED () (INVALID \"\\\\|\") \"\")))"},
		{"{{\\||a}}", "(BLOCK (PARA (EMBED () (HOSTED \"a\") \"\" (TEXT \"|\"))))"},
		{"{{b\\||a}}", "(BLOCK (PARA (EMBED () (HOSTED \"a\") \"\" (TEXT \"b|\"))))"},
		{"{{b\\|c|a}}", "(BLOCK (PARA (EMBED () (HOSTED \"a\") \"\" (TEXT \"b|c\"))))"},
		{"{{\\}}}", "(BLOCK (PARA (EMBED () (INVALID \"\\\\}\") \"\")))"},
		{"{{\\}|a}}", "(BLOCK (PARA (EMBED () (HOSTED \"a\") \"\" (TEXT \"}\"))))"},
		{"{{b\\}|a}}", "(BLOCK (PARA (EMBED () (HOSTED \"a\") \"\" (TEXT \"b}\"))))"},
		{"{{\\}\\||a}}", "(BLOCK (PARA (EMBED () (HOSTED \"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 \"}}\")))"},
		{"{{{{a}}}}", "(BLOCK (PARA (TEXT \"{{\") (EMBED () (HOSTED \"a\") \"\") (TEXT \"}}\")))"},
	})
}

func TestCite(t *testing.T) {
	t.Parallel()
	checkTcs(t, TestCases{
		{"[@", "(BLOCK (PARA (TEXT \"[@\")))"},
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323








324
325
326
327
328
329
330
318
319
320
321
322
323
324








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







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







		{"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,
		"_": zsx.SymFormatEmph,
		"*": zsx.SymFormatStrong,
		">": zsx.SymFormatInsert,
		"~": zsx.SymFormatDelete,
		"^": zsx.SymFormatSuper,
		",": zsx.SymFormatSub,
		"#": zsx.SymFormatMark,
		":": zsx.SymFormatSpan,
	}
	t.Parallel()
	// Not for Insert / '>', because collision with quoted list
	// Not for Quote / '"', because escaped representation.
	for _, ch := range []string{"_", "*", "~", "^", ",", "#", ":"} {
		checkTcs(t, replace(ch, symMap, TestCases{
			{"$", "(BLOCK (PARA (TEXT \"$\")))"},
350
351
352
353
354
355
356
357

358
359
360
361
362
363
364
359
360
361
362
363
364
365

366
367
368
369
370
371
372
373







-
+







			{"$$a\n\na$$", "(BLOCK (PARA (TEXT \"$$a\")) (PARA (TEXT \"a$$\")))"},
			{"$$a$${go}", "(BLOCK (PARA ($% ((\"go\" . \"\")) (TEXT \"a\"))))"},
		}))
		checkTcs(t, replace(ch, symMap, TestCases{
			{"$$a\n\na$$", "(BLOCK (PARA (TEXT \"$$a\")) (PARA (TEXT \"a$$\")))"},
		}))
	}
	checkTcs(t, replace(`"`, symbolMap{`"`: sz.SymFormatQuote}, TestCases{
	checkTcs(t, replace(`"`, symbolMap{`"`: zsx.SymFormatQuote}, TestCases{
		{"$", "(BLOCK (PARA (TEXT \"\\\"\")))"},
		{"$$", "(BLOCK (PARA (TEXT \"\\\"\\\"\")))"},
		{"$$$", "(BLOCK (PARA (TEXT \"\\\"\\\"\\\"\")))"},
		{"$$$$", "(BLOCK (PARA ($% ())))"},

		{"$$a$$", "(BLOCK (PARA ($% () (TEXT \"a\"))))"},
		{"$$a$$$", "(BLOCK (PARA ($% () (TEXT \"a\")) (TEXT \"\\\"\")))"},
379
380
381
382
383
384
385
386
387
388



389
390
391
392
393
394
395
388
389
390
391
392
393
394



395
396
397
398
399
400
401
402
403
404







-
-
-
+
+
+







		{"__**a**__", "(BLOCK (PARA (FORMAT-EMPH () (FORMAT-STRONG () (TEXT \"a\")))))"},
		{"__**__**", "(BLOCK (PARA (TEXT \"__\") (FORMAT-STRONG () (TEXT \"__\"))))"},
	})
}

func TestLiteral(t *testing.T) {
	symMap := symbolMap{
		"`": sz.SymLiteralCode,
		"'": sz.SymLiteralInput,
		"=": sz.SymLiteralOutput,
		"`": zsx.SymLiteralCode,
		"'": zsx.SymLiteralInput,
		"=": zsx.SymLiteralOutput,
	}
	t.Parallel()
	for _, ch := range []string{"`", "'", "="} {
		checkTcs(t, replace(ch, symMap, TestCases{
			{"$", "(BLOCK (PARA (TEXT \"$\")))"},
			{"$$", "(BLOCK (PARA (TEXT \"$$\")))"},
			{"$$$", "(BLOCK (PARA (TEXT \"$$$\")))"},
608
609
610
611
612
613
614
615
616
617
618
619





620
621
622
623
624
625
626
617
618
619
620
621
622
623





624
625
626
627
628
629
630
631
632
633
634
635







-
-
-
-
-
+
+
+
+
+







		{"=", "(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 () (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 \") (EMBED () (HOSTED \"a\") \"\")))"},
		{"=== h{{a}}", "(BLOCK (HEADING 1 () \"\" \"\" (TEXT \"h\") (EMBED () (HOSTED \"a\") \"\")))"},
		{"=== {{a}}", "(BLOCK (HEADING 1 () \"\" \"\" (EMBED () (HOSTED \"a\") \"\")))"},
		{"=== h {{a}}{-}", "(BLOCK (HEADING 1 () \"\" \"\" (TEXT \"h \") (EMBED ((\"-\" . \"\")) (HOSTED \"a\") \"\")))"},
		{"=== h {{a}} {-}", "(BLOCK (HEADING 1 ((\"-\" . \"\")) \"\" \"\" (TEXT \"h \") (EMBED () (HOSTED \"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) {
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
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
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
764
765
766
767
768
769
770
771
772
773







774
775
776
777
778
779
780
659
660
661
662
663
664
665


















666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685

686
687
688

689
690
691

692
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
752
753
754








755
756
757
758
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







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


-
+


-
+


-
+


-
+


-
+






-
-
-
-
-
+
+
+
+
+






-
+









-
-
-
-
-
+
+
+
+
+



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


-
+

-
+







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

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






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







			{"$$$", "(BLOCK (PARA (TEXT \"$$$\")))"},
			{"$ ", "(BLOCK (PARA (TEXT \"$\")))"},
			{"$$ ", "(BLOCK (PARA (TEXT \"$$\")))"},
			{"$$$ ", "(BLOCK (PARA (TEXT \"$$$\")))"},
		}))
	}
	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\")))"},
		{"* abc\n* def", "(BLOCK (UNORDERED (BLOCK (PARA (TEXT \"abc\"))) (BLOCK (PARA (TEXT \"def\")))))"},
		{"* abc\n  def", "(BLOCK (UNORDERED (BLOCK (PARA (TEXT \"abc\") (SOFT) (TEXT \"def\")))))"},
		{"* abc\n   def", "(BLOCK (UNORDERED (BLOCK (PARA (TEXT \"abc\") (SOFT) (TEXT \"def\")))))"},
		{"* abc\n\ndef", "(BLOCK (UNORDERED (BLOCK (PARA (TEXT \"abc\")))) (PARA (TEXT \"def\")))"},
		{"* abc\n\n def", "(BLOCK (UNORDERED (BLOCK (PARA (TEXT \"abc\")))) (PARA (TEXT \"def\")))"},
		{"* abc\n\n  def", "(BLOCK (UNORDERED (BLOCK (PARA (TEXT \"abc\")) (PARA (TEXT \"def\")))))"},
		{"* abc\n\n   def", "(BLOCK (UNORDERED (BLOCK (PARA (TEXT \"abc\")) (PARA (TEXT \"def\")))))"},
		{"* abc\n** def", "(BLOCK (UNORDERED (BLOCK (PARA (TEXT \"abc\")) (UNORDERED (BLOCK (PARA (TEXT \"def\")))))))"},
		{"* abc\n** def\n* ghi", "(BLOCK (UNORDERED (BLOCK (PARA (TEXT \"abc\")) (UNORDERED (BLOCK (PARA (TEXT \"def\"))))) (BLOCK (PARA (TEXT \"ghi\")))))"},
		{"* abc\n\n  def\n* ghi", "(BLOCK (UNORDERED (BLOCK (PARA (TEXT \"abc\")) (PARA (TEXT \"def\"))) (BLOCK (PARA (TEXT \"ghi\")))))"},
		{"* abc\n** def\n   ghi\n  jkl", "(BLOCK (UNORDERED (BLOCK (PARA (TEXT \"abc\")) (UNORDERED (BLOCK (PARA (TEXT \"def\") (SOFT) (TEXT \"ghi\")))) (PARA (TEXT \"jkl\")))))"},
		{"* 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\")))"},
		{"* abc\n* def", "(BLOCK (UNORDERED () (BLOCK (PARA (TEXT \"abc\"))) (BLOCK (PARA (TEXT \"def\")))))"},
		{"* abc\n  def", "(BLOCK (UNORDERED () (BLOCK (PARA (TEXT \"abc\") (SOFT) (TEXT \"def\")))))"},
		{"* abc\n   def", "(BLOCK (UNORDERED () (BLOCK (PARA (TEXT \"abc\") (SOFT) (TEXT \"def\")))))"},
		{"* abc\n\ndef", "(BLOCK (UNORDERED () (BLOCK (PARA (TEXT \"abc\")))) (PARA (TEXT \"def\")))"},
		{"* abc\n\n def", "(BLOCK (UNORDERED () (BLOCK (PARA (TEXT \"abc\")))) (PARA (TEXT \"def\")))"},
		{"* abc\n\n  def", "(BLOCK (UNORDERED () (BLOCK (PARA (TEXT \"abc\")) (PARA (TEXT \"def\")))))"},
		{"* abc\n\n   def", "(BLOCK (UNORDERED () (BLOCK (PARA (TEXT \"abc\")) (PARA (TEXT \"def\")))))"},
		{"* abc\n** def", "(BLOCK (UNORDERED () (BLOCK (PARA (TEXT \"abc\")) (UNORDERED () (BLOCK (PARA (TEXT \"def\")))))))"},
		{"* abc\n** def\n* ghi", "(BLOCK (UNORDERED () (BLOCK (PARA (TEXT \"abc\")) (UNORDERED () (BLOCK (PARA (TEXT \"def\"))))) (BLOCK (PARA (TEXT \"ghi\")))))"},
		{"* abc\n\n  def\n* ghi", "(BLOCK (UNORDERED () (BLOCK (PARA (TEXT \"abc\")) (PARA (TEXT \"def\"))) (BLOCK (PARA (TEXT \"ghi\")))))"},
		{"* abc\n** def\n   ghi\n  jkl", "(BLOCK (UNORDERED () (BLOCK (PARA (TEXT \"abc\")) (UNORDERED () (BLOCK (PARA (TEXT \"def\") (SOFT) (TEXT \"ghi\")))) (PARA (TEXT \"jkl\")))))"},

		// A list does not last beyond a region
		{":::\n# abc\n:::\n# def", "(BLOCK (REGION-BLOCK () ((ORDERED (BLOCK (PARA (TEXT \"abc\")))))) (ORDERED (BLOCK (PARA (TEXT \"def\")))))"},
		{":::\n# abc\n:::\n# def", "(BLOCK (REGION-BLOCK () ((ORDERED () (BLOCK (PARA (TEXT \"abc\")))))) (ORDERED () (BLOCK (PARA (TEXT \"def\")))))"},

		// A HRule creates a new list
		{"* abc\n---\n* def", "(BLOCK (UNORDERED (BLOCK (PARA (TEXT \"abc\")))) (THEMATIC ()) (UNORDERED (BLOCK (PARA (TEXT \"def\")))))"},
		{"* abc\n---\n* def", "(BLOCK (UNORDERED () (BLOCK (PARA (TEXT \"abc\")))) (THEMATIC ()) (UNORDERED () (BLOCK (PARA (TEXT \"def\")))))"},

		// Changing list type adds a new list
		{"* abc\n# def", "(BLOCK (UNORDERED (BLOCK (PARA (TEXT \"abc\")))) (ORDERED (BLOCK (PARA (TEXT \"def\")))))"},
		{"* abc\n# def", "(BLOCK (UNORDERED () (BLOCK (PARA (TEXT \"abc\")))) (ORDERED () (BLOCK (PARA (TEXT \"def\")))))"},

		// Quotation lists may have empty items
		{">", "(BLOCK (QUOTATION (BLOCK)))"},
		{">", "(BLOCK (QUOTATION () (BLOCK)))"},

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

func TestQuoteList(t *testing.T) {
	t.Parallel()
	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\")))))"},
		{"> 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, TestCases{
		{"abc\n* def", "(BLOCK (PARA (TEXT \"abc\")) (UNORDERED (BLOCK (PARA (TEXT \"def\")))))"},
		{"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, 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\"))))"},
		{"; 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\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\"))))))"},
		{"; 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\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 \":\")))"},
		{"; 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\"))))))"},
		{"; abc\n: def\n  ", "(BLOCK (DESCRIPTION () ((TEXT \"abc\")) (BLOCK (BLOCK (PARA (TEXT \"def\"))))))"},
	})
}

func TestTable(t *testing.T) {
	t.Parallel()
	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\")))))"},
		{"|a\n|b", "(BLOCK (TABLE () ((CELL (TEXT \"a\"))) ((CELL (TEXT \"b\")))))"},
		{"|a|b\n|c|d", "(BLOCK (TABLE () ((CELL (TEXT \"a\")) (CELL (TEXT \"b\"))) ((CELL (TEXT \"c\")) (CELL (TEXT \"d\")))))"},
		{"||", "(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\")))))"},
		{"|a\n|b", "(BLOCK (TABLE () ((CELL () (TEXT \"a\"))) ((CELL () (TEXT \"b\")))))"},
		{"|a|b\n|c|d", "(BLOCK (TABLE () ((CELL () (TEXT \"a\")) (CELL () (TEXT \"b\"))) ((CELL () (TEXT \"c\")) (CELL () (TEXT \"d\")))))"},
		{"|%", "()"},
		{"|=a", "(BLOCK (TABLE ((CELL (TEXT \"a\")))))"},
		{"|=a\n|b", "(BLOCK (TABLE ((CELL (TEXT \"a\"))) ((CELL (TEXT \"b\")))))"},
		{"|a|b\n|%---\n|c|d", "(BLOCK (TABLE () ((CELL (TEXT \"a\")) (CELL (TEXT \"b\"))) ((CELL (TEXT \"c\")) (CELL (TEXT \"d\")))))"},
		{"|a|b\n|c", "(BLOCK (TABLE () ((CELL (TEXT \"a\")) (CELL (TEXT \"b\"))) ((CELL (TEXT \"c\")) (CELL))))"},
		{"|=<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))))"},
		{"|=a", "(BLOCK (TABLE ((CELL () (TEXT \"a\")))))"},
		{"|=a\n|b", "(BLOCK (TABLE ((CELL () (TEXT \"a\"))) ((CELL () (TEXT \"b\")))))"},
		{"|a|b\n|%---\n|c|d", "(BLOCK (TABLE () ((CELL () (TEXT \"a\")) (CELL () (TEXT \"b\"))) ((CELL () (TEXT \"c\")) (CELL () (TEXT \"d\")))))"},
		{"|a|b\n|c", "(BLOCK (TABLE () ((CELL () (TEXT \"a\")) (CELL () (TEXT \"b\"))) ((CELL () (TEXT \"c\")) (CELL ()))))"},
		{"|=<a>\n|b|c", "(BLOCK (TABLE ((CELL ((align . \"left\")) (TEXT \"a\")) (CELL ())) ((CELL ((align . \"right\")) (TEXT \"b\")) (CELL () (TEXT \"c\")))))"},
		{"|=<a|=b>\n||", "(BLOCK (TABLE ((CELL ((align . \"left\")) (TEXT \"a\")) (CELL ((align . \"right\")) (TEXT \"b\"))) ((CELL ()) (CELL ((align . \"right\"))))))"},
	})
}

func TestTransclude(t *testing.T) {
	t.Parallel()
	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 () (EXTERNAL \"a\") \"\")))"},
		{"{{{a}}}{go=b}", "(BLOCK (TRANSCLUDE ((\"go\" . \"b\")) (EXTERNAL \"a\")))"},
		{"{{{a}}}", "(BLOCK (TRANSCLUDE () (HOSTED \"a\")))"},
		{"{{{a}}}b", "(BLOCK (TRANSCLUDE ((\"\" . \"b\")) (HOSTED \"a\")))"},
		{"{{{a}}}}", "(BLOCK (TRANSCLUDE () (HOSTED \"a\")))"},
		{"{{{a\\}}}}", "(BLOCK (TRANSCLUDE () (INVALID \"a\\\\}\")))"},
		{"{{{a\\}}}}b", "(BLOCK (TRANSCLUDE ((\"\" . \"b\")) (INVALID \"a\\\\}\")))"},
		{"{{{a}}", "(BLOCK (PARA (TEXT \"{\") (EMBED () (HOSTED \"a\") \"\")))"},
		{"{{{a}}}{go=b}", "(BLOCK (TRANSCLUDE ((\"go\" . \"b\")) (HOSTED \"a\")))"},
	})
}

func TestBlockAttr(t *testing.T) {
	t.Parallel()
	checkTcs(t, TestCases{
		{":::go\na\n:::", "(BLOCK (REGION-BLOCK ((\"\" . \"go\")) ((PARA (TEXT \"a\")))))"},

Changes to text/text.go.

14
15
16
17
18
19
20
21
22


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


21
22
23
24
25
26
27
28
29







-
-
+
+







// Package text provides types, constants and function to work with text output.
package text

import (
	"strings"

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

// Encoder is the structure to hold relevant data to execute the encoding.
type Encoder struct {
	sb strings.Builder
}

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







-
+


















-
+

-
+





	if !isPair {
		return
	}
	sym := cmd.Car()
	if sx.IsNil(sym) {
		return
	}
	if sym.IsEqual(sz.SymText) {
	if sym.IsEqual(zsx.SymText) {
		args := cmd.Tail()
		if args == nil {
			return
		}
		if val, isString := sx.GetString(args.Car()); isString {
			hadSpace := false
			for _, ch := range val.GetValue() {
				if input.IsSpace(ch) {
					if !hadSpace {
						enc.sb.WriteByte(' ')
						hadSpace = true
					}
				} else {
					enc.sb.WriteRune(ch)
					hadSpace = false
				}
			}
		}
	} else if sym.IsEqual(sz.SymSoft) {
	} else if sym.IsEqual(zsx.SymSoft) {
		enc.sb.WriteByte(' ')
	} else if sym.IsEqual(sz.SymHard) {
	} else if sym.IsEqual(zsx.SymHard) {
		enc.sb.WriteByte('\n')
	} else if !sym.IsEqual(sx.SymbolQuote) {
		enc.executeList(cmd.Tail())
	}
}

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
16
17
18
19
20
21
22


+
+
+
+
+
+
+
+
+
+
+

-
+







<title>Change Log</title>

<a name="0_22"></a>
<h2>Changes for Version 0.22.0 (pending)</h2>

<a name="0_21"></a>
<h2>Changes for Version 0.21.0 (2025-04-17)</h2>
  *  Sz encoding changed (LINK-* -> LINK, lists, descriptions, block BLOBs,
     tables and its cells got attributes; cell attributes now defines cell
     alignment).
  *  Add API call to retrieve external references from a zettel.
  *  Move some code to package t73f.de/r/zsx

<a name="0_20"></a>
<h2>Changes for Version 0.20.0 (pending)</h2>
<h2>Changes for Version 0.20.0 (2025-03-07)</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>

Changes to www/index.wiki.

1

2
3
4
5
6
7
8
9
10
11






12
13
14
15
16
17
18

1
2
3
4
5






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




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







<title>Home</title>
<title>ZSC</title>

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

<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]
<h3>Latest Release: 0.21.0 (2025-03-07)</h3>
  *  [./changes.wiki#0_21|Change summary]
  *  [/timeline?p=v0.21.0&bt=v0.20.0&y=ci|Check-ins for version 0.21],
     [/vdiff?to=v0.21.0&from=v0.20.0|content diff]
  *  [/timeline?df=v0.21.0&y=ci|Check-ins derived from the 0.21 release],
     [/vdiff?from=v0.21.0&to=trunk|content diff]
  *  [/timeline?t=release|Timeline of all past releases]

<h2>Usage instructions</h2>

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.