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
	"net/http"

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

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

// QueryZettel returns a list of all Zettel based on the given query.
//
// query is a search expression, as described in [Query the list of all zettel].







|







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/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
		return "", "", nil, err
	}
	hVals, err := sexp.ParseList(vals[2], "ys")
	if err != nil {
		return "", "", nil, err
	}
	metaList, err := parseMetaList(vals[3].(*sx.Pair))
	return sz.GoValue(qVals[1]), sz.GoValue(hVals[1]), metaList, err
}

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







|







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

	return api.MetaRights{
		Meta:   meta,
		Rights: rights,
	}, 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
	}







>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>







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
	// System zettel
	ZidVersion              = Zid(1)
	ZidHost                 = Zid(2)
	ZidOperatingSystem      = Zid(3)
	ZidLicense              = Zid(4)
	ZidAuthors              = Zid(5)
	ZidDependencies         = Zid(6)

	ZidLog                  = Zid(7)
	ZidMemory               = Zid(8)
	ZidSx                   = Zid(9)
	ZidHTTP                 = Zid(10)
	ZidAPI                  = Zid(11)
	ZidWebUI                = Zid(12)
	ZidConsole              = Zid(13)
	ZidBoxManager           = Zid(20)
	ZidZettel               = Zid(21)
	ZidIndex                = Zid(22)
	ZidQuery                = Zid(23)
	ZidMetadataKey          = Zid(90)
	ZidParser               = Zid(92)
	ZidStartupConfiguration = Zid(96)







>
|
|
|
|
|
|
|







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(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
import (
	"iter"
	"slices"
	"strings"

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

// NewFromInput parses the meta data of a zettel.
func NewFromInput(zid id.Zid, inp *input.Input) *Meta {
	if inp.Ch == '-' && inp.PeekN(0) == '-' && inp.PeekN(1) == '-' {
		skipToEOL(inp)
		inp.EatEOL()







|







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/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
import (
	"iter"
	"slices"
	"strings"
	"testing"

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

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

func TestEmpty(t *testing.T) {







|







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/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
	"iter"
	"slices"
	"strings"
	"time"

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

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

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







|







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/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
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/zero v0.0.0-20250226205915-c4194684acb7

)





|
|
|

>

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


t73f.de/r/sx v0.0.0-20250226205800-c12af029b6d3 h1:Jek4x1Qp59SWXI1enWVTeP1wxcVO96FuBpJBnnwOY98=
t73f.de/r/sx v0.0.0-20250226205800-c12af029b6d3/go.mod h1:hzg05uSCMk3D/DWaL0pdlowfL2aWQeGIfD1S04vV+Xg=
t73f.de/r/sxwebs v0.0.0-20250226210617-7bc3145c269b h1:X+9mMDd3fKML5SPcQk4n28oDGFUwqjDiSmQrH2LHZwI=
t73f.de/r/sxwebs v0.0.0-20250226210617-7bc3145c269b/go.mod h1:p+3JCSzNm9e+Yyub0ODRiLDeKaGVYWvBKYANZaAWYIA=
t73f.de/r/webs v0.0.0-20250226210341-4a531b8bfb18 h1:p7rOFBzP6FE/aYN5MUfmGDrKP1H1IFs6v19T7hm7rXI=
t73f.de/r/webs v0.0.0-20250226210341-4a531b8bfb18/go.mod h1:zk92hSKB4iWyT290+163seNzu350TA9XLATC9kOldqo=
t73f.de/r/zero v0.0.0-20250226205915-c4194684acb7 h1:OuzHSfniY8UzLmo5zp1w23Kd9h7x9CSXP2jQ+kppeqU=
t73f.de/r/zero v0.0.0-20250226205915-c4194684acb7/go.mod h1:T1vFcHoymUQcr7+vENBkS1yryZRZ3YB8uRtnMy8yRBA=


|
|
|
|
|
|


>
>
1
2
3
4
5
6
7
8
9
10
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
	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")







<







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 (

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

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







|
|
|







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

// 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 {
	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) {
			plist = plist.Cons(sx.Cons(sx.MakeSymbol(key), sx.MakeString(a[key])))
		}
	}
	if plist == nil {
		return nil
	}
	return plist.Cons(sxhtml.SymAttr)







|
|







|







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 != "" }

// 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 != 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
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) {
	if value, ok := a.Get("lang"); ok {
		env.langStack.Push(value)
	} else {
		env.langStack.Dup()
	}
}








|







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







|







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(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
			sb.WriteByte(' ')
			sb.WriteString(getString(obj, env).GetValue())
		}
		s := sb.String()
		if len(s) > 0 {
			s = s[1:]
		}
		a := make(attrs.Attributes, 2).
			Set("name", getSymbol(args[0], env).GetValue()).
			Set("content", s)
		return ev.EvaluateMeta(a)
	}
	ev.bind(sz.SymTypeIDSet, 2, evalMetaSet)
	ev.bind(sz.SymTypeTagSet, 2, evalMetaSet)
}

// 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) bindBlocks() {
	ev.bind(sz.SymBlock, 0, ev.evalList)
	ev.bind(sz.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 {
		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))
			}
			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 {
		result := sx.Nil()
		if len(args) > 0 {
			if attrList := getList(args[0], env); attrList != nil {
				result = result.Cons(EvaluateAttrbute(sz.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 {
			return sx.Nil()
		}
		var items sx.ListBuilder

















		items.Add(symDL)



		for pos := 0; pos < len(args); pos++ {
			term := ev.evalDescriptionTerm(getList(args[pos], env), env)
			items.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))
			}
		}
		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 {
		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 {







|









|
|



|
|


|

















|





|



|





|
|
|
|


|
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
|
>
>
>
|

|










|
<
<
<
<
<
<
<
<
<
<
<
<
<





|







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

func (ev *Evaluator) bindBlocks() {
	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(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(EvaluateAttributes(a))
			}
			return result.Cons(headingSymbol)
		}
		return sx.MakeList(headingSymbol, sx.MakeString("<MISSING TEXT>"))
	})
	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(EvaluateAttributes(zsx.GetAttributes(attrList)))
			}
		}
		return result.Cons(SymHR)
	})

	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 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)
			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)
				result.Add(dditem.Cons(symDD))













			}
		}
		return result.List()
	})

	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
			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(sz.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 {
		return evalVerbatim(GetAttributes(args[0], env).AddClass("zs-eval"), getString(args[1], env))
	})
	ev.bind(sz.SymVerbatimHTML, 2, ev.evalHTML)
	ev.bind(sz.SymVerbatimMath, 2, func(args sx.Vector, env *Environment) sx.Object {
		return evalVerbatim(GetAttributes(args[0], env).AddClass("zs-math"), getString(args[1], env))
	})
	ev.bind(sz.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(sz.SymTransclude, 2, func(args sx.Vector, env *Environment) sx.Object {
		ref, isPair := sx.GetPair(args[1])
		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")
				// TODO: if len(args) > 2, add "alt" attr based on args[2:], as in SymEmbed
				return sx.Nil().Cons(sx.Nil().Cons(EvaluateAttrbute(a)).Cons(SymIMG)).Cons(SymP)
			}
			return sx.MakeList(
				sxhtml.SymInlineComment,
				sx.MakeString("transclude"),
				refKind,
				sx.MakeString("->"),
				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)




		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)

		}
		return result.List()
	}
}

