Zettelstore Client

Check-in Differences
Login

Check-in Differences

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

Difference From v0.21.0 To trunk

2025-08-06
17:17
Adapt to sxwebs changes; update webs dependency ... (Leaf check-in: 38bd9f293b user: stern tags: trunk)
2025-07-07
07:21
Update t73fde dependencies ... (check-in: be388711ad user: stern tags: trunk)
2025-05-02
14:34
Remove Zid for SxnPrelude ... (check-in: b5c74e61e1 user: stern tags: trunk)
2025-04-17
14:58
Version 0.21.0 ... (check-in: ef331b4f0c user: stern tags: release, trunk, v0.21.0)
09:21
Update changelog ... (check-in: 6f32e74255 user: stern tags: trunk)

Changes to api/const.go.
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
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







-
+


+
+
-
+



-
+









+
+







const (
	CommandAuthenticated = Command("authenticated")
	CommandRefresh       = Command("refresh")
)

// Supported search operator representations.
const (
	BackwardDirective = "BACKWARD" // Backward-only context
	BackwardDirective = "BACKWARD" // Backward-only context / thread
	ContextDirective  = "CONTEXT"  // Context directive
	CostDirective     = "COST"     // Maximum cost of a context operation
	DirectedDirective = "DIRECTED" // Context/thread collection can have general directions
	FolgeDirective    = "FOLGE"    // Folge thread
	ForwardDirective  = "FORWARD"  // Forward-only context
	ForwardDirective  = "FORWARD"  // Forward-only context / thread
	FullDirective     = "FULL"     // Include tags in context
	IdentDirective    = "IDENT"    // Use only specified zettel
	ItemsDirective    = "ITEMS"    // Select list elements in a zettel
	MaxDirective      = "MAX"      // Maximum number of context results
	MaxDirective      = "MAX"      // Maximum number of context / thread results
	MinDirective      = "MIN"      // Minimum number of context results
	LimitDirective    = "LIMIT"    // Maximum number of zettel
	OffsetDirective   = "OFFSET"   // Offset to start returned zettel list
	OrDirective       = "OR"       // Combine several search expression with an "or"
	OrderDirective    = "ORDER"    // Specify metadata keys for the order of returned list
	PhraseDirective   = "PHRASE"   // Only unlinked zettel with given phrase
	PickDirective     = "PICK"     // Pick some random zettel
	RandomDirective   = "RANDOM"   // Order zettel list randomly
	ReverseDirective  = "REVERSE"  // Reverse the order of a zettel list
	SequelDirective   = "SEQUEL"   // Sequel / branching thread
	ThreadDirective   = "THREAD"   // Both folge and Sequel thread
	UnlinkedDirective = "UNLINKED" // Search for zettel that contain a phase(s) but do not link

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

	KeysAction     = "KEYS"     // Provide metadata key used
	MinAction      = "MIN"      // Return only those values with a minimum amount of zettel
	MaxAction      = "MAX"      // Return only those values with a maximum amount of zettel
Changes to client/client_test.go.
23
24
25
26
27
28
29
30

31
32
33
34
35
36
37
38
39

40
41

42
43
44
45
46
47
48
49
50
51
52
53
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







-
+
-

-





-
+
-
-
+
-
-


-







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

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

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

func TestGetSzZettel(t *testing.T) {
	c := getClient()
	value, err := c.GetEvaluatedSz(context.Background(), id.ZidDefaultHome, api.PartContent)
	if err != nil {
Changes to domain/id/id.go.
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
85
86
87
88
89
90
91

92
93
94
95
96
97
98







-








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

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

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

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

Changes to domain/id/idset/idset.go.
129
130
131
132
133
134
135
136
137






138
139
140
141
142
143
144
129
130
131
132
133
134
135


136
137
138
139
140
141
142
143
144
145
146
147
148







-
-
+
+
+
+
+
+








// IntersectOrSet removes all zettel identifier that are not in the other set.
// Both sets can be modified by this method. One of them is the set returned.
// It contains the intersection of both, if s is not nil.
//
// If s == nil, then the other set is always returned.
func (s *Set) IntersectOrSet(other *Set) *Set {
	if s == nil || other == nil {
		return other.Clone()
	if s == nil {
		return other // no other.Clone(), since other != nil, i.e. "not found"
	}
	if other == nil {
		s.seq = s.seq[:0]
		return s
	}
	topos, spos, opos := 0, 0, 0
	for spos < len(s.seq) && opos < len(other.seq) {
		sz, oz := s.seq[spos], other.seq[opos]
		if sz < oz {
			spos++
			continue
Changes to domain/meta/meta.go.
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
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







-






-




















-


















-







	KeyFolge        = "folge"
	KeyFolgeRole    = "folge-role"
	KeyForward      = "forward"
	KeyLang         = "lang"
	KeyLicense      = "license"
	KeyModified     = "modified"
	KeyPrecursor    = "precursor"
	KeyPredecessor  = "predecessor"
	KeyPrequel      = "prequel"
	KeyPublished    = "published"
	KeyQuery        = "query"
	KeyReadOnly     = "read-only"
	KeySequel       = "sequel"
	KeySubordinates = "subordinates"
	KeySuccessors   = "successors"
	KeySummary      = "summary"
	KeySuperior     = "superior"
	KeyURL          = "url"
	KeyUselessFiles = "useless-files"
	KeyUserID       = "user-id"
	KeyUserRole     = "user-role"
	KeyVisibility   = "visibility"
)

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

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

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







+
-
+
+




+
+
+
+
+
+
+
+
+
+




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







	}
}

// SetNonEmpty stores the given value under the given key, if the value is non-empty.
// An empty value will delete the previous association.
func (m *Meta) SetNonEmpty(key string, value Value) {
	if value == "" {
		if key != KeyID {
		delete(m.pairs, key) // TODO: key != KeyID
			delete(m.pairs, key)
		}
	} else {
		m.Set(key, value.TrimSpace())
	}
}

// Has returns true, if the given key is used in the metadata.
func (m *Meta) Has(key string) bool {
	if m != nil {
		if _, found := m.pairs[key]; found || key == KeyID {
			return true
		}
	}
	return false
}

// Get retrieves the string value of a given key. The bool value signals,
// whether there was a value stored or not.
func (m *Meta) Get(key string) (Value, bool) {
	if m == nil {
		return "", false
	}
	if key == KeyID {
		return Value(m.Zid.String()), true
	}
	if m != nil {
		if value, found := m.pairs[key]; found {
			return value, true
		}
		if key == KeyID {
			return Value(m.Zid.String()), true
		}
	value, ok := m.pairs[key]
	return value, ok
	}
	return "", false
}

// GetDefault retrieves the string value of the given key. If no value was
// stored, the given default value is returned.
func (m *Meta) GetDefault(key string, def Value) Value {
	if value, found := m.Get(key); found {
		return value
Changes to domain/meta/type.go.
90
91
92
93
94
95
96


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







+
+








var (
	cachedTypedKeys = make(map[string]*DescriptionType)
	mxTypedKey      sync.RWMutex
	suffixTypes     = map[string]*DescriptionType{
		"-date":       TypeTimestamp,
		"-number":     TypeNumber,
		"-ref":        TypeID,
		"-refs":       TypeIDSet,
		SuffixKeyRole: TypeWord,
		"-time":       TypeTimestamp,
		SuffixKeyURL:  TypeURL,
		"-zettel":     TypeID,
		"-zid":        TypeID,
		"-zids":       TypeIDSet,
	}
Changes to go.mod.
1
2
3
4
5
6
7
8
9
10





11
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
	t73f.de/r/sx v0.0.0-20250707071435-95b82f7d24bb
	t73f.de/r/sxwebs v0.0.0-20250806170342-a33d11a3e7c5
	t73f.de/r/webs v0.0.0-20250723141744-5e8deae4d17b
	t73f.de/r/zero v0.0.0-20250703105709-bb38976d4455
	t73f.de/r/zsx v0.0.0-20250707071920-5e29047e4db7
)
Changes to go.sum.
1
2
3
4
5
6
7
8
9
10




















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=
t73f.de/r/sx v0.0.0-20250707071435-95b82f7d24bb h1:cYvTOpaJinh/EPB7i8nx7PtT7hniuSP+NZr74P9U+fE=
t73f.de/r/sx v0.0.0-20250707071435-95b82f7d24bb/go.mod h1:uglbFdRHlcpQVVyCNh4Fd7jbKo8alGBCjRp0aZv8IIg=
t73f.de/r/sxwebs v0.0.0-20250806170342-a33d11a3e7c5 h1:7r1SP0h9QySrRDz9qYWZ/xuEEyJi3XYHtxRIoW2BhoM=
t73f.de/r/sxwebs v0.0.0-20250806170342-a33d11a3e7c5/go.mod h1:CvFDV0czGR0qFVdTYrdy8WIIu5OvA3tFD/td1mim/lA=
t73f.de/r/webs v0.0.0-20250723141744-5e8deae4d17b h1:LmJ0STcaUyGgH2jVa/nKifmJedl0njdnMwPxqX6zLQg=
t73f.de/r/webs v0.0.0-20250723141744-5e8deae4d17b/go.mod h1:b8/5E5Pe6WSWqh+T+sxLO5ZLiGVkuL5tgh86kx2OAIg=
t73f.de/r/zero v0.0.0-20250703105709-bb38976d4455 h1:TFRPPexX2WrwuF03hC+Be2ONx2bPzMMBlNDn0rk88eI=
t73f.de/r/zero v0.0.0-20250703105709-bb38976d4455/go.mod h1:Ovx7CYsjz45BNuIEMGZfqA7NdQxERydJqUGnOBoQaXQ=
t73f.de/r/zsx v0.0.0-20250707071920-5e29047e4db7 h1:ERxpb1Hqln+NXoZDK6sqjmX3BzeoLO+O64f4bK0B6dk=
t73f.de/r/zsx v0.0.0-20250707071920-5e29047e4db7/go.mod h1:64/AjQ1GnEBoBhXI1D0bDMGDj7JCbtZUTT3WoA7kS0s=
Changes to sexp/sexp.go.
17
18
19
20
21
22
23

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







+








import (
	"errors"
	"fmt"
	"sort"

	"t73f.de/r/sx"
	"t73f.de/r/sx/sxbuiltins"
	"t73f.de/r/zsc/api"
)

// EncodeZettel transforms zettel data into a sx object.
func EncodeZettel(zettel api.ZettelData) sx.Object {
	return sx.MakeList(
		sx.MakeSymbol("zettel"),
78
79
80
81
82
83
84
85

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

86
87
88
89
90
91
92
93







-
+







		Content:  contentVals[1].(sx.String).GetValue(),
	}, nil
}

// EncodeMetaRights translates metadata/rights into a sx object.
func EncodeMetaRights(mr api.MetaRights) *sx.Pair {
	return sx.MakeList(
		sx.SymbolList,
		sx.MakeSymbol(sxbuiltins.List.Name),
		meta2sz(mr.Meta),
		sx.MakeList(sx.MakeSymbol("rights"), sx.Int64(int64(mr.Rights))),
	)
}

func meta2sz(m api.ZettelMeta) sx.Object {
	var result sx.ListBuilder
Changes to shtml/shtml.go.
68
69
70
71
72
73
74
75
76

77
78
79
80
81
82
83
84
85
68
69
70
71
72
73
74


75


76
77
78
79
80
81
82







-
-
+
-
-







	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
	}
	return plist.Cons(sxhtml.SymAttr)
}

// Evaluate a metadata s-expression into a list of HTML s-expressions.
func (ev *Evaluator) Evaluate(lst *sx.Pair, env *Environment) (*sx.Pair, error) {
	result := ev.Eval(lst, env)
	if err := env.err; err != nil {
		return nil, err
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
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







-
+






-
+
-





-
+
-







	if env.err != nil || len(env.endnotes) == 0 {
		return nil
	}

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

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

		var li sx.ListBuilder
		li.AddN(SymLI, attrs)
		li.ExtendBang(fni.noteHx)
		li.AddN(sx.MakeString(" "), backref)
		result.Add(li.List())
640
641
642
643
644
645
646
647

648
649
650

651
652
653
654
655
656
657
635
636
637
638
639
640
641

642

643

644
645
646
647
648
649
650
651







-
+
-

-
+








		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(sx.Cons(SymAttrClass, sx.MakeString("zs-noteref")))
			Cons(sxhtml.SymAttr)
		href := sx.Nil().Cons(sx.MakeString(noteNum)).Cons(hrefAttr).Cons(SymA)
		supAttr := sx.Nil().Cons(sx.Cons(SymAttrID, sx.MakeString("fnref:"+noteID))).Cons(sxhtml.SymAttr)
		supAttr := sx.Nil().Cons(sx.Cons(SymAttrID, sx.MakeString("fnref:"+noteID)))
		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))
Changes to sz/zmk/zmk_test.go.
64
65
66
67
68
69
70
71
72


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


71
72
73
74
75
76
77
78
79







-
-
+
+







			}
		})
	}
}

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 (astWalker) VisitBefore(*sx.Pair, *sx.Pair) (sx.Object, bool) { return sx.Nil(), false }
func (astWalker) VisitAfter(node *sx.Pair, _ *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\") \"\")))))"},
	})
}