DELETED .github/dependabot.yml Index: .github/dependabot.yml ================================================================== --- .github/dependabot.yml +++ /dev/null @@ -1,12 +0,0 @@ -# To get started with Dependabot version updates, you'll need to specify which -# package ecosystems to update and where the package manifests are located. -# Please see the documentation for all configuration options: -# https://help.github.com/github/administering-a-repository/configuration-options-for-dependency-updates - -version: 2 -updates: - - package-ecosystem: "gomod" # See documentation for possible values - directory: "/" # Location of package manifests - schedule: - interval: "daily" - rebase-strategy: "disabled" Index: api/api.go ================================================================== --- api/api.go +++ api/api.go @@ -4,10 +4,13 @@ // This file is part of zettelstore-client. // // Zettelstore client is licensed under the latest version of the EUPL // (European Union Public License). Please see file LICENSE.txt for your rights // and obligations under this license. +// +// SPDX-License-Identifier: EUPL-1.2 +// SPDX-FileCopyrightText: 2021-present Detlef Stern //----------------------------------------------------------------------------- // Package api contains common definitions used for client and server. package api @@ -20,11 +23,11 @@ // IsValid returns true, if the idenfifier contains 14 digits. func (zid ZettelID) IsValid() bool { if len(zid) != 14 { return false } - for i := 0; i < 14; i++ { + for i := range 14 { ch := zid[i] if ch < '0' || '9' < ch { return false } } Index: api/const.go ================================================================== --- api/const.go +++ api/const.go @@ -4,10 +4,13 @@ // This file is part of zettelstore-client. // // Zettelstore client is licensed under the latest version of the EUPL // (European Union Public License). Please see file LICENSE.txt for your rights // and obligations under this license. +// +// SPDX-License-Identifier: EUPL-1.2 +// SPDX-FileCopyrightText: 2021-present Detlef Stern //----------------------------------------------------------------------------- package api import "fmt" @@ -84,11 +87,10 @@ MetaString = "String" MetaTagSet = "TagSet" MetaTimestamp = "Timestamp" MetaURL = "URL" MetaWord = "Word" - MetaWordSet = "WordSet" MetaZettelmarkup = "Zettelmarkup" ) // Predefined general Metadata keys const ( @@ -267,10 +269,11 @@ const ( BackwardDirective = "BACKWARD" ContextDirective = "CONTEXT" CostDirective = "COST" ForwardDirective = "FORWARD" + FullDirective = "FULL" IdentDirective = "IDENT" ItemsDirective = "ITEMS" MaxDirective = "MAX" LimitDirective = "LIMIT" OffsetDirective = "OFFSET" @@ -282,10 +285,20 @@ ReverseDirective = "REVERSE" UnlinkedDirective = "UNLINKED" ActionSeparator = "|" + AtomAction = "ATOM" + KeysAction = "KEYS" + MinAction = "MIN" + MaxAction = "MAX" + NumberedAction = "NUMBERED" + RedirectAction = "REDIRECT" + ReIndexAction = "REINDEX" + RSSAction = "RSS" + TitleAction = "TITLE" + ExistOperator = "?" ExistNotOperator = "!?" SearchOperatorNot = "!" SearchOperatorEqual = "=" @@ -301,5 +314,8 @@ SearchOperatorLess = "<" SearchOperatorNotLess = "!<" SearchOperatorGreater = ">" SearchOperatorNotGreater = "!>" ) + +// QueryPrefix is the prefix that denotes a query expression within a reference. +const QueryPrefix = "query:" Index: api/urlbuilder.go ================================================================== --- api/urlbuilder.go +++ api/urlbuilder.go @@ -4,10 +4,13 @@ // This file is part of zettelstore-client. // // Zettelstore client is licensed under the latest version of the EUPL // (European Union Public License). Please see file LICENSE.txt for your rights // and obligations under this license. +// +// SPDX-License-Identifier: EUPL-1.2 +// SPDX-FileCopyrightText: 2020-present Detlef Stern //----------------------------------------------------------------------------- package api import ( Index: attrs/attrs.go ================================================================== --- attrs/attrs.go +++ attrs/attrs.go @@ -4,10 +4,13 @@ // This file is part of zettelstore-client. // // Zettelstore client is licensed under the latest version of the EUPL // (European Union Public License). Please see file LICENSE.txt for your rights // and obligations under this license. +// +// SPDX-License-Identifier: EUPL-1.2 +// SPDX-FileCopyrightText: 2020-present Detlef Stern //----------------------------------------------------------------------------- // Package attrs stores attributes of zettel parts. package attrs Index: attrs/attrs_test.go ================================================================== --- attrs/attrs_test.go +++ attrs/attrs_test.go @@ -4,10 +4,13 @@ // This file is part of zettelstore-client. // // Zettelstore client is licensed under the latest version of the EUPL // (European Union Public License). Please see file LICENSE.txt for your rights // and obligations under this license. +// +// SPDX-License-Identifier: EUPL-1.2 +// SPDX-FileCopyrightText: 2020-present Detlef Stern //----------------------------------------------------------------------------- package attrs_test import ( Index: client/client.go ================================================================== --- client/client.go +++ client/client.go @@ -4,10 +4,13 @@ // This file is part of zettelstore-client. // // Zettelstore client is licensed under the latest version of the EUPL // (European Union Public License). Please see file LICENSE.txt for your rights // and obligations under this license. +// +// SPDX-License-Identifier: EUPL-1.2 +// SPDX-FileCopyrightText: 2021-present Detlef Stern //----------------------------------------------------------------------------- // Package client provides a client for accessing the Zettelstore via its API. package client @@ -24,10 +27,11 @@ "strings" "time" "zettelstore.de/client.fossil/api" "zettelstore.de/client.fossil/sexp" + "zettelstore.de/client.fossil/sz" "zettelstore.de/sx.fossil" "zettelstore.de/sx.fossil/sxreader" ) // Client contains all data to execute requests. @@ -123,11 +127,12 @@ Message: resp.Status[4:], Body: body, } } -func (c *Client) newURLBuilder(key byte) *api.URLBuilder { +// NewURLBuilder creates a new URL builder for the client with the given key. +func (c *Client) NewURLBuilder(key byte) *api.URLBuilder { return api.NewURLBuilder(c.base, key) } func (*Client) newRequest(ctx context.Context, method string, ub *api.URLBuilder, body io.Reader) (*http.Request, error) { return http.NewRequestWithContext(ctx, method, ub.String(), body) } @@ -187,16 +192,16 @@ } vals, err := sexp.ParseList(obj, "ssi") if err != nil { return err } - token := vals[1].(sx.String).String() + token := string(vals[1].(sx.String)) if len(token) < 4 { return fmt.Errorf("no valid token found: %q", token) } c.token = token - c.tokenType = vals[0].(sx.String).String() + c.tokenType = string(vals[0].(sx.String)) c.expires = time.Now().Add(time.Duration(vals[2].(sx.Int64)*9/10) * time.Second) return nil } func (c *Client) updateToken(ctx context.Context) error { @@ -210,30 +215,30 @@ } // Authenticate sets a new token by sending user name and password. func (c *Client) Authenticate(ctx context.Context) error { authData := url.Values{"username": {c.username}, "password": {c.password}} - req, err := c.newRequest(ctx, http.MethodPost, c.newURLBuilder('a'), strings.NewReader(authData.Encode())) + req, err := c.newRequest(ctx, http.MethodPost, c.NewURLBuilder('a'), strings.NewReader(authData.Encode())) if err != nil { return err } req.Header.Set("Content-Type", "application/x-www-form-urlencoded") return c.executeAuthRequest(req) } // RefreshToken updates the access token func (c *Client) RefreshToken(ctx context.Context) error { - req, err := c.newRequest(ctx, http.MethodPut, c.newURLBuilder('a'), nil) + req, err := c.newRequest(ctx, http.MethodPut, c.NewURLBuilder('a'), nil) if err != nil { return err } return c.executeAuthRequest(req) } // CreateZettel creates a new zettel and returns its URL. func (c *Client) CreateZettel(ctx context.Context, data []byte) (api.ZettelID, error) { - ub := c.newURLBuilder('z') + ub := c.NewURLBuilder('z') resp, err := c.buildAndExecuteRequest(ctx, http.MethodPost, ub, bytes.NewBuffer(data), nil) if err != nil { return api.InvalidZID, err } defer resp.Body.Close() @@ -254,11 +259,11 @@ func (c *Client) CreateZettelData(ctx context.Context, data api.ZettelData) (api.ZettelID, error) { var buf bytes.Buffer if _, err := sx.Print(&buf, sexp.EncodeZettel(data)); err != nil { return api.InvalidZID, err } - ub := c.newURLBuilder('z').AppendKVQuery(api.QueryKeyEncoding, api.EncodingData) + ub := c.NewURLBuilder('z').AppendKVQuery(api.QueryKeyEncoding, api.EncodingData) resp, err := c.buildAndExecuteRequest(ctx, http.MethodPost, ub, &buf, nil) if err != nil { return api.InvalidZID, err } defer resp.Body.Close() @@ -275,11 +280,11 @@ var bsLF = []byte{'\n'} // QueryZettel returns a list of all Zettel. func (c *Client) QueryZettel(ctx context.Context, query string) ([][]byte, error) { - ub := c.newURLBuilder('z').AppendQuery(query) + ub := c.NewURLBuilder('z').AppendQuery(query) resp, err := c.buildAndExecuteRequest(ctx, http.MethodGet, ub, nil, nil) if err != nil { return nil, err } defer resp.Body.Close() @@ -301,11 +306,11 @@ return lines, nil } // QueryZettelData returns a list of zettel metadata. func (c *Client) QueryZettelData(ctx context.Context, query string) (string, string, []api.ZidMetaRights, error) { - ub := c.newURLBuilder('z').AppendKVQuery(api.QueryKeyEncoding, api.EncodingData).AppendQuery(query) + ub := c.NewURLBuilder('z').AppendKVQuery(api.QueryKeyEncoding, api.EncodingData).AppendQuery(query) resp, err := c.buildAndExecuteRequest(ctx, http.MethodGet, ub, nil, nil) if err != nil { return "", "", nil, err } defer resp.Body.Close() @@ -332,11 +337,11 @@ hVals, err := sexp.ParseList(vals[2], "ys") if err != nil { return "", "", nil, err } metaList, err := parseMetaList(vals[3].(*sx.Pair)) - return qVals[1].String(), hVals[1].String(), metaList, err + return sz.GoValue(qVals[1]), sz.GoValue(hVals[1]), metaList, err } func parseMetaList(metaPair *sx.Pair) ([]api.ZidMetaRights, error) { if metaPair == nil { return nil, fmt.Errorf("no zettel list") @@ -406,11 +411,11 @@ return zid, nil } // QueryAggregate returns a aggregate as a result of a query. // It is most often used in a query with an action, where the action is either -// a metadata key of type Word, WordSet, or TagSet. +// a metadata key of type Word or of type TagSet. func (c *Client) QueryAggregate(ctx context.Context, query string) (api.Aggregate, error) { lines, err := c.QueryZettel(ctx, query) if err != nil { return nil, err } @@ -447,11 +452,11 @@ func (c *Client) fetchTagOrRoleZettel(ctx context.Context, key, val string) (api.ZettelID, error) { if c.client.CheckRedirect == nil { panic("client does not allow to track redirect") } - ub := c.newURLBuilder('z').AppendKVQuery(key, val) + ub := c.NewURLBuilder('z').AppendKVQuery(key, val) resp, err := c.buildAndExecuteRequest(ctx, http.MethodGet, ub, nil, nil) if err != nil { return api.InvalidZID, err } defer resp.Body.Close() @@ -474,11 +479,11 @@ } } // GetZettel returns a zettel as a string. func (c *Client) GetZettel(ctx context.Context, zid api.ZettelID, part string) ([]byte, error) { - ub := c.newURLBuilder('z').SetZid(zid) + ub := c.NewURLBuilder('z').SetZid(zid) if part != "" && part != api.PartContent { ub.AppendKVQuery(api.QueryKeyPart, part) } resp, err := c.buildAndExecuteRequest(ctx, http.MethodGet, ub, nil, nil) if err != nil { @@ -496,11 +501,11 @@ return data, err } // GetZettelData returns a zettel as a struct of its parts. func (c *Client) GetZettelData(ctx context.Context, zid api.ZettelID) (api.ZettelData, error) { - ub := c.newURLBuilder('z').SetZid(zid) + ub := c.NewURLBuilder('z').SetZid(zid) ub.AppendKVQuery(api.QueryKeyEncoding, api.EncodingData) ub.AppendKVQuery(api.QueryKeyPart, api.PartZettel) resp, err := c.buildAndExecuteRequest(ctx, http.MethodGet, ub, nil, nil) if err == nil { defer resp.Body.Close() @@ -525,11 +530,11 @@ func (c *Client) GetEvaluatedZettel(ctx context.Context, zid api.ZettelID, enc api.EncodingEnum) ([]byte, error) { return c.getZettelString(ctx, zid, enc, false) } func (c *Client) getZettelString(ctx context.Context, zid api.ZettelID, enc api.EncodingEnum, parseOnly bool) ([]byte, error) { - ub := c.newURLBuilder('z').SetZid(zid) + ub := c.NewURLBuilder('z').SetZid(zid) ub.AppendKVQuery(api.QueryKeyEncoding, enc.String()) ub.AppendKVQuery(api.QueryKeyPart, api.PartContent) if parseOnly { ub.AppendKVQuery(api.QueryKeyParseOnly, "") } @@ -546,21 +551,21 @@ } return io.ReadAll(resp.Body) } // GetParsedSz returns an parsed zettel as a Sexpr-decoded data structure. -func (c *Client) GetParsedSz(ctx context.Context, zid api.ZettelID, part string, sf sx.SymbolFactory) (sx.Object, error) { - return c.getSz(ctx, zid, part, true, sf) +func (c *Client) GetParsedSz(ctx context.Context, zid api.ZettelID, part string) (sx.Object, error) { + return c.getSz(ctx, zid, part, true) } // GetEvaluatedSz returns an evaluated zettel as a Sexpr-decoded data structure. -func (c *Client) GetEvaluatedSz(ctx context.Context, zid api.ZettelID, part string, sf sx.SymbolFactory) (sx.Object, error) { - return c.getSz(ctx, zid, part, false, sf) +func (c *Client) GetEvaluatedSz(ctx context.Context, zid api.ZettelID, part string) (sx.Object, error) { + return c.getSz(ctx, zid, part, false) } -func (c *Client) getSz(ctx context.Context, zid api.ZettelID, part string, parseOnly bool, sf sx.SymbolFactory) (sx.Object, error) { - ub := c.newURLBuilder('z').SetZid(zid) +func (c *Client) getSz(ctx context.Context, zid api.ZettelID, part string, parseOnly bool) (sx.Object, error) { + ub := c.NewURLBuilder('z').SetZid(zid) ub.AppendKVQuery(api.QueryKeyEncoding, api.EncodingSz) if part != "" { ub.AppendKVQuery(api.QueryKeyPart, part) } if parseOnly { @@ -572,16 +577,16 @@ } defer resp.Body.Close() if resp.StatusCode != http.StatusOK { return nil, statusToError(resp) } - return sxreader.MakeReader(bufio.NewReaderSize(resp.Body, 8), sxreader.WithSymbolFactory(sf)).Read() + return sxreader.MakeReader(bufio.NewReaderSize(resp.Body, 8)).Read() } // GetMetaData returns the metadata of a zettel. func (c *Client) GetMetaData(ctx context.Context, zid api.ZettelID) (api.MetaRights, error) { - ub := c.newURLBuilder('z').SetZid(zid) + ub := c.NewURLBuilder('z').SetZid(zid) ub.AppendKVQuery(api.QueryKeyEncoding, api.EncodingData) ub.AppendKVQuery(api.QueryKeyPart, api.PartMeta) resp, err := c.buildAndExecuteRequest(ctx, http.MethodGet, ub, nil, nil) if err != nil { return api.MetaRights{}, err @@ -619,11 +624,11 @@ }, nil } // UpdateZettel updates an existing zettel. func (c *Client) UpdateZettel(ctx context.Context, zid api.ZettelID, data []byte) error { - ub := c.newURLBuilder('z').SetZid(zid) + ub := c.NewURLBuilder('z').SetZid(zid) resp, err := c.buildAndExecuteRequest(ctx, http.MethodPut, ub, bytes.NewBuffer(data), nil) if err != nil { return err } defer resp.Body.Close() @@ -637,11 +642,11 @@ func (c *Client) UpdateZettelData(ctx context.Context, zid api.ZettelID, data api.ZettelData) error { var buf bytes.Buffer if _, err := sx.Print(&buf, sexp.EncodeZettel(data)); err != nil { return err } - ub := c.newURLBuilder('z').SetZid(zid).AppendKVQuery(api.QueryKeyEncoding, api.EncodingData) + ub := c.NewURLBuilder('z').SetZid(zid).AppendKVQuery(api.QueryKeyEncoding, api.EncodingData) resp, err := c.buildAndExecuteRequest(ctx, http.MethodPut, ub, &buf, nil) if err != nil { return err } defer resp.Body.Close() @@ -651,13 +656,13 @@ return nil } // RenameZettel renames a zettel. func (c *Client) RenameZettel(ctx context.Context, oldZid, newZid api.ZettelID) error { - ub := c.newURLBuilder('z').SetZid(oldZid) + ub := c.NewURLBuilder('z').SetZid(oldZid) h := http.Header{ - api.HeaderDestination: {c.newURLBuilder('z').SetZid(newZid).String()}, + api.HeaderDestination: {c.NewURLBuilder('z').SetZid(newZid).String()}, } resp, err := c.buildAndExecuteRequest(ctx, api.MethodMove, ub, nil, h) if err != nil { return err } @@ -668,11 +673,11 @@ return nil } // DeleteZettel deletes a zettel with the given identifier. func (c *Client) DeleteZettel(ctx context.Context, zid api.ZettelID) error { - ub := c.newURLBuilder('z').SetZid(zid) + ub := c.NewURLBuilder('z').SetZid(zid) resp, err := c.buildAndExecuteRequest(ctx, http.MethodDelete, ub, nil, nil) if err != nil { return err } defer resp.Body.Close() @@ -682,11 +687,11 @@ return nil } // ExecuteCommand will execute a given command at the Zettelstore. func (c *Client) ExecuteCommand(ctx context.Context, command api.Command) error { - ub := c.newURLBuilder('x').AppendKVQuery(api.QueryKeyCommand, string(command)) + ub := c.NewURLBuilder('x').AppendKVQuery(api.QueryKeyCommand, string(command)) resp, err := c.buildAndExecuteRequest(ctx, http.MethodPost, ub, nil, nil) if err != nil { return err } defer resp.Body.Close() @@ -696,11 +701,11 @@ return nil } // GetVersionInfo returns version information.. func (c *Client) GetVersionInfo(ctx context.Context) (VersionInfo, error) { - resp, err := c.buildAndExecuteRequest(ctx, http.MethodGet, c.newURLBuilder('x'), nil, nil) + resp, err := c.buildAndExecuteRequest(ctx, http.MethodGet, c.NewURLBuilder('x'), nil, nil) if err != nil { return VersionInfo{}, err } defer resp.Body.Close() if resp.StatusCode != http.StatusOK { @@ -712,12 +717,12 @@ if vals, errVals := sexp.ParseList(obj, "iiiss"); errVals == nil { return VersionInfo{ Major: int(vals[0].(sx.Int64)), Minor: int(vals[1].(sx.Int64)), Patch: int(vals[2].(sx.Int64)), - Info: vals[3].(sx.String).String(), - Hash: vals[4].(sx.String).String(), + Info: string(vals[3].(sx.String)), + Hash: string(vals[4].(sx.String)), }, nil } } return VersionInfo{}, err } @@ -728,5 +733,23 @@ Minor int Patch int Info string Hash string } + +// Get executes a GET request to the given URL and returns the read data. +func (c *Client) Get(ctx context.Context, ub *api.URLBuilder) ([]byte, error) { + resp, err := c.buildAndExecuteRequest(ctx, http.MethodGet, ub, nil, nil) + if err != nil { + return nil, err + } + defer resp.Body.Close() + switch resp.StatusCode { + case http.StatusOK: + case http.StatusNoContent: + return nil, nil + default: + return nil, statusToError(resp) + } + data, err := io.ReadAll(resp.Body) + return data, err +} Index: client/client_test.go ================================================================== --- client/client_test.go +++ client/client_test.go @@ -4,10 +4,13 @@ // This file is part of zettelstore-client. // // Zettelstore client is licensed under the latest version of the EUPL // (European Union Public License). Please see file LICENSE.txt for your rights // and obligations under this license. +// +// SPDX-License-Identifier: EUPL-1.2 +// SPDX-FileCopyrightText: 2022-present Detlef Stern //----------------------------------------------------------------------------- package client_test import ( @@ -17,12 +20,10 @@ "net/url" "testing" "zettelstore.de/client.fossil/api" "zettelstore.de/client.fossil/client" - "zettelstore.de/client.fossil/sz" - "zettelstore.de/sx.fossil" ) func TestZettelList(t *testing.T) { c := getClient() _, err := c.QueryZettel(context.Background(), "") @@ -45,14 +46,11 @@ } } func TestGetSzZettel(t *testing.T) { c := getClient() - sf := sx.MakeMappedFactory(1024) - var zetSyms sz.ZettelSymbols - zetSyms.InitializeZettelSymbols(sf) - value, err := c.GetEvaluatedSz(context.Background(), api.ZidDefaultHome, api.PartContent, sf) + value, err := c.GetEvaluatedSz(context.Background(), api.ZidDefaultHome, api.PartContent) if err != nil { t.Error(err) return } if value.IsNil() { Index: go.mod ================================================================== --- go.mod +++ go.mod @@ -1,5 +1,5 @@ module zettelstore.de/client.fossil -go 1.21 +go 1.22 -require zettelstore.de/sx.fossil v0.0.0-20231130150648-05ef116ba207 +require zettelstore.de/sx.fossil v0.0.0-20240304124557-67e0a1799d1d Index: go.sum ================================================================== --- go.sum +++ go.sum @@ -1,2 +1,2 @@ -zettelstore.de/sx.fossil v0.0.0-20231130150648-05ef116ba207 h1:8ch54z0w53bps6a00NDofEqo3AJ1l7ITXyC3XyLmlY4= -zettelstore.de/sx.fossil v0.0.0-20231130150648-05ef116ba207/go.mod h1:Uw3OLM1ufOM4Xe0G51mvkTDUv2okd+HyDBMx+0ZG7ME= +zettelstore.de/sx.fossil v0.0.0-20240304124557-67e0a1799d1d h1:Gl5ZmdNV5wJsNMIQYjAd/sWLq2ng4NP+eglWU7lQP+I= +zettelstore.de/sx.fossil v0.0.0-20240304124557-67e0a1799d1d/go.mod h1:/iGHxFXoo6GSV04PUkwaLuFrrCa5LMorxD73iLMAruI= ADDED input/entity.go Index: input/entity.go ================================================================== --- /dev/null +++ input/entity.go @@ -0,0 +1,162 @@ +//----------------------------------------------------------------------------- +// Copyright (c) 2022-present Detlef Stern +// +// This file is part of zettelstore-client. +// +// Zettelstore client is licensed under the latest version of the EUPL +// (European Union Public License). Please see file LICENSE.txt for your rights +// and obligations under this license. +// +// SPDX-License-Identifier: EUPL-1.2 +// SPDX-FileCopyrightText: 2022-present Detlef Stern +//----------------------------------------------------------------------------- + +package input + +import ( + "html" + "unicode" +) + +// ScanEntity scans either a named or a numbered entity and returns it as a string. +// +// For numbered entities (like { or ģ) html.UnescapeString returns +// sometimes other values as expected, if the number is not well-formed. This +// may happen because of some strange HTML parsing rules. But these do not +// apply to Zettelmarkup. Therefore, I parse the number here in the code. +func (inp *Input) ScanEntity() (res string, success bool) { + if inp.Ch != '&' { + return "", false + } + pos := inp.Pos + inp.Next() + if inp.Ch == '#' { + inp.Next() + if inp.Ch == 'x' || inp.Ch == 'X' { + return inp.scanEntityBase16() + } + return inp.scanEntityBase10() + } + return inp.scanEntityNamed(pos) +} + +func (inp *Input) scanEntityBase16() (string, bool) { + inp.Next() + if inp.Ch == ';' { + return "", false + } + code := 0 + for { + switch ch := inp.Ch; ch { + case ';': + inp.Next() + if r := rune(code); isValidEntity(r) { + return string(r), true + } + return "", false + case '0', '1', '2', '3', '4', '5', '6', '7', '8', '9': + code = 16*code + int(ch-'0') + case 'a', 'b', 'c', 'd', 'e', 'f': + code = 16*code + int(ch-'a'+10) + case 'A', 'B', 'C', 'D', 'E', 'F': + code = 16*code + int(ch-'A'+10) + default: + return "", false + } + if code > unicode.MaxRune { + return "", false + } + inp.Next() + } +} + +func (inp *Input) scanEntityBase10() (string, bool) { + // Base 10 code + if inp.Ch == ';' { + return "", false + } + code := 0 + for { + switch ch := inp.Ch; ch { + case ';': + inp.Next() + if r := rune(code); isValidEntity(r) { + return string(r), true + } + return "", false + case '0', '1', '2', '3', '4', '5', '6', '7', '8', '9': + code = 10*code + int(ch-'0') + default: + return "", false + } + if code > unicode.MaxRune { + return "", false + } + inp.Next() + } +} +func (inp *Input) scanEntityNamed(pos int) (string, bool) { + for { + switch inp.Ch { + case EOS, '\n', '\r', '&': + return "", false + case ';': + inp.Next() + es := string(inp.Src[pos:inp.Pos]) + ues := html.UnescapeString(es) + if es == ues { + return "", false + } + return ues, true + default: + inp.Next() + } + } +} + +// isValidEntity checks if the given code is valid for an entity. +// +// According to https://html.spec.whatwg.org/multipage/syntax.html#character-references +// ""The numeric character reference forms described above are allowed to reference any code point +// excluding U+000D CR, noncharacters, and controls other than ASCII whitespace."" +func isValidEntity(r rune) bool { + // No C0 control and no "code point in the range U+007F DELETE to U+009F APPLICATION PROGRAM COMMAND, inclusive." + if r < ' ' || ('\u007f' <= r && r <= '\u009f') { + return false + } + + // If below any noncharacter code point, return true + // + // See: https://infra.spec.whatwg.org/#noncharacter + if r < '\ufdd0' { + return true + } + + // First range of noncharacter code points: "(...) in the range U+FDD0 to U+FDEF, inclusive" + if r <= '\ufdef' { + return false + } + + // Other noncharacter code points: + switch r { + case '\uFFFE', '\uFFFF', + '\U0001FFFE', '\U0001FFFF', + '\U0002FFFE', '\U0002FFFF', + '\U0003FFFE', '\U0003FFFF', + '\U0004FFFE', '\U0004FFFF', + '\U0005FFFE', '\U0005FFFF', + '\U0006FFFE', '\U0006FFFF', + '\U0007FFFE', '\U0007FFFF', + '\U0008FFFE', '\U0008FFFF', + '\U0009FFFE', '\U0009FFFF', + '\U000AFFFE', '\U000AFFFF', + '\U000BFFFE', '\U000BFFFF', + '\U000CFFFE', '\U000CFFFF', + '\U000DFFFE', '\U000DFFFF', + '\U000EFFFE', '\U000EFFFF', + '\U000FFFFE', '\U000FFFFF', + '\U0010FFFE', '\U0010FFFF': + return false + } + return true +} ADDED input/entity_test.go Index: input/entity_test.go ================================================================== --- /dev/null +++ input/entity_test.go @@ -0,0 +1,64 @@ +//----------------------------------------------------------------------------- +// Copyright (c) 2020-present Detlef Stern +// +// This file is part of zettelstore-client. +// +// Zettelstore client is licensed under the latest version of the EUPL +// (European Union Public License). Please see file LICENSE.txt for your rights +// and obligations under this license. +// +// SPDX-License-Identifier: EUPL-1.2 +// SPDX-FileCopyrightText: 2020-present Detlef Stern +//----------------------------------------------------------------------------- + +package input_test + +import ( + "testing" + + "zettelstore.de/client.fossil/input" +) + +func TestScanEntity(t *testing.T) { + t.Parallel() + var testcases = []struct { + text string + exp string + }{ + {"", ""}, + {"a", ""}, + {"&", "&"}, + {"!", "!"}, + {"3", "3"}, + {""", "\""}, + } + for id, tc := range testcases { + inp := input.NewInput([]byte(tc.text)) + got, ok := inp.ScanEntity() + if !ok { + if tc.exp != "" { + t.Errorf("ID=%d, text=%q: expected error, but got %q", id, tc.text, got) + } + if inp.Pos != 0 { + t.Errorf("ID=%d, text=%q: input position advances to %d", id, tc.text, inp.Pos) + } + continue + } + if tc.exp != got { + t.Errorf("ID=%d, text=%q: expected %q, but got %q", id, tc.text, tc.exp, got) + } + } +} + +func TestScanIllegalEntity(t *testing.T) { + t.Parallel() + testcases := []string{"", "a", "& Input →", " ", ""} + for i, tc := range testcases { + inp := input.NewInput([]byte(tc)) + got, ok := inp.ScanEntity() + if ok { + t.Errorf("%d: scanning %q was unexpected successful, got %q", i, tc, got) + continue + } + } +} ADDED input/input.go Index: input/input.go ================================================================== --- /dev/null +++ input/input.go @@ -0,0 +1,156 @@ +//----------------------------------------------------------------------------- +// Copyright (c) 2020-present Detlef Stern +// +// This file is part of zettelstore-client. +// +// Zettelstore client is licensed under the latest version of the EUPL +// (European Union Public License). Please see file LICENSE.txt for your rights +// and obligations under this license. +// +// SPDX-License-Identifier: EUPL-1.2 +// SPDX-FileCopyrightText: 2020-present Detlef Stern +//----------------------------------------------------------------------------- + +// Package input provides an abstraction for data to be read. +package input + +import "unicode/utf8" + +// Input is an abstract input source +type Input struct { + // Read-only, will never change + Src []byte // The source string + + // Read-only, will change + Ch rune // current character + Pos int // character position in src + readPos int // reading position (position after current character) +} + +// NewInput creates a new input source. +func NewInput(src []byte) *Input { + inp := &Input{Src: src} + inp.Next() + return inp +} + +// EOS = End of source +const EOS = rune(-1) + +// Next reads the next rune into inp.Ch and returns it too. +func (inp *Input) Next() rune { + if inp.readPos >= len(inp.Src) { + inp.Pos = len(inp.Src) + inp.Ch = EOS + return EOS + } + inp.Pos = inp.readPos + r, w := rune(inp.Src[inp.readPos]), 1 + if r >= utf8.RuneSelf { + r, w = utf8.DecodeRune(inp.Src[inp.readPos:]) + } + inp.readPos += w + inp.Ch = r + return r +} + +// Peek returns the rune following the most recently read rune without +// advancing. If end-of-source was already found peek returns EOS. +func (inp *Input) Peek() rune { + return inp.PeekN(0) +} + +// PeekN returns the n-th rune after the most recently read rune without +// advancing. If end-of-source was already found peek returns EOS. +func (inp *Input) PeekN(n int) rune { + pos := inp.readPos + n + if pos < len(inp.Src) { + r := rune(inp.Src[pos]) + if r >= utf8.RuneSelf { + r, _ = utf8.DecodeRune(inp.Src[pos:]) + } + if r == '\t' { + return ' ' + } + return r + } + return EOS +} + +// Accept checks if the given string is a prefix of the text to be parsed. +// If successful, advance position and current character. +// String must only contain bytes < 128. +// If not successful, everything remains as it is. +func (inp *Input) Accept(s string) bool { + pos := inp.Pos + remaining := len(inp.Src) - pos + if s == "" || len(s) > remaining { + return false + } + // According to internal documentation of bytes.Equal, the string() will not allocate any memory. + if readPos := pos + len(s); s == string(inp.Src[pos:readPos]) { + inp.readPos = readPos + inp.Next() + return true + } + return false +} + +// IsEOLEOS returns true if char is either EOS or EOL. +func IsEOLEOS(ch rune) bool { + switch ch { + case EOS, '\n', '\r': + return true + } + return false +} + +// EatEOL transforms both "\r" and "\r\n" into "\n". +func (inp *Input) EatEOL() { + switch inp.Ch { + case '\r': + if inp.Peek() == '\n' { + inp.Next() + } + inp.Ch = '\n' + inp.Next() + case '\n': + inp.Next() + } +} + +// SetPos allows to reset the read position. +func (inp *Input) SetPos(pos int) { + if inp.Pos != pos { + inp.readPos = pos + inp.Next() + } +} + +// SkipToEOL reads until the next end-of-line. +func (inp *Input) SkipToEOL() { + for { + switch inp.Ch { + case EOS, '\n', '\r': + return + } + inp.Next() + } +} + +// ScanLineContent reads the reaining input stream and interprets it as lines of text. +func (inp *Input) ScanLineContent() []byte { + result := make([]byte, 0, len(inp.Src)-inp.Pos+1) + for { + inp.EatEOL() + posL := inp.Pos + if inp.Ch == EOS { + return result + } + inp.SkipToEOL() + if len(result) > 0 { + result = append(result, '\n') + } + result = append(result, inp.Src[posL:inp.Pos]...) + } +} ADDED input/input_test.go Index: input/input_test.go ================================================================== --- /dev/null +++ input/input_test.go @@ -0,0 +1,68 @@ +//----------------------------------------------------------------------------- +// Copyright (c) 2020-present Detlef Stern +// +// This file is part of zettelstore-client. +// +// Zettelstore client is licensed under the latest version of the EUPL +// (European Union Public License). Please see file LICENSE.txt for your rights +// and obligations under this license. +// +// SPDX-License-Identifier: EUPL-1.2 +// SPDX-FileCopyrightText: 2020-present Detlef Stern +//----------------------------------------------------------------------------- + +// Package input_test provides some unit-tests for reading data. +package input_test + +import ( + "testing" + + "zettelstore.de/client.fossil/input" +) + +func TestEatEOL(t *testing.T) { + t.Parallel() + inp := input.NewInput(nil) + inp.EatEOL() + if inp.Ch != input.EOS { + t.Errorf("No EOS found: %q", inp.Ch) + } + if inp.Pos != 0 { + t.Errorf("Pos != 0: %d", inp.Pos) + } + + inp = input.NewInput([]byte("ABC")) + if inp.Ch != 'A' { + t.Errorf("First ch != 'A', got %q", inp.Ch) + } + inp.EatEOL() + if inp.Ch != 'A' { + t.Errorf("First ch != 'A', got %q", inp.Ch) + } +} + +func TestAccept(t *testing.T) { + t.Parallel() + testcases := []struct { + accept string + src string + acc bool + exp rune + }{ + {"", "", false, input.EOS}, + {"AB", "abc", false, 'a'}, + {"AB", "ABC", true, 'C'}, + {"AB", "AB", true, input.EOS}, + {"AB", "A", false, 'A'}, + } + for i, tc := range testcases { + inp := input.NewInput([]byte(tc.src)) + acc := inp.Accept(tc.accept) + if acc != tc.acc { + t.Errorf("%d: %q.Accept(%q) == %v, but got %v", i, tc.src, tc.accept, tc.acc, acc) + } + if got := inp.Ch; tc.exp != got { + t.Errorf("%d: %q.Accept(%q) should result in run %v, but got %v", i, tc.src, tc.accept, tc.exp, got) + } + } +} ADDED input/runes.go Index: input/runes.go ================================================================== --- /dev/null +++ input/runes.go @@ -0,0 +1,27 @@ +//----------------------------------------------------------------------------- +// Copyright (c) 2020-present Detlef Stern +// +// This file is part of zettelstore-client. +// +// Zettelstore client is licensed under the latest version of the EUPL +// (European Union Public License). Please see file LICENSE.txt for your rights +// and obligations under this license. +// +// SPDX-License-Identifier: EUPL-1.2 +// SPDX-FileCopyrightText: 2020-present Detlef Stern +//----------------------------------------------------------------------------- + +package input + +import "unicode" + +// IsSpace returns true if rune is a whitespace. +func IsSpace(ch rune) bool { + switch ch { + case ' ', '\t': + return true + case '\n', '\r', EOS: + return false + } + return unicode.IsSpace(ch) +} Index: maps/maps.go ================================================================== --- maps/maps.go +++ maps/maps.go @@ -4,10 +4,13 @@ // This file is part of zettelstore-client. // // Zettelstore client is licensed under the latest version of the EUPL // (European Union Public License). Please see file LICENSE.txt for your rights // and obligations under this license. +// +// SPDX-License-Identifier: EUPL-1.2 +// SPDX-FileCopyrightText: 2022-present Detlef Stern //----------------------------------------------------------------------------- package maps import "sort" Index: maps/maps_test.go ================================================================== --- maps/maps_test.go +++ maps/maps_test.go @@ -4,10 +4,13 @@ // This file is part of zettelstore-client. // // Zettelstore client is licensed under the latest version of the EUPL // (European Union Public License). Please see file LICENSE.txt for your rights // and obligations under this license. +// +// SPDX-License-Identifier: EUPL-1.2 +// SPDX-FileCopyrightText: 2022-present Detlef Stern //----------------------------------------------------------------------------- package maps_test import ( Index: sexp/sexp.go ================================================================== --- sexp/sexp.go +++ sexp/sexp.go @@ -4,10 +4,13 @@ // This file is part of zettelstore-client. // // Zettelstore client is licensed under the latest version of the EUPL // (European Union Public License). Please see file LICENSE.txt for your rights // and obligations under this license. +// +// SPDX-License-Identifier: EUPL-1.2 +// SPDX-FileCopyrightText: 2023-present Detlef Stern //----------------------------------------------------------------------------- // Package sexp contains helper function to work with s-expression in an alien // environment. package sexp @@ -21,17 +24,16 @@ "zettelstore.de/sx.fossil" ) // EncodeZettel transforms zettel data into a sx object. func EncodeZettel(zettel api.ZettelData) sx.Object { - sf := sx.MakeMappedFactory(1024) return sx.MakeList( - sf.MustMake("zettel"), - meta2sz(zettel.Meta, sf), - sx.MakeList(sf.MustMake("rights"), sx.Int64(int64(zettel.Rights))), - sx.MakeList(sf.MustMake("encoding"), sx.String(zettel.Encoding)), - sx.MakeList(sf.MustMake("content"), sx.String(zettel.Content)), + sx.MakeSymbol("zettel"), + meta2sz(zettel.Meta), + sx.MakeList(sx.MakeSymbol("rights"), sx.Int64(int64(zettel.Rights))), + sx.MakeList(sx.MakeSymbol("encoding"), sx.String(zettel.Encoding)), + sx.MakeList(sx.MakeSymbol("content"), sx.String(zettel.Content)), ) } func ParseZettel(obj sx.Object) (api.ZettelData, error) { vals, err := ParseList(obj, "ypppp") @@ -69,38 +71,37 @@ } return api.ZettelData{ Meta: meta, Rights: rights, - Encoding: encVals[1].(sx.String).String(), - Content: contentVals[1].(sx.String).String(), + Encoding: string(encVals[1].(sx.String)), + Content: string(contentVals[1].(sx.String)), }, nil } // EncodeMetaRights translates metadata/rights into a sx object. func EncodeMetaRights(mr api.MetaRights) *sx.Pair { - sf := sx.MakeMappedFactory(1024) return sx.MakeList( - sf.MustMake("list"), - meta2sz(mr.Meta, sf), - sx.MakeList(sf.MustMake("rights"), sx.Int64(int64(mr.Rights))), + sx.SymbolList, + meta2sz(mr.Meta), + sx.MakeList(sx.MakeSymbol("rights"), sx.Int64(int64(mr.Rights))), ) } -func meta2sz(m api.ZettelMeta, sf sx.SymbolFactory) sx.Object { - result := sx.Nil().Cons(sf.MustMake("meta")) - curr := result +func meta2sz(m api.ZettelMeta) sx.Object { + var result sx.ListBuilder + result.Add(sx.MakeSymbol("meta")) keys := make([]string, 0, len(m)) for k := range m { keys = append(keys, k) } sort.Strings(keys) for _, k := range keys { - val := sx.MakeList(sf.MustMake(k), sx.String(m[k])) - curr = curr.AppendBang(val) + val := sx.MakeList(sx.MakeSymbol(k), sx.String(m[k])) + result.Add(val) } - return result + return result.List() } // ParseMeta translates the given list to metadata. func ParseMeta(pair *sx.Pair) (api.ZettelMeta, error) { if err := CheckSymbol(pair.Car(), "meta"); err != nil { @@ -110,11 +111,11 @@ for node := pair.Tail(); node != nil; node = node.Tail() { mVals, err := ParseList(node.Car(), "ys") if err != nil { return nil, err } - res[mVals[0].(*sx.Symbol).Name()] = mVals[1].(sx.String).String() + res[(mVals[0].(*sx.Symbol)).GetValue()] = string(mVals[1].(sx.String)) } return res, nil } // ParseRights returns the rights values of the given object. @@ -132,11 +133,11 @@ } return api.ZettelRights(i64), nil } // ParseList parses the given object as a proper list, based on a type specification. -func ParseList(obj sx.Object, spec string) ([]sx.Object, error) { +func ParseList(obj sx.Object, spec string) (sx.Vector, error) { pair, isPair := sx.GetPair(obj) if !isPair { return nil, fmt.Errorf("not a list: %T/%v", obj, obj) } if pair == nil { @@ -144,11 +145,11 @@ return nil, nil } return nil, ErrElementsMissing } - result := make([]sx.Object, 0, len(spec)) + result := make(sx.Vector, 0, len(spec)) node, i := pair, 0 for ; node != nil; i++ { if i >= len(spec) { return nil, ErrNoSpec } @@ -194,10 +195,10 @@ func CheckSymbol(obj sx.Object, name string) error { sym, isSymbol := sx.GetSymbol(obj) if !isSymbol { return fmt.Errorf("object %v/%T is not a symbol", obj, obj) } - if got := sym.Name(); got != name { + if got := sym.GetValue(); got != name { return fmt.Errorf("symbol %q expected, but got: %q", name, got) } return nil } Index: sexp/sexp_test.go ================================================================== --- sexp/sexp_test.go +++ sexp/sexp_test.go @@ -4,10 +4,13 @@ // This file is part of zettelstore-client. // // Zettelstore client is licensed under the latest version of the EUPL // (European Union Public License). Please see file LICENSE.txt for your rights // and obligations under this license. +// +// SPDX-License-Identifier: EUPL-1.2 +// SPDX-FileCopyrightText: 2023-present Detlef Stern //----------------------------------------------------------------------------- package sexp_test import ( ADDED shtml/const.go Index: shtml/const.go ================================================================== --- /dev/null +++ shtml/const.go @@ -0,0 +1,83 @@ +//----------------------------------------------------------------------------- +// Copyright (c) 2024-present Detlef Stern +// +// This file is part of zettelstore-client. +// +// Zettelstore client is licensed under the latest version of the EUPL +// (European Union Public License). Please see file LICENSE.txt for your rights +// and obligations under this license. +// +// SPDX-License-Identifier: EUPL-1.2 +// SPDX-FileCopyrightText: 2024-present Detlef Stern +//----------------------------------------------------------------------------- + +package shtml + +import "zettelstore.de/sx.fossil" + +// 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") +) + +// 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") + symTR = sx.MakeSymbol("tr") + SymUL = sx.MakeSymbol("ul") +) + +// Symbols for HTML attribute keys +var ( + symAttrAlt = sx.MakeSymbol("alt") + SymAttrClass = sx.MakeSymbol("class") + SymAttrHref = sx.MakeSymbol("href") + SymAttrId = sx.MakeSymbol("id") + SymAttrLang = sx.MakeSymbol("lang") + SymAttrOpen = sx.MakeSymbol("open") + SymAttrRel = sx.MakeSymbol("rel") + SymAttrRole = sx.MakeSymbol("role") + SymAttrSrc = sx.MakeSymbol("src") + SymAttrTarget = sx.MakeSymbol("target") + SymAttrTitle = sx.MakeSymbol("title") + SymAttrType = sx.MakeSymbol("type") + SymAttrValue = sx.MakeSymbol("value") +) Index: shtml/shtml.go ================================================================== --- shtml/shtml.go +++ shtml/shtml.go @@ -4,10 +4,13 @@ // This file is part of zettelstore-client. // // Zettelstore client is licensed under the latest version of the EUPL // (European Union Public License). Please see file LICENSE.txt for your rights // and obligations under this license. +// +// SPDX-License-Identifier: EUPL-1.2 +// SPDX-FileCopyrightText: 2023-present Detlef Stern //----------------------------------------------------------------------------- // Package shtml transforms a s-expr encoded zettel AST into a s-expr representation of HTML. package shtml @@ -26,70 +29,37 @@ ) // Evaluator will transform a s-expression that encodes the zettel AST into an s-expression // that represents HTML. type Evaluator struct { - sf sx.SymbolFactory headingOffset int64 unique string noLinks bool // true iff output must not include links - symAttr *sx.Symbol - symList *sx.Symbol - symNoEscape *sx.Symbol - symClass *sx.Symbol - symMeta *sx.Symbol - symP *sx.Symbol - symLI *sx.Symbol - symA *sx.Symbol - symHREF *sx.Symbol - symSpan *sx.Symbol - fns map[string]EvalFn minArgs map[string]int } // NewEvaluator creates a new Evaluator object. -func NewEvaluator(headingOffset int, sf sx.SymbolFactory) *Evaluator { - if sf == nil { - sf = sx.MakeMappedFactory(128) - } +func NewEvaluator(headingOffset int) *Evaluator { ev := &Evaluator{ - sf: sf, headingOffset: int64(headingOffset), - symAttr: sf.MustMake(sxhtml.NameSymAttr), - symList: sf.MustMake(sxhtml.NameSymList), - symNoEscape: sf.MustMake(sxhtml.NameSymNoEscape), - symClass: sf.MustMake("class"), - symMeta: sf.MustMake("meta"), - symP: sf.MustMake("p"), - symLI: sf.MustMake("li"), - symA: sf.MustMake("a"), - symHREF: sf.MustMake("href"), - symSpan: sf.MustMake("span"), fns: make(map[string]EvalFn, 128), minArgs: make(map[string]int, 128), } - ev.bindCommon() ev.bindMetadata() ev.bindBlocks() ev.bindInlines() return ev } // SetUnique sets a prefix to make several HTML ids unique. func (tr *Evaluator) SetUnique(s string) { tr.unique = s } -// SymbolFactory returns the symbol factory of this evaluator. -func (ev *Evaluator) SymbolFactory() sx.SymbolFactory { return ev.sf } - // IsValidName returns true, if name is a valid symbol name. -func (tr *Evaluator) IsValidName(s string) bool { return tr.sf.IsValidName(s) } - -// Make a new HTML symbol. -func (tr *Evaluator) Make(s string) *sx.Symbol { return tr.sf.MustMake(s) } +func (tr *Evaluator) IsValidName(s string) bool { return s != "" } // EvaluateAttrbute transforms the given attributes into a HTML s-expression. func (tr *Evaluator) EvaluateAttrbute(a attrs.Attributes) *sx.Pair { if len(a) == 0 { return nil @@ -97,84 +67,91 @@ plist := sx.Nil() keys := a.Keys() for i := len(keys) - 1; i >= 0; i-- { key := keys[i] if key != attrs.DefaultAttribute && tr.IsValidName(key) { - plist = plist.Cons(sx.Cons(tr.Make(key), sx.String(a[key]))) + plist = plist.Cons(sx.Cons(sx.MakeSymbol(key), sx.String(a[key]))) } } if plist == nil { return nil } - return plist.Cons(tr.symAttr) + 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) + result := ev.Eval(lst, env) if err := env.err; err != nil { return nil, err } pair, isPair := sx.GetPair(result) if !isPair { return nil, fmt.Errorf("evaluation does not result in a pair, but %T/%v", result, result) } for i := 0; i < len(env.endnotes); i++ { - // May extend tr.endnotes + // May extend tr.endnotes -> do not use for i := range len(...)!!! if env.endnotes[i].noteHx != nil { continue } - objHx := ev.eval(env.endnotes[i].noteAST, env) - if env.err != nil { - break - } - noteHx, isHx := sx.GetPair(objHx) - if !isHx { - return nil, fmt.Errorf("endnote evaluation does not result in pair, but %T/%v", objHx, objHx) - } + noteHx, _ := ev.EvaluateList(env.endnotes[i].noteAST, env) env.endnotes[i].noteHx = noteHx } return pair, nil } + +// EvaluateList will evaluate all list elements separately and returns them as a sx.Pair list +func (ev *Evaluator) EvaluateList(lst sx.Vector, env *Environment) (*sx.Pair, error) { + var result sx.ListBuilder + for _, elem := range lst { + p := ev.Eval(elem, env) + result.Add(p) + } + if err := env.err; err != nil { + return nil, err + } + return result.List(), nil +} // Endnotes returns a SHTML object with all collected endnotes. func (ev *Evaluator) Endnotes(env *Environment) *sx.Pair { if env.err != nil || len(env.endnotes) == 0 { return nil } - result := sx.Nil().Cons(ev.Make("ol")) - symValue, symId, symRole := ev.Make("value"), ev.Make("id"), ev.Make("role") - - currResult := result.AppendBang(sx.Nil().Cons(sx.Cons(ev.symClass, sx.String("zs-endnotes"))).Cons(ev.symAttr)) + var result sx.ListBuilder + result.Add(SymOL) + result.Add(sx.Nil().Cons(sx.Cons(SymAttrClass, sx.String("zs-endnotes"))).Cons(sxhtml.SymAttr)) for i, fni := range env.endnotes { noteNum := strconv.Itoa(i + 1) - attrs := fni.attrs.Cons(sx.Cons(ev.symClass, sx.String("zs-endnote"))). - Cons(sx.Cons(symValue, sx.String(noteNum))). - Cons(sx.Cons(symId, sx.String("fn:"+fni.noteID))). - Cons(sx.Cons(symRole, sx.String("doc-endnote"))). - Cons(ev.symAttr) + attrs := fni.attrs.Cons(sx.Cons(SymAttrClass, sx.String("zs-endnote"))). + Cons(sx.Cons(SymAttrValue, sx.String(noteNum))). + Cons(sx.Cons(SymAttrId, sx.String("fn:"+fni.noteID))). + Cons(sx.Cons(SymAttrRole, sx.String("doc-endnote"))). + Cons(sxhtml.SymAttr) backref := sx.Nil().Cons(sx.String("\u21a9\ufe0e")). Cons(sx.Nil(). - Cons(sx.Cons(ev.symClass, sx.String("zs-endnote-backref"))). - Cons(sx.Cons(ev.symHREF, sx.String("#fnref:"+fni.noteID))). - Cons(sx.Cons(symRole, sx.String("doc-backlink"))). - Cons(ev.symAttr)). - Cons(ev.symA) - - li := sx.Nil().Cons(ev.symLI) - li.AppendBang(attrs). - ExtendBang(fni.noteHx). - AppendBang(sx.String(" ")).AppendBang(backref) - currResult = currResult.AppendBang(li) - } - return result + Cons(sx.Cons(SymAttrClass, sx.String("zs-endnote-backref"))). + Cons(sx.Cons(SymAttrHref, sx.String("#fnref:"+fni.noteID))). + Cons(sx.Cons(SymAttrRole, sx.String("doc-backlink"))). + Cons(sxhtml.SymAttr)). + Cons(SymA) + + var li sx.ListBuilder + li.Add(SymLI) + li.Add(attrs) + li.ExtendBang(fni.noteHx) + li.Add(sx.String(" ")) + li.Add(backref) + result.Add(li.List()) + } + return result.List() } // Environment where sz objects are evaluated to shtml objects type Environment struct { err error @@ -182,11 +159,11 @@ endnotes []endnoteInfo quoteNesting uint } type endnoteInfo struct { noteID string // link id - noteAST sx.Object // Endnote as AST + noteAST sx.Vector // Endnote as list of AST inline elements attrs *sx.Pair // attrs a-list noteHx *sx.Pair // Endnote as SxHTML } // MakeEnvironment builds a new evaluation environment. @@ -229,250 +206,243 @@ func (env *Environment) getLanguage() string { return env.langStack[len(env.langStack)-1] } // EvalFn is a function to be called for evaluation. -type EvalFn func([]sx.Object, *Environment) sx.Object +type EvalFn func(sx.Vector, *Environment) sx.Object -func (ev *Evaluator) bind(name string, minArgs int, fn EvalFn) { - ev.fns[name] = fn +func (ev *Evaluator) bind(sym *sx.Symbol, minArgs int, fn EvalFn) { + symVal := sym.GetValue() + ev.fns[symVal] = fn if minArgs > 0 { - ev.minArgs[name] = minArgs + ev.minArgs[symVal] = minArgs } } // ResolveBinding returns the function bound to the given name. -func (ev *Evaluator) ResolveBinding(name string) EvalFn { - if fn, found := ev.fns[name]; found { +func (ev *Evaluator) ResolveBinding(sym *sx.Symbol) EvalFn { + if fn, found := ev.fns[sym.GetValue()]; found { return fn } return nil } // Rebind overwrites a binding, but leaves the minimum number of arguments intact. -func (ev *Evaluator) Rebind(name string, fn EvalFn) { - if _, found := ev.fns[name]; !found { - panic(name) - } - ev.fns[name] = fn -} - -func (ev *Evaluator) bindCommon() { - ev.bind(sx.ListName, 0, ev.evalList) - ev.bind("quote", 1, func(args []sx.Object, _ *Environment) sx.Object { return args[0] }) +func (ev *Evaluator) Rebind(sym *sx.Symbol, fn EvalFn) { + symVal := sym.GetValue() + if _, found := ev.fns[symVal]; !found { + panic(sym) + } + ev.fns[symVal] = fn } func (ev *Evaluator) bindMetadata() { - ev.bind(sz.NameSymMeta, 0, ev.evalList) - evalMetaString := func(args []sx.Object, env *Environment) sx.Object { + ev.bind(sz.SymMeta, 0, ev.evalList) + evalMetaString := func(args sx.Vector, env *Environment) sx.Object { a := make(attrs.Attributes, 2). - Set("name", ev.getSymbol(ev.eval(args[0], env), env).Name()). - Set("content", getString(args[1], env).String()) + Set("name", ev.getSymbol(args[0], env).GetValue()). + Set("content", string(getString(args[1], env))) return ev.EvaluateMeta(a) } - ev.bind(sz.NameSymTypeCredential, 2, evalMetaString) - ev.bind(sz.NameSymTypeEmpty, 2, evalMetaString) - ev.bind(sz.NameSymTypeID, 2, evalMetaString) - ev.bind(sz.NameSymTypeNumber, 2, evalMetaString) - ev.bind(sz.NameSymTypeString, 2, evalMetaString) - ev.bind(sz.NameSymTypeTimestamp, 2, evalMetaString) - ev.bind(sz.NameSymTypeURL, 2, evalMetaString) - ev.bind(sz.NameSymTypeWord, 2, evalMetaString) - - evalMetaSet := func(args []sx.Object, env *Environment) sx.Object { + ev.bind(sz.SymTypeCredential, 2, evalMetaString) + ev.bind(sz.SymTypeEmpty, 2, evalMetaString) + ev.bind(sz.SymTypeID, 2, evalMetaString) + ev.bind(sz.SymTypeNumber, 2, evalMetaString) + ev.bind(sz.SymTypeString, 2, evalMetaString) + ev.bind(sz.SymTypeTimestamp, 2, evalMetaString) + ev.bind(sz.SymTypeURL, 2, evalMetaString) + ev.bind(sz.SymTypeWord, 2, evalMetaString) + + evalMetaSet := func(args sx.Vector, env *Environment) sx.Object { var sb strings.Builder - lst := ev.eval(args[1], env) - for elem := getList(lst, env); elem != nil; elem = elem.Tail() { + for elem := getList(args[1], env); elem != nil; elem = elem.Tail() { sb.WriteByte(' ') - sb.WriteString(getString(elem.Car(), env).String()) + sb.WriteString(string(getString(elem.Car(), env))) } s := sb.String() if len(s) > 0 { s = s[1:] } a := make(attrs.Attributes, 2). - Set("name", ev.getSymbol(ev.eval(args[0], env), env).Name()). + Set("name", ev.getSymbol(args[0], env).GetValue()). Set("content", s) return ev.EvaluateMeta(a) } - ev.bind(sz.NameSymTypeIDSet, 2, evalMetaSet) - ev.bind(sz.NameSymTypeTagSet, 2, evalMetaSet) - ev.bind(sz.NameSymTypeWordSet, 2, evalMetaSet) - ev.bind(sz.NameSymTypeZettelmarkup, 2, func(args []sx.Object, env *Environment) sx.Object { + ev.bind(sz.SymTypeIDSet, 2, evalMetaSet) + ev.bind(sz.SymTypeTagSet, 2, evalMetaSet) + ev.bind(sz.SymTypeZettelmarkup, 2, func(args sx.Vector, env *Environment) sx.Object { a := make(attrs.Attributes, 2). - Set("name", ev.getSymbol(ev.eval(args[0], env), env).String()). + Set("name", ev.getSymbol(args[0], env).GetValue()). Set("content", text.EvaluateInlineString(getList(args[1], env))) return ev.EvaluateMeta(a) }) } // EvaluateMeta returns HTML meta object for an attribute. func (ev *Evaluator) EvaluateMeta(a attrs.Attributes) *sx.Pair { - return sx.Nil().Cons(ev.EvaluateAttrbute(a)).Cons(ev.symMeta) + return sx.Nil().Cons(ev.EvaluateAttrbute(a)).Cons(SymMeta) } func (ev *Evaluator) bindBlocks() { - ev.bind(sz.NameSymBlock, 0, ev.evalList) - ev.bind(sz.NameSymPara, 0, func(args []sx.Object, env *Environment) sx.Object { - return ev.evalSlice(args, env).Cons(ev.symP) + ev.bind(sz.SymBlock, 0, ev.evalList) + ev.bind(sz.SymPara, 0, func(args sx.Vector, env *Environment) sx.Object { + return ev.evalSlice(args, env).Cons(SymP) }) - ev.bind(sz.NameSymHeading, 5, func(args []sx.Object, env *Environment) sx.Object { + ev.bind(sz.SymHeading, 5, func(args sx.Vector, env *Environment) sx.Object { nLevel := getInt64(args[0], env) if nLevel <= 0 { env.err = fmt.Errorf("%v is a negative level", nLevel) return sx.Nil() } level := strconv.FormatInt(nLevel+ev.headingOffset, 10) + headingSymbol := sx.MakeSymbol("h" + level) - a := ev.getAttributes(args[1], env) + a := ev.GetAttributes(args[1], env) env.pushAttributes(a) defer env.popAttributes() - if fragment := getString(args[3], env).String(); fragment != "" { + if fragment := string(getString(args[3], env)); fragment != "" { a = a.Set("id", ev.unique+fragment) } - if result, isPair := sx.GetPair(ev.eval(args[4], env)); isPair && result != nil { + if result, _ := ev.EvaluateList(args[4:], env); result != nil { if len(a) > 0 { result = result.Cons(ev.EvaluateAttrbute(a)) } - return result.Cons(ev.Make("h" + level)) + return result.Cons(headingSymbol) } - return sx.MakeList(ev.Make("h"+level), sx.String("")) + return sx.MakeList(headingSymbol, sx.String("")) }) - ev.bind(sz.NameSymThematic, 0, func(args []sx.Object, env *Environment) sx.Object { + ev.bind(sz.SymThematic, 0, func(args sx.Vector, env *Environment) sx.Object { result := sx.Nil() if len(args) > 0 { - if attrList := getList(ev.eval(args[0], env), env); attrList != nil { + if attrList := getList(args[0], env); attrList != nil { result = result.Cons(ev.EvaluateAttrbute(sz.GetAttributes(attrList))) } } - return result.Cons(ev.Make("hr")) + return result.Cons(SymHR) }) - ev.bind(sz.NameSymListOrdered, 0, ev.makeListFn("ol")) - ev.bind(sz.NameSymListUnordered, 0, ev.makeListFn("ul")) - ev.bind(sz.NameSymDescription, 0, func(args []sx.Object, env *Environment) sx.Object { + ev.bind(sz.SymListOrdered, 0, ev.makeListFn(SymOL)) + ev.bind(sz.SymListUnordered, 0, ev.makeListFn(SymUL)) + ev.bind(sz.SymDescription, 0, func(args sx.Vector, env *Environment) sx.Object { if len(args) == 0 { return sx.Nil() } - items := sx.Nil().Cons(ev.Make("dl")) - curItem := items + var items sx.ListBuilder + items.Add(symDL) for pos := 0; pos < len(args); pos++ { - term := getList(ev.eval(args[pos], env), env) - curItem = curItem.AppendBang(term.Cons(ev.Make("dt"))) + term := ev.evalDescriptionTerm(getList(args[pos], env), env) + items.Add(term.Cons(symDT)) pos++ if pos >= len(args) { break } - ddBlock := getList(ev.eval(args[pos], env), env) + ddBlock := getList(ev.Eval(args[pos], env), env) if ddBlock == nil { continue } for ddlst := ddBlock; ddlst != nil; ddlst = ddlst.Tail() { dditem := getList(ddlst.Car(), env) - curItem = curItem.AppendBang(dditem.Cons(ev.Make("dd"))) + items.Add(dditem.Cons(symDD)) } } - return items + return items.List() }) - ev.bind(sz.NameSymListQuote, 0, func(args []sx.Object, env *Environment) sx.Object { + ev.bind(sz.SymListQuote, 0, func(args sx.Vector, env *Environment) sx.Object { if args == nil { return sx.Nil() } - result := sx.Nil().Cons(ev.Make("blockquote")) - currResult := result + var result sx.ListBuilder + result.Add(symBLOCKQUOTE) for _, elem := range args { - if quote, isPair := sx.GetPair(ev.eval(elem, env)); isPair { - currResult = currResult.AppendBang(quote.Cons(ev.symP)) + if quote, isPair := sx.GetPair(ev.Eval(elem, env)); isPair { + result.Add(quote.Cons(sxhtml.SymListSplice)) } } - return result + return result.List() }) - ev.bind(sz.NameSymTable, 1, func(args []sx.Object, env *Environment) sx.Object { + ev.bind(sz.SymTable, 1, func(args sx.Vector, env *Environment) sx.Object { thead := sx.Nil() - if header := getList(ev.eval(args[0], env), env); !sx.IsNil(header) { - thead = sx.Nil().Cons(ev.evalTableRow(header)).Cons(ev.Make("thead")) + if header := getList(args[0], env); !sx.IsNil(header) { + thead = sx.Nil().Cons(ev.evalTableRow(header, env)).Cons(symTHEAD) } - tbody := sx.Nil() + var tbody sx.ListBuilder if len(args) > 1 { - tbody = sx.Nil().Cons(ev.Make("tbody")) - curBody := tbody + tbody.Add(symTBODY) for _, row := range args[1:] { - curBody = curBody.AppendBang(ev.evalTableRow(getList(ev.eval(row, env), env))) + tbody.Add(ev.evalTableRow(getList(row, env), env)) } } table := sx.Nil() - if tbody != nil { - table = table.Cons(tbody) + if !tbody.IsEmpty() { + table = table.Cons(tbody.List()) } if thead != nil { table = table.Cons(thead) } if table == nil { return sx.Nil() } - return table.Cons(ev.Make("table")) - }) - ev.bind(sz.NameSymCell, 0, ev.makeCellFn("")) - ev.bind(sz.NameSymCellCenter, 0, ev.makeCellFn("center")) - ev.bind(sz.NameSymCellLeft, 0, ev.makeCellFn("left")) - ev.bind(sz.NameSymCellRight, 0, ev.makeCellFn("right")) - - symDiv := ev.Make("div") - ev.bind(sz.NameSymRegionBlock, 2, ev.makeRegionFn(symDiv, true)) - ev.bind(sz.NameSymRegionQuote, 2, ev.makeRegionFn(ev.Make("blockquote"), false)) - ev.bind(sz.NameSymRegionVerse, 2, ev.makeRegionFn(symDiv, false)) - - ev.bind(sz.NameSymVerbatimComment, 1, func(args []sx.Object, env *Environment) sx.Object { - if ev.getAttributes(args[0], env).HasDefault() { + return table.Cons(symTABLE) + }) + ev.bind(sz.SymCell, 0, ev.makeCellFn("")) + ev.bind(sz.SymCellCenter, 0, ev.makeCellFn("center")) + ev.bind(sz.SymCellLeft, 0, ev.makeCellFn("left")) + ev.bind(sz.SymCellRight, 0, ev.makeCellFn("right")) + + ev.bind(sz.SymRegionBlock, 2, ev.makeRegionFn(SymDIV, true)) + ev.bind(sz.SymRegionQuote, 2, ev.makeRegionFn(symBLOCKQUOTE, false)) + ev.bind(sz.SymRegionVerse, 2, ev.makeRegionFn(SymDIV, false)) + + ev.bind(sz.SymVerbatimComment, 1, func(args sx.Vector, env *Environment) sx.Object { + if ev.GetAttributes(args[0], env).HasDefault() { if len(args) > 1 { if s := getString(args[1], env); s != "" { - t := sx.String(s.String()) - return sx.Nil().Cons(t).Cons(ev.Make(sxhtml.NameSymBlockComment)) + return sx.Nil().Cons(s).Cons(sxhtml.SymBlockComment) } } } return nil }) - ev.bind(sz.NameSymVerbatimEval, 2, func(args []sx.Object, env *Environment) sx.Object { - return ev.evalVerbatim(ev.getAttributes(args[0], env).AddClass("zs-eval"), getString(args[1], env)) - }) - ev.bind(sz.NameSymVerbatimHTML, 2, ev.evalHTML) - ev.bind(sz.NameSymVerbatimMath, 2, func(args []sx.Object, env *Environment) sx.Object { - return ev.evalVerbatim(ev.getAttributes(args[0], env).AddClass("zs-math"), getString(args[1], env)) - }) - ev.bind(sz.NameSymVerbatimProg, 2, func(args []sx.Object, env *Environment) sx.Object { - a := ev.getAttributes(args[0], env) + ev.bind(sz.SymVerbatimEval, 2, func(args sx.Vector, env *Environment) sx.Object { + return ev.evalVerbatim(ev.GetAttributes(args[0], env).AddClass("zs-eval"), getString(args[1], env)) + }) + ev.bind(sz.SymVerbatimHTML, 2, ev.evalHTML) + ev.bind(sz.SymVerbatimMath, 2, func(args sx.Vector, env *Environment) sx.Object { + return ev.evalVerbatim(ev.GetAttributes(args[0], env).AddClass("zs-math"), getString(args[1], env)) + }) + ev.bind(sz.SymVerbatimProg, 2, func(args sx.Vector, env *Environment) sx.Object { + a := ev.GetAttributes(args[0], env) content := getString(args[1], env) if a.HasDefault() { - content = sx.String(visibleReplacer.Replace(content.String())) + content = sx.String(visibleReplacer.Replace(string(content))) } return ev.evalVerbatim(a, content) }) - ev.bind(sz.NameSymVerbatimZettel, 0, nilFn) - ev.bind(sz.NameSymBLOB, 3, func(args []sx.Object, env *Environment) sx.Object { + ev.bind(sz.SymVerbatimZettel, 0, nilFn) + ev.bind(sz.SymBLOB, 3, func(args sx.Vector, env *Environment) sx.Object { return ev.evalBLOB(getList(args[0], env), getString(args[1], env), getString(args[2], env)) }) - ev.bind(sz.NameSymTransclude, 2, func(args []sx.Object, env *Environment) sx.Object { - ref, isPair := sx.GetPair(ev.eval(args[1], env)) + ev.bind(sz.SymTransclude, 2, func(args sx.Vector, env *Environment) sx.Object { + ref, isPair := sx.GetPair(args[1]) if !isPair { return sx.Nil() } refKind := ref.Car() if sx.IsNil(refKind) { return sx.Nil() } if refValue := getString(ref.Tail().Car(), env); refValue != "" { - if refSym, isRefSym := sx.GetSymbol(refKind); isRefSym && refSym.Name() == sz.NameSymRefStateExternal { - a := ev.getAttributes(args[0], env).Set("src", refValue.String()).AddClass("external") - return sx.Nil().Cons(sx.Nil().Cons(ev.EvaluateAttrbute(a)).Cons(ev.Make("img"))).Cons(ev.symP) + if refSym, isRefSym := sx.GetSymbol(refKind); isRefSym && refSym.IsEqual(sz.SymRefStateExternal) { + a := ev.GetAttributes(args[0], env).Set("src", string(refValue)).AddClass("external") + return sx.Nil().Cons(sx.Nil().Cons(ev.EvaluateAttrbute(a)).Cons(SymIMG)).Cons(SymP) } return sx.MakeList( - ev.Make(sxhtml.NameSymInlineComment), + sxhtml.SymInlineComment, sx.String("transclude"), refKind, sx.String("->"), refValue, ) @@ -479,189 +449,197 @@ } return ev.evalSlice(args, env) }) } -func (ev *Evaluator) makeListFn(tag string) EvalFn { - sym := ev.Make(tag) - return func(args []sx.Object, env *Environment) sx.Object { - result := sx.Nil().Cons(sym) - last := result +func (ev *Evaluator) makeListFn(sym *sx.Symbol) EvalFn { + return func(args sx.Vector, env *Environment) sx.Object { + var result sx.ListBuilder + result.Add(sym) for _, elem := range args { - item := sx.Nil().Cons(ev.symLI) - if res, isPair := sx.GetPair(ev.eval(elem, env)); isPair { + item := sx.Nil().Cons(SymLI) + if res, isPair := sx.GetPair(ev.Eval(elem, env)); isPair { item.ExtendBang(res) } - last = last.AppendBang(item) + result.Add(item) } - return result + return result.List() + } +} + +func (ev *Evaluator) evalDescriptionTerm(term *sx.Pair, env *Environment) *sx.Pair { + var result sx.ListBuilder + for node := term; node != nil; node = node.Tail() { + elem := ev.Eval(node.Car(), env) + result.Add(elem) } + return result.List() } -func (ev *Evaluator) evalTableRow(pairs *sx.Pair) *sx.Pair { - row := sx.Nil().Cons(ev.Make("tr")) +func (ev *Evaluator) evalTableRow(pairs *sx.Pair, env *Environment) *sx.Pair { if pairs == nil { return nil } - curRow := row + var row sx.ListBuilder + row.Add(symTR) for pair := pairs; pair != nil; pair = pair.Tail() { - curRow = curRow.AppendBang(pair.Car()) + row.Add(ev.Eval(pair.Car(), env)) } - return row + return row.List() } func (ev *Evaluator) makeCellFn(align string) EvalFn { - return func(args []sx.Object, env *Environment) sx.Object { + return func(args sx.Vector, env *Environment) sx.Object { tdata := ev.evalSlice(args, env) if align != "" { tdata = tdata.Cons(ev.EvaluateAttrbute(attrs.Attributes{"class": align})) } - return tdata.Cons(ev.Make("td")) + return tdata.Cons(symTD) } } func (ev *Evaluator) makeRegionFn(sym *sx.Symbol, genericToClass bool) EvalFn { - return func(args []sx.Object, env *Environment) sx.Object { - a := ev.getAttributes(args[0], env) + return func(args sx.Vector, env *Environment) sx.Object { + a := ev.GetAttributes(args[0], env) env.pushAttributes(a) defer env.popAttributes() if genericToClass { if val, found := a.Get(""); found { a = a.Remove("").AddClass(val) } } - result := sx.Nil() + var result sx.ListBuilder + result.Add(sym) if len(a) > 0 { - result = result.Cons(ev.EvaluateAttrbute(a)) + result.Add(ev.EvaluateAttrbute(a)) } - result = result.Cons(sym) - currResult := result.LastPair() - if region, isPair := sx.GetPair(ev.eval(args[1], env)); isPair { - currResult = currResult.ExtendBang(region) + if region, isPair := sx.GetPair(args[1]); isPair { + if evalRegion := ev.EvalPairList(region, env); evalRegion != nil { + result.ExtendBang(evalRegion) + } } if len(args) > 2 { - if cite, isPair := sx.GetPair(ev.eval(args[2], env)); isPair && cite != nil { - currResult.AppendBang(cite.Cons(ev.Make("cite"))) + if cite, _ := ev.EvaluateList(args[2:], env); cite != nil { + result.Add(cite.Cons(symCITE)) } } - return result + return result.List() } } func (ev *Evaluator) evalVerbatim(a attrs.Attributes, s sx.String) sx.Object { a = setProgLang(a) code := sx.Nil().Cons(s) if al := ev.EvaluateAttrbute(a); al != nil { code = code.Cons(al) } - code = code.Cons(ev.Make("code")) - return sx.Nil().Cons(code).Cons(ev.Make("pre")) + code = code.Cons(symCODE) + return sx.Nil().Cons(code).Cons(symPRE) } func (ev *Evaluator) bindInlines() { - ev.bind(sz.NameSymInline, 0, ev.evalList) - ev.bind(sz.NameSymText, 1, func(args []sx.Object, env *Environment) sx.Object { return getString(args[0], env) }) - ev.bind(sz.NameSymSpace, 0, func(args []sx.Object, env *Environment) sx.Object { + ev.bind(sz.SymInline, 0, ev.evalList) + ev.bind(sz.SymText, 1, func(args sx.Vector, env *Environment) sx.Object { return getString(args[0], env) }) + ev.bind(sz.SymSpace, 0, func(args sx.Vector, env *Environment) sx.Object { if len(args) == 0 { return sx.String(" ") } return getString(args[0], env) }) - ev.bind(sz.NameSymSoft, 0, func([]sx.Object, *Environment) sx.Object { return sx.String(" ") }) - symBR := ev.Make("br") - ev.bind(sz.NameSymHard, 0, func([]sx.Object, *Environment) sx.Object { return sx.Nil().Cons(symBR) }) + ev.bind(sz.SymSoft, 0, func(sx.Vector, *Environment) sx.Object { return sx.String(" ") }) + ev.bind(sz.SymHard, 0, func(sx.Vector, *Environment) sx.Object { return sx.Nil().Cons(symBR) }) - ev.bind(sz.NameSymLinkInvalid, 2, func(args []sx.Object, env *Environment) sx.Object { - a := ev.getAttributes(args[0], env) + ev.bind(sz.SymLinkInvalid, 2, func(args sx.Vector, env *Environment) sx.Object { + a := ev.GetAttributes(args[0], env) env.pushAttributes(a) defer env.popAttributes() var inline *sx.Pair if len(args) > 2 { inline = ev.evalSlice(args[2:], env) } if inline == nil { - inline = sx.Nil().Cons(ev.eval(args[1], env)) + inline = sx.Nil().Cons(ev.Eval(args[1], env)) } - return inline.Cons(ev.symSpan) + return inline.Cons(SymSPAN) }) - evalHREF := func(args []sx.Object, env *Environment) sx.Object { - a := ev.getAttributes(args[0], env) + evalHREF := func(args sx.Vector, env *Environment) sx.Object { + a := ev.GetAttributes(args[0], env) env.pushAttributes(a) defer env.popAttributes() refValue := getString(args[1], env) - return ev.evalLink(a.Set("href", refValue.String()), refValue, args[2:], env) + return ev.evalLink(a.Set("href", string(refValue)), refValue, args[2:], env) } - ev.bind(sz.NameSymLinkZettel, 2, evalHREF) - ev.bind(sz.NameSymLinkSelf, 2, evalHREF) - ev.bind(sz.NameSymLinkFound, 2, evalHREF) - ev.bind(sz.NameSymLinkBroken, 2, func(args []sx.Object, env *Environment) sx.Object { - a := ev.getAttributes(args[0], env) + ev.bind(sz.SymLinkZettel, 2, evalHREF) + ev.bind(sz.SymLinkSelf, 2, evalHREF) + ev.bind(sz.SymLinkFound, 2, evalHREF) + ev.bind(sz.SymLinkBroken, 2, func(args sx.Vector, env *Environment) sx.Object { + a := ev.GetAttributes(args[0], env) env.pushAttributes(a) defer env.popAttributes() refValue := getString(args[1], env) return ev.evalLink(a.AddClass("broken"), refValue, args[2:], env) }) - ev.bind(sz.NameSymLinkHosted, 2, evalHREF) - ev.bind(sz.NameSymLinkBased, 2, evalHREF) - ev.bind(sz.NameSymLinkQuery, 2, func(args []sx.Object, env *Environment) sx.Object { - a := ev.getAttributes(args[0], env) - env.pushAttributes(a) - defer env.popAttributes() - refValue := getString(args[1], env) - query := "?" + api.QueryKeyQuery + "=" + url.QueryEscape(refValue.String()) - return ev.evalLink(a.Set("href", query), refValue, args[2:], env) - }) - ev.bind(sz.NameSymLinkExternal, 2, func(args []sx.Object, env *Environment) sx.Object { - a := ev.getAttributes(args[0], env) - env.pushAttributes(a) - defer env.popAttributes() - refValue := getString(args[1], env) - return ev.evalLink(a.Set("href", refValue.String()).AddClass("external"), refValue, args[2:], env) - }) - - ev.bind(sz.NameSymEmbed, 3, func(args []sx.Object, env *Environment) sx.Object { - ref := getList(ev.eval(args[1], env), env) - syntax := getString(args[2], env) - if syntax == api.ValueSyntaxSVG { - embedAttr := sx.MakeList( - ev.symAttr, - sx.Cons(ev.Make("type"), sx.String("image/svg+xml")), - sx.Cons(ev.Make("src"), sx.String("/"+getString(ref.Tail(), env).String()+".svg")), - ) - return sx.MakeList( - ev.Make("figure"), - sx.MakeList( - ev.Make("embed"), + ev.bind(sz.SymLinkHosted, 2, evalHREF) + ev.bind(sz.SymLinkBased, 2, evalHREF) + ev.bind(sz.SymLinkQuery, 2, func(args sx.Vector, env *Environment) sx.Object { + a := ev.GetAttributes(args[0], env) + env.pushAttributes(a) + defer env.popAttributes() + refValue := getString(args[1], env) + query := "?" + api.QueryKeyQuery + "=" + url.QueryEscape(string(refValue)) + return ev.evalLink(a.Set("href", query), refValue, args[2:], env) + }) + ev.bind(sz.SymLinkExternal, 2, func(args sx.Vector, env *Environment) sx.Object { + a := ev.GetAttributes(args[0], env) + env.pushAttributes(a) + defer env.popAttributes() + refValue := getString(args[1], env) + return ev.evalLink(a.Set("href", string(refValue)).AddClass("external"), refValue, args[2:], env) + }) + + ev.bind(sz.SymEmbed, 3, func(args sx.Vector, env *Environment) sx.Object { + ref := getList(args[1], env) + syntax := getString(args[2], env) + if syntax == api.ValueSyntaxSVG { + embedAttr := sx.MakeList( + sxhtml.SymAttr, + sx.Cons(SymAttrType, sx.String("image/svg+xml")), + sx.Cons(SymAttrSrc, sx.String("/"+string(getString(ref.Tail(), env))+".svg")), + ) + return sx.MakeList( + SymFIGURE, + sx.MakeList( + SymEMBED, embedAttr, ), ) } - a := ev.getAttributes(args[0], env) - a = a.Set("src", getString(ref.Tail().Car(), env).String()) + a := ev.GetAttributes(args[0], env) + a = a.Set("src", string(getString(ref.Tail().Car(), env))) if len(args) > 3 { var sb strings.Builder flattenText(&sb, sx.MakeList(args[3:]...)) if d := sb.String(); d != "" { a = a.Set("alt", d) } } - return sx.MakeList(ev.Make("img"), ev.EvaluateAttrbute(a)) + return sx.MakeList(SymIMG, ev.EvaluateAttrbute(a)) }) - ev.bind(sz.NameSymEmbedBLOB, 3, func(args []sx.Object, env *Environment) sx.Object { - a, syntax, data := ev.getAttributes(args[0], env), getString(args[1], env), getString(args[2], env) + ev.bind(sz.SymEmbedBLOB, 3, func(args sx.Vector, env *Environment) sx.Object { + a, syntax, data := ev.GetAttributes(args[0], env), getString(args[1], env), getString(args[2], env) summary, hasSummary := a.Get(api.KeySummary) if !hasSummary { summary = "" } return ev.evalBLOB( - sx.MakeList(ev.Make(sx.ListName), sx.String(summary)), + sx.MakeList(sxhtml.SymListSplice, sx.String(summary)), syntax, data, ) }) - ev.bind(sz.NameSymCite, 2, func(args []sx.Object, env *Environment) sx.Object { - a := ev.getAttributes(args[0], env) + ev.bind(sz.SymCite, 2, func(args sx.Vector, env *Environment) sx.Object { + a := ev.GetAttributes(args[0], env) env.pushAttributes(a) defer env.popAttributes() result := sx.Nil() if key := getString(args[1], env); key != "" { if len(args) > 2 { @@ -673,95 +651,87 @@ result = result.Cons(ev.EvaluateAttrbute(a)) } if result == nil { return nil } - return result.Cons(ev.symSpan) + return result.Cons(SymSPAN) }) - ev.bind(sz.NameSymMark, 3, func(args []sx.Object, env *Environment) sx.Object { + ev.bind(sz.SymMark, 3, func(args sx.Vector, env *Environment) sx.Object { result := ev.evalSlice(args[3:], env) if !ev.noLinks { if fragment := getString(args[2], env); fragment != "" { - a := attrs.Attributes{"id": fragment.String() + ev.unique} - return result.Cons(ev.EvaluateAttrbute(a)).Cons(ev.symA) + a := attrs.Attributes{"id": string(fragment) + ev.unique} + return result.Cons(ev.EvaluateAttrbute(a)).Cons(SymA) } } - return result.Cons(ev.symSpan) + return result.Cons(SymSPAN) }) - ev.bind(sz.NameSymEndnote, 1, func(args []sx.Object, env *Environment) sx.Object { - a := ev.getAttributes(args[0], env) + ev.bind(sz.SymEndnote, 1, func(args sx.Vector, env *Environment) sx.Object { + a := ev.GetAttributes(args[0], env) env.pushAttributes(a) defer env.popAttributes() attrPlist := sx.Nil() if len(a) > 0 { if attrs := ev.EvaluateAttrbute(a); attrs != nil { attrPlist = attrs.Tail() } } - text, isPair := sx.GetPair(args[1]) - if !isPair { - return sx.Nil() - } - noteNum := strconv.Itoa(len(env.endnotes) + 1) - noteID := ev.unique + noteNum - env.endnotes = append(env.endnotes, endnoteInfo{ - noteID: noteID, noteAST: ev.eval(text, env), noteHx: nil, attrs: attrPlist}) - hrefAttr := sx.Nil().Cons(sx.Cons(ev.Make("role"), sx.String("doc-noteref"))). - Cons(sx.Cons(ev.symHREF, sx.String("#fn:"+noteID))). - Cons(sx.Cons(ev.symClass, sx.String("zs-noteref"))). - Cons(ev.symAttr) - href := sx.Nil().Cons(sx.String(noteNum)).Cons(hrefAttr).Cons(ev.symA) - supAttr := sx.Nil().Cons(sx.Cons(ev.Make("id"), sx.String("fnref:"+noteID))).Cons(ev.symAttr) - return sx.Nil().Cons(href).Cons(supAttr).Cons(ev.Make("sup")) - }) - - ev.bind(sz.NameSymFormatDelete, 1, ev.makeFormatFn("del")) - ev.bind(sz.NameSymFormatEmph, 1, ev.makeFormatFn("em")) - ev.bind(sz.NameSymFormatInsert, 1, ev.makeFormatFn("ins")) - ev.bind(sz.NameSymFormatMark, 1, ev.makeFormatFn("mark")) - ev.bind(sz.NameSymFormatQuote, 1, ev.evalQuote) - ev.bind(sz.NameSymFormatSpan, 1, ev.makeFormatFn("span")) - ev.bind(sz.NameSymFormatStrong, 1, ev.makeFormatFn("strong")) - ev.bind(sz.NameSymFormatSub, 1, ev.makeFormatFn("sub")) - ev.bind(sz.NameSymFormatSuper, 1, ev.makeFormatFn("sup")) - - ev.bind(sz.NameSymLiteralComment, 1, func(args []sx.Object, env *Environment) sx.Object { - if ev.getAttributes(args[0], env).HasDefault() { - if len(args) > 1 { - if s := getString(ev.eval(args[1], env), env); s != "" { - return sx.Nil().Cons(s).Cons(ev.Make(sxhtml.NameSymInlineComment)) - } - } - } - return sx.Nil() - }) - ev.bind(sz.NameSymLiteralHTML, 2, ev.evalHTML) - kbdSym := ev.Make("kbd") - ev.bind(sz.NameSymLiteralInput, 2, func(args []sx.Object, env *Environment) sx.Object { - return ev.evalLiteral(args, nil, kbdSym, env) - }) - codeSym := ev.Make("code") - ev.bind(sz.NameSymLiteralMath, 2, func(args []sx.Object, env *Environment) sx.Object { - a := ev.getAttributes(args[0], env).AddClass("zs-math") - return ev.evalLiteral(args, a, codeSym, env) - }) - sampSym := ev.Make("samp") - ev.bind(sz.NameSymLiteralOutput, 2, func(args []sx.Object, env *Environment) sx.Object { - return ev.evalLiteral(args, nil, sampSym, env) - }) - ev.bind(sz.NameSymLiteralProg, 2, func(args []sx.Object, env *Environment) sx.Object { - return ev.evalLiteral(args, nil, codeSym, env) - }) - - ev.bind(sz.NameSymLiteralZettel, 0, nilFn) -} - -func (ev *Evaluator) makeFormatFn(tag string) EvalFn { - sym := ev.Make(tag) - return func(args []sx.Object, env *Environment) sx.Object { - a := ev.getAttributes(args[0], env) + 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.String("doc-noteref"))). + Cons(sx.Cons(SymAttrHref, sx.String("#fn:"+noteID))). + Cons(sx.Cons(SymAttrClass, sx.String("zs-noteref"))). + Cons(sxhtml.SymAttr) + href := sx.Nil().Cons(sx.String(noteNum)).Cons(hrefAttr).Cons(SymA) + supAttr := sx.Nil().Cons(sx.Cons(SymAttrId, sx.String("fnref:"+noteID))).Cons(sxhtml.SymAttr) + return sx.Nil().Cons(href).Cons(supAttr).Cons(symSUP) + }) + + ev.bind(sz.SymFormatDelete, 1, ev.makeFormatFn(symDEL)) + ev.bind(sz.SymFormatEmph, 1, ev.makeFormatFn(symEM)) + ev.bind(sz.SymFormatInsert, 1, ev.makeFormatFn(symINS)) + ev.bind(sz.SymFormatMark, 1, ev.makeFormatFn(symMARK)) + ev.bind(sz.SymFormatQuote, 1, ev.evalQuote) + ev.bind(sz.SymFormatSpan, 1, ev.makeFormatFn(SymSPAN)) + ev.bind(sz.SymFormatStrong, 1, ev.makeFormatFn(SymSTRONG)) + ev.bind(sz.SymFormatSub, 1, ev.makeFormatFn(symSUB)) + ev.bind(sz.SymFormatSuper, 1, ev.makeFormatFn(symSUP)) + + ev.bind(sz.SymLiteralComment, 1, func(args sx.Vector, env *Environment) sx.Object { + if ev.GetAttributes(args[0], env).HasDefault() { + if len(args) > 1 { + if s := getString(ev.Eval(args[1], env), env); s != "" { + return sx.Nil().Cons(s).Cons(sxhtml.SymInlineComment) + } + } + } + return sx.Nil() + }) + ev.bind(sz.SymLiteralHTML, 2, ev.evalHTML) + ev.bind(sz.SymLiteralInput, 2, func(args sx.Vector, env *Environment) sx.Object { + return ev.evalLiteral(args, nil, symKBD, env) + }) + ev.bind(sz.SymLiteralMath, 2, func(args sx.Vector, env *Environment) sx.Object { + a := ev.GetAttributes(args[0], env).AddClass("zs-math") + return ev.evalLiteral(args, a, symCODE, env) + }) + ev.bind(sz.SymLiteralOutput, 2, func(args sx.Vector, env *Environment) sx.Object { + return ev.evalLiteral(args, nil, symSAMP, env) + }) + ev.bind(sz.SymLiteralProg, 2, func(args sx.Vector, env *Environment) sx.Object { + return ev.evalLiteral(args, nil, symCODE, env) + }) + + ev.bind(sz.SymLiteralZettel, 0, nilFn) +} + +func (ev *Evaluator) makeFormatFn(sym *sx.Symbol) EvalFn { + return func(args sx.Vector, env *Environment) sx.Object { + a := ev.GetAttributes(args[0], env) env.pushAttributes(a) defer env.popAttributes() if val, hasClass := a.Get(""); hasClass { a = a.Remove("").AddClass(val) } @@ -804,12 +774,12 @@ return data.primLeft, data.primRight } return data.secLeft, data.secRight } -func (ev *Evaluator) evalQuote(args []sx.Object, env *Environment) sx.Object { - a := ev.getAttributes(args[0], env) +func (ev *Evaluator) evalQuote(args sx.Vector, env *Environment) sx.Object { + a := ev.GetAttributes(args[0], env) env.pushAttributes(a) defer env.popAttributes() if val, hasClass := a.Get(""); hasClass { a = a.Remove("").AddClass(val) @@ -821,35 +791,35 @@ res := ev.evalSlice(args[1:], env) env.quoteNesting-- lastPair := res.LastPair() if lastPair.IsNil() { - res = sx.Cons(sx.MakeList(ev.symNoEscape, sx.String(leftQ), sx.String(rightQ)), sx.Nil()) + res = sx.Cons(sx.MakeList(sxhtml.SymNoEscape, sx.String(leftQ), sx.String(rightQ)), sx.Nil()) } else { if quotes.nbsp { - lastPair.AppendBang(sx.MakeList(ev.symNoEscape, sx.String(" "), sx.String(rightQ))) - res = res.Cons(sx.MakeList(ev.symNoEscape, sx.String(leftQ), sx.String(" "))) + lastPair.AppendBang(sx.MakeList(sxhtml.SymNoEscape, sx.String(" "), sx.String(rightQ))) + res = res.Cons(sx.MakeList(sxhtml.SymNoEscape, sx.String(leftQ), sx.String(" "))) } else { - lastPair.AppendBang(sx.MakeList(ev.symNoEscape, sx.String(rightQ))) - res = res.Cons(sx.MakeList(ev.symNoEscape, sx.String(leftQ))) + lastPair.AppendBang(sx.MakeList(sxhtml.SymNoEscape, sx.String(rightQ))) + res = res.Cons(sx.MakeList(sxhtml.SymNoEscape, sx.String(leftQ))) } } if len(a) > 0 { res = res.Cons(ev.EvaluateAttrbute(a)) - return res.Cons(ev.symSpan) + return res.Cons(SymSPAN) } - return res.Cons(ev.symList) + return res.Cons(sxhtml.SymListSplice) } var visibleReplacer = strings.NewReplacer(" ", "\u2423") -func (ev *Evaluator) evalLiteral(args []sx.Object, a attrs.Attributes, sym *sx.Symbol, env *Environment) sx.Object { +func (ev *Evaluator) evalLiteral(args sx.Vector, a attrs.Attributes, sym *sx.Symbol, env *Environment) sx.Object { if a == nil { - a = ev.getAttributes(args[0], env) + a = ev.GetAttributes(args[0], env) } a = setProgLang(a) - literal := getString(args[1], env).String() + literal := string(getString(args[1], env)) if a.HasDefault() { a = a.RemoveDefault() literal = visibleReplacer.Replace(literal) } res := sx.Nil().Cons(sx.String(literal)) @@ -863,13 +833,13 @@ a = a.AddClass("language-" + val).Remove("") } return a } -func (ev *Evaluator) evalHTML(args []sx.Object, env *Environment) sx.Object { - if s := getString(ev.eval(args[1], env), env); s != "" && IsSafe(s.String()) { - return sx.Nil().Cons(s).Cons(ev.symNoEscape) +func (ev *Evaluator) evalHTML(args sx.Vector, env *Environment) sx.Object { + if s := getString(ev.Eval(args[1], env), env); s != "" && IsSafe(string(s)) { + return sx.Nil().Cons(s).Cons(sxhtml.SymNoEscape) } return nil } func (ev *Evaluator) evalBLOB(description *sx.Pair, syntax, data sx.String) sx.Object { @@ -878,44 +848,45 @@ } switch syntax { case "": return sx.Nil() case api.ValueSyntaxSVG: - return sx.Nil().Cons(sx.Nil().Cons(data).Cons(ev.symNoEscape)).Cons(ev.symP) + return sx.Nil().Cons(sx.Nil().Cons(data).Cons(sxhtml.SymNoEscape)).Cons(SymP) default: - imgAttr := sx.Nil().Cons(sx.Cons(ev.Make("src"), sx.String("data:image/"+syntax.String()+";base64,"+data.String()))) + imgAttr := sx.Nil().Cons(sx.Cons(SymAttrSrc, sx.String("data:image/"+string(syntax)+";base64,"+string(data)))) var sb strings.Builder flattenText(&sb, description) if d := sb.String(); d != "" { - imgAttr = imgAttr.Cons(sx.Cons(ev.Make("alt"), sx.String(d))) + imgAttr = imgAttr.Cons(sx.Cons(symAttrAlt, sx.String(d))) } - return sx.Nil().Cons(sx.Nil().Cons(imgAttr.Cons(ev.symAttr)).Cons(ev.Make("img"))).Cons(ev.symP) + return sx.Nil().Cons(sx.Nil().Cons(imgAttr.Cons(sxhtml.SymAttr)).Cons(SymIMG)).Cons(SymP) } } func flattenText(sb *strings.Builder, lst *sx.Pair) { for elem := lst; elem != nil; elem = elem.Tail() { switch obj := elem.Car().(type) { case sx.String: - sb.WriteString(obj.String()) + sb.WriteString(string(obj)) case *sx.Symbol: - if obj.Name() == sz.NameSymSpace { + if obj.IsEqual(sz.SymSpace) { sb.WriteByte(' ') break } case *sx.Pair: flattenText(sb, obj) } } } -func (ev *Evaluator) evalList(args []sx.Object, env *Environment) sx.Object { +func (ev *Evaluator) evalList(args sx.Vector, env *Environment) sx.Object { return ev.evalSlice(args, env) } -func nilFn([]sx.Object, *Environment) sx.Object { return sx.Nil() } +func nilFn(sx.Vector, *Environment) sx.Object { return sx.Nil() } -func (ev *Evaluator) eval(obj sx.Object, env *Environment) sx.Object { +// Eval evaluates an object in an environment. +func (ev *Evaluator) Eval(obj sx.Object, env *Environment) sx.Object { if env.err != nil { return sx.Nil() } if sx.IsNil(obj) { return obj @@ -927,28 +898,28 @@ sym, found := sx.GetSymbol(lst.Car()) if !found { env.err = fmt.Errorf("symbol expected, but got %T/%v", lst.Car(), lst.Car()) return sx.Nil() } - name := sym.Name() - fn, found := ev.fns[name] + symVal := sym.GetValue() + fn, found := ev.fns[symVal] if !found { - env.err = fmt.Errorf("symbol %q not bound", name) + env.err = fmt.Errorf("symbol %q not bound", sym) return sx.Nil() } - var args []sx.Object + var args sx.Vector for cdr := lst.Cdr(); !sx.IsNil(cdr); { pair, isPair := sx.GetPair(cdr) if !isPair { break } args = append(args, pair.Car()) cdr = pair.Cdr() } - if minArgs, hasMinArgs := ev.minArgs[name]; hasMinArgs { + if minArgs, hasMinArgs := ev.minArgs[symVal]; hasMinArgs { if minArgs > len(args) { - env.err = fmt.Errorf("%v needs at least %d arguments, but got only %d", name, minArgs, len(args)) + env.err = fmt.Errorf("%v needs at least %d arguments, but got only %d", sym, minArgs, len(args)) return sx.Nil() } } result := fn(args, env) if env.err != nil { @@ -955,42 +926,54 @@ return sx.Nil() } return result } -func (ev *Evaluator) evalSlice(args []sx.Object, env *Environment) *sx.Pair { - result := sx.Cons(sx.Nil(), sx.Nil()) - curr := result +func (ev *Evaluator) evalSlice(args sx.Vector, env *Environment) *sx.Pair { + var result sx.ListBuilder for _, arg := range args { - elem := ev.eval(arg, env) - if env.err != nil { - return nil - } - curr = curr.AppendBang(elem) + elem := ev.Eval(arg, env) + result.Add(elem) + } + if env.err == nil { + return result.List() + } + return nil +} + +// EvaluatePairList evaluates a list of lists. +func (ev *Evaluator) EvalPairList(pair *sx.Pair, env *Environment) *sx.Pair { + var result sx.ListBuilder + for node := pair; node != nil; node = node.Tail() { + elem := ev.Eval(node.Car(), env) + result.Add(elem) + } + if env.err == nil { + return result.List() } - return result.Tail() + return nil } -func (ev *Evaluator) evalLink(a attrs.Attributes, refValue sx.String, inline []sx.Object, env *Environment) sx.Object { +func (ev *Evaluator) evalLink(a attrs.Attributes, refValue sx.String, inline sx.Vector, env *Environment) sx.Object { result := ev.evalSlice(inline, env) if len(inline) == 0 { result = sx.Nil().Cons(refValue) } if ev.noLinks { - return result.Cons(ev.symSpan) + return result.Cons(SymSPAN) } - return result.Cons(ev.EvaluateAttrbute(a)).Cons(ev.symA) + return result.Cons(ev.EvaluateAttrbute(a)).Cons(SymA) } -func (ev *Evaluator) getSymbol(val sx.Object, env *Environment) *sx.Symbol { +func (ev *Evaluator) getSymbol(obj sx.Object, env *Environment) *sx.Symbol { if env.err == nil { - if sym, ok := sx.GetSymbol(val); ok { + if sym, ok := sx.GetSymbol(obj); ok { return sym } - env.err = fmt.Errorf("%v/%T is not a symbol", val, val) + env.err = fmt.Errorf("%v/%T is not a symbol", obj, obj) } - return ev.Make("???") + return sx.MakeSymbol("???") } func getString(val sx.Object, env *Environment) sx.String { if env.err != nil { return "" } @@ -1017,12 +1000,15 @@ return int64(num.(sx.Int64)) } env.err = fmt.Errorf("%v/%T is not a number", val, val) return -1017 } -func (ev *Evaluator) getAttributes(arg sx.Object, env *Environment) attrs.Attributes { - return sz.GetAttributes(getList(ev.eval(arg, env), env)) + +// GetAttributes evaluates the given arg in the given environment and returns +// the contained attributes. +func (ev *Evaluator) GetAttributes(arg sx.Object, env *Environment) attrs.Attributes { + return sz.GetAttributes(getList(arg, env)) } var unsafeSnippets = []string{ "Change Log + +