func (ev *Evaluator) evalDescriptionTerm(term *sx.Pair, env *Environment) *sx.Pair {
	var result sx.ListBuilder







|
|
|
|
>
>
>
>
>
|
>
>
>
>
|
|
|

|









|


|
|


|







|
|
>
|

|
|
<
<
<
<
<
<
<
<
|
|

|




|

|










>
>
>
>
|
|
|
|
|
|
>







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(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(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(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(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(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(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(zsx.SymTransclude, 2, func(args sx.Vector, env *Environment) sx.Object {
		if refSym, refValue := GetReference(args[1], env); refSym != nil {








			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(EvaluateAttributes(a)).Cons(SymIMG)).Cons(SymP)
			}
			return sx.MakeList(
				sxhtml.SymInlineComment,
				sx.MakeString("transclude"),
				refSym,
				sx.MakeString("->"),
				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[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
	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))
		}
		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 {
	a = setProgLang(a)
	code := sx.Nil().Cons(s)
	if al := EvaluateAttrbute(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(sz.SymLinkInvalid, 2, func(args sx.Vector, env *Environment) sx.Object {
		a := GetAttributes(args[0], env)
		env.pushAttributes(a)
		defer env.popAttributes()

















		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))
		}
		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)
		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))
	})
	ev.bind(sz.SymEmbedBLOB, 3, func(args sx.Vector, env *Environment) sx.Object {
		a, syntax, data := GetAttributes(args[0], env), getString(args[1], env), getString(args[2], env)
		summary, hasSummary := a.Get(meta.KeySummary)
		if !hasSummary {
			summary = ""
		}
		return evalBLOB(

			sx.MakeList(sxhtml.SymListSplice, sx.MakeString(summary)),
			syntax,
			data,
		)
	})

	ev.bind(sz.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))
		}
		if result == nil {
			return nil
		}
		return result.Cons(SymSPAN)
	})
	ev.bind(sz.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)
			}
		}
		return result.Cons(SymSPAN)
	})
	ev.bind(sz.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 {
				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(sz.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 {
		return evalLiteral(args, nil, symKBD, env)
	})
	ev.bind(sz.SymLiteralMath, 2, func(args sx.Vector, env *Environment) sx.Object {
		a := GetAttributes(args[0], env).AddClass("zs-math")
		return evalLiteral(args, a, symCODE, env)
	})
	ev.bind(sz.SymLiteralOutput, 2, func(args sx.Vector, env *Environment) sx.Object {
		return evalLiteral(args, nil, symSAMP, env)
	})
	ev.bind(sz.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))
		}
		return res.Cons(sym)
	}
}

func (ev *Evaluator) evalQuote(args sx.Vector, env *Environment) sx.Object {
	a := GetAttributes(args[0], env)







<
<
<
<
<
<
<
<
<














|















|


|







|
|
|
|

|



>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>





|



<
<
<
<
<
<
|
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
|
|
|
<







|

|






>






|











|






|



|
|




|





|

















|
|
|
|
|
|
|
|
|

|









|


|



|


|














|







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) 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(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 zsx.Attributes, s sx.String) sx.Object {
	a = setProgLang(a)
	code := sx.Nil().Cons(s)
	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(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(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(sx.MakeString(refValue))
		}
		return inline.Cons(SymSPAN)
	})



































	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)

		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, EvaluateAttributes(a))
	})
	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(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(EvaluateAttributes(a))
		}
		if result == nil {
			return nil
		}
		return result.Cons(SymSPAN)
	})
	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 := zsx.Attributes{"id": fragment + ev.unique}
				return result.Cons(EvaluateAttributes(a)).Cons(SymA)
			}
		}
		return result.Cons(SymSPAN)
	})
	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 := 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(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(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(zsx.SymLiteralInput, 2, func(args sx.Vector, env *Environment) sx.Object {
		return evalLiteral(args, nil, symKBD, env)
	})
	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(zsx.SymLiteralOutput, 2, func(args sx.Vector, env *Environment) sx.Object {
		return evalLiteral(args, nil, symSAMP, env)
	})
	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(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
			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))
		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 {
	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))
	}
	return res.Cons(sym)
}
func setProgLang(a attrs.Attributes) attrs.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 {
	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())))
		var sb strings.Builder
		flattenText(&sb, description)
		if d := sb.String(); d != "" {
			imgAttr = imgAttr.Cons(sx.Cons(symAttrAlt, sx.MakeString(d)))
		}
		return sx.Nil().Cons(sx.Nil().Cons(imgAttr.Cons(sxhtml.SymAttr)).Cons(SymIMG)).Cons(SymP)
	}
}

