DELETED .github/dependabot.yml Index: .github/dependabot.yml ================================================================== --- .github/dependabot.yml +++ .github/dependabot.yml @@ -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 } } @@ -43,85 +46,31 @@ ZettelCanCreate // Current user is allowed to create a new zettel ZettelCanRead // Requesting user is allowed to read the zettel ZettelCanWrite // Requesting user is allowed to update the zettel ZettelCanRename // Requesting user is allowed to provide the zettel with a new identifier ZettelCanDelete // Requesting user is allowed to delete the zettel -) - -// AuthJSON contains the result of an authentication call. -type AuthJSON struct { - Token string `json:"token"` - Type string `json:"token_type"` - Expires int `json:"expires_in"` -} - -// ZidJSON contains the identifier data of a zettel. -type ZidJSON struct { - ID ZettelID `json:"id"` -} - -// MetaJSON contains the metadata of a zettel. -type MetaJSON struct { - Meta ZettelMeta `json:"meta"` - Rights ZettelRights `json:"rights"` -} - -// ZidMetaJSON contains the identifier and the metadata of a zettel. -type ZidMetaJSON struct { - ID ZettelID `json:"id"` - Meta ZettelMeta `json:"meta"` - Rights ZettelRights `json:"rights"` -} - -// ZidMetaRelatedList contains identifier/metadata of a zettel and the same for related zettel -type ZidMetaRelatedList struct { - ID ZettelID `json:"id"` - Meta ZettelMeta `json:"meta"` - Rights ZettelRights `json:"rights"` - List []ZidMetaJSON `json:"list"` -} - -// ZettelDataJSON contains all data for a zettel. -type ZettelDataJSON struct { - Meta ZettelMeta `json:"meta"` - Encoding string `json:"encoding"` - Content string `json:"content"` -} - -// ZettelJSON contains all data for a zettel, the identifier, the metadata, and the content. -type ZettelJSON struct { - ID ZettelID `json:"id"` - Meta ZettelMeta `json:"meta"` - Encoding string `json:"encoding"` - Content string `json:"content"` - Rights ZettelRights `json:"rights"` -} - -// ZettelContentJSON contains all elements to transfer the content of a zettel. -type ZettelContentJSON struct { - Encoding string `json:"encoding"` - Content string `json:"content"` -} - -// ZettelListJSON contains data for a zettel list. -type ZettelListJSON struct { - Query string `json:"query"` - Human string `json:"human"` - List []ZidMetaJSON `json:"list"` -} - -// MapMeta maps metadata keys to list of metadata. -type MapMeta map[string][]ZettelID - -// MapListJSON specifies the map of metadata key to list of metadata that contains the key. -type MapListJSON struct { - Map MapMeta `json:"map"` -} - -// VersionJSON contains version information. -type VersionJSON struct { - Major int `json:"major"` - Minor int `json:"minor"` - Patch int `json:"patch"` - Info string `json:"info"` - Hash string `json:"hash"` -} + ZettelMaxRight // Sentinel value +) + +// MetaRights contains the metadata of a zettel, and its rights. +type MetaRights struct { + Meta ZettelMeta + Rights ZettelRights +} + +// ZidMetaRights contains the identifier, the metadata of a zettel, and its rights. +type ZidMetaRights struct { + ID ZettelID + Meta ZettelMeta + Rights ZettelRights +} + +// ZettelData contains all data for a zettel. +type ZettelData struct { + Meta ZettelMeta + Rights ZettelRights + Encoding string + Content string +} + +// Aggregate maps metadata keys to list of zettel identifier. +type Aggregate map[string][]ZettelID 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" @@ -20,41 +23,55 @@ ZidOperatingSystem = ZettelID("00000000000003") ZidLicense = ZettelID("00000000000004") ZidAuthors = ZettelID("00000000000005") ZidDependencies = ZettelID("00000000000006") ZidLog = ZettelID("00000000000007") + ZidMemory = ZettelID("00000000000008") ZidBoxManager = ZettelID("00000000000020") ZidMetadataKey = ZettelID("00000000000090") ZidParser = ZettelID("00000000000092") ZidStartupConfiguration = ZettelID("00000000000096") ZidConfiguration = ZettelID("00000000000100") // WebUI HTML templates are in the range 10000..19999 - ZidBaseTemplate = ZettelID("00000000010100") - ZidLoginTemplate = ZettelID("00000000010200") - ZidListTemplate = ZettelID("00000000010300") - ZidZettelTemplate = ZettelID("00000000010401") - ZidInfoTemplate = ZettelID("00000000010402") - ZidFormTemplate = ZettelID("00000000010403") - ZidRenameTemplate = ZettelID("00000000010404") - ZidDeleteTemplate = ZettelID("00000000010405") - ZidContextTemplate = ZettelID("00000000010406") - ZidErrorTemplate = ZettelID("00000000010700") + ZidBaseTemplate = ZettelID("00000000010100") + ZidLoginTemplate = ZettelID("00000000010200") + ZidListTemplate = ZettelID("00000000010300") + ZidZettelTemplate = ZettelID("00000000010401") + ZidInfoTemplate = ZettelID("00000000010402") + ZidFormTemplate = ZettelID("00000000010403") + ZidRenameTemplate = ZettelID("00000000010404") + ZidDeleteTemplate = ZettelID("00000000010405") + ZidErrorTemplate = ZettelID("00000000010700") + + // WebUI sxn code zettel are in the range 19000..19999 + ZidSxnStart = ZettelID("00000000019000") + ZidSxnBase = ZettelID("00000000019990") // CSS-related zettel are in the range 20000..29999 - ZidBaseCSS = ZettelID("00000000020001") - ZidUserCSS = ZettelID("00000000025001") - ZidRoleCSSMap = ZettelID("00000000029000") // Maps roles to CSS zettel, which should be in the range 29001..29999. + ZidBaseCSS = ZettelID("00000000020001") + ZidUserCSS = ZettelID("00000000025001") // WebUI JS zettel are in the range 30000..39999 // WebUI image zettel are in the range 40000..49999 ZidEmoji = ZettelID("00000000040001") + // Other sxn code zettel are in the range 50000..59999 + ZidSxnPrelude = ZettelID("00000000059900") + + // Predefined Zettelmarkup zettel are in the range 60000..69999 + ZidRoleZettelZettel = ZettelID("00000000060010") + ZidRoleConfigurationZettel = ZettelID("00000000060020") + ZidRoleRoleZettel = ZettelID("00000000060030") + ZidRoleTagZettel = ZettelID("00000000060040") + // Range 90000...99999 is reserved for zettel templates ZidTOCNewTemplate = ZettelID("00000000090000") ZidTemplateNewZettel = ZettelID("00000000090001") + ZidTemplateNewRole = ZettelID("00000000090004") + ZidTemplateNewTag = ZettelID("00000000090003") ZidTemplateNewUser = ZettelID("00000000090002") ZidDefaultHome = ZettelID("00010000000000") ) @@ -71,11 +88,10 @@ MetaString = "String" MetaTagSet = "TagSet" MetaTimestamp = "Timestamp" MetaURL = "URL" MetaWord = "Word" - MetaWordSet = "WordSet" MetaZettelmarkup = "Zettelmarkup" ) // Predefined general Metadata keys const ( @@ -90,21 +106,25 @@ KeyBoxNumber = "box-number" KeyCopyright = "copyright" KeyCreated = "created" KeyCredential = "credential" KeyDead = "dead" + KeyExpire = "expire" KeyFolge = "folge" + KeyFolgeRole = "folge-role" KeyForward = "forward" KeyLang = "lang" KeyLicense = "license" KeyModified = "modified" KeyPrecursor = "precursor" KeyPredecessor = "predecessor" KeyPublished = "published" KeyQuery = "query" KeyReadOnly = "read-only" + KeySubordinates = "subordinates" KeySuccessors = "successors" + KeySuperior = "superior" KeySummary = "summary" KeyURL = "url" KeyUselessFiles = "useless-files" KeyUserID = "user-id" KeyUserRole = "user-role" @@ -115,20 +135,22 @@ const ( ValueFalse = "false" ValueTrue = "true" ValueLangEN = "en" ValueRoleConfiguration = "configuration" + ValueRoleTag = "tag" + ValueRoleRole = "role" ValueRoleZettel = "zettel" ValueSyntaxCSS = "css" ValueSyntaxDraw = "draw" ValueSyntaxGif = "gif" ValueSyntaxHTML = "html" ValueSyntaxMarkdown = "markdown" ValueSyntaxMD = "md" - ValueSyntaxMustache = "mustache" ValueSyntaxNone = "none" ValueSyntaxSVG = "svg" + ValueSyntaxSxn = "sxn" ValueSyntaxText = "text" ValueSyntaxZmk = "zmk" ValueUserRoleCreator = "creator" ValueUserRoleOwner = "owner" ValueUserRoleReader = "reader" @@ -151,50 +173,43 @@ ) // Values for HTTP query parameter. const ( QueryKeyCommand = "cmd" - QueryKeyCost = "cost" - QueryKeyDir = "dir" QueryKeyEncoding = "enc" QueryKeyParseOnly = "parseonly" - QueryKeyLimit = "limit" QueryKeyPart = "part" QueryKeyPhrase = "phrase" QueryKeyQuery = "q" + QueryKeyRole = "role" QueryKeySeed = "_seed" -) - -// Supported dir values. -const ( - DirBackward = "backward" - DirForward = "forward" + QueryKeyTag = "tag" ) // Supported encoding values. const ( EncodingHTML = "html" EncodingMD = "md" - EncodingSexpr = "sexpr" EncodingSHTML = "shtml" + EncodingSz = "sz" EncodingText = "text" EncodingZMK = "zmk" EncodingPlain = "plain" - EncodingJson = "json" + EncodingData = "data" ) var mapEncodingEnum = map[string]EncodingEnum{ EncodingHTML: EncoderHTML, EncodingMD: EncoderMD, - EncodingSexpr: EncoderSexpr, EncodingSHTML: EncoderSHTML, + EncodingSz: EncoderSz, EncodingText: EncoderText, EncodingZMK: EncoderZmk, EncodingPlain: EncoderPlain, - EncodingJson: EncoderJson, + EncodingData: EncoderData, } var mapEnumEncoding = map[EncodingEnum]string{} func init() { for k, v := range mapEncodingEnum { @@ -216,17 +231,17 @@ // Values for EncoderEnum const ( EncoderUnknown EncodingEnum = iota EncoderHTML EncoderMD - EncoderSexpr EncoderSHTML + EncoderSz EncoderText EncoderZmk EncoderPlain - EncoderJson + EncoderData ) // String representation of an encoder key. func (e EncodingEnum) String() string { if f, ok := mapEnumEncoding[e]; ok { @@ -251,18 +266,57 @@ CommandRefresh = Command("refresh") ) // Supported search operator representations const ( - ActionSeparator = "|" - ExistOperator = "?" - ExistNotOperator = "!?" - SearchOperatorNot = "!" - SearchOperatorHas = ":" - SearchOperatorHasNot = "!:" - SearchOperatorPrefix = ">" - SearchOperatorNoPrefix = "!>" - SearchOperatorSuffix = "<" - SearchOperatorNoSuffix = "!<" - SearchOperatorMatch = "~" - SearchOperatorNoMatch = "!~" -) + BackwardDirective = "BACKWARD" + ContextDirective = "CONTEXT" + CostDirective = "COST" + ForwardDirective = "FORWARD" + FullDirective = "FULL" + IdentDirective = "IDENT" + ItemsDirective = "ITEMS" + MaxDirective = "MAX" + LimitDirective = "LIMIT" + OffsetDirective = "OFFSET" + OrDirective = "OR" + OrderDirective = "ORDER" + PhraseDirective = "PHRASE" + PickDirective = "PICK" + RandomDirective = "RANDOM" + 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 = "=" + SearchOperatorNotEqual = "!=" + SearchOperatorHas = ":" + SearchOperatorHasNot = "!:" + SearchOperatorPrefix = "[" + SearchOperatorNoPrefix = "![" + SearchOperatorSuffix = "]" + SearchOperatorNoSuffix = "!]" + SearchOperatorMatch = "~" + SearchOperatorNoMatch = "!~" + 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,155 +4,82 @@ // 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 ( - "net/url" - "strings" -) - -type urlQuery struct{ key, val string } +import "t73f.de/r/webs/urlbuilder" // URLBuilder should be used to create zettelstore URLs. type URLBuilder struct { - prefix string - key byte - rawLocal string - path []string - query []urlQuery - fragment string + base urlbuilder.URLBuilder + prefix string } // NewURLBuilder creates a new URL builder with the given prefix and key. func NewURLBuilder(prefix string, key byte) *URLBuilder { - return &URLBuilder{prefix: prefix, key: key} + for len(prefix) > 0 && prefix[len(prefix)-1] == '/' { + prefix = prefix[0 : len(prefix)-1] + } + result := URLBuilder{prefix: prefix} + if key != '/' { + result.base.AddPath(string([]byte{key})) + } + return &result } // Clone an URLBuilder func (ub *URLBuilder) Clone() *URLBuilder { cpy := new(URLBuilder) - cpy.key = ub.key - if len(ub.path) > 0 { - cpy.path = make([]string, 0, len(ub.path)) - cpy.path = append(cpy.path, ub.path...) - } - if len(ub.query) > 0 { - cpy.query = make([]urlQuery, 0, len(ub.query)) - cpy.query = append(cpy.query, ub.query...) - } - cpy.fragment = ub.fragment + ub.base.Copy(&cpy.base) + cpy.prefix = ub.prefix return cpy } -// SetRawLocal sets everything that follows the prefix / key. -func (ub *URLBuilder) SetRawLocal(rawLocal string) *URLBuilder { - for len(rawLocal) > 0 && rawLocal[0] == '/' { - rawLocal = rawLocal[1:] - } - ub.rawLocal = rawLocal - ub.path = nil - ub.query = nil - ub.fragment = "" - return ub -} - // SetZid sets the zettel identifier. func (ub *URLBuilder) SetZid(zid ZettelID) *URLBuilder { - if len(ub.path) > 0 { - panic("Cannot add Zid") - } - ub.rawLocal = "" - ub.path = append(ub.path, string(zid)) + ub.base.AddPath(string(zid)) return ub } // AppendPath adds a new path element func (ub *URLBuilder) AppendPath(p string) *URLBuilder { - ub.rawLocal = "" - for len(p) > 0 && p[0] == '/' { - p = p[1:] - } - if p != "" { - ub.path = append(ub.path, p) - } + ub.base.AddPath(p) return ub } // AppendKVQuery adds a new key/value query parameter func (ub *URLBuilder) AppendKVQuery(key, value string) *URLBuilder { - ub.rawLocal = "" - ub.query = append(ub.query, urlQuery{key, value}) + ub.base.AddQuery(key, value) return ub } // AppendQuery adds a new query func (ub *URLBuilder) AppendQuery(value string) *URLBuilder { - ub.rawLocal = "" - ub.query = append(ub.query, urlQuery{QueryKeyQuery, value}) + if value != "" { + ub.base.AddQuery(QueryKeyQuery, value) + } return ub } // ClearQuery removes all query parameters. func (ub *URLBuilder) ClearQuery() *URLBuilder { - ub.rawLocal = "" - ub.query = nil - ub.fragment = "" + ub.base.RemoveQueries() return ub } // SetFragment stores the fragment func (ub *URLBuilder) SetFragment(s string) *URLBuilder { - ub.rawLocal = "" - ub.fragment = s + ub.base.SetFragment(s) return ub } // String produces a string value. func (ub *URLBuilder) String() string { - return ub.asString("&") -} - -// AttrString returns the string value of the URL suitable to be placed in a HTML attribute. -func (ub *URLBuilder) AttrString() string { - return ub.asString("&") -} - -func (ub *URLBuilder) asString(qsep string) string { - var sb strings.Builder - - sb.WriteString(ub.prefix) - if ub.key != '/' { - sb.WriteByte(ub.key) - } - if ub.rawLocal != "" { - sb.WriteString(ub.rawLocal) - return sb.String() - } - for i, p := range ub.path { - if i > 0 || ub.key != '/' { - sb.WriteByte('/') - } - sb.WriteString(url.PathEscape(p)) - } - if len(ub.fragment) > 0 { - sb.WriteByte('#') - sb.WriteString(ub.fragment) - } - for i, q := range ub.query { - if i == 0 { - sb.WriteByte('?') - } else { - sb.WriteString(qsep) - } - sb.WriteString(q.key) - if val := q.val; val != "" { - sb.WriteByte('=') - sb.WriteString(url.QueryEscape(val)) - } - } - return sb.String() + return ub.prefix + ub.base.String() } Index: attrs/attrs.go ================================================================== --- attrs/attrs.go +++ attrs/attrs.go @@ -4,19 +4,22 @@ // This file is part of zettelstore-client. // // Zettelstore client is licensed under the latest version of the EUPL // (European Union Public License). Please see file LICENSE.txt for your rights // and obligations under this license. +// +// SPDX-License-Identifier: EUPL-1.2 +// SPDX-FileCopyrightText: 2020-present Detlef Stern //----------------------------------------------------------------------------- // Package attrs stores attributes of zettel parts. package attrs import ( "strings" - "zettelstore.de/c/maps" + "t73f.de/r/zsc/maps" ) // Attributes store additional information about some node types. type Attributes map[string]string Index: attrs/attrs_test.go ================================================================== --- attrs/attrs_test.go +++ attrs/attrs_test.go @@ -4,18 +4,21 @@ // This file is part of zettelstore-client. // // Zettelstore client is licensed under the latest version of the EUPL // (European Union Public License). Please see file LICENSE.txt for your rights // and obligations under this license. +// +// SPDX-License-Identifier: EUPL-1.2 +// SPDX-FileCopyrightText: 2020-present Detlef Stern //----------------------------------------------------------------------------- package attrs_test import ( "testing" - "zettelstore.de/c/attrs" + "t73f.de/r/zsc/attrs" ) func TestHasDefault(t *testing.T) { t.Parallel() attr := attrs.Attributes{} Index: client/client.go ================================================================== --- client/client.go +++ client/client.go @@ -4,31 +4,36 @@ // 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 import ( "bufio" "bytes" "context" - "encoding/json" + "fmt" "io" "net" "net/http" "net/url" "strconv" "strings" "time" - "codeberg.org/t73fde/sxpf" - "codeberg.org/t73fde/sxpf/reader" - "zettelstore.de/c/api" + "t73f.de/r/sx" + "t73f.de/r/sx/sxreader" + "t73f.de/r/zsc/api" + "t73f.de/r/zsc/sexp" + "t73f.de/r/zsc/sz" ) // Client contains all data to execute requests. type Client struct { base string @@ -50,13 +55,10 @@ myURL.ForceQuery = false myURL.RawQuery = "" myURL.Fragment = "" myURL.RawFragment = "" base := myURL.String() - if !strings.HasSuffix(base, "/") { - base += "/" - } c := Client{ base: base, client: http.Client{ Timeout: 10 * time.Second, Transport: &http.Transport{ @@ -67,10 +69,23 @@ }, }, } return &c } + +// AllowRedirect will modify the client to not follow redirect status code when +// using the Zettelstore. The original behaviour can be restored by settinh +// "allow" to false. +func (c *Client) AllowRedirect(allow bool) { + if allow { + c.client.CheckRedirect = func(req *http.Request, via []*http.Request) error { + return http.ErrUseLastResponse + } + } else { + c.client.CheckRedirect = nil + } +} // Error encapsulates the possible client call errors. type Error struct { StatusCode int Message string @@ -109,11 +124,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) } @@ -164,19 +180,26 @@ } defer resp.Body.Close() if resp.StatusCode != http.StatusOK { return statusToError(resp) } - dec := json.NewDecoder(resp.Body) - var tinfo api.AuthJSON - err = dec.Decode(&tinfo) + rd := sxreader.MakeReader(resp.Body) + obj, err := rd.Read() + if err != nil { + return err + } + vals, err := sexp.ParseList(obj, "ssi") if err != nil { return err } - c.token = tinfo.Token - c.tokenType = tinfo.Type - c.expires = time.Now().Add(time.Duration(tinfo.Expires*10/9) * time.Second) + token := vals[1].(sx.String).GetValue() + if len(token) < 4 { + return fmt.Errorf("no valid token found: %q", token) + } + c.token = token + c.tokenType = vals[0].(sx.String).GetValue() + 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 { if c.username == "" { @@ -189,30 +212,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() @@ -227,60 +250,51 @@ return zid, nil } return api.InvalidZID, err } -// CreateZettelJSON creates a new zettel and returns its URL. -func (c *Client) CreateZettelJSON(ctx context.Context, data *api.ZettelDataJSON) (api.ZettelID, error) { +// CreateZettelData creates a new zettel and returns its URL. +func (c *Client) CreateZettelData(ctx context.Context, data api.ZettelData) (api.ZettelID, error) { var buf bytes.Buffer - if err := encodeZettelData(&buf, data); err != nil { + if _, err := sx.Print(&buf, sexp.EncodeZettel(data)); err != nil { return api.InvalidZID, err } - ub := c.newURLBuilder('z').AppendKVQuery(api.QueryKeyEncoding, api.EncodingJson) + 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() + rdr := sxreader.MakeReader(resp.Body) + obj, err := rdr.Read() if resp.StatusCode != http.StatusCreated { return api.InvalidZID, statusToError(resp) } - dec := json.NewDecoder(resp.Body) - var newZid api.ZidJSON - err = dec.Decode(&newZid) if err != nil { return api.InvalidZID, err } - if zid := newZid.ID; zid.IsValid() { - return zid, nil - } - return api.InvalidZID, err -} - -func encodeZettelData(buf *bytes.Buffer, data *api.ZettelDataJSON) error { - enc := json.NewEncoder(buf) - enc.SetEscapeHTML(false) - return enc.Encode(&data) + return makeZettelID(obj) } var bsLF = []byte{'\n'} -// ListZettel returns a list of all Zettel. -func (c *Client) ListZettel(ctx context.Context, query string) ([][]byte, error) { - ub := c.newURLBuilder('z').AppendQuery(query) +// QueryZettel returns a list of all Zettel. +func (c *Client) QueryZettel(ctx context.Context, query string) ([][]byte, error) { + 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() + data, err := io.ReadAll(resp.Body) switch resp.StatusCode { case http.StatusOK: case http.StatusNoContent: + return nil, nil default: return nil, statusToError(resp) } - data, err := io.ReadAll(resp.Body) if err != nil { return nil, err } lines := bytes.Split(data, bsLF) if len(lines[len(lines)-1]) == 0 { @@ -287,12 +301,33 @@ lines = lines[:len(lines)-1] } return lines, nil } -// ListZettelJSON returns a list of zettel. -func (c *Client) ListZettelJSON(ctx context.Context, query string) (string, string, []api.ZidMetaJSON, error) { - ub := c.newURLBuilder('z').AppendKVQuery(api.QueryKeyEncoding, api.EncodingJson).AppendQuery(query) +// 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) resp, err := c.buildAndExecuteRequest(ctx, http.MethodGet, ub, nil, nil) if err != nil { return "", "", nil, err } + defer resp.Body.Close() + rdr := sxreader.MakeReader(resp.Body) + obj, err := rdr.Read() + switch resp.StatusCode { + case http.StatusOK: + case http.StatusNoContent: + return "", "", nil, nil + default: + return "", "", nil, statusToError(resp) + } + if err != nil { + return "", "", nil, err + } + vals, err := sexp.ParseList(obj, "yppp") + if err != nil { + return "", "", nil, err + } + qVals, err := sexp.ParseList(vals[1], "ys") + if err != nil { + return "", "", nil, err + } @@ -299,58 +334,190 @@ - defer resp.Body.Close() - if resp.StatusCode != http.StatusOK { - return "", "", nil, statusToError(resp) - } - dec := json.NewDecoder(resp.Body) - var zl api.ZettelListJSON - err = dec.Decode(&zl) + hVals, err := sexp.ParseList(vals[2], "ys") if err != nil { return "", "", nil, err } - return zl.Query, zl.Human, zl.List, nil + metaList, err := parseMetaList(vals[3].(*sx.Pair)) + return sz.GoValue(qVals[1]), sz.GoValue(hVals[1]), metaList, err +} + +func parseMetaList(metaPair *sx.Pair) ([]api.ZidMetaRights, error) { + if metaPair == nil { + return nil, fmt.Errorf("no zettel list") + } + if errSym := sexp.CheckSymbol(metaPair.Car(), "list"); errSym != nil { + return nil, errSym + } + var result []api.ZidMetaRights + for node := metaPair.Cdr(); !sx.IsNil(node); { + elem, isPair := sx.GetPair(node) + if !isPair { + return nil, fmt.Errorf("meta-list not a proper list: %v", metaPair.String()) + } + node = elem.Cdr() + vals, err := sexp.ParseList(elem.Car(), "yppp") + if err != nil { + return nil, err + } + + if errSym := sexp.CheckSymbol(vals[0], "zettel"); errSym != nil { + return nil, errSym + } + + idVals, err := sexp.ParseList(vals[1], "yi") + if err != nil { + return nil, err + } + if errSym := sexp.CheckSymbol(idVals[0], "id"); errSym != nil { + return nil, errSym + } + zid, err := makeZettelID(idVals[1]) + if err != nil { + return nil, err + } + + meta, err := sexp.ParseMeta(vals[2].(*sx.Pair)) + if err != nil { + return nil, err + } + + rights, err := sexp.ParseRights(vals[3]) + if err != nil { + return nil, err + } + + result = append(result, api.ZidMetaRights{ + ID: zid, + Meta: meta, + Rights: rights, + }) + } + return result, nil +} +func makeZettelID(obj sx.Object) (api.ZettelID, error) { + val, isInt64 := obj.(sx.Int64) + if !isInt64 || val <= 0 { + return api.InvalidZID, fmt.Errorf("invalid zettel ID: %v", val) + } + sVal := strconv.FormatInt(int64(val), 10) + if len(sVal) < 14 { + sVal = "00000000000000"[0:14-len(sVal)] + sVal + } + zid := api.ZettelID(sVal) + if !zid.IsValid() { + return api.InvalidZID, fmt.Errorf("invalid zettel ID: %v", val) + } + 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 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 + } + if len(lines) == 0 { + return nil, nil + } + agg := make(api.Aggregate, len(lines)) + for _, line := range lines { + if fields := bytes.Fields(line); len(fields) > 1 { + key := string(fields[0]) + for _, field := range fields[1:] { + if zid := api.ZettelID(string(field)); zid.IsValid() { + agg[key] = append(agg[key], zid) + } + } + } + } + return agg, nil +} + +// TagZettel returns the tag zettel of a given tag. +// +// This method only works if c.AllowRedirect(true) was called. +func (c *Client) TagZettel(ctx context.Context, tag string) (api.ZettelID, error) { + return c.fetchTagOrRoleZettel(ctx, api.QueryKeyTag, tag) +} + +// RoleZettel returns the tag zettel of a given tag. +// +// This method only works if c.AllowRedirect(true) was called. +func (c *Client) RoleZettel(ctx context.Context, role string) (api.ZettelID, error) { + return c.fetchTagOrRoleZettel(ctx, api.QueryKeyRole, role) +} + +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) + resp, err := c.buildAndExecuteRequest(ctx, http.MethodGet, ub, nil, nil) + if err != nil { + return api.InvalidZID, err + } + defer resp.Body.Close() + data, err := io.ReadAll(resp.Body) + if err != nil { + return api.InvalidZID, err + } + + switch resp.StatusCode { + case http.StatusNotFound: + return "", nil + case http.StatusFound: + zid := api.ZettelID(data) + if zid.IsValid() { + return zid, nil + } + return api.InvalidZID, nil + default: + return api.InvalidZID, statusToError(resp) + } } // 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 { return nil, err } defer resp.Body.Close() + data, err := io.ReadAll(resp.Body) switch resp.StatusCode { case http.StatusOK: case http.StatusNoContent: + return nil, nil default: return nil, statusToError(resp) } - return io.ReadAll(resp.Body) + return data, err } -// GetZettelJSON returns a zettel as a JSON struct. -func (c *Client) GetZettelJSON(ctx context.Context, zid api.ZettelID) (*api.ZettelDataJSON, error) { - ub := c.newURLBuilder('z').SetZid(zid) - ub.AppendKVQuery(api.QueryKeyEncoding, api.EncodingJson) +// 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.AppendKVQuery(api.QueryKeyEncoding, api.EncodingData) ub.AppendKVQuery(api.QueryKeyPart, api.PartZettel) resp, err := c.buildAndExecuteRequest(ctx, http.MethodGet, ub, nil, nil) - if err != nil { - return nil, err - } - defer resp.Body.Close() - if resp.StatusCode != http.StatusOK { - return nil, statusToError(resp) - } - dec := json.NewDecoder(resp.Body) - var out api.ZettelDataJSON - err = dec.Decode(&out) - if err != nil { - return nil, err - } - return &out, nil + if err == nil { + defer resp.Body.Close() + if resp.StatusCode != http.StatusOK { + return api.ZettelData{}, statusToError(resp) + } + rdr := sxreader.MakeReader(resp.Body) + obj, err2 := rdr.Read() + if err2 == nil { + return sexp.ParseZettel(obj) + } + } + return api.ZettelData{}, err } // GetParsedZettel return a parsed zettel in a defined encoding. func (c *Client) GetParsedZettel(ctx context.Context, zid api.ZettelID, enc api.EncodingEnum) ([]byte, error) { return c.getZettelString(ctx, zid, enc, true) @@ -360,11 +527,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, "") } @@ -380,23 +547,23 @@ return nil, statusToError(resp) } return io.ReadAll(resp.Body) } -// GetParsedSexpr returns an parsed zettel as a Sexpr-decoded data structure. -func (c *Client) GetParsedSexpr(ctx context.Context, zid api.ZettelID, part string, sf sxpf.SymbolFactory) (sxpf.Object, error) { - return c.getSexpr(ctx, zid, part, true, sf) +// GetParsedSz returns an parsed zettel as a Sexpr-decoded data structure. +func (c *Client) GetParsedSz(ctx context.Context, zid api.ZettelID, part string) (sx.Object, error) { + return c.getSz(ctx, zid, part, true) } -// GetEvaluatedSexpr returns an evaluated zettel as a Sexpr-decoded data structure. -func (c *Client) GetEvaluatedSexpr(ctx context.Context, zid api.ZettelID, part string, sf sxpf.SymbolFactory) (sxpf.Object, error) { - return c.getSexpr(ctx, zid, part, false, sf) +// GetEvaluatedSz returns an evaluated zettel as a Sexpr-decoded data structure. +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) getSexpr(ctx context.Context, zid api.ZettelID, part string, parseOnly bool, sf sxpf.SymbolFactory) (sxpf.Object, error) { - ub := c.newURLBuilder('z').SetZid(zid) - ub.AppendKVQuery(api.QueryKeyEncoding, api.EncodingSexpr) +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 { ub.AppendKVQuery(api.QueryKeyParseOnly, "") @@ -407,127 +574,58 @@ } defer resp.Body.Close() if resp.StatusCode != http.StatusOK { return nil, statusToError(resp) } - return reader.MakeReader(bufio.NewReaderSize(resp.Body, 8), reader.WithSymbolFactory(sf)).Read() + return sxreader.MakeReader(bufio.NewReaderSize(resp.Body, 8)).Read() } -// GetMeta returns the metadata of a zettel. -func (c *Client) GetMeta(ctx context.Context, zid api.ZettelID) (api.ZettelMeta, error) { - ub := c.newURLBuilder('z').SetZid(zid) - ub.AppendKVQuery(api.QueryKeyEncoding, api.EncodingJson) +// 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.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 nil, err - } - defer resp.Body.Close() - if resp.StatusCode != http.StatusOK { - return nil, statusToError(resp) - } - dec := json.NewDecoder(resp.Body) - var out api.MetaJSON - err = dec.Decode(&out) - if err != nil { - return nil, err - } - return out.Meta, nil -} - -// GetZettelOrder returns metadata of the given zettel and, more important, -// metadata of zettel that are referenced in a list within the first zettel. -func (c *Client) GetZettelOrder(ctx context.Context, zid api.ZettelID) (*api.ZidMetaRelatedList, error) { - ub := c.newURLBuilder('o').SetZid(zid) - resp, err := c.buildAndExecuteRequest(ctx, http.MethodGet, ub, nil, nil) - if err != nil { - return nil, err - } - defer resp.Body.Close() - if resp.StatusCode != http.StatusOK { - return nil, statusToError(resp) - } - dec := json.NewDecoder(resp.Body) - var out api.ZidMetaRelatedList - err = dec.Decode(&out) - if err != nil { - return nil, err - } - return &out, nil -} - -// ContextDirection specifies how the context should be calculated. -type ContextDirection uint8 - -// Allowed values for ContextDirection -const ( - _ ContextDirection = iota - DirBoth - DirBackward - DirForward -) - -// GetZettelContext returns metadata of the given zettel and, more important, -// metadata of zettel that for the context of the first zettel. -func (c *Client) GetZettelContext( - ctx context.Context, zid api.ZettelID, dir ContextDirection, cost, limit int) ( - *api.ZidMetaRelatedList, error, -) { - ub := c.newURLBuilder('x').SetZid(zid) - switch dir { - case DirBackward: - ub.AppendKVQuery(api.QueryKeyDir, api.DirBackward) - case DirForward: - ub.AppendKVQuery(api.QueryKeyDir, api.DirForward) - } - if cost > 0 { - ub.AppendKVQuery(api.QueryKeyCost, strconv.Itoa(cost)) - } - if limit > 0 { - ub.AppendKVQuery(api.QueryKeyLimit, strconv.Itoa(limit)) - } - resp, err := c.buildAndExecuteRequest(ctx, http.MethodGet, ub, nil, nil) - if err != nil { - return nil, err - } - defer resp.Body.Close() - if resp.StatusCode != http.StatusOK { - return nil, statusToError(resp) - } - dec := json.NewDecoder(resp.Body) - var out api.ZidMetaRelatedList - err = dec.Decode(&out) - if err != nil { - return nil, err - } - return &out, nil -} - -// GetUnlinkedReferences returns connections to other zettel, embedded material, externals URLs. -func (c *Client) GetUnlinkedReferences( - ctx context.Context, zid api.ZettelID, query url.Values) (*api.ZidMetaRelatedList, error) { - ub := c.newQueryURLBuilder('u', query).SetZid(zid) - resp, err := c.buildAndExecuteRequest(ctx, http.MethodGet, ub, nil, nil) - if err != nil { - return nil, err - } - defer resp.Body.Close() - if resp.StatusCode != http.StatusOK { - return nil, statusToError(resp) - } - dec := json.NewDecoder(resp.Body) - var out api.ZidMetaRelatedList - err = dec.Decode(&out) - if err != nil { - return nil, err - } - return &out, nil + return api.MetaRights{}, err + } + defer resp.Body.Close() + rdr := sxreader.MakeReader(resp.Body) + obj, err := rdr.Read() + if resp.StatusCode != http.StatusOK { + return api.MetaRights{}, statusToError(resp) + } + if err != nil { + return api.MetaRights{}, err + } + vals, err := sexp.ParseList(obj, "ypp") + if err != nil { + return api.MetaRights{}, err + } + if errSym := sexp.CheckSymbol(vals[0], "list"); errSym != nil { + return api.MetaRights{}, err + } + + meta, err := sexp.ParseMeta(vals[1].(*sx.Pair)) + if err != nil { + return api.MetaRights{}, err + } + + rights, err := sexp.ParseRights(vals[2]) + if err != nil { + return api.MetaRights{}, err + } + + return api.MetaRights{ + Meta: meta, + Rights: rights, + }, 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() @@ -535,17 +633,17 @@ return statusToError(resp) } return nil } -// UpdateZettelJSON updates an existing zettel. -func (c *Client) UpdateZettelJSON(ctx context.Context, zid api.ZettelID, data *api.ZettelDataJSON) error { +// UpdateZettelData updates an existing zettel. +func (c *Client) UpdateZettelData(ctx context.Context, zid api.ZettelID, data api.ZettelData) error { var buf bytes.Buffer - if err := encodeZettelData(&buf, data); err != nil { + if _, err := sx.Print(&buf, sexp.EncodeZettel(data)); err != nil { return err } - ub := c.newURLBuilder('z').SetZid(zid).AppendKVQuery(api.QueryKeyEncoding, api.EncodingJson) + 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() @@ -555,13 +653,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 } @@ -572,11 +670,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() @@ -586,11 +684,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() @@ -598,64 +696,57 @@ return statusToError(resp) } return nil } -func (c *Client) newQueryURLBuilder(key byte, query url.Values) *api.URLBuilder { - ub := c.newURLBuilder(key) - for key, values := range query { - if key == api.QueryKeyEncoding { - continue - } - for _, val := range values { - ub.AppendKVQuery(key, val) - } - } - return ub -} - -// QueryMapMeta returns a map of all metadata values with the given query action to the -// list of zettel IDs containing this value. -func (c *Client) QueryMapMeta(ctx context.Context, query string) (api.MapMeta, error) { - err := c.updateToken(ctx) - if err != nil { - return nil, err - } - req, err := c.newRequest(ctx, http.MethodGet, c.newURLBuilder('z').AppendKVQuery(api.QueryKeyEncoding, api.EncodingJson).AppendQuery(query), nil) - if err != nil { - return nil, err - } - resp, err := c.executeRequest(req) - if err != nil { - return nil, err - } - defer resp.Body.Close() - if resp.StatusCode != http.StatusOK { - return nil, statusToError(resp) - } - dec := json.NewDecoder(resp.Body) - var mlj api.MapListJSON - err = dec.Decode(&mlj) - if err != nil { - return nil, err - } - return mlj.Map, nil -} - -// GetVersionJSON returns version information.. -func (c *Client) GetVersionJSON(ctx context.Context) (api.VersionJSON, error) { - resp, err := c.buildAndExecuteRequest(ctx, http.MethodGet, c.newURLBuilder('x'), nil, nil) - if err != nil { - return api.VersionJSON{}, err - } - defer resp.Body.Close() - if resp.StatusCode != http.StatusOK { - return api.VersionJSON{}, statusToError(resp) - } - dec := json.NewDecoder(resp.Body) - var version api.VersionJSON - err = dec.Decode(&version) - if err != nil { - return api.VersionJSON{}, err - } - return version, 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) + if err != nil { + return VersionInfo{}, err + } + defer resp.Body.Close() + if resp.StatusCode != http.StatusOK { + return VersionInfo{}, statusToError(resp) + } + rdr := sxreader.MakeReader(resp.Body) + obj, err := rdr.Read() + if err == nil { + 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).GetValue(), + Hash: vals[4].(sx.String).GetValue(), + }, nil + } + } + return VersionInfo{}, err +} + +// VersionInfo contains version information. +type VersionInfo struct { + Major int + 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 ( @@ -15,19 +18,17 @@ "flag" "net/http" "net/url" "testing" - "codeberg.org/t73fde/sxpf" - "zettelstore.de/c/api" - "zettelstore.de/c/client" - "zettelstore.de/c/sexpr" + "t73f.de/r/zsc/api" + "t73f.de/r/zsc/client" ) func TestZettelList(t *testing.T) { c := getClient() - _, err := c.ListZettel(context.Background(), "") + _, err := c.QueryZettel(context.Background(), "") if err != nil { t.Error(err) return } } @@ -43,16 +44,13 @@ } return } } -func TestGetSexprZettel(t *testing.T) { +func TestGetSzZettel(t *testing.T) { c := getClient() - sf := sxpf.MakeMappedFactory() - var zetSyms sexpr.ZettelSymbols - zetSyms.InitializeZettelSymbols(sf) - value, err := c.GetEvaluatedSexpr(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,8 +1,9 @@ -module zettelstore.de/c +module t73f.de/r/zsc -go 1.20 +go 1.22 require ( - codeberg.org/t73fde/sxhtml v0.0.0-20230317170051-24321195e197 - codeberg.org/t73fde/sxpf v0.0.0-20230319111333-7de220f3b475 + t73f.de/r/sx v0.0.0-20240418072254-b6eff7d787f9 + t73f.de/r/sxwebs v0.0.0-20240422143910-320427142398 + t73f.de/r/webs v0.0.0-20240422103534-8f5067bc11bc ) Index: go.sum ================================================================== --- go.sum +++ go.sum @@ -1,4 +1,6 @@ -codeberg.org/t73fde/sxhtml v0.0.0-20230317170051-24321195e197 h1:6kX7TY25agLFlHNvByO1Jc3GrBA7mu7aOa8tCOniUew= -codeberg.org/t73fde/sxhtml v0.0.0-20230317170051-24321195e197/go.mod h1:Dp3EwBSsE3TvdPw9QZ4Wm25ZragluVT2OayRFRiq6jk= -codeberg.org/t73fde/sxpf v0.0.0-20230319111333-7de220f3b475 h1:0OTzV3FYY/Y7YsaVaSzF4Wd17pXzdH6DaSvMeqteJc4= -codeberg.org/t73fde/sxpf v0.0.0-20230319111333-7de220f3b475/go.mod h1:iSbMygOmtRQYp8pryNKYzRuMibYDSR80smU2b6qm1bc= +t73f.de/r/sx v0.0.0-20240418072254-b6eff7d787f9 h1:lVPkYN8+J9f6JA9SmoF6icvpLxz4u3h1MCTuDYJYwdU= +t73f.de/r/sx v0.0.0-20240418072254-b6eff7d787f9/go.mod h1:G9pD1j2R6y9ZkPBb81mSnmwaAvTOg7r6jKp/OF7WeFA= +t73f.de/r/sxwebs v0.0.0-20240422143910-320427142398 h1:/G054FNxS8zEYbdhOTNk+GhdhjWBVt398FTm1Ud4A4o= +t73f.de/r/sxwebs v0.0.0-20240422143910-320427142398/go.mod h1:PtIkpRfTTiQITciKaWcTiAwy9FJ63WSQKciTp/dJbOA= +t73f.de/r/webs v0.0.0-20240422103534-8f5067bc11bc h1:i6tm/AEJUs8J8m7iDP8bTZgM0wYERh97RR47soJglxs= +t73f.de/r/webs v0.0.0-20240422103534-8f5067bc11bc/go.mod h1:UGAAtul0TK5ACeZ6zTS3SX6GqwMFXxlUpHiV8oqNq5w= ADDED input/entity.go Index: input/entity.go ================================================================== --- input/entity.go +++ 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 ================================================================== --- input/entity_test.go +++ 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" + + "t73f.de/r/zsc/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 ================================================================== --- input/input.go +++ 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 ================================================================== --- input/input_test.go +++ 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" + + "t73f.de/r/zsc/input" +) + +func TestEatEOL(t *testing.T) { + t.Parallel() + inp := input.NewInput(nil) + inp.EatEOL() + if inp.Ch != input.EOS { + t.Errorf("No EOS found: %q", inp.Ch) + } + if inp.Pos != 0 { + t.Errorf("Pos != 0: %d", inp.Pos) + } + + inp = input.NewInput([]byte("ABC")) + if inp.Ch != 'A' { + t.Errorf("First ch != 'A', got %q", inp.Ch) + } + inp.EatEOL() + if inp.Ch != 'A' { + t.Errorf("First ch != 'A', got %q", inp.Ch) + } +} + +func TestAccept(t *testing.T) { + t.Parallel() + testcases := []struct { + accept string + src string + acc bool + exp rune + }{ + {"", "", false, input.EOS}, + {"AB", "abc", false, 'a'}, + {"AB", "ABC", true, 'C'}, + {"AB", "AB", true, input.EOS}, + {"AB", "A", false, 'A'}, + } + for i, tc := range testcases { + inp := input.NewInput([]byte(tc.src)) + acc := inp.Accept(tc.accept) + if acc != tc.acc { + t.Errorf("%d: %q.Accept(%q) == %v, but got %v", i, tc.src, tc.accept, tc.acc, acc) + } + if got := inp.Ch; tc.exp != got { + t.Errorf("%d: %q.Accept(%q) should result in run %v, but got %v", i, tc.src, tc.accept, tc.exp, got) + } + } +} ADDED input/runes.go Index: input/runes.go ================================================================== --- input/runes.go +++ 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,18 +4,21 @@ // 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 ( "testing" - "zettelstore.de/c/maps" + "t73f.de/r/zsc/maps" ) func isSorted(seq []string) bool { for i := 1; i < len(seq); i++ { if seq[i] < seq[i-1] { ADDED sexp/sexp.go Index: sexp/sexp.go ================================================================== --- sexp/sexp.go +++ sexp/sexp.go @@ -0,0 +1,204 @@ +//----------------------------------------------------------------------------- +// Copyright (c) 2023-present Detlef Stern +// +// This file is part of zettelstore-client. +// +// Zettelstore client is licensed under the latest version of the EUPL +// (European Union Public License). Please see file LICENSE.txt for your rights +// and obligations under this license. +// +// SPDX-License-Identifier: EUPL-1.2 +// SPDX-FileCopyrightText: 2023-present Detlef Stern +//----------------------------------------------------------------------------- + +// Package sexp contains helper function to work with s-expression in an alien +// environment. +package sexp + +import ( + "errors" + "fmt" + "sort" + + "t73f.de/r/sx" + "t73f.de/r/zsc/api" +) + +// EncodeZettel transforms zettel data into a sx object. +func EncodeZettel(zettel api.ZettelData) sx.Object { + return sx.MakeList( + sx.MakeSymbol("zettel"), + meta2sz(zettel.Meta), + sx.MakeList(sx.MakeSymbol("rights"), sx.Int64(int64(zettel.Rights))), + sx.MakeList(sx.MakeSymbol("encoding"), sx.MakeString(zettel.Encoding)), + sx.MakeList(sx.MakeSymbol("content"), sx.MakeString(zettel.Content)), + ) +} + +func ParseZettel(obj sx.Object) (api.ZettelData, error) { + vals, err := ParseList(obj, "ypppp") + if err != nil { + return api.ZettelData{}, err + } + if errSym := CheckSymbol(vals[0], "zettel"); errSym != nil { + return api.ZettelData{}, errSym + } + + meta, err := ParseMeta(vals[1].(*sx.Pair)) + if err != nil { + return api.ZettelData{}, err + } + + rights, err := ParseRights(vals[2]) + if err != nil { + return api.ZettelData{}, err + } + + encVals, err := ParseList(vals[3], "ys") + if err != nil { + return api.ZettelData{}, err + } + if errSym := CheckSymbol(encVals[0], "encoding"); errSym != nil { + return api.ZettelData{}, errSym + } + + contentVals, err := ParseList(vals[4], "ys") + if err != nil { + return api.ZettelData{}, err + } + if errSym := CheckSymbol(contentVals[0], "content"); errSym != nil { + return api.ZettelData{}, errSym + } + + return api.ZettelData{ + Meta: meta, + Rights: rights, + Encoding: encVals[1].(sx.String).GetValue(), + Content: contentVals[1].(sx.String).GetValue(), + }, nil +} + +// EncodeMetaRights translates metadata/rights into a sx object. +func EncodeMetaRights(mr api.MetaRights) *sx.Pair { + return sx.MakeList( + sx.SymbolList, + meta2sz(mr.Meta), + sx.MakeList(sx.MakeSymbol("rights"), sx.Int64(int64(mr.Rights))), + ) +} + +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(sx.MakeSymbol(k), sx.MakeString(m[k])) + result.Add(val) + } + 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 { + return nil, err + } + res := api.ZettelMeta{} + 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)).GetValue()] = mVals[1].(sx.String).GetValue() + } + return res, nil +} + +// ParseRights returns the rights values of the given object. +func ParseRights(obj sx.Object) (api.ZettelRights, error) { + rVals, err := ParseList(obj, "yi") + if err != nil { + return api.ZettelMaxRight, err + } + if errSym := CheckSymbol(rVals[0], "rights"); errSym != nil { + return api.ZettelMaxRight, errSym + } + i64 := int64(rVals[1].(sx.Int64)) + if i64 < 0 && i64 >= int64(api.ZettelMaxRight) { + return api.ZettelMaxRight, fmt.Errorf("invalid zettel right value: %v", i64) + } + 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.Vector, error) { + pair, isPair := sx.GetPair(obj) + if !isPair { + return nil, fmt.Errorf("not a list: %T/%v", obj, obj) + } + if pair == nil { + if spec == "" { + return nil, nil + } + return nil, ErrElementsMissing + } + + result := make(sx.Vector, 0, len(spec)) + node, i := pair, 0 + for ; node != nil; i++ { + if i >= len(spec) { + return nil, ErrNoSpec + } + var val sx.Object + var ok bool + car := node.Car() + switch spec[i] { + case 'b': + val, ok = sx.MakeBoolean(!sx.IsNil(car)), true + case 'i': + val, ok = car.(sx.Int64) + case 'o': + val, ok = car, true + case 'p': + val, ok = sx.GetPair(car) + case 's': + val, ok = sx.GetString(car) + case 'y': + val, ok = sx.GetSymbol(car) + default: + return nil, fmt.Errorf("unknown spec '%c'", spec[i]) + } + if !ok { + return nil, fmt.Errorf("does not match spec '%v': %v", spec[i], car) + } + result = append(result, val) + next, isNextPair := sx.GetPair(node.Cdr()) + if !isNextPair { + return nil, sx.ErrImproper{Pair: pair} + } + node = next + } + if i < len(spec) { + return nil, ErrElementsMissing + } + return result, nil +} + +var ErrElementsMissing = errors.New("spec contains more data") +var ErrNoSpec = errors.New("no spec for elements") + +// CheckSymbol ensures that the given object is a symbol with the given name. +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.GetValue(); got != name { + return fmt.Errorf("symbol %q expected, but got: %q", name, got) + } + return nil +} ADDED sexp/sexp_test.go Index: sexp/sexp_test.go ================================================================== --- sexp/sexp_test.go +++ sexp/sexp_test.go @@ -0,0 +1,57 @@ +//----------------------------------------------------------------------------- +// Copyright (c) 2023-present Detlef Stern +// +// This file is part of zettelstore-client. +// +// Zettelstore client is licensed under the latest version of the EUPL +// (European Union Public License). Please see file LICENSE.txt for your rights +// and obligations under this license. +// +// SPDX-License-Identifier: EUPL-1.2 +// SPDX-FileCopyrightText: 2023-present Detlef Stern +//----------------------------------------------------------------------------- + +package sexp_test + +import ( + "testing" + + "t73f.de/r/sx" + "t73f.de/r/zsc/sexp" +) + +func TestParseObject(t *testing.T) { + if elems, err := sexp.ParseList(sx.MakeString("a"), "s"); err == nil { + t.Error("expected an error, but got: ", elems) + } + if elems, err := sexp.ParseList(sx.Nil(), ""); err != nil { + t.Error(err) + } else if len(elems) != 0 { + t.Error("Must be empty, but got:", elems) + } + if elems, err := sexp.ParseList(sx.Nil(), "b"); err == nil { + t.Error("expected error, but got: ", elems) + } + + if elems, err := sexp.ParseList(sx.MakeList(sx.MakeString("a")), "ss"); err == nil { + t.Error("expected error, but got: ", elems) + } + if elems, err := sexp.ParseList(sx.MakeList(sx.MakeString("a")), ""); err == nil { + t.Error("expected error, but got: ", elems) + } + if _, err := sexp.ParseList(sx.MakeList(sx.MakeString("a")), "b"); err != nil { + t.Error("expected [1], but got error: ", err) + } + if elems, err := sexp.ParseList(sx.Cons(sx.Nil(), sx.MakeString("a")), "ps"); err == nil { + t.Error("expected error, but got: ", elems) + } + + if elems, err := sexp.ParseList(sx.MakeList(sx.MakeString("a")), "s"); err != nil { + t.Error(err) + } else if len(elems) != 1 { + t.Error("length == 1, but got: ", elems) + } else { + _ = elems[0].(sx.String) + } + +} DELETED sexpr/const.go Index: sexpr/const.go ================================================================== --- sexpr/const.go +++ sexpr/const.go @@ -1,297 +0,0 @@ -//----------------------------------------------------------------------------- -// 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. -//----------------------------------------------------------------------------- - -package sexpr - -import "codeberg.org/t73fde/sxpf" - -// Various constants for Zettel data. Some of them are technically variables. - -const ( - // Symbols for Metanodes - NameSymBlock = "BLOCK" - NameSymInline = "INLINE" - NameSymList = "LIST" - NameSymMeta = "META" - NameSymQuote = "quote" - - // Symbols for Zettel node types. - NameSymBLOB = "BLOB" - NameSymCell = "CELL" - NameSymCellCenter = "CELL-CENTER" - NameSymCellLeft = "CELL-LEFT" - NameSymCellRight = "CELL-RIGHT" - NameSymCite = "CITE" - NameSymDescription = "DESCRIPTION" - NameSymEmbed = "EMBED" - NameSymEmbedBLOB = "EMBED-BLOB" - NameSymEndnote = "ENDNOTE" - NameSymFormatEmph = "FORMAT-EMPH" - NameSymFormatDelete = "FORMAT-DELETE" - NameSymFormatInsert = "FORMAT-INSERT" - NameSymFormatQuote = "FORMAT-QUOTE" - NameSymFormatSpan = "FORMAT-SPAN" - NameSymFormatSub = "FORMAT-SUB" - NameSymFormatSuper = "FORMAT-SUPER" - NameSymFormatStrong = "FORMAT-STRONG" - NameSymHard = "HARD" - NameSymHeading = "HEADING" - NameSymLinkInvalid = "LINK-INVALID" - NameSymLinkZettel = "LINK-ZETTEL" - NameSymLinkSelf = "LINK-SELF" - NameSymLinkFound = "LINK-FOUND" - NameSymLinkBroken = "LINK-BROKEN" - NameSymLinkHosted = "LINK-HOSTED" - NameSymLinkBased = "LINK-BASED" - NameSymLinkQuery = "LINK-QUERY" - NameSymLinkExternal = "LINK-EXTERNAL" - NameSymListOrdered = "ORDERED" - NameSymListUnordered = "UNORDERED" - NameSymListQuote = "QUOTATION" - NameSymLiteralProg = "LITERAL-CODE" - NameSymLiteralComment = "LITERAL-COMMENT" - NameSymLiteralHTML = "LITERAL-HTML" - NameSymLiteralInput = "LITERAL-INPUT" - NameSymLiteralMath = "LITERAL-MATH" - NameSymLiteralOutput = "LITERAL-OUTPUT" - NameSymLiteralZettel = "LITERAL-ZETTEL" - NameSymMark = "MARK" - NameSymPara = "PARA" - NameSymRegionBlock = "REGION-BLOCK" - NameSymRegionQuote = "REGION-QUOTE" - NameSymRegionVerse = "REGION-VERSE" - NameSymSoft = "SOFT" - NameSymSpace = "SPACE" - NameSymTable = "TABLE" - NameSymText = "TEXT" - NameSymThematic = "THEMATIC" - NameSymTransclude = "TRANSCLUDE" - NameSymUnknown = "UNKNOWN-NODE" - NameSymVerbatimComment = "VERBATIM-COMMENT" - NameSymVerbatimEval = "VERBATIM-EVAL" - NameSymVerbatimHTML = "VERBATIM-HTML" - NameSymVerbatimMath = "VERBATIM-MATH" - NameSymVerbatimProg = "VERBATIM-CODE" - NameSymVerbatimZettel = "VERBATIM-ZETTEL" - - // Constant symbols for reference states. - NameSymRefStateInvalid = "INVALID" - NameSymRefStateZettel = "ZETTEL" - NameSymRefStateSelf = "SELF" - NameSymRefStateFound = "FOUND" - NameSymRefStateBroken = "BROKEN" - NameSymRefStateHosted = "HOSTED" - NameSymRefStateBased = "BASED" - NameSymRefStateQuery = "QUERY" - NameSymRefStateExternal = "EXTERNAL" - - // Symbols for metadata types. - NameSymTypeCredential = "CREDENTIAL" - NameSymTypeEmpty = "EMPTY-STRING" - NameSymTypeID = "ZID" - NameSymTypeIDSet = "ZID-SET" - NameSymTypeNumber = "NUMBER" - NameSymTypeString = "STRING" - NameSymTypeTagSet = "TAG-SET" - NameSymTypeTimestamp = "TIMESTAMP" - NameSymTypeURL = "URL" - NameSymTypeWord = "WORD" - NameSymTypeWordSet = "WORD-SET" - NameSymTypeZettelmarkup = "ZETTELMARKUP" -) - -// ZettelSymbols collect all symbols needed to represent zettel data. -type ZettelSymbols struct { - // Symbols for Metanodes - SymBlock *sxpf.Symbol - SymInline *sxpf.Symbol - SymList *sxpf.Symbol - SymMeta *sxpf.Symbol - SymQuote *sxpf.Symbol - - // Symbols for Zettel node types. - SymBLOB *sxpf.Symbol - SymCell *sxpf.Symbol - SymCellCenter *sxpf.Symbol - SymCellLeft *sxpf.Symbol - SymCellRight *sxpf.Symbol - SymCite *sxpf.Symbol - SymDescription *sxpf.Symbol - SymEmbed *sxpf.Symbol - SymEmbedBLOB *sxpf.Symbol - SymEndnote *sxpf.Symbol - SymFormatEmph *sxpf.Symbol - SymFormatDelete *sxpf.Symbol - SymFormatInsert *sxpf.Symbol - SymFormatQuote *sxpf.Symbol - SymFormatSpan *sxpf.Symbol - SymFormatSub *sxpf.Symbol - SymFormatSuper *sxpf.Symbol - SymFormatStrong *sxpf.Symbol - SymHard *sxpf.Symbol - SymHeading *sxpf.Symbol - SymLinkInvalid *sxpf.Symbol - SymLinkZettel *sxpf.Symbol - SymLinkSelf *sxpf.Symbol - SymLinkFound *sxpf.Symbol - SymLinkBroken *sxpf.Symbol - SymLinkHosted *sxpf.Symbol - SymLinkBased *sxpf.Symbol - SymLinkQuery *sxpf.Symbol - SymLinkExternal *sxpf.Symbol - SymListOrdered *sxpf.Symbol - SymListUnordered *sxpf.Symbol - SymListQuote *sxpf.Symbol - SymLiteralProg *sxpf.Symbol - SymLiteralComment *sxpf.Symbol - SymLiteralHTML *sxpf.Symbol - SymLiteralInput *sxpf.Symbol - SymLiteralMath *sxpf.Symbol - SymLiteralOutput *sxpf.Symbol - SymLiteralZettel *sxpf.Symbol - SymMark *sxpf.Symbol - SymPara *sxpf.Symbol - SymRegionBlock *sxpf.Symbol - SymRegionQuote *sxpf.Symbol - SymRegionVerse *sxpf.Symbol - SymSoft *sxpf.Symbol - SymSpace *sxpf.Symbol - SymTable *sxpf.Symbol - SymText *sxpf.Symbol - SymThematic *sxpf.Symbol - SymTransclude *sxpf.Symbol - SymUnknown *sxpf.Symbol - SymVerbatimComment *sxpf.Symbol - SymVerbatimEval *sxpf.Symbol - SymVerbatimHTML *sxpf.Symbol - SymVerbatimMath *sxpf.Symbol - SymVerbatimProg *sxpf.Symbol - SymVerbatimZettel *sxpf.Symbol - - // Constant symbols for reference states. - - SymRefStateInvalid *sxpf.Symbol - SymRefStateZettel *sxpf.Symbol - SymRefStateSelf *sxpf.Symbol - SymRefStateFound *sxpf.Symbol - SymRefStateBroken *sxpf.Symbol - SymRefStateHosted *sxpf.Symbol - SymRefStateBased *sxpf.Symbol - SymRefStateQuery *sxpf.Symbol - SymRefStateExternal *sxpf.Symbol - - // Symbols for metadata types - - SymTypeCredential *sxpf.Symbol - SymTypeEmpty *sxpf.Symbol - SymTypeID *sxpf.Symbol - SymTypeIDSet *sxpf.Symbol - SymTypeNumber *sxpf.Symbol - SymTypeString *sxpf.Symbol - SymTypeTagSet *sxpf.Symbol - SymTypeTimestamp *sxpf.Symbol - SymTypeURL *sxpf.Symbol - SymTypeWord *sxpf.Symbol - SymTypeWordSet *sxpf.Symbol - SymTypeZettelmarkup *sxpf.Symbol -} - -func (zs *ZettelSymbols) InitializeZettelSymbols(sf sxpf.SymbolFactory) { - // Symbols for Metanodes - zs.SymBlock = sf.MustMake(NameSymBlock) - zs.SymInline = sf.MustMake(NameSymInline) - zs.SymList = sf.MustMake(NameSymList) - zs.SymMeta = sf.MustMake(NameSymMeta) - zs.SymQuote = sf.MustMake(NameSymQuote) - - // Symbols for Zettel node types. - zs.SymBLOB = sf.MustMake(NameSymBLOB) - zs.SymCell = sf.MustMake(NameSymCell) - zs.SymCellCenter = sf.MustMake(NameSymCellCenter) - zs.SymCellLeft = sf.MustMake(NameSymCellLeft) - zs.SymCellRight = sf.MustMake(NameSymCellRight) - zs.SymCite = sf.MustMake(NameSymCite) - zs.SymDescription = sf.MustMake(NameSymDescription) - zs.SymEmbed = sf.MustMake(NameSymEmbed) - zs.SymEmbedBLOB = sf.MustMake(NameSymEmbedBLOB) - zs.SymEndnote = sf.MustMake(NameSymEndnote) - zs.SymFormatEmph = sf.MustMake(NameSymFormatEmph) - zs.SymFormatDelete = sf.MustMake(NameSymFormatDelete) - zs.SymFormatInsert = sf.MustMake(NameSymFormatInsert) - zs.SymFormatQuote = sf.MustMake(NameSymFormatQuote) - zs.SymFormatSpan = sf.MustMake(NameSymFormatSpan) - zs.SymFormatSub = sf.MustMake(NameSymFormatSub) - zs.SymFormatSuper = sf.MustMake(NameSymFormatSuper) - zs.SymFormatStrong = sf.MustMake(NameSymFormatStrong) - zs.SymHard = sf.MustMake(NameSymHard) - zs.SymHeading = sf.MustMake(NameSymHeading) - zs.SymLinkInvalid = sf.MustMake(NameSymLinkInvalid) - zs.SymLinkZettel = sf.MustMake(NameSymLinkZettel) - zs.SymLinkSelf = sf.MustMake(NameSymLinkSelf) - zs.SymLinkFound = sf.MustMake(NameSymLinkFound) - zs.SymLinkBroken = sf.MustMake(NameSymLinkBroken) - zs.SymLinkHosted = sf.MustMake(NameSymLinkHosted) - zs.SymLinkBased = sf.MustMake(NameSymLinkBased) - zs.SymLinkQuery = sf.MustMake(NameSymLinkQuery) - zs.SymLinkExternal = sf.MustMake(NameSymLinkExternal) - zs.SymListOrdered = sf.MustMake(NameSymListOrdered) - zs.SymListUnordered = sf.MustMake(NameSymListUnordered) - zs.SymListQuote = sf.MustMake(NameSymListQuote) - zs.SymLiteralProg = sf.MustMake(NameSymLiteralProg) - zs.SymLiteralComment = sf.MustMake(NameSymLiteralComment) - zs.SymLiteralHTML = sf.MustMake(NameSymLiteralHTML) - zs.SymLiteralInput = sf.MustMake(NameSymLiteralInput) - zs.SymLiteralMath = sf.MustMake(NameSymLiteralMath) - zs.SymLiteralOutput = sf.MustMake(NameSymLiteralOutput) - zs.SymLiteralZettel = sf.MustMake(NameSymLiteralZettel) - zs.SymMark = sf.MustMake(NameSymMark) - zs.SymPara = sf.MustMake(NameSymPara) - zs.SymRegionBlock = sf.MustMake(NameSymRegionBlock) - zs.SymRegionQuote = sf.MustMake(NameSymRegionQuote) - zs.SymRegionVerse = sf.MustMake(NameSymRegionVerse) - zs.SymSoft = sf.MustMake(NameSymSoft) - zs.SymSpace = sf.MustMake(NameSymSpace) - zs.SymTable = sf.MustMake(NameSymTable) - zs.SymText = sf.MustMake(NameSymText) - zs.SymThematic = sf.MustMake(NameSymThematic) - zs.SymTransclude = sf.MustMake(NameSymTransclude) - zs.SymUnknown = sf.MustMake(NameSymUnknown) - zs.SymVerbatimComment = sf.MustMake(NameSymVerbatimComment) - zs.SymVerbatimEval = sf.MustMake(NameSymVerbatimEval) - zs.SymVerbatimHTML = sf.MustMake(NameSymVerbatimHTML) - zs.SymVerbatimMath = sf.MustMake(NameSymVerbatimMath) - zs.SymVerbatimProg = sf.MustMake(NameSymVerbatimProg) - zs.SymVerbatimZettel = sf.MustMake(NameSymVerbatimZettel) - - // Constant symbols for reference states. - zs.SymRefStateInvalid = sf.MustMake(NameSymRefStateInvalid) - zs.SymRefStateZettel = sf.MustMake(NameSymRefStateZettel) - zs.SymRefStateSelf = sf.MustMake(NameSymRefStateSelf) - zs.SymRefStateFound = sf.MustMake(NameSymRefStateFound) - zs.SymRefStateBroken = sf.MustMake(NameSymRefStateBroken) - zs.SymRefStateHosted = sf.MustMake(NameSymRefStateHosted) - zs.SymRefStateBased = sf.MustMake(NameSymRefStateBased) - zs.SymRefStateQuery = sf.MustMake(NameSymRefStateQuery) - zs.SymRefStateExternal = sf.MustMake(NameSymRefStateExternal) - - // Symbols for metadata types. - zs.SymTypeCredential = sf.MustMake(NameSymTypeCredential) - zs.SymTypeEmpty = sf.MustMake(NameSymTypeEmpty) - zs.SymTypeID = sf.MustMake(NameSymTypeID) - zs.SymTypeIDSet = sf.MustMake(NameSymTypeIDSet) - zs.SymTypeNumber = sf.MustMake(NameSymTypeNumber) - zs.SymTypeString = sf.MustMake(NameSymTypeString) - zs.SymTypeTagSet = sf.MustMake(NameSymTypeTagSet) - zs.SymTypeTimestamp = sf.MustMake(NameSymTypeTimestamp) - zs.SymTypeURL = sf.MustMake(NameSymTypeURL) - zs.SymTypeWord = sf.MustMake(NameSymTypeWord) - zs.SymTypeWordSet = sf.MustMake(NameSymTypeWordSet) - zs.SymTypeZettelmarkup = sf.MustMake(NameSymTypeZettelmarkup) -} DELETED sexpr/const_test.go Index: sexpr/const_test.go ================================================================== --- sexpr/const_test.go +++ sexpr/const_test.go @@ -1,26 +0,0 @@ -//----------------------------------------------------------------------------- -// Copyright (c) 2023-present Detlef Stern -// -// This file is part of zettelstore-client. -// -// Zettelstore client is licensed under the latest version of the EUPL -// (European Union Public License). Please see file LICENSE.txt for your rights -// and obligations under this license. -//----------------------------------------------------------------------------- - -package sexpr_test - -import ( - "testing" - - "codeberg.org/t73fde/sxpf" - "zettelstore.de/c/sexpr" -) - -func BenchmarkInitializeZettelSymbols(b *testing.B) { - sf := sxpf.MakeMappedFactory() - for i := 0; i < b.N; i++ { - var zs sexpr.ZettelSymbols - zs.InitializeZettelSymbols(sf) - } -} DELETED sexpr/sexpr.go Index: sexpr/sexpr.go ================================================================== --- sexpr/sexpr.go +++ sexpr/sexpr.go @@ -1,126 +0,0 @@ -//----------------------------------------------------------------------------- -// 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. -//----------------------------------------------------------------------------- - -package sexpr - -import ( - "codeberg.org/t73fde/sxpf" - "zettelstore.de/c/attrs" -) - -// GetAttributes traverses a s-expression list and returns an attribute structure. -func GetAttributes(seq *sxpf.List) (result attrs.Attributes) { - for elem := seq; elem != nil; elem = elem.Tail() { - p, ok := elem.Car().(*sxpf.List) - if !ok || p == nil { - continue - } - key := p.Car() - if !sxpf.IsAtom(key) { - continue - } - val := p.Cdr() - if tail, ok2 := val.(*sxpf.List); ok2 { - val = tail.Car() - } - if !sxpf.IsAtom(val) { - continue - } - result = result.Set(key.String(), val.String()) - } - return result -} - -// GetMetaContent returns the metadata and the content of a sexpr encoded zettel. -func GetMetaContent(zettel sxpf.Object) (Meta, *sxpf.List) { - if pair, ok := zettel.(*sxpf.List); ok { - m := pair.Car() - if s := pair.Tail(); s != nil { - if content, ok2 := s.Car().(*sxpf.List); ok2 { - return MakeMeta(m), content - } - } - return MakeMeta(m), nil - } - return nil, nil -} - -type Meta map[string]MetaValue -type MetaValue struct { - Type string - Key string - Value sxpf.Object -} - -func MakeMeta(val sxpf.Object) Meta { - if result := doMakeMeta(val); len(result) > 0 { - return result - } - return nil -} -func doMakeMeta(val sxpf.Object) Meta { - result := make(map[string]MetaValue) - for { - if sxpf.IsNil(val) { - return result - } - lst, ok := val.(*sxpf.List) - if !ok { - return result - } - if mv, ok2 := makeMetaValue(lst); ok2 { - result[mv.Key] = mv - } - val = lst.Cdr() - } -} -func makeMetaValue(pair *sxpf.List) (MetaValue, bool) { - var result MetaValue - typePair, ok := pair.Car().(*sxpf.List) - if !ok { - return result, false - } - typeVal, ok := typePair.Car().(*sxpf.Symbol) - if !ok { - return result, false - } - keyPair, ok := typePair.Cdr().(*sxpf.List) - if !ok { - return result, false - } - keyStr, ok := keyPair.Car().(sxpf.String) - if !ok { - return result, false - } - valPair, ok := keyPair.Cdr().(*sxpf.List) - if !ok { - return result, false - } - result.Type = typeVal.CanonicalName() - result.Key = keyStr.String() - result.Value = valPair.Car() - return result, true -} - -func (m Meta) GetString(key string) string { - if v, found := m[key]; found { - return v.Value.String() - } - return "" -} - -func (m Meta) GetList(key string) *sxpf.List { - if mv, found := m[key]; found { - if seq, ok := mv.Value.(*sxpf.List); ok { - return seq - } - } - return nil -} ADDED shtml/const.go Index: shtml/const.go ================================================================== --- shtml/const.go +++ 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 "t73f.de/r/sx" + +// 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 @@ -15,870 +18,997 @@ "fmt" "net/url" "strconv" "strings" - "codeberg.org/t73fde/sxhtml" - "codeberg.org/t73fde/sxpf" - "codeberg.org/t73fde/sxpf/builtins/quote" - "codeberg.org/t73fde/sxpf/eval" - "zettelstore.de/c/api" - "zettelstore.de/c/attrs" - "zettelstore.de/c/sexpr" - "zettelstore.de/c/text" -) - -// Transformer will transform a s-expression that encodes the zettel AST into an s-expression -// that represents HTML. -type Transformer struct { - sf sxpf.SymbolFactory - rebinder RebindProc + "t73f.de/r/sx" + "t73f.de/r/sxwebs/sxhtml" + "t73f.de/r/zsc/api" + "t73f.de/r/zsc/attrs" + "t73f.de/r/zsc/sz" + "t73f.de/r/zsc/text" +) + +// Evaluator will transform a s-expression that encodes the zettel AST into an s-expression +// that represents HTML. +type Evaluator struct { headingOffset int64 unique string - endnotes []endnoteInfo noLinks bool // true iff output must not include links - symAttr *sxpf.Symbol - symClass *sxpf.Symbol - symMeta *sxpf.Symbol - symA *sxpf.Symbol - symSpan *sxpf.Symbol -} - -type endnoteInfo struct { - noteAST *sxpf.List // Endnote as AST - noteHx *sxpf.List // Endnote as SxHTML - attrs *sxpf.List // attrs a-list -} - -// NewTransformer creates a new transformer object. -func NewTransformer(headingOffset int, sf sxpf.SymbolFactory) *Transformer { - if sf == nil { - sf = sxpf.MakeMappedFactory() - } - return &Transformer{ - sf: sf, - rebinder: nil, + + fns map[string]EvalFn + minArgs map[string]int +} + +// NewEvaluator creates a new Evaluator object. +func NewEvaluator(headingOffset int) *Evaluator { + ev := &Evaluator{ headingOffset: int64(headingOffset), - symAttr: sf.MustMake(sxhtml.NameSymAttr), - symClass: sf.MustMake("class"), - symMeta: sf.MustMake("meta"), - symA: sf.MustMake("a"), - symSpan: sf.MustMake("span"), - } -} - -// SymbolFactory returns the symbol factory to create HTML symbols. -func (tr *Transformer) SymbolFactory() sxpf.SymbolFactory { return tr.sf } + + fns: make(map[string]EvalFn, 128), + minArgs: make(map[string]int, 128), + } + ev.bindMetadata() + ev.bindBlocks() + ev.bindInlines() + return ev +} // SetUnique sets a prefix to make several HTML ids unique. -func (tr *Transformer) SetUnique(s string) { tr.unique = s } +func (tr *Evaluator) SetUnique(s string) { tr.unique = s } // IsValidName returns true, if name is a valid symbol name. -func (tr *Transformer) IsValidName(s string) bool { return tr.sf.IsValidName(s) } - -// Make a new HTML symbol. -func (tr *Transformer) Make(s string) *sxpf.Symbol { return tr.sf.MustMake(s) } - -// RebindProc is a procedure which is called every time before a tranformation takes place. -type RebindProc func(*TransformEnv) - -// SetRebinder sets the rebinder procedure. -func (tr *Transformer) SetRebinder(rb RebindProc) { tr.rebinder = rb } - -// TransformAttrbute transforms the given attributes into a HTML s-expression. -func (tr *Transformer) TransformAttrbute(a attrs.Attributes) *sxpf.List { +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 sxpf.Nil() + return nil } - plist := sxpf.Nil() + 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(sxpf.Cons(tr.Make(key), sxpf.MakeString(a[key]))) + plist = plist.Cons(sx.Cons(sx.MakeSymbol(key), sx.MakeString(a[key]))) } } if plist == nil { - return sxpf.Nil() - } - return plist.Cons(tr.symAttr) -} - -// TransformMeta creates a HTML meta s-expression -func (tr *Transformer) TransformMeta(a attrs.Attributes) *sxpf.List { - return sxpf.Nil().Cons(tr.TransformAttrbute(a)).Cons(tr.symMeta) -} - -// Transform an AST s-expression into a list of HTML s-expressions. -func (tr *Transformer) Transform(lst *sxpf.List) (*sxpf.List, error) { - astSF := sxpf.FindSymbolFactory(lst) - if astSF != nil { - if astSF == tr.sf { - panic("Invalid AST SymbolFactory") - } - } else { - astSF = sxpf.MakeMappedFactory() - } - astEnv := sxpf.MakeRootEnvironment() - engine := eval.MakeEngine(astSF, astEnv, eval.MakeDefaultParser(), eval.MakeSimpleExecutor()) - quote.InstallQuote(engine, sexpr.NameSymQuote, nil, 0) - te := TransformEnv{ - tr: tr, - astSF: astSF, - astEnv: astEnv, - err: nil, - textEnc: text.NewEncoder(astSF), - } - te.initialize() - if rb := tr.rebinder; rb != nil { - rb(&te) - } - - val, err := engine.Eval(te.astEnv, lst) - if err != nil { - return sxpf.Nil(), err - } - res, ok := val.(*sxpf.List) - if !ok { - panic("Result is not a list") - } - for i := 0; i < len(tr.endnotes); i++ { - // May extend tr.endnotes - val, err = engine.Eval(te.astEnv, tr.endnotes[i].noteAST) - if err != nil { - return res, err - } - en, ok2 := val.(*sxpf.List) - if !ok2 { - panic("Endnote is not a list") - } - tr.endnotes[i].noteHx = en - } - return res, err - + return nil + } + return plist.Cons(sxhtml.SymAttr) +} + +// Evaluate a metadata s-expression into a list of HTML s-expressions. +func (ev *Evaluator) Evaluate(lst *sx.Pair, env *Environment) (*sx.Pair, error) { + result := ev.Eval(lst, env) + if err := env.err; err != nil { + return nil, err + } + 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 -> do not use for i := range len(...)!!! + + if env.endnotes[i].noteHx != nil { + continue + } + + 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 (tr *Transformer) Endnotes() *sxpf.List { - if len(tr.endnotes) == 0 { +func (ev *Evaluator) Endnotes(env *Environment) *sx.Pair { + if env.err != nil || len(env.endnotes) == 0 { return nil } - result := sxpf.Nil().Cons(tr.Make("ol")) - currResult := result.AppendBang(sxpf.Nil().Cons(sxpf.Cons(tr.symClass, sxpf.MakeString("zs-endnotes"))).Cons(tr.symAttr)) - for i, fni := range tr.endnotes { + + var result sx.ListBuilder + result.Add(SymOL) + result.Add(sx.Nil().Cons(sx.Cons(SymAttrClass, sx.MakeString("zs-endnotes"))).Cons(sxhtml.SymAttr)) + for i, fni := range env.endnotes { noteNum := strconv.Itoa(i + 1) - noteID := tr.unique + noteNum - - attrs := fni.attrs.Cons(sxpf.Cons(tr.symClass, sxpf.MakeString("zs-endnote"))). - Cons(sxpf.Cons(tr.Make("value"), sxpf.MakeString(noteNum))). - Cons(sxpf.Cons(tr.Make("id"), sxpf.MakeString("fn:"+noteID))). - Cons(sxpf.Cons(tr.Make("role"), sxpf.MakeString("doc-endnote"))). - Cons(tr.symAttr) - - backref := sxpf.Nil().Cons(sxpf.MakeString("\u21a9\ufe0e")). - Cons(sxpf.Nil(). - Cons(sxpf.Cons(tr.symClass, sxpf.MakeString("zs-endnote-backref"))). - Cons(sxpf.Cons(tr.Make("href"), sxpf.MakeString("#fnref:"+noteID))). - Cons(sxpf.Cons(tr.Make("role"), sxpf.MakeString("doc-backlink"))). - Cons(tr.symAttr)). - Cons(tr.symA) - - li := sxpf.Nil().Cons(tr.Make("li")) - li.AppendBang(attrs). - ExtendBang(fni.noteHx). - AppendBang(sxpf.MakeString(" ")).AppendBang(backref) - currResult = currResult.AppendBang(li) - } - tr.endnotes = nil - return result -} - -// TransformEnv is the environment where the actual transformation takes places. -type TransformEnv struct { - tr *Transformer - astSF sxpf.SymbolFactory - astEnv sxpf.Environment - err error - textEnc *text.Encoder - symNoEscape *sxpf.Symbol - symAttr *sxpf.Symbol - symMeta *sxpf.Symbol - symA *sxpf.Symbol - symSpan *sxpf.Symbol - symP *sxpf.Symbol -} - -func (te *TransformEnv) initialize() { - te.symNoEscape = te.Make(sxhtml.NameSymNoEscape) - te.symAttr = te.tr.symAttr - te.symMeta = te.tr.symMeta - te.symA = te.tr.symA - te.symSpan = te.tr.symSpan - te.symP = te.Make("p") - - te.bind(sexpr.NameSymList, 0, listArgs) - te.bindMetadata() - te.bindBlocks() - te.bindInlines() -} - -func listArgs(args *sxpf.List) sxpf.Object { return args } - -func (te *TransformEnv) bindMetadata() { - te.bind(sexpr.NameSymMeta, 0, listArgs) - te.bind(sexpr.NameSymTypeZettelmarkup, 2, func(args *sxpf.List) sxpf.Object { - a := make(attrs.Attributes, 2). - Set("name", te.getString(args).String()). - Set("content", te.textEnc.Encode(te.getList(args.Tail()))) - return te.transformMeta(a) - }) - metaString := func(args *sxpf.List) sxpf.Object { - a := make(attrs.Attributes, 2). - Set("name", te.getString(args).String()). - Set("content", te.getString(args.Tail()).String()) - return te.transformMeta(a) - } - te.bind(sexpr.NameSymTypeCredential, 2, metaString) - te.bind(sexpr.NameSymTypeEmpty, 2, metaString) - te.bind(sexpr.NameSymTypeID, 2, metaString) - te.bind(sexpr.NameSymTypeNumber, 2, metaString) - te.bind(sexpr.NameSymTypeString, 2, metaString) - te.bind(sexpr.NameSymTypeTimestamp, 2, metaString) - te.bind(sexpr.NameSymTypeURL, 2, metaString) - te.bind(sexpr.NameSymTypeWord, 2, metaString) - metaSet := func(args *sxpf.List) sxpf.Object { - var sb strings.Builder - for elem := te.getList(args.Tail()); elem != nil; elem = elem.Tail() { - sb.WriteByte(' ') - sb.WriteString(te.getString(elem).String()) + 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) + + 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(SymA) + + var li sx.ListBuilder + li.Add(SymLI) + li.Add(attrs) + li.ExtendBang(fni.noteHx) + li.Add(sx.MakeString(" ")) + li.Add(backref) + result.Add(li.List()) + } + return result.List() +} + +// Environment where sz objects are evaluated to shtml objects +type Environment struct { + err error + langStack []string + endnotes []endnoteInfo + quoteNesting uint +} +type endnoteInfo struct { + noteID string // link id + 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. +func MakeEnvironment(lang string) Environment { + langStack := make([]string, 1, 16) + langStack[0] = lang + return Environment{ + err: nil, + langStack: langStack, + endnotes: nil, + quoteNesting: 0, + } +} + +// GetError returns the last error found. +func (env *Environment) GetError() error { return env.err } + +// Reset the environment. +func (env *Environment) Reset() { + env.langStack = env.langStack[0:1] + env.endnotes = nil + env.quoteNesting = 0 +} + +// PushAttribute adds the current attributes to the environment. +func (env *Environment) pushAttributes(a attrs.Attributes) { + if value, ok := a.Get("lang"); ok { + env.langStack = append(env.langStack, value) + } else { + env.langStack = append(env.langStack, env.getLanguage()) + } +} + +// popAttributes removes the current attributes from the envrionment +func (env *Environment) popAttributes() { + env.langStack = env.langStack[0 : len(env.langStack)-1] +} + +// getLanguage returns the current language +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.Vector, *Environment) sx.Object + +func (ev *Evaluator) bind(sym *sx.Symbol, minArgs int, fn EvalFn) { + symVal := sym.GetValue() + ev.fns[symVal] = fn + if minArgs > 0 { + ev.minArgs[symVal] = minArgs + } +} + +// ResolveBinding returns the function bound to the given name. +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(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.SymMeta, 0, ev.evalList) + evalMetaString := func(args sx.Vector, env *Environment) sx.Object { + a := make(attrs.Attributes, 2). + Set("name", ev.getSymbol(args[0], env).GetValue()). + Set("content", getString(args[1], env).GetValue()) + return ev.EvaluateMeta(a) + } + ev.bind(sz.SymTypeCredential, 2, evalMetaString) + ev.bind(sz.SymTypeEmpty, 2, evalMetaString) + ev.bind(sz.SymTypeID, 2, evalMetaString) + 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 + for elem := getList(args[1], env); elem != nil; elem = elem.Tail() { + sb.WriteByte(' ') + sb.WriteString(getString(elem.Car(), env).GetValue()) } s := sb.String() if len(s) > 0 { s = s[1:] } a := make(attrs.Attributes, 2). - Set("name", te.getString(args).String()). - Set("content", s) - return te.transformMeta(a) - } - te.bind(sexpr.NameSymTypeIDSet, 2, metaSet) - te.bind(sexpr.NameSymTypeTagSet, 2, metaSet) - te.bind(sexpr.NameSymTypeWordSet, 2, metaSet) -} - -func (te *TransformEnv) bindBlocks() { - te.bind(sexpr.NameSymBlock, 0, listArgs) - te.bind(sexpr.NameSymPara, 0, func(args *sxpf.List) sxpf.Object { - for ; args != nil; args = args.Tail() { - lst, ok := sxpf.GetList(args.Car()) - if !ok || lst != nil { - break - } - } - return args.Cons(te.symP) - }) - te.bind(sexpr.NameSymHeading, 5, func(args *sxpf.List) sxpf.Object { - nLevel := te.getInt64(args) - if nLevel <= 0 { - te.err = fmt.Errorf("%v is a negative level", nLevel) - return sxpf.Nil() - } - level := strconv.FormatInt(nLevel+te.tr.headingOffset, 10) - - argAttr := args.Tail() - a := te.getAttributes(argAttr) - argFragment := argAttr.Tail().Tail() - if fragment := te.getString(argFragment).String(); fragment != "" { - a = a.Set("id", te.tr.unique+fragment) - } - - if result, ok := sxpf.GetList(argFragment.Tail().Car()); ok && result != nil { - if len(a) > 0 { - result = result.Cons(te.transformAttribute(a)) - } - return result.Cons(te.Make("h" + level)) - } - return sxpf.MakeList(te.Make("h"+level), sxpf.MakeString("")) - }) - te.bind(sexpr.NameSymThematic, 0, func(args *sxpf.List) sxpf.Object { - result := sxpf.Nil() - if args != nil { - if attrList := te.getList(args); attrList != nil { - result = result.Cons(te.transformAttribute(sexpr.GetAttributes(attrList))) - } - } - return result.Cons(te.Make("hr")) - }) - te.bind(sexpr.NameSymListOrdered, 0, te.makeListFn("ol")) - te.bind(sexpr.NameSymListUnordered, 0, te.makeListFn("ul")) - te.bind(sexpr.NameSymDescription, 0, func(args *sxpf.List) sxpf.Object { - if args == nil { - return sxpf.Nil() - } - items := sxpf.Nil().Cons(te.Make("dl")) - curItem := items - for elem := args; elem != nil; elem = elem.Tail() { - term := te.getList(elem) - curItem = curItem.AppendBang(term.Cons(te.Make("dt"))) - elem = elem.Tail() - if elem == nil { - break - } - ddBlock := te.getList(elem) - if ddBlock == nil { - break - } - for ddlst := ddBlock; ddlst != nil; ddlst = ddlst.Tail() { - dditem := te.getList(ddlst) - curItem = curItem.AppendBang(dditem.Cons(te.Make("dd"))) - } - } - return items - }) - - te.bind(sexpr.NameSymListQuote, 0, func(args *sxpf.List) sxpf.Object { - if args == nil { - return sxpf.Nil() - } - result := sxpf.Nil().Cons(te.Make("blockquote")) - currResult := result - for elem := args; elem != nil; elem = elem.Tail() { - if quote, ok := elem.Car().(*sxpf.List); ok { - currResult = currResult.AppendBang(quote.Cons(te.symP)) - } - } - return result - }) - - te.bind(sexpr.NameSymTable, 1, func(args *sxpf.List) sxpf.Object { - thead := sxpf.Nil() - if header := te.getList(args); header != nil { - thead = sxpf.Nil().Cons(te.transformTableRow(header)).Cons(te.Make("thead")) - } - - tbody := sxpf.Nil() - if argBody := args.Tail(); argBody != nil { - tbody = sxpf.Nil().Cons(te.Make("tbody")) - curBody := tbody - for row := argBody; row != nil; row = row.Tail() { - curBody = curBody.AppendBang(te.transformTableRow(te.getList(row))) - } - } - - table := sxpf.Nil() - if tbody != nil { - table = table.Cons(tbody) + Set("name", ev.getSymbol(args[0], env).GetValue()). + Set("content", s) + return ev.EvaluateMeta(a) + } + 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(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(SymMeta) +} + +func (ev *Evaluator) bindBlocks() { + ev.bind(sz.SymBlock, 0, ev.evalList) + ev.bind(sz.SymPara, 0, func(args sx.Vector, env *Environment) sx.Object { + return ev.evalSlice(args, env).Cons(SymP) + }) + ev.bind(sz.SymHeading, 5, func(args sx.Vector, env *Environment) sx.Object { + nLevel := getInt64(args[0], env) + if nLevel <= 0 { + env.err = fmt.Errorf("%v is a negative level", nLevel) + return sx.Nil() + } + level := strconv.FormatInt(nLevel+ev.headingOffset, 10) + headingSymbol := sx.MakeSymbol("h" + level) + + a := ev.GetAttributes(args[1], env) + env.pushAttributes(a) + defer env.popAttributes() + if fragment := getString(args[3], env).GetValue(); fragment != "" { + a = a.Set("id", ev.unique+fragment) + } + + if result, _ := ev.EvaluateList(args[4:], env); result != nil { + if len(a) > 0 { + result = result.Cons(ev.EvaluateAttrbute(a)) + } + return result.Cons(headingSymbol) + } + return sx.MakeList(headingSymbol, sx.MakeString("")) + }) + ev.bind(sz.SymThematic, 0, func(args sx.Vector, env *Environment) sx.Object { + result := sx.Nil() + if len(args) > 0 { + if attrList := getList(args[0], env); attrList != nil { + result = result.Cons(ev.EvaluateAttrbute(sz.GetAttributes(attrList))) + } + } + return result.Cons(SymHR) + }) + + ev.bind(sz.SymListOrdered, 0, ev.makeListFn(SymOL)) + ev.bind(sz.SymListUnordered, 0, ev.makeListFn(SymUL)) + ev.bind(sz.SymDescription, 0, func(args sx.Vector, env *Environment) sx.Object { + if len(args) == 0 { + return sx.Nil() + } + var items sx.ListBuilder + items.Add(symDL) + for pos := 0; pos < len(args); pos++ { + term := ev.evalDescriptionTerm(getList(args[pos], env), env) + items.Add(term.Cons(symDT)) + pos++ + if pos >= len(args) { + break + } + ddBlock := getList(ev.Eval(args[pos], env), env) + if ddBlock == nil { + continue + } + for ddlst := ddBlock; ddlst != nil; ddlst = ddlst.Tail() { + dditem := getList(ddlst.Car(), env) + items.Add(dditem.Cons(symDD)) + } + } + return items.List() + }) + ev.bind(sz.SymListQuote, 0, func(args sx.Vector, env *Environment) sx.Object { + if args == nil { + return sx.Nil() + } + var result sx.ListBuilder + result.Add(symBLOCKQUOTE) + for _, elem := range args { + if quote, isPair := sx.GetPair(ev.Eval(elem, env)); isPair { + result.Add(quote.Cons(sxhtml.SymListSplice)) + } + } + return result.List() + }) + + ev.bind(sz.SymTable, 1, func(args sx.Vector, env *Environment) sx.Object { + thead := sx.Nil() + if header := getList(args[0], env); !sx.IsNil(header) { + thead = sx.Nil().Cons(ev.evalTableRow(header, env)).Cons(symTHEAD) + } + + var tbody sx.ListBuilder + if len(args) > 1 { + tbody.Add(symTBODY) + for _, row := range args[1:] { + tbody.Add(ev.evalTableRow(getList(row, env), env)) + } + } + + table := sx.Nil() + if !tbody.IsEmpty() { + table = table.Cons(tbody.List()) } if thead != nil { table = table.Cons(thead) } if table == nil { - return sxpf.Nil() - } - return table.Cons(te.Make("table")) - }) - te.bind(sexpr.NameSymCell, 0, te.makeCellFn("")) - te.bind(sexpr.NameSymCellCenter, 0, te.makeCellFn("center")) - te.bind(sexpr.NameSymCellLeft, 0, te.makeCellFn("left")) - te.bind(sexpr.NameSymCellRight, 0, te.makeCellFn("right")) - - te.bind(sexpr.NameSymRegionBlock, 2, te.makeRegionFn(te.Make("div"), true)) - te.bind(sexpr.NameSymRegionQuote, 2, te.makeRegionFn(te.Make("blockquote"), false)) - te.bind(sexpr.NameSymRegionVerse, 2, te.makeRegionFn(te.Make("div"), false)) - - te.bind(sexpr.NameSymVerbatimComment, 1, func(args *sxpf.List) sxpf.Object { - if te.getAttributes(args).HasDefault() { - if s := te.getString(args.Tail()); s != "" { - t := sxpf.MakeString(s.String()) - return sxpf.Nil().Cons(t).Cons(te.Make(sxhtml.NameSymBlockComment)) + return sx.Nil() + } + return table.Cons(symTABLE) + }) + ev.bind(sz.SymCell, 0, ev.makeCellFn("")) + ev.bind(sz.SymCellCenter, 0, ev.makeCellFn("center")) + ev.bind(sz.SymCellLeft, 0, ev.makeCellFn("left")) + ev.bind(sz.SymCellRight, 0, ev.makeCellFn("right")) + + ev.bind(sz.SymRegionBlock, 2, ev.makeRegionFn(SymDIV, true)) + ev.bind(sz.SymRegionQuote, 2, ev.makeRegionFn(symBLOCKQUOTE, false)) + ev.bind(sz.SymRegionVerse, 2, ev.makeRegionFn(SymDIV, false)) + + ev.bind(sz.SymVerbatimComment, 1, func(args sx.Vector, env *Environment) sx.Object { + if ev.GetAttributes(args[0], env).HasDefault() { + if len(args) > 1 { + if s := getString(args[1], env); s.GetValue() != "" { + return sx.Nil().Cons(s).Cons(sxhtml.SymBlockComment) + } } } return nil }) - - te.bind(sexpr.NameSymVerbatimEval, 2, func(args *sxpf.List) sxpf.Object { - return te.transformVerbatim(te.getAttributes(args).AddClass("zs-eval"), te.getString(args.Tail())) - }) - te.bind(sexpr.NameSymVerbatimHTML, 2, te.transformHTML) - te.bind(sexpr.NameSymVerbatimMath, 2, func(args *sxpf.List) sxpf.Object { - return te.transformVerbatim(te.getAttributes(args).AddClass("zs-math"), te.getString(args.Tail())) - }) - te.bind(sexpr.NameSymVerbatimProg, 2, func(args *sxpf.List) sxpf.Object { - a := te.getAttributes(args) - content := te.getString(args.Tail()) + 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 = sxpf.MakeString(visibleReplacer.Replace(content.String())) - } - return te.transformVerbatim(a, content) - }) - te.bind(sexpr.NameSymVerbatimZettel, 0, func(*sxpf.List) sxpf.Object { return sxpf.Nil() }) - - te.bind(sexpr.NameSymBLOB, 3, func(args *sxpf.List) sxpf.Object { - argSyntax := args.Tail() - return te.transformBLOB(te.getList(args), te.getString(argSyntax), te.getString(argSyntax.Tail())) - }) - - te.bind(sexpr.NameSymTransclude, 2, func(args *sxpf.List) sxpf.Object { - ref, ok := args.Tail().Car().(*sxpf.List) - if !ok { - return sxpf.Nil() + content = sx.MakeString(visibleReplacer.Replace(content.GetValue())) + } + return ev.evalVerbatim(a, content) + }) + 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.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 sxpf.IsNil(refKind) { - return sxpf.Nil() - } - if refValue := te.getString(ref.Tail()); refValue != "" { - if te.astSF.MustMake(sexpr.NameSymRefStateExternal).IsEqual(refKind) { - a := te.getAttributes(args).Set("src", refValue.String()).AddClass("external") - return sxpf.Nil().Cons(sxpf.Nil().Cons(te.transformAttribute(a)).Cons(te.Make("img"))).Cons(te.symP) - } - return sxpf.MakeList( - te.Make(sxhtml.NameSymInlineComment), - sxpf.MakeString("transclude"), + if sx.IsNil(refKind) { + return sx.Nil() + } + if refValue := getString(ref.Tail().Car(), env); refValue.GetValue() != "" { + if refSym, isRefSym := sx.GetSymbol(refKind); isRefSym && refSym.IsEqual(sz.SymRefStateExternal) { + a := ev.GetAttributes(args[0], env).Set("src", refValue.GetValue()).AddClass("external") + return sx.Nil().Cons(sx.Nil().Cons(ev.EvaluateAttrbute(a)).Cons(SymIMG)).Cons(SymP) + } + return sx.MakeList( + sxhtml.SymInlineComment, + sx.MakeString("transclude"), refKind, - sxpf.MakeString("->"), + sx.MakeString("->"), refValue, ) } - return args + return ev.evalSlice(args, env) }) } -func (te *TransformEnv) makeListFn(tag string) transformFn { - sym := te.Make(tag) - return func(args *sxpf.List) sxpf.Object { - result := sxpf.Nil().Cons(sym) - last := result - for elem := args; elem != nil; elem = elem.Tail() { - item := sxpf.Nil().Cons(te.Make("li")) - if res, ok := elem.Car().(*sxpf.List); ok { +func (ev *Evaluator) makeListFn(sym *sx.Symbol) EvalFn { + return func(args sx.Vector, env *Environment) sx.Object { + var result sx.ListBuilder + result.Add(sym) + for _, elem := range args { + item := sx.Nil().Cons(SymLI) + if res, isPair := sx.GetPair(ev.Eval(elem, env)); isPair { item.ExtendBang(res) } - last = last.AppendBang(item) - } - return result - } -} -func (te *TransformEnv) transformTableRow(cells *sxpf.List) *sxpf.List { - row := sxpf.Nil().Cons(te.Make("tr")) - if cells == nil { - return sxpf.Nil() - } - curRow := row - for cell := cells; cell != nil; cell = cell.Tail() { - curRow = curRow.AppendBang(cell.Car()) - } - return row -} - -func (te *TransformEnv) makeCellFn(align string) transformFn { - return func(args *sxpf.List) sxpf.Object { - tdata := args + result.Add(item) + } + 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, env *Environment) *sx.Pair { + if pairs == nil { + return nil + } + var row sx.ListBuilder + row.Add(symTR) + for pair := pairs; pair != nil; pair = pair.Tail() { + row.Add(ev.Eval(pair.Car(), env)) + } + return row.List() +} +func (ev *Evaluator) makeCellFn(align string) EvalFn { + return func(args sx.Vector, env *Environment) sx.Object { + tdata := ev.evalSlice(args, env) if align != "" { - tdata = tdata.Cons(te.transformAttribute(attrs.Attributes{"class": align})) + tdata = tdata.Cons(ev.EvaluateAttrbute(attrs.Attributes{"class": align})) } - return tdata.Cons(te.Make("td")) + return tdata.Cons(symTD) } } -func (te *TransformEnv) makeRegionFn(sym *sxpf.Symbol, genericToClass bool) transformFn { - return func(args *sxpf.List) sxpf.Object { - a := te.getAttributes(args) +func (ev *Evaluator) makeRegionFn(sym *sx.Symbol, genericToClass bool) EvalFn { + 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 := sxpf.Nil() + var result sx.ListBuilder + result.Add(sym) if len(a) > 0 { - result = result.Cons(te.transformAttribute(a)) - } - result = result.Cons(sym) - currResult := result.Last() - blockArg := args.Tail() - if region, ok := blockArg.Car().(*sxpf.List); ok { - currResult = currResult.ExtendBang(region) - } - if citeArg := blockArg.Tail(); citeArg != nil { - if cite, ok := citeArg.Car().(*sxpf.List); ok && cite != nil { - currResult.AppendBang(cite.Cons(te.Make("cite"))) - } - } - return result - } -} - -func (te *TransformEnv) transformVerbatim(a attrs.Attributes, s sxpf.String) sxpf.Object { + result.Add(ev.EvaluateAttrbute(a)) + } + if region, isPair := sx.GetPair(args[1]); isPair { + if evalRegion := ev.EvalPairList(region, env); evalRegion != nil { + result.ExtendBang(evalRegion) + } + } + if len(args) > 2 { + if cite, _ := ev.EvaluateList(args[2:], env); cite != nil { + result.Add(cite.Cons(symCITE)) + } + } + return result.List() + } +} + +func (ev *Evaluator) evalVerbatim(a attrs.Attributes, s sx.String) sx.Object { a = setProgLang(a) - code := sxpf.Nil().Cons(s) - if al := te.transformAttribute(a); al != nil { + code := sx.Nil().Cons(s) + if al := ev.EvaluateAttrbute(a); al != nil { code = code.Cons(al) } - code = code.Cons(te.Make("code")) - return sxpf.Nil().Cons(code).Cons(te.Make("pre")) + code = code.Cons(symCODE) + return sx.Nil().Cons(code).Cons(symPRE) } -func (te *TransformEnv) bindInlines() { - te.bind(sexpr.NameSymInline, 0, listArgs) - te.bind(sexpr.NameSymText, 1, func(args *sxpf.List) sxpf.Object { return te.getString(args) }) - te.bind(sexpr.NameSymSpace, 0, func(args *sxpf.List) sxpf.Object { - if args.IsNil() { - return sxpf.MakeString(" ") - } - return te.getString(args) +func (ev *Evaluator) bindInlines() { + ev.bind(sz.SymInline, 0, ev.evalList) + ev.bind(sz.SymText, 1, func(args sx.Vector, env *Environment) sx.Object { return getString(args[0], env) }) + ev.bind(sz.SymSpace, 0, func(args sx.Vector, env *Environment) sx.Object { + if len(args) == 0 { + return sx.MakeString(" ") + } + return getString(args[0], env) }) - te.bind(sexpr.NameSymSoft, 0, func(*sxpf.List) sxpf.Object { return sxpf.MakeString(" ") }) - brSym := te.Make("br") - te.bind(sexpr.NameSymHard, 0, func(*sxpf.List) sxpf.Object { return sxpf.Nil().Cons(brSym) }) - - te.bind(sexpr.NameSymLinkInvalid, 2, func(args *sxpf.List) sxpf.Object { - // a := te.getAttributes(args) - refArg := args.Tail() - inline := refArg.Tail() + ev.bind(sz.SymSoft, 0, func(sx.Vector, *Environment) sx.Object { return sx.MakeString(" ") }) + ev.bind(sz.SymHard, 0, func(sx.Vector, *Environment) sx.Object { return sx.Nil().Cons(symBR) }) + + ev.bind(sz.SymLinkInvalid, 2, func(args sx.Vector, env *Environment) sx.Object { + a := 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 = sxpf.Nil().Cons(refArg.Car()) - } - return inline.Cons(te.symSpan) - }) - transformHREF := func(args *sxpf.List) sxpf.Object { - a := te.getAttributes(args) - refValue := te.getString(args.Tail()) - return te.transformLink(a.Set("href", refValue.String()), refValue, args.Tail().Tail()) - } - te.bind(sexpr.NameSymLinkZettel, 2, transformHREF) - te.bind(sexpr.NameSymLinkSelf, 2, transformHREF) - te.bind(sexpr.NameSymLinkFound, 2, transformHREF) - te.bind(sexpr.NameSymLinkBroken, 2, func(args *sxpf.List) sxpf.Object { - a := te.getAttributes(args) - refValue := te.getString(args.Tail()) - return te.transformLink(a.AddClass("broken"), refValue, args.Tail().Tail()) - }) - te.bind(sexpr.NameSymLinkHosted, 2, transformHREF) - te.bind(sexpr.NameSymLinkBased, 2, transformHREF) - te.bind(sexpr.NameSymLinkQuery, 2, func(args *sxpf.List) sxpf.Object { - a := te.getAttributes(args) - refValue := te.getString(args.Tail()) - query := "?" + api.QueryKeyQuery + "=" + url.QueryEscape(refValue.String()) - return te.transformLink(a.Set("href", query), refValue, args.Tail().Tail()) - }) - te.bind(sexpr.NameSymLinkExternal, 2, func(args *sxpf.List) sxpf.Object { - a := te.getAttributes(args) - refValue := te.getString(args.Tail()) - return te.transformLink(a.Set("href", refValue.String()).AddClass("external"), refValue, args.Tail().Tail()) - }) - - te.bind(sexpr.NameSymEmbed, 3, func(args *sxpf.List) sxpf.Object { - argRef := args.Tail() - ref := te.getList(argRef) - syntax := te.getString(argRef.Tail()) + inline = sx.Nil().Cons(ev.Eval(args[1], env)) + } + return inline.Cons(SymSPAN) + }) + 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.GetValue()), refValue, args[2:], env) + } + ev.bind(sz.SymLinkZettel, 2, evalHREF) + ev.bind(sz.SymLinkSelf, 2, evalHREF) + ev.bind(sz.SymLinkFound, 2, evalHREF) + ev.bind(sz.SymLinkBroken, 2, func(args sx.Vector, env *Environment) sx.Object { + a := 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.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(refValue.GetValue()) + return ev.evalLink(a.Set("href", query), refValue, args[2:], env) + }) + ev.bind(sz.SymLinkExternal, 2, func(args sx.Vector, env *Environment) sx.Object { + a := ev.GetAttributes(args[0], env) + env.pushAttributes(a) + defer env.popAttributes() + refValue := getString(args[1], env) + return ev.evalLink(a.Set("href", refValue.GetValue()).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).GetValue() if syntax == api.ValueSyntaxSVG { - embedAttr := sxpf.MakeList( - te.symAttr, - sxpf.Cons(te.Make("type"), sxpf.MakeString("image/svg+xml")), - sxpf.Cons(te.Make("src"), sxpf.MakeString("/"+te.getString(ref.Tail()).String()+".svg")), - ) - return sxpf.MakeList( - te.Make("figure"), - sxpf.MakeList( - te.Make("embed"), + embedAttr := sx.MakeList( + sxhtml.SymAttr, + sx.Cons(SymAttrType, sx.MakeString("image/svg+xml")), + sx.Cons(SymAttrSrc, sx.MakeString("/"+getString(ref.Tail(), env).GetValue()+".svg")), + ) + return sx.MakeList( + SymFIGURE, + sx.MakeList( + SymEMBED, embedAttr, ), ) } - a := te.getAttributes(args) - a = a.Set("src", string(te.getString(ref.Tail()))) - var sb strings.Builder - te.flattenText(&sb, ref.Tail().Tail().Tail()) - if d := sb.String(); d != "" { - a = a.Set("alt", d) - } - return sxpf.MakeList(te.Make("img"), te.transformAttribute(a)) + a := ev.GetAttributes(args[0], env) + a = a.Set("src", getString(ref.Tail().Car(), env).GetValue()) + if len(args) > 3 { + var sb strings.Builder + flattenText(&sb, sx.MakeList(args[3:]...)) + if d := sb.String(); d != "" { + a = a.Set("alt", d) + } + } + return sx.MakeList(SymIMG, ev.EvaluateAttrbute(a)) }) - te.bind(sexpr.NameSymEmbedBLOB, 3, func(args *sxpf.List) sxpf.Object { - argSyntax := args.Tail() - a, syntax, data := te.getAttributes(args), te.getString(argSyntax), te.getString(argSyntax.Tail()) - summary, _ := a.Get(api.KeySummary) - return te.transformBLOB( - sxpf.MakeList(te.astSF.MustMake(sexpr.NameSymInline), sxpf.MakeString(summary)), + 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(sxhtml.SymListSplice, sx.MakeString(summary)), syntax, data, ) }) - te.bind(sexpr.NameSymCite, 2, func(args *sxpf.List) sxpf.Object { - result := sxpf.Nil() - argKey := args.Tail() - if key := te.getString(argKey); key != "" { - if text := argKey.Tail(); text != nil { - result = text.Cons(sxpf.MakeString(", ")) - } - result = result.Cons(key) - } - if a := te.getAttributes(args); len(a) > 0 { - result = result.Cons(te.transformAttribute(a)) - } - if result == nil { - return nil - } - return result.Cons(te.symSpan) - }) - - te.bind(sexpr.NameSymMark, 3, func(args *sxpf.List) sxpf.Object { - argFragment := args.Tail().Tail() - result := argFragment.Tail() - if !te.tr.noLinks { - if fragment := te.getString(argFragment); fragment != "" { - a := attrs.Attributes{"id": fragment.String() + te.tr.unique} - return result.Cons(te.transformAttribute(a)).Cons(te.symA) - } - } - return result.Cons(te.symSpan) - }) - - te.bind(sexpr.NameSymEndnote, 1, func(args *sxpf.List) sxpf.Object { - attrPlist := sxpf.Nil() - if a := te.getAttributes(args); len(a) > 0 { - if attrs := te.transformAttribute(a); attrs != nil { - attrPlist = attrs.Tail() - } - } - - text, ok := args.Tail().Car().(*sxpf.List) - if !ok { - return sxpf.Nil() - } - te.tr.endnotes = append(te.tr.endnotes, endnoteInfo{noteAST: text, noteHx: nil, attrs: attrPlist}) - noteNum := strconv.Itoa(len(te.tr.endnotes)) - noteID := te.tr.unique + noteNum - hrefAttr := sxpf.Nil().Cons(sxpf.Cons(te.Make("role"), sxpf.MakeString("doc-noteref"))). - Cons(sxpf.Cons(te.Make("href"), sxpf.MakeString("#fn:"+noteID))). - Cons(sxpf.Cons(te.tr.symClass, sxpf.MakeString("zs-noteref"))). - Cons(te.symAttr) - href := sxpf.Nil().Cons(sxpf.MakeString(noteNum)).Cons(hrefAttr).Cons(te.symA) - supAttr := sxpf.Nil().Cons(sxpf.Cons(te.Make("id"), sxpf.MakeString("fnref:"+noteID))).Cons(te.symAttr) - return sxpf.Nil().Cons(href).Cons(supAttr).Cons(te.Make("sup")) - }) - - te.bind(sexpr.NameSymFormatDelete, 1, te.makeFormatFn("del")) - te.bind(sexpr.NameSymFormatEmph, 1, te.makeFormatFn("em")) - te.bind(sexpr.NameSymFormatInsert, 1, te.makeFormatFn("ins")) - te.bind(sexpr.NameSymFormatQuote, 1, te.transformQuote) - te.bind(sexpr.NameSymFormatSpan, 1, te.makeFormatFn("span")) - te.bind(sexpr.NameSymFormatStrong, 1, te.makeFormatFn("strong")) - te.bind(sexpr.NameSymFormatSub, 1, te.makeFormatFn("sub")) - te.bind(sexpr.NameSymFormatSuper, 1, te.makeFormatFn("sup")) - - te.bind(sexpr.NameSymLiteralComment, 1, func(args *sxpf.List) sxpf.Object { - if te.getAttributes(args).HasDefault() { - if s := te.getString(args.Tail()); s != "" { - return sxpf.Nil().Cons(s).Cons(te.Make(sxhtml.NameSymInlineComment)) - } - } - return sxpf.Nil() - }) - te.bind(sexpr.NameSymLiteralHTML, 2, te.transformHTML) - kbdSym := te.Make("kbd") - te.bind(sexpr.NameSymLiteralInput, 2, func(args *sxpf.List) sxpf.Object { - return te.transformLiteral(args, nil, kbdSym) - }) - codeSym := te.Make("code") - te.bind(sexpr.NameSymLiteralMath, 2, func(args *sxpf.List) sxpf.Object { - a := te.getAttributes(args).AddClass("zs-math") - return te.transformLiteral(args, a, codeSym) - }) - sampSym := te.Make("samp") - te.bind(sexpr.NameSymLiteralOutput, 2, func(args *sxpf.List) sxpf.Object { - return te.transformLiteral(args, nil, sampSym) - }) - te.bind(sexpr.NameSymLiteralProg, 2, func(args *sxpf.List) sxpf.Object { - return te.transformLiteral(args, nil, codeSym) - }) - - te.bind(sexpr.NameSymLiteralZettel, 0, func(*sxpf.List) sxpf.Object { return sxpf.Nil() }) -} - -func (te *TransformEnv) makeFormatFn(tag string) transformFn { - sym := te.Make(tag) - return func(args *sxpf.List) sxpf.Object { - a := te.getAttributes(args) - if val, found := a.Get(""); found { - a = a.Remove("").AddClass(val) - } - res := args.Tail() - if len(a) > 0 { - res = res.Cons(te.transformAttribute(a)) - } - return res.Cons(sym) - } -} -func (te *TransformEnv) transformQuote(args *sxpf.List) sxpf.Object { - const langAttr = "lang" - a := te.getAttributes(args) - langVal, found := a.Get(langAttr) - if found { - a = a.Remove(langAttr) - } - if val, found2 := a.Get(""); found2 { - a = a.Remove("").AddClass(val) - } - res := args.Tail() - if len(a) > 0 { - res = res.Cons(te.transformAttribute(a)) - } - res = res.Cons(te.Make("q")) - if found { - res = sxpf.Nil().Cons(res).Cons(te.transformAttribute(attrs.Attributes{}.Set(langAttr, langVal))).Cons(te.symSpan) - } - return res -} - -var visibleReplacer = strings.NewReplacer(" ", "\u2423") - -func (te *TransformEnv) transformLiteral(args *sxpf.List, a attrs.Attributes, sym *sxpf.Symbol) sxpf.Object { - if a == nil { - a = te.getAttributes(args) - } - a = setProgLang(a) - literal := te.getString(args.Tail()).String() - if a.HasDefault() { - a = a.RemoveDefault() - literal = visibleReplacer.Replace(literal) - } - res := sxpf.Nil().Cons(sxpf.MakeString(literal)) - if len(a) > 0 { - res = res.Cons(te.transformAttribute(a)) - } - return res.Cons(sym) -} - + 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.GetValue() != "" { + if len(args) > 2 { + result = ev.evalSlice(args[2:], env).Cons(sx.MakeString(", ")) + } + result = result.Cons(key) + } + if len(a) > 0 { + result = result.Cons(ev.EvaluateAttrbute(a)) + } + if result == nil { + return nil + } + return result.Cons(SymSPAN) + }) + ev.bind(sz.SymMark, 3, func(args sx.Vector, env *Environment) sx.Object { + result := ev.evalSlice(args[3:], env) + if !ev.noLinks { + if fragment := getString(args[2], env).GetValue(); fragment != "" { + a := attrs.Attributes{"id": fragment + ev.unique} + return result.Cons(ev.EvaluateAttrbute(a)).Cons(SymA) + } + } + return result.Cons(SymSPAN) + }) + 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() + } + } + + noteNum := strconv.Itoa(len(env.endnotes) + 1) + noteID := ev.unique + noteNum + env.endnotes = append(env.endnotes, endnoteInfo{ + noteID: noteID, noteAST: args[1:], noteHx: nil, attrs: attrPlist}) + hrefAttr := sx.Nil().Cons(sx.Cons(SymAttrRole, sx.MakeString("doc-noteref"))). + Cons(sx.Cons(SymAttrHref, sx.MakeString("#fn:"+noteID))). + Cons(sx.Cons(SymAttrClass, sx.MakeString("zs-noteref"))). + Cons(sxhtml.SymAttr) + href := sx.Nil().Cons(sx.MakeString(noteNum)).Cons(hrefAttr).Cons(SymA) + supAttr := sx.Nil().Cons(sx.Cons(SymAttrId, sx.MakeString("fnref:"+noteID))).Cons(sxhtml.SymAttr) + return sx.Nil().Cons(href).Cons(supAttr).Cons(symSUP) + }) + + ev.bind(sz.SymFormatDelete, 1, ev.makeFormatFn(symDEL)) + ev.bind(sz.SymFormatEmph, 1, ev.makeFormatFn(symEM)) + ev.bind(sz.SymFormatInsert, 1, ev.makeFormatFn(symINS)) + ev.bind(sz.SymFormatMark, 1, ev.makeFormatFn(symMARK)) + ev.bind(sz.SymFormatQuote, 1, ev.evalQuote) + ev.bind(sz.SymFormatSpan, 1, ev.makeFormatFn(SymSPAN)) + ev.bind(sz.SymFormatStrong, 1, ev.makeFormatFn(SymSTRONG)) + ev.bind(sz.SymFormatSub, 1, ev.makeFormatFn(symSUB)) + ev.bind(sz.SymFormatSuper, 1, ev.makeFormatFn(symSUP)) + + ev.bind(sz.SymLiteralComment, 1, func(args sx.Vector, env *Environment) sx.Object { + if ev.GetAttributes(args[0], env).HasDefault() { + if len(args) > 1 { + if s := getString(ev.Eval(args[1], env), env); s.GetValue() != "" { + return sx.Nil().Cons(s).Cons(sxhtml.SymInlineComment) + } + } + } + return sx.Nil() + }) + ev.bind(sz.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) + } + res := ev.evalSlice(args[1:], env) + if len(a) > 0 { + res = res.Cons(ev.EvaluateAttrbute(a)) + } + return res.Cons(sym) + } +} + +type quoteData struct { + primLeft, primRight string + secLeft, secRight string + nbsp bool +} + +var langQuotes = map[string]quoteData{ + "": {""", """, """, """, false}, + api.ValueLangEN: {"“", "”", "‘", "’", false}, + "de": {"„", "“", "‚", "‘", false}, + "fr": {"«", "»", "‹", "›", true}, +} + +func getQuoteData(lang string) quoteData { + langFields := strings.FieldsFunc(lang, func(r rune) bool { return r == '-' || r == '_' }) + for len(langFields) > 0 { + langSup := strings.Join(langFields, "-") + quotes, ok := langQuotes[langSup] + if ok { + return quotes + } + langFields = langFields[0 : len(langFields)-1] + } + return langQuotes[""] +} + +func getQuotes(data *quoteData, env *Environment) (string, string) { + if env.quoteNesting%2 == 0 { + return data.primLeft, data.primRight + } + return data.secLeft, data.secRight +} + +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) + } + quotes := getQuoteData(env.getLanguage()) + leftQ, rightQ := getQuotes("es, env) + + env.quoteNesting++ + res := ev.evalSlice(args[1:], env) + env.quoteNesting-- + + lastPair := res.LastPair() + if lastPair.IsNil() { + res = sx.Cons(sx.MakeList(sxhtml.SymNoEscape, sx.MakeString(leftQ), sx.MakeString(rightQ)), sx.Nil()) + } else { + if quotes.nbsp { + lastPair.AppendBang(sx.MakeList(sxhtml.SymNoEscape, sx.MakeString(" "), sx.MakeString(rightQ))) + res = res.Cons(sx.MakeList(sxhtml.SymNoEscape, sx.MakeString(leftQ), sx.MakeString(" "))) + } else { + lastPair.AppendBang(sx.MakeList(sxhtml.SymNoEscape, sx.MakeString(rightQ))) + res = res.Cons(sx.MakeList(sxhtml.SymNoEscape, sx.MakeString(leftQ))) + } + } + if len(a) > 0 { + res = res.Cons(ev.EvaluateAttrbute(a)) + return res.Cons(SymSPAN) + } + return res.Cons(sxhtml.SymListSplice) +} + +var visibleReplacer = strings.NewReplacer(" ", "\u2423") + +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 = setProgLang(a) + literal := getString(args[1], env).GetValue() + if a.HasDefault() { + a = a.RemoveDefault() + literal = visibleReplacer.Replace(literal) + } + res := sx.Nil().Cons(sx.MakeString(literal)) + if len(a) > 0 { + res = res.Cons(ev.EvaluateAttrbute(a)) + } + return res.Cons(sym) +} func setProgLang(a attrs.Attributes) attrs.Attributes { if val, found := a.Get(""); found { a = a.AddClass("language-" + val).Remove("") } return a } -func (te *TransformEnv) transformHTML(args *sxpf.List) sxpf.Object { - if s := te.getString(args.Tail()); s != "" && IsSafe(s.String()) { - return sxpf.Nil().Cons(s).Cons(te.symNoEscape) +func (ev *Evaluator) evalHTML(args sx.Vector, env *Environment) sx.Object { + if s := getString(ev.Eval(args[1], env), env); s.GetValue() != "" && IsSafe(s.GetValue()) { + return sx.Nil().Cons(s).Cons(sxhtml.SymNoEscape) + } + return nil +} + +func (ev *Evaluator) evalBLOB(description *sx.Pair, syntax, data sx.String) sx.Object { + if data.GetValue() == "" { + return sx.Nil() + } + switch syntax.GetValue() { + case "": + return sx.Nil() + case api.ValueSyntaxSVG: + return sx.Nil().Cons(sx.Nil().Cons(data).Cons(sxhtml.SymNoEscape)).Cons(SymP) + default: + imgAttr := sx.Nil().Cons(sx.Cons(SymAttrSrc, sx.MakeString("data:image/"+syntax.GetValue()+";base64,"+data.GetValue()))) + var sb strings.Builder + flattenText(&sb, description) + if d := sb.String(); d != "" { + imgAttr = imgAttr.Cons(sx.Cons(symAttrAlt, sx.MakeString(d))) + } + return sx.Nil().Cons(sx.Nil().Cons(imgAttr.Cons(sxhtml.SymAttr)).Cons(SymIMG)).Cons(SymP) + } +} + +func flattenText(sb *strings.Builder, lst *sx.Pair) { + for elem := lst; elem != nil; elem = elem.Tail() { + switch obj := elem.Car().(type) { + case sx.String: + sb.WriteString(obj.GetValue()) + case *sx.Symbol: + if obj.IsEqual(sz.SymSpace) { + sb.WriteByte(' ') + break + } + case *sx.Pair: + flattenText(sb, obj) + } + } +} + +func (ev *Evaluator) evalList(args sx.Vector, env *Environment) sx.Object { + return ev.evalSlice(args, env) +} +func nilFn(sx.Vector, *Environment) sx.Object { return sx.Nil() } + +// 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 + } + lst, isLst := sx.GetPair(obj) + if !isLst { + return obj + } + 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() + } + symVal := sym.GetValue() + fn, found := ev.fns[symVal] + if !found { + env.err = fmt.Errorf("symbol %q not bound", sym) + return sx.Nil() + } + 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[symVal]; hasMinArgs { + if 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 { + return sx.Nil() + } + return 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) + 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 nil +} + +func (ev *Evaluator) evalLink(a attrs.Attributes, refValue sx.String, inline sx.Vector, env *Environment) sx.Object { + result := ev.evalSlice(inline, env) + if len(inline) == 0 { + result = sx.Nil().Cons(refValue) + } + if ev.noLinks { + return result.Cons(SymSPAN) + } + return result.Cons(ev.EvaluateAttrbute(a)).Cons(SymA) +} + +func (ev *Evaluator) getSymbol(obj sx.Object, env *Environment) *sx.Symbol { + if env.err == nil { + if sym, ok := sx.GetSymbol(obj); ok { + return sym + } + env.err = fmt.Errorf("%v/%T is not a symbol", obj, obj) + } + return sx.MakeSymbol("???") +} +func getString(val sx.Object, env *Environment) sx.String { + if env.err != nil { + return sx.String{} + } + if s, ok := sx.GetString(val); ok { + return s + } + env.err = fmt.Errorf("%v/%T is not a string", val, val) + return sx.String{} +} +func getList(val sx.Object, env *Environment) *sx.Pair { + if env.err == nil { + if res, isPair := sx.GetPair(val); isPair { + return res + } + env.err = fmt.Errorf("%v/%T is not a list", val, val) } return nil } - -func (te *TransformEnv) transformBLOB(description *sxpf.List, syntax, data sxpf.String) sxpf.Object { - if data == "" { - return sxpf.Nil() - } - switch syntax { - case "": - return sxpf.Nil() - case api.ValueSyntaxSVG: - return sxpf.Nil().Cons(sxpf.Nil().Cons(data).Cons(te.symNoEscape)).Cons(te.symP) - default: - imgAttr := sxpf.Nil().Cons(sxpf.Cons(te.Make("src"), sxpf.MakeString("data:image/"+syntax.String()+";base64,"+data.String()))) - var sb strings.Builder - te.flattenText(&sb, description) - if d := sb.String(); d != "" { - imgAttr = imgAttr.Cons(sxpf.Cons(te.Make("alt"), sxpf.MakeString(d))) - } - return sxpf.Nil().Cons(sxpf.Nil().Cons(imgAttr.Cons(te.symAttr)).Cons(te.Make("img"))).Cons(te.symP) - } -} - -func (te *TransformEnv) flattenText(sb *strings.Builder, lst *sxpf.List) { - for elem := lst; elem != nil; elem = elem.Tail() { - switch obj := elem.Car().(type) { - case sxpf.String: - sb.WriteString(obj.String()) - case *sxpf.List: - te.flattenText(sb, obj) - } - } -} - -type transformFn func(*sxpf.List) sxpf.Object - -func (te *TransformEnv) bind(name string, minArity int, fn transformFn) { - te.astEnv.Bind(te.astSF.MustMake(name), eval.MakeBuiltin(name, func(_ sxpf.Environment, args *sxpf.List) (sxpf.Object, error) { - if nArgs := args.Length(); nArgs < minArity { - return sxpf.Nil(), fmt.Errorf("not enough arguments (%d) for form %v (%d)", nArgs, name, minArity) - } - res := fn(args) - return res, te.err - })) -} - -func (te *TransformEnv) Rebind(name string, fn func(sxpf.Environment, *sxpf.List, sxpf.Callable) sxpf.Object) { - sym := te.astSF.MustMake(name) - obj, found := te.astEnv.Lookup(sym) - if !found { - panic(sym.String()) - } - preFn, ok := obj.(sxpf.Callable) - if !ok { - panic(sym.String()) - } - te.astEnv.Bind(sym, eval.MakeBuiltin(name, func(env sxpf.Environment, args *sxpf.List) (sxpf.Object, error) { - res := fn(env, args, preFn) - return res, te.err - })) -} - -func (te *TransformEnv) Make(name string) *sxpf.Symbol { return te.tr.Make(name) } -func (te *TransformEnv) getString(lst *sxpf.List) sxpf.String { - if te.err != nil { - return "" - } - val := lst.Car() - if s, ok := val.(sxpf.String); ok { - return s - } - te.err = fmt.Errorf("%v/%T is not a string", val, val) - return "" -} -func (te *TransformEnv) getInt64(lst *sxpf.List) int64 { - if te.err != nil { - return -1017 - } - val := lst.Car() - if num, ok := val.(*sxpf.Number); ok { - return num.GetInt64() - } - te.err = fmt.Errorf("%v/%T is not a number", val, val) - return -1017 -} -func (te *TransformEnv) getList(lst *sxpf.List) *sxpf.List { - if te.err == nil { - val := lst.Car() - if res, ok := val.(*sxpf.List); ok { - return res - } - te.err = fmt.Errorf("%v/%T is not a list", val, val) - } - return sxpf.Nil() -} -func (te *TransformEnv) getAttributes(args *sxpf.List) attrs.Attributes { - return sexpr.GetAttributes(te.getList(args)) -} - -func (te *TransformEnv) transformLink(a attrs.Attributes, refValue sxpf.String, inline *sxpf.List) sxpf.Object { - result := inline - if inline.IsNil() { - result = sxpf.Nil().Cons(refValue) - } - if te.tr.noLinks { - return result.Cons(te.symSpan) - } - return result.Cons(te.transformAttribute(a)).Cons(te.symA) -} - -func (te *TransformEnv) transformAttribute(a attrs.Attributes) *sxpf.List { - return te.tr.TransformAttrbute(a) -} - -func (te *TransformEnv) transformMeta(a attrs.Attributes) *sxpf.List { - return te.tr.TransformMeta(a) +func getInt64(val sx.Object, env *Environment) int64 { + if env.err != nil { + return -1017 + } + if num, ok := sx.GetNumber(val); ok { + return int64(num.(sx.Int64)) + } + env.err = fmt.Errorf("%v/%T is not a number", val, val) + return -1017 +} + +// GetAttributes evaluates the given arg in the given environment and returns +// the contained attributes. +func (ev *Evaluator) GetAttributes(arg sx.Object, env *Environment) attrs.Attributes { + return sz.GetAttributes(getList(arg, env)) } var unsafeSnippets = []string{ " 0 { + return result + } + return nil +} +func doMakeMeta(obj sx.Object) Meta { + lst, isList := sx.GetPair(obj) + if !isList || !lst.Car().IsEqual(SymMeta) { + return nil + } + result := make(map[string]MetaValue) + for node := lst.Tail(); node != nil; node = node.Tail() { + if mv, found := makeMetaValue(node.Head()); found { + result[mv.Key] = mv + } + } + return result +} +func makeMetaValue(mnode *sx.Pair) (MetaValue, bool) { + var result MetaValue + typeSym, isSymbol := sx.GetSymbol(mnode.Car()) + if !isSymbol { + return result, false + } + next := mnode.Tail() + keySym, isSymbol := sx.GetSymbol(next.Car()) + if !isSymbol { + return result, false + } + next = next.Tail() + result.Type = typeSym.GetValue() + result.Key = keySym.GetValue() + result.Value = next.Car() + return result, true +} + +func (m Meta) GetString(key string) string { + if v, found := m[key]; found { + return GoValue(v.Value) + } + return "" +} + +func (m Meta) GetPair(key string) *sx.Pair { + if mv, found := m[key]; found { + if pair, isPair := sx.GetPair(mv.Value); isPair { + return pair + } + } + return nil +} Index: text/text.go ================================================================== --- text/text.go +++ text/text.go @@ -4,90 +4,74 @@ // 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 text provides types, constants and function to work with text output. package text import ( "strings" - "codeberg.org/t73fde/sxpf" - "zettelstore.de/c/sexpr" + "t73f.de/r/sx" + "t73f.de/r/zsc/sz" ) // Encoder is the structure to hold relevant data to execute the encoding. type Encoder struct { - sf sxpf.SymbolFactory - sb strings.Builder - - symText *sxpf.Symbol - symSpace *sxpf.Symbol - symSoft *sxpf.Symbol - symHard *sxpf.Symbol - symQuote *sxpf.Symbol -} - -func NewEncoder(sf sxpf.SymbolFactory) *Encoder { - if sf == nil { - return nil - } - enc := &Encoder{ - sf: sf, - sb: strings.Builder{}, - symText: sf.MustMake(sexpr.NameSymText), - symSpace: sf.MustMake(sexpr.NameSymSpace), - symSoft: sf.MustMake(sexpr.NameSymSoft), - symHard: sf.MustMake(sexpr.NameSymHard), - symQuote: sf.MustMake(sexpr.NameSymQuote), + sb strings.Builder +} + +func NewEncoder() *Encoder { + enc := &Encoder{ + sb: strings.Builder{}, } return enc } -func (enc *Encoder) Encode(lst *sxpf.List) string { +func (enc *Encoder) Encode(lst *sx.Pair) string { enc.executeList(lst) result := enc.sb.String() enc.sb.Reset() return result } // EvaluateInlineString returns the text content of the given inline list as a string. -func EvaluateInlineString(lst *sxpf.List) string { - if sf := sxpf.FindSymbolFactory(lst); sf != nil { - return NewEncoder(sf).Encode(lst) - } - return "" +func EvaluateInlineString(lst *sx.Pair) string { + return NewEncoder().Encode(lst) } -func (enc *Encoder) executeList(lst *sxpf.List) { +func (enc *Encoder) executeList(lst *sx.Pair) { for elem := lst; elem != nil; elem = elem.Tail() { enc.execute(elem.Car()) } } -func (enc *Encoder) execute(obj sxpf.Object) { - cmd, ok := obj.(*sxpf.List) - if !ok { +func (enc *Encoder) execute(obj sx.Object) { + cmd, isPair := sx.GetPair(obj) + if !isPair { return } sym := cmd.Car() - if sxpf.IsNil(sym) { + if sx.IsNil(sym) { return } - if sym.IsEqual(enc.symText) { + if sym.IsEqual(sz.SymText) { args := cmd.Tail() if args == nil { return } - if val, ok2 := args.Car().(sxpf.String); ok2 { - enc.sb.WriteString(val.String()) + if val, isString := sx.GetString(args.Car()); isString { + enc.sb.WriteString(val.GetValue()) } - } else if sym.IsEqual(enc.symSpace) || sym.IsEqual(enc.symSoft) { + } else if sym.IsEqual(sz.SymSpace) || sym.IsEqual(sz.SymSoft) { enc.sb.WriteByte(' ') - } else if sym.IsEqual(enc.symHard) { + } else if sym.IsEqual(sz.SymHard) { enc.sb.WriteByte('\n') - } else if !sym.IsEqual(enc.symQuote) { + } else if !sym.IsEqual(sx.SymbolQuote) { enc.executeList(cmd.Tail()) } } Index: text/text_test.go ================================================================== --- text/text_test.go +++ text/text_test.go @@ -4,43 +4,46 @@ // 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 text_test import ( "strings" "testing" - "codeberg.org/t73fde/sxpf" - "codeberg.org/t73fde/sxpf/reader" - "zettelstore.de/c/text" + "t73f.de/r/sx" + "t73f.de/r/sx/sxreader" + "t73f.de/r/zsc/text" ) -func TestSexprText(t *testing.T) { +func TestSzText(t *testing.T) { testcases := []struct { src string exp string }{ {"()", ""}, {`(INLINE (TEXT "a"))`, "a"}, {`(INLINE (SPACE "a"))`, " "}, } for i, tc := range testcases { - sval, err := reader.MakeReader(strings.NewReader(tc.src)).Read() + sval, err := sxreader.MakeReader(strings.NewReader(tc.src)).Read() if err != nil { t.Error(err) continue } - seq, ok := sval.(*sxpf.List) - if !ok { + seq, isPair := sx.GetPair(sval) + if !isPair { t.Errorf("%d: not a list: %v", i, sval) } got := text.EvaluateInlineString(seq) if got != tc.exp { t.Errorf("%d: EncodeBlock(%q) == %q, but got %q", i, tc.src, tc.exp, got) } } } Index: www/changes.wiki ================================================================== --- www/changes.wiki +++ www/changes.wiki @@ -1,9 +1,41 @@ Change Log + +

