Index: api/const.go ================================================================== --- api/const.go +++ api/const.go @@ -117,27 +117,31 @@ 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 - ForwardDirective = "FORWARD" // Forward-only context + DirectedDirective = "DIRECTED" // Context/thread collection can have general directions + FolgeDirective = "FOLGE" // Folge thread + 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 Index: client/client_test.go ================================================================== --- client/client_test.go +++ client/client_test.go @@ -25,27 +25,21 @@ "t73f.de/r/zsc/domain/id" ) func TestZettelList(t *testing.T) { c := getClient() - _, err := c.QueryZettel(context.Background(), "") - if err != nil { + if _, err := c.QueryZettel(context.Background(), ""); err != nil { t.Error(err) - return } } func TestGetProtectedZettel(t *testing.T) { c := getClient() - _, err := c.GetZettel(context.Background(), id.ZidStartupConfiguration, api.PartZettel) - if err != nil { - if cErr, ok := err.(*client.Error); ok && cErr.StatusCode == http.StatusForbidden { - return - } else { + if _, err := c.GetZettel(context.Background(), id.ZidStartupConfiguration, api.PartZettel); err != nil { + if cErr, ok := err.(*client.Error); !ok || cErr.StatusCode != http.StatusForbidden { t.Error(err) } - return } } func TestGetSzZettel(t *testing.T) { c := getClient() Index: domain/id/id.go ================================================================== --- domain/id/id.go +++ domain/id/id.go @@ -87,11 +87,10 @@ // 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) Index: domain/id/idset/idset.go ================================================================== --- domain/id/idset/idset.go +++ domain/id/idset/idset.go @@ -131,12 +131,16 @@ // 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 { Index: domain/meta/meta.go ================================================================== --- domain/meta/meta.go +++ domain/meta/meta.go @@ -144,18 +144,16 @@ 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" @@ -172,11 +170,10 @@ 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, "") @@ -191,11 +188,10 @@ 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, "") @@ -277,27 +273,40 @@ // SetNonEmpty stores the given value under the given key, if the value is non-empty. // An empty value will delete the previous association. func (m *Meta) SetNonEmpty(key string, value Value) { if value == "" { - delete(m.pairs, key) // TODO: key != KeyID + if 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 - } - value, ok := m.pairs[key] - return value, ok + if m != nil { + if value, found := m.pairs[key]; found { + return value, true + } + if key == KeyID { + return Value(m.Zid.String()), true + } + } + 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 { Index: domain/meta/type.go ================================================================== --- domain/meta/type.go +++ domain/meta/type.go @@ -92,10 +92,12 @@ 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, Index: go.mod ================================================================== --- go.mod +++ go.mod @@ -1,11 +1,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 ) Index: go.sum ================================================================== --- go.sum +++ go.sum @@ -1,10 +1,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= Index: sexp/sexp.go ================================================================== --- sexp/sexp.go +++ sexp/sexp.go @@ -19,10 +19,11 @@ "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 { @@ -80,11 +81,11 @@ } // 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))), ) } Index: shtml/shtml.go ================================================================== --- shtml/shtml.go +++ shtml/shtml.go @@ -70,14 +70,11 @@ 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) + return plist } // 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) @@ -123,26 +120,24 @@ } 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(sxhtml.SymAttr) + Cons(sx.Cons(SymAttrRole, sx.MakeString("doc-endnote"))) backref := sx.Nil().Cons(sx.MakeString("\u21a9\ufe0e")). Cons(sx.Nil(). Cons(sx.Cons(SymAttrClass, sx.MakeString("zs-endnote-backref"))). Cons(sx.Cons(SymAttrHref, sx.MakeString("#fnref:"+fni.noteID))). - Cons(sx.Cons(SymAttrRole, sx.MakeString("doc-backlink"))). - Cons(sxhtml.SymAttr)). + Cons(sx.Cons(SymAttrRole, sx.MakeString("doc-backlink")))). Cons(SymA) var li sx.ListBuilder li.AddN(SymLI, attrs) li.ExtendBang(fni.noteHx) @@ -642,14 +637,13 @@ 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) + Cons(sx.Cons(SymAttrClass, sx.MakeString("zs-noteref"))) 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)) Index: sz/zmk/zmk_test.go ================================================================== --- sz/zmk/zmk_test.go +++ sz/zmk/zmk_test.go @@ -66,12 +66,12 @@ } } 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\") \"\")))))"},