func flattenText(sb *strings.Builder, lst *sx.Pair) {
	for elem := range lst.Values() {
		switch obj := elem.(type) {
		case sx.String:







|







|











|



|













|









|



|

|







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(EvaluateAttributes(a))
		return res.Cons(SymSPAN)
	}
	return res.Cons(sxhtml.SymListSplice)
}

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

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(EvaluateAttributes(a))
	}
	return res.Cons(sym)
}
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(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:
		a = a.Add("src", "data:image/"+syntax.GetValue()+";base64,"+data.GetValue())
		var sb strings.Builder
		flattenText(&sb, description)
		if d := sb.String(); d != "" {
			a = a.Add("alt", d)
		}
		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
	}
	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 {
	result := ev.evalSlice(inline, env)
	if len(inline) == 0 {
		result = sx.Nil().Cons(refValue)
	}
	if ev.noLinks {
		return result.Cons(SymSPAN)
	}
	return result.Cons(EvaluateAttrbute(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 {
		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{}
}
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)







|


|




|












|
<
<
|
|
|
|
>
|







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 zsx.Attributes, refValue string, inline sx.Vector, env *Environment) sx.Object {
	result := ev.evalSlice(inline, env)
	if len(inline) == 0 {
		result = sx.Nil().Cons(sx.MakeString(refValue))
	}
	if ev.noLinks {
		return result.Cons(SymSPAN)
	}
	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 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
	}
	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))














}

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








|
|
>
>
>
>
>
>
>
>
>
>
>
>
>
>







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

package sz

import "t73f.de/r/sx"

// Various constants for Zettel data. They are technically variables.


var (
	// Symbols for Metanodes
	SymBlock  = sx.MakeSymbol("BLOCK")
	SymInline = sx.MakeSymbol("INLINE")
	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")
	SymRefStateSelf     = sx.MakeSymbol("SELF")
	SymRefStateFound    = sx.MakeSymbol("FOUND")
	SymRefStateBroken   = sx.MakeSymbol("BROKEN")
	SymRefStateHosted   = sx.MakeSymbol("HOSTED")
	SymRefStateBased    = sx.MakeSymbol("BASED")
	SymRefStateQuery    = sx.MakeSymbol("QUERY")
	SymRefStateExternal = sx.MakeSymbol("EXTERNAL")

	// Symbols for metadata types.
	SymTypeCredential = sx.MakeSymbol("CREDENTIAL")
	SymTypeEmpty      = sx.MakeSymbol("EMPTY-STRING")
	SymTypeID         = sx.MakeSymbol("ZID")
	SymTypeIDSet      = sx.MakeSymbol("ZID-SET")
	SymTypeNumber     = sx.MakeSymbol("NUMBER")







>
>


<
<
|

<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<

<
|
<
|
|
<
|
|
<







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


	SymMeta = sx.MakeSymbol("META")


























































	// Constant symbols for reference states.

	SymRefStateZettel = sx.MakeSymbol("ZETTEL")

	SymRefStateFound  = sx.MakeSymbol("FOUND")
	SymRefStateBroken = sx.MakeSymbol("BROKEN")

	SymRefStateBased  = sx.MakeSymbol("BASED")
	SymRefStateQuery  = sx.MakeSymbol("QUERY")


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

package sz

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

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

// --- Contains some simple parsers

// ---- Syntax: none

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

// ---- 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
	} else {
		sym = SymVerbatimCode
	}
	return sx.MakeList(
		sym,
		sx.MakeList(sx.Cons(sx.MakeString(""), sx.MakeString(syntax))),
		sx.MakeString(string(inp.ScanLineContent())),
	)
}







>
|















|

|







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/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 = zsx.SymVerbatimHTML
	} else {
		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
//-----------------------------------------------------------------------------

package sz_test

import (
	"testing"

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

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








|
|







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

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

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

// GetAttributes traverses a s-expression list and returns an attribute structure.
func GetAttributes(seq *sx.Pair) (result attrs.Attributes) {
	for 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 {







|

<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<







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/zsx"
)



































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

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







|














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

























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

package zmk

import (
	"fmt"

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

// parseBlock parses one block.














func (cp *zmkP) parseBlock(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







|
|



>
>
>
>
>
>
>
>
>
>
>
>
>
>
|







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/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 *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
			bn, success = nil, cp.parseIndent()
		case '|':
			cp.lists = nil
			cp.descrl = nil
			bn, success = cp.parseRow(), true
		case '{':
			cp.clearStacked()
			bn, success = parseTransclusion(inp)
		}

		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
}

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 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() {
	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 {
			lastPair.Head().LastPair().AppendBang(sx.Cons(symSeparator, nil))
		}
	}
}

// parseColon determines which element should be parsed.
func (cp *zmkP) 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 {
	var lb sx.ListBuilder
	for {
		in := cp.parseInline()
		if in == nil {
			return lb.List()
		}
		lb.Add(in)
		if sz.IsBreakSym(in.Car()) {
			ch := cp.inp.Ch
			switch ch {
			// Must contain all cases from above switch in parseBlock.
			case input.EOS, '\n', '\r', '@', '`', runeModGrave, '%', '~', '$', '"', '<', '=', '-', '*', '#', '>', ';', ':', ' ', '|', '{':
				return lb.List()
			}
		}







|
















|



















|
















|




|






|









|







|







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 = 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 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(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 *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 > 2 && pos%2 != 0 {
			lastPair.Head().LastPair().AppendBang(sx.Cons(symSeparator, nil))
		}
	}
}

// parseColon determines which element should be parsed.
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 *Parser) parsePara() *sx.Pair {
	var lb sx.ListBuilder
	for {
		in := cp.parseInline()
		if in == nil {
			return lb.List()
		}
		lb.Add(in)
		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
	inp.SkipToEOL()
	if inp.Ch == input.EOS {
		return nil, false
	}
	var sym *sx.Symbol
	switch fch {
	case '@':
		sym = sz.SymVerbatimZettel
	case '`', runeModGrave:
		sym = sz.SymVerbatimCode
	case '%':
		sym = sz.SymVerbatimComment
	case '~':
		sym = sz.SymVerbatimEval
	case '$':
		sym = sz.SymVerbatimMath
	default:
		panic(fmt.Sprintf("%q is not a verbatim char", fch))
	}
	content := make([]byte, 0, 512)
	for {
		inp.EatEOL()
		posL := inp.Pos
		switch inp.Ch {
		case fch:
			if countDelim(inp, fch) >= cnt {
				inp.SkipToEOL()
				return sz.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) {
	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
	case '<':
		sym = sz.SymRegionQuote
	case '"':
		sym = sz.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
			}
			inp.SetPos(posL)
		case input.EOS:
			return nil, false
		}
		bn, cont := cp.parseBlock(lastPara)
		if bn != nil {
			blocksBuilder.Add(bn)
		}
		if !cont {
			lastPara = bn
		}
	}
}

// parseRegionLastLine parses the last line of a region and returns its inline text.
func (cp *zmkP) 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) {
	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
		}
		in := cp.parseInline()
		if in == nil {
			return sz.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
		}
	}
}

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

