Index: .fossil-settings/ignore-glob ================================================================== --- .fossil-settings/ignore-glob +++ .fossil-settings/ignore-glob @@ -1,2 +1,3 @@ bin/* releases/* +parser/pikchr/*.out Index: VERSION ================================================================== --- VERSION +++ VERSION @@ -1,1 +1,1 @@ -0.6.0 +0.7.1 Index: ast/ast.go ================================================================== --- ast/ast.go +++ ast/ast.go @@ -82,8 +82,8 @@ RefStateSelf // Reference to same zettel with a fragment RefStateFound // Reference to an existing internal zettel, URL is ajusted RefStateBroken // Reference to a non-existing internal zettel RefStateHosted // Reference to local hosted non-Zettel, without URL change RefStateBased // Reference to local non-Zettel, to be prefixed - RefStateSearch // Reference to a zettel search + RefStateQuery // Reference to a zettel query RefStateExternal // Reference to external material ) Index: ast/block.go ================================================================== --- ast/block.go +++ ast/block.go @@ -270,11 +270,12 @@ //-------------------------------------------------------------------------- // TranscludeNode specifies block content from other zettel to embedded in // current zettel type TranscludeNode struct { - Ref *Reference + Attrs attrs.Attributes + Ref *Reference } func (*TranscludeNode) blockNode() { /* Just a marker */ } // WalkChildren does nothing. Index: ast/ref.go ================================================================== --- ast/ref.go +++ ast/ref.go @@ -15,20 +15,20 @@ "strings" "zettelstore.de/z/domain/id" ) -// SearchPrefix is the prefix that denotes a search expression. -const SearchPrefix = "search:" +// QueryPrefix is the prefix that denotes a query expression. +const QueryPrefix = "query:" // ParseReference parses a string and returns a reference. func ParseReference(s string) *Reference { if s == "" || s == "00000000000000" { return &Reference{URL: nil, Value: s, State: RefStateInvalid} } - if strings.HasPrefix(s, SearchPrefix) { - return &Reference{URL: nil, Value: s[len(SearchPrefix):], State: RefStateSearch} + if strings.HasPrefix(s, QueryPrefix) { + return &Reference{URL: nil, Value: s[len(QueryPrefix):], State: RefStateQuery} } if state, ok := localState(s); ok { if state == RefStateBased { s = s[1:] } @@ -71,12 +71,12 @@ // String returns the string representation of a reference. func (r Reference) String() string { if r.URL != nil { return r.URL.String() } - if r.State == RefStateSearch { - return SearchPrefix + r.Value + if r.State == RefStateQuery { + return QueryPrefix + r.Value } return r.Value } // IsValid returns true if reference is valid Index: auth/auth.go ================================================================== --- auth/auth.go +++ auth/auth.go @@ -1,9 +1,9 @@ //----------------------------------------------------------------------------- -// Copyright (c) 2021 Detlef Stern +// Copyright (c) 2021-2022 Detlef Stern // -// This file is part of zettelstore. +// 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. //----------------------------------------------------------------------------- @@ -16,11 +16,10 @@ "zettelstore.de/z/box" "zettelstore.de/z/config" "zettelstore.de/z/domain/id" "zettelstore.de/z/domain/meta" - "zettelstore.de/z/web/server" ) // BaseManager allows to check some base auth modes. type BaseManager interface { // IsReadonly returns true, if the systems is configured to run in read-only-mode. @@ -77,11 +76,11 @@ // Manager is the main interface for providing the service. type Manager interface { TokenManager AuthzManager - BoxWithPolicy(auth server.Auth, unprotectedBox box.Box, rtConfig config.Config) (box.Box, Policy) + BoxWithPolicy(unprotectedBox box.Box, rtConfig config.Config) (box.Box, Policy) } // Policy is an interface for checking access authorization. type Policy interface { // User is allowed to create a new zettel. Index: auth/impl/impl.go ================================================================== --- auth/impl/impl.go +++ auth/impl/impl.go @@ -25,11 +25,10 @@ "zettelstore.de/z/box" "zettelstore.de/z/config" "zettelstore.de/z/domain/id" "zettelstore.de/z/domain/meta" "zettelstore.de/z/kernel" - "zettelstore.de/z/web/server" ) type myAuth struct { readonly bool owner id.Zid @@ -172,8 +171,8 @@ } } 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) +func (a *myAuth) BoxWithPolicy(unprotectedBox box.Box, rtConfig config.Config) (box.Box, auth.Policy) { + return policy.BoxWithPolicy(a, unprotectedBox, rtConfig) } Index: auth/policy/box.go ================================================================== --- auth/policy/box.go +++ auth/policy/box.go @@ -1,9 +1,9 @@ //----------------------------------------------------------------------------- -// Copyright (c) 2020-2021 Detlef Stern +// Copyright (c) 2020-2022 Detlef Stern // -// This file is part of zettelstore. +// 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. //----------------------------------------------------------------------------- @@ -17,36 +17,33 @@ "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/search" + "zettelstore.de/z/query" "zettelstore.de/z/web/server" ) // BoxWithPolicy wraps the given box inside a policy box. func BoxWithPolicy( - auth server.Auth, manager auth.AuthzManager, box box.Box, authConfig config.AuthConfig, ) (box.Box, auth.Policy) { pol := newPolicy(manager, authConfig) - return newBox(auth, box, pol), pol + return newBox(box, pol), pol } // polBox implements a policy box. type polBox struct { - auth server.Auth box box.Box policy auth.Policy } // newBox creates a new policy box. -func newBox(auth server.Auth, box box.Box, policy auth.Policy) box.Box { +func newBox(box box.Box, policy auth.Policy) box.Box { return &polBox{ - auth: auth, box: box, policy: policy, } } @@ -57,11 +54,11 @@ func (pp *polBox) CanCreateZettel(ctx context.Context) bool { return pp.box.CanCreateZettel(ctx) } func (pp *polBox) CreateZettel(ctx context.Context, zettel domain.Zettel) (id.Zid, error) { - user := pp.auth.GetUser(ctx) + user := server.GetUser(ctx) if pp.policy.CanCreate(user, zettel.Meta) { return pp.box.CreateZettel(ctx, zettel) } return id.Invalid, box.NewErrNotAllowed("Create", user, id.Invalid) } @@ -69,11 +66,11 @@ func (pp *polBox) GetZettel(ctx context.Context, zid id.Zid) (domain.Zettel, error) { zettel, err := pp.box.GetZettel(ctx, zid) if err != nil { return domain.Zettel{}, err } - user := pp.auth.GetUser(ctx) + user := server.GetUser(ctx) if pp.policy.CanRead(user, zettel.Meta) { return zettel, nil } return domain.Zettel{}, box.NewErrNotAllowed("GetZettel", user, zid) } @@ -85,11 +82,11 @@ func (pp *polBox) GetMeta(ctx context.Context, zid id.Zid) (*meta.Meta, error) { m, err := pp.box.GetMeta(ctx, zid) if err != nil { return nil, err } - user := pp.auth.GetUser(ctx) + user := server.GetUser(ctx) if pp.policy.CanRead(user, m) { return m, nil } return nil, box.NewErrNotAllowed("GetMeta", user, zid) } @@ -97,27 +94,27 @@ func (pp *polBox) GetAllMeta(ctx context.Context, zid id.Zid) ([]*meta.Meta, error) { return pp.box.GetAllMeta(ctx, zid) } func (pp *polBox) FetchZids(ctx context.Context) (id.Set, error) { - return nil, box.NewErrNotAllowed("fetch-zids", pp.auth.GetUser(ctx), id.Invalid) + return nil, box.NewErrNotAllowed("fetch-zids", server.GetUser(ctx), id.Invalid) } -func (pp *polBox) SelectMeta(ctx context.Context, s *search.Search) ([]*meta.Meta, error) { - user := pp.auth.GetUser(ctx) +func (pp *polBox) SelectMeta(ctx context.Context, q *query.Query) ([]*meta.Meta, error) { + user := server.GetUser(ctx) canRead := pp.policy.CanRead - s = s.AddPreMatch(func(m *meta.Meta) bool { return canRead(user, m) }) - return pp.box.SelectMeta(ctx, s) + q = q.SetPreMatch(func(m *meta.Meta) bool { return canRead(user, m) }) + return pp.box.SelectMeta(ctx, q) } func (pp *polBox) CanUpdateZettel(ctx context.Context, zettel domain.Zettel) bool { return pp.box.CanUpdateZettel(ctx, zettel) } func (pp *polBox) UpdateZettel(ctx context.Context, zettel domain.Zettel) error { zid := zettel.Meta.Zid - user := pp.auth.GetUser(ctx) + user := server.GetUser(ctx) if !zid.IsValid() { return &box.ErrInvalidID{Zid: zid} } // Write existing zettel oldMeta, err := pp.box.GetMeta(ctx, zid) @@ -137,11 +134,11 @@ func (pp *polBox) RenameZettel(ctx context.Context, curZid, newZid id.Zid) error { meta, err := pp.box.GetMeta(ctx, curZid) if err != nil { return err } - user := pp.auth.GetUser(ctx) + user := server.GetUser(ctx) if pp.policy.CanRename(user, meta) { return pp.box.RenameZettel(ctx, curZid, newZid) } return box.NewErrNotAllowed("Rename", user, curZid) } @@ -153,19 +150,19 @@ func (pp *polBox) DeleteZettel(ctx context.Context, zid id.Zid) error { meta, err := pp.box.GetMeta(ctx, zid) if err != nil { return err } - user := pp.auth.GetUser(ctx) + user := server.GetUser(ctx) if pp.policy.CanDelete(user, meta) { return pp.box.DeleteZettel(ctx, zid) } return box.NewErrNotAllowed("Delete", user, zid) } func (pp *polBox) Refresh(ctx context.Context) error { - user := pp.auth.GetUser(ctx) + user := server.GetUser(ctx) if pp.policy.CanRefresh(user) { return pp.box.Refresh(ctx) } return box.NewErrNotAllowed("Refresh", user, id.Invalid) } Index: box/box.go ================================================================== --- box/box.go +++ box/box.go @@ -22,11 +22,11 @@ "zettelstore.de/c/api" "zettelstore.de/z/domain" "zettelstore.de/z/domain/id" "zettelstore.de/z/domain/meta" - "zettelstore.de/z/search" + "zettelstore.de/z/query" ) // BaseBox is implemented by all Zettel boxes. type BaseBox interface { // Location returns some information where the box is located. @@ -74,14 +74,14 @@ // ManagedBox is the interface of managed boxes. type ManagedBox interface { BaseBox // Apply identifier of every zettel to the given function, if predicate returns true. - ApplyZid(context.Context, ZidFunc, search.RetrievePredicate) error + ApplyZid(context.Context, ZidFunc, query.RetrievePredicate) error // Apply metadata of every zettel to the given function, if predicate returns true. - ApplyMeta(context.Context, MetaFunc, search.RetrievePredicate) error + ApplyMeta(context.Context, MetaFunc, query.RetrievePredicate) error // ReadStats populates st with box statistics ReadStats(st *ManagedBoxStats) } @@ -116,11 +116,11 @@ // FetchZids returns the set of all zettel identifer managed by the box. FetchZids(ctx context.Context) (id.Set, error) // SelectMeta returns a list of metadata that comply to the given selection criteria. - SelectMeta(ctx context.Context, s *search.Search) ([]*meta.Meta, error) + SelectMeta(ctx context.Context, q *query.Query) ([]*meta.Meta, error) // GetAllZettel retrieves a specific zettel from all managed boxes. GetAllZettel(ctx context.Context, zid id.Zid) ([]domain.Zettel, error) // GetAllMeta retrieves the meta data of a specific zettel from all managed boxes. @@ -182,12 +182,11 @@ // Values for Reason const ( _ UpdateReason = iota OnReload // Box was reloaded - OnUpdate // A zettel was created or changed - OnDelete // A zettel was removed + OnZettel // Something with a zettel happened ) // UpdateInfo contains all the data about a changed zettel. type UpdateInfo struct { Box Box Index: box/compbox/compbox.go ================================================================== --- box/compbox/compbox.go +++ box/compbox/compbox.go @@ -21,11 +21,11 @@ "zettelstore.de/z/domain" "zettelstore.de/z/domain/id" "zettelstore.de/z/domain/meta" "zettelstore.de/z/kernel" "zettelstore.de/z/logger" - "zettelstore.de/z/search" + "zettelstore.de/z/query" ) func init() { manager.Register( " comp", @@ -108,11 +108,11 @@ } cb.log.Trace().Err(box.ErrNotFound).Msg("GetMeta/Err") return nil, box.ErrNotFound } -func (cb *compBox) ApplyZid(_ context.Context, handle box.ZidFunc, constraint search.RetrievePredicate) error { +func (cb *compBox) ApplyZid(_ context.Context, handle box.ZidFunc, constraint query.RetrievePredicate) error { cb.log.Trace().Int("entries", int64(len(myZettel))).Msg("ApplyMeta") for zid, gen := range myZettel { if !constraint(zid) { continue } @@ -123,11 +123,11 @@ } } return nil } -func (cb *compBox) ApplyMeta(ctx context.Context, handle box.MetaFunc, constraint search.RetrievePredicate) error { +func (cb *compBox) ApplyMeta(ctx context.Context, handle box.MetaFunc, constraint query.RetrievePredicate) error { cb.log.Trace().Int("entries", int64(len(myZettel))).Msg("ApplyMeta") for zid, gen := range myZettel { if !constraint(zid) { continue } Index: box/compbox/config.go ================================================================== --- box/compbox/config.go +++ box/compbox/config.go @@ -14,18 +14,20 @@ "bytes" "zettelstore.de/c/api" "zettelstore.de/z/domain/id" "zettelstore.de/z/domain/meta" + "zettelstore.de/z/kernel" ) func genConfigZettelM(zid id.Zid) *meta.Meta { if myConfig == nil { return nil } m := meta.New(zid) m.Set(api.KeyTitle, "Zettelstore Startup Configuration") + m.Set(api.KeyCreated, kernel.Main.GetConfig(kernel.CoreService, kernel.CoreStarted).(string)) m.Set(api.KeyVisibility, api.ValueVisibilityExpert) return m } func genConfigZettelC(*meta.Meta) []byte { Index: box/compbox/keys.go ================================================================== --- box/compbox/keys.go +++ box/compbox/keys.go @@ -15,15 +15,17 @@ "fmt" "zettelstore.de/c/api" "zettelstore.de/z/domain/id" "zettelstore.de/z/domain/meta" + "zettelstore.de/z/kernel" ) func genKeysM(zid id.Zid) *meta.Meta { m := meta.New(zid) m.Set(api.KeyTitle, "Zettelstore Supported Metadata Keys") + m.Set(api.KeyCreated, kernel.Main.GetConfig(kernel.CoreService, kernel.CoreVTime).(string)) m.Set(api.KeyVisibility, api.ValueVisibilityLogin) return m } func genKeysC(*meta.Meta) []byte { Index: box/compbox/log.go ================================================================== --- box/compbox/log.go +++ box/compbox/log.go @@ -1,9 +1,9 @@ //----------------------------------------------------------------------------- -// Copyright (c) 2021 Detlef Stern +// Copyright (c) 2021-2022 Detlef Stern // -// This file is part of zettelstore. +// 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. //----------------------------------------------------------------------------- @@ -21,10 +21,12 @@ func genLogM(zid id.Zid) *meta.Meta { m := meta.New(zid) m.Set(api.KeyTitle, "Zettelstore Log") m.Set(api.KeySyntax, api.ValueSyntaxText) + m.Set(api.KeyCreated, kernel.Main.GetConfig(kernel.CoreService, kernel.CoreStarted).(string)) + m.Set(api.KeyModified, kernel.Main.GetLastLogTime().Local().Format(id.ZidLayout)) return m } func genLogC(*meta.Meta) []byte { const tsFormat = "2006-01-02 15:04:05.999999" Index: box/compbox/manager.go ================================================================== --- box/compbox/manager.go +++ box/compbox/manager.go @@ -21,10 +21,11 @@ ) func genManagerM(zid id.Zid) *meta.Meta { m := meta.New(zid) m.Set(api.KeyTitle, "Zettelstore Box Manager") + m.Set(api.KeyCreated, kernel.Main.GetConfig(kernel.CoreService, kernel.CoreStarted).(string)) return m } func genManagerC(*meta.Meta) []byte { kvl := kernel.Main.GetServiceStatistics(kernel.BoxService) Index: box/compbox/parser.go ================================================================== --- box/compbox/parser.go +++ box/compbox/parser.go @@ -17,16 +17,18 @@ "strings" "zettelstore.de/c/api" "zettelstore.de/z/domain/id" "zettelstore.de/z/domain/meta" + "zettelstore.de/z/kernel" "zettelstore.de/z/parser" ) func genParserM(zid id.Zid) *meta.Meta { m := meta.New(zid) m.Set(api.KeyTitle, "Zettelstore Supported Parser") + m.Set(api.KeyCreated, kernel.Main.GetConfig(kernel.CoreService, kernel.CoreVTime).(string)) m.Set(api.KeyVisibility, api.ValueVisibilityLogin) return m } func genParserC(*meta.Meta) []byte { Index: box/compbox/version.go ================================================================== --- box/compbox/version.go +++ box/compbox/version.go @@ -24,26 +24,31 @@ return m } func genVersionBuildM(zid id.Zid) *meta.Meta { m := getVersionMeta(zid, "Zettelstore Version") + m.Set(api.KeyCreated, kernel.Main.GetConfig(kernel.CoreService, kernel.CoreVTime).(string)) m.Set(api.KeyVisibility, api.ValueVisibilityLogin) return m } func genVersionBuildC(*meta.Meta) []byte { return []byte(kernel.Main.GetConfig(kernel.CoreService, kernel.CoreVersion).(string)) } func genVersionHostM(zid id.Zid) *meta.Meta { - return getVersionMeta(zid, "Zettelstore Host") + m := getVersionMeta(zid, "Zettelstore Host") + m.Set(api.KeyCreated, kernel.Main.GetConfig(kernel.CoreService, kernel.CoreStarted).(string)) + return m } func genVersionHostC(*meta.Meta) []byte { return []byte(kernel.Main.GetConfig(kernel.CoreService, kernel.CoreHostname).(string)) } func genVersionOSM(zid id.Zid) *meta.Meta { - return getVersionMeta(zid, "Zettelstore Operating System") + m := getVersionMeta(zid, "Zettelstore Operating System") + m.Set(api.KeyCreated, kernel.Main.GetConfig(kernel.CoreService, kernel.CoreStarted).(string)) + return m } func genVersionOSC(*meta.Meta) []byte { goOS := kernel.Main.GetConfig(kernel.CoreService, kernel.CoreGoOS).(string) goArch := kernel.Main.GetConfig(kernel.CoreService, kernel.CoreGoArch).(string) result := make([]byte, 0, len(goOS)+len(goArch)+1) Index: box/constbox/base.css ================================================================== --- box/constbox/base.css +++ box/constbox/base.css @@ -81,11 +81,10 @@ h3 { font-size:1.15rem; margin:.75rem 0 } h4 { font-size:1.05rem; margin:.8rem 0; font-weight: bold } h5 { font-size:1.05rem; margin:.8rem 0 } h6 { font-size:1.05rem; margin:.8rem 0; font-weight: lighter } p { margin: .5rem 0 0 0 } - ol,ul { padding-left: 1.1rem } li,figure,figcaption,dl { margin: 0 } dt { margin: .5rem 0 0 0 } dt+dd { margin-top: 0 } dd { margin: .5rem 0 0 2rem } dd > p:first-child { margin: 0 0 0 0 } Index: box/constbox/base.mustache ================================================================== --- box/constbox/base.mustache +++ box/constbox/base.mustache @@ -52,11 +52,11 @@ {{/NewZettelLinks}} {{/HasNewZettelLinks}}
The endpoint to work with evaluated metadata and content of a specific zettel is /v/{ID}, where {ID} is a placeholder for the zettel identifier.
... ``` -You also can use the query parameter ''_part=[[PART|00001012920800]]'' to specify which parts of a zettel must be encoded. +You also can use the query parameter ''part=PART'' to specify which [[parts|00001012920800]] 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' +# curl 'http://127.0.0.1:23123/v/00001012053500?enc=html&part=meta' @@ -60,11 +61,11 @@ ; ''200'' : Retrieval was successful, the body contains an appropriate JSON object. ; ''400'' : Request was not valid. There are several reasons for this. - Maybe the zettel identifier did not consist of exactly 14 digits or ''_enc'' / ''_part'' contained illegal values. + Maybe the zettel identifier did not consist of exactly 14 digits or ''enc'' / ''part'' contained illegal values. ; ''403'' : You are not allowed to retrieve data of the given zettel. ; ''404'' : Zettel not found. You probably used a zettel identifier that is not used in the Zettelstore. Index: docs/manual/00001012053600.zettel ================================================================== --- docs/manual/00001012053600.zettel +++ docs/manual/00001012053600.zettel @@ -1,11 +1,12 @@ 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: 20211124180746 +created: 20210126175322 +modified: 20220908163514 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|00001006050000]]. 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. @@ -24,11 +25,11 @@ ; ''200'' : Retrieval was successful, the body contains an appropriate JSON object. ; ''400'' : Request was not valid. There are several reasons for this. - Maybe the zettel identifier did not consist of exactly 14 digits or ''_enc'' / ''_part'' contained illegal values. + Maybe the zettel identifier did not consist of exactly 14 digits or ''enc'' / ''part'' contained illegal values. ; ''403'' : You are not allowed to retrieve data of the given zettel. ; ''404'' : Zettel not found. You probably used a zettel identifier that is not used in the Zettelstore. Index: docs/manual/00001012053900.zettel ================================================================== --- docs/manual/00001012053900.zettel +++ docs/manual/00001012053900.zettel @@ -1,11 +1,12 @@ id: 00001012053900 title: API: Retrieve unlinked references to an existing zettel role: manual tags: #api #manual #zettelstore syntax: zmk -modified: 20220805144656 +created: 20211119133357 +modified: 20220913152019 The value of a personal Zettelstore is determined in part by explicit connections between related zettel. If the number of zettel grow, some of these connections are missing. There are various reasons for this. Maybe, you forgot that a zettel exists. @@ -45,19 +46,18 @@ The other zettel must not link to the specified zettel. The title must not occur within a link (e.g. to another zettel), in a [[heading|00001007030300]], in a [[citation|00001007040340]], and must have a uniform formatting. The match must be exact, but is case-insensitive. If the title of the specified zettel contains some extra character that probably reduce the number of found unlinked references, -you can specify the title phase to be searched for as a query parameter ''_phrase'': +you can specify the title phase to be searched for as a query parameter ''phrase'': ```` # curl 'http://127.0.0.1:23123/u/00001007000000?phrase=markdown' {"id": "00001007000000","meta": {...},"list": [{"id": "00001008010000","meta": {...},"rights":62},{"id": "00001004020000","meta": {...},"rights":62}]} ```` -In addition, you are allowed to specify all query parameter to [[select zettel based on their metadata|00001012051810]], to [[limit the length of the returned list|00001012051830]], and to [[sort the returned list|00001012052000]]. -You are allowed to limit the search by a [[search expression|00001012051840]], which may search for zettel content. +%%TODO: In addition, you are allowed to limit the search by a [[query expression|00001012051840]], which may search for zettel content. === Keys The following top-level JSON keys are returned: ; ''id'' : The [[zettel identifier|00001006050000]] for which the unlinked references were requested. Index: docs/manual/00001012080100.zettel ================================================================== --- docs/manual/00001012080100.zettel +++ docs/manual/00001012080100.zettel @@ -1,14 +1,15 @@ id: 00001012080100 title: API: Execute commands role: manual tags: #api #manual #zettelstore syntax: zmk -modified: 20220805174227 +created: 20211230230441 +modified: 20220908163125 The [[endpoint|00001012920000]] ''/x'' allows you to execute some (administrative) commands. -To differentiate between the possible commands, you have to set the query parameter ''_cmd'' to a specific value: +To differentiate between the possible commands, you have to set the query parameter ''cmd'' to a specific value: ; ''authenticated'' : [[Check for authentication|00001012080200]] ; ''refresh'' : [[Refresh internal data|00001012080500]] Index: docs/manual/00001012080200.zettel ================================================================== --- docs/manual/00001012080200.zettel +++ docs/manual/00001012080200.zettel @@ -1,21 +1,22 @@ id: 00001012080200 title: API: Check for authentication role: manual tags: #api #manual #zettelstore syntax: zmk -modified: 20220805174236 +created: 20220103224858 +modified: 20220908163156 API clients typically wants to know, whether [[authentication is enabled|00001010040100]] or not. If authentication is enabled, they present some form of user interface to get user name and password for the actual authentication. Then they try to [[obtain an access token|00001012050200]]. If authentication is disabled, these steps are not needed. -To check for enabled authentication, you must send a HTTP POST request to the [[endpoint|00001012920000]] ''/x'' and you must specify the query parameter ''_cmd=authenticated''. +To check for enabled authentication, you must send a HTTP POST request to the [[endpoint|00001012920000]] ''/x'' and you must specify the query parameter ''cmd=authenticated''. ```sh -# curl -X POST 'http://127.0.0.1:23123/x?_cmd=authenticated' +# curl -X POST 'http://127.0.0.1:23123/x?cmd=authenticated' ``` If authentication is not enabled, you will get a HTTP status code 200 (OK) with an empty HTTP body. Otherwise, authentication is enabled. @@ -28,8 +29,8 @@ ; ''204'' : Authentication is enabled and a valid access token was provided. ; ''400'' : Request was not valid. There are several reasons for this. - Most likely, no query parameter ''_cmd'' was given, or it did not contain the value ""authenticate"". + Most likely, no query parameter ''cmd'' was given, or it did not contain the value ""authenticate"". ; ''401'' : Authentication is enabled and not valid access token was provided. Index: docs/manual/00001012080500.zettel ================================================================== --- docs/manual/00001012080500.zettel +++ docs/manual/00001012080500.zettel @@ -1,11 +1,12 @@ id: 00001012080500 title: API: Refresh internal data role: manual tags: #api #manual #zettelstore syntax: zmk -modified: 20220805174246 +created: 20211230230441 +modified: 20220908163223 Zettelstore maintains some internal data to allow faster operations. One example is the [[content search|00001012051840]] for a term: Zettelstore does not need to scan all zettel to find all occurrences for the term. Instead, all word are stored internally, with a list of zettel where they occur. @@ -15,14 +16,14 @@ All these internal data may become stale. This should not happen, but when it comes e.g. to file handling, every operating systems behaves differently in very subtle ways. To avoid stopping and re-starting Zettelstore, you can use the API to force Zettelstore to refresh its internal data if you think it is needed. -To do this, you must send a HTTP POST request to the [[endpoint|00001012920000]] ''/x'' and you must specify the query parameter ''_cmd=refresh''. +To do this, you must send a HTTP POST request to the [[endpoint|00001012920000]] ''/x'' and you must specify the query parameter ''cmd=refresh''. ```sh -# curl -X POST 'http://127.0.0.1:23123/x?_cmd=refresh' +# curl -X POST 'http://127.0.0.1:23123/x?cmd=refresh' ``` If successful, you will get a HTTP status code 204 (No Content) with an empty HTTP body. The request will be successful if either: @@ -33,8 +34,8 @@ ; ''204'' : Operation was successful, the body is empty. ; ''400'' : Request was not valid. There are several reasons for this. - Most likely, no query parameter ''_cmd'' was given, or it did not contain the value ""refresh"". + Most likely, no query parameter ''cmd'' was given, or it did not contain the value ""refresh"". ; ''403'' : You are not allowed to perform this operation. Index: docs/manual/00001012920000.zettel ================================================================== --- docs/manual/00001012920000.zettel +++ docs/manual/00001012920000.zettel @@ -1,11 +1,12 @@ id: 00001012920000 title: Endpoints used by the API role: manual tags: #api #manual #reference #zettelstore syntax: zmk -modified: 20220627183408 +created: 20210126175322 +modified: 20220912115218 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'' @@ -20,13 +21,14 @@ | | PUT: [[renew access token|00001012050400]] | | ''j'' | GET: [[list zettel AS JSON|00001012051200]] | GET: [[retrieve zettel AS JSON|00001012053300]] | **J**SON | | POST: [[create new zettel|00001012053200]] | PUT: [[update a zettel|00001012054200]] | | | DELETE: [[delete the zettel|00001012054600]] | | | MOVE: [[rename the zettel|00001012054400]] -| ''m'' | GET: [[map metadata values|00001012052400]] | GET: [[retrieve metadata|00001012053400]] | **M**etadata +| ''m'' | GET: [[map metadata values|00001012052400]] (deprecated) | GET: [[retrieve metadata|00001012053400]] | **M**etadata | ''o'' | | GET: [[list zettel order|00001012054000]] | **O**rder | ''p'' | | GET: [[retrieve parsed zettel|00001012053600]]| **P**arsed +| ''q'' | GET: [[query zettel list|00001012051400]] | | **Q**uery | ''u'' | | GET [[unlinked references|00001012053900]] | **U**nlinked | ''v'' | | GET: [[retrieve evaluated zettel|00001012053500]] | E**v**aluated | ''x'' | GET: [[retrieve administrative data|00001012070500]] | GET: [[list zettel context|00001012053800]] | Conte**x**t | | POST: [[execute command|00001012080100]] | ''z'' | GET: [[list zettel|00001012051200#plain]] | GET: [[retrieve zettel|00001012053300#plain]] | **Z**ettel Index: docs/manual/00001012920503.zettel ================================================================== --- docs/manual/00001012920503.zettel +++ docs/manual/00001012920503.zettel @@ -1,20 +1,21 @@ id: 00001012920503 title: ZJSON Encoding role: manual tags: #api #manual #reference #zettelstore syntax: zmk -modified: 20220422191748 +created: 20210126175322 +modified: 20220908163450 A zettel representation that allows to process the syntactic structure of a zettel. It is a JSON-based encoding format, but different to the structures returned by [[endpoint|00001012920000]] ''/j/{ID}''. For an example, take a look at the ZJSON encoding of this page, which is available via the ""Info"" sub-page of this zettel: -* [[//v/00001012920503?_enc=zjson&_part=zettel]], -* [[//v/00001012920503?_enc=zjson&_part=meta]], -* [[//v/00001012920503?_enc=zjson&_part=content]]. +* [[//v/00001012920503?enc=zjson&part=zettel]], +* [[//v/00001012920503?enc=zjson&part=meta]], +* [[//v/00001012920503?enc=zjson&part=content]]. If transferred via HTTP, the content type will be ''application/json''. A full zettel encoding results in a JSON object with two keys: ''"meta"'' and ''"content"''. Both values are the same as if you have requested just the appropriate [[part|00001012920800]]. Index: docs/manual/00001012920516.zettel ================================================================== --- docs/manual/00001012920516.zettel +++ docs/manual/00001012920516.zettel @@ -1,22 +1,23 @@ id: 00001012920516 title: Sexpr Encoding role: manual tags: #api #manual #reference #zettelstore syntax: zmk -modified: 20220724170637 +created: 20220422181104 +modified: 20220908163427 A zettel representation that is a [[s-expression|https://en.wikipedia.org/wiki/S-expression]] (also known as symbolic expression). It is an alternative to the [[ZJSON encoding|00001012920503]]. Both encodings are (relatively) easy to parse and contain all relevant information of a zettel, metadata and content. For example, take a look at the Sexpr encoding of this page, which is available via the ""Info"" sub-page of this zettel: -* [[//v/00001012920516?_enc=sexpr&_part=zettel]], -* [[//v/00001012920516?_enc=sexpr&_part=meta]], -* [[//v/00001012920516?_enc=sexpr&_part=content]]. +* [[//v/00001012920516?enc=sexpr&part=zettel]], +* [[//v/00001012920516?enc=sexpr&part=meta]], +* [[//v/00001012920516?enc=sexpr&part=content]]. If transferred via HTTP, the content type will be ''text/plain''. === Syntax of s-expressions There are only two types of elements: atoms and lists. Index: docs/manual/00001017000000.zettel ================================================================== --- docs/manual/00001017000000.zettel +++ docs/manual/00001017000000.zettel @@ -1,11 +1,12 @@ id: 00001017000000 title: Tips and Tricks role: manual tags: #manual #zettelstore syntax: zmk -modified: 20220805174255 +created: 20220803170112 +modified: 20220916132030 === Welcome Zettel * **Problem:** You want to put your Zettelstore into the public and need a starting zettel for your users. In addition, you still want a ""home zettel"", with all your references to internal, non-public zettel. Zettelstore only allows to specify one [[''home-zettel''|00001004020000#home-zettel]]. @@ -16,5 +17,61 @@ It must have syntax [[Zettelmarkup|00001008000000#zmk]], i.e. the syntax metadata must be set to ''zmk''. If needed, set the runtime configuration [[''home-zettel|00001004020000#home-zettel]] to the value of the identifier of this zettel. *# At the beginning of the start zettel, add the following [[Zettelmarkup|00001007000000]] text in a separate paragraph: ``{{{20220803182600}}}`` (you have to adapt to the actual value of the zettel identifier for your non-public home zettel). * **Discussion:** As stated in the description for a [[transclusion|00001007031100]], a transclusion will be ignored, if the transcluded zettel is not visible to the current user. In effect, the transclusion statement (above paragraph that contained ''{{{...}}}'') is ignored when rendering the zettel. + +=== Role-specific Layout of Zettel in Web User Interface (WebUI) +[!role-css] +* **Problem:** You want to add some CSS when displaying zettel of a specific [[role|00001006020000#role]]. + For example, you might want to add a yellow background color for all [[configuration|00001006020100#configuration]] zettel. + Or you want a multi-column layout. +* **Solution:** If you enable [[''expert-mode''|00001004020000#expert-mode]], you will have access to a zettel called ""[[Zettelstore Role to CSS Map|00000000029000]]"" (its identifier is ''00000000029000''). + This zettel maps a role name to a zettel that must contain the role-specific CSS code. + + First, create a zettel containing the needed CSS: give it any title, its role is preferably ""configuration"" (but this is not a must). + Set its [[''syntax''|00001006020000#syntax]] must be set to ""[[css|00001008000000#css]]"". + The content must contain the role-specific CSS code, for example ``body {background-color: #FFFFD0}``for a background in a light yellow color. + + Let's assume, the newly created CSS zettel got the identifier ''20220825200100''. + + Now, you have to connect this zettel to the zettel called ""Zettelstore Role CSS Map"". + Since you have enabled ''expert-mode'', you are allowed to modify it. + Add the following metadata ''css-configuration-zid: 20220825200100'' to assign the role-specific CSS code for the role ""configuration"" to the CSS zettel containing that CSS. + + In general, its role-assigning metadata must be like this pattern: ''css-ROLE-zid: ID'', where ''ROLE'' is the placeholder for the role, and ''ID'' for the zettel identifier containing CSS code. + It is allowed to assign more than one role to a specific CSS zettel. +* **Discussion:** you have to ensure that the CSS zettel is allowed to be read by the intended audience of the zettel with that given role. + For example, if you made zettel with a specific role public visible, the CSS zettel must also have a [[''visibility: public''|00001010070200]] metadata. +* **Extension:** if you have already established a role-specific layout for zettel, but you additionally want just one another zettel with another role to be rendered with the same CSS, you have to add metadata to the one zettel: ''css-role: ROLE'', where ''ROLE'' is the placeholder for the role that already is assigned to a specific CSS-based layout. + +=== Zettel synchronization with iCloud (Apple) +* **Problem:** You use Zettelstore on various macOS computers and you want to use the sameset of zettel across all computers. +* **Solution:** Place your zettel in an iCloud folder. + + To configure Zettelstore to use the folder, you must specify its location within you directory structure as [[''box-uri-X''|00001004010000#box-uri-x]] (replace ''X'' with an appropriate number). + Your iCloud folder is typically placed in the folder ''~/Library/Mobile Documents/com~apple~CloudDocs''. + The ""''~''"" is a shortcut and specifies your home folder. + + Unfortunately, Zettelstore does not yet support this shortcut. + Therefore you must replace it with the absolute name of your home folder. + In addition, a space character is not allowed in an URI. + You have to replace it with the sequence ""''%20''"". + + Let us assume, that you stored your zettel box inside the folder ""zettel"", which is located top-level in your iCloud folder. + In this case, you must specify the following box URI within the startup configuration: ''box-uri-1: dir:///Users/USERNAME/Library/Mobile%20Documents/com~apple~CloudDocs/zettel'', replacing ''USERNAME'' with the username of that specific computer (and assuming you want to use it as the first box). +* **Solution 2:** If you typically start your Zettelstore on the command line, you could use the ''-d DIR'' option for the [[''run''|00001004051000#d]] sub-command. + In this case you are allowed to use the character ""''~''"". + + ''zettelstore run -d ~/Library/Mobile\\ Documents/com\\~apple\\~CloudDocs/zettel'' + + (The ""''\\''"" is needed by the command line processor to mask the following character to be processed in unintended ways.) +* **Discussion:** Zettel files are synchronized between your computers via iCloud. + Is does not matter, if one of your computer is offline / switched off. + iCloud will synchronize the zettel files if it later comes online. + + However, if you use more than one computer simultaneously, you must be aware that synchronization takes some time. + It might take several seconds, maybe longer, that new new version of a zettel appears on the other computer. + If you update the same zettel on multiple computers at nearly the same time, iCloud will not be able to synchronize the different versions in a safe manner. + Zettelstore is intentionally not aware of any synchronization within its zettel boxes. + + If Zettelstore behaves strangely after a synchronization took place, the page about [[Troubleshooting|00001018000000#working-with-files]] might contain some useful information. Index: docs/manual/00001018000000.zettel ================================================================== --- docs/manual/00001018000000.zettel +++ docs/manual/00001018000000.zettel @@ -1,11 +1,11 @@ id: 00001018000000 title: Troubleshooting role: manual tags: #manual #zettelstore syntax: zmk -modified: 20220805174305 +modified: 20220823195041 This page lists some problems and their solutions that may occur when using your Zettelstore. === Installation * **Problem:** When you double-click on the Zettelstore executable icon, macOS complains that Zettelstore is an application from an unknown developer. @@ -22,5 +22,15 @@ ** **Explanation:** A local running Zettelstore typically means, that you are accessing the Zettelstore using an URL with schema ''http://'', and not ''https://'', for example ''http://localhost:23123''. The difference between these two is the missing encryption of user name / password and for the answer of the Zettelstore if you use the ''http://'' schema. To be secure by default, the Zettelstore will not work in an insecure environment. ** **Solution 1:** If you are sure that your communication medium is safe, even if you use the ''http:/\/'' schema (for example, you are running the Zettelstore on the same computer you are working on, or if the Zettelstore is running on a computer in your protected local network), then you could add the entry ''insecure-cookie: true'' in you [[startup configuration|00001004010000#insecure-cookie]] file. ** **Solution 2:** If you are not sure about the security of your communication medium (for example, if unknown persons might use your local network), then you should run an [[external server|00001010090100]] in front of your Zettelstore to enable the use of the ''https://'' schema. + +=== Working with Zettel Files +* **Problem:** When you delete a zettel file by removing it from the ""disk"", e.g. by dropping it into the trash folder, by dragging into another folder, or by removing it from the command line, Zettelstore sometimes did not detect that change. + If you access the zettel via Zettelstore, a fatal error is reported. +** **Explanation:** Sometimes, the operating system does not tell Zettelstore about the removed zettel. + This occurs mostly under MacOS. +** **Solution 1:** If you are running Zettelstore in [[""simple-mode""|00001004051100]] or if you have enabled [[''expert-mode''|00001004020000#expert-mode]], you are allowed to refresh the internal data by selecting ""Refresh"" in the Web User Interface (you find it in the menu ""Lists""). +** **Solution 2:** There is an [[API|00001012080500]] call to make Zettelstore aware of this change. +** **Solution 3:** If you have an enabled [[Administrator Console|00001004100000]] you can use the command [[''refresh''|00001004101000#refresh]] to make your changes visible. +** **Solution 4:** You configure the zettel box as [[""simple""|00001004011400]]. Index: domain/id/id.go ================================================================== --- domain/id/id.go +++ domain/id/id.go @@ -29,13 +29,14 @@ const ( Invalid = Zid(0) // Invalid is a Zid that will never be valid ) // ZettelIDs that are used as Zid more than once. +// // Note: if you change some values, ensure that you also change them in the -// constant box. They are mentioned there literally, because these -// constants are not available there. +// Constant box. They are mentioned there literally, because these +// constants are not available there. var ( ConfigurationZid = MustParse(api.ZidConfiguration) BaseTemplateZid = MustParse(api.ZidBaseTemplate) LoginTemplateZid = MustParse(api.ZidLoginTemplate) ListTemplateZid = MustParse(api.ZidListTemplate) @@ -43,12 +44,10 @@ InfoTemplateZid = MustParse(api.ZidInfoTemplate) FormTemplateZid = MustParse(api.ZidFormTemplate) RenameTemplateZid = MustParse(api.ZidRenameTemplate) DeleteTemplateZid = MustParse(api.ZidDeleteTemplate) ContextTemplateZid = MustParse(api.ZidContextTemplate) - RolesTemplateZid = MustParse(api.ZidRolesTemplate) - TagsTemplateZid = MustParse(api.ZidTagsTemplate) ErrorTemplateZid = MustParse(api.ZidErrorTemplate) RoleCSSMapZid = MustParse(api.ZidRoleCSSMap) EmojiZid = MustParse(api.ZidEmoji) TOCNewTemplateZid = MustParse(api.ZidTOCNewTemplate) DefaultHomeZid = MustParse(api.ZidDefaultHome) @@ -141,21 +140,24 @@ result[13] = byte(second%10) + '0' } // 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 } + +// ZidLayout to transform a date into a Zid and into other internal dates. +const ZidLayout = "20060102150405" // New returns a new zettel id based on the current time. func New(withSeconds bool) Zid { - now := time.Now() + now := time.Now().Local() var s string if withSeconds { - s = now.Format("20060102150405") + s = now.Format(ZidLayout) } else { s = now.Format("20060102150400") } res, err := Parse(s) if err != nil { panic(err) } return res } Index: domain/meta/meta.go ================================================================== --- domain/meta/meta.go +++ domain/meta/meta.go @@ -47,10 +47,13 @@ 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 } +// IsStoredComputed retruns true, if metadata is computed, but also stored. +func (kd *DescriptionKey) IsStoredComputed() bool { return kd.usage == usageComputed } + var registeredKeys = make(map[string]*DescriptionKey) func registerKey(name string, t *DescriptionType, usage keyUsage, inverse string) { if _, ok := registeredKeys[name]; ok { panic("Key '" + name + "' already defined") @@ -86,10 +89,18 @@ if kd, ok := registeredKeys[name]; ok { return kd.IsProperty() } return false } + +// IsStoredComputed returns true, if key denotes a computed metadata key that is stored. +func IsStoredComputed(name string) bool { + if kd, ok := registeredKeys[name]; ok { + return kd.IsStoredComputed() + } + return false +} // Inverse returns the name of the inverse key. func Inverse(name string) string { if kd, ok := registeredKeys[name]; ok { return kd.Inverse @@ -122,15 +133,17 @@ registerKey(api.KeyRole, TypeWord, usageUser, "") registerKey(api.KeyTags, TypeTagSet, usageUser, "") registerKey(api.KeySyntax, TypeWord, usageUser, "") registerKey(api.KeyAllTags, TypeTagSet, usageProperty, "") + registerKey(api.KeyAuthor, TypeString, usageUser, "") registerKey(api.KeyBack, TypeIDSet, usageProperty, "") registerKey(api.KeyBackward, TypeIDSet, usageProperty, "") registerKey(api.KeyBoxNumber, TypeNumber, usageProperty, "") registerKey(api.KeyContentTags, TypeTagSet, usageProperty, "") registerKey(api.KeyCopyright, TypeString, usageUser, "") + registerKey(api.KeyCreated, TypeTimestamp, usageComputed, "") registerKey(api.KeyCredential, TypeCredential, usageUser, "") registerKey(api.KeyDead, TypeIDSet, usageProperty, "") registerKey(api.KeyFolge, TypeIDSet, usageProperty, "") registerKey(api.KeyForward, TypeIDSet, usageProperty, "") registerKey(api.KeyLang, TypeWord, usageUser, "") Index: domain/meta/parse.go ================================================================== --- domain/meta/parse.go +++ domain/meta/parse.go @@ -152,11 +152,11 @@ return } switch Type(key) { case TypeTagSet: - addSet(m, key, strings.ToLower(v), func(s string) bool { return s[0] == '#' }) + addSet(m, key, strings.ToLower(v), func(s string) bool { return s[0] == '#' && len(s) > 1 }) case TypeWord: m.Set(key, strings.ToLower(v)) case TypeWordSet: addSet(m, key, strings.ToLower(v), func(s string) bool { return true }) case TypeID: Index: domain/meta/parse_test.go ================================================================== --- domain/meta/parse_test.go +++ domain/meta/parse_test.go @@ -10,10 +10,11 @@ // Package meta_test provides tests for the domain specific type 'meta'. package meta_test import ( + "strings" "testing" "zettelstore.de/c/api" "zettelstore.de/z/domain/meta" "zettelstore.de/z/input" @@ -55,10 +56,46 @@ t.Log(m) t.Errorf("TC=%d: expected %q, got %q", i, tc.e, got) } } } + +func TestTags(t *testing.T) { + t.Parallel() + testcases := []struct { + src string + exp string + }{ + {"", ""}, + {api.KeyTags + ":", ""}, + {api.KeyTags + ": c", ""}, + {api.KeyTags + ": #", ""}, + {api.KeyTags + ": #c", "c"}, + {api.KeyTags + ": #c #", "c"}, + {api.KeyTags + ": #c #b", "b c"}, + {api.KeyTags + ": #c # #", "c"}, + {api.KeyTags + ": #c # #b", "b c"}, + } + for i, tc := range testcases { + m := parseMetaStr(tc.src) + tags, found := m.GetTags(api.KeyTags) + if !found { + if tc.exp != "" { + t.Errorf("%d / %q: no %s found", i, tc.src, api.KeyTags) + } + continue + } + if tc.exp == "" && len(tags) > 0 { + t.Errorf("%d / %q: expected no %s, but got %v", i, tc.src, api.KeyTags, tags) + continue + } + got := strings.Join(tags, " ") + if tc.exp != got { + t.Errorf("%d / %q: expected %q, got: %q", i, tc.src, tc.exp, got) + } + } +} func TestNewFromInput(t *testing.T) { t.Parallel() testcases := []struct { input string Index: domain/meta/type.go ================================================================== --- domain/meta/type.go +++ domain/meta/type.go @@ -16,10 +16,11 @@ "strings" "sync" "time" "zettelstore.de/c/api" + "zettelstore.de/z/domain/id" ) // DescriptionType is a description of a specific key type. type DescriptionType struct { Name string @@ -110,11 +111,11 @@ } } // SetNow stores the current timestamp under the given key. func (m *Meta) SetNow(key string) { - m.Set(key, time.Now().Format("20060102150405")) + m.Set(key, time.Now().Local().Format(id.ZidLayout)) } // BoolValue returns the value interpreted as a bool. func BoolValue(value string) bool { if len(value) > 0 { @@ -134,11 +135,11 @@ return false } // TimeValue returns the time value of the given value. func TimeValue(value string) (time.Time, bool) { - if t, err := time.Parse("20060102150405", value); err == nil { + if t, err := time.Parse(id.ZidLayout, value); err == nil { return t, true } return time.Time{}, false } Index: encoder/encoder_block_test.go ================================================================== --- encoder/encoder_block_test.go +++ encoder/encoder_block_test.go @@ -279,10 +279,21 @@ encoderSexpr: `((PARA (TEXT "Text") (FOOTNOTE () (TEXT "Footnote"))))`, encoderText: "Text Footnote", encoderZmk: useZmk, }, }, + { + descr: "Transclusion", + zmk: `{{{http://example.com/image}}}{width="100px"}`, + expect: expectMap{ + encoderZJSON: `[{"":"Transclude","a":{"width":"100px"},"q":"external","s":"http://example.com/image"}]`, + encoderHTML: ``, + encoderSexpr: `((TRANSCLUDE (("width" "100px")) (EXTERNAL "http://example.com/image")))`, + encoderText: "", + encoderZmk: useZmk, + }, + }, { descr: "", zmk: ``, expect: expectMap{ encoderZJSON: `[]`, Index: encoder/encoder_inline_test.go ================================================================== --- encoder/encoder_inline_test.go +++ encoder/encoder_inline_test.go @@ -439,28 +439,28 @@ encoderText: `R`, encoderZmk: useZmk, }, }, { - descr: "Search link w/o text", - zmk: `[[search:title:syntax]]`, + descr: "Query link w/o text", + zmk: `[[query:title:syntax]]`, expect: expectMap{ - encoderZJSON: `[{"":"Link","q":"search","s":"title:syntax"}]`, - encoderHTML: `title:syntax`, - encoderSexpr: `((LINK-SEARCH () "title:syntax"))`, + encoderZJSON: `[{"":"Link","q":"query","s":"title:syntax"}]`, + encoderHTML: `title:syntax`, + encoderSexpr: `((LINK-QUERY () "title:syntax"))`, encoderText: ``, encoderZmk: useZmk, }, }, { - descr: "Search link with text", - zmk: `[[S|search:title:syntax]]`, + descr: "Query link with text", + zmk: `[[Q|query:title:syntax]]`, expect: expectMap{ - encoderZJSON: `[{"":"Link","q":"search","s":"title:syntax","i":[{"":"Text","s":"S"}]}]`, - encoderHTML: `S`, - encoderSexpr: `((LINK-SEARCH () "title:syntax" (TEXT "S")))`, - encoderText: `S`, + encoderZJSON: `[{"":"Link","q":"query","s":"title:syntax","i":[{"":"Text","s":"Q"}]}]`, + encoderHTML: `Q`, + encoderSexpr: `((LINK-QUERY () "title:syntax" (TEXT "Q")))`, + encoderText: `Q`, encoderZmk: useZmk, }, }, { descr: "Dummy Embed", @@ -471,10 +471,21 @@ encoderSexpr: `((EMBED () (EXTERNAL "abc") ""))`, encoderText: ``, encoderZmk: useZmk, }, }, + { + descr: "Inline HTML Zettel", + zmk: `@@...). +** +** The subroutine implemented by this file is intended to be stand-alone. +** It uses no external routines other than routines commonly found in +** the standard C library. +** +**************************************************************************** +** COMPILING: +** +** The original source text is a mixture of C99 and "Lemon" +** (See https://sqlite.org/src/file/doc/lemon.html). Lemon is an LALR(1) +** parser generator program, similar to Yacc. The grammar of the +** input language is specified in Lemon. C-code is attached. Lemon +** runs to generate a single output file ("pikchr.c") which is then +** compiled to generate the Pikchr library. This header comment is +** preserved in the Lemon output, so you might be reading this in either +** the generated "pikchr.c" file that is output by Lemon, or in the +** "pikchr.y" source file that is input into Lemon. If you make changes, +** you should change the input source file "pikchr.y", not the +** Lemon-generated output file. +** +** Basic compilation steps: +** +** lemon pikchr.y +** cc pikchr.c -o pikchr.o +** +** Add -DPIKCHR_SHELL to add a main() routine that reads input files +** and sends them through Pikchr, for testing. Add -DPIKCHR_FUZZ for +** -fsanitizer=fuzzer testing. +** +**************************************************************************** +** IMPLEMENTATION NOTES (for people who want to understand the internal +** operation of this software, perhaps to extend the code or to fix bugs): +** +** Each call to pikchr() uses a single instance of the Pik structure to +** track its internal state. The Pik structure lives for the duration +** of the pikchr() call. +** +** The input is a sequence of objects or "statements". Each statement is +** parsed into a PObj object. These are stored on an extensible array +** called PList. All parameters to each PObj are computed as the +** object is parsed. (Hence, the parameters to a PObj may only refer +** to prior statements.) Once the PObj is completely assembled, it is +** added to the end of a PList and never changes thereafter - except, +** PObj objects that are part of a "[...]" block might have their +** absolute position shifted when the outer [...] block is positioned. +** But apart from this repositioning, PObj objects are unchanged once +** they are added to the list. The order of statements on a PList does +** not change. +** +** After all input has been parsed, the top-level PList is walked to +** generate output. Sub-lists resulting from [...] blocks are scanned +** as they are encountered. All input must be collected and parsed ahead +** of output generation because the size and position of statements must be +** known in order to compute a bounding box on the output. +** +** Each PObj is on a "layer". (The common case is that all PObj's are +** on a single layer, but multiple layers are possible.) A separate pass +** is made through the list for each layer. +** +** After all output is generated, the Pik object and all the PList +** and PObj objects are deallocated and the generated output string is +** returned. Upon any error, the Pik.nErr flag is set, processing quickly +** stops, and the stack unwinds. No attempt is made to continue reading +** input after an error. +** +** Most statements begin with a class name like "box" or "arrow" or "move". +** There is a class named "text" which is used for statements that begin +** with a string literal. You can also specify the "text" class. +** A Sublist ("[...]") is a single object that contains a pointer to +** its substatements, all gathered onto a separate PList object. +** +** Variables go into PVar objects that form a linked list. +** +** Each PObj has zero or one names. Input constructs that attempt +** to assign a new name from an older name, for example: +** +** Abc: Abc + (0.5cm, 0) +** +** Statements like these generate a new "noop" object at the specified +** place and with the given name. As place-names are searched by scanning +** the list in reverse order, this has the effect of overriding the "Abc" +** name when referenced by subsequent objects. + */ + +package internal + +import ( + "bytes" + "fmt" + "io" + "math" + "os" + "regexp" + "strconv" + "strings" +) + +// Numeric value +type PNum = float64 + +// Compass points +const ( + CP_N uint8 = iota + 1 + CP_NE + CP_E + CP_SE + CP_S + CP_SW + CP_W + CP_NW + CP_C /* .center or .c */ + CP_END /* .end */ + CP_START /* .start */ +) + +/* Heading angles corresponding to compass points */ +var pik_hdg_angle = []PNum{ + /* none */ 0.0, + /* N */ 0.0, + /* NE */ 45.0, + /* E */ 90.0, + /* SE */ 135.0, + /* S */ 180.0, + /* SW */ 225.0, + /* W */ 270.0, + /* NW */ 315.0, + /* C */ 0.0, +} + +/* Built-in functions */ +const ( + FN_ABS = 0 + FN_COS = 1 + FN_INT = 2 + FN_MAX = 3 + FN_MIN = 4 + FN_SIN = 5 + FN_SQRT = 6 +) + +/* Text position and style flags. Stored in PToken.eCode so limited +** to 15 bits. */ +const ( + TP_LJUST = 0x0001 /* left justify...... */ + TP_RJUST = 0x0002 /* ...Right justify */ + TP_JMASK = 0x0003 /* Mask for justification bits */ + TP_ABOVE2 = 0x0004 /* Position text way above PObj.ptAt */ + TP_ABOVE = 0x0008 /* Position text above PObj.ptAt */ + TP_CENTER = 0x0010 /* On the line */ + TP_BELOW = 0x0020 /* Position text below PObj.ptAt */ + TP_BELOW2 = 0x0040 /* Position text way below PObj.ptAt */ + TP_VMASK = 0x007c /* Mask for text positioning flags */ + TP_BIG = 0x0100 /* Larger font */ + TP_SMALL = 0x0200 /* Smaller font */ + TP_XTRA = 0x0400 /* Amplify TP_BIG or TP_SMALL */ + TP_SZMASK = 0x0700 /* Font size mask */ + TP_ITALIC = 0x1000 /* Italic font */ + TP_BOLD = 0x2000 /* Bold font */ + TP_FMASK = 0x3000 /* Mask for font style */ + TP_ALIGN = 0x4000 /* Rotate to align with the line */ +) + +/* An object to hold a position in 2-D space */ +type PPoint struct { + /* X and Y coordinates */ + x PNum + y PNum +} + +/* A bounding box */ +type PBox struct { + /* Lower-left and top-right corners */ + sw PPoint + ne PPoint +} + +/* An Absolute or a relative distance. The absolute distance +** is stored in rAbs and the relative distance is stored in rRel. +** Usually, one or the other will be 0.0. When using a PRel to +** update an existing value, the computation is usually something +** like this: +** +** value = PRel.rAbs + value*PRel.rRel +** + */ +type PRel struct { + rAbs PNum /* Absolute value */ + rRel PNum /* Value relative to current value */ +} + +/* A variable created by the ID = EXPR construct of the PIKCHR script +** +** PIKCHR (and PIC) scripts do not use many varaibles, so it is reasonable +** to store them all on a linked list. + */ +type PVar struct { + zName string /* Name of the variable */ + val PNum /* Value of the variable */ + pNext *PVar /* Next variable in a list of them all */ +} + +/* A single token in the parser input stream + */ +type PToken struct { + z []byte /* Pointer to the token text */ + n int /* Length of the token in bytes */ + + eCode int16 /* Auxiliary code */ + eType uint8 /* The numeric parser code */ + eEdge uint8 /* Corner value for corner keywords */ +} + +func (p PToken) String() string { + return string(p.z[:p.n]) +} + +/* Return negative, zero, or positive if pToken is less than, equal to +** or greater than the zero-terminated string z[] + */ +func pik_token_eq(pToken *PToken, z string) int { + c := bytencmp(pToken.z, z, pToken.n) + if c == 0 && len(z) > pToken.n && z[pToken.n] != 0 { + c = -1 + } + return c +} + +/* Extra token types not generated by LEMON but needed by the +** tokenizer + */ +const ( + T_PARAMETER = 253 /* $1, $2, ..., $9 */ + T_WHITESPACE = 254 /* Whitespace of comments */ + T_ERROR = 255 /* Any text that is not a valid token */ +) + +/* Directions of movement */ +const ( + DIR_RIGHT = 0 + DIR_DOWN = 1 + DIR_LEFT = 2 + DIR_UP = 3 +) + +func ValidDir(x uint8) bool { + return x >= 0 && x <= 3 +} + +func IsUpDown(x uint8) bool { + return x&1 == 1 +} + +func IsLeftRight(x uint8) bool { + return x&1 == 0 +} + +/* Bitmask for the various attributes for PObj. These bits are +** collected in PObj.mProp and PObj.mCalc to check for constraint +** errors. */ +const ( + A_WIDTH = 0x0001 + A_HEIGHT = 0x0002 + A_RADIUS = 0x0004 + A_THICKNESS = 0x0008 + A_DASHED = 0x0010 /* Includes "dotted" */ + A_FILL = 0x0020 + A_COLOR = 0x0040 + A_ARROW = 0x0080 + A_FROM = 0x0100 + A_CW = 0x0200 + A_AT = 0x0400 + A_TO = 0x0800 /* one or more movement attributes */ + A_FIT = 0x1000 +) + +/* A single graphics object */ +type PObj struct { + typ *PClass /* Object type or class */ + errTok PToken /* Reference token for error messages */ + ptAt PPoint /* Reference point for the object */ + ptEnter PPoint /* Entry and exit points */ + ptExit PPoint + pSublist []*PObj /* Substructure for [...] objects */ + zName string /* Name assigned to this statement */ + w PNum /* "width" property */ + h PNum /* "height" property */ + rad PNum /* "radius" property */ + sw PNum /* "thickness" property. (Mnemonic: "stroke width")*/ + dotted PNum /* "dotted" property. <=0.0 for off */ + dashed PNum /* "dashed" property. <=0.0 for off */ + fill PNum /* "fill" property. Negative for off */ + color PNum /* "color" property */ + with PPoint /* Position constraint from WITH clause */ + eWith uint8 /* Type of heading point on WITH clause */ + cw bool /* True for clockwise arc */ + larrow bool /* Arrow at beginning (<- or <->) */ + rarrow bool /* Arrow at end (-> or <->) */ + bClose bool /* True if "close" is seen */ + bChop bool /* True if "chop" is seen */ + nTxt uint8 /* Number of text values */ + mProp uint /* Masks of properties set so far */ + mCalc uint /* Values computed from other constraints */ + aTxt [5]PToken /* Text with .eCode holding TP flags */ + iLayer int /* Rendering order */ + inDir uint8 /* Entry and exit directions */ + outDir uint8 + nPath int /* Number of path points */ + aPath []PPoint /* Array of path points */ + pFrom *PObj /* End-point objects of a path */ + pTo *PObj + bbox PBox /* Bounding box */ +} + +// A list of graphics objects. +type PList = []*PObj + +/* A macro definition */ +type PMacro struct { + pNext *PMacro /* Next in the list */ + macroName PToken /* Name of the macro */ + macroBody PToken /* Body of the macro */ + inUse bool /* Do not allow recursion */ +} + +/* Each call to the pikchr() subroutine uses an instance of the following +** object to pass around context to all of its subroutines. + */ +type Pik struct { + nErr int /* Number of errors seen */ + sIn PToken /* Input Pikchr-language text */ + zOut bytes.Buffer /* Result accumulates here */ + nOut uint /* Bytes written to zOut[] so far */ + nOutAlloc uint /* Space allocated to zOut[] */ + eDir uint8 /* Current direction */ + mFlags uint /* Flags passed to pikchr() */ + cur *PObj /* Object under construction */ + lastRef *PObj /* Last object references by name */ + list []*PObj /* Object list under construction */ + pMacros *PMacro /* List of all defined macros */ + pVar *PVar /* Application-defined variables */ + bbox PBox /* Bounding box around all statements */ + + /* Cache of layout values. <=0.0 for unknown... */ + rScale PNum /* Multiply to convert inches to pixels */ + fontScale PNum /* Scale fonts by this percent */ + charWidth PNum /* Character width */ + charHeight PNum /* Character height */ + wArrow PNum /* Width of arrowhead at the fat end */ + hArrow PNum /* Ht of arrowhead - dist from tip to fat end */ + bLayoutVars bool /* True if cache is valid */ + thenFlag bool /* True if "then" seen */ + samePath bool /* aTPath copied by "same" */ + zClass string /* Class name for the