DELETED .github/dependabot.yml Index: .github/dependabot.yml ================================================================== --- .github/dependabot.yml +++ /dev/null @@ -1,12 +0,0 @@ -# To get started with Dependabot version updates, you'll need to specify which -# package ecosystems to update and where the package manifests are located. -# Please see the documentation for all configuration options: -# https://help.github.com/github/administering-a-repository/configuration-options-for-dependency-updates - -version: 2 -updates: - - package-ecosystem: "gomod" # See documentation for possible values - directory: "/" # Location of package manifests - schedule: - interval: "daily" - rebase-strategy: "disabled" Index: api/api.go ================================================================== --- api/api.go +++ api/api.go @@ -4,36 +4,39 @@ // This file is part of zettelstore-client. // // Zettelstore client is licensed under the latest version of the EUPL // (European Union Public License). Please see file LICENSE.txt for your rights // and obligations under this license. +// +// SPDX-License-Identifier: EUPL-1.2 +// SPDX-FileCopyrightText: 2021-present Detlef Stern //----------------------------------------------------------------------------- // Package api contains common definitions used for client and server. package api // ZettelID contains the identifier of a zettel. It is a string with 14 digits. type ZettelID string -// InvalidZID is an invalif zettel identifier +// InvalidZID is an invalid zettel identifier const InvalidZID = "" // IsValid returns true, if the idenfifier contains 14 digits. func (zid ZettelID) IsValid() bool { if len(zid) != 14 { return false } - for i := 0; i < 14; i++ { + for i := range 14 { ch := zid[i] if ch < '0' || '9' < ch { return false } } 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 @@ -41,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. @@ -60,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 @@ -4,70 +4,106 @@ // This file is part of zettelstore-client. // // Zettelstore client is licensed under the latest version of the EUPL // (European Union Public License). Please see file LICENSE.txt for your rights // and obligations under this license. +// +// SPDX-License-Identifier: EUPL-1.2 +// SPDX-FileCopyrightText: 2021-present Detlef Stern //----------------------------------------------------------------------------- package api import "fmt" -// 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") 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") ZidLoginTemplate = ZettelID("00000000010200") ZidListTemplate = ZettelID("00000000010300") ZidZettelTemplate = ZettelID("00000000010401") ZidInfoTemplate = ZettelID("00000000010402") ZidFormTemplate = ZettelID("00000000010403") - ZidRenameTemplate = ZettelID("00000000010404") ZidDeleteTemplate = ZettelID("00000000010405") ZidErrorTemplate = ZettelID("00000000010700") // WebUI sxn code zettel are in the range 19000..19999 - ZidSxnTemplate = ZettelID("00000000019100") + ZidSxnStart = ZettelID("00000000019000") + ZidSxnBase = ZettelID("00000000019990") // CSS-related zettel are in the range 20000..29999 - ZidBaseCSS = ZettelID("00000000020001") - ZidUserCSS = ZettelID("00000000025001") - ZidRoleCSSMap = ZettelID("00000000029000") // Maps roles to CSS zettel, which should be in the range 29001..29999. + ZidBaseCSS = ZettelID("00000000020001") + ZidUserCSS = ZettelID("00000000025001") // WebUI JS zettel are in the range 30000..39999 // WebUI image zettel are in the range 40000..49999 ZidEmoji = ZettelID("00000000040001") // Other sxn code zettel are in the range 50000..59999 + ZidSxnPrelude = ZettelID("00000000059900") + + // Predefined Zettelmarkup zettel are in the range 60000..69999 + ZidRoleZettelZettel = ZettelID("00000000060010") + ZidRoleConfigurationZettel = ZettelID("00000000060020") + ZidRoleRoleZettel = ZettelID("00000000060030") + ZidRoleTagZettel = ZettelID("00000000060040") + + // Range 80000...89999 is reserved for web ui menus + ZidTOCListsMenu = ZettelID("00000000080001") // "Lists" menu // Range 90000...99999 is reserved for zettel templates 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" @@ -75,15 +111,18 @@ MetaString = "String" MetaTagSet = "TagSet" MetaTimestamp = "Timestamp" MetaURL = "URL" MetaWord = "Word" - MetaWordSet = "WordSet" 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" @@ -103,42 +142,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" - 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" @@ -148,12 +191,10 @@ ValueVisibilityPublic = "public" ) // Additional HTTP constants. const ( - MethodMove = "MOVE" // HTTP method for renaming a zettel - HeaderAccept = "Accept" HeaderContentType = "Content-Type" HeaderDestination = "Destination" HeaderLocation = "Location" ) @@ -164,25 +205,26 @@ QueryKeyEncoding = "enc" QueryKeyParseOnly = "parseonly" QueryKeyPart = "part" QueryKeyPhrase = "phrase" QueryKeyQuery = "q" + QueryKeyRole = "role" QueryKeySeed = "_seed" + QueryKeyTag = "tag" ) // Supported encoding values. const ( - EncodingHTML = "html" - EncodingMD = "md" - EncodingSHTML = "shtml" - EncodingSz = "sz" - EncodingText = "text" - EncodingZMK = "zmk" - - EncodingPlain = "plain" - EncodingData = "data" - EncodingJson = "json" + 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, @@ -191,11 +233,10 @@ EncodingText: EncoderText, EncodingZMK: EncoderZmk, EncodingPlain: EncoderPlain, EncodingData: EncoderData, - EncodingJson: EncoderJson, } var mapEnumEncoding = map[EncodingEnum]string{} func init() { for k, v := range mapEncodingEnum { @@ -224,11 +265,10 @@ EncoderText EncoderZmk EncoderPlain EncoderData - EncoderJson ) // String representation of an encoder key. func (e EncodingEnum) String() string { if f, ok := mapEnumEncoding[e]; ok { @@ -245,51 +285,62 @@ ) // 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 +// Supported search operator representations. const ( - BackwardDirective = "BACKWARD" - ContextDirective = "CONTEXT" - CostDirective = "COST" - ForwardDirective = "FORWARD" - 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 = "|" - - ExistOperator = "?" - ExistNotOperator = "!?" + 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 + + 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. + + 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 @@ -4,148 +4,84 @@ // This file is part of zettelstore-client. // // Zettelstore client is licensed under the latest version of the EUPL // (European Union Public License). Please see file LICENSE.txt for your rights // and obligations under this license. +// +// SPDX-License-Identifier: EUPL-1.2 +// SPDX-FileCopyrightText: 2020-present Detlef Stern //----------------------------------------------------------------------------- package api -import ( - "net/url" - "strings" -) - -type urlQuery struct{ key, val string } +import "t73f.de/r/webs/urlbuilder" // URLBuilder should be used to create zettelstore URLs. type URLBuilder struct { - prefix string - key byte - rawLocal string - path []string - query []urlQuery - fragment string + base urlbuilder.URLBuilder + prefix string } // NewURLBuilder creates a new URL builder with the given prefix and key. func NewURLBuilder(prefix string, key byte) *URLBuilder { - return &URLBuilder{prefix: prefix, key: key} + for len(prefix) > 0 && prefix[len(prefix)-1] == '/' { + prefix = prefix[0 : len(prefix)-1] + } + result := URLBuilder{prefix: prefix} + if key != '/' { + result.base.AddPath(string([]byte{key})) + } + return &result } -// Clone an URLBuilder +// Clone an URLBuilder. func (ub *URLBuilder) Clone() *URLBuilder { cpy := new(URLBuilder) - cpy.key = ub.key - if len(ub.path) > 0 { - cpy.path = make([]string, 0, len(ub.path)) - cpy.path = append(cpy.path, ub.path...) - } - if len(ub.query) > 0 { - cpy.query = make([]urlQuery, 0, len(ub.query)) - cpy.query = append(cpy.query, ub.query...) - } - cpy.fragment = ub.fragment + ub.base.Copy(&cpy.base) + cpy.prefix = ub.prefix return cpy } -// SetRawLocal sets everything that follows the prefix / key. -func (ub *URLBuilder) SetRawLocal(rawLocal string) *URLBuilder { - for len(rawLocal) > 0 && rawLocal[0] == '/' { - rawLocal = rawLocal[1:] - } - ub.rawLocal = rawLocal - ub.path = nil - ub.query = nil - ub.fragment = "" - return ub -} - // SetZid sets the zettel identifier. func (ub *URLBuilder) SetZid(zid ZettelID) *URLBuilder { - if len(ub.path) > 0 { - panic("Cannot add Zid") - } - ub.rawLocal = "" - ub.path = append(ub.path, string(zid)) - return ub -} - -// AppendPath adds a new path element -func (ub *URLBuilder) AppendPath(p string) *URLBuilder { - ub.rawLocal = "" - for len(p) > 0 && p[0] == '/' { - p = p[1:] - } - if p != "" { - ub.path = append(ub.path, p) - } - return ub -} - -// AppendKVQuery adds a new key/value query parameter -func (ub *URLBuilder) AppendKVQuery(key, value string) *URLBuilder { - ub.rawLocal = "" - ub.query = append(ub.query, urlQuery{key, value}) - return ub -} - -// AppendQuery adds a new query -func (ub *URLBuilder) AppendQuery(value string) *URLBuilder { - if value != "" { - ub.rawLocal = "" - ub.query = append(ub.query, urlQuery{QueryKeyQuery, value}) + ub.base.AddPath(string(zid)) + return ub +} + +// 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. +func (ub *URLBuilder) AppendKVQuery(key, value string) *URLBuilder { + ub.base.AddQuery(key, value) + return ub +} + +// 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 } // ClearQuery removes all query parameters. func (ub *URLBuilder) ClearQuery() *URLBuilder { - ub.rawLocal = "" - ub.query = nil - ub.fragment = "" + ub.base.RemoveQueries() return ub } -// SetFragment stores the fragment +// SetFragment sets the fragment. func (ub *URLBuilder) SetFragment(s string) *URLBuilder { - ub.rawLocal = "" - ub.fragment = s + ub.base.SetFragment(s) return ub } // String produces a string value. func (ub *URLBuilder) String() string { - var sb strings.Builder - - sb.WriteString(ub.prefix) - if ub.key != '/' { - sb.WriteByte(ub.key) - } - if ub.rawLocal != "" { - sb.WriteString(ub.rawLocal) - return sb.String() - } - for i, p := range ub.path { - if i > 0 || ub.key != '/' { - sb.WriteByte('/') - } - sb.WriteString(url.PathEscape(p)) - } - if len(ub.fragment) > 0 { - sb.WriteByte('#') - sb.WriteString(ub.fragment) - } - for i, q := range ub.query { - if i == 0 { - sb.WriteByte('?') - } else { - sb.WriteByte('&') - } - sb.WriteString(q.key) - if val := q.val; val != "" { - sb.WriteByte('=') - sb.WriteString(url.QueryEscape(val)) - } - } - return sb.String() + return ub.prefix + ub.base.String() } Index: attrs/attrs.go ================================================================== --- attrs/attrs.go +++ attrs/attrs.go @@ -4,19 +4,23 @@ // This file is part of zettelstore-client. // // Zettelstore client is licensed under the latest version of the EUPL // (European Union Public License). Please see file LICENSE.txt for your rights // and obligations under this license. +// +// SPDX-License-Identifier: EUPL-1.2 +// SPDX-FileCopyrightText: 2020-present Detlef Stern //----------------------------------------------------------------------------- // Package attrs stores attributes of zettel parts. package attrs import ( + "slices" "strings" - "zettelstore.de/client.fossil/maps" + "t73f.de/r/zsc/maps" ) // Attributes store additional information about some node types. type Attributes map[string]string @@ -82,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 @@ -4,18 +4,21 @@ // This file is part of zettelstore-client. // // Zettelstore client is licensed under the latest version of the EUPL // (European Union Public License). Please see file LICENSE.txt for your rights // and obligations under this license. +// +// SPDX-License-Identifier: EUPL-1.2 +// SPDX-FileCopyrightText: 2020-present Detlef Stern //----------------------------------------------------------------------------- package attrs_test import ( "testing" - "zettelstore.de/client.fossil/attrs" + "t73f.de/r/zsc/attrs" ) func TestHasDefault(t *testing.T) { t.Parallel() attr := attrs.Attributes{} @@ -52,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 @@ -4,17 +4,19 @@ // This file is part of zettelstore-client. // // Zettelstore client is licensed under the latest version of the EUPL // (European Union Public License). Please see file LICENSE.txt for your rights // and obligations under this license. +// +// SPDX-License-Identifier: EUPL-1.2 +// SPDX-FileCopyrightText: 2021-present Detlef Stern //----------------------------------------------------------------------------- // Package client provides a client for accessing the Zettelstore via its API. package client import ( - "bufio" "bytes" "context" "fmt" "io" "net" @@ -22,14 +24,14 @@ "net/url" "strconv" "strings" "time" - "zettelstore.de/client.fossil/api" - "zettelstore.de/client.fossil/sexp" - "zettelstore.de/sx.fossil" - "zettelstore.de/sx.fossil/sxreader" + "t73f.de/r/sx" + "t73f.de/r/sx/sxreader" + "t73f.de/r/zsc/api" + "t73f.de/r/zsc/sexp" ) // Client contains all data to execute requests. type Client struct { base string @@ -42,22 +44,19 @@ } // 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 = "" myURL.Fragment = "" myURL.RawFragment = "" base := myURL.String() - if !strings.HasSuffix(base, "/") { - base += "/" - } c := Client{ base: base, client: http.Client{ Timeout: 10 * time.Second, Transport: &http.Transport{ @@ -68,18 +67,36 @@ }, }, } return &c } + +// AllowRedirect will modify the client to not follow redirect status code when +// 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(*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 { @@ -110,11 +127,17 @@ Message: resp.Status[4:], Body: body, } } -func (c *Client) newURLBuilder(key byte) *api.URLBuilder { +// NewURLBuilder creates a new URL builder for the client with the given key. +// +// 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) } @@ -124,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 = "" @@ -174,16 +200,16 @@ } vals, err := sexp.ParseList(obj, "ssi") if err != nil { return err } - token := vals[1].(sx.String).String() + token := vals[1].(sx.String).GetValue() if len(token) < 4 { return fmt.Errorf("no valid token found: %q", token) } c.token = token - c.tokenType = vals[0].(sx.String).String() + c.tokenType = vals[0].(sx.String).GetValue() c.expires = time.Now().Add(time.Duration(vals[2].(sx.Int64)*9/10) * time.Second) return nil } func (c *Client) updateToken(ctx context.Context) error { @@ -195,33 +221,41 @@ } 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())) + req, err := c.newRequest(ctx, http.MethodPost, c.NewURLBuilder('a'), strings.NewReader(authData.Encode())) if err != nil { return err } req.Header.Set("Content-Type", "application/x-www-form-urlencoded") return c.executeAuthRequest(req) } // RefreshToken updates the access token +// +// [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) + 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) + ub := c.NewURLBuilder('z') + 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 { @@ -236,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) + ub := c.NewURLBuilder('z').AppendKVQuery(api.QueryKeyEncoding, api.EncodingData) + 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) @@ -258,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 qVals[1].String(), hVals[1].String(), 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) } @@ -391,184 +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, WordSet, or 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 -} - -// 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, sf sx.SymbolFactory) (sx.Object, error) { - return c.getSz(ctx, zid, part, true, sf) -} - -// GetEvaluatedSz returns an evaluated zettel as a Sexpr-decoded data structure. -func (c *Client) GetEvaluatedSz(ctx context.Context, zid api.ZettelID, part string, sf sx.SymbolFactory) (sx.Object, error) { - return c.getSz(ctx, zid, part, false, sf) -} - -func (c *Client) getSz(ctx context.Context, zid api.ZettelID, part string, parseOnly bool, sf sx.SymbolFactory) (sx.Object, error) { - ub := c.newURLBuilder('z').SetZid(zid) - 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), sxreader.WithSymbolFactory(sf)).Read() -} - -// GetMetaData returns the metadata of a zettel. -func (c *Client) GetMetaData(ctx context.Context, zid api.ZettelID) (api.MetaRights, error) { - ub := c.newURLBuilder('z').SetZid(zid) - ub.AppendKVQuery(api.QueryKeyEncoding, api.EncodingData) - 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) + ub := c.NewURLBuilder('z').SetZid(zid) + 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 { @@ -575,35 +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. -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) + ub := c.NewURLBuilder('z').SetZid(zid).AppendKVQuery(api.QueryKeyEncoding, api.EncodingData) + resp, err := c.buildAndExecuteRequest(ctx, http.MethodPut, ub, &buf) if err != nil { return err } defer resp.Body.Close() if resp.StatusCode != http.StatusNoContent { @@ -612,12 +348,12 @@ return nil } // DeleteZettel deletes a zettel with the given identifier. func (c *Client) DeleteZettel(ctx context.Context, zid api.ZettelID) error { - ub := c.newURLBuilder('z').SetZid(zid) - resp, err := c.buildAndExecuteRequest(ctx, http.MethodDelete, ub, nil, nil) + ub := c.NewURLBuilder('z').SetZid(zid) + resp, err := c.buildAndExecuteRequest(ctx, http.MethodDelete, ub, nil) if err != nil { return err } defer resp.Body.Close() if resp.StatusCode != http.StatusNoContent { @@ -625,52 +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) + ub := c.NewURLBuilder('x').AppendKVQuery(api.QueryKeyCommand, string(command)) + 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).String(), - Hash: vals[4].(sx.String).String(), - }, nil - } - } - return VersionInfo{}, err -} - -// VersionInfo contains version information. -type VersionInfo struct { - Major int - Minor int - Patch int - Info string - Hash string -} Index: client/client_test.go ================================================================== --- client/client_test.go +++ client/client_test.go @@ -4,10 +4,13 @@ // This file is part of zettelstore-client. // // Zettelstore client is licensed under the latest version of the EUPL // (European Union Public License). Please see file LICENSE.txt for your rights // and obligations under this license. +// +// SPDX-License-Identifier: EUPL-1.2 +// SPDX-FileCopyrightText: 2022-present Detlef Stern //----------------------------------------------------------------------------- package client_test import ( @@ -15,14 +18,12 @@ "flag" "net/http" "net/url" "testing" - "zettelstore.de/client.fossil/api" - "zettelstore.de/client.fossil/client" - "zettelstore.de/client.fossil/sz" - "zettelstore.de/sx.fossil" + "t73f.de/r/zsc/api" + "t73f.de/r/zsc/client" ) func TestZettelList(t *testing.T) { c := getClient() _, err := c.QueryZettel(context.Background(), "") @@ -45,14 +46,11 @@ } } func TestGetSzZettel(t *testing.T) { c := getClient() - sf := sx.MakeMappedFactory() - var zetSyms sz.ZettelSymbols - zetSyms.InitializeZettelSymbols(sf) - value, err := c.GetEvaluatedSz(context.Background(), api.ZidDefaultHome, api.PartContent, sf) + value, err := c.GetEvaluatedSz(context.Background(), api.ZidDefaultHome, api.PartContent) if err != nil { t.Error(err) return } if value.IsNil() { 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,5 +1,9 @@ -module zettelstore.de/client.fossil +module t73f.de/r/zsc -go 1.20 +go 1.23 -require zettelstore.de/sx.fossil v0.0.0-20230727172325-adec5a7ba284 +require ( + 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,2 +1,6 @@ -zettelstore.de/sx.fossil v0.0.0-20230727172325-adec5a7ba284 h1:26xwZWEjdyL3wObczc/PKugv0EY6mgSH5NUe5kYFCt4= -zettelstore.de/sx.fossil v0.0.0-20230727172325-adec5a7ba284/go.mod h1:nsWXVrQG8RNKtoXzEMrWXNMdnpfIDU6Hb0pk56KpVKE= +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= ADDED input/entity.go Index: input/entity.go ================================================================== --- /dev/null +++ input/entity.go @@ -0,0 +1,162 @@ +//----------------------------------------------------------------------------- +// Copyright (c) 2022-present Detlef Stern +// +// This file is part of zettelstore-client. +// +// Zettelstore client is licensed under the latest version of the EUPL +// (European Union Public License). Please see file LICENSE.txt for your rights +// and obligations under this license. +// +// SPDX-License-Identifier: EUPL-1.2 +// SPDX-FileCopyrightText: 2022-present Detlef Stern +//----------------------------------------------------------------------------- + +package input + +import ( + "html" + "unicode" +) + +// ScanEntity scans either a named or a numbered entity and returns it as a string. +// +// For numbered entities (like { or ģ) html.UnescapeString returns +// sometimes other values as expected, if the number is not well-formed. This +// may happen because of some strange HTML parsing rules. But these do not +// apply to Zettelmarkup. Therefore, I parse the number here in the code. +func (inp *Input) ScanEntity() (res string, success bool) { + if inp.Ch != '&' { + return "", false + } + pos := inp.Pos + inp.Next() + if inp.Ch == '#' { + inp.Next() + if inp.Ch == 'x' || inp.Ch == 'X' { + return inp.scanEntityBase16() + } + return inp.scanEntityBase10() + } + return inp.scanEntityNamed(pos) +} + +func (inp *Input) scanEntityBase16() (string, bool) { + inp.Next() + if inp.Ch == ';' { + return "", false + } + code := 0 + for { + switch ch := inp.Ch; ch { + case ';': + inp.Next() + if r := rune(code); isValidEntity(r) { + return string(r), true + } + return "", false + case '0', '1', '2', '3', '4', '5', '6', '7', '8', '9': + code = 16*code + int(ch-'0') + case 'a', 'b', 'c', 'd', 'e', 'f': + code = 16*code + int(ch-'a'+10) + case 'A', 'B', 'C', 'D', 'E', 'F': + code = 16*code + int(ch-'A'+10) + default: + return "", false + } + if code > unicode.MaxRune { + return "", false + } + inp.Next() + } +} + +func (inp *Input) scanEntityBase10() (string, bool) { + // Base 10 code + if inp.Ch == ';' { + return "", false + } + code := 0 + for { + switch ch := inp.Ch; ch { + case ';': + inp.Next() + if r := rune(code); isValidEntity(r) { + return string(r), true + } + return "", false + case '0', '1', '2', '3', '4', '5', '6', '7', '8', '9': + code = 10*code + int(ch-'0') + default: + return "", false + } + if code > unicode.MaxRune { + return "", false + } + inp.Next() + } +} +func (inp *Input) scanEntityNamed(pos int) (string, bool) { + for { + switch inp.Ch { + case EOS, '\n', '\r', '&': + return "", false + case ';': + inp.Next() + es := string(inp.Src[pos:inp.Pos]) + ues := html.UnescapeString(es) + if es == ues { + return "", false + } + return ues, true + default: + inp.Next() + } + } +} + +// isValidEntity checks if the given code is valid for an entity. +// +// According to https://html.spec.whatwg.org/multipage/syntax.html#character-references +// ""The numeric character reference forms described above are allowed to reference any code point +// excluding U+000D CR, noncharacters, and controls other than ASCII whitespace."" +func isValidEntity(r rune) bool { + // No C0 control and no "code point in the range U+007F DELETE to U+009F APPLICATION PROGRAM COMMAND, inclusive." + if r < ' ' || ('\u007f' <= r && r <= '\u009f') { + return false + } + + // If below any noncharacter code point, return true + // + // See: https://infra.spec.whatwg.org/#noncharacter + if r < '\ufdd0' { + return true + } + + // First range of noncharacter code points: "(...) in the range U+FDD0 to U+FDEF, inclusive" + if r <= '\ufdef' { + return false + } + + // Other noncharacter code points: + switch r { + case '\uFFFE', '\uFFFF', + '\U0001FFFE', '\U0001FFFF', + '\U0002FFFE', '\U0002FFFF', + '\U0003FFFE', '\U0003FFFF', + '\U0004FFFE', '\U0004FFFF', + '\U0005FFFE', '\U0005FFFF', + '\U0006FFFE', '\U0006FFFF', + '\U0007FFFE', '\U0007FFFF', + '\U0008FFFE', '\U0008FFFF', + '\U0009FFFE', '\U0009FFFF', + '\U000AFFFE', '\U000AFFFF', + '\U000BFFFE', '\U000BFFFF', + '\U000CFFFE', '\U000CFFFF', + '\U000DFFFE', '\U000DFFFF', + '\U000EFFFE', '\U000EFFFF', + '\U000FFFFE', '\U000FFFFF', + '\U0010FFFE', '\U0010FFFF': + return false + } + return true +} ADDED input/entity_test.go Index: input/entity_test.go ================================================================== --- /dev/null +++ input/entity_test.go @@ -0,0 +1,64 @@ +//----------------------------------------------------------------------------- +// Copyright (c) 2020-present Detlef Stern +// +// This file is part of zettelstore-client. +// +// Zettelstore client is licensed under the latest version of the EUPL +// (European Union Public License). Please see file LICENSE.txt for your rights +// and obligations under this license. +// +// SPDX-License-Identifier: EUPL-1.2 +// SPDX-FileCopyrightText: 2020-present Detlef Stern +//----------------------------------------------------------------------------- + +package input_test + +import ( + "testing" + + "t73f.de/r/zsc/input" +) + +func TestScanEntity(t *testing.T) { + t.Parallel() + var testcases = []struct { + text string + exp string + }{ + {"", ""}, + {"a", ""}, + {"&", "&"}, + {"!", "!"}, + {"3", "3"}, + {""", "\""}, + } + for id, tc := range testcases { + inp := input.NewInput([]byte(tc.text)) + got, ok := inp.ScanEntity() + if !ok { + if tc.exp != "" { + t.Errorf("ID=%d, text=%q: expected error, but got %q", id, tc.text, got) + } + if inp.Pos != 0 { + t.Errorf("ID=%d, text=%q: input position advances to %d", id, tc.text, inp.Pos) + } + continue + } + if tc.exp != got { + t.Errorf("ID=%d, text=%q: expected %q, but got %q", id, tc.text, tc.exp, got) + } + } +} + +func TestScanIllegalEntity(t *testing.T) { + t.Parallel() + testcases := []string{"", "a", "& Input →", " ", ""} + for i, tc := range testcases { + inp := input.NewInput([]byte(tc)) + got, ok := inp.ScanEntity() + if ok { + t.Errorf("%d: scanning %q was unexpected successful, got %q", i, tc, got) + continue + } + } +} ADDED input/input.go Index: input/input.go ================================================================== --- /dev/null +++ input/input.go @@ -0,0 +1,157 @@ +//----------------------------------------------------------------------------- +// Copyright (c) 2020-present Detlef Stern +// +// This file is part of zettelstore-client. +// +// Zettelstore client is licensed under the latest version of the EUPL +// (European Union Public License). Please see file LICENSE.txt for your rights +// and obligations under this license. +// +// SPDX-License-Identifier: EUPL-1.2 +// SPDX-FileCopyrightText: 2020-present Detlef Stern +//----------------------------------------------------------------------------- + +// Package input provides an abstraction for data to be read. +package input + +import "unicode/utf8" + +// Input is an abstract input source +type Input struct { + // Read-only, will never change + Src []byte // The source string + + // Read-only, will change + Ch rune // current character + Pos int // character position in src + readPos int // reading position (position after current character) +} + +// NewInput creates a new input source. +func NewInput(src []byte) *Input { + inp := &Input{Src: src} + inp.Next() + return inp +} + +// EOS = End of source +const EOS = rune(-1) + +// Next reads the next rune into inp.Ch and returns it too. +func (inp *Input) Next() rune { + if inp.readPos >= len(inp.Src) { + inp.Pos = len(inp.Src) + inp.Ch = EOS + return EOS + } + inp.Pos = inp.readPos + r, w := rune(inp.Src[inp.readPos]), 1 + if r >= utf8.RuneSelf { + r, w = utf8.DecodeRune(inp.Src[inp.readPos:]) + } + inp.readPos += w + inp.Ch = r + return r +} + +// Peek returns the rune following the most recently read rune without +// advancing. If end-of-source was already found peek returns EOS. +func (inp *Input) Peek() rune { + return inp.PeekN(0) +} + +// PeekN returns the n-th rune after the most recently read rune without +// advancing. If end-of-source was already found peek returns EOS. +func (inp *Input) PeekN(n int) rune { + pos := inp.readPos + n + if pos < len(inp.Src) { + r := rune(inp.Src[pos]) + if r >= utf8.RuneSelf { + r, _ = utf8.DecodeRune(inp.Src[pos:]) + } + if r == '\t' { + return ' ' + } + return r + } + return EOS +} + +// Accept checks if the given string is a prefix of the text to be parsed. +// If successful, advance position and current character. +// String must only contain bytes < 128. +// If not successful, everything remains as it is. +func (inp *Input) Accept(s string) bool { + pos := inp.Pos + remaining := len(inp.Src) - pos + if s == "" || len(s) > remaining { + return false + } + // According to internal documentation of bytes.Equal, the string() will not allocate any memory. + if readPos := pos + len(s); s == string(inp.Src[pos:readPos]) { + inp.readPos = readPos + inp.Next() + return true + } + return false +} + +// IsEOLEOS returns true if char is either EOS or EOL. +func IsEOLEOS(ch rune) bool { 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': + if inp.Peek() == '\n' { + inp.Next() + } + inp.Ch = '\n' + inp.Next() + case '\n': + inp.Next() + } +} + +// SetPos allows to reset the read position. +func (inp *Input) SetPos(pos int) { + if inp.Pos != pos { + inp.readPos = pos + inp.Next() + } +} + +// 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 { + case EOS, '\n', '\r': + return + } + inp.Next() + } +} + +// ScanLineContent reads the reaining input stream and interprets it as lines of text. +func (inp *Input) ScanLineContent() []byte { + result := make([]byte, 0, len(inp.Src)-inp.Pos+1) + for { + inp.EatEOL() + posL := inp.Pos + if inp.Ch == EOS { + return result + } + inp.SkipToEOL() + if len(result) > 0 { + result = append(result, '\n') + } + result = append(result, inp.Src[posL:inp.Pos]...) + } +} ADDED input/input_test.go Index: input/input_test.go ================================================================== --- /dev/null +++ input/input_test.go @@ -0,0 +1,68 @@ +//----------------------------------------------------------------------------- +// Copyright (c) 2020-present Detlef Stern +// +// This file is part of zettelstore-client. +// +// Zettelstore client is licensed under the latest version of the EUPL +// (European Union Public License). Please see file LICENSE.txt for your rights +// and obligations under this license. +// +// SPDX-License-Identifier: EUPL-1.2 +// SPDX-FileCopyrightText: 2020-present Detlef Stern +//----------------------------------------------------------------------------- + +// Package input_test provides some unit-tests for reading data. +package input_test + +import ( + "testing" + + "t73f.de/r/zsc/input" +) + +func TestEatEOL(t *testing.T) { + t.Parallel() + inp := input.NewInput(nil) + inp.EatEOL() + if inp.Ch != input.EOS { + t.Errorf("No EOS found: %q", inp.Ch) + } + if inp.Pos != 0 { + t.Errorf("Pos != 0: %d", inp.Pos) + } + + inp = input.NewInput([]byte("ABC")) + if inp.Ch != 'A' { + t.Errorf("First ch != 'A', got %q", inp.Ch) + } + inp.EatEOL() + if inp.Ch != 'A' { + t.Errorf("First ch != 'A', got %q", inp.Ch) + } +} + +func TestAccept(t *testing.T) { + t.Parallel() + testcases := []struct { + accept string + src string + acc bool + exp rune + }{ + {"", "", false, input.EOS}, + {"AB", "abc", false, 'a'}, + {"AB", "ABC", true, 'C'}, + {"AB", "AB", true, input.EOS}, + {"AB", "A", false, 'A'}, + } + for i, tc := range testcases { + inp := input.NewInput([]byte(tc.src)) + acc := inp.Accept(tc.accept) + if acc != tc.acc { + t.Errorf("%d: %q.Accept(%q) == %v, but got %v", i, tc.src, tc.accept, tc.acc, acc) + } + if got := inp.Ch; tc.exp != got { + t.Errorf("%d: %q.Accept(%q) should result in run %v, but got %v", i, tc.src, tc.accept, tc.exp, got) + } + } +} ADDED input/runes.go Index: input/runes.go ================================================================== --- /dev/null +++ input/runes.go @@ -0,0 +1,30 @@ +//----------------------------------------------------------------------------- +// Copyright (c) 2020-present Detlef Stern +// +// This file is part of zettelstore-client. +// +// Zettelstore client is licensed under the latest version of the EUPL +// (European Union Public License). Please see file LICENSE.txt for your rights +// and obligations under this license. +// +// SPDX-License-Identifier: EUPL-1.2 +// SPDX-FileCopyrightText: 2020-present Detlef Stern +//----------------------------------------------------------------------------- + +package input + +import "unicode" + +// IsSpace returns true if rune is a whitespace. +func IsSpace(ch rune) bool { + switch ch { + case ' ', '\t': + return true + case '\n', '\r', EOS: + return false + } + return unicode.IsSpace(ch) +} + +// 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 @@ -4,16 +4,21 @@ // This file is part of zettelstore-client. // // Zettelstore client is licensed under the latest version of the EUPL // (European Union Public License). Please see file LICENSE.txt for your rights // and obligations under this license. +// +// SPDX-License-Identifier: EUPL-1.2 +// SPDX-FileCopyrightText: 2022-present Detlef Stern //----------------------------------------------------------------------------- +// Package maps 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: maps/maps_test.go ================================================================== --- maps/maps_test.go +++ maps/maps_test.go @@ -4,18 +4,21 @@ // This file is part of zettelstore-client. // // Zettelstore client is licensed under the latest version of the EUPL // (European Union Public License). Please see file LICENSE.txt for your rights // and obligations under this license. +// +// SPDX-License-Identifier: EUPL-1.2 +// SPDX-FileCopyrightText: 2022-present Detlef Stern //----------------------------------------------------------------------------- package maps_test import ( "testing" - "zettelstore.de/client.fossil/maps" + "t73f.de/r/zsc/maps" ) func isSorted(seq []string) bool { for i := 1; i < len(seq); i++ { if seq[i] < seq[i-1] { Index: sexp/sexp.go ================================================================== --- sexp/sexp.go +++ sexp/sexp.go @@ -4,10 +4,13 @@ // This file is part of zettelstore-client. // // Zettelstore client is licensed under the latest version of the EUPL // (European Union Public License). Please see file LICENSE.txt for your rights // and obligations under this license. +// +// SPDX-License-Identifier: EUPL-1.2 +// SPDX-FileCopyrightText: 2023-present Detlef Stern //----------------------------------------------------------------------------- // Package sexp contains helper function to work with s-expression in an alien // environment. package sexp @@ -15,26 +18,26 @@ import ( "errors" "fmt" "sort" - "zettelstore.de/client.fossil/api" - "zettelstore.de/sx.fossil" + "t73f.de/r/sx" + "t73f.de/r/zsc/api" ) // EncodeZettel transforms zettel data into a sx object. func EncodeZettel(zettel api.ZettelData) sx.Object { - sf := sx.MakeMappedFactory() return sx.MakeList( - sf.MustMake("zettel"), - meta2sz(zettel.Meta, sf), - sx.MakeList(sf.MustMake("rights"), sx.Int64(int64(zettel.Rights))), - sx.MakeList(sf.MustMake("encoding"), sx.MakeString(zettel.Encoding)), - sx.MakeList(sf.MustMake("content"), sx.MakeString(zettel.Content)), + sx.MakeSymbol("zettel"), + meta2sz(zettel.Meta), + sx.MakeList(sx.MakeSymbol("rights"), sx.Int64(int64(zettel.Rights))), + sx.MakeList(sx.MakeSymbol("encoding"), sx.MakeString(zettel.Encoding)), + sx.MakeList(sx.MakeSymbol("content"), sx.MakeString(zettel.Content)), ) } +// 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 } @@ -69,38 +72,37 @@ } return api.ZettelData{ Meta: meta, Rights: rights, - Encoding: encVals[1].(sx.String).String(), - Content: contentVals[1].(sx.String).String(), + Encoding: encVals[1].(sx.String).GetValue(), + Content: contentVals[1].(sx.String).GetValue(), }, nil } // EncodeMetaRights translates metadata/rights into a sx object. func EncodeMetaRights(mr api.MetaRights) *sx.Pair { - sf := sx.MakeMappedFactory() return sx.MakeList( - sf.MustMake("list"), - meta2sz(mr.Meta, sf), - sx.MakeList(sf.MustMake("rights"), sx.Int64(int64(mr.Rights))), + sx.SymbolList, + meta2sz(mr.Meta), + sx.MakeList(sx.MakeSymbol("rights"), sx.Int64(int64(mr.Rights))), ) } -func meta2sz(m api.ZettelMeta, sf sx.SymbolFactory) sx.Object { - result := sx.Nil().Cons(sf.MustMake("meta")) - curr := result +func meta2sz(m api.ZettelMeta) sx.Object { + var result sx.ListBuilder + result.Add(sx.MakeSymbol("meta")) keys := make([]string, 0, len(m)) for k := range m { keys = append(keys, k) } sort.Strings(keys) for _, k := range keys { - val := sx.MakeList(sf.MustMake(k), sx.MakeString(m[k])) - curr = curr.AppendBang(val) + val := sx.MakeList(sx.MakeSymbol(k), sx.MakeString(m[k])) + result.Add(val) } - return result + return result.List() } // ParseMeta translates the given list to metadata. func ParseMeta(pair *sx.Pair) (api.ZettelMeta, error) { if err := CheckSymbol(pair.Car(), "meta"); err != nil { @@ -110,11 +112,11 @@ for node := pair.Tail(); node != nil; node = node.Tail() { mVals, err := ParseList(node.Car(), "ys") if err != nil { return nil, err } - res[mVals[0].(*sx.Symbol).Name()] = mVals[1].(sx.String).String() + res[(mVals[0].(*sx.Symbol)).GetValue()] = mVals[1].(sx.String).GetValue() } return res, nil } // ParseRights returns the rights values of the given object. @@ -132,11 +134,11 @@ } return api.ZettelRights(i64), nil } // ParseList parses the given object as a proper list, based on a type specification. -func ParseList(obj sx.Object, spec string) ([]sx.Object, error) { +func ParseList(obj sx.Object, spec string) (sx.Vector, error) { pair, isPair := sx.GetPair(obj) if !isPair { return nil, fmt.Errorf("not a list: %T/%v", obj, obj) } if pair == nil { @@ -144,11 +146,11 @@ return nil, nil } return nil, ErrElementsMissing } - result := make([]sx.Object, 0, len(spec)) + result := make(sx.Vector, 0, len(spec)) node, i := pair, 0 for ; node != nil; i++ { if i >= len(spec) { return nil, ErrNoSpec } @@ -155,11 +157,11 @@ var val sx.Object var ok bool car := node.Car() switch spec[i] { case 'b': - val, ok = sx.GetBoolean(car) + val, ok = sx.MakeBoolean(!sx.IsNil(car)), true case 'i': val, ok = car.(sx.Int64) case 'o': val, ok = car, true case 'p': @@ -185,19 +187,24 @@ 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) if !isSymbol { return fmt.Errorf("object %v/%T is not a symbol", obj, obj) } - if got := sym.Name(); got != name { + if got := sym.GetValue(); got != name { return fmt.Errorf("symbol %q expected, but got: %q", name, got) } return nil } Index: sexp/sexp_test.go ================================================================== --- sexp/sexp_test.go +++ sexp/sexp_test.go @@ -4,19 +4,22 @@ // This file is part of zettelstore-client. // // Zettelstore client is licensed under the latest version of the EUPL // (European Union Public License). Please see file LICENSE.txt for your rights // and obligations under this license. +// +// SPDX-License-Identifier: EUPL-1.2 +// SPDX-FileCopyrightText: 2023-present Detlef Stern //----------------------------------------------------------------------------- package sexp_test import ( "testing" - "zettelstore.de/client.fossil/sexp" - "zettelstore.de/sx.fossil" + "t73f.de/r/sx" + "t73f.de/r/zsc/sexp" ) func TestParseObject(t *testing.T) { if elems, err := sexp.ParseList(sx.MakeString("a"), "s"); err == nil { t.Error("expected an error, but got: ", elems) @@ -34,12 +37,12 @@ t.Error("expected error, but got: ", elems) } if elems, err := sexp.ParseList(sx.MakeList(sx.MakeString("a")), ""); err == nil { t.Error("expected error, but got: ", elems) } - if elems, err := sexp.ParseList(sx.MakeList(sx.MakeString("a")), "b"); err == nil { - t.Error("expected error, but got: ", elems) + if _, err := sexp.ParseList(sx.MakeList(sx.MakeString("a")), "b"); err != nil { + t.Error("expected [1], but got error: ", err) } if elems, err := sexp.ParseList(sx.Cons(sx.Nil(), sx.MakeString("a")), "ps"); err == nil { t.Error("expected error, but got: ", elems) } ADDED shtml/const.go Index: shtml/const.go ================================================================== --- /dev/null +++ shtml/const.go @@ -0,0 +1,84 @@ +//----------------------------------------------------------------------------- +// Copyright (c) 2024-present Detlef Stern +// +// This file is part of zettelstore-client. +// +// Zettelstore client is licensed under the latest version of the EUPL +// (European Union Public License). Please see file LICENSE.txt for your rights +// and obligations under this license. +// +// SPDX-License-Identifier: EUPL-1.2 +// SPDX-FileCopyrightText: 2024-present Detlef Stern +//----------------------------------------------------------------------------- + +package shtml + +import "t73f.de/r/sx" + +// Symbols for HTML header tags +var ( + SymBody = sx.MakeSymbol("body") + SymHead = sx.MakeSymbol("head") + SymHTML = sx.MakeSymbol("html") + SymMeta = sx.MakeSymbol("meta") + SymScript = sx.MakeSymbol("script") + SymTitle = sx.MakeSymbol("title") +) + +// Symbols for HTML body tags +var ( + SymA = sx.MakeSymbol("a") + SymASIDE = sx.MakeSymbol("aside") + symBLOCKQUOTE = sx.MakeSymbol("blockquote") + symBR = sx.MakeSymbol("br") + symCITE = sx.MakeSymbol("cite") + symCODE = sx.MakeSymbol("code") + symDD = sx.MakeSymbol("dd") + symDEL = sx.MakeSymbol("del") + SymDIV = sx.MakeSymbol("div") + symDL = sx.MakeSymbol("dl") + symDT = sx.MakeSymbol("dt") + symEM = sx.MakeSymbol("em") + SymEMBED = sx.MakeSymbol("embed") + SymFIGURE = sx.MakeSymbol("figure") + SymH1 = sx.MakeSymbol("h1") + SymH2 = sx.MakeSymbol("h2") + SymHR = sx.MakeSymbol("hr") + SymIMG = sx.MakeSymbol("img") + symINS = sx.MakeSymbol("ins") + symKBD = sx.MakeSymbol("kbd") + SymLI = sx.MakeSymbol("li") + symMARK = sx.MakeSymbol("mark") + SymOL = sx.MakeSymbol("ol") + SymP = sx.MakeSymbol("p") + symPRE = sx.MakeSymbol("pre") + symSAMP = sx.MakeSymbol("samp") + SymSPAN = sx.MakeSymbol("span") + SymSTRONG = sx.MakeSymbol("strong") + symSUB = sx.MakeSymbol("sub") + symSUP = sx.MakeSymbol("sup") + symTABLE = sx.MakeSymbol("table") + symTBODY = sx.MakeSymbol("tbody") + symTHEAD = sx.MakeSymbol("thead") + symTD = sx.MakeSymbol("td") + symTH = sx.MakeSymbol("th") + symTR = sx.MakeSymbol("tr") + SymUL = sx.MakeSymbol("ul") +) + +// Symbols for HTML attribute keys +var ( + symAttrAlt = sx.MakeSymbol("alt") + SymAttrClass = sx.MakeSymbol("class") + SymAttrHref = sx.MakeSymbol("href") + SymAttrID = sx.MakeSymbol("id") + SymAttrLang = sx.MakeSymbol("lang") + SymAttrOpen = sx.MakeSymbol("open") + SymAttrRel = sx.MakeSymbol("rel") + SymAttrRole = sx.MakeSymbol("role") + SymAttrSrc = sx.MakeSymbol("src") + SymAttrTarget = sx.MakeSymbol("target") + SymAttrTitle = sx.MakeSymbol("title") + SymAttrType = sx.MakeSymbol("type") + SymAttrValue = sx.MakeSymbol("value") +) 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 @@ -4,10 +4,13 @@ // This file is part of zettelstore-client. // // Zettelstore client is licensed under the latest version of the EUPL // (European Union Public License). Please see file LICENSE.txt for your rights // and obligations under this license. +// +// SPDX-License-Identifier: EUPL-1.2 +// SPDX-FileCopyrightText: 2023-present Detlef Stern //----------------------------------------------------------------------------- // Package shtml transforms a s-expr encoded zettel AST into a s-expr representation of HTML. package shtml @@ -15,873 +18,937 @@ "fmt" "net/url" "strconv" "strings" - "zettelstore.de/client.fossil/api" - "zettelstore.de/client.fossil/attrs" - "zettelstore.de/client.fossil/sz" - "zettelstore.de/client.fossil/text" - "zettelstore.de/sx.fossil" - "zettelstore.de/sx.fossil/sxbuiltins/quote" - "zettelstore.de/sx.fossil/sxeval" - "zettelstore.de/sx.fossil/sxhtml" -) - -// Transformer will transform a s-expression that encodes the zettel AST into an s-expression -// that represents HTML. -type Transformer struct { - sf sx.SymbolFactory - rebinder RebindProc + "t73f.de/r/sx" + "t73f.de/r/sxwebs/sxhtml" + "t73f.de/r/zsc/api" + "t73f.de/r/zsc/attrs" + "t73f.de/r/zsc/sz" + "t73f.de/r/zsc/text" +) + +// Evaluator will transform a s-expression that encodes the zettel AST into an s-expression +// that represents HTML. +type Evaluator struct { headingOffset int64 unique string - endnotes []endnoteInfo noLinks bool // true iff output must not include links - symAttr *sx.Symbol - symClass *sx.Symbol - symMeta *sx.Symbol - symA *sx.Symbol - symSpan *sx.Symbol -} - -type endnoteInfo struct { - noteAST *sx.Pair // Endnote as AST - noteHx *sx.Pair // Endnote as SxHTML - attrs *sx.Pair // attrs a-list -} - -// NewTransformer creates a new transformer object. -func NewTransformer(headingOffset int, sf sx.SymbolFactory) *Transformer { - if sf == nil { - sf = sx.MakeMappedFactory() - } - return &Transformer{ - sf: sf, - rebinder: nil, + + fns map[string]EvalFn + minArgs map[string]int +} + +// NewEvaluator creates a new Evaluator object. +func NewEvaluator(headingOffset int) *Evaluator { + ev := &Evaluator{ headingOffset: int64(headingOffset), - symAttr: sf.MustMake(sxhtml.NameSymAttr), - symClass: sf.MustMake("class"), - symMeta: sf.MustMake("meta"), - symA: sf.MustMake("a"), - symSpan: sf.MustMake("span"), - } -} - -// SymbolFactory returns the symbol factory to create HTML symbols. -func (tr *Transformer) SymbolFactory() sx.SymbolFactory { return tr.sf } + + fns: make(map[string]EvalFn, 128), + minArgs: make(map[string]int, 128), + } + ev.bindMetadata() + ev.bindBlocks() + ev.bindInlines() + return ev +} // SetUnique sets a prefix to make several HTML ids unique. -func (tr *Transformer) SetUnique(s string) { tr.unique = s } +func (ev *Evaluator) SetUnique(s string) { ev.unique = s } // IsValidName returns true, if name is a valid symbol name. -func (tr *Transformer) IsValidName(s string) bool { return tr.sf.IsValidName(s) } - -// Make a new HTML symbol. -func (tr *Transformer) Make(s string) *sx.Symbol { return tr.sf.MustMake(s) } - -// RebindProc is a procedure which is called every time before a tranformation takes place. -type RebindProc func(*TransformEnv) - -// SetRebinder sets the rebinder procedure. -func (tr *Transformer) SetRebinder(rb RebindProc) { tr.rebinder = rb } - -// TransformAttrbute transforms the given attributes into a HTML s-expression. -func (tr *Transformer) TransformAttrbute(a attrs.Attributes) *sx.Pair { +func isValidName(s string) bool { return s != "" } + +// EvaluateAttrbute transforms the given attributes into a HTML s-expression. +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) { - plist = plist.Cons(sx.Cons(tr.Make(key), sx.MakeString(a[key]))) + if key != attrs.DefaultAttribute && isValidName(key) { + plist = plist.Cons(sx.Cons(sx.MakeSymbol(key), sx.MakeString(a[key]))) } } if plist == nil { return nil } - return plist.Cons(tr.symAttr) -} - -// TransformMeta creates a HTML meta s-expression -func (tr *Transformer) TransformMeta(a attrs.Attributes) *sx.Pair { - return sx.Nil().Cons(tr.TransformAttrbute(a)).Cons(tr.symMeta) -} - -// Transform an AST s-expression into a list of HTML s-expressions. -func (tr *Transformer) Transform(lst *sx.Pair) (*sx.Pair, error) { - astSF := sx.FindSymbolFactory(lst) - if astSF != nil { - if astSF == tr.sf { - panic("Invalid AST SymbolFactory") - } - } else { - astSF = sx.MakeMappedFactory() - } - astEnv := sx.MakeRootEnvironment() - engine := sxeval.MakeEngine(astSF, astEnv) - quote.InstallQuoteSyntax(astEnv, astSF.MustMake(sz.NameSymQuote)) - te := TransformEnv{ - tr: tr, - astSF: astSF, - astEnv: astEnv, - err: nil, - textEnc: text.NewEncoder(astSF), - } - te.initialize() - if rb := tr.rebinder; rb != nil { - rb(&te) - } - - val, err := engine.Eval(te.astEnv, lst) - if err != nil { + return plist.Cons(sxhtml.SymAttr) +} + +// Evaluate a metadata s-expression into a list of HTML s-expressions. +func (ev *Evaluator) Evaluate(lst *sx.Pair, env *Environment) (*sx.Pair, error) { + result := ev.Eval(lst, env) + if err := env.err; err != nil { + return nil, err + } + pair, isPair := sx.GetPair(result) + if !isPair { + return nil, fmt.Errorf("evaluation does not result in a pair, but %T/%v", result, result) + } + + for i := 0; i < len(env.endnotes); i++ { + // May extend tr.endnotes -> do not use for i := range len(...)!!! + + if env.endnotes[i].noteHx != nil { + continue + } + + noteHx, _ := ev.EvaluateList(env.endnotes[i].noteAST, env) + env.endnotes[i].noteHx = noteHx + } + + return pair, nil +} + +// EvaluateList will evaluate all list elements separately and returns them as a sx.Pair list +func (ev *Evaluator) EvaluateList(lst sx.Vector, env *Environment) (*sx.Pair, error) { + var result sx.ListBuilder + for _, elem := range lst { + p := ev.Eval(elem, env) + result.Add(p) + } + if err := env.err; err != nil { return nil, err } - res, isPair := sx.GetPair(val) - if !isPair { - panic("Result is not a list") - } - for i := 0; i < len(tr.endnotes); i++ { - // May extend tr.endnotes - val, err = engine.Eval(te.astEnv, tr.endnotes[i].noteAST) - if err != nil { - return res, err - } - en, ok := sx.GetPair(val) - if !ok { - panic("Endnote is not a list") - } - tr.endnotes[i].noteHx = en - } - return res, err - + return result.List(), nil } // Endnotes returns a SHTML object with all collected endnotes. -func (tr *Transformer) Endnotes() *sx.Pair { - if len(tr.endnotes) == 0 { +func Endnotes(env *Environment) *sx.Pair { + if env.err != nil || len(env.endnotes) == 0 { return nil } - result := sx.Nil().Cons(tr.Make("ol")) - currResult := result.AppendBang(sx.Nil().Cons(sx.Cons(tr.symClass, sx.MakeString("zs-endnotes"))).Cons(tr.symAttr)) - for i, fni := range tr.endnotes { + + var result sx.ListBuilder + result.Add(SymOL) + result.Add(sx.Nil().Cons(sx.Cons(SymAttrClass, sx.MakeString("zs-endnotes"))).Cons(sxhtml.SymAttr)) + for i, fni := range env.endnotes { noteNum := strconv.Itoa(i + 1) - noteID := tr.unique + noteNum - - attrs := fni.attrs.Cons(sx.Cons(tr.symClass, sx.MakeString("zs-endnote"))). - Cons(sx.Cons(tr.Make("value"), sx.MakeString(noteNum))). - Cons(sx.Cons(tr.Make("id"), sx.MakeString("fn:"+noteID))). - Cons(sx.Cons(tr.Make("role"), sx.MakeString("doc-endnote"))). - Cons(tr.symAttr) + attrs := fni.attrs.Cons(sx.Cons(SymAttrClass, sx.MakeString("zs-endnote"))). + Cons(sx.Cons(SymAttrValue, sx.MakeString(noteNum))). + Cons(sx.Cons(SymAttrID, sx.MakeString("fn:"+fni.noteID))). + Cons(sx.Cons(SymAttrRole, sx.MakeString("doc-endnote"))). + Cons(sxhtml.SymAttr) backref := sx.Nil().Cons(sx.MakeString("\u21a9\ufe0e")). Cons(sx.Nil(). - Cons(sx.Cons(tr.symClass, sx.MakeString("zs-endnote-backref"))). - Cons(sx.Cons(tr.Make("href"), sx.MakeString("#fnref:"+noteID))). - Cons(sx.Cons(tr.Make("role"), sx.MakeString("doc-backlink"))). - Cons(tr.symAttr)). - Cons(tr.symA) - - li := sx.Nil().Cons(tr.Make("li")) - li.AppendBang(attrs). - ExtendBang(fni.noteHx). - AppendBang(sx.MakeString(" ")).AppendBang(backref) - currResult = currResult.AppendBang(li) - } - tr.endnotes = nil - return result -} - -// TransformEnv is the environment where the actual transformation takes places. -type TransformEnv struct { - tr *Transformer - astSF sx.SymbolFactory - astEnv sx.Environment - err error - textEnc *text.Encoder - symNoEscape *sx.Symbol - symAttr *sx.Symbol - symA *sx.Symbol - symSpan *sx.Symbol - symP *sx.Symbol -} - -func (te *TransformEnv) initialize() { - te.symNoEscape = te.Make(sxhtml.NameSymNoEscape) - te.symAttr = te.tr.symAttr - te.symA = te.tr.symA - te.symSpan = te.tr.symSpan - te.symP = te.Make("p") - - te.bind(sz.NameSymList, 0, listArgs) - te.bindMetadata() - te.bindBlocks() - te.bindInlines() -} - -func listArgs(args []sx.Object) sx.Object { return sx.MakeList(args...) } - -func (te *TransformEnv) bindMetadata() { - te.bind(sz.NameSymMeta, 0, listArgs) - te.bind(sz.NameSymTypeZettelmarkup, 2, func(args []sx.Object) sx.Object { - a := make(attrs.Attributes, 2). - Set("name", te.getSymbol(args[0]).String()). - Set("content", te.textEnc.Encode(te.getList(args[1]))) - return te.transformMeta(a) - }) - metaString := func(args []sx.Object) sx.Object { - a := make(attrs.Attributes, 2). - Set("name", te.getSymbol(args[0]).Name()). - Set("content", te.getString(args[1]).String()) - return te.transformMeta(a) - } - te.bind(sz.NameSymTypeCredential, 2, metaString) - te.bind(sz.NameSymTypeEmpty, 2, metaString) - te.bind(sz.NameSymTypeID, 2, metaString) - te.bind(sz.NameSymTypeNumber, 2, metaString) - te.bind(sz.NameSymTypeString, 2, metaString) - te.bind(sz.NameSymTypeTimestamp, 2, metaString) - te.bind(sz.NameSymTypeURL, 2, metaString) - te.bind(sz.NameSymTypeWord, 2, metaString) - metaSet := func(args []sx.Object) sx.Object { - var sb strings.Builder - for elem := te.getList(args[1]); elem != nil; elem = elem.Tail() { - sb.WriteByte(' ') - sb.WriteString(te.getString(elem.Car()).String()) + Cons(sx.Cons(SymAttrClass, sx.MakeString("zs-endnote-backref"))). + Cons(sx.Cons(SymAttrHref, sx.MakeString("#fnref:"+fni.noteID))). + Cons(sx.Cons(SymAttrRole, sx.MakeString("doc-backlink"))). + Cons(sxhtml.SymAttr)). + Cons(SymA) + + var li sx.ListBuilder + li.Add(SymLI) + li.Add(attrs) + li.ExtendBang(fni.noteHx) + li.Add(sx.MakeString(" ")) + li.Add(backref) + result.Add(li.List()) + } + return result.List() +} + +// Environment where sz objects are evaluated to shtml objects +type Environment struct { + err error + langStack LangStack + endnotes []endnoteInfo + quoteNesting uint +} +type endnoteInfo struct { + noteID string // link id + noteAST sx.Vector // Endnote as list of AST inline elements + attrs *sx.Pair // attrs a-list + noteHx *sx.Pair // Endnote as SxHTML +} + +// MakeEnvironment builds a new evaluation environment. +func MakeEnvironment(lang string) Environment { + return Environment{ + err: nil, + langStack: NewLangStack(lang), + endnotes: nil, + quoteNesting: 0, + } +} + +// GetError returns the last error found. +func (env *Environment) GetError() error { return env.err } + +// Reset the environment. +func (env *Environment) Reset() { + env.langStack.Reset() + env.endnotes = nil + env.quoteNesting = 0 +} + +// pushAttribute adds the current attributes to the environment. +func (env *Environment) pushAttributes(a attrs.Attributes) { + if value, ok := a.Get("lang"); ok { + env.langStack.Push(value) + } else { + env.langStack.Dup() + } +} + +// 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 + +func (ev *Evaluator) bind(sym *sx.Symbol, minArgs int, fn EvalFn) { + symVal := sym.GetValue() + ev.fns[symVal] = fn + if minArgs > 0 { + ev.minArgs[symVal] = minArgs + } +} + +// ResolveBinding returns the function bound to the given name. +func (ev *Evaluator) ResolveBinding(sym *sx.Symbol) EvalFn { + if fn, found := ev.fns[sym.GetValue()]; found { + return fn + } + return nil +} + +// Rebind overwrites a binding, but leaves the minimum number of arguments intact. +func (ev *Evaluator) Rebind(sym *sx.Symbol, fn EvalFn) { + symVal := sym.GetValue() + if _, found := ev.fns[symVal]; !found { + panic(sym) + } + ev.fns[symVal] = fn +} + +func (ev *Evaluator) bindMetadata() { + ev.bind(sz.SymMeta, 0, ev.evalList) + evalMetaString := func(args sx.Vector, env *Environment) sx.Object { + a := make(attrs.Attributes, 2). + Set("name", getSymbol(args[0], env).GetValue()). + Set("content", getString(args[1], env).GetValue()) + return ev.EvaluateMeta(a) + } + ev.bind(sz.SymTypeCredential, 2, evalMetaString) + ev.bind(sz.SymTypeEmpty, 2, evalMetaString) + ev.bind(sz.SymTypeID, 2, evalMetaString) + ev.bind(sz.SymTypeNumber, 2, evalMetaString) + ev.bind(sz.SymTypeString, 2, evalMetaString) + ev.bind(sz.SymTypeTimestamp, 2, evalMetaString) + ev.bind(sz.SymTypeURL, 2, evalMetaString) + ev.bind(sz.SymTypeWord, 2, evalMetaString) + + evalMetaSet := func(args sx.Vector, env *Environment) sx.Object { + var sb strings.Builder + for elem := getList(args[1], env); elem != nil; elem = elem.Tail() { + sb.WriteByte(' ') + sb.WriteString(getString(elem.Car(), env).GetValue()) } s := sb.String() if len(s) > 0 { s = s[1:] } a := make(attrs.Attributes, 2). - Set("name", te.getSymbol(args[0]).Name()). + Set("name", getSymbol(args[0], env).GetValue()). Set("content", s) - return te.transformMeta(a) - } - te.bind(sz.NameSymTypeIDSet, 2, metaSet) - te.bind(sz.NameSymTypeTagSet, 2, metaSet) - te.bind(sz.NameSymTypeWordSet, 2, metaSet) -} - -func (te *TransformEnv) bindBlocks() { - te.bind(sz.NameSymBlock, 0, listArgs) - te.bind(sz.NameSymPara, 0, func(args []sx.Object) sx.Object { - // for ; args != nil; args = args.Tail() { - // lst, ok := sx.GetList(args.Car()) - // if !ok || lst != nil { - // break - // } - // } - return sx.MakeList(args...).Cons(te.symP) - }) - te.bind(sz.NameSymHeading, 5, func(args []sx.Object) sx.Object { - nLevel := te.getInt64(args[0]) + 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", 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(EvaluateAttrbute(a)).Cons(SymMeta) +} + +func (ev *Evaluator) bindBlocks() { + ev.bind(sz.SymBlock, 0, ev.evalList) + ev.bind(sz.SymPara, 0, func(args sx.Vector, env *Environment) sx.Object { + return ev.evalSlice(args, env).Cons(SymP) + }) + ev.bind(sz.SymHeading, 5, func(args sx.Vector, env *Environment) sx.Object { + nLevel := getInt64(args[0], env) if nLevel <= 0 { - te.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+te.tr.headingOffset, 10) + level := strconv.FormatInt(nLevel+ev.headingOffset, 10) + headingSymbol := sx.MakeSymbol("h" + level) - a := te.getAttributes(args[1]) - if fragment := te.getString(args[3]).String(); fragment != "" { - a = a.Set("id", te.tr.unique+fragment) + 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, isPair := sx.GetPair(args[4]); isPair && result != nil { + if result, _ := ev.EvaluateList(args[4:], env); result != nil { if len(a) > 0 { - result = result.Cons(te.transformAttribute(a)) + result = result.Cons(EvaluateAttrbute(a)) } - return result.Cons(te.Make("h" + level)) + return result.Cons(headingSymbol) } - return sx.MakeList(te.Make("h"+level), sx.MakeString("")) + return sx.MakeList(headingSymbol, sx.MakeString("")) }) - te.bind(sz.NameSymThematic, 0, func(args []sx.Object) sx.Object { + ev.bind(sz.SymThematic, 0, func(args sx.Vector, env *Environment) sx.Object { result := sx.Nil() if len(args) > 0 { - if attrList := te.getList(args[0]); attrList != nil { - result = result.Cons(te.transformAttribute(sz.GetAttributes(attrList))) + if attrList := getList(args[0], env); attrList != nil { + result = result.Cons(EvaluateAttrbute(sz.GetAttributes(attrList))) } } - return result.Cons(te.Make("hr")) + return result.Cons(SymHR) }) - te.bind(sz.NameSymListOrdered, 0, te.makeListFn("ol")) - te.bind(sz.NameSymListUnordered, 0, te.makeListFn("ul")) - te.bind(sz.NameSymDescription, 0, func(args []sx.Object) sx.Object { + + ev.bind(sz.SymListOrdered, 0, ev.makeListFn(SymOL)) + ev.bind(sz.SymListUnordered, 0, ev.makeListFn(SymUL)) + ev.bind(sz.SymDescription, 0, func(args sx.Vector, env *Environment) sx.Object { if len(args) == 0 { return sx.Nil() } - items := sx.Nil().Cons(te.Make("dl")) - curItem := items + var items sx.ListBuilder + items.Add(symDL) for pos := 0; pos < len(args); pos++ { - term := te.getList(args[pos]) - curItem = curItem.AppendBang(term.Cons(te.Make("dt"))) + term := ev.evalDescriptionTerm(getList(args[pos], env), env) + items.Add(term.Cons(symDT)) pos++ if pos >= len(args) { break } - ddBlock := te.getList(args[pos]) + ddBlock := getList(ev.Eval(args[pos], env), env) if ddBlock == nil { - break + continue } for ddlst := ddBlock; ddlst != nil; ddlst = ddlst.Tail() { - dditem := te.getList(ddlst.Car()) - curItem = curItem.AppendBang(dditem.Cons(te.Make("dd"))) + dditem := getList(ddlst.Car(), env) + items.Add(dditem.Cons(symDD)) } } - return items + return items.List() }) - - te.bind(sz.NameSymListQuote, 0, func(args []sx.Object) sx.Object { + ev.bind(sz.SymListQuote, 0, func(args sx.Vector, env *Environment) sx.Object { if args == nil { return sx.Nil() } - result := sx.Nil().Cons(te.Make("blockquote")) - currResult := result + var result sx.ListBuilder + result.Add(symBLOCKQUOTE) for _, elem := range args { - if quote, isPair := sx.GetPair(elem); isPair { - currResult = currResult.AppendBang(quote.Cons(te.symP)) + if quote, isPair := sx.GetPair(ev.Eval(elem, env)); isPair { + result.Add(quote.Cons(sxhtml.SymListSplice)) } } - return result + return result.List() }) - te.bind(sz.NameSymTable, 1, func(args []sx.Object) sx.Object { + ev.bind(sz.SymTable, 1, func(args sx.Vector, env *Environment) sx.Object { thead := sx.Nil() - if header := te.getList(args[0]); !sx.IsNil(header) { - thead = sx.Nil().Cons(te.transformTableRow(header)).Cons(te.Make("thead")) + if header := getList(args[0], env); !sx.IsNil(header) { + thead = sx.Nil().Cons(ev.evalTableRow(symTH, header, env)).Cons(symTHEAD) } - tbody := sx.Nil() + var tbody sx.ListBuilder if len(args) > 1 { - tbody = sx.Nil().Cons(te.Make("tbody")) - curBody := tbody + tbody.Add(symTBODY) for _, row := range args[1:] { - curBody = curBody.AppendBang(te.transformTableRow(te.getList(row))) + tbody.Add(ev.evalTableRow(symTD, getList(row, env), env)) } } table := sx.Nil() - if tbody != nil { - table = table.Cons(tbody) + if !tbody.IsEmpty() { + table = table.Cons(tbody.List()) } if thead != nil { table = table.Cons(thead) } if table == nil { return sx.Nil() } - return table.Cons(te.Make("table")) - }) - te.bind(sz.NameSymCell, 0, te.makeCellFn("")) - te.bind(sz.NameSymCellCenter, 0, te.makeCellFn("center")) - te.bind(sz.NameSymCellLeft, 0, te.makeCellFn("left")) - te.bind(sz.NameSymCellRight, 0, te.makeCellFn("right")) - - te.bind(sz.NameSymRegionBlock, 2, te.makeRegionFn(te.Make("div"), true)) - te.bind(sz.NameSymRegionQuote, 2, te.makeRegionFn(te.Make("blockquote"), false)) - te.bind(sz.NameSymRegionVerse, 2, te.makeRegionFn(te.Make("div"), false)) - - te.bind(sz.NameSymVerbatimComment, 1, func(args []sx.Object) sx.Object { - if te.getAttributes(args[0]).HasDefault() { + return table.Cons(symTABLE) + }) + ev.bind(sz.SymCell, 0, ev.makeCellFn("")) + ev.bind(sz.SymCellCenter, 0, ev.makeCellFn("center")) + ev.bind(sz.SymCellLeft, 0, ev.makeCellFn("left")) + ev.bind(sz.SymCellRight, 0, ev.makeCellFn("right")) + + ev.bind(sz.SymRegionBlock, 2, ev.makeRegionFn(SymDIV, true)) + ev.bind(sz.SymRegionQuote, 2, ev.makeRegionFn(symBLOCKQUOTE, false)) + ev.bind(sz.SymRegionVerse, 2, ev.makeRegionFn(SymDIV, false)) + + ev.bind(sz.SymVerbatimComment, 1, func(args sx.Vector, env *Environment) sx.Object { + if GetAttributes(args[0], env).HasDefault() { if len(args) > 1 { - if s := te.getString(args[1]); s != "" { - t := sx.MakeString(s.String()) - return sx.Nil().Cons(t).Cons(te.Make(sxhtml.NameSymBlockComment)) + if s := getString(args[1], env); s.GetValue() != "" { + return sx.Nil().Cons(s).Cons(sxhtml.SymBlockComment) } } } return nil }) - - te.bind(sz.NameSymVerbatimEval, 2, func(args []sx.Object) sx.Object { - return te.transformVerbatim(te.getAttributes(args[0]).AddClass("zs-eval"), te.getString(args[1])) - }) - te.bind(sz.NameSymVerbatimHTML, 2, te.transformHTML) - te.bind(sz.NameSymVerbatimMath, 2, func(args []sx.Object) sx.Object { - return te.transformVerbatim(te.getAttributes(args[0]).AddClass("zs-math"), te.getString(args[1])) - }) - te.bind(sz.NameSymVerbatimProg, 2, func(args []sx.Object) sx.Object { - a := te.getAttributes(args[0]) - content := te.getString(args[1]) + ev.bind(sz.SymVerbatimEval, 2, func(args sx.Vector, env *Environment) sx.Object { + 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 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 := GetAttributes(args[0], env) + content := getString(args[1], env) if a.HasDefault() { - content = sx.MakeString(visibleReplacer.Replace(content.String())) - } - return te.transformVerbatim(a, content) - }) - te.bind(sz.NameSymVerbatimZettel, 0, func([]sx.Object) sx.Object { return sx.Nil() }) - - te.bind(sz.NameSymBLOB, 3, func(args []sx.Object) sx.Object { - return te.transformBLOB(te.getList(args[0]), te.getString(args[1]), te.getString(args[2])) - }) - - te.bind(sz.NameSymTransclude, 2, func(args []sx.Object) sx.Object { + content = sx.MakeString(visibleReplacer.Replace(content.GetValue())) + } + return evalVerbatim(a, content) + }) + ev.bind(sz.SymVerbatimZettel, 0, nilFn) + ev.bind(sz.SymBLOB, 3, func(args sx.Vector, env *Environment) sx.Object { + 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() } refKind := ref.Car() if sx.IsNil(refKind) { return sx.Nil() } - if refValue := te.getString(ref.Tail().Car()); refValue != "" { - if te.astSF.MustMake(sz.NameSymRefStateExternal).IsEqual(refKind) { - a := te.getAttributes(args[0]).Set("src", refValue.String()).AddClass("external") - return sx.Nil().Cons(sx.Nil().Cons(te.transformAttribute(a)).Cons(te.Make("img"))).Cons(te.symP) + if refValue := getString(ref.Tail().Car(), env); refValue.GetValue() != "" { + 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( - te.Make(sxhtml.NameSymInlineComment), + sxhtml.SymInlineComment, sx.MakeString("transclude"), refKind, sx.MakeString("->"), refValue, ) } - return sx.MakeList(args...) + return ev.evalSlice(args, env) }) } -func (te *TransformEnv) makeListFn(tag string) transformFn { - sym := te.Make(tag) - return func(args []sx.Object) sx.Object { - result := sx.Nil().Cons(sym) - last := result +func (ev *Evaluator) makeListFn(sym *sx.Symbol) EvalFn { + return func(args sx.Vector, env *Environment) sx.Object { + var result sx.ListBuilder + result.Add(sym) for _, elem := range args { - item := sx.Nil().Cons(te.Make("li")) - if res, isPair := sx.GetPair(elem); isPair { + item := sx.Nil().Cons(SymLI) + if res, isPair := sx.GetPair(ev.Eval(elem, env)); isPair { item.ExtendBang(res) } - last = last.AppendBang(item) + result.Add(item) } - return result + return result.List() + } +} + +func (ev *Evaluator) evalDescriptionTerm(term *sx.Pair, env *Environment) *sx.Pair { + var result sx.ListBuilder + for node := term; node != nil; node = node.Tail() { + elem := ev.Eval(node.Car(), env) + result.Add(elem) } + return result.List() } -func (te *TransformEnv) transformTableRow(pairs *sx.Pair) *sx.Pair { - row := sx.Nil().Cons(te.Make("tr")) + +func (ev *Evaluator) evalTableRow(sym *sx.Symbol, pairs *sx.Pair, env *Environment) *sx.Pair { if pairs == nil { return nil } - curRow := row + var row sx.ListBuilder + row.Add(symTR) for pair := pairs; pair != nil; pair = pair.Tail() { - curRow = curRow.AppendBang(pair.Car()) - } - return row -} - -func (te *TransformEnv) makeCellFn(align string) transformFn { - return func(args []sx.Object) sx.Object { - tdata := sx.MakeList(args...) + 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(te.transformAttribute(attrs.Attributes{"class": align})) + tdata = tdata.Cons(EvaluateAttrbute(attrs.Attributes{"class": align})) } - return tdata.Cons(te.Make("td")) + return tdata } } -func (te *TransformEnv) makeRegionFn(sym *sx.Symbol, genericToClass bool) transformFn { - return func(args []sx.Object) sx.Object { - a := te.getAttributes(args[0]) +func (ev *Evaluator) makeRegionFn(sym *sx.Symbol, genericToClass bool) EvalFn { + return func(args sx.Vector, env *Environment) sx.Object { + a := GetAttributes(args[0], env) + env.pushAttributes(a) + defer env.popAttributes() if genericToClass { if val, found := a.Get(""); found { a = a.Remove("").AddClass(val) } } - result := sx.Nil() + var result sx.ListBuilder + result.Add(sym) if len(a) > 0 { - result = result.Cons(te.transformAttribute(a)) + result.Add(EvaluateAttrbute(a)) } - result = result.Cons(sym) - currResult := result.LastPair() if region, isPair := sx.GetPair(args[1]); isPair { - currResult = currResult.ExtendBang(region) + if evalRegion := ev.EvalPairList(region, env); evalRegion != nil { + result.ExtendBang(evalRegion) + } } if len(args) > 2 { - if cite, isPair := sx.GetPair(args[2]); isPair && cite != nil { - currResult.AppendBang(cite.Cons(te.Make("cite"))) + if cite, _ := ev.EvaluateList(args[2:], env); cite != nil { + result.Add(cite.Cons(symCITE)) } } - return result + return result.List() } } -func (te *TransformEnv) transformVerbatim(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 := te.transformAttribute(a); al != nil { + if al := EvaluateAttrbute(a); al != nil { code = code.Cons(al) } - code = code.Cons(te.Make("code")) - return sx.Nil().Cons(code).Cons(te.Make("pre")) -} - -func (te *TransformEnv) bindInlines() { - te.bind(sz.NameSymInline, 0, listArgs) - te.bind(sz.NameSymText, 1, func(args []sx.Object) sx.Object { return te.getString(args[0]) }) - te.bind(sz.NameSymSpace, 0, func(args []sx.Object) sx.Object { - if len(args) == 0 { - return sx.MakeString(" ") - } - return te.getString(args[0]) - }) - te.bind(sz.NameSymSoft, 0, func([]sx.Object) sx.Object { return sx.MakeString(" ") }) - brSym := te.Make("br") - te.bind(sz.NameSymHard, 0, func([]sx.Object) sx.Object { return sx.Nil().Cons(brSym) }) - - te.bind(sz.NameSymLinkInvalid, 2, func(args []sx.Object) sx.Object { - // a := te.getAttributes(args) + code = code.Cons(symCODE) + return sx.Nil().Cons(code).Cons(symPRE) +} + +func (ev *Evaluator) bindInlines() { + ev.bind(sz.SymInline, 0, ev.evalList) + ev.bind(sz.SymText, 1, func(args sx.Vector, env *Environment) sx.Object { return getString(args[0], env) }) + ev.bind(sz.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 := GetAttributes(args[0], env) + env.pushAttributes(a) + defer env.popAttributes() var inline *sx.Pair if len(args) > 2 { - inline = sx.MakeList(args[2:]...) + inline = ev.evalSlice(args[2:], env) } if inline == nil { - inline = sx.Nil().Cons(args[1]) - } - return inline.Cons(te.symSpan) - }) - transformHREF := func(args []sx.Object) sx.Object { - a := te.getAttributes(args[0]) - refValue := te.getString(args[1]) - return te.transformLink(a.Set("href", refValue.String()), refValue, args[2:]) - } - te.bind(sz.NameSymLinkZettel, 2, transformHREF) - te.bind(sz.NameSymLinkSelf, 2, transformHREF) - te.bind(sz.NameSymLinkFound, 2, transformHREF) - te.bind(sz.NameSymLinkBroken, 2, func(args []sx.Object) sx.Object { - a := te.getAttributes(args[0]) - refValue := te.getString(args[1]) - return te.transformLink(a.AddClass("broken"), refValue, args[2:]) - }) - te.bind(sz.NameSymLinkHosted, 2, transformHREF) - te.bind(sz.NameSymLinkBased, 2, transformHREF) - te.bind(sz.NameSymLinkQuery, 2, func(args []sx.Object) sx.Object { - a := te.getAttributes(args[0]) - refValue := te.getString(args[1]) - query := "?" + api.QueryKeyQuery + "=" + url.QueryEscape(refValue.String()) - return te.transformLink(a.Set("href", query), refValue, args[2:]) - }) - te.bind(sz.NameSymLinkExternal, 2, func(args []sx.Object) sx.Object { - a := te.getAttributes(args[0]) - refValue := te.getString(args[1]) - return te.transformLink(a.Set("href", refValue.String()).AddClass("external"), refValue, args[2:]) - }) - - te.bind(sz.NameSymEmbed, 3, func(args []sx.Object) sx.Object { - ref := te.getList(args[1]) - syntax := te.getString(args[2]) - if syntax == api.ValueSyntaxSVG { - embedAttr := sx.MakeList( - te.symAttr, - sx.Cons(te.Make("type"), sx.MakeString("image/svg+xml")), - sx.Cons(te.Make("src"), sx.MakeString("/"+te.getString(ref.Tail()).String()+".svg")), - ) - return sx.MakeList( - te.Make("figure"), - sx.MakeList( - te.Make("embed"), - embedAttr, - ), - ) - } - a := te.getAttributes(args[0]) - a = a.Set("src", string(te.getString(ref.Tail().Car()))) - var sb strings.Builder - te.flattenText(&sb, ref.Tail().Tail().Tail()) - if d := sb.String(); d != "" { - a = a.Set("alt", d) - } - return sx.MakeList(te.Make("img"), te.transformAttribute(a)) - }) - te.bind(sz.NameSymEmbedBLOB, 3, func(args []sx.Object) sx.Object { - a, syntax, data := te.getAttributes(args[0]), te.getString(args[1]), te.getString(args[2]) - summary, _ := a.Get(api.KeySummary) - return te.transformBLOB( - sx.MakeList(te.astSF.MustMake(sz.NameSymInline), sx.MakeString(summary)), + inline = sx.Nil().Cons(ev.Eval(args[1], env)) + } + return inline.Cons(SymSPAN) + }) + evalHREF := func(args sx.Vector, env *Environment) sx.Object { + 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 := 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 := 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 := GetAttributes(args[0], env) + env.pushAttributes(a) + defer env.popAttributes() + refValue := getString(args[1], 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) + 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, EvaluateAttrbute(a)) + }) + ev.bind(sz.SymEmbedBLOB, 3, func(args sx.Vector, env *Environment) sx.Object { + 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 evalBLOB( + sx.MakeList(sxhtml.SymListSplice, sx.MakeString(summary)), syntax, data, ) }) - te.bind(sz.NameSymCite, 2, func(args []sx.Object) sx.Object { - result := sx.Nil() - if key := te.getString(args[1]); key != "" { - if len(args) > 2 { - result = sx.MakeList(args[2:]...).Cons(sx.MakeString(", ")) - } - result = result.Cons(key) - } - if a := te.getAttributes(args[0]); len(a) > 0 { - result = result.Cons(te.transformAttribute(a)) - } - if result == nil { - return nil - } - return result.Cons(te.symSpan) - }) - - te.bind(sz.NameSymMark, 3, func(args []sx.Object) sx.Object { - result := sx.MakeList(args[3:]...) - if !te.tr.noLinks { - if fragment := te.getString(args[2]); fragment != "" { - a := attrs.Attributes{"id": fragment.String() + te.tr.unique} - return result.Cons(te.transformAttribute(a)).Cons(te.symA) - } - } - return result.Cons(te.symSpan) - }) - - te.bind(sz.NameSymEndnote, 1, func(args []sx.Object) sx.Object { - attrPlist := sx.Nil() - if a := te.getAttributes(args[0]); len(a) > 0 { - if attrs := te.transformAttribute(a); attrs != nil { - attrPlist = attrs.Tail() - } - } - - text, isPair := sx.GetPair(args[1]) - if !isPair { - return sx.Nil() - } - te.tr.endnotes = append(te.tr.endnotes, endnoteInfo{noteAST: text, noteHx: nil, attrs: attrPlist}) - noteNum := strconv.Itoa(len(te.tr.endnotes)) - noteID := te.tr.unique + noteNum - hrefAttr := sx.Nil().Cons(sx.Cons(te.Make("role"), sx.MakeString("doc-noteref"))). - Cons(sx.Cons(te.Make("href"), sx.MakeString("#fn:"+noteID))). - Cons(sx.Cons(te.tr.symClass, sx.MakeString("zs-noteref"))). - Cons(te.symAttr) - href := sx.Nil().Cons(sx.MakeString(noteNum)).Cons(hrefAttr).Cons(te.symA) - supAttr := sx.Nil().Cons(sx.Cons(te.Make("id"), sx.MakeString("fnref:"+noteID))).Cons(te.symAttr) - return sx.Nil().Cons(href).Cons(supAttr).Cons(te.Make("sup")) - }) - - te.bind(sz.NameSymFormatDelete, 1, te.makeFormatFn("del")) - te.bind(sz.NameSymFormatEmph, 1, te.makeFormatFn("em")) - te.bind(sz.NameSymFormatInsert, 1, te.makeFormatFn("ins")) - te.bind(sz.NameSymFormatQuote, 1, te.transformQuote) - te.bind(sz.NameSymFormatSpan, 1, te.makeFormatFn("span")) - te.bind(sz.NameSymFormatStrong, 1, te.makeFormatFn("strong")) - te.bind(sz.NameSymFormatSub, 1, te.makeFormatFn("sub")) - te.bind(sz.NameSymFormatSuper, 1, te.makeFormatFn("sup")) - - te.bind(sz.NameSymLiteralComment, 1, func(args []sx.Object) sx.Object { - if te.getAttributes(args[0]).HasDefault() { - if len(args) > 1 { - if s := te.getString(args[1]); s != "" { - return sx.Nil().Cons(s).Cons(te.Make(sxhtml.NameSymInlineComment)) - } - } - } - return sx.Nil() - }) - te.bind(sz.NameSymLiteralHTML, 2, te.transformHTML) - kbdSym := te.Make("kbd") - te.bind(sz.NameSymLiteralInput, 2, func(args []sx.Object) sx.Object { - return te.transformLiteral(args, nil, kbdSym) - }) - codeSym := te.Make("code") - te.bind(sz.NameSymLiteralMath, 2, func(args []sx.Object) sx.Object { - a := te.getAttributes(args[0]).AddClass("zs-math") - return te.transformLiteral(args, a, codeSym) - }) - sampSym := te.Make("samp") - te.bind(sz.NameSymLiteralOutput, 2, func(args []sx.Object) sx.Object { - return te.transformLiteral(args, nil, sampSym) - }) - te.bind(sz.NameSymLiteralProg, 2, func(args []sx.Object) sx.Object { - return te.transformLiteral(args, nil, codeSym) - }) - - te.bind(sz.NameSymLiteralZettel, 0, func([]sx.Object) sx.Object { return sx.Nil() }) -} - -func (te *TransformEnv) makeFormatFn(tag string) transformFn { - sym := te.Make(tag) - return func(args []sx.Object) sx.Object { - a := te.getAttributes(args[0]) - if val, found := a.Get(""); found { - a = a.Remove("").AddClass(val) - } - res := sx.MakeList(args[1:]...) - if len(a) > 0 { - res = res.Cons(te.transformAttribute(a)) - } - return res.Cons(sym) - } -} -func (te *TransformEnv) transformQuote(args []sx.Object) sx.Object { - const langAttr = "lang" - a := te.getAttributes(args[0]) - langVal, found := a.Get(langAttr) - if found { - a = a.Remove(langAttr) - } - if val, found2 := a.Get(""); found2 { - a = a.Remove("").AddClass(val) - } - res := sx.MakeList(args[1:]...) - if len(a) > 0 { - res = res.Cons(te.transformAttribute(a)) - } - res = res.Cons(te.Make("q")) - if found { - res = sx.Nil().Cons(res).Cons(te.transformAttribute(attrs.Attributes{}.Set(langAttr, langVal))).Cons(te.symSpan) - } - return res -} - -var visibleReplacer = strings.NewReplacer(" ", "\u2423") - -func (te *TransformEnv) transformLiteral(args []sx.Object, a attrs.Attributes, sym *sx.Symbol) sx.Object { - if a == nil { - a = te.getAttributes(args[0]) - } - a = setProgLang(a) - literal := te.getString(args[1]).String() + ev.bind(sz.SymCite, 2, func(args sx.Vector, env *Environment) sx.Object { + 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 { + result = ev.evalSlice(args[2:], env).Cons(sx.MakeString(", ")) + } + result = result.Cons(key) + } + if len(a) > 0 { + result = result.Cons(EvaluateAttrbute(a)) + } + if result == nil { + return nil + } + return result.Cons(SymSPAN) + }) + ev.bind(sz.SymMark, 3, func(args sx.Vector, env *Environment) sx.Object { + result := ev.evalSlice(args[3:], env) + if !ev.noLinks { + if fragment := getString(args[2], env).GetValue(); fragment != "" { + a := attrs.Attributes{"id": fragment + ev.unique} + return result.Cons(EvaluateAttrbute(a)).Cons(SymA) + } + } + return result.Cons(SymSPAN) + }) + ev.bind(sz.SymEndnote, 1, func(args sx.Vector, env *Environment) sx.Object { + a := GetAttributes(args[0], env) + env.pushAttributes(a) + defer env.popAttributes() + attrPlist := sx.Nil() + if len(a) > 0 { + if attrs := EvaluateAttrbute(a); attrs != nil { + attrPlist = attrs.Tail() + } + } + + noteNum := strconv.Itoa(len(env.endnotes) + 1) + noteID := ev.unique + noteNum + env.endnotes = append(env.endnotes, endnoteInfo{ + noteID: noteID, noteAST: args[1:], noteHx: nil, attrs: attrPlist}) + hrefAttr := sx.Nil().Cons(sx.Cons(SymAttrRole, sx.MakeString("doc-noteref"))). + Cons(sx.Cons(SymAttrHref, sx.MakeString("#fn:"+noteID))). + Cons(sx.Cons(SymAttrClass, sx.MakeString("zs-noteref"))). + Cons(sxhtml.SymAttr) + href := sx.Nil().Cons(sx.MakeString(noteNum)).Cons(hrefAttr).Cons(SymA) + supAttr := sx.Nil().Cons(sx.Cons(SymAttrID, sx.MakeString("fnref:"+noteID))).Cons(sxhtml.SymAttr) + return sx.Nil().Cons(href).Cons(supAttr).Cons(symSUP) + }) + + ev.bind(sz.SymFormatDelete, 1, ev.makeFormatFn(symDEL)) + ev.bind(sz.SymFormatEmph, 1, ev.makeFormatFn(symEM)) + ev.bind(sz.SymFormatInsert, 1, ev.makeFormatFn(symINS)) + ev.bind(sz.SymFormatMark, 1, ev.makeFormatFn(symMARK)) + ev.bind(sz.SymFormatQuote, 1, ev.evalQuote) + ev.bind(sz.SymFormatSpan, 1, ev.makeFormatFn(SymSPAN)) + ev.bind(sz.SymFormatStrong, 1, ev.makeFormatFn(SymSTRONG)) + ev.bind(sz.SymFormatSub, 1, ev.makeFormatFn(symSUB)) + ev.bind(sz.SymFormatSuper, 1, ev.makeFormatFn(symSUP)) + + ev.bind(sz.SymLiteralComment, 1, func(args sx.Vector, env *Environment) sx.Object { + if GetAttributes(args[0], env).HasDefault() { + if len(args) > 1 { + if s := getString(ev.Eval(args[1], env), env); s.GetValue() != "" { + return sx.Nil().Cons(s).Cons(sxhtml.SymInlineComment) + } + } + } + return sx.Nil() + }) + ev.bind(sz.SymLiteralHTML, 2, ev.evalHTML) + ev.bind(sz.SymLiteralInput, 2, func(args sx.Vector, env *Environment) sx.Object { + return evalLiteral(args, nil, symKBD, env) + }) + ev.bind(sz.SymLiteralMath, 2, func(args sx.Vector, env *Environment) sx.Object { + 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 evalLiteral(args, nil, symSAMP, env) + }) + ev.bind(sz.SymLiteralProg, 2, func(args sx.Vector, env *Environment) sx.Object { + 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 := 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(EvaluateAttrbute(a)) + } + return res.Cons(sym) + } +} + +func (ev *Evaluator) evalQuote(args sx.Vector, env *Environment) sx.Object { + a := GetAttributes(args[0], env) + env.pushAttributes(a) + defer env.popAttributes() + + if val, hasClass := a.Get(""); hasClass { + a = a.Remove("").AddClass(val) + } + 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 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(EvaluateAttrbute(a)) + return res.Cons(SymSPAN) + } + return res.Cons(sxhtml.SymListSplice) +} + +var visibleReplacer = strings.NewReplacer(" ", "\u2423") + +func evalLiteral(args sx.Vector, a attrs.Attributes, sym *sx.Symbol, env *Environment) sx.Object { + if a == nil { + 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(te.transformAttribute(a)) + res = res.Cons(EvaluateAttrbute(a)) } return res.Cons(sym) } - func setProgLang(a attrs.Attributes) attrs.Attributes { if val, found := a.Get(""); found { a = a.AddClass("language-" + val).Remove("") } return a } -func (te *TransformEnv) transformHTML(args []sx.Object) sx.Object { - if s := te.getString(args[1]); s != "" && IsSafe(s.String()) { - return sx.Nil().Cons(s).Cons(te.symNoEscape) - } - return nil -} - -func (te *TransformEnv) transformBLOB(description *sx.Pair, syntax, data sx.String) sx.Object { - if data == "" { - return sx.Nil() - } - switch syntax { - case "": - return sx.Nil() - case api.ValueSyntaxSVG: - return sx.Nil().Cons(sx.Nil().Cons(data).Cons(te.symNoEscape)).Cons(te.symP) - default: - imgAttr := sx.Nil().Cons(sx.Cons(te.Make("src"), sx.MakeString("data:image/"+syntax.String()+";base64,"+data.String()))) - var sb strings.Builder - te.flattenText(&sb, description) - if d := sb.String(); d != "" { - imgAttr = imgAttr.Cons(sx.Cons(te.Make("alt"), sx.MakeString(d))) - } - return sx.Nil().Cons(sx.Nil().Cons(imgAttr.Cons(te.symAttr)).Cons(te.Make("img"))).Cons(te.symP) - } -} - -func (te *TransformEnv) flattenText(sb *strings.Builder, lst *sx.Pair) { - for elem := lst; elem != nil; elem = elem.Tail() { - switch obj := elem.Car().(type) { - case sx.String: - sb.WriteString(obj.String()) - case *sx.Pair: - te.flattenText(sb, obj) - } - } -} - -type transformFn func([]sx.Object) sx.Object - -func (te *TransformEnv) bind(name string, minArity int, fn transformFn) { - te.astEnv.Bind(te.astSF.MustMake(name), sxeval.BuiltinA(func(args []sx.Object) (sx.Object, error) { - if nArgs := len(args); nArgs < minArity { - return sx.Nil(), fmt.Errorf("not enough arguments (%d) for form %v (%d)", nArgs, name, minArity) - } - res := fn(args) - return res, te.err - })) -} - -func (te *TransformEnv) Rebind(name string, fn func([]sx.Object, sxeval.Callable) sx.Object) { - sym := te.astSF.MustMake(name) - obj, found := te.astEnv.Lookup(sym) - if !found { - panic(sym.String()) - } - preFn, ok := sxeval.GetCallable(obj) - if !ok { - panic(sym.String()) - } - te.astEnv.Bind(sym, sxeval.BuiltinA(func(args []sx.Object) (sx.Object, error) { - res := fn(args, preFn) - return res, te.err - })) -} - -func (te *TransformEnv) Make(name string) *sx.Symbol { return te.tr.Make(name) } -func (te *TransformEnv) getSymbol(val sx.Object) *sx.Symbol { - if te.err != nil { - return nil - } - if sym, ok := sx.GetSymbol(val); ok { - return sym - } - te.err = fmt.Errorf("%v/%T is not a symbol", val, val) - return nil -} -func (te *TransformEnv) getString(val sx.Object) sx.String { - if te.err != nil { - return "" +func (ev *Evaluator) evalHTML(args sx.Vector, env *Environment) sx.Object { + if s := getString(ev.Eval(args[1], env), env); s.GetValue() != "" && IsSafe(s.GetValue()) { + return sx.Nil().Cons(s).Cons(sxhtml.SymNoEscape) + } + return nil +} + +func evalBLOB(description *sx.Pair, syntax, data sx.String) sx.Object { + if data.GetValue() == "" { + return sx.Nil() + } + switch syntax.GetValue() { + case "": + return sx.Nil() + case api.ValueSyntaxSVG: + return sx.Nil().Cons(sx.Nil().Cons(data).Cons(sxhtml.SymNoEscape)).Cons(SymP) + default: + imgAttr := sx.Nil().Cons(sx.Cons(SymAttrSrc, sx.MakeString("data:image/"+syntax.GetValue()+";base64,"+data.GetValue()))) + var sb strings.Builder + flattenText(&sb, description) + if d := sb.String(); d != "" { + imgAttr = imgAttr.Cons(sx.Cons(symAttrAlt, sx.MakeString(d))) + } + return sx.Nil().Cons(sx.Nil().Cons(imgAttr.Cons(sxhtml.SymAttr)).Cons(SymIMG)).Cons(SymP) + } +} + +func flattenText(sb *strings.Builder, lst *sx.Pair) { + for elem := lst; elem != nil; elem = elem.Tail() { + switch obj := elem.Car().(type) { + case sx.String: + sb.WriteString(obj.GetValue()) + case *sx.Pair: + flattenText(sb, obj) + } + } +} + +func (ev *Evaluator) evalList(args sx.Vector, env *Environment) sx.Object { + return ev.evalSlice(args, env) +} +func nilFn(sx.Vector, *Environment) sx.Object { return sx.Nil() } + +// Eval evaluates an object in an environment. +func (ev *Evaluator) Eval(obj sx.Object, env *Environment) sx.Object { + if env.err != nil { + return sx.Nil() + } + if sx.IsNil(obj) { + return obj + } + lst, isLst := sx.GetPair(obj) + if !isLst { + return obj + } + sym, found := sx.GetSymbol(lst.Car()) + if !found { + env.err = fmt.Errorf("symbol expected, but got %T/%v", lst.Car(), lst.Car()) + return sx.Nil() + } + symVal := sym.GetValue() + fn, found := ev.fns[symVal] + if !found { + env.err = fmt.Errorf("symbol %q not bound", sym) + return sx.Nil() + } + var args sx.Vector + for cdr := lst.Cdr(); !sx.IsNil(cdr); { + pair, isPair := sx.GetPair(cdr) + if !isPair { + break + } + args = append(args, pair.Car()) + cdr = pair.Cdr() + } + if minArgs, hasMinArgs := ev.minArgs[symVal]; hasMinArgs { + if minArgs > len(args) { + env.err = fmt.Errorf("%v needs at least %d arguments, but got only %d", sym, minArgs, len(args)) + return sx.Nil() + } + } + result := fn(args, env) + if env.err != nil { + return sx.Nil() + } + return result +} + +func (ev *Evaluator) evalSlice(args sx.Vector, env *Environment) *sx.Pair { + var result sx.ListBuilder + for _, arg := range args { + elem := ev.Eval(arg, env) + result.Add(elem) + } + if env.err == nil { + return result.List() + } + return nil +} + +// 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) + } + if env.err == nil { + return result.List() + } + return nil +} + +func (ev *Evaluator) evalLink(a attrs.Attributes, refValue sx.String, inline sx.Vector, env *Environment) sx.Object { + result := ev.evalSlice(inline, env) + if len(inline) == 0 { + result = sx.Nil().Cons(refValue) + } + if ev.noLinks { + return result.Cons(SymSPAN) + } + return result.Cons(EvaluateAttrbute(a)).Cons(SymA) +} + +func getSymbol(obj sx.Object, env *Environment) *sx.Symbol { + if env.err == nil { + if sym, ok := sx.GetSymbol(obj); ok { + return sym + } + env.err = fmt.Errorf("%v/%T is not a symbol", obj, obj) + } + return sx.MakeSymbol("???") +} +func getString(val sx.Object, env *Environment) sx.String { + if env.err != nil { + return sx.String{} } if s, ok := sx.GetString(val); ok { return s } - te.err = fmt.Errorf("%v/%T is not a string", val, val) - return "" + env.err = fmt.Errorf("%v/%T is not a string", val, val) + return sx.String{} +} +func getList(val sx.Object, env *Environment) *sx.Pair { + if env.err == nil { + if res, isPair := sx.GetPair(val); isPair { + return res + } + env.err = fmt.Errorf("%v/%T is not a list", val, val) + } + return nil } -func (te *TransformEnv) getInt64(val sx.Object) int64 { - if te.err != nil { +func getInt64(val sx.Object, env *Environment) int64 { + if env.err != nil { return -1017 } if num, ok := sx.GetNumber(val); ok { return int64(num.(sx.Int64)) } - te.err = fmt.Errorf("%v/%T is not a number", val, val) - return -1017 -} -func (te *TransformEnv) getList(val sx.Object) *sx.Pair { - if te.err == nil { - if res, isPair := sx.GetPair(val); isPair { - return res - } - te.err = fmt.Errorf("%v/%T is not a list", val, val) - } - return nil -} -func (te *TransformEnv) getAttributes(args sx.Object) attrs.Attributes { - return sz.GetAttributes(te.getList(args)) -} - -func (te *TransformEnv) transformLink(a attrs.Attributes, refValue sx.String, inline []sx.Object) sx.Object { - result := sx.MakeList(inline...) - if len(inline) == 0 { - result = sx.Nil().Cons(refValue) - } - if te.tr.noLinks { - return result.Cons(te.symSpan) - } - return result.Cons(te.transformAttribute(a)).Cons(te.symA) -} - -func (te *TransformEnv) transformAttribute(a attrs.Attributes) *sx.Pair { - return te.tr.TransformAttrbute(a) -} - -func (te *TransformEnv) transformMeta(a attrs.Attributes) *sx.Pair { - return te.tr.TransformMeta(a) + env.err = fmt.Errorf("%v/%T is not a number", val, val) + return -1017 +} + +// GetAttributes evaluates the given arg in the given environment and returns +// the contained attributes. +func GetAttributes(arg sx.Object, env *Environment) attrs.Attributes { + return sz.GetAttributes(getList(arg, env)) } var unsafeSnippets = []string{ " 0 { return result } return nil } func doMakeMeta(obj sx.Object) Meta { - result := make(map[string]MetaValue) - for { - if sx.IsNil(obj) { - return result - } - pair, isPair := sx.GetPair(obj) - if !isPair { - return result - } - if mv, ok2 := makeMetaValue(pair); ok2 { - result[mv.Key] = mv - } - obj = pair.Cdr() - } -} -func makeMetaValue(mnode *sx.Pair) (MetaValue, bool) { - var result MetaValue - mval, isPair := sx.GetPair(mnode.Car()) - if !isPair { - return result, false - } - typeSym, isSymbol := sx.GetSymbol(mval.Car()) - if !isSymbol { - return result, false - } - keyPair, isPair := sx.GetPair(mval.Cdr()) - if !isPair { - return result, false - } - keyList, isPair := sx.GetPair(keyPair.Car()) - if !isPair { - return result, false - } - quoteSym, isSymbol := sx.GetSymbol(keyList.Car()) - if !isSymbol || quoteSym.Name() != "quote" { - return result, false - } - keySym, isSymbol := sx.GetSymbol(keyList.Tail().Car()) - if !isSymbol { - return result, false - } - valPair, isPair := sx.GetPair(keyPair.Cdr()) - if !isPair { - return result, false - } - result.Type = typeSym.Name() - result.Key = keySym.Name() - result.Value = valPair.Car() - return result, true -} - -func (m Meta) GetString(key string) string { - if v, found := m[key]; found { - return v.Value.String() + lst, isList := sx.GetPair(obj) + if !isList || !lst.Car().IsEqual(SymMeta) { + return nil + } + result := make(map[string]MetaValue) + for node := lst.Tail(); node != nil; node = node.Tail() { + if mv, found := makeMetaValue(node.Head()); found { + result[mv.Key] = mv + } + } + return result +} +func makeMetaValue(mnode *sx.Pair) (MetaValue, bool) { + var result MetaValue + typeSym, isSymbol := sx.GetSymbol(mnode.Car()) + if !isSymbol { + return result, false + } + next := mnode.Tail() + keySym, isSymbol := sx.GetSymbol(next.Car()) + if !isSymbol { + return result, false + } + next = next.Tail() + result.Type = typeSym.GetValue() + result.Key = keySym.GetValue() + result.Value = next.Car() + return result, true +} + +// 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 } } return nil } + +// MapRefStateToLinkEmbed maps a reference state symbol to a link symbol or to +// an embed symbol, depending on 'forLink'. +func MapRefStateToLinkEmbed(symRefState *sx.Symbol, forLink bool) *sx.Symbol { + if !forLink { + return SymEmbed + } + if sym, found := mapRefStateLink[symRefState]; found { + return sym + } + return SymLinkInvalid +} + +var mapRefStateLink = map[*sx.Symbol]*sx.Symbol{ + SymRefStateInvalid: SymLinkInvalid, + SymRefStateZettel: SymLinkZettel, + SymRefStateSelf: SymLinkSelf, + SymRefStateFound: SymLinkFound, + SymRefStateBroken: SymLinkBroken, + SymRefStateHosted: SymLinkHosted, + SymRefStateBased: SymLinkBased, + SymRefStateQuery: SymLinkQuery, + SymRefStateExternal: SymLinkExternal, +} + +// IsBreakSym return true if the object is either a soft or a hard break symbol. +func IsBreakSym(obj sx.Object) bool { + return SymSoft.IsEqual(obj) || SymHard.IsEqual(obj) +} ADDED sz/walk.go Index: sz/walk.go ================================================================== --- /dev/null +++ sz/walk.go @@ -0,0 +1,218 @@ +//----------------------------------------------------------------------------- +// Copyright (c) 2024-present Detlef Stern +// +// This file is part of zettelstore-client. +// +// Zettelstore client is licensed under the latest version of the EUPL +// (European Union Public License). Please see file LICENSE.txt for your rights +// and obligations under this license. +// +// SPDX-License-Identifier: EUPL-1.2 +// SPDX-FileCopyrightText: 2024-present Detlef Stern +//----------------------------------------------------------------------------- + +package sz + +import "t73f.de/r/sx" + +// Visitor is walking the sx-based AST. +type Visitor interface { + Visit(node *sx.Pair, env *sx.Pair) sx.Object +} + +// Walk a sx-based AST through a Visitor. +func Walk(v Visitor, node *sx.Pair, env *sx.Pair) *sx.Pair { + if node == nil { + return nil + } + if result, isPair := sx.GetPair(v.Visit(node, env)); isPair { + return result + } + + if sym, isSymbol := sx.GetSymbol(node.Car()); isSymbol { + if fn, found := mapChildrenWalk[sym]; found { + return fn(v, node, env) + } + return node + } + panic(node) +} + +var mapChildrenWalk map[*sx.Symbol]func(Visitor, *sx.Pair, *sx.Pair) *sx.Pair + +func init() { + mapChildrenWalk = map[*sx.Symbol]func(Visitor, *sx.Pair, *sx.Pair) *sx.Pair{ + SymBlock: walkChildrenTail, + SymPara: walkChildrenTail, + SymRegionBlock: walkChildrenRegion, + SymRegionQuote: walkChildrenRegion, + SymRegionVerse: walkChildrenRegion, + SymHeading: walkChildrenHeading, + SymListOrdered: walkChildrenTail, + SymListUnordered: walkChildrenTail, + SymListQuote: walkChildrenTail, + SymDescription: walkChildrenDescription, + SymTable: walkChildrenTable, + + SymInline: walkChildrenTail, + SymEndnote: walkChildrenInlines3, + SymMark: walkChildrenMark, + SymLinkBased: walkChildrenInlines4, + SymLinkBroken: walkChildrenInlines4, + SymLinkExternal: walkChildrenInlines4, + SymLinkFound: walkChildrenInlines4, + SymLinkHosted: walkChildrenInlines4, + SymLinkInvalid: walkChildrenInlines4, + SymLinkQuery: walkChildrenInlines4, + SymLinkSelf: walkChildrenInlines4, + SymLinkZettel: walkChildrenInlines4, + SymEmbed: walkChildrenEmbed, + SymCite: walkChildrenInlines4, + SymFormatDelete: walkChildrenInlines3, + SymFormatEmph: walkChildrenInlines3, + SymFormatInsert: walkChildrenInlines3, + SymFormatMark: walkChildrenInlines3, + SymFormatQuote: walkChildrenInlines3, + SymFormatStrong: walkChildrenInlines3, + SymFormatSpan: walkChildrenInlines3, + SymFormatSub: walkChildrenInlines3, + SymFormatSuper: walkChildrenInlines3, + } +} + +func walkChildrenTail(v Visitor, node *sx.Pair, env *sx.Pair) *sx.Pair { + hasNil := false + for n := node.Tail(); n != nil; n = n.Tail() { + obj := Walk(v, n.Head(), env) + if sx.IsNil(obj) { + hasNil = true + } + n.SetCar(obj) + } + if !hasNil { + return node + } + for n := node; ; { + next := n.Tail() + if next == nil { + break + } + if sx.IsNil(next.Car()) { + n.SetCdr(next.Cdr()) + continue + } + n = next + } + return node +} + +func walkChildrenList(v Visitor, lst *sx.Pair, env *sx.Pair) *sx.Pair { + hasNil := false + for n := lst; n != nil; n = n.Tail() { + obj := Walk(v, n.Head(), env) + if sx.IsNil(obj) { + hasNil = true + } + n.SetCar(obj) + } + if !hasNil { + return lst + } + var result sx.ListBuilder + for n := lst; n != nil; n = n.Tail() { + obj := n.Car() + if !sx.IsNil(obj) { + result.Add(obj) + } + } + return result.List() +} + +func walkChildrenRegion(v Visitor, node *sx.Pair, env *sx.Pair) *sx.Pair { + // sym := node.Car() + next := node.Tail() + // attrs := next.Car() + next = next.Tail() + next.SetCar(walkChildrenList(v, next.Head(), env)) + next.SetCdr(walkChildrenList(v, next.Tail(), env)) + return node +} + +func walkChildrenHeading(v Visitor, node *sx.Pair, env *sx.Pair) *sx.Pair { + // sym := node.Car() + next := node.Tail() + // level := next.Car() + next = next.Tail() + // attrs := next.Car() + next = next.Tail() + // slug := next.Car() + next = next.Tail() + // fragment := next.Car() + next.SetCdr(walkChildrenList(v, next.Tail(), env)) + return node +} + +func walkChildrenDescription(v Visitor, dn *sx.Pair, env *sx.Pair) *sx.Pair { + for n := dn.Tail(); n != nil; n = n.Tail() { + n.SetCar(walkChildrenList(v, n.Head(), env)) + n = n.Tail() + if n == nil { + break + } + n.SetCar(Walk(v, n.Head(), env)) + } + return dn +} + +func walkChildrenTable(v Visitor, tn *sx.Pair, env *sx.Pair) *sx.Pair { + for row := tn.Tail(); row != nil; row = row.Tail() { + row.SetCar(walkChildrenList(v, row.Head(), env)) + } + return tn +} + +func walkChildrenMark(v Visitor, mn *sx.Pair, env *sx.Pair) *sx.Pair { + // sym := mn.Car() + next := mn.Tail() + // mark := next.Car() + next = next.Tail() + // slug := next.Car() + 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() + next = next.Tail() + // val3 := next.Car() + next.SetCdr(walkChildrenList(v, next.Tail(), env)) + return ln +} + +func walkChildrenInlines3(v Visitor, node *sx.Pair, env *sx.Pair) *sx.Pair { + // sym := node.Car() + next := node.Tail() // Attrs + // attrs := next.Car() + next.SetCdr(walkChildrenList(v, next.Tail(), env)) + return node +} ADDED sz/zmk/block.go Index: sz/zmk/block.go ================================================================== --- /dev/null +++ sz/zmk/block.go @@ -0,0 +1,721 @@ +//----------------------------------------------------------------------------- +// Copyright (c) 2020-present Detlef Stern +// +// This file is part of zettelstore-client. +// +// Zettelstore client is licensed under the latest version of the EUPL +// (European Union Public License). Please see file LICENSE.txt for your rights +// and obligations under this license. +// +// SPDX-License-Identifier: EUPL-1.2 +// SPDX-FileCopyrightText: 2020-present Detlef Stern +//----------------------------------------------------------------------------- + +package zmk + +import ( + "fmt" + + "t73f.de/r/sx" + "t73f.de/r/zsc/input" + "t73f.de/r/zsc/sz" +) + +// parseBlock parses one block. +func (cp *zmkP) parseBlock(lastPara *sx.Pair) (res *sx.Pair, cont bool) { + inp := cp.inp + pos := inp.Pos + if cp.nestingLevel <= maxNestingLevel { + cp.nestingLevel++ + defer func() { cp.nestingLevel-- }() + + var bn *sx.Pair + success := false + + switch inp.Ch { + case input.EOS: + return nil, false + case '\n', '\r': + inp.EatEOL() + cp.cleanupListsAfterEOL() + return nil, false + case ':': + bn, success = cp.parseColon() + case '@', '`', runeModGrave, '%', '~', '$': + cp.clearStacked() + bn, success = cp.parseVerbatim() + case '"', '<': + cp.clearStacked() + bn, success = cp.parseRegion() + case '=': + cp.clearStacked() + bn, success = cp.parseHeading() + case '-': + cp.clearStacked() + bn, success = cp.parseHRule() + case '*', '#', '>': + cp.lastRow = nil + cp.descrl = nil + bn, success = cp.parseNestedList() + case ';': + cp.lists = nil + cp.lastRow = nil + bn, success = cp.parseDefTerm() + case ' ': + cp.lastRow = nil + bn, success = nil, cp.parseIndent() + case '|': + cp.lists = nil + cp.descrl = nil + bn, success = cp.parseRow(), true + case '{': + cp.clearStacked() + bn, success = cp.parseTransclusion() + } + + if success { + return bn, false + } + } + inp.SetPos(pos) + cp.clearStacked() + ins := cp.parsePara() + if startsWithSpaceSoftBreak(ins) { + ins = ins[2:] + } else if lastPara != nil { + lastPair := lastPara.LastPair() + lastPair.ExtendBang(sx.MakeList(ins...)) + return nil, true + } + return sx.MakeList(ins...).Cons(sz.SymPara), false +} + +func startsWithSpaceSoftBreak(ins sx.Vector) bool { + if len(ins) < 2 { + return false + } + pair0, isPair0 := sx.GetPair(ins[0]) + pair1, isPair1 := sx.GetPair(ins[0]) + if !isPair0 || !isPair1 { + return false + } + if pair0.Car().IsEqual(sz.SymText) && sz.IsBreakSym(pair1.Car()) { + if args := pair0.Tail(); args != nil { + if val, isString := sx.GetString(args.Car()); isString { + for _, ch := range val.GetValue() { + if !input.IsSpace(ch) { + return false + } + } + return true + } + } + } + return false +} + +var symSeparator = sx.MakeSymbol("sEpArAtOr") + +func (cp *zmkP) cleanupListsAfterEOL() { + for _, l := range cp.lists { + l.LastPair().Head().LastPair().AppendBang(sx.Cons(symSeparator, nil)) + } + if descrl := cp.descrl; descrl != nil { + if lastPair, pos := lastPairPos(descrl); pos > 1 && pos%2 == 0 { + lastPair.Head().LastPair().AppendBang(sx.Cons(symSeparator, nil)) + } + } +} + +// parseColon determines which element should be parsed. +func (cp *zmkP) parseColon() (*sx.Pair, bool) { + inp := cp.inp + if inp.PeekN(1) == ':' { + cp.clearStacked() + return cp.parseRegion() + } + return cp.parseDefDescr() +} + +// parsePara parses paragraphed inline material as a sx.Vector. +func (cp *zmkP) parsePara() (result sx.Vector) { + for { + in := cp.parseInline() + if in == nil { + return result + } + result = append(result, in) + if sz.IsBreakSym(in.Car()) { + ch := cp.inp.Ch + switch ch { + // Must contain all cases from above switch in parseBlock. + case input.EOS, '\n', '\r', '@', '`', runeModGrave, '%', '~', '$', '"', '<', '=', '-', '*', '#', '>', ';', ':', ' ', '|', '{': + return result + } + } + } +} + +// countDelim read from input until a non-delimiter is found and returns number of delimiter chars. +func (cp *zmkP) countDelim(delim rune) int { + inp := cp.inp + cnt := 0 + for inp.Ch == delim { + cnt++ + inp.Next() + } + return cnt +} + +// parseVerbatim parses a verbatim block. +func (cp *zmkP) parseVerbatim() (rn *sx.Pair, success bool) { + inp := cp.inp + fch := inp.Ch + cnt := cp.countDelim(fch) + if cnt < 3 { + return nil, false + } + attrs := cp.parseBlockAttributes() + inp.SkipToEOL() + if inp.Ch == input.EOS { + return nil, false + } + var sym *sx.Symbol + switch fch { + case '@': + sym = sz.SymVerbatimZettel + case '`', runeModGrave: + sym = sz.SymVerbatimProg + case '%': + sym = sz.SymVerbatimComment + case '~': + sym = sz.SymVerbatimEval + case '$': + sym = sz.SymVerbatimMath + default: + panic(fmt.Sprintf("%q is not a verbatim char", fch)) + } + content := make([]byte, 0, 512) + for { + inp.EatEOL() + posL := inp.Pos + switch inp.Ch { + case fch: + if cp.countDelim(fch) >= cnt { + inp.SkipToEOL() + rn = sx.MakeList(sym, attrs, sx.MakeString(string(content))) + return rn, true + } + inp.SetPos(posL) + case input.EOS: + return nil, false + } + inp.SkipToEOL() + if len(content) > 0 { + content = append(content, '\n') + } + content = append(content, inp.Src[posL:inp.Pos]...) + } +} + +// parseRegion parses a block region. +func (cp *zmkP) parseRegion() (rn *sx.Pair, success bool) { + inp := cp.inp + fch := inp.Ch + cnt := cp.countDelim(fch) + if cnt < 3 { + return nil, false + } + + var sym *sx.Symbol + oldInVerse := cp.inVerse + defer func() { cp.inVerse = oldInVerse }() + switch fch { + case ':': + sym = sz.SymRegionBlock + case '<': + sym = sz.SymRegionQuote + case '"': + sym = sz.SymRegionVerse + cp.inVerse = true + default: + panic(fmt.Sprintf("%q is not a region char", fch)) + } + attrs := cp.parseBlockAttributes() + inp.SkipToEOL() + if inp.Ch == input.EOS { + return nil, false + } + var blocksBuilder sx.ListBuilder + var lastPara *sx.Pair + inp.EatEOL() + for { + posL := inp.Pos + switch inp.Ch { + case fch: + if cp.countDelim(fch) >= cnt { + ins := cp.parseRegionLastLine() + rn = ins.Cons(blocksBuilder.List()).Cons(attrs).Cons(sym) + return rn, true + } + inp.SetPos(posL) + case input.EOS: + return nil, false + } + bn, cont := cp.parseBlock(lastPara) + if bn != nil { + blocksBuilder.Add(bn) + } + if !cont { + lastPara = bn + } + } +} + +// 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 + inp.SkipSpace() + var region sx.ListBuilder + for { + switch inp.Ch { + case input.EOS, '\n', '\r': + return region.List() + } + in := cp.parseInline() + if in == nil { + return region.List() + } + region.Add(in) + } +} + +// parseHeading parses a head line. +func (cp *zmkP) parseHeading() (hn *sx.Pair, success bool) { + inp := cp.inp + delims := cp.countDelim(inp.Ch) + if delims < 3 { + return nil, false + } + if inp.Ch != ' ' { + return nil, false + } + inp.Next() + inp.SkipSpace() + if delims > 7 { + delims = 7 + } + level := int64(delims - 2) + var attrs *sx.Pair + var text sx.ListBuilder + for { + if input.IsEOLEOS(inp.Ch) { + return createHeading(level, attrs, text.List()), true + } + in := cp.parseInline() + if in == nil { + return createHeading(level, attrs, text.List()), true + } + text.Add(in) + if inp.Ch == '{' && inp.Peek() != '{' { + attrs = cp.parseBlockAttributes() + inp.SkipToEOL() + return createHeading(level, attrs, text.List()), true + } + } +} +func createHeading(level int64, attrs, text *sx.Pair) *sx.Pair { + return text. + Cons(sx.MakeString("")). // Fragment + Cons(sx.MakeString("")). // Slug + Cons(attrs). + Cons(sx.Int64(level)). + Cons(sz.SymHeading) +} + +// parseHRule parses a horizontal rule. +func (cp *zmkP) parseHRule() (hn *sx.Pair, success bool) { + inp := cp.inp + if cp.countDelim(inp.Ch) < 3 { + return nil, false + } + + attrs := cp.parseBlockAttributes() + inp.SkipToEOL() + return sx.MakeList(sz.SymThematic, attrs), true +} + +// parseNestedList parses a list. +func (cp *zmkP) parseNestedList() (res *sx.Pair, success bool) { + kinds := cp.parseNestedListKinds() + if len(kinds) == 0 { + return nil, false + } + 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)] + } + ln, newLnCount := cp.buildNestedList(kinds) + pv := cp.parseLinePara() + bn := sx.Cons(sz.SymBlock, nil) + if len(pv) != 0 { + bn.AppendBang(pv.MakeList().Cons(sz.SymPara)) + } + lastItemPair := ln.LastPair() + lastItemPair.AppendBang(bn) + return cp.cleanupParsedNestedList(newLnCount) +} + +func (cp *zmkP) parseNestedListKinds() []*sx.Symbol { + inp := cp.inp + result := make([]*sx.Symbol, 0, 8) + for { + var sym *sx.Symbol + switch inp.Ch { + case '*': + sym = sz.SymListUnordered + case '#': + sym = sz.SymListOrdered + case '>': + sym = sz.SymListQuote + default: + panic(fmt.Sprintf("%q is not a region char", inp.Ch)) + } + result = append(result, sym) + switch inp.Next() { + case '*', '#', '>': + case ' ', input.EOS, '\n', '\r': + return result + default: + return nil + } + } +} + +func (cp *zmkP) buildNestedList(kinds []*sx.Symbol) (ln *sx.Pair, newLnCount int) { + for i, kind := range kinds { + if i < len(cp.lists) { + if !cp.lists[i].Car().IsEqual(kind) { + ln = sx.Cons(kind, nil) + newLnCount++ + cp.lists[i] = ln + cp.lists = cp.lists[:i+1] + } else { + ln = cp.lists[i] + } + } else { + ln = sx.Cons(kind, nil) + newLnCount++ + cp.lists = append(cp.lists, ln) + } + } + return ln, newLnCount +} + +func (cp *zmkP) cleanupParsedNestedList(newLnCount int) (res *sx.Pair, success bool) { + childPos := len(cp.lists) - 1 + parentPos := childPos - 1 + for i := 0; i < newLnCount; i++ { + if parentPos < 0 { + return cp.lists[0], true + } + parentLn := cp.lists[parentPos] + childLn := cp.lists[childPos] + if firstParent := parentLn.Tail(); firstParent != nil { + // Add list to last item of the parent list + lastParent := firstParent.LastPair() + lastParent.Head().LastPair().AppendBang(childLn) + } else { + // Set list to first child of parent. + childBlock := sx.MakeList(sz.SymBlock, cp.lists[childPos]) + parentLn.LastPair().AppendBang(childBlock) + } + childPos-- + parentPos-- + } + return nil, true +} + +// parseDefTerm parses a term of a definition list. +func (cp *zmkP) parseDefTerm() (res *sx.Pair, success bool) { + inp := cp.inp + if inp.Next() != ' ' { + return nil, false + } + inp.Next() + inp.SkipSpace() + descrl := cp.descrl + if descrl == nil { + descrl = sx.Cons(sz.SymDescription, nil) + cp.descrl = descrl + res = descrl + } + lastPair, pos := lastPairPos(descrl) + for { + in := cp.parseInline() + if in == nil { + if pos%2 == 0 { + // lastPair is either the empty description list or the last block of definitions + return nil, false + } + // lastPair is the definition term + return res, true + } + if pos%2 == 0 { + // lastPair is either the empty description list or the last block of definitions + lastPair = lastPair.AppendBang(sx.Cons(in, nil)) + pos++ + } else { + // lastPair is the term part and we need to append the inline list just read + lastPair.Head().LastPair().AppendBang(in) + } + if sz.IsBreakSym(in.Car()) { + return res, true + } + } +} + +// parseDefDescr parses a description of a definition list. +func (cp *zmkP) parseDefDescr() (res *sx.Pair, success bool) { + inp := cp.inp + if inp.Next() != ' ' { + return nil, false + } + inp.Next() + inp.SkipSpace() + descrl := cp.descrl + lastPair, pos := lastPairPos(descrl) + if descrl == nil || pos <= 0 { + // No term given + return nil, false + } + + pn := cp.parseLinePara() + if len(pn) == 0 { + return nil, false + } + + newDef := sx.MakeList(sz.SymBlock, pn.MakeList().Cons(sz.SymPara)) + if pos%2 == 1 { + // Just a term, but no definitions + lastPair.AppendBang(sx.MakeList(sz.SymBlock, newDef)) + } else { + // lastPara points a the last definition + lastPair.Head().LastPair().AppendBang(newDef) + } + return nil, true +} + +func lastPairPos(p *sx.Pair) (*sx.Pair, int) { + cnt := 0 + for node := p; node != nil; { + next := node.Tail() + if next == nil { + return node, cnt + } + node = next + cnt++ + } + return nil, -1 +} + +// parseIndent parses initial spaces to continue a list. +func (cp *zmkP) parseIndent() bool { + inp := cp.inp + cnt := 0 + for { + if inp.Next() != ' ' { + break + } + cnt++ + } + if cp.lists != nil { + return cp.parseIndentForList(cnt) + } + if cp.descrl != nil { + return cp.parseIndentForDescription(cnt) + } + return false +} + +func (cp *zmkP) parseIndentForList(cnt int) bool { + if len(cp.lists) < cnt { + cnt = len(cp.lists) + } + cp.lists = cp.lists[:cnt] + if cnt == 0 { + return false + } + pv := cp.parseLinePara() + if len(pv) == 0 { + return false + } + ln := cp.lists[cnt-1] + lbn := ln.LastPair().Head() + lpn := lbn.LastPair().Head() + pvList := pv.MakeList() + if lpn.Car().IsEqual(sz.SymPara) { + lpn.LastPair().SetCdr(pvList) + } else { + lbn.LastPair().AppendBang(pvList.Cons(sz.SymPara)) + } + return true +} + +func (cp *zmkP) parseIndentForDescription(cnt int) bool { + descrl := cp.descrl + lastPair, pos := lastPairPos(descrl) + if cnt < 1 || pos < 1 { + return false + } + if pos%2 == 1 { + // Continuation of a definition term + for { + in := cp.parseInline() + if in == nil { + return true + } + lastPair.Head().LastPair().AppendBang(in) + if sz.IsBreakSym(in.Car()) { + return true + } + } + } + + // Continuation of a definition description + pn := cp.parseLinePara() + if len(pn) == 0 { + return false + } + bn := lastPair.Head() + para := bn.LastPair().Head().LastPair().Head() + pnList := pn.MakeList() + if para.Car().IsEqual(sz.SymPara) { + para.LastPair().SetCdr(pnList) + } else { + bn.LastPair().AppendBang(pnList.Cons(sz.SymPara)) + } + return true +} + +// parseLinePara parses one paragraph of inline material. +func (cp *zmkP) parseLinePara() sx.Vector { + var ins sx.Vector + for { + in := cp.parseInline() + if in == nil { + return ins + } + ins = append(ins, in) + if sz.IsBreakSym(in.Car()) { + return ins + } + } +} + +// parseRow parse one table row. +func (cp *zmkP) parseRow() *sx.Pair { + inp := cp.inp + if inp.Peek() == '%' { + inp.SkipToEOL() + return nil + } + //var row, curr *sx.Pair + var row sx.ListBuilder + for { + inp.Next() + cell := cp.parseCell() + if cell != nil { + row.Add(cell) + } + switch inp.Ch { + case '\n', '\r': + inp.EatEOL() + fallthrough + case input.EOS: + // add to table + if cp.lastRow == nil { + if row.IsEmpty() { + return nil + } + cp.lastRow = sx.Cons(row.List(), nil) + return cp.lastRow.Cons(nil).Cons(sz.SymTable) + } + cp.lastRow = cp.lastRow.AppendBang(row.List()) + return nil + } + // inp.Ch must be '|' + } +} + +// parseCell parses one single cell of a table row. +func (cp *zmkP) parseCell() *sx.Pair { + inp := cp.inp + var cell sx.ListBuilder + for { + if input.IsEOLEOS(inp.Ch) { + if cell.IsEmpty() { + return nil + } + return cell.List().Cons(sz.SymCell) + } + if inp.Ch == '|' { + return cell.List().Cons(sz.SymCell) + } + + in := cp.parseInline() + cell.Add(in) + } +} + +// parseTransclusion parses '{' '{' '{' ZID '}' '}' '}' +func (cp *zmkP) parseTransclusion() (*sx.Pair, bool) { + if cp.countDelim('{') != 3 { + return nil, false + } + inp := cp.inp + posA, posE := inp.Pos, 0 + +loop: + + for { + switch inp.Ch { + case input.EOS: + return nil, false + case '\n', '\r', ' ', '\t': + if !hasQueryPrefix(inp.Src[posA:]) { + return nil, false + } + case '\\': + switch inp.Next() { + case input.EOS, '\n', '\r': + return nil, false + } + case '}': + posE = inp.Pos + if posA >= posE { + return nil, false + } + if inp.Next() != '}' { + continue + } + if inp.Next() != '}' { + continue + } + break loop + } + inp.Next() + } + inp.Next() // consume last '}' + a := cp.parseBlockAttributes() + inp.SkipToEOL() + refText := string(inp.Src[posA:posE]) + ref := ParseReference(refText) + return sx.MakeList(sz.SymTransclude, a, ref), true +} ADDED sz/zmk/inline.go Index: sz/zmk/inline.go ================================================================== --- /dev/null +++ sz/zmk/inline.go @@ -0,0 +1,513 @@ +//----------------------------------------------------------------------------- +// Copyright (c) 2020-present Detlef Stern +// +// This file is part of zettelstore-client. +// +// Zettelstore client is licensed under the latest version of the EUPL +// (European Union Public License). Please see file LICENSE.txt for your rights +// and obligations under this license. +// +// SPDX-License-Identifier: EUPL-1.2 +// SPDX-FileCopyrightText: 2020-present Detlef Stern +//----------------------------------------------------------------------------- + +package zmk + +import ( + "fmt" + "strings" + + "t73f.de/r/sx" + "t73f.de/r/zsc/api" + "t73f.de/r/zsc/input" + "t73f.de/r/zsc/sz" +) + +func (cp *zmkP) parseInline() *sx.Pair { + inp := cp.inp + pos := inp.Pos + if cp.nestingLevel <= maxNestingLevel { + cp.nestingLevel++ + defer func() { cp.nestingLevel-- }() + + var in *sx.Pair + success := false + switch inp.Ch { + case input.EOS: + return nil + case '\n', '\r': + return cp.parseSoftBreak() + case '[': + switch inp.Next() { + case '[': + in, success = cp.parseLinkEmbed('[', ']', true) + case '@': + in, success = cp.parseCite() + case '^': + in, success = cp.parseEndnote() + case '!': + in, success = cp.parseMark() + } + case '{': + if inp.Next() == '{' { + in, success = cp.parseLinkEmbed('{', '}', false) + } + case '%': + in, success = cp.parseComment() + case '_', '*', '>', '~', '^', ',', '"', '#', ':': + in, success = cp.parseFormat() + case '@', '\'', '`', '=', runeModGrave: + in, success = cp.parseLiteral() + case '$': + in, success = cp.parseLiteralMath() + case '\\': + return cp.parseBackslash() + case '-': + in, success = cp.parseNdash() + case '&': + in, success = cp.parseEntity() + } + if success { + return in + } + } + inp.SetPos(pos) + return cp.parseText() +} + +func (cp *zmkP) parseText() *sx.Pair { + return sx.MakeList(sz.SymText, cp.parseString()) +} + +func (cp *zmkP) parseString() sx.String { + inp := cp.inp + pos := inp.Pos + if inp.Ch == '\\' { + cp.inp.Next() + return cp.parseBackslashRest() + } + for { + switch inp.Next() { + // The following case must contain all runes that occur in parseInline! + // Plus the closing brackets ] and } and ) and the middle | + case input.EOS, '\n', '\r', '[', ']', '{', '}', '(', ')', '|', '%', '_', '*', '>', '~', '^', ',', '"', '#', ':', '\'', '@', '`', runeModGrave, '$', '=', '\\', '-', '&': + return sx.MakeString(string(inp.Src[pos:inp.Pos])) + } + } +} + +func (cp *zmkP) parseBackslash() *sx.Pair { + inp := cp.inp + switch inp.Next() { + case '\n', '\r': + inp.EatEOL() + return sx.MakeList(sz.SymHard) + default: + return sx.MakeList(sz.SymText, cp.parseBackslashRest()) + } +} + +func (cp *zmkP) parseBackslashRest() sx.String { + inp := cp.inp + if input.IsEOLEOS(inp.Ch) { + return sx.MakeString("\\") + } + if inp.Ch == ' ' { + inp.Next() + return sx.MakeString("\u00a0") + } + pos := inp.Pos + inp.Next() + return sx.MakeString(string(inp.Src[pos:inp.Pos])) +} + +func (cp *zmkP) parseSoftBreak() *sx.Pair { + cp.inp.EatEOL() + return sx.MakeList(sz.SymSoft) +} + +func (cp *zmkP) parseLinkEmbed(openCh, closeCh rune, forLink bool) (*sx.Pair, bool) { + if refString, text, ok := cp.parseReference(openCh, closeCh); ok { + attrs := cp.parseInlineAttributes() + if len(refString) > 0 { + ref := ParseReference(refString) + refSym, _ := sx.GetSymbol(ref.Car()) + sym := sz.MapRefStateToLinkEmbed(refSym, forLink) + ln := text. + Cons(ref.Tail().Car()). // reference value + Cons(attrs). + Cons(sym) + return ln, true + } + } + return nil, false +} + +func hasQueryPrefix(src []byte) bool { + return len(src) > len(api.QueryPrefix) && string(src[:len(api.QueryPrefix)]) == api.QueryPrefix +} + +func (cp *zmkP) parseReference(openCh, closeCh rune) (ref string, text *sx.Pair, _ bool) { + inp := cp.inp + inp.Next() + inp.SkipSpace() + if inp.Ch == openCh { + // Additional opening chars result in a fail + return "", nil, false + } + var is sx.Vector + pos := inp.Pos + if !hasQueryPrefix(inp.Src[pos:]) { + hasSpace, ok := cp.readReferenceToSep(closeCh) + if !ok { + return "", nil, false + } + if inp.Ch == '|' { // First part must be inline text + if pos == inp.Pos { // [[| or {{| + return "", nil, false + } + cp.inp = input.NewInput(inp.Src[pos:inp.Pos]) + for { + in := cp.parseInline() + if in == nil { + break + } + is = append(is, in) + } + cp.inp = inp + inp.Next() + } else { + if hasSpace { + return "", nil, false + } + inp.SetPos(pos) + } + } + + inp.SkipSpace() + pos = inp.Pos + if !cp.readReferenceToClose(closeCh) { + return "", nil, false + } + ref = strings.TrimSpace(string(inp.Src[pos:inp.Pos])) + if inp.Next() != closeCh { + return "", nil, false + } + inp.Next() + if len(is) == 0 { + return ref, nil, true + } + return ref, sx.MakeList(is...), true +} + +func (cp *zmkP) readReferenceToSep(closeCh rune) (bool, bool) { + hasSpace := false + inp := cp.inp + for { + switch inp.Ch { + case input.EOS: + return false, false + case '\n', '\r', ' ': + hasSpace = true + case '|': + return hasSpace, true + case '\\': + switch inp.Next() { + case input.EOS: + return false, false + case '\n', '\r': + hasSpace = true + } + case '%': + if inp.Next() == '%' { + inp.SkipToEOL() + } + continue + case closeCh: + if inp.Next() == closeCh { + return hasSpace, true + } + continue + } + inp.Next() + } +} + +func (cp *zmkP) readReferenceToClose(closeCh rune) bool { + inp := cp.inp + pos := inp.Pos + for { + switch inp.Ch { + case input.EOS: + return false + case '\t', '\r', '\n', ' ': + if !hasQueryPrefix(inp.Src[pos:]) { + return false + } + case '\\': + switch inp.Next() { + case input.EOS, '\n', '\r': + return false + } + case closeCh: + return true + } + inp.Next() + } +} + +func (cp *zmkP) parseCite() (*sx.Pair, bool) { + inp := cp.inp + switch inp.Next() { + case ' ', ',', '|', ']', '\n', '\r': + return nil, false + } + pos := inp.Pos +loop: + for { + switch inp.Ch { + case input.EOS: + return nil, false + case ' ', ',', '|', ']', '\n', '\r': + break loop + } + inp.Next() + } + posL := inp.Pos + switch inp.Ch { + case ' ', ',', '|': + inp.Next() + } + ins, ok := cp.parseLinkLikeRest() + if !ok { + return nil, false + } + attrs := cp.parseInlineAttributes() + cn := ins.Cons(sx.MakeString(string(inp.Src[pos:posL]))).Cons(attrs).Cons(sz.SymCite) + return cn, true +} + +func (cp *zmkP) parseEndnote() (*sx.Pair, bool) { + cp.inp.Next() + ins, ok := cp.parseLinkLikeRest() + if !ok { + return nil, false + } + attrs := cp.parseInlineAttributes() + return ins.Cons(attrs).Cons(sz.SymEndnote), true +} + +func (cp *zmkP) parseMark() (*sx.Pair, bool) { + inp := cp.inp + inp.Next() + pos := inp.Pos + for inp.Ch != '|' && inp.Ch != ']' { + if !isNameRune(inp.Ch) { + return nil, false + } + inp.Next() + } + mark := inp.Src[pos:inp.Pos] + var ins *sx.Pair + if inp.Ch == '|' { + inp.Next() + var ok bool + ins, ok = cp.parseLinkLikeRest() + if !ok { + return nil, false + } + } else { + inp.Next() + } + mn := ins. + Cons(sx.MakeString("")). // Fragment + Cons(sx.MakeString("")). // Slug + Cons(sx.MakeString(string(mark))). + Cons(sz.SymMark) + return mn, true + // 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) { + var ins sx.Vector + inp := cp.inp + inp.SkipSpace() + for inp.Ch != ']' { + in := cp.parseInline() + if in == nil { + return nil, false + } + ins = append(ins, in) + if input.IsEOLEOS(inp.Ch) && sz.IsBreakSym(in.Car()) { + return nil, false + } + } + inp.Next() + if len(ins) == 0 { + return nil, true + } + return sx.MakeList(ins...), true +} + +func (cp *zmkP) parseComment() (res *sx.Pair, success bool) { + inp := cp.inp + if inp.Next() != '%' { + return nil, false + } + for inp.Ch == '%' { + inp.Next() + } + attrs := cp.parseInlineAttributes() + inp.SkipSpace() + pos := inp.Pos + for { + if input.IsEOLEOS(inp.Ch) { + return sx.MakeList( + sz.SymLiteralComment, + attrs, + sx.MakeString(string(inp.Src[pos:inp.Pos])), + ), true + } + inp.Next() + } +} + +var mapRuneFormat = map[rune]*sx.Symbol{ + '_': sz.SymFormatEmph, + '*': sz.SymFormatStrong, + '>': sz.SymFormatInsert, + '~': sz.SymFormatDelete, + '^': sz.SymFormatSuper, + ',': sz.SymFormatSub, + '"': sz.SymFormatQuote, + '#': sz.SymFormatMark, + ':': sz.SymFormatSpan, +} + +func (cp *zmkP) parseFormat() (res *sx.Pair, success bool) { + inp := cp.inp + fch := inp.Ch + symFormat, ok := mapRuneFormat[fch] + if !ok { + panic(fmt.Sprintf("%q is not a formatting char", fch)) + } + // read 2nd formatting character + if inp.Next() != fch { + return nil, false + } + inp.Next() + var inlines sx.Vector + for { + if inp.Ch == input.EOS { + return nil, false + } + if inp.Ch == fch { + if inp.Next() == fch { + inp.Next() + attrs := cp.parseInlineAttributes() + fn := sx.MakeList(inlines...).Cons(attrs).Cons(symFormat) + return fn, true + } + inlines = append(inlines, sx.MakeList(sz.SymText, sx.MakeString(string(fch)))) + } else if in := cp.parseInline(); in != nil { + if input.IsEOLEOS(inp.Ch) && sz.IsBreakSym(in.Car()) { + return nil, false + } + inlines = append(inlines, in) + } + } +} + +var mapRuneLiteral = map[rune]*sx.Symbol{ + '@': sz.SymLiteralZettel, + '`': sz.SymLiteralProg, + runeModGrave: sz.SymLiteralProg, + '\'': sz.SymLiteralInput, + '=': sz.SymLiteralOutput, + // No '$': sz.SymLiteralMath, because pairing literal math is a little different +} + +func (cp *zmkP) parseLiteral() (res *sx.Pair, success bool) { + inp := cp.inp + fch := inp.Ch + symLiteral, ok := mapRuneLiteral[fch] + if !ok { + panic(fmt.Sprintf("%q is not a formatting char", fch)) + } + // read 2nd formatting character + if inp.Next() != fch { + return nil, false + } + inp.Next() + var sb strings.Builder + for { + if inp.Ch == input.EOS { + return nil, false + } + if inp.Ch == fch { + if inp.Peek() == fch { + inp.Next() + inp.Next() + return createLiteralNode(symLiteral, cp.parseInlineAttributes(), sb.String()), true + } + sb.WriteRune(fch) + inp.Next() + } else { + s := cp.parseString() + sb.WriteString(s.GetValue()) + } + } +} + +func createLiteralNode(sym *sx.Symbol, attrs *sx.Pair, content string) *sx.Pair { + 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("")) + } + } + } + return sx.MakeList(sym, attrs, sx.MakeString(content)) +} + +func (cp *zmkP) parseLiteralMath() (res *sx.Pair, success bool) { + inp := cp.inp + // read 2nd formatting character + if inp.Next() != '$' { + return nil, false + } + inp.Next() + pos := inp.Pos + for { + if inp.Ch == input.EOS { + return nil, false + } + if inp.Ch == '$' && inp.Peek() == '$' { + content := append([]byte{}, inp.Src[pos:inp.Pos]...) + inp.Next() + inp.Next() + fn := sx.MakeList(sz.SymLiteralMath, cp.parseInlineAttributes(), sx.MakeString(string(content))) + return fn, true + } + inp.Next() + } +} + +func (cp *zmkP) parseNdash() (res *sx.Pair, success bool) { + inp := cp.inp + if inp.Peek() != inp.Ch { + return nil, false + } + inp.Next() + inp.Next() + return sx.MakeList(sz.SymText, sx.MakeString("\u2013")), true +} + +func (cp *zmkP) parseEntity() (res *sx.Pair, success bool) { + if text, ok := cp.inp.ScanEntity(); ok { + return sx.MakeList(sz.SymText, sx.MakeString(text)), true + } + return nil, false +} ADDED sz/zmk/post-processor.go Index: sz/zmk/post-processor.go ================================================================== --- /dev/null +++ sz/zmk/post-processor.go @@ -0,0 +1,569 @@ +//----------------------------------------------------------------------------- +// Copyright (c) 2020-present Detlef Stern +// +// This file is part of zettelstore-client. +// +// Zettelstore client is licensed under the latest version of the EUPL +// (European Union Public License). Please see file LICENSE.txt for your rights +// and obligations under this license. +// +// SPDX-License-Identifier: EUPL-1.2 +// SPDX-FileCopyrightText: 2020-present Detlef Stern +//----------------------------------------------------------------------------- + +package zmk + +import ( + "strings" + + "t73f.de/r/sx" + "t73f.de/r/zsc/sz" +) + +var symInVerse = sx.MakeSymbol("in-verse") +var symNoBlock = sx.MakeSymbol("no-block") + +type postProcessor struct{} + +func (pp *postProcessor) Visit(lst *sx.Pair, env *sx.Pair) sx.Object { + if lst == nil { + return nil + } + sym, isSym := sx.GetSymbol(lst.Car()) + if !isSym { + panic(lst) + } + if fn, found := symMap[sym]; found { + return fn(pp, lst, env) + } + return sx.Int64(0) +} + +func (pp *postProcessor) visitPairList(lst *sx.Pair, env *sx.Pair) *sx.Pair { + var pList sx.ListBuilder + for node := lst; node != nil; node = node.Tail() { + if elem := sz.Walk(pp, node.Head(), env); elem != nil { + pList.Add(elem) + } + } + return pList.List() +} + +var symMap map[*sx.Symbol]func(*postProcessor, *sx.Pair, *sx.Pair) *sx.Pair + +func init() { + symMap = map[*sx.Symbol]func(*postProcessor, *sx.Pair, *sx.Pair) *sx.Pair{ + sz.SymBlock: postProcessBlockList, + sz.SymPara: postProcessInlineList, + sz.SymRegionBlock: postProcessRegion, + sz.SymRegionQuote: postProcessRegion, + sz.SymRegionVerse: postProcessRegionVerse, + sz.SymVerbatimComment: postProcessVerbatim, + sz.SymVerbatimEval: postProcessVerbatim, + sz.SymVerbatimMath: postProcessVerbatim, + sz.SymVerbatimProg: postProcessVerbatim, + sz.SymVerbatimZettel: postProcessVerbatim, + sz.SymHeading: postProcessHeading, + sz.SymListOrdered: postProcessItemList, + sz.SymListUnordered: postProcessItemList, + sz.SymListQuote: postProcessQuoteList, + sz.SymDescription: postProcessDescription, + sz.SymTable: postProcessTable, + + sz.SymInline: postProcessInlineList, + sz.SymText: postProcessText, + sz.SymSoft: postProcessSoft, + sz.SymEndnote: postProcessEndnote, + sz.SymMark: postProcessMark, + sz.SymLinkBased: postProcessInlines4, + sz.SymLinkBroken: postProcessInlines4, + sz.SymLinkExternal: postProcessInlines4, + sz.SymLinkFound: postProcessInlines4, + sz.SymLinkHosted: postProcessInlines4, + sz.SymLinkInvalid: postProcessInlines4, + sz.SymLinkQuery: postProcessInlines4, + sz.SymLinkSelf: postProcessInlines4, + sz.SymLinkZettel: postProcessInlines4, + sz.SymEmbed: postProcessInlines4, + sz.SymCite: postProcessInlines4, + sz.SymFormatDelete: postProcessFormat, + sz.SymFormatEmph: postProcessFormat, + sz.SymFormatInsert: postProcessFormat, + sz.SymFormatMark: postProcessFormat, + sz.SymFormatQuote: postProcessFormat, + sz.SymFormatStrong: postProcessFormat, + sz.SymFormatSpan: postProcessFormat, + sz.SymFormatSub: postProcessFormat, + sz.SymFormatSuper: postProcessFormat, + + symSeparator: ignoreProcess, + } +} + +func ignoreProcess(*postProcessor, *sx.Pair, *sx.Pair) *sx.Pair { return nil } + +func postProcessBlockList(pp *postProcessor, lst *sx.Pair, env *sx.Pair) *sx.Pair { + result := pp.visitPairList(lst.Tail(), env) + if result == nil { + if noBlockPair := env.Assoc(symNoBlock); noBlockPair == nil || sx.IsTrue(noBlockPair.Cdr()) { + return nil + } + } + return result.Cons(lst.Car()) +} + +func postProcessInlineList(pp *postProcessor, lst *sx.Pair, env *sx.Pair) *sx.Pair { + sym := lst.Car() + if rest := pp.visitInlines(lst.Tail(), env); rest != nil { + return rest.Cons(sym) + } + return nil +} + +func postProcessRegion(pp *postProcessor, rn *sx.Pair, env *sx.Pair) *sx.Pair { + return doPostProcessRegion(pp, rn, env, env) +} + +func postProcessRegionVerse(pp *postProcessor, rn *sx.Pair, env *sx.Pair) *sx.Pair { + return doPostProcessRegion(pp, rn, env.Cons(sx.Cons(symInVerse, nil)), env) +} + +func doPostProcessRegion(pp *postProcessor, rn *sx.Pair, envBlock, envInline *sx.Pair) *sx.Pair { + + sym := rn.Car() + next := rn.Tail() + attrs := next.Car() + next = next.Tail() + blocks := pp.visitPairList(next.Head(), envBlock) + text := pp.visitInlines(next.Tail(), envInline) + if blocks == nil && text == nil { + return nil + } + return text.Cons(blocks).Cons(attrs).Cons(sym) +} + +func postProcessVerbatim(_ *postProcessor, verb *sx.Pair, _ *sx.Pair) *sx.Pair { + if content, isString := sx.GetString(verb.Tail().Tail().Car()); isString && content.GetValue() != "" { + return verb + } + return nil +} + +func postProcessHeading(pp *postProcessor, hn *sx.Pair, env *sx.Pair) *sx.Pair { + sym := hn.Car() + next := hn.Tail() + level := next.Car() + next = next.Tail() + attrs := next.Car() + next = next.Tail() + slug := next.Car() + next = next.Tail() + fragment := next.Car() + if text := pp.visitInlines(next.Tail(), env); text != nil { + return text.Cons(fragment).Cons(slug).Cons(attrs).Cons(level).Cons(sym) + } + return nil +} + +func postProcessItemList(pp *postProcessor, ln *sx.Pair, env *sx.Pair) *sx.Pair { + elems := pp.visitListElems(ln, env) + if elems == nil { + return nil + } + return elems.Cons(ln.Car()) +} + +func postProcessQuoteList(pp *postProcessor, ln *sx.Pair, env *sx.Pair) *sx.Pair { + elems := pp.visitListElems(ln, env.Cons(sx.Cons(symNoBlock, nil))) + + // Collect multiple paragraph items into one item. + + var newElems sx.ListBuilder + var newPara sx.ListBuilder + + addtoParagraph := func() { + if !newPara.IsEmpty() { + newElems.Add(sx.MakeList(sz.SymBlock, newPara.List().Cons(sz.SymPara))) + } + } + for node := elems; node != nil; node = node.Tail() { + item := node.Head() + if !item.Car().IsEqual(sz.SymBlock) { + continue + } + itemTail := item.Tail() + if itemTail == nil || itemTail.Tail() != nil { + addtoParagraph() + newElems.Add(item) + continue + } + if pn := itemTail.Head(); pn.Car().IsEqual(sz.SymPara) { + if !newPara.IsEmpty() { + newPara.Add(sx.Cons(sz.SymSoft, nil)) + } + newPara.ExtendBang(pn.Tail()) + continue + } + addtoParagraph() + newElems.Add(item) + } + addtoParagraph() + return newElems.List().Cons(ln.Car()) +} + +func (pp *postProcessor) visitListElems(ln *sx.Pair, env *sx.Pair) *sx.Pair { + var pList sx.ListBuilder + for node := ln.Tail(); node != nil; node = node.Tail() { + if elem := sz.Walk(pp, node.Head(), env); elem != nil { + pList.Add(elem) + } + } + return pList.List() +} + +func postProcessDescription(pp *postProcessor, dl *sx.Pair, env *sx.Pair) *sx.Pair { + var dList sx.ListBuilder + isTerm := false + for node := dl.Tail(); node != nil; node = node.Tail() { + isTerm = !isTerm + if isTerm { + dList.Add(pp.visitInlines(node.Head(), env)) + } else { + dList.Add(sz.Walk(pp, node.Head(), env)) + } + } + return dList.List().Cons(dl.Car()) +} + +func postProcessTable(pp *postProcessor, tbl *sx.Pair, env *sx.Pair) *sx.Pair { + sym := tbl.Car() + next := tbl.Tail() + header := next.Head() + if header != nil { + // Already post-processed + return tbl + } + rows, width := pp.visitRows(next.Tail(), env) + if rows == nil { + // Header and row are nil -> no table + return nil + } + header, rows, align := splitTableHeader(rows, width) + alignRow(header, align) + for node := rows; node != nil; node = node.Tail() { + alignRow(node.Head(), align) + } + return rows.Cons(header).Cons(sym) +} + +func (pp *postProcessor) visitRows(rows *sx.Pair, env *sx.Pair) (*sx.Pair, int) { + maxWidth := 0 + var pRows sx.ListBuilder + for node := rows; node != nil; node = node.Tail() { + row := node.Head() + row, width := pp.visitCells(row, env) + if maxWidth < width { + maxWidth = width + } + pRows.Add(row) + } + return pRows.List(), maxWidth +} + +func (pp *postProcessor) visitCells(cells *sx.Pair, env *sx.Pair) (*sx.Pair, int) { + width := 0 + var pCells sx.ListBuilder + for node := cells; node != nil; node = node.Tail() { + cell := node.Head() + ins := pp.visitInlines(cell.Tail(), env) + newCell := ins.Cons(cell.Car()) + pCells.Add(newCell) + width++ + } + return pCells.List(), width +} + +func splitTableHeader(rows *sx.Pair, width int) (header, realRows *sx.Pair, align []*sx.Symbol) { + align = make([]*sx.Symbol, width) + + foundHeader := false + cellCount := 0 + + // assert: rows != nil (checked in postProcessTable) + for node := rows.Head(); node != nil; node = node.Tail() { + cellCount++ + cell := node.Head() + cellTail := cell.Tail() + if cellTail == nil { + continue + } + + // elem is first cell inline element + elem := cellTail.Head() + if elem.Car().IsEqual(sz.SymText) { + if s, isString := sx.GetString(elem.Tail().Car()); isString && s.GetValue() != "" { + str := s.GetValue() + if str[0] == '=' { + foundHeader = true + elem.SetCdr(sx.Cons(sx.MakeString(str[1:]), nil)) + } + } + } + + // move to the last cell inline element + for { + next := cellTail.Tail() + if next == nil { + break + } + cellTail = next + } + + 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.IsEqualSymbol(sz.SymCell) { + elem.SetCdr(sx.Cons(sx.MakeString(str[0:len(str)-1]), nil)) + } + align[cellCount-1] = cellAlign + cell.SetCar(cellAlign) + } + } + } + + if !foundHeader { + for i := 0; i < width; i++ { + align[i] = sz.SymCell // Default alignment + } + return nil, rows, align + } + + for i := 0; i < width; i++ { + if align[i] == nil { + align[i] = sz.SymCell // Default alignment + } + } + return rows.Head(), rows.Tail(), align +} + +func alignRow(row *sx.Pair, align []*sx.Symbol) { + if row == nil { + return + } + var lastCellNode *sx.Pair + cellCount := 0 + for node := row; node != nil; node = node.Tail() { + lastCellNode = node + cell := node.Head() + cell.SetCar(align[cellCount]) + cellCount++ + cellTail := cell.Tail() + if cellTail == nil { + continue + } + + // elem is first cell inline element + 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.IsEqualSymbol(sz.SymCell) { + elem.SetCdr(sx.Cons(sx.MakeString(str[1:]), nil)) + cell.SetCar(cellAlign) + } + } + } + } + + for cellCount < len(align) { + lastCellNode = lastCellNode.AppendBang(sx.Cons(align[cellCount], nil)) + cellCount++ + } +} + +func getCellAlignment(ch byte) *sx.Symbol { + switch ch { + case ':': + return sz.SymCellCenter + case '<': + return sz.SymCellLeft + case '>': + return sz.SymCellRight + default: + return sz.SymCell + } +} + +func (pp *postProcessor) visitInlines(lst *sx.Pair, env *sx.Pair) *sx.Pair { + length := lst.Length() + if length <= 0 { + return nil + } + inVerse := env.Assoc(symInVerse) != nil + vector := make([]*sx.Pair, 0, length) + // 1st phase: process all childs, ignore ' ' / '\t' at start, and merge some elements + for node := lst; node != nil; node = node.Tail() { + elem := sz.Walk(pp, node.Head(), env) + if elem == nil { + continue + } + elemSym := elem.Car() + if len(vector) == 0 { + // If the 1st element is a TEXT, remove all ' ', '\t' at the beginning, if outside a verse block. + if inVerse || !elemSym.IsEqual(sz.SymText) { + vector = append(vector, elem) + continue + } + + elemTail := elem.Tail() + elemText := elemTail.Car().(sx.String).GetValue() + if elemText != "" && (elemText[0] == ' ' || elemText[0] == '\t') { + for elemText != "" { + if ch := elemText[0]; ch != ' ' && ch != '\t' { + break + } + elemText = elemText[1:] + } + elemTail.SetCar(sx.MakeString(elemText)) + } + if elemText != "" { + vector = append(vector, elem) + } + continue + } + last := vector[len(vector)-1] + lastSym := last.Car() + + if lastSym.IsEqual(sz.SymText) && elemSym.IsEqual(sz.SymText) { + // Merge two TEXT elements into one + lastText := last.Tail().Car().(sx.String).GetValue() + elemText := elem.Tail().Car().(sx.String).GetValue() + last.SetCdr(sx.Cons(sx.MakeString(lastText+elemText), sx.Nil())) + continue + } + + if lastSym.IsEqual(sz.SymText) && elemSym.IsEqual(sz.SymSoft) { + // Merge (TEXT "... ") (SOFT) to (TEXT "...") (HARD) + lastTail := last.Tail() + if lastText := lastTail.Car().(sx.String).GetValue(); strings.HasSuffix(lastText, " ") { + newText := removeTrailingSpaces(lastText) + if newText == "" { + vector[len(vector)-1] = sx.Cons(sz.SymHard, sx.Nil()) + continue + } + lastTail.SetCar(sx.MakeString(newText)) + elemSym = sz.SymHard + elem.SetCar(elemSym) + } + } + + vector = append(vector, elem) + } + if len(vector) == 0 { + return nil + } + + // 2nd phase: remove (SOFT), (HARD) at the end, remove trailing spaces in (TEXT "...") + lastPos := len(vector) - 1 + for lastPos >= 0 { + elem := vector[lastPos] + elemSym := elem.Car() + if elemSym.IsEqual(sz.SymText) { + elemTail := elem.Tail() + elemText := elemTail.Car().(sx.String).GetValue() + newText := removeTrailingSpaces(elemText) + if newText != "" { + elemTail.SetCar(sx.MakeString(newText)) + break + } + lastPos-- + } else if sz.IsBreakSym(elemSym) { + lastPos-- + } else { + break + } + } + if lastPos < 0 { + return nil + } + + result := sx.Cons(vector[0], nil) + curr := result + for i := 1; i <= lastPos; i++ { + curr = curr.AppendBang(vector[i]) + } + return result +} + +func removeTrailingSpaces(s string) string { + for len(s) > 0 { + if ch := s[len(s)-1]; ch != ' ' && ch != '\t' { + return s + } + s = s[0 : len(s)-1] + } + return "" +} + +func postProcessText(_ *postProcessor, txt *sx.Pair, _ *sx.Pair) *sx.Pair { + if tail := txt.Tail(); tail != nil { + if content, isString := sx.GetString(tail.Car()); isString && content.GetValue() != "" { + return txt + } + } + return nil +} + +func postProcessSoft(_ *postProcessor, sn *sx.Pair, env *sx.Pair) *sx.Pair { + if env.Assoc(symInVerse) == nil { + return sn + } + return sx.Cons(sz.SymHard, nil) +} + +func postProcessEndnote(pp *postProcessor, en *sx.Pair, env *sx.Pair) *sx.Pair { + sym := en.Car() + next := en.Tail() + attrs := next.Car() + if text := pp.visitInlines(next.Tail(), env); text != nil { + return text.Cons(attrs).Cons(sym) + } + return sx.MakeList(sym, attrs) +} + +func postProcessMark(pp *postProcessor, en *sx.Pair, env *sx.Pair) *sx.Pair { + sym := en.Car() + next := en.Tail() + mark := next.Car() + next = next.Tail() + slug := next.Car() + next = next.Tail() + fragment := next.Car() + text := pp.visitInlines(next.Tail(), env) + return text.Cons(fragment).Cons(slug).Cons(mark).Cons(sym) +} + +func postProcessInlines4(pp *postProcessor, ln *sx.Pair, env *sx.Pair) *sx.Pair { + sym := ln.Car() + next := ln.Tail() + attrs := next.Car() + next = next.Tail() + val3 := next.Car() + text := pp.visitInlines(next.Tail(), env) + return text.Cons(val3).Cons(attrs).Cons(sym) +} + +func postProcessFormat(pp *postProcessor, fn *sx.Pair, env *sx.Pair) *sx.Pair { + symFormat := fn.Car() + next := fn.Tail() // Attrs + attrs := next.Car() + next = next.Tail() // Possible inlines + if next == nil { + return fn + } + inlines := pp.visitInlines(next, env) + return inlines.Cons(attrs).Cons(symFormat) +} ADDED sz/zmk/ref.go Index: sz/zmk/ref.go ================================================================== --- /dev/null +++ sz/zmk/ref.go @@ -0,0 +1,107 @@ +// ----------------------------------------------------------------------------- +// Copyright (c) 2020-present Detlef Stern +// +// This file is part of zettelstore-client. +// +// Zettelstore client is licensed under the latest version of the EUPL +// (European Union Public License). Please see file LICENSE.txt for your rights +// and obligations under this license. +// +// SPDX-License-Identifier: EUPL-1.2 +// SPDX-FileCopyrightText: 2020-present Detlef Stern +// ----------------------------------------------------------------------------- + +package zmk + +import ( + "net/url" + "strings" + + "t73f.de/r/sx" + "t73f.de/r/zsc/api" + "t73f.de/r/zsc/sz" +) + +// ParseReference parses a string and returns a reference. +func ParseReference(s string) *sx.Pair { + if invalidReference(s) { + return makePairRef(sz.SymRefStateInvalid, s) + } + if strings.HasPrefix(s, api.QueryPrefix) { + return makePairRef(sz.SymRefStateQuery, s[len(api.QueryPrefix):]) + } + if state, ok := localState(s); ok { + if state.IsEqualSymbol(sz.SymRefStateBased) { + s = s[1:] + } + _, err := url.Parse(s) + if err == nil { + return makePairRef(state, s) + } + } + u, err := url.Parse(s) + if err != nil { + return makePairRef(sz.SymRefStateInvalid, s) + } + if !externalURL(u) { + zid := api.ZettelID(u.Path) + if zid.IsValid() { + return makePairRef(sz.SymRefStateZettel, s) + } + if u.Path == "" && u.Fragment != "" { + return makePairRef(sz.SymRefStateSelf, s) + } + } + return makePairRef(sz.SymRefStateExternal, s) +} +func makePairRef(sym *sx.Symbol, val string) *sx.Pair { + return sx.MakeList(sym, sx.MakeString(val)) +} + +func invalidReference(s string) bool { return s == "" || s == "00000000000000" } + +func externalURL(u *url.URL) bool { + return u.Scheme != "" || u.Opaque != "" || u.Host != "" || u.User != nil +} + +func localState(path string) (*sx.Symbol, bool) { + if len(path) > 0 && path[0] == '/' { + if len(path) > 1 && path[1] == '/' { + return sz.SymRefStateBased, true + } + return sz.SymRefStateHosted, true + } + if len(path) > 1 && path[0] == '.' { + if len(path) > 2 && path[1] == '.' && path[2] == '/' { + return sz.SymRefStateHosted, true + } + return sz.SymRefStateHosted, path[1] == '/' + } + return sz.SymRefStateInvalid, false +} + +// ReferenceIsValid returns true if reference is valid +func ReferenceIsValid(ref *sx.Pair) bool { + return !ref.Car().IsEqual(sz.SymRefStateInvalid) +} + +// ReferenceIsZettel returns true if it is a reference to a local zettel. +func ReferenceIsZettel(ref *sx.Pair) bool { + state := ref.Car() + return state.IsEqual(sz.SymRefStateZettel) || + state.IsEqual(sz.SymRefStateSelf) || + state.IsEqual(sz.SymRefStateFound) || + state.IsEqual(sz.SymRefStateBroken) +} + +// ReferenceIsLocal returns true if reference is local +func ReferenceIsLocal(ref *sx.Pair) bool { + state := ref.Car() + return state.IsEqual(sz.SymRefStateHosted) || + state.IsEqual(sz.SymRefStateBased) +} + +// ReferenceIsExternal returns true if it is a reference to external material. +func ReferenceIsExternal(ref *sx.Pair) bool { + return ref.Car().IsEqual(sz.SymRefStateExternal) +} ADDED sz/zmk/ref_test.go Index: sz/zmk/ref_test.go ================================================================== --- /dev/null +++ sz/zmk/ref_test.go @@ -0,0 +1,99 @@ +// ----------------------------------------------------------------------------- +// Copyright (c) 2020-present Detlef Stern +// +// This file is part of zettelstore-client. +// +// Zettelstore client is licensed under the latest version of the EUPL +// (European Union Public License). Please see file LICENSE.txt for your rights +// and obligations under this license. +// +// SPDX-License-Identifier: EUPL-1.2 +// SPDX-FileCopyrightText: 2020-present Detlef Stern +// ----------------------------------------------------------------------------- +package zmk_test + +import ( + "testing" + + "t73f.de/r/zsc/sz/zmk" +) + +func TestParseReference(t *testing.T) { + t.Parallel() + testcases := []struct { + link string + err bool + exp string + }{ + {"", true, ""}, + {"12345678901234", false, "(ZETTEL \"12345678901234\")"}, + {"123", false, "(EXTERNAL \"123\")"}, + {",://", true, ""}, + } + + for i, tc := range testcases { + got := zmk.ParseReference(tc.link) + gotIsValid := zmk.ReferenceIsValid(got) + if gotIsValid == tc.err { + t.Errorf( + "TC=%d, expected parse error of %q: %v, but got %q", i, tc.link, tc.err, got) + } + if gotIsValid && got.String() != tc.exp { + t.Errorf("TC=%d, Reference of %q is %q, but got %q", i, tc.link, tc.exp, got) + } + } +} + +func TestReferenceIsZettelMaterial(t *testing.T) { + t.Parallel() + testcases := []struct { + link string + isZettel bool + isExternal bool + isLocal bool + }{ + {"", false, false, false}, + {"00000000000000", false, false, false}, + {"http://zettelstore.de/z/ast", false, true, false}, + {"12345678901234", true, false, false}, + {"12345678901234#local", true, false, false}, + {"http://12345678901234", false, true, false}, + {"http://zettelstore.de/z/12345678901234", false, true, false}, + {"http://zettelstore.de/12345678901234", false, true, false}, + {"/12345678901234", false, false, true}, + {"//12345678901234", false, false, true}, + {"./12345678901234", false, false, true}, + {"../12345678901234", false, false, true}, + {".../12345678901234", false, true, false}, + } + + for i, tc := range testcases { + ref := zmk.ParseReference(tc.link) + isZettel := zmk.ReferenceIsZettel(ref) + if isZettel != tc.isZettel { + t.Errorf( + "TC=%d, Reference %q isZettel=%v expected, but got %v", + i, + tc.link, + tc.isZettel, + isZettel) + } + isLocal := zmk.ReferenceIsLocal(ref) + if isLocal != tc.isLocal { + t.Errorf( + "TC=%d, Reference %q isLocal=%v expected, but got %v", + i, + tc.link, + tc.isLocal, isLocal) + } + isExternal := zmk.ReferenceIsExternal(ref) + if isExternal != tc.isExternal { + t.Errorf( + "TC=%d, Reference %q isExternal=%v expected, but got %v", + i, + tc.link, + tc.isExternal, + isExternal) + } + } +} ADDED sz/zmk/zmk.go Index: sz/zmk/zmk.go ================================================================== --- /dev/null +++ sz/zmk/zmk.go @@ -0,0 +1,282 @@ +//----------------------------------------------------------------------------- +// Copyright (c) 2020-present Detlef Stern +// +// This file is part of zettelstore-client. +// +// Zettelstore client is licensed under the latest version of the EUPL +// (European Union Public License). Please see file LICENSE.txt for your rights +// and obligations under this license. +// +// SPDX-License-Identifier: EUPL-1.2 +// SPDX-FileCopyrightText: 2020-present Detlef Stern +//----------------------------------------------------------------------------- + +// Package zmk provides a parser for zettelmarkup. +package zmk + +import ( + "slices" + "strings" + "unicode" + + "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 + for inp.Ch != input.EOS { + bn, cont := parser.parseBlock(lastPara) + if bn != nil { + blkBuild.Add(bn) + } + if !cont { + if bn.Car().IsEqual(sz.SymPara) { + lastPara = bn + } else { + lastPara = nil + } + } + } + if parser.nestingLevel != 0 { + panic("Nesting level was not decremented") + } + + bnl := blkBuild.List() + var pp postProcessor + if bs := pp.visitPairList(bnl, nil); bs != nil { + 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() + if in == nil { + break + } + ins = append(ins, in) + } + + inl := ins.MakeList().Cons(sz.SymInline) + var pp postProcessor + return sz.Walk(&pp, inl, nil) +} + +type zmkP struct { + inp *input.Input // Input stream + lists []*sx.Pair // Stack of lists + lastRow *sx.Pair // Last row of table, or nil if not in table. + descrl *sx.Pair // Current description list + nestingLevel int // Count nesting of block and inline elements + + inVerse bool // Currently in a vers region? +} + +// runeModGrave is Unicode code point U+02CB (715) called "MODIFIER LETTER +// GRAVE ACCENT". On the iPad it is much more easier to type in this code point +// than U+0060 (96) "Grave accent" (aka backtick). Therefore, U+02CB will be +// considered equivalent to U+0060. +const runeModGrave = 'ˋ' // This is NOT '`'! + +const maxNestingLevel = 50 + +// clearStacked removes all multi-line nodes from parser. +func (cp *zmkP) clearStacked() { + cp.lists = nil + cp.lastRow = nil + cp.descrl = nil +} + +type attrMap map[string]string + +func (attrs attrMap) updateAttrs(key, val string) { + if prevVal := attrs[key]; len(prevVal) > 0 { + attrs[key] = prevVal + " " + val + } else { + attrs[key] = val + } +} + +func (attrs attrMap) asPairAssoc() *sx.Pair { + names := make([]string, 0, len(attrs)) + for n := range attrs { + names = append(names, n) + } + slices.Sort(names) + 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 +} + +func (cp *zmkP) parseNormalAttribute(attrs attrMap) bool { + inp := cp.inp + posK := inp.Pos + for isNameRune(inp.Ch) { + inp.Next() + } + if posK == inp.Pos { + return false + } + key := string(inp.Src[posK:inp.Pos]) + if inp.Ch != '=' { + attrs[key] = "" + return true + } + return cp.parseAttributeValue(key, attrs) +} + +func (cp *zmkP) parseAttributeValue(key string, attrs attrMap) bool { + inp := cp.inp + if inp.Next() == '"' { + return cp.parseQuotedAttributeValue(key, attrs) + } + posV := inp.Pos + for { + switch inp.Ch { + case input.EOS: + return false + case '\n', '\r', ' ', ',', '}': + attrs.updateAttrs(key, string(inp.Src[posV:inp.Pos])) + return true + } + inp.Next() + } +} + +func (cp *zmkP) parseQuotedAttributeValue(key string, attrs attrMap) bool { + inp := cp.inp + inp.Next() + var sb strings.Builder + for { + switch inp.Ch { + case input.EOS: + return false + case '"': + attrs.updateAttrs(key, sb.String()) + inp.Next() + return true + case '\\': + switch inp.Next() { + case input.EOS, '\n', '\r': + return false + } + fallthrough + default: + sb.WriteRune(inp.Ch) + inp.Next() + } + } + +} + +func (cp *zmkP) parseBlockAttributes() *sx.Pair { + inp := cp.inp + pos := inp.Pos + for isNameRune(inp.Ch) { + inp.Next() + } + if pos < inp.Pos { + return attrMap{"": string(inp.Src[pos:inp.Pos])}.asPairAssoc() + } + + // No immediate name: skip spaces + inp.SkipSpace() + return cp.parseInlineAttributes() +} + +func (cp *zmkP) parseInlineAttributes() *sx.Pair { + inp := cp.inp + pos := inp.Pos + if attrs, success := cp.doParseAttributes(); success { + return attrs + } + inp.SetPos(pos) + return nil +} + +// doParseAttributes reads attributes. +func (cp *zmkP) doParseAttributes() (res *sx.Pair, success bool) { + inp := cp.inp + if inp.Ch != '{' { + return nil, false + } + inp.Next() + a := attrMap{} + if !cp.parseAttributeValues(a) { + return nil, false + } + inp.Next() + return a.asPairAssoc(), true +} + +func (cp *zmkP) parseAttributeValues(a attrMap) bool { + inp := cp.inp + for { + cp.skipSpaceLine() + switch inp.Ch { + case input.EOS: + return false + case '}': + return true + case '.': + inp.Next() + posC := inp.Pos + for isNameRune(inp.Ch) { + inp.Next() + } + if posC == inp.Pos { + return false + } + a.updateAttrs("class", string(inp.Src[posC:inp.Pos])) + case '=': + delete(a, "") + if !cp.parseAttributeValue("", a) { + return false + } + default: + if !cp.parseNormalAttribute(a) { + return false + } + } + + switch inp.Ch { + case '}': + return true + case '\n', '\r': + case ' ', ',': + inp.Next() + default: + return false + } + } +} + +func (cp *zmkP) skipSpaceLine() { + for inp := cp.inp; ; { + switch inp.Ch { + case ' ': + inp.Next() + case '\n', '\r': + inp.EatEOL() + default: + return + } + } +} + +func isNameRune(ch rune) bool { + return unicode.IsLetter(ch) || unicode.IsDigit(ch) || ch == '-' || ch == '_' +} ADDED sz/zmk/zmk_fuzz_test.go Index: sz/zmk/zmk_fuzz_test.go ================================================================== --- /dev/null +++ sz/zmk/zmk_fuzz_test.go @@ -0,0 +1,29 @@ +//----------------------------------------------------------------------------- +// Copyright (c) 2022-present Detlef Stern +// +// This file is part of zettelstore-client. +// +// Zettelstore client is licensed under the latest version of the EUPL +// (European Union Public License). Please see file LICENSE.txt for your rights +// and obligations under this license. +// +// SPDX-License-Identifier: EUPL-1.2 +// SPDX-FileCopyrightText: 2022-present Detlef Stern +//----------------------------------------------------------------------------- + +package zmk_test + +import ( + "testing" + + "t73f.de/r/zsc/input" + "t73f.de/r/zsc/sz/zmk" +) + +func FuzzParseBlocks(f *testing.F) { + f.Fuzz(func(t *testing.T, src []byte) { + t.Parallel() + inp := input.NewInput(src) + zmk.ParseBlocks(inp) + }) +} ADDED sz/zmk/zmk_test.go Index: sz/zmk/zmk_test.go ================================================================== --- /dev/null +++ sz/zmk/zmk_test.go @@ -0,0 +1,864 @@ +//----------------------------------------------------------------------------- +// Copyright (c) 2020-present Detlef Stern +// +// This file is part of zettelstore-client. +// +// Zettelstore client is licensed under the latest version of the EUPL +// (European Union Public License). Please see file LICENSE.txt for your rights +// and obligations under this license. +// +// SPDX-License-Identifier: EUPL-1.2 +// SPDX-FileCopyrightText: 2020-present Detlef Stern +//----------------------------------------------------------------------------- + +// Package zmk_test provides some tests for the zettelmarkup parser. +package zmk_test + +import ( + "fmt" + "strings" + "testing" + + "t73f.de/r/sx" + "t73f.de/r/zsc/input" + "t73f.de/r/zsc/sz" + "t73f.de/r/zsc/sz/zmk" +) + +type TestCase struct{ source, want string } +type TestCases []TestCase +type symbolMap map[string]*sx.Symbol + +func replace(s string, sm symbolMap, tcs TestCases) TestCases { + var sym string + if len(sm) > 0 { + sym = sm[s].GetValue() + } + var testCases TestCases + for _, tc := range tcs { + source := strings.ReplaceAll(tc.source, "$", s) + want := tc.want + if sym != "" { + want = strings.ReplaceAll(want, "$%", sym) + } + want = strings.ReplaceAll(want, "$", s) + testCases = append(testCases, TestCase{source, want}) + } + return testCases +} + +func checkTcs(t *testing.T, isBlock bool, tcs TestCases) { + t.Helper() + + 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.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{ + {"", "()"}, + {"\n", "()"}, + {"\r", "()"}, + {"\r\n", "()"}, + {"\n\n", "()"}, + }) + } +} + +func TestText(t *testing.T) { + t.Parallel() + checkTcs(t, false, TestCases{ + {"abcd", "(INLINE (TEXT \"abcd\"))"}, + {"ab cd", "(INLINE (TEXT \"ab cd\"))"}, + {"abcd ", "(INLINE (TEXT \"abcd\"))"}, + {" abcd", "(INLINE (TEXT \"abcd\"))"}, + {"\\", "(INLINE (TEXT \"\\\\\"))"}, + {"\\\n", "()"}, + {"\\\ndef", "(INLINE (HARD) (TEXT \"def\"))"}, + {"\\\r", "()"}, + {"\\\rdef", "(INLINE (HARD) (TEXT \"def\"))"}, + {"\\\r\n", "()"}, + {"\\\r\ndef", "(INLINE (HARD) (TEXT \"def\"))"}, + {"\\a", "(INLINE (TEXT \"a\"))"}, + {"\\aa", "(INLINE (TEXT \"aa\"))"}, + {"a\\a", "(INLINE (TEXT \"aa\"))"}, + {"\\+", "(INLINE (TEXT \"+\"))"}, + {"\\ ", "(INLINE (TEXT \"\u00a0\"))"}, + {"http://a, http://b", "(INLINE (TEXT \"http://a, http://b\"))"}, + }) +} + +func TestSpace(t *testing.T) { + t.Parallel() + for _, isBlock := range []bool{true, false} { + checkTcs(t, isBlock, TestCases{ + {" ", "()"}, + {"\t", "()"}, + {" ", "()"}, + }) + } +} + +func TestSoftBreak(t *testing.T) { + t.Parallel() + checkTcs(t, false, TestCases{ + {"x\ny", "(INLINE (TEXT \"x\") (SOFT) (TEXT \"y\"))"}, + {"z\n", "(INLINE (TEXT \"z\"))"}, + {" \n ", "()"}, + {" \n", "()"}, + }) +} + +func TestHardBreak(t *testing.T) { + t.Parallel() + checkTcs(t, false, TestCases{ + {"x \ny", "(INLINE (TEXT \"x\") (HARD) (TEXT \"y\"))"}, + {"z \n", "(INLINE (TEXT \"z\"))"}, + {" \n ", "()"}, + {" \n", "()"}, + }) +} + +func TestLink(t *testing.T) { + t.Parallel() + checkTcs(t, false, TestCases{ + {"[", "(INLINE (TEXT \"[\"))"}, + {"[[", "(INLINE (TEXT \"[[\"))"}, + {"[[|", "(INLINE (TEXT \"[[|\"))"}, + {"[[]", "(INLINE (TEXT \"[[]\"))"}, + {"[[|]", "(INLINE (TEXT \"[[|]\"))"}, + {"[[]]", "(INLINE (TEXT \"[[]]\"))"}, + {"[[|]]", "(INLINE (TEXT \"[[|]]\"))"}, + {"[[ ]]", "(INLINE (TEXT \"[[ ]]\"))"}, + {"[[\n]]", "(INLINE (TEXT \"[[\") (SOFT) (TEXT \"]]\"))"}, + {"[[ a]]", "(INLINE (LINK-EXTERNAL () \"a\"))"}, + {"[[a ]]", "(INLINE (TEXT \"[[a ]]\"))"}, + {"[[a\n]]", "(INLINE (TEXT \"[[a\") (SOFT) (TEXT \"]]\"))"}, + {"[[a]]", "(INLINE (LINK-EXTERNAL () \"a\"))"}, + {"[[12345678901234]]", "(INLINE (LINK-ZETTEL () \"12345678901234\"))"}, + {"[[a]", "(INLINE (TEXT \"[[a]\"))"}, + {"[[|a]]", "(INLINE (TEXT \"[[|a]]\"))"}, + {"[[b|]]", "(INLINE (TEXT \"[[b|]]\"))"}, + {"[[b|a]]", "(INLINE (LINK-EXTERNAL () \"a\" (TEXT \"b\")))"}, + {"[[b| a]]", "(INLINE (LINK-EXTERNAL () \"a\" (TEXT \"b\")))"}, + {"[[b%c|a]]", "(INLINE (LINK-EXTERNAL () \"a\" (TEXT \"b%c\")))"}, + {"[[b%%c|a]]", "(INLINE (TEXT \"[[b\") (LITERAL-COMMENT () \"c|a]]\"))"}, + {"[[b|a]", "(INLINE (TEXT \"[[b|a]\"))"}, + {"[[b\nc|a]]", "(INLINE (LINK-EXTERNAL () \"a\" (TEXT \"b\") (SOFT) (TEXT \"c\")))"}, + {"[[b c|a#n]]", "(INLINE (LINK-EXTERNAL () \"a#n\" (TEXT \"b c\")))"}, + {"[[a]]go", "(INLINE (LINK-EXTERNAL () \"a\") (TEXT \"go\"))"}, + {"[[b|a]]{go}", "(INLINE (LINK-EXTERNAL ((\"go\" . \"\")) \"a\" (TEXT \"b\")))"}, + {"[[[[a]]|b]]", "(INLINE (TEXT \"[[\") (LINK-EXTERNAL () \"a\") (TEXT \"|b]]\"))"}, + {"[[a[b]c|d]]", "(INLINE (LINK-EXTERNAL () \"d\" (TEXT \"a[b]c\")))"}, + {"[[[b]c|d]]", "(INLINE (TEXT \"[\") (LINK-EXTERNAL () \"d\" (TEXT \"b]c\")))"}, + {"[[a[]c|d]]", "(INLINE (LINK-EXTERNAL () \"d\" (TEXT \"a[]c\")))"}, + {"[[a[b]|d]]", "(INLINE (LINK-EXTERNAL () \"d\" (TEXT \"a[b]\")))"}, + {"[[\\|]]", "(INLINE (LINK-EXTERNAL () \"\\\\|\"))"}, + {"[[\\||a]]", "(INLINE (LINK-EXTERNAL () \"a\" (TEXT \"|\")))"}, + {"[[b\\||a]]", "(INLINE (LINK-EXTERNAL () \"a\" (TEXT \"b|\")))"}, + {"[[b\\|c|a]]", "(INLINE (LINK-EXTERNAL () \"a\" (TEXT \"b|c\")))"}, + {"[[\\]]]", "(INLINE (LINK-EXTERNAL () \"\\\\]\"))"}, + {"[[\\]|a]]", "(INLINE (LINK-EXTERNAL () \"a\" (TEXT \"]\")))"}, + {"[[b\\]|a]]", "(INLINE (LINK-EXTERNAL () \"a\" (TEXT \"b]\")))"}, + {"[[\\]\\||a]]", "(INLINE (LINK-EXTERNAL () \"a\" (TEXT \"]|\")))"}, + {"[[http://a]]", "(INLINE (LINK-EXTERNAL () \"http://a\"))"}, + {"[[http://a|http://a]]", "(INLINE (LINK-EXTERNAL () \"http://a\" (TEXT \"http://a\")))"}, + {"[[[[a]]]]", "(INLINE (TEXT \"[[\") (LINK-EXTERNAL () \"a\") (TEXT \"]]\"))"}, + {"[[query:title]]", "(INLINE (LINK-QUERY () \"title\"))"}, + {"[[query:title syntax]]", "(INLINE (LINK-QUERY () \"title syntax\"))"}, + {"[[query:title | action]]", "(INLINE (LINK-QUERY () \"title | action\"))"}, + {"[[Text|query:title]]", "(INLINE (LINK-QUERY () \"title\" (TEXT \"Text\")))"}, + {"[[Text|query:title syntax]]", "(INLINE (LINK-QUERY () \"title syntax\" (TEXT \"Text\")))"}, + {"[[Text|query:title | action]]", "(INLINE (LINK-QUERY () \"title | action\" (TEXT \"Text\")))"}, + }) +} + +func TestEmbed(t *testing.T) { + t.Parallel() + checkTcs(t, false, TestCases{ + {"{", "(INLINE (TEXT \"{\"))"}, + {"{{", "(INLINE (TEXT \"{{\"))"}, + {"{{|", "(INLINE (TEXT \"{{|\"))"}, + {"{{}", "(INLINE (TEXT \"{{}\"))"}, + {"{{|}", "(INLINE (TEXT \"{{|}\"))"}, + {"{{}}", "(INLINE (TEXT \"{{}}\"))"}, + {"{{|}}", "(INLINE (TEXT \"{{|}}\"))"}, + {"{{ }}", "(INLINE (TEXT \"{{ }}\"))"}, + {"{{\n}}", "(INLINE (TEXT \"{{\") (SOFT) (TEXT \"}}\"))"}, + {"{{a }}", "(INLINE (TEXT \"{{a }}\"))"}, + {"{{a\n}}", "(INLINE (TEXT \"{{a\") (SOFT) (TEXT \"}}\"))"}, + {"{{a}}", "(INLINE (EMBED () \"a\"))"}, + {"{{12345678901234}}", "(INLINE (EMBED () \"12345678901234\"))"}, + {"{{ a}}", "(INLINE (EMBED () \"a\"))"}, + {"{{a}", "(INLINE (TEXT \"{{a}\"))"}, + {"{{|a}}", "(INLINE (TEXT \"{{|a}}\"))"}, + {"{{b|}}", "(INLINE (TEXT \"{{b|}}\"))"}, + {"{{b|a}}", "(INLINE (EMBED () \"a\" (TEXT \"b\")))"}, + {"{{b| a}}", "(INLINE (EMBED () \"a\" (TEXT \"b\")))"}, + {"{{b|a}", "(INLINE (TEXT \"{{b|a}\"))"}, + {"{{b\nc|a}}", "(INLINE (EMBED () \"a\" (TEXT \"b\") (SOFT) (TEXT \"c\")))"}, + {"{{b c|a#n}}", "(INLINE (EMBED () \"a#n\" (TEXT \"b c\")))"}, + {"{{a}}{go}", "(INLINE (EMBED ((\"go\" . \"\")) \"a\"))"}, + {"{{{{a}}|b}}", "(INLINE (TEXT \"{{\") (EMBED () \"a\") (TEXT \"|b}}\"))"}, + {"{{\\|}}", "(INLINE (EMBED () \"\\\\|\"))"}, + {"{{\\||a}}", "(INLINE (EMBED () \"a\" (TEXT \"|\")))"}, + {"{{b\\||a}}", "(INLINE (EMBED () \"a\" (TEXT \"b|\")))"}, + {"{{b\\|c|a}}", "(INLINE (EMBED () \"a\" (TEXT \"b|c\")))"}, + {"{{\\}}}", "(INLINE (EMBED () \"\\\\}\"))"}, + {"{{\\}|a}}", "(INLINE (EMBED () \"a\" (TEXT \"}\")))"}, + {"{{b\\}|a}}", "(INLINE (EMBED () \"a\" (TEXT \"b}\")))"}, + {"{{\\}\\||a}}", "(INLINE (EMBED () \"a\" (TEXT \"}|\")))"}, + {"{{http://a}}", "(INLINE (EMBED () \"http://a\"))"}, + {"{{http://a|http://a}}", "(INLINE (EMBED () \"http://a\" (TEXT \"http://a\")))"}, + {"{{{{a}}}}", "(INLINE (TEXT \"{{\") (EMBED () \"a\") (TEXT \"}}\"))"}, + }) +} + +func TestCite(t *testing.T) { + t.Parallel() + checkTcs(t, false, TestCases{ + {"[@", "(INLINE (TEXT \"[@\"))"}, + {"[@]", "(INLINE (TEXT \"[@]\"))"}, + {"[@a]", "(INLINE (CITE () \"a\"))"}, + {"[@ a]", "(INLINE (TEXT \"[@ a]\"))"}, + {"[@a ]", "(INLINE (CITE () \"a\"))"}, + {"[@a\n]", "(INLINE (CITE () \"a\"))"}, + {"[@a\nx]", "(INLINE (CITE () \"a\" (SOFT) (TEXT \"x\")))"}, + {"[@a\n\n]", "(INLINE (TEXT \"[@a\") (SOFT) (SOFT) (TEXT \"]\"))"}, + {"[@a,\n]", "(INLINE (CITE () \"a\"))"}, + {"[@a,n]", "(INLINE (CITE () \"a\" (TEXT \"n\")))"}, + {"[@a| n]", "(INLINE (CITE () \"a\" (TEXT \"n\")))"}, + {"[@a|n ]", "(INLINE (CITE () \"a\" (TEXT \"n\")))"}, + {"[@a,[@b]]", "(INLINE (CITE () \"a\" (CITE () \"b\")))"}, + {"[@a]{color=green}", "(INLINE (CITE ((\"color\" . \"green\")) \"a\"))"}, + }) + checkTcs(t, true, TestCases{ + {"[@a\n\n]", "(BLOCK (PARA (TEXT \"[@a\")) (PARA (TEXT \"]\")))"}, + }) +} + +func TestEndnote(t *testing.T) { + t.Parallel() + checkTcs(t, false, TestCases{ + {"[^", "(INLINE (TEXT \"[^\"))"}, + {"[^]", "(INLINE (ENDNOTE ()))"}, + {"[^abc]", "(INLINE (ENDNOTE () (TEXT \"abc\")))"}, + {"[^abc ]", "(INLINE (ENDNOTE () (TEXT \"abc\")))"}, + {"[^abc\ndef]", "(INLINE (ENDNOTE () (TEXT \"abc\") (SOFT) (TEXT \"def\")))"}, + {"[^abc\n\ndef]", "(INLINE (TEXT \"[^abc\") (SOFT) (SOFT) (TEXT \"def]\"))"}, + {"[^abc[^def]]", "(INLINE (ENDNOTE () (TEXT \"abc\") (ENDNOTE () (TEXT \"def\"))))"}, + {"[^abc]{-}", "(INLINE (ENDNOTE ((\"-\" . \"\")) (TEXT \"abc\")))"}, + }) + checkTcs(t, true, TestCases{ + {"[^abc\n\ndef]", "(BLOCK (PARA (TEXT \"[^abc\")) (PARA (TEXT \"def]\")))"}, + }) +} + +func TestMark(t *testing.T) { + t.Parallel() + checkTcs(t, false, TestCases{ + {"[!", "(INLINE (TEXT \"[!\"))"}, + {"[!\n", "(INLINE (TEXT \"[!\"))"}, + {"[!]", "(INLINE (MARK \"\" \"\" \"\"))"}, + {"[!][!]", "(INLINE (MARK \"\" \"\" \"\") (MARK \"\" \"\" \"\"))"}, + {"[! ]", "(INLINE (TEXT \"[! ]\"))"}, + {"[!a]", "(INLINE (MARK \"a\" \"\" \"\"))"}, + {"[!a][!a]", "(INLINE (MARK \"a\" \"\" \"\") (MARK \"a\" \"\" \"\"))"}, + {"[!a ]", "(INLINE (TEXT \"[!a ]\"))"}, + {"[!a_]", "(INLINE (MARK \"a_\" \"\" \"\"))"}, + {"[!a_][!a]", "(INLINE (MARK \"a_\" \"\" \"\") (MARK \"a\" \"\" \"\"))"}, + {"[!a-b]", "(INLINE (MARK \"a-b\" \"\" \"\"))"}, + {"[!a|b]", "(INLINE (MARK \"a\" \"\" \"\" (TEXT \"b\")))"}, + {"[!a|]", "(INLINE (MARK \"a\" \"\" \"\"))"}, + {"[!|b]", "(INLINE (MARK \"\" \"\" \"\" (TEXT \"b\")))"}, + {"[!|b ]", "(INLINE (MARK \"\" \"\" \"\" (TEXT \"b\")))"}, + {"[!|b c]", "(INLINE (MARK \"\" \"\" \"\" (TEXT \"b c\")))"}, + }) +} + +func TestComment(t *testing.T) { + t.Parallel() + checkTcs(t, false, TestCases{ + {"%", "(INLINE (TEXT \"%\"))"}, + {"%%", "(INLINE (LITERAL-COMMENT () \"\"))"}, + {"%\n", "(INLINE (TEXT \"%\"))"}, + {"%%\n", "(INLINE (LITERAL-COMMENT () \"\"))"}, + {"%%a", "(INLINE (LITERAL-COMMENT () \"a\"))"}, + {"%%%a", "(INLINE (LITERAL-COMMENT () \"a\"))"}, + {"%% a", "(INLINE (LITERAL-COMMENT () \"a\"))"}, + {"%%% a", "(INLINE (LITERAL-COMMENT () \"a\"))"}, + {"%% % a", "(INLINE (LITERAL-COMMENT () \"% a\"))"}, + {"%%a", "(INLINE (LITERAL-COMMENT () \"a\"))"}, + {"a%%b", "(INLINE (TEXT \"a\") (LITERAL-COMMENT () \"b\"))"}, + {"a %%b", "(INLINE (TEXT \"a \") (LITERAL-COMMENT () \"b\"))"}, + {" %%b", "(INLINE (LITERAL-COMMENT () \"b\"))"}, + {"%%b ", "(INLINE (LITERAL-COMMENT () \"b \"))"}, + {"100%", "(INLINE (TEXT \"100%\"))"}, + {"%%{=}a", "(INLINE (LITERAL-COMMENT ((\"\" . \"\")) \"a\"))"}, + }) +} + +func TestFormat(t *testing.T) { + symMap := symbolMap{ + "_": sz.SymFormatEmph, + "*": sz.SymFormatStrong, + ">": sz.SymFormatInsert, + "~": sz.SymFormatDelete, + "^": sz.SymFormatSuper, + ",": sz.SymFormatSub, + "#": sz.SymFormatMark, + ":": sz.SymFormatSpan, + } + t.Parallel() + // Not for Insert / '>', because collision with quoted list + // Not for Quote / '"', because escaped representation. + for _, ch := range []string{"_", "*", "~", "^", ",", "#", ":"} { + checkTcs(t, false, replace(ch, symMap, TestCases{ + {"$", "(INLINE (TEXT \"$\"))"}, + {"$$", "(INLINE (TEXT \"$$\"))"}, + {"$$$", "(INLINE (TEXT \"$$$\"))"}, + {"$$$$", "(INLINE ($% ()))"}, + })) + } + // Not for Quote / '"', because escaped representation. + for _, ch := range []string{"_", "*", ">", "~", "^", ",", "#", ":"} { + checkTcs(t, false, replace(ch, symMap, TestCases{ + {"$$a$$", "(INLINE ($% () (TEXT \"a\")))"}, + {"$$a$$$", "(INLINE ($% () (TEXT \"a\")) (TEXT \"$\"))"}, + {"$$$a$$", "(INLINE ($% () (TEXT \"$a\")))"}, + {"$$$a$$$", "(INLINE ($% () (TEXT \"$a\")) (TEXT \"$\"))"}, + {"$\\$", "(INLINE (TEXT \"$$\"))"}, + {"$\\$$", "(INLINE (TEXT \"$$$\"))"}, + {"$$\\$", "(INLINE (TEXT \"$$$\"))"}, + {"$$a\\$$", "(INLINE (TEXT \"$$a$$\"))"}, + {"$$a$\\$", "(INLINE (TEXT \"$$a$$\"))"}, + {"$$a\\$$$", "(INLINE ($% () (TEXT \"a$\")))"}, + {"$$a\na$$", "(INLINE ($% () (TEXT \"a\") (SOFT) (TEXT \"a\")))"}, + {"$$a\n\na$$", "(INLINE (TEXT \"$$a\") (SOFT) (SOFT) (TEXT \"a$$\"))"}, + {"$$a$${go}", "(INLINE ($% ((\"go\" . \"\")) (TEXT \"a\")))"}, + })) + checkTcs(t, true, replace(ch, symMap, TestCases{ + {"$$a\n\na$$", "(BLOCK (PARA (TEXT \"$$a\")) (PARA (TEXT \"a$$\")))"}, + })) + } + checkTcs(t, false, replace(`"`, symbolMap{`"`: sz.SymFormatQuote}, TestCases{ + {"$", "(INLINE (TEXT \"\\\"\"))"}, + {"$$", "(INLINE (TEXT \"\\\"\\\"\"))"}, + {"$$$", "(INLINE (TEXT \"\\\"\\\"\\\"\"))"}, + {"$$$$", "(INLINE ($% ()))"}, + + {"$$a$$", "(INLINE ($% () (TEXT \"a\")))"}, + {"$$a$$$", "(INLINE ($% () (TEXT \"a\")) (TEXT \"\\\"\"))"}, + {"$$$a$$", "(INLINE ($% () (TEXT \"\\\"a\")))"}, + {"$$$a$$$", "(INLINE ($% () (TEXT \"\\\"a\")) (TEXT \"\\\"\"))"}, + {"$\\$", "(INLINE (TEXT \"\\\"\\\"\"))"}, + {"$\\$$", "(INLINE (TEXT \"\\\"\\\"\\\"\"))"}, + {"$$\\$", "(INLINE (TEXT \"\\\"\\\"\\\"\"))"}, + {"$$a\\$$", "(INLINE (TEXT \"\\\"\\\"a\\\"\\\"\"))"}, + {"$$a$\\$", "(INLINE (TEXT \"\\\"\\\"a\\\"\\\"\"))"}, + {"$$a\\$$$", "(INLINE ($% () (TEXT \"a\\\"\")))"}, + {"$$a\na$$", "(INLINE ($% () (TEXT \"a\") (SOFT) (TEXT \"a\")))"}, + {"$$a\n\na$$", "(INLINE (TEXT \"\\\"\\\"a\") (SOFT) (SOFT) (TEXT \"a\\\"\\\"\"))"}, + {"$$a$${go}", "(INLINE ($% ((\"go\" . \"\")) (TEXT \"a\")))"}, + })) + checkTcs(t, false, TestCases{ + {"__****__", "(INLINE (FORMAT-EMPH () (FORMAT-STRONG ())))"}, + {"__**a**__", "(INLINE (FORMAT-EMPH () (FORMAT-STRONG () (TEXT \"a\"))))"}, + {"__**__**", "(INLINE (TEXT \"__\") (FORMAT-STRONG () (TEXT \"__\")))"}, + }) +} + +func TestLiteral(t *testing.T) { + symMap := symbolMap{ + "@": sz.SymLiteralZettel, + "`": sz.SymLiteralProg, + "'": sz.SymLiteralInput, + "=": sz.SymLiteralOutput, + } + t.Parallel() + for _, ch := range []string{"@", "`", "'", "="} { + checkTcs(t, false, replace(ch, symMap, TestCases{ + {"$", "(INLINE (TEXT \"$\"))"}, + {"$$", "(INLINE (TEXT \"$$\"))"}, + {"$$$", "(INLINE (TEXT \"$$$\"))"}, + {"$$$$", "(INLINE ($% () \"\"))"}, + {"$$a$$", "(INLINE ($% () \"a\"))"}, + {"$$a$$$", "(INLINE ($% () \"a\") (TEXT \"$\"))"}, + {"$$$a$$", "(INLINE ($% () \"$a\"))"}, + {"$$$a$$$", "(INLINE ($% () \"$a\") (TEXT \"$\"))"}, + {"$\\$", "(INLINE (TEXT \"$$\"))"}, + {"$\\$$", "(INLINE (TEXT \"$$$\"))"}, + {"$$\\$", "(INLINE (TEXT \"$$$\"))"}, + {"$$a\\$$", "(INLINE (TEXT \"$$a$$\"))"}, + {"$$a$\\$", "(INLINE (TEXT \"$$a$$\"))"}, + {"$$a\\$$$", "(INLINE ($% () \"a$\"))"}, + {"$$a$${go}", "(INLINE ($% ((\"go\" . \"\")) \"a\"))"}, + })) + } + checkTcs(t, false, TestCases{ + {"''````''", "(INLINE (LITERAL-INPUT () \"````\"))"}, + {"''``a``''", "(INLINE (LITERAL-INPUT () \"``a``\"))"}, + {"''``''``", "(INLINE (LITERAL-INPUT () \"``\") (TEXT \"``\"))"}, + {"''\\'''", "(INLINE (LITERAL-INPUT () \"'\"))"}, + }) + checkTcs(t, false, TestCases{ + {"@@HTML@@{=html}", "(INLINE (LITERAL-HTML () \"HTML\"))"}, + {"@@HTML@@{=html lang=en}", "(INLINE (LITERAL-HTML ((\"lang\" . \"en\")) \"HTML\"))"}, + {"@@HTML@@{=html,lang=en}", "(INLINE (LITERAL-HTML ((\"lang\" . \"en\")) \"HTML\"))"}, + }) +} + +func TestLiteralMath(t *testing.T) { + t.Parallel() + checkTcs(t, false, TestCases{ + {"$", "(INLINE (TEXT \"$\"))"}, + {"$$", "(INLINE (TEXT \"$$\"))"}, + {"$$$", "(INLINE (TEXT \"$$$\"))"}, + {"$$$$", "(INLINE (LITERAL-MATH () \"\"))"}, + {"$$a$$", "(INLINE (LITERAL-MATH () \"a\"))"}, + {"$$a$$$", "(INLINE (LITERAL-MATH () \"a\") (TEXT \"$\"))"}, + {"$$$a$$", "(INLINE (LITERAL-MATH () \"$a\"))"}, + {"$$$a$$$", "(INLINE (LITERAL-MATH () \"$a\") (TEXT \"$\"))"}, + {`$\$`, "(INLINE (TEXT \"$$\"))"}, + {`$\$$`, "(INLINE (TEXT \"$$$\"))"}, + {`$$\$`, "(INLINE (TEXT \"$$$\"))"}, + {`$$a\$$`, "(INLINE (LITERAL-MATH () \"a\\\\\"))"}, + {`$$a$\$`, "(INLINE (TEXT \"$$a$$\"))"}, + {`$$a\$$$`, "(INLINE (LITERAL-MATH () \"a\\\\\") (TEXT \"$\"))"}, + {"$$a$${go}", "(INLINE (LITERAL-MATH ((\"go\" . \"\")) \"a\"))"}, + }) +} + +func TestMixFormatCode(t *testing.T) { + t.Parallel() + checkTcs(t, false, TestCases{ + {"__abc__\n**def**", "(INLINE (FORMAT-EMPH () (TEXT \"abc\")) (SOFT) (FORMAT-STRONG () (TEXT \"def\")))"}, + {"''abc''\n==def==", "(INLINE (LITERAL-INPUT () \"abc\") (SOFT) (LITERAL-OUTPUT () \"def\"))"}, + {"__abc__\n==def==", "(INLINE (FORMAT-EMPH () (TEXT \"abc\")) (SOFT) (LITERAL-OUTPUT () \"def\"))"}, + {"__abc__\n``def``", "(INLINE (FORMAT-EMPH () (TEXT \"abc\")) (SOFT) (LITERAL-CODE () \"def\"))"}, + { + "\"\"ghi\"\"\n::abc::\n``def``\n", + "(INLINE (FORMAT-QUOTE () (TEXT \"ghi\")) (SOFT) (FORMAT-SPAN () (TEXT \"abc\")) (SOFT) (LITERAL-CODE () \"def\"))", + }, + }) +} + +func TestNDash(t *testing.T) { + t.Parallel() + checkTcs(t, false, TestCases{ + {"--", "(INLINE (TEXT \"\u2013\"))"}, + {"a--b", "(INLINE (TEXT \"a\u2013b\"))"}, + }) +} + +func TestEntity(t *testing.T) { + t.Parallel() + checkTcs(t, false, TestCases{ + {"&", "(INLINE (TEXT \"&\"))"}, + {"&;", "(INLINE (TEXT \"&;\"))"}, + {"&#;", "(INLINE (TEXT \"&#;\"))"}, + {"a;", "(INLINE (TEXT \"a;\"))"}, + {"&#x;", "(INLINE (TEXT \"&#x;\"))"}, + {"�z;", "(INLINE (TEXT \"�z;\"))"}, + {"&1;", "(INLINE (TEXT \"&1;\"))"}, + {" ", "(INLINE (TEXT \" \"))"}, // Numeric entities below space are not allowed. + {"", "(INLINE (TEXT \"\"))"}, + + // Good cases + {"<", "(INLINE (TEXT \"<\"))"}, + {"0", "(INLINE (TEXT \"0\"))"}, + {"J", "(INLINE (TEXT \"J\"))"}, + {"J", "(INLINE (TEXT \"J\"))"}, + {"…", "(INLINE (TEXT \"\u2026\"))"}, + {" ", "(INLINE (TEXT \"\u00a0\"))"}, + {"E: &,?;c.", "(INLINE (TEXT \"E: &,?;c.\"))"}, + }) +} + +func TestVerbatimZettel(t *testing.T) { + t.Parallel() + checkTcs(t, true, TestCases{ + {"@@@\n@@@", "()"}, + {"@@@\nabc\n@@@", "(BLOCK (VERBATIM-ZETTEL () \"abc\"))"}, + {"@@@@def\nabc\n@@@@", "(BLOCK (VERBATIM-ZETTEL ((\"\" . \"def\")) \"abc\"))"}, + }) +} + +func TestVerbatimCode(t *testing.T) { + t.Parallel() + checkTcs(t, true, TestCases{ + {"```\n```", "()"}, + {"```\nabc\n```", "(BLOCK (VERBATIM-CODE () \"abc\"))"}, + {"```\nabc\n````", "(BLOCK (VERBATIM-CODE () \"abc\"))"}, + {"````\nabc\n````", "(BLOCK (VERBATIM-CODE () \"abc\"))"}, + {"````\nabc\n```\n````", "(BLOCK (VERBATIM-CODE () \"abc\\n```\"))"}, + {"````go\nabc\n````", "(BLOCK (VERBATIM-CODE ((\"\" . \"go\")) \"abc\"))"}, + }) +} + +func TestVerbatimEval(t *testing.T) { + t.Parallel() + checkTcs(t, true, TestCases{ + {"~~~\n~~~", "()"}, + {"~~~\nabc\n~~~", "(BLOCK (VERBATIM-EVAL () \"abc\"))"}, + {"~~~\nabc\n~~~~", "(BLOCK (VERBATIM-EVAL () \"abc\"))"}, + {"~~~~\nabc\n~~~~", "(BLOCK (VERBATIM-EVAL () \"abc\"))"}, + {"~~~~\nabc\n~~~\n~~~~", "(BLOCK (VERBATIM-EVAL () \"abc\\n~~~\"))"}, + {"~~~~go\nabc\n~~~~", "(BLOCK (VERBATIM-EVAL ((\"\" . \"go\")) \"abc\"))"}, + }) +} + +func TestVerbatimMath(t *testing.T) { + t.Parallel() + checkTcs(t, true, TestCases{ + {"$$$\n$$$", "()"}, + {"$$$\nabc\n$$$", "(BLOCK (VERBATIM-MATH () \"abc\"))"}, + {"$$$\nabc\n$$$$", "(BLOCK (VERBATIM-MATH () \"abc\"))"}, + {"$$$$\nabc\n$$$$", "(BLOCK (VERBATIM-MATH () \"abc\"))"}, + {"$$$$\nabc\n$$$\n$$$$", "(BLOCK (VERBATIM-MATH () \"abc\\n$$$\"))"}, + {"$$$$go\nabc\n$$$$", "(BLOCK (VERBATIM-MATH ((\"\" . \"go\")) \"abc\"))"}, + }) +} + +func TestVerbatimComment(t *testing.T) { + t.Parallel() + checkTcs(t, true, TestCases{ + {"%%%\n%%%", "()"}, + {"%%%\nabc\n%%%", "(BLOCK (VERBATIM-COMMENT () \"abc\"))"}, + {"%%%%go\nabc\n%%%%", "(BLOCK (VERBATIM-COMMENT ((\"\" . \"go\")) \"abc\"))"}, + }) +} + +func TestPara(t *testing.T) { + t.Parallel() + checkTcs(t, true, TestCases{ + {"a\n\nb", "(BLOCK (PARA (TEXT \"a\")) (PARA (TEXT \"b\")))"}, + {"a\n \nb", "(BLOCK (PARA (TEXT \"a\") (SOFT) (HARD) (TEXT \"b\")))"}, + }) +} + +func TestSpanRegion(t *testing.T) { + t.Parallel() + checkTcs(t, true, TestCases{ + {":::\n:::", "()"}, + {":::\nabc\n:::", "(BLOCK (REGION-BLOCK () ((PARA (TEXT \"abc\")))))"}, + {":::\nabc\n::::", "(BLOCK (REGION-BLOCK () ((PARA (TEXT \"abc\")))))"}, + {"::::\nabc\n::::", "(BLOCK (REGION-BLOCK () ((PARA (TEXT \"abc\")))))"}, + {"::::\nabc\n:::\ndef\n:::\n::::", "(BLOCK (REGION-BLOCK () ((PARA (TEXT \"abc\")) (REGION-BLOCK () ((PARA (TEXT \"def\")))))))"}, + {":::{go}\n:::a", "(BLOCK (REGION-BLOCK ((\"go\" . \"\")) () (TEXT \"a\")))"}, + {":::\nabc\n::: def ", "(BLOCK (REGION-BLOCK () ((PARA (TEXT \"abc\"))) (TEXT \"def\")))"}, + }) +} + +func TestQuoteRegion(t *testing.T) { + t.Parallel() + checkTcs(t, true, TestCases{ + {"<<<\n<<<", "()"}, + {"<<<\nabc\n<<<", "(BLOCK (REGION-QUOTE () ((PARA (TEXT \"abc\")))))"}, + {"<<<\nabc\n<<<<", "(BLOCK (REGION-QUOTE () ((PARA (TEXT \"abc\")))))"}, + {"<<<<\nabc\n<<<<", "(BLOCK (REGION-QUOTE () ((PARA (TEXT \"abc\")))))"}, + {"<<<<\nabc\n<<<\ndef\n<<<\n<<<<", "(BLOCK (REGION-QUOTE () ((PARA (TEXT \"abc\")) (REGION-QUOTE () ((PARA (TEXT \"def\")))))))"}, + {"<<" in the following, because quotation lists may have empty items. + for _, ch := range []string{"*", "#"} { + checkTcs(t, true, replace(ch, nil, TestCases{ + {"$", "(BLOCK (PARA (TEXT \"$\")))"}, + {"$$", "(BLOCK (PARA (TEXT \"$$\")))"}, + {"$$$", "(BLOCK (PARA (TEXT \"$$$\")))"}, + {"$ ", "(BLOCK (PARA (TEXT \"$\")))"}, + {"$$ ", "(BLOCK (PARA (TEXT \"$$\")))"}, + {"$$$ ", "(BLOCK (PARA (TEXT \"$$$\")))"}, + })) + } + checkTcs(t, true, TestCases{ + {"* abc", "(BLOCK (UNORDERED (BLOCK (PARA (TEXT \"abc\")))))"}, + {"** abc", "(BLOCK (UNORDERED (BLOCK (UNORDERED (BLOCK (PARA (TEXT \"abc\")))))))"}, + {"*** abc", "(BLOCK (UNORDERED (BLOCK (UNORDERED (BLOCK (UNORDERED (BLOCK (PARA (TEXT \"abc\")))))))))"}, + {"**** abc", "(BLOCK (UNORDERED (BLOCK (UNORDERED (BLOCK (UNORDERED (BLOCK (UNORDERED (BLOCK (PARA (TEXT \"abc\")))))))))))"}, + {"** abc\n**** def", "(BLOCK (UNORDERED (BLOCK (UNORDERED (BLOCK (PARA (TEXT \"abc\")) (UNORDERED (BLOCK (UNORDERED (BLOCK (PARA (TEXT \"def\")))))))))))"}, + {"* abc\ndef", "(BLOCK (UNORDERED (BLOCK (PARA (TEXT \"abc\")))) (PARA (TEXT \"def\")))"}, + {"* abc\n def", "(BLOCK (UNORDERED (BLOCK (PARA (TEXT \"abc\")))) (PARA (TEXT \"def\")))"}, + {"* abc\n* def", "(BLOCK (UNORDERED (BLOCK (PARA (TEXT \"abc\"))) (BLOCK (PARA (TEXT \"def\")))))"}, + {"* abc\n def", "(BLOCK (UNORDERED (BLOCK (PARA (TEXT \"abc\") (SOFT) (TEXT \"def\")))))"}, + {"* abc\n def", "(BLOCK (UNORDERED (BLOCK (PARA (TEXT \"abc\") (SOFT) (TEXT \"def\")))))"}, + {"* abc\n\ndef", "(BLOCK (UNORDERED (BLOCK (PARA (TEXT \"abc\")))) (PARA (TEXT \"def\")))"}, + {"* abc\n\n def", "(BLOCK (UNORDERED (BLOCK (PARA (TEXT \"abc\")))) (PARA (TEXT \"def\")))"}, + {"* abc\n\n def", "(BLOCK (UNORDERED (BLOCK (PARA (TEXT \"abc\")) (PARA (TEXT \"def\")))))"}, + {"* abc\n\n def", "(BLOCK (UNORDERED (BLOCK (PARA (TEXT \"abc\")) (PARA (TEXT \"def\")))))"}, + {"* abc\n** def", "(BLOCK (UNORDERED (BLOCK (PARA (TEXT \"abc\")) (UNORDERED (BLOCK (PARA (TEXT \"def\")))))))"}, + {"* abc\n** def\n* ghi", "(BLOCK (UNORDERED (BLOCK (PARA (TEXT \"abc\")) (UNORDERED (BLOCK (PARA (TEXT \"def\"))))) (BLOCK (PARA (TEXT \"ghi\")))))"}, + {"* abc\n\n def\n* ghi", "(BLOCK (UNORDERED (BLOCK (PARA (TEXT \"abc\")) (PARA (TEXT \"def\"))) (BLOCK (PARA (TEXT \"ghi\")))))"}, + {"* abc\n** def\n ghi\n jkl", "(BLOCK (UNORDERED (BLOCK (PARA (TEXT \"abc\")) (UNORDERED (BLOCK (PARA (TEXT \"def\") (SOFT) (TEXT \"ghi\")))) (PARA (TEXT \"jkl\")))))"}, + + // A list does not last beyond a region + {":::\n# abc\n:::\n# def", "(BLOCK (REGION-BLOCK () ((ORDERED (BLOCK (PARA (TEXT \"abc\")))))) (ORDERED (BLOCK (PARA (TEXT \"def\")))))"}, + + // A HRule creates a new list + {"* abc\n---\n* def", "(BLOCK (UNORDERED (BLOCK (PARA (TEXT \"abc\")))) (THEMATIC ()) (UNORDERED (BLOCK (PARA (TEXT \"def\")))))"}, + + // Changing list type adds a new list + {"* abc\n# def", "(BLOCK (UNORDERED (BLOCK (PARA (TEXT \"abc\")))) (ORDERED (BLOCK (PARA (TEXT \"def\")))))"}, + + // Quotation lists may have empty items + {">", "(BLOCK (QUOTATION (BLOCK)))"}, + + // Empty continuation + {"* abc\n ", "(BLOCK (UNORDERED (BLOCK (PARA (TEXT \"abc\")))))"}, + }) +} + +func TestQuoteList(t *testing.T) { + t.Parallel() + checkTcs(t, true, TestCases{ + {"> w1 w2", "(BLOCK (QUOTATION (BLOCK (PARA (TEXT \"w1 w2\")))))"}, + {"> w1\n> w2", "(BLOCK (QUOTATION (BLOCK (PARA (TEXT \"w1\") (SOFT) (TEXT \"w2\")))))"}, + {"> w1\n>w2", "(BLOCK (QUOTATION (BLOCK (PARA (TEXT \"w1\")))) (PARA (TEXT \">w2\")))"}, + {"> w1\n>\n>w2", "(BLOCK (QUOTATION (BLOCK (PARA (TEXT \"w1\"))) (BLOCK)) (PARA (TEXT \">w2\")))"}, + {"> w1\n> \n> w2", "(BLOCK (QUOTATION (BLOCK (PARA (TEXT \"w1\"))) (BLOCK) (BLOCK (PARA (TEXT \"w2\")))))"}, + }) +} + +func TestEnumAfterPara(t *testing.T) { + t.Parallel() + checkTcs(t, true, TestCases{ + {"abc\n* def", "(BLOCK (PARA (TEXT \"abc\")) (UNORDERED (BLOCK (PARA (TEXT \"def\")))))"}, + {"abc\n*def", "(BLOCK (PARA (TEXT \"abc\") (SOFT) (TEXT \"*def\")))"}, + }) +} + +func TestDefinition(t *testing.T) { + t.Parallel() + checkTcs(t, true, TestCases{ + {";", "(BLOCK (PARA (TEXT \";\")))"}, + {"; ", "(BLOCK (PARA (TEXT \";\")))"}, + {"; abc", "(BLOCK (DESCRIPTION ((TEXT \"abc\"))))"}, + {"; abc\ndef", "(BLOCK (DESCRIPTION ((TEXT \"abc\"))) (PARA (TEXT \"def\")))"}, + {"; abc\n def", "(BLOCK (DESCRIPTION ((TEXT \"abc\"))) (PARA (TEXT \"def\")))"}, + {"; abc\n def", "(BLOCK (DESCRIPTION ((TEXT \"abc\") (SOFT) (TEXT \"def\"))))"}, + {"; abc\n def\n ghi", "(BLOCK (DESCRIPTION ((TEXT \"abc\") (SOFT) (TEXT \"def\") (SOFT) (TEXT \"ghi\"))))"}, + {":", "(BLOCK (PARA (TEXT \":\")))"}, + {": ", "(BLOCK (PARA (TEXT \":\")))"}, + {": abc", "(BLOCK (PARA (TEXT \": abc\")))"}, + {"; abc\n: def", "(BLOCK (DESCRIPTION ((TEXT \"abc\")) (BLOCK (BLOCK (PARA (TEXT \"def\"))))))"}, + {"; abc\n: def\nghi", "(BLOCK (DESCRIPTION ((TEXT \"abc\")) (BLOCK (BLOCK (PARA (TEXT \"def\"))))) (PARA (TEXT \"ghi\")))"}, + {"; abc\n: def\n ghi", "(BLOCK (DESCRIPTION ((TEXT \"abc\")) (BLOCK (BLOCK (PARA (TEXT \"def\"))))) (PARA (TEXT \"ghi\")))"}, + {"; abc\n: def\n ghi", "(BLOCK (DESCRIPTION ((TEXT \"abc\")) (BLOCK (BLOCK (PARA (TEXT \"def\") (SOFT) (TEXT \"ghi\"))))))"}, + {"; abc\n: def\n\n ghi", "(BLOCK (DESCRIPTION ((TEXT \"abc\")) (BLOCK (BLOCK (PARA (TEXT \"def\"))) (PARA (TEXT \"ghi\")))))"}, + {"; abc\n:", "(BLOCK (DESCRIPTION ((TEXT \"abc\"))) (PARA (TEXT \":\")))"}, + {"; abc\n: def\n: ghi", "(BLOCK (DESCRIPTION ((TEXT \"abc\")) (BLOCK (BLOCK (PARA (TEXT \"def\"))) (BLOCK (PARA (TEXT \"ghi\"))))))"}, + {"; abc\n: def\n; ghi\n: jkl", "(BLOCK (DESCRIPTION ((TEXT \"abc\")) (BLOCK (BLOCK (PARA (TEXT \"def\")))) ((TEXT \"ghi\")) (BLOCK (BLOCK (PARA (TEXT \"jkl\"))))))"}, + + // Empty description + {"; abc\n: ", "(BLOCK (DESCRIPTION ((TEXT \"abc\"))) (PARA (TEXT \":\")))"}, + // Empty continuation of definition + {"; abc\n: def\n ", "(BLOCK (DESCRIPTION ((TEXT \"abc\")) (BLOCK (BLOCK (PARA (TEXT \"def\"))))))"}, + }) +} + +func TestTable(t *testing.T) { + t.Parallel() + checkTcs(t, true, TestCases{ + {"|", "()"}, + {"||", "(BLOCK (TABLE () ((CELL))))"}, + {"| |", "(BLOCK (TABLE () ((CELL))))"}, + {"|a", "(BLOCK (TABLE () ((CELL (TEXT \"a\")))))"}, + {"|a|", "(BLOCK (TABLE () ((CELL (TEXT \"a\")))))"}, + {"|a| ", "(BLOCK (TABLE () ((CELL (TEXT \"a\")) (CELL))))"}, + {"|a|b", "(BLOCK (TABLE () ((CELL (TEXT \"a\")) (CELL (TEXT \"b\")))))"}, + {"|a\n|b", "(BLOCK (TABLE () ((CELL (TEXT \"a\"))) ((CELL (TEXT \"b\")))))"}, + {"|a|b\n|c|d", "(BLOCK (TABLE () ((CELL (TEXT \"a\")) (CELL (TEXT \"b\"))) ((CELL (TEXT \"c\")) (CELL (TEXT \"d\")))))"}, + {"|%", "()"}, + {"|=a", "(BLOCK (TABLE ((CELL (TEXT \"a\")))))"}, + {"|=a\n|b", "(BLOCK (TABLE ((CELL (TEXT \"a\"))) ((CELL (TEXT \"b\")))))"}, + {"|a|b\n|%---\n|c|d", "(BLOCK (TABLE () ((CELL (TEXT \"a\")) (CELL (TEXT \"b\"))) ((CELL (TEXT \"c\")) (CELL (TEXT \"d\")))))"}, + {"|a|b\n|c", "(BLOCK (TABLE () ((CELL (TEXT \"a\")) (CELL (TEXT \"b\"))) ((CELL (TEXT \"c\")) (CELL))))"}, + {"|=\n|b|c", "(BLOCK (TABLE ((CELL-LEFT (TEXT \"a\")) (CELL)) ((CELL-RIGHT (TEXT \"b\")) (CELL (TEXT \"c\")))))"}, + {"|=\n||", "(BLOCK (TABLE ((CELL-LEFT (TEXT \"a\")) (CELL-RIGHT (TEXT \"b\"))) ((CELL) (CELL-RIGHT))))"}, + }) +} + +func TestTransclude(t *testing.T) { + t.Parallel() + checkTcs(t, true, TestCases{ + {"{{{a}}}", "(BLOCK (TRANSCLUDE () (EXTERNAL \"a\")))"}, + {"{{{a}}}b", "(BLOCK (TRANSCLUDE ((\"\" . \"b\")) (EXTERNAL \"a\")))"}, + {"{{{a}}}}", "(BLOCK (TRANSCLUDE () (EXTERNAL \"a\")))"}, + {"{{{a\\}}}}", "(BLOCK (TRANSCLUDE () (EXTERNAL \"a\\\\}\")))"}, + {"{{{a\\}}}}b", "(BLOCK (TRANSCLUDE ((\"\" . \"b\")) (EXTERNAL \"a\\\\}\")))"}, + {"{{{a}}", "(BLOCK (PARA (TEXT \"{\") (EMBED () \"a\")))"}, + {"{{{a}}}{go=b}", "(BLOCK (TRANSCLUDE ((\"go\" . \"b\")) (EXTERNAL \"a\")))"}, + }) +} + +func TestBlockAttr(t *testing.T) { + t.Parallel() + checkTcs(t, true, TestCases{ + {":::go\na\n:::", "(BLOCK (REGION-BLOCK ((\"\" . \"go\")) ((PARA (TEXT \"a\")))))"}, + {":::go=\na\n:::", "(BLOCK (REGION-BLOCK ((\"\" . \"go\")) ((PARA (TEXT \"a\")))))"}, + {":::{}\na\n:::", "(BLOCK (REGION-BLOCK () ((PARA (TEXT \"a\")))))"}, + {":::{ }\na\n:::", "(BLOCK (REGION-BLOCK () ((PARA (TEXT \"a\")))))"}, + {":::{.go}\na\n:::", "(BLOCK (REGION-BLOCK ((\"class\" . \"go\")) ((PARA (TEXT \"a\")))))"}, + {":::{=go}\na\n:::", "(BLOCK (REGION-BLOCK ((\"\" . \"go\")) ((PARA (TEXT \"a\")))))"}, + {":::{go}\na\n:::", "(BLOCK (REGION-BLOCK ((\"go\" . \"\")) ((PARA (TEXT \"a\")))))"}, + {":::{go=py}\na\n:::", "(BLOCK (REGION-BLOCK ((\"go\" . \"py\")) ((PARA (TEXT \"a\")))))"}, + {":::{.go=py}\na\n:::", "(BLOCK (REGION-BLOCK () ((PARA (TEXT \"a\")))))"}, + {":::{go=}\na\n:::", "(BLOCK (REGION-BLOCK ((\"go\" . \"\")) ((PARA (TEXT \"a\")))))"}, + {":::{.go=}\na\n:::", "(BLOCK (REGION-BLOCK () ((PARA (TEXT \"a\")))))"}, + {":::{go py}\na\n:::", "(BLOCK (REGION-BLOCK ((\"go\" . \"\") (\"py\" . \"\")) ((PARA (TEXT \"a\")))))"}, + {":::{go\npy}\na\n:::", "(BLOCK (REGION-BLOCK ((\"go\" . \"\") (\"py\" . \"\")) ((PARA (TEXT \"a\")))))"}, + {":::{.go py}\na\n:::", "(BLOCK (REGION-BLOCK ((\"class\" . \"go\") (\"py\" . \"\")) ((PARA (TEXT \"a\")))))"}, + {":::{go .py}\na\n:::", "(BLOCK (REGION-BLOCK ((\"class\" . \"py\") (\"go\" . \"\")) ((PARA (TEXT \"a\")))))"}, + {":::{.go py=3}\na\n:::", "(BLOCK (REGION-BLOCK ((\"class\" . \"go\") (\"py\" . \"3\")) ((PARA (TEXT \"a\")))))"}, + {"::: { go } \na\n:::", "(BLOCK (REGION-BLOCK ((\"go\" . \"\")) ((PARA (TEXT \"a\")))))"}, + {"::: { .go } \na\n:::", "(BLOCK (REGION-BLOCK ((\"class\" . \"go\")) ((PARA (TEXT \"a\")))))"}, + }) + checkTcs(t, true, replace("\"", nil, TestCases{ + {":::{py=3}\na\n:::", "(BLOCK (REGION-BLOCK ((\"py\" . \"3\")) ((PARA (TEXT \"a\")))))"}, + {":::{py=$2 3$}\na\n:::", "(BLOCK (REGION-BLOCK ((\"py\" . \"2 3\")) ((PARA (TEXT \"a\")))))"}, + {":::{py=$2\\$3$}\na\n:::", "(BLOCK (REGION-BLOCK ((\"py\" . \"2\\\"3\")) ((PARA (TEXT \"a\")))))"}, + {":::{py=2$3}\na\n:::", "(BLOCK (REGION-BLOCK ((\"py\" . \"2\\\"3\")) ((PARA (TEXT \"a\")))))"}, + {":::{py=$2\n3$}\na\n:::", "(BLOCK (REGION-BLOCK ((\"py\" . \"2\\n3\")) ((PARA (TEXT \"a\")))))"}, + {":::{py=$2 3}\na\n:::", "(BLOCK (REGION-BLOCK () ((PARA (TEXT \"a\")))))"}, + {":::{py=2 py=3}\na\n:::", "(BLOCK (REGION-BLOCK ((\"py\" . \"2 3\")) ((PARA (TEXT \"a\")))))"}, + {":::{.go .py}\na\n:::", "(BLOCK (REGION-BLOCK ((\"class\" . \"go py\")) ((PARA (TEXT \"a\")))))"}, + {":::{go go}\na\n:::", "(BLOCK (REGION-BLOCK ((\"go\" . \"\")) ((PARA (TEXT \"a\")))))"}, + {":::{=py =go}\na\n:::", "(BLOCK (REGION-BLOCK ((\"\" . \"go\")) ((PARA (TEXT \"a\")))))"}, + })) +} + +func TestInlineAttr(t *testing.T) { + t.Parallel() + checkTcs(t, true, TestCases{ + {"::a::{}", "(BLOCK (PARA (FORMAT-SPAN () (TEXT \"a\"))))"}, + {"::a::{ }", "(BLOCK (PARA (FORMAT-SPAN () (TEXT \"a\"))))"}, + {"::a::{.go}", "(BLOCK (PARA (FORMAT-SPAN ((\"class\" . \"go\")) (TEXT \"a\"))))"}, + {"::a::{=go}", "(BLOCK (PARA (FORMAT-SPAN ((\"\" . \"go\")) (TEXT \"a\"))))"}, + {"::a::{go}", "(BLOCK (PARA (FORMAT-SPAN ((\"go\" . \"\")) (TEXT \"a\"))))"}, + {"::a::{go=py}", "(BLOCK (PARA (FORMAT-SPAN ((\"go\" . \"py\")) (TEXT \"a\"))))"}, + {"::a::{.go=py}", "(BLOCK (PARA (FORMAT-SPAN () (TEXT \"a\")) (TEXT \"{.go=py}\")))"}, + {"::a::{go=}", "(BLOCK (PARA (FORMAT-SPAN ((\"go\" . \"\")) (TEXT \"a\"))))"}, + {"::a::{.go=}", "(BLOCK (PARA (FORMAT-SPAN () (TEXT \"a\")) (TEXT \"{.go=}\")))"}, + {"::a::{go py}", "(BLOCK (PARA (FORMAT-SPAN ((\"go\" . \"\") (\"py\" . \"\")) (TEXT \"a\"))))"}, + {"::a::{go\npy}", "(BLOCK (PARA (FORMAT-SPAN ((\"go\" . \"\") (\"py\" . \"\")) (TEXT \"a\"))))"}, + {"::a::{.go py}", "(BLOCK (PARA (FORMAT-SPAN ((\"class\" . \"go\") (\"py\" . \"\")) (TEXT \"a\"))))"}, + {"::a::{go .py}", "(BLOCK (PARA (FORMAT-SPAN ((\"class\" . \"py\") (\"go\" . \"\")) (TEXT \"a\"))))"}, + {"::a::{ \n go \n .py\n \n}", "(BLOCK (PARA (FORMAT-SPAN ((\"class\" . \"py\") (\"go\" . \"\")) (TEXT \"a\"))))"}, + {"::a::{ \n go \n .py\n\n}", "(BLOCK (PARA (FORMAT-SPAN ((\"class\" . \"py\") (\"go\" . \"\")) (TEXT \"a\"))))"}, + {"::a::{\ngo\n}", "(BLOCK (PARA (FORMAT-SPAN ((\"go\" . \"\")) (TEXT \"a\"))))"}, + }) + checkTcs(t, true, TestCases{ + {"::a::{py=3}", "(BLOCK (PARA (FORMAT-SPAN ((\"py\" . \"3\")) (TEXT \"a\"))))"}, + {"::a::{py=\"2 3\"}", "(BLOCK (PARA (FORMAT-SPAN ((\"py\" . \"2 3\")) (TEXT \"a\"))))"}, + {"::a::{py=\"2\\\"3\"}", "(BLOCK (PARA (FORMAT-SPAN ((\"py\" . \"2\\\"3\")) (TEXT \"a\"))))"}, + {"::a::{py=2\"3}", "(BLOCK (PARA (FORMAT-SPAN ((\"py\" . \"2\\\"3\")) (TEXT \"a\"))))"}, + {"::a::{py=\"2\n3\"}", "(BLOCK (PARA (FORMAT-SPAN ((\"py\" . \"2\\n3\")) (TEXT \"a\"))))"}, + {"::a::{py=\"2 3}", "(BLOCK (PARA (FORMAT-SPAN () (TEXT \"a\")) (TEXT \"{py=\\\"2 3}\")))"}, + }) + checkTcs(t, true, TestCases{ + {"::a::{py=2 py=3}", "(BLOCK (PARA (FORMAT-SPAN ((\"py\" . \"2 3\")) (TEXT \"a\"))))"}, + {"::a::{.go .py}", "(BLOCK (PARA (FORMAT-SPAN ((\"class\" . \"go py\")) (TEXT \"a\"))))"}, + }) +} + +func TestTemp(t *testing.T) { + t.Parallel() + checkTcs(t, true, TestCases{ + {"", "()"}, + }) +} Index: text/text.go ================================================================== --- text/text.go +++ text/text.go @@ -4,63 +4,50 @@ // This file is part of zettelstore-client. // // Zettelstore client is licensed under the latest version of the EUPL // (European Union Public License). Please see file LICENSE.txt for your rights // and obligations under this license. +// +// SPDX-License-Identifier: EUPL-1.2 +// SPDX-FileCopyrightText: 2022-present Detlef Stern //----------------------------------------------------------------------------- // Package text provides types, constants and function to work with text output. package text import ( "strings" - "zettelstore.de/client.fossil/sz" - "zettelstore.de/sx.fossil" + "t73f.de/r/sx" + "t73f.de/r/zsc/input" + "t73f.de/r/zsc/sz" ) // Encoder is the structure to hold relevant data to execute the encoding. type Encoder struct { - sf sx.SymbolFactory sb strings.Builder - - symText *sx.Symbol - symSpace *sx.Symbol - symSoft *sx.Symbol - symHard *sx.Symbol - symQuote *sx.Symbol } -func NewEncoder(sf sx.SymbolFactory) *Encoder { - if sf == nil { - return nil - } +// NewEncoder returns a new text encoder. +func NewEncoder() *Encoder { enc := &Encoder{ - sf: sf, - sb: strings.Builder{}, - symText: sf.MustMake(sz.NameSymText), - symSpace: sf.MustMake(sz.NameSymSpace), - symSoft: sf.MustMake(sz.NameSymSoft), - symHard: sf.MustMake(sz.NameSymHard), - symQuote: sf.MustMake(sz.NameSymQuote), + 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 } // EvaluateInlineString returns the text content of the given inline list as a string. func EvaluateInlineString(lst *sx.Pair) string { - if sf := sx.FindSymbolFactory(lst); sf != nil { - return NewEncoder(sf).Encode(lst) - } - return "" + return NewEncoder().Encode(lst) } func (enc *Encoder) executeList(lst *sx.Pair) { for elem := lst; elem != nil; elem = elem.Tail() { enc.execute(elem.Car()) @@ -73,21 +60,32 @@ } sym := cmd.Car() if sx.IsNil(sym) { return } - if sym.IsEqual(enc.symText) { + if sym.IsEqual(sz.SymText) { args := cmd.Tail() if args == nil { return } if val, isString := sx.GetString(args.Car()); isString { - enc.sb.WriteString(val.String()) + hadSpace := false + for _, ch := range val.GetValue() { + if input.IsSpace(ch) { + if !hadSpace { + enc.sb.WriteByte(' ') + hadSpace = true + } + } else { + enc.sb.WriteRune(ch) + hadSpace = false + } + } } - } else if sym.IsEqual(enc.symSpace) || sym.IsEqual(enc.symSoft) { + } else if sym.IsEqual(sz.SymSoft) { enc.sb.WriteByte(' ') - } else if sym.IsEqual(enc.symHard) { + } else if sym.IsEqual(sz.SymHard) { enc.sb.WriteByte('\n') - } else if !sym.IsEqual(enc.symQuote) { + } else if !sym.IsEqual(sx.SymbolQuote) { enc.executeList(cmd.Tail()) } } Index: text/text_test.go ================================================================== --- text/text_test.go +++ text/text_test.go @@ -4,31 +4,35 @@ // This file is part of zettelstore-client. // // Zettelstore client is licensed under the latest version of the EUPL // (European Union Public License). Please see file LICENSE.txt for your rights // and obligations under this license. +// +// SPDX-License-Identifier: EUPL-1.2 +// SPDX-FileCopyrightText: 2022-present Detlef Stern //----------------------------------------------------------------------------- package text_test import ( "strings" "testing" - "zettelstore.de/client.fossil/text" - "zettelstore.de/sx.fossil" - "zettelstore.de/sx.fossil/sxreader" + "t73f.de/r/sx" + "t73f.de/r/sx/sxreader" + "t73f.de/r/zsc/text" ) func TestSzText(t *testing.T) { testcases := []struct { src string exp string }{ {"()", ""}, {`(INLINE (TEXT "a"))`, "a"}, - {`(INLINE (SPACE "a"))`, " "}, + {`(INLINE (TEXT " "))`, " "}, + {`(INLINE (TEXT " "))`, " "}, } for i, tc := range testcases { sval, err := sxreader.MakeReader(strings.NewReader(tc.src)).Read() if err != nil { t.Error(err) Index: www/changes.wiki ================================================================== --- www/changes.wiki +++ www/changes.wiki @@ -1,9 +1,52 @@ Change Log + +

