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,30 +24,35 @@ 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) result = append(result, goOS...) result = append(result, '/') return append(result, goArch...) } 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,15 +52,15 @@ {{/NewZettelLinks}} {{/HasNewZettelLinks}}
- +
{{{Content}}}
{{#FooterHTML}}{{/FooterHTML}} {{#DebugMode}}
WARNING: Debug mode is enabled. DO NOT USE IN PRODUCTION!
{{/DebugMode}} Index: box/constbox/constbox.go ================================================================== --- box/constbox/constbox.go +++ box/constbox/constbox.go @@ -22,11 +22,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( " const", @@ -80,21 +80,21 @@ } cb.log.Trace().Err(box.ErrNotFound).Msg("GetMeta") return nil, box.ErrNotFound } -func (cb *constBox) ApplyZid(_ context.Context, handle box.ZidFunc, constraint search.RetrievePredicate) error { +func (cb *constBox) ApplyZid(_ context.Context, handle box.ZidFunc, constraint query.RetrievePredicate) error { cb.log.Trace().Int("entries", int64(len(cb.zettel))).Msg("ApplyZid") for zid := range cb.zettel { if constraint(zid) { handle(zid) } } return nil } -func (cb *constBox) ApplyMeta(ctx context.Context, handle box.MetaFunc, constraint search.RetrievePredicate) error { +func (cb *constBox) ApplyMeta(ctx context.Context, handle box.MetaFunc, constraint query.RetrievePredicate) error { cb.log.Trace().Int("entries", int64(len(cb.zettel))).Msg("ApplyMeta") for zid, zettel := range cb.zettel { if constraint(zid) { m := meta.NewWithData(zid, zettel.header) cb.enricher.Enrich(ctx, m, cb.number) @@ -148,28 +148,32 @@ id.ConfigurationZid: { constHeader{ api.KeyTitle: "Zettelstore Runtime Configuration", api.KeyRole: api.ValueRoleConfiguration, api.KeySyntax: api.ValueSyntaxNone, + api.KeyCreated: "20200804111624", api.KeyVisibility: api.ValueVisibilityOwner, }, domain.NewContent(nil)}, id.MustParse(api.ZidLicense): { constHeader{ api.KeyTitle: "Zettelstore License", api.KeyRole: api.ValueRoleConfiguration, api.KeySyntax: api.ValueSyntaxText, + api.KeyCreated: "20210504135842", api.KeyLang: api.ValueLangEN, + api.KeyModified: "20220131153422", api.KeyReadOnly: api.ValueTrue, api.KeyVisibility: api.ValueVisibilityPublic, }, domain.NewContent(contentLicense)}, id.MustParse(api.ZidAuthors): { constHeader{ api.KeyTitle: "Zettelstore Contributors", api.KeyRole: api.ValueRoleConfiguration, api.KeySyntax: api.ValueSyntaxZmk, + api.KeyCreated: "20210504135842", api.KeyLang: api.ValueLangEN, api.KeyReadOnly: api.ValueTrue, api.KeyVisibility: api.ValueVisibilityLogin, }, domain.NewContent(contentContributors)}, @@ -179,175 +183,179 @@ api.KeyRole: api.ValueRoleConfiguration, api.KeySyntax: api.ValueSyntaxZmk, api.KeyLang: api.ValueLangEN, api.KeyReadOnly: api.ValueTrue, api.KeyVisibility: api.ValueVisibilityLogin, + api.KeyCreated: "20210504135842", + api.KeyModified: "20220824161200", }, domain.NewContent(contentDependencies)}, id.BaseTemplateZid: { constHeader{ api.KeyTitle: "Zettelstore Base HTML Template", api.KeyRole: api.ValueRoleConfiguration, api.KeySyntax: syntaxTemplate, + api.KeyCreated: "20210504135842", api.KeyVisibility: api.ValueVisibilityExpert, }, domain.NewContent(contentBaseMustache)}, id.LoginTemplateZid: { constHeader{ api.KeyTitle: "Zettelstore Login Form HTML Template", api.KeyRole: api.ValueRoleConfiguration, api.KeySyntax: syntaxTemplate, + api.KeyCreated: "20200804111624", api.KeyVisibility: api.ValueVisibilityExpert, }, domain.NewContent(contentLoginMustache)}, id.ZettelTemplateZid: { constHeader{ api.KeyTitle: "Zettelstore Zettel HTML Template", api.KeyRole: api.ValueRoleConfiguration, api.KeySyntax: syntaxTemplate, + api.KeyCreated: "20200804111624", api.KeyVisibility: api.ValueVisibilityExpert, }, domain.NewContent(contentZettelMustache)}, id.InfoTemplateZid: { constHeader{ api.KeyTitle: "Zettelstore Info HTML Template", api.KeyRole: api.ValueRoleConfiguration, api.KeySyntax: syntaxTemplate, + api.KeyCreated: "20200804111624", api.KeyVisibility: api.ValueVisibilityExpert, }, domain.NewContent(contentInfoMustache)}, id.ContextTemplateZid: { constHeader{ api.KeyTitle: "Zettelstore Context HTML Template", api.KeyRole: api.ValueRoleConfiguration, api.KeySyntax: syntaxTemplate, + api.KeyCreated: "20210218181140", api.KeyVisibility: api.ValueVisibilityExpert, }, domain.NewContent(contentContextMustache)}, id.FormTemplateZid: { constHeader{ api.KeyTitle: "Zettelstore Form HTML Template", api.KeyRole: api.ValueRoleConfiguration, api.KeySyntax: syntaxTemplate, + api.KeyCreated: "20200804111624", api.KeyVisibility: api.ValueVisibilityExpert, }, domain.NewContent(contentFormMustache)}, id.RenameTemplateZid: { constHeader{ api.KeyTitle: "Zettelstore Rename Form HTML Template", api.KeyRole: api.ValueRoleConfiguration, api.KeySyntax: syntaxTemplate, + api.KeyCreated: "20200804111624", api.KeyVisibility: api.ValueVisibilityExpert, }, domain.NewContent(contentRenameMustache)}, id.DeleteTemplateZid: { constHeader{ api.KeyTitle: "Zettelstore Delete HTML Template", api.KeyRole: api.ValueRoleConfiguration, api.KeySyntax: syntaxTemplate, + api.KeyCreated: "20200804111624", api.KeyVisibility: api.ValueVisibilityExpert, }, domain.NewContent(contentDeleteMustache)}, id.ListTemplateZid: { constHeader{ api.KeyTitle: "Zettelstore List Zettel HTML Template", api.KeyRole: api.ValueRoleConfiguration, api.KeySyntax: syntaxTemplate, + api.KeyCreated: "20200804111624", api.KeyVisibility: api.ValueVisibilityExpert, }, domain.NewContent(contentListZettelMustache)}, - id.RolesTemplateZid: { - constHeader{ - api.KeyTitle: "Zettelstore List Roles HTML Template", - api.KeyRole: api.ValueRoleConfiguration, - api.KeySyntax: syntaxTemplate, - api.KeyVisibility: api.ValueVisibilityExpert, - }, - domain.NewContent(contentListRolesMustache)}, - id.TagsTemplateZid: { - constHeader{ - api.KeyTitle: "Zettelstore List Tags HTML Template", - api.KeyRole: api.ValueRoleConfiguration, - api.KeySyntax: syntaxTemplate, - api.KeyVisibility: api.ValueVisibilityExpert, - }, - domain.NewContent(contentListTagsMustache)}, id.ErrorTemplateZid: { constHeader{ api.KeyTitle: "Zettelstore Error HTML Template", api.KeyRole: api.ValueRoleConfiguration, api.KeySyntax: syntaxTemplate, + api.KeyCreated: "20210305133215", api.KeyVisibility: api.ValueVisibilityExpert, }, domain.NewContent(contentErrorMustache)}, id.MustParse(api.ZidBaseCSS): { constHeader{ api.KeyTitle: "Zettelstore Base CSS", api.KeyRole: api.ValueRoleConfiguration, api.KeySyntax: "css", + api.KeyCreated: "20200804111624", api.KeyVisibility: api.ValueVisibilityPublic, }, domain.NewContent(contentBaseCSS)}, id.MustParse(api.ZidUserCSS): { constHeader{ api.KeyTitle: "Zettelstore User CSS", api.KeyRole: api.ValueRoleConfiguration, api.KeySyntax: "css", + api.KeyCreated: "20210622110143", api.KeyVisibility: api.ValueVisibilityPublic, }, domain.NewContent([]byte("/* User-defined CSS */"))}, id.RoleCSSMapZid: { constHeader{ api.KeyTitle: "Zettelstore Role to CSS Map", api.KeyRole: api.ValueRoleConfiguration, api.KeySyntax: api.ValueSyntaxNone, + api.KeyCreated: "20220321183214", api.KeyVisibility: api.ValueVisibilityExpert, }, domain.NewContent(nil)}, id.EmojiZid: { constHeader{ api.KeyTitle: "Zettelstore Generic Emoji", api.KeyRole: api.ValueRoleConfiguration, api.KeySyntax: api.ValueSyntaxGif, api.KeyReadOnly: api.ValueTrue, + api.KeyCreated: "20210504175807", api.KeyVisibility: api.ValueVisibilityPublic, }, domain.NewContent(contentEmoji)}, id.TOCNewTemplateZid: { constHeader{ api.KeyTitle: "New Menu", api.KeyRole: api.ValueRoleConfiguration, api.KeySyntax: api.ValueSyntaxZmk, api.KeyLang: api.ValueLangEN, + api.KeyCreated: "20210217161829", api.KeyVisibility: api.ValueVisibilityCreator, }, domain.NewContent(contentNewTOCZettel)}, id.MustParse(api.ZidTemplateNewZettel): { constHeader{ api.KeyTitle: "New Zettel", api.KeyRole: api.ValueRoleZettel, api.KeySyntax: api.ValueSyntaxZmk, + api.KeyCreated: "20201028185209", api.KeyVisibility: api.ValueVisibilityCreator, }, domain.NewContent(nil)}, id.MustParse(api.ZidTemplateNewUser): { constHeader{ api.KeyTitle: "New User", api.KeyRole: api.ValueRoleConfiguration, api.KeySyntax: api.ValueSyntaxNone, + api.KeyCreated: "20201028185209", meta.NewPrefix + api.KeyCredential: "", meta.NewPrefix + api.KeyUserID: "", meta.NewPrefix + api.KeyUserRole: api.ValueUserRoleReader, api.KeyVisibility: api.ValueVisibilityOwner, }, domain.NewContent(nil)}, id.DefaultHomeZid: { constHeader{ - api.KeyTitle: "Home", - api.KeyRole: api.ValueRoleZettel, - api.KeySyntax: api.ValueSyntaxZmk, - api.KeyLang: api.ValueLangEN, + api.KeyTitle: "Home", + api.KeyRole: api.ValueRoleZettel, + api.KeySyntax: api.ValueSyntaxZmk, + api.KeyLang: api.ValueLangEN, + api.KeyCreated: "20210210190757", }, domain.NewContent(contentHomeZettel)}, } //go:embed license.txt @@ -384,16 +392,10 @@ var contentDeleteMustache []byte //go:embed listzettel.mustache var contentListZettelMustache []byte -//go:embed listroles.mustache -var contentListRolesMustache []byte - -//go:embed listtags.mustache -var contentListTagsMustache []byte - //go:embed error.mustache var contentErrorMustache []byte //go:embed base.css var contentBaseCSS []byte Index: box/constbox/context.mustache ================================================================== --- box/constbox/context.mustache +++ box/constbox/context.mustache @@ -1,6 +1,5 @@ - +{{{Content}}} Index: box/constbox/dependencies.zettel ================================================================== --- box/constbox/dependencies.zettel +++ box/constbox/dependencies.zettel @@ -101,10 +101,44 @@ DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. ``` + +=== gopikchr +; URL & Source +: [[https://github.com/gopikchr/gopikchr]] +; License +: MIT License +; Remarks +: Author is [[Zellyn Hunter|https://github.com/zellyn]], he wrote a blog post [[gopikchr: a yakshave|https://zellyn.com/2022/01/gopikchr-a-yakshave/]] about his work. +: Gopikchr was incorporated into the source code of Zettelstore, moving it into package ''zettelstore.de/z/parser/pikchr''. + Later, the source code was changed to adapt it to the needs of Zettelstore. + For details, read README.txt in the appropriate source code folder. +``` +MIT License + +Copyright (c) 2022 gopikchr + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. +``` === hoisie/mustache / cbroglie/mustache ; URL & Source : [[https://github.com/hoisie/mustache]] / [[https://github.com/cbroglie/mustache]] ; License Index: box/constbox/info.mustache ================================================================== --- box/constbox/info.mustache +++ box/constbox/info.mustache @@ -19,32 +19,28 @@ {{#Valid}}
  • {{Zid}}
  • {{/Valid}} {{^Valid}}
  • {{Zid}}
  • {{/Valid}} {{/LocLinks}} {{/HasLocLinks}} -{{#HasSearchLinks}} -

    Searches

    +{{#HasQueryLinks}} +

    Queries

    -{{/HasSearchLinks}} +{{/HasQueryLinks}} {{#HasExtLinks}}

    External

    {{/HasExtLinks}}

    Unlinked

    - +{{{UnLinksContent}}}

    Parts and encodings

    DELETED box/constbox/listroles.mustache Index: box/constbox/listroles.mustache ================================================================== --- box/constbox/listroles.mustache +++ box/constbox/listroles.mustache @@ -1,8 +0,0 @@ - DELETED box/constbox/listtags.mustache Index: box/constbox/listtags.mustache ================================================================== --- box/constbox/listtags.mustache +++ box/constbox/listtags.mustache @@ -1,10 +0,0 @@ - Index: box/constbox/listzettel.mustache ================================================================== --- box/constbox/listzettel.mustache +++ box/constbox/listzettel.mustache @@ -1,9 +1,7 @@

    {{Title}}

    - +
    - +{{{Content}}} Index: box/constbox/zettel.mustache ================================================================== --- box/constbox/zettel.mustache +++ box/constbox/zettel.mustache @@ -9,10 +9,11 @@ {{#HasTags}}· {{#Tags}} {{Text}}{{/Tags}}{{/HasTags}} {{#CanCopy}}· Copy{{/CanCopy}} {{#CanFolge}}· Folge{{/CanFolge}} {{#PrecursorRefs}}
    Precursor: {{{PrecursorRefs}}}{{/PrecursorRefs}} {{#HasExtURL}}
    URL: {{ExtURL}}{{/HasExtURL}} +{{#Author}}
    By {{Author}}{{/Author}} {{{Content}}} {{#HasFolgeLinks}} Index: box/dirbox/dirbox.go ================================================================== --- box/dirbox/dirbox.go +++ box/dirbox/dirbox.go @@ -25,11 +25,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("dir", func(u *url.URL, cdata *manager.ConnectData) (box.ManagedBox, error) { var log *logger.Logger @@ -220,11 +220,11 @@ err = dp.srvSetZettel(ctx, &entry, zettel) if err == nil { err = dp.dirSrv.UpdateDirEntry(&entry) } - dp.notifyChanged(box.OnUpdate, meta.Zid) + dp.notifyChanged(box.OnZettel, meta.Zid) dp.log.Trace().Err(err).Zid(meta.Zid).Msg("CreateZettel") return meta.Zid, err } func (dp *dirBox) GetZettel(ctx context.Context, zid id.Zid) (domain.Zettel, error) { @@ -256,20 +256,20 @@ return nil, err } return m, nil } -func (dp *dirBox) ApplyZid(_ context.Context, handle box.ZidFunc, constraint search.RetrievePredicate) error { +func (dp *dirBox) ApplyZid(_ context.Context, handle box.ZidFunc, constraint query.RetrievePredicate) error { entries := dp.dirSrv.GetDirEntries(constraint) dp.log.Trace().Int("entries", int64(len(entries))).Msg("ApplyZid") for _, entry := range entries { handle(entry.Zid) } return nil } -func (dp *dirBox) ApplyMeta(ctx context.Context, handle box.MetaFunc, constraint search.RetrievePredicate) error { +func (dp *dirBox) ApplyMeta(ctx context.Context, handle box.MetaFunc, constraint query.RetrievePredicate) error { entries := dp.dirSrv.GetDirEntries(constraint) dp.log.Trace().Int("entries", int64(len(entries))).Msg("ApplyMeta") // The following loop could be parallelized if needed for performance. for _, entry := range entries { @@ -305,11 +305,11 @@ } dp.updateEntryFromMetaContent(entry, meta, zettel.Content) dp.dirSrv.UpdateDirEntry(entry) err := dp.srvSetZettel(ctx, entry, zettel) if err == nil { - dp.notifyChanged(box.OnUpdate, zid) + dp.notifyChanged(box.OnZettel, zid) } dp.log.Trace().Zid(zid).Err(err).Msg("UpdateZettel") return err } @@ -354,12 +354,12 @@ dp.dirSrv.RenameDirEntry(&newEntry, curZid) return err } err = dp.srvDeleteZettel(ctx, curEntry, curZid) if err == nil { - dp.notifyChanged(box.OnDelete, curZid) - dp.notifyChanged(box.OnUpdate, newZid) + dp.notifyChanged(box.OnZettel, curZid) + dp.notifyChanged(box.OnZettel, newZid) } dp.log.Trace().Zid(curZid).Zid(newZid).Err(err).Msg("RenameZettel") return err } @@ -384,11 +384,11 @@ if err != nil { return nil } err = dp.srvDeleteZettel(ctx, entry, zid) if err == nil { - dp.notifyChanged(box.OnDelete, zid) + dp.notifyChanged(box.OnZettel, zid) } dp.log.Trace().Zid(zid).Err(err).Msg("DeleteZettel") return err } Index: box/filebox/zipbox.go ================================================================== --- box/filebox/zipbox.go +++ box/filebox/zipbox.go @@ -21,11 +21,11 @@ "zettelstore.de/z/domain" "zettelstore.de/z/domain/id" "zettelstore.de/z/domain/meta" "zettelstore.de/z/input" "zettelstore.de/z/logger" - "zettelstore.de/z/search" + "zettelstore.de/z/query" ) type zipBox struct { log *logger.Logger number int @@ -137,20 +137,20 @@ m, err := zb.readZipMeta(reader, zid, entry) zb.log.Trace().Err(err).Zid(zid).Msg("GetMeta") return m, err } -func (zb *zipBox) ApplyZid(_ context.Context, handle box.ZidFunc, constraint search.RetrievePredicate) error { +func (zb *zipBox) ApplyZid(_ context.Context, handle box.ZidFunc, constraint query.RetrievePredicate) error { entries := zb.dirSrv.GetDirEntries(constraint) zb.log.Trace().Int("entries", int64(len(entries))).Msg("ApplyZid") for _, entry := range entries { handle(entry.Zid) } return nil } -func (zb *zipBox) ApplyMeta(ctx context.Context, handle box.MetaFunc, constraint search.RetrievePredicate) error { +func (zb *zipBox) ApplyMeta(ctx context.Context, handle box.MetaFunc, constraint query.RetrievePredicate) error { reader, err := zip.OpenReader(zb.name) if err != nil { return err } defer reader.Close() Index: box/manager/anteroom.go ================================================================== --- box/manager/anteroom.go +++ box/manager/anteroom.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. //----------------------------------------------------------------------------- @@ -19,12 +19,11 @@ type arAction int const ( arNothing arAction = iota arReload - arUpdate - arDelete + arZettel ) type anteroom struct { num uint64 next *anteroom @@ -43,45 +42,36 @@ func newAnterooms(maxLoad int) *anterooms { return &anterooms{maxLoad: maxLoad} } -func (ar *anterooms) Enqueue(zid id.Zid, action arAction) { - if !zid.IsValid() || action == arNothing || action == arReload { +func (ar *anterooms) EnqueueZettel(zid id.Zid) { + if !zid.IsValid() { return } ar.mx.Lock() defer ar.mx.Unlock() if ar.first == nil { - ar.first = ar.makeAnteroom(zid, action) + ar.first = ar.makeAnteroom(zid, arZettel) ar.last = ar.first return } for room := ar.first; room != nil; room = room.next { if room.reload { continue // Do not put zettel in reload room } - a, ok := room.waiting[zid] - if !ok { - continue - } - switch action { - case a: - return - case arUpdate: - room.waiting[zid] = action - case arDelete: - room.waiting[zid] = action - } - return + if _, ok := room.waiting[zid]; ok { + // Zettel is already waiting. + return + } } if room := ar.last; !room.reload && (ar.maxLoad == 0 || room.curLoad < ar.maxLoad) { - room.waiting[zid] = action + room.waiting[zid] = arZettel room.curLoad++ return } - room := ar.makeAnteroom(zid, action) + room := ar.makeAnteroom(zid, arZettel) ar.last.next = room ar.last = room } func (ar *anterooms) makeAnteroom(zid id.Zid, action arAction) *anteroom { @@ -103,11 +93,11 @@ } func (ar *anterooms) Reload(newZids id.Set) uint64 { ar.mx.Lock() defer ar.mx.Unlock() - newWaiting := createWaitingSet(newZids, arUpdate) + newWaiting := createWaitingSet(newZids) ar.deleteReloadedRooms() if ns := len(newWaiting); ns > 0 { ar.nextNum++ ar.first = &anteroom{num: ar.nextNum, next: ar.first, waiting: newWaiting, curLoad: ns} @@ -120,15 +110,15 @@ ar.first = nil ar.last = nil return 0 } -func createWaitingSet(zids id.Set, action arAction) map[id.Zid]arAction { +func createWaitingSet(zids id.Set) map[id.Zid]arAction { waitingSet := make(map[id.Zid]arAction, len(zids)) for zid := range zids { if zid.IsValid() { - waitingSet[zid] = action + waitingSet[zid] = arZettel } } return waitingSet } Index: box/manager/anteroom_test.go ================================================================== --- box/manager/anteroom_test.go +++ box/manager/anteroom_test.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. //----------------------------------------------------------------------------- @@ -17,25 +17,25 @@ ) func TestSimple(t *testing.T) { t.Parallel() ar := newAnterooms(2) - ar.Enqueue(id.Zid(1), arUpdate) + ar.EnqueueZettel(id.Zid(1)) action, zid, rno := ar.Dequeue() - if zid != id.Zid(1) || action != arUpdate || rno != 1 { - t.Errorf("Expected arUpdate/1/1, but got %v/%v/%v", action, zid, rno) + if zid != id.Zid(1) || action != arZettel || rno != 1 { + t.Errorf("Expected arZettel/1/1, but got %v/%v/%v", action, zid, rno) } - action, zid, _ = ar.Dequeue() - if zid != id.Invalid && action != arDelete { + _, zid, _ = ar.Dequeue() + if zid != id.Invalid { t.Errorf("Expected invalid Zid, but got %v", zid) } - ar.Enqueue(id.Zid(1), arUpdate) - ar.Enqueue(id.Zid(2), arUpdate) + ar.EnqueueZettel(id.Zid(1)) + ar.EnqueueZettel(id.Zid(2)) if ar.first != ar.last { t.Errorf("Expected one room, but got more") } - ar.Enqueue(id.Zid(3), arUpdate) + ar.EnqueueZettel(id.Zid(3)) if ar.first == ar.last { t.Errorf("Expected more than one room, but got only one") } count := 0 @@ -51,58 +51,56 @@ } func TestReset(t *testing.T) { t.Parallel() ar := newAnterooms(1) - ar.Enqueue(id.Zid(1), arUpdate) + ar.EnqueueZettel(id.Zid(1)) ar.Reset() action, zid, _ := ar.Dequeue() if action != arReload || zid != id.Invalid { t.Errorf("Expected reload & invalid Zid, but got %v/%v", action, zid) } ar.Reload(id.NewSet(3, 4)) - ar.Enqueue(id.Zid(5), arUpdate) - ar.Enqueue(id.Zid(5), arDelete) - ar.Enqueue(id.Zid(5), arDelete) - ar.Enqueue(id.Zid(5), arUpdate) + ar.EnqueueZettel(id.Zid(5)) + ar.EnqueueZettel(id.Zid(5)) if ar.first == ar.last || ar.first.next != ar.last /*|| ar.first.next.next != ar.last*/ { t.Errorf("Expected 2 rooms") } action, zid1, _ := ar.Dequeue() - if action != arUpdate { - t.Errorf("Expected arUpdate, but got %v", action) + if action != arZettel { + t.Errorf("Expected arZettel, but got %v", action) } action, zid2, _ := ar.Dequeue() - if action != arUpdate { - t.Errorf("Expected arUpdate, but got %v", action) + if action != arZettel { + t.Errorf("Expected arZettel, but got %v", action) } if !(zid1 == id.Zid(3) && zid2 == id.Zid(4) || zid1 == id.Zid(4) && zid2 == id.Zid(3)) { t.Errorf("Zids must be 3 or 4, but got %v/%v", zid1, zid2) } action, zid, _ = ar.Dequeue() - if zid != id.Zid(5) || action != arUpdate { - t.Errorf("Expected 5/arUpdate, but got %v/%v", zid, action) + if zid != id.Zid(5) || action != arZettel { + t.Errorf("Expected 5/arZettel, but got %v/%v", zid, action) } action, zid, _ = ar.Dequeue() if action != arNothing || zid != id.Invalid { t.Errorf("Expected nothing & invalid Zid, but got %v/%v", action, zid) } ar = newAnterooms(1) ar.Reload(id.NewSet(id.Zid(6))) action, zid, _ = ar.Dequeue() - if zid != id.Zid(6) || action != arUpdate { - t.Errorf("Expected 6/arUpdate, but got %v/%v", zid, action) + if zid != id.Zid(6) || action != arZettel { + t.Errorf("Expected 6/arZettel, but got %v/%v", zid, action) } action, zid, _ = ar.Dequeue() if action != arNothing || zid != id.Invalid { t.Errorf("Expected nothing & invalid Zid, but got %v/%v", action, zid) } ar = newAnterooms(1) - ar.Enqueue(id.Zid(8), arUpdate) + ar.EnqueueZettel(id.Zid(8)) ar.Reload(nil) action, zid, _ = ar.Dequeue() if action != arNothing || zid != id.Invalid { t.Errorf("Expected nothing & invalid Zid, but got %v/%v", action, zid) } } Index: box/manager/box.go ================================================================== --- box/manager/box.go +++ box/manager/box.go @@ -17,11 +17,11 @@ "zettelstore.de/z/box" "zettelstore.de/z/domain" "zettelstore.de/z/domain/id" "zettelstore.de/z/domain/meta" - "zettelstore.de/z/search" + "zettelstore.de/z/query" ) // Conatains all box.Box related functions // Location returns some information where the box is located. @@ -155,50 +155,53 @@ type metaMap map[id.Zid]*meta.Meta // SelectMeta returns all zettel meta data that match the selection // criteria. The result is ordered by descending zettel id. -func (mgr *Manager) SelectMeta(ctx context.Context, s *search.Search) ([]*meta.Meta, error) { +func (mgr *Manager) SelectMeta(ctx context.Context, q *query.Query) ([]*meta.Meta, error) { if msg := mgr.mgrLog.Debug(); msg.Enabled() { - msg.Str("query", s.String()).Msg("SelectMeta") + msg.Str("query", q.String()).Msg("SelectMeta") } mgr.mgrMx.RLock() defer mgr.mgrMx.RUnlock() if !mgr.started { return nil, box.ErrStopped } - searchPred, match := s.RetrieveAndCompileMatch(mgr) - selected, rejected := metaMap{}, id.Set{} - handleMeta := func(m *meta.Meta) { - zid := m.Zid - if rejected.Contains(zid) { - mgr.mgrLog.Trace().Zid(zid).Msg("SelectMeta/alreadyRejected") - return - } - if _, ok := selected[zid]; ok { - mgr.mgrLog.Trace().Zid(zid).Msg("SelectMeta/alreadySelected") - return - } - if match(m) { - selected[zid] = m - mgr.mgrLog.Trace().Zid(zid).Msg("SelectMeta/match") - } else { - rejected.Zid(zid) - mgr.mgrLog.Trace().Zid(zid).Msg("SelectMeta/reject") - } - } - for _, p := range mgr.boxes { - if err := p.ApplyMeta(ctx, handleMeta, searchPred); err != nil { - return nil, err + compSearch := q.RetrieveAndCompile(mgr) + selected := metaMap{} + for _, term := range compSearch.Terms { + rejected := id.Set{} + handleMeta := func(m *meta.Meta) { + zid := m.Zid + if rejected.Contains(zid) { + mgr.mgrLog.Trace().Zid(zid).Msg("SelectMeta/alreadyRejected") + return + } + if _, ok := selected[zid]; ok { + mgr.mgrLog.Trace().Zid(zid).Msg("SelectMeta/alreadySelected") + return + } + if compSearch.PreMatch(m) && term.Match(m) { + selected[zid] = m + mgr.mgrLog.Trace().Zid(zid).Msg("SelectMeta/match") + } else { + rejected.Zid(zid) + mgr.mgrLog.Trace().Zid(zid).Msg("SelectMeta/reject") + } + } + for _, p := range mgr.boxes { + if err := p.ApplyMeta(ctx, handleMeta, term.Retrieve); err != nil { + return nil, err + } } } result := make([]*meta.Meta, 0, len(selected)) for _, m := range selected { result = append(result, m) } - return s.Sort(result), nil + return q.Sort(result), nil } // CanUpdateZettel returns true, if box could possibly update the given zettel. func (mgr *Manager) CanUpdateZettel(ctx context.Context, zettel domain.Zettel) bool { mgr.mgrMx.RLock() Index: box/manager/collect.go ================================================================== --- box/manager/collect.go +++ box/manager/collect.go @@ -54,10 +54,12 @@ data.itags.Add("#" + strings.ToLower(n.Tag)) case *ast.LinkNode: data.addRef(n.Ref) case *ast.EmbedRefNode: data.addRef(n.Ref) + case *ast.CiteNode: + data.addText(n.Key) case *ast.LiteralNode: data.addText(string(n.Content)) } return data } Index: box/manager/enrich.go ================================================================== --- box/manager/enrich.go +++ box/manager/enrich.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. //----------------------------------------------------------------------------- @@ -14,24 +14,90 @@ "context" "strconv" "zettelstore.de/c/api" "zettelstore.de/z/box" + "zettelstore.de/z/domain/id" "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) { + + // Calculate computed, but stored values. + if _, ok := m.Get(api.KeyCreated); !ok { + m.Set(api.KeyCreated, computeCreated(m.Zid)) + } + 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 + // because of other reasons -> ignore this call, do not update metadata return } - m.Set(api.KeyBoxNumber, strconv.Itoa(boxNumber)) computePublished(m) + m.Set(api.KeyBoxNumber, strconv.Itoa(boxNumber)) mgr.idxStore.Enrich(ctx, m) } + +func computeCreated(zid id.Zid) string { + if zid <= 10101000000 { + // A year 0000 is not allowed and therefore an artificaial Zid. + // In the year 0001, the month must be > 0. + // In the month 000101, the day must be > 0. + return "00010101000000" + } + seconds := zid % 100 + if seconds > 59 { + seconds = 59 + } + zid /= 100 + minutes := zid % 100 + if minutes > 59 { + minutes = 59 + } + zid /= 100 + hours := zid % 100 + if hours > 23 { + hours = 23 + } + zid /= 100 + day := zid % 100 + if day < 1 { + day = 1 + } + zid /= 100 + month := zid % 100 + if month < 1 { + month = 1 + } + if month > 12 { + month = 12 + } + year := zid / 100 + switch month { + case 1, 3, 5, 7, 8, 10, 12: + if day > 31 { + day = 32 + } + case 4, 6, 9, 11: + if day > 30 { + day = 30 + } + case 2: + if year%4 != 0 || (year%100 == 0 && year%400 != 0) { + if day > 28 { + day = 28 + } + } else { + if day > 29 { + day = 29 + } + } + } + created := ((((year*100+month)*100+day)*100+hours)*100+minutes)*100 + seconds + return created.String() +} func computePublished(m *meta.Meta) { if _, ok := m.Get(api.KeyPublished); ok { return } @@ -38,10 +104,16 @@ if modified, ok := m.Get(api.KeyModified); ok { if _, ok = meta.TimeValue(modified); ok { m.Set(api.KeyPublished, modified) return } + } + if created, ok := m.Get(api.KeyCreated); ok { + if _, ok = meta.TimeValue(created); ok { + m.Set(api.KeyPublished, created) + return + } } zid := m.Zid.String() if _, ok := meta.TimeValue(zid); ok { m.Set(api.KeyPublished, zid) return Index: box/manager/indexer.go ================================================================== --- box/manager/indexer.go +++ box/manager/indexer.go @@ -107,42 +107,34 @@ start = time.Now() if rno := mgr.idxAr.Reload(zids); rno > 0 { roomNum = rno } mgr.idxMx.Lock() - mgr.idxLastReload = time.Now() + mgr.idxLastReload = time.Now().Local() mgr.idxSinceReload = 0 mgr.idxMx.Unlock() } - case arUpdate: - mgr.idxLog.Debug().Zid(zid).Msg("update") + case arZettel: + mgr.idxLog.Debug().Zid(zid).Msg("zettel") zettel, err := mgr.GetZettel(ctx, zid) if err != nil { - // TODO: on some errors put the zid into a "try later" set + // Zettel was deleted or is not accessible b/c of other reasons + mgr.idxLog.Trace().Zid(zid).Msg("delete") + mgr.idxMx.Lock() + mgr.idxSinceReload++ + mgr.idxMx.Unlock() + mgr.idxDeleteZettel(zid) continue } + mgr.idxLog.Trace().Zid(zid).Msg("update") mgr.idxMx.Lock() if arRoomNum == roomNum { mgr.idxDurReload = time.Since(start) } mgr.idxSinceReload++ mgr.idxMx.Unlock() mgr.idxUpdateZettel(ctx, zettel) - case arDelete: - mgr.idxLog.Debug().Zid(zid).Msg("delete") - if _, err := mgr.GetMeta(ctx, zid); err == nil { - // Zettel was not deleted. This might occur, if zettel was - // deleted in secondary dirbox, but is still present in - // first dirbox (or vice versa). Re-index zettel in case - // a hidden zettel was recovered - mgr.idxLog.Debug().Zid(zid).Msg("not deleted") - mgr.idxAr.Enqueue(zid, arUpdate) - } - mgr.idxMx.Lock() - mgr.idxSinceReload++ - mgr.idxMx.Unlock() - mgr.idxDeleteZettel(zid) } } } func (mgr *Manager) idxSleepService(timer *time.Timer, timerDuration time.Duration) bool { @@ -166,11 +158,11 @@ } func (mgr *Manager) idxUpdateZettel(ctx context.Context, zettel domain.Zettel) { var cData collectData cData.initialize() - collectZettelIndexData(parser.ParseZettel(zettel, "", mgr.rtConfig), &cData) + collectZettelIndexData(parser.ParseZettel(ctx, zettel, "", mgr.rtConfig), &cData) m := zettel.Meta zi := store.NewZettelIndex(m.Zid) mgr.idxCollectFromMeta(ctx, m, zi, &cData) mgr.idxProcessData(ctx, zi, &cData) @@ -177,13 +169,13 @@ toCheck := mgr.idxStore.UpdateReferences(ctx, zi) mgr.idxCheckZettel(toCheck) } func (mgr *Manager) idxCollectFromMeta(ctx context.Context, m *meta.Meta, zi *store.ZettelIndex, cData *collectData) { - for _, pair := range m.Pairs() { + for _, pair := range m.ComputedPairs() { descr := meta.GetDescription(pair.Key) - if descr.IsComputed() { + if descr.IsProperty() { continue } switch descr.Type { case meta.TypeID: mgr.idxUpdateValue(ctx, descr.Inverse, pair.Value, zi) @@ -240,8 +232,8 @@ mgr.idxCheckZettel(toCheck) } func (mgr *Manager) idxCheckZettel(s id.Set) { for zid := range s { - mgr.idxAr.Enqueue(zid, arUpdate) + mgr.idxAr.EnqueueZettel(zid) } } Index: box/manager/manager.go ================================================================== --- box/manager/manager.go +++ box/manager/manager.go @@ -228,14 +228,12 @@ func (mgr *Manager) idxEnqueue(reason box.UpdateReason, zid id.Zid) { switch reason { case box.OnReload: mgr.idxAr.Reset() - case box.OnUpdate: - mgr.idxAr.Enqueue(zid, arUpdate) - case box.OnDelete: - mgr.idxAr.Enqueue(zid, arDelete) + case box.OnZettel: + mgr.idxAr.EnqueueZettel(zid) default: return } select { case mgr.idxReady <- struct{}{}: Index: box/manager/memstore/memstore.go ================================================================== --- box/manager/memstore/memstore.go +++ box/manager/memstore/memstore.go @@ -160,17 +160,22 @@ result := ms.selectWithPred(prefix, strings.HasPrefix) l := len(prefix) if l > 14 { return result } - minZid, err := id.Parse(prefix + "00000000000000"[:14-l]) + maxZid, err := id.Parse(prefix + "99999999999999"[:14-l]) if err != nil { return result } - maxZid, err := id.Parse(prefix + "99999999999999"[:14-l]) - if err != nil { - return result + var minZid id.Zid + if l < 14 && prefix == "0000000000000"[:l] { + minZid = id.Zid(1) + } else { + minZid, err = id.Parse(prefix + "00000000000000"[:14-l]) + if err != nil { + return result + } } for zid, zi := range ms.idx { if minZid <= zid && zid <= maxZid { addBackwardZids(result, zid, zi) } Index: box/manager/store/store.go ================================================================== --- box/manager/store/store.go +++ box/manager/store/store.go @@ -15,11 +15,11 @@ "context" "io" "zettelstore.de/z/domain/id" "zettelstore.de/z/domain/meta" - "zettelstore.de/z/search" + "zettelstore.de/z/query" ) // Stats records statistics about the store. type Stats struct { // Zettel is the number of zettel managed by the indexer. @@ -36,11 +36,11 @@ } // Store all relevant zettel data. There may be multiple implementations, i.e. // memory-based, file-based, based on SQLite, ... type Store interface { - search.Searcher + query.Searcher // Entrich metadata with data from store. Enrich(ctx context.Context, m *meta.Meta) // UpdateReferences for a specific zettel. Index: box/membox/membox.go ================================================================== --- box/membox/membox.go +++ box/membox/membox.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( "mem", @@ -102,11 +102,11 @@ meta.Zid = zid zettel.Meta = meta mb.zettel[zid] = zettel mb.curBytes = newBytes mb.mx.Unlock() - mb.notifyChanged(box.OnUpdate, zid) + mb.notifyChanged(box.OnZettel, zid) mb.log.Trace().Zid(zid).Msg("CreateZettel") return zid, nil } func (mb *memBox) GetZettel(_ context.Context, zid id.Zid) (domain.Zettel, error) { @@ -130,11 +130,11 @@ } mb.log.Trace().Msg("GetMeta") return zettel.Meta.Clone(), nil } -func (mb *memBox) ApplyZid(_ context.Context, handle box.ZidFunc, constraint search.RetrievePredicate) error { +func (mb *memBox) ApplyZid(_ context.Context, handle box.ZidFunc, constraint query.RetrievePredicate) error { mb.mx.RLock() defer mb.mx.RUnlock() mb.log.Trace().Int("entries", int64(len(mb.zettel))).Msg("ApplyZid") for zid := range mb.zettel { if constraint(zid) { @@ -142,11 +142,11 @@ } } return nil } -func (mb *memBox) ApplyMeta(ctx context.Context, handle box.MetaFunc, constraint search.RetrievePredicate) error { +func (mb *memBox) ApplyMeta(ctx context.Context, handle box.MetaFunc, constraint query.RetrievePredicate) error { mb.mx.RLock() defer mb.mx.RUnlock() mb.log.Trace().Int("entries", int64(len(mb.zettel))).Msg("ApplyMeta") for zid, zettel := range mb.zettel { if constraint(zid) { @@ -191,11 +191,11 @@ zettel.Meta = m mb.zettel[m.Zid] = zettel mb.curBytes = newBytes mb.mx.Unlock() - mb.notifyChanged(box.OnUpdate, m.Zid) + mb.notifyChanged(box.OnZettel, m.Zid) mb.log.Trace().Msg("UpdateZettel") return nil } func (*memBox) AllowRenameZettel(context.Context, id.Zid) bool { return true } @@ -218,12 +218,12 @@ meta.Zid = newZid zettel.Meta = meta mb.zettel[newZid] = zettel delete(mb.zettel, curZid) mb.mx.Unlock() - mb.notifyChanged(box.OnDelete, curZid) - mb.notifyChanged(box.OnUpdate, newZid) + mb.notifyChanged(box.OnZettel, curZid) + mb.notifyChanged(box.OnZettel, newZid) mb.log.Trace().Msg("RenameZettel") return nil } func (mb *memBox) CanDeleteZettel(_ context.Context, zid id.Zid) bool { @@ -241,11 +241,11 @@ return box.ErrNotFound } delete(mb.zettel, zid) mb.curBytes -= oldZettel.Length() mb.mx.Unlock() - mb.notifyChanged(box.OnDelete, zid) + mb.notifyChanged(box.OnZettel, zid) mb.log.Trace().Msg("DeleteZettel") return nil } func (mb *memBox) ReadStats(st *box.ManagedBoxStats) { Index: box/notify/directory.go ================================================================== --- box/notify/directory.go +++ box/notify/directory.go @@ -20,11 +20,11 @@ "zettelstore.de/z/box" "zettelstore.de/z/domain/id" "zettelstore.de/z/logger" "zettelstore.de/z/parser" - "zettelstore.de/z/search" + "zettelstore.de/z/query" "zettelstore.de/z/strfun" ) type entrySet map[id.Zid]*DirEntry @@ -107,11 +107,11 @@ } return len(ds.entries) } // GetDirEntries returns a list of directory entries, which satisfy the given constraint. -func (ds *DirService) GetDirEntries(constraint search.RetrievePredicate) []*DirEntry { +func (ds *DirService) GetDirEntries(constraint query.RetrievePredicate) []*DirEntry { ds.mx.RLock() defer ds.mx.RUnlock() if ds.entries == nil { return nil } @@ -258,16 +258,19 @@ case Update: ds.mx.Lock() zid := ds.onUpdateFileEvent(ds.entries, ev.Name) ds.mx.Unlock() if zid != id.Invalid { - ds.notifyChange(box.OnUpdate, zid) + ds.notifyChange(box.OnZettel, zid) } case Delete: ds.mx.Lock() - ds.onDeleteFileEvent(ds.entries, ev.Name) + zid := ds.onDeleteFileEvent(ds.entries, ev.Name) ds.mx.Unlock() + if zid != id.Invalid { + ds.notifyChange(box.OnZettel, zid) + } default: ds.log.Warn().Str("event", fmt.Sprintf("%v", ev)).Msg("Unknown zettel notification event") } } } @@ -280,18 +283,18 @@ return zids } func (ds *DirService) onCreateDirectory(zids id.Slice, prevEntries entrySet) { for _, zid := range zids { - ds.notifyChange(box.OnUpdate, zid) + ds.notifyChange(box.OnZettel, zid) delete(prevEntries, zid) } // These were previously stored, by are not found now. // Notify system that these were deleted, e.g. for updating the index. for zid := range prevEntries { - ds.notifyChange(box.OnDelete, zid) + ds.notifyChange(box.OnZettel, zid) } } func (ds *DirService) onDestroyDirectory() { ds.mx.Lock() @@ -298,11 +301,11 @@ entries := ds.entries ds.entries = nil ds.state = dsMissing ds.mx.Unlock() for zid := range entries { - ds.notifyChange(box.OnDelete, zid) + ds.notifyChange(box.OnZettel, zid) } } var validFileName = regexp.MustCompile(`^(\d{14})`) @@ -344,30 +347,31 @@ if dupName1 != "" { ds.log.Warn().Str("name", dupName1).Msg("Duplicate content (is ignored)") if dupName2 != "" { ds.log.Warn().Str("name", dupName2).Msg("Duplicate content (is ignored)") } + return id.Invalid } return zid } -func (ds *DirService) onDeleteFileEvent(entries entrySet, name string) { +func (ds *DirService) onDeleteFileEvent(entries entrySet, name string) id.Zid { if entries == nil { - return + return id.Invalid } zid := seekZid(name) if zid == id.Invalid { - return + return id.Invalid } entry, found := entries[zid] if !found { - return + return zid } for i, dupName := range entry.UselessFiles { if dupName == name { removeDuplicate(entry, i) - return + return zid } } if name == entry.ContentName { entry.ContentName = "" entry.ContentExt = "" @@ -376,12 +380,12 @@ entry.MetaName = "" ds.replayUpdateUselessFiles(entry) } if entry.ContentName == "" && entry.MetaName == "" { delete(entries, zid) - ds.notifyChange(box.OnDelete, zid) } + return zid } func removeDuplicate(entry *DirEntry, i int) { if len(entry.UselessFiles) == 1 { entry.UselessFiles = nil Index: box/notify/directory_test.go ================================================================== --- box/notify/directory_test.go +++ box/notify/directory_test.go @@ -16,10 +16,11 @@ "zettelstore.de/c/api" "zettelstore.de/z/domain/id" _ "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/pikchr" // Allow to use pikchr parser. _ "zettelstore.de/z/parser/plain" // Allow to use plain parser. _ "zettelstore.de/z/parser/zettelmark" // Allow to use zettelmark parser. ) func TestSeekZid(t *testing.T) { @@ -46,11 +47,11 @@ } func TestNewExtIsBetter(t *testing.T) { extVals := []string{ // Main Formats - api.ValueSyntaxZmk, "markdown", "md", + api.ValueSyntaxZmk, "pikchr", "markdown", "md", // Other supported text formats "css", "txt", api.ValueSyntaxHTML, api.ValueSyntaxNone, "mustache", api.ValueSyntaxText, "plain", // Supported graphics formats api.ValueSyntaxGif, "png", api.ValueSyntaxSVG, "jpeg", "jpg", // Unsupported syntax values Index: box/notify/fsdir.go ================================================================== --- box/notify/fsdir.go +++ box/notify/fsdir.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. //----------------------------------------------------------------------------- @@ -137,25 +137,22 @@ return fsdn.processFileEvent(ev) } return true } -const deleteFsOps = fsnotify.Remove | fsnotify.Rename -const updateFsOps = fsnotify.Create | fsnotify.Write +func (fsdn *fsdirNotifier) processDirEvent(ev *fsnotify.Event) bool { + const deleteFsDirOps = fsnotify.Remove | fsnotify.Rename -func (fsdn *fsdirNotifier) processDirEvent(ev *fsnotify.Event) bool { - if ev.Op&deleteFsOps != 0 { + if ev.Op&deleteFsDirOps != 0 { fsdn.log.Debug().Str("name", fsdn.path).Msg("Directory removed") fsdn.base.Remove(fsdn.path) select { case fsdn.events <- Event{Op: Destroy}: case <-fsdn.done: return false } - return true - } - if ev.Op&fsnotify.Create != 0 { + } else if ev.Op&fsnotify.Create != 0 { err := fsdn.base.Add(fsdn.path) if err != nil { fsdn.log.IfErr(err).Str("name", fsdn.path).Msg("Unable to add directory") select { case fsdn.events <- Event{Op: Error, Err: err}: @@ -163,36 +160,41 @@ return false } } fsdn.log.Debug().Str("name", fsdn.path).Msg("Directory added") return listDirElements(fsdn.log, fsdn.fetcher, fsdn.events, fsdn.done) + } else { + fsdn.log.Trace().Str("name", ev.Name).Str("op", ev.Op.String()).Msg("Directory processed") } return true } func (fsdn *fsdirNotifier) processFileEvent(ev *fsnotify.Event) bool { - if ev.Op&deleteFsOps != 0 { - fsdn.log.Trace().Str("name", ev.Name).Uint("op", uint64(ev.Op)).Msg("File deleted") - select { - case fsdn.events <- Event{Op: Delete, Name: filepath.Base(ev.Name)}: - case <-fsdn.done: - return false - } - return true - } - if ev.Op&updateFsOps != 0 { + const deleteFsFileOps = fsnotify.Remove + const updateFsFileOps = fsnotify.Create | fsnotify.Write | fsnotify.Rename + + if ev.Op&updateFsFileOps != 0 { if fi, err := os.Lstat(ev.Name); err != nil || !fi.Mode().IsRegular() { return true } - fsdn.log.Trace().Str("name", ev.Name).Uint("op", uint64(ev.Op)).Msg("File updated") + fsdn.log.Trace().Str("name", ev.Name).Str("op", ev.Op.String()).Msg("File updated") select { case fsdn.events <- Event{Op: Update, Name: filepath.Base(ev.Name)}: case <-fsdn.done: return false } + } else if ev.Op&deleteFsFileOps != 0 { + fsdn.log.Trace().Str("name", ev.Name).Str("op", ev.Op.String()).Msg("File deleted") + select { + case fsdn.events <- Event{Op: Delete, Name: filepath.Base(ev.Name)}: + case <-fsdn.done: + return false + } + } else { + fsdn.log.Trace().Str("name", ev.Name).Str("op", ev.Op.String()).Msg("File processed") } return true } func (fsdn *fsdirNotifier) Close() { close(fsdn.done) } Index: cmd/cmd_file.go ================================================================== --- cmd/cmd_file.go +++ cmd/cmd_file.go @@ -9,10 +9,11 @@ //----------------------------------------------------------------------------- package cmd import ( + "context" "flag" "fmt" "io" "os" @@ -32,10 +33,11 @@ m, inp, err := getInput(fs.Args()) if m == nil { return 2, err } z := parser.ParseZettel( + context.Background(), domain.Zettel{ Meta: m, Content: domain.NewContent(inp.Src[inp.Pos:]), }, m.GetDefault(api.KeySyntax, api.ValueSyntaxZmk), Index: cmd/cmd_run.go ================================================================== --- cmd/cmd_run.go +++ cmd/cmd_run.go @@ -9,15 +9,18 @@ //----------------------------------------------------------------------------- package cmd import ( + "context" "flag" + "net/http" "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" @@ -44,25 +47,26 @@ kernel.Main.WaitForShutdown() return exitCode, err } func setupRouting(webSrv server.Server, boxManager box.Manager, authManager auth.Manager, rtConfig config.Config) { - protectedBoxManager, authPolicy := authManager.BoxWithPolicy(webSrv, boxManager, rtConfig) + protectedBoxManager, authPolicy := authManager.BoxWithPolicy(boxManager, rtConfig) kern := kernel.Main webLog := kern.GetLogger(kernel.WebService) a := api.New( webLog.Clone().Str("adapter", "api").Child(), - webSrv, authManager, authManager, webSrv, rtConfig, authPolicy) + webSrv, authManager, authManager, rtConfig, authPolicy) wui := webui.New( webLog.Clone().Str("adapter", "wui").Child(), webSrv, authManager, rtConfig, authManager, boxManager, authPolicy) - authLog := kern.GetLogger(kernel.AuthService) - ucLog := kern.GetLogger(kernel.CoreService).WithUser(webSrv) - ucAuthenticate := usecase.NewAuthenticate(authLog, authManager, authManager, boxManager) - ucIsAuth := usecase.NewIsAuthenticated(ucLog, webSrv, authManager) - ucCreateZettel := usecase.NewCreateZettel(ucLog, rtConfig, protectedBoxManager) + var getUser getUserImpl + logAuth := kern.GetLogger(kernel.AuthService) + logUc := kern.GetLogger(kernel.CoreService).WithUser(&getUser) + ucAuthenticate := usecase.NewAuthenticate(logAuth, authManager, authManager, boxManager) + ucIsAuth := usecase.NewIsAuthenticated(logUc, &getUser, authManager) + ucCreateZettel := usecase.NewCreateZettel(logUc, rtConfig, protectedBoxManager) ucGetMeta := usecase.NewGetMeta(protectedBoxManager) ucGetAllMeta := usecase.NewGetAllMeta(protectedBoxManager) ucGetZettel := usecase.NewGetZettel(protectedBoxManager) ucParseZettel := usecase.NewParseZettel(rtConfig, ucGetZettel) ucListMeta := usecase.NewListMeta(protectedBoxManager) @@ -69,18 +73,22 @@ ucEvaluate := usecase.NewEvaluate(rtConfig, ucGetZettel, ucGetMeta, ucListMeta) ucListSyntax := usecase.NewListSyntax(protectedBoxManager) ucListRoles := usecase.NewListRoles(protectedBoxManager) ucListTags := usecase.NewListTags(protectedBoxManager) ucZettelContext := usecase.NewZettelContext(protectedBoxManager, rtConfig) - ucDelete := usecase.NewDeleteZettel(ucLog, protectedBoxManager) - ucUpdate := usecase.NewUpdateZettel(ucLog, protectedBoxManager) - ucRename := usecase.NewRenameZettel(ucLog, protectedBoxManager) + ucDelete := usecase.NewDeleteZettel(logUc, protectedBoxManager) + ucUpdate := usecase.NewUpdateZettel(logUc, protectedBoxManager) + ucRename := usecase.NewRenameZettel(logUc, protectedBoxManager) ucUnlinkedRefs := usecase.NewUnlinkedReferences(protectedBoxManager, rtConfig) - ucRefresh := usecase.NewRefresh(ucLog, protectedBoxManager) + ucRefresh := usecase.NewRefresh(logUc, protectedBoxManager) ucVersion := usecase.NewVersion(kernel.Main.GetConfig(kernel.CoreService, kernel.CoreVersion).(string)) webSrv.Handle("/", wui.MakeGetRootHandler(protectedBoxManager)) + if assetDir := kern.GetConfig(kernel.WebService, kernel.WebAssetDir).(string); assetDir != "" { + const assetPrefix = "/assets/" + webSrv.Handle(assetPrefix, http.StripPrefix(assetPrefix, http.FileServer(http.Dir(assetDir)))) + } // Web user interface if !authManager.IsReadonly() { webSrv.AddZettelRoute('b', server.MethodGet, wui.MakeGetRenameZettelHandler( ucGetMeta, &ucEvaluate)) @@ -93,12 +101,11 @@ webSrv.AddZettelRoute('d', server.MethodPost, wui.MakePostDeleteZettelHandler(&ucDelete)) webSrv.AddZettelRoute('e', server.MethodGet, wui.MakeEditGetZettelHandler(ucGetZettel, ucListRoles, ucListSyntax)) webSrv.AddZettelRoute('e', server.MethodPost, wui.MakeEditSetZettelHandler(&ucUpdate)) } webSrv.AddListRoute('g', server.MethodGet, wui.MakeGetGoActionHandler(&ucRefresh)) - webSrv.AddListRoute('h', server.MethodGet, wui.MakeListHTMLMetaHandler( - ucListMeta, ucListRoles, ucListTags, &ucEvaluate)) + webSrv.AddListRoute('h', server.MethodGet, wui.MakeListHTMLMetaHandler(ucListMeta, &ucEvaluate)) webSrv.AddZettelRoute('h', server.MethodGet, wui.MakeGetHTMLZettelHandler( &ucEvaluate, ucGetMeta)) webSrv.AddListRoute('i', server.MethodGet, wui.MakeGetLoginOutHandler()) webSrv.AddListRoute('i', server.MethodPost, wui.MakePostLoginHandler(&ucAuthenticate)) webSrv.AddZettelRoute('i', server.MethodGet, wui.MakeGetInfoHandler( @@ -114,10 +121,11 @@ webSrv.AddListRoute('m', server.MethodGet, a.MakeListMapMetaHandler(ucListRoles, ucListTags)) webSrv.AddZettelRoute('m', server.MethodGet, a.MakeGetMetaHandler(ucGetMeta)) webSrv.AddZettelRoute('o', server.MethodGet, a.MakeGetOrderHandler( usecase.NewZettelOrder(protectedBoxManager, ucEvaluate))) webSrv.AddZettelRoute('p', server.MethodGet, a.MakeGetParsedZettelHandler(ucParseZettel)) + webSrv.AddListRoute('q', server.MethodGet, a.MakeQueryHandler(ucListMeta)) webSrv.AddZettelRoute('u', server.MethodGet, a.MakeListUnlinkedMetaHandler( ucGetMeta, ucUnlinkedRefs, &ucEvaluate)) webSrv.AddZettelRoute('v', server.MethodGet, a.MakeGetEvalZettelHandler(ucEvaluate)) webSrv.AddListRoute('x', server.MethodGet, a.MakeGetDataHandler(ucVersion)) webSrv.AddListRoute('x', server.MethodPost, a.MakePostCommandHandler(&ucIsAuth, &ucRefresh)) @@ -137,5 +145,9 @@ if authManager.WithAuth() { webSrv.SetUserRetriever(usecase.NewGetUserByZid(boxManager)) } } + +type getUserImpl struct{} + +func (*getUserImpl) GetUser(ctx context.Context) *meta.Meta { return server.GetUser(ctx) } Index: cmd/main.go ================================================================== --- cmd/main.go +++ cmd/main.go @@ -9,19 +9,21 @@ //----------------------------------------------------------------------------- package cmd import ( + "crypto/sha256" "errors" "flag" "fmt" "net" "net/url" "os" "runtime/debug" "strconv" "strings" + "time" "zettelstore.de/c/api" "zettelstore.de/z/auth" "zettelstore.de/z/auth/impl" "zettelstore.de/z/box" @@ -167,10 +169,12 @@ } } const ( keyAdminPort = "admin-port" + keyAssetDir = "asset-dir" + keyBaseURL = "base-url" keyDebug = "debug-mode" keyDefaultDirBoxType = "default-dir-box-type" keyInsecureCookie = "insecure-cookie" keyListenAddr = "listen-addr" keyLogLevel = "log-level" @@ -218,20 +222,28 @@ } ok = setConfigValue( ok, kernel.WebService, kernel.WebListenAddress, cfg.GetDefault(keyListenAddr, "127.0.0.1:23123")) - ok = setConfigValue(ok, kernel.WebService, kernel.WebURLPrefix, cfg.GetDefault(keyURLPrefix, "/")) + if val, found := cfg.Get(keyBaseURL); found { + ok = setConfigValue(ok, kernel.WebService, kernel.WebBaseURL, val) + } + if val, found := cfg.Get(keyURLPrefix); found { + ok = setConfigValue(ok, kernel.WebService, kernel.WebURLPrefix, val) + } ok = setConfigValue(ok, kernel.WebService, kernel.WebSecureCookie, !cfg.GetBool(keyInsecureCookie)) ok = setConfigValue(ok, kernel.WebService, kernel.WebPersistentCookie, cfg.GetBool(keyPersistentCookie)) if val, found := cfg.Get(keyMaxRequestSize); found { ok = setConfigValue(ok, kernel.WebService, kernel.WebMaxRequestSize, val) } ok = setConfigValue( ok, kernel.WebService, kernel.WebTokenLifetimeAPI, cfg.GetDefault(keyTokenLifetimeAPI, "")) ok = setConfigValue( ok, kernel.WebService, kernel.WebTokenLifetimeHTML, cfg.GetDefault(keyTokenLifetimeHTML, "")) + if val, found := cfg.Get(keyAssetDir); found { + ok = setConfigValue(ok, kernel.WebService, kernel.WebAssetDir, val) + } if !ok { return errors.New("unable to set configuration") } return nil @@ -276,10 +288,12 @@ secret := cfg.GetDefault("secret", "") if len(secret) < 16 && cfg.GetDefault(keyOwner, "") != "" { fmt.Fprintf(os.Stderr, "secret must have at least length 16 when authentication is enabled, but is %q\n", secret) return 2 } + cfg.Delete("secret") + secret = fmt.Sprintf("%x", sha256.Sum256([]byte(secret))) kern.SetCreators( func(readonly bool, owner id.Zid) (auth.Manager, error) { return impl.New(readonly, owner, secret), nil }, @@ -319,13 +333,16 @@ var cpuprofile = flag.String("cpuprofile", "", "write cpu profile to `file`") var memprofile = flag.String("memprofile", "", "write memory profile to `file`") // Main is the real entrypoint of the zettelstore. func Main(progName, buildVersion string) int { - fullVersion := retrieveFullVersion(buildVersion) - kernel.Main.SetConfig(kernel.CoreService, kernel.CoreProgname, progName) - kernel.Main.SetConfig(kernel.CoreService, kernel.CoreVersion, fullVersion) + info := retrieveVCSInfo(buildVersion) + fullVersion := info.revision + if info.dirty { + fullVersion += "-dirty" + } + kernel.Main.Setup(progName, fullVersion, info.time) flag.Parse() if *cpuprofile != "" || *memprofile != "" { if *cpuprofile != "" { kernel.Main.StartProfiling(kernel.ProfileCPU, *cpuprofile) } else { @@ -338,26 +355,38 @@ return runSimple() } return executeCommand(args[0], args[1:]...) } -func retrieveFullVersion(version string) string { +type vcsInfo struct { + revision string + dirty bool + time time.Time +} + +func retrieveVCSInfo(version string) vcsInfo { + buildTime := time.Date(1970, 1, 1, 0, 0, 0, 0, time.UTC) info, ok := debug.ReadBuildInfo() if !ok { - return version + return vcsInfo{revision: version, dirty: false, time: buildTime} } - var revision, dirty string + result := vcsInfo{time: buildTime} for _, kv := range info.Settings { switch kv.Key { case "vcs.revision": - revision = "+" + kv.Value + revision := "+" + kv.Value if len(revision) > 11 { revision = revision[:11] } + result.revision = version + revision case "vcs.modified": if kv.Value == "true" { - dirty = "-dirty" + result.dirty = true + } + case "vcs.time": + if t, err := time.Parse(time.RFC3339, kv.Value); err == nil { + result.time = t } } } - return version + revision + dirty + return result } Index: cmd/register.go ================================================================== --- cmd/register.go +++ cmd/register.go @@ -25,8 +25,9 @@ _ "zettelstore.de/z/encoder/zmkenc" // Allow to use zmk encoder. _ "zettelstore.de/z/kernel/impl" // Allow kernel implementation to create itself _ "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/pikchr" // Allow to use PIC/Pikchr parser. _ "zettelstore.de/z/parser/plain" // Allow to use plain parser. _ "zettelstore.de/z/parser/zettelmark" // Allow to use zettelmark parser. ) Index: config/config.go ================================================================== --- config/config.go +++ config/config.go @@ -10,24 +10,33 @@ // Package config provides functions to retrieve runtime configuration data. package config import ( - "zettelstore.de/c/api" + "context" + "zettelstore.de/z/domain/id" "zettelstore.de/z/domain/meta" ) + +// Key values that are supported by Config.Get +const ( + KeyFooterHTML = "footer-html" + // api.KeyLang + KeyMarkerExternal = "marker-external" +) // Config allows to retrieve all defined configuration values that can be changed during runtime. type Config interface { AuthConfig + + // Get returns the value of the given key. It searches first in the given metadata, + // then in the data of the current user, and at last in the system-wide data. + Get(ctx context.Context, m *meta.Meta, key string) string // AddDefaultValues enriches the given meta data with its default values. - AddDefaultValues(m *meta.Meta) *meta.Meta - - // GetDefaultLang returns the current value of the "default-lang" key. - GetDefaultLang() string + AddDefaultValues(context.Context, *meta.Meta) *meta.Meta // GetSiteName returns the current value of the "site-name" key. GetSiteName() string // GetHomeZettel returns the value of the "home-zettel" key. @@ -39,17 +48,10 @@ // GetYAMLHeader returns the current value of the "yaml-header" key. GetYAMLHeader() bool // GetZettelFileSyntax returns the current value of the "zettel-file-syntax" key. GetZettelFileSyntax() []string - - // GetMarkerExternal returns the current value of the "marker-external" key. - GetMarkerExternal() string - - // GetFooterHTML returns HTML code that should be embedded into the footer - // of each WebUI page. - GetFooterHTML() string } // AuthConfig are relevant configuration values for authentication. type AuthConfig interface { // GetSimpleMode returns true if system tuns in simple-mode. @@ -59,14 +61,5 @@ GetExpertMode() bool // GetVisibility returns the visibility value of the metadata. GetVisibility(m *meta.Meta) meta.Visibility } - -// GetLang returns the value of the "lang" key of the given meta. If there is -// no such value, GetDefaultLang is returned. -func GetLang(m *meta.Meta, cfg Config) string { - if val, ok := m.Get(api.KeyLang); ok { - return val - } - return cfg.GetDefaultLang() -} Index: docs/manual/00001004010000.zettel ================================================================== --- docs/manual/00001004010000.zettel +++ docs/manual/00001004010000.zettel @@ -1,11 +1,12 @@ id: 00001004010000 title: Zettelstore startup configuration role: manual tags: #configuration #manual #zettelstore syntax: zmk -modified: 20220419193611 +created: 20210126175322 +modified: 20220914183434 The configuration file, as specified by the ''-c CONFIGFILE'' [[command line option|00001004051000]], allows you to specify some startup options. These options cannot be stored in a [[configuration zettel|00001004020000]] because either they are needed before Zettelstore can start or because of security reasons. For example, Zettelstore need to know in advance, on which network address is must listen or where zettel are stored. An attacker that is able to change the owner can do anything. @@ -22,10 +23,28 @@ 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"" +; [!asset-dir|''asset-dir''] +: Allows to specify a directory whose files are allowed be transferred directly with the help of the web server. + The URL prefix for these files is ''/assets/''. + You can use this if you want to transfer files that are too large for a note to users. + Examples would be presentation files, PDF files, music files or video files. + + Files within the given directory will not be managed by Zettelstore.[^They will be managed by Zettelstore just in the case that the directory is one of the configured [[boxes|#box-uri-x]].] + + If you specify only the URL prefix, then the contents of the directory are listed to the user. + To avoid this, create an empty file in the directory named ""index.html"". + + Default: """", no asset directory is set, the URL prefix ''/assets/'' is invalid. +; [!base-url|''base-url''] +: Sets the absolute base URL for the service. + + Note: [[''url-prefix''|#url-prefix]] must be the suffix of ''base-url'', otherwise the web service will not start. + + Default: ""http://127.0.0.1:23123/"". ; [!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. @@ -104,13 +123,15 @@ It is automatically extended, when a new HTML view is rendered. Default: ""60"". ; [!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). + + Note: ''url-prefix'' must be the suffix of [[''base-url''|#base-url]], otherwise the web service will not start. Default: ""/"". This allows to use a forwarding proxy [[server|00001010090100]] in front of the Zettelstore. ; [!verbose-mode|''verbose-mode''] : Be more verbose when logging data, if set to a [[true value|00001006030500]]. Default: ""false"" Index: docs/manual/00001004020000.zettel ================================================================== --- docs/manual/00001004020000.zettel +++ docs/manual/00001004020000.zettel @@ -1,11 +1,12 @@ id: 00001004020000 title: Configure the running Zettelstore role: manual tags: #configuration #manual #zettelstore syntax: zmk -modified: 20220628110920 +created: 20210126175322 +modified: 20220827180953 You can configure a running Zettelstore by modifying the special zettel with the ID [[00000000000100]]. This zettel is called __configuration zettel__. The following metadata keys change the appearance / behavior of Zettelstore: @@ -12,17 +13,10 @@ ; [!default-copyright|''default-copyright''] : Copyright value to be used when rendering content. Can be overwritten in a zettel with [[meta key|00001006020000]] ''copyright''. Default: (the empty string). -; [!default-lang|''default-lang''] -: Default language to be used when displaying content. - Can be overwritten in a zettel with [[meta key|00001006020000]] ''lang''. Default: ""en"". - - This value is also used to specify the language for all non-zettel content, e.g. lists or search results. - - Use values according to the language definition of [[RFC-5646|https://tools.ietf.org/html/rfc5646]]. ; [!default-license|''default-license''] : License value to be used when rendering content. Can be overwritten in a zettel with [[meta key|00001006020000]] ''license''. Default: (the empty string). ; [!default-visibility|''default-visibility''] @@ -41,10 +35,19 @@ : Specifies the identifier of the zettel, that should be presented for the default view / home view. If not given or if the identifier does not identify a zettel, the zettel with the identifier ''00010000000000'' is shown. ; [!marker-external|''marker-external''] : Some HTML code that is displayed after a [[reference to external material|00001007040310]]. Default: ""&\#10138;"", to display a ""➚"" sign. +; [!lang|''lang''] +: Language to be used when displaying content. + + Default: ""en"". + + This value is used as a default value, if it is not set in an user's zettel or in a zettel. + It is also used to specify the language for all non-zettel content, e.g. lists or search results. + + Use values according to the language definition of [[RFC-5646|https://tools.ietf.org/html/rfc5646]]. ; [!max-transclusions|''max-transclusions''] : Maximum number of indirect transclusion. This is used to avoid an exploding ""transclusion bomb"", a form of a [[billion laughs attack|https://en.wikipedia.org/wiki/Billion_laughs_attack]]. Default: ""1024"". ; [!site-name|''site-name''] Index: docs/manual/00001004101000.zettel ================================================================== --- docs/manual/00001004101000.zettel +++ docs/manual/00001004101000.zettel @@ -1,11 +1,11 @@ id: 00001004101000 title: List of supported commands of the administrator console role: manual tags: #configuration #manual #zettelstore syntax: zmk -modified: 20220218133526 +modified: 20220823194553 ; [!bye|''bye''] : Closes the connection to the administrator console. ; [!config|''config SERVICE''] : Displays all valid configuration keys for the given service. @@ -71,10 +71,12 @@ See the [[Go documentation|https://pkg.go.dev/runtime/pprof#Profile]] for details. This feature is dependent on the internal implementation language of Zettelstore, Go. It may be removed without any further notice at any time. In most cases, it is a tool for software developers to optimize Zettelstore's internal workings. +; [!refresh|''refresh''] +: Refresh all internal data about zettel. ; [!restart|''restart SERVICE''] : Restart the given service and all other that depend on this. ; [!services|''services''] : Displays s list of all available services and their current status. ; [!set-config|''set-config SERVICE KEY VALUE''] Index: docs/manual/00001005090000.zettel ================================================================== --- docs/manual/00001005090000.zettel +++ docs/manual/00001005090000.zettel @@ -1,11 +1,12 @@ id: 00001005090000 title: List of predefined zettel role: manual tags: #manual #reference #zettelstore syntax: zmk -modified: 20220321192401 +created: 20210126175322 +modified: 20220909180240 The following table lists all predefined zettel with their purpose. |= Identifier :|= Title | Purpose | [[00000000000001]] | Zettelstore Version | Contains the version string of the running Zettelstore @@ -20,25 +21,23 @@ | [[00000000000092]] | Zettelstore Supported Parser | Lists all supported values for metadata [[syntax|00001006020000#syntax]] that are recognized by Zettelstore | [[00000000000096]] | Zettelstore Startup Configuration | Contains the effective values of the [[startup configuration|00001004010000]] | [[00000000000100]] | Zettelstore Runtime Configuration | Allows to [[configure Zettelstore at runtime|00001004020000]] | [[00000000010100]] | Zettelstore Base HTML Template | Contains the general layout of the HTML view | [[00000000010200]] | Zettelstore Login Form HTML Template | Layout of the login form, when authentication is [[enabled|00001010040100]] -| [[00000000010300]] | Zettelstore List Meta HTML Template | Used when displaying a list of zettel +| [[00000000010300]] | Zettelstore List Zettel HTML Template | Used when displaying a list of zettel | [[00000000010401]] | Zettelstore Detail HTML Template | Layout for the HTML detail view of one zettel | [[00000000010402]] | Zettelstore Info HTML Template | Layout for the information view of a specific zettel | [[00000000010403]] | Zettelstore Form HTML Template | Form that is used to create a new or to change an existing zettel that contains text | [[00000000010404]] | Zettelstore Rename Form HTML Template | View that is displayed to change the [[zettel identifier|00001006050000]] | [[00000000010405]] | Zettelstore Delete HTML Template | View to confirm the deletion of a zettel -| [[00000000010500]] | Zettelstore List Roles HTML Template | Layout for listing all roles -| [[00000000010600]] | Zettelstore List Tags HTML Template | Layout of tags lists | [[00000000020001]] | Zettelstore Base CSS | System-defined CSS file that is included by the [[Base HTML Template|00000000010100]] | [[00000000025001]] | Zettelstore User CSS | User-defined CSS file that is included by the [[Base HTML Template|00000000010100]] -| [[00000000029000]] | Zettelstore Role to CSS Map | Maps [[role|00001006020000#role]] to a zettel identifier that is included by the [[Base HTML Template|00000000010100]] as an CSS file +| [[00000000029000]] | Zettelstore Role to CSS Map | [[Maps|00001017000000#role-css]] [[role|00001006020000#role]] to a zettel identifier that is included by the [[Base HTML Template|00000000010100]] as an CSS file | [[00000000040001]] | Generic Emoji | Image that is shown if [[original image reference|00001007040322]] is invalid | [[00000000090000]] | New Menu | Contains items that should contain in the zettel template menu | [[00000000090001]] | New Zettel | Template for a new zettel with role ""[[zettel|00001006020100]]"" | [[00000000090002]] | New User | Template for a new [[user zettel|00001010040200]] | [[00010000000000]] | Home | Default home zettel, contains some welcome information If a zettel is not linked, it is not accessible for the current user. **Important:** All identifier may change until a stable version of the software is released. Index: docs/manual/00001006020000.zettel ================================================================== --- docs/manual/00001006020000.zettel +++ docs/manual/00001006020000.zettel @@ -1,19 +1,23 @@ id: 00001006020000 title: Supported Metadata Keys role: manual tags: #manual #meta #reference #zettel #zettelstore syntax: zmk -modified: 20220810111207 +created: 20210126175322 +modified: 20220915181826 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]] and the value of [[''content-tags''|#content-tags]]. +; [!author|''author''] +: A string value describing the author of a zettel. + If given, it will be shown in the [[web user interface|00001014000000]] for the zettel. ; [!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''|#backward]], 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. @@ -24,10 +28,21 @@ ; [!content-tags|''content-tags''] : A property that contains all [[inline tags|00001007040000#tag]] defined within the content. ; [!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. +; [!created|''created''] +: Date and time when a zettel was created through Zettelstore. + If you create 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. + + If it is not stored within a zettel, it will be computed based on the value of the [[Zettel Identifier|00001006050000]]: if it contains a value >= 19700101000000, it will be coerced to da date/time; otherwise the version time of the running software will be used. + + Please note that the value von ''created'' will be different (in most cases) to the value of [[''id''|#id]] / the zettel identifier, because it is exact up to the second. + When calculating a zettel identifier, Zettelstore tries to set the second value to zero, if possible. ; [!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"". @@ -41,11 +56,13 @@ : Contains the [[zettel identifier|00001006050000]], as given by the Zettelstore. It cannot be set manually, because it is a computed value. ; [!lang|''lang''] : Language for the zettel. Mostly used for HTML rendering of the zettel. - If not given, the value ''default-lang'' from the [[configuration zettel|00001004020000#default-lang]] will be used. + + If not given, the value ''lang'' from the zettel of the [[current user|00001010040200]] will be used. + If that value is also not available, it is read from the [[configuration zettel|00001004020000#lang]] will be used. Use values according to the language definition of [[RFC-5646|https://tools.ietf.org/html/rfc5646]]. ; [!license|''license''] : Defines a license string that will be rendered. If not given, the value ''default-license'' from the [[configuration zettel|00001004020000#default-license]] will be used. ; [!modified|''modified''] @@ -57,15 +74,16 @@ ; [!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. + If [[''modified''|#modified]] is set with a valid timestamp, it contains the its value. + Otherwise, if [[''created''|#created]] is set with a valid timestamp, it contains the its value. Otherwise, if the zettel identifier contains a valid timestamp, the identifier is used. In all other cases, this property is not set. - It can be used for [[sorting|00001012052000]] zettel based on their publication date. + It can be used for [[sorting|00001007700000]] zettel based on their publication date. It is a computed value. There is no need to set it via Zettelstore. ; [!read-only|''read-only''] : Marks a zettel as read-only. Index: docs/manual/00001006031000.zettel ================================================================== --- docs/manual/00001006031000.zettel +++ docs/manual/00001006031000.zettel @@ -1,17 +1,20 @@ id: 00001006031000 title: Credential Key Type role: manual tags: #manual #meta #reference #zettel #zettelstore syntax: zmk +created: 20210212135017 +modified: 20220914130324 Values of this type denote a credential value, e.g. an encrypted password. === Allowed values All printable characters are allowed. Since a credential contains some kind of secret, the sequence of characters might have some hidden syntax to be interpreted by other parts of Zettelstore. -=== Match operator -A credential never matches to any other value. +=== Query operators +A credential never compares to any other value. +A comparison will never match in any way. === Sorting If a list of zettel should be sorted based on a credential value, the identifier of the respective zettel is used instead. Index: docs/manual/00001006031500.zettel ================================================================== --- docs/manual/00001006031500.zettel +++ docs/manual/00001006031500.zettel @@ -1,26 +1,25 @@ id: 00001006031500 title: EString Key Type role: manual tags: #manual #meta #reference #zettel #zettelstore syntax: zmk +created: 20210212135017 +modified: 20220914130448 Values of this type are just a sequence of character, possibly an empty sequence. An EString is the most general metadata key type, as it places no restrictions to the character sequence.[^Well, there are some minor restrictions that follow from the [[metadata syntax|00001006010000]].] === Allowed values All printable characters are allowed. -=== Match operator -A value matches an EString value, if the first value is part of the EString value. -This check is done case-insensitive. - -For example, ""hell"" matches ""Hello"". +=== Query operator +All comparisons are done case-insensitive, i.e. ""hell"" will be the prefix of ""Hello"". === Sorting To sort two values, the underlying encoding is used to determine which value is less than the other. Uppercase letters are typically interpreted as less than their corresponding lowercase letters, i.e. ``A < a``. Comparison is done character-wise by finding the first difference in the respective character sequence. For example, ``abc > aBc``. Index: docs/manual/00001006032000.zettel ================================================================== --- docs/manual/00001006032000.zettel +++ docs/manual/00001006032000.zettel @@ -1,20 +1,21 @@ id: 00001006032000 title: Identifier Key Type role: manual tags: #manual #meta #reference #zettel #zettelstore syntax: zmk +created: 20210212135017 +modified: 20220914134914 Values of this type denote a [[zettel identifier|00001006050000]]. === Allowed values Must be a sequence of 14 digits (""0""--""9""). -=== Match operator -A value matches an identifier value, if the first value is the prefix of the identifier value. - +=== Query operator +Comparison is done with the string representation of the identifiers. For example, ""000010"" matches ""[[00001006032000]]"". === Sorting Sorting is done by comparing the [[String|00001006033500]] values. If both values are identifiers, this works well because both have the same length. Index: docs/manual/00001006032500.zettel ================================================================== --- docs/manual/00001006032500.zettel +++ docs/manual/00001006032500.zettel @@ -1,21 +1,22 @@ id: 00001006032500 title: IdentifierSet Key Type role: manual tags: #manual #meta #reference #zettel #zettelstore syntax: zmk -modified: 20220111103731 +created: 20210212135017 +modified: 20220914131354 Values of this type denote a (sorted) set of [[zettel identifier|00001006050000]]. A set is different to a list, as no duplicate values are allowed. === Allowed values Must be at least one sequence of 14 digits (""0""--""9""), separated by space characters. -=== Match operator -A value matches an identifier set value, if the first value is a prefix of one of the identifier value. +=== Query operator +A value matches an identifier set value, if the value matches any of the identifier set values. -For example, ""000010"" matches ""[[00001006032000]] [[00001006032500]]"". +For example, ""000010060325"" is a prefix ""[[00001006032000]] [[00001006032500]]"". === Sorting Sorting is done by comparing the [[String|00001006033500]] values. Index: docs/manual/00001006033000.zettel ================================================================== --- docs/manual/00001006033000.zettel +++ docs/manual/00001006033000.zettel @@ -1,18 +1,18 @@ id: 00001006033000 title: Number Key Type role: manual tags: #manual #meta #reference #zettel #zettelstore syntax: zmk +created: 20210212135017 +modified: 20220914131211 Values of this type denote a numeric integer value. === Allowed values Must be a sequence of digits (""0""--""9""), optionally prefixed with a ""-"" or a ""+"" character. -=== Match operator -The match operator is the equals operator, i.e. two values must be numeric equal to match. - -This includes that ""+12"" is equal to ""12"", therefore both values match. +=== Query operator +All comparisons are done on the given string representation of the number, ""+12"" will be treated as a different number of ""12"". === Sorting Sorting is done by comparing the numeric values. Index: docs/manual/00001006033500.zettel ================================================================== --- docs/manual/00001006033500.zettel +++ docs/manual/00001006033500.zettel @@ -1,25 +1,24 @@ id: 00001006033500 title: String Key Type role: manual tags: #manual #meta #reference #zettel #zettelstore syntax: zmk +created: 20210212135017 +modified: 20220914130505 Values of this type are just a sequence of character, but not an empty sequence. === Allowed values All printable characters are allowed. There must be at least one such character. -=== Match operator -A value matches a String value, if the first value is part of the String value. -This check is done case-insensitive. - -For example, ""hell"" matches ""Hello"". +=== Query operator +All comparisons are done case-insensitive, i.e. ""hell"" will be the prefix of ""Hello"". === Sorting To sort two values, the underlying encoding is used to determine which value is less than the other. Uppercase letters are typically interpreted as less than their corresponding lowercase letters, i.e. ``A < a``. Comparison is done character-wise by finding the first difference in the respective character sequence. For example, ``abc > aBc``. Index: docs/manual/00001006034000.zettel ================================================================== --- docs/manual/00001006034000.zettel +++ docs/manual/00001006034000.zettel @@ -1,11 +1,12 @@ id: 00001006034000 title: TagSet Key Type role: manual tags: #manual #meta #reference #zettel #zettelstore syntax: zmk -modified: 20220218130413 +created: 20210212135017 +modified: 20220914131048 Values of this type denote a (sorted) set of tags. A set is different to a list, as no duplicate values are allowed. @@ -13,16 +14,10 @@ Every tag must must begin with the number sign character (""''#''"", U+0023), followed by at least one printable character. Tags are separated by space characters. All characters are mapped to their lower case values. -=== Match operator -It depends of the first character of a search string how it is matched against a tag set value: - -* If the first character of the search string is a number sign character, - it must exactly match one of the values of a tag. -* In other cases, the search string must be the prefix of at least one tag. - -Conceptually, all number sign characters are removed at the beginning of the search string and of all tags. +=== Query operator +All comparisons are done case-sensitive, i.e. ""#hell"" will not be the prefix of ""#Hello"". === Sorting Sorting is done by comparing the [[String|00001006033500]] values. Index: docs/manual/00001006034500.zettel ================================================================== --- docs/manual/00001006034500.zettel +++ docs/manual/00001006034500.zettel @@ -1,11 +1,12 @@ id: 00001006034500 title: Timestamp Key Type role: manual tags: #manual #meta #reference #zettel #zettelstore syntax: zmk -modified: 20210511131903 +created: 20210212135017 +modified: 20220914130919 Values of this type denote a point in time. === Allowed values Must be a sequence of 14 digits (""0""--""9"") (same as an [[Identifier|00001006032000]]), with the restriction that is conforms to the pattern ""YYYYMMDDhhmmss"". @@ -15,14 +16,12 @@ * DD is the day, * hh is the hour, * mm is the minute, * ss is the second. -=== Match operator -A value matches a timestamp value, if the first value is the prefix of the timestamp value. - -For example, ""202102"" matches ""20210212143200"". +=== Query operator +All comparisons assume that up to 14 digits are given. === Sorting Sorting is done by comparing the [[String|00001006033500]] values. If both values are timestamp values, this works well because both have the same length. Index: docs/manual/00001006035000.zettel ================================================================== --- docs/manual/00001006035000.zettel +++ docs/manual/00001006035000.zettel @@ -1,19 +1,19 @@ id: 00001006035000 title: URL Key Type role: manual tags: #manual #meta #reference #zettel #zettelstore syntax: zmk +created: 20210212135017 +modified: 20220914130809 Values of this type denote an URL. === Allowed values All characters of an URL / URI are allowed. -=== Match operator -A value matches a URL value, if the first value is part of the URL value. -This check is done case-insensitive. - -For example, ""hell"" matches ""http://example.com/Hello"". +=== Query operator +All comparisons are done case-insensitive. +For example, ""hello"" is the suffix of ""http://example.com/Hello"". === Sorting Sorting is done by comparing the [[String|00001006033500]] values. Index: docs/manual/00001006035500.zettel ================================================================== --- docs/manual/00001006035500.zettel +++ docs/manual/00001006035500.zettel @@ -1,19 +1,20 @@ id: 00001006035500 title: Word Key Type role: manual tags: #manual #meta #reference #zettel #zettelstore syntax: zmk -modified: 20210817201304 +created: 20210212135017 +modified: 20220914130655 Values of this type denote a single word. === Allowed values Must be a non-empty sequence of characters, but without the space character. All characters are mapped to their lower case values. -=== Match operator -A value matches a word value, if both value are character-wise equal, ignoring upper / lower case. +=== Query operator +All comparisons are done case-insensitive, i.e. ""hell"" will be the prefix of ""Hello"". === Sorting Sorting is done by comparing the [[String|00001006033500]] values. Index: docs/manual/00001006036000.zettel ================================================================== --- docs/manual/00001006036000.zettel +++ docs/manual/00001006036000.zettel @@ -1,19 +1,20 @@ id: 00001006036000 title: WordSet Key Type role: manual tags: #manual #meta #reference #zettel #zettelstore syntax: zmk -modified: 20220724201056 +created: 20210212135017 +modified: 20220914130725 Values of this type denote a (sorted) set of [[words|00001006035500]]. A set is different to a list, as no duplicate values are allowed. === Allowed values Must be a sequence of at least one word, separated by space characters. -=== Match operator -A value matches a WordSet value, if the first value is equal to one of the word values in the word set. +=== Query operator +All comparisons are done case-insensitive, i.e. ""hell"" will be the prefix of ""World, Hello"". === Sorting Sorting is done by comparing the [[String|00001006033500]] values. Index: docs/manual/00001006036500.zettel ================================================================== --- docs/manual/00001006036500.zettel +++ docs/manual/00001006036500.zettel @@ -1,25 +1,27 @@ id: 00001006036500 title: Zettelmarkup Key Type role: manual tags: #manual #meta #reference #zettel #zettelstore syntax: zmk +created: 20210212135017 +modified: 20220914135405 Values of this type are [[String|00001006033500]] values, interpreted as [[Zettelmarkup|00001007000000]]. === Allowed values All printable characters are allowed. There must be at least one such character. -=== Match operator -A value matches a String value, if the first value is part of the String value. -This check is done case-insensitive. +=== Query operator +Comparison is done similar to the full-text search: both the value to compare and the metadata value are normalized according to Unicode NKFD, ignoring everything except letters and numbers. +Letters are mapped to the corresponding lower-case value. -For example, ""hell"" matches ""Hello"". +For example, ""Brücke"" will be the prefix of ""(Bruckenpfeiler,"". === Sorting To sort two values, the underlying encoding is used to determine which value is less than the other. Uppercase letters are typically interpreted as less than their corresponding lowercase letters, i.e. ``A < a``. Comparison is done character-wise by finding the first difference in the respective character sequence. For example, ``abc > aBc``. Index: docs/manual/00001007000000.zettel ================================================================== --- docs/manual/00001007000000.zettel +++ docs/manual/00001007000000.zettel @@ -1,11 +1,12 @@ id: 00001007000000 title: Zettelmarkup role: manual tags: #manual #zettelmarkup #zettelstore syntax: zmk -modified: 20220810194655 +created: 20210126175322 +modified: 20220913135505 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. Zettelmarkup supports the longevity of stored notes by providing a syntax that any person can easily read, as well as a computer. @@ -32,8 +33,8 @@ * [[General principles|00001007010000]] * [[Basic definitions|00001007020000]] * [[Block-structured elements|00001007030000]] * [[Inline-structured element|00001007040000]] * [[Attributes|00001007050000]] -* [[Search expressions|00001007700000]] +* [[Query expressions|00001007700000]] * [[Summary of formatting characters|00001007800000]] * [[Tutorial|00001007900000]] Index: docs/manual/00001007030400.zettel ================================================================== --- docs/manual/00001007030400.zettel +++ docs/manual/00001007030400.zettel @@ -1,16 +1,17 @@ id: 00001007030400 title: Zettelmarkup: Horizontal Rules / Thematic Break role: manual tags: #manual #zettelmarkup #zettelstore syntax: zmk -modified: 20220218133651 +created: 20210126175322 +modified: 20220825185533 To signal a thematic break, you can specify a horizontal rule. This is done by entering at least three hyphen-minus characters (""''-''"", U+002D) at the first position of a line. You can add some [[attributes|00001007050000]], although the horizontal rule does not support the default attribute. -Any other character in this line will be ignored +Any other characters in this line will be ignored. If you do not enter the three hyphen-minus character at the very first position of a line, the are interpreted as [[inline elements|00001007040000]], typically as an ""en-dash" followed by a hyphen-minus. Example: Index: docs/manual/00001007031100.zettel ================================================================== --- docs/manual/00001007031100.zettel +++ docs/manual/00001007031100.zettel @@ -1,21 +1,24 @@ id: 00001007031100 title: Zettelmarkup: Transclusion role: manual tags: #manual #zettelmarkup #zettelstore syntax: zmk -modified: 20220809144920 +created: 20220131151022 +modified: 20220913135545 A transclusion allows to include the content of other zettel into the current zettel. The transclusion specification begins with three consecutive left curly bracket characters (""''{''"", U+007B) at the first position of a line and ends with three consecutive right curly bracket characters (""''}''"", U+007D). The curly brackets delimit either a [[zettel identifier|00001006050000]] or a searched zettel list. +You can add some [[attributes|00001007050000]], although a transclusion does not support the default attribute. +Any other characters in this line will be ignored. This leads to two variants of transclusion: # Transclusion of the content of another zettel into the current zettel. This is done if you specify a zettel identifier, and is called ""zettel transclusion"". -# Transclusion of the list of zettel references that satisfy a [[search expression|00001007700000]]. - This is called ""search transclusion"". +# Transclusion of the list of zettel references that satisfy a [[query expression|00001007700000]]. + This is called ""query transclusion"". The variants are described on separate zettel: * [[Zettel transclusion|00001007031110]] -* [[Search transclusion|00001007031140]] +* [[Query transclusion|00001007031140]] Index: docs/manual/00001007031110.zettel ================================================================== --- docs/manual/00001007031110.zettel +++ docs/manual/00001007031110.zettel @@ -1,10 +1,12 @@ id: 00001007031110 title: Zettelmarkup: Zettel Transclusion role: manual tags: #manual #zettelmarkup #zettelstore syntax: zmk +created: 20220809132350 +modified: 20220825190116 A zettel transclusion is specified by the following sequence, starting at the first position in a line: ''{{{zettel-identifier}}}''. When evaluated, the referenced zettel is read. If it contains some transclusions itself, these will be expanded, recursively. @@ -31,7 +33,12 @@ In addition, if a zettel __z__ transcludes a zettel __t__, but the current user is not allowed to view zettel __t__ (but zettel __z__), then the transclusion will not take place. To the current user, it seems that there was no transclusion in zettel __z__. This allows to create a zettel with content that seems to be changed, depending on the authorization of the current user. +--- +Any [[attributes|00001007050000]] added to the transclusion will set/overwrite the appropriate metadata of the included zettel. +Of course, this applies only to thoes attribtues, which have a valid name for a metadata key. +This allows to control the evaluation of the included zettel, especially for zettel containing a diagram description. + === See also [[Inline-mode transclusion|00001007040324]] does not work at the paragraph / block level, but is used for [[inline-structured elements|00001007040000]]. Index: docs/manual/00001007031140.zettel ================================================================== --- docs/manual/00001007031140.zettel +++ docs/manual/00001007031140.zettel @@ -1,30 +1,57 @@ id: 00001007031140 -title: Zettelmarkup: Search Transclusion +title: Zettelmarkup: Query Transclusion role: manual tags: #manual #search #zettelmarkup #zettelstore syntax: zmk -modified: 20220811141604 - -A search transclusion is specified by the following sequence, starting at the first position in a line: ''{{{search:search-expression}}}''. -The line must literally start with the sequence ''{{{search:''. -Everything after this prefix is interpreted as a [[search expression|00001007700000]]. - -When evaluated, the search expression is evaluated, leading to a list of [[links|00001007040310]] to zettel, matching the search expression. -Every link references the found zettel, with its title as link text. - -This list replaces the search transclusion element. - -For example, to include the list of all zettel with the [[all-tags|00001006020000#all-tags]] ""#search"", ordered by title specify the following search transclude element: +created: 20220809132350 +modified: 20220913145104 + +A query transclusion is specified by the following sequence, starting at the first position in a line: ''{{{query:query-expression}}}''. +The line must literally start with the sequence ''{{{query:''. +Everything after this prefix is interpreted as a [[query expression|00001007700000]]. + +When evaluated, the query expression is evaluated, often resulting in a list of [[links|00001007040310]] to zettel, matching the query expression. +The result replaces the query transclusion element. + +For example, to include the list of all zettel with the [[all-tags|00001006020000#all-tags]] ""#search"", ordered by title specify the following query transclude element: ```zmk -{{{search:all-tags:#search ORDER title}}} +{{{query:all-tags:#search ORDER title}}} ``` This will result in: :::zs-example -{{{search:all-tags:#search ORDER title}}} +{{{query:all-tags:#search ORDER title}}} ::: -Please note: if the referenced zettel is changed, all transclusions will also change. - For example, this allows to create a dynamic list of zettel inside a zettel, maybe to provide some introductory text followed by a list of child zettel. -The search will deliver only those zettel, which the current user is allowed to read. +The query will deliver only those zettel, which the current user is allowed to read. + +In the above example, the action list is empty. +This leads to the described list of zettel. + +The following actions are supported, parameter and aggregate actions: +; ''N'' (or any word that starts with ""''N''"" (parameter) +: The resulting list will be a numbered list. +; ''MINn'' (parameter) +: Emit only those values with at least __n__ aggregated values. + __n__ must be a positive integer, ''MIN'' must be given in upper-case letters. +; ''MAXn'' (parameter) +: Emit only those values with at most __n__ aggregated values. + __n__ must be a positive integer, ''MAX'' must be given in upper-case letters. +; ''TITLE'' (parameter) +: All words following ''TITLE'' are joined together to form a title. + It is used for the ''RSS'' action. +; ''RSS'' (aggregate) +: Transform the zettel list into an [[RSS 2.0|https://www.rssboard.org/rss-specification]]-conformant document. + The document is embedded into the referencing zettel. +; Any [[metadata key|00001006020000]] of type [[Word|00001006035500]], [[WordSet|00001006036000]], or [[TagSet|00001006034000]] (aggregates) +: Emit an aggregate of the given metadata key. + The key can be given in any letter case. + +```zmk +{{{query:all-tags:#search | all-tags}}} +``` +This in a tag cloud of all tags that are used together with the tag #search: +:::zs-example +{{{query:all-tags:#search | all-tags}}} +::: Index: docs/manual/00001007040000.zettel ================================================================== --- docs/manual/00001007040000.zettel +++ docs/manual/00001007040000.zettel @@ -1,11 +1,12 @@ id: 00001007040000 title: Zettelmarkup: Inline-Structured Elements role: manual tags: #manual #zettelmarkup #zettelstore syntax: zmk -modified: 20220809171453 +created: 20210126175322 +modified: 20220913144717 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. @@ -41,10 +42,30 @@ ==== 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. +**This element is deprecated in version 0.7 and will be removed in version 0.8!** + +The use of inline tags is problematic, because: +* The number sign is often used as, well, a number sign, esp. in the English language. + This introduces unintended tags. +* An inline tag is rendered in HTML as a link. + However, an inline tag may be the contained in the text part of a [[link element|00001007040310]]. + This will produce a HTML link within a HTML link. +* Similar, an inline tag may be part of the title of a zettel. + When a zettel list is rendered in HTML, this also produces a HTML link for each zettel, which contains the inline tag HTML link. +* The naming of metadata, [[''tags''|00001006020000#tags]] (names the tags within the metadata section of a zettel), [[''content-tags''|00001006020000#content-tags]] (all inline tags), and [[''all-tags''|00001006020000#all-tags]], is confusing for some users. + For example, if you follow the link of a tag, it is converted into a [[query|00001007700000]] for the key ''all-tags''. + A search for ''tags'' will most likely produce different results. + +To find all zettel with inline tags, please use the query [[::query:content-tags?::|query:content-tags?]]. +There are two, non-exclusive options for migration: +# Move inline tags into the metadata section of a zettel, under the key ''tags''. + This will allow you to find the zettel via a search for tags in the future. +# Replace the inline tag with a link to a search for that tag: ``#TAG`` could be replace with ``[[#TAG|query:tags:TAG]]``, where ''TAG'' is the placeholder for the actual tag. + ==== 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). Index: docs/manual/00001007040310.zettel ================================================================== --- docs/manual/00001007040310.zettel +++ docs/manual/00001007040310.zettel @@ -1,11 +1,12 @@ id: 00001007040310 title: Zettelmarkup: Links role: manual tags: #manual #zettelmarkup #zettelstore syntax: zmk -modified: 20220808161918 +created: 20210810155955 +modified: 20220913144754 There are two kinds of links, regardless of links to (internal) other zettel or to (external) material. Both kinds begin with two consecutive left square bracket characters (""''[''"", U+005B) and ends with two consecutive right square bracket characters (""'']''"", U+005D). The first form provides some text plus the link specification, delimited by a vertical bar character (""''|''"", U+007C): ``[[text|linkspecification]]``. @@ -18,13 +19,13 @@ === Link specifications The link specification for another zettel within the same Zettelstore is just the [[zettel identifier|00001006050000]]. To reference some content within a zettel, you can append a number sign character (""''#''"", U+0023) and the name of the mark to the zettel identifier. The resulting reference is called ""zettel reference"". -If the link specification begins with the string ''search:'', the text following this string will be interpreted as a [[search expression|00001007700000]]. -The resulting reference is called ""search reference"". -When this type of references is rendered, it will reference a list of all zettel that fulfills the search expression. +If the link specification begins with the string ''query:'', the text following this string will be interpreted as a [[query expression|00001007700000]]. +The resulting reference is called ""query reference"". +When this type of references is rendered, it will typically reference a list of all zettel that fulfills the query expression. A link specification starting with one slash character (""''/''"", U+002F), or one or two full stop characters (""''.''"", U+002E) followed by a slash character, will be interpreted as a local reference, called ""hosted reference"". Such references will be interpreted relative to the web server hosting the Zettelstore. Index: docs/manual/00001007050100.zettel ================================================================== --- docs/manual/00001007050100.zettel +++ docs/manual/00001007050100.zettel @@ -1,10 +1,12 @@ id: 00001007050100 title: Zettelmarkup: Supported Attribute Values for Natural Languages +role: manual tags: #manual #reference #zettelmarkup #zettelstore syntax: zmk -role: manual +created: 20210126175322 +modified: 20220827182130 With an [[attribute|00001007050000]] it is possible to specify the natural language of a text region. This is important, if you want to render your markup into an environment, where this is significant. HTML is such an environment. @@ -17,13 +19,6 @@ * ``{lang=de}`` for the german language * ``{lang=de-at}`` for the german language dialect spoken in Austria * ``{lang=de-de}`` for the german language dialect spoken in Germany The actual [[typographic quotations marks|00001007040100]] (``""...""``) are derived from the current language. -The language of a zettel (meta key ''lang'') or of the whole Zettelstore (''default-lang'' of the [[configuration zettel|00001004020000#default-lang]]) can be overwritten by an attribute: ``""...""{lang=fr}``{=zmk}. -Currently, Zettelstore supports the following primary languages: - -* ''de'' -* ''en'' -* ''fr'' - -These are used, even if a dialect was specified. +The language of a zettel (meta key ''lang'') can be overwritten by an attribute: ``""...""{lang=fr}``{=zmk}. Index: docs/manual/00001007700000.zettel ================================================================== --- docs/manual/00001007700000.zettel +++ docs/manual/00001007700000.zettel @@ -1,14 +1,22 @@ id: 00001007700000 -title: Search expression +title: Query expression role: manual tags: #manual #search #zettelstore syntax: zmk -modified: 20220810164112 +created: 20220805150154 +modified: 20220913135434 + +A query expression allows you to search for specific zettel and to perform some actions on them. +You may select zettel based on a full-text search, based on specific metadata values, or both. + +A query expression consists of a __search expression__ and of an optional __action list__. +Both are separated by a vertical bar character (""''|''"", U+007C). + +A query expression follows a [[formal syntax|00001007780000]]. -A search expression allows you to search for specific zettel. -You may select zettel based on a full-text search, based on specifc metadata values, or both. +=== Search expression In its simplest form, a search expression just contains a string to be search for with the help of a full-text search. For example, the string ''syntax'' will search for all zettel containing the word ""syntax"". If you want to search for all zettel with a title containing the word ""syntax"", you must specify ''title:syntax''. @@ -16,14 +24,26 @@ The colon character (""'':''"") is a [[search operator|00001007705000]], in this example to specify a match. ""syntax"" is the [[search value|00001007706000]] that must match to the value of the given metadata key, here ""title"". A search expression may contain more than one search term, such as ''title:syntax''. Search terms must be separated by one or more space characters, for example ''title:syntax title:search''. +All terms of a select expression must be true so that a zettel is selected. * [[Search terms|00001007702000]] * [[Search operator|00001007705000]] * [[Search value|00001007706000]] -A search expression follows a [[formal syntax|00001007780000]]. - -Here are some examples of search expressions, which can be used to manage a Zettelstore: +Here are [[some examples|00001007790000]] of search expressions, which can be used to manage a Zettelstore: {{{00001007790000}}} + +=== Action List + +With a search expression, a list of zettel is selected. +Actions allow to modify this list to a certain degree. + +Which actions are allowed depends on the context. +However, actions are further separated into __parameter action__ and __aggregate actions__. +A parameter action just sets a parameter for an aggregate action. +An aggregate action transforms the list of selected zettel into a different, aggregate form. +Only the first aggregate form is executed, following aggregate actions are ignored. + +In most contexts, valid actions include the name of metadata keys, at least of type [[Word|00001006035500]], [[WordSet|00001006036000]], or [[TagSet|00001006034000]]. Index: docs/manual/00001007702000.zettel ================================================================== --- docs/manual/00001007702000.zettel +++ docs/manual/00001007702000.zettel @@ -1,16 +1,16 @@ id: 00001007702000 title: Search term role: manual tags: #manual #search #zettelstore syntax: zmk -modified: 20220808130055 +modified: 20220821163727 A search term allows you to specify one search restriction. The result [[search expression|00001007700000]], which contains more than one search term, will be the applications of all restrictions. -A search term can be one of the following: +A search term can be one of the following (the first three term are collectively called __search literals__): * A metadata-based search, by specifying the name of a [[metadata key|00001006010000]], followed by a [[search operator|00001007705000]], followed by an optional [[search value|00001007706000]]. All zettel containing the given metadata key with a allowed value (depending on the search operator) are selected. If no search value is given, then all zettel containing the given metadata key are selected (or ignored, for a negated search operator). @@ -20,39 +20,49 @@ **Note:** the search value will be normalized according to Unicode NKFD, ignoring everything except letters and numbers. Therefore, the following search expression are essentially the same: ''"search syntax"'' and ''search syntax''. The first is a search expression with one search value, which is normalized to two strings to be searched for. The second is a search expression containing two search values, giving two string to be searched for. -* The string ''NEGATE'' will negate (sic!) the behavior of the whole search expression. - If it occurs multiple times, the negation will be negated. +* A metadata key followed by ""''?''"" or ""''!?''"". + + Is true, if zettel metadata contains / does not contain the given key. +* The string ''OR'' signals that following search literals may occur alternatively in the result. + + Since search literals may be negated, it is possible to form any boolean search expression. + Any search expression will be in a [[disjunctive normal form|https://en.wikipedia.org/wiki/Disjunctive_normal_form]]. + + It has no effect on the following search terms initiated with a special uppercase word. * The string ''ORDER'', followed by a non-empty sequence of spaces and the name of a metadata key, will specify an ordering of the result list. If you include the string ''REVERSE'' after ''ORDER'' but before the metadata key, the ordering will be reversed. Example: ''ORDER published'' will order the resulting list based on the publishing data, while ''ORDER REVERSED published'' will return a reversed result order. - Currently, only the first term specifying the order of the resulting list will be used. - Other ordering terms will be ignored. - An explicit order field will take precedence over the random order described below. + If no random order is effective, a ``ORDER REVERSE id`` will be added. + This makes the sort stable. + + Example: ``ORDER created`` will be interpreted as ``ORDER created ORDER REVERSE id``. + + Any ordering by zettel identifier will make following order terms to be ignored. + + Example: ``ORDER id ORDER created`` will be interpreted as ``ORDER id``. * The string ''RANDOM'' will provide a random order of the resulting list. Currently, only the first term specifying the order of the resulting list will be used. Other ordering terms will be ignored. A random order specification will be ignored, if there is an explicit ordering given. Example: ''RANDOM ORDER published'' will be interpreted as ''ORDER published''. - * The string ''OFFSET'', followed by a non-empty sequence of spaces and a number greater zero (called ""N""). This will ignore the first N elements of the result list, based on the specified sort order. A zero value of N will produce the same result as if nothing was specified. If specified multiple times, the higher value takes precedence. Example: ''OFFSET 4 OFFSET 8'' will be interpreted as ''OFFSET 8''. - * The string ''LIMIT'', followed by a non-empty sequence of spaces and a number greater zero (called ""N""). This will limit the result list to the first N elements, based on the specified sort order. A zero value of N will produce the same result as if nothing was specified. If specified multiple times, the lower value takes precedence. Index: docs/manual/00001007705000.zettel ================================================================== --- docs/manual/00001007705000.zettel +++ docs/manual/00001007705000.zettel @@ -1,45 +1,32 @@ id: 00001007705000 title: Search operator role: manual tags: #manual #search #zettelstore syntax: zmk -modified: 20220811141050 +modified: 20220819194709 A search operator specifies how the comparison of a search value and a zettel should be executed. Every comparison is done case-insensitive, treating all uppercase letters the same as lowercase letters. The following are allowed search operator characters: * The exclamation mark character (""!"", U+0021) negates the meaning -* The tilde character (""''~''"", U+007E) compares on containment (""contains operator"") +* The tilde character (""''~''"", U+007E) compares on matching (""match operator"") * The greater-than sign character (""''>''"", U+003E) matches if there is some prefix (""prefix operator"") * The less-than sign character (""''<''"", U+003C) compares a suffix relationship (""suffix operator"") -* The colon character (""'':''"", U+003A) specifies the __default comparison__, i.e. one of the previous comparisons. - - **Please note:** this operator will be changed in version 0.7.0. - It was included to allow the transition of the previous mechanism into search expressions. - - With version 0.7.0, this operator will take the role of the ''=''-operator. -* The equal sign character (""''=''"", U+003D) compares on equal words (""equal operator"") - - **Please note:** this operator will be removed in version 0.7.0. - It was included to allow the transition of the previous mechanism into search expressions. - -Since the exclamation mark character can be combined with the other, there are 12 possible combinations: -# ""''!''"": is an abbreviation of the ""''!:''"" operator. -# ""'':''"": depending on the [[metadata key type|00001006030000]] one of the other operators is chosen. - For example, a [[numeric key type|00001006033000]] will execute the equals operator, while for a [[string type|00001006033500]] a contains operator will be executed. - - With version 0.7.0 its meaning will be changed to that of the ''='' operator. -# ""''!:''"": similar to the ""match operator"" above, the appropriate negated search operator will be chosen, depending on the metadata key type - - With version 0.7.0 its meaning will be changed to that of the ''!='' operator. -# ""''~''"": is successful if the search value is contained in the value to be compared. -# ""''!~''"": is successful if the search value is not contained in the value to be compared. -# ""''=''"": is successful if the search value is equal to one word of the value to be compared. -# ""''!=''"": is successful if the search value is not equal to any word of the value to be compared. +* The colon character (""'':''"", U+003A) compares on equal words (""has operator"") +* The question mark (""''?''"", U+003F) checks for an existing metadata key (""exist operator"") + +Since the exclamation mark character can be combined with the other, there are 10 possible combinations: +# ""''!''"": is an abbreviation of the ""''!~''"" operator. +# ""''~''"": is successful if the search value matched the value to be compared. +# ""''!~''"": is successful if the search value does not match the value to be compared. +# ""'':''"": is successful if the search value is equal to one word of the value to be compared. +# ""''!:''"": is successful if the search value is not equal to any word of the value to be compared. # ""''>''"": is successful if the search value is a prefix of the value to be compared. # ""''!>''"": is successful if the search value is not a prefix of the value to be compared. # ""''<''"": is successful if the search value is a suffix of the value to be compared. # ""''!<''"": is successful if the search value is not a suffix of the value to be compared. +# ""''?''"": is successful if the metadata contains the given key. +# ""''!?''"": is successful if the metadata does not contain the given key. # ""''''"": a missing search operator can only occur for a full-text search. It is equal to the ""''~''"" operator. Index: docs/manual/00001007780000.zettel ================================================================== --- docs/manual/00001007780000.zettel +++ docs/manual/00001007780000.zettel @@ -1,24 +1,30 @@ id: 00001007780000 -title: Forma syntax of search expressions +title: Formal syntax of query expressions role: manual tags: #manual #reference #search #zettelstore syntax: zmk -modified: 20220811141423 +created: 20220810144539 +modified: 20220913134024 ``` +QueryExpression := SearchExpression ActionExpression? SearchExpression := SearchTerm (SPACE+ SearchTerm)*. -SearchTerm := "NEGATE" - | SearchOperator? SearchValue +SearchTerm := SearchOperator? SearchValue | SearchKey SearchOperator SearchValue? + | SearchKey ExistOperator + | "OR" | "RANDOM" | "ORDER" SPACE+ ("REVERSE" SPACE+)? SearchKey | "OFFSET" SPACE+ PosInt | "LIMIT" SPACE+ PosInt. -SearchValue := NO-SPACE (NO-SPACE)*. +SearchValue := Word. SearchKey := MetadataKey. SearchOperator := '!' - | ('!')? '=' ← removed in version 0.7.0 - | ('!')? (':' | '<' | '>'). + | ('!')? ('~' | ':' | '<' | '>'). +ExistOperator := '?' + | '!' '?'. PosInt := '0' | ('1' .. '9') DIGIT*. +ActionExpression := '|' (Word (SPACE+ Word)*)? +Word := NO-SPACE NO-SPACE* ``` Index: docs/manual/00001007790000.zettel ================================================================== --- docs/manual/00001007790000.zettel +++ docs/manual/00001007790000.zettel @@ -1,17 +1,18 @@ id: 00001007790000 -title: Useful search expressions +title: Useful query expressions role: manual tags: #example #manual #search #zettelstore syntax: zmk -modified: 20220811141224 - -|= Search Expression |= Meaning -| [[search:role:configuration]] | Zettel that contains some configuration data for the Zettelstore -| [[search:ORDER REVERSE id LIMIT 40]] | 40 recently created zettel -| [[search:ORDER REVERSE published LIMIT 40]] | 40 recently updated zettel -| [[search:RANDOM LIMIT 40]] | 40 random zettel -| [[search:dead:]] | Zettel with invalid / dead links -| [[search:backward!: precursor!:]] | Zettel that are not referenced by other zettel -| [[search:all-tags!:]] | Zettel without any tags -| [[search:tags!:]] | Zettel without tags that are defined within metadata -| [[search:content-tags:]] | Zettel with tags within content +created: 20220810144539 +modified: 20220913144959 + +|= Query Expression |= Meaning +| [[query:role:configuration]] | Zettel that contains some configuration data for the Zettelstore +| [[query:ORDER REVERSE created LIMIT 40]] | 40 recently created zettel +| [[query:ORDER REVERSE published LIMIT 40]] | 40 recently updated zettel +| [[query:RANDOM LIMIT 40]] | 40 random zettel +| [[query:dead?]] | Zettel with invalid / dead links +| [[query:backward!? precursor!?]] | Zettel that are not referenced by other zettel +| [[query:all-tags!?]] | Zettel without any tags +| [[query:tags!?]] | Zettel without tags that are defined within metadata +| [[query:content-tags?]] | Zettel with tags within content Index: docs/manual/00001008000000.zettel ================================================================== --- docs/manual/00001008000000.zettel +++ docs/manual/00001008000000.zettel @@ -1,11 +1,12 @@ id: 00001008000000 title: Other Markup Languages role: manual tags: #manual #zettelstore syntax: zmk -modified: 20220627192329 +created: 20210126175300 +modified: 20220824114649 [[Zettelmarkup|00001007000000]] is not the only markup language you can use to define your content. Zettelstore is quite agnostic with respect to markup languages. Of course, Zettelmarkup plays an important role. However, with the exception of zettel titles, you can use any (markup) language that is supported: @@ -41,14 +42,16 @@ ; [!none|''none''] : Only the metadata of a zettel is ""parsed"". Useful for displaying the full metadata. The [[runtime configuration zettel|00000000000100]] uses this syntax. The zettel content is ignored. +; [!pikchr]''pikchr'' +: A [[PIC|https://en.wikipedia.org/wiki/Pic_language]]-like [[markup language for diagrams|https://pikchr.org/]]. ; [!svg|''svg''] -: A [[Scalable Vector Graphics|https://www.w3.org/TR/SVG2/]]. +: [[Scalable Vector Graphics|https://www.w3.org/TR/SVG2/]]. ; [!text|''text''], [!plain|''plain''], [!txt|''txt''] -: Just plain text that must not be interpreted further. +: Plain text that must not be interpreted further. ; [!zmk|''zmk''] : [[Zettelmarkup|00001007000000]]. The actual values are also listed in a zettel named [[Zettelstore Supported Parser|00000000000092]]. Index: docs/manual/00001010070200.zettel ================================================================== --- docs/manual/00001010070200.zettel +++ docs/manual/00001010070200.zettel @@ -1,11 +1,12 @@ id: 00001010070200 title: Visibility rules for zettel role: manual tags: #authorization #configuration #manual #security #zettelstore syntax: zmk -modified: 20220808152359 +created: 20210126175322 +modified: 20220913144845 For every zettel you can specify under which condition the zettel is visible to others. This is controlled with the metadata key [[''visibility''|00001006020000#visibility]]. The following values are supported: @@ -25,11 +26,11 @@ : Only the owner of the Zettelstore can access the zettel, if runtime configuration [[''expert-mode''|00001004020000#expert-mode]] is set to a [[boolean true value|00001006030500]]. This is for zettel with sensitive content that might irritate the owner. Computed zettel with internal runtime information are examples for such a zettel. -When you install a Zettelstore, only [[some zettel|//h?visibility=public]] have visibility ""public"". +When you install a Zettelstore, only [[some zettel|query:visibility:public]] have visibility ""public"". One is the zettel that contains [[CSS|00000000020001]] for displaying the [[web user interface|00001014000000]]. This is to ensure that the web interface looks nice even for not authenticated users. Another is the zettel containing the Zettelstore [[license|00000000000004]]. The [[default image|00000000040001]], used if an image reference is invalid, is also public visible. @@ -38,12 +39,12 @@ In this case, the [[runtime configuration zettel|00001004020000]] is shown (its visibility is ""owner""). The [[startup configuration|00001004010000]] is not shown, because the associated computed zettel with identifier ''00000000000096'' is stored with the visibility ""expert"". If you want to show such a zettel, you must set ''expert-mode'' to true. === Examples -Similar to the [[API|00001012051810]], you can easily create a zettel list based on the ''visibility'' metadata key: +Similar to the [[API|00001012051840]], you can easily create a zettel list based on the ''visibility'' metadata key: -| public | [[search:visibility:public]] -| login | [[search:visibility:login]] -| creator | [[search:visibility:creator]] -| owner | [[search:visibility:owner]] -| expert | [[search:visibility:expert]][^Only if [[''expert-mode''|00001004020000#expert-mode]] is enabled, this list will show some zettel.] +| public | [[query:visibility:public]] +| login | [[query:visibility:login]] +| creator | [[query:visibility:creator]] +| owner | [[query:visibility:owner]] +| expert | [[query:visibility:expert]][^Only if [[''expert-mode''|00001004020000#expert-mode]] is enabled, this list will show some zettel.] Index: docs/manual/00001012000000.zettel ================================================================== --- docs/manual/00001012000000.zettel +++ docs/manual/00001012000000.zettel @@ -1,11 +1,12 @@ id: 00001012000000 title: API role: manual tags: #api #manual #zettelstore syntax: zmk -modified: 20220805144406 +created: 20210126175322 +modified: 20220913141632 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. @@ -20,16 +21,13 @@ * [[Renew an access token|00001012050400]] without costly re-authentication * [[Provide an access token|00001012050600]] when doing an API call === Zettel lists * [[List metadata of all zettel|00001012051200]] -* [[Shape the list of zettel metadata|00001012051800]] -** [[Selection of zettel|00001012051810]] -** [[Limit the list length|00001012051830]] -** [[Search expressions|00001012051840]] (includes content search) -** [[Sort the list of zettel metadata|00001012052000]] +** [[Query expressions|00001012051840]] (includes content search) * [[Map metadata values to lists of zettel identifier|00001012052400]] +* [[Query the list of all zettel|00001012051400]] === Working with zettel * [[Create a new zettel|00001012053200]] * [[Retrieve metadata and content of an existing zettel|00001012053300]] * [[Retrieve metadata of an existing zettel|00001012053400]] Index: docs/manual/00001012051200.zettel ================================================================== --- docs/manual/00001012051200.zettel +++ docs/manual/00001012051200.zettel @@ -1,11 +1,12 @@ id: 00001012051200 title: API: List metadata of all zettel role: manual tags: #api #manual #zettelstore syntax: zmk -modified: 20220201180649 +created: 20210126175322 +modified: 20220913151852 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 @@ -15,19 +16,21 @@ The JSON object contains a key ''"list"'' where its value is a list of zettel JSON objects. These zettel JSON objects themselves contains the keys ''"id"'' (value is a string containing the [[zettel identifier|00001006050000]]), ''"meta"'' (value as a JSON object), and ''"rights"'' (encodes the [[access rights|00001012921200]] for the given zettel). The value of key ''"meta"'' effectively contains all metadata of the identified zettel, where metadata keys are encoded as JSON object keys and metadata values encoded as JSON strings. -Additionally, the JSON object contains a key ''"query"'' with a string value. -It will contain a textual description of the underlying query if you [[select only some zettel|00001012051810]]. -Without a selection, the value is the empty string. +Additionally, the JSON object contains the keys ''"query"'' and ''"human"'' with a string value. +Both will contain a textual description of the underlying query if you select only some zettel with a [[query expression|00001012051840]]. +Without a selection, the values are the empty string. +''"query"'' returns the normalized query expression itself, while ''"human"'' is the normalized query expression to be read by humans. If you reformat the JSON output from the ''GET /j'' call, you'll see its structure better: ```json { "query": "", + "human": "", "list": [ { "id": "00001012051200", "meta": { "title": "API: List for all zettel some data", ADDED docs/manual/00001012051400.zettel Index: docs/manual/00001012051400.zettel ================================================================== --- docs/manual/00001012051400.zettel +++ docs/manual/00001012051400.zettel @@ -0,0 +1,66 @@ +id: 00001012051400 +title: API: Query the list of all zettel +role: manual +tags: #api #manual #zettelstore +syntax: zmk +created: 20220912111111 +modified: 20220913150204 + +The [[endpoint|00001012920000]] ''/q'' allows to query the list of all zettel. + +A [[query|00001007700000]] is an optional [[search expression|00001007700000#search-expression]], together with an optional [[list of actions|00001007700000#action-list]] (described below). +An empty search expression will select all zettel. +An empty list of action will return nothing. +It is an error, if both are empty. + +Search expression and action list are separated by a vertical bar character (""''|''"", U+007C), and must be given with the query parameter ''q''. + +For example, to list all roles used in the Zettelstore, send a HTTP GET request to the endpoint ''/q?q=|role''. +If successful, the output is a JSON object: + +```sh +# curl http://127.0.0.1:23123/q?q=|role +{"map":{"configuration":["00000000090002","00000000090000", ... ,"00000000000001"],"manual":["00001014000000", ... ,"00001000000000"],"zettel":["00010000000000", ... ,"00001012070500","00000000090001"]}} +``` + +The JSON object only contains the key ''"map"'' with the value of another object. +This second object contains all role names as keys and the list of identifier of those zettel with this specific role as a value. + +Similar, to list all tags used in the Zettelstore, send a HTTP GET request to the endpoint ''/q?q=|tags''. +If successful, the output is a JSON object: + +```sh +# curl http://127.0.0.1:23123/q?q=|tags +{"map":{"#api":[:["00001012921000","00001012920800","00001012920522",...],"#authorization":["00001010040700","00001010040400",...],...,"#zettelstore":["00010000000000","00001014000000",...,"00001001000000"]}} +``` + +The JSON object only contains the key ''"map"'' with the value of another object. +This second object contains all tags as keys and the list of identifier of those zettel with this tag as a value. + +If you want only those tags that occur at least 100 times, use the endpoint ''/q?q=|MIN100+tags''. +You see from this that actions are separated by space characters. + +There are two types of actions: parameters and aggregates. +The following actions are supported: +; ''MINn'' (parameter) +: Emit only those values with at least __n__ aggregated values. + __n__ must be a positive integer, ''MIN'' must be given in upper-case letters. +; ''MAXn'' (parameter) +: Emit only those values with at most __n__ aggregated values. + __n__ must be a positive integer, ''MAX'' must be given in upper-case letters. +; Any [[metadata key|00001006020000]] of type [[Word|00001006035500]], [[WordSet|00001006036000]], or [[TagSet|00001006034000]] (aggregates) +: Emit an aggregate of the given metadata key. + The key can be given in any letter case. + +Only the first aggregate action will be executed. + +=== HTTP Status codes +; ''200'' +: Query was successful. +; ''204'' +: Query was successful, but results in no content. + Most likely, you specified no appropriate aggregator. +; ''400'' +: Request was not valid. + There are several reasons for this. + Maybe the access bearer token was not valid, or you forgot to specify a valid query. DELETED docs/manual/00001012051800.zettel Index: docs/manual/00001012051800.zettel ================================================================== --- docs/manual/00001012051800.zettel +++ docs/manual/00001012051800.zettel @@ -1,15 +0,0 @@ -id: 00001012051800 -title: API: Shape the list of zettel metadata -role: manual -tags: #api #manual #zettelstore -syntax: zmk -modified: 20220805144809 - -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]]. -* [[Specifying a search expression|00001012051840]], e.g. searching for zettel content and/or metadata, is another way of selecting some zettel. -* The resulting list can be [[sorted|00001012052000]] according to various criteria. DELETED docs/manual/00001012051810.zettel Index: docs/manual/00001012051810.zettel ================================================================== --- docs/manual/00001012051810.zettel +++ docs/manual/00001012051810.zettel @@ -1,58 +0,0 @@ -id: 00001012051810 -title: API: Select zettel based on their metadata -role: manual -tags: #api #manual #zettelstore -syntax: zmk -modified: 20220811141840 - -Every query parameter that does __not__ begin with the low line character (""_"", U+005F) is treated as the name of a [[metadata|00001006010000]] key. -According to the [[type|00001006030000]] of a metadata key, zettel are possibly selected. -All [[supported|00001006020000]] metadata keys have a well-defined type. -User-defined keys have the type ''e'' (string, possibly empty). - -For example, if you want to retrieve all zettel that contain the string ""API"" in its title, your request will be: -```sh -# curl 'http://127.0.0.1:23123/j?title=API' -{"query":"title MATCH API","list":[{"id":"00001012921000","meta":{"title":"API: JSON structure of an access token","tags":"#api #manual #reference #zettelstore","syntax":"zmk","role":"manual"}},{"id":"00001012920500","meta":{"title":"Formats available by the API","tags":"#api #manual #reference #zettelstore","syntax":"zmk","role":"manual"}},{"id":"00001012920000","meta":{"title":"Endpoints used by the API","tags":"#api #manual #reference #zettelstore","syntax":"zmk","role":"manual"}}, ... -``` - -However, if you want all zettel that does not match a given value, you must prefix the value with the exclamation mark character (""!"", U+0021). -For example, if you want to retrieve all zettel that do not contain the string ""API"" in their title, your request will be: -```sh -# curl 'http://127.0.0.1:23123/j?title=!API' -{"query":"title NOT MATCH API","list":[{"id":"00010000000000","meta":{"back":"00001003000000 00001005090000","backward":"00001003000000 00001005090000","copyright":"(c) 2020-2021 by Detlef Stern ","forward":"00000000000001 00000000000003 00000000000096 00000000000100","lang":"en","license":"EUPL-1.2-or-later","role":"zettel","syntax":"zmk","title":"Home"}},{"id":"00001014000000","meta":{"back":"00001000000000 00001004020000 00001012920510","backward":"00001000000000 00001004020000 00001012000000 00001012920510","copyright":"(c) 2020-2021 by Detlef Stern ","forward":"00001012000000","lang":"en","license":"EUPL-1.2-or-later","published":"00001014000000","role":"manual","syntax":"zmk","tags":"#manual #webui #zettelstore","title":"Web user interface"}}, -... -``` - -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. - -As stated above, the exact rule for comparison depends on the [[type|00001006030000]] of the specified metadata key. -By using a [[simple search syntax|00001012051890]], you are able to specify other comparison operations.[^One is the already mentioned exclamation mark character.] - -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. -The first query produces a zettel list, where each zettel does have a ''url'' metadata value, which does not contain the characters ""com"". -The second query produces a zettel list, that excludes any zettel containing a ''url'' metadata value that contains the characters ""com""; this also includes all zettel that do not contain the metadata key ''url''. - -Alternatively, you also can use the [[endpoint|00001012920000]] ''/z'' for a simpler result format. -The first example translates to: -```sh -# curl 'http://127.0.0.1:23123/z?title=API' -00001012921000 API: JSON structure of an access token -00001012920500 Formats available by the API -00001012920000 Endpoints used by the API -... -``` -=== Deprecation -Comparisons via URL query parameter are deprecated since version 0.6.0. -They will be removed in version 0.7.0 DELETED docs/manual/00001012051830.zettel Index: docs/manual/00001012051830.zettel ================================================================== --- docs/manual/00001012051830.zettel +++ docs/manual/00001012051830.zettel @@ -1,32 +0,0 @@ -id: 00001012051830 -title: API: Shape the list of zettel metadata by limiting its length -role: manual -tags: #api #manual #zettelstore -syntax: zmk -modified: 20211004124642 - -=== Limit -By using the query parameter ""''_limit''"", which must have an integer value, you specifying an upper limit of list elements: -```sh -# curl 'http://127.0.0.1:23123/j?title=API&_sort=id&_limit=2' -{"query":"title MATCH API LIMIT 2","list":[{"id":"00001012000000","meta":{"all-tags":"#api #manual #zettelstore","back":"00001000000000 00001004020000","backward":"00001000000000 00001004020000 00001012053200 00001012054000 00001014000000","box-number":"1","forward":"00001010040100 00001010040700 00001012050200 00001012050400 00001012050600 00001012051200 00001012051800 00001012051810 00001012051830 00001012051840 00001012052000 00001012052200 00001012052400 00001012052600 00001012053200 00001012053300 00001012053500 00001012053600 00001012053700 00001012053800 00001012054000 00001012054200 00001012054400 00001012054600 00001012920000 00001014000000","modified":"20210817160844","published":"20210817160844","role":"manual","syntax":"zmk","tags":"#api #manual #zettelstore","title":"API"}},{"id":"00001012050200","meta":{"all-tags":"#api #manual #zettelstore","back":"00001012000000 00001012050400 00001012050600 00001012051200 00001012053400 00001012053500 00001012053600","backward":"00001010040700 00001012000000 00001012050400 00001012050600 00001012051200 00001012053400 00001012053500 00001012053600 00001012920000 00001012921000","box-number":"1","forward":"00001004010000 00001010040200 00001010040700 00001012920000 00001012921000","modified":"20210726123709","published":"20210726123709","role":"manual","syntax":"zmk","tags":"#api #manual #zettelstore","title":"API: Authenticate a client"}}]} -``` - -```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' -{"query":"title MATCH API OFFSET 1 LIMIT 2","list":[{"id":"00001012050200","meta":{"all-tags":"#api #manual #zettelstore","back":"00001012000000 00001012050400 00001012050600 00001012051200 00001012053400 00001012053500 00001012053600","backward":"00001010040700 00001012000000 00001012050400 00001012050600 00001012051200 00001012053400 00001012053500 00001012053600 00001012920000 00001012921000","box-number":"1","forward":"00001004010000 00001010040200 00001010040700 00001012920000 00001012921000","modified":"20210726123709","published":"20210726123709","role":"manual","syntax":"zmk","tags":"#api #manual #zettelstore","title":"API: Authenticate a client"}},{"id":"00001012050400","meta":{"all-tags":"#api #manual #zettelstore","back":"00001010040700 00001012000000","backward":"00001010040700 00001012000000 00001012920000 00001012921000","box-number":"1","forward":"00001010040100 00001012050200 00001012920000 00001012921000","modified":"20210726123745","published":"20210726123745","role":"manual","syntax":"zmk","tags":"#api #manual #zettelstore","title":"API: Renew an access token"}}]} -``` - -```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 -``` Index: docs/manual/00001012051840.zettel ================================================================== --- docs/manual/00001012051840.zettel +++ docs/manual/00001012051840.zettel @@ -1,13 +1,43 @@ id: 00001012051840 -title: API: Shape the list of zettel metadata by specifying a search expression +title: API: Shape the list of zettel metadata by specifying a query expression role: manual tags: #api #manual #search #zettelstore syntax: zmk -modified: 20220805165619 +created: 20210709143714 +modified: 20220913150355 -The query parameter ""''_s''"" allows you to specify [[search expressions|00001007700000]] for a full-text search of all zettel content and/or restricting the search according to specific metadata. +The query parameter ""''q''"" allows you to specify [[query expressions|00001007700000]] for a full-text search of all zettel content and/or restricting the search according to specific metadata. You are allowed to specify this query parameter more than once, as well as the other query parameters. All results will be intersected, i.e. a zettel will be included into the list if all of the provided values match. This parameter loosely resembles the search form of the [[web user interface|00001014000000]]. + +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?q=title%3AAPI' +{"query":"title MATCH API","list":[{"id":"00001012921000","meta":{"title":"API: JSON structure of an access token","tags":"#api #manual #reference #zettelstore","syntax":"zmk","role":"manual"}},{"id":"00001012920500","meta":{"title":"Formats available by the API","tags":"#api #manual #reference #zettelstore","syntax":"zmk","role":"manual"}},{"id":"00001012920000","meta":{"title":"Endpoints used by the API","tags":"#api #manual #reference #zettelstore","syntax":"zmk","role":"manual"}}, ... +``` + +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?q=title!%3AAPI' +{"query":"title NOT MATCH API","list":[{"id":"00010000000000","meta":{"back":"00001003000000 00001005090000","backward":"00001003000000 00001005090000","copyright":"(c) 2020-2021 by Detlef Stern ","forward":"00000000000001 00000000000003 00000000000096 00000000000100","lang":"en","license":"EUPL-1.2-or-later","role":"zettel","syntax":"zmk","title":"Home"}},{"id":"00001014000000","meta":{"back":"00001000000000 00001004020000 00001012920510","backward":"00001000000000 00001004020000 00001012000000 00001012920510","copyright":"(c) 2020-2021 by Detlef Stern ","forward":"00001012000000","lang":"en","license":"EUPL-1.2-or-later","published":"00001014000000","role":"manual","syntax":"zmk","tags":"#manual #webui #zettelstore","title":"Web user interface"}}, +... +``` + +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?q=url%3A'`` and ``curl 'http://localhost:23123/j?q=url%3A!'`` may result in an empty list. + +Alternatively, you also can use the [[endpoint|00001012920000]] ''/z'' for a simpler result format. +The first example translates to: +```sh +# curl 'http://127.0.0.1:23123/z?q=title%3AAPI' +00001012921000 API: JSON structure of an access token +00001012920500 Formats available by the API +00001012920000 Endpoints used by the API +... +``` DELETED docs/manual/00001012051890.zettel Index: docs/manual/00001012051890.zettel ================================================================== --- docs/manual/00001012051890.zettel +++ docs/manual/00001012051890.zettel @@ -1,44 +0,0 @@ -id: 00001012051890 -title: API: Comparison syntax (simple) -role: manual -tags: #api #manual #search #zettelstore -syntax: zmk -modified: 20220811141804 - -By using a simple syntax for comparing metadata values, you can modify the default comparison. -Note, this syntax is intend-fully similar to the syntax of the more general [[search operators|00001007705000]], which are part of [[search expressions|00001007700000]]. - -If the search string starts with the exclamation mark character (""!"", U+0021), it will be removed and the query matches all values that **do not match** the search string. - -In the next step, the first character of the search string will be inspected. -If it contains one of the characters ""'':''"", ""''=''"", ""''>''"", ""''<''"", or ""''~''"", this will modify how the search will be performed. -The character will be removed from the start of the search string. - -For example, assume the search string is ""def"": - -; The colon character (""'':''"", U+003A) (or none of these characters) -: This is the __default__ comparison. - The comparison chosen depends on the [[metadata key type|00001006030000]]. - - It you omit the the comparison character, the default comparison is also used. -; The tilde character (""''~''"", U+007E) -: The inspected text[^Either all words of the zettel content and/or some metadata values] contains the search string. - ""def"", ""defghi"", and ""abcdefghi"" are matching the search string. -; The equal sign character (""''=''"", U+003D) -: The inspected text must contain a word that is equal to the search string. - Only the word ""def"" matches the search string. -; The greater-than sign character (""''>''"", U+003E) -: The inspected text must contain a word with the search string as a prefix. - A word like ""def"" or ""defghi"" matches the search string. -; The less-than sign character (""''<''"", U+003C) -: The inspected text must contain a word with the search string as a suffix. - A word like ""def"" or ""abcdef"" matches the search string. - -If you want to include an initial ""''!''"" into the search string, you must prefix that with the escape character ""''\\''"". -For example ""\\!abc"" will search for the string ""!abc"". -A similar rule applies to the characters that specify the way how the search will be done. -For example, ""!\\=abc"" will search for content that does not contains the string ""=abc"". - -=== Deprecation -Comparisons via URL query parameter are deprecated since version 0.6.0. -They will be removed in version 0.7.0 DELETED docs/manual/00001012052000.zettel Index: docs/manual/00001012052000.zettel ================================================================== --- docs/manual/00001012052000.zettel +++ docs/manual/00001012052000.zettel @@ -1,23 +0,0 @@ -id: 00001012052000 -title: API: Sort the list of zettel metadata -role: manual -tags: #api #manual #zettelstore -syntax: zmk -modified: 20220218131937 - -If not specified, the list of zettel is sorted descending by the value of the [[zettel identifier|00001006050000]]. -The highest zettel identifier, which is a number, comes first. -You change that with the ""''_sort''"" query parameter. -Alternatively, you can also use the ""''_order''"" query parameter. -It is an alias. - -Its value is the name of a metadata key, optionally prefixed with a hyphen-minus character (""''-''"", U+002D). -According to the [[type|00001006030000]] of a metadata key, the list of zettel is sorted. -If hyphen-minus is given, the order is descending, else ascending. - -If you want a random list of zettel, specify the value ""_random"" in place of the metadata key. -""''_sort=_random''"" (or ""''_order=_random''"") is the query parameter in this case. -If can be combined with ""[[''_limit=1''|00001012051830]]"" to obtain just one random zettel. - -Currently, only the first occurrence of ''_sort'' is recognized. -In the future it will be possible to specify a combined sort key. Index: docs/manual/00001012052400.zettel ================================================================== --- docs/manual/00001012052400.zettel +++ docs/manual/00001012052400.zettel @@ -1,36 +1,40 @@ id: 00001012052400 title: API: Map metadata values to list of zettel identifier role: manual tags: #api #manual #zettelstore syntax: zmk -modified: 20220627183323 +created: 20210216172944 +modified: 20220912112253 +**Note**: this endpoint is deprecated since v0.7 and will be removed in v0.8. +Please use a [[query|00001012051400]] instead. +--- The [[endpoint|00001012920000]] ''/m'' allows to retrieve a map of metadata values (of a specific key) to the list of zettel identifier, which reference zettel containing this value under the given metadata key. Currently, two keys are supported: * [[''role''|00001006020100]] * [[''tags''|00001006020000#tags]] -To list all roles used in the Zettelstore, send a HTTP GET request to the endpoint ''/m?_key=role''. +To list all roles used in the Zettelstore, send a HTTP GET request to the endpoint ''/m?key=role''. If successful, the output is a JSON object: ```sh -# curl http://127.0.0.1:23123/m?_key=role +# curl http://127.0.0.1:23123/m?key=role {"map":{"configuration":["00000000090002","00000000090000", ... ,"00000000000001"],"manual":["00001014000000", ... ,"00001000000000"],"zettel":["00010000000000", ... ,"00001012070500","00000000090001"]}} ``` The JSON object only contains the key ''"map"'' with the value of another object. This second object contains all role names as keys and the list of identifier of those zettel with this specific role as a value. -Similar, to list all tags used in the Zettelstore, send a HTTP GET request to the endpoint ''/m?_key=tags''. +Similar, to list all tags used in the Zettelstore, send a HTTP GET request to the endpoint ''/m?key=tags''. If successful, the output is a JSON object: ```sh -# curl http://127.0.0.1:23123/m?_key=tags +# curl http://127.0.0.1:23123/m?key=tags {"map":{"#api":[:["00001012921000","00001012920800","00001012920522",...],"#authorization":["00001010040700","00001010040400",...],...,"#zettelstore":["00010000000000","00001014000000",...,"00001001000000"]}} ``` The JSON object only contains the key ''"map"'' with the value of another object. This second object contains all tags as keys and the list of identifier of those zettel with this tag as a value. Please note that this structure will likely change in the future to be more compliant with other API calls. Index: docs/manual/00001012053300.zettel ================================================================== --- docs/manual/00001012053300.zettel +++ docs/manual/00001012053300.zettel @@ -1,11 +1,12 @@ id: 00001012053300 title: API: Retrieve metadata and content of an existing zettel role: manual tags: #api #manual #zettelstore syntax: zmk -modified: 20220724163741 +created: 20211004093206 +modified: 20220908162927 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|00001006050000]]. For example, to retrieve some data about this zettel you are currently viewing, just send a HTTP GET request to the endpoint ''/j/00001012053300''[^If [[authentication is enabled|00001010040100]], you must include the a valid [[access token|00001012050200]] in the ''Authorization'' header]. If successful, the output is a JSON object: @@ -55,12 +56,12 @@ === Plain zettel [!plain]Additionally, you can retrieve the plain zettel, without using JSON. Just change the [[endpoint|00001012920000]] to ''/z/{ID}'' Optionally, you may provide which parts of the zettel you are requesting. -In this case, add an additional query parameter ''_part=[[PART|00001012920800]]''. -Valid values are ""zettel"", ""[[meta|00001012053400]]"", and ""content"" (the default value). +In this case, add an additional query parameter ''part=PART''. +Valid values for [[''PART''|00001012920800]] are ""zettel"", ""[[meta|00001012053400]]"", and ""content"" (the default value). ````sh # curl 'http://127.0.0.1:23123/z/00001012053300' The [[endpoint|00001012920000]] to work with metadata and content of a specific zettel is ''/j/{ID}'', where ''{ID}'' is a placeholder for the [[zettel identifier|00001006050000]]. @@ -69,11 +70,11 @@ ```sh ... ```` ````sh -# curl 'http://127.0.0.1:23123/z/00001012053300?_part=zettel' +# curl 'http://127.0.0.1:23123/z/00001012053300?part=zettel' title: API: Retrieve metadata and content of an existing zettel role: manual tags: #api #manual #zettelstore syntax: zmk @@ -86,15 +87,15 @@ === HTTP Status codes ; ''200'' : Retrieval was successful, the body contains an appropriate JSON object / plain zettel data. ; ''204'' : Request was valid, but there is no data to be returned. - Most likely, you specified the query parameter ''_part=content'', but the zettel does not contain any content. + Most likely, you specified the query parameter ''part=content'', but the zettel does not contain any content. ; ''400'' : Request was not valid. There are several reasons for this. Maybe the [[zettel identifier|00001006050000]] did not consists of exactly 14 digits. ; ''403'' : You are not allowed to retrieve data of the given zettel. ; ''404'' : Zettel not found. You probably used a zettel identifier that is not used in the Zettelstore. Index: docs/manual/00001012053400.zettel ================================================================== --- docs/manual/00001012053400.zettel +++ docs/manual/00001012053400.zettel @@ -1,11 +1,12 @@ id: 00001012053400 title: API: Retrieve metadata of an existing zettel role: manual tags: #api #manual #zettelstore syntax: zmk -modified: 20220202112048 +created: 20210726174524 +modified: 20220908162635 The [[endpoint|00001012920000]] to work with metadata of a specific zettel is ''/m/{ID}'', where ''{ID}'' is a placeholder for the [[zettel identifier|00001006050000]]. 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: @@ -40,14 +41,14 @@ Please consider the [[list of supported metadata keys|00001006020000]] (and their value types). ; ''"rights"'' : An integer number that describes the [[access rights|00001012921200]] for the zettel. [!plain]Additionally, you can retrieve the plain metadata of a zettel, without using JSON. -Just change the [[endpoint|00001012920000]] to ''/z/{ID}?_part=meta'' +Just change the [[endpoint|00001012920000]] to ''/z/{ID}?part=meta'' ````sh -# curl 'http://127.0.0.1:23123/z/00001012053400?_part=meta' +# curl 'http://127.0.0.1:23123/z/00001012053400?part=meta' title: API: Retrieve metadata of an existing zettel role: manual tags: #api #manual #zettelstore syntax: zmk ```` Index: docs/manual/00001012053500.zettel ================================================================== --- docs/manual/00001012053500.zettel +++ docs/manual/00001012053500.zettel @@ -1,11 +1,12 @@ 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: 20220410153546 +created: 20210726174524 +modified: 20220908162843 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|00001006050000]]. 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: @@ -12,15 +13,15 @@ ```sh # curl http://127.0.0.1:23123/v/00001012053500 {"meta":{"title":[{"t":"Text","s":"API:"},{"t":"Space"},{"t":"Text","s":"Retrieve"},{"t":"Space"},{"t":"Text","s":"evaluated"},{"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"}, ... ``` -To select another encoding, you can provide a query parameter ''_enc=[[ENCODING|00001012920500]]''. -The default encoding is ""[[zjson|00001012920503]]"". +To select another encoding, you can provide a query parameter ''enc=ENCODING''. +The default value for [[''ENCODING''|00001012920500]] is ""[[zjson|00001012920503]]"". Others are ""[[html|00001012920510]]"", ""[[text|00001012920519]]"", and some more. ```sh -# curl 'http://127.0.0.1:23123/v/00001012053500?_enc=html' +# curl 'http://127.0.0.1:23123/v/00001012053500?enc=html' API: Retrieve evaluated metadata and content of an existing zettel in various encodings @@ -37,14 +38,14 @@

    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,16 +1,17 @@ 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]] Other commands will be defined in the future. 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: `@@
    @@{="html"}`, + expect: expectMap{ + encoderZJSON: `[{"":"HTML","s":"
    "}]`, + encoderHTML: `
    `, + encoderSexpr: `((LITERAL-HTML () "
    "))`, + encoderText: `
    `, + encoderZmk: useZmk, + }, + }, { descr: "", zmk: ``, expect: expectMap{ encoderZJSON: `[]`, Index: encoder/sexprenc/transform.go ================================================================== --- encoder/sexprenc/transform.go +++ encoder/sexprenc/transform.go @@ -76,11 +76,11 @@ case *ast.DescriptionListNode: return t.getDescriptionList(n) case *ast.TableNode: return t.getTable(n) case *ast.TranscludeNode: - return sxpf.NewPairFromValues(sexpr.SymTransclude, getReference(n.Ref)) + return sxpf.NewPairFromValues(sexpr.SymTransclude, getAttributes(n.Attrs), getReference(n.Ref)) case *ast.BLOBNode: return getBLOB(n) case *ast.TextNode: return sxpf.NewPairFromValues(sexpr.SymText, sxpf.NewString(n.Text)) case *ast.TagNode: @@ -305,11 +305,11 @@ ast.RefStateSelf: sexpr.SymLinkSelf, ast.RefStateFound: sexpr.SymLinkFound, ast.RefStateBroken: sexpr.SymLinkBroken, ast.RefStateHosted: sexpr.SymLinkHosted, ast.RefStateBased: sexpr.SymLinkBased, - ast.RefStateSearch: sexpr.SymLinkSearch, + ast.RefStateQuery: sexpr.SymLinkQuery, ast.RefStateExternal: sexpr.SymLinkExternal, } func (t *transformer) getLink(ln *ast.LinkNode) *sxpf.Pair { return sxpf.NewPair( @@ -397,11 +397,11 @@ ast.RefStateSelf: sexpr.SymRefStateSelf, ast.RefStateFound: sexpr.SymRefStateFound, ast.RefStateBroken: sexpr.SymRefStateBroken, ast.RefStateHosted: sexpr.SymRefStateHosted, ast.RefStateBased: sexpr.SymRefStateBased, - ast.RefStateSearch: sexpr.SymRefStateSearch, + ast.RefStateQuery: sexpr.SymRefStateQuery, ast.RefStateExternal: sexpr.SymRefStateExternal, } func getReference(ref *ast.Reference) *sxpf.Pair { return sxpf.NewPair( Index: encoder/zjsonenc/zjsonenc.go ================================================================== --- encoder/zjsonenc/zjsonenc.go +++ encoder/zjsonenc/zjsonenc.go @@ -111,10 +111,11 @@ v.visitDescriptionList(n) case *ast.TableNode: v.visitTable(n) case *ast.TranscludeNode: v.writeNodeStart(zjson.TypeTransclude) + v.visitAttributes(n.Attrs) v.writeContentStart(zjson.NameString2) writeEscaped(&v.b, mapRefState[n.Ref.State]) v.writeContentStart(zjson.NameString) writeEscaped(&v.b, n.Ref.String()) case *ast.BLOBNode: @@ -355,21 +356,21 @@ ast.RefStateSelf: zjson.RefStateSelf, ast.RefStateFound: zjson.RefStateFound, ast.RefStateBroken: zjson.RefStateBroken, ast.RefStateHosted: zjson.RefStateHosted, ast.RefStateBased: zjson.RefStateBased, - ast.RefStateSearch: zjson.RefStateSearch, + ast.RefStateQuery: zjson.RefStateQuery, ast.RefStateExternal: zjson.RefStateExternal, } func (v *visitor) visitLink(ln *ast.LinkNode) { v.writeNodeStart(zjson.TypeLink) v.visitAttributes(ln.Attrs) v.writeContentStart(zjson.NameString2) writeEscaped(&v.b, mapRefState[ln.Ref.State]) v.writeContentStart(zjson.NameString) - if ln.Ref.State == ast.RefStateSearch { + if ln.Ref.State == ast.RefStateQuery { writeEscaped(&v.b, ln.Ref.Value) } else { writeEscaped(&v.b, ln.Ref.String()) } if len(ln.Inlines) > 0 { Index: encoder/zmkenc/zmkenc.go ================================================================== --- encoder/zmkenc/zmkenc.go +++ encoder/zmkenc/zmkenc.go @@ -124,10 +124,11 @@ v.visitDescriptionList(n) case *ast.TableNode: v.visitTable(n) case *ast.TranscludeNode: v.b.WriteStrings("{{{", n.Ref.String(), "}}}") + v.visitAttributes(n.Attrs) case *ast.BLOBNode: v.visitBLOB(n) case *ast.TextNode: v.visitText(n) case *ast.TagNode: @@ -474,11 +475,11 @@ v.b.WriteString("%%") v.visitAttributes(ln.Attrs) v.b.WriteByte(' ') v.b.Write(ln.Content) case ast.LiteralHTML: - v.writeLiteral('x', syntaxToHTML(ln.Attrs), ln.Content) + v.writeLiteral('@', syntaxToHTML(ln.Attrs), ln.Content) default: panic(fmt.Sprintf("Unknown literal kind %v", ln.Kind)) } } ADDED encoding/rss/rss.go Index: encoding/rss/rss.go ================================================================== --- encoding/rss/rss.go +++ encoding/rss/rss.go @@ -0,0 +1,149 @@ +//----------------------------------------------------------------------------- +// Copyright (c) 2022 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 rss provides a RSS encoding. +package rss + +import ( + "bytes" + "context" + "encoding/xml" + "time" + + "zettelstore.de/c/api" + "zettelstore.de/z/config" + "zettelstore.de/z/domain/id" + "zettelstore.de/z/domain/meta" + "zettelstore.de/z/encoder/textenc" + "zettelstore.de/z/kernel" + "zettelstore.de/z/parser" + "zettelstore.de/z/query" +) + +const ContentType = "application/rss+xml" + +type Configuration struct { + Title string + Language string + Copyright string + Generator string + NewURLBuilderAbs func() *api.URLBuilder +} + +func (c *Configuration) Setup(ctx context.Context, cfg config.Config) { + baseURL := kernel.Main.GetConfig(kernel.WebService, kernel.WebBaseURL).(string) + defVals := cfg.AddDefaultValues(ctx, &meta.Meta{}) + + c.Title = cfg.GetSiteName() + c.Language = defVals.GetDefault(api.KeyLang, "") + c.Copyright = defVals.GetDefault(api.KeyCopyright, "") + c.Generator = (kernel.Main.GetConfig(kernel.CoreService, kernel.CoreProgname).(string) + + " " + + kernel.Main.GetConfig(kernel.CoreService, kernel.CoreVersion).(string)) + c.NewURLBuilderAbs = func() *api.URLBuilder { return api.NewURLBuilder(baseURL, 'h') } +} + +func (c *Configuration) Marshal(q *query.Query, ml []*meta.Meta) ([]byte, error) { + textEnc := textenc.Create() + rssItems := make([]*RssItem, 0, len(ml)) + maxPublished := time.Date(1, time.January, 1, 0, 0, 0, 0, time.Local) + for _, m := range ml { + var title bytes.Buffer + titleIns := parser.ParseMetadata(m.GetTitle()) + if _, err := textEnc.WriteInlines(&title, &titleIns); err != nil { + title.Reset() + title.WriteString(m.GetTitle()) + } + + itemPublished := "" + if val, found := m.Get(api.KeyPublished); found { + if published, err := time.ParseInLocation(id.ZidLayout, val, time.Local); err == nil { + itemPublished = published.UTC().Format(time.RFC1123Z) + if maxPublished.Before(published) { + maxPublished = published + } + } + } + + link := c.NewURLBuilderAbs().SetZid(api.ZettelID(m.Zid.String())).String() + rssItems = append(rssItems, &RssItem{ + Title: title.String(), + Link: link, + GUID: link, + PubDate: itemPublished, + }) + } + + rssPublished := "" + if maxPublished.Year() > 1 { + rssPublished = maxPublished.UTC().Format(time.RFC1123Z) + } + var atomLink *AtomLink + if s := q.String(); s != "" { + atomLink = &AtomLink{ + Href: c.NewURLBuilderAbs().AppendQuery(s).String(), + Rel: "self", + Type: ContentType, + } + } + rssFeed := RssFeed{ + Version: "2.0", + AtomNamespace: "http://www.w3.org/2005/Atom", + Channel: &RssChannel{ + Title: c.Title, + Link: c.NewURLBuilderAbs().String(), + Language: c.Language, + Copyright: c.Copyright, + PubDate: rssPublished, + LastBuildDate: rssPublished, + Generator: c.Generator, + Docs: "https://www.rssboard.org/rss-specification", + AtomLink: atomLink, + Items: rssItems, + }, + } + return xml.MarshalIndent(&rssFeed, "", " ") +} + +type ( + RssFeed struct { + XMLName xml.Name `xml:"rss"` + Version string `xml:"version,attr"` + AtomNamespace string `xml:"xmlns:atom,attr"` + Channel *RssChannel + } + RssChannel struct { + XMLName xml.Name `xml:"channel"` + Title string `xml:"title"` + Link string `xml:"link"` + Description string `xml:"description"` + Language string `xml:"language,omitempty"` + Copyright string `xml:"copyright,omitempty"` + PubDate string `xml:"pubDate,omitempty"` // RFC822 + LastBuildDate string `xml:"lastBuildDate,omitempty"` // RFC822 + Generator string `xml:"generator,omitempty"` + Docs string `xml:"docs,omitempty"` + AtomLink *AtomLink + Items []*RssItem `xml:"item"` + } + AtomLink struct { + XMLName xml.Name `xml:"atom:link"` + Href string `xml:"href,attr"` + Rel string `xml:"rel,attr"` + Type string `xml:"type,attr"` + } + RssItem struct { + XMLName xml.Name `xml:"item"` + Title string `xml:"title"` + Link string `xml:"link"` // Needed, b/c Miniflux does not use GUID for URL + GUID string `xml:"guid"` + PubDate string `xml:"pubDate,omitempty"` // RFC822 + } +) Index: evaluator/evaluator.go ================================================================== --- evaluator/evaluator.go +++ evaluator/evaluator.go @@ -28,18 +28,18 @@ "zettelstore.de/z/domain/meta" "zettelstore.de/z/input" "zettelstore.de/z/parser" "zettelstore.de/z/parser/cleaner" "zettelstore.de/z/parser/draw" - "zettelstore.de/z/search" + "zettelstore.de/z/query" ) // 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) - SelectMeta(ctx context.Context, s *search.Search) ([]*meta.Meta, error) + SelectMeta(ctx context.Context, q *query.Query) ([]*meta.Meta, error) } // EvaluateZettel evaluates the given zettel in the given context, with the // given ports, and the given environment. func EvaluateZettel(ctx context.Context, port Port, rtConfig config.Config, zn *ast.ZettelNode) { @@ -46,12 +46,18 @@ if zn.Syntax == api.ValueSyntaxNone { // AST is empty, evaluate to a description list of metadata. zn.Ast = evaluateMetadata(zn.Meta) return } - evaluateNode(ctx, port, rtConfig, &zn.Ast) - cleaner.CleanBlockSlice(&zn.Ast) + EvaluateBlock(ctx, port, rtConfig, &zn.Ast) +} + +// EvaluateBlock evaluates the given block list in the given context, with +// the given ports, and the given environment. +func EvaluateBlock(ctx context.Context, port Port, rtConfig config.Config, bns *ast.BlockSlice) { + evaluateNode(ctx, port, rtConfig, bns) + cleaner.CleanBlockSlice(bns) } // EvaluateInline evaluates the given inline list in the given context, with // the given ports, and the given environment. func EvaluateInline(ctx context.Context, port Port, rtConfig config.Config, is *ast.InlineSlice) { @@ -197,13 +203,13 @@ case ast.RefStateSelf: e.transcludeCount++ return makeBlockNode(createInlineErrorText(ref, "Self", "transclusion", "reference")) case ast.RefStateFound, ast.RefStateHosted, ast.RefStateBased, ast.RefStateExternal: return tn - case ast.RefStateSearch: + case ast.RefStateQuery: e.transcludeCount++ - return e.evalSearchTransclusion(tn.Ref.Value) + return e.evalQueryTransclusion(tn.Ref.Value) default: return makeBlockNode(createInlineErrorText(ref, "Illegal", "block", "state", strconv.Itoa(int(ref.State)))) } zid, err := id.Parse(ref.URL.Path) @@ -224,10 +230,11 @@ return nil } e.transcludeCount++ return makeBlockNode(createInlineErrorText(ref, "Unable", "to", "get", "zettel")) } + setMetadataFromAttributes(zettel.Meta, tn.Attrs) ec := e.transcludeCount e.costMap[zid] = transcludeCost{zn: e.marker, ec: ec} zn = e.evaluateEmbeddedZettel(zettel) e.costMap[zid] = transcludeCost{zn: zn, ec: e.transcludeCount - ec} e.transcludeCount = 0 // No stack needed, because embedding is done left-recursive, depth-first. @@ -237,40 +244,23 @@ e.transcludeCount += cost.ec } return &zn.Ast } -func (e *evaluator) evalSearchTransclusion(expr string) ast.BlockNode { - ml, err := e.port.SelectMeta(e.ctx, search.Parse(expr)) +func (e *evaluator) evalQueryTransclusion(expr string) ast.BlockNode { + q := query.Parse(expr) + ml, err := e.port.SelectMeta(e.ctx, q) if err != nil { if errors.Is(err, &box.ErrNotAllowed{}) { return nil } return makeBlockNode(createInlineErrorText(nil, "Unable", "to", "search", "zettel")) } - if len(ml) == 0 { - return nil - } - items := make([]ast.ItemSlice, 0, len(ml)) - for _, m := range ml { - zid := m.Zid.String() - title, found := m.Get(api.KeyTitle) - if !found { - title = zid - } - items = append(items, ast.ItemSlice{ast.CreateParaNode(&ast.LinkNode{ - Attrs: nil, - Ref: ast.ParseReference(zid), - Inlines: parser.ParseMetadataNoLink(title), - })}) - } - result := &ast.NestedListNode{ - Kind: ast.NestedListUnordered, - Items: items, - Attrs: nil, - } - ast.Walk(e, result) + result := QueryAction(e.ctx, q, ml, e.rtConfig) + if result != nil { + ast.Walk(e, result) + } return result } func (e *evaluator) checkMaxTransclusions(ref *ast.Reference) ast.InlineNode { if maxTrans := e.transcludeMax; e.transcludeCount > maxTrans { @@ -281,10 +271,18 @@ } return nil } func makeBlockNode(in ast.InlineNode) ast.BlockNode { return ast.CreateParaNode(in) } + +func setMetadataFromAttributes(m *meta.Meta, a attrs.Attributes) { + for aKey, aVal := range a { + if meta.KeyIsValid(aKey) { + m.Set(aKey, aVal) + } + } +} func (e *evaluator) visitInlineSlice(is *ast.InlineSlice) { for i := 0; i < len(*is); i++ { in := (*is)[i] ast.Walk(e, in) @@ -495,11 +493,11 @@ ast.Walk(e, &is) return is } func (e *evaluator) evaluateEmbeddedZettel(zettel domain.Zettel) *ast.ZettelNode { - zn := parser.ParseZettel(zettel, zettel.Meta.GetDefault(api.KeySyntax, ""), e.rtConfig) + zn := parser.ParseZettel(e.ctx, zettel, zettel.Meta.GetDefault(api.KeySyntax, ""), e.rtConfig) ast.Walk(e, &zn.Ast) return zn } func findInlineSlice(bs *ast.BlockSlice, fragment string) ast.InlineSlice { ADDED evaluator/list.go Index: evaluator/list.go ================================================================== --- evaluator/list.go +++ evaluator/list.go @@ -0,0 +1,283 @@ +//----------------------------------------------------------------------------- +// Copyright (c) 2022 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 evaluator + +import ( + "bytes" + "context" + "log" + "sort" + "strconv" + "strings" + + "zettelstore.de/c/api" + "zettelstore.de/c/attrs" + "zettelstore.de/z/ast" + "zettelstore.de/z/config" + "zettelstore.de/z/domain/meta" + "zettelstore.de/z/encoding/rss" + "zettelstore.de/z/parser" + "zettelstore.de/z/query" +) + +// QueryAction transforms a list of metadata according to query actions into a AST nested list. +func QueryAction(ctx context.Context, q *query.Query, ml []*meta.Meta, rtConfig config.Config) ast.BlockNode { + ap := actionPara{ + ctx: ctx, + q: q, + ml: ml, + kind: ast.NestedListUnordered, + min: -1, + max: -1, + title: rtConfig.GetSiteName(), + } + if actions := q.Actions(); len(actions) > 0 { + acts := make([]string, 0, len(actions)) + for i, act := range actions { + if strings.HasPrefix(act, "N") { + ap.kind = ast.NestedListOrdered + continue + } + if strings.HasPrefix(act, "MIN") { + if num, err := strconv.Atoi(act[3:]); err == nil && num > 0 { + ap.min = num + continue + } + } + if strings.HasPrefix(act, "MAX") { + if num, err := strconv.Atoi(act[3:]); err == nil && num > 0 { + ap.max = num + continue + } + } + if act == "TITLE" && i+1 < len(actions) { + ap.title = strings.Join(actions[i+1:], " ") + break + } + acts = append(acts, act) + } + for _, act := range acts { + if act == "RSS" { + return ap.createBlockNodeRSS(rtConfig) + } + key := strings.ToLower(act) + switch meta.Type(key) { + case meta.TypeWord: + return ap.createBlockNodeWord(key) + case meta.TypeTagSet: + return ap.createBlockNodeTagSet(key) + } + } + } + return ap.createBlockNodeMeta() +} + +type actionPara struct { + ctx context.Context + q *query.Query + ml []*meta.Meta + kind ast.NestedListKind + min int + max int + title string +} + +func (ap *actionPara) createBlockNodeWord(key string) ast.BlockNode { + var buf bytes.Buffer + ccs, bufLen := ap.prepareCatAction(key, &buf) + if len(ccs) == 0 { + return nil + } + items := make([]ast.ItemSlice, 0, len(ccs)) + ccs.SortByName() + for _, cat := range ccs { + buf.WriteString(cat.Name) + items = append(items, ast.ItemSlice{ast.CreateParaNode(&ast.LinkNode{ + Attrs: nil, + Ref: ast.ParseReference(buf.String()), + Inlines: ast.InlineSlice{&ast.TextNode{Text: cat.Name}}, + })}) + buf.Truncate(bufLen) + } + return &ast.NestedListNode{ + Kind: ap.kind, + Items: items, + Attrs: nil, + } +} + +func (ap *actionPara) createBlockNodeTagSet(key string) ast.BlockNode { + var buf bytes.Buffer + ccs, bufLen := ap.prepareCatAction(key, &buf) + if len(ccs) == 0 { + return nil + } + ccs.SortByCount() + if min, max := ap.min, ap.max; min > 0 || max > 0 { + if min < 0 { + min = ccs[len(ccs)-1].Count + } + if max < 0 { + max = ccs[0].Count + } + if ccs[len(ccs)-1].Count < min || max < ccs[0].Count { + temp := make(meta.CountedCategories, 0, len(ccs)) + for _, cat := range ccs { + if min <= cat.Count && cat.Count <= max { + temp = append(temp, cat) + } + } + ccs = temp + } + } + countMap := ap.calcFontSizes(ccs) + + para := make(ast.InlineSlice, 0, len(ccs)) + ccs.SortByName() + for i, cat := range ccs { + if i > 0 { + para = append(para, &ast.SpaceNode{ + Lexeme: " ", + }) + } + buf.WriteString(cat.Name) + para = append(para, + &ast.LinkNode{ + Attrs: countMap[cat.Count], + Ref: ast.ParseReference(buf.String()), + Inlines: ast.InlineSlice{ + &ast.TextNode{Text: cat.Name}, + }, + }, + &ast.FormatNode{ + Kind: ast.FormatSuper, + Attrs: nil, + Inlines: ast.InlineSlice{&ast.TextNode{Text: strconv.Itoa(cat.Count)}}, + }, + ) + buf.Truncate(bufLen) + } + return &ast.ParaNode{ + Inlines: para, + } +} + +func (ap *actionPara) createBlockNodeMeta() ast.BlockNode { + if len(ap.ml) == 0 { + return nil + } + items := make([]ast.ItemSlice, 0, len(ap.ml)) + for _, m := range ap.ml { + zid := m.Zid.String() + title, found := m.Get(api.KeyTitle) + if !found { + title = zid + } + items = append(items, ast.ItemSlice{ast.CreateParaNode(&ast.LinkNode{ + Attrs: nil, + Ref: ast.ParseReference(zid), + Inlines: parser.ParseMetadataNoLink(title), + })}) + } + return &ast.NestedListNode{ + Kind: ap.kind, + Items: items, + Attrs: nil, + } +} + +func (ap *actionPara) prepareCatAction(key string, buf *bytes.Buffer) (meta.CountedCategories, int) { + if len(ap.ml) == 0 { + return nil, 0 + } + ccs := meta.CreateArrangement(ap.ml, key).Counted() + if len(ccs) == 0 { + return nil, 0 + } + + sea := ap.q.Clone() + sea.RemoveActions() + buf.WriteString(ast.QueryPrefix) + sea.Print(buf) + if buf.Len() > len(ast.QueryPrefix) { + buf.WriteByte(' ') + } + buf.WriteString(key) + buf.WriteByte(':') + bufLen := buf.Len() + + return ccs, bufLen +} + +const fontSizes = 6 // Must be the number of CSS classes zs-font-size-* in base.css + +func (*actionPara) calcFontSizes(ccs meta.CountedCategories) map[int]attrs.Attributes { + var fsAttrs [fontSizes]attrs.Attributes + var a attrs.Attributes + for i := 0; i < fontSizes; i++ { + fsAttrs[i] = a.AddClass("zs-font-size-" + strconv.Itoa(i)) + } + + countMap := make(map[int]int, len(ccs)) + for _, cat := range ccs { + countMap[cat.Count]++ + } + + countList := make([]int, 0, len(countMap)) + for count := range countMap { + countList = append(countList, count) + } + sort.Ints(countList) + + result := make(map[int]attrs.Attributes, len(countList)) + if len(countList) <= fontSizes { + // If we have less different counts, center them inside the fsAttrs vector. + curSize := (fontSizes - len(countList)) / 2 + for _, count := range countList { + result[count] = fsAttrs[curSize] + curSize++ + } + return result + } + + // Idea: the number of occurences for a specific count is substracted from a budget. + budget := len(ccs) / (fontSizes - 1) + curBudget := budget + curSize := 0 + for _, count := range countList { + result[count] = fsAttrs[curSize] + curBudget -= countMap[count] + for curBudget <= 0 { + curBudget += budget + curSize++ + if curSize >= fontSizes { + curSize = fontSizes - 1 + } + } + } + return result +} + +func (ap *actionPara) createBlockNodeRSS(cfg config.Config) ast.BlockNode { + var rssConfig rss.Configuration + rssConfig.Setup(ap.ctx, cfg) + rssConfig.Title = ap.title + data, err := rssConfig.Marshal(ap.q, ap.ml) + if err != nil { + log.Println("ERRR", err) + return nil + } + return &ast.VerbatimNode{ + Kind: ast.VerbatimProg, + Attrs: attrs.Attributes{"lang": "xml"}, + Content: data, + } +} Index: go.mod ================================================================== --- go.mod +++ go.mod @@ -4,13 +4,13 @@ require ( codeberg.org/t73fde/sxpf v0.0.0-20220719090054-749a39d0a7a0 github.com/fsnotify/fsnotify v1.5.4 github.com/pascaldekloe/jwt v1.12.0 - github.com/yuin/goldmark v1.4.13 - golang.org/x/crypto v0.0.0-20220722155217-630584e8d5aa + github.com/yuin/goldmark v1.4.14 + golang.org/x/crypto v0.0.0-20220829220503-c86fa9a7ed90 golang.org/x/term v0.0.0-20220722155259-a9ba230a4035 golang.org/x/text v0.3.7 - zettelstore.de/c v0.6.0 + zettelstore.de/c v0.7.0 ) -require golang.org/x/sys v0.0.0-20220804214406-8e32c043e418 // indirect +require golang.org/x/sys v0.0.0-20220915200043-7b5979e65e41 // indirect Index: go.sum ================================================================== --- go.sum +++ go.sum @@ -2,18 +2,18 @@ codeberg.org/t73fde/sxpf v0.0.0-20220719090054-749a39d0a7a0/go.mod h1:4fAHEF3VH+ofbZkF6NzqiItTNy2X11tVCnZX99jXouA= github.com/fsnotify/fsnotify v1.5.4 h1:jRbGcIw6P2Meqdwuo0H1p6JVLbL5DHKAKlYndzMwVZI= github.com/fsnotify/fsnotify v1.5.4/go.mod h1:OVB6XrOHzAwXMpEM7uPOzcehqUV2UqJxmVXmkdnm1bU= github.com/pascaldekloe/jwt v1.12.0 h1:imQSkPOtAIBAXoKKjL9ZVJuF/rVqJ+ntiLGpLyeqMUQ= github.com/pascaldekloe/jwt v1.12.0/go.mod h1:LiIl7EwaglmH1hWThd/AmydNCnHf/mmfluBlNqHbk8U= -github.com/yuin/goldmark v1.4.13 h1:fVcFKWvrslecOb/tg+Cc05dkeYx540o0FuFt3nUVDoE= -github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY= -golang.org/x/crypto v0.0.0-20220722155217-630584e8d5aa h1:zuSxTR4o9y82ebqCUJYNGJbGPo6sKVl54f/TVDObg1c= -golang.org/x/crypto v0.0.0-20220722155217-630584e8d5aa/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4= +github.com/yuin/goldmark v1.4.14 h1:jwww1XQfhJN7Zm+/a1ZA/3WUiEBEroYFNTiV3dKwM8U= +github.com/yuin/goldmark v1.4.14/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY= +golang.org/x/crypto v0.0.0-20220829220503-c86fa9a7ed90 h1:Y/gsMcFOcR+6S6f3YeMKl5g+dZMEWqcz5Czj/GWYbkM= +golang.org/x/crypto v0.0.0-20220829220503-c86fa9a7ed90/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4= golang.org/x/sys v0.0.0-20220412211240-33da011f77ad/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.0.0-20220804214406-8e32c043e418 h1:9vYwv7OjYaky/tlAeD7C4oC9EsPTlaFl1H2jS++V+ME= -golang.org/x/sys v0.0.0-20220804214406-8e32c043e418/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220915200043-7b5979e65e41 h1:ohgcoMbSofXygzo6AD2I1kz3BFmW1QArPYTtwEM3UXc= +golang.org/x/sys v0.0.0-20220915200043-7b5979e65e41/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/term v0.0.0-20220722155259-a9ba230a4035 h1:Q5284mrmYTpACcm+eAKjKJH48BBwSyfJqmmGDTtT8Vc= golang.org/x/term v0.0.0-20220722155259-a9ba230a4035/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= golang.org/x/text v0.3.7 h1:olpwvP2KacW1ZWvsR7uQhoyTYvKAupfQrRGBFM352Gk= golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= -zettelstore.de/c v0.6.0 h1:5EXEgIpDxFG0zBrq0qBmLzAmbye57oDro1Wy3Zxmw6U= -zettelstore.de/c v0.6.0/go.mod h1:+SoneUhKQ81A2Id/bC6FdDYYQAHYfVryh7wHFnnklew= +zettelstore.de/c v0.7.0 h1:+DmAB81uVLtgf5xFKy4HqFqja+6itFyuk45S9QZeP+k= +zettelstore.de/c v0.7.0/go.mod h1:+SoneUhKQ81A2Id/bC6FdDYYQAHYfVryh7wHFnnklew= Index: kernel/impl/box.go ================================================================== --- kernel/impl/box.go +++ kernel/impl/box.go @@ -75,11 +75,11 @@ } boxURIs = append(boxURIs, u.(*url.URL)) } ps.mxService.Lock() defer ps.mxService.Unlock() - mgr, err := ps.createManager(boxURIs, kern.auth.manager, kern.cfg.rtConfig) + mgr, err := ps.createManager(boxURIs, kern.auth.manager, &kern.cfg) if err != nil { ps.logger.Fatal().Err(err).Msg("Unable to create manager") return err } ps.logger.Info().Str("location", mgr.Location()).Msg("Start Manager") Index: kernel/impl/cfg.go ================================================================== --- kernel/impl/cfg.go +++ kernel/impl/cfg.go @@ -10,38 +10,39 @@ package impl import ( "context" + "fmt" + "strconv" "strings" "sync" "zettelstore.de/c/api" "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/logger" + "zettelstore.de/z/web/server" ) type configService struct { srvConfig mxService sync.RWMutex - rtConfig *myConfig + orig *meta.Meta } // Predefined Metadata keys for runtime configuration // See: https://zettelstore.de/manual/h/00001004020000 const ( keyDefaultCopyright = "default-copyright" - keyDefaultLang = "default-lang" keyDefaultLicense = "default-license" keyDefaultVisibility = "default-visibility" keyExpertMode = "expert-mode" - keyFooterHTML = "footer-html" keyHomeZettel = "home-zettel" - keyMarkerExternal = "marker-external" keyMaxTransclusions = "max-transclusions" keySiteName = "site-name" keyYAMLHeader = "yaml-header" keyZettelFileSyntax = "zettel-file-syntax" ) @@ -48,11 +49,10 @@ func (cs *configService) Initialize(logger *logger.Logger) { cs.logger = logger cs.descr = descriptionMap{ keyDefaultCopyright: {"Default copyright", parseString, true}, - keyDefaultLang: {"Default language", parseString, true}, keyDefaultLicense: {"Default license", parseString, true}, keyDefaultVisibility: { "Default zettel visibility", func(val string) interface{} { vis := meta.GetVisibility(val) @@ -61,38 +61,39 @@ } return vis }, true, }, - keyExpertMode: {"Expert mode", parseBool, true}, - keyFooterHTML: {"Footer HTML", parseString, true}, - keyHomeZettel: {"Home zettel", parseZid, true}, - keyMarkerExternal: {"Marker external URL", parseString, true}, - keyMaxTransclusions: {"Maximum transclusions", parseInt64, true}, - keySiteName: {"Site name", parseString, true}, - keyYAMLHeader: {"YAML header", parseBool, true}, + keyExpertMode: {"Expert mode", parseBool, true}, + config.KeyFooterHTML: {"Footer HTML", parseString, true}, + keyHomeZettel: {"Home zettel", parseZid, true}, + api.KeyLang: {"Language", parseString, true}, + config.KeyMarkerExternal: {"Marker external URL", parseString, true}, + keyMaxTransclusions: {"Maximum transclusions", parseInt64, true}, + keySiteName: {"Site name", parseString, true}, + keyYAMLHeader: {"YAML header", parseBool, true}, keyZettelFileSyntax: { "Zettel file syntax", func(val string) interface{} { return strings.Fields(val) }, true, }, kernel.ConfigSimpleMode: {"Simple mode", cs.noFrozen(parseBool), true}, } cs.next = interfaceMap{ - keyDefaultCopyright: "", - keyDefaultLang: api.ValueLangEN, - keyDefaultLicense: "", - keyDefaultVisibility: meta.VisibilityLogin, - keyExpertMode: false, - keyFooterHTML: "", - keyHomeZettel: id.DefaultHomeZid, - keyMarkerExternal: "➚", - keyMaxTransclusions: int64(1024), - keySiteName: "Zettelstore", - keyYAMLHeader: false, - keyZettelFileSyntax: nil, - kernel.ConfigSimpleMode: false, + keyDefaultCopyright: "", + keyDefaultLicense: "", + keyDefaultVisibility: meta.VisibilityLogin, + keyExpertMode: false, + config.KeyFooterHTML: "", + keyHomeZettel: id.DefaultHomeZid, + api.KeyLang: api.ValueLangEN, + config.KeyMarkerExternal: "➚", + keyMaxTransclusions: int64(1024), + keySiteName: "Zettelstore", + keyYAMLHeader: false, + keyZettelFileSyntax: nil, + kernel.ConfigSimpleMode: false, } } func (cs *configService) GetLogger() *logger.Logger { return cs.logger } func (cs *configService) Start(*myKernel) error { @@ -100,195 +101,191 @@ data := meta.New(id.ConfigurationZid) for _, kv := range cs.GetNextConfigList() { data.Set(kv.Key, kv.Value) } cs.mxService.Lock() - cs.rtConfig = newConfig(cs.logger, data) + cs.orig = data cs.mxService.Unlock() return nil } func (cs *configService) IsStarted() bool { cs.mxService.RLock() defer cs.mxService.RUnlock() - return cs.rtConfig != nil + return cs.orig != nil } func (cs *configService) Stop(*myKernel) { cs.logger.Info().Msg("Stop Service") cs.mxService.Lock() - cs.rtConfig = nil + cs.orig = nil cs.mxService.Unlock() } func (*configService) GetStatistics() []kernel.KeyValue { return nil } func (cs *configService) setBox(mgr box.Manager) { - cs.rtConfig.setBox(mgr) -} - -// myConfig contains all runtime configuration data relevant for the software. -type myConfig struct { - log *logger.Logger - mx sync.RWMutex - orig *meta.Meta - data *meta.Meta -} - -// New creates a new Config value. -func newConfig(logger *logger.Logger, orig *meta.Meta) *myConfig { - cfg := myConfig{ - log: logger, - orig: orig, - data: orig.Clone(), - } - return &cfg -} -func (cfg *myConfig) setBox(mgr box.Manager) { - mgr.RegisterObserver(cfg.observe) - cfg.doUpdate(mgr) -} - -func (cfg *myConfig) doUpdate(p box.Box) error { - m, err := p.GetMeta(context.Background(), cfg.data.Zid) + mgr.RegisterObserver(cs.observe) + cs.doUpdate(mgr) +} + +func (cs *configService) doUpdate(p box.Box) error { + m, err := p.GetMeta(context.Background(), cs.orig.Zid) + cs.logger.Trace().Err(err).Msg("got config meta") if err != nil { return err } - cfg.mx.Lock() - for _, pair := range cfg.data.Pairs() { + cs.mxService.Lock() + for _, pair := range cs.orig.Pairs() { key := pair.Key if val, ok := m.Get(key); ok { - cfg.data.Set(key, val) - } else if defVal, defFound := cfg.orig.Get(key); defFound { - cfg.data.Set(key, defVal) + cs.SetConfig(key, val) + } else if defVal, defFound := cs.orig.Get(key); defFound { + cs.SetConfig(key, defVal) } } - cfg.mx.Unlock() + cs.mxService.Unlock() + cs.SwitchNextToCur() // Poor man's restart return nil } -func (cfg *myConfig) observe(ci box.UpdateInfo) { - if ci.Reason == box.OnReload || ci.Zid == id.ConfigurationZid { - cfg.log.Debug().Uint("reason", uint64(ci.Reason)).Zid(ci.Zid).Msg("observe") - go func() { cfg.doUpdate(ci.Box) }() +func (cs *configService) observe(ci box.UpdateInfo) { + if ci.Reason == box.OnReload { + cs.logger.Debug().Msg("reload") + go func() { cs.doUpdate(ci.Box) }() + } else if ci.Zid == id.ConfigurationZid { + cs.logger.Debug().Uint("reason", uint64(ci.Reason)).Zid(ci.Zid).Msg("observe") + go func() { cs.doUpdate(ci.Box) }() } } -var defaultKeys = map[string]string{ - api.KeyCopyright: keyDefaultCopyright, - api.KeyLang: keyDefaultLang, - api.KeyLicense: keyDefaultLicense, - api.KeyVisibility: keyDefaultVisibility, +// --- config.Config + +func (cs *configService) Get(ctx context.Context, m *meta.Meta, key string) string { + if m != nil { + if val, found := m.Get(key); found { + return val + } + } + if user := server.GetUser(ctx); user != nil { + if val, found := user.Get(key); found { + return val + } + } + result := cs.GetConfig(key) + if result == nil { + return "" + } + switch val := result.(type) { + case string: + return val + case bool: + if val { + return api.ValueTrue + } + return api.ValueFalse + case id.Zid: + return val.String() + case int: + return strconv.Itoa(val) + case []string: + return strings.Join(val, " ") + case meta.Visibility: + return val.String() + case fmt.Stringer: + return val.String() + case fmt.GoStringer: + return val.GoString() + } + return fmt.Sprintf("%v", result) } // AddDefaultValues enriches the given meta data with its default values. -func (cfg *myConfig) AddDefaultValues(m *meta.Meta) *meta.Meta { - if cfg == nil { +func (cs *configService) AddDefaultValues(ctx context.Context, m *meta.Meta) *meta.Meta { + if cs == nil { return m } result := m - cfg.mx.RLock() - for k, d := range defaultKeys { - if _, ok := result.Get(k); !ok { - if val, ok2 := cfg.data.Get(d); ok2 && val != "" { - if result == m { - result = m.Clone() - } - result.Set(k, val) - } - } - } - cfg.mx.RUnlock() + cs.mxService.RLock() + if _, found := m.Get(api.KeyCopyright); !found { + result = updateMeta(result, m, api.KeyCopyright, cs.GetConfig(keyDefaultCopyright).(string)) + } + if _, found := m.Get(api.KeyLang); !found { + result = updateMeta(result, m, api.KeyLang, cs.Get(ctx, nil, api.KeyLang)) + } + if _, found := m.Get(api.KeyLicense); !found { + result = updateMeta(result, m, api.KeyLicense, cs.GetConfig(keyDefaultLicense).(string)) + } + if _, found := m.Get(api.KeyVisibility); !found { + result = updateMeta(result, m, api.KeyVisibility, cs.GetConfig(keyDefaultVisibility).(meta.Visibility).String()) + } + cs.mxService.RUnlock() + return result +} +func updateMeta(result, m *meta.Meta, key, val string) *meta.Meta { + if result == m { + result = m.Clone() + } + result.Set(key, val) return result } -func (cfg *myConfig) getString(key string) string { - cfg.mx.RLock() - val, _ := cfg.data.Get(key) - cfg.mx.RUnlock() - return val -} -func (cfg *myConfig) getBool(key string) bool { - cfg.mx.RLock() - val := cfg.data.GetBool(key) - cfg.mx.RUnlock() - return val -} - -// GetDefaultLang returns the current value of the "default-lang" key. -func (cfg *myConfig) GetDefaultLang() string { return cfg.getString(keyDefaultLang) } - // GetSiteName returns the current value of the "site-name" key. -func (cfg *myConfig) GetSiteName() string { return cfg.getString(keySiteName) } +func (cs *configService) GetSiteName() string { return cs.GetConfig(keySiteName).(string) } // GetHomeZettel returns the value of the "home-zettel" key. -func (cfg *myConfig) GetHomeZettel() id.Zid { - val := cfg.getString(keyHomeZettel) - if homeZid, err := id.Parse(val); err == nil { +func (cs *configService) GetHomeZettel() id.Zid { + homeZid := cs.GetConfig(keyHomeZettel).(id.Zid) + if homeZid != id.Invalid { return homeZid } - cfg.mx.RLock() - val, _ = cfg.orig.Get(keyHomeZettel) - homeZid, _ := id.Parse(val) - cfg.mx.RUnlock() + cs.mxService.RLock() + val, _ := cs.orig.Get(keyHomeZettel) + homeZid, _ = id.Parse(val) + cs.mxService.RUnlock() return homeZid } // GetMaxTransclusions return the maximum number of indirect transclusions. -func (cfg *myConfig) GetMaxTransclusions() int { - const defaultValue = 1024 - cfg.mx.RLock() - val := cfg.data.GetNumber(keyMaxTransclusions, defaultValue) - cfg.mx.RUnlock() - if 0 < val && val < 100000000 { - return int(val) - } - return defaultValue +func (cs *configService) GetMaxTransclusions() int { + return int(cs.GetConfig(keyMaxTransclusions).(int64)) } // GetYAMLHeader returns the current value of the "yaml-header" key. -func (cfg *myConfig) GetYAMLHeader() bool { return cfg.getBool(keyYAMLHeader) } - -// GetMarkerExternal returns the current value of the "marker-external" key. -func (cfg *myConfig) GetMarkerExternal() string { - return cfg.getString(keyMarkerExternal) -} - -// GetFooterHTML returns HTML code that should be embedded into the footer -// of each WebUI page. -func (cfg *myConfig) GetFooterHTML() string { return cfg.getString(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(keyZettelFileSyntax) -} - -// --- AuthConfig - -// GetSimpleMode returns true if system tuns in simple-mode. -func (cfg *myConfig) GetSimpleMode() bool { return cfg.getBool(kernel.ConfigSimpleMode) } - -// GetExpertMode returns the current value of the "expert-mode" key. -func (cfg *myConfig) GetExpertMode() bool { return cfg.getBool(keyExpertMode) } - -// GetVisibility returns the visibility value, or "login" if none is given. -func (cfg *myConfig) GetVisibility(m *meta.Meta) meta.Visibility { +func (cs *configService) GetYAMLHeader() bool { return cs.GetConfig(keyYAMLHeader).(bool) } + +// GetZettelFileSyntax returns the current value of the "zettel-file-syntax" key. +func (cs *configService) GetZettelFileSyntax() []string { + if zfs := cs.GetConfig(keyZettelFileSyntax); zfs != nil { + return zfs.([]string) + } + return nil +} + +// --- config.AuthConfig + +// GetSimpleMode returns true if system tuns in simple-mode. +func (cs *configService) GetSimpleMode() bool { return cs.GetConfig(kernel.ConfigSimpleMode).(bool) } + +// GetExpertMode returns the current value of the "expert-mode" key. +func (cs *configService) GetExpertMode() bool { return cs.GetConfig(keyExpertMode).(bool) } + +// GetVisibility returns the visibility value, or "login" if none is given. +func (cs *configService) GetVisibility(m *meta.Meta) meta.Visibility { if val, ok := m.Get(api.KeyVisibility); ok { if vis := meta.GetVisibility(val); vis != meta.VisibilityUnknown { return vis } } - val := cfg.getString(keyDefaultVisibility) - if vis := meta.GetVisibility(val); vis != meta.VisibilityUnknown { + vis := cs.GetConfig(keyDefaultVisibility).(meta.Visibility) + if vis != meta.VisibilityUnknown { return vis } - cfg.mx.RLock() - val, _ = cfg.orig.Get(keyDefaultVisibility) - vis := meta.GetVisibility(val) - cfg.mx.RUnlock() + cs.mxService.RLock() + val, _ := cs.orig.Get(keyDefaultVisibility) + vis = meta.GetVisibility(val) + cs.mxService.RUnlock() return vis } Index: kernel/impl/core.go ================================================================== --- kernel/impl/core.go +++ kernel/impl/core.go @@ -17,10 +17,11 @@ "runtime" "sync" "time" "zettelstore.de/c/maps" + "zettelstore.de/z/domain/id" "zettelstore.de/z/kernel" "zettelstore.de/z/logger" "zettelstore.de/z/strfun" ) @@ -57,10 +58,11 @@ return port }), true, }, kernel.CoreProgname: {"Program name", nil, false}, + kernel.CoreStarted: {"Start time", nil, false}, kernel.CoreVerbose: {"Verbose output", parseBool, true}, kernel.CoreVersion: { "Version", cs.noFrozen(func(val string) interface{} { if val == "" { @@ -68,18 +70,20 @@ } return val }), false, }, + kernel.CoreVTime: {"Version time", nil, false}, } cs.next = interfaceMap{ kernel.CoreDebug: false, kernel.CoreGoArch: runtime.GOARCH, kernel.CoreGoOS: runtime.GOOS, kernel.CoreGoVersion: runtime.Version(), kernel.CoreHostname: "*unknown host*", kernel.CorePort: 0, + kernel.CoreStarted: time.Now().Local().Format(id.ZidLayout), kernel.CoreVerbose: false, } if hn, err := os.Hostname(); err == nil { cs.next[kernel.CoreHostname] = hn } @@ -141,11 +145,11 @@ func (cs *coreService) updateRecoverInfo(name string, recoverInfo interface{}, stack []byte) { cs.mxRecover.Lock() ri := cs.mapRecover[name] ri.count++ - ri.ts = time.Now() + ri.ts = time.Now().Local() ri.info = recoverInfo ri.stack = stack cs.mapRecover[name] = ri cs.mxRecover.Unlock() } Index: kernel/impl/impl.go ================================================================== --- kernel/impl/impl.go +++ kernel/impl/impl.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. //----------------------------------------------------------------------------- @@ -23,11 +23,13 @@ "runtime/pprof" "strconv" "strings" "sync" "syscall" + "time" + "zettelstore.de/z/domain/id" "zettelstore.de/z/kernel" "zettelstore.de/z/logger" ) // myKernel is the main internal kernel. @@ -113,10 +115,16 @@ kern.depStop[dep] = append(kern.depStop[dep], srv) } } return kern } + +func (kern *myKernel) Setup(progname, version string, versionTime time.Time) { + kern.SetConfig(kernel.CoreService, kernel.CoreProgname, progname) + kern.SetConfig(kernel.CoreService, kernel.CoreVersion, version) + kern.SetConfig(kernel.CoreService, kernel.CoreVTime, versionTime.Local().Format(id.ZidLayout)) +} func (kern *myKernel) Start(headline, lineServer bool) { for _, srvD := range kern.srvs { srvD.srv.Freeze() } @@ -209,10 +217,14 @@ } func (kern *myKernel) RetrieveLogEntries() []kernel.LogEntry { return kern.logWriter.retrieveLogEntries() } + +func (kern *myKernel) GetLastLogTime() time.Time { + return kern.logWriter.getLastLogTime() +} // LogRecover outputs some information about the previous panic. func (kern *myKernel) LogRecover(name string, recoverInfo interface{}) bool { return kern.doLogRecover(name, recoverInfo) } Index: kernel/impl/log.go ================================================================== --- kernel/impl/log.go +++ kernel/impl/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. //----------------------------------------------------------------------------- @@ -20,10 +20,11 @@ ) // kernelLogWriter adapts an io.Writer to a LogWriter type kernelLogWriter struct { mx sync.RWMutex // protects buf, serializes w.Write and retrieveLogEntries + lastLog time.Time buf []byte writePos int data []logEntry full bool } @@ -32,19 +33,21 @@ func newKernelLogWriter(capacity int) *kernelLogWriter { if capacity < 1 { capacity = 1 } return &kernelLogWriter{ - buf: make([]byte, 0, 500), - data: make([]logEntry, capacity), + lastLog: time.Now(), + buf: make([]byte, 0, 500), + data: make([]logEntry, capacity), } } func (klw *kernelLogWriter) WriteMessage(level logger.Level, ts time.Time, prefix, msg string, details []byte) error { klw.mx.Lock() if level > logger.DebugLevel { + klw.lastLog = ts klw.data[klw.writePos] = logEntry{ level: level, ts: ts, prefix: prefix, msg: msg, @@ -138,12 +141,18 @@ copyE2E(&result[pos], &klw.data[j]) pos++ } return result } + +func (klw *kernelLogWriter) getLastLogTime() time.Time { + klw.mx.RLock() + defer klw.mx.RUnlock() + return klw.lastLog +} func copyE2E(result *kernel.LogEntry, origin *logEntry) { result.Level = origin.level result.TS = origin.ts result.Prefix = origin.prefix result.Message = origin.msg + string(origin.details) } Index: kernel/impl/web.go ================================================================== --- kernel/impl/web.go +++ kernel/impl/web.go @@ -9,11 +9,15 @@ //----------------------------------------------------------------------------- package impl import ( + "errors" "net" + "net/url" + "os" + "path/filepath" "strconv" "strings" "sync" "time" @@ -31,10 +35,31 @@ } func (ws *webService) Initialize(logger *logger.Logger) { ws.logger = logger ws.descr = descriptionMap{ + kernel.WebAssetDir: { + "Asset file directory", + func(val string) any { + val = filepath.Clean(val) + if finfo, err := os.Stat(val); err == nil && finfo.IsDir() { + return val + } + return nil + }, + true, + }, + kernel.WebBaseURL: { + "Base URL", + func(val string) any { + if _, err := url.Parse(val); err != nil { + return nil + } + return val + }, + true, + }, kernel.WebListenAddress: { "Listen address", func(val string) interface{} { host, port, err := net.SplitHostPort(val) if err != nil { @@ -69,10 +94,12 @@ }, true, }, } ws.next = interfaceMap{ + kernel.WebAssetDir: "", + kernel.WebBaseURL: "http://127.0.0.1:23123/", kernel.WebListenAddress: "127.0.0.1:23123", kernel.WebMaxRequestSize: int64(16 * 1024 * 1024), kernel.WebPersistentCookie: false, kernel.WebSecureCookie: true, kernel.WebTokenLifetimeAPI: 1 * time.Hour, @@ -95,24 +122,33 @@ } return defDur } } +var errWrongBasePrefix = errors.New(kernel.WebURLPrefix + " does not match " + kernel.WebBaseURL) + func (ws *webService) GetLogger() *logger.Logger { return ws.logger } func (ws *webService) Start(kern *myKernel) error { + baseURL := ws.GetNextConfig(kernel.WebBaseURL).(string) listenAddr := ws.GetNextConfig(kernel.WebListenAddress).(string) urlPrefix := ws.GetNextConfig(kernel.WebURLPrefix).(string) persistentCookie := ws.GetNextConfig(kernel.WebPersistentCookie).(bool) secureCookie := ws.GetNextConfig(kernel.WebSecureCookie).(bool) maxRequestSize := ws.GetNextConfig(kernel.WebMaxRequestSize).(int64) if maxRequestSize < 1024 { maxRequestSize = 1024 } - srvw := impl.New(ws.logger, listenAddr, urlPrefix, persistentCookie, secureCookie, maxRequestSize, kern.auth.manager) - err := kern.web.setupServer(srvw, kern.box.manager, kern.auth.manager, kern.cfg.rtConfig) + if !strings.HasSuffix(baseURL, urlPrefix) { + ws.logger.Fatal().Str("base-url", baseURL).Str("url-prefix", urlPrefix).Msg( + "url-prefix is not a suffix of base-url") + return errWrongBasePrefix + } + + srvw := impl.New(ws.logger, listenAddr, baseURL, urlPrefix, persistentCookie, secureCookie, maxRequestSize, kern.auth.manager) + err := kern.web.setupServer(srvw, kern.box.manager, kern.auth.manager, &kern.cfg) if err != nil { ws.logger.Fatal().Err(err).Msg("Unable to create") return err } if kern.core.GetNextConfig(kernel.CoreDebug).(bool) { @@ -120,11 +156,11 @@ } if err = srvw.Run(); err != nil { ws.logger.Fatal().Err(err).Msg("Unable to start") return err } - ws.logger.Info().Str("listen", listenAddr).Msg("Start Service") + ws.logger.Info().Str("listen", listenAddr).Str("base-url", baseURL).Msg("Start Service") ws.mxService.Lock() ws.srvw = srvw ws.mxService.Unlock() if kern.cfg.GetConfig(kernel.ConfigSimpleMode).(bool) { Index: kernel/kernel.go ================================================================== --- kernel/kernel.go +++ kernel/kernel.go @@ -24,10 +24,14 @@ "zettelstore.de/z/web/server" ) // Kernel is the main internal service. type Kernel interface { + // Setup sets the most basic data of a software: its name, its version, + // and when the version was created. + Setup(progname, version string, versionTime time.Time) + // Start the service. Start(headline bool, lineServer bool) // WaitForShutdown blocks the call until Shutdown is called. WaitForShutdown() @@ -72,10 +76,13 @@ // SetLevel sets the logging level for the given service. SetLevel(Service, logger.Level) // RetrieveLogEntries returns all buffered log entries. RetrieveLogEntries() []LogEntry + + // GetLastLogTime returns the time when the last logging with level > DEBUG happened. + GetLastLogTime() time.Time // StartService start the given service. StartService(Service) error // RestartService stops and restarts the given service, while maintaining service dependencies. @@ -129,12 +136,14 @@ CoreGoOS = "go-os" CoreGoVersion = "go-version" CoreHostname = "hostname" CorePort = "port" CoreProgname = "progname" + CoreStarted = "started" CoreVerbose = "verbose" CoreVersion = "version" + CoreVTime = "vtime" ) // Defined values for core service. const ( CoreDefaultVersion = "unknown" @@ -163,10 +172,12 @@ BoxDirTypeSimple = "simple" ) // Constants for web service keys. const ( + WebAssetDir = "asset-dir" + WebBaseURL = "base-url" WebListenAddress = "listen" WebPersistentCookie = "persistent" WebMaxRequestSize = "max-request-size" WebSecureCookie = "secure" WebTokenLifetimeAPI = "api-lifetime" Index: logger/logger.go ================================================================== --- logger/logger.go +++ logger/logger.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. //----------------------------------------------------------------------------- @@ -226,7 +226,7 @@ uProvider: up, } } func (l *Logger) writeMessage(level Level, msg string, details []byte) error { - return l.topParent.lw.WriteMessage(level, time.Now(), l.prefix, msg, details) + return l.topParent.lw.WriteMessage(level, time.Now().Local(), l.prefix, msg, details) } Index: parser/parser.go ================================================================== --- parser/parser.go +++ parser/parser.go @@ -10,10 +10,11 @@ // Package parser provides a generic interface to a range of different parsers. package parser import ( + "context" "fmt" "zettelstore.de/c/api" "zettelstore.de/z/ast" "zettelstore.de/z/config" @@ -117,15 +118,15 @@ cleaner.CleanInlineLinks(&in) return in } // ParseZettel parses the zettel based on the syntax. -func ParseZettel(zettel domain.Zettel, syntax string, rtConfig config.Config) *ast.ZettelNode { +func ParseZettel(ctx context.Context, zettel domain.Zettel, syntax string, rtConfig config.Config) *ast.ZettelNode { m := zettel.Meta inhMeta := m if rtConfig != nil { - inhMeta = rtConfig.AddDefaultValues(inhMeta) + inhMeta = rtConfig.AddDefaultValues(ctx, inhMeta) } if syntax == "" { syntax, _ = inhMeta.Get(api.KeySyntax) } parseMeta := inhMeta ADDED parser/pikchr/internal/ORIG_LICENSE Index: parser/pikchr/internal/ORIG_LICENSE ================================================================== --- parser/pikchr/internal/ORIG_LICENSE +++ parser/pikchr/internal/ORIG_LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2022 gopikchr + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. ADDED parser/pikchr/internal/README.txt Index: parser/pikchr/internal/README.txt ================================================================== --- parser/pikchr/internal/README.txt +++ parser/pikchr/internal/README.txt @@ -0,0 +1,21 @@ +This is a fork of gopikchr/gopikchr, adapted to the needs of Zettelstore. + +File gopikchr.go is generated by gopikchr.y +You should not modify gopikchr.go, only gopikchr.Y + +To generate gopikchr.go you have to install gopikchr/golemon first: + + go install github.com/gopikchr/golemon@latest + +Invoke golemon: + + golemon gopikchr.y + +This will produce the files gopikchr.go and gopikchr.out +You can safely remove gopikchr.out + +You probably should reformat the generated go file: + + gofmt -w gopikchr.go + +In the future, golemon might be incorporated too, to make generation easier and more self-hosted. ADDED parser/pikchr/internal/pikchr.go Index: parser/pikchr/internal/pikchr.go ================================================================== --- parser/pikchr/internal/pikchr.go +++ parser/pikchr/internal/pikchr.go @@ -0,0 +1,8465 @@ +/* This file is automatically generated by Lemon from input grammar +** source file "pikchr.y". */ +//lint:file-ignore *,U1000 Ignore all unused code, it's generated + +/* +** Zero-Clause BSD license: +** +** Copyright (C) 2020-09-01 by D. Richard Hipp +** +** Permission to use, copy, modify, and/or distribute this software for +** any purpose with or without fee is hereby granted. +** +**************************************************************************** +** +** This software translates a PIC-inspired diagram language into SVG. +** +** PIKCHR (pronounced like "picture") is *mostly* backwards compatible +** with legacy PIC, though some features of legacy PIC are removed +** (for example, the "sh" command is removed for security) and +** many enhancements are added. +** +** PIKCHR is designed for use in an internet facing web environment. +** In particular, PIKCHR is designed to safely generate benign SVG from +** source text that provided by a hostile agent. +** +** This code was originally written by D. Richard Hipp using documentation +** from prior PIC implementations but without reference to prior code. +** All of the code in this project is original. +** +** This file implements a C-language subroutine that accepts a string +** of PIKCHR language text and generates a second string of SVG output that +** renders the drawing defined by the input. Space to hold the returned +** string is obtained from malloc() and should be freed by the caller. +** NULL might be returned if there is a memory allocation error. +** +** If there are errors in the PIKCHR input, the output will consist of an +** error message and the original PIKCHR input text (inside of
    ...
    ). +** +** 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 */ + wSVG int /* Width and height of the */ + hSVG int + fgcolor int /* foreground color value, or -1 for none */ + bgcolor int /* background color value, or -1 for none */ + + /* Paths for lines are constructed here first, then transferred into + ** the PObj object at the end: */ + nTPath int /* Number of entries on aTPath[] */ + mTPath int /* For last entry, 1: x set, 2: y set */ + aTPath [1000]PPoint /* Path under construction */ + + /* Error contexts */ + nCtx int /* Number of error contexts */ + aCtx [10]PToken /* Nested error contexts */ + + svgWidth, svgHeight string // Explicit width/height, if not given by scale. + svgFontScale PNum +} + +/* Include PIKCHR_PLAINTEXT_ERRORS among the bits of mFlags on the 3rd +** argument to pikchr() in order to cause error message text to come out +** as text/plain instead of as text/html + */ +const PIKCHR_PLAINTEXT_ERRORS = 0x0001 + +/* Include PIKCHR_DARK_MODE among the mFlag bits to invert colors. + */ +const PIKCHR_DARK_MODE = 0x0002 + +/* +** The behavior of an object class is defined by an instance of +** this structure. This is the "virtual method" table. + */ +type PClass struct { + zName string /* Name of class */ + isLine bool /* True if a line class */ + eJust int8 /* Use box-style text justification */ + + xInit func(*Pik, *PObj) /* Initializer */ + xNumProp func(*Pik, *PObj, *PToken) /* Value change notification */ + xCheck func(*Pik, *PObj) /* Checks to do after parsing */ + xChop func(*Pik, *PObj, *PPoint) PPoint /* Chopper */ + xOffset func(*Pik, *PObj, uint8) PPoint /* Offset from .c to edge point */ + xFit func(pik *Pik, pobj *PObj, w PNum, h PNum) /* Size to fit text */ + xRender func(*Pik, *PObj) /* Render */ +} + +func yytestcase(condition bool) {} + +//line 475 "pikchr.go" + +/**************** End of %include directives **********************************/ +/* These constants specify the various numeric values for terminal symbols. +***************** Begin token definitions *************************************/ + +const ( + T_ID = 1 + T_EDGEPT = 2 + T_OF = 3 + T_PLUS = 4 + T_MINUS = 5 + T_STAR = 6 + T_SLASH = 7 + T_PERCENT = 8 + T_UMINUS = 9 + T_EOL = 10 + T_ASSIGN = 11 + T_PLACENAME = 12 + T_COLON = 13 + T_ASSERT = 14 + T_LP = 15 + T_EQ = 16 + T_RP = 17 + T_DEFINE = 18 + T_CODEBLOCK = 19 + T_FILL = 20 + T_COLOR = 21 + T_THICKNESS = 22 + T_PRINT = 23 + T_STRING = 24 + T_COMMA = 25 + T_CLASSNAME = 26 + T_LB = 27 + T_RB = 28 + T_UP = 29 + T_DOWN = 30 + T_LEFT = 31 + T_RIGHT = 32 + T_CLOSE = 33 + T_CHOP = 34 + T_FROM = 35 + T_TO = 36 + T_THEN = 37 + T_HEADING = 38 + T_GO = 39 + T_AT = 40 + T_WITH = 41 + T_SAME = 42 + T_AS = 43 + T_FIT = 44 + T_BEHIND = 45 + T_UNTIL = 46 + T_EVEN = 47 + T_DOT_E = 48 + T_HEIGHT = 49 + T_WIDTH = 50 + T_RADIUS = 51 + T_DIAMETER = 52 + T_DOTTED = 53 + T_DASHED = 54 + T_CW = 55 + T_CCW = 56 + T_LARROW = 57 + T_RARROW = 58 + T_LRARROW = 59 + T_INVIS = 60 + T_THICK = 61 + T_THIN = 62 + T_SOLID = 63 + T_CENTER = 64 + T_LJUST = 65 + T_RJUST = 66 + T_ABOVE = 67 + T_BELOW = 68 + T_ITALIC = 69 + T_BOLD = 70 + T_ALIGNED = 71 + T_BIG = 72 + T_SMALL = 73 + T_AND = 74 + T_LT = 75 + T_GT = 76 + T_ON = 77 + T_WAY = 78 + T_BETWEEN = 79 + T_THE = 80 + T_NTH = 81 + T_VERTEX = 82 + T_TOP = 83 + T_BOTTOM = 84 + T_START = 85 + T_END = 86 + T_IN = 87 + T_THIS = 88 + T_DOT_U = 89 + T_LAST = 90 + T_NUMBER = 91 + T_FUNC1 = 92 + T_FUNC2 = 93 + T_DIST = 94 + T_DOT_XY = 95 + T_X = 96 + T_Y = 97 + T_DOT_L = 98 +) + +/**************** End token definitions ***************************************/ + +/* The next sections is a series of control #defines. +** various aspects of the generated parser. +** YYCODETYPE is the data type used to store the integer codes +** that represent terminal and non-terminal symbols. +** "unsigned char" is used if there are fewer than +** 256 symbols. Larger types otherwise. +** YYNOCODE is a number of type YYCODETYPE that is not used for +** any terminal or nonterminal symbol. +** YYFALLBACK If defined, this indicates that one or more tokens +** (also known as: "terminal symbols") have fall-back +** values which should be used if the original symbol +** would not parse. This permits keywords to sometimes +** be used as identifiers, for example. +** YYACTIONTYPE is the data type used for "action codes" - numbers +** that indicate what to do in response to the next +** token. +** pik_parserTOKENTYPE is the data type used for minor type for terminal +** symbols. Background: A "minor type" is a semantic +** value associated with a terminal or non-terminal +** symbols. For example, for an "ID" terminal symbol, +** the minor type might be the name of the identifier. +** Each non-terminal can have a different minor type. +** Terminal symbols all have the same minor type, though. +** This macros defines the minor type for terminal +** symbols. +** YYMINORTYPE is the data type used for all minor types. +** This is typically a union of many types, one of +** which is pik_parserTOKENTYPE. The entry in the union +** for terminal symbols is called "yy0". +** YYSTACKDEPTH is the maximum depth of the parser's stack. If +** zero the stack is dynamically sized using realloc() +** pik_parserARG_SDECL A static variable declaration for the %extra_argument +** pik_parserARG_PDECL A parameter declaration for the %extra_argument +** pik_parserARG_PARAM Code to pass %extra_argument as a subroutine parameter +** pik_parserARG_STORE Code to store %extra_argument into yypParser +** pik_parserARG_FETCH Code to extract %extra_argument from yypParser +** pik_parserCTX_* As pik_parserARG_ except for %extra_context +** YYERRORSYMBOL is the code number of the error symbol. If not +** defined, then do no error processing. +** YYNSTATE the combined number of states. +** YYNRULE the number of rules in the grammar +** YYNTOKEN Number of terminal symbols +** YY_MAX_SHIFT Maximum value for shift actions +** YY_MIN_SHIFTREDUCE Minimum value for shift-reduce actions +** YY_MAX_SHIFTREDUCE Maximum value for shift-reduce actions +** YY_ERROR_ACTION The yy_action[] code for syntax error +** YY_ACCEPT_ACTION The yy_action[] code for accept +** YY_NO_ACTION The yy_action[] code for no-op +** YY_MIN_REDUCE Minimum value for reduce actions +** YY_MAX_REDUCE Maximum value for reduce actions + */ +/************* Begin control #defines *****************************************/ +const YYNOCODE = 135 + +type YYCODETYPE = uint8 +type YYACTIONTYPE = uint16 +type pik_parserTOKENTYPE = PToken +type YYMINORTYPE struct { + yyinit int + yy0 pik_parserTOKENTYPE + yy10 PRel + yy79 PPoint + yy104 *PObj + yy112 int + yy153 PNum + yy186 []*PObj +} + +const YYWILDCARD = 0 +const YYSTACKDEPTH = 100 +const YYNOERRORRECOVERY = false +const YYCOVERAGE = false +const YYTRACKMAXSTACKDEPTH = false +const NDEBUG = false +const YYERRORSYMBOL = 0 +const YYFALLBACK = true +const YYNSTATE = 164 +const YYNRULE = 156 +const YYNRULE_WITH_ACTION = 116 +const YYNTOKEN = 99 +const YY_MAX_SHIFT = 163 +const YY_MIN_SHIFTREDUCE = 287 +const YY_MAX_SHIFTREDUCE = 442 +const YY_ERROR_ACTION = 443 +const YY_ACCEPT_ACTION = 444 +const YY_NO_ACTION = 445 +const YY_MIN_REDUCE = 446 +const YY_MAX_REDUCE = 601 + +/************* End control #defines *******************************************/ + +/* Applications can choose to define yytestcase() in the %include section +** to a macro that can assist in verifying code coverage. For production +** code the yytestcase() macro should be turned off. But it is useful +** for testing. + */ + +/* Next are the tables used to determine what action to take based on the +** current state and lookahead token. These tables are used to implement +** functions that take a state number and lookahead value and return an +** action integer. +** +** Suppose the action integer is N. Then the action is determined as +** follows +** +** 0 <= N <= YY_MAX_SHIFT Shift N. That is, push the lookahead +** token onto the stack and goto state N. +** +** N between YY_MIN_SHIFTREDUCE Shift to an arbitrary state then +** and YY_MAX_SHIFTREDUCE reduce by rule N-YY_MIN_SHIFTREDUCE. +** +** N == YY_ERROR_ACTION A syntax error has occurred. +** +** N == YY_ACCEPT_ACTION The parser accepts its input. +** +** N == YY_NO_ACTION No such action. Denotes unused +** slots in the yy_action[] table. +** +** N between YY_MIN_REDUCE Reduce by rule N-YY_MIN_REDUCE +** and YY_MAX_REDUCE +** +** The action table is constructed as a single large table named yy_action[]. +** Given state S and lookahead X, the action is computed as either: +** +** (A) N = yy_action[ yy_shift_ofst[S] + X ] +** (B) N = yy_default[S] +** +** The (A) formula is preferred. The B formula is used instead if +** yy_lookahead[yy_shift_ofst[S]+X] is not equal to X. +** +** The formulas above are for computing the action when the lookahead is +** a terminal symbol. If the lookahead is a non-terminal (as occurs after +** a reduce action) then the yy_reduce_ofst[] array is used in place of +** the yy_shift_ofst[] array. +** +** The following are the tables generated in this section: +** +** yy_action[] A single table containing all actions. +** yy_lookahead[] A table containing the lookahead for each entry in +** yy_action. Used to detect hash collisions. +** yy_shift_ofst[] For each state, the offset into yy_action for +** shifting terminals. +** yy_reduce_ofst[] For each state, the offset into yy_action for +** shifting non-terminals after a reduce. +** yy_default[] Default action for each state. +** +*********** Begin parsing tables **********************************************/ +const YY_ACTTAB_COUNT = 1303 + +var yy_action = []YYACTIONTYPE{ + /* 0 */ 575, 495, 161, 119, 25, 452, 29, 74, 129, 148, + /* 10 */ 575, 492, 161, 119, 453, 113, 120, 161, 119, 530, + /* 20 */ 427, 428, 339, 559, 81, 30, 560, 561, 575, 64, + /* 30 */ 63, 62, 61, 322, 323, 9, 8, 33, 149, 32, + /* 40 */ 7, 71, 127, 38, 335, 66, 48, 37, 28, 339, + /* 50 */ 339, 339, 339, 425, 426, 340, 341, 342, 343, 344, + /* 60 */ 345, 346, 347, 348, 474, 528, 161, 119, 577, 77, + /* 70 */ 577, 73, 376, 148, 474, 533, 161, 119, 112, 113, + /* 80 */ 120, 161, 119, 128, 427, 428, 339, 357, 81, 531, + /* 90 */ 161, 119, 474, 36, 330, 13, 306, 322, 323, 9, + /* 100 */ 8, 33, 149, 32, 7, 71, 127, 328, 335, 66, + /* 110 */ 579, 310, 31, 339, 339, 339, 339, 425, 426, 340, + /* 120 */ 341, 342, 343, 344, 345, 346, 347, 348, 394, 435, + /* 130 */ 46, 59, 60, 64, 63, 62, 61, 54, 51, 376, + /* 140 */ 69, 108, 2, 47, 403, 83, 297, 435, 375, 84, + /* 150 */ 117, 80, 35, 308, 79, 133, 122, 126, 441, 440, + /* 160 */ 299, 123, 3, 404, 405, 406, 408, 80, 298, 308, + /* 170 */ 79, 4, 411, 412, 413, 414, 441, 440, 350, 350, + /* 180 */ 350, 350, 350, 350, 350, 350, 350, 350, 62, 61, + /* 190 */ 67, 434, 1, 75, 378, 158, 74, 76, 148, 411, + /* 200 */ 412, 413, 414, 124, 113, 120, 161, 119, 106, 434, + /* 210 */ 436, 437, 438, 439, 5, 375, 6, 117, 393, 155, + /* 220 */ 154, 153, 394, 435, 69, 59, 60, 149, 436, 437, + /* 230 */ 438, 439, 535, 376, 398, 399, 2, 424, 427, 428, + /* 240 */ 339, 156, 156, 156, 423, 394, 435, 65, 59, 60, + /* 250 */ 162, 131, 441, 440, 397, 72, 376, 148, 118, 2, + /* 260 */ 380, 157, 125, 113, 120, 161, 119, 339, 339, 339, + /* 270 */ 339, 425, 426, 535, 11, 441, 440, 394, 356, 535, + /* 280 */ 59, 60, 535, 379, 159, 434, 149, 12, 102, 446, + /* 290 */ 432, 42, 138, 14, 435, 139, 301, 302, 303, 36, + /* 300 */ 305, 430, 106, 16, 436, 437, 438, 439, 434, 375, + /* 310 */ 18, 117, 393, 155, 154, 153, 44, 142, 140, 64, + /* 320 */ 63, 62, 61, 441, 440, 106, 19, 436, 437, 438, + /* 330 */ 439, 45, 375, 20, 117, 393, 155, 154, 153, 68, + /* 340 */ 55, 114, 64, 63, 62, 61, 147, 146, 394, 473, + /* 350 */ 359, 59, 60, 43, 23, 391, 434, 106, 26, 376, + /* 360 */ 57, 58, 42, 49, 375, 392, 117, 393, 155, 154, + /* 370 */ 153, 64, 63, 62, 61, 436, 437, 438, 439, 384, + /* 380 */ 382, 383, 22, 21, 377, 473, 160, 70, 39, 445, + /* 390 */ 24, 445, 145, 141, 431, 142, 140, 64, 63, 62, + /* 400 */ 61, 394, 15, 445, 59, 60, 64, 63, 62, 61, + /* 410 */ 391, 445, 376, 445, 445, 42, 445, 445, 55, 391, + /* 420 */ 156, 156, 156, 445, 147, 146, 445, 52, 106, 445, + /* 430 */ 445, 43, 445, 445, 445, 375, 445, 117, 393, 155, + /* 440 */ 154, 153, 445, 394, 143, 445, 59, 60, 64, 63, + /* 450 */ 62, 61, 313, 445, 376, 378, 158, 42, 445, 445, + /* 460 */ 22, 21, 121, 447, 454, 29, 445, 445, 24, 450, + /* 470 */ 145, 141, 431, 142, 140, 64, 63, 62, 61, 445, + /* 480 */ 163, 106, 445, 445, 444, 27, 445, 445, 375, 445, + /* 490 */ 117, 393, 155, 154, 153, 445, 55, 74, 445, 148, + /* 500 */ 445, 445, 147, 146, 497, 113, 120, 161, 119, 43, + /* 510 */ 445, 394, 445, 445, 59, 60, 445, 445, 445, 118, + /* 520 */ 445, 445, 376, 106, 445, 42, 445, 445, 149, 445, + /* 530 */ 375, 445, 117, 393, 155, 154, 153, 445, 22, 21, + /* 540 */ 394, 144, 445, 59, 60, 445, 24, 445, 145, 141, + /* 550 */ 431, 376, 445, 445, 42, 445, 132, 130, 394, 445, + /* 560 */ 445, 59, 60, 109, 447, 454, 29, 445, 445, 376, + /* 570 */ 450, 445, 42, 445, 394, 445, 445, 59, 60, 445, + /* 580 */ 445, 163, 445, 445, 445, 102, 27, 445, 42, 445, + /* 590 */ 445, 106, 445, 64, 63, 62, 61, 445, 375, 445, + /* 600 */ 117, 393, 155, 154, 153, 394, 355, 445, 59, 60, + /* 610 */ 445, 445, 445, 445, 445, 74, 376, 148, 445, 40, + /* 620 */ 106, 445, 496, 113, 120, 161, 119, 375, 445, 117, + /* 630 */ 393, 155, 154, 153, 445, 448, 454, 29, 106, 445, + /* 640 */ 445, 450, 445, 445, 445, 375, 149, 117, 393, 155, + /* 650 */ 154, 153, 163, 445, 106, 445, 445, 27, 445, 445, + /* 660 */ 445, 375, 445, 117, 393, 155, 154, 153, 394, 445, + /* 670 */ 445, 59, 60, 64, 63, 62, 61, 445, 445, 376, + /* 680 */ 445, 445, 41, 445, 445, 106, 354, 64, 63, 62, + /* 690 */ 61, 445, 375, 445, 117, 393, 155, 154, 153, 445, + /* 700 */ 445, 445, 74, 445, 148, 445, 88, 445, 445, 490, + /* 710 */ 113, 120, 161, 119, 445, 120, 161, 119, 17, 74, + /* 720 */ 445, 148, 110, 110, 445, 445, 484, 113, 120, 161, + /* 730 */ 119, 445, 445, 149, 74, 445, 148, 152, 445, 445, + /* 740 */ 445, 483, 113, 120, 161, 119, 445, 445, 106, 445, + /* 750 */ 149, 445, 445, 107, 445, 375, 445, 117, 393, 155, + /* 760 */ 154, 153, 120, 161, 119, 149, 478, 74, 445, 148, + /* 770 */ 445, 88, 445, 445, 480, 113, 120, 161, 119, 445, + /* 780 */ 120, 161, 119, 74, 152, 148, 10, 479, 479, 445, + /* 790 */ 134, 113, 120, 161, 119, 445, 445, 445, 149, 74, + /* 800 */ 445, 148, 152, 445, 445, 445, 517, 113, 120, 161, + /* 810 */ 119, 445, 445, 74, 149, 148, 445, 445, 445, 445, + /* 820 */ 137, 113, 120, 161, 119, 74, 445, 148, 445, 445, + /* 830 */ 149, 445, 525, 113, 120, 161, 119, 445, 74, 445, + /* 840 */ 148, 445, 445, 445, 149, 527, 113, 120, 161, 119, + /* 850 */ 445, 445, 74, 445, 148, 445, 149, 445, 445, 524, + /* 860 */ 113, 120, 161, 119, 74, 445, 148, 445, 445, 149, + /* 870 */ 445, 526, 113, 120, 161, 119, 445, 445, 74, 445, + /* 880 */ 148, 445, 88, 149, 445, 523, 113, 120, 161, 119, + /* 890 */ 445, 120, 161, 119, 74, 149, 148, 85, 111, 111, + /* 900 */ 445, 522, 113, 120, 161, 119, 120, 161, 119, 149, + /* 910 */ 74, 445, 148, 152, 445, 445, 445, 521, 113, 120, + /* 920 */ 161, 119, 445, 445, 74, 149, 148, 445, 152, 445, + /* 930 */ 445, 520, 113, 120, 161, 119, 74, 445, 148, 445, + /* 940 */ 445, 149, 445, 519, 113, 120, 161, 119, 445, 74, + /* 950 */ 445, 148, 445, 445, 445, 149, 150, 113, 120, 161, + /* 960 */ 119, 445, 445, 74, 445, 148, 445, 149, 445, 445, + /* 970 */ 151, 113, 120, 161, 119, 74, 445, 148, 445, 445, + /* 980 */ 149, 445, 136, 113, 120, 161, 119, 445, 445, 74, + /* 990 */ 445, 148, 107, 445, 149, 445, 135, 113, 120, 161, + /* 1000 */ 119, 120, 161, 119, 445, 463, 149, 445, 88, 445, + /* 1010 */ 445, 445, 78, 78, 445, 445, 107, 120, 161, 119, + /* 1020 */ 149, 445, 445, 152, 82, 120, 161, 119, 445, 463, + /* 1030 */ 445, 466, 86, 34, 445, 88, 445, 569, 445, 152, + /* 1040 */ 445, 120, 161, 119, 120, 161, 119, 152, 107, 445, + /* 1050 */ 445, 475, 64, 63, 62, 61, 445, 120, 161, 119, + /* 1060 */ 98, 451, 445, 152, 89, 396, 152, 90, 445, 120, + /* 1070 */ 161, 119, 445, 120, 161, 119, 120, 161, 119, 152, + /* 1080 */ 445, 64, 63, 62, 61, 445, 445, 445, 445, 445, + /* 1090 */ 87, 152, 445, 99, 395, 152, 100, 445, 152, 120, + /* 1100 */ 161, 119, 120, 161, 119, 120, 161, 119, 445, 101, + /* 1110 */ 64, 63, 62, 61, 445, 445, 445, 445, 120, 161, + /* 1120 */ 119, 152, 91, 391, 152, 445, 445, 152, 103, 445, + /* 1130 */ 445, 120, 161, 119, 445, 92, 445, 120, 161, 119, + /* 1140 */ 152, 93, 445, 445, 120, 161, 119, 104, 445, 445, + /* 1150 */ 120, 161, 119, 152, 445, 445, 120, 161, 119, 152, + /* 1160 */ 445, 445, 445, 445, 94, 445, 152, 445, 445, 445, + /* 1170 */ 105, 445, 152, 120, 161, 119, 445, 95, 152, 120, + /* 1180 */ 161, 119, 96, 445, 445, 445, 120, 161, 119, 445, + /* 1190 */ 445, 120, 161, 119, 97, 152, 445, 445, 445, 445, + /* 1200 */ 549, 152, 445, 120, 161, 119, 548, 445, 152, 120, + /* 1210 */ 161, 119, 445, 152, 445, 120, 161, 119, 445, 445, + /* 1220 */ 445, 445, 445, 547, 445, 152, 445, 445, 445, 445, + /* 1230 */ 445, 152, 120, 161, 119, 546, 445, 152, 445, 115, + /* 1240 */ 445, 445, 116, 445, 120, 161, 119, 445, 120, 161, + /* 1250 */ 119, 120, 161, 119, 152, 64, 63, 62, 61, 64, + /* 1260 */ 63, 62, 61, 445, 445, 445, 152, 445, 445, 445, + /* 1270 */ 152, 445, 445, 152, 445, 445, 50, 445, 445, 445, + /* 1280 */ 53, 64, 63, 62, 61, 445, 445, 445, 445, 445, + /* 1290 */ 445, 445, 445, 445, 445, 445, 445, 445, 445, 445, + /* 1300 */ 445, 445, 56, +} +var yy_lookahead = []YYCODETYPE{ + /* 0 */ 0, 112, 113, 114, 133, 101, 102, 103, 105, 105, + /* 10 */ 10, 112, 113, 114, 110, 111, 112, 113, 114, 105, + /* 20 */ 20, 21, 22, 104, 24, 125, 107, 108, 28, 4, + /* 30 */ 5, 6, 7, 33, 34, 35, 36, 37, 134, 39, + /* 40 */ 40, 41, 42, 104, 44, 45, 107, 108, 106, 49, + /* 50 */ 50, 51, 52, 53, 54, 55, 56, 57, 58, 59, + /* 60 */ 60, 61, 62, 63, 0, 112, 113, 114, 129, 130, + /* 70 */ 131, 103, 12, 105, 10, 112, 113, 114, 110, 111, + /* 80 */ 112, 113, 114, 105, 20, 21, 22, 17, 24, 112, + /* 90 */ 113, 114, 28, 10, 2, 25, 25, 33, 34, 35, + /* 100 */ 36, 37, 134, 39, 40, 41, 42, 2, 44, 45, + /* 110 */ 132, 28, 127, 49, 50, 51, 52, 53, 54, 55, + /* 120 */ 56, 57, 58, 59, 60, 61, 62, 63, 1, 2, + /* 130 */ 38, 4, 5, 4, 5, 6, 7, 4, 5, 12, + /* 140 */ 3, 81, 15, 38, 1, 115, 17, 2, 88, 115, + /* 150 */ 90, 24, 128, 26, 27, 12, 1, 14, 31, 32, + /* 160 */ 19, 18, 16, 20, 21, 22, 23, 24, 17, 26, + /* 170 */ 27, 15, 29, 30, 31, 32, 31, 32, 64, 65, + /* 180 */ 66, 67, 68, 69, 70, 71, 72, 73, 6, 7, + /* 190 */ 43, 64, 13, 48, 26, 27, 103, 48, 105, 29, + /* 200 */ 30, 31, 32, 110, 111, 112, 113, 114, 81, 64, + /* 210 */ 83, 84, 85, 86, 40, 88, 40, 90, 91, 92, + /* 220 */ 93, 94, 1, 2, 87, 4, 5, 134, 83, 84, + /* 230 */ 85, 86, 48, 12, 96, 97, 15, 41, 20, 21, + /* 240 */ 22, 20, 21, 22, 41, 1, 2, 98, 4, 5, + /* 250 */ 82, 47, 31, 32, 17, 103, 12, 105, 90, 15, + /* 260 */ 26, 27, 110, 111, 112, 113, 114, 49, 50, 51, + /* 270 */ 52, 53, 54, 89, 25, 31, 32, 1, 17, 95, + /* 280 */ 4, 5, 98, 26, 27, 64, 134, 74, 12, 0, + /* 290 */ 79, 15, 78, 3, 2, 80, 20, 21, 22, 10, + /* 300 */ 24, 79, 81, 3, 83, 84, 85, 86, 64, 88, + /* 310 */ 3, 90, 91, 92, 93, 94, 38, 2, 3, 4, + /* 320 */ 5, 6, 7, 31, 32, 81, 3, 83, 84, 85, + /* 330 */ 86, 16, 88, 3, 90, 91, 92, 93, 94, 3, + /* 340 */ 25, 95, 4, 5, 6, 7, 31, 32, 1, 2, + /* 350 */ 76, 4, 5, 38, 25, 17, 64, 81, 15, 12, + /* 360 */ 15, 15, 15, 25, 88, 17, 90, 91, 92, 93, + /* 370 */ 94, 4, 5, 6, 7, 83, 84, 85, 86, 28, + /* 380 */ 28, 28, 67, 68, 12, 38, 89, 3, 11, 135, + /* 390 */ 75, 135, 77, 78, 79, 2, 3, 4, 5, 6, + /* 400 */ 7, 1, 35, 135, 4, 5, 4, 5, 6, 7, + /* 410 */ 17, 135, 12, 135, 135, 15, 135, 135, 25, 17, + /* 420 */ 20, 21, 22, 135, 31, 32, 135, 25, 81, 135, + /* 430 */ 135, 38, 135, 135, 135, 88, 135, 90, 91, 92, + /* 440 */ 93, 94, 135, 1, 2, 135, 4, 5, 4, 5, + /* 450 */ 6, 7, 8, 135, 12, 26, 27, 15, 135, 135, + /* 460 */ 67, 68, 99, 100, 101, 102, 135, 135, 75, 106, + /* 470 */ 77, 78, 79, 2, 3, 4, 5, 6, 7, 135, + /* 480 */ 117, 81, 135, 135, 121, 122, 135, 135, 88, 135, + /* 490 */ 90, 91, 92, 93, 94, 135, 25, 103, 135, 105, + /* 500 */ 135, 135, 31, 32, 110, 111, 112, 113, 114, 38, + /* 510 */ 135, 1, 135, 135, 4, 5, 135, 135, 135, 90, + /* 520 */ 135, 135, 12, 81, 135, 15, 135, 135, 134, 135, + /* 530 */ 88, 135, 90, 91, 92, 93, 94, 135, 67, 68, + /* 540 */ 1, 2, 135, 4, 5, 135, 75, 135, 77, 78, + /* 550 */ 79, 12, 135, 135, 15, 135, 46, 47, 1, 135, + /* 560 */ 135, 4, 5, 99, 100, 101, 102, 135, 135, 12, + /* 570 */ 106, 135, 15, 135, 1, 135, 135, 4, 5, 135, + /* 580 */ 135, 117, 135, 135, 135, 12, 122, 135, 15, 135, + /* 590 */ 135, 81, 135, 4, 5, 6, 7, 135, 88, 135, + /* 600 */ 90, 91, 92, 93, 94, 1, 17, 135, 4, 5, + /* 610 */ 135, 135, 135, 135, 135, 103, 12, 105, 135, 15, + /* 620 */ 81, 135, 110, 111, 112, 113, 114, 88, 135, 90, + /* 630 */ 91, 92, 93, 94, 135, 100, 101, 102, 81, 135, + /* 640 */ 135, 106, 135, 135, 135, 88, 134, 90, 91, 92, + /* 650 */ 93, 94, 117, 135, 81, 135, 135, 122, 135, 135, + /* 660 */ 135, 88, 135, 90, 91, 92, 93, 94, 1, 135, + /* 670 */ 135, 4, 5, 4, 5, 6, 7, 135, 135, 12, + /* 680 */ 135, 135, 15, 135, 135, 81, 17, 4, 5, 6, + /* 690 */ 7, 135, 88, 135, 90, 91, 92, 93, 94, 135, + /* 700 */ 135, 135, 103, 135, 105, 135, 103, 135, 135, 110, + /* 710 */ 111, 112, 113, 114, 135, 112, 113, 114, 35, 103, + /* 720 */ 135, 105, 119, 120, 135, 135, 110, 111, 112, 113, + /* 730 */ 114, 135, 135, 134, 103, 135, 105, 134, 135, 135, + /* 740 */ 135, 110, 111, 112, 113, 114, 135, 135, 81, 135, + /* 750 */ 134, 135, 135, 103, 135, 88, 135, 90, 91, 92, + /* 760 */ 93, 94, 112, 113, 114, 134, 116, 103, 135, 105, + /* 770 */ 135, 103, 135, 135, 110, 111, 112, 113, 114, 135, + /* 780 */ 112, 113, 114, 103, 134, 105, 118, 119, 120, 135, + /* 790 */ 110, 111, 112, 113, 114, 135, 135, 135, 134, 103, + /* 800 */ 135, 105, 134, 135, 135, 135, 110, 111, 112, 113, + /* 810 */ 114, 135, 135, 103, 134, 105, 135, 135, 135, 135, + /* 820 */ 110, 111, 112, 113, 114, 103, 135, 105, 135, 135, + /* 830 */ 134, 135, 110, 111, 112, 113, 114, 135, 103, 135, + /* 840 */ 105, 135, 135, 135, 134, 110, 111, 112, 113, 114, + /* 850 */ 135, 135, 103, 135, 105, 135, 134, 135, 135, 110, + /* 860 */ 111, 112, 113, 114, 103, 135, 105, 135, 135, 134, + /* 870 */ 135, 110, 111, 112, 113, 114, 135, 135, 103, 135, + /* 880 */ 105, 135, 103, 134, 135, 110, 111, 112, 113, 114, + /* 890 */ 135, 112, 113, 114, 103, 134, 105, 103, 119, 120, + /* 900 */ 135, 110, 111, 112, 113, 114, 112, 113, 114, 134, + /* 910 */ 103, 135, 105, 134, 135, 135, 135, 110, 111, 112, + /* 920 */ 113, 114, 135, 135, 103, 134, 105, 135, 134, 135, + /* 930 */ 135, 110, 111, 112, 113, 114, 103, 135, 105, 135, + /* 940 */ 135, 134, 135, 110, 111, 112, 113, 114, 135, 103, + /* 950 */ 135, 105, 135, 135, 135, 134, 110, 111, 112, 113, + /* 960 */ 114, 135, 135, 103, 135, 105, 135, 134, 135, 135, + /* 970 */ 110, 111, 112, 113, 114, 103, 135, 105, 135, 135, + /* 980 */ 134, 135, 110, 111, 112, 113, 114, 135, 135, 103, + /* 990 */ 135, 105, 103, 135, 134, 135, 110, 111, 112, 113, + /* 1000 */ 114, 112, 113, 114, 135, 116, 134, 135, 103, 135, + /* 1010 */ 135, 135, 123, 124, 135, 135, 103, 112, 113, 114, + /* 1020 */ 134, 135, 135, 134, 119, 112, 113, 114, 135, 116, + /* 1030 */ 135, 126, 103, 128, 135, 103, 135, 124, 135, 134, + /* 1040 */ 135, 112, 113, 114, 112, 113, 114, 134, 103, 135, + /* 1050 */ 135, 119, 4, 5, 6, 7, 135, 112, 113, 114, + /* 1060 */ 103, 116, 135, 134, 103, 17, 134, 103, 135, 112, + /* 1070 */ 113, 114, 135, 112, 113, 114, 112, 113, 114, 134, + /* 1080 */ 135, 4, 5, 6, 7, 135, 135, 135, 135, 135, + /* 1090 */ 103, 134, 135, 103, 17, 134, 103, 135, 134, 112, + /* 1100 */ 113, 114, 112, 113, 114, 112, 113, 114, 135, 103, + /* 1110 */ 4, 5, 6, 7, 135, 135, 135, 135, 112, 113, + /* 1120 */ 114, 134, 103, 17, 134, 135, 135, 134, 103, 135, + /* 1130 */ 135, 112, 113, 114, 135, 103, 135, 112, 113, 114, + /* 1140 */ 134, 103, 135, 135, 112, 113, 114, 103, 135, 135, + /* 1150 */ 112, 113, 114, 134, 135, 135, 112, 113, 114, 134, + /* 1160 */ 135, 135, 135, 135, 103, 135, 134, 135, 135, 135, + /* 1170 */ 103, 135, 134, 112, 113, 114, 135, 103, 134, 112, + /* 1180 */ 113, 114, 103, 135, 135, 135, 112, 113, 114, 135, + /* 1190 */ 135, 112, 113, 114, 103, 134, 135, 135, 135, 135, + /* 1200 */ 103, 134, 135, 112, 113, 114, 103, 135, 134, 112, + /* 1210 */ 113, 114, 135, 134, 135, 112, 113, 114, 135, 135, + /* 1220 */ 135, 135, 135, 103, 135, 134, 135, 135, 135, 135, + /* 1230 */ 135, 134, 112, 113, 114, 103, 135, 134, 135, 103, + /* 1240 */ 135, 135, 103, 135, 112, 113, 114, 135, 112, 113, + /* 1250 */ 114, 112, 113, 114, 134, 4, 5, 6, 7, 4, + /* 1260 */ 5, 6, 7, 135, 135, 135, 134, 135, 135, 135, + /* 1270 */ 134, 135, 135, 134, 135, 135, 25, 135, 135, 135, + /* 1280 */ 25, 4, 5, 6, 7, 135, 135, 135, 135, 135, + /* 1290 */ 135, 135, 135, 135, 135, 135, 135, 135, 135, 135, + /* 1300 */ 135, 135, 25, 135, 135, 135, 135, 135, 135, 135, + /* 1310 */ 135, 135, 135, 135, 135, 135, 135, 135, 135, 135, + /* 1320 */ 135, 135, 135, 135, 135, 135, 135, 135, 135, 135, + /* 1330 */ 135, 135, 135, 135, 135, 135, 135, 135, 135, 135, + /* 1340 */ 135, 135, 135, 135, 135, 135, 135, 135, 135, 135, + /* 1350 */ 135, 135, 135, 135, 135, 135, 135, 135, 135, 135, + /* 1360 */ 135, 135, 135, 135, 135, 135, 135, 135, 135, 135, + /* 1370 */ 135, 135, 135, 135, 135, 135, 135, 135, 135, 135, + /* 1380 */ 135, 99, 99, 99, 99, 99, 99, 99, 99, 99, + /* 1390 */ 99, 99, 99, 99, 99, 99, 99, 99, 99, 99, + /* 1400 */ 99, 99, +} + +const YY_SHIFT_COUNT = 163 +const YY_SHIFT_MIN = 0 +const YY_SHIFT_MAX = 1277 + +var yy_shift_ofst = []uint16{ + /* 0 */ 143, 127, 221, 244, 244, 244, 244, 244, 244, 244, + /* 10 */ 244, 244, 244, 244, 244, 244, 244, 244, 244, 244, + /* 20 */ 244, 244, 244, 244, 244, 244, 244, 276, 510, 557, + /* 30 */ 276, 143, 347, 347, 0, 64, 143, 573, 557, 573, + /* 40 */ 400, 400, 400, 442, 539, 557, 557, 557, 557, 557, + /* 50 */ 557, 604, 557, 557, 667, 557, 557, 557, 557, 557, + /* 60 */ 557, 557, 557, 557, 557, 218, 60, 60, 60, 60, + /* 70 */ 60, 145, 315, 393, 471, 292, 292, 170, 71, 1303, + /* 80 */ 1303, 1303, 1303, 114, 114, 338, 402, 129, 444, 367, + /* 90 */ 683, 589, 1251, 669, 1255, 1048, 1277, 1077, 1106, 25, + /* 100 */ 25, 25, 184, 25, 25, 25, 168, 25, 429, 83, + /* 110 */ 92, 105, 70, 133, 138, 182, 182, 234, 257, 137, + /* 120 */ 149, 289, 141, 155, 151, 146, 156, 147, 174, 176, + /* 130 */ 196, 203, 204, 179, 237, 249, 213, 261, 211, 214, + /* 140 */ 215, 222, 290, 300, 307, 278, 323, 330, 336, 246, + /* 150 */ 274, 329, 246, 343, 345, 346, 348, 351, 352, 353, + /* 160 */ 372, 297, 384, 377, +} + +const YY_REDUCE_COUNT = 82 +const YY_REDUCE_MIN = -129 +const YY_REDUCE_MAX = 1139 + +var yy_reduce_ofst = []int16{ + /* 0 */ 363, -96, -32, 93, 152, 394, 512, 599, 616, 631, + /* 10 */ 664, 680, 696, 710, 722, 735, 749, 761, 775, 791, + /* 20 */ 807, 821, 833, 846, 860, 872, 886, 889, 668, 905, + /* 30 */ 913, 464, 603, 779, -61, -61, 535, 650, 932, 945, + /* 40 */ 794, 929, 957, 961, 964, 987, 990, 993, 1006, 1019, + /* 50 */ 1025, 1032, 1038, 1044, 1061, 1067, 1074, 1079, 1091, 1097, + /* 60 */ 1103, 1120, 1132, 1136, 1139, -81, -111, -101, -47, -37, + /* 70 */ -23, -22, -129, -129, -129, -97, -86, -58, -100, -15, + /* 80 */ 30, 34, 24, +} +var yy_default = []YYACTIONTYPE{ + /* 0 */ 449, 443, 443, 443, 443, 443, 443, 443, 443, 443, + /* 10 */ 443, 443, 443, 443, 443, 443, 443, 443, 443, 443, + /* 20 */ 443, 443, 443, 443, 443, 443, 443, 443, 473, 576, + /* 30 */ 443, 449, 580, 485, 581, 581, 449, 443, 443, 443, + /* 40 */ 443, 443, 443, 443, 443, 443, 443, 443, 477, 443, + /* 50 */ 443, 443, 443, 443, 443, 443, 443, 443, 443, 443, + /* 60 */ 443, 443, 443, 443, 443, 443, 443, 443, 443, 443, + /* 70 */ 443, 443, 443, 443, 443, 443, 443, 443, 455, 470, + /* 80 */ 508, 508, 576, 468, 493, 443, 443, 443, 471, 443, + /* 90 */ 443, 443, 443, 443, 443, 443, 443, 443, 443, 488, + /* 100 */ 486, 476, 459, 512, 511, 510, 443, 566, 443, 443, + /* 110 */ 443, 443, 443, 588, 443, 545, 544, 540, 443, 532, + /* 120 */ 529, 443, 443, 443, 443, 443, 443, 491, 443, 443, + /* 130 */ 443, 443, 443, 443, 443, 443, 443, 443, 443, 443, + /* 140 */ 443, 443, 443, 443, 443, 443, 443, 443, 443, 592, + /* 150 */ 443, 443, 443, 443, 443, 443, 443, 443, 443, 443, + /* 160 */ 443, 601, 443, 443, +} + +/********** End of lemon-generated parsing tables *****************************/ + +/* The next table maps tokens (terminal symbols) into fallback tokens. +** If a construct like the following: +** +** %fallback ID X Y Z. +** +** appears in the grammar, then ID becomes a fallback token for X, Y, +** and Z. Whenever one of the tokens X, Y, or Z is input to the parser +** but it does not parse, the type of the token is changed to ID and +** the parse is retried before an error is thrown. +** +** This feature can be used, for example, to cause some keywords in a language +** to revert to identifiers if they keyword does not apply in the context where +** it appears. + */ +var yyFallback = []YYCODETYPE{ + // + 0, /* $ => nothing */ + 0, /* ID => nothing */ + 1, /* EDGEPT => ID */ + 0, /* OF => nothing */ + 0, /* PLUS => nothing */ + 0, /* MINUS => nothing */ + 0, /* STAR => nothing */ + 0, /* SLASH => nothing */ + 0, /* PERCENT => nothing */ + 0, /* UMINUS => nothing */ + 0, /* EOL => nothing */ + 0, /* ASSIGN => nothing */ + 0, /* PLACENAME => nothing */ + 0, /* COLON => nothing */ + 0, /* ASSERT => nothing */ + 0, /* LP => nothing */ + 0, /* EQ => nothing */ + 0, /* RP => nothing */ + 0, /* DEFINE => nothing */ + 0, /* CODEBLOCK => nothing */ + 0, /* FILL => nothing */ + 0, /* COLOR => nothing */ + 0, /* THICKNESS => nothing */ + 0, /* PRINT => nothing */ + 0, /* STRING => nothing */ + 0, /* COMMA => nothing */ + 0, /* CLASSNAME => nothing */ + 0, /* LB => nothing */ + 0, /* RB => nothing */ + 0, /* UP => nothing */ + 0, /* DOWN => nothing */ + 0, /* LEFT => nothing */ + 0, /* RIGHT => nothing */ + 0, /* CLOSE => nothing */ + 0, /* CHOP => nothing */ + 0, /* FROM => nothing */ + 0, /* TO => nothing */ + 0, /* THEN => nothing */ + 0, /* HEADING => nothing */ + 0, /* GO => nothing */ + 0, /* AT => nothing */ + 0, /* WITH => nothing */ + 0, /* SAME => nothing */ + 0, /* AS => nothing */ + 0, /* FIT => nothing */ + 0, /* BEHIND => nothing */ + 0, /* UNTIL => nothing */ + 0, /* EVEN => nothing */ + 0, /* DOT_E => nothing */ + 0, /* HEIGHT => nothing */ + 0, /* WIDTH => nothing */ + 0, /* RADIUS => nothing */ + 0, /* DIAMETER => nothing */ + 0, /* DOTTED => nothing */ + 0, /* DASHED => nothing */ + 0, /* CW => nothing */ + 0, /* CCW => nothing */ + 0, /* LARROW => nothing */ + 0, /* RARROW => nothing */ + 0, /* LRARROW => nothing */ + 0, /* INVIS => nothing */ + 0, /* THICK => nothing */ + 0, /* THIN => nothing */ + 0, /* SOLID => nothing */ + 0, /* CENTER => nothing */ + 0, /* LJUST => nothing */ + 0, /* RJUST => nothing */ + 0, /* ABOVE => nothing */ + 0, /* BELOW => nothing */ + 0, /* ITALIC => nothing */ + 0, /* BOLD => nothing */ + 0, /* ALIGNED => nothing */ + 0, /* BIG => nothing */ + 0, /* SMALL => nothing */ + 0, /* AND => nothing */ + 0, /* LT => nothing */ + 0, /* GT => nothing */ + 0, /* ON => nothing */ + 0, /* WAY => nothing */ + 0, /* BETWEEN => nothing */ + 0, /* THE => nothing */ + 0, /* NTH => nothing */ + 0, /* VERTEX => nothing */ + 0, /* TOP => nothing */ + 0, /* BOTTOM => nothing */ + 0, /* START => nothing */ + 0, /* END => nothing */ + 0, /* IN => nothing */ + 0, /* THIS => nothing */ + 0, /* DOT_U => nothing */ + 0, /* LAST => nothing */ + 0, /* NUMBER => nothing */ + 0, /* FUNC1 => nothing */ + 0, /* FUNC2 => nothing */ + 0, /* DIST => nothing */ + 0, /* DOT_XY => nothing */ + 0, /* X => nothing */ + 0, /* Y => nothing */ + 0, /* DOT_L => nothing */ +} + +/* The following structure represents a single element of the +** parser's stack. Information stored includes: +** +** + The state number for the parser at this level of the stack. +** +** + The value of the token stored at this level of the stack. +** (In other words, the "major" token.) +** +** + The semantic value stored at this level of the stack. This is +** the information used by the action routines in the grammar. +** It is sometimes called the "minor" token. +** +** After the "shift" half of a SHIFTREDUCE action, the stateno field +** actually contains the reduce action for the second half of the +** SHIFTREDUCE. + */ +type yyStackEntry struct { + stateno YYACTIONTYPE /* The state-number, or reduce action in SHIFTREDUCE */ + major YYCODETYPE /* The major token value. This is the code + ** number for the token at this stack level */ + minor YYMINORTYPE /* The user-supplied minor token value. This + ** is the value of the token */ +} + +/* The state of the parser is completely contained in an instance of +** the following structure */ +type yyParser struct { + yytos int /* Index of top element on the stack */ + // #ifdef YYTRACKMAXSTACKDEPTH + yyhwm int /* High-water mark of the stack */ + // #endif + // #ifndef YYNOERRORRECOVERY + yyerrcnt int /* Shifts left before out of the error */ + // #endif + /* A place to hold %extra_argument */ + p *Pik /* A place to hold %extra_context */ + yystack []yyStackEntry +} + +var yyTraceFILE *os.File +var yyTracePrompt string + +/* +** Turn parser tracing on by giving a stream to which to write the trace +** and a prompt to preface each trace message. Tracing is turned off +** by making either argument NULL +** +** Inputs: +**
      +**
    • A FILE* to which trace output should be written. +** If NULL, then tracing is turned off. +**
    • A prefix string written at the beginning of every +** line of trace output. If NULL, then tracing is +** turned off. +**
    +** +** Outputs: +** None. + */ +func pik_parserTrace(TraceFILE *os.File, zTracePrompt string) { + yyTraceFILE = TraceFILE + yyTracePrompt = zTracePrompt + if yyTraceFILE == nil { + yyTracePrompt = "" + } else if yyTracePrompt == "" { + yyTraceFILE = nil + } +} + +/* For tracing shifts, the names of all terminals and nonterminals +** are required. The following table supplies these names */ +var yyTokenName = []string{ + /* 0 */ "$", + /* 1 */ "ID", + /* 2 */ "EDGEPT", + /* 3 */ "OF", + /* 4 */ "PLUS", + /* 5 */ "MINUS", + /* 6 */ "STAR", + /* 7 */ "SLASH", + /* 8 */ "PERCENT", + /* 9 */ "UMINUS", + /* 10 */ "EOL", + /* 11 */ "ASSIGN", + /* 12 */ "PLACENAME", + /* 13 */ "COLON", + /* 14 */ "ASSERT", + /* 15 */ "LP", + /* 16 */ "EQ", + /* 17 */ "RP", + /* 18 */ "DEFINE", + /* 19 */ "CODEBLOCK", + /* 20 */ "FILL", + /* 21 */ "COLOR", + /* 22 */ "THICKNESS", + /* 23 */ "PRINT", + /* 24 */ "STRING", + /* 25 */ "COMMA", + /* 26 */ "CLASSNAME", + /* 27 */ "LB", + /* 28 */ "RB", + /* 29 */ "UP", + /* 30 */ "DOWN", + /* 31 */ "LEFT", + /* 32 */ "RIGHT", + /* 33 */ "CLOSE", + /* 34 */ "CHOP", + /* 35 */ "FROM", + /* 36 */ "TO", + /* 37 */ "THEN", + /* 38 */ "HEADING", + /* 39 */ "GO", + /* 40 */ "AT", + /* 41 */ "WITH", + /* 42 */ "SAME", + /* 43 */ "AS", + /* 44 */ "FIT", + /* 45 */ "BEHIND", + /* 46 */ "UNTIL", + /* 47 */ "EVEN", + /* 48 */ "DOT_E", + /* 49 */ "HEIGHT", + /* 50 */ "WIDTH", + /* 51 */ "RADIUS", + /* 52 */ "DIAMETER", + /* 53 */ "DOTTED", + /* 54 */ "DASHED", + /* 55 */ "CW", + /* 56 */ "CCW", + /* 57 */ "LARROW", + /* 58 */ "RARROW", + /* 59 */ "LRARROW", + /* 60 */ "INVIS", + /* 61 */ "THICK", + /* 62 */ "THIN", + /* 63 */ "SOLID", + /* 64 */ "CENTER", + /* 65 */ "LJUST", + /* 66 */ "RJUST", + /* 67 */ "ABOVE", + /* 68 */ "BELOW", + /* 69 */ "ITALIC", + /* 70 */ "BOLD", + /* 71 */ "ALIGNED", + /* 72 */ "BIG", + /* 73 */ "SMALL", + /* 74 */ "AND", + /* 75 */ "LT", + /* 76 */ "GT", + /* 77 */ "ON", + /* 78 */ "WAY", + /* 79 */ "BETWEEN", + /* 80 */ "THE", + /* 81 */ "NTH", + /* 82 */ "VERTEX", + /* 83 */ "TOP", + /* 84 */ "BOTTOM", + /* 85 */ "START", + /* 86 */ "END", + /* 87 */ "IN", + /* 88 */ "THIS", + /* 89 */ "DOT_U", + /* 90 */ "LAST", + /* 91 */ "NUMBER", + /* 92 */ "FUNC1", + /* 93 */ "FUNC2", + /* 94 */ "DIST", + /* 95 */ "DOT_XY", + /* 96 */ "X", + /* 97 */ "Y", + /* 98 */ "DOT_L", + /* 99 */ "statement_list", + /* 100 */ "statement", + /* 101 */ "unnamed_statement", + /* 102 */ "basetype", + /* 103 */ "expr", + /* 104 */ "numproperty", + /* 105 */ "edge", + /* 106 */ "direction", + /* 107 */ "dashproperty", + /* 108 */ "colorproperty", + /* 109 */ "locproperty", + /* 110 */ "position", + /* 111 */ "place", + /* 112 */ "object", + /* 113 */ "objectname", + /* 114 */ "nth", + /* 115 */ "textposition", + /* 116 */ "rvalue", + /* 117 */ "lvalue", + /* 118 */ "even", + /* 119 */ "relexpr", + /* 120 */ "optrelexpr", + /* 121 */ "document", + /* 122 */ "print", + /* 123 */ "prlist", + /* 124 */ "pritem", + /* 125 */ "prsep", + /* 126 */ "attribute_list", + /* 127 */ "savelist", + /* 128 */ "alist", + /* 129 */ "attribute", + /* 130 */ "go", + /* 131 */ "boolproperty", + /* 132 */ "withclause", + /* 133 */ "between", + /* 134 */ "place2", +} + +/* For tracing reduce actions, the names of all rules are required. + */ +var yyRuleName = []string{ + /* 0 */ "document ::= statement_list", + /* 1 */ "statement_list ::= statement", + /* 2 */ "statement_list ::= statement_list EOL statement", + /* 3 */ "statement ::=", + /* 4 */ "statement ::= direction", + /* 5 */ "statement ::= lvalue ASSIGN rvalue", + /* 6 */ "statement ::= PLACENAME COLON unnamed_statement", + /* 7 */ "statement ::= PLACENAME COLON position", + /* 8 */ "statement ::= unnamed_statement", + /* 9 */ "statement ::= print prlist", + /* 10 */ "statement ::= ASSERT LP expr EQ expr RP", + /* 11 */ "statement ::= ASSERT LP position EQ position RP", + /* 12 */ "statement ::= DEFINE ID CODEBLOCK", + /* 13 */ "rvalue ::= PLACENAME", + /* 14 */ "pritem ::= FILL", + /* 15 */ "pritem ::= COLOR", + /* 16 */ "pritem ::= THICKNESS", + /* 17 */ "pritem ::= rvalue", + /* 18 */ "pritem ::= STRING", + /* 19 */ "prsep ::= COMMA", + /* 20 */ "unnamed_statement ::= basetype attribute_list", + /* 21 */ "basetype ::= CLASSNAME", + /* 22 */ "basetype ::= STRING textposition", + /* 23 */ "basetype ::= LB savelist statement_list RB", + /* 24 */ "savelist ::=", + /* 25 */ "relexpr ::= expr", + /* 26 */ "relexpr ::= expr PERCENT", + /* 27 */ "optrelexpr ::=", + /* 28 */ "attribute_list ::= relexpr alist", + /* 29 */ "attribute ::= numproperty relexpr", + /* 30 */ "attribute ::= dashproperty expr", + /* 31 */ "attribute ::= dashproperty", + /* 32 */ "attribute ::= colorproperty rvalue", + /* 33 */ "attribute ::= go direction optrelexpr", + /* 34 */ "attribute ::= go direction even position", + /* 35 */ "attribute ::= CLOSE", + /* 36 */ "attribute ::= CHOP", + /* 37 */ "attribute ::= FROM position", + /* 38 */ "attribute ::= TO position", + /* 39 */ "attribute ::= THEN", + /* 40 */ "attribute ::= THEN optrelexpr HEADING expr", + /* 41 */ "attribute ::= THEN optrelexpr EDGEPT", + /* 42 */ "attribute ::= GO optrelexpr HEADING expr", + /* 43 */ "attribute ::= GO optrelexpr EDGEPT", + /* 44 */ "attribute ::= AT position", + /* 45 */ "attribute ::= SAME", + /* 46 */ "attribute ::= SAME AS object", + /* 47 */ "attribute ::= STRING textposition", + /* 48 */ "attribute ::= FIT", + /* 49 */ "attribute ::= BEHIND object", + /* 50 */ "withclause ::= DOT_E edge AT position", + /* 51 */ "withclause ::= edge AT position", + /* 52 */ "numproperty ::= HEIGHT|WIDTH|RADIUS|DIAMETER|THICKNESS", + /* 53 */ "boolproperty ::= CW", + /* 54 */ "boolproperty ::= CCW", + /* 55 */ "boolproperty ::= LARROW", + /* 56 */ "boolproperty ::= RARROW", + /* 57 */ "boolproperty ::= LRARROW", + /* 58 */ "boolproperty ::= INVIS", + /* 59 */ "boolproperty ::= THICK", + /* 60 */ "boolproperty ::= THIN", + /* 61 */ "boolproperty ::= SOLID", + /* 62 */ "textposition ::=", + /* 63 */ "textposition ::= textposition CENTER|LJUST|RJUST|ABOVE|BELOW|ITALIC|BOLD|ALIGNED|BIG|SMALL", + /* 64 */ "position ::= expr COMMA expr", + /* 65 */ "position ::= place PLUS expr COMMA expr", + /* 66 */ "position ::= place MINUS expr COMMA expr", + /* 67 */ "position ::= place PLUS LP expr COMMA expr RP", + /* 68 */ "position ::= place MINUS LP expr COMMA expr RP", + /* 69 */ "position ::= LP position COMMA position RP", + /* 70 */ "position ::= LP position RP", + /* 71 */ "position ::= expr between position AND position", + /* 72 */ "position ::= expr LT position COMMA position GT", + /* 73 */ "position ::= expr ABOVE position", + /* 74 */ "position ::= expr BELOW position", + /* 75 */ "position ::= expr LEFT OF position", + /* 76 */ "position ::= expr RIGHT OF position", + /* 77 */ "position ::= expr ON HEADING EDGEPT OF position", + /* 78 */ "position ::= expr HEADING EDGEPT OF position", + /* 79 */ "position ::= expr EDGEPT OF position", + /* 80 */ "position ::= expr ON HEADING expr FROM position", + /* 81 */ "position ::= expr HEADING expr FROM position", + /* 82 */ "place ::= edge OF object", + /* 83 */ "place2 ::= object", + /* 84 */ "place2 ::= object DOT_E edge", + /* 85 */ "place2 ::= NTH VERTEX OF object", + /* 86 */ "object ::= nth", + /* 87 */ "object ::= nth OF|IN object", + /* 88 */ "objectname ::= THIS", + /* 89 */ "objectname ::= PLACENAME", + /* 90 */ "objectname ::= objectname DOT_U PLACENAME", + /* 91 */ "nth ::= NTH CLASSNAME", + /* 92 */ "nth ::= NTH LAST CLASSNAME", + /* 93 */ "nth ::= LAST CLASSNAME", + /* 94 */ "nth ::= LAST", + /* 95 */ "nth ::= NTH LB RB", + /* 96 */ "nth ::= NTH LAST LB RB", + /* 97 */ "nth ::= LAST LB RB", + /* 98 */ "expr ::= expr PLUS expr", + /* 99 */ "expr ::= expr MINUS expr", + /* 100 */ "expr ::= expr STAR expr", + /* 101 */ "expr ::= expr SLASH expr", + /* 102 */ "expr ::= MINUS expr", + /* 103 */ "expr ::= PLUS expr", + /* 104 */ "expr ::= LP expr RP", + /* 105 */ "expr ::= LP FILL|COLOR|THICKNESS RP", + /* 106 */ "expr ::= NUMBER", + /* 107 */ "expr ::= ID", + /* 108 */ "expr ::= FUNC1 LP expr RP", + /* 109 */ "expr ::= FUNC2 LP expr COMMA expr RP", + /* 110 */ "expr ::= DIST LP position COMMA position RP", + /* 111 */ "expr ::= place2 DOT_XY X", + /* 112 */ "expr ::= place2 DOT_XY Y", + /* 113 */ "expr ::= object DOT_L numproperty", + /* 114 */ "expr ::= object DOT_L dashproperty", + /* 115 */ "expr ::= object DOT_L colorproperty", + /* 116 */ "lvalue ::= ID", + /* 117 */ "lvalue ::= FILL", + /* 118 */ "lvalue ::= COLOR", + /* 119 */ "lvalue ::= THICKNESS", + /* 120 */ "rvalue ::= expr", + /* 121 */ "print ::= PRINT", + /* 122 */ "prlist ::= pritem", + /* 123 */ "prlist ::= prlist prsep pritem", + /* 124 */ "direction ::= UP", + /* 125 */ "direction ::= DOWN", + /* 126 */ "direction ::= LEFT", + /* 127 */ "direction ::= RIGHT", + /* 128 */ "optrelexpr ::= relexpr", + /* 129 */ "attribute_list ::= alist", + /* 130 */ "alist ::=", + /* 131 */ "alist ::= alist attribute", + /* 132 */ "attribute ::= boolproperty", + /* 133 */ "attribute ::= WITH withclause", + /* 134 */ "go ::= GO", + /* 135 */ "go ::=", + /* 136 */ "even ::= UNTIL EVEN WITH", + /* 137 */ "even ::= EVEN WITH", + /* 138 */ "dashproperty ::= DOTTED", + /* 139 */ "dashproperty ::= DASHED", + /* 140 */ "colorproperty ::= FILL", + /* 141 */ "colorproperty ::= COLOR", + /* 142 */ "position ::= place", + /* 143 */ "between ::= WAY BETWEEN", + /* 144 */ "between ::= BETWEEN", + /* 145 */ "between ::= OF THE WAY BETWEEN", + /* 146 */ "place ::= place2", + /* 147 */ "edge ::= CENTER", + /* 148 */ "edge ::= EDGEPT", + /* 149 */ "edge ::= TOP", + /* 150 */ "edge ::= BOTTOM", + /* 151 */ "edge ::= START", + /* 152 */ "edge ::= END", + /* 153 */ "edge ::= RIGHT", + /* 154 */ "edge ::= LEFT", + /* 155 */ "object ::= objectname", +} + +/* +** Try to increase the size of the parser stack. Return the number +** of errors. Return 0 on success. + */ +func (p *yyParser) yyGrowStack() { + oldSize := len(p.yystack) + newSize := oldSize*2 + 100 + pNew := make([]yyStackEntry, newSize) + copy(pNew, p.yystack) + p.yystack = pNew + + if !NDEBUG { // #ifndef NDEBUG + if yyTraceFILE != nil { + fmt.Fprintf(yyTraceFILE, "%sStack grows from %d to %d entries.\n", + yyTracePrompt, oldSize, newSize) + } + } // #endif +} + +/* Datatype of the argument to the memory allocated passed as the +** second argument to pik_parserAlloc() below. This can be changed by +** putting an appropriate #define in the %include section of the input +** grammar. + */ +// #ifndef YYMALLOCARGTYPE +// # define YYMALLOCARGTYPE size_t +// #endif + +/* Initialize a new parser that has already been allocated. + */ +func (yypParser *yyParser) pik_parserInit(p *Pik) { + yypParser.p = p + + if !YYNOERRORRECOVERY { + yypParser.yyerrcnt = -1 + } + if YYSTACKDEPTH > 0 { + yypParser.yystack = make([]yyStackEntry, YYSTACKDEPTH) + } else { + yypParser.yystack = []yyStackEntry{{}} + } + yypParser.yytos = 0 +} + +/* +** This function allocates a new parser. +** The only argument is a pointer to a function which works like +** malloc. +** +** Inputs: +** A pointer to the function used to allocate memory. +** +** Outputs: +** A pointer to a parser. This pointer is used in subsequent calls +** to pik_parser and pik_parserFree. + */ +func pik_parserAlloc(p *Pik) *yyParser { + yypParser := &yyParser{} + yypParser.p = p + + yypParser.pik_parserInit(p) + return yypParser +} + +/* The following function deletes the "minor type" or semantic value +** associated with a symbol. The symbol can be either a terminal +** or nonterminal. "yymajor" is the symbol code, and "yypminor" is +** a pointer to the value to be deleted. The code used to do the +** deletions is derived from the %destructor and/or %token_destructor +** directives of the input grammar. + */ +func (yypParser *yyParser) yy_destructor( + yymajor YYCODETYPE, /* Type code for object to destroy */ + yypminor *YYMINORTYPE, /* The object to be destroyed */ +) { + + p := yypParser.p + _ = p + + switch yymajor { + /* Here is inserted the actions which take place when a + ** terminal or non-terminal is destroyed. This can happen + ** when the symbol is popped from the stack during a + ** reduce or during error processing or when a parser is + ** being destroyed before it is finished parsing. + ** + ** Note: during a reduce, the only symbols destroyed are those + ** which appear on the RHS of the rule, but which are *not* used + ** inside the C code. + */ + /********* Begin destructor definitions ***************************************/ + case 99: /* statement_list */ + { +//line 455 "pikchr.y" + p.pik_elist_free(&(yypminor.yy186)) +//line 1651 "pikchr.go" + } + break + case 100: /* statement */ + case 101: /* unnamed_statement */ + case 102: /* basetype */ + { +//line 457 "pikchr.y" + p.pik_elem_free((yypminor.yy104)) +//line 1660 "pikchr.go" + } + break + /********* End destructor definitions *****************************************/ + default: + break /* If no destructor action specified: do nothing */ + } +} + +/* +** Pop the parser's stack once. +** +** If there is a destructor routine associated with the token which +** is popped from the stack, then call it. + */ +func (pParser *yyParser) yy_pop_parser_stack() { + assert(pParser.yytos > 0, "pParser.yytos>0") + yytos := pParser.yystack[pParser.yytos] + pParser.yytos-- + if !NDEBUG { + if yyTraceFILE != nil { + fmt.Fprintf(yyTraceFILE, "%sPopping %s\n", + yyTracePrompt, + yyTokenName[yytos.major]) + } + } + pParser.yy_destructor(yytos.major, &yytos.minor) +} + +/* +** Clear all secondary memory allocations from the parser + */ +func (pParser *yyParser) pik_parserFinalize() { + for pParser.yytos > 0 { + pParser.yy_pop_parser_stack() + } +} + +/* +** Deallocate and destroy a parser. Destructors are called for +** all stack elements before shutting the parser down. +** +** If the YYPARSEFREENEVERNULL macro exists (for example because it +** is defined in a %include section of the input grammar) then it is +** assumed that the input pointer is never NULL. + */ +func (pParser *yyParser) pik_parserFree() { + pParser.pik_parserFinalize() +} + +/* +** Return the peak depth of the stack for a parser. + */ +func (pParser *yyParser) pik_parserStackPeak() int { + return pParser.yyhwm +} + +/* This array of booleans keeps track of the parser statement +** coverage. The element yycoverage[X][Y] is set when the parser +** is in state X and has a lookahead token Y. In a well-tested +** systems, every element of this matrix should end up being set. + */ +var yycoverage = [YYNSTATE][YYNTOKEN]bool{} + +/* +** Write into out a description of every state/lookahead combination that +** +** (1) has not been used by the parser, and +** (2) is not a syntax error. +** +** Return the number of missed state/lookahead combinations. + */ +func pik_parserCoverage(out io.Writer) int { + nMissed := 0 + for stateno := 0; stateno < YYNSTATE; stateno++ { + i := yy_shift_ofst[stateno] + for iLookAhead := 0; iLookAhead < YYNTOKEN; iLookAhead++ { + if yy_lookahead[int(i)+iLookAhead] != YYCODETYPE(iLookAhead) { + continue + } + if !yycoverage[stateno][iLookAhead] { + nMissed++ + } + if out != nil { + ok := "missed" + if yycoverage[stateno][iLookAhead] { + ok = "ok" + } + fmt.Fprintf(out, "State %d lookahead %s %s\n", stateno, + yyTokenName[iLookAhead], + ok) + } + } + } + return nMissed +} + +/* +** Find the appropriate action for a parser given the terminal +** look-ahead token iLookAhead. + */ +func yy_find_shift_action( + lookAhead YYCODETYPE, /* The look-ahead token */ + stateno YYACTIONTYPE, /* Current state number */ +) YYACTIONTYPE { + iLookAhead := int(lookAhead) + + if stateno > YY_MAX_SHIFT { + return stateno + } + assert(stateno <= YY_SHIFT_COUNT, "stateno <= YY_SHIFT_COUNT") + if YYCOVERAGE { + yycoverage[stateno][iLookAhead] = true + } + for { + i := int(yy_shift_ofst[stateno]) + assert(i >= 0, "i>=0") + assert(i <= YY_ACTTAB_COUNT, "i<=YY_ACTTAB_COUNT") + assert(i+YYNTOKEN <= len(yy_lookahead), "i+YYNTOKEN<=len(yy_lookahead)") + assert(iLookAhead != YYNOCODE, "iLookAhead!=YYNOCODE") + assert(iLookAhead < YYNTOKEN, "iLookAhead < YYNTOKEN") + i += iLookAhead + assert(i < len(yy_lookahead), "i %s\n", + yyTracePrompt, yyTokenName[iLookAhead], yyTokenName[iFallback]) + } + } + assert(yyFallback[iFallback] == 0, "yyFallback[iFallback]==0") /* Fallback loop must terminate */ + iLookAhead = iFallback + continue + } + } + if YYWILDCARD > 0 { + { + j := i - iLookAhead + YYWILDCARD + assert(j < len(yy_lookahead), "j < len(yy_lookahead)") + if int(yy_lookahead[j]) == YYWILDCARD && iLookAhead > 0 { + if !NDEBUG { + if yyTraceFILE != nil { + fmt.Fprintf(yyTraceFILE, "%sWILDCARD %s => %s\n", + yyTracePrompt, yyTokenName[iLookAhead], + yyTokenName[YYWILDCARD]) + } + } /* NDEBUG */ + return yy_action[j] + } + } + } /* YYWILDCARD */ + return yy_default[stateno] + } else { + assert(i >= 0 && i < len(yy_action), "i >= 0 && i < len(yy_action)") + return yy_action[i] + } + } +} + +/* +** Find the appropriate action for a parser given the non-terminal +** look-ahead token iLookAhead. + */ +func yy_find_reduce_action( + stateno YYACTIONTYPE, /* Current state number */ + lookAhead YYCODETYPE, /* The look-ahead token */ +) YYACTIONTYPE { + iLookAhead := int(lookAhead) + if YYERRORSYMBOL > 0 { + if stateno > YY_REDUCE_COUNT { + return yy_default[stateno] + } + } else { + assert(stateno <= YY_REDUCE_COUNT, "stateno <= YY_REDUCE_COUNT") + } + i := int(yy_reduce_ofst[stateno]) + assert(iLookAhead != YYNOCODE, "iLookAhead != YYNOCODE") + i += iLookAhead + if YYERRORSYMBOL > 0 { + if i < 0 || i >= YY_ACTTAB_COUNT || int(yy_lookahead[i]) != iLookAhead { + return yy_default[stateno] + } + } else { + assert(i >= 0 && i < YY_ACTTAB_COUNT, "i >= 0 && i < YY_ACTTAB_COUNT") + assert(int(yy_lookahead[i]) == iLookAhead, "int(yy_lookahead[i]) == iLookAhead") + } + return yy_action[i] +} + +/* +** The following routine is called if the stack overflows. + */ +func (yypParser *yyParser) yyStackOverflow() { + + p := yypParser.p + _ = p + + if !NDEBUG { + if yyTraceFILE != nil { + fmt.Fprintf(yyTraceFILE, "%sStack Overflow!\n", yyTracePrompt) + } + } + for yypParser.yytos > 0 { + yypParser.yy_pop_parser_stack() + } + /* Here code is inserted which will execute if the parser + ** stack every overflows */ + /******** Begin %stack_overflow code ******************************************/ +//line 488 "pikchr.y" + + p.pik_error(nil, "parser stack overflow") +//line 1874 "pikchr.go" + /******** End %stack_overflow code ********************************************/ + /* Suppress warning about unused %extra_argument var */ + yypParser.p = p + +} + +/* +** Print tracing information for a SHIFT action + */ +func (yypParser *yyParser) yyTraceShift(yyNewState int, zTag string) { + if !NDEBUG { + if yyTraceFILE != nil { + if yyNewState < YYNSTATE { + fmt.Fprintf(yyTraceFILE, "%s%s '%s', go to state %d\n", + yyTracePrompt, zTag, yyTokenName[yypParser.yystack[yypParser.yytos].major], + yyNewState) + } else { + fmt.Fprintf(yyTraceFILE, "%s%s '%s', pending reduce %d\n", + yyTracePrompt, zTag, yyTokenName[yypParser.yystack[yypParser.yytos].major], + yyNewState-YY_MIN_REDUCE) + } + } + } +} + +/* +** Perform a shift action. + */ +func (yypParser *yyParser) yy_shift( + yyNewState YYACTIONTYPE, /* The new state to shift in */ + yyMajor YYCODETYPE, /* The major token to shift in */ + yyMinor pik_parserTOKENTYPE, /* The minor token to shift in */ +) { + yypParser.yytos++ + + if YYTRACKMAXSTACKDEPTH { + if yypParser.yytos > yypParser.yyhwm { + yypParser.yyhwm++ + assert(yypParser.yyhwm == yypParser.yytos, "yypParser.yyhwm == yypParser.yytos") + } + } + if YYSTACKDEPTH > 0 { + if yypParser.yytos >= YYSTACKDEPTH { + yypParser.yyStackOverflow() + return + } + } else { + if yypParser.yytos+1 >= len(yypParser.yystack) { + yypParser.yyGrowStack() + } + } + + if yyNewState > YY_MAX_SHIFT { + yyNewState += YY_MIN_REDUCE - YY_MIN_SHIFTREDUCE + } + + yytos := &yypParser.yystack[yypParser.yytos] + yytos.stateno = yyNewState + yytos.major = yyMajor + yytos.minor.yy0 = yyMinor + + yypParser.yyTraceShift(int(yyNewState), "Shift") +} + +/* For rule J, yyRuleInfoLhs[J] contains the symbol on the left-hand side +** of that rule */ +var yyRuleInfoLhs = []YYCODETYPE{ + 121, /* (0) document ::= statement_list */ + 99, /* (1) statement_list ::= statement */ + 99, /* (2) statement_list ::= statement_list EOL statement */ + 100, /* (3) statement ::= */ + 100, /* (4) statement ::= direction */ + 100, /* (5) statement ::= lvalue ASSIGN rvalue */ + 100, /* (6) statement ::= PLACENAME COLON unnamed_statement */ + 100, /* (7) statement ::= PLACENAME COLON position */ + 100, /* (8) statement ::= unnamed_statement */ + 100, /* (9) statement ::= print prlist */ + 100, /* (10) statement ::= ASSERT LP expr EQ expr RP */ + 100, /* (11) statement ::= ASSERT LP position EQ position RP */ + 100, /* (12) statement ::= DEFINE ID CODEBLOCK */ + 116, /* (13) rvalue ::= PLACENAME */ + 124, /* (14) pritem ::= FILL */ + 124, /* (15) pritem ::= COLOR */ + 124, /* (16) pritem ::= THICKNESS */ + 124, /* (17) pritem ::= rvalue */ + 124, /* (18) pritem ::= STRING */ + 125, /* (19) prsep ::= COMMA */ + 101, /* (20) unnamed_statement ::= basetype attribute_list */ + 102, /* (21) basetype ::= CLASSNAME */ + 102, /* (22) basetype ::= STRING textposition */ + 102, /* (23) basetype ::= LB savelist statement_list RB */ + 127, /* (24) savelist ::= */ + 119, /* (25) relexpr ::= expr */ + 119, /* (26) relexpr ::= expr PERCENT */ + 120, /* (27) optrelexpr ::= */ + 126, /* (28) attribute_list ::= relexpr alist */ + 129, /* (29) attribute ::= numproperty relexpr */ + 129, /* (30) attribute ::= dashproperty expr */ + 129, /* (31) attribute ::= dashproperty */ + 129, /* (32) attribute ::= colorproperty rvalue */ + 129, /* (33) attribute ::= go direction optrelexpr */ + 129, /* (34) attribute ::= go direction even position */ + 129, /* (35) attribute ::= CLOSE */ + 129, /* (36) attribute ::= CHOP */ + 129, /* (37) attribute ::= FROM position */ + 129, /* (38) attribute ::= TO position */ + 129, /* (39) attribute ::= THEN */ + 129, /* (40) attribute ::= THEN optrelexpr HEADING expr */ + 129, /* (41) attribute ::= THEN optrelexpr EDGEPT */ + 129, /* (42) attribute ::= GO optrelexpr HEADING expr */ + 129, /* (43) attribute ::= GO optrelexpr EDGEPT */ + 129, /* (44) attribute ::= AT position */ + 129, /* (45) attribute ::= SAME */ + 129, /* (46) attribute ::= SAME AS object */ + 129, /* (47) attribute ::= STRING textposition */ + 129, /* (48) attribute ::= FIT */ + 129, /* (49) attribute ::= BEHIND object */ + 132, /* (50) withclause ::= DOT_E edge AT position */ + 132, /* (51) withclause ::= edge AT position */ + 104, /* (52) numproperty ::= HEIGHT|WIDTH|RADIUS|DIAMETER|THICKNESS */ + 131, /* (53) boolproperty ::= CW */ + 131, /* (54) boolproperty ::= CCW */ + 131, /* (55) boolproperty ::= LARROW */ + 131, /* (56) boolproperty ::= RARROW */ + 131, /* (57) boolproperty ::= LRARROW */ + 131, /* (58) boolproperty ::= INVIS */ + 131, /* (59) boolproperty ::= THICK */ + 131, /* (60) boolproperty ::= THIN */ + 131, /* (61) boolproperty ::= SOLID */ + 115, /* (62) textposition ::= */ + 115, /* (63) textposition ::= textposition CENTER|LJUST|RJUST|ABOVE|BELOW|ITALIC|BOLD|ALIGNED|BIG|SMALL */ + 110, /* (64) position ::= expr COMMA expr */ + 110, /* (65) position ::= place PLUS expr COMMA expr */ + 110, /* (66) position ::= place MINUS expr COMMA expr */ + 110, /* (67) position ::= place PLUS LP expr COMMA expr RP */ + 110, /* (68) position ::= place MINUS LP expr COMMA expr RP */ + 110, /* (69) position ::= LP position COMMA position RP */ + 110, /* (70) position ::= LP position RP */ + 110, /* (71) position ::= expr between position AND position */ + 110, /* (72) position ::= expr LT position COMMA position GT */ + 110, /* (73) position ::= expr ABOVE position */ + 110, /* (74) position ::= expr BELOW position */ + 110, /* (75) position ::= expr LEFT OF position */ + 110, /* (76) position ::= expr RIGHT OF position */ + 110, /* (77) position ::= expr ON HEADING EDGEPT OF position */ + 110, /* (78) position ::= expr HEADING EDGEPT OF position */ + 110, /* (79) position ::= expr EDGEPT OF position */ + 110, /* (80) position ::= expr ON HEADING expr FROM position */ + 110, /* (81) position ::= expr HEADING expr FROM position */ + 111, /* (82) place ::= edge OF object */ + 134, /* (83) place2 ::= object */ + 134, /* (84) place2 ::= object DOT_E edge */ + 134, /* (85) place2 ::= NTH VERTEX OF object */ + 112, /* (86) object ::= nth */ + 112, /* (87) object ::= nth OF|IN object */ + 113, /* (88) objectname ::= THIS */ + 113, /* (89) objectname ::= PLACENAME */ + 113, /* (90) objectname ::= objectname DOT_U PLACENAME */ + 114, /* (91) nth ::= NTH CLASSNAME */ + 114, /* (92) nth ::= NTH LAST CLASSNAME */ + 114, /* (93) nth ::= LAST CLASSNAME */ + 114, /* (94) nth ::= LAST */ + 114, /* (95) nth ::= NTH LB RB */ + 114, /* (96) nth ::= NTH LAST LB RB */ + 114, /* (97) nth ::= LAST LB RB */ + 103, /* (98) expr ::= expr PLUS expr */ + 103, /* (99) expr ::= expr MINUS expr */ + 103, /* (100) expr ::= expr STAR expr */ + 103, /* (101) expr ::= expr SLASH expr */ + 103, /* (102) expr ::= MINUS expr */ + 103, /* (103) expr ::= PLUS expr */ + 103, /* (104) expr ::= LP expr RP */ + 103, /* (105) expr ::= LP FILL|COLOR|THICKNESS RP */ + 103, /* (106) expr ::= NUMBER */ + 103, /* (107) expr ::= ID */ + 103, /* (108) expr ::= FUNC1 LP expr RP */ + 103, /* (109) expr ::= FUNC2 LP expr COMMA expr RP */ + 103, /* (110) expr ::= DIST LP position COMMA position RP */ + 103, /* (111) expr ::= place2 DOT_XY X */ + 103, /* (112) expr ::= place2 DOT_XY Y */ + 103, /* (113) expr ::= object DOT_L numproperty */ + 103, /* (114) expr ::= object DOT_L dashproperty */ + 103, /* (115) expr ::= object DOT_L colorproperty */ + 117, /* (116) lvalue ::= ID */ + 117, /* (117) lvalue ::= FILL */ + 117, /* (118) lvalue ::= COLOR */ + 117, /* (119) lvalue ::= THICKNESS */ + 116, /* (120) rvalue ::= expr */ + 122, /* (121) print ::= PRINT */ + 123, /* (122) prlist ::= pritem */ + 123, /* (123) prlist ::= prlist prsep pritem */ + 106, /* (124) direction ::= UP */ + 106, /* (125) direction ::= DOWN */ + 106, /* (126) direction ::= LEFT */ + 106, /* (127) direction ::= RIGHT */ + 120, /* (128) optrelexpr ::= relexpr */ + 126, /* (129) attribute_list ::= alist */ + 128, /* (130) alist ::= */ + 128, /* (131) alist ::= alist attribute */ + 129, /* (132) attribute ::= boolproperty */ + 129, /* (133) attribute ::= WITH withclause */ + 130, /* (134) go ::= GO */ + 130, /* (135) go ::= */ + 118, /* (136) even ::= UNTIL EVEN WITH */ + 118, /* (137) even ::= EVEN WITH */ + 107, /* (138) dashproperty ::= DOTTED */ + 107, /* (139) dashproperty ::= DASHED */ + 108, /* (140) colorproperty ::= FILL */ + 108, /* (141) colorproperty ::= COLOR */ + 110, /* (142) position ::= place */ + 133, /* (143) between ::= WAY BETWEEN */ + 133, /* (144) between ::= BETWEEN */ + 133, /* (145) between ::= OF THE WAY BETWEEN */ + 111, /* (146) place ::= place2 */ + 105, /* (147) edge ::= CENTER */ + 105, /* (148) edge ::= EDGEPT */ + 105, /* (149) edge ::= TOP */ + 105, /* (150) edge ::= BOTTOM */ + 105, /* (151) edge ::= START */ + 105, /* (152) edge ::= END */ + 105, /* (153) edge ::= RIGHT */ + 105, /* (154) edge ::= LEFT */ + 112, /* (155) object ::= objectname */ +} + +/* For rule J, yyRuleInfoNRhs[J] contains the negative of the number +** of symbols on the right-hand side of that rule. */ +var yyRuleInfoNRhs = []int8{ + -1, /* (0) document ::= statement_list */ + -1, /* (1) statement_list ::= statement */ + -3, /* (2) statement_list ::= statement_list EOL statement */ + 0, /* (3) statement ::= */ + -1, /* (4) statement ::= direction */ + -3, /* (5) statement ::= lvalue ASSIGN rvalue */ + -3, /* (6) statement ::= PLACENAME COLON unnamed_statement */ + -3, /* (7) statement ::= PLACENAME COLON position */ + -1, /* (8) statement ::= unnamed_statement */ + -2, /* (9) statement ::= print prlist */ + -6, /* (10) statement ::= ASSERT LP expr EQ expr RP */ + -6, /* (11) statement ::= ASSERT LP position EQ position RP */ + -3, /* (12) statement ::= DEFINE ID CODEBLOCK */ + -1, /* (13) rvalue ::= PLACENAME */ + -1, /* (14) pritem ::= FILL */ + -1, /* (15) pritem ::= COLOR */ + -1, /* (16) pritem ::= THICKNESS */ + -1, /* (17) pritem ::= rvalue */ + -1, /* (18) pritem ::= STRING */ + -1, /* (19) prsep ::= COMMA */ + -2, /* (20) unnamed_statement ::= basetype attribute_list */ + -1, /* (21) basetype ::= CLASSNAME */ + -2, /* (22) basetype ::= STRING textposition */ + -4, /* (23) basetype ::= LB savelist statement_list RB */ + 0, /* (24) savelist ::= */ + -1, /* (25) relexpr ::= expr */ + -2, /* (26) relexpr ::= expr PERCENT */ + 0, /* (27) optrelexpr ::= */ + -2, /* (28) attribute_list ::= relexpr alist */ + -2, /* (29) attribute ::= numproperty relexpr */ + -2, /* (30) attribute ::= dashproperty expr */ + -1, /* (31) attribute ::= dashproperty */ + -2, /* (32) attribute ::= colorproperty rvalue */ + -3, /* (33) attribute ::= go direction optrelexpr */ + -4, /* (34) attribute ::= go direction even position */ + -1, /* (35) attribute ::= CLOSE */ + -1, /* (36) attribute ::= CHOP */ + -2, /* (37) attribute ::= FROM position */ + -2, /* (38) attribute ::= TO position */ + -1, /* (39) attribute ::= THEN */ + -4, /* (40) attribute ::= THEN optrelexpr HEADING expr */ + -3, /* (41) attribute ::= THEN optrelexpr EDGEPT */ + -4, /* (42) attribute ::= GO optrelexpr HEADING expr */ + -3, /* (43) attribute ::= GO optrelexpr EDGEPT */ + -2, /* (44) attribute ::= AT position */ + -1, /* (45) attribute ::= SAME */ + -3, /* (46) attribute ::= SAME AS object */ + -2, /* (47) attribute ::= STRING textposition */ + -1, /* (48) attribute ::= FIT */ + -2, /* (49) attribute ::= BEHIND object */ + -4, /* (50) withclause ::= DOT_E edge AT position */ + -3, /* (51) withclause ::= edge AT position */ + -1, /* (52) numproperty ::= HEIGHT|WIDTH|RADIUS|DIAMETER|THICKNESS */ + -1, /* (53) boolproperty ::= CW */ + -1, /* (54) boolproperty ::= CCW */ + -1, /* (55) boolproperty ::= LARROW */ + -1, /* (56) boolproperty ::= RARROW */ + -1, /* (57) boolproperty ::= LRARROW */ + -1, /* (58) boolproperty ::= INVIS */ + -1, /* (59) boolproperty ::= THICK */ + -1, /* (60) boolproperty ::= THIN */ + -1, /* (61) boolproperty ::= SOLID */ + 0, /* (62) textposition ::= */ + -2, /* (63) textposition ::= textposition CENTER|LJUST|RJUST|ABOVE|BELOW|ITALIC|BOLD|ALIGNED|BIG|SMALL */ + -3, /* (64) position ::= expr COMMA expr */ + -5, /* (65) position ::= place PLUS expr COMMA expr */ + -5, /* (66) position ::= place MINUS expr COMMA expr */ + -7, /* (67) position ::= place PLUS LP expr COMMA expr RP */ + -7, /* (68) position ::= place MINUS LP expr COMMA expr RP */ + -5, /* (69) position ::= LP position COMMA position RP */ + -3, /* (70) position ::= LP position RP */ + -5, /* (71) position ::= expr between position AND position */ + -6, /* (72) position ::= expr LT position COMMA position GT */ + -3, /* (73) position ::= expr ABOVE position */ + -3, /* (74) position ::= expr BELOW position */ + -4, /* (75) position ::= expr LEFT OF position */ + -4, /* (76) position ::= expr RIGHT OF position */ + -6, /* (77) position ::= expr ON HEADING EDGEPT OF position */ + -5, /* (78) position ::= expr HEADING EDGEPT OF position */ + -4, /* (79) position ::= expr EDGEPT OF position */ + -6, /* (80) position ::= expr ON HEADING expr FROM position */ + -5, /* (81) position ::= expr HEADING expr FROM position */ + -3, /* (82) place ::= edge OF object */ + -1, /* (83) place2 ::= object */ + -3, /* (84) place2 ::= object DOT_E edge */ + -4, /* (85) place2 ::= NTH VERTEX OF object */ + -1, /* (86) object ::= nth */ + -3, /* (87) object ::= nth OF|IN object */ + -1, /* (88) objectname ::= THIS */ + -1, /* (89) objectname ::= PLACENAME */ + -3, /* (90) objectname ::= objectname DOT_U PLACENAME */ + -2, /* (91) nth ::= NTH CLASSNAME */ + -3, /* (92) nth ::= NTH LAST CLASSNAME */ + -2, /* (93) nth ::= LAST CLASSNAME */ + -1, /* (94) nth ::= LAST */ + -3, /* (95) nth ::= NTH LB RB */ + -4, /* (96) nth ::= NTH LAST LB RB */ + -3, /* (97) nth ::= LAST LB RB */ + -3, /* (98) expr ::= expr PLUS expr */ + -3, /* (99) expr ::= expr MINUS expr */ + -3, /* (100) expr ::= expr STAR expr */ + -3, /* (101) expr ::= expr SLASH expr */ + -2, /* (102) expr ::= MINUS expr */ + -2, /* (103) expr ::= PLUS expr */ + -3, /* (104) expr ::= LP expr RP */ + -3, /* (105) expr ::= LP FILL|COLOR|THICKNESS RP */ + -1, /* (106) expr ::= NUMBER */ + -1, /* (107) expr ::= ID */ + -4, /* (108) expr ::= FUNC1 LP expr RP */ + -6, /* (109) expr ::= FUNC2 LP expr COMMA expr RP */ + -6, /* (110) expr ::= DIST LP position COMMA position RP */ + -3, /* (111) expr ::= place2 DOT_XY X */ + -3, /* (112) expr ::= place2 DOT_XY Y */ + -3, /* (113) expr ::= object DOT_L numproperty */ + -3, /* (114) expr ::= object DOT_L dashproperty */ + -3, /* (115) expr ::= object DOT_L colorproperty */ + -1, /* (116) lvalue ::= ID */ + -1, /* (117) lvalue ::= FILL */ + -1, /* (118) lvalue ::= COLOR */ + -1, /* (119) lvalue ::= THICKNESS */ + -1, /* (120) rvalue ::= expr */ + -1, /* (121) print ::= PRINT */ + -1, /* (122) prlist ::= pritem */ + -3, /* (123) prlist ::= prlist prsep pritem */ + -1, /* (124) direction ::= UP */ + -1, /* (125) direction ::= DOWN */ + -1, /* (126) direction ::= LEFT */ + -1, /* (127) direction ::= RIGHT */ + -1, /* (128) optrelexpr ::= relexpr */ + -1, /* (129) attribute_list ::= alist */ + 0, /* (130) alist ::= */ + -2, /* (131) alist ::= alist attribute */ + -1, /* (132) attribute ::= boolproperty */ + -2, /* (133) attribute ::= WITH withclause */ + -1, /* (134) go ::= GO */ + 0, /* (135) go ::= */ + -3, /* (136) even ::= UNTIL EVEN WITH */ + -2, /* (137) even ::= EVEN WITH */ + -1, /* (138) dashproperty ::= DOTTED */ + -1, /* (139) dashproperty ::= DASHED */ + -1, /* (140) colorproperty ::= FILL */ + -1, /* (141) colorproperty ::= COLOR */ + -1, /* (142) position ::= place */ + -2, /* (143) between ::= WAY BETWEEN */ + -1, /* (144) between ::= BETWEEN */ + -4, /* (145) between ::= OF THE WAY BETWEEN */ + -1, /* (146) place ::= place2 */ + -1, /* (147) edge ::= CENTER */ + -1, /* (148) edge ::= EDGEPT */ + -1, /* (149) edge ::= TOP */ + -1, /* (150) edge ::= BOTTOM */ + -1, /* (151) edge ::= START */ + -1, /* (152) edge ::= END */ + -1, /* (153) edge ::= RIGHT */ + -1, /* (154) edge ::= LEFT */ + -1, /* (155) object ::= objectname */ +} + +/* +** Perform a reduce action and the shift that must immediately +** follow the reduce. +** +** The yyLookahead and yyLookaheadToken parameters provide reduce actions +** access to the lookahead token (if any). The yyLookahead will be YYNOCODE +** if the lookahead token has already been consumed. As this procedure is +** only called from one place, optimizing compilers will in-line it, which +** means that the extra parameters have no performance impact. + */ +func (yypParser *yyParser) yy_reduce( + yyruleno YYACTIONTYPE, /* Number of the rule by which to reduce */ + yyLookahead YYCODETYPE, /* Lookahead token, or YYNOCODE if none */ + yyLookaheadToken pik_parserTOKENTYPE, /* Value of the lookahead token */ + p *Pik /* %extra_context */) YYACTIONTYPE { + var ( + yygoto YYCODETYPE /* The next state */ + yyact YYACTIONTYPE /* The next action */ + yymsp int /* The top of the parser's stack */ + yysize int /* Amount to pop the stack */ + yylhsminor YYMINORTYPE + ) + yymsp = yypParser.yytos + _ = yylhsminor + + switch yyruleno { + /* Beginning here are the reduction cases. A typical example + ** follows: + ** case 0: + ** #line + ** { ... } // User supplied code + ** #line + ** break; + */ + /********** Begin reduce actions **********************************************/ + case 0: /* document ::= statement_list */ +//line 492 "pikchr.y" + { + p.pik_render(yypParser.yystack[yypParser.yytos+0].minor.yy186) + } +//line 2301 "pikchr.go" + break + case 1: /* statement_list ::= statement */ +//line 495 "pikchr.y" + { + yylhsminor.yy186 = p.pik_elist_append(nil, yypParser.yystack[yypParser.yytos+0].minor.yy104) + } +//line 2306 "pikchr.go" + yypParser.yystack[yypParser.yytos+0].minor.yy186 = yylhsminor.yy186 + break + case 2: /* statement_list ::= statement_list EOL statement */ +//line 497 "pikchr.y" + { + yylhsminor.yy186 = p.pik_elist_append(yypParser.yystack[yypParser.yytos+-2].minor.yy186, yypParser.yystack[yypParser.yytos+0].minor.yy104) + } +//line 2312 "pikchr.go" + yypParser.yystack[yypParser.yytos+-2].minor.yy186 = yylhsminor.yy186 + break + case 3: /* statement ::= */ +//line 500 "pikchr.y" + { + yypParser.yystack[yypParser.yytos+1].minor.yy104 = nil + } +//line 2318 "pikchr.go" + break + case 4: /* statement ::= direction */ +//line 501 "pikchr.y" + { + p.pik_set_direction(uint8(yypParser.yystack[yypParser.yytos+0].minor.yy0.eCode)) + yylhsminor.yy104 = nil + } +//line 2323 "pikchr.go" + yypParser.yystack[yypParser.yytos+0].minor.yy104 = yylhsminor.yy104 + break + case 5: /* statement ::= lvalue ASSIGN rvalue */ +//line 502 "pikchr.y" + { + p.pik_set_var(&yypParser.yystack[yypParser.yytos+-2].minor.yy0, yypParser.yystack[yypParser.yytos+0].minor.yy153, &yypParser.yystack[yypParser.yytos+-1].minor.yy0) + yylhsminor.yy104 = nil + } +//line 2329 "pikchr.go" + yypParser.yystack[yypParser.yytos+-2].minor.yy104 = yylhsminor.yy104 + break + case 6: /* statement ::= PLACENAME COLON unnamed_statement */ +//line 504 "pikchr.y" + { + yylhsminor.yy104 = yypParser.yystack[yypParser.yytos+0].minor.yy104 + p.pik_elem_setname(yypParser.yystack[yypParser.yytos+0].minor.yy104, &yypParser.yystack[yypParser.yytos+-2].minor.yy0) + } +//line 2335 "pikchr.go" + yypParser.yystack[yypParser.yytos+-2].minor.yy104 = yylhsminor.yy104 + break + case 7: /* statement ::= PLACENAME COLON position */ +//line 506 "pikchr.y" + { + yylhsminor.yy104 = p.pik_elem_new(nil, nil, nil) + if yylhsminor.yy104 != nil { + yylhsminor.yy104.ptAt = yypParser.yystack[yypParser.yytos+0].minor.yy79 + p.pik_elem_setname(yylhsminor.yy104, &yypParser.yystack[yypParser.yytos+-2].minor.yy0) + } + } +//line 2342 "pikchr.go" + yypParser.yystack[yypParser.yytos+-2].minor.yy104 = yylhsminor.yy104 + break + case 8: /* statement ::= unnamed_statement */ +//line 508 "pikchr.y" + { + yylhsminor.yy104 = yypParser.yystack[yypParser.yytos+0].minor.yy104 + } +//line 2348 "pikchr.go" + yypParser.yystack[yypParser.yytos+0].minor.yy104 = yylhsminor.yy104 + break + case 9: /* statement ::= print prlist */ +//line 509 "pikchr.y" + { + p.pik_append("
    \n") + yypParser.yystack[yypParser.yytos+-1].minor.yy104 = nil + } +//line 2354 "pikchr.go" + break + case 10: /* statement ::= ASSERT LP expr EQ expr RP */ +//line 514 "pikchr.y" + { + yypParser.yystack[yypParser.yytos+-5].minor.yy104 = p.pik_assert(yypParser.yystack[yypParser.yytos+-3].minor.yy153, &yypParser.yystack[yypParser.yytos+-2].minor.yy0, yypParser.yystack[yypParser.yytos+-1].minor.yy153) + } +//line 2359 "pikchr.go" + break + case 11: /* statement ::= ASSERT LP position EQ position RP */ +//line 516 "pikchr.y" + { + yypParser.yystack[yypParser.yytos+-5].minor.yy104 = p.pik_position_assert(&yypParser.yystack[yypParser.yytos+-3].minor.yy79, &yypParser.yystack[yypParser.yytos+-2].minor.yy0, &yypParser.yystack[yypParser.yytos+-1].minor.yy79) + } +//line 2364 "pikchr.go" + break + case 12: /* statement ::= DEFINE ID CODEBLOCK */ +//line 517 "pikchr.y" + { + yypParser.yystack[yypParser.yytos+-2].minor.yy104 = nil + p.pik_add_macro(&yypParser.yystack[yypParser.yytos+-1].minor.yy0, &yypParser.yystack[yypParser.yytos+0].minor.yy0) + } +//line 2369 "pikchr.go" + break + case 13: /* rvalue ::= PLACENAME */ +//line 528 "pikchr.y" + { + yylhsminor.yy153 = p.pik_lookup_color(&yypParser.yystack[yypParser.yytos+0].minor.yy0) + } +//line 2374 "pikchr.go" + yypParser.yystack[yypParser.yytos+0].minor.yy153 = yylhsminor.yy153 + break + case 14: /* pritem ::= FILL */ + fallthrough + case 15: /* pritem ::= COLOR */ + yytestcase(yyruleno == 15) + fallthrough + case 16: /* pritem ::= THICKNESS */ + yytestcase(yyruleno == 16) +//line 533 "pikchr.y" + { + p.pik_append_num("", p.pik_value(yypParser.yystack[yypParser.yytos+0].minor.yy0.String(), nil)) + } +//line 2384 "pikchr.go" + break + case 17: /* pritem ::= rvalue */ +//line 536 "pikchr.y" + { + p.pik_append_num("", yypParser.yystack[yypParser.yytos+0].minor.yy153) + } +//line 2389 "pikchr.go" + break + case 18: /* pritem ::= STRING */ +//line 537 "pikchr.y" + { + p.pik_append_text(string(yypParser.yystack[yypParser.yytos+0].minor.yy0.z[1:yypParser.yystack[yypParser.yytos+0].minor.yy0.n-1]), 0) + } +//line 2394 "pikchr.go" + break + case 19: /* prsep ::= COMMA */ +//line 538 "pikchr.y" + { + p.pik_append(" ") + } +//line 2399 "pikchr.go" + break + case 20: /* unnamed_statement ::= basetype attribute_list */ +//line 541 "pikchr.y" + { + yylhsminor.yy104 = yypParser.yystack[yypParser.yytos+-1].minor.yy104 + p.pik_after_adding_attributes(yylhsminor.yy104) + } +//line 2404 "pikchr.go" + yypParser.yystack[yypParser.yytos+-1].minor.yy104 = yylhsminor.yy104 + break + case 21: /* basetype ::= CLASSNAME */ +//line 543 "pikchr.y" + { + yylhsminor.yy104 = p.pik_elem_new(&yypParser.yystack[yypParser.yytos+0].minor.yy0, nil, nil) + } +//line 2410 "pikchr.go" + yypParser.yystack[yypParser.yytos+0].minor.yy104 = yylhsminor.yy104 + break + case 22: /* basetype ::= STRING textposition */ +//line 545 "pikchr.y" + { + yypParser.yystack[yypParser.yytos+-1].minor.yy0.eCode = int16(yypParser.yystack[yypParser.yytos+0].minor.yy112) + yylhsminor.yy104 = p.pik_elem_new(nil, &yypParser.yystack[yypParser.yytos+-1].minor.yy0, nil) + } +//line 2416 "pikchr.go" + yypParser.yystack[yypParser.yytos+-1].minor.yy104 = yylhsminor.yy104 + break + case 23: /* basetype ::= LB savelist statement_list RB */ +//line 547 "pikchr.y" + { + p.list = yypParser.yystack[yypParser.yytos+-2].minor.yy186 + yypParser.yystack[yypParser.yytos+-3].minor.yy104 = p.pik_elem_new(nil, nil, yypParser.yystack[yypParser.yytos+-1].minor.yy186) + if yypParser.yystack[yypParser.yytos+-3].minor.yy104 != nil { + yypParser.yystack[yypParser.yytos+-3].minor.yy104.errTok = yypParser.yystack[yypParser.yytos+0].minor.yy0 + } + } +//line 2422 "pikchr.go" + break + case 24: /* savelist ::= */ +//line 552 "pikchr.y" + { + yypParser.yystack[yypParser.yytos+1].minor.yy186 = p.list + p.list = nil + } +//line 2427 "pikchr.go" + break + case 25: /* relexpr ::= expr */ +//line 559 "pikchr.y" + { + yylhsminor.yy10.rAbs = yypParser.yystack[yypParser.yytos+0].minor.yy153 + yylhsminor.yy10.rRel = 0 + } +//line 2432 "pikchr.go" + yypParser.yystack[yypParser.yytos+0].minor.yy10 = yylhsminor.yy10 + break + case 26: /* relexpr ::= expr PERCENT */ +//line 560 "pikchr.y" + { + yylhsminor.yy10.rAbs = 0 + yylhsminor.yy10.rRel = yypParser.yystack[yypParser.yytos+-1].minor.yy153 / 100 + } +//line 2438 "pikchr.go" + yypParser.yystack[yypParser.yytos+-1].minor.yy10 = yylhsminor.yy10 + break + case 27: /* optrelexpr ::= */ +//line 562 "pikchr.y" + { + yypParser.yystack[yypParser.yytos+1].minor.yy10.rAbs = 0 + yypParser.yystack[yypParser.yytos+1].minor.yy10.rRel = 1.0 + } +//line 2444 "pikchr.go" + break + case 28: /* attribute_list ::= relexpr alist */ +//line 564 "pikchr.y" + { + p.pik_add_direction(nil, &yypParser.yystack[yypParser.yytos+-1].minor.yy10) + } +//line 2449 "pikchr.go" + break + case 29: /* attribute ::= numproperty relexpr */ +//line 568 "pikchr.y" + { + p.pik_set_numprop(&yypParser.yystack[yypParser.yytos+-1].minor.yy0, &yypParser.yystack[yypParser.yytos+0].minor.yy10) + } +//line 2454 "pikchr.go" + break + case 30: /* attribute ::= dashproperty expr */ +//line 569 "pikchr.y" + { + p.pik_set_dashed(&yypParser.yystack[yypParser.yytos+-1].minor.yy0, &yypParser.yystack[yypParser.yytos+0].minor.yy153) + } +//line 2459 "pikchr.go" + break + case 31: /* attribute ::= dashproperty */ +//line 570 "pikchr.y" + { + p.pik_set_dashed(&yypParser.yystack[yypParser.yytos+0].minor.yy0, nil) + } +//line 2464 "pikchr.go" + break + case 32: /* attribute ::= colorproperty rvalue */ +//line 571 "pikchr.y" + { + p.pik_set_clrprop(&yypParser.yystack[yypParser.yytos+-1].minor.yy0, yypParser.yystack[yypParser.yytos+0].minor.yy153) + } +//line 2469 "pikchr.go" + break + case 33: /* attribute ::= go direction optrelexpr */ +//line 572 "pikchr.y" + { + p.pik_add_direction(&yypParser.yystack[yypParser.yytos+-1].minor.yy0, &yypParser.yystack[yypParser.yytos+0].minor.yy10) + } +//line 2474 "pikchr.go" + break + case 34: /* attribute ::= go direction even position */ +//line 573 "pikchr.y" + { + p.pik_evenwith(&yypParser.yystack[yypParser.yytos+-2].minor.yy0, &yypParser.yystack[yypParser.yytos+0].minor.yy79) + } +//line 2479 "pikchr.go" + break + case 35: /* attribute ::= CLOSE */ +//line 574 "pikchr.y" + { + p.pik_close_path(&yypParser.yystack[yypParser.yytos+0].minor.yy0) + } +//line 2484 "pikchr.go" + break + case 36: /* attribute ::= CHOP */ +//line 575 "pikchr.y" + { + p.cur.bChop = true + } +//line 2489 "pikchr.go" + break + case 37: /* attribute ::= FROM position */ +//line 576 "pikchr.y" + { + p.pik_set_from(p.cur, &yypParser.yystack[yypParser.yytos+-1].minor.yy0, &yypParser.yystack[yypParser.yytos+0].minor.yy79) + } +//line 2494 "pikchr.go" + break + case 38: /* attribute ::= TO position */ +//line 577 "pikchr.y" + { + p.pik_add_to(p.cur, &yypParser.yystack[yypParser.yytos+-1].minor.yy0, &yypParser.yystack[yypParser.yytos+0].minor.yy79) + } +//line 2499 "pikchr.go" + break + case 39: /* attribute ::= THEN */ +//line 578 "pikchr.y" + { + p.pik_then(&yypParser.yystack[yypParser.yytos+0].minor.yy0, p.cur) + } +//line 2504 "pikchr.go" + break + case 40: /* attribute ::= THEN optrelexpr HEADING expr */ + fallthrough + case 42: /* attribute ::= GO optrelexpr HEADING expr */ + yytestcase(yyruleno == 42) +//line 580 "pikchr.y" + { + p.pik_move_hdg(&yypParser.yystack[yypParser.yytos+-2].minor.yy10, &yypParser.yystack[yypParser.yytos+-1].minor.yy0, yypParser.yystack[yypParser.yytos+0].minor.yy153, nil, &yypParser.yystack[yypParser.yytos+-3].minor.yy0) + } +//line 2511 "pikchr.go" + break + case 41: /* attribute ::= THEN optrelexpr EDGEPT */ + fallthrough + case 43: /* attribute ::= GO optrelexpr EDGEPT */ + yytestcase(yyruleno == 43) +//line 581 "pikchr.y" + { + p.pik_move_hdg(&yypParser.yystack[yypParser.yytos+-1].minor.yy10, nil, 0, &yypParser.yystack[yypParser.yytos+0].minor.yy0, &yypParser.yystack[yypParser.yytos+-2].minor.yy0) + } +//line 2518 "pikchr.go" + break + case 44: /* attribute ::= AT position */ +//line 586 "pikchr.y" + { + p.pik_set_at(nil, &yypParser.yystack[yypParser.yytos+0].minor.yy79, &yypParser.yystack[yypParser.yytos+-1].minor.yy0) + } +//line 2523 "pikchr.go" + break + case 45: /* attribute ::= SAME */ +//line 588 "pikchr.y" + { + p.pik_same(nil, &yypParser.yystack[yypParser.yytos+0].minor.yy0) + } +//line 2528 "pikchr.go" + break + case 46: /* attribute ::= SAME AS object */ +//line 589 "pikchr.y" + { + p.pik_same(yypParser.yystack[yypParser.yytos+0].minor.yy104, &yypParser.yystack[yypParser.yytos+-2].minor.yy0) + } +//line 2533 "pikchr.go" + break + case 47: /* attribute ::= STRING textposition */ +//line 590 "pikchr.y" + { + p.pik_add_txt(&yypParser.yystack[yypParser.yytos+-1].minor.yy0, int16(yypParser.yystack[yypParser.yytos+0].minor.yy112)) + } +//line 2538 "pikchr.go" + break + case 48: /* attribute ::= FIT */ +//line 591 "pikchr.y" + { + p.pik_size_to_fit(&yypParser.yystack[yypParser.yytos+0].minor.yy0, 3) + } +//line 2543 "pikchr.go" + break + case 49: /* attribute ::= BEHIND object */ +//line 592 "pikchr.y" + { + p.pik_behind(yypParser.yystack[yypParser.yytos+0].minor.yy104) + } +//line 2548 "pikchr.go" + break + case 50: /* withclause ::= DOT_E edge AT position */ + fallthrough + case 51: /* withclause ::= edge AT position */ + yytestcase(yyruleno == 51) +//line 600 "pikchr.y" + { + p.pik_set_at(&yypParser.yystack[yypParser.yytos+-2].minor.yy0, &yypParser.yystack[yypParser.yytos+0].minor.yy79, &yypParser.yystack[yypParser.yytos+-1].minor.yy0) + } +//line 2555 "pikchr.go" + break + case 52: /* numproperty ::= HEIGHT|WIDTH|RADIUS|DIAMETER|THICKNESS */ +//line 604 "pikchr.y" + { + yylhsminor.yy0 = yypParser.yystack[yypParser.yytos+0].minor.yy0 + } +//line 2560 "pikchr.go" + yypParser.yystack[yypParser.yytos+0].minor.yy0 = yylhsminor.yy0 + break + case 53: /* boolproperty ::= CW */ +//line 615 "pikchr.y" + { + p.cur.cw = true + } +//line 2566 "pikchr.go" + break + case 54: /* boolproperty ::= CCW */ +//line 616 "pikchr.y" + { + p.cur.cw = false + } +//line 2571 "pikchr.go" + break + case 55: /* boolproperty ::= LARROW */ +//line 617 "pikchr.y" + { + p.cur.larrow = true + p.cur.rarrow = false + } +//line 2576 "pikchr.go" + break + case 56: /* boolproperty ::= RARROW */ +//line 618 "pikchr.y" + { + p.cur.larrow = false + p.cur.rarrow = true + } +//line 2581 "pikchr.go" + break + case 57: /* boolproperty ::= LRARROW */ +//line 619 "pikchr.y" + { + p.cur.larrow = true + p.cur.rarrow = true + } +//line 2586 "pikchr.go" + break + case 58: /* boolproperty ::= INVIS */ +//line 620 "pikchr.y" + { + p.cur.sw = 0.0 + } +//line 2591 "pikchr.go" + break + case 59: /* boolproperty ::= THICK */ +//line 621 "pikchr.y" + { + p.cur.sw *= 1.5 + } +//line 2596 "pikchr.go" + break + case 60: /* boolproperty ::= THIN */ +//line 622 "pikchr.y" + { + p.cur.sw *= 0.67 + } +//line 2601 "pikchr.go" + break + case 61: /* boolproperty ::= SOLID */ +//line 623 "pikchr.y" + { + p.cur.sw = p.pik_value("thickness", nil) + p.cur.dotted = 0.0 + p.cur.dashed = 0.0 + } +//line 2607 "pikchr.go" + break + case 62: /* textposition ::= */ +//line 626 "pikchr.y" + { + yypParser.yystack[yypParser.yytos+1].minor.yy112 = 0 + } +//line 2612 "pikchr.go" + break + case 63: /* textposition ::= textposition CENTER|LJUST|RJUST|ABOVE|BELOW|ITALIC|BOLD|ALIGNED|BIG|SMALL */ +//line 629 "pikchr.y" + { + yylhsminor.yy112 = pik_text_position(yypParser.yystack[yypParser.yytos+-1].minor.yy112, &yypParser.yystack[yypParser.yytos+0].minor.yy0) + } +//line 2617 "pikchr.go" + yypParser.yystack[yypParser.yytos+-1].minor.yy112 = yylhsminor.yy112 + break + case 64: /* position ::= expr COMMA expr */ +//line 632 "pikchr.y" + { + yylhsminor.yy79.x = yypParser.yystack[yypParser.yytos+-2].minor.yy153 + yylhsminor.yy79.y = yypParser.yystack[yypParser.yytos+0].minor.yy153 + } +//line 2623 "pikchr.go" + yypParser.yystack[yypParser.yytos+-2].minor.yy79 = yylhsminor.yy79 + break + case 65: /* position ::= place PLUS expr COMMA expr */ +//line 634 "pikchr.y" + { + yylhsminor.yy79.x = yypParser.yystack[yypParser.yytos+-4].minor.yy79.x + yypParser.yystack[yypParser.yytos+-2].minor.yy153 + yylhsminor.yy79.y = yypParser.yystack[yypParser.yytos+-4].minor.yy79.y + yypParser.yystack[yypParser.yytos+0].minor.yy153 + } +//line 2629 "pikchr.go" + yypParser.yystack[yypParser.yytos+-4].minor.yy79 = yylhsminor.yy79 + break + case 66: /* position ::= place MINUS expr COMMA expr */ +//line 635 "pikchr.y" + { + yylhsminor.yy79.x = yypParser.yystack[yypParser.yytos+-4].minor.yy79.x - yypParser.yystack[yypParser.yytos+-2].minor.yy153 + yylhsminor.yy79.y = yypParser.yystack[yypParser.yytos+-4].minor.yy79.y - yypParser.yystack[yypParser.yytos+0].minor.yy153 + } +//line 2635 "pikchr.go" + yypParser.yystack[yypParser.yytos+-4].minor.yy79 = yylhsminor.yy79 + break + case 67: /* position ::= place PLUS LP expr COMMA expr RP */ +//line 637 "pikchr.y" + { + yylhsminor.yy79.x = yypParser.yystack[yypParser.yytos+-6].minor.yy79.x + yypParser.yystack[yypParser.yytos+-3].minor.yy153 + yylhsminor.yy79.y = yypParser.yystack[yypParser.yytos+-6].minor.yy79.y + yypParser.yystack[yypParser.yytos+-1].minor.yy153 + } +//line 2641 "pikchr.go" + yypParser.yystack[yypParser.yytos+-6].minor.yy79 = yylhsminor.yy79 + break + case 68: /* position ::= place MINUS LP expr COMMA expr RP */ +//line 639 "pikchr.y" + { + yylhsminor.yy79.x = yypParser.yystack[yypParser.yytos+-6].minor.yy79.x - yypParser.yystack[yypParser.yytos+-3].minor.yy153 + yylhsminor.yy79.y = yypParser.yystack[yypParser.yytos+-6].minor.yy79.y - yypParser.yystack[yypParser.yytos+-1].minor.yy153 + } +//line 2647 "pikchr.go" + yypParser.yystack[yypParser.yytos+-6].minor.yy79 = yylhsminor.yy79 + break + case 69: /* position ::= LP position COMMA position RP */ +//line 640 "pikchr.y" + { + yypParser.yystack[yypParser.yytos+-4].minor.yy79.x = yypParser.yystack[yypParser.yytos+-3].minor.yy79.x + yypParser.yystack[yypParser.yytos+-4].minor.yy79.y = yypParser.yystack[yypParser.yytos+-1].minor.yy79.y + } +//line 2653 "pikchr.go" + break + case 70: /* position ::= LP position RP */ +//line 641 "pikchr.y" + { + yypParser.yystack[yypParser.yytos+-2].minor.yy79 = yypParser.yystack[yypParser.yytos+-1].minor.yy79 + } +//line 2658 "pikchr.go" + break + case 71: /* position ::= expr between position AND position */ +//line 643 "pikchr.y" + { + yylhsminor.yy79 = pik_position_between(yypParser.yystack[yypParser.yytos+-4].minor.yy153, yypParser.yystack[yypParser.yytos+-2].minor.yy79, yypParser.yystack[yypParser.yytos+0].minor.yy79) + } +//line 2663 "pikchr.go" + yypParser.yystack[yypParser.yytos+-4].minor.yy79 = yylhsminor.yy79 + break + case 72: /* position ::= expr LT position COMMA position GT */ +//line 645 "pikchr.y" + { + yylhsminor.yy79 = pik_position_between(yypParser.yystack[yypParser.yytos+-5].minor.yy153, yypParser.yystack[yypParser.yytos+-3].minor.yy79, yypParser.yystack[yypParser.yytos+-1].minor.yy79) + } +//line 2669 "pikchr.go" + yypParser.yystack[yypParser.yytos+-5].minor.yy79 = yylhsminor.yy79 + break + case 73: /* position ::= expr ABOVE position */ +//line 646 "pikchr.y" + { + yylhsminor.yy79 = yypParser.yystack[yypParser.yytos+0].minor.yy79 + yylhsminor.yy79.y += yypParser.yystack[yypParser.yytos+-2].minor.yy153 + } +//line 2675 "pikchr.go" + yypParser.yystack[yypParser.yytos+-2].minor.yy79 = yylhsminor.yy79 + break + case 74: /* position ::= expr BELOW position */ +//line 647 "pikchr.y" + { + yylhsminor.yy79 = yypParser.yystack[yypParser.yytos+0].minor.yy79 + yylhsminor.yy79.y -= yypParser.yystack[yypParser.yytos+-2].minor.yy153 + } +//line 2681 "pikchr.go" + yypParser.yystack[yypParser.yytos+-2].minor.yy79 = yylhsminor.yy79 + break + case 75: /* position ::= expr LEFT OF position */ +//line 648 "pikchr.y" + { + yylhsminor.yy79 = yypParser.yystack[yypParser.yytos+0].minor.yy79 + yylhsminor.yy79.x -= yypParser.yystack[yypParser.yytos+-3].minor.yy153 + } +//line 2687 "pikchr.go" + yypParser.yystack[yypParser.yytos+-3].minor.yy79 = yylhsminor.yy79 + break + case 76: /* position ::= expr RIGHT OF position */ +//line 649 "pikchr.y" + { + yylhsminor.yy79 = yypParser.yystack[yypParser.yytos+0].minor.yy79 + yylhsminor.yy79.x += yypParser.yystack[yypParser.yytos+-3].minor.yy153 + } +//line 2693 "pikchr.go" + yypParser.yystack[yypParser.yytos+-3].minor.yy79 = yylhsminor.yy79 + break + case 77: /* position ::= expr ON HEADING EDGEPT OF position */ +//line 651 "pikchr.y" + { + yylhsminor.yy79 = pik_position_at_hdg(yypParser.yystack[yypParser.yytos+-5].minor.yy153, &yypParser.yystack[yypParser.yytos+-2].minor.yy0, yypParser.yystack[yypParser.yytos+0].minor.yy79) + } +//line 2699 "pikchr.go" + yypParser.yystack[yypParser.yytos+-5].minor.yy79 = yylhsminor.yy79 + break + case 78: /* position ::= expr HEADING EDGEPT OF position */ +//line 653 "pikchr.y" + { + yylhsminor.yy79 = pik_position_at_hdg(yypParser.yystack[yypParser.yytos+-4].minor.yy153, &yypParser.yystack[yypParser.yytos+-2].minor.yy0, yypParser.yystack[yypParser.yytos+0].minor.yy79) + } +//line 2705 "pikchr.go" + yypParser.yystack[yypParser.yytos+-4].minor.yy79 = yylhsminor.yy79 + break + case 79: /* position ::= expr EDGEPT OF position */ +//line 655 "pikchr.y" + { + yylhsminor.yy79 = pik_position_at_hdg(yypParser.yystack[yypParser.yytos+-3].minor.yy153, &yypParser.yystack[yypParser.yytos+-2].minor.yy0, yypParser.yystack[yypParser.yytos+0].minor.yy79) + } +//line 2711 "pikchr.go" + yypParser.yystack[yypParser.yytos+-3].minor.yy79 = yylhsminor.yy79 + break + case 80: /* position ::= expr ON HEADING expr FROM position */ +//line 657 "pikchr.y" + { + yylhsminor.yy79 = pik_position_at_angle(yypParser.yystack[yypParser.yytos+-5].minor.yy153, yypParser.yystack[yypParser.yytos+-2].minor.yy153, yypParser.yystack[yypParser.yytos+0].minor.yy79) + } +//line 2717 "pikchr.go" + yypParser.yystack[yypParser.yytos+-5].minor.yy79 = yylhsminor.yy79 + break + case 81: /* position ::= expr HEADING expr FROM position */ +//line 659 "pikchr.y" + { + yylhsminor.yy79 = pik_position_at_angle(yypParser.yystack[yypParser.yytos+-4].minor.yy153, yypParser.yystack[yypParser.yytos+-2].minor.yy153, yypParser.yystack[yypParser.yytos+0].minor.yy79) + } +//line 2723 "pikchr.go" + yypParser.yystack[yypParser.yytos+-4].minor.yy79 = yylhsminor.yy79 + break + case 82: /* place ::= edge OF object */ +//line 671 "pikchr.y" + { + yylhsminor.yy79 = p.pik_place_of_elem(yypParser.yystack[yypParser.yytos+0].minor.yy104, &yypParser.yystack[yypParser.yytos+-2].minor.yy0) + } +//line 2729 "pikchr.go" + yypParser.yystack[yypParser.yytos+-2].minor.yy79 = yylhsminor.yy79 + break + case 83: /* place2 ::= object */ +//line 672 "pikchr.y" + { + yylhsminor.yy79 = p.pik_place_of_elem(yypParser.yystack[yypParser.yytos+0].minor.yy104, nil) + } +//line 2735 "pikchr.go" + yypParser.yystack[yypParser.yytos+0].minor.yy79 = yylhsminor.yy79 + break + case 84: /* place2 ::= object DOT_E edge */ +//line 673 "pikchr.y" + { + yylhsminor.yy79 = p.pik_place_of_elem(yypParser.yystack[yypParser.yytos+-2].minor.yy104, &yypParser.yystack[yypParser.yytos+0].minor.yy0) + } +//line 2741 "pikchr.go" + yypParser.yystack[yypParser.yytos+-2].minor.yy79 = yylhsminor.yy79 + break + case 85: /* place2 ::= NTH VERTEX OF object */ +//line 674 "pikchr.y" + { + yylhsminor.yy79 = p.pik_nth_vertex(&yypParser.yystack[yypParser.yytos+-3].minor.yy0, &yypParser.yystack[yypParser.yytos+-2].minor.yy0, yypParser.yystack[yypParser.yytos+0].minor.yy104) + } +//line 2747 "pikchr.go" + yypParser.yystack[yypParser.yytos+-3].minor.yy79 = yylhsminor.yy79 + break + case 86: /* object ::= nth */ +//line 686 "pikchr.y" + { + yylhsminor.yy104 = p.pik_find_nth(nil, &yypParser.yystack[yypParser.yytos+0].minor.yy0) + } +//line 2753 "pikchr.go" + yypParser.yystack[yypParser.yytos+0].minor.yy104 = yylhsminor.yy104 + break + case 87: /* object ::= nth OF|IN object */ +//line 687 "pikchr.y" + { + yylhsminor.yy104 = p.pik_find_nth(yypParser.yystack[yypParser.yytos+0].minor.yy104, &yypParser.yystack[yypParser.yytos+-2].minor.yy0) + } +//line 2759 "pikchr.go" + yypParser.yystack[yypParser.yytos+-2].minor.yy104 = yylhsminor.yy104 + break + case 88: /* objectname ::= THIS */ +//line 689 "pikchr.y" + { + yypParser.yystack[yypParser.yytos+0].minor.yy104 = p.cur + } +//line 2765 "pikchr.go" + break + case 89: /* objectname ::= PLACENAME */ +//line 690 "pikchr.y" + { + yylhsminor.yy104 = p.pik_find_byname(nil, &yypParser.yystack[yypParser.yytos+0].minor.yy0) + } +//line 2770 "pikchr.go" + yypParser.yystack[yypParser.yytos+0].minor.yy104 = yylhsminor.yy104 + break + case 90: /* objectname ::= objectname DOT_U PLACENAME */ +//line 692 "pikchr.y" + { + yylhsminor.yy104 = p.pik_find_byname(yypParser.yystack[yypParser.yytos+-2].minor.yy104, &yypParser.yystack[yypParser.yytos+0].minor.yy0) + } +//line 2776 "pikchr.go" + yypParser.yystack[yypParser.yytos+-2].minor.yy104 = yylhsminor.yy104 + break + case 91: /* nth ::= NTH CLASSNAME */ +//line 694 "pikchr.y" + { + yylhsminor.yy0 = yypParser.yystack[yypParser.yytos+0].minor.yy0 + yylhsminor.yy0.eCode = p.pik_nth_value(&yypParser.yystack[yypParser.yytos+-1].minor.yy0) + } +//line 2782 "pikchr.go" + yypParser.yystack[yypParser.yytos+-1].minor.yy0 = yylhsminor.yy0 + break + case 92: /* nth ::= NTH LAST CLASSNAME */ +//line 695 "pikchr.y" + { + yylhsminor.yy0 = yypParser.yystack[yypParser.yytos+0].minor.yy0 + yylhsminor.yy0.eCode = -p.pik_nth_value(&yypParser.yystack[yypParser.yytos+-2].minor.yy0) + } +//line 2788 "pikchr.go" + yypParser.yystack[yypParser.yytos+-2].minor.yy0 = yylhsminor.yy0 + break + case 93: /* nth ::= LAST CLASSNAME */ +//line 696 "pikchr.y" + { + yypParser.yystack[yypParser.yytos+-1].minor.yy0 = yypParser.yystack[yypParser.yytos+0].minor.yy0 + yypParser.yystack[yypParser.yytos+-1].minor.yy0.eCode = -1 + } +//line 2794 "pikchr.go" + break + case 94: /* nth ::= LAST */ +//line 697 "pikchr.y" + { + yylhsminor.yy0 = yypParser.yystack[yypParser.yytos+0].minor.yy0 + yylhsminor.yy0.eCode = -1 + } +//line 2799 "pikchr.go" + yypParser.yystack[yypParser.yytos+0].minor.yy0 = yylhsminor.yy0 + break + case 95: /* nth ::= NTH LB RB */ +//line 698 "pikchr.y" + { + yylhsminor.yy0 = yypParser.yystack[yypParser.yytos+-1].minor.yy0 + yylhsminor.yy0.eCode = p.pik_nth_value(&yypParser.yystack[yypParser.yytos+-2].minor.yy0) + } +//line 2805 "pikchr.go" + yypParser.yystack[yypParser.yytos+-2].minor.yy0 = yylhsminor.yy0 + break + case 96: /* nth ::= NTH LAST LB RB */ +//line 699 "pikchr.y" + { + yylhsminor.yy0 = yypParser.yystack[yypParser.yytos+-1].minor.yy0 + yylhsminor.yy0.eCode = -p.pik_nth_value(&yypParser.yystack[yypParser.yytos+-3].minor.yy0) + } +//line 2811 "pikchr.go" + yypParser.yystack[yypParser.yytos+-3].minor.yy0 = yylhsminor.yy0 + break + case 97: /* nth ::= LAST LB RB */ +//line 700 "pikchr.y" + { + yypParser.yystack[yypParser.yytos+-2].minor.yy0 = yypParser.yystack[yypParser.yytos+-1].minor.yy0 + yypParser.yystack[yypParser.yytos+-2].minor.yy0.eCode = -1 + } +//line 2817 "pikchr.go" + break + case 98: /* expr ::= expr PLUS expr */ +//line 702 "pikchr.y" + { + yylhsminor.yy153 = yypParser.yystack[yypParser.yytos+-2].minor.yy153 + yypParser.yystack[yypParser.yytos+0].minor.yy153 + } +//line 2822 "pikchr.go" + yypParser.yystack[yypParser.yytos+-2].minor.yy153 = yylhsminor.yy153 + break + case 99: /* expr ::= expr MINUS expr */ +//line 703 "pikchr.y" + { + yylhsminor.yy153 = yypParser.yystack[yypParser.yytos+-2].minor.yy153 - yypParser.yystack[yypParser.yytos+0].minor.yy153 + } +//line 2828 "pikchr.go" + yypParser.yystack[yypParser.yytos+-2].minor.yy153 = yylhsminor.yy153 + break + case 100: /* expr ::= expr STAR expr */ +//line 704 "pikchr.y" + { + yylhsminor.yy153 = yypParser.yystack[yypParser.yytos+-2].minor.yy153 * yypParser.yystack[yypParser.yytos+0].minor.yy153 + } +//line 2834 "pikchr.go" + yypParser.yystack[yypParser.yytos+-2].minor.yy153 = yylhsminor.yy153 + break + case 101: /* expr ::= expr SLASH expr */ +//line 705 "pikchr.y" + { + if yypParser.yystack[yypParser.yytos+0].minor.yy153 == 0.0 { + p.pik_error(&yypParser.yystack[yypParser.yytos+-1].minor.yy0, "division by zero") + yylhsminor.yy153 = 0.0 + } else { + yylhsminor.yy153 = yypParser.yystack[yypParser.yytos+-2].minor.yy153 / yypParser.yystack[yypParser.yytos+0].minor.yy153 + } + } +//line 2842 "pikchr.go" + yypParser.yystack[yypParser.yytos+-2].minor.yy153 = yylhsminor.yy153 + break + case 102: /* expr ::= MINUS expr */ +//line 708 "pikchr.y" + { + yypParser.yystack[yypParser.yytos+-1].minor.yy153 = -yypParser.yystack[yypParser.yytos+0].minor.yy153 + } +//line 2848 "pikchr.go" + break + case 103: /* expr ::= PLUS expr */ +//line 709 "pikchr.y" + { + yypParser.yystack[yypParser.yytos+-1].minor.yy153 = yypParser.yystack[yypParser.yytos+0].minor.yy153 + } +//line 2853 "pikchr.go" + break + case 104: /* expr ::= LP expr RP */ +//line 710 "pikchr.y" + { + yypParser.yystack[yypParser.yytos+-2].minor.yy153 = yypParser.yystack[yypParser.yytos+-1].minor.yy153 + } +//line 2858 "pikchr.go" + break + case 105: /* expr ::= LP FILL|COLOR|THICKNESS RP */ +//line 711 "pikchr.y" + { + yypParser.yystack[yypParser.yytos+-2].minor.yy153 = p.pik_get_var(&yypParser.yystack[yypParser.yytos+-1].minor.yy0) + } +//line 2863 "pikchr.go" + break + case 106: /* expr ::= NUMBER */ +//line 712 "pikchr.y" + { + yylhsminor.yy153 = pik_atof(&yypParser.yystack[yypParser.yytos+0].minor.yy0) + } +//line 2868 "pikchr.go" + yypParser.yystack[yypParser.yytos+0].minor.yy153 = yylhsminor.yy153 + break + case 107: /* expr ::= ID */ +//line 713 "pikchr.y" + { + yylhsminor.yy153 = p.pik_get_var(&yypParser.yystack[yypParser.yytos+0].minor.yy0) + } +//line 2874 "pikchr.go" + yypParser.yystack[yypParser.yytos+0].minor.yy153 = yylhsminor.yy153 + break + case 108: /* expr ::= FUNC1 LP expr RP */ +//line 714 "pikchr.y" + { + yylhsminor.yy153 = p.pik_func(&yypParser.yystack[yypParser.yytos+-3].minor.yy0, yypParser.yystack[yypParser.yytos+-1].minor.yy153, 0.0) + } +//line 2880 "pikchr.go" + yypParser.yystack[yypParser.yytos+-3].minor.yy153 = yylhsminor.yy153 + break + case 109: /* expr ::= FUNC2 LP expr COMMA expr RP */ +//line 715 "pikchr.y" + { + yylhsminor.yy153 = p.pik_func(&yypParser.yystack[yypParser.yytos+-5].minor.yy0, yypParser.yystack[yypParser.yytos+-3].minor.yy153, yypParser.yystack[yypParser.yytos+-1].minor.yy153) + } +//line 2886 "pikchr.go" + yypParser.yystack[yypParser.yytos+-5].minor.yy153 = yylhsminor.yy153 + break + case 110: /* expr ::= DIST LP position COMMA position RP */ +//line 716 "pikchr.y" + { + yypParser.yystack[yypParser.yytos+-5].minor.yy153 = pik_dist(&yypParser.yystack[yypParser.yytos+-3].minor.yy79, &yypParser.yystack[yypParser.yytos+-1].minor.yy79) + } +//line 2892 "pikchr.go" + break + case 111: /* expr ::= place2 DOT_XY X */ +//line 717 "pikchr.y" + { + yylhsminor.yy153 = yypParser.yystack[yypParser.yytos+-2].minor.yy79.x + } +//line 2897 "pikchr.go" + yypParser.yystack[yypParser.yytos+-2].minor.yy153 = yylhsminor.yy153 + break + case 112: /* expr ::= place2 DOT_XY Y */ +//line 718 "pikchr.y" + { + yylhsminor.yy153 = yypParser.yystack[yypParser.yytos+-2].minor.yy79.y + } +//line 2903 "pikchr.go" + yypParser.yystack[yypParser.yytos+-2].minor.yy153 = yylhsminor.yy153 + break + case 113: /* expr ::= object DOT_L numproperty */ + fallthrough + case 114: /* expr ::= object DOT_L dashproperty */ + yytestcase(yyruleno == 114) + fallthrough + case 115: /* expr ::= object DOT_L colorproperty */ + yytestcase(yyruleno == 115) +//line 719 "pikchr.y" + { + yylhsminor.yy153 = pik_property_of(yypParser.yystack[yypParser.yytos+-2].minor.yy104, &yypParser.yystack[yypParser.yytos+0].minor.yy0) + } +//line 2913 "pikchr.go" + yypParser.yystack[yypParser.yytos+-2].minor.yy153 = yylhsminor.yy153 + break + default: + /* (116) lvalue ::= ID */ yytestcase(yyruleno == 116) + /* (117) lvalue ::= FILL */ yytestcase(yyruleno == 117) + /* (118) lvalue ::= COLOR */ yytestcase(yyruleno == 118) + /* (119) lvalue ::= THICKNESS */ yytestcase(yyruleno == 119) + /* (120) rvalue ::= expr */ yytestcase(yyruleno == 120) + /* (121) print ::= PRINT */ yytestcase(yyruleno == 121) + /* (122) prlist ::= pritem (OPTIMIZED OUT) */ assert(yyruleno != 122, "yyruleno!=122") + /* (123) prlist ::= prlist prsep pritem */ yytestcase(yyruleno == 123) + /* (124) direction ::= UP */ yytestcase(yyruleno == 124) + /* (125) direction ::= DOWN */ yytestcase(yyruleno == 125) + /* (126) direction ::= LEFT */ yytestcase(yyruleno == 126) + /* (127) direction ::= RIGHT */ yytestcase(yyruleno == 127) + /* (128) optrelexpr ::= relexpr (OPTIMIZED OUT) */ assert(yyruleno != 128, "yyruleno!=128") + /* (129) attribute_list ::= alist */ yytestcase(yyruleno == 129) + /* (130) alist ::= */ yytestcase(yyruleno == 130) + /* (131) alist ::= alist attribute */ yytestcase(yyruleno == 131) + /* (132) attribute ::= boolproperty (OPTIMIZED OUT) */ assert(yyruleno != 132, "yyruleno!=132") + /* (133) attribute ::= WITH withclause */ yytestcase(yyruleno == 133) + /* (134) go ::= GO */ yytestcase(yyruleno == 134) + /* (135) go ::= */ yytestcase(yyruleno == 135) + /* (136) even ::= UNTIL EVEN WITH */ yytestcase(yyruleno == 136) + /* (137) even ::= EVEN WITH */ yytestcase(yyruleno == 137) + /* (138) dashproperty ::= DOTTED */ yytestcase(yyruleno == 138) + /* (139) dashproperty ::= DASHED */ yytestcase(yyruleno == 139) + /* (140) colorproperty ::= FILL */ yytestcase(yyruleno == 140) + /* (141) colorproperty ::= COLOR */ yytestcase(yyruleno == 141) + /* (142) position ::= place */ yytestcase(yyruleno == 142) + /* (143) between ::= WAY BETWEEN */ yytestcase(yyruleno == 143) + /* (144) between ::= BETWEEN */ yytestcase(yyruleno == 144) + /* (145) between ::= OF THE WAY BETWEEN */ yytestcase(yyruleno == 145) + /* (146) place ::= place2 */ yytestcase(yyruleno == 146) + /* (147) edge ::= CENTER */ yytestcase(yyruleno == 147) + /* (148) edge ::= EDGEPT */ yytestcase(yyruleno == 148) + /* (149) edge ::= TOP */ yytestcase(yyruleno == 149) + /* (150) edge ::= BOTTOM */ yytestcase(yyruleno == 150) + /* (151) edge ::= START */ yytestcase(yyruleno == 151) + /* (152) edge ::= END */ yytestcase(yyruleno == 152) + /* (153) edge ::= RIGHT */ yytestcase(yyruleno == 153) + /* (154) edge ::= LEFT */ yytestcase(yyruleno == 154) + /* (155) object ::= objectname */ yytestcase(yyruleno == 155) + break + /********** End reduce actions ************************************************/ + } + assert(int(yyruleno) < len(yyRuleInfoLhs), "yyruleno < len(yyRuleInfoLhs)") + yygoto = yyRuleInfoLhs[yyruleno] + yysize = int(yyRuleInfoNRhs[yyruleno]) + yyact = yy_find_reduce_action(yypParser.yystack[yymsp+yysize].stateno, yygoto) + + /* There are no SHIFTREDUCE actions on nonterminals because the table + ** generator has simplified them to pure REDUCE actions. */ + assert(!(yyact > YY_MAX_SHIFT && yyact <= YY_MAX_SHIFTREDUCE), + "!(yyact > YY_MAX_SHIFT && yyact <= YY_MAX_SHIFTREDUCE)") + + /* It is not possible for a REDUCE to be followed by an error */ + assert(yyact != YY_ERROR_ACTION, "yyact != YY_ERROR_ACTION") + + yymsp += yysize + 1 + yypParser.yytos = yymsp + yypParser.yystack[yymsp].stateno = yyact + yypParser.yystack[yymsp].major = yygoto + yypParser.yyTraceShift(int(yyact), "... then shift") + return yyact +} + +/* +** The following code executes when the parse fails + */ +func (yypParser *yyParser) yy_parse_failed() { + + p := yypParser.p + _ = p + + if !NDEBUG { + if yyTraceFILE != nil { + fmt.Fprintf(yyTraceFILE, "%sFail!\n", yyTracePrompt) + } + } + for yypParser.yytos > 0 { + yypParser.yy_pop_parser_stack() + } + /* Here code is inserted which will be executed whenever the + ** parser fails */ + /************ Begin %parse_failure code ***************************************/ + + /************ End %parse_failure code *****************************************/ + /* Suppress warning about unused %extra_argument variable */ + yypParser.p = p + +} + +/* +** The following code executes when a syntax error first occurs. + */ +func (yypParser *yyParser) yy_syntax_error( + yymajor YYCODETYPE, /* The major type of the error token */ + yyminor pik_parserTOKENTYPE, /* The minor type of the error token */ +) { + + p := yypParser.p + _ = p + + TOKEN := yyminor + _ = TOKEN + /************ Begin %syntax_error code ****************************************/ +//line 481 "pikchr.y" + + if TOKEN.z != nil && TOKEN.z[0] != 0 { + p.pik_error(&TOKEN, "syntax error") + } else { + p.pik_error(nil, "syntax error") + } +//line 3026 "pikchr.go" + + /************ End %syntax_error code ******************************************/ + /* Suppress warning about unused %extra_argument variable */ + yypParser.p = p + +} + +/* +** The following is executed when the parser accepts + */ +func (yypParser *yyParser) yy_accept() { + + p := yypParser.p + _ = p + + if !NDEBUG { + if yyTraceFILE != nil { + fmt.Fprintf(yyTraceFILE, "%sAccept!\n", yyTracePrompt) + } + } + if !YYNOERRORRECOVERY { + yypParser.yyerrcnt = -1 + } + assert(yypParser.yytos == 0, fmt.Sprintf("want yypParser.yytos == 0; got %d", yypParser.yytos)) + /* Here code is inserted which will be executed whenever the + ** parser accepts */ + /*********** Begin %parse_accept code *****************************************/ + + /*********** End %parse_accept code *******************************************/ + /* Suppress warning about unused %extra_argument variable */ + yypParser.p = p + +} + +/* The main parser program. +** The first argument is a pointer to a structure obtained from +** "pik_parserAlloc" which describes the current state of the parser. +** The second argument is the major token number. The third is +** the minor token. The fourth optional argument is whatever the +** user wants (and specified in the grammar) and is available for +** use by the action routines. +** +** Inputs: +**
      +**
    • A pointer to the parser (an opaque structure.) +**
    • The major token number. +**
    • The minor token number. +**
    • An option argument of a grammar-specified type. +**
    +** +** Outputs: +** None. + */ +func (yypParser *yyParser) pik_parser( + yymajor YYCODETYPE, /* The major token code number */ + yyminor pik_parserTOKENTYPE, /* The value for the token */ + /* Optional %extra_argument parameter */ +) { + var ( + yyminorunion YYMINORTYPE + yyact YYACTIONTYPE /* The parser action. */ + yyendofinput bool /* True if we are at the end of input */ + yyerrorhit bool /* True if yymajor has invoked an error */ + ) + + p := yypParser.p + _ = p + + assert(yypParser.yystack != nil, "yypParser.yystack != nil") + if YYERRORSYMBOL == 0 && !YYNOERRORRECOVERY { + yyendofinput = (yymajor == 0) + } + + yyact = yypParser.yystack[yypParser.yytos].stateno + if !NDEBUG { + if yyTraceFILE != nil { + if yyact < YY_MIN_REDUCE { + fmt.Fprintf(yyTraceFILE, "%sInput '%s' in state %d\n", + yyTracePrompt, yyTokenName[yymajor], yyact) + } else { + fmt.Fprintf(yyTraceFILE, "%sInput '%s' with pending reduce %d\n", + yyTracePrompt, yyTokenName[yymajor], yyact-YY_MIN_REDUCE) + } + } + } + + for { /* Exit by "break" */ + assert(yypParser.yytos >= 0, "yypParser.yytos >= 0") + assert(yyact == yypParser.yystack[yypParser.yytos].stateno, "yyact == yypParser.yystack[yypParser.yytos].stateno") + yyact = yy_find_shift_action(yymajor, yyact) + if yyact >= YY_MIN_REDUCE { + yyruleno := yyact - YY_MIN_REDUCE /* Reduce by this rule */ + if !NDEBUG { + assert(int(yyruleno) < len(yyRuleName), "int(yyruleno) < len(yyRuleName)") + if yyTraceFILE != nil { + yysize := yyRuleInfoNRhs[yyruleno] + wea := " without external action" + if yyruleno < YYNRULE_WITH_ACTION { + wea = "" + } + if yysize != 0 { + fmt.Fprintf(yyTraceFILE, "%sReduce %d [%s]%s, pop back to state %d.\n", + yyTracePrompt, + yyruleno, yyRuleName[yyruleno], + wea, + yypParser.yystack[yypParser.yytos+int(yysize)].stateno) + } else { + fmt.Fprintf(yyTraceFILE, "%sReduce %d [%s]%s.\n", + yyTracePrompt, yyruleno, yyRuleName[yyruleno], + wea) + } + } + } /* NDEBUG */ + + /* Check that the stack is large enough to grow by a single entry + ** if the RHS of the rule is empty. This ensures that there is room + ** enough on the stack to push the LHS value */ + if yyRuleInfoNRhs[yyruleno] == 0 { + if YYTRACKMAXSTACKDEPTH { + if yypParser.yytos > yypParser.yyhwm { + yypParser.yyhwm++ + assert(yypParser.yyhwm == yypParser.yytos, "yypParser.yyhwm == yypParser.yytos") + } + } + if YYSTACKDEPTH > 0 { + if yypParser.yytos >= YYSTACKDEPTH-1 { + yypParser.yyStackOverflow() + break + } + } else { + if yypParser.yytos+1 >= len(yypParser.yystack)-1 { + yypParser.yyGrowStack() + } + } + } + yyact = yypParser.yy_reduce(yyruleno, yymajor, yyminor, + p) + } else if yyact <= YY_MAX_SHIFTREDUCE { + yypParser.yy_shift(yyact, yymajor, yyminor) + if !YYNOERRORRECOVERY { + yypParser.yyerrcnt-- + } + break + } else if yyact == YY_ACCEPT_ACTION { + yypParser.yytos-- + yypParser.yy_accept() + return + } else { + assert(yyact == YY_ERROR_ACTION, "yyact == YY_ERROR_ACTION") + yyminorunion.yy0 = yyminor + + if !NDEBUG { + if yyTraceFILE != nil { + fmt.Fprintf(yyTraceFILE, "%sSyntax Error!\n", yyTracePrompt) + } + } + if YYERRORSYMBOL > 0 { + /* A syntax error has occurred. + ** The response to an error depends upon whether or not the + ** grammar defines an error token "ERROR". + ** + ** This is what we do if the grammar does define ERROR: + ** + ** * Call the %syntax_error function. + ** + ** * Begin popping the stack until we enter a state where + ** it is legal to shift the error symbol, then shift + ** the error symbol. + ** + ** * Set the error count to three. + ** + ** * Begin accepting and shifting new tokens. No new error + ** processing will occur until three tokens have been + ** shifted successfully. + ** + */ + if yypParser.yyerrcnt < 0 { + yypParser.yy_syntax_error(yymajor, yyminor) + } + yymx := yypParser.yystack[yypParser.yytos].major + if int(yymx) == YYERRORSYMBOL || yyerrorhit { + if !NDEBUG { + if yyTraceFILE != nil { + fmt.Fprintf(yyTraceFILE, "%sDiscard input token %s\n", + yyTracePrompt, yyTokenName[yymajor]) + } + } + yypParser.yy_destructor(yymajor, &yyminorunion) + yymajor = YYNOCODE + } else { + for yypParser.yytos > 0 { + yyact = yy_find_reduce_action(yypParser.yystack[yypParser.yytos].stateno, + YYERRORSYMBOL) + if yyact <= YY_MAX_SHIFTREDUCE { + break + } + yypParser.yy_pop_parser_stack() + } + if yypParser.yytos <= 0 || yymajor == 0 { + yypParser.yy_destructor(yymajor, &yyminorunion) + yypParser.yy_parse_failed() + if !YYNOERRORRECOVERY { + yypParser.yyerrcnt = -1 + } + yymajor = YYNOCODE + } else if yymx != YYERRORSYMBOL { + yypParser.yy_shift(yyact, YYERRORSYMBOL, yyminor) + } + } + yypParser.yyerrcnt = 3 + yyerrorhit = true + if yymajor == YYNOCODE { + break + } + yyact = yypParser.yystack[yypParser.yytos].stateno + } else if YYNOERRORRECOVERY { + /* If the YYNOERRORRECOVERY macro is defined, then do not attempt to + ** do any kind of error recovery. Instead, simply invoke the syntax + ** error routine and continue going as if nothing had happened. + ** + ** Applications can set this macro (for example inside %include) if + ** they intend to abandon the parse upon the first syntax error seen. + */ + yypParser.yy_syntax_error(yymajor, yyminor) + yypParser.yy_destructor(yymajor, &yyminorunion) + break + } else { /* YYERRORSYMBOL is not defined */ + /* This is what we do if the grammar does not define ERROR: + ** + ** * Report an error message, and throw away the input token. + ** + ** * If the input token is $, then fail the parse. + ** + ** As before, subsequent error messages are suppressed until + ** three input tokens have been successfully shifted. + */ + if yypParser.yyerrcnt <= 0 { + yypParser.yy_syntax_error(yymajor, yyminor) + } + yypParser.yyerrcnt = 3 + yypParser.yy_destructor(yymajor, &yyminorunion) + if yyendofinput { + yypParser.yy_parse_failed() + if !YYNOERRORRECOVERY { + yypParser.yyerrcnt = -1 + } + } + break + } + } + } + if !NDEBUG { + if yyTraceFILE != nil { + cDiv := '[' + fmt.Fprintf(yyTraceFILE, "%sReturn. Stack=", yyTracePrompt) + for _, i := range yypParser.yystack[1 : yypParser.yytos+1] { + fmt.Fprintf(yyTraceFILE, "%c%s", cDiv, yyTokenName[i.major]) + cDiv = ' ' + } + fmt.Fprintf(yyTraceFILE, "]\n") + } + } + return +} + +/* +** Return the fallback token corresponding to canonical token iToken, or +** 0 if iToken has no fallback. + */ +func pik_parserFallback(iToken int) YYCODETYPE { + if YYFALLBACK { + assert(iToken < len(yyFallback), "iToken < len(yyFallback)") + return yyFallback[iToken] + } else { + return 0 + } +} + +// assert is used in various places in the generated and template code +// to check invariants. +func assert(condition bool, message string) { + if !condition { + panic(message) + } +} + +//line 724 "pikchr.y" + +/* Chart of the 148 official CSS color names with their +** corresponding RGB values thru Color Module Level 4: +** https://developer.mozilla.org/en-US/docs/Web/CSS/color_value +** +** Two new names "None" and "Off" are added with a value +** of -1. + */ +var aColor = []struct { + zName string /* Name of the color */ + val int /* RGB value */ +}{ + {"AliceBlue", 0xf0f8ff}, + {"AntiqueWhite", 0xfaebd7}, + {"Aqua", 0x00ffff}, + {"Aquamarine", 0x7fffd4}, + {"Azure", 0xf0ffff}, + {"Beige", 0xf5f5dc}, + {"Bisque", 0xffe4c4}, + {"Black", 0x000000}, + {"BlanchedAlmond", 0xffebcd}, + {"Blue", 0x0000ff}, + {"BlueViolet", 0x8a2be2}, + {"Brown", 0xa52a2a}, + {"BurlyWood", 0xdeb887}, + {"CadetBlue", 0x5f9ea0}, + {"Chartreuse", 0x7fff00}, + {"Chocolate", 0xd2691e}, + {"Coral", 0xff7f50}, + {"CornflowerBlue", 0x6495ed}, + {"Cornsilk", 0xfff8dc}, + {"Crimson", 0xdc143c}, + {"Cyan", 0x00ffff}, + {"DarkBlue", 0x00008b}, + {"DarkCyan", 0x008b8b}, + {"DarkGoldenrod", 0xb8860b}, + {"DarkGray", 0xa9a9a9}, + {"DarkGreen", 0x006400}, + {"DarkGrey", 0xa9a9a9}, + {"DarkKhaki", 0xbdb76b}, + {"DarkMagenta", 0x8b008b}, + {"DarkOliveGreen", 0x556b2f}, + {"DarkOrange", 0xff8c00}, + {"DarkOrchid", 0x9932cc}, + {"DarkRed", 0x8b0000}, + {"DarkSalmon", 0xe9967a}, + {"DarkSeaGreen", 0x8fbc8f}, + {"DarkSlateBlue", 0x483d8b}, + {"DarkSlateGray", 0x2f4f4f}, + {"DarkSlateGrey", 0x2f4f4f}, + {"DarkTurquoise", 0x00ced1}, + {"DarkViolet", 0x9400d3}, + {"DeepPink", 0xff1493}, + {"DeepSkyBlue", 0x00bfff}, + {"DimGray", 0x696969}, + {"DimGrey", 0x696969}, + {"DodgerBlue", 0x1e90ff}, + {"Firebrick", 0xb22222}, + {"FloralWhite", 0xfffaf0}, + {"ForestGreen", 0x228b22}, + {"Fuchsia", 0xff00ff}, + {"Gainsboro", 0xdcdcdc}, + {"GhostWhite", 0xf8f8ff}, + {"Gold", 0xffd700}, + {"Goldenrod", 0xdaa520}, + {"Gray", 0x808080}, + {"Green", 0x008000}, + {"GreenYellow", 0xadff2f}, + {"Grey", 0x808080}, + {"Honeydew", 0xf0fff0}, + {"HotPink", 0xff69b4}, + {"IndianRed", 0xcd5c5c}, + {"Indigo", 0x4b0082}, + {"Ivory", 0xfffff0}, + {"Khaki", 0xf0e68c}, + {"Lavender", 0xe6e6fa}, + {"LavenderBlush", 0xfff0f5}, + {"LawnGreen", 0x7cfc00}, + {"LemonChiffon", 0xfffacd}, + {"LightBlue", 0xadd8e6}, + {"LightCoral", 0xf08080}, + {"LightCyan", 0xe0ffff}, + {"LightGoldenrodYellow", 0xfafad2}, + {"LightGray", 0xd3d3d3}, + {"LightGreen", 0x90ee90}, + {"LightGrey", 0xd3d3d3}, + {"LightPink", 0xffb6c1}, + {"LightSalmon", 0xffa07a}, + {"LightSeaGreen", 0x20b2aa}, + {"LightSkyBlue", 0x87cefa}, + {"LightSlateGray", 0x778899}, + {"LightSlateGrey", 0x778899}, + {"LightSteelBlue", 0xb0c4de}, + {"LightYellow", 0xffffe0}, + {"Lime", 0x00ff00}, + {"LimeGreen", 0x32cd32}, + {"Linen", 0xfaf0e6}, + {"Magenta", 0xff00ff}, + {"Maroon", 0x800000}, + {"MediumAquamarine", 0x66cdaa}, + {"MediumBlue", 0x0000cd}, + {"MediumOrchid", 0xba55d3}, + {"MediumPurple", 0x9370db}, + {"MediumSeaGreen", 0x3cb371}, + {"MediumSlateBlue", 0x7b68ee}, + {"MediumSpringGreen", 0x00fa9a}, + {"MediumTurquoise", 0x48d1cc}, + {"MediumVioletRed", 0xc71585}, + {"MidnightBlue", 0x191970}, + {"MintCream", 0xf5fffa}, + {"MistyRose", 0xffe4e1}, + {"Moccasin", 0xffe4b5}, + {"NavajoWhite", 0xffdead}, + {"Navy", 0x000080}, + {"None", -1}, /* Non-standard addition */ + {"Off", -1}, /* Non-standard addition */ + {"OldLace", 0xfdf5e6}, + {"Olive", 0x808000}, + {"OliveDrab", 0x6b8e23}, + {"Orange", 0xffa500}, + {"OrangeRed", 0xff4500}, + {"Orchid", 0xda70d6}, + {"PaleGoldenrod", 0xeee8aa}, + {"PaleGreen", 0x98fb98}, + {"PaleTurquoise", 0xafeeee}, + {"PaleVioletRed", 0xdb7093}, + {"PapayaWhip", 0xffefd5}, + {"PeachPuff", 0xffdab9}, + {"Peru", 0xcd853f}, + {"Pink", 0xffc0cb}, + {"Plum", 0xdda0dd}, + {"PowderBlue", 0xb0e0e6}, + {"Purple", 0x800080}, + {"RebeccaPurple", 0x663399}, + {"Red", 0xff0000}, + {"RosyBrown", 0xbc8f8f}, + {"RoyalBlue", 0x4169e1}, + {"SaddleBrown", 0x8b4513}, + {"Salmon", 0xfa8072}, + {"SandyBrown", 0xf4a460}, + {"SeaGreen", 0x2e8b57}, + {"Seashell", 0xfff5ee}, + {"Sienna", 0xa0522d}, + {"Silver", 0xc0c0c0}, + {"SkyBlue", 0x87ceeb}, + {"SlateBlue", 0x6a5acd}, + {"SlateGray", 0x708090}, + {"SlateGrey", 0x708090}, + {"Snow", 0xfffafa}, + {"SpringGreen", 0x00ff7f}, + {"SteelBlue", 0x4682b4}, + {"Tan", 0xd2b48c}, + {"Teal", 0x008080}, + {"Thistle", 0xd8bfd8}, + {"Tomato", 0xff6347}, + {"Turquoise", 0x40e0d0}, + {"Violet", 0xee82ee}, + {"Wheat", 0xf5deb3}, + {"White", 0xffffff}, + {"WhiteSmoke", 0xf5f5f5}, + {"Yellow", 0xffff00}, + {"YellowGreen", 0x9acd32}, +} + +/* Built-in variable names. +** +** This array is constant. When a script changes the value of one of +** these built-ins, a new PVar record is added at the head of +** the Pik.pVar list, which is searched first. Thus the new PVar entry +** will override this default value. +** +** Units are in inches, except for "color" and "fill" which are +** interpreted as 24-bit RGB values. +** +** Binary search used. Must be kept in sorted order. + */ + +var aBuiltin = []struct { + zName string + val PNum +}{ + {"arcrad", 0.25}, + {"arrowhead", 2.0}, + {"arrowht", 0.08}, + {"arrowwid", 0.06}, + {"boxht", 0.5}, + {"boxrad", 0.0}, + {"boxwid", 0.75}, + {"charht", 0.14}, + {"charwid", 0.08}, + {"circlerad", 0.25}, + {"color", 0.0}, + {"cylht", 0.5}, + {"cylrad", 0.075}, + {"cylwid", 0.75}, + {"dashwid", 0.05}, + {"dotrad", 0.015}, + {"ellipseht", 0.5}, + {"ellipsewid", 0.75}, + {"fileht", 0.75}, + {"filerad", 0.15}, + {"filewid", 0.5}, + {"fill", -1.0}, + {"lineht", 0.5}, + {"linewid", 0.5}, + {"movewid", 0.5}, + {"ovalht", 0.5}, + {"ovalwid", 1.0}, + {"scale", 1.0}, + {"textht", 0.5}, + {"textwid", 0.75}, + {"thickness", 0.015}, +} + +/* Methods for the "arc" class */ +func arcInit(p *Pik, pObj *PObj) { + pObj.w = p.pik_value("arcrad", nil) + pObj.h = pObj.w +} + +/* Hack: Arcs are here rendered as quadratic Bezier curves rather +** than true arcs. Multiple reasons: (1) the legacy-PIC parameters +** that control arcs are obscure and I could not figure out what they +** mean based on available documentation. (2) Arcs are rarely used, +** and so do not seem that important. + */ +func arcControlPoint(cw bool, f PPoint, t PPoint, rScale PNum) PPoint { + var m PPoint + var dx, dy PNum + m.x = 0.5 * (f.x + t.x) + m.y = 0.5 * (f.y + t.y) + dx = t.x - f.x + dy = t.y - f.y + if cw { + m.x -= 0.5 * rScale * dy + m.y += 0.5 * rScale * dx + } else { + m.x += 0.5 * rScale * dy + m.y -= 0.5 * rScale * dx + } + return m +} +func arcCheck(p *Pik, pObj *PObj) { + if p.nTPath > 2 { + p.pik_error(&pObj.errTok, "arc geometry error") + return + } + m := arcControlPoint(pObj.cw, p.aTPath[0], p.aTPath[1], 0.5) + pik_bbox_add_xy(&pObj.bbox, m.x, m.y) +} +func arcRender(p *Pik, pObj *PObj) { + if pObj.nPath < 2 { + return + } + if pObj.sw <= 0.0 { + return + } + f := pObj.aPath[0] + t := pObj.aPath[1] + m := arcControlPoint(pObj.cw, f, t, 1.0) + if pObj.larrow { + p.pik_draw_arrowhead(&m, &f, pObj) + } + if pObj.rarrow { + p.pik_draw_arrowhead(&m, &t, pObj) + } + p.pik_append_xy("\n") + + p.pik_append_txt(pObj, nil) +} + +/* Methods for the "arrow" class */ +func arrowInit(p *Pik, pObj *PObj) { + pObj.w = p.pik_value("linewid", nil) + pObj.h = p.pik_value("lineht", nil) + pObj.rad = p.pik_value("linerad", nil) + pObj.rarrow = true +} + +/* Methods for the "box" class */ +func boxInit(p *Pik, pObj *PObj) { + pObj.w = p.pik_value("boxwid", nil) + pObj.h = p.pik_value("boxht", nil) + pObj.rad = p.pik_value("boxrad", nil) +} + +/* Return offset from the center of the box to the compass point +** given by parameter cp */ +func boxOffset(p *Pik, pObj *PObj, cp uint8) PPoint { + pt := PPoint{} + var w2 PNum = 0.5 * pObj.w + var h2 PNum = 0.5 * pObj.h + var rad PNum = pObj.rad + var rx PNum + if rad <= 0.0 { + rx = 0.0 + } else { + if rad > w2 { + rad = w2 + } + if rad > h2 { + rad = h2 + } + rx = 0.29289321881345252392 * rad + } + switch cp { + case CP_C: + case CP_N: + pt.x = 0.0 + pt.y = h2 + case CP_NE: + pt.x = w2 - rx + pt.y = h2 - rx + case CP_E: + pt.x = w2 + pt.y = 0.0 + case CP_SE: + pt.x = w2 - rx + pt.y = rx - h2 + case CP_S: + pt.x = 0.0 + pt.y = -h2 + case CP_SW: + pt.x = rx - w2 + pt.y = rx - h2 + case CP_W: + pt.x = -w2 + pt.y = 0.0 + case CP_NW: + pt.x = rx - w2 + pt.y = h2 - rx + default: + assert(false, "false") + } + return pt +} +func boxChop(p *Pik, pObj *PObj, pPt *PPoint) PPoint { + var dx, dy PNum + cp := CP_C + chop := pObj.ptAt + if pObj.w <= 0.0 { + return chop + } + if pObj.h <= 0.0 { + return chop + } + dx = (pPt.x - pObj.ptAt.x) * pObj.h / pObj.w + dy = (pPt.y - pObj.ptAt.y) + if dx > 0.0 { + if dy >= 2.414*dx { + cp = CP_N + } else if dy >= 0.414*dx { + cp = CP_NE + } else if dy >= -0.414*dx { + cp = CP_E + } else if dy > -2.414*dx { + cp = CP_SE + } else { + cp = CP_S + } + } else { + if dy >= -2.414*dx { + cp = CP_N + } else if dy >= -0.414*dx { + cp = CP_NW + } else if dy >= 0.414*dx { + cp = CP_W + } else if dy > 2.414*dx { + cp = CP_SW + } else { + cp = CP_S + } + } + chop = pObj.typ.xOffset(p, pObj, cp) + chop.x += pObj.ptAt.x + chop.y += pObj.ptAt.y + return chop +} +func boxFit(p *Pik, pObj *PObj, w PNum, h PNum) { + if w > 0 { + pObj.w = w + } + if h > 0 { + pObj.h = h + } +} +func boxRender(p *Pik, pObj *PObj) { + var w2 PNum = 0.5 * pObj.w + var h2 PNum = 0.5 * pObj.h + rad := pObj.rad + pt := pObj.ptAt + if pObj.sw > 0.0 { + if rad <= 0.0 { + p.pik_append_xy(" w2 { + rad = w2 + } + if rad > h2 { + rad = h2 + } + var x0 PNum = pt.x - w2 + var x1 PNum = x0 + rad + var x3 PNum = pt.x + w2 + var x2 PNum = x3 - rad + var y0 PNum = pt.y - h2 + var y1 PNum = y0 + rad + var y3 PNum = pt.y + h2 + var y2 PNum = y3 - rad + p.pik_append_xy(" x1 { + p.pik_append_xy("L", x2, y0) + } + p.pik_append_arc(rad, rad, x3, y1) + if y2 > y1 { + p.pik_append_xy("L", x3, y2) + } + p.pik_append_arc(rad, rad, x2, y3) + if x2 > x1 { + p.pik_append_xy("L", x1, y3) + } + p.pik_append_arc(rad, rad, x0, y2) + if y2 > y1 { + p.pik_append_xy("L", x0, y1) + } + p.pik_append_arc(rad, rad, x1, y0) + p.pik_append("Z\" ") + } + p.pik_append_style(pObj, 3) + p.pik_append("\" />\n") + } + p.pik_append_txt(pObj, nil) +} + +/* Methods for the "circle" class */ +func circleInit(p *Pik, pObj *PObj) { + pObj.w = p.pik_value("circlerad", nil) * 2 + pObj.h = pObj.w + pObj.rad = 0.5 * pObj.w +} +func circleNumProp(p *Pik, pObj *PObj, pId *PToken) { + /* For a circle, the width must equal the height and both must + ** be twice the radius. Enforce those constraints. */ + switch pId.eType { + case T_RADIUS: + pObj.w = 2.0 * pObj.rad + pObj.h = 2.0 * pObj.rad + case T_WIDTH: + pObj.h = pObj.w + pObj.rad = 0.5 * pObj.w + case T_HEIGHT: + pObj.w = pObj.h + pObj.rad = 0.5 * pObj.w + } +} +func circleChop(p *Pik, pObj *PObj, pPt *PPoint) PPoint { + var chop PPoint + var dx PNum = pPt.x - pObj.ptAt.x + var dy PNum = pPt.y - pObj.ptAt.y + var dist PNum = math.Hypot(dx, dy) + if dist < pObj.rad || dist <= 0 { + return pObj.ptAt + } + chop.x = pObj.ptAt.x + dx*pObj.rad/dist + chop.y = pObj.ptAt.y + dy*pObj.rad/dist + return chop +} +func circleFit(p *Pik, pObj *PObj, w PNum, h PNum) { + var mx PNum = 0.0 + if w > 0 { + mx = w + } + if h > mx { + mx = h + } + if w*h > 0 && (w*w+h*h) > mx*mx { + mx = math.Hypot(w, h) + } + if mx > 0.0 { + pObj.rad = 0.5 * mx + pObj.w = mx + pObj.h = mx + } +} + +func circleRender(p *Pik, pObj *PObj) { + r := pObj.rad + pt := pObj.ptAt + if pObj.sw > 0.0 { + p.pik_append_x("\n") + } + p.pik_append_txt(pObj, nil) +} + +/* Methods for the "cylinder" class */ +func cylinderInit(p *Pik, pObj *PObj) { + pObj.w = p.pik_value("cylwid", nil) + pObj.h = p.pik_value("cylht", nil) + pObj.rad = p.pik_value("cylrad", nil) /* Minor radius of ellipses */ +} +func cylinderFit(p *Pik, pObj *PObj, w PNum, h PNum) { + if w > 0 { + pObj.w = w + } + if h > 0 { + pObj.h = h + 0.25*pObj.rad + pObj.sw + } +} +func cylinderRender(p *Pik, pObj *PObj) { + var w2 PNum = 0.5 * pObj.w + var h2 PNum = 0.5 * pObj.h + rad := pObj.rad + pt := pObj.ptAt + if pObj.sw > 0.0 { + if rad > h2 { + rad = h2 + } else if rad < 0 { + rad = 0 + } + p.pik_append_xy("\n") + } + p.pik_append_txt(pObj, nil) +} +func cylinderOffset(p *Pik, pObj *PObj, cp uint8) PPoint { + pt := PPoint{} + var w2 PNum = pObj.w * 0.5 + var h1 PNum = pObj.h * 0.5 + var h2 PNum = h1 - pObj.rad + switch cp { + case CP_C: + case CP_N: + pt.x = 0.0 + pt.y = h1 + case CP_NE: + pt.x = w2 + pt.y = h2 + case CP_E: + pt.x = w2 + pt.y = 0.0 + case CP_SE: + pt.x = w2 + pt.y = -h2 + case CP_S: + pt.x = 0.0 + pt.y = -h1 + case CP_SW: + pt.x = -w2 + pt.y = -h2 + case CP_W: + pt.x = -w2 + pt.y = 0.0 + case CP_NW: + pt.x = -w2 + pt.y = h2 + default: + assert(false, "false") + } + return pt +} + +/* Methods for the "dot" class */ +func dotInit(p *Pik, pObj *PObj) { + pObj.rad = p.pik_value("dotrad", nil) + pObj.h = pObj.rad * 6 + pObj.w = pObj.rad * 6 + pObj.fill = pObj.color +} +func dotNumProp(p *Pik, pObj *PObj, pId *PToken) { + switch pId.eType { + case T_COLOR: + pObj.fill = pObj.color + case T_FILL: + pObj.color = pObj.fill + } +} +func dotCheck(p *Pik, pObj *PObj) { + pObj.w = 0 + pObj.h = 0 + pik_bbox_addellipse(&pObj.bbox, pObj.ptAt.x, pObj.ptAt.y, + pObj.rad, pObj.rad) +} +func dotOffset(p *Pik, pObj *PObj, cp uint8) PPoint { + return PPoint{} +} +func dotRender(p *Pik, pObj *PObj) { + r := pObj.rad + pt := pObj.ptAt + if pObj.sw > 0.0 { + p.pik_append_x("\n") + } + p.pik_append_txt(pObj, nil) +} + +/* Methods for the "ellipse" class */ +func ellipseInit(p *Pik, pObj *PObj) { + pObj.w = p.pik_value("ellipsewid", nil) + pObj.h = p.pik_value("ellipseht", nil) +} +func ellipseChop(p *Pik, pObj *PObj, pPt *PPoint) PPoint { + var chop PPoint + var s, dq, dist PNum + var dx PNum = pPt.x - pObj.ptAt.x + var dy PNum = pPt.y - pObj.ptAt.y + if pObj.w <= 0.0 { + return pObj.ptAt + } + if pObj.h <= 0.0 { + return pObj.ptAt + } + s = pObj.h / pObj.w + dq = dx * s + dist = math.Hypot(dq, dy) + if dist < pObj.h { + return pObj.ptAt + } + chop.x = pObj.ptAt.x + 0.5*dq*pObj.h/(dist*s) + chop.y = pObj.ptAt.y + 0.5*dy*pObj.h/dist + return chop +} +func ellipseOffset(p *Pik, pObj *PObj, cp uint8) PPoint { + pt := PPoint{} + var w PNum = pObj.w * 0.5 + var w2 PNum = w * 0.70710678118654747608 + var h PNum = pObj.h * 0.5 + var h2 PNum = h * 0.70710678118654747608 + switch cp { + case CP_C: + case CP_N: + pt.x = 0.0 + pt.y = h + case CP_NE: + pt.x = w2 + pt.y = h2 + case CP_E: + pt.x = w + pt.y = 0.0 + case CP_SE: + pt.x = w2 + pt.y = -h2 + case CP_S: + pt.x = 0.0 + pt.y = -h + case CP_SW: + pt.x = -w2 + pt.y = -h2 + case CP_W: + pt.x = -w + pt.y = 0.0 + case CP_NW: + pt.x = -w2 + pt.y = h2 + default: + assert(false, "false") + } + return pt +} +func ellipseRender(p *Pik, pObj *PObj) { + w := pObj.w + h := pObj.h + pt := pObj.ptAt + if pObj.sw > 0.0 { + p.pik_append_x("\n") + } + p.pik_append_txt(pObj, nil) +} + +/* Methods for the "file" object */ +func fileInit(p *Pik, pObj *PObj) { + pObj.w = p.pik_value("filewid", nil) + pObj.h = p.pik_value("fileht", nil) + pObj.rad = p.pik_value("filerad", nil) +} + +/* Return offset from the center of the file to the compass point +** given by parameter cp */ +func fileOffset(p *Pik, pObj *PObj, cp uint8) PPoint { + pt := PPoint{} + var w2 PNum = 0.5 * pObj.w + var h2 PNum = 0.5 * pObj.h + var rx PNum = pObj.rad + mn := h2 + if w2 < h2 { + mn = w2 + } + if rx > mn { + rx = mn + } + if rx < mn*0.25 { + rx = mn * 0.25 + } + pt.x = 0.0 + pt.y = 0.0 + rx *= 0.5 + switch cp { + case CP_C: + case CP_N: + pt.x = 0.0 + pt.y = h2 + case CP_NE: + pt.x = w2 - rx + pt.y = h2 - rx + case CP_E: + pt.x = w2 + pt.y = 0.0 + case CP_SE: + pt.x = w2 + pt.y = -h2 + case CP_S: + pt.x = 0.0 + pt.y = -h2 + case CP_SW: + pt.x = -w2 + pt.y = -h2 + case CP_W: + pt.x = -w2 + pt.y = 0.0 + case CP_NW: + pt.x = -w2 + pt.y = h2 + default: + assert(false, "false") + } + return pt +} +func fileFit(p *Pik, pObj *PObj, w PNum, h PNum) { + if w > 0 { + pObj.w = w + } + if h > 0 { + pObj.h = h + 2*pObj.rad + } +} +func fileRender(p *Pik, pObj *PObj) { + var w2 PNum = 0.5 * pObj.w + var h2 PNum = 0.5 * pObj.h + rad := pObj.rad + pt := pObj.ptAt + mn := h2 + if w2 < h2 { + mn = w2 + } + if rad > mn { + rad = mn + } + if rad < mn*0.25 { + rad = mn * 0.25 + } + if pObj.sw > 0.0 { + p.pik_append_xy("\n") + p.pik_append_xy("\n") + } + p.pik_append_txt(pObj, nil) +} + +/* Methods for the "line" class */ +func lineInit(p *Pik, pObj *PObj) { + pObj.w = p.pik_value("linewid", nil) + pObj.h = p.pik_value("lineht", nil) + pObj.rad = p.pik_value("linerad", nil) +} +func lineOffset(p *Pik, pObj *PObj, cp uint8) PPoint { + if false { // #if 0 + /* In legacy PIC, the .center of an unclosed line is half way between + ** its .start and .end. */ + if cp == CP_C && !pObj.bClose { + var out PPoint + out.x = 0.5*(pObj.ptEnter.x+pObj.ptExit.x) - pObj.ptAt.x + out.y = 0.5*(pObj.ptEnter.x+pObj.ptExit.y) - pObj.ptAt.y + return out + } + } // #endif + return boxOffset(p, pObj, cp) +} +func lineRender(p *Pik, pObj *PObj) { + if pObj.sw > 0.0 { + z := "\n") + } + p.pik_append_txt(pObj, nil) +} + +/* Methods for the "move" class */ +func moveInit(p *Pik, pObj *PObj) { + pObj.w = p.pik_value("movewid", nil) + pObj.h = pObj.w + pObj.fill = -1.0 + pObj.color = -1.0 + pObj.sw = -1.0 +} +func moveRender(p *Pik, pObj *PObj) { + /* No-op */ +} + +/* Methods for the "oval" class */ +func ovalInit(p *Pik, pObj *PObj) { + pObj.h = p.pik_value("ovalht", nil) + pObj.w = p.pik_value("ovalwid", nil) + if pObj.h < pObj.w { + pObj.rad = 0.5 * pObj.h + } else { + pObj.rad = 0.5 * pObj.w + } +} +func ovalNumProp(p *Pik, pObj *PObj, pId *PToken) { + /* Always adjust the radius to be half of the smaller of + ** the width and height. */ + if pObj.h < pObj.w { + pObj.rad = 0.5 * pObj.h + } else { + pObj.rad = 0.5 * pObj.w + } +} +func ovalFit(p *Pik, pObj *PObj, w PNum, h PNum) { + if w > 0 { + pObj.w = w + } + if h > 0 { + pObj.h = h + } + if pObj.w < pObj.h { + pObj.w = pObj.h + } + if pObj.h < pObj.w { + pObj.rad = 0.5 * pObj.h + } else { + pObj.rad = 0.5 * pObj.w + } +} + +/* Methods for the "spline" class */ +func splineInit(p *Pik, pObj *PObj) { + pObj.w = p.pik_value("linewid", nil) + pObj.h = p.pik_value("lineht", nil) + pObj.rad = 1000 +} + +/* Return a point along the path from "f" to "t" that is r units +** prior to reaching "t", except if the path is less than 2*r total, +** return the midpoint. + */ +func radiusMidpoint(f PPoint, t PPoint, r PNum, pbMid *bool) PPoint { + var dx PNum = t.x - f.x + var dy PNum = t.y - f.y + var dist PNum = math.Hypot(dx, dy) + if dist <= 0.0 { + return t + } + dx /= dist + dy /= dist + if r > 0.5*dist { + r = 0.5 * dist + *pbMid = true + } else { + *pbMid = false + } + return PPoint{ + x: t.x - r*dx, + y: t.y - r*dy, + } +} +func (p *Pik) radiusPath(pObj *PObj, r PNum) { + n := pObj.nPath + a := pObj.aPath + an := a[n-1] + isMid := false + iLast := n - 1 + if pObj.bClose { + iLast = n + } + + p.pik_append_xy("\n") +} +func splineRender(p *Pik, pObj *PObj) { + if pObj.sw > 0.0 { + n := pObj.nPath + r := pObj.rad + if n < 3 || r <= 0.0 { + lineRender(p, pObj) + return + } + if pObj.larrow { + p.pik_draw_arrowhead(&pObj.aPath[1], &pObj.aPath[0], pObj) + } + if pObj.rarrow { + p.pik_draw_arrowhead(&pObj.aPath[n-2], &pObj.aPath[n-1], pObj) + } + p.radiusPath(pObj, pObj.rad) + } + p.pik_append_txt(pObj, nil) +} + +/* Methods for the "text" class */ +func textInit(p *Pik, pObj *PObj) { + p.pik_value("textwid", nil) + p.pik_value("textht", nil) + pObj.sw = 0.0 +} +func textOffset(p *Pik, pObj *PObj, cp uint8) PPoint { + /* Automatically slim-down the width and height of text + ** statements so that the bounding box tightly encloses the text, + ** then get boxOffset() to do the offset computation. + */ + p.pik_size_to_fit(&pObj.errTok, 3) + return boxOffset(p, pObj, cp) +} + +/* Methods for the "sublist" class */ +func sublistInit(p *Pik, pObj *PObj) { + pList := pObj.pSublist + pik_bbox_init(&pObj.bbox) + for i := 0; i < len(pList); i++ { + pik_bbox_addbox(&pObj.bbox, &pList[i].bbox) + } + pObj.w = pObj.bbox.ne.x - pObj.bbox.sw.x + pObj.h = pObj.bbox.ne.y - pObj.bbox.sw.y + pObj.ptAt.x = 0.5 * (pObj.bbox.ne.x + pObj.bbox.sw.x) + pObj.ptAt.y = 0.5 * (pObj.bbox.ne.y + pObj.bbox.sw.y) + pObj.mCalc |= A_WIDTH | A_HEIGHT | A_RADIUS +} + +/* +** The following array holds all the different kinds of objects. +** The special [] object is separate. + */ +var aClass = []PClass{ + { + zName: "arc", + isLine: true, + eJust: 0, + xInit: arcInit, + xNumProp: nil, + xCheck: arcCheck, + xChop: nil, + xOffset: boxOffset, + xFit: nil, + xRender: arcRender, + }, + { + zName: "arrow", + isLine: true, + eJust: 0, + xInit: arrowInit, + xNumProp: nil, + xCheck: nil, + xChop: nil, + xOffset: lineOffset, + xFit: nil, + xRender: splineRender, + }, + { + zName: "box", + isLine: false, + eJust: 1, + xInit: boxInit, + xNumProp: nil, + xCheck: nil, + xChop: boxChop, + xOffset: boxOffset, + xFit: boxFit, + xRender: boxRender, + }, + { + zName: "circle", + isLine: false, + eJust: 0, + xInit: circleInit, + xNumProp: circleNumProp, + xCheck: nil, + xChop: circleChop, + xOffset: ellipseOffset, + xFit: circleFit, + xRender: circleRender, + }, + { + zName: "cylinder", + isLine: false, + eJust: 1, + xInit: cylinderInit, + xNumProp: nil, + xCheck: nil, + xChop: boxChop, + xOffset: cylinderOffset, + xFit: cylinderFit, + xRender: cylinderRender, + }, + { + zName: "dot", + isLine: false, + eJust: 0, + xInit: dotInit, + xNumProp: dotNumProp, + xCheck: dotCheck, + xChop: circleChop, + xOffset: dotOffset, + xFit: nil, + xRender: dotRender, + }, + { + zName: "ellipse", + isLine: false, + eJust: 0, + xInit: ellipseInit, + xNumProp: nil, + xCheck: nil, + xChop: ellipseChop, + xOffset: ellipseOffset, + xFit: boxFit, + xRender: ellipseRender, + }, + { + zName: "file", + isLine: false, + eJust: 1, + xInit: fileInit, + xNumProp: nil, + xCheck: nil, + xChop: boxChop, + xOffset: fileOffset, + xFit: fileFit, + xRender: fileRender, + }, + { + zName: "line", + isLine: true, + eJust: 0, + xInit: lineInit, + xNumProp: nil, + xCheck: nil, + xChop: nil, + xOffset: lineOffset, + xFit: nil, + xRender: splineRender, + }, + { + zName: "move", + isLine: true, + eJust: 0, + xInit: moveInit, + xNumProp: nil, + xCheck: nil, + xChop: nil, + xOffset: boxOffset, + xFit: nil, + xRender: moveRender, + }, + { + zName: "oval", + isLine: false, + eJust: 1, + xInit: ovalInit, + xNumProp: ovalNumProp, + xCheck: nil, + xChop: boxChop, + xOffset: boxOffset, + xFit: ovalFit, + xRender: boxRender, + }, + { + zName: "spline", + isLine: true, + eJust: 0, + xInit: splineInit, + xNumProp: nil, + xCheck: nil, + xChop: nil, + xOffset: lineOffset, + xFit: nil, + xRender: splineRender, + }, + { + zName: "text", + isLine: false, + eJust: 0, + xInit: textInit, + xNumProp: nil, + xCheck: nil, + xChop: boxChop, + xOffset: textOffset, + xFit: boxFit, + xRender: boxRender, + }, +} +var sublistClass = PClass{ + zName: "[]", + isLine: false, + eJust: 0, + xInit: sublistInit, + xNumProp: nil, + xCheck: nil, + xChop: nil, + xOffset: boxOffset, + xFit: nil, + xRender: nil, +} +var noopClass = PClass{ + zName: "noop", + isLine: false, + eJust: 0, + xInit: nil, + xNumProp: nil, + xCheck: nil, + xChop: nil, + xOffset: boxOffset, + xFit: nil, + xRender: nil, +} + +/* +** Reduce the length of the line segment by amt (if possible) by +** modifying the location of *t. + */ +func pik_chop(f *PPoint, t *PPoint, amt PNum) { + var dx PNum = t.x - f.x + var dy PNum = t.y - f.y + var dist PNum = math.Hypot(dx, dy) + if dist <= amt { + *t = *f + return + } + var r PNum = 1.0 - amt/dist + t.x = f.x + r*dx + t.y = f.y + r*dy +} + +/* +** Draw an arrowhead on the end of the line segment from pFrom to pTo. +** Also, shorten the line segment (by changing the value of pTo) so that +** the shaft of the arrow does not extend into the arrowhead. + */ +func (p *Pik) pik_draw_arrowhead(f *PPoint, t *PPoint, pObj *PObj) { + var dx PNum = t.x - f.x + var dy PNum = t.y - f.y + var dist PNum = math.Hypot(dx, dy) + var h PNum = p.hArrow * pObj.sw + var w PNum = p.wArrow * pObj.sw + if pObj.color < 0.0 { + return + } + if pObj.sw <= 0.0 { + return + } + if dist <= 0.0 { + return + } /* Unable */ + dx /= dist + dy /= dist + var e1 PNum = dist - h + if e1 < 0.0 { + e1 = 0.0 + h = dist + } + var ddx PNum = -w * dy + var ddy PNum = w * dx + var bx PNum = f.x + e1*dx + var by PNum = f.y + e1*dy + p.pik_append_xy("\n", false) + pik_chop(f, t, h/2) +} + +/* +** Compute the relative offset to an edge location from the reference for a +** an statement. + */ +func (p *Pik) pik_elem_offset(pObj *PObj, cp uint8) PPoint { + return pObj.typ.xOffset(p, pObj, cp) +} + +/* +** Append raw text to zOut + */ +func (p *Pik) pik_append(zText string) { + p.zOut.WriteString(zText) +} + +var html_re_with_space = regexp.MustCompile(`[<>& ]`) + +/* +** Append text to zOut with HTML characters escaped. +** +** * The space character is changed into non-breaking space (U+00a0) +** if mFlags has the 0x01 bit set. This is needed when outputting +** text to preserve leading and trailing whitespace. Turns out we +** cannot use   as that is an HTML-ism and is not valid in XML. +** +** * The "&" character is changed into "&" if mFlags has the +** 0x02 bit set. This is needed when generating error message text. +** +** * Except for the above, only "<" and ">" are escaped. + */ +func (p *Pik) pik_append_text(zText string, mFlags int) { + bQSpace := mFlags&1 > 0 + bQAmp := mFlags&2 > 0 + + text := html_re_with_space.ReplaceAllStringFunc(zText, func(s string) string { + switch { + case s == "<": + return "<" + case s == ">": + return ">" + case s == "&" && bQAmp: + return "&" + case s == " " && bQSpace: + return "\302\240" + default: + return s + } + }) + p.pik_append(text) +} + +/* +** Append error message text. This is either a raw append, or an append +** with HTML escapes, depending on whether the PIKCHR_PLAINTEXT_ERRORS flag +** is set. + */ +func (p *Pik) pik_append_errtxt(zText string) { + if p.mFlags&PIKCHR_PLAINTEXT_ERRORS != 0 { + p.pik_append(zText) + } else { + p.pik_append_text(zText, 0) + } +} + +/* Append a PNum value + */ +func (p *Pik) pik_append_num(z string, v PNum) { + p.pik_append(z) + p.pik_append(fmt.Sprintf("%.10g", v)) +} + +/* Append a PPoint value (Used for debugging only) + */ +func (p *Pik) pik_append_point(z string, pPt *PPoint) { + buf := fmt.Sprintf("%.10g,%.10g", pPt.x, pPt.y) + p.pik_append(z) + p.pik_append(buf) +} + +/* +** Invert the RGB color so that it is appropriate for dark mode. +** Variable x hold the initial color. The color is intended for use +** as a background color if isBg is true, and as a foreground color +** if isBg is false. + */ +func pik_color_to_dark_mode(x int, isBg bool) int { + x = 0xffffff - x + r := (x >> 16) & 0xff + g := (x >> 8) & 0xff + b := x & 0xff + mx := r + if g > mx { + mx = g + } + if b > mx { + mx = b + } + mn := r + if g < mn { + mn = g + } + if b < mn { + mn = b + } + r = mn + (mx - r) + g = mn + (mx - g) + b = mn + (mx - b) + if isBg { + if mx > 127 { + r = (127 * r) / mx + g = (127 * g) / mx + b = (127 * b) / mx + } + } else { + if mn < 128 && mx > mn { + r = 127 + ((r-mn)*128)/(mx-mn) + g = 127 + ((g-mn)*128)/(mx-mn) + b = 127 + ((b-mn)*128)/(mx-mn) + } + } + return r*0x10000 + g*0x100 + b +} + +/* Append a PNum value surrounded by text. Do coordinate transformations +** on the value. + */ +func (p *Pik) pik_append_x(z1 string, v PNum, z2 string) { + v -= p.bbox.sw.x + p.pik_append(fmt.Sprintf("%s%d%s", z1, pik_round(p.rScale*v), z2)) +} +func (p *Pik) pik_append_y(z1 string, v PNum, z2 string) { + v = p.bbox.ne.y - v + p.pik_append(fmt.Sprintf("%s%d%s", z1, pik_round(p.rScale*v), z2)) +} +func (p *Pik) pik_append_xy(z1 string, x PNum, y PNum) { + x = x - p.bbox.sw.x + y = p.bbox.ne.y - y + p.pik_append(fmt.Sprintf("%s%d,%d", z1, pik_round(p.rScale*x), pik_round(p.rScale*y))) +} +func (p *Pik) pik_append_dis(z1 string, v PNum, z2 string) { + p.pik_append(fmt.Sprintf("%s%.6g%s", z1, p.rScale*v, z2)) +} + +/* Append a color specification to the output. +** +** In PIKCHR_DARK_MODE, the color is inverted. The "bg" flags indicates that +** the color is intended for use as a background color if true, or as a +** foreground color if false. The distinction only matters for color +** inversions in PIKCHR_DARK_MODE. + */ +func (p *Pik) pik_append_clr(z1 string, v PNum, z2 string, bg bool) { + x := pik_round(v) + if x == 0 && p.fgcolor > 0 && !bg { + x = p.fgcolor + } else if bg && x >= 0xffffff && p.bgcolor > 0 { + x = p.bgcolor + } else if p.mFlags&PIKCHR_DARK_MODE != 0 { + x = pik_color_to_dark_mode(x, bg) + } + r := (x >> 16) & 0xff + g := (x >> 8) & 0xff + b := x & 0xff + buf := fmt.Sprintf("%srgb(%d,%d,%d)%s", z1, r, g, b, z2) + p.pik_append(buf) +} + +/* Append an SVG path A record: +** +** A r1 r2 0 0 0 x y + */ +func (p *Pik) pik_append_arc(r1 PNum, r2 PNum, x PNum, y PNum) { + x = x - p.bbox.sw.x + y = p.bbox.ne.y - y + buf := fmt.Sprintf("A%d %d 0 0 0 %d %d", + pik_round(p.rScale*r1), pik_round(p.rScale*r2), + pik_round(p.rScale*x), pik_round(p.rScale*y)) + p.pik_append(buf) +} + +/* Append a style="..." text. But, leave the quote unterminated, in case +** the caller wants to add some more. +** +** eFill is non-zero to fill in the background, or 0 if no fill should +** occur. Non-zero values of eFill determine the "bg" flag to pik_append_clr() +** for cases when pObj.fill==pObj.color +** +** 1 fill is background, and color is foreground. +** 2 fill and color are both foreground. (Used by "dot" objects) +** 3 fill and color are both background. (Used by most other objs) + */ +func (p *Pik) pik_append_style(pObj *PObj, eFill int) { + clrIsBg := false + p.pik_append(" style=\"") + if pObj.fill >= 0 && eFill != 0 { + fillIsBg := true + if pObj.fill == pObj.color { + if eFill == 2 { + fillIsBg = false + } + if eFill == 3 { + clrIsBg = true + } + } + p.pik_append_clr("fill:", pObj.fill, ";", fillIsBg) + } else { + p.pik_append("fill:none;") + } + if pObj.sw > 0.0 && pObj.color >= 0.0 { + sw := pObj.sw + p.pik_append_dis("stroke-width:", sw, ";") + if pObj.nPath > 2 && pObj.rad <= pObj.sw { + p.pik_append("stroke-linejoin:round;") + } + p.pik_append_clr("stroke:", pObj.color, ";", clrIsBg) + if pObj.dotted > 0.0 { + v := pObj.dotted + if sw < 2.1/p.rScale { + sw = 2.1 / p.rScale + } + p.pik_append_dis("stroke-dasharray:", sw, "") + p.pik_append_dis(",", v, ";") + } else if pObj.dashed > 0.0 { + v := pObj.dashed + p.pik_append_dis("stroke-dasharray:", v, "") + p.pik_append_dis(",", v, ";") + } + } +} + +/* +** Compute the vertical locations for all text items in the +** object pObj. In other words, set every pObj.aTxt[*].eCode +** value to contain exactly one of: TP_ABOVE2, TP_ABOVE, TP_CENTER, +** TP_BELOW, or TP_BELOW2 is set. + */ +func pik_txt_vertical_layout(pObj *PObj) { + n := int(pObj.nTxt) + if n == 0 { + return + } + aTxt := pObj.aTxt[:] + if n == 1 { + if (aTxt[0].eCode & TP_VMASK) == 0 { + aTxt[0].eCode |= TP_CENTER + } + } else { + allSlots := int16(0) + var aFree [5]int16 + /* If there is more than one TP_ABOVE, change the first to TP_ABOVE2. */ + for j, mJust, i := 0, int16(0), n-1; i >= 0; i-- { + if aTxt[i].eCode&TP_ABOVE != 0 { + if j == 0 { + j++ + mJust = aTxt[i].eCode & TP_JMASK + } else if j == 1 && mJust != 0 && (aTxt[i].eCode&mJust) == 0 { + j++ + } else { + aTxt[i].eCode = (aTxt[i].eCode &^ TP_VMASK) | TP_ABOVE2 + break + } + } + } + /* If there is more than one TP_BELOW, change the last to TP_BELOW2 */ + for j, mJust, i := 0, int16(0), 0; i < n; i++ { + if aTxt[i].eCode&TP_BELOW != 0 { + if j == 0 { + j++ + mJust = aTxt[i].eCode & TP_JMASK + } else if j == 1 && mJust != 0 && (aTxt[i].eCode&mJust) == 0 { + j++ + } else { + aTxt[i].eCode = (aTxt[i].eCode &^ TP_VMASK) | TP_BELOW2 + break + } + } + } + /* Compute a mask of all slots used */ + for i := 0; i < n; i++ { + allSlots |= aTxt[i].eCode & TP_VMASK + } + /* Set of an array of available slots */ + if n == 2 && ((aTxt[0].eCode|aTxt[1].eCode)&TP_JMASK) == (TP_LJUST|TP_RJUST) { + /* Special case of two texts that have opposite justification: + ** Allow them both to float to center. */ + aFree[0] = TP_CENTER + aFree[1] = TP_CENTER + } else { + /* Set up the arrow so that available slots are filled from top to + ** bottom */ + iSlot := 0 + if n >= 4 && (allSlots&TP_ABOVE2) == 0 { + aFree[iSlot] = TP_ABOVE2 + iSlot++ + } + if (allSlots & TP_ABOVE) == 0 { + aFree[iSlot] = TP_ABOVE + iSlot++ + } + if (n & 1) != 0 { + aFree[iSlot] = TP_CENTER + iSlot++ + } + if (allSlots & TP_BELOW) == 0 { + aFree[iSlot] = TP_BELOW + iSlot++ + } + if n >= 4 && (allSlots&TP_BELOW2) == 0 { + aFree[iSlot] = TP_BELOW2 + iSlot++ + } + } + /* Set the VMASK for all unassigned texts */ + for i, iSlot := 0, 0; i < n; i++ { + if (aTxt[i].eCode & TP_VMASK) == 0 { + aTxt[i].eCode |= aFree[iSlot] + iSlot++ + } + } + } +} + +/* Return the font scaling factor associated with the input text attribute. + */ +func (p *Pik) pik_font_scale(t PToken) PNum { + scale := p.svgFontScale + if t.eCode&TP_BIG != 0 { + scale *= 1.25 + } + if t.eCode&TP_SMALL != 0 { + scale *= 0.8 + } + if t.eCode&TP_XTRA != 0 { + scale *= scale + } + return scale +} + +/* Append multiple SVG elements for the text fields of the PObj. +** Parameters: +** +** p The Pik object into which we are rendering +** +** pObj Object containing the text to be rendered +** +** pBox If not NULL, do no rendering at all. Instead +** expand the box object so that it will include all +** of the text. + */ +func (p *Pik) pik_append_txt(pObj *PObj, pBox *PBox) { + var jw PNum /* Justification margin relative to center */ + var ha2 PNum = 0.0 /* Height of the top row of text */ + var ha1 PNum = 0.0 /* Height of the second "above" row */ + var hc PNum = 0.0 /* Height of the center row */ + var hb1 PNum = 0.0 /* Height of the first "below" row of text */ + var hb2 PNum = 0.0 /* Height of the second "below" row */ + var yBase PNum = 0.0 + allMask := int16(0) + + if p.nErr != 0 { + return + } + if pObj.nTxt == 0 { + return + } + aTxt := pObj.aTxt[:] + n := int(pObj.nTxt) + pik_txt_vertical_layout(pObj) + x := pObj.ptAt.x + for i := 0; i < n; i++ { + allMask |= pObj.aTxt[i].eCode + } + if pObj.typ.isLine { + hc = pObj.sw * 1.5 + } else if pObj.rad > 0.0 && pObj.typ.zName == "cylinder" { + yBase = -0.75 * pObj.rad + } + if allMask&TP_CENTER != 0 { + for i := 0; i < n; i++ { + if pObj.aTxt[i].eCode&TP_CENTER != 0 { + s := p.pik_font_scale(pObj.aTxt[i]) + if hc < s*p.charHeight { + hc = s * p.charHeight + } + } + } + } + if allMask&TP_ABOVE != 0 { + for i := 0; i < n; i++ { + if pObj.aTxt[i].eCode&TP_ABOVE != 0 { + s := p.pik_font_scale(pObj.aTxt[i]) * p.charHeight + if ha1 < s { + ha1 = s + } + } + } + if allMask&TP_ABOVE2 != 0 { + for i := 0; i < n; i++ { + if pObj.aTxt[i].eCode&TP_ABOVE2 != 0 { + s := p.pik_font_scale(pObj.aTxt[i]) * p.charHeight + if ha2 < s { + ha2 = s + } + } + } + } + } + if allMask&TP_BELOW != 0 { + for i := 0; i < n; i++ { + if pObj.aTxt[i].eCode&TP_BELOW != 0 { + s := p.pik_font_scale(pObj.aTxt[i]) * p.charHeight + if hb1 < s { + hb1 = s + } + } + } + if allMask&TP_BELOW2 != 0 { + for i := 0; i < n; i++ { + if pObj.aTxt[i].eCode&TP_BELOW2 != 0 { + s := p.pik_font_scale(pObj.aTxt[i]) * p.charHeight + if hb2 < s { + hb2 = s + } + } + } + } + } + if pObj.typ.eJust == 1 { + jw = 0.5 * (pObj.w - 0.5*(p.charWidth+pObj.sw)) + } else { + jw = 0.0 + } + for i := 0; i < n; i++ { + t := aTxt[i] + xtraFontScale := p.pik_font_scale(t) + var nx PNum = 0 + orig_y := pObj.ptAt.y + y := yBase + if t.eCode&TP_ABOVE2 != 0 { + y += 0.5*hc + ha1 + 0.5*ha2 + } + if t.eCode&TP_ABOVE != 0 { + y += 0.5*hc + 0.5*ha1 + } + if t.eCode&TP_BELOW != 0 { + y -= 0.5*hc + 0.5*hb1 + } + if t.eCode&TP_BELOW2 != 0 { + y -= 0.5*hc + hb1 + 0.5*hb2 + } + if t.eCode&TP_LJUST != 0 { + nx -= jw + } + if t.eCode&TP_RJUST != 0 { + nx += jw + } + + if pBox != nil { + /* If pBox is not NULL, do not draw any . Instead, just expand + ** pBox to include the text */ + var cw PNum = PNum(pik_text_length(t)) * p.charWidth * xtraFontScale * 0.01 + var ch PNum = p.charHeight * 0.5 * xtraFontScale + var x0, y0, x1, y1 PNum /* Boundary of text relative to pObj.ptAt */ + if t.eCode&TP_BOLD != 0 { + cw *= 1.1 + } + if t.eCode&TP_RJUST != 0 { + x0 = nx + y0 = y - ch + x1 = nx - cw + y1 = y + ch + } else if t.eCode&TP_LJUST != 0 { + x0 = nx + y0 = y - ch + x1 = nx + cw + y1 = y + ch + } else { + x0 = nx + cw/2 + y0 = y + ch + x1 = nx - cw/2 + y1 = y - ch + } + if (t.eCode&TP_ALIGN) != 0 && pObj.nPath >= 2 { + nn := pObj.nPath + var dx PNum = pObj.aPath[nn-1].x - pObj.aPath[0].x + var dy PNum = pObj.aPath[nn-1].y - pObj.aPath[0].y + if dx != 0 || dy != 0 { + var dist PNum = math.Hypot(dx, dy) + var tt PNum + dx /= dist + dy /= dist + tt = dx*x0 - dy*y0 + y0 = dy*x0 - dx*y0 + x0 = tt + tt = dx*x1 - dy*y1 + y1 = dy*x1 - dx*y1 + x1 = tt + } + } + pik_bbox_add_xy(pBox, x+x0, orig_y+y0) + pik_bbox_add_xy(pBox, x+x1, orig_y+y1) + continue + } + nx += x + y += orig_y + + p.pik_append_x("= 0.0 { + p.pik_append_clr(" fill=\"", pObj.color, "\"", false) + } + xtraFontScale *= p.fontScale + if xtraFontScale <= 0.99 || xtraFontScale >= 1.01 { + p.pik_append_num(" font-size=\"", xtraFontScale*100.0) + p.pik_append("%\"") + } + if (t.eCode&TP_ALIGN) != 0 && pObj.nPath >= 2 { + nn := pObj.nPath + var dx PNum = pObj.aPath[nn-1].x - pObj.aPath[0].x + var dy PNum = pObj.aPath[nn-1].y - pObj.aPath[0].y + if dx != 0 || dy != 0 { + var ang PNum = math.Atan2(dy, dx) * -180 / math.Pi + p.pik_append_num(" transform=\"rotate(", ang) + p.pik_append_xy(" ", x, orig_y) + p.pik_append(")\"") + } + } + p.pik_append(" dominant-baseline=\"central\">") + var z []byte + var nz int + if t.n >= 2 && t.z[0] == '"' { + z = t.z[1:] + nz = t.n - 2 + } else { + z = t.z + nz = t.n + } + for nz > 0 { + var j int + for j = 0; j < nz && z[j] != '\\'; j++ { + } + if j != 0 { + p.pik_append_text(string(z[:j]), 0x3) + } + if j < nz && (j+1 == nz || z[j+1] == '\\') { + p.pik_append("\") + j++ + } + nz -= j + 1 + if nz > 0 { + z = z[j+1:] + } + } + p.pik_append("\n") + } +} + +/* +** Append text (that will go inside of a
    ...
    ) that +** shows the context of an error token. + */ +func (p *Pik) pik_error_context(pErr *PToken, nContext int) { + var ( + iErrPt int /* Index of first byte of error from start of input */ + iErrCol int /* Column of the error token on its line */ + iStart int /* Start position of the error context */ + iEnd int /* End position of the error context */ + iLineno int /* Line number of the error */ + iFirstLineno int /* Line number of start of error context */ + i int /* Loop counter */ + iBump = 0 /* Bump the location of the error cursor */ + ) + + iErrPt = len(p.sIn.z) - len(pErr.z) // in C, uses pointer math: iErrPt = (int)(pErr->z - p->sIn.z); + if iErrPt >= p.sIn.n { + iErrPt = p.sIn.n - 1 + iBump = 1 + } else { + for iErrPt > 0 && (p.sIn.z[iErrPt] == '\n' || p.sIn.z[iErrPt] == '\r') { + iErrPt-- + iBump = 1 + } + } + iLineno = 1 + for i = 0; i < iErrPt; i++ { + if p.sIn.z[i] == '\n' { + iLineno++ + } + } + iStart = 0 + iFirstLineno = 1 + for iFirstLineno+nContext < iLineno { + for p.sIn.z[iStart] != '\n' { + iStart++ + } + iStart++ + iFirstLineno++ + } + for iEnd = iErrPt; p.sIn.z[iEnd] != 0 && p.sIn.z[iEnd] != '\n'; iEnd++ { + } + i = iStart + for iFirstLineno <= iLineno { + zLineno := fmt.Sprintf("/* %4d */ ", iFirstLineno) + iFirstLineno++ + p.pik_append(zLineno) + for i = iStart; p.sIn.z[i] != 0 && p.sIn.z[i] != '\n'; i++ { + } + p.pik_append_errtxt(string(p.sIn.z[iStart:i])) + iStart = i + 1 + p.pik_append("\n") + } + for iErrCol, i = 0, iErrPt; i > 0 && p.sIn.z[i] != '\n'; iErrCol, i = iErrCol+1, i-1 { + } + for i = 0; i < iErrCol+11+iBump; i++ { + p.pik_append(" ") + } + for i = 0; i < pErr.n; i++ { + p.pik_append("^") + } + p.pik_append("\n") +} + +/* +** Generate an error message for the output. pErr is the token at which +** the error should point. zMsg is the text of the error message. If +** either pErr or zMsg is NULL, generate an out-of-memory error message. +** +** This routine is a no-op if there has already been an error reported. + */ +func (p *Pik) pik_error(pErr *PToken, zMsg string) { + if p == nil { + return + } + if p.nErr > 0 { + return + } + p.nErr++ + if zMsg == "" { + if p.mFlags&PIKCHR_PLAINTEXT_ERRORS != 0 { + p.pik_append("\nOut of memory\n") + } else { + p.pik_append("\n

    Out of memory

    \n") + } + return + } + if pErr == nil { + p.pik_append("\n") + p.pik_append_errtxt(zMsg) + return + } + if (p.mFlags & PIKCHR_PLAINTEXT_ERRORS) == 0 { + p.pik_append("
    \n")
    +	}
    +	p.pik_error_context(pErr, 5)
    +	p.pik_append("ERROR: ")
    +	p.pik_append_errtxt(zMsg)
    +	p.pik_append("\n")
    +	for i := p.nCtx - 1; i >= 0; i-- {
    +		p.pik_append("Called from:\n")
    +		p.pik_error_context(&p.aCtx[i], 0)
    +	}
    +	if (p.mFlags & PIKCHR_PLAINTEXT_ERRORS) == 0 {
    +		p.pik_append("
    \n") + } +} + +/* + ** Process an "assert( e1 == e2 )" statement. Always return `nil`. + */ +func (p *Pik) pik_assert(e1 PNum, pEq *PToken, e2 PNum) *PObj { + /* Convert the numbers to strings using %g for comparison. This + ** limits the precision of the comparison to account for rounding error. */ + zE1 := fmt.Sprintf("%g", e1) + zE2 := fmt.Sprintf("%g", e2) + if zE1 != zE2 { + p.pik_error(pEq, fmt.Sprintf("%.50s != %.50s", zE1, zE2)) + } + return nil +} + +/* +** Process an "assert( place1 == place2 )" statement. Always return `nil`. + */ +func (p *Pik) pik_position_assert(e1 *PPoint, pEq *PToken, e2 *PPoint) *PObj { + /* Convert the numbers to strings using %g for comparison. This + ** limits the precision of the comparison to account for rounding error. */ + zE1 := fmt.Sprintf("(%g,%g)", e1.x, e1.y) + zE2 := fmt.Sprintf("(%g,%g)", e2.x, e2.y) + if zE1 != zE2 { + p.pik_error(pEq, fmt.Sprintf("%s != %s", zE1, zE2)) + } + return nil +} + +/* Free a complete list of objects */ +func (p *Pik) pik_elist_free(pList *PList) { + if pList == nil || *pList == nil { + return + } + for i := 0; i < len(*pList); i++ { + p.pik_elem_free((*pList)[i]) + } +} + +/* Free a single object, and its substructure */ +func (p *Pik) pik_elem_free(pObj *PObj) { + if pObj == nil { + return + } + p.pik_elist_free(&pObj.pSublist) +} + +/* Convert a numeric literal into a number. Return that number. +** There is no error handling because the tokenizer has already +** assured us that the numeric literal is valid. +** +** Allowed number forms: +** +** (1) Floating point literal +** (2) Same as (1) but followed by a unit: "cm", "mm", "in", +** "px", "pt", or "pc". +** (3) Hex integers: 0x000000 +** +** This routine returns the result in inches. If a different unit +** is specified, the conversion happens automatically. + */ +func pik_atof(num *PToken) PNum { + if num.n >= 3 && num.z[0] == '0' && (num.z[1] == 'x' || num.z[1] == 'X') { + i, err := strconv.ParseInt(string(num.z[2:num.n]), 16, 64) + if err != nil { + return 0 + } + return PNum(i) + } + factor := 1.0 + + z := num.String() + + if num.n > 2 { + hasSuffix := true + switch string(num.z[num.n-2 : num.n]) { + case "cm": + factor = 1 / 2.54 + case "mm": + factor = 1 / 25.4 + case "px": + factor = 1 / 96.0 + case "pt": + factor = 1 / 72.0 + case "pc": + factor = 1 / 6.0 + case "in": + factor = 1.0 + default: + hasSuffix = false + } + if hasSuffix { + z = z[:len(z)-2] + } + } + + ans, err := strconv.ParseFloat(z, 64) + ans *= factor + if err != nil { + return 0.0 + } + return PNum(ans) +} + +/* +** Compute the distance between two points + */ +func pik_dist(pA *PPoint, pB *PPoint) PNum { + dx := pB.x - pA.x + dy := pB.y - pA.y + return math.Hypot(dx, dy) +} + +/* Return true if a bounding box is empty. + */ +func pik_bbox_isempty(p *PBox) bool { + return p.sw.x > p.ne.x +} + +/* Return true if point pPt is contained within the bounding box pBox + */ +func pik_bbox_contains_point(pBox *PBox, pPt *PPoint) bool { + if pik_bbox_isempty(pBox) { + return false + } + if pPt.x < pBox.sw.x { + return false + } + if pPt.x > pBox.ne.x { + return false + } + if pPt.y < pBox.sw.y { + return false + } + if pPt.y > pBox.ne.y { + return false + } + return true +} + +/* Initialize a bounding box to an empty container + */ +func pik_bbox_init(p *PBox) { + p.sw.x = 1.0 + p.sw.y = 1.0 + p.ne.x = 0.0 + p.ne.y = 0.0 +} + +/* Enlarge the PBox of the first argument so that it fully +** covers the second PBox + */ +func pik_bbox_addbox(pA *PBox, pB *PBox) { + if pik_bbox_isempty(pA) { + *pA = *pB + } + if pik_bbox_isempty(pB) { + return + } + if pA.sw.x > pB.sw.x { + pA.sw.x = pB.sw.x + } + if pA.sw.y > pB.sw.y { + pA.sw.y = pB.sw.y + } + if pA.ne.x < pB.ne.x { + pA.ne.x = pB.ne.x + } + if pA.ne.y < pB.ne.y { + pA.ne.y = pB.ne.y + } +} + +/* Enlarge the PBox of the first argument, if necessary, so that +** it contains the point described by the 2nd and 3rd arguments. + */ +func pik_bbox_add_xy(pA *PBox, x PNum, y PNum) { + if pik_bbox_isempty(pA) { + pA.ne.x = x + pA.ne.y = y + pA.sw.x = x + pA.sw.y = y + return + } + if pA.sw.x > x { + pA.sw.x = x + } + if pA.sw.y > y { + pA.sw.y = y + } + if pA.ne.x < x { + pA.ne.x = x + } + if pA.ne.y < y { + pA.ne.y = y + } +} + +/* Enlarge the PBox so that it is able to contain an ellipse +** centered at x,y and with radiuses rx and ry. + */ +func pik_bbox_addellipse(pA *PBox, x PNum, y PNum, rx PNum, ry PNum) { + if pik_bbox_isempty(pA) { + pA.ne.x = x + rx + pA.ne.y = y + ry + pA.sw.x = x - rx + pA.sw.y = y - ry + return + } + if pA.sw.x > x-rx { + pA.sw.x = x - rx + } + if pA.sw.y > y-ry { + pA.sw.y = y - ry + } + if pA.ne.x < x+rx { + pA.ne.x = x + rx + } + if pA.ne.y < y+ry { + pA.ne.y = y + ry + } +} + +/* Append a new object onto the end of an object list. The +** object list is created if it does not already exist. Return +** the new object list. + */ +func (p *Pik) pik_elist_append(pList PList, pObj *PObj) PList { + if pObj == nil { + return pList + } + pList = append(pList, pObj) + p.list = pList + return pList +} + +/* Convert an object class name into a PClass pointer + */ +func pik_find_class(pId *PToken) *PClass { + zString := pId.String() + first := 0 + last := len(aClass) - 1 + for { + mid := (first + last) / 2 + c := strings.Compare(aClass[mid].zName, zString) + if c == 0 { + return &aClass[mid] + } + if c < 0 { + first = mid + 1 + } else { + last = mid - 1 + } + + if first > last { + return nil + } + } +} + +/* Allocate and return a new PObj object. +** +** If pId!=0 then pId is an identifier that defines the object class. +** If pStr!=0 then it is a STRING literal that defines a text object. +** If pSublist!=0 then this is a [...] object. If all three parameters +** are NULL then this is a no-op object used to define a PLACENAME. + */ +func (p *Pik) pik_elem_new(pId *PToken, pStr *PToken, pSublist PList) *PObj { + miss := false + if p.nErr != 0 { + return nil + } + pNew := &PObj{} + + p.cur = pNew + p.nTPath = 1 + p.thenFlag = false + if len(p.list) == 0 { + pNew.ptAt.x = 0.0 + pNew.ptAt.y = 0.0 + pNew.eWith = CP_C + } else { + pPrior := p.list[len(p.list)-1] + pNew.ptAt = pPrior.ptExit + switch p.eDir { + default: + pNew.eWith = CP_W + case DIR_LEFT: + pNew.eWith = CP_E + case DIR_UP: + pNew.eWith = CP_S + case DIR_DOWN: + pNew.eWith = CP_N + } + } + p.aTPath[0] = pNew.ptAt + pNew.with = pNew.ptAt + pNew.outDir = p.eDir + pNew.inDir = p.eDir + pNew.iLayer = p.pik_value_int("layer", &miss) + if miss { + pNew.iLayer = 1000 + } + if pNew.iLayer < 0 { + pNew.iLayer = 0 + } + if pSublist != nil { + pNew.typ = &sublistClass + pNew.pSublist = pSublist + sublistClass.xInit(p, pNew) + return pNew + } + if pStr != nil { + n := PToken{ + z: []byte("text"), + n: 4, + } + pNew.typ = pik_find_class(&n) + assert(pNew.typ != nil, "pNew.typ!=nil") + pNew.errTok = *pStr + pNew.typ.xInit(p, pNew) + p.pik_add_txt(pStr, pStr.eCode) + return pNew + } + if pId != nil { + pNew.errTok = *pId + pClass := pik_find_class(pId) + if pClass != nil { + pNew.typ = pClass + pNew.sw = p.pik_value("thickness", nil) + pNew.fill = p.pik_value("fill", nil) + pNew.color = p.pik_value("color", nil) + pClass.xInit(p, pNew) + return pNew + } + p.pik_error(pId, "unknown object type") + p.pik_elem_free(pNew) + return nil + } + pNew.typ = &noopClass + pNew.ptExit = pNew.ptAt + pNew.ptEnter = pNew.ptAt + return pNew +} + +/* +** If the ID token in the argument is the name of a macro, return +** the PMacro object for that macro + */ +func (p *Pik) pik_find_macro(pId *PToken) *PMacro { + for pMac := p.pMacros; pMac != nil; pMac = pMac.pNext { + if pMac.macroName.n == pId.n && bytesEq(pMac.macroName.z[:pMac.macroName.n], pId.z[:pId.n]) { + return pMac + } + } + return nil +} + +/* Add a new macro + */ +func (p *Pik) pik_add_macro( + pId *PToken, /* The ID token that defines the macro name */ + pCode *PToken, /* Macro body inside of {...} */ +) { + pNew := p.pik_find_macro(pId) + if pNew == nil { + pNew = &PMacro{ + pNext: p.pMacros, + macroName: *pId, + } + p.pMacros = pNew + } + pNew.macroBody.z = pCode.z[1:] + pNew.macroBody.n = pCode.n - 2 + pNew.inUse = false +} + +/* +** Set the output direction and exit point for an object + */ +func pik_elem_set_exit(pObj *PObj, eDir uint8) { + assert(ValidDir(eDir), "ValidDir(eDir)") + pObj.outDir = eDir + if !pObj.typ.isLine || pObj.bClose { + pObj.ptExit = pObj.ptAt + switch pObj.outDir { + default: + pObj.ptExit.x += pObj.w * 0.5 + case DIR_LEFT: + pObj.ptExit.x -= pObj.w * 0.5 + case DIR_UP: + pObj.ptExit.y += pObj.h * 0.5 + case DIR_DOWN: + pObj.ptExit.y -= pObj.h * 0.5 + } + } +} + +/* Change the layout direction. + */ +func (p *Pik) pik_set_direction(eDir uint8) { + assert(ValidDir(eDir), "ValidDir(eDir)") + p.eDir = eDir + + /* It seems to make sense to reach back into the last object and + ** change its exit point (its ".end") to correspond to the new + ** direction. Things just seem to work better this way. However, + ** legacy PIC does *not* do this. + ** + ** The difference can be seen in a script like this: + ** + ** arrow; circle; down; arrow + ** + ** You can make pikchr render the above exactly like PIC + ** by deleting the following three lines. But I (drh) think + ** it works better with those lines in place. + */ + if len(p.list) > 0 { + pik_elem_set_exit(p.list[len(p.list)-1], eDir) + } +} + +/* Move all coordinates contained within an object (and within its +** substructure) by dx, dy + */ +func pik_elem_move(pObj *PObj, dx PNum, dy PNum) { + pObj.ptAt.x += dx + pObj.ptAt.y += dy + pObj.ptEnter.x += dx + pObj.ptEnter.y += dy + pObj.ptExit.x += dx + pObj.ptExit.y += dy + pObj.bbox.ne.x += dx + pObj.bbox.ne.y += dy + pObj.bbox.sw.x += dx + pObj.bbox.sw.y += dy + for i := 0; i < pObj.nPath; i++ { + pObj.aPath[i].x += dx + pObj.aPath[i].y += dy + } + if pObj.pSublist != nil { + pik_elist_move(pObj.pSublist, dx, dy) + } +} +func pik_elist_move(pList PList, dx PNum, dy PNum) { + for i := 0; i < len(pList); i++ { + pik_elem_move(pList[i], dx, dy) + } +} + +/* +** Check to see if it is ok to set the value of paraemeter mThis. +** Return 0 if it is ok. If it not ok, generate an appropriate +** error message and return non-zero. +** +** Flags are set in pObj so that the same object or conflicting +** objects may not be set again. +** +** To be ok, bit mThis must be clear and no more than one of +** the bits identified by mBlockers may be set. + */ +func (p *Pik) pik_param_ok( + pObj *PObj, /* The object under construction */ + pId *PToken, /* Make the error point to this token */ + mThis uint, /* Value we are trying to set */ +) bool { + if pObj.mProp&mThis != 0 { + p.pik_error(pId, "value is already set") + return true + } + if pObj.mCalc&mThis != 0 { + p.pik_error(pId, "value already fixed by prior constraints") + return true + } + pObj.mProp |= mThis + return false +} + +/* +** Set a numeric property like "width 7" or "radius 200%". +** +** The rAbs term is an absolute value to add in. rRel is +** a relative value by which to change the current value. + */ +func (p *Pik) pik_set_numprop(pId *PToken, pVal *PRel) { + pObj := p.cur + switch pId.eType { + case T_HEIGHT: + if p.pik_param_ok(pObj, pId, A_HEIGHT) { + return + } + pObj.h = pObj.h*pVal.rRel + pVal.rAbs + case T_WIDTH: + if p.pik_param_ok(pObj, pId, A_WIDTH) { + return + } + pObj.w = pObj.w*pVal.rRel + pVal.rAbs + case T_RADIUS: + if p.pik_param_ok(pObj, pId, A_RADIUS) { + return + } + pObj.rad = pObj.rad*pVal.rRel + pVal.rAbs + case T_DIAMETER: + if p.pik_param_ok(pObj, pId, A_RADIUS) { + return + } + pObj.rad = pObj.rad*pVal.rRel + 0.5*pVal.rAbs /* diam it 2x rad */ + case T_THICKNESS: + if p.pik_param_ok(pObj, pId, A_THICKNESS) { + return + } + pObj.sw = pObj.sw*pVal.rRel + pVal.rAbs + } + if pObj.typ.xNumProp != nil { + pObj.typ.xNumProp(p, pObj, pId) + } +} + +/* +** Set a color property. The argument is an RGB value. + */ +func (p *Pik) pik_set_clrprop(pId *PToken, rClr PNum) { + pObj := p.cur + switch pId.eType { + case T_FILL: + if p.pik_param_ok(pObj, pId, A_FILL) { + return + } + pObj.fill = rClr + case T_COLOR: + if p.pik_param_ok(pObj, pId, A_COLOR) { + return + } + pObj.color = rClr + break + } + if pObj.typ.xNumProp != nil { + pObj.typ.xNumProp(p, pObj, pId) + } +} + +/* +** Set a "dashed" property like "dash 0.05" +** +** Use the value supplied by pVal if available. If pVal==0, use +** a default. + */ +func (p *Pik) pik_set_dashed(pId *PToken, pVal *PNum) { + pObj := p.cur + switch pId.eType { + case T_DOTTED: + if pVal != nil { + pObj.dotted = *pVal + } else { + pObj.dotted = p.pik_value("dashwid", nil) + } + pObj.dashed = 0.0 + case T_DASHED: + if pVal != nil { + pObj.dashed = *pVal + } else { + pObj.dashed = p.pik_value("dashwid", nil) + } + pObj.dotted = 0.0 + } +} + +/* +** If the current path information came from a "same" or "same as" +** reset it. + */ +func (p *Pik) pik_reset_samepath() { + if p.samePath { + p.samePath = false + p.nTPath = 1 + } +} + +/* Add a new term to the path for a line-oriented object by transferring +** the information in the ptTo field over onto the path and into ptFrom +** resetting the ptTo. + */ +func (p *Pik) pik_then(pToken *PToken, pObj *PObj) { + if !pObj.typ.isLine { + p.pik_error(pToken, "use with line-oriented objects only") + return + } + n := p.nTPath - 1 + if n < 1 && (pObj.mProp&A_FROM) == 0 { + p.pik_error(pToken, "no prior path points") + return + } + p.thenFlag = true +} + +/* Advance to the next entry in p.aTPath. Return its index. + */ +func (p *Pik) pik_next_rpath(pErr *PToken) int { + n := p.nTPath - 1 + if n+1 >= len(p.aTPath) { + (*Pik)(nil).pik_error(pErr, "too many path elements") + return n + } + n++ + p.nTPath++ + p.aTPath[n] = p.aTPath[n-1] + p.mTPath = 0 + return n +} + +/* Add a direction term to an object. "up 0.5", or "left 3", or "down" +** or "down 50%". + */ +func (p *Pik) pik_add_direction(pDir *PToken, pVal *PRel) { + pObj := p.cur + if !pObj.typ.isLine { + if pDir != nil { + p.pik_error(pDir, "use with line-oriented objects only") + } else { + x := pik_next_semantic_token(&pObj.errTok) + p.pik_error(&x, "syntax error") + } + return + } + p.pik_reset_samepath() + n := p.nTPath - 1 + if p.thenFlag || p.mTPath == 3 || n == 0 { + n = p.pik_next_rpath(pDir) + p.thenFlag = false + } + dir := p.eDir + if pDir != nil { + dir = uint8(pDir.eCode) + } + switch dir { + case DIR_UP: + if p.mTPath&2 > 0 { + n = p.pik_next_rpath(pDir) + } + p.aTPath[n].y += pVal.rAbs + pObj.h*pVal.rRel + p.mTPath |= 2 + case DIR_DOWN: + if p.mTPath&2 > 0 { + n = p.pik_next_rpath(pDir) + } + p.aTPath[n].y -= pVal.rAbs + pObj.h*pVal.rRel + p.mTPath |= 2 + case DIR_RIGHT: + if p.mTPath&1 > 0 { + n = p.pik_next_rpath(pDir) + } + p.aTPath[n].x += pVal.rAbs + pObj.w*pVal.rRel + p.mTPath |= 1 + case DIR_LEFT: + if p.mTPath&1 > 0 { + n = p.pik_next_rpath(pDir) + } + p.aTPath[n].x -= pVal.rAbs + pObj.w*pVal.rRel + p.mTPath |= 1 + } + pObj.outDir = dir +} + +/* Process a movement attribute of one of these forms: +** +** pDist pHdgKW rHdg pEdgept +** GO distance HEADING angle +** GO distance compasspoint + */ +func (p *Pik) pik_move_hdg( + pDist *PRel, /* Distance to move */ + pHeading *PToken, /* "heading" keyword if present */ + rHdg PNum, /* Angle argument to "heading" keyword */ + pEdgept *PToken, /* EDGEPT keyword "ne", "sw", etc... */ + pErr *PToken, /* Token to use for error messages */ +) { + pObj := p.cur + var rDist PNum = pDist.rAbs + p.pik_value("linewid", nil)*pDist.rRel + if !pObj.typ.isLine { + p.pik_error(pErr, "use with line-oriented objects only") + return + } + p.pik_reset_samepath() + n := 0 + for n < 1 { + n = p.pik_next_rpath(pErr) + } + if pHeading != nil { + if rHdg < 0.0 || rHdg > 360.0 { + p.pik_error(pHeading, "headings should be between 0 and 360") + return + } + } else if pEdgept.eEdge == CP_C { + p.pik_error(pEdgept, "syntax error") + return + } else { + rHdg = pik_hdg_angle[pEdgept.eEdge] + } + if rHdg <= 45.0 { + pObj.outDir = DIR_UP + } else if rHdg <= 135.0 { + pObj.outDir = DIR_RIGHT + } else if rHdg <= 225.0 { + pObj.outDir = DIR_DOWN + } else if rHdg <= 315.0 { + pObj.outDir = DIR_LEFT + } else { + pObj.outDir = DIR_UP + } + rHdg *= 0.017453292519943295769 /* degrees to radians */ + p.aTPath[n].x += rDist * math.Sin(rHdg) + p.aTPath[n].y += rDist * math.Cos(rHdg) + p.mTPath = 2 +} + +/* Process a movement attribute of the form "right until even with ..." + ** + ** pDir is the first keyword, "right" or "left" or "up" or "down". + ** The movement is in that direction until its closest approach to + ** the point specified by pPoint. + */ +func (p *Pik) pik_evenwith(pDir *PToken, pPlace *PPoint) { + pObj := p.cur + + if !pObj.typ.isLine { + p.pik_error(pDir, "use with line-oriented objects only") + return + } + p.pik_reset_samepath() + n := p.nTPath - 1 + if p.thenFlag || p.mTPath == 3 || n == 0 { + n = p.pik_next_rpath(pDir) + p.thenFlag = false + } + switch pDir.eCode { + case DIR_DOWN, DIR_UP: + if p.mTPath&2 != 0 { + n = p.pik_next_rpath(pDir) + } + p.aTPath[n].y = pPlace.y + p.mTPath |= 2 + case DIR_RIGHT, DIR_LEFT: + if p.mTPath&1 != 0 { + n = p.pik_next_rpath(pDir) + } + p.aTPath[n].x = pPlace.x + p.mTPath |= 1 + } + pObj.outDir = uint8(pDir.eCode) +} + +/* If the last referenced object is centered at point pPt then return +** a pointer to that object. If there is no prior object reference, +** or if the points are not the same, return NULL. +** +** This is a side-channel hack used to find the objects at which a +** line begins and ends. For example, in +** +** arrow from OBJ1 to OBJ2 chop +** +** The arrow object is normally just handed the coordinates of the +** centers for OBJ1 and OBJ2. But we also want to know the specific +** object named in case there are multiple objects centered at the +** same point. +** +** See forum post 1d46e3a0bc + */ +func (p *Pik) pik_last_ref_object(pPt *PPoint) *PObj { + var pRes *PObj + if p.lastRef == nil { + return nil + } + if p.lastRef.ptAt.x == pPt.x && p.lastRef.ptAt.y == pPt.y { + pRes = p.lastRef + } + p.lastRef = nil + return pRes +} + +/* Set the "from" of an object + */ +func (p *Pik) pik_set_from(pObj *PObj, pTk *PToken, pPt *PPoint) { + if !pObj.typ.isLine { + p.pik_error(pTk, "use \"at\" to position this object") + return + } + if pObj.mProp&A_FROM != 0 { + p.pik_error(pTk, "line start location already fixed") + return + } + if pObj.bClose { + p.pik_error(pTk, "polygon is closed") + return + } + if p.nTPath > 1 { + var dx PNum = pPt.x - p.aTPath[0].x + var dy PNum = pPt.y - p.aTPath[0].y + for i := 1; i < p.nTPath; i++ { + p.aTPath[i].x += dx + p.aTPath[i].y += dy + } + } + p.aTPath[0] = *pPt + p.mTPath = 3 + pObj.mProp |= A_FROM + pObj.pFrom = p.pik_last_ref_object(pPt) +} + +/* Set the "to" of an object + */ +func (p *Pik) pik_add_to(pObj *PObj, pTk *PToken, pPt *PPoint) { + n := p.nTPath - 1 + if !pObj.typ.isLine { + p.pik_error(pTk, "use \"at\" to position this object") + return + } + if pObj.bClose { + p.pik_error(pTk, "polygon is closed") + return + } + p.pik_reset_samepath() + if n == 0 || p.mTPath == 3 || p.thenFlag { + n = p.pik_next_rpath(pTk) + } + p.aTPath[n] = *pPt + p.mTPath = 3 + pObj.pTo = p.pik_last_ref_object(pPt) +} + +func (p *Pik) pik_close_path(pErr *PToken) { + pObj := p.cur + if p.nTPath < 3 { + p.pik_error(pErr, + "need at least 3 vertexes in order to close the polygon") + return + } + if pObj.bClose { + p.pik_error(pErr, "polygon already closed") + return + } + pObj.bClose = true +} + +/* Lower the layer of the current object so that it is behind the +** given object. + */ +func (p *Pik) pik_behind(pOther *PObj) { + pObj := p.cur + if p.nErr == 0 && pObj.iLayer >= pOther.iLayer { + pObj.iLayer = pOther.iLayer - 1 + } +} + +/* Set the "at" of an object + */ +func (p *Pik) pik_set_at(pEdge *PToken, pAt *PPoint, pErrTok *PToken) { + eDirToCp := []uint8{CP_E, CP_S, CP_W, CP_N} + if p.nErr != 0 { + return + } + pObj := p.cur + + if pObj.typ.isLine { + p.pik_error(pErrTok, "use \"from\" and \"to\" to position this object") + return + } + if pObj.mProp&A_AT != 0 { + p.pik_error(pErrTok, "location fixed by prior \"at\"") + return + } + pObj.mProp |= A_AT + pObj.eWith = CP_C + if pEdge != nil { + pObj.eWith = pEdge.eEdge + } + if pObj.eWith >= CP_END { + dir := pObj.inDir + if pObj.eWith == CP_END { + dir = pObj.outDir + } + pObj.eWith = eDirToCp[int(dir)] + } + pObj.with = *pAt +} + +/* +** Try to add a text attribute to an object + */ +func (p *Pik) pik_add_txt(pTxt *PToken, iPos int16) { + pObj := p.cur + if int(pObj.nTxt) >= len(pObj.aTxt) { + p.pik_error(pTxt, "too many text terms") + return + } + pT := &pObj.aTxt[pObj.nTxt] + pObj.nTxt++ + *pT = *pTxt + pT.eCode = iPos +} + +/* Merge "text-position" flags + */ +func pik_text_position(iPrev int, pFlag *PToken) int { + iRes := iPrev + switch pFlag.eType { + case T_LJUST: + iRes = (iRes &^ TP_JMASK) | TP_LJUST + case T_RJUST: + iRes = (iRes &^ TP_JMASK) | TP_RJUST + case T_ABOVE: + iRes = (iRes &^ TP_VMASK) | TP_ABOVE + case T_CENTER: + iRes = (iRes &^ TP_VMASK) | TP_CENTER + case T_BELOW: + iRes = (iRes &^ TP_VMASK) | TP_BELOW + case T_ITALIC: + iRes |= TP_ITALIC + case T_BOLD: + iRes |= TP_BOLD + case T_ALIGNED: + iRes |= TP_ALIGN + case T_BIG: + if iRes&TP_BIG != 0 { + iRes |= TP_XTRA + } else { + iRes = (iRes &^ TP_SZMASK) | TP_BIG + } + case T_SMALL: + if iRes&TP_SMALL != 0 { + iRes |= TP_XTRA + } else { + iRes = (iRes &^ TP_SZMASK) | TP_SMALL + } + } + return iRes +} + +/* +** Table of scale-factor estimates for variable-width characters. +** Actual character widths vary by font. These numbers are only +** guesses. And this table only provides data for ASCII. +** +** 100 means normal width. + */ +var awChar = []byte{ + /* Skip initial 32 control characters */ + /* ' ' */ 45, + /* '!' */ 55, + /* '"' */ 62, + /* '#' */ 115, + /* '$' */ 90, + /* '%' */ 132, + /* '&' */ 125, + /* '\''*/ 40, + + /* '(' */ 55, + /* ')' */ 55, + /* '*' */ 71, + /* '+' */ 115, + /* ',' */ 45, + /* '-' */ 48, + /* '.' */ 45, + /* '/' */ 50, + + /* '0' */ 91, + /* '1' */ 91, + /* '2' */ 91, + /* '3' */ 91, + /* '4' */ 91, + /* '5' */ 91, + /* '6' */ 91, + /* '7' */ 91, + + /* '8' */ 91, + /* '9' */ 91, + /* ':' */ 50, + /* ';' */ 50, + /* '<' */ 120, + /* '=' */ 120, + /* '>' */ 120, + /* '?' */ 78, + + /* '@' */ 142, + /* 'A' */ 102, + /* 'B' */ 105, + /* 'C' */ 110, + /* 'D' */ 115, + /* 'E' */ 105, + /* 'F' */ 98, + /* 'G' */ 105, + + /* 'H' */ 125, + /* 'I' */ 58, + /* 'J' */ 58, + /* 'K' */ 107, + /* 'L' */ 95, + /* 'M' */ 145, + /* 'N' */ 125, + /* 'O' */ 115, + + /* 'P' */ 95, + /* 'Q' */ 115, + /* 'R' */ 107, + /* 'S' */ 95, + /* 'T' */ 97, + /* 'U' */ 118, + /* 'V' */ 102, + /* 'W' */ 150, + + /* 'X' */ 100, + /* 'Y' */ 93, + /* 'Z' */ 100, + /* '[' */ 58, + /* '\\'*/ 50, + /* ']' */ 58, + /* '^' */ 119, + /* '_' */ 72, + + /* '`' */ 72, + /* 'a' */ 86, + /* 'b' */ 92, + /* 'c' */ 80, + /* 'd' */ 92, + /* 'e' */ 85, + /* 'f' */ 52, + /* 'g' */ 92, + + /* 'h' */ 92, + /* 'i' */ 47, + /* 'j' */ 47, + /* 'k' */ 88, + /* 'l' */ 48, + /* 'm' */ 135, + /* 'n' */ 92, + /* 'o' */ 86, + + /* 'p' */ 92, + /* 'q' */ 92, + /* 'r' */ 69, + /* 's' */ 75, + /* 't' */ 58, + /* 'u' */ 92, + /* 'v' */ 80, + /* 'w' */ 121, + + /* 'x' */ 81, + /* 'y' */ 80, + /* 'z' */ 76, + /* '{' */ 91, + /* '|'*/ 49, + /* '}' */ 91, + /* '~' */ 118, +} + +/* Return an estimate of the width of the displayed characters +** in a character string. The returned value is 100 times the +** average character width. +** +** Omit "\" used to escape characters. And count entities like +** "<" as a single character. Multi-byte UTF8 characters count +** as a single character. +** +** Attempt to scale the answer by the actual characters seen. Wide +** characters count more than narrow characters. But the widths are +** only guesses. + */ +func pik_text_length(pToken PToken) int { + n := pToken.n + z := pToken.z + cnt := 0 + for j := 1; j < n-1; j++ { + c := z[j] + if c == '\\' && z[j+1] != '&' { + j++ + c = z[j] + } else if c == '&' { + var k int + for k = j + 1; k < j+7 && z[k] != 0 && z[k] != ';'; k++ { + } + if z[k] == ';' { + j = k + } + cnt += 150 + continue + } + if (c & 0xc0) == 0xc0 { + for j+1 < n-1 && (z[j+1]&0xc0) == 0x80 { + j++ + } + cnt += 100 + continue + } + if c >= 0x20 && c <= 0x7e { + cnt += int(awChar[int(c-0x20)]) + } else { + cnt += 100 + } + } + return cnt +} + +/* Adjust the width, height, and/or radius of the object so that +** it fits around the text that has been added so far. +** +** (1) Only text specified prior to this attribute is considered. +** (2) The text size is estimated based on the charht and charwid +** variable settings. +** (3) The fitted attributes can be changed again after this +** attribute, for example using "width 110%" if this auto-fit +** underestimates the text size. +** (4) Previously set attributes will not be altered. In other words, +** "width 1in fit" might cause the height to change, but the +** width is now set. +** (5) This only works for attributes that have an xFit method. +** +** The eWhich parameter is: +** +** 1: Fit horizontally only +** 2: Fit vertically only +** 3: Fit both ways + */ +func (p *Pik) pik_size_to_fit(pFit *PToken, eWhich int) { + var w, h PNum + var bbox PBox + + if p.nErr != 0 { + return + } + pObj := p.cur + + if pObj.nTxt == 0 { + (*Pik)(nil).pik_error(pFit, "no text to fit to") + return + } + if pObj.typ.xFit == nil { + return + } + pik_bbox_init(&bbox) + p.pik_compute_layout_settings() + p.pik_append_txt(pObj, &bbox) + if eWhich&1 != 0 { + w = (bbox.ne.x - bbox.sw.x) + p.charWidth + } + if eWhich&2 != 0 { + var h1, h2 PNum + h1 = bbox.ne.y - pObj.ptAt.y + h2 = pObj.ptAt.y - bbox.sw.y + hmax := h1 + if h1 < h2 { + hmax = h2 + } + h = 2.0*hmax + 0.5*p.charHeight + } else { + h = 0 + } + pObj.typ.xFit(p, pObj, w, h) + pObj.mProp |= A_FIT +} + +/* Set a local variable name to "val". +** +** The name might be a built-in variable or a color name. In either case, +** a new application-defined variable is set. Since app-defined variables +** are searched first, this will override any built-in variables. + */ +func (p *Pik) pik_set_var(pId *PToken, val PNum, pOp *PToken) { + pVar := p.pVar + for pVar != nil { + if pik_token_eq(pId, pVar.zName) == 0 { + break + } + pVar = pVar.pNext + } + if pVar == nil { + pVar = &PVar{ + zName: pId.String(), + pNext: p.pVar, + val: p.pik_value(pId.String(), nil), + } + p.pVar = pVar + } + switch pOp.eCode { + case T_PLUS: + pVar.val += val + case T_STAR: + pVar.val *= val + case T_MINUS: + pVar.val -= val + case T_SLASH: + if val == 0.0 { + p.pik_error(pOp, "division by zero") + } else { + pVar.val /= val + } + default: + pVar.val = val + } + p.bLayoutVars = false /* Clear the layout setting cache */ +} + +/* +** Round a PNum into the nearest integer + */ +func pik_round(v PNum) int { + switch { + case math.IsNaN(v): + return 0 + case v < -2147483647: + return (-2147483647 - 1) + case v >= 2147483647: + return 2147483647 + default: + return int(v + math.Copysign(1e-15, v)) + } +} + +/* +** Search for the variable named z[0..n-1] in: +** +** * Application defined variables +** * Built-in variables +** +** Return the value of the variable if found. If not found +** return 0.0. Also if pMiss is not NULL, then set it to 1 +** if not found. +** +** This routine is a subroutine to pik_get_var(). But it is also +** used by object implementations to look up (possibly overwritten) +** values for built-in variables like "boxwid". + */ +func (p *Pik) pik_value(z string, pMiss *bool) PNum { + for pVar := p.pVar; pVar != nil; pVar = pVar.pNext { + if pVar.zName == z { + return pVar.val + } + } + first := 0 + last := len(aBuiltin) - 1 + for first <= last { + mid := (first + last) / 2 + zName := aBuiltin[mid].zName + + if zName == z { + return aBuiltin[mid].val + } else if z > zName { + first = mid + 1 + } else { + last = mid - 1 + } + } + if pMiss != nil { + *pMiss = true + } + return 0.0 +} + +func (p *Pik) pik_value_int(z string, pMiss *bool) int { + return pik_round(p.pik_value(z, pMiss)) +} + +/* +** Look up a color-name. Unlike other names in this program, the +** color-names are not case sensitive. So "DarkBlue" and "darkblue" +** and "DARKBLUE" all find the same value (139). +** +** If not found, return -99.0. Also post an error if p!=NULL. +** +** Special color names "None" and "Off" return -1.0 without causing +** an error. + */ +func (p *Pik) pik_lookup_color(pId *PToken) PNum { + first := 0 + last := len(aColor) - 1 + zId := strings.ToLower(pId.String()) + for first <= last { + mid := (first + last) / 2 + zClr := strings.ToLower(aColor[mid].zName) + c := strings.Compare(zId, zClr) + + if c == 0 { + return PNum(aColor[mid].val) + } + if c > 0 { + first = mid + 1 + } else { + last = mid - 1 + } + } + if p != nil { + p.pik_error(pId, "not a known color name") + } + return -99.0 +} + +/* Get the value of a variable. +** +** Search in order: +** +** * Application defined variables +** * Built-in variables +** * Color names +** +** If no such variable is found, throw an error. + */ +func (p *Pik) pik_get_var(pId *PToken) PNum { + miss := false + v := p.pik_value(pId.String(), &miss) + if !miss { + return v + } + v = (*Pik)(nil).pik_lookup_color(pId) + if v > -90.0 { + return v + } + p.pik_error(pId, "no such variable") + return 0.0 +} + +/* Convert a T_NTH token (ex: "2nd", "5th"} into a numeric value and + ** return that value. Throw an error if the value is too big. + */ +func (p *Pik) pik_nth_value(pNth *PToken) int16 { + s := pNth.String() + if s == "first" { + return 1 + } + + i, err := strconv.Atoi(s[:len(s)-2]) + if err != nil { + p.pik_error(pNth, "value can't be parsed as a number") + } + if i > 1000 { + p.pik_error(pNth, "value too big - max '1000th'") + i = 1 + } + return int16(i) +} + +/* Search for the NTH object. +** +** If pBasis is not NULL then it should be a [] object. Use the +** sublist of that [] object for the search. If pBasis is not a [] +** object, then throw an error. +** +** The pNth token describes the N-th search. The pNth.eCode value +** is one more than the number of items to skip. It is negative +** to search backwards. If pNth.eType==T_ID, then it is the name +** of a class to search for. If pNth.eType==T_LB, then +** search for a [] object. If pNth.eType==T_LAST, then search for +** any type. +** +** Raise an error if the item is not found. + */ +func (p *Pik) pik_find_nth(pBasis *PObj, pNth *PToken) *PObj { + var pList PList + var pClass *PClass + if pBasis == nil { + pList = p.list + } else { + pList = pBasis.pSublist + } + if pList == nil { + p.pik_error(pNth, "no such object") + return nil + } + if pNth.eType == T_LAST { + pClass = nil + } else if pNth.eType == T_LB { + pClass = &sublistClass + } else { + pClass = pik_find_class(pNth) + if pClass == nil { + (*Pik)(nil).pik_error(pNth, "no such object type") + return nil + } + } + n := pNth.eCode + if n < 0 { + for i := len(pList) - 1; i >= 0; i-- { + pObj := pList[i] + if pClass != nil && pObj.typ != pClass { + continue + } + n++ + if n == 0 { + return pObj + } + } + } else { + for i := 0; i < len(pList); i++ { + pObj := pList[i] + if pClass != nil && pObj.typ != pClass { + continue + } + n-- + if n == 0 { + return pObj + } + } + } + p.pik_error(pNth, "no such object") + return nil +} + +/* Search for an object by name. +** +** Search in pBasis.pSublist if pBasis is not NULL. If pBasis is NULL +** then search in p.list. + */ +func (p *Pik) pik_find_byname(pBasis *PObj, pName *PToken) *PObj { + var pList PList + if pBasis == nil { + pList = p.list + } else { + pList = pBasis.pSublist + } + if pList == nil { + p.pik_error(pName, "no such object") + return nil + } + /* First look explicitly tagged objects */ + for i := len(pList) - 1; i >= 0; i-- { + pObj := pList[i] + if pObj.zName != "" && pik_token_eq(pName, pObj.zName) == 0 { + p.lastRef = pObj + return pObj + } + } + /* If not found, do a second pass looking for any object containing + ** text which exactly matches pName */ + for i := len(pList) - 1; i >= 0; i-- { + pObj := pList[i] + for j := 0; j < int(pObj.nTxt); j++ { + t := pObj.aTxt[j].n + if t == pName.n+2 && bytesEq(pObj.aTxt[j].z[1:t-1], pName.z[:pName.n]) { + p.lastRef = pObj + return pObj + } + } + } + p.pik_error(pName, "no such object") + return nil +} + +/* Change most of the settings for the current object to be the +** same as the pOther object, or the most recent object of the same +** type if pOther is NULL. + */ +func (p *Pik) pik_same(pOther *PObj, pErrTok *PToken) { + pObj := p.cur + if p.nErr != 0 { + return + } + if pOther == nil { + var i int + for i = len(p.list) - 1; i >= 0; i-- { + pOther = p.list[i] + if pOther.typ == pObj.typ { + break + } + } + if i < 0 { + p.pik_error(pErrTok, "no prior objects of the same type") + return + } + } + if pOther.nPath != 0 && pObj.typ.isLine { + var dx, dy PNum + dx = p.aTPath[0].x - pOther.aPath[0].x + dy = p.aTPath[0].y - pOther.aPath[0].y + for i := 1; i < pOther.nPath; i++ { + p.aTPath[i].x = pOther.aPath[i].x + dx + p.aTPath[i].y = pOther.aPath[i].y + dy + } + p.nTPath = pOther.nPath + p.mTPath = 3 + p.samePath = true + } + if !pObj.typ.isLine { + pObj.w = pOther.w + pObj.h = pOther.h + } + pObj.rad = pOther.rad + pObj.sw = pOther.sw + pObj.dashed = pOther.dashed + pObj.dotted = pOther.dotted + pObj.fill = pOther.fill + pObj.color = pOther.color + pObj.cw = pOther.cw + pObj.larrow = pOther.larrow + pObj.rarrow = pOther.rarrow + pObj.bClose = pOther.bClose + pObj.bChop = pOther.bChop + pObj.inDir = pOther.inDir + pObj.outDir = pOther.outDir + pObj.iLayer = pOther.iLayer +} + +/* Return a "Place" associated with object pObj. If pEdge is NULL +** return the center of the object. Otherwise, return the corner +** described by pEdge. + */ +func (p *Pik) pik_place_of_elem(pObj *PObj, pEdge *PToken) PPoint { + pt := PPoint{} + var pClass *PClass + if pObj == nil { + return pt + } + if pEdge == nil { + return pObj.ptAt + } + pClass = pObj.typ + if pEdge.eType == T_EDGEPT || (pEdge.eEdge > 0 && pEdge.eEdge < CP_END) { + pt = pClass.xOffset(p, pObj, pEdge.eEdge) + pt.x += pObj.ptAt.x + pt.y += pObj.ptAt.y + return pt + } + if pEdge.eType == T_START { + return pObj.ptEnter + } else { + return pObj.ptExit + } +} + +/* Do a linear interpolation of two positions. + */ +func pik_position_between(x PNum, p1 PPoint, p2 PPoint) PPoint { + var out PPoint + out.x = p2.x*x + p1.x*(1.0-x) + out.y = p2.y*x + p1.y*(1.0-x) + return out +} + +/* Compute the position that is dist away from pt at an heading angle of r +** +** The angle is a compass heading in degrees. North is 0 (or 360). +** East is 90. South is 180. West is 270. And so forth. + */ +func pik_position_at_angle(dist PNum, r PNum, pt PPoint) PPoint { + r *= 0.017453292519943295769 /* degrees to radians */ + pt.x += dist * math.Sin(r) + pt.y += dist * math.Cos(r) + return pt +} + +/* Compute the position that is dist away at a compass point + */ +func pik_position_at_hdg(dist PNum, pD *PToken, pt PPoint) PPoint { + return pik_position_at_angle(dist, pik_hdg_angle[pD.eEdge], pt) +} + +/* Return the coordinates for the n-th vertex of a line. + */ +func (p *Pik) pik_nth_vertex(pNth *PToken, pErr *PToken, pObj *PObj) PPoint { + var n int + zero := PPoint{} + if p.nErr != 0 || pObj == nil { + return p.aTPath[0] + } + if !pObj.typ.isLine { + p.pik_error(pErr, "object is not a line") + return zero + } + n, err := strconv.Atoi(string(pNth.z[:pNth.n-2])) + if err != nil || n < 1 || n > pObj.nPath { + p.pik_error(pNth, "no such vertex") + return zero + } + return pObj.aPath[n-1] +} + +/* Return the value of a property of an object. + */ +func pik_property_of(pObj *PObj, pProp *PToken) PNum { + var v PNum + if pObj != nil { + switch pProp.eType { + case T_HEIGHT: + v = pObj.h + case T_WIDTH: + v = pObj.w + case T_RADIUS: + v = pObj.rad + case T_DIAMETER: + v = pObj.rad * 2.0 + case T_THICKNESS: + v = pObj.sw + case T_DASHED: + v = pObj.dashed + case T_DOTTED: + v = pObj.dotted + case T_FILL: + v = pObj.fill + case T_COLOR: + v = pObj.color + case T_X: + v = pObj.ptAt.x + case T_Y: + v = pObj.ptAt.y + case T_TOP: + v = pObj.bbox.ne.y + case T_BOTTOM: + v = pObj.bbox.sw.y + case T_LEFT: + v = pObj.bbox.sw.x + case T_RIGHT: + v = pObj.bbox.ne.x + } + } + return v +} + +/* Compute one of the built-in functions + */ +func (p *Pik) pik_func(pFunc *PToken, x PNum, y PNum) PNum { + var v PNum + switch pFunc.eCode { + case FN_ABS: + v = x + if v < 0 { + v = -v + } + case FN_COS: + v = math.Cos(x) + case FN_INT: + v = math.Round(x) + case FN_SIN: + v = math.Sin(x) + case FN_SQRT: + if x < 0.0 { + p.pik_error(pFunc, "sqrt of negative value") + v = 0.0 + } else { + v = math.Sqrt(x) + } + case FN_MAX: + if x > y { + v = x + } else { + v = y + } + case FN_MIN: + if x < y { + v = x + } else { + v = y + } + default: + v = 0.0 + } + return v +} + +/* Attach a name to an object + */ +func (p *Pik) pik_elem_setname(pObj *PObj, pName *PToken) { + if pObj == nil { + return + } + if pName == nil { + return + } + pObj.zName = pName.String() +} + +/* +** Search for object located at *pCenter that has an xChop method and +** that does not enclose point pOther. +** +** Return a pointer to the object, or NULL if not found. + */ +func pik_find_chopper(pList PList, pCenter *PPoint, pOther *PPoint) *PObj { + if pList == nil { + return nil + } + for i := len(pList) - 1; i >= 0; i-- { + pObj := pList[i] + if pObj.typ.xChop != nil && + pObj.ptAt.x == pCenter.x && + pObj.ptAt.y == pCenter.y && + !pik_bbox_contains_point(&pObj.bbox, pOther) { + return pObj + } else if pObj.pSublist != nil { + pObj = pik_find_chopper(pObj.pSublist, pCenter, pOther) + if pObj != nil { + return pObj + } + } + } + return nil +} + +/* +** There is a line traveling from pFrom to pTo. +** +** If pObj is not null and is a choppable object, then chop at +** the boundary of pObj - where the line crosses the boundary +** of pObj. +** +** If pObj is NULL or has no xChop method, then search for some +** other object centered at pTo that is choppable and use it +** instead. + */ +func (p *Pik) pik_autochop(pFrom *PPoint, pTo *PPoint, pObj *PObj) { + if pObj == nil || pObj.typ.xChop == nil { + pObj = pik_find_chopper(p.list, pTo, pFrom) + } + if pObj != nil { + *pTo = pObj.typ.xChop(p, pObj, pFrom) + } +} + +/* This routine runs after all attributes have been received +** on an object. + */ +func (p *Pik) pik_after_adding_attributes(pObj *PObj) { + if p.nErr != 0 { + return + } + + /* Position block objects */ + if !pObj.typ.isLine { + /* A height or width less than or equal to zero means "autofit". + ** Change the height or width to be big enough to contain the text, + */ + if pObj.h <= 0.0 { + if pObj.nTxt == 0 { + pObj.h = 0.0 + } else if pObj.w <= 0.0 { + p.pik_size_to_fit(&pObj.errTok, 3) + } else { + p.pik_size_to_fit(&pObj.errTok, 2) + } + } + if pObj.w <= 0.0 { + if pObj.nTxt == 0 { + pObj.w = 0.0 + } else { + p.pik_size_to_fit(&pObj.errTok, 1) + } + } + ofst := p.pik_elem_offset(pObj, pObj.eWith) + var dx PNum = (pObj.with.x - ofst.x) - pObj.ptAt.x + var dy PNum = (pObj.with.y - ofst.y) - pObj.ptAt.y + if dx != 0 || dy != 0 { + pik_elem_move(pObj, dx, dy) + } + } + + /* For a line object with no movement specified, a single movement + ** of the default length in the current direction + */ + if pObj.typ.isLine && p.nTPath < 2 { + p.pik_next_rpath(nil) + assert(p.nTPath == 2, fmt.Sprintf("want p.nTPath==2; got %d", p.nTPath)) + switch pObj.inDir { + default: + p.aTPath[1].x += pObj.w + case DIR_DOWN: + p.aTPath[1].y -= pObj.h + case DIR_LEFT: + p.aTPath[1].x -= pObj.w + case DIR_UP: + p.aTPath[1].y += pObj.h + } + if pObj.typ.zName == "arc" { + add := uint8(3) + if pObj.cw { + add = 1 + } + pObj.outDir = (pObj.inDir + add) % 4 + p.eDir = pObj.outDir + switch pObj.outDir { + default: + p.aTPath[1].x += pObj.w + case DIR_DOWN: + p.aTPath[1].y -= pObj.h + case DIR_LEFT: + p.aTPath[1].x -= pObj.w + case DIR_UP: + p.aTPath[1].y += pObj.h + } + } + } + + /* Initialize the bounding box prior to running xCheck */ + pik_bbox_init(&pObj.bbox) + + /* Run object-specific code */ + if pObj.typ.xCheck != nil { + pObj.typ.xCheck(p, pObj) + if p.nErr != 0 { + return + } + } + + /* Compute final bounding box, entry and exit points, center + ** point (ptAt) and path for the object + */ + if pObj.typ.isLine { + pObj.aPath = make([]PPoint, p.nTPath) + pObj.nPath = p.nTPath + copy(pObj.aPath, p.aTPath[:p.nTPath]) + + /* "chop" processing: + ** If the line goes to the center of an object with an + ** xChop method, then use the xChop method to trim the line. + */ + if pObj.bChop && pObj.nPath >= 2 { + n := pObj.nPath + p.pik_autochop(&pObj.aPath[n-2], &pObj.aPath[n-1], pObj.pTo) + p.pik_autochop(&pObj.aPath[1], &pObj.aPath[0], pObj.pFrom) + } + + pObj.ptEnter = pObj.aPath[0] + pObj.ptExit = pObj.aPath[pObj.nPath-1] + + /* Compute the center of the line based on the bounding box over + ** the vertexes. This is a difference from PIC. In Pikchr, the + ** center of a line is the center of its bounding box. In PIC, the + ** center of a line is halfway between its .start and .end. For + ** straight lines, this is the same point, but for multi-segment + ** lines the result is usually diferent */ + for i := 0; i < pObj.nPath; i++ { + pik_bbox_add_xy(&pObj.bbox, pObj.aPath[i].x, pObj.aPath[i].y) + } + pObj.ptAt.x = (pObj.bbox.ne.x + pObj.bbox.sw.x) / 2.0 + pObj.ptAt.y = (pObj.bbox.ne.y + pObj.bbox.sw.y) / 2.0 + + /* Reset the width and height of the object to be the width and height + ** of the bounding box over vertexes */ + pObj.w = pObj.bbox.ne.x - pObj.bbox.sw.x + pObj.h = pObj.bbox.ne.y - pObj.bbox.sw.y + + /* If this is a polygon (if it has the "close" attribute), then + ** adjust the exit point */ + if pObj.bClose { + /* For "closed" lines, the .end is one of the .e, .s, .w, or .n + ** points of the bounding box, as with block objects. */ + pik_elem_set_exit(pObj, pObj.inDir) + } + } else { + var w2 PNum = pObj.w / 2.0 + var h2 PNum = pObj.h / 2.0 + pObj.ptEnter = pObj.ptAt + pObj.ptExit = pObj.ptAt + switch pObj.inDir { + default: + pObj.ptEnter.x -= w2 + case DIR_LEFT: + pObj.ptEnter.x += w2 + case DIR_UP: + pObj.ptEnter.y -= h2 + case DIR_DOWN: + pObj.ptEnter.y += h2 + } + switch pObj.outDir { + default: + pObj.ptExit.x += w2 + case DIR_LEFT: + pObj.ptExit.x -= w2 + case DIR_UP: + pObj.ptExit.y += h2 + case DIR_DOWN: + pObj.ptExit.y -= h2 + } + pik_bbox_add_xy(&pObj.bbox, pObj.ptAt.x-w2, pObj.ptAt.y-h2) + pik_bbox_add_xy(&pObj.bbox, pObj.ptAt.x+w2, pObj.ptAt.y+h2) + } + p.eDir = pObj.outDir +} + +/* Show basic information about each object as a comment in the +** generated HTML. Used for testing and debugging. Activated +** by the (undocumented) "debug = 1;" +** command. + */ +func (p *Pik) pik_elem_render(pObj *PObj) { + var zDir string + if pObj == nil { + return + } + p.pik_append("\n") +} + +/* Render a list of objects + */ +func (p *Pik) pik_elist_render(pList PList) { + var iNextLayer, iThisLayer int + bMoreToDo := true + mDebug := p.pik_value_int("debug", nil) + for bMoreToDo { + bMoreToDo = false + iThisLayer = iNextLayer + iNextLayer = 0x7fffffff + for i := 0; i < len(pList); i++ { + pObj := pList[i] + if pObj.iLayer > iThisLayer { + if pObj.iLayer < iNextLayer { + iNextLayer = pObj.iLayer + } + bMoreToDo = true + continue /* Defer until another round */ + } else if pObj.iLayer < iThisLayer { + continue + } + if mDebug&1 != 0 { + p.pik_elem_render(pObj) + } + xRender := pObj.typ.xRender + if xRender != nil { + xRender(p, pObj) + } + if pObj.pSublist != nil { + p.pik_elist_render(pObj.pSublist) + } + } + } + + /* If the color_debug_label value is defined, then go through + ** and paint a dot at every label location */ + miss := false + var colorLabel PNum = p.pik_value("debug_label_color", &miss) + if !miss && colorLabel >= 0.0 { + dot := PObj{} + dot.typ = &noopClass + dot.rad = 0.015 + dot.sw = 0.015 + dot.fill = colorLabel + dot.color = colorLabel + dot.nTxt = 1 + dot.aTxt[0].eCode = TP_ABOVE + for i := 0; i < len(pList); i++ { + pObj := pList[i] + if pObj.zName == "" { + continue + } + dot.ptAt = pObj.ptAt + dot.aTxt[0].z = []byte(pObj.zName) + dot.aTxt[0].n = len(dot.aTxt[0].z) + dotRender(p, &dot) + } + } +} + +/* Add all objects of the list pList to the bounding box + */ +func (p *Pik) pik_bbox_add_elist(pList PList, wArrow PNum) { + for i := 0; i < len(pList); i++ { + pObj := pList[i] + if pObj.sw > 0.0 { + pik_bbox_addbox(&p.bbox, &pObj.bbox) + } + p.pik_append_txt(pObj, &p.bbox) + if pObj.pSublist != nil { + p.pik_bbox_add_elist(pObj.pSublist, wArrow) + } + + /* Expand the bounding box to account for arrowheads on lines */ + if pObj.typ.isLine && pObj.nPath > 0 { + if pObj.larrow { + pik_bbox_addellipse(&p.bbox, pObj.aPath[0].x, pObj.aPath[0].y, + wArrow, wArrow) + } + if pObj.rarrow { + j := pObj.nPath - 1 + pik_bbox_addellipse(&p.bbox, pObj.aPath[j].x, pObj.aPath[j].y, + wArrow, wArrow) + } + } + } +} + +/* Recompute key layout parameters from variables. */ +func (p *Pik) pik_compute_layout_settings() { + var thickness PNum /* Line thickness */ + var wArrow PNum /* Width of arrowheads */ + + /* Set up rendering parameters */ + if p.bLayoutVars { + return + } + thickness = p.pik_value("thickness", nil) + if thickness <= 0.01 { + thickness = 0.01 + } + wArrow = 0.5 * p.pik_value("arrowwid", nil) + p.wArrow = wArrow / thickness + p.hArrow = p.pik_value("arrowht", nil) / thickness + p.fontScale = p.pik_value("fontscale", nil) + if p.fontScale <= 0.0 { + p.fontScale = 1.0 + } + p.rScale = 144.0 + p.charWidth = p.pik_value("charwid", nil) * p.fontScale + p.charHeight = p.pik_value("charht", nil) * p.fontScale + p.bLayoutVars = true +} + +/* Render a list of objects. Write the SVG into p.zOut. +** Delete the input object_list before returnning. + */ +func (p *Pik) pik_render(pList PList) { + if pList == nil { + return + } + if p.nErr == 0 { + var ( + thickness PNum /* Stroke width */ + margin PNum /* Extra bounding box margin */ + w, h PNum /* Drawing width and height */ + wArrow PNum + pikScale PNum /* Value of the "scale" variable */ + ) + + /* Set up rendering parameters */ + p.pik_compute_layout_settings() + thickness = p.pik_value("thickness", nil) + if thickness <= 0.01 { + thickness = 0.01 + } + margin = p.pik_value("margin", nil) + margin += thickness + wArrow = p.wArrow * thickness + miss := false + p.fgcolor = p.pik_value_int("fgcolor", &miss) + if miss { + var t PToken + t.z = []byte("fgcolor") + t.n = 7 + p.fgcolor = pik_round((*Pik)(nil).pik_lookup_color(&t)) + } + miss = false + p.bgcolor = p.pik_value_int("bgcolor", &miss) + if miss { + var t PToken + t.z = []byte("bgcolor") + t.n = 7 + p.bgcolor = pik_round((*Pik)(nil).pik_lookup_color(&t)) + } + + /* Compute a bounding box over all objects so that we can know + ** how big to declare the SVG canvas */ + pik_bbox_init(&p.bbox) + p.pik_bbox_add_elist(pList, wArrow) + + /* Expand the bounding box slightly to account for line thickness + ** and the optional "margin = EXPR" setting. */ + p.bbox.ne.x += margin + p.pik_value("rightmargin", nil) + p.bbox.ne.y += margin + p.pik_value("topmargin", nil) + p.bbox.sw.x -= margin + p.pik_value("leftmargin", nil) + p.bbox.sw.y -= margin + p.pik_value("bottommargin", nil) + + /* Output the SVG */ + p.pik_append("= 0.001 && pikScale <= 1000.0 && + (pikScale < 0.99 || pikScale > 1.01) { + p.wSVG = pik_round(PNum(p.wSVG) * pikScale) + p.hSVG = pik_round(PNum(p.hSVG) * pikScale) + p.pik_append_num(" width=\"", PNum(p.wSVG)) + p.pik_append_num("\" height=\"", PNum(p.hSVG)) + p.pik_append("\"") + } else { + if p.svgWidth != "" { + p.pik_append(` width="`) + p.pik_append(p.svgWidth) + p.pik_append(`"`) + } + if p.svgHeight != "" { + p.pik_append(` height="`) + p.pik_append(p.svgHeight) + p.pik_append(`"`) + } + } + p.pik_append_dis(" viewBox=\"0 0 ", w, "") + p.pik_append_dis(" ", h, "\">\n") + p.pik_elist_render(pList) + p.pik_append("\n") + } else { + p.wSVG = -1 + p.hSVG = -1 + } + p.pik_elist_free(&pList) +} + +/* +** An array of this structure defines a list of keywords. + */ +type PikWord struct { + zWord string /* Text of the keyword */ + //TODO(zellyn): do we need this? + nChar uint8 /* Length of keyword text in bytes */ + eType uint8 /* Token code */ + eCode uint8 /* Extra code for the token */ + eEdge uint8 /* CP_* code for corner/edge keywords */ +} + +/* +** Keywords + */ +var pik_keywords = []PikWord{ + {"above", 5, T_ABOVE, 0, 0}, + {"abs", 3, T_FUNC1, FN_ABS, 0}, + {"aligned", 7, T_ALIGNED, 0, 0}, + {"and", 3, T_AND, 0, 0}, + {"as", 2, T_AS, 0, 0}, + {"assert", 6, T_ASSERT, 0, 0}, + {"at", 2, T_AT, 0, 0}, + {"behind", 6, T_BEHIND, 0, 0}, + {"below", 5, T_BELOW, 0, 0}, + {"between", 7, T_BETWEEN, 0, 0}, + {"big", 3, T_BIG, 0, 0}, + {"bold", 4, T_BOLD, 0, 0}, + {"bot", 3, T_EDGEPT, 0, CP_S}, + {"bottom", 6, T_BOTTOM, 0, CP_S}, + {"c", 1, T_EDGEPT, 0, CP_C}, + {"ccw", 3, T_CCW, 0, 0}, + {"center", 6, T_CENTER, 0, CP_C}, + {"chop", 4, T_CHOP, 0, 0}, + {"close", 5, T_CLOSE, 0, 0}, + {"color", 5, T_COLOR, 0, 0}, + {"cos", 3, T_FUNC1, FN_COS, 0}, + {"cw", 2, T_CW, 0, 0}, + {"dashed", 6, T_DASHED, 0, 0}, + {"define", 6, T_DEFINE, 0, 0}, + {"diameter", 8, T_DIAMETER, 0, 0}, + {"dist", 4, T_DIST, 0, 0}, + {"dotted", 6, T_DOTTED, 0, 0}, + {"down", 4, T_DOWN, DIR_DOWN, 0}, + {"e", 1, T_EDGEPT, 0, CP_E}, + {"east", 4, T_EDGEPT, 0, CP_E}, + {"end", 3, T_END, 0, CP_END}, + {"even", 4, T_EVEN, 0, 0}, + {"fill", 4, T_FILL, 0, 0}, + {"first", 5, T_NTH, 0, 0}, + {"fit", 3, T_FIT, 0, 0}, + {"from", 4, T_FROM, 0, 0}, + {"go", 2, T_GO, 0, 0}, + {"heading", 7, T_HEADING, 0, 0}, + {"height", 6, T_HEIGHT, 0, 0}, + {"ht", 2, T_HEIGHT, 0, 0}, + {"in", 2, T_IN, 0, 0}, + {"int", 3, T_FUNC1, FN_INT, 0}, + {"invis", 5, T_INVIS, 0, 0}, + {"invisible", 9, T_INVIS, 0, 0}, + {"italic", 6, T_ITALIC, 0, 0}, + {"last", 4, T_LAST, 0, 0}, + {"left", 4, T_LEFT, DIR_LEFT, CP_W}, + {"ljust", 5, T_LJUST, 0, 0}, + {"max", 3, T_FUNC2, FN_MAX, 0}, + {"min", 3, T_FUNC2, FN_MIN, 0}, + {"n", 1, T_EDGEPT, 0, CP_N}, + {"ne", 2, T_EDGEPT, 0, CP_NE}, + {"north", 5, T_EDGEPT, 0, CP_N}, + {"nw", 2, T_EDGEPT, 0, CP_NW}, + {"of", 2, T_OF, 0, 0}, + {"previous", 8, T_LAST, 0, 0}, + {"print", 5, T_PRINT, 0, 0}, + {"rad", 3, T_RADIUS, 0, 0}, + {"radius", 6, T_RADIUS, 0, 0}, + {"right", 5, T_RIGHT, DIR_RIGHT, CP_E}, + {"rjust", 5, T_RJUST, 0, 0}, + {"s", 1, T_EDGEPT, 0, CP_S}, + {"same", 4, T_SAME, 0, 0}, + {"se", 2, T_EDGEPT, 0, CP_SE}, + {"sin", 3, T_FUNC1, FN_SIN, 0}, + {"small", 5, T_SMALL, 0, 0}, + {"solid", 5, T_SOLID, 0, 0}, + {"south", 5, T_EDGEPT, 0, CP_S}, + {"sqrt", 4, T_FUNC1, FN_SQRT, 0}, + {"start", 5, T_START, 0, CP_START}, + {"sw", 2, T_EDGEPT, 0, CP_SW}, + {"t", 1, T_TOP, 0, CP_N}, + {"the", 3, T_THE, 0, 0}, + {"then", 4, T_THEN, 0, 0}, + {"thick", 5, T_THICK, 0, 0}, + {"thickness", 9, T_THICKNESS, 0, 0}, + {"thin", 4, T_THIN, 0, 0}, + {"this", 4, T_THIS, 0, 0}, + {"to", 2, T_TO, 0, 0}, + {"top", 3, T_TOP, 0, CP_N}, + {"until", 5, T_UNTIL, 0, 0}, + {"up", 2, T_UP, DIR_UP, 0}, + {"vertex", 6, T_VERTEX, 0, 0}, + {"w", 1, T_EDGEPT, 0, CP_W}, + {"way", 3, T_WAY, 0, 0}, + {"west", 4, T_EDGEPT, 0, CP_W}, + {"wid", 3, T_WIDTH, 0, 0}, + {"width", 5, T_WIDTH, 0, 0}, + {"with", 4, T_WITH, 0, 0}, + {"x", 1, T_X, 0, 0}, + {"y", 1, T_Y, 0, 0}, +} + +/* +** Search a PikWordlist for the given keyword. Return a pointer to the +** keyword entry found. Or return 0 if not found. + */ +func pik_find_word( + zIn string, /* Word to search for */ + aList []PikWord, /* List to search */ +) *PikWord { + first := 0 + last := len(aList) - 1 + for first <= last { + mid := (first + last) / 2 + c := strings.Compare(zIn, aList[mid].zWord) + if c == 0 { + return &aList[mid] + } + if c < 0 { + last = mid - 1 + } else { + first = mid + 1 + } + } + return nil +} + +/* +** Set a symbolic debugger breakpoint on this routine to receive a +** breakpoint when the "#breakpoint" token is parsed. + */ +func pik_breakpoint(z []byte) { + /* Prevent C compilers from optimizing out this routine. */ + if z[2] == 'X' { + os.Exit(1) + } +} + +var aEntity = []struct { + eCode int /* Corresponding token code */ + zEntity string /* Name of the HTML entity */ +}{ + {T_RARROW, "→"}, /* Same as . */ + {T_RARROW, "→"}, /* Same as . */ + {T_LARROW, "←"}, /* Same as <- */ + {T_LARROW, "←"}, /* Same as <- */ + {T_LRARROW, "↔"}, /* Same as <. */ +} + +/* +** Return the length of next token. The token starts on +** the pToken->z character. Fill in other fields of the +** pToken object as appropriate. + */ +func pik_token_length(pToken *PToken, bAllowCodeBlock bool) int { + z := pToken.z + var i int + switch z[0] { + case '\\': + pToken.eType = T_WHITESPACE + for i = 1; z[i] == '\r' || z[i] == ' ' || z[i] == '\t'; i++ { + } + if z[i] == '\n' { + return i + 1 + } + pToken.eType = T_ERROR + return 1 + + case ';', '\n': + pToken.eType = T_EOL + return 1 + + case '"': + for i = 1; z[i] != 0; i++ { + c := z[i] + if c == '\\' { + if z[i+1] == 0 { + break + } + i++ + continue + } + if c == '"' { + pToken.eType = T_STRING + return i + 1 + } + } + pToken.eType = T_ERROR + return i + + case ' ', '\t', '\f', '\r': + for i = 1; z[i] == ' ' || z[i] == '\t' || z[i] == '\r' || z[i] == '\f'; i++ { + } + pToken.eType = T_WHITESPACE + return i + + case '#': + for i = 1; z[i] != 0 && z[i] != '\n'; i++ { + } + pToken.eType = T_WHITESPACE + /* If the comment is "#breakpoint" then invoke the pik_breakpoint() + ** routine. The pik_breakpoint() routie is a no-op that serves as + ** a convenient place to set a gdb breakpoint when debugging. */ + + if i >= 11 && string(z[:11]) == "#breakpoint" { + pik_breakpoint(z) + } + return i + + case '/': + if z[1] == '*' { + for i = 2; z[i] != 0 && (z[i] != '*' || z[i+1] != '/'); i++ { + } + if z[i] == '*' { + pToken.eType = T_WHITESPACE + return i + 2 + } else { + pToken.eType = T_ERROR + return i + } + } else if z[1] == '/' { + for i = 2; z[i] != 0 && z[i] != '\n'; i++ { + } + pToken.eType = T_WHITESPACE + return i + } else if z[1] == '=' { + pToken.eType = T_ASSIGN + pToken.eCode = T_SLASH + return 2 + } else { + pToken.eType = T_SLASH + return 1 + } + + case '+': + if z[1] == '=' { + pToken.eType = T_ASSIGN + pToken.eCode = T_PLUS + return 2 + } + pToken.eType = T_PLUS + return 1 + + case '*': + if z[1] == '=' { + pToken.eType = T_ASSIGN + pToken.eCode = T_STAR + return 2 + } + pToken.eType = T_STAR + return 1 + + case '%': + pToken.eType = T_PERCENT + return 1 + case '(': + pToken.eType = T_LP + return 1 + case ')': + pToken.eType = T_RP + return 1 + case '[': + pToken.eType = T_LB + return 1 + case ']': + pToken.eType = T_RB + return 1 + case ',': + pToken.eType = T_COMMA + return 1 + case ':': + pToken.eType = T_COLON + return 1 + case '>': + pToken.eType = T_GT + return 1 + case '=': + if z[1] == '=' { + pToken.eType = T_EQ + return 2 + } + pToken.eType = T_ASSIGN + pToken.eCode = T_ASSIGN + return 1 + + case '-': + if z[1] == '>' { + pToken.eType = T_RARROW + return 2 + } else if z[1] == '=' { + pToken.eType = T_ASSIGN + pToken.eCode = T_MINUS + return 2 + } else { + pToken.eType = T_MINUS + return 1 + } + + case '<': + if z[1] == '-' { + if z[2] == '>' { + pToken.eType = T_LRARROW + return 3 + } else { + pToken.eType = T_LARROW + return 2 + } + } else { + pToken.eType = T_LT + return 1 + } + + case 0xe2: + if z[1] == 0x86 { + if z[2] == 0x90 { + pToken.eType = T_LARROW /* <- */ + return 3 + } + if z[2] == 0x92 { + pToken.eType = T_RARROW /* . */ + return 3 + } + if z[2] == 0x94 { + pToken.eType = T_LRARROW /* <. */ + return 3 + } + } + pToken.eType = T_ERROR + return 1 + + case '{': + var depth int + i = 1 + if bAllowCodeBlock { + depth = 1 + for z[i] != 0 && depth > 0 { + var x PToken + x.z = z[i:] + len := pik_token_length(&x, false) + if len == 1 { + if z[i] == '{' { + depth++ + } + if z[i] == '}' { + depth-- + } + } + i += len + } + } else { + depth = 0 + } + if depth != 0 { + pToken.eType = T_ERROR + return 1 + } + pToken.eType = T_CODEBLOCK + return i + + case '&': + for i, ent := range aEntity { + if bytencmp(z, aEntity[i].zEntity, len(aEntity[i].zEntity)) == 0 { + pToken.eType = uint8(ent.eCode) + return len(aEntity[i].zEntity) + } + } + pToken.eType = T_ERROR + return 1 + + default: + c := z[0] + if c == '.' { + c1 := z[1] + if islower(c1) { + for i = 2; z[i] >= 'a' && z[i] <= 'z'; i++ { + } + pFound := pik_find_word(string(z[1:i]), pik_keywords) + if pFound != nil && (pFound.eEdge > 0 || + pFound.eType == T_EDGEPT || + pFound.eType == T_START || + pFound.eType == T_END) { + /* Dot followed by something that is a 2-D place value */ + pToken.eType = T_DOT_E + } else if pFound != nil && (pFound.eType == T_X || pFound.eType == T_Y) { + /* Dot followed by "x" or "y" */ + pToken.eType = T_DOT_XY + } else { + /* Any other "dot" */ + pToken.eType = T_DOT_L + } + return 1 + } else if isdigit(c1) { + i = 0 + /* no-op. Fall through to number handling */ + } else if isupper(c1) { + for i = 2; z[i] != 0 && (isalnum(z[i]) || z[i] == '_'); i++ { + } + pToken.eType = T_DOT_U + return 1 + } else { + pToken.eType = T_ERROR + return 1 + } + } + if (c >= '0' && c <= '9') || c == '.' { + var nDigit int + isInt := true + if c != '.' { + nDigit = 1 + for i = 1; ; i++ { + c = z[i] + if c < '0' || c > '9' { + break + } + nDigit++ + } + if i == 1 && (c == 'x' || c == 'X') { + for i = 2; z[i] != 0 && isxdigit(z[i]); i++ { + } + pToken.eType = T_NUMBER + return i + } + } else { + isInt = false + nDigit = 0 + i = 0 + } + if c == '.' { + isInt = false + for i++; ; i++ { + c = z[i] + if c < '0' || c > '9' { + break + } + nDigit++ + } + } + if nDigit == 0 { + pToken.eType = T_ERROR + return i + } + if c == 'e' || c == 'E' { + iBefore := i + i++ + c2 := z[i] + if c2 == '+' || c2 == '-' { + i++ + c2 = z[i] + } + if c2 < '0' || c > '9' { + /* This is not an exp */ + i = iBefore + } else { + i++ + isInt = false + for { + c = z[i] + if c < '0' || c > '9' { + break + } + i++ + } + } + } + var c2 byte + if c != 0 { + c2 = z[i+1] + } + if isInt { + if (c == 't' && c2 == 'h') || + (c == 'r' && c2 == 'd') || + (c == 'n' && c2 == 'd') || + (c == 's' && c2 == 't') { + pToken.eType = T_NTH + return i + 2 + } + } + if (c == 'i' && c2 == 'n') || + (c == 'c' && c2 == 'm') || + (c == 'm' && c2 == 'm') || + (c == 'p' && c2 == 't') || + (c == 'p' && c2 == 'x') || + (c == 'p' && c2 == 'c') { + i += 2 + } + pToken.eType = T_NUMBER + return i + } else if islower(c) { + for i = 1; z[i] != 0 && (isalnum(z[i]) || z[i] == '_'); i++ { + } + pFound := pik_find_word(string(z[:i]), pik_keywords) + if pFound != nil { + pToken.eType = pFound.eType + pToken.eCode = int16(pFound.eCode) + pToken.eEdge = pFound.eEdge + return i + } + pToken.n = i + if pik_find_class(pToken) != nil { + pToken.eType = T_CLASSNAME + } else { + pToken.eType = T_ID + } + return i + } else if c >= 'A' && c <= 'Z' { + for i = 1; z[i] != 0 && (isalnum(z[i]) || z[i] == '_'); i++ { + } + pToken.eType = T_PLACENAME + return i + } else if c == '$' && z[1] >= '1' && z[1] <= '9' && !isdigit(z[2]) { + pToken.eType = T_PARAMETER + pToken.eCode = int16(z[1] - '1') + return 2 + } else if c == '_' || c == '$' || c == '@' { + for i = 1; z[i] != 0 && (isalnum(z[i]) || z[i] == '_'); i++ { + } + pToken.eType = T_ID + return i + } else { + pToken.eType = T_ERROR + return 1 + } + } +} + +/* +** Return a pointer to the next non-whitespace token after pThis. +** This is used to help form error messages. + */ +func pik_next_semantic_token(pThis *PToken) PToken { + var x PToken + i := pThis.n + x.z = pThis.z + for { + x.z = pThis.z[i:] + sz := pik_token_length(&x, true) + if x.eType != T_WHITESPACE { + x.n = sz + return x + } + i += sz + } +} + +/* Parser arguments to a macro invocation +** +** (arg1, arg2, ...) +** +** Arguments are comma-separated, except that commas within string +** literals or with (...), {...}, or [...] do not count. The argument +** list begins and ends with parentheses. There can be at most 9 +** arguments. +** +** Return the number of bytes in the argument list. + */ +func (p *Pik) pik_parse_macro_args( + z []byte, /* Start of the argument list */ + n int, /* Available bytes */ + args []PToken, /* Fill in with the arguments */ + pOuter []PToken, /* Arguments of the next outer context, or NULL */ +) int { + nArg := 0 + var i, sz int + depth := 0 + var x PToken + if z[0] != '(' { + return 0 + } + args[0].z = z[1:] + iStart := 1 + for i = 1; i < n && z[i] != ')'; i += sz { + x.z = z[i:] + sz = pik_token_length(&x, false) + if sz != 1 { + continue + } + if z[i] == ',' && depth <= 0 { + args[nArg].n = i - iStart + if nArg == 8 { + x.z = z + x.n = 1 + p.pik_error(&x, "too many macro arguments - max 9") + return 0 + } + nArg++ + args[nArg].z = z[i+1:] + iStart = i + 1 + depth = 0 + } else if z[i] == '(' || z[i] == '{' || z[i] == '[' { + depth++ + } else if z[i] == ')' || z[i] == '}' || z[i] == ']' { + depth-- + } + } + if z[i] == ')' { + args[nArg].n = i - iStart + /* Remove leading and trailing whitespace from each argument. + ** If what remains is one of $1, $2, ... $9 then transfer the + ** corresponding argument from the outer context */ + for j := 0; j <= nArg; j++ { + t := &args[j] + for t.n > 0 && isspace(t.z[0]) { + t.n-- + t.z = t.z[1:] + } + for t.n > 0 && isspace(t.z[t.n-1]) { + t.n-- + } + if t.n == 2 && t.z[0] == '$' && t.z[1] >= '1' && t.z[1] <= '9' { + if pOuter != nil { + *t = pOuter[t.z[1]-'1'] + } else { + t.n = 0 + } + } + } + return i + 1 + } + x.z = z + x.n = 1 + p.pik_error(&x, "unterminated macro argument list") + return 0 +} + +/* +** Split up the content of a PToken into multiple tokens and +** send each to the parser. + */ +func (p *Pik) pik_tokenize(pIn *PToken, pParser *yyParser, aParam []PToken) { + sz := 0 + var token PToken + for i := 0; i < pIn.n && pIn.z[i] != 0 && p.nErr == 0; i += sz { + token.eCode = 0 + token.eEdge = 0 + token.z = pIn.z[i:] + sz = pik_token_length(&token, true) + if token.eType == T_WHITESPACE { + continue + /* no-op */ + } + if sz > 50000 { + token.n = 1 + p.pik_error(&token, "token is too long - max length 50000 bytes") + break + } + if token.eType == T_ERROR { + token.n = sz + p.pik_error(&token, "unrecognized token") + break + } + if sz+i > pIn.n { + token.n = pIn.n - i + p.pik_error(&token, "syntax error") + break + } + if token.eType == T_PARAMETER { + /* Substitute a parameter into the input stream */ + if aParam == nil || aParam[token.eCode].n == 0 { + continue + } + token.n = sz + if p.nCtx >= len(p.aCtx) { + p.pik_error(&token, "macros nested too deep") + } else { + p.aCtx[p.nCtx] = token + p.nCtx++ + p.pik_tokenize(&aParam[token.eCode], pParser, nil) + p.nCtx-- + } + continue + } + + if token.eType == T_ID { + token.n = sz + pMac := p.pik_find_macro(&token) + if pMac != nil { + args := make([]PToken, 9) + j := i + sz + if pMac.inUse { + p.pik_error(&pMac.macroName, "recursive macro definition") + break + } + token.n = sz + if p.nCtx >= len(p.aCtx) { + p.pik_error(&token, "macros nested too deep") + break + } + pMac.inUse = true + p.aCtx[p.nCtx] = token + p.nCtx++ + sz += p.pik_parse_macro_args(pIn.z[j:], pIn.n-j, args, aParam) + p.pik_tokenize(&pMac.macroBody, pParser, args) + p.nCtx-- + pMac.inUse = false + continue + } + } + if false { // #if 0 + n := sz + if isspace(token.z[0]) { + n = 0 + } + + fmt.Printf("******** Token %s (%d): \"%s\" **************\n", + yyTokenName[token.eType], token.eType, string(token.z[:n])) + } // #endif + token.n = sz + pParser.pik_parser(token.eType, token) + } +} + +/* +** Parse the PIKCHR script contained in zText[]. Return a rendering. Or +** if an error is encountered, return the error text. The error message +** is HTML formatted. So regardless of what happens, the return text +** is safe to be insertd into an HTML output stream. +** +** If pnWidth and pnHeight are not NULL, then this routine writes the +** width and height of the object into the integers that they +** point to. A value of -1 is written if an error is seen. +** +** If zClass is not NULL, then it is a class name to be included in +** the markup. +** +** The returned string is contained in memory obtained from malloc() +** and should be released by the caller. + */ +func Pikchr( + zText []byte, /* Input PIKCHR source text. zero-terminated */ + zClass string, /* Add class="%s" to markup */ + mFlags uint, /* Flags used to influence rendering behavior */ + svgWidth, svgHeight string, + svgFontScale PNum, + pnWidth *int, /* Write width of here, if not NULL */ + pnHeight *int, /* Write height here, if not NULL */ +) []byte { + s := Pik{} + var sParse yyParser + + s.sIn.n = len(zText) + s.sIn.z = append(zText, 0) + s.eDir = DIR_RIGHT + s.zClass = zClass + s.mFlags = mFlags + s.svgWidth = svgWidth + s.svgHeight = svgHeight + s.svgFontScale = svgFontScale + sParse.pik_parserInit(&s) + if false { // #if 0 + pik_parserTrace(os.Stdout, "parser: ") + } // #endif + s.pik_tokenize(&s.sIn, &sParse, nil) + if s.nErr == 0 { + var token PToken + if s.sIn.n > 0 { + token.z = zText[s.sIn.n-1:] + } else { + token.z = zText + } + token.n = 1 + sParse.pik_parser(0, token) + } + sParse.pik_parserFinalize() + if s.zOut.Len() == 0 && s.nErr == 0 { + s.pik_append("\n") + } + if pnWidth != nil { + if s.nErr != 0 { + *pnWidth = -1 + } else { + *pnWidth = s.wSVG + } + } + if pnHeight != nil { + if s.nErr != 0 { + *pnHeight = -1 + } else { + *pnHeight = s.hSVG + } + } + return s.zOut.Bytes() +} + +// #if defined(PIKCHR_FUZZ) +// #include +// int LLVMFuzzerTestOneInput(const uint8_t *aData, size_t nByte){ +// int w,h; +// char *zIn, *zOut; +// unsigned int mFlags = nByte & 3; +// zIn = malloc( nByte + 1 ); +// if( zIn==0 ) return 0; +// memcpy(zIn, aData, nByte); +// zIn[nByte] = 0; +// zOut = pikchr(zIn, "pikchr", mFlags, &w, &h); +// free(zIn); +// free(zOut); +// return 0; +// } +// #endif /* PIKCHR_FUZZ */ + +// Helpers added for port to Go +func isxdigit(b byte) bool { + return (b >= '0' && b <= '9') || (b >= 'a' && b <= 'f') || (b >= 'A' && b <= 'F') +} + +func isalnum(b byte) bool { + return (b >= '0' && b <= '9') || (b >= 'a' && b <= 'z') || (b >= 'A' && b <= 'Z') +} + +func isdigit(b byte) bool { + return (b >= '0' && b <= '9') +} + +func isspace(b byte) bool { + return b == ' ' || b == '\n' || b == '\t' || b == '\f' +} + +func isupper(b byte) bool { + return (b >= 'A' && b <= 'Z') +} + +func islower(b byte) bool { + return (b >= 'a' && b <= 'z') +} + +func bytencmp(a []byte, s string, n int) int { + return strings.Compare(string(a[:n]), s) +} + +func bytesEq(a, b []byte) bool { + if len(a) != len(b) { + return false + } + for i, bb := range a { + if b[i] != bb { + return false + } + } + return true +} + +//line 8230 "pikchr.go" ADDED parser/pikchr/internal/pikchr.y Index: parser/pikchr/internal/pikchr.y ================================================================== --- parser/pikchr/internal/pikchr.y +++ parser/pikchr/internal/pikchr.y @@ -0,0 +1,5641 @@ +%include { +//lint:file-ignore *,U1000 Ignore all unused code, it's generated + +/* +** Zero-Clause BSD license: +** +** Copyright (C) 2020-09-01 by D. Richard Hipp +** +** Permission to use, copy, modify, and/or distribute this software for +** any purpose with or without fee is hereby granted. +** +**************************************************************************** +** +** This software translates a PIC-inspired diagram language into SVG. +** +** PIKCHR (pronounced like "picture") is *mostly* backwards compatible +** with legacy PIC, though some features of legacy PIC are removed +** (for example, the "sh" command is removed for security) and +** many enhancements are added. +** +** PIKCHR is designed for use in an internet facing web environment. +** In particular, PIKCHR is designed to safely generate benign SVG from +** source text that provided by a hostile agent. +** +** This code was originally written by D. Richard Hipp using documentation +** from prior PIC implementations but without reference to prior code. +** All of the code in this project is original. +** +** This file implements a C-language subroutine that accepts a string +** of PIKCHR language text and generates a second string of SVG output that +** renders the drawing defined by the input. Space to hold the returned +** string is obtained from malloc() and should be freed by the caller. +** NULL might be returned if there is a memory allocation error. +** +** If there are errors in the PIKCHR input, the output will consist of an +** error message and the original PIKCHR input text (inside of
    ...
    ). +** +** 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 */ + wSVG int /* Width and height of the */ + hSVG int + fgcolor int /* foreground color value, or -1 for none */ + bgcolor int /* background color value, or -1 for none */ + + /* Paths for lines are constructed here first, then transferred into + ** the PObj object at the end: */ + nTPath int /* Number of entries on aTPath[] */ + mTPath int /* For last entry, 1: x set, 2: y set */ + aTPath [1000]PPoint /* Path under construction */ + + /* Error contexts */ + nCtx int /* Number of error contexts */ + aCtx [10]PToken /* Nested error contexts */ + + svgWidth, svgHeight string // Explicit width/height, if not given by scale. + svgFontScale PNum +} + +/* Include PIKCHR_PLAINTEXT_ERRORS among the bits of mFlags on the 3rd +** argument to pikchr() in order to cause error message text to come out +** as text/plain instead of as text/html + */ +const PIKCHR_PLAINTEXT_ERRORS = 0x0001 + +/* Include PIKCHR_DARK_MODE among the mFlag bits to invert colors. + */ +const PIKCHR_DARK_MODE = 0x0002 + +/* +** The behavior of an object class is defined by an instance of +** this structure. This is the "virtual method" table. + */ +type PClass struct { + zName string /* Name of class */ + isLine bool /* True if a line class */ + eJust int8 /* Use box-style text justification */ + + xInit func(*Pik, *PObj) /* Initializer */ + xNumProp func(*Pik, *PObj, *PToken) /* Value change notification */ + xCheck func(*Pik, *PObj) /* Checks to do after parsing */ + xChop func(*Pik, *PObj, *PPoint) PPoint /* Chopper */ + xOffset func(*Pik, *PObj, uint8) PPoint /* Offset from .c to edge point */ + xFit func(pik *Pik, pobj *PObj, w PNum, h PNum) /* Size to fit text */ + xRender func(*Pik, *PObj) /* Render */ +} + +func yytestcase(condition bool) {} + +} // end %include + +%name pik_parser +%token_prefix T_ +%token_type {PToken} +%extra_context {p *Pik} + +%fallback ID EDGEPT. + +// precedence rules. +%left OF. +%left PLUS MINUS. +%left STAR SLASH PERCENT. +%right UMINUS. + +%type statement_list {[]*PObj} +%destructor statement_list {p.pik_elist_free(&$$)} +%type statement {*PObj} +%destructor statement {p.pik_elem_free($$)} +%type unnamed_statement {*PObj} +%destructor unnamed_statement {p.pik_elem_free($$)} +%type basetype {*PObj} +%destructor basetype {p.pik_elem_free($$)} +%type expr {PNum} +%type numproperty {PToken} +%type edge {PToken} +%type direction {PToken} +%type dashproperty {PToken} +%type colorproperty {PToken} +%type locproperty {PToken} +%type position {PPoint} +%type place {PPoint} +%type object {*PObj} +%type objectname {*PObj} +%type nth {PToken} +%type textposition {int} +%type rvalue {PNum} +%type lvalue {PToken} +%type even {PToken} +%type relexpr {PRel} +%type optrelexpr {PRel} + +%syntax_error { + if TOKEN.z != nil && TOKEN.z[0] != 0 { + p.pik_error(&TOKEN, "syntax error") + }else{ + p.pik_error(nil, "syntax error") + } +} +%stack_overflow { + p.pik_error(nil, "parser stack overflow") +} + +document ::= statement_list(X). {p.pik_render(X)} + + +statement_list(A) ::= statement(X). { A = p.pik_elist_append(nil,X) } +statement_list(A) ::= statement_list(B) EOL statement(X). + { A = p.pik_elist_append(B,X) } + + +statement(A) ::= . { A = nil } +statement(A) ::= direction(D). { p.pik_set_direction(uint8(D.eCode)); A=nil } +statement(A) ::= lvalue(N) ASSIGN(OP) rvalue(X). {p.pik_set_var(&N,X,&OP); A=nil} +statement(A) ::= PLACENAME(N) COLON unnamed_statement(X). + { A = X; p.pik_elem_setname(X,&N) } +statement(A) ::= PLACENAME(N) COLON position(P). + { A = p.pik_elem_new(nil,nil,nil) + if A!=nil { A.ptAt = P; p.pik_elem_setname(A,&N) }} +statement(A) ::= unnamed_statement(X). {A = X} +statement(A) ::= print prlist. {p.pik_append("
    \n"); A=nil} + +// assert() statements are undocumented and are intended for testing and +// debugging use only. If the equality comparison of the assert() fails +// then an error message is generated. +statement(A) ::= ASSERT LP expr(X) EQ(OP) expr(Y) RP. {A=p.pik_assert(X,&OP,Y)} +statement(A) ::= ASSERT LP position(X) EQ(OP) position(Y) RP. + {A=p.pik_position_assert(&X,&OP,&Y)} +statement(A) ::= DEFINE ID(ID) CODEBLOCK(C). {A=nil; p.pik_add_macro(&ID,&C)} + +lvalue(A) ::= ID(A). +lvalue(A) ::= FILL(A). +lvalue(A) ::= COLOR(A). +lvalue(A) ::= THICKNESS(A). + +// PLACENAME might actually be a color name (ex: DarkBlue). But we +// cannot make it part of expr due to parsing ambiguities. The +// rvalue non-terminal means "general expression or a colorname" +rvalue(A) ::= expr(A). +rvalue(A) ::= PLACENAME(C). {A = p.pik_lookup_color(&C)} + +print ::= PRINT. +prlist ::= pritem. +prlist ::= prlist prsep pritem. +pritem ::= FILL(X). {p.pik_append_num("",p.pik_value(X.String(),nil))} +pritem ::= COLOR(X). {p.pik_append_num("",p.pik_value(X.String(),nil))} +pritem ::= THICKNESS(X). {p.pik_append_num("",p.pik_value(X.String(),nil))} +pritem ::= rvalue(X). {p.pik_append_num("",X)} +pritem ::= STRING(S). {p.pik_append_text(string(S.z[1:S.n-1]),0)} +prsep ::= COMMA. {p.pik_append(" ")} + +unnamed_statement(A) ::= basetype(X) attribute_list. + {A = X; p.pik_after_adding_attributes(A)} + +basetype(A) ::= CLASSNAME(N). {A = p.pik_elem_new(&N,nil,nil) } +basetype(A) ::= STRING(N) textposition(P). + {N.eCode = int16(P); A = p.pik_elem_new(nil,&N,nil) } +basetype(A) ::= LB savelist(L) statement_list(X) RB(E). + { p.list = L; A = p.pik_elem_new(nil,nil,X); if A!=nil {A.errTok = E} } + +%type savelist {[]*PObj} +// No destructor required as this same PList is also held by +// an "statement" non-terminal deeper on the stack. +savelist(A) ::= . {A = p.list; p.list = nil} + +direction(A) ::= UP(A). +direction(A) ::= DOWN(A). +direction(A) ::= LEFT(A). +direction(A) ::= RIGHT(A). + +relexpr(A) ::= expr(B). {A.rAbs = B; A.rRel = 0} +relexpr(A) ::= expr(B) PERCENT. {A.rAbs = 0; A.rRel = B/100} +optrelexpr(A) ::= relexpr(A). +optrelexpr(A) ::= . {A.rAbs = 0; A.rRel = 1.0} + +attribute_list ::= relexpr(X) alist. {p.pik_add_direction(nil,&X)} +attribute_list ::= alist. +alist ::=. +alist ::= alist attribute. +attribute ::= numproperty(P) relexpr(X). { p.pik_set_numprop(&P,&X) } +attribute ::= dashproperty(P) expr(X). { p.pik_set_dashed(&P,&X) } +attribute ::= dashproperty(P). { p.pik_set_dashed(&P,nil); } +attribute ::= colorproperty(P) rvalue(X). { p.pik_set_clrprop(&P,X) } +attribute ::= go direction(D) optrelexpr(X). { p.pik_add_direction(&D,&X)} +attribute ::= go direction(D) even position(P). {p.pik_evenwith(&D,&P)} +attribute ::= CLOSE(E). { p.pik_close_path(&E) } +attribute ::= CHOP. { p.cur.bChop = true } +attribute ::= FROM(T) position(X). { p.pik_set_from(p.cur,&T,&X) } +attribute ::= TO(T) position(X). { p.pik_add_to(p.cur,&T,&X) } +attribute ::= THEN(T). { p.pik_then(&T, p.cur) } +attribute ::= THEN(E) optrelexpr(D) HEADING(H) expr(A). + {p.pik_move_hdg(&D,&H,A,nil,&E)} +attribute ::= THEN(E) optrelexpr(D) EDGEPT(C). {p.pik_move_hdg(&D,nil,0,&C,&E)} +attribute ::= GO(E) optrelexpr(D) HEADING(H) expr(A). + {p.pik_move_hdg(&D,&H,A,nil,&E)} +attribute ::= GO(E) optrelexpr(D) EDGEPT(C). {p.pik_move_hdg(&D,nil,0,&C,&E)} +attribute ::= boolproperty. +attribute ::= AT(A) position(P). { p.pik_set_at(nil,&P,&A) } +attribute ::= WITH withclause. +attribute ::= SAME(E). {p.pik_same(nil,&E)} +attribute ::= SAME(E) AS object(X). {p.pik_same(X,&E)} +attribute ::= STRING(T) textposition(P). {p.pik_add_txt(&T,int16(P))} +attribute ::= FIT(E). {p.pik_size_to_fit(&E,3) } +attribute ::= BEHIND object(X). {p.pik_behind(X)} + +go ::= GO. +go ::= . + +even ::= UNTIL EVEN WITH. +even ::= EVEN WITH. + +withclause ::= DOT_E edge(E) AT(A) position(P).{ p.pik_set_at(&E,&P,&A) } +withclause ::= edge(E) AT(A) position(P). { p.pik_set_at(&E,&P,&A) } + +// Properties that require an argument +numproperty(A) ::= HEIGHT|WIDTH|RADIUS|DIAMETER|THICKNESS(P). {A = P} + +// Properties with optional arguments +dashproperty(A) ::= DOTTED(A). +dashproperty(A) ::= DASHED(A). + +// Color properties +colorproperty(A) ::= FILL(A). +colorproperty(A) ::= COLOR(A). + +// Properties with no argument +boolproperty ::= CW. {p.cur.cw = true} +boolproperty ::= CCW. {p.cur.cw = false} +boolproperty ::= LARROW. {p.cur.larrow=true; p.cur.rarrow=false } +boolproperty ::= RARROW. {p.cur.larrow=false; p.cur.rarrow=true } +boolproperty ::= LRARROW. {p.cur.larrow=true; p.cur.rarrow=true } +boolproperty ::= INVIS. {p.cur.sw = 0.0} +boolproperty ::= THICK. {p.cur.sw *= 1.5} +boolproperty ::= THIN. {p.cur.sw *= 0.67} +boolproperty ::= SOLID. {p.cur.sw = p.pik_value("thickness",nil) + p.cur.dotted = 0.0; p.cur.dashed = 0.0} + +textposition(A) ::= . {A = 0} +textposition(A) ::= textposition(B) + CENTER|LJUST|RJUST|ABOVE|BELOW|ITALIC|BOLD|ALIGNED|BIG|SMALL(F). + {A = pik_text_position(B,&F)} + + +position(A) ::= expr(X) COMMA expr(Y). {A.x=X; A.y=Y} +position(A) ::= place(A). +position(A) ::= place(B) PLUS expr(X) COMMA expr(Y). {A.x=B.x+X; A.y=B.y+Y} +position(A) ::= place(B) MINUS expr(X) COMMA expr(Y). {A.x=B.x-X; A.y=B.y-Y} +position(A) ::= place(B) PLUS LP expr(X) COMMA expr(Y) RP. + {A.x=B.x+X; A.y=B.y+Y} +position(A) ::= place(B) MINUS LP expr(X) COMMA expr(Y) RP. + {A.x=B.x-X; A.y=B.y-Y} +position(A) ::= LP position(X) COMMA position(Y) RP. {A.x=X.x; A.y=Y.y} +position(A) ::= LP position(X) RP. {A=X} +position(A) ::= expr(X) between position(P1) AND position(P2). + {A = pik_position_between(X,P1,P2)} +position(A) ::= expr(X) LT position(P1) COMMA position(P2) GT. + {A = pik_position_between(X,P1,P2)} +position(A) ::= expr(X) ABOVE position(B). {A=B; A.y += X} +position(A) ::= expr(X) BELOW position(B). {A=B; A.y -= X} +position(A) ::= expr(X) LEFT OF position(B). {A=B; A.x -= X} +position(A) ::= expr(X) RIGHT OF position(B). {A=B; A.x += X} +position(A) ::= expr(D) ON HEADING EDGEPT(E) OF position(P). + {A = pik_position_at_hdg(D,&E,P)} +position(A) ::= expr(D) HEADING EDGEPT(E) OF position(P). + {A = pik_position_at_hdg(D,&E,P)} +position(A) ::= expr(D) EDGEPT(E) OF position(P). + {A = pik_position_at_hdg(D,&E,P)} +position(A) ::= expr(D) ON HEADING expr(G) FROM position(P). + {A = pik_position_at_angle(D,G,P)} +position(A) ::= expr(D) HEADING expr(G) FROM position(P). + {A = pik_position_at_angle(D,G,P)} + +between ::= WAY BETWEEN. +between ::= BETWEEN. +between ::= OF THE WAY BETWEEN. + +// place2 is the same as place, but excludes the forms like +// "RIGHT of object" to avoid a parsing ambiguity with "place .x" +// and "place .y" expressions +%type place2 {PPoint} + +place(A) ::= place2(A). +place(A) ::= edge(X) OF object(O). {A = p.pik_place_of_elem(O,&X)} +place2(A) ::= object(O). {A = p.pik_place_of_elem(O,nil)} +place2(A) ::= object(O) DOT_E edge(X). {A = p.pik_place_of_elem(O,&X)} +place2(A) ::= NTH(N) VERTEX(E) OF object(X). {A = p.pik_nth_vertex(&N,&E,X)} + +edge(A) ::= CENTER(A). +edge(A) ::= EDGEPT(A). +edge(A) ::= TOP(A). +edge(A) ::= BOTTOM(A). +edge(A) ::= START(A). +edge(A) ::= END(A). +edge(A) ::= RIGHT(A). +edge(A) ::= LEFT(A). + +object(A) ::= objectname(A). +object(A) ::= nth(N). {A = p.pik_find_nth(nil,&N)} +object(A) ::= nth(N) OF|IN object(B). {A = p.pik_find_nth(B,&N)} + +objectname(A) ::= THIS. {A = p.cur} +objectname(A) ::= PLACENAME(N). {A = p.pik_find_byname(nil,&N)} +objectname(A) ::= objectname(B) DOT_U PLACENAME(N). + {A = p.pik_find_byname(B,&N)} + +nth(A) ::= NTH(N) CLASSNAME(ID). {A=ID; A.eCode = p.pik_nth_value(&N) } +nth(A) ::= NTH(N) LAST CLASSNAME(ID). {A=ID; A.eCode = -p.pik_nth_value(&N) } +nth(A) ::= LAST CLASSNAME(ID). {A=ID; A.eCode = -1} +nth(A) ::= LAST(ID). {A=ID; A.eCode = -1} +nth(A) ::= NTH(N) LB(ID) RB. {A=ID; A.eCode = p.pik_nth_value(&N)} +nth(A) ::= NTH(N) LAST LB(ID) RB. {A=ID; A.eCode = -p.pik_nth_value(&N)} +nth(A) ::= LAST LB(ID) RB. {A=ID; A.eCode = -1 } + +expr(A) ::= expr(X) PLUS expr(Y). {A=X+Y} +expr(A) ::= expr(X) MINUS expr(Y). {A=X-Y} +expr(A) ::= expr(X) STAR expr(Y). {A=X*Y} +expr(A) ::= expr(X) SLASH(E) expr(Y). { + if Y==0.0 { p.pik_error(&E, "division by zero"); A = 0.0 } else{ A = X/Y } +} +expr(A) ::= MINUS expr(X). [UMINUS] {A=-X} +expr(A) ::= PLUS expr(X). [UMINUS] {A=X} +expr(A) ::= LP expr(X) RP. {A=X} +expr(A) ::= LP FILL|COLOR|THICKNESS(X) RP. {A=p.pik_get_var(&X)} +expr(A) ::= NUMBER(N). {A=pik_atof(&N)} +expr(A) ::= ID(N). {A=p.pik_get_var(&N)} +expr(A) ::= FUNC1(F) LP expr(X) RP. {A = p.pik_func(&F,X,0.0)} +expr(A) ::= FUNC2(F) LP expr(X) COMMA expr(Y) RP. {A = p.pik_func(&F,X,Y)} +expr(A) ::= DIST LP position(X) COMMA position(Y) RP. {A = pik_dist(&X,&Y)} +expr(A) ::= place2(B) DOT_XY X. {A = B.x} +expr(A) ::= place2(B) DOT_XY Y. {A = B.y} +expr(A) ::= object(B) DOT_L numproperty(P). {A=pik_property_of(B,&P)} +expr(A) ::= object(B) DOT_L dashproperty(P). {A=pik_property_of(B,&P)} +expr(A) ::= object(B) DOT_L colorproperty(P). {A=pik_property_of(B,&P)} + + +%code { + +/* Chart of the 148 official CSS color names with their +** corresponding RGB values thru Color Module Level 4: +** https://developer.mozilla.org/en-US/docs/Web/CSS/color_value +** +** Two new names "None" and "Off" are added with a value +** of -1. + */ +var aColor = []struct { + zName string /* Name of the color */ + val int /* RGB value */ +}{ + {"AliceBlue", 0xf0f8ff}, + {"AntiqueWhite", 0xfaebd7}, + {"Aqua", 0x00ffff}, + {"Aquamarine", 0x7fffd4}, + {"Azure", 0xf0ffff}, + {"Beige", 0xf5f5dc}, + {"Bisque", 0xffe4c4}, + {"Black", 0x000000}, + {"BlanchedAlmond", 0xffebcd}, + {"Blue", 0x0000ff}, + {"BlueViolet", 0x8a2be2}, + {"Brown", 0xa52a2a}, + {"BurlyWood", 0xdeb887}, + {"CadetBlue", 0x5f9ea0}, + {"Chartreuse", 0x7fff00}, + {"Chocolate", 0xd2691e}, + {"Coral", 0xff7f50}, + {"CornflowerBlue", 0x6495ed}, + {"Cornsilk", 0xfff8dc}, + {"Crimson", 0xdc143c}, + {"Cyan", 0x00ffff}, + {"DarkBlue", 0x00008b}, + {"DarkCyan", 0x008b8b}, + {"DarkGoldenrod", 0xb8860b}, + {"DarkGray", 0xa9a9a9}, + {"DarkGreen", 0x006400}, + {"DarkGrey", 0xa9a9a9}, + {"DarkKhaki", 0xbdb76b}, + {"DarkMagenta", 0x8b008b}, + {"DarkOliveGreen", 0x556b2f}, + {"DarkOrange", 0xff8c00}, + {"DarkOrchid", 0x9932cc}, + {"DarkRed", 0x8b0000}, + {"DarkSalmon", 0xe9967a}, + {"DarkSeaGreen", 0x8fbc8f}, + {"DarkSlateBlue", 0x483d8b}, + {"DarkSlateGray", 0x2f4f4f}, + {"DarkSlateGrey", 0x2f4f4f}, + {"DarkTurquoise", 0x00ced1}, + {"DarkViolet", 0x9400d3}, + {"DeepPink", 0xff1493}, + {"DeepSkyBlue", 0x00bfff}, + {"DimGray", 0x696969}, + {"DimGrey", 0x696969}, + {"DodgerBlue", 0x1e90ff}, + {"Firebrick", 0xb22222}, + {"FloralWhite", 0xfffaf0}, + {"ForestGreen", 0x228b22}, + {"Fuchsia", 0xff00ff}, + {"Gainsboro", 0xdcdcdc}, + {"GhostWhite", 0xf8f8ff}, + {"Gold", 0xffd700}, + {"Goldenrod", 0xdaa520}, + {"Gray", 0x808080}, + {"Green", 0x008000}, + {"GreenYellow", 0xadff2f}, + {"Grey", 0x808080}, + {"Honeydew", 0xf0fff0}, + {"HotPink", 0xff69b4}, + {"IndianRed", 0xcd5c5c}, + {"Indigo", 0x4b0082}, + {"Ivory", 0xfffff0}, + {"Khaki", 0xf0e68c}, + {"Lavender", 0xe6e6fa}, + {"LavenderBlush", 0xfff0f5}, + {"LawnGreen", 0x7cfc00}, + {"LemonChiffon", 0xfffacd}, + {"LightBlue", 0xadd8e6}, + {"LightCoral", 0xf08080}, + {"LightCyan", 0xe0ffff}, + {"LightGoldenrodYellow", 0xfafad2}, + {"LightGray", 0xd3d3d3}, + {"LightGreen", 0x90ee90}, + {"LightGrey", 0xd3d3d3}, + {"LightPink", 0xffb6c1}, + {"LightSalmon", 0xffa07a}, + {"LightSeaGreen", 0x20b2aa}, + {"LightSkyBlue", 0x87cefa}, + {"LightSlateGray", 0x778899}, + {"LightSlateGrey", 0x778899}, + {"LightSteelBlue", 0xb0c4de}, + {"LightYellow", 0xffffe0}, + {"Lime", 0x00ff00}, + {"LimeGreen", 0x32cd32}, + {"Linen", 0xfaf0e6}, + {"Magenta", 0xff00ff}, + {"Maroon", 0x800000}, + {"MediumAquamarine", 0x66cdaa}, + {"MediumBlue", 0x0000cd}, + {"MediumOrchid", 0xba55d3}, + {"MediumPurple", 0x9370db}, + {"MediumSeaGreen", 0x3cb371}, + {"MediumSlateBlue", 0x7b68ee}, + {"MediumSpringGreen", 0x00fa9a}, + {"MediumTurquoise", 0x48d1cc}, + {"MediumVioletRed", 0xc71585}, + {"MidnightBlue", 0x191970}, + {"MintCream", 0xf5fffa}, + {"MistyRose", 0xffe4e1}, + {"Moccasin", 0xffe4b5}, + {"NavajoWhite", 0xffdead}, + {"Navy", 0x000080}, + {"None", -1}, /* Non-standard addition */ + {"Off", -1}, /* Non-standard addition */ + {"OldLace", 0xfdf5e6}, + {"Olive", 0x808000}, + {"OliveDrab", 0x6b8e23}, + {"Orange", 0xffa500}, + {"OrangeRed", 0xff4500}, + {"Orchid", 0xda70d6}, + {"PaleGoldenrod", 0xeee8aa}, + {"PaleGreen", 0x98fb98}, + {"PaleTurquoise", 0xafeeee}, + {"PaleVioletRed", 0xdb7093}, + {"PapayaWhip", 0xffefd5}, + {"PeachPuff", 0xffdab9}, + {"Peru", 0xcd853f}, + {"Pink", 0xffc0cb}, + {"Plum", 0xdda0dd}, + {"PowderBlue", 0xb0e0e6}, + {"Purple", 0x800080}, + {"RebeccaPurple", 0x663399}, + {"Red", 0xff0000}, + {"RosyBrown", 0xbc8f8f}, + {"RoyalBlue", 0x4169e1}, + {"SaddleBrown", 0x8b4513}, + {"Salmon", 0xfa8072}, + {"SandyBrown", 0xf4a460}, + {"SeaGreen", 0x2e8b57}, + {"Seashell", 0xfff5ee}, + {"Sienna", 0xa0522d}, + {"Silver", 0xc0c0c0}, + {"SkyBlue", 0x87ceeb}, + {"SlateBlue", 0x6a5acd}, + {"SlateGray", 0x708090}, + {"SlateGrey", 0x708090}, + {"Snow", 0xfffafa}, + {"SpringGreen", 0x00ff7f}, + {"SteelBlue", 0x4682b4}, + {"Tan", 0xd2b48c}, + {"Teal", 0x008080}, + {"Thistle", 0xd8bfd8}, + {"Tomato", 0xff6347}, + {"Turquoise", 0x40e0d0}, + {"Violet", 0xee82ee}, + {"Wheat", 0xf5deb3}, + {"White", 0xffffff}, + {"WhiteSmoke", 0xf5f5f5}, + {"Yellow", 0xffff00}, + {"YellowGreen", 0x9acd32}, +} + +/* Built-in variable names. +** +** This array is constant. When a script changes the value of one of +** these built-ins, a new PVar record is added at the head of +** the Pik.pVar list, which is searched first. Thus the new PVar entry +** will override this default value. +** +** Units are in inches, except for "color" and "fill" which are +** interpreted as 24-bit RGB values. +** +** Binary search used. Must be kept in sorted order. + */ + +var aBuiltin = []struct { + zName string + val PNum +}{ + {"arcrad", 0.25}, + {"arrowhead", 2.0}, + {"arrowht", 0.08}, + {"arrowwid", 0.06}, + {"boxht", 0.5}, + {"boxrad", 0.0}, + {"boxwid", 0.75}, + {"charht", 0.14}, + {"charwid", 0.08}, + {"circlerad", 0.25}, + {"color", 0.0}, + {"cylht", 0.5}, + {"cylrad", 0.075}, + {"cylwid", 0.75}, + {"dashwid", 0.05}, + {"dotrad", 0.015}, + {"ellipseht", 0.5}, + {"ellipsewid", 0.75}, + {"fileht", 0.75}, + {"filerad", 0.15}, + {"filewid", 0.5}, + {"fill", -1.0}, + {"lineht", 0.5}, + {"linewid", 0.5}, + {"movewid", 0.5}, + {"ovalht", 0.5}, + {"ovalwid", 1.0}, + {"scale", 1.0}, + {"textht", 0.5}, + {"textwid", 0.75}, + {"thickness", 0.015}, +} + +/* Methods for the "arc" class */ +func arcInit(p *Pik, pObj *PObj) { + pObj.w = p.pik_value("arcrad", nil) + pObj.h = pObj.w +} + +/* Hack: Arcs are here rendered as quadratic Bezier curves rather +** than true arcs. Multiple reasons: (1) the legacy-PIC parameters +** that control arcs are obscure and I could not figure out what they +** mean based on available documentation. (2) Arcs are rarely used, +** and so do not seem that important. + */ +func arcControlPoint(cw bool, f PPoint, t PPoint, rScale PNum) PPoint { + var m PPoint + var dx, dy PNum + m.x = 0.5 * (f.x + t.x) + m.y = 0.5 * (f.y + t.y) + dx = t.x - f.x + dy = t.y - f.y + if cw { + m.x -= 0.5 * rScale * dy + m.y += 0.5 * rScale * dx + } else { + m.x += 0.5 * rScale * dy + m.y -= 0.5 * rScale * dx + } + return m +} +func arcCheck(p *Pik, pObj *PObj) { + if p.nTPath > 2 { + p.pik_error(&pObj.errTok, "arc geometry error") + return + } + m := arcControlPoint(pObj.cw, p.aTPath[0], p.aTPath[1], 0.5) + pik_bbox_add_xy(&pObj.bbox, m.x, m.y) +} +func arcRender(p *Pik, pObj *PObj) { + if pObj.nPath < 2 { + return + } + if pObj.sw <= 0.0 { + return + } + f := pObj.aPath[0] + t := pObj.aPath[1] + m := arcControlPoint(pObj.cw, f, t, 1.0) + if pObj.larrow { + p.pik_draw_arrowhead(&m, &f, pObj) + } + if pObj.rarrow { + p.pik_draw_arrowhead(&m, &t, pObj) + } + p.pik_append_xy("\n") + + p.pik_append_txt(pObj, nil) +} + +/* Methods for the "arrow" class */ +func arrowInit(p *Pik, pObj *PObj) { + pObj.w = p.pik_value("linewid", nil) + pObj.h = p.pik_value("lineht", nil) + pObj.rad = p.pik_value("linerad", nil) + pObj.rarrow = true +} + +/* Methods for the "box" class */ +func boxInit(p *Pik, pObj *PObj) { + pObj.w = p.pik_value("boxwid", nil) + pObj.h = p.pik_value("boxht", nil) + pObj.rad = p.pik_value("boxrad", nil) +} + +/* Return offset from the center of the box to the compass point +** given by parameter cp */ +func boxOffset(p *Pik, pObj *PObj, cp uint8) PPoint { + pt := PPoint{} + var w2 PNum = 0.5 * pObj.w + var h2 PNum = 0.5 * pObj.h + var rad PNum = pObj.rad + var rx PNum + if rad <= 0.0 { + rx = 0.0 + } else { + if rad > w2 { + rad = w2 + } + if rad > h2 { + rad = h2 + } + rx = 0.29289321881345252392 * rad + } + switch cp { + case CP_C: + case CP_N: + pt.x = 0.0 + pt.y = h2 + case CP_NE: + pt.x = w2 - rx + pt.y = h2 - rx + case CP_E: + pt.x = w2 + pt.y = 0.0 + case CP_SE: + pt.x = w2 - rx + pt.y = rx - h2 + case CP_S: + pt.x = 0.0 + pt.y = -h2 + case CP_SW: + pt.x = rx - w2 + pt.y = rx - h2 + case CP_W: + pt.x = -w2 + pt.y = 0.0 + case CP_NW: + pt.x = rx - w2 + pt.y = h2 - rx + default: + assert(false, "false") + } + return pt +} +func boxChop(p *Pik, pObj *PObj, pPt *PPoint) PPoint { + var dx, dy PNum + cp := CP_C + chop := pObj.ptAt + if pObj.w <= 0.0 { + return chop + } + if pObj.h <= 0.0 { + return chop + } + dx = (pPt.x - pObj.ptAt.x) * pObj.h / pObj.w + dy = (pPt.y - pObj.ptAt.y) + if dx > 0.0 { + if dy >= 2.414*dx { + cp = CP_N + } else if dy >= 0.414*dx { + cp = CP_NE + } else if dy >= -0.414*dx { + cp = CP_E + } else if dy > -2.414*dx { + cp = CP_SE + } else { + cp = CP_S + } + } else { + if dy >= -2.414*dx { + cp = CP_N + } else if dy >= -0.414*dx { + cp = CP_NW + } else if dy >= 0.414*dx { + cp = CP_W + } else if dy > 2.414*dx { + cp = CP_SW + } else { + cp = CP_S + } + } + chop = pObj.typ.xOffset(p, pObj, cp) + chop.x += pObj.ptAt.x + chop.y += pObj.ptAt.y + return chop +} +func boxFit(p *Pik, pObj *PObj, w PNum, h PNum) { + if w > 0 { + pObj.w = w + } + if h > 0 { + pObj.h = h + } +} +func boxRender(p *Pik, pObj *PObj) { + var w2 PNum = 0.5 * pObj.w + var h2 PNum = 0.5 * pObj.h + rad := pObj.rad + pt := pObj.ptAt + if pObj.sw > 0.0 { + if rad <= 0.0 { + p.pik_append_xy(" w2 { + rad = w2 + } + if rad > h2 { + rad = h2 + } + var x0 PNum = pt.x - w2 + var x1 PNum = x0 + rad + var x3 PNum = pt.x + w2 + var x2 PNum = x3 - rad + var y0 PNum = pt.y - h2 + var y1 PNum = y0 + rad + var y3 PNum = pt.y + h2 + var y2 PNum = y3 - rad + p.pik_append_xy(" x1 { + p.pik_append_xy("L", x2, y0) + } + p.pik_append_arc(rad, rad, x3, y1) + if y2 > y1 { + p.pik_append_xy("L", x3, y2) + } + p.pik_append_arc(rad, rad, x2, y3) + if x2 > x1 { + p.pik_append_xy("L", x1, y3) + } + p.pik_append_arc(rad, rad, x0, y2) + if y2 > y1 { + p.pik_append_xy("L", x0, y1) + } + p.pik_append_arc(rad, rad, x1, y0) + p.pik_append("Z\" ") + } + p.pik_append_style(pObj, 3) + p.pik_append("\" />\n") + } + p.pik_append_txt(pObj, nil) +} + +/* Methods for the "circle" class */ +func circleInit(p *Pik, pObj *PObj) { + pObj.w = p.pik_value("circlerad", nil) * 2 + pObj.h = pObj.w + pObj.rad = 0.5 * pObj.w +} +func circleNumProp(p *Pik, pObj *PObj, pId *PToken) { + /* For a circle, the width must equal the height and both must + ** be twice the radius. Enforce those constraints. */ + switch pId.eType { + case T_RADIUS: + pObj.w = 2.0 * pObj.rad + pObj.h = 2.0 * pObj.rad + case T_WIDTH: + pObj.h = pObj.w + pObj.rad = 0.5 * pObj.w + case T_HEIGHT: + pObj.w = pObj.h + pObj.rad = 0.5 * pObj.w + } +} +func circleChop(p *Pik, pObj *PObj, pPt *PPoint) PPoint { + var chop PPoint + var dx PNum = pPt.x - pObj.ptAt.x + var dy PNum = pPt.y - pObj.ptAt.y + var dist PNum = math.Hypot(dx, dy) + if dist < pObj.rad || dist <= 0 { + return pObj.ptAt + } + chop.x = pObj.ptAt.x + dx*pObj.rad/dist + chop.y = pObj.ptAt.y + dy*pObj.rad/dist + return chop +} +func circleFit(p *Pik, pObj *PObj, w PNum, h PNum) { + var mx PNum = 0.0 + if w > 0 { + mx = w + } + if h > mx { + mx = h + } + if w*h > 0 && (w*w+h*h) > mx*mx { + mx = math.Hypot(w, h) + } + if mx > 0.0 { + pObj.rad = 0.5 * mx + pObj.w = mx + pObj.h = mx + } +} + +func circleRender(p *Pik, pObj *PObj) { + r := pObj.rad + pt := pObj.ptAt + if pObj.sw > 0.0 { + p.pik_append_x("\n") + } + p.pik_append_txt(pObj, nil) +} + +/* Methods for the "cylinder" class */ +func cylinderInit(p *Pik, pObj *PObj) { + pObj.w = p.pik_value("cylwid", nil) + pObj.h = p.pik_value("cylht", nil) + pObj.rad = p.pik_value("cylrad", nil) /* Minor radius of ellipses */ +} +func cylinderFit(p *Pik, pObj *PObj, w PNum, h PNum) { + if w > 0 { + pObj.w = w + } + if h > 0 { + pObj.h = h + 0.25*pObj.rad + pObj.sw + } +} +func cylinderRender(p *Pik, pObj *PObj) { + var w2 PNum = 0.5 * pObj.w + var h2 PNum = 0.5 * pObj.h + rad := pObj.rad + pt := pObj.ptAt + if pObj.sw > 0.0 { + if rad > h2 { + rad = h2 + } else if rad < 0 { + rad = 0 + } + p.pik_append_xy("\n") + } + p.pik_append_txt(pObj, nil) +} +func cylinderOffset(p *Pik, pObj *PObj, cp uint8) PPoint { + pt := PPoint{} + var w2 PNum = pObj.w * 0.5 + var h1 PNum = pObj.h * 0.5 + var h2 PNum = h1 - pObj.rad + switch cp { + case CP_C: + case CP_N: + pt.x = 0.0 + pt.y = h1 + case CP_NE: + pt.x = w2 + pt.y = h2 + case CP_E: + pt.x = w2 + pt.y = 0.0 + case CP_SE: + pt.x = w2 + pt.y = -h2 + case CP_S: + pt.x = 0.0 + pt.y = -h1 + case CP_SW: + pt.x = -w2 + pt.y = -h2 + case CP_W: + pt.x = -w2 + pt.y = 0.0 + case CP_NW: + pt.x = -w2 + pt.y = h2 + default: + assert(false, "false") + } + return pt +} + +/* Methods for the "dot" class */ +func dotInit(p *Pik, pObj *PObj) { + pObj.rad = p.pik_value("dotrad", nil) + pObj.h = pObj.rad * 6 + pObj.w = pObj.rad * 6 + pObj.fill = pObj.color +} +func dotNumProp(p *Pik, pObj *PObj, pId *PToken) { + switch pId.eType { + case T_COLOR: + pObj.fill = pObj.color + case T_FILL: + pObj.color = pObj.fill + } +} +func dotCheck(p *Pik, pObj *PObj) { + pObj.w = 0 + pObj.h = 0 + pik_bbox_addellipse(&pObj.bbox, pObj.ptAt.x, pObj.ptAt.y, + pObj.rad, pObj.rad) +} +func dotOffset(p *Pik, pObj *PObj, cp uint8) PPoint { + return PPoint{} +} +func dotRender(p *Pik, pObj *PObj) { + r := pObj.rad + pt := pObj.ptAt + if pObj.sw > 0.0 { + p.pik_append_x("\n") + } + p.pik_append_txt(pObj, nil) +} + +/* Methods for the "ellipse" class */ +func ellipseInit(p *Pik, pObj *PObj) { + pObj.w = p.pik_value("ellipsewid", nil) + pObj.h = p.pik_value("ellipseht", nil) +} +func ellipseChop(p *Pik, pObj *PObj, pPt *PPoint) PPoint { + var chop PPoint + var s, dq, dist PNum + var dx PNum = pPt.x - pObj.ptAt.x + var dy PNum = pPt.y - pObj.ptAt.y + if pObj.w <= 0.0 { + return pObj.ptAt + } + if pObj.h <= 0.0 { + return pObj.ptAt + } + s = pObj.h / pObj.w + dq = dx * s + dist = math.Hypot(dq, dy) + if dist < pObj.h { + return pObj.ptAt + } + chop.x = pObj.ptAt.x + 0.5*dq*pObj.h/(dist*s) + chop.y = pObj.ptAt.y + 0.5*dy*pObj.h/dist + return chop +} +func ellipseOffset(p *Pik, pObj *PObj, cp uint8) PPoint { + pt := PPoint{} + var w PNum = pObj.w * 0.5 + var w2 PNum = w * 0.70710678118654747608 + var h PNum = pObj.h * 0.5 + var h2 PNum = h * 0.70710678118654747608 + switch cp { + case CP_C: + case CP_N: + pt.x = 0.0 + pt.y = h + case CP_NE: + pt.x = w2 + pt.y = h2 + case CP_E: + pt.x = w + pt.y = 0.0 + case CP_SE: + pt.x = w2 + pt.y = -h2 + case CP_S: + pt.x = 0.0 + pt.y = -h + case CP_SW: + pt.x = -w2 + pt.y = -h2 + case CP_W: + pt.x = -w + pt.y = 0.0 + case CP_NW: + pt.x = -w2 + pt.y = h2 + default: + assert(false, "false") + } + return pt +} +func ellipseRender(p *Pik, pObj *PObj) { + w := pObj.w + h := pObj.h + pt := pObj.ptAt + if pObj.sw > 0.0 { + p.pik_append_x("\n") + } + p.pik_append_txt(pObj, nil) +} + +/* Methods for the "file" object */ +func fileInit(p *Pik, pObj *PObj) { + pObj.w = p.pik_value("filewid", nil) + pObj.h = p.pik_value("fileht", nil) + pObj.rad = p.pik_value("filerad", nil) +} + +/* Return offset from the center of the file to the compass point +** given by parameter cp */ +func fileOffset(p *Pik, pObj *PObj, cp uint8) PPoint { + pt := PPoint{} + var w2 PNum = 0.5 * pObj.w + var h2 PNum = 0.5 * pObj.h + var rx PNum = pObj.rad + mn := h2 + if w2 < h2 { + mn = w2 + } + if rx > mn { + rx = mn + } + if rx < mn*0.25 { + rx = mn * 0.25 + } + pt.x = 0.0 + pt.y = 0.0 + rx *= 0.5 + switch cp { + case CP_C: + case CP_N: + pt.x = 0.0 + pt.y = h2 + case CP_NE: + pt.x = w2 - rx + pt.y = h2 - rx + case CP_E: + pt.x = w2 + pt.y = 0.0 + case CP_SE: + pt.x = w2 + pt.y = -h2 + case CP_S: + pt.x = 0.0 + pt.y = -h2 + case CP_SW: + pt.x = -w2 + pt.y = -h2 + case CP_W: + pt.x = -w2 + pt.y = 0.0 + case CP_NW: + pt.x = -w2 + pt.y = h2 + default: + assert(false, "false") + } + return pt +} +func fileFit(p *Pik, pObj *PObj, w PNum, h PNum) { + if w > 0 { + pObj.w = w + } + if h > 0 { + pObj.h = h + 2*pObj.rad + } +} +func fileRender(p *Pik, pObj *PObj) { + var w2 PNum = 0.5 * pObj.w + var h2 PNum = 0.5 * pObj.h + rad := pObj.rad + pt := pObj.ptAt + mn := h2 + if w2 < h2 { + mn = w2 + } + if rad > mn { + rad = mn + } + if rad < mn*0.25 { + rad = mn * 0.25 + } + if pObj.sw > 0.0 { + p.pik_append_xy("\n") + p.pik_append_xy("\n") + } + p.pik_append_txt(pObj, nil) +} + +/* Methods for the "line" class */ +func lineInit(p *Pik, pObj *PObj) { + pObj.w = p.pik_value("linewid", nil) + pObj.h = p.pik_value("lineht", nil) + pObj.rad = p.pik_value("linerad", nil) +} +func lineOffset(p *Pik, pObj *PObj, cp uint8) PPoint { + if false { // #if 0 + /* In legacy PIC, the .center of an unclosed line is half way between + ** its .start and .end. */ + if cp == CP_C && !pObj.bClose { + var out PPoint + out.x = 0.5*(pObj.ptEnter.x+pObj.ptExit.x) - pObj.ptAt.x + out.y = 0.5*(pObj.ptEnter.x+pObj.ptExit.y) - pObj.ptAt.y + return out + } + } // #endif + return boxOffset(p, pObj, cp) +} +func lineRender(p *Pik, pObj *PObj) { + if pObj.sw > 0.0 { + z := "\n") + } + p.pik_append_txt(pObj, nil) +} + +/* Methods for the "move" class */ +func moveInit(p *Pik, pObj *PObj) { + pObj.w = p.pik_value("movewid", nil) + pObj.h = pObj.w + pObj.fill = -1.0 + pObj.color = -1.0 + pObj.sw = -1.0 +} +func moveRender(p *Pik, pObj *PObj) { + /* No-op */ +} + +/* Methods for the "oval" class */ +func ovalInit(p *Pik, pObj *PObj) { + pObj.h = p.pik_value("ovalht", nil) + pObj.w = p.pik_value("ovalwid", nil) + if pObj.h < pObj.w { + pObj.rad = 0.5 * pObj.h + } else { + pObj.rad = 0.5 * pObj.w + } +} +func ovalNumProp(p *Pik, pObj *PObj, pId *PToken) { + /* Always adjust the radius to be half of the smaller of + ** the width and height. */ + if pObj.h < pObj.w { + pObj.rad = 0.5 * pObj.h + } else { + pObj.rad = 0.5 * pObj.w + } +} +func ovalFit(p *Pik, pObj *PObj, w PNum, h PNum) { + if w > 0 { + pObj.w = w + } + if h > 0 { + pObj.h = h + } + if pObj.w < pObj.h { + pObj.w = pObj.h + } + if pObj.h < pObj.w { + pObj.rad = 0.5 * pObj.h + } else { + pObj.rad = 0.5 * pObj.w + } +} + +/* Methods for the "spline" class */ +func splineInit(p *Pik, pObj *PObj) { + pObj.w = p.pik_value("linewid", nil) + pObj.h = p.pik_value("lineht", nil) + pObj.rad = 1000 +} + +/* Return a point along the path from "f" to "t" that is r units +** prior to reaching "t", except if the path is less than 2*r total, +** return the midpoint. + */ +func radiusMidpoint(f PPoint, t PPoint, r PNum, pbMid *bool) PPoint { + var dx PNum = t.x - f.x + var dy PNum = t.y - f.y + var dist PNum = math.Hypot(dx, dy) + if dist <= 0.0 { + return t + } + dx /= dist + dy /= dist + if r > 0.5*dist { + r = 0.5 * dist + *pbMid = true + } else { + *pbMid = false + } + return PPoint{ + x: t.x - r*dx, + y: t.y - r*dy, + } +} +func (p *Pik) radiusPath(pObj *PObj, r PNum) { + n := pObj.nPath + a := pObj.aPath + an := a[n-1] + isMid := false + iLast := n - 1 + if pObj.bClose { + iLast = n + } + + p.pik_append_xy("\n") +} +func splineRender(p *Pik, pObj *PObj) { + if pObj.sw > 0.0 { + n := pObj.nPath + r := pObj.rad + if n < 3 || r <= 0.0 { + lineRender(p, pObj) + return + } + if pObj.larrow { + p.pik_draw_arrowhead(&pObj.aPath[1], &pObj.aPath[0], pObj) + } + if pObj.rarrow { + p.pik_draw_arrowhead(&pObj.aPath[n-2], &pObj.aPath[n-1], pObj) + } + p.radiusPath(pObj, pObj.rad) + } + p.pik_append_txt(pObj, nil) +} + +/* Methods for the "text" class */ +func textInit(p *Pik, pObj *PObj) { + p.pik_value("textwid", nil) + p.pik_value("textht", nil) + pObj.sw = 0.0 +} +func textOffset(p *Pik, pObj *PObj, cp uint8) PPoint { + /* Automatically slim-down the width and height of text + ** statements so that the bounding box tightly encloses the text, + ** then get boxOffset() to do the offset computation. + */ + p.pik_size_to_fit(&pObj.errTok, 3) + return boxOffset(p, pObj, cp) +} + +/* Methods for the "sublist" class */ +func sublistInit(p *Pik, pObj *PObj) { + pList := pObj.pSublist + pik_bbox_init(&pObj.bbox) + for i := 0; i < len(pList); i++ { + pik_bbox_addbox(&pObj.bbox, &pList[i].bbox) + } + pObj.w = pObj.bbox.ne.x - pObj.bbox.sw.x + pObj.h = pObj.bbox.ne.y - pObj.bbox.sw.y + pObj.ptAt.x = 0.5 * (pObj.bbox.ne.x + pObj.bbox.sw.x) + pObj.ptAt.y = 0.5 * (pObj.bbox.ne.y + pObj.bbox.sw.y) + pObj.mCalc |= A_WIDTH | A_HEIGHT | A_RADIUS +} + +/* +** The following array holds all the different kinds of objects. +** The special [] object is separate. + */ +var aClass = []PClass{ + { + zName: "arc", + isLine: true, + eJust: 0, + xInit: arcInit, + xNumProp: nil, + xCheck: arcCheck, + xChop: nil, + xOffset: boxOffset, + xFit: nil, + xRender: arcRender, + }, + { + zName: "arrow", + isLine: true, + eJust: 0, + xInit: arrowInit, + xNumProp: nil, + xCheck: nil, + xChop: nil, + xOffset: lineOffset, + xFit: nil, + xRender: splineRender, + }, + { + zName: "box", + isLine: false, + eJust: 1, + xInit: boxInit, + xNumProp: nil, + xCheck: nil, + xChop: boxChop, + xOffset: boxOffset, + xFit: boxFit, + xRender: boxRender, + }, + { + zName: "circle", + isLine: false, + eJust: 0, + xInit: circleInit, + xNumProp: circleNumProp, + xCheck: nil, + xChop: circleChop, + xOffset: ellipseOffset, + xFit: circleFit, + xRender: circleRender, + }, + { + zName: "cylinder", + isLine: false, + eJust: 1, + xInit: cylinderInit, + xNumProp: nil, + xCheck: nil, + xChop: boxChop, + xOffset: cylinderOffset, + xFit: cylinderFit, + xRender: cylinderRender, + }, + { + zName: "dot", + isLine: false, + eJust: 0, + xInit: dotInit, + xNumProp: dotNumProp, + xCheck: dotCheck, + xChop: circleChop, + xOffset: dotOffset, + xFit: nil, + xRender: dotRender, + }, + { + zName: "ellipse", + isLine: false, + eJust: 0, + xInit: ellipseInit, + xNumProp: nil, + xCheck: nil, + xChop: ellipseChop, + xOffset: ellipseOffset, + xFit: boxFit, + xRender: ellipseRender, + }, + { + zName: "file", + isLine: false, + eJust: 1, + xInit: fileInit, + xNumProp: nil, + xCheck: nil, + xChop: boxChop, + xOffset: fileOffset, + xFit: fileFit, + xRender: fileRender, + }, + { + zName: "line", + isLine: true, + eJust: 0, + xInit: lineInit, + xNumProp: nil, + xCheck: nil, + xChop: nil, + xOffset: lineOffset, + xFit: nil, + xRender: splineRender, + }, + { + zName: "move", + isLine: true, + eJust: 0, + xInit: moveInit, + xNumProp: nil, + xCheck: nil, + xChop: nil, + xOffset: boxOffset, + xFit: nil, + xRender: moveRender, + }, + { + zName: "oval", + isLine: false, + eJust: 1, + xInit: ovalInit, + xNumProp: ovalNumProp, + xCheck: nil, + xChop: boxChop, + xOffset: boxOffset, + xFit: ovalFit, + xRender: boxRender, + }, + { + zName: "spline", + isLine: true, + eJust: 0, + xInit: splineInit, + xNumProp: nil, + xCheck: nil, + xChop: nil, + xOffset: lineOffset, + xFit: nil, + xRender: splineRender, + }, + { + zName: "text", + isLine: false, + eJust: 0, + xInit: textInit, + xNumProp: nil, + xCheck: nil, + xChop: boxChop, + xOffset: textOffset, + xFit: boxFit, + xRender: boxRender, + }, +} +var sublistClass = PClass{ + zName: "[]", + isLine: false, + eJust: 0, + xInit: sublistInit, + xNumProp: nil, + xCheck: nil, + xChop: nil, + xOffset: boxOffset, + xFit: nil, + xRender: nil, +} +var noopClass = PClass{ + zName: "noop", + isLine: false, + eJust: 0, + xInit: nil, + xNumProp: nil, + xCheck: nil, + xChop: nil, + xOffset: boxOffset, + xFit: nil, + xRender: nil, +} + +/* +** Reduce the length of the line segment by amt (if possible) by +** modifying the location of *t. + */ +func pik_chop(f *PPoint, t *PPoint, amt PNum) { + var dx PNum = t.x - f.x + var dy PNum = t.y - f.y + var dist PNum = math.Hypot(dx, dy) + if dist <= amt { + *t = *f + return + } + var r PNum = 1.0 - amt/dist + t.x = f.x + r*dx + t.y = f.y + r*dy +} + +/* +** Draw an arrowhead on the end of the line segment from pFrom to pTo. +** Also, shorten the line segment (by changing the value of pTo) so that +** the shaft of the arrow does not extend into the arrowhead. + */ +func (p *Pik) pik_draw_arrowhead(f *PPoint, t *PPoint, pObj *PObj) { + var dx PNum = t.x - f.x + var dy PNum = t.y - f.y + var dist PNum = math.Hypot(dx, dy) + var h PNum = p.hArrow * pObj.sw + var w PNum = p.wArrow * pObj.sw + if pObj.color < 0.0 { + return + } + if pObj.sw <= 0.0 { + return + } + if dist <= 0.0 { + return + } /* Unable */ + dx /= dist + dy /= dist + var e1 PNum = dist - h + if e1 < 0.0 { + e1 = 0.0 + h = dist + } + var ddx PNum = -w * dy + var ddy PNum = w * dx + var bx PNum = f.x + e1*dx + var by PNum = f.y + e1*dy + p.pik_append_xy("\n", false) + pik_chop(f, t, h/2) +} + +/* +** Compute the relative offset to an edge location from the reference for a +** an statement. + */ +func (p *Pik) pik_elem_offset(pObj *PObj, cp uint8) PPoint { + return pObj.typ.xOffset(p, pObj, cp) +} + +/* +** Append raw text to zOut + */ +func (p *Pik) pik_append(zText string) { + p.zOut.WriteString(zText) +} + +var html_re_with_space = regexp.MustCompile(`[<>& ]`) + +/* +** Append text to zOut with HTML characters escaped. +** +** * The space character is changed into non-breaking space (U+00a0) +** if mFlags has the 0x01 bit set. This is needed when outputting +** text to preserve leading and trailing whitespace. Turns out we +** cannot use   as that is an HTML-ism and is not valid in XML. +** +** * The "&" character is changed into "&" if mFlags has the +** 0x02 bit set. This is needed when generating error message text. +** +** * Except for the above, only "<" and ">" are escaped. + */ +func (p *Pik) pik_append_text(zText string, mFlags int) { + bQSpace := mFlags&1 > 0 + bQAmp := mFlags&2 > 0 + + text := html_re_with_space.ReplaceAllStringFunc(zText, func(s string) string { + switch { + case s == "<": + return "<" + case s == ">": + return ">" + case s == "&" && bQAmp: + return "&" + case s == " " && bQSpace: + return "\302\240" + default: + return s + } + }) + p.pik_append(text) +} + +/* +** Append error message text. This is either a raw append, or an append +** with HTML escapes, depending on whether the PIKCHR_PLAINTEXT_ERRORS flag +** is set. + */ +func (p *Pik) pik_append_errtxt(zText string) { + if p.mFlags&PIKCHR_PLAINTEXT_ERRORS != 0 { + p.pik_append(zText) + } else { + p.pik_append_text(zText, 0) + } +} + +/* Append a PNum value + */ +func (p *Pik) pik_append_num(z string, v PNum) { + p.pik_append(z) + p.pik_append(fmt.Sprintf("%.10g", v)) +} + +/* Append a PPoint value (Used for debugging only) + */ +func (p *Pik) pik_append_point(z string, pPt *PPoint) { + buf := fmt.Sprintf("%.10g,%.10g", pPt.x, pPt.y) + p.pik_append(z) + p.pik_append(buf) +} + +/* +** Invert the RGB color so that it is appropriate for dark mode. +** Variable x hold the initial color. The color is intended for use +** as a background color if isBg is true, and as a foreground color +** if isBg is false. + */ +func pik_color_to_dark_mode(x int, isBg bool) int { + x = 0xffffff - x + r := (x >> 16) & 0xff + g := (x >> 8) & 0xff + b := x & 0xff + mx := r + if g > mx { + mx = g + } + if b > mx { + mx = b + } + mn := r + if g < mn { + mn = g + } + if b < mn { + mn = b + } + r = mn + (mx - r) + g = mn + (mx - g) + b = mn + (mx - b) + if isBg { + if mx > 127 { + r = (127 * r) / mx + g = (127 * g) / mx + b = (127 * b) / mx + } + } else { + if mn < 128 && mx > mn { + r = 127 + ((r-mn)*128)/(mx-mn) + g = 127 + ((g-mn)*128)/(mx-mn) + b = 127 + ((b-mn)*128)/(mx-mn) + } + } + return r*0x10000 + g*0x100 + b +} + +/* Append a PNum value surrounded by text. Do coordinate transformations +** on the value. + */ +func (p *Pik) pik_append_x(z1 string, v PNum, z2 string) { + v -= p.bbox.sw.x + p.pik_append(fmt.Sprintf("%s%d%s", z1, pik_round(p.rScale*v), z2)) +} +func (p *Pik) pik_append_y(z1 string, v PNum, z2 string) { + v = p.bbox.ne.y - v + p.pik_append(fmt.Sprintf("%s%d%s", z1, pik_round(p.rScale*v), z2)) +} +func (p *Pik) pik_append_xy(z1 string, x PNum, y PNum) { + x = x - p.bbox.sw.x + y = p.bbox.ne.y - y + p.pik_append(fmt.Sprintf("%s%d,%d", z1, pik_round(p.rScale*x), pik_round(p.rScale*y))) +} +func (p *Pik) pik_append_dis(z1 string, v PNum, z2 string) { + p.pik_append(fmt.Sprintf("%s%.6g%s", z1, p.rScale*v, z2)) +} + +/* Append a color specification to the output. +** +** In PIKCHR_DARK_MODE, the color is inverted. The "bg" flags indicates that +** the color is intended for use as a background color if true, or as a +** foreground color if false. The distinction only matters for color +** inversions in PIKCHR_DARK_MODE. + */ +func (p *Pik) pik_append_clr(z1 string, v PNum, z2 string, bg bool) { + x := pik_round(v) + if x == 0 && p.fgcolor > 0 && !bg { + x = p.fgcolor + } else if bg && x >= 0xffffff && p.bgcolor > 0 { + x = p.bgcolor + } else if p.mFlags&PIKCHR_DARK_MODE != 0 { + x = pik_color_to_dark_mode(x, bg) + } + r := (x >> 16) & 0xff + g := (x >> 8) & 0xff + b := x & 0xff + buf := fmt.Sprintf("%srgb(%d,%d,%d)%s", z1, r, g, b, z2) + p.pik_append(buf) +} + +/* Append an SVG path A record: +** +** A r1 r2 0 0 0 x y + */ +func (p *Pik) pik_append_arc(r1 PNum, r2 PNum, x PNum, y PNum) { + x = x - p.bbox.sw.x + y = p.bbox.ne.y - y + buf := fmt.Sprintf("A%d %d 0 0 0 %d %d", + pik_round(p.rScale*r1), pik_round(p.rScale*r2), + pik_round(p.rScale*x), pik_round(p.rScale*y)) + p.pik_append(buf) +} + +/* Append a style="..." text. But, leave the quote unterminated, in case +** the caller wants to add some more. +** +** eFill is non-zero to fill in the background, or 0 if no fill should +** occur. Non-zero values of eFill determine the "bg" flag to pik_append_clr() +** for cases when pObj.fill==pObj.color +** +** 1 fill is background, and color is foreground. +** 2 fill and color are both foreground. (Used by "dot" objects) +** 3 fill and color are both background. (Used by most other objs) + */ +func (p *Pik) pik_append_style(pObj *PObj, eFill int) { + clrIsBg := false + p.pik_append(" style=\"") + if pObj.fill >= 0 && eFill != 0 { + fillIsBg := true + if pObj.fill == pObj.color { + if eFill == 2 { + fillIsBg = false + } + if eFill == 3 { + clrIsBg = true + } + } + p.pik_append_clr("fill:", pObj.fill, ";", fillIsBg) + } else { + p.pik_append("fill:none;") + } + if pObj.sw > 0.0 && pObj.color >= 0.0 { + sw := pObj.sw + p.pik_append_dis("stroke-width:", sw, ";") + if pObj.nPath > 2 && pObj.rad <= pObj.sw { + p.pik_append("stroke-linejoin:round;") + } + p.pik_append_clr("stroke:", pObj.color, ";", clrIsBg) + if pObj.dotted > 0.0 { + v := pObj.dotted + if sw < 2.1/p.rScale { + sw = 2.1 / p.rScale + } + p.pik_append_dis("stroke-dasharray:", sw, "") + p.pik_append_dis(",", v, ";") + } else if pObj.dashed > 0.0 { + v := pObj.dashed + p.pik_append_dis("stroke-dasharray:", v, "") + p.pik_append_dis(",", v, ";") + } + } +} + +/* +** Compute the vertical locations for all text items in the +** object pObj. In other words, set every pObj.aTxt[*].eCode +** value to contain exactly one of: TP_ABOVE2, TP_ABOVE, TP_CENTER, +** TP_BELOW, or TP_BELOW2 is set. + */ +func pik_txt_vertical_layout(pObj *PObj) { + n := int(pObj.nTxt) + if n == 0 { + return + } + aTxt := pObj.aTxt[:] + if n == 1 { + if (aTxt[0].eCode & TP_VMASK) == 0 { + aTxt[0].eCode |= TP_CENTER + } + } else { + allSlots := int16(0) + var aFree [5]int16 + /* If there is more than one TP_ABOVE, change the first to TP_ABOVE2. */ + for j, mJust, i := 0, int16(0), n-1; i >= 0; i-- { + if aTxt[i].eCode&TP_ABOVE != 0 { + if j == 0 { + j++ + mJust = aTxt[i].eCode & TP_JMASK + } else if j == 1 && mJust != 0 && (aTxt[i].eCode&mJust) == 0 { + j++ + } else { + aTxt[i].eCode = (aTxt[i].eCode &^ TP_VMASK) | TP_ABOVE2 + break + } + } + } + /* If there is more than one TP_BELOW, change the last to TP_BELOW2 */ + for j, mJust, i := 0, int16(0), 0; i < n; i++ { + if aTxt[i].eCode&TP_BELOW != 0 { + if j == 0 { + j++ + mJust = aTxt[i].eCode & TP_JMASK + } else if j == 1 && mJust != 0 && (aTxt[i].eCode&mJust) == 0 { + j++ + } else { + aTxt[i].eCode = (aTxt[i].eCode &^ TP_VMASK) | TP_BELOW2 + break + } + } + } + /* Compute a mask of all slots used */ + for i := 0; i < n; i++ { + allSlots |= aTxt[i].eCode & TP_VMASK + } + /* Set of an array of available slots */ + if n == 2 && ((aTxt[0].eCode|aTxt[1].eCode)&TP_JMASK) == (TP_LJUST|TP_RJUST) { + /* Special case of two texts that have opposite justification: + ** Allow them both to float to center. */ + aFree[0] = TP_CENTER + aFree[1] = TP_CENTER + } else { + /* Set up the arrow so that available slots are filled from top to + ** bottom */ + iSlot := 0 + if n >= 4 && (allSlots&TP_ABOVE2) == 0 { + aFree[iSlot] = TP_ABOVE2 + iSlot++ + } + if (allSlots & TP_ABOVE) == 0 { + aFree[iSlot] = TP_ABOVE + iSlot++ + } + if (n & 1) != 0 { + aFree[iSlot] = TP_CENTER + iSlot++ + } + if (allSlots & TP_BELOW) == 0 { + aFree[iSlot] = TP_BELOW + iSlot++ + } + if n >= 4 && (allSlots&TP_BELOW2) == 0 { + aFree[iSlot] = TP_BELOW2 + iSlot++ + } + } + /* Set the VMASK for all unassigned texts */ + for i, iSlot := 0, 0; i < n; i++ { + if (aTxt[i].eCode & TP_VMASK) == 0 { + aTxt[i].eCode |= aFree[iSlot] + iSlot++ + } + } + } +} + +/* Return the font scaling factor associated with the input text attribute. + */ +func (p* Pik) pik_font_scale(t PToken) PNum { + scale := p.svgFontScale + if t.eCode&TP_BIG != 0 { + scale *= 1.25 + } + if t.eCode&TP_SMALL != 0 { + scale *= 0.8 + } + if t.eCode&TP_XTRA != 0 { + scale *= scale + } + return scale +} + +/* Append multiple SVG elements for the text fields of the PObj. +** Parameters: +** +** p The Pik object into which we are rendering +** +** pObj Object containing the text to be rendered +** +** pBox If not NULL, do no rendering at all. Instead +** expand the box object so that it will include all +** of the text. + */ +func (p *Pik) pik_append_txt(pObj *PObj, pBox *PBox) { + var jw PNum /* Justification margin relative to center */ + var ha2 PNum = 0.0 /* Height of the top row of text */ + var ha1 PNum = 0.0 /* Height of the second "above" row */ + var hc PNum = 0.0 /* Height of the center row */ + var hb1 PNum = 0.0 /* Height of the first "below" row of text */ + var hb2 PNum = 0.0 /* Height of the second "below" row */ + var yBase PNum = 0.0 + allMask := int16(0) + + if p.nErr != 0 { + return + } + if pObj.nTxt == 0 { + return + } + aTxt := pObj.aTxt[:] + n := int(pObj.nTxt) + pik_txt_vertical_layout(pObj) + x := pObj.ptAt.x + for i := 0; i < n; i++ { + allMask |= pObj.aTxt[i].eCode + } + if pObj.typ.isLine { + hc = pObj.sw * 1.5 + } else if pObj.rad > 0.0 && pObj.typ.zName == "cylinder" { + yBase = -0.75 * pObj.rad + } + if allMask&TP_CENTER != 0 { + for i := 0; i < n; i++ { + if pObj.aTxt[i].eCode&TP_CENTER != 0 { + s := p.pik_font_scale(pObj.aTxt[i]) + if hc < s*p.charHeight { + hc = s * p.charHeight + } + } + } + } + if allMask&TP_ABOVE != 0 { + for i := 0; i < n; i++ { + if pObj.aTxt[i].eCode&TP_ABOVE != 0 { + s := p.pik_font_scale(pObj.aTxt[i]) * p.charHeight + if ha1 < s { + ha1 = s + } + } + } + if allMask&TP_ABOVE2 != 0 { + for i := 0; i < n; i++ { + if pObj.aTxt[i].eCode&TP_ABOVE2 != 0 { + s := p.pik_font_scale(pObj.aTxt[i]) * p.charHeight + if ha2 < s { + ha2 = s + } + } + } + } + } + if allMask&TP_BELOW != 0 { + for i := 0; i < n; i++ { + if pObj.aTxt[i].eCode&TP_BELOW != 0 { + s := p.pik_font_scale(pObj.aTxt[i]) * p.charHeight + if hb1 < s { + hb1 = s + } + } + } + if allMask&TP_BELOW2 != 0 { + for i := 0; i < n; i++ { + if pObj.aTxt[i].eCode&TP_BELOW2 != 0 { + s := p.pik_font_scale(pObj.aTxt[i]) * p.charHeight + if hb2 < s { + hb2 = s + } + } + } + } + } + if pObj.typ.eJust == 1 { + jw = 0.5 * (pObj.w - 0.5*(p.charWidth+pObj.sw)) + } else { + jw = 0.0 + } + for i := 0; i < n; i++ { + t := aTxt[i] + xtraFontScale := p.pik_font_scale(t) + var nx PNum = 0 + orig_y := pObj.ptAt.y + y := yBase + if t.eCode&TP_ABOVE2 != 0 { + y += 0.5*hc + ha1 + 0.5*ha2 + } + if t.eCode&TP_ABOVE != 0 { + y += 0.5*hc + 0.5*ha1 + } + if t.eCode&TP_BELOW != 0 { + y -= 0.5*hc + 0.5*hb1 + } + if t.eCode&TP_BELOW2 != 0 { + y -= 0.5*hc + hb1 + 0.5*hb2 + } + if t.eCode&TP_LJUST != 0 { + nx -= jw + } + if t.eCode&TP_RJUST != 0 { + nx += jw + } + + if pBox != nil { + /* If pBox is not NULL, do not draw any . Instead, just expand + ** pBox to include the text */ + var cw PNum = PNum(pik_text_length(t)) * p.charWidth * xtraFontScale * 0.01 + var ch PNum = p.charHeight * 0.5 * xtraFontScale + var x0, y0, x1, y1 PNum /* Boundary of text relative to pObj.ptAt */ + if t.eCode&TP_BOLD != 0 { + cw *= 1.1 + } + if t.eCode&TP_RJUST != 0 { + x0 = nx + y0 = y - ch + x1 = nx - cw + y1 = y + ch + } else if t.eCode&TP_LJUST != 0 { + x0 = nx + y0 = y - ch + x1 = nx + cw + y1 = y + ch + } else { + x0 = nx + cw/2 + y0 = y + ch + x1 = nx - cw/2 + y1 = y - ch + } + if (t.eCode&TP_ALIGN) != 0 && pObj.nPath >= 2 { + nn := pObj.nPath + var dx PNum = pObj.aPath[nn-1].x - pObj.aPath[0].x + var dy PNum = pObj.aPath[nn-1].y - pObj.aPath[0].y + if dx != 0 || dy != 0 { + var dist PNum = math.Hypot(dx, dy) + var tt PNum + dx /= dist + dy /= dist + tt = dx*x0 - dy*y0 + y0 = dy*x0 - dx*y0 + x0 = tt + tt = dx*x1 - dy*y1 + y1 = dy*x1 - dx*y1 + x1 = tt + } + } + pik_bbox_add_xy(pBox, x+x0, orig_y+y0) + pik_bbox_add_xy(pBox, x+x1, orig_y+y1) + continue + } + nx += x + y += orig_y + + p.pik_append_x("= 0.0 { + p.pik_append_clr(" fill=\"", pObj.color, "\"", false) + } + xtraFontScale *= p.fontScale + if xtraFontScale <= 0.99 || xtraFontScale >= 1.01 { + p.pik_append_num(" font-size=\"", xtraFontScale*100.0) + p.pik_append("%\"") + } + if (t.eCode&TP_ALIGN) != 0 && pObj.nPath >= 2 { + nn := pObj.nPath + var dx PNum = pObj.aPath[nn-1].x - pObj.aPath[0].x + var dy PNum = pObj.aPath[nn-1].y - pObj.aPath[0].y + if dx != 0 || dy != 0 { + var ang PNum = math.Atan2(dy, dx) * -180 / math.Pi + p.pik_append_num(" transform=\"rotate(", ang) + p.pik_append_xy(" ", x, orig_y) + p.pik_append(")\"") + } + } + p.pik_append(" dominant-baseline=\"central\">") + var z []byte + var nz int + if t.n >= 2 && t.z[0] == '"' { + z = t.z[1:] + nz = t.n - 2 + } else { + z = t.z + nz = t.n + } + for nz > 0 { + var j int + for j = 0; j < nz && z[j] != '\\'; j++ { + } + if j != 0 { + p.pik_append_text(string(z[:j]), 0x3) + } + if j < nz && (j+1 == nz || z[j+1] == '\\') { + p.pik_append("\") + j++ + } + nz -= j + 1 + if nz > 0 { + z = z[j+1:] + } + } + p.pik_append("\n") + } +} + +/* +** Append text (that will go inside of a
    ...
    ) that +** shows the context of an error token. + */ +func (p *Pik) pik_error_context(pErr *PToken, nContext int) { + var ( + iErrPt int /* Index of first byte of error from start of input */ + iErrCol int /* Column of the error token on its line */ + iStart int /* Start position of the error context */ + iEnd int /* End position of the error context */ + iLineno int /* Line number of the error */ + iFirstLineno int /* Line number of start of error context */ + i int /* Loop counter */ + iBump = 0 /* Bump the location of the error cursor */ + ) + + iErrPt = len(p.sIn.z) - len(pErr.z) // in C, uses pointer math: iErrPt = (int)(pErr->z - p->sIn.z); + if iErrPt >= p.sIn.n { + iErrPt = p.sIn.n - 1 + iBump = 1 + } else { + for iErrPt > 0 && (p.sIn.z[iErrPt] == '\n' || p.sIn.z[iErrPt] == '\r') { + iErrPt-- + iBump = 1 + } + } + iLineno = 1 + for i = 0; i < iErrPt; i++ { + if p.sIn.z[i] == '\n' { + iLineno++ + } + } + iStart = 0 + iFirstLineno = 1 + for iFirstLineno+nContext < iLineno { + for p.sIn.z[iStart] != '\n' { + iStart++ + } + iStart++ + iFirstLineno++ + } + for iEnd = iErrPt; p.sIn.z[iEnd] != 0 && p.sIn.z[iEnd] != '\n'; iEnd++ { + } + i = iStart + for iFirstLineno <= iLineno { + zLineno := fmt.Sprintf("/* %4d */ ", iFirstLineno) + iFirstLineno++ + p.pik_append(zLineno) + for i = iStart; p.sIn.z[i] != 0 && p.sIn.z[i] != '\n'; i++ { + } + p.pik_append_errtxt(string(p.sIn.z[iStart:i])) + iStart = i + 1 + p.pik_append("\n") + } + for iErrCol, i = 0, iErrPt; i > 0 && p.sIn.z[i] != '\n'; iErrCol, i = iErrCol+1, i-1 { + } + for i = 0; i < iErrCol+11+iBump; i++ { + p.pik_append(" ") + } + for i = 0; i < pErr.n; i++ { + p.pik_append("^") + } + p.pik_append("\n") +} + +/* +** Generate an error message for the output. pErr is the token at which +** the error should point. zMsg is the text of the error message. If +** either pErr or zMsg is NULL, generate an out-of-memory error message. +** +** This routine is a no-op if there has already been an error reported. + */ +func (p *Pik) pik_error(pErr *PToken, zMsg string) { + if p == nil { + return + } + if p.nErr > 0 { + return + } + p.nErr++ + if zMsg == "" { + if p.mFlags&PIKCHR_PLAINTEXT_ERRORS != 0 { + p.pik_append("\nOut of memory\n") + } else { + p.pik_append("\n

    Out of memory

    \n") + } + return + } + if pErr == nil { + p.pik_append("\n") + p.pik_append_errtxt(zMsg) + return + } + if (p.mFlags & PIKCHR_PLAINTEXT_ERRORS) == 0 { + p.pik_append("
    \n")
    +	}
    +	p.pik_error_context(pErr, 5)
    +	p.pik_append("ERROR: ")
    +	p.pik_append_errtxt(zMsg)
    +	p.pik_append("\n")
    +	for i := p.nCtx - 1; i >= 0; i-- {
    +		p.pik_append("Called from:\n")
    +		p.pik_error_context(&p.aCtx[i], 0)
    +	}
    +	if (p.mFlags & PIKCHR_PLAINTEXT_ERRORS) == 0 {
    +		p.pik_append("
    \n") + } +} + +/* + ** Process an "assert( e1 == e2 )" statement. Always return `nil`. + */ +func (p *Pik) pik_assert(e1 PNum, pEq *PToken, e2 PNum) *PObj { + /* Convert the numbers to strings using %g for comparison. This + ** limits the precision of the comparison to account for rounding error. */ + zE1 := fmt.Sprintf("%g", e1) + zE2 := fmt.Sprintf("%g", e2) + if zE1 != zE2 { + p.pik_error(pEq, fmt.Sprintf("%.50s != %.50s", zE1, zE2)) + } + return nil +} + +/* +** Process an "assert( place1 == place2 )" statement. Always return `nil`. + */ +func (p *Pik) pik_position_assert(e1 *PPoint, pEq *PToken, e2 *PPoint) *PObj { + /* Convert the numbers to strings using %g for comparison. This + ** limits the precision of the comparison to account for rounding error. */ + zE1 := fmt.Sprintf("(%g,%g)", e1.x, e1.y) + zE2 := fmt.Sprintf("(%g,%g)", e2.x, e2.y) + if zE1 != zE2 { + p.pik_error(pEq, fmt.Sprintf("%s != %s", zE1, zE2)) + } + return nil +} + +/* Free a complete list of objects */ +func (p *Pik) pik_elist_free(pList *PList) { + if pList == nil || *pList == nil { + return + } + for i := 0; i < len(*pList); i++ { + p.pik_elem_free((*pList)[i]) + } +} + +/* Free a single object, and its substructure */ +func (p *Pik) pik_elem_free(pObj *PObj) { + if pObj == nil { + return + } + p.pik_elist_free(&pObj.pSublist) +} + +/* Convert a numeric literal into a number. Return that number. +** There is no error handling because the tokenizer has already +** assured us that the numeric literal is valid. +** +** Allowed number forms: +** +** (1) Floating point literal +** (2) Same as (1) but followed by a unit: "cm", "mm", "in", +** "px", "pt", or "pc". +** (3) Hex integers: 0x000000 +** +** This routine returns the result in inches. If a different unit +** is specified, the conversion happens automatically. + */ +func pik_atof(num *PToken) PNum { + if num.n >= 3 && num.z[0] == '0' && (num.z[1] == 'x' || num.z[1] == 'X') { + i, err := strconv.ParseInt(string(num.z[2:num.n]), 16, 64) + if err != nil { + return 0 + } + return PNum(i) + } + factor := 1.0 + + z := num.String() + + if num.n > 2 { + hasSuffix := true + switch string(num.z[num.n-2 : num.n]) { + case "cm": + factor = 1 / 2.54 + case "mm": + factor = 1 / 25.4 + case "px": + factor = 1 / 96.0 + case "pt": + factor = 1 / 72.0 + case "pc": + factor = 1 / 6.0 + case "in": + factor = 1.0 + default: + hasSuffix = false + } + if hasSuffix { + z = z[:len(z)-2] + } + } + + ans, err := strconv.ParseFloat(z, 64) + ans *= factor + if err != nil { + return 0.0 + } + return PNum(ans) +} + +/* +** Compute the distance between two points + */ +func pik_dist(pA *PPoint, pB *PPoint) PNum { + dx := pB.x - pA.x + dy := pB.y - pA.y + return math.Hypot(dx, dy) +} + +/* Return true if a bounding box is empty. + */ +func pik_bbox_isempty(p *PBox) bool { + return p.sw.x > p.ne.x +} + +/* Return true if point pPt is contained within the bounding box pBox + */ +func pik_bbox_contains_point(pBox *PBox, pPt *PPoint) bool { + if pik_bbox_isempty(pBox) { + return false + } + if pPt.x < pBox.sw.x { + return false + } + if pPt.x > pBox.ne.x { + return false + } + if pPt.y < pBox.sw.y { + return false + } + if pPt.y > pBox.ne.y { + return false + } + return true +} + +/* Initialize a bounding box to an empty container + */ +func pik_bbox_init(p *PBox) { + p.sw.x = 1.0 + p.sw.y = 1.0 + p.ne.x = 0.0 + p.ne.y = 0.0 +} + +/* Enlarge the PBox of the first argument so that it fully +** covers the second PBox + */ +func pik_bbox_addbox(pA *PBox, pB *PBox) { + if pik_bbox_isempty(pA) { + *pA = *pB + } + if pik_bbox_isempty(pB) { + return + } + if pA.sw.x > pB.sw.x { + pA.sw.x = pB.sw.x + } + if pA.sw.y > pB.sw.y { + pA.sw.y = pB.sw.y + } + if pA.ne.x < pB.ne.x { + pA.ne.x = pB.ne.x + } + if pA.ne.y < pB.ne.y { + pA.ne.y = pB.ne.y + } +} + +/* Enlarge the PBox of the first argument, if necessary, so that +** it contains the point described by the 2nd and 3rd arguments. + */ +func pik_bbox_add_xy(pA *PBox, x PNum, y PNum) { + if pik_bbox_isempty(pA) { + pA.ne.x = x + pA.ne.y = y + pA.sw.x = x + pA.sw.y = y + return + } + if pA.sw.x > x { + pA.sw.x = x + } + if pA.sw.y > y { + pA.sw.y = y + } + if pA.ne.x < x { + pA.ne.x = x + } + if pA.ne.y < y { + pA.ne.y = y + } +} + +/* Enlarge the PBox so that it is able to contain an ellipse +** centered at x,y and with radiuses rx and ry. + */ +func pik_bbox_addellipse(pA *PBox, x PNum, y PNum, rx PNum, ry PNum) { + if pik_bbox_isempty(pA) { + pA.ne.x = x + rx + pA.ne.y = y + ry + pA.sw.x = x - rx + pA.sw.y = y - ry + return + } + if pA.sw.x > x-rx { + pA.sw.x = x - rx + } + if pA.sw.y > y-ry { + pA.sw.y = y - ry + } + if pA.ne.x < x+rx { + pA.ne.x = x + rx + } + if pA.ne.y < y+ry { + pA.ne.y = y + ry + } +} + +/* Append a new object onto the end of an object list. The +** object list is created if it does not already exist. Return +** the new object list. + */ +func (p *Pik) pik_elist_append(pList PList, pObj *PObj) PList { + if pObj == nil { + return pList + } + pList = append(pList, pObj) + p.list = pList + return pList +} + +/* Convert an object class name into a PClass pointer + */ +func pik_find_class(pId *PToken) *PClass { + zString := pId.String() + first := 0 + last := len(aClass) - 1 + for { + mid := (first + last) / 2 + c := strings.Compare(aClass[mid].zName, zString) + if c == 0 { + return &aClass[mid] + } + if c < 0 { + first = mid + 1 + } else { + last = mid - 1 + } + + if first > last { + return nil + } + } +} + +/* Allocate and return a new PObj object. +** +** If pId!=0 then pId is an identifier that defines the object class. +** If pStr!=0 then it is a STRING literal that defines a text object. +** If pSublist!=0 then this is a [...] object. If all three parameters +** are NULL then this is a no-op object used to define a PLACENAME. + */ +func (p *Pik) pik_elem_new(pId *PToken, pStr *PToken, pSublist PList) *PObj { + miss := false + if p.nErr != 0 { + return nil + } + pNew := &PObj{} + + p.cur = pNew + p.nTPath = 1 + p.thenFlag = false + if len(p.list) == 0 { + pNew.ptAt.x = 0.0 + pNew.ptAt.y = 0.0 + pNew.eWith = CP_C + } else { + pPrior := p.list[len(p.list)-1] + pNew.ptAt = pPrior.ptExit + switch p.eDir { + default: + pNew.eWith = CP_W + case DIR_LEFT: + pNew.eWith = CP_E + case DIR_UP: + pNew.eWith = CP_S + case DIR_DOWN: + pNew.eWith = CP_N + } + } + p.aTPath[0] = pNew.ptAt + pNew.with = pNew.ptAt + pNew.outDir = p.eDir + pNew.inDir = p.eDir + pNew.iLayer = p.pik_value_int("layer", &miss) + if miss { + pNew.iLayer = 1000 + } + if pNew.iLayer < 0 { + pNew.iLayer = 0 + } + if pSublist != nil { + pNew.typ = &sublistClass + pNew.pSublist = pSublist + sublistClass.xInit(p, pNew) + return pNew + } + if pStr != nil { + n := PToken{ + z: []byte("text"), + n: 4, + } + pNew.typ = pik_find_class(&n) + assert(pNew.typ != nil, "pNew.typ!=nil") + pNew.errTok = *pStr + pNew.typ.xInit(p, pNew) + p.pik_add_txt(pStr, pStr.eCode) + return pNew + } + if pId != nil { + pNew.errTok = *pId + pClass := pik_find_class(pId) + if pClass != nil { + pNew.typ = pClass + pNew.sw = p.pik_value("thickness", nil) + pNew.fill = p.pik_value("fill", nil) + pNew.color = p.pik_value("color", nil) + pClass.xInit(p, pNew) + return pNew + } + p.pik_error(pId, "unknown object type") + p.pik_elem_free(pNew) + return nil + } + pNew.typ = &noopClass + pNew.ptExit = pNew.ptAt + pNew.ptEnter = pNew.ptAt + return pNew +} + +/* +** If the ID token in the argument is the name of a macro, return +** the PMacro object for that macro + */ +func (p *Pik) pik_find_macro(pId *PToken) *PMacro { + for pMac := p.pMacros; pMac != nil; pMac = pMac.pNext { + if pMac.macroName.n == pId.n && bytesEq(pMac.macroName.z[:pMac.macroName.n], pId.z[:pId.n]) { + return pMac + } + } + return nil +} + +/* Add a new macro + */ +func (p *Pik) pik_add_macro( + pId *PToken, /* The ID token that defines the macro name */ + pCode *PToken, /* Macro body inside of {...} */ +) { + pNew := p.pik_find_macro(pId) + if pNew == nil { + pNew = &PMacro{ + pNext: p.pMacros, + macroName: *pId, + } + p.pMacros = pNew + } + pNew.macroBody.z = pCode.z[1:] + pNew.macroBody.n = pCode.n - 2 + pNew.inUse = false +} + +/* +** Set the output direction and exit point for an object + */ +func pik_elem_set_exit(pObj *PObj, eDir uint8) { + assert(ValidDir(eDir), "ValidDir(eDir)") + pObj.outDir = eDir + if !pObj.typ.isLine || pObj.bClose { + pObj.ptExit = pObj.ptAt + switch pObj.outDir { + default: + pObj.ptExit.x += pObj.w * 0.5 + case DIR_LEFT: + pObj.ptExit.x -= pObj.w * 0.5 + case DIR_UP: + pObj.ptExit.y += pObj.h * 0.5 + case DIR_DOWN: + pObj.ptExit.y -= pObj.h * 0.5 + } + } +} + +/* Change the layout direction. + */ +func (p *Pik) pik_set_direction(eDir uint8) { + assert(ValidDir(eDir), "ValidDir(eDir)") + p.eDir = eDir + + /* It seems to make sense to reach back into the last object and + ** change its exit point (its ".end") to correspond to the new + ** direction. Things just seem to work better this way. However, + ** legacy PIC does *not* do this. + ** + ** The difference can be seen in a script like this: + ** + ** arrow; circle; down; arrow + ** + ** You can make pikchr render the above exactly like PIC + ** by deleting the following three lines. But I (drh) think + ** it works better with those lines in place. + */ + if len(p.list) > 0 { + pik_elem_set_exit(p.list[len(p.list)-1], eDir) + } +} + +/* Move all coordinates contained within an object (and within its +** substructure) by dx, dy + */ +func pik_elem_move(pObj *PObj, dx PNum, dy PNum) { + pObj.ptAt.x += dx + pObj.ptAt.y += dy + pObj.ptEnter.x += dx + pObj.ptEnter.y += dy + pObj.ptExit.x += dx + pObj.ptExit.y += dy + pObj.bbox.ne.x += dx + pObj.bbox.ne.y += dy + pObj.bbox.sw.x += dx + pObj.bbox.sw.y += dy + for i := 0; i < pObj.nPath; i++ { + pObj.aPath[i].x += dx + pObj.aPath[i].y += dy + } + if pObj.pSublist != nil { + pik_elist_move(pObj.pSublist, dx, dy) + } +} +func pik_elist_move(pList PList, dx PNum, dy PNum) { + for i := 0; i < len(pList); i++ { + pik_elem_move(pList[i], dx, dy) + } +} + +/* +** Check to see if it is ok to set the value of paraemeter mThis. +** Return 0 if it is ok. If it not ok, generate an appropriate +** error message and return non-zero. +** +** Flags are set in pObj so that the same object or conflicting +** objects may not be set again. +** +** To be ok, bit mThis must be clear and no more than one of +** the bits identified by mBlockers may be set. + */ +func (p *Pik) pik_param_ok( + pObj *PObj, /* The object under construction */ + pId *PToken, /* Make the error point to this token */ + mThis uint, /* Value we are trying to set */ +) bool { + if pObj.mProp&mThis != 0 { + p.pik_error(pId, "value is already set") + return true + } + if pObj.mCalc&mThis != 0 { + p.pik_error(pId, "value already fixed by prior constraints") + return true + } + pObj.mProp |= mThis + return false +} + +/* +** Set a numeric property like "width 7" or "radius 200%". +** +** The rAbs term is an absolute value to add in. rRel is +** a relative value by which to change the current value. + */ +func (p *Pik) pik_set_numprop(pId *PToken, pVal *PRel) { + pObj := p.cur + switch pId.eType { + case T_HEIGHT: + if p.pik_param_ok(pObj, pId, A_HEIGHT) { + return + } + pObj.h = pObj.h*pVal.rRel + pVal.rAbs + case T_WIDTH: + if p.pik_param_ok(pObj, pId, A_WIDTH) { + return + } + pObj.w = pObj.w*pVal.rRel + pVal.rAbs + case T_RADIUS: + if p.pik_param_ok(pObj, pId, A_RADIUS) { + return + } + pObj.rad = pObj.rad*pVal.rRel + pVal.rAbs + case T_DIAMETER: + if p.pik_param_ok(pObj, pId, A_RADIUS) { + return + } + pObj.rad = pObj.rad*pVal.rRel + 0.5*pVal.rAbs /* diam it 2x rad */ + case T_THICKNESS: + if p.pik_param_ok(pObj, pId, A_THICKNESS) { + return + } + pObj.sw = pObj.sw*pVal.rRel + pVal.rAbs + } + if pObj.typ.xNumProp != nil { + pObj.typ.xNumProp(p, pObj, pId) + } +} + +/* +** Set a color property. The argument is an RGB value. + */ +func (p *Pik) pik_set_clrprop(pId *PToken, rClr PNum) { + pObj := p.cur + switch pId.eType { + case T_FILL: + if p.pik_param_ok(pObj, pId, A_FILL) { + return + } + pObj.fill = rClr + case T_COLOR: + if p.pik_param_ok(pObj, pId, A_COLOR) { + return + } + pObj.color = rClr + break + } + if pObj.typ.xNumProp != nil { + pObj.typ.xNumProp(p, pObj, pId) + } +} + +/* +** Set a "dashed" property like "dash 0.05" +** +** Use the value supplied by pVal if available. If pVal==0, use +** a default. + */ +func (p *Pik) pik_set_dashed(pId *PToken, pVal *PNum) { + pObj := p.cur + switch pId.eType { + case T_DOTTED: + if pVal != nil { + pObj.dotted = *pVal + } else { + pObj.dotted = p.pik_value("dashwid", nil) + } + pObj.dashed = 0.0 + case T_DASHED: + if pVal != nil { + pObj.dashed = *pVal + } else { + pObj.dashed = p.pik_value("dashwid", nil) + } + pObj.dotted = 0.0 + } +} + +/* +** If the current path information came from a "same" or "same as" +** reset it. + */ +func (p *Pik) pik_reset_samepath() { + if p.samePath { + p.samePath = false + p.nTPath = 1 + } +} + +/* Add a new term to the path for a line-oriented object by transferring +** the information in the ptTo field over onto the path and into ptFrom +** resetting the ptTo. + */ +func (p *Pik) pik_then(pToken *PToken, pObj *PObj) { + if !pObj.typ.isLine { + p.pik_error(pToken, "use with line-oriented objects only") + return + } + n := p.nTPath - 1 + if n < 1 && (pObj.mProp&A_FROM) == 0 { + p.pik_error(pToken, "no prior path points") + return + } + p.thenFlag = true +} + +/* Advance to the next entry in p.aTPath. Return its index. + */ +func (p *Pik) pik_next_rpath(pErr *PToken) int { + n := p.nTPath - 1 + if n+1 >= len(p.aTPath) { + (*Pik)(nil).pik_error(pErr, "too many path elements") + return n + } + n++ + p.nTPath++ + p.aTPath[n] = p.aTPath[n-1] + p.mTPath = 0 + return n +} + +/* Add a direction term to an object. "up 0.5", or "left 3", or "down" +** or "down 50%". + */ +func (p *Pik) pik_add_direction(pDir *PToken, pVal *PRel) { + pObj := p.cur + if !pObj.typ.isLine { + if pDir != nil { + p.pik_error(pDir, "use with line-oriented objects only") + } else { + x := pik_next_semantic_token(&pObj.errTok) + p.pik_error(&x, "syntax error") + } + return + } + p.pik_reset_samepath() + n := p.nTPath - 1 + if p.thenFlag || p.mTPath == 3 || n == 0 { + n = p.pik_next_rpath(pDir) + p.thenFlag = false + } + dir := p.eDir + if pDir != nil { + dir = uint8(pDir.eCode) + } + switch dir { + case DIR_UP: + if p.mTPath&2 > 0 { + n = p.pik_next_rpath(pDir) + } + p.aTPath[n].y += pVal.rAbs + pObj.h*pVal.rRel + p.mTPath |= 2 + case DIR_DOWN: + if p.mTPath&2 > 0 { + n = p.pik_next_rpath(pDir) + } + p.aTPath[n].y -= pVal.rAbs + pObj.h*pVal.rRel + p.mTPath |= 2 + case DIR_RIGHT: + if p.mTPath&1 > 0 { + n = p.pik_next_rpath(pDir) + } + p.aTPath[n].x += pVal.rAbs + pObj.w*pVal.rRel + p.mTPath |= 1 + case DIR_LEFT: + if p.mTPath&1 > 0 { + n = p.pik_next_rpath(pDir) + } + p.aTPath[n].x -= pVal.rAbs + pObj.w*pVal.rRel + p.mTPath |= 1 + } + pObj.outDir = dir +} + +/* Process a movement attribute of one of these forms: +** +** pDist pHdgKW rHdg pEdgept +** GO distance HEADING angle +** GO distance compasspoint + */ +func (p *Pik) pik_move_hdg( + pDist *PRel, /* Distance to move */ + pHeading *PToken, /* "heading" keyword if present */ + rHdg PNum, /* Angle argument to "heading" keyword */ + pEdgept *PToken, /* EDGEPT keyword "ne", "sw", etc... */ + pErr *PToken, /* Token to use for error messages */ +) { + pObj := p.cur + var rDist PNum = pDist.rAbs + p.pik_value("linewid", nil)*pDist.rRel + if !pObj.typ.isLine { + p.pik_error(pErr, "use with line-oriented objects only") + return + } + p.pik_reset_samepath() + n := 0 + for n < 1 { + n = p.pik_next_rpath(pErr) + } + if pHeading != nil { + if rHdg < 0.0 || rHdg > 360.0 { + p.pik_error(pHeading, "headings should be between 0 and 360") + return + } + } else if pEdgept.eEdge == CP_C { + p.pik_error(pEdgept, "syntax error") + return + } else { + rHdg = pik_hdg_angle[pEdgept.eEdge] + } + if rHdg <= 45.0 { + pObj.outDir = DIR_UP + } else if rHdg <= 135.0 { + pObj.outDir = DIR_RIGHT + } else if rHdg <= 225.0 { + pObj.outDir = DIR_DOWN + } else if rHdg <= 315.0 { + pObj.outDir = DIR_LEFT + } else { + pObj.outDir = DIR_UP + } + rHdg *= 0.017453292519943295769 /* degrees to radians */ + p.aTPath[n].x += rDist * math.Sin(rHdg) + p.aTPath[n].y += rDist * math.Cos(rHdg) + p.mTPath = 2 +} + +/* Process a movement attribute of the form "right until even with ..." + ** + ** pDir is the first keyword, "right" or "left" or "up" or "down". + ** The movement is in that direction until its closest approach to + ** the point specified by pPoint. + */ +func (p *Pik) pik_evenwith(pDir *PToken, pPlace *PPoint) { + pObj := p.cur + + if !pObj.typ.isLine { + p.pik_error(pDir, "use with line-oriented objects only") + return + } + p.pik_reset_samepath() + n := p.nTPath - 1 + if p.thenFlag || p.mTPath == 3 || n == 0 { + n = p.pik_next_rpath(pDir) + p.thenFlag = false + } + switch pDir.eCode { + case DIR_DOWN, DIR_UP: + if p.mTPath&2 != 0 { + n = p.pik_next_rpath(pDir) + } + p.aTPath[n].y = pPlace.y + p.mTPath |= 2 + case DIR_RIGHT, DIR_LEFT: + if p.mTPath&1 != 0 { + n = p.pik_next_rpath(pDir) + } + p.aTPath[n].x = pPlace.x + p.mTPath |= 1 + } + pObj.outDir = uint8(pDir.eCode) +} + +/* If the last referenced object is centered at point pPt then return +** a pointer to that object. If there is no prior object reference, +** or if the points are not the same, return NULL. +** +** This is a side-channel hack used to find the objects at which a +** line begins and ends. For example, in +** +** arrow from OBJ1 to OBJ2 chop +** +** The arrow object is normally just handed the coordinates of the +** centers for OBJ1 and OBJ2. But we also want to know the specific +** object named in case there are multiple objects centered at the +** same point. +** +** See forum post 1d46e3a0bc + */ +func (p *Pik) pik_last_ref_object(pPt *PPoint) *PObj { + var pRes *PObj + if p.lastRef == nil { + return nil + } + if p.lastRef.ptAt.x == pPt.x && p.lastRef.ptAt.y == pPt.y { + pRes = p.lastRef + } + p.lastRef = nil + return pRes +} + +/* Set the "from" of an object + */ +func (p *Pik) pik_set_from(pObj *PObj, pTk *PToken, pPt *PPoint) { + if !pObj.typ.isLine { + p.pik_error(pTk, "use \"at\" to position this object") + return + } + if pObj.mProp&A_FROM != 0 { + p.pik_error(pTk, "line start location already fixed") + return + } + if pObj.bClose { + p.pik_error(pTk, "polygon is closed") + return + } + if p.nTPath > 1 { + var dx PNum = pPt.x - p.aTPath[0].x + var dy PNum = pPt.y - p.aTPath[0].y + for i := 1; i < p.nTPath; i++ { + p.aTPath[i].x += dx + p.aTPath[i].y += dy + } + } + p.aTPath[0] = *pPt + p.mTPath = 3 + pObj.mProp |= A_FROM + pObj.pFrom = p.pik_last_ref_object(pPt) +} + +/* Set the "to" of an object + */ +func (p *Pik) pik_add_to(pObj *PObj, pTk *PToken, pPt *PPoint) { + n := p.nTPath - 1 + if !pObj.typ.isLine { + p.pik_error(pTk, "use \"at\" to position this object") + return + } + if pObj.bClose { + p.pik_error(pTk, "polygon is closed") + return + } + p.pik_reset_samepath() + if n == 0 || p.mTPath == 3 || p.thenFlag { + n = p.pik_next_rpath(pTk) + } + p.aTPath[n] = *pPt + p.mTPath = 3 + pObj.pTo = p.pik_last_ref_object(pPt) +} + +func (p *Pik) pik_close_path(pErr *PToken) { + pObj := p.cur + if p.nTPath < 3 { + p.pik_error(pErr, + "need at least 3 vertexes in order to close the polygon") + return + } + if pObj.bClose { + p.pik_error(pErr, "polygon already closed") + return + } + pObj.bClose = true +} + +/* Lower the layer of the current object so that it is behind the +** given object. + */ +func (p *Pik) pik_behind(pOther *PObj) { + pObj := p.cur + if p.nErr == 0 && pObj.iLayer >= pOther.iLayer { + pObj.iLayer = pOther.iLayer - 1 + } +} + +/* Set the "at" of an object + */ +func (p *Pik) pik_set_at(pEdge *PToken, pAt *PPoint, pErrTok *PToken) { + eDirToCp := []uint8{CP_E, CP_S, CP_W, CP_N} + if p.nErr != 0 { + return + } + pObj := p.cur + + if pObj.typ.isLine { + p.pik_error(pErrTok, "use \"from\" and \"to\" to position this object") + return + } + if pObj.mProp&A_AT != 0 { + p.pik_error(pErrTok, "location fixed by prior \"at\"") + return + } + pObj.mProp |= A_AT + pObj.eWith = CP_C + if pEdge != nil { + pObj.eWith = pEdge.eEdge + } + if pObj.eWith >= CP_END { + dir := pObj.inDir + if pObj.eWith == CP_END { + dir = pObj.outDir + } + pObj.eWith = eDirToCp[int(dir)] + } + pObj.with = *pAt +} + +/* +** Try to add a text attribute to an object + */ +func (p *Pik) pik_add_txt(pTxt *PToken, iPos int16) { + pObj := p.cur + if int(pObj.nTxt) >= len(pObj.aTxt) { + p.pik_error(pTxt, "too many text terms") + return + } + pT := &pObj.aTxt[pObj.nTxt] + pObj.nTxt++ + *pT = *pTxt + pT.eCode = iPos +} + +/* Merge "text-position" flags + */ +func pik_text_position(iPrev int, pFlag *PToken) int { + iRes := iPrev + switch pFlag.eType { + case T_LJUST: + iRes = (iRes &^ TP_JMASK) | TP_LJUST + case T_RJUST: + iRes = (iRes &^ TP_JMASK) | TP_RJUST + case T_ABOVE: + iRes = (iRes &^ TP_VMASK) | TP_ABOVE + case T_CENTER: + iRes = (iRes &^ TP_VMASK) | TP_CENTER + case T_BELOW: + iRes = (iRes &^ TP_VMASK) | TP_BELOW + case T_ITALIC: + iRes |= TP_ITALIC + case T_BOLD: + iRes |= TP_BOLD + case T_ALIGNED: + iRes |= TP_ALIGN + case T_BIG: + if iRes&TP_BIG != 0 { + iRes |= TP_XTRA + } else { + iRes = (iRes &^ TP_SZMASK) | TP_BIG + } + case T_SMALL: + if iRes&TP_SMALL != 0 { + iRes |= TP_XTRA + } else { + iRes = (iRes &^ TP_SZMASK) | TP_SMALL + } + } + return iRes +} + +/* +** Table of scale-factor estimates for variable-width characters. +** Actual character widths vary by font. These numbers are only +** guesses. And this table only provides data for ASCII. +** +** 100 means normal width. + */ +var awChar = []byte{ + /* Skip initial 32 control characters */ + /* ' ' */ 45, + /* '!' */ 55, + /* '"' */ 62, + /* '#' */ 115, + /* '$' */ 90, + /* '%' */ 132, + /* '&' */ 125, + /* '\''*/ 40, + + /* '(' */ 55, + /* ')' */ 55, + /* '*' */ 71, + /* '+' */ 115, + /* ',' */ 45, + /* '-' */ 48, + /* '.' */ 45, + /* '/' */ 50, + + /* '0' */ 91, + /* '1' */ 91, + /* '2' */ 91, + /* '3' */ 91, + /* '4' */ 91, + /* '5' */ 91, + /* '6' */ 91, + /* '7' */ 91, + + /* '8' */ 91, + /* '9' */ 91, + /* ':' */ 50, + /* ';' */ 50, + /* '<' */ 120, + /* '=' */ 120, + /* '>' */ 120, + /* '?' */ 78, + + /* '@' */ 142, + /* 'A' */ 102, + /* 'B' */ 105, + /* 'C' */ 110, + /* 'D' */ 115, + /* 'E' */ 105, + /* 'F' */ 98, + /* 'G' */ 105, + + /* 'H' */ 125, + /* 'I' */ 58, + /* 'J' */ 58, + /* 'K' */ 107, + /* 'L' */ 95, + /* 'M' */ 145, + /* 'N' */ 125, + /* 'O' */ 115, + + /* 'P' */ 95, + /* 'Q' */ 115, + /* 'R' */ 107, + /* 'S' */ 95, + /* 'T' */ 97, + /* 'U' */ 118, + /* 'V' */ 102, + /* 'W' */ 150, + + /* 'X' */ 100, + /* 'Y' */ 93, + /* 'Z' */ 100, + /* '[' */ 58, + /* '\\'*/ 50, + /* ']' */ 58, + /* '^' */ 119, + /* '_' */ 72, + + /* '`' */ 72, + /* 'a' */ 86, + /* 'b' */ 92, + /* 'c' */ 80, + /* 'd' */ 92, + /* 'e' */ 85, + /* 'f' */ 52, + /* 'g' */ 92, + + /* 'h' */ 92, + /* 'i' */ 47, + /* 'j' */ 47, + /* 'k' */ 88, + /* 'l' */ 48, + /* 'm' */ 135, + /* 'n' */ 92, + /* 'o' */ 86, + + /* 'p' */ 92, + /* 'q' */ 92, + /* 'r' */ 69, + /* 's' */ 75, + /* 't' */ 58, + /* 'u' */ 92, + /* 'v' */ 80, + /* 'w' */ 121, + + /* 'x' */ 81, + /* 'y' */ 80, + /* 'z' */ 76, + /* '{' */ 91, + /* '|'*/ 49, + /* '}' */ 91, + /* '~' */ 118, +} + +/* Return an estimate of the width of the displayed characters +** in a character string. The returned value is 100 times the +** average character width. +** +** Omit "\" used to escape characters. And count entities like +** "<" as a single character. Multi-byte UTF8 characters count +** as a single character. +** +** Attempt to scale the answer by the actual characters seen. Wide +** characters count more than narrow characters. But the widths are +** only guesses. + */ +func pik_text_length(pToken PToken) int { + n := pToken.n + z := pToken.z + cnt := 0 + for j := 1; j < n-1; j++ { + c := z[j] + if c == '\\' && z[j+1] != '&' { + j++ + c = z[j] + } else if c == '&' { + var k int + for k = j + 1; k < j+7 && z[k] != 0 && z[k] != ';'; k++ { + } + if z[k] == ';' { + j = k + } + cnt += 150 + continue + } + if (c & 0xc0) == 0xc0 { + for j+1 < n-1 && (z[j+1]&0xc0) == 0x80 { + j++ + } + cnt += 100 + continue + } + if c >= 0x20 && c <= 0x7e { + cnt += int(awChar[int(c-0x20)]) + } else { + cnt += 100 + } + } + return cnt +} + +/* Adjust the width, height, and/or radius of the object so that +** it fits around the text that has been added so far. +** +** (1) Only text specified prior to this attribute is considered. +** (2) The text size is estimated based on the charht and charwid +** variable settings. +** (3) The fitted attributes can be changed again after this +** attribute, for example using "width 110%" if this auto-fit +** underestimates the text size. +** (4) Previously set attributes will not be altered. In other words, +** "width 1in fit" might cause the height to change, but the +** width is now set. +** (5) This only works for attributes that have an xFit method. +** +** The eWhich parameter is: +** +** 1: Fit horizontally only +** 2: Fit vertically only +** 3: Fit both ways + */ +func (p *Pik) pik_size_to_fit(pFit *PToken, eWhich int) { + var w, h PNum + var bbox PBox + + if p.nErr != 0 { + return + } + pObj := p.cur + + if pObj.nTxt == 0 { + (*Pik)(nil).pik_error(pFit, "no text to fit to") + return + } + if pObj.typ.xFit == nil { + return + } + pik_bbox_init(&bbox) + p.pik_compute_layout_settings() + p.pik_append_txt(pObj, &bbox) + if eWhich&1 != 0 { + w = (bbox.ne.x - bbox.sw.x) + p.charWidth + } + if eWhich&2 != 0 { + var h1, h2 PNum + h1 = bbox.ne.y - pObj.ptAt.y + h2 = pObj.ptAt.y - bbox.sw.y + hmax := h1 + if h1 < h2 { + hmax = h2 + } + h = 2.0*hmax + 0.5*p.charHeight + } else { + h = 0 + } + pObj.typ.xFit(p, pObj, w, h) + pObj.mProp |= A_FIT +} + +/* Set a local variable name to "val". +** +** The name might be a built-in variable or a color name. In either case, +** a new application-defined variable is set. Since app-defined variables +** are searched first, this will override any built-in variables. + */ +func (p *Pik) pik_set_var(pId *PToken, val PNum, pOp *PToken) { + pVar := p.pVar + for pVar != nil { + if pik_token_eq(pId, pVar.zName) == 0 { + break + } + pVar = pVar.pNext + } + if pVar == nil { + pVar = &PVar{ + zName: pId.String(), + pNext: p.pVar, + val: p.pik_value(pId.String(), nil), + } + p.pVar = pVar + } + switch pOp.eCode { + case T_PLUS: + pVar.val += val + case T_STAR: + pVar.val *= val + case T_MINUS: + pVar.val -= val + case T_SLASH: + if val == 0.0 { + p.pik_error(pOp, "division by zero") + } else { + pVar.val /= val + } + default: + pVar.val = val + } + p.bLayoutVars = false /* Clear the layout setting cache */ +} + +/* +** Round a PNum into the nearest integer + */ +func pik_round(v PNum) int { + switch { + case math.IsNaN(v): + return 0 + case v < -2147483647: + return (-2147483647 - 1) + case v >= 2147483647: + return 2147483647 + default: + return int(v + math.Copysign(1e-15, v)) + } +} + +/* +** Search for the variable named z[0..n-1] in: +** +** * Application defined variables +** * Built-in variables +** +** Return the value of the variable if found. If not found +** return 0.0. Also if pMiss is not NULL, then set it to 1 +** if not found. +** +** This routine is a subroutine to pik_get_var(). But it is also +** used by object implementations to look up (possibly overwritten) +** values for built-in variables like "boxwid". + */ +func (p *Pik) pik_value(z string, pMiss *bool) PNum { + for pVar := p.pVar; pVar != nil; pVar = pVar.pNext { + if pVar.zName == z { + return pVar.val + } + } + first := 0 + last := len(aBuiltin) - 1 + for first <= last { + mid := (first + last) / 2 + zName := aBuiltin[mid].zName + + if zName == z { + return aBuiltin[mid].val + } else if z > zName { + first = mid + 1 + } else { + last = mid - 1 + } + } + if pMiss != nil { + *pMiss = true + } + return 0.0 +} + +func (p *Pik) pik_value_int(z string, pMiss *bool) int { + return pik_round(p.pik_value(z, pMiss)) +} + +/* +** Look up a color-name. Unlike other names in this program, the +** color-names are not case sensitive. So "DarkBlue" and "darkblue" +** and "DARKBLUE" all find the same value (139). +** +** If not found, return -99.0. Also post an error if p!=NULL. +** +** Special color names "None" and "Off" return -1.0 without causing +** an error. + */ +func (p *Pik) pik_lookup_color(pId *PToken) PNum { + first := 0 + last := len(aColor) - 1 + zId := strings.ToLower(pId.String()) + for first <= last { + mid := (first + last) / 2 + zClr := strings.ToLower(aColor[mid].zName) + c := strings.Compare(zId, zClr) + + if c == 0 { + return PNum(aColor[mid].val) + } + if c > 0 { + first = mid + 1 + } else { + last = mid - 1 + } + } + if p != nil { + p.pik_error(pId, "not a known color name") + } + return -99.0 +} + +/* Get the value of a variable. +** +** Search in order: +** +** * Application defined variables +** * Built-in variables +** * Color names +** +** If no such variable is found, throw an error. + */ +func (p *Pik) pik_get_var(pId *PToken) PNum { + miss := false + v := p.pik_value(pId.String(), &miss) + if !miss { + return v + } + v = (*Pik)(nil).pik_lookup_color(pId) + if v > -90.0 { + return v + } + p.pik_error(pId, "no such variable") + return 0.0 +} + +/* Convert a T_NTH token (ex: "2nd", "5th"} into a numeric value and + ** return that value. Throw an error if the value is too big. + */ +func (p *Pik) pik_nth_value(pNth *PToken) int16 { + s := pNth.String() + if s == "first" { + return 1 + } + + i, err := strconv.Atoi(s[:len(s)-2]) + if err != nil { + p.pik_error(pNth, "value can't be parsed as a number") + } + if i > 1000 { + p.pik_error(pNth, "value too big - max '1000th'") + i = 1 + } + return int16(i) +} + +/* Search for the NTH object. +** +** If pBasis is not NULL then it should be a [] object. Use the +** sublist of that [] object for the search. If pBasis is not a [] +** object, then throw an error. +** +** The pNth token describes the N-th search. The pNth.eCode value +** is one more than the number of items to skip. It is negative +** to search backwards. If pNth.eType==T_ID, then it is the name +** of a class to search for. If pNth.eType==T_LB, then +** search for a [] object. If pNth.eType==T_LAST, then search for +** any type. +** +** Raise an error if the item is not found. + */ +func (p *Pik) pik_find_nth(pBasis *PObj, pNth *PToken) *PObj { + var pList PList + var pClass *PClass + if pBasis == nil { + pList = p.list + } else { + pList = pBasis.pSublist + } + if pList == nil { + p.pik_error(pNth, "no such object") + return nil + } + if pNth.eType == T_LAST { + pClass = nil + } else if pNth.eType == T_LB { + pClass = &sublistClass + } else { + pClass = pik_find_class(pNth) + if pClass == nil { + (*Pik)(nil).pik_error(pNth, "no such object type") + return nil + } + } + n := pNth.eCode + if n < 0 { + for i := len(pList) - 1; i >= 0; i-- { + pObj := pList[i] + if pClass != nil && pObj.typ != pClass { + continue + } + n++ + if n == 0 { + return pObj + } + } + } else { + for i := 0; i < len(pList); i++ { + pObj := pList[i] + if pClass != nil && pObj.typ != pClass { + continue + } + n-- + if n == 0 { + return pObj + } + } + } + p.pik_error(pNth, "no such object") + return nil +} + +/* Search for an object by name. +** +** Search in pBasis.pSublist if pBasis is not NULL. If pBasis is NULL +** then search in p.list. + */ +func (p *Pik) pik_find_byname(pBasis *PObj, pName *PToken) *PObj { + var pList PList + if pBasis == nil { + pList = p.list + } else { + pList = pBasis.pSublist + } + if pList == nil { + p.pik_error(pName, "no such object") + return nil + } + /* First look explicitly tagged objects */ + for i := len(pList) - 1; i >= 0; i-- { + pObj := pList[i] + if pObj.zName != "" && pik_token_eq(pName, pObj.zName) == 0 { + p.lastRef = pObj + return pObj + } + } + /* If not found, do a second pass looking for any object containing + ** text which exactly matches pName */ + for i := len(pList) - 1; i >= 0; i-- { + pObj := pList[i] + for j := 0; j < int(pObj.nTxt); j++ { + t := pObj.aTxt[j].n + if t == pName.n+2 && bytesEq(pObj.aTxt[j].z[1:t-1], pName.z[:pName.n]) { + p.lastRef = pObj + return pObj + } + } + } + p.pik_error(pName, "no such object") + return nil +} + +/* Change most of the settings for the current object to be the +** same as the pOther object, or the most recent object of the same +** type if pOther is NULL. + */ +func (p *Pik) pik_same(pOther *PObj, pErrTok *PToken) { + pObj := p.cur + if p.nErr != 0 { + return + } + if pOther == nil { + var i int + for i = len(p.list) - 1; i >= 0; i-- { + pOther = p.list[i] + if pOther.typ == pObj.typ { + break + } + } + if i < 0 { + p.pik_error(pErrTok, "no prior objects of the same type") + return + } + } + if pOther.nPath != 0 && pObj.typ.isLine { + var dx, dy PNum + dx = p.aTPath[0].x - pOther.aPath[0].x + dy = p.aTPath[0].y - pOther.aPath[0].y + for i := 1; i < pOther.nPath; i++ { + p.aTPath[i].x = pOther.aPath[i].x + dx + p.aTPath[i].y = pOther.aPath[i].y + dy + } + p.nTPath = pOther.nPath + p.mTPath = 3 + p.samePath = true + } + if !pObj.typ.isLine { + pObj.w = pOther.w + pObj.h = pOther.h + } + pObj.rad = pOther.rad + pObj.sw = pOther.sw + pObj.dashed = pOther.dashed + pObj.dotted = pOther.dotted + pObj.fill = pOther.fill + pObj.color = pOther.color + pObj.cw = pOther.cw + pObj.larrow = pOther.larrow + pObj.rarrow = pOther.rarrow + pObj.bClose = pOther.bClose + pObj.bChop = pOther.bChop + pObj.inDir = pOther.inDir + pObj.outDir = pOther.outDir + pObj.iLayer = pOther.iLayer +} + +/* Return a "Place" associated with object pObj. If pEdge is NULL +** return the center of the object. Otherwise, return the corner +** described by pEdge. + */ +func (p *Pik) pik_place_of_elem(pObj *PObj, pEdge *PToken) PPoint { + pt := PPoint{} + var pClass *PClass + if pObj == nil { + return pt + } + if pEdge == nil { + return pObj.ptAt + } + pClass = pObj.typ + if pEdge.eType == T_EDGEPT || (pEdge.eEdge > 0 && pEdge.eEdge < CP_END) { + pt = pClass.xOffset(p, pObj, pEdge.eEdge) + pt.x += pObj.ptAt.x + pt.y += pObj.ptAt.y + return pt + } + if pEdge.eType == T_START { + return pObj.ptEnter + } else { + return pObj.ptExit + } +} + +/* Do a linear interpolation of two positions. + */ +func pik_position_between(x PNum, p1 PPoint, p2 PPoint) PPoint { + var out PPoint + out.x = p2.x*x + p1.x*(1.0-x) + out.y = p2.y*x + p1.y*(1.0-x) + return out +} + +/* Compute the position that is dist away from pt at an heading angle of r +** +** The angle is a compass heading in degrees. North is 0 (or 360). +** East is 90. South is 180. West is 270. And so forth. + */ +func pik_position_at_angle(dist PNum, r PNum, pt PPoint) PPoint { + r *= 0.017453292519943295769 /* degrees to radians */ + pt.x += dist * math.Sin(r) + pt.y += dist * math.Cos(r) + return pt +} + +/* Compute the position that is dist away at a compass point + */ +func pik_position_at_hdg(dist PNum, pD *PToken, pt PPoint) PPoint { + return pik_position_at_angle(dist, pik_hdg_angle[pD.eEdge], pt) +} + +/* Return the coordinates for the n-th vertex of a line. + */ +func (p *Pik) pik_nth_vertex(pNth *PToken, pErr *PToken, pObj *PObj) PPoint { + var n int + zero := PPoint{} + if p.nErr != 0 || pObj == nil { + return p.aTPath[0] + } + if !pObj.typ.isLine { + p.pik_error(pErr, "object is not a line") + return zero + } + n, err := strconv.Atoi(string(pNth.z[:pNth.n-2])) + if err != nil || n < 1 || n > pObj.nPath { + p.pik_error(pNth, "no such vertex") + return zero + } + return pObj.aPath[n-1] +} + +/* Return the value of a property of an object. + */ +func pik_property_of(pObj *PObj, pProp *PToken) PNum { + var v PNum + if pObj != nil { + switch pProp.eType { + case T_HEIGHT: + v = pObj.h + case T_WIDTH: + v = pObj.w + case T_RADIUS: + v = pObj.rad + case T_DIAMETER: + v = pObj.rad * 2.0 + case T_THICKNESS: + v = pObj.sw + case T_DASHED: + v = pObj.dashed + case T_DOTTED: + v = pObj.dotted + case T_FILL: + v = pObj.fill + case T_COLOR: + v = pObj.color + case T_X: + v = pObj.ptAt.x + case T_Y: + v = pObj.ptAt.y + case T_TOP: + v = pObj.bbox.ne.y + case T_BOTTOM: + v = pObj.bbox.sw.y + case T_LEFT: + v = pObj.bbox.sw.x + case T_RIGHT: + v = pObj.bbox.ne.x + } + } + return v +} + +/* Compute one of the built-in functions + */ +func (p *Pik) pik_func(pFunc *PToken, x PNum, y PNum) PNum { + var v PNum + switch pFunc.eCode { + case FN_ABS: + v = x + if v < 0 { + v = -v + } + case FN_COS: + v = math.Cos(x) + case FN_INT: + v = math.Round(x) + case FN_SIN: + v = math.Sin(x) + case FN_SQRT: + if x < 0.0 { + p.pik_error(pFunc, "sqrt of negative value") + v = 0.0 + } else { + v = math.Sqrt(x) + } + case FN_MAX: + if x > y { + v = x + } else { + v = y + } + case FN_MIN: + if x < y { + v = x + } else { + v = y + } + default: + v = 0.0 + } + return v +} + +/* Attach a name to an object + */ +func (p *Pik) pik_elem_setname(pObj *PObj, pName *PToken) { + if pObj == nil { + return + } + if pName == nil { + return + } + pObj.zName = pName.String() +} + +/* +** Search for object located at *pCenter that has an xChop method and +** that does not enclose point pOther. +** +** Return a pointer to the object, or NULL if not found. + */ +func pik_find_chopper(pList PList, pCenter *PPoint, pOther *PPoint) *PObj { + if pList == nil { + return nil + } + for i := len(pList) - 1; i >= 0; i-- { + pObj := pList[i] + if pObj.typ.xChop != nil && + pObj.ptAt.x == pCenter.x && + pObj.ptAt.y == pCenter.y && + !pik_bbox_contains_point(&pObj.bbox, pOther) { + return pObj + } else if pObj.pSublist != nil { + pObj = pik_find_chopper(pObj.pSublist, pCenter, pOther) + if pObj != nil { + return pObj + } + } + } + return nil +} + +/* +** There is a line traveling from pFrom to pTo. +** +** If pObj is not null and is a choppable object, then chop at +** the boundary of pObj - where the line crosses the boundary +** of pObj. +** +** If pObj is NULL or has no xChop method, then search for some +** other object centered at pTo that is choppable and use it +** instead. + */ +func (p *Pik) pik_autochop(pFrom *PPoint, pTo *PPoint, pObj *PObj) { + if pObj == nil || pObj.typ.xChop == nil { + pObj = pik_find_chopper(p.list, pTo, pFrom) + } + if pObj != nil { + *pTo = pObj.typ.xChop(p, pObj, pFrom) + } +} + +/* This routine runs after all attributes have been received +** on an object. + */ +func (p *Pik) pik_after_adding_attributes(pObj *PObj) { + if p.nErr != 0 { + return + } + + /* Position block objects */ + if !pObj.typ.isLine { + /* A height or width less than or equal to zero means "autofit". + ** Change the height or width to be big enough to contain the text, + */ + if pObj.h <= 0.0 { + if pObj.nTxt == 0 { + pObj.h = 0.0 + } else if pObj.w <= 0.0 { + p.pik_size_to_fit(&pObj.errTok, 3) + } else { + p.pik_size_to_fit(&pObj.errTok, 2) + } + } + if pObj.w <= 0.0 { + if pObj.nTxt == 0 { + pObj.w = 0.0 + } else { + p.pik_size_to_fit(&pObj.errTok, 1) + } + } + ofst := p.pik_elem_offset(pObj, pObj.eWith) + var dx PNum = (pObj.with.x - ofst.x) - pObj.ptAt.x + var dy PNum = (pObj.with.y - ofst.y) - pObj.ptAt.y + if dx != 0 || dy != 0 { + pik_elem_move(pObj, dx, dy) + } + } + + /* For a line object with no movement specified, a single movement + ** of the default length in the current direction + */ + if pObj.typ.isLine && p.nTPath < 2 { + p.pik_next_rpath(nil) + assert(p.nTPath == 2, fmt.Sprintf("want p.nTPath==2; got %d", p.nTPath)) + switch pObj.inDir { + default: + p.aTPath[1].x += pObj.w + case DIR_DOWN: + p.aTPath[1].y -= pObj.h + case DIR_LEFT: + p.aTPath[1].x -= pObj.w + case DIR_UP: + p.aTPath[1].y += pObj.h + } + if pObj.typ.zName == "arc" { + add := uint8(3) + if pObj.cw { + add = 1 + } + pObj.outDir = (pObj.inDir + add) % 4 + p.eDir = pObj.outDir + switch pObj.outDir { + default: + p.aTPath[1].x += pObj.w + case DIR_DOWN: + p.aTPath[1].y -= pObj.h + case DIR_LEFT: + p.aTPath[1].x -= pObj.w + case DIR_UP: + p.aTPath[1].y += pObj.h + } + } + } + + /* Initialize the bounding box prior to running xCheck */ + pik_bbox_init(&pObj.bbox) + + /* Run object-specific code */ + if pObj.typ.xCheck != nil { + pObj.typ.xCheck(p, pObj) + if p.nErr != 0 { + return + } + } + + /* Compute final bounding box, entry and exit points, center + ** point (ptAt) and path for the object + */ + if pObj.typ.isLine { + pObj.aPath = make([]PPoint, p.nTPath) + pObj.nPath = p.nTPath + copy(pObj.aPath, p.aTPath[:p.nTPath]) + + /* "chop" processing: + ** If the line goes to the center of an object with an + ** xChop method, then use the xChop method to trim the line. + */ + if pObj.bChop && pObj.nPath >= 2 { + n := pObj.nPath + p.pik_autochop(&pObj.aPath[n-2], &pObj.aPath[n-1], pObj.pTo) + p.pik_autochop(&pObj.aPath[1], &pObj.aPath[0], pObj.pFrom) + } + + pObj.ptEnter = pObj.aPath[0] + pObj.ptExit = pObj.aPath[pObj.nPath-1] + + /* Compute the center of the line based on the bounding box over + ** the vertexes. This is a difference from PIC. In Pikchr, the + ** center of a line is the center of its bounding box. In PIC, the + ** center of a line is halfway between its .start and .end. For + ** straight lines, this is the same point, but for multi-segment + ** lines the result is usually diferent */ + for i := 0; i < pObj.nPath; i++ { + pik_bbox_add_xy(&pObj.bbox, pObj.aPath[i].x, pObj.aPath[i].y) + } + pObj.ptAt.x = (pObj.bbox.ne.x + pObj.bbox.sw.x) / 2.0 + pObj.ptAt.y = (pObj.bbox.ne.y + pObj.bbox.sw.y) / 2.0 + + /* Reset the width and height of the object to be the width and height + ** of the bounding box over vertexes */ + pObj.w = pObj.bbox.ne.x - pObj.bbox.sw.x + pObj.h = pObj.bbox.ne.y - pObj.bbox.sw.y + + /* If this is a polygon (if it has the "close" attribute), then + ** adjust the exit point */ + if pObj.bClose { + /* For "closed" lines, the .end is one of the .e, .s, .w, or .n + ** points of the bounding box, as with block objects. */ + pik_elem_set_exit(pObj, pObj.inDir) + } + } else { + var w2 PNum = pObj.w / 2.0 + var h2 PNum = pObj.h / 2.0 + pObj.ptEnter = pObj.ptAt + pObj.ptExit = pObj.ptAt + switch pObj.inDir { + default: + pObj.ptEnter.x -= w2 + case DIR_LEFT: + pObj.ptEnter.x += w2 + case DIR_UP: + pObj.ptEnter.y -= h2 + case DIR_DOWN: + pObj.ptEnter.y += h2 + } + switch pObj.outDir { + default: + pObj.ptExit.x += w2 + case DIR_LEFT: + pObj.ptExit.x -= w2 + case DIR_UP: + pObj.ptExit.y += h2 + case DIR_DOWN: + pObj.ptExit.y -= h2 + } + pik_bbox_add_xy(&pObj.bbox, pObj.ptAt.x-w2, pObj.ptAt.y-h2) + pik_bbox_add_xy(&pObj.bbox, pObj.ptAt.x+w2, pObj.ptAt.y+h2) + } + p.eDir = pObj.outDir +} + +/* Show basic information about each object as a comment in the +** generated HTML. Used for testing and debugging. Activated +** by the (undocumented) "debug = 1;" +** command. + */ +func (p *Pik) pik_elem_render(pObj *PObj) { + var zDir string + if pObj == nil { + return + } + p.pik_append("\n") +} + +/* Render a list of objects + */ +func (p *Pik) pik_elist_render(pList PList) { + var iNextLayer, iThisLayer int + bMoreToDo := true + mDebug := p.pik_value_int("debug", nil) + for bMoreToDo { + bMoreToDo = false + iThisLayer = iNextLayer + iNextLayer = 0x7fffffff + for i := 0; i < len(pList); i++ { + pObj := pList[i] + if pObj.iLayer > iThisLayer { + if pObj.iLayer < iNextLayer { + iNextLayer = pObj.iLayer + } + bMoreToDo = true + continue /* Defer until another round */ + } else if pObj.iLayer < iThisLayer { + continue + } + if mDebug&1 != 0 { + p.pik_elem_render(pObj) + } + xRender := pObj.typ.xRender + if xRender != nil { + xRender(p, pObj) + } + if pObj.pSublist != nil { + p.pik_elist_render(pObj.pSublist) + } + } + } + + /* If the color_debug_label value is defined, then go through + ** and paint a dot at every label location */ + miss := false + var colorLabel PNum = p.pik_value("debug_label_color", &miss) + if !miss && colorLabel >= 0.0 { + dot := PObj{} + dot.typ = &noopClass + dot.rad = 0.015 + dot.sw = 0.015 + dot.fill = colorLabel + dot.color = colorLabel + dot.nTxt = 1 + dot.aTxt[0].eCode = TP_ABOVE + for i := 0; i < len(pList); i++ { + pObj := pList[i] + if pObj.zName == "" { + continue + } + dot.ptAt = pObj.ptAt + dot.aTxt[0].z = []byte(pObj.zName) + dot.aTxt[0].n = len(dot.aTxt[0].z) + dotRender(p, &dot) + } + } +} + +/* Add all objects of the list pList to the bounding box + */ +func (p *Pik) pik_bbox_add_elist(pList PList, wArrow PNum) { + for i := 0; i < len(pList); i++ { + pObj := pList[i] + if pObj.sw > 0.0 { + pik_bbox_addbox(&p.bbox, &pObj.bbox) + } + p.pik_append_txt(pObj, &p.bbox) + if pObj.pSublist != nil { + p.pik_bbox_add_elist(pObj.pSublist, wArrow) + } + + /* Expand the bounding box to account for arrowheads on lines */ + if pObj.typ.isLine && pObj.nPath > 0 { + if pObj.larrow { + pik_bbox_addellipse(&p.bbox, pObj.aPath[0].x, pObj.aPath[0].y, + wArrow, wArrow) + } + if pObj.rarrow { + j := pObj.nPath - 1 + pik_bbox_addellipse(&p.bbox, pObj.aPath[j].x, pObj.aPath[j].y, + wArrow, wArrow) + } + } + } +} + +/* Recompute key layout parameters from variables. */ +func (p *Pik) pik_compute_layout_settings() { + var thickness PNum /* Line thickness */ + var wArrow PNum /* Width of arrowheads */ + + /* Set up rendering parameters */ + if p.bLayoutVars { + return + } + thickness = p.pik_value("thickness", nil) + if thickness <= 0.01 { + thickness = 0.01 + } + wArrow = 0.5 * p.pik_value("arrowwid", nil) + p.wArrow = wArrow / thickness + p.hArrow = p.pik_value("arrowht", nil) / thickness + p.fontScale = p.pik_value("fontscale", nil) + if p.fontScale <= 0.0 { + p.fontScale = 1.0 + } + p.rScale = 144.0 + p.charWidth = p.pik_value("charwid", nil) * p.fontScale + p.charHeight = p.pik_value("charht", nil) * p.fontScale + p.bLayoutVars = true +} + +/* Render a list of objects. Write the SVG into p.zOut. +** Delete the input object_list before returnning. + */ +func (p *Pik) pik_render(pList PList) { + if pList == nil { + return + } + if p.nErr == 0 { + var ( + thickness PNum /* Stroke width */ + margin PNum /* Extra bounding box margin */ + w, h PNum /* Drawing width and height */ + wArrow PNum + pikScale PNum /* Value of the "scale" variable */ + ) + + /* Set up rendering parameters */ + p.pik_compute_layout_settings() + thickness = p.pik_value("thickness", nil) + if thickness <= 0.01 { + thickness = 0.01 + } + margin = p.pik_value("margin", nil) + margin += thickness + wArrow = p.wArrow * thickness + miss := false + p.fgcolor = p.pik_value_int("fgcolor", &miss) + if miss { + var t PToken + t.z = []byte("fgcolor") + t.n = 7 + p.fgcolor = pik_round((*Pik)(nil).pik_lookup_color(&t)) + } + miss = false + p.bgcolor = p.pik_value_int("bgcolor", &miss) + if miss { + var t PToken + t.z = []byte("bgcolor") + t.n = 7 + p.bgcolor = pik_round((*Pik)(nil).pik_lookup_color(&t)) + } + + /* Compute a bounding box over all objects so that we can know + ** how big to declare the SVG canvas */ + pik_bbox_init(&p.bbox) + p.pik_bbox_add_elist(pList, wArrow) + + /* Expand the bounding box slightly to account for line thickness + ** and the optional "margin = EXPR" setting. */ + p.bbox.ne.x += margin + p.pik_value("rightmargin", nil) + p.bbox.ne.y += margin + p.pik_value("topmargin", nil) + p.bbox.sw.x -= margin + p.pik_value("leftmargin", nil) + p.bbox.sw.y -= margin + p.pik_value("bottommargin", nil) + + /* Output the SVG */ + p.pik_append("= 0.001 && pikScale <= 1000.0 && + (pikScale < 0.99 || pikScale > 1.01) { + p.wSVG = pik_round(PNum(p.wSVG) * pikScale) + p.hSVG = pik_round(PNum(p.hSVG) * pikScale) + p.pik_append_num(" width=\"", PNum(p.wSVG)) + p.pik_append_num("\" height=\"", PNum(p.hSVG)) + p.pik_append("\"") + } else { + if p.svgWidth != "" { + p.pik_append(` width="`) + p.pik_append(p.svgWidth) + p.pik_append(`"`) + } + if p.svgHeight != "" { + p.pik_append(` height="`) + p.pik_append(p.svgHeight) + p.pik_append(`"`) + } + } + p.pik_append_dis(" viewBox=\"0 0 ", w, "") + p.pik_append_dis(" ", h, "\">\n") + p.pik_elist_render(pList) + p.pik_append("\n") + } else { + p.wSVG = -1 + p.hSVG = -1 + } + p.pik_elist_free(&pList) +} + +/* +** An array of this structure defines a list of keywords. + */ +type PikWord struct { + zWord string /* Text of the keyword */ + //TODO(zellyn): do we need this? + nChar uint8 /* Length of keyword text in bytes */ + eType uint8 /* Token code */ + eCode uint8 /* Extra code for the token */ + eEdge uint8 /* CP_* code for corner/edge keywords */ +} + +/* +** Keywords + */ +var pik_keywords = []PikWord{ + {"above", 5, T_ABOVE, 0, 0}, + {"abs", 3, T_FUNC1, FN_ABS, 0}, + {"aligned", 7, T_ALIGNED, 0, 0}, + {"and", 3, T_AND, 0, 0}, + {"as", 2, T_AS, 0, 0}, + {"assert", 6, T_ASSERT, 0, 0}, + {"at", 2, T_AT, 0, 0}, + {"behind", 6, T_BEHIND, 0, 0}, + {"below", 5, T_BELOW, 0, 0}, + {"between", 7, T_BETWEEN, 0, 0}, + {"big", 3, T_BIG, 0, 0}, + {"bold", 4, T_BOLD, 0, 0}, + {"bot", 3, T_EDGEPT, 0, CP_S}, + {"bottom", 6, T_BOTTOM, 0, CP_S}, + {"c", 1, T_EDGEPT, 0, CP_C}, + {"ccw", 3, T_CCW, 0, 0}, + {"center", 6, T_CENTER, 0, CP_C}, + {"chop", 4, T_CHOP, 0, 0}, + {"close", 5, T_CLOSE, 0, 0}, + {"color", 5, T_COLOR, 0, 0}, + {"cos", 3, T_FUNC1, FN_COS, 0}, + {"cw", 2, T_CW, 0, 0}, + {"dashed", 6, T_DASHED, 0, 0}, + {"define", 6, T_DEFINE, 0, 0}, + {"diameter", 8, T_DIAMETER, 0, 0}, + {"dist", 4, T_DIST, 0, 0}, + {"dotted", 6, T_DOTTED, 0, 0}, + {"down", 4, T_DOWN, DIR_DOWN, 0}, + {"e", 1, T_EDGEPT, 0, CP_E}, + {"east", 4, T_EDGEPT, 0, CP_E}, + {"end", 3, T_END, 0, CP_END}, + {"even", 4, T_EVEN, 0, 0}, + {"fill", 4, T_FILL, 0, 0}, + {"first", 5, T_NTH, 0, 0}, + {"fit", 3, T_FIT, 0, 0}, + {"from", 4, T_FROM, 0, 0}, + {"go", 2, T_GO, 0, 0}, + {"heading", 7, T_HEADING, 0, 0}, + {"height", 6, T_HEIGHT, 0, 0}, + {"ht", 2, T_HEIGHT, 0, 0}, + {"in", 2, T_IN, 0, 0}, + {"int", 3, T_FUNC1, FN_INT, 0}, + {"invis", 5, T_INVIS, 0, 0}, + {"invisible", 9, T_INVIS, 0, 0}, + {"italic", 6, T_ITALIC, 0, 0}, + {"last", 4, T_LAST, 0, 0}, + {"left", 4, T_LEFT, DIR_LEFT, CP_W}, + {"ljust", 5, T_LJUST, 0, 0}, + {"max", 3, T_FUNC2, FN_MAX, 0}, + {"min", 3, T_FUNC2, FN_MIN, 0}, + {"n", 1, T_EDGEPT, 0, CP_N}, + {"ne", 2, T_EDGEPT, 0, CP_NE}, + {"north", 5, T_EDGEPT, 0, CP_N}, + {"nw", 2, T_EDGEPT, 0, CP_NW}, + {"of", 2, T_OF, 0, 0}, + {"previous", 8, T_LAST, 0, 0}, + {"print", 5, T_PRINT, 0, 0}, + {"rad", 3, T_RADIUS, 0, 0}, + {"radius", 6, T_RADIUS, 0, 0}, + {"right", 5, T_RIGHT, DIR_RIGHT, CP_E}, + {"rjust", 5, T_RJUST, 0, 0}, + {"s", 1, T_EDGEPT, 0, CP_S}, + {"same", 4, T_SAME, 0, 0}, + {"se", 2, T_EDGEPT, 0, CP_SE}, + {"sin", 3, T_FUNC1, FN_SIN, 0}, + {"small", 5, T_SMALL, 0, 0}, + {"solid", 5, T_SOLID, 0, 0}, + {"south", 5, T_EDGEPT, 0, CP_S}, + {"sqrt", 4, T_FUNC1, FN_SQRT, 0}, + {"start", 5, T_START, 0, CP_START}, + {"sw", 2, T_EDGEPT, 0, CP_SW}, + {"t", 1, T_TOP, 0, CP_N}, + {"the", 3, T_THE, 0, 0}, + {"then", 4, T_THEN, 0, 0}, + {"thick", 5, T_THICK, 0, 0}, + {"thickness", 9, T_THICKNESS, 0, 0}, + {"thin", 4, T_THIN, 0, 0}, + {"this", 4, T_THIS, 0, 0}, + {"to", 2, T_TO, 0, 0}, + {"top", 3, T_TOP, 0, CP_N}, + {"until", 5, T_UNTIL, 0, 0}, + {"up", 2, T_UP, DIR_UP, 0}, + {"vertex", 6, T_VERTEX, 0, 0}, + {"w", 1, T_EDGEPT, 0, CP_W}, + {"way", 3, T_WAY, 0, 0}, + {"west", 4, T_EDGEPT, 0, CP_W}, + {"wid", 3, T_WIDTH, 0, 0}, + {"width", 5, T_WIDTH, 0, 0}, + {"with", 4, T_WITH, 0, 0}, + {"x", 1, T_X, 0, 0}, + {"y", 1, T_Y, 0, 0}, +} + +/* +** Search a PikWordlist for the given keyword. Return a pointer to the +** keyword entry found. Or return 0 if not found. + */ +func pik_find_word( + zIn string, /* Word to search for */ + aList []PikWord, /* List to search */ +) *PikWord { + first := 0 + last := len(aList) - 1 + for first <= last { + mid := (first + last) / 2 + c := strings.Compare(zIn, aList[mid].zWord) + if c == 0 { + return &aList[mid] + } + if c < 0 { + last = mid - 1 + } else { + first = mid + 1 + } + } + return nil +} + +/* +** Set a symbolic debugger breakpoint on this routine to receive a +** breakpoint when the "#breakpoint" token is parsed. + */ +func pik_breakpoint(z []byte) { + /* Prevent C compilers from optimizing out this routine. */ + if z[2] == 'X' { + os.Exit(1) + } +} + +var aEntity = []struct { + eCode int /* Corresponding token code */ + zEntity string /* Name of the HTML entity */ +}{ + {T_RARROW, "→"}, /* Same as . */ + {T_RARROW, "→"}, /* Same as . */ + {T_LARROW, "←"}, /* Same as <- */ + {T_LARROW, "←"}, /* Same as <- */ + {T_LRARROW, "↔"}, /* Same as <. */ +} + +/* +** Return the length of next token. The token starts on +** the pToken->z character. Fill in other fields of the +** pToken object as appropriate. + */ +func pik_token_length(pToken *PToken, bAllowCodeBlock bool) int { + z := pToken.z + var i int + switch z[0] { + case '\\': + pToken.eType = T_WHITESPACE + for i = 1; z[i] == '\r' || z[i] == ' ' || z[i] == '\t'; i++ { + } + if z[i] == '\n' { + return i + 1 + } + pToken.eType = T_ERROR + return 1 + + case ';', '\n': + pToken.eType = T_EOL + return 1 + + case '"': + for i = 1; z[i] != 0; i++ { + c := z[i] + if c == '\\' { + if z[i+1] == 0 { + break + } + i++ + continue + } + if c == '"' { + pToken.eType = T_STRING + return i + 1 + } + } + pToken.eType = T_ERROR + return i + + case ' ', '\t', '\f', '\r': + for i = 1; z[i] == ' ' || z[i] == '\t' || z[i] == '\r' || z[i] == '\f'; i++ { + } + pToken.eType = T_WHITESPACE + return i + + case '#': + for i = 1; z[i] != 0 && z[i] != '\n'; i++ { + } + pToken.eType = T_WHITESPACE + /* If the comment is "#breakpoint" then invoke the pik_breakpoint() + ** routine. The pik_breakpoint() routie is a no-op that serves as + ** a convenient place to set a gdb breakpoint when debugging. */ + + if i >= 11 && string(z[:11]) == "#breakpoint" { + pik_breakpoint(z) + } + return i + + case '/': + if z[1] == '*' { + for i = 2; z[i] != 0 && (z[i] != '*' || z[i+1] != '/'); i++ { + } + if z[i] == '*' { + pToken.eType = T_WHITESPACE + return i + 2 + } else { + pToken.eType = T_ERROR + return i + } + } else if z[1] == '/' { + for i = 2; z[i] != 0 && z[i] != '\n'; i++ { + } + pToken.eType = T_WHITESPACE + return i + } else if z[1] == '=' { + pToken.eType = T_ASSIGN + pToken.eCode = T_SLASH + return 2 + } else { + pToken.eType = T_SLASH + return 1 + } + + case '+': + if z[1] == '=' { + pToken.eType = T_ASSIGN + pToken.eCode = T_PLUS + return 2 + } + pToken.eType = T_PLUS + return 1 + + case '*': + if z[1] == '=' { + pToken.eType = T_ASSIGN + pToken.eCode = T_STAR + return 2 + } + pToken.eType = T_STAR + return 1 + + case '%': + pToken.eType = T_PERCENT + return 1 + case '(': + pToken.eType = T_LP + return 1 + case ')': + pToken.eType = T_RP + return 1 + case '[': + pToken.eType = T_LB + return 1 + case ']': + pToken.eType = T_RB + return 1 + case ',': + pToken.eType = T_COMMA + return 1 + case ':': + pToken.eType = T_COLON + return 1 + case '>': + pToken.eType = T_GT + return 1 + case '=': + if z[1] == '=' { + pToken.eType = T_EQ + return 2 + } + pToken.eType = T_ASSIGN + pToken.eCode = T_ASSIGN + return 1 + + case '-': + if z[1] == '>' { + pToken.eType = T_RARROW + return 2 + } else if z[1] == '=' { + pToken.eType = T_ASSIGN + pToken.eCode = T_MINUS + return 2 + } else { + pToken.eType = T_MINUS + return 1 + } + + case '<': + if z[1] == '-' { + if z[2] == '>' { + pToken.eType = T_LRARROW + return 3 + } else { + pToken.eType = T_LARROW + return 2 + } + } else { + pToken.eType = T_LT + return 1 + } + + case 0xe2: + if z[1] == 0x86 { + if z[2] == 0x90 { + pToken.eType = T_LARROW /* <- */ + return 3 + } + if z[2] == 0x92 { + pToken.eType = T_RARROW /* . */ + return 3 + } + if z[2] == 0x94 { + pToken.eType = T_LRARROW /* <. */ + return 3 + } + } + pToken.eType = T_ERROR + return 1 + + case '{': + var depth int + i = 1 + if bAllowCodeBlock { + depth = 1 + for z[i] != 0 && depth > 0 { + var x PToken + x.z = z[i:] + len := pik_token_length(&x, false) + if len == 1 { + if z[i] == '{' { + depth++ + } + if z[i] == '}' { + depth-- + } + } + i += len + } + } else { + depth = 0 + } + if depth != 0 { + pToken.eType = T_ERROR + return 1 + } + pToken.eType = T_CODEBLOCK + return i + + case '&': + for i, ent := range aEntity { + if bytencmp(z, aEntity[i].zEntity, len(aEntity[i].zEntity)) == 0 { + pToken.eType = uint8(ent.eCode) + return len(aEntity[i].zEntity) + } + } + pToken.eType = T_ERROR + return 1 + + default: + c := z[0] + if c == '.' { + c1 := z[1] + if islower(c1) { + for i = 2; z[i] >= 'a' && z[i] <= 'z'; i++ { + } + pFound := pik_find_word(string(z[1:i]), pik_keywords) + if pFound != nil && (pFound.eEdge > 0 || + pFound.eType == T_EDGEPT || + pFound.eType == T_START || + pFound.eType == T_END) { + /* Dot followed by something that is a 2-D place value */ + pToken.eType = T_DOT_E + } else if pFound != nil && (pFound.eType == T_X || pFound.eType == T_Y) { + /* Dot followed by "x" or "y" */ + pToken.eType = T_DOT_XY + } else { + /* Any other "dot" */ + pToken.eType = T_DOT_L + } + return 1 + } else if isdigit(c1) { + i = 0 + /* no-op. Fall through to number handling */ + } else if isupper(c1) { + for i = 2; z[i] != 0 && (isalnum(z[i]) || z[i] == '_'); i++ { + } + pToken.eType = T_DOT_U + return 1 + } else { + pToken.eType = T_ERROR + return 1 + } + } + if (c >= '0' && c <= '9') || c == '.' { + var nDigit int + isInt := true + if c != '.' { + nDigit = 1 + for i = 1; ; i++ { + c = z[i] + if c < '0' || c > '9' { + break + } + nDigit++ + } + if i == 1 && (c == 'x' || c == 'X') { + for i = 2; z[i] != 0 && isxdigit(z[i]); i++ { + } + pToken.eType = T_NUMBER + return i + } + } else { + isInt = false + nDigit = 0 + i = 0 + } + if c == '.' { + isInt = false + for i++; ; i++ { + c = z[i] + if c < '0' || c > '9' { + break + } + nDigit++ + } + } + if nDigit == 0 { + pToken.eType = T_ERROR + return i + } + if c == 'e' || c == 'E' { + iBefore := i + i++ + c2 := z[i] + if c2 == '+' || c2 == '-' { + i++ + c2 = z[i] + } + if c2 < '0' || c > '9' { + /* This is not an exp */ + i = iBefore + } else { + i++ + isInt = false + for { + c = z[i] + if c < '0' || c > '9' { + break + } + i++ + } + } + } + var c2 byte + if c != 0 { + c2 = z[i+1] + } + if isInt { + if (c == 't' && c2 == 'h') || + (c == 'r' && c2 == 'd') || + (c == 'n' && c2 == 'd') || + (c == 's' && c2 == 't') { + pToken.eType = T_NTH + return i + 2 + } + } + if (c == 'i' && c2 == 'n') || + (c == 'c' && c2 == 'm') || + (c == 'm' && c2 == 'm') || + (c == 'p' && c2 == 't') || + (c == 'p' && c2 == 'x') || + (c == 'p' && c2 == 'c') { + i += 2 + } + pToken.eType = T_NUMBER + return i + } else if islower(c) { + for i = 1; z[i] != 0 && (isalnum(z[i]) || z[i] == '_'); i++ { + } + pFound := pik_find_word(string(z[:i]), pik_keywords) + if pFound != nil { + pToken.eType = pFound.eType + pToken.eCode = int16(pFound.eCode) + pToken.eEdge = pFound.eEdge + return i + } + pToken.n = i + if pik_find_class(pToken) != nil { + pToken.eType = T_CLASSNAME + } else { + pToken.eType = T_ID + } + return i + } else if c >= 'A' && c <= 'Z' { + for i = 1; z[i] != 0 && (isalnum(z[i]) || z[i] == '_'); i++ { + } + pToken.eType = T_PLACENAME + return i + } else if c == '$' && z[1] >= '1' && z[1] <= '9' && !isdigit(z[2]) { + pToken.eType = T_PARAMETER + pToken.eCode = int16(z[1] - '1') + return 2 + } else if c == '_' || c == '$' || c == '@' { + for i = 1; z[i] != 0 && (isalnum(z[i]) || z[i] == '_'); i++ { + } + pToken.eType = T_ID + return i + } else { + pToken.eType = T_ERROR + return 1 + } + } +} + +/* +** Return a pointer to the next non-whitespace token after pThis. +** This is used to help form error messages. + */ +func pik_next_semantic_token(pThis *PToken) PToken { + var x PToken + i := pThis.n + x.z = pThis.z + for { + x.z = pThis.z[i:] + sz := pik_token_length(&x, true) + if x.eType != T_WHITESPACE { + x.n = sz + return x + } + i += sz + } +} + +/* Parser arguments to a macro invocation +** +** (arg1, arg2, ...) +** +** Arguments are comma-separated, except that commas within string +** literals or with (...), {...}, or [...] do not count. The argument +** list begins and ends with parentheses. There can be at most 9 +** arguments. +** +** Return the number of bytes in the argument list. + */ +func (p *Pik) pik_parse_macro_args( + z []byte, /* Start of the argument list */ + n int, /* Available bytes */ + args []PToken, /* Fill in with the arguments */ + pOuter []PToken, /* Arguments of the next outer context, or NULL */ +) int { + nArg := 0 + var i, sz int + depth := 0 + var x PToken + if z[0] != '(' { + return 0 + } + args[0].z = z[1:] + iStart := 1 + for i = 1; i < n && z[i] != ')'; i += sz { + x.z = z[i:] + sz = pik_token_length(&x, false) + if sz != 1 { + continue + } + if z[i] == ',' && depth <= 0 { + args[nArg].n = i - iStart + if nArg == 8 { + x.z = z + x.n = 1 + p.pik_error(&x, "too many macro arguments - max 9") + return 0 + } + nArg++ + args[nArg].z = z[i+1:] + iStart = i + 1 + depth = 0 + } else if z[i] == '(' || z[i] == '{' || z[i] == '[' { + depth++ + } else if z[i] == ')' || z[i] == '}' || z[i] == ']' { + depth-- + } + } + if z[i] == ')' { + args[nArg].n = i - iStart + /* Remove leading and trailing whitespace from each argument. + ** If what remains is one of $1, $2, ... $9 then transfer the + ** corresponding argument from the outer context */ + for j := 0; j <= nArg; j++ { + t := &args[j] + for t.n > 0 && isspace(t.z[0]) { + t.n-- + t.z = t.z[1:] + } + for t.n > 0 && isspace(t.z[t.n-1]) { + t.n-- + } + if t.n == 2 && t.z[0] == '$' && t.z[1] >= '1' && t.z[1] <= '9' { + if pOuter != nil { + *t = pOuter[t.z[1]-'1'] + } else { + t.n = 0 + } + } + } + return i + 1 + } + x.z = z + x.n = 1 + p.pik_error(&x, "unterminated macro argument list") + return 0 +} + +/* +** Split up the content of a PToken into multiple tokens and +** send each to the parser. + */ +func (p *Pik) pik_tokenize(pIn *PToken, pParser *yyParser, aParam []PToken) { + sz := 0 + var token PToken + for i := 0; i < pIn.n && pIn.z[i] != 0 && p.nErr == 0; i += sz { + token.eCode = 0 + token.eEdge = 0 + token.z = pIn.z[i:] + sz = pik_token_length(&token, true) + if token.eType == T_WHITESPACE { + continue + /* no-op */ + } + if sz > 50000 { + token.n = 1 + p.pik_error(&token, "token is too long - max length 50000 bytes") + break + } + if token.eType == T_ERROR { + token.n = sz + p.pik_error(&token, "unrecognized token") + break + } + if sz+i > pIn.n { + token.n = pIn.n - i + p.pik_error(&token, "syntax error") + break + } + if token.eType == T_PARAMETER { + /* Substitute a parameter into the input stream */ + if aParam == nil || aParam[token.eCode].n == 0 { + continue + } + token.n = sz + if p.nCtx >= len(p.aCtx) { + p.pik_error(&token, "macros nested too deep") + } else { + p.aCtx[p.nCtx] = token + p.nCtx++ + p.pik_tokenize(&aParam[token.eCode], pParser, nil) + p.nCtx-- + } + continue + } + + if token.eType == T_ID { + token.n = sz + pMac := p.pik_find_macro(&token) + if pMac != nil { + args := make([]PToken, 9) + j := i + sz + if pMac.inUse { + p.pik_error(&pMac.macroName, "recursive macro definition") + break + } + token.n = sz + if p.nCtx >= len(p.aCtx) { + p.pik_error(&token, "macros nested too deep") + break + } + pMac.inUse = true + p.aCtx[p.nCtx] = token + p.nCtx++ + sz += p.pik_parse_macro_args(pIn.z[j:], pIn.n-j, args, aParam) + p.pik_tokenize(&pMac.macroBody, pParser, args) + p.nCtx-- + pMac.inUse = false + continue + } + } + if false { // #if 0 + n := sz + if isspace(token.z[0]) { + n = 0 + } + + fmt.Printf("******** Token %s (%d): \"%s\" **************\n", + yyTokenName[token.eType], token.eType, string(token.z[:n])) + } // #endif + token.n = sz + pParser.pik_parser(token.eType, token) + } +} + +/* +** Parse the PIKCHR script contained in zText[]. Return a rendering. Or +** if an error is encountered, return the error text. The error message +** is HTML formatted. So regardless of what happens, the return text +** is safe to be insertd into an HTML output stream. +** +** If pnWidth and pnHeight are not NULL, then this routine writes the +** width and height of the object into the integers that they +** point to. A value of -1 is written if an error is seen. +** +** If zClass is not NULL, then it is a class name to be included in +** the markup. +** +** The returned string is contained in memory obtained from malloc() +** and should be released by the caller. + */ +func Pikchr( + zText []byte, /* Input PIKCHR source text. zero-terminated */ + zClass string, /* Add class="%s" to markup */ + mFlags uint, /* Flags used to influence rendering behavior */ + svgWidth, svgHeight string, + svgFontScale PNum, + pnWidth *int, /* Write width of here, if not NULL */ + pnHeight *int, /* Write height here, if not NULL */ +) []byte { + s := Pik{} + var sParse yyParser + + s.sIn.n = len(zText) + s.sIn.z = append(zText, 0) + s.eDir = DIR_RIGHT + s.zClass = zClass + s.mFlags = mFlags + s.svgWidth = svgWidth + s.svgHeight = svgHeight + s.svgFontScale = svgFontScale + sParse.pik_parserInit(&s) + if false { // #if 0 + pik_parserTrace(os.Stdout, "parser: ") + } // #endif + s.pik_tokenize(&s.sIn, &sParse, nil) + if s.nErr == 0 { + var token PToken + if s.sIn.n > 0 { + token.z = zText[s.sIn.n-1:] + } else { + token.z = zText + } + token.n = 1 + sParse.pik_parser(0, token) + } + sParse.pik_parserFinalize() + if s.zOut.Len() == 0 && s.nErr == 0 { + s.pik_append("\n") + } + if pnWidth != nil { + if s.nErr != 0 { + *pnWidth = -1 + } else { + *pnWidth = s.wSVG + } + } + if pnHeight != nil { + if s.nErr != 0 { + *pnHeight = -1 + } else { + *pnHeight = s.hSVG + } + } + return s.zOut.Bytes() +} + +// #if defined(PIKCHR_FUZZ) +// #include +// int LLVMFuzzerTestOneInput(const uint8_t *aData, size_t nByte){ +// int w,h; +// char *zIn, *zOut; +// unsigned int mFlags = nByte & 3; +// zIn = malloc( nByte + 1 ); +// if( zIn==0 ) return 0; +// memcpy(zIn, aData, nByte); +// zIn[nByte] = 0; +// zOut = pikchr(zIn, "pikchr", mFlags, &w, &h); +// free(zIn); +// free(zOut); +// return 0; +// } +// #endif /* PIKCHR_FUZZ */ + +// Helpers added for port to Go +func isxdigit(b byte) bool { + return (b >= '0' && b <= '9') || (b >= 'a' && b <= 'f') || (b >= 'A' && b <= 'F') +} + +func isalnum(b byte) bool { + return (b >= '0' && b <= '9') || (b >= 'a' && b <= 'z') || (b >= 'A' && b <= 'Z') +} + +func isdigit(b byte) bool { + return (b >= '0' && b <= '9') +} + +func isspace(b byte) bool { + return b == ' ' || b == '\n' || b == '\t' || b == '\f' +} + +func isupper(b byte) bool { + return (b >= 'A' && b <= 'Z') +} + +func islower(b byte) bool { + return (b >= 'a' && b <= 'z') +} + +func bytencmp(a []byte, s string, n int) int { + return strings.Compare(string(a[:n]), s) +} + +func bytesEq(a, b []byte) bool { + if len(a) != len(b) { + return false + } + for i, bb := range a { + if b[i] != bb { + return false + } + } + return true +} + +} // end %code ADDED parser/pikchr/pikchr.go Index: parser/pikchr/pikchr.go ================================================================== --- parser/pikchr/pikchr.go +++ parser/pikchr/pikchr.go @@ -0,0 +1,70 @@ +//----------------------------------------------------------------------------- +// Copyright (c) 2022 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 pikchr provides a parser to create SVG from a textual PIC-like description. +package pikchr + +import ( + "strconv" + + "zettelstore.de/c/api" + "zettelstore.de/z/ast" + "zettelstore.de/z/domain/meta" + "zettelstore.de/z/input" + "zettelstore.de/z/parser" + "zettelstore.de/z/parser/pikchr/internal" +) + +func init() { + parser.Register(&parser.Info{ + Name: "pikchr", + AltNames: nil, + IsTextParser: true, + IsImageFormat: false, + ParseBlocks: parseBlocks, + ParseInlines: parseInlines, + }) +} + +func parseBlocks(inp *input.Input, m *meta.Meta, _ string) ast.BlockSlice { + var w, h int + bsSVG := internal.Pikchr( + inp.Src[inp.Pos:], "", 0, + m.GetDefault("width", ""), m.GetDefault("height", ""), getScale(m, "font-scale"), + &w, &h) + if w == -1 { + return ast.BlockSlice{ + &ast.ParaNode{ + Inlines: ast.CreateInlineSliceFromWords("Pikchr", "error:"), + }, + &ast.VerbatimNode{ + Kind: ast.VerbatimHTML, + Content: bsSVG, + }, + } + } + return ast.BlockSlice{&ast.BLOBNode{ + Title: "", + Syntax: api.ValueSyntaxSVG, + Blob: bsSVG, + }} +} + +func parseInlines(_ *input.Input, syntax string) ast.InlineSlice { + return ast.CreateInlineSliceFromWords("No", "inline", "code", "allowed", "for", "syntax:", syntax) +} +func getScale(m *meta.Meta, key string) internal.PNum { + if val, found := m.Get(key); found { + if scale, err := strconv.ParseFloat(val, 64); err == nil && scale > 0.001 && scale < 1000.0 { + return internal.PNum(scale) + } + } + return 1.0 +} Index: parser/zettelmark/block.go ================================================================== --- parser/zettelmark/block.go +++ parser/zettelmark/block.go @@ -641,11 +641,11 @@ for { switch inp.Ch { case input.EOS: return nil, false case '\n', '\r', ' ', '\t': - if !hasSearchPrefix(inp.Src[posA:]) { + if !hasQueryPrefix(inp.Src[posA:]) { return nil, false } case '\\': inp.Next() switch inp.Ch { @@ -667,10 +667,12 @@ } break loop } inp.Next() } + inp.Next() // consume last '}' + a := cp.parseBlockAttributes() inp.SkipToEOL() refText := string(inp.Src[posA:posE]) ref := ast.ParseReference(refText) - return &ast.TranscludeNode{Ref: ref}, true + return &ast.TranscludeNode{Attrs: a, Ref: ref}, true } Index: parser/zettelmark/inline.go ================================================================== --- parser/zettelmark/inline.go +++ parser/zettelmark/inline.go @@ -13,10 +13,12 @@ import ( "bytes" "fmt" "strings" + "zettelstore.de/c/api" + "zettelstore.de/c/attrs" "zettelstore.de/z/ast" "zettelstore.de/z/input" ) // parseInlineSlice parses a sequence of Inlines until EOS. @@ -164,42 +166,44 @@ } } return nil, false } -func hasSearchPrefix(src []byte) bool { - return len(src) > len(ast.SearchPrefix) && string(src[:len(ast.SearchPrefix)]) == ast.SearchPrefix +func hasQueryPrefix(src []byte) bool { + return len(src) > len(ast.QueryPrefix) && string(src[:len(ast.QueryPrefix)]) == ast.QueryPrefix } -func (cp *zmkP) parseReference(closeCh rune) (ref string, is ast.InlineSlice, ok bool) { +func (cp *zmkP) parseReference(closeCh rune) (ref string, is ast.InlineSlice, _ bool) { inp := cp.inp inp.Next() cp.skipSpace() pos := inp.Pos - hasSpace, ok := cp.readReferenceToSep(closeCh) - if !ok { - return "", nil, false - } - if inp.Ch == '|' { // First part must be inline text - if pos == inp.Pos { // [[| or {{| - return "", nil, false - } - cp.inp = input.NewInput(inp.Src[pos:inp.Pos]) - for { - in := cp.parseInline() - if in == nil { - break - } - is = append(is, in) - } - cp.inp = inp - inp.Next() - } else { - if hasSpace && !hasSearchPrefix(inp.Src[pos:]) { - return "", nil, false - } - inp.SetPos(pos) + if !hasQueryPrefix(inp.Src[pos:]) { + hasSpace, ok := cp.readReferenceToSep(closeCh) + if !ok { + return "", nil, false + } + if inp.Ch == '|' { // First part must be inline text + if pos == inp.Pos { // [[| or {{| + return "", nil, false + } + cp.inp = input.NewInput(inp.Src[pos:inp.Pos]) + for { + in := cp.parseInline() + if in == nil { + break + } + is = append(is, in) + } + cp.inp = inp + inp.Next() + } else { + if hasSpace { + return "", nil, false + } + inp.SetPos(pos) + } } cp.skipSpace() pos = inp.Pos if !cp.readReferenceToClose(closeCh) { @@ -259,11 +263,11 @@ for { switch inp.Ch { case input.EOS: return false case '\t', '\r', '\n', ' ': - if !hasSearchPrefix(inp.Src[pos:]) { + if !hasQueryPrefix(inp.Src[pos:]) { return false } case '\\': inp.Next() switch inp.Ch { @@ -481,11 +485,10 @@ } inp.Next() // read 2nd formatting character if inp.Ch != fch { return nil, false } - litn := &ast.LiteralNode{Kind: kind} inp.Next() var buf bytes.Buffer for { if inp.Ch == input.EOS { return nil, false @@ -492,22 +495,34 @@ } if inp.Ch == fch { if inp.Peek() == fch { inp.Next() inp.Next() - litn.Attrs = cp.parseInlineAttributes() - litn.Content = buf.Bytes() - return litn, true + return createLiteralNode(kind, cp.parseInlineAttributes(), buf.Bytes()), true } buf.WriteRune(fch) inp.Next() } else { tn := cp.parseText() buf.WriteString(tn.Text) } } } + +func createLiteralNode(kind ast.LiteralKind, a attrs.Attributes, content []byte) *ast.LiteralNode { + if kind == ast.LiteralZettel { + if val, found := a.Get(""); found && val == api.ValueSyntaxHTML { + kind = ast.LiteralHTML + a = a.Remove("") + } + } + return &ast.LiteralNode{ + Kind: kind, + Attrs: a, + Content: content, + } +} func (cp *zmkP) parseLiteralMath() (res ast.InlineNode, success bool) { inp := cp.inp inp.Next() // read 2nd formatting character if inp.Ch != '$' { Index: parser/zettelmark/zettelmark_test.go ================================================================== --- parser/zettelmark/zettelmark_test.go +++ parser/zettelmark/zettelmark_test.go @@ -162,14 +162,16 @@ {"[[b\\]|a]]", "(PARA (LINK a b]))"}, {"[[\\]\\||a]]", "(PARA (LINK a ]|))"}, {"[[http://a]]", "(PARA (LINK http://a))"}, {"[[http://a|http://a]]", "(PARA (LINK http://a http://a))"}, {"[[[[a]]]]", "(PARA (LINK [[a) ]])"}, - {"[[search:title]]", "(PARA (LINK search:title))"}, - {"[[search:title syntax]]", "(PARA (LINK search:title syntax))"}, - {"[[Text|search:title]]", "(PARA (LINK search:title Text))"}, - {"[[Text|search:title syntax]]", "(PARA (LINK search:title syntax Text))"}, + {"[[query:title]]", "(PARA (LINK query:title))"}, + {"[[query:title syntax]]", "(PARA (LINK query:title syntax))"}, + {"[[query:title | action]]", "(PARA (LINK query:title | action))"}, + {"[[Text|query:title]]", "(PARA (LINK query:title Text))"}, + {"[[Text|query:title syntax]]", "(PARA (LINK query:title syntax Text))"}, + {"[[Text|query:title | action]]", "(PARA (LINK query:title | action Text))"}, }) } func TestCite(t *testing.T) { t.Parallel() @@ -677,19 +679,20 @@ {"|a|b\n|%---\n|c|d", "(TAB (TR (TD a)(TD b))(TR (TD c)(TD d)))"}, {"|a|b\n|c", "(TAB (TR (TD a)(TD b))(TR (TD c)(TD)))"}, }) } -func TestBlockEmbed(t *testing.T) { +func TestTransclude(t *testing.T) { t.Parallel() checkTcs(t, TestCases{ {"{{{a}}}", "(TRANSCLUDE a)"}, - {"{{{a}}}b", "(TRANSCLUDE a)"}, + {"{{{a}}}b", "(TRANSCLUDE a)[ATTR =b]"}, {"{{{a}}}}", "(TRANSCLUDE a)"}, {"{{{a\\}}}}", "(TRANSCLUDE a%5C%7D)"}, - {"{{{a\\}}}}b", "(TRANSCLUDE a%5C%7D)"}, + {"{{{a\\}}}}b", "(TRANSCLUDE a%5C%7D)[ATTR =b]"}, {"{{{a}}", "(PARA (EMBED %7Ba))"}, + {"{{{a}}}{go=b}", "(TRANSCLUDE a)[ATTR go=b]"}, }) } func TestBlockAttr(t *testing.T) { t.Parallel() @@ -875,10 +878,11 @@ } } tv.buf.WriteString(")") case *ast.TranscludeNode: fmt.Fprintf(&tv.buf, "(TRANSCLUDE %v)", n.Ref) + tv.visitAttributes(n.Attrs) case *ast.BLOBNode: tv.buf.WriteString("(BLOB ") tv.buf.WriteString(n.Syntax) tv.buf.WriteString(")") case *ast.TextNode: ADDED query/parser.go Index: query/parser.go ================================================================== --- query/parser.go +++ query/parser.go @@ -0,0 +1,345 @@ +//----------------------------------------------------------------------------- +// Copyright (c) 2022 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 query + +import ( + "strconv" + + "zettelstore.de/z/domain/meta" + "zettelstore.de/z/input" +) + +// Parse the query specification and return a Query object. +func Parse(spec string) (q *Query) { return q.Parse(spec) } + +// Parse the query string and update the Query object. +func (q *Query) Parse(spec string) *Query { + state := parserState{ + inp: input.NewInput([]byte(spec)), + } + q = state.parse(q) + if q != nil { + for len(q.terms) > 1 && q.terms[len(q.terms)-1].isEmpty() { + q.terms = q.terms[:len(q.terms)-1] + } + } + return q +} + +type parserState struct { + inp *input.Input +} + +func (ps *parserState) mustStop() bool { return ps.inp.Ch == input.EOS } +func (ps *parserState) acceptSingleKw(s string) bool { + if ps.inp.Accept(s) && (ps.isSpace() || ps.mustStop()) { + return true + } + return false +} +func (ps *parserState) acceptKwArgs(s string) bool { + if ps.inp.Accept(s) && ps.isSpace() { + ps.skipSpace() + return true + } + return false +} + +const ( + actionSeparatorChar = '|' + existOperatorChar = '?' + searchOperatorNotChar = '!' + searchOperatorHasChar = ':' + searchOperatorPrefixChar = '>' + searchOperatorSuffixChar = '<' + searchOperatorMatchChar = '~' + + kwLimit = "LIMIT" + kwOffset = "OFFSET" + kwOr = "OR" + kwOrder = "ORDER" + kwRandom = "RANDOM" + kwReverse = "REVERSE" +) + +func (ps *parserState) parse(q *Query) *Query { + inp := ps.inp + for { + ps.skipSpace() + if ps.mustStop() { + break + } + pos := inp.Pos + if ps.acceptSingleKw(kwOr) { + q = createIfNeeded(q) + if !q.terms[len(q.terms)-1].isEmpty() { + q.terms = append(q.terms, conjTerms{}) + } + continue + } + inp.SetPos(pos) + if ps.acceptSingleKw(kwRandom) { + q = createIfNeeded(q) + if len(q.order) == 0 { + q.order = []sortOrder{{"", false}} + } + continue + } + inp.SetPos(pos) + if ps.acceptKwArgs(kwOrder) { + if s, ok := ps.parseOrder(q); ok { + q = s + continue + } + } + inp.SetPos(pos) + if ps.acceptKwArgs(kwOffset) { + if s, ok := ps.parseOffset(q); ok { + q = s + continue + } + } + inp.SetPos(pos) + if ps.acceptKwArgs(kwLimit) { + if s, ok := ps.parseLimit(q); ok { + q = s + continue + } + } + inp.SetPos(pos) + if isActionSep(inp.Ch) { + q = ps.parseActions(q) + break + } + q = ps.parseText(q) + } + return q +} + +func (ps *parserState) parseOrder(q *Query) (*Query, bool) { + reverse := false + if ps.acceptKwArgs(kwReverse) { + reverse = true + } + word := ps.scanWord() + if len(word) == 0 { + return q, false + } + if sWord := string(word); meta.KeyIsValid(sWord) { + q = createIfNeeded(q) + if len(q.order) == 1 && q.order[0].isRandom() { + q.order = nil + } + q.order = append(q.order, sortOrder{sWord, reverse}) + return q, true + } + return q, false +} + +func (ps *parserState) parseOffset(q *Query) (*Query, bool) { + num, ok := ps.scanPosInt() + if !ok { + return q, false + } + q = createIfNeeded(q) + if q.offset <= num { + q.offset = num + } + return q, true +} + +func (ps *parserState) parseLimit(q *Query) (*Query, bool) { + num, ok := ps.scanPosInt() + if !ok { + return q, false + } + q = createIfNeeded(q) + if q.limit == 0 || q.limit >= num { + q.limit = num + } + return q, true +} + +func (ps *parserState) parseActions(q *Query) *Query { + ps.inp.Next() + var words []string + for { + ps.skipSpace() + word := ps.scanWord() + if len(word) == 0 { + break + } + words = append(words, string(word)) + } + if len(words) > 0 { + q = createIfNeeded(q) + q.actions = words + } + return q +} + +func (ps *parserState) parseText(q *Query) *Query { + inp := ps.inp + pos := inp.Pos + op, hasOp := ps.scanSearchOp() + if hasOp && (op == cmpExist || op == cmpNotExist) { + inp.SetPos(pos) + hasOp = false + } + text, key := ps.scanSearchTextOrKey(hasOp) + if len(key) > 0 { + // Assert: hasOp == false + op, hasOp = ps.scanSearchOp() + // Assert hasOp == true + if op == cmpExist || op == cmpNotExist { + if ps.isSpace() || isActionSep(inp.Ch) || ps.mustStop() { + return q.addKey(string(key), op) + } + ps.inp.SetPos(pos) + hasOp = false + text = ps.scanWord() + key = nil + } else { + text = ps.scanWord() + } + } else if len(text) == 0 { + // Only an empty search operation is found -> ignore it + return q + } + q = createIfNeeded(q) + if hasOp { + if key == nil { + q.addSearch(expValue{string(text), op}) + } else { + last := len(q.terms) - 1 + if q.terms[last].mvals == nil { + q.terms[last].mvals = expMetaValues{string(key): {expValue{string(text), op}}} + } else { + sKey := string(key) + q.terms[last].mvals[sKey] = append(q.terms[last].mvals[sKey], expValue{string(text), op}) + } + } + } else { + // Assert key == nil + q.addSearch(expValue{string(text), cmpMatch}) + } + return q +} + +func (ps *parserState) scanSearchTextOrKey(hasOp bool) ([]byte, []byte) { + inp := ps.inp + pos := inp.Pos + allowKey := !hasOp + + for !ps.isSpace() && !isActionSep(inp.Ch) && !ps.mustStop() { + if allowKey { + switch inp.Ch { + case searchOperatorNotChar, existOperatorChar, searchOperatorHasChar, + searchOperatorPrefixChar, searchOperatorSuffixChar, searchOperatorMatchChar: + allowKey = false + if key := inp.Src[pos:inp.Pos]; meta.KeyIsValid(string(key)) { + return nil, key + } + } + } + inp.Next() + } + return inp.Src[pos:inp.Pos], nil +} + +func (ps *parserState) scanWord() []byte { + inp := ps.inp + pos := inp.Pos + for !ps.isSpace() && !isActionSep(inp.Ch) && !ps.mustStop() { + inp.Next() + } + return inp.Src[pos:inp.Pos] +} + +func (ps *parserState) scanPosInt() (int, bool) { + inp := ps.inp + ch := inp.Ch + if ch == '0' { + ch = inp.Next() + if isSpace(ch) || isActionSep(inp.Ch) || ps.mustStop() { + return 0, true + } + return 0, false + } + word := ps.scanWord() + if len(word) == 0 { + return 0, false + } + uval, err := strconv.ParseUint(string(word), 10, 63) + if err != nil { + return 0, false + } + return int(uval), true +} + +func (ps *parserState) scanSearchOp() (compareOp, bool) { + inp := ps.inp + ch := inp.Ch + negate := false + if ch == searchOperatorNotChar { + ch = inp.Next() + negate = true + } + op := cmpUnknown + switch ch { + case existOperatorChar: + inp.Next() + op = cmpExist + case searchOperatorHasChar: + inp.Next() + op = cmpHas + case searchOperatorSuffixChar: + inp.Next() + op = cmpSuffix + case searchOperatorPrefixChar: + inp.Next() + op = cmpPrefix + case searchOperatorMatchChar: + inp.Next() + op = cmpMatch + default: + if negate { + return cmpNoMatch, true + } + return cmpUnknown, false + } + if negate { + return op.negate(), true + } + return op, true +} + +func (ps *parserState) isSpace() bool { + return isSpace(ps.inp.Ch) +} + +func isSpace(ch rune) bool { + switch ch { + case input.EOS: + return false + case ' ', '\t', '\n', '\r': + return true + } + return input.IsSpace(ch) +} + +func (ps *parserState) skipSpace() { + for ps.isSpace() { + ps.inp.Next() + } +} + +func isActionSep(ch rune) bool { return ch == actionSeparatorChar } ADDED query/parser_test.go Index: query/parser_test.go ================================================================== --- query/parser_test.go +++ query/parser_test.go @@ -0,0 +1,80 @@ +//----------------------------------------------------------------------------- +// Copyright (c) 2022 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 query_test + +import ( + "testing" + + "zettelstore.de/z/query" +) + +func TestParser(t *testing.T) { + t.Parallel() + testcases := []struct { + spec string + exp string + }{ + {"?", "?"}, {"!?", "!?"}, {"?a", "?a"}, {"!?a", "!?a"}, + {"key?", "key?"}, {"key!?", "key!?"}, + {"b key?", "key? b"}, {"b key!?", "key!? b"}, + {"key?a", "key?a"}, {"key!?a", "key!?a"}, + {"", ""}, {"!", ""}, {":", ""}, {"!:", ""}, {">", ""}, {"!>", ""}, {"<", ""}, {"!<", ""}, {"~", ""}, {"!~", ""}, + {`a`, `a`}, {`!a`, `!a`}, + {`:a`, `:a`}, {`!:a`, `!:a`}, + {`>a`, `>a`}, {`!>a`, `!>a`}, + {``, `key>`}, {`key!>`, `key!>`}, + {`key<`, `key<`}, {`key!<`, `key!<`}, + {`key~`, `key~`}, {`key!~`, `key!~`}, + {`key:a`, `key:a`}, {`key!:a`, `key!:a`}, + {`key>a`, `key>a`}, {`key!>a`, `key!>a`}, + {`key 0 { + env.writeString(" OR") + } + for _, name := range maps.Keys(term.keys) { + env.printSpace() + env.writeString(name) + if op := term.keys[name]; op == cmpExist || op == cmpNotExist { + env.writeString(op2string[op]) + } else { + env.writeString(api.ExistOperator) + env.printSpace() + env.writeString(name) + env.writeString(api.ExistNotOperator) + } + } + for _, name := range maps.Keys(term.mvals) { + env.printExprValues(name, term.mvals[name]) + } + if len(term.search) > 0 { + env.printExprValues("", term.search) + } + } + env.printOrder(q.order) + env.printPosInt(kwOffset, q.offset) + env.printPosInt(kwLimit, q.limit) + env.printActions(q.actions) +} + +type printEnv struct { + w io.Writer + space bool +} + +var bsSpace = []byte{' '} + +func (pe *printEnv) printSpace() { + if pe.space { + pe.w.Write(bsSpace) + return + } + pe.space = true +} +func (pe *printEnv) write(ch byte) { pe.w.Write([]byte{ch}) } +func (pe *printEnv) writeString(s string) { io.WriteString(pe.w, s) } + +func (pe *printEnv) printExprValues(key string, values []expValue) { + for _, val := range values { + pe.printSpace() + pe.writeString(key) + switch op := val.op; op { + case cmpMatch: + // An empty key signals a full-text search. Since "~" is the default op in this case, + // it can be ignored. Therefore, print only "~" if there is a key. + if key != "" { + pe.writeString(api.SearchOperatorMatch) + } + case cmpNoMatch: + // An empty key signals a full-text search. Since "!" is the shortcut for "!~", + // it can be ignored. Therefore, print only "!~" if there is a key. + if key == "" { + pe.writeString(api.SearchOperatorNot) + } else { + pe.writeString(api.SearchOperatorNoMatch) + } + default: + if s, found := op2string[op]; found { + pe.writeString(s) + } else { + pe.writeString("%" + strconv.Itoa(int(op))) + } + } + if s := val.value; s != "" { + pe.writeString(s) + } + } +} + +func (q *Query) Human() string { + var sb strings.Builder + q.PrintHuman(&sb) + return sb.String() +} + +// PrintHuman the query to a writer in a human readable form. +func (q *Query) PrintHuman(w io.Writer) { + if q == nil { + return + } + env := printEnv{w: w} + for i, term := range q.terms { + if i > 0 { + env.writeString(" OR ") + env.space = false + } + for _, name := range maps.Keys(term.keys) { + if env.space { + env.writeString(" AND ") + } + env.writeString(name) + switch term.keys[name] { + case cmpExist: + env.writeString(" EXIST") + case cmpNotExist: + env.writeString(" NOT EXIST") + default: + env.writeString(" IS SCHRÖDINGER'S CAT") + } + env.space = true + } + for _, name := range maps.Keys(term.mvals) { + if env.space { + env.writeString(" AND ") + } + env.writeString(name) + env.printHumanSelectExprValues(term.mvals[name]) + env.space = true + } + if len(term.search) > 0 { + if env.space { + env.writeString(" ") + } + env.writeString("ANY") + env.printHumanSelectExprValues(term.search) + env.space = true + } + } + + env.printOrder(q.order) + env.printPosInt(kwOffset, q.offset) + env.printPosInt(kwLimit, q.limit) + env.printActions(q.actions) +} + +func (pe *printEnv) printHumanSelectExprValues(values []expValue) { + if len(values) == 0 { + pe.writeString(" MATCH ANY") + return + } + + for j, val := range values { + if j > 0 { + pe.writeString(" AND") + } + switch val.op { + case cmpHas: + pe.writeString(" HAS ") + case cmpHasNot: + pe.writeString(" HAS NOT ") + case cmpPrefix: + pe.writeString(" PREFIX ") + case cmpNoPrefix: + pe.writeString(" NOT PREFIX ") + case cmpSuffix: + pe.writeString(" SUFFIX ") + case cmpNoSuffix: + pe.writeString(" NOT SUFFIX ") + case cmpMatch: + pe.writeString(" MATCH ") + case cmpNoMatch: + pe.writeString(" NOT MATCH ") + default: + pe.writeString(" MaTcH ") + } + if val.value == "" { + pe.writeString("NOTHING") + } else { + pe.writeString(val.value) + } + } +} + +func (pe *printEnv) printOrder(order []sortOrder) { + for _, o := range order { + if o.isRandom() { + pe.printSpace() + pe.writeString(kwRandom) + continue + } else if o.key == api.KeyID && o.descending { + continue + } + pe.printSpace() + pe.writeString(kwOrder) + if o.descending { + pe.printSpace() + pe.writeString(kwReverse) + } + pe.printSpace() + pe.writeString(o.key) + } +} + +func (pe *printEnv) printPosInt(key string, val int) { + if val > 0 { + pe.printSpace() + pe.writeString(key) + pe.writeString(" ") + pe.writeString(strconv.Itoa(val)) + } +} + +func (pe *printEnv) printActions(words []string) { + if len(words) > 0 { + pe.printSpace() + pe.write(actionSeparatorChar) + for _, word := range words { + pe.printSpace() + pe.writeString(word) + } + } +} ADDED query/query.go Index: query/query.go ================================================================== --- query/query.go +++ query/query.go @@ -0,0 +1,399 @@ +//----------------------------------------------------------------------------- +// Copyright (c) 2020-2022 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 query provides a query for zettel. +package query + +import ( + "math/rand" + "sort" + + "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. + // The word must be normalized through Unicode NKFD, trimmed and not empty. + SearchEqual(word string) id.Set + + // Select all zettel that have a word with the given prefix. + // The prefix must be normalized through Unicode NKFD, trimmed and not empty. + SearchPrefix(prefix string) id.Set + + // Select all zettel that have a word with the given suffix. + // The suffix must be normalized through Unicode NKFD, trimmed and not empty. + SearchSuffix(suffix string) id.Set + + // Select all zettel that contains the given string. + // The string must be normalized through Unicode NKFD, trimmed and not empty. + SearchContains(s string) id.Set +} + +// Query specifies a mechanism for querying zettel. +type Query struct { + // Fields to be used for selecting + preMatch MetaMatchFunc // Match that must be true + terms []conjTerms + + // Fields to be used for sorting + order []sortOrder + offset int // <= 0: no offset + limit int // <= 0: no limit + + // Execute specification + actions []string +} + +// Compiled is a compiled query, to be used in a Box +type Compiled struct { + PreMatch MetaMatchFunc // Precondition for Match and Retrieve + Terms []CompiledTerm +} + +// MetaMatchFunc is a function determine whethe some metadata should be selected or not. +type MetaMatchFunc func(*meta.Meta) bool + +func matchAlways(*meta.Meta) bool { return true } +func matchNever(*meta.Meta) bool { return false } + +// CompiledTerm is the preprocessed sequence of conjugated search terms. +type CompiledTerm struct { + Match MetaMatchFunc // Match on metadata + Retrieve RetrievePredicate // Retrieve from full-text search +} + +// RetrievePredicate returns true, if the given Zid is contained in the (full-text) search. +type RetrievePredicate func(id.Zid) bool + +type keyExistMap map[string]compareOp +type expMetaValues map[string][]expValue + +type conjTerms struct { + keys keyExistMap + mvals expMetaValues // Expected values for a meta datum + search []expValue // Search string +} + +func (ct *conjTerms) isEmpty() bool { + return len(ct.keys) == 0 && len(ct.mvals) == 0 && len(ct.search) == 0 +} +func (ct *conjTerms) addKey(key string, op compareOp) { + if ct.keys == nil { + ct.keys = map[string]compareOp{key: op} + return + } + if prevOp, found := ct.keys[key]; found { + if prevOp != op { + ct.keys[key] = cmpUnknown + } + return + } + ct.keys[key] = op +} +func (ct *conjTerms) addSearch(val expValue) { ct.search = append(ct.search, val) } + +type sortOrder struct { + key string + descending bool +} + +func (so *sortOrder) isRandom() bool { return so.key == "" } + +func createIfNeeded(q *Query) *Query { + if q == nil { + return &Query{ + terms: []conjTerms{{}}, + } + } + return q +} + +// Clone the query value. +func (q *Query) Clone() *Query { + if q == nil { + return nil + } + c := new(Query) + c.preMatch = q.preMatch + c.terms = make([]conjTerms, len(q.terms)) + for i, term := range q.terms { + if len(term.keys) > 0 { + c.terms[i].keys = make(keyExistMap, len(term.keys)) + for k, v := range term.keys { + c.terms[i].keys[k] = v + } + } + // if len(c.mvals) > 0 { + c.terms[i].mvals = make(expMetaValues, len(term.mvals)) + for k, v := range term.mvals { + c.terms[i].mvals[k] = v + } + // } + if len(term.search) > 0 { + c.terms[i].search = append([]expValue{}, term.search...) + } + } + if len(q.order) > 0 { + c.order = append([]sortOrder{}, q.order...) + } + c.offset = q.offset + c.limit = q.limit + c.actions = q.actions + return c +} + +type compareOp uint8 + +const ( + cmpUnknown compareOp = iota + cmpExist + cmpNotExist + cmpHas + cmpHasNot + cmpPrefix + cmpNoPrefix + cmpSuffix + cmpNoSuffix + cmpMatch + cmpNoMatch +) + +var negateMap = map[compareOp]compareOp{ + cmpUnknown: cmpUnknown, + cmpExist: cmpNotExist, + cmpHas: cmpHasNot, + cmpHasNot: cmpHas, + cmpPrefix: cmpNoPrefix, + cmpNoPrefix: cmpPrefix, + cmpSuffix: cmpNoSuffix, + cmpNoSuffix: cmpSuffix, + cmpMatch: cmpNoMatch, + cmpNoMatch: cmpMatch, +} + +func (op compareOp) negate() compareOp { return negateMap[op] } + +var negativeMap = map[compareOp]bool{ + cmpNotExist: true, + cmpHasNot: true, + cmpNoPrefix: true, + cmpNoSuffix: true, + cmpNoMatch: true, +} + +func (op compareOp) isNegated() bool { return negativeMap[op] } + +type expValue struct { + value string + op compareOp +} + +func (q *Query) addSearch(val expValue) { q.terms[len(q.terms)-1].addSearch(val) } + +func (q *Query) addKey(key string, op compareOp) *Query { + q = createIfNeeded(q) + q.terms[len(q.terms)-1].addKey(key, op) + return q +} + +// SetPreMatch sets the pre-selection predicate. +func (q *Query) SetPreMatch(preMatch MetaMatchFunc) *Query { + q = createIfNeeded(q) + if q.preMatch != nil { + panic("search PreMatch already set") + } + q.preMatch = preMatch + return q +} + +// SetLimit sets the given limit of the query object. +func (q *Query) SetLimit(limit int) *Query { + q = createIfNeeded(q) + if limit < 0 { + limit = 0 + } + q.limit = limit + return q +} + +// GetLimit returns the current offset value. +func (q *Query) GetLimit() int { + if q == nil { + return 0 + } + return q.limit +} + +// Actions returns the slice of action specifications +func (q *Query) Actions() []string { + if q == nil { + return nil + } + return q.actions +} + +// RemoveActions will remove the action part of a query. +func (q *Query) RemoveActions() { + if q != nil { + q.actions = nil + } +} + +// EnrichNeeded returns true, if the query references a metadata key that +// is calculated via metadata enrichments. +func (q *Query) EnrichNeeded() bool { + if q == nil { + return false + } + if len(q.actions) > 0 { + // Unknown, what an action will use. Example: RSS needs api.KeyPublished. + return true + } + for _, term := range q.terms { + for key := range term.keys { + if meta.IsProperty(key) { + return true + } + } + for key := range term.mvals { + if meta.IsProperty(key) { + return true + } + } + } + for _, o := range q.order { + if meta.IsProperty(o.key) { + return true + } + } + return false +} + +// RetrieveAndCompile queries the search index and returns a predicate +// for its results and returns a matching predicate. +func (q *Query) RetrieveAndCompile(searcher Searcher) Compiled { + if q == nil { + return Compiled{ + PreMatch: matchAlways, + Terms: []CompiledTerm{{ + Match: matchAlways, + Retrieve: alwaysIncluded, + }}} + } + q = q.Clone() + + preMatch := q.preMatch + if preMatch == nil { + preMatch = matchAlways + } + result := Compiled{PreMatch: preMatch} + + for _, term := range q.terms { + cTerm := term.retrievAndCompileTerm(searcher) + if cTerm.Retrieve == nil { + if cTerm.Match == nil { + // no restriction on match/retrieve -> all will match + return Compiled{ + PreMatch: preMatch, + Terms: []CompiledTerm{{ + Match: matchAlways, + Retrieve: alwaysIncluded, + }}} + } + cTerm.Retrieve = alwaysIncluded + } + if cTerm.Match == nil { + cTerm.Match = matchAlways + } + result.Terms = append(result.Terms, cTerm) + } + return result +} + +func (ct *conjTerms) retrievAndCompileTerm(searcher Searcher) CompiledTerm { + match := ct.compileMeta() // Match might add some searches + var pred RetrievePredicate + if searcher != nil { + pred = ct.retrieveIndex(searcher) + } + return CompiledTerm{Match: match, Retrieve: pred} +} + +// retrieveIndex and return a predicate to ask for results. +func (ct *conjTerms) retrieveIndex(searcher Searcher) RetrievePredicate { + if len(ct.search) == 0 { + return nil + } + normCalls, plainCalls, negCalls := prepareRetrieveCalls(searcher, ct.search) + if hasConflictingCalls(normCalls, plainCalls, negCalls) { + return neverIncluded + } + + positives := retrievePositives(normCalls, plainCalls) + if positives == nil { + // No positive search for words, must contain only words for a negative search. + // Otherwise len(search) == 0 (see above) + negatives := retrieveNegatives(negCalls) + return func(zid id.Zid) bool { return !negatives.Contains(zid) } + } + if len(positives) == 0 { + // Positive search didn't found anything. We can omit the negative search. + return neverIncluded + } + if len(negCalls) == 0 { + // Positive search found something, but there is no negative search. + return positives.Contains + } + negatives := retrieveNegatives(negCalls) + return func(zid id.Zid) bool { + return positives.Contains(zid) && !negatives.Contains(zid) + } +} + +// Sort applies the sorter to the slice of meta data. +func (q *Query) Sort(metaList []*meta.Meta) []*meta.Meta { + if len(metaList) == 0 { + return metaList + } + + if q == nil || len(q.order) == 0 { + sort.Slice(metaList, func(i, j int) bool { return metaList[i].Zid > metaList[j].Zid }) + if q == nil { + return metaList + } + } else if q.order[0].isRandom() { + rand.Shuffle(len(metaList), func(i, j int) { + metaList[i], metaList[j] = metaList[j], metaList[i] + }) + } else { + sort.Slice(metaList, createSortFunc(q.order, metaList)) + } + + if q.offset > 0 { + if q.offset > len(metaList) { + return nil + } + metaList = metaList[q.offset:] + } + return q.Limit(metaList) +} + +// Limit returns only s.GetLimit() elements of the given list. +func (q *Query) Limit(metaList []*meta.Meta) []*meta.Meta { + if q == nil { + return metaList + } + if q.limit > 0 && q.limit < len(metaList) { + return metaList[:q.limit] + } + return metaList +} ADDED query/retrieve.go Index: query/retrieve.go ================================================================== --- query/retrieve.go +++ query/retrieve.go @@ -0,0 +1,169 @@ +//----------------------------------------------------------------------------- +// Copyright (c) 2021-2022 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 query + +// This file contains helper functions to search within the index. + +import ( + "fmt" + "strings" + + "zettelstore.de/z/domain/id" + "zettelstore.de/z/strfun" +) + +type searchOp struct { + s string + op compareOp +} +type searchFunc func(string) id.Set +type searchCallMap map[searchOp]searchFunc + +var cmpPred = map[compareOp]func(string, string) bool{ + cmpHas: func(s, t string) bool { return s == t }, + cmpPrefix: strings.HasPrefix, + cmpSuffix: strings.HasSuffix, + cmpMatch: strings.Contains, +} + +func (scm searchCallMap) addSearch(s string, op compareOp, sf searchFunc) { + pred := cmpPred[op] + for k := range scm { + if op == cmpMatch { + if strings.Contains(k.s, s) { + return + } + if strings.Contains(s, k.s) { + delete(scm, k) + break + } + } + if k.op != op { + continue + } + if pred(k.s, s) { + return + } + if pred(s, k.s) { + delete(scm, k) + } + } + scm[searchOp{s: s, op: op}] = sf +} + +func alwaysIncluded(id.Zid) bool { return true } +func neverIncluded(id.Zid) bool { return false } + +func prepareRetrieveCalls(searcher Searcher, search []expValue) (normCalls, plainCalls, negCalls searchCallMap) { + normCalls = make(searchCallMap, len(search)) + negCalls = make(searchCallMap, len(search)) + for _, val := range search { + for _, word := range strfun.NormalizeWords(val.value) { + if cmpOp := val.op; cmpOp.isNegated() { + cmpOp = cmpOp.negate() + negCalls.addSearch(word, cmpOp, getSearchFunc(searcher, cmpOp)) + } else { + normCalls.addSearch(word, cmpOp, getSearchFunc(searcher, cmpOp)) + } + } + } + + plainCalls = make(searchCallMap, len(search)) + for _, val := range search { + word := strings.ToLower(strings.TrimSpace(val.value)) + if cmpOp := val.op; cmpOp.isNegated() { + cmpOp = cmpOp.negate() + negCalls.addSearch(word, cmpOp, getSearchFunc(searcher, cmpOp)) + } else { + plainCalls.addSearch(word, cmpOp, getSearchFunc(searcher, cmpOp)) + } + } + return normCalls, plainCalls, negCalls +} + +func hasConflictingCalls(normCalls, plainCalls, negCalls searchCallMap) bool { + for val := range negCalls { + if _, found := normCalls[val]; found { + return true + } + if _, found := plainCalls[val]; found { + return true + } + } + return false +} + +func retrievePositives(normCalls, plainCalls searchCallMap) id.Set { + if isSuperset(normCalls, plainCalls) { + var normResult id.Set + for c, sf := range normCalls { + normResult = normResult.IntersectOrSet(sf(c.s)) + } + return normResult + } + + type searchResults map[searchOp]id.Set + var cache searchResults + var plainResult id.Set + for c, sf := range plainCalls { + result := sf(c.s) + if _, found := normCalls[c]; found { + if cache == nil { + cache = make(searchResults) + } + cache[c] = result + } + plainResult = plainResult.IntersectOrSet(result) + } + var normResult id.Set + for c, sf := range normCalls { + if cache != nil { + if result, found := cache[c]; found { + normResult = normResult.IntersectOrSet(result) + continue + } + } + normResult = normResult.IntersectOrSet(sf(c.s)) + } + return normResult.Add(plainResult) +} + +func isSuperset(normCalls, plainCalls searchCallMap) bool { + for c := range plainCalls { + if _, found := normCalls[c]; !found { + return false + } + } + return true +} + +func retrieveNegatives(negCalls searchCallMap) id.Set { + var negatives id.Set + for val, sf := range negCalls { + negatives = negatives.Add(sf(val.s)) + } + return negatives +} + +func getSearchFunc(searcher Searcher, op compareOp) searchFunc { + switch op { + case cmpHas: + return searcher.SearchEqual + case cmpPrefix: + return searcher.SearchPrefix + case cmpSuffix: + return searcher.SearchSuffix + case cmpMatch: + return searcher.SearchContains + default: + panic(fmt.Sprintf("Unexpected value of comparison operation: %v", op)) + } +} ADDED query/select.go Index: query/select.go ================================================================== --- query/select.go +++ query/select.go @@ -0,0 +1,452 @@ +//----------------------------------------------------------------------------- +// Copyright (c) 2020-2022 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 query + +import ( + "bytes" + "fmt" + "strings" + "unicode/utf8" + + "zettelstore.de/z/domain/meta" + "zettelstore.de/z/encoder/textenc" + "zettelstore.de/z/parser" + "zettelstore.de/z/strfun" +) + +type matchValueFunc func(value string) bool + +func matchValueNever(string) bool { return false } + +type matchSpec struct { + key string + match matchValueFunc +} + +// compileMeta calculates a selection func based on the given select criteria. +func (ct *conjTerms) compileMeta() MetaMatchFunc { + for key := range ct.mvals { + // All queried keys must exist + ct.addKey(key, cmpExist) + } + for _, op := range ct.keys { + if op != cmpExist && op != cmpNotExist { + return matchNever + } + } + posSpecs, negSpecs := ct.createSelectSpecs() + if len(posSpecs) > 0 || len(negSpecs) > 0 || len(ct.keys) > 0 { + return makeSearchMetaMatchFunc(posSpecs, negSpecs, ct.keys) + } + return nil +} + +func (ct *conjTerms) createSelectSpecs() (posSpecs, negSpecs []matchSpec) { + posSpecs = make([]matchSpec, 0, len(ct.mvals)) + negSpecs = make([]matchSpec, 0, len(ct.mvals)) + for key, values := range ct.mvals { + if !meta.KeyIsValid(key) { + continue + } + posMatch, negMatch := createPosNegMatchFunc(key, values, ct.addSearch) + if posMatch != nil { + posSpecs = append(posSpecs, matchSpec{key, posMatch}) + } + if negMatch != nil { + negSpecs = append(negSpecs, matchSpec{key, negMatch}) + } + } + return posSpecs, negSpecs +} + +type addSearchFunc func(val expValue) + +func noAddSearch(expValue) {} + +func createPosNegMatchFunc(key string, values []expValue, addSearch addSearchFunc) (posMatch, negMatch matchValueFunc) { + posValues := make([]expValue, 0, len(values)) + negValues := make([]expValue, 0, len(values)) + for _, val := range values { + if val.op.isNegated() { + negValues = append(negValues, val) + } else { + posValues = append(posValues, val) + } + } + if meta.IsProperty(key) { + // Properties are not stored in the Zettelstore and in the search index. + addSearch = noAddSearch + } + return createMatchFunc(key, posValues, addSearch), createMatchFunc(key, negValues, addSearch) +} + +func createMatchFunc(key string, values []expValue, addSearch addSearchFunc) matchValueFunc { + if len(values) == 0 { + return nil + } + switch meta.Type(key) { + case meta.TypeCredential: + return matchValueNever + case meta.TypeID, meta.TypeTimestamp: // ID and timestamp use the same layout + return createMatchIDFunc(values, addSearch) + case meta.TypeIDSet: + return createMatchIDSetFunc(values, addSearch) + case meta.TypeTagSet: + return createMatchTagSetFunc(values, addSearch) + case meta.TypeWord: + return createMatchWordFunc(values, addSearch) + case meta.TypeWordSet: + return createMatchWordSetFunc(values, addSearch) + case meta.TypeZettelmarkup: + return createMatchZmkFunc(values, addSearch) + } + return createMatchStringFunc(values, addSearch) +} + +func createMatchIDFunc(values []expValue, addSearch addSearchFunc) matchValueFunc { + preds := valuesToStringPredicates(values, addSearch) + return func(value string) bool { + for _, pred := range preds { + if !pred(value) { + return false + } + } + return true + } +} + +func createMatchIDSetFunc(values []expValue, addSearch addSearchFunc) matchValueFunc { + predList := valuesToStringSetPredicates(preprocessSet(values), addSearch) + return func(value string) bool { + ids := meta.ListFromValue(value) + for _, preds := range predList { + for _, pred := range preds { + if !pred(ids) { + return false + } + } + } + return true + } +} + +func createMatchTagSetFunc(values []expValue, addSearch addSearchFunc) matchValueFunc { + predList := valuesToStringSetPredicates(processTagSet(preprocessSet(sliceToLower(values))), addSearch) + 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 _, preds := range predList { + for _, pred := range preds { + if !pred(tags) { + return false + } + } + } + return true + } +} + +func processTagSet(valueSet [][]expValue) [][]expValue { + result := make([][]expValue, len(valueSet)) + for i, values := range valueSet { + tags := make([]expValue, len(values)) + for j, val := range values { + if tval := val.value; tval != "" && tval[0] == '#' { + tval = meta.CleanTag(tval) + tags[j] = expValue{value: tval, op: val.op} + } else { + tags[j] = expValue{value: tval, op: val.op} + } + } + result[i] = tags + } + return result +} + +func createMatchWordFunc(values []expValue, addSearch addSearchFunc) matchValueFunc { + preds := valuesToStringPredicates(sliceToLower(values), addSearch) + return func(value string) bool { + value = strings.ToLower(value) + for _, pred := range preds { + if !pred(value) { + return false + } + } + return true + } +} + +func createMatchWordSetFunc(values []expValue, addSearch addSearchFunc) matchValueFunc { + predsList := valuesToStringSetPredicates(preprocessSet(sliceToLower(values)), addSearch) + return func(value string) bool { + words := meta.ListFromValue(value) + for _, preds := range predsList { + for _, pred := range preds { + if !pred(words) { + return false + } + } + } + return true + } +} + +func createMatchStringFunc(values []expValue, addSearch addSearchFunc) matchValueFunc { + preds := valuesToStringPredicates(sliceToLower(values), addSearch) + return func(value string) bool { + value = strings.ToLower(value) + for _, pred := range preds { + if !pred(value) { + return false + } + } + return true + } +} + +func sliceToLower(sl []expValue) []expValue { + result := make([]expValue, 0, len(sl)) + for _, s := range sl { + result = append(result, expValue{ + value: strings.ToLower(s.value), + op: s.op, + }) + } + return result +} + +func createMatchZmkFunc(values []expValue, addSearch addSearchFunc) matchValueFunc { + normPreds := make([]stringPredicate, 0, len(values)) + negPreds := make([]stringPredicate, 0, len(values)) + for _, v := range values { + for _, word := range strfun.NormalizeWords(v.value) { + if cmpOp := v.op; cmpOp.isNegated() { + cmpOp = cmpOp.negate() + negPreds = append(negPreds, createStringCompareFunc(word, cmpOp)) + } else { + normPreds = append(normPreds, createStringCompareFunc(word, cmpOp)) + addSearch(expValue{word, cmpOp}) // addSearch only for positive selections + } + } + } + return func(metaValue string) bool { + temp := strings.Fields(zmk2text(metaValue)) + values := make([]string, 0, len(temp)) + for _, s := range temp { + values = append(values, strfun.NormalizeWords(s)...) + } + for _, pred := range normPreds { + if noneOf(pred, values) { + return false + } + } + for _, pred := range negPreds { + for _, val := range values { + if pred(val) { + return false + } + } + } + return true + } +} + +func noneOf(pred stringPredicate, values []string) bool { + for _, value := range values { + if pred(value) { + return false + } + } + return true +} + +func zmk2text(zmk string) string { + isASCII, hasUpper, needParse := true, false, false + for i := 0; i < len(zmk); i++ { + ch := zmk[i] + if ch >= utf8.RuneSelf { + isASCII = false + break + } + hasUpper = hasUpper || ('A' <= ch && ch <= 'Z') + needParse = needParse || !(('A' <= ch && ch <= 'Z') || ('a' <= ch && ch <= 'z') || ('0' <= ch && ch <= '9') || ch == ' ') + } + if isASCII { + if !needParse { + if !hasUpper { + return zmk + } + return strings.ToLower(zmk) + } + } + is := parser.ParseMetadata(zmk) + var buf bytes.Buffer + if _, err := textenc.Create().WriteInlines(&buf, &is); err != nil { + return strings.ToLower(zmk) + } + return strings.ToLower(buf.String()) +} + +func preprocessSet(set []expValue) [][]expValue { + result := make([][]expValue, 0, len(set)) + for _, elem := range set { + splitElems := strings.Split(elem.value, ",") + valueElems := make([]expValue, 0, len(splitElems)) + for _, se := range splitElems { + e := strings.TrimSpace(se) + if len(e) > 0 { + valueElems = append(valueElems, expValue{value: e, op: elem.op}) + } + } + if len(valueElems) > 0 { + result = append(result, valueElems) + } + } + return result +} + +type stringPredicate func(string) bool + +func valuesToStringPredicates(values []expValue, addSearch addSearchFunc) []stringPredicate { + result := make([]stringPredicate, len(values)) + for i, v := range values { + if !v.op.isNegated() { + addSearch(v) // addSearch only for positive selections + } + result[i] = createStringCompareFunc(v.value, v.op) + } + return result +} + +func createStringCompareFunc(cmpVal string, cmpOp compareOp) stringPredicate { + switch cmpOp { + case cmpHas: + return func(metaVal string) bool { return metaVal == cmpVal } + case cmpHasNot: + return func(metaVal string) bool { return metaVal != cmpVal } + case cmpPrefix: + return func(metaVal string) bool { return strings.HasPrefix(metaVal, cmpVal) } + case cmpNoPrefix: + return func(metaVal string) bool { return !strings.HasPrefix(metaVal, cmpVal) } + case cmpSuffix: + return func(metaVal string) bool { return strings.HasSuffix(metaVal, cmpVal) } + case cmpNoSuffix: + return func(metaVal string) bool { return !strings.HasSuffix(metaVal, cmpVal) } + case cmpMatch: + return func(metaVal string) bool { return strings.Contains(metaVal, cmpVal) } + case cmpNoMatch: + return func(metaVal string) bool { return !strings.Contains(metaVal, cmpVal) } + default: + panic(fmt.Sprintf("Unknown compare operation %d with value %q", cmpOp, cmpVal)) + } +} + +type stringSetPredicate func(value []string) bool + +func valuesToStringSetPredicates(values [][]expValue, addSearch addSearchFunc) [][]stringSetPredicate { + result := make([][]stringSetPredicate, len(values)) + for i, val := range values { + elemPreds := make([]stringSetPredicate, len(val)) + for j, v := range val { + opVal := v.value // loop variable is used in closure --> save needed value + switch v.op { + case cmpHas: + addSearch(v) // addSearch only for positive selections + elemPreds[j] = makeStringSetPredicate(opVal, stringEqual, true) + case cmpHasNot: + elemPreds[j] = makeStringSetPredicate(opVal, stringEqual, false) + case cmpPrefix: + addSearch(v) + elemPreds[j] = makeStringSetPredicate(opVal, strings.HasPrefix, true) + case cmpNoPrefix: + elemPreds[j] = makeStringSetPredicate(opVal, strings.HasPrefix, false) + case cmpSuffix: + addSearch(v) + elemPreds[j] = makeStringSetPredicate(opVal, strings.HasSuffix, true) + case cmpNoSuffix: + elemPreds[j] = makeStringSetPredicate(opVal, strings.HasSuffix, false) + case cmpMatch: + addSearch(v) + elemPreds[j] = makeStringSetPredicate(opVal, strings.Contains, true) + case cmpNoMatch: + elemPreds[j] = makeStringSetPredicate(opVal, strings.Contains, false) + default: + panic(fmt.Sprintf("Unknown compare operation %d with value %q", v.op, opVal)) + } + } + result[i] = elemPreds + } + return result +} + +func stringEqual(val1, val2 string) bool { return val1 == val2 } + +type compareStringFunc func(val1, val2 string) bool + +func makeStringSetPredicate(neededValue string, compare compareStringFunc, foundResult bool) stringSetPredicate { + return func(metaVals []string) bool { + for _, metaVal := range metaVals { + if compare(metaVal, neededValue) { + return foundResult + } + } + return !foundResult + } +} + +func makeSearchMetaMatchFunc(posSpecs, negSpecs []matchSpec, kem keyExistMap) MetaMatchFunc { + // Optimize: no specs --> just check kwhether key exists + if len(posSpecs) == 0 && len(negSpecs) == 0 { + if len(kem) == 0 { + return nil + } + return func(m *meta.Meta) bool { return matchMetaKeyExists(m, kem) } + } + + // Optimize: only negative or only positive matching + if len(posSpecs) == 0 { + return func(m *meta.Meta) bool { + return matchMetaKeyExists(m, kem) && matchMetaSpecs(m, negSpecs) + } + } + if len(negSpecs) == 0 { + return func(m *meta.Meta) bool { + return matchMetaKeyExists(m, kem) && matchMetaSpecs(m, posSpecs) + } + } + + return func(m *meta.Meta) bool { + return matchMetaKeyExists(m, kem) && + matchMetaSpecs(m, posSpecs) && + matchMetaSpecs(m, negSpecs) + } +} + +func matchMetaKeyExists(m *meta.Meta, kem keyExistMap) bool { + for key, op := range kem { + _, found := m.Get(key) + if found != (op == cmpExist) { + return false + } + } + return true +} +func matchMetaSpecs(m *meta.Meta, specs []matchSpec) bool { + for _, s := range specs { + if value := m.GetDefault(s.key, ""); !s.match(value) { + return false + } + } + return true +} ADDED query/select_test.go Index: query/select_test.go ================================================================== --- query/select_test.go +++ query/select_test.go @@ -0,0 +1,44 @@ +//----------------------------------------------------------------------------- +// Copyright (c) 2021-2022 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 query_test + +import ( + "testing" + + "zettelstore.de/c/api" + "zettelstore.de/z/domain/id" + "zettelstore.de/z/domain/meta" + "zettelstore.de/z/query" +) + +func TestMatchZidNegate(t *testing.T) { + q := query.Parse(api.KeyID + api.SearchOperatorHasNot + string(api.ZidVersion) + " " + api.KeyID + api.SearchOperatorHasNot + string(api.ZidLicense)) + compiled := q.RetrieveAndCompile(nil) + + testCases := []struct { + zid api.ZettelID + exp bool + }{ + {api.ZidVersion, false}, + {api.ZidLicense, false}, + {api.ZidAuthors, true}, + } + for i, tc := range testCases { + m := meta.New(id.MustParse(tc.zid)) + if compiled.Terms[0].Match(m) != tc.exp { + if tc.exp { + t.Errorf("%d: meta %v must match %q", i, m.Zid, q) + } else { + t.Errorf("%d: meta %v must not match %q", i, m.Zid, q) + } + } + } +} ADDED query/sorter.go Index: query/sorter.go ================================================================== --- query/sorter.go +++ query/sorter.go @@ -0,0 +1,103 @@ +//----------------------------------------------------------------------------- +// Copyright (c) 2020-2022 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 query + +import ( + "strconv" + + "zettelstore.de/c/api" + "zettelstore.de/z/domain/meta" +) + +type sortFunc func(i, j int) bool + +func createSortFunc(order []sortOrder, ml []*meta.Meta) sortFunc { + hasID := false + sortFuncs := make([]sortFunc, 0, len(order)+1) + for _, o := range order { + sortFuncs = append(sortFuncs, createOneSortFunc(o.key, o.descending, ml)) + if o.key == api.KeyID { + hasID = true + break + } + } + if !hasID { + sortFuncs = append(sortFuncs, func(i, j int) bool { return ml[i].Zid > ml[j].Zid }) + } + // return sortFuncs[0] + if len(sortFuncs) == 1 { + return sortFuncs[0] + } + return func(i, j int) bool { + for _, sf := range sortFuncs { + if sf(i, j) { + return true + } + if sf(j, i) { + return false + } + } + return false + } +} + +func createOneSortFunc(key string, descending bool, ml []*meta.Meta) sortFunc { + keyType := meta.Type(key) + if key == api.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.TypeNumber { + return createSortNumberFunc(ml, key, descending) + } + return createSortStringFunc(ml, key, descending) +} + +func createSortNumberFunc(ml []*meta.Meta, key string, descending bool) sortFunc { + if descending { + return func(i, j int) bool { + iVal, iOk := getNum(ml[i], key) + jVal, jOk := getNum(ml[j], key) + return (iOk && (!jOk || iVal > jVal)) || !jOk + } + } + return func(i, j int) bool { + iVal, iOk := getNum(ml[i], key) + jVal, jOk := getNum(ml[j], key) + return (iOk && (!jOk || iVal < jVal)) || !jOk + } +} + +func createSortStringFunc(ml []*meta.Meta, key string, descending bool) sortFunc { + if descending { + return func(i, j int) bool { + iVal, iOk := ml[i].Get(key) + jVal, jOk := ml[j].Get(key) + return (iOk && (!jOk || iVal > jVal)) || !jOk + } + } + return func(i, j int) bool { + iVal, iOk := ml[i].Get(key) + jVal, jOk := ml[j].Get(key) + return (iOk && (!jOk || iVal < jVal)) || !jOk + } +} + +func getNum(m *meta.Meta, key string) (int64, bool) { + if s, ok := m.Get(key); ok { + if i, err := strconv.ParseInt(s, 10, 64); err == nil { + return i, true + } + } + return 0, false +} DELETED search/parser.go Index: search/parser.go ================================================================== --- search/parser.go +++ search/parser.go @@ -1,262 +0,0 @@ -//----------------------------------------------------------------------------- -// Copyright (c) 2022 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 search - -import ( - "strconv" - - "zettelstore.de/z/domain/meta" - "zettelstore.de/z/input" -) - -// Parse the search specification and return a Search object. -func Parse(spec string) *Search { - state := parserState{ - inp: input.NewInput([]byte(spec)), - } - return state.parse() -} - -type parserState struct { - inp *input.Input -} - -func (ps *parserState) mustStop() bool { return ps.inp.Ch == input.EOS } -func (ps *parserState) acceptSingleKw(s string) bool { - return ps.inp.Accept(s) && (ps.isSpace() || ps.mustStop()) -} -func (ps *parserState) acceptKwArgs(s string) bool { - if ps.inp.Accept(s) && ps.isSpace() { - ps.skipSpace() - return true - } - return false -} - -const ( - kwLimit = "LIMIT" - kwNegate = "NEGATE" - kwOffset = "OFFSET" - kwOrder = "ORDER" - kwRandom = "RANDOM" - kwReverse = "REVERSE" -) - -func (ps *parserState) parse() *Search { - inp := ps.inp - var result *Search - for { - ps.skipSpace() - if ps.mustStop() { - break - } - pos := inp.Pos - if ps.acceptSingleKw(kwNegate) { - result = createIfNeeded(result) - result.negate = !result.negate - continue - } - if ps.acceptSingleKw(kwRandom) { - result = createIfNeeded(result) - if len(result.order) == 0 { - result.order = []sortOrder{{"", false}} - } - continue - } - if ps.acceptKwArgs(kwOrder) { - if s, ok := ps.parseOrder(result); ok { - result = s - continue - } - } - if ps.acceptKwArgs(kwOffset) { - if s, ok := ps.parseOffset(result); ok { - result = s - continue - } - } - if ps.acceptKwArgs(kwLimit) { - if s, ok := ps.parseLimit(result); ok { - result = s - continue - } - } - inp.SetPos(pos) - result = ps.parseText(result) - } - return result -} -func (ps *parserState) parseOrder(s *Search) (*Search, bool) { - reverse := false - if ps.acceptKwArgs(kwReverse) { - reverse = true - } - word := ps.scanWord() - if len(word) == 0 { - return s, false - } - if sWord := string(word); meta.KeyIsValid(sWord) { - s = createIfNeeded(s) - if len(s.order) == 1 && s.order[0].isRandom() { - s.order = nil - } - s.order = append(s.order, sortOrder{sWord, reverse}) - return s, true - } - return s, false -} - -func (ps *parserState) parseOffset(s *Search) (*Search, bool) { - num, ok := ps.scanPosInt() - if !ok { - return s, false - } - s = createIfNeeded(s) - if s.offset <= num { - s.offset = num - } - return s, true -} - -func (ps *parserState) parseLimit(s *Search) (*Search, bool) { - num, ok := ps.scanPosInt() - if !ok { - return s, false - } - s = createIfNeeded(s) - if s.limit == 0 || s.limit >= num { - s.limit = num - } - return s, true -} - -func (ps *parserState) parseText(s *Search) *Search { - hasOp, cmpOp, cmpNegate := ps.scanSearchOp() - text, key := ps.scanSearchTextOrKey(hasOp) - if key != nil { - // Assert: hasOp == false - hasOp, cmpOp, cmpNegate = ps.scanSearchOp() - // Assert hasOp == true - text = ps.scanWord() - } else if text == nil { - // Only an empty search operation is found -> ignore it - return s - } - s = createIfNeeded(s) - if hasOp { - s.addExpValue(string(key), expValue{string(text), cmpOp, cmpNegate}) - } else { - // Assert key == nil - s.addExpValue("", expValue{string(text), cmpDefault, false}) - } - return s -} - -func (ps *parserState) scanSearchTextOrKey(hasOp bool) ([]byte, []byte) { - inp := ps.inp - pos := inp.Pos - allowKey := !hasOp - - for !ps.isSpace() && !ps.mustStop() { - if allowKey { - switch inp.Ch { - case '!', ':', '=', '>', '<', '~': - allowKey = false - if key := inp.Src[pos:inp.Pos]; meta.KeyIsValid(string(key)) { - return nil, key - } - } - } - inp.Next() - } - return inp.Src[pos:inp.Pos], nil -} - -func (ps *parserState) scanWord() []byte { - inp := ps.inp - pos := inp.Pos - for !ps.isSpace() && !ps.mustStop() { - inp.Next() - } - return inp.Src[pos:inp.Pos] -} - -func (ps *parserState) scanPosInt() (int, bool) { - inp := ps.inp - ch := inp.Ch - if ch == '0' { - ch = inp.Next() - if isSpace(ch) || ps.mustStop() { - return 0, true - } - return 0, false - } - word := ps.scanWord() - if len(word) == 0 { - return 0, false - } - uval, err := strconv.ParseUint(string(word), 10, 63) - if err != nil { - return 0, false - } - return int(uval), true -} - -func (ps *parserState) scanSearchOp() (bool, compareOp, bool) { - inp := ps.inp - ch := inp.Ch - negate := false - if ch == '!' { - ch = inp.Next() - negate = true - } - switch ch { - case ':': - inp.Next() - return true, cmpDefault, negate - case '=': - inp.Next() - return true, cmpEqual, negate - case '<': - inp.Next() - return true, cmpSuffix, negate - case '>': - inp.Next() - return true, cmpPrefix, negate - case '~': - inp.Next() - return true, cmpContains, negate - } - if negate { - return true, cmpDefault, true - } - return false, cmpUnknown, false -} - -func (ps *parserState) isSpace() bool { - return isSpace(ps.inp.Ch) -} - -func isSpace(ch rune) bool { - switch ch { - case input.EOS: - return false - case ' ', '\t', '\n', '\r': - return true - } - return input.IsSpace(ch) -} - -func (ps *parserState) skipSpace() { - for ps.isSpace() { - ps.inp.Next() - } -} DELETED search/parser_test.go Index: search/parser_test.go ================================================================== --- search/parser_test.go +++ search/parser_test.go @@ -1,73 +0,0 @@ -//----------------------------------------------------------------------------- -// Copyright (c) 2022 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 search_test - -import ( - "testing" - - "zettelstore.de/z/search" -) - -func TestParser(t *testing.T) { - t.Parallel() - testcases := []struct { - spec string - exp string - }{ - {"", ""}, - {`a`, `a`}, {`!a`, `!a`}, - {`:a`, `a`}, {`!:a`, `!a`}, - {`=a`, `=a`}, {`!=a`, `!=a`}, - {`>a`, `>a`}, {`!>a`, `!>a`}, - {``, `key>`}, {`key!>`, `key!>`}, - {`key<`, `key<`}, {`key!<`, `key!<`}, - {`key~`, `key~`}, {`key!~`, `key!~`}, - {`key:a`, `key:a`}, {`key!:a`, `key!:a`}, - {`key=a`, `key=a`}, {`key!=a`, `key!=a`}, - {`key>a`, `key>a`}, {`key!>a`, `key!>a`}, - {`key 0 { - env.printExprValues("", s.search) - } - for _, name := range maps.Keys(s.mvals) { - env.printExprValues(name, s.mvals[name]) - } - env.printOrder(s.order) - env.printPosInt(kwOffset, s.offset) - env.printPosInt(kwLimit, s.limit) -} - -type printEnv struct { - w io.Writer - space bool -} - -var bsSpace = []byte{' '} - -func (pe *printEnv) printSpace() { - if pe.space { - pe.w.Write(bsSpace) - return - } - pe.space = true -} -func (pe *printEnv) writeString(s string) { io.WriteString(pe.w, s) } - -func (pe *printEnv) printExprValues(key string, values []expValue) { - for _, val := range values { - pe.printSpace() - pe.writeString(key) - if val.negate { - pe.writeString("!") - } - switch val.op { - case cmpDefault: - pe.writeString(":") - case cmpEqual: - pe.writeString("=") - case cmpPrefix: - pe.writeString(">") - case cmpSuffix: - pe.writeString("<") - case cmpContains: - // An empty key signals a full-text search. Since "~" is the default op in this case, - // it can be ignored. Therefore, print only "~" if there is a key. - if key != "" { - pe.writeString("~") - } - } - if s := val.value; s != "" { - pe.writeString(s) - } - } -} - -func (s *Search) Human() string { - var sb strings.Builder - s.PrintHuman(&sb) - return sb.String() -} - -// PrintHuman the search to a writer in a human readable form. -func (s *Search) PrintHuman(w io.Writer) { - if s == nil { - return - } - env := printEnv{w: w} - if s.negate { - env.writeString("NOT (") - } - if len(s.search) > 0 { - env.writeString("ANY") - env.printHumanSelectExprValues(s.search) - env.space = true - } - for _, name := range maps.Keys(s.mvals) { - if env.space { - env.writeString(" AND ") - } - env.writeString(name) - env.printHumanSelectExprValues(s.mvals[name]) - env.space = true - } - if s.negate { - env.writeString(")") - env.space = true - } - - env.printOrder(s.order) - env.printPosInt(kwOffset, s.offset) - env.printPosInt(kwLimit, s.limit) -} - -func (pe *printEnv) printHumanSelectExprValues(values []expValue) { - if len(values) == 0 { - pe.writeString(" MATCH ANY") - return - } - - for j, val := range values { - if j > 0 { - pe.writeString(" AND") - } - if val.negate { - pe.writeString(" NOT") - } - switch val.op { - case cmpDefault: - pe.writeString(" MATCH ") - case cmpEqual: - pe.writeString(" EQUAL ") - case cmpPrefix: - pe.writeString(" PREFIX ") - case cmpSuffix: - pe.writeString(" SUFFIX ") - case cmpContains: - pe.writeString(" CONTAINS ") - default: - pe.writeString(" MaTcH ") - } - if val.value == "" { - pe.writeString("ANY") - } else { - pe.writeString(val.value) - } - } -} - -func (pe *printEnv) printOrder(order []sortOrder) { - for _, o := range order { - if o.isRandom() { - pe.printSpace() - pe.writeString(kwRandom) - continue - } else if o.key == api.KeyID && o.descending { - continue - } - pe.printSpace() - pe.writeString(kwOrder) - if o.descending { - pe.printSpace() - pe.writeString(kwReverse) - } - pe.printSpace() - pe.writeString(o.key) - } -} - -func (pe *printEnv) printPosInt(key string, val int) { - if val > 0 { - pe.printSpace() - pe.writeString(key) - pe.writeString(" ") - pe.writeString(strconv.Itoa(val)) - } -} DELETED search/retrieve.go Index: search/retrieve.go ================================================================== --- search/retrieve.go +++ search/retrieve.go @@ -1,169 +0,0 @@ -//----------------------------------------------------------------------------- -// Copyright (c) 2021-2022 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 search - -// This file contains helper functions to search within the index. - -import ( - "fmt" - "strings" - - "zettelstore.de/z/domain/id" - "zettelstore.de/z/strfun" -) - -type searchOp struct { - s string - op compareOp -} -type searchFunc func(string) id.Set -type searchCallMap map[searchOp]searchFunc - -var cmpPred = map[compareOp]func(string, string) bool{ - cmpEqual: func(s, t string) bool { return s == t }, - cmpPrefix: strings.HasPrefix, - cmpSuffix: strings.HasSuffix, - cmpContains: strings.Contains, -} - -func (scm searchCallMap) addSearch(s string, op compareOp, sf searchFunc) { - pred := cmpPred[op] - for k := range scm { - if op == cmpContains { - if strings.Contains(k.s, s) { - return - } - if strings.Contains(s, k.s) { - delete(scm, k) - break - } - } - if k.op != op { - continue - } - if pred(k.s, s) { - return - } - if pred(s, k.s) { - delete(scm, k) - } - } - scm[searchOp{s: s, op: op}] = sf -} - -func alwaysIncluded(id.Zid) bool { return true } -func neverIncluded(id.Zid) bool { return false } - -func prepareRetrieveCalls(searcher Searcher, search []expValue) (normCalls, plainCalls, negCalls searchCallMap) { - normCalls = make(searchCallMap, len(search)) - negCalls = make(searchCallMap, len(search)) - for _, val := range search { - for _, word := range strfun.NormalizeWords(val.value) { - sf := getSearchFunc(searcher, val.op) - if val.negate { - negCalls.addSearch(word, val.op, sf) - } else { - normCalls.addSearch(word, val.op, sf) - } - } - } - - plainCalls = make(searchCallMap, len(search)) - for _, val := range search { - word := strings.ToLower(strings.TrimSpace(val.value)) - sf := getSearchFunc(searcher, val.op) - if val.negate { - negCalls.addSearch(word, val.op, sf) - } else { - plainCalls.addSearch(word, val.op, sf) - } - } - return normCalls, plainCalls, negCalls -} - -func hasConflictingCalls(normCalls, plainCalls, negCalls searchCallMap) bool { - for val := range negCalls { - if _, found := normCalls[val]; found { - return true - } - if _, found := plainCalls[val]; found { - return true - } - } - return false -} - -func retrievePositives(normCalls, plainCalls searchCallMap) id.Set { - if isSuperset(normCalls, plainCalls) { - var normResult id.Set - for c, sf := range normCalls { - normResult = normResult.IntersectOrSet(sf(c.s)) - } - return normResult - } - - type searchResults map[searchOp]id.Set - var cache searchResults - var plainResult id.Set - for c, sf := range plainCalls { - result := sf(c.s) - if _, found := normCalls[c]; found { - if cache == nil { - cache = make(searchResults) - } - cache[c] = result - } - plainResult = plainResult.IntersectOrSet(result) - } - var normResult id.Set - for c, sf := range normCalls { - if cache != nil { - if result, found := cache[c]; found { - normResult = normResult.IntersectOrSet(result) - continue - } - } - normResult = normResult.IntersectOrSet(sf(c.s)) - } - return normResult.Add(plainResult) -} - -func isSuperset(normCalls, plainCalls searchCallMap) bool { - for c := range plainCalls { - if _, found := normCalls[c]; !found { - return false - } - } - return true -} - -func retrieveNegatives(negCalls searchCallMap) id.Set { - var negatives id.Set - for val, sf := range negCalls { - negatives = negatives.Add(sf(val.s)) - } - return negatives -} - -func getSearchFunc(searcher Searcher, op compareOp) searchFunc { - switch op { - case cmpEqual: - return searcher.SearchEqual - case cmpPrefix: - return searcher.SearchPrefix - case cmpSuffix: - return searcher.SearchSuffix - case cmpContains: - return searcher.SearchContains - default: - panic(fmt.Sprintf("Unexpected value of comparison operation: %v", op)) - } -} DELETED search/search.go Index: search/search.go ================================================================== --- search/search.go +++ search/search.go @@ -1,457 +0,0 @@ -//----------------------------------------------------------------------------- -// Copyright (c) 2020-2022 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 search provides a zettel search. -package search - -import ( - "fmt" - "math/rand" - "sort" - "strings" - "sync" - - "zettelstore.de/c/api" - "zettelstore.de/z/domain/id" - "zettelstore.de/z/domain/meta" -) - -// Searcher is used to select zettel identifier based on search criteria. -type Searcher interface { - // Select all zettel that contains the given exact word. - // The word must be normalized through Unicode NKFD, trimmed and not empty. - SearchEqual(word string) id.Set - - // Select all zettel that have a word with the given prefix. - // The prefix must be normalized through Unicode NKFD, trimmed and not empty. - SearchPrefix(prefix string) id.Set - - // Select all zettel that have a word with the given suffix. - // The suffix must be normalized through Unicode NKFD, trimmed and not empty. - SearchSuffix(suffix string) id.Set - - // Select all zettel that contains the given string. - // The string must be normalized through Unicode NKFD, trimmed and not empty. - SearchContains(s string) id.Set -} - -// MetaMatchFunc is a function determine whethe some metadata should be selected or not. -type MetaMatchFunc func(*meta.Meta) bool - -// RetrieveFunc retrieves the index based on a Search. -type RetrieveFunc func() id.Set - -// RetrievePredicate returns true, if the given Zid is contained in the (full-text) search. -type RetrievePredicate func(id.Zid) bool - -// Search specifies a mechanism for selecting zettel. -type Search struct { - mx sync.RWMutex // Protects other attributes - - // Fields to be used for selecting - preMatch MetaMatchFunc // Match that must be true - mvals expMetaValues // Expected values for a meta datum - search []expValue // Search string - negate bool // Negate the result of the whole selecting process - - // Fields to be used for sorting - order []sortOrder - offset int // <= 0: no offset - limit int // <= 0: no limit -} - -type sortOrder struct { - key string - descending bool -} - -func (so *sortOrder) isRandom() bool { return so.key == "" } - -type expMetaValues map[string][]expValue - -func createIfNeeded(s *Search) *Search { - if s == nil { - return new(Search) - } - return s -} - -// Clone the search value. -func (s *Search) Clone() *Search { - if s == nil { - return nil - } - c := new(Search) - c.preMatch = s.preMatch - c.mvals = make(expMetaValues, len(s.mvals)) - for k, v := range s.mvals { - c.mvals[k] = v - } - c.search = append([]expValue{}, s.search...) - c.negate = s.negate - c.order = append([]sortOrder{}, s.order...) - c.offset = s.offset - c.limit = s.limit - return c -} - -// RandomOrder is a pseudo metadata key that selects a random order. -const RandomOrder = "_random" - -type compareOp uint8 - -const ( - cmpUnknown compareOp = iota - cmpDefault - cmpNotDefault - cmpEqual - cmpNotEqual - cmpPrefix - cmpNoPrefix - cmpSuffix - cmpNoSuffix - cmpContains - cmpNotContains -) - -var negateMap = map[compareOp]compareOp{ - cmpUnknown: cmpUnknown, - cmpDefault: cmpNotDefault, - cmpNotDefault: cmpDefault, - cmpEqual: cmpNotEqual, - cmpNotEqual: cmpEqual, - cmpPrefix: cmpNoPrefix, - cmpNoPrefix: cmpPrefix, - cmpSuffix: cmpNoSuffix, - cmpNoSuffix: cmpSuffix, - cmpContains: cmpNotContains, - cmpNotContains: cmpContains, -} - -func (op compareOp) negate() compareOp { - return negateMap[op] -} - -type expValue struct { - value string - op compareOp - negate bool -} - -// AddExpr adds a match expression to the search. -func (s *Search) AddExpr(key, value string) *Search { - val := parseOp(strings.TrimSpace(value)) - if s == nil { - s = new(Search) - } - s.mx.Lock() - defer s.mx.Unlock() - s.addExpValue(key, val) - return s -} - -func (s *Search) addExpValue(key string, val expValue) { - if key == "" { - s.addSearch(val) - } else if s.mvals == nil { - s.mvals = expMetaValues{key: {val}} - } else { - s.mvals[key] = append(s.mvals[key], val) - } -} - -func (s *Search) addSearch(val expValue) { - if val.negate { - val.op = val.op.negate() - val.negate = false - } - switch val.op { - case cmpDefault: - val.op = cmpContains - case cmpNotDefault: - val.op = cmpContains - val.negate = true - case cmpNotEqual, cmpNoPrefix, cmpNoSuffix, cmpNotContains: - val.op = val.op.negate() - val.negate = true - } - s.search = append(s.search, val) -} - -func parseOp(s string) expValue { - if s == "" { - return expValue{value: s, op: cmpDefault, negate: false} - } - if s[0] == '\\' { - return expValue{value: s[1:], op: cmpDefault, negate: false} - } - negate := false - if s[0] == '!' { - negate = true - s = s[1:] - } - if s == "" { - return expValue{value: s, op: cmpDefault, negate: negate} - } - if s[0] == '\\' { - return expValue{value: s[1:], op: cmpDefault, negate: negate} - } - switch s[0] { - case ':': - return expValue{value: s[1:], op: cmpDefault, negate: negate} - case '=': - return expValue{value: s[1:], op: cmpEqual, negate: negate} - case '>': - return expValue{value: s[1:], op: cmpPrefix, negate: negate} - case '<': - return expValue{value: s[1:], op: cmpSuffix, negate: negate} - case '~': - return expValue{value: s[1:], op: cmpContains, negate: negate} - } - return expValue{value: s, op: cmpDefault, negate: negate} -} - -// SetNegate changes the search to reverse its selection. -func (s *Search) SetNegate() *Search { - s = createIfNeeded(s) - s.mx.Lock() - defer s.mx.Unlock() - s.negate = true - return s -} - -// AddPreMatch adds the pre-selection predicate. -func (s *Search) AddPreMatch(preMatch MetaMatchFunc) *Search { - s = createIfNeeded(s) - s.mx.Lock() - defer s.mx.Unlock() - if pre := s.preMatch; pre == nil { - s.preMatch = preMatch - } else { - s.preMatch = func(m *meta.Meta) bool { - return preMatch(m) && pre(m) - } - } - return s -} - -// AddOrder adds the given order to the search object. -func (s *Search) AddOrder(key string, descending bool) *Search { - s = createIfNeeded(s) - s.mx.Lock() - defer s.mx.Unlock() - if len(s.order) > 0 { - panic("order field already set: " + fmt.Sprintf("%v", s.order)) - } - if key == RandomOrder { - s.order = []sortOrder{{"", false}} - } else { - s.order = []sortOrder{{key, descending}} - } - return s -} - -// SetOffset sets the given offset of the search object. -func (s *Search) SetOffset(offset int) *Search { - s = createIfNeeded(s) - s.mx.Lock() - defer s.mx.Unlock() - if offset < 0 { - offset = 0 - } - s.offset = offset - return s -} - -// GetOffset returns the current offset value. -func (s *Search) GetOffset() int { - if s == nil { - return 0 - } - s.mx.RLock() - defer s.mx.RUnlock() - return s.offset -} - -// SetLimit sets the given limit of the search object. -func (s *Search) SetLimit(limit int) *Search { - s = createIfNeeded(s) - s.mx.Lock() - defer s.mx.Unlock() - if limit < 0 { - limit = 0 - } - s.limit = limit - return s -} - -// GetLimit returns the current offset value. -func (s *Search) GetLimit() int { - if s == nil { - return 0 - } - s.mx.RLock() - defer s.mx.RUnlock() - return s.limit -} - -// EnrichNeeded returns true, if the search references a metadata key that -// is calculated via metadata enrichments. -func (s *Search) EnrichNeeded() bool { - if s == nil { - return false - } - s.mx.RLock() - defer s.mx.RUnlock() - for key := range s.mvals { - if meta.IsComputed(key) { - return true - } - } - for _, o := range s.order { - if meta.IsComputed(o.key) { - return true - } - } - return false -} - -// RetrieveAndCompileMatch queries the search index and returns a predicate -// for its results and returns a matching predicate. -func (s *Search) RetrieveAndCompileMatch(searcher Searcher) (RetrievePredicate, MetaMatchFunc) { - if s == nil { - return alwaysIncluded, matchAlways - } - s = s.Clone() - match := s.compileMatch() // Match might add some searches - var pred RetrievePredicate - if searcher != nil { - pred = s.retrieveIndex(searcher) - } - - if pred == nil { - if match == nil { - if s.negate { - return neverIncluded, matchNever - } - return alwaysIncluded, matchAlways - } - return alwaysIncluded, match - } - if match == nil { - return pred, matchAlways - } - return pred, match -} - -// retrieveIndex and return a predicate to ask for results. -func (s *Search) retrieveIndex(searcher Searcher) RetrievePredicate { - if len(s.search) == 0 { - return nil - } - normCalls, plainCalls, negCalls := prepareRetrieveCalls(searcher, s.search) - if hasConflictingCalls(normCalls, plainCalls, negCalls) { - return s.neverWithNegate() - } - - negate := s.negate - positives := retrievePositives(normCalls, plainCalls) - if positives == nil { - // No positive search for words, must contain only words for a negative search. - // Otherwise len(search) == 0 (see above) - negatives := retrieveNegatives(negCalls) - return func(zid id.Zid) bool { return negatives.Contains(zid) == negate } - } - if len(positives) == 0 { - // Positive search didn't found anything. We can omit the negative search. - return s.neverWithNegate() - } - if len(negCalls) == 0 { - // Positive search found something, but there is no negative search. - return func(zid id.Zid) bool { return positives.Contains(zid) != negate } - } - negatives := retrieveNegatives(negCalls) - return func(zid id.Zid) bool { - return (positives.Contains(zid) && !negatives.Contains(zid)) != negate - } -} - -func (s *Search) neverWithNegate() RetrievePredicate { - if s.negate { - return alwaysIncluded - } - return neverIncluded -} - -// compileMatch returns a function to match metadata based on select specification. -func (s *Search) compileMatch() MetaMatchFunc { - compMeta := s.compileMeta() - preMatch := s.preMatch - if compMeta == nil { - if preMatch == nil { - return nil - } - return preMatch - } - if s.negate { - if preMatch == nil { - return func(m *meta.Meta) bool { return !compMeta(m) } - } - return func(m *meta.Meta) bool { return preMatch(m) && !compMeta(m) } - } - if preMatch == nil { - return compMeta - } - return func(m *meta.Meta) bool { return preMatch(m) && compMeta(m) } -} - -func matchAlways(*meta.Meta) bool { return true } -func matchNever(*meta.Meta) bool { return false } - -// Sort applies the sorter to the slice of meta data. -func (s *Search) Sort(metaList []*meta.Meta) []*meta.Meta { - if len(metaList) == 0 { - return metaList - } - - if s == nil { - sort.Slice(metaList, func(i, j int) bool { return metaList[i].Zid > metaList[j].Zid }) - return metaList - } - - if len(s.order) == 0 { - sort.Slice(metaList, createSortFunc(api.KeyID, true, metaList)) - } else if s.order[0].isRandom() { - rand.Shuffle(len(metaList), func(i, j int) { - metaList[i], metaList[j] = metaList[j], metaList[i] - }) - } else { - sort.Slice(metaList, createSortFunc(s.order[0].key, s.order[0].descending, metaList)) - } - - if s.offset > 0 { - if s.offset > len(metaList) { - return nil - } - metaList = metaList[s.offset:] - } - return s.Limit(metaList) -} - -// Limit returns only s.GetLimit() elements of the given list. -func (s *Search) Limit(metaList []*meta.Meta) []*meta.Meta { - if s == nil { - return metaList - } - if s.limit > 0 && s.limit < len(metaList) { - return metaList[:s.limit] - } - return metaList -} DELETED search/select.go Index: search/select.go ================================================================== --- search/select.go +++ search/select.go @@ -1,404 +0,0 @@ -//----------------------------------------------------------------------------- -// Copyright (c) 2020-2022 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 search - -import ( - "fmt" - "strings" - - "zettelstore.de/z/domain/meta" -) - -type matchValueFunc func(value string) bool - -func matchValueNever(string) bool { return false } -func matchValueAlways(string) bool { return true } - -type matchSpec struct { - key string - match matchValueFunc -} - -// compileMeta calculates a selection func based on the given select criteria. -func (s *Search) compileMeta() MetaMatchFunc { - posSpecs, negSpecs, nomatch := s.createSelectSpecs() - if len(posSpecs) > 0 || len(negSpecs) > 0 || len(nomatch) > 0 { - return makeSearchMetaMatchFunc(posSpecs, negSpecs, nomatch) - } - return nil -} - -func (s *Search) createSelectSpecs() (posSpecs, negSpecs []matchSpec, nomatch []string) { - posSpecs = make([]matchSpec, 0, len(s.mvals)) - negSpecs = make([]matchSpec, 0, len(s.mvals)) - for key, values := range s.mvals { - if !meta.KeyIsValid(key) { - continue - } - if always, never := countEmptyValues(values); always+never > 0 { - if never == 0 { - posSpecs = append(posSpecs, matchSpec{key, matchValueAlways}) - continue - } - if always == 0 { - negSpecs = append(negSpecs, matchSpec{key, nil}) - continue - } - // value must match always AND never, at the same time. This results in a no-match. - nomatch = append(nomatch, key) - continue - } - posMatch, negMatch := createPosNegMatchFunc( - key, values, - func(val string, op compareOp) { s.addSearch(expValue{value: val, op: op, negate: false}) }) - if posMatch != nil { - posSpecs = append(posSpecs, matchSpec{key, posMatch}) - } - if negMatch != nil { - negSpecs = append(negSpecs, matchSpec{key, negMatch}) - } - } - return posSpecs, negSpecs, nomatch -} - -func countEmptyValues(values []expValue) (always, never int) { - for _, v := range values { - if v.value == "" { - if v.negate { - never++ - } else { - always++ - } - } - } - return always, never -} - -type addSearchFunc func(val string, op compareOp) - -func createPosNegMatchFunc(key string, values []expValue, addSearch addSearchFunc) (posMatch, negMatch matchValueFunc) { - posValues := make([]opValue, 0, len(values)) - negValues := make([]opValue, 0, len(values)) - for _, val := range values { - if val.negate { - negValues = append(negValues, opValue{value: val.value, op: val.op.negate()}) - } else { - posValues = append(posValues, opValue{value: val.value, op: val.op}) - } - } - return createMatchFunc(key, posValues, addSearch), createMatchFunc(key, negValues, addSearch) -} - -// opValue is an expValue, but w/o the field "negate" -type opValue struct { - value string - op compareOp -} - -func createMatchFunc(key string, values []opValue, addSearch addSearchFunc) matchValueFunc { - if len(values) == 0 { - return nil - } - switch meta.Type(key) { - case meta.TypeCredential: - return matchValueNever - case meta.TypeID, meta.TypeTimestamp: // ID and timestamp use the same layout - return createMatchIDFunc(values, addSearch) - case meta.TypeIDSet: - return createMatchIDSetFunc(values, addSearch) - case meta.TypeTagSet: - return createMatchTagSetFunc(values, addSearch) - case meta.TypeWord: - return createMatchWordFunc(values, addSearch) - case meta.TypeWordSet: - return createMatchWordSetFunc(values, addSearch) - } - return createMatchStringFunc(values, addSearch) -} - -func createMatchIDFunc(values []opValue, addSearch addSearchFunc) matchValueFunc { - preds := valuesToStringPredicates(values, cmpPrefix, addSearch) - return func(value string) bool { - for _, pred := range preds { - if !pred(value) { - return false - } - } - return true - } -} - -func createMatchIDSetFunc(values []opValue, addSearch addSearchFunc) matchValueFunc { - predList := valuesToStringSetPredicates(preprocessSet(values), cmpPrefix, addSearch) - return func(value string) bool { - ids := meta.ListFromValue(value) - for _, preds := range predList { - for _, pred := range preds { - if !pred(ids) { - return false - } - } - } - return true - } -} - -func createMatchTagSetFunc(values []opValue, addSearch addSearchFunc) matchValueFunc { - predList := valuesToStringSetPredicates(processTagSet(preprocessSet(sliceToLower(values))), cmpEqual, addSearch) - 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 _, preds := range predList { - for _, pred := range preds { - if !pred(tags) { - return false - } - } - } - return true - } -} - -func processTagSet(valueSet [][]opValue) [][]opValue { - result := make([][]opValue, len(valueSet)) - for i, values := range valueSet { - tags := make([]opValue, len(values)) - for j, val := range values { - if tval := val.value; tval != "" && tval[0] == '#' { - tval = meta.CleanTag(tval) - tags[j] = opValue{value: tval, op: resolveDefaultOp(val.op, cmpEqual)} - } else { - tags[j] = opValue{value: tval, op: resolveDefaultOp(val.op, cmpPrefix)} - } - } - result[i] = tags - } - return result -} - -func createMatchWordFunc(values []opValue, addSearch addSearchFunc) matchValueFunc { - preds := valuesToStringPredicates(sliceToLower(values), cmpEqual, addSearch) - return func(value string) bool { - value = strings.ToLower(value) - for _, pred := range preds { - if !pred(value) { - return false - } - } - return true - } -} - -func createMatchWordSetFunc(values []opValue, addSearch addSearchFunc) matchValueFunc { - predsList := valuesToStringSetPredicates(preprocessSet(sliceToLower(values)), cmpEqual, addSearch) - return func(value string) bool { - words := meta.ListFromValue(value) - for _, preds := range predsList { - for _, pred := range preds { - if !pred(words) { - return false - } - } - } - return true - } -} - -func createMatchStringFunc(values []opValue, addSearch addSearchFunc) matchValueFunc { - preds := valuesToStringPredicates(sliceToLower(values), cmpContains, addSearch) - return func(value string) bool { - value = strings.ToLower(value) - for _, pred := range preds { - if !pred(value) { - return false - } - } - return true - } -} - -func sliceToLower(sl []opValue) []opValue { - result := make([]opValue, 0, len(sl)) - for _, s := range sl { - result = append(result, opValue{ - value: strings.ToLower(s.value), - op: s.op, - }) - } - return result -} - -func preprocessSet(set []opValue) [][]opValue { - result := make([][]opValue, 0, len(set)) - for _, elem := range set { - splitElems := strings.Split(elem.value, ",") - valueElems := make([]opValue, 0, len(splitElems)) - for _, se := range splitElems { - e := strings.TrimSpace(se) - if len(e) > 0 { - valueElems = append(valueElems, opValue{value: e, op: elem.op}) - } - } - if len(valueElems) > 0 { - result = append(result, valueElems) - } - } - return result -} - -type stringPredicate func(string) bool - -func valuesToStringPredicates(values []opValue, defOp compareOp, addSearch addSearchFunc) []stringPredicate { - result := make([]stringPredicate, len(values)) - for i, v := range values { - opVal := v.value // loop variable is used in closure --> save needed value - op := resolveDefaultOp(v.op, defOp) - switch op { - case cmpEqual: - addSearch(opVal, op) // addSearch only for positive selections - result[i] = func(metaVal string) bool { return metaVal == opVal } - case cmpNotEqual: - result[i] = func(metaVal string) bool { return metaVal != opVal } - case cmpPrefix: - addSearch(opVal, op) - result[i] = func(metaVal string) bool { return strings.HasPrefix(metaVal, opVal) } - case cmpNoPrefix: - result[i] = func(metaVal string) bool { return !strings.HasPrefix(metaVal, opVal) } - case cmpSuffix: - addSearch(opVal, op) - result[i] = func(metaVal string) bool { return strings.HasSuffix(metaVal, opVal) } - case cmpNoSuffix: - result[i] = func(metaVal string) bool { return !strings.HasSuffix(metaVal, opVal) } - case cmpContains: - addSearch(opVal, op) - result[i] = func(metaVal string) bool { return strings.Contains(metaVal, opVal) } - case cmpNotContains: - result[i] = func(metaVal string) bool { return !strings.Contains(metaVal, opVal) } - default: - panic(fmt.Sprintf("Unknown compare operation %d/%d with value %q", op, v.op, opVal)) - } - } - return result -} - -type stringSetPredicate func(value []string) bool - -func valuesToStringSetPredicates(values [][]opValue, defOp compareOp, addSearch addSearchFunc) [][]stringSetPredicate { - result := make([][]stringSetPredicate, len(values)) - for i, val := range values { - elemPreds := make([]stringSetPredicate, len(val)) - for j, v := range val { - opVal := v.value // loop variable is used in closure --> save needed value - op := resolveDefaultOp(v.op, defOp) - switch op { - case cmpEqual: - addSearch(opVal, op) // addSearch only for positive selections - elemPreds[j] = makeStringSetPredicate(opVal, stringEqual, true) - case cmpNotEqual: - elemPreds[j] = makeStringSetPredicate(opVal, stringEqual, false) - case cmpPrefix: - addSearch(opVal, op) - elemPreds[j] = makeStringSetPredicate(opVal, strings.HasPrefix, true) - case cmpNoPrefix: - elemPreds[j] = makeStringSetPredicate(opVal, strings.HasPrefix, false) - case cmpSuffix: - addSearch(opVal, op) - elemPreds[j] = makeStringSetPredicate(opVal, strings.HasSuffix, true) - case cmpNoSuffix: - elemPreds[j] = makeStringSetPredicate(opVal, strings.HasSuffix, false) - case cmpContains: - addSearch(opVal, op) - elemPreds[j] = makeStringSetPredicate(opVal, strings.Contains, true) - case cmpNotContains: - elemPreds[j] = makeStringSetPredicate(opVal, strings.Contains, false) - default: - panic(fmt.Sprintf("Unknown compare operation %d/%d with value %q", op, v.op, opVal)) - } - } - result[i] = elemPreds - } - return result -} - -func stringEqual(val1, val2 string) bool { return val1 == val2 } - -type compareStringFunc func(val1, val2 string) bool - -func makeStringSetPredicate(neededValue string, compare compareStringFunc, foundResult bool) stringSetPredicate { - return func(metaVals []string) bool { - for _, metaVal := range metaVals { - if compare(metaVal, neededValue) { - return foundResult - } - } - return !foundResult - } -} - -func resolveDefaultOp(op, defOp compareOp) compareOp { - if op == cmpDefault { - return defOp - } - if op == cmpNotDefault { - return defOp.negate() - } - return op -} - -func makeSearchMetaMatchFunc(posSpecs, negSpecs []matchSpec, nomatch []string) MetaMatchFunc { - if len(nomatch) == 0 { - // Optimize for simple cases: only negative or only positive matching - - if len(posSpecs) == 0 { - return func(m *meta.Meta) bool { return matchMetaNegSpecs(m, negSpecs) } - } - if len(negSpecs) == 0 { - return func(m *meta.Meta) bool { return matchMetaPosSpecs(m, posSpecs) } - } - } - return func(m *meta.Meta) bool { - return matchMetaNoMatch(m, nomatch) && - matchMetaPosSpecs(m, posSpecs) && - matchMetaNegSpecs(m, negSpecs) - } -} - -func matchMetaNoMatch(m *meta.Meta, nomatch []string) bool { - for _, key := range nomatch { - if _, ok := m.Get(key); ok { - return false - } - } - return true -} -func matchMetaPosSpecs(m *meta.Meta, posSpecs []matchSpec) bool { - for _, s := range posSpecs { - if value, ok := m.Get(s.key); !ok || !s.match(value) { - return false - } - } - return true -} -func matchMetaNegSpecs(m *meta.Meta, negSpecs []matchSpec) bool { - for _, s := range negSpecs { - if s.match == nil { - if value, ok := m.Get(s.key); ok && matchValueAlways(value) { - return false - } - } else if value, ok := m.Get(s.key); !ok || !s.match(value) { - return false - } - } - return true -} DELETED search/select_test.go Index: search/select_test.go ================================================================== --- search/select_test.go +++ search/select_test.go @@ -1,46 +0,0 @@ -//----------------------------------------------------------------------------- -// Copyright (c) 2021 Detlef Stern -// -// This file is part of zettelstore. -// -// Zettelstore is licensed under the latest version of the EUPL (European Union -// Public License). Please see file LICENSE.txt for your rights and obligations -// under this license. -//----------------------------------------------------------------------------- - -package search_test - -import ( - "testing" - - "zettelstore.de/c/api" - "zettelstore.de/z/domain/id" - "zettelstore.de/z/domain/meta" - "zettelstore.de/z/search" -) - -func TestMatchZidNegate(t *testing.T) { - var s *search.Search - s = s.AddExpr(api.KeyID, "!="+string(api.ZidVersion)) - s = s.AddExpr(api.KeyID, "!="+string(api.ZidLicense)) - _, matchFunc := s.RetrieveAndCompileMatch(nil) - - testCases := []struct { - zid api.ZettelID - exp bool - }{ - {api.ZidVersion, false}, - {api.ZidLicense, false}, - {api.ZidAuthors, true}, - } - for i, tc := range testCases { - m := meta.New(id.MustParse(tc.zid)) - if matchFunc(m) != tc.exp { - if tc.exp { - t.Errorf("%d: meta %v must match %q", i, m.Zid, s) - } else { - t.Errorf("%d: meta %v must not match %q", i, m.Zid, s) - } - } - } -} DELETED search/sorter.go Index: search/sorter.go ================================================================== --- search/sorter.go +++ search/sorter.go @@ -1,74 +0,0 @@ -//----------------------------------------------------------------------------- -// Copyright (c) 2020-2022 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 search provides a zettel search. -package search - -import ( - "strconv" - - "zettelstore.de/c/api" - "zettelstore.de/z/domain/meta" -) - -type sortFunc func(i, j int) bool - -func createSortFunc(key string, descending bool, ml []*meta.Meta) sortFunc { - keyType := meta.Type(key) - if key == api.KeyID || keyType == meta.TypeCredential { - if 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.TypeNumber { - return createSortNumberFunc(ml, key, descending) - } - return createSortStringFunc(ml, key, descending) -} - -func createSortNumberFunc(ml []*meta.Meta, key string, descending bool) sortFunc { - if descending { - return func(i, j int) bool { - iVal, iOk := getNum(ml[i], key) - jVal, jOk := getNum(ml[j], key) - return (iOk && (!jOk || iVal > jVal)) || !jOk - } - } - return func(i, j int) bool { - iVal, iOk := getNum(ml[i], key) - jVal, jOk := getNum(ml[j], key) - return (iOk && (!jOk || iVal < jVal)) || !jOk - } -} - -func createSortStringFunc(ml []*meta.Meta, key string, descending bool) sortFunc { - if descending { - return func(i, j int) bool { - iVal, iOk := ml[i].Get(key) - jVal, jOk := ml[j].Get(key) - return (iOk && (!jOk || iVal > jVal)) || !jOk - } - } - return func(i, j int) bool { - iVal, iOk := ml[i].Get(key) - jVal, jOk := ml[j].Get(key) - return (iOk && (!jOk || iVal < jVal)) || !jOk - } -} - -func getNum(m *meta.Meta, key string) (int64, bool) { - if s, ok := m.Get(key); ok { - if i, err := strconv.ParseInt(s, 10, 64); err == nil { - return i, true - } - } - return 0, false -} Index: strfun/slugify.go ================================================================== --- strfun/slugify.go +++ strfun/slugify.go @@ -1,16 +1,15 @@ //----------------------------------------------------------------------------- -// 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. //----------------------------------------------------------------------------- -// Package strfun provides some string functions. package strfun import ( "strings" "unicode" @@ -17,12 +16,11 @@ "golang.org/x/text/unicode/norm" ) // NormalizeWords produces a word list that is normalized for better searching. -func NormalizeWords(s string) []string { - result := make([]string, 0, 1) +func NormalizeWords(s string) (result []string) { word := make([]rune, 0, len(s)) for _, r := range norm.NFKD.String(s) { if unicode.Is(unicode.Diacritic, r) { continue } ADDED testdata/naughty/LICENSE Index: testdata/naughty/LICENSE ================================================================== --- testdata/naughty/LICENSE +++ testdata/naughty/LICENSE @@ -0,0 +1,22 @@ +The MIT License (MIT) + +Copyright (c) 2015-2020 Max Woolf + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. + ADDED testdata/naughty/README.md Index: testdata/naughty/README.md ================================================================== --- testdata/naughty/README.md +++ testdata/naughty/README.md @@ -0,0 +1,6 @@ +# Big List of Naughty Strings + +A list of strings which have a high probability of causing issues when used as user-input data. + +* Source: https://github.com/minimaxir/big-list-of-naughty-strings +* License: MIT, (c) 2015-2020 Max Woolf (see file LICENSE) ADDED testdata/naughty/blns.txt Index: testdata/naughty/blns.txt ================================================================== --- testdata/naughty/blns.txt +++ testdata/naughty/blns.txt @@ -0,0 +1,742 @@ +# Reserved Strings +# +# Strings which may be used elsewhere in code + +undefined +undef +null +NULL +(null) +nil +NIL +true +false +True +False +TRUE +FALSE +None +hasOwnProperty +then +constructor +\ +\\ + +# Numeric Strings +# +# Strings which can be interpreted as numeric + +0 +1 +1.00 +$1.00 +1/2 +1E2 +1E02 +1E+02 +-1 +-1.00 +-$1.00 +-1/2 +-1E2 +-1E02 +-1E+02 +1/0 +0/0 +-2147483648/-1 +-9223372036854775808/-1 +-0 +-0.0 ++0 ++0.0 +0.00 +0..0 +. +0.0.0 +0,00 +0,,0 +, +0,0,0 +0.0/0 +1.0/0.0 +0.0/0.0 +1,0/0,0 +0,0/0,0 +--1 +- +-. +-, +999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999 +NaN +Infinity +-Infinity +INF +1#INF +-1#IND +1#QNAN +1#SNAN +1#IND +0x0 +0xffffffff +0xffffffffffffffff +0xabad1dea +123456789012345678901234567890123456789 +1,000.00 +1 000.00 +1'000.00 +1,000,000.00 +1 000 000.00 +1'000'000.00 +1.000,00 +1 000,00 +1'000,00 +1.000.000,00 +1 000 000,00 +1'000'000,00 +01000 +08 +09 +2.2250738585072011e-308 + +# Special Characters +# +# ASCII punctuation. All of these characters may need to be escaped in some +# contexts. Divided into three groups based on (US-layout) keyboard position. + +,./;'[]\-= +<>?:"{}|_+ +!@#$%^&*()`~ + +# Non-whitespace C0 controls: U+0001 through U+0008, U+000E through U+001F, +# and U+007F (DEL) +# Often forbidden to appear in various text-based file formats (e.g. XML), +# or reused for internal delimiters on the theory that they should never +# appear in input. +# The next line may appear to be blank or mojibake in some viewers. + + +# Non-whitespace C1 controls: U+0080 through U+0084 and U+0086 through U+009F. +# Commonly misinterpreted as additional graphic characters. +# The next line may appear to be blank, mojibake, or dingbats in some viewers. +€‚ƒ„†‡ˆ‰Š‹ŒŽ‘’“”•–—˜™š›œžŸ + +# Whitespace: all of the characters with category Zs, Zl, or Zp (in Unicode +# version 8.0.0), plus U+0009 (HT), U+000B (VT), U+000C (FF), U+0085 (NEL), +# and U+200B (ZERO WIDTH SPACE), which are in the C categories but are often +# treated as whitespace in some contexts. +# This file unfortunately cannot express strings containing +# U+0000, U+000A, or U+000D (NUL, LF, CR). +# The next line may appear to be blank or mojibake in some viewers. +# The next line may be flagged for "trailing whitespace" in some viewers. + …             ​

    + +# Unicode additional control characters: all of the characters with +# general category Cf (in Unicode 8.0.0). +# The next line may appear to be blank or mojibake in some viewers. +­؀؁؂؃؄؅؜۝܏᠎​‌‍‎‏‪‫‬‭‮⁠⁡⁢⁣⁤⁦⁧⁨⁩𑂽𛲠𛲡𛲢𛲣𝅳𝅴𝅵𝅶𝅷𝅸𝅹𝅺󠀁󠀠󠀡󠀢󠀣󠀤󠀥󠀦󠀧󠀨󠀩󠀪󠀫󠀬󠀭󠀮󠀯󠀰󠀱󠀲󠀳󠀴󠀵󠀶󠀷󠀸󠀹󠀺󠀻󠀼󠀽󠀾󠀿󠁀󠁁󠁂󠁃󠁄󠁅󠁆󠁇󠁈󠁉󠁊󠁋󠁌󠁍󠁎󠁏󠁐󠁑󠁒󠁓󠁔󠁕󠁖󠁗󠁘󠁙󠁚󠁛󠁜󠁝󠁞󠁟󠁠󠁡󠁢󠁣󠁤󠁥󠁦󠁧󠁨󠁩󠁪󠁫󠁬󠁭󠁮󠁯󠁰󠁱󠁲󠁳󠁴󠁵󠁶󠁷󠁸󠁹󠁺󠁻󠁼󠁽󠁾󠁿 + +# "Byte order marks", U+FEFF and U+FFFE, each on its own line. +# The next two lines may appear to be blank or mojibake in some viewers. + +￾ + +# Unicode Symbols +# +# Strings which contain common unicode symbols (e.g. smart quotes) + +Ω≈ç√∫˜µ≤≥÷ +åß∂ƒ©˙∆˚¬…æ +œ∑´®†¥¨ˆøπ“‘ +¡™£¢∞§¶•ªº–≠ +¸˛Ç◊ı˜Â¯˘¿ +ÅÍÎÏ˝ÓÔÒÚÆ☃ +Œ„´‰ˇÁ¨ˆØ∏”’ +`⁄€‹›fifl‡°·‚—± +⅛⅜⅝⅞ +ЁЂЃЄЅІЇЈЉЊЋЌЍЎЏАБВГДЕЖЗИЙКЛМНОПРСТУФХЦЧШЩЪЫЬЭЮЯабвгдежзийклмнопрстуфхцчшщъыьэюя +٠١٢٣٤٥٦٧٨٩ + +# Unicode Subscript/Superscript/Accents +# +# Strings which contain unicode subscripts/superscripts; can cause rendering issues + +⁰⁴⁵ +₀₁₂ +⁰⁴⁵₀₁₂ +ด้้้้้็็็็็้้้้้็็็็็้้้้้้้้็็็็็้้้้้็็็็็้้้้้้้้็็็็็้้้้้็็็็็้้้้้้้้็็็็็้้้้้็็็็ ด้้้้้็็็็็้้้้้็็็็็้้้้้้้้็็็็็้้้้้็็็็็้้้้้้้้็็็็็้้้้้็็็็็้้้้้้้้็็็็็้้้้้็็็็ ด้้้้้็็็็็้้้้้็็็็็้้้้้้้้็็็็็้้้้้็็็็็้้้้้้้้็็็็็้้้้้็็็็็้้้้้้้้็็็็็้้้้้็็็็ + +# Quotation Marks +# +# Strings which contain misplaced quotation marks; can cause encoding errors + +' +" +'' +"" +'"' +"''''"'" +"'"'"''''" + + + + + +# Two-Byte Characters +# +# Strings which contain two-byte characters: can cause rendering issues or character-length issues + +田中さんにあげて下さい +パーティーへ行かないか +和製漢語 +部落格 +사회과학원 어학연구소 +찦차를 타고 온 펲시맨과 쑛다리 똠방각하 +社會科學院語學研究所 +울란바토르 +𠜎𠜱𠝹𠱓𠱸𠲖𠳏 + +# Strings which contain two-byte letters: can cause issues with naïve UTF-16 capitalizers which think that 16 bits == 1 character + +𐐜 𐐔𐐇𐐝𐐀𐐡𐐇𐐓 𐐙𐐊𐐡𐐝𐐓/𐐝𐐇𐐗𐐊𐐤𐐔 𐐒𐐋𐐗 𐐒𐐌 𐐜 𐐡𐐀𐐖𐐇𐐤𐐓𐐝 𐐱𐑂 𐑄 𐐔𐐇𐐝𐐀𐐡𐐇𐐓 𐐏𐐆𐐅𐐤𐐆𐐚𐐊𐐡𐐝𐐆𐐓𐐆 + +# Special Unicode Characters Union +# +# A super string recommended by VMware Inc. Globalization Team: can effectively cause rendering issues or character-length issues to validate product globalization readiness. +# +# 表 CJK_UNIFIED_IDEOGRAPHS (U+8868) +# ポ KATAKANA LETTER PO (U+30DD) +# あ HIRAGANA LETTER A (U+3042) +# A LATIN CAPITAL LETTER A (U+0041) +# 鷗 CJK_UNIFIED_IDEOGRAPHS (U+9DD7) +# Œ LATIN SMALL LIGATURE OE (U+0153) +# é LATIN SMALL LETTER E WITH ACUTE (U+00E9) +# B FULLWIDTH LATIN CAPITAL LETTER B (U+FF22) +# 逍 CJK_UNIFIED_IDEOGRAPHS (U+900D) +# Ü LATIN SMALL LETTER U WITH DIAERESIS (U+00FC) +# ß LATIN SMALL LETTER SHARP S (U+00DF) +# ª FEMININE ORDINAL INDICATOR (U+00AA) +# ą LATIN SMALL LETTER A WITH OGONEK (U+0105) +# ñ LATIN SMALL LETTER N WITH TILDE (U+00F1) +# 丂 CJK_UNIFIED_IDEOGRAPHS (U+4E02) +# 㐀 CJK Ideograph Extension A, First (U+3400) +# 𠀀 CJK Ideograph Extension B, First (U+20000) + +表ポあA鷗ŒéB逍Üߪąñ丂㐀𠀀 + +# Changing length when lowercased +# +# Characters which increase in length (2 to 3 bytes) when lowercased +# Credit: https://twitter.com/jifa/status/625776454479970304 + +Ⱥ +Ⱦ + +# Japanese Emoticons +# +# Strings which consists of Japanese-style emoticons which are popular on the web + +ヽ༼ຈل͜ຈ༽ノ ヽ༼ຈل͜ຈ༽ノ +(。◕ ∀ ◕。) +`ィ(´∀`∩ +__ロ(,_,*) +・( ̄∀ ̄)・:*: +゚・✿ヾ╲(。◕‿◕。)╱✿・゚ +,。・:*:・゜’( ☻ ω ☻ )。・:*:・゜’ +(╯°□°)╯︵ ┻━┻) +(ノಥ益ಥ)ノ ┻━┻ +┬─┬ノ( º _ ºノ) +( ͡° ͜ʖ ͡°) +¯\_(ツ)_/¯ + +# Emoji +# +# Strings which contain Emoji; should be the same behavior as two-byte characters, but not always + +😍 +👩🏽 +👨‍🦰 👨🏿‍🦰 👨‍🦱 👨🏿‍🦱 🦹🏿‍♂️ +👾 🙇 💁 🙅 🙆 🙋 🙎 🙍 +🐵 🙈 🙉 🙊 +❤️ 💔 💌 💕 💞 💓 💗 💖 💘 💝 💟 💜 💛 💚 💙 +✋🏿 💪🏿 👐🏿 🙌🏿 👏🏿 🙏🏿 +👨‍👩‍👦 👨‍👩‍👧‍👦 👨‍👨‍👦 👩‍👩‍👧 👨‍👦 👨‍👧‍👦 👩‍👦 👩‍👧‍👦 +🚾 🆒 🆓 🆕 🆖 🆗 🆙 🏧 +0️⃣ 1️⃣ 2️⃣ 3️⃣ 4️⃣ 5️⃣ 6️⃣ 7️⃣ 8️⃣ 9️⃣ 🔟 + +# Regional Indicator Symbols +# +# Regional Indicator Symbols can be displayed differently across +# fonts, and have a number of special behaviors + +🇺🇸🇷🇺🇸 🇦🇫🇦🇲🇸 +🇺🇸🇷🇺🇸🇦🇫🇦🇲 +🇺🇸🇷🇺🇸🇦 + +# Unicode Numbers +# +# Strings which contain unicode numbers; if the code is localized, it should see the input as numeric + +123 +١٢٣ + +# Right-To-Left Strings +# +# Strings which contain text that should be rendered RTL if possible (e.g. Arabic, Hebrew) + +ثم نفس سقطت وبالتحديد،, جزيرتي باستخدام أن دنو. إذ هنا؟ الستار وتنصيب كان. أهّل ايطاليا، بريطانيا-فرنسا قد أخذ. سليمان، إتفاقية بين ما, يذكر الحدود أي بعد, معاملة بولندا، الإطلاق عل إيو. +בְּרֵאשִׁית, בָּרָא אֱלֹהִים, אֵת הַשָּׁמַיִם, וְאֵת הָאָרֶץ +הָיְתָהtestالصفحات التّحول +﷽ +ﷺ +مُنَاقَشَةُ سُبُلِ اِسْتِخْدَامِ اللُّغَةِ فِي النُّظُمِ الْقَائِمَةِ وَفِيم يَخُصَّ التَّطْبِيقَاتُ الْحاسُوبِيَّةُ، +الكل في المجمو عة (5) + +# Ogham Text +# +# The only unicode alphabet to use a space which isn't empty but should still act like a space. + +᚛ᚄᚓᚐᚋᚒᚄ ᚑᚄᚂᚑᚏᚅ᚜ +᚛                 ᚜ + +# Trick Unicode +# +# Strings which contain unicode with unusual properties (e.g. Right-to-left override) (c.f. http://www.unicode.org/charts/PDF/U2000.pdf) + +‪‪test‪ +‫test‫ +
test
 +test⁠test‫ +⁦test⁧ + +# Zalgo Text +# +# Strings which contain "corrupted" text. The corruption will not appear in non-HTML text, however. (via http://www.eeemo.net) + +Ṱ̺̺̕o͞ ̷i̲̬͇̪͙n̝̗͕v̟̜̘̦͟o̶̙̰̠kè͚̮̺̪̹̱̤ ̖t̝͕̳̣̻̪͞h̼͓̲̦̳̘̲e͇̣̰̦̬͎ ̢̼̻̱̘h͚͎͙̜̣̲ͅi̦̲̣̰̤v̻͍e̺̭̳̪̰-m̢iͅn̖̺̞̲̯̰d̵̼̟͙̩̼̘̳ ̞̥̱̳̭r̛̗̘e͙p͠r̼̞̻̭̗e̺̠̣͟s̘͇̳͍̝͉e͉̥̯̞̲͚̬͜ǹ̬͎͎̟̖͇̤t͍̬̤͓̼̭͘ͅi̪̱n͠g̴͉ ͏͉ͅc̬̟h͡a̫̻̯͘o̫̟̖͍̙̝͉s̗̦̲.̨̹͈̣ +̡͓̞ͅI̗̘̦͝n͇͇͙v̮̫ok̲̫̙͈i̖͙̭̹̠̞n̡̻̮̣̺g̲͈͙̭͙̬͎ ̰t͔̦h̞̲e̢̤ ͍̬̲͖f̴̘͕̣è͖ẹ̥̩l͖͔͚i͓͚̦͠n͖͍̗͓̳̮g͍ ̨o͚̪͡f̘̣̬ ̖̘͖̟͙̮c҉͔̫͖͓͇͖ͅh̵̤̣͚͔á̗̼͕ͅo̼̣̥s̱͈̺̖̦̻͢.̛̖̞̠̫̰ +̗̺͖̹̯͓Ṯ̤͍̥͇͈h̲́e͏͓̼̗̙̼̣͔ ͇̜̱̠͓͍ͅN͕͠e̗̱z̘̝̜̺͙p̤̺̹͍̯͚e̠̻̠͜r̨̤͍̺̖͔̖̖d̠̟̭̬̝͟i̦͖̩͓͔̤a̠̗̬͉̙n͚͜ ̻̞̰͚ͅh̵͉i̳̞v̢͇ḙ͎͟-҉̭̩̼͔m̤̭̫i͕͇̝̦n̗͙ḍ̟ ̯̲͕͞ǫ̟̯̰̲͙̻̝f ̪̰̰̗̖̭̘͘c̦͍̲̞͍̩̙ḥ͚a̮͎̟̙͜ơ̩̹͎s̤.̝̝ ҉Z̡̖̜͖̰̣͉̜a͖̰͙̬͡l̲̫̳͍̩g̡̟̼̱͚̞̬ͅo̗͜.̟ +̦H̬̤̗̤͝e͜ ̜̥̝̻͍̟́w̕h̖̯͓o̝͙̖͎̱̮ ҉̺̙̞̟͈W̷̼̭a̺̪͍į͈͕̭͙̯̜t̶̼̮s̘͙͖̕ ̠̫̠B̻͍͙͉̳ͅe̵h̵̬͇̫͙i̹͓̳̳̮͎̫̕n͟d̴̪̜̖ ̰͉̩͇͙̲͞ͅT͖̼͓̪͢h͏͓̮̻e̬̝̟ͅ ̤̹̝W͙̞̝͔͇͝ͅa͏͓͔̹̼̣l̴͔̰̤̟͔ḽ̫.͕ +Z̮̞̠͙͔ͅḀ̗̞͈̻̗Ḷ͙͎̯̹̞͓G̻O̭̗̮ + +# Unicode Upsidedown +# +# Strings which contain unicode with an "upsidedown" effect (via http://www.upsidedowntext.com) + +˙ɐnbᴉlɐ ɐuƃɐɯ ǝɹolop ʇǝ ǝɹoqɐl ʇn ʇunpᴉpᴉɔuᴉ ɹodɯǝʇ poɯsnᴉǝ op pǝs 'ʇᴉlǝ ƃuᴉɔsᴉdᴉpɐ ɹnʇǝʇɔǝsuoɔ 'ʇǝɯɐ ʇᴉs ɹolop ɯnsdᴉ ɯǝɹo˥ +00˙Ɩ$- + +# Unicode font +# +# Strings which contain bold/italic/etc. versions of normal characters + +The quick brown fox jumps over the lazy dog +𝐓𝐡𝐞 𝐪𝐮𝐢𝐜𝐤 𝐛𝐫𝐨𝐰𝐧 𝐟𝐨𝐱 𝐣𝐮𝐦𝐩𝐬 𝐨𝐯𝐞𝐫 𝐭𝐡𝐞 𝐥𝐚𝐳𝐲 𝐝𝐨𝐠 +𝕿𝖍𝖊 𝖖𝖚𝖎𝖈𝖐 𝖇𝖗𝖔𝖜𝖓 𝖋𝖔𝖝 𝖏𝖚𝖒𝖕𝖘 𝖔𝖛𝖊𝖗 𝖙𝖍𝖊 𝖑𝖆𝖟𝖞 𝖉𝖔𝖌 +𝑻𝒉𝒆 𝒒𝒖𝒊𝒄𝒌 𝒃𝒓𝒐𝒘𝒏 𝒇𝒐𝒙 𝒋𝒖𝒎𝒑𝒔 𝒐𝒗𝒆𝒓 𝒕𝒉𝒆 𝒍𝒂𝒛𝒚 𝒅𝒐𝒈 +𝓣𝓱𝓮 𝓺𝓾𝓲𝓬𝓴 𝓫𝓻𝓸𝔀𝓷 𝓯𝓸𝔁 𝓳𝓾𝓶𝓹𝓼 𝓸𝓿𝓮𝓻 𝓽𝓱𝓮 𝓵𝓪𝔃𝔂 𝓭𝓸𝓰 +𝕋𝕙𝕖 𝕢𝕦𝕚𝕔𝕜 𝕓𝕣𝕠𝕨𝕟 𝕗𝕠𝕩 𝕛𝕦𝕞𝕡𝕤 𝕠𝕧𝕖𝕣 𝕥𝕙𝕖 𝕝𝕒𝕫𝕪 𝕕𝕠𝕘 +𝚃𝚑𝚎 𝚚𝚞𝚒𝚌𝚔 𝚋𝚛𝚘𝚠𝚗 𝚏𝚘𝚡 𝚓𝚞𝚖𝚙𝚜 𝚘𝚟𝚎𝚛 𝚝𝚑𝚎 𝚕𝚊𝚣𝚢 𝚍𝚘𝚐 +⒯⒣⒠ ⒬⒰⒤⒞⒦ ⒝⒭⒪⒲⒩ ⒡⒪⒳ ⒥⒰⒨⒫⒮ ⒪⒱⒠⒭ ⒯⒣⒠ ⒧⒜⒵⒴ ⒟⒪⒢ + +# Script Injection +# +# Strings which attempt to invoke a benign script injection; shows vulnerability to XSS + + +<script>alert('1');</script> + + +"> +'> +> + +< / script >< script >alert(8)< / script > + onfocus=JaVaSCript:alert(9) autofocus +" onfocus=JaVaSCript:alert(10) autofocus +' onfocus=JaVaSCript:alert(11) autofocus +<script>alert(12)</script> +ript>alert(13)ript> +--> +";alert(15);t=" +';alert(16);t=' +JavaSCript:alert(17) +;alert(18); +src=JaVaSCript:prompt(19) +">javascript:alert(25); +javascript:alert(26); +javascript:alert(27); +javascript:alert(28); +javascript:alert(29); +javascript:alert(30); +javascript:alert(31); +'`"><\x3Cscript>javascript:alert(32) +'`"><\x00script>javascript:alert(33) +ABC
    DEF +ABC
    DEF +ABC
    DEF +ABC
    DEF +ABC
    DEF +ABC
    DEF +ABC
    DEF +ABC
    DEF +ABC
    DEF +ABC
    DEF +ABC
    DEF +ABC
    DEF +ABC
    DEF +ABC
    DEF +ABC
    DEF +ABC
    DEF +ABC
    DEF +ABC
    DEF +ABC
    DEF +ABC
    DEF +ABC
    DEF +ABC
    DEF +ABC
    DEF +ABC
    DEF +ABC
    DEF +ABC
    DEF +ABC
    DEF +test +test +test +test +test +test +test +test +test +test +test +test +test +test +test +test +test +test +test +test +test +test +test +test +test +test +test +test +test +test +test +test +test +test +test +test +test +test +test +test +test +test +test +test +test +test +test +test +test +test +test +test +test +test +test +test +test +`"'> +`"'> +`"'> +`"'> +`"'> +`"'> +`"'> +`"'> +`"'> +`"'> +"`'> +"`'> +"`'> +"`'> +"`'> +"`'> +"`'> +"`'> +"`'> +"`'> +"`'> +"`'> +"`'> +"`'> +"`'> +"`'> +"`'> +"`'> +"`'> +"`'> +"`'> +"`'> +"`'> +"`'> +"`'> +"`'> +"`'> +"`'> +"`'> +"`'> +"`'> +"`'> +"`'> +"`'> +"`'> +"`'> +"`'> + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +XXX + + + +<a href=http://foo.bar/#x=`y></a><img alt="`><img src=x:x onerror=javascript:alert(203)></a>"> +<!--[if]><script>javascript:alert(204)</script --> +<!--[if<img src=x onerror=javascript:alert(205)//]> --> +<script src="/\%(jscript)s"></script> +<script src="\\%(jscript)s"></script> +<IMG """><SCRIPT>alert("206")</SCRIPT>"> +<IMG SRC=javascript:alert(String.fromCharCode(50,48,55))> +<IMG SRC=# onmouseover="alert('208')"> +<IMG SRC= onmouseover="alert('209')"> +<IMG onmouseover="alert('210')"> +<IMG SRC=javascript:alert('211')> +<IMG SRC=javascript:alert('212')> +<IMG SRC=javascript:alert('213')> +<IMG SRC="jav   ascript:alert('214');"> +<IMG SRC="jav ascript:alert('215');"> +<IMG SRC="jav ascript:alert('216');"> +<IMG SRC="jav ascript:alert('217');"> +perl -e 'print "<IMG SRC=java\0script:alert(\"218\")>";' > out +<IMG SRC="   javascript:alert('219');"> +<SCRIPT/XSS SRC="http://ha.ckers.org/xss.js"></SCRIPT> +<BODY onload!#$%&()*~+-_.,:;?@[/|\]^`=alert("220")> +<SCRIPT/SRC="http://ha.ckers.org/xss.js"></SCRIPT> +<<SCRIPT>alert("221");//<</SCRIPT> +<SCRIPT SRC=http://ha.ckers.org/xss.js?< B > +<SCRIPT SRC=//ha.ckers.org/.j> +<IMG SRC="javascript:alert('222')" +<iframe src=http://ha.ckers.org/scriptlet.html < +\";alert('223');// +<u oncopy=alert()> Copy me</u> +<i onwheel=alert(224)> Scroll over me </i> +<plaintext> +http://a/%%30%30 +</textarea><script>alert(225)</script> + +# SQL Injection +# +# Strings which can cause a SQL injection if inputs are not sanitized + +1;DROP TABLE users +1'; DROP TABLE users-- 1 +' OR 1=1 -- 1 +' OR '1'='1 +'; EXEC sp_MSForEachTable 'DROP TABLE ?'; -- + +% +_ + +# Server Code Injection +# +# Strings which can cause user to run code on server as a privileged user (c.f. https://news.ycombinator.com/item?id=7665153) + +- +-- +--version +--help +$USER +/dev/null; touch /tmp/blns.fail ; echo +`touch /tmp/blns.fail` +$(touch /tmp/blns.fail) +@{[system "touch /tmp/blns.fail"]} + +# Command Injection (Ruby) +# +# Strings which can call system commands within Ruby/Rails applications + +eval("puts 'hello world'") +System("ls -al /") +`ls -al /` +Kernel.exec("ls -al /") +Kernel.exit(1) +%x('ls -al /') + +# XXE Injection (XML) +# +# String which can reveal system files when parsed by a badly configured XML parser + +<?xml version="1.0" encoding="ISO-8859-1"?><!DOCTYPE foo [ <!ELEMENT foo ANY ><!ENTITY xxe SYSTEM "file:///etc/passwd" >]><foo>&xxe;</foo> + +# Unwanted Interpolation +# +# Strings which can be accidentally expanded into different strings if evaluated in the wrong context, e.g. used as a printf format string or via Perl or shell eval. Might expose sensitive data from the program doing the interpolation, or might just represent the wrong string. + +$HOME +$ENV{'HOME'} +%d +%s%s%s%s%s +{0} +%*.*s +%@ +%n +File:/// + +# File Inclusion +# +# Strings which can cause user to pull in files that should not be a part of a web server + +../../../../../../../../../../../etc/passwd%00 +../../../../../../../../../../../etc/hosts + +# Known CVEs and Vulnerabilities +# +# Strings that test for known vulnerabilities + +() { 0; }; touch /tmp/blns.shellshock1.fail; +() { _; } >_[$($())] { touch /tmp/blns.shellshock2.fail; } +<<< %s(un='%s') = %u ++++ATH0 + +# MSDOS/Windows Special Filenames +# +# Strings which are reserved characters in MSDOS/Windows + +CON +PRN +AUX +CLOCK$ +NUL +A: +ZZ: +COM1 +LPT1 +LPT2 +LPT3 +COM2 +COM3 +COM4 + +# IRC specific strings +# +# Strings that may occur on IRC clients that make security products freak out + +DCC SEND STARTKEYLOGGER 0 0 0 + +# Scunthorpe Problem +# +# Innocuous strings which may be blocked by profanity filters (https://en.wikipedia.org/wiki/Scunthorpe_problem) + +Scunthorpe General Hospital +Penistone Community Church +Lightwater Country Park +Jimmy Clitheroe +Horniman Museum +shitake mushrooms +RomansInSussex.co.uk +http://www.cum.qc.ca/ +Craig Cockburn, Software Specialist +Linda Callahan +Dr. Herman I. Libshitz +magna cum laude +Super Bowl XXX +medieval erection of parapets +evaluate +mocha +expression +Arsenal canal +classic +Tyson Gay +Dick Van Dyke +basement + +# Human injection +# +# Strings which may cause human to reinterpret worldview + +If you're reading this, you've been in a coma for almost 20 years now. We're trying a new technique. We don't know where this message will end up in your dream, but we hope it works. Please wake up, we miss you. + +# Terminal escape codes +# +# Strings which punish the fools who use cat/type on this file + +Roses are red, violets are blue. Hope you enjoy terminal hue +But now...for my greatest trick... +The quick brown fox... [Beeeep] + +# iOS Vulnerabilities +# +# Strings which crashed iMessage in various versions of iOS + +Powerلُلُصّبُلُلصّبُررً ॣ ॣh ॣ ॣ冗 +🏳0🌈️ +జ్ఞ‌ా + +# Persian special characters +# +# This is a four characters string which includes Persian special characters (گچپژ) + +گچپژ + +# jinja2 injection +# +# first one is supposed to raise "MemoryError" exception +# second, obviously, prints contents of /etc/passwd + +{% print 'x' * 64 * 1024**3 %} +{{ "".__class__.__mro__[2].__subclasses__()[40]("/etc/passwd").read() }} Index: tests/client/client_test.go ================================================================== --- tests/client/client_test.go +++ tests/client/client_test.go @@ -47,14 +47,14 @@ } } func TestListZettel(t *testing.T) { const ( - ownerZettel = 49 - configRoleZettel = 31 - writerZettel = ownerZettel - 25 - readerZettel = ownerZettel - 25 + ownerZettel = 47 + configRoleZettel = 29 + writerZettel = ownerZettel - 23 + readerZettel = ownerZettel - 23 creatorZettel = 7 publicZettel = 4 ) testdata := []struct { @@ -68,15 +68,14 @@ {"owner", ownerZettel}, } 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) - q, h, l, err := c.ListZettelJSON(context.Background(), query) + q, h, l, err := c.ListZettelJSON(context.Background(), "") if err != nil { tt.Error(err) return } if q != "" { @@ -89,29 +88,29 @@ if got != tc.exp { tt.Errorf("List of length %d expected, but got %d\n%v", tc.exp, got, l) } }) } - q, h, l, err := c.ListZettelJSON(context.Background(), url.Values{api.KeyRole: {api.ValueRoleConfiguration}}) + q, h, l, err := c.ListZettelJSON(context.Background(), api.KeyRole+api.SearchOperatorHas+api.ValueRoleConfiguration) if err != nil { t.Error(err) return } expQ := "role:configuration" if q != expQ { t.Errorf("Query should be %q, but is %q", expQ, q) } - expH := "role MATCH configuration" + expH := "role HAS configuration" if h != expH { t.Errorf("Human should be %q, but is %q", expH, h) } got := len(l) if got != configRoleZettel { t.Errorf("List of length %d expected, but got %d\n%v", configRoleZettel, got, l) } - pl, err := c.ListZettel(context.Background(), url.Values{api.KeyRole: {api.ValueRoleConfiguration}}) + pl, err := c.ListZettel(context.Background(), api.KeyRole+api.SearchOperatorHas+api.ValueRoleConfiguration) if err != nil { t.Error(err) return } compareZettelList(t, pl, l) @@ -338,11 +337,11 @@ func TestListTags(t *testing.T) { t.Parallel() c := getClient() c.SetAuth("owner", "owner") - tm, err := c.ListMapMeta(context.Background(), api.KeyTags) + tm, err := c.QueryMapMeta(context.Background(), api.ActionSeparator+api.KeyTags) if err != nil { t.Error(err) return } tags := []struct { @@ -372,11 +371,11 @@ func TestListRoles(t *testing.T) { t.Parallel() c := getClient() c.SetAuth("owner", "owner") - rl, err := c.ListMapMeta(context.Background(), api.KeyRole) + rl, err := c.QueryMapMeta(context.Background(), api.ActionSeparator+api.KeyRole) if err != nil { t.Error(err) return } exp := []string{"configuration", "user", "zettel"} Index: tests/markdown_test.go ================================================================== --- tests/markdown_test.go +++ tests/markdown_test.go @@ -6,11 +6,10 @@ // 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 tests provides some higher-level tests. package tests import ( "bytes" "encoding/json" @@ -66,15 +65,19 @@ if err = json.Unmarshal(content, &testcases); err != nil { panic(err) } for _, tc := range testcases { - ast := parser.ParseBlocks(input.NewInput([]byte(tc.Markdown)), nil, "markdown") + ast := createMDBlockSlice(tc.Markdown) testAllEncodings(t, tc, &ast) testZmkEncoding(t, tc, &ast) } } + +func createMDBlockSlice(markdown string) ast.BlockSlice { + return parser.ParseBlocks(input.NewInput([]byte(markdown)), nil, "markdown") +} func testAllEncodings(t *testing.T, tc markdownTestCase, ast *ast.BlockSlice) { var buf bytes.Buffer testID := tc.Example*100 + 1 for _, enc := range encodings { @@ -113,5 +116,25 @@ if gotSecond != gotThird { st.Errorf("\n1st: %q\n2nd: %q", gotSecond, gotThird) } }) } + +func TestAdditionalMarkdown(t *testing.T) { + testcases := []struct { + md string + exp string + }{ + {`abc<br>def`, `abc@@<br>@@{="html"}def`}, + } + zmkEncoder := encoder.Create(api.EncoderZmk) + var buf bytes.Buffer + for i, tc := range testcases { + ast := createMDBlockSlice(tc.md) + buf.Reset() + zmkEncoder.WriteBlocks(&buf, &ast) + got := buf.String() + if got != tc.exp { + t.Errorf("%d: %q -> %q, but got %q", i, tc.md, tc.exp, got) + } + } +} ADDED tests/naughtystrings_test.go Index: tests/naughtystrings_test.go ================================================================== --- tests/naughtystrings_test.go +++ tests/naughtystrings_test.go @@ -0,0 +1,96 @@ +//----------------------------------------------------------------------------- +// Copyright (c) 2020-2022 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 tests + +import ( + "bufio" + "io" + "os" + "path/filepath" + "testing" + + _ "zettelstore.de/z/cmd" + "zettelstore.de/z/domain/meta" + "zettelstore.de/z/encoder" + "zettelstore.de/z/input" + "zettelstore.de/z/parser" +) + +// Test all parser / encoder with a list of "naughty strings", i.e. unusual strings +// that often crash software. + +func getNaughtyStrings() (result []string, err error) { + fpath := filepath.Join("..", "testdata", "naughty", "blns.txt") + file, err := os.Open(fpath) + if err != nil { + return nil, err + } + defer file.Close() + scanner := bufio.NewScanner(file) + for scanner.Scan() { + if text := scanner.Text(); text != "" && text[0] != '#' { + result = append(result, text) + } + } + return result, scanner.Err() +} + +func getAllParser() (result []*parser.Info) { + for _, pname := range parser.GetSyntaxes() { + pinfo := parser.Get(pname) + if pname == pinfo.Name { + result = append(result, pinfo) + } + } + return result +} + +func getAllEncoder() (result []encoder.Encoder) { + for _, enc := range encoder.GetEncodings() { + e := encoder.Create(enc) + result = append(result, e) + } + return result +} + +func TestNaughtyStringParser(t *testing.T) { + blns, err := getNaughtyStrings() + if err != nil { + t.Fatal(err) + } + if len(blns) == 0 { + t.Fatal("no naughty strings found") + } + pinfos := getAllParser() + if len(pinfos) == 0 { + t.Fatal("no parser found") + } + encs := getAllEncoder() + if len(encs) == 0 { + t.Fatal("no encoder found") + } + for _, s := range blns { + for _, pinfo := range pinfos { + bs := pinfo.ParseBlocks(input.NewInput([]byte(s)), &meta.Meta{}, pinfo.Name) + is := pinfo.ParseInlines(input.NewInput([]byte(s)), pinfo.Name) + for _, enc := range encs { + _, err = enc.WriteBlocks(io.Discard, &bs) + if err != nil { + t.Error(err) + } + _, err = enc.WriteInlines(io.Discard, &is) + if err != nil { + t.Error(err) + } + } + } + } +} Index: tests/regression_test.go ================================================================== --- tests/regression_test.go +++ tests/regression_test.go @@ -143,11 +143,11 @@ for _, meta := range metaList { zettel, err2 := p.GetZettel(context.Background(), meta.Zid) if err2 != nil { panic(err2) } - z := parser.ParseZettel(zettel, "", testConfig) + z := parser.ParseZettel(context.Background(), 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) }) @@ -156,19 +156,19 @@ ss.Stop(context.Background()) } type myConfig struct{} -func (*myConfig) AddDefaultValues(m *meta.Meta) *meta.Meta { return m } -func (*myConfig) GetDefaultLang() string { return "" } -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 "" } -func (*myConfig) GetYAMLHeader() bool { return false } -func (*myConfig) GetZettelFileSyntax() []string { return nil } +func (*myConfig) Get(context.Context, *meta.Meta, string) string { return "" } +func (*myConfig) AddDefaultValues(_ context.Context, m *meta.Meta) *meta.Meta { + return m +} +func (*myConfig) GetHomeZettel() id.Zid { return id.Invalid } +func (*myConfig) GetListPageSize() int { return 0 } +func (*myConfig) GetSiteName() string { return "" } +func (*myConfig) GetYAMLHeader() bool { return false } +func (*myConfig) GetZettelFileSyntax() []string { return nil } func (*myConfig) GetSimpleMode() bool { return false } func (*myConfig) GetExpertMode() bool { return false } func (*myConfig) GetVisibility(*meta.Meta) meta.Visibility { return meta.VisibilityPublic } func (*myConfig) GetMaxTransclusions() int { return 1024 } Index: tools/build.go ================================================================== --- tools/build.go +++ tools/build.go @@ -383,11 +383,11 @@ if fossil := getFossilDirty(); fossil != "" { fmt.Fprintln(os.Stderr, "Warning: releasing a dirty version") } base := getVersion() if strings.HasSuffix(base, "dev") { - return base[:len(base)-3] + "preview-" + time.Now().Format("20060102") + return base[:len(base)-3] + "preview-" + time.Now().Local().Format("20060102") } return base } func cmdRelease() error { Index: usecase/authenticate.go ================================================================== --- usecase/authenticate.go +++ usecase/authenticate.go @@ -20,17 +20,17 @@ "zettelstore.de/z/auth" "zettelstore.de/z/auth/cred" "zettelstore.de/z/domain/id" "zettelstore.de/z/domain/meta" "zettelstore.de/z/logger" - "zettelstore.de/z/search" + "zettelstore.de/z/query" ) // AuthenticatePort is the interface used by this use case. type AuthenticatePort interface { - GetMeta(ctx context.Context, zid id.Zid) (*meta.Meta, error) - SelectMeta(ctx context.Context, s *search.Search) ([]*meta.Meta, error) + GetMeta(context.Context, id.Zid) (*meta.Meta, error) + SelectMeta(context.Context, *query.Query) ([]*meta.Meta, error) } // Authenticate is the data for this use case. type Authenticate struct { log *logger.Logger Index: usecase/create_zettel.go ================================================================== --- usecase/create_zettel.go +++ usecase/create_zettel.go @@ -10,10 +10,11 @@ package usecase import ( "context" + "time" "zettelstore.de/c/api" "zettelstore.de/z/config" "zettelstore.de/z/domain" "zettelstore.de/z/domain/id" @@ -112,13 +113,14 @@ m := zettel.Meta if m.Zid.IsValid() { return m.Zid, nil // TODO: new error: already exists } + m.Set(api.KeyCreated, time.Now().Local().Format(id.ZidLayout)) m.Delete(api.KeyModified) m.YamlSep = uc.rtConfig.GetYAMLHeader() zettel.Content.TrimSpace() zid, err := uc.port.CreateZettel(ctx, zettel) uc.log.Info().User(ctx).Zid(zid).Err(err).Msg("Create zettel") return zid, err } Index: usecase/evaluate.go ================================================================== --- usecase/evaluate.go +++ usecase/evaluate.go @@ -18,11 +18,11 @@ "zettelstore.de/z/domain" "zettelstore.de/z/domain/id" "zettelstore.de/z/domain/meta" "zettelstore.de/z/evaluator" "zettelstore.de/z/parser" - "zettelstore.de/z/search" + "zettelstore.de/z/query" ) // Evaluate is the data for this use case. type Evaluate struct { rtConfig config.Config @@ -45,18 +45,28 @@ func (uc *Evaluate) Run(ctx context.Context, zid id.Zid, syntax string) (*ast.ZettelNode, error) { zettel, err := uc.getZettel.Run(ctx, zid) if err != nil { return nil, err } - zn, err := parser.ParseZettel(zettel, syntax, uc.rtConfig), nil + zn, err := parser.ParseZettel(ctx, zettel, syntax, uc.rtConfig), nil if err != nil { return nil, err } evaluator.EvaluateZettel(ctx, uc, uc.rtConfig, zn) return zn, nil } + +// RunBlockNode executes the use case for a metadata list. +func (uc *Evaluate) RunBlockNode(ctx context.Context, bn ast.BlockNode) ast.BlockSlice { + if bn == nil { + return nil + } + bns := ast.BlockSlice{bn} + evaluator.EvaluateBlock(ctx, uc, uc.rtConfig, &bns) + return bns +} // RunMetadata executes the use case for a metadata value. func (uc *Evaluate) RunMetadata(ctx context.Context, value string) ast.InlineSlice { is := parser.ParseMetadata(value) evaluator.EvaluateInline(ctx, uc, uc.rtConfig, &is) @@ -79,8 +89,8 @@ func (uc *Evaluate) GetZettel(ctx context.Context, zid id.Zid) (domain.Zettel, error) { return uc.getZettel.Run(ctx, zid) } // SelectMeta returns a list of metadata that comply to the given selection criteria. -func (uc *Evaluate) SelectMeta(ctx context.Context, s *search.Search) ([]*meta.Meta, error) { - return uc.listMeta.Run(ctx, s) +func (uc *Evaluate) SelectMeta(ctx context.Context, q *query.Query) ([]*meta.Meta, error) { + return uc.listMeta.Run(ctx, q) } Index: usecase/get_user.go ================================================================== --- usecase/get_user.go +++ usecase/get_user.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. //----------------------------------------------------------------------------- @@ -16,20 +16,20 @@ "zettelstore.de/c/api" "zettelstore.de/z/auth" "zettelstore.de/z/box" "zettelstore.de/z/domain/id" "zettelstore.de/z/domain/meta" - "zettelstore.de/z/search" + "zettelstore.de/z/query" ) // Use case: return user identified by meta key ident. // --------------------------------------------------- // GetUserPort is the interface used by this use case. type GetUserPort interface { GetMeta(ctx context.Context, zid id.Zid) (*meta.Meta, error) - SelectMeta(ctx context.Context, s *search.Search) ([]*meta.Meta, error) + SelectMeta(ctx context.Context, q *query.Query) ([]*meta.Meta, error) } // GetUser is the data for this use case. type GetUser struct { authz auth.AuthzManager @@ -51,14 +51,12 @@ identMeta, err := uc.port.GetMeta(ctx, uc.authz.Owner()) if err == nil && identMeta.GetDefault(api.KeyUserID, "") == ident { return identMeta, nil } // Owner was not found or has another ident. Try via list search. - var s *search.Search - s = s.AddExpr("", "="+ident) - s = s.AddExpr(api.KeyUserID, ident) - metaList, err := uc.port.SelectMeta(ctx, s) + q := query.Parse(api.KeyUserID + api.SearchOperatorHas + ident + " " + api.SearchOperatorHas + ident) + metaList, err := uc.port.SelectMeta(ctx, q) if err != nil { return nil, err } if len(metaList) < 1 { return nil, nil Index: usecase/lists.go ================================================================== --- usecase/lists.go +++ usecase/lists.go @@ -15,17 +15,17 @@ "zettelstore.de/c/api" "zettelstore.de/z/box" "zettelstore.de/z/domain/meta" "zettelstore.de/z/parser" - "zettelstore.de/z/search" + "zettelstore.de/z/query" ) // ListMetaPort is the interface used by this use case. type ListMetaPort interface { // SelectMeta returns all zettel metadata that match the selection criteria. - SelectMeta(ctx context.Context, s *search.Search) ([]*meta.Meta, error) + SelectMeta(ctx context.Context, q *query.Query) ([]*meta.Meta, error) } // ListMeta is the data for this use case. type ListMeta struct { port ListMetaPort @@ -35,20 +35,20 @@ func NewListMeta(port ListMetaPort) ListMeta { return ListMeta{port: port} } // Run executes the use case. -func (uc ListMeta) Run(ctx context.Context, s *search.Search) ([]*meta.Meta, error) { - return uc.port.SelectMeta(ctx, s) +func (uc ListMeta) Run(ctx context.Context, q *query.Query) ([]*meta.Meta, error) { + return uc.port.SelectMeta(ctx, q) } // -------- List roles ------------------------------------------------------- // ListSyntaxPort is the interface used by this use case. type ListSyntaxPort interface { // SelectMeta returns all zettel metadata that match the selection criteria. - SelectMeta(ctx context.Context, s *search.Search) ([]*meta.Meta, error) + SelectMeta(ctx context.Context, q *query.Query) ([]*meta.Meta, error) } // ListSyntax is the data for this use case. type ListSyntax struct { port ListSyntaxPort @@ -59,20 +59,19 @@ return ListSyntax{port: port} } // Run executes the use case. func (uc ListSyntax) Run(ctx context.Context) (meta.Arrangement, error) { - var s *search.Search - s = s.AddExpr(api.KeySyntax, "") // We look for all metadata with a syntax key - metas, err := uc.port.SelectMeta(box.NoEnrichContext(ctx), s) + q := query.Parse(api.KeySyntax + api.ExistOperator) // We look for all metadata with a syntax key + metas, err := uc.port.SelectMeta(box.NoEnrichContext(ctx), q) if err != nil { return nil, err } result := meta.CreateArrangement(metas, api.KeySyntax) for _, syn := range parser.GetSyntaxes() { if _, found := result[syn]; !found { - result[syn] = nil + delete(result, syn) } } return result, nil } @@ -79,11 +78,11 @@ // -------- List roles ------------------------------------------------------- // ListRolesPort is the interface used by this use case. type ListRolesPort interface { // SelectMeta returns all zettel metadata that match the selection criteria. - SelectMeta(ctx context.Context, s *search.Search) ([]*meta.Meta, error) + SelectMeta(ctx context.Context, q *query.Query) ([]*meta.Meta, error) } // ListRoles is the data for this use case. type ListRoles struct { port ListRolesPort @@ -94,13 +93,12 @@ return ListRoles{port: port} } // Run executes the use case. func (uc ListRoles) Run(ctx context.Context) (meta.Arrangement, error) { - var s *search.Search - s = s.AddExpr(api.KeyRole, "") // We look for all metadata with a role key - metas, err := uc.port.SelectMeta(box.NoEnrichContext(ctx), s) + q := query.Parse(api.KeyRole + api.ExistOperator) // We look for all metadata with an existing role key + metas, err := uc.port.SelectMeta(box.NoEnrichContext(ctx), q) if err != nil { return nil, err } return meta.CreateArrangement(metas, api.KeyRole), nil } @@ -108,11 +106,11 @@ // -------- List tags -------------------------------------------------------- // ListTagsPort is the interface used by this use case. type ListTagsPort interface { // SelectMeta returns all zettel metadata that match the selection criteria. - SelectMeta(ctx context.Context, s *search.Search) ([]*meta.Meta, error) + SelectMeta(ctx context.Context, q *query.Query) ([]*meta.Meta, error) } // ListTags is the data for this use case. type ListTags struct { port ListTagsPort @@ -123,13 +121,12 @@ return ListTags{port: port} } // Run executes the use case. func (uc ListTags) Run(ctx context.Context, minCount int) (meta.Arrangement, error) { - var s *search.Search - s = s.AddExpr(api.KeyTags, "") // We look for all metadata with a tag - metas, err := uc.port.SelectMeta(ctx, s) + q := query.Parse(api.KeyAllTags + api.ExistOperator) // We look for all metadata with a tag + metas, err := uc.port.SelectMeta(ctx, q) if err != nil { return nil, err } result := meta.CreateArrangement(metas, api.KeyAllTags) if minCount > 1 { Index: usecase/parse_zettel.go ================================================================== --- usecase/parse_zettel.go +++ usecase/parse_zettel.go @@ -35,7 +35,7 @@ zettel, err := uc.getZettel.Run(ctx, zid) if err != nil { return nil, err } - return parser.ParseZettel(zettel, syntax, uc.rtConfig), nil + return parser.ParseZettel(ctx, zettel, syntax, uc.rtConfig), nil } Index: usecase/unlinked_refs.go ================================================================== --- usecase/unlinked_refs.go +++ usecase/unlinked_refs.go @@ -22,18 +22,18 @@ "zettelstore.de/z/domain/id" "zettelstore.de/z/domain/meta" "zettelstore.de/z/encoder/textenc" "zettelstore.de/z/evaluator" "zettelstore.de/z/parser" - "zettelstore.de/z/search" + "zettelstore.de/z/query" ) // UnlinkedReferencesPort is the interface used by this use case. type UnlinkedReferencesPort interface { GetMeta(ctx context.Context, zid id.Zid) (*meta.Meta, error) GetZettel(ctx context.Context, zid id.Zid) (domain.Zettel, error) - SelectMeta(ctx context.Context, s *search.Search) ([]*meta.Meta, error) + SelectMeta(ctx context.Context, q *query.Query) ([]*meta.Meta, error) } // UnlinkedReferences is the data for this use case. type UnlinkedReferences struct { port UnlinkedReferencesPort @@ -49,29 +49,32 @@ encText: textenc.Create(), } } // Run executes the usecase with already evaluated title value. -func (uc *UnlinkedReferences) Run(ctx context.Context, title string, s *search.Search) ([]*meta.Meta, error) { - words := makeWords(title) +func (uc *UnlinkedReferences) Run(ctx context.Context, phrase string, q *query.Query) ([]*meta.Meta, error) { + words := makeWords(phrase) if len(words) == 0 { return nil, nil } + var sb strings.Builder for _, word := range words { - s = s.AddExpr("", "="+word) + sb.WriteString(" :") + sb.WriteString(word) } + q = q.Parse(sb.String()) // Limit applies to the filtering process, not to SelectMeta - limit := s.GetLimit() - s = s.SetLimit(0) + limit := q.GetLimit() + q = q.SetLimit(0) - candidates, err := uc.port.SelectMeta(ctx, s) + candidates, err := uc.port.SelectMeta(ctx, q) if err != nil { return nil, err } - s = s.SetLimit(limit) // Restore limit - return s.Limit(uc.filterCandidates(ctx, candidates, words)), nil + q = q.SetLimit(limit) // Restore limit + return q.Limit(uc.filterCandidates(ctx, candidates, words)), nil } func makeWords(text string) []string { return strings.FieldsFunc(text, func(r rune) bool { return unicode.In(r, unicode.C, unicode.P, unicode.Z) @@ -107,11 +110,11 @@ syntax := zettel.Meta.GetDefault(api.KeySyntax, "") if !parser.IsTextParser(syntax) { continue } - zn, err := parser.ParseZettel(zettel, syntax, nil), nil + zn, err := parser.ParseZettel(ctx, zettel, syntax, nil), nil if err != nil { continue } evaluator.EvaluateZettel(ctx, uc.port, uc.rtConfig, zn) ast.Walk(&v, &zn.Ast) Index: usecase/update_zettel.go ================================================================== --- usecase/update_zettel.go +++ usecase/update_zettel.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. //----------------------------------------------------------------------------- @@ -48,18 +48,27 @@ return err } if zettel.Equal(oldZettel, false) { return nil } + + // Update relevant computed, but stored values. + if _, found := m.Get(api.KeyCreated); !found { + if val, crFound := oldZettel.Meta.Get(api.KeyCreated); crFound { + m.Set(api.KeyCreated, val) + } + } m.SetNow(api.KeyModified) + m.YamlSep = oldZettel.Meta.YamlSep if m.Zid == id.ConfigurationZid { m.Set(api.KeySyntax, api.ValueSyntaxNone) } + if !hasContent { zettel.Content = oldZettel.Content } zettel.Content.TrimSpace() err = uc.port.UpdateZettel(ctx, zettel) uc.log.Sense().User(ctx).Zid(m.Zid).Err(err).Msg("Update zettel") return err } Index: web/adapter/api/api.go ================================================================== --- web/adapter/api/api.go +++ web/adapter/api/api.go @@ -31,26 +31,24 @@ type API struct { log *logger.Logger b server.Builder authz auth.AuthzManager token auth.TokenManager - auth server.Auth rtConfig config.Config policy auth.Policy tokenLifetime time.Duration } // New creates a new API object. func New(log *logger.Logger, b server.Builder, authz auth.AuthzManager, token auth.TokenManager, - auth server.Auth, rtConfig config.Config, pol auth.Policy) *API { + rtConfig config.Config, pol auth.Policy) *API { a := &API{ log: log, b: b, authz: authz, token: token, - auth: auth, rtConfig: rtConfig, policy: pol, tokenLifetime: kernel.Main.GetConfig(kernel.WebService, kernel.WebTokenLifetimeAPI).(time.Duration), } @@ -62,11 +60,11 @@ // NewURLBuilder creates a new URL builder object with the given key. func (a *API) NewURLBuilder(key byte) *api.URLBuilder { return a.b.NewURLBuilder(key) } func (a *API) getAuthData(ctx context.Context) *server.AuthData { - return a.auth.GetAuthData(ctx) + return server.GetAuthData(ctx) } func (a *API) withAuth() bool { return a.authz.WithAuth() } func (a *API) getToken(ident *meta.Meta) ([]byte, error) { return a.token.GetToken(ident, a.tokenLifetime, auth.KindJSON) } @@ -93,11 +91,11 @@ return err } func (a *API) getRights(ctx context.Context, m *meta.Meta) (result api.ZettelRights) { pol := a.policy - user := a.auth.GetUser(ctx) + user := server.GetUser(ctx) if pol.CanCreate(user, m) { result |= api.ZettelCanCreate } if pol.CanRead(user, m) { result |= api.ZettelCanRead Index: web/adapter/api/command.go ================================================================== --- web/adapter/api/command.go +++ web/adapter/api/command.go @@ -25,10 +25,13 @@ ) http.HandlerFunc { return func(w http.ResponseWriter, r *http.Request) { ctx := r.Context() q := r.URL.Query() cmd := q.Get(api.QueryKeyCommand) + if cmd == "" { + cmd = q.Get("_cmd") + } switch api.Command(cmd) { case api.CommandAuthenticated: handleIsAuthenticated(ctx, w, ucIsAuth) return case api.CommandRefresh: Index: web/adapter/api/get_lists.go ================================================================== --- web/adapter/api/get_lists.go +++ web/adapter/api/get_lists.go @@ -6,11 +6,10 @@ // Zettelstore is licensed under the latest version of the EUPL (European Union // Public License). Please see file LICENSE.txt for your rights and obligations // under this license. //----------------------------------------------------------------------------- -// Package api provides api handlers for web requests. package api import ( "bytes" "net/http" @@ -24,18 +23,28 @@ // MakeListMapMetaHandler creates a new HTTP handler to retrieve mappings of // metadata values of a specific key to the list of zettel IDs, which contain // this value. func (a *API) MakeListMapMetaHandler(listRole usecase.ListRoles, listTags usecase.ListTags) http.HandlerFunc { return func(w http.ResponseWriter, r *http.Request) { - var ar meta.Arrangement + ctx := r.Context() + var buf bytes.Buffer query := r.URL.Query() - iMinCount, err := strconv.Atoi(query.Get(api.QueryKeyMin)) + + minVal := query.Get(api.QueryKeyMin) + if minVal == "" { + minVal = query.Get("_min") + } + iMinCount, err := strconv.Atoi(minVal) if err != nil || iMinCount < 0 { iMinCount = 0 } - ctx := r.Context() key := query.Get(api.QueryKeyKey) + if key == "" { + key = query.Get("_key") + } + + var ar meta.Arrangement switch key { case api.KeyRole: ar, err = listRole.Run(ctx) case api.KeyTags: ar, err = listTags.Run(ctx, iMinCount) @@ -56,11 +65,11 @@ zidList = append(zidList, api.ZettelID(m.Zid.String())) } mm[tag] = zidList } - var buf bytes.Buffer + buf.Reset() err = encodeJSONData(&buf, api.MapListJSON{Map: mm}) if err != nil { a.log.Fatal().Err(err).Msg("Unable to store map list in buffer") http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError) return Index: web/adapter/api/get_unlinked_refs.go ================================================================== --- web/adapter/api/get_unlinked_refs.go +++ web/adapter/api/get_unlinked_refs.go @@ -39,12 +39,15 @@ if err != nil { a.reportUsecaseError(w, err) return } - q := r.URL.Query() - phrase := q.Get(api.QueryKeyPhrase) + que := r.URL.Query() + phrase := que.Get(api.QueryKeyPhrase) + if phrase == "" { + phrase = que.Get("_phrase") + } if phrase == "" { if zmkTitle, found := zm.Get(api.KeyTitle); found { isTitle := evaluate.RunMetadata(ctx, zmkTitle) encdr := textenc.Create() var b strings.Builder @@ -54,11 +57,11 @@ } } } metaList, err := unlinkedRefs.Run( - ctx, phrase, adapter.AddUnlinkedRefsToSearch(adapter.GetSearch(q), zm)) + ctx, phrase, adapter.AddUnlinkedRefsToQuery(adapter.GetQuery(que), zm)) if err != nil { a.reportUsecaseError(w, err) return } Index: web/adapter/api/get_zettel_context.go ================================================================== --- web/adapter/api/get_zettel_context.go +++ web/adapter/api/get_zettel_context.go @@ -27,16 +27,26 @@ if err != nil { http.NotFound(w, r) return } q := r.URL.Query() - dir := adapter.GetZCDirection(q.Get(api.QueryKeyDir)) + dirVal := q.Get(api.QueryKeyDir) + if dirVal == "" { + dirVal = q.Get("_dir") + } + dir := adapter.GetZCDirection(dirVal) depth, ok := adapter.GetInteger(q, api.QueryKeyDepth) + if !ok { + depth, ok = adapter.GetInteger(q, "_depth") + } if !ok || depth < 0 { depth = 5 } limit, ok := adapter.GetInteger(q, api.QueryKeyLimit) + if !ok { + limit, ok = adapter.GetInteger(q, "_limit") + } if !ok || limit < 0 { limit = 200 } ctx := r.Context() metaList, err := getContext.Run(ctx, zid, dir, depth, limit) Index: web/adapter/api/get_zettel_list.go ================================================================== --- web/adapter/api/get_zettel_list.go +++ web/adapter/api/get_zettel_list.go @@ -23,13 +23,12 @@ // MakeListMetaHandler creates a new HTTP handler for the use case "list some zettel". func (a *API) 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) + q := adapter.GetQuery(r.URL.Query()) + metaList, err := listMeta.Run(ctx, q) if err != nil { a.reportUsecaseError(w, err) return } @@ -42,12 +41,12 @@ }) } var buf bytes.Buffer err = encodeJSONData(&buf, api.ZettelListJSON{ - Query: s.String(), - Human: s.Human(), + Query: q.String(), + Human: q.Human(), List: result, }) if err != nil { a.log.Fatal().Err(err).Msg("Unable to store meta list in buffer") http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError) @@ -61,13 +60,11 @@ // 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) + metaList, err := listMeta.Run(ctx, adapter.GetQuery(r.URL.Query())) if err != nil { a.reportUsecaseError(w, err) return } ADDED web/adapter/api/query.go Index: web/adapter/api/query.go ================================================================== --- web/adapter/api/query.go +++ web/adapter/api/query.go @@ -0,0 +1,125 @@ +//----------------------------------------------------------------------------- +// Copyright (c) 2022 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 + +import ( + "bytes" + "io" + "net/http" + "strconv" + "strings" + + "zettelstore.de/c/api" + "zettelstore.de/z/box" + "zettelstore.de/z/domain/meta" + "zettelstore.de/z/query" + "zettelstore.de/z/usecase" + "zettelstore.de/z/web/adapter" +) + +// MakeQueryHandler creates a new HTTP handler to perform a query. +func (a *API) MakeQueryHandler(listMeta usecase.ListMeta) http.HandlerFunc { + return func(w http.ResponseWriter, r *http.Request) { + ctx := r.Context() + q := adapter.GetQuery(r.URL.Query()) + if q == nil { + a.log.Sense().Msg("no parameter for query") + http.Error(w, http.StatusText(http.StatusBadRequest), http.StatusBadRequest) + return + } + if !q.EnrichNeeded() { + ctx = box.NoEnrichContext(ctx) + } + metaList, err := listMeta.Run(ctx, q) + if err != nil { + a.reportUsecaseError(w, err) + return + } + + var buf bytes.Buffer + contentType, err := queryAction(&buf, q, metaList) + if err != nil { + a.reportUsecaseError(w, err) + return + } + err = writeBuffer(w, &buf, contentType) + a.log.IfErr(err).Msg("write action") + } +} + +func queryAction(w io.Writer, q *query.Query, ml []*meta.Meta) (string, error) { + ap := actionPara{ + w: w, + q: q, + ml: ml, + min: -1, + max: -1, + } + if actions := q.Actions(); len(actions) > 0 { + acts := make([]string, 0, len(actions)) + for _, act := range actions { + if strings.HasPrefix(act, "MIN") { + if num, err := strconv.Atoi(act[3:]); err == nil && num > 0 { + ap.min = num + continue + } + } + if strings.HasPrefix(act, "MAX") { + if num, err := strconv.Atoi(act[3:]); err == nil && num > 0 { + ap.max = num + continue + } + } + acts = append(acts, act) + } + for _, act := range acts { + key := strings.ToLower(act) + switch meta.Type(key) { + case meta.TypeWord, meta.TypeTagSet: + return ap.createMapMeta(key) + } + } + } + return "", nil +} + +type actionPara struct { + w io.Writer + q *query.Query + ml []*meta.Meta + min int + max int +} + +func (ap *actionPara) createMapMeta(key string) (string, error) { + if len(ap.ml) == 0 { + return "", nil + } + arr := meta.CreateArrangement(ap.ml, key) + if len(arr) == 0 { + return "", nil + } + min, max := ap.min, ap.max + mm := make(api.MapMeta, len(arr)) + for tag, metaList := range arr { + if len(metaList) < min || (max > 0 && len(metaList) > max) { + continue + } + zidList := make([]api.ZettelID, 0, len(metaList)) + for _, m := range metaList { + zidList = append(zidList, api.ZettelID(m.Zid.String())) + } + mm[tag] = zidList + } + + err := encodeJSONData(ap.w, api.MapListJSON{Map: mm}) + return ctJSON, err +} Index: web/adapter/api/request.go ================================================================== --- web/adapter/api/request.go +++ web/adapter/api/request.go @@ -24,11 +24,14 @@ ) // getEncoding returns the data encoding selected by the caller. func getEncoding(r *http.Request, q url.Values, defEncoding api.EncodingEnum) (api.EncodingEnum, string) { encoding := q.Get(api.QueryKeyEncoding) - if len(encoding) > 0 { + if encoding == "" { + encoding = q.Get("_enc") + } + if encoding != "" { return api.Encoder(encoding), encoding } if enc, ok := getOneEncoding(r, api.HeaderAccept); ok { return api.Encoder(enc), enc } @@ -76,10 +79,13 @@ } func getPart(q url.Values, defPart partType) partType { if part, ok := partMap[q.Get(api.QueryKeyPart)]; ok { return part + } + if part, ok := partMap[q.Get("_part")]; ok { + return part } return defPart } func (p partType) String() string { Index: web/adapter/request.go ================================================================== --- web/adapter/request.go +++ web/adapter/request.go @@ -17,11 +17,11 @@ "strings" "zettelstore.de/c/api" "zettelstore.de/z/domain/meta" "zettelstore.de/z/kernel" - "zettelstore.de/z/search" + "zettelstore.de/z/query" "zettelstore.de/z/usecase" ) // GetCredentialsViaForm retrieves the authentication credentions from a form. func GetCredentialsViaForm(r *http.Request) (ident, cred string, ok bool) { @@ -48,73 +48,19 @@ } } return 0, false } -// GetSearch retrieves the specified search and sorting options from a query. -func GetSearch(q url.Values) (s *search.Search) { - if exprs, found := q[api.QueryKeySearch]; found { - s = search.Parse(strings.Join(exprs, " ")) - } - for key, values := range q { - switch key { - case api.QueryKeySort, api.QueryKeyOrder: - s = extractOrderFromQuery(values, s) - case api.QueryKeyOffset: - s = extractOffsetFromQuery(values, s) - case api.QueryKeyLimit: - s = extractLimitFromQuery(values, s) - case api.QueryKeyNegate: - s = s.SetNegate() - case api.QueryKeySearch: // Ignore, already processed to top of method. - default: - if meta.KeyIsValid(key) { - s = setCleanedQueryValues(s, key, values) - } - } - } - return s -} - -func extractOrderFromQuery(values []string, s *search.Search) *search.Search { - if len(values) > 0 { - descending := false - sortkey := values[0] - if strings.HasPrefix(sortkey, "-") { - descending = true - sortkey = sortkey[1:] - } - if meta.KeyIsValid(sortkey) || sortkey == search.RandomOrder { - s = s.AddOrder(sortkey, descending) - } - } - return s -} - -func extractOffsetFromQuery(values []string, s *search.Search) *search.Search { - if len(values) > 0 { - if offset, err := strconv.Atoi(values[0]); err == nil && offset > 0 { - s = s.SetOffset(offset) - } - } - return s -} - -func extractLimitFromQuery(values []string, s *search.Search) *search.Search { - if len(values) > 0 { - if limit, err := strconv.Atoi(values[0]); err == nil && limit > 0 { - s = s.SetLimit(limit) - } - } - return s -} - -func setCleanedQueryValues(s *search.Search, key string, values []string) *search.Search { - for _, val := range values { - s = s.AddExpr(key, val) - } - return s +// GetQuery retrieves the specified options from a query. +func GetQuery(vals url.Values) *query.Query { + if exprs, found := vals[api.QueryKeyQuery]; found { + return query.Parse(strings.Join(exprs, " ")) + } + if exprs, found := vals["_s"]; found { + return query.Parse(strings.Join(exprs, " ")) + } + return nil } // GetZCDirection returns a direction value for a given string. func GetZCDirection(s string) usecase.ZettelContextDirection { switch s { @@ -124,21 +70,30 @@ return usecase.ZettelContextForward } return usecase.ZettelContextBoth } -// AddUnlinkedRefsToSearch inspects metadata and enhances the given search to ignore +// AddUnlinkedRefsToQuery inspects metadata and enhances the given query to ignore // some zettel identifier. -func AddUnlinkedRefsToSearch(s *search.Search, m *meta.Meta) *search.Search { - s = s.AddExpr(api.KeyID, "!="+m.Zid.String()) +func AddUnlinkedRefsToQuery(q *query.Query, m *meta.Meta) *query.Query { + var sb strings.Builder + sb.WriteString(api.KeyID) + sb.WriteString("!:") + sb.WriteString(m.Zid.String()) for _, pair := range m.ComputedPairsRest() { switch meta.Type(pair.Key) { case meta.TypeID: - s = s.AddExpr(api.KeyID, "!="+pair.Value) + sb.WriteByte(' ') + sb.WriteString(api.KeyID) + sb.WriteString("!:") + sb.WriteString(pair.Value) case meta.TypeIDSet: for _, value := range meta.ListFromValue(pair.Value) { - s = s.AddExpr(api.KeyID, "!="+value) + sb.WriteByte(' ') + sb.WriteString(api.KeyID) + sb.WriteString("!:") + sb.WriteString(value) } } } - return s + return q.Parse(sb.String()) } Index: web/adapter/response.go ================================================================== --- web/adapter/response.go +++ web/adapter/response.go @@ -6,11 +6,10 @@ // 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" Index: web/adapter/webui/create_zettel.go ================================================================== --- web/adapter/webui/create_zettel.go +++ web/adapter/webui/create_zettel.go @@ -14,17 +14,17 @@ "context" "net/http" "zettelstore.de/c/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/parser" "zettelstore.de/z/usecase" "zettelstore.de/z/web/adapter" + "zettelstore.de/z/web/server" ) // MakeGetCreateZettelHandler creates a new HTTP handler to display the // HTML edit view for the various zettel creation methods. func (wui *WebUI) MakeGetCreateZettelHandler( @@ -90,14 +90,14 @@ zettel domain.Zettel, title, heading string, roleData []string, syntaxData []string, ) { - user := wui.getUser(ctx) + user := server.GetUser(ctx) m := zettel.Meta var base baseData - wui.makeBaseData(ctx, config.GetLang(m, wui.rtConfig), title, "", user, &base) + wui.makeBaseData(ctx, wui.rtConfig.Get(ctx, m, api.KeyLang), title, "", user, &base) wui.renderTemplate(ctx, w, id.FormTemplateZid, &base, formZettelData{ Heading: heading, MetaTitle: m.GetDefault(api.KeyTitle, ""), MetaTags: m.GetDefault(api.KeyTags, ""), MetaRole: m.GetDefault(api.KeyRole, ""), Index: web/adapter/webui/delete_zettel.go ================================================================== --- web/adapter/webui/delete_zettel.go +++ web/adapter/webui/delete_zettel.go @@ -14,15 +14,15 @@ "net/http" "zettelstore.de/c/api" "zettelstore.de/c/maps" "zettelstore.de/z/box" - "zettelstore.de/z/config" "zettelstore.de/z/domain/id" "zettelstore.de/z/domain/meta" "zettelstore.de/z/strfun" "zettelstore.de/z/usecase" + "zettelstore.de/z/web/server" ) // MakeGetDeleteZettelHandler creates a new HTTP handler to display the // HTML delete view of a zettel. func (wui *WebUI) MakeGetDeleteZettelHandler( @@ -48,18 +48,18 @@ var shadowedBox string var incomingLinks []simpleLink if len(ms) > 1 { shadowedBox = ms[1].GetDefault(api.KeyBoxNumber, "???") } else { - getTextTitle := wui.makeGetTextTitle(ctx, getMeta, evaluate) + getTextTitle := wui.makeGetTextTitle(createGetMetadataFunc(ctx, getMeta), createEvalMetadataFunc(ctx, evaluate)) incomingLinks = wui.encodeIncoming(m, getTextTitle) } uselessFiles := retrieveUselessFiles(m) - user := wui.getUser(ctx) + user := server.GetUser(ctx) var base baseData - wui.makeBaseData(ctx, config.GetLang(m, wui.rtConfig), "Delete Zettel "+m.Zid.String(), "", user, &base) + wui.makeBaseData(ctx, wui.rtConfig.Get(ctx, m, api.KeyLang), "Delete Zettel "+m.Zid.String(), "", user, &base) wui.renderTemplate(ctx, w, id.DeleteTemplateZid, &base, struct { Zid string MetaPairs []meta.Pair HasShadows bool ShadowedBox string Index: web/adapter/webui/get_info.go ================================================================== --- web/adapter/webui/get_info.go +++ web/adapter/webui/get_info.go @@ -19,15 +19,16 @@ "zettelstore.de/c/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/encoder" + "zettelstore.de/z/evaluator" "zettelstore.de/z/usecase" "zettelstore.de/z/web/adapter" + "zettelstore.de/z/web/server" ) type metaDataInfo struct { Key string Value string @@ -60,14 +61,15 @@ if err != nil { wui.reportError(ctx, w, err) return } + evalMetadata := createEvalMetadataFunc(ctx, evaluate) enc := wui.getSimpleHTMLEncoder() pairs := zn.Meta.ComputedPairs() metaData := make([]metaDataInfo, len(pairs)) - getTextTitle := wui.makeGetTextTitle(ctx, getMeta, evaluate) + getTextTitle := wui.makeGetTextTitle(createGetMetadataFunc(ctx, getMeta), evalMetadata) for i, p := range pairs { var buf bytes.Buffer wui.writeHTMLMetaValue( &buf, p.Key, p.Value, getTextTitle, @@ -76,42 +78,47 @@ }, enc) metaData[i] = metaDataInfo{p.Key, buf.String()} } summary := collect.References(zn) - locLinks, searchQuery, extLinks := splitLocSeaExtLinks(append(summary.Links, summary.Embeds...)) - searchLinks := make([]simpleLink, len(searchQuery)) - for i, sq := range searchQuery { - searchLinks[i].Text = sq - searchLinks[i].URL = wui.NewURLBuilder('h').AppendSearch(sq).String() + locLinks, qLinks, extLinks := splitLocSeaExtLinks(append(summary.Links, summary.Embeds...)) + queryLinks := make([]simpleLink, len(qLinks)) + for i, sq := range qLinks { + queryLinks[i].Text = sq + queryLinks[i].URL = wui.NewURLBuilder('h').AppendQuery(sq).String() } - textTitle := wui.encodeTitleAsText(ctx, zn.InhMeta, evaluate) + textTitle := encodeEvaluatedTitleText(zn.InhMeta, evalMetadata, wui.gentext) phrase := q.Get(api.QueryKeyPhrase) if phrase == "" { phrase = textTitle } phrase = strings.TrimSpace(phrase) unlinkedMeta, err := unlinkedRefs.Run( - ctx, phrase, adapter.AddUnlinkedRefsToSearch(nil, zn.InhMeta)) + ctx, phrase, adapter.AddUnlinkedRefsToQuery(nil, zn.InhMeta)) + if err != nil { + wui.reportError(ctx, w, err) + return + } + bns := evaluate.RunBlockNode(ctx, evaluator.QueryAction(ctx, nil, unlinkedMeta, wui.rtConfig)) + unlinkedContent, err := enc.BlocksString(&bns) if err != nil { wui.reportError(ctx, w, err) return } - unLinks := wui.buildHTMLMetaList(unlinkedMeta, func(val string) ast.InlineSlice { return evaluate.RunMetadata(ctx, val) }) shadowLinks := getShadowLinks(ctx, zid, getAllMeta) endnotes, err := enc.BlocksString(&ast.BlockSlice{}) if err != nil { endnotes = "" } - user := wui.getUser(ctx) + user := server.GetUser(ctx) canCreate := wui.canCreate(ctx, user) apiZid := api.ZettelID(zid.String()) var base baseData - wui.makeBaseData(ctx, config.GetLang(zn.InhMeta, wui.rtConfig), textTitle, "", user, &base) + wui.makeBaseData(ctx, wui.rtConfig.Get(ctx, zn.InhMeta, api.KeyLang), textTitle, "", user, &base) wui.renderTemplate(ctx, w, id.InfoTemplateZid, &base, struct { Zid string WebURL string ContextURL string CanWrite bool @@ -125,16 +132,16 @@ CanDelete bool DeleteURL string MetaData []metaDataInfo HasLocLinks bool LocLinks []localLink - HasSearchLinks bool - SearchLinks []simpleLink + HasQueryLinks bool + QueryLinks []simpleLink HasExtLinks bool ExtLinks []string ExtNewWindow string - UnLinks []simpleLink + UnLinksContent string UnLinksPhrase string QueryKeyPhrase string EvalMatrix []matrixLine ParseMatrix []matrixLine HasShadowLinks bool @@ -145,26 +152,26 @@ WebURL: wui.NewURLBuilder('h').SetZid(apiZid).String(), ContextURL: wui.NewURLBuilder('k').SetZid(apiZid).String(), CanWrite: wui.canWrite(ctx, user, zn.Meta, zn.Content), EditURL: wui.NewURLBuilder('e').SetZid(apiZid).String(), CanFolge: canCreate, - FolgeURL: wui.NewURLBuilder('c').SetZid(apiZid).AppendQuery(queryKeyAction, valueActionFolge).String(), + FolgeURL: wui.NewURLBuilder('c').SetZid(apiZid).AppendKVQuery(queryKeyAction, valueActionFolge).String(), CanCopy: canCreate && !zn.Content.IsBinary(), - CopyURL: wui.NewURLBuilder('c').SetZid(apiZid).AppendQuery(queryKeyAction, valueActionCopy).String(), + CopyURL: wui.NewURLBuilder('c').SetZid(apiZid).AppendKVQuery(queryKeyAction, valueActionCopy).String(), CanRename: wui.canRename(ctx, user, zn.Meta), RenameURL: wui.NewURLBuilder('b').SetZid(apiZid).String(), CanDelete: wui.canDelete(ctx, user, zn.Meta), DeleteURL: wui.NewURLBuilder('d').SetZid(apiZid).String(), MetaData: metaData, HasLocLinks: len(locLinks) > 0, LocLinks: locLinks, - HasSearchLinks: len(searchQuery) > 0, - SearchLinks: searchLinks, + HasQueryLinks: len(queryLinks) > 0, + QueryLinks: queryLinks, HasExtLinks: len(extLinks) > 0, ExtLinks: extLinks, ExtNewWindow: htmlAttrNewWindow(len(extLinks) > 0), - UnLinks: unLinks, + UnLinksContent: unlinkedContent, UnLinksPhrase: phrase, QueryKeyPhrase: api.QueryKeyPhrase, EvalMatrix: wui.infoAPIMatrix('v', zid), ParseMatrix: wui.infoAPIMatrixPlain('p', zid), HasShadowLinks: len(shadowLinks) > 0, @@ -177,29 +184,29 @@ type localLink struct { Valid bool Zid string } -func splitLocSeaExtLinks(links []*ast.Reference) (locLinks []localLink, searchQuery, extLinks []string) { +func splitLocSeaExtLinks(links []*ast.Reference) (locLinks []localLink, queries, extLinks []string) { if len(links) == 0 { return nil, nil, nil } for _, ref := range links { if ref.State == ast.RefStateSelf || ref.IsZettel() { continue } - if ref.State == ast.RefStateSearch { - searchQuery = append(searchQuery, ref.Value) + if ref.State == ast.RefStateQuery { + queries = append(queries, ref.Value) continue } if ref.IsExternal() { extLinks = append(extLinks, ref.String()) continue } locLinks = append(locLinks, localLink{ref.IsValid(), ref.String()}) } - return locLinks, searchQuery, extLinks + return locLinks, queries, extLinks } func (wui *WebUI) infoAPIMatrix(key byte, zid id.Zid) []matrixLine { encodings := encoder.GetEncodings() encTexts := make([]string, 0, len(encodings)) @@ -212,13 +219,13 @@ matrix := make([]matrixLine, 0, len(parts)) u := wui.NewURLBuilder(key).SetZid(api.ZettelID(zid.String())) for _, part := range parts { row := make([]simpleLink, len(encTexts)) for j, enc := range encTexts { - u.AppendQuery(api.QueryKeyPart, part) + u.AppendKVQuery(api.QueryKeyPart, part) if enc != defEncoding { - u.AppendQuery(api.QueryKeyEncoding, enc) + u.AppendKVQuery(api.QueryKeyEncoding, enc) } row[j] = simpleLink{enc, u.String()} u.ClearQuery() } matrix = append(matrix, matrixLine{part, row}) @@ -231,11 +238,11 @@ apiZid := api.ZettelID(zid.String()) // Append plain and JSON format u := wui.NewURLBuilder('z').SetZid(apiZid) for i, part := range getParts() { - u.AppendQuery(api.QueryKeyPart, part) + u.AppendKVQuery(api.QueryKeyPart, part) matrix[i].Elements = append(matrix[i].Elements, simpleLink{"plain", u.String()}) u.ClearQuery() } u = wui.NewURLBuilder('j').SetZid(apiZid) matrix[0].Elements = append(matrix[0].Elements, simpleLink{"json", u.String()}) Index: web/adapter/webui/get_zettel.go ================================================================== --- web/adapter/webui/get_zettel.go +++ web/adapter/webui/get_zettel.go @@ -15,15 +15,15 @@ "net/http" "zettelstore.de/c/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/textenc" "zettelstore.de/z/usecase" + "zettelstore.de/z/web/server" ) // 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) { @@ -40,17 +40,14 @@ if err != nil { wui.reportError(ctx, w, err) return } - enc := wui.createZettelEncoder() - evalMetadata := func(value string) ast.InlineSlice { - return evaluate.RunMetadata(ctx, value) - } - metaHeader := enc.MetaString(zn.InhMeta, evalMetadata) - textTitle := wui.encodeTitleAsText(ctx, zn.InhMeta, evaluate) - htmlTitle := encodeZmkMetadata(zn.InhMeta.GetTitle(), evalMetadata, enc) + enc := wui.createZettelEncoder(ctx, zn.InhMeta) + evalMetadata := createEvalMetadataFunc(ctx, evaluate) + textTitle := encodeEvaluatedTitleText(zn.InhMeta, evalMetadata, wui.gentext) + htmlTitle := encodeEvaluatedTitleHTML(zn.InhMeta, evalMetadata, enc) htmlContent, err := enc.BlocksString(&zn.Ast) if err != nil { wui.reportError(ctx, w, err) return } @@ -61,22 +58,22 @@ return } if cssZid != id.Invalid { roleCSSURL = wui.NewURLBuilder('z').SetZid(api.ZettelID(cssZid.String())).String() } - user := wui.getUser(ctx) - roleText := zn.Meta.GetDefault(api.KeyRole, "*") + user := server.GetUser(ctx) + roleText := zn.Meta.GetDefault(api.KeyRole, "") tags := wui.buildTagInfos(zn.Meta) canCreate := wui.canCreate(ctx, user) - getTextTitle := wui.makeGetTextTitle(ctx, getMeta, evaluate) + getTextTitle := wui.makeGetTextTitle(createGetMetadataFunc(ctx, getMeta), evalMetadata) extURL, hasExtURL := zn.Meta.Get(api.KeyURL) folgeLinks := wui.encodeZettelLinks(zn.InhMeta, api.KeyFolge, getTextTitle) backLinks := wui.encodeZettelLinks(zn.InhMeta, api.KeyBack, getTextTitle) apiZid := api.ZettelID(zid.String()) var base baseData - wui.makeBaseData(ctx, config.GetLang(zn.InhMeta, wui.rtConfig), textTitle, roleCSSURL, user, &base) - base.MetaHeader = metaHeader + wui.makeBaseData(ctx, wui.rtConfig.Get(ctx, zn.InhMeta, api.KeyLang), textTitle, roleCSSURL, user, &base) + base.MetaHeader = enc.MetaString(zn.InhMeta, evalMetadata) wui.renderTemplate(ctx, w, id.ZettelTemplateZid, &base, struct { HTMLTitle string RoleCSS string CanWrite bool EditURL string @@ -92,10 +89,11 @@ FolgeURL string PrecursorRefs string HasExtURL bool ExtURL string ExtNewWindow string + Author string Content string HasFolgeLinks bool FolgeLinks []simpleLink HasBackLinks bool BackLinks []simpleLink @@ -105,21 +103,22 @@ CanWrite: wui.canWrite(ctx, user, zn.Meta, zn.Content), EditURL: wui.NewURLBuilder('e').SetZid(apiZid).String(), Zid: zid.String(), InfoURL: wui.NewURLBuilder('i').SetZid(apiZid).String(), RoleText: roleText, - RoleURL: wui.NewURLBuilder('h').AppendQuery("role", roleText).String(), + RoleURL: wui.NewURLBuilder('h').AppendQuery(api.KeyRole + api.SearchOperatorHas + roleText).String(), HasTags: len(tags) > 0, Tags: tags, CanCopy: canCreate && !zn.Content.IsBinary(), - CopyURL: wui.NewURLBuilder('c').SetZid(apiZid).AppendQuery(queryKeyAction, valueActionCopy).String(), + CopyURL: wui.NewURLBuilder('c').SetZid(apiZid).AppendKVQuery(queryKeyAction, valueActionCopy).String(), CanFolge: canCreate, - FolgeURL: wui.NewURLBuilder('c').SetZid(apiZid).AppendQuery(queryKeyAction, valueActionFolge).String(), + FolgeURL: wui.NewURLBuilder('c').SetZid(apiZid).AppendKVQuery(queryKeyAction, valueActionFolge).String(), PrecursorRefs: wui.encodeIdentifierSet(zn.InhMeta, api.KeyPrecursor, getTextTitle), ExtURL: extURL, HasExtURL: hasExtURL, ExtNewWindow: htmlAttrNewWindow(hasExtURL), + Author: zn.Meta.GetDefault(api.KeyAuthor, ""), Content: htmlContent, HasFolgeLinks: len(folgeLinks) > 0, FolgeLinks: folgeLinks, HasBackLinks: len(backLinks) > 0, BackLinks: backLinks, @@ -144,11 +143,11 @@ var tagInfos []simpleLink if tags, ok := m.GetList(api.KeyTags); ok { ub := wui.NewURLBuilder('h') tagInfos = make([]simpleLink, len(tags)) for i, tag := range tags { - tagInfos[i] = simpleLink{Text: tag, URL: ub.AppendQuery(api.KeyAllTags, tag).String()} + tagInfos[i] = simpleLink{Text: tag, URL: ub.AppendQuery(api.KeyAllTags + api.SearchOperatorHas + tag).String()} ub.ClearQuery() } } return tagInfos } Index: web/adapter/webui/home.go ================================================================== --- web/adapter/webui/home.go +++ web/adapter/webui/home.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,10 +17,11 @@ "zettelstore.de/c/api" "zettelstore.de/z/box" "zettelstore.de/z/domain/id" "zettelstore.de/z/domain/meta" + "zettelstore.de/z/web/server" ) type getRootStore interface { // GetMeta retrieves just the meta data of a specific zettel. GetMeta(ctx context.Context, zid id.Zid) (*meta.Meta, error) @@ -46,12 +47,12 @@ _, err := s.GetMeta(ctx, homeZid) if err == nil { wui.redirectFound(w, r, wui.NewURLBuilder('h').SetZid(apiHomeZid)) return } - if errors.Is(err, &box.ErrNotAllowed{}) && wui.authz.WithAuth() && wui.getUser(ctx) == nil { + if errors.Is(err, &box.ErrNotAllowed{}) && wui.authz.WithAuth() && server.GetUser(ctx) == nil { wui.redirectFound(w, r, wui.NewURLBuilder('i')) return } wui.redirectFound(w, r, wui.NewURLBuilder('h')) } } Index: web/adapter/webui/htmlgen.go ================================================================== --- web/adapter/webui/htmlgen.go +++ web/adapter/webui/htmlgen.go @@ -21,11 +21,11 @@ "zettelstore.de/z/ast" "zettelstore.de/z/domain/meta" "zettelstore.de/z/encoder" "zettelstore.de/z/encoder/sexprenc" "zettelstore.de/z/encoder/textenc" - "zettelstore.de/z/search" + "zettelstore.de/z/query" "zettelstore.de/z/strfun" ) // Builder allows to build new URLs for the web service. type urlBuilder interface { @@ -35,29 +35,27 @@ type htmlGenerator struct { builder urlBuilder textEnc *textenc.Encoder extMarker string - newWindow bool env *html.EncEnvironment } -func createGenerator(builder urlBuilder, extMarker string, newWindow bool) *htmlGenerator { +func createGenerator(builder urlBuilder, extMarker string) *htmlGenerator { env := html.NewEncEnvironment(nil, 1) gen := &htmlGenerator{ builder: builder, textEnc: textenc.Create(), extMarker: extMarker, - newWindow: newWindow, env: env, } env.Builtins.Set(sexpr.SymTag, sxpf.NewBuiltin("tag", true, 0, -1, gen.generateTag)) env.Builtins.Set(sexpr.SymLinkZettel, sxpf.NewBuiltin("linkZ", true, 2, -1, gen.generateLinkZettel)) env.Builtins.Set(sexpr.SymLinkFound, sxpf.NewBuiltin("linkZ", true, 2, -1, gen.generateLinkZettel)) env.Builtins.Set(sexpr.SymLinkBased, sxpf.NewBuiltin("linkB", true, 2, -1, gen.generateLinkBased)) - env.Builtins.Set(sexpr.SymLinkSearch, sxpf.NewBuiltin("linkS", true, 2, -1, gen.generateLinkSearch)) + env.Builtins.Set(sexpr.SymLinkQuery, sxpf.NewBuiltin("linkQ", true, 2, -1, gen.generateLinkQuery)) env.Builtins.Set(sexpr.SymLinkExternal, sxpf.NewBuiltin("linkE", true, 2, -1, gen.generateLinkExternal)) f, err := env.Builtins.LookupForm(sexpr.SymEmbed) if err != nil { panic(err) @@ -146,11 +144,11 @@ env := senv.(*html.EncEnvironment) s := env.GetString(args) if env.IgnoreLinks() { env.WriteEscaped(s) } else { - u := g.builder.NewURLBuilder('h').AppendQuery(api.KeyAllTags, "#"+strings.ToLower(s)) + u := g.builder.NewURLBuilder('h').AppendQuery(api.KeyAllTags + ":#" + strings.ToLower(s)) env.WriteStrings(`<a href="`, u.String(), `">#`) env.WriteEscaped(s) env.WriteString("</a>") } } @@ -177,15 +175,15 @@ html.WriteLink(env, args, a.Set("href", u.String()), refValue, "") } return nil, nil } -func (g *htmlGenerator) generateLinkSearch(senv sxpf.Environment, args *sxpf.Pair, _ int) (sxpf.Value, error) { +func (g *htmlGenerator) generateLinkQuery(senv sxpf.Environment, args *sxpf.Pair, _ int) (sxpf.Value, error) { env := senv.(*html.EncEnvironment) if a, refValue, ok := html.PrepareLink(env, args); ok { - searchExpr := search.Parse(refValue).String() - u := g.builder.NewURLBuilder('h').AppendSearch(searchExpr) + queryExpr := query.Parse(refValue).String() + u := g.builder.NewURLBuilder('h').AppendQuery(queryExpr) html.WriteLink(env, args, a.Set("href", u.String()), refValue, "") } return nil, nil } Index: web/adapter/webui/htmlmeta.go ================================================================== --- web/adapter/webui/htmlmeta.go +++ web/adapter/webui/htmlmeta.go @@ -27,12 +27,10 @@ "zettelstore.de/z/usecase" ) var space = []byte{' '} -type evalMetadataFunc = func(string) ast.InlineSlice - func (wui *WebUI) writeHTMLMetaValue( w io.Writer, key, value string, getTextTitle getTextTitleFunc, evalMetadata evalMetadataFunc, @@ -147,40 +145,40 @@ wui.writeWord(w, key, word) } } func (wui *WebUI) writeLink(w io.Writer, key, value, text string) { - fmt.Fprintf(w, "<a href=\"%v?%v=%v\">", wui.NewURLBuilder('h'), url.QueryEscape(key), url.QueryEscape(value)) + fmt.Fprintf(w, `<a href="%v">`, wui.NewURLBuilder('h').AppendQuery(key+api.SearchOperatorHas+value)) html.Escape(w, text) io.WriteString(w, "</a>") } + +type getMetadataFunc func(id.Zid) (*meta.Meta, error) + +func createGetMetadataFunc(ctx context.Context, getMeta usecase.GetMeta) getMetadataFunc { + return func(zid id.Zid) (*meta.Meta, error) { return getMeta.Run(box.NoEnrichContext(ctx), zid) } +} + +type evalMetadataFunc = func(string) ast.InlineSlice + +func createEvalMetadataFunc(ctx context.Context, evaluate *usecase.Evaluate) evalMetadataFunc { + return func(value string) ast.InlineSlice { return evaluate.RunMetadata(ctx, value) } +} type getTextTitleFunc func(id.Zid) (string, int) -func (wui *WebUI) makeGetTextTitle( - ctx context.Context, - getMeta usecase.GetMeta, evaluate *usecase.Evaluate, -) getTextTitleFunc { +func (wui *WebUI) makeGetTextTitle(getMetadata getMetadataFunc, evalMetadata evalMetadataFunc) getTextTitleFunc { return func(zid id.Zid) (string, int) { - m, err := getMeta.Run(box.NoEnrichContext(ctx), zid) + m, err := getMetadata(zid) if err != nil { if errors.Is(err, &box.ErrNotAllowed{}) { return "", -1 } return "", 0 } - return wui.encodeTitleAsText(ctx, m, evaluate), 1 - } -} - -func (wui *WebUI) encodeTitleAsText(ctx context.Context, m *meta.Meta, evaluate *usecase.Evaluate) string { - is := evaluate.RunMetadata(ctx, m.GetTitle()) - result, err := encodeInlinesText(&is, wui.gentext) - if err != nil { - return err.Error() - } - return result + return encodeEvaluatedTitleText(m, evalMetadata, wui.gentext), 1 + } } func encodeZmkMetadata(value string, evalMetadata evalMetadataFunc, gen *htmlGenerator) string { is := evalMetadata(value) result, err := gen.InlinesString(&is) Index: web/adapter/webui/lists.go ================================================================== --- web/adapter/webui/lists.go +++ web/adapter/webui/lists.go @@ -10,182 +10,92 @@ package webui import ( "bytes" - "net/http" - "net/url" - "sort" - "strconv" - - "zettelstore.de/c/api" - "zettelstore.de/z/ast" - "zettelstore.de/z/box" - "zettelstore.de/z/domain/id" - "zettelstore.de/z/domain/meta" - "zettelstore.de/z/search" - "zettelstore.de/z/usecase" - "zettelstore.de/z/web/adapter" -) - -// MakeListHTMLMetaHandler creates a HTTP handler for rendering the list of -// zettel as HTML. -func (wui *WebUI) MakeListHTMLMetaHandler( - listMeta usecase.ListMeta, - listRole usecase.ListRoles, - listTags usecase.ListTags, - evaluate *usecase.Evaluate, -) http.HandlerFunc { - return func(w http.ResponseWriter, r *http.Request) { - query := r.URL.Query() - switch query.Get("_l") { - case "r": - wui.renderRolesList(w, r, listRole) - case "t": - wui.renderTagsList(w, r, listTags) - default: - wui.renderZettelList(w, r, listMeta, evaluate) - } - } -} - -func (wui *WebUI) renderZettelList( - w http.ResponseWriter, r *http.Request, - listMeta usecase.ListMeta, evaluate *usecase.Evaluate, -) { - query := r.URL.Query() - s := adapter.GetSearch(query) - ctx := r.Context() - if !s.EnrichNeeded() { - ctx = box.NoEnrichContext(ctx) - } - metaList, err := listMeta.Run(ctx, s) - if err != nil { - wui.reportError(ctx, w, err) - return - } - user := wui.getUser(ctx) - metas := wui.buildHTMLMetaList(metaList, func(val string) ast.InlineSlice { return evaluate.RunMetadataNoLink(ctx, val) }) - var base baseData - wui.makeBaseData(ctx, wui.rtConfig.GetDefaultLang(), wui.rtConfig.GetSiteName(), "", user, &base) - wui.renderTemplate(ctx, w, id.ListTemplateZid, &base, struct { - Title string - SearchURL string - SearchValue string - QueryKeySearch string - Metas []simpleLink - }{ - Title: wui.listTitleSearch(s), - SearchURL: base.SearchURL, - SearchValue: s.String(), - QueryKeySearch: base.QueryKeySearch, - Metas: metas, - }) -} - -type roleInfo struct { - Text string - URL string -} - -func (wui *WebUI) renderRolesList(w http.ResponseWriter, r *http.Request, listRole usecase.ListRoles) { - ctx := r.Context() - roleArrangement, err := listRole.Run(ctx) - if err != nil { - wui.reportError(ctx, w, err) - return - } - roleList := roleArrangement.Counted() - roleList.SortByName() - - roleInfos := make([]roleInfo, len(roleList)) - for i, role := range roleList { - roleInfos[i] = roleInfo{role.Name, wui.NewURLBuilder('h').AppendQuery("role", role.Name).String()} - } - - user := wui.getUser(ctx) - var base baseData - wui.makeBaseData(ctx, wui.rtConfig.GetDefaultLang(), wui.rtConfig.GetSiteName(), "", user, &base) - wui.renderTemplate(ctx, w, id.RolesTemplateZid, &base, struct { - Roles []roleInfo - }{ - Roles: roleInfos, - }) -} - -type countInfo struct { - Count string - URL string -} - -type tagInfo struct { - Name string - URL string - iCount int - Count string - Size string -} - -const fontSizes = 6 // Must be the number of CSS classes zs-font-size-* in base.css - -func (wui *WebUI) renderTagsList(w http.ResponseWriter, r *http.Request, listTags usecase.ListTags) { - ctx := r.Context() - iMinCount, err := strconv.Atoi(r.URL.Query().Get("min")) - if err != nil || iMinCount < 0 { - iMinCount = 0 - } - tagData, err := listTags.Run(ctx, iMinCount) - if err != nil { - wui.reportError(ctx, w, err) - return - } - - user := wui.getUser(ctx) - tagsList := make([]tagInfo, 0, len(tagData)) - countMap := make(map[int]int) - baseTagListURL := wui.NewURLBuilder('h') - for tag, ml := range tagData { - count := len(ml) - countMap[count]++ - tagsList = append( - tagsList, - tagInfo{tag, baseTagListURL.AppendQuery(api.KeyAllTags, tag).String(), count, "", ""}) - baseTagListURL.ClearQuery() - } - sort.Slice(tagsList, func(i, j int) bool { return tagsList[i].Name < tagsList[j].Name }) - - countList := make([]int, 0, len(countMap)) - for count := range countMap { - countList = append(countList, count) - } - sort.Ints(countList) - for pos, count := range countList { - countMap[count] = (pos * 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]) - } - - var base baseData - wui.makeBaseData(ctx, wui.rtConfig.GetDefaultLang(), wui.rtConfig.GetSiteName(), "", user, &base) - minCounts := make([]countInfo, 0, len(countList)) - for _, c := range countList { - sCount := strconv.Itoa(c) - minCounts = append(minCounts, countInfo{sCount, base.ListTagsURL + "&min=" + sCount}) - } - - wui.renderTemplate(ctx, w, id.TagsTemplateZid, &base, struct { - ListTagsURL string - MinCounts []countInfo - Tags []tagInfo - }{ - ListTagsURL: base.ListTagsURL, - MinCounts: minCounts, - Tags: tagsList, - }) + "context" + "encoding/xml" + "io" + "net/http" + "net/url" + "strings" + + "zettelstore.de/c/api" + "zettelstore.de/z/box" + "zettelstore.de/z/domain/id" + "zettelstore.de/z/domain/meta" + "zettelstore.de/z/encoding/rss" + "zettelstore.de/z/evaluator" + "zettelstore.de/z/query" + "zettelstore.de/z/usecase" + "zettelstore.de/z/web/adapter" + "zettelstore.de/z/web/server" +) + +// MakeListHTMLMetaHandler creates a HTTP handler for rendering the list of +// zettel as HTML. +func (wui *WebUI) MakeListHTMLMetaHandler(listMeta usecase.ListMeta, evaluate *usecase.Evaluate) http.HandlerFunc { + return func(w http.ResponseWriter, r *http.Request) { + q := adapter.GetQuery(r.URL.Query()) + ctx := r.Context() + if !q.EnrichNeeded() { + ctx = box.NoEnrichContext(ctx) + } + metaList, err := listMeta.Run(ctx, q) + if err != nil { + wui.reportError(ctx, w, err) + return + } + if actions := q.Actions(); len(actions) > 0 && actions[0] == "RSS" { + wui.renderRSS(ctx, w, q, metaList) + return + } + bns := evaluate.RunBlockNode(ctx, evaluator.QueryAction(ctx, q, metaList, wui.rtConfig)) + enc := wui.getSimpleHTMLEncoder() + htmlContent, err := enc.BlocksString(&bns) + if err != nil { + wui.reportError(ctx, w, err) + return + } + user := server.GetUser(ctx) + var base baseData + wui.makeBaseData(ctx, wui.rtConfig.Get(ctx, nil, api.KeyLang), wui.rtConfig.GetSiteName(), "", user, &base) + wui.renderTemplate(ctx, w, id.ListTemplateZid, &base, struct { + Title string + SearchURL string + QueryValue string + QueryKeyQuery string + Content string + }{ + Title: wui.listTitleQuery(q), + SearchURL: base.SearchURL, + QueryValue: q.String(), + QueryKeyQuery: base.QueryKeyQuery, + Content: htmlContent, + }) + } +} + +func (wui *WebUI) renderRSS(ctx context.Context, w http.ResponseWriter, q *query.Query, ml []*meta.Meta) { + var rssConfig rss.Configuration + rssConfig.Setup(ctx, wui.rtConfig) + if actions := q.Actions(); len(actions) > 2 && actions[1] == "TITLE" { + rssConfig.Title = strings.Join(actions[2:], " ") + } + data, err := rssConfig.Marshal(q, ml) + if err != nil { + wui.reportError(ctx, w, err) + return + } + adapter.PrepareHeader(w, rss.ContentType) + w.WriteHeader(http.StatusOK) + if _, err = io.WriteString(w, xml.Header); err == nil { + _, err = w.Write(data) + } + if err != nil { + wui.log.IfErr(err).Msg("unable to write RSS data") + } } // MakeZettelContextHandler creates a new HTTP handler for the use case "zettel context". func (wui *WebUI) MakeZettelContextHandler(getContext usecase.ZettelContext, evaluate *usecase.Evaluate) http.HandlerFunc { return func(w http.ResponseWriter, r *http.Request) { @@ -201,43 +111,47 @@ limit := getIntParameter(q, api.QueryKeyLimit, 200) metaList, err := getContext.Run(ctx, zid, dir, depth, limit) if err != nil { wui.reportError(ctx, w, err) return + } + bns := evaluate.RunBlockNode(ctx, evaluator.QueryAction(ctx, nil, metaList, wui.rtConfig)) + enc := wui.getSimpleHTMLEncoder() + htmlContent, err := enc.BlocksString(&bns) + if err != nil { + wui.reportError(ctx, w, err) + return } apiZid := api.ZettelID(zid.String()) - metaLinks := wui.buildHTMLMetaList(metaList, func(val string) ast.InlineSlice { return evaluate.RunMetadataNoLink(ctx, val) }) depths := []string{"2", "3", "4", "5", "6", "7", "8", "9", "10"} depthLinks := make([]simpleLink, len(depths)) depthURL := wui.NewURLBuilder('k').SetZid(apiZid) for i, depth := range depths { depthURL.ClearQuery() switch dir { case usecase.ZettelContextBackward: - depthURL.AppendQuery(api.QueryKeyDir, api.DirBackward) + depthURL.AppendKVQuery(api.QueryKeyDir, api.DirBackward) case usecase.ZettelContextForward: - depthURL.AppendQuery(api.QueryKeyDir, api.DirForward) + depthURL.AppendKVQuery(api.QueryKeyDir, api.DirForward) } - depthURL.AppendQuery(api.QueryKeyDepth, depth) + depthURL.AppendKVQuery(api.QueryKeyDepth, depth) depthLinks[i].Text = depth depthLinks[i].URL = depthURL.String() } var base baseData - user := wui.getUser(ctx) - wui.makeBaseData(ctx, wui.rtConfig.GetDefaultLang(), wui.rtConfig.GetSiteName(), "", user, &base) + user := server.GetUser(ctx) + wui.makeBaseData(ctx, wui.rtConfig.Get(ctx, nil, api.KeyLang), wui.rtConfig.GetSiteName(), "", user, &base) wui.renderTemplate(ctx, w, id.ContextTemplateZid, &base, struct { Title string InfoURL string Depths []simpleLink - Start simpleLink - Metas []simpleLink + Content string }{ Title: "Zettel Context", InfoURL: wui.NewURLBuilder('i').SetZid(apiZid).String(), Depths: depthLinks, - Start: metaLinks[0], - Metas: metaLinks[1:], + Content: htmlContent, }) } } func getIntParameter(q url.Values, key string, minValue int) int { @@ -246,26 +160,13 @@ return minValue } return val } -func (wui *WebUI) listTitleSearch(s *search.Search) string { - if s == nil { +func (wui *WebUI) listTitleQuery(q *query.Query) string { + if q == nil { return wui.rtConfig.GetSiteName() } var buf bytes.Buffer - s.PrintHuman(&buf) + q.PrintHuman(&buf) return buf.String() } - -// buildHTMLMetaList builds a zettel list based on a meta list for HTML rendering. -func (wui *WebUI) buildHTMLMetaList(metaList []*meta.Meta, evalMetadata evalMetadataFunc) []simpleLink { - metas := make([]simpleLink, 0, len(metaList)) - encHTML := wui.getSimpleHTMLEncoder() - for _, m := range metaList { - metas = append(metas, simpleLink{ - Text: encodeZmkMetadata(m.GetTitle(), evalMetadata, encHTML), - URL: wui.NewURLBuilder('h').SetZid(api.ZettelID(m.Zid.String())).String(), - }) - } - return metas -} Index: web/adapter/webui/login.go ================================================================== --- web/adapter/webui/login.go +++ web/adapter/webui/login.go @@ -12,10 +12,11 @@ import ( "context" "net/http" + "zettelstore.de/c/api" "zettelstore.de/z/auth" "zettelstore.de/z/domain/id" "zettelstore.de/z/usecase" "zettelstore.de/z/web/adapter" ) @@ -34,11 +35,11 @@ } } func (wui *WebUI) renderLoginForm(ctx context.Context, w http.ResponseWriter, retry bool) { var base baseData - wui.makeBaseData(ctx, wui.rtConfig.GetDefaultLang(), "Login", "", nil, &base) + wui.makeBaseData(ctx, wui.rtConfig.Get(ctx, nil, api.KeyLang), "Login", "", nil, &base) wui.renderTemplate(ctx, w, id.LoginTemplateZid, &base, struct { Title string Retry bool }{ Title: base.Title, Index: web/adapter/webui/rename_zettel.go ================================================================== --- web/adapter/webui/rename_zettel.go +++ web/adapter/webui/rename_zettel.go @@ -15,15 +15,15 @@ "net/http" "strings" "zettelstore.de/c/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" + "zettelstore.de/z/web/server" ) // MakeGetRenameZettelHandler creates a new HTTP handler to display the // HTML rename view of a zettel. func (wui *WebUI) MakeGetRenameZettelHandler(getMeta usecase.GetMeta, evaluate *usecase.Evaluate) http.HandlerFunc { @@ -39,17 +39,17 @@ if err != nil { wui.reportError(ctx, w, err) return } - getTextTitle := wui.makeGetTextTitle(ctx, getMeta, evaluate) + getTextTitle := wui.makeGetTextTitle(createGetMetadataFunc(ctx, getMeta), createEvalMetadataFunc(ctx, evaluate)) incomingLinks := wui.encodeIncoming(m, getTextTitle) uselessFiles := retrieveUselessFiles(m) - user := wui.getUser(ctx) + user := server.GetUser(ctx) var base baseData - wui.makeBaseData(ctx, config.GetLang(m, wui.rtConfig), "Rename Zettel "+zid.String(), "", user, &base) + wui.makeBaseData(ctx, wui.rtConfig.Get(ctx, m, api.KeyLang), "Rename Zettel "+zid.String(), "", user, &base) wui.renderTemplate(ctx, w, id.RenameTemplateZid, &base, struct { Zid string MetaPairs []meta.Pair HasIncoming bool Incoming []simpleLink ADDED web/adapter/webui/titleenc.go Index: web/adapter/webui/titleenc.go ================================================================== --- web/adapter/webui/titleenc.go +++ web/adapter/webui/titleenc.go @@ -0,0 +1,30 @@ +//----------------------------------------------------------------------------- +// Copyright (c) 2020-2022 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 webui + +import ( + "zettelstore.de/z/domain/meta" + "zettelstore.de/z/encoder/textenc" +) + +func encodeEvaluatedTitleHTML(m *meta.Meta, evalMetadata evalMetadataFunc, gen *htmlGenerator) string { + return encodeZmkMetadata(m.GetTitle(), evalMetadata, gen) +} + +func encodeEvaluatedTitleText(m *meta.Meta, evalMetadata evalMetadataFunc, enc *textenc.Encoder) string { + is := evalMetadata(m.GetTitle()) + result, err := encodeInlinesText(&is, enc) + if err != nil { + return err.Error() + } + return result + +} Index: web/adapter/webui/webui.go ================================================================== --- web/adapter/webui/webui.go +++ web/adapter/webui/webui.go @@ -97,16 +97,16 @@ tokenLifetime: kernel.Main.GetConfig(kernel.WebService, kernel.WebTokenLifetimeHTML).(time.Duration), cssBaseURL: ab.NewURLBuilder('z').SetZid(api.ZidBaseCSS).String(), cssUserURL: ab.NewURLBuilder('z').SetZid(api.ZidUserCSS).String(), 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(), - refreshURL: ab.NewURLBuilder('g').AppendQuery("_c", "r").String(), + listRolesURL: ab.NewURLBuilder('h').AppendQuery(api.ActionSeparator + api.KeyRole).String(), + listTagsURL: ab.NewURLBuilder('h').AppendQuery(api.ActionSeparator + api.KeyAllTags).String(), + refreshURL: ab.NewURLBuilder('g').AppendKVQuery("_c", "r").String(), withAuth: authz.WithAuth(), loginURL: loginoutBase.String(), - logoutURL: loginoutBase.AppendQuery("logout", "").String(), + logoutURL: loginoutBase.AppendKVQuery("logout", "").String(), searchURL: ab.NewURLBuilder('h').String(), } wui.observe(box.UpdateInfo{Box: mgr, Reason: box.OnReload, Zid: id.Invalid}) mgr.RegisterObserver(wui.observe) return wui @@ -252,11 +252,11 @@ CanRefresh bool RefreshURL string HasNewZettelLinks bool NewZettelLinks []simpleLink SearchURL string - QueryKeySearch string + QueryKeyQuery string Content string FooterHTML string DebugMode bool } @@ -290,20 +290,18 @@ data.CanRefresh = wui.canRefresh(user) data.RefreshURL = wui.refreshURL data.HasNewZettelLinks = len(newZettelLinks) > 0 data.NewZettelLinks = newZettelLinks data.SearchURL = wui.searchURL - data.QueryKeySearch = api.QueryKeySearch - data.FooterHTML = wui.rtConfig.GetFooterHTML() + data.QueryKeyQuery = api.QueryKeyQuery + data.FooterHTML = wui.rtConfig.Get(ctx, nil, config.KeyFooterHTML) data.DebugMode = wui.debug } -func (wui *WebUI) getSimpleHTMLEncoder() *htmlGenerator { - return createGenerator(wui, "", false) -} -func (wui *WebUI) createZettelEncoder() *htmlGenerator { - return createGenerator(wui, wui.rtConfig.GetMarkerExternal(), true) +func (wui *WebUI) getSimpleHTMLEncoder() *htmlGenerator { return createGenerator(wui, "") } +func (wui *WebUI) createZettelEncoder(ctx context.Context, m *meta.Meta) *htmlGenerator { + return createGenerator(wui, wui.rtConfig.Get(ctx, m, config.KeyMarkerExternal)) } // htmlAttrNewWindow returns HTML attribute string for opening a link in a new window. // If hasURL is false an empty string is returned. func htmlAttrNewWindow(hasURL bool) string { @@ -320,11 +318,11 @@ } menu, err := wui.box.GetZettel(ctx, id.TOCNewTemplateZid) if err != nil { return nil } - refs := collect.Order(parser.ParseZettel(menu, "", wui.rtConfig)) + refs := collect.Order(parser.ParseZettel(ctx, menu, "", wui.rtConfig)) for _, ref := range refs { zid, err2 := id.Parse(ref.URL.Path) if err2 != nil { continue } @@ -345,11 +343,11 @@ } } result = append(result, simpleLink{ Text: menuTitle, URL: wui.NewURLBuilder('c').SetZid(api.ZettelID(m.Zid.String())). - AppendQuery(queryKeyAction, valueActionNew).String(), + AppendKVQuery(queryKeyAction, valueActionNew).String(), }) } return result } @@ -365,11 +363,11 @@ func (wui *WebUI) reportError(ctx context.Context, w http.ResponseWriter, err error) { code, text := adapter.CodeMessageFromError(err) if code == http.StatusInternalServerError { wui.log.Error().Msg(err.Error()) } - user := wui.getUser(ctx) + user := server.GetUser(ctx) var base baseData wui.makeBaseData(ctx, api.ValueLangEN, "Error", "", user, &base) wui.renderTemplateStatus(ctx, w, code, id.ErrorTemplateZid, &base, struct { ErrorTitle string ErrorText string @@ -397,11 +395,11 @@ if err != nil { wui.log.IfErr(err).Zid(templateID).Msg("Unable to get template") http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError) return } - if user := wui.getUser(ctx); user != nil { + if user := server.GetUser(ctx); user != nil { if tok, err1 := wui.token.GetToken(user, wui.tokenLifetime, auth.KindHTML); err1 == nil { wui.setToken(w, tok) } } var content bytes.Buffer @@ -414,12 +412,10 @@ if err != nil { wui.log.IfErr(err).Msg("Unable to write HTML via template") } } -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) } Index: web/server/impl/impl.go ================================================================== --- web/server/impl/impl.go +++ web/server/impl/impl.go @@ -23,20 +23,22 @@ "zettelstore.de/z/web/server" ) type myServer struct { log *logger.Logger + baseURL string server httpServer router httpRouter persistentCookie bool secureCookie bool } // New creates a new web server. -func New(log *logger.Logger, listenAddr, urlPrefix string, persistentCookie, secureCookie bool, maxRequestSize int64, auth auth.TokenManager) server.Server { +func New(log *logger.Logger, listenAddr, baseURL, urlPrefix string, persistentCookie, secureCookie bool, maxRequestSize int64, auth auth.TokenManager) server.Server { srv := myServer{ log: log, + baseURL: baseURL, persistentCookie: persistentCookie, secureCookie: secureCookie, } srv.router.initializeRouter(log, urlPrefix, maxRequestSize, auth) srv.server.initializeHTTPServer(listenAddr, &srv.router) @@ -53,21 +55,19 @@ srv.router.addZettelRoute(key, method, handler) } func (srv *myServer) SetUserRetriever(ur server.UserRetriever) { srv.router.ur = ur } -func (srv *myServer) GetUser(ctx context.Context) *meta.Meta { - if data := srv.GetAuthData(ctx); data != nil { - return data.User - } - return nil + +func (srv *myServer) GetURLPrefix() string { + return srv.router.urlPrefix } func (srv *myServer) NewURLBuilder(key byte) *api.URLBuilder { return api.NewURLBuilder(srv.GetURLPrefix(), key) } -func (srv *myServer) GetURLPrefix() string { - return srv.router.urlPrefix +func (srv *myServer) NewURLBuilderAbs(key byte) *api.URLBuilder { + return api.NewURLBuilder(srv.baseURL, key) } const sessionName = "zsession" func (srv *myServer) SetToken(w http.ResponseWriter, token []byte, d time.Duration) { @@ -90,40 +90,27 @@ } } // ClearToken invalidates the session cookie by sending an empty one. func (srv *myServer) ClearToken(ctx context.Context, w http.ResponseWriter) context.Context { - if authData := srv.GetAuthData(ctx); authData == nil { + if authData := server.GetAuthData(ctx); authData == nil { // No authentication data stored in session, nothing to do. return ctx } if w != nil { srv.SetToken(w, nil, 0) } return updateContext(ctx, nil, nil) } -// GetAuthData returns the full authentication data from the context. -func (*myServer) GetAuthData(ctx context.Context) *server.AuthData { - data, ok := ctx.Value(ctxKeySession).(*server.AuthData) - if ok { - return data - } - return nil -} - -type ctxKeyTypeSession struct{} - -var ctxKeySession ctxKeyTypeSession - func updateContext(ctx context.Context, user *meta.Meta, data *auth.TokenData) context.Context { if data == nil { - return context.WithValue(ctx, ctxKeySession, &server.AuthData{User: user}) + return context.WithValue(ctx, server.CtxKeySession, &server.AuthData{User: user}) } return context.WithValue( ctx, - ctxKeySession, + server.CtxKeySession, &server.AuthData{ User: user, Token: data.Token, Now: data.Now, Issued: data.Issued, Index: web/server/server.go ================================================================== --- web/server/server.go +++ web/server/server.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. //----------------------------------------------------------------------------- @@ -50,22 +50,20 @@ // Builder allows to build new URLs for the web service. type Builder interface { GetURLPrefix() string NewURLBuilder(key byte) *api.URLBuilder + NewURLBuilderAbs(key byte) *api.URLBuilder } // Auth is the authencation interface. type Auth interface { - GetUser(context.Context) *meta.Meta + // SetToken sends the token to the client. SetToken(w http.ResponseWriter, token []byte, d time.Duration) // ClearToken invalidates the session cookie by sending an empty one. ClearToken(ctx context.Context, w http.ResponseWriter) context.Context - - // GetAuthData returns the full authentication data from the context. - GetAuthData(ctx context.Context) *AuthData } // AuthData stores all relevant authentication data for a context. type AuthData struct { User *meta.Meta @@ -72,10 +70,35 @@ Token []byte Now time.Time Issued time.Time Expires time.Time } + +// GetAuthData returns the full authentication data from the context. +func GetAuthData(ctx context.Context) *AuthData { + if ctx != nil { + data, ok := ctx.Value(CtxKeySession).(*AuthData) + if ok { + return data + } + } + return nil +} + +// GetUser returns the metadata of the current user, or nil if there is no one. +func GetUser(ctx context.Context) *meta.Meta { + if data := GetAuthData(ctx); data != nil { + return data.User + } + return nil +} + +// CtxKeyTypeSession is just an additional type to make context value retrieval unambiguous. +type CtxKeyTypeSession struct{} + +// CtxKeySession is the key value to retrieve Authdata +var CtxKeySession CtxKeyTypeSession // AuthBuilder is a Builder that also allows to execute authentication functions. type AuthBuilder interface { Auth Builder Index: www/changes.wiki ================================================================== --- www/changes.wiki +++ www/changes.wiki @@ -1,11 +1,106 @@ <title>Change Log + +

    Changes for Version 0.8.0 (pending)

    + -

    Changes for Version 0.7.0 (pending)

    +

    Changes for Version 0.7.1 (2022-09-18)

    + * Produce a RSS feed compatible to Miniflux. + (minor) + * Make sure to always produce a pubdata in RSS feed. + (bug) + * Prefix search for data that looks like a zettel identifier may end with a + 0. + (bug) + * Fix glitch on manual zettel. + (bug) + +

    Changes for Version 0.7.0 (2022-09-17)

    + * Removes support for URL query parameter to search for metadata values, + sorting, offset, and limit a zettel list. Deprecated in version 0.6.0 + (breaking: api, webui) + * Allow to search for the existence / non-existence of a metadata key with + the "?" operator: key? and key!?. Previously, the ":" + operator was used for this by specifying an empty search value. Now you + can use the ":" operator to find empty / non-empty metadata values. If you + specify a search operator for metadata, the specified key is assumed to + exist. + (breaking: api, webui) + * Rename “search expression” into “query + expressions”. Similar, the reference prefix search: to + specify a query link or a query transclusion is renamed to query: + (breaking: zettelmarkup) + * Rename query parameter for query expression from _s to + q. + (breaking: api, webui) + * Cleanup names for HTTP query parameters in WebUI. Update your bookmarks + if you used them. (For API: see below) + (breaking: webui) + * Allow search terms to be OR-ed. This allows to specify any search + expression in disjunctive normal form. Therefore, the NEGATE term is not + needed any more. + (breaking: api, webui) + * Replace runtime configuration default-lang with lang. + Additionally, lang set at the zettel of the current user, will + provide a default value for the current user, overwriting the global + default value. + (breaking) + * Add new syntax pikchr, a markup language for diagrams in + technical documentation. + (major) + * Add endpoint /q to query the zettelstore and aggregate resulting + values. This is done by extending the query syntax. + (major: api) + * Add support for query actions. Actions may aggregate w.r.t. some metadata + keys, or produce an RSS feed. + (major: api, webui) + * Query results can be ordered for more than one metadata key. Ordering by + zettel identifier is an implicit last order expression to produce stable + results. + (minor: api, webui) + * Add support for an asset directory, accessible via URL prefix + /assests/. + (minor: server) + * Add support for metadata key created, a timestamp when the zettel + was created. Since key published is now either created + or modified, it will now always contains a valid time stamp. + (minor) + * Add support for metadata key author. It will be displayed on a + zettel, if set. + (minor: webui) + * Remove CSS for lists. The browsers default value for padding-left + will be used. + (minor: webui) + * Removed templates for rendering roles and tags lists. This is now done by + query actions. + (minor: webui) + * Tags within zettel content are deprecated in version 0.8. This affects the + computed metadata keys content-tags and all-tags. They + will be removed. The number sign of a content tag introduces unintended + tags, esp. in the english language; content tags may occur within links + → links within links, when rendered as HTML; content tags may occur + in the title of a zettel; naming of content tags, zettel tags, and their + union is confusing for many. Migration: use zettel tags or replace content + tag with a search. + (deprecated: zettelmarkup) + * Cleanup names for HTTP query parameter for API calls. Essentially, + underscore characters in front are removed. Please use new names, old + names will be deprecated in version 0.8. + (deprecated: api) + * Some smaller bug fixes and improvements, to the software and to the + documentation. +

    Changes for Version 0.6.2 (2022-08-22)

    + * Recognize renaming of zettel file external to Zettelstore. + (bug) + +

    Changes for Version 0.6.1 (2022-08-22)

    + * Ignore empty tags when reading metadata. + (bug) +

    Changes for Version 0.6.0 (2022-08-11)

    * Translating of "..." into horizontal ellipsis is no longer supported. Use &hellip; instead. (breaking: zettelmarkup) * Allow to specify search expressions, which allow to specify search @@ -31,11 +126,11 @@ ordering, an offset, and a limit for the resulting list, will be removed in version 0.7. Replace these with the more useable search expressions. Please be aware that the = search operator is also deprecated. It was only introduced to help the migration. (deprecated: api, webui) - * Some smaller bug fixes and inprovements, to the software and to the + * Some smaller bug fixes and improvements, to the software and to the documentation.

    Changes for Version 0.5.1 (2022-08-02)

    * Log missing authentication tokens in debug level (was: sense level) @@ -96,11 +191,11 @@ web server log messages. (minor: web server) * New startup configuration key max-request-size to limit a web request body to prevent client sending too large requests. (minor: web server) - * Many smaller bug fixes and inprovements, to the software and to the + * Many smaller bug fixes and improvements, to the software and to the documentation.

    Changes for Version 0.4 (2022-03-08)

    * Encoding “djson” renamed to “zjson” (zettel @@ -152,11 +247,11 @@ to use it with public access. (minor: box) * Disallow to cache the authentication cookie. Will remove most unexpected log-outs when using a mobile device. (minor: webui) - * Many smaller bug fixes and inprovements, to the software and to the + * Many smaller bug fixes and improvements, to the software and to the documentation.

    Changes for Version 0.3 (2022-02-09)

    * Zettel files with extension .meta are now treated as content @@ -174,11 +269,11 @@ access rights for the given zettel. (minor: api) * A previously duplicate file that is now useful (because another file was deleted) is now logged as such. (minor: directory and file/zip box) - * Many smaller bug fixes and inprovements, to the software and to the + * Many smaller bug fixes and improvements, to the software and to the documentation.

    Changes for Version 0.2 (2022-01-19)

    * v0.2.1 (2021-02-01) updates the license year in some documents @@ -240,11 +335,11 @@ * Metadata key duplicates stores the duplicate file names, instead of just a boolean value that there were duplicate file names. (minor) * Document autostarting Zettelstore on Windows, macOS, and Linux. (minor) - * Many smaller bug fixes and inprovements, to the software and to the + * Many smaller bug fixes and improvements, to the software and to the documentation.

    Changes for Version 0.1 (2021-11-11)

    * v0.1.3 (2021-12-15) fixes a bug where the modification date could be set @@ -305,11 +400,11 @@ (minor: webui) * Separate repository for [https://zettelstore.de/contrib/|contributed] software. First entry is a software for creating a presentation by using zettel. (info) - * Many smaller bug fixes and inprovements, to the software and to the + * Many smaller bug fixes and improvements, to the software and to the documentation.

    Changes for Version 0.0.15 (2021-09-17)

    * Move again endpoint characters for authentication to make room for future @@ -360,11 +455,11 @@ * Add API endpoint /p/{ID} to retrieve a parsed, but not evaluated zettel in various encodings. (minor: api) * Fix: do not list a shadowed zettel that matches the select criteria. (minor) - * Many smaller bug fixes and inprovements, to the software and to the + * Many smaller bug fixes and improvements, to the software and to the documentation.

    Changes for Version 0.0.14 (2021-07-23)

    * Rename “place” into “box”. This also affects the @@ -409,11 +504,11 @@ directory, these characters are preserved when zettel is updated. (bug) * The phase “filtering a zettel list” is more precise “selecting zettel” (documentation) - * Many smaller bug fixes and inprovements, to the software and to the + * Many smaller bug fixes and improvements, to the software and to the documentation.

    Changes for Version 0.0.13 (2021-06-01)

    * Startup configuration box-X-uri (where X is a @@ -453,11 +548,11 @@ (minor: webui) * Add zettelmarkup syntax for a table row that should be ignored: |%. This allows to paste output of the administrator console into a zettel. (minor: zmk) - * Many smaller bug fixes and inprovements, to the software and to the + * Many smaller bug fixes and improvements, to the software and to the documentation.

    Changes for Version 0.0.12 (2021-04-16)

    * Raise the per-process limit of open files on macOS to 1.048.576. This @@ -507,11 +602,11 @@ * Selecting zettel depending on tag values can be both by comparing only the prefix or the whole string. If a search value begins with '#', only zettel with the exact tag will be returned. Otherwise a zettel will be returned if the search string just matches the prefix of only one of its tags. (minor: api, webui) - * Many smaller bug fixes and inprovements, to the software and to the documentation. + * Many smaller bug fixes and improvements, to the software and to the documentation. A note for users of macOS: in the current release and with macOS's default values, a zettel directory must not contain more than approx. 250 files. There are three options to mitigate this limitation temporarily: # You update the per-process limit of open files on macOS. @@ -558,11 +653,11 @@ (“↗︎”), which also needed the additional “&#xfe0e;” to disable the conversion to an emoji on iPadOS. (minor: webui) * A pre-build binary for macOS ARM64 (also known as Apple silicon) is available. (minor: infrastructure) - * Many smaller bug fixes and inprovements, to the software and to the documentation. + * Many smaller bug fixes and improvements, to the software and to the documentation.

    Changes for Version 0.0.9 (2021-01-29)

    This is the first version that is managed by [https://fossil-scm.org|Fossil] instead of GitHub. To access older versions, use the Git repository under Index: www/download.wiki ================================================================== --- www/download.wiki +++ www/download.wiki @@ -7,20 +7,20 @@ * However, it is in use by the main developer since March 2020 without any damage. * It may be useful for you. It is useful for me. * Take a look at the [https://zettelstore.de/manual/|manual] to know how to start and use it.

    ZIP-ped Executables

    -Build: v0.6.0 (2022-08-11). +Build: v0.7.1 (2022-09-18). - * [/uv/zettelstore-0.6.0-linux-amd64.zip|Linux] (amd64) - * [/uv/zettelstore-0.6.0-linux-arm.zip|Linux] (arm6, e.g. Raspberry Pi) - * [/uv/zettelstore-0.6.0-windows-amd64.zip|Windows] (amd64) - * [/uv/zettelstore-0.6.0-darwin-amd64.zip|macOS] (amd64) - * [/uv/zettelstore-0.6.0-darwin-arm64.zip|macOS] (arm64, aka Apple silicon) + * [/uv/zettelstore-0.7.1-linux-amd64.zip|Linux] (amd64) + * [/uv/zettelstore-0.7.1-linux-arm.zip|Linux] (arm6, e.g. Raspberry Pi) + * [/uv/zettelstore-0.7.1-windows-amd64.zip|Windows] (amd64) + * [/uv/zettelstore-0.7.1-darwin-amd64.zip|macOS] (amd64) + * [/uv/zettelstore-0.7.1-darwin-arm64.zip|macOS] (arm64, aka Apple silicon) Unzip the appropriate file, install and execute Zettelstore according to the manual.

    Zettel for the manual

    As a starter, you can download the zettel for the manual -[/uv/manual-0.6.0.zip|here]. +[/uv/manual-0.7.0.zip|here]. Just unzip the contained files and put them into your zettel folder or configure a file box to read the zettel directly from the ZIP file. Index: www/index.wiki ================================================================== --- www/index.wiki +++ www/index.wiki @@ -22,17 +22,17 @@ software, which often connects to Zettelstore via its API. Some of the software packages may be experimental. [https://twitter.com/zettelstore|Stay tuned] …
    -

    Latest Release: 0.6.0 (2022-08-11)

    +

    Latest Release: 0.7.1 (2022-09-18)

    * [./download.wiki|Download] - * [./changes.wiki#0_6|Change summary] - * [/timeline?p=v0.6.0&bt=v0.5.0&y=ci|Check-ins for version 0.6.0], - [/vdiff?to=v0.6.0&from=v0.5.0|content diff] - * [/timeline?df=v0.6.0&y=ci|Check-ins derived from the 0.6.0 release], - [/vdiff?from=v0.6.0&to=trunk|content diff] + * [./changes.wiki#0_7|Change summary] + * [/timeline?p=v0.7.1&bt=v0.6.0&y=ci|Check-ins for version 0.7.1], + [/vdiff?to=v0.7.1&from=v0.6.0|content diff] + * [/timeline?df=v0.7.0&y=ci|Check-ins derived from the 0.7.0 release], + [/vdiff?from=v0.7.0&to=trunk|content diff] * [./plan.wiki|Limitations and planned improvements] * [/timeline?t=release|Timeline of all past releases]

    Build instructions