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 @@ -84,11 +84,11 @@ return len(s.seq) } // Clone returns a copy of the given set. func (s *Set) Clone() *Set { - if s == nil || len(s.seq) == 0 { + if s == nil { return nil } return &Set{seq: slices.Clone(s.seq)} } @@ -131,12 +131,19 @@ // 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 { + if other == nil { + return nil + } + return other.Clone() // must call other.Clone(), otherwise poss. race + } + 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 { @@ -236,11 +243,11 @@ // Remove the identifier from the set. func (s *Set) Remove(zid id.Zid) *Set { if s == nil || len(s.seq) == 0 { return nil } - if pos, found := s.find(zid); found { + if pos, found := slices.BinarySearch(s.seq, zid); found { copy(s.seq[pos:], s.seq[pos+1:]) s.seq = s.seq[:len(s.seq)-1] } if len(s.seq) == 0 { return nil @@ -297,29 +304,14 @@ } return &Set{seq: seq} } func (s *Set) add(zid id.Zid) { - if pos, found := s.find(zid); !found { + if pos, found := slices.BinarySearch(s.seq, zid); !found { s.seq = slices.Insert(s.seq, pos, zid) } } func (s *Set) contains(zid id.Zid) bool { - _, found := s.find(zid) + _, found := slices.BinarySearch(s.seq, zid) return found } - -func (s *Set) find(zid id.Zid) (int, bool) { - hi := len(s.seq) - for lo := 0; lo < hi; { - m := lo + (hi-lo)/2 - if z := s.seq[m]; z == zid { - return m, true - } else if z < zid { - lo = m + 1 - } else { - hi = m - } - } - return hi, false -} Index: domain/id/idset/idset_test.go ================================================================== --- domain/id/idset/idset_test.go +++ domain/id/idset/idset_test.go @@ -40,10 +40,36 @@ if got != tc.exp { t.Errorf("%d: %v.ContainsOrNil(%v) == %v, but got %v", i, tc.s, tc.zid, tc.exp, got) } } } + +func TestSetContains(t *testing.T) { + testcases := []id.Zid{2, 4, 6, 8, 10, 12, 14, 16, 18, 20, 22} + var s *idset.Set + for _, tc := range testcases { + if s.Contains(tc) { + t.Errorf("nil set contains %v", tc) + } + } + s = idset.New() + data := slices.Clone(testcases) + slices.Reverse(data) + s = s.AddSlice(data) + for _, tc := range testcases { + if !s.Contains(tc) { + t.Errorf("set does not contain %v", tc) + } + } + notFounds := []id.Zid{0, 1, 3, 5, 23} + for _, zid := range notFounds { + if s.Contains(zid) { + t.Errorf("set does contain %v", zid) + + } + } +} func TestSetAdd(t *testing.T) { t.Parallel() testcases := []struct { s1, s2 *idset.Set @@ -119,13 +145,13 @@ testcases := []struct { s1, s2 *idset.Set exp *idset.Set }{ {nil, nil, nil}, - {idset.New(), nil, nil}, + {idset.New(), nil, idset.New()}, {nil, idset.New(), nil}, - {idset.New(), idset.New(), nil}, + {idset.New(), idset.New(), idset.New()}, {idset.New(1), nil, idset.New(1)}, {nil, idset.New(1), idset.New(1)}, {idset.New(1), idset.New(), idset.New(1)}, {idset.New(), idset.New(1), idset.New(1)}, {idset.New(1), idset.New(2), idset.New(1, 2)}, 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: domain/meta/values.go ================================================================== --- domain/meta/values.go +++ domain/meta/values.go @@ -20,10 +20,11 @@ "strings" "time" zeroiter "t73f.de/r/zero/iter" "t73f.de/r/zsc/domain/id" + "t73f.de/r/zsx" "t73f.de/r/zsx/input" ) // Value ist a single metadata value. type Value string @@ -130,11 +131,11 @@ ValueSyntaxMarkdown = "markdown" // Syntax: Markdown / CommonMark ValueSyntaxMD = "md" // Syntax: Markdown / CommonMark ValueSyntaxNone = "none" // Syntax: no syntax / content, just metadata ValueSyntaxPlain = "plain" // Syntax: plain text ValueSyntaxPNG = "png" // Syntax: PNG image - ValueSyntaxSVG = "svg" // Syntax: SVG + ValueSyntaxSVG = zsx.SyntaxSVG // Syntax: SVG ValueSyntaxSxn = "sxn" // Syntax: S-Expression ValueSyntaxText = "text" // Syntax: plain text ValueSyntaxTxt = "txt" // Syntax: plain text ValueSyntaxWebp = "webp" // Syntax: WEBP image ValueSyntaxZmk = "zmk" // Syntax: Zettelmarkup Index: go.mod ================================================================== --- go.mod +++ go.mod @@ -1,11 +1,11 @@ module t73f.de/r/zsc -go 1.24 +go 1.25 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-20251106134512-57bd2ba0e8e3 + t73f.de/r/sxwebs v0.0.0-20251106134640-a792e6dfefd8 + t73f.de/r/webs v0.0.0-20251106132628-d89a2b2c2373 + t73f.de/r/zero v0.0.0-20251106132433-8bb93fc3269f + t73f.de/r/zsx v0.0.0-20251107173432-98137df22e7d ) 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-20251106134512-57bd2ba0e8e3 h1:8EQK+viyafJrYKprKFjxv8KO80XLjH2Av5gaI24298g= +t73f.de/r/sx v0.0.0-20251106134512-57bd2ba0e8e3/go.mod h1:4hTEMWfmRB4ocR4ir0WuVoV+q0ed58pwGZwsznSXt1Q= +t73f.de/r/sxwebs v0.0.0-20251106134640-a792e6dfefd8 h1:4I4hAwUuMb4bDNqttF1ZXGbZRnTTBvgHeWZOdELUZw8= +t73f.de/r/sxwebs v0.0.0-20251106134640-a792e6dfefd8/go.mod h1:0rjepPI8mKx3vIe3+IVN3qJVr25uma2A6ozUDpnqbwY= +t73f.de/r/webs v0.0.0-20251106132628-d89a2b2c2373 h1:puhtswOd5A9+RzeTBRfuSsvhs0Nsn9e3PHM7zM4Yges= +t73f.de/r/webs v0.0.0-20251106132628-d89a2b2c2373/go.mod h1:n6yEgRO4XVZYi8F5ilHWgv7oFzVsRDZrXorCdXxdVqk= +t73f.de/r/zero v0.0.0-20251106132433-8bb93fc3269f h1:v334GYGrruo8wr9Wh9R/KahJr8gMX/Nbysg/wpgtAC8= +t73f.de/r/zero v0.0.0-20251106132433-8bb93fc3269f/go.mod h1:6TIoFD0Qn7oEE4GYUzA1cQzwrvhGAADYsm930FK6Yz0= +t73f.de/r/zsx v0.0.0-20251107173432-98137df22e7d h1:/RQiZsDDmadlZkq97aH0LvkIqwncCm8vkRLMeZPPqXY= +t73f.de/r/zsx v0.0.0-20251107173432-98137df22e7d/go.mod h1:sQaYZqc37hSYmHENHrBqIiazhuoZc4Q4dpP9Wc6AM/I= 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/const.go ================================================================== --- shtml/const.go +++ shtml/const.go @@ -11,73 +11,73 @@ // SPDX-FileCopyrightText: 2024-present Detlef Stern //----------------------------------------------------------------------------- package shtml -import "t73f.de/r/sx" +import "t73f.de/r/sxwebs/sxhtml" // Symbols for HTML header tags var ( - SymBody = sx.MakeSymbol("body") - SymHead = sx.MakeSymbol("head") - SymHTML = sx.MakeSymbol("html") - SymMeta = sx.MakeSymbol("meta") - SymScript = sx.MakeSymbol("script") - SymTitle = sx.MakeSymbol("title") + SymBody = sxhtml.MakeSymbol("body") + SymHead = sxhtml.MakeSymbol("head") + SymHTML = sxhtml.MakeSymbol("html") + SymMeta = sxhtml.MakeSymbol("meta") + SymScript = sxhtml.MakeSymbol("script") + SymTitle = sxhtml.MakeSymbol("title") ) // Symbols for HTML body tags var ( - SymA = sx.MakeSymbol("a") - SymASIDE = sx.MakeSymbol("aside") - symBLOCKQUOTE = sx.MakeSymbol("blockquote") - symBR = sx.MakeSymbol("br") - symCITE = sx.MakeSymbol("cite") - symCODE = sx.MakeSymbol("code") - symDD = sx.MakeSymbol("dd") - symDEL = sx.MakeSymbol("del") - SymDIV = sx.MakeSymbol("div") - symDL = sx.MakeSymbol("dl") - symDT = sx.MakeSymbol("dt") - symEM = sx.MakeSymbol("em") - SymEMBED = sx.MakeSymbol("embed") - SymFIGURE = sx.MakeSymbol("figure") - SymH1 = sx.MakeSymbol("h1") - SymH2 = sx.MakeSymbol("h2") - SymHR = sx.MakeSymbol("hr") - SymIMG = sx.MakeSymbol("img") - symINS = sx.MakeSymbol("ins") - symKBD = sx.MakeSymbol("kbd") - SymLI = sx.MakeSymbol("li") - symMARK = sx.MakeSymbol("mark") - SymOL = sx.MakeSymbol("ol") - SymP = sx.MakeSymbol("p") - symPRE = sx.MakeSymbol("pre") - symSAMP = sx.MakeSymbol("samp") - SymSPAN = sx.MakeSymbol("span") - SymSTRONG = sx.MakeSymbol("strong") - symSUB = sx.MakeSymbol("sub") - symSUP = sx.MakeSymbol("sup") - symTABLE = sx.MakeSymbol("table") - symTBODY = sx.MakeSymbol("tbody") - symTHEAD = sx.MakeSymbol("thead") - symTD = sx.MakeSymbol("td") - symTH = sx.MakeSymbol("th") - symTR = sx.MakeSymbol("tr") - SymUL = sx.MakeSymbol("ul") + SymA = sxhtml.MakeSymbol("a") + SymASIDE = sxhtml.MakeSymbol("aside") + symBLOCKQUOTE = sxhtml.MakeSymbol("blockquote") + symBR = sxhtml.MakeSymbol("br") + symCITE = sxhtml.MakeSymbol("cite") + symCODE = sxhtml.MakeSymbol("code") + symDD = sxhtml.MakeSymbol("dd") + symDEL = sxhtml.MakeSymbol("del") + SymDIV = sxhtml.MakeSymbol("div") + symDL = sxhtml.MakeSymbol("dl") + symDT = sxhtml.MakeSymbol("dt") + symEM = sxhtml.MakeSymbol("em") + SymEMBED = sxhtml.MakeSymbol("embed") + SymFIGURE = sxhtml.MakeSymbol("figure") + SymH1 = sxhtml.MakeSymbol("h1") + SymH2 = sxhtml.MakeSymbol("h2") + SymHR = sxhtml.MakeSymbol("hr") + SymIMG = sxhtml.MakeSymbol("img") + symINS = sxhtml.MakeSymbol("ins") + symKBD = sxhtml.MakeSymbol("kbd") + SymLI = sxhtml.MakeSymbol("li") + symMARK = sxhtml.MakeSymbol("mark") + SymOL = sxhtml.MakeSymbol("ol") + SymP = sxhtml.MakeSymbol("p") + symPRE = sxhtml.MakeSymbol("pre") + symSAMP = sxhtml.MakeSymbol("samp") + SymSPAN = sxhtml.MakeSymbol("span") + SymSTRONG = sxhtml.MakeSymbol("strong") + symSUB = sxhtml.MakeSymbol("sub") + symSUP = sxhtml.MakeSymbol("sup") + symTABLE = sxhtml.MakeSymbol("table") + symTBODY = sxhtml.MakeSymbol("tbody") + symTHEAD = sxhtml.MakeSymbol("thead") + symTD = sxhtml.MakeSymbol("td") + symTH = sxhtml.MakeSymbol("th") + symTR = sxhtml.MakeSymbol("tr") + SymUL = sxhtml.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") - SymAttrSrc = sx.MakeSymbol("src") - SymAttrTarget = sx.MakeSymbol("target") - SymAttrTitle = sx.MakeSymbol("title") - SymAttrType = sx.MakeSymbol("type") - SymAttrValue = sx.MakeSymbol("value") + SymAttrClass = sxhtml.MakeSymbol("class") + SymAttrHref = sxhtml.MakeSymbol("href") + SymAttrID = sxhtml.MakeSymbol("id") + SymAttrLang = sxhtml.MakeSymbol("lang") + SymAttrOpen = sxhtml.MakeSymbol("open") + SymAttrRel = sxhtml.MakeSymbol("rel") + SymAttrRole = sxhtml.MakeSymbol("role") + SymAttrSrc = sxhtml.MakeSymbol("src") + SymAttrTarget = sxhtml.MakeSymbol("target") + SymAttrTitle = sxhtml.MakeSymbol("title") + SymAttrType = sxhtml.MakeSymbol("type") + SymAttrValue = sxhtml.MakeSymbol("value") ) Index: shtml/shtml.go ================================================================== --- shtml/shtml.go +++ shtml/shtml.go @@ -67,17 +67,14 @@ 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]))) + plist = plist.Cons(sx.Cons(sxhtml.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) @@ -236,14 +231,11 @@ } 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) + return ev.evalMetaString(args[0], getString(args[1], env).GetValue(), env) } ev.bind(sz.SymTypeCredential, 2, evalMetaString) ev.bind(sz.SymTypeEmpty, 2, evalMetaString) ev.bind(sz.SymTypeID, 2, evalMetaString) ev.bind(sz.SymTypeNumber, 2, evalMetaString) @@ -260,37 +252,55 @@ } 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) + return ev.evalMetaString(args[0], s, env) } ev.bind(sz.SymTypeIDSet, 2, evalMetaSet) ev.bind(sz.SymTypeTagSet, 2, evalMetaSet) } + +func (ev *Evaluator) evalMetaString(nameObj sx.Object, content string, env *Environment) sx.Object { + if env.err == nil { + if nameSym, ok := sx.GetSymbol(nameObj); ok { + a := make(zsx.Attributes, 2). + Set("name", nameSym.GetValue()). + Set("content", content) + return ev.EvaluateMeta(a) + } + env.err = fmt.Errorf("%v/%T is not a symbol", nameObj, nameObj) + } + return sx.Nil() +} // 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) + if sl := ev.evalSlice(args, env); sl != nil { + return sl.Cons(SymP) + } + return nil }) 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) + hLevel := nLevel + ev.headingOffset + if hLevel > 6 { + env.err = fmt.Errorf("%v is a too large heading level", hLevel) + return sx.Nil() + } + sLevel := strconv.FormatInt(hLevel, 10) + headingSymbol := sxhtml.MakeSymbol("h" + sLevel) a := GetAttributes(args[1], env) env.pushAttributes(a) defer env.popAttributes() if fragment := getString(args[3], env).GetValue(); fragment != "" { @@ -324,12 +334,13 @@ var result sx.ListBuilder result.Add(symBLOCKQUOTE) if attrs := EvaluateAttributes(GetAttributes(args[0], env)); attrs != nil { result.Add(attrs) } + isCompact := isCompactList(args[1:]) for _, elem := range args[1:] { - if quote, isPair := sx.GetPair(ev.Eval(elem, env)); isPair { + if quote, isPair := sx.GetPair(ev.Eval(makeCompactItem(isCompact, elem), env)); isPair { result.Add(quote.Cons(sxhtml.SymListSplice)) } } return result.List() }) @@ -360,20 +371,20 @@ } } return result.List() }) - ev.bind(zsx.SymTable, 1, func(args sx.Vector, env *Environment) sx.Object { + ev.bind(zsx.SymTable, 2, func(args sx.Vector, env *Environment) sx.Object { thead := sx.Nil() - if header := getList(args[0], env); !sx.IsNil(header) { + if header := getList(args[1], env); !sx.IsNil(header) { thead = sx.Nil().Cons(ev.evalTableRow(symTH, header, env)).Cons(symTHEAD) } var tbody sx.ListBuilder - if len(args) > 1 { + if len(args) > 2 { tbody.Add(symTBODY) - for _, row := range args[1:] { + for _, row := range args[2:] { tbody.Add(ev.evalTableRow(symTD, getList(row, env), env)) } } table := sx.Nil() @@ -430,13 +441,13 @@ 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 { + ev.bind(zsx.SymBLOB, 3, 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)) + return evalBLOB(a, ev.evalSlice(args[3:], env), getString(args[1], env), getString(args[2], 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") @@ -461,20 +472,50 @@ result.Add(sym) if attrs := EvaluateAttributes(GetAttributes(args[0], env)); attrs != nil { result.Add(attrs) } if len(args) > 1 { + isCompact := isCompactList(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) + var itemLb sx.ListBuilder + itemLb.Add(SymLI) + if res, isPair := sx.GetPair(ev.Eval(makeCompactItem(isCompact, elem), env)); isPair { + itemLb.ExtendBang(res) } - result.Add(item) + result.Add(itemLb.List()) } } return result.List() } +} +func isCompactList(elems sx.Vector) bool { + for _, elem := range elems { + item, isPair := sx.GetPair(elem) + if !isPair { + return false + } + if !zsx.SymBlock.IsEqual(item.Car()) { + return false + } + item = item.Tail() + if item.Tail() != nil { // more than two elements -> multiple paragraphs in item + return false + } + head := item.Head() + if !zsx.SymPara.IsEqual(head.Car()) { + return false + } + } + return true +} +func makeCompactItem(isCompact bool, elem sx.Object) sx.Object { + if isCompact { + if item, isPair := sx.GetPair(elem); isPair { + elem = item.Tail().Head().Tail().Cons(zsx.SymInline) + } + } + return elem } func (ev *Evaluator) evalDescriptionTerm(term *sx.Pair, env *Environment) *sx.Pair { var result sx.ListBuilder for obj := range term.Values() { @@ -600,14 +641,16 @@ 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() + var result *sx.Pair if key := getString(args[1], env); key.GetValue() != "" { if len(args) > 2 { - result = ev.evalSlice(args[2:], env).Cons(sx.MakeString(", ")) + if sl := ev.evalSlice(args[2:], env); sl != nil { + result = sl.Cons(sx.MakeString(", ")) + } } result = result.Cons(key) } if len(a) > 0 { result = result.Cons(EvaluateAttributes(a)) @@ -623,11 +666,14 @@ 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) + if result != nil { + return result.Cons(SymSPAN) + } + return nil }) ev.bind(zsx.SymEndnote, 1, func(args sx.Vector, env *Environment) sx.Object { a := GetAttributes(args[0], env) env.pushAttributes(a) defer env.popAttributes() @@ -642,14 +688,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)) @@ -696,11 +741,14 @@ } res := ev.evalSlice(args[1:], env) if len(a) > 0 { res = res.Cons(EvaluateAttributes(a)) } - return res.Cons(sym) + if res != nil { + return res.Cons(sym) + } + return nil } } func (ev *Evaluator) evalQuote(args sx.Vector, env *Environment) sx.Object { a := GetAttributes(args[0], env) @@ -730,11 +778,14 @@ } if len(a) > 0 { res = res.Cons(EvaluateAttributes(a)) return res.Cons(SymSPAN) } - return res.Cons(sxhtml.SymListSplice) + if res != nil { + return res.Cons(sxhtml.SymListSplice) + } + return nil } var visibleReplacer = strings.NewReplacer(" ", "\u2423") func evalLiteral(args sx.Vector, a zsx.Attributes, sym *sx.Symbol, env *Environment) sx.Object { @@ -873,28 +924,19 @@ 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 { + if result == nil { 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 } @@ -930,11 +972,11 @@ // 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) + sym, val := zsx.GetReference(p) if sym != nil { return sym, val } env.err = fmt.Errorf("%v/%T is not a reference", val, val) } Index: sz/parser.go ================================================================== --- sz/parser.go +++ sz/parser.go @@ -23,23 +23,21 @@ // --- Contains some simple parsers // ---- Syntax: none // ParseNoneBlocks parses no block. -func ParseNoneBlocks(*input.Input) *sx.Pair { return nil } +func ParseNoneBlocks(*input.Input) *sx.Pair { return zsx.MakeBlock() } // ---- 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 + sym := zsx.SymVerbatimCode if syntax == meta.ValueSyntaxHTML { sym = zsx.SymVerbatimHTML - } else { - sym = zsx.SymVerbatimCode } - return sx.MakeList( + return zsx.MakeBlock(zsx.MakeVerbatim( sym, sx.MakeList(sx.Cons(sx.MakeString(""), sx.MakeString(syntax))), - sx.MakeString(string(inp.ScanLineContent())), - ) + string(inp.ScanLineContent()), + )) } Index: sz/parser_test.go ================================================================== --- sz/parser_test.go +++ sz/parser_test.go @@ -15,20 +15,22 @@ import ( "testing" "t73f.de/r/zsc/sz" + "t73f.de/r/zsx" "t73f.de/r/zsx/input" ) func TestParseNone(t *testing.T) { - if got := sz.ParseNoneBlocks(nil); got != nil { + exp := zsx.MakeBlock() + if got := sz.ParseNoneBlocks(nil); !exp.IsEqual(got) { t.Error("GOTB", got) } inp := input.NewInput([]byte("1234\n6789")) - if got := sz.ParseNoneBlocks(inp); got != nil { + if got := sz.ParseNoneBlocks(inp); !exp.IsEqual(got) { t.Error("GOTI", got) } } func TestParsePlani(t *testing.T) { @@ -35,14 +37,14 @@ testcases := []struct { src string syntax string expBlocks string }{ - {"abc", "html", "(VERBATIM-HTML ((\"\" . \"html\")) \"abc\")"}, - {"abc\ndef", "html", "(VERBATIM-HTML ((\"\" . \"html\")) \"abc\\ndef\")"}, - {"abc", "text", "(VERBATIM-CODE ((\"\" . \"text\")) \"abc\")"}, - {"abc\nDEF", "text", "(VERBATIM-CODE ((\"\" . \"text\")) \"abc\\nDEF\")"}, + {"abc", "html", "(BLOCK (VERBATIM-HTML ((\"\" . \"html\")) \"abc\"))"}, + {"abc\ndef", "html", "(BLOCK (VERBATIM-HTML ((\"\" . \"html\")) \"abc\\ndef\"))"}, + {"abc", "text", "(BLOCK (VERBATIM-CODE ((\"\" . \"text\")) \"abc\"))"}, + {"abc\nDEF", "text", "(BLOCK (VERBATIM-CODE ((\"\" . \"text\")) \"abc\\nDEF\"))"}, } for i, tc := range testcases { t.Run(tc.syntax+":"+tc.src, func(t *testing.T) { inp := input.NewInput([]byte(tc.src)) if got := sz.ParsePlainBlocks(inp, tc.syntax).String(); tc.expBlocks != got { Index: sz/ref.go ================================================================== --- sz/ref.go +++ sz/ref.go @@ -12,89 +12,100 @@ //----------------------------------------------------------------------------- package sz import ( + "io" "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) + return zsx.MakeReference(SymRefStateZettel, s) } if s == "00000000000000" { - return MakeReference(zsx.SymRefStateInvalid, s) + return zsx.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 zsx.MakeReference(zsx.SymRefStateInvalid, s) } - return MakeReference(SymRefStateZettel, s) + return zsx.MakeReference(SymRefStateZettel, s) } if zidPart == "00000000000000" { - return MakeReference(zsx.SymRefStateInvalid, s) + return zsx.MakeReference(zsx.SymRefStateInvalid, s) } } if strings.HasPrefix(s, api.QueryPrefix) { - return MakeReference(SymRefStateQuery, s[len(api.QueryPrefix):]) + return zsx.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 zsx.MakeReference(SymRefStateBased, s[1:]) } - return MakeReference(zsx.SymRefStateInvalid, s) + return zsx.MakeReference(zsx.SymRefStateInvalid, s) } } } if s == "" { - return MakeReference(zsx.SymRefStateInvalid, s) + return zsx.MakeReference(zsx.SymRefStateInvalid, s) } u, err := url.Parse(s) if err != nil || u.String() != s { - return MakeReference(zsx.SymRefStateInvalid, s) + return zsx.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) + return zsx.MakeReference(sym, s) +} + +// WriteReference writes the given reference to the writer. If the output is +// scanned via [ScanReference], the given reference should be returned. +func WriteReference(w io.Writer, ref *sx.Pair) (err error) { + refSym, refVal := zsx.GetReference(ref) + if SymRefStateBased.IsEqualSymbol(refSym) { + _, err = io.WriteString(w, "/") + } else if SymRefStateQuery.IsEqualSymbol(refSym) { + _, err = io.WriteString(w, api.QueryPrefix) + } + if err == nil { + _, err = io.WriteString(w, refVal) + } + return err +} + +// ReferenceString returns the reference as a string. +func ReferenceString(ref *sx.Pair) string { + var sb strings.Builder + if err := WriteReference(&sb, ref); err != nil { + return "" + } + return sb.String() +} + +// SplitFragment slices a reference value into the base value and the +// (optional) fragment. Both are separated by the first "#". +func SplitFragment(refValue string) (string, string) { + baseRef, fragment, _ := strings.Cut(refValue, "#") + return baseRef, fragment } Index: sz/ref_test.go ================================================================== --- sz/ref_test.go +++ sz/ref_test.go @@ -12,13 +12,16 @@ // ----------------------------------------------------------------------------- package sz_test import ( + "strings" "testing" + "t73f.de/r/sx" "t73f.de/r/zsc/sz" + "t73f.de/r/zsx" ) func TestParseReference(t *testing.T) { t.Parallel() testcases := []struct { @@ -72,5 +75,68 @@ t.Errorf("%q should be %q, but got %q", tc.s, tc.exp, got) } }) } } + +func TestWriteReference(t *testing.T) { + t.Parallel() + testcases := []struct { + src *sx.Pair + exp string + }{ + {nil, ""}, + {zsx.MakeReference(sz.SymRefStateZettel, "12345678901234"), "12345678901234"}, + {zsx.MakeReference(sz.SymRefStateQuery, "12345678901234"), "query:12345678901234"}, + {zsx.MakeReference(sz.SymRefStateBased, "/based"), "//based"}, + {zsx.MakeReference(zsx.SymRefStateHosted, "/hosted"), "/hosted"}, + } + for _, tc := range testcases { + t.Run(tc.src.String(), func(t *testing.T) { + var sb strings.Builder + err := sz.WriteReference(&sb, tc.src) + if err != nil { + t.Error(err) + return + } + if got := sb.String(); got != tc.exp { + t.Errorf("expect %q, but got %q", tc.exp, got) + } + if got := sz.ReferenceString(tc.src); got != tc.exp { + t.Errorf("expect %q, but got %q", tc.exp, got) + } + if tc.src != nil { + if got := sz.ScanReference(tc.exp); !got.IsEqual(tc.src) { + t.Errorf("expect %v, but got %v", tc.src, got) + } + } + }) + } +} + +func TestSplitFragment(t *testing.T) { + t.Parallel() + testcases := []struct { + value string + base string + frag string + }{ + {"", "", ""}, + {"#", "", ""}, + {"123", "123", ""}, + {"123#", "123", ""}, + {"#123", "", "123"}, + {"123#456", "123", "456"}, + } + for _, tc := range testcases { + t.Run(tc.value, func(t *testing.T) { + if gotBase, gotFrag := sz.SplitFragment(tc.value); gotBase != tc.base || gotFrag != tc.frag { + if gotBase != tc.base { + t.Errorf("base %q expected, but got %q", tc.base, gotBase) + } + if gotFrag != tc.frag { + t.Errorf("frag %q expected, but got %q", tc.frag, gotFrag) + } + } + }) + } +} Index: sz/sz.go ================================================================== --- sz/sz.go +++ sz/sz.go @@ -13,10 +13,12 @@ // Package sz contains zettel data handling as sx expressions. package sz import ( + "strings" + "t73f.de/r/sx" "t73f.de/r/zsx" ) // GetMetaContent returns the metadata and the content of a sz encoded zettel. @@ -101,5 +103,8 @@ return pair } } return nil } + +// NormalizedSpacedText returns the given string, but normalize multiple spaces to one space. +func NormalizedSpacedText(s string) string { return strings.Join(strings.Fields(s), " ") } Index: sz/zmk/block.go ================================================================== --- sz/zmk/block.go +++ sz/zmk/block.go @@ -28,11 +28,11 @@ blocksBuilder.Add(bn) } if cont { return lastPara } - if bn.Car().IsEqual(zsx.SymPara) { + if zsx.SymPara.IsEqual(bn.Car()) { return bn } return nil } @@ -92,10 +92,13 @@ } } inp.SetPos(pos) cp.clearStacked() ins := cp.parsePara() + if ins == nil { + return nil, true + } if startsWithSpaceSoftBreak(ins) { ins = ins.Tail().Tail() } else if lastPara != nil { lastPair := lastPara.LastPair() lastPair.ExtendBang(ins) @@ -119,11 +122,11 @@ pair1, isPair1 := sx.GetPair(next.Car()) if pair1 == nil || !isPair1 { return false } - if pair0.Car().IsEqual(zsx.SymText) && isBreakSym(pair1.Car()) { + if zsx.SymText.IsEqual(pair0.Car()) && 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 @@ -142,11 +145,13 @@ 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)) + if lp := lastPair.Head().LastPair(); !symSeparator.IsEqual(lp.Head().Car()) { + lp.AppendBang(sx.Cons(symSeparator, nil)) + } } } } // parseColon determines which element should be parsed. @@ -354,11 +359,11 @@ kinds := parseNestedListKinds(inp) if len(kinds) == 0 { return nil, false } inp.SkipSpace() - if !kinds[len(kinds)-1].IsEqual(zsx.SymListQuote) && input.IsEOLEOS(inp.Ch) { + if !zsx.SymListQuote.IsEqual(kinds[len(kinds)-1]) && input.IsEOLEOS(inp.Ch) { return nil, false } if len(kinds) < len(cp.lists) { cp.lists = cp.lists[:len(kinds)] @@ -400,11 +405,11 @@ } 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) { + if !kind.IsEqual(cp.lists[i].Car()) { ln = sx.Cons(kind, sx.Cons(sx.Nil(), sx.Nil())) newLnCount++ cp.lists[i] = ln cp.lists = cp.lists[:i+1] } else { @@ -511,26 +516,32 @@ 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) + lp := lastPair.Head().LastPair() + if symSeparator.IsEqual(lp.Head().Car()) { + // Separator now not needed any more. Replace it with newDef + lp.SetCar(newDef) + } else { + lp.AppendBang(newDef) + } } return nil, true } func lastPairPos(p *sx.Pair) (*sx.Pair, int) { - cnt := 0 - for node := p; node != nil; { + if p == nil { + return nil, -1 + } + for node, cnt := p, 0; ; cnt++ { next := node.Tail() if next == nil { return node, cnt } node = next - cnt++ } - return nil, -1 } // parseIndent parses initial spaces to continue a list. func (cp *Parser) parseIndent() bool { inp := cp.inp @@ -563,11 +574,11 @@ return false } ln := cp.lists[cnt-1] lbn := ln.LastPair().Head() lpn := lbn.LastPair().Head() - if lpn.Car().IsEqual(zsx.SymPara) { + if zsx.SymPara.IsEqual(lpn.Car()) { lpn.LastPair().SetCdr(pv) } else { lbn.LastPair().AppendBang(zsx.MakeParaList(pv)) } return true @@ -620,11 +631,11 @@ curr = next } // Continuation of existing paragraph para := bn.LastPair().Head().LastPair().Head() - if para.Car().IsEqual(zsx.SymPara) { + if zsx.SymPara.IsEqual(para.Car()) { para.LastPair().SetCdr(pn) } else { bn.LastPair().AppendBang(zsx.MakeParaList(pn)) } return true @@ -669,11 +680,11 @@ if cp.lastRow == nil { if row.IsEmpty() { return nil } cp.lastRow = sx.Cons(row.List(), nil) - return cp.lastRow.Cons(nil).Cons(zsx.SymTable) + return cp.lastRow.Cons(nil).Cons(nil).Cons(zsx.SymTable) } cp.lastRow = cp.lastRow.AppendBang(row.List()) return nil } // inp.Ch must be '|' @@ -741,7 +752,7 @@ 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 + return zsx.MakeTransclusion(attrs, ref, nil), true } Index: sz/zmk/inline.go ================================================================== --- sz/zmk/inline.go +++ sz/zmk/inline.go @@ -281,10 +281,13 @@ ins, ok := cp.parseLinkLikeRest() if !ok { return nil, false } attrs := parseInlineAttributes(cp.inp) + if ins == nil { + return nil, true + } return zsx.MakeEndnote(attrs, ins), true } func (cp *Parser) parseMark() (*sx.Pair, bool) { inp := cp.inp Index: sz/zmk/post-processor.go ================================================================== --- sz/zmk/post-processor.go +++ sz/zmk/post-processor.go @@ -23,30 +23,30 @@ var symInVerse = sx.MakeSymbol("in-verse") var symNoBlock = sx.MakeSymbol("no-block") type postProcessor struct{} -func (pp *postProcessor) VisitBefore(lst *sx.Pair, env *sx.Pair) (sx.Object, bool) { +func (pp *postProcessor) VisitBefore(lst *sx.Pair, alst *sx.Pair) (sx.Object, bool) { if lst == nil { return nil, true } sym, isSym := sx.GetSymbol(lst.Car()) if !isSym { panic(lst) } if fn, found := symMap[sym]; found { - return fn(pp, lst, env), true + return fn(pp, lst, alst), true } return nil, false } func (pp *postProcessor) VisitAfter(lst *sx.Pair, _ *sx.Pair) sx.Object { return lst } -func (pp *postProcessor) visitPairList(lst *sx.Pair, env *sx.Pair) *sx.Pair { +func (pp *postProcessor) visitPairList(lst *sx.Pair, alst *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 { + if elem, isPair := sx.GetPair(zsx.Walk(pp, node.Head(), alst)); isPair && elem != nil { pList.Add(elem) } } return pList.List() } @@ -94,43 +94,43 @@ } } func ignoreProcess(*postProcessor, *sx.Pair, *sx.Pair) *sx.Pair { return nil } -func postProcessBlockList(pp *postProcessor, lst *sx.Pair, env *sx.Pair) *sx.Pair { - result := pp.visitPairList(lst.Tail(), env) +func postProcessBlockList(pp *postProcessor, lst *sx.Pair, alst *sx.Pair) *sx.Pair { + result := pp.visitPairList(lst.Tail(), alst) if result == nil { - if noBlockPair := env.Assoc(symNoBlock); noBlockPair == nil || sx.IsTrue(noBlockPair.Cdr()) { + if noBlockPair := alst.Assoc(symNoBlock); noBlockPair == nil || sx.IsTrue(noBlockPair.Cdr()) { return nil } } return result.Cons(lst.Car()) } -func postProcessInlineList(pp *postProcessor, lst *sx.Pair, env *sx.Pair) *sx.Pair { +func postProcessInlineList(pp *postProcessor, lst *sx.Pair, alst *sx.Pair) *sx.Pair { sym := lst.Car() - if rest := pp.visitInlines(lst.Tail(), env); rest != nil { + if rest := pp.visitInlines(lst.Tail(), alst); rest != nil { return rest.Cons(sym) } return nil } -func postProcessRegion(pp *postProcessor, rn *sx.Pair, env *sx.Pair) *sx.Pair { - return doPostProcessRegion(pp, rn, env, env) +func postProcessRegion(pp *postProcessor, rn *sx.Pair, alst *sx.Pair) *sx.Pair { + return doPostProcessRegion(pp, rn, alst, alst) } -func postProcessRegionVerse(pp *postProcessor, rn *sx.Pair, env *sx.Pair) *sx.Pair { - return doPostProcessRegion(pp, rn, env.Cons(sx.Cons(symInVerse, nil)), env) +func postProcessRegionVerse(pp *postProcessor, rn *sx.Pair, alst *sx.Pair) *sx.Pair { + return doPostProcessRegion(pp, rn, alst.Cons(sx.Cons(symInVerse, nil)), alst) } -func doPostProcessRegion(pp *postProcessor, rn *sx.Pair, envBlock, envInline *sx.Pair) *sx.Pair { +func doPostProcessRegion(pp *postProcessor, rn *sx.Pair, alstBlock, alstInline *sx.Pair) *sx.Pair { sym := rn.Car().(*sx.Symbol) next := rn.Tail() attrs := next.Car().(*sx.Pair) next = next.Tail() - blocks := pp.visitPairList(next.Head(), envBlock) - text := pp.visitInlines(next.Tail(), envInline) + blocks := pp.visitPairList(next.Head(), alstBlock) + text := pp.visitInlines(next.Tail(), alstInline) if blocks == nil && text == nil { return nil } return zsx.MakeRegion(sym, attrs, blocks, text) } @@ -140,37 +140,37 @@ return verb } return nil } -func postProcessHeading(pp *postProcessor, hn *sx.Pair, env *sx.Pair) *sx.Pair { +func postProcessHeading(pp *postProcessor, hn *sx.Pair, alst *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 { + if text := pp.visitInlines(next.Tail(), alst); 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 { +func postProcessItemList(pp *postProcessor, ln *sx.Pair, alst *sx.Pair) *sx.Pair { attrs := ln.Tail().Head() - elems := pp.visitListElems(ln.Tail(), env) + elems := pp.visitListElems(ln.Tail(), alst) 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 { +func postProcessQuoteList(pp *postProcessor, ln *sx.Pair, alst *sx.Pair) *sx.Pair { attrs := ln.Tail().Head() - elems := pp.visitListElems(ln.Tail(), env.Cons(sx.Cons(symNoBlock, nil))) + elems := pp.visitListElems(ln.Tail(), alst.Cons(sx.Cons(symNoBlock, nil))) // Collect multiple paragraph items into one item. var newElems sx.ListBuilder var newPara sx.ListBuilder @@ -181,20 +181,20 @@ newPara.Reset() } } for node := range elems.Pairs() { item := node.Head() - if !item.Car().IsEqual(zsx.SymBlock) { + if !zsx.SymBlock.IsEqual(item.Car()) { 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 pn := itemTail.Head(); zsx.SymPara.IsEqual(pn.Car()) { if !newPara.IsEmpty() { newPara.Add(sx.Cons(zsx.SymSoft, nil)) } newPara.ExtendBang(pn.Tail()) continue @@ -204,78 +204,79 @@ } addtoParagraph() return zsx.MakeList(ln.Car().(*sx.Symbol), attrs, newElems.List()) } -func (pp *postProcessor) visitListElems(ln *sx.Pair, env *sx.Pair) *sx.Pair { +func (pp *postProcessor) visitListElems(ln *sx.Pair, alst *sx.Pair) *sx.Pair { var pList sx.ListBuilder for node := range ln.Tail().Pairs() { - if elem := zsx.Walk(pp, node.Head(), env); elem != nil { + if elem := zsx.Walk(pp, node.Head(), alst); elem != nil { pList.Add(elem) } } return pList.List() } -func postProcessDescription(pp *postProcessor, dl *sx.Pair, env *sx.Pair) *sx.Pair { +func postProcessDescription(pp *postProcessor, dl *sx.Pair, alst *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)) + dList.Add(pp.visitInlines(node.Head(), alst)) } else { - dList.Add(zsx.Walk(pp, node.Head(), env)) + dList.Add(zsx.Walk(pp, node.Head(), alst.Cons(sx.Cons(symNoBlock, nil)))) } } 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() +func postProcessTable(pp *postProcessor, tbl *sx.Pair, alst *sx.Pair) *sx.Pair { + sym, next := tbl.Car(), tbl.Tail() + attrs := next.Head() + next = next.Tail() header := next.Head() if header != nil { // Already post-processed return tbl } - rows, width := pp.visitRows(next.Tail(), env) + rows, width := pp.visitRows(next.Tail(), alst) if rows == nil { // Header and row are nil -> no table return nil } header, rows, align := splitTableHeader(rows, width) alignRow(header, align) for node := range rows.Pairs() { alignRow(node.Head(), align) } - return rows.Cons(header).Cons(sym) + return rows.Cons(header).Cons(attrs).Cons(sym) } -func (pp *postProcessor) visitRows(rows *sx.Pair, env *sx.Pair) (*sx.Pair, int) { +func (pp *postProcessor) visitRows(rows *sx.Pair, alst *sx.Pair) (*sx.Pair, int) { maxWidth := 0 var pRows sx.ListBuilder for node := range rows.Pairs() { row := node.Head() - row, width := pp.visitCells(row, env) + row, width := pp.visitCells(row, alst) if maxWidth < width { maxWidth = width } pRows.Add(row) } return pRows.List(), maxWidth } -func (pp *postProcessor) visitCells(cells *sx.Pair, env *sx.Pair) (*sx.Pair, int) { +func (pp *postProcessor) visitCells(cells *sx.Pair, alst *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) + ins := pp.visitInlines(rest.Tail(), alst) pCells.Add(zsx.MakeCell(attrs, ins)) width++ } return pCells.List(), width } @@ -296,11 +297,11 @@ continue } // elem is first cell inline element elem := cellInlines.Head() - if elem.Car().IsEqual(zsx.SymText) { + if zsx.SymText.IsEqual(elem.Car()) { 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)) @@ -316,11 +317,11 @@ } cellInlines = next } elem = cellInlines.Head() - if elem.Car().IsEqual(zsx.SymText) { + if zsx.SymText.IsEqual(elem.Car()) { 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)) @@ -357,11 +358,11 @@ continue } // elem is first cell inline element elem := cellInlines.Head() - if elem.Car().IsEqual(zsx.SymText) { + if zsx.SymText.IsEqual(elem.Car()) { 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)) @@ -396,37 +397,37 @@ default: return sx.MakeString(""), false } } -func (pp *postProcessor) visitInlines(lst *sx.Pair, env *sx.Pair) *sx.Pair { +func (pp *postProcessor) visitInlines(lst *sx.Pair, alst *sx.Pair) *sx.Pair { length := lst.Length() if length <= 0 { return nil } - inVerse := env.Assoc(symInVerse) != nil + inVerse := alst.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)) + elem, isPair := sx.GetPair(zsx.Walk(pp, node.Head(), alst)) if !isPair || elem == nil { continue } elemSym := elem.Car() elemTail := elem.Tail() - if inVerse && elemSym.IsEqual(zsx.SymText) { + if inVerse && zsx.SymText.IsEqual(elemSym) { 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) { + if !zsx.SymText.IsEqual(elemSym) { vector = append(vector, elem) continue } elemText := elemTail.Car().(sx.String).GetValue() @@ -445,19 +446,19 @@ continue } last := vector[len(vector)-1] lastSym := last.Car() - if lastSym.IsEqual(zsx.SymText) && elemSym.IsEqual(zsx.SymText) { + if zsx.SymText.IsEqual(lastSym) && zsx.SymText.IsEqual(elemSym) { // 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) { + if zsx.SymText.IsEqual(lastSym) && zsx.SymSoft.IsEqual(elemSym) { // Merge (TEXT "... ") (SOFT) to (TEXT "...") (HARD) lastTail := last.Tail() if lastText := lastTail.Car().(sx.String).GetValue(); strings.HasSuffix(lastText, " ") { newText := removeTrailingSpaces(lastText) if newText == "" { @@ -479,11 +480,11 @@ // 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) { + if zsx.SymText.IsEqual(elemSym) { elemTail := elem.Tail() elemText := elemTail.Car().(sx.String).GetValue() newText := removeTrailingSpaces(elemText) if newText != "" { elemTail.SetCar(sx.MakeString(newText)) @@ -525,64 +526,64 @@ } } return nil } -func postProcessSoft(_ *postProcessor, sn *sx.Pair, env *sx.Pair) *sx.Pair { - if env.Assoc(symInVerse) == nil { +func postProcessSoft(_ *postProcessor, sn *sx.Pair, alst *sx.Pair) *sx.Pair { + if alst.Assoc(symInVerse) == nil { return sn } return sx.Cons(zsx.SymHard, nil) } -func postProcessEndnote(pp *postProcessor, en *sx.Pair, env *sx.Pair) *sx.Pair { +func postProcessEndnote(pp *postProcessor, en *sx.Pair, alst *sx.Pair) *sx.Pair { next := en.Tail() attrs := next.Car().(*sx.Pair) - if text := pp.visitInlines(next.Tail(), env); text != nil { + if text := pp.visitInlines(next.Tail(), alst); 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 { +func postProcessMark(pp *postProcessor, en *sx.Pair, alst *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) + text := pp.visitInlines(next.Tail(), alst) return zsx.MakeMark(mark.GetValue(), slug.GetValue(), fragment.GetValue(), text) } -func postProcessInlines4(pp *postProcessor, ln *sx.Pair, env *sx.Pair) *sx.Pair { +func postProcessInlines4(pp *postProcessor, ln *sx.Pair, alst *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) + text := pp.visitInlines(next.Tail(), alst) return text.Cons(val3).Cons(attrs).Cons(sym) } -func postProcessEmbed(pp *postProcessor, ln *sx.Pair, env *sx.Pair) *sx.Pair { +func postProcessEmbed(pp *postProcessor, ln *sx.Pair, alst *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) + text := pp.visitInlines(next.Tail(), alst) return zsx.MakeEmbed(attrs, ref, syntax.GetValue(), text) } -func postProcessFormat(pp *postProcessor, fn *sx.Pair, env *sx.Pair) *sx.Pair { +func postProcessFormat(pp *postProcessor, fn *sx.Pair, alst *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) + inlines := pp.visitInlines(next, alst) return zsx.MakeFormat(symFormat, attrs, inlines) } Index: sz/zmk/zmk.go ================================================================== --- sz/zmk/zmk.go +++ sz/zmk/zmk.go @@ -59,14 +59,11 @@ 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 + return pp.visitPairList(blkBuild.List(), nil).Cons(zsx.SymBlock) } func withQueryPrefix(src []byte) bool { return len(src) > len(api.QueryPrefix) && string(src[:len(api.QueryPrefix)]) == api.QueryPrefix } Index: sz/zmk/zmk_test.go ================================================================== --- sz/zmk/zmk_test.go +++ sz/zmk/zmk_test.go @@ -23,87 +23,101 @@ "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 testCase struct{ src, exp string } +type testCases []testCase type symbolMap map[string]*sx.Symbol -func replace(s string, sm symbolMap, tcs TestCases) TestCases { +func replace(s string, sm symbolMap, tcs testCases) testCases { var sym string if len(sm) > 0 { sym = sm[s].GetValue() } - var testCases TestCases + var testcases testCases for _, tc := range tcs { - source := strings.ReplaceAll(tc.source, "$", s) - want := tc.want + source := strings.ReplaceAll(tc.src, "$", s) + want := tc.exp if sym != "" { want = strings.ReplaceAll(want, "$%", sym) } want = strings.ReplaceAll(want, "$", s) - testCases = append(testCases, TestCase{source, want}) + testcases = append(testcases, testCase{source, want}) } - return testCases + return testcases } -func checkTcs(t *testing.T, tcs 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) { + t.Run(fmt.Sprintf("TC=%02d,src=%q", tcn, tc.src), func(st *testing.T) { st.Helper() - inp := input.NewInput([]byte(tc.source)) + inp := input.NewInput([]byte(tc.src)) 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) + found := false + if got := ast.String(); tc.exp != got { + st.Errorf("none\nwant=%q\n got=%q", tc.exp, got) + found = true + } + copyAST := zsx.Walk(astWalker{}, ast, nil) + if got := copyAST.String(); tc.exp != got && !found { + st.Errorf("copy\nwant=%q\n got=%q", tc.exp, got) + found = true + } + zsx.WalkIt(astWalkerIt{}, ast, nil) + if got := ast.String(); tc.exp != got && !found { + st.Errorf("itit\nwant=%q\n got=%q", tc.exp, 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 (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 } + +type astWalkerIt struct{} + +func (astWalkerIt) VisitItBefore(*sx.Pair, *sx.Pair) bool { return false } +func (astWalkerIt) VisitItAfter(*sx.Pair, *sx.Pair) {} func TestEdges(t *testing.T) { t.Parallel() - checkTcs(t, TestCases{ + 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", "()"}, - {"\r\n", "()"}, - {"\n\n", "()"}, + checkTcs(t, testCases{ + {"", "(BLOCK)"}, + {"\n", "(BLOCK)"}, + {"\r", "(BLOCK)"}, + {"\r\n", "(BLOCK)"}, + {"\n\n", "(BLOCK)"}, }) } func TestText(t *testing.T) { t.Parallel() - checkTcs(t, TestCases{ + checkTcs(t, testCases{ {"abcd", "(BLOCK (PARA (TEXT \"abcd\")))"}, {"ab cd", "(BLOCK (PARA (TEXT \"ab cd\")))"}, {"abcd ", "(BLOCK (PARA (TEXT \"abcd\")))"}, {" abcd", "(BLOCK (PARA (TEXT \"abcd\")))"}, {"\\", "(BLOCK (PARA (TEXT \"\\\\\")))"}, - {"\\\n", "()"}, + {"\\\n", "(BLOCK)"}, {"\\\ndef", "(BLOCK (PARA (HARD) (TEXT \"def\")))"}, - {"\\\r", "()"}, + {"\\\r", "(BLOCK)"}, {"\\\rdef", "(BLOCK (PARA (HARD) (TEXT \"def\")))"}, - {"\\\r\n", "()"}, + {"\\\r\n", "(BLOCK)"}, {"\\\r\ndef", "(BLOCK (PARA (HARD) (TEXT \"def\")))"}, {"\\a", "(BLOCK (PARA (TEXT \"a\")))"}, {"\\aa", "(BLOCK (PARA (TEXT \"aa\")))"}, {"a\\a", "(BLOCK (PARA (TEXT \"aa\")))"}, {"\\+", "(BLOCK (PARA (TEXT \"+\")))"}, @@ -112,40 +126,40 @@ }) } func TestSpace(t *testing.T) { t.Parallel() - checkTcs(t, TestCases{ - {" ", "()"}, - {"\t", "()"}, - {" ", "()"}, + checkTcs(t, testCases{ + {" ", "(BLOCK)"}, + {"\t", "(BLOCK)"}, + {" ", "(BLOCK)"}, }) } func TestSoftBreak(t *testing.T) { t.Parallel() - checkTcs(t, TestCases{ + checkTcs(t, testCases{ {"x\ny", "(BLOCK (PARA (TEXT \"x\") (SOFT) (TEXT \"y\")))"}, {"z\n", "(BLOCK (PARA (TEXT \"z\")))"}, - {" \n ", "()"}, - {" \n", "()"}, + {" \n ", "(BLOCK)"}, + {" \n", "(BLOCK)"}, }) } func TestHardBreak(t *testing.T) { t.Parallel() - checkTcs(t, TestCases{ + checkTcs(t, testCases{ {"x \ny", "(BLOCK (PARA (TEXT \"x\") (HARD) (TEXT \"y\")))"}, {"z \n", "(BLOCK (PARA (TEXT \"z\")))"}, - {" \n ", "()"}, - {" \n", "()"}, + {" \n ", "(BLOCK)"}, + {" \n", "(BLOCK)"}, }) } func TestLink(t *testing.T) { t.Parallel() - checkTcs(t, TestCases{ + checkTcs(t, testCases{ {"[", "(BLOCK (PARA (TEXT \"[\")))"}, {"[[", "(BLOCK (PARA (TEXT \"[[\")))"}, {"[[|", "(BLOCK (PARA (TEXT \"[[|\")))"}, {"[[]", "(BLOCK (PARA (TEXT \"[[]\")))"}, {"[[|]", "(BLOCK (PARA (TEXT \"[[|]\")))"}, @@ -195,11 +209,11 @@ }) } func TestEmbed(t *testing.T) { t.Parallel() - checkTcs(t, TestCases{ + checkTcs(t, testCases{ {"{", "(BLOCK (PARA (TEXT \"{\")))"}, {"{{", "(BLOCK (PARA (TEXT \"{{\")))"}, {"{{|", "(BLOCK (PARA (TEXT \"{{|\")))"}, {"{{}", "(BLOCK (PARA (TEXT \"{{}\")))"}, {"{{|}", "(BLOCK (PARA (TEXT \"{{|}\")))"}, @@ -236,11 +250,11 @@ }) } func TestCite(t *testing.T) { t.Parallel() - checkTcs(t, TestCases{ + checkTcs(t, testCases{ {"[@", "(BLOCK (PARA (TEXT \"[@\")))"}, {"[@]", "(BLOCK (PARA (TEXT \"[@]\")))"}, {"[@a]", "(BLOCK (PARA (CITE () \"a\")))"}, {"[@ a]", "(BLOCK (PARA (TEXT \"[@ a]\")))"}, {"[@a ]", "(BLOCK (PARA (CITE () \"a\")))"}, @@ -252,35 +266,35 @@ {"[@a| n]", "(BLOCK (PARA (CITE () \"a\" (TEXT \"n\"))))"}, {"[@a|n ]", "(BLOCK (PARA (CITE () \"a\" (TEXT \"n\"))))"}, {"[@a,[@b]]", "(BLOCK (PARA (CITE () \"a\" (CITE () \"b\"))))"}, {"[@a]{color=green}", "(BLOCK (PARA (CITE ((\"color\" . \"green\")) \"a\")))"}, }) - checkTcs(t, TestCases{ + checkTcs(t, testCases{ {"[@a\n\n]", "(BLOCK (PARA (TEXT \"[@a\")) (PARA (TEXT \"]\")))"}, }) } func TestEndnote(t *testing.T) { t.Parallel() - checkTcs(t, TestCases{ + checkTcs(t, testCases{ {"[^", "(BLOCK (PARA (TEXT \"[^\")))"}, - {"[^]", "(BLOCK (PARA (ENDNOTE ())))"}, + {"[^]", "(BLOCK)"}, {"[^abc]", "(BLOCK (PARA (ENDNOTE () (TEXT \"abc\"))))"}, {"[^abc ]", "(BLOCK (PARA (ENDNOTE () (TEXT \"abc\"))))"}, {"[^abc\ndef]", "(BLOCK (PARA (ENDNOTE () (TEXT \"abc\") (SOFT) (TEXT \"def\"))))"}, {"[^abc\n\ndef]", "(BLOCK (PARA (TEXT \"[^abc\")) (PARA (TEXT \"def]\")))"}, {"[^abc[^def]]", "(BLOCK (PARA (ENDNOTE () (TEXT \"abc\") (ENDNOTE () (TEXT \"def\")))))"}, {"[^abc]{-}", "(BLOCK (PARA (ENDNOTE ((\"-\" . \"\")) (TEXT \"abc\"))))"}, }) - checkTcs(t, TestCases{ + checkTcs(t, testCases{ {"[^abc\n\ndef]", "(BLOCK (PARA (TEXT \"[^abc\")) (PARA (TEXT \"def]\")))"}, }) } func TestMark(t *testing.T) { t.Parallel() - checkTcs(t, TestCases{ + checkTcs(t, testCases{ {"[!", "(BLOCK (PARA (TEXT \"[!\")))"}, {"[!\n", "(BLOCK (PARA (TEXT \"[!\")))"}, {"[!]", "(BLOCK (PARA (MARK \"\" \"\" \"\")))"}, {"[!][!]", "(BLOCK (PARA (MARK \"\" \"\" \"\") (MARK \"\" \"\" \"\")))"}, {"[! ]", "(BLOCK (PARA (TEXT \"[! ]\")))"}, @@ -298,11 +312,11 @@ }) } func TestComment(t *testing.T) { t.Parallel() - checkTcs(t, TestCases{ + checkTcs(t, testCases{ {"%", "(BLOCK (PARA (TEXT \"%\")))"}, {"%%", "(BLOCK (PARA (LITERAL-COMMENT () \"\")))"}, {"%\n", "(BLOCK (PARA (TEXT \"%\")))"}, {"%%\n", "(BLOCK (PARA (LITERAL-COMMENT () \"\")))"}, {"%%a", "(BLOCK (PARA (LITERAL-COMMENT () \"a\")))"}, @@ -333,20 +347,20 @@ } 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{ + checkTcs(t, replace(ch, symMap, testCases{ {"$", "(BLOCK (PARA (TEXT \"$\")))"}, {"$$", "(BLOCK (PARA (TEXT \"$$\")))"}, {"$$$", "(BLOCK (PARA (TEXT \"$$$\")))"}, {"$$$$", "(BLOCK (PARA ($% ())))"}, })) } // Not for Quote / '"', because escaped representation. for _, ch := range []string{"_", "*", ">", "~", "^", ",", "#", ":"} { - checkTcs(t, replace(ch, symMap, TestCases{ + checkTcs(t, replace(ch, symMap, testCases{ {"$$a$$", "(BLOCK (PARA ($% () (TEXT \"a\"))))"}, {"$$a$$$", "(BLOCK (PARA ($% () (TEXT \"a\")) (TEXT \"$\")))"}, {"$$$a$$", "(BLOCK (PARA ($% () (TEXT \"$a\"))))"}, {"$$$a$$$", "(BLOCK (PARA ($% () (TEXT \"$a\")) (TEXT \"$\")))"}, {"$\\$", "(BLOCK (PARA (TEXT \"$$\")))"}, @@ -357,15 +371,15 @@ {"$$a\\$$$", "(BLOCK (PARA ($% () (TEXT \"a$\"))))"}, {"$$a\na$$", "(BLOCK (PARA ($% () (TEXT \"a\") (SOFT) (TEXT \"a\"))))"}, {"$$a\n\na$$", "(BLOCK (PARA (TEXT \"$$a\")) (PARA (TEXT \"a$$\")))"}, {"$$a$${go}", "(BLOCK (PARA ($% ((\"go\" . \"\")) (TEXT \"a\"))))"}, })) - checkTcs(t, replace(ch, symMap, TestCases{ + checkTcs(t, replace(ch, symMap, testCases{ {"$$a\n\na$$", "(BLOCK (PARA (TEXT \"$$a\")) (PARA (TEXT \"a$$\")))"}, })) } - checkTcs(t, replace(`"`, symbolMap{`"`: zsx.SymFormatQuote}, TestCases{ + checkTcs(t, replace(`"`, symbolMap{`"`: zsx.SymFormatQuote}, testCases{ {"$", "(BLOCK (PARA (TEXT \"\\\"\")))"}, {"$$", "(BLOCK (PARA (TEXT \"\\\"\\\"\")))"}, {"$$$", "(BLOCK (PARA (TEXT \"\\\"\\\"\\\"\")))"}, {"$$$$", "(BLOCK (PARA ($% ())))"}, @@ -381,11 +395,11 @@ {"$$a\\$$$", "(BLOCK (PARA ($% () (TEXT \"a\\\"\"))))"}, {"$$a\na$$", "(BLOCK (PARA ($% () (TEXT \"a\") (SOFT) (TEXT \"a\"))))"}, {"$$a\n\na$$", "(BLOCK (PARA (TEXT \"\\\"\\\"a\")) (PARA (TEXT \"a\\\"\\\"\")))"}, {"$$a$${go}", "(BLOCK (PARA ($% ((\"go\" . \"\")) (TEXT \"a\"))))"}, })) - checkTcs(t, TestCases{ + checkTcs(t, testCases{ {"__****__", "(BLOCK (PARA (FORMAT-EMPH () (FORMAT-STRONG ()))))"}, {"__**a**__", "(BLOCK (PARA (FORMAT-EMPH () (FORMAT-STRONG () (TEXT \"a\")))))"}, {"__**__**", "(BLOCK (PARA (TEXT \"__\") (FORMAT-STRONG () (TEXT \"__\"))))"}, }) } @@ -396,11 +410,11 @@ "'": zsx.SymLiteralInput, "=": zsx.SymLiteralOutput, } t.Parallel() for _, ch := range []string{"`", "'", "="} { - checkTcs(t, replace(ch, symMap, TestCases{ + checkTcs(t, replace(ch, symMap, testCases{ {"$", "(BLOCK (PARA (TEXT \"$\")))"}, {"$$", "(BLOCK (PARA (TEXT \"$$\")))"}, {"$$$", "(BLOCK (PARA (TEXT \"$$$\")))"}, {"$$$$", "(BLOCK (PARA ($% () \"\")))"}, {"$$a$$", "(BLOCK (PARA ($% () \"a\")))"}, @@ -414,11 +428,11 @@ {"$$a$\\$", "(BLOCK (PARA (TEXT \"$$a$$\")))"}, {"$$a\\$$$", "(BLOCK (PARA ($% () \"a$\")))"}, {"$$a$${go}", "(BLOCK (PARA ($% ((\"go\" . \"\")) \"a\")))"}, })) } - checkTcs(t, TestCases{ + checkTcs(t, testCases{ {"``