// parseNestedList parses a list.
func (cp *zmkP) 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) {
		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()
	if pv != nil {
		bn.AppendBang(sz.MakePara(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
		case '#':
			sym = sz.SymListOrdered
		case '>':
			sym = sz.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) {
	for i, kind := range kinds {
		if i < len(cp.lists) {
			if !cp.lists[i].Car().IsEqual(kind) {
				ln = sx.Cons(kind, nil)
				newLnCount++
				cp.lists[i] = ln
				cp.lists = cp.lists[:i+1]
			} else {
				ln = cp.lists[i]
			}
		} else {
			ln = sx.Cons(kind, nil)
			newLnCount++
			cp.lists = append(cp.lists, ln)
		}
	}
	return ln, newLnCount
}

func (cp *zmkP) 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 {
			// 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]))
		}
		childPos--
		parentPos--
	}
	return nil, true
}

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

// parseDefDescr parses a description of a definition list.
func (cp *zmkP) parseDefDescr() (res *sx.Pair, success bool) {
	inp := cp.inp
	if inp.Next() != ' ' {
		return nil, false
	}
	inp.Next()
	inp.SkipSpace()
	descrl := cp.descrl
	lastPair, 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 {
		// Just a term, but no definitions
		lastPair.AppendBang(sz.MakeBlock(newDef))
	} else {
		// lastPara points a the last definition
		lastPair.Head().LastPair().AppendBang(newDef)
	}
	return nil, true
}








|

|

|

|

|











|














|










|

|

|

















|





<
<
<
|
<
|
<




|


















|


















|



|





|












|



|






|








|

|












|

|

|














|



|







|







|








|





|








|








|







|






|






|






|






|


















|
|

|







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 = zsx.SymVerbatimZettel
	case '`', runeModGrave:
		sym = zsx.SymVerbatimCode
	case '%':
		sym = zsx.SymVerbatimComment
	case '~':
		sym = zsx.SymVerbatimEval
	case '$':
		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 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 *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 = zsx.SymRegionBlock
	case '<':
		sym = zsx.SymRegionQuote
	case '"':
		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 zsx.MakeRegion(sym, attrs, blocksBuilder.List(), ins), true
			}
			inp.SetPos(posL)
		case input.EOS:
			return nil, false
		}





		lastPara = cp.parseBlock(&blocksBuilder, lastPara)

	}
}

// parseRegionLastLine parses the last line of a region and returns its inline text.
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 *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 zsx.MakeHeading(level, attrs, text.List(), "", ""), true
		}
		in := cp.parseInline()
		if in == nil {
			return zsx.MakeHeading(level, attrs, text.List(), "", ""), true
		}
		text.Add(in)
		if inp.Ch == '{' && inp.Peek() != '{' {
			attrs = parseBlockAttributes(inp)
			inp.SkipToEOL()
			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 zsx.MakeThematic(attrs), true
}

// parseNestedList parses a list.
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(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 := zsx.MakeBlock()
	if pv != nil {
		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 = zsx.SymListUnordered
		case '#':
			sym = zsx.SymListOrdered
		case '>':
			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 *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, 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, sx.Cons(sx.Nil(), sx.Nil()))
			newLnCount++
			cp.lists = append(cp.lists, ln)
		}
	}
	return ln, newLnCount
}

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().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(zsx.MakeBlock(cp.lists[childPos]))
		}
		childPos--
		parentPos--
	}
	return nil, true
}

// parseDefTerm parses a term of a definition list.
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(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 {
				// lastPair is either the empty description list or the last block of definitions
				return nil, false
			}
			// lastPair is the definition term
			return res, true
		}
		if pos%2 != 0 {
			// lastPair is either the empty description list or the last block of definitions
			lastPair = lastPair.AppendBang(sx.Cons(in, nil))
			pos++
		} else if first {
			// Previous term had no description
			lastPair = lastPair.
				AppendBang(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 isBreakSym(in.Car()) {
			return res, true
		}
	}
}

// parseDefDescr parses a description of a definition list.
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 := zsx.MakeBlock(zsx.MakeParaList(pn))
	if lpPos%2 == 0 {
		// Just a term, but no definitions
		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
		node = next
		cnt++
	}
	return nil, -1
}

// parseIndent parses initial spaces to continue a list.
func (cp *zmkP) 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 {
	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) {
		lpn.LastPair().SetCdr(pv)
	} else {
		lbn.LastPair().AppendBang(sz.MakePara(pv))
	}
	return true
}

func (cp *zmkP) parseIndentForDescription(cnt int) bool {
	descrl := cp.descrl
	lastPair, pos := lastPairPos(descrl)
	if cnt < 1 || pos < 1 {
		return false
	}
	if pos%2 == 1 {
		// Continuation of a definition term
		for {
			in := cp.parseInline()
			if in == nil {
				return true
			}
			lastPair.Head().LastPair().AppendBang(in)
			if sz.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.







|

















|














|


|




|


|


|







|







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 *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 *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(zsx.SymPara) {
		lpn.LastPair().SetCdr(pv)
	} else {
		lbn.LastPair().AppendBang(zsx.MakeParaList(pv))
	}
	return true
}

func (cp *Parser) parseIndentForDescription(cnt int) bool {
	descrl := cp.descrl
	lastPair, pos := lastPairPos(descrl)
	if cnt < 1 || pos < 2 {
		return false
	}
	if pos%2 == 0 {
		// Continuation of a definition term
		for {
			in := cp.parseInline()
			if in == nil {
				return true
			}
			lastPair.Head().LastPair().AppendBang(in)
			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
		}
		next := curr.Tail()
		if next == nil {
			break
		}
		if symSeparator.IsEqual(next.Head().Car()) {
			// It is a new paragraph!
			obj.LastPair().AppendBang(sz.MakePara(pn))
			return true
		}
		curr = next
	}

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

// parseLinePara parses one paragraph of inline material.
func (cp *zmkP) 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()) {
			return lb.List()
		}
	}
}

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

	var row sx.ListBuilder







