Index: api/api.go ================================================================== --- api/api.go +++ api/api.go @@ -32,11 +32,11 @@ } } return true } -// ZettelMeta is a map containg the metadata of a zettel. +// ZettelMeta is a map containg the normalized metadata of a zettel. type ZettelMeta map[string]string // ZettelRights is an integer that encode access rights for a zettel. type ZettelRights uint8 @@ -44,11 +44,11 @@ const ( ZettelCanNone ZettelRights = 1 << iota 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 + placeholdergo1 // Was assigned to rename right, which is now removed ZettelCanDelete // Requesting user is allowed to delete the zettel ZettelMaxRight // Sentinel value ) // MetaRights contains the metadata of a zettel, and its rights. @@ -63,14 +63,19 @@ Meta ZettelMeta Rights ZettelRights } // ZettelData contains all data for a zettel. +// +// - Meta is a map containing the metadata of the zettel. +// - Rights is an integer specifying the access rights. +// - Encoding is a string specifying the encoding of the zettel content. +// - Content is the zettel content itself. type ZettelData struct { Meta ZettelMeta Rights ZettelRights Encoding string - Content string + Content string // raw, uninterpreted zettel content } // Aggregate maps metadata keys to list of zettel identifier. type Aggregate map[string][]ZettelID Index: api/const.go ================================================================== --- api/const.go +++ api/const.go @@ -13,87 +13,94 @@ package api import "fmt" -// Predefined Zettel Identifier +// Predefined zettel identifier. +// +// See [List of predefined zettel]. +// +// [List of predefined zettel]: https://zettelstore.de/manual/h/00001005090000 const ( // System zettel - ZidVersion = ZettelID("00000000000001") // -> 0001 - ZidHost = ZettelID("00000000000002") // -> 0002 - ZidOperatingSystem = ZettelID("00000000000003") // -> 0003 - ZidLicense = ZettelID("00000000000004") // -> 0004 - ZidAuthors = ZettelID("00000000000005") // -> 0005 - ZidDependencies = ZettelID("00000000000006") // -> 0006 - ZidLog = ZettelID("00000000000007") // -> 0007 - ZidMemory = ZettelID("00000000000008") // -> 0008 - ZidSx = ZettelID("00000000000009") // -> 0009 - ZidHTTP = ZettelID("00000000000010") // -> 000a - ZidAPI = ZettelID("00000000000011") // -> 000b - ZidWebUI = ZettelID("00000000000012") // -> 000c - ZidConsole = ZettelID("00000000000013") // -> 000d - ZidBoxManager = ZettelID("00000000000020") // -> 000e - ZidZettel = ZettelID("00000000000021") // -> 000f - ZidIndex = ZettelID("00000000000022") // -> 000g - ZidQuery = ZettelID("00000000000023") // -> 000h - ZidMetadataKey = ZettelID("00000000000090") // -> 000i - ZidParser = ZettelID("00000000000092") // -> 000j - ZidStartupConfiguration = ZettelID("00000000000096") // -> 000k - ZidConfiguration = ZettelID("00000000000100") // -> 000l - ZidDirectory = ZettelID("00000000000101") // -> 000m - ZidWarnings = ZettelID("00000000000102") // -> 000n + ZidVersion = ZettelID("00000000000001") + ZidHost = ZettelID("00000000000002") + ZidOperatingSystem = ZettelID("00000000000003") + ZidLicense = ZettelID("00000000000004") + ZidAuthors = ZettelID("00000000000005") + ZidDependencies = ZettelID("00000000000006") + ZidLog = ZettelID("00000000000007") + ZidMemory = ZettelID("00000000000008") + ZidSx = ZettelID("00000000000009") + ZidHTTP = ZettelID("00000000000010") + ZidAPI = ZettelID("00000000000011") + ZidWebUI = ZettelID("00000000000012") + ZidConsole = ZettelID("00000000000013") + ZidBoxManager = ZettelID("00000000000020") + ZidZettel = ZettelID("00000000000021") + ZidIndex = ZettelID("00000000000022") + ZidQuery = ZettelID("00000000000023") + ZidMetadataKey = ZettelID("00000000000090") + ZidParser = ZettelID("00000000000092") + ZidStartupConfiguration = ZettelID("00000000000096") + ZidConfiguration = ZettelID("00000000000100") + ZidDirectory = ZettelID("00000000000101") // WebUI HTML templates are in the range 10000..19999 - ZidBaseTemplate = ZettelID("00000000010100") // -> 000s - ZidLoginTemplate = ZettelID("00000000010200") // -> 000t - ZidListTemplate = ZettelID("00000000010300") // -> 000u - ZidZettelTemplate = ZettelID("00000000010401") // -> 000v - ZidInfoTemplate = ZettelID("00000000010402") // -> 000w - ZidFormTemplate = ZettelID("00000000010403") // -> 000x - ZidRenameTemplate = ZettelID("00000000010404") // -> 001z - ZidDeleteTemplate = ZettelID("00000000010405") // -> 000y - ZidErrorTemplate = ZettelID("00000000010700") // -> 000z + ZidBaseTemplate = ZettelID("00000000010100") + ZidLoginTemplate = ZettelID("00000000010200") + ZidListTemplate = ZettelID("00000000010300") + ZidZettelTemplate = ZettelID("00000000010401") + ZidInfoTemplate = ZettelID("00000000010402") + ZidFormTemplate = ZettelID("00000000010403") + ZidDeleteTemplate = ZettelID("00000000010405") + ZidErrorTemplate = ZettelID("00000000010700") // WebUI sxn code zettel are in the range 19000..19999 - ZidSxnStart = ZettelID("00000000019000") // -> 000q - ZidSxnBase = ZettelID("00000000019990") // -> 000r + ZidSxnStart = ZettelID("00000000019000") + ZidSxnBase = ZettelID("00000000019990") // CSS-related zettel are in the range 20000..29999 - ZidBaseCSS = ZettelID("00000000020001") // -> 0010 - ZidUserCSS = ZettelID("00000000025001") // -> 0011 + ZidBaseCSS = ZettelID("00000000020001") + ZidUserCSS = ZettelID("00000000025001") // WebUI JS zettel are in the range 30000..39999 // WebUI image zettel are in the range 40000..49999 - ZidEmoji = ZettelID("00000000040001") // -> 000o + ZidEmoji = ZettelID("00000000040001") // Other sxn code zettel are in the range 50000..59999 - ZidSxnPrelude = ZettelID("00000000059900") // -> 000p + ZidSxnPrelude = ZettelID("00000000059900") // Predefined Zettelmarkup zettel are in the range 60000..69999 - ZidRoleZettelZettel = ZettelID("00000000060010") // -> 0012 - ZidRoleConfigurationZettel = ZettelID("00000000060020") // -> 0013 - ZidRoleRoleZettel = ZettelID("00000000060030") // -> 0014 - ZidRoleTagZettel = ZettelID("00000000060040") // -> 0015 + ZidRoleZettelZettel = ZettelID("00000000060010") + ZidRoleConfigurationZettel = ZettelID("00000000060020") + ZidRoleRoleZettel = ZettelID("00000000060030") + ZidRoleTagZettel = ZettelID("00000000060040") // Range 90000...99999 is reserved for zettel templates - ZidTOCNewTemplate = ZettelID("00000000090000") // -> 0016 - ZidTemplateNewZettel = ZettelID("00000000090001") // -> 0017 - ZidTemplateNewRole = ZettelID("00000000090004") // -> 001a - ZidTemplateNewTag = ZettelID("00000000090003") // -> 0019 - ZidTemplateNewUser = ZettelID("00000000090002") // -> 0018 - - ZidSession = ZettelID("00009999999997") // -> 00zx - ZidAppDirectory = ZettelID("00009999999998") // -> 00zy - ZidMapping = ZettelID("00009999999999") // -> 00zz - ZidDefaultHome = ZettelID("00010000000000") // -> 0100 + ZidTOCNewTemplate = ZettelID("00000000090000") + ZidTemplateNewZettel = ZettelID("00000000090001") + ZidTemplateNewRole = ZettelID("00000000090004") + ZidTemplateNewTag = ZettelID("00000000090003") + ZidTemplateNewUser = ZettelID("00000000090002") + + // Range 00000999999900...00000999999999 are predefined zettel to be searched by content. + ZidAppDirectory = ZettelID("00000999999999") + + // Default Home Zettel + ZidDefaultHome = ZettelID("00010000000000") ) // LengthZid factors the constant length of a zettel identifier const LengthZid = len(ZidDefaultHome) -// Values of the metadata key/value type. +// Values of the metadata key/value types. +// +// See [Supported Key Types]. +// +// [Supported Key Types]: https://zettelstore.de/manual/h/00001006030000 const ( MetaCredential = "Credential" MetaEmpty = "EString" MetaID = "Identifier" MetaIDSet = "IdentifierSet" @@ -104,11 +111,15 @@ MetaURL = "URL" MetaWord = "Word" MetaZettelmarkup = "Zettelmarkup" ) -// Predefined general Metadata keys +// Predefined / supported metadata keys. +// +// See [Supported Metadata Keys]. +// +// [Supported Metadata Keys]: https://zettelstore.de/manual/h/00001006020000 const ( KeyID = "id" KeyTitle = "title" KeyRole = "role" KeyTags = "tags" @@ -128,44 +139,46 @@ KeyLang = "lang" KeyLicense = "license" KeyModified = "modified" KeyPrecursor = "precursor" KeyPredecessor = "predecessor" + KeyPrequel = "prequel" KeyPublished = "published" KeyQuery = "query" KeyReadOnly = "read-only" + KeySequel = "sequel" KeySubordinates = "subordinates" KeySuccessors = "successors" - KeySuperior = "superior" KeySummary = "summary" + KeySuperior = "superior" KeyURL = "url" KeyUselessFiles = "useless-files" KeyUserID = "user-id" KeyUserRole = "user-role" KeyVisibility = "visibility" ) -// Predefined Metadata values +// Predefined metadata values. 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" - ValueSyntaxNone = "none" - ValueSyntaxSVG = "svg" - ValueSyntaxSxn = "sxn" - ValueSyntaxText = "text" - ValueSyntaxZmk = "zmk" + ValueLangEN = "en" // Default for "lang" + ValueRoleConfiguration = "configuration" // A role for internal zettel + ValueRoleTag = "tag" // A role for tag zettel + ValueRoleRole = "role" // A role for role zettel + ValueRoleZettel = "zettel" // A role for zettel + ValueSyntaxCSS = "css" // Syntax: CSS + ValueSyntaxDraw = "draw" // Syntax: Drawing + ValueSyntaxGif = "gif" // Syntax GIF image + ValueSyntaxHTML = "html" // Syntax: HTML + ValueSyntaxMarkdown = "markdown" // Syntax: Markdown / CommonMark + ValueSyntaxMD = "md" // Syntax: Markdown / CommonMark + ValueSyntaxNone = "none" // Syntax: no syntax / content, just metadata + ValueSyntaxSVG = "svg" // Syntax: SVG + ValueSyntaxSxn = "sxn" // Syntax: S-Expression + ValueSyntaxText = "text" // Syntax: plain text + ValueSyntaxZmk = "zmk" // Syntax: Zettelmarkup ValueUserRoleCreator = "creator" ValueUserRoleOwner = "owner" ValueUserRoleReader = "reader" ValueUserRoleWriter = "writer" ValueVisibilityCreator = "creator" @@ -175,12 +188,10 @@ ValueVisibilityPublic = "public" ) // Additional HTTP constants. const ( - MethodMove = "MOVE" // HTTP method for renaming a zettel - HeaderAccept = "Accept" HeaderContentType = "Content-Type" HeaderDestination = "Destination" HeaderLocation = "Location" ) @@ -198,19 +209,19 @@ QueryKeyTag = "tag" ) // Supported encoding values. const ( - EncodingHTML = "html" - EncodingMD = "md" - EncodingSHTML = "shtml" - EncodingSz = "sz" - EncodingText = "text" - EncodingZMK = "zmk" - - EncodingPlain = "plain" - EncodingData = "data" + EncodingHTML = "html" // Plain HTML + EncodingMD = "md" // Markdown + EncodingSHTML = "shtml" // SxHTML + EncodingSz = "sz" // Structure of zettel, encoded a an S-expression + EncodingText = "text" // plain text content + EncodingZMK = "zmk" // Zettelmarkup + + EncodingPlain = "plain" // Plain zettel, no processing + EncodingData = "data" // Plain zettel, metadata as S-Expression ) var mapEncodingEnum = map[string]EncodingEnum{ EncodingHTML: EncoderHTML, EncodingMD: EncoderMD, @@ -271,65 +282,65 @@ ) // Command to be executed atthe Zettelstore type Command string -// Supported command values +// Supported command values. const ( CommandAuthenticated = Command("authenticated") 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 = "!?" +// Supported search operator representations. +const ( + BackwardDirective = "BACKWARD" // Backward-only context + ContextDirective = "CONTEXT" // Context directive + CostDirective = "COST" // Maximum cost of a context operation + ForwardDirective = "FORWARD" // Forward-only context + FullDirective = "FULL" // Include tags in context + IdentDirective = "IDENT" // Use only specified zettel + ItemsDirective = "ITEMS" // Select list elements in a zettel + MaxDirective = "MAX" // Maximum number of context results + LimitDirective = "LIMIT" // Maximum number of zettel + OffsetDirective = "OFFSET" // Offset to start returned zettel list + OrDirective = "OR" // Combine several search expression with an "or" + OrderDirective = "ORDER" // Specify metadata keys for the order of returned list + PhraseDirective = "PHRASE" // Only unlinked zettel with given phrase + PickDirective = "PICK" // Pick some random zettel + RandomDirective = "RANDOM" // Order zettel list randomly + ReverseDirective = "REVERSE" // Reverse the order of a zettel list + UnlinkedDirective = "UNLINKED" // Search for zettel that contain a phase(s) but do not link + + ActionSeparator = "|" // Separates action list of previous elements of query expression + + AtomAction = "ATOM" // Return an Atom web feed + KeysAction = "KEYS" // Provide metadata key used + MinAction = "MIN" // Return only those values with a minimum amount of zettel + MaxAction = "MAX" // Return only those values with a maximum amount of zettel + NumberedAction = "NUMBERED" // Return a numbered list + RedirectAction = "REDIRECT" // Return the first zettel in list + ReIndexAction = "REINDEX" // Ensure that zettel is/are indexed. + RSSAction = "RSS" // Return a RSS web feed + TitleAction = "TITLE" // Set a title for Atom or RSS web feed + + ExistOperator = "?" // Does zettel have metadata with given key? + ExistNotOperator = "!?" // True id zettel does not have metadata with given key. SearchOperatorNot = "!" - SearchOperatorEqual = "=" - SearchOperatorNotEqual = "!=" - SearchOperatorHas = ":" - SearchOperatorHasNot = "!:" - SearchOperatorPrefix = "[" - SearchOperatorNoPrefix = "![" - SearchOperatorSuffix = "]" - SearchOperatorNoSuffix = "!]" - SearchOperatorMatch = "~" - SearchOperatorNoMatch = "!~" - SearchOperatorLess = "<" - SearchOperatorNotLess = "!<" - SearchOperatorGreater = ">" - SearchOperatorNotGreater = "!>" + SearchOperatorEqual = "=" // True if values are equal + SearchOperatorNotEqual = "!=" // False if values are equal + SearchOperatorHas = ":" // True if values are equal/included + SearchOperatorHasNot = "!:" // False if values are equal/included + SearchOperatorPrefix = "[" // True if value is prefix of the other + SearchOperatorNoPrefix = "![" // False if value is prefix of the other + SearchOperatorSuffix = "]" // True if value is suffix of other + SearchOperatorNoSuffix = "!]" // False if value is suffix of other + SearchOperatorMatch = "~" // True if value is included in other + SearchOperatorNoMatch = "!~" // False if value is included in other + SearchOperatorLess = "<" // True if value is smaller than other + SearchOperatorNotLess = "!<" // False if value is smaller than other + SearchOperatorGreater = ">" // True if value is greater than other + SearchOperatorNotGreater = "!>" // False if value is greater than other ) // QueryPrefix is the prefix that denotes a query expression within a reference. const QueryPrefix = "query:" Index: api/urlbuilder.go ================================================================== --- api/urlbuilder.go +++ api/urlbuilder.go @@ -31,11 +31,11 @@ result.base.AddPath(string([]byte{key})) } return &result } -// Clone an URLBuilder +// Clone an URLBuilder. func (ub *URLBuilder) Clone() *URLBuilder { cpy := new(URLBuilder) ub.base.Copy(&cpy.base) cpy.prefix = ub.prefix return cpy @@ -45,23 +45,25 @@ func (ub *URLBuilder) SetZid(zid ZettelID) *URLBuilder { ub.base.AddPath(string(zid)) return ub } -// AppendPath adds a new path element +// AppendPath adds a new path element. func (ub *URLBuilder) AppendPath(p string) *URLBuilder { ub.base.AddPath(p) return ub } -// AppendKVQuery adds a new key/value query parameter +// AppendKVQuery adds a new key/value query parameter. func (ub *URLBuilder) AppendKVQuery(key, value string) *URLBuilder { ub.base.AddQuery(key, value) return ub } -// AppendQuery adds a new query +// AppendQuery adds a new query. +// +// Basically the same as [URLBuilder.AppendKVQuery]([api.QueryKeyQuery], value) func (ub *URLBuilder) AppendQuery(value string) *URLBuilder { if value != "" { ub.base.AddQuery(QueryKeyQuery, value) } return ub @@ -71,15 +73,15 @@ func (ub *URLBuilder) ClearQuery() *URLBuilder { ub.base.RemoveQueries() return ub } -// SetFragment stores the fragment +// SetFragment sets the fragment. func (ub *URLBuilder) SetFragment(s string) *URLBuilder { ub.base.SetFragment(s) return ub } // String produces a string value. func (ub *URLBuilder) String() string { return ub.prefix + ub.base.String() } Index: attrs/attrs.go ================================================================== --- attrs/attrs.go +++ attrs/attrs.go @@ -13,10 +13,11 @@ // Package attrs stores attributes of zettel parts. package attrs import ( + "slices" "strings" "t73f.de/r/zsc/maps" ) @@ -85,44 +86,41 @@ delete(a, key) } return a } -// AddClass adds a value to the class attribute. -func (a Attributes) AddClass(class string) Attributes { - if a == nil { - return map[string]string{"class": class} - } - classes := a.GetClasses() - for _, cls := range classes { - if cls == class { - return a - } - } - classes = append(classes, class) - a["class"] = strings.Join(classes, " ") - return a -} - -// GetClasses returns the class values as a string slice -func (a Attributes) GetClasses() []string { - if a == nil { - return nil - } - classes, ok := a["class"] - if !ok { - return nil - } - return strings.Fields(classes) -} - -// HasClass returns true, if attributes contains the given class. -func (a Attributes) HasClass(s string) bool { - if a == nil { - return false - } - classes, found := a["class"] - if !found { - return false - } - return strings.Contains(" "+classes+" ", " "+s+" ") -} +// Add a value to an attribute key. +func (a Attributes) Add(key, value string) Attributes { + if a == nil { + return map[string]string{key: value} + } + values := a.Values(key) + if !slices.Contains(values, value) { + values = append(values, value) + a[key] = strings.Join(values, " ") + } + return a +} + +// Values are the space separated values of an attribute. +func (a Attributes) Values(key string) []string { + if a != nil { + if value, ok := a[key]; ok { + return strings.Fields(value) + } + } + return nil +} + +// Has the attribute key a value? +func (a Attributes) Has(key, value string) bool { + return slices.Contains(a.Values(key), value) +} + +// AddClass adds a value to the class attribute. +func (a Attributes) AddClass(class string) Attributes { return a.Add("class", class) } + +// GetClasses returns the class values as a string slice +func (a Attributes) GetClasses() []string { return a.Values("class") } + +// HasClass returns true, if attributes contains the given class. +func (a Attributes) HasClass(s string) bool { return a.Has("class", s) } Index: attrs/attrs_test.go ================================================================== --- attrs/attrs_test.go +++ attrs/attrs_test.go @@ -55,11 +55,11 @@ testcases := []struct { classes string class string exp bool }{ - {"", "", true}, + {"", "", false}, {"x", "", false}, {"x", "x", true}, {"x", "y", false}, {"abc def ghi", "abc", true}, {"abc def ghi", "def", true}, Index: client/client.go ================================================================== --- client/client.go +++ client/client.go @@ -13,11 +13,10 @@ // Package client provides a client for accessing the Zettelstore via its API. package client import ( - "bufio" "bytes" "context" "fmt" "io" "net" @@ -29,11 +28,10 @@ "t73f.de/r/sx" "t73f.de/r/sx/sxreader" "t73f.de/r/zsc/api" "t73f.de/r/zsc/sexp" - "t73f.de/r/zsc/sz" ) // Client contains all data to execute requests. type Client struct { base string @@ -46,11 +44,11 @@ } // Base returns the base part of the URLs that are used to communicate with a Zettelstore. func (c *Client) Base() string { return c.base } -// NewClient create a new client. +// NewClient creates a new client with a given base URL to a Zettelstore. func NewClient(u *url.URL) *Client { myURL := *u myURL.User = nil myURL.ForceQuery = false myURL.RawQuery = "" @@ -71,29 +69,34 @@ } 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. +// using the Zettelstore. The original behaviour can be restored by setting +// allow to false. func (c *Client) AllowRedirect(allow bool) { if allow { - c.client.CheckRedirect = func(req *http.Request, via []*http.Request) error { + c.client.CheckRedirect = func(*http.Request, []*http.Request) error { return http.ErrUseLastResponse } } else { c.client.CheckRedirect = nil } } // Error encapsulates the possible client call errors. +// +// - StatusCode is the HTTP status code, e.g. 200 +// - Message is the HTTP message, e.g. "OK" +// - Body is the HTTP body returned by a request. type Error struct { StatusCode int Message string Body []byte } +// Error returns the error as a string. func (err *Error) Error() string { var body string if err.Body == nil { body = "nil" } else if bl := len(err.Body); bl == 0 { @@ -125,10 +128,15 @@ Body: body, } } // NewURLBuilder creates a new URL builder for the client with the given key. +// +// key is one of the defined lower case letters to specify an endpoint. +// See [Endpoints used by the API] for details. +// +// [Endpoints used by the API]: https://zettelstore.de/manual/h/00001012920000 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) @@ -139,34 +147,37 @@ req.Header.Add("Authorization", c.tokenType+" "+c.token) } resp, err := c.client.Do(req) if err != nil { if resp != nil && resp.Body != nil { - resp.Body.Close() + _ = resp.Body.Close() } return nil, err } return resp, err } func (c *Client) buildAndExecuteRequest( - ctx context.Context, method string, ub *api.URLBuilder, body io.Reader, h http.Header) (*http.Response, error) { + ctx context.Context, + method string, + ub *api.URLBuilder, + body io.Reader, +) (*http.Response, error) { req, err := c.newRequest(ctx, method, ub, body) if err != nil { return nil, err } err = c.updateToken(ctx) if err != nil { return nil, err } - for key, val := range h { - req.Header[key] = append(req.Header[key], val...) - } return c.executeRequest(req) } // SetAuth sets authentication data. +// +// username and password are the same values that are used to authenticate via the Web-UI. func (c *Client) SetAuth(username, password string) { c.username = username c.password = password c.token = "" c.tokenType = "" @@ -210,10 +221,12 @@ } return c.RefreshToken(ctx) } // Authenticate sets a new token by sending user name and password. +// +// [Client.SetAuth] should be called before. 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())) if err != nil { return err @@ -221,22 +234,28 @@ req.Header.Set("Content-Type", "application/x-www-form-urlencoded") return c.executeAuthRequest(req) } // RefreshToken updates the access token +// +// [Client.SetAuth] should be called before. func (c *Client) RefreshToken(ctx context.Context) error { 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. +// +// data contains the zettel metadata and content, as it is stored in a file in a zettel box, +// or as returned by [Client.GetZettel]. +// Metadata is separated from zettel content by an empty line. func (c *Client) CreateZettel(ctx context.Context, data []byte) (api.ZettelID, error) { ub := c.NewURLBuilder('z') - resp, err := c.buildAndExecuteRequest(ctx, http.MethodPost, ub, bytes.NewBuffer(data), nil) + resp, err := c.buildAndExecuteRequest(ctx, http.MethodPost, ub, bytes.NewBuffer(data)) if err != nil { return api.InvalidZID, err } defer resp.Body.Close() if resp.StatusCode != http.StatusCreated { @@ -251,17 +270,19 @@ } return api.InvalidZID, err } // CreateZettelData creates a new zettel and returns its URL. +// +// data contains the zettel date, encoded as explicit struct. func (c *Client) CreateZettelData(ctx context.Context, data api.ZettelData) (api.ZettelID, error) { var buf bytes.Buffer if _, err := sx.Print(&buf, sexp.EncodeZettel(data)); err != nil { return api.InvalidZID, err } ub := c.NewURLBuilder('z').AppendKVQuery(api.QueryKeyEncoding, api.EncodingData) - resp, err := c.buildAndExecuteRequest(ctx, http.MethodPost, ub, &buf, nil) + resp, err := c.buildAndExecuteRequest(ctx, http.MethodPost, ub, &buf) if err != nil { return api.InvalidZID, err } defer resp.Body.Close() rdr := sxreader.MakeReader(resp.Body) @@ -273,127 +294,10 @@ return api.InvalidZID, err } return makeZettelID(obj) } -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) - 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) - } - if err != nil { - return nil, err - } - lines := bytes.Split(data, bsLF) - if len(lines[len(lines)-1]) == 0 { - 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) } @@ -406,227 +310,18 @@ return api.InvalidZID, fmt.Errorf("invalid zettel ID: %v", val) } return zid, nil } -// QueryAggregate returns a aggregate as a result of a query. -// It is most often used in a query with an action, where the action is either -// a metadata key of type Word or of type TagSet. -func (c *Client) QueryAggregate(ctx context.Context, query string) (api.Aggregate, error) { - lines, err := c.QueryZettel(ctx, query) - if err != nil { - return nil, err - } - if len(lines) == 0 { - return nil, nil - } - agg := make(api.Aggregate, len(lines)) - for _, line := range lines { - if fields := bytes.Fields(line); len(fields) > 1 { - key := string(fields[0]) - for _, field := range fields[1:] { - if zid := api.ZettelID(string(field)); zid.IsValid() { - agg[key] = append(agg[key], zid) - } - } - } - } - return agg, nil -} - -// TagZettel returns the tag zettel of a given tag. -// -// This method only works if c.AllowRedirect(true) was called. -func (c *Client) TagZettel(ctx context.Context, tag string) (api.ZettelID, error) { - return c.fetchTagOrRoleZettel(ctx, api.QueryKeyTag, tag) -} - -// RoleZettel returns the tag zettel of a given tag. -// -// This method only works if c.AllowRedirect(true) was called. -func (c *Client) RoleZettel(ctx context.Context, role string) (api.ZettelID, error) { - return c.fetchTagOrRoleZettel(ctx, api.QueryKeyRole, role) -} - -func (c *Client) fetchTagOrRoleZettel(ctx context.Context, key, val string) (api.ZettelID, error) { - if c.client.CheckRedirect == nil { - panic("client does not allow to track redirect") - } - ub := c.NewURLBuilder('z').AppendKVQuery(key, val) - resp, err := c.buildAndExecuteRequest(ctx, http.MethodGet, ub, nil, nil) - if err != nil { - return api.InvalidZID, err - } - defer resp.Body.Close() - data, err := io.ReadAll(resp.Body) - if err != nil { - return api.InvalidZID, err - } - - switch resp.StatusCode { - case http.StatusNotFound: - return "", nil - case http.StatusFound: - zid := api.ZettelID(data) - if zid.IsValid() { - return zid, nil - } - return api.InvalidZID, nil - default: - return api.InvalidZID, statusToError(resp) - } -} - -// GetZettel returns a zettel as a string. -func (c *Client) GetZettel(ctx context.Context, zid api.ZettelID, part string) ([]byte, error) { - ub := c.NewURLBuilder('z').SetZid(zid) - 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 -} - -// GetZettelData returns a zettel as a struct of its parts. -func (c *Client) GetZettelData(ctx context.Context, zid api.ZettelID) (api.ZettelData, error) { - ub := c.NewURLBuilder('z').SetZid(zid) - ub.AppendKVQuery(api.QueryKeyEncoding, api.EncodingData) - ub.AppendKVQuery(api.QueryKeyPart, api.PartZettel) - resp, err := c.buildAndExecuteRequest(ctx, http.MethodGet, ub, nil, nil) - if err == nil { - defer resp.Body.Close() - if resp.StatusCode != http.StatusOK { - return api.ZettelData{}, statusToError(resp) - } - rdr := sxreader.MakeReader(resp.Body) - obj, err2 := rdr.Read() - if err2 == nil { - return sexp.ParseZettel(obj) - } - } - return api.ZettelData{}, err -} - -// GetParsedZettel return a parsed zettel in a defined encoding. -func (c *Client) GetParsedZettel(ctx context.Context, zid api.ZettelID, enc api.EncodingEnum) ([]byte, error) { - return c.getZettelString(ctx, zid, enc, true) -} - -// GetEvaluatedZettel return an evaluated zettel in a defined encoding. -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.AppendKVQuery(api.QueryKeyEncoding, enc.String()) - ub.AppendKVQuery(api.QueryKeyPart, api.PartContent) - if parseOnly { - ub.AppendKVQuery(api.QueryKeyParseOnly, "") - } - resp, err := c.buildAndExecuteRequest(ctx, http.MethodGet, ub, nil, nil) - if err != nil { - return nil, err - } - defer resp.Body.Close() - switch resp.StatusCode { - case http.StatusOK: - case http.StatusNoContent: - default: - 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) -} - -// GetEvaluatedSz returns an evaluated zettel as a Sexpr-decoded data structure. -func (c *Client) GetEvaluatedSz(ctx context.Context, zid api.ZettelID, part string) (sx.Object, error) { - return c.getSz(ctx, zid, part, false) -} - -func (c *Client) getSz(ctx context.Context, zid api.ZettelID, part string, parseOnly bool) (sx.Object, error) { - ub := c.NewURLBuilder('z').SetZid(zid) - ub.AppendKVQuery(api.QueryKeyEncoding, api.EncodingSz) - if part != "" { - ub.AppendKVQuery(api.QueryKeyPart, part) - } - if parseOnly { - ub.AppendKVQuery(api.QueryKeyParseOnly, "") - } - 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) - } - return sxreader.MakeReader(bufio.NewReaderSize(resp.Body, 8)).Read() -} - -// GetMetaData returns the metadata of a zettel. -func (c *Client) GetMetaData(ctx context.Context, zid api.ZettelID) (api.MetaRights, error) { - ub := c.NewURLBuilder('z').SetZid(zid) - ub.AppendKVQuery(api.QueryKeyEncoding, api.EncodingData) - ub.AppendKVQuery(api.QueryKeyPart, api.PartMeta) - resp, err := c.buildAndExecuteRequest(ctx, http.MethodGet, ub, nil, nil) - if err != nil { - return api.MetaRights{}, err - } - defer resp.Body.Close() - rdr := sxreader.MakeReader(resp.Body) - obj, err := rdr.Read() - if resp.StatusCode != http.StatusOK { - return api.MetaRights{}, statusToError(resp) - } - if err != nil { - return api.MetaRights{}, err - } - vals, err := sexp.ParseList(obj, "ypp") - if err != nil { - return api.MetaRights{}, err - } - if errSym := sexp.CheckSymbol(vals[0], "list"); errSym != nil { - return api.MetaRights{}, err - } - - meta, err := sexp.ParseMeta(vals[1].(*sx.Pair)) - if err != nil { - return api.MetaRights{}, err - } - - rights, err := sexp.ParseRights(vals[2]) - if err != nil { - return api.MetaRights{}, err - } - - return api.MetaRights{ - Meta: meta, - Rights: rights, - }, nil -} - -// UpdateZettel updates an existing zettel. +// UpdateZettel updates an existing zettel, specified by its zettel identifier. +// +// data contains the zettel metadata and content, as it is stored in a file in a zettel box, +// or as returned by [Client.GetZettel]. +// Metadata is separated from zettel content by an empty line. func (c *Client) UpdateZettel(ctx context.Context, zid api.ZettelID, data []byte) error { ub := c.NewURLBuilder('z').SetZid(zid) - resp, err := c.buildAndExecuteRequest(ctx, http.MethodPut, ub, bytes.NewBuffer(data), nil) + resp, err := c.buildAndExecuteRequest(ctx, http.MethodPut, ub, bytes.NewBuffer(data)) if err != nil { return err } defer resp.Body.Close() if resp.StatusCode != http.StatusNoContent { @@ -633,37 +328,18 @@ return statusToError(resp) } return nil } -// UpdateZettelData updates an existing zettel. +// UpdateZettelData updates an existing zettel, specified by its zettel identifier. func (c *Client) UpdateZettelData(ctx context.Context, zid api.ZettelID, data api.ZettelData) error { var buf bytes.Buffer if _, err := sx.Print(&buf, sexp.EncodeZettel(data)); err != nil { return err } ub := c.NewURLBuilder('z').SetZid(zid).AppendKVQuery(api.QueryKeyEncoding, api.EncodingData) - resp, err := c.buildAndExecuteRequest(ctx, http.MethodPut, ub, &buf, nil) - if err != nil { - return err - } - defer resp.Body.Close() - if resp.StatusCode != http.StatusNoContent { - return statusToError(resp) - } - return nil -} - -// RenameZettel renames a zettel. -// -// This function is deprecated and will be removed in v0.19 (or later). -func (c *Client) RenameZettel(ctx context.Context, oldZid, newZid api.ZettelID) error { - ub := c.NewURLBuilder('z').SetZid(oldZid) - h := http.Header{ - api.HeaderDestination: {c.NewURLBuilder('z').SetZid(newZid).String()}, - } - resp, err := c.buildAndExecuteRequest(ctx, api.MethodMove, ub, nil, h) + resp, err := c.buildAndExecuteRequest(ctx, http.MethodPut, ub, &buf) if err != nil { return err } defer resp.Body.Close() if resp.StatusCode != http.StatusNoContent { @@ -673,11 +349,11 @@ } // 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) - resp, err := c.buildAndExecuteRequest(ctx, http.MethodDelete, ub, nil, nil) + resp, err := c.buildAndExecuteRequest(ctx, http.MethodDelete, ub, nil) if err != nil { return err } defer resp.Body.Close() if resp.StatusCode != http.StatusNoContent { @@ -685,88 +361,21 @@ } return nil } // ExecuteCommand will execute a given command at the Zettelstore. +// +// See [API commands] for a list of valid commands. +// +// [API commands]: https://zettelstore.de/manual/h/00001012080100 func (c *Client) ExecuteCommand(ctx context.Context, command api.Command) error { ub := c.NewURLBuilder('x').AppendKVQuery(api.QueryKeyCommand, string(command)) - resp, err := c.buildAndExecuteRequest(ctx, http.MethodPost, ub, nil, nil) + resp, err := c.buildAndExecuteRequest(ctx, http.MethodPost, ub, nil) if err != nil { return err } defer resp.Body.Close() if resp.StatusCode != http.StatusNoContent { 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) - if err != nil { - return VersionInfo{}, err - } - defer resp.Body.Close() - if resp.StatusCode != http.StatusOK { - return VersionInfo{}, statusToError(resp) - } - rdr := sxreader.MakeReader(resp.Body) - obj, err := rdr.Read() - if err == nil { - if vals, errVals := sexp.ParseList(obj, "iiiss"); errVals == nil { - return VersionInfo{ - Major: int(vals[0].(sx.Int64)), - Minor: int(vals[1].(sx.Int64)), - Patch: int(vals[2].(sx.Int64)), - Info: vals[3].(sx.String).GetValue(), - Hash: vals[4].(sx.String).GetValue(), - }, nil - } - } - return VersionInfo{}, err -} - -// VersionInfo contains version information. -type VersionInfo struct { - Major int - Minor int - Patch int - Info string - Hash string -} - -// GetApplicationZid returns the zettel identifier used to configure client -// application with the given name. -func (c *Client) GetApplicationZid(ctx context.Context, appname string) (api.ZettelID, error) { - mr, err := c.GetMetaData(ctx, api.ZidAppDirectory) - if err != nil { - return api.InvalidZID, err - } - key := appname + "-zid" - val, found := mr.Meta[key] - if !found { - return api.InvalidZID, fmt.Errorf("no application registered: %v", appname) - } - if zid := api.ZettelID(val); zid.IsValid() { - return zid, nil - } - return api.InvalidZID, fmt.Errorf("invalid identifier for application %v: %v", appname, val) -} - -// Get executes a GET request to the given URL and returns the read data. -func (c *Client) Get(ctx context.Context, ub *api.URLBuilder) ([]byte, error) { - resp, err := c.buildAndExecuteRequest(ctx, http.MethodGet, ub, nil, nil) - if err != nil { - return nil, err - } - defer resp.Body.Close() - switch resp.StatusCode { - case http.StatusOK: - case http.StatusNoContent: - return nil, nil - default: - return nil, statusToError(resp) - } - data, err := io.ReadAll(resp.Body) - return data, err -} ADDED client/retrieve.go Index: client/retrieve.go ================================================================== --- /dev/null +++ client/retrieve.go @@ -0,0 +1,498 @@ +//----------------------------------------------------------------------------- +// Copyright (c) 2021-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: 2021-present Detlef Stern +//----------------------------------------------------------------------------- + +package client + +import ( + "bufio" + "bytes" + "context" + "fmt" + "io" + "net/http" + + "t73f.de/r/sx" + "t73f.de/r/sx/sxreader" + "t73f.de/r/zsc/api" + "t73f.de/r/zsc/sexp" + "t73f.de/r/zsc/sz" +) + +var bsLF = []byte{'\n'} + +// QueryZettel returns a list of all Zettel based on the given query. +// +// query is a search expression, as described in [Query the list of all zettel]. +// +// The functions returns a slice of bytes slices, where each byte slice contains the +// zettel identifier within its first 14 bytes. The next byte is a space character, +// followed by the title of the zettel. +// +// [Query the list of all zettel]: https://zettelstore.de/manual/h/00001012051400 +func (c *Client) QueryZettel(ctx context.Context, query string) ([][]byte, error) { + ub := c.NewURLBuilder('z').AppendQuery(query) + resp, err := c.buildAndExecuteRequest(ctx, http.MethodGet, ub, nil) + 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) + } + if err != nil { + return nil, err + } + lines := bytes.Split(data, bsLF) + if len(lines[len(lines)-1]) == 0 { + lines = lines[:len(lines)-1] + } + return lines, nil +} + +// QueryZettelData returns a list of zettel metadata. +// +// query is a search expression, as described in [Query the list of all zettel]. +// +// The functions returns the normalized query and its human-readable representation as +// its first two result values. +// +// [Query the list of all zettel]: https://zettelstore.de/manual/h/00001012051400 +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) + 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 +} + +// 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. +// +// query is a search expression, as described in [Query the list of all zettel]. +// It must contain an aggregate action. +// +// [Query the list of all zettel]: https://zettelstore.de/manual/h/00001012051400 +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 identifier of the tag zettel for 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 identifier of the tag zettel for a given role. +// +// 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) + if err != nil { + return api.InvalidZID, err + } + defer resp.Body.Close() + data, err := io.ReadAll(resp.Body) + if err != nil { + return api.InvalidZID, err + } + + switch resp.StatusCode { + case http.StatusNotFound: + return "", nil + case http.StatusFound: + zid := api.ZettelID(data) + if zid.IsValid() { + return zid, nil + } + return api.InvalidZID, nil + default: + return api.InvalidZID, statusToError(resp) + } +} + +// GetZettel returns a zettel as a byte slice. +// +// part must be one of "meta", "content", or "zettel". +// +// The format of the byte slice is described in [Layout of a zettel]. +// +// [Layout of a zettel]: https://zettelstore.de/manual/h/00001006000000 +func (c *Client) GetZettel(ctx context.Context, zid api.ZettelID, part string) ([]byte, error) { + 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) + 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 +} + +// GetZettelData returns a zettel as a struct of its parts. +func (c *Client) GetZettelData(ctx context.Context, zid api.ZettelID) (api.ZettelData, error) { + ub := c.NewURLBuilder('z').SetZid(zid) + ub.AppendKVQuery(api.QueryKeyEncoding, api.EncodingData) + ub.AppendKVQuery(api.QueryKeyPart, api.PartZettel) + resp, err := c.buildAndExecuteRequest(ctx, http.MethodGet, ub, nil) + if err == nil { + defer resp.Body.Close() + if resp.StatusCode != http.StatusOK { + return api.ZettelData{}, statusToError(resp) + } + rdr := sxreader.MakeReader(resp.Body) + obj, err2 := rdr.Read() + if err2 == nil { + return sexp.ParseZettel(obj) + } + } + return api.ZettelData{}, err +} + +// GetParsedZettel return a parsed zettel in a specified text-based encoding. +// +// A parsed zettel is just read from its box and is not processed any further. +// +// Valid encoding values are given as constants. They are described in more +// detail in [Encodings available via the API]. +// +// [Encodings available via the API]: https://zettelstore.de/manual/h/00001012920500 +func (c *Client) GetParsedZettel(ctx context.Context, zid api.ZettelID, enc api.EncodingEnum) ([]byte, error) { + return c.getZettelString(ctx, zid, enc, true) +} + +// GetEvaluatedZettel return an evaluated zettel in a specified text-based encoding. +// +// An evaluated zettel was parsed, and any transclusions etc. are resolved. +// This is the zettel representation you typically see on the Web UI. +// +// Valid encoding values are given as constants. They are described in more +// detail in [Encodings available via the API]. +// +// [Encodings available via the API]: https://zettelstore.de/manual/h/00001012920500 +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.AppendKVQuery(api.QueryKeyEncoding, enc.String()) + ub.AppendKVQuery(api.QueryKeyPart, api.PartContent) + if parseOnly { + ub.AppendKVQuery(api.QueryKeyParseOnly, "") + } + resp, err := c.buildAndExecuteRequest(ctx, http.MethodGet, ub, nil) + if err != nil { + return nil, err + } + defer resp.Body.Close() + switch resp.StatusCode { + case http.StatusOK: + case http.StatusNoContent: + default: + return nil, statusToError(resp) + } + return io.ReadAll(resp.Body) +} + +// GetParsedSz returns a part of an parsed zettel as a Sexpr-decoded data structure. +// +// A parsed zettel is just read from its box and is not processed any further. +// +// part must be one of "meta", "content", or "zettel". +// +// Basically, this function returns the sz encoding of a part of a zettel. +func (c *Client) GetParsedSz(ctx context.Context, zid api.ZettelID, part string) (sx.Object, error) { + return c.getSz(ctx, zid, part, true) +} + +// GetEvaluatedSz returns an evaluated zettel as a Sexpr-decoded data structure. +// +// An evaluated zettel was parsed, and any transclusions etc. are resolved. +// This is the zettel representation you typically see on the Web UI. +// +// part must be one of "meta", "content", or "zettel". +// +// Basically, this function returns the sz encoding of a part of a zettel. +func (c *Client) GetEvaluatedSz(ctx context.Context, zid api.ZettelID, part string) (sx.Object, error) { + return c.getSz(ctx, zid, part, false) +} + +func (c *Client) getSz(ctx context.Context, zid api.ZettelID, part string, parseOnly bool) (sx.Object, error) { + ub := c.NewURLBuilder('z').SetZid(zid) + ub.AppendKVQuery(api.QueryKeyEncoding, api.EncodingSz) + if part != "" { + ub.AppendKVQuery(api.QueryKeyPart, part) + } + if parseOnly { + ub.AppendKVQuery(api.QueryKeyParseOnly, "") + } + resp, err := c.buildAndExecuteRequest(ctx, http.MethodGet, ub, nil) + if err != nil { + return nil, err + } + defer resp.Body.Close() + if resp.StatusCode != http.StatusOK { + return nil, statusToError(resp) + } + return sxreader.MakeReader(bufio.NewReaderSize(resp.Body, 8)).Read() +} + +// GetMetaData returns the metadata of a zettel. +func (c *Client) GetMetaData(ctx context.Context, zid api.ZettelID) (api.MetaRights, error) { + ub := c.NewURLBuilder('z').SetZid(zid) + ub.AppendKVQuery(api.QueryKeyEncoding, api.EncodingData) + ub.AppendKVQuery(api.QueryKeyPart, api.PartMeta) + resp, err := c.buildAndExecuteRequest(ctx, http.MethodGet, ub, nil) + if err != nil { + return api.MetaRights{}, err + } + defer resp.Body.Close() + rdr := sxreader.MakeReader(resp.Body) + obj, err := rdr.Read() + if resp.StatusCode != http.StatusOK { + return api.MetaRights{}, statusToError(resp) + } + if err != nil { + return api.MetaRights{}, err + } + vals, err := sexp.ParseList(obj, "ypp") + if err != nil { + return api.MetaRights{}, err + } + if errSym := sexp.CheckSymbol(vals[0], "list"); errSym != nil { + return api.MetaRights{}, err + } + + meta, err := sexp.ParseMeta(vals[1].(*sx.Pair)) + if err != nil { + return api.MetaRights{}, err + } + + rights, err := sexp.ParseRights(vals[2]) + if err != nil { + return api.MetaRights{}, err + } + + return api.MetaRights{ + Meta: meta, + Rights: rights, + }, nil +} + +// GetVersionInfo returns version information of the Zettelstore that is used. +func (c *Client) GetVersionInfo(ctx context.Context) (VersionInfo, error) { + resp, err := c.buildAndExecuteRequest(ctx, http.MethodGet, c.NewURLBuilder('x'), nil) + if err != nil { + return VersionInfo{}, err + } + defer resp.Body.Close() + if resp.StatusCode != http.StatusOK { + return VersionInfo{}, statusToError(resp) + } + rdr := sxreader.MakeReader(resp.Body) + obj, err := rdr.Read() + if err == nil { + if vals, errVals := sexp.ParseList(obj, "iiiss"); errVals == nil { + return VersionInfo{ + Major: int(vals[0].(sx.Int64)), + Minor: int(vals[1].(sx.Int64)), + Patch: int(vals[2].(sx.Int64)), + Info: vals[3].(sx.String).GetValue(), + Hash: vals[4].(sx.String).GetValue(), + }, nil + } + } + return VersionInfo{}, err +} + +// VersionInfo contains version information of the associated Zettelstore. +// +// - Major is an integer containing the major software version of Zettelstore. +// If its value is greater than zero, different major versions are not compatible. +// - Minor is an integer specifying the minor software version for the given major version. +// If the major version is greater than zero, minor versions are backward compatible. +// - Patch is an integer that specifies a change within a minor version. +// A version that have equal major and minor versions and differ in patch version are +// always compatible, even if the major version equals zero. +// - Info contains some optional text, i.e. it may be the empty string. Typically, Info +// specifies a developer version by containing the string "dev". +// - Hash contains the value of the source code version stored in the Zettelstore repository. +// You can use it to reproduce bugs that occured, when source code was changed since +// its introduction. +type VersionInfo struct { + Major int + Minor int + Patch int + Info string + Hash string +} + +// GetApplicationZid returns the zettel identifier used to configure a client +// application with the given name. +func (c *Client) GetApplicationZid(ctx context.Context, appname string) (api.ZettelID, error) { + mr, err := c.GetMetaData(ctx, api.ZidAppDirectory) + if err != nil { + return api.InvalidZID, err + } + key := appname + "-zid" + val, found := mr.Meta[key] + if !found { + return api.InvalidZID, fmt.Errorf("no application registered: %v", appname) + } + if zid := api.ZettelID(val); zid.IsValid() { + return zid, nil + } + return api.InvalidZID, fmt.Errorf("invalid identifier for application %v: %v", appname, val) +} + +// 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) + if err != nil { + return nil, err + } + defer resp.Body.Close() + switch resp.StatusCode { + case http.StatusOK: + case http.StatusNoContent: + return nil, nil + default: + return nil, statusToError(resp) + } + data, err := io.ReadAll(resp.Body) + return data, err +} Index: go.mod ================================================================== --- go.mod +++ go.mod @@ -1,9 +1,9 @@ module t73f.de/r/zsc -go 1.22 +go 1.23 require ( - t73f.de/r/sx v0.0.0-20240513163553-ec4fcc6539ca - t73f.de/r/sxwebs v0.0.0-20240613142113-66fc5a284245 - t73f.de/r/webs v0.0.0-20240617100047-8730e9917915 + t73f.de/r/sx v0.0.0-20240814083626-4df0ec6454b5 + t73f.de/r/sxwebs v0.0.0-20241031144449-53c3b2ed1a6f + t73f.de/r/webs v0.0.0-20241031141359-cd4f76a622cd ) Index: go.sum ================================================================== --- go.sum +++ go.sum @@ -1,6 +1,6 @@ -t73f.de/r/sx v0.0.0-20240513163553-ec4fcc6539ca h1:vvDqiuUfBLf+t/gpiSyqIFAdvZ7FLigOH38bqMY+v8k= -t73f.de/r/sx v0.0.0-20240513163553-ec4fcc6539ca/go.mod h1:G9pD1j2R6y9ZkPBb81mSnmwaAvTOg7r6jKp/OF7WeFA= -t73f.de/r/sxwebs v0.0.0-20240613142113-66fc5a284245 h1:raE7KUgoGsp2DzXOko9dDXEsSJ/VvoXCDYeICx7i6uo= -t73f.de/r/sxwebs v0.0.0-20240613142113-66fc5a284245/go.mod h1:ErPBVUyE2fOktL/8M7lp/PR93wP/o9RawMajB1uSqj8= -t73f.de/r/webs v0.0.0-20240617100047-8730e9917915 h1:rwUaPBIH3shrUIkmw51f4RyCplsCU+ISZHailsLiHTE= -t73f.de/r/webs v0.0.0-20240617100047-8730e9917915/go.mod h1:UGAAtul0TK5ACeZ6zTS3SX6GqwMFXxlUpHiV8oqNq5w= +t73f.de/r/sx v0.0.0-20240814083626-4df0ec6454b5 h1:ug4hohM6pK28M8Uo0o3+XvjBure2wfEtuCnHVIdqBZY= +t73f.de/r/sx v0.0.0-20240814083626-4df0ec6454b5/go.mod h1:VRvsWoBErPKvMieDMMk1hsh1tb9sA4ijEQWGw/TbtQ0= +t73f.de/r/sxwebs v0.0.0-20241031144449-53c3b2ed1a6f h1:VJ4S7YWy9tCJuFz5MckqUjjktPaf0kpnTkNBVRVXpo4= +t73f.de/r/sxwebs v0.0.0-20241031144449-53c3b2ed1a6f/go.mod h1:IaM+U+LvYTYeuiIS5cwZW6kcEpdwoKBYVCU7LZr4Sgk= +t73f.de/r/webs v0.0.0-20241031141359-cd4f76a622cd h1:+7cqJonXKDso+uPvsvOPl7BiLkhj8VQT/Has8qC5VIQ= +t73f.de/r/webs v0.0.0-20241031141359-cd4f76a622cd/go.mod h1:NSoOON8be62MfQZzlCApK27Jt2zhIa6Vrmo9RJ4tOnQ= Index: input/input.go ================================================================== --- input/input.go +++ input/input.go @@ -95,17 +95,11 @@ } 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 -} +func IsEOLEOS(ch rune) bool { return ch == EOS || ch == '\n' || ch == '\r' } // EatEOL transforms both "\r" and "\r\n" into "\n". func (inp *Input) EatEOL() { switch inp.Ch { case '\r': @@ -124,10 +118,17 @@ if inp.Pos != pos { inp.readPos = pos inp.Next() } } + +// SkipSpace reads while the current character is not a space character. +func (inp *Input) SkipSpace() { + for ch := inp.Ch; IsSpace(ch); { + ch = inp.Next() + } +} // SkipToEOL reads until the next end-of-line. func (inp *Input) SkipToEOL() { for { switch inp.Ch { Index: input/runes.go ================================================================== --- input/runes.go +++ input/runes.go @@ -23,5 +23,8 @@ case '\n', '\r', EOS: return false } return unicode.IsSpace(ch) } + +// IsSpace returns true if current character is a whitespace. +func (inp *Input) IsSpace() bool { return IsSpace(inp.Ch) } Index: maps/maps.go ================================================================== --- maps/maps.go +++ maps/maps.go @@ -9,14 +9,16 @@ // // SPDX-License-Identifier: EUPL-1.2 // SPDX-FileCopyrightText: 2022-present Detlef Stern //----------------------------------------------------------------------------- +// Package maps provides utility functions to work with maps. package maps import "sort" +// Keys returns the sorted list of string keys of the given map. func Keys[T any](m map[string]T) []string { if len(m) == 0 { return nil } result := make([]string, 0, len(m)) Index: sexp/sexp.go ================================================================== --- sexp/sexp.go +++ sexp/sexp.go @@ -33,10 +33,11 @@ sx.MakeList(sx.MakeSymbol("encoding"), sx.MakeString(zettel.Encoding)), sx.MakeList(sx.MakeSymbol("content"), sx.MakeString(zettel.Content)), ) } +// ParseZettel parses an object to contain all needed data for a zettel. func ParseZettel(obj sx.Object) (api.ZettelData, error) { vals, err := ParseList(obj, "ypppp") if err != nil { return api.ZettelData{}, err } @@ -186,11 +187,16 @@ return nil, ErrElementsMissing } return result, nil } +// ErrElementsMissing is returned, +// if ParseList is called with a list smaller than the number of type specifications. var ErrElementsMissing = errors.New("spec contains more data") + +// ErrNoSpec is returned, +// if ParseList if called with a list greater than the number of type specifications. 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) Index: shtml/const.go ================================================================== --- shtml/const.go +++ shtml/const.go @@ -59,10 +59,11 @@ symSUP = sx.MakeSymbol("sup") symTABLE = sx.MakeSymbol("table") symTBODY = sx.MakeSymbol("tbody") symTHEAD = sx.MakeSymbol("thead") symTD = sx.MakeSymbol("td") + symTH = sx.MakeSymbol("th") symTR = sx.MakeSymbol("tr") SymUL = sx.MakeSymbol("ul") ) // Symbols for HTML attribute keys ADDED shtml/lang.go Index: shtml/lang.go ================================================================== --- /dev/null +++ shtml/lang.go @@ -0,0 +1,106 @@ +//----------------------------------------------------------------------------- +// 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 shtml + +import ( + "strings" + + "t73f.de/r/zsc/api" +) + +// LangStack is a stack to store the nesting of "lang" attribute values. +// It is used to generate typographically correct quotes. +type LangStack []string + +// NewLangStack creates a new language stack. +func NewLangStack(lang string) LangStack { + ls := make([]string, 1, 16) + ls[0] = lang + return ls +} + +// Reset restores the language stack to its initial value. +func (ls *LangStack) Reset() { + *ls = (*ls)[0:1] +} + +// Push adds a new language value. +func (ls *LangStack) Push(lang string) { + *ls = append(*ls, lang) +} + +// Pop removes the topmost language value. +func (ls *LangStack) Pop() { + *ls = (*ls)[0 : len(*ls)-1] +} + +// Top returns the topmost language value. +func (ls *LangStack) Top() string { + return (*ls)[len(*ls)-1] +} + +// Dup duplicates the topmost language value. +func (ls *LangStack) Dup() { + *ls = append(*ls, (*ls)[len(*ls)-1]) +} + +// QuoteInfo contains language specific data about quotes. +type QuoteInfo struct { + primLeft, primRight string + secLeft, secRight string + nbsp bool +} + +// GetPrimary returns the primary left and right quote entity. +func (qi *QuoteInfo) GetPrimary() (string, string) { + return qi.primLeft, qi.primRight +} + +// GetSecondary returns the secondary left and right quote entity. +func (qi *QuoteInfo) GetSecondary() (string, string) { + return qi.secLeft, qi.secRight +} + +// GetQuotes returns quotes based on a nesting level. +func (qi *QuoteInfo) GetQuotes(level uint) (string, string) { + if level%2 == 0 { + return qi.GetPrimary() + } + return qi.GetSecondary() +} + +// GetNBSp returns true, if there must be a non-breaking space between the +// quote entities and the quoted text. +func (qi *QuoteInfo) GetNBSp() bool { return qi.nbsp } + +var langQuotes = map[string]*QuoteInfo{ + "": {""", """, """, """, false}, + api.ValueLangEN: {"“", "”", "‘", "’", false}, + "de": {"„", "“", "‚", "‘", false}, + "fr": {"«", "»", "‹", "›", true}, +} + +// GetQuoteInfo returns language specific data about quotes. +func GetQuoteInfo(lang string) *QuoteInfo { + 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[""] +} Index: shtml/shtml.go ================================================================== --- shtml/shtml.go +++ shtml/shtml.go @@ -52,25 +52,25 @@ ev.bindInlines() return ev } // SetUnique sets a prefix to make several HTML ids unique. -func (tr *Evaluator) SetUnique(s string) { tr.unique = s } +func (ev *Evaluator) SetUnique(s string) { ev.unique = s } // IsValidName returns true, if name is a valid symbol name. -func (tr *Evaluator) IsValidName(s string) bool { return s != "" } +func isValidName(s string) bool { return s != "" } // EvaluateAttrbute transforms the given attributes into a HTML s-expression. -func (tr *Evaluator) EvaluateAttrbute(a attrs.Attributes) *sx.Pair { +func EvaluateAttrbute(a attrs.Attributes) *sx.Pair { if len(a) == 0 { return nil } plist := sx.Nil() keys := a.Keys() for i := len(keys) - 1; i >= 0; i-- { key := keys[i] - if key != attrs.DefaultAttribute && tr.IsValidName(key) { + if key != attrs.DefaultAttribute && isValidName(key) { plist = plist.Cons(sx.Cons(sx.MakeSymbol(key), sx.MakeString(a[key]))) } } if plist == nil { return nil @@ -115,11 +115,11 @@ } return result.List(), nil } // Endnotes returns a SHTML object with all collected endnotes. -func (ev *Evaluator) Endnotes(env *Environment) *sx.Pair { +func Endnotes(env *Environment) *sx.Pair { if env.err != nil || len(env.endnotes) == 0 { return nil } var result sx.ListBuilder @@ -153,11 +153,11 @@ } // Environment where sz objects are evaluated to shtml objects type Environment struct { err error - langStack []string + langStack LangStack endnotes []endnoteInfo quoteNesting uint } type endnoteInfo struct { noteID string // link id @@ -166,15 +166,13 @@ 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, + langStack: NewLangStack(lang), endnotes: nil, quoteNesting: 0, } } @@ -181,32 +179,34 @@ // 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.langStack.Reset() env.endnotes = nil env.quoteNesting = 0 } -// PushAttribute adds the current attributes to the environment. +// 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) + env.langStack.Push(value) } else { - env.langStack = append(env.langStack, env.getLanguage()) + env.langStack.Dup() } } -// 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] +// popAttributes removes the current attributes from the envrionment. +func (env *Environment) popAttributes() { env.langStack.Pop() } + +// getLanguage returns the current language. +func (env *Environment) getLanguage() string { return env.langStack.Top() } + +func (env *Environment) getQuotes() (string, string, bool) { + qi := GetQuoteInfo(env.getLanguage()) + leftQ, rightQ := qi.GetQuotes(env.quoteNesting) + return leftQ, rightQ, qi.GetNBSp() } // EvalFn is a function to be called for evaluation. type EvalFn func(sx.Vector, *Environment) sx.Object @@ -237,11 +237,11 @@ 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("name", 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) @@ -261,27 +261,27 @@ 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", getSymbol(args[0], env).GetValue()). Set("content", s) return ev.EvaluateMeta(a) } ev.bind(sz.SymTypeIDSet, 2, evalMetaSet) ev.bind(sz.SymTypeTagSet, 2, evalMetaSet) ev.bind(sz.SymTypeZettelmarkup, 2, func(args sx.Vector, env *Environment) sx.Object { a := make(attrs.Attributes, 2). - Set("name", ev.getSymbol(args[0], env).GetValue()). + Set("name", 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) + return sx.Nil().Cons(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 { @@ -288,36 +288,36 @@ return ev.evalSlice(args, env).Cons(SymP) }) ev.bind(sz.SymHeading, 5, func(args sx.Vector, env *Environment) sx.Object { nLevel := getInt64(args[0], env) if nLevel <= 0 { - env.err = fmt.Errorf("%v is a negative level", nLevel) + env.err = fmt.Errorf("%v is a negative heading level", nLevel) return sx.Nil() } level := strconv.FormatInt(nLevel+ev.headingOffset, 10) headingSymbol := sx.MakeSymbol("h" + level) - a := ev.GetAttributes(args[1], env) + a := GetAttributes(args[1], env) env.pushAttributes(a) defer env.popAttributes() if fragment := getString(args[3], env).GetValue(); fragment != "" { a = a.Set("id", ev.unique+fragment) } if result, _ := ev.EvaluateList(args[4:], env); result != nil { if len(a) > 0 { - result = result.Cons(ev.EvaluateAttrbute(a)) + result = result.Cons(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))) + result = result.Cons(EvaluateAttrbute(sz.GetAttributes(attrList))) } } return result.Cons(SymHR) }) @@ -362,18 +362,18 @@ }) 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) + thead = sx.Nil().Cons(ev.evalTableRow(symTH, 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)) + tbody.Add(ev.evalTableRow(symTD, getList(row, env), env)) } } table := sx.Nil() if !tbody.IsEmpty() { @@ -395,37 +395,37 @@ 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 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)) + return evalVerbatim(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)) + return evalVerbatim(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) + a := GetAttributes(args[0], env) content := getString(args[1], env) if a.HasDefault() { content = sx.MakeString(visibleReplacer.Replace(content.GetValue())) } - return ev.evalVerbatim(a, content) + return 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)) + return 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() @@ -433,13 +433,13 @@ 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) + if refSym, isRefSym := sx.GetSymbol(refKind); isRefSym && refSym.IsEqualSymbol(sz.SymRefStateExternal) { + a := GetAttributes(args[0], env).Set("src", refValue.GetValue()).AddClass("external") + return sx.Nil().Cons(sx.Nil().Cons(EvaluateAttrbute(a)).Cons(SymIMG)).Cons(SymP) } return sx.MakeList( sxhtml.SymInlineComment, sx.MakeString("transclude"), refKind, @@ -473,34 +473,34 @@ result.Add(elem) } return result.List() } -func (ev *Evaluator) evalTableRow(pairs *sx.Pair, env *Environment) *sx.Pair { +func (ev *Evaluator) evalTableRow(sym *sx.Symbol, 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)) + row.Add(sx.Cons(sym, 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})) + tdata = tdata.Cons(EvaluateAttrbute(attrs.Attributes{"class": align})) } - return tdata.Cons(symTD) + return tdata } } 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) + a := GetAttributes(args[0], env) env.pushAttributes(a) defer env.popAttributes() if genericToClass { if val, found := a.Get(""); found { a = a.Remove("").AddClass(val) @@ -507,11 +507,11 @@ } } var result sx.ListBuilder result.Add(sym) if len(a) > 0 { - result.Add(ev.EvaluateAttrbute(a)) + result.Add(EvaluateAttrbute(a)) } if region, isPair := sx.GetPair(args[1]); isPair { if evalRegion := ev.EvalPairList(region, env); evalRegion != nil { result.ExtendBang(evalRegion) } @@ -523,14 +523,14 @@ } return result.List() } } -func (ev *Evaluator) evalVerbatim(a attrs.Attributes, s sx.String) sx.Object { +func evalVerbatim(a attrs.Attributes, s sx.String) sx.Object { a = setProgLang(a) code := sx.Nil().Cons(s) - if al := ev.EvaluateAttrbute(a); al != nil { + if al := EvaluateAttrbute(a); al != nil { code = code.Cons(al) } code = code.Cons(symCODE) return sx.Nil().Cons(code).Cons(symPRE) } @@ -540,11 +540,11 @@ ev.bind(sz.SymText, 1, func(args sx.Vector, env *Environment) sx.Object { return getString(args[0], env) }) 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) + a := GetAttributes(args[0], env) env.pushAttributes(a) defer env.popAttributes() var inline *sx.Pair if len(args) > 2 { inline = ev.evalSlice(args[2:], env) @@ -553,87 +553,72 @@ 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) + a := 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) + a := 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) + a := 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) + a := 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) + return ev.evalLink(a.Set("href", refValue.GetValue()).Add("rel", "external"), refValue, args[2:], env) }) ev.bind(sz.SymEmbed, 3, func(args sx.Vector, env *Environment) sx.Object { ref := getList(args[1], env) - syntax := getString(args[2], env).GetValue() - if syntax == api.ValueSyntaxSVG { - embedAttr := sx.MakeList( - sxhtml.SymAttr, - sx.Cons(SymAttrType, sx.MakeString("image/svg+xml")), - sx.Cons(SymAttrSrc, sx.MakeString("/"+getString(ref.Tail(), env).GetValue()+".svg")), - ) - return sx.MakeList( - SymFIGURE, - sx.MakeList( - SymEMBED, - embedAttr, - ), - ) - } - a := ev.GetAttributes(args[0], env) + a := 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)) + return sx.MakeList(SymIMG, 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) + a, syntax, data := GetAttributes(args[0], env), getString(args[1], env), getString(args[2], env) summary, hasSummary := a.Get(api.KeySummary) if !hasSummary { summary = "" } - return ev.evalBLOB( + return evalBLOB( sx.MakeList(sxhtml.SymListSplice, sx.MakeString(summary)), syntax, data, ) }) ev.bind(sz.SymCite, 2, func(args sx.Vector, env *Environment) sx.Object { - a := ev.GetAttributes(args[0], env) + a := 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 { @@ -640,11 +625,11 @@ result = ev.evalSlice(args[2:], env).Cons(sx.MakeString(", ")) } result = result.Cons(key) } if len(a) > 0 { - result = result.Cons(ev.EvaluateAttrbute(a)) + result = result.Cons(EvaluateAttrbute(a)) } if result == nil { return nil } return result.Cons(SymSPAN) @@ -652,22 +637,22 @@ 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(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) + a := GetAttributes(args[0], env) env.pushAttributes(a) defer env.popAttributes() attrPlist := sx.Nil() if len(a) > 0 { - if attrs := ev.EvaluateAttrbute(a); attrs != nil { + if attrs := EvaluateAttrbute(a); attrs != nil { attrPlist = attrs.Tail() } } noteNum := strconv.Itoa(len(env.endnotes) + 1) @@ -692,11 +677,11 @@ 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 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) } } @@ -703,124 +688,90 @@ } 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) + return 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) + a := GetAttributes(args[0], env).AddClass("zs-math") + return 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) + return 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) + return 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) + a := 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)) + res = res.Cons(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) + a := 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) + leftQ, rightQ, withNbsp := env.getQuotes() 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 { + if withNbsp { 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)) + res = res.Cons(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 { +func evalLiteral(args sx.Vector, a attrs.Attributes, sym *sx.Symbol, env *Environment) sx.Object { if a == nil { - a = ev.GetAttributes(args[0], env) + a = 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)) + res = res.Cons(EvaluateAttrbute(a)) } return res.Cons(sym) } func setProgLang(a attrs.Attributes) attrs.Attributes { if val, found := a.Get(""); found { @@ -834,11 +785,11 @@ return sx.Nil().Cons(s).Cons(sxhtml.SymNoEscape) } return nil } -func (ev *Evaluator) evalBLOB(description *sx.Pair, syntax, data sx.String) sx.Object { +func evalBLOB(description *sx.Pair, syntax, data sx.String) sx.Object { if data.GetValue() == "" { return sx.Nil() } switch syntax.GetValue() { case "": @@ -927,11 +878,11 @@ return result.List() } return nil } -// EvaluatePairList evaluates a list of lists. +// EvalPairList 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) @@ -948,14 +899,14 @@ result = sx.Nil().Cons(refValue) } if ev.noLinks { return result.Cons(SymSPAN) } - return result.Cons(ev.EvaluateAttrbute(a)).Cons(SymA) + return result.Cons(EvaluateAttrbute(a)).Cons(SymA) } -func (ev *Evaluator) getSymbol(obj sx.Object, env *Environment) *sx.Symbol { +func getSymbol(obj sx.Object, env *Environment) *sx.Symbol { if env.err == nil { if sym, ok := sx.GetSymbol(obj); ok { return sym } env.err = fmt.Errorf("%v/%T is not a symbol", obj, obj) @@ -992,11 +943,11 @@ 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 { +func GetAttributes(arg sx.Object, env *Environment) attrs.Attributes { return sz.GetAttributes(getList(arg, env)) } var unsafeSnippets = []string{ " 0 { return result } return nil @@ -109,17 +117,20 @@ result.Key = keySym.GetValue() result.Value = next.Car() return result, true } +// GetString return the metadata string value associated with the given key. func (m Meta) GetString(key string) string { if v, found := m[key]; found { return GoValue(v.Value) } return "" } +// GetPair return the metadata value associated with the given key, +// as a list of objects. func (m Meta) GetPair(key string) *sx.Pair { if mv, found := m[key]; found { if pair, isPair := sx.GetPair(mv.Value); isPair { return pair } Index: sz/walk.go ================================================================== --- sz/walk.go +++ sz/walk.go @@ -64,11 +64,11 @@ SymLinkHosted: walkChildrenInlines4, SymLinkInvalid: walkChildrenInlines4, SymLinkQuery: walkChildrenInlines4, SymLinkSelf: walkChildrenInlines4, SymLinkZettel: walkChildrenInlines4, - SymEmbed: walkChildrenInlines4, + SymEmbed: walkChildrenEmbed, SymCite: walkChildrenInlines4, SymFormatDelete: walkChildrenInlines3, SymFormatEmph: walkChildrenInlines3, SymFormatInsert: walkChildrenInlines3, SymFormatMark: walkChildrenInlines3, @@ -163,13 +163,11 @@ } return dn } func walkChildrenTable(v Visitor, tn *sx.Pair, env *sx.Pair) *sx.Pair { - header := tn.Tail() - header.SetCar(walkChildrenList(v, header.Tail(), env)) - for row := header.Tail(); row != nil; row = row.Tail() { + for row := tn.Tail(); row != nil; row = row.Tail() { row.SetCar(walkChildrenList(v, row.Head(), env)) } return tn } @@ -182,10 +180,26 @@ next = next.Tail() // fragment := next.Car() next.SetCdr(walkChildrenList(v, next.Tail(), env)) return mn } + +func walkChildrenEmbed(v Visitor, en *sx.Pair, env *sx.Pair) *sx.Pair { + // sym := en.Car() + next := en.Tail() + // attr := next.Car() + next = next.Tail() + // ref := next.Car() + next = next.Tail() + // syntax := next.Car() + next = next.Tail() + if next != nil { + // text := next.Car() + next.SetCar(Walk(v, next.Head(), env)) + } + return en +} func walkChildrenInlines4(v Visitor, ln *sx.Pair, env *sx.Pair) *sx.Pair { // sym := ln.Car() next := ln.Tail() // attrs := next.Car() Index: sz/zmk/block.go ================================================================== --- sz/zmk/block.go +++ sz/zmk/block.go @@ -272,15 +272,16 @@ } } // parseRegionLastLine parses the last line of a region and returns its inline text. func (cp *zmkP) parseRegionLastLine() *sx.Pair { + inp := cp.inp cp.clearStacked() // remove any lists defined in the region - cp.skipSpace() + inp.SkipSpace() var region sx.ListBuilder for { - switch cp.inp.Ch { + switch inp.Ch { case input.EOS, '\n', '\r': return region.List() } in := cp.parseInline() if in == nil { @@ -299,11 +300,11 @@ } if inp.Ch != ' ' { return nil, false } inp.Next() - cp.skipSpace() + inp.SkipSpace() if delims > 7 { delims = 7 } level := int64(delims - 2) var attrs *sx.Pair @@ -349,12 +350,13 @@ func (cp *zmkP) parseNestedList() (res *sx.Pair, success bool) { kinds := cp.parseNestedListKinds() if len(kinds) == 0 { return nil, false } - cp.skipSpace() - if !kinds[len(kinds)-1].IsEqual(sz.SymListQuote) && input.IsEOLEOS(cp.inp.Ch) { + inp := cp.inp + inp.SkipSpace() + if !kinds[len(kinds)-1].IsEqual(sz.SymListQuote) && input.IsEOLEOS(inp.Ch) { return nil, false } if len(kinds) < len(cp.lists) { cp.lists = cp.lists[:len(kinds)] @@ -445,11 +447,11 @@ inp := cp.inp if inp.Next() != ' ' { return nil, false } inp.Next() - cp.skipSpace() + inp.SkipSpace() descrl := cp.descrl if descrl == nil { descrl = sx.Cons(sz.SymDescription, nil) cp.descrl = descrl res = descrl @@ -484,11 +486,11 @@ inp := cp.inp if inp.Next() != ' ' { return nil, false } inp.Next() - cp.skipSpace() + inp.SkipSpace() descrl := cp.descrl lastPair, pos := lastPairPos(descrl) if descrl == nil || pos <= 0 { // No term given return nil, false Index: sz/zmk/inline.go ================================================================== --- sz/zmk/inline.go +++ sz/zmk/inline.go @@ -148,11 +148,11 @@ } func (cp *zmkP) parseReference(openCh, closeCh rune) (ref string, text *sx.Pair, _ bool) { inp := cp.inp inp.Next() - cp.skipSpace() + inp.SkipSpace() if inp.Ch == openCh { // Additional opening chars result in a fail return "", nil, false } var is sx.Vector @@ -182,11 +182,11 @@ } inp.SetPos(pos) } } - cp.skipSpace() + inp.SkipSpace() pos = inp.Pos if !cp.readReferenceToClose(closeCh) { return "", nil, false } ref = strings.TrimSpace(string(inp.Src[pos:inp.Pos])) @@ -328,13 +328,13 @@ // Problematisch ist, dass hier noch nicht mn.Fragment und mn.Slug gesetzt werden. // Evtl. muss es ein PreMark-Symbol geben } func (cp *zmkP) parseLinkLikeRest() (*sx.Pair, bool) { - cp.skipSpace() var ins sx.Vector inp := cp.inp + inp.SkipSpace() for inp.Ch != ']' { in := cp.parseInline() if in == nil { return nil, false } @@ -357,11 +357,11 @@ } for inp.Ch == '%' { inp.Next() } attrs := cp.parseInlineAttributes() - cp.skipSpace() + inp.SkipSpace() pos := inp.Pos for { if input.IsEOLEOS(inp.Ch) { return sx.MakeList( sz.SymLiteralComment, @@ -459,11 +459,11 @@ } } } func createLiteralNode(sym *sx.Symbol, attrs *sx.Pair, content string) *sx.Pair { - if sym.IsEqual(sz.SymLiteralZettel) { + if sym.IsEqualSymbol(sz.SymLiteralZettel) { if p := attrs.Assoc(sx.MakeString("")); p != nil { if val, isString := sx.GetString(p.Cdr()); isString && val.GetValue() == api.ValueSyntaxHTML { sym = sz.SymLiteralHTML attrs = attrs.RemoveAssoc(sx.MakeString("")) } Index: sz/zmk/post-processor.go ================================================================== --- sz/zmk/post-processor.go +++ sz/zmk/post-processor.go @@ -322,11 +322,11 @@ elem = cellTail.Head() if elem.Car().IsEqual(sz.SymText) { if s, isString := sx.GetString(elem.Tail().Car()); isString && s.GetValue() != "" { str := s.GetValue() cellAlign := getCellAlignment(str[len(str)-1]) - if !cellAlign.IsEqual(sz.SymCell) { + if !cellAlign.IsEqualSymbol(sz.SymCell) { elem.SetCdr(sx.Cons(sx.MakeString(str[0:len(str)-1]), nil)) } align[cellCount-1] = cellAlign cell.SetCar(cellAlign) } @@ -368,11 +368,11 @@ elem := cellTail.Head() if elem.Car().IsEqual(sz.SymText) { if s, isString := sx.GetString(elem.Tail().Car()); isString && s.GetValue() != "" { str := s.GetValue() cellAlign := getCellAlignment(str[0]) - if !cellAlign.IsEqual(sz.SymCell) { + if !cellAlign.IsEqualSymbol(sz.SymCell) { elem.SetCdr(sx.Cons(sx.MakeString(str[1:]), nil)) cell.SetCar(cellAlign) } } } Index: sz/zmk/ref.go ================================================================== --- sz/zmk/ref.go +++ sz/zmk/ref.go @@ -8,10 +8,11 @@ // and obligations under this license. // // SPDX-License-Identifier: EUPL-1.2 // SPDX-FileCopyrightText: 2020-present Detlef Stern // ----------------------------------------------------------------------------- + package zmk import ( "net/url" "strings" @@ -28,11 +29,11 @@ } if strings.HasPrefix(s, api.QueryPrefix) { return makePairRef(sz.SymRefStateQuery, s[len(api.QueryPrefix):]) } if state, ok := localState(s); ok { - if state.IsEqual(sz.SymRefStateBased) { + if state.IsEqualSymbol(sz.SymRefStateBased) { s = s[1:] } _, err := url.Parse(s) if err == nil { return makePairRef(state, s) Index: sz/zmk/zmk.go ================================================================== --- sz/zmk/zmk.go +++ sz/zmk/zmk.go @@ -22,10 +22,11 @@ "t73f.de/r/sx" "t73f.de/r/zsc/input" "t73f.de/r/zsc/sz" ) +// ParseBlocks tries to parse the input as a block element. func ParseBlocks(inp *input.Input) *sx.Pair { parser := zmkP{inp: inp} var lastPara *sx.Pair var blkBuild sx.ListBuilder @@ -52,10 +53,11 @@ return bs.Cons(sz.SymBlock) } return nil } +// ParseInlines tries to parse the input as an inline element. func ParseInlines(inp *input.Input) *sx.Pair { parser := zmkP{inp: inp} var ins sx.Vector for inp.Ch != input.EOS { in := parser.parseInline() @@ -109,11 +111,11 @@ names := make([]string, 0, len(attrs)) for n := range attrs { names = append(names, n) } slices.Sort(names) - var assoc *sx.Pair = nil + var assoc *sx.Pair for i := len(names) - 1; i >= 0; i-- { n := names[i] assoc = assoc.Cons(sx.Cons(sx.MakeString(n), sx.MakeString(attrs[n]))) } return assoc @@ -189,11 +191,11 @@ if pos < inp.Pos { return attrMap{"": string(inp.Src[pos:inp.Pos])}.asPairAssoc() } // No immediate name: skip spaces - cp.skipSpace() + inp.SkipSpace() return cp.parseInlineAttributes() } func (cp *zmkP) parseInlineAttributes() *sx.Pair { inp := cp.inp @@ -273,14 +275,8 @@ return } } } -func (cp *zmkP) skipSpace() { - for inp := cp.inp; inp.Ch == ' '; { - inp.Next() - } -} - func isNameRune(ch rune) bool { return unicode.IsLetter(ch) || unicode.IsDigit(ch) || ch == '-' || ch == '_' } Index: sz/zmk/zmk_test.go ================================================================== --- sz/zmk/zmk_test.go +++ sz/zmk/zmk_test.go @@ -52,27 +52,32 @@ for tcn, tc := range tcs { t.Run(fmt.Sprintf("TC=%02d,src=%q", tcn, tc.source), func(st *testing.T) { st.Helper() ast := parseInput(tc.source, isBlock) + sz.Walk(astWalker{}, ast, nil) got := ast.String() if tc.want != got { st.Errorf("\nwant=%q\n got=%q", tc.want, got) } }) } } -func parseInput(src string, asBlock bool) sx.Sequence { +func parseInput(src string, asBlock bool) *sx.Pair { inp := input.NewInput([]byte(src)) if asBlock { bl := zmk.ParseBlocks(inp) return bl } il := zmk.ParseInlines(inp) return il } +type astWalker struct{} + +func (astWalker) Visit(node *sx.Pair, env *sx.Pair) sx.Object { return sx.MakeBoolean(true) } + func TestEOL(t *testing.T) { t.Parallel() for _, isBlock := range []bool{true, false} { checkTcs(t, isBlock, TestCases{ {"", "()"}, Index: text/text.go ================================================================== --- text/text.go +++ text/text.go @@ -25,17 +25,19 @@ // Encoder is the structure to hold relevant data to execute the encoding. type Encoder struct { sb strings.Builder } +// NewEncoder returns a new text encoder. func NewEncoder() *Encoder { enc := &Encoder{ sb: strings.Builder{}, } return enc } +// Encode the object list as a string. func (enc *Encoder) Encode(lst *sx.Pair) string { enc.executeList(lst) result := enc.sb.String() enc.sb.Reset() return result Index: www/changes.wiki ================================================================== --- www/changes.wiki +++ www/changes.wiki @@ -1,9 +1,17 @@ Change Log + +