Changes for Version 0.18.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, + where previous transformation was bottom.up. + * Add API call to retrieve role zettel. + * Added constants for role zettel and to mark text within zettelmarkup. + + +

Changes for Version 0.15.0 (2023-10-26)

+ * Tag zettel: API, constant values + * Refactorings b/c Sx + + +

Changes for Version 0.14.0 (2023-09-23)

+ * Remove support for JSON encoding + + +

Changes for Version 0.13.0 (2023-08-07)

+ * API uses plain data or sx data, but no JSON encoded data. + * Dependency sx is now hosted on Fossil repository, same for this library. + -

Changes for Version 0.12.0 (pending)

+

Changes for Version 0.12.0 (2023-06-05)

+ * Rename "sexpr" to "sz".

Changes for Version 0.11.0 (2023-03-27)

* Remove all zjson related declarations. * Generate HTML via SxHTML, not manually and direct. Index: www/index.wiki ================================================================== --- www/index.wiki +++ www/index.wiki @@ -1,12 +1,35 @@ Home This repository contains Go client software to access [https://zettelstore.de|Zettelstore] via its API. -

Latest Release: 0.11.0 (2023-03-27)

- * [./changes.wiki#0_11|Change summary] - * [/timeline?p=v0.11.0&bt=v0.10.0&y=ci|Check-ins for version 0.11.0], - [/vdiff?to=v0.11.0&from=v0.10.0|content diff] - * [/timeline?df=v0.11.0&y=ci|Check-ins derived from the 0.11.0 release], - [/vdiff?from=v0.11.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, +you must execute a go get command. Since Go treats non-standard +software and non-standard platforms quite badly, you must use some non-standard +commands. + +First, you must install the version control system +[https://fossil-scm.org|Fossil], which is a superior solution compared to Git, +in too many use cases. It is just a single executable, nothing more. Make sure, +it is in your search path for commands. + +How you can execute the following Go command to retrieve a given version of +this library: + +GOVCS=zettelstore.de:fossil go get -x t73f.de/r/zsc@HASH + +where HASH is the hash value of the commit you want to use. + +Go currently seems not to support software versions when the software is +managed by Fossil. This explains the need for the hash value. However, this +methods works and you can use the client software to access a Zettelstore.