Changes for Version 0.18.0 (pending)

+ -

Changes for Version 0.17.0 (pending)

+

Changes for Version 0.17.0 (2024-03-04)

+ * Generic GET method for HTTP client. + * Adapt to sz changes; see manual for current syntax. + * Got some packages from Zettelstore, for general use.

Changes for Version 0.16.0 (2023-11-30)

* Refactor shtml transformator to support evaluating the language tree of a zettel AST. Its API has been changes, since evaluation is now top-down, Index: www/index.wiki ================================================================== --- www/index.wiki +++ www/index.wiki @@ -1,16 +1,16 @@ Home This repository contains Go client software to access [https://zettelstore.de|Zettelstore] via its API. -

Latest Release: 0.16.0 (2023-11-30)

- * [./changes.wiki#0_16|Change summary] - * [/timeline?p=v0.16.0&bt=v0.15.0&y=ci|Check-ins for version 0.16.0], - [/vdiff?to=v0.16.0&from=v0.15.0|content diff] - * [/timeline?df=v0.16.0&y=ci|Check-ins derived from the 0.16.0 release], - [/vdiff?from=v0.16.0&to=trunk|content diff] +

Latest Release: 0.17.0 (2024-03-04)

+ * [./changes.wiki#0_17|Change summary] + * [/timeline?p=v0.17.0&bt=v0.16.0&y=ci|Check-ins for version 0.17.0], + [/vdiff?to=v0.17.0&from=v0.16.0|content diff] + * [/timeline?df=v0.17.0&y=ci|Check-ins derived from the 0.17.0 release], + [/vdiff?from=v0.17.0&to=trunk|content diff] * [/timeline?t=release|Timeline of all past releases]

Use instructions

If you want to import this library into your own [https://go.dev/|Go] software,