|







|


|





|







|






|







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(zsx.MakeParaList(pn))
			return true
		}
		curr = next
	}

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

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

// parseRow parse one table row.
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
		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)
			}
			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 {
	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())
		}
		if inp.Ch == '|' {
			return sz.MakeCell(sz.SymCell, cell.List())
		}

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

// parseTransclusion parses '{' '{' '{' ZID '}' '}' '}'
func parseTransclusion(inp *input.Input) (*sx.Pair, bool) {

	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:]) {
				return nil, false
			}
		case '\\':
			switch inp.Next() {
			case input.EOS, '\n', '\r':
				return nil, false
			}







|









|







|


|








|
>












|







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(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 *Parser) parseCell() *sx.Pair {
	inp := cp.inp
	var cell sx.ListBuilder
	for {
		if input.IsEOLEOS(inp.Ch) {
			if cell.IsEmpty() {
				return nil
			}
			return zsx.MakeCell(nil, cell.List())
		}
		if inp.Ch == '|' {
			return zsx.MakeCell(nil, cell.List())
		}

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

// parseTransclusion parses '{' '{' '{' ZID '}' '}' '}'
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 !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
		}
		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
}







|
|

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

import (
	"fmt"
	"slices"
	"strings"

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

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

		var in *sx.Pair







|
|
<


|







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

)

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
			return in
		}
	}
	inp.SetPos(pos)
	return parseText(inp)
}

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

func parseString(inp *input.Input) string {
	pos := inp.Pos
	if inp.Ch == '\\' {
		inp.Next()
		return parseBackslashRest(inp)
	}







|







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

func parseBackslash(inp *input.Input) *sx.Pair {
	switch inp.Next() {
	case '\n', '\r':
		inp.EatEOL()
		return sz.MakeHard()
	default:
		return sz.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()
}

func (cp *zmkP) 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)
			refSym, _ := sx.GetSymbol(ref.Car())
			sym := sz.MapRefStateToLink(refSym)
			return sz.MakeLink(sym, attrs, ref.Tail().Car().(sx.String).GetValue(), text), true
		}
	}
	return nil, false
}
func (cp *zmkP) parseEmbed(openCh, closeCh rune) (*sx.Pair, bool) {
	if refString, text, ok := cp.parseReference(openCh, closeCh); ok {
		attrs := parseInlineAttributes(cp.inp)
		if len(refString) > 0 {
			return sz.MakeEmbed(attrs, ParseReference(refString), "", text), true
		}
	}
	return nil, false
}

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

func (cp *zmkP) parseReference(openCh, closeCh rune) (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:]) {
		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







|

|


















|


|



|
<
<
|




|



|





<
<
<
<
|









|







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 zsx.MakeHard()
	default:
		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 zsx.MakeSoft()
}

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 := cp.scanReference(refString)


			return zsx.MakeLink(attrs, ref, text), true
		}
	}
	return nil, false
}
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 zsx.MakeEmbed(attrs, cp.scanReference(refString), "", text), true
		}
	}
	return nil, false
}





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 !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
			}
			inp.SetPos(pos)
		}
	}

	inp.SkipSpace()
	pos = inp.Pos
	if !readReferenceToClose(inp, closeCh) {
		return "", nil, false
	}
	ref := strings.TrimSpace(string(inp.Src[pos:inp.Pos]))
	if inp.Next() != closeCh {
		return "", nil, false
	}
	inp.Next()







|







176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
			}
			inp.SetPos(pos)
		}
	}

	inp.SkipSpace()
	pos = inp.Pos
	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
			}
			continue
		}
		inp.Next()
	}
}

func readReferenceToClose(inp *input.Input, closeCh rune) bool {

	pos := inp.Pos
	for {
		switch inp.Ch {
		case input.EOS:
			return false
		case '\t', '\r', '\n', ' ':
			if !hasQueryPrefix(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) {
	inp := cp.inp
	switch inp.Next() {
	case ' ', ',', '|', ']', '\n', '\r':
		return nil, false
	}
	pos := inp.Pos
loop:







|
>






|














|







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 (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 !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 *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
		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
}

func (cp *zmkP) 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
}

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







|


|






|


|







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 zsx.MakeCite(attrs, string(inp.Src[pos:posL]), ins), true
}

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

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
		ins, ok = cp.parseLinkLikeRest()
		if !ok {
			return nil, false
		}
	} else {
		inp.Next()
	}
	return sz.MakeMark(mark, "", "", ins), true
	// Problematisch ist, dass hier noch nicht mn.Fragment und mn.Slug gesetzt werden.
	// Evtl. muss es ein PreMark-Symbol geben
}

func (cp *zmkP) parseLinkLikeRest() (*sx.Pair, bool) {
	var ins sx.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()) {
			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
		}
		inp.Next()
	}
}

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

func (cp *zmkP) parseFormat() (*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
			}
			inlines.Add(sz.MakeText(string(fch)))
		} else if in := cp.parseInline(); in != nil {
			if input.IsEOLEOS(inp.Ch) && sz.IsBreakSym(in.Car()) {
				return nil, false
			}
			inlines.Add(in)
		}
	}
}

var mapRuneLiteral = map[rune]*sx.Symbol{
	'`':          sz.SymLiteralCode,
	runeModGrave: sz.SymLiteralCode,
	'\'':         sz.SymLiteralInput,
	'=':          sz.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 {







|




|









|



















|






|
|
|
|
|
|
|
|
|


|




















|

|

|








|
|
|
|







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 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 *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) && 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 zsx.MakeLiteral(zsx.SymLiteralComment, attrs, string(inp.Src[pos:inp.Pos])), true
		}
		inp.Next()
	}
}

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

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 zsx.MakeFormat(symFormat, attrs, inlines.List()), true
			}
			inlines.Add(zsx.MakeText(string(fch)))
		} else if in := cp.parseInline(); in != nil {
			if input.IsEOLEOS(inp.Ch) && isBreakSym(in.Car()) {
				return nil, false
			}
			inlines.Add(in)
		}
	}
}

var mapRuneLiteral = map[rune]*sx.Symbol{
	'`':          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
		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
			}
			sb.WriteRune(fch)
			inp.Next()
		} else {
			s := parseString(inp)
			sb.WriteString(s)
		}







|







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

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







|











|