Changes for Version 0.20.0 (pending)

+ -

Changes for Version 0.19.0 (pending)

+

Changes for Version 0.19.0 (2024-12-13)

+ * Remove support for rename operation; removed all associated constants + * Make quote handling in shtml public, to be used by other encoders + * shtml generates external links with rel attribute + * Add some input handling methods + * Enhance docs for api/client

Changes for Version 0.18.0 (2024-07-11)

* Add client method GetApplicationZid to retrieve the zettel identifier of an configuration zettel for a specific application. Index: www/index.wiki ================================================================== --- www/index.wiki +++ www/index.wiki @@ -1,29 +1,29 @@ Home This repository contains Go client software to access [https://zettelstore.de|Zettelstore] via its API. -

Latest Release: 0.18.0 (2024-07-11)

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

Latest Release: 0.19.0 (2024-12-13)

+ * [./changes.wiki#0_19|Change summary] + * [/timeline?p=v0.19.0&bt=v0.18.0&y=ci|Check-ins for version 0.19], + [/vdiff?to=v0.19.0&from=v0.18.0|content diff] + * [/timeline?df=v0.19.0&y=ci|Check-ins derived from the 0.19 release], + [/vdiff?from=v0.19.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. +First, you must install the [https://fossil-scm.org|Fossil] version control +system. In too many use cases it is a superior solution compared to Git, 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