ADDED .github/dependabot.yml Index: .github/dependabot.yml ================================================================== --- .github/dependabot.yml +++ .github/dependabot.yml @@ -0,0 +1,12 @@ +# 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,13 +4,10 @@ // 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 @@ -23,11 +20,11 @@ // IsValid returns true, if the idenfifier contains 14 digits. func (zid ZettelID) IsValid() bool { if len(zid) != 14 { return false } - for i := range 14 { + for i := 0; i < 14; i++ { ch := zid[i] if ch < '0' || '9' < ch { return false } } @@ -46,31 +43,85 @@ 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 - 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 +) + +// 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"` +} Index: api/const.go ================================================================== --- api/const.go +++ api/const.go @@ -4,13 +4,10 @@ // 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" @@ -23,55 +20,41 @@ 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") - ZidErrorTemplate = ZettelID("00000000010700") - - // WebUI sxn code zettel are in the range 19000..19999 - ZidSxnStart = ZettelID("00000000019000") - ZidSxnBase = ZettelID("00000000019990") + 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") // CSS-related zettel are in the range 20000..29999 - ZidBaseCSS = ZettelID("00000000020001") - ZidUserCSS = ZettelID("00000000025001") + ZidBaseCSS = ZettelID("00000000020001") + ZidUserCSS = ZettelID("00000000025001") + ZidRoleCSSMap = ZettelID("00000000029000") // Maps roles to CSS zettel, which should be in the range 29001..29999. // 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") ) @@ -88,10 +71,11 @@ MetaString = "String" MetaTagSet = "TagSet" MetaTimestamp = "Timestamp" MetaURL = "URL" MetaWord = "Word" + MetaWordSet = "WordSet" MetaZettelmarkup = "Zettelmarkup" ) // Predefined general Metadata keys const ( @@ -106,25 +90,21 @@ 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" @@ -135,22 +115,20 @@ 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" @@ -173,43 +151,50 @@ ) // 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" - QueryKeyTag = "tag" +) + +// Supported dir values. +const ( + DirBackward = "backward" + DirForward = "forward" ) // Supported encoding values. const ( EncodingHTML = "html" EncodingMD = "md" + EncodingSexpr = "sexpr" EncodingSHTML = "shtml" - EncodingSz = "sz" EncodingText = "text" EncodingZMK = "zmk" EncodingPlain = "plain" - EncodingData = "data" + EncodingJson = "json" ) var mapEncodingEnum = map[string]EncodingEnum{ EncodingHTML: EncoderHTML, EncodingMD: EncoderMD, + EncodingSexpr: EncoderSexpr, EncodingSHTML: EncoderSHTML, - EncodingSz: EncoderSz, EncodingText: EncoderText, EncodingZMK: EncoderZmk, EncodingPlain: EncoderPlain, - EncodingData: EncoderData, + EncodingJson: EncoderJson, } var mapEnumEncoding = map[EncodingEnum]string{} func init() { for k, v := range mapEncodingEnum { @@ -231,17 +216,17 @@ // Values for EncoderEnum const ( EncoderUnknown EncodingEnum = iota EncoderHTML EncoderMD + EncoderSexpr EncoderSHTML - EncoderSz EncoderText EncoderZmk EncoderPlain - EncoderData + EncoderJson ) // String representation of an encoder key. func (e EncodingEnum) String() string { if f, ok := mapEnumEncoding[e]; ok { @@ -266,57 +251,18 @@ CommandRefresh = Command("refresh") ) // Supported search operator representations const ( - 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:" + ActionSeparator = "|" + ExistOperator = "?" + ExistNotOperator = "!?" + SearchOperatorNot = "!" + SearchOperatorHas = ":" + SearchOperatorHasNot = "!:" + SearchOperatorPrefix = ">" + SearchOperatorNoPrefix = "!>" + SearchOperatorSuffix = "<" + SearchOperatorNoSuffix = "!<" + SearchOperatorMatch = "~" + SearchOperatorNoMatch = "!~" +) Index: api/urlbuilder.go ================================================================== --- api/urlbuilder.go +++ api/urlbuilder.go @@ -4,82 +4,155 @@ // 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 "t73f.de/r/webs/urlbuilder" +import ( + "net/url" + "strings" +) + +type urlQuery struct{ key, val string } // URLBuilder should be used to create zettelstore URLs. type URLBuilder struct { - base urlbuilder.URLBuilder - prefix string + prefix string + key byte + rawLocal string + path []string + query []urlQuery + fragment string } // NewURLBuilder creates a new URL builder with the given prefix and key. func NewURLBuilder(prefix string, key byte) *URLBuilder { - 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 + return &URLBuilder{prefix: prefix, key: key} } // Clone an URLBuilder func (ub *URLBuilder) Clone() *URLBuilder { cpy := new(URLBuilder) - ub.base.Copy(&cpy.base) - cpy.prefix = ub.prefix + 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 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 { - ub.base.AddPath(string(zid)) + if len(ub.path) > 0 { + panic("Cannot add Zid") + } + ub.rawLocal = "" + ub.path = append(ub.path, string(zid)) return ub } // AppendPath adds a new path element func (ub *URLBuilder) AppendPath(p string) *URLBuilder { - ub.base.AddPath(p) + ub.rawLocal = "" + for len(p) > 0 && p[0] == '/' { + p = p[1:] + } + if p != "" { + ub.path = append(ub.path, p) + } return ub } // AppendKVQuery adds a new key/value query parameter func (ub *URLBuilder) AppendKVQuery(key, value string) *URLBuilder { - ub.base.AddQuery(key, value) + ub.rawLocal = "" + ub.query = append(ub.query, urlQuery{key, value}) return ub } // AppendQuery adds a new query func (ub *URLBuilder) AppendQuery(value string) *URLBuilder { - if value != "" { - ub.base.AddQuery(QueryKeyQuery, value) - } + ub.rawLocal = "" + ub.query = append(ub.query, urlQuery{QueryKeyQuery, value}) return ub } // ClearQuery removes all query parameters. func (ub *URLBuilder) ClearQuery() *URLBuilder { - ub.base.RemoveQueries() + ub.rawLocal = "" + ub.query = nil + ub.fragment = "" return ub } // SetFragment stores the fragment func (ub *URLBuilder) SetFragment(s string) *URLBuilder { - ub.base.SetFragment(s) + ub.rawLocal = "" + ub.fragment = s return ub } // String produces a string value. func (ub *URLBuilder) String() string { - return ub.prefix + ub.base.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() } Index: attrs/attrs.go ================================================================== --- attrs/attrs.go +++ attrs/attrs.go @@ -4,22 +4,19 @@ // 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" - "t73f.de/r/zsc/maps" + "zettelstore.de/c/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,21 +4,18 @@ // 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" - "t73f.de/r/zsc/attrs" + "zettelstore.de/c/attrs" ) func TestHasDefault(t *testing.T) { t.Parallel() attr := attrs.Attributes{} Index: client/client.go ================================================================== --- client/client.go +++ client/client.go @@ -4,36 +4,31 @@ // 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" - "fmt" + "encoding/json" "io" "net" "net/http" "net/url" "strconv" "strings" "time" - "t73f.de/r/sx" - "t73f.de/r/sx/sxreader" - "t73f.de/r/zsc/api" - "t73f.de/r/zsc/sexp" - "t73f.de/r/zsc/sz" + "codeberg.org/t73fde/sxpf" + "codeberg.org/t73fde/sxpf/reader" + "zettelstore.de/c/api" ) // Client contains all data to execute requests. type Client struct { base string @@ -55,10 +50,13 @@ 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{ @@ -70,23 +68,10 @@ }, } 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 Body []byte @@ -124,12 +109,11 @@ Message: resp.Status[4:], Body: body, } } -// NewURLBuilder creates a new URL builder for the client with the given key. -func (c *Client) NewURLBuilder(key byte) *api.URLBuilder { +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) } @@ -180,26 +164,19 @@ } defer resp.Body.Close() if resp.StatusCode != http.StatusOK { return statusToError(resp) } - rd := sxreader.MakeReader(resp.Body) - obj, err := rd.Read() - if err != nil { - return err - } - vals, err := sexp.ParseList(obj, "ssi") + dec := json.NewDecoder(resp.Body) + var tinfo api.AuthJSON + err = dec.Decode(&tinfo) if err != nil { return err } - 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) + c.token = tinfo.Token + c.tokenType = tinfo.Type + c.expires = time.Now().Add(time.Duration(tinfo.Expires*10/9) * time.Second) return nil } func (c *Client) updateToken(ctx context.Context) error { if c.username == "" { @@ -212,30 +189,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() @@ -250,51 +227,60 @@ return zid, nil } return api.InvalidZID, err } -// CreateZettelData creates a new zettel and returns its URL. -func (c *Client) CreateZettelData(ctx context.Context, data api.ZettelData) (api.ZettelID, error) { +// CreateZettelJSON creates a new zettel and returns its URL. +func (c *Client) CreateZettelJSON(ctx context.Context, data *api.ZettelDataJSON) (api.ZettelID, error) { var buf bytes.Buffer - if _, err := sx.Print(&buf, sexp.EncodeZettel(data)); err != nil { + if err := encodeZettelData(&buf, data); err != nil { return api.InvalidZID, err } - ub := c.NewURLBuilder('z').AppendKVQuery(api.QueryKeyEncoding, api.EncodingData) + ub := c.newURLBuilder('z').AppendKVQuery(api.QueryKeyEncoding, api.EncodingJson) 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 } - return makeZettelID(obj) + 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) } var bsLF = []byte{'\n'} -// QueryZettel returns a list of all Zettel. -func (c *Client) QueryZettel(ctx context.Context, query string) ([][]byte, error) { - ub := c.NewURLBuilder('z').AppendQuery(query) +// ListZettel returns a list of all Zettel. +func (c *Client) ListZettel(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 { @@ -301,223 +287,70 @@ lines = lines[:len(lines)-1] } return lines, nil } -// QueryZettelData returns a list of zettel metadata. -func (c *Client) QueryZettelData(ctx context.Context, query string) (string, string, []api.ZidMetaRights, error) { - ub := c.NewURLBuilder('z').AppendKVQuery(api.QueryKeyEncoding, api.EncodingData).AppendQuery(query) - 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 - } - hVals, err := sexp.ParseList(vals[2], "ys") - if err != nil { - return "", "", nil, err - } - 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) - } +// 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) + 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 zl api.ZettelListJSON + err = dec.Decode(&zl) + if err != nil { + return "", "", nil, err + } + return zl.Query, zl.Human, zl.List, nil } // 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 data, err + return io.ReadAll(resp.Body) } -// 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) +// 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) ub.AppendKVQuery(api.QueryKeyPart, api.PartZettel) resp, err := c.buildAndExecuteRequest(ctx, http.MethodGet, ub, nil, 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 + 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 } // 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) @@ -527,11 +360,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, "") } @@ -547,23 +380,23 @@ return nil, statusToError(resp) } return io.ReadAll(resp.Body) } -// GetParsedSz returns an parsed zettel as a Sexpr-decoded data structure. -func (c *Client) GetParsedSz(ctx context.Context, zid api.ZettelID, part string) (sx.Object, error) { - return c.getSz(ctx, zid, part, true) +// 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) } -// 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) +// 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) } -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) +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) if part != "" { ub.AppendKVQuery(api.QueryKeyPart, part) } if parseOnly { ub.AppendKVQuery(api.QueryKeyParseOnly, "") @@ -574,58 +407,127 @@ } defer resp.Body.Close() if resp.StatusCode != http.StatusOK { return nil, statusToError(resp) } - return sxreader.MakeReader(bufio.NewReaderSize(resp.Body, 8)).Read() + return reader.MakeReader(bufio.NewReaderSize(resp.Body, 8), reader.WithSymbolFactory(sf)).Read() } -// GetMetaData returns the metadata of a zettel. -func (c *Client) GetMetaData(ctx context.Context, zid api.ZettelID) (api.MetaRights, error) { - ub := c.NewURLBuilder('z').SetZid(zid) - ub.AppendKVQuery(api.QueryKeyEncoding, api.EncodingData) +// 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) ub.AppendKVQuery(api.QueryKeyPart, api.PartMeta) resp, err := c.buildAndExecuteRequest(ctx, http.MethodGet, ub, nil, nil) if err != nil { - return api.MetaRights{}, err + 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() - rdr := sxreader.MakeReader(resp.Body) - obj, err := rdr.Read() + 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 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 + 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 } // 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() @@ -633,17 +535,17 @@ return statusToError(resp) } return nil } -// UpdateZettelData updates an existing zettel. -func (c *Client) UpdateZettelData(ctx context.Context, zid api.ZettelID, data api.ZettelData) error { +// UpdateZettelJSON updates an existing zettel. +func (c *Client) UpdateZettelJSON(ctx context.Context, zid api.ZettelID, data *api.ZettelDataJSON) error { var buf bytes.Buffer - if _, err := sx.Print(&buf, sexp.EncodeZettel(data)); err != nil { + if err := encodeZettelData(&buf, data); err != nil { return err } - ub := c.NewURLBuilder('z').SetZid(zid).AppendKVQuery(api.QueryKeyEncoding, api.EncodingData) + ub := c.newURLBuilder('z').SetZid(zid).AppendKVQuery(api.QueryKeyEncoding, api.EncodingJson) resp, err := c.buildAndExecuteRequest(ctx, http.MethodPut, ub, &buf, nil) if err != nil { return err } defer resp.Body.Close() @@ -653,13 +555,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 } @@ -670,11 +572,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() @@ -684,11 +586,11 @@ return nil } // ExecuteCommand will execute a given command at the Zettelstore. func (c *Client) ExecuteCommand(ctx context.Context, command api.Command) error { - ub := c.NewURLBuilder('x').AppendKVQuery(api.QueryKeyCommand, string(command)) + ub := c.newURLBuilder('x').AppendKVQuery(api.QueryKeyCommand, string(command)) resp, err := c.buildAndExecuteRequest(ctx, http.MethodPost, ub, nil, nil) if err != nil { return err } defer resp.Body.Close() @@ -696,57 +598,64 @@ return statusToError(resp) } return nil } -// GetVersionInfo returns version information.. -func (c *Client) GetVersionInfo(ctx context.Context) (VersionInfo, error) { - resp, err := c.buildAndExecuteRequest(ctx, http.MethodGet, c.NewURLBuilder('x'), nil, nil) +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 VersionInfo{}, err + return nil, 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) + return nil, statusToError(resp) + } + dec := json.NewDecoder(resp.Body) + var mlj api.MapListJSON + err = dec.Decode(&mlj) 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 + 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 } Index: client/client_test.go ================================================================== --- client/client_test.go +++ client/client_test.go @@ -4,13 +4,10 @@ // 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 ( @@ -18,17 +15,19 @@ "flag" "net/http" "net/url" "testing" - "t73f.de/r/zsc/api" - "t73f.de/r/zsc/client" + "codeberg.org/t73fde/sxpf" + "zettelstore.de/c/api" + "zettelstore.de/c/client" + "zettelstore.de/c/sexpr" ) func TestZettelList(t *testing.T) { c := getClient() - _, err := c.QueryZettel(context.Background(), "") + _, err := c.ListZettel(context.Background(), "") if err != nil { t.Error(err) return } } @@ -44,13 +43,16 @@ } return } } -func TestGetSzZettel(t *testing.T) { +func TestGetSexprZettel(t *testing.T) { c := getClient() - value, err := c.GetEvaluatedSz(context.Background(), api.ZidDefaultHome, api.PartContent) + sf := sxpf.MakeMappedFactory() + var zetSyms sexpr.ZettelSymbols + zetSyms.InitializeZettelSymbols(sf) + value, err := c.GetEvaluatedSexpr(context.Background(), api.ZidDefaultHome, api.PartContent, sf) if err != nil { t.Error(err) return } if value.IsNil() { Index: go.mod ================================================================== --- go.mod +++ go.mod @@ -1,9 +1,8 @@ -module t73f.de/r/zsc +module zettelstore.de/c -go 1.22 +go 1.20 require ( - t73f.de/r/sx v0.0.0-20240418072254-b6eff7d787f9 - t73f.de/r/sxhtml v0.0.0-20240418073213-2d735b1e4353 - t73f.de/r/webs v0.0.0-20240418131849-b90a59f7f704 + codeberg.org/t73fde/sxhtml v0.0.0-20230317170051-24321195e197 + codeberg.org/t73fde/sxpf v0.0.0-20230319111333-7de220f3b475 ) Index: go.sum ================================================================== --- go.sum +++ go.sum @@ -1,6 +1,4 @@ -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/sxhtml v0.0.0-20240418073213-2d735b1e4353 h1:FYE8m1ewouQXG3uc4FoUcr+++qzX8OtIJ9ZhI8CSs1s= -t73f.de/r/sxhtml v0.0.0-20240418073213-2d735b1e4353/go.mod h1:AGX5DjZ1x6agvQA8VVK8/bwae9Hcr9qBkt/kXgGQDjE= -t73f.de/r/webs v0.0.0-20240418131849-b90a59f7f704 h1:UkPXoJC0DUczgvuQbptvjKJ6N9vnBTQ6gIflBp7tC/k= -t73f.de/r/webs v0.0.0-20240418131849-b90a59f7f704/go.mod h1:UGAAtul0TK5ACeZ6zTS3SX6GqwMFXxlUpHiV8oqNq5w= +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= DELETED input/entity.go Index: input/entity.go ================================================================== --- input/entity.go +++ input/entity.go @@ -1,162 +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. -// -// 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 -} DELETED input/entity_test.go Index: input/entity_test.go ================================================================== --- input/entity_test.go +++ input/entity_test.go @@ -1,64 +0,0 @@ -//----------------------------------------------------------------------------- -// 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 - } - } -} DELETED input/input.go Index: input/input.go ================================================================== --- input/input.go +++ input/input.go @@ -1,156 +0,0 @@ -//----------------------------------------------------------------------------- -// 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]...) - } -} DELETED input/input_test.go Index: input/input_test.go ================================================================== --- input/input_test.go +++ input/input_test.go @@ -1,68 +0,0 @@ -//----------------------------------------------------------------------------- -// 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) - } - } -} DELETED input/runes.go Index: input/runes.go ================================================================== --- input/runes.go +++ input/runes.go @@ -1,27 +0,0 @@ -//----------------------------------------------------------------------------- -// 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,13 +4,10 @@ // 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,21 +4,18 @@ // 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" - "t73f.de/r/zsc/maps" + "zettelstore.de/c/maps" ) func isSorted(seq []string) bool { for i := 1; i < len(seq); i++ { if seq[i] < seq[i-1] { DELETED sexp/sexp.go Index: sexp/sexp.go ================================================================== --- sexp/sexp.go +++ sexp/sexp.go @@ -1,204 +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. -// -// 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 -} DELETED sexp/sexp_test.go Index: sexp/sexp_test.go ================================================================== --- sexp/sexp_test.go +++ sexp/sexp_test.go @@ -1,57 +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. -// -// 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) - } - -} ADDED sexpr/const.go Index: sexpr/const.go ================================================================== --- sexpr/const.go +++ sexpr/const.go @@ -0,0 +1,297 @@ +//----------------------------------------------------------------------------- +// 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) +} ADDED sexpr/const_test.go Index: sexpr/const_test.go ================================================================== --- sexpr/const_test.go +++ sexpr/const_test.go @@ -0,0 +1,26 @@ +//----------------------------------------------------------------------------- +// 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) + } +} ADDED sexpr/sexpr.go Index: sexpr/sexpr.go ================================================================== --- sexpr/sexpr.go +++ sexpr/sexpr.go @@ -0,0 +1,126 @@ +//----------------------------------------------------------------------------- +// 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 +} DELETED shtml/const.go Index: shtml/const.go ================================================================== --- shtml/const.go +++ shtml/const.go @@ -1,83 +0,0 @@ -//----------------------------------------------------------------------------- -// 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,13 +4,10 @@ // 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 @@ -18,997 +15,870 @@ "fmt" "net/url" "strconv" "strings" - "t73f.de/r/sx" - "t73f.de/r/sxhtml" - "t73f.de/r/zsc/api" - "t73f.de/r/zsc/attrs" - "t73f.de/r/zsc/sz" - "t73f.de/r/zsc/text" + "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" ) -// Evaluator will transform a s-expression that encodes the zettel AST into an s-expression +// Transformer will transform a s-expression that encodes the zettel AST into an s-expression // that represents HTML. -type Evaluator struct { +type Transformer struct { + sf sxpf.SymbolFactory + rebinder RebindProc 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 +} - fns map[string]EvalFn - minArgs map[string]int +type endnoteInfo struct { + noteAST *sxpf.List // Endnote as AST + noteHx *sxpf.List // Endnote as SxHTML + attrs *sxpf.List // attrs a-list } -// NewEvaluator creates a new Evaluator object. -func NewEvaluator(headingOffset int) *Evaluator { - ev := &Evaluator{ +// 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, headingOffset: int64(headingOffset), - - fns: make(map[string]EvalFn, 128), - minArgs: make(map[string]int, 128), - } - ev.bindMetadata() - ev.bindBlocks() - ev.bindInlines() - return ev -} + 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 } // SetUnique sets a prefix to make several HTML ids unique. -func (tr *Evaluator) SetUnique(s string) { tr.unique = s } +func (tr *Transformer) SetUnique(s string) { tr.unique = s } // IsValidName returns true, if name is a valid symbol name. -func (tr *Evaluator) IsValidName(s string) bool { return s != "" } +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 } -// EvaluateAttrbute transforms the given attributes into a HTML s-expression. -func (tr *Evaluator) EvaluateAttrbute(a attrs.Attributes) *sx.Pair { +// TransformAttrbute transforms the given attributes into a HTML s-expression. +func (tr *Transformer) TransformAttrbute(a attrs.Attributes) *sxpf.List { if len(a) == 0 { - return nil + return sxpf.Nil() } - plist := sx.Nil() + plist := sxpf.Nil() keys := a.Keys() for i := len(keys) - 1; i >= 0; i-- { key := keys[i] if key != attrs.DefaultAttribute && tr.IsValidName(key) { - plist = plist.Cons(sx.Cons(sx.MakeSymbol(key), sx.MakeString(a[key]))) + plist = plist.Cons(sxpf.Cons(tr.Make(key), sxpf.MakeString(a[key]))) } } if plist == nil { - 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 + 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 + } // Endnotes returns a SHTML object with all collected endnotes. -func (ev *Evaluator) Endnotes(env *Environment) *sx.Pair { - if env.err != nil || len(env.endnotes) == 0 { - return nil - } - - 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) - 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()) +func (tr *Transformer) Endnotes() *sxpf.List { + if len(tr.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 { + 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()) } s := sb.String() if len(s) > 0 { s = s[1:] } a := make(attrs.Attributes, 2). - Set("name", ev.getSymbol(args[0], env).GetValue()). + Set("name", te.getString(args).String()). 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) + 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 { - 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) + 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, _ := ev.EvaluateList(args[4:], env); result != nil { + if result, ok := sxpf.GetList(argFragment.Tail().Car()); ok && 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) { + 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 } - 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)) + dditem := te.getList(ddlst) + curItem = curItem.AppendBang(dditem.Cons(te.Make("dd"))) } } - return items.List() + return items }) - ev.bind(sz.SymListQuote, 0, func(args sx.Vector, env *Environment) sx.Object { + + te.bind(sexpr.NameSymListQuote, 0, func(args *sxpf.List) sxpf.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()) + 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) } if thead != nil { table = table.Cons(thead) } if table == nil { - 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 - }) - ev.bind(sz.SymVerbatimEval, 2, func(args sx.Vector, env *Environment) sx.Object { - return ev.evalVerbatim(ev.GetAttributes(args[0], env).AddClass("zs-eval"), getString(args[1], env)) - }) - ev.bind(sz.SymVerbatimHTML, 2, ev.evalHTML) - ev.bind(sz.SymVerbatimMath, 2, func(args sx.Vector, env *Environment) sx.Object { - return ev.evalVerbatim(ev.GetAttributes(args[0], env).AddClass("zs-math"), getString(args[1], env)) - }) - ev.bind(sz.SymVerbatimProg, 2, func(args sx.Vector, env *Environment) sx.Object { - a := ev.GetAttributes(args[0], env) - content := getString(args[1], env) - if a.HasDefault() { - content = sx.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 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, - sx.MakeString("->"), - refValue, - ) - } - return ev.evalSlice(args, env) - }) -} - -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) - } - 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(ev.EvaluateAttrbute(attrs.Attributes{"class": align})) - } - return tdata.Cons(symTD) - } -} - -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() + 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 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()) + 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() + } + 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"), + refKind, + sxpf.MakeString("->"), + refValue, + ) + } + return args + }) +} + +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 { + 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 + if align != "" { + tdata = tdata.Cons(te.transformAttribute(attrs.Attributes{"class": align})) + } + return tdata.Cons(te.Make("td")) + } +} + +func (te *TransformEnv) makeRegionFn(sym *sxpf.Symbol, genericToClass bool) transformFn { + return func(args *sxpf.List) sxpf.Object { + a := te.getAttributes(args) if genericToClass { if val, found := a.Get(""); found { a = a.Remove("").AddClass(val) } } - var result sx.ListBuilder - result.Add(sym) + result := sxpf.Nil() if len(a) > 0 { - 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 := sx.Nil().Cons(s) - if al := ev.EvaluateAttrbute(a); al != nil { + 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 { + a = setProgLang(a) + code := sxpf.Nil().Cons(s) + if al := te.transformAttribute(a); al != nil { code = code.Cons(al) } - code = code.Cons(symCODE) - return sx.Nil().Cons(code).Cons(symPRE) + code = code.Cons(te.Make("code")) + return sxpf.Nil().Cons(code).Cons(te.Make("pre")) } -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) +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) }) - 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) - } + 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() if inline == nil { - 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() + 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()) if syntax == api.ValueSyntaxSVG { - 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 := 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, ), ) } - 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)) - }) - 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)), + 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)) + }) + 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)), syntax, data, ) }) - 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) -} + 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) +} + func setProgLang(a attrs.Attributes) attrs.Attributes { if val, found := a.Get(""); found { a = a.AddClass("language-" + val).Remove("") } return a } -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 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)) +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) + } + 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) } 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,74 +4,90 @@ // 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" - "t73f.de/r/sx" - "t73f.de/r/zsc/sz" + "codeberg.org/t73fde/sxpf" + "zettelstore.de/c/sexpr" ) // 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() *Encoder { +func NewEncoder(sf sxpf.SymbolFactory) *Encoder { + if sf == nil { + return nil + } enc := &Encoder{ - sb: strings.Builder{}, + 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), } return enc } -func (enc *Encoder) Encode(lst *sx.Pair) string { +func (enc *Encoder) Encode(lst *sxpf.List) 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 *sx.Pair) string { - return NewEncoder().Encode(lst) +func EvaluateInlineString(lst *sxpf.List) string { + if sf := sxpf.FindSymbolFactory(lst); sf != nil { + return NewEncoder(sf).Encode(lst) + } + return "" } -func (enc *Encoder) executeList(lst *sx.Pair) { +func (enc *Encoder) executeList(lst *sxpf.List) { for elem := lst; elem != nil; elem = elem.Tail() { enc.execute(elem.Car()) } } -func (enc *Encoder) execute(obj sx.Object) { - cmd, isPair := sx.GetPair(obj) - if !isPair { +func (enc *Encoder) execute(obj sxpf.Object) { + cmd, ok := obj.(*sxpf.List) + if !ok { return } sym := cmd.Car() - if sx.IsNil(sym) { + if sxpf.IsNil(sym) { return } - if sym.IsEqual(sz.SymText) { + if sym.IsEqual(enc.symText) { args := cmd.Tail() if args == nil { return } - if val, isString := sx.GetString(args.Car()); isString { - enc.sb.WriteString(val.GetValue()) + if val, ok2 := args.Car().(sxpf.String); ok2 { + enc.sb.WriteString(val.String()) } - } else if sym.IsEqual(sz.SymSpace) || sym.IsEqual(sz.SymSoft) { + } else if sym.IsEqual(enc.symSpace) || sym.IsEqual(enc.symSoft) { enc.sb.WriteByte(' ') - } else if sym.IsEqual(sz.SymHard) { + } else if sym.IsEqual(enc.symHard) { enc.sb.WriteByte('\n') - } else if !sym.IsEqual(sx.SymbolQuote) { + } else if !sym.IsEqual(enc.symQuote) { enc.executeList(cmd.Tail()) } } Index: text/text_test.go ================================================================== --- text/text_test.go +++ text/text_test.go @@ -4,46 +4,43 @@ // 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" - "t73f.de/r/sx" - "t73f.de/r/sx/sxreader" - "t73f.de/r/zsc/text" + "codeberg.org/t73fde/sxpf" + "codeberg.org/t73fde/sxpf/reader" + "zettelstore.de/c/text" ) -func TestSzText(t *testing.T) { +func TestSexprText(t *testing.T) { testcases := []struct { src string exp string }{ {"()", ""}, {`(INLINE (TEXT "a"))`, "a"}, {`(INLINE (SPACE "a"))`, " "}, } for i, tc := range testcases { - sval, err := sxreader.MakeReader(strings.NewReader(tc.src)).Read() + sval, err := reader.MakeReader(strings.NewReader(tc.src)).Read() if err != nil { t.Error(err) continue } - seq, isPair := sx.GetPair(sval) - if !isPair { + seq, ok := sval.(*sxpf.List) + if !ok { 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,41 +1,9 @@ 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 (2023-06-05)

- * Rename "sexpr" to "sz". +

Changes for Version 0.12.0 (pending)

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,35 +1,12 @@ Home This repository contains Go client software to access [https://zettelstore.de|Zettelstore] via its API. -

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] +

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] * [/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.