Index: Makefile ================================================================== --- Makefile +++ Makefile @@ -5,18 +5,15 @@ ## ## Zettelstore 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. -.PHONY: check relcheck api build release clean +.PHONY: check api build release clean check: go run tools/build.go check -relcheck: - go run tools/build.go relcheck - api: go run tools/build.go testapi version: @echo $(shell go run tools/build.go version) Index: README.md ================================================================== --- README.md +++ README.md @@ -11,16 +11,10 @@ To get an initial impression, take a look at the [manual](https://zettelstore.de/manual/). It is a live example of the zettelstore software, running in read-only mode. -[Zettelstore Client](https://zettelstore.de/client) provides client -software to access Zettelstore via its API more easily, [Zettelstore -Contrib](https://zettelstore.de/contrib) contains contributed software, which -often connects to Zettelstore via its API. Some of the software packages may be -experimental. - The software, including the manual, is licensed under the [European Union Public License 1.2 (or later)](https://zettelstore.de/home/file?name=LICENSE.txt&ci=trunk). [Stay tuned](https://twitter.com/zettelstore)… Index: VERSION ================================================================== --- VERSION +++ VERSION @@ -1,1 +1,1 @@ -0.1.2 +0.0.15 ADDED api/api.go Index: api/api.go ================================================================== --- api/api.go +++ api/api.go @@ -0,0 +1,85 @@ +//----------------------------------------------------------------------------- +// Copyright (c) 2021 Detlef Stern +// +// This file is part of zettelstore. +// +// Zettelstore is licensed under the latest version of the EUPL (European Union +// Public License). Please see file LICENSE.txt for your rights and obligations +// under this license. +//----------------------------------------------------------------------------- + +// Package api contains common definition used for client and server. +package api + +// AuthJSON contains the result of an authentication call. +type AuthJSON struct { + Token string `json:"token"` + Type string `json:"token_type"` + Expires int `json:"expires_in"` +} + +// ZidJSON contains the identifier data of a zettel. +type ZidJSON struct { + ID string `json:"id"` +} + +// ZidMetaJSON contains the identifier and the metadata of a zettel. +type ZidMetaJSON struct { + ID string `json:"id"` + Meta map[string]string `json:"meta"` +} + +// ZidMetaRelatedList contains identifier/metadata of a zettel and the same for related zettel +type ZidMetaRelatedList struct { + ID string `json:"id"` + Meta map[string]string `json:"meta"` + List []ZidMetaJSON `json:"list"` +} + +// ZettelLinksJSON store all links / connections from one zettel to other. +type ZettelLinksJSON struct { + ID string `json:"id"` + Linked struct { + Incoming []string `json:"incoming,omitempty"` + Outgoing []string `json:"outgoing,omitempty"` + Local []string `json:"local,omitempty"` + External []string `json:"external,omitempty"` + Meta []string `json:"meta,omitempty"` + } `json:"linked"` + Embedded struct { + Outgoing []string `json:"outgoing,omitempty"` + Local []string `json:"local,omitempty"` + External []string `json:"external,omitempty"` + } `json:"embedded,omitempty"` + Cites []string `json:"cites,omitempty"` +} + +// ZettelDataJSON contains all data for a zettel. +type ZettelDataJSON struct { + Meta map[string]string `json:"meta"` + Encoding string `json:"encoding"` + Content string `json:"content"` +} + +// ZettelJSON contains all data for a zettel, the identifier, the metadata, and the content. +type ZettelJSON struct { + ID string `json:"id"` + Meta map[string]string `json:"meta"` + Encoding string `json:"encoding"` + Content string `json:"content"` +} + +// ZettelListJSON contains data for a zettel list. +type ZettelListJSON struct { + List []ZidMetaJSON `json:"list"` +} + +// TagListJSON specifies the list/map of tags +type TagListJSON struct { + Tags map[string][]string `json:"tags"` +} + +// RoleListJSON specifies the list of roles. +type RoleListJSON struct { + Roles []string `json:"role-list"` +} ADDED api/const.go Index: api/const.go ================================================================== --- api/const.go +++ api/const.go @@ -0,0 +1,104 @@ +//----------------------------------------------------------------------------- +// Copyright (c) 2021 Detlef Stern +// +// This file is part of zettelstore. +// +// Zettelstore is licensed under the latest version of the EUPL (European Union +// Public License). Please see file LICENSE.txt for your rights and obligations +// under this license. +//----------------------------------------------------------------------------- + +// Package api contains common definition used for client and server. +package api + +import "fmt" + +// Additional HTTP constants used. +const ( + MethodMove = "MOVE" // HTTP method for renaming a zettel + + HeaderAccept = "Accept" + HeaderContentType = "Content-Type" + HeaderDestination = "Destination" + HeaderLocation = "Location" +) + +// Values for HTTP query parameter. +const ( + QueryKeyDepth = "depth" + QueryKeyDir = "dir" + QueryKeyEncoding = "_enc" + QueryKeyLimit = "_limit" + QueryKeyNegate = "_negate" + QueryKeyOffset = "_offset" + QueryKeyOrder = "_order" + QueryKeyPart = "_part" + QueryKeySearch = "_s" + QueryKeySort = "_sort" +) + +// Supported dir values. +const ( + DirBackward = "backward" + DirForward = "forward" +) + +// Supported encoding values. +const ( + EncodingDJSON = "djson" + EncodingHTML = "html" + EncodingNative = "native" + EncodingText = "text" + EncodingZMK = "zmk" +) + +var mapEncodingEnum = map[string]EncodingEnum{ + EncodingDJSON: EncoderDJSON, + EncodingHTML: EncoderHTML, + EncodingNative: EncoderNative, + EncodingText: EncoderText, + EncodingZMK: EncoderZmk, +} +var mapEnumEncoding = map[EncodingEnum]string{} + +func init() { + for k, v := range mapEncodingEnum { + mapEnumEncoding[v] = k + } +} + +// Encoder returns the internal encoder code for the given encoding string. +func Encoder(encoding string) EncodingEnum { + if e, ok := mapEncodingEnum[encoding]; ok { + return e + } + return EncoderUnknown +} + +// EncodingEnum lists all valid encoder keys. +type EncodingEnum uint8 + +// Values for EncoderEnum +const ( + EncoderUnknown EncodingEnum = iota + EncoderDJSON + EncoderHTML + EncoderNative + EncoderText + EncoderZmk +) + +// String representation of an encoder key. +func (e EncodingEnum) String() string { + if f, ok := mapEnumEncoding[e]; ok { + return f + } + return fmt.Sprintf("*Unknown*(%d)", e) +} + +// Supported part values. +const ( + PartMeta = "meta" + PartContent = "content" + PartZettel = "zettel" +) ADDED api/urlbuilder.go Index: api/urlbuilder.go ================================================================== --- api/urlbuilder.go +++ api/urlbuilder.go @@ -0,0 +1,114 @@ +//----------------------------------------------------------------------------- +// Copyright (c) 2020-2021 Detlef Stern +// +// This file is part of zettelstore. +// +// Zettelstore is licensed under the latest version of the EUPL (European Union +// Public License). Please see file LICENSE.txt for your rights and obligations +// under this license. +//----------------------------------------------------------------------------- + +// Package api contains common definition used for client and server. +package api + +import ( + "net/url" + "strings" + + "zettelstore.de/z/domain/id" +) + +type urlQuery struct{ key, val string } + +// URLBuilder should be used to create zettelstore URLs. +type URLBuilder struct { + prefix string + key byte + path []string + query []urlQuery + fragment string +} + +// NewURLBuilder creates a new URL builder with the given prefix and key. +func NewURLBuilder(prefix string, key byte) *URLBuilder { + return &URLBuilder{prefix: prefix, key: key} +} + +// 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 + return cpy +} + +// SetZid sets the zettel identifier. +func (ub *URLBuilder) SetZid(zid id.Zid) *URLBuilder { + if len(ub.path) > 0 { + panic("Cannot add Zid") + } + ub.path = append(ub.path, zid.String()) + return ub +} + +// AppendPath adds a new path element +func (ub *URLBuilder) AppendPath(p string) *URLBuilder { + ub.path = append(ub.path, p) + return ub +} + +// AppendQuery adds a new query parameter +func (ub *URLBuilder) AppendQuery(key, value string) *URLBuilder { + ub.query = append(ub.query, urlQuery{key, value}) + return ub +} + +// ClearQuery removes all query parameters. +func (ub *URLBuilder) ClearQuery() *URLBuilder { + ub.query = nil + ub.fragment = "" + return ub +} + +// SetFragment stores the fragment +func (ub *URLBuilder) SetFragment(s string) *URLBuilder { + ub.fragment = 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) + } + for _, p := range ub.path { + 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) + sb.WriteByte('=') + sb.WriteString(url.QueryEscape(q.val)) + } + return sb.String() +} Index: ast/inline.go ================================================================== --- ast/inline.go +++ ast/inline.go @@ -199,23 +199,26 @@ // FormatKind specifies the format that is applied to the inline nodes. type FormatKind uint8 // Constants for FormatCode const ( - _ FormatKind = iota - FormatEmph // Emphasized text. - FormatStrong // Strongly emphasized text. - FormatInsert // Inserted text. - FormatDelete // Deleted text. - FormatSuper // Superscripted text. - FormatSub // SubscriptedText. - FormatQuote // Quoted text. - FormatQuotation // Quotation text. - FormatSmall // Smaller text. - FormatSpan // Generic inline container. - FormatMonospace // Monospaced text. - FormatEmphDeprecated // Deprecated kind of emphasized text. + _ FormatKind = iota + FormatItalic // Italic text. + FormatEmph // Semantically emphasized text. + FormatBold // Bold text. + FormatStrong // Semantically strongly emphasized text. + FormatUnder // Underlined text. + FormatInsert // Inserted text. + FormatStrike // Text that is no longer relevant or no longer accurate. + FormatDelete // Deleted text. + FormatSuper // Superscripted text. + FormatSub // SubscriptedText. + FormatQuote // Quoted text. + FormatQuotation // Quotation text. + FormatSmall // Smaller text. + FormatSpan // Generic inline container. + FormatMonospace // Monospaced text. ) func (*FormatNode) inlineNode() { /* Just a marker */ } // WalkChildren walks to the formatted text. Index: ast/ref.go ================================================================== --- ast/ref.go +++ ast/ref.go @@ -35,11 +35,11 @@ u, err := url.Parse(s) if err != nil { return &Reference{URL: nil, Value: s, State: RefStateInvalid} } if len(u.Scheme)+len(u.Opaque)+len(u.Host) == 0 && u.User == nil { - if _, err = id.Parse(u.Path); err == nil { + if _, err := id.Parse(u.Path); err == nil { return &Reference{URL: u, Value: s, State: RefStateZettel} } if u.Path == "" && u.Fragment != "" { return &Reference{URL: u, Value: s, State: RefStateSelf} } Index: auth/impl/impl.go ================================================================== --- auth/impl/impl.go +++ auth/impl/impl.go @@ -17,11 +17,10 @@ "io" "time" "github.com/pascaldekloe/jwt" - "zettelstore.de/c/api" "zettelstore.de/z/auth" "zettelstore.de/z/auth/policy" "zettelstore.de/z/box" "zettelstore.de/z/config" "zettelstore.de/z/domain/id" @@ -82,14 +81,14 @@ // ErrNoZid signals that the 'zid' key is missing. var ErrNoZid = errors.New("auth: missing zettel id") // GetToken returns a token to be used for authentification. func (a *myAuth) GetToken(ident *meta.Meta, d time.Duration, kind auth.TokenKind) ([]byte, error) { - if role, ok := ident.Get(api.KeyRole); !ok || role != api.ValueRoleUser { + if role, ok := ident.Get(meta.KeyRole); !ok || role != meta.ValueRoleUser { return nil, ErrNoUser } - subject, ok := ident.Get(api.KeyUserID) + subject, ok := ident.Get(meta.KeyUserID) if !ok || subject == "" { return nil, ErrNoIdent } now := time.Now().Round(time.Second) @@ -132,12 +131,12 @@ ident := claims.Subject if ident == "" { return auth.TokenData{}, ErrNoIdent } if zidS, ok := claims.Set["zid"].(string); ok { - if zid, err2 := id.Parse(zidS); err2 == nil { - if kind, ok2 := claims.Set["_tk"].(float64); ok2 { + if zid, err := id.Parse(zidS); err == nil { + if kind, ok := claims.Set["_tk"].(float64); ok { if auth.TokenKind(kind) == k { return auth.TokenData{ Token: token, Now: now, Issued: claims.Issued.Time(), @@ -170,11 +169,11 @@ return meta.UserRoleOwner } if a.IsOwner(user.Zid) { return meta.UserRoleOwner } - if val, ok := user.Get(api.KeyUserRole); ok { + if val, ok := user.Get(meta.KeyUserRole); ok { if ur := meta.GetUserRole(val); ur != meta.UserRoleUnknown { return ur } } return meta.UserRoleReader Index: auth/policy/default.go ================================================================== --- auth/policy/default.go +++ auth/policy/default.go @@ -10,47 +10,46 @@ // Package policy provides some interfaces and implementation for authorizsation policies. package policy import ( - "zettelstore.de/c/api" "zettelstore.de/z/auth" "zettelstore.de/z/domain/meta" ) type defaultPolicy struct { manager auth.AuthzManager } -func (*defaultPolicy) CanCreate(_, _ *meta.Meta) bool { return true } -func (*defaultPolicy) CanRead(_, _ *meta.Meta) bool { return true } -func (d *defaultPolicy) CanWrite(user, oldMeta, _ *meta.Meta) bool { +func (d *defaultPolicy) CanCreate(user, newMeta *meta.Meta) bool { return true } +func (d *defaultPolicy) CanRead(user, m *meta.Meta) bool { return true } +func (d *defaultPolicy) CanWrite(user, oldMeta, newMeta *meta.Meta) bool { return d.canChange(user, oldMeta) } func (d *defaultPolicy) CanRename(user, m *meta.Meta) bool { return d.canChange(user, m) } func (d *defaultPolicy) CanDelete(user, m *meta.Meta) bool { return d.canChange(user, m) } func (d *defaultPolicy) canChange(user, m *meta.Meta) bool { - metaRo, ok := m.Get(api.KeyReadOnly) + metaRo, ok := m.Get(meta.KeyReadOnly) if !ok { return true } if user == nil { // If we are here, there is no authentication. // See owner.go:CanWrite. // No authentication: check for owner-like restriction, because the user // acts as an owner - return metaRo != api.ValueUserRoleOwner && !meta.BoolValue(metaRo) + return metaRo != meta.ValueUserRoleOwner && !meta.BoolValue(metaRo) } userRole := d.manager.GetUserRole(user) switch metaRo { - case api.ValueUserRoleReader: + case meta.ValueUserRoleReader: return userRole > meta.UserRoleReader - case api.ValueUserRoleWriter: + case meta.ValueUserRoleWriter: return userRole > meta.UserRoleWriter - case api.ValueUserRoleOwner: + case meta.ValueUserRoleOwner: return userRole > meta.UserRoleOwner } return !meta.BoolValue(metaRo) } Index: auth/policy/owner.go ================================================================== --- auth/policy/owner.go +++ auth/policy/owner.go @@ -10,11 +10,10 @@ // Package policy provides some interfaces and implementation for authorizsation policies. package policy import ( - "zettelstore.de/c/api" "zettelstore.de/z/auth" "zettelstore.de/z/config" "zettelstore.de/z/domain/meta" ) @@ -33,11 +32,11 @@ func (o *ownerPolicy) userCanCreate(user, newMeta *meta.Meta) bool { if o.manager.GetUserRole(user) == meta.UserRoleReader { return false } - if role, ok := newMeta.Get(api.KeyRole); ok && role == api.ValueRoleUser { + if role, ok := newMeta.Get(meta.KeyRole); ok && role == meta.ValueRoleUser { return false } return true } @@ -59,11 +58,11 @@ return true } if user == nil { return false } - if role, ok := m.Get(api.KeyRole); ok && role == api.ValueRoleUser { + if role, ok := m.Get(meta.KeyRole); ok && role == meta.ValueRoleUser { // Only the user can read its own zettel return user.Zid == m.Zid } switch o.manager.GetUserRole(user) { case meta.UserRoleReader, meta.UserRoleWriter, meta.UserRoleOwner: @@ -74,14 +73,14 @@ return false } } var noChangeUser = []string{ - api.KeyID, - api.KeyRole, - api.KeyUserID, - api.KeyUserRole, + meta.KeyID, + meta.KeyRole, + meta.KeyUserID, + meta.KeyUserRole, } func (o *ownerPolicy) CanWrite(user, oldMeta, newMeta *meta.Meta) bool { if user == nil || !o.pre.CanWrite(user, oldMeta, newMeta) { return false @@ -94,11 +93,11 @@ return true } if !o.userCanRead(user, oldMeta, vis) { return false } - if role, ok := oldMeta.Get(api.KeyRole); ok && role == api.ValueRoleUser { + if role, ok := oldMeta.Get(meta.KeyRole); ok && role == meta.ValueRoleUser { // Here we know, that user.Zid == newMeta.Zid (because of userCanRead) and // user.Zid == newMeta.Zid (because oldMeta.Zid == newMeta.Zid) for _, key := range noChangeUser { if oldMeta.GetDefault(key, "") != newMeta.GetDefault(key, "") { return false @@ -145,10 +144,10 @@ return false } if o.manager.IsOwner(user.Zid) { return true } - if val, ok := user.Get(api.KeyUserRole); ok && val == api.ValueUserRoleOwner { + if val, ok := user.Get(meta.KeyUserRole); ok && val == meta.ValueUserRoleOwner { return true } return false } Index: auth/policy/policy_test.go ================================================================== --- auth/policy/policy_test.go +++ auth/policy/policy_test.go @@ -13,11 +13,10 @@ import ( "fmt" "testing" - "zettelstore.de/c/api" "zettelstore.de/z/auth" "zettelstore.de/z/domain/id" "zettelstore.de/z/domain/meta" ) @@ -58,13 +57,13 @@ type testAuthzManager struct { readOnly bool withAuth bool } -func (a *testAuthzManager) IsReadonly() bool { return a.readOnly } -func (*testAuthzManager) Owner() id.Zid { return ownerZid } -func (*testAuthzManager) IsOwner(zid id.Zid) bool { return zid == ownerZid } +func (a *testAuthzManager) IsReadonly() bool { return a.readOnly } +func (a *testAuthzManager) Owner() id.Zid { return ownerZid } +func (a *testAuthzManager) IsOwner(zid id.Zid) bool { return zid == ownerZid } func (a *testAuthzManager) WithAuth() bool { return a.withAuth } func (a *testAuthzManager) GetUserRole(user *meta.Meta) meta.UserRole { if user == nil { @@ -74,11 +73,11 @@ return meta.UserRoleOwner } if a.IsOwner(user.Zid) { return meta.UserRoleOwner } - if val, ok := user.Get(api.KeyUserRole); ok { + if val, ok := user.Get(meta.KeyUserRole); ok { if ur := meta.GetUserRole(val); ur != meta.UserRoleUnknown { return ur } } return meta.UserRoleReader @@ -86,12 +85,12 @@ type authConfig struct{ expert bool } func (ac *authConfig) GetExpertMode() bool { return ac.expert } -func (*authConfig) GetVisibility(m *meta.Meta) meta.Visibility { - if vis, ok := m.Get(api.KeyVisibility); ok { +func (ac *authConfig) GetVisibility(m *meta.Meta) meta.Visibility { + if vis, ok := m.Get(meta.KeyVisibility); ok { return meta.GetVisibility(vis) } return meta.VisibilityLogin } @@ -250,11 +249,11 @@ loginZettel := newLoginZettel() ownerZettel := newOwnerZettel() expertZettel := newExpertZettel() userZettel := newUserZettel() writerNew := writer.Clone() - writerNew.Set(api.KeyUserRole, owner.GetDefault(api.KeyUserRole, "")) + writerNew.Set(meta.KeyUserRole, owner.GetDefault(meta.KeyUserRole, "")) roFalse := newRoFalseZettel() roTrue := newRoTrueZettel() roReader := newRoReaderZettel() roWriter := newRoWriterZettel() roOwner := newRoOwnerZettel() @@ -577,109 +576,109 @@ ) func newAnon() *meta.Meta { return nil } func newCreator() *meta.Meta { user := meta.New(creatorZid) - user.Set(api.KeyTitle, "Creator") - user.Set(api.KeyRole, api.ValueRoleUser) - user.Set(api.KeyUserRole, api.ValueUserRoleCreator) + user.Set(meta.KeyTitle, "Creator") + user.Set(meta.KeyRole, meta.ValueRoleUser) + user.Set(meta.KeyUserRole, meta.ValueUserRoleCreator) return user } func newReader() *meta.Meta { user := meta.New(readerZid) - user.Set(api.KeyTitle, "Reader") - user.Set(api.KeyRole, api.ValueRoleUser) - user.Set(api.KeyUserRole, api.ValueUserRoleReader) + user.Set(meta.KeyTitle, "Reader") + user.Set(meta.KeyRole, meta.ValueRoleUser) + user.Set(meta.KeyUserRole, meta.ValueUserRoleReader) return user } func newWriter() *meta.Meta { user := meta.New(writerZid) - user.Set(api.KeyTitle, "Writer") - user.Set(api.KeyRole, api.ValueRoleUser) - user.Set(api.KeyUserRole, api.ValueUserRoleWriter) + user.Set(meta.KeyTitle, "Writer") + user.Set(meta.KeyRole, meta.ValueRoleUser) + user.Set(meta.KeyUserRole, meta.ValueUserRoleWriter) return user } func newOwner() *meta.Meta { user := meta.New(ownerZid) - user.Set(api.KeyTitle, "Owner") - user.Set(api.KeyRole, api.ValueRoleUser) - user.Set(api.KeyUserRole, api.ValueUserRoleOwner) + user.Set(meta.KeyTitle, "Owner") + user.Set(meta.KeyRole, meta.ValueRoleUser) + user.Set(meta.KeyUserRole, meta.ValueUserRoleOwner) return user } func newOwner2() *meta.Meta { user := meta.New(owner2Zid) - user.Set(api.KeyTitle, "Owner 2") - user.Set(api.KeyRole, api.ValueRoleUser) - user.Set(api.KeyUserRole, api.ValueUserRoleOwner) + user.Set(meta.KeyTitle, "Owner 2") + user.Set(meta.KeyRole, meta.ValueRoleUser) + user.Set(meta.KeyUserRole, meta.ValueUserRoleOwner) return user } func newZettel() *meta.Meta { m := meta.New(zettelZid) - m.Set(api.KeyTitle, "Any Zettel") + m.Set(meta.KeyTitle, "Any Zettel") return m } func newPublicZettel() *meta.Meta { m := meta.New(visZid) - m.Set(api.KeyTitle, "Public Zettel") - m.Set(api.KeyVisibility, api.ValueVisibilityPublic) + m.Set(meta.KeyTitle, "Public Zettel") + m.Set(meta.KeyVisibility, meta.ValueVisibilityPublic) return m } func newCreatorZettel() *meta.Meta { m := meta.New(visZid) - m.Set(api.KeyTitle, "Creator Zettel") - m.Set(api.KeyVisibility, api.ValueVisibilityCreator) + m.Set(meta.KeyTitle, "Creator Zettel") + m.Set(meta.KeyVisibility, meta.ValueVisibilityCreator) return m } func newLoginZettel() *meta.Meta { m := meta.New(visZid) - m.Set(api.KeyTitle, "Login Zettel") - m.Set(api.KeyVisibility, api.ValueVisibilityLogin) + m.Set(meta.KeyTitle, "Login Zettel") + m.Set(meta.KeyVisibility, meta.ValueVisibilityLogin) return m } func newOwnerZettel() *meta.Meta { m := meta.New(visZid) - m.Set(api.KeyTitle, "Owner Zettel") - m.Set(api.KeyVisibility, api.ValueVisibilityOwner) + m.Set(meta.KeyTitle, "Owner Zettel") + m.Set(meta.KeyVisibility, meta.ValueVisibilityOwner) return m } func newExpertZettel() *meta.Meta { m := meta.New(visZid) - m.Set(api.KeyTitle, "Expert Zettel") - m.Set(api.KeyVisibility, api.ValueVisibilityExpert) + m.Set(meta.KeyTitle, "Expert Zettel") + m.Set(meta.KeyVisibility, meta.ValueVisibilityExpert) return m } func newRoFalseZettel() *meta.Meta { m := meta.New(zettelZid) - m.Set(api.KeyTitle, "No r/o Zettel") - m.Set(api.KeyReadOnly, api.ValueFalse) + m.Set(meta.KeyTitle, "No r/o Zettel") + m.Set(meta.KeyReadOnly, "false") return m } func newRoTrueZettel() *meta.Meta { m := meta.New(zettelZid) - m.Set(api.KeyTitle, "A r/o Zettel") - m.Set(api.KeyReadOnly, api.ValueTrue) + m.Set(meta.KeyTitle, "A r/o Zettel") + m.Set(meta.KeyReadOnly, "true") return m } func newRoReaderZettel() *meta.Meta { m := meta.New(zettelZid) - m.Set(api.KeyTitle, "Reader r/o Zettel") - m.Set(api.KeyReadOnly, api.ValueUserRoleReader) + m.Set(meta.KeyTitle, "Reader r/o Zettel") + m.Set(meta.KeyReadOnly, meta.ValueUserRoleReader) return m } func newRoWriterZettel() *meta.Meta { m := meta.New(zettelZid) - m.Set(api.KeyTitle, "Writer r/o Zettel") - m.Set(api.KeyReadOnly, api.ValueUserRoleWriter) + m.Set(meta.KeyTitle, "Writer r/o Zettel") + m.Set(meta.KeyReadOnly, meta.ValueUserRoleWriter) return m } func newRoOwnerZettel() *meta.Meta { m := meta.New(zettelZid) - m.Set(api.KeyTitle, "Owner r/o Zettel") - m.Set(api.KeyReadOnly, api.ValueUserRoleOwner) + m.Set(meta.KeyTitle, "Owner r/o Zettel") + m.Set(meta.KeyReadOnly, meta.ValueUserRoleOwner) return m } func newUserZettel() *meta.Meta { m := meta.New(userZid) - m.Set(api.KeyTitle, "Any User") - m.Set(api.KeyRole, api.ValueRoleUser) + m.Set(meta.KeyTitle, "Any User") + m.Set(meta.KeyRole, meta.ValueRoleUser) return m } Index: box/box.go ================================================================== --- box/box.go +++ box/box.go @@ -16,11 +16,10 @@ "errors" "fmt" "io" "time" - "zettelstore.de/c/api" "zettelstore.de/z/domain" "zettelstore.de/z/domain/id" "zettelstore.de/z/domain/meta" "zettelstore.de/z/search" ) @@ -247,22 +246,22 @@ if err.Zid.IsValid() { return fmt.Sprintf( "operation %q on zettel %v not allowed for user %v/%v", err.Op, err.Zid.String(), - err.User.GetDefault(api.KeyUserID, "?"), + err.User.GetDefault(meta.KeyUserID, "?"), err.User.Zid.String()) } return fmt.Sprintf( "operation %q not allowed for user %v/%v", err.Op, - err.User.GetDefault(api.KeyUserID, "?"), + err.User.GetDefault(meta.KeyUserID, "?"), err.User.Zid.String()) } // Is return true, if the error is of type ErrNotAllowed. -func (*ErrNotAllowed) Is(error) bool { return true } +func (err *ErrNotAllowed) Is(target error) bool { return true } // ErrStarted is returned when trying to start an already started box. var ErrStarted = errors.New("box is already started") // ErrStopped is returned if calling methods on a box that was not started. Index: box/compbox/compbox.go ================================================================== --- box/compbox/compbox.go +++ box/compbox/compbox.go @@ -13,11 +13,10 @@ import ( "context" "net/url" - "zettelstore.de/c/api" "zettelstore.de/z/box" "zettelstore.de/z/box/manager" "zettelstore.de/z/domain" "zettelstore.de/z/domain/id" "zettelstore.de/z/domain/meta" @@ -37,18 +36,18 @@ } var myConfig *meta.Meta var myZettel = map[id.Zid]struct { meta func(id.Zid) *meta.Meta - content func(*meta.Meta) []byte + content func(*meta.Meta) string }{ - id.MustParse(api.ZidVersion): {genVersionBuildM, genVersionBuildC}, - id.MustParse(api.ZidHost): {genVersionHostM, genVersionHostC}, - id.MustParse(api.ZidOperatingSystem): {genVersionOSM, genVersionOSC}, - id.MustParse(api.ZidBoxManager): {genManagerM, genManagerC}, - id.MustParse(api.ZidMetadataKey): {genKeysM, genKeysC}, - id.MustParse(api.ZidStartupConfiguration): {genConfigZettelM, genConfigZettelC}, + id.VersionZid: {genVersionBuildM, genVersionBuildC}, + id.HostZid: {genVersionHostM, genVersionHostC}, + id.OperatingSystemZid: {genVersionOSM, genVersionOSC}, + id.BoxManagerZid: {genManagerM, genManagerC}, + id.MetadataKeyZid: {genKeysM, genKeysC}, + id.StartupConfigurationZid: {genConfigZettelM, genConfigZettelC}, } // Get returns the one program box. func getCompBox(boxNumber int, mf box.Enricher) box.ManagedBox { return &compBox{number: boxNumber, enricher: mf} @@ -146,14 +145,14 @@ st.ReadOnly = true st.Zettel = len(myZettel) } func updateMeta(m *meta.Meta) { - m.Set(api.KeyNoIndex, api.ValueTrue) - m.Set(api.KeySyntax, api.ValueSyntaxZmk) - m.Set(api.KeyRole, api.ValueRoleConfiguration) - m.Set(api.KeyLang, api.ValueLangEN) - m.Set(api.KeyReadOnly, api.ValueTrue) - if _, ok := m.Get(api.KeyVisibility); !ok { - m.Set(api.KeyVisibility, api.ValueVisibilityExpert) + m.Set(meta.KeyNoIndex, meta.ValueTrue) + m.Set(meta.KeySyntax, meta.ValueSyntaxZmk) + m.Set(meta.KeyRole, meta.ValueRoleConfiguration) + m.Set(meta.KeyLang, meta.ValueLangEN) + m.Set(meta.KeyReadOnly, meta.ValueTrue) + if _, ok := m.Get(meta.KeyVisibility); !ok { + m.Set(meta.KeyVisibility, meta.ValueVisibilityExpert) } } Index: box/compbox/config.go ================================================================== --- box/compbox/config.go +++ box/compbox/config.go @@ -10,44 +10,43 @@ // Package compbox provides zettel that have computed content. package compbox import ( - "bytes" + "strings" - "zettelstore.de/c/api" "zettelstore.de/z/domain/id" "zettelstore.de/z/domain/meta" ) func genConfigZettelM(zid id.Zid) *meta.Meta { if myConfig == nil { return nil } m := meta.New(zid) - m.Set(api.KeyTitle, "Zettelstore Startup Configuration") - m.Set(api.KeyVisibility, api.ValueVisibilityExpert) + m.Set(meta.KeyTitle, "Zettelstore Startup Configuration") + m.Set(meta.KeyVisibility, meta.ValueVisibilityExpert) return m } -func genConfigZettelC(*meta.Meta) []byte { - var buf bytes.Buffer +func genConfigZettelC(m *meta.Meta) string { + var sb strings.Builder for i, p := range myConfig.Pairs(false) { if i > 0 { - buf.WriteByte('\n') + sb.WriteByte('\n') } - buf.WriteString("; ''") - buf.WriteString(p.Key) - buf.WriteString("''") + sb.WriteString("; ''") + sb.WriteString(p.Key) + sb.WriteString("''") if p.Value != "" { - buf.WriteString("\n: ``") + sb.WriteString("\n: ``") for _, r := range p.Value { if r == '`' { - buf.WriteByte('\\') + sb.WriteByte('\\') } - buf.WriteRune(r) + sb.WriteRune(r) } - buf.WriteString("``") + sb.WriteString("``") } } - return buf.Bytes() + return sb.String() } Index: box/compbox/keys.go ================================================================== --- box/compbox/keys.go +++ box/compbox/keys.go @@ -10,30 +10,29 @@ // Package compbox provides zettel that have computed content. package compbox import ( - "bytes" "fmt" + "strings" - "zettelstore.de/c/api" "zettelstore.de/z/domain/id" "zettelstore.de/z/domain/meta" ) func genKeysM(zid id.Zid) *meta.Meta { m := meta.New(zid) - m.Set(api.KeyTitle, "Zettelstore Supported Metadata Keys") - m.Set(api.KeyVisibility, api.ValueVisibilityLogin) + m.Set(meta.KeyTitle, "Zettelstore Supported Metadata Keys") + m.Set(meta.KeyVisibility, meta.ValueVisibilityLogin) return m } -func genKeysC(*meta.Meta) []byte { +func genKeysC(*meta.Meta) string { keys := meta.GetSortedKeyDescriptions() - var buf bytes.Buffer - buf.WriteString("|=Name<|=Type<|=Computed?:|=Property?:\n") + var sb strings.Builder + sb.WriteString("|=Name<|=Type<|=Computed?:|=Property?:\n") for _, kd := range keys { - fmt.Fprintf(&buf, + fmt.Fprintf(&sb, "|%v|%v|%v|%v\n", kd.Name, kd.Type.Name, kd.IsComputed(), kd.IsProperty()) } - return buf.Bytes() + return sb.String() } Index: box/compbox/manager.go ================================================================== --- box/compbox/manager.go +++ box/compbox/manager.go @@ -10,32 +10,31 @@ // Package compbox provides zettel that have computed content. package compbox import ( - "bytes" "fmt" + "strings" - "zettelstore.de/c/api" "zettelstore.de/z/domain/id" "zettelstore.de/z/domain/meta" "zettelstore.de/z/kernel" ) func genManagerM(zid id.Zid) *meta.Meta { m := meta.New(zid) - m.Set(api.KeyTitle, "Zettelstore Box Manager") + m.Set(meta.KeyTitle, "Zettelstore Box Manager") return m } -func genManagerC(*meta.Meta) []byte { +func genManagerC(*meta.Meta) string { kvl := kernel.Main.GetServiceStatistics(kernel.BoxService) if len(kvl) == 0 { - return nil + return "No statistics available" } - var buf bytes.Buffer - buf.WriteString("|=Name|=Value>\n") + var sb strings.Builder + sb.WriteString("|=Name|=Value>\n") for _, kv := range kvl { - fmt.Fprintf(&buf, "| %v | %v\n", kv.Key, kv.Value) + fmt.Fprintf(&sb, "| %v | %v\n", kv.Key, kv.Value) } - return buf.Bytes() + return sb.String() } Index: box/compbox/version.go ================================================================== --- box/compbox/version.go +++ box/compbox/version.go @@ -10,45 +10,45 @@ // Package compbox provides zettel that have computed content. package compbox import ( - "zettelstore.de/c/api" + "fmt" + "zettelstore.de/z/domain/id" "zettelstore.de/z/domain/meta" "zettelstore.de/z/kernel" ) func getVersionMeta(zid id.Zid, title string) *meta.Meta { m := meta.New(zid) - m.Set(api.KeyTitle, title) - m.Set(api.KeyVisibility, api.ValueVisibilityExpert) + m.Set(meta.KeyTitle, title) + m.Set(meta.KeyVisibility, meta.ValueVisibilityExpert) return m } func genVersionBuildM(zid id.Zid) *meta.Meta { m := getVersionMeta(zid, "Zettelstore Version") - m.Set(api.KeyVisibility, api.ValueVisibilityPublic) + m.Set(meta.KeyVisibility, meta.ValueVisibilityPublic) return m } -func genVersionBuildC(*meta.Meta) []byte { - return []byte(kernel.Main.GetConfig(kernel.CoreService, kernel.CoreVersion).(string)) +func genVersionBuildC(*meta.Meta) string { + return kernel.Main.GetConfig(kernel.CoreService, kernel.CoreVersion).(string) } func genVersionHostM(zid id.Zid) *meta.Meta { return getVersionMeta(zid, "Zettelstore Host") } -func genVersionHostC(*meta.Meta) []byte { - return []byte(kernel.Main.GetConfig(kernel.CoreService, kernel.CoreHostname).(string)) +func genVersionHostC(*meta.Meta) string { + return kernel.Main.GetConfig(kernel.CoreService, kernel.CoreHostname).(string) } func genVersionOSM(zid id.Zid) *meta.Meta { return getVersionMeta(zid, "Zettelstore Operating System") } -func genVersionOSC(*meta.Meta) []byte { - goOS := kernel.Main.GetConfig(kernel.CoreService, kernel.CoreGoOS).(string) - goArch := kernel.Main.GetConfig(kernel.CoreService, kernel.CoreGoArch).(string) - result := make([]byte, 0, len(goOS)+len(goArch)+1) - result = append(result, goOS...) - result = append(result, '/') - return append(result, goArch...) +func genVersionOSC(*meta.Meta) string { + return fmt.Sprintf( + "%v/%v", + kernel.Main.GetConfig(kernel.CoreService, kernel.CoreGoOS).(string), + kernel.Main.GetConfig(kernel.CoreService, kernel.CoreGoArch).(string), + ) } Index: box/constbox/base.css ================================================================== --- box/constbox/base.css +++ box/constbox/base.css @@ -8,11 +8,13 @@ height: 100%; } body { margin: 0; min-height: 100vh; + text-rendering: optimizeSpeed; line-height: 1.4; + overflow-x: hidden; background-color: #f8f8f8 ; height: 100%; } nav.zs-menu { background-color: hsl(210, 28%, 90%); @@ -229,34 +231,15 @@ border-radius: .25rem; padding: .1rem .2rem; font-size: 95%; } .zs-example { border-style: dotted !important } - .zs-info { - background-color: lightblue; - padding: .5rem 1rem; - } - .zs-warning { - background-color: lightyellow; - padding: .5rem 1rem; - } .zs-error { background-color: lightpink; border-style: none !important; font-weight: bold; } - .zs-ta-left { text-align:left } - .zs-ta-center { text-align:center } - .zs-ta-right { text-align:right } - .zs-monospace { font-family:monospace } - .zs-font-size-0 { font-size:75% } - .zs-font-size-1 { font-size:83% } - .zs-font-size-2 { font-size:100% } - .zs-font-size-3 { font-size:117% } - .zs-font-size-4 { font-size:150% } - .zs-font-size-5 { font-size:200% } - .zs-deprecated { border-style: dashed; padding: .2rem } kbd { background: hsl(210, 5%, 100%); border: 1px solid hsl(210, 5%, 70%); border-radius: .25rem; padding: .1rem .2rem; Index: box/constbox/base.mustache ================================================================== --- box/constbox/base.mustache +++ box/constbox/base.mustache @@ -1,6 +1,10 @@ + + + + {{{MetaHeader}}} @@ -56,5 +60,7 @@ {{#FooterHTML}} {{/FooterHTML}} + + Index: box/constbox/constbox.go ================================================================== --- box/constbox/constbox.go +++ box/constbox/constbox.go @@ -14,11 +14,10 @@ import ( "context" _ "embed" // Allow to embed file content "net/url" - "zettelstore.de/c/api" "zettelstore.de/z/box" "zettelstore.de/z/box/manager" "zettelstore.de/z/domain" "zettelstore.de/z/domain/id" "zettelstore.de/z/domain/meta" @@ -119,270 +118,270 @@ const syntaxTemplate = "mustache" var constZettelMap = map[id.Zid]constZettel{ id.ConfigurationZid: { constHeader{ - api.KeyTitle: "Zettelstore Runtime Configuration", - api.KeyRole: api.ValueRoleConfiguration, - api.KeySyntax: api.ValueSyntaxNone, - api.KeyNoIndex: api.ValueTrue, - api.KeyVisibility: api.ValueVisibilityOwner, + meta.KeyTitle: "Zettelstore Runtime Configuration", + meta.KeyRole: meta.ValueRoleConfiguration, + meta.KeySyntax: meta.ValueSyntaxNone, + meta.KeyNoIndex: meta.ValueTrue, + meta.KeyVisibility: meta.ValueVisibilityOwner, }, - domain.NewContent(nil)}, - id.MustParse(api.ZidLicense): { + domain.NewContent("")}, + id.LicenseZid: { constHeader{ - api.KeyTitle: "Zettelstore License", - api.KeyRole: api.ValueRoleConfiguration, - api.KeySyntax: api.ValueSyntaxText, - api.KeyLang: api.ValueLangEN, - api.KeyReadOnly: api.ValueTrue, - api.KeyVisibility: api.ValueVisibilityPublic, + meta.KeyTitle: "Zettelstore License", + meta.KeyRole: meta.ValueRoleConfiguration, + meta.KeySyntax: meta.ValueSyntaxText, + meta.KeyLang: meta.ValueLangEN, + meta.KeyReadOnly: meta.ValueTrue, + meta.KeyVisibility: meta.ValueVisibilityPublic, }, domain.NewContent(contentLicense)}, - id.MustParse(api.ZidAuthors): { + id.AuthorsZid: { constHeader{ - api.KeyTitle: "Zettelstore Contributors", - api.KeyRole: api.ValueRoleConfiguration, - api.KeySyntax: api.ValueSyntaxZmk, - api.KeyLang: api.ValueLangEN, - api.KeyReadOnly: api.ValueTrue, - api.KeyVisibility: api.ValueVisibilityPublic, + meta.KeyTitle: "Zettelstore Contributors", + meta.KeyRole: meta.ValueRoleConfiguration, + meta.KeySyntax: meta.ValueSyntaxZmk, + meta.KeyLang: meta.ValueLangEN, + meta.KeyReadOnly: meta.ValueTrue, + meta.KeyVisibility: meta.ValueVisibilityPublic, }, domain.NewContent(contentContributors)}, - id.MustParse(api.ZidDependencies): { + id.DependenciesZid: { constHeader{ - api.KeyTitle: "Zettelstore Dependencies", - api.KeyRole: api.ValueRoleConfiguration, - api.KeySyntax: api.ValueSyntaxZmk, - api.KeyLang: api.ValueLangEN, - api.KeyReadOnly: api.ValueTrue, - api.KeyVisibility: api.ValueVisibilityPublic, + meta.KeyTitle: "Zettelstore Dependencies", + meta.KeyRole: meta.ValueRoleConfiguration, + meta.KeySyntax: meta.ValueSyntaxZmk, + meta.KeyLang: meta.ValueLangEN, + meta.KeyReadOnly: meta.ValueTrue, + meta.KeyVisibility: meta.ValueVisibilityPublic, }, domain.NewContent(contentDependencies)}, id.BaseTemplateZid: { constHeader{ - api.KeyTitle: "Zettelstore Base HTML Template", - api.KeyRole: api.ValueRoleConfiguration, - api.KeySyntax: syntaxTemplate, - api.KeyNoIndex: api.ValueTrue, - api.KeyVisibility: api.ValueVisibilityExpert, + meta.KeyTitle: "Zettelstore Base HTML Template", + meta.KeyRole: meta.ValueRoleConfiguration, + meta.KeySyntax: syntaxTemplate, + meta.KeyNoIndex: meta.ValueTrue, + meta.KeyVisibility: meta.ValueVisibilityExpert, }, domain.NewContent(contentBaseMustache)}, id.LoginTemplateZid: { constHeader{ - api.KeyTitle: "Zettelstore Login Form HTML Template", - api.KeyRole: api.ValueRoleConfiguration, - api.KeySyntax: syntaxTemplate, - api.KeyNoIndex: api.ValueTrue, - api.KeyVisibility: api.ValueVisibilityExpert, + meta.KeyTitle: "Zettelstore Login Form HTML Template", + meta.KeyRole: meta.ValueRoleConfiguration, + meta.KeySyntax: syntaxTemplate, + meta.KeyNoIndex: meta.ValueTrue, + meta.KeyVisibility: meta.ValueVisibilityExpert, }, domain.NewContent(contentLoginMustache)}, id.ZettelTemplateZid: { constHeader{ - api.KeyTitle: "Zettelstore Zettel HTML Template", - api.KeyRole: api.ValueRoleConfiguration, - api.KeySyntax: syntaxTemplate, - api.KeyNoIndex: api.ValueTrue, - api.KeyVisibility: api.ValueVisibilityExpert, + meta.KeyTitle: "Zettelstore Zettel HTML Template", + meta.KeyRole: meta.ValueRoleConfiguration, + meta.KeySyntax: syntaxTemplate, + meta.KeyNoIndex: meta.ValueTrue, + meta.KeyVisibility: meta.ValueVisibilityExpert, }, domain.NewContent(contentZettelMustache)}, id.InfoTemplateZid: { constHeader{ - api.KeyTitle: "Zettelstore Info HTML Template", - api.KeyRole: api.ValueRoleConfiguration, - api.KeySyntax: syntaxTemplate, - api.KeyNoIndex: api.ValueTrue, - api.KeyVisibility: api.ValueVisibilityExpert, + meta.KeyTitle: "Zettelstore Info HTML Template", + meta.KeyRole: meta.ValueRoleConfiguration, + meta.KeySyntax: syntaxTemplate, + meta.KeyNoIndex: meta.ValueTrue, + meta.KeyVisibility: meta.ValueVisibilityExpert, }, domain.NewContent(contentInfoMustache)}, id.ContextTemplateZid: { constHeader{ - api.KeyTitle: "Zettelstore Context HTML Template", - api.KeyRole: api.ValueRoleConfiguration, - api.KeySyntax: syntaxTemplate, - api.KeyNoIndex: api.ValueTrue, - api.KeyVisibility: api.ValueVisibilityExpert, + meta.KeyTitle: "Zettelstore Context HTML Template", + meta.KeyRole: meta.ValueRoleConfiguration, + meta.KeySyntax: syntaxTemplate, + meta.KeyNoIndex: meta.ValueTrue, + meta.KeyVisibility: meta.ValueVisibilityExpert, }, domain.NewContent(contentContextMustache)}, id.FormTemplateZid: { constHeader{ - api.KeyTitle: "Zettelstore Form HTML Template", - api.KeyRole: api.ValueRoleConfiguration, - api.KeySyntax: syntaxTemplate, - api.KeyNoIndex: api.ValueTrue, - api.KeyVisibility: api.ValueVisibilityExpert, + meta.KeyTitle: "Zettelstore Form HTML Template", + meta.KeyRole: meta.ValueRoleConfiguration, + meta.KeySyntax: syntaxTemplate, + meta.KeyNoIndex: meta.ValueTrue, + meta.KeyVisibility: meta.ValueVisibilityExpert, }, domain.NewContent(contentFormMustache)}, id.RenameTemplateZid: { constHeader{ - api.KeyTitle: "Zettelstore Rename Form HTML Template", - api.KeyRole: api.ValueRoleConfiguration, - api.KeySyntax: syntaxTemplate, - api.KeyNoIndex: api.ValueTrue, - api.KeyVisibility: api.ValueVisibilityExpert, + meta.KeyTitle: "Zettelstore Rename Form HTML Template", + meta.KeyRole: meta.ValueRoleConfiguration, + meta.KeySyntax: syntaxTemplate, + meta.KeyNoIndex: meta.ValueTrue, + meta.KeyVisibility: meta.ValueVisibilityExpert, }, domain.NewContent(contentRenameMustache)}, id.DeleteTemplateZid: { constHeader{ - api.KeyTitle: "Zettelstore Delete HTML Template", - api.KeyRole: api.ValueRoleConfiguration, - api.KeySyntax: syntaxTemplate, - api.KeyNoIndex: api.ValueTrue, - api.KeyVisibility: api.ValueVisibilityExpert, + meta.KeyTitle: "Zettelstore Delete HTML Template", + meta.KeyRole: meta.ValueRoleConfiguration, + meta.KeySyntax: syntaxTemplate, + meta.KeyNoIndex: meta.ValueTrue, + meta.KeyVisibility: meta.ValueVisibilityExpert, }, domain.NewContent(contentDeleteMustache)}, id.ListTemplateZid: { constHeader{ - api.KeyTitle: "Zettelstore List Zettel HTML Template", - api.KeyRole: api.ValueRoleConfiguration, - api.KeySyntax: syntaxTemplate, - api.KeyNoIndex: api.ValueTrue, - api.KeyVisibility: api.ValueVisibilityExpert, + meta.KeyTitle: "Zettelstore List Zettel HTML Template", + meta.KeyRole: meta.ValueRoleConfiguration, + meta.KeySyntax: syntaxTemplate, + meta.KeyNoIndex: meta.ValueTrue, + meta.KeyVisibility: meta.ValueVisibilityExpert, }, domain.NewContent(contentListZettelMustache)}, id.RolesTemplateZid: { constHeader{ - api.KeyTitle: "Zettelstore List Roles HTML Template", - api.KeyRole: api.ValueRoleConfiguration, - api.KeySyntax: syntaxTemplate, - api.KeyNoIndex: api.ValueTrue, - api.KeyVisibility: api.ValueVisibilityExpert, + meta.KeyTitle: "Zettelstore List Roles HTML Template", + meta.KeyRole: meta.ValueRoleConfiguration, + meta.KeySyntax: syntaxTemplate, + meta.KeyNoIndex: meta.ValueTrue, + meta.KeyVisibility: meta.ValueVisibilityExpert, }, domain.NewContent(contentListRolesMustache)}, id.TagsTemplateZid: { constHeader{ - api.KeyTitle: "Zettelstore List Tags HTML Template", - api.KeyRole: api.ValueRoleConfiguration, - api.KeySyntax: syntaxTemplate, - api.KeyNoIndex: api.ValueTrue, - api.KeyVisibility: api.ValueVisibilityExpert, + meta.KeyTitle: "Zettelstore List Tags HTML Template", + meta.KeyRole: meta.ValueRoleConfiguration, + meta.KeySyntax: syntaxTemplate, + meta.KeyNoIndex: meta.ValueTrue, + meta.KeyVisibility: meta.ValueVisibilityExpert, }, domain.NewContent(contentListTagsMustache)}, id.ErrorTemplateZid: { constHeader{ - api.KeyTitle: "Zettelstore Error HTML Template", - api.KeyRole: api.ValueRoleConfiguration, - api.KeySyntax: syntaxTemplate, - api.KeyNoIndex: api.ValueTrue, - api.KeyVisibility: api.ValueVisibilityExpert, + meta.KeyTitle: "Zettelstore Error HTML Template", + meta.KeyRole: meta.ValueRoleConfiguration, + meta.KeySyntax: syntaxTemplate, + meta.KeyNoIndex: meta.ValueTrue, + meta.KeyVisibility: meta.ValueVisibilityExpert, }, domain.NewContent(contentErrorMustache)}, - id.MustParse(api.ZidBaseCSS): { + id.BaseCSSZid: { constHeader{ - api.KeyTitle: "Zettelstore Base CSS", - api.KeyRole: api.ValueRoleConfiguration, - api.KeySyntax: "css", - api.KeyNoIndex: api.ValueTrue, - api.KeyVisibility: api.ValueVisibilityPublic, + meta.KeyTitle: "Zettelstore Base CSS", + meta.KeyRole: meta.ValueRoleConfiguration, + meta.KeySyntax: "css", + meta.KeyNoIndex: meta.ValueTrue, + meta.KeyVisibility: meta.ValueVisibilityPublic, }, domain.NewContent(contentBaseCSS)}, - id.MustParse(api.ZidUserCSS): { + id.UserCSSZid: { constHeader{ - api.KeyTitle: "Zettelstore User CSS", - api.KeyRole: api.ValueRoleConfiguration, - api.KeySyntax: "css", - api.KeyVisibility: api.ValueVisibilityPublic, + meta.KeyTitle: "Zettelstore User CSS", + meta.KeyRole: meta.ValueRoleConfiguration, + meta.KeySyntax: "css", + meta.KeyVisibility: meta.ValueVisibilityPublic, }, - domain.NewContent([]byte("/* User-defined CSS */"))}, + domain.NewContent("/* User-defined CSS */")}, id.EmojiZid: { constHeader{ - api.KeyTitle: "Generic Emoji", - api.KeyRole: api.ValueRoleConfiguration, - api.KeySyntax: api.ValueSyntaxGif, - api.KeyReadOnly: api.ValueTrue, - api.KeyVisibility: api.ValueVisibilityPublic, + meta.KeyTitle: "Generic Emoji", + meta.KeyRole: meta.ValueRoleConfiguration, + meta.KeySyntax: meta.ValueSyntaxGif, + meta.KeyReadOnly: meta.ValueTrue, + meta.KeyVisibility: meta.ValueVisibilityPublic, }, domain.NewContent(contentEmoji)}, id.TOCNewTemplateZid: { constHeader{ - api.KeyTitle: "New Menu", - api.KeyRole: api.ValueRoleConfiguration, - api.KeySyntax: api.ValueSyntaxZmk, - api.KeyLang: api.ValueLangEN, - api.KeyVisibility: api.ValueVisibilityCreator, + meta.KeyTitle: "New Menu", + meta.KeyRole: meta.ValueRoleConfiguration, + meta.KeySyntax: meta.ValueSyntaxZmk, + meta.KeyLang: meta.ValueLangEN, + meta.KeyVisibility: meta.ValueVisibilityCreator, }, domain.NewContent(contentNewTOCZettel)}, - id.MustParse(api.ZidTemplateNewZettel): { - constHeader{ - api.KeyTitle: "New Zettel", - api.KeyRole: api.ValueRoleZettel, - api.KeySyntax: api.ValueSyntaxZmk, - api.KeyVisibility: api.ValueVisibilityCreator, - }, - domain.NewContent(nil)}, - id.MustParse(api.ZidTemplateNewUser): { - constHeader{ - api.KeyTitle: "New User", - api.KeyRole: api.ValueRoleUser, - api.KeySyntax: api.ValueSyntaxNone, - meta.NewPrefix + api.KeyCredential: "", - meta.NewPrefix + api.KeyUserID: "", - meta.NewPrefix + api.KeyUserRole: api.ValueUserRoleReader, - api.KeyVisibility: api.ValueVisibilityOwner, - }, - domain.NewContent(nil)}, + id.TemplateNewZettelZid: { + constHeader{ + meta.KeyTitle: "New Zettel", + meta.KeyRole: meta.ValueRoleZettel, + meta.KeySyntax: meta.ValueSyntaxZmk, + meta.KeyVisibility: meta.ValueVisibilityCreator, + }, + domain.NewContent("")}, + id.TemplateNewUserZid: { + constHeader{ + meta.KeyTitle: "New User", + meta.KeyRole: meta.ValueRoleUser, + meta.KeySyntax: meta.ValueSyntaxNone, + meta.NewPrefix + meta.KeyCredential: "", + meta.NewPrefix + meta.KeyUserID: "", + meta.NewPrefix + meta.KeyUserRole: meta.ValueUserRoleReader, + meta.KeyVisibility: meta.ValueVisibilityOwner, + }, + domain.NewContent("")}, id.DefaultHomeZid: { constHeader{ - api.KeyTitle: "Home", - api.KeyRole: api.ValueRoleZettel, - api.KeySyntax: api.ValueSyntaxZmk, - api.KeyLang: api.ValueLangEN, + meta.KeyTitle: "Home", + meta.KeyRole: meta.ValueRoleZettel, + meta.KeySyntax: meta.ValueSyntaxZmk, + meta.KeyLang: meta.ValueLangEN, }, domain.NewContent(contentHomeZettel)}, } //go:embed license.txt -var contentLicense []byte +var contentLicense string //go:embed contributors.zettel -var contentContributors []byte +var contentContributors string //go:embed dependencies.zettel -var contentDependencies []byte +var contentDependencies string //go:embed base.mustache -var contentBaseMustache []byte +var contentBaseMustache string //go:embed login.mustache -var contentLoginMustache []byte +var contentLoginMustache string //go:embed zettel.mustache -var contentZettelMustache []byte +var contentZettelMustache string //go:embed info.mustache -var contentInfoMustache []byte +var contentInfoMustache string //go:embed context.mustache -var contentContextMustache []byte +var contentContextMustache string //go:embed form.mustache -var contentFormMustache []byte +var contentFormMustache string //go:embed rename.mustache -var contentRenameMustache []byte +var contentRenameMustache string //go:embed delete.mustache -var contentDeleteMustache []byte +var contentDeleteMustache string //go:embed listzettel.mustache -var contentListZettelMustache []byte +var contentListZettelMustache string //go:embed listroles.mustache -var contentListRolesMustache []byte +var contentListRolesMustache string //go:embed listtags.mustache -var contentListTagsMustache []byte +var contentListTagsMustache string //go:embed error.mustache -var contentErrorMustache []byte +var contentErrorMustache string //go:embed base.css -var contentBaseCSS []byte +var contentBaseCSS string //go:embed emoji_spin.gif -var contentEmoji []byte +var contentEmoji string //go:embed newtoc.zettel -var contentNewTOCZettel []byte +var contentNewTOCZettel string //go:embed home.zettel -var contentHomeZettel []byte +var contentHomeZettel string Index: box/constbox/delete.mustache ================================================================== --- box/constbox/delete.mustache +++ box/constbox/delete.mustache @@ -1,27 +1,10 @@

Delete Zettel {{Zid}}

Do you really want to delete this zettel?

-{{#HasShadows}} -
-

Infomation

-

If you delete this zettel, the previoulsy shadowed zettel from overlayed box {{ShadowedBox}} becomes available.

-
-{{/HasShadows}} -{{#HasIncoming}} -
-

Warning!

-

If you delete this zettel, incoming references from the following zettel will become invalid.

-
    -{{#Incoming}} -
  • {{Text}}
  • -{{/Incoming}} -
-
-{{/HasIncoming}}
{{#MetaPairs}}
{{Key}}:
{{Value}}
{{/MetaPairs}}
Index: box/constbox/listtags.mustache ================================================================== --- box/constbox/listtags.mustache +++ box/constbox/listtags.mustache @@ -3,8 +3,8 @@

Currently used tags

All{{#MinCounts}}, {{Count}}{{/MinCounts}}
-{{#Tags}} {{Name}}{{Count}} +{{#Tags}} {{Name}}{{Count}} {{/Tags}} Index: box/constbox/rename.mustache ================================================================== --- box/constbox/rename.mustache +++ box/constbox/rename.mustache @@ -1,21 +1,10 @@

Rename Zettel {{.Zid}}

Do you really want to rename this zettel?

-{{#HasIncoming}} -
-

Warning!

-

If you rename this zettel, incoming references from the following zettel will become invalid.

-
    -{{#Incoming}} -
  • {{Text}}
  • -{{/Incoming}} -
-
-{{/HasIncoming}}
Index: box/dirbox/dirbox.go ================================================================== --- box/dirbox/dirbox.go +++ box/dirbox/dirbox.go @@ -20,11 +20,10 @@ "strconv" "strings" "sync" "time" - "zettelstore.de/c/api" "zettelstore.de/z/box" "zettelstore.de/z/box/dirbox/directory" "zettelstore.de/z/box/filebox" "zettelstore.de/z/box/manager" "zettelstore.de/z/domain" @@ -295,13 +294,13 @@ func (dp *dirBox) calcSpecExt(m *meta.Meta) (directory.MetaSpec, string) { if m.YamlSep { return directory.MetaSpecHeader, "zettel" } - syntax := m.GetDefault(api.KeySyntax, "bin") + syntax := m.GetDefault(meta.KeySyntax, "bin") switch syntax { - case api.ValueSyntaxNone, api.ValueSyntaxZmk: + case meta.ValueSyntaxNone, meta.ValueSyntaxZmk: return directory.MetaSpecHeader, "zettel" } for _, s := range dp.cdata.Config.GetZettelFileSyntax() { if s == syntax { return directory.MetaSpecHeader, "zettel" @@ -391,15 +390,15 @@ st.ReadOnly = dp.readonly st.Zettel, _ = dp.dirSrv.NumEntries() } func (dp *dirBox) cleanupMeta(m *meta.Meta) { - if role, ok := m.Get(api.KeyRole); !ok || role == "" { - m.Set(api.KeyRole, dp.cdata.Config.GetDefaultRole()) + if role, ok := m.Get(meta.KeyRole); !ok || role == "" { + m.Set(meta.KeyRole, dp.cdata.Config.GetDefaultRole()) } - if syntax, ok := m.Get(api.KeySyntax); !ok || syntax == "" { - m.Set(api.KeySyntax, dp.cdata.Config.GetDefaultSyntax()) + if syntax, ok := m.Get(meta.KeySyntax); !ok || syntax == "" { + m.Set(meta.KeySyntax, dp.cdata.Config.GetDefaultSyntax()) } } func renamePath(path string, curID, newID id.Zid) string { dir, file := filepath.Split(path) Index: box/dirbox/service.go ================================================================== --- box/dirbox/service.go +++ box/dirbox/service.go @@ -73,11 +73,11 @@ // COMMAND: getMetaContent ---------------------------------------- // // Retrieves the meta data and the content of a zettel. -func getMetaContent(dp *dirBox, entry *directory.Entry, zid id.Zid) (*meta.Meta, []byte, error) { +func getMetaContent(dp *dirBox, entry *directory.Entry, zid id.Zid) (*meta.Meta, string, error) { rc := make(chan resGetMetaContent) dp.getFileChan(zid) <- &fileGetMetaContent{entry, rc} res := <-rc close(rc) return res.meta, res.content, res.err @@ -87,17 +87,17 @@ entry *directory.Entry rc chan<- resGetMetaContent } type resGetMetaContent struct { meta *meta.Meta - content []byte + content string err error } func (cmd *fileGetMetaContent) run() { var m *meta.Meta - var content []byte + var content string var err error entry := cmd.entry switch entry.MetaSpec { case directory.MetaSpecFile: @@ -225,12 +225,16 @@ cmd.rc <- err } // Utility functions ---------------------------------------- -func readFileContent(path string) ([]byte, error) { - return os.ReadFile(path) +func readFileContent(path string) (string, error) { + data, err := os.ReadFile(path) + if err != nil { + return "", err + } + return string(data), nil } func parseMetaFile(zid id.Zid, path string) (*meta.Meta, error) { src, err := readFileContent(path) if err != nil { @@ -238,14 +242,14 @@ } inp := input.NewInput(src) return meta.NewFromInput(zid, inp), nil } -func parseMetaContentFile(zid id.Zid, path string) (*meta.Meta, []byte, error) { +func parseMetaContentFile(zid id.Zid, path string) (*meta.Meta, string, error) { src, err := readFileContent(path) if err != nil { - return nil, nil, err + return nil, "", err } inp := input.NewInput(src) meta := meta.NewFromInput(zid, inp) return meta, src[inp.Pos:], nil } Index: box/dirbox/simpledir/simpledir.go ================================================================== --- box/dirbox/simpledir/simpledir.go +++ box/dirbox/simpledir/simpledir.go @@ -40,39 +40,38 @@ defer ss.mx.Unlock() _, err := os.ReadDir(ss.dirPath) return err } -func (*simpleService) Stop() error { +func (ss *simpleService) Stop() error { return nil } func (ss *simpleService) NumEntries() (int, error) { ss.mx.Lock() defer ss.mx.Unlock() - entries, err := ss.doGetEntries() + entries, err := ss.getEntries() if err == nil { return len(entries), nil } return 0, err } func (ss *simpleService) GetEntries() ([]*directory.Entry, error) { ss.mx.Lock() defer ss.mx.Unlock() - entrySet, err := ss.doGetEntries() + entrySet, err := ss.getEntries() if err != nil { return nil, err } result := make([]*directory.Entry, 0, len(entrySet)) for _, entry := range entrySet { result = append(result, entry) } return result, nil } - -func (ss *simpleService) doGetEntries() (map[id.Zid]*directory.Entry, error) { +func (ss *simpleService) getEntries() (map[id.Zid]*directory.Entry, error) { dirEntries, err := os.ReadDir(ss.dirPath) if err != nil { return nil, err } entrySet := make(map[id.Zid]*directory.Entry) @@ -86,12 +85,12 @@ name := dirEntry.Name() match := matchValidFileName(name) if len(match) == 0 { continue } - zid, err2 := id.Parse(match[1]) - if err2 != nil { + zid, err := id.Parse(match[1]) + if err != nil { continue } var entry *directory.Entry if e, ok := entrySet[zid]; ok { entry = e @@ -130,14 +129,13 @@ } func (ss *simpleService) GetEntry(zid id.Zid) (*directory.Entry, error) { ss.mx.Lock() defer ss.mx.Unlock() - return ss.doGetEntry(zid) + return ss.getEntry(zid) } - -func (ss *simpleService) doGetEntry(zid id.Zid) (*directory.Entry, error) { +func (ss *simpleService) getEntry(zid id.Zid) (*directory.Entry, error) { pattern := filepath.Join(ss.dirPath, zid.String()) + "*.*" paths, err := filepath.Glob(pattern) if err != nil { return nil, err } @@ -157,11 +155,11 @@ func (ss *simpleService) GetNew() (*directory.Entry, error) { ss.mx.Lock() defer ss.mx.Unlock() zid, err := box.GetNewZid(func(zid id.Zid) (bool, error) { - entry, err := ss.doGetEntry(zid) + entry, err := ss.getEntry(zid) if err != nil { return false, nil } return !entry.IsValid(), nil }) @@ -169,19 +167,19 @@ return nil, err } return &directory.Entry{Zid: zid}, nil } -func (*simpleService) UpdateEntry(*directory.Entry) error { +func (ss *simpleService) UpdateEntry(entry *directory.Entry) error { // Nothing to to, since the actual file update is done by dirbox. return nil } -func (*simpleService) RenameEntry(_, _ *directory.Entry) error { +func (ss *simpleService) RenameEntry(curEntry, newEntry *directory.Entry) error { // Nothing to to, since the actual file rename is done by dirbox. return nil } -func (*simpleService) DeleteEntry(id.Zid) error { +func (ss *simpleService) DeleteEntry(zid id.Zid) error { // Nothing to to, since the actual file delete is done by dirbox. return nil } Index: box/filebox/filebox.go ================================================================== --- box/filebox/filebox.go +++ box/filebox/filebox.go @@ -15,11 +15,10 @@ "errors" "net/url" "path/filepath" "strings" - "zettelstore.de/c/api" "zettelstore.de/z/box" "zettelstore.de/z/box/manager" "zettelstore.de/z/domain/id" "zettelstore.de/z/domain/meta" ) @@ -65,31 +64,31 @@ } // CalcDefaultMeta returns metadata with default values for the given entry. func CalcDefaultMeta(zid id.Zid, ext string) *meta.Meta { m := meta.New(zid) - m.Set(api.KeyTitle, zid.String()) - m.Set(api.KeySyntax, calculateSyntax(ext)) + m.Set(meta.KeyTitle, zid.String()) + m.Set(meta.KeySyntax, calculateSyntax(ext)) return m } // CleanupMeta enhances the given metadata. func CleanupMeta(m *meta.Meta, zid id.Zid, ext string, inMeta, duplicates bool) { - if title, ok := m.Get(api.KeyTitle); !ok || title == "" { - m.Set(api.KeyTitle, zid.String()) + if title, ok := m.Get(meta.KeyTitle); !ok || title == "" { + m.Set(meta.KeyTitle, zid.String()) } if inMeta { - if syntax, ok := m.Get(api.KeySyntax); !ok || syntax == "" { + if syntax, ok := m.Get(meta.KeySyntax); !ok || syntax == "" { dm := CalcDefaultMeta(zid, ext) - syntax, ok = dm.Get(api.KeySyntax) + syntax, ok = dm.Get(meta.KeySyntax) if !ok { panic("Default meta must contain syntax") } - m.Set(api.KeySyntax, syntax) + m.Set(meta.KeySyntax, syntax) } } if duplicates { - m.Set(api.KeyDuplicates, api.ValueTrue) + m.Set(meta.KeyDuplicates, meta.ValueTrue) } } Index: box/filebox/zipbox.go ================================================================== --- box/filebox/zipbox.go +++ box/filebox/zipbox.go @@ -62,12 +62,12 @@ for _, f := range reader.File { match := matchValidFileName(f.Name) if len(match) < 1 { continue } - zid, err2 := id.Parse(match[1]) - if err2 != nil { + zid, err := id.Parse(match[1]) + if err != nil { continue } zp.addFile(zid, f.Name, match[3]) } return nil @@ -118,11 +118,11 @@ return domain.Zettel{}, err } defer reader.Close() var m *meta.Meta - var src []byte + var src string var inMeta bool if entry.metaInHeader { src, err = readZipFileContent(reader, entry.contentName) if err != nil { return domain.Zettel{}, err @@ -172,12 +172,12 @@ if err != nil { return err } defer reader.Close() for zid, entry := range zp.zettel { - m, err2 := readZipMeta(reader, zid, entry) - if err2 != nil { + m, err := readZipMeta(reader, zid, entry) + if err != nil { continue } zp.enricher.Enrich(ctx, m, zp.number) handle(m) } @@ -237,13 +237,17 @@ } inp := input.NewInput(src) return meta.NewFromInput(zid, inp), nil } -func readZipFileContent(reader *zip.ReadCloser, name string) ([]byte, error) { +func readZipFileContent(reader *zip.ReadCloser, name string) (string, error) { f, err := reader.Open(name) if err != nil { - return nil, err + return "", err } defer f.Close() - return io.ReadAll(f) + buf, err := io.ReadAll(f) + if err != nil { + return "", err + } + return string(buf), nil } Index: box/manager/anteroom_test.go ================================================================== --- box/manager/anteroom_test.go +++ box/manager/anteroom_test.go @@ -39,11 +39,11 @@ t.Errorf("Expected more than one room, but got only one") } count := 0 for ; count < 1000; count++ { - action, _, _ = ar.Dequeue() + action, _, _ := ar.Dequeue() if action == arNothing { break } } if count != 3 { Index: box/manager/box.go ================================================================== --- box/manager/box.go +++ box/manager/box.go @@ -10,13 +10,13 @@ // Package manager coordinates the various boxes and indexes of a Zettelstore. package manager import ( - "bytes" "context" "errors" + "strings" "zettelstore.de/z/box" "zettelstore.de/z/domain" "zettelstore.de/z/domain/id" "zettelstore.de/z/domain/meta" @@ -28,18 +28,18 @@ // Location returns some information where the box is located. func (mgr *Manager) Location() string { if len(mgr.boxes) <= 2 { return "NONE" } - var buf bytes.Buffer + var sb strings.Builder for i := 0; i < len(mgr.boxes)-2; i++ { if i > 0 { - buf.WriteString(", ") + sb.WriteString(", ") } - buf.WriteString(mgr.boxes[i].Location()) + sb.WriteString(mgr.boxes[i].Location()) } - return buf.String() + return sb.String() } // CanCreateZettel returns true, if box could possibly create a new zettel. func (mgr *Manager) CanCreateZettel(ctx context.Context) bool { mgr.mgrMx.RLock() Index: box/manager/enrich.go ================================================================== --- box/manager/enrich.go +++ box/manager/enrich.go @@ -13,11 +13,10 @@ import ( "context" "strconv" - "zettelstore.de/c/api" "zettelstore.de/z/box" "zettelstore.de/z/domain/meta" ) // Enrich computes additional properties and updates the given metadata. @@ -25,29 +24,29 @@ if box.DoNotEnrich(ctx) { // Enrich is called indirectly via indexer or enrichment is not requested // because of other reasons -> ignore this call, do not update meta data return } - m.Set(api.KeyBoxNumber, strconv.Itoa(boxNumber)) + m.Set(meta.KeyBoxNumber, strconv.Itoa(boxNumber)) computePublished(m) mgr.idxStore.Enrich(ctx, m) } func computePublished(m *meta.Meta) { - if _, ok := m.Get(api.KeyPublished); ok { + if _, ok := m.Get(meta.KeyPublished); ok { return } - if modified, ok := m.Get(api.KeyModified); ok { + if modified, ok := m.Get(meta.KeyModified); ok { if _, ok = meta.TimeValue(modified); ok { - m.Set(api.KeyPublished, modified) + m.Set(meta.KeyPublished, modified) return } } zid := m.Zid.String() if _, ok := meta.TimeValue(zid); ok { - m.Set(api.KeyPublished, zid) + m.Set(meta.KeyPublished, zid) return } // Neither the zettel was modified nor the zettel identifer contains a valid // timestamp. In this case do not set the "published" property. } Index: box/manager/indexer.go ================================================================== --- box/manager/indexer.go +++ box/manager/indexer.go @@ -14,11 +14,10 @@ import ( "context" "net/url" "time" - "zettelstore.de/c/api" "zettelstore.de/z/box" "zettelstore.de/z/box/manager/store" "zettelstore.de/z/domain" "zettelstore.de/z/domain/id" "zettelstore.de/z/domain/meta" @@ -142,11 +141,11 @@ return true } func (mgr *Manager) idxUpdateZettel(ctx context.Context, zettel domain.Zettel) { m := zettel.Meta - if m.GetBool(api.KeyNoIndex) { + if m.GetBool(meta.KeyNoIndex) { // Zettel maybe in index toCheck := mgr.idxStore.DeleteZettel(ctx, m.Zid) mgr.idxCheckZettel(toCheck) return } @@ -204,11 +203,11 @@ func (mgr *Manager) idxUpdateValue(ctx context.Context, inverseKey, value string, zi *store.ZettelIndex) { zid, err := id.Parse(value) if err != nil { return } - if _, err = mgr.GetMeta(ctx, zid); err != nil { + if _, err := mgr.GetMeta(ctx, zid); err != nil { zi.AddDeadRef(zid) return } if inverseKey == "" { zi.AddBackRef(zid) Index: box/manager/manager.go ================================================================== --- box/manager/manager.go +++ box/manager/manager.go @@ -12,10 +12,11 @@ package manager import ( "context" "io" + "log" "net/url" "sort" "sync" "time" @@ -70,11 +71,11 @@ var registry = map[string]createFunc{} // Register the encoder for later retrieval. func Register(scheme string, create createFunc) { if _, ok := registry[scheme]; ok { - panic(scheme) + log.Fatalf("Box with scheme %q already registered", scheme) } registry[scheme] = create } // GetSchemes returns all registered scheme, ordered by scheme string. @@ -232,11 +233,11 @@ err := ssi.Start(ctx) if err == nil { continue } for j := i + 1; j < len(mgr.boxes); j++ { - if ssj, ok2 := mgr.boxes[j].(box.StartStopper); ok2 { + if ssj, ok := mgr.boxes[j].(box.StartStopper); ok { ssj.Stop(ctx) } } mgr.mgrMx.Unlock() return err Index: box/manager/memstore/memstore.go ================================================================== --- box/manager/memstore/memstore.go +++ box/manager/memstore/memstore.go @@ -17,11 +17,10 @@ "io" "sort" "strings" "sync" - "zettelstore.de/c/api" "zettelstore.de/z/box/manager/store" "zettelstore.de/z/domain/id" "zettelstore.de/z/domain/meta" ) @@ -85,44 +84,45 @@ if !ok { return false } var updated bool if len(zi.dead) > 0 { - m.Set(api.KeyDead, zi.dead.String()) + m.Set(meta.KeyDead, zi.dead.String()) updated = true } back := removeOtherMetaRefs(m, zi.backward.Copy()) if len(zi.backward) > 0 { - m.Set(api.KeyBackward, zi.backward.String()) + m.Set(meta.KeyBackward, zi.backward.String()) updated = true } if len(zi.forward) > 0 { - m.Set(api.KeyForward, zi.forward.String()) + m.Set(meta.KeyForward, zi.forward.String()) back = remRefs(back, zi.forward) updated = true } - for k, refs := range zi.meta { - if len(refs.backward) > 0 { - m.Set(k, refs.backward.String()) - back = remRefs(back, refs.backward) - updated = true + if len(zi.meta) > 0 { + for k, refs := range zi.meta { + if len(refs.backward) > 0 { + m.Set(k, refs.backward.String()) + back = remRefs(back, refs.backward) + updated = true + } } } if len(back) > 0 { - m.Set(api.KeyBack, back.String()) + m.Set(meta.KeyBack, back.String()) updated = true } - if itags := zi.itags; itags != "" { - m.Set(api.KeyContentTags, itags) - if tags, ok2 := m.Get(api.KeyTags); ok2 { - m.Set(api.KeyAllTags, tags+" "+itags) + if zi.itags != "" { + if tags, ok := m.Get(meta.KeyTags); ok { + m.Set(meta.KeyAllTags, tags+" "+zi.itags) } else { - m.Set(api.KeyAllTags, itags) + m.Set(meta.KeyAllTags, zi.itags) } updated = true - } else if tags, ok2 := m.Get(api.KeyTags); ok2 { - m.Set(api.KeyAllTags, tags) + } else if tags, ok := m.Get(meta.KeyTags); ok { + m.Set(meta.KeyAllTags, tags) updated = true } return updated } ADDED client/client.go Index: client/client.go ================================================================== --- client/client.go +++ client/client.go @@ -0,0 +1,539 @@ +//----------------------------------------------------------------------------- +// Copyright (c) 2021 Detlef Stern +// +// This file is part of zettelstore. +// +// Zettelstore is licensed under the latest version of the EUPL (European Union +// Public License). Please see file LICENSE.txt for your rights and obligations +// under this license. +//----------------------------------------------------------------------------- + +// Package client provides a client for accessing the Zettelstore via its API. +package client + +import ( + "bytes" + "context" + "encoding/json" + "errors" + "io" + "net" + "net/http" + "net/url" + "strconv" + "strings" + "time" + + "zettelstore.de/z/api" + "zettelstore.de/z/domain/id" +) + +// Client contains all data to execute requests. +type Client struct { + baseURL string + username string + password string + token string + tokenType string + expires time.Time + client http.Client +} + +// NewClient create a new client. +func NewClient(baseURL string) *Client { + if !strings.HasSuffix(baseURL, "/") { + baseURL += "/" + } + c := Client{ + baseURL: baseURL, + client: http.Client{ + Timeout: 10 * time.Second, + Transport: &http.Transport{ + DialContext: (&net.Dialer{ + Timeout: 5 * time.Second, // TCP connect timeout + }).DialContext, + TLSHandshakeTimeout: 5 * time.Second, + }, + }, + } + return &c +} + +func (c *Client) newURLBuilder(key byte) *api.URLBuilder { + return api.NewURLBuilder(c.baseURL, 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) +} + +func (c *Client) executeRequest(req *http.Request) (*http.Response, error) { + if c.token != "" { + 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() + } + 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) { + 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. +func (c *Client) SetAuth(username, password string) { + c.username = username + c.password = password + c.token = "" + c.tokenType = "" + c.expires = time.Time{} +} + +func (c *Client) executeAuthRequest(req *http.Request) error { + resp, err := c.executeRequest(req) + if err != nil { + return err + } + defer resp.Body.Close() + if resp.StatusCode != http.StatusOK { + return errors.New(resp.Status) + } + dec := json.NewDecoder(resp.Body) + var tinfo api.AuthJSON + err = dec.Decode(&tinfo) + if err != nil { + return err + } + c.token = tinfo.Token + c.tokenType = tinfo.Type + c.expires = time.Now().Add(time.Duration(tinfo.Expires*10/9) * time.Second) + return nil +} + +func (c *Client) updateToken(ctx context.Context) error { + if c.username == "" { + return nil + } + if time.Now().After(c.expires) { + return c.Authenticate(ctx) + } + return c.RefreshToken(ctx) +} + +// Authenticate sets a new token by sending user name and password. +func (c *Client) Authenticate(ctx context.Context) error { + authData := url.Values{"username": {c.username}, "password": {c.password}} + req, err := c.newRequest(ctx, http.MethodPost, c.newURLBuilder('a'), strings.NewReader(authData.Encode())) + if err != nil { + return err + } + req.Header.Set("Content-Type", "application/x-www-form-urlencoded") + return c.executeAuthRequest(req) +} + +// RefreshToken updates the access token +func (c *Client) RefreshToken(ctx context.Context) error { + req, err := c.newRequest(ctx, http.MethodPut, c.newURLBuilder('a'), nil) + if err != nil { + return err + } + return c.executeAuthRequest(req) +} + +// CreateZettel creates a new zettel and returns its URL. +func (c *Client) CreateZettel(ctx context.Context, data string) (id.Zid, error) { + ub := c.jsonZettelURLBuilder('z', nil) + resp, err := c.buildAndExecuteRequest(ctx, http.MethodPost, ub, strings.NewReader(data), nil) + if err != nil { + return id.Invalid, err + } + defer resp.Body.Close() + if resp.StatusCode != http.StatusCreated { + return id.Invalid, errors.New(resp.Status) + } + b, err := io.ReadAll(resp.Body) + if err != nil { + return id.Invalid, err + } + zid, err := id.Parse(string(b)) + if err != nil { + return id.Invalid, err + } + return zid, nil +} + +// CreateZettelJSON creates a new zettel and returns its URL. +func (c *Client) CreateZettelJSON(ctx context.Context, data *api.ZettelDataJSON) (id.Zid, error) { + var buf bytes.Buffer + if err := encodeZettelData(&buf, data); err != nil { + return id.Invalid, err + } + ub := c.jsonZettelURLBuilder('j', nil) + resp, err := c.buildAndExecuteRequest(ctx, http.MethodPost, ub, &buf, nil) + if err != nil { + return id.Invalid, err + } + defer resp.Body.Close() + if resp.StatusCode != http.StatusCreated { + return id.Invalid, errors.New(resp.Status) + } + dec := json.NewDecoder(resp.Body) + var newZid api.ZidJSON + err = dec.Decode(&newZid) + if err != nil { + return id.Invalid, err + } + zid, err := id.Parse(newZid.ID) + if err != nil { + return id.Invalid, err + } + return zid, nil +} + +func encodeZettelData(buf *bytes.Buffer, data *api.ZettelDataJSON) error { + enc := json.NewEncoder(buf) + enc.SetEscapeHTML(false) + return enc.Encode(&data) +} + +// ListZettel returns a list of all Zettel. +func (c *Client) ListZettel(ctx context.Context, query url.Values) (string, error) { + ub := c.jsonZettelURLBuilder('z', query) + resp, err := c.buildAndExecuteRequest(ctx, http.MethodGet, ub, nil, nil) + if err != nil { + return "", err + } + defer resp.Body.Close() + if resp.StatusCode != http.StatusOK { + return "", errors.New(resp.Status) + } + data, err := io.ReadAll(resp.Body) + if err != nil { + return "", err + } + return string(data), nil +} + +// ListZettelJSON returns a list of all Zettel. +func (c *Client) ListZettelJSON(ctx context.Context, query url.Values) ([]api.ZidMetaJSON, error) { + ub := c.jsonZettelURLBuilder('j', query) + resp, err := c.buildAndExecuteRequest(ctx, http.MethodGet, ub, nil, nil) + if err != nil { + return nil, err + } + defer resp.Body.Close() + if resp.StatusCode != http.StatusOK { + return nil, errors.New(resp.Status) + } + dec := json.NewDecoder(resp.Body) + var zl api.ZettelListJSON + err = dec.Decode(&zl) + if err != nil { + return nil, err + } + return zl.List, nil +} + +// GetZettel returns a zettel as a string. +func (c *Client) GetZettel(ctx context.Context, zid id.Zid, part string) (string, error) { + ub := c.jsonZettelURLBuilder('z', nil).SetZid(zid) + if part != "" && part != api.PartContent { + ub.AppendQuery(api.QueryKeyPart, part) + } + resp, err := c.buildAndExecuteRequest(ctx, http.MethodGet, ub, nil, nil) + if err != nil { + return "", err + } + defer resp.Body.Close() + if resp.StatusCode != http.StatusOK { + return "", errors.New(resp.Status) + } + data, err := io.ReadAll(resp.Body) + if err != nil { + return "", err + } + return string(data), nil +} + +// GetZettelJSON returns a zettel as a JSON struct. +func (c *Client) GetZettelJSON(ctx context.Context, zid id.Zid) (*api.ZettelDataJSON, error) { + ub := c.jsonZettelURLBuilder('j', nil).SetZid(zid) + resp, err := c.buildAndExecuteRequest(ctx, http.MethodGet, ub, nil, nil) + if err != nil { + return nil, err + } + defer resp.Body.Close() + if resp.StatusCode != http.StatusOK { + return nil, errors.New(resp.Status) + } + dec := json.NewDecoder(resp.Body) + var out api.ZettelDataJSON + err = dec.Decode(&out) + if err != nil { + return nil, err + } + return &out, nil +} + +// GetParsedZettel return a parsed zettel in a defined encoding. +func (c *Client) GetParsedZettel(ctx context.Context, zid id.Zid, enc api.EncodingEnum) (string, error) { + return c.getZettelString(ctx, 'p', zid, enc) +} + +// GetEvaluatedZettel return an evaluated zettel in a defined encoding. +func (c *Client) GetEvaluatedZettel(ctx context.Context, zid id.Zid, enc api.EncodingEnum) (string, error) { + return c.getZettelString(ctx, 'v', zid, enc) +} + +func (c *Client) getZettelString(ctx context.Context, key byte, zid id.Zid, enc api.EncodingEnum) (string, error) { + ub := c.jsonZettelURLBuilder(key, nil).SetZid(zid) + ub.AppendQuery(api.QueryKeyEncoding, enc.String()) + ub.AppendQuery(api.QueryKeyPart, api.PartContent) + resp, err := c.buildAndExecuteRequest(ctx, http.MethodGet, ub, nil, nil) + if err != nil { + return "", err + } + defer resp.Body.Close() + if resp.StatusCode != http.StatusOK { + return "", errors.New(resp.Status) + } + content, err := io.ReadAll(resp.Body) + if err != nil { + return "", err + } + return string(content), nil +} + +// GetZettelOrder returns metadata of the given zettel and, more important, +// metadata of zettel that are referenced in a list within the first zettel. +func (c *Client) GetZettelOrder(ctx context.Context, zid id.Zid) (*api.ZidMetaRelatedList, error) { + ub := c.newURLBuilder('o').SetZid(zid) + resp, err := c.buildAndExecuteRequest(ctx, http.MethodGet, ub, nil, nil) + if err != nil { + return nil, err + } + defer resp.Body.Close() + if resp.StatusCode != http.StatusOK { + return nil, errors.New(resp.Status) + } + dec := json.NewDecoder(resp.Body) + var out api.ZidMetaRelatedList + err = dec.Decode(&out) + if err != nil { + return nil, err + } + return &out, nil +} + +// ContextDirection specifies how the context should be calculated. +type ContextDirection uint8 + +// Allowed values for ContextDirection +const ( + _ ContextDirection = iota + DirBoth + DirBackward + DirForward +) + +// GetZettelContext returns metadata of the given zettel and, more important, +// metadata of zettel that for the context of the first zettel. +func (c *Client) GetZettelContext( + ctx context.Context, zid id.Zid, dir ContextDirection, depth, limit int) ( + *api.ZidMetaRelatedList, error, +) { + ub := c.newURLBuilder('x').SetZid(zid) + switch dir { + case DirBackward: + ub.AppendQuery(api.QueryKeyDir, api.DirBackward) + case DirForward: + ub.AppendQuery(api.QueryKeyDir, api.DirForward) + } + if depth > 0 { + ub.AppendQuery(api.QueryKeyDepth, strconv.Itoa(depth)) + } + if limit > 0 { + ub.AppendQuery(api.QueryKeyLimit, strconv.Itoa(limit)) + } + resp, err := c.buildAndExecuteRequest(ctx, http.MethodGet, ub, nil, nil) + if err != nil { + return nil, err + } + defer resp.Body.Close() + if resp.StatusCode != http.StatusOK { + return nil, errors.New(resp.Status) + } + dec := json.NewDecoder(resp.Body) + var out api.ZidMetaRelatedList + err = dec.Decode(&out) + if err != nil { + return nil, err + } + return &out, nil +} + +// GetZettelLinks returns connections to other zettel, embedded material, externals URLs. +func (c *Client) GetZettelLinks(ctx context.Context, zid id.Zid) (*api.ZettelLinksJSON, error) { + ub := c.newURLBuilder('l').SetZid(zid) + resp, err := c.buildAndExecuteRequest(ctx, http.MethodGet, ub, nil, nil) + if err != nil { + return nil, err + } + defer resp.Body.Close() + if resp.StatusCode != http.StatusOK { + return nil, errors.New(resp.Status) + } + dec := json.NewDecoder(resp.Body) + var out api.ZettelLinksJSON + err = dec.Decode(&out) + if err != nil { + return nil, err + } + return &out, nil +} + +// UpdateZettel updates an existing zettel. +func (c *Client) UpdateZettel(ctx context.Context, zid id.Zid, data string) error { + ub := c.jsonZettelURLBuilder('z', nil).SetZid(zid) + resp, err := c.buildAndExecuteRequest(ctx, http.MethodPut, ub, strings.NewReader(data), nil) + if err != nil { + return err + } + defer resp.Body.Close() + if resp.StatusCode != http.StatusNoContent { + return errors.New(resp.Status) + } + return nil +} + +// UpdateZettelJSON updates an existing zettel. +func (c *Client) UpdateZettelJSON(ctx context.Context, zid id.Zid, data *api.ZettelDataJSON) error { + var buf bytes.Buffer + if err := encodeZettelData(&buf, data); err != nil { + return err + } + ub := c.jsonZettelURLBuilder('j', nil).SetZid(zid) + 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 errors.New(resp.Status) + } + return nil +} + +// RenameZettel renames a zettel. +func (c *Client) RenameZettel(ctx context.Context, oldZid, newZid id.Zid) error { + ub := c.jsonZettelURLBuilder('z', nil).SetZid(oldZid) + h := http.Header{ + api.HeaderDestination: {c.jsonZettelURLBuilder('z', nil).SetZid(newZid).String()}, + } + resp, err := c.buildAndExecuteRequest(ctx, api.MethodMove, ub, nil, h) + if err != nil { + return err + } + defer resp.Body.Close() + if resp.StatusCode != http.StatusNoContent { + return errors.New(resp.Status) + } + return nil +} + +// DeleteZettel deletes a zettel with the given identifier. +func (c *Client) DeleteZettel(ctx context.Context, zid id.Zid) error { + ub := c.jsonZettelURLBuilder('z', nil).SetZid(zid) + resp, err := c.buildAndExecuteRequest(ctx, http.MethodDelete, ub, nil, nil) + if err != nil { + return err + } + defer resp.Body.Close() + if resp.StatusCode != http.StatusNoContent { + return errors.New(resp.Status) + } + return nil +} + +func (c *Client) jsonZettelURLBuilder(key byte, query url.Values) *api.URLBuilder { + ub := c.newURLBuilder(key) + for key, values := range query { + if key == api.QueryKeyEncoding { + continue + } + for _, val := range values { + ub.AppendQuery(key, val) + } + } + return ub +} + +// ListTags returns a map of all tags, together with the associated zettel containing this tag. +func (c *Client) ListTags(ctx context.Context) (map[string][]string, error) { + err := c.updateToken(ctx) + if err != nil { + return nil, err + } + req, err := c.newRequest(ctx, http.MethodGet, c.newURLBuilder('t'), nil) + if err != nil { + return nil, err + } + resp, err := c.executeRequest(req) + if err != nil { + return nil, err + } + defer resp.Body.Close() + if resp.StatusCode != http.StatusOK { + return nil, errors.New(resp.Status) + } + dec := json.NewDecoder(resp.Body) + var tl api.TagListJSON + err = dec.Decode(&tl) + if err != nil { + return nil, err + } + return tl.Tags, nil +} + +// ListRoles returns a list of all roles. +func (c *Client) ListRoles(ctx context.Context) ([]string, error) { + err := c.updateToken(ctx) + if err != nil { + return nil, err + } + req, err := c.newRequest(ctx, http.MethodGet, c.newURLBuilder('r'), nil) + if err != nil { + return nil, err + } + resp, err := c.executeRequest(req) + if err != nil { + return nil, err + } + defer resp.Body.Close() + if resp.StatusCode != http.StatusOK { + return nil, errors.New(resp.Status) + } + dec := json.NewDecoder(resp.Body) + var rl api.RoleListJSON + err = dec.Decode(&rl) + if err != nil { + return nil, err + } + return rl.Roles, nil +} ADDED client/client_test.go Index: client/client_test.go ================================================================== --- client/client_test.go +++ client/client_test.go @@ -0,0 +1,446 @@ +//----------------------------------------------------------------------------- +// Copyright (c) 2021 Detlef Stern +// +// This file is part of zettelstore. +// +// Zettelstore is licensed under the latest version of the EUPL (European Union +// Public License). Please see file LICENSE.txt for your rights and obligations +// under this license. +//----------------------------------------------------------------------------- + +// Package client provides a client for accessing the Zettelstore via its API. +package client_test + +import ( + "context" + "flag" + "fmt" + "net/url" + "strings" + "testing" + + "zettelstore.de/z/api" + "zettelstore.de/z/client" + "zettelstore.de/z/domain/id" + "zettelstore.de/z/domain/meta" +) + +func TestCreateGetRenameDeleteZettel(t *testing.T) { + // Is not to be allowed to run in parallel with other tests. + zettel := `title: A Test + +Example content.` + c := getClient() + c.SetAuth("owner", "owner") + zid, err := c.CreateZettel(context.Background(), zettel) + if err != nil { + t.Error("Cannot create zettel:", err) + return + } + if !zid.IsValid() { + t.Error("Invalid zettel ID", zid) + return + } + data, err := c.GetZettel(context.Background(), zid, api.PartZettel) + if err != nil { + t.Error("Cannot read zettel", zid, err) + return + } + exp := `title: A Test +role: zettel +syntax: zmk + +Example content.` + if data != exp { + t.Errorf("Expected zettel data: %q, but got %q", exp, data) + } + newZid := zid + 1 + err = c.RenameZettel(context.Background(), zid, newZid) + if err != nil { + t.Error("Cannot rename", zid, ":", err) + newZid = zid + } + err = c.DeleteZettel(context.Background(), newZid) + if err != nil { + t.Error("Cannot delete", zid, ":", err) + return + } +} + +func TestCreateRenameDeleteZettelJSON(t *testing.T) { + // Is not to be allowed to run in parallel with other tests. + c := getClient() + c.SetAuth("creator", "creator") + zid, err := c.CreateZettelJSON(context.Background(), &api.ZettelDataJSON{ + Meta: nil, + Encoding: "", + Content: "Example", + }) + if err != nil { + t.Error("Cannot create zettel:", err) + return + } + if !zid.IsValid() { + t.Error("Invalid zettel ID", zid) + return + } + newZid := zid + 1 + c.SetAuth("owner", "owner") + err = c.RenameZettel(context.Background(), zid, newZid) + if err != nil { + t.Error("Cannot rename", zid, ":", err) + newZid = zid + } + err = c.DeleteZettel(context.Background(), newZid) + if err != nil { + t.Error("Cannot delete", zid, ":", err) + return + } +} + +func TestUpdateZettel(t *testing.T) { + t.Parallel() + c := getClient() + c.SetAuth("owner", "owner") + z, err := c.GetZettel(context.Background(), id.DefaultHomeZid, api.PartZettel) + if err != nil { + t.Error(err) + return + } + if !strings.HasPrefix(z, "title: Home\n") { + t.Error("Got unexpected zettel", z) + return + } + newZettel := `title: New Home +role: zettel +syntax: zmk + +Empty` + err = c.UpdateZettel(context.Background(), id.DefaultHomeZid, newZettel) + if err != nil { + t.Error(err) + return + } + zt, err := c.GetZettel(context.Background(), id.DefaultHomeZid, api.PartZettel) + if err != nil { + t.Error(err) + return + } + if zt != newZettel { + t.Errorf("Expected zettel %q, got %q", newZettel, zt) + } + // Must delete to clean up for next tests + err = c.DeleteZettel(context.Background(), id.DefaultHomeZid) + if err != nil { + t.Error("Cannot delete", id.DefaultHomeZid, ":", err) + return + } +} + +func TestUpdateZettelJSON(t *testing.T) { + t.Parallel() + c := getClient() + c.SetAuth("writer", "writer") + z, err := c.GetZettelJSON(context.Background(), id.DefaultHomeZid) + if err != nil { + t.Error(err) + return + } + if got := z.Meta[meta.KeyTitle]; got != "Home" { + t.Errorf("Title of zettel is not \"Home\", but %q", got) + return + } + newTitle := "New Home" + z.Meta[meta.KeyTitle] = newTitle + err = c.UpdateZettelJSON(context.Background(), id.DefaultHomeZid, z) + if err != nil { + t.Error(err) + return + } + zt, err := c.GetZettelJSON(context.Background(), id.DefaultHomeZid) + if err != nil { + t.Error(err) + return + } + if got := zt.Meta[meta.KeyTitle]; got != newTitle { + t.Errorf("Title of zettel is not %q, but %q", newTitle, got) + } + // No need to clean up, because we just changed the title. +} + +func TestListZettel(t *testing.T) { + testdata := []struct { + user string + exp int + }{ + {"", 7}, + {"creator", 10}, + {"reader", 12}, + {"writer", 12}, + {"owner", 34}, + } + + t.Parallel() + c := getClient() + query := url.Values{api.QueryKeyEncoding: {api.EncodingHTML}} // Client must remove "html" + for i, tc := range testdata { + t.Run(fmt.Sprintf("User %d/%q", i, tc.user), func(tt *testing.T) { + c.SetAuth(tc.user, tc.user) + l, err := c.ListZettelJSON(context.Background(), query) + if err != nil { + tt.Error(err) + return + } + got := len(l) + if got != tc.exp { + tt.Errorf("List of length %d expected, but got %d\n%v", tc.exp, got, l) + } + }) + } + l, err := c.ListZettelJSON(context.Background(), url.Values{meta.KeyRole: {meta.ValueRoleConfiguration}}) + if err != nil { + t.Error(err) + return + } + got := len(l) + if got != 27 { + t.Errorf("List of length %d expected, but got %d\n%v", 27, got, l) + } + + pl, err := c.ListZettel(context.Background(), url.Values{meta.KeyRole: {meta.ValueRoleConfiguration}}) + if err != nil { + t.Error(err) + return + } + lines := strings.Split(pl, "\n") + if lines[len(lines)-1] == "" { + lines = lines[:len(lines)-1] + } + if len(lines) != len(l) { + t.Errorf("Different list lenght: Plain=%d, JSON=%d", len(lines), len(l)) + } else { + for i, line := range lines { + if got := line[:14]; got != l[i].ID { + t.Errorf("%d: JSON=%q, got=%q", i, l[i].ID, got) + } + } + } +} + +func TestGetZettelJSON(t *testing.T) { + t.Parallel() + c := getClient() + c.SetAuth("owner", "owner") + z, err := c.GetZettelJSON(context.Background(), id.DefaultHomeZid) + if err != nil { + t.Error(err) + return + } + if m := z.Meta; len(m) == 0 { + t.Errorf("Exptected non-empty meta, but got %v", z.Meta) + } + if z.Content == "" || z.Encoding != "" { + t.Errorf("Expect non-empty content, but empty encoding (got %q)", z.Encoding) + } +} + +func TestGetParsedEvaluatedZettel(t *testing.T) { + t.Parallel() + c := getClient() + c.SetAuth("owner", "owner") + encodings := []api.EncodingEnum{ + api.EncoderDJSON, + api.EncoderHTML, + api.EncoderNative, + api.EncoderText, + } + for _, enc := range encodings { + content, err := c.GetParsedZettel(context.Background(), id.DefaultHomeZid, enc) + if err != nil { + t.Error(err) + continue + } + if len(content) == 0 { + t.Errorf("Empty content for parsed encoding %v", enc) + } + content, err = c.GetEvaluatedZettel(context.Background(), id.DefaultHomeZid, enc) + if err != nil { + t.Error(err) + continue + } + if len(content) == 0 { + t.Errorf("Empty content for evaluated encoding %v", enc) + } + } +} + +func checkZid(t *testing.T, expected id.Zid, got string) bool { + t.Helper() + if exp := expected.String(); exp != got { + t.Errorf("Expected a Zid %q, but got %q", exp, got) + return false + } + return true +} + +func checkListZid(t *testing.T, l []api.ZidMetaJSON, pos int, expected id.Zid) { + t.Helper() + exp := expected.String() + if got := l[pos].ID; got != exp { + t.Errorf("Expected result[%d]=%v, but got %v", pos, exp, got) + } +} + +func TestGetZettelOrder(t *testing.T) { + t.Parallel() + c := getClient() + c.SetAuth("owner", "owner") + rl, err := c.GetZettelOrder(context.Background(), id.TOCNewTemplateZid) + if err != nil { + t.Error(err) + return + } + if !checkZid(t, id.TOCNewTemplateZid, rl.ID) { + return + } + l := rl.List + if got := len(l); got != 2 { + t.Errorf("Expected list fo length 2, got %d", got) + return + } + checkListZid(t, l, 0, id.TemplateNewZettelZid) + checkListZid(t, l, 1, id.TemplateNewUserZid) +} + +func TestGetZettelContext(t *testing.T) { + t.Parallel() + c := getClient() + c.SetAuth("owner", "owner") + rl, err := c.GetZettelContext(context.Background(), id.VersionZid, client.DirBoth, 0, 3) + if err != nil { + t.Error(err) + return + } + if !checkZid(t, id.VersionZid, rl.ID) { + return + } + l := rl.List + if got := len(l); got != 3 { + t.Errorf("Expected list fo length 3, got %d", got) + return + } + checkListZid(t, l, 0, id.DefaultHomeZid) + checkListZid(t, l, 1, id.OperatingSystemZid) + checkListZid(t, l, 2, id.StartupConfigurationZid) + + rl, err = c.GetZettelContext(context.Background(), id.VersionZid, client.DirBackward, 0, 0) + if err != nil { + t.Error(err) + return + } + if !checkZid(t, id.VersionZid, rl.ID) { + return + } + l = rl.List + if got := len(l); got != 1 { + t.Errorf("Expected list fo length 1, got %d", got) + return + } + checkListZid(t, l, 0, id.DefaultHomeZid) +} + +func TestGetZettelLinks(t *testing.T) { + t.Parallel() + c := getClient() + c.SetAuth("owner", "owner") + zl, err := c.GetZettelLinks(context.Background(), id.DefaultHomeZid) + if err != nil { + t.Error(err) + return + } + if !checkZid(t, id.DefaultHomeZid, zl.ID) { + return + } + if len(zl.Linked.Incoming) != 0 { + t.Error("No incomings expected", zl.Linked.Incoming) + } + if got := len(zl.Linked.Outgoing); got != 4 { + t.Errorf("Expected 4 outgoing links, got %d", got) + } + if got := len(zl.Linked.Local); got != 1 { + t.Errorf("Expected 1 local link, got %d", got) + } + if got := len(zl.Linked.External); got != 4 { + t.Errorf("Expected 4 external link, got %d", got) + } +} + +func TestListTags(t *testing.T) { + t.Parallel() + c := getClient() + c.SetAuth("owner", "owner") + tm, err := c.ListTags(context.Background()) + if err != nil { + t.Error(err) + return + } + tags := []struct { + key string + size int + }{ + {"#invisible", 1}, + {"#user", 4}, + {"#test", 4}, + } + if len(tm) != len(tags) { + t.Errorf("Expected %d different tags, but got only %d (%v)", len(tags), len(tm), tm) + } + for _, tag := range tags { + if zl, ok := tm[tag.key]; !ok { + t.Errorf("No tag %v: %v", tag.key, tm) + } else if len(zl) != tag.size { + t.Errorf("Expected %d zettel with tag %v, but got %v", tag.size, tag.key, zl) + } + } + for i, id := range tm["#user"] { + if id != tm["#test"][i] { + t.Errorf("Tags #user and #test have different content: %v vs %v", tm["#user"], tm["#test"]) + } + } +} + +func TestListRoles(t *testing.T) { + t.Parallel() + c := getClient() + c.SetAuth("owner", "owner") + rl, err := c.ListRoles(context.Background()) + if err != nil { + t.Error(err) + return + } + exp := []string{"configuration", "user", "zettel"} + if len(rl) != len(exp) { + t.Errorf("Expected %d different tags, but got only %d (%v)", len(exp), len(rl), rl) + } + for i, id := range exp { + if id != rl[i] { + t.Errorf("Role list pos %d: expected %q, got %q", i, id, rl[i]) + } + } +} + +var baseURL string + +func init() { + flag.StringVar(&baseURL, "base-url", "", "Base URL") +} + +func getClient() *client.Client { return client.NewClient(baseURL) } + +// TestMain controls whether client API tests should run or not. +func TestMain(m *testing.M) { + flag.Parse() + if baseURL != "" { + m.Run() + } +} Index: cmd/cmd_file.go ================================================================== --- cmd/cmd_file.go +++ cmd/cmd_file.go @@ -14,11 +14,11 @@ "flag" "fmt" "io" "os" - "zettelstore.de/c/api" + "zettelstore.de/z/api" "zettelstore.de/z/domain" "zettelstore.de/z/domain/id" "zettelstore.de/z/domain/meta" "zettelstore.de/z/encoder" "zettelstore.de/z/input" @@ -25,11 +25,11 @@ "zettelstore.de/z/parser" ) // ---------- Subcommand: file ----------------------------------------------- -func cmdFile(fs *flag.FlagSet) (int, error) { +func cmdFile(fs *flag.FlagSet, _ *meta.Meta) (int, error) { enc := fs.Lookup("t").Value.String() m, inp, err := getInput(fs.Args()) if m == nil { return 2, err } @@ -36,15 +36,15 @@ z := parser.ParseZettel( domain.Zettel{ Meta: m, Content: domain.NewContent(inp.Src[inp.Pos:]), }, - m.GetDefault(api.KeySyntax, api.ValueSyntaxZmk), + m.GetDefault(meta.KeySyntax, meta.ValueSyntaxZmk), nil, ) encdr := encoder.Create(api.Encoder(enc), &encoder.Environment{ - Lang: m.GetDefault(api.KeyLang, api.ValueLangEN), + Lang: m.GetDefault(meta.KeyLang, meta.ValueLangEN), }) if encdr == nil { fmt.Fprintf(os.Stderr, "Unknown format %q\n", enc) return 2, nil } @@ -61,26 +61,26 @@ if len(args) < 1 { src, err := io.ReadAll(os.Stdin) if err != nil { return nil, nil, err } - inp := input.NewInput(src) + inp := input.NewInput(string(src)) m := meta.NewFromInput(id.New(true), inp) return m, inp, nil } src, err := os.ReadFile(args[0]) if err != nil { return nil, nil, err } - inp := input.NewInput(src) + inp := input.NewInput(string(src)) m := meta.NewFromInput(id.New(true), inp) if len(args) > 1 { - src, err = os.ReadFile(args[1]) + src, err := os.ReadFile(args[1]) if err != nil { return nil, nil, err } - inp = input.NewInput(src) + inp = input.NewInput(string(src)) } return m, inp, nil } Index: cmd/cmd_password.go ================================================================== --- cmd/cmd_password.go +++ cmd/cmd_password.go @@ -1,7 +1,7 @@ //----------------------------------------------------------------------------- -// Copyright (c) 2020-2021 Detlef Stern +// Copyright (c) 2020 Detlef Stern // // This file is part of zettelstore. // // Zettelstore is licensed under the latest version of the EUPL (European Union // Public License). Please see file LICENSE.txt for your rights and obligations @@ -15,18 +15,18 @@ "fmt" "os" "golang.org/x/term" - "zettelstore.de/c/api" "zettelstore.de/z/auth/cred" "zettelstore.de/z/domain/id" + "zettelstore.de/z/domain/meta" ) // ---------- Subcommand: password ------------------------------------------- -func cmdPassword(fs *flag.FlagSet) (int, error) { +func cmdPassword(fs *flag.FlagSet, cfg *meta.Meta) (int, error) { if fs.NArg() == 0 { fmt.Fprintln(os.Stderr, "User name and user zettel identification missing") return 2, nil } if fs.NArg() == 1 { @@ -58,12 +58,12 @@ hashedPassword, err := cred.HashCredential(zid, ident, password) if err != nil { return 2, err } fmt.Printf("%v: %s\n%v: %s\n", - api.KeyCredential, hashedPassword, - api.KeyUserID, ident, + meta.KeyCredential, hashedPassword, + meta.KeyUserID, ident, ) return 0, nil } func getPassword(prompt string) (string, error) { Index: cmd/cmd_run.go ================================================================== --- cmd/cmd_run.go +++ cmd/cmd_run.go @@ -14,10 +14,11 @@ "flag" "zettelstore.de/z/auth" "zettelstore.de/z/box" "zettelstore.de/z/config" + "zettelstore.de/z/domain/meta" "zettelstore.de/z/kernel" "zettelstore.de/z/usecase" "zettelstore.de/z/web/adapter/api" "zettelstore.de/z/web/adapter/webui" "zettelstore.de/z/web/server" @@ -33,18 +34,25 @@ fs.Bool("r", false, "system-wide read-only mode") fs.Bool("v", false, "verbose mode") fs.Bool("debug", false, "debug mode") } -func runFunc(*flag.FlagSet) (int, error) { - exitCode, err := doRun() +func withDebug(fs *flag.FlagSet) bool { + dbg := fs.Lookup("debug") + return dbg != nil && dbg.Value.String() == "true" +} + +func runFunc(fs *flag.FlagSet, _ *meta.Meta) (int, error) { + exitCode, err := doRun(withDebug(fs)) kernel.Main.WaitForShutdown() return exitCode, err } -func doRun() (int, error) { - if err := kernel.Main.StartService(kernel.WebService); err != nil { +func doRun(debug bool) (int, error) { + kern := kernel.Main + kern.SetDebug(debug) + if err := kern.StartService(kernel.WebService); err != nil { return 1, err } return 0, nil } @@ -61,27 +69,25 @@ ucParseZettel := usecase.NewParseZettel(rtConfig, ucGetZettel) ucEvaluate := usecase.NewEvaluate(rtConfig, ucGetZettel, ucGetMeta) ucListMeta := usecase.NewListMeta(protectedBoxManager) ucListRoles := usecase.NewListRole(protectedBoxManager) ucListTags := usecase.NewListTags(protectedBoxManager) - ucZettelContext := usecase.NewZettelContext(protectedBoxManager, rtConfig) + ucZettelContext := usecase.NewZettelContext(protectedBoxManager) ucDelete := usecase.NewDeleteZettel(protectedBoxManager) ucUpdate := usecase.NewUpdateZettel(protectedBoxManager) ucRename := usecase.NewRenameZettel(protectedBoxManager) webSrv.Handle("/", wui.MakeGetRootHandler(protectedBoxManager)) // Web user interface if !authManager.IsReadonly() { - webSrv.AddZettelRoute('b', server.MethodGet, wui.MakeGetRenameZettelHandler( - ucGetMeta, &ucEvaluate)) + webSrv.AddZettelRoute('b', server.MethodGet, wui.MakeGetRenameZettelHandler(ucGetMeta)) webSrv.AddZettelRoute('b', server.MethodPost, wui.MakePostRenameZettelHandler(ucRename)) webSrv.AddZettelRoute('c', server.MethodGet, wui.MakeGetCopyZettelHandler( ucGetZettel, usecase.NewCopyZettel())) webSrv.AddZettelRoute('c', server.MethodPost, wui.MakePostCreateZettelHandler(ucCreateZettel)) - webSrv.AddZettelRoute('d', server.MethodGet, wui.MakeGetDeleteZettelHandler( - ucGetMeta, ucGetAllMeta, &ucEvaluate)) + webSrv.AddZettelRoute('d', server.MethodGet, wui.MakeGetDeleteZettelHandler(ucGetZettel)) webSrv.AddZettelRoute('d', server.MethodPost, wui.MakePostDeleteZettelHandler(ucDelete)) webSrv.AddZettelRoute('e', server.MethodGet, wui.MakeEditGetZettelHandler(ucGetZettel)) webSrv.AddZettelRoute('e', server.MethodPost, wui.MakeEditSetZettelHandler(ucUpdate)) webSrv.AddZettelRoute('f', server.MethodGet, wui.MakeGetFolgeZettelHandler( ucGetZettel, usecase.NewFolgeZettel(rtConfig))) @@ -108,17 +114,15 @@ webSrv.AddListRoute('a', server.MethodPost, a.MakePostLoginHandler(ucAuthenticate)) webSrv.AddListRoute('a', server.MethodPut, a.MakeRenewAuthHandler()) webSrv.AddListRoute('j', server.MethodGet, api.MakeListMetaHandler(ucListMeta)) webSrv.AddZettelRoute('j', server.MethodGet, api.MakeGetZettelHandler(ucGetZettel)) webSrv.AddZettelRoute('l', server.MethodGet, api.MakeGetLinksHandler(ucEvaluate)) - webSrv.AddZettelRoute('m', server.MethodGet, api.MakeGetMetaHandler(ucGetMeta)) webSrv.AddZettelRoute('o', server.MethodGet, api.MakeGetOrderHandler( usecase.NewZettelOrder(protectedBoxManager, ucEvaluate))) webSrv.AddZettelRoute('p', server.MethodGet, a.MakeGetParsedZettelHandler(ucParseZettel)) webSrv.AddListRoute('r', server.MethodGet, api.MakeListRoleHandler(ucListRoles)) webSrv.AddListRoute('t', server.MethodGet, api.MakeListTagsHandler(ucListTags)) - webSrv.AddListRoute('v', server.MethodPost, a.MakePostEncodeInlinesHandler(ucEvaluate)) webSrv.AddZettelRoute('v', server.MethodGet, a.MakeGetEvalZettelHandler(ucEvaluate)) webSrv.AddZettelRoute('x', server.MethodGet, api.MakeZettelContextHandler(ucZettelContext)) webSrv.AddListRoute('z', server.MethodGet, a.MakeListPlainHandler(ucListMeta)) webSrv.AddZettelRoute('z', server.MethodGet, a.MakeGetPlainZettelHandler(ucGetZettel)) if !authManager.IsReadonly() { Index: cmd/cmd_run_simple.go ================================================================== --- cmd/cmd_run_simple.go +++ cmd/cmd_run_simple.go @@ -14,21 +14,22 @@ "flag" "fmt" "os" "strings" + "zettelstore.de/z/domain/meta" "zettelstore.de/z/kernel" ) func flgSimpleRun(fs *flag.FlagSet) { fs.String("d", "", "zettel directory") } -func runSimpleFunc(*flag.FlagSet) (int, error) { +func runSimpleFunc(fs *flag.FlagSet, cfg *meta.Meta) (int, error) { kern := kernel.Main listenAddr := kern.GetConfig(kernel.WebService, kernel.WebListenAddress).(string) - exitCode, err := doRun() + exitCode, err := doRun(false) if idx := strings.LastIndexByte(listenAddr, ':'); idx >= 0 { kern.Log() kern.Log("--------------------------") kern.Log("Open your browser and enter the following URL:") kern.Log() Index: cmd/command.go ================================================================== --- cmd/command.go +++ cmd/command.go @@ -11,10 +11,12 @@ package cmd import ( "flag" "sort" + + "zettelstore.de/z/domain/meta" ) // Command stores information about commands / sub-commands. type Command struct { Name string // command name as it appears on the command line @@ -22,16 +24,17 @@ Boxes bool // if true then boxes will be set up Header bool // Print a heading on startup LineServer bool // Start admin line server Flags func(*flag.FlagSet) // function to set up flag.FlagSet flags *flag.FlagSet // flags that belong to the command + } // CommandFunc is the function that executes the command. // It accepts the parsed command line parameters. // It returns the exit code and an error. -type CommandFunc func(*flag.FlagSet) (int, error) +type CommandFunc func(*flag.FlagSet, *meta.Meta) (int, error) // GetFlags return the flag.FlagSet defined for the command. func (c *Command) GetFlags() *flag.FlagSet { return c.flags } var commands = make(map[string]Command) Index: cmd/main.go ================================================================== --- cmd/main.go +++ cmd/main.go @@ -18,11 +18,11 @@ "net/url" "os" "strconv" "strings" - "zettelstore.de/c/api" + "zettelstore.de/z/api" "zettelstore.de/z/auth" "zettelstore.de/z/auth/impl" "zettelstore.de/z/box" "zettelstore.de/z/box/compbox" "zettelstore.de/z/box/manager" @@ -39,21 +39,21 @@ ) func init() { RegisterCommand(Command{ Name: "help", - Func: func(*flag.FlagSet) (int, error) { + Func: func(*flag.FlagSet, *meta.Meta) (int, error) { fmt.Println("Available commands:") for _, name := range List() { fmt.Printf("- %q\n", name) } return 0, nil }, }) RegisterCommand(Command{ Name: "version", - Func: func(*flag.FlagSet) (int, error) { return 0, nil }, + Func: func(*flag.FlagSet, *meta.Meta) (int, error) { return 0, nil }, Header: true, }) RegisterCommand(Command{ Name: "run", Func: runFunc, @@ -91,11 +91,11 @@ } content, err := os.ReadFile(configFile) if err != nil { return meta.New(id.Invalid) } - return meta.NewFromInput(id.Invalid, input.NewInput(content)) + return meta.NewFromInput(id.Invalid, input.NewInput(string(content))) } func getConfig(fs *flag.FlagSet) *meta.Meta { cfg := readConfig(fs) fs.Visit(func(flg *flag.Flag) { @@ -113,14 +113,11 @@ if strings.HasPrefix(val, "/") { val = "dir://" + val } else { val = "dir:" + val } - deleteConfiguredBoxes(cfg) cfg.Set(keyBoxOneURI, val) - case "debug": - cfg.Set(keyDebug, flg.Value.String()) case "r": cfg.Set(keyReadOnly, flg.Value.String()) case "v": cfg.Set(keyVerbose, flg.Value.String()) } @@ -135,21 +132,12 @@ return "", err } return strconv.Itoa(port), nil } -func deleteConfiguredBoxes(cfg *meta.Meta) { - for _, p := range cfg.PairsRest(false) { - if key := p.Key; strings.HasPrefix(key, kernel.BoxURIs) { - cfg.Delete(key) - } - } -} - const ( keyAdminPort = "admin-port" - keyDebug = "debug-mode" keyDefaultDirBoxType = "default-dir-box-type" keyInsecureCookie = "insecure-cookie" keyListenAddr = "listen-addr" keyOwner = "owner" keyPersistentCookie = "persistent-cookie" @@ -156,16 +144,15 @@ keyBoxOneURI = kernel.BoxURIs + "1" keyReadOnly = "read-only-mode" keyTokenLifetimeHTML = "token-lifetime-html" keyTokenLifetimeAPI = "token-lifetime-api" keyURLPrefix = "url-prefix" - keyVerbose = "verbose-mode" + keyVerbose = "verbose" ) func setServiceConfig(cfg *meta.Meta) error { - ok := setConfigValue(true, kernel.CoreService, kernel.CoreDebug, cfg.GetBool(keyDebug)) - ok = setConfigValue(ok, kernel.CoreService, kernel.CoreVerbose, cfg.GetBool(keyVerbose)) + ok := setConfigValue(true, kernel.CoreService, kernel.CoreVerbose, cfg.GetBool(keyVerbose)) if val, found := cfg.Get(keyAdminPort); found { ok = setConfigValue(ok, kernel.CoreService, kernel.CorePort, val) } ok = setConfigValue(ok, kernel.AuthService, kernel.AuthOwner, cfg.GetDefault(keyOwner, "")) @@ -256,11 +243,11 @@ fmt.Fprintf(os.Stderr, "%s: %v\n", name, err) return 2 } setupOperations(cfg, command.Boxes) kernel.Main.Start(command.Header, command.LineServer) - exitCode, err := command.Func(fs) + exitCode, err := command.Func(fs, cfg) if err != nil { fmt.Fprintf(os.Stderr, "%s: %v\n", name, err) } kernel.Main.Shutdown(true) return exitCode Index: config/config.go ================================================================== --- config/config.go +++ config/config.go @@ -10,11 +10,10 @@ // Package config provides functions to retrieve runtime configuration data. package config import ( - "zettelstore.de/c/api" "zettelstore.de/z/domain/id" "zettelstore.de/z/domain/meta" ) // Config allows to retrieve all defined configuration values that can be changed during runtime. @@ -72,11 +71,11 @@ } // GetTitle returns the value of the "title" key of the given meta. If there // is no such value, GetDefaultTitle is returned. func GetTitle(m *meta.Meta, cfg Config) string { - if val, ok := m.Get(api.KeyTitle); ok { + if val, ok := m.Get(meta.KeyTitle); ok { return val } if cfg != nil { return cfg.GetDefaultTitle() } @@ -84,28 +83,28 @@ } // GetRole returns the value of the "role" key of the given meta. If there // is no such value, GetDefaultRole is returned. func GetRole(m *meta.Meta, cfg Config) string { - if val, ok := m.Get(api.KeyRole); ok { + if val, ok := m.Get(meta.KeyRole); ok { return val } return cfg.GetDefaultRole() } // GetSyntax returns the value of the "syntax" key of the given meta. If there // is no such value, GetDefaultSyntax is returned. func GetSyntax(m *meta.Meta, cfg Config) string { - if val, ok := m.Get(api.KeySyntax); ok { + if val, ok := m.Get(meta.KeySyntax); ok { return val } return cfg.GetDefaultSyntax() } // GetLang returns the value of the "lang" key of the given meta. If there is // no such value, GetDefaultLang is returned. func GetLang(m *meta.Meta, cfg Config) string { - if val, ok := m.Get(api.KeyLang); ok { + if val, ok := m.Get(meta.KeyLang); ok { return val } return cfg.GetDefaultLang() } Index: docs/development/20210916193200.zettel ================================================================== --- docs/development/20210916193200.zettel +++ docs/development/20210916193200.zettel @@ -1,20 +1,20 @@ id: 20210916193200 title: Required Software role: zettel syntax: zmk -modified: 20211111143928 +modified: 20210916194748 The following software must be installed: * A current, supported [[release of Go|https://golang.org/doc/devel/release.html]], * [[golint|https://github.com/golang/lint]], * [[shadow|https://pkg.go.dev/golang.org/x/tools/go/analysis/passes/shadow]] via ``go install golang.org/x/tools/go/analysis/passes/shadow/cmd/shadow@latest``, * [[staticcheck|https://staticcheck.io/]] via ``go get honnef.co/go/tools/cmd/staticcheck``, -* [[unparam|https://mvdan.cc/unparam]][^[[GitHub|https://github.com/mvdan/unparam]]] via ``go install mvdan.cc/unparam@latest`` +* [[unparam|mvdan.cc/unparam]][^[[GitHub|https://github.com/mvdan/unparam]]] via ``go install mvdan.cc/unparam@latest`` Make sure that the software is in your path, e.g. via: ```sh export PATH=$PATH:/usr/local/go/bin export PATH=$PATH:$(go env GOPATH)/bin ``` Index: docs/development/20210916194900.zettel ================================================================== --- docs/development/20210916194900.zettel +++ docs/development/20210916194900.zettel @@ -1,15 +1,18 @@ id: 20210916194900 title: Checklist for Release role: zettel syntax: zmk -modified: 20211110193448 +modified: 20210917165448 +# Check for unused parameters: +#* ``unparam ./...`` +#* ``unparam -exported -tests ./...`` # Clean up your Go workspace: #* ``go run tools/build.go clean`` (alternatively: ``make clean``). # All internal tests must succeed: -#* ``go run tools/build.go relcheck`` (alternatively: ``make relcheck``). +#* ``go run tools/build.go check`` (alternatively: ``make check``). # The API tests must succeed on every development platform: #* ``go run tools/build.go testapi`` (alternatively: ``make api``). # Run [[linkchecker|https://linkchecker.github.io/linkchecker/]] with the manual: #* ``go run -race cmd/zettelstore/main.go run -d docs/manual`` #* ``linkchecker http://127.0.0.1:23123 2>&1 | tee lc.txt`` @@ -30,13 +33,11 @@ #* plan.wiki # Set file ''VERSION'' to the new release version # Disable Fossil autosync mode: #* ``fossil setting autosync off`` # Commit the new release version: -#* ``fossil commit --tag release --tag vVERSION -m "Version VERSION"`` -#* **Important:** the tag must follow the given pattern, e.g. ''v0.0.15''. - Otherwise client will not be able to import ''zettelkasten.de/z''. +#* ``fossil commit --tag release --tag version-VERSION -m "Version VERSION"`` # Clean up your Go workspace: #* ``go run tools/build.go clean`` (alternatively: ``make clean``). # Create the release: #* ``go run tools/build.go release`` (alternatively: ``make release``). # Remove previous executables: Index: docs/manual/00001000000000.zettel ================================================================== --- docs/manual/00001000000000.zettel +++ docs/manual/00001000000000.zettel @@ -1,11 +1,10 @@ id: 00001000000000 title: Zettelstore Manual role: manual tags: #manual #zettelstore syntax: zmk -modified: 20211027121716 * [[Introduction|00001001000000]] * [[Design goals|00001002000000]] * [[Installation|00001003000000]] * [[Configuration|00001004000000]] @@ -14,9 +13,9 @@ * [[Zettelmarkup|00001007000000]] * [[Other markup languages|00001008000000]] * [[Security|00001010000000]] * [[API|00001012000000]] * [[Web user interface|00001014000000]] -* [[Troubleshooting|00001018000000]] +* Troubleshooting * Frequently asked questions Licensed under the EUPL-1.2-or-later. Index: docs/manual/00001004010000.zettel ================================================================== --- docs/manual/00001004010000.zettel +++ docs/manual/00001004010000.zettel @@ -1,11 +1,11 @@ id: 00001004010000 title: Zettelstore startup configuration role: manual tags: #configuration #manual #zettelstore syntax: zmk -modified: 20211109172143 +modified: 20210712234656 The configuration file, as specified by the ''-c CONFIGFILE'' [[command line option|00001004051000]], allows you to specify some startup options. These options cannot be stored in a [[configuration zettel|00001004020000]] because either they are needed before Zettelstore can start or because of security reasons. For example, Zettelstore need to know in advance, on which network address is must listen or where zettel are stored. An attacker that is able to change the owner can do anything. @@ -22,24 +22,17 @@ The administrator console will only be enabled if Zettelstore is started with the [[''run'' sub-command|00001004051000]]. On most operating systems, the value must be greater than ''1024'' unless you start Zettelstore with the full privileges of a system administrator (which is not recommended). Default: ''0'' -; [!box-uri-x]''box-uri-__X__'', where __X__ is a number greater or equal to one +; [!box-uri-x]''box-uri-//X//'', where //X// is a number greater or equal to one : Specifies a [[box|00001004011200]] where zettel are stored. - During startup __X__ is counted up, starting with one, until no key is found. + During startup //X// is counted up, starting with one, until no key is found. This allows to configure more than one box. If no ''box-uri-1'' key is given, the overall effect will be the same as if only ''box-uri-1'' was specified with the value ''dir://.zettel''. In this case, even a key ''box-uri-2'' will be ignored. -; [!debug-mode]''debug-mode'' -: Allows to debug the Zettelstore software (mostly used by the developers). - Disables any timeout values of the internal web server and does not send some security-related data. - - Do not enable it for a production server. - - Default: ''false'' ; [!default-dir-box-type]''default-dir-box-type'' : Specifies the default value for the (sub-) type of [[directory boxes|00001004011400#type]]. Zettel are typically stored in such boxes. Default: ''notify'' @@ -85,8 +78,8 @@ : Add the given string as a prefix to the local part of a Zettelstore local URL/URI when rendering zettel representations. Must begin and end with a slash character (""''/''"", ''U+002F''). Default: ''"/"''. This allows to use a forwarding proxy [[server|00001010090100]] in front of the Zettelstore. -; [!verbose-mode]''verbose-mode'' -: Be more verbose when logging data. +; ''verbose'' +: Be more verbose inf logging data. Default: false Index: docs/manual/00001004011200.zettel ================================================================== --- docs/manual/00001004011200.zettel +++ docs/manual/00001004011200.zettel @@ -1,40 +1,40 @@ id: 00001004011200 title: Zettelstore boxes role: manual tags: #configuration #manual #zettelstore syntax: zmk -modified: 20211103163225 +modified: 20210525121452 A Zettelstore must store its zettel somehow and somewhere. In most cases you want to store your zettel as files in a directory. Under certain circumstances you may want to store your zettel elsewhere. An example are the [[predefined zettel|00001005090000]] that come with a Zettelstore. They are stored within the software itself. In another situation you may want to store your zettel volatile, e.g. if you want to provide a sandbox for experimenting. -To cope with these (and more) situations, you configure Zettelstore to use one or more __boxes__. +To cope with these (and more) situations, you configure Zettelstore to use one or more //boxes//{-}. This is done via the ''box-uri-X'' keys of the [[startup configuration|00001004010000#box-uri-X]] (X is a number). Boxes are specified using special [[URIs|https://en.wikipedia.org/wiki/Uniform_Resource_Identifier]], somehow similar to web addresses. The following box URIs are supported: -; [!dir]''dir:\//DIR'' +; ''dir:\//DIR'' : Specifies a directory where zettel files are stored. ''DIR'' is the file path. Although it is possible to use relative file paths, such as ''./zettel'' (→ URI is ''dir:\//.zettel''), it is preferable to use absolute file paths, e.g. ''/home/user/zettel''. The directory must exist before starting the Zettelstore[^There is one exception: when Zettelstore is [[started without any parameter|00001004050000]], e.g. via double-clicking its icon, an directory called ''./zettel'' will be created.]. It is possible to [[configure|00001004011400]] a directory box. -; [!file]''file:FILE.zip'' oder ''file:/\//path/to/file.zip'' +; ''file:FILE.zip'' oder ''file:/\//path/to/file.zip'' : Specifies a ZIP file which contains files that store zettel. You can create such a ZIP file, if you zip a directory full of zettel files. This box is always read-only. -; [!mem]''mem:'' +; ''mem:'' : Stores all its zettel in volatile memory. If you stop the Zettelstore, all changes are lost. All boxes that you configure via the ''box-uri-X'' keys form a chain of boxes. If a zettel should be retrieved, a search starts in the box specified with the ''box-uri-2'' key, then ''box-uri-3'' and so on. Index: docs/manual/00001004051000.zettel ================================================================== --- docs/manual/00001004051000.zettel +++ docs/manual/00001004051000.zettel @@ -1,11 +1,11 @@ id: 00001004051000 title: The ''run'' sub-command role: manual tags: #command #configuration #manual #zettelstore syntax: zmk -modified: 20211109172046 +modified: 20210712234419 === ``zettelstore run`` This starts the web service. ``` @@ -41,8 +41,8 @@ : Puts the Zettelstore in read-only mode. No changes are possible via the web interface / via the API. This allows to publish your content without any risks of unauthorized changes. ; [!v]''-v'' -: Be more verbose when writing logs. +: Be more verbose in writing logs. Command line options take precedence over [[configuration file|00001004010000]] options. Index: docs/manual/00001004100000.zettel ================================================================== --- docs/manual/00001004100000.zettel +++ docs/manual/00001004100000.zettel @@ -1,19 +1,19 @@ id: 00001004100000 title: Zettelstore Administrator Console role: manual tags: #configuration #manual #zettelstore syntax: zmk -modified: 20211103162926 +modified: 20210510155859 The administrator console is a service accessible only on the same computer on which Zettelstore is running. It allows an experienced user to monitor and control some of the inner workings of Zettelstore. You enable the administrator console by specifying a TCP port number greater than zero (better: greater than 1024) for it, either via the [[command-line parameter ''-a''|00001004051000#a]] or via the ''admin-port'' key of the [[startup configuration file|00001004010000#admin-port]]. After you enable the administrator console, you can use tools such as [[PuTTY|https://www.chiark.greenend.org.uk/~sgtatham/putty/]] or other telnet software to connect to the administrator console. -In fact, the administrator console is __not__ a full telnet service. +In fact, the administrator console is //not// a full telnet service. It is merely a simple line-oriented service where each input line is interpreted separately. Therefore, you can also use tools like [[netcat|https://nc110.sourceforge.io/]], [[socat|http://www.dest-unreach.org/socat/]], etc. After connecting to the administrator console, there is no further authentication. It is not needed because you must be logged in on the same computer where Zettelstore is running. Index: docs/manual/00001004101000.zettel ================================================================== --- docs/manual/00001004101000.zettel +++ docs/manual/00001004101000.zettel @@ -1,11 +1,11 @@ id: 00001004101000 title: List of supported commands of the administrator console role: manual tags: #configuration #manual #zettelstore syntax: zmk -modified: 20211103161956 +modified: 20210525161623 ; ''bye'' : Closes the connection to the administrator console. ; ''config SERVICE'' : Displays all valid configuration keys for the given service. @@ -58,14 +58,14 @@ : Sets a single configuration value for the next configuration of a given service. It will become effective if the service is restarted. If the key specifies a list value, all other list values with a number greater than the given key are deleted. You can use the special number ""0"" to delete all values. - E.g. ``set-config box box-uri-0 any_text`` will remove all values of the list __box-uri-__. + E.g. ``set-config box box-uri-0 any_text`` will remove all values of the list //box-uri-//. ; ''shutdown'' : Terminate the Zettelstore itself (and closes the connection to the administrator console). ; ''start SERVICE'' : Start the given bservice and all dependent services. ; ''stat SERVICE'' : Display some statistical values for the given service. ; ''stop SERVICE'' : Stop the given service and all other that depend on this. Index: docs/manual/00001005000000.zettel ================================================================== --- docs/manual/00001005000000.zettel +++ docs/manual/00001005000000.zettel @@ -1,11 +1,11 @@ id: 00001005000000 title: Structure of Zettelstore role: manual tags: #design #manual #zettelstore syntax: zmk -modified: 20211103163918 +modified: 20210614165848 Zettelstore is a software that manages your zettel. Since every zettel must be readable without any special tool, most zettel has to be stored as ordinary files within specific directories. Typically, file names and file content must comply to specific rules so that Zettelstore can manage them. If you add, delete, or change zettel files with other tools, e.g. a text editor, Zettelstore will monitor these actions. @@ -30,11 +30,11 @@ If you create a new zettel via the web interface or the API, the zettel identifier will be the timestamp of the current date and time (format is ''YYYYMMDDhhmmss''). This allows zettel to be sorted naturally by creation time. Since the only restriction on zettel identifiers are the 14 digits, you are free to use other digit sequences. The [[configuration zettel|00001004020000]] is one prominent example, as well as these manual zettel. -You can create these special zettel identifiers either with the __rename__ function of Zettelstore or by manually renaming the underlying zettel files. +You can create these special zettel identifiers either with the //rename// function of Zettelstore or by manually renaming the underlying zettel files. It is allowed that the file name contains other characters after the 14 digits. These are ignored by Zettelstore. The file name must have an file extension. @@ -76,14 +76,14 @@ * [[List of predefined zettel|00001005090000]] === Boxes: other ways to store zettel As described above, a zettel may be stored as a file inside a directory or inside the Zettelstore software itself. -Zettelstore allows other ways to store zettel by providing an abstraction called __box__.[^Formerly, zettel were stored physically in boxes, often made of wood.] +Zettelstore allows other ways to store zettel by providing an abstraction called //box//.[^Formerly, zettel were stored physically in boxes, often made of wood.] A file directory which stores zettel is called a ""directory box"". But zettel may be also stored in a ZIP file, which is called ""file box"". -For testing purposes, zettel may be stored in volatile memory (called __RAM__). +For testing purposes, zettel may be stored in volatile memeory (called //RAM//). This way is called ""memory box"". Other types of boxes could be added to Zettelstore. What about a ""remote Zettelstore box""? Index: docs/manual/00001006010000.zettel ================================================================== --- docs/manual/00001006010000.zettel +++ docs/manual/00001006010000.zettel @@ -1,11 +1,10 @@ id: 00001006010000 title: Syntax of Metadata -role: manual tags: #manual #syntax #zettelstore syntax: zmk -modified: 20211103164152 +role: manual The metadata of a zettel is a collection of key-value pairs. The syntax roughly resembles the internal header of an email ([[RFC5322|https://tools.ietf.org/html/rfc5322]]). The key is a sequence of alphanumeric characters, a hyphen-minus character (""''-''"") is also allowed. @@ -15,11 +14,11 @@ * a colon character (""'':''""), * a non-empty sequence of space characters, * a sequence of space characters, followed by a colon, followed by a sequence of space characters. A Value is a sequence of printable characters. -If the value should be continued in the following line, that following line (""continuation line"") must begin with a non-empty sequence of space characters. +If the value should be continued in the following line, that following line (//continuation line//) must begin with a non-empty sequence of space characters. The rest of the following line will be interpreted as the next part of the value. There can be more than one continuation line for a value. A non-continuation line that contains a possibly empty sequence of characters, followed by the percent sign character (""''%''"") is treated as a comment line. It will be ignored. Index: docs/manual/00001006020000.zettel ================================================================== --- docs/manual/00001006020000.zettel +++ docs/manual/00001006020000.zettel @@ -1,11 +1,11 @@ id: 00001006020000 title: Supported Metadata Keys role: manual tags: #manual #meta #reference #zettel #zettelstore syntax: zmk -modified: 20211103163613 +modified: 20210822234119 Although you are free to define your own metadata, by using any key (according to the [[syntax|00001006010000]]), some keys have a special meaning that is enforced by Zettelstore. See the [[computed list of supported metadata keys|00000000000090]] for details. Most keys conform to a [[type|00001006030000]]. @@ -15,27 +15,21 @@ ; [!back]''back'' : Is a property that contains the identifier of all zettel that reference the zettel of this metadata, that are not referenced by this zettel. Basically, it is the value of [[''backward''|#bachward]], but without any zettel identifier that is contained in [[''forward''|#forward]]. ; [!backward]''backward'' : Is a property that contains the identifier of all zettel that reference the zettel of this metadata. - References within invertible values are not included here, e.g. [[''precursor''|#precursor]]. -; [!box-number]''box-number'' -: Is a computed value and contains the number of the box where the zettel was found. - For all but the [[predefined zettel|00001005090000]], this number is equal to the number __X__ specified in startup configuration key [[''box-uri-__X__''|00001004010000#box-uri-x]]. + References within inversable values are not included here, e.g. [[''precursor''|#precursor]]. ; [!copyright]''copyright'' : Defines a copyright string that will be encoded. If not given, the value ''default-copyright'' from the [[configuration zettel|00001004020000#default-copyright]] will be used. ; [!credential]''credential'' : Contains the hashed password, as it was emitted by [[``zettelstore password``|00001004051400]]. It is internally created by hashing the password, the [[zettel identifier|00001006050000]], and the value of the ''ident'' key. It is only used for zettel with a ''role'' value of ""user"". ; [!dead]''dead'' -: Property that contains all references that does __not__ identify a zettel. -; [!duplicates]''duplicates'' -: Is set to the value ""true"" if there is more than one file that could contain the content of a zettel. - Is used for [[directory boxes|00001004011400]] and [[file boxes|00001004011200#file]]. +: Property that contains all references that does //not// identify a zettel. ; [!folge]''folge'' : Is a property that contains identifier of all zettel that reference this zettel through the [[''precursor''|#precursor]] value. ; [!forward]''forward'' : Property that contains all references that identify another zettel within the content of the zettel. ; [!id]''id'' @@ -54,11 +48,14 @@ If you edit a zettel with an editor software outside Zettelstore, you should set it manually to an appropriate value. This is a computed value. There is no need to set it via Zettelstore. ; [!no-index]''no-index'' -: If set to a ""true"" value, the zettel will not be indexed and therefore not be found in full-text searches. +: If set to true, the zettel will not be indexed and therefore not be found in full-text searches. +; [!box-number]''box-number'' +: Is a computed value and contains the number of the box where the zettel was found. + For all but the [[predefined zettel|00001005090000]], this number is equal to the number //X// specified in startup configuration key [[''box-uri-//X//''|00001004010000#box-uri-x]]. ; [!precursor]''precursor'' : References zettel for which this zettel is a ""Folgezettel"" / follow-up zettel. Basically the inverse of key [[''folge''|#folge]]. ; [!published]''published'' : This property contains the timestamp of the mast modification / creation of the zettel. Index: docs/manual/00001006055000.zettel ================================================================== --- docs/manual/00001006055000.zettel +++ docs/manual/00001006055000.zettel @@ -1,11 +1,11 @@ id: 00001006055000 title: Reserved zettel identifier role: manual tags: #design #manual #zettelstore syntax: zmk -modified: 20211001125243 +modified: 20210917154229 [[Zettel identifier|00001006050000]] are typically created by examine the current date and time. By renaming a zettel, you are able to provide any sequence of 14 digits. If no other zettel has the same identifier, you are allowed to rename a zettel. @@ -38,6 +38,6 @@ This list may change in the future. ==== External Applications |= From | To | Description -| 00009000001000 | 00009000001999 | [[Zettel Presenter|https://zettelstore.de/contrib]], an application to display zettel as a HTML-based slideshow +| 00009000001000 | 00009000001999 | ZS Slides, an application to display zettel as a HTML-based slideshow Index: docs/manual/00001007000000.zettel ================================================================== --- docs/manual/00001007000000.zettel +++ docs/manual/00001007000000.zettel @@ -1,11 +1,10 @@ id: 00001007000000 title: Zettelmarkup -role: manual tags: #manual #zettelmarkup #zettelstore syntax: zmk -modified: 20211103162744 +role: manual Zettelmarkup is a rich plain-text based markup language for writing zettel content. Besides the zettel content, Zettelmarkup is also used for specifying the title of a zettel, regardless of the syntax of a zettel. Zettelmark supports the longevity of stored notes by providing a syntax that any person can easily read, as well as a computer. @@ -20,11 +19,11 @@ Additionally, it is allowed to embed other languages into HTML, such as CSS or even JavaScript. This could create problems with longevity as well as security problems. Zettelmarkup is a rich markup language, but it focusses on relatively short zettel content. It allows embedding other content, simple tables, quotations, description lists, and images. -It provides a broad range of inline formatting, including __emphasized__, **strong**, ;;small;;, ~~deleted~~{-} and >>inserted>> text. +It provides a broad range of inline formatting, including //emphasized//{-}, **strong**{-}, __underlined__, ;;small;;, ~~deleted~~{-} and __inserted__{-} text. Footnotes[^like this] are supported, links to other zettel and to external material, as well as citation keys. Zettelmarkup might be seen as a proprietary markup language. But if you want to use Markdown/CommonMark and you need support for footnotes, you'll end up with a proprietary extension. However, the Zettelstore supports CommonMark as a zettel syntax, so you can mix both Zettelmarkup zettel and CommonMark zettel in one store to get the best of both worlds. Index: docs/manual/00001007030100.zettel ================================================================== --- docs/manual/00001007030100.zettel +++ docs/manual/00001007030100.zettel @@ -1,16 +1,15 @@ id: 00001007030100 title: Zettelmarkup: Description Lists -role: manual tags: #manual #zettelmarkup #zettelstore syntax: zmk -modified: 20211103163447 +role: manual A description list is a sequence of terms to be described together with the descriptions of each term. Every term can described in multiple ways. -A description term (short: __term__) is specified with one semicolon (""'';''"", ''U+003B'') at the first position, followed by a space character and the described term, specified as a sequence of line elements. +A description term (short: //term//) is specified with one semicolon (""'';''"", ''U+003B'') at the first position, followed by a space character and the described term, specified as a sequence of line elements. If the following lines should also be part of the term, exactly two spaces must be given at the beginning of each following line. The description of a term is given with one colon (""'':''"", ''U+003A'') at the first position, followed by a space character and the description itself, specified as a sequence of inline elements. Similar to terms, following lines can also be part of the actual description, if they begin at each line with exactly two space characters. Index: docs/manual/00001007030200.zettel ================================================================== --- docs/manual/00001007030200.zettel +++ docs/manual/00001007030200.zettel @@ -1,16 +1,15 @@ id: 00001007030200 title: Zettelmarkup: Nested Lists -role: manual tags: #manual #zettelmarkup #zettelstore syntax: zmk -modified: 20211103162835 +role: manual There are thee kinds of lists that can be nested: ordered lists, unordered lists, and quotation lists. Ordered lists are specified with the number sign (""''#''"", ''U+0023''), unordered lists use the asterisk (""''*''"", ''U+002A''), and quotation lists are specified with the greater-than sing (""''>''"", ''U+003E''). -Let's call these three characters __list characters__. +Let's call these three characters //list characters//. Any nested list item is specified by a non-empty sequence of list characters, followed by a space character and a sequence of inline elements. In case of a quotation list as the last list character, the space character followed by a sequence of inline elements is optional. The number / count of list characters gives the nesting of the lists. If the following lines should also be part of the list item, exactly the same number of spaces must be given at the beginning of each of the following lines as it is the lists are nested, plus one additional space character. Index: docs/manual/00001007030300.zettel ================================================================== --- docs/manual/00001007030300.zettel +++ docs/manual/00001007030300.zettel @@ -1,11 +1,10 @@ id: 00001007030300 title: Zettelmarkup: Headings -role: manual tags: #manual #zettelmarkup #zettelstore syntax: zmk -modified: 20211103163105 +role: manual To specify a (sub-) section of a zettel, you should use the headings syntax: at the beginning of a new line type at least three equal signs (""''=''"", ''U+003D''), plus at least one space and enter the text of the heading as inline elements. @@ -31,12 +30,12 @@ The heading level is translated to a HTML heading by adding 1 to the level, e.g. ``=== Level 1 Heading``{=zmk} translates to ==

Level 1 Heading

=={=html}. The ==

=={=html} tag is rendered for the zettel title. This syntax is often used in a similar way in wiki implementation. -However, trailing equal signs are __not__ removed, they are part of the heading text. +However, trailing equal signs are //not//{-} removed, they are part of the heading text. If you use command line tools, you can easily create a draft table of contents with the command: ```sh grep -h '^====* ' ZETTEL_ID.zettel ``` Index: docs/manual/00001007031000.zettel ================================================================== --- docs/manual/00001007031000.zettel +++ docs/manual/00001007031000.zettel @@ -1,16 +1,16 @@ id: 00001007031000 title: Zettelmarkup: Tables role: manual tags: #manual #zettelmarkup #zettelstore syntax: zmk -modified: 20211103161859 +modified: 20210523185812 Tables are used to show some data in a two-dimenensional fashion. -In zettelmarkup, table are not specified explicitly, but by entering __table rows__. +In zettelmarkup, table are not specified explicitly, but by entering //table rows//. Therefore, a table can be seen as a sequence of table rows. -A table row is nothing as a sequence of __table cells__. +A table row is nothing as a sequence of //table cells//. The length of a table is the number of table rows, the width of a table is the maximum length of its rows. The first cell of a row must begin with the vertical bar character (""''|''"", ''U+007C'') at the first position of a line. The other cells of a row begin with the same vertical bar character at later positions in that line. A cell is delimited by the vertical bar character of the next cell or by the end of the current line. @@ -30,11 +30,11 @@ | b1 | b2 | b3 | c1 | c2 ::: === Header row -If any cell in the first row of a table contains an equal sing character (""''=''"", ''U+003D'') as the very first character, then this first row will be interpreted as a __table header__ row. +If any cell in the first row of a table contains an equal sing character (""''=''"", ''U+003D'') as the very first character, then this first row will be interpreted as a //table header// row. For example: ```zmk | a1 | a2 |= a3| | b1 | b2 | b3 Index: docs/manual/00001007040000.zettel ================================================================== --- docs/manual/00001007040000.zettel +++ docs/manual/00001007040000.zettel @@ -1,11 +1,11 @@ id: 00001007040000 title: Zettelmarkup: Inline-Structured Elements role: manual tags: #manual #zettelmarkup #zettelstore syntax: zmk -modified: 20211103162150 +modified: 20210822234205 Most characters you type is concerned with inline-structured elements. The content of a zettel contains is many cases just ordinary text, lightly formatted. Inline-structured elements allow to format your text and add some helpful links or images. Sometimes, you want to enter characters that have no representation on your keyboard. @@ -33,16 +33,16 @@ It ends at the end of the line where it begins. ==== Backslash The backslash character (""''\\''"", ''U+005C'') gives the next character another meaning. * If a space character follows, it is converted in a non-breaking space (''U+00A0''). -* If a line ending follows the backslash character, the line break is converted from a __soft break__ into a __hard break__. +* If a line ending follows the backslash character, the line break is converted from a //soft break// into a //hard break//. * Every other character is taken as itself, but without the interpretation of a Zettelmarkup element. For example, if you want to enter a ""'']''"" into a footnote text, you should escape it with a backslash. ==== Tag -Any text that begins with a number sign character (""''#''"", ''U+0023''), followed by a non-empty sequence of Unicode letters, Unicode digits, the hyphen-minus character (""''-''"", ''U+002D''), or the low line character (""''_''"", ''U+005F'') is interpreted as an __inline tag__. +Any text that begins with a number sign character (""''#''"", ''U+0023''), followed by a non-empty sequence of Unicode letters, Unicode digits, the hyphen-minus character (""''-''"", ''U+002D''), or the low line character (""''_''"", ''U+005F'') is interpreted as an //inline tag//. They are be considered equivalent to tags in metadata. ==== Entities & more Sometimes it is not easy to enter special characters. If you know the Unicode code point of that character, or its name according to the [[HTML standard|https://html.spec.whatwg.org/multipage/named-characters.html]], you can enter it by number or by name. @@ -57,12 +57,12 @@ You also can enter its numeric code point as a hex number, if you put the letter ""x"" after the numeric sign character. Example: ``&`` is rendered in HTML as ::&::{=example}. Since some Unicode character are used quite often, a special notation is introduced for them: -* Two consecutive hyphen-minus characters result in an __en-dash__ character. +* Two consecutive hyphen-minus characters result in an //en-dash// character. It is typically used in numeric ranges. ``pages 4--7`` will be rendered in HTML as: ::pages 4--7::{=example}. Alternative specifications are: ``–``, ``&x8211``, and ``–``. * Three consecutive full stop characters (""''.''"", ''U+002E'') after a space result in an horizontal ellipsis character. ``to be continued ... later`` will be rendered in HTML as: ::to be continued, ... later::{=example}. Alternative specifications are: ``…``, ``&x8230``, and ``…``. Index: docs/manual/00001007040100.zettel ================================================================== --- docs/manual/00001007040100.zettel +++ docs/manual/00001007040100.zettel @@ -1,11 +1,10 @@ id: 00001007040100 title: Zettelmarkup: Text Formatting -role: manual tags: #manual #zettelmarkup #zettelstore syntax: zmk -modified: 20211113175248 +role: manual Text formatting is the way to make your text visually different. Every text formatting element begins with two same characters. It ends when these two same characters occur the second time. It is possible that some [[attributes|00001007050000]] follow immediately, without any separating character. @@ -12,21 +11,22 @@ Text formatting can be nested, up to a reasonable limit. The following characters begin a text formatting: -* The low line character (""''_''"", ''U+005F'') emphasizes its text. -** Example: ``abc __def__ ghi`` is rendered in HTML as: ::abc __def__ ghi::{=example}. -** The slash character (""''/''"", ''U+002F'') is still allowed to emphasize text, but its use is deprecated. - With [[version 0.2.0|https://zettelstore.de/home/doc/trunk/www/changes.wiki#0_2_0]] it will no longer be supported. -*** Example: ``abc //def// ghi`` is rendered in HTML as: ::abc //def// ghi::{=example}. -* The asterisk character (""''*''"", ''U+002A'') strongly emphasized its enclosed text. +* The slash character (""''/''"", ''U+002F'') emphasizes its text. Often, such text is rendered in italics. If the default attribute is specified, the emphasized text is not just rendered as such, but also internally marked as emphasized. +** Example: ``abc //def// ghi`` is rendered in HTML as: ::abc //def// ghi::{=example}. +** Example: ``abc //def//{-} ghi`` is rendered in HTML as: ::abc //def//{-} ghi::{=example}. +* The asterisk character (""''*''"", ''U+002A'') strongly emphasized its enclosed text. The text is often rendered in bold. Again, the default attribute will force a explicit semantic meaning of strong emphasizing. ** Example: ``abc **def** ghi`` is rendered in HTML as: ::abc **def** ghi::{=example}. -* The greater-than sign character (""''>''"", ''U+003E'') marks text as inserted. -** Example: ``abc >>def>> ghi`` is rendered in HTML as: ::abc >>def>> ghi::{=example}. -* Similar, the tilde character (""''~''"", ''U+007E'') marks deleted text. +** Example: ``abc **def**{-} ghi`` is rendered in HTML as: ::abc **def**{-} ghi::{=example}. +* The low line character (""''_''"", ''U+005F'') produces underlined text. If the default attribute was specified, it is semantically rendered as inserted text. +** Example: ``abc __def__ ghi`` is rendered in HTML as: ::abc __def__ ghi::{=example}. +** Example: ``abc __def__{-} ghi`` is rendered in HTML as: ::abc __def__{-} ghi::{=example}. +* Similar, the tilde character (""''~''"", ''U+007E'') produces text that is strike-through. If the default attribute was specified, it is semantically rendered as deleted text. ** Example: ``abc ~~def~~ ghi`` is rendered in HTML as: ::abc ~~def~~ ghi::{=example}. +** Example: ``abc ~~def~~{-} ghi`` is rendered in HTML as: ::abc ~~def~~{-} ghi::{=example}. * The apostrophe character (""''\'''"", ''U+0027'') renders text in mono-space / fixed font width. ** Example: ``abc ''def'' ghi`` is rendered in HTML as: ::abc ''def'' ghi::{=example}. * The circumflex accent character (""''^''"", ''U+005E'') allows to enter superscripted text. ** Example: ``e=mc^^2^^`` is rendered in HTML as: ::e=mc^^2^^::{=example}. * The comma character (""'',''"", ''U+002C'') produces subscripted text. Index: docs/manual/00001007040322.zettel ================================================================== --- docs/manual/00001007040322.zettel +++ docs/manual/00001007040322.zettel @@ -1,19 +1,19 @@ id: 00001007040322 title: Zettelmarkup: Image Embedding role: manual tags: #manual #zettelmarkup #zettelstore syntax: zmk -modified: 20211103163657 +modified: 20210811165719 Image content is assumed, if an URL is used or if the referenced zettel contains an image. Supported formats are: * Portable Network Graphics (""PNG""), as defined by [[RFC\ 2083|https://tools.ietf.org/html/rfc2083]]. * Graphics Interchange Format (""GIF"), as defined by [[https://www.w3.org/Graphics/GIF/spec-gif89a.txt]]. -* JPEG / JPG, defined by the __Joint Photographic Experts Group__. +* JPEG / JPG, defined by the //Joint Photographic Experts Group//. * Scalable Vector Graphics (SVG), defined by [[https://www.w3.org/Graphics/SVG/]] If the text is given, it will be interpreted as an alternative textual representation, to help persons with some visual disabilities. [[Attributes|00001007050000]] are supported. Index: docs/manual/00001007040350.zettel ================================================================== --- docs/manual/00001007040350.zettel +++ docs/manual/00001007040350.zettel @@ -1,18 +1,17 @@ id: 00001007040350 title: Zettelmarkup: Mark role: manual tags: #manual #zettelmarkup #zettelstore syntax: zmk -modified: 20211103163338 A mark allows to name a point within a zettel. -This is useful if you want to reference some content in a bigger-sized zettel, currently with a [[link|00001007040310]] only[^Other uses of marks will be given, if Zettelmarkup is extended by a concept called __transclusion__.]. +This is useful if you want to reference some content in a bigger-sized zettel, currently with a [[link|00001007040310]] only[^Other uses of marks will be given, if Zettelmarkup is extended by a concept called //transclusion//.]. A mark begins with a left square bracket, followed by an exclamation mark character (""''!''"", ''U+0021''). Now the optional mark name follows. It is a (possibly empty) sequence of Unicode letters, Unicode digits, the hyphen-minus character (""''-''"", ''U+002D''), or the low-line character (""''_''"", ''U+005F''). The mark element ends with a right square bracket. Examples: * ``[!]`` is a mark without a name, the empty mark. * ``[!mark]`` is a mark with the name ""mark"". Index: docs/manual/00001007050000.zettel ================================================================== --- docs/manual/00001007050000.zettel +++ docs/manual/00001007050000.zettel @@ -1,11 +1,11 @@ id: 00001007050000 title: Zettelmarkup: Attributes role: manual tags: #manual #zettelmarkup #zettelstore syntax: zmk -modified: 20211103162617 +modified: 20210830161438 Attributes allows to modify the way how material is presented. Alternatively, they provide additional information to markup elements. To some degree, attributes are similar to [[HTML attributes|https://html.spec.whatwg.org/multipage/dom.html#global-attributes]]. @@ -22,20 +22,19 @@ If the value must contain a space or the right curly bracket, the value can be specified within two quotation marks (""''"''"", ''U+0022''). Within the quotation marks, the backslash character functions as an escape character to specify the quotation mark (and the backslash character too). Some examples: -* ``{key=value}`` sets the attribute __key__ to value __value__. +* ``{key=value}`` sets the attribute //key// to value //value//. * ``{key="value with space"}`` sets the attribute to the given value. * ``{key="value with quote \\" (and backslash \\\\)"}`` -* ``{name}`` sets the attribute __name__. - It has no corresponding value. - It is equivalent to ``{name=}``. -* ``{=key}`` sets the __generic attribute__ to the given value. - It is mostly used for modifying behaviour according to a programming language. -* ``{.key}`` sets the __class attribute__ to the given value. - It is equivalent to ``{class=key}``. +* ``{name}`` sets the attribute //name//. It has no corresponding value. It is + equivalent to ``{name=}``. +* ``{=key}`` sets the //generic attribute//{-} to the given value. It is mostly + used for modifying behaviour according to a programming language. +* ``{.key}`` sets the //class attribute//{-} to the given value. It is + equivalent to ``{class=key}``. In these examples, ``key`` must conform the the syntax of attribute keys, even if it is used as a value. If a key is given more than once in an attribute, the values are concatenated (and separated by a space). @@ -46,11 +45,11 @@ This is not true for the generic attribute. In ``{=key1 =key2}``, the first key is ignored. Therefore it is equivalent to ``{=key2}``. The key ""''-''"" (just hyphen-minus) is special. -It is called __default attribute__ and has a markup specific meaning. +It is called //default attribute//{-} and has a markup specific meaning. For example, when used for plain text, it replaces the non-visible space with a visible representation: * ++``Hello, world``{-}++ produces ==Hello, world=={-}. * ++``Hello, world``++ produces ==Hello, world==. Index: docs/manual/00001007060000.zettel ================================================================== --- docs/manual/00001007060000.zettel +++ docs/manual/00001007060000.zettel @@ -1,11 +1,11 @@ id: 00001007060000 title: Zettelmarkup: Summary of Formatting Characters role: manual tags: #manual #reference #zettelmarkup #zettelstore syntax: zmk -modified: 20211113180517 +modified: 20210728162830 The following table gives an overview about the use of all characters that begin a markup element. |= Character :|= Blocks <|= Inlines < | ''!'' | (free) | (free) @@ -15,28 +15,28 @@ | ''%'' | [[Comment block|00001007030900]] | [[Comment|00001007040000]] | ''&'' | (free) | [[Entity|00001007040000]] | ''\''' | (free) | [[Monospace text|00001007040100]] | ''('' | (free) | (free) | '')'' | (free) | (free) -| ''*'' | [[Unordered list|00001007030200]] | [[strongly emphasized text|00001007040100]] +| ''*'' | [[Unordered list|00001007030200]] | [[strongly emphasized / bold text|00001007040100]] | ''+'' | (free) | [[Keyboard input|00001007040200]] | '','' | (free) | [[Subscripted text|00001007040100]] | ''-'' | [[Horizonal rule|00001007030400]] | ""[[en-dash|00001007040000]]"" | ''.'' | (free) | [[Horizontal ellipsis|00001007040000]] -| ''/'' | (free) | [[//Emphasized text//|00001007040100]] (deprecated for v0.2.0) +| ''/'' | (free) | [[Emphasized / italics text|00001007040100]] | '':'' | [[Region block|00001007030800]] / [[description text|00001007030100]] | [[Inline region|00001007040100]] | '';'' | [[Description term|00001007030100]] | [[Small text|00001007040100]] | ''<'' | [[Quotation block|00001007030600]] | [[Short inline quote|00001007040100]] | ''='' | [[Headings|00001007030300]] | [[Computer output|00001007040200]] -| ''>'' | [[Quotation lists|00001007030200]] | [[Inserted text|00001007040100]] +| ''>'' | [[Quotation lists|00001007030200]] | (blocked: to remove anyambiguity with quotation lists) | ''?'' | (free) | (free) | ''@'' | (free) | (reserved) | ''['' | (reserved) | [[Linked material|00001007040300]], [[citation key|00001007040300]], [[footnote|00001007040300]], [[mark|00001007040300]] | ''\\'' | (blocked by inline meaning) | [[Escape character|00001007040000]] | '']'' | (reserved) | End of link, citation key, footnote, mark | ''^'' | (free) | [[Superscripted text|00001007040100]] -| ''_'' | (free) | [[Emphasized text|00001007040100]] +| ''_'' | (free) | [[Underlined text|00001007040100]] | ''`'' | [[Verbatim block|00001007030500]] | [[Literal text|00001007040200]] | ''{'' | (reserved) | [[Embedded material|00001007040300]], [[Attribute|00001007050000]] | ''|'' | [[Table row / table cell|00001007031000]] | Separator within link and embed formatting | ''}'' | (reserved) | End of embedded material, End of Attribute -| ''~'' | (free) | [[Deleted text|00001007040100]] +| ''~'' | (free) | [[Strike-through text|00001007040100]] Index: docs/manual/00001010070400.zettel ================================================================== --- docs/manual/00001010070400.zettel +++ docs/manual/00001010070400.zettel @@ -1,13 +1,12 @@ id: 00001010070400 title: Authorization and read-only mode -role: manual tags: #authorization #configuration #manual #security #zettelstore syntax: zmk -modified: 20211103164251 +role: manual -It is possible to enable both the read-only mode of the Zettelstore __and__ authentication/authorization. +It is possible to enable both the read-only mode of the Zettelstore //and// authentication/authorization. Both modes are independent from each other. This gives four use cases: ; Not read-only, no authorization : Zettelstore runs on your local computer and you only work with it. Index: docs/manual/00001010090100.zettel ================================================================== --- docs/manual/00001010090100.zettel +++ docs/manual/00001010090100.zettel @@ -1,11 +1,11 @@ id: 00001010090100 title: External server to encrypt message transport role: manual tags: #configuration #encryption #manual #security #zettelstore syntax: zmk -modified: 20211027125733 +modified: 20210511131719 Since Zettelstore does not encrypt the messages it exchanges with its clients, you may need some additional software to enable encryption. === Public-key encryption To enable encryption, you probably use some kind of encryption keys. @@ -65,6 +65,6 @@ ``` This will forwards requests with the prefix ""/manual/"" to the running Zettelstore. All other requests will be handled by Caddy itself. In this case you must specify the [[startup configuration key ''url-prefix''|00001004010000#url-prefix]] with the value ""/manual/"". -This is to allow Zettelstore to ignore the prefix while reading web requests and to give the correct URLs with the given prefix when sending a web response. +This is to allow Zettelstore ignore the prefix while reading web requests and to give the correct URLs with the given prefix when sending a web response. Index: docs/manual/00001012000000.zettel ================================================================== --- docs/manual/00001012000000.zettel +++ docs/manual/00001012000000.zettel @@ -1,11 +1,11 @@ id: 00001012000000 title: API role: manual tags: #api #manual #zettelstore syntax: zmk -modified: 20211004111103 +modified: 20210908220502 The API (short for ""**A**pplication **P**rogramming **I**nterface"") is the primary way to communicate with a running Zettelstore. Most integration with other systems and services is done through the API. The [[web user interface|00001014000000]] is just an alternative, secondary way of interacting with a Zettelstore. @@ -30,18 +30,14 @@ * [[List all tags|00001012052400]] * [[List all roles|00001012052600]] === Working with zettel * [[Create a new zettel|00001012053200]] -* [[Retrieve metadata and content of an existing zettel|00001012053300]] -* [[Retrieve metadata of an existing zettel|00001012053400]] +* [[Retrieve metadata and content of an existing zettel|00001012053400]] * [[Retrieve evaluated metadata and content of an existing zettel in various encodings|00001012053500]] * [[Retrieve parsed metadata and content of an existing zettel in various encodings|00001012053600]] * [[Retrieve references of an existing zettel|00001012053700]] * [[Retrieve context of an existing zettel|00001012053800]] * [[Retrieve zettel order within an existing zettel|00001012054000]] * [[Update metadata and content of a zettel|00001012054200]] * [[Rename a zettel|00001012054400]] * [[Delete a zettel|00001012054600]] - -=== Various helper method -* [[Encode Zettelmarkup inline material as HTML/Text|00001012070500]] Index: docs/manual/00001012051200.zettel ================================================================== --- docs/manual/00001012051200.zettel +++ docs/manual/00001012051200.zettel @@ -1,33 +1,28 @@ id: 00001012051200 title: API: List metadata of all zettel role: manual tags: #api #manual #zettelstore syntax: zmk -modified: 20211004124401 +modified: 20210905203616 To list the metadata of all zettel just send a HTTP GET request to the [[endpoint|00001012920000]] ''/j''[^If [[authentication is enabled|00001010040100]], you must include the a valid [[access token|00001012050200]] in the ''Authorization'' header]. If successful, the output is a JSON object: ```sh # curl http://127.0.0.1:23123/j -{"query":"","list":[{"id":"00001012051200","meta":{"title":"API: Renew an access token","tags":"#api #manual #zettelstore","syntax":"zmk","role":"manual"}},{"id":"00001012050600","meta":{"title":"API: Provide an access token","tags":"#api #manual #zettelstore","syntax":"zmk","role":"manual"}},{"id":"00001012050400","meta":{"title":"API: Renew an access token","tags":"#api #manual #zettelstore","syntax":"zmk","role":"manual"}},{"id":"00001012050200","meta":{"title":"API: Authenticate a client","tags":"#api #manual #zettelstore","syntax":"zmk","role":"manual"}},{"id":"00001012000000","meta":{"title":"API","tags":"#api #manual #zettelstore","syntax":"zmk","role":"manual"}}]} +{"list":[{"id":"00001012051200","meta":{"title":"API: Renew an access token","tags":"#api #manual #zettelstore","syntax":"zmk","role":"manual"}},{"id":"00001012050600","meta":{"title":"API: Provide an access token","tags":"#api #manual #zettelstore","syntax":"zmk","role":"manual"}},{"id":"00001012050400","meta":{"title":"API: Renew an access token","tags":"#api #manual #zettelstore","syntax":"zmk","role":"manual"}},{"id":"00001012050200","meta":{"title":"API: Authenticate a client","tags":"#api #manual #zettelstore","syntax":"zmk","role":"manual"}},{"id":"00001012000000","meta":{"title":"API","tags":"#api #manual #zettelstore","syntax":"zmk","role":"manual"}}]} ``` The JSON object contains a key ''"list"'' where its value is a list of zettel JSON objects. -These zettel JSON objects themselves contains the keys ''"id"'' (value is a string containing the zettel identifier), , and ''"meta"'' (value as a JSON object). +These zettel JSON objects themself contains the keys ''"id"'' (value is a string containing the zettel identifier), , and ''"meta"'' (value as a JSON object). The value of key ''"meta"'' effectively contains all metadata of the identified zettel, where metadata keys are encoded as JSON object keys and metadata values encoded as JSON strings. -Additionally, the JSON object contains a key ''"query"'' with a string value. -It will contain a textual description of the underlying query if you [[select only some zettel|00001012051810]]. -Without a selection, the value is the empty string. - If you reformat the JSON output from the ''GET /j'' call, you'll see its structure better: ```json { - "query": "", "list": [ { "id": "00001012051200", "meta": { "title": "API: List for all zettel some data", Index: docs/manual/00001012051800.zettel ================================================================== --- docs/manual/00001012051800.zettel +++ docs/manual/00001012051800.zettel @@ -1,15 +1,15 @@ id: 00001012051800 title: API: Shape the list of zettel metadata role: manual tags: #api #manual #zettelstore syntax: zmk -modified: 20211103162259 +modified: 20210905203719 -In most cases, it is not essential to list __all__ zettel. +In most cases, it is not essential to list //all// zettel. Typically, you are interested only in a subset of the zettel maintained by your Zettelstore. This is done by adding some query parameters to the general ''GET /j'' request. * [[Select|00001012051810]] just some zettel, based on metadata. * Only a specific amount of zettel will be selected by specifying [[a length and/or an offset|00001012051830]]. * [[Searching for specific content|00001012051840]], not just the metadata, is another way of selecting some zettel. * The resulting list can be [[sorted|00001012052000]] according to various criteria. Index: docs/manual/00001012051810.zettel ================================================================== --- docs/manual/00001012051810.zettel +++ docs/manual/00001012051810.zettel @@ -1,38 +1,38 @@ id: 00001012051810 title: API: Select zettel based on their metadata role: manual tags: #api #manual #zettelstore syntax: zmk -modified: 20211103164030 +modified: 20210905203929 -Every query parameter that does __not__ begin with the low line character (""_"", ''U+005F'') is treated as the name of a [[metadata|00001006010000]] key. +Every query parameter that does //not// begin with the low line character (""_"", ''U+005F'') is treated as the name of a [[metadata|00001006010000]] key. According to the [[type|00001006030000]] of a metadata key, zettel are possibly selected. All [[supported|00001006020000]] metadata keys have a well-defined type. User-defined keys have the type ''e'' (string, possibly empty). For example, if you want to retrieve all zettel that contain the string ""API"" in its title, your request will be: ```sh # curl 'http://127.0.0.1:23123/j?title=API' -{"query":"title MATCH API","list":[{"id":"00001012921000","meta":{"title":"API: JSON structure of an access token","tags":"#api #manual #reference #zettelstore","syntax":"zmk","role":"manual"}},{"id":"00001012920500","meta":{"title":"Formats available by the API","tags":"#api #manual #reference #zettelstore","syntax":"zmk","role":"manual"}},{"id":"00001012920000","meta":{"title":"Endpoints used by the API","tags":"#api #manual #reference #zettelstore","syntax":"zmk","role":"manual"}}, ... +{"list":[{"id":"00001012921000","meta":{"title":"API: JSON structure of an access token","tags":"#api #manual #reference #zettelstore","syntax":"zmk","role":"manual"}},{"id":"00001012920500","meta":{"title":"Formats available by the API","tags":"#api #manual #reference #zettelstore","syntax":"zmk","role":"manual"}},{"id":"00001012920000","meta":{"title":"Endpoints used by the API","tags":"#api #manual #reference #zettelstore","syntax":"zmk","role":"manual"}}, ... ``` -However, if you want all zettel that does not match a given value, you must prefix the value with the exclamation mark character (""!"", ''U+0021''). +However, if you want all zettel that does //not// match a given value, you must prefix the value with the exclamation mark character (""!"", ''U+0021''). For example, if you want to retrieve all zettel that do not contain the string ""API"" in their title, your request will be: ```sh # curl 'http://127.0.0.1:23123/j?title=!API' -{"query":"title NOT MATCH API","list":[{"id":"00010000000000","meta":{"back":"00001003000000 00001005090000","backward":"00001003000000 00001005090000","copyright":"(c) 2020-2021 by Detlef Stern ","forward":"00000000000001 00000000000003 00000000000096 00000000000100","lang":"en","license":"EUPL-1.2-or-later","role":"zettel","syntax":"zmk","title":"Home"}},{"id":"00001014000000","meta":{"back":"00001000000000 00001004020000 00001012920510","backward":"00001000000000 00001004020000 00001012000000 00001012920510","copyright":"(c) 2020-2021 by Detlef Stern ","forward":"00001012000000","lang":"en","license":"EUPL-1.2-or-later","published":"00001014000000","role":"manual","syntax":"zmk","tags":"#manual #webui #zettelstore","title":"Web user interface"}}, +{"list":[{"id":"00010000000000","meta":{"back":"00001003000000 00001005090000","backward":"00001003000000 00001005090000","copyright":"(c) 2020-2021 by Detlef Stern ","forward":"00000000000001 00000000000003 00000000000096 00000000000100","lang":"en","license":"EUPL-1.2-or-later","role":"zettel","syntax":"zmk","title":"Home"}},{"id":"00001014000000","meta":{"back":"00001000000000 00001004020000 00001012920510","backward":"00001000000000 00001004020000 00001012000000 00001012920510","copyright":"(c) 2020-2021 by Detlef Stern ","forward":"00001012000000","lang":"en","license":"EUPL-1.2-or-later","published":"00001014000000","role":"manual","syntax":"zmk","tags":"#manual #webui #zettelstore","title":"Web user interface"}}, ... ``` In both cases, an implicit precondition is that the zettel must contain the given metadata key. For a metadata key like [[''title''|00001006020000#title]], which has a default value, this precondition should always be true. But the situation is different for a key like [[''url''|00001006020000#url]]. Both ``curl 'http://localhost:23123/j?url='`` and ``curl 'http://localhost:23123/j?url=!'`` may result in an empty list. The empty query parameter values matches all zettel that contain the given metadata key. -Similar, if you specify just the exclamation mark character as a query parameter value, only those zettel match that does not contain the given metadata key. +Similar, if you specify just the exclamation mark character as a query parameter value, only those zettel match that does //not// contain the given metadata key. This is in contrast to above rule that the metadata value must exist before a match is done. For example ``curl 'http://localhost:23123/j?back=!&backward='`` returns all zettel that are reachable via other zettel, but also references these zettel. Above example shows that all sub-expressions of a select specification must be true so that no zettel is rejected from the final list. Index: docs/manual/00001012051830.zettel ================================================================== --- docs/manual/00001012051830.zettel +++ docs/manual/00001012051830.zettel @@ -1,17 +1,17 @@ id: 00001012051830 title: API: Shape the list of zettel metadata by limiting its length role: manual tags: #api #manual #zettelstore syntax: zmk -modified: 20211004124642 +modified: 20210905204006 === Limit By using the query parameter ""''_limit''"", which must have an integer value, you specifying an upper limit of list elements: ```sh # curl 'http://127.0.0.1:23123/j?title=API&_sort=id&_limit=2' -{"query":"title MATCH API LIMIT 2","list":[{"id":"00001012000000","meta":{"all-tags":"#api #manual #zettelstore","back":"00001000000000 00001004020000","backward":"00001000000000 00001004020000 00001012053200 00001012054000 00001014000000","box-number":"1","forward":"00001010040100 00001010040700 00001012050200 00001012050400 00001012050600 00001012051200 00001012051800 00001012051810 00001012051830 00001012051840 00001012052000 00001012052200 00001012052400 00001012052600 00001012053200 00001012053300 00001012053500 00001012053600 00001012053700 00001012053800 00001012054000 00001012054200 00001012054400 00001012054600 00001012920000 00001014000000","modified":"20210817160844","published":"20210817160844","role":"manual","syntax":"zmk","tags":"#api #manual #zettelstore","title":"API"}},{"id":"00001012050200","meta":{"all-tags":"#api #manual #zettelstore","back":"00001012000000 00001012050400 00001012050600 00001012051200 00001012053400 00001012053500 00001012053600","backward":"00001010040700 00001012000000 00001012050400 00001012050600 00001012051200 00001012053400 00001012053500 00001012053600 00001012920000 00001012921000","box-number":"1","forward":"00001004010000 00001010040200 00001010040700 00001012920000 00001012921000","modified":"20210726123709","published":"20210726123709","role":"manual","syntax":"zmk","tags":"#api #manual #zettelstore","title":"API: Authenticate a client"}}]} +{"list":[{"id":"00001012000000","meta":{"all-tags":"#api #manual #zettelstore","back":"00001000000000 00001004020000","backward":"00001000000000 00001004020000 00001012053200 00001012054000 00001014000000","box-number":"1","forward":"00001010040100 00001010040700 00001012050200 00001012050400 00001012050600 00001012051200 00001012051800 00001012051810 00001012051830 00001012051840 00001012052000 00001012052200 00001012052400 00001012052600 00001012053200 00001012053400 00001012053500 00001012053600 00001012053700 00001012053800 00001012054000 00001012054200 00001012054400 00001012054600 00001012920000 00001014000000","modified":"20210817160844","published":"20210817160844","role":"manual","syntax":"zmk","tags":"#api #manual #zettelstore","title":"API"}},{"id":"00001012050200","meta":{"all-tags":"#api #manual #zettelstore","back":"00001012000000 00001012050400 00001012050600 00001012051200 00001012053400 00001012053500 00001012053600","backward":"00001010040700 00001012000000 00001012050400 00001012050600 00001012051200 00001012053400 00001012053500 00001012053600 00001012920000 00001012921000","box-number":"1","forward":"00001004010000 00001010040200 00001010040700 00001012920000 00001012921000","modified":"20210726123709","published":"20210726123709","role":"manual","syntax":"zmk","tags":"#api #manual #zettelstore","title":"API: Authenticate a client"}}]} ``` ```sh # curl 'http://127.0.0.1:23123/z?title=API&_sort=id&_limit=2' 00001012000000 API @@ -20,13 +20,13 @@ === Offset The query parameter ""''_offset''"" allows to list not only the first elements, but to begin at a specific element: ```sh # curl 'http://127.0.0.1:23123/j?title=API&_sort=id&_limit=2&_offset=1' -{"query":"title MATCH API OFFSET 1 LIMIT 2","list":[{"id":"00001012050200","meta":{"all-tags":"#api #manual #zettelstore","back":"00001012000000 00001012050400 00001012050600 00001012051200 00001012053400 00001012053500 00001012053600","backward":"00001010040700 00001012000000 00001012050400 00001012050600 00001012051200 00001012053400 00001012053500 00001012053600 00001012920000 00001012921000","box-number":"1","forward":"00001004010000 00001010040200 00001010040700 00001012920000 00001012921000","modified":"20210726123709","published":"20210726123709","role":"manual","syntax":"zmk","tags":"#api #manual #zettelstore","title":"API: Authenticate a client"}},{"id":"00001012050400","meta":{"all-tags":"#api #manual #zettelstore","back":"00001010040700 00001012000000","backward":"00001010040700 00001012000000 00001012920000 00001012921000","box-number":"1","forward":"00001010040100 00001012050200 00001012920000 00001012921000","modified":"20210726123745","published":"20210726123745","role":"manual","syntax":"zmk","tags":"#api #manual #zettelstore","title":"API: Renew an access token"}}]} +{"list":[{"id":"00001012050200","meta":{"all-tags":"#api #manual #zettelstore","back":"00001012000000 00001012050400 00001012050600 00001012051200 00001012053400 00001012053500 00001012053600","backward":"00001010040700 00001012000000 00001012050400 00001012050600 00001012051200 00001012053400 00001012053500 00001012053600 00001012920000 00001012921000","box-number":"1","forward":"00001004010000 00001010040200 00001010040700 00001012920000 00001012921000","modified":"20210726123709","published":"20210726123709","role":"manual","syntax":"zmk","tags":"#api #manual #zettelstore","title":"API: Authenticate a client"}},{"id":"00001012050400","meta":{"all-tags":"#api #manual #zettelstore","back":"00001010040700 00001012000000","backward":"00001010040700 00001012000000 00001012920000 00001012921000","box-number":"1","forward":"00001010040100 00001012050200 00001012920000 00001012921000","modified":"20210726123745","published":"20210726123745","role":"manual","syntax":"zmk","tags":"#api #manual #zettelstore","title":"API: Renew an access token"}}]} ``` ```sh # curl 'http://127.0.0.1:23123/z?title=API&_sort=id&_limit=2&_offset=1' 00001012050200 API: Authenticate a client 00001012050400 API: Renew an access token ``` DELETED docs/manual/00001012053300.zettel Index: docs/manual/00001012053300.zettel ================================================================== --- docs/manual/00001012053300.zettel +++ docs/manual/00001012053300.zettel @@ -1,78 +0,0 @@ -id: 00001012053300 -title: Retrieve metadata and content of an existing zettel -role: manual -tags: #api #manual #zettelstore -syntax: zmk -modified: 20211004111804 - -The [[endpoint|00001012920000]] to work with metadata and content of a specific zettel is ''/j/{ID}'', where ''{ID}'' is a placeholder for the zettel identifier (14 digits). - -For example, to retrieve some data about this zettel you are currently viewing, just send a HTTP GET request to the endpoint ''/j/00001012053300''[^If [[authentication is enabled|00001010040100]], you must include the a valid [[access token|00001012050200]] in the ''Authorization'' header]. -If successful, the output is a JSON object: -```sh -# curl http://127.0.0.1:23123/j/00001012053300 -{"id":"00001012053300","meta":{"title":"API: Retrieve data for an exisiting zettel","tags":"#api #manual #zettelstore","syntax":"zmk","role":"manual","copyright":"(c) 2020 by Detlef Stern ","lang":"en","license":"CC BY-SA 4.0"},"content":"The endpoint to work with a specific zettel is ''/j/{ID}'', where ''{ID}'' is a placeholder for the zettel identifier (14 digits).\n\nFor example, ... -``` - -Pretty-printed, this results in: -``` -{ - "id": "00001012053300", - "meta": { - "back": "00001012000000 00001012053200 00001012054400", - "backward": "00001012000000 00001012053200 00001012054400 00001012920000", - "box-number": "1", - "forward": "00001010040100 00001012050200 00001012920000 00001012920800", - "modified": "20210726190012", - "published": "20210726190012", - "role": "manual", - "syntax": "zmk", - "tags": "#api #manual #zettelstore", - "title": "API: Retrieve metadata and content of an existing zettel" - }, - "encoding": "", - "content": "The [[endpoint|00001012920000]] to work with metadata and content of a specific zettel is ''/j/{ID}'', where ''{ID}'' (...) -} -``` - -[!plain]Additionally, you can retrieve the plain zettel, without using JSON. -Just change the [[endpoint|00001012920000]] to ''/z/{ID}'' -Optionally, you may provide which parts of the zettel you are requesting. -In this case, add an additional query parameter ''_part=[[PART|00001012920800]]''. -Valid values are ""zettel"", ""[[meta|00001012053400]]"", and ""content"" (the default value). - -````sh -# curl 'http://127.0.0.1:23123/z/00001012053300' -The [[endpoint|00001012920000]] to work with metadata and content of a specific zettel is ''/j/{ID}'', where ''{ID}'' is a placeholder for the zettel identifier (14 digits). - -For example, to retrieve some data about this zettel you are currently viewing, just send a HTTP GET request to the endpoint ''/j/00001012053300''[^If [[authentication is enabled|00001010040100]], you must include the a valid [[access token|00001012050200]] in the ''Authorization'' header]. -If successful, the output is a JSON object: -```sh -... -```` - -````sh -# curl 'http://127.0.0.1:23123/z/00001012053300?_part=zettel' -title: API: Retrieve metadata and content of an existing zettel -role: manual -tags: #api #manual #zettelstore -syntax: zmk - -The [[endpoint|00001012920000]] to work with metadata and content of a specific zettel is ''/j/{ID}'', where ''{ID}'' is a placeholder for the zettel identifier (14 digits). - -For example, to retrieve some data about this zettel you are currently viewing, just send a HTTP GET request to the endpoint -... -```` - -=== HTTP Status codes -; ''200'' -: Retrieval was successful, the body contains an appropriate JSON object. -; ''400'' -: Request was not valid. - There are several reasons for this. - Maybe the zettel identifier did not consist of exactly 14 digits. -; ''403'' -: You are not allowed to retrieve data of the given zettel. -; ''404'' -: Zettel not found. - You probably used a zettel identifier that is not used in the Zettelstore. Index: docs/manual/00001012053400.zettel ================================================================== --- docs/manual/00001012053400.zettel +++ docs/manual/00001012053400.zettel @@ -1,46 +1,61 @@ id: 00001012053400 -title: API: Retrieve metadata of an existing zettel +title: API: Retrieve metadata and content of an existing zettel role: manual tags: #api #manual #zettelstore syntax: zmk -modified: 20211004112257 +modified: 20210905204428 -The [[endpoint|00001012920000]] to work with metadata of a specific zettel is ''/m/{ID}'', where ''{ID}'' is a placeholder for the zettel identifier (14 digits). +The [[endpoint|00001012920000]] to work with metadata and content of a specific zettel is ''/j/{ID}'', where ''{ID}'' is a placeholder for the zettel identifier (14 digits). For example, to retrieve some data about this zettel you are currently viewing, just send a HTTP GET request to the endpoint ''/j/00001012053400''[^If [[authentication is enabled|00001010040100]], you must include the a valid [[access token|00001012050200]] in the ''Authorization'' header]. If successful, the output is a JSON object: ```sh -# curl http://127.0.0.1:23123/m/00001012053400 -{"meta":{"all-tags":"#api #manual #zettelstore","back":"00001012000000 00001012053300","backward":"00001012000000 00001012053300 00001012920000","box-number":"1","forward":"00001010040100 00001012050200 00001012920000 00001012920800","modified":"20211004111240","published":"20211004111240","role":"manual","syntax":"zmk","tags":"#api #manual #zettelstore","title":"API: Retrieve metadata of an existing zettel"}} +# curl http://127.0.0.1:23123/j/00001012053400 +{"id":"00001012053400","meta":{"title":"API: Retrieve data for an exisiting zettel","tags":"#api #manual #zettelstore","syntax":"zmk","role":"manual","copyright":"(c) 2020 by Detlef Stern ","lang":"en","license":"CC BY-SA 4.0"},"content":"The endpoint to work with a specific zettel is ''/j/{ID}'', where ''{ID}'' is a placeholder for the zettel identifier (14 digits).\n\nFor example, ... ``` Pretty-printed, this results in: ``` { + "id": "00001012053400", "meta": { - "all-tags": "#api #manual #zettelstore", - "back": "00001012000000 00001012053300", - "backward": "00001012000000 00001012053300 00001012920000", + "back": "00001012000000 00001012053200 00001012054400", + "backward": "00001012000000 00001012053200 00001012054400 00001012920000", "box-number": "1", "forward": "00001010040100 00001012050200 00001012920000 00001012920800", - "modified": "20211004111240", - "published": "20211004111240", + "modified": "20210726190012", + "published": "20210726190012", "role": "manual", "syntax": "zmk", "tags": "#api #manual #zettelstore", - "title": "API: Retrieve metadata of an existing zettel" - } + "title": "API: Retrieve metadata and content of an existing zettel" + }, + "encoding": "", + "content": "The [[endpoint|00001012920000]] to work with metadata and content of a specific zettel is ''/j/{ID}'', where ''{ID}'' (...) } ``` -[!plain]Additionally, you can retrieve the plain metadata of a zettel, without using JSON. -Just change the [[endpoint|00001012920000]] to ''/z/{ID}?_part=meta'' +[!plain]Additionally, you can retrieve the plain zettel, without using JSON. +Just change the [[endpoint|00001012920000]] to ''/z/{ID}'' +Optionally, you may provide which parts of the zettel you are requesting. +In this case, add an additional query parameter ''_part=[[PART|00001012920800]]''. +Valid values are ""zettel"", ""meta"", and ""content"" (the default value). + +````sh +# curl 'http://127.0.0.1:23123/z/00001012053400' +The [[endpoint|00001012920000]] to work with metadata and content of a specific zettel is ''/j/{ID}'', where ''{ID}'' is a placeholder for the zettel identifier (14 digits). + +For example, to retrieve some data about this zettel you are currently viewing, just send a HTTP GET request to the endpoint ''/j/00001012053400''[^If [[authentication is enabled|00001010040100]], you must include the a valid [[access token|00001012050200]] in the ''Authorization'' header]. +If successful, the output is a JSON object: +```sh +... +```` ````sh # curl 'http://127.0.0.1:23123/z/00001012053400?_part=meta' -title: API: Retrieve metadata of an existing zettel +title: API: Retrieve metadata and content of an existing zettel role: manual tags: #api #manual #zettelstore syntax: zmk ```` Index: docs/manual/00001012053500.zettel ================================================================== --- docs/manual/00001012053500.zettel +++ docs/manual/00001012053500.zettel @@ -1,11 +1,11 @@ id: 00001012053500 title: API: Retrieve evaluated metadata and content of an existing zettel in various encodings role: manual tags: #api #manual #zettelstore syntax: zmk -modified: 20211028191044 +modified: 20210826224328 The [[endpoint|00001012920000]] to work with evaluated metadata and content of a specific zettel is ''/v/{ID}'', where ''{ID}'' is a placeholder for the zettel identifier (14 digits). For example, to retrieve some evaluated data about this zettel you are currently viewing, just send a HTTP GET request to the endpoint ''/v/00001012053500''[^If [[authentication is enabled|00001010040100]], you must include the a valid [[access token|00001012050200]] in the ''Authorization'' header]. If successful, the output is a JSON object: @@ -33,11 +33,11 @@ -

The endpoint to work with evaluated metadata and content of a specific zettel is /v/{ID}, where {ID} is a placeholder for the zettel identifier (14 digits).

+

The endpoint to work with metadata and content of a specific zettel is /v/{ID}, where {ID} is a placeholder for the zettel identifier (14 digits).

... ``` You also can use the query parameter ''_part=[[PART|00001012920800]]'' to specify which parts of a zettel must be encoded. In this case, its default value is ''content''. Index: docs/manual/00001012053600.zettel ================================================================== --- docs/manual/00001012053600.zettel +++ docs/manual/00001012053600.zettel @@ -1,15 +1,15 @@ id: 00001012053600 title: API: Retrieve parsed metadata and content of an existing zettel in various encodings role: manual tags: #api #manual #zettelstore syntax: zmk -modified: 20211103164337 +modified: 20210830165056 The [[endpoint|00001012920000]] to work with parsed metadata and content of a specific zettel is ''/p/{ID}'', where ''{ID}'' is a placeholder for the zettel identifier (14 digits). -A __parsed__ zettel is basically an [[unevaluated|00001012053500]] zettel: the zettel is read and analyzed, but its content is not __evaluated__. +A //parsed// zettel is basically an [[unevaluated|00001012053500]] zettel: the zettel is read and analyzed, but its content is //not evaluated//. By using this endpoint, you are able to retrieve the structure of a zettel before it is evaluated. For example, to retrieve some data about this zettel you are currently viewing, just send a HTTP GET request to the endpoint ''/v/00001012053600''[^If [[authentication is enabled|00001010040100]], you must include the a valid [[access token|00001012050200]] in the ''Authorization'' header]. If successful, the output is a JSON object: ```sh Index: docs/manual/00001012053800.zettel ================================================================== --- docs/manual/00001012053800.zettel +++ docs/manual/00001012053800.zettel @@ -1,16 +1,17 @@ id: 00001012053800 title: API: Retrieve context of an existing zettel role: manual tags: #api #manual #zettelstore syntax: zmk -modified: 20211103163027 +modified: 20210825194347 The context of an origin zettel consists of those zettel that are somehow connected to the origin zettel. Direct connections of an origin zettel to other zettel are visible via [[metadata values|00001006020000]], such as ''backward'', ''forward'' or other values with type [[identifier|00001006032000]] or [[set of identifier|00001006032500]]. +Zettel are also connected by using same [[tags|00001006020000#tags]]. -The context is defined by a __direction__, a __depth__, and a __limit__: +The context is defined by a //direction//, a //depth//, and a //limit//: * Direction: connections are directed. For example, the metadata value of ''backward'' lists all zettel that link to the current zettel, while ''formward'' list all zettel to which the current zettel links. When you are only interested in one direction, set the parameter ''dir'' either to the value ""backward"" or ""forward"". All other values, including a missing value, is interpreted as ""both"". * Depth: a direct connection has depth 1, an indirect connection is the length of the shortest path between two zettel. @@ -19,13 +20,14 @@ A value of ""0"" does disable any depth check. * Limit: to set an upper bound for the returned context, you should use the parameter ''limit''. Its default value is ""200"". A value of ""0"" disables does not limit the number of elements returned. -The search for the context of a zettel stops at the [[home zettel|00001004020000#home-zettel]]. -This zettel is connected to all other zettel. -If it is included, the context would become too big and therefore unusable. +Zettel with same tags as the origin zettel are considered depth 1. +Only for the origin zettel, tags are used to calculate a connection. +Currently, only some of the newest zettel with a given tag are considered a connection.[^The number of zettel is given by the value of parameter ''depth''.] +Otherwise the context would become too big and therefore unusable. To retrieve the context of an existing zettel, use the [[endpoint|00001012920000]] ''/x/{ID}''[^Mnemonic: conte**X**t]. ```` # curl 'http://127.0.0.1:23123/x/00001012053800?limit=3&dir=forward&depth=2' Index: docs/manual/00001012054400.zettel ================================================================== --- docs/manual/00001012054400.zettel +++ docs/manual/00001012054400.zettel @@ -1,11 +1,11 @@ id: 00001012054400 title: API: Rename a zettel role: manual tags: #api #manual #zettelstore syntax: zmk -modified: 20211004111301 +modified: 20210905204715 Renaming a zettel is effectively just specifying a new identifier for the zettel. Since more than one [[box|00001004011200]] might contain a zettel with the old identifier, the rename operation must success in every relevant box to be overall successful. If the rename operation fails in one box, Zettelstore tries to rollback previous successful operations. @@ -19,11 +19,11 @@ ``` Only the last 14 characters of the value of ''Destination'' are taken into account and those must form an unused zettel identifier. If the value contains less than 14 characters that do not form an unused zettel identifier, the response will contain a HTTP status code ''400''. All other characters, besides those 14 digits, are effectively ignored. -However, the value should form a valid URL that could be used later to [[read the content|00001012053300]] of the freshly renamed zettel. +However, the value should form a valid URL that could be used later to [[read the content|00001012053400]] of the freshly renamed zettel. [!plain]Alternatively, you can also use the [[endpoint|00001012920000]] ''/z/{ID}''. Both endpoints behave identical. === HTTP Status codes DELETED docs/manual/00001012070500.zettel Index: docs/manual/00001012070500.zettel ================================================================== --- docs/manual/00001012070500.zettel +++ docs/manual/00001012070500.zettel @@ -1,56 +0,0 @@ -id: 00001012070500 -title: API: Encode Zettelmarkup inline material as HTML/Text -role: zettel -tags: #api #manual #zettelstore -syntax: zmk -modified: 20211111145202 - -To encode [[Zettelmarkup inline material|00001007040000]] send a HTTP POST request to the [[endpoint|00001012920000]] ''/v''. -The POST body must contain a JSON encoded list of Zettelmarkup inline material to be encoded: - -; ''first-zmk'' -: Contains the first Zettelmarkup encoded material. - This will be encoded as [[HTML|00001012920510]] and [[Text|00001012920519]]. -; ''other-zmk'' -: Contain more material. - The list can be empty. - These will be encoded in HTML only. -; ''lang'' -: Specifies the language for HTML encoding. - If empty, the default language of the Zettelstore instance will be used. -; ''no-links'' -: A boolean value, which specifies whether links should be encoded (``"no-links":false``) or should be not encoded (``"no-links":true``). - Default: ''false''. - -Typically, this call will be used to encode the [[title|00001006020000#title]] of a zettel. - -If successful, the call will return the following JSON document: - -; ''first-html'' -: HTML encoding of ''first-zmk'' -; ''first-text'' -: Text encoding of ''first-zmk'' -; ''other_html'' -: HTML encoding of the corresponding value in ''other-zmk''. - -Encoding takes place in the context of all other zettel in the Zettelstore. -For example, links and images are evaluated according to this context. - -A simple example: -```sh -# curl -X POST --data '{"first-zmk":"hallo [[00000000000001]]"}' http://127.0.0.1:23123/v -{"first-html":"hallo 00000000000001","first-text":"hallo ","other-html":null} - -# curl -X POST --data '{"first-zmk":"hallo [[00000000000001]]","no-links":true}' http://127.0.0.1:23123/v -{"first-html":"hallo 00000000000001","first-text":"hallo ","other-html":null} -``` - -=== HTTP Status codes -; ''200'' -: Operation was successful, the body contains a JSON object as described above. -; ''400'' -: Request was not valid. - There are several reasons for this. - Most likely, the JSON was not formed according to above rules. -; ''403'' -: You are not allowed to perform this operation. Index: docs/manual/00001012920000.zettel ================================================================== --- docs/manual/00001012920000.zettel +++ docs/manual/00001012920000.zettel @@ -1,11 +1,11 @@ id: 00001012920000 title: Endpoints used by the API role: manual tags: #api #manual #reference #zettelstore syntax: zmk -modified: 20211004111932 +modified: 20210908220438 All API endpoints conform to the pattern ''[PREFIX]LETTER[/ZETTEL-ID]'', where: ; ''PREFIX'' : is the URL prefix (default: ''/''), configured via the ''url-prefix'' [[startup configuration|00001004010000]], ; ''LETTER'' @@ -16,26 +16,25 @@ The following letters are currently in use: |= Letter:| Without zettel identifier | With [[zettel identifier|00001006050000]] | Mnemonic | ''a'' | POST: [[client authentication|00001012050200]] | | **A**uthenticate | | PUT: [[renew access token|00001012050400]] | -| ''j'' | GET: [[list zettel AS JSON|00001012051200]] | GET: [[retrieve zettel AS JSON|00001012053300]] | **J**SON +| ''j'' | GET: [[list zettel AS JSON|00001012051200]] | GET: [[retrieve zettel AS JSON|00001012053400]] | **J**SON | | POST: [[create new zettel|00001012053200]] | PUT: [[update a zettel|00001012054200]] | | | DELETE: [[delete the zettel|00001012054600]] | | | MOVE: [[rename the zettel|00001012054400]] | ''l'' | | GET: [[list references|00001012053700]] | **L**inks -| ''m'' | | GET: [[retrieve metadata|00001012053400]] | **M**etadata | ''o'' | | GET: [[list zettel order|00001012054000]] | **O**rder | ''p'' | | GET: [[retrieve parsed zettel|00001012053600]]| **P**arsed | ''r'' | GET: [[list roles|00001012052600]] | | **R**oles | ''t'' | GET: [[list tags|00001012052400]] || **T**ags -| ''v'' | POST: [[encode inlines|00001012070500]] | GET: [[retrieve evaluated zettel|00001012053500]] | E**v**aluated +| ''v'' | | GET: [[retrieve evaluated zettel|00001012053500]] | E**v**aluated | ''x'' | | GET: [[list zettel context|00001012053800]] | Conte**x**t -| ''z'' | GET: [[list zettel|00001012051200#plain]] | GET: [[retrieve zettel|00001012053300#plain]] | **Z**ettel +| ''z'' | GET: [[list zettel|00001012051200#plain]] | GET: [[retrieve zettel|00001012053400#plain]] | **Z**ettel | | POST: [[create new zettel|00001012053200#plain]] | PUT: [[update a zettel|00001012054200#plain]] | | | DELETE: [[delete zettel|00001012054600#plain]] | | | MOVE: [[rename zettel|00001012054400#plain]] The full URL will contain either the ''http'' oder ''https'' scheme, a host name, and an optional port number. The API examples will assume the ''http'' schema, the local host ''127.0.0.1'', the default port ''23123'', and the default empty ''PREFIX''. Therefore, all URLs in the API documentation will begin with ''http://127.0.0.1:23123''. DELETED docs/manual/00001018000000.zettel Index: docs/manual/00001018000000.zettel ================================================================== --- docs/manual/00001018000000.zettel +++ docs/manual/00001018000000.zettel @@ -1,17 +0,0 @@ -id: 00001018000000 -title: Troubleshooting -role: zettel -tags: #manual #zettelstore -syntax: zmk -modified: 20211027125603 - -This page lists some problems and their solutions that may occur when using your Zettelstore. - -=== Authentication -* **Problem:** [[Authentication is enabled|00001010040100]] for a local running Zettelstore and there is a valid [[user zettel|00001010040200]] for the owner. - But entering user name and password at the [[web user interface|00001014000000]] seems to be ignored, while entering a wrong password will result in an error message. -** **Explanation:** A local running Zettelstore typically means, that you are accessing the Zettelstore using an URL with schema ''http:/\/'', and not ''http**s**:/\/'', for example ''http:/\/localhost:23123''. - The difference between these two is the missing encryption of user name / password and for the answer of the Zettelstore if you use the ''http:/\/'' schema. - To be secure by default, the Zettelstore will not work in an insecure environment. -** **Solution 1:** If you are sure that your communication medium is safe, even if you use the ''http:/\/'' schema (for example, you are running the Zettelstore on the same computer you are working on, or if the Zettelstore is running on a computer in your protected local network), then you could add the entry ''insecure-cookie: true'' in you [[startup configuration|00001004010000#insecure-cookie]] file. -** **Solution 2:** If you are not sure about the security of your communication medium (for example, if unknown persons might use your local network), then you should run an [[external server|00001010090100]] in front of your Zettelstore to enable the use of the ''http**s**:/\/'' schema. Index: domain/content.go ================================================================== --- domain/content.go +++ domain/content.go @@ -10,56 +10,45 @@ // Package domain provides domain specific types, constants, and functions. package domain import ( - "bytes" "encoding/base64" "errors" "io" - "unicode" "unicode/utf8" + + "zettelstore.de/z/strfun" ) // Content is just the content of a zettel. type Content struct { - data []byte + data string isBinary bool } // NewContent creates a new content from a string. -func NewContent(data []byte) Content { - return Content{data: data, isBinary: calcIsBinary(data)} -} - -// Equal compares two content values. -func (zc *Content) Equal(o *Content) bool { - if zc == nil { - return o == nil - } - if zc.isBinary != o.isBinary { - return false - } - return bytes.Equal(zc.data, o.data) +func NewContent(s string) Content { + return Content{data: s, isBinary: calcIsBinary(s)} } // Set content to new string value. -func (zc *Content) Set(data []byte) { - zc.data = data - zc.isBinary = calcIsBinary(data) +func (zc *Content) Set(s string) { + zc.data = s + zc.isBinary = calcIsBinary(s) } // Write it to a Writer func (zc *Content) Write(w io.Writer) (int, error) { - return w.Write(zc.data) + return io.WriteString(w, zc.data) } // AsString returns the content itself is a string. -func (zc *Content) AsString() string { return string(zc.data) } +func (zc *Content) AsString() string { return zc.data } // AsBytes returns the content itself is a byte slice. -func (zc *Content) AsBytes() []byte { return zc.data } +func (zc *Content) AsBytes() []byte { return []byte(zc.data) } // IsBinary returns true if the content contains non-unicode values or is, // interpreted a text, with a high probability binary content. func (zc *Content) IsBinary() bool { return zc.isBinary } @@ -66,46 +55,46 @@ // TrimSpace remove some space character in content, if it is not binary content. func (zc *Content) TrimSpace() { if zc.isBinary { return } - zc.data = bytes.TrimRightFunc(zc.data, unicode.IsSpace) + zc.data = strfun.TrimSpaceRight(zc.data) } // Encode content for future transmission. func (zc *Content) Encode() (data, encoding string) { if !zc.isBinary { - return zc.AsString(), "" + return zc.data, "" } - return base64.StdEncoding.EncodeToString(zc.data), "base64" + return base64.StdEncoding.EncodeToString([]byte(zc.data)), "base64" } // SetDecoded content to the decoded value of the given string. func (zc *Content) SetDecoded(data, encoding string) error { switch encoding { case "": - zc.data = []byte(data) + zc.data = data case "base64": decoded, err := base64.StdEncoding.DecodeString(data) if err != nil { return err } - zc.data = decoded + zc.data = string(decoded) default: return errors.New("unknown encoding " + encoding) } zc.isBinary = calcIsBinary(zc.data) return nil } -func calcIsBinary(data []byte) bool { - if !utf8.Valid(data) { +func calcIsBinary(s string) bool { + if !utf8.ValidString(s) { return true } - l := len(data) + l := len(s) for i := 0; i < l; i++ { - if data[i] == 0 { + if s[i] == 0 { return true } } return false } Index: domain/content_test.go ================================================================== --- domain/content_test.go +++ domain/content_test.go @@ -26,12 +26,12 @@ {"äöü", false}, {"", false}, {string([]byte{0}), true}, } for i, tc := range td { - content := domain.NewContent([]byte(tc.s)) + content := domain.NewContent(tc.s) got := content.IsBinary() if got != tc.exp { t.Errorf("TC=%d: expected %v, got %v", i, tc.exp, got) } } } Index: domain/id/id.go ================================================================== --- domain/id/id.go +++ domain/id/id.go @@ -13,46 +13,66 @@ package id import ( "strconv" "time" - - "zettelstore.de/c/api" ) // Zid is the internal identifier of a zettel. Typically, it is a // time stamp of the form "YYYYMMDDHHmmSS" converted to an unsigned integer. // A zettelstore implementation should try to set the last two digits to zero, // e.g. the seconds should be zero, type Zid uint64 // Some important ZettelIDs. -const ( - Invalid = Zid(0) // Invalid is a Zid that will never be valid -) - -// ZettelIDs that are used as Zid more than once. // Note: if you change some values, ensure that you also change them in the // constant box. They are mentioned there literally, because these // constants are not available there. -var ( - ConfigurationZid = MustParse(api.ZidConfiguration) - BaseTemplateZid = MustParse(api.ZidBaseTemplate) - LoginTemplateZid = MustParse(api.ZidLoginTemplate) - ListTemplateZid = MustParse(api.ZidListTemplate) - ZettelTemplateZid = MustParse(api.ZidZettelTemplate) - InfoTemplateZid = MustParse(api.ZidInfoTemplate) - FormTemplateZid = MustParse(api.ZidFormTemplate) - RenameTemplateZid = MustParse(api.ZidRenameTemplate) - DeleteTemplateZid = MustParse(api.ZidDeleteTemplate) - ContextTemplateZid = MustParse(api.ZidContextTemplate) - RolesTemplateZid = MustParse(api.ZidRolesTemplate) - TagsTemplateZid = MustParse(api.ZidTagsTemplate) - ErrorTemplateZid = MustParse(api.ZidErrorTemplate) - EmojiZid = MustParse(api.ZidEmoji) - TOCNewTemplateZid = MustParse(api.ZidTOCNewTemplate) - DefaultHomeZid = MustParse(api.ZidDefaultHome) +const ( + Invalid = Zid(0) // Invalid is a Zid that will never be valid + + // System zettel + VersionZid = Zid(1) + HostZid = Zid(2) + OperatingSystemZid = Zid(3) + LicenseZid = Zid(4) + AuthorsZid = Zid(5) + DependenciesZid = Zid(6) + BoxManagerZid = Zid(20) + MetadataKeyZid = Zid(90) + StartupConfigurationZid = Zid(96) + ConfigurationZid = Zid(100) + + // WebUI HTML templates are in the range 10000..19999 + BaseTemplateZid = Zid(10100) + LoginTemplateZid = Zid(10200) + ListTemplateZid = Zid(10300) + ZettelTemplateZid = Zid(10401) + InfoTemplateZid = Zid(10402) + FormTemplateZid = Zid(10403) + RenameTemplateZid = Zid(10404) + DeleteTemplateZid = Zid(10405) + ContextTemplateZid = Zid(10406) + RolesTemplateZid = Zid(10500) + TagsTemplateZid = Zid(10600) + ErrorTemplateZid = Zid(10700) + + // WebUI CSS zettel are in the range 20000..29999 + BaseCSSZid = Zid(20001) + UserCSSZid = Zid(25001) + + // WebUI JS zettel are in the range 30000..39999 + + // WebUI image zettel are in the range 40000..49999 + EmojiZid = Zid(40001) + + // Range 90000...99999 is reserved for zettel templates + TOCNewTemplateZid = Zid(90000) + TemplateNewZettelZid = Zid(90001) + TemplateNewUserZid = Zid(90002) + + DefaultHomeZid = Zid(10000000000) ) const maxZid = 99999999999999 // ParseUint interprets a string as a possible zettel identifier @@ -79,20 +99,10 @@ return Invalid, err } return Zid(res), nil } -// MustParse tries to interpret a string as a zettel identifier and returns -// its value or panics otherwise. -func MustParse(s api.ZettelID) Zid { - zid, err := Parse(string(s)) - if err == nil { - return zid - } - panic(err) -} - const digits = "0123456789" // String converts the zettel identification to a string of 14 digits. // Only defined for valid ids. func (zid Zid) String() string { @@ -100,15 +110,14 @@ } // Bytes converts the zettel identification to a byte slice of 14 digits. // Only defined for valid ids. func (zid Zid) Bytes() []byte { - n := uint64(zid) result := make([]byte, 14) for i := 13; i >= 0; i-- { - result[i] = digits[n%10] - n /= 10 + result[i] = digits[zid%10] + zid /= 10 } return result } // IsValid determines if zettel id is a valid one, e.g. consists of max. 14 digits. Index: domain/id/id_test.go ================================================================== --- domain/id/id_test.go +++ domain/id/id_test.go @@ -45,11 +45,11 @@ t.Errorf("i=%d: sid=%q is not valid, but should be. err=%v", i, sid, err) } s := zid.String() if s != sid { t.Errorf( - "i=%d: zid=%v does not format to %q, but to %q", i, zid, sid, s) + "i=%d: zid=%v does not format to %q, but to %q", i, sid, zid, s) } } invalidIDs := []string{ "", "0", "a", Index: domain/id/slice.go ================================================================== --- domain/id/slice.go +++ domain/id/slice.go @@ -11,12 +11,12 @@ // Package id provides domain specific types, constants, and functions about // zettel identifier. package id import ( - "bytes" "sort" + "strings" ) // Slice is a sequence of zettel identifier. A special case is a sorted slice. type Slice []Zid @@ -56,14 +56,14 @@ func (zs Slice) String() string { if len(zs) == 0 { return "" } - var buf bytes.Buffer + var sb strings.Builder for i, zid := range zs { if i > 0 { - buf.WriteByte(' ') + sb.WriteByte(' ') } - buf.WriteString(zid.String()) + sb.WriteString(zid.String()) } - return buf.String() + return sb.String() } Index: domain/meta/meta.go ================================================================== --- domain/meta/meta.go +++ domain/meta/meta.go @@ -10,18 +10,14 @@ // Package meta provides the domain specific type 'meta'. package meta import ( - "bytes" "regexp" "sort" "strings" - "unicode" - "unicode/utf8" - "zettelstore.de/c/api" "zettelstore.de/z/domain/id" "zettelstore.de/z/input" ) type keyUsage int @@ -47,11 +43,11 @@ // IsProperty returns true, if metadata is a computed property. func (kd *DescriptionKey) IsProperty() bool { return kd.usage >= usageProperty } var registeredKeys = make(map[string]*DescriptionKey) -func registerKey(name string, t *DescriptionType, usage keyUsage, inverse string) { +func registerKey(name string, t *DescriptionType, usage keyUsage, inverse string) string { if _, ok := registeredKeys[name]; ok { panic("Key '" + name + "' already defined") } if inverse != "" { if t != TypeID && t != TypeIDSet { @@ -67,10 +63,11 @@ if inv.Type != TypeIDSet { panic("Inverse Key '" + inverse + "' is not an identifier set, but " + inv.Type.String()) } } registeredKeys[name] = &DescriptionKey{name, t, usage, inverse} + return name } // IsComputed returns true, if key denotes a computed metadata key. func IsComputed(name string) bool { if kd, ok := registeredKeys[name]; ok { @@ -108,60 +105,79 @@ } return result } // Supported keys. -func init() { - registerKey(api.KeyID, TypeID, usageComputed, "") - registerKey(api.KeyTitle, TypeZettelmarkup, usageUser, "") - registerKey(api.KeyRole, TypeWord, usageUser, "") - registerKey(api.KeyTags, TypeTagSet, usageUser, "") - registerKey(api.KeySyntax, TypeWord, usageUser, "") - registerKey(api.KeyAllTags, TypeTagSet, usageProperty, "") - registerKey(api.KeyBack, TypeIDSet, usageProperty, "") - registerKey(api.KeyBackward, TypeIDSet, usageProperty, "") - registerKey(api.KeyBoxNumber, TypeNumber, usageComputed, "") - registerKey(api.KeyContentTags, TypeTagSet, usageProperty, "") - registerKey(api.KeyCopyright, TypeString, usageUser, "") - registerKey(api.KeyCredential, TypeCredential, usageUser, "") - registerKey(api.KeyDead, TypeIDSet, usageProperty, "") - registerKey(api.KeyDuplicates, TypeBool, usageProperty, "") - registerKey(api.KeyFolge, TypeIDSet, usageProperty, "") - registerKey(api.KeyForward, TypeIDSet, usageProperty, "") - registerKey(api.KeyLang, TypeWord, usageUser, "") - registerKey(api.KeyLicense, TypeEmpty, usageUser, "") - registerKey(api.KeyModified, TypeTimestamp, usageComputed, "") - registerKey(api.KeyNoIndex, TypeBool, usageUser, "") - registerKey(api.KeyPrecursor, TypeIDSet, usageUser, api.KeyFolge) - registerKey(api.KeyPublished, TypeTimestamp, usageProperty, "") - registerKey(api.KeyReadOnly, TypeWord, usageUser, "") - registerKey(api.KeyURL, TypeURL, usageUser, "") - registerKey(api.KeyUserID, TypeWord, usageUser, "") - registerKey(api.KeyUserRole, TypeWord, usageUser, "") - registerKey(api.KeyVisibility, TypeWord, usageUser, "") - - // Keys for runtime configuration zettel - // See: https://zettelstore.de/manual/h/00001004020000 - registerKey(api.KeyDefaultCopyright, TypeString, usageUser, "") - registerKey(api.KeyDefaultLang, TypeWord, usageUser, "") - registerKey(api.KeyDefaultLicense, TypeEmpty, usageUser, "") - registerKey(api.KeyDefaultRole, TypeWord, usageUser, "") - registerKey(api.KeyDefaultSyntax, TypeWord, usageUser, "") - registerKey(api.KeyDefaultTitle, TypeZettelmarkup, usageUser, "") - registerKey(api.KeyDefaultVisibility, TypeWord, usageUser, "") - registerKey(api.KeyExpertMode, TypeBool, usageUser, "") - registerKey(api.KeyFooterHTML, TypeString, usageUser, "") - registerKey(api.KeyHomeZettel, TypeID, usageUser, "") - registerKey(api.KeyMarkerExternal, TypeEmpty, usageUser, "") - registerKey(api.KeyMaxTransclusions, TypeNumber, usageUser, "") - registerKey(api.KeySiteName, TypeString, usageUser, "") - registerKey(api.KeyYAMLHeader, TypeBool, usageUser, "") - registerKey(api.KeyZettelFileSyntax, TypeWordSet, usageUser, "") -} +var ( + KeyID = registerKey("id", TypeID, usageComputed, "") + KeyTitle = registerKey("title", TypeZettelmarkup, usageUser, "") + KeyRole = registerKey("role", TypeWord, usageUser, "") + KeyTags = registerKey("tags", TypeTagSet, usageUser, "") + KeySyntax = registerKey("syntax", TypeWord, usageUser, "") + KeyAllTags = registerKey("all-"+KeyTags, TypeTagSet, usageProperty, "") + KeyBack = registerKey("back", TypeIDSet, usageProperty, "") + KeyBackward = registerKey("backward", TypeIDSet, usageProperty, "") + KeyBoxNumber = registerKey("box-number", TypeNumber, usageComputed, "") + KeyCopyright = registerKey("copyright", TypeString, usageUser, "") + KeyCredential = registerKey("credential", TypeCredential, usageUser, "") + KeyDead = registerKey("dead", TypeIDSet, usageProperty, "") + KeyDefaultCopyright = registerKey("default-copyright", TypeString, usageUser, "") + KeyDefaultLang = registerKey("default-lang", TypeWord, usageUser, "") + KeyDefaultLicense = registerKey("default-license", TypeEmpty, usageUser, "") + KeyDefaultRole = registerKey("default-role", TypeWord, usageUser, "") + KeyDefaultSyntax = registerKey("default-syntax", TypeWord, usageUser, "") + KeyDefaultTitle = registerKey("default-title", TypeZettelmarkup, usageUser, "") + KeyDefaultVisibility = registerKey("default-visibility", TypeWord, usageUser, "") + KeyDuplicates = registerKey("duplicates", TypeBool, usageUser, "") + KeyExpertMode = registerKey("expert-mode", TypeBool, usageUser, "") + KeyFolge = registerKey("folge", TypeIDSet, usageProperty, "") + KeyFooterHTML = registerKey("footer-html", TypeString, usageUser, "") + KeyForward = registerKey("forward", TypeIDSet, usageProperty, "") + KeyHomeZettel = registerKey("home-zettel", TypeID, usageUser, "") + KeyLang = registerKey("lang", TypeWord, usageUser, "") + KeyLicense = registerKey("license", TypeEmpty, usageUser, "") + KeyMarkerExternal = registerKey("marker-external", TypeEmpty, usageUser, "") + KeyMaxTransclusions = registerKey("max-transclusions", TypeNumber, usageUser, "") + KeyModified = registerKey("modified", TypeTimestamp, usageComputed, "") + KeyNoIndex = registerKey("no-index", TypeBool, usageUser, "") + KeyPrecursor = registerKey("precursor", TypeIDSet, usageUser, KeyFolge) + KeyPublished = registerKey("published", TypeTimestamp, usageProperty, "") + KeyReadOnly = registerKey("read-only", TypeWord, usageUser, "") + KeySiteName = registerKey("site-name", TypeString, usageUser, "") + KeyURL = registerKey("url", TypeURL, usageUser, "") + KeyUserID = registerKey("user-id", TypeWord, usageUser, "") + KeyUserRole = registerKey("user-role", TypeWord, usageUser, "") + KeyVisibility = registerKey("visibility", TypeWord, usageUser, "") + KeyYAMLHeader = registerKey("yaml-header", TypeBool, usageUser, "") + KeyZettelFileSyntax = registerKey("zettel-file-syntax", TypeWordSet, usageUser, "") +) // NewPrefix is the prefix for metadata key in template zettel for creating new zettel. const NewPrefix = "new-" + +// Important values for some keys. +const ( + ValueRoleConfiguration = "configuration" + ValueRoleUser = "user" + ValueRoleZettel = "zettel" + ValueSyntaxNone = "none" + ValueSyntaxGif = "gif" + ValueSyntaxText = "text" + ValueSyntaxZmk = "zmk" + ValueTrue = "true" + ValueFalse = "false" + ValueLangEN = "en" + ValueUserRoleCreator = "creator" + ValueUserRoleReader = "reader" + ValueUserRoleWriter = "writer" + ValueUserRoleOwner = "owner" + ValueVisibilityCreator = "creator" + ValueVisibilityExpert = "expert" + ValueVisibilityOwner = "owner" + ValueVisibilityLogin = "login" + ValueVisibilityPublic = "public" +) // Meta contains all meta-data of a zettel. type Meta struct { Zid id.Zid pairs map[string]string @@ -211,11 +227,11 @@ type Pair struct { Key string Value string } -var firstKeys = []string{api.KeyTitle, api.KeyRole, api.KeyTags, api.KeySyntax} +var firstKeys = []string{KeyTitle, KeyRole, KeyTags, KeySyntax} var firstKeySet map[string]bool func init() { firstKeySet = make(map[string]bool, len(firstKeys)) for _, k := range firstKeys { @@ -223,11 +239,11 @@ } } // Set stores the given string value under the given key. func (m *Meta) Set(key, value string) { - if key != api.KeyID { + if key != KeyID { m.pairs[key] = trimValue(value) } } func trimValue(value string) string { @@ -235,11 +251,11 @@ } // Get retrieves the string value of a given key. The bool value signals, // whether there was a value stored or not. func (m *Meta) Get(key string) (string, bool) { - if key == api.KeyID { + if key == KeyID { return m.Zid.String(), true } value, ok := m.pairs[key] return value, ok } @@ -290,11 +306,11 @@ return result } // Delete removes a key from the data. func (m *Meta) Delete(key string) { - if key != api.KeyID { + if key != KeyID { delete(m.pairs, key) } } // Equal compares to metas for equality. @@ -326,40 +342,5 @@ return false } } return true } - -// Sanitize all metadata keys and values, so that they can be written safely into a file. -func (m *Meta) Sanitize() { - if m == nil { - return - } - for k, v := range m.pairs { - m.pairs[RemoveNonGraphic(k)] = RemoveNonGraphic(v) - } -} - -// RemoveNonGraphic changes the given string not to include non-graphical characters. -// It is needed to sanitize meta data. -func RemoveNonGraphic(s string) string { - if s == "" { - return "" - } - pos := 0 - var buf bytes.Buffer - for pos < len(s) { - nextPos := strings.IndexFunc(s[pos:], func(r rune) bool { return !unicode.IsGraphic(r) }) - if nextPos < 0 { - break - } - buf.WriteString(s[pos:nextPos]) - buf.WriteByte(' ') - _, size := utf8.DecodeRuneInString(s[nextPos:]) - pos = nextPos + size - } - if pos == 0 { - return strings.TrimSpace(s) - } - buf.WriteString(s[pos:]) - return strings.TrimSpace(buf.String()) -} Index: domain/meta/meta_test.go ================================================================== --- domain/meta/meta_test.go +++ domain/meta/meta_test.go @@ -13,11 +13,10 @@ import ( "strings" "testing" - "zettelstore.de/c/api" "zettelstore.de/z/domain/id" ) const testID = id.Zid(98765432101234) @@ -38,33 +37,33 @@ } func TestTitleHeader(t *testing.T) { t.Parallel() m := New(testID) - if got, ok := m.Get(api.KeyTitle); ok || got != "" { + if got, ok := m.Get(KeyTitle); ok || got != "" { t.Errorf("Title is not empty, but %q", got) } - addToMeta(m, api.KeyTitle, " ") - if got, ok := m.Get(api.KeyTitle); ok || got != "" { + addToMeta(m, KeyTitle, " ") + if got, ok := m.Get(KeyTitle); ok || got != "" { t.Errorf("Title is not empty, but %q", got) } const st = "A simple text" - addToMeta(m, api.KeyTitle, " "+st+" ") - if got, ok := m.Get(api.KeyTitle); !ok || got != st { + addToMeta(m, KeyTitle, " "+st+" ") + if got, ok := m.Get(KeyTitle); !ok || got != st { t.Errorf("Title is not %q, but %q", st, got) } - addToMeta(m, api.KeyTitle, " "+st+"\t") + addToMeta(m, KeyTitle, " "+st+"\t") const exp = st + " " + st - if got, ok := m.Get(api.KeyTitle); !ok || got != exp { + if got, ok := m.Get(KeyTitle); !ok || got != exp { t.Errorf("Title is not %q, but %q", exp, got) } m = New(testID) const at = "A Title" - addToMeta(m, api.KeyTitle, at) - addToMeta(m, api.KeyTitle, " ") - if got, ok := m.Get(api.KeyTitle); !ok || got != at { + addToMeta(m, KeyTitle, at) + addToMeta(m, KeyTitle, " ") + if got, ok := m.Get(KeyTitle); !ok || got != at { t.Errorf("Title is not %q, but %q", at, got) } } func checkSet(t *testing.T, exp []string, m *Meta, key string) { @@ -85,42 +84,42 @@ } func TestTagsHeader(t *testing.T) { t.Parallel() m := New(testID) - checkSet(t, []string{}, m, api.KeyTags) - - addToMeta(m, api.KeyTags, "") - checkSet(t, []string{}, m, api.KeyTags) - - addToMeta(m, api.KeyTags, " #t1 #t2 #t3 #t4 ") - checkSet(t, []string{"#t1", "#t2", "#t3", "#t4"}, m, api.KeyTags) - - addToMeta(m, api.KeyTags, "#t5") - checkSet(t, []string{"#t1", "#t2", "#t3", "#t4", "#t5"}, m, api.KeyTags) - - addToMeta(m, api.KeyTags, "t6") - checkSet(t, []string{"#t1", "#t2", "#t3", "#t4", "#t5"}, m, api.KeyTags) + checkSet(t, []string{}, m, KeyTags) + + addToMeta(m, KeyTags, "") + checkSet(t, []string{}, m, KeyTags) + + addToMeta(m, KeyTags, " #t1 #t2 #t3 #t4 ") + checkSet(t, []string{"#t1", "#t2", "#t3", "#t4"}, m, KeyTags) + + addToMeta(m, KeyTags, "#t5") + checkSet(t, []string{"#t1", "#t2", "#t3", "#t4", "#t5"}, m, KeyTags) + + addToMeta(m, KeyTags, "t6") + checkSet(t, []string{"#t1", "#t2", "#t3", "#t4", "#t5"}, m, KeyTags) } func TestSyntax(t *testing.T) { t.Parallel() m := New(testID) - if got, ok := m.Get(api.KeySyntax); ok || got != "" { + if got, ok := m.Get(KeySyntax); ok || got != "" { t.Errorf("Syntax is not %q, but %q", "", got) } - addToMeta(m, api.KeySyntax, " ") - if got, _ := m.Get(api.KeySyntax); got != "" { + addToMeta(m, KeySyntax, " ") + if got, _ := m.Get(KeySyntax); got != "" { t.Errorf("Syntax is not %q, but %q", "", got) } - addToMeta(m, api.KeySyntax, "MarkDown") + addToMeta(m, KeySyntax, "MarkDown") const exp = "markdown" - if got, ok := m.Get(api.KeySyntax); !ok || got != exp { + if got, ok := m.Get(KeySyntax); !ok || got != exp { t.Errorf("Syntax is not %q, but %q", exp, got) } - addToMeta(m, api.KeySyntax, " ") - if got, _ := m.Get(api.KeySyntax); got != "" { + addToMeta(m, KeySyntax, " ") + if got, _ := m.Get(KeySyntax); got != "" { t.Errorf("Syntax is not %q, but %q", "", got) } } func checkHeader(t *testing.T, exp map[string]string, gotP []Pair) { @@ -189,14 +188,14 @@ }{ {nil, nil, true, true}, {nil, nil, false, true}, {[]string{"a", "a"}, nil, false, false}, {[]string{"a", "a"}, nil, true, false}, - {[]string{api.KeyFolge, "0"}, nil, true, false}, - {[]string{api.KeyFolge, "0"}, nil, false, true}, - {[]string{api.KeyFolge, "0"}, []string{api.KeyFolge, "0"}, true, true}, - {[]string{api.KeyFolge, "0"}, []string{api.KeyFolge, "0"}, false, true}, + {[]string{KeyFolge, "0"}, nil, true, false}, + {[]string{KeyFolge, "0"}, nil, false, true}, + {[]string{KeyFolge, "0"}, []string{KeyFolge, "0"}, true, true}, + {[]string{KeyFolge, "0"}, []string{KeyFolge, "0"}, false, true}, } for i, tc := range testcases { m1 := pairs2meta(tc.pairs1) m2 := pairs2meta(tc.pairs2) got := m1.Equal(m2, tc.allowComputed) @@ -235,28 +234,5 @@ for i := 0; i < len(pairs); i = i + 2 { m.Set(pairs[i], pairs[i+1]) } return m } - -func TestRemoveNonGraphic(t *testing.T) { - testCases := []struct { - inp string - exp string - }{ - {"", ""}, - {" ", ""}, - {"a", "a"}, - {"a ", "a"}, - {"a b", "a b"}, - {"\n", ""}, - {"a\n", "a"}, - {"a\nb", "a b"}, - {"a\tb", "a b"}, - } - for i, tc := range testCases { - got := RemoveNonGraphic(tc.inp) - if tc.exp != got { - t.Errorf("%q/%d: expected %q, but got %q", tc.inp, i, tc.exp, got) - } - } -} Index: domain/meta/parse.go ================================================================== --- domain/meta/parse.go +++ domain/meta/parse.go @@ -13,11 +13,10 @@ import ( "sort" "strings" - "zettelstore.de/c/api" "zettelstore.de/z/domain/id" "zettelstore.de/z/input" ) // NewFromInput parses the meta data of a zettel. @@ -63,23 +62,23 @@ key := inp.Src[pos:inp.Pos] skipSpace(inp) if inp.Ch == ':' { inp.Next() } - var val []byte + var val string for { skipSpace(inp) pos = inp.Pos skipToEOL(inp) - val = append(val, inp.Src[pos:inp.Pos]...) + val += inp.Src[pos:inp.Pos] inp.EatEOL() if !input.IsSpace(inp.Ch) { break } - val = append(val, ' ') + val += " " } - addToMeta(m, string(key), string(val)) + addToMeta(m, key, val) } func skipSpace(inp *input.Input) { for input.IsSpace(inp.Ch) { inp.Next() @@ -150,11 +149,11 @@ key = strings.ToLower(key) if !KeyIsValid(key) { return } switch key { - case "", api.KeyID: + case "", KeyID: // Empty key and 'id' key will be ignored return } switch Type(key) { Index: domain/meta/parse_test.go ================================================================== --- domain/meta/parse_test.go +++ domain/meta/parse_test.go @@ -12,55 +12,54 @@ package meta_test import ( "testing" - "zettelstore.de/c/api" "zettelstore.de/z/domain/meta" "zettelstore.de/z/input" ) func parseMetaStr(src string) *meta.Meta { - return meta.NewFromInput(testID, input.NewInput([]byte(src))) + return meta.NewFromInput(testID, input.NewInput(src)) } func TestEmpty(t *testing.T) { t.Parallel() m := parseMetaStr("") - if got, ok := m.Get(api.KeySyntax); ok || got != "" { + if got, ok := m.Get(meta.KeySyntax); ok || got != "" { t.Errorf("Syntax is not %q, but %q", "", got) } - if got, ok := m.GetList(api.KeyTags); ok || len(got) > 0 { + if got, ok := m.GetList(meta.KeyTags); ok || len(got) > 0 { t.Errorf("Tags are not nil, but %v", got) } } func TestTitle(t *testing.T) { t.Parallel() td := []struct{ s, e string }{ - {api.KeyTitle + ": a title", "a title"}, - {api.KeyTitle + ": a\n\t title", "a title"}, - {api.KeyTitle + ": a\n\t title\r\n x", "a title x"}, - {api.KeyTitle + " AbC", "AbC"}, - {api.KeyTitle + " AbC\n ded", "AbC ded"}, - {api.KeyTitle + ": o\ntitle: p", "o p"}, - {api.KeyTitle + ": O\n\ntitle: P", "O"}, - {api.KeyTitle + ": b\r\ntitle: c", "b c"}, - {api.KeyTitle + ": B\r\n\r\ntitle: C", "B"}, - {api.KeyTitle + ": r\rtitle: q", "r q"}, - {api.KeyTitle + ": R\r\rtitle: Q", "R"}, + {meta.KeyTitle + ": a title", "a title"}, + {meta.KeyTitle + ": a\n\t title", "a title"}, + {meta.KeyTitle + ": a\n\t title\r\n x", "a title x"}, + {meta.KeyTitle + " AbC", "AbC"}, + {meta.KeyTitle + " AbC\n ded", "AbC ded"}, + {meta.KeyTitle + ": o\ntitle: p", "o p"}, + {meta.KeyTitle + ": O\n\ntitle: P", "O"}, + {meta.KeyTitle + ": b\r\ntitle: c", "b c"}, + {meta.KeyTitle + ": B\r\n\r\ntitle: C", "B"}, + {meta.KeyTitle + ": r\rtitle: q", "r q"}, + {meta.KeyTitle + ": R\r\rtitle: Q", "R"}, } for i, tc := range td { m := parseMetaStr(tc.s) - if got, ok := m.Get(api.KeyTitle); !ok || got != tc.e { + if got, ok := m.Get(meta.KeyTitle); !ok || got != tc.e { t.Log(m) t.Errorf("TC=%d: expected %q, got %q", i, tc.e, got) } } - m := parseMetaStr(api.KeyTitle + ": ") - if title, ok := m.Get(api.KeyTitle); ok { + m := parseMetaStr(meta.KeyTitle + ": ") + if title, ok := m.Get(meta.KeyTitle); ok { t.Errorf("Expected a missing title key, but got %q (meta=%v)", title, m) } } func TestNewFromInput(t *testing.T) { @@ -86,11 +85,11 @@ t.Errorf("TC=%d: expected=%v, got=%v", i, tc.exp, got) } } // Test, whether input position is correct. - inp := input.NewInput([]byte("---\na:b\n---\nX")) + inp := input.NewInput("---\na:b\n---\nX") m := meta.NewFromInput(testID, inp) exp := []meta.Pair{{"a", "b"}} if got := m.Pairs(true); !equalPairs(exp, got) { t.Errorf("Expected=%v, got=%v", exp, got) } @@ -125,11 +124,11 @@ {"12345678901234 123", "12345678901234"}, {"01234567890123 123 12345678901234", "01234567890123 12345678901234"}, {"12345678901234 01234567890123", "01234567890123 12345678901234"}, } for i, tc := range testdata { - m := parseMetaStr(api.KeyPrecursor + ": " + tc.inp) - if got, ok := m.Get(api.KeyPrecursor); (!ok && tc.exp != "") || tc.exp != got { + m := parseMetaStr(meta.KeyPrecursor + ": " + tc.inp) + if got, ok := m.Get(meta.KeyPrecursor); (!ok && tc.exp != "") || tc.exp != got { t.Errorf("TC=%d: expected %q, but got %q when parsing %q", i, tc.exp, got, tc.inp) } } } Index: domain/meta/type.go ================================================================== --- domain/meta/type.go +++ domain/meta/type.go @@ -14,12 +14,10 @@ import ( "strconv" "strings" "sync" "time" - - "zettelstore.de/c/api" ) // DescriptionType is a description of a specific key type. type DescriptionType struct { Name string @@ -97,11 +95,11 @@ return TypeEmpty } // SetList stores the given string list value under the given key. func (m *Meta) SetList(key string, values []string) { - if key != api.KeyID { + if key != KeyID { for i, val := range values { values[i] = trimValue(val) } m.pairs[key] = strings.Join(values, " ") } Index: domain/meta/type_test.go ================================================================== --- domain/meta/type_test.go +++ domain/meta/type_test.go @@ -32,11 +32,11 @@ t.Errorf("Value is not 14 digits long: %q", val) } if _, err := strconv.ParseInt(val, 10, 64); err != nil { t.Errorf("Unable to parse %q as an int64: %v", val, err) } - if _, ok = m.GetTime("key"); !ok { + if _, ok := m.GetTime("key"); !ok { t.Errorf("Unable to get time from value %q", val) } } func TestGetTime(t *testing.T) { Index: domain/meta/values.go ================================================================== --- domain/meta/values.go +++ domain/meta/values.go @@ -9,12 +9,10 @@ //----------------------------------------------------------------------------- // Package meta provides the domain specific type 'meta'. package meta -import "zettelstore.de/c/api" - // Visibility enumerates the variations of the 'visibility' meta key. type Visibility int // Supported values for visibility. const ( @@ -26,15 +24,15 @@ VisibilityOwner VisibilityExpert ) var visMap = map[string]Visibility{ - api.ValueVisibilityPublic: VisibilityPublic, - api.ValueVisibilityCreator: VisibilityCreator, - api.ValueVisibilityLogin: VisibilityLogin, - api.ValueVisibilityOwner: VisibilityOwner, - api.ValueVisibilityExpert: VisibilityExpert, + ValueVisibilityPublic: VisibilityPublic, + ValueVisibilityCreator: VisibilityCreator, + ValueVisibilityLogin: VisibilityLogin, + ValueVisibilityOwner: VisibilityOwner, + ValueVisibilityExpert: VisibilityExpert, } // GetVisibility returns the visibility value of the given string func GetVisibility(val string) Visibility { if vis, ok := visMap[val]; ok { @@ -55,18 +53,18 @@ UserRoleWriter UserRoleOwner ) var urMap = map[string]UserRole{ - api.ValueUserRoleCreator: UserRoleCreator, - api.ValueUserRoleReader: UserRoleReader, - api.ValueUserRoleWriter: UserRoleWriter, - api.ValueUserRoleOwner: UserRoleOwner, + ValueUserRoleCreator: UserRoleCreator, + ValueUserRoleReader: UserRoleReader, + ValueUserRoleWriter: UserRoleWriter, + ValueUserRoleOwner: UserRoleOwner, } // GetUserRole role returns the user role of the given string. func GetUserRole(val string) UserRole { if ur, ok := urMap[val]; ok { return ur } return UserRoleUnknown } Index: domain/meta/write_test.go ================================================================== --- domain/meta/write_test.go +++ domain/meta/write_test.go @@ -10,39 +10,37 @@ // Package meta_test provides tests for the domain specific type 'meta'. package meta_test import ( - "bytes" "strings" "testing" - "zettelstore.de/c/api" "zettelstore.de/z/domain/id" "zettelstore.de/z/domain/meta" ) const testID = id.Zid(98765432101234) func newMeta(title string, tags []string, syntax string) *meta.Meta { m := meta.New(testID) if title != "" { - m.Set(api.KeyTitle, title) + m.Set(meta.KeyTitle, title) } if tags != nil { - m.Set(api.KeyTags, strings.Join(tags, " ")) + m.Set(meta.KeyTags, strings.Join(tags, " ")) } if syntax != "" { - m.Set(api.KeySyntax, syntax) + m.Set(meta.KeySyntax, syntax) } return m } func assertWriteMeta(t *testing.T, m *meta.Meta, expected string) { t.Helper() - var buf bytes.Buffer - m.Write(&buf, true) - if got := buf.String(); got != expected { + sb := strings.Builder{} + m.Write(&sb, true) + if got := sb.String(); got != expected { t.Errorf("\nExp: %q\ngot: %q", expected, got) } } func TestWriteMeta(t *testing.T) { Index: domain/zettel.go ================================================================== --- domain/zettel.go +++ domain/zettel.go @@ -21,7 +21,7 @@ Content Content // The content of the zettel itself. } // Equal compares two zettel for equality. func (z Zettel) Equal(o Zettel, allowComputed bool) bool { - return z.Meta.Equal(o.Meta, allowComputed) && z.Content.Equal(&o.Content) + return z.Meta.Equal(o.Meta, allowComputed) && z.Content == o.Content } Index: encoder/djsonenc/djsonenc.go ================================================================== --- encoder/djsonenc/djsonenc.go +++ encoder/djsonenc/djsonenc.go @@ -15,11 +15,11 @@ "fmt" "io" "sort" "strconv" - "zettelstore.de/c/api" + "zettelstore.de/z/api" "zettelstore.de/z/ast" "zettelstore.de/z/domain/meta" "zettelstore.de/z/encoder" "zettelstore.de/z/strfun" ) @@ -384,22 +384,25 @@ v.b.WriteByte('"') } } var mapFormatKind = map[ast.FormatKind]string{ - ast.FormatEmphDeprecated: "EmphD", - ast.FormatEmph: "Emph", - ast.FormatStrong: "Strong", - ast.FormatMonospace: "Mono", - ast.FormatDelete: "Delete", - ast.FormatInsert: "Insert", - ast.FormatSuper: "Super", - ast.FormatSub: "Sub", - ast.FormatQuote: "Quote", - ast.FormatQuotation: "Quotation", - ast.FormatSmall: "Small", - ast.FormatSpan: "Span", + ast.FormatItalic: "Italic", + ast.FormatEmph: "Emph", + ast.FormatBold: "Bold", + ast.FormatStrong: "Strong", + ast.FormatMonospace: "Mono", + ast.FormatStrike: "Strikethrough", + ast.FormatDelete: "Delete", + ast.FormatUnder: "Underline", + ast.FormatInsert: "Insert", + ast.FormatSuper: "Super", + ast.FormatSub: "Sub", + ast.FormatQuote: "Quote", + ast.FormatQuotation: "Quotation", + ast.FormatSmall: "Small", + ast.FormatSpan: "Span", } var mapLiteralKind = map[ast.LiteralKind]string{ ast.LiteralProg: "Code", ast.LiteralKeyb: "Input", Index: encoder/encoder.go ================================================================== --- encoder/encoder.go +++ encoder/encoder.go @@ -15,11 +15,11 @@ import ( "errors" "fmt" "io" - "zettelstore.de/c/api" + "zettelstore.de/z/api" "zettelstore.de/z/ast" "zettelstore.de/z/domain/meta" ) // Encoder is an interface that allows to encode different parts of a zettel. DELETED encoder/encoder_blob_test.go Index: encoder/encoder_blob_test.go ================================================================== --- encoder/encoder_blob_test.go +++ encoder/encoder_blob_test.go @@ -1,59 +0,0 @@ -//----------------------------------------------------------------------------- -// Copyright (c) 2021 Detlef Stern -// -// This file is part of zettelstore. -// -// Zettelstore is licensed under the latest version of the EUPL (European Union -// Public License). Please see file LICENSE.txt for your rights and obligations -// under this license. -//----------------------------------------------------------------------------- - -package encoder_test - -import ( - "testing" - - "zettelstore.de/c/api" - "zettelstore.de/z/domain/id" - "zettelstore.de/z/domain/meta" - "zettelstore.de/z/input" - "zettelstore.de/z/parser" - - _ "zettelstore.de/z/parser/blob" // Allow to use BLOB parser. -) - -type blobTestCase struct { - descr string - blob []byte - expect expectMap -} - -var pngTestCases = []blobTestCase{ - { - descr: "Minimal PNG", - blob: []byte{ - 0x89, 0x50, 0x4e, 0x47, 0x0d, 0x0a, 0x1a, 0x0a, 0x00, 0x00, 0x00, 0x0d, 0x49, 0x48, 0x44, 0x52, - 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x01, 0x08, 0x00, 0x00, 0x00, 0x00, 0x3a, 0x7e, 0x9b, - 0x55, 0x00, 0x00, 0x00, 0x0a, 0x49, 0x44, 0x41, 0x54, 0x78, 0x9c, 0x63, 0x62, 0x00, 0x00, 0x00, - 0x06, 0x00, 0x03, 0x36, 0x37, 0x7c, 0xa8, 0x00, 0x00, 0x00, 0x00, 0x49, 0x45, 0x4e, 0x44, 0xae, - 0x42, 0x60, 0x82, - }, - expect: expectMap{ - encoderDJSON: `[{"t":"Blob","q":"PNG","s":"png","o":"iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAAAAAA6fptVAAAACklEQVR4nGNiAAAABgADNjd8qAAAAABJRU5ErkJggg=="}]`, - encoderHTML: ``, - encoderNative: `[BLOB "PNG" "png" "iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAAAAAA6fptVAAAACklEQVR4nGNiAAAABgADNjd8qAAAAABJRU5ErkJggg=="]`, - encoderText: "", - encoderZmk: `%% Unable to display BLOB with title 'PNG' and syntax 'png'.`, - }, - }, -} - -func TestBlob(t *testing.T) { - m := meta.New(id.Invalid) - m.Set(api.KeyTitle, "PNG") - for testNum, tc := range pngTestCases { - inp := input.NewInput(tc.blob) - pe := &peBlocks{bln: parser.ParseBlocks(inp, m, "png")} - checkEncodings(t, testNum, pe, tc.descr, tc.expect, "???") - } -} DELETED encoder/encoder_block_test.go Index: encoder/encoder_block_test.go ================================================================== --- encoder/encoder_block_test.go +++ encoder/encoder_block_test.go @@ -1,306 +0,0 @@ -//----------------------------------------------------------------------------- -// Copyright (c) 2021 Detlef Stern -// -// This file is part of zettelstore. -// -// Zettelstore is licensed under the latest version of the EUPL (European Union -// Public License). Please see file LICENSE.txt for your rights and obligations -// under this license. -//----------------------------------------------------------------------------- - -package encoder_test - -var tcsBlock = []zmkTestCase{ - { - descr: "Empty Zettelmarkup should produce near nothing", - zmk: "", - expect: expectMap{ - encoderDJSON: `[]`, - encoderHTML: "", - encoderNative: ``, - encoderText: "", - encoderZmk: useZmk, - }, - }, - { - descr: "Simple text: Hello, world", - zmk: "Hello, world", - expect: expectMap{ - encoderDJSON: `[{"t":"Para","i":[{"t":"Text","s":"Hello,"},{"t":"Space"},{"t":"Text","s":"world"}]}]`, - encoderHTML: "

Hello, world

", - encoderNative: `[Para Text "Hello,",Space,Text "world"]`, - encoderText: "Hello, world", - encoderZmk: useZmk, - }, - }, - { - descr: "Simple block comment", - zmk: "%%%\nNo\nrender\n%%%", - expect: expectMap{ - encoderDJSON: `[{"t":"CommentBlock","l":["No","render"]}]`, - encoderHTML: ``, - encoderNative: `[CommentBlock "No\nrender"]`, - encoderText: ``, - encoderZmk: useZmk, - }, - }, - { - descr: "Rendered block comment", - zmk: "%%%{-}\nRender\n%%%", - expect: expectMap{ - encoderDJSON: `[{"t":"CommentBlock","a":{"-":""},"l":["Render"]}]`, - encoderHTML: "", - encoderNative: `[CommentBlock ("",[-]) "Render"]`, - encoderText: ``, - encoderZmk: useZmk, - }, - }, - { - descr: "Simple Heading", - zmk: `=== Top`, - expect: expectMap{ - encoderDJSON: `[{"t":"Heading","n":1,"s":"top","i":[{"t":"Text","s":"Top"}]}]`, - encoderHTML: "

Top

", - encoderNative: `[Heading 1 #top Text "Top"]`, - encoderText: `Top`, - encoderZmk: useZmk, - }, - }, - { - descr: "Einfache Liste", - zmk: "* A\n* B\n* C", - expect: expectMap{ - encoderDJSON: `[{"t":"BulletList","c":[[{"t":"Para","i":[{"t":"Text","s":"A"}]}],[{"t":"Para","i":[{"t":"Text","s":"B"}]}],[{"t":"Para","i":[{"t":"Text","s":"C"}]}]]}]`, - encoderHTML: "
    \n
  • A
  • \n
  • B
  • \n
  • C
  • \n
", - encoderNative: `[BulletList - [[Para Text "A"]], - [[Para Text "B"]], - [[Para Text "C"]]]`, - encoderText: "A\nB\nC", - encoderZmk: useZmk, - }, - }, - { - descr: "Schachtelliste", - zmk: "* T1\n** T2\n* T3\n** T4\n* T5", - expect: expectMap{ - encoderDJSON: `[{"t":"BulletList","c":[[{"t":"Para","i":[{"t":"Text","s":"T1"}]},{"t":"BulletList","c":[[{"t":"Para","i":[{"t":"Text","s":"T2"}]}]]}],[{"t":"Para","i":[{"t":"Text","s":"T3"}]},{"t":"BulletList","c":[[{"t":"Para","i":[{"t":"Text","s":"T4"}]}]]}],[{"t":"Para","i":[{"t":"Text","s":"T5"}]}]]}]`, - encoderHTML: `
    -
  • -

    T1

    -
      -
    • T2
    • -
    -
  • -
  • -

    T3

    -
      -
    • T4
    • -
    -
  • -
  • -

    T5

    -
  • -
`, - encoderNative: `[BulletList - [[Para Text "T1"], - [BulletList - [[Para Text "T2"]]]], - [[Para Text "T3"], - [BulletList - [[Para Text "T4"]]]], - [[Para Text "T5"]]]`, - encoderText: "T1\nT2\nT3\nT4\nT5", - encoderZmk: useZmk, - }, - }, - { - descr: "Zwei Listen hintereinander", - zmk: "* Item1.1\n* Item1.2\n* Item1.3\n\n* Item2.1\n* Item2.2", - expect: expectMap{ - encoderDJSON: `[{"t":"BulletList","c":[[{"t":"Para","i":[{"t":"Text","s":"Item1.1"}]}],[{"t":"Para","i":[{"t":"Text","s":"Item1.2"}]}],[{"t":"Para","i":[{"t":"Text","s":"Item1.3"}]}],[{"t":"Para","i":[{"t":"Text","s":"Item2.1"}]}],[{"t":"Para","i":[{"t":"Text","s":"Item2.2"}]}]]}]`, - encoderHTML: "
    \n
  • Item1.1
  • \n
  • Item1.2
  • \n
  • Item1.3
  • \n
  • Item2.1
  • \n
  • Item2.2
  • \n
", - encoderNative: `[BulletList - [[Para Text "Item1.1"]], - [[Para Text "Item1.2"]], - [[Para Text "Item1.3"]], - [[Para Text "Item2.1"]], - [[Para Text "Item2.2"]]]`, - encoderText: "Item1.1\nItem1.2\nItem1.3\nItem2.1\nItem2.2", - encoderZmk: "* Item1.1\n* Item1.2\n* Item1.3\n* Item2.1\n* Item2.2", - }, - }, - { - descr: "Simple horizontal rule", - zmk: `---`, - expect: expectMap{ - encoderDJSON: `[{"t":"Hrule"}]`, - encoderHTML: "
", - encoderNative: `[Hrule]`, - encoderText: ``, - encoderZmk: useZmk, - }, - }, - { - descr: "No list after paragraph", - zmk: "Text\n*abc", - expect: expectMap{ - encoderDJSON: `[{"t":"Para","i":[{"t":"Text","s":"Text"},{"t":"Soft"},{"t":"Text","s":"*abc"}]}]`, - encoderHTML: "

Text\n*abc

", - encoderNative: `[Para Text "Text",Space,Text "*abc"]`, - encoderText: `Text *abc`, - encoderZmk: useZmk, - }, - }, - { - descr: "A list after paragraph", - zmk: "Text\n* abc", - expect: expectMap{ - encoderDJSON: `[{"t":"Para","i":[{"t":"Text","s":"Text"}]},{"t":"BulletList","c":[[{"t":"Para","i":[{"t":"Text","s":"abc"}]}]]}]`, - encoderHTML: "

Text

\n
    \n
  • abc
  • \n
", - encoderNative: `[Para Text "Text"], -[BulletList - [[Para Text "abc"]]]`, - encoderText: "Text\nabc", - encoderZmk: useZmk, - }, - }, - { - descr: "Simple Quote Block", - zmk: "<<<\nToBeOrNotToBe\n<<< Romeo", - expect: expectMap{ - encoderDJSON: `[{"t":"QuoteBlock","b":[{"t":"Para","i":[{"t":"Text","s":"ToBeOrNotToBe"}]}],"i":[{"t":"Text","s":"Romeo"}]}]`, - encoderHTML: "
\n

ToBeOrNotToBe

\nRomeo\n
", - encoderNative: `[QuoteBlock - [[Para Text "ToBeOrNotToBe"]], - [Cite Text "Romeo"]]`, - encoderText: "ToBeOrNotToBe\nRomeo", - encoderZmk: useZmk, - }, - }, - { - descr: "Verse block", - zmk: `""" -A line - another line -Back - -Paragraph - - Spacy Para -""" Author`, - expect: expectMap{ - encoderDJSON: "[{\"t\":\"VerseBlock\",\"b\":[{\"t\":\"Para\",\"i\":[{\"t\":\"Text\",\"s\":\"A\u00a0line\"},{\"t\":\"Hard\"},{\"t\":\"Text\",\"s\":\"\u00a0\u00a0another\u00a0line\"},{\"t\":\"Hard\"},{\"t\":\"Text\",\"s\":\"Back\"}]},{\"t\":\"Para\",\"i\":[{\"t\":\"Text\",\"s\":\"Paragraph\"}]},{\"t\":\"Para\",\"i\":[{\"t\":\"Text\",\"s\":\"\u00a0\u00a0\u00a0\u00a0Spacy\u00a0\u00a0Para\"}]}],\"i\":[{\"t\":\"Text\",\"s\":\"Author\"}]}]", - encoderHTML: "
\n

A\u00a0line
\n\u00a0\u00a0another\u00a0line
\nBack

\n

Paragraph

\n

\u00a0\u00a0\u00a0\u00a0Spacy\u00a0\u00a0Para

\nAuthor\n
", - encoderNative: "[VerseBlock\n [[Para Text \"A\u00a0line\",Break,Text \"\u00a0\u00a0another\u00a0line\",Break,Text \"Back\"],\n [Para Text \"Paragraph\"],\n [Para Text \"\u00a0\u00a0\u00a0\u00a0Spacy\u00a0\u00a0Para\"]],\n [Cite Text \"Author\"]]", - encoderText: "A\u00a0line\n\u00a0\u00a0another\u00a0line\nBack\nParagraph\n\u00a0\u00a0\u00a0\u00a0Spacy\u00a0\u00a0Para\nAuthor", - encoderZmk: "\"\"\"\nA\u00a0line\\\n\u00a0\u00a0another\u00a0line\\\nBack\nParagraph\n\u00a0\u00a0\u00a0\u00a0Spacy\u00a0\u00a0Para\n\"\"\" Author", - }, - }, - { - descr: "Span Block", - zmk: `::: -A simple - span -and much more -:::`, - expect: expectMap{ - encoderDJSON: `[{"t":"SpanBlock","b":[{"t":"Para","i":[{"t":"Text","s":"A"},{"t":"Space"},{"t":"Text","s":"simple"},{"t":"Soft"},{"t":"Space","n":3},{"t":"Text","s":"span"},{"t":"Soft"},{"t":"Text","s":"and"},{"t":"Space"},{"t":"Text","s":"much"},{"t":"Space"},{"t":"Text","s":"more"}]}]}]`, - encoderHTML: "
\n

A simple\n span\nand much more

\n
", - encoderNative: `[SpanBlock - [[Para Text "A",Space,Text "simple",Space,Space 3,Text "span",Space,Text "and",Space,Text "much",Space,Text "more"]]]`, - encoderText: `A simple span and much more`, - encoderZmk: useZmk, - }, - }, - { - descr: "Simple Verbatim", - zmk: "```\nHello\nWorld\n```", - expect: expectMap{ - encoderDJSON: `[{"t":"CodeBlock","l":["Hello","World"]}]`, - encoderHTML: "
Hello\nWorld\n
", - encoderNative: `[CodeBlock "Hello\nWorld"]`, - encoderText: "Hello\nWorld", - encoderZmk: useZmk, - }, - }, - { - descr: "Simple Description List", - zmk: "; Zettel\n: Paper\n: Note\n; Zettelkasten\n: Slip box", - expect: expectMap{ - encoderDJSON: `[{"t":"DescriptionList","g":[[[{"t":"Text","s":"Zettel"}],[{"t":"Para","i":[{"t":"Text","s":"Paper"}]}],[{"t":"Para","i":[{"t":"Text","s":"Note"}]}]],[[{"t":"Text","s":"Zettelkasten"}],[{"t":"Para","i":[{"t":"Text","s":"Slip"},{"t":"Space"},{"t":"Text","s":"box"}]}]]]}]`, - encoderHTML: "
\n
Zettel
\n
Paper
\n
Note
\n
Zettelkasten
\n
Slip box
\n
", - encoderNative: `[DescriptionList - [Term [Text "Zettel"], - [Description - [Para Text "Paper"]], - [Description - [Para Text "Note"]]], - [Term [Text "Zettelkasten"], - [Description - [Para Text "Slip",Space,Text "box"]]]]`, - encoderText: "Zettel\nPaper\nNote\nZettelkasten\nSlip box", - encoderZmk: useZmk, - }, - }, - { - descr: "Simple Table", - zmk: "|c1|c2|c3\n|d1||d3", - expect: expectMap{ - encoderDJSON: `[{"t":"Table","p":[[],[[["",[{"t":"Text","s":"c1"}]],["",[{"t":"Text","s":"c2"}]],["",[{"t":"Text","s":"c3"}]]],[["",[{"t":"Text","s":"d1"}]],["",[]],["",[{"t":"Text","s":"d3"}]]]]]}]`, - encoderHTML: ` - - - - -
c1c2c3
d1d3
`, - encoderNative: `[Table - [Row [Cell Default Text "c1"],[Cell Default Text "c2"],[Cell Default Text "c3"]], - [Row [Cell Default Text "d1"],[Cell Default],[Cell Default Text "d3"]]]`, - encoderText: "c1 c2 c3\nd1 d3", - encoderZmk: useZmk, - }, - }, - { - descr: "Table with alignment and comment", - zmk: `|h1>|=h2|h3:| -|%--+---+---+ -|",[{"t":"Text","s":"h1"}]],["",[{"t":"Text","s":"h2"}]],[":",[{"t":"Text","s":"h3"}]]],[[["<",[{"t":"Text","s":"c1"}]],["",[{"t":"Text","s":"c2"}]],[":",[{"t":"Text","s":"c3"}]]],[[">",[{"t":"Text","s":"f1"}]],["",[{"t":"Text","s":"f2"}]],[":",[{"t":"Text","s":"=f3"}]]]]]}]`, - encoderHTML: ` - - - - - - - -
h1h2h3
c1c2c3
f1f2=f3
`, - encoderNative: `[Table - [Header [Cell Right Text "h1"],[Cell Default Text "h2"],[Cell Center Text "h3"]], - [Row [Cell Left Text "c1"],[Cell Default Text "c2"],[Cell Center Text "c3"]], - [Row [Cell Right Text "f1"],[Cell Default Text "f2"],[Cell Center Text "=f3"]]]`, - encoderText: "h1 h2 h3\nc1 c2 c3\nf1 f2 =f3", - encoderZmk: `|=h1>|=h2|=h3: -|emph", - encoderNative: `Emph [Text "emph"]`, - encoderText: "emph", - encoderZmk: useZmk, - }, - }, - { - descr: "Emphasized formatting (deprecated)", - zmk: "//emphd//", - expect: expectMap{ - encoderDJSON: `[{"t":"EmphD","i":[{"t":"Text","s":"emphd"}]}]`, - encoderHTML: `emphd`, - encoderNative: `EmphD [Text "emphd"]`, - encoderText: "emphd", - encoderZmk: "__emphd__", - }, - }, - { - descr: "Strong formatting", - zmk: "**strong**", - expect: expectMap{ - encoderDJSON: `[{"t":"Strong","i":[{"t":"Text","s":"strong"}]}]`, - encoderHTML: "strong", - encoderNative: `Strong [Text "strong"]`, - encoderText: "strong", - encoderZmk: useZmk, - }, - }, - { - descr: "Insert formatting", - zmk: ">>insert>>", - expect: expectMap{ - encoderDJSON: `[{"t":"Insert","i":[{"t":"Text","s":"insert"}]}]`, - encoderHTML: "insert", - encoderNative: `Insert [Text "insert"]`, - encoderText: "insert", - encoderZmk: useZmk, - }, - }, - { - descr: "Delete formatting", - zmk: "~~delete~~", - expect: expectMap{ - encoderDJSON: `[{"t":"Delete","i":[{"t":"Text","s":"delete"}]}]`, - encoderHTML: "delete", - encoderNative: `Delete [Text "delete"]`, - encoderText: "delete", - encoderZmk: useZmk, - }, - }, - { - descr: "Update formatting", - zmk: "~~old~~>>new>>", - expect: expectMap{ - encoderDJSON: `[{"t":"Delete","i":[{"t":"Text","s":"old"}]},{"t":"Insert","i":[{"t":"Text","s":"new"}]}]`, - encoderHTML: "oldnew", - encoderNative: `Delete [Text "old"],Insert [Text "new"]`, - encoderText: "oldnew", - encoderZmk: useZmk, - }, - }, - { - descr: "Monospace formatting", - zmk: "''monospace''", - expect: expectMap{ - encoderDJSON: `[{"t":"Mono","i":[{"t":"Text","s":"monospace"}]}]`, - encoderHTML: `monospace`, - encoderNative: `Mono [Text "monospace"]`, - encoderText: "monospace", - encoderZmk: useZmk, - }, - }, - { - descr: "Superscript formatting", - zmk: "^^superscript^^", - expect: expectMap{ - encoderDJSON: `[{"t":"Super","i":[{"t":"Text","s":"superscript"}]}]`, - encoderHTML: `superscript`, - encoderNative: `Super [Text "superscript"]`, - encoderText: `superscript`, - encoderZmk: useZmk, - }, - }, - { - descr: "Subscript formatting", - zmk: ",,subscript,,", - expect: expectMap{ - encoderDJSON: `[{"t":"Sub","i":[{"t":"Text","s":"subscript"}]}]`, - encoderHTML: `subscript`, - encoderNative: `Sub [Text "subscript"]`, - encoderText: `subscript`, - encoderZmk: useZmk, - }, - }, - { - descr: "Quotes formatting", - zmk: `""quotes""`, - expect: expectMap{ - encoderDJSON: `[{"t":"Quote","i":[{"t":"Text","s":"quotes"}]}]`, - encoderHTML: `"quotes"`, - encoderNative: `Quote [Text "quotes"]`, - encoderText: `quotes`, - encoderZmk: useZmk, - }, - }, - { - descr: "Quotes formatting (german)", - zmk: `""quotes""{lang=de}`, - expect: expectMap{ - encoderDJSON: `[{"t":"Quote","a":{"lang":"de"},"i":[{"t":"Text","s":"quotes"}]}]`, - encoderHTML: `„quotes“`, - encoderNative: `Quote ("",[lang="de"]) [Text "quotes"]`, - encoderText: `quotes`, - encoderZmk: `""quotes""{lang="de"}`, - }, - }, - { - descr: "Quotation formatting", - zmk: `<quotation`, - encoderNative: `Quotation [Text "quotation"]`, - encoderText: `quotation`, - encoderZmk: useZmk, - }, - }, - { - descr: "Small formatting", - zmk: `;;small;;`, - expect: expectMap{ - encoderDJSON: `[{"t":"Small","i":[{"t":"Text","s":"small"}]}]`, - encoderHTML: `small`, - encoderNative: `Small [Text "small"]`, - encoderText: `small`, - encoderZmk: useZmk, - }, - }, - { - descr: "Span formatting", - zmk: `::span::`, - expect: expectMap{ - encoderDJSON: `[{"t":"Span","i":[{"t":"Text","s":"span"}]}]`, - encoderHTML: `span`, - encoderNative: `Span [Text "span"]`, - encoderText: `span`, - encoderZmk: useZmk, - }, - }, - { - descr: "Code formatting", - zmk: "``code``", - expect: expectMap{ - encoderDJSON: `[{"t":"Code","s":"code"}]`, - encoderHTML: `code`, - encoderNative: `Code "code"`, - encoderText: `code`, - encoderZmk: useZmk, - }, - }, - { - descr: "Input formatting", - zmk: `++input++`, - expect: expectMap{ - encoderDJSON: `[{"t":"Input","s":"input"}]`, - encoderHTML: `input`, - encoderNative: `Input "input"`, - encoderText: `input`, - encoderZmk: useZmk, - }, - }, - { - descr: "Output formatting", - zmk: `==output==`, - expect: expectMap{ - encoderDJSON: `[{"t":"Output","s":"output"}]`, - encoderHTML: `output`, - encoderNative: `Output "output"`, - encoderText: `output`, - encoderZmk: useZmk, - }, - }, - { - descr: "Nested Span Quote formatting", - zmk: `::""abc""::{lang=fr}`, - expect: expectMap{ - encoderDJSON: `[{"t":"Span","a":{"lang":"fr"},"i":[{"t":"Quote","i":[{"t":"Text","s":"abc"}]}]}]`, - encoderHTML: `« abc »`, - encoderNative: `Span ("",[lang="fr"]) [Quote [Text "abc"]]`, - encoderText: `abc`, - encoderZmk: `::""abc""::{lang="fr"}`, - }, - }, - { - descr: "Simple Citation", - zmk: `[@Stern18]`, - expect: expectMap{ - encoderDJSON: `[{"t":"Cite","s":"Stern18"}]`, - encoderHTML: `Stern18`, // TODO - encoderNative: `Cite "Stern18"`, - encoderText: ``, - encoderZmk: useZmk, - }, - }, - { - descr: "No comment", - zmk: `% comment`, - expect: expectMap{ - encoderDJSON: `[{"t":"Text","s":"%"},{"t":"Space"},{"t":"Text","s":"comment"}]`, - encoderHTML: `% comment`, - encoderNative: `Text "%",Space,Text "comment"`, - encoderText: `% comment`, - encoderZmk: useZmk, - }, - }, - { - descr: "Line comment", - zmk: `%% line comment`, - expect: expectMap{ - encoderDJSON: `[{"t":"Comment","s":"line comment"}]`, - encoderHTML: ``, - encoderNative: `Comment "line comment"`, - encoderText: ``, - encoderZmk: useZmk, - }, - }, - { - descr: "Comment after text", - zmk: `Text %% comment`, - expect: expectMap{ - encoderDJSON: `[{"t":"Text","s":"Text"},{"t":"Comment","s":"comment"}]`, - encoderHTML: `Text `, - encoderNative: `Text "Text",Comment "comment"`, - encoderText: `Text`, - encoderZmk: useZmk, - }, - }, - { - descr: "Simple footnote", - zmk: `[^footnote]`, - expect: expectMap{ - encoderDJSON: `[{"t":"Footnote","i":[{"t":"Text","s":"footnote"}]}]`, - encoderHTML: `0`, - encoderNative: `Footnote [Text "footnote"]`, - encoderText: `footnote`, - encoderZmk: useZmk, - }, - }, - { - descr: "Simple mark", - zmk: `[!mark]`, - expect: expectMap{ - encoderDJSON: `[{"t":"Mark","s":"mark"}]`, - encoderHTML: ``, - encoderNative: `Mark "mark"`, - encoderText: ``, - encoderZmk: useZmk, - }, - }, - { - descr: "Dummy Link", - zmk: `[[abc]]`, - expect: expectMap{ - encoderDJSON: `[{"t":"Link","q":"external","s":"abc","i":[{"t":"Text","s":"abc"}]}]`, - encoderHTML: `abc`, - encoderNative: `Link EXTERNAL "abc" []`, - encoderText: ``, - encoderZmk: useZmk, - }, - }, - { - descr: "Simple URL", - zmk: `[[https://zettelstore.de]]`, - expect: expectMap{ - encoderDJSON: `[{"t":"Link","q":"external","s":"https://zettelstore.de","i":[{"t":"Text","s":"https://zettelstore.de"}]}]`, - encoderHTML: `https://zettelstore.de`, - encoderNative: `Link EXTERNAL "https://zettelstore.de" []`, - encoderText: ``, - encoderZmk: useZmk, - }, - }, - { - descr: "URL with Text", - zmk: `[[Home|https://zettelstore.de]]`, - expect: expectMap{ - encoderDJSON: `[{"t":"Link","q":"external","s":"https://zettelstore.de","i":[{"t":"Text","s":"Home"}]}]`, - encoderHTML: `Home`, - encoderNative: `Link EXTERNAL "https://zettelstore.de" [Text "Home"]`, - encoderText: `Home`, - encoderZmk: useZmk, - }, - }, - { - descr: "Simple Zettel ID", - zmk: `[[00000000000100]]`, - expect: expectMap{ - encoderDJSON: `[{"t":"Link","q":"zettel","s":"00000000000100","i":[{"t":"Text","s":"00000000000100"}]}]`, - encoderHTML: `00000000000100`, - encoderNative: `Link ZETTEL "00000000000100" []`, - encoderText: ``, - encoderZmk: useZmk, - }, - }, - { - descr: "Zettel ID with Text", - zmk: `[[Config|00000000000100]]`, - expect: expectMap{ - encoderDJSON: `[{"t":"Link","q":"zettel","s":"00000000000100","i":[{"t":"Text","s":"Config"}]}]`, - encoderHTML: `Config`, - encoderNative: `Link ZETTEL "00000000000100" [Text "Config"]`, - encoderText: `Config`, - encoderZmk: useZmk, - }, - }, - { - descr: "Simple Zettel ID with fragment", - zmk: `[[00000000000100#frag]]`, - expect: expectMap{ - encoderDJSON: `[{"t":"Link","q":"zettel","s":"00000000000100#frag","i":[{"t":"Text","s":"00000000000100#frag"}]}]`, - encoderHTML: `00000000000100#frag`, - encoderNative: `Link ZETTEL "00000000000100#frag" []`, - encoderText: ``, - encoderZmk: useZmk, - }, - }, - { - descr: "Zettel ID with Text and fragment", - zmk: `[[Config|00000000000100#frag]]`, - expect: expectMap{ - encoderDJSON: `[{"t":"Link","q":"zettel","s":"00000000000100#frag","i":[{"t":"Text","s":"Config"}]}]`, - encoderHTML: `Config`, - encoderNative: `Link ZETTEL "00000000000100#frag" [Text "Config"]`, - encoderText: `Config`, - encoderZmk: useZmk, - }, - }, - { - descr: "Fragment link to self", - zmk: `[[#frag]]`, - expect: expectMap{ - encoderDJSON: `[{"t":"Link","q":"self","s":"#frag","i":[{"t":"Text","s":"#frag"}]}]`, - encoderHTML: `#frag`, - encoderNative: `Link SELF "#frag" []`, - encoderText: ``, - encoderZmk: useZmk, - }, - }, - { - descr: "Hosted link", - zmk: `[[H|/hosted]]`, - expect: expectMap{ - encoderDJSON: `[{"t":"Link","q":"local","s":"/hosted","i":[{"t":"Text","s":"H"}]}]`, - encoderHTML: `H`, - encoderNative: `Link LOCAL "/hosted" [Text "H"]`, - encoderText: `H`, - encoderZmk: useZmk, - }, - }, - { - descr: "Based link", - zmk: `[[B|/based]]`, - expect: expectMap{ - encoderDJSON: `[{"t":"Link","q":"local","s":"/based","i":[{"t":"Text","s":"B"}]}]`, - encoderHTML: `B`, - encoderNative: `Link LOCAL "/based" [Text "B"]`, - encoderText: `B`, - encoderZmk: useZmk, - }, - }, - { - descr: "Relative link", - zmk: `[[R|../relative]]`, - expect: expectMap{ - encoderDJSON: `[{"t":"Link","q":"local","s":"../relative","i":[{"t":"Text","s":"R"}]}]`, - encoderHTML: `R`, - encoderNative: `Link LOCAL "../relative" [Text "R"]`, - encoderText: `R`, - encoderZmk: useZmk, - }, - }, - { - descr: "Dummy Embed", - zmk: `{{abc}}`, - expect: expectMap{ - encoderDJSON: `[{"t":"Embed","s":"abc"}]`, - encoderHTML: ``, - encoderNative: `Embed EXTERNAL "abc"`, - encoderText: ``, - encoderZmk: useZmk, - }, - }, - { - descr: "", - zmk: ``, - expect: expectMap{ - encoderDJSON: `[]`, - encoderHTML: ``, - encoderNative: ``, - encoderText: ``, - encoderZmk: useZmk, - }, - }, -} DELETED encoder/encoder_test.go Index: encoder/encoder_test.go ================================================================== --- encoder/encoder_test.go +++ encoder/encoder_test.go @@ -1,126 +0,0 @@ -//----------------------------------------------------------------------------- -// Copyright (c) 2021 Detlef Stern -// -// This file is part of zettelstore. -// -// Zettelstore is licensed under the latest version of the EUPL (European Union -// Public License). Please see file LICENSE.txt for your rights and obligations -// under this license. -//----------------------------------------------------------------------------- - -package encoder_test - -import ( - "bytes" - "fmt" - "testing" - - "zettelstore.de/c/api" - "zettelstore.de/z/ast" - "zettelstore.de/z/encoder" - "zettelstore.de/z/input" - "zettelstore.de/z/parser" - - _ "zettelstore.de/z/encoder/djsonenc" // Allow to use DJSON encoder. - _ "zettelstore.de/z/encoder/htmlenc" // Allow to use HTML encoder. - _ "zettelstore.de/z/encoder/nativeenc" // Allow to use native encoder. - _ "zettelstore.de/z/encoder/textenc" // Allow to use text encoder. - _ "zettelstore.de/z/encoder/zmkenc" // Allow to use zmk encoder. - _ "zettelstore.de/z/parser/zettelmark" // Allow to use zettelmark parser. -) - -type zmkTestCase struct { - descr string - zmk string - inline bool - expect expectMap -} - -type expectMap map[api.EncodingEnum]string - -const useZmk = "\000" - -const ( - encoderDJSON = api.EncoderDJSON - encoderHTML = api.EncoderHTML - encoderNative = api.EncoderNative - encoderText = api.EncoderText - encoderZmk = api.EncoderZmk -) - -func TestEncoder(t *testing.T) { - for i := range tcsInline { - tcsInline[i].inline = true - } - executeTestCases(t, append(tcsBlock, tcsInline...)) -} - -func executeTestCases(t *testing.T, testCases []zmkTestCase) { - t.Helper() - for testNum, tc := range testCases { - inp := input.NewInput([]byte(tc.zmk)) - var pe parserEncoder - if tc.inline { - pe = &peInlines{iln: parser.ParseInlines(inp, api.ValueSyntaxZmk)} - } else { - pe = &peBlocks{bln: parser.ParseBlocks(inp, nil, api.ValueSyntaxZmk)} - } - checkEncodings(t, testNum, pe, tc.descr, tc.expect, tc.zmk) - } -} - -func checkEncodings(t *testing.T, testNum int, pe parserEncoder, descr string, expected expectMap, zmkDefault string) { - t.Helper() - for enc, exp := range expected { - encdr := encoder.Create(enc, nil) - got, err := pe.encode(encdr) - if err != nil { - t.Error(err) - continue - } - if enc == api.EncoderZmk && exp == "\000" { - exp = zmkDefault - } - if got != exp { - prefix := fmt.Sprintf("Test #%d", testNum) - if d := descr; d != "" { - prefix += "\nReason: " + d - } - prefix += "\nMode: " + pe.mode() - t.Errorf("%s\nEncoder: %s\nExpected: %q\nGot: %q", prefix, enc, exp, got) - } - } -} - -type parserEncoder interface { - encode(encoder.Encoder) (string, error) - mode() string -} - -type peInlines struct { - iln *ast.InlineListNode -} - -func (in peInlines) encode(encdr encoder.Encoder) (string, error) { - var buf bytes.Buffer - if _, err := encdr.WriteInlines(&buf, in.iln); err != nil { - return "", err - } - return buf.String(), nil -} - -func (peInlines) mode() string { return "inline" } - -type peBlocks struct { - bln *ast.BlockListNode -} - -func (bl peBlocks) encode(encdr encoder.Encoder) (string, error) { - var buf bytes.Buffer - if _, err := encdr.WriteBlocks(&buf, bl.bln); err != nil { - return "", err - } - return buf.String(), nil - -} -func (peBlocks) mode() string { return "block" } Index: encoder/htmlenc/block.go ================================================================== --- encoder/htmlenc/block.go +++ encoder/htmlenc/block.go @@ -31,21 +31,21 @@ v.b.WriteByte('>') for _, line := range vn.Lines { v.writeHTMLEscaped(line) v.b.WriteByte('\n') } - v.b.WriteString("") + v.b.WriteString("\n") v.visibleSpace = oldVisible case ast.VerbatimComment: if vn.Attrs.HasDefault() { v.b.WriteString("") + v.b.WriteString("-->\n") } case ast.VerbatimHTML: for _, line := range vn.Lines { if !ignoreHTMLText(line) { @@ -118,23 +118,23 @@ v.b.WriteStrings("<", code) v.visitAttributes(attrs) v.b.WriteString(">\n") ast.Walk(v, rn.Blocks) if rn.Inlines != nil { - v.b.WriteString("\n") + v.b.WriteString("") ast.Walk(v, rn.Inlines) - v.b.WriteString("") + v.b.WriteString("\n") } - v.b.WriteStrings("\n") + v.b.WriteStrings("\n") v.inVerse = oldVerse } func (v *visitor) visitHeading(hn *ast.HeadingNode) { v.lang.push(hn.Attrs) defer v.lang.pop() - lvl := hn.Level + 1 + lvl := hn.Level if lvl > 6 { lvl = 6 // HTML has H1..H6 } strLvl := strconv.Itoa(lvl) v.b.WriteStrings("') ast.Walk(v, hn.Inlines) - v.b.WriteStrings("") + v.b.WriteStrings("\n") } var mapNestedListKind = map[ast.NestedListKind]string{ ast.NestedListOrdered: "ol", ast.NestedListUnordered: "ul", @@ -176,11 +176,11 @@ for _, item := range ln.Items { v.b.WriteString("
  • ") v.writeItemSliceOrPara(item, compact) v.b.WriteString("
  • \n") } - v.b.WriteStrings("") + v.b.WriteStrings("\n") } func (v *visitor) writeQuotationList(ln *ast.NestedListNode) { v.b.WriteString("
    \n") inPara := false @@ -249,17 +249,11 @@ if para, ok := ins[0].(*ast.ParaNode); ok { ast.Walk(v, para.Inlines) return } } - for i, in := range ins { - if i >= 0 { - v.b.WriteByte('\n') - } - ast.Walk(v, in) - } - v.b.WriteByte('\n') + ast.WalkItemSlice(v, ins) } func (v *visitor) writeDescriptionsSlice(ds ast.DescriptionSlice) { if len(ds) == 1 { if para, ok := ds[0].(*ast.ParaNode); ok { @@ -281,11 +275,11 @@ v.b.WriteString("
    ") v.writeDescriptionsSlice(b) v.b.WriteString("
    \n") } } - v.b.WriteString("") + v.b.WriteString("\n") } func (v *visitor) visitTable(tn *ast.TableNode) { v.b.WriteString("\n") if len(tn.Header) > 0 { @@ -298,18 +292,18 @@ for _, row := range tn.Rows { v.writeRow(row, "") } v.b.WriteString("\n") } - v.b.WriteString("
    ") + v.b.WriteString("\n") } var alignStyle = map[ast.Alignment]string{ ast.AlignDefault: ">", - ast.AlignLeft: " class=\"zs-ta-left\">", - ast.AlignCenter: " class=\"zs-ta-center\">", - ast.AlignRight: " class=\"zs-ta-right\">", + ast.AlignLeft: " style=\"text-align:left\">", + ast.AlignCenter: " style=\"text-align:center\">", + ast.AlignRight: " style=\"text-align:right\">", } func (v *visitor) writeRow(row ast.TableRow, cellStart, cellEnd string) { v.b.WriteString("") for _, cell := range row { @@ -330,14 +324,14 @@ case "gif", "jpeg", "png": v.b.WriteStrings("") + v.b.WriteString("\">\n") default: - v.b.WriteStrings("

    Unable to display BLOB with syntax '", bn.Syntax, "'.

    ") + v.b.WriteStrings("

    Unable to display BLOB with syntax '", bn.Syntax, "'.

    \n") } } func (v *visitor) writeEndPara() { - v.b.WriteString("

    ") + v.b.WriteString("

    \n") } Index: encoder/htmlenc/htmlenc.go ================================================================== --- encoder/htmlenc/htmlenc.go +++ encoder/htmlenc/htmlenc.go @@ -12,11 +12,11 @@ package htmlenc import ( "io" - "zettelstore.de/c/api" + "zettelstore.de/z/api" "zettelstore.de/z/ast" "zettelstore.de/z/domain/meta" "zettelstore.de/z/encoder" ) @@ -40,11 +40,11 @@ v.b.WriteStrings("\n") } else { v.b.WriteStrings("") } v.b.WriteString("\n\n\n") - plainTitle, hasTitle := zn.InhMeta.Get(api.KeyTitle) + plainTitle, hasTitle := zn.InhMeta.Get(meta.KeyTitle) if hasTitle { v.b.WriteStrings("", v.evalValue(plainTitle, evalMeta), "") } v.acceptMeta(zn.InhMeta, evalMeta) v.b.WriteString("\n\n\n") @@ -65,12 +65,12 @@ // WriteMeta encodes meta data as HTML5. func (he *htmlEncoder) WriteMeta(w io.Writer, m *meta.Meta, evalMeta encoder.EvalMetaFunc) (int, error) { v := newVisitor(he, w) // Write title - if title, ok := m.Get(api.KeyTitle); ok { - v.b.WriteStrings("") } // Write other metadata Index: encoder/htmlenc/inline.go ================================================================== --- encoder/htmlenc/inline.go +++ encoder/htmlenc/inline.go @@ -14,12 +14,12 @@ import ( "fmt" "strconv" "strings" - "zettelstore.de/c/api" "zettelstore.de/z/ast" + "zettelstore.de/z/domain/meta" ) func (v *visitor) visitBreak(bn *ast.BreakNode) { if bn.Hard { if v.env.IsXHTML() { @@ -157,18 +157,24 @@ defer v.lang.pop() var code string attrs := fn.Attrs.Clone() switch fn.Kind { - case ast.FormatEmphDeprecated: - code, attrs = "em", attrs.AddClass("zs-deprecated") + case ast.FormatItalic: + code = "i" case ast.FormatEmph: code = "em" + case ast.FormatBold: + code = "b" case ast.FormatStrong: code = "strong" + case ast.FormatUnder: + code = "u" // TODO: ändern in case ast.FormatInsert: code = "ins" + case ast.FormatStrike: + code = "s" case ast.FormatDelete: code = "del" case ast.FormatSuper: code = "sup" case ast.FormatSub: @@ -179,11 +185,12 @@ code = "small" case ast.FormatSpan: v.writeSpan(fn.Inlines, processSpanAttributes(attrs)) return case ast.FormatMonospace: - code, attrs = "span", attrs.AddClass("zs-monospace") + code = "span" + attrs = attrs.Set("style", "font-family:monospace") case ast.FormatQuote: v.visitQuotes(fn) return default: panic(fmt.Sprintf("Unknown format kind %v", fn.Kind)) @@ -203,13 +210,13 @@ v.b.WriteString("") } var langQuotes = map[string][2]string{ - api.ValueLangEN: {"“", "”"}, - "de": {"„", "“"}, - "fr": {"« ", " »"}, + meta.ValueLangEN: {"“", "”"}, + "de": {"„", "“"}, + "fr": {"« ", " »"}, } func getQuotes(lang string) (string, string) { langFields := strings.FieldsFunc(lang, func(r rune) bool { return r == '-' || r == '_' }) for len(langFields) > 0 { @@ -246,13 +253,10 @@ case ast.LiteralKeyb: v.writeLiteral("", ln.Attrs, ln.Text) case ast.LiteralOutput: v.writeLiteral("", ln.Attrs, ln.Text) case ast.LiteralComment: - if v.inlinePos > 0 { - v.b.WriteByte(' ') - } v.b.WriteString("") case ast.LiteralHTML: if !ignoreHTMLText(ln.Text) { Index: encoder/htmlenc/visitor.go ================================================================== --- encoder/htmlenc/visitor.go +++ encoder/htmlenc/visitor.go @@ -10,17 +10,16 @@ // Package htmlenc encodes the abstract syntax tree into HTML5. package htmlenc import ( - "bytes" "io" "sort" "strconv" "strings" - "zettelstore.de/c/api" + "zettelstore.de/z/api" "zettelstore.de/z/ast" "zettelstore.de/z/domain/meta" "zettelstore.de/z/encoder" "zettelstore.de/z/strfun" ) @@ -32,11 +31,10 @@ visibleSpace bool // Show space character in plain text inVerse bool // In verse block inInteractive bool // Rendered interactive HTML code lang langStack textEnc encoder.Encoder - inlinePos int // Element position in inline list node } func newVisitor(he *htmlEncoder, w io.Writer) *visitor { var lang string if he.env != nil { @@ -50,23 +48,10 @@ } } func (v *visitor) Visit(node ast.Node) ast.Visitor { switch n := node.(type) { - case *ast.BlockListNode: - for i, bn := range n.List { - if i > 0 { - v.b.WriteByte('\n') - } - ast.Walk(v, bn) - } - case *ast.InlineListNode: - for i, in := range n.List { - v.inlinePos = i - ast.Walk(v, in) - } - v.inlinePos = 0 case *ast.ParaNode: v.b.WriteString("

    ") ast.Walk(v, n.Inlines) v.writeEndPara() case *ast.VerbatimNode: @@ -77,13 +62,13 @@ v.visitHeading(n) case *ast.HRuleNode: v.b.WriteString("") + v.b.WriteString(" />\n") } else { - v.b.WriteBytes('>') + v.b.WriteString(">\n") } case *ast.NestedListNode: v.visitNestedList(n) case *ast.DescriptionListNode: v.visitDescriptionList(n) @@ -92,10 +77,11 @@ case *ast.BLOBNode: v.visitBLOB(n) case *ast.TextNode: v.writeHTMLEscaped(n.Text) case *ast.TagNode: + // TODO: erst mal als span. Link wäre gut, muss man vermutlich via Callback lösen. v.b.WriteString("#") v.writeHTMLEscaped(n.Tag) v.b.WriteString("") case *ast.SpaceNode: if v.inVerse || v.env.IsXHTML() { @@ -124,24 +110,24 @@ } return nil } var mapMetaKey = map[string]string{ - api.KeyCopyright: "copyright", - api.KeyLicense: "license", + meta.KeyCopyright: "copyright", + meta.KeyLicense: "license", } func (v *visitor) acceptMeta(m *meta.Meta, evalMeta encoder.EvalMetaFunc) { ignore := v.setupIgnoreSet() - ignore[api.KeyTitle] = true - if tags, ok := m.Get(api.KeyAllTags); ok { - v.writeTags(tags) - ignore[api.KeyAllTags] = true - ignore[api.KeyTags] = true - } else if tags, ok = m.Get(api.KeyTags); ok { - v.writeTags(tags) - ignore[api.KeyTags] = true + ignore[meta.KeyTitle] = true + if tags, ok := m.Get(meta.KeyAllTags); ok { + v.writeTags(tags) + ignore[meta.KeyAllTags] = true + ignore[meta.KeyTags] = true + } else if tags, ok := m.Get(meta.KeyTags); ok { + v.writeTags(tags) + ignore[meta.KeyTags] = true } for _, p := range m.Pairs(true) { key := p.Key if ignore[key] { @@ -160,14 +146,14 @@ } } } func (v *visitor) evalValue(value string, evalMeta encoder.EvalMetaFunc) string { - var buf bytes.Buffer - _, err := v.textEnc.WriteInlines(&buf, evalMeta(value)) + var sb strings.Builder + _, err := v.textEnc.WriteInlines(&sb, evalMeta(value)) if err == nil { - return buf.String() + return sb.String() } return "" } func (v *visitor) setupIgnoreSet() map[string]bool { Index: encoder/nativeenc/nativeenc.go ================================================================== --- encoder/nativeenc/nativeenc.go +++ encoder/nativeenc/nativeenc.go @@ -15,11 +15,11 @@ "fmt" "io" "sort" "strconv" - "zettelstore.de/c/api" + "zettelstore.de/z/api" "zettelstore.de/z/ast" "zettelstore.de/z/domain/meta" "zettelstore.de/z/encoder" ) @@ -186,14 +186,14 @@ rawDoubleQuote = []byte{'\\', '"'} rawNewline = []byte{'\\', 'n'} ) func (v *visitor) acceptMeta(m *meta.Meta, evalMeta encoder.EvalMetaFunc) { - v.writeZettelmarkup("Title", m.GetDefault(api.KeyTitle, ""), evalMeta) - v.writeMetaString(m, api.KeyRole, "Role") - v.writeMetaList(m, api.KeyTags, "Tags") - v.writeMetaString(m, api.KeySyntax, "Syntax") + v.writeZettelmarkup("Title", m.GetDefault(meta.KeyTitle, ""), evalMeta) + v.writeMetaString(m, meta.KeyRole, "Role") + v.writeMetaList(m, meta.KeyTags, "Tags") + v.writeMetaString(m, meta.KeySyntax, "Syntax") pairs := m.PairsRest(true) if len(pairs) == 0 { return } v.b.WriteString("\n[Header") @@ -479,22 +479,25 @@ v.writeEscaped(fragment) } } var mapFormatKind = map[ast.FormatKind][]byte{ - ast.FormatEmphDeprecated: []byte("EmphD"), - ast.FormatEmph: []byte("Emph"), - ast.FormatStrong: []byte("Strong"), - ast.FormatInsert: []byte("Insert"), - ast.FormatMonospace: []byte("Mono"), - ast.FormatDelete: []byte("Delete"), - ast.FormatSuper: []byte("Super"), - ast.FormatSub: []byte("Sub"), - ast.FormatQuote: []byte("Quote"), - ast.FormatQuotation: []byte("Quotation"), - ast.FormatSmall: []byte("Small"), - ast.FormatSpan: []byte("Span"), + ast.FormatItalic: []byte("Italic"), + ast.FormatEmph: []byte("Emph"), + ast.FormatBold: []byte("Bold"), + ast.FormatStrong: []byte("Strong"), + ast.FormatUnder: []byte("Underline"), + ast.FormatInsert: []byte("Insert"), + ast.FormatMonospace: []byte("Mono"), + ast.FormatStrike: []byte("Strikethrough"), + ast.FormatDelete: []byte("Delete"), + ast.FormatSuper: []byte("Super"), + ast.FormatSub: []byte("Sub"), + ast.FormatQuote: []byte("Quote"), + ast.FormatQuotation: []byte("Quotation"), + ast.FormatSmall: []byte("Small"), + ast.FormatSpan: []byte("Span"), } var mapLiteralKind = map[ast.LiteralKind][]byte{ ast.LiteralProg: []byte("Code"), ast.LiteralKeyb: []byte("Input"), Index: encoder/textenc/textenc.go ================================================================== --- encoder/textenc/textenc.go +++ encoder/textenc/textenc.go @@ -12,11 +12,11 @@ package textenc import ( "io" - "zettelstore.de/c/api" + "zettelstore.de/z/api" "zettelstore.de/z/ast" "zettelstore.de/z/domain/meta" "zettelstore.de/z/encoder" ) @@ -95,12 +95,11 @@ return length, err } // visitor writes the abstract syntax tree to an io.Writer. type visitor struct { - b encoder.BufWriter - inlinePos int + b encoder.BufWriter } func newVisitor(w io.Writer) *visitor { return &visitor{b: encoder.NewBufWriter(w)} } @@ -107,17 +106,10 @@ func (v *visitor) Visit(node ast.Node) ast.Visitor { switch n := node.(type) { case *ast.BlockListNode: v.visitBlockList(n) - case *ast.InlineListNode: - for i, in := range n.List { - v.inlinePos = i - ast.Walk(v, in) - } - v.inlinePos = 0 - return nil case *ast.VerbatimNode: v.visitVerbatim(n) return nil case *ast.RegionNode: v.visitBlockList(n.Blocks) @@ -155,14 +147,12 @@ if !n.OnlyRef { ast.Walk(v, n.Inlines) } return nil case *ast.FootnoteNode: - if v.inlinePos > 0 { - v.b.WriteByte(' ') - } - // No 'return nil' to write text + v.b.WriteByte(' ') + return v // No 'return nil' to write text case *ast.LiteralNode: if n.Kind != ast.LiteralComment { v.b.WriteString(n.Text) } } Index: encoder/zmkenc/zmkenc.go ================================================================== --- encoder/zmkenc/zmkenc.go +++ encoder/zmkenc/zmkenc.go @@ -14,11 +14,11 @@ import ( "fmt" "io" "sort" - "zettelstore.de/c/api" + "zettelstore.de/z/api" "zettelstore.de/z/ast" "zettelstore.de/z/domain/meta" "zettelstore.de/z/encoder" ) @@ -85,14 +85,13 @@ return length, err } // visitor writes the abstract syntax tree to an io.Writer. type visitor struct { - b encoder.BufWriter - prefix []byte - enc *zmkEncoder - inlinePos int + b encoder.BufWriter + prefix []byte + enc *zmkEncoder } func newVisitor(w io.Writer, enc *zmkEncoder) *visitor { return &visitor{ b: encoder.NewBufWriter(w), @@ -100,42 +99,36 @@ } } func (v *visitor) Visit(node ast.Node) ast.Visitor { switch n := node.(type) { - case *ast.BlockListNode: - for i, bn := range n.List { - if i > 0 { - v.b.WriteByte('\n') - } - ast.Walk(v, bn) - } - case *ast.InlineListNode: - for i, in := range n.List { - v.inlinePos = i - ast.Walk(v, in) - } - v.inlinePos = 0 + case *ast.ParaNode: + ast.Walk(v, n.Inlines) + v.b.WriteByte('\n') + if len(v.prefix) == 0 { + v.b.WriteByte('\n') + } case *ast.VerbatimNode: v.visitVerbatim(n) case *ast.RegionNode: v.visitRegion(n) case *ast.HeadingNode: v.visitHeading(n) case *ast.HRuleNode: v.b.WriteString("---") v.visitAttributes(n.Attrs) + v.b.WriteByte('\n') case *ast.NestedListNode: v.visitNestedList(n) case *ast.DescriptionListNode: v.visitDescriptionList(n) case *ast.TableNode: v.visitTable(n) case *ast.BLOBNode: v.b.WriteStrings( "%% Unable to display BLOB with title '", n.Title, - "' and syntax '", n.Syntax, "'.") + "' and syntax '", n.Syntax, "'\n") case *ast.TextNode: v.visitText(n) case *ast.TagNode: v.b.WriteStrings("#", n.Tag) case *ast.SpaceNode: @@ -163,30 +156,19 @@ return v } return nil } -var mapVerbatimKind = map[ast.VerbatimKind]string{ - ast.VerbatimComment: "%%%", - ast.VerbatimHTML: "???", - ast.VerbatimProg: "```", -} - func (v *visitor) visitVerbatim(vn *ast.VerbatimNode) { - kind, ok := mapVerbatimKind[vn.Kind] - if !ok { - panic(fmt.Sprintf("Unknown verbatim kind %d", vn.Kind)) - } - // TODO: scan cn.Lines to find embedded "`"s at beginning - v.b.WriteString(kind) + v.b.WriteString("```") v.visitAttributes(vn.Attrs) v.b.WriteByte('\n') for _, line := range vn.Lines { v.b.WriteStrings(line, "\n") } - v.b.WriteString(kind) + v.b.WriteString("```\n") } var mapRegionKind = map[ast.RegionKind]string{ ast.RegionSpan: ":::", ast.RegionQuote: "<<<", @@ -201,23 +183,26 @@ } v.b.WriteString(kind) v.visitAttributes(rn.Attrs) v.b.WriteByte('\n') ast.Walk(v, rn.Blocks) - v.b.WriteByte('\n') v.b.WriteString(kind) if rn.Inlines != nil { v.b.WriteByte(' ') ast.Walk(v, rn.Inlines) } + v.b.WriteByte('\n') } func (v *visitor) visitHeading(hn *ast.HeadingNode) { - const headingSigns = "========= " - v.b.WriteString(headingSigns[len(headingSigns)-hn.Level-3:]) + for i := 0; i <= hn.Level; i++ { + v.b.WriteByte('=') + } + v.b.WriteByte(' ') ast.Walk(v, hn.Inlines) v.visitAttributes(hn.Attrs) + v.b.WriteByte('\n') } var mapNestedListKind = map[ast.NestedListKind]byte{ ast.NestedListOrdered: '#', ast.NestedListUnordered: '*', @@ -224,46 +209,39 @@ ast.NestedListQuote: '>', } func (v *visitor) visitNestedList(ln *ast.NestedListNode) { v.prefix = append(v.prefix, mapNestedListKind[ln.Kind]) - for i, item := range ln.Items { - if i > 0 { - v.b.WriteByte('\n') - } + for _, item := range ln.Items { v.b.Write(v.prefix) v.b.WriteByte(' ') - for j, in := range item { - if j > 0 { - v.b.WriteByte('\n') + for i, in := range item { + if i > 0 { if _, ok := in.(*ast.ParaNode); ok { - v.writePrefixSpaces() + v.b.WriteByte('\n') + for j := 0; j <= len(v.prefix); j++ { + v.b.WriteByte(' ') + } } } ast.Walk(v, in) } } v.prefix = v.prefix[:len(v.prefix)-1] -} - -func (v *visitor) writePrefixSpaces() { - for i := 0; i <= len(v.prefix); i++ { - v.b.WriteByte(' ') - } + v.b.WriteByte('\n') } func (v *visitor) visitDescriptionList(dn *ast.DescriptionListNode) { - for i, descr := range dn.Descriptions { - if i > 0 { - v.b.WriteByte('\n') - } + for _, descr := range dn.Descriptions { v.b.WriteString("; ") ast.Walk(v, descr.Term) + v.b.WriteByte('\n') for _, b := range descr.Descriptions { - v.b.WriteString("\n: ") + v.b.WriteString(": ") ast.WalkDescriptionSlice(v, b) + v.b.WriteByte('\n') } } } var alignCode = map[ast.Alignment]string{ @@ -272,44 +250,35 @@ ast.AlignCenter: ":", ast.AlignRight: ">", } func (v *visitor) visitTable(tn *ast.TableNode) { - if header := tn.Header; len(header) > 0 { - v.writeTableHeader(header, tn.Align) - v.b.WriteByte('\n') - } - for i, row := range tn.Rows { - if i > 0 { - v.b.WriteByte('\n') - } - v.writeTableRow(row, tn.Align) - } -} - -func (v *visitor) writeTableHeader(header ast.TableRow, align []ast.Alignment) { - for pos, cell := range header { - v.b.WriteString("|=") - colAlign := align[pos] - if cell.Align != colAlign { - v.b.WriteString(alignCode[cell.Align]) - } - ast.Walk(v, cell.Inlines) - if colAlign != ast.AlignDefault { - v.b.WriteString(alignCode[colAlign]) - } - } -} - -func (v *visitor) writeTableRow(row ast.TableRow, align []ast.Alignment) { - for pos, cell := range row { - v.b.WriteByte('|') - if cell.Align != align[pos] { - v.b.WriteString(alignCode[cell.Align]) - } - ast.Walk(v, cell.Inlines) - } + if len(tn.Header) > 0 { + for pos, cell := range tn.Header { + v.b.WriteString("|=") + colAlign := tn.Align[pos] + if cell.Align != colAlign { + v.b.WriteString(alignCode[cell.Align]) + } + ast.Walk(v, cell.Inlines) + if colAlign != ast.AlignDefault { + v.b.WriteString(alignCode[colAlign]) + } + } + v.b.WriteByte('\n') + } + for _, row := range tn.Rows { + for pos, cell := range row { + v.b.WriteByte('|') + if cell.Align != tn.Align[pos] { + v.b.WriteString(alignCode[cell.Align]) + } + ast.Walk(v, cell.Inlines) + } + v.b.WriteByte('\n') + } + v.b.WriteByte('\n') } var escapeSeqs = map[string]bool{ "\\": true, "//": true, @@ -400,33 +369,43 @@ v.b.WriteByte(']') v.visitAttributes(cn.Attrs) } var mapFormatKind = map[ast.FormatKind][]byte{ - ast.FormatEmphDeprecated: []byte("__"), - ast.FormatEmph: []byte("__"), - ast.FormatStrong: []byte("**"), - ast.FormatInsert: []byte(">>"), - ast.FormatDelete: []byte("~~"), - ast.FormatSuper: []byte("^^"), - ast.FormatSub: []byte(",,"), - ast.FormatQuotation: []byte("<<"), - ast.FormatQuote: []byte("\"\""), - ast.FormatSmall: []byte(";;"), - ast.FormatSpan: []byte("::"), - ast.FormatMonospace: []byte("''"), + ast.FormatItalic: []byte("//"), + ast.FormatEmph: []byte("//"), + ast.FormatBold: []byte("**"), + ast.FormatStrong: []byte("**"), + ast.FormatUnder: []byte("__"), + ast.FormatInsert: []byte("__"), + ast.FormatStrike: []byte("~~"), + ast.FormatDelete: []byte("~~"), + ast.FormatSuper: []byte("^^"), + ast.FormatSub: []byte(",,"), + ast.FormatQuotation: []byte("<<"), + ast.FormatQuote: []byte("\"\""), + ast.FormatSmall: []byte(";;"), + ast.FormatSpan: []byte("::"), + ast.FormatMonospace: []byte("''"), } func (v *visitor) visitFormat(fn *ast.FormatNode) { kind, ok := mapFormatKind[fn.Kind] if !ok { panic(fmt.Sprintf("Unknown format kind %d", fn.Kind)) } + attrs := fn.Attrs + switch fn.Kind { + case ast.FormatEmph, ast.FormatStrong, ast.FormatInsert, ast.FormatDelete: + attrs = attrs.Clone() + attrs.Set("-", "") + } + v.b.Write(kind) ast.Walk(v, fn.Inlines) v.b.Write(kind) - v.visitAttributes(fn.Attrs) + v.visitAttributes(attrs) } func (v *visitor) visitLiteral(ln *ast.LiteralNode) { switch ln.Kind { case ast.LiteralProg: @@ -434,13 +413,10 @@ case ast.LiteralKeyb: v.writeLiteral('+', ln.Attrs, ln.Text) case ast.LiteralOutput: v.writeLiteral('=', ln.Attrs, ln.Text) case ast.LiteralComment: - if v.inlinePos > 0 { - v.b.WriteByte(' ') - } v.b.WriteStrings("%% ", ln.Text) case ast.LiteralHTML: v.b.WriteString("``") v.writeEscaped(ln.Text, '`') v.b.WriteString("``{=html,.warning}") Index: evaluator/evaluator.go ================================================================== --- evaluator/evaluator.go +++ evaluator/evaluator.go @@ -15,11 +15,10 @@ "context" "errors" "fmt" "strconv" - "zettelstore.de/c/api" "zettelstore.de/z/ast" "zettelstore.de/z/box" "zettelstore.de/z/config" "zettelstore.de/z/domain" "zettelstore.de/z/domain/id" @@ -28,14 +27,14 @@ "zettelstore.de/z/parser/cleaner" ) // Environment contains values to control the evaluation. type Environment struct { - GetTagRef func(string) *ast.Reference - GetHostedRef func(string) *ast.Reference - GetFoundRef func(zid id.Zid, fragment string) *ast.Reference - GetImageMaterial func(zettel domain.Zettel, syntax string) ast.MaterialNode + EmbedImage bool + GetTagRef func(string) *ast.Reference + GetHostedRef func(string) *ast.Reference + GetFoundRef func(zid id.Zid, fragment string) *ast.Reference } // Port contains all methods to retrieve zettel (or part of it) to evaluate a zettel. type Port interface { GetMeta(context.Context, id.Zid) (*meta.Meta, error) @@ -65,11 +64,11 @@ e := evaluator{ ctx: ctx, port: port, env: env, rtConfig: rtConfig, - costMap: map[id.Zid]embedCost{}, + astMap: map[id.Zid]*ast.ZettelNode{}, embedMap: map[string]*ast.InlineListNode{}, embedCount: 0, marker: &ast.ZettelNode{}, } ast.Walk(&e, n) @@ -78,21 +77,16 @@ type evaluator struct { ctx context.Context port Port env *Environment rtConfig config.Config - costMap map[id.Zid]embedCost + astMap map[id.Zid]*ast.ZettelNode marker *ast.ZettelNode embedMap map[string]*ast.InlineListNode embedCount int } -type embedCost struct { - zn *ast.ZettelNode - ec int -} - func (e *evaluator) Visit(node ast.Node) ast.Visitor { switch n := node.(type) { case *ast.InlineListNode: e.visitInlineList(n) default: @@ -109,16 +103,16 @@ case *ast.TagNode: iln.List[i] = e.visitTag(n) case *ast.LinkNode: iln.List[i] = e.evalLinkNode(n) case *ast.EmbedNode: - in2 := e.evalEmbedNode(n) - if ln, ok := in2.(*ast.InlineListNode); ok { + in := e.evalEmbedNode(n) + if ln, ok := in.(*ast.InlineListNode); ok { iln.List = replaceWithInlineNodes(iln.List, i, ln.List) i += len(ln.List) - 1 } else { - iln.List[i] = in2 + iln.List[i] = in } } } } @@ -186,16 +180,10 @@ } return ln } func (e *evaluator) evalEmbedNode(en *ast.EmbedNode) ast.InlineNode { - if maxTrans := e.rtConfig.GetMaxTransclusions(); e.embedCount > maxTrans { - e.embedCount = maxTrans + 1 // To prevent e.embedCount from counting - return createErrorText(en, - "Too", "many", "transclusions", "(must", "be", "at", "most", strconv.Itoa(maxTrans)+",", - "see", "runtime", "configuration", "key", "max-transclusions):") - } switch en.Material.(type) { case *ast.ReferenceMaterialNode: case *ast.BLOBMaterialNode: return en default: @@ -202,17 +190,17 @@ panic(fmt.Sprintf("Unknown material type %t for %v", en.Material, en.Material)) } ref := en.Material.(*ast.ReferenceMaterialNode) switch ref.Ref.State { - case ast.RefStateInvalid, ast.RefStateBroken: - e.embedCount++ + case ast.RefStateInvalid: return e.createErrorImage(en) case ast.RefStateZettel, ast.RefStateFound: case ast.RefStateSelf: - e.embedCount++ return createErrorText(en, "Self", "embed", "reference:") + case ast.RefStateBroken: + return e.createErrorImage(en) case ast.RefStateHosted, ast.RefStateBased, ast.RefStateExternal: return en default: panic(fmt.Sprintf("Unknown state %v for reference %v", ref.Ref.State, ref.Ref)) } @@ -221,128 +209,107 @@ if err != nil { panic(err) } zettel, err := e.port.GetZettel(box.NoEnrichContext(e.ctx), zid) if err != nil { - e.embedCount++ return e.createErrorImage(en) } - if syntax := e.getSyntax(zettel.Meta); parser.IsImageFormat(syntax) { - return e.embedImage(en, zettel) - } else if !parser.IsTextParser(syntax) { + syntax := e.getSyntax(zettel.Meta) + if parser.IsImageFormat(syntax) { + return e.embedImage(en, zettel, syntax) + } + if !parser.IsTextParser(syntax) { // Not embeddable. - e.embedCount++ return createErrorText(en, "Not", "embeddable (syntax="+syntax+"):") } - cost, ok := e.costMap[zid] - zn := cost.zn + zn, ok := e.astMap[zid] if zn == e.marker { - e.embedCount++ return createErrorText(en, "Recursive", "transclusion:") } if !ok { - ec := e.embedCount - e.costMap[zid] = embedCost{zn: e.marker, ec: ec} - zn = e.evaluateEmbeddedZettel(zettel) - e.costMap[zid] = embedCost{zn: zn, ec: e.embedCount - ec} - e.embedCount = 0 // No stack needed, because embedding is done left-recursive, depth-first. + e.astMap[zid] = e.marker + zn = e.evaluateEmbeddedZettel(zettel, syntax) + e.astMap[zid] = zn } - e.embedCount++ result, ok := e.embedMap[ref.Ref.Value] if !ok { // Search for text to be embedded. result = findInlineList(zn.Ast, ref.Ref.URL.Fragment) e.embedMap[ref.Ref.Value] = result - } - if result.IsEmpty() { - return &ast.LiteralNode{ - Kind: ast.LiteralComment, - Text: "Nothing to transclude: " + en.Material.(*ast.ReferenceMaterialNode).Ref.String(), + if result.IsEmpty() { + return createErrorText(en, "Nothing", "to", "transclude:") } } - if ec := cost.ec; ec > 0 { - e.embedCount += cost.ec + e.embedCount++ + if maxTrans := e.rtConfig.GetMaxTransclusions(); e.embedCount > maxTrans { + return createErrorText(en, "Too", "many", "transclusions ("+strconv.Itoa(maxTrans)+"):") } return result } func (e *evaluator) getSyntax(m *meta.Meta) string { if cfg := e.rtConfig; cfg != nil { return config.GetSyntax(m, cfg) } - return m.GetDefault(api.KeySyntax, "") -} - -func (e *evaluator) getTitle(m *meta.Meta) string { - if cfg := e.rtConfig; cfg != nil { - return config.GetTitle(m, cfg) - } - return m.GetDefault(api.KeyTitle, "") -} - -func (e *evaluator) createErrorImage(en *ast.EmbedNode) *ast.EmbedNode { - errorZid := id.EmojiZid - if gim := e.env.GetImageMaterial; gim != nil { - zettel, err := e.port.GetZettel(box.NoEnrichContext(e.ctx), errorZid) - if err != nil { - panic(err) - } - en.Material = gim(zettel, e.getSyntax(zettel.Meta)) - if en.Inlines == nil { - if title := e.getTitle(zettel.Meta); title != "" { - en.Inlines = parser.ParseMetadata(title) - } - } - return en - } - en.Material = &ast.ReferenceMaterialNode{Ref: ast.ParseReference(errorZid.String())} - if en.Inlines == nil { - en.Inlines = parser.ParseMetadata("Error placeholder") - } - return en -} - -func (e *evaluator) embedImage(en *ast.EmbedNode, zettel domain.Zettel) *ast.EmbedNode { - if gim := e.env.GetImageMaterial; gim != nil { - en.Material = gim(zettel, e.getSyntax(zettel.Meta)) - return en + return m.GetDefault(meta.KeySyntax, "") +} + +func (e *evaluator) createErrorImage(en *ast.EmbedNode) *ast.EmbedNode { + zid := id.EmojiZid + if !e.env.EmbedImage { + en.Material = &ast.ReferenceMaterialNode{Ref: ast.ParseReference(zid.String())} + return en + } + zettel, err := e.port.GetZettel(box.NoEnrichContext(e.ctx), zid) + if err == nil { + return doEmbedImage(en, zettel, e.getSyntax(zettel.Meta)) + } + panic(err) +} + +func (e *evaluator) embedImage(en *ast.EmbedNode, zettel domain.Zettel, syntax string) *ast.EmbedNode { + if e.env.EmbedImage { + return doEmbedImage(en, zettel, syntax) + } + return en +} + +func doEmbedImage(en *ast.EmbedNode, zettel domain.Zettel, syntax string) *ast.EmbedNode { + en.Material = &ast.BLOBMaterialNode{ + Blob: zettel.Content.AsBytes(), + Syntax: syntax, } return en } func createErrorText(en *ast.EmbedNode, msgWords ...string) ast.InlineNode { - ln := linkNodeToEmbeddedReference(en) + ref := en.Material.(*ast.ReferenceMaterialNode) + ln := &ast.LinkNode{ + Ref: ref.Ref, + Inlines: ast.CreateInlineListNodeFromWords(ref.Ref.String()), + OnlyRef: true, + } text := ast.CreateInlineListNodeFromWords(msgWords...) - text.Append(&ast.SpaceNode{Lexeme: " "}, ln, &ast.TextNode{Text: "."}, &ast.SpaceNode{Lexeme: " "}) + text.Append(&ast.SpaceNode{Lexeme: " "}, ln) fn := &ast.FormatNode{ Kind: ast.FormatMonospace, Inlines: text, } fn = &ast.FormatNode{ - Kind: ast.FormatStrong, + Kind: ast.FormatBold, Inlines: ast.CreateInlineListNode(fn), } fn.Attrs = fn.Attrs.AddClass("error") return fn } -func linkNodeToEmbeddedReference(en *ast.EmbedNode) *ast.LinkNode { - ref := en.Material.(*ast.ReferenceMaterialNode) - ln := &ast.LinkNode{ - Ref: ref.Ref, - Inlines: ast.CreateInlineListNodeFromWords(ref.Ref.String()), - OnlyRef: true, - } - return ln -} - -func (e *evaluator) evaluateEmbeddedZettel(zettel domain.Zettel) *ast.ZettelNode { - zn := parser.ParseZettel(zettel, e.getSyntax(zettel.Meta), e.rtConfig) +func (e *evaluator) evaluateEmbeddedZettel(zettel domain.Zettel, syntax string) *ast.ZettelNode { + zn := parser.ParseZettel(zettel, syntax, e.rtConfig) ast.Walk(e, zn.Ast) return zn } func findInlineList(bnl *ast.BlockListNode, fragment string) *ast.InlineListNode { Index: go.mod ================================================================== --- go.mod +++ go.mod @@ -3,13 +3,12 @@ go 1.17 require ( github.com/fsnotify/fsnotify v1.5.1 github.com/pascaldekloe/jwt v1.10.0 - github.com/yuin/goldmark v1.4.3 + github.com/yuin/goldmark v1.4.1 golang.org/x/crypto v0.0.0-20210616213533-5ff15b29337e golang.org/x/term v0.0.0-20210615171337-6886f2dfbf5b golang.org/x/text v0.3.7 - zettelstore.de/c v0.0.0-20211025140135-ccc3f543d0e9 ) require golang.org/x/sys v0.0.0-20210630005230-0f9fa26af87c // indirect Index: go.sum ================================================================== --- go.sum +++ go.sum @@ -1,11 +1,11 @@ github.com/fsnotify/fsnotify v1.5.1 h1:mZcQUHVQUQWoPXXtuf9yuEXKudkV2sx1E06UadKWpgI= github.com/fsnotify/fsnotify v1.5.1/go.mod h1:T3375wBYaZdLLcVNkcVbzGHY7f1l/uK5T5Ai1i3InKU= github.com/pascaldekloe/jwt v1.10.0 h1:ktcIUV4TPvh404R5dIBEnPCsSwj0sqi3/0+XafE5gJs= github.com/pascaldekloe/jwt v1.10.0/go.mod h1:TKhllgThT7TOP5rGr2zMLKEDZRAgJfBbtKyVeRsNB9A= -github.com/yuin/goldmark v1.4.3 h1:eTEYYWtQLjK7+WK45Tk81OTkp/0UvAyqUj8flU0nTO4= -github.com/yuin/goldmark v1.4.3/go.mod h1:rmuwmfZ0+bvzB24eSC//bk1R1Zp3hM0OXYv/G2LIilg= +github.com/yuin/goldmark v1.4.1 h1:/vn0k+RBvwlxEmP5E7SZMqNxPhfMVFEJiykr15/0XKM= +github.com/yuin/goldmark v1.4.1/go.mod h1:mwnBkeHKe2W/ZEtQ+71ViKU8L12m81fl3OWwC1Zlc8k= golang.org/x/crypto v0.0.0-20210616213533-5ff15b29337e h1:gsTQYXdTw2Gq7RBsWvlQ91b+aEQ6bXFUngBGuR8sPpI= golang.org/x/crypto v0.0.0-20210616213533-5ff15b29337e/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= @@ -16,7 +16,5 @@ golang.org/x/term v0.0.0-20210615171337-6886f2dfbf5b/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.3.7 h1:olpwvP2KacW1ZWvsR7uQhoyTYvKAupfQrRGBFM352Gk= golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= -zettelstore.de/c v0.0.0-20211025140135-ccc3f543d0e9 h1:X6Kb1JO0SEFxNrXiELXJeDxIMhjv0l0UaXUnRkAofjY= -zettelstore.de/c v0.0.0-20211025140135-ccc3f543d0e9/go.mod h1:Hx/qzHCaQ8zzXEzBglBj/2aGkQpBQG81/4XztCIGJ84= Index: input/input.go ================================================================== --- input/input.go +++ input/input.go @@ -18,20 +18,20 @@ ) // Input is an abstract input source type Input struct { // Read-only, will never change - Src []byte // The source string + Src string // 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 { +func NewInput(src string) *Input { inp := &Input{Src: src} inp.Next() return inp } @@ -42,11 +42,11 @@ func (inp *Input) Next() { if inp.readPos < len(inp.Src) { inp.Pos = inp.readPos r, w := rune(inp.Src[inp.readPos]), 1 if r >= utf8.RuneSelf { - r, w = utf8.DecodeRune(inp.Src[inp.readPos:]) + r, w = utf8.DecodeRuneInString(inp.Src[inp.readPos:]) } inp.readPos += w inp.Ch = r } else { inp.Pos = len(inp.Src) @@ -65,11 +65,11 @@ 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:]) + r, _ = utf8.DecodeRuneInString(inp.Src[pos:]) } if r == '\t' { return ' ' } return r @@ -193,15 +193,15 @@ switch inp.Ch { case EOS, '\n', '\r': return "", false case ';': inp.Next() - es := string(inp.Src[pos:inp.Pos]) + es := inp.Src[pos:inp.Pos] ues := html.UnescapeString(es) if es == ues { return "", false } return ues, true } inp.Next() } } Index: input/input_test.go ================================================================== --- input/input_test.go +++ input/input_test.go @@ -17,20 +17,20 @@ "zettelstore.de/z/input" ) func TestEatEOL(t *testing.T) { t.Parallel() - inp := input.NewInput(nil) + inp := input.NewInput("") 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")) + inp = input.NewInput("ABC") if inp.Ch != 'A' { t.Errorf("First ch != 'A', got %q", inp.Ch) } inp.EatEOL() if inp.Ch != 'A' { @@ -49,11 +49,11 @@ {"&", "&"}, {" ", "\t"}, {""", "\""}, } for id, tc := range testcases { - inp := input.NewInput([]byte(tc.text)) + inp := input.NewInput(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) } Index: kernel/impl/box.go ================================================================== --- kernel/impl/box.go +++ kernel/impl/box.go @@ -78,11 +78,11 @@ if err != nil { kern.doLog("Unable to create box manager:", err) return err } kern.doLog("Start Box Manager:", mgr.Location()) - if err = mgr.Start(context.Background()); err != nil { + if err := mgr.Start(context.Background()); err != nil { kern.doLog("Unable to start box manager:", err) } kern.cfg.setBox(mgr) ps.manager = mgr return nil Index: kernel/impl/cfg.go ================================================================== --- kernel/impl/cfg.go +++ kernel/impl/cfg.go @@ -15,11 +15,10 @@ "context" "fmt" "strings" "sync" - "zettelstore.de/c/api" "zettelstore.de/z/box" "zettelstore.de/z/domain/id" "zettelstore.de/z/domain/meta" "zettelstore.de/z/kernel" ) @@ -30,16 +29,16 @@ rtConfig *myConfig } func (cs *configService) Initialize() { cs.descr = descriptionMap{ - api.KeyDefaultCopyright: {"Default copyright", parseString, true}, - api.KeyDefaultLang: {"Default language", parseString, true}, - api.KeyDefaultRole: {"Default role", parseString, true}, - api.KeyDefaultSyntax: {"Default syntax", parseString, true}, - api.KeyDefaultTitle: {"Default title", parseString, true}, - api.KeyDefaultVisibility: { + meta.KeyDefaultCopyright: {"Default copyright", parseString, true}, + meta.KeyDefaultLang: {"Default language", parseString, true}, + meta.KeyDefaultRole: {"Default role", parseString, true}, + meta.KeyDefaultSyntax: {"Default syntax", parseString, true}, + meta.KeyDefaultTitle: {"Default title", parseString, true}, + meta.KeyDefaultVisibility: { "Default zettel visibility", func(val string) interface{} { vis := meta.GetVisibility(val) if vis == meta.VisibilityUnknown { return nil @@ -46,38 +45,38 @@ } return vis }, true, }, - api.KeyExpertMode: {"Expert mode", parseBool, true}, - api.KeyFooterHTML: {"Footer HTML", parseString, true}, - api.KeyHomeZettel: {"Home zettel", parseZid, true}, - api.KeyMarkerExternal: {"Marker external URL", parseString, true}, - api.KeyMaxTransclusions: {"Maximum transclusions", parseInt, true}, - api.KeySiteName: {"Site name", parseString, true}, - api.KeyYAMLHeader: {"YAML header", parseBool, true}, - api.KeyZettelFileSyntax: { + meta.KeyExpertMode: {"Expert mode", parseBool, true}, + meta.KeyFooterHTML: {"Footer HTML", parseString, true}, + meta.KeyHomeZettel: {"Home zettel", parseZid, true}, + meta.KeyMarkerExternal: {"Marker external URL", parseString, true}, + meta.KeyMaxTransclusions: {"Maximum transclusions", parseInt, true}, + meta.KeySiteName: {"Site name", parseString, true}, + meta.KeyYAMLHeader: {"YAML header", parseBool, true}, + meta.KeyZettelFileSyntax: { "Zettel file syntax", func(val string) interface{} { return strings.Fields(val) }, true, }, } cs.next = interfaceMap{ - api.KeyDefaultCopyright: "", - api.KeyDefaultLang: api.ValueLangEN, - api.KeyDefaultRole: api.ValueRoleZettel, - api.KeyDefaultSyntax: api.ValueSyntaxZmk, - api.KeyDefaultTitle: "Untitled", - api.KeyDefaultVisibility: meta.VisibilityLogin, - api.KeyExpertMode: false, - api.KeyFooterHTML: "", - api.KeyHomeZettel: id.DefaultHomeZid, - api.KeyMarkerExternal: "➚", - api.KeyMaxTransclusions: 1024, - api.KeySiteName: "Zettelstore", - api.KeyYAMLHeader: false, - api.KeyZettelFileSyntax: nil, + meta.KeyDefaultCopyright: "", + meta.KeyDefaultLang: meta.ValueLangEN, + meta.KeyDefaultRole: meta.ValueRoleZettel, + meta.KeyDefaultSyntax: meta.ValueSyntaxZmk, + meta.KeyDefaultTitle: "Untitled", + meta.KeyDefaultVisibility: meta.VisibilityLogin, + meta.KeyExpertMode: false, + meta.KeyFooterHTML: "", + meta.KeyHomeZettel: id.DefaultHomeZid, + meta.KeyMarkerExternal: "➚", + meta.KeyMaxTransclusions: 1024, + meta.KeySiteName: "Zettelstore", + meta.KeyYAMLHeader: false, + meta.KeyZettelFileSyntax: nil, } } func (cs *configService) Start(kern *myKernel) error { kern.doLog("Start Config Service") @@ -103,11 +102,11 @@ cs.rtConfig = nil cs.mxService.Unlock() return nil } -func (*configService) GetStatistics() []kernel.KeyValue { +func (cs *configService) GetStatistics() []kernel.KeyValue { return nil } func (cs *configService) setBox(mgr box.Manager) { cs.rtConfig.setBox(mgr) @@ -153,16 +152,16 @@ go func() { cfg.doUpdate(ci.Box) }() } } var defaultKeys = map[string]string{ - api.KeyCopyright: api.KeyDefaultCopyright, - api.KeyLang: api.KeyDefaultLang, - api.KeyLicense: api.KeyDefaultLicense, - api.KeyRole: api.KeyDefaultRole, - api.KeySyntax: api.KeyDefaultSyntax, - api.KeyTitle: api.KeyDefaultTitle, + meta.KeyCopyright: meta.KeyDefaultCopyright, + meta.KeyLang: meta.KeyDefaultLang, + meta.KeyLicense: meta.KeyDefaultLicense, + meta.KeyRole: meta.KeyDefaultRole, + meta.KeySyntax: meta.KeyDefaultSyntax, + meta.KeyTitle: meta.KeyDefaultTitle, } // AddDefaultValues enriches the given meta data with its default values. func (cfg *myConfig) AddDefaultValues(m *meta.Meta) *meta.Meta { if cfg == nil { @@ -170,11 +169,11 @@ } result := m cfg.mx.RLock() for k, d := range defaultKeys { if _, ok := result.Get(k); !ok { - if val, ok2 := cfg.data.Get(d); ok2 && val != "" { + if val, ok := cfg.data.Get(d); ok && val != "" { if result == m { result = m.Clone() } result.Set(k, val) } @@ -196,89 +195,89 @@ cfg.mx.RUnlock() return val } // GetDefaultTitle returns the current value of the "default-title" key. -func (cfg *myConfig) GetDefaultTitle() string { return cfg.getString(api.KeyDefaultTitle) } +func (cfg *myConfig) GetDefaultTitle() string { return cfg.getString(meta.KeyDefaultTitle) } // GetDefaultRole returns the current value of the "default-role" key. -func (cfg *myConfig) GetDefaultRole() string { return cfg.getString(api.KeyDefaultRole) } +func (cfg *myConfig) GetDefaultRole() string { return cfg.getString(meta.KeyDefaultRole) } // GetDefaultSyntax returns the current value of the "default-syntax" key. -func (cfg *myConfig) GetDefaultSyntax() string { return cfg.getString(api.KeyDefaultSyntax) } +func (cfg *myConfig) GetDefaultSyntax() string { return cfg.getString(meta.KeyDefaultSyntax) } // GetDefaultLang returns the current value of the "default-lang" key. -func (cfg *myConfig) GetDefaultLang() string { return cfg.getString(api.KeyDefaultLang) } +func (cfg *myConfig) GetDefaultLang() string { return cfg.getString(meta.KeyDefaultLang) } // GetSiteName returns the current value of the "site-name" key. -func (cfg *myConfig) GetSiteName() string { return cfg.getString(api.KeySiteName) } +func (cfg *myConfig) GetSiteName() string { return cfg.getString(meta.KeySiteName) } // GetHomeZettel returns the value of the "home-zettel" key. func (cfg *myConfig) GetHomeZettel() id.Zid { - val := cfg.getString(api.KeyHomeZettel) + val := cfg.getString(meta.KeyHomeZettel) if homeZid, err := id.Parse(val); err == nil { return homeZid } cfg.mx.RLock() - val, _ = cfg.orig.Get(api.KeyHomeZettel) + val, _ = cfg.orig.Get(meta.KeyHomeZettel) homeZid, _ := id.Parse(val) cfg.mx.RUnlock() return homeZid } // GetDefaultVisibility returns the default value for zettel visibility. func (cfg *myConfig) GetDefaultVisibility() meta.Visibility { - val := cfg.getString(api.KeyDefaultVisibility) + val := cfg.getString(meta.KeyDefaultVisibility) if vis := meta.GetVisibility(val); vis != meta.VisibilityUnknown { return vis } cfg.mx.RLock() - val, _ = cfg.orig.Get(api.KeyDefaultVisibility) + val, _ = cfg.orig.Get(meta.KeyDefaultVisibility) vis := meta.GetVisibility(val) cfg.mx.RUnlock() return vis } // GetMaxTransclusions return the maximum number of indirect transclusions. func (cfg *myConfig) GetMaxTransclusions() int { cfg.mx.RLock() - val, ok := cfg.data.GetNumber(api.KeyMaxTransclusions) + val, ok := cfg.data.GetNumber(meta.KeyMaxTransclusions) cfg.mx.RUnlock() if ok && val > 0 { return val } return 1024 } // GetYAMLHeader returns the current value of the "yaml-header" key. -func (cfg *myConfig) GetYAMLHeader() bool { return cfg.getBool(api.KeyYAMLHeader) } +func (cfg *myConfig) GetYAMLHeader() bool { return cfg.getBool(meta.KeyYAMLHeader) } // GetMarkerExternal returns the current value of the "marker-external" key. func (cfg *myConfig) GetMarkerExternal() string { - return cfg.getString(api.KeyMarkerExternal) + return cfg.getString(meta.KeyMarkerExternal) } // GetFooterHTML returns HTML code that should be embedded into the footer // of each WebUI page. -func (cfg *myConfig) GetFooterHTML() string { return cfg.getString(api.KeyFooterHTML) } +func (cfg *myConfig) GetFooterHTML() string { return cfg.getString(meta.KeyFooterHTML) } // GetZettelFileSyntax returns the current value of the "zettel-file-syntax" key. func (cfg *myConfig) GetZettelFileSyntax() []string { cfg.mx.RLock() defer cfg.mx.RUnlock() - return cfg.data.GetListOrNil(api.KeyZettelFileSyntax) + return cfg.data.GetListOrNil(meta.KeyZettelFileSyntax) } // --- AuthConfig // GetExpertMode returns the current value of the "expert-mode" key -func (cfg *myConfig) GetExpertMode() bool { return cfg.getBool(api.KeyExpertMode) } +func (cfg *myConfig) GetExpertMode() bool { return cfg.getBool(meta.KeyExpertMode) } // GetVisibility returns the visibility value, or "login" if none is given. func (cfg *myConfig) GetVisibility(m *meta.Meta) meta.Visibility { - if val, ok := m.Get(api.KeyVisibility); ok { + if val, ok := m.Get(meta.KeyVisibility); ok { if vis := meta.GetVisibility(val); vis != meta.VisibilityUnknown { return vis } } return cfg.GetDefaultVisibility() } Index: kernel/impl/core.go ================================================================== --- kernel/impl/core.go +++ kernel/impl/core.go @@ -39,11 +39,10 @@ } func (cs *coreService) Initialize() { cs.mapRecover = make(map[string]recoverInfo) cs.descr = descriptionMap{ - kernel.CoreDebug: {"Debug mode", parseBool, false}, kernel.CoreGoArch: {"Go processor architecture", nil, false}, kernel.CoreGoOS: {"Go Operating System", nil, false}, kernel.CoreGoVersion: {"Go Version", nil, false}, kernel.CoreHostname: {"Host name", nil, false}, kernel.CorePort: { @@ -69,11 +68,10 @@ }), false, }, } cs.next = interfaceMap{ - kernel.CoreDebug: false, kernel.CoreGoArch: runtime.GOARCH, kernel.CoreGoOS: runtime.GOOS, kernel.CoreGoVersion: runtime.Version(), kernel.CoreHostname: "*unknown host*", kernel.CorePort: 0, @@ -82,11 +80,11 @@ if hn, err := os.Hostname(); err == nil { cs.next[kernel.CoreHostname] = hn } } -func (cs *coreService) Start(*myKernel) error { +func (cs *coreService) Start(kern *myKernel) error { cs.started = true return nil } func (cs *coreService) IsStarted() bool { return cs.started } func (cs *coreService) Stop(*myKernel) error { Index: kernel/impl/impl.go ================================================================== --- kernel/impl/impl.go +++ kernel/impl/impl.go @@ -30,10 +30,11 @@ type myKernel struct { // started bool wg sync.WaitGroup mx sync.RWMutex interrupt chan os.Signal + debug bool core coreService cfg configService auth authService box boxService @@ -106,11 +107,11 @@ // Wait for interrupt. sig := <-kern.interrupt if strSig := sig.String(); strSig != "" { kern.doLog("Shut down Zettelstore:", strSig) } - kern.doShutdown() + kern.shutdown() kern.wg.Done() }() kern.StartService(kernel.CoreService) if headline { @@ -121,15 +122,10 @@ kern.core.GetConfig(kernel.CoreGoVersion), kern.core.GetConfig(kernel.CoreGoOS), kern.core.GetConfig(kernel.CoreGoArch), )) kern.doLog("Licensed under the latest version of the EUPL (European Union Public License)") - if kern.core.GetConfig(kernel.CoreDebug).(bool) { - kern.doLog("-------------------------------------------------") - kern.doLog("WARNING: DEBUG MODE, DO NO USE THIS IN PRODUCTION") - kern.doLog("-------------------------------------------------") - } if kern.auth.GetConfig(kernel.AuthReadonly).(bool) { kern.doLog("Read-only mode") } } if lineServer { @@ -139,17 +135,25 @@ startLineServer(kern, listenAddr) } } } -func (kern *myKernel) doShutdown() { +func (kern *myKernel) shutdown() { kern.StopService(kernel.CoreService) // Will stop all other services. } func (kern *myKernel) WaitForShutdown() { kern.wg.Wait() } + +func (kern *myKernel) SetDebug(enable bool) bool { + kern.mx.Lock() + prevDebug := kern.debug + kern.debug = enable + kern.mx.Unlock() + return prevDebug +} // --- Shutdown operation ---------------------------------------------------- // Shutdown the service. Waits for all concurrent activity to stop. func (kern *myKernel) Shutdown(silent bool) { @@ -162,21 +166,21 @@ if s.silent { return "" } return "shutdown" } -func (*shutdownSignal) Signal() { /* Just a signal */ } +func (s *shutdownSignal) Signal() { /* Just a signal */ } // --- Log operation --------------------------------------------------------- // Log some activity. func (kern *myKernel) Log(args ...interface{}) { kern.mx.Lock() defer kern.mx.Unlock() kern.doLog(args...) } -func (*myKernel) doLog(args ...interface{}) { +func (kern *myKernel) doLog(args ...interface{}) { log.Println(args...) } // LogRecover outputs some information about the previous panic. func (kern *myKernel) LogRecover(name string, recoverInfo interface{}) bool { Index: kernel/impl/web.go ================================================================== --- kernel/impl/web.go +++ kernel/impl/web.go @@ -46,11 +46,11 @@ func(val string) interface{} { host, port, err := net.SplitHostPort(val) if err != nil { return nil } - if _, err = net.LookupPort("tcp", port); err != nil { + if _, err := net.LookupPort("tcp", port); err != nil { return nil } return net.JoinHostPort(host, port) }, true}, @@ -113,14 +113,14 @@ err := kern.web.setupServer(srvw, kern.box.manager, kern.auth.manager, kern.cfg.rtConfig) if err != nil { kern.doLog("Unable to create Web Server:", err) return err } - if kern.core.GetConfig(kernel.CoreDebug).(bool) { + if kern.debug { srvw.SetDebug() } - if err = srvw.Run(); err != nil { + if err := srvw.Run(); err != nil { kern.doLog("Unable to start Web Service:", err) return err } kern.doLog("Start Web Service:", listenAddr) ws.mxService.Lock() Index: kernel/kernel.go ================================================================== --- kernel/kernel.go +++ kernel/kernel.go @@ -27,10 +27,13 @@ // Start the service. Start(headline bool, lineServer bool) // WaitForShutdown blocks the call until Shutdown is called. WaitForShutdown() + + // SetDebug to enable/disable debug mode + SetDebug(enable bool) bool // Shutdown the service. Waits for all concurrent activities to stop. Shutdown(silent bool) // Log some activity. @@ -89,11 +92,10 @@ WebService ) // Constants for core service system keys. const ( - CoreDebug = "debug" CoreGoArch = "go-arch" CoreGoOS = "go-os" CoreGoVersion = "go-version" CoreHostname = "hostname" CorePort = "port" Index: parser/blob/blob.go ================================================================== --- parser/blob/blob.go +++ parser/blob/blob.go @@ -1,7 +1,7 @@ //----------------------------------------------------------------------------- -// Copyright (c) 2020-2021 Detlef Stern +// Copyright (c) 2020 Detlef Stern // // This file is part of zettelstore. // // Zettelstore is licensed under the latest version of the EUPL (European Union // Public License). Please see file LICENSE.txt for your rights and obligations @@ -10,11 +10,10 @@ // Package blob provides a parser of binary data. package blob import ( - "zettelstore.de/c/api" "zettelstore.de/z/ast" "zettelstore.de/z/domain/meta" "zettelstore.de/z/input" "zettelstore.de/z/parser" ) @@ -48,11 +47,11 @@ func parseBlocks(inp *input.Input, m *meta.Meta, syntax string) *ast.BlockListNode { if p := parser.Get(syntax); p != nil { syntax = p.Name } - title, _ := m.Get(api.KeyTitle) + title, _ := m.Get(meta.KeyTitle) return &ast.BlockListNode{List: []ast.BlockNode{ &ast.BLOBNode{ Title: title, Syntax: syntax, Blob: []byte(inp.Src), Index: parser/cleaner/cleaner.go ================================================================== --- parser/cleaner/cleaner.go +++ parser/cleaner/cleaner.go @@ -10,14 +10,14 @@ // Package cleaner provides funxtions to clean up the parsed AST. package cleaner import ( - "bytes" "strconv" + "strings" - "zettelstore.de/c/api" + "zettelstore.de/z/api" "zettelstore.de/z/ast" "zettelstore.de/z/encoder" "zettelstore.de/z/strfun" ) @@ -62,16 +62,16 @@ func (cv *cleanVisitor) visitHeading(hn *ast.HeadingNode) { if cv.doMark || hn == nil || hn.Inlines.IsEmpty() { return } if hn.Slug == "" { - var buf bytes.Buffer - _, err := cv.textEnc.WriteInlines(&buf, hn.Inlines) + var sb strings.Builder + _, err := cv.textEnc.WriteInlines(&sb, hn.Inlines) if err != nil { return } - hn.Slug = strfun.Slugify(buf.String()) + hn.Slug = strfun.Slugify(sb.String()) } if hn.Slug != "" { hn.Fragment = cv.addIdentifier(hn.Slug, hn) } } @@ -99,14 +99,14 @@ } if n, ok := cv.ids[id]; ok && n != node { prefix := id + "-" for count := 1; ; count++ { newID := prefix + strconv.Itoa(count) - if n2, ok2 := cv.ids[newID]; !ok2 || n2 == node { + if n, ok := cv.ids[newID]; !ok || n == node { cv.ids[newID] = node return newID } } } cv.ids[id] = node return id } Index: parser/markdown/markdown.go ================================================================== --- parser/markdown/markdown.go +++ parser/markdown/markdown.go @@ -18,11 +18,11 @@ gm "github.com/yuin/goldmark" gmAst "github.com/yuin/goldmark/ast" gmText "github.com/yuin/goldmark/text" - "zettelstore.de/c/api" + "zettelstore.de/z/api" "zettelstore.de/z/ast" "zettelstore.de/z/domain/meta" "zettelstore.de/z/encoder" "zettelstore.de/z/input" "zettelstore.de/z/parser" @@ -132,11 +132,11 @@ } func (p *mdP) acceptFencedCodeBlock(node *gmAst.FencedCodeBlock) *ast.VerbatimNode { var attrs *ast.Attributes if language := node.Language(p.source); len(language) > 0 { - attrs = attrs.Set("class", "language-"+cleanText(language, true)) + attrs = attrs.Set("class", "language-"+cleanText(string(language), true)) } return &ast.VerbatimNode{ Kind: ast.VerbatimProg, Attrs: attrs, Lines: p.acceptRawText(node), @@ -266,11 +266,11 @@ } ins := splitText(string(segment.Value(p.source))) result := make([]ast.InlineNode, 0, len(ins)+1) for _, in := range ins { if tn, ok := in.(*ast.TextNode); ok { - tn.Text = cleanText([]byte(tn.Text), true) + tn.Text = cleanText(tn.Text, true) } result = append(result, in) } if node.HardLineBreak() { result = append(result, &ast.BreakNode{Hard: true}) @@ -323,65 +323,71 @@ ']': true, '^': true, '_': true, '`': true, '{': true, '|': true, '}': true, '~': true, } // cleanText removes backslashes from TextNodes and expands entities -func cleanText(text []byte, cleanBS bool) string { +func cleanText(text string, cleanBS bool) string { lastPos := 0 - var buf bytes.Buffer + var sb strings.Builder for pos, ch := range text { if pos < lastPos { continue } if ch == '&' { - inp := input.NewInput([]byte(text[pos:])) + inp := input.NewInput(text[pos:]) if s, ok := inp.ScanEntity(); ok { - buf.Write(text[lastPos:pos]) - buf.WriteString(s) + sb.WriteString(text[lastPos:pos]) + sb.WriteString(s) lastPos = pos + inp.Pos } continue } if cleanBS && ch == '\\' && pos < len(text)-1 && ignoreAfterBS[text[pos+1]] { - buf.Write(text[lastPos:pos]) - buf.WriteByte(text[pos+1]) + sb.WriteString(text[lastPos:pos]) + sb.WriteByte(text[pos+1]) lastPos = pos + 2 } + } + if lastPos == 0 { + return text } if lastPos < len(text) { - buf.Write(text[lastPos:]) + sb.WriteString(text[lastPos:]) } - return buf.String() + return sb.String() } func (p *mdP) acceptCodeSpan(node *gmAst.CodeSpan) []ast.InlineNode { return []ast.InlineNode{ &ast.LiteralNode{ Kind: ast.LiteralProg, Attrs: nil, //TODO - Text: cleanCodeSpan(node.Text(p.source)), + Text: cleanCodeSpan(string(node.Text(p.source))), }, } } -func cleanCodeSpan(text []byte) string { - if len(text) == 0 { +func cleanCodeSpan(text string) string { + if text == "" { return "" } lastPos := 0 - var buf bytes.Buffer + var sb strings.Builder for pos, ch := range text { if ch == '\n' { - buf.Write(text[lastPos:pos]) + sb.WriteString(text[lastPos:pos]) if pos < len(text)-1 { - buf.WriteByte(' ') + sb.WriteByte(' ') } lastPos = pos + 1 } } - buf.Write(text[lastPos:]) - return buf.String() + if lastPos == 0 { + return text + } + sb.WriteString(text[lastPos:]) + return sb.String() } func (p *mdP) acceptEmphasis(node *gmAst.Emphasis) []ast.InlineNode { kind := ast.FormatEmph if node.Level == 2 { @@ -395,13 +401,13 @@ }, } } func (p *mdP) acceptLink(node *gmAst.Link) []ast.InlineNode { - ref := ast.ParseReference(cleanText(node.Destination, true)) + ref := ast.ParseReference(cleanText(string(node.Destination), true)) var attrs *ast.Attributes - if title := node.Title; len(title) > 0 { + if title := string(node.Title); len(title) > 0 { attrs = attrs.Set("title", cleanText(title, true)) } return []ast.InlineNode{ &ast.LinkNode{ Ref: ref, @@ -411,13 +417,13 @@ }, } } func (p *mdP) acceptImage(node *gmAst.Image) []ast.InlineNode { - ref := ast.ParseReference(cleanText(node.Destination, true)) + ref := ast.ParseReference(cleanText(string(node.Destination), true)) var attrs *ast.Attributes - if title := node.Title; len(title) > 0 { + if title := string(node.Title); len(title) > 0 { attrs = attrs.Set("title", cleanText(title, true)) } return []ast.InlineNode{ &ast.EmbedNode{ Material: &ast.ReferenceMaterialNode{Ref: ref}, @@ -427,31 +433,32 @@ } } func (p *mdP) flattenInlineList(node gmAst.Node) *ast.InlineListNode { iln := p.acceptInlineChildren(node) - var buf bytes.Buffer - _, err := p.textEnc.WriteInlines(&buf, iln) + var sb strings.Builder + _, err := p.textEnc.WriteInlines(&sb, iln) if err != nil { panic(err) } - if buf.Len() == 0 { + text := sb.String() + if text == "" { return nil } - return ast.CreateInlineListNode(&ast.TextNode{Text: buf.String()}) + return ast.CreateInlineListNode(&ast.TextNode{Text: text}) } func (p *mdP) acceptAutoLink(node *gmAst.AutoLink) []ast.InlineNode { - u := node.URL(p.source) + url := node.URL(p.source) if node.AutoLinkType == gmAst.AutoLinkEmail && - !bytes.HasPrefix(bytes.ToLower(u), []byte("mailto:")) { - u = append([]byte("mailto:"), u...) + !bytes.HasPrefix(bytes.ToLower(url), []byte("mailto:")) { + url = append([]byte("mailto:"), url...) } - ref := ast.ParseReference(cleanText(u, false)) + ref := ast.ParseReference(cleanText(string(url), false)) label := node.Label(p.source) if len(label) == 0 { - label = u + label = url } return []ast.InlineNode{ &ast.LinkNode{ Ref: ref, Inlines: ast.CreateInlineListNode(&ast.TextNode{Text: string(label)}), Index: parser/markdown/markdown_test.go ================================================================== --- parser/markdown/markdown_test.go +++ parser/markdown/markdown_test.go @@ -10,11 +10,11 @@ // Package markdown provides a parser for markdown. package markdown import ( - "bytes" + "strings" "testing" "zettelstore.de/z/ast" ) @@ -30,24 +30,24 @@ {"abc def", "TabcS Tdef"}, {"abc def ", "TabcS TdefS "}, {" abc def ", "S TabcS TdefS "}, } for i, tc := range testcases { - var buf bytes.Buffer + var sb strings.Builder for _, in := range splitText(tc.text) { switch n := in.(type) { case *ast.TextNode: - buf.WriteByte('T') - buf.WriteString(n.Text) + sb.WriteByte('T') + sb.WriteString(n.Text) case *ast.SpaceNode: - buf.WriteByte('S') - buf.WriteString(n.Lexeme) + sb.WriteByte('S') + sb.WriteString(n.Lexeme) default: - buf.WriteByte('Q') + sb.WriteByte('Q') } } - got := buf.String() + got := sb.String() if tc.exp != got { t.Errorf("TC=%d, text=%q, exp=%q, got=%q", i, tc.text, tc.exp, got) } } } Index: parser/none/none.go ================================================================== --- parser/none/none.go +++ parser/none/none.go @@ -10,20 +10,19 @@ // Package none provides a none-parser for meta data. package none import ( - "zettelstore.de/c/api" "zettelstore.de/z/ast" "zettelstore.de/z/domain/meta" "zettelstore.de/z/input" "zettelstore.de/z/parser" ) func init() { parser.Register(&parser.Info{ - Name: api.ValueSyntaxNone, + Name: meta.ValueSyntaxNone, AltNames: []string{}, IsTextParser: false, IsImageFormat: false, ParseBlocks: parseBlocks, ParseInlines: parseInlines, @@ -89,10 +88,10 @@ return ast.CreateInlineListNode( &ast.FormatNode{ Kind: ast.FormatSpan, Attrs: &ast.Attributes{Attrs: map[string]string{"class": "warning"}}, Inlines: ast.CreateInlineListNodeFromWords( - "parser.meta.ParseInlines:", "not", "possible", "("+string(inp.Src[0:inp.Pos])+")", + "parser.meta.ParseInlines:", "not", "possible", "("+inp.Src[0:inp.Pos]+")", ), }, ) } Index: parser/parser.go ================================================================== --- parser/parser.go +++ parser/parser.go @@ -13,11 +13,10 @@ import ( "fmt" "log" - "zettelstore.de/c/api" "zettelstore.de/z/ast" "zettelstore.de/z/config" "zettelstore.de/z/domain" "zettelstore.de/z/domain/meta" "zettelstore.de/z/input" @@ -39,11 +38,11 @@ } var registry = map[string]*Info{} // Register the parser (info) for later retrieval. -func Register(pi *Info) { +func Register(pi *Info) *Info { if _, ok := registry[pi.Name]; ok { panic(fmt.Sprintf("Parser %q already registered", pi.Name)) } registry[pi.Name] = pi for _, alt := range pi.AltNames { @@ -50,10 +49,11 @@ if _, ok := registry[alt]; ok { panic(fmt.Sprintf("Parser %q already registered", alt)) } registry[alt] = pi } + return pi } // GetSyntaxes returns a list of syntaxes implemented by all registered parsers. func GetSyntaxes() []string { result := make([]string, 0, len(registry)) @@ -106,11 +106,11 @@ } // ParseMetadata parses a string as Zettelmarkup, resulting in an inline slice. // Typically used to parse the title or other metadata of type Zettelmarkup. func ParseMetadata(value string) *ast.InlineListNode { - return ParseInlines(input.NewInput([]byte(value)), api.ValueSyntaxZmk) + return ParseInlines(input.NewInput(value), meta.ValueSyntaxZmk) } // ParseZettel parses the zettel based on the syntax. func ParseZettel(zettel domain.Zettel, syntax string, rtConfig config.Config) *ast.ZettelNode { m := zettel.Meta @@ -117,19 +117,19 @@ inhMeta := m if rtConfig != nil { inhMeta = rtConfig.AddDefaultValues(inhMeta) } if syntax == "" { - syntax, _ = inhMeta.Get(api.KeySyntax) + syntax, _ = inhMeta.Get(meta.KeySyntax) } parseMeta := inhMeta - if syntax == api.ValueSyntaxNone { + if syntax == meta.ValueSyntaxNone { parseMeta = m } return &ast.ZettelNode{ Meta: m, Content: zettel.Content, Zid: m.Zid, InhMeta: inhMeta, - Ast: ParseBlocks(input.NewInput(zettel.Content.AsBytes()), parseMeta, syntax), + Ast: ParseBlocks(input.NewInput(zettel.Content.AsString()), parseMeta, syntax), } } Index: parser/parser_test.go ================================================================== --- parser/parser_test.go +++ parser/parser_test.go @@ -12,11 +12,11 @@ package parser_test import ( "testing" - "zettelstore.de/c/api" + "zettelstore.de/z/domain/meta" "zettelstore.de/z/parser" _ "zettelstore.de/z/parser/blob" // Allow to use BLOB parser. _ "zettelstore.de/z/parser/markdown" // Allow to use markdown parser. _ "zettelstore.de/z/parser/none" // Allow to use none parser. @@ -41,17 +41,17 @@ {"jpeg", false, true}, {"jpg", false, true}, {"markdown", true, false}, {"md", true, false}, {"mustache", false, false}, - {api.ValueSyntaxNone, false, false}, + {meta.ValueSyntaxNone, false, false}, {"plain", false, false}, {"png", false, true}, {"svg", false, true}, {"text", false, false}, {"txt", false, false}, - {api.ValueSyntaxZmk, true, false}, + {meta.ValueSyntaxZmk, true, false}, } for _, tc := range testCases { delete(syntaxSet, tc.syntax) if got := parser.IsTextParser(tc.syntax); got != tc.text { t.Errorf("Syntax %q is text: %v, but got %v", tc.syntax, tc.text, got) Index: parser/plain/plain.go ================================================================== --- parser/plain/plain.go +++ parser/plain/plain.go @@ -85,11 +85,11 @@ posL := inp.Pos if inp.Ch == input.EOS { return lines } inp.SkipToEOL() - lines = append(lines, string(inp.Src[posL:inp.Pos])) + lines = append(lines, inp.Src[posL:inp.Pos]) } } func parseInlines(inp *input.Input, syntax string) *ast.InlineListNode { return doParseInlines(inp, syntax, ast.LiteralProg) @@ -100,11 +100,11 @@ func doParseInlines(inp *input.Input, syntax string, kind ast.LiteralKind) *ast.InlineListNode { inp.SkipToEOL() return ast.CreateInlineListNode(&ast.LiteralNode{ Kind: kind, Attrs: &ast.Attributes{Attrs: map[string]string{"": syntax}}, - Text: string(inp.Src[0:inp.Pos]), + Text: inp.Src[0:inp.Pos], }) } func parseSVGBlocks(inp *input.Input, _ *meta.Meta, syntax string) *ast.BlockListNode { iln := parseSVGInlines(inp, syntax) @@ -129,12 +129,12 @@ func scanSVG(inp *input.Input) string { for input.IsSpace(inp.Ch) { inp.Next() } - svgSrc := string(inp.Src[inp.Pos:]) + svgSrc := inp.Src[inp.Pos:] if !strings.HasPrefix(svgSrc, " return svgSrc } Index: parser/zettelmark/block.go ================================================================== --- parser/zettelmark/block.go +++ parser/zettelmark/block.go @@ -191,11 +191,11 @@ inp.SetPos(posL) case input.EOS: return nil, false } inp.SkipToEOL() - rn.Lines = append(rn.Lines, string(inp.Src[posL:inp.Pos])) + rn.Lines = append(rn.Lines, inp.Src[posL:inp.Pos]) } } var runeRegion = map[rune]ast.RegionKind{ ':': ast.RegionSpan, @@ -272,23 +272,23 @@ } // parseHeading parses a head line. func (cp *zmkP) parseHeading() (hn *ast.HeadingNode, success bool) { inp := cp.inp - delims := cp.countDelim(inp.Ch) - if delims < 3 { + lvl := cp.countDelim(inp.Ch) + if lvl < 3 { return nil, false + } + if lvl > 7 { + lvl = 7 } if inp.Ch != ' ' { return nil, false } inp.Next() cp.skipSpace() - if delims > 7 { - delims = 7 - } - hn = &ast.HeadingNode{Level: delims - 2, Inlines: &ast.InlineListNode{}} + hn = &ast.HeadingNode{Level: lvl - 1, Inlines: &ast.InlineListNode{}} for { if input.IsEOLEOS(inp.Ch) { return hn, true } in := cp.parseInline() @@ -539,11 +539,11 @@ descrPos := len(cp.descrl.Descriptions[defPos].Descriptions) - 1 lbn := cp.descrl.Descriptions[defPos].Descriptions[descrPos] if lpn, ok := lbn[len(lbn)-1].(*ast.ParaNode); ok { lpn.Inlines.Append(pn.Inlines.List...) } else { - descrPos = len(cp.descrl.Descriptions[defPos].Descriptions) - 1 + descrPos := len(cp.descrl.Descriptions[defPos].Descriptions) - 1 cp.descrl.Descriptions[defPos].Descriptions[descrPos] = append(cp.descrl.Descriptions[defPos].Descriptions[descrPos], pn) } return true } Index: parser/zettelmark/inline.go ================================================================== --- parser/zettelmark/inline.go +++ parser/zettelmark/inline.go @@ -10,12 +10,12 @@ // Package zettelmark provides a parser for zettelmarkup. package zettelmark import ( - "bytes" "fmt" + "strings" "zettelstore.de/z/ast" "zettelstore.de/z/input" ) @@ -68,11 +68,11 @@ } case '#': return cp.parseTag() case '%': in, success = cp.parseComment() - case '/', '_', '*', '>', '~', '\'', '^', ',', '<', '"', ';', ':': + case '/', '*', '_', '~', '\'', '^', ',', '<', '"', ';', ':': in, success = cp.parseFormat() case '+', '`', '=', runeModGrave: in, success = cp.parseLiteral() case '\\': return cp.parseBackslash() @@ -98,12 +98,12 @@ for { inp.Next() switch inp.Ch { // 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', ' ', '\t', '[', ']', '{', '}', '(', ')', '|', '#', '%', '_', '*', '>', '~', '\'', '^', ',', '<', '"', ';', ':', '+', '`', runeModGrave, '=', '\\', '-', '&': - return &ast.TextNode{Text: string(inp.Src[pos:inp.Pos])} + case input.EOS, '\n', '\r', ' ', '\t', '[', ']', '{', '}', '(', ')', '|', '#', '%', '/', '*', '_', '~', '\'', '^', ',', '<', '"', ';', ':', '+', '`', runeModGrave, '=', '\\', '-', '&': + return &ast.TextNode{Text: inp.Src[pos:inp.Pos]} } } } func (cp *zmkP) parseTextBackslash() *ast.TextNode { @@ -132,11 +132,11 @@ inp.Next() return &ast.TextNode{Text: "\u00a0"} } pos := inp.Pos inp.Next() - return &ast.TextNode{Text: string(inp.Src[pos:inp.Pos])} + return &ast.TextNode{Text: inp.Src[pos:inp.Pos]} } func (cp *zmkP) parseSpace() *ast.SpaceNode { inp := cp.inp pos := inp.Pos @@ -143,11 +143,11 @@ for { inp.Next() switch inp.Ch { case ' ', '\t': default: - return &ast.SpaceNode{Lexeme: string(inp.Src[pos:inp.Pos])} + return &ast.SpaceNode{Lexeme: inp.Src[pos:inp.Pos]} } } } func (cp *zmkP) parseSoftBreak() *ast.BreakNode { @@ -209,11 +209,11 @@ cp.skipSpace() pos = inp.Pos if !cp.readReferenceToClose(closeCh) { return "", nil, false } - ref = string(inp.Src[pos:inp.Pos]) + ref = inp.Src[pos:inp.Pos] inp.Next() if inp.Ch != closeCh { return "", nil, false } inp.Next() @@ -304,11 +304,11 @@ ins, ok := cp.parseLinkLikeRest() if !ok { return nil, false } attrs := cp.parseAttributes(false) - return &ast.CiteNode{Key: string(inp.Src[pos:posL]), Inlines: ins, Attrs: attrs}, true + return &ast.CiteNode{Key: inp.Src[pos:posL], Inlines: ins, Attrs: attrs}, true } func (cp *zmkP) parseFootnote() (*ast.FootnoteNode, bool) { cp.inp.Next() iln, ok := cp.parseLinkLikeRest() @@ -366,11 +366,11 @@ if !isNameRune(inp.Ch) { return nil, false } inp.Next() } - mn := &ast.MarkNode{Text: string(inp.Src[pos:inp.Pos])} + mn := &ast.MarkNode{Text: inp.Src[pos:inp.Pos]} inp.Next() return mn, true } func (cp *zmkP) parseTag() ast.InlineNode { @@ -380,13 +380,13 @@ pos := inp.Pos for isNameRune(inp.Ch) { inp.Next() } if pos == inp.Pos || inp.Ch == '#' { - return &ast.TextNode{Text: string(inp.Src[posH:inp.Pos])} + return &ast.TextNode{Text: inp.Src[posH:inp.Pos]} } - return &ast.TagNode{Tag: string(inp.Src[pos:inp.Pos])} + return &ast.TagNode{Tag: inp.Src[pos:inp.Pos]} } func (cp *zmkP) parseComment() (res *ast.LiteralNode, success bool) { inp := cp.inp inp.Next() @@ -398,22 +398,21 @@ } cp.skipSpace() pos := inp.Pos for { if input.IsEOLEOS(inp.Ch) { - return &ast.LiteralNode{Kind: ast.LiteralComment, Text: string(inp.Src[pos:inp.Pos])}, true + return &ast.LiteralNode{Kind: ast.LiteralComment, Text: inp.Src[pos:inp.Pos]}, true } inp.Next() } } var mapRuneFormat = map[rune]ast.FormatKind{ - '/': ast.FormatEmphDeprecated, - '_': ast.FormatEmph, - '*': ast.FormatStrong, - '>': ast.FormatInsert, - '~': ast.FormatDelete, + '/': ast.FormatItalic, + '*': ast.FormatBold, + '_': ast.FormatUnder, + '~': ast.FormatStrike, '\'': ast.FormatMonospace, '^': ast.FormatSuper, ',': ast.FormatSub, '<': ast.FormatQuotation, '"': ast.FormatQuote, @@ -445,11 +444,11 @@ fn.Attrs = cp.parseAttributes(false) return fn, true } fn.Inlines.Append(&ast.TextNode{Text: string(fch)}) } else if in := cp.parseInline(); in != nil { - if _, ok = in.(*ast.BreakNode); ok && input.IsEOLEOS(inp.Ch) { + if _, ok := in.(*ast.BreakNode); ok && input.IsEOLEOS(inp.Ch) { return nil, false } fn.Inlines.Append(in) } } @@ -473,28 +472,28 @@ if inp.Ch != fch { return nil, false } fn := &ast.LiteralNode{Kind: kind} inp.Next() - var buf bytes.Buffer + 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() fn.Attrs = cp.parseAttributes(false) - fn.Text = buf.String() + fn.Text = sb.String() return fn, true } - buf.WriteRune(fch) + sb.WriteRune(fch) inp.Next() } else { tn := cp.parseText() - buf.WriteString(tn.Text) + sb.WriteString(tn.Text) } } } func (cp *zmkP) parseNdash() (res *ast.TextNode, success bool) { Index: parser/zettelmark/post-processor.go ================================================================== --- parser/zettelmark/post-processor.go +++ parser/zettelmark/post-processor.go @@ -61,10 +61,11 @@ case *ast.CiteNode: return pp case *ast.FootnoteNode: return pp case *ast.FormatNode: + pp.visitFormat(n) return pp } return nil } @@ -109,11 +110,11 @@ } } pp.visitTableRows(tn, width) } -func (*postProcessor) visitTableHeader(tn *ast.TableNode) { +func (pp *postProcessor) visitTableHeader(tn *ast.TableNode) { for pos, cell := range tn.Header { ins := cell.Inlines.List if len(ins) == 0 { continue } @@ -214,10 +215,26 @@ if tn, ok := ins[0].(*ast.TextNode); ok && len(tn.Text) > 0 { return tn } return nil } + +var mapSemantic = map[ast.FormatKind]ast.FormatKind{ + ast.FormatItalic: ast.FormatEmph, + ast.FormatBold: ast.FormatStrong, + ast.FormatUnder: ast.FormatInsert, + ast.FormatStrike: ast.FormatDelete, +} + +func (pp *postProcessor) visitFormat(fn *ast.FormatNode) { + if fn.Attrs.HasDefault() { + if newKind, ok := mapSemantic[fn.Kind]; ok { + fn.Attrs.RemoveDefault() + fn.Kind = newKind + } + } +} func (pp *postProcessor) visitBlockList(bln *ast.BlockListNode) { if bln == nil { return } @@ -377,13 +394,35 @@ for fromPos < maxPos { ins[toPos] = ins[fromPos] fromPos++ switch in := ins[toPos].(type) { case *ast.TextNode: - fromPos = processTextNode(ins, maxPos, in, fromPos) + for fromPos < maxPos { + if tn, ok := ins[fromPos].(*ast.TextNode); ok { + in.Text = in.Text + tn.Text + fromPos++ + } else { + break + } + } case *ast.SpaceNode: - again, fromPos = pp.processSpaceNode(ins, maxPos, in, toPos, again, fromPos) + if fromPos < maxPos { + switch nn := ins[fromPos].(type) { + case *ast.BreakNode: + if len(in.Lexeme) > 1 { + nn.Hard = true + ins[toPos] = nn + fromPos++ + } + case *ast.TextNode: + if pp.inVerse { + ins[toPos] = &ast.TextNode{Text: strings.Repeat("\u00a0", len(in.Lexeme)) + nn.Text} + fromPos++ + again = true + } + } + } case *ast.BreakNode: if pp.inVerse { in.Hard = true } } @@ -390,51 +429,12 @@ toPos++ } return again, toPos } -func processTextNode(ins []ast.InlineNode, maxPos int, in *ast.TextNode, fromPos int) int { - for fromPos < maxPos { - if tn, ok := ins[fromPos].(*ast.TextNode); ok { - in.Text = in.Text + tn.Text - fromPos++ - } else { - break - } - } - return fromPos -} - -func (pp *postProcessor) processSpaceNode( - ins []ast.InlineNode, maxPos int, in *ast.SpaceNode, toPos int, again bool, fromPos int, -) (bool, int) { - if fromPos < maxPos { - switch nn := ins[fromPos].(type) { - case *ast.BreakNode: - if len(in.Lexeme) > 1 { - nn.Hard = true - ins[toPos] = nn - fromPos++ - } - case *ast.TextNode: - if pp.inVerse { - ins[toPos] = &ast.TextNode{Text: strings.Repeat("\u00a0", len(in.Lexeme)) + nn.Text} - fromPos++ - again = true - } - case *ast.LiteralNode: - if nn.Kind == ast.LiteralComment { - ins[toPos] = ins[fromPos] - fromPos++ - } - } - } - return again, fromPos -} - // processInlineSliceTail removes empty text nodes, breaks and spaces at the end. -func (*postProcessor) processInlineSliceTail(iln *ast.InlineListNode, toPos int) int { +func (pp *postProcessor) processInlineSliceTail(iln *ast.InlineListNode, toPos int) int { ins := iln.List for toPos > 0 { switch n := ins[toPos-1].(type) { case *ast.TextNode: if len(n.Text) > 0 { @@ -449,11 +449,11 @@ ins[toPos] = nil // Kill node to enable garbage collection } return toPos } -func (*postProcessor) processInlineListInplace(iln *ast.InlineListNode) { +func (pp *postProcessor) processInlineListInplace(iln *ast.InlineListNode) { for _, in := range iln.List { if n, ok := in.(*ast.TextNode); ok { if n.Text == "..." { n.Text = "\u2026" } else if len(n.Text) == 4 && strings.IndexByte(",;:!?", n.Text[3]) >= 0 && n.Text[:3] == "..." { Index: parser/zettelmark/zettelmark.go ================================================================== --- parser/zettelmark/zettelmark.go +++ parser/zettelmark/zettelmark.go @@ -12,20 +12,19 @@ package zettelmark import ( "unicode" - "zettelstore.de/c/api" "zettelstore.de/z/ast" "zettelstore.de/z/domain/meta" "zettelstore.de/z/input" "zettelstore.de/z/parser" ) func init() { parser.Register(&parser.Info{ - Name: api.ValueSyntaxZmk, + Name: meta.ValueSyntaxZmk, AltNames: nil, IsTextParser: true, IsImageFormat: false, ParseBlocks: parseBlocks, ParseInlines: parseInlines, @@ -105,11 +104,11 @@ if sameLine { return false } fallthrough case ' ', '}': - updateAttrs(attrs, key, string(inp.Src[posV:inp.Pos])) + updateAttrs(attrs, key, inp.Src[posV:inp.Pos]) return true } inp.Next() } } @@ -167,11 +166,11 @@ pos := inp.Pos for isNameRune(inp.Ch) { inp.Next() } if pos < inp.Pos { - return &ast.Attributes{Attrs: map[string]string{"": string(inp.Src[pos:inp.Pos])}} + return &ast.Attributes{Attrs: map[string]string{"": inp.Src[pos:inp.Pos]}} } // No immediate name: skip spaces cp.skipSpace() } @@ -215,11 +214,11 @@ inp.Next() } if posC == inp.Pos { return false } - updateAttrs(attrs, "class", string(inp.Src[posC:inp.Pos])) + updateAttrs(attrs, "class", inp.Src[posC:inp.Pos]) case '=': delete(attrs, "") if !cp.parseAttributeValue("", attrs, sameLine) { return false } Index: parser/zettelmark/zettelmark_test.go ================================================================== --- parser/zettelmark/zettelmark_test.go +++ parser/zettelmark/zettelmark_test.go @@ -10,18 +10,17 @@ // Package zettelmark_test provides some tests for the zettelmarkup parser. package zettelmark_test import ( - "bytes" "fmt" "sort" "strings" "testing" - "zettelstore.de/c/api" "zettelstore.de/z/ast" + "zettelstore.de/z/domain/meta" "zettelstore.de/z/input" "zettelstore.de/z/parser" // Ensure that the text encoder is available. // Needed by parser/cleanup.go @@ -46,12 +45,12 @@ t.Helper() for tcn, tc := range tcs { t.Run(fmt.Sprintf("TC=%02d,src=%q", tcn, tc.source), func(st *testing.T) { st.Helper() - inp := input.NewInput([]byte(tc.source)) - bns := parser.ParseBlocks(inp, nil, api.ValueSyntaxZmk) + inp := input.NewInput(tc.source) + bns := parser.ParseBlocks(inp, nil, meta.ValueSyntaxZmk) var tv TestVisitor ast.Walk(&tv, bns) got := tv.String() if tc.want != got { st.Errorf("\nwant=%q\n got=%q", tc.want, got) @@ -96,11 +95,10 @@ {"...:", "(PARA \u2026:)"}, {"...!", "(PARA \u2026!)"}, {"...?", "(PARA \u2026?)"}, {"...-", "(PARA ...-)"}, {"a...b", "(PARA a...b)"}, - // {"http://a, http://b", "(PARA http://a, SP http://b)"}, }) } func TestSpace(t *testing.T) { t.Parallel() @@ -292,30 +290,25 @@ {"%% a", "(PARA {% a})"}, {"%%% a", "(PARA {% a})"}, {"%% % a", "(PARA {% % a})"}, {"%%a", "(PARA {% a})"}, {"a%%b", "(PARA a {% b})"}, - {"a %%b", "(PARA a {% b})"}, + {"a %%b", "(PARA a SP {% b})"}, {" %%b", "(PARA {% b})"}, {"%%b ", "(PARA {% b })"}, {"100%", "(PARA 100%)"}, }) } func TestFormat(t *testing.T) { t.Parallel() - // Not for Insert / '>', because collision with quoted list - for _, ch := range []string{"_", "*", "~", "'", "^", ",", "<", "\"", ";", ":"} { + for _, ch := range []string{"/", "*", "_", "~", "'", "^", ",", "<", "\"", ";", ":"} { checkTcs(t, replace(ch, TestCases{ {"$", "(PARA $)"}, {"$$", "(PARA $$)"}, {"$$$", "(PARA $$$)"}, {"$$$$", "(PARA {$})"}, - })) - } - for _, ch := range []string{"_", "*", ">", "~", "'", "^", ",", "<", "\"", ";", ":"} { - checkTcs(t, replace(ch, TestCases{ {"$$a$$", "(PARA {$ a})"}, {"$$a$$$", "(PARA {$ a} $)"}, {"$$$a$$", "(PARA {$ $a})"}, {"$$$a$$$", "(PARA {$ $a} $)"}, {"$\\$", "(PARA $$)"}, @@ -328,13 +321,13 @@ {"$$a\n\na$$", "(PARA $$a)(PARA a$$)"}, {"$$a$${go}", "(PARA {$ a}[ATTR go])"}, })) } checkTcs(t, TestCases{ - {"__****__", "(PARA {_ {*}})"}, - {"__**a**__", "(PARA {_ {* a}})"}, - {"__**__**", "(PARA __ {* __})"}, + {"//****//", "(PARA {/ {*}})"}, + {"//**a**//", "(PARA {/ {* a}})"}, + {"//**//**", "(PARA // {* //})"}, }) } func TestLiteral(t *testing.T) { t.Parallel() @@ -366,14 +359,14 @@ } func TestMixFormatCode(t *testing.T) { t.Parallel() checkTcs(t, TestCases{ - {"__abc__\n**def**", "(PARA {_ abc} SB {* def})"}, + {"//abc//\n**def**", "(PARA {/ abc} SB {* def})"}, {"++abc++\n==def==", "(PARA {+ abc} SB {= def})"}, - {"__abc__\n==def==", "(PARA {_ abc} SB {= def})"}, - {"__abc__\n``def``", "(PARA {_ abc} SB {` def})"}, + {"//abc//\n==def==", "(PARA {/ abc} SB {= def})"}, + {"//abc//\n``def``", "(PARA {/ abc} SB {` def})"}, {"\"\"ghi\"\"\n::abc::\n``def``\n", "(PARA {\" ghi} SB {: abc} SB {` def})"}, }) } func TestNDash(t *testing.T) { @@ -462,32 +455,32 @@ {"=h", "(PARA =h)"}, {"= h", "(PARA = SP h)"}, {"==h", "(PARA ==h)"}, {"== h", "(PARA == SP h)"}, {"===h", "(PARA ===h)"}, - {"=== h", "(H1 h #h)"}, - {"=== h", "(H1 h #h)"}, - {"==== h", "(H2 h #h)"}, - {"===== h", "(H3 h #h)"}, - {"====== h", "(H4 h #h)"}, - {"======= h", "(H5 h #h)"}, - {"======== h", "(H5 h #h)"}, + {"=== h", "(H2 h #h)"}, + {"=== h", "(H2 h #h)"}, + {"==== h", "(H3 h #h)"}, + {"===== h", "(H4 h #h)"}, + {"====== h", "(H5 h #h)"}, + {"======= h", "(H6 h #h)"}, + {"======== h", "(H6 h #h)"}, {"=", "(PARA =)"}, - {"=== h=__=a__", "(H1 h= {_ =a} #h-a)"}, + {"=== h=//=a//", "(H2 h= {/ =a} #h-a)"}, {"=\n", "(PARA =)"}, {"a=", "(PARA a=)"}, {" =", "(PARA =)"}, - {"=== h\na", "(H1 h #h)(PARA a)"}, - {"=== h i {-}", "(H1 h SP i #h-i)[ATTR -]"}, - {"=== h {{a}}", "(H1 h SP (EMBED a) #h)"}, - {"=== h{{a}}", "(H1 h (EMBED a) #h)"}, - {"=== {{a}}", "(H1 (EMBED a))"}, - {"=== h {{a}}{-}", "(H1 h SP (EMBED a)[ATTR -] #h)"}, - {"=== h {{a}} {-}", "(H1 h SP (EMBED a) #h)[ATTR -]"}, - {"=== h {-}{{a}}", "(H1 h #h)[ATTR -]"}, - {"=== h{id=abc}", "(H1 h #h)[ATTR id=abc]"}, - {"=== h\n=== h", "(H1 h #h)(H1 h #h-1)"}, + {"=== h\na", "(H2 h #h)(PARA a)"}, + {"=== h i {-}", "(H2 h SP i #h-i)[ATTR -]"}, + {"=== h {{a}}", "(H2 h SP (EMBED a) #h)"}, + {"=== h{{a}}", "(H2 h (EMBED a) #h)"}, + {"=== {{a}}", "(H2 (EMBED a))"}, + {"=== h {{a}}{-}", "(H2 h SP (EMBED a)[ATTR -] #h)"}, + {"=== h {{a}} {-}", "(H2 h SP (EMBED a) #h)[ATTR -]"}, + {"=== h {-}{{a}}", "(H2 h #h)[ATTR -]"}, + {"=== h{id=abc}", "(H2 h #h)[ATTR id=abc]"}, + {"=== h\n=== h", "(H2 h #h)(H2 h #h-1)"}, }) } func TestHRule(t *testing.T) { t.Parallel() @@ -677,196 +670,196 @@ // -------------------------------------------------------------------------- // TestVisitor serializes the abstract syntax tree to a string. type TestVisitor struct { - buf bytes.Buffer + b strings.Builder } -func (tv *TestVisitor) String() string { return tv.buf.String() } +func (tv *TestVisitor) String() string { return tv.b.String() } func (tv *TestVisitor) Visit(node ast.Node) ast.Visitor { switch n := node.(type) { case *ast.InlineListNode: tv.visitInlineList(n) case *ast.ParaNode: - tv.buf.WriteString("(PARA") + tv.b.WriteString("(PARA") ast.Walk(tv, n.Inlines) - tv.buf.WriteByte(')') + tv.b.WriteByte(')') case *ast.VerbatimNode: code, ok := mapVerbatimKind[n.Kind] if !ok { panic(fmt.Sprintf("Unknown verbatim code %v", n.Kind)) } - tv.buf.WriteString(code) + tv.b.WriteString(code) for _, line := range n.Lines { - tv.buf.WriteByte('\n') - tv.buf.WriteString(line) + tv.b.WriteByte('\n') + tv.b.WriteString(line) } - tv.buf.WriteByte(')') + tv.b.WriteByte(')') tv.visitAttributes(n.Attrs) case *ast.RegionNode: code, ok := mapRegionKind[n.Kind] if !ok { panic(fmt.Sprintf("Unknown region code %v", n.Kind)) } - tv.buf.WriteString(code) + tv.b.WriteString(code) if n.Blocks != nil && len(n.Blocks.List) > 0 { - tv.buf.WriteByte(' ') + tv.b.WriteByte(' ') ast.Walk(tv, n.Blocks) } if n.Inlines != nil { - tv.buf.WriteString(" (LINE") + tv.b.WriteString(" (LINE") ast.Walk(tv, n.Inlines) - tv.buf.WriteByte(')') + tv.b.WriteByte(')') } - tv.buf.WriteByte(')') + tv.b.WriteByte(')') tv.visitAttributes(n.Attrs) case *ast.HeadingNode: - fmt.Fprintf(&tv.buf, "(H%d", n.Level) + fmt.Fprintf(&tv.b, "(H%d", n.Level) ast.Walk(tv, n.Inlines) if n.Fragment != "" { - tv.buf.WriteString(" #") - tv.buf.WriteString(n.Fragment) + tv.b.WriteString(" #") + tv.b.WriteString(n.Fragment) } - tv.buf.WriteByte(')') + tv.b.WriteByte(')') tv.visitAttributes(n.Attrs) case *ast.HRuleNode: - tv.buf.WriteString("(HR)") + tv.b.WriteString("(HR)") tv.visitAttributes(n.Attrs) case *ast.NestedListNode: - tv.buf.WriteString(mapNestedListKind[n.Kind]) + tv.b.WriteString(mapNestedListKind[n.Kind]) for _, item := range n.Items { - tv.buf.WriteString(" {") + tv.b.WriteString(" {") ast.WalkItemSlice(tv, item) - tv.buf.WriteByte('}') + tv.b.WriteByte('}') } - tv.buf.WriteByte(')') + tv.b.WriteByte(')') case *ast.DescriptionListNode: - tv.buf.WriteString("(DL") + tv.b.WriteString("(DL") for _, def := range n.Descriptions { - tv.buf.WriteString(" (DT") + tv.b.WriteString(" (DT") ast.Walk(tv, def.Term) - tv.buf.WriteByte(')') + tv.b.WriteByte(')') for _, b := range def.Descriptions { - tv.buf.WriteString(" (DD ") + tv.b.WriteString(" (DD ") ast.WalkDescriptionSlice(tv, b) - tv.buf.WriteByte(')') + tv.b.WriteByte(')') } } - tv.buf.WriteByte(')') + tv.b.WriteByte(')') case *ast.TableNode: - tv.buf.WriteString("(TAB") + tv.b.WriteString("(TAB") if len(n.Header) > 0 { - tv.buf.WriteString(" (TR") + tv.b.WriteString(" (TR") for _, cell := range n.Header { - tv.buf.WriteString(" (TH") - tv.buf.WriteString(alignString[cell.Align]) + tv.b.WriteString(" (TH") + tv.b.WriteString(alignString[cell.Align]) ast.Walk(tv, cell.Inlines) - tv.buf.WriteString(")") + tv.b.WriteString(")") } - tv.buf.WriteString(")") + tv.b.WriteString(")") } if len(n.Rows) > 0 { - tv.buf.WriteString(" ") - for _, row := range n.Rows { - tv.buf.WriteString("(TR") - for i, cell := range row { - if i == 0 { - tv.buf.WriteString(" ") - } - tv.buf.WriteString("(TD") - tv.buf.WriteString(alignString[cell.Align]) - ast.Walk(tv, cell.Inlines) - tv.buf.WriteString(")") - } - tv.buf.WriteString(")") - } - } - tv.buf.WriteString(")") - case *ast.BLOBNode: - tv.buf.WriteString("(BLOB ") - tv.buf.WriteString(n.Syntax) - tv.buf.WriteString(")") - case *ast.TextNode: - tv.buf.WriteString(n.Text) - case *ast.TagNode: - tv.buf.WriteByte('#') - tv.buf.WriteString(n.Tag) - tv.buf.WriteByte('#') - case *ast.SpaceNode: - if len(n.Lexeme) == 1 { - tv.buf.WriteString("SP") - } else { - fmt.Fprintf(&tv.buf, "SP%d", len(n.Lexeme)) + tv.b.WriteString(" ") + for _, row := range n.Rows { + tv.b.WriteString("(TR") + for i, cell := range row { + if i == 0 { + tv.b.WriteString(" ") + } + tv.b.WriteString("(TD") + tv.b.WriteString(alignString[cell.Align]) + ast.Walk(tv, cell.Inlines) + tv.b.WriteString(")") + } + tv.b.WriteString(")") + } + } + tv.b.WriteString(")") + case *ast.BLOBNode: + tv.b.WriteString("(BLOB ") + tv.b.WriteString(n.Syntax) + tv.b.WriteString(")") + case *ast.TextNode: + tv.b.WriteString(n.Text) + case *ast.TagNode: + tv.b.WriteByte('#') + tv.b.WriteString(n.Tag) + tv.b.WriteByte('#') + case *ast.SpaceNode: + if len(n.Lexeme) == 1 { + tv.b.WriteString("SP") + } else { + fmt.Fprintf(&tv.b, "SP%d", len(n.Lexeme)) } case *ast.BreakNode: if n.Hard { - tv.buf.WriteString("HB") + tv.b.WriteString("HB") } else { - tv.buf.WriteString("SB") + tv.b.WriteString("SB") } case *ast.LinkNode: - fmt.Fprintf(&tv.buf, "(LINK %v", n.Ref) + fmt.Fprintf(&tv.b, "(LINK %v", n.Ref) ast.Walk(tv, n.Inlines) - tv.buf.WriteByte(')') + tv.b.WriteByte(')') tv.visitAttributes(n.Attrs) case *ast.EmbedNode: switch m := n.Material.(type) { case *ast.ReferenceMaterialNode: - fmt.Fprintf(&tv.buf, "(EMBED %v", m.Ref) + fmt.Fprintf(&tv.b, "(EMBED %v", m.Ref) if n.Inlines != nil { ast.Walk(tv, n.Inlines) } - tv.buf.WriteByte(')') + tv.b.WriteByte(')') tv.visitAttributes(n.Attrs) case *ast.BLOBMaterialNode: panic("TODO: zmktest blob") default: panic(fmt.Sprintf("Unknown material type %t for %v", n.Material, n.Material)) } case *ast.CiteNode: - fmt.Fprintf(&tv.buf, "(CITE %s", n.Key) + fmt.Fprintf(&tv.b, "(CITE %s", n.Key) if n.Inlines != nil { ast.Walk(tv, n.Inlines) } - tv.buf.WriteByte(')') + tv.b.WriteByte(')') tv.visitAttributes(n.Attrs) case *ast.FootnoteNode: - tv.buf.WriteString("(FN") + tv.b.WriteString("(FN") ast.Walk(tv, n.Inlines) - tv.buf.WriteByte(')') + tv.b.WriteByte(')') tv.visitAttributes(n.Attrs) case *ast.MarkNode: - tv.buf.WriteString("(MARK") + tv.b.WriteString("(MARK") if n.Text != "" { - tv.buf.WriteString(" \"") - tv.buf.WriteString(n.Text) - tv.buf.WriteByte('"') + tv.b.WriteString(" \"") + tv.b.WriteString(n.Text) + tv.b.WriteByte('"') } if n.Fragment != "" { - tv.buf.WriteString(" #") - tv.buf.WriteString(n.Fragment) + tv.b.WriteString(" #") + tv.b.WriteString(n.Fragment) } - tv.buf.WriteByte(')') + tv.b.WriteByte(')') case *ast.FormatNode: - fmt.Fprintf(&tv.buf, "{%c", mapFormatKind[n.Kind]) + fmt.Fprintf(&tv.b, "{%c", mapFormatKind[n.Kind]) ast.Walk(tv, n.Inlines) - tv.buf.WriteByte('}') + tv.b.WriteByte('}') tv.visitAttributes(n.Attrs) case *ast.LiteralNode: code, ok := mapLiteralKind[n.Kind] if !ok { panic(fmt.Sprintf("No element for code %v", n.Kind)) } - tv.buf.WriteByte('{') - tv.buf.WriteRune(code) + tv.b.WriteByte('{') + tv.b.WriteRune(code) if n.Text != "" { - tv.buf.WriteByte(' ') - tv.buf.WriteString(n.Text) + tv.b.WriteByte(' ') + tv.b.WriteString(n.Text) } - tv.buf.WriteByte('}') + tv.b.WriteByte('}') tv.visitAttributes(n.Attrs) default: return tv } return nil @@ -894,14 +887,14 @@ ast.AlignCenter: "c", ast.AlignRight: "r", } var mapFormatKind = map[ast.FormatKind]rune{ - ast.FormatEmph: '_', - ast.FormatStrong: '*', - ast.FormatInsert: '>', - ast.FormatDelete: '~', + ast.FormatItalic: '/', + ast.FormatBold: '*', + ast.FormatUnder: '_', + ast.FormatStrike: '~', ast.FormatMonospace: '\'', ast.FormatSuper: '^', ast.FormatSub: ',', ast.FormatQuote: '"', ast.FormatQuotation: '<', @@ -916,40 +909,40 @@ ast.LiteralComment: '%', } func (tv *TestVisitor) visitInlineList(iln *ast.InlineListNode) { for _, in := range iln.List { - tv.buf.WriteByte(' ') + tv.b.WriteByte(' ') ast.Walk(tv, in) } } func (tv *TestVisitor) visitAttributes(a *ast.Attributes) { if a.IsEmpty() { return } - tv.buf.WriteString("[ATTR") + tv.b.WriteString("[ATTR") keys := make([]string, 0, len(a.Attrs)) for k := range a.Attrs { keys = append(keys, k) } sort.Strings(keys) for _, k := range keys { - tv.buf.WriteByte(' ') - tv.buf.WriteString(k) + tv.b.WriteByte(' ') + tv.b.WriteString(k) v := a.Attrs[k] if len(v) > 0 { - tv.buf.WriteByte('=') + tv.b.WriteByte('=') if strings.ContainsRune(v, ' ') { - tv.buf.WriteByte('"') - tv.buf.WriteString(v) - tv.buf.WriteByte('"') + tv.b.WriteByte('"') + tv.b.WriteString(v) + tv.b.WriteByte('"') } else { - tv.buf.WriteString(v) + tv.b.WriteString(v) } } } - tv.buf.WriteByte(']') + tv.b.WriteByte(']') } Index: search/print.go ================================================================== --- search/print.go +++ search/print.go @@ -14,11 +14,11 @@ import ( "io" "sort" "strconv" - "zettelstore.de/c/api" + "zettelstore.de/z/domain/meta" ) // Print the search to a writer. func (s *Search) Print(w io.Writer) { if s.negate { @@ -48,11 +48,11 @@ space = true } if ord := s.order; len(ord) > 0 { switch ord { - case api.KeyID: + case meta.KeyID: // Ignore case RandomOrder: space = printSpace(w, space) io.WriteString(w, "RANDOM") default: Index: search/search.go ================================================================== --- search/search.go +++ search/search.go @@ -15,11 +15,10 @@ "math/rand" "sort" "strings" "sync" - "zettelstore.de/c/api" "zettelstore.de/z/domain/id" "zettelstore.de/z/domain/meta" ) // Searcher is used to select zettel identifier based on search criteria. @@ -231,15 +230,15 @@ return false } s.mx.RLock() defer s.mx.RUnlock() for key := range s.tags { - if meta.IsComputed(key) || key == api.KeyTags { + if meta.IsComputed(key) || key == meta.KeyTags { return true } } - if order := s.order; order != "" && (meta.IsComputed(order) || order == api.KeyTags) { + if order := s.order; order != "" && (meta.IsComputed(order) || order == meta.KeyTags) { return true } return false } @@ -257,11 +256,11 @@ return compilePreMatch(preMatch, compMeta, compSearch, s.negate) } return compileNoPreMatch(compMeta, compSearch, s.negate) } -func selectNone(*meta.Meta) bool { return true } +func selectNone(m *meta.Meta) bool { return true } func compilePreMatch(preMatch, compMeta, compSearch MetaMatchFunc, negate bool) MetaMatchFunc { if compMeta == nil { if compSearch == nil { return preMatch @@ -318,11 +317,11 @@ sort.Slice(metaList, func(i, j int) bool { return metaList[i].Zid > metaList[j].Zid }) return metaList } if s.order == "" { - sort.Slice(metaList, createSortFunc(api.KeyID, true, metaList)) + sort.Slice(metaList, createSortFunc(meta.KeyID, true, metaList)) } else if s.order == RandomOrder { rand.Shuffle(len(metaList), func(i, j int) { metaList[i], metaList[j] = metaList[j], metaList[i] }) } else { Index: search/select.go ================================================================== --- search/select.go +++ search/select.go @@ -12,18 +12,17 @@ package search import ( "strings" - "zettelstore.de/c/api" "zettelstore.de/z/domain/meta" ) type matchFunc func(value string) bool -func matchNever(string) bool { return false } -func matchAlways(string) bool { return true } +func matchNever(value string) bool { return false } +func matchAlways(value string) bool { return true } type matchSpec struct { key string match matchFunc } @@ -90,76 +89,76 @@ negValues = append(negValues, opValue{value: val.value, op: val.op}) } else { posValues = append(posValues, opValue{value: val.value, op: val.op}) } } - return createMatchFunc(key, posValues, false), createMatchFunc(key, negValues, true) + return createMatchFunc(key, posValues), createMatchFunc(key, negValues) } // opValue is an expValue, but w/o the field "negate" type opValue struct { value string op compareOp } -func createMatchFunc(key string, values []opValue, negate bool) matchFunc { +func createMatchFunc(key string, values []opValue) matchFunc { if len(values) == 0 { return nil } switch meta.Type(key) { case meta.TypeBool: - return createMatchBoolFunc(values, negate) + return createMatchBoolFunc(values) case meta.TypeCredential: return matchNever case meta.TypeID, meta.TypeTimestamp: // ID and timestamp use the same layout - return createMatchIDFunc(values, negate) + return createMatchIDFunc(values) case meta.TypeIDSet: - return createMatchIDSetFunc(values, negate) + return createMatchIDSetFunc(values) case meta.TypeTagSet: - return createMatchTagSetFunc(values, negate) + return createMatchTagSetFunc(values) case meta.TypeWord: - return createMatchWordFunc(values, negate) + return createMatchWordFunc(values) case meta.TypeWordSet: - return createMatchWordSetFunc(values, negate) + return createMatchWordSetFunc(values) } - return createMatchStringFunc(values, negate) + return createMatchStringFunc(values) } -func createMatchBoolFunc(values []opValue, negate bool) matchFunc { +func createMatchBoolFunc(values []opValue) matchFunc { preValues := make([]bool, 0, len(values)) for _, v := range values { preValues = append(preValues, meta.BoolValue(v.value)) } return func(value string) bool { bValue := meta.BoolValue(value) for _, v := range preValues { - if (bValue == v) == negate { + if bValue != v { return false } } return true } } -func createMatchIDFunc(values []opValue, negate bool) matchFunc { +func createMatchIDFunc(values []opValue) matchFunc { return func(value string) bool { for _, v := range values { - if strings.HasPrefix(value, v.value) == negate { + if !strings.HasPrefix(value, v.value) { return false } } return true } } -func createMatchIDSetFunc(values []opValue, negate bool) matchFunc { +func createMatchIDSetFunc(values []opValue) matchFunc { idValues := preprocessSet(sliceToLower(values)) return func(value string) bool { ids := meta.ListFromValue(value) for _, neededIDs := range idValues { for _, neededID := range neededIDs { - if matchAllID(ids, neededID.value) == negate { + if !matchAllID(ids, neededID.value) { return false } } } return true @@ -173,21 +172,21 @@ } } return false } -func createMatchTagSetFunc(values []opValue, negate bool) matchFunc { +func createMatchTagSetFunc(values []opValue) matchFunc { tagValues := processTagSet(preprocessSet(sliceToLower(values))) return func(value string) bool { tags := meta.ListFromValue(value) // Remove leading '#' from each tag for i, tag := range tags { tags[i] = meta.CleanTag(tag) } for _, neededTags := range tagValues { for _, neededTag := range neededTags { - if matchAllTag(tags, neededTag.value, neededTag.equal) == negate { + if !matchAllTag(tags, neededTag.value, neededTag.equal) { return false } } } return true @@ -231,44 +230,44 @@ } } return false } -func createMatchWordFunc(values []opValue, negate bool) matchFunc { +func createMatchWordFunc(values []opValue) matchFunc { values = sliceToLower(values) return func(value string) bool { value = strings.ToLower(value) for _, v := range values { - if (value == v.value) == negate { + if value != v.value { return false } } return true } } -func createMatchWordSetFunc(values []opValue, negate bool) matchFunc { +func createMatchWordSetFunc(values []opValue) matchFunc { wordValues := preprocessSet(sliceToLower(values)) return func(value string) bool { words := meta.ListFromValue(value) for _, neededWords := range wordValues { for _, neededWord := range neededWords { - if matchAllWord(words, neededWord.value) == negate { + if !matchAllWord(words, neededWord.value) { return false } } } return true } } -func createMatchStringFunc(values []opValue, negate bool) matchFunc { +func createMatchStringFunc(values []opValue) matchFunc { values = sliceToLower(values) return func(value string) bool { value = strings.ToLower(value) for _, v := range values { - if strings.Contains(value, v.value) == negate { + if !strings.Contains(value, v.value) { return false } } return true } @@ -289,21 +288,21 @@ for _, s := range negSpecs { if s.match == nil { if _, ok := m.Get(s.key); ok { return false } - } else if value, ok := getMeta(m, s.key); !ok || !s.match(value) { + } else if value, ok := getMeta(m, s.key); !ok || s.match(value) { return false } } return true } } func getMeta(m *meta.Meta, key string) (string, bool) { - if key == api.KeyTags { - return m.Get(api.KeyAllTags) + if key == meta.KeyTags { + return m.Get(meta.KeyAllTags) } return m.Get(key) } func sliceToLower(sl []opValue) []opValue { Index: search/sorter.go ================================================================== --- search/sorter.go +++ search/sorter.go @@ -12,19 +12,18 @@ package search import ( "strconv" - "zettelstore.de/c/api" "zettelstore.de/z/domain/meta" ) type sortFunc func(i, j int) bool func createSortFunc(key string, descending bool, ml []*meta.Meta) sortFunc { keyType := meta.Type(key) - if key == api.KeyID || keyType == meta.TypeCredential { + if key == meta.KeyID || keyType == meta.TypeCredential { if descending { return func(i, j int) bool { return ml[i].Zid > ml[j].Zid } } return func(i, j int) bool { return ml[i].Zid < ml[j].Zid } } Index: strfun/strfun.go ================================================================== --- strfun/strfun.go +++ strfun/strfun.go @@ -10,14 +10,20 @@ // Package strfun provides some string functions. package strfun import ( - "bytes" "strings" + "unicode" "unicode/utf8" ) + +// TrimSpaceRight returns a slice of the string s, with all trailing white space removed, +// as defined by Unicode. +func TrimSpaceRight(s string) string { + return strings.TrimRightFunc(s, unicode.IsSpace) +} // Length returns the number of runes in the given string. func Length(s string) int { return utf8.RuneCountInString(s) } @@ -34,21 +40,21 @@ if len(runes) > maxLen { runes = runes[:maxLen] runes[maxLen-1] = '\u2025' } - var buf bytes.Buffer + var sb strings.Builder for _, r := range runes { - buf.WriteRune(r) + sb.WriteRune(r) } for i := 0; i < maxLen-len(runes); i++ { - buf.WriteRune(pad) + sb.WriteRune(pad) } - return buf.String() + return sb.String() } // SplitLines splits the given string into a list of lines. func SplitLines(s string) []string { return strings.FieldsFunc(s, func(r rune) bool { return r == '\n' || r == '\r' }) } Index: strfun/strfun_test.go ================================================================== --- strfun/strfun_test.go +++ strfun/strfun_test.go @@ -14,10 +14,43 @@ import ( "testing" "zettelstore.de/z/strfun" ) + +func TestTrimSpaceRight(t *testing.T) { + t.Parallel() + const space = "\t\v\r\f\n\u0085\u00a0\u2000\u3000" + testcases := []struct { + in string + exp string + }{ + {"", ""}, + {"abc", "abc"}, + {" ", ""}, + {space, ""}, + {space + "abc" + space, space + "abc"}, + {" \t\r\n \t\t\r\r\n\n ", ""}, + {" \t\r\n x\t\t\r\r\n\n ", " \t\r\n x"}, + {" \u2000\t\r\n x\t\t\r\r\ny\n \u3000", " \u2000\t\r\n x\t\t\r\r\ny"}, + {"1 \t\r\n2", "1 \t\r\n2"}, + {" x\x80", " x\x80"}, + {" x\xc0", " x\xc0"}, + {"x \xc0\xc0 ", "x \xc0\xc0"}, + {"x \xc0", "x \xc0"}, + {"x \xc0 ", "x \xc0"}, + {"x \xc0\xc0 ", "x \xc0\xc0"}, + {"x ☺\xc0\xc0 ", "x ☺\xc0\xc0"}, + {"x ☺ ", "x ☺"}, + } + for i, tc := range testcases { + got := strfun.TrimSpaceRight(tc.in) + if got != tc.exp { + t.Errorf("%d/%q: expected %q, got %q", i, tc.in, tc.exp, got) + } + } +} func TestLength(t *testing.T) { t.Parallel() testcases := []struct { in string Index: template/mustache.go ================================================================== --- template/mustache.go +++ template/mustache.go @@ -335,11 +335,11 @@ case '!': //ignore comment case '#', '^': name := strings.TrimSpace(tag[1:]) sn := §ionNode{name, tag[0] == '^', tmpl.curline, []node{}} - err = tmpl.parseSection(sn) + err := tmpl.parseSection(sn) if err != nil { return err } section.nodes = append(section.nodes, sn) case '/': @@ -407,11 +407,11 @@ case '!': //ignore comment case '#', '^': name := strings.TrimSpace(tag[1:]) sn := §ionNode{name, tag[0] == '^', tmpl.curline, []node{}} - err = tmpl.parseSection(sn) + err := tmpl.parseSection(sn) if err != nil { return err } tmpl.nodes = append(tmpl.nodes, sn) case '/': @@ -562,11 +562,11 @@ } topStack := len(stack) stack = append(stack, enumeration[0]) for _, elem := range enumeration { stack[topStack] = elem - if err = tmpl.renderNodes(w, section.nodes, stack); err != nil { + if err := tmpl.renderNodes(w, section.nodes, stack); err != nil { return err } } return nil case reflect.Map, reflect.Struct: @@ -612,11 +612,11 @@ case *partialNode: partial, err := getPartials(n.prov, n.name, n.indent) if err != nil { return err } - if err = partial.renderTemplate(w, stack); err != nil { + if err := partial.renderTemplate(w, stack); err != nil { return err } } return nil } Index: template/spec_test.go ================================================================== --- template/spec_test.go +++ template/spec_test.go @@ -205,18 +205,18 @@ continue } if enabled == nil { continue } - b, err2 := os.ReadFile(path) - if err2 != nil { - t.Fatal(err2) + b, err := os.ReadFile(path) + if err != nil { + t.Fatal(err) } var suite specTestSuite - err2 = json.Unmarshal(b, &suite) - if err2 != nil { - t.Fatal(err2) + err = json.Unmarshal(b, &suite) + if err != nil { + t.Fatal(err) } for _, test := range suite.Tests { runTest(t, file, &test) } } ADDED testdata/content/blockcomment/20200215204700.zettel Index: testdata/content/blockcomment/20200215204700.zettel ================================================================== --- testdata/content/blockcomment/20200215204700.zettel +++ testdata/content/blockcomment/20200215204700.zettel @@ -0,0 +1,8 @@ +title: Simple Test + +%%% +No render +%%% +%%%{-} +Render +%%% ADDED testdata/content/cite/20200215204700.zettel Index: testdata/content/cite/20200215204700.zettel ================================================================== --- testdata/content/cite/20200215204700.zettel +++ testdata/content/cite/20200215204700.zettel @@ -0,0 +1,3 @@ +title: Simple Test + +[@Stern18]{-} ADDED testdata/content/comment/20200215204700.zettel Index: testdata/content/comment/20200215204700.zettel ================================================================== --- testdata/content/comment/20200215204700.zettel +++ testdata/content/comment/20200215204700.zettel @@ -0,0 +1,4 @@ +title: Simple Test + +% No comment +%% Comment ADDED testdata/content/descrlist/20200226122100.zettel Index: testdata/content/descrlist/20200226122100.zettel ================================================================== --- testdata/content/descrlist/20200226122100.zettel +++ testdata/content/descrlist/20200226122100.zettel @@ -0,0 +1,7 @@ +title: Simple Test + +; Zettel +: Paper +: Note +; Zettelkasten +: Slip box ADDED testdata/content/edit/20200215204700.zettel Index: testdata/content/edit/20200215204700.zettel ================================================================== --- testdata/content/edit/20200215204700.zettel +++ testdata/content/edit/20200215204700.zettel @@ -0,0 +1,5 @@ +title: Simple Test + +~~delete~~{-} +__insert__{-} +~~kill~~{-}__create__{-} ADDED testdata/content/embed/20200215204700.zettel Index: testdata/content/embed/20200215204700.zettel ================================================================== --- testdata/content/embed/20200215204700.zettel +++ testdata/content/embed/20200215204700.zettel @@ -0,0 +1,3 @@ +title: Simple Test + +{{abc}} ADDED testdata/content/footnote/20200215204700.zettel Index: testdata/content/footnote/20200215204700.zettel ================================================================== --- testdata/content/footnote/20200215204700.zettel +++ testdata/content/footnote/20200215204700.zettel @@ -0,0 +1,3 @@ +title: Simple Test + +Text[^foot]{=sidebar} ADDED testdata/content/format/20200215204700.zettel Index: testdata/content/format/20200215204700.zettel ================================================================== --- testdata/content/format/20200215204700.zettel +++ testdata/content/format/20200215204700.zettel @@ -0,0 +1,19 @@ +title: Simple Test +role: zettel + +//italic// +//emph//{-} +**bold** +**strong**{-} +__unterline__ +~~strike~~ +''monospace'' +^^superscript^^ +,,subscript,, +""Quotes"" +<|=h2|h3:| +|%--+---+---+ +|\n// JavaScript example\n\ndocument.getElementById(\"demo\").innerHTML = \"Hello JavaScript!\";\n\nokay\n", // 170 + "1. *bar*\n", // 178 + "- foo\n - bar\n - baz\n - boo\n", // 294 + "10) foo\n - bar\n", // 296 + "- # Foo\n- Bar\n ---\n baz\n", // 300 + "- foo\n\n- bar\n\n\n- baz\n", // 306 + "- foo\n - bar\n - baz\n\n\n bim\n", // 307 + "1. a\n\n 2. b\n\n 3. c\n", // 311 + "1. a\n\n 2. b\n\n 3. c\n", // 313 + "- a\n- b\n\n- c\n", // 314 + "* a\n*\n\n* c\n", // 315 + "- a\n- b\n\n [ref]: /url\n- d\n", // 317 + "- a\n - b\n\n c\n- d\n", // 319 + "* a\n > b\n >\n* c\n", // 320 + "- a\n > b\n ```\n c\n ```\n- d\n", // 321 + "- a\n - b\n", // 323 + "`\n", // 345 + "[foo\n", // 525 + "[foo\n\n[ref]: /uri\n", // 537 + "\n", // 581 + "\n", // 594 +} + +var reHeadingID = regexp.MustCompile(` id="[^"]*"`) func TestEncoderAvailability(t *testing.T) { t.Parallel() encoderMissing := false for _, enc := range encodings { @@ -64,54 +93,86 @@ } var testcases []markdownTestCase if err = json.Unmarshal(content, &testcases); err != nil { panic(err) } - + excMap := make(map[string]bool, len(exceptions)) + for _, exc := range exceptions { + excMap[exc] = true + } for _, tc := range testcases { - ast := parser.ParseBlocks(input.NewInput([]byte(tc.Markdown)), nil, "markdown") + ast := parser.ParseBlocks(input.NewInput(tc.Markdown), nil, "markdown") testAllEncodings(t, tc, ast) + if _, found := excMap[tc.Markdown]; !found { + testHTMLEncoding(t, tc, ast) + } testZmkEncoding(t, tc, ast) } } func testAllEncodings(t *testing.T, tc markdownTestCase, ast *ast.BlockListNode) { - var buf bytes.Buffer + var sb strings.Builder testID := tc.Example*100 + 1 for _, enc := range encodings { t.Run(fmt.Sprintf("Encode %v %v", enc, testID), func(st *testing.T) { - encoder.Create(enc, nil).WriteBlocks(&buf, ast) - buf.Reset() + encoder.Create(enc, nil).WriteBlocks(&sb, ast) + sb.Reset() }) } } + +func testHTMLEncoding(t *testing.T, tc markdownTestCase, ast *ast.BlockListNode) { + htmlEncoder := encoder.Create(api.EncoderHTML, &encoder.Environment{Xhtml: true}) + var sb strings.Builder + testID := tc.Example*100 + 1 + t.Run(fmt.Sprintf("Encode md html %v", testID), func(st *testing.T) { + htmlEncoder.WriteBlocks(&sb, ast) + gotHTML := sb.String() + sb.Reset() + + mdHTML := tc.HTML + mdHTML = strings.ReplaceAll(mdHTML, "\"MAILTO:", "\"mailto:") + gotHTML = strings.ReplaceAll(gotHTML, " class=\"zs-external\"", "") + gotHTML = strings.ReplaceAll(gotHTML, "%2A", "*") // url.QueryEscape + if strings.Count(gotHTML, " 0 { + gotHTML = reHeadingID.ReplaceAllString(gotHTML, "") + } + if gotHTML != mdHTML { + mdHTML = strings.ReplaceAll(mdHTML, "

  • \n", "
  • ") + if gotHTML != mdHTML { + st.Errorf("\nCMD: %q\nExp: %q\nGot: %q", tc.Markdown, mdHTML, gotHTML) + } + } + }) +} func testZmkEncoding(t *testing.T, tc markdownTestCase, ast *ast.BlockListNode) { zmkEncoder := encoder.Create(api.EncoderZmk, nil) - var buf bytes.Buffer + var sb strings.Builder testID := tc.Example*100 + 1 t.Run(fmt.Sprintf("Encode zmk %14d", testID), func(st *testing.T) { - buf.Reset() - zmkEncoder.WriteBlocks(&buf, ast) - // gotFirst := buf.String() + zmkEncoder.WriteBlocks(&sb, ast) + gotFirst := sb.String() + sb.Reset() testID = tc.Example*100 + 2 - secondAst := parser.ParseBlocks(input.NewInput(buf.Bytes()), nil, api.ValueSyntaxZmk) - buf.Reset() - zmkEncoder.WriteBlocks(&buf, secondAst) - gotSecond := buf.String() + secondAst := parser.ParseBlocks(input.NewInput(gotFirst), nil, "zmk") + zmkEncoder.WriteBlocks(&sb, secondAst) + gotSecond := sb.String() + sb.Reset() // if gotFirst != gotSecond { // st.Errorf("\nCMD: %q\n1st: %q\n2nd: %q", tc.Markdown, gotFirst, gotSecond) // } testID = tc.Example*100 + 3 - thirdAst := parser.ParseBlocks(input.NewInput(buf.Bytes()), nil, api.ValueSyntaxZmk) - buf.Reset() - zmkEncoder.WriteBlocks(&buf, thirdAst) - gotThird := buf.String() + thirdAst := parser.ParseBlocks(input.NewInput(gotFirst), nil, "zmk") + zmkEncoder.WriteBlocks(&sb, thirdAst) + gotThird := sb.String() + sb.Reset() if gotSecond != gotThird { st.Errorf("\n1st: %q\n2nd: %q", gotSecond, gotThird) } }) + } Index: tests/regression_test.go ================================================================== --- tests/regression_test.go +++ tests/regression_test.go @@ -10,30 +10,38 @@ // Package tests provides some higher-level tests. package tests import ( - "bytes" "context" "fmt" "io" "net/url" "os" "path/filepath" + "strings" "testing" - "zettelstore.de/c/api" + "zettelstore.de/z/api" "zettelstore.de/z/ast" "zettelstore.de/z/box" "zettelstore.de/z/box/manager" + "zettelstore.de/z/domain" "zettelstore.de/z/domain/id" "zettelstore.de/z/domain/meta" "zettelstore.de/z/encoder" "zettelstore.de/z/kernel" "zettelstore.de/z/parser" _ "zettelstore.de/z/box/dirbox" + _ "zettelstore.de/z/encoder/djsonenc" + _ "zettelstore.de/z/encoder/htmlenc" + _ "zettelstore.de/z/encoder/nativeenc" + _ "zettelstore.de/z/encoder/textenc" + _ "zettelstore.de/z/encoder/zmkenc" + _ "zettelstore.de/z/parser/blob" + _ "zettelstore.de/z/parser/zettelmark" ) var encodings = []api.EncodingEnum{ api.EncoderHTML, api.EncoderDJSON, @@ -49,17 +57,17 @@ } cdata := manager.ConnectData{Config: testConfig, Enricher: &noEnrich{}, Notify: nil} for _, entry := range entries { if entry.IsDir() { - u, err2 := url.Parse("dir://" + filepath.Join(root, entry.Name()) + "?type=" + kernel.BoxDirTypeSimple) - if err2 != nil { - panic(err2) + u, err := url.Parse("dir://" + filepath.Join(root, entry.Name()) + "?type=" + kernel.BoxDirTypeSimple) + if err != nil { + panic(err) } - box, err2 := manager.Connect(u, &noAuth{}, &cdata) - if err2 != nil { - panic(err2) + box, err := manager.Connect(u, &noAuth{}, &cdata) + if err != nil { + panic(err) } boxes = append(boxes, box) } } return root, boxes @@ -102,26 +110,99 @@ wantContent = trimLastEOL(wantContent) if gotContent != wantContent { t.Errorf("\nWant: %q\nGot: %q", wantContent, gotContent) } } + +func checkBlocksFile(t *testing.T, resultName string, zn *ast.ZettelNode, enc api.EncodingEnum) { + t.Helper() + var env encoder.Environment + if enc := encoder.Create(enc, &env); enc != nil { + var sb strings.Builder + enc.WriteBlocks(&sb, zn.Ast) + checkFileContent(t, resultName, sb.String()) + return + } + panic(fmt.Sprintf("Unknown writer encoding %q", enc)) +} + +func checkZmkEncoder(t *testing.T, zn *ast.ZettelNode) { + zmkEncoder := encoder.Create(api.EncoderZmk, nil) + var sb strings.Builder + zmkEncoder.WriteBlocks(&sb, zn.Ast) + gotFirst := sb.String() + sb.Reset() + + newZettel := parser.ParseZettel(domain.Zettel{ + Meta: zn.Meta, Content: domain.NewContent("\n" + gotFirst)}, "", testConfig) + zmkEncoder.WriteBlocks(&sb, newZettel.Ast) + gotSecond := sb.String() + sb.Reset() + + if gotFirst != gotSecond { + t.Errorf("\n1st: %q\n2nd: %q", gotFirst, gotSecond) + } +} func getBoxName(p box.ManagedBox, root string) string { u, err := url.Parse(p.Location()) if err != nil { panic("Unable to parse URL '" + p.Location() + "': " + err.Error()) } return u.Path[len(root):] } + +func checkContentBox(t *testing.T, p box.ManagedBox, wd, boxName string) { + ss := p.(box.StartStopper) + if err := ss.Start(context.Background()); err != nil { + panic(err) + } + metaList := []*meta.Meta{} + err := p.ApplyMeta(context.Background(), func(m *meta.Meta) { metaList = append(metaList, m) }) + if err != nil { + panic(err) + } + for _, meta := range metaList { + zettel, err := p.GetZettel(context.Background(), meta.Zid) + if err != nil { + panic(err) + } + z := parser.ParseZettel(zettel, "", testConfig) + for _, enc := range encodings { + t.Run(fmt.Sprintf("%s::%d(%s)", p.Location(), meta.Zid, enc), func(st *testing.T) { + resultName := filepath.Join(wd, "result", "content", boxName, z.Zid.String()+"."+enc.String()) + checkBlocksFile(st, resultName, z, enc) + }) + } + t.Run(fmt.Sprintf("%s::%d", p.Location(), meta.Zid), func(st *testing.T) { + checkZmkEncoder(st, z) + }) + } + if err := ss.Stop(context.Background()); err != nil { + panic(err) + } +} + +func TestContentRegression(t *testing.T) { + t.Parallel() + wd, err := os.Getwd() + if err != nil { + panic(err) + } + root, boxes := getFileBoxes(wd, "content") + for _, p := range boxes { + checkContentBox(t, p, wd, getBoxName(p, root)) + } +} func checkMetaFile(t *testing.T, resultName string, zn *ast.ZettelNode, enc api.EncodingEnum) { t.Helper() if enc := encoder.Create(enc, nil); enc != nil { - var buf bytes.Buffer - enc.WriteMeta(&buf, zn.Meta, parser.ParseMetadata) - checkFileContent(t, resultName, buf.String()) + var sb strings.Builder + enc.WriteMeta(&sb, zn.Meta, parser.ParseMetadata) + checkFileContent(t, resultName, sb.String()) return } panic(fmt.Sprintf("Unknown writer encoding %q", enc)) } @@ -134,33 +215,33 @@ err := p.ApplyMeta(context.Background(), func(m *meta.Meta) { metaList = append(metaList, m) }) if err != nil { panic(err) } for _, meta := range metaList { - zettel, err2 := p.GetZettel(context.Background(), meta.Zid) - if err2 != nil { - panic(err2) + zettel, err := p.GetZettel(context.Background(), meta.Zid) + if err != nil { + panic(err) } z := parser.ParseZettel(zettel, "", testConfig) for _, enc := range encodings { t.Run(fmt.Sprintf("%s::%d(%s)", p.Location(), meta.Zid, enc), func(st *testing.T) { resultName := filepath.Join(wd, "result", "meta", boxName, z.Zid.String()+"."+enc.String()) checkMetaFile(st, resultName, z, enc) }) } } - if err = ss.Stop(context.Background()); err != nil { + if err := ss.Stop(context.Background()); err != nil { panic(err) } } type myConfig struct{} func (*myConfig) AddDefaultValues(m *meta.Meta) *meta.Meta { return m } func (*myConfig) GetDefaultTitle() string { return "" } -func (*myConfig) GetDefaultRole() string { return api.ValueRoleZettel } -func (*myConfig) GetDefaultSyntax() string { return api.ValueSyntaxZmk } +func (*myConfig) GetDefaultRole() string { return meta.ValueRoleZettel } +func (*myConfig) GetDefaultSyntax() string { return meta.ValueSyntaxZmk } func (*myConfig) GetDefaultLang() string { return "" } func (*myConfig) GetDefaultVisibility() meta.Visibility { return meta.VisibilityPublic } func (*myConfig) GetFooterHTML() string { return "" } func (*myConfig) GetHomeZettel() id.Zid { return id.Invalid } func (*myConfig) GetListPageSize() int { return 0 } ADDED tests/result/content/blockcomment/20200215204700.djson Index: tests/result/content/blockcomment/20200215204700.djson ================================================================== --- tests/result/content/blockcomment/20200215204700.djson +++ tests/result/content/blockcomment/20200215204700.djson @@ -0,0 +1,1 @@ +[{"t":"CommentBlock","l":["No render"]},{"t":"CommentBlock","a":{"-":""},"l":["Render"]}] ADDED tests/result/content/blockcomment/20200215204700.html Index: tests/result/content/blockcomment/20200215204700.html ================================================================== --- tests/result/content/blockcomment/20200215204700.html +++ tests/result/content/blockcomment/20200215204700.html @@ -0,0 +1,3 @@ + ADDED tests/result/content/blockcomment/20200215204700.native Index: tests/result/content/blockcomment/20200215204700.native ================================================================== --- tests/result/content/blockcomment/20200215204700.native +++ tests/result/content/blockcomment/20200215204700.native @@ -0,0 +1,2 @@ +[CommentBlock "No render"], +[CommentBlock ("",[-]) "Render"] ADDED tests/result/content/blockcomment/20200215204700.text Index: tests/result/content/blockcomment/20200215204700.text ================================================================== --- tests/result/content/blockcomment/20200215204700.text +++ tests/result/content/blockcomment/20200215204700.text ADDED tests/result/content/cite/20200215204700.djson Index: tests/result/content/cite/20200215204700.djson ================================================================== --- tests/result/content/cite/20200215204700.djson +++ tests/result/content/cite/20200215204700.djson @@ -0,0 +1,1 @@ +[{"t":"Para","i":[{"t":"Cite","a":{"-":""},"s":"Stern18"}]}] ADDED tests/result/content/cite/20200215204700.html Index: tests/result/content/cite/20200215204700.html ================================================================== --- tests/result/content/cite/20200215204700.html +++ tests/result/content/cite/20200215204700.html @@ -0,0 +1,1 @@ +

    Stern18

    ADDED tests/result/content/cite/20200215204700.native Index: tests/result/content/cite/20200215204700.native ================================================================== --- tests/result/content/cite/20200215204700.native +++ tests/result/content/cite/20200215204700.native @@ -0,0 +1,1 @@ +[Para Cite ("",[-]) "Stern18"] ADDED tests/result/content/cite/20200215204700.text Index: tests/result/content/cite/20200215204700.text ================================================================== --- tests/result/content/cite/20200215204700.text +++ tests/result/content/cite/20200215204700.text ADDED tests/result/content/comment/20200215204700.djson Index: tests/result/content/comment/20200215204700.djson ================================================================== --- tests/result/content/comment/20200215204700.djson +++ tests/result/content/comment/20200215204700.djson @@ -0,0 +1,1 @@ +[{"t":"Para","i":[{"t":"Text","s":"%"},{"t":"Space"},{"t":"Text","s":"No"},{"t":"Space"},{"t":"Text","s":"comment"},{"t":"Soft"},{"t":"Comment","s":"Comment"}]}] ADDED tests/result/content/comment/20200215204700.html Index: tests/result/content/comment/20200215204700.html ================================================================== --- tests/result/content/comment/20200215204700.html +++ tests/result/content/comment/20200215204700.html @@ -0,0 +1,2 @@ +

    % No comment +

    ADDED tests/result/content/comment/20200215204700.native Index: tests/result/content/comment/20200215204700.native ================================================================== --- tests/result/content/comment/20200215204700.native +++ tests/result/content/comment/20200215204700.native @@ -0,0 +1,1 @@ +[Para Text "%",Space,Text "No",Space,Text "comment",Space,Comment "Comment"] ADDED tests/result/content/comment/20200215204700.text Index: tests/result/content/comment/20200215204700.text ================================================================== --- tests/result/content/comment/20200215204700.text +++ tests/result/content/comment/20200215204700.text @@ -0,0 +1,1 @@ +% No comment ADDED tests/result/content/descrlist/20200226122100.djson Index: tests/result/content/descrlist/20200226122100.djson ================================================================== --- tests/result/content/descrlist/20200226122100.djson +++ tests/result/content/descrlist/20200226122100.djson @@ -0,0 +1,1 @@ +[{"t":"DescriptionList","g":[[[{"t":"Text","s":"Zettel"}],[{"t":"Para","i":[{"t":"Text","s":"Paper"}]}],[{"t":"Para","i":[{"t":"Text","s":"Note"}]}]],[[{"t":"Text","s":"Zettelkasten"}],[{"t":"Para","i":[{"t":"Text","s":"Slip"},{"t":"Space"},{"t":"Text","s":"box"}]}]]]}] ADDED tests/result/content/descrlist/20200226122100.html Index: tests/result/content/descrlist/20200226122100.html ================================================================== --- tests/result/content/descrlist/20200226122100.html +++ tests/result/content/descrlist/20200226122100.html @@ -0,0 +1,7 @@ +
    +
    Zettel
    +
    Paper
    +
    Note
    +
    Zettelkasten
    +
    Slip box
    +
    ADDED tests/result/content/descrlist/20200226122100.native Index: tests/result/content/descrlist/20200226122100.native ================================================================== --- tests/result/content/descrlist/20200226122100.native +++ tests/result/content/descrlist/20200226122100.native @@ -0,0 +1,9 @@ +[DescriptionList + [Term [Text "Zettel"], + [Description + [Para Text "Paper"]], + [Description + [Para Text "Note"]]], + [Term [Text "Zettelkasten"], + [Description + [Para Text "Slip",Space,Text "box"]]]] ADDED tests/result/content/descrlist/20200226122100.text Index: tests/result/content/descrlist/20200226122100.text ================================================================== --- tests/result/content/descrlist/20200226122100.text +++ tests/result/content/descrlist/20200226122100.text @@ -0,0 +1,5 @@ +Zettel +Paper +Note +Zettelkasten +Slip box ADDED tests/result/content/edit/20200215204700.djson Index: tests/result/content/edit/20200215204700.djson ================================================================== --- tests/result/content/edit/20200215204700.djson +++ tests/result/content/edit/20200215204700.djson @@ -0,0 +1,1 @@ +[{"t":"Para","i":[{"t":"Delete","i":[{"t":"Text","s":"delete"}]},{"t":"Soft"},{"t":"Insert","i":[{"t":"Text","s":"insert"}]},{"t":"Soft"},{"t":"Delete","i":[{"t":"Text","s":"kill"}]},{"t":"Insert","i":[{"t":"Text","s":"create"}]}]}] ADDED tests/result/content/edit/20200215204700.html Index: tests/result/content/edit/20200215204700.html ================================================================== --- tests/result/content/edit/20200215204700.html +++ tests/result/content/edit/20200215204700.html @@ -0,0 +1,3 @@ +

    delete +insert +killcreate

    ADDED tests/result/content/edit/20200215204700.native Index: tests/result/content/edit/20200215204700.native ================================================================== --- tests/result/content/edit/20200215204700.native +++ tests/result/content/edit/20200215204700.native @@ -0,0 +1,1 @@ +[Para Delete [Text "delete"],Space,Insert [Text "insert"],Space,Delete [Text "kill"],Insert [Text "create"]] ADDED tests/result/content/edit/20200215204700.text Index: tests/result/content/edit/20200215204700.text ================================================================== --- tests/result/content/edit/20200215204700.text +++ tests/result/content/edit/20200215204700.text @@ -0,0 +1,1 @@ +delete insert killcreate ADDED tests/result/content/embed/20200215204700.djson Index: tests/result/content/embed/20200215204700.djson ================================================================== --- tests/result/content/embed/20200215204700.djson +++ tests/result/content/embed/20200215204700.djson @@ -0,0 +1,1 @@ +[{"t":"Para","i":[{"t":"Embed","s":"abc"}]}] ADDED tests/result/content/embed/20200215204700.html Index: tests/result/content/embed/20200215204700.html ================================================================== --- tests/result/content/embed/20200215204700.html +++ tests/result/content/embed/20200215204700.html @@ -0,0 +1,1 @@ +

    ADDED tests/result/content/embed/20200215204700.native Index: tests/result/content/embed/20200215204700.native ================================================================== --- tests/result/content/embed/20200215204700.native +++ tests/result/content/embed/20200215204700.native @@ -0,0 +1,1 @@ +[Para Embed EXTERNAL "abc"] ADDED tests/result/content/embed/20200215204700.text Index: tests/result/content/embed/20200215204700.text ================================================================== --- tests/result/content/embed/20200215204700.text +++ tests/result/content/embed/20200215204700.text ADDED tests/result/content/footnote/20200215204700.djson Index: tests/result/content/footnote/20200215204700.djson ================================================================== --- tests/result/content/footnote/20200215204700.djson +++ tests/result/content/footnote/20200215204700.djson @@ -0,0 +1,1 @@ +[{"t":"Para","i":[{"t":"Text","s":"Text"},{"t":"Footnote","a":{"":"sidebar"},"i":[{"t":"Text","s":"foot"}]}]}] ADDED tests/result/content/footnote/20200215204700.html Index: tests/result/content/footnote/20200215204700.html ================================================================== --- tests/result/content/footnote/20200215204700.html +++ tests/result/content/footnote/20200215204700.html @@ -0,0 +1,4 @@ +

    Text1

    +
      +
    1. foot ↩︎
    2. +
    ADDED tests/result/content/footnote/20200215204700.native Index: tests/result/content/footnote/20200215204700.native ================================================================== --- tests/result/content/footnote/20200215204700.native +++ tests/result/content/footnote/20200215204700.native @@ -0,0 +1,1 @@ +[Para Text "Text",Footnote ("sidebar",[]) [Text "foot"]] ADDED tests/result/content/footnote/20200215204700.text Index: tests/result/content/footnote/20200215204700.text ================================================================== --- tests/result/content/footnote/20200215204700.text +++ tests/result/content/footnote/20200215204700.text @@ -0,0 +1,1 @@ +Text foot ADDED tests/result/content/format/20200215204700.djson Index: tests/result/content/format/20200215204700.djson ================================================================== --- tests/result/content/format/20200215204700.djson +++ tests/result/content/format/20200215204700.djson @@ -0,0 +1,1 @@ +[{"t":"Para","i":[{"t":"Italic","i":[{"t":"Text","s":"italic"}]},{"t":"Soft"},{"t":"Emph","i":[{"t":"Text","s":"emph"}]},{"t":"Soft"},{"t":"Bold","i":[{"t":"Text","s":"bold"}]},{"t":"Soft"},{"t":"Strong","i":[{"t":"Text","s":"strong"}]},{"t":"Soft"},{"t":"Underline","i":[{"t":"Text","s":"unterline"}]},{"t":"Soft"},{"t":"Strikethrough","i":[{"t":"Text","s":"strike"}]},{"t":"Soft"},{"t":"Mono","i":[{"t":"Text","s":"monospace"}]},{"t":"Soft"},{"t":"Super","i":[{"t":"Text","s":"superscript"}]},{"t":"Soft"},{"t":"Sub","i":[{"t":"Text","s":"subscript"}]},{"t":"Soft"},{"t":"Quote","i":[{"t":"Text","s":"Quotes"}]},{"t":"Soft"},{"t":"Quotation","i":[{"t":"Text","s":"Quotation"}]},{"t":"Soft"},{"t":"Small","i":[{"t":"Text","s":"small"}]},{"t":"Soft"},{"t":"Span","i":[{"t":"Text","s":"span"}]},{"t":"Soft"},{"t":"Code","s":"code"},{"t":"Soft"},{"t":"Input","s":"input"},{"t":"Soft"},{"t":"Output","s":"output"}]}] ADDED tests/result/content/format/20200215204700.html Index: tests/result/content/format/20200215204700.html ================================================================== --- tests/result/content/format/20200215204700.html +++ tests/result/content/format/20200215204700.html @@ -0,0 +1,16 @@ +

    italic +emph +bold +strong +unterline +strike +monospace +superscript +subscript +"Quotes" +Quotation +small +span +code +input +output

    ADDED tests/result/content/format/20200215204700.native Index: tests/result/content/format/20200215204700.native ================================================================== --- tests/result/content/format/20200215204700.native +++ tests/result/content/format/20200215204700.native @@ -0,0 +1,1 @@ +[Para Italic [Text "italic"],Space,Emph [Text "emph"],Space,Bold [Text "bold"],Space,Strong [Text "strong"],Space,Underline [Text "unterline"],Space,Strikethrough [Text "strike"],Space,Mono [Text "monospace"],Space,Super [Text "superscript"],Space,Sub [Text "subscript"],Space,Quote [Text "Quotes"],Space,Quotation [Text "Quotation"],Space,Small [Text "small"],Space,Span [Text "span"],Space,Code "code",Space,Input "input",Space,Output "output"] ADDED tests/result/content/format/20200215204700.text Index: tests/result/content/format/20200215204700.text ================================================================== --- tests/result/content/format/20200215204700.text +++ tests/result/content/format/20200215204700.text @@ -0,0 +1,1 @@ +italic emph bold strong unterline strike monospace superscript subscript Quotes Quotation small span code input output ADDED tests/result/content/format/20201107164400.djson Index: tests/result/content/format/20201107164400.djson ================================================================== --- tests/result/content/format/20201107164400.djson +++ tests/result/content/format/20201107164400.djson @@ -0,0 +1,1 @@ +[{"t":"Para","i":[{"t":"Span","a":{"lang":"fr"},"i":[{"t":"Quote","i":[{"t":"Text","s":"abc"}]}]}]}] ADDED tests/result/content/format/20201107164400.html Index: tests/result/content/format/20201107164400.html ================================================================== --- tests/result/content/format/20201107164400.html +++ tests/result/content/format/20201107164400.html @@ -0,0 +1,1 @@ +

    « abc »

    ADDED tests/result/content/format/20201107164400.native Index: tests/result/content/format/20201107164400.native ================================================================== --- tests/result/content/format/20201107164400.native +++ tests/result/content/format/20201107164400.native @@ -0,0 +1,1 @@ +[Para Span ("",[lang="fr"]) [Quote [Text "abc"]]] ADDED tests/result/content/format/20201107164400.text Index: tests/result/content/format/20201107164400.text ================================================================== --- tests/result/content/format/20201107164400.text +++ tests/result/content/format/20201107164400.text @@ -0,0 +1,1 @@ +abc ADDED tests/result/content/heading/20200215204700.djson Index: tests/result/content/heading/20200215204700.djson ================================================================== --- tests/result/content/heading/20200215204700.djson +++ tests/result/content/heading/20200215204700.djson @@ -0,0 +1,1 @@ +[{"t":"Heading","n":2,"s":"first","i":[{"t":"Text","s":"First"}]}] ADDED tests/result/content/heading/20200215204700.html Index: tests/result/content/heading/20200215204700.html ================================================================== --- tests/result/content/heading/20200215204700.html +++ tests/result/content/heading/20200215204700.html @@ -0,0 +1,1 @@ +

    First

    ADDED tests/result/content/heading/20200215204700.native Index: tests/result/content/heading/20200215204700.native ================================================================== --- tests/result/content/heading/20200215204700.native +++ tests/result/content/heading/20200215204700.native @@ -0,0 +1,1 @@ +[Heading 2 #first Text "First"] ADDED tests/result/content/heading/20200215204700.text Index: tests/result/content/heading/20200215204700.text ================================================================== --- tests/result/content/heading/20200215204700.text +++ tests/result/content/heading/20200215204700.text @@ -0,0 +1,1 @@ +First ADDED tests/result/content/hrule/20200215204700.djson Index: tests/result/content/hrule/20200215204700.djson ================================================================== --- tests/result/content/hrule/20200215204700.djson +++ tests/result/content/hrule/20200215204700.djson @@ -0,0 +1,1 @@ +[{"t":"Hrule"}] ADDED tests/result/content/hrule/20200215204700.html Index: tests/result/content/hrule/20200215204700.html ================================================================== --- tests/result/content/hrule/20200215204700.html +++ tests/result/content/hrule/20200215204700.html @@ -0,0 +1,1 @@ +
    ADDED tests/result/content/hrule/20200215204700.native Index: tests/result/content/hrule/20200215204700.native ================================================================== --- tests/result/content/hrule/20200215204700.native +++ tests/result/content/hrule/20200215204700.native @@ -0,0 +1,1 @@ +[Hrule] ADDED tests/result/content/hrule/20200215204700.text Index: tests/result/content/hrule/20200215204700.text ================================================================== --- tests/result/content/hrule/20200215204700.text +++ tests/result/content/hrule/20200215204700.text ADDED tests/result/content/link/20200215204700.djson Index: tests/result/content/link/20200215204700.djson ================================================================== --- tests/result/content/link/20200215204700.djson +++ tests/result/content/link/20200215204700.djson @@ -0,0 +1,1 @@ +[{"t":"Para","i":[{"t":"Link","q":"external","s":"https://zettelstore.de/z","i":[{"t":"Text","s":"Home"}]},{"t":"Soft"},{"t":"Link","q":"external","s":"https://zettelstore.de","i":[{"t":"Text","s":"https://zettelstore.de"}]},{"t":"Soft"},{"t":"Link","q":"zettel","s":"00000000000100","i":[{"t":"Text","s":"Config"}]},{"t":"Soft"},{"t":"Link","q":"zettel","s":"00000000000100","i":[{"t":"Text","s":"00000000000100"}]},{"t":"Soft"},{"t":"Link","q":"self","s":"#frag","i":[{"t":"Text","s":"Frag"}]},{"t":"Soft"},{"t":"Link","q":"self","s":"#frag","i":[{"t":"Text","s":"#frag"}]},{"t":"Soft"},{"t":"Link","q":"local","s":"/hosted","i":[{"t":"Text","s":"H"}]},{"t":"Soft"},{"t":"Link","q":"based","s":"/based","i":[{"t":"Text","s":"B"}]},{"t":"Soft"},{"t":"Link","q":"local","s":"../rel","i":[{"t":"Text","s":"R"}]}]}] ADDED tests/result/content/link/20200215204700.html Index: tests/result/content/link/20200215204700.html ================================================================== --- tests/result/content/link/20200215204700.html +++ tests/result/content/link/20200215204700.html @@ -0,0 +1,9 @@ +

    Home +https://zettelstore.de +Config +00000000000100 +Frag +#frag +H +B +R

    ADDED tests/result/content/link/20200215204700.native Index: tests/result/content/link/20200215204700.native ================================================================== --- tests/result/content/link/20200215204700.native +++ tests/result/content/link/20200215204700.native @@ -0,0 +1,1 @@ +[Para Link EXTERNAL "https://zettelstore.de/z" [Text "Home"],Space,Link EXTERNAL "https://zettelstore.de" [],Space,Link ZETTEL "00000000000100" [Text "Config"],Space,Link ZETTEL "00000000000100" [],Space,Link SELF "#frag" [Text "Frag"],Space,Link SELF "#frag" [],Space,Link LOCAL "/hosted" [Text "H"],Space,Link BASED "/based" [Text "B"],Space,Link LOCAL "../rel" [Text "R"]] ADDED tests/result/content/link/20200215204700.text Index: tests/result/content/link/20200215204700.text ================================================================== --- tests/result/content/link/20200215204700.text +++ tests/result/content/link/20200215204700.text @@ -0,0 +1,1 @@ +Home Config Frag H B R ADDED tests/result/content/list/20200215204700.djson Index: tests/result/content/list/20200215204700.djson ================================================================== --- tests/result/content/list/20200215204700.djson +++ tests/result/content/list/20200215204700.djson @@ -0,0 +1,1 @@ +[{"t":"BulletList","c":[[{"t":"Para","i":[{"t":"Text","s":"Item"},{"t":"Space"},{"t":"Text","s":"1"}]}],[{"t":"Para","i":[{"t":"Text","s":"Item"},{"t":"Space"},{"t":"Text","s":"2"}]}],[{"t":"Para","i":[{"t":"Text","s":"Item"},{"t":"Space"},{"t":"Text","s":"3"}]}]]}] ADDED tests/result/content/list/20200215204700.html Index: tests/result/content/list/20200215204700.html ================================================================== --- tests/result/content/list/20200215204700.html +++ tests/result/content/list/20200215204700.html @@ -0,0 +1,5 @@ +
      +
    • Item 1
    • +
    • Item 2
    • +
    • Item 3
    • +
    ADDED tests/result/content/list/20200215204700.native Index: tests/result/content/list/20200215204700.native ================================================================== --- tests/result/content/list/20200215204700.native +++ tests/result/content/list/20200215204700.native @@ -0,0 +1,4 @@ +[BulletList + [[Para Text "Item",Space,Text "1"]], + [[Para Text "Item",Space,Text "2"]], + [[Para Text "Item",Space,Text "3"]]] ADDED tests/result/content/list/20200215204700.text Index: tests/result/content/list/20200215204700.text ================================================================== --- tests/result/content/list/20200215204700.text +++ tests/result/content/list/20200215204700.text @@ -0,0 +1,3 @@ +Item 1 +Item 2 +Item 3 ADDED tests/result/content/list/20200217194800.djson Index: tests/result/content/list/20200217194800.djson ================================================================== --- tests/result/content/list/20200217194800.djson +++ tests/result/content/list/20200217194800.djson @@ -0,0 +1,1 @@ +[{"t":"BulletList","c":[[{"t":"Para","i":[{"t":"Text","s":"Item1.1"}]}],[{"t":"Para","i":[{"t":"Text","s":"Item1.2"}]}],[{"t":"Para","i":[{"t":"Text","s":"Item1.3"}]}],[{"t":"Para","i":[{"t":"Text","s":"Item2.1"}]}],[{"t":"Para","i":[{"t":"Text","s":"Item2.2"}]}]]}] ADDED tests/result/content/list/20200217194800.html Index: tests/result/content/list/20200217194800.html ================================================================== --- tests/result/content/list/20200217194800.html +++ tests/result/content/list/20200217194800.html @@ -0,0 +1,7 @@ +
      +
    • Item1.1
    • +
    • Item1.2
    • +
    • Item1.3
    • +
    • Item2.1
    • +
    • Item2.2
    • +
    ADDED tests/result/content/list/20200217194800.native Index: tests/result/content/list/20200217194800.native ================================================================== --- tests/result/content/list/20200217194800.native +++ tests/result/content/list/20200217194800.native @@ -0,0 +1,6 @@ +[BulletList + [[Para Text "Item1.1"]], + [[Para Text "Item1.2"]], + [[Para Text "Item1.3"]], + [[Para Text "Item2.1"]], + [[Para Text "Item2.2"]]] ADDED tests/result/content/list/20200217194800.text Index: tests/result/content/list/20200217194800.text ================================================================== --- tests/result/content/list/20200217194800.text +++ tests/result/content/list/20200217194800.text @@ -0,0 +1,5 @@ +Item1.1 +Item1.2 +Item1.3 +Item2.1 +Item2.2 ADDED tests/result/content/list/20200516105700.djson Index: tests/result/content/list/20200516105700.djson ================================================================== --- tests/result/content/list/20200516105700.djson +++ tests/result/content/list/20200516105700.djson @@ -0,0 +1,1 @@ +[{"t":"BulletList","c":[[{"t":"Para","i":[{"t":"Text","s":"T1"}]},{"t":"BulletList","c":[[{"t":"Para","i":[{"t":"Text","s":"T2"}]}]]}],[{"t":"Para","i":[{"t":"Text","s":"T3"}]},{"t":"BulletList","c":[[{"t":"Para","i":[{"t":"Text","s":"T4"}]}]]}],[{"t":"Para","i":[{"t":"Text","s":"T5"}]}]]}] ADDED tests/result/content/list/20200516105700.html Index: tests/result/content/list/20200516105700.html ================================================================== --- tests/result/content/list/20200516105700.html +++ tests/result/content/list/20200516105700.html @@ -0,0 +1,14 @@ +
      +
    • T1

      +
        +
      • T2
      • +
      +
    • +
    • T3

      +
        +
      • T4
      • +
      +
    • +
    • T5

      +
    • +
    ADDED tests/result/content/list/20200516105700.native Index: tests/result/content/list/20200516105700.native ================================================================== --- tests/result/content/list/20200516105700.native +++ tests/result/content/list/20200516105700.native @@ -0,0 +1,8 @@ +[BulletList + [[Para Text "T1"], + [BulletList + [[Para Text "T2"]]]], + [[Para Text "T3"], + [BulletList + [[Para Text "T4"]]]], + [[Para Text "T5"]]] ADDED tests/result/content/list/20200516105700.text Index: tests/result/content/list/20200516105700.text ================================================================== --- tests/result/content/list/20200516105700.text +++ tests/result/content/list/20200516105700.text @@ -0,0 +1,5 @@ +T1 +T2 +T3 +T4 +T5 ADDED tests/result/content/literal/20200215204700.djson Index: tests/result/content/literal/20200215204700.djson ================================================================== --- tests/result/content/literal/20200215204700.djson +++ tests/result/content/literal/20200215204700.djson @@ -0,0 +1,1 @@ +[{"t":"Para","i":[{"t":"Input","s":"input"},{"t":"Soft"},{"t":"Code","s":"program"},{"t":"Soft"},{"t":"Output","s":"output"}]}] ADDED tests/result/content/literal/20200215204700.html Index: tests/result/content/literal/20200215204700.html ================================================================== --- tests/result/content/literal/20200215204700.html +++ tests/result/content/literal/20200215204700.html @@ -0,0 +1,3 @@ +

    input +program +output

    ADDED tests/result/content/literal/20200215204700.native Index: tests/result/content/literal/20200215204700.native ================================================================== --- tests/result/content/literal/20200215204700.native +++ tests/result/content/literal/20200215204700.native @@ -0,0 +1,1 @@ +[Para Input "input",Space,Code "program",Space,Output "output"] ADDED tests/result/content/literal/20200215204700.text Index: tests/result/content/literal/20200215204700.text ================================================================== --- tests/result/content/literal/20200215204700.text +++ tests/result/content/literal/20200215204700.text @@ -0,0 +1,1 @@ +input program output ADDED tests/result/content/mark/20200215204700.djson Index: tests/result/content/mark/20200215204700.djson ================================================================== --- tests/result/content/mark/20200215204700.djson +++ tests/result/content/mark/20200215204700.djson @@ -0,0 +1,1 @@ +[{"t":"Para","i":[{"t":"Mark","s":"mark","q":"mark"}]}] ADDED tests/result/content/mark/20200215204700.html Index: tests/result/content/mark/20200215204700.html ================================================================== --- tests/result/content/mark/20200215204700.html +++ tests/result/content/mark/20200215204700.html @@ -0,0 +1,1 @@ +

    ADDED tests/result/content/mark/20200215204700.native Index: tests/result/content/mark/20200215204700.native ================================================================== --- tests/result/content/mark/20200215204700.native +++ tests/result/content/mark/20200215204700.native @@ -0,0 +1,1 @@ +[Para Mark "mark" #mark] ADDED tests/result/content/mark/20200215204700.text Index: tests/result/content/mark/20200215204700.text ================================================================== --- tests/result/content/mark/20200215204700.text +++ tests/result/content/mark/20200215204700.text ADDED tests/result/content/paragraph/20200215185900.djson Index: tests/result/content/paragraph/20200215185900.djson ================================================================== --- tests/result/content/paragraph/20200215185900.djson +++ tests/result/content/paragraph/20200215185900.djson @@ -0,0 +1,1 @@ +[{"t":"Para","i":[{"t":"Text","s":"This"},{"t":"Space"},{"t":"Text","s":"is"},{"t":"Space"},{"t":"Text","s":"a"},{"t":"Space"},{"t":"Text","s":"zettel"},{"t":"Space"},{"t":"Text","s":"for"},{"t":"Space"},{"t":"Text","s":"testing."}]}] ADDED tests/result/content/paragraph/20200215185900.html Index: tests/result/content/paragraph/20200215185900.html ================================================================== --- tests/result/content/paragraph/20200215185900.html +++ tests/result/content/paragraph/20200215185900.html @@ -0,0 +1,1 @@ +

    This is a zettel for testing.

    ADDED tests/result/content/paragraph/20200215185900.native Index: tests/result/content/paragraph/20200215185900.native ================================================================== --- tests/result/content/paragraph/20200215185900.native +++ tests/result/content/paragraph/20200215185900.native @@ -0,0 +1,1 @@ +[Para Text "This",Space,Text "is",Space,Text "a",Space,Text "zettel",Space,Text "for",Space,Text "testing."] ADDED tests/result/content/paragraph/20200215185900.text Index: tests/result/content/paragraph/20200215185900.text ================================================================== --- tests/result/content/paragraph/20200215185900.text +++ tests/result/content/paragraph/20200215185900.text @@ -0,0 +1,1 @@ +This is a zettel for testing. ADDED tests/result/content/paragraph/20200217151800.djson Index: tests/result/content/paragraph/20200217151800.djson ================================================================== --- tests/result/content/paragraph/20200217151800.djson +++ tests/result/content/paragraph/20200217151800.djson @@ -0,0 +1,1 @@ +[{"t":"Para","i":[{"t":"Text","s":"Text"},{"t":"Space"},{"t":"Text","s":"Text"},{"t":"Soft"},{"t":"Text","s":"*abc"}]},{"t":"Para","i":[{"t":"Text","s":"Text"},{"t":"Space"},{"t":"Text","s":"Text"}]},{"t":"BulletList","c":[[{"t":"Para","i":[{"t":"Text","s":"abc"}]}]]}] ADDED tests/result/content/paragraph/20200217151800.html Index: tests/result/content/paragraph/20200217151800.html ================================================================== --- tests/result/content/paragraph/20200217151800.html +++ tests/result/content/paragraph/20200217151800.html @@ -0,0 +1,6 @@ +

    Text Text +*abc

    +

    Text Text

    +
      +
    • abc
    • +
    ADDED tests/result/content/paragraph/20200217151800.native Index: tests/result/content/paragraph/20200217151800.native ================================================================== --- tests/result/content/paragraph/20200217151800.native +++ tests/result/content/paragraph/20200217151800.native @@ -0,0 +1,4 @@ +[Para Text "Text",Space,Text "Text",Space,Text "*abc"], +[Para Text "Text",Space,Text "Text"], +[BulletList + [[Para Text "abc"]]] ADDED tests/result/content/paragraph/20200217151800.text Index: tests/result/content/paragraph/20200217151800.text ================================================================== --- tests/result/content/paragraph/20200217151800.text +++ tests/result/content/paragraph/20200217151800.text @@ -0,0 +1,3 @@ +Text Text *abc +Text Text +abc ADDED tests/result/content/png/20200512180900.djson Index: tests/result/content/png/20200512180900.djson ================================================================== --- tests/result/content/png/20200512180900.djson +++ tests/result/content/png/20200512180900.djson @@ -0,0 +1,1 @@ +[{"t":"Blob","q":"20200512180900","s":"png","o":"iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAAAAAA6fptVAAAACklEQVR4nGNiAAAABgADNjd8qAAAAABJRU5ErkJggg=="}] ADDED tests/result/content/png/20200512180900.html Index: tests/result/content/png/20200512180900.html ================================================================== --- tests/result/content/png/20200512180900.html +++ tests/result/content/png/20200512180900.html @@ -0,0 +1,1 @@ + ADDED tests/result/content/png/20200512180900.native Index: tests/result/content/png/20200512180900.native ================================================================== --- tests/result/content/png/20200512180900.native +++ tests/result/content/png/20200512180900.native @@ -0,0 +1,1 @@ +[BLOB "20200512180900" "png" "iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAAAAAA6fptVAAAACklEQVR4nGNiAAAABgADNjd8qAAAAABJRU5ErkJggg=="] ADDED tests/result/content/png/20200512180900.text Index: tests/result/content/png/20200512180900.text ================================================================== --- tests/result/content/png/20200512180900.text +++ tests/result/content/png/20200512180900.text ADDED tests/result/content/quoteblock/20200215204700.djson Index: tests/result/content/quoteblock/20200215204700.djson ================================================================== --- tests/result/content/quoteblock/20200215204700.djson +++ tests/result/content/quoteblock/20200215204700.djson @@ -0,0 +1,1 @@ +[{"t":"QuoteBlock","b":[{"t":"Para","i":[{"t":"Text","s":"To"},{"t":"Space"},{"t":"Text","s":"be"},{"t":"Space"},{"t":"Text","s":"or"},{"t":"Space"},{"t":"Text","s":"not"},{"t":"Space"},{"t":"Text","s":"to"},{"t":"Space"},{"t":"Text","s":"be."}]}],"i":[{"t":"Text","s":"Romeo"}]}] ADDED tests/result/content/quoteblock/20200215204700.html Index: tests/result/content/quoteblock/20200215204700.html ================================================================== --- tests/result/content/quoteblock/20200215204700.html +++ tests/result/content/quoteblock/20200215204700.html @@ -0,0 +1,4 @@ +
    +

    To be or not to be.

    +Romeo +
    ADDED tests/result/content/quoteblock/20200215204700.native Index: tests/result/content/quoteblock/20200215204700.native ================================================================== --- tests/result/content/quoteblock/20200215204700.native +++ tests/result/content/quoteblock/20200215204700.native @@ -0,0 +1,3 @@ +[QuoteBlock + [[Para Text "To",Space,Text "be",Space,Text "or",Space,Text "not",Space,Text "to",Space,Text "be."]], + [Cite Text "Romeo"]] ADDED tests/result/content/quoteblock/20200215204700.text Index: tests/result/content/quoteblock/20200215204700.text ================================================================== --- tests/result/content/quoteblock/20200215204700.text +++ tests/result/content/quoteblock/20200215204700.text @@ -0,0 +1,2 @@ +To be or not to be. +Romeo ADDED tests/result/content/spanblock/20200215204700.djson Index: tests/result/content/spanblock/20200215204700.djson ================================================================== --- tests/result/content/spanblock/20200215204700.djson +++ tests/result/content/spanblock/20200215204700.djson @@ -0,0 +1,1 @@ +[{"t":"SpanBlock","b":[{"t":"Para","i":[{"t":"Text","s":"A"},{"t":"Space"},{"t":"Text","s":"simple"},{"t":"Soft"},{"t":"Space","n":3},{"t":"Text","s":"span"},{"t":"Soft"},{"t":"Text","s":"and"},{"t":"Space"},{"t":"Text","s":"much"},{"t":"Space"},{"t":"Text","s":"more"}]}]}] ADDED tests/result/content/spanblock/20200215204700.html Index: tests/result/content/spanblock/20200215204700.html ================================================================== --- tests/result/content/spanblock/20200215204700.html +++ tests/result/content/spanblock/20200215204700.html @@ -0,0 +1,5 @@ +
    +

    A simple + span +and much more

    +
    ADDED tests/result/content/spanblock/20200215204700.native Index: tests/result/content/spanblock/20200215204700.native ================================================================== --- tests/result/content/spanblock/20200215204700.native +++ tests/result/content/spanblock/20200215204700.native @@ -0,0 +1,2 @@ +[SpanBlock + [[Para Text "A",Space,Text "simple",Space,Space 3,Text "span",Space,Text "and",Space,Text "much",Space,Text "more"]]] ADDED tests/result/content/spanblock/20200215204700.text Index: tests/result/content/spanblock/20200215204700.text ================================================================== --- tests/result/content/spanblock/20200215204700.text +++ tests/result/content/spanblock/20200215204700.text @@ -0,0 +1,1 @@ +A simple span and much more ADDED tests/result/content/table/20200215204700.djson Index: tests/result/content/table/20200215204700.djson ================================================================== --- tests/result/content/table/20200215204700.djson +++ tests/result/content/table/20200215204700.djson @@ -0,0 +1,1 @@ +[{"t":"Table","p":[[],[[["",[{"t":"Text","s":"c1"}]],["",[{"t":"Text","s":"c2"}]],["",[{"t":"Text","s":"c3"}]]],[["",[{"t":"Text","s":"d1"}]],["",[]],["",[{"t":"Text","s":"d3"}]]]]]}] ADDED tests/result/content/table/20200215204700.html Index: tests/result/content/table/20200215204700.html ================================================================== --- tests/result/content/table/20200215204700.html +++ tests/result/content/table/20200215204700.html @@ -0,0 +1,6 @@ + + + + + +
    c1c2c3
    d1d3
    ADDED tests/result/content/table/20200215204700.native Index: tests/result/content/table/20200215204700.native ================================================================== --- tests/result/content/table/20200215204700.native +++ tests/result/content/table/20200215204700.native @@ -0,0 +1,3 @@ +[Table + [Row [Cell Default Text "c1"],[Cell Default Text "c2"],[Cell Default Text "c3"]], + [Row [Cell Default Text "d1"],[Cell Default],[Cell Default Text "d3"]]] ADDED tests/result/content/table/20200215204700.text Index: tests/result/content/table/20200215204700.text ================================================================== --- tests/result/content/table/20200215204700.text +++ tests/result/content/table/20200215204700.text @@ -0,0 +1,2 @@ +c1 c2 c3 +d1 d3 ADDED tests/result/content/table/20200618140700.djson Index: tests/result/content/table/20200618140700.djson ================================================================== --- tests/result/content/table/20200618140700.djson +++ tests/result/content/table/20200618140700.djson @@ -0,0 +1,1 @@ +[{"t":"Table","p":[[[">",[{"t":"Text","s":"h1"}]],["",[{"t":"Text","s":"h2"}]],[":",[{"t":"Text","s":"h3"}]]],[[["<",[{"t":"Text","s":"c1"}]],["",[{"t":"Text","s":"c2"}]],[":",[{"t":"Text","s":"c3"}]]],[[">",[{"t":"Text","s":"f1"}]],["",[{"t":"Text","s":"f2"}]],[":",[{"t":"Text","s":"=f3"}]]]]]}] ADDED tests/result/content/table/20200618140700.html Index: tests/result/content/table/20200618140700.html ================================================================== --- tests/result/content/table/20200618140700.html +++ tests/result/content/table/20200618140700.html @@ -0,0 +1,9 @@ + + + + + + + + +
    h1h2h3
    c1c2c3
    f1f2=f3
    ADDED tests/result/content/table/20200618140700.native Index: tests/result/content/table/20200618140700.native ================================================================== --- tests/result/content/table/20200618140700.native +++ tests/result/content/table/20200618140700.native @@ -0,0 +1,4 @@ +[Table + [Header [Cell Right Text "h1"],[Cell Default Text "h2"],[Cell Center Text "h3"]], + [Row [Cell Left Text "c1"],[Cell Default Text "c2"],[Cell Center Text "c3"]], + [Row [Cell Right Text "f1"],[Cell Default Text "f2"],[Cell Center Text "=f3"]]] ADDED tests/result/content/table/20200618140700.text Index: tests/result/content/table/20200618140700.text ================================================================== --- tests/result/content/table/20200618140700.text +++ tests/result/content/table/20200618140700.text @@ -0,0 +1,3 @@ +h1 h2 h3 +c1 c2 c3 +f1 f2 =f3 ADDED tests/result/content/verbatim/20200215204700.djson Index: tests/result/content/verbatim/20200215204700.djson ================================================================== --- tests/result/content/verbatim/20200215204700.djson +++ tests/result/content/verbatim/20200215204700.djson @@ -0,0 +1,1 @@ +[{"t":"CodeBlock","l":["if __name__ == \"main\":"," print(\"Hello, World\")","exit(0)"]}] ADDED tests/result/content/verbatim/20200215204700.html Index: tests/result/content/verbatim/20200215204700.html ================================================================== --- tests/result/content/verbatim/20200215204700.html +++ tests/result/content/verbatim/20200215204700.html @@ -0,0 +1,4 @@ +
    if __name__ == "main":
    +  print("Hello, World")
    +exit(0)
    +
    ADDED tests/result/content/verbatim/20200215204700.native Index: tests/result/content/verbatim/20200215204700.native ================================================================== --- tests/result/content/verbatim/20200215204700.native +++ tests/result/content/verbatim/20200215204700.native @@ -0,0 +1,1 @@ +[CodeBlock "if __name__ == \"main\":\n print(\"Hello, World\")\nexit(0)"] ADDED tests/result/content/verbatim/20200215204700.text Index: tests/result/content/verbatim/20200215204700.text ================================================================== --- tests/result/content/verbatim/20200215204700.text +++ tests/result/content/verbatim/20200215204700.text @@ -0,0 +1,3 @@ +if __name__ == "main": + print("Hello, World") +exit(0) ADDED tests/result/content/verseblock/20200215204700.djson Index: tests/result/content/verseblock/20200215204700.djson ================================================================== --- tests/result/content/verseblock/20200215204700.djson +++ tests/result/content/verseblock/20200215204700.djson @@ -0,0 +1,1 @@ +[{"t":"VerseBlock","b":[{"t":"Para","i":[{"t":"Text","s":"A line"},{"t":"Hard"},{"t":"Text","s":"  another line"},{"t":"Hard"},{"t":"Text","s":"Back"}]},{"t":"Para","i":[{"t":"Text","s":"Paragraph"}]},{"t":"Para","i":[{"t":"Text","s":"    Spacy  Para"}]}],"i":[{"t":"Text","s":"Author"}]}] ADDED tests/result/content/verseblock/20200215204700.html Index: tests/result/content/verseblock/20200215204700.html ================================================================== --- tests/result/content/verseblock/20200215204700.html +++ tests/result/content/verseblock/20200215204700.html @@ -0,0 +1,8 @@ +
    +

    A line
    +  another line
    +Back

    +

    Paragraph

    +

        Spacy  Para

    +Author +
    ADDED tests/result/content/verseblock/20200215204700.native Index: tests/result/content/verseblock/20200215204700.native ================================================================== --- tests/result/content/verseblock/20200215204700.native +++ tests/result/content/verseblock/20200215204700.native @@ -0,0 +1,5 @@ +[VerseBlock + [[Para Text "A line",Break,Text "  another line",Break,Text "Back"], + [Para Text "Paragraph"], + [Para Text "    Spacy  Para"]], + [Cite Text "Author"]] ADDED tests/result/content/verseblock/20200215204700.text Index: tests/result/content/verseblock/20200215204700.text ================================================================== --- tests/result/content/verseblock/20200215204700.text +++ tests/result/content/verseblock/20200215204700.text @@ -0,0 +1,6 @@ +A line +  another line +Back +Paragraph +    Spacy  Para +Author Index: tests/result/meta/title/20200310110300.djson ================================================================== --- tests/result/meta/title/20200310110300.djson +++ tests/result/meta/title/20200310110300.djson @@ -1,1 +1,1 @@ -{"title":[{"t":"Text","s":"A"},{"t":"Space"},{"t":"Quote","i":[{"t":"Text","s":"Title"}]},{"t":"Space"},{"t":"Text","s":"with"},{"t":"Space"},{"t":"Emph","i":[{"t":"Text","s":"Markup"}]},{"t":"Text","s":","},{"t":"Space"},{"t":"Code","a":{"":"zmk"},"s":"Zettelmarkup"}],"role":"zettel","syntax":"zmk"} +{"title":[{"t":"Text","s":"A"},{"t":"Space"},{"t":"Quote","i":[{"t":"Text","s":"Title"}]},{"t":"Space"},{"t":"Text","s":"with"},{"t":"Space"},{"t":"Italic","i":[{"t":"Text","s":"Markup"}]},{"t":"Text","s":","},{"t":"Space"},{"t":"Code","a":{"":"zmk"},"s":"Zettelmarkup"}],"role":"zettel","syntax":"zmk"} Index: tests/result/meta/title/20200310110300.native ================================================================== --- tests/result/meta/title/20200310110300.native +++ tests/result/meta/title/20200310110300.native @@ -1,3 +1,3 @@ -[Title Text "A",Space,Quote [Text "Title"],Space,Text "with",Space,Emph [Text "Markup"],Text ",",Space,Code ("zmk",[]) "Zettelmarkup"] +[Title Text "A",Space,Quote [Text "Title"],Space,Text "with",Space,Italic [Text "Markup"],Text ",",Space,Code ("zmk",[]) "Zettelmarkup"] [Role "zettel"] [Syntax "zmk"] Index: tools/build.go ================================================================== --- tools/build.go +++ tools/build.go @@ -129,32 +129,29 @@ return calcVersion(base, vcs) } func findExec(cmd string) string { if path, err := executeCommand(nil, "which", cmd); err == nil && path != "" { - return strings.TrimSpace(path) + return path } return "" } -func cmdCheck(forRelease bool) error { +func cmdCheck() error { if err := checkGoTest("./..."); err != nil { return err } if err := checkGoVet(); err != nil { return err } if err := checkGoLint(); err != nil { return err } - if err := checkShadow(forRelease); err != nil { + if err := checkGoVetShadow(); err != nil { return err } - if err := checkStaticcheck(forRelease); err != nil { - return err - } - if err := checkUnparam(forRelease); err != nil { + if err := checkStaticcheck(); err != nil { return err } return checkFossilExtra() } @@ -191,71 +188,35 @@ fmt.Fprint(os.Stderr, out) } return err } -func checkShadow(forRelease bool) error { - path, err := findExecStrict("shadow", forRelease) +func checkGoVetShadow() error { + path := findExec("shadow") if path == "" { - return err + return nil } - out, err := executeCommand(nil, path, "-strict", "./...") + out, err := executeCommand(nil, "go", "vet", "-vettool", strings.TrimSpace(path), "./...") if err != nil { fmt.Fprintln(os.Stderr, "Some shadowed variables found") if len(out) > 0 { fmt.Fprintln(os.Stderr, out) } } return err } - -func checkStaticcheck(forRelease bool) error { - path, err := findExecStrict("staticcheck", forRelease) - if path == "" { - return err - } - out, err := executeCommand(nil, path, "./...") +func checkStaticcheck() error { + out, err := executeCommand(nil, "staticcheck", "./...") if err != nil { fmt.Fprintln(os.Stderr, "Some staticcheck problems found") if len(out) > 0 { fmt.Fprintln(os.Stderr, out) } } return err } -func checkUnparam(forRelease bool) error { - path, err := findExecStrict("unparam", forRelease) - if path == "" { - return err - } - out, err := executeCommand(nil, path, "./...") - if err != nil { - fmt.Fprintln(os.Stderr, "Some unparam problems found") - if len(out) > 0 { - fmt.Fprintln(os.Stderr, out) - } - } - if forRelease { - if out2, err2 := executeCommand(nil, path, "-exported", "-tests", "./..."); err2 != nil { - fmt.Fprintln(os.Stderr, "Some optional unparam problems found") - if len(out2) > 0 { - fmt.Fprintln(os.Stderr, out2) - } - } - } - return err -} - -func findExecStrict(cmd string, forRelease bool) (string, error) { - path := findExec(cmd) - if path != "" || !forRelease { - return path, nil - } - return "", errors.New("Command '" + cmd + "' not installed, but required for release") -} - func checkFossilExtra() error { out, err := executeCommand(nil, "fossil", "extra") if err != nil { fmt.Fprintln(os.Stderr, "Unable to execute 'fossil extra'") return err @@ -287,11 +248,11 @@ err = startZettelstore(&info) } if err != nil { return err } - err = checkGoTest("zettelstore.de/z/tests/client", "-base-url", "http://127.0.0.1:23123") + err = checkGoTest("zettelstore.de/z/client", "-base-url", "http://127.0.0.1:23123") if needServer { err1 := stopZettelstore(&info) if err == nil { err = err1 } @@ -427,11 +388,11 @@ } return base, fossil } func cmdRelease() error { - if err := cmdCheck(true); err != nil { + if err := cmdCheck(); err != nil { return err } base, fossil := getReleaseVersionData() releases := []struct { arch string @@ -536,22 +497,21 @@ Options: -v Verbose output. Commands: - build Build the software for local computer. - check Check current working state: execute tests, - static analysis tools, extra files, ... - Is automatically done when releasing the software. - clean Remove all build and release directories. - help Outputs this text. - manual Create a ZIP file with all manual zettel - relcheck Check current working state for release. - release Create the software for various platforms and put them in - appropriate named ZIP files. - testapi Starts a Zettelstore and execute API tests. - version Print the current version of the software. + build Build the software for local computer. + check Check current working state: execute tests, static analysis tools, + extra files, ... + Is automatically done when releasing the software. + clean Remove all build and release directories. + help Outputs this text. + manual Create a ZIP file with all manual zettel + release Create the software for various platforms and put them in + appropriate named ZIP files. + testapi Starts a Zettelstore and execute API tests. + version Print the current version of the software. All commands can be abbreviated as long as they remain unique.`) } var ( @@ -576,13 +536,11 @@ case "cl", "cle", "clea", "clean": err = cmdClean() case "v", "ve", "ver", "vers", "versi", "versio", "version": fmt.Print(getVersion()) case "ch", "che", "chec", "check": - err = cmdCheck(false) - case "relc", "relch", "relche", "relchec", "relcheck": - err = cmdCheck(true) + err = cmdCheck() case "t", "te", "tes", "test", "testa", "testap", "testapi": cmdTestAPI() case "h", "he", "hel", "help": cmdHelp() default: Index: usecase/authenticate.go ================================================================== --- usecase/authenticate.go +++ usecase/authenticate.go @@ -14,11 +14,10 @@ import ( "context" "math/rand" "time" - "zettelstore.de/c/api" "zettelstore.de/z/auth" "zettelstore.de/z/auth/cred" "zettelstore.de/z/domain/id" "zettelstore.de/z/domain/meta" "zettelstore.de/z/search" @@ -54,19 +53,19 @@ if identMeta == nil || err != nil { compensateCompare() return nil, err } - if hashCred, ok := identMeta.Get(api.KeyCredential); ok { - ok, err = cred.CompareHashAndCredential(hashCred, identMeta.Zid, ident, credential) + if hashCred, ok := identMeta.Get(meta.KeyCredential); ok { + ok, err := cred.CompareHashAndCredential(hashCred, identMeta.Zid, ident, credential) if err != nil { return nil, err } if ok { - token, err2 := uc.token.GetToken(identMeta, d, k) - if err2 != nil { - return nil, err2 + token, err := uc.token.GetToken(identMeta, d, k) + if err != nil { + return nil, err } return token, nil } return nil, nil } Index: usecase/context.go ================================================================== --- usecase/context.go +++ usecase/context.go @@ -12,11 +12,10 @@ package usecase import ( "context" - "zettelstore.de/c/api" "zettelstore.de/z/domain/id" "zettelstore.de/z/domain/meta" ) // ZettelContextPort is the interface used by this use case. @@ -23,25 +22,18 @@ type ZettelContextPort interface { // GetMeta retrieves just the meta data of a specific zettel. GetMeta(ctx context.Context, zid id.Zid) (*meta.Meta, error) } -// ZettelContextConfig is the interface to allow the usecase to read some config data. -type ZettelContextConfig interface { - // GetHomeZettel returns the value of the "home-zettel" key. - GetHomeZettel() id.Zid -} - // ZettelContext is the data for this use case. type ZettelContext struct { - port ZettelContextPort - config ZettelContextConfig + port ZettelContextPort } // NewZettelContext creates a new use case. -func NewZettelContext(port ZettelContextPort, config ZettelContextConfig) ZettelContext { - return ZettelContext{port: port, config: config} +func NewZettelContext(port ZettelContextPort) ZettelContext { + return ZettelContext{port: port} } // ZettelContextDirection determines the way, the context is calculated. type ZettelContextDirection int @@ -57,105 +49,85 @@ func (uc ZettelContext) Run(ctx context.Context, zid id.Zid, dir ZettelContextDirection, depth, limit int) (result []*meta.Meta, err error) { start, err := uc.port.GetMeta(ctx, zid) if err != nil { return nil, err } - tasks := newQueue(start, depth, limit, uc.config.GetHomeZettel()) + tasks := ztlCtx{depth: depth} + tasks.add(start, 0) + visited := id.NewSet() isBackward := dir == ZettelContextBoth || dir == ZettelContextBackward isForward := dir == ZettelContextBoth || dir == ZettelContextForward - for { - m, curDepth, found := tasks.next() - if !found { + for !tasks.empty() { + m, curDepth := tasks.pop() + if _, ok := visited[m.Zid]; ok { + continue + } + visited[m.Zid] = true + result = append(result, m) + if limit > 0 && len(result) > limit { // start is the first element of result break } - result = append(result, m) - + curDepth++ for _, p := range m.PairsRest(true) { - tasks.addPair(ctx, uc.port, p.Key, p.Value, curDepth+1, isBackward, isForward) + if p.Key == meta.KeyBackward { + if isBackward { + uc.addIDSet(ctx, &tasks, curDepth, p.Value) + } + continue + } + if p.Key == meta.KeyForward { + if isForward { + uc.addIDSet(ctx, &tasks, curDepth, p.Value) + } + continue + } + if p.Key != meta.KeyBack { + hasInverse := meta.Inverse(p.Key) != "" + if (!hasInverse || !isBackward) && (hasInverse || !isForward) { + continue + } + if t := meta.Type(p.Key); t == meta.TypeID { + uc.addID(ctx, &tasks, curDepth, p.Value) + } else if t == meta.TypeIDSet { + uc.addIDSet(ctx, &tasks, curDepth, p.Value) + } + } } } return result, nil } + +func (uc ZettelContext) addID(ctx context.Context, tasks *ztlCtx, depth int, value string) { + if zid, err := id.Parse(value); err == nil { + if m, err := uc.port.GetMeta(ctx, zid); err == nil { + tasks.add(m, depth) + } + } +} + +func (uc ZettelContext) addIDSet(ctx context.Context, tasks *ztlCtx, depth int, value string) { + for _, val := range meta.ListFromValue(value) { + uc.addID(ctx, tasks, depth, val) + } +} type ztlCtxTask struct { next *ztlCtxTask meta *meta.Meta depth int } -type contextQueue struct { - home id.Zid - seen id.Set - first *ztlCtxTask - last *ztlCtxTask - maxDepth int - limit int -} - -func newQueue(m *meta.Meta, maxDepth, limit int, home id.Zid) *contextQueue { - task := &ztlCtxTask{ - next: nil, - meta: m, - depth: 0, - } - result := &contextQueue{ - home: home, - seen: id.NewSet(), - first: task, - last: task, - maxDepth: maxDepth, - limit: limit, - } - return result -} - -func (zc *contextQueue) addPair( - ctx context.Context, port ZettelContextPort, - key, value string, - curDepth int, isBackward, isForward bool, -) { - if key == api.KeyBackward { - if isBackward { - zc.addIDSet(ctx, port, curDepth, value) - } - return - } - if key == api.KeyForward { - if isForward { - zc.addIDSet(ctx, port, curDepth, value) - } - return - } - if key == api.KeyBack { - return - } - hasInverse := meta.Inverse(key) != "" - if (!hasInverse || !isBackward) && (hasInverse || !isForward) { - return - } - if t := meta.Type(key); t == meta.TypeID { - zc.addID(ctx, port, curDepth, value) - } else if t == meta.TypeIDSet { - zc.addIDSet(ctx, port, curDepth, value) - } -} - -func (zc *contextQueue) addID(ctx context.Context, port ZettelContextPort, depth int, value string) { - if (zc.maxDepth > 0 && depth > zc.maxDepth) || zc.hasLimit() { - return - } - - zid, err := id.Parse(value) - if err != nil || zid == zc.home { - return - } - - m, err := port.GetMeta(ctx, zid) - if err != nil { - return - } - +type ztlCtx struct { + first *ztlCtxTask + last *ztlCtxTask + depth int +} + +func (zc *ztlCtx) add(m *meta.Meta, depth int) { + if zc.depth > 0 && depth > zc.depth { + return + } task := &ztlCtxTask{next: nil, meta: m, depth: depth} if zc.first == nil { zc.first = task zc.last = task } else { @@ -162,37 +134,20 @@ zc.last.next = task zc.last = task } } -func (zc *contextQueue) addIDSet(ctx context.Context, port ZettelContextPort, curDepth int, value string) { - for _, val := range meta.ListFromValue(value) { - zc.addID(ctx, port, curDepth, val) - } -} - -func (zc *contextQueue) next() (*meta.Meta, int, bool) { - if zc.hasLimit() { - return nil, -1, false - } - for zc.first != nil { - task := zc.first - zc.first = task.next - if zc.first == nil { - zc.last = nil - } - m := task.meta - zid := m.Zid - _, found := zc.seen[zid] - if found { - continue - } - zc.seen[zid] = true - return m, task.depth, true - } - return nil, -1, false -} - -func (zc *contextQueue) hasLimit() bool { - limit := zc.limit - return limit > 0 && len(zc.seen) > limit +func (zc *ztlCtx) empty() bool { + return zc.first == nil +} + +func (zc *ztlCtx) pop() (*meta.Meta, int) { + task := zc.first + if task == nil { + return nil, -1 + } + zc.first = task.next + if zc.first == nil { + zc.last = nil + } + return task.meta, task.depth } Index: usecase/copy_zettel.go ================================================================== --- usecase/copy_zettel.go +++ usecase/copy_zettel.go @@ -10,12 +10,12 @@ // Package usecase provides (business) use cases for the zettelstore. package usecase import ( - "zettelstore.de/c/api" "zettelstore.de/z/domain" + "zettelstore.de/z/domain/meta" ) // CopyZettel is the data for this use case. type CopyZettel struct{} @@ -23,19 +23,19 @@ func NewCopyZettel() CopyZettel { return CopyZettel{} } // Run executes the use case. -func (CopyZettel) Run(origZettel domain.Zettel) domain.Zettel { +func (uc CopyZettel) Run(origZettel domain.Zettel) domain.Zettel { m := origZettel.Meta.Clone() - if title, ok := m.Get(api.KeyTitle); ok { + if title, ok := m.Get(meta.KeyTitle); ok { if len(title) > 0 { title = "Copy of " + title } else { title = "Copy" } - m.Set(api.KeyTitle, title) + m.Set(meta.KeyTitle, title) } content := origZettel.Content content.TrimSpace() return domain.Zettel{Meta: m, Content: content} } Index: usecase/create_zettel.go ================================================================== --- usecase/create_zettel.go +++ usecase/create_zettel.go @@ -12,14 +12,14 @@ package usecase import ( "context" - "zettelstore.de/c/api" "zettelstore.de/z/config" "zettelstore.de/z/domain" "zettelstore.de/z/domain/id" + "zettelstore.de/z/domain/meta" ) // CreateZettelPort is the interface used by this use case. type CreateZettelPort interface { // CreateZettel creates a new zettel. @@ -44,19 +44,20 @@ func (uc CreateZettel) Run(ctx context.Context, zettel domain.Zettel) (id.Zid, error) { m := zettel.Meta if m.Zid.IsValid() { return m.Zid, nil // TODO: new error: already exists } - if title, ok := m.Get(api.KeyTitle); !ok || title == "" { - m.Set(api.KeyTitle, uc.rtConfig.GetDefaultTitle()) - } - if role, ok := m.Get(api.KeyRole); !ok || role == "" { - m.Set(api.KeyRole, uc.rtConfig.GetDefaultRole()) - } - if syntax, ok := m.Get(api.KeySyntax); !ok || syntax == "" { - m.Set(api.KeySyntax, uc.rtConfig.GetDefaultSyntax()) + + if title, ok := m.Get(meta.KeyTitle); !ok || title == "" { + m.Set(meta.KeyTitle, uc.rtConfig.GetDefaultTitle()) + } + if role, ok := m.Get(meta.KeyRole); !ok || role == "" { + m.Set(meta.KeyRole, uc.rtConfig.GetDefaultRole()) + } + if syntax, ok := m.Get(meta.KeySyntax); !ok || syntax == "" { + m.Set(meta.KeySyntax, uc.rtConfig.GetDefaultSyntax()) } m.YamlSep = uc.rtConfig.GetYAMLHeader() zettel.Content.TrimSpace() return uc.port.CreateZettel(ctx, zettel) } Index: usecase/folge_zettel.go ================================================================== --- usecase/folge_zettel.go +++ usecase/folge_zettel.go @@ -10,11 +10,10 @@ // Package usecase provides (business) use cases for the zettelstore. package usecase import ( - "zettelstore.de/c/api" "zettelstore.de/z/config" "zettelstore.de/z/domain" "zettelstore.de/z/domain/id" "zettelstore.de/z/domain/meta" ) @@ -31,19 +30,19 @@ // Run executes the use case. func (uc FolgeZettel) Run(origZettel domain.Zettel) domain.Zettel { origMeta := origZettel.Meta m := meta.New(id.Invalid) - if title, ok := origMeta.Get(api.KeyTitle); ok { + if title, ok := origMeta.Get(meta.KeyTitle); ok { if len(title) > 0 { title = "Folge of " + title } else { title = "Folge" } - m.Set(api.KeyTitle, title) + m.Set(meta.KeyTitle, title) } - m.Set(api.KeyRole, config.GetRole(origMeta, uc.rtConfig)) - m.Set(api.KeyTags, origMeta.GetDefault(api.KeyTags, "")) - m.Set(api.KeySyntax, uc.rtConfig.GetDefaultSyntax()) - m.Set(api.KeyPrecursor, origMeta.Zid.String()) - return domain.Zettel{Meta: m, Content: domain.NewContent(nil)} + m.Set(meta.KeyRole, config.GetRole(origMeta, uc.rtConfig)) + m.Set(meta.KeyTags, origMeta.GetDefault(meta.KeyTags, "")) + m.Set(meta.KeySyntax, uc.rtConfig.GetDefaultSyntax()) + m.Set(meta.KeyPrecursor, origMeta.Zid.String()) + return domain.Zettel{Meta: m, Content: domain.NewContent("")} } Index: usecase/get_user.go ================================================================== --- usecase/get_user.go +++ usecase/get_user.go @@ -12,11 +12,10 @@ package usecase import ( "context" - "zettelstore.de/c/api" "zettelstore.de/z/auth" "zettelstore.de/z/box" "zettelstore.de/z/domain/id" "zettelstore.de/z/domain/meta" "zettelstore.de/z/search" @@ -48,21 +47,21 @@ // It is important to try first with the owner. First, because another user // could give herself the same ''ident''. Second, in most cases the owner // will authenticate. identMeta, err := uc.port.GetMeta(ctx, uc.authz.Owner()) - if err == nil && identMeta.GetDefault(api.KeyUserID, "") == ident { - if role, ok := identMeta.Get(api.KeyRole); !ok || - role != api.ValueRoleUser { + if err == nil && identMeta.GetDefault(meta.KeyUserID, "") == ident { + if role, ok := identMeta.Get(meta.KeyRole); !ok || + role != meta.ValueRoleUser { return nil, nil } return identMeta, nil } // Owner was not found or has another ident. Try via list search. var s *search.Search - s = s.AddExpr(api.KeyRole, api.ValueRoleUser) - s = s.AddExpr(api.KeyUserID, ident) + s = s.AddExpr(meta.KeyRole, meta.ValueRoleUser) + s = s.AddExpr(meta.KeyUserID, ident) metaList, err := uc.port.SelectMeta(ctx, s) if err != nil { return nil, err } if len(metaList) < 1 { @@ -94,10 +93,10 @@ userMeta, err := uc.port.GetMeta(box.NoEnrichContext(ctx), zid) if err != nil { return nil, err } - if val, ok := userMeta.Get(api.KeyUserID); !ok || val != ident { + if val, ok := userMeta.Get(meta.KeyUserID); !ok || val != ident { return nil, nil } return userMeta, nil } Index: usecase/list_role.go ================================================================== --- usecase/list_role.go +++ usecase/list_role.go @@ -13,11 +13,10 @@ import ( "context" "sort" - "zettelstore.de/c/api" "zettelstore.de/z/box" "zettelstore.de/z/domain/meta" "zettelstore.de/z/search" ) @@ -43,11 +42,11 @@ if err != nil { return nil, err } roles := make(map[string]bool, 8) for _, m := range metas { - if role, ok := m.Get(api.KeyRole); ok && role != "" { + if role, ok := m.Get(meta.KeyRole); ok && role != "" { roles[role] = true } } result := make([]string, 0, len(roles)) for role := range roles { Index: usecase/list_tags.go ================================================================== --- usecase/list_tags.go +++ usecase/list_tags.go @@ -12,11 +12,10 @@ package usecase import ( "context" - "zettelstore.de/c/api" "zettelstore.de/z/domain/meta" "zettelstore.de/z/search" ) // ListTagsPort is the interface used by this use case. @@ -44,11 +43,11 @@ if err != nil { return nil, err } result := make(TagData) for _, m := range metas { - if tl, ok := m.GetList(api.KeyAllTags); ok && len(tl) > 0 { + if tl, ok := m.GetList(meta.KeyAllTags); ok && len(tl) > 0 { for _, t := range tl { result[t] = append(result[t], m) } } } Index: usecase/new_zettel.go ================================================================== --- usecase/new_zettel.go +++ usecase/new_zettel.go @@ -10,11 +10,10 @@ // Package usecase provides (business) use cases for the zettelstore. package usecase import ( - "zettelstore.de/c/api" "zettelstore.de/z/domain" "zettelstore.de/z/domain/id" "zettelstore.de/z/domain/meta" ) @@ -25,17 +24,17 @@ func NewNewZettel() NewZettel { return NewZettel{} } // Run executes the use case. -func (NewZettel) Run(origZettel domain.Zettel) domain.Zettel { +func (uc NewZettel) Run(origZettel domain.Zettel) domain.Zettel { m := meta.New(id.Invalid) om := origZettel.Meta - m.Set(api.KeyTitle, om.GetDefault(api.KeyTitle, "")) - m.Set(api.KeyRole, om.GetDefault(api.KeyRole, "")) - m.Set(api.KeyTags, om.GetDefault(api.KeyTags, "")) - m.Set(api.KeySyntax, om.GetDefault(api.KeySyntax, "")) + m.Set(meta.KeyTitle, om.GetDefault(meta.KeyTitle, "")) + m.Set(meta.KeyRole, om.GetDefault(meta.KeyRole, "")) + m.Set(meta.KeyTags, om.GetDefault(meta.KeyTags, "")) + m.Set(meta.KeySyntax, om.GetDefault(meta.KeySyntax, "")) const prefixLen = len(meta.NewPrefix) for _, pair := range om.PairsRest(false) { if key := pair.Key; len(key) > prefixLen && key[0:prefixLen] == meta.NewPrefix { m.Set(key[prefixLen:], pair.Value) Index: usecase/order.go ================================================================== --- usecase/order.go +++ usecase/order.go @@ -43,13 +43,13 @@ zn, err := uc.evaluate.Run(ctx, zid, syntax, nil) if err != nil { return nil, nil, err } for _, ref := range collect.Order(zn) { - if collectedZid, err2 := id.Parse(ref.URL.Path); err2 == nil { - if m, err3 := uc.port.GetMeta(ctx, collectedZid); err3 == nil { + if zid, err := id.Parse(ref.URL.Path); err == nil { + if m, err := uc.port.GetMeta(ctx, zid); err == nil { result = append(result, m) } } } return zn.Meta, result, nil } Index: usecase/update_zettel.go ================================================================== --- usecase/update_zettel.go +++ usecase/update_zettel.go @@ -12,14 +12,14 @@ package usecase import ( "context" - "zettelstore.de/c/api" "zettelstore.de/z/box" "zettelstore.de/z/domain" "zettelstore.de/z/domain/id" + "zettelstore.de/z/domain/meta" ) // UpdateZettelPort is the interface used by this use case. type UpdateZettelPort interface { // GetZettel retrieves a specific zettel. @@ -47,16 +47,16 @@ return err } if zettel.Equal(oldZettel, false) { return nil } - m.SetNow(api.KeyModified) + m.SetNow(meta.KeyModified) m.YamlSep = oldZettel.Meta.YamlSep if m.Zid == id.ConfigurationZid { - m.Set(api.KeySyntax, api.ValueSyntaxNone) + m.Set(meta.KeySyntax, meta.ValueSyntaxNone) } if !hasContent { zettel.Content = oldZettel.Content zettel.Content.TrimSpace() } return uc.port.UpdateZettel(ctx, zettel) } Index: web/adapter/api/api.go ================================================================== --- web/adapter/api/api.go +++ web/adapter/api/api.go @@ -13,11 +13,11 @@ import ( "context" "time" - "zettelstore.de/c/api" + "zettelstore.de/z/api" "zettelstore.de/z/auth" "zettelstore.de/z/config" "zettelstore.de/z/domain/meta" "zettelstore.de/z/kernel" "zettelstore.de/z/web/server" Index: web/adapter/api/content_type.go ================================================================== --- web/adapter/api/content_type.go +++ web/adapter/api/content_type.go @@ -9,11 +9,11 @@ //----------------------------------------------------------------------------- // Package api provides api handlers for web requests. package api -import "zettelstore.de/c/api" +import "zettelstore.de/z/api" const ctHTML = "text/html; charset=utf-8" const ctJSON = "application/json" const ctPlainText = "text/plain; charset=utf-8" @@ -24,14 +24,15 @@ api.EncoderText: ctPlainText, api.EncoderZmk: ctPlainText, } func encoding2ContentType(enc api.EncodingEnum) string { - if ct, ok := mapEncoding2CT[enc]; ok { - return ct + ct, ok := mapEncoding2CT[enc] + if !ok { + return "application/octet-stream" } - return "application/octet-stream" + return ct } var mapSyntax2CT = map[string]string{ "css": "text/css; charset=utf-8", "gif": "image/gif", Index: web/adapter/api/create_zettel.go ================================================================== --- web/adapter/api/create_zettel.go +++ web/adapter/api/create_zettel.go @@ -12,11 +12,11 @@ package api import ( "net/http" - "zettelstore.de/c/api" + zsapi "zettelstore.de/z/api" "zettelstore.de/z/domain/id" "zettelstore.de/z/usecase" "zettelstore.de/z/web/adapter" ) @@ -34,13 +34,14 @@ newZid, err := createZettel.Run(ctx, zettel) if err != nil { adapter.ReportUsecaseError(w, err) return } - u := a.NewURLBuilder('z').SetZid(api.ZettelID(newZid.String())).String() - h := adapter.PrepareHeader(w, ctPlainText) - h.Set(api.HeaderLocation, u) + u := a.NewURLBuilder('z').SetZid(newZid).String() + h := w.Header() + h.Set(zsapi.HeaderContentType, ctPlainText) + h.Set(zsapi.HeaderLocation, u) w.WriteHeader(http.StatusCreated) if _, err = w.Write(newZid.Bytes()); err != nil { adapter.InternalServerError(w, "Write Plain", err) } } @@ -60,14 +61,15 @@ newZid, err := createZettel.Run(ctx, zettel) if err != nil { adapter.ReportUsecaseError(w, err) return } - u := a.NewURLBuilder('j').SetZid(api.ZettelID(newZid.String())).String() - h := adapter.PrepareHeader(w, ctJSON) - h.Set(api.HeaderLocation, u) + u := a.NewURLBuilder('j').SetZid(newZid).String() + h := w.Header() + h.Set(zsapi.HeaderContentType, ctJSON) + h.Set(zsapi.HeaderLocation, u) w.WriteHeader(http.StatusCreated) - if err = encodeJSONData(w, api.ZidJSON{ID: api.ZettelID(newZid.String())}); err != nil { + if err = encodeJSONData(w, zsapi.ZidJSON{ID: newZid.String()}); err != nil { adapter.InternalServerError(w, "Write JSON", err) } } } Index: web/adapter/api/delete_zettel.go ================================================================== --- web/adapter/api/delete_zettel.go +++ web/adapter/api/delete_zettel.go @@ -26,12 +26,12 @@ if err != nil { http.NotFound(w, r) return } - if err = deleteZettel.Run(r.Context(), zid); err != nil { + if err := deleteZettel.Run(r.Context(), zid); err != nil { adapter.ReportUsecaseError(w, err) return } w.WriteHeader(http.StatusNoContent) } } DELETED web/adapter/api/encode_inlines.go Index: web/adapter/api/encode_inlines.go ================================================================== --- web/adapter/api/encode_inlines.go +++ web/adapter/api/encode_inlines.go @@ -1,95 +0,0 @@ -//----------------------------------------------------------------------------- -// Copyright (c) 2021 Detlef Stern -// -// This file is part of zettelstore. -// -// Zettelstore is licensed under the latest version of the EUPL (European Union -// Public License). Please see file LICENSE.txt for your rights and obligations -// under this license. -//----------------------------------------------------------------------------- - -// Package api provides api handlers for web requests. -package api - -import ( - "bytes" - "encoding/json" - "net/http" - - "zettelstore.de/c/api" - "zettelstore.de/z/ast" - "zettelstore.de/z/encoder" - "zettelstore.de/z/evaluator" - "zettelstore.de/z/usecase" - "zettelstore.de/z/web/adapter" -) - -// MakePostEncodeInlinesHandler creates a new HTTP handler to encode given -// Zettelmarkup inline material -func (a *API) MakePostEncodeInlinesHandler(evaluate usecase.Evaluate) http.HandlerFunc { - return func(w http.ResponseWriter, r *http.Request) { - dec := json.NewDecoder(r.Body) - var reqJSON api.EncodeInlineReqJSON - if err := dec.Decode(&reqJSON); err != nil { - http.Error(w, "Unable to read request", http.StatusBadRequest) - return - } - envEnc := &encoder.Environment{ - Lang: reqJSON.Lang, - Interactive: reqJSON.NoLinks, - } - if envEnc.Lang == "" { - envEnc.Lang = a.rtConfig.GetDefaultLang() - } - htmlEnc := encoder.Create(api.EncoderHTML, envEnc) - ctx := r.Context() - envEval := evaluator.Environment{} - var respJSON api.EncodedInlineRespJSON - if iln := evaluate.RunMetadata(ctx, reqJSON.FirstZmk, &envEval); iln != nil { - s, err := encodeInlines(htmlEnc, iln) - if err != nil { - http.Error(w, "Unable to encode first as HTML", http.StatusBadRequest) - return - } - respJSON.FirstHTML = s - - s, err = encodeInlines(encoder.Create(api.EncoderText, nil), iln) - if err != nil { - http.Error(w, "Unable to encode first as Text", http.StatusBadRequest) - return - } - respJSON.FirstText = s - } - - if reqLen := len(reqJSON.OtherZmk); reqLen > 0 { - respJSON.OtherHTML = make([]string, reqLen) - for i, zmk := range reqJSON.OtherZmk { - iln := evaluate.RunMetadata(ctx, zmk, &envEval) - if iln == nil { - continue - } - s, err := encodeInlines(htmlEnc, iln) - if err != nil { - http.Error(w, "Unable to encode other as HTML", http.StatusBadRequest) - return - } - respJSON.OtherHTML[i] = s - } - } - - adapter.PrepareHeader(w, ctJSON) - err := encodeJSONData(w, respJSON) - if err != nil { - adapter.InternalServerError(w, "Write JSON for encoded Zettelmarkup", err) - } - } -} - -func encodeInlines(encdr encoder.Encoder, inl *ast.InlineListNode) (string, error) { - var buf bytes.Buffer - _, err := encdr.WriteInlines(&buf, inl) - if err != nil { - return "", err - } - return buf.String(), nil -} Index: web/adapter/api/get_eval_zettel.go ================================================================== --- web/adapter/api/get_eval_zettel.go +++ web/adapter/api/get_eval_zettel.go @@ -12,14 +12,14 @@ package api import ( "net/http" - "zettelstore.de/c/api" + zsapi "zettelstore.de/z/api" "zettelstore.de/z/ast" - "zettelstore.de/z/domain" "zettelstore.de/z/domain/id" + "zettelstore.de/z/domain/meta" "zettelstore.de/z/encoder" "zettelstore.de/z/evaluator" "zettelstore.de/z/usecase" "zettelstore.de/z/web/adapter" ) @@ -35,20 +35,18 @@ ctx := r.Context() q := r.URL.Query() enc, encStr := adapter.GetEncoding(r, q, encoder.GetDefaultEncoding()) part := getPart(q, partContent) - var env evaluator.Environment - if enc == api.EncoderHTML { - env.GetImageMaterial = func(zettel domain.Zettel, syntax string) ast.MaterialNode { - return &ast.BLOBMaterialNode{ - Blob: zettel.Content.AsBytes(), - Syntax: syntax, - } - } - } - zn, err := evaluate.Run(ctx, zid, q.Get(api.KeySyntax), &env) + var embedImage bool + if enc == zsapi.EncoderHTML { + embedImage = true + } + env := evaluator.Environment{ + EmbedImage: embedImage, + } + zn, err := evaluate.Run(ctx, zid, q.Get(meta.KeySyntax), &env) if err != nil { adapter.ReportUsecaseError(w, err) return } evalMeta := func(value string) *ast.InlineListNode { Index: web/adapter/api/get_links.go ================================================================== --- web/adapter/api/get_links.go +++ web/adapter/api/get_links.go @@ -12,11 +12,11 @@ package api import ( "net/http" - "zettelstore.de/c/api" + zsapi "zettelstore.de/z/api" "zettelstore.de/z/ast" "zettelstore.de/z/collect" "zettelstore.de/z/domain/id" "zettelstore.de/z/domain/meta" "zettelstore.de/z/usecase" @@ -31,18 +31,20 @@ http.NotFound(w, r) return } ctx := r.Context() q := r.URL.Query() - zn, err := evaluate.Run(ctx, zid, q.Get(api.KeySyntax), nil) + zn, err := evaluate.Run(ctx, zid, q.Get(meta.KeySyntax), nil) if err != nil { adapter.ReportUsecaseError(w, err) return } summary := collect.References(zn) - outData := api.ZettelLinksJSON{ID: api.ZettelID(zid.String())} + outData := zsapi.ZettelLinksJSON{ID: zid.String()} + // TODO: calculate incoming links from other zettel (via "backward" metadata?) + outData.Linked.Incoming = nil zetRefs, locRefs, extRefs := collect.DivideReferences(summary.Links) outData.Linked.Outgoing = idRefs(zetRefs) outData.Linked.Local = stringRefs(locRefs) outData.Linked.External = stringRefs(extRefs) for _, p := range zn.Meta.PairsRest(false) { @@ -56,11 +58,11 @@ outData.Embedded.Local = stringRefs(locRefs) outData.Embedded.External = stringRefs(extRefs) outData.Cites = stringCites(summary.Cites) - adapter.PrepareHeader(w, ctJSON) + w.Header().Set(zsapi.HeaderContentType, ctJSON) encodeJSONData(w, outData) } } func idRefs(refs []*ast.Reference) []string { Index: web/adapter/api/get_order.go ================================================================== --- web/adapter/api/get_order.go +++ web/adapter/api/get_order.go @@ -12,12 +12,12 @@ package api import ( "net/http" - "zettelstore.de/c/api" "zettelstore.de/z/domain/id" + "zettelstore.de/z/domain/meta" "zettelstore.de/z/usecase" "zettelstore.de/z/web/adapter" ) // MakeGetOrderHandler creates a new API handler to return zettel references @@ -29,13 +29,13 @@ http.NotFound(w, r) return } ctx := r.Context() q := r.URL.Query() - start, metas, err := zettelOrder.Run(ctx, zid, q.Get(api.KeySyntax)) + start, metas, err := zettelOrder.Run(ctx, zid, q.Get(meta.KeySyntax)) if err != nil { adapter.ReportUsecaseError(w, err) return } writeMetaList(w, start, metas) } } Index: web/adapter/api/get_parsed_zettel.go ================================================================== --- web/adapter/api/get_parsed_zettel.go +++ web/adapter/api/get_parsed_zettel.go @@ -13,14 +13,15 @@ import ( "fmt" "net/http" - "zettelstore.de/c/api" + zsapi "zettelstore.de/z/api" "zettelstore.de/z/ast" "zettelstore.de/z/config" "zettelstore.de/z/domain/id" + "zettelstore.de/z/domain/meta" "zettelstore.de/z/encoder" "zettelstore.de/z/parser" "zettelstore.de/z/usecase" "zettelstore.de/z/web/adapter" ) @@ -35,11 +36,11 @@ } q := r.URL.Query() enc, encStr := adapter.GetEncoding(r, q, encoder.GetDefaultEncoding()) part := getPart(q, partContent) - zn, err := parseZettel.Run(r.Context(), zid, q.Get(api.KeySyntax)) + zn, err := parseZettel.Run(r.Context(), zid, q.Get(meta.KeySyntax)) if err != nil { adapter.ReportUsecaseError(w, err) return } a.writeEncodedZettelPart(w, zn, parser.ParseMetadata, enc, encStr, part) @@ -47,25 +48,25 @@ } func (a *API) writeEncodedZettelPart( w http.ResponseWriter, zn *ast.ZettelNode, evalMeta encoder.EvalMetaFunc, - enc api.EncodingEnum, encStr string, part partType, + enc zsapi.EncodingEnum, encStr string, part partType, ) { env := encoder.Environment{ Lang: config.GetLang(zn.InhMeta, a.rtConfig), Xhtml: false, MarkerExternal: "", NewWindow: false, - IgnoreMeta: map[string]bool{api.KeyLang: true}, + IgnoreMeta: map[string]bool{meta.KeyLang: true}, } encdr := encoder.Create(enc, &env) if encdr == nil { adapter.BadRequest(w, fmt.Sprintf("Zettel %q not available in encoding %q", zn.Meta.Zid.String(), encStr)) return } - adapter.PrepareHeader(w, encoding2ContentType(enc)) + w.Header().Set(zsapi.HeaderContentType, encoding2ContentType(enc)) var err error switch part { case partZettel: _, err = encdr.WriteZettel(w, zn, evalMeta) case partMeta: Index: web/adapter/api/get_role_list.go ================================================================== --- web/adapter/api/get_role_list.go +++ web/adapter/api/get_role_list.go @@ -12,11 +12,11 @@ package api import ( "net/http" - "zettelstore.de/c/api" + zsapi "zettelstore.de/z/api" "zettelstore.de/z/usecase" "zettelstore.de/z/web/adapter" ) // MakeListRoleHandler creates a new HTTP handler for the use case "list some zettel". @@ -26,9 +26,9 @@ if err != nil { adapter.ReportUsecaseError(w, err) return } - adapter.PrepareHeader(w, ctJSON) - encodeJSONData(w, api.RoleListJSON{Roles: roleList}) + w.Header().Set(zsapi.HeaderContentType, ctJSON) + encodeJSONData(w, zsapi.RoleListJSON{Roles: roleList}) } } Index: web/adapter/api/get_tags_list.go ================================================================== --- web/adapter/api/get_tags_list.go +++ web/adapter/api/get_tags_list.go @@ -13,11 +13,11 @@ import ( "net/http" "strconv" - "zettelstore.de/c/api" + zsapi "zettelstore.de/z/api" "zettelstore.de/z/usecase" "zettelstore.de/z/web/adapter" ) // MakeListTagsHandler creates a new HTTP handler for the use case "list some zettel". @@ -28,17 +28,17 @@ if err != nil { adapter.ReportUsecaseError(w, err) return } - tagMap := make(map[string][]api.ZettelID, len(tagData)) + w.Header().Set(zsapi.HeaderContentType, ctJSON) + tagMap := make(map[string][]string, len(tagData)) for tag, metaList := range tagData { - zidList := make([]api.ZettelID, 0, len(metaList)) + zidList := make([]string, 0, len(metaList)) for _, m := range metaList { - zidList = append(zidList, api.ZettelID(m.Zid.String())) + zidList = append(zidList, m.Zid.String()) } tagMap[tag] = zidList } - adapter.PrepareHeader(w, ctJSON) - encodeJSONData(w, api.TagListJSON{Tags: tagMap}) + encodeJSONData(w, zsapi.TagListJSON{Tags: tagMap}) } } Index: web/adapter/api/get_zettel.go ================================================================== --- web/adapter/api/get_zettel.go +++ web/adapter/api/get_zettel.go @@ -13,11 +13,11 @@ import ( "context" "net/http" - "zettelstore.de/c/api" + zsapi "zettelstore.de/z/api" "zettelstore.de/z/box" "zettelstore.de/z/config" "zettelstore.de/z/domain" "zettelstore.de/z/domain/id" "zettelstore.de/z/usecase" @@ -30,14 +30,14 @@ z, err := getZettelFromPath(r.Context(), w, r, getZettel) if err != nil { return } - adapter.PrepareHeader(w, ctJSON) + w.Header().Set(zsapi.HeaderContentType, ctJSON) content, encoding := z.Content.Encode() - err = encodeJSONData(w, api.ZettelJSON{ - ID: api.ZettelID(z.Meta.Zid.String()), + err = encodeJSONData(w, zsapi.ZettelJSON{ + ID: z.Meta.Zid.String(), Meta: z.Meta.Map(), Encoding: encoding, Content: content, }) if err != nil { @@ -62,15 +62,15 @@ } if err == nil { _, err = z.Content.Write(w) } case partMeta: - adapter.PrepareHeader(w, ctPlainText) + w.Header().Set(zsapi.HeaderContentType, ctPlainText) _, err = z.Meta.Write(w, false) case partContent: if ct, ok := syntax2contentType(config.GetSyntax(z.Meta, a.rtConfig)); ok { - adapter.PrepareHeader(w, ct) + w.Header().Set(zsapi.HeaderContentType, ct) } _, err = z.Content.Write(w) } if err != nil { adapter.InternalServerError(w, "Write plain zettel", err) @@ -90,30 +90,5 @@ adapter.ReportUsecaseError(w, err) return domain.Zettel{}, err } return z, nil } - -// MakeGetMetaHandler creates a new HTTP handler to return metadata of a zettel. -func MakeGetMetaHandler(getMeta usecase.GetMeta) http.HandlerFunc { - return func(w http.ResponseWriter, r *http.Request) { - zid, err := id.Parse(r.URL.Path[1:]) - if err != nil { - http.NotFound(w, r) - return - } - - m, err := getMeta.Run(r.Context(), zid) - if err != nil { - adapter.ReportUsecaseError(w, err) - return - } - - adapter.PrepareHeader(w, ctJSON) - err = encodeJSONData(w, api.MetaJSON{ - Meta: m.Map(), - }) - if err != nil { - adapter.InternalServerError(w, "Write Meta JSON", err) - } - } -} Index: web/adapter/api/get_zettel_context.go ================================================================== --- web/adapter/api/get_zettel_context.go +++ web/adapter/api/get_zettel_context.go @@ -12,11 +12,11 @@ package api import ( "net/http" - "zettelstore.de/c/api" + zsapi "zettelstore.de/z/api" "zettelstore.de/z/domain/id" "zettelstore.de/z/usecase" "zettelstore.de/z/web/adapter" ) @@ -27,16 +27,16 @@ if err != nil { http.NotFound(w, r) return } q := r.URL.Query() - dir := adapter.GetZCDirection(q.Get(api.QueryKeyDir)) - depth, ok := adapter.GetInteger(q, api.QueryKeyDepth) + dir := adapter.GetZCDirection(q.Get(zsapi.QueryKeyDir)) + depth, ok := adapter.GetInteger(q, zsapi.QueryKeyDepth) if !ok || depth < 0 { depth = 5 } - limit, ok := adapter.GetInteger(q, api.QueryKeyLimit) + limit, ok := adapter.GetInteger(q, zsapi.QueryKeyLimit) if !ok || limit < 0 { limit = 200 } ctx := r.Context() metaList, err := getContext.Run(ctx, zid, dir, depth, limit) Index: web/adapter/api/get_zettel_list.go ================================================================== --- web/adapter/api/get_zettel_list.go +++ web/adapter/api/get_zettel_list.go @@ -10,15 +10,14 @@ // Package api provides api handlers for web requests. package api import ( - "bytes" "fmt" "net/http" - "zettelstore.de/c/api" + zsapi "zettelstore.de/z/api" "zettelstore.de/z/config" "zettelstore.de/z/usecase" "zettelstore.de/z/web/adapter" ) @@ -32,27 +31,21 @@ if err != nil { adapter.ReportUsecaseError(w, err) return } - var queryText bytes.Buffer - if s != nil { - s.Print(&queryText) - } - - result := make([]api.ZidMetaJSON, 0, len(metaList)) + result := make([]zsapi.ZidMetaJSON, 0, len(metaList)) for _, m := range metaList { - result = append(result, api.ZidMetaJSON{ - ID: api.ZettelID(m.Zid.String()), + result = append(result, zsapi.ZidMetaJSON{ + ID: m.Zid.String(), Meta: m.Map(), }) } - adapter.PrepareHeader(w, ctJSON) - err = encodeJSONData(w, api.ZettelListJSON{ - Query: queryText.String(), - List: result, + w.Header().Set(zsapi.HeaderContentType, ctJSON) + err = encodeJSONData(w, zsapi.ZettelListJSON{ + List: result, }) if err != nil { adapter.InternalServerError(w, "Write Zettel list JSON", err) } } @@ -68,11 +61,11 @@ if err != nil { adapter.ReportUsecaseError(w, err) return } - adapter.PrepareHeader(w, ctPlainText) + w.Header().Set(zsapi.HeaderContentType, ctPlainText) for _, m := range metaList { _, err = fmt.Fprintln(w, m.Zid.String(), config.GetTitle(m, a.rtConfig)) if err != nil { break } Index: web/adapter/api/json.go ================================================================== --- web/adapter/api/json.go +++ web/adapter/api/json.go @@ -14,49 +14,48 @@ import ( "encoding/json" "io" "net/http" - "zettelstore.de/c/api" + zsapi "zettelstore.de/z/api" "zettelstore.de/z/domain" "zettelstore.de/z/domain/id" "zettelstore.de/z/domain/meta" - "zettelstore.de/z/web/adapter" ) func encodeJSONData(w io.Writer, data interface{}) error { enc := json.NewEncoder(w) enc.SetEscapeHTML(false) return enc.Encode(data) } func writeMetaList(w http.ResponseWriter, m *meta.Meta, metaList []*meta.Meta) error { - outList := make([]api.ZidMetaJSON, len(metaList)) + outList := make([]zsapi.ZidMetaJSON, len(metaList)) for i, m := range metaList { - outList[i].ID = api.ZettelID(m.Zid.String()) + outList[i].ID = m.Zid.String() outList[i].Meta = m.Map() } - adapter.PrepareHeader(w, ctJSON) - return encodeJSONData(w, api.ZidMetaRelatedList{ - ID: api.ZettelID(m.Zid.String()), + w.Header().Set(zsapi.HeaderContentType, ctJSON) + return encodeJSONData(w, zsapi.ZidMetaRelatedList{ + ID: m.Zid.String(), Meta: m.Map(), List: outList, }) } func buildZettelFromJSONData(r *http.Request, zid id.Zid) (domain.Zettel, error) { var zettel domain.Zettel dec := json.NewDecoder(r.Body) - var zettelData api.ZettelDataJSON + var zettelData zsapi.ZettelDataJSON if err := dec.Decode(&zettelData); err != nil { return zettel, err } m := meta.New(zid) for k, v := range zettelData.Meta { - m.Set(meta.RemoveNonGraphic(k), meta.RemoveNonGraphic(v)) + m.Set(k, v) } zettel.Meta = m if err := zettel.Content.SetDecoded(zettelData.Content, zettelData.Encoding); err != nil { return zettel, err } return zettel, nil } Index: web/adapter/api/login.go ================================================================== --- web/adapter/api/login.go +++ web/adapter/api/login.go @@ -14,21 +14,21 @@ import ( "encoding/json" "net/http" "time" - "zettelstore.de/c/api" + zsapi "zettelstore.de/z/api" "zettelstore.de/z/auth" "zettelstore.de/z/usecase" "zettelstore.de/z/web/adapter" ) // MakePostLoginHandler creates a new HTTP handler to authenticate the given user via API. func (a *API) MakePostLoginHandler(ucAuth usecase.Authenticate) http.HandlerFunc { return func(w http.ResponseWriter, r *http.Request) { if !a.withAuth() { - adapter.PrepareHeader(w, ctJSON) + w.Header().Set(zsapi.HeaderContentType, ctJSON) writeJSONToken(w, "freeaccess", 24*366*10*time.Hour) return } var token []byte if ident, cred := retrieveIdentCred(r); ident != "" { @@ -43,11 +43,11 @@ w.Header().Set("WWW-Authenticate", `Bearer realm="Default"`) http.Error(w, "Authentication failed", http.StatusUnauthorized) return } - adapter.PrepareHeader(w, ctJSON) + w.Header().Set(zsapi.HeaderContentType, ctJSON) writeJSONToken(w, string(token), a.tokenLifetime) } } func retrieveIdentCred(r *http.Request) (string, string) { @@ -60,11 +60,11 @@ return "", "" } func writeJSONToken(w http.ResponseWriter, token string, lifetime time.Duration) { je := json.NewEncoder(w) - je.Encode(api.AuthJSON{ + je.Encode(zsapi.AuthJSON{ Token: token, Type: "Bearer", Expires: int(lifetime / time.Second), }) } @@ -80,11 +80,11 @@ } totalLifetime := authData.Expires.Sub(authData.Issued) currentLifetime := authData.Now.Sub(authData.Issued) // If we are in the first quarter of the tokens lifetime, return the token if currentLifetime*4 < totalLifetime { - adapter.PrepareHeader(w, ctJSON) + w.Header().Set(zsapi.HeaderContentType, ctJSON) writeJSONToken(w, string(authData.Token), totalLifetime-currentLifetime) return } // Token is a little bit aged. Create a new one @@ -91,9 +91,9 @@ token, err := a.getToken(authData.User) if err != nil { adapter.ReportUsecaseError(w, err) return } - adapter.PrepareHeader(w, ctJSON) + w.Header().Set(zsapi.HeaderContentType, ctJSON) writeJSONToken(w, string(token), a.tokenLifetime) } } Index: web/adapter/api/rename_zettel.go ================================================================== --- web/adapter/api/rename_zettel.go +++ web/adapter/api/rename_zettel.go @@ -13,11 +13,11 @@ import ( "net/http" "net/url" - "zettelstore.de/c/api" + "zettelstore.de/z/api" "zettelstore.de/z/domain/id" "zettelstore.de/z/usecase" "zettelstore.de/z/web/adapter" ) @@ -32,11 +32,11 @@ newZid, found := getDestinationZid(r) if !found { http.Error(w, http.StatusText(http.StatusBadRequest), http.StatusBadRequest) return } - if err = renameZettel.Run(r.Context(), zid, newZid); err != nil { + if err := renameZettel.Run(r.Context(), zid, newZid); err != nil { adapter.ReportUsecaseError(w, err) return } w.WriteHeader(http.StatusNoContent) } @@ -43,27 +43,29 @@ } func getDestinationZid(r *http.Request) (id.Zid, bool) { if values, ok := r.Header[api.HeaderDestination]; ok { for _, value := range values { - if zid, ok2 := getZidFromURL(value); ok2 { + if zid, ok := getZidFromURL(value); ok { return zid, true } } } return id.Invalid, false } + +var zidLength = len(id.VersionZid.Bytes()) func getZidFromURL(val string) (id.Zid, bool) { u, err := url.Parse(val) if err != nil { return id.Invalid, false } - if len(u.Path) < len(api.ZidVersion) { + if len(u.Path) < zidLength { return id.Invalid, false } - zid, err := id.Parse(u.Path[len(u.Path)-len(api.ZidVersion):]) + zid, err := id.Parse(u.Path[len(u.Path)-zidLength:]) if err != nil { return id.Invalid, false } return zid, true } Index: web/adapter/api/request.go ================================================================== --- web/adapter/api/request.go +++ web/adapter/api/request.go @@ -14,11 +14,11 @@ import ( "io" "net/http" "net/url" - "zettelstore.de/c/api" + "zettelstore.de/z/api" "zettelstore.de/z/domain" "zettelstore.de/z/domain/id" "zettelstore.de/z/domain/meta" "zettelstore.de/z/input" ) @@ -67,13 +67,13 @@ func buildZettelFromPlainData(r *http.Request, zid id.Zid) (domain.Zettel, error) { b, err := io.ReadAll(r.Body) if err != nil { return domain.Zettel{}, err } - inp := input.NewInput(b) + inp := input.NewInput(string(b)) m := meta.NewFromInput(zid, inp) return domain.Zettel{ Meta: m, Content: domain.NewContent(inp.Src[inp.Pos:]), }, nil } Index: web/adapter/api/update_zettel.go ================================================================== --- web/adapter/api/update_zettel.go +++ web/adapter/api/update_zettel.go @@ -30,11 +30,11 @@ zettel, err := buildZettelFromPlainData(r, zid) if err != nil { adapter.ReportUsecaseError(w, adapter.NewErrBadRequest(err.Error())) return } - if err = updateZettel.Run(r.Context(), zettel, true); err != nil { + if err := updateZettel.Run(r.Context(), zettel, true); err != nil { adapter.ReportUsecaseError(w, err) return } w.WriteHeader(http.StatusNoContent) } @@ -51,12 +51,12 @@ zettel, err := buildZettelFromJSONData(r, zid) if err != nil { adapter.ReportUsecaseError(w, adapter.NewErrBadRequest(err.Error())) return } - if err = updateZettel.Run(r.Context(), zettel, true); err != nil { + if err := updateZettel.Run(r.Context(), zettel, true); err != nil { adapter.ReportUsecaseError(w, err) return } w.WriteHeader(http.StatusNoContent) } } Index: web/adapter/request.go ================================================================== --- web/adapter/request.go +++ web/adapter/request.go @@ -16,11 +16,11 @@ "net/http" "net/url" "strconv" "strings" - "zettelstore.de/c/api" + "zettelstore.de/z/api" "zettelstore.de/z/domain/meta" "zettelstore.de/z/search" "zettelstore.de/z/usecase" ) @@ -67,11 +67,11 @@ } func getOneEncoding(r *http.Request, key string) (string, bool) { if values, ok := r.Header[key]; ok { for _, value := range values { - if enc, ok2 := contentType2encoding(value); ok2 { + if enc, ok := contentType2encoding(value); ok { return enc, true } } } return "", false Index: web/adapter/response.go ================================================================== --- web/adapter/response.go +++ web/adapter/response.go @@ -1,7 +1,7 @@ //----------------------------------------------------------------------------- -// Copyright (c) 2020-2021 Detlef Stern +// Copyright (c) 2020 Detlef Stern // // This file is part of zettelstore. // // Zettelstore is licensed under the latest version of the EUPL (European Union // Public License). Please see file LICENSE.txt for your rights and obligations @@ -15,34 +15,25 @@ "errors" "fmt" "log" "net/http" - "zettelstore.de/c/api" + "zettelstore.de/z/api" "zettelstore.de/z/ast" "zettelstore.de/z/box" "zettelstore.de/z/domain/id" + "zettelstore.de/z/domain/meta" "zettelstore.de/z/usecase" "zettelstore.de/z/web/server" ) -// PrepareHeader sets the HTTP header to defined values. -func PrepareHeader(w http.ResponseWriter, contentType string) http.Header { - h := w.Header() - if contentType != "" { - h.Set(api.HeaderContentType, contentType) - } - return h -} - // ReportUsecaseError returns an appropriate HTTP status code for errors in use cases. func ReportUsecaseError(w http.ResponseWriter, err error) { code, text := CodeMessageFromError(err) if code == http.StatusInternalServerError { log.Printf("%v: %v", text, err) } - // TODO: must call PrepareHeader somehow http.Error(w, text, code) } // ErrBadRequest is returned if the caller made an invalid HTTP request. type ErrBadRequest struct { @@ -80,11 +71,11 @@ return http.StatusInternalServerError, err.Error() } // CreateTagReference builds a reference to list all tags. func CreateTagReference(b server.Builder, key byte, enc, s string) *ast.Reference { - u := b.NewURLBuilder(key).AppendQuery(api.QueryKeyEncoding, enc).AppendQuery(api.KeyTags, s) + u := b.NewURLBuilder(key).AppendQuery(api.QueryKeyEncoding, enc).AppendQuery(meta.KeyTags, s) ref := ast.ParseReference(u.String()) ref.State = ast.RefStateHosted return ref } @@ -96,11 +87,11 @@ return ref } // CreateFoundReference builds a reference for a found zettel. func CreateFoundReference(b server.Builder, key byte, part, enc string, zid id.Zid, fragment string) *ast.Reference { - ub := b.NewURLBuilder(key).SetZid(api.ZettelID(zid.String())) + ub := b.NewURLBuilder(key).SetZid(zid) if part != "" { ub.AppendQuery(api.QueryKeyPart, part) } if enc != "" { ub.AppendQuery(api.QueryKeyEncoding, enc) Index: web/adapter/webui/create_zettel.go ================================================================== --- web/adapter/webui/create_zettel.go +++ web/adapter/webui/create_zettel.go @@ -14,16 +14,18 @@ import ( "context" "fmt" "net/http" - "zettelstore.de/c/api" + "zettelstore.de/z/api" "zettelstore.de/z/box" "zettelstore.de/z/config" "zettelstore.de/z/domain" "zettelstore.de/z/domain/id" + "zettelstore.de/z/domain/meta" "zettelstore.de/z/encoder" + "zettelstore.de/z/input" "zettelstore.de/z/parser" "zettelstore.de/z/usecase" "zettelstore.de/z/web/adapter" ) @@ -64,11 +66,11 @@ if err != nil { wui.reportError(ctx, w, err) return } m := origZettel.Meta - title := parser.ParseMetadata(config.GetTitle(m, wui.rtConfig)) + title := parser.ParseInlines(input.NewInput(config.GetTitle(m, wui.rtConfig)), meta.ValueSyntaxZmk) textTitle, err := encodeInlines(title, api.EncoderText, nil) if err != nil { wui.reportError(ctx, w, err) return } @@ -114,12 +116,12 @@ m := zettel.Meta var base baseData wui.makeBaseData(ctx, config.GetLang(m, wui.rtConfig), title, user, &base) wui.renderTemplate(ctx, w, id.FormTemplateZid, &base, formZettelData{ Heading: heading, - MetaTitle: m.GetDefault(api.KeyTitle, ""), - MetaTags: m.GetDefault(api.KeyTags, ""), + MetaTitle: m.GetDefault(meta.KeyTitle, ""), + MetaTags: m.GetDefault(meta.KeyTags, ""), MetaRole: config.GetRole(m, wui.rtConfig), MetaSyntax: config.GetSyntax(m, wui.rtConfig), MetaPairsRest: m.PairsRest(false), IsTextContent: !zettel.Content.IsBinary(), Content: zettel.Content.AsString(), @@ -144,8 +146,8 @@ newZid, err := createZettel.Run(ctx, zettel) if err != nil { wui.reportError(ctx, w, err) return } - redirectFound(w, r, wui.NewURLBuilder('h').SetZid(api.ZettelID(newZid.String()))) + redirectFound(w, r, wui.NewURLBuilder('h').SetZid(newZid)) } } Index: web/adapter/webui/delete_zettel.go ================================================================== --- web/adapter/webui/delete_zettel.go +++ web/adapter/webui/delete_zettel.go @@ -12,13 +12,12 @@ package webui import ( "fmt" "net/http" - "sort" - "zettelstore.de/c/api" + "zettelstore.de/z/api" "zettelstore.de/z/box" "zettelstore.de/z/config" "zettelstore.de/z/domain/id" "zettelstore.de/z/domain/meta" "zettelstore.de/z/usecase" @@ -25,15 +24,11 @@ "zettelstore.de/z/web/adapter" ) // MakeGetDeleteZettelHandler creates a new HTTP handler to display the // HTML delete view of a zettel. -func (wui *WebUI) MakeGetDeleteZettelHandler( - getMeta usecase.GetMeta, - getAllMeta usecase.GetAllMeta, - evaluate *usecase.Evaluate, -) http.HandlerFunc { +func (wui *WebUI) MakeGetDeleteZettelHandler(getZettel usecase.GetZettel) http.HandlerFunc { return func(w http.ResponseWriter, r *http.Request) { ctx := r.Context() if enc, encText := adapter.GetEncoding(r, r.URL.Query(), api.EncoderHTML); enc != api.EncoderHTML { wui.reportError(ctx, w, adapter.NewErrBadRequest( fmt.Sprintf("Delete zettel not possible in encoding %q", encText))) @@ -44,43 +39,26 @@ if err != nil { wui.reportError(ctx, w, box.ErrNotFound) return } - ms, err := getAllMeta.Run(ctx, zid) + zettel, err := getZettel.Run(ctx, zid) if err != nil { wui.reportError(ctx, w, err) return } - m := ms[0] - - var shadowedBox string - var incomingLinks []simpleLink - if len(ms) > 1 { - shadowedBox = ms[1].GetDefault(api.KeyBoxNumber, "???") - } else { - getTextTitle := wui.makeGetTextTitle(ctx, getMeta, evaluate) - incomingLinks = wui.encodeIncoming(m, getTextTitle) - } user := wui.getUser(ctx) + m := zettel.Meta var base baseData wui.makeBaseData(ctx, config.GetLang(m, wui.rtConfig), "Delete Zettel "+m.Zid.String(), user, &base) wui.renderTemplate(ctx, w, id.DeleteTemplateZid, &base, struct { - Zid string - MetaPairs []meta.Pair - HasShadows bool - ShadowedBox string - HasIncoming bool - Incoming []simpleLink + Zid string + MetaPairs []meta.Pair }{ - Zid: zid.String(), - MetaPairs: m.Pairs(true), - HasShadows: shadowedBox != "", - ShadowedBox: shadowedBox, - HasIncoming: len(incomingLinks) > 0, - Incoming: incomingLinks, + Zid: zid.String(), + MetaPairs: m.Pairs(true), }) } } // MakePostDeleteZettelHandler creates a new HTTP handler to delete a zettel. @@ -91,46 +69,12 @@ if err != nil { wui.reportError(ctx, w, box.ErrNotFound) return } - if err = deleteZettel.Run(r.Context(), zid); err != nil { + if err := deleteZettel.Run(r.Context(), zid); err != nil { wui.reportError(ctx, w, err) return } redirectFound(w, r, wui.NewURLBuilder('/')) } } - -func (wui *WebUI) encodeIncoming(m *meta.Meta, getTextTitle getTextTitleFunc) []simpleLink { - zidMap := make(map[string]bool) - addListValues(zidMap, m, api.KeyBackward) - for _, kd := range meta.GetSortedKeyDescriptions() { - inverseKey := kd.Inverse - if inverseKey == "" { - continue - } - ikd := meta.GetDescription(inverseKey) - switch ikd.Type { - case meta.TypeID: - if val, ok := m.Get(inverseKey); ok { - zidMap[val] = true - } - case meta.TypeIDSet: - addListValues(zidMap, m, inverseKey) - } - } - values := make([]string, 0, len(zidMap)) - for val := range zidMap { - values = append(values, val) - } - sort.Strings(values) - return wui.encodeZidLinks(values, getTextTitle) -} - -func addListValues(zidMap map[string]bool, m *meta.Meta, key string) { - if values, ok := m.GetList(key); ok { - for _, val := range values { - zidMap[val] = true - } - } -} Index: web/adapter/webui/edit_zettel.go ================================================================== --- web/adapter/webui/edit_zettel.go +++ web/adapter/webui/edit_zettel.go @@ -13,14 +13,15 @@ import ( "fmt" "net/http" - "zettelstore.de/c/api" + "zettelstore.de/z/api" "zettelstore.de/z/box" "zettelstore.de/z/config" "zettelstore.de/z/domain/id" + "zettelstore.de/z/domain/meta" "zettelstore.de/z/usecase" "zettelstore.de/z/web/adapter" ) // MakeEditGetZettelHandler creates a new HTTP handler to display the @@ -50,14 +51,14 @@ m := zettel.Meta var base baseData wui.makeBaseData(ctx, config.GetLang(m, wui.rtConfig), "Edit Zettel", user, &base) wui.renderTemplate(ctx, w, id.FormTemplateZid, &base, formZettelData{ Heading: base.Title, - MetaTitle: m.GetDefault(api.KeyTitle, ""), - MetaRole: m.GetDefault(api.KeyRole, ""), - MetaTags: m.GetDefault(api.KeyTags, ""), - MetaSyntax: m.GetDefault(api.KeySyntax, ""), + MetaTitle: m.GetDefault(meta.KeyTitle, ""), + MetaRole: m.GetDefault(meta.KeyRole, ""), + MetaTags: m.GetDefault(meta.KeyTags, ""), + MetaSyntax: m.GetDefault(meta.KeySyntax, ""), MetaPairsRest: m.PairsRest(false), IsTextContent: !zettel.Content.IsBinary(), Content: zettel.Content.AsString(), }) } @@ -78,12 +79,12 @@ if err != nil { wui.reportError(ctx, w, adapter.NewErrBadRequest("Unable to read zettel form")) return } - if err = updateZettel.Run(r.Context(), zettel, hasContent); err != nil { + if err := updateZettel.Run(r.Context(), zettel, hasContent); err != nil { wui.reportError(ctx, w, err) return } - redirectFound(w, r, wui.NewURLBuilder('h').SetZid(api.ZettelID(zid.String()))) + redirectFound(w, r, wui.NewURLBuilder('h').SetZid(zid)) } } Index: web/adapter/webui/forms.go ================================================================== --- web/adapter/webui/forms.go +++ web/adapter/webui/forms.go @@ -10,15 +10,13 @@ // Package webui provides web-UI handlers for web requests. package webui import ( - "bytes" "net/http" "strings" - "zettelstore.de/c/api" "zettelstore.de/z/domain" "zettelstore.de/z/domain/id" "zettelstore.de/z/domain/meta" "zettelstore.de/z/input" ) @@ -32,55 +30,46 @@ MetaPairsRest []meta.Pair IsTextContent bool Content string } -var ( - bsCRLF = []byte{'\r', '\n'} - bsLF = []byte{'\n'} -) - func parseZettelForm(r *http.Request, zid id.Zid) (domain.Zettel, bool, error) { err := r.ParseForm() if err != nil { return domain.Zettel{}, false, err } var m *meta.Meta if postMeta, ok := trimmedFormValue(r, "meta"); ok { - m = meta.NewFromInput(zid, input.NewInput([]byte(postMeta))) - m.Sanitize() + m = meta.NewFromInput(zid, input.NewInput(postMeta)) } else { m = meta.New(zid) } if postTitle, ok := trimmedFormValue(r, "title"); ok { - m.Set(api.KeyTitle, meta.RemoveNonGraphic(postTitle)) + m.Set(meta.KeyTitle, postTitle) } if postTags, ok := trimmedFormValue(r, "tags"); ok { - if tags := strings.Fields(meta.RemoveNonGraphic(postTags)); len(tags) > 0 { - m.SetList(api.KeyTags, tags) + if tags := strings.Fields(postTags); len(tags) > 0 { + m.SetList(meta.KeyTags, tags) } } if postRole, ok := trimmedFormValue(r, "role"); ok { - m.Set(api.KeyRole, meta.RemoveNonGraphic(postRole)) + m.Set(meta.KeyRole, postRole) } if postSyntax, ok := trimmedFormValue(r, "syntax"); ok { - m.Set(api.KeySyntax, meta.RemoveNonGraphic(postSyntax)) + m.Set(meta.KeySyntax, postSyntax) } if values, ok := r.PostForm["content"]; ok && len(values) > 0 { return domain.Zettel{ Meta: m, Content: domain.NewContent( - bytes.ReplaceAll( - bytes.TrimSpace([]byte(values[0])), - bsCRLF, - bsLF)), + strings.ReplaceAll(strings.TrimSpace(values[0]), "\r\n", "\n")), }, true, nil } return domain.Zettel{ Meta: m, - Content: domain.NewContent(nil), + Content: domain.NewContent(""), }, false, nil } func trimmedFormValue(r *http.Request, key string) (string, bool) { if values, ok := r.PostForm[key]; ok && len(values) > 0 { Index: web/adapter/webui/get_info.go ================================================================== --- web/adapter/webui/get_info.go +++ web/adapter/webui/get_info.go @@ -10,23 +10,23 @@ // Package webui provides web-UI handlers for web requests. package webui import ( - "bytes" "context" "fmt" "net/http" "sort" + "strings" - "zettelstore.de/c/api" + "zettelstore.de/z/api" "zettelstore.de/z/ast" "zettelstore.de/z/box" "zettelstore.de/z/collect" "zettelstore.de/z/config" - "zettelstore.de/z/domain" "zettelstore.de/z/domain/id" + "zettelstore.de/z/domain/meta" "zettelstore.de/z/encoder" "zettelstore.de/z/evaluator" "zettelstore.de/z/usecase" "zettelstore.de/z/web/adapter" ) @@ -61,48 +61,40 @@ if err != nil { wui.reportError(ctx, w, box.ErrNotFound) return } - zn, err := parseZettel.Run(ctx, zid, q.Get(api.KeySyntax)) + zn, err := parseZettel.Run(ctx, zid, q.Get(meta.KeySyntax)) if err != nil { wui.reportError(ctx, w, err) return } summary := collect.References(zn) locLinks, extLinks := splitLocExtLinks(append(summary.Links, summary.Embeds...)) envEval := evaluator.Environment{ + EmbedImage: true, GetTagRef: func(s string) *ast.Reference { return adapter.CreateTagReference(wui, 'h', api.EncodingHTML, s) }, GetHostedRef: func(s string) *ast.Reference { return adapter.CreateHostedReference(wui, s) }, GetFoundRef: func(zid id.Zid, fragment string) *ast.Reference { return adapter.CreateFoundReference(wui, 'h', "", "", zid, fragment) }, - GetImageMaterial: func(zettel domain.Zettel, _ string) ast.MaterialNode { - return wui.createImageMaterial(zettel.Meta.Zid) - }, } lang := config.GetLang(zn.InhMeta, wui.rtConfig) envHTML := encoder.Environment{Lang: lang} pairs := zn.Meta.Pairs(true) metaData := make([]metaDataInfo, len(pairs)) getTextTitle := wui.makeGetTextTitle(ctx, getMeta, evaluate) for i, p := range pairs { - var buf bytes.Buffer - wui.writeHTMLMetaValue( - &buf, p.Key, p.Value, - getTextTitle, - func(val string) *ast.InlineListNode { - return evaluate.RunMetadata(ctx, val, &envEval) - }, - &envHTML) - metaData[i] = metaDataInfo{p.Key, buf.String()} + var html strings.Builder + wui.writeHTMLMetaValue(ctx, &html, p.Key, p.Value, getTextTitle, evaluate, &envEval, &envHTML) + metaData[i] = metaDataInfo{p.Key, html.String()} } shadowLinks := getShadowLinks(ctx, zid, getAllMeta) endnotes, err := encodeBlocks(&ast.BlockListNode{}, api.EncoderHTML, &envHTML) if err != nil { endnotes = "" @@ -109,11 +101,10 @@ } textTitle := wui.encodeTitleAsText(ctx, zn.InhMeta, evaluate) user := wui.getUser(ctx) canCreate := wui.canCreate(ctx, user) - apiZid := api.ZettelID(zid.String()) var base baseData wui.makeBaseData(ctx, lang, textTitle, user, &base) wui.renderTemplate(ctx, w, id.InfoTemplateZid, &base, struct { Zid string WebURL string @@ -140,22 +131,22 @@ HasShadowLinks bool ShadowLinks []string Endnotes string }{ Zid: zid.String(), - WebURL: wui.NewURLBuilder('h').SetZid(apiZid).String(), - ContextURL: wui.NewURLBuilder('k').SetZid(apiZid).String(), + WebURL: wui.NewURLBuilder('h').SetZid(zid).String(), + ContextURL: wui.NewURLBuilder('k').SetZid(zid).String(), CanWrite: wui.canWrite(ctx, user, zn.Meta, zn.Content), - EditURL: wui.NewURLBuilder('e').SetZid(apiZid).String(), + EditURL: wui.NewURLBuilder('e').SetZid(zid).String(), CanFolge: canCreate, - FolgeURL: wui.NewURLBuilder('f').SetZid(apiZid).String(), + FolgeURL: wui.NewURLBuilder('f').SetZid(zid).String(), CanCopy: canCreate && !zn.Content.IsBinary(), - CopyURL: wui.NewURLBuilder('c').SetZid(apiZid).String(), + CopyURL: wui.NewURLBuilder('c').SetZid(zid).String(), CanRename: wui.canRename(ctx, user, zn.Meta), - RenameURL: wui.NewURLBuilder('b').SetZid(apiZid).String(), + RenameURL: wui.NewURLBuilder('b').SetZid(zid).String(), CanDelete: wui.canDelete(ctx, user, zn.Meta), - DeleteURL: wui.NewURLBuilder('d').SetZid(apiZid).String(), + DeleteURL: wui.NewURLBuilder('d').SetZid(zid).String(), MetaData: metaData, HasLinks: len(extLinks)+len(locLinks) > 0, HasLocLinks: len(locLinks) > 0, LocLinks: locLinks, HasExtLinks: len(extLinks) > 0, @@ -201,13 +192,13 @@ for _, f := range encodings { encTexts = append(encTexts, f.String()) } sort.Strings(encTexts) defEncoding := encoder.GetDefaultEncoding().String() - parts := getParts() + parts := []string{api.PartZettel, api.PartMeta, api.PartContent} matrix := make([]matrixLine, 0, len(parts)) - u := wui.NewURLBuilder(key).SetZid(api.ZettelID(zid.String())) + u := wui.NewURLBuilder(key).SetZid(zid) for _, part := range parts { row := make([]simpleLink, len(encTexts)) for j, enc := range encTexts { u.AppendQuery(api.QueryKeyPart, part) if enc != defEncoding { @@ -221,38 +212,32 @@ return matrix } func (wui *WebUI) infoAPIMatrixPlain(key byte, zid id.Zid) []matrixLine { matrix := wui.infoAPIMatrix(key, zid) - apiZid := api.ZettelID(zid.String()) // Append plain and JSON format - u := wui.NewURLBuilder('z').SetZid(apiZid) - for i, part := range getParts() { + u := wui.NewURLBuilder('z').SetZid(zid) + parts := []string{api.PartZettel, api.PartMeta, api.PartContent} + for i, part := range parts { u.AppendQuery(api.QueryKeyPart, part) matrix[i].Elements = append(matrix[i].Elements, simpleLink{"plain", u.String()}) u.ClearQuery() } - u = wui.NewURLBuilder('j').SetZid(apiZid) + u = wui.NewURLBuilder('j').SetZid(zid) matrix[0].Elements = append(matrix[0].Elements, simpleLink{"json", u.String()}) - u = wui.NewURLBuilder('m').SetZid(apiZid) - matrix[1].Elements = append(matrix[1].Elements, simpleLink{"json", u.String()}) return matrix } -func getParts() []string { - return []string{api.PartZettel, api.PartMeta, api.PartContent} -} - func getShadowLinks(ctx context.Context, zid id.Zid, getAllMeta usecase.GetAllMeta) []string { ml, err := getAllMeta.Run(ctx, zid) if err != nil || len(ml) < 2 { return nil } result := make([]string, 0, len(ml)-1) for _, m := range ml[1:] { - if boxNo, ok := m.Get(api.KeyBoxNumber); ok { + if boxNo, ok := m.Get(meta.KeyBoxNumber); ok { result = append(result, boxNo) } } return result } Index: web/adapter/webui/get_zettel.go ================================================================== --- web/adapter/webui/get_zettel.go +++ web/adapter/webui/get_zettel.go @@ -13,16 +13,16 @@ import ( "bytes" "errors" "net/http" + "strings" - "zettelstore.de/c/api" + "zettelstore.de/z/api" "zettelstore.de/z/ast" "zettelstore.de/z/box" "zettelstore.de/z/config" - "zettelstore.de/z/domain" "zettelstore.de/z/domain/id" "zettelstore.de/z/domain/meta" "zettelstore.de/z/encoder" "zettelstore.de/z/evaluator" "zettelstore.de/z/usecase" @@ -39,24 +39,22 @@ return } q := r.URL.Query() env := evaluator.Environment{ + EmbedImage: true, GetTagRef: func(s string) *ast.Reference { return adapter.CreateTagReference(wui, 'h', api.EncodingHTML, s) }, GetHostedRef: func(s string) *ast.Reference { return adapter.CreateHostedReference(wui, s) }, GetFoundRef: func(zid id.Zid, fragment string) *ast.Reference { return adapter.CreateFoundReference(wui, 'h', "", "", zid, fragment) }, - GetImageMaterial: func(zettel domain.Zettel, _ string) ast.MaterialNode { - return wui.createImageMaterial(zettel.Meta.Zid) - }, } - zn, err := evaluate.Run(ctx, zid, q.Get(api.KeySyntax), &env) + zn, err := evaluate.Run(ctx, zid, q.Get(meta.KeySyntax), &env) if err != nil { wui.reportError(ctx, w, err) return } @@ -68,11 +66,11 @@ envHTML := encoder.Environment{ Lang: lang, Xhtml: false, MarkerExternal: wui.rtConfig.GetMarkerExternal(), NewWindow: true, - IgnoreMeta: map[string]bool{api.KeyTitle: true, api.KeyLang: true}, + IgnoreMeta: map[string]bool{meta.KeyTitle: true, meta.KeyLang: true}, } metaHeader, err := encodeMeta(zn.InhMeta, evalMeta, api.EncoderHTML, &envHTML) if err != nil { wui.reportError(ctx, w, err) return @@ -83,18 +81,17 @@ if err != nil { wui.reportError(ctx, w, err) return } user := wui.getUser(ctx) - roleText := zn.Meta.GetDefault(api.KeyRole, "*") + roleText := zn.Meta.GetDefault(meta.KeyRole, "*") tags := wui.buildTagInfos(zn.Meta) canCreate := wui.canCreate(ctx, user) getTextTitle := wui.makeGetTextTitle(ctx, getMeta, evaluate) - extURL, hasExtURL := zn.Meta.Get(api.KeyURL) - folgeLinks := wui.encodeZettelLinks(zn.InhMeta, api.KeyFolge, getTextTitle) - backLinks := wui.encodeZettelLinks(zn.InhMeta, api.KeyBack, getTextTitle) - apiZid := api.ZettelID(zid.String()) + extURL, hasExtURL := zn.Meta.Get(meta.KeyURL) + folgeLinks := wui.encodeZettelLinks(zn.InhMeta, meta.KeyFolge, getTextTitle) + backLinks := wui.encodeZettelLinks(zn.InhMeta, meta.KeyBack, getTextTitle) var base baseData wui.makeBaseData(ctx, lang, textTitle, user, &base) base.MetaHeader = metaHeader wui.renderTemplate(ctx, w, id.ZettelTemplateZid, &base, struct { HTMLTitle string @@ -120,22 +117,22 @@ HasBackLinks bool BackLinks []simpleLink }{ HTMLTitle: htmlTitle, CanWrite: wui.canWrite(ctx, user, zn.Meta, zn.Content), - EditURL: wui.NewURLBuilder('e').SetZid(apiZid).String(), + EditURL: wui.NewURLBuilder('e').SetZid(zid).String(), Zid: zid.String(), - InfoURL: wui.NewURLBuilder('i').SetZid(apiZid).String(), + InfoURL: wui.NewURLBuilder('i').SetZid(zid).String(), RoleText: roleText, RoleURL: wui.NewURLBuilder('h').AppendQuery("role", roleText).String(), HasTags: len(tags) > 0, Tags: tags, CanCopy: canCreate && !zn.Content.IsBinary(), - CopyURL: wui.NewURLBuilder('c').SetZid(apiZid).String(), + CopyURL: wui.NewURLBuilder('c').SetZid(zid).String(), CanFolge: canCreate, - FolgeURL: wui.NewURLBuilder('f').SetZid(apiZid).String(), - PrecursorRefs: wui.encodeIdentifierSet(zn.InhMeta, api.KeyPrecursor, getTextTitle), + FolgeURL: wui.NewURLBuilder('f').SetZid(zid).String(), + PrecursorRefs: wui.encodeIdentifierSet(zn.InhMeta, meta.KeyPrecursor, getTextTitle), ExtURL: extURL, HasExtURL: hasExtURL, ExtNewWindow: htmlAttrNewWindow(envHTML.NewWindow && hasExtURL), Content: htmlContent, HasFolgeLinks: len(folgeLinks) > 0, @@ -157,30 +154,30 @@ encdr := encoder.Create(enc, env) if encdr == nil { return "", errNoSuchEncoding } - var buf bytes.Buffer - _, err := encdr.WriteInlines(&buf, is) + var content strings.Builder + _, err := encdr.WriteInlines(&content, is) if err != nil { return "", err } - return buf.String(), nil + return content.String(), nil } func encodeBlocks(bln *ast.BlockListNode, enc api.EncodingEnum, env *encoder.Environment) (string, error) { encdr := encoder.Create(enc, env) if encdr == nil { return "", errNoSuchEncoding } - var buf bytes.Buffer - _, err := encdr.WriteBlocks(&buf, bln) + var content strings.Builder + _, err := encdr.WriteBlocks(&content, bln) if err != nil { return "", err } - return buf.String(), nil + return content.String(), nil } func encodeMeta( m *meta.Meta, evalMeta encoder.EvalMetaFunc, enc api.EncodingEnum, env *encoder.Environment, @@ -188,21 +185,21 @@ encdr := encoder.Create(enc, env) if encdr == nil { return "", errNoSuchEncoding } - var buf bytes.Buffer - _, err := encdr.WriteMeta(&buf, m, evalMeta) + var content strings.Builder + _, err := encdr.WriteMeta(&content, m, evalMeta) if err != nil { return "", err } - return buf.String(), nil + return content.String(), nil } func (wui *WebUI) buildTagInfos(m *meta.Meta) []simpleLink { var tagInfos []simpleLink - if tags, ok := m.GetList(api.KeyTags); ok { + if tags, ok := m.GetList(meta.KeyTags); ok { ub := wui.NewURLBuilder('h') tagInfos = make([]simpleLink, len(tags)) for i, tag := range tags { tagInfos[i] = simpleLink{Text: tag, URL: ub.AppendQuery("tags", tag).String()} ub.ClearQuery() @@ -223,26 +220,22 @@ func (wui *WebUI) encodeZettelLinks(m *meta.Meta, key string, getTextTitle getTextTitleFunc) []simpleLink { values, ok := m.GetList(key) if !ok || len(values) == 0 { return nil } - return wui.encodeZidLinks(values, getTextTitle) -} - -func (wui *WebUI) encodeZidLinks(values []string, getTextTitle getTextTitleFunc) []simpleLink { result := make([]simpleLink, 0, len(values)) for _, val := range values { zid, err := id.Parse(val) if err != nil { continue } if title, found := getTextTitle(zid); found > 0 { - url := wui.NewURLBuilder('h').SetZid(api.ZettelID(zid.String())).String() + url := wui.NewURLBuilder('h').SetZid(zid).String() if title == "" { result = append(result, simpleLink{Text: val, URL: url}) } else { result = append(result, simpleLink{Text: title, URL: url}) } } } return result } Index: web/adapter/webui/home.go ================================================================== --- web/adapter/webui/home.go +++ web/adapter/webui/home.go @@ -14,11 +14,10 @@ import ( "context" "errors" "net/http" - "zettelstore.de/c/api" "zettelstore.de/z/box" "zettelstore.de/z/domain/id" "zettelstore.de/z/domain/meta" ) @@ -34,25 +33,24 @@ if r.URL.Path != "/" { wui.reportError(ctx, w, box.ErrNotFound) return } homeZid := wui.rtConfig.GetHomeZettel() - apiHomeZid := api.ZettelID(homeZid.String()) if homeZid != id.DefaultHomeZid { if _, err := s.GetMeta(ctx, homeZid); err == nil { - redirectFound(w, r, wui.NewURLBuilder('h').SetZid(apiHomeZid)) + redirectFound(w, r, wui.NewURLBuilder('h').SetZid(homeZid)) return } homeZid = id.DefaultHomeZid } _, err := s.GetMeta(ctx, homeZid) if err == nil { - redirectFound(w, r, wui.NewURLBuilder('h').SetZid(apiHomeZid)) + redirectFound(w, r, wui.NewURLBuilder('h').SetZid(homeZid)) return } if errors.Is(err, &box.ErrNotAllowed{}) && wui.authz.WithAuth() && wui.getUser(ctx) == nil { redirectFound(w, r, wui.NewURLBuilder('i')) return } redirectFound(w, r, wui.NewURLBuilder('h')) } } Index: web/adapter/webui/htmlmeta.go ================================================================== --- web/adapter/webui/htmlmeta.go +++ web/adapter/webui/htmlmeta.go @@ -17,12 +17,11 @@ "fmt" "io" "net/url" "time" - "zettelstore.de/c/api" - "zettelstore.de/z/ast" + "zettelstore.de/z/api" "zettelstore.de/z/box" "zettelstore.de/z/config" "zettelstore.de/z/domain/id" "zettelstore.de/z/domain/meta" "zettelstore.de/z/encoder" @@ -31,17 +30,15 @@ "zettelstore.de/z/usecase" ) var space = []byte{' '} -type evalMetadataFunc = func(string) *ast.InlineListNode - func (wui *WebUI) writeHTMLMetaValue( - w io.Writer, - key, value string, + ctx context.Context, + w io.Writer, key, value string, getTextTitle getTextTitleFunc, - evalMetadata evalMetadataFunc, + evaluate *usecase.Evaluate, envEval *evaluator.Environment, envEnc *encoder.Environment, ) { switch kt := meta.Type(key); kt { case meta.TypeBool: wui.writeHTMLBool(w, key, value) @@ -68,11 +65,11 @@ case meta.TypeWord: wui.writeWord(w, key, value) case meta.TypeWordSet: wui.writeWordSet(w, key, meta.ListFromValue(value)) case meta.TypeZettelmarkup: - io.WriteString(w, encodeZmkMetadata(value, evalMetadata, api.EncoderHTML, envEnc)) + io.WriteString(w, encodeZmkMetadata(ctx, value, evaluate, envEval, api.EncoderHTML, envEnc)) default: strfun.HTMLEscape(w, value, false) fmt.Fprintf(w, " (Unhandled type: %v, key: %v)", kt, key) } } @@ -100,15 +97,14 @@ return } title, found := getTextTitle(zid) switch { case found > 0: - ub := wui.NewURLBuilder('h').SetZid(api.ZettelID(zid.String())) if title == "" { - fmt.Fprintf(w, "%v", ub, zid) + fmt.Fprintf(w, "%v", wui.NewURLBuilder('h').SetZid(zid), zid) } else { - fmt.Fprintf(w, "%v", ub, title, zid) + fmt.Fprintf(w, "%v", wui.NewURLBuilder('h').SetZid(zid), title, zid) } case found == 0: fmt.Fprintf(w, "%v", val) case found < 0: io.WriteString(w, val) @@ -197,39 +193,30 @@ ctx context.Context, m *meta.Meta, evaluate *usecase.Evaluate, envEval *evaluator.Environment, envHTML *encoder.Environment, ) string { plainTitle := config.GetTitle(m, wui.rtConfig) - return encodeZmkMetadata( - plainTitle, - func(val string) *ast.InlineListNode { - return evaluate.RunMetadata(ctx, plainTitle, envEval) - }, - api.EncoderHTML, envHTML) + return encodeZmkMetadata(ctx, plainTitle, evaluate, envEval, api.EncoderHTML, envHTML) } func (wui *WebUI) encodeTitleAsText( ctx context.Context, m *meta.Meta, evaluate *usecase.Evaluate, ) string { plainTitle := config.GetTitle(m, wui.rtConfig) - return encodeZmkMetadata( - plainTitle, - func(val string) *ast.InlineListNode { - return evaluate.RunMetadata(ctx, plainTitle, nil) - }, - api.EncoderText, nil) + return encodeZmkMetadata(ctx, plainTitle, evaluate, nil, api.EncoderText, nil) } func encodeZmkMetadata( - value string, evalMetadata evalMetadataFunc, + ctx context.Context, value string, + evaluate *usecase.Evaluate, envEval *evaluator.Environment, enc api.EncodingEnum, envHTML *encoder.Environment, ) string { - iln := evalMetadata(value) + iln := evaluate.RunMetadata(ctx, value, envEval) if iln.IsEmpty() { return "" } result, err := encodeInlines(iln, enc, envHTML) - if err != nil { - return err.Error() + if err == nil { + return result } - return result + return err.Error() } Index: web/adapter/webui/lists.go ================================================================== --- web/adapter/webui/lists.go +++ web/adapter/webui/lists.go @@ -10,18 +10,18 @@ // Package webui provides web-UI handlers for web requests. package webui import ( - "bytes" "context" "net/http" "net/url" "sort" "strconv" + "strings" - "zettelstore.de/c/api" + "zettelstore.de/z/api" "zettelstore.de/z/box" "zettelstore.de/z/domain/id" "zettelstore.de/z/domain/meta" "zettelstore.de/z/encoder" "zettelstore.de/z/search" @@ -111,11 +111,11 @@ iCount int Count string Size string } -const fontSizes = 6 // Must be the number of CSS classes zs-font-size-* in base.css +var fontSizes = [...]int{75, 83, 100, 117, 150, 200} func (wui *WebUI) renderTagsList(w http.ResponseWriter, r *http.Request, listTags usecase.ListTags) { ctx := r.Context() iMinCount, _ := strconv.Atoi(r.URL.Query().Get("min")) tagData, err := listTags.Run(ctx, iMinCount) @@ -142,11 +142,11 @@ for count := range countMap { countList = append(countList, count) } sort.Ints(countList) for pos, count := range countList { - countMap[count] = (pos * fontSizes) / len(countList) + countMap[count] = fontSizes[(pos*len(fontSizes))/len(countList)] } for i := 0; i < len(tagsList); i++ { count := tagsList[i].iCount tagsList[i].Count = strconv.Itoa(count) tagsList[i].Size = strconv.Itoa(countMap[count]) @@ -211,15 +211,14 @@ metaList, err := getContext.Run(ctx, zid, dir, depth, limit) if err != nil { wui.reportError(ctx, w, err) return } - apiZid := api.ZettelID(zid.String()) metaLinks := wui.buildHTMLMetaList(ctx, metaList, evaluate) depths := []string{"2", "3", "4", "5", "6", "7", "8", "9", "10"} depthLinks := make([]simpleLink, len(depths)) - depthURL := wui.NewURLBuilder('k').SetZid(apiZid) + depthURL := wui.NewURLBuilder('k').SetZid(zid) for i, depth := range depths { depthURL.ClearQuery() switch dir { case usecase.ZettelContextBackward: depthURL.AppendQuery(api.QueryKeyDir, api.DirBackward) @@ -239,11 +238,11 @@ Depths []simpleLink Start simpleLink Metas []simpleLink }{ Title: "Zettel Context", - InfoURL: wui.NewURLBuilder('i').SetZid(apiZid).String(), + InfoURL: wui.NewURLBuilder('i').SetZid(zid).String(), Depths: depthLinks, Start: metaLinks[0], Metas: metaLinks[1:], }) } @@ -286,17 +285,17 @@ func (wui *WebUI) listTitleSearch(prefix string, s *search.Search) string { if s == nil { return wui.rtConfig.GetSiteName() } - var buf bytes.Buffer - buf.WriteString(prefix) + var sb strings.Builder + sb.WriteString(prefix) if s != nil { - buf.WriteString(": ") - s.Print(&buf) + sb.WriteString(": ") + s.Print(&sb) } - return buf.String() + return sb.String() } // buildHTMLMetaList builds a zettel list based on a meta list for HTML rendering. func (wui *WebUI) buildHTMLMetaList( ctx context.Context, metaList []*meta.Meta, evaluate *usecase.Evaluate, @@ -303,18 +302,18 @@ ) []simpleLink { defaultLang := wui.rtConfig.GetDefaultLang() metas := make([]simpleLink, 0, len(metaList)) for _, m := range metaList { var lang string - if val, ok := m.Get(api.KeyLang); ok { + if val, ok := m.Get(meta.KeyLang); ok { lang = val } else { lang = defaultLang } env := encoder.Environment{Lang: lang, Interactive: true} metas = append(metas, simpleLink{ Text: wui.encodeTitleAsHTML(ctx, m, evaluate, nil, &env), - URL: wui.NewURLBuilder('h').SetZid(api.ZettelID(m.Zid.String())).String(), + URL: wui.NewURLBuilder('h').SetZid(m.Zid).String(), }) } return metas } Index: web/adapter/webui/rename_zettel.go ================================================================== --- web/adapter/webui/rename_zettel.go +++ web/adapter/webui/rename_zettel.go @@ -14,11 +14,11 @@ import ( "fmt" "net/http" "strings" - "zettelstore.de/c/api" + "zettelstore.de/z/api" "zettelstore.de/z/box" "zettelstore.de/z/config" "zettelstore.de/z/domain/id" "zettelstore.de/z/domain/meta" "zettelstore.de/z/usecase" @@ -25,11 +25,11 @@ "zettelstore.de/z/web/adapter" ) // MakeGetRenameZettelHandler creates a new HTTP handler to display the // HTML rename view of a zettel. -func (wui *WebUI) MakeGetRenameZettelHandler(getMeta usecase.GetMeta, evaluate *usecase.Evaluate) http.HandlerFunc { +func (wui *WebUI) MakeGetRenameZettelHandler(getMeta usecase.GetMeta) http.HandlerFunc { return func(w http.ResponseWriter, r *http.Request) { ctx := r.Context() zid, err := id.Parse(r.URL.Path[1:]) if err != nil { wui.reportError(ctx, w, box.ErrNotFound) @@ -46,26 +46,19 @@ wui.reportError(ctx, w, adapter.NewErrBadRequest( fmt.Sprintf("Rename zettel %q not possible in encoding %q", zid.String(), encText))) return } - getTextTitle := wui.makeGetTextTitle(ctx, getMeta, evaluate) - incomingLinks := wui.encodeIncoming(m, getTextTitle) - user := wui.getUser(ctx) var base baseData wui.makeBaseData(ctx, config.GetLang(m, wui.rtConfig), "Rename Zettel "+zid.String(), user, &base) wui.renderTemplate(ctx, w, id.RenameTemplateZid, &base, struct { - Zid string - MetaPairs []meta.Pair - HasIncoming bool - Incoming []simpleLink + Zid string + MetaPairs []meta.Pair }{ - Zid: zid.String(), - MetaPairs: m.Pairs(true), - HasIncoming: len(incomingLinks) > 0, - Incoming: incomingLinks, + Zid: zid.String(), + MetaPairs: m.Pairs(true), }) } } // MakePostRenameZettelHandler creates a new HTTP handler to rename an existing zettel. @@ -85,20 +78,18 @@ if formCurZid, err1 := id.Parse( r.PostFormValue("curzid")); err1 != nil || formCurZid != curZid { wui.reportError(ctx, w, adapter.NewErrBadRequest("Invalid value for current zettel id in form")) return } - formNewZid := strings.TrimSpace(r.PostFormValue("newzid")) - newZid, err := id.Parse(formNewZid) + newZid, err := id.Parse(strings.TrimSpace(r.PostFormValue("newzid"))) if err != nil { - wui.reportError( - ctx, w, adapter.NewErrBadRequest(fmt.Sprintf("Invalid new zettel id %q", formNewZid))) + wui.reportError(ctx, w, adapter.NewErrBadRequest(fmt.Sprintf("Invalid new zettel id %q", newZid))) return } - if err = renameZettel.Run(r.Context(), curZid, newZid); err != nil { + if err := renameZettel.Run(r.Context(), curZid, newZid); err != nil { wui.reportError(ctx, w, err) return } - redirectFound(w, r, wui.NewURLBuilder('h').SetZid(api.ZettelID(newZid.String()))) + redirectFound(w, r, wui.NewURLBuilder('h').SetZid(newZid)) } } Index: web/adapter/webui/response.go ================================================================== --- web/adapter/webui/response.go +++ web/adapter/webui/response.go @@ -12,20 +12,11 @@ package webui import ( "net/http" - "zettelstore.de/c/api" - "zettelstore.de/z/ast" - "zettelstore.de/z/domain/id" + "zettelstore.de/z/api" ) func redirectFound(w http.ResponseWriter, r *http.Request, ub *api.URLBuilder) { http.Redirect(w, r, ub.String(), http.StatusFound) } - -func (wui *WebUI) createImageMaterial(zid id.Zid) ast.MaterialNode { - ub := wui.NewURLBuilder('z').SetZid(api.ZettelID(zid.String())) - ref := ast.ParseReference(ub.String()) - ref.State = ast.RefStateFound - return &ast.ReferenceMaterialNode{Ref: ref} -} Index: web/adapter/webui/webui.go ================================================================== --- web/adapter/webui/webui.go +++ web/adapter/webui/webui.go @@ -12,35 +12,34 @@ package webui import ( "bytes" "context" - "io" "log" "net/http" "sync" "time" - "zettelstore.de/c/api" + "zettelstore.de/z/api" "zettelstore.de/z/auth" "zettelstore.de/z/box" "zettelstore.de/z/collect" "zettelstore.de/z/config" "zettelstore.de/z/domain" "zettelstore.de/z/domain/id" "zettelstore.de/z/domain/meta" "zettelstore.de/z/encoder" + "zettelstore.de/z/input" "zettelstore.de/z/kernel" "zettelstore.de/z/parser" "zettelstore.de/z/template" "zettelstore.de/z/web/adapter" "zettelstore.de/z/web/server" ) // WebUI holds all data for delivering the web ui. type WebUI struct { - debug bool ab server.AuthBuilder authz auth.AuthzManager rtConfig config.Config token auth.TokenManager box webuiBox @@ -74,21 +73,20 @@ // New creates a new WebUI struct. func New(ab server.AuthBuilder, authz auth.AuthzManager, rtConfig config.Config, token auth.TokenManager, mgr box.Manager, pol auth.Policy) *WebUI { loginoutBase := ab.NewURLBuilder('i') wui := &WebUI{ - debug: kernel.Main.GetConfig(kernel.CoreService, kernel.CoreDebug).(bool), ab: ab, rtConfig: rtConfig, authz: authz, token: token, box: mgr, policy: pol, tokenLifetime: kernel.Main.GetConfig(kernel.WebService, kernel.WebTokenLifetimeHTML).(time.Duration), - cssBaseURL: ab.NewURLBuilder('z').SetZid(api.ZidBaseCSS).String(), - cssUserURL: ab.NewURLBuilder('z').SetZid(api.ZidUserCSS).String(), + cssBaseURL: ab.NewURLBuilder('z').SetZid(id.BaseCSSZid).String(), + cssUserURL: ab.NewURLBuilder('z').SetZid(id.UserCSSZid).String(), homeURL: ab.NewURLBuilder('/').String(), listZettelURL: ab.NewURLBuilder('h').String(), listRolesURL: ab.NewURLBuilder('h').AppendQuery("_l", "r").String(), listTagsURL: ab.NewURLBuilder('h').AppendQuery("_l", "t").String(), withAuth: authz.WithAuth(), @@ -194,12 +192,12 @@ var userZettelURL string var userIdent string userIsValid := user != nil if userIsValid { - userZettelURL = wui.NewURLBuilder('h').SetZid(api.ZettelID(user.Zid.String())).String() - userIdent = user.GetDefault(api.KeyUserID, "") + userZettelURL = wui.NewURLBuilder('h').SetZid(user.Zid).String() + userIdent = user.GetDefault(meta.KeyUserID, "") } newZettelLinks := wui.fetchNewTemplates(ctx, user) data.Lang = lang data.CSSBaseURL = wui.cssBaseURL @@ -241,34 +239,34 @@ if err != nil { return nil } refs := collect.Order(parser.ParseZettel(menu, "", wui.rtConfig)) for _, ref := range refs { - zid, err2 := id.Parse(ref.URL.Path) - if err2 != nil { + zid, err := id.Parse(ref.URL.Path) + if err != nil { continue } - m, err2 := wui.box.GetMeta(ctx, zid) - if err2 != nil { + m, err := wui.box.GetMeta(ctx, zid) + if err != nil { continue } if !wui.policy.CanRead(user, m) { continue } title := config.GetTitle(m, wui.rtConfig) - astTitle := parser.ParseMetadata(title) + astTitle := parser.ParseInlines(input.NewInput(title), meta.ValueSyntaxZmk) env := encoder.Environment{Lang: config.GetLang(m, wui.rtConfig)} - menuTitle, err2 := encodeInlines(astTitle, api.EncoderHTML, &env) - if err2 != nil { - menuTitle, err2 = encodeInlines(astTitle, api.EncoderText, nil) - if err2 != nil { + menuTitle, err := encodeInlines(astTitle, api.EncoderHTML, &env) + if err != nil { + menuTitle, err = encodeInlines(astTitle, api.EncoderText, nil) + if err != nil { menuTitle = title } } result = append(result, simpleLink{ Text: menuTitle, - URL: wui.NewURLBuilder('g').SetZid(api.ZettelID(m.Zid.String())).String(), + URL: wui.NewURLBuilder('g').SetZid(m.Zid).String(), }) } return result } @@ -286,11 +284,11 @@ if code == http.StatusInternalServerError { log.Printf("%v: %v", text, err) } user := wui.getUser(ctx) var base baseData - wui.makeBaseData(ctx, api.ValueLangEN, "Error", user, &base) + wui.makeBaseData(ctx, meta.ValueLangEN, "Error", user, &base) wui.renderTemplateStatus(ctx, w, code, id.ErrorTemplateZid, &base, struct { ErrorTitle string ErrorText string }{ ErrorTitle: http.StatusText(code), @@ -322,53 +320,18 @@ } } var content bytes.Buffer err = t.Render(&content, data) if err == nil { - wui.prepareAndWriteHeader(w, code) - err = writeHTMLStart(w, base.Lang) - if err == nil { - base.Content = content.String() - err = bt.Render(w, base) - if err == nil { - err = wui.writeHTMLEnd(w) - } - } - } - if err != nil { - log.Println("Unable to write HTML via template", err) - } -} - -func writeHTMLStart(w http.ResponseWriter, lang string) error { - _, err := io.WriteString(w, "\n\n\n") - } - } else { - _, err = io.WriteString(w, ">\n\n") - } - return err -} - -func (wui *WebUI) writeHTMLEnd(w http.ResponseWriter) error { - if wui.debug { - _, err := io.WriteString(w, "
    WARNING: Debug mode is enabled. DO NOT USE IN PRODUCTION!
    \n") - if err != nil { - return err - } - } - _, err := io.WriteString(w, "\n") - return err + base.Content = content.String() + w.Header().Set(api.HeaderContentType, "text/html; charset=utf-8") + w.WriteHeader(code) + err = bt.Render(w, base) + } + if err != nil { + log.Println("Unable to render template", err) + } } func (wui *WebUI) getUser(ctx context.Context) *meta.Meta { return wui.ab.GetUser(ctx) } // GetURLPrefix returns the configured URL prefix of the web server. @@ -378,21 +341,8 @@ func (wui *WebUI) NewURLBuilder(key byte) *api.URLBuilder { return wui.ab.NewURLBuilder(key) } func (wui *WebUI) clearToken(ctx context.Context, w http.ResponseWriter) context.Context { return wui.ab.ClearToken(ctx, w) } - func (wui *WebUI) setToken(w http.ResponseWriter, token []byte) { wui.ab.SetToken(w, token, wui.tokenLifetime) } - -func (wui *WebUI) prepareAndWriteHeader(w http.ResponseWriter, statusCode int) { - h := adapter.PrepareHeader(w, "text/html; charset=utf-8") - h.Set("Content-Security-Policy", "default-src 'self'; img-src * data:; style-src 'self' 'unsafe-inline'") - h.Set("Permissions-Policy", "payment=(), interest-cohort=()") - h.Set("Referrer-Policy", "no-referrer") - h.Set("X-Content-Type-Options", "nosniff") - if !wui.debug { - h.Set("X-Frame-Options", "sameorigin") - } - w.WriteHeader(statusCode) -} Index: web/server/impl/impl.go ================================================================== --- web/server/impl/impl.go +++ web/server/impl/impl.go @@ -14,11 +14,11 @@ import ( "context" "net/http" "time" - "zettelstore.de/c/api" + "zettelstore.de/z/api" "zettelstore.de/z/auth" "zettelstore.de/z/domain/meta" "zettelstore.de/z/web/server" ) @@ -89,11 +89,11 @@ } return updateContext(ctx, nil, nil) } // GetAuthData returns the full authentication data from the context. -func (*myServer) GetAuthData(ctx context.Context) *server.AuthData { +func (srv *myServer) GetAuthData(ctx context.Context) *server.AuthData { data, ok := ctx.Value(ctxKeySession).(*server.AuthData) if ok { return data } return nil Index: web/server/impl/router.go ================================================================== --- web/server/impl/router.go +++ web/server/impl/router.go @@ -14,11 +14,11 @@ import ( "net/http" "regexp" "strings" - "zettelstore.de/c/api" + "zettelstore.de/z/api" "zettelstore.de/z/auth" "zettelstore.de/z/kernel" "zettelstore.de/z/web/server" ) @@ -77,11 +77,11 @@ mh = new(methodHandler) table[key] = mh } mh[method] = handler if method == server.MethodGet { - if prevHandler := mh[server.MethodHead]; prevHandler == nil { + if handler := mh[server.MethodHead]; handler == nil { mh[server.MethodHead] = handler } } } Index: web/server/server.go ================================================================== --- web/server/server.go +++ web/server/server.go @@ -14,11 +14,11 @@ import ( "context" "net/http" "time" - "zettelstore.de/c/api" + "zettelstore.de/z/api" "zettelstore.de/z/domain/id" "zettelstore.de/z/domain/meta" ) // UserRetriever allows to retrieve user data based on a given zettel identifier. Index: www/changes.wiki ================================================================== --- www/changes.wiki +++ www/changes.wiki @@ -1,72 +1,9 @@ Change Log - -

    Changes for Version 0.2.0 (pending)

    - - -

    Changes for Version 0.1 (2021-11-11)

    - * v0.1.2 (2021-11-18) fixes a bug when selecting zettel from a list when - more than one comparison is negated. - * v0.1.1 (2021-11-12) updates the documentation, mostly related to the - deprecation of the // markup. - * Remove visual Zettelmarkup (italic, underline). Semantic Zettelmarkup - (emphasize, insert) is still allowed, but got a different syntax. The new - syntax for inserted text is >>inserted>>, - while its previous syntax now denotes emphasized text: - __emphasized__. The previous syntax for emphasized text is now - deprecated: //deprecated emphasized//. Starting with - Version 0.2.0, the deprecated syntax will not be supported. The - reason is the collision with URLs that also contain the characters - //. The ZMK encoding of a zettel may help with the transition - (/v/{ZettelID}?_part=zettel&_enc=zmk, on the Info page of - each zettel in the WebUI). Additionally, all deprecated uses of - // will be rendered with a dashed box within the WebUI. - (breaking: Zettelmarkup). - * API client software is now a [https://zettelstore.de/client/|separate] - project. - (breaking) - * Initial support for HTTP security headers (Content-Security-Policy, - Permissions-Policy, Referrer-Policy, X-Content-Type-Options, - X-Frame-Options). Header values are currently some constant values. - (possibly breaking: api, webui) - * Remove visual Zettelmarkup (bold, striketrough). Semantic Zettelmarkup - (strong, delete) is still allowed and replaces the visual elements - syntactically. The visual appearance should not change (depends on your - changes / additions to CSS zettel). - (possibly breaking: Zettelmarkup). - * Add API endpoint POST /v to retrieve HTMl and text encoded - strings from given ZettelMarkup encoded values. This will be used to - render a HTML page from a given zettel: in many cases the title of - a zettel must be treated separately. - (minor: api) - * Add API endpoint /m to retrieve only the metadata of a zettel. - (minor: api) - * New metadata value content-tags contains the tags that were given - in the zettel content. To put it simply, all-tags = tags - + content-tags. - (minor) - * Calculating the context of a zettel stops at the home zettel. - (minor: api, webui) - * When renaming or deleting a zettel, a warning will be given, if other - zettel references the given zettel, or when “deleting” will - uncover zettel in overlay box. - (minor: webui) - * Fix: do not allow control characters in JSON-based creating/updating API. - Otherwise, the created / updated zettel might not be parseable by the - software (but still by a human). In certain cases, even the WebUI might be - affected. - (minor: api, webui) - * Fix: when a very long word (longer than width of browser window) is given, - still allow to scroll horizontally. - (minor: webui) - * Separate repository for [https://zettelstore.de/contrib/|contributed] - software. First entry is a software for creating a presentation by using - zettel. - (info) - * Many smaller bug fixes and inprovements, to the software and to the - documentation. + +

    Changes for Version 0.0.16 (pending)

    Changes for Version 0.0.15 (2021-09-17)

    * Move again endpoint characters for authentication to make room for future features. WebUI authentication moves from /a to /i Index: www/download.wiki ================================================================== --- www/download.wiki +++ www/download.wiki @@ -7,20 +7,20 @@ * However, it is in use by the main developer since March 2020 without any damage. * It may be useful for you. It is useful for me. * Take a look at the [https://zettelstore.de/manual/|manual] to know how to start and use it.

    ZIP-ped Executables

    -Build: v0.1 (2021-11-11). +Build: v0.0.15 (2021-09-17). - * [/uv/zettelstore-0.1.2-linux-amd64.zip|Linux] (amd64) - * [/uv/zettelstore-0.1.2-linux-arm.zip|Linux] (arm6, e.g. Raspberry Pi) - * [/uv/zettelstore-0.1.2-windows-amd64.zip|Windows] (amd64) - * [/uv/zettelstore-0.1.2-darwin-amd64.zip|macOS] (amd64) - * [/uv/zettelstore-0.1.2-darwin-arm64.zip|macOS] (arm64, aka Apple silicon) + * [/uv/zettelstore-0.0.15-linux-amd64.zip|Linux] (amd64) + * [/uv/zettelstore-0.0.15-linux-arm.zip|Linux] (arm6, e.g. Raspberry Pi) + * [/uv/zettelstore-0.0.15-windows-amd64.zip|Windows] (amd64) + * [/uv/zettelstore-0.0.15-darwin-amd64.zip|macOS] (amd64) + * [/uv/zettelstore-0.0.15-darwin-arm64.zip|macOS] (arm64, aka Apple silicon) Unzip the appropriate file, install and execute Zettelstore according to the manual.

    Zettel for the manual

    As a starter, you can download the zettel for the manual -[/uv/manual-0.1.2.zip|here]. Just unzip the contained files and put them into +[/uv/manual-0.0.15.zip|here]. Just unzip the contained files and put them into your zettel folder or configure a file box to read the zettel directly from the ZIP file. Index: www/index.wiki ================================================================== --- www/index.wiki +++ www/index.wiki @@ -1,38 +1,31 @@ Home Zettelstore is a software that collects and relates your notes (“zettel”) to represent and enhance your knowledge. It helps with many tasks of personal knowledge management by explicitly supporting the -[https://en.wikipedia.org/wiki/Zettelkasten|Zettelkasten method]. The method is -based on creating many individual notes, each with one idea or information, -that are related to each other. Since knowledge is typically build up -gradually, one major focus is a long-term store of these notes, hence the name -“Zettelstore”. - -To get an initial impression, take a look at the -[https://zettelstore.de/manual/|manual]. It is a live example of the -zettelstore software, running in read-only mode. +[https://en.wikipedia.org/wiki/Zettelkasten|Zettelkasten method]. The +method is based on creating many individual notes, each with one idea or +information, that are related to each other. Since knowledge is typically build +up gradually, one major focus is a long-term store of these notes, hence the +name “Zettelstore”. + +To get an initial impression, take a look at the [https://zettelstore.de/manual/|manual]. +It is a live example of the zettelstore software, running in read-only mode. The software, including the manual, is licensed under the [/file?name=LICENSE.txt&ci=trunk|European Union Public License 1.2 (or later)]. -[https://zettelstore.de/client|Zettelstore Client] provides client software to -access Zettelstore via its API more easily, -[https://zettelstore.de/contrib|Zettelstore Contrib] contains contributed -software, which often connects to Zettelstore via its API. Some of the software -packages may be experimental. - [https://twitter.com/zettelstore|Stay tuned]…
    -

    Latest Release: 0.1.2 (2021-11-18)

    +

    Latest Release: 0.0.15 (2021-09-17)

    * [./download.wiki|Download] - * [./changes.wiki#0_1|Change summary] - * [/timeline?p=v0.1.2&bt=v0.0.15&y=ci|Check-ins for version 0.1.2], - [/vdiff?to=v0.1.2&from=v0.0.15|content diff] - * [/timeline?df=v0.1&y=ci|Check-ins derived from the 0.1 release], - [/vdiff?from=v0.1&to=trunk|content diff] + * [./changes.wiki#0_0_15|Change summary] + * [/timeline?p=version-0.0.15&bt=version-0.0.14&y=ci|Check-ins for version 0.0.15], + [/vdiff?to=version-0.0.15&from=version-0.0.14|content diff] + * [/timeline?df=version-0.0.15&y=ci|Check-ins derived from the 0.0.15 release], + [/vdiff?from=version-0.0.15&to=trunk|content diff] * [./plan.wiki|Limitations and planned improvements] * [/timeline?t=release|Timeline of all past releases]

    Build instructions