Many hyperlinks are disabled.
Use anonymous login
to enable hyperlinks.
Difference From v0.1.2 To v0.0.15
2021-11-18
| ||
15:41 | Typo in download page ... (check-in: 41b1f504ee user: stern tags: branch-0.1) | |
15:31 | Version 0.1.2 ... (check-in: 8065f433c2 user: stern tags: release, branch-0.1, v0.1.2) | |
2021-11-13
| ||
17:09 | Version 0.1.1 ... (check-in: 8d9dc27ae1 user: stern tags: trunk, release, v0.1.1) | |
2021-09-23
| ||
18:03 | Increase version to 0.0.16-dev to begin next development cycle ... (check-in: 9748c52fdb user: stern tags: trunk) | |
2021-09-17
| ||
14:55 | Version 0.0.15 ... (check-in: 66498fc30a user: stern tags: trunk, release, version-0.0.15, v0.0.15) | |
2021-09-16
| ||
18:02 | Initial development zettel ... (check-in: 64e566f5db user: stern tags: trunk) | |
Changes to Makefile.
1 2 3 4 5 6 7 8 9 | ## 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. | | < < < | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 | ## 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. .PHONY: check api build release clean check: go run tools/build.go check api: go run tools/build.go testapi version: @echo $(shell go run tools/build.go version) build: |
︙ | ︙ |
Changes to README.md.
︙ | ︙ | |||
9 10 11 12 13 14 15 | 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 [manual](https://zettelstore.de/manual/). It is a live example of the zettelstore software, running in read-only mode. | < < < < < < | 9 10 11 12 13 14 15 16 17 18 19 20 | 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 [manual](https://zettelstore.de/manual/). It is a live example of the zettelstore software, running in read-only mode. 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)… |
Changes to VERSION.
|
| | | 1 | 0.0.15 |
Added api/api.go.
> > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 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.
> > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 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.
> > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 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() } |
Changes to ast/inline.go.
︙ | ︙ | |||
197 198 199 200 201 202 203 | } // FormatKind specifies the format that is applied to the inline nodes. type FormatKind uint8 // Constants for FormatCode const ( | | > | > | > | > | | | | | | | | < | 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 | } // FormatKind specifies the format that is applied to the inline nodes. type FormatKind uint8 // Constants for FormatCode const ( _ 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. func (fn *FormatNode) WalkChildren(v Visitor) { Walk(v, fn.Inlines) |
︙ | ︙ |
Changes to ast/ref.go.
︙ | ︙ | |||
33 34 35 36 37 38 39 | } } 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 { | | | 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 | } } 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 { return &Reference{URL: u, Value: s, State: RefStateZettel} } if u.Path == "" && u.Fragment != "" { return &Reference{URL: u, Value: s, State: RefStateSelf} } } return &Reference{URL: u, Value: s, State: RefStateExternal} |
︙ | ︙ |
Changes to auth/impl/impl.go.
︙ | ︙ | |||
15 16 17 18 19 20 21 | "errors" "hash/fnv" "io" "time" "github.com/pascaldekloe/jwt" | < | 15 16 17 18 19 20 21 22 23 24 25 26 27 28 | "errors" "hash/fnv" "io" "time" "github.com/pascaldekloe/jwt" "zettelstore.de/z/auth" "zettelstore.de/z/auth/policy" "zettelstore.de/z/box" "zettelstore.de/z/config" "zettelstore.de/z/domain/id" "zettelstore.de/z/domain/meta" "zettelstore.de/z/kernel" |
︙ | ︙ | |||
80 81 82 83 84 85 86 | var ErrOtherKind = errors.New("auth: wrong token kind") // 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) { | | | | 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 | var ErrOtherKind = errors.New("auth: wrong token kind") // 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(meta.KeyRole); !ok || role != meta.ValueRoleUser { return nil, ErrNoUser } subject, ok := ident.Get(meta.KeyUserID) if !ok || subject == "" { return nil, ErrNoIdent } now := time.Now().Round(time.Second) claims := jwt.Claims{ Registered: jwt.Registered{ |
︙ | ︙ | |||
130 131 132 133 134 135 136 | return auth.TokenData{}, ErrTokenExpired } ident := claims.Subject if ident == "" { return auth.TokenData{}, ErrNoIdent } if zidS, ok := claims.Set["zid"].(string); ok { | | | | 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 | return auth.TokenData{}, ErrTokenExpired } ident := claims.Subject if ident == "" { return auth.TokenData{}, ErrNoIdent } if zidS, ok := claims.Set["zid"].(string); ok { 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(), Expires: expires, Ident: ident, |
︙ | ︙ | |||
168 169 170 171 172 173 174 | return meta.UserRoleUnknown } return meta.UserRoleOwner } if a.IsOwner(user.Zid) { return meta.UserRoleOwner } | | | 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 | return meta.UserRoleUnknown } return meta.UserRoleOwner } if a.IsOwner(user.Zid) { return meta.UserRoleOwner } if val, ok := user.Get(meta.KeyUserRole); ok { if ur := meta.GetUserRole(val); ur != meta.UserRoleUnknown { return ur } } return meta.UserRoleReader } func (a *myAuth) BoxWithPolicy(auth server.Auth, unprotectedBox box.Box, rtConfig config.Config) (box.Box, auth.Policy) { return policy.BoxWithPolicy(auth, a, unprotectedBox, rtConfig) } |
Changes to auth/policy/default.go.
︙ | ︙ | |||
8 9 10 11 12 13 14 | // under this license. //----------------------------------------------------------------------------- // Package policy provides some interfaces and implementation for authorizsation policies. package policy import ( | < | | | | | | | | | 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 | // under this license. //----------------------------------------------------------------------------- // Package policy provides some interfaces and implementation for authorizsation policies. package policy import ( "zettelstore.de/z/auth" "zettelstore.de/z/domain/meta" ) type defaultPolicy struct { manager auth.AuthzManager } 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(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 != meta.ValueUserRoleOwner && !meta.BoolValue(metaRo) } userRole := d.manager.GetUserRole(user) switch metaRo { case meta.ValueUserRoleReader: return userRole > meta.UserRoleReader case meta.ValueUserRoleWriter: return userRole > meta.UserRoleWriter case meta.ValueUserRoleOwner: return userRole > meta.UserRoleOwner } return !meta.BoolValue(metaRo) } |
Changes to auth/policy/owner.go.
︙ | ︙ | |||
8 9 10 11 12 13 14 | // under this license. //----------------------------------------------------------------------------- // Package policy provides some interfaces and implementation for authorizsation policies. package policy import ( | < | 8 9 10 11 12 13 14 15 16 17 18 19 20 21 | // under this license. //----------------------------------------------------------------------------- // Package policy provides some interfaces and implementation for authorizsation policies. package policy import ( "zettelstore.de/z/auth" "zettelstore.de/z/config" "zettelstore.de/z/domain/meta" ) type ownerPolicy struct { manager auth.AuthzManager |
︙ | ︙ | |||
31 32 33 34 35 36 37 | return o.userIsOwner(user) || o.userCanCreate(user, newMeta) } func (o *ownerPolicy) userCanCreate(user, newMeta *meta.Meta) bool { if o.manager.GetUserRole(user) == meta.UserRoleReader { return false } | | | 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 | return o.userIsOwner(user) || o.userCanCreate(user, newMeta) } func (o *ownerPolicy) userCanCreate(user, newMeta *meta.Meta) bool { if o.manager.GetUserRole(user) == meta.UserRoleReader { return false } if role, ok := newMeta.Get(meta.KeyRole); ok && role == meta.ValueRoleUser { return false } return true } func (o *ownerPolicy) CanRead(user, m *meta.Meta) bool { // No need to call o.pre.CanRead(user, meta), because it will always return true. |
︙ | ︙ | |||
57 58 59 60 61 62 63 | return false case meta.VisibilityPublic: return true } if user == nil { return false } | | | | | | | | 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 | return false case meta.VisibilityPublic: return true } if user == nil { return false } 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: return true case meta.UserRoleCreator: return vis == meta.VisibilityCreator default: return false } } var noChangeUser = []string{ 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 } vis := o.authConfig.GetVisibility(oldMeta) if res, ok := o.checkVisibility(user, vis); ok { return res } if o.userIsOwner(user) { return true } if !o.userCanRead(user, oldMeta, vis) { return false } 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 } } |
︙ | ︙ | |||
143 144 145 146 147 148 149 | func (o *ownerPolicy) userIsOwner(user *meta.Meta) bool { if user == nil { return false } if o.manager.IsOwner(user.Zid) { return true } | | | 142 143 144 145 146 147 148 149 150 151 152 153 | func (o *ownerPolicy) userIsOwner(user *meta.Meta) bool { if user == nil { return false } if o.manager.IsOwner(user.Zid) { return true } if val, ok := user.Get(meta.KeyUserRole); ok && val == meta.ValueUserRoleOwner { return true } return false } |
Changes to auth/policy/policy_test.go.
︙ | ︙ | |||
11 12 13 14 15 16 17 | // Package policy provides some interfaces and implementation for authorizsation policies. package policy import ( "fmt" "testing" | < | 11 12 13 14 15 16 17 18 19 20 21 22 23 24 | // Package policy provides some interfaces and implementation for authorizsation policies. package policy import ( "fmt" "testing" "zettelstore.de/z/auth" "zettelstore.de/z/domain/id" "zettelstore.de/z/domain/meta" ) func TestPolicies(t *testing.T) { t.Parallel() |
︙ | ︙ | |||
56 57 58 59 60 61 62 | } type testAuthzManager struct { readOnly bool withAuth bool } | | | | | | | | 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 | } type testAuthzManager struct { readOnly bool withAuth bool } 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 { if a.WithAuth() { return meta.UserRoleUnknown } return meta.UserRoleOwner } if a.IsOwner(user.Zid) { return meta.UserRoleOwner } if val, ok := user.Get(meta.KeyUserRole); ok { if ur := meta.GetUserRole(val); ur != meta.UserRoleUnknown { return ur } } return meta.UserRoleReader } type authConfig struct{ expert bool } func (ac *authConfig) GetExpertMode() bool { return ac.expert } func (ac *authConfig) GetVisibility(m *meta.Meta) meta.Visibility { if vis, ok := m.Get(meta.KeyVisibility); ok { return meta.GetVisibility(vis) } return meta.VisibilityLogin } func testCreate(t *testing.T, pol auth.Policy, withAuth, readonly bool) { t.Helper() |
︙ | ︙ | |||
248 249 250 251 252 253 254 | zettel := newZettel() publicZettel := newPublicZettel() loginZettel := newLoginZettel() ownerZettel := newOwnerZettel() expertZettel := newExpertZettel() userZettel := newUserZettel() writerNew := writer.Clone() | | | 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 | zettel := newZettel() publicZettel := newPublicZettel() loginZettel := newLoginZettel() ownerZettel := newOwnerZettel() expertZettel := newExpertZettel() userZettel := newUserZettel() writerNew := writer.Clone() writerNew.Set(meta.KeyUserRole, owner.GetDefault(meta.KeyUserRole, "")) roFalse := newRoFalseZettel() roTrue := newRoTrueZettel() roReader := newRoReaderZettel() roWriter := newRoWriterZettel() roOwner := newRoOwnerZettel() notAuthNotReadonly := !withAuth && !readonly testCases := []struct { |
︙ | ︙ | |||
575 576 577 578 579 580 581 | visZid = id.Zid(1023) userZid = id.Zid(1025) ) func newAnon() *meta.Meta { return nil } func newCreator() *meta.Meta { user := meta.New(creatorZid) | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | 574 575 576 577 578 579 580 581 582 583 584 585 586 587 588 589 590 591 592 593 594 595 596 597 598 599 600 601 602 603 604 605 606 607 608 609 610 611 612 613 614 615 616 617 618 619 620 621 622 623 624 625 626 627 628 629 630 631 632 633 634 635 636 637 638 639 640 641 642 643 644 645 646 647 648 649 650 651 652 653 654 655 656 657 658 659 660 661 662 663 664 665 666 667 668 669 670 671 672 673 674 675 676 677 678 679 680 681 682 683 684 | visZid = id.Zid(1023) userZid = id.Zid(1025) ) func newAnon() *meta.Meta { return nil } func newCreator() *meta.Meta { user := meta.New(creatorZid) 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(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(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(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(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(meta.KeyTitle, "Any Zettel") return m } func newPublicZettel() *meta.Meta { m := meta.New(visZid) m.Set(meta.KeyTitle, "Public Zettel") m.Set(meta.KeyVisibility, meta.ValueVisibilityPublic) return m } func newCreatorZettel() *meta.Meta { m := meta.New(visZid) m.Set(meta.KeyTitle, "Creator Zettel") m.Set(meta.KeyVisibility, meta.ValueVisibilityCreator) return m } func newLoginZettel() *meta.Meta { m := meta.New(visZid) m.Set(meta.KeyTitle, "Login Zettel") m.Set(meta.KeyVisibility, meta.ValueVisibilityLogin) return m } func newOwnerZettel() *meta.Meta { m := meta.New(visZid) m.Set(meta.KeyTitle, "Owner Zettel") m.Set(meta.KeyVisibility, meta.ValueVisibilityOwner) return m } func newExpertZettel() *meta.Meta { m := meta.New(visZid) m.Set(meta.KeyTitle, "Expert Zettel") m.Set(meta.KeyVisibility, meta.ValueVisibilityExpert) return m } func newRoFalseZettel() *meta.Meta { m := meta.New(zettelZid) 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(meta.KeyTitle, "A r/o Zettel") m.Set(meta.KeyReadOnly, "true") return m } func newRoReaderZettel() *meta.Meta { m := meta.New(zettelZid) 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(meta.KeyTitle, "Writer r/o Zettel") m.Set(meta.KeyReadOnly, meta.ValueUserRoleWriter) return m } func newRoOwnerZettel() *meta.Meta { m := meta.New(zettelZid) 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(meta.KeyTitle, "Any User") m.Set(meta.KeyRole, meta.ValueRoleUser) return m } |
Changes to box/box.go.
︙ | ︙ | |||
14 15 16 17 18 19 20 | import ( "context" "errors" "fmt" "io" "time" | < | 14 15 16 17 18 19 20 21 22 23 24 25 26 27 | import ( "context" "errors" "fmt" "io" "time" "zettelstore.de/z/domain" "zettelstore.de/z/domain/id" "zettelstore.de/z/domain/meta" "zettelstore.de/z/search" ) // BaseBox is implemented by all Zettel boxes. |
︙ | ︙ | |||
245 246 247 248 249 250 251 | return fmt.Sprintf("operation %q not allowed for not authorized user", err.Op) } if err.Zid.IsValid() { return fmt.Sprintf( "operation %q on zettel %v not allowed for user %v/%v", err.Op, err.Zid.String(), | | | | | 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 | return fmt.Sprintf("operation %q not allowed for not authorized user", err.Op) } 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(meta.KeyUserID, "?"), err.User.Zid.String()) } return fmt.Sprintf( "operation %q not allowed for user %v/%v", err.Op, err.User.GetDefault(meta.KeyUserID, "?"), err.User.Zid.String()) } // Is return true, if the error is of type ErrNotAllowed. 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. var ErrStopped = errors.New("box is stopped") |
︙ | ︙ |
Changes to box/compbox/compbox.go.
︙ | ︙ | |||
11 12 13 14 15 16 17 | // Package compbox provides zettel that have computed content. package compbox import ( "context" "net/url" | < | 11 12 13 14 15 16 17 18 19 20 21 22 23 24 | // Package compbox provides zettel that have computed content. package compbox import ( "context" "net/url" "zettelstore.de/z/box" "zettelstore.de/z/box/manager" "zettelstore.de/z/domain" "zettelstore.de/z/domain/id" "zettelstore.de/z/domain/meta" ) |
︙ | ︙ | |||
35 36 37 38 39 40 41 | number int enricher box.Enricher } var myConfig *meta.Meta var myZettel = map[id.Zid]struct { meta func(id.Zid) *meta.Meta | | | | | | | | | 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 | number int enricher box.Enricher } var myConfig *meta.Meta var myZettel = map[id.Zid]struct { meta func(id.Zid) *meta.Meta content func(*meta.Meta) string }{ 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} } |
︙ | ︙ | |||
144 145 146 147 148 149 150 | func (*compBox) ReadStats(st *box.ManagedBoxStats) { st.ReadOnly = true st.Zettel = len(myZettel) } func updateMeta(m *meta.Meta) { | | | | | | | | | 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 | func (*compBox) ReadStats(st *box.ManagedBoxStats) { st.ReadOnly = true st.Zettel = len(myZettel) } func updateMeta(m *meta.Meta) { 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) } } |
Changes to box/compbox/config.go.
︙ | ︙ | |||
8 9 10 11 12 13 14 | // under this license. //----------------------------------------------------------------------------- // Package compbox provides zettel that have computed content. package compbox import ( | | < | | | | | | | | | | | | | | 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 | // under this license. //----------------------------------------------------------------------------- // Package compbox provides zettel that have computed content. package compbox import ( "strings" "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(meta.KeyTitle, "Zettelstore Startup Configuration") m.Set(meta.KeyVisibility, meta.ValueVisibilityExpert) return m } func genConfigZettelC(m *meta.Meta) string { var sb strings.Builder for i, p := range myConfig.Pairs(false) { if i > 0 { sb.WriteByte('\n') } sb.WriteString("; ''") sb.WriteString(p.Key) sb.WriteString("''") if p.Value != "" { sb.WriteString("\n: ``") for _, r := range p.Value { if r == '`' { sb.WriteByte('\\') } sb.WriteRune(r) } sb.WriteString("``") } } return sb.String() } |
Changes to box/compbox/keys.go.
︙ | ︙ | |||
8 9 10 11 12 13 14 | // under this license. //----------------------------------------------------------------------------- // Package compbox provides zettel that have computed content. package compbox import ( | < > < | | | | | | | | 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 | // under this license. //----------------------------------------------------------------------------- // Package compbox provides zettel that have computed content. package compbox import ( "fmt" "strings" "zettelstore.de/z/domain/id" "zettelstore.de/z/domain/meta" ) func genKeysM(zid id.Zid) *meta.Meta { m := meta.New(zid) m.Set(meta.KeyTitle, "Zettelstore Supported Metadata Keys") m.Set(meta.KeyVisibility, meta.ValueVisibilityLogin) return m } func genKeysC(*meta.Meta) string { keys := meta.GetSortedKeyDescriptions() var sb strings.Builder sb.WriteString("|=Name<|=Type<|=Computed?:|=Property?:\n") for _, kd := range keys { fmt.Fprintf(&sb, "|%v|%v|%v|%v\n", kd.Name, kd.Type.Name, kd.IsComputed(), kd.IsProperty()) } return sb.String() } |
Changes to box/compbox/manager.go.
︙ | ︙ | |||
8 9 10 11 12 13 14 | // under this license. //----------------------------------------------------------------------------- // Package compbox provides zettel that have computed content. package compbox import ( | < > < | | | | | | | | 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 | // under this license. //----------------------------------------------------------------------------- // Package compbox provides zettel that have computed content. package compbox import ( "fmt" "strings" "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(meta.KeyTitle, "Zettelstore Box Manager") return m } func genManagerC(*meta.Meta) string { kvl := kernel.Main.GetServiceStatistics(kernel.BoxService) if len(kvl) == 0 { return "No statistics available" } var sb strings.Builder sb.WriteString("|=Name|=Value>\n") for _, kv := range kvl { fmt.Fprintf(&sb, "| %v | %v\n", kv.Key, kv.Value) } return sb.String() } |
Changes to box/compbox/version.go.
︙ | ︙ | |||
8 9 10 11 12 13 14 | // under this license. //----------------------------------------------------------------------------- // Package compbox provides zettel that have computed content. package compbox import ( | > | | | | | | | | | > > | | < < < < > | 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 | // under this license. //----------------------------------------------------------------------------- // Package compbox provides zettel that have computed content. package compbox import ( "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(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(meta.KeyVisibility, meta.ValueVisibilityPublic) return m } 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) 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) string { return fmt.Sprintf( "%v/%v", kernel.Main.GetConfig(kernel.CoreService, kernel.CoreGoOS).(string), kernel.Main.GetConfig(kernel.CoreService, kernel.CoreGoArch).(string), ) } |
Changes to box/constbox/base.css.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 | *,*::before,*::after { box-sizing: border-box; } html { font-size: 1rem; font-family: serif; scroll-behavior: smooth; height: 100%; } body { margin: 0; min-height: 100vh; line-height: 1.4; background-color: #f8f8f8 ; height: 100%; } nav.zs-menu { background-color: hsl(210, 28%, 90%); overflow: auto; white-space: nowrap; | > > | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 | *,*::before,*::after { box-sizing: border-box; } html { font-size: 1rem; font-family: serif; scroll-behavior: smooth; 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%); overflow: auto; white-space: nowrap; |
︙ | ︙ | |||
227 228 229 230 231 232 233 | span.zs-indication { border: 1px solid black; border-radius: .25rem; padding: .1rem .2rem; font-size: 95%; } .zs-example { border-style: dotted !important } | < < < < < < < < < < < < < < < < < < < | 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 | span.zs-indication { border: 1px solid black; border-radius: .25rem; padding: .1rem .2rem; font-size: 95%; } .zs-example { border-style: dotted !important } .zs-error { background-color: lightpink; border-style: none !important; font-weight: bold; } kbd { background: hsl(210, 5%, 100%); border: 1px solid hsl(210, 5%, 70%); border-radius: .25rem; padding: .1rem .2rem; font-size: 75%; } |
︙ | ︙ |
Changes to box/constbox/base.mustache.
1 2 3 4 5 6 7 8 | <meta charset="utf-8"> <meta name="viewport" content="width=device-width, initial-scale=1.0"> <meta name="generator" content="Zettelstore"> <meta name="format-detection" content="telephone=no"> {{{MetaHeader}}} <link rel="stylesheet" href="{{{CSSBaseURL}}}"> <link rel="stylesheet" href="{{{CSSUserURL}}}"> <title>{{Title}}</title> | > > > > | 1 2 3 4 5 6 7 8 9 10 11 12 | <!DOCTYPE html> <html{{#Lang}} lang="{{Lang}}"{{/Lang}}> <head> <meta charset="utf-8"> <meta name="referrer" content="no-referrer"> <meta name="viewport" content="width=device-width, initial-scale=1.0"> <meta name="generator" content="Zettelstore"> <meta name="format-detection" content="telephone=no"> {{{MetaHeader}}} <link rel="stylesheet" href="{{{CSSBaseURL}}}"> <link rel="stylesheet" href="{{{CSSUserURL}}}"> <title>{{Title}}</title> |
︙ | ︙ | |||
54 55 56 57 58 59 60 | {{{Content}}} </main> {{#FooterHTML}} <footer> {{{FooterHTML}}} </footer> {{/FooterHTML}} | > > | 58 59 60 61 62 63 64 65 66 | {{{Content}}} </main> {{#FooterHTML}} <footer> {{{FooterHTML}}} </footer> {{/FooterHTML}} </body> </html> |
Changes to box/constbox/constbox.go.
︙ | ︙ | |||
12 13 14 15 16 17 18 | package constbox import ( "context" _ "embed" // Allow to embed file content "net/url" | < | 12 13 14 15 16 17 18 19 20 21 22 23 24 25 | package constbox import ( "context" _ "embed" // Allow to embed file content "net/url" "zettelstore.de/z/box" "zettelstore.de/z/box/manager" "zettelstore.de/z/domain" "zettelstore.de/z/domain/id" "zettelstore.de/z/domain/meta" ) |
︙ | ︙ | |||
117 118 119 120 121 122 123 | } const syntaxTemplate = "mustache" var constZettelMap = map[id.Zid]constZettel{ id.ConfigurationZid: { constHeader{ | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 326 327 328 329 330 331 332 333 334 335 336 337 338 339 340 341 342 343 344 345 346 347 348 349 350 351 352 353 354 355 356 357 358 359 360 361 362 363 364 365 366 367 368 369 370 371 372 373 374 375 376 377 378 379 380 381 382 383 384 385 386 387 | } const syntaxTemplate = "mustache" var constZettelMap = map[id.Zid]constZettel{ id.ConfigurationZid: { constHeader{ meta.KeyTitle: "Zettelstore Runtime Configuration", meta.KeyRole: meta.ValueRoleConfiguration, meta.KeySyntax: meta.ValueSyntaxNone, meta.KeyNoIndex: meta.ValueTrue, meta.KeyVisibility: meta.ValueVisibilityOwner, }, domain.NewContent("")}, id.LicenseZid: { constHeader{ 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.AuthorsZid: { constHeader{ 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.DependenciesZid: { constHeader{ 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{ 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{ 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{ 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{ 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{ 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{ 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{ 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{ 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{ 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{ 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{ 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{ 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.BaseCSSZid: { constHeader{ meta.KeyTitle: "Zettelstore Base CSS", meta.KeyRole: meta.ValueRoleConfiguration, meta.KeySyntax: "css", meta.KeyNoIndex: meta.ValueTrue, meta.KeyVisibility: meta.ValueVisibilityPublic, }, domain.NewContent(contentBaseCSS)}, id.UserCSSZid: { constHeader{ meta.KeyTitle: "Zettelstore User CSS", meta.KeyRole: meta.ValueRoleConfiguration, meta.KeySyntax: "css", meta.KeyVisibility: meta.ValueVisibilityPublic, }, domain.NewContent("/* User-defined CSS */")}, id.EmojiZid: { constHeader{ 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{ meta.KeyTitle: "New Menu", meta.KeyRole: meta.ValueRoleConfiguration, meta.KeySyntax: meta.ValueSyntaxZmk, meta.KeyLang: meta.ValueLangEN, meta.KeyVisibility: meta.ValueVisibilityCreator, }, domain.NewContent(contentNewTOCZettel)}, 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{ meta.KeyTitle: "Home", meta.KeyRole: meta.ValueRoleZettel, meta.KeySyntax: meta.ValueSyntaxZmk, meta.KeyLang: meta.ValueLangEN, }, domain.NewContent(contentHomeZettel)}, } //go:embed license.txt var contentLicense string //go:embed contributors.zettel var contentContributors string //go:embed dependencies.zettel var contentDependencies string //go:embed base.mustache var contentBaseMustache string //go:embed login.mustache var contentLoginMustache string //go:embed zettel.mustache var contentZettelMustache string //go:embed info.mustache var contentInfoMustache string //go:embed context.mustache var contentContextMustache string //go:embed form.mustache var contentFormMustache string //go:embed rename.mustache var contentRenameMustache string //go:embed delete.mustache var contentDeleteMustache string //go:embed listzettel.mustache var contentListZettelMustache string //go:embed listroles.mustache var contentListRolesMustache string //go:embed listtags.mustache var contentListTagsMustache string //go:embed error.mustache var contentErrorMustache string //go:embed base.css var contentBaseCSS string //go:embed emoji_spin.gif var contentEmoji string //go:embed newtoc.zettel var contentNewTOCZettel string //go:embed home.zettel var contentHomeZettel string |
Changes to box/constbox/delete.mustache.
1 2 3 4 5 | <article> <header> <h1>Delete Zettel {{Zid}}</h1> </header> <p>Do you really want to delete this zettel?</p> | < < < < < < < < < < < < < < < < < | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 | <article> <header> <h1>Delete Zettel {{Zid}}</h1> </header> <p>Do you really want to delete this zettel?</p> <dl> {{#MetaPairs}} <dt>{{Key}}:</dt><dd>{{Value}}</dd> {{/MetaPairs}} </dl> <form method="POST"> <input class="zs-button" type="submit" value="Delete"> </form> </article> {{end}} |
Changes to box/constbox/listtags.mustache.
1 2 3 4 5 6 7 | <nav> <header> <h1>Currently used tags</h1> <div class="zs-meta"> <a href="{{{ListTagsURL}}}">All</a>{{#MinCounts}}, <a href="{{{URL}}}">{{Count}}</a>{{/MinCounts}} </div> </header> | | | 1 2 3 4 5 6 7 8 9 10 | <nav> <header> <h1>Currently used tags</h1> <div class="zs-meta"> <a href="{{{ListTagsURL}}}">All</a>{{#MinCounts}}, <a href="{{{URL}}}">{{Count}}</a>{{/MinCounts}} </div> </header> {{#Tags}} <a href="{{{URL}}}" style="font-size:{{Size}}%">{{Name}}</a><sup>{{Count}}</sup> {{/Tags}} </nav> |
Changes to box/constbox/rename.mustache.
1 2 3 4 5 | <article> <header> <h1>Rename Zettel {{.Zid}}</h1> </header> <p>Do you really want to rename this zettel?</p> | < < < < < < < < < < < | 1 2 3 4 5 6 7 8 9 10 11 12 | <article> <header> <h1>Rename Zettel {{.Zid}}</h1> </header> <p>Do you really want to rename this zettel?</p> <form method="POST"> <div> <label for="newid">New zettel id</label> <input class="zs-input" type="text" id="newzid" name="newzid" placeholder="ZID.." value="{{Zid}}" autofocus> </div> <input type="hidden" id="curzid" name="curzid" value="{{Zid}}"> <input class="zs-button" type="submit" value="Rename"> |
︙ | ︙ |
Changes to box/dirbox/dirbox.go.
︙ | ︙ | |||
18 19 20 21 22 23 24 | "os" "path/filepath" "strconv" "strings" "sync" "time" | < | 18 19 20 21 22 23 24 25 26 27 28 29 30 31 | "os" "path/filepath" "strconv" "strings" "sync" "time" "zettelstore.de/z/box" "zettelstore.de/z/box/dirbox/directory" "zettelstore.de/z/box/filebox" "zettelstore.de/z/box/manager" "zettelstore.de/z/domain" "zettelstore.de/z/domain/id" "zettelstore.de/z/domain/meta" |
︙ | ︙ | |||
293 294 295 296 297 298 299 | return p[0 : len(p)-len(filepath.Ext(p))] } func (dp *dirBox) calcSpecExt(m *meta.Meta) (directory.MetaSpec, string) { if m.YamlSep { return directory.MetaSpecHeader, "zettel" } | | | | 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 | return p[0 : len(p)-len(filepath.Ext(p))] } func (dp *dirBox) calcSpecExt(m *meta.Meta) (directory.MetaSpec, string) { if m.YamlSep { return directory.MetaSpecHeader, "zettel" } syntax := m.GetDefault(meta.KeySyntax, "bin") switch syntax { case meta.ValueSyntaxNone, meta.ValueSyntaxZmk: return directory.MetaSpecHeader, "zettel" } for _, s := range dp.cdata.Config.GetZettelFileSyntax() { if s == syntax { return directory.MetaSpecHeader, "zettel" } } |
︙ | ︙ | |||
389 390 391 392 393 394 395 | func (dp *dirBox) ReadStats(st *box.ManagedBoxStats) { st.ReadOnly = dp.readonly st.Zettel, _ = dp.dirSrv.NumEntries() } func (dp *dirBox) cleanupMeta(m *meta.Meta) { | | | | | | 388 389 390 391 392 393 394 395 396 397 398 399 400 401 402 403 404 405 406 407 408 409 410 | func (dp *dirBox) ReadStats(st *box.ManagedBoxStats) { st.ReadOnly = dp.readonly st.Zettel, _ = dp.dirSrv.NumEntries() } func (dp *dirBox) cleanupMeta(m *meta.Meta) { if role, ok := m.Get(meta.KeyRole); !ok || role == "" { m.Set(meta.KeyRole, dp.cdata.Config.GetDefaultRole()) } 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) if cur := curID.String(); strings.HasPrefix(file, cur) { file = newID.String() + file[len(cur):] return filepath.Join(dir, file) } return path } |
Changes to box/dirbox/service.go.
︙ | ︙ | |||
71 72 73 74 75 76 77 | cmd.rc <- resGetMeta{m, err} } // COMMAND: getMetaContent ---------------------------------------- // // Retrieves the meta data and the content of a zettel. | | | | | 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 | cmd.rc <- resGetMeta{m, err} } // COMMAND: getMetaContent ---------------------------------------- // // Retrieves the meta data and the content of a zettel. 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 } type fileGetMetaContent struct { entry *directory.Entry rc chan<- resGetMetaContent } type resGetMetaContent struct { meta *meta.Meta content string err error } func (cmd *fileGetMetaContent) run() { var m *meta.Meta var content string var err error entry := cmd.entry switch entry.MetaSpec { case directory.MetaSpecFile: m, err = parseMetaFile(entry.Zid, entry.MetaPath) if err == nil { |
︙ | ︙ | |||
223 224 225 226 227 228 229 | panic("TODO: ???") } cmd.rc <- err } // Utility functions ---------------------------------------- | | | > > > > | | | 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 | panic("TODO: ???") } cmd.rc <- err } // Utility functions ---------------------------------------- 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 { return nil, err } inp := input.NewInput(src) return meta.NewFromInput(zid, inp), nil } func parseMetaContentFile(zid id.Zid, path string) (*meta.Meta, string, error) { src, err := readFileContent(path) if err != nil { return nil, "", err } inp := input.NewInput(src) meta := meta.NewFromInput(zid, inp) return meta, src[inp.Pos:], nil } func cmdCleanupMeta(m *meta.Meta, entry *directory.Entry) { |
︙ | ︙ |
Changes to box/dirbox/simpledir/simpledir.go.
︙ | ︙ | |||
38 39 40 41 42 43 44 | func (ss *simpleService) Start() error { ss.mx.Lock() defer ss.mx.Unlock() _, err := os.ReadDir(ss.dirPath) return err } | | | | < | | | | 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 | func (ss *simpleService) Start() error { ss.mx.Lock() defer ss.mx.Unlock() _, err := os.ReadDir(ss.dirPath) return err } func (ss *simpleService) Stop() error { return nil } func (ss *simpleService) NumEntries() (int, error) { ss.mx.Lock() defer ss.mx.Unlock() 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.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) 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) for _, dirEntry := range dirEntries { if dirEntry.IsDir() { continue } if info, err1 := dirEntry.Info(); err1 != nil || !info.Mode().IsRegular() { continue } name := dirEntry.Name() match := matchValidFileName(name) if len(match) == 0 { continue } zid, err := id.Parse(match[1]) if err != nil { continue } var entry *directory.Entry if e, ok := entrySet[zid]; ok { entry = e } else { entry = &directory.Entry{Zid: zid} |
︙ | ︙ | |||
128 129 130 131 132 133 134 | entry.ContentExt = ext } } func (ss *simpleService) GetEntry(zid id.Zid) (*directory.Entry, error) { ss.mx.Lock() defer ss.mx.Unlock() | | < | | 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 | entry.ContentExt = ext } } func (ss *simpleService) GetEntry(zid id.Zid) (*directory.Entry, error) { ss.mx.Lock() defer ss.mx.Unlock() return ss.getEntry(zid) } 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 } if len(paths) == 0 { return nil, nil |
︙ | ︙ | |||
155 156 157 158 159 160 161 | return entry, nil } func (ss *simpleService) GetNew() (*directory.Entry, error) { ss.mx.Lock() defer ss.mx.Unlock() zid, err := box.GetNewZid(func(zid id.Zid) (bool, error) { | | | | | | 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 | return entry, nil } 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.getEntry(zid) if err != nil { return false, nil } return !entry.IsValid(), nil }) if err != nil { return nil, err } return &directory.Entry{Zid: zid}, nil } func (ss *simpleService) UpdateEntry(entry *directory.Entry) error { // Nothing to to, since the actual file update is done by dirbox. return nil } func (ss *simpleService) RenameEntry(curEntry, newEntry *directory.Entry) error { // Nothing to to, since the actual file rename is done by dirbox. return nil } func (ss *simpleService) DeleteEntry(zid id.Zid) error { // Nothing to to, since the actual file delete is done by dirbox. return nil } |
Changes to box/filebox/filebox.go.
︙ | ︙ | |||
13 14 15 16 17 18 19 | import ( "errors" "net/url" "path/filepath" "strings" | < | 13 14 15 16 17 18 19 20 21 22 23 24 25 26 | import ( "errors" "net/url" "path/filepath" "strings" "zettelstore.de/z/box" "zettelstore.de/z/box/manager" "zettelstore.de/z/domain/id" "zettelstore.de/z/domain/meta" ) func init() { |
︙ | ︙ | |||
63 64 65 66 67 68 69 | } return ext } // CalcDefaultMeta returns metadata with default values for the given entry. func CalcDefaultMeta(zid id.Zid, ext string) *meta.Meta { m := meta.New(zid) | | | | | | | | | | 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 | } return ext } // 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(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(meta.KeyTitle); !ok || title == "" { m.Set(meta.KeyTitle, zid.String()) } if inMeta { if syntax, ok := m.Get(meta.KeySyntax); !ok || syntax == "" { dm := CalcDefaultMeta(zid, ext) syntax, ok = dm.Get(meta.KeySyntax) if !ok { panic("Default meta must contain syntax") } m.Set(meta.KeySyntax, syntax) } } if duplicates { m.Set(meta.KeyDuplicates, meta.ValueTrue) } } |
Changes to box/filebox/zipbox.go.
︙ | ︙ | |||
60 61 62 63 64 65 66 | defer reader.Close() zp.zettel = make(map[id.Zid]*zipEntry) for _, f := range reader.File { match := matchValidFileName(f.Name) if len(match) < 1 { continue } | | | | 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 | defer reader.Close() zp.zettel = make(map[id.Zid]*zipEntry) for _, f := range reader.File { match := matchValidFileName(f.Name) if len(match) < 1 { continue } zid, err := id.Parse(match[1]) if err != nil { continue } zp.addFile(zid, f.Name, match[3]) } return nil } |
︙ | ︙ | |||
116 117 118 119 120 121 122 | reader, err := zip.OpenReader(zp.name) if err != nil { return domain.Zettel{}, err } defer reader.Close() var m *meta.Meta | | | 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 | reader, err := zip.OpenReader(zp.name) if err != nil { return domain.Zettel{}, err } defer reader.Close() var m *meta.Meta var src string var inMeta bool if entry.metaInHeader { src, err = readZipFileContent(reader, entry.contentName) if err != nil { return domain.Zettel{}, err } inp := input.NewInput(src) |
︙ | ︙ | |||
170 171 172 173 174 175 176 | func (zp *zipBox) ApplyMeta(ctx context.Context, handle box.MetaFunc) error { reader, err := zip.OpenReader(zp.name) if err != nil { return err } defer reader.Close() for zid, entry := range zp.zettel { | | | | 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 | func (zp *zipBox) ApplyMeta(ctx context.Context, handle box.MetaFunc) error { reader, err := zip.OpenReader(zp.name) if err != nil { return err } defer reader.Close() for zid, entry := range zp.zettel { m, err := readZipMeta(reader, zid, entry) if err != nil { continue } zp.enricher.Enrich(ctx, m, zp.number) handle(m) } return nil } |
︙ | ︙ | |||
235 236 237 238 239 240 241 | if err != nil { return nil, err } inp := input.NewInput(src) return meta.NewFromInput(zid, inp), nil } | | | | > > | > > | 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 | if err != nil { return nil, err } inp := input.NewInput(src) return meta.NewFromInput(zid, inp), nil } func readZipFileContent(reader *zip.ReadCloser, name string) (string, error) { f, err := reader.Open(name) if err != nil { return "", err } defer f.Close() buf, err := io.ReadAll(f) if err != nil { return "", err } return string(buf), nil } |
Changes to box/manager/anteroom_test.go.
︙ | ︙ | |||
37 38 39 40 41 42 43 | ar.Enqueue(id.Zid(3), arUpdate) if ar.first == ar.last { t.Errorf("Expected more than one room, but got only one") } count := 0 for ; count < 1000; count++ { | | | 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 | ar.Enqueue(id.Zid(3), arUpdate) if ar.first == ar.last { t.Errorf("Expected more than one room, but got only one") } count := 0 for ; count < 1000; count++ { action, _, _ := ar.Dequeue() if action == arNothing { break } } if count != 3 { t.Errorf("Expected 3 dequeues, but got %v", count) } |
︙ | ︙ |
Changes to box/manager/box.go.
︙ | ︙ | |||
8 9 10 11 12 13 14 | // under this license. //----------------------------------------------------------------------------- // Package manager coordinates the various boxes and indexes of a Zettelstore. package manager import ( | < > | | | | | 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 | // under this license. //----------------------------------------------------------------------------- // Package manager coordinates the various boxes and indexes of a Zettelstore. package manager import ( "context" "errors" "strings" "zettelstore.de/z/box" "zettelstore.de/z/domain" "zettelstore.de/z/domain/id" "zettelstore.de/z/domain/meta" "zettelstore.de/z/search" ) // Conatains all box.Box related functions // Location returns some information where the box is located. func (mgr *Manager) Location() string { if len(mgr.boxes) <= 2 { return "NONE" } var sb strings.Builder for i := 0; i < len(mgr.boxes)-2; i++ { if i > 0 { sb.WriteString(", ") } sb.WriteString(mgr.boxes[i].Location()) } 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() defer mgr.mgrMx.RUnlock() return mgr.started && mgr.boxes[0].CanCreateZettel(ctx) |
︙ | ︙ |
Changes to box/manager/enrich.go.
︙ | ︙ | |||
11 12 13 14 15 16 17 | // Package manager coordinates the various boxes and indexes of a Zettelstore. package manager import ( "context" "strconv" | < | | | | | | 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 | // Package manager coordinates the various boxes and indexes of a Zettelstore. package manager import ( "context" "strconv" "zettelstore.de/z/box" "zettelstore.de/z/domain/meta" ) // Enrich computes additional properties and updates the given metadata. func (mgr *Manager) Enrich(ctx context.Context, m *meta.Meta, boxNumber int) { 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(meta.KeyBoxNumber, strconv.Itoa(boxNumber)) computePublished(m) mgr.idxStore.Enrich(ctx, m) } func computePublished(m *meta.Meta) { if _, ok := m.Get(meta.KeyPublished); ok { return } if modified, ok := m.Get(meta.KeyModified); ok { if _, ok = meta.TimeValue(modified); ok { m.Set(meta.KeyPublished, modified) return } } zid := m.Zid.String() if _, ok := meta.TimeValue(zid); ok { 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. } |
Changes to box/manager/indexer.go.
︙ | ︙ | |||
12 13 14 15 16 17 18 | package manager import ( "context" "net/url" "time" | < | 12 13 14 15 16 17 18 19 20 21 22 23 24 25 | package manager import ( "context" "net/url" "time" "zettelstore.de/z/box" "zettelstore.de/z/box/manager/store" "zettelstore.de/z/domain" "zettelstore.de/z/domain/id" "zettelstore.de/z/domain/meta" "zettelstore.de/z/kernel" "zettelstore.de/z/parser" |
︙ | ︙ | |||
140 141 142 143 144 145 146 | return false } return true } func (mgr *Manager) idxUpdateZettel(ctx context.Context, zettel domain.Zettel) { m := zettel.Meta | | | 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 | return false } return true } func (mgr *Manager) idxUpdateZettel(ctx context.Context, zettel domain.Zettel) { m := zettel.Meta if m.GetBool(meta.KeyNoIndex) { // Zettel maybe in index toCheck := mgr.idxStore.DeleteZettel(ctx, m.Zid) mgr.idxCheckZettel(toCheck) return } var cData collectData |
︙ | ︙ | |||
202 203 204 205 206 207 208 | } func (mgr *Manager) idxUpdateValue(ctx context.Context, inverseKey, value string, zi *store.ZettelIndex) { zid, err := id.Parse(value) if err != nil { return } | | | 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 | } 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 { zi.AddDeadRef(zid) return } if inverseKey == "" { zi.AddBackRef(zid) return } |
︙ | ︙ |
Changes to box/manager/manager.go.
︙ | ︙ | |||
10 11 12 13 14 15 16 17 18 19 20 21 22 23 | // Package manager coordinates the various boxes and indexes of a Zettelstore. package manager import ( "context" "io" "net/url" "sort" "sync" "time" "zettelstore.de/z/auth" "zettelstore.de/z/box" | > | 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 | // Package manager coordinates the various boxes and indexes of a Zettelstore. package manager import ( "context" "io" "log" "net/url" "sort" "sync" "time" "zettelstore.de/z/auth" "zettelstore.de/z/box" |
︙ | ︙ | |||
68 69 70 71 72 73 74 | type createFunc func(*url.URL, *ConnectData) (box.ManagedBox, error) var registry = map[string]createFunc{} // Register the encoder for later retrieval. func Register(scheme string, create createFunc) { if _, ok := registry[scheme]; ok { | | | 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 | type createFunc func(*url.URL, *ConnectData) (box.ManagedBox, error) var registry = map[string]createFunc{} // Register the encoder for later retrieval. func Register(scheme string, create createFunc) { if _, ok := registry[scheme]; ok { log.Fatalf("Box with scheme %q already registered", scheme) } registry[scheme] = create } // GetSchemes returns all registered scheme, ordered by scheme string. func GetSchemes() []string { result := make([]string, 0, len(registry)) |
︙ | ︙ | |||
230 231 232 233 234 235 236 | continue } err := ssi.Start(ctx) if err == nil { continue } for j := i + 1; j < len(mgr.boxes); j++ { | | | 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 | continue } err := ssi.Start(ctx) if err == nil { continue } for j := i + 1; j < len(mgr.boxes); j++ { if ssj, ok := mgr.boxes[j].(box.StartStopper); ok { ssj.Stop(ctx) } } mgr.mgrMx.Unlock() return err } mgr.idxAr.Reset() // Ensure an initial index run |
︙ | ︙ |
Changes to box/manager/memstore/memstore.go.
︙ | ︙ | |||
15 16 17 18 19 20 21 | "context" "fmt" "io" "sort" "strings" "sync" | < | 15 16 17 18 19 20 21 22 23 24 25 26 27 28 | "context" "fmt" "io" "sort" "strings" "sync" "zettelstore.de/z/box/manager/store" "zettelstore.de/z/domain/id" "zettelstore.de/z/domain/meta" ) type metaRefs struct { forward id.Slice |
︙ | ︙ | |||
83 84 85 86 87 88 89 | defer ms.mx.RUnlock() zi, ok := ms.idx[m.Zid] if !ok { return false } var updated bool if len(zi.dead) > 0 { | | | | > | | | | | > | | < | | | | | | 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 | defer ms.mx.RUnlock() zi, ok := ms.idx[m.Zid] if !ok { return false } var updated bool if len(zi.dead) > 0 { m.Set(meta.KeyDead, zi.dead.String()) updated = true } back := removeOtherMetaRefs(m, zi.backward.Copy()) if len(zi.backward) > 0 { m.Set(meta.KeyBackward, zi.backward.String()) updated = true } if len(zi.forward) > 0 { m.Set(meta.KeyForward, zi.forward.String()) back = remRefs(back, zi.forward) 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(meta.KeyBack, back.String()) updated = true } if zi.itags != "" { if tags, ok := m.Get(meta.KeyTags); ok { m.Set(meta.KeyAllTags, tags+" "+zi.itags) } else { m.Set(meta.KeyAllTags, zi.itags) } updated = true } else if tags, ok := m.Get(meta.KeyTags); ok { m.Set(meta.KeyAllTags, tags) updated = true } return updated } // SearchEqual returns all zettel that contains the given exact word. // The word must be normalized through Unicode NKFD, trimmed and not empty. |
︙ | ︙ |
Added client/client.go.
> > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 326 327 328 329 330 331 332 333 334 335 336 337 338 339 340 341 342 343 344 345 346 347 348 349 350 351 352 353 354 355 356 357 358 359 360 361 362 363 364 365 366 367 368 369 370 371 372 373 374 375 376 377 378 379 380 381 382 383 384 385 386 387 388 389 390 391 392 393 394 395 396 397 398 399 400 401 402 403 404 405 406 407 408 409 410 411 412 413 414 415 416 417 418 419 420 421 422 423 424 425 426 427 428 429 430 431 432 433 434 435 436 437 438 439 440 441 442 443 444 445 446 447 448 449 450 451 452 453 454 455 456 457 458 459 460 461 462 463 464 465 466 467 468 469 470 471 472 473 474 475 476 477 478 479 480 481 482 483 484 485 486 487 488 489 490 491 492 493 494 495 496 497 498 499 500 501 502 503 504 505 506 507 508 509 510 511 512 513 514 515 516 517 518 519 520 521 522 523 524 525 526 527 528 529 530 531 532 533 534 535 536 537 538 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.
> > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 326 327 328 329 330 331 332 333 334 335 336 337 338 339 340 341 342 343 344 345 346 347 348 349 350 351 352 353 354 355 356 357 358 359 360 361 362 363 364 365 366 367 368 369 370 371 372 373 374 375 376 377 378 379 380 381 382 383 384 385 386 387 388 389 390 391 392 393 394 395 396 397 398 399 400 401 402 403 404 405 406 407 408 409 410 411 412 413 414 415 416 417 418 419 420 421 422 423 424 425 426 427 428 429 430 431 432 433 434 435 436 437 438 439 440 441 442 443 444 445 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() } } |
Changes to cmd/cmd_file.go.
︙ | ︙ | |||
12 13 14 15 16 17 18 | import ( "flag" "fmt" "io" "os" | | | | | | | | | | 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 | import ( "flag" "fmt" "io" "os" "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" "zettelstore.de/z/parser" ) // ---------- Subcommand: file ----------------------------------------------- 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 } z := parser.ParseZettel( domain.Zettel{ Meta: m, Content: domain.NewContent(inp.Src[inp.Pos:]), }, m.GetDefault(meta.KeySyntax, meta.ValueSyntaxZmk), nil, ) encdr := encoder.Create(api.Encoder(enc), &encoder.Environment{ Lang: m.GetDefault(meta.KeyLang, meta.ValueLangEN), }) if encdr == nil { fmt.Fprintf(os.Stderr, "Unknown format %q\n", enc) return 2, nil } _, err = encdr.WriteZettel(os.Stdout, z, parser.ParseMetadata) if err != nil { return 2, err } fmt.Println() return 0, nil } func getInput(args []string) (*meta.Meta, *input.Input, error) { if len(args) < 1 { src, err := io.ReadAll(os.Stdin) if err != nil { return nil, nil, err } 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(string(src)) m := meta.NewFromInput(id.New(true), inp) if len(args) > 1 { src, err := os.ReadFile(args[1]) if err != nil { return nil, nil, err } inp = input.NewInput(string(src)) } return m, inp, nil } |
Changes to cmd/cmd_password.go.
1 | //----------------------------------------------------------------------------- | | | | | | | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 | //----------------------------------------------------------------------------- // 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 // under this license. //----------------------------------------------------------------------------- package cmd import ( "flag" "fmt" "os" "golang.org/x/term" "zettelstore.de/z/auth/cred" "zettelstore.de/z/domain/id" "zettelstore.de/z/domain/meta" ) // ---------- Subcommand: password ------------------------------------------- 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 { fmt.Fprintln(os.Stderr, "User zettel identification missing") return 2, nil |
︙ | ︙ | |||
56 57 58 59 60 61 62 | ident := fs.Arg(0) hashedPassword, err := cred.HashCredential(zid, ident, password) if err != nil { return 2, err } fmt.Printf("%v: %s\n%v: %s\n", | | | | 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 | ident := fs.Arg(0) hashedPassword, err := cred.HashCredential(zid, ident, password) if err != nil { return 2, err } fmt.Printf("%v: %s\n%v: %s\n", meta.KeyCredential, hashedPassword, meta.KeyUserID, ident, ) return 0, nil } func getPassword(prompt string) (string, error) { fmt.Fprintf(os.Stderr, "%s: ", prompt) password, err := term.ReadPassword(int(os.Stdin.Fd())) fmt.Fprintln(os.Stderr) return string(password), err } |
Changes to cmd/cmd_run.go.
︙ | ︙ | |||
12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 | import ( "flag" "zettelstore.de/z/auth" "zettelstore.de/z/box" "zettelstore.de/z/config" "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" ) // ---------- Subcommand: run ------------------------------------------------ func flgRun(fs *flag.FlagSet) { fs.String("c", defConfigfile, "configuration file") fs.Uint("a", 0, "port number kernel service (0=disable)") fs.Uint("p", 23123, "port number web service") fs.String("d", "", "zettel directory") fs.Bool("r", false, "system-wide read-only mode") fs.Bool("v", false, "verbose mode") fs.Bool("debug", false, "debug mode") } | > | > > > > > | | > > | | | < | < | 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 | import ( "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" ) // ---------- Subcommand: run ------------------------------------------------ func flgRun(fs *flag.FlagSet) { fs.String("c", defConfigfile, "configuration file") fs.Uint("a", 0, "port number kernel service (0=disable)") fs.Uint("p", 23123, "port number web service") fs.String("d", "", "zettel directory") fs.Bool("r", false, "system-wide read-only mode") fs.Bool("v", false, "verbose mode") fs.Bool("debug", false, "debug mode") } 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(debug bool) (int, error) { kern := kernel.Main kern.SetDebug(debug) if err := kern.StartService(kernel.WebService); err != nil { return 1, err } return 0, nil } func setupRouting(webSrv server.Server, boxManager box.Manager, authManager auth.Manager, rtConfig config.Config) { protectedBoxManager, authPolicy := authManager.BoxWithPolicy(webSrv, boxManager, rtConfig) a := api.New(webSrv, authManager, authManager, webSrv, rtConfig) wui := webui.New(webSrv, authManager, rtConfig, authManager, boxManager, authPolicy) ucAuthenticate := usecase.NewAuthenticate(authManager, authManager, boxManager) ucCreateZettel := usecase.NewCreateZettel(rtConfig, protectedBoxManager) ucGetMeta := usecase.NewGetMeta(protectedBoxManager) ucGetAllMeta := usecase.NewGetAllMeta(protectedBoxManager) ucGetZettel := usecase.NewGetZettel(protectedBoxManager) 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) 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)) 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(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))) webSrv.AddZettelRoute('f', server.MethodPost, wui.MakePostCreateZettelHandler(ucCreateZettel)) webSrv.AddZettelRoute('g', server.MethodGet, wui.MakeGetNewZettelHandler( |
︙ | ︙ | |||
106 107 108 109 110 111 112 | // API 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)) | < < | 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 | // API 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('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.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() { webSrv.AddListRoute('j', server.MethodPost, a.MakePostCreateZettelHandler(ucCreateZettel)) webSrv.AddZettelRoute('j', server.MethodPut, api.MakeUpdateZettelHandler(ucUpdate)) |
︙ | ︙ |
Changes to cmd/cmd_run_simple.go.
︙ | ︙ | |||
12 13 14 15 16 17 18 19 20 21 22 23 24 25 | import ( "flag" "fmt" "os" "strings" "zettelstore.de/z/kernel" ) func flgSimpleRun(fs *flag.FlagSet) { fs.String("d", "", "zettel directory") } | > | | | 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 | import ( "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(fs *flag.FlagSet, cfg *meta.Meta) (int, error) { kern := kernel.Main listenAddr := kern.GetConfig(kernel.WebService, kernel.WebListenAddress).(string) 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() kern.Log(fmt.Sprintf(" http://localhost%v", listenAddr[idx:])) kern.Log() |
︙ | ︙ |
Changes to cmd/command.go.
︙ | ︙ | |||
9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 | //----------------------------------------------------------------------------- package cmd import ( "flag" "sort" ) // Command stores information about commands / sub-commands. type Command struct { Name string // command name as it appears on the command line Func CommandFunc // function that executes a command 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. | > > > | | 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 | //----------------------------------------------------------------------------- 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 Func CommandFunc // function that executes a command 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, *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) // RegisterCommand registers the given command. |
︙ | ︙ |
Changes to cmd/main.go.
︙ | ︙ | |||
16 17 18 19 20 21 22 | "fmt" "net" "net/url" "os" "strconv" "strings" | | | | | 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 | "fmt" "net" "net/url" "os" "strconv" "strings" "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" "zettelstore.de/z/config" "zettelstore.de/z/domain/id" "zettelstore.de/z/domain/meta" "zettelstore.de/z/input" "zettelstore.de/z/kernel" "zettelstore.de/z/web/server" ) const ( defConfigfile = ".zscfg" ) func init() { RegisterCommand(Command{ Name: "help", 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, *meta.Meta) (int, error) { return 0, nil }, Header: true, }) RegisterCommand(Command{ Name: "run", Func: runFunc, Boxes: true, Header: true, |
︙ | ︙ | |||
89 90 91 92 93 94 95 | } else { configFile = defConfigfile } content, err := os.ReadFile(configFile) if err != nil { return meta.New(id.Invalid) } | | | 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 | } else { configFile = defConfigfile } content, err := os.ReadFile(configFile) if err != nil { return meta.New(id.Invalid) } 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) { switch flg.Name { case "p": |
︙ | ︙ | |||
111 112 113 114 115 116 117 | case "d": val := flg.Value.String() if strings.HasPrefix(val, "/") { val = "dir://" + val } else { val = "dir:" + val } | < < < < < < < < < < < < | | < | 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 | case "d": val := flg.Value.String() if strings.HasPrefix(val, "/") { val = "dir://" + val } else { val = "dir:" + val } cfg.Set(keyBoxOneURI, val) case "r": cfg.Set(keyReadOnly, flg.Value.String()) case "v": cfg.Set(keyVerbose, flg.Value.String()) } }) return cfg } func parsePort(s string) (string, error) { port, err := net.LookupPort("tcp", s) if err != nil { fmt.Fprintf(os.Stderr, "Wrong port specification: %q", s) return "", err } return strconv.Itoa(port), nil } const ( keyAdminPort = "admin-port" keyDefaultDirBoxType = "default-dir-box-type" keyInsecureCookie = "insecure-cookie" keyListenAddr = "listen-addr" keyOwner = "owner" keyPersistentCookie = "persistent-cookie" keyBoxOneURI = kernel.BoxURIs + "1" keyReadOnly = "read-only-mode" keyTokenLifetimeHTML = "token-lifetime-html" keyTokenLifetimeAPI = "token-lifetime-api" keyURLPrefix = "url-prefix" keyVerbose = "verbose" ) func setServiceConfig(cfg *meta.Meta) error { 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, "")) ok = setConfigValue(ok, kernel.AuthService, kernel.AuthReadonly, cfg.GetBool(keyReadOnly)) |
︙ | ︙ | |||
254 255 256 257 258 259 260 | cfg := getConfig(fs) if err := setServiceConfig(cfg); err != nil { fmt.Fprintf(os.Stderr, "%s: %v\n", name, err) return 2 } setupOperations(cfg, command.Boxes) kernel.Main.Start(command.Header, command.LineServer) | | | 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 | cfg := getConfig(fs) if err := setServiceConfig(cfg); err != nil { 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, cfg) if err != nil { fmt.Fprintf(os.Stderr, "%s: %v\n", name, err) } kernel.Main.Shutdown(true) return exitCode } |
︙ | ︙ |
Changes to config/config.go.
︙ | ︙ | |||
8 9 10 11 12 13 14 | // under this license. //----------------------------------------------------------------------------- // Package config provides functions to retrieve runtime configuration data. package config import ( | < | 8 9 10 11 12 13 14 15 16 17 18 19 20 21 | // under this license. //----------------------------------------------------------------------------- // Package config provides functions to retrieve runtime configuration data. package config import ( "zettelstore.de/z/domain/id" "zettelstore.de/z/domain/meta" ) // Config allows to retrieve all defined configuration values that can be changed during runtime. type Config interface { AuthConfig |
︙ | ︙ | |||
70 71 72 73 74 75 76 | // GetVisibility returns the visibility value of the metadata. GetVisibility(m *meta.Meta) meta.Visibility } // 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 { | | | | | | 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 | // GetVisibility returns the visibility value of the metadata. GetVisibility(m *meta.Meta) meta.Visibility } // 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(meta.KeyTitle); ok { return val } if cfg != nil { return cfg.GetDefaultTitle() } return "Untitled" } // 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(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(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(meta.KeyLang); ok { return val } return cfg.GetDefaultLang() } |
Changes to docs/development/20210916193200.zettel.
1 2 3 4 | id: 20210916193200 title: Required Software role: zettel syntax: zmk | | | | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 | id: 20210916193200 title: Required Software role: zettel syntax: zmk 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|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 ``` |
Changes to docs/development/20210916194900.zettel.
1 2 3 4 | id: 20210916194900 title: Checklist for Release role: zettel syntax: zmk | | > > > | | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 | id: 20210916194900 title: Checklist for Release role: zettel syntax: zmk 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 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`` #* Check all ""Error: 404 Not Found"" #* Check all ""Error: 403 Forbidden"": allowed for endpoint ''/p'' with encoding ''html'' for those zettel that are accessible only in ''expert-mode''. |
︙ | ︙ | |||
28 29 30 31 32 33 34 | #* download.wiki #* changes.wiki #* plan.wiki # Set file ''VERSION'' to the new release version # Disable Fossil autosync mode: #* ``fossil setting autosync off`` # Commit the new release version: | | < < | 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 | #* download.wiki #* changes.wiki #* 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 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: #* ``fossil uv remove --glob '*-PREVVERSION*'`` # Add executables for release: |
︙ | ︙ |
Changes to docs/manual/00001000000000.zettel.
1 2 3 4 5 | id: 00001000000000 title: Zettelstore Manual role: manual tags: #manual #zettelstore syntax: zmk | < | | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 | id: 00001000000000 title: Zettelstore Manual role: manual tags: #manual #zettelstore syntax: zmk * [[Introduction|00001001000000]] * [[Design goals|00001002000000]] * [[Installation|00001003000000]] * [[Configuration|00001004000000]] * [[Structure of Zettelstore|00001005000000]] * [[Layout of a zettel|00001006000000]] * [[Zettelmarkup|00001007000000]] * [[Other markup languages|00001008000000]] * [[Security|00001010000000]] * [[API|00001012000000]] * [[Web user interface|00001014000000]] * Troubleshooting * Frequently asked questions Licensed under the EUPL-1.2-or-later. |
Changes to docs/manual/00001004010000.zettel.
1 2 3 4 5 | id: 00001004010000 title: Zettelstore startup configuration role: manual tags: #configuration #manual #zettelstore syntax: zmk | | | | < < < < < < < | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 | id: 00001004010000 title: Zettelstore startup configuration role: manual tags: #configuration #manual #zettelstore syntax: zmk 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. Therefore only the owner of the computer on which Zettelstore runs can change this information. The file for startup configuration must be created via a text editor in advance. The syntax of the configuration file is the same as for any zettel metadata. The following keys are supported: ; [!admin-port]''admin-port'' : Specifies the TCP port through which you can reach the [[administrator console|00001004100000]]. A value of ''0'' (the default) disables the administrator console. 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 : Specifies a [[box|00001004011200]] where zettel are stored. 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. ; [!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'' ; [!insecure-cookie]''insecure-cookie'' : Must be set to ''true'', if authentication is enabled and Zettelstore is not accessible not via HTTPS (but via HTTP). |
︙ | ︙ | |||
83 84 85 86 87 88 89 | It is automatically extended, when a new HTML view is rendered. ; [!url-prefix]''url-prefix'' : 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. | | | | 76 77 78 79 80 81 82 83 84 85 | It is automatically extended, when a new HTML view is rendered. ; [!url-prefix]''url-prefix'' : 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'' : Be more verbose inf logging data. Default: false |
Changes to docs/manual/00001004011200.zettel.
1 2 3 4 5 | id: 00001004011200 title: Zettelstore boxes role: manual tags: #configuration #manual #zettelstore syntax: zmk | | | | | | | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 | id: 00001004011200 title: Zettelstore boxes role: manual tags: #configuration #manual #zettelstore syntax: zmk 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//{-}. 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'' : 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.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:'' : 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. If a zettel is created or changed, it is always stored in the box specified with the ''box-uri-1'' key. This allows to overwrite zettel from other boxes, e.g. the predefined zettel. If you use the ''mem:'' box, where zettel are stored in volatile memory, it makes only sense if you configure it as ''box-uri-1''. Such a box will be empty when Zettelstore starts and only the first box will receive updates. You must make sure that your computer has enough RAM to store all zettel. |
Changes to docs/manual/00001004051000.zettel.
1 2 3 4 5 | id: 00001004051000 title: The ''run'' sub-command role: manual tags: #command #configuration #manual #zettelstore syntax: zmk | | | 1 2 3 4 5 6 7 8 9 10 11 12 13 | id: 00001004051000 title: The ''run'' sub-command role: manual tags: #command #configuration #manual #zettelstore syntax: zmk modified: 20210712234419 === ``zettelstore run`` This starts the web service. ``` zettelstore run [-a PORT] [-c CONFIGFILE] [-d DIR] [-debug] [-p PORT] [-r] [-v] ``` |
︙ | ︙ | |||
39 40 41 42 43 44 45 | If you want to listen on network card to process requests from other computer, please use [[''listen-addr''|00001004010000#listen-addr]] of the configuration file as described below. ; [!r]''-r'' : 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'' | | | 39 40 41 42 43 44 45 46 47 48 | If you want to listen on network card to process requests from other computer, please use [[''listen-addr''|00001004010000#listen-addr]] of the configuration file as described below. ; [!r]''-r'' : 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 in writing logs. Command line options take precedence over [[configuration file|00001004010000]] options. |
Changes to docs/manual/00001004100000.zettel.
1 2 3 4 5 | id: 00001004100000 title: Zettelstore Administrator Console role: manual tags: #configuration #manual #zettelstore syntax: zmk | | | | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 | id: 00001004100000 title: Zettelstore Administrator Console role: manual tags: #configuration #manual #zettelstore syntax: zmk 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. 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. You cannot connect to the administrator console if you are on a different computer. Of course, on multi-user systems with untrusted users, you should not enable the administrator console. * Enable via [[command line|00001004051000#a]] * Enable via [[configuration file|00001004010000#admin-port]] * [[List of supported commands|00001004101000]] |
Changes to docs/manual/00001004101000.zettel.
1 2 3 4 5 | id: 00001004101000 title: List of supported commands of the administrator console role: manual tags: #configuration #manual #zettelstore syntax: zmk | | | 1 2 3 4 5 6 7 8 9 10 11 12 13 | id: 00001004101000 title: List of supported commands of the administrator console role: manual tags: #configuration #manual #zettelstore syntax: zmk modified: 20210525161623 ; ''bye'' : Closes the connection to the administrator console. ; ''config SERVICE'' : Displays all valid configuration keys for the given service. If a key ends with the hyphen-minus character (""''-''"", ''U+002D''), the key denotes a list value. |
︙ | ︙ | |||
56 57 58 59 60 61 62 | : Displays s list of all available services and their current status. ; ''set-config SERVICE KEY VALUE'' : 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. | | | 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 | : Displays s list of all available services and their current status. ; ''set-config SERVICE KEY VALUE'' : 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-//. ; ''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. |
Changes to docs/manual/00001005000000.zettel.
1 2 3 4 5 | id: 00001005000000 title: Structure of Zettelstore role: manual tags: #design #manual #zettelstore syntax: zmk | | | 1 2 3 4 5 6 7 8 9 10 11 12 13 | id: 00001005000000 title: Structure of Zettelstore role: manual tags: #design #manual #zettelstore syntax: zmk 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. Zettelstore provides additional services to the user. |
︙ | ︙ | |||
28 29 30 31 32 33 34 | Every file in this directory that should be monitored by Zettelstore must have a file name that begins with 14 digits (0-9), the [[zettel identifier|00001006050000]]. 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. | | | 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 | Every file in this directory that should be monitored by Zettelstore must have a file name that begins with 14 digits (0-9), the [[zettel identifier|00001006050000]]. 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. 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. Two file extensions are used by Zettelstore: ''.meta'' and ''.zettel''. Other file extensions are used to determine the ""syntax"" of a zettel. |
︙ | ︙ | |||
74 75 76 77 78 79 80 | If you want to read the original zettel, you either have to delete the zettel (which removes it from the file directory), or you have to rename it to another zettel identifier. Now we have two places where zettel are stored: in the specific directory and within the Zettelstore software. * [[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. | | | | 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 | If you want to read the original zettel, you either have to delete the zettel (which removes it from the file directory), or you have to rename it to another zettel identifier. Now we have two places where zettel are stored: in the specific directory and within the Zettelstore software. * [[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.] 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 memeory (called //RAM//). This way is called ""memory box"". Other types of boxes could be added to Zettelstore. What about a ""remote Zettelstore box""? |
Changes to docs/manual/00001006010000.zettel.
1 2 | id: 00001006010000 title: Syntax of Metadata | < | | | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 | id: 00001006010000 title: Syntax of Metadata tags: #manual #syntax #zettelstore syntax: zmk 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. It begins at the first position of a new line. A key is separated from its value either by * 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. 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. Parsing metadata ends, if an empty line is found or if a line with at least three hyphen-minus characters is found. |
︙ | ︙ |
Changes to docs/manual/00001006020000.zettel.
1 2 3 4 5 | id: 00001006020000 title: Supported Metadata Keys role: manual tags: #manual #meta #reference #zettel #zettelstore syntax: zmk | | | < < < | < < < | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 | id: 00001006020000 title: Supported Metadata Keys role: manual tags: #manual #meta #reference #zettel #zettelstore syntax: zmk 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]]. ; [!all-tags]''all-tags'' : A property (a computed values that is not stored) that contains both the value of [[''tags''|#tags]] together with all [[tags|00001007040000#tag]] that are specified within the content. ; [!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 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. ; [!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'' : Contains the [[zettel identifier|00001006050000]], as given by the Zettelstore. It cannot be set manually, because it is a computed value. |
︙ | ︙ | |||
52 53 54 55 56 57 58 | ; [!modified]''modified'' : Date and time when a zettel was modified through Zettelstore. 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'' | | > > > | 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 | ; [!modified]''modified'' : Date and time when a zettel was modified through Zettelstore. 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 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. If [[''modified''|#modified]] is set, it contains the same value. Otherwise, if the zettel identifier contains a valid timestamp, the identifier is used. |
︙ | ︙ |
Changes to docs/manual/00001006055000.zettel.
1 2 3 4 5 | id: 00001006055000 title: Reserved zettel identifier role: manual tags: #design #manual #zettelstore syntax: zmk | | | 1 2 3 4 5 6 7 8 9 10 11 12 13 | id: 00001006055000 title: Reserved zettel identifier role: manual tags: #design #manual #zettelstore syntax: zmk 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. To make things easier, you normally should not use zettel identifier that begin with four zeroes (''0000''). |
︙ | ︙ | |||
36 37 38 39 40 41 42 | | 00000200000000 | 0000899999999 | Reserved for future use | 00009000000000 | 0000999999999 | Reserved for applications This list may change in the future. ==== External Applications |= From | To | Description | | | 36 37 38 39 40 41 42 43 | | 00000200000000 | 0000899999999 | Reserved for future use | 00009000000000 | 0000999999999 | Reserved for applications This list may change in the future. ==== External Applications |= From | To | Description | 00009000001000 | 00009000001999 | ZS Slides, an application to display zettel as a HTML-based slideshow |
Changes to docs/manual/00001007000000.zettel.
1 2 | id: 00001007000000 title: Zettelmarkup | < | | | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 | id: 00001007000000 title: Zettelmarkup tags: #manual #zettelmarkup #zettelstore syntax: zmk 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. Zettelmark can be much easier parsed / consumed by a software compared to other markup languages. Writing a parser for [[Markdown|https://daringfireball.net/projects/markdown/syntax]] is quite challenging. [[CommonMark|https://commonmark.org/]] is an attempt to make it simpler by providing a comprehensive specification, combined with an extra chapter to give hints for the implementation. Zettelmark follows some simple principles that anybody who knows to ho write software should be able understand to create an implementation. Zettelmarkup is a markup language on its own. This is in contrast to Markdown, which is basically a superset of HTML. While HTML is a markup language that will probably last for a long time, it cannot be easily translated to other formats, such as PDF, JSON, or LaTeX. 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**{-}, __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. * [[General principles|00001007010000]] * [[Basic definitions|00001007020000]] * [[Block-structured elements|00001007030000]] * [[Inline-structured element|00001007040000]] * [[Attributes|00001007050000]] * [[Summary of formatting characters|00001007060000]] |
Changes to docs/manual/00001007030100.zettel.
1 2 | id: 00001007030100 title: Zettelmarkup: Description Lists | < | | | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 | id: 00001007030100 title: Zettelmarkup: Description Lists tags: #manual #zettelmarkup #zettelstore syntax: zmk 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. 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. In contrast to terms, the actual descriptions are merged into a paragraph. This is because, an actual description can contain more than one paragraph. |
︙ | ︙ |
Changes to docs/manual/00001007030200.zettel.
1 2 | id: 00001007030200 title: Zettelmarkup: Nested Lists | < | | | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 | id: 00001007030200 title: Zettelmarkup: Nested Lists tags: #manual #zettelmarkup #zettelstore syntax: zmk 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//. 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. In other words: the inline elements must begin at the same column as it was on the previous line. |
︙ | ︙ |
Changes to docs/manual/00001007030300.zettel.
1 2 | id: 00001007030300 title: Zettelmarkup: Headings | < | | 1 2 3 4 5 6 7 8 9 10 11 12 | id: 00001007030300 title: Zettelmarkup: Headings tags: #manual #zettelmarkup #zettelstore syntax: zmk 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. ```zmk === Level 1 Heading |
︙ | ︙ | |||
29 30 31 32 33 34 35 | === Notes The heading level is translated to a HTML heading by adding 1 to the level, e.g. ``=== Level 1 Heading``{=zmk} translates to ==<h2>Level 1 Heading</h2>=={=html}. The ==<h1>=={=html} tag is rendered for the zettel title. This syntax is often used in a similar way in wiki implementation. | | | 28 29 30 31 32 33 34 35 36 37 38 39 40 41 | === Notes The heading level is translated to a HTML heading by adding 1 to the level, e.g. ``=== Level 1 Heading``{=zmk} translates to ==<h2>Level 1 Heading</h2>=={=html}. The ==<h1>=={=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. If you use command line tools, you can easily create a draft table of contents with the command: ```sh grep -h '^====* ' ZETTEL_ID.zettel ``` |
Changes to docs/manual/00001007031000.zettel.
1 2 3 4 5 | id: 00001007031000 title: Zettelmarkup: Tables role: manual tags: #manual #zettelmarkup #zettelstore syntax: zmk | | | | | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 | id: 00001007031000 title: Zettelmarkup: Tables role: manual tags: #manual #zettelmarkup #zettelstore syntax: zmk 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//. Therefore, a table can be seen as a sequence of table rows. 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. A vertical bar character as the last character of a line will not result in a table cell. It will be ignored. |
︙ | ︙ | |||
28 29 30 31 32 33 34 | :::example | a1 | a2 | a3| | b1 | b2 | b3 | c1 | c2 ::: === Header row | | | 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 | :::example | a1 | a2 | a3| | 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. For example: ```zmk | a1 | a2 |= a3| | b1 | b2 | b3 | c1 | c2 ``` |
︙ | ︙ |
Changes to docs/manual/00001007040000.zettel.
1 2 3 4 5 | id: 00001007040000 title: Zettelmarkup: Inline-Structured Elements role: manual tags: #manual #zettelmarkup #zettelstore syntax: zmk | | | 1 2 3 4 5 6 7 8 9 10 11 12 13 | id: 00001007040000 title: Zettelmarkup: Inline-Structured Elements role: manual tags: #manual #zettelmarkup #zettelstore syntax: zmk 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. ; Text formatting |
︙ | ︙ | |||
31 32 33 34 35 36 37 | ==== Comments A comment begins with two consecutive percent sign characters (""''%''"", ''U+0025''). 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''). | | | | | 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 | ==== Comments A comment begins with two consecutive percent sign characters (""''%''"", ''U+0025''). 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//. * 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//. 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. Regardless which method you use, an entity always begins with an ampersand character (""''&''"", ''U+0026'') and ends with a semicolon character (""'';''"", ''U+003B''). If you know the HTML name of the character you want to enter, put it between these two character. Example: ``&`` is rendered as ::&::{=example}. If you want to enter its numeric code point, a number sign character must follow the ampersand character, followed by digits to base 10. Example: ``&`` is rendered in HTML as ::&::{=example}. 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. 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 ``…``. |
Changes to docs/manual/00001007040100.zettel.
1 2 | id: 00001007040100 title: Zettelmarkup: Text Formatting | < | < < | < | > | > | | > | > | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 | id: 00001007040100 title: Zettelmarkup: Text Formatting tags: #manual #zettelmarkup #zettelstore syntax: zmk 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. Text formatting can be nested, up to a reasonable limit. The following characters begin a text formatting: * 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}. ** 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. ** Example: ``H,,2,,O`` is rendered in HTML as: ::H,,2,,O::{=example}. * The less-than sign character (""''<''"", ''U+003C'') marks an inline quotation. |
︙ | ︙ |
Changes to docs/manual/00001007040322.zettel.
1 2 3 4 5 | id: 00001007040322 title: Zettelmarkup: Image Embedding role: manual tags: #manual #zettelmarkup #zettelstore syntax: zmk | | | | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 | id: 00001007040322 title: Zettelmarkup: Image Embedding role: manual tags: #manual #zettelmarkup #zettelstore syntax: zmk 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//. * 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. They must follow the last right curly bracket character immediately. One prominent example is to specify an explicit title attribute that is shown on certain web browsers when the zettel is rendered in HTML: Examples: * [!spin] ``{{Spinning Emoji|00000000040001}}{title=Emoji width=30}`` is rendered as ::{{Spinning Emoji|00000000040001}}{title=Emoji width=30}::{=example}. * The above image is also the placeholder for a non-existent zettel: ** ``{{00000000009999}}`` will be rendered as ::{{00000000009999}}::{=example}. |
Changes to docs/manual/00001007040350.zettel.
1 2 3 4 5 | id: 00001007040350 title: Zettelmarkup: Mark role: manual tags: #manual #zettelmarkup #zettelstore syntax: zmk | < | | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 | id: 00001007040350 title: Zettelmarkup: Mark role: manual tags: #manual #zettelmarkup #zettelstore syntax: zmk 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//.]. 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"". |
Changes to docs/manual/00001007050000.zettel.
1 2 3 4 5 | id: 00001007050000 title: Zettelmarkup: Attributes role: manual tags: #manual #zettelmarkup #zettelstore syntax: zmk | | | | < | | | | | | | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 | id: 00001007050000 title: Zettelmarkup: Attributes role: manual tags: #manual #zettelmarkup #zettelstore syntax: zmk 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]]. Typical use cases for attributes are to specify the (natural) [[language|00001007050100]] for a text region, to specify the [[programming language|00001007050200]] for highlighting program code, or to make white space visible in plain text. Attributes are specified within curly brackets ``{...}``. Of course, more than one attribute can be specified. Attributes are separated by a sequence of space characters or by a comma character. An attribute normally consists of an optional key and an optional value. The key is a sequence of letters, digits, a hyphen-minus (""''-''"", ''U+002D'', and a low line / underscore (""''_''"", ''U+005D''). It can be empty. The value is a sequence of any character, except space and the right curly bracket (""''}''"", ''U+007D''). 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 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}``. 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). * ``{key=value1 key=value2}`` is the same as ``{key"value1 value2"}``. * ``{key key}`` is the same as ``{key}``. * ``{.class1 .class2}`` is equivalent to ``{class="class1 class2"}``. 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. 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==. For some block elements, there is a syntax variant if you only want to specify a generic attribute. For all line-range blocks you can specify the generic attributes directly in the first line, after the three (or more) block characters. |
︙ | ︙ |
Changes to docs/manual/00001007060000.zettel.
1 2 3 4 5 | id: 00001007060000 title: Zettelmarkup: Summary of Formatting Characters role: manual tags: #manual #reference #zettelmarkup #zettelstore syntax: zmk | | | | | | | | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 | id: 00001007060000 title: Zettelmarkup: Summary of Formatting Characters role: manual tags: #manual #reference #zettelmarkup #zettelstore syntax: zmk modified: 20210728162830 The following table gives an overview about the use of all characters that begin a markup element. |= Character :|= Blocks <|= Inlines < | ''!'' | (free) | (free) | ''"'' | [[Verse block|00001007030700]] | [[Typographic quotation mark|00001007040100]] | ''#'' | [[Ordered list|00001007030200]] | [[Tag|00001007040000]] | ''$'' | (reserved) | (reserved) | ''%'' | [[Comment block|00001007030900]] | [[Comment|00001007040000]] | ''&'' | (free) | [[Entity|00001007040000]] | ''\''' | (free) | [[Monospace text|00001007040100]] | ''('' | (free) | (free) | '')'' | (free) | (free) | ''*'' | [[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 / 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]] | (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) | [[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) | [[Strike-through text|00001007040100]] |
Changes to docs/manual/00001010070400.zettel.
1 2 | id: 00001010070400 title: Authorization and read-only mode | < | | | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 | id: 00001010070400 title: Authorization and read-only mode tags: #authorization #configuration #manual #security #zettelstore syntax: zmk role: manual 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. ; Not read-only, with authorization : Zettelstore is accessed remotely. You need authentication to ensure that only valid users access your Zettelstore. ; With read-only, no authorization : Zettelstore present publicly its full content to everybody. ; With read-only, with authorization : Nobody is allowed to change the content of the Zettelstore, but only specific zettel should be presented to the public. |
Changes to docs/manual/00001010090100.zettel.
1 2 3 4 5 | id: 00001010090100 title: External server to encrypt message transport role: manual tags: #configuration #encryption #manual #security #zettelstore syntax: zmk | | | 1 2 3 4 5 6 7 8 9 10 11 12 13 | id: 00001010090100 title: External server to encrypt message transport role: manual tags: #configuration #encryption #manual #security #zettelstore syntax: zmk 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. In most cases, you need to deploy a ""public-key encryption"" process, where your side publish a public encryption key that only works with a corresponding private decryption key. Technically, this is not trivial. |
︙ | ︙ | |||
63 64 65 66 67 68 69 | } } ``` 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/"". | | | 63 64 65 66 67 68 69 70 | } } ``` 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 ignore the prefix while reading web requests and to give the correct URLs with the given prefix when sending a web response. |
Changes to docs/manual/00001012000000.zettel.
1 2 3 4 5 | id: 00001012000000 title: API role: manual tags: #api #manual #zettelstore syntax: zmk | | | 1 2 3 4 5 6 7 8 9 10 11 12 13 | id: 00001012000000 title: API role: manual tags: #api #manual #zettelstore syntax: zmk 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. === Background The API is HTTP-based and uses plain text and JSON as its main encoding format for exchanging messages between a Zettelstore and its client software. |
︙ | ︙ | |||
28 29 30 31 32 33 34 | ** [[Content search|00001012051840]] ** [[Sort the list of zettel metadata|00001012052000]] * [[List all tags|00001012052400]] * [[List all roles|00001012052600]] === Working with zettel * [[Create a new zettel|00001012053200]] | | < < < < | 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 | ** [[Content search|00001012051840]] ** [[Sort the list of zettel metadata|00001012052000]] * [[List all tags|00001012052400]] * [[List all roles|00001012052600]] === Working with zettel * [[Create a new zettel|00001012053200]] * [[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]] |
Changes to docs/manual/00001012051200.zettel.
1 2 3 4 5 | id: 00001012051200 title: API: List metadata of all zettel role: manual tags: #api #manual #zettelstore syntax: zmk | | | | < < < < < | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 | id: 00001012051200 title: API: List metadata of all zettel role: manual tags: #api #manual #zettelstore syntax: zmk 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 {"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 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. If you reformat the JSON output from the ''GET /j'' call, you'll see its structure better: ```json { "list": [ { "id": "00001012051200", "meta": { "title": "API: List for all zettel some data", "tags": "#api #manual #zettelstore", "syntax": "zmk", |
︙ | ︙ |
Changes to docs/manual/00001012051800.zettel.
1 2 3 4 5 | id: 00001012051800 title: API: Shape the list of zettel metadata role: manual tags: #api #manual #zettelstore syntax: zmk | | | | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 | id: 00001012051800 title: API: Shape the list of zettel metadata role: manual tags: #api #manual #zettelstore syntax: zmk modified: 20210905203719 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. |
Changes to docs/manual/00001012051810.zettel.
1 2 3 4 5 | id: 00001012051810 title: API: Select zettel based on their metadata role: manual tags: #api #manual #zettelstore syntax: zmk | | | | | | | | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 | id: 00001012051810 title: API: Select zettel based on their metadata role: manual tags: #api #manual #zettelstore syntax: zmk 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. 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' {"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''). 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' {"list":[{"id":"00010000000000","meta":{"back":"00001003000000 00001005090000","backward":"00001003000000 00001005090000","copyright":"(c) 2020-2021 by Detlef Stern <ds@zettelstore.de>","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 <ds@zettelstore.de>","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. 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. If you specify the query parameter ''_negate'', either with or without a value, the whole selection will be negated. Because of the precondition described above, ``curl 'http://127.0.0.1:23123/j?url=!com'`` and ``curl 'http://127.0.0.1:23123/j?url=com&_negate'`` may produce different lists. |
︙ | ︙ |
Changes to docs/manual/00001012051830.zettel.
1 2 3 4 5 | id: 00001012051830 title: API: Shape the list of zettel metadata by limiting its length role: manual tags: #api #manual #zettelstore syntax: zmk | | | | | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 | id: 00001012051830 title: API: Shape the list of zettel metadata by limiting its length role: manual tags: #api #manual #zettelstore syntax: zmk 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' {"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 00001012050200 API: Authenticate a client ``` === 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' {"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.
|
| < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < |
Changes to docs/manual/00001012053400.zettel.
1 | id: 00001012053400 | | | | | | > < | | | | | | > > | | > > > > > > > > > > > > > | | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 | id: 00001012053400 title: API: Retrieve metadata and content of an existing zettel role: manual tags: #api #manual #zettelstore syntax: zmk modified: 20210905204428 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/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 <ds@zettelstore.de>","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": { "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"", 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 and content of an existing zettel role: manual tags: #api #manual #zettelstore syntax: zmk ```` === HTTP Status codes ; ''200'' |
︙ | ︙ |
Changes to docs/manual/00001012053500.zettel.
1 2 3 4 5 | id: 00001012053500 title: API: Retrieve evaluated metadata and content of an existing zettel in various encodings role: manual tags: #api #manual #zettelstore syntax: zmk | | | 1 2 3 4 5 6 7 8 9 10 11 12 13 | 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: 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: ```sh # curl http://127.0.0.1:23123/v/00001012053500 |
︙ | ︙ | |||
31 32 33 34 35 36 37 | <meta name="zs-backward" content="00001012000000"> <meta name="zs-box-number" content="1"> <meta name="copyright" content="(c) 2020-2021 by Detlef Stern <ds@zettelstore.de>"> <meta name="zs-forward" content="00001010040100 00001012050200 00001012920000 00001012920800"> <meta name="zs-published" content="00001012053500"> </head> <body> | | | 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 | <meta name="zs-backward" content="00001012000000"> <meta name="zs-box-number" content="1"> <meta name="copyright" content="(c) 2020-2021 by Detlef Stern <ds@zettelstore.de>"> <meta name="zs-forward" content="00001010040100 00001012050200 00001012920000 00001012920800"> <meta name="zs-published" content="00001012053500"> </head> <body> <p>The <a href="/v/00001012920000?_enc=html">endpoint</a> to work with metadata and content of a specific zettel is <span style="font-family:monospace">/v/{ID}</span>, where <span style="font-family:monospace">{ID}</span> is a placeholder for the zettel identifier (14 digits).</p> ... ``` 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''. ```sh # curl 'http://127.0.0.1:23123/v/00001012053500?_enc=html&_part=meta' |
︙ | ︙ |
Changes to docs/manual/00001012053600.zettel.
1 2 3 4 5 | id: 00001012053600 title: API: Retrieve parsed metadata and content of an existing zettel in various encodings role: manual tags: #api #manual #zettelstore syntax: zmk | | | | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 | 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: 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//. 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 # curl http://127.0.0.1:23123/p/00001012053600 {"meta":{"title":[{"t":"Text","s":"Retrieve"},{"t":"Space"},{"t":"Text","s":"parsed"},{"t":"Space"},{"t":"Text","s":"metadata"},{"t":"Space"},{"t":"Text","s":"and"},{"t":"Space"},{"t":"Text","s":"content"},{"t":"Space"},{"t":"Text","s":"of"},{"t":"Space"},{"t":"Text","s":"an"},{"t":"Space"},{"t":"Text","s":"existing"},{"t":"Space"},{"t":"Text","s":"zettel"},{"t":"Space"},{"t":"Text","s":"in"},{"t":"Space"},{"t":"Text","s":"various"},{"t":"Space"},{"t":"Text","s":"encodings"}],"role":"manual","tags":["#api", ... |
︙ | ︙ |
Changes to docs/manual/00001012053800.zettel.
1 2 3 4 5 | id: 00001012053800 title: API: Retrieve context of an existing zettel role: manual tags: #api #manual #zettelstore syntax: zmk | | > | > | | | | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 | id: 00001012053800 title: API: Retrieve context of an existing zettel role: manual tags: #api #manual #zettelstore syntax: zmk 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//: * 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. You should limit the depth by using the parameter ''depth''. Its default value is ""5"". 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. 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' {"id": "00001012053800","meta": {...},"list": [{"id": "00001012921000","meta": {...}},{"id": "00001012920800","meta": {...}},{"id": "00010000000000","meta": {...}}]} ```` |
︙ | ︙ |
Changes to docs/manual/00001012054400.zettel.
1 2 3 4 5 | id: 00001012054400 title: API: Rename a zettel role: manual tags: #api #manual #zettelstore syntax: zmk | | | | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 | id: 00001012054400 title: API: Rename a zettel role: manual tags: #api #manual #zettelstore syntax: zmk 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. As a consequence, you cannot rename a zettel when its identifier is used in a read-only box. This applies to all predefined zettel, for example. The [[endpoint|00001012920000]] to rename a zettel is ''/j/{ID}'', where ''{ID}'' is a placeholder for the zettel identifier (14 digits). You must send a HTTP MOVE request to this endpoint, and you must specify the new zettel identifier as an URL, placed under the HTTP request header key ''Destination''. ``` # curl -X MOVE -H "Destination: 10000000000001" http://127.0.0.1:23123/j/00001000000000 ``` 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|00001012053400]] of the freshly renamed zettel. [!plain]Alternatively, you can also use the [[endpoint|00001012920000]] ''/z/{ID}''. Both endpoints behave identical. === HTTP Status codes ; ''204'' : Rename was successful, there is no body in the response. |
︙ | ︙ |
Deleted docs/manual/00001012070500.zettel.
|
| < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < |
Changes to docs/manual/00001012920000.zettel.
1 2 3 4 5 | id: 00001012920000 title: Endpoints used by the API role: manual tags: #api #manual #reference #zettelstore syntax: zmk | | | < | | | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 | id: 00001012920000 title: Endpoints used by the API role: manual tags: #api #manual #reference #zettelstore syntax: zmk 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'' : is a single letter that specifies the ressource type, ; ''ZETTEL-ID'' : is an optional 14 digits string that uniquely [[identify a zettel|00001006050000]]. 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|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 | ''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'' | | 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|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.
|
| < < < < < < < < < < < < < < < < < |
Changes to domain/content.go.
︙ | ︙ | |||
8 9 10 11 12 13 14 | // under this license. //----------------------------------------------------------------------------- // Package domain provides domain specific types, constants, and functions. package domain import ( | < | | > | | | < < < < < < < < < < < | | | | | | | | | | | | | | | | 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 | // under this license. //----------------------------------------------------------------------------- // Package domain provides domain specific types, constants, and functions. package domain import ( "encoding/base64" "errors" "io" "unicode/utf8" "zettelstore.de/z/strfun" ) // Content is just the content of a zettel. type Content struct { data string isBinary bool } // NewContent creates a new content from a string. func NewContent(s string) Content { return Content{data: s, isBinary: calcIsBinary(s)} } // Set content to new string value. 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 io.WriteString(w, zc.data) } // AsString returns the content itself is a string. func (zc *Content) AsString() string { return zc.data } // AsBytes returns the content itself is a byte slice. 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 } // TrimSpace remove some space character in content, if it is not binary content. func (zc *Content) TrimSpace() { if zc.isBinary { return } zc.data = strfun.TrimSpaceRight(zc.data) } // Encode content for future transmission. func (zc *Content) Encode() (data, encoding string) { if !zc.isBinary { return zc.data, "" } 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 = data case "base64": decoded, err := base64.StdEncoding.DecodeString(data) if err != nil { return err } zc.data = string(decoded) default: return errors.New("unknown encoding " + encoding) } zc.isBinary = calcIsBinary(zc.data) return nil } func calcIsBinary(s string) bool { if !utf8.ValidString(s) { return true } l := len(s) for i := 0; i < l; i++ { if s[i] == 0 { return true } } return false } |
Changes to domain/content_test.go.
︙ | ︙ | |||
24 25 26 27 28 29 30 | }{ {"abc", false}, {"äöü", false}, {"", false}, {string([]byte{0}), true}, } for i, tc := range td { | | | 24 25 26 27 28 29 30 31 32 33 34 35 36 37 | }{ {"abc", false}, {"äöü", false}, {"", false}, {string([]byte{0}), true}, } for i, tc := range td { content := domain.NewContent(tc.s) got := content.IsBinary() if got != tc.exp { t.Errorf("TC=%d: expected %v, got %v", i, tc.exp, got) } } } |
Changes to domain/id/id.go.
︙ | ︙ | |||
11 12 13 14 15 16 17 | // Package id provides domain specific types, constants, and functions about // zettel identifier. package id import ( "strconv" "time" | < < > > > | | | < | > | > > > > | | > > | | | | | | | | | | | | | > > > > > > > > > > | > > > | | 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 | // Package id provides domain specific types, constants, and functions about // zettel identifier. package id import ( "strconv" "time" ) // 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. // 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. 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 // and returns its integer value. func ParseUint(s string) (uint64, error) { |
︙ | ︙ | |||
77 78 79 80 81 82 83 | res, err := ParseUint(s) if err != nil { return Invalid, err } return Zid(res), nil } | < < < < < < < < < < < | | | 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 | res, err := ParseUint(s) if err != nil { return Invalid, err } return Zid(res), nil } const digits = "0123456789" // String converts the zettel identification to a string of 14 digits. // Only defined for valid ids. func (zid Zid) String() string { return string(zid.Bytes()) } // Bytes converts the zettel identification to a byte slice of 14 digits. // Only defined for valid ids. func (zid Zid) Bytes() []byte { result := make([]byte, 14) for i := 13; i >= 0; i-- { 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. func (zid Zid) IsValid() bool { return 0 < zid && zid <= maxZid } |
︙ | ︙ |
Changes to domain/id/id_test.go.
︙ | ︙ | |||
43 44 45 46 47 48 49 | zid, err := id.Parse(sid) if err != nil { 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( | | | 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 | zid, err := id.Parse(sid) if err != nil { 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, sid, zid, s) } } invalidIDs := []string{ "", "0", "a", "00000000000000", "0000000000000a", |
︙ | ︙ |
Changes to domain/id/slice.go.
︙ | ︙ | |||
9 10 11 12 13 14 15 | //----------------------------------------------------------------------------- // Package id provides domain specific types, constants, and functions about // zettel identifier. package id import ( | < > | 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 | //----------------------------------------------------------------------------- // Package id provides domain specific types, constants, and functions about // zettel identifier. package id import ( "sort" "strings" ) // Slice is a sequence of zettel identifier. A special case is a sorted slice. type Slice []Zid func (zs Slice) Len() int { return len(zs) } func (zs Slice) Less(i, j int) bool { return zs[i] < zs[j] } |
︙ | ︙ | |||
54 55 56 57 58 59 60 | return true } func (zs Slice) String() string { if len(zs) == 0 { return "" } | | | | | | 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 | return true } func (zs Slice) String() string { if len(zs) == 0 { return "" } var sb strings.Builder for i, zid := range zs { if i > 0 { sb.WriteByte(' ') } sb.WriteString(zid.String()) } return sb.String() } |
Changes to domain/meta/meta.go.
︙ | ︙ | |||
8 9 10 11 12 13 14 | // under this license. //----------------------------------------------------------------------------- // Package meta provides the domain specific type 'meta'. package meta import ( | < < < < | 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 | // under this license. //----------------------------------------------------------------------------- // Package meta provides the domain specific type 'meta'. package meta import ( "regexp" "sort" "strings" "zettelstore.de/z/domain/id" "zettelstore.de/z/input" ) type keyUsage int const ( |
︙ | ︙ | |||
45 46 47 48 49 50 51 | func (kd *DescriptionKey) IsComputed() bool { return kd.usage >= usageComputed } // IsProperty returns true, if metadata is a computed property. func (kd *DescriptionKey) IsProperty() bool { return kd.usage >= usageProperty } var registeredKeys = make(map[string]*DescriptionKey) | | > | 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 | func (kd *DescriptionKey) IsComputed() bool { return kd.usage >= usageComputed } // 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) string { if _, ok := registeredKeys[name]; ok { panic("Key '" + name + "' already defined") } if inverse != "" { if t != TypeID && t != TypeIDSet { panic("Inversable key '" + name + "' is not identifier type, but " + t.String()) } inv, ok := registeredKeys[inverse] if !ok { panic("Inverse Key '" + inverse + "' not found") } if !inv.IsComputed() { panic("Inverse Key '" + inverse + "' is not computed.") } 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 { return kd.IsComputed() } |
︙ | ︙ | |||
106 107 108 109 110 111 112 | for _, n := range names { result = append(result, registeredKeys[n]) } return result } // Supported keys. | | | | | | | | | | | < | | | | < < | | < | | | | | | | | | < < | | | | | | | > > | | | | | | | | < > > > > > > > > > > > > > > > > > > > > > > > > | 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 | for _, n := range names { result = append(result, registeredKeys[n]) } return result } // Supported keys. 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 YamlSep bool } |
︙ | ︙ | |||
209 210 211 212 213 214 215 | // Pair is one key-value-pair of a Zettel meta. type Pair struct { Key string Value string } | | | | | 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 | // Pair is one key-value-pair of a Zettel meta. type Pair struct { Key string Value string } 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 { firstKeySet[k] = true } } // Set stores the given string value under the given key. func (m *Meta) Set(key, value string) { if key != KeyID { m.pairs[key] = trimValue(value) } } func trimValue(value string) string { return strings.TrimFunc(value, input.IsSpace) } // 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 == KeyID { return m.Zid.String(), true } value, ok := m.pairs[key] return value, ok } // GetDefault retrieves the string value of the given key. If no value was |
︙ | ︙ | |||
288 289 290 291 292 293 294 | result = append(result, Pair{k, m.pairs[k]}) } return result } // Delete removes a key from the data. func (m *Meta) Delete(key string) { | | | 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 | result = append(result, Pair{k, m.pairs[k]}) } return result } // Delete removes a key from the data. func (m *Meta) Delete(key string) { if key != KeyID { delete(m.pairs, key) } } // Equal compares to metas for equality. func (m *Meta) Equal(o *Meta, allowComputed bool) bool { if m == nil && o == nil { |
︙ | ︙ | |||
324 325 326 327 328 329 330 | if allowComputed || !IsComputed(key) { if valO, ok := other.pairs[key]; !ok || val != valO { return false } } return true } | < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < | 340 341 342 343 344 345 346 | if allowComputed || !IsComputed(key) { if valO, ok := other.pairs[key]; !ok || val != valO { return false } } return true } |
Changes to domain/meta/meta_test.go.
︙ | ︙ | |||
11 12 13 14 15 16 17 | // Package meta provides the domain specific type 'meta'. package meta import ( "strings" "testing" | < | 11 12 13 14 15 16 17 18 19 20 21 22 23 24 | // Package meta provides the domain specific type 'meta'. package meta import ( "strings" "testing" "zettelstore.de/z/domain/id" ) const testID = id.Zid(98765432101234) func TestKeyIsValid(t *testing.T) { t.Parallel() |
︙ | ︙ | |||
36 37 38 39 40 41 42 | } } } func TestTitleHeader(t *testing.T) { t.Parallel() m := New(testID) | | | | | | | | | | | | 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 | } } } func TestTitleHeader(t *testing.T) { t.Parallel() m := New(testID) if got, ok := m.Get(KeyTitle); ok || got != "" { t.Errorf("Title is not empty, but %q", 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, KeyTitle, " "+st+" ") if got, ok := m.Get(KeyTitle); !ok || got != st { t.Errorf("Title is not %q, but %q", st, got) } addToMeta(m, KeyTitle, " "+st+"\t") const exp = st + " " + st 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, 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) { t.Helper() got, _ := m.GetList(key) |
︙ | ︙ | |||
83 84 85 86 87 88 89 | t.Errorf("Extra tags: %q", got[len(exp):]) } } func TestTagsHeader(t *testing.T) { t.Parallel() m := New(testID) | | | | | | | | | | | | | | | | | | 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 | t.Errorf("Extra tags: %q", got[len(exp):]) } } func TestTagsHeader(t *testing.T) { t.Parallel() m := New(testID) 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(KeySyntax); ok || got != "" { t.Errorf("Syntax is not %q, but %q", "", got) } addToMeta(m, KeySyntax, " ") if got, _ := m.Get(KeySyntax); got != "" { t.Errorf("Syntax is not %q, but %q", "", got) } addToMeta(m, KeySyntax, "MarkDown") const exp = "markdown" if got, ok := m.Get(KeySyntax); !ok || got != exp { t.Errorf("Syntax is not %q, but %q", exp, 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) { t.Helper() got := make(map[string]string, len(gotP)) |
︙ | ︙ | |||
187 188 189 190 191 192 193 | allowComputed bool exp bool }{ {nil, nil, true, true}, {nil, nil, false, true}, {[]string{"a", "a"}, nil, false, false}, {[]string{"a", "a"}, nil, true, false}, | | | | | | 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 | allowComputed bool exp bool }{ {nil, nil, true, true}, {nil, nil, false, true}, {[]string{"a", "a"}, nil, false, false}, {[]string{"a", "a"}, nil, true, false}, {[]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) if tc.exp != got { t.Errorf("%d: %v =?= %v: expected=%v, but got=%v", i, tc.pairs1, tc.pairs2, tc.exp, got) |
︙ | ︙ | |||
233 234 235 236 237 238 239 | func pairs2meta(pairs []string) *Meta { m := New(testID) for i := 0; i < len(pairs); i = i + 2 { m.Set(pairs[i], pairs[i+1]) } return m } | < < < < < < < < < < < < < < < < < < < < < < < | 232 233 234 235 236 237 238 | func pairs2meta(pairs []string) *Meta { m := New(testID) for i := 0; i < len(pairs); i = i + 2 { m.Set(pairs[i], pairs[i+1]) } return m } |
Changes to domain/meta/parse.go.
︙ | ︙ | |||
11 12 13 14 15 16 17 | // Package meta provides the domain specific type 'meta'. package meta import ( "sort" "strings" | < | 11 12 13 14 15 16 17 18 19 20 21 22 23 24 | // Package meta provides the domain specific type 'meta'. package meta import ( "sort" "strings" "zettelstore.de/z/domain/id" "zettelstore.de/z/input" ) // NewFromInput parses the meta data of a zettel. func NewFromInput(zid id.Zid, inp *input.Input) *Meta { if inp.Ch == '-' && inp.PeekN(0) == '-' && inp.PeekN(1) == '-' { |
︙ | ︙ | |||
61 62 63 64 65 66 67 | inp.Next() } key := inp.Src[pos:inp.Pos] skipSpace(inp) if inp.Ch == ':' { inp.Next() } | | | | | | 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 | inp.Next() } key := inp.Src[pos:inp.Pos] skipSpace(inp) if inp.Ch == ':' { inp.Next() } var val string for { skipSpace(inp) pos = inp.Pos skipToEOL(inp) val += inp.Src[pos:inp.Pos] inp.EatEOL() if !input.IsSpace(inp.Ch) { break } val += " " } addToMeta(m, key, val) } func skipSpace(inp *input.Input) { for input.IsSpace(inp.Ch) { inp.Next() } } |
︙ | ︙ | |||
148 149 150 151 152 153 154 | func addToMeta(m *Meta, key, val string) { v := trimValue(val) key = strings.ToLower(key) if !KeyIsValid(key) { return } switch key { | | | 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 | func addToMeta(m *Meta, key, val string) { v := trimValue(val) key = strings.ToLower(key) if !KeyIsValid(key) { return } switch key { case "", KeyID: // Empty key and 'id' key will be ignored return } switch Type(key) { case TypeString, TypeZettelmarkup: if v != "" { |
︙ | ︙ |
Changes to domain/meta/parse_test.go.
︙ | ︙ | |||
10 11 12 13 14 15 16 | // Package meta_test provides tests for the domain specific type 'meta'. package meta_test import ( "testing" | < | | | | | | | | | | | | | | | | | | 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 | // Package meta_test provides tests for the domain specific type 'meta'. package meta_test import ( "testing" "zettelstore.de/z/domain/meta" "zettelstore.de/z/input" ) func parseMetaStr(src string) *meta.Meta { return meta.NewFromInput(testID, input.NewInput(src)) } func TestEmpty(t *testing.T) { t.Parallel() m := parseMetaStr("") if got, ok := m.Get(meta.KeySyntax); ok || got != "" { t.Errorf("Syntax is not %q, but %q", "", got) } 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 }{ {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(meta.KeyTitle); !ok || got != tc.e { t.Log(m) t.Errorf("TC=%d: expected %q, got %q", i, tc.e, got) } } 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) { t.Parallel() testcases := []struct { |
︙ | ︙ | |||
84 85 86 87 88 89 90 | meta := parseMetaStr(tc.input) if got := meta.Pairs(true); !equalPairs(tc.exp, got) { t.Errorf("TC=%d: expected=%v, got=%v", i, tc.exp, got) } } // Test, whether input position is correct. | | | 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 | meta := parseMetaStr(tc.input) if got := meta.Pairs(true); !equalPairs(tc.exp, got) { t.Errorf("TC=%d: expected=%v, got=%v", i, tc.exp, got) } } // Test, whether input position is correct. 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) } expCh := 'X' if gotCh := inp.Ch; gotCh != expCh { |
︙ | ︙ | |||
123 124 125 126 127 128 129 | {"12345678901234", "12345678901234"}, {"123 12345678901234", "12345678901234"}, {"12345678901234 123", "12345678901234"}, {"01234567890123 123 12345678901234", "01234567890123 12345678901234"}, {"12345678901234 01234567890123", "01234567890123 12345678901234"}, } for i, tc := range testdata { | | | | 122 123 124 125 126 127 128 129 130 131 132 133 134 | {"12345678901234", "12345678901234"}, {"123 12345678901234", "12345678901234"}, {"12345678901234 123", "12345678901234"}, {"01234567890123 123 12345678901234", "01234567890123 12345678901234"}, {"12345678901234 01234567890123", "01234567890123 12345678901234"}, } for i, tc := range testdata { 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) } } } |
Changes to domain/meta/type.go.
︙ | ︙ | |||
12 13 14 15 16 17 18 | package meta import ( "strconv" "strings" "sync" "time" | < < | 12 13 14 15 16 17 18 19 20 21 22 23 24 25 | package meta import ( "strconv" "strings" "sync" "time" ) // DescriptionType is a description of a specific key type. type DescriptionType struct { Name string IsSet bool } |
︙ | ︙ | |||
95 96 97 98 99 100 101 | } } return TypeEmpty } // SetList stores the given string list value under the given key. func (m *Meta) SetList(key string, values []string) { | | | 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 | } } return TypeEmpty } // SetList stores the given string list value under the given key. func (m *Meta) SetList(key string, values []string) { if key != KeyID { for i, val := range values { values[i] = trimValue(val) } m.pairs[key] = strings.Join(values, " ") } } |
︙ | ︙ |
Changes to domain/meta/type_test.go.
︙ | ︙ | |||
30 31 32 33 34 35 36 | } if len(val) != 14 { 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) } | | | 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 | } if len(val) != 14 { 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 { t.Errorf("Unable to get time from value %q", val) } } func TestGetTime(t *testing.T) { t.Parallel() testCases := []struct { |
︙ | ︙ |
Changes to domain/meta/values.go.
1 2 3 4 5 6 7 8 9 10 11 12 13 | //----------------------------------------------------------------------------- // 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 meta provides the domain specific type 'meta'. package meta | < < | | | | | | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 | //----------------------------------------------------------------------------- // 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 meta provides the domain specific type 'meta'. package meta // Visibility enumerates the variations of the 'visibility' meta key. type Visibility int // Supported values for visibility. const ( _ Visibility = iota VisibilityUnknown VisibilityPublic VisibilityCreator VisibilityLogin VisibilityOwner VisibilityExpert ) var visMap = map[string]Visibility{ 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 { return vis } |
︙ | ︙ | |||
53 54 55 56 57 58 59 | UserRoleCreator UserRoleReader UserRoleWriter UserRoleOwner ) var urMap = map[string]UserRole{ | | | | | | 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 | UserRoleCreator UserRoleReader UserRoleWriter UserRoleOwner ) var urMap = map[string]UserRole{ 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 } |
Changes to domain/meta/write_test.go.
︙ | ︙ | |||
8 9 10 11 12 13 14 | // under this license. //----------------------------------------------------------------------------- // Package meta_test provides tests for the domain specific type 'meta'. package meta_test import ( | < < | | | | | | | 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 | // under this license. //----------------------------------------------------------------------------- // Package meta_test provides tests for the domain specific type 'meta'. package meta_test import ( "strings" "testing" "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(meta.KeyTitle, title) } if tags != nil { m.Set(meta.KeyTags, strings.Join(tags, " ")) } if syntax != "" { m.Set(meta.KeySyntax, syntax) } return m } func assertWriteMeta(t *testing.T, m *meta.Meta, expected string) { t.Helper() 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) { t.Parallel() assertWriteMeta(t, newMeta("", nil, ""), "") |
︙ | ︙ |
Changes to domain/zettel.go.
︙ | ︙ | |||
19 20 21 22 23 24 25 | type Zettel struct { Meta *meta.Meta // Some additional meta-data. Content Content // The content of the zettel itself. } // Equal compares two zettel for equality. func (z Zettel) Equal(o Zettel, allowComputed bool) bool { | | | 19 20 21 22 23 24 25 26 27 | type Zettel struct { Meta *meta.Meta // Some additional meta-data. 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 == o.Content } |
Changes to encoder/djsonenc/djsonenc.go.
︙ | ︙ | |||
13 14 15 16 17 18 19 | import ( "fmt" "io" "sort" "strconv" | | | 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 | import ( "fmt" "io" "sort" "strconv" "zettelstore.de/z/api" "zettelstore.de/z/ast" "zettelstore.de/z/domain/meta" "zettelstore.de/z/encoder" "zettelstore.de/z/strfun" ) func init() { |
︙ | ︙ | |||
382 383 384 385 386 387 388 | v.b.WriteByte('"') v.b.WriteString(fragment) v.b.WriteByte('"') } } var mapFormatKind = map[ast.FormatKind]string{ | | | > | | > | > | | | | | | | | 382 383 384 385 386 387 388 389 390 391 392 393 394 395 396 397 398 399 400 401 402 403 404 405 406 407 408 409 410 | v.b.WriteByte('"') v.b.WriteString(fragment) v.b.WriteByte('"') } } var mapFormatKind = map[ast.FormatKind]string{ 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", ast.LiteralOutput: "Output", ast.LiteralComment: "Comment", |
︙ | ︙ |
Changes to encoder/encoder.go.
︙ | ︙ | |||
13 14 15 16 17 18 19 | package encoder import ( "errors" "fmt" "io" | | | 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 | package encoder import ( "errors" "fmt" "io" "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. type Encoder interface { WriteZettel(io.Writer, *ast.ZettelNode, EvalMetaFunc) (int, error) |
︙ | ︙ |
Deleted encoder/encoder_blob_test.go.
|
| < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < |
Deleted encoder/encoder_block_test.go.
|
| < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < |
Deleted encoder/encoder_inline_test.go.
|
| < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < |
Deleted encoder/encoder_test.go.
|
| < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < |
Changes to encoder/htmlenc/block.go.
︙ | ︙ | |||
29 30 31 32 33 34 35 | v.b.WriteString("<pre><code") v.visitAttributes(vn.Attrs) v.b.WriteByte('>') for _, line := range vn.Lines { v.writeHTMLEscaped(line) v.b.WriteByte('\n') } | | | | 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 | v.b.WriteString("<pre><code") v.visitAttributes(vn.Attrs) v.b.WriteByte('>') for _, line := range vn.Lines { v.writeHTMLEscaped(line) v.b.WriteByte('\n') } v.b.WriteString("</code></pre>\n") v.visibleSpace = oldVisible case ast.VerbatimComment: if vn.Attrs.HasDefault() { v.b.WriteString("<!--\n") for _, line := range vn.Lines { v.writeHTMLEscaped(line) v.b.WriteByte('\n') } v.b.WriteString("-->\n") } case ast.VerbatimHTML: for _, line := range vn.Lines { if !ignoreHTMLText(line) { v.b.WriteStrings(line, "\n") } |
︙ | ︙ | |||
116 117 118 119 120 121 122 | defer v.lang.pop() v.b.WriteStrings("<", code) v.visitAttributes(attrs) v.b.WriteString(">\n") ast.Walk(v, rn.Blocks) if rn.Inlines != nil { | | | | | | | 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 | defer v.lang.pop() v.b.WriteStrings("<", code) v.visitAttributes(attrs) v.b.WriteString(">\n") ast.Walk(v, rn.Blocks) if rn.Inlines != nil { v.b.WriteString("<cite>") ast.Walk(v, rn.Inlines) v.b.WriteString("</cite>\n") } v.b.WriteStrings("</", code, ">\n") v.inVerse = oldVerse } func (v *visitor) visitHeading(hn *ast.HeadingNode) { v.lang.push(hn.Attrs) defer v.lang.pop() lvl := hn.Level if lvl > 6 { lvl = 6 // HTML has H1..H6 } strLvl := strconv.Itoa(lvl) v.b.WriteStrings("<h", strLvl) v.visitAttributes(hn.Attrs) if _, ok := hn.Attrs.Get("id"); !ok { if fragment := hn.Fragment; fragment != "" { v.b.WriteStrings(" id=\"", fragment, "\"") } } v.b.WriteByte('>') ast.Walk(v, hn.Inlines) v.b.WriteStrings("</h", strLvl, ">\n") } var mapNestedListKind = map[ast.NestedListKind]string{ ast.NestedListOrdered: "ol", ast.NestedListUnordered: "ul", } |
︙ | ︙ | |||
174 175 176 177 178 179 180 | v.visitAttributes(ln.Attrs) v.b.WriteString(">\n") for _, item := range ln.Items { v.b.WriteString("<li>") v.writeItemSliceOrPara(item, compact) v.b.WriteString("</li>\n") } | | | 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 | v.visitAttributes(ln.Attrs) v.b.WriteString(">\n") for _, item := range ln.Items { v.b.WriteString("<li>") v.writeItemSliceOrPara(item, compact) v.b.WriteString("</li>\n") } v.b.WriteStrings("</", code, ">\n") } func (v *visitor) writeQuotationList(ln *ast.NestedListNode) { v.b.WriteString("<blockquote>\n") inPara := false for _, item := range ln.Items { if pn := getParaItem(item); pn != nil { |
︙ | ︙ | |||
247 248 249 250 251 252 253 | func (v *visitor) writeItemSliceOrPara(ins ast.ItemSlice, compact bool) { if compact && len(ins) == 1 { if para, ok := ins[0].(*ast.ParaNode); ok { ast.Walk(v, para.Inlines) return } } | < < < < | < < | 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 | func (v *visitor) writeItemSliceOrPara(ins ast.ItemSlice, compact bool) { if compact && len(ins) == 1 { if para, ok := ins[0].(*ast.ParaNode); ok { ast.Walk(v, para.Inlines) return } } ast.WalkItemSlice(v, ins) } func (v *visitor) writeDescriptionsSlice(ds ast.DescriptionSlice) { if len(ds) == 1 { if para, ok := ds[0].(*ast.ParaNode); ok { ast.Walk(v, para.Inlines) return |
︙ | ︙ | |||
279 280 281 282 283 284 285 | for _, b := range descr.Descriptions { v.b.WriteString("<dd>") v.writeDescriptionsSlice(b) v.b.WriteString("</dd>\n") } } | | | | | | | 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 | for _, b := range descr.Descriptions { v.b.WriteString("<dd>") v.writeDescriptionsSlice(b) v.b.WriteString("</dd>\n") } } v.b.WriteString("</dl>\n") } func (v *visitor) visitTable(tn *ast.TableNode) { v.b.WriteString("<table>\n") if len(tn.Header) > 0 { v.b.WriteString("<thead>\n") v.writeRow(tn.Header, "<th", "</th>") v.b.WriteString("</thead>\n") } if len(tn.Rows) > 0 { v.b.WriteString("<tbody>\n") for _, row := range tn.Rows { v.writeRow(row, "<td", "</td>") } v.b.WriteString("</tbody>\n") } v.b.WriteString("</table>\n") } var alignStyle = map[ast.Alignment]string{ ast.AlignDefault: ">", 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("<tr>") for _, cell := range row { v.b.WriteString(cellStart) if cell.Inlines.IsEmpty() { |
︙ | ︙ | |||
328 329 330 331 332 333 334 | func (v *visitor) visitBLOB(bn *ast.BLOBNode) { switch bn.Syntax { case "gif", "jpeg", "png": v.b.WriteStrings("<img src=\"data:image/", bn.Syntax, ";base64,") v.b.WriteBase64(bn.Blob) v.b.WriteString("\" title=\"") v.writeQuotedEscaped(bn.Title) | | | | | 322 323 324 325 326 327 328 329 330 331 332 333 334 335 336 337 | func (v *visitor) visitBLOB(bn *ast.BLOBNode) { switch bn.Syntax { case "gif", "jpeg", "png": v.b.WriteStrings("<img src=\"data:image/", bn.Syntax, ";base64,") v.b.WriteBase64(bn.Blob) v.b.WriteString("\" title=\"") v.writeQuotedEscaped(bn.Title) v.b.WriteString("\">\n") default: v.b.WriteStrings("<p class=\"error\">Unable to display BLOB with syntax '", bn.Syntax, "'.</p>\n") } } func (v *visitor) writeEndPara() { v.b.WriteString("</p>\n") } |
Changes to encoder/htmlenc/htmlenc.go.
︙ | ︙ | |||
10 11 12 13 14 15 16 | // Package htmlenc encodes the abstract syntax tree into HTML5. package htmlenc import ( "io" | | | 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 | // Package htmlenc encodes the abstract syntax tree into HTML5. package htmlenc import ( "io" "zettelstore.de/z/api" "zettelstore.de/z/ast" "zettelstore.de/z/domain/meta" "zettelstore.de/z/encoder" ) func init() { encoder.Register(api.EncoderHTML, encoder.Info{ |
︙ | ︙ | |||
38 39 40 41 42 43 44 | } if env := he.env; env != nil && env.Lang == "" { v.b.WriteStrings("<html>\n<head>") } else { v.b.WriteStrings("<html lang=\"", env.Lang, "\">") } v.b.WriteString("\n<head>\n<meta charset=\"utf-8\">\n") | | | 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 | } if env := he.env; env != nil && env.Lang == "" { v.b.WriteStrings("<html>\n<head>") } else { v.b.WriteStrings("<html lang=\"", env.Lang, "\">") } v.b.WriteString("\n<head>\n<meta charset=\"utf-8\">\n") plainTitle, hasTitle := zn.InhMeta.Get(meta.KeyTitle) if hasTitle { v.b.WriteStrings("<title>", v.evalValue(plainTitle, evalMeta), "</title>") } v.acceptMeta(zn.InhMeta, evalMeta) v.b.WriteString("\n</head>\n<body>\n") if hasTitle { if ilnTitle := evalMeta(plainTitle); ilnTitle != nil { |
︙ | ︙ | |||
63 64 65 66 67 68 69 | } // 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 | | | | 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 | } // 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(meta.KeyTitle); ok { v.b.WriteStrings("<meta name=\"zs-", meta.KeyTitle, "\" content=\"") v.writeQuotedEscaped(v.evalValue(title, evalMeta)) v.b.WriteString("\">") } // Write other metadata v.acceptMeta(m, evalMeta) length, err := v.b.Flush() |
︙ | ︙ |
Changes to encoder/htmlenc/inline.go.
︙ | ︙ | |||
12 13 14 15 16 17 18 | package htmlenc import ( "fmt" "strconv" "strings" | | | | 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 | package htmlenc import ( "fmt" "strconv" "strings" "zettelstore.de/z/ast" "zettelstore.de/z/domain/meta" ) func (v *visitor) visitBreak(bn *ast.BreakNode) { if bn.Hard { if v.env.IsXHTML() { v.b.WriteString("<br />\n") } else { |
︙ | ︙ | |||
155 156 157 158 159 160 161 | func (v *visitor) visitFormat(fn *ast.FormatNode) { v.lang.push(fn.Attrs) defer v.lang.pop() var code string attrs := fn.Attrs.Clone() switch fn.Kind { | | | > > > > > > | > | 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 | func (v *visitor) visitFormat(fn *ast.FormatNode) { v.lang.push(fn.Attrs) defer v.lang.pop() var code string attrs := fn.Attrs.Clone() switch fn.Kind { 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 <span class="XXX"> case ast.FormatInsert: code = "ins" case ast.FormatStrike: code = "s" case ast.FormatDelete: code = "del" case ast.FormatSuper: code = "sup" case ast.FormatSub: code = "sub" case ast.FormatQuotation: code = "q" case ast.FormatSmall: code = "small" case ast.FormatSpan: v.writeSpan(fn.Inlines, processSpanAttributes(attrs)) return case ast.FormatMonospace: 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)) } v.b.WriteStrings("<", code) |
︙ | ︙ | |||
201 202 203 204 205 206 207 | v.b.WriteByte('>') ast.Walk(v, iln) v.b.WriteString("</span>") } var langQuotes = map[string][2]string{ | | | | | 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 | v.b.WriteByte('>') ast.Walk(v, iln) v.b.WriteString("</span>") } var langQuotes = map[string][2]string{ 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 { langSup := strings.Join(langFields, "-") quotes, ok := langQuotes[langSup] |
︙ | ︙ | |||
244 245 246 247 248 249 250 | case ast.LiteralProg: v.writeLiteral("<code", "</code>", ln.Attrs, ln.Text) case ast.LiteralKeyb: v.writeLiteral("<kbd", "</kbd>", ln.Attrs, ln.Text) case ast.LiteralOutput: v.writeLiteral("<samp", "</samp>", ln.Attrs, ln.Text) case ast.LiteralComment: | < < < | 251 252 253 254 255 256 257 258 259 260 261 262 263 264 | case ast.LiteralProg: v.writeLiteral("<code", "</code>", ln.Attrs, ln.Text) case ast.LiteralKeyb: v.writeLiteral("<kbd", "</kbd>", ln.Attrs, ln.Text) case ast.LiteralOutput: v.writeLiteral("<samp", "</samp>", ln.Attrs, ln.Text) case ast.LiteralComment: v.b.WriteString("<!-- ") v.writeHTMLEscaped(ln.Text) // writeCommentEscaped v.b.WriteString(" -->") case ast.LiteralHTML: if !ignoreHTMLText(ln.Text) { v.b.WriteString(ln.Text) } |
︙ | ︙ |
Changes to encoder/htmlenc/visitor.go.
︙ | ︙ | |||
8 9 10 11 12 13 14 | // under this license. //----------------------------------------------------------------------------- // Package htmlenc encodes the abstract syntax tree into HTML5. package htmlenc import ( | < | < < < < < < < < < < < < < < | | > | 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 | // under this license. //----------------------------------------------------------------------------- // Package htmlenc encodes the abstract syntax tree into HTML5. package htmlenc import ( "io" "sort" "strconv" "strings" "zettelstore.de/z/api" "zettelstore.de/z/ast" "zettelstore.de/z/domain/meta" "zettelstore.de/z/encoder" "zettelstore.de/z/strfun" ) // visitor writes the abstract syntax tree to an io.Writer. type visitor struct { env *encoder.Environment b encoder.BufWriter visibleSpace bool // Show space character in plain text inVerse bool // In verse block inInteractive bool // Rendered interactive HTML code lang langStack textEnc encoder.Encoder } func newVisitor(he *htmlEncoder, w io.Writer) *visitor { var lang string if he.env != nil { lang = he.env.Lang } return &visitor{ env: he.env, b: encoder.NewBufWriter(w), lang: newLangStack(lang), textEnc: encoder.Create(api.EncoderText, nil), } } func (v *visitor) Visit(node ast.Node) ast.Visitor { switch n := node.(type) { case *ast.ParaNode: v.b.WriteString("<p>") ast.Walk(v, n.Inlines) v.writeEndPara() case *ast.VerbatimNode: v.visitVerbatim(n) case *ast.RegionNode: v.visitRegion(n) case *ast.HeadingNode: v.visitHeading(n) case *ast.HRuleNode: v.b.WriteString("<hr") v.visitAttributes(n.Attrs) if v.env.IsXHTML() { v.b.WriteString(" />\n") } else { v.b.WriteString(">\n") } case *ast.NestedListNode: v.visitNestedList(n) case *ast.DescriptionListNode: v.visitDescriptionList(n) case *ast.TableNode: v.visitTable(n) 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("<span class=\"zettel-tag\">#") v.writeHTMLEscaped(n.Tag) v.b.WriteString("</span>") case *ast.SpaceNode: if v.inVerse || v.env.IsXHTML() { v.b.WriteString(n.Lexeme) } else { |
︙ | ︙ | |||
122 123 124 125 126 127 128 | default: return v } return nil } var mapMetaKey = map[string]string{ | | | | | | | | | | 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 | default: return v } return nil } var mapMetaKey = map[string]string{ meta.KeyCopyright: "copyright", meta.KeyLicense: "license", } func (v *visitor) acceptMeta(m *meta.Meta, evalMeta encoder.EvalMetaFunc) { ignore := v.setupIgnoreSet() 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] { continue } |
︙ | ︙ | |||
158 159 160 161 162 163 164 | } else { v.writeMeta("zs-", key, value) } } } func (v *visitor) evalValue(value string, evalMeta encoder.EvalMetaFunc) string { | | | | | 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 | } else { v.writeMeta("zs-", key, value) } } } func (v *visitor) evalValue(value string, evalMeta encoder.EvalMetaFunc) string { var sb strings.Builder _, err := v.textEnc.WriteInlines(&sb, evalMeta(value)) if err == nil { return sb.String() } return "" } func (v *visitor) setupIgnoreSet() map[string]bool { if v.env == nil || v.env.IgnoreMeta == nil { return make(map[string]bool) |
︙ | ︙ |
Changes to encoder/nativeenc/nativeenc.go.
︙ | ︙ | |||
13 14 15 16 17 18 19 | import ( "fmt" "io" "sort" "strconv" | | | 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 | import ( "fmt" "io" "sort" "strconv" "zettelstore.de/z/api" "zettelstore.de/z/ast" "zettelstore.de/z/domain/meta" "zettelstore.de/z/encoder" ) func init() { encoder.Register(api.EncoderNative, encoder.Info{ |
︙ | ︙ | |||
184 185 186 187 188 189 190 | var ( rawBackslash = []byte{'\\', '\\'} rawDoubleQuote = []byte{'\\', '"'} rawNewline = []byte{'\\', 'n'} ) func (v *visitor) acceptMeta(m *meta.Meta, evalMeta encoder.EvalMetaFunc) { | | | | | | 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 | var ( rawBackslash = []byte{'\\', '\\'} rawDoubleQuote = []byte{'\\', '"'} rawNewline = []byte{'\\', 'n'} ) func (v *visitor) acceptMeta(m *meta.Meta, evalMeta encoder.EvalMetaFunc) { 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") v.level++ for i, p := range pairs { |
︙ | ︙ | |||
477 478 479 480 481 482 483 | if fragment := mn.Fragment; fragment != "" { v.b.WriteString(" #") v.writeEscaped(fragment) } } var mapFormatKind = map[ast.FormatKind][]byte{ | | | > | > | | > | | | | | | | | 477 478 479 480 481 482 483 484 485 486 487 488 489 490 491 492 493 494 495 496 497 498 499 500 501 502 503 504 505 | if fragment := mn.Fragment; fragment != "" { v.b.WriteString(" #") v.writeEscaped(fragment) } } var mapFormatKind = map[ast.FormatKind][]byte{ 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"), ast.LiteralOutput: []byte("Output"), ast.LiteralComment: []byte("Comment"), |
︙ | ︙ |
Changes to encoder/textenc/textenc.go.
︙ | ︙ | |||
10 11 12 13 14 15 16 | // Package textenc encodes the abstract syntax tree into its text. package textenc import ( "io" | | | 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 | // Package textenc encodes the abstract syntax tree into its text. package textenc import ( "io" "zettelstore.de/z/api" "zettelstore.de/z/ast" "zettelstore.de/z/domain/meta" "zettelstore.de/z/encoder" ) func init() { encoder.Register(api.EncoderText, encoder.Info{ |
︙ | ︙ | |||
93 94 95 96 97 98 99 | ast.Walk(v, iln) length, err := v.b.Flush() return length, err } // visitor writes the abstract syntax tree to an io.Writer. type visitor struct { | | < < < < < < < < | 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 | ast.Walk(v, iln) length, err := v.b.Flush() return length, err } // visitor writes the abstract syntax tree to an io.Writer. type visitor struct { b encoder.BufWriter } func newVisitor(w io.Writer) *visitor { return &visitor{b: encoder.NewBufWriter(w)} } func (v *visitor) Visit(node ast.Node) ast.Visitor { switch n := node.(type) { case *ast.BlockListNode: v.visitBlockList(n) case *ast.VerbatimNode: v.visitVerbatim(n) return nil case *ast.RegionNode: v.visitBlockList(n.Blocks) if n.Inlines != nil { v.b.WriteByte('\n') |
︙ | ︙ | |||
153 154 155 156 157 158 159 | return nil case *ast.LinkNode: if !n.OnlyRef { ast.Walk(v, n.Inlines) } return nil case *ast.FootnoteNode: | < | < | | 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 | return nil case *ast.LinkNode: if !n.OnlyRef { ast.Walk(v, n.Inlines) } return nil case *ast.FootnoteNode: v.b.WriteByte(' ') return v // No 'return nil' to write text case *ast.LiteralNode: if n.Kind != ast.LiteralComment { v.b.WriteString(n.Text) } } return v } |
︙ | ︙ |
Changes to encoder/zmkenc/zmkenc.go.
︙ | ︙ | |||
12 13 14 15 16 17 18 | package zmkenc import ( "fmt" "io" "sort" | | | 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 | package zmkenc import ( "fmt" "io" "sort" "zettelstore.de/z/api" "zettelstore.de/z/ast" "zettelstore.de/z/domain/meta" "zettelstore.de/z/encoder" ) func init() { encoder.Register(api.EncoderZmk, encoder.Info{ |
︙ | ︙ | |||
83 84 85 86 87 88 89 | ast.Walk(v, iln) length, err := v.b.Flush() return length, err } // visitor writes the abstract syntax tree to an io.Writer. type visitor struct { | | | | < | | < | < | < < < | < < > | | 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 | ast.Walk(v, iln) length, err := v.b.Flush() return length, err } // visitor writes the abstract syntax tree to an io.Writer. type visitor struct { b encoder.BufWriter prefix []byte enc *zmkEncoder } func newVisitor(w io.Writer, enc *zmkEncoder) *visitor { return &visitor{ b: encoder.NewBufWriter(w), enc: enc, } } func (v *visitor) Visit(node ast.Node) ast.Visitor { switch n := node.(type) { 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, "'\n") case *ast.TextNode: v.visitText(n) case *ast.TagNode: v.b.WriteStrings("#", n.Tag) case *ast.SpaceNode: v.b.WriteString(n.Lexeme) case *ast.BreakNode: |
︙ | ︙ | |||
161 162 163 164 165 166 167 | v.visitLiteral(n) default: return v } return nil } | < < < < < < < < < < < | | < > | | > > > | < < < | | > | | | > < < < < | < | < < < > | > | < < < < < < < < < < < < | | | | | | | | | | | > | | < | | | | | | | > > > | 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 | v.visitLiteral(n) default: return v } return nil } func (v *visitor) visitVerbatim(vn *ast.VerbatimNode) { // TODO: scan cn.Lines to find embedded "`"s at beginning v.b.WriteString("```") v.visitAttributes(vn.Attrs) v.b.WriteByte('\n') for _, line := range vn.Lines { v.b.WriteStrings(line, "\n") } v.b.WriteString("```\n") } var mapRegionKind = map[ast.RegionKind]string{ ast.RegionSpan: ":::", ast.RegionQuote: "<<<", ast.RegionVerse: "\"\"\"", } func (v *visitor) visitRegion(rn *ast.RegionNode) { // Scan rn.Blocks for embedded regions to adjust length of regionCode kind, ok := mapRegionKind[rn.Kind] if !ok { panic(fmt.Sprintf("Unknown region kind %d", rn.Kind)) } v.b.WriteString(kind) v.visitAttributes(rn.Attrs) v.b.WriteByte('\n') ast.Walk(v, rn.Blocks) 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) { 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: '*', ast.NestedListQuote: '>', } func (v *visitor) visitNestedList(ln *ast.NestedListNode) { v.prefix = append(v.prefix, mapNestedListKind[ln.Kind]) for _, item := range ln.Items { v.b.Write(v.prefix) v.b.WriteByte(' ') for i, in := range item { if i > 0 { if _, ok := in.(*ast.ParaNode); ok { 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] v.b.WriteByte('\n') } func (v *visitor) visitDescriptionList(dn *ast.DescriptionListNode) { 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(": ") ast.WalkDescriptionSlice(v, b) v.b.WriteByte('\n') } } } var alignCode = map[ast.Alignment]string{ ast.AlignDefault: "", ast.AlignLeft: "<", ast.AlignCenter: ":", ast.AlignRight: ">", } func (v *visitor) visitTable(tn *ast.TableNode) { 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, "**": true, "__": true, |
︙ | ︙ | |||
398 399 400 401 402 403 404 | ast.Walk(v, cn.Inlines) } v.b.WriteByte(']') v.visitAttributes(cn.Attrs) } var mapFormatKind = map[ast.FormatKind][]byte{ | | | > | > | > | | | | | | | | > > > > > > > | < < < | 367 368 369 370 371 372 373 374 375 376 377 378 379 380 381 382 383 384 385 386 387 388 389 390 391 392 393 394 395 396 397 398 399 400 401 402 403 404 405 406 407 408 409 410 411 412 413 414 415 416 417 418 419 420 421 422 423 424 | ast.Walk(v, cn.Inlines) } v.b.WriteByte(']') v.visitAttributes(cn.Attrs) } var mapFormatKind = map[ast.FormatKind][]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(attrs) } func (v *visitor) visitLiteral(ln *ast.LiteralNode) { switch ln.Kind { case ast.LiteralProg: v.writeLiteral('`', ln.Attrs, ln.Text) case ast.LiteralKeyb: v.writeLiteral('+', ln.Attrs, ln.Text) case ast.LiteralOutput: v.writeLiteral('=', ln.Attrs, ln.Text) case ast.LiteralComment: v.b.WriteStrings("%% ", ln.Text) case ast.LiteralHTML: v.b.WriteString("``") v.writeEscaped(ln.Text, '`') v.b.WriteString("``{=html,.warning}") default: panic(fmt.Sprintf("Unknown literal kind %v", ln.Kind)) |
︙ | ︙ |
Changes to evaluator/evaluator.go.
︙ | ︙ | |||
13 14 15 16 17 18 19 | import ( "context" "errors" "fmt" "strconv" | < > | | | < | 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 | import ( "context" "errors" "fmt" "strconv" "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/parser" "zettelstore.de/z/parser/cleaner" ) // Environment contains values to control the evaluation. type Environment struct { 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) GetZettel(context.Context, id.Zid) (domain.Zettel, error) } |
︙ | ︙ | |||
63 64 65 66 67 68 69 | env = &emptyEnv } e := evaluator{ ctx: ctx, port: port, env: env, rtConfig: rtConfig, | | | < < < < < | | | | 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 | env = &emptyEnv } e := evaluator{ ctx: ctx, port: port, env: env, rtConfig: rtConfig, astMap: map[id.Zid]*ast.ZettelNode{}, embedMap: map[string]*ast.InlineListNode{}, embedCount: 0, marker: &ast.ZettelNode{}, } ast.Walk(&e, n) } type evaluator struct { ctx context.Context port Port env *Environment rtConfig config.Config astMap map[id.Zid]*ast.ZettelNode marker *ast.ZettelNode embedMap map[string]*ast.InlineListNode embedCount int } func (e *evaluator) Visit(node ast.Node) ast.Visitor { switch n := node.(type) { case *ast.InlineListNode: e.visitInlineList(n) default: return e } return nil } func (e *evaluator) visitInlineList(iln *ast.InlineListNode) { for i := 0; i < len(iln.List); i++ { in := iln.List[i] ast.Walk(e, in) switch n := in.(type) { case *ast.TagNode: iln.List[i] = e.visitTag(n) case *ast.LinkNode: iln.List[i] = e.evalLinkNode(n) case *ast.EmbedNode: 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] = in } } } } func replaceWithInlineNodes(ins []ast.InlineNode, i int, replaceIns []ast.InlineNode) []ast.InlineNode { if len(replaceIns) == 1 { |
︙ | ︙ | |||
184 185 186 187 188 189 190 | if gfr := e.env.GetFoundRef; gfr != nil { ln.Ref = gfr(zid, ref.URL.EscapedFragment()) } return ln } func (e *evaluator) evalEmbedNode(en *ast.EmbedNode) ast.InlineNode { | < < < < < < | < < > > < | > | > | < | < < < | | | < < < | | < < < | > > | < < < < < < < | | > > > | | < < | < < < | > | < | < > | < > | < | | > > | > > > > | | < < < < < < < < < < | | | 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 | if gfr := e.env.GetFoundRef; gfr != nil { ln.Ref = gfr(zid, ref.URL.EscapedFragment()) } return ln } func (e *evaluator) evalEmbedNode(en *ast.EmbedNode) ast.InlineNode { switch en.Material.(type) { case *ast.ReferenceMaterialNode: case *ast.BLOBMaterialNode: return en default: 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: return e.createErrorImage(en) case ast.RefStateZettel, ast.RefStateFound: case ast.RefStateSelf: 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)) } zid, err := id.Parse(ref.Ref.URL.Path) if err != nil { panic(err) } zettel, err := e.port.GetZettel(box.NoEnrichContext(e.ctx), zid) if err != nil { return e.createErrorImage(en) } syntax := e.getSyntax(zettel.Meta) if parser.IsImageFormat(syntax) { return e.embedImage(en, zettel, syntax) } if !parser.IsTextParser(syntax) { // Not embeddable. return createErrorText(en, "Not", "embeddable (syntax="+syntax+"):") } zn, ok := e.astMap[zid] if zn == e.marker { return createErrorText(en, "Recursive", "transclusion:") } if !ok { e.astMap[zid] = e.marker zn = e.evaluateEmbeddedZettel(zettel, syntax) e.astMap[zid] = zn } 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 createErrorText(en, "Nothing", "to", "transclude:") } } 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(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 { 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) fn := &ast.FormatNode{ Kind: ast.FormatMonospace, Inlines: text, } fn = &ast.FormatNode{ Kind: ast.FormatBold, Inlines: ast.CreateInlineListNode(fn), } fn.Attrs = fn.Attrs.AddClass("error") return fn } 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 { if fragment == "" { return firstFirstTopLevelParagraph(bnl.List) |
︙ | ︙ |
Changes to go.mod.
1 2 3 4 5 6 7 | module zettelstore.de/z go 1.17 require ( github.com/fsnotify/fsnotify v1.5.1 github.com/pascaldekloe/jwt v1.10.0 | | < | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 | module zettelstore.de/z go 1.17 require ( github.com/fsnotify/fsnotify v1.5.1 github.com/pascaldekloe/jwt v1.10.0 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 ) require golang.org/x/sys v0.0.0-20210630005230-0f9fa26af87c // indirect |
Changes to go.sum.
1 2 3 4 | 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= | | | < < | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 | 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.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= golang.org/x/sys v0.0.0-20210630005230-0f9fa26af87c h1:F1jZWGFhYfh0Ci55sIpILtKKK8p3i2/krTr0H1rg74I= golang.org/x/sys v0.0.0-20210630005230-0f9fa26af87c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= golang.org/x/term v0.0.0-20210615171337-6886f2dfbf5b h1:9zKuko04nR4gjZ4+DNjHqRlAJqbJETHwiNKDqTfOjfE= 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= |
Changes to input/input.go.
︙ | ︙ | |||
16 17 18 19 20 21 22 | "unicode" "unicode/utf8" ) // Input is an abstract input source type Input struct { // Read-only, will never change | | | | | 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 | "unicode" "unicode/utf8" ) // Input is an abstract input source type Input struct { // Read-only, will never change 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 string) *Input { inp := &Input{Src: src} inp.Next() return inp } // EOS = End of source const EOS = rune(-1) // Next reads the next rune into inp.Ch. 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.DecodeRuneInString(inp.Src[inp.readPos:]) } inp.readPos += w inp.Ch = r } else { inp.Pos = len(inp.Src) inp.Ch = EOS } |
︙ | ︙ | |||
63 64 65 66 67 68 69 | // PeekN returns the n-th rune after the most recently read rune without // advancing. If end-of-source was already found peek returns EOS. func (inp *Input) PeekN(n int) rune { pos := inp.readPos + n if pos < len(inp.Src) { r := rune(inp.Src[pos]) if r >= utf8.RuneSelf { | | | 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 | // PeekN returns the n-th rune after the most recently read rune without // advancing. If end-of-source was already found peek returns EOS. func (inp *Input) PeekN(n int) rune { pos := inp.readPos + n if pos < len(inp.Src) { r := rune(inp.Src[pos]) if r >= utf8.RuneSelf { r, _ = utf8.DecodeRuneInString(inp.Src[pos:]) } if r == '\t' { return ' ' } return r } return EOS |
︙ | ︙ | |||
191 192 193 194 195 196 197 | func (inp *Input) scanEntityNamed(pos int) (string, bool) { for { switch inp.Ch { case EOS, '\n', '\r': return "", false case ';': inp.Next() | | | 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 | func (inp *Input) scanEntityNamed(pos int) (string, bool) { for { switch inp.Ch { case EOS, '\n', '\r': return "", false case ';': inp.Next() es := inp.Src[pos:inp.Pos] ues := html.UnescapeString(es) if es == ues { return "", false } return ues, true } inp.Next() } } |
Changes to input/input_test.go.
︙ | ︙ | |||
15 16 17 18 19 20 21 | "testing" "zettelstore.de/z/input" ) func TestEatEOL(t *testing.T) { t.Parallel() | | | | 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 | "testing" "zettelstore.de/z/input" ) func TestEatEOL(t *testing.T) { t.Parallel() 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("ABC") if inp.Ch != 'A' { t.Errorf("First ch != 'A', got %q", inp.Ch) } inp.EatEOL() if inp.Ch != 'A' { t.Errorf("First ch != 'A', got %q", inp.Ch) } |
︙ | ︙ | |||
47 48 49 50 51 52 53 | {"", ""}, {"a", ""}, {"&", "&"}, {"	", "\t"}, {""", "\""}, } for id, tc := range testcases { | | | 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 | {"", ""}, {"a", ""}, {"&", "&"}, {"	", "\t"}, {""", "\""}, } for id, tc := range testcases { 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) } if inp.Pos != 0 { t.Errorf("ID=%d, text=%q: input position advances to %d", id, tc.text, inp.Pos) |
︙ | ︙ |
Changes to kernel/impl/box.go.
︙ | ︙ | |||
76 77 78 79 80 81 82 | defer ps.mxService.Unlock() mgr, err := ps.createManager(boxURIs, kern.auth.manager, kern.cfg.rtConfig) if err != nil { kern.doLog("Unable to create box manager:", err) return err } kern.doLog("Start Box Manager:", mgr.Location()) | | | 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 | defer ps.mxService.Unlock() mgr, err := ps.createManager(boxURIs, kern.auth.manager, kern.cfg.rtConfig) 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 { kern.doLog("Unable to start box manager:", err) } kern.cfg.setBox(mgr) ps.manager = mgr return nil } |
︙ | ︙ |
Changes to kernel/impl/cfg.go.
︙ | ︙ | |||
13 14 15 16 17 18 19 | import ( "context" "fmt" "strings" "sync" | < | | | | | | | | | | | | | | | | | | | | | | | | | | | | | 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 | import ( "context" "fmt" "strings" "sync" "zettelstore.de/z/box" "zettelstore.de/z/domain/id" "zettelstore.de/z/domain/meta" "zettelstore.de/z/kernel" ) type configService struct { srvConfig mxService sync.RWMutex rtConfig *myConfig } func (cs *configService) Initialize() { cs.descr = descriptionMap{ 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 } return vis }, true, }, 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{ 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") data := meta.New(id.ConfigurationZid) for _, kv := range cs.GetNextConfigList() { |
︙ | ︙ | |||
101 102 103 104 105 106 107 | kern.doLog("Stop Config Service") cs.mxService.Lock() cs.rtConfig = nil cs.mxService.Unlock() return nil } | | | 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 | kern.doLog("Stop Config Service") cs.mxService.Lock() cs.rtConfig = nil cs.mxService.Unlock() return nil } func (cs *configService) GetStatistics() []kernel.KeyValue { return nil } func (cs *configService) setBox(mgr box.Manager) { cs.rtConfig.setBox(mgr) } |
︙ | ︙ | |||
151 152 153 154 155 156 157 | func (cfg *myConfig) observe(ci box.UpdateInfo) { if ci.Reason == box.OnReload || ci.Zid == id.ConfigurationZid { go func() { cfg.doUpdate(ci.Box) }() } } var defaultKeys = map[string]string{ | | | | | | | | | 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 | func (cfg *myConfig) observe(ci box.UpdateInfo) { if ci.Reason == box.OnReload || ci.Zid == id.ConfigurationZid { go func() { cfg.doUpdate(ci.Box) }() } } var defaultKeys = map[string]string{ 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 { return m } result := m cfg.mx.RLock() for k, d := range defaultKeys { if _, ok := result.Get(k); !ok { if val, ok := cfg.data.Get(d); ok && val != "" { if result == m { result = m.Clone() } result.Set(k, val) } } } |
︙ | ︙ | |||
194 195 196 197 198 199 200 | cfg.mx.RLock() val := cfg.data.GetBool(key) cfg.mx.RUnlock() return val } // GetDefaultTitle returns the current value of the "default-title" key. | | | | | | | | | | | | | | | | | | 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 | cfg.mx.RLock() val := cfg.data.GetBool(key) cfg.mx.RUnlock() return val } // GetDefaultTitle returns the current value of the "default-title" key. 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(meta.KeyDefaultRole) } // GetDefaultSyntax returns the current value of the "default-syntax" key. 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(meta.KeyDefaultLang) } // GetSiteName returns the current value of the "site-name" key. 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(meta.KeyHomeZettel) if homeZid, err := id.Parse(val); err == nil { return homeZid } cfg.mx.RLock() 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(meta.KeyDefaultVisibility) if vis := meta.GetVisibility(val); vis != meta.VisibilityUnknown { return vis } cfg.mx.RLock() 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(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(meta.KeyYAMLHeader) } // GetMarkerExternal returns the current value of the "marker-external" key. func (cfg *myConfig) GetMarkerExternal() string { 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(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(meta.KeyZettelFileSyntax) } // --- AuthConfig // GetExpertMode returns the current value of the "expert-mode" key 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(meta.KeyVisibility); ok { if vis := meta.GetVisibility(val); vis != meta.VisibilityUnknown { return vis } } return cfg.GetDefaultVisibility() } |
Changes to kernel/impl/core.go.
︙ | ︙ | |||
37 38 39 40 41 42 43 | info interface{} stack []byte } func (cs *coreService) Initialize() { cs.mapRecover = make(map[string]recoverInfo) cs.descr = descriptionMap{ | < | 37 38 39 40 41 42 43 44 45 46 47 48 49 50 | info interface{} stack []byte } func (cs *coreService) Initialize() { cs.mapRecover = make(map[string]recoverInfo) cs.descr = descriptionMap{ 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: { "Port of command line server", cs.noFrozen(func(val string) interface{} { |
︙ | ︙ | |||
67 68 69 70 71 72 73 | } return val }), false, }, } cs.next = interfaceMap{ | < | | 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 | } return val }), false, }, } cs.next = interfaceMap{ kernel.CoreGoArch: runtime.GOARCH, kernel.CoreGoOS: runtime.GOOS, kernel.CoreGoVersion: runtime.Version(), kernel.CoreHostname: "*unknown host*", kernel.CorePort: 0, kernel.CoreVerbose: false, } if hn, err := os.Hostname(); err == nil { cs.next[kernel.CoreHostname] = hn } } 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 { cs.started = false return nil |
︙ | ︙ |
Changes to kernel/impl/impl.go.
︙ | ︙ | |||
28 29 30 31 32 33 34 35 36 37 38 39 40 41 | // myKernel is the main internal kernel. type myKernel struct { // started bool wg sync.WaitGroup mx sync.RWMutex interrupt chan os.Signal core coreService cfg configService auth authService box boxService web webService | > | 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 | // myKernel is the main internal kernel. 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 web webService |
︙ | ︙ | |||
104 105 106 107 108 109 110 | signal.Notify(kern.interrupt, os.Interrupt, syscall.SIGTERM) go func() { // Wait for interrupt. sig := <-kern.interrupt if strSig := sig.String(); strSig != "" { kern.doLog("Shut down Zettelstore:", strSig) } | | < < < < < | > > > > > > > > | | | 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 | signal.Notify(kern.interrupt, os.Interrupt, syscall.SIGTERM) go func() { // Wait for interrupt. sig := <-kern.interrupt if strSig := sig.String(); strSig != "" { kern.doLog("Shut down Zettelstore:", strSig) } kern.shutdown() kern.wg.Done() }() kern.StartService(kernel.CoreService) if headline { kern.doLog(fmt.Sprintf( "%v %v (%v@%v/%v)", kern.core.GetConfig(kernel.CoreProgname), kern.core.GetConfig(kernel.CoreVersion), 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.auth.GetConfig(kernel.AuthReadonly).(bool) { kern.doLog("Read-only mode") } } if lineServer { port := kern.core.GetNextConfig(kernel.CorePort).(int) if port > 0 { listenAddr := net.JoinHostPort("127.0.0.1", strconv.Itoa(port)) startLineServer(kern, listenAddr) } } } 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) { kern.interrupt <- &shutdownSignal{silent: silent} } type shutdownSignal struct{ silent bool } func (s *shutdownSignal) String() string { if s.silent { return "" } return "shutdown" } 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 (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 { return kern.doLogRecover(name, recoverInfo) } |
︙ | ︙ |
Changes to kernel/impl/web.go.
︙ | ︙ | |||
44 45 46 47 48 49 50 | kernel.WebListenAddress: { "Listen address", func(val string) interface{} { host, port, err := net.SplitHostPort(val) if err != nil { return nil } | | | 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 | kernel.WebListenAddress: { "Listen address", func(val string) interface{} { host, port, err := net.SplitHostPort(val) if err != nil { return nil } if _, err := net.LookupPort("tcp", port); err != nil { return nil } return net.JoinHostPort(host, port) }, true}, kernel.WebPersistentCookie: {"Persistent cookie", parseBool, true}, kernel.WebSecureCookie: {"Secure cookie", parseBool, true}, |
︙ | ︙ | |||
111 112 113 114 115 116 117 | srvw := impl.New(listenAddr, urlPrefix, persistentCookie, secureCookie, kern.auth.manager) 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 } | | | | 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 | srvw := impl.New(listenAddr, urlPrefix, persistentCookie, secureCookie, kern.auth.manager) 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.debug { srvw.SetDebug() } 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() ws.srvw = srvw ws.mxService.Unlock() |
︙ | ︙ |
Changes to kernel/kernel.go.
︙ | ︙ | |||
25 26 27 28 29 30 31 32 33 34 35 36 37 38 | // Kernel is the main internal service. type Kernel interface { // Start the service. Start(headline bool, lineServer bool) // WaitForShutdown blocks the call until Shutdown is called. WaitForShutdown() // Shutdown the service. Waits for all concurrent activities to stop. Shutdown(silent bool) // Log some activity. Log(args ...interface{}) | > > > | 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 | // Kernel is the main internal service. type Kernel interface { // 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. Log(args ...interface{}) |
︙ | ︙ | |||
87 88 89 90 91 92 93 | AuthService BoxService WebService ) // Constants for core service system keys. const ( | < | 90 91 92 93 94 95 96 97 98 99 100 101 102 103 | AuthService BoxService WebService ) // Constants for core service system keys. const ( CoreGoArch = "go-arch" CoreGoOS = "go-os" CoreGoVersion = "go-version" CoreHostname = "hostname" CorePort = "port" CoreProgname = "progname" CoreVerbose = "verbose" |
︙ | ︙ |
Changes to parser/blob/blob.go.
1 | //----------------------------------------------------------------------------- | | < | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 | //----------------------------------------------------------------------------- // 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 // under this license. //----------------------------------------------------------------------------- // Package blob provides a parser of binary data. package blob import ( "zettelstore.de/z/ast" "zettelstore.de/z/domain/meta" "zettelstore.de/z/input" "zettelstore.de/z/parser" ) func init() { |
︙ | ︙ | |||
46 47 48 49 50 51 52 | }) } func parseBlocks(inp *input.Input, m *meta.Meta, syntax string) *ast.BlockListNode { if p := parser.Get(syntax); p != nil { syntax = p.Name } | | | 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 | }) } 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(meta.KeyTitle) return &ast.BlockListNode{List: []ast.BlockNode{ &ast.BLOBNode{ Title: title, Syntax: syntax, Blob: []byte(inp.Src), }, }} } func parseInlines(*input.Input, string) *ast.InlineListNode { return nil } |
Changes to parser/cleaner/cleaner.go.
︙ | ︙ | |||
8 9 10 11 12 13 14 | // under this license. //----------------------------------------------------------------------------- // Package cleaner provides funxtions to clean up the parsed AST. package cleaner import ( | < > | | 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 | // under this license. //----------------------------------------------------------------------------- // Package cleaner provides funxtions to clean up the parsed AST. package cleaner import ( "strconv" "strings" "zettelstore.de/z/api" "zettelstore.de/z/ast" "zettelstore.de/z/encoder" "zettelstore.de/z/strfun" ) // CleanBlockList cleans the given block list. func CleanBlockList(bln *ast.BlockListNode) { cleanNode(bln) } |
︙ | ︙ | |||
60 61 62 63 64 65 66 | } func (cv *cleanVisitor) visitHeading(hn *ast.HeadingNode) { if cv.doMark || hn == nil || hn.Inlines.IsEmpty() { return } if hn.Slug == "" { | | | | | 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 | } func (cv *cleanVisitor) visitHeading(hn *ast.HeadingNode) { if cv.doMark || hn == nil || hn.Inlines.IsEmpty() { return } if hn.Slug == "" { var sb strings.Builder _, err := cv.textEnc.WriteInlines(&sb, hn.Inlines) if err != nil { return } hn.Slug = strfun.Slugify(sb.String()) } if hn.Slug != "" { hn.Fragment = cv.addIdentifier(hn.Slug, hn) } } func (cv *cleanVisitor) visitMark(mn *ast.MarkNode) { |
︙ | ︙ | |||
97 98 99 100 101 102 103 | cv.ids = map[string]ast.Node{id: node} return id } if n, ok := cv.ids[id]; ok && n != node { prefix := id + "-" for count := 1; ; count++ { newID := prefix + strconv.Itoa(count) | | | 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 | cv.ids = map[string]ast.Node{id: node} return id } if n, ok := cv.ids[id]; ok && n != node { prefix := id + "-" for count := 1; ; count++ { newID := prefix + strconv.Itoa(count) if n, ok := cv.ids[newID]; !ok || n == node { cv.ids[newID] = node return newID } } } cv.ids[id] = node return id } |
Changes to parser/markdown/markdown.go.
︙ | ︙ | |||
16 17 18 19 20 21 22 | "fmt" "strings" gm "github.com/yuin/goldmark" gmAst "github.com/yuin/goldmark/ast" gmText "github.com/yuin/goldmark/text" | | | 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 | "fmt" "strings" gm "github.com/yuin/goldmark" gmAst "github.com/yuin/goldmark/ast" gmText "github.com/yuin/goldmark/text" "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" ) |
︙ | ︙ | |||
130 131 132 133 134 135 136 | Lines: p.acceptRawText(node), } } func (p *mdP) acceptFencedCodeBlock(node *gmAst.FencedCodeBlock) *ast.VerbatimNode { var attrs *ast.Attributes if language := node.Language(p.source); len(language) > 0 { | | | 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 | Lines: p.acceptRawText(node), } } 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(string(language), true)) } return &ast.VerbatimNode{ Kind: ast.VerbatimProg, Attrs: attrs, Lines: p.acceptRawText(node), } } |
︙ | ︙ | |||
264 265 266 267 268 269 270 | if node.IsRaw() { return splitText(string(segment.Value(p.source))) } 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 { | | | 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 | if node.IsRaw() { return splitText(string(segment.Value(p.source))) } 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(tn.Text, true) } result = append(result, in) } if node.HardLineBreak() { result = append(result, &ast.BreakNode{Hard: true}) } else if node.SoftLineBreak() { result = append(result, &ast.BreakNode{Hard: false}) |
︙ | ︙ | |||
321 322 323 324 325 326 327 | '-': true, '.': true, '/': true, ':': true, ';': true, '<': true, '=': true, '>': true, '?': true, '@': true, '[': true, '\\': true, ']': true, '^': true, '_': true, '`': true, '{': true, '|': true, '}': true, '~': true, } // cleanText removes backslashes from TextNodes and expands entities | | | | | | | | > > > | | | | | | | | > > > | | | | | | | | > | | | | | | | | 321 322 323 324 325 326 327 328 329 330 331 332 333 334 335 336 337 338 339 340 341 342 343 344 345 346 347 348 349 350 351 352 353 354 355 356 357 358 359 360 361 362 363 364 365 366 367 368 369 370 371 372 373 374 375 376 377 378 379 380 381 382 383 384 385 386 387 388 389 390 391 392 393 394 395 396 397 398 399 400 401 402 403 404 405 406 407 408 409 410 411 412 413 414 415 416 417 418 419 420 421 422 423 424 425 426 427 428 429 430 431 432 433 434 435 436 437 438 439 440 441 442 443 444 445 446 447 448 449 450 451 452 453 454 455 456 457 458 459 460 461 462 463 464 465 466 | '-': true, '.': true, '/': true, ':': true, ';': true, '<': true, '=': true, '>': true, '?': true, '@': true, '[': true, '\\': true, ']': true, '^': true, '_': true, '`': true, '{': true, '|': true, '}': true, '~': true, } // cleanText removes backslashes from TextNodes and expands entities func cleanText(text string, cleanBS bool) string { lastPos := 0 var sb strings.Builder for pos, ch := range text { if pos < lastPos { continue } if ch == '&' { inp := input.NewInput(text[pos:]) if s, ok := inp.ScanEntity(); ok { sb.WriteString(text[lastPos:pos]) sb.WriteString(s) lastPos = pos + inp.Pos } continue } if cleanBS && ch == '\\' && pos < len(text)-1 && ignoreAfterBS[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) { sb.WriteString(text[lastPos:]) } 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(string(node.Text(p.source))), }, } } func cleanCodeSpan(text string) string { if text == "" { return "" } lastPos := 0 var sb strings.Builder for pos, ch := range text { if ch == '\n' { sb.WriteString(text[lastPos:pos]) if pos < len(text)-1 { sb.WriteByte(' ') } lastPos = pos + 1 } } 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 { kind = ast.FormatStrong } return []ast.InlineNode{ &ast.FormatNode{ Kind: kind, Attrs: nil, //TODO Inlines: p.acceptInlineChildren(node), }, } } func (p *mdP) acceptLink(node *gmAst.Link) []ast.InlineNode { ref := ast.ParseReference(cleanText(string(node.Destination), true)) var attrs *ast.Attributes if title := string(node.Title); len(title) > 0 { attrs = attrs.Set("title", cleanText(title, true)) } return []ast.InlineNode{ &ast.LinkNode{ Ref: ref, Inlines: p.acceptInlineChildren(node), OnlyRef: false, Attrs: attrs, }, } } func (p *mdP) acceptImage(node *gmAst.Image) []ast.InlineNode { ref := ast.ParseReference(cleanText(string(node.Destination), true)) var attrs *ast.Attributes 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}, Inlines: p.flattenInlineList(node), Attrs: attrs, }, } } func (p *mdP) flattenInlineList(node gmAst.Node) *ast.InlineListNode { iln := p.acceptInlineChildren(node) var sb strings.Builder _, err := p.textEnc.WriteInlines(&sb, iln) if err != nil { panic(err) } text := sb.String() if text == "" { return nil } return ast.CreateInlineListNode(&ast.TextNode{Text: text}) } func (p *mdP) acceptAutoLink(node *gmAst.AutoLink) []ast.InlineNode { url := node.URL(p.source) if node.AutoLinkType == gmAst.AutoLinkEmail && !bytes.HasPrefix(bytes.ToLower(url), []byte("mailto:")) { url = append([]byte("mailto:"), url...) } ref := ast.ParseReference(cleanText(string(url), false)) label := node.Label(p.source) if len(label) == 0 { label = url } return []ast.InlineNode{ &ast.LinkNode{ Ref: ref, Inlines: ast.CreateInlineListNode(&ast.TextNode{Text: string(label)}), OnlyRef: true, Attrs: nil, //TODO |
︙ | ︙ |
Changes to parser/markdown/markdown_test.go.
︙ | ︙ | |||
8 9 10 11 12 13 14 | // under this license. //----------------------------------------------------------------------------- // Package markdown provides a parser for markdown. package markdown import ( | | | | | | | | | | 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 | // under this license. //----------------------------------------------------------------------------- // Package markdown provides a parser for markdown. package markdown import ( "strings" "testing" "zettelstore.de/z/ast" ) func TestSplitText(t *testing.T) { t.Parallel() var testcases = []struct { text string exp string }{ {"", ""}, {"abc", "Tabc"}, {" ", "S "}, {"abc def", "TabcS Tdef"}, {"abc def ", "TabcS TdefS "}, {" abc def ", "S TabcS TdefS "}, } for i, tc := range testcases { var sb strings.Builder for _, in := range splitText(tc.text) { switch n := in.(type) { case *ast.TextNode: sb.WriteByte('T') sb.WriteString(n.Text) case *ast.SpaceNode: sb.WriteByte('S') sb.WriteString(n.Lexeme) default: sb.WriteByte('Q') } } got := sb.String() if tc.exp != got { t.Errorf("TC=%d, text=%q, exp=%q, got=%q", i, tc.text, tc.exp, got) } } } |
Changes to parser/none/none.go.
︙ | ︙ | |||
8 9 10 11 12 13 14 | // under this license. //----------------------------------------------------------------------------- // Package none provides a none-parser for meta data. package none import ( | < | | 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 | // under this license. //----------------------------------------------------------------------------- // Package none provides a none-parser for meta data. package none import ( "zettelstore.de/z/ast" "zettelstore.de/z/domain/meta" "zettelstore.de/z/input" "zettelstore.de/z/parser" ) func init() { parser.Register(&parser.Info{ Name: meta.ValueSyntaxNone, AltNames: []string{}, IsTextParser: false, IsImageFormat: false, ParseBlocks: parseBlocks, ParseInlines: parseInlines, }) } |
︙ | ︙ | |||
87 88 89 90 91 92 93 | func parseInlines(inp *input.Input, _ string) *ast.InlineListNode { inp.SkipToEOL() return ast.CreateInlineListNode( &ast.FormatNode{ Kind: ast.FormatSpan, Attrs: &ast.Attributes{Attrs: map[string]string{"class": "warning"}}, Inlines: ast.CreateInlineListNodeFromWords( | | | 86 87 88 89 90 91 92 93 94 95 96 97 | func parseInlines(inp *input.Input, _ string) *ast.InlineListNode { inp.SkipToEOL() 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", "("+inp.Src[0:inp.Pos]+")", ), }, ) } |
Changes to parser/parser.go.
︙ | ︙ | |||
11 12 13 14 15 16 17 | // Package parser provides a generic interface to a range of different parsers. package parser import ( "fmt" "log" | < | 11 12 13 14 15 16 17 18 19 20 21 22 23 24 | // Package parser provides a generic interface to a range of different parsers. package parser import ( "fmt" "log" "zettelstore.de/z/ast" "zettelstore.de/z/config" "zettelstore.de/z/domain" "zettelstore.de/z/domain/meta" "zettelstore.de/z/input" "zettelstore.de/z/parser/cleaner" ) |
︙ | ︙ | |||
37 38 39 40 41 42 43 | ParseBlocks func(*input.Input, *meta.Meta, string) *ast.BlockListNode ParseInlines func(*input.Input, string) *ast.InlineListNode } var registry = map[string]*Info{} // Register the parser (info) for later retrieval. | | > | 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 | ParseBlocks func(*input.Input, *meta.Meta, string) *ast.BlockListNode ParseInlines func(*input.Input, string) *ast.InlineListNode } var registry = map[string]*Info{} // Register the parser (info) for later retrieval. 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 { 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)) for syntax := range registry { result = append(result, syntax) |
︙ | ︙ | |||
104 105 106 107 108 109 110 | func ParseInlines(inp *input.Input, syntax string) *ast.InlineListNode { return Get(syntax).ParseInlines(inp, syntax) } // 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 { | | | | | | 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 | func ParseInlines(inp *input.Input, syntax string) *ast.InlineListNode { return Get(syntax).ParseInlines(inp, syntax) } // 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(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 inhMeta := m if rtConfig != nil { inhMeta = rtConfig.AddDefaultValues(inhMeta) } if syntax == "" { syntax, _ = inhMeta.Get(meta.KeySyntax) } parseMeta := inhMeta 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.AsString()), parseMeta, syntax), } } |
Changes to parser/parser_test.go.
︙ | ︙ | |||
10 11 12 13 14 15 16 | // Package parser provides a generic interface to a range of different parsers. package parser_test import ( "testing" | | | 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 | // Package parser provides a generic interface to a range of different parsers. package parser_test import ( "testing" "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. _ "zettelstore.de/z/parser/plain" // Allow to use plain parser. _ "zettelstore.de/z/parser/zettelmark" // Allow to use zettelmark parser. |
︙ | ︙ | |||
39 40 41 42 43 44 45 | {"css", false, false}, {"gif", false, true}, {"jpeg", false, true}, {"jpg", false, true}, {"markdown", true, false}, {"md", true, false}, {"mustache", false, false}, | | | | 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 | {"css", false, false}, {"gif", false, true}, {"jpeg", false, true}, {"jpg", false, true}, {"markdown", true, false}, {"md", true, false}, {"mustache", false, false}, {meta.ValueSyntaxNone, false, false}, {"plain", false, false}, {"png", false, true}, {"svg", false, true}, {"text", false, false}, {"txt", false, 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) } if got := parser.IsImageFormat(tc.syntax); got != tc.image { |
︙ | ︙ |
Changes to parser/plain/plain.go.
︙ | ︙ | |||
83 84 85 86 87 88 89 | for { inp.EatEOL() posL := inp.Pos if inp.Ch == input.EOS { return lines } inp.SkipToEOL() | | | | 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 | for { inp.EatEOL() posL := inp.Pos if inp.Ch == input.EOS { return lines } inp.SkipToEOL() lines = append(lines, inp.Src[posL:inp.Pos]) } } func parseInlines(inp *input.Input, syntax string) *ast.InlineListNode { return doParseInlines(inp, syntax, ast.LiteralProg) } func parseInlinesHTML(inp *input.Input, syntax string) *ast.InlineListNode { return doParseInlines(inp, syntax, ast.LiteralHTML) } 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: inp.Src[0:inp.Pos], }) } func parseSVGBlocks(inp *input.Input, _ *meta.Meta, syntax string) *ast.BlockListNode { iln := parseSVGInlines(inp, syntax) if iln == nil { return nil |
︙ | ︙ | |||
127 128 129 130 131 132 133 | }) } func scanSVG(inp *input.Input) string { for input.IsSpace(inp.Ch) { inp.Next() } | | | 127 128 129 130 131 132 133 134 135 136 137 138 139 140 | }) } func scanSVG(inp *input.Input) string { for input.IsSpace(inp.Ch) { inp.Next() } svgSrc := inp.Src[inp.Pos:] if !strings.HasPrefix(svgSrc, "<svg ") { return "" } // TODO: check proper end </svg> return svgSrc } |
Changes to parser/zettelmark/block.go.
︙ | ︙ | |||
189 190 191 192 193 194 195 | return rn, true } inp.SetPos(posL) case input.EOS: return nil, false } inp.SkipToEOL() | | | 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 | return rn, true } inp.SetPos(posL) case input.EOS: return nil, false } inp.SkipToEOL() rn.Lines = append(rn.Lines, inp.Src[posL:inp.Pos]) } } var runeRegion = map[rune]ast.RegionKind{ ':': ast.RegionSpan, '<': ast.RegionQuote, '"': ast.RegionVerse, |
︙ | ︙ | |||
270 271 272 273 274 275 276 | } } // parseHeading parses a head line. func (cp *zmkP) parseHeading() (hn *ast.HeadingNode, success bool) { inp := cp.inp | | | > > > < < < | | 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 | } } // parseHeading parses a head line. func (cp *zmkP) parseHeading() (hn *ast.HeadingNode, success bool) { inp := cp.inp 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() hn = &ast.HeadingNode{Level: lvl - 1, Inlines: &ast.InlineListNode{}} for { if input.IsEOLEOS(inp.Ch) { return hn, true } in := cp.parseInline() if in == nil { return hn, true |
︙ | ︙ | |||
537 538 539 540 541 542 543 | return false } 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 { | | | 537 538 539 540 541 542 543 544 545 546 547 548 549 550 551 | return false } 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 cp.descrl.Descriptions[defPos].Descriptions[descrPos] = append(cp.descrl.Descriptions[defPos].Descriptions[descrPos], pn) } return true } // parseLinePara parses one line of inline material. func (cp *zmkP) parseLinePara() *ast.ParaNode { |
︙ | ︙ |
Changes to parser/zettelmark/inline.go.
︙ | ︙ | |||
8 9 10 11 12 13 14 | // under this license. //----------------------------------------------------------------------------- // Package zettelmark provides a parser for zettelmarkup. package zettelmark import ( | < > | 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 | // under this license. //----------------------------------------------------------------------------- // Package zettelmark provides a parser for zettelmarkup. package zettelmark import ( "fmt" "strings" "zettelstore.de/z/ast" "zettelstore.de/z/input" ) // parseInlineList parses a sequence of Inlines until EOS. func (cp *zmkP) parseInlineList() *ast.InlineListNode { |
︙ | ︙ | |||
66 67 68 69 70 71 72 | if inp.Ch == '{' { in, success = cp.parseEmbed() } case '#': return cp.parseTag() case '%': in, success = cp.parseComment() | | | 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 | if inp.Ch == '{' { in, success = cp.parseEmbed() } case '#': return cp.parseTag() case '%': in, success = cp.parseComment() case '/', '*', '_', '~', '\'', '^', ',', '<', '"', ';', ':': in, success = cp.parseFormat() case '+', '`', '=', runeModGrave: in, success = cp.parseLiteral() case '\\': return cp.parseBackslash() case '-': in, success = cp.parseNdash() |
︙ | ︙ | |||
96 97 98 99 100 101 102 | return cp.parseTextBackslash() } 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 | | | | | 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 | return cp.parseTextBackslash() } 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: inp.Src[pos:inp.Pos]} } } } func (cp *zmkP) parseTextBackslash() *ast.TextNode { cp.inp.Next() return cp.parseBackslashRest() |
︙ | ︙ | |||
130 131 132 133 134 135 136 | } if inp.Ch == ' ' { inp.Next() return &ast.TextNode{Text: "\u00a0"} } pos := inp.Pos inp.Next() | | | | 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 | } if inp.Ch == ' ' { inp.Next() return &ast.TextNode{Text: "\u00a0"} } pos := inp.Pos inp.Next() return &ast.TextNode{Text: inp.Src[pos:inp.Pos]} } func (cp *zmkP) parseSpace() *ast.SpaceNode { inp := cp.inp pos := inp.Pos for { inp.Next() switch inp.Ch { case ' ', '\t': default: return &ast.SpaceNode{Lexeme: inp.Src[pos:inp.Pos]} } } } func (cp *zmkP) parseSoftBreak() *ast.BreakNode { cp.inp.EatEOL() return &ast.BreakNode{} |
︙ | ︙ | |||
207 208 209 210 211 212 213 | } cp.skipSpace() pos = inp.Pos if !cp.readReferenceToClose(closeCh) { return "", nil, false } | | | 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 | } cp.skipSpace() pos = inp.Pos if !cp.readReferenceToClose(closeCh) { return "", nil, false } ref = inp.Src[pos:inp.Pos] inp.Next() if inp.Ch != closeCh { return "", nil, false } inp.Next() if len(ins) == 0 { return ref, nil, true |
︙ | ︙ | |||
302 303 304 305 306 307 308 | inp.Next() } ins, ok := cp.parseLinkLikeRest() if !ok { return nil, false } attrs := cp.parseAttributes(false) | | | 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 | inp.Next() } ins, ok := cp.parseLinkLikeRest() if !ok { return nil, false } attrs := cp.parseAttributes(false) 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() if !ok { return nil, false |
︙ | ︙ | |||
364 365 366 367 368 369 370 | pos := inp.Pos for inp.Ch != ']' { if !isNameRune(inp.Ch) { return nil, false } inp.Next() } | | | | | | < | | | | 364 365 366 367 368 369 370 371 372 373 374 375 376 377 378 379 380 381 382 383 384 385 386 387 388 389 390 391 392 393 394 395 396 397 398 399 400 401 402 403 404 405 406 407 408 409 410 411 412 413 414 415 416 417 418 419 420 | pos := inp.Pos for inp.Ch != ']' { if !isNameRune(inp.Ch) { return nil, false } inp.Next() } mn := &ast.MarkNode{Text: inp.Src[pos:inp.Pos]} inp.Next() return mn, true } func (cp *zmkP) parseTag() ast.InlineNode { inp := cp.inp posH := inp.Pos inp.Next() pos := inp.Pos for isNameRune(inp.Ch) { inp.Next() } if pos == inp.Pos || inp.Ch == '#' { return &ast.TextNode{Text: inp.Src[posH: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() if inp.Ch != '%' { return nil, false } for inp.Ch == '%' { inp.Next() } cp.skipSpace() pos := inp.Pos for { if input.IsEOLEOS(inp.Ch) { return &ast.LiteralNode{Kind: ast.LiteralComment, Text: inp.Src[pos:inp.Pos]}, true } inp.Next() } } var mapRuneFormat = map[rune]ast.FormatKind{ '/': ast.FormatItalic, '*': ast.FormatBold, '_': ast.FormatUnder, '~': ast.FormatStrike, '\'': ast.FormatMonospace, '^': ast.FormatSuper, ',': ast.FormatSub, '<': ast.FormatQuotation, '"': ast.FormatQuote, ';': ast.FormatSmall, ':': ast.FormatSpan, |
︙ | ︙ | |||
443 444 445 446 447 448 449 | if inp.Ch == fch { inp.Next() fn.Attrs = cp.parseAttributes(false) return fn, true } fn.Inlines.Append(&ast.TextNode{Text: string(fch)}) } else if in := cp.parseInline(); in != nil { | | | 442 443 444 445 446 447 448 449 450 451 452 453 454 455 456 | if inp.Ch == fch { inp.Next() 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) { return nil, false } fn.Inlines.Append(in) } } } |
︙ | ︙ | |||
471 472 473 474 475 476 477 | } inp.Next() // read 2nd formatting character if inp.Ch != fch { return nil, false } fn := &ast.LiteralNode{Kind: kind} inp.Next() | | | | | | 470 471 472 473 474 475 476 477 478 479 480 481 482 483 484 485 486 487 488 489 490 491 492 493 494 495 496 497 498 499 500 501 | } inp.Next() // read 2nd formatting character if inp.Ch != fch { return nil, false } fn := &ast.LiteralNode{Kind: kind} inp.Next() var sb strings.Builder for { if inp.Ch == input.EOS { return nil, false } if inp.Ch == fch { if inp.Peek() == fch { inp.Next() inp.Next() fn.Attrs = cp.parseAttributes(false) fn.Text = sb.String() return fn, true } sb.WriteRune(fch) inp.Next() } else { tn := cp.parseText() sb.WriteString(tn.Text) } } } func (cp *zmkP) parseNdash() (res *ast.TextNode, success bool) { inp := cp.inp if inp.Peek() != inp.Ch { |
︙ | ︙ |
Changes to parser/zettelmark/post-processor.go.
︙ | ︙ | |||
59 60 61 62 63 64 65 66 67 68 69 70 71 72 | case *ast.EmbedNode: return pp case *ast.CiteNode: return pp case *ast.FootnoteNode: return pp case *ast.FormatNode: return pp } return nil } func (pp *postProcessor) visitRegion(rn *ast.RegionNode) { oldVerse := pp.inVerse | > | 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 | case *ast.EmbedNode: return pp case *ast.CiteNode: return pp case *ast.FootnoteNode: return pp case *ast.FormatNode: pp.visitFormat(n) return pp } return nil } func (pp *postProcessor) visitRegion(rn *ast.RegionNode) { oldVerse := pp.inVerse |
︙ | ︙ | |||
107 108 109 110 111 112 113 | for i, cell := range tn.Header { pp.processCell(cell, tn.Align[i]) } } pp.visitTableRows(tn, width) } | | | 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 | for i, cell := range tn.Header { pp.processCell(cell, tn.Align[i]) } } pp.visitTableRows(tn, width) } func (pp *postProcessor) visitTableHeader(tn *ast.TableNode) { for pos, cell := range tn.Header { ins := cell.Inlines.List if len(ins) == 0 { continue } if textNode, ok := ins[0].(*ast.TextNode); ok { textNode.Text = strings.TrimPrefix(textNode.Text, "=") |
︙ | ︙ | |||
212 213 214 215 216 217 218 219 220 221 222 223 224 225 | return nil } if tn, ok := ins[0].(*ast.TextNode); ok && len(tn.Text) > 0 { return tn } return nil } func (pp *postProcessor) visitBlockList(bln *ast.BlockListNode) { if bln == nil { return } if len(bln.List) == 0 { bln.List = nil | > > > > > > > > > > > > > > > > | 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 | return nil } 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 } if len(bln.List) == 0 { bln.List = nil |
︙ | ︙ | |||
375 376 377 378 379 380 381 | again := false fromPos, toPos := 0, 0 for fromPos < maxPos { ins[toPos] = ins[fromPos] fromPos++ switch in := ins[toPos].(type) { case *ast.TextNode: | > > > | > > > > | > > > > > > > > > > > > > > > < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < | | | 392 393 394 395 396 397 398 399 400 401 402 403 404 405 406 407 408 409 410 411 412 413 414 415 416 417 418 419 420 421 422 423 424 425 426 427 428 429 430 431 432 433 434 435 436 437 438 439 440 441 442 443 444 445 446 447 448 449 450 451 452 453 454 455 456 457 458 459 460 461 462 463 464 | again := false fromPos, toPos := 0, 0 for fromPos < maxPos { ins[toPos] = ins[fromPos] fromPos++ switch in := ins[toPos].(type) { case *ast.TextNode: for fromPos < maxPos { if tn, ok := ins[fromPos].(*ast.TextNode); ok { in.Text = in.Text + tn.Text fromPos++ } else { break } } case *ast.SpaceNode: 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 } } toPos++ } return again, toPos } // processInlineSliceTail removes empty text nodes, breaks and spaces at the end. 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 { return toPos } case *ast.BreakNode: case *ast.SpaceNode: default: return toPos } toPos-- ins[toPos] = nil // Kill node to enable garbage collection } return toPos } 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] == "..." { n.Text = "\u2026" + n.Text[3:] } } } } |
Changes to parser/zettelmark/zettelmark.go.
︙ | ︙ | |||
10 11 12 13 14 15 16 | // Package zettelmark provides a parser for zettelmarkup. package zettelmark import ( "unicode" | < | | 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 | // Package zettelmark provides a parser for zettelmarkup. package zettelmark import ( "unicode" "zettelstore.de/z/ast" "zettelstore.de/z/domain/meta" "zettelstore.de/z/input" "zettelstore.de/z/parser" ) func init() { parser.Register(&parser.Info{ Name: meta.ValueSyntaxZmk, AltNames: nil, IsTextParser: true, IsImageFormat: false, ParseBlocks: parseBlocks, ParseInlines: parseInlines, }) } |
︙ | ︙ | |||
103 104 105 106 107 108 109 | return false case '\n', '\r': if sameLine { return false } fallthrough case ' ', '}': | | | 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 | return false case '\n', '\r': if sameLine { return false } fallthrough case ' ', '}': updateAttrs(attrs, key, inp.Src[posV:inp.Pos]) return true } inp.Next() } } func (cp *zmkP) parseQuotedAttributeValue(key string, attrs map[string]string, sameLine bool) bool { |
︙ | ︙ | |||
165 166 167 168 169 170 171 | inp := cp.inp if sameLine { pos := inp.Pos for isNameRune(inp.Ch) { inp.Next() } if pos < inp.Pos { | | | 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 | inp := cp.inp if sameLine { pos := inp.Pos for isNameRune(inp.Ch) { inp.Next() } if pos < inp.Pos { return &ast.Attributes{Attrs: map[string]string{"": inp.Src[pos:inp.Pos]}} } // No immediate name: skip spaces cp.skipSpace() } pos := inp.Pos |
︙ | ︙ | |||
213 214 215 216 217 218 219 | posC := inp.Pos for isNameRune(inp.Ch) { inp.Next() } if posC == inp.Pos { return false } | | | 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 | posC := inp.Pos for isNameRune(inp.Ch) { inp.Next() } if posC == inp.Pos { return false } updateAttrs(attrs, "class", inp.Src[posC:inp.Pos]) case '=': delete(attrs, "") if !cp.parseAttributeValue("", attrs, sameLine) { return false } default: if !cp.parseNormalAttribute(attrs, sameLine) { |
︙ | ︙ |
Changes to parser/zettelmark/zettelmark_test.go.
︙ | ︙ | |||
8 9 10 11 12 13 14 | // under this license. //----------------------------------------------------------------------------- // Package zettelmark_test provides some tests for the zettelmarkup parser. package zettelmark_test import ( | < | | | 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 | // under this license. //----------------------------------------------------------------------------- // Package zettelmark_test provides some tests for the zettelmarkup parser. package zettelmark_test import ( "fmt" "sort" "strings" "testing" "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 _ "zettelstore.de/z/encoder/textenc" ) |
︙ | ︙ | |||
44 45 46 47 48 49 50 | func checkTcs(t *testing.T, tcs TestCases) { t.Helper() for tcn, tc := range tcs { t.Run(fmt.Sprintf("TC=%02d,src=%q", tcn, tc.source), func(st *testing.T) { st.Helper() | | | | 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 | func checkTcs(t *testing.T, tcs TestCases) { t.Helper() for tcn, tc := range tcs { t.Run(fmt.Sprintf("TC=%02d,src=%q", tcn, tc.source), func(st *testing.T) { st.Helper() 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) } }) |
︙ | ︙ | |||
94 95 96 97 98 99 100 | {"...,", "(PARA \u2026,)"}, {"...;", "(PARA \u2026;)"}, {"...:", "(PARA \u2026:)"}, {"...!", "(PARA \u2026!)"}, {"...?", "(PARA \u2026?)"}, {"...-", "(PARA ...-)"}, {"a...b", "(PARA a...b)"}, | < | 93 94 95 96 97 98 99 100 101 102 103 104 105 106 | {"...,", "(PARA \u2026,)"}, {"...;", "(PARA \u2026;)"}, {"...:", "(PARA \u2026:)"}, {"...!", "(PARA \u2026!)"}, {"...?", "(PARA \u2026?)"}, {"...-", "(PARA ...-)"}, {"a...b", "(PARA a...b)"}, }) } func TestSpace(t *testing.T) { t.Parallel() checkTcs(t, TestCases{ {" ", ""}, |
︙ | ︙ | |||
290 291 292 293 294 295 296 | {"%%a", "(PARA {% a})"}, {"%%%a", "(PARA {% a})"}, {"%% a", "(PARA {% a})"}, {"%%% a", "(PARA {% a})"}, {"%% % a", "(PARA {% % a})"}, {"%%a", "(PARA {% a})"}, {"a%%b", "(PARA a {% b})"}, | | < | < < < < | | | | 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 326 327 328 329 330 331 332 333 334 335 | {"%%a", "(PARA {% a})"}, {"%%%a", "(PARA {% a})"}, {"%% a", "(PARA {% a})"}, {"%%% a", "(PARA {% a})"}, {"%% % a", "(PARA {% % a})"}, {"%%a", "(PARA {% a})"}, {"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() for _, ch := range []string{"/", "*", "_", "~", "'", "^", ",", "<", "\"", ";", ":"} { checkTcs(t, replace(ch, TestCases{ {"$", "(PARA $)"}, {"$$", "(PARA $$)"}, {"$$$", "(PARA $$$)"}, {"$$$$", "(PARA {$})"}, {"$$a$$", "(PARA {$ a})"}, {"$$a$$$", "(PARA {$ a} $)"}, {"$$$a$$", "(PARA {$ $a})"}, {"$$$a$$$", "(PARA {$ $a} $)"}, {"$\\$", "(PARA $$)"}, {"$\\$$", "(PARA $$$)"}, {"$$\\$", "(PARA $$$)"}, {"$$a\\$$", "(PARA $$a$$)"}, {"$$a$\\$", "(PARA $$a$$)"}, {"$$a\\$$$", "(PARA {$ a$})"}, {"$$a\na$$", "(PARA {$ a SB a})"}, {"$$a\n\na$$", "(PARA $$a)(PARA a$$)"}, {"$$a$${go}", "(PARA {$ a}[ATTR go])"}, })) } checkTcs(t, TestCases{ {"//****//", "(PARA {/ {*}})"}, {"//**a**//", "(PARA {/ {* a}})"}, {"//**//**", "(PARA // {* //})"}, }) } func TestLiteral(t *testing.T) { t.Parallel() for _, ch := range []string{"`", "+", "="} { checkTcs(t, replace(ch, TestCases{ |
︙ | ︙ | |||
364 365 366 367 368 369 370 | {"++\\+++", "(PARA {+ +})"}, }) } func TestMixFormatCode(t *testing.T) { t.Parallel() checkTcs(t, TestCases{ | | | | | 357 358 359 360 361 362 363 364 365 366 367 368 369 370 371 372 373 374 | {"++\\+++", "(PARA {+ +})"}, }) } 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})"}, {"\"\"ghi\"\"\n::abc::\n``def``\n", "(PARA {\" ghi} SB {: abc} SB {` def})"}, }) } func TestNDash(t *testing.T) { t.Parallel() checkTcs(t, TestCases{ |
︙ | ︙ | |||
460 461 462 463 464 465 466 | t.Parallel() checkTcs(t, TestCases{ {"=h", "(PARA =h)"}, {"= h", "(PARA = SP h)"}, {"==h", "(PARA ==h)"}, {"== h", "(PARA == SP h)"}, {"===h", "(PARA ===h)"}, | | | | | | | | | | | | | | | | | | | | 453 454 455 456 457 458 459 460 461 462 463 464 465 466 467 468 469 470 471 472 473 474 475 476 477 478 479 480 481 482 483 484 485 486 487 488 | t.Parallel() checkTcs(t, TestCases{ {"=h", "(PARA =h)"}, {"= h", "(PARA = SP h)"}, {"==h", "(PARA ==h)"}, {"== h", "(PARA == SP h)"}, {"===h", "(PARA ===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//", "(H2 h= {/ =a} #h-a)"}, {"=\n", "(PARA =)"}, {"a=", "(PARA a=)"}, {" =", "(PARA =)"}, {"=== 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() checkTcs(t, TestCases{ {"-", "(PARA -)"}, |
︙ | ︙ | |||
675 676 677 678 679 680 681 | }) } // -------------------------------------------------------------------------- // TestVisitor serializes the abstract syntax tree to a string. type TestVisitor struct { | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | 668 669 670 671 672 673 674 675 676 677 678 679 680 681 682 683 684 685 686 687 688 689 690 691 692 693 694 695 696 697 698 699 700 701 702 703 704 705 706 707 708 709 710 711 712 713 714 715 716 717 718 719 720 721 722 723 724 725 726 727 728 729 730 731 732 733 734 735 736 737 738 739 740 741 742 743 744 745 746 747 748 749 750 751 752 753 754 755 756 757 758 759 760 761 762 763 764 765 766 767 768 769 770 771 772 773 774 775 776 777 778 779 780 781 782 783 784 785 786 787 788 789 790 791 792 793 794 795 796 797 798 799 800 801 802 803 804 805 806 807 808 809 810 811 812 813 814 815 816 817 818 819 820 821 822 823 824 825 826 827 828 829 830 831 832 833 834 835 836 837 838 839 840 841 842 843 844 845 846 847 848 849 850 851 852 853 854 855 856 857 858 859 860 861 862 863 864 865 866 867 | }) } // -------------------------------------------------------------------------- // TestVisitor serializes the abstract syntax tree to a string. type TestVisitor struct { b strings.Builder } 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.b.WriteString("(PARA") ast.Walk(tv, n.Inlines) tv.b.WriteByte(')') case *ast.VerbatimNode: code, ok := mapVerbatimKind[n.Kind] if !ok { panic(fmt.Sprintf("Unknown verbatim code %v", n.Kind)) } tv.b.WriteString(code) for _, line := range n.Lines { tv.b.WriteByte('\n') tv.b.WriteString(line) } 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.b.WriteString(code) if n.Blocks != nil && len(n.Blocks.List) > 0 { tv.b.WriteByte(' ') ast.Walk(tv, n.Blocks) } if n.Inlines != nil { tv.b.WriteString(" (LINE") ast.Walk(tv, n.Inlines) tv.b.WriteByte(')') } tv.b.WriteByte(')') tv.visitAttributes(n.Attrs) case *ast.HeadingNode: fmt.Fprintf(&tv.b, "(H%d", n.Level) ast.Walk(tv, n.Inlines) if n.Fragment != "" { tv.b.WriteString(" #") tv.b.WriteString(n.Fragment) } tv.b.WriteByte(')') tv.visitAttributes(n.Attrs) case *ast.HRuleNode: tv.b.WriteString("(HR)") tv.visitAttributes(n.Attrs) case *ast.NestedListNode: tv.b.WriteString(mapNestedListKind[n.Kind]) for _, item := range n.Items { tv.b.WriteString(" {") ast.WalkItemSlice(tv, item) tv.b.WriteByte('}') } tv.b.WriteByte(')') case *ast.DescriptionListNode: tv.b.WriteString("(DL") for _, def := range n.Descriptions { tv.b.WriteString(" (DT") ast.Walk(tv, def.Term) tv.b.WriteByte(')') for _, b := range def.Descriptions { tv.b.WriteString(" (DD ") ast.WalkDescriptionSlice(tv, b) tv.b.WriteByte(')') } } tv.b.WriteByte(')') case *ast.TableNode: tv.b.WriteString("(TAB") if len(n.Header) > 0 { tv.b.WriteString(" (TR") for _, cell := range n.Header { tv.b.WriteString(" (TH") tv.b.WriteString(alignString[cell.Align]) ast.Walk(tv, cell.Inlines) tv.b.WriteString(")") } tv.b.WriteString(")") } if len(n.Rows) > 0 { 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.b.WriteString("HB") } else { tv.b.WriteString("SB") } case *ast.LinkNode: fmt.Fprintf(&tv.b, "(LINK %v", n.Ref) ast.Walk(tv, n.Inlines) tv.b.WriteByte(')') tv.visitAttributes(n.Attrs) case *ast.EmbedNode: switch m := n.Material.(type) { case *ast.ReferenceMaterialNode: fmt.Fprintf(&tv.b, "(EMBED %v", m.Ref) if n.Inlines != nil { ast.Walk(tv, n.Inlines) } 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.b, "(CITE %s", n.Key) if n.Inlines != nil { ast.Walk(tv, n.Inlines) } tv.b.WriteByte(')') tv.visitAttributes(n.Attrs) case *ast.FootnoteNode: tv.b.WriteString("(FN") ast.Walk(tv, n.Inlines) tv.b.WriteByte(')') tv.visitAttributes(n.Attrs) case *ast.MarkNode: tv.b.WriteString("(MARK") if n.Text != "" { tv.b.WriteString(" \"") tv.b.WriteString(n.Text) tv.b.WriteByte('"') } if n.Fragment != "" { tv.b.WriteString(" #") tv.b.WriteString(n.Fragment) } tv.b.WriteByte(')') case *ast.FormatNode: fmt.Fprintf(&tv.b, "{%c", mapFormatKind[n.Kind]) ast.Walk(tv, n.Inlines) 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.b.WriteByte('{') tv.b.WriteRune(code) if n.Text != "" { tv.b.WriteByte(' ') tv.b.WriteString(n.Text) } tv.b.WriteByte('}') tv.visitAttributes(n.Attrs) default: return tv } return nil } |
︙ | ︙ | |||
892 893 894 895 896 897 898 | ast.AlignDefault: "", ast.AlignLeft: "l", ast.AlignCenter: "c", ast.AlignRight: "r", } var mapFormatKind = map[ast.FormatKind]rune{ | | | | | | | | | | | | | | | | 885 886 887 888 889 890 891 892 893 894 895 896 897 898 899 900 901 902 903 904 905 906 907 908 909 910 911 912 913 914 915 916 917 918 919 920 921 922 923 924 925 926 927 928 929 930 931 932 933 934 935 936 937 938 939 940 941 942 943 944 945 946 947 948 | ast.AlignDefault: "", ast.AlignLeft: "l", ast.AlignCenter: "c", ast.AlignRight: "r", } var mapFormatKind = map[ast.FormatKind]rune{ ast.FormatItalic: '/', ast.FormatBold: '*', ast.FormatUnder: '_', ast.FormatStrike: '~', ast.FormatMonospace: '\'', ast.FormatSuper: '^', ast.FormatSub: ',', ast.FormatQuote: '"', ast.FormatQuotation: '<', ast.FormatSmall: ';', ast.FormatSpan: ':', } var mapLiteralKind = map[ast.LiteralKind]rune{ ast.LiteralProg: '`', ast.LiteralKeyb: '+', ast.LiteralOutput: '=', ast.LiteralComment: '%', } func (tv *TestVisitor) visitInlineList(iln *ast.InlineListNode) { for _, in := range iln.List { tv.b.WriteByte(' ') ast.Walk(tv, in) } } func (tv *TestVisitor) visitAttributes(a *ast.Attributes) { if a.IsEmpty() { return } 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.b.WriteByte(' ') tv.b.WriteString(k) v := a.Attrs[k] if len(v) > 0 { tv.b.WriteByte('=') if strings.ContainsRune(v, ' ') { tv.b.WriteByte('"') tv.b.WriteString(v) tv.b.WriteByte('"') } else { tv.b.WriteString(v) } } } tv.b.WriteByte(']') } |
Changes to search/print.go.
︙ | ︙ | |||
12 13 14 15 16 17 18 | package search import ( "io" "sort" "strconv" | | | 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 | package search import ( "io" "sort" "strconv" "zettelstore.de/z/domain/meta" ) // Print the search to a writer. func (s *Search) Print(w io.Writer) { if s.negate { io.WriteString(w, "NOT (") } |
︙ | ︙ | |||
46 47 48 49 50 51 52 | if s.negate { io.WriteString(w, ")") space = true } if ord := s.order; len(ord) > 0 { switch ord { | | | 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 | if s.negate { io.WriteString(w, ")") space = true } if ord := s.order; len(ord) > 0 { switch ord { case meta.KeyID: // Ignore case RandomOrder: space = printSpace(w, space) io.WriteString(w, "RANDOM") default: space = printSpace(w, space) io.WriteString(w, "SORT ") |
︙ | ︙ |
Changes to search/search.go.
︙ | ︙ | |||
13 14 15 16 17 18 19 | import ( "math/rand" "sort" "strings" "sync" | < | 13 14 15 16 17 18 19 20 21 22 23 24 25 26 | import ( "math/rand" "sort" "strings" "sync" "zettelstore.de/z/domain/id" "zettelstore.de/z/domain/meta" ) // Searcher is used to select zettel identifier based on search criteria. type Searcher interface { // Select all zettel that contains the given exact word. |
︙ | ︙ | |||
229 230 231 232 233 234 235 | func (s *Search) EnrichNeeded() bool { if s == nil { return false } s.mx.RLock() defer s.mx.RUnlock() for key := range s.tags { | | | | 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 | func (s *Search) EnrichNeeded() bool { if s == nil { return false } s.mx.RLock() defer s.mx.RUnlock() for key := range s.tags { if meta.IsComputed(key) || key == meta.KeyTags { return true } } if order := s.order; order != "" && (meta.IsComputed(order) || order == meta.KeyTags) { return true } return false } // CompileMatch returns a function to match meta data based on select specification. func (s *Search) CompileMatch(searcher Searcher) MetaMatchFunc { |
︙ | ︙ | |||
255 256 257 258 259 260 261 | compSearch := compileFullSearch(searcher, s.search) if preMatch := s.preMatch; preMatch != nil { return compilePreMatch(preMatch, compMeta, compSearch, s.negate) } return compileNoPreMatch(compMeta, compSearch, s.negate) } | | | 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 | compSearch := compileFullSearch(searcher, s.search) if preMatch := s.preMatch; preMatch != nil { return compilePreMatch(preMatch, compMeta, compSearch, s.negate) } return compileNoPreMatch(compMeta, compSearch, s.negate) } 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 } if negate { |
︙ | ︙ | |||
316 317 318 319 320 321 322 | if s == nil { sort.Slice(metaList, func(i, j int) bool { return metaList[i].Zid > metaList[j].Zid }) return metaList } if s.order == "" { | | | 315 316 317 318 319 320 321 322 323 324 325 326 327 328 329 | if s == nil { sort.Slice(metaList, func(i, j int) bool { return metaList[i].Zid > metaList[j].Zid }) return metaList } if s.order == "" { 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 { sort.Slice(metaList, createSortFunc(s.order, s.descending, metaList)) } |
︙ | ︙ |
Changes to search/select.go.
︙ | ︙ | |||
10 11 12 13 14 15 16 | // Package search provides a zettel search. package search import ( "strings" | < | | | 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 | // Package search provides a zettel search. package search import ( "strings" "zettelstore.de/z/domain/meta" ) type matchFunc func(value string) bool func matchNever(value string) bool { return false } func matchAlways(value string) bool { return true } type matchSpec struct { key string match matchFunc } // compileSelect calculates a selection func based on the given select criteria. |
︙ | ︙ | |||
88 89 90 91 92 93 94 | for _, val := range values { if val.negate { negValues = append(negValues, opValue{value: val.value, op: val.op}) } else { posValues = append(posValues, opValue{value: val.value, op: val.op}) } } | | | | | | | | | | | | | | | | | | | 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 | for _, val := range values { if val.negate { 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), 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) matchFunc { if len(values) == 0 { return nil } switch meta.Type(key) { case meta.TypeBool: return createMatchBoolFunc(values) case meta.TypeCredential: return matchNever case meta.TypeID, meta.TypeTimestamp: // ID and timestamp use the same layout return createMatchIDFunc(values) case meta.TypeIDSet: return createMatchIDSetFunc(values) case meta.TypeTagSet: return createMatchTagSetFunc(values) case meta.TypeWord: return createMatchWordFunc(values) case meta.TypeWordSet: return createMatchWordSetFunc(values) } return createMatchStringFunc(values) } 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 { return false } } return true } } func createMatchIDFunc(values []opValue) matchFunc { return func(value string) bool { for _, v := range values { if !strings.HasPrefix(value, v.value) { return false } } return true } } 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) { return false } } } return true } } func matchAllID(zettelIDs []string, neededID string) bool { for _, zt := range zettelIDs { if strings.HasPrefix(zt, neededID) { return true } } return false } 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) { return false } } } return true } } |
︙ | ︙ | |||
229 230 231 232 233 234 235 | return true } } } return false } | | | | | | | | 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 | return true } } } return false } 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 { return false } } return true } } 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) { return false } } } return true } } 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) { return false } } return true } } |
︙ | ︙ | |||
287 288 289 290 291 292 293 | } } for _, s := range negSpecs { if s.match == nil { if _, ok := m.Get(s.key); ok { return false } | | | | | 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 | } } 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) { return false } } return true } } func getMeta(m *meta.Meta, key string) (string, bool) { if key == meta.KeyTags { return m.Get(meta.KeyAllTags) } return m.Get(key) } func sliceToLower(sl []opValue) []opValue { result := make([]opValue, 0, len(sl)) for _, s := range sl { |
︙ | ︙ |
Changes to search/sorter.go.
︙ | ︙ | |||
10 11 12 13 14 15 16 | // Package search provides a zettel search. package search import ( "strconv" | < | | 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 | // Package search provides a zettel search. package search import ( "strconv" "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 == 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 } } if keyType == meta.TypeBool { return createSortBoolFunc(ml, key, descending) |
︙ | ︙ |
Changes to strfun/strfun.go.
︙ | ︙ | |||
8 9 10 11 12 13 14 | // under this license. //----------------------------------------------------------------------------- // Package strfun provides some string functions. package strfun import ( | < > > > > > > > | | | | | 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 | // under this license. //----------------------------------------------------------------------------- // Package strfun provides some string functions. package strfun import ( "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) } // JustifyLeft ensures that the string has a defined length. func JustifyLeft(s string, maxLen int, pad rune) string { if maxLen < 1 { return "" } runes := make([]rune, 0, len(s)) for _, r := range s { runes = append(runes, r) } if len(runes) > maxLen { runes = runes[:maxLen] runes[maxLen-1] = '\u2025' } var sb strings.Builder for _, r := range runes { sb.WriteRune(r) } for i := 0; i < maxLen-len(runes); i++ { sb.WriteRune(pad) } 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' }) } |
Changes to strfun/strfun_test.go.
︙ | ︙ | |||
12 13 14 15 16 17 18 19 20 21 22 23 24 25 | package strfun_test import ( "testing" "zettelstore.de/z/strfun" ) func TestLength(t *testing.T) { t.Parallel() testcases := []struct { in string exp int }{ | > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > | 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 | package strfun_test 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 exp int }{ |
︙ | ︙ |
Changes to template/mustache.go.
︙ | ︙ | |||
333 334 335 336 337 338 339 | tag := tagResult.tag switch tag[0] { case '!': //ignore comment case '#', '^': name := strings.TrimSpace(tag[1:]) sn := §ionNode{name, tag[0] == '^', tmpl.curline, []node{}} | | | 333 334 335 336 337 338 339 340 341 342 343 344 345 346 347 | tag := tagResult.tag switch tag[0] { case '!': //ignore comment case '#', '^': name := strings.TrimSpace(tag[1:]) sn := §ionNode{name, tag[0] == '^', tmpl.curline, []node{}} err := tmpl.parseSection(sn) if err != nil { return err } section.nodes = append(section.nodes, sn) case '/': name := strings.TrimSpace(tag[1:]) if name != section.name { |
︙ | ︙ | |||
405 406 407 408 409 410 411 | tag := tagResult.tag switch tag[0] { case '!': //ignore comment case '#', '^': name := strings.TrimSpace(tag[1:]) sn := §ionNode{name, tag[0] == '^', tmpl.curline, []node{}} | | | 405 406 407 408 409 410 411 412 413 414 415 416 417 418 419 | tag := tagResult.tag switch tag[0] { case '!': //ignore comment case '#', '^': name := strings.TrimSpace(tag[1:]) sn := §ionNode{name, tag[0] == '^', tmpl.curline, []node{}} err := tmpl.parseSection(sn) if err != nil { return err } tmpl.nodes = append(tmpl.nodes, sn) case '/': return parseError{tmpl.curline, "unmatched close tag"} case '>': |
︙ | ︙ | |||
560 561 562 563 564 565 566 | for i := 0; i < valLen; i++ { enumeration[i] = val.Index(i) } topStack := len(stack) stack = append(stack, enumeration[0]) for _, elem := range enumeration { stack[topStack] = elem | | | 560 561 562 563 564 565 566 567 568 569 570 571 572 573 574 | for i := 0; i < valLen; i++ { enumeration[i] = val.Index(i) } 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 { return err } } return nil case reflect.Map, reflect.Struct: return tmpl.renderNodes(w, section.nodes, append(stack, value)) } |
︙ | ︙ | |||
610 611 612 613 614 615 616 | return err } case *partialNode: partial, err := getPartials(n.prov, n.name, n.indent) if err != nil { return err } | | | 610 611 612 613 614 615 616 617 618 619 620 621 622 623 624 | return err } case *partialNode: partial, err := getPartials(n.prov, n.name, n.indent) if err != nil { return err } if err := partial.renderTemplate(w, stack); err != nil { return err } } return nil } func (tmpl *Template) renderTemplate(w io.Writer, stack []reflect.Value) error { |
︙ | ︙ |
Changes to template/spec_test.go.
︙ | ︙ | |||
203 204 205 206 207 208 209 | if !ok { t.Errorf("Unexpected file %s, consider adding to enabledFiles", file) continue } if enabled == nil { continue } | | | | | | | | 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 | if !ok { t.Errorf("Unexpected file %s, consider adding to enabledFiles", file) continue } if enabled == nil { continue } b, err := os.ReadFile(path) if err != nil { t.Fatal(err) } var suite specTestSuite 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.
> > > > > > > > | 1 2 3 4 5 6 7 8 | title: Simple Test %%% No render %%% %%%{-} Render %%% |
Added testdata/content/cite/20200215204700.zettel.
> > > | 1 2 3 | title: Simple Test [@Stern18]{-} |
Added testdata/content/comment/20200215204700.zettel.
> > > > | 1 2 3 4 | title: Simple Test % No comment %% Comment |
Added testdata/content/descrlist/20200226122100.zettel.
> > > > > > > | 1 2 3 4 5 6 7 | title: Simple Test ; Zettel : Paper : Note ; Zettelkasten : Slip box |
Added testdata/content/edit/20200215204700.zettel.
> > > > > | 1 2 3 4 5 | title: Simple Test ~~delete~~{-} __insert__{-} ~~kill~~{-}__create__{-} |
Added testdata/content/embed/20200215204700.zettel.
> > > | 1 2 3 | title: Simple Test {{abc}} |
Added testdata/content/footnote/20200215204700.zettel.
> > > | 1 2 3 | title: Simple Test Text[^foot]{=sidebar} |
Added testdata/content/format/20200215204700.zettel.
> > > > > > > > > > > > > > > > > > > | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 | title: Simple Test role: zettel //italic// //emph//{-} **bold** **strong**{-} __unterline__ ~~strike~~ ''monospace'' ^^superscript^^ ,,subscript,, ""Quotes"" <<Quotation<< ;;small;; ::span:: ``code`` ++input++ ==output== |
Added testdata/content/format/20201107164400.zettel.
> > > > | 1 2 3 4 | title: Nested Lang role: zettel ::""abc""::{lang=fr} |
Added testdata/content/heading/20200215204700.zettel.
> > > | 1 2 3 | title: Simple Test === First |
Added testdata/content/hrule/20200215204700.zettel.
> > > | 1 2 3 | title: Simple Test --- |
Added testdata/content/link/20200215204700.zettel.
> > > > > > > > > > > | 1 2 3 4 5 6 7 8 9 10 11 | title: Simple Test [[Home|https://zettelstore.de/z]] [[https://zettelstore.de]] [[Config|00000000000100]] [[00000000000100]] [[Frag|#frag]] [[#frag]] [[H|/hosted]] [[B|//based]] [[R|../rel]] |
Added testdata/content/list/20200215204700.zettel.
> > > > > | 1 2 3 4 5 | title: Simple Test * Item 1 * Item 2 * Item 3 |
Added testdata/content/list/20200217194800.zettel.
> > > > > > > > | 1 2 3 4 5 6 7 8 | title: Second list * Item1.1 * Item1.2 * Item1.3 * Item2.1 * Item2.2 |
Added testdata/content/list/20200516105700.zettel.
> > > > > > > | 1 2 3 4 5 6 7 | title: Schachtelliste * T1 ** T2 * T3 ** T4 * T5 |
Added testdata/content/literal/20200215204700.zettel.
> > > > > | 1 2 3 4 5 | title: Simple Test ++input++ ``program`` ==output== |
Added testdata/content/mark/20200215204700.zettel.
> > > | 1 2 3 | title: Simple Test [!mark] |
Added testdata/content/paragraph/20200215185900.zettel.
> > > > | 1 2 3 4 | title: Simple Zettel tags: #test #simple This is a zettel for testing. |
Added testdata/content/paragraph/20200217151800.zettel.
> > > > > > > > | 1 2 3 4 5 6 7 8 | title: Continuation after Paragraph tags: #test #paragraph Text Text *abc Text Text * abc |
Added testdata/content/png/20200512180900.png.
cannot compute difference between binary files
Added testdata/content/quoteblock/20200215204700.zettel.
> > > > > | 1 2 3 4 5 | title: Simple Test <<< To be or not to be. <<< Romeo |
Added testdata/content/spanblock/20200215204700.zettel.
> > > > > > > | 1 2 3 4 5 6 7 | title: Simple Test ::: A simple span and much more ::: |
Added testdata/content/table/20200215204700.zettel.
> > > > | 1 2 3 4 | title: Simple Test |c1|c2|c3| |d1||d3 |
Added testdata/content/table/20200618140700.zettel.
> > > > > > | 1 2 3 4 5 6 | title: Testtable |h1>|=h2|h3:| |%--+---+---+ |<c1|c2|:c3| |f1|f2|=f3 |
Added testdata/content/verbatim/20200215204700.zettel.
> > > > > > > | 1 2 3 4 5 6 7 | title: Simple Test ``` if __name__ == "main": print("Hello, World") exit(0) ``` |
Added testdata/content/verseblock/20200215204700.zettel.
> > > > > > > > > > > | 1 2 3 4 5 6 7 8 9 10 11 | title: Simple Test """ A line another line Back Paragraph Spacy Para """ Author |
Changes to testdata/meta/title/20200310110300.zettel.
|
| | | 1 | title: A ""Title"" with //Markup//, ``Zettelmarkup``{=zmk} |
Deleted testdata/testbox/20211019200500.zettel.
|
| < < < < < < < < < |
Deleted testdata/testbox/20211020121000.zettel.
|
| < < < < < < < < |
Deleted testdata/testbox/20211020121100.zettel.
|
| < < < < < < |
Deleted testdata/testbox/20211020121145.zettel.
|
| < < < < < < |
Deleted testdata/testbox/20211020121300.zettel.
|
| < < < < < < |
Deleted testdata/testbox/20211020121400.zettel.
|
| < < < < < < |
Deleted testdata/testbox/20211020182600.zettel.
|
| < < < < < < < |
Deleted testdata/testbox/20211020183700.zettel.
|
| < < < < < < < |
Deleted testdata/testbox/20211020183800.zettel.
|
| < < < < < < |
Deleted testdata/testbox/20211020184300.zettel.
|
| < < < < < |
Deleted testdata/testbox/20211020184342.zettel.
|
| < < < < < < |
Deleted testdata/testbox/20211020185400.zettel.
|
| < < < < < < < < |
Deleted tests/client/client_test.go.
|
| < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < |
Deleted tests/client/crud_test.go.
|
| < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < |
Deleted tests/client/embed_test.go.
|
| < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < |
Changes to tests/markdown_test.go.
︙ | ︙ | |||
8 9 10 11 12 13 14 | // under this license. //----------------------------------------------------------------------------- // Package tests provides some higher-level tests. package tests import ( | < > > | | 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 | // under this license. //----------------------------------------------------------------------------- // Package tests provides some higher-level tests. package tests import ( "encoding/json" "fmt" "os" "regexp" "strings" "testing" "zettelstore.de/z/api" "zettelstore.de/z/ast" "zettelstore.de/z/encoder" _ "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" |
︙ | ︙ | |||
36 37 38 39 40 41 42 43 44 45 46 47 48 49 | Markdown string `json:"markdown"` HTML string `json:"html"` Example int `json:"example"` StartLine int `json:"start_line"` EndLine int `json:"end_line"` Section string `json:"section"` } func TestEncoderAvailability(t *testing.T) { t.Parallel() encoderMissing := false for _, enc := range encodings { enc := encoder.Create(enc, nil) if enc == nil { | > > > > > > > > > > > > > > > > > > > > > > > > > > > > | 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 | Markdown string `json:"markdown"` HTML string `json:"html"` Example int `json:"example"` StartLine int `json:"start_line"` EndLine int `json:"end_line"` Section string `json:"section"` } // exceptions lists all CommonMark tests that should not be tested for identical HTML output var exceptions = []string{ " - foo\n - bar\n\t - baz\n", // 9 "<script type=\"text/javascript\">\n// JavaScript example\n\ndocument.getElementById(\"demo\").innerHTML = \"Hello JavaScript!\";\n</script>\nokay\n", // 170 "<script>\nfoo\n</script>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 "<http://foo.bar.`baz>`\n", // 345 "[foo<http://example.com/?search=](uri)>\n", // 525 "[foo<http://example.com/?search=][ref]>\n\n[ref]: /uri\n", // 537 "<http://example.com?find=\\*>\n", // 581 "<http://foo.bar.baz/test?q=hello&id=22&boolean>\n", // 594 } var reHeadingID = regexp.MustCompile(` id="[^"]*"`) func TestEncoderAvailability(t *testing.T) { t.Parallel() encoderMissing := false for _, enc := range encodings { enc := encoder.Create(enc, nil) if enc == nil { |
︙ | ︙ | |||
62 63 64 65 66 67 68 | if err != nil { panic(err) } var testcases []markdownTestCase if err = json.Unmarshal(content, &testcases); err != nil { panic(err) } | > > > | | > > > | | | > > > > > > > > > > > > > > > > > > > > > > > > > | < | | > | < | | > | < | | > | > | 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 | if err != nil { panic(err) } 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(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 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(&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, "<h") > 0 { gotHTML = reHeadingID.ReplaceAllString(gotHTML, "") } if gotHTML != mdHTML { mdHTML = strings.ReplaceAll(mdHTML, "<li>\n", "<li>") 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 sb strings.Builder testID := tc.Example*100 + 1 t.Run(fmt.Sprintf("Encode zmk %14d", testID), func(st *testing.T) { zmkEncoder.WriteBlocks(&sb, ast) gotFirst := sb.String() sb.Reset() testID = tc.Example*100 + 2 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(gotFirst), nil, "zmk") zmkEncoder.WriteBlocks(&sb, thirdAst) gotThird := sb.String() sb.Reset() if gotSecond != gotThird { st.Errorf("\n1st: %q\n2nd: %q", gotSecond, gotThird) } }) } |
Changes to tests/regression_test.go.
︙ | ︙ | |||
8 9 10 11 12 13 14 | // under this license. //----------------------------------------------------------------------------- // Package tests provides some higher-level tests. package tests import ( | < > | > > > > > > > > | | | | | | | 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 | // under this license. //----------------------------------------------------------------------------- // Package tests provides some higher-level tests. package tests import ( "context" "fmt" "io" "net/url" "os" "path/filepath" "strings" "testing" "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, api.EncoderNative, api.EncoderText, } func getFileBoxes(wd, kind string) (root string, boxes []box.ManagedBox) { root = filepath.Clean(filepath.Join(wd, "..", "testdata", kind)) entries, err := os.ReadDir(root) if err != nil { panic(err) } cdata := manager.ConnectData{Config: testConfig, Enricher: &noEnrich{}, Notify: nil} for _, entry := range entries { if entry.IsDir() { u, err := url.Parse("dir://" + filepath.Join(root, entry.Name()) + "?type=" + kernel.BoxDirTypeSimple) if err != nil { panic(err) } box, err := manager.Connect(u, &noAuth{}, &cdata) if err != nil { panic(err) } boxes = append(boxes, box) } } return root, boxes } |
︙ | ︙ | |||
100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 | } gotContent = trimLastEOL(gotContent) wantContent = trimLastEOL(wantContent) if gotContent != wantContent { t.Errorf("\nWant: %q\nGot: %q", wantContent, gotContent) } } 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 checkMetaFile(t *testing.T, resultName string, zn *ast.ZettelNode, enc api.EncodingEnum) { t.Helper() if enc := encoder.Create(enc, nil); enc != nil { | > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > | | | | | | | | | | 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 | } gotContent = trimLastEOL(gotContent) 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 sb strings.Builder enc.WriteMeta(&sb, zn.Meta, parser.ParseMetadata) checkFileContent(t, resultName, sb.String()) return } panic(fmt.Sprintf("Unknown writer encoding %q", enc)) } func checkMetaBox(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", "meta", boxName, z.Zid.String()+"."+enc.String()) checkMetaFile(st, resultName, z, enc) }) } } 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 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 } func (*myConfig) GetMarkerExternal() string { return "" } func (*myConfig) GetSiteName() string { return "" } |
︙ | ︙ |
Added tests/result/content/blockcomment/20200215204700.djson.
> | 1 | [{"t":"CommentBlock","l":["No render"]},{"t":"CommentBlock","a":{"-":""},"l":["Render"]}] |
Added tests/result/content/blockcomment/20200215204700.html.
> > > | 1 2 3 | <!-- Render --> |
Added tests/result/content/blockcomment/20200215204700.native.
> > | 1 2 | [CommentBlock "No render"], [CommentBlock ("",[-]) "Render"] |
Added tests/result/content/blockcomment/20200215204700.text.
Added tests/result/content/cite/20200215204700.djson.
> | 1 | [{"t":"Para","i":[{"t":"Cite","a":{"-":""},"s":"Stern18"}]}] |
Added tests/result/content/cite/20200215204700.html.
> | 1 | <p>Stern18</p> |
Added tests/result/content/cite/20200215204700.native.
> | 1 | [Para Cite ("",[-]) "Stern18"] |
Added tests/result/content/cite/20200215204700.text.
Added tests/result/content/comment/20200215204700.djson.
> | 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.
> > | 1 2 | <p>% No comment <!-- Comment --></p> |
Added tests/result/content/comment/20200215204700.native.
> | 1 | [Para Text "%",Space,Text "No",Space,Text "comment",Space,Comment "Comment"] |
Added tests/result/content/comment/20200215204700.text.
> | 1 | % No comment |
Added tests/result/content/descrlist/20200226122100.djson.
> | 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.
> > > > > > > | 1 2 3 4 5 6 7 | <dl> <dt>Zettel</dt> <dd>Paper</dd> <dd>Note</dd> <dt>Zettelkasten</dt> <dd>Slip box</dd> </dl> |
Added tests/result/content/descrlist/20200226122100.native.
> > > > > > > > > | 1 2 3 4 5 6 7 8 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.
> > > > > | 1 2 3 4 5 | Zettel Paper Note Zettelkasten Slip box |
Added tests/result/content/edit/20200215204700.djson.
> | 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.
> > > | 1 2 3 | <p><del>delete</del> <ins>insert</ins> <del>kill</del><ins>create</ins></p> |
Added tests/result/content/edit/20200215204700.native.
> | 1 | [Para Delete [Text "delete"],Space,Insert [Text "insert"],Space,Delete [Text "kill"],Insert [Text "create"]] |
Added tests/result/content/edit/20200215204700.text.
> | 1 | delete insert killcreate |
Added tests/result/content/embed/20200215204700.djson.
> | 1 | [{"t":"Para","i":[{"t":"Embed","s":"abc"}]}] |
Added tests/result/content/embed/20200215204700.html.
> | 1 | <p><img src="abc" alt=""></p> |
Added tests/result/content/embed/20200215204700.native.
> | 1 | [Para Embed EXTERNAL "abc"] |
Added tests/result/content/embed/20200215204700.text.
Added tests/result/content/footnote/20200215204700.djson.
> | 1 | [{"t":"Para","i":[{"t":"Text","s":"Text"},{"t":"Footnote","a":{"":"sidebar"},"i":[{"t":"Text","s":"foot"}]}]}] |
Added tests/result/content/footnote/20200215204700.html.
> > > > | 1 2 3 4 | <p>Text<sup id="fnref:1"><a href="#fn:1" class="zs-footnote-ref" role="doc-noteref">1</a></sup></p> <ol class="zs-endnotes"> <li id="fn:1" role="doc-endnote">foot <a href="#fnref:1" class="zs-footnote-backref" role="doc-backlink">↩︎</a></li> </ol> |
Added tests/result/content/footnote/20200215204700.native.
> | 1 | [Para Text "Text",Footnote ("sidebar",[]) [Text "foot"]] |
Added tests/result/content/footnote/20200215204700.text.
> | 1 | Text foot |
Added tests/result/content/format/20200215204700.djson.
> | 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.
> > > > > > > > > > > > > > > > | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 | <p><i>italic</i> <em>emph</em> <b>bold</b> <strong>strong</strong> <u>unterline</u> <s>strike</s> <span style="font-family:monospace">monospace</span> <sup>superscript</sup> <sub>subscript</sub> "Quotes" <q>Quotation</q> <small>small</small> <span>span</span> <code>code</code> <kbd>input</kbd> <samp>output</samp></p> |
Added tests/result/content/format/20200215204700.native.
> | 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.
> | 1 | italic emph bold strong unterline strike monospace superscript subscript Quotes Quotation small span code input output |
Added tests/result/content/format/20201107164400.djson.
> | 1 | [{"t":"Para","i":[{"t":"Span","a":{"lang":"fr"},"i":[{"t":"Quote","i":[{"t":"Text","s":"abc"}]}]}]}] |
Added tests/result/content/format/20201107164400.html.
> | 1 | <p><span lang="fr">« abc »</span></p> |
Added tests/result/content/format/20201107164400.native.
> | 1 | [Para Span ("",[lang="fr"]) [Quote [Text "abc"]]] |
Added tests/result/content/format/20201107164400.text.
> | 1 | abc |
Added tests/result/content/heading/20200215204700.djson.
> | 1 | [{"t":"Heading","n":2,"s":"first","i":[{"t":"Text","s":"First"}]}] |
Added tests/result/content/heading/20200215204700.html.
> | 1 | <h2 id="first">First</h2> |
Added tests/result/content/heading/20200215204700.native.
> | 1 | [Heading 2 #first Text "First"] |
Added tests/result/content/heading/20200215204700.text.
> | 1 | First |
Added tests/result/content/hrule/20200215204700.djson.
> | 1 | [{"t":"Hrule"}] |
Added tests/result/content/hrule/20200215204700.html.
> | 1 | <hr> |
Added tests/result/content/hrule/20200215204700.native.
> | 1 | [Hrule] |
Added tests/result/content/hrule/20200215204700.text.
Added tests/result/content/link/20200215204700.djson.
> | 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.
> > > > > > > > > | 1 2 3 4 5 6 7 8 9 | <p><a href="https://zettelstore.de/z" class="zs-external">Home</a> <a href="https://zettelstore.de" class="zs-external">https://zettelstore.de</a> <a href="00000000000100">Config</a> <a href="00000000000100">00000000000100</a> <a href="#frag">Frag</a> <a href="#frag">#frag</a> <a href="/hosted">H</a> <a href="/based">B</a> <a href="../rel">R</a></p> |
Added tests/result/content/link/20200215204700.native.
> | 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.
> | 1 | Home Config Frag H B R |
Added tests/result/content/list/20200215204700.djson.
> | 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.
> > > > > | 1 2 3 4 5 | <ul> <li>Item 1</li> <li>Item 2</li> <li>Item 3</li> </ul> |
Added tests/result/content/list/20200215204700.native.
> > > > | 1 2 3 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.
> > > | 1 2 3 | Item 1 Item 2 Item 3 |
Added tests/result/content/list/20200217194800.djson.
> | 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.
> > > > > > > | 1 2 3 4 5 6 7 | <ul> <li>Item1.1</li> <li>Item1.2</li> <li>Item1.3</li> <li>Item2.1</li> <li>Item2.2</li> </ul> |
Added tests/result/content/list/20200217194800.native.
> > > > > > | 1 2 3 4 5 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.
> > > > > | 1 2 3 4 5 | Item1.1 Item1.2 Item1.3 Item2.1 Item2.2 |
Added tests/result/content/list/20200516105700.djson.
> | 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.
> > > > > > > > > > > > > > | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 | <ul> <li><p>T1</p> <ul> <li>T2</li> </ul> </li> <li><p>T3</p> <ul> <li>T4</li> </ul> </li> <li><p>T5</p> </li> </ul> |
Added tests/result/content/list/20200516105700.native.
> > > > > > > > | 1 2 3 4 5 6 7 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.
> > > > > | 1 2 3 4 5 | T1 T2 T3 T4 T5 |
Added tests/result/content/literal/20200215204700.djson.
> | 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.
> > > | 1 2 3 | <p><kbd>input</kbd> <code>program</code> <samp>output</samp></p> |
Added tests/result/content/literal/20200215204700.native.
> | 1 | [Para Input "input",Space,Code "program",Space,Output "output"] |
Added tests/result/content/literal/20200215204700.text.
> | 1 | input program output |
Added tests/result/content/mark/20200215204700.djson.
> | 1 | [{"t":"Para","i":[{"t":"Mark","s":"mark","q":"mark"}]}] |
Added tests/result/content/mark/20200215204700.html.
> | 1 | <p><a id="mark"></a></p> |
Added tests/result/content/mark/20200215204700.native.
> | 1 | [Para Mark "mark" #mark] |
Added tests/result/content/mark/20200215204700.text.
Added tests/result/content/paragraph/20200215185900.djson.
> | 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.
> | 1 | <p>This is a zettel for testing.</p> |
Added tests/result/content/paragraph/20200215185900.native.
> | 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.
> | 1 | This is a zettel for testing. |
Added tests/result/content/paragraph/20200217151800.djson.
> | 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.
> > > > > > | 1 2 3 4 5 6 | <p>Text Text *abc</p> <p>Text Text</p> <ul> <li>abc</li> </ul> |
Added tests/result/content/paragraph/20200217151800.native.
> > > > | 1 2 3 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.
> > > | 1 2 3 | Text Text *abc Text Text abc |
Added tests/result/content/png/20200512180900.djson.
> | 1 | [{"t":"Blob","q":"20200512180900","s":"png","o":"iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAAAAAA6fptVAAAACklEQVR4nGNiAAAABgADNjd8qAAAAABJRU5ErkJggg=="}] |
Added tests/result/content/png/20200512180900.html.
> | 1 | <img src="data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAAAAAA6fptVAAAACklEQVR4nGNiAAAABgADNjd8qAAAAABJRU5ErkJggg==" title="20200512180900"> |
Added tests/result/content/png/20200512180900.native.
> | 1 | [BLOB "20200512180900" "png" "iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAAAAAA6fptVAAAACklEQVR4nGNiAAAABgADNjd8qAAAAABJRU5ErkJggg=="] |
Added tests/result/content/png/20200512180900.text.
Added tests/result/content/quoteblock/20200215204700.djson.
> | 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.
> > > > | 1 2 3 4 | <blockquote> <p>To be or not to be.</p> <cite>Romeo</cite> </blockquote> |
Added tests/result/content/quoteblock/20200215204700.native.
> > > | 1 2 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.
> > | 1 2 | To be or not to be. Romeo |
Added tests/result/content/spanblock/20200215204700.djson.
> | 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.
> > > > > | 1 2 3 4 5 | <div> <p>A simple span and much more</p> </div> |
Added tests/result/content/spanblock/20200215204700.native.
> > | 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.
> | 1 | A simple span and much more |
Added tests/result/content/table/20200215204700.djson.
> | 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.
> > > > > > | 1 2 3 4 5 6 | <table> <tbody> <tr><td>c1</td><td>c2</td><td>c3</td></tr> <tr><td>d1</td><td></td><td>d3</td></tr> </tbody> </table> |
Added tests/result/content/table/20200215204700.native.
> > > | 1 2 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.
> > | 1 2 | c1 c2 c3 d1 d3 |
Added tests/result/content/table/20200618140700.djson.
> | 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.
> > > > > > > > > | 1 2 3 4 5 6 7 8 9 | <table> <thead> <tr><th style="text-align:right">h1</th><th>h2</th><th style="text-align:center">h3</th></tr> </thead> <tbody> <tr><td style="text-align:left">c1</td><td>c2</td><td style="text-align:center">c3</td></tr> <tr><td style="text-align:right">f1</td><td>f2</td><td style="text-align:center">=f3</td></tr> </tbody> </table> |
Added tests/result/content/table/20200618140700.native.
> > > > | 1 2 3 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.
> > > | 1 2 3 | h1 h2 h3 c1 c2 c3 f1 f2 =f3 |
Added tests/result/content/verbatim/20200215204700.djson.
> | 1 | [{"t":"CodeBlock","l":["if __name__ == \"main\":"," print(\"Hello, World\")","exit(0)"]}] |
Added tests/result/content/verbatim/20200215204700.html.
> > > > | 1 2 3 4 | <pre><code>if __name__ == "main": print("Hello, World") exit(0) </code></pre> |
Added tests/result/content/verbatim/20200215204700.native.
> | 1 | [CodeBlock "if __name__ == \"main\":\n print(\"Hello, World\")\nexit(0)"] |
Added tests/result/content/verbatim/20200215204700.text.
> > > | 1 2 3 | if __name__ == "main": print("Hello, World") exit(0) |
Added tests/result/content/verseblock/20200215204700.djson.
> | 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.
> > > > > > > > | 1 2 3 4 5 6 7 8 | <div> <p>A line<br>   another line<br> Back</p> <p>Paragraph</p> <p>    Spacy  Para</p> <cite>Author</cite> </div> |
Added tests/result/content/verseblock/20200215204700.native.
> > > > > | 1 2 3 4 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.
> > > > > > | 1 2 3 4 5 6 | A line   another line Back Paragraph     Spacy  Para Author |
Changes to tests/result/meta/title/20200310110300.djson.
|
| | | 1 | {"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"} |
Changes to tests/result/meta/title/20200310110300.native.
|
| | | 1 2 3 | [Title Text "A",Space,Quote [Text "Title"],Space,Text "with",Space,Italic [Text "Markup"],Text ",",Space,Code ("zmk",[]) "Zettelmarkup"] [Role "zettel"] [Syntax "zmk"] |
Changes to tools/build.go.
︙ | ︙ | |||
127 128 129 130 131 132 133 | func getVersion() string { base, vcs := getVersionData() return calcVersion(base, vcs) } func findExec(cmd string) string { if path, err := executeCommand(nil, "which", cmd); err == nil && path != "" { | | | | | < < < | 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 | func getVersion() string { base, vcs := getVersionData() return calcVersion(base, vcs) } func findExec(cmd string) string { if path, err := executeCommand(nil, "which", cmd); err == nil && path != "" { return path } return "" } 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 := checkGoVetShadow(); err != nil { return err } if err := checkStaticcheck(); err != nil { return err } return checkFossilExtra() } func checkGoTest(pkg string, testParams ...string) error { args := []string{"test", pkg} |
︙ | ︙ | |||
189 190 191 192 193 194 195 | if out != "" { fmt.Fprintln(os.Stderr, "Some lints failed") fmt.Fprint(os.Stderr, out) } return err } | | | | | < | < < < < | < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < | 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 | if out != "" { fmt.Fprintln(os.Stderr, "Some lints failed") fmt.Fprint(os.Stderr, out) } return err } func checkGoVetShadow() error { path := findExec("shadow") if path == "" { return nil } 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() 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 checkFossilExtra() error { out, err := executeCommand(nil, "fossil", "extra") if err != nil { fmt.Fprintln(os.Stderr, "Unable to execute 'fossil extra'") return err } if len(out) > 0 { |
︙ | ︙ | |||
285 286 287 288 289 290 291 | needServer := !addressInUse(":23123") if needServer { err = startZettelstore(&info) } if err != nil { return err } | | | 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 | needServer := !addressInUse(":23123") if needServer { err = startZettelstore(&info) } if err != nil { return err } err = checkGoTest("zettelstore.de/z/client", "-base-url", "http://127.0.0.1:23123") if needServer { err1 := stopZettelstore(&info) if err == nil { err = err1 } } return err |
︙ | ︙ | |||
425 426 427 428 429 430 431 | fmt.Fprintf(os.Stderr, "Warning: releasing a dirty version %v\n", fossil) base = base + dirtySuffix } return base, fossil } func cmdRelease() error { | | | 386 387 388 389 390 391 392 393 394 395 396 397 398 399 400 | fmt.Fprintf(os.Stderr, "Warning: releasing a dirty version %v\n", fossil) base = base + dirtySuffix } return base, fossil } func cmdRelease() error { if err := cmdCheck(); err != nil { return err } base, fossil := getReleaseVersionData() releases := []struct { arch string os string env []string |
︙ | ︙ | |||
534 535 536 537 538 539 540 | func cmdHelp() { fmt.Println(`Usage: go run tools/build.go [-v] COMMAND Options: -v Verbose output. Commands: | | | | | | | | < | | | | | 495 496 497 498 499 500 501 502 503 504 505 506 507 508 509 510 511 512 513 514 515 516 517 518 519 | func cmdHelp() { fmt.Println(`Usage: go run tools/build.go [-v] COMMAND 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 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 ( verbose bool ) |
︙ | ︙ | |||
574 575 576 577 578 579 580 | case "r", "re", "rel", "rele", "relea", "releas", "release": err = cmdRelease() case "cl", "cle", "clea", "clean": err = cmdClean() case "v", "ve", "ver", "vers", "versi", "versio", "version": fmt.Print(getVersion()) case "ch", "che", "chec", "check": | | < < | 534 535 536 537 538 539 540 541 542 543 544 545 546 547 548 | case "r", "re", "rel", "rele", "relea", "releas", "release": err = cmdRelease() 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() case "t", "te", "tes", "test", "testa", "testap", "testapi": cmdTestAPI() case "h", "he", "hel", "help": cmdHelp() default: fmt.Fprintf(os.Stderr, "Unknown command %q\n", args[0]) cmdHelp() |
︙ | ︙ |
Changes to usecase/authenticate.go.
︙ | ︙ | |||
12 13 14 15 16 17 18 | package usecase import ( "context" "math/rand" "time" | < | 12 13 14 15 16 17 18 19 20 21 22 23 24 25 | package usecase import ( "context" "math/rand" "time" "zettelstore.de/z/auth" "zettelstore.de/z/auth/cred" "zettelstore.de/z/domain/id" "zettelstore.de/z/domain/meta" "zettelstore.de/z/search" ) |
︙ | ︙ | |||
52 53 54 55 56 57 58 | defer addDelay(time.Now(), 500*time.Millisecond, 100*time.Millisecond) if identMeta == nil || err != nil { compensateCompare() return nil, err } | | | | | | | 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 | defer addDelay(time.Now(), 500*time.Millisecond, 100*time.Millisecond) if identMeta == nil || err != nil { compensateCompare() return nil, err } 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, err := uc.token.GetToken(identMeta, d, k) if err != nil { return nil, err } return token, nil } return nil, nil } compensateCompare() return nil, nil |
︙ | ︙ |
Changes to usecase/context.go.
︙ | ︙ | |||
10 11 12 13 14 15 16 | // Package usecase provides (business) use cases for the Zettelstore. package usecase import ( "context" | < < < < < < < | < | | | > > | | | < > > > > | > > | > | > | > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > | < < | | | < < < < < < < < < < < < < < < < < | < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < | < < < < < < < < < < < | < | | | | < | > | < < | | | | | < < < < | < < < < < < < < < < | 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 | // Package usecase provides (business) use cases for the Zettelstore. package usecase import ( "context" "zettelstore.de/z/domain/id" "zettelstore.de/z/domain/meta" ) // ZettelContextPort is the interface used by this use case. type ZettelContextPort interface { // GetMeta retrieves just the meta data of a specific zettel. GetMeta(ctx context.Context, zid id.Zid) (*meta.Meta, error) } // ZettelContext is the data for this use case. type ZettelContext struct { port ZettelContextPort } // NewZettelContext creates a new use case. func NewZettelContext(port ZettelContextPort) ZettelContext { return ZettelContext{port: port} } // ZettelContextDirection determines the way, the context is calculated. type ZettelContextDirection int // Constant values for ZettelContextDirection const ( _ ZettelContextDirection = iota ZettelContextForward // Traverse all forwarding links ZettelContextBackward // Traverse all backwaring links ZettelContextBoth // Traverse both directions ) // Run executes the use case. 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 := ztlCtx{depth: depth} tasks.add(start, 0) visited := id.NewSet() isBackward := dir == ZettelContextBoth || dir == ZettelContextBackward isForward := dir == ZettelContextBoth || dir == ZettelContextForward 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 } curDepth++ for _, p := range m.PairsRest(true) { 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 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 { zc.last.next = task zc.last = task } } 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 } |
Changes to usecase/copy_zettel.go.
︙ | ︙ | |||
8 9 10 11 12 13 14 | // under this license. //----------------------------------------------------------------------------- // Package usecase provides (business) use cases for the zettelstore. package usecase import ( | | | | | | | 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 | // under this license. //----------------------------------------------------------------------------- // Package usecase provides (business) use cases for the zettelstore. package usecase import ( "zettelstore.de/z/domain" "zettelstore.de/z/domain/meta" ) // CopyZettel is the data for this use case. type CopyZettel struct{} // NewCopyZettel creates a new use case. func NewCopyZettel() CopyZettel { return CopyZettel{} } // Run executes the use case. func (uc CopyZettel) Run(origZettel domain.Zettel) domain.Zettel { m := origZettel.Meta.Clone() if title, ok := m.Get(meta.KeyTitle); ok { if len(title) > 0 { title = "Copy of " + title } else { title = "Copy" } m.Set(meta.KeyTitle, title) } content := origZettel.Content content.TrimSpace() return domain.Zettel{Meta: m, Content: content} } |
Changes to usecase/create_zettel.go.
︙ | ︙ | |||
10 11 12 13 14 15 16 | // Package usecase provides (business) use cases for the zettelstore. package usecase import ( "context" | < > | 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 | // Package usecase provides (business) use cases for the zettelstore. package usecase import ( "context" "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. CreateZettel(ctx context.Context, zettel domain.Zettel) (id.Zid, error) } |
︙ | ︙ | |||
42 43 44 45 46 47 48 | // Run executes the use case. 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 } | > | | | | | | | 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 | // Run executes the use case. 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(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) } |
Changes to usecase/folge_zettel.go.
︙ | ︙ | |||
8 9 10 11 12 13 14 | // under this license. //----------------------------------------------------------------------------- // Package usecase provides (business) use cases for the zettelstore. package usecase import ( | < | | | | | | | | 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 | // under this license. //----------------------------------------------------------------------------- // Package usecase provides (business) use cases for the zettelstore. package usecase import ( "zettelstore.de/z/config" "zettelstore.de/z/domain" "zettelstore.de/z/domain/id" "zettelstore.de/z/domain/meta" ) // FolgeZettel is the data for this use case. type FolgeZettel struct { rtConfig config.Config } // NewFolgeZettel creates a new use case. func NewFolgeZettel(rtConfig config.Config) FolgeZettel { return FolgeZettel{rtConfig} } // 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(meta.KeyTitle); ok { if len(title) > 0 { title = "Folge of " + title } else { title = "Folge" } m.Set(meta.KeyTitle, title) } 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("")} } |
Changes to usecase/get_user.go.
︙ | ︙ | |||
10 11 12 13 14 15 16 | // Package usecase provides (business) use cases for the zettelstore. package usecase import ( "context" | < | 10 11 12 13 14 15 16 17 18 19 20 21 22 23 | // Package usecase provides (business) use cases for the zettelstore. package usecase import ( "context" "zettelstore.de/z/auth" "zettelstore.de/z/box" "zettelstore.de/z/domain/id" "zettelstore.de/z/domain/meta" "zettelstore.de/z/search" ) |
︙ | ︙ | |||
46 47 48 49 50 51 52 | func (uc GetUser) Run(ctx context.Context, ident string) (*meta.Meta, error) { ctx = box.NoEnrichContext(ctx) // 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()) | | | | | | | 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 | func (uc GetUser) Run(ctx context.Context, ident string) (*meta.Meta, error) { ctx = box.NoEnrichContext(ctx) // 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(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(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 { return nil, nil } |
︙ | ︙ | |||
92 93 94 95 96 97 98 | // GetUser executes the use case. func (uc GetUserByZid) GetUser(ctx context.Context, zid id.Zid, ident string) (*meta.Meta, error) { userMeta, err := uc.port.GetMeta(box.NoEnrichContext(ctx), zid) if err != nil { return nil, err } | | | 91 92 93 94 95 96 97 98 99 100 101 102 | // GetUser executes the use case. func (uc GetUserByZid) GetUser(ctx context.Context, zid id.Zid, ident string) (*meta.Meta, error) { userMeta, err := uc.port.GetMeta(box.NoEnrichContext(ctx), zid) if err != nil { return nil, err } if val, ok := userMeta.Get(meta.KeyUserID); !ok || val != ident { return nil, nil } return userMeta, nil } |
Changes to usecase/list_role.go.
︙ | ︙ | |||
11 12 13 14 15 16 17 | // Package usecase provides (business) use cases for the zettelstore. package usecase import ( "context" "sort" | < | 11 12 13 14 15 16 17 18 19 20 21 22 23 24 | // Package usecase provides (business) use cases for the zettelstore. package usecase import ( "context" "sort" "zettelstore.de/z/box" "zettelstore.de/z/domain/meta" "zettelstore.de/z/search" ) // ListRolePort is the interface used by this use case. type ListRolePort interface { |
︙ | ︙ | |||
41 42 43 44 45 46 47 | func (uc ListRole) Run(ctx context.Context) ([]string, error) { metas, err := uc.port.SelectMeta(box.NoEnrichContext(ctx), nil) if err != nil { return nil, err } roles := make(map[string]bool, 8) for _, m := range metas { | | | 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 | func (uc ListRole) Run(ctx context.Context) ([]string, error) { metas, err := uc.port.SelectMeta(box.NoEnrichContext(ctx), nil) if err != nil { return nil, err } roles := make(map[string]bool, 8) for _, m := range metas { if role, ok := m.Get(meta.KeyRole); ok && role != "" { roles[role] = true } } result := make([]string, 0, len(roles)) for role := range roles { result = append(result, role) } sort.Strings(result) return result, nil } |
Changes to usecase/list_tags.go.
︙ | ︙ | |||
10 11 12 13 14 15 16 | // Package usecase provides (business) use cases for the zettelstore. package usecase import ( "context" | < | 10 11 12 13 14 15 16 17 18 19 20 21 22 23 | // Package usecase provides (business) use cases for the zettelstore. package usecase import ( "context" "zettelstore.de/z/domain/meta" "zettelstore.de/z/search" ) // ListTagsPort is the interface used by this use case. type ListTagsPort interface { // SelectMeta returns all zettel meta data that match the selection criteria. |
︙ | ︙ | |||
42 43 44 45 46 47 48 | func (uc ListTags) Run(ctx context.Context, minCount int) (TagData, error) { metas, err := uc.port.SelectMeta(ctx, nil) if err != nil { return nil, err } result := make(TagData) for _, m := range metas { | | | 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 | func (uc ListTags) Run(ctx context.Context, minCount int) (TagData, error) { metas, err := uc.port.SelectMeta(ctx, nil) if err != nil { return nil, err } result := make(TagData) for _, m := range metas { if tl, ok := m.GetList(meta.KeyAllTags); ok && len(tl) > 0 { for _, t := range tl { result[t] = append(result[t], m) } } } if minCount > 1 { for t, ms := range result { |
︙ | ︙ |
Changes to usecase/new_zettel.go.
︙ | ︙ | |||
8 9 10 11 12 13 14 | // under this license. //----------------------------------------------------------------------------- // Package usecase provides (business) use cases for the zettelstore. package usecase import ( | < | | | | | | 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 | // under this license. //----------------------------------------------------------------------------- // Package usecase provides (business) use cases for the zettelstore. package usecase import ( "zettelstore.de/z/domain" "zettelstore.de/z/domain/id" "zettelstore.de/z/domain/meta" ) // NewZettel is the data for this use case. type NewZettel struct{} // NewNewZettel creates a new use case. func NewNewZettel() NewZettel { return NewZettel{} } // Run executes the use case. func (uc NewZettel) Run(origZettel domain.Zettel) domain.Zettel { m := meta.New(id.Invalid) om := origZettel.Meta 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) } } content := origZettel.Content content.TrimSpace() return domain.Zettel{Meta: m, Content: content} } |
Changes to usecase/order.go.
︙ | ︙ | |||
41 42 43 44 45 46 47 | start *meta.Meta, result []*meta.Meta, err error, ) { zn, err := uc.evaluate.Run(ctx, zid, syntax, nil) if err != nil { return nil, nil, err } for _, ref := range collect.Order(zn) { | | | | 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 | start *meta.Meta, result []*meta.Meta, err error, ) { zn, err := uc.evaluate.Run(ctx, zid, syntax, nil) if err != nil { return nil, nil, err } for _, ref := range collect.Order(zn) { 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 } |
Changes to usecase/update_zettel.go.
︙ | ︙ | |||
10 11 12 13 14 15 16 | // Package usecase provides (business) use cases for the zettelstore. package usecase import ( "context" | < > | 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 | // Package usecase provides (business) use cases for the zettelstore. package usecase import ( "context" "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. GetZettel(ctx context.Context, zid id.Zid) (domain.Zettel, error) |
︙ | ︙ | |||
45 46 47 48 49 50 51 | oldZettel, err := uc.port.GetZettel(box.NoEnrichContext(ctx), m.Zid) if err != nil { return err } if zettel.Equal(oldZettel, false) { return nil } | | | | 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 | oldZettel, err := uc.port.GetZettel(box.NoEnrichContext(ctx), m.Zid) if err != nil { return err } if zettel.Equal(oldZettel, false) { return nil } m.SetNow(meta.KeyModified) m.YamlSep = oldZettel.Meta.YamlSep if m.Zid == id.ConfigurationZid { m.Set(meta.KeySyntax, meta.ValueSyntaxNone) } if !hasContent { zettel.Content = oldZettel.Content zettel.Content.TrimSpace() } return uc.port.UpdateZettel(ctx, zettel) } |
Changes to web/adapter/api/api.go.
︙ | ︙ | |||
11 12 13 14 15 16 17 | // Package api provides api handlers for web requests. package api import ( "context" "time" | | | 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 | // Package api provides api handlers for web requests. package api import ( "context" "time" "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" ) |
︙ | ︙ |
Changes to web/adapter/api/content_type.go.
1 2 3 4 5 6 7 8 9 10 11 12 13 | //----------------------------------------------------------------------------- // 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 provides api handlers for web requests. package api | | | > | | | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 | //----------------------------------------------------------------------------- // 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 provides api handlers for web requests. package api import "zettelstore.de/z/api" const ctHTML = "text/html; charset=utf-8" const ctJSON = "application/json" const ctPlainText = "text/plain; charset=utf-8" var mapEncoding2CT = map[api.EncodingEnum]string{ api.EncoderHTML: ctHTML, api.EncoderNative: ctPlainText, api.EncoderDJSON: ctJSON, api.EncoderText: ctPlainText, api.EncoderZmk: ctPlainText, } func encoding2ContentType(enc api.EncodingEnum) string { ct, ok := mapEncoding2CT[enc] if !ok { return "application/octet-stream" } return ct } var mapSyntax2CT = map[string]string{ "css": "text/css; charset=utf-8", "gif": "image/gif", "html": "text/html; charset=utf-8", "jpeg": "image/jpeg", |
︙ | ︙ |
Changes to web/adapter/api/create_zettel.go.
︙ | ︙ | |||
10 11 12 13 14 15 16 | // Package api provides api handlers for web requests. package api import ( "net/http" | | | 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 | // Package api provides api handlers for web requests. package api import ( "net/http" zsapi "zettelstore.de/z/api" "zettelstore.de/z/domain/id" "zettelstore.de/z/usecase" "zettelstore.de/z/web/adapter" ) // MakePostCreatePlainZettelHandler creates a new HTTP handler to store content of // an existing zettel. |
︙ | ︙ | |||
32 33 34 35 36 37 38 | } newZid, err := createZettel.Run(ctx, zettel) if err != nil { adapter.ReportUsecaseError(w, err) return } | | > | | | 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 | } newZid, err := createZettel.Run(ctx, zettel) if err != nil { adapter.ReportUsecaseError(w, err) return } 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) } } } |
︙ | ︙ | |||
58 59 60 61 62 63 64 | } newZid, err := createZettel.Run(ctx, zettel) if err != nil { adapter.ReportUsecaseError(w, err) return } | | | > | | | 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 | } newZid, err := createZettel.Run(ctx, zettel) if err != nil { adapter.ReportUsecaseError(w, err) return } 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, zsapi.ZidJSON{ID: newZid.String()}); err != nil { adapter.InternalServerError(w, "Write JSON", err) } } } |
Changes to web/adapter/api/delete_zettel.go.
︙ | ︙ | |||
24 25 26 27 28 29 30 | return func(w http.ResponseWriter, r *http.Request) { zid, err := id.Parse(r.URL.Path[1:]) if err != nil { http.NotFound(w, r) return } | | | 24 25 26 27 28 29 30 31 32 33 34 35 36 37 | return func(w http.ResponseWriter, r *http.Request) { zid, err := id.Parse(r.URL.Path[1:]) if err != nil { http.NotFound(w, r) return } 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.
|
| < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < |
Changes to web/adapter/api/get_eval_zettel.go.
︙ | ︙ | |||
10 11 12 13 14 15 16 | // Package api provides api handlers for web requests. package api import ( "net/http" | | | | | | | < < < | > > | < | | 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 | // Package api provides api handlers for web requests. package api import ( "net/http" zsapi "zettelstore.de/z/api" "zettelstore.de/z/ast" "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" ) // MakeGetEvalZettelHandler creates a new HTTP handler to return a evaluated zettel. func (a *API) MakeGetEvalZettelHandler(evaluate usecase.Evaluate) 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 } ctx := r.Context() q := r.URL.Query() enc, encStr := adapter.GetEncoding(r, q, encoder.GetDefaultEncoding()) part := getPart(q, partContent) 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 { return evaluate.RunMetadata(ctx, value, &env) } a.writeEncodedZettelPart(w, zn, evalMeta, enc, encStr, part) } } |
Changes to web/adapter/api/get_links.go.
︙ | ︙ | |||
10 11 12 13 14 15 16 | // Package api provides api handlers for web requests. package api import ( "net/http" | | | | > > | | 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 | // Package api provides api handlers for web requests. package api import ( "net/http" 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" "zettelstore.de/z/web/adapter" ) // MakeGetLinksHandler creates a new API handler to return links to other material. func MakeGetLinksHandler(evaluate usecase.Evaluate) 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 } ctx := r.Context() q := r.URL.Query() zn, err := evaluate.Run(ctx, zid, q.Get(meta.KeySyntax), nil) if err != nil { adapter.ReportUsecaseError(w, err) return } summary := collect.References(zn) 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) { if meta.Type(p.Key) == meta.TypeURL { outData.Linked.Meta = append(outData.Linked.Meta, p.Value) } } zetRefs, locRefs, extRefs = collect.DivideReferences(summary.Embeds) outData.Embedded.Outgoing = idRefs(zetRefs) outData.Embedded.Local = stringRefs(locRefs) outData.Embedded.External = stringRefs(extRefs) outData.Cites = stringCites(summary.Cites) w.Header().Set(zsapi.HeaderContentType, ctJSON) encodeJSONData(w, outData) } } func idRefs(refs []*ast.Reference) []string { result := make([]string, len(refs)) for i, ref := range refs { |
︙ | ︙ |
Changes to web/adapter/api/get_order.go.
︙ | ︙ | |||
10 11 12 13 14 15 16 | // Package api provides api handlers for web requests. package api import ( "net/http" | | | | | 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 | // Package api provides api handlers for web requests. package api import ( "net/http" "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 // of a given zettel. func MakeGetOrderHandler(zettelOrder usecase.ZettelOrder) 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 } ctx := r.Context() q := r.URL.Query() start, metas, err := zettelOrder.Run(ctx, zid, q.Get(meta.KeySyntax)) if err != nil { adapter.ReportUsecaseError(w, err) return } writeMetaList(w, start, metas) } } |
Changes to web/adapter/api/get_parsed_zettel.go.
︙ | ︙ | |||
11 12 13 14 15 16 17 | // Package api provides api handlers for web requests. package api import ( "fmt" "net/http" | | > | | | | | 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 | // Package api provides api handlers for web requests. package api import ( "fmt" "net/http" 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" ) // MakeGetParsedZettelHandler creates a new HTTP handler to return a parsed zettel. func (a *API) MakeGetParsedZettelHandler(parseZettel usecase.ParseZettel) 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 } 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(meta.KeySyntax)) if err != nil { adapter.ReportUsecaseError(w, err) return } a.writeEncodedZettelPart(w, zn, parser.ParseMetadata, enc, encStr, part) } } func (a *API) writeEncodedZettelPart( w http.ResponseWriter, zn *ast.ZettelNode, evalMeta encoder.EvalMetaFunc, 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{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 } w.Header().Set(zsapi.HeaderContentType, encoding2ContentType(enc)) var err error switch part { case partZettel: _, err = encdr.WriteZettel(w, zn, evalMeta) case partMeta: _, err = encdr.WriteMeta(w, zn.InhMeta, evalMeta) case partContent: _, err = encdr.WriteContent(w, zn) } if err != nil { adapter.InternalServerError(w, "Write encoded zettel", err) } } |
Changes to web/adapter/api/get_role_list.go.
︙ | ︙ | |||
10 11 12 13 14 15 16 | // Package api provides api handlers for web requests. package api import ( "net/http" | | | | | 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 | // Package api provides api handlers for web requests. package api import ( "net/http" 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". func MakeListRoleHandler(listRole usecase.ListRole) http.HandlerFunc { return func(w http.ResponseWriter, r *http.Request) { roleList, err := listRole.Run(r.Context()) if err != nil { adapter.ReportUsecaseError(w, err) return } w.Header().Set(zsapi.HeaderContentType, ctJSON) encodeJSONData(w, zsapi.RoleListJSON{Roles: roleList}) } } |
Changes to web/adapter/api/get_tags_list.go.
︙ | ︙ | |||
11 12 13 14 15 16 17 | // Package api provides api handlers for web requests. package api import ( "net/http" "strconv" | | > | | | < | | 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 | // Package api provides api handlers for web requests. package api import ( "net/http" "strconv" 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". func MakeListTagsHandler(listTags usecase.ListTags) http.HandlerFunc { return func(w http.ResponseWriter, r *http.Request) { iMinCount, _ := strconv.Atoi(r.URL.Query().Get("min")) tagData, err := listTags.Run(r.Context(), iMinCount) if err != nil { adapter.ReportUsecaseError(w, err) return } w.Header().Set(zsapi.HeaderContentType, ctJSON) tagMap := make(map[string][]string, len(tagData)) for tag, metaList := range tagData { zidList := make([]string, 0, len(metaList)) for _, m := range metaList { zidList = append(zidList, m.Zid.String()) } tagMap[tag] = zidList } encodeJSONData(w, zsapi.TagListJSON{Tags: tagMap}) } } |
Changes to web/adapter/api/get_zettel.go.
︙ | ︙ | |||
11 12 13 14 15 16 17 | // Package api provides api handlers for web requests. package api import ( "context" "net/http" | | | | | | 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 | // Package api provides api handlers for web requests. package api import ( "context" "net/http" 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" "zettelstore.de/z/web/adapter" ) // MakeGetZettelHandler creates a new HTTP handler to return a zettel. func MakeGetZettelHandler(getZettel usecase.GetZettel) http.HandlerFunc { return func(w http.ResponseWriter, r *http.Request) { z, err := getZettelFromPath(r.Context(), w, r, getZettel) if err != nil { return } w.Header().Set(zsapi.HeaderContentType, ctJSON) content, encoding := z.Content.Encode() err = encodeJSONData(w, zsapi.ZettelJSON{ ID: z.Meta.Zid.String(), Meta: z.Meta.Map(), Encoding: encoding, Content: content, }) if err != nil { adapter.InternalServerError(w, "Write Zettel JSON", err) } |
︙ | ︙ | |||
60 61 62 63 64 65 66 | if err == nil { _, err = w.Write([]byte{'\n'}) } if err == nil { _, err = z.Content.Write(w) } case partMeta: | | | | 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 | if err == nil { _, err = w.Write([]byte{'\n'}) } if err == nil { _, err = z.Content.Write(w) } case partMeta: 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 { w.Header().Set(zsapi.HeaderContentType, ct) } _, err = z.Content.Write(w) } if err != nil { adapter.InternalServerError(w, "Write plain zettel", err) } } |
︙ | ︙ | |||
88 89 90 91 92 93 94 | z, err := getZettel.Run(ctx, zid) if err != nil { adapter.ReportUsecaseError(w, err) return domain.Zettel{}, err } return z, nil } | < < < < < < < < < < < < < < < < < < < < < < < < < | 88 89 90 91 92 93 94 | z, err := getZettel.Run(ctx, zid) if err != nil { adapter.ReportUsecaseError(w, err) return domain.Zettel{}, err } return z, nil } |
Changes to web/adapter/api/get_zettel_context.go.
︙ | ︙ | |||
10 11 12 13 14 15 16 | // Package api provides api handlers for web requests. package api import ( "net/http" | | | | | | 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 | // Package api provides api handlers for web requests. package api import ( "net/http" zsapi "zettelstore.de/z/api" "zettelstore.de/z/domain/id" "zettelstore.de/z/usecase" "zettelstore.de/z/web/adapter" ) // MakeZettelContextHandler creates a new HTTP handler for the use case "zettel context". func MakeZettelContextHandler(getContext usecase.ZettelContext) 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 } q := r.URL.Query() 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, zsapi.QueryKeyLimit) if !ok || limit < 0 { limit = 200 } ctx := r.Context() metaList, err := getContext.Run(ctx, zid, dir, depth, limit) if err != nil { adapter.ReportUsecaseError(w, err) return } writeMetaList(w, metaList[0], metaList[1:]) } } |
Changes to web/adapter/api/get_zettel_list.go.
︙ | ︙ | |||
8 9 10 11 12 13 14 | // under this license. //----------------------------------------------------------------------------- // Package api provides api handlers for web requests. package api import ( | < | < < < < < | | | | | < | | | 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 | // under this license. //----------------------------------------------------------------------------- // Package api provides api handlers for web requests. package api import ( "fmt" "net/http" zsapi "zettelstore.de/z/api" "zettelstore.de/z/config" "zettelstore.de/z/usecase" "zettelstore.de/z/web/adapter" ) // MakeListMetaHandler creates a new HTTP handler for the use case "list some zettel". func MakeListMetaHandler(listMeta usecase.ListMeta) http.HandlerFunc { return func(w http.ResponseWriter, r *http.Request) { ctx := r.Context() q := r.URL.Query() s := adapter.GetSearch(q) metaList, err := listMeta.Run(ctx, s) if err != nil { adapter.ReportUsecaseError(w, err) return } result := make([]zsapi.ZidMetaJSON, 0, len(metaList)) for _, m := range metaList { result = append(result, zsapi.ZidMetaJSON{ ID: m.Zid.String(), Meta: m.Map(), }) } w.Header().Set(zsapi.HeaderContentType, ctJSON) err = encodeJSONData(w, zsapi.ZettelListJSON{ List: result, }) if err != nil { adapter.InternalServerError(w, "Write Zettel list JSON", err) } } } // MakeListPlainHandler creates a new HTTP handler for the use case "list some zettel". func (a *API) MakeListPlainHandler(listMeta usecase.ListMeta) http.HandlerFunc { return func(w http.ResponseWriter, r *http.Request) { ctx := r.Context() q := r.URL.Query() s := adapter.GetSearch(q) metaList, err := listMeta.Run(ctx, s) if err != nil { adapter.ReportUsecaseError(w, err) return } 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 } } if err != nil { adapter.InternalServerError(w, "Write Zettel list plain", err) } } } |
Changes to web/adapter/api/json.go.
︙ | ︙ | |||
12 13 14 15 16 17 18 | package api import ( "encoding/json" "io" "net/http" | | < | | | | | | | | 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 | package api import ( "encoding/json" "io" "net/http" zsapi "zettelstore.de/z/api" "zettelstore.de/z/domain" "zettelstore.de/z/domain/id" "zettelstore.de/z/domain/meta" ) 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([]zsapi.ZidMetaJSON, len(metaList)) for i, m := range metaList { outList[i].ID = m.Zid.String() outList[i].Meta = m.Map() } 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 zsapi.ZettelDataJSON if err := dec.Decode(&zettelData); err != nil { return zettel, err } m := meta.New(zid) for k, v := range zettelData.Meta { m.Set(k, v) } zettel.Meta = m if err := zettel.Content.SetDecoded(zettelData.Content, zettelData.Encoding); err != nil { return zettel, err } return zettel, nil } |
Changes to web/adapter/api/login.go.
︙ | ︙ | |||
12 13 14 15 16 17 18 | package api import ( "encoding/json" "net/http" "time" | | | | | | | | 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 | package api import ( "encoding/json" "net/http" "time" 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() { w.Header().Set(zsapi.HeaderContentType, ctJSON) writeJSONToken(w, "freeaccess", 24*366*10*time.Hour) return } var token []byte if ident, cred := retrieveIdentCred(r); ident != "" { var err error token, err = ucAuth.Run(r.Context(), ident, cred, a.tokenLifetime, auth.KindJSON) if err != nil { adapter.ReportUsecaseError(w, err) return } } if len(token) == 0 { w.Header().Set("WWW-Authenticate", `Bearer realm="Default"`) http.Error(w, "Authentication failed", http.StatusUnauthorized) return } w.Header().Set(zsapi.HeaderContentType, ctJSON) writeJSONToken(w, string(token), a.tokenLifetime) } } func retrieveIdentCred(r *http.Request) (string, string) { if ident, cred, ok := adapter.GetCredentialsViaForm(r); ok { return ident, cred } if ident, cred, ok := r.BasicAuth(); ok { return ident, cred } return "", "" } func writeJSONToken(w http.ResponseWriter, token string, lifetime time.Duration) { je := json.NewEncoder(w) je.Encode(zsapi.AuthJSON{ Token: token, Type: "Bearer", Expires: int(lifetime / time.Second), }) } // MakeRenewAuthHandler creates a new HTTP handler to renew the authenticate of a user. func (a *API) MakeRenewAuthHandler() http.HandlerFunc { return func(w http.ResponseWriter, r *http.Request) { ctx := r.Context() authData := a.getAuthData(ctx) if authData == nil || len(authData.Token) == 0 || authData.User == nil { adapter.BadRequest(w, "Not authenticated") return } 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 { w.Header().Set(zsapi.HeaderContentType, ctJSON) writeJSONToken(w, string(authData.Token), totalLifetime-currentLifetime) return } // Token is a little bit aged. Create a new one token, err := a.getToken(authData.User) if err != nil { adapter.ReportUsecaseError(w, err) return } w.Header().Set(zsapi.HeaderContentType, ctJSON) writeJSONToken(w, string(token), a.tokenLifetime) } } |
Changes to web/adapter/api/rename_zettel.go.
︙ | ︙ | |||
11 12 13 14 15 16 17 | // Package api provides api handlers for web requests. package api import ( "net/http" "net/url" | | | | > > | | | 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 | // Package api provides api handlers for web requests. package api import ( "net/http" "net/url" "zettelstore.de/z/api" "zettelstore.de/z/domain/id" "zettelstore.de/z/usecase" "zettelstore.de/z/web/adapter" ) // MakeRenameZettelHandler creates a new HTTP handler to update a zettel. func MakeRenameZettelHandler(renameZettel usecase.RenameZettel) 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 } 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 { adapter.ReportUsecaseError(w, err) return } w.WriteHeader(http.StatusNoContent) } } func getDestinationZid(r *http.Request) (id.Zid, bool) { if values, ok := r.Header[api.HeaderDestination]; ok { for _, value := range values { 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) < zidLength { return id.Invalid, false } zid, err := id.Parse(u.Path[len(u.Path)-zidLength:]) if err != nil { return id.Invalid, false } return zid, true } |
Changes to web/adapter/api/request.go.
︙ | ︙ | |||
12 13 14 15 16 17 18 | package api import ( "io" "net/http" "net/url" | | | 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 | package api import ( "io" "net/http" "net/url" "zettelstore.de/z/api" "zettelstore.de/z/domain" "zettelstore.de/z/domain/id" "zettelstore.de/z/domain/meta" "zettelstore.de/z/input" ) type partType int |
︙ | ︙ | |||
65 66 67 68 69 70 71 | } func buildZettelFromPlainData(r *http.Request, zid id.Zid) (domain.Zettel, error) { b, err := io.ReadAll(r.Body) if err != nil { return domain.Zettel{}, err } | | | 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 | } 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(string(b)) m := meta.NewFromInput(zid, inp) return domain.Zettel{ Meta: m, Content: domain.NewContent(inp.Src[inp.Pos:]), }, nil } |
Changes to web/adapter/api/update_zettel.go.
︙ | ︙ | |||
28 29 30 31 32 33 34 | return } zettel, err := buildZettelFromPlainData(r, zid) if err != nil { adapter.ReportUsecaseError(w, adapter.NewErrBadRequest(err.Error())) return } | | | | 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 | return } 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 { adapter.ReportUsecaseError(w, err) return } w.WriteHeader(http.StatusNoContent) } } // MakeUpdateZettelHandler creates a new HTTP handler to update a zettel. func MakeUpdateZettelHandler(updateZettel usecase.UpdateZettel) 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 } 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 { adapter.ReportUsecaseError(w, err) return } w.WriteHeader(http.StatusNoContent) } } |
Changes to web/adapter/request.go.
︙ | ︙ | |||
14 15 16 17 18 19 20 | import ( "log" "net/http" "net/url" "strconv" "strings" | | | 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 | import ( "log" "net/http" "net/url" "strconv" "strings" "zettelstore.de/z/api" "zettelstore.de/z/domain/meta" "zettelstore.de/z/search" "zettelstore.de/z/usecase" ) // GetCredentialsViaForm retrieves the authentication credentions from a form. func GetCredentialsViaForm(r *http.Request) (ident, cred string, ok bool) { |
︙ | ︙ | |||
65 66 67 68 69 70 71 | } return defEncoding, defEncoding.String() } func getOneEncoding(r *http.Request, key string) (string, bool) { if values, ok := r.Header[key]; ok { for _, value := range values { | | | 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 | } return defEncoding, defEncoding.String() } func getOneEncoding(r *http.Request, key string) (string, bool) { if values, ok := r.Header[key]; ok { for _, value := range values { if enc, ok := contentType2encoding(value); ok { return enc, true } } } return "", false } |
︙ | ︙ |
Changes to web/adapter/response.go.
1 | //----------------------------------------------------------------------------- | | | > < < < < < < < < < < | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 | //----------------------------------------------------------------------------- // 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 // under this license. //----------------------------------------------------------------------------- // Package adapter provides handlers for web requests. package adapter import ( "errors" "fmt" "log" "net/http" "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" ) // 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) } http.Error(w, text, code) } // ErrBadRequest is returned if the caller made an invalid HTTP request. type ErrBadRequest struct { Text string } |
︙ | ︙ | |||
78 79 80 81 82 83 84 | return http.StatusConflict, "Zettelstore operations conflicted" } 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 { | | | | 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 | return http.StatusConflict, "Zettelstore operations conflicted" } 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(meta.KeyTags, s) ref := ast.ParseReference(u.String()) ref.State = ast.RefStateHosted return ref } // CreateHostedReference builds a reference with state "hosted". func CreateHostedReference(b server.Builder, s string) *ast.Reference { urlPrefix := b.GetURLPrefix() ref := ast.ParseReference(urlPrefix + s) ref.State = ast.RefStateHosted 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(zid) if part != "" { ub.AppendQuery(api.QueryKeyPart, part) } if enc != "" { ub.AppendQuery(api.QueryKeyEncoding, enc) } if fragment != "" { |
︙ | ︙ |
Changes to web/adapter/webui/create_zettel.go.
︙ | ︙ | |||
12 13 14 15 16 17 18 | package webui import ( "context" "fmt" "net/http" | | > > | 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 | package webui import ( "context" "fmt" "net/http" "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" ) // MakeGetCopyZettelHandler creates a new HTTP handler to display the // HTML edit view of a copied zettel. |
︙ | ︙ | |||
62 63 64 65 66 67 68 | ctx := r.Context() origZettel, err := getOrigZettel(ctx, r, getZettel, "New") if err != nil { wui.reportError(ctx, w, err) return } m := origZettel.Meta | | | 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 | ctx := r.Context() origZettel, err := getOrigZettel(ctx, r, getZettel, "New") if err != nil { wui.reportError(ctx, w, err) return } m := origZettel.Meta 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 } env := encoder.Environment{Lang: config.GetLang(m, wui.rtConfig)} htmlTitle, err := encodeInlines(title, api.EncoderHTML, &env) |
︙ | ︙ | |||
112 113 114 115 116 117 118 | ctx := r.Context() user := wui.getUser(ctx) 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, | | | | 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 | ctx := r.Context() user := wui.getUser(ctx) 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(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(), }) } |
︙ | ︙ | |||
142 143 144 145 146 147 148 | } newZid, err := createZettel.Run(ctx, zettel) if err != nil { wui.reportError(ctx, w, err) return } | | | 144 145 146 147 148 149 150 151 152 153 | } newZid, err := createZettel.Run(ctx, zettel) if err != nil { wui.reportError(ctx, w, err) return } redirectFound(w, r, wui.NewURLBuilder('h').SetZid(newZid)) } } |
Changes to web/adapter/webui/delete_zettel.go.
︙ | ︙ | |||
10 11 12 13 14 15 16 | // Package webui provides web-UI handlers for web requests. package webui import ( "fmt" "net/http" | < | | < < < < | < < < < < < < < < < > | | < < < < | | < < < < | < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < | 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 | // Package webui provides web-UI handlers for web requests. package webui import ( "fmt" "net/http" "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" ) // MakeGetDeleteZettelHandler creates a new HTTP handler to display the // HTML delete view of a zettel. 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))) return } zid, err := id.Parse(r.URL.Path[1:]) if err != nil { wui.reportError(ctx, w, box.ErrNotFound) return } zettel, err := getZettel.Run(ctx, zid) if err != nil { wui.reportError(ctx, w, err) return } 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 }{ Zid: zid.String(), MetaPairs: m.Pairs(true), }) } } // MakePostDeleteZettelHandler creates a new HTTP handler to delete a zettel. func (wui *WebUI) MakePostDeleteZettelHandler(deleteZettel usecase.DeleteZettel) 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) return } if err := deleteZettel.Run(r.Context(), zid); err != nil { wui.reportError(ctx, w, err) return } redirectFound(w, r, wui.NewURLBuilder('/')) } } |
Changes to web/adapter/webui/edit_zettel.go.
︙ | ︙ | |||
11 12 13 14 15 16 17 | // Package webui provides web-UI handlers for web requests. package webui import ( "fmt" "net/http" | | > | 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 | // Package webui provides web-UI handlers for web requests. package webui import ( "fmt" "net/http" "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 // HTML edit view of a zettel. func (wui *WebUI) MakeEditGetZettelHandler(getZettel usecase.GetZettel) http.HandlerFunc { |
︙ | ︙ | |||
48 49 50 51 52 53 54 | user := wui.getUser(ctx) 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, | | | | | | 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 | user := wui.getUser(ctx) 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(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(), }) } } |
︙ | ︙ | |||
76 77 78 79 80 81 82 | zettel, hasContent, err := parseZettelForm(r, zid) if err != nil { wui.reportError(ctx, w, adapter.NewErrBadRequest("Unable to read zettel form")) return } | | | | 77 78 79 80 81 82 83 84 85 86 87 88 89 90 | zettel, hasContent, err := parseZettelForm(r, zid) 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 { wui.reportError(ctx, w, err) return } redirectFound(w, r, wui.NewURLBuilder('h').SetZid(zid)) } } |
Changes to web/adapter/webui/forms.go.
︙ | ︙ | |||
8 9 10 11 12 13 14 | // under this license. //----------------------------------------------------------------------------- // Package webui provides web-UI handlers for web requests. package webui import ( | < < < < < < < | < | | | | | | < < < | | 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 | // under this license. //----------------------------------------------------------------------------- // Package webui provides web-UI handlers for web requests. package webui import ( "net/http" "strings" "zettelstore.de/z/domain" "zettelstore.de/z/domain/id" "zettelstore.de/z/domain/meta" "zettelstore.de/z/input" ) type formZettelData struct { Heading string MetaTitle string MetaRole string MetaTags string MetaSyntax string MetaPairsRest []meta.Pair IsTextContent bool Content string } 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(postMeta)) } else { m = meta.New(zid) } if postTitle, ok := trimmedFormValue(r, "title"); ok { m.Set(meta.KeyTitle, postTitle) } if postTags, ok := trimmedFormValue(r, "tags"); ok { if tags := strings.Fields(postTags); len(tags) > 0 { m.SetList(meta.KeyTags, tags) } } if postRole, ok := trimmedFormValue(r, "role"); ok { m.Set(meta.KeyRole, postRole) } if postSyntax, ok := trimmedFormValue(r, "syntax"); ok { m.Set(meta.KeySyntax, postSyntax) } if values, ok := r.PostForm["content"]; ok && len(values) > 0 { return domain.Zettel{ Meta: m, Content: domain.NewContent( strings.ReplaceAll(strings.TrimSpace(values[0]), "\r\n", "\n")), }, true, nil } return domain.Zettel{ Meta: m, Content: domain.NewContent(""), }, false, nil } func trimmedFormValue(r *http.Request, key string) (string, bool) { if values, ok := r.PostForm[key]; ok && len(values) > 0 { value := strings.TrimSpace(values[0]) if len(value) > 0 { return value, true } } return "", false } |
Changes to web/adapter/webui/get_info.go.
︙ | ︙ | |||
8 9 10 11 12 13 14 | // under this license. //----------------------------------------------------------------------------- // Package webui provides web-UI handlers for web requests. package webui import ( | < > | | | | 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 | // under this license. //----------------------------------------------------------------------------- // Package webui provides web-UI handlers for web requests. package webui import ( "context" "fmt" "net/http" "sort" "strings" "zettelstore.de/z/api" "zettelstore.de/z/ast" "zettelstore.de/z/box" "zettelstore.de/z/collect" "zettelstore.de/z/config" "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" ) type metaDataInfo struct { |
︙ | ︙ | |||
59 60 61 62 63 64 65 | zid, err := id.Parse(r.URL.Path[1:]) if err != nil { wui.reportError(ctx, w, box.ErrNotFound) return } | | > < < < | | < < < < < < | < | 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 | zid, err := id.Parse(r.URL.Path[1:]) if err != nil { wui.reportError(ctx, w, box.ErrNotFound) return } 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) }, } 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 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 = "" } textTitle := wui.encodeTitleAsText(ctx, zn.InhMeta, evaluate) user := wui.getUser(ctx) canCreate := wui.canCreate(ctx, user) var base baseData wui.makeBaseData(ctx, lang, textTitle, user, &base) wui.renderTemplate(ctx, w, id.InfoTemplateZid, &base, struct { Zid string WebURL string ContextURL string CanWrite bool |
︙ | ︙ | |||
138 139 140 141 142 143 144 | EvalMatrix []matrixLine ParseMatrix []matrixLine HasShadowLinks bool ShadowLinks []string Endnotes string }{ Zid: zid.String(), | | | | | | | | | 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 | EvalMatrix []matrixLine ParseMatrix []matrixLine HasShadowLinks bool ShadowLinks []string Endnotes string }{ Zid: zid.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(zid).String(), CanFolge: canCreate, FolgeURL: wui.NewURLBuilder('f').SetZid(zid).String(), CanCopy: canCreate && !zn.Content.IsBinary(), CopyURL: wui.NewURLBuilder('c').SetZid(zid).String(), CanRename: wui.canRename(ctx, user, zn.Meta), RenameURL: wui.NewURLBuilder('b').SetZid(zid).String(), CanDelete: wui.canDelete(ctx, user, zn.Meta), 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, ExtLinks: extLinks, ExtNewWindow: htmlAttrNewWindow(len(extLinks) > 0), |
︙ | ︙ | |||
199 200 201 202 203 204 205 | encodings := encoder.GetEncodings() encTexts := make([]string, 0, len(encodings)) for _, f := range encodings { encTexts = append(encTexts, f.String()) } sort.Strings(encTexts) defEncoding := encoder.GetDefaultEncoding().String() | | | < | > | | < < < < < < | | 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 | encodings := encoder.GetEncodings() encTexts := make([]string, 0, len(encodings)) for _, f := range encodings { encTexts = append(encTexts, f.String()) } sort.Strings(encTexts) defEncoding := encoder.GetDefaultEncoding().String() parts := []string{api.PartZettel, api.PartMeta, api.PartContent} matrix := make([]matrixLine, 0, len(parts)) 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 { u.AppendQuery(api.QueryKeyEncoding, enc) } row[j] = simpleLink{enc, u.String()} u.ClearQuery() } matrix = append(matrix, matrixLine{part, row}) } return matrix } func (wui *WebUI) infoAPIMatrixPlain(key byte, zid id.Zid) []matrixLine { matrix := wui.infoAPIMatrix(key, zid) // Append plain and JSON format 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(zid) matrix[0].Elements = append(matrix[0].Elements, simpleLink{"json", u.String()}) return matrix } 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(meta.KeyBoxNumber); ok { result = append(result, boxNo) } } return result } |
Changes to web/adapter/webui/get_zettel.go.
︙ | ︙ | |||
11 12 13 14 15 16 17 18 | // Package webui provides web-UI handlers for web requests. package webui import ( "bytes" "errors" "net/http" | > | < > < < < | | | | | | < | 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 | // Package webui provides web-UI handlers for web requests. package webui import ( "bytes" "errors" "net/http" "strings" "zettelstore.de/z/api" "zettelstore.de/z/ast" "zettelstore.de/z/box" "zettelstore.de/z/config" "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" ) // MakeGetHTMLZettelHandler creates a new HTTP handler for the use case "get zettel". func (wui *WebUI) MakeGetHTMLZettelHandler(evaluate *usecase.Evaluate, 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) 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) }, } zn, err := evaluate.Run(ctx, zid, q.Get(meta.KeySyntax), &env) if err != nil { wui.reportError(ctx, w, err) return } evalMeta := func(value string) *ast.InlineListNode { return evaluate.RunMetadata(ctx, value, &env) } lang := config.GetLang(zn.InhMeta, wui.rtConfig) envHTML := encoder.Environment{ Lang: lang, Xhtml: false, MarkerExternal: wui.rtConfig.GetMarkerExternal(), NewWindow: 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 } textTitle := wui.encodeTitleAsText(ctx, zn.InhMeta, evaluate) htmlTitle := wui.encodeTitleAsHTML(ctx, zn.InhMeta, evaluate, &env, &envHTML) htmlContent, err := encodeBlocks(zn.Ast, api.EncoderHTML, &envHTML) if err != nil { wui.reportError(ctx, w, err) return } user := wui.getUser(ctx) 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(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 CanWrite bool EditURL string |
︙ | ︙ | |||
118 119 120 121 122 123 124 | HasFolgeLinks bool FolgeLinks []simpleLink HasBackLinks bool BackLinks []simpleLink }{ HTMLTitle: htmlTitle, CanWrite: wui.canWrite(ctx, user, zn.Meta, zn.Content), | | | | | | | 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 | HasFolgeLinks bool FolgeLinks []simpleLink HasBackLinks bool BackLinks []simpleLink }{ HTMLTitle: htmlTitle, CanWrite: wui.canWrite(ctx, user, zn.Meta, zn.Content), EditURL: wui.NewURLBuilder('e').SetZid(zid).String(), Zid: zid.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(zid).String(), CanFolge: canCreate, 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, FolgeLinks: folgeLinks, HasBackLinks: len(backLinks) > 0, |
︙ | ︙ | |||
155 156 157 158 159 160 161 | return "", nil } encdr := encoder.Create(enc, env) if encdr == nil { return "", errNoSuchEncoding } | | | | | | | | | | | | 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 | return "", nil } encdr := encoder.Create(enc, env) if encdr == nil { return "", errNoSuchEncoding } var content strings.Builder _, err := encdr.WriteInlines(&content, is) if err != nil { return "", err } 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 content strings.Builder _, err := encdr.WriteBlocks(&content, bln) if err != nil { return "", err } return content.String(), nil } func encodeMeta( m *meta.Meta, evalMeta encoder.EvalMetaFunc, enc api.EncodingEnum, env *encoder.Environment, ) (string, error) { encdr := encoder.Create(enc, env) if encdr == nil { return "", errNoSuchEncoding } var content strings.Builder _, err := encdr.WriteMeta(&content, m, evalMeta) if err != nil { return "", err } return content.String(), nil } func (wui *WebUI) buildTagInfos(m *meta.Meta) []simpleLink { var tagInfos []simpleLink 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() } } |
︙ | ︙ | |||
221 222 223 224 225 226 227 | } func (wui *WebUI) encodeZettelLinks(m *meta.Meta, key string, getTextTitle getTextTitleFunc) []simpleLink { values, ok := m.GetList(key) if !ok || len(values) == 0 { return nil } | < < < < | | 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 | } func (wui *WebUI) encodeZettelLinks(m *meta.Meta, key string, getTextTitle getTextTitleFunc) []simpleLink { values, ok := m.GetList(key) if !ok || len(values) == 0 { return nil } 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(zid).String() if title == "" { result = append(result, simpleLink{Text: val, URL: url}) } else { result = append(result, simpleLink{Text: title, URL: url}) } } } return result } |
Changes to web/adapter/webui/home.go.
︙ | ︙ | |||
12 13 14 15 16 17 18 | package webui import ( "context" "errors" "net/http" | < < | | | 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 | package webui import ( "context" "errors" "net/http" "zettelstore.de/z/box" "zettelstore.de/z/domain/id" "zettelstore.de/z/domain/meta" ) type getRootStore interface { // GetMeta retrieves just the meta data of a specific zettel. GetMeta(ctx context.Context, zid id.Zid) (*meta.Meta, error) } // MakeGetRootHandler creates a new HTTP handler to show the root URL. func (wui *WebUI) MakeGetRootHandler(s getRootStore) http.HandlerFunc { return func(w http.ResponseWriter, r *http.Request) { ctx := r.Context() if r.URL.Path != "/" { wui.reportError(ctx, w, box.ErrNotFound) return } homeZid := wui.rtConfig.GetHomeZettel() if homeZid != id.DefaultHomeZid { if _, err := s.GetMeta(ctx, homeZid); err == nil { 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(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')) } } |
Changes to web/adapter/webui/htmlmeta.go.
︙ | ︙ | |||
15 16 17 18 19 20 21 | "context" "errors" "fmt" "io" "net/url" "time" | | < < < | | < > | 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 | "context" "errors" "fmt" "io" "net/url" "time" "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" "zettelstore.de/z/evaluator" "zettelstore.de/z/strfun" "zettelstore.de/z/usecase" ) var space = []byte{' '} func (wui *WebUI) writeHTMLMetaValue( ctx context.Context, w io.Writer, key, value string, getTextTitle getTextTitleFunc, evaluate *usecase.Evaluate, envEval *evaluator.Environment, envEnc *encoder.Environment, ) { switch kt := meta.Type(key); kt { case meta.TypeBool: wui.writeHTMLBool(w, key, value) case meta.TypeCredential: writeCredential(w, value) |
︙ | ︙ | |||
66 67 68 69 70 71 72 | case meta.TypeURL: writeURL(w, value) case meta.TypeWord: wui.writeWord(w, key, value) case meta.TypeWordSet: wui.writeWordSet(w, key, meta.ListFromValue(value)) case meta.TypeZettelmarkup: | | | 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 | case meta.TypeURL: writeURL(w, value) 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(ctx, value, evaluate, envEval, api.EncoderHTML, envEnc)) default: strfun.HTMLEscape(w, value, false) fmt.Fprintf(w, " <b>(Unhandled type: %v, key: %v)</b>", kt, key) } } func (wui *WebUI) writeHTMLBool(w io.Writer, key, value string) { |
︙ | ︙ | |||
98 99 100 101 102 103 104 | if err != nil { strfun.HTMLEscape(w, val, false) return } title, found := getTextTitle(zid) switch { case found > 0: | < | | | 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 | if err != nil { strfun.HTMLEscape(w, val, false) return } title, found := getTextTitle(zid) switch { case found > 0: if title == "" { fmt.Fprintf(w, "<a href=\"%v\">%v</a>", wui.NewURLBuilder('h').SetZid(zid), zid) } else { fmt.Fprintf(w, "<a href=\"%v\" title=\"%v\">%v</a>", wui.NewURLBuilder('h').SetZid(zid), title, zid) } case found == 0: fmt.Fprintf(w, "<s>%v</s>", val) case found < 0: io.WriteString(w, val) } } |
︙ | ︙ | |||
195 196 197 198 199 200 201 | func (wui *WebUI) encodeTitleAsHTML( ctx context.Context, m *meta.Meta, evaluate *usecase.Evaluate, envEval *evaluator.Environment, envHTML *encoder.Environment, ) string { plainTitle := config.GetTitle(m, wui.rtConfig) | | < < < < < | < < < < < | > | | | | | 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 | func (wui *WebUI) encodeTitleAsHTML( ctx context.Context, m *meta.Meta, evaluate *usecase.Evaluate, envEval *evaluator.Environment, envHTML *encoder.Environment, ) string { plainTitle := config.GetTitle(m, wui.rtConfig) 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(ctx, plainTitle, evaluate, nil, api.EncoderText, nil) } func encodeZmkMetadata( ctx context.Context, value string, evaluate *usecase.Evaluate, envEval *evaluator.Environment, enc api.EncodingEnum, envHTML *encoder.Environment, ) string { iln := evaluate.RunMetadata(ctx, value, envEval) if iln.IsEmpty() { return "" } result, err := encodeInlines(iln, enc, envHTML) if err == nil { return result } return err.Error() } |
Changes to web/adapter/webui/lists.go.
︙ | ︙ | |||
8 9 10 11 12 13 14 | // under this license. //----------------------------------------------------------------------------- // Package webui provides web-UI handlers for web requests. package webui import ( | < > | | 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 | // under this license. //----------------------------------------------------------------------------- // Package webui provides web-UI handlers for web requests. package webui import ( "context" "net/http" "net/url" "sort" "strconv" "strings" "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" "zettelstore.de/z/usecase" "zettelstore.de/z/web/adapter" |
︙ | ︙ | |||
109 110 111 112 113 114 115 | Name string URL string iCount int Count string Size string } | | | 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 | Name string URL string iCount int Count string Size string } 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) if err != nil { wui.reportError(ctx, w, err) |
︙ | ︙ | |||
140 141 142 143 144 145 146 | countList := make([]int, 0, len(countMap)) for count := range countMap { countList = append(countList, count) } sort.Ints(countList) for pos, count := range countList { | | | 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 | countList := make([]int, 0, len(countMap)) for count := range countMap { countList = append(countList, count) } sort.Ints(countList) for pos, count := range 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]) } |
︙ | ︙ | |||
209 210 211 212 213 214 215 | depth := getIntParameter(q, api.QueryKeyDepth, 5) limit := getIntParameter(q, api.QueryKeyLimit, 200) metaList, err := getContext.Run(ctx, zid, dir, depth, limit) if err != nil { wui.reportError(ctx, w, err) return } | < | | 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 | depth := getIntParameter(q, api.QueryKeyDepth, 5) limit := getIntParameter(q, api.QueryKeyLimit, 200) metaList, err := getContext.Run(ctx, zid, dir, depth, limit) if err != nil { wui.reportError(ctx, w, err) return } 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(zid) for i, depth := range depths { depthURL.ClearQuery() switch dir { case usecase.ZettelContextBackward: depthURL.AppendQuery(api.QueryKeyDir, api.DirBackward) case usecase.ZettelContextForward: depthURL.AppendQuery(api.QueryKeyDir, api.DirForward) |
︙ | ︙ | |||
237 238 239 240 241 242 243 | Title string InfoURL string Depths []simpleLink Start simpleLink Metas []simpleLink }{ Title: "Zettel Context", | | | 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 | Title string InfoURL string Depths []simpleLink Start simpleLink Metas []simpleLink }{ Title: "Zettel Context", InfoURL: wui.NewURLBuilder('i').SetZid(zid).String(), Depths: depthLinks, Start: metaLinks[0], Metas: metaLinks[1:], }) } } |
︙ | ︙ | |||
284 285 286 287 288 289 290 | }) } func (wui *WebUI) listTitleSearch(prefix string, s *search.Search) string { if s == nil { return wui.rtConfig.GetSiteName() } | | | | | | | | | 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 | }) } func (wui *WebUI) listTitleSearch(prefix string, s *search.Search) string { if s == nil { return wui.rtConfig.GetSiteName() } var sb strings.Builder sb.WriteString(prefix) if s != nil { sb.WriteString(": ") s.Print(&sb) } 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, ) []simpleLink { defaultLang := wui.rtConfig.GetDefaultLang() metas := make([]simpleLink, 0, len(metaList)) for _, m := range metaList { var lang string 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(m.Zid).String(), }) } return metas } |
Changes to web/adapter/webui/rename_zettel.go.
︙ | ︙ | |||
12 13 14 15 16 17 18 | package webui import ( "fmt" "net/http" "strings" | | | < < < | | < < | | < < | 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 | package webui import ( "fmt" "net/http" "strings" "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" ) // MakeGetRenameZettelHandler creates a new HTTP handler to display the // HTML rename view of a zettel. 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) return } m, err := getMeta.Run(ctx, zid) if err != nil { wui.reportError(ctx, w, err) return } if enc, encText := adapter.GetEncoding(r, r.URL.Query(), api.EncoderHTML); enc != api.EncoderHTML { wui.reportError(ctx, w, adapter.NewErrBadRequest( fmt.Sprintf("Rename zettel %q not possible in encoding %q", zid.String(), encText))) return } 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 }{ Zid: zid.String(), MetaPairs: m.Pairs(true), }) } } // MakePostRenameZettelHandler creates a new HTTP handler to rename an existing zettel. func (wui *WebUI) MakePostRenameZettelHandler(renameZettel usecase.RenameZettel) http.HandlerFunc { return func(w http.ResponseWriter, r *http.Request) { |
︙ | ︙ | |||
83 84 85 86 87 88 89 | return } 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 } | < | < | | | | 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 | return } 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 } 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", newZid))) return } if err := renameZettel.Run(r.Context(), curZid, newZid); err != nil { wui.reportError(ctx, w, err) return } redirectFound(w, r, wui.NewURLBuilder('h').SetZid(newZid)) } } |
Changes to web/adapter/webui/response.go.
︙ | ︙ | |||
10 11 12 13 14 15 16 | // Package webui provides web-UI handlers for web requests. package webui import ( "net/http" | < < | < < < < < < < | 10 11 12 13 14 15 16 17 18 19 20 21 22 | // Package webui provides web-UI handlers for web requests. package webui import ( "net/http" "zettelstore.de/z/api" ) func redirectFound(w http.ResponseWriter, r *http.Request, ub *api.URLBuilder) { http.Redirect(w, r, ub.String(), http.StatusFound) } |
Changes to web/adapter/webui/webui.go.
︙ | ︙ | |||
10 11 12 13 14 15 16 | // Package webui provides web-UI handlers for web requests. package webui import ( "bytes" "context" | < | > < | 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 | // Package webui provides web-UI handlers for web requests. package webui import ( "bytes" "context" "log" "net/http" "sync" "time" "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 { ab server.AuthBuilder authz auth.AuthzManager rtConfig config.Config token auth.TokenManager box webuiBox policy auth.Policy |
︙ | ︙ | |||
72 73 74 75 76 77 78 | } // 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{ | < | | | 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 | } // 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{ 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(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(), loginURL: loginoutBase.String(), logoutURL: loginoutBase.AppendQuery("logout", "").String(), |
︙ | ︙ | |||
192 193 194 195 196 197 198 | func (wui *WebUI) makeBaseData(ctx context.Context, lang, title string, user *meta.Meta, data *baseData) { var userZettelURL string var userIdent string userIsValid := user != nil if userIsValid { | | | | 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 | func (wui *WebUI) makeBaseData(ctx context.Context, lang, title string, user *meta.Meta, data *baseData) { var userZettelURL string var userIdent string userIsValid := user != nil if userIsValid { 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 data.CSSUserURL = wui.cssUserURL data.Title = title |
︙ | ︙ | |||
239 240 241 242 243 244 245 | } menu, err := wui.box.GetZettel(ctx, id.TOCNewTemplateZid) if err != nil { return nil } refs := collect.Order(parser.ParseZettel(menu, "", wui.rtConfig)) for _, ref := range refs { | | | | | | | | | | | | 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 | } menu, err := wui.box.GetZettel(ctx, id.TOCNewTemplateZid) if err != nil { return nil } refs := collect.Order(parser.ParseZettel(menu, "", wui.rtConfig)) for _, ref := range refs { zid, err := id.Parse(ref.URL.Path) if err != nil { continue } 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.ParseInlines(input.NewInput(title), meta.ValueSyntaxZmk) env := encoder.Environment{Lang: config.GetLang(m, wui.rtConfig)} 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(m.Zid).String(), }) } return result } func (wui *WebUI) renderTemplate( ctx context.Context, |
︙ | ︙ | |||
284 285 286 287 288 289 290 | func (wui *WebUI) reportError(ctx context.Context, w http.ResponseWriter, err error) { code, text := adapter.CodeMessageFromError(err) if code == http.StatusInternalServerError { log.Printf("%v: %v", text, err) } user := wui.getUser(ctx) var base baseData | | | 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 | func (wui *WebUI) reportError(ctx context.Context, w http.ResponseWriter, err error) { code, text := adapter.CodeMessageFromError(err) if code == http.StatusInternalServerError { log.Printf("%v: %v", text, err) } user := wui.getUser(ctx) var base baseData 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), ErrorText: text, }) |
︙ | ︙ | |||
320 321 322 323 324 325 326 | if tok, err1 := wui.token.GetToken(user, wui.tokenLifetime, auth.KindHTML); err1 == nil { wui.setToken(w, tok) } } var content bytes.Buffer err = t.Render(&content, data) if err == nil { | < < < | > > | < < | < < | < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < | 318 319 320 321 322 323 324 325 326 327 328 329 330 331 332 333 334 335 336 337 338 339 340 341 342 343 344 345 346 347 348 | if tok, err1 := wui.token.GetToken(user, wui.tokenLifetime, auth.KindHTML); err1 == nil { wui.setToken(w, tok) } } var content bytes.Buffer err = t.Render(&content, data) if err == nil { 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. func (wui *WebUI) GetURLPrefix() string { return wui.ab.GetURLPrefix() } // NewURLBuilder creates a new URL builder object with the given key. 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) } |
Changes to web/server/impl/impl.go.
︙ | ︙ | |||
12 13 14 15 16 17 18 | package impl import ( "context" "net/http" "time" | | | 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 | package impl import ( "context" "net/http" "time" "zettelstore.de/z/api" "zettelstore.de/z/auth" "zettelstore.de/z/domain/meta" "zettelstore.de/z/web/server" ) type myServer struct { server httpServer |
︙ | ︙ | |||
87 88 89 90 91 92 93 | if w != nil { srv.SetToken(w, nil, 0) } return updateContext(ctx, nil, nil) } // GetAuthData returns the full authentication data from the context. | | | 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 | if w != nil { srv.SetToken(w, nil, 0) } return updateContext(ctx, nil, nil) } // GetAuthData returns the full authentication data from the context. func (srv *myServer) GetAuthData(ctx context.Context) *server.AuthData { data, ok := ctx.Value(ctxKeySession).(*server.AuthData) if ok { return data } return nil } |
︙ | ︙ |
Changes to web/server/impl/router.go.
︙ | ︙ | |||
12 13 14 15 16 17 18 | package impl import ( "net/http" "regexp" "strings" | | | 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 | package impl import ( "net/http" "regexp" "strings" "zettelstore.de/z/api" "zettelstore.de/z/auth" "zettelstore.de/z/kernel" "zettelstore.de/z/web/server" ) type ( methodHandler [server.MethodLAST]http.Handler |
︙ | ︙ | |||
75 76 77 78 79 80 81 | mh := table[key] if mh == nil { mh = new(methodHandler) table[key] = mh } mh[method] = handler if method == server.MethodGet { | | | 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 | mh := table[key] if mh == nil { mh = new(methodHandler) table[key] = mh } mh[method] = handler if method == server.MethodGet { if handler := mh[server.MethodHead]; handler == nil { mh[server.MethodHead] = handler } } } // addListRoute adds a route for the given key and HTTP method to work with a list. func (rt *httpRouter) addListRoute(key byte, method server.Method, handler http.Handler) { |
︙ | ︙ |
Changes to web/server/server.go.
︙ | ︙ | |||
12 13 14 15 16 17 18 | package server import ( "context" "net/http" "time" | | | 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 | package server import ( "context" "net/http" "time" "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. type UserRetriever interface { GetUser(ctx context.Context, zid id.Zid, ident string) (*meta.Meta, error) |
︙ | ︙ |
Changes to www/changes.wiki.
1 2 | <title>Change Log</title> | | | < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < | 1 2 3 4 5 6 7 8 9 10 11 | <title>Change Log</title> <a name="0_0_16"></a> <h2>Changes for Version 0.0.16 (pending)</h2> <a name="0_0_15"></a> <h2>Changes for Version 0.0.15 (2021-09-17)</h2> * Move again endpoint characters for authentication to make room for future features. WebUI authentication moves from <tt>/a</tt> to <tt>/i</tt> (login) and <tt>/i?logout</tt> (logout). API authentication moves from <tt>/v</tt> to </tt>/a</tt>. JSON-based basic zettel handling moves from |
︙ | ︙ |
Changes to www/download.wiki.
1 2 3 4 5 6 7 8 9 10 11 | <title>Download</title> <h1>Download of Zettelstore Software</h1> <h2>Foreword</h2> * Zettelstore is free/libre open source software, licensed under EUPL-1.2-or-later. * The software is provided as-is. * There is no guarantee that it will not damage your system. * 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. <h2>ZIP-ped Executables</h2> | | | | | | | | | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 | <title>Download</title> <h1>Download of Zettelstore Software</h1> <h2>Foreword</h2> * Zettelstore is free/libre open source software, licensed under EUPL-1.2-or-later. * The software is provided as-is. * There is no guarantee that it will not damage your system. * 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. <h2>ZIP-ped Executables</h2> Build: <code>v0.0.15</code> (2021-09-17). * [/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. <h2>Zettel for the manual</h2> As a starter, you can download the zettel for the manual [/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. |
Changes to www/index.wiki.
1 2 3 4 5 | <title>Home</title> <b>Zettelstore</b> 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 | | | | | | | < | < < < < < < | | | | | | | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 | <title>Home</title> <b>Zettelstore</b> 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. The software, including the manual, is licensed under the [/file?name=LICENSE.txt&ci=trunk|European Union Public License 1.2 (or later)]. [https://twitter.com/zettelstore|Stay tuned]… <hr> <h3>Latest Release: 0.0.15 (2021-09-17)</h3> * [./download.wiki|Download] * [./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] <hr> <h2>Build instructions</h2> Just install [https://golang.org/dl/|Go] and some Go-based tools. Please read the [./build.md|instructions] for details. * [/dir?ci=trunk|Source code] * [/download|Download the source code] as a tarball or a ZIP file (you must [/login|login] as user "anonymous"). |