Changes for Version 0.20.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. + * Rename to be package t73f.de/r/zsc + * Reserve some zettel identifier for future use + * Mark client.Client.RenameZettel as deprecated + * Remove space node from (Sx-) AST + + +

Changes for Version 0.17.0 (2024-03-04)

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

Changes for Version 0.16.0 (2023-11-30)

+ * Refactor shtml transformator to support evaluating the language tree of a + zettel AST. Its API has been changes, since evaluation is now top-down, + where previous transformation was bottom.up. + * Add API call to retrieve role zettel. + * Added constants for role zettel and to mark text within zettelmarkup. + + +

Changes for Version 0.15.0 (2023-10-26)

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

Changes for Version 0.14.0 (2023-09-23)

+ * Remove support for JSON encoding + -

Changes for Version 0.13.0 (pending)

+

Changes for Version 0.13.0 (2023-08-07)

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

Changes for Version 0.12.0 (2023-06-05)

Index: www/index.wiki ================================================================== --- www/index.wiki +++ www/index.wiki @@ -1,12 +1,35 @@ Home This repository contains Go client software to access [https://zettelstore.de|Zettelstore] via its API. -

Latest Release: 0.13.0 (2023-08-07)

- * [./changes.wiki#0_13|Change summary] - * [/timeline?p=v0.13.0&bt=v0.20.0&y=ci|Check-ins for version 0.13.0], - [/vdiff?to=v0.13.0&from=v0.20.0|content diff] - * [/timeline?df=v0.13.0&y=ci|Check-ins derived from the 0.13.0 release], - [/vdiff?from=v0.13.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 [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 + +where HASH is the hash value of the commit you want to use. + +Go currently seems not to support software versions when the software is +managed by Fossil. This explains the need for the hash value. However, this +methods works and you can use the client software to access a Zettelstore.