|
|



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 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 zsx.MakeText("\u2013"), true
}

func parseEntity(inp *input.Input) (*sx.Pair, bool) {
	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

package zmk

import (
	"strings"

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

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

type postProcessor struct{}








|







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

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

		sz.SymInline:       postProcessInlineList,
		sz.SymText:         postProcessText,
		sz.SymSoft:         postProcessSoft,
		sz.SymEndnote:      postProcessEndnote,
		sz.SymMark:         postProcessMark,
		sz.SymLinkBased:    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,

		symSeparator: ignoreProcess,
	}
}

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








|










|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|

|
|
|
|
|
|
<
<
<
<
<
<
<
<
|
|
|
|
|
|
|
|
|
|
|







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

		zsx.SymInline:       postProcessInlineList,
		zsx.SymText:         postProcessText,
		zsx.SymSoft:         postProcessSoft,
		zsx.SymEndnote:      postProcessEndnote,
		zsx.SymMark:         postProcessMark,
		zsx.SymLink:         postProcessInlines4,








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

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

func postProcessItemList(pp *postProcessor, ln *sx.Pair, env *sx.Pair) *sx.Pair {

	elems := pp.visitListElems(ln, env)
	if elems == nil {
		return nil
	}
	return elems.Cons(ln.Car())
}

func postProcessQuoteList(pp *postProcessor, ln *sx.Pair, env *sx.Pair) *sx.Pair {

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

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

func postProcessDescription(pp *postProcessor, dl *sx.Pair, env *sx.Pair) *sx.Pair {

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







|



















|





>
|



|



>
|








|





|








|

|








|





|







>


|




|


|







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 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 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.Tail(), env)
	if elems == nil {
		return nil
	}
	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.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(zsx.SymBlock, newPara.List().Cons(zsx.SymPara)))
			newPara.Reset()
		}
	}
	for node := range elems.Pairs() {
		item := node.Head()
		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(zsx.SymPara) {
			if !newPara.IsEmpty() {
				newPara.Add(sx.Cons(zsx.SymSoft, nil))
			}
			newPara.ExtendBang(pn.Tail())
			continue
		}
		addtoParagraph()
		newElems.Add(item)
	}
	addtoParagraph()
	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 := 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().Tail().Pairs() {
		isTerm = !isTerm
		if isTerm {
			dList.Add(pp.visitInlines(node.Head(), env))
		} else {
			dList.Add(zsx.Walk(pp, node.Head(), env))
		}
	}
	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
}

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


		ins := pp.visitInlines(cell.Tail(), env)
		newCell := ins.Cons(cell.Car())
		pCells.Add(newCell)
		width++
	}
	return pCells.List(), width
}

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

	foundHeader := false
	cellCount := 0

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

		if cellTail == nil {
			continue
		}

		// 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()
				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()
			if next == nil {
				break
			}
			cellTail = next
		}

		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[len(str)-1])
				if !cellAlign.IsEqualSymbol(sz.SymCell) {
					elem.SetCdr(sx.Cons(sx.MakeString(str[0:len(str)-1]), nil))

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




		if cellTail == nil {
			continue
		}








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

	for cellCount < len(align) {




		lastCellNode = lastCellNode.AppendBang(sx.Cons(align[cellCount], nil))
		cellCount++
	}
}





func getCellAlignment(ch byte) *sx.Symbol {
	switch ch {
	case ':':
		return sz.SymCellCenter
	case '<':
		return sz.SymCellLeft
	case '>':
		return sz.SymCellRight
	default:
		return sz.SymCell
	}
}

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))
		if !isPair || elem == nil {
			continue
		}
		elemSym := elem.Car()
		elemTail := elem.Tail()

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

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

			elemText := elemTail.Car().(sx.String).GetValue()
			if elemText != "" && (elemText[0] == ' ' || elemText[0] == '\t') {
				for elemText != "" {







>
>
|
<
|





|
|








|
>
|




|
|











|



|


|
|


|
|

>

|
<





<
<
<



<
<
<
<
<



|




|



<
|
|
>
>
>
>
|


>
>
>
>
>
>
>
|
<
<
<
<
<
<
<

|





|
>
>
>
>
|
|



>
>
>
>
|


|

|

|

|












|






|









|







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(rest.Tail(), env)

		pCells.Add(zsx.MakeCell(attrs, ins))
		width++
	}
	return pCells.List(), 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++
		rest := cell.Tail() // attrs := rest.Head()
		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()
				if str[0] == '=' {
					foundHeader = true
					elem.SetCdr(sx.Cons(sx.MakeString(str[1:]), nil))
				}
			}
		}

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

		elem = cellInlines.Head()
		if elem.Car().IsEqual(zsx.SymText) {
			if s, isString := sx.GetString(elem.Tail().Car()); isString && s.GetValue() != "" {
				str := s.GetValue()
				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] = lastByte

			}
		}
	}

	if !foundHeader {



		return nil, rows, align
	}






	return rows.Head(), rows.Tail(), align
}

func alignRow(row *sx.Pair, defaultAlign []byte) {
	if row == nil {
		return
	}
	var lastCellNode *sx.Pair
	cellColumnNo := 0
	for node := range row.Pairs() {
		lastCellNode = node
		cell := node.Head()

		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.SetCdr(sx.Cons(sx.MakeString(str[1:]), nil))
					rest.SetCar(makeCellAttrs(cellAlign))
				}
			}
		}
	}

	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.String, bool) {
	switch ch {
	case ':':
		return zsx.AttrAlignCenter, true
	case '<':
		return zsx.AttrAlignLeft, true
	case '>':
		return zsx.AttrAlignRight, true
	default:
		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(zsx.Walk(pp, node.Head(), env))
		if !isPair || elem == nil {
			continue
		}
		elemSym := elem.Car()
		elemTail := elem.Tail()

		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(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
				vector = append(vector, elem)
			}
			continue
		}
		last := vector[len(vector)-1]
		lastSym := last.Car()

		if lastSym.IsEqual(sz.SymText) && elemSym.IsEqual(sz.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) {
			// 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())
					continue
				}
				lastTail.SetCar(sx.MakeString(newText))
				elemSym = sz.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) {
			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) {
			lastPos--
		} else {
			break
		}
	}
	if lastPos < 0 {
		return nil







|







|





|



|















|








|







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(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(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(zsx.SymHard, sx.Nil())
					continue
				}
				lastTail.SetCar(sx.MakeString(newText))
				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(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 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
	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)
}

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 sz.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)
}

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

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

func postProcessFormat(pp *postProcessor, fn *sx.Pair, env *sx.Pair) *sx.Pair {
	symFormat := fn.Car().(*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)
}







|






|

|










|




















|











|

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(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 zsx.MakeEndnote(attrs, text)
	}
	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 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 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 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
import (
	"maps"
	"slices"
	"strings"
	"unicode"

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

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

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



































}

// 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() {
	cp.lists = nil
	cp.lastRow = nil
	cp.descrl = nil
}

type attrMap map[string]string








|

<
|
<
<
<
|
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
|
<
<
<
<
|
<
<
|
|





>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>











|







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

	"t73f.de/r/zsx"



	"t73f.de/r/zsx/input"


















)







// 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 *Parser) clearStacked() {
	cp.lists = nil
	cp.lastRow = nil
	cp.descrl = nil
}

type attrMap map[string]string

245
246
247
248
249
250
251





		}
	}
}

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












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

package zmk_test

import (
	"testing"

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

func FuzzParseBlocks(f *testing.F) {

	f.Fuzz(func(t *testing.T, src []byte) {
		t.Parallel()
		inp := input.NewInput(src)

		zmk.Parse(inp)
	})
}







|
|



>



>
|


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

import (
	"fmt"
	"strings"
	"testing"

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

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

func replace(s string, sm symbolMap, tcs TestCases) TestCases {







|
|
|







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

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


	for tcn, tc := range tcs {
		t.Run(fmt.Sprintf("TC=%02d,src=%q", tcn, tc.source), func(st *testing.T) {
			st.Helper()
			inp := input.NewInput([]byte(tc.source))

			ast := zmk.Parse(inp)
			sz.Walk(astWalker{}, ast, nil)
			got := ast.String()
			if tc.want != got {
				st.Errorf("\nwant=%q\n got=%q", tc.want, got)
			}
		})
	}
}

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







>




>
|
|












>
>
>
>
>
>
>







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 := 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
		{"[[|", "(BLOCK (PARA (TEXT \"[[|\")))"},
		{"[[]", "(BLOCK (PARA (TEXT \"[[]\")))"},
		{"[[|]", "(BLOCK (PARA (TEXT \"[[|]\")))"},
		{"[[]]", "(BLOCK (PARA (TEXT \"[[]]\")))"},
		{"[[|]]", "(BLOCK (PARA (TEXT \"[[|]]\")))"},
		{"[[ ]]", "(BLOCK (PARA (TEXT \"[[ ]]\")))"},
		{"[[\n]]", "(BLOCK (PARA (TEXT \"[[\") (SOFT) (TEXT \"]]\")))"},
		{"[[ a]]", "(BLOCK (PARA (LINK-EXTERNAL () \"a\")))"},
		{"[[a ]]", "(BLOCK (PARA (TEXT \"[[a ]]\")))"},
		{"[[a\n]]", "(BLOCK (PARA (TEXT \"[[a\") (SOFT) (TEXT \"]]\")))"},
		{"[[a]]", "(BLOCK (PARA (LINK-EXTERNAL () \"a\")))"},
		{"[[12345678901234]]", "(BLOCK (PARA (LINK-ZETTEL () \"12345678901234\")))"},
		{"[[a]", "(BLOCK (PARA (TEXT \"[[a]\")))"},
		{"[[|a]]", "(BLOCK (PARA (TEXT \"[[|a]]\")))"},
		{"[[b|]]", "(BLOCK (PARA (TEXT \"[[b|]]\")))"},
		{"[[b|a]]", "(BLOCK (PARA (LINK-EXTERNAL () \"a\" (TEXT \"b\"))))"},
		{"[[b| a]]", "(BLOCK (PARA (LINK-EXTERNAL () \"a\" (TEXT \"b\"))))"},
		{"[[b%c|a]]", "(BLOCK (PARA (LINK-EXTERNAL () \"a\" (TEXT \"b%c\"))))"},
		{"[[b%%c|a]]", "(BLOCK (PARA (TEXT \"[[b\") (LITERAL-COMMENT () \"c|a]]\")))"},
		{"[[b|a]", "(BLOCK (PARA (TEXT \"[[b|a]\")))"},
		{"[[b\nc|a]]", "(BLOCK (PARA (LINK-EXTERNAL () \"a\" (TEXT \"b\") (SOFT) (TEXT \"c\"))))"},
		{"[[b c|a#n]]", "(BLOCK (PARA (LINK-EXTERNAL () \"a#n\" (TEXT \"b c\"))))"},
		{"[[a]]go", "(BLOCK (PARA (LINK-EXTERNAL () \"a\") (TEXT \"go\")))"},
		{"[[b|a]]{go}", "(BLOCK (PARA (LINK-EXTERNAL ((\"go\" . \"\")) \"a\" (TEXT \"b\"))))"},
		{"[[[[a]]|b]]", "(BLOCK (PARA (TEXT \"[[\") (LINK-EXTERNAL () \"a\") (TEXT \"|b]]\")))"},
		{"[[a[b]c|d]]", "(BLOCK (PARA (LINK-EXTERNAL () \"d\" (TEXT \"a[b]c\"))))"},
		{"[[[b]c|d]]", "(BLOCK (PARA (TEXT \"[\") (LINK-EXTERNAL () \"d\" (TEXT \"b]c\"))))"},
		{"[[a[]c|d]]", "(BLOCK (PARA (LINK-EXTERNAL () \"d\" (TEXT \"a[]c\"))))"},
		{"[[a[b]|d]]", "(BLOCK (PARA (LINK-EXTERNAL () \"d\" (TEXT \"a[b]\"))))"},
		{"[[\\|]]", "(BLOCK (PARA (LINK-EXTERNAL () \"\\\\|\")))"},
		{"[[\\||a]]", "(BLOCK (PARA (LINK-EXTERNAL () \"a\" (TEXT \"|\"))))"},
		{"[[b\\||a]]", "(BLOCK (PARA (LINK-EXTERNAL () \"a\" (TEXT \"b|\"))))"},
		{"[[b\\|c|a]]", "(BLOCK (PARA (LINK-EXTERNAL () \"a\" (TEXT \"b|c\"))))"},
		{"[[\\]]]", "(BLOCK (PARA (LINK-EXTERNAL () \"\\\\]\")))"},
		{"[[\\]|a]]", "(BLOCK (PARA (LINK-EXTERNAL () \"a\" (TEXT \"]\"))))"},
		{"[[b\\]|a]]", "(BLOCK (PARA (LINK-EXTERNAL () \"a\" (TEXT \"b]\"))))"},
		{"[[\\]\\||a]]", "(BLOCK (PARA (LINK-EXTERNAL () \"a\" (TEXT \"]|\"))))"},
		{"[[http://a]]", "(BLOCK (PARA (LINK-EXTERNAL () \"http://a\")))"},
		{"[[http://a|http://a]]", "(BLOCK (PARA (LINK-EXTERNAL () \"http://a\" (TEXT \"http://a\"))))"},
		{"[[[[a]]]]", "(BLOCK (PARA (TEXT \"[[\") (LINK-EXTERNAL () \"a\") (TEXT \"]]\")))"},
		{"[[query:title]]", "(BLOCK (PARA (LINK-QUERY () \"title\")))"},
		{"[[query:title syntax]]", "(BLOCK (PARA (LINK-QUERY () \"title syntax\")))"},
		{"[[query:title | action]]", "(BLOCK (PARA (LINK-QUERY () \"title | action\")))"},
		{"[[Text|query:title]]", "(BLOCK (PARA (LINK-QUERY () \"title\" (TEXT \"Text\"))))"},
		{"[[Text|query:title syntax]]", "(BLOCK (PARA (LINK-QUERY () \"title syntax\" (TEXT \"Text\"))))"},
		{"[[Text|query:title | action]]", "(BLOCK (PARA (LINK-QUERY () \"title | action\" (TEXT \"Text\"))))"},
	})
}

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

func TestCite(t *testing.T) {
	t.Parallel()
	checkTcs(t, TestCases{
		{"[@", "(BLOCK (PARA (TEXT \"[@\")))"},







|


|
|



|
|
|


|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|

















|

|



|
|

|
|
|
|
|
|
|
|
|
|
|
|


|







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 () (HOSTED \"a\"))))"},
		{"[[a ]]", "(BLOCK (PARA (TEXT \"[[a ]]\")))"},
		{"[[a\n]]", "(BLOCK (PARA (TEXT \"[[a\") (SOFT) (TEXT \"]]\")))"},
		{"[[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 () (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 () (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 () (HOSTED \"a\") \"\")))"},
		{"{{12345678901234}}", "(BLOCK (PARA (EMBED () (ZETTEL \"12345678901234\") \"\")))"},
		{"{{ 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 () (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 () (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 () (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
		{"100%", "(BLOCK (PARA (TEXT \"100%\")))"},
		{"%%{=}a", "(BLOCK (PARA (LITERAL-COMMENT ((\"\" . \"\")) \"a\")))"},
	})
}

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







|
|
|
|
|
|
|
|







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{
		"_": 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
			{"$$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{
		{"$", "(BLOCK (PARA (TEXT \"\\\"\")))"},
		{"$$", "(BLOCK (PARA (TEXT \"\\\"\\\"\")))"},
		{"$$$", "(BLOCK (PARA (TEXT \"\\\"\\\"\\\"\")))"},
		{"$$$$", "(BLOCK (PARA ($% ())))"},

		{"$$a$$", "(BLOCK (PARA ($% () (TEXT \"a\"))))"},
		{"$$a$$$", "(BLOCK (PARA ($% () (TEXT \"a\")) (TEXT \"\\\"\")))"},







|







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{`"`: 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
		{"__**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,
	}
	t.Parallel()
	for _, ch := range []string{"`", "'", "="} {
		checkTcs(t, replace(ch, symMap, TestCases{
			{"$", "(BLOCK (PARA (TEXT \"$\")))"},
			{"$$", "(BLOCK (PARA (TEXT \"$$\")))"},
			{"$$$", "(BLOCK (PARA (TEXT \"$$$\")))"},







|
|
|







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{
		"`": 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
		{"=", "(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\")))"},
		{"=== 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) {







|
|
|
|
|







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 () (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
			{"$$$", "(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\")))))"},

		// 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\")))))"},

		// A HRule creates a new list
		{"* 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\")))))"},

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

		// Empty continuation
		{"* 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\")))))"},
	})
}

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\") (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\"))))"},
		{":", "(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\"))))))"},

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

func TestTable(t *testing.T) {
	t.Parallel()
	checkTcs(t, 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\")))))"},
		{"|%", "()"},
		{"|=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))))"},
	})
}

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

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







|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|


|


|


|


|


|






|
|
|
|
|






|









|
|
|
|
|



|
|
|
|
|
|
|
|
|


|

|







|
|
|
|
|
|
|
|

|
|
|
|
|
|






|
|
|
|
|
|
|







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

		// 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\")))))"},

		// A HRule creates a new list
		{"* 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\")))))"},

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

		// Empty continuation
		{"* 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\")))))"},
	})
}

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\") (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\"))))"},
		{":", "(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\"))))))"},

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

func TestTable(t *testing.T) {
	t.Parallel()
	checkTcs(t, 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\")))))"},
		{"|%", "()"},
		{"|=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 () (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
// 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"
)

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








|
|







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/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
	if !isPair {
		return
	}
	sym := cmd.Car()
	if sx.IsNil(sym) {
		return
	}
	if sym.IsEqual(sz.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) {
		enc.sb.WriteByte(' ')
	} else if sym.IsEqual(sz.SymHard) {
		enc.sb.WriteByte('\n')
	} else if !sym.IsEqual(sx.SymbolQuote) {
		enc.executeList(cmd.Tail())
	}
}







|


















|

|





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(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(zsx.SymSoft) {
		enc.sb.WriteByte(' ')
	} 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
<title>Change Log</title>












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

<a name="0_19"></a>
<h2>Changes for Version 0.19.0 (2024-12-13)</h2>


>
>
>
>
>
>
>
>
>
>
>

|







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 (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
<title>Home</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]
  *  [/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.
|




|
|
|
|
|
|







1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
<title>ZSC</title>

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

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