Index: .fossil-settings/ignore-glob ================================================================== --- .fossil-settings/ignore-glob +++ .fossil-settings/ignore-glob @@ -1,3 +1,2 @@ bin/* releases/* -parser/pikchr/*.out Index: Makefile ================================================================== --- Makefile +++ Makefile @@ -5,27 +5,27 @@ ## ## Zettelstore is licensed under the latest version of the EUPL (European Union ## Public License). Please see file LICENSE.txt for your rights and obligations ## under this license. -.PHONY: check relcheck api build release clean +.PHONY: check relcheck api version build release clean check: - go run tools/build.go check + go run tools/check/check.go relcheck: - go run tools/build.go relcheck + go run tools/check/check.go -r api: - go run tools/build.go testapi + go run tools/testapi/testapi.go version: - @echo $(shell go run tools/build.go version) + @echo $(shell go run tools/build/build.go version) build: - go run tools/build.go build + go run tools/build/build.go build release: - go run tools/build.go release + go run tools/build/build.go release clean: - go run tools/build.go clean + go run tools/clean/clean.go Index: VERSION ================================================================== --- VERSION +++ VERSION @@ -1,1 +1,1 @@ -0.16.0 +0.17.0 Index: ast/ast.go ================================================================== --- ast/ast.go +++ ast/ast.go @@ -4,10 +4,13 @@ // 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. +// +// SPDX-License-Identifier: EUPL-1.2 +// SPDX-FileCopyrightText: 2020-present Detlef Stern //----------------------------------------------------------------------------- // Package ast provides the abstract syntax tree for parsed zettel content. package ast Index: ast/block.go ================================================================== --- ast/block.go +++ ast/block.go @@ -4,10 +4,13 @@ // 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. +// +// SPDX-License-Identifier: EUPL-1.2 +// SPDX-FileCopyrightText: 2020-present Detlef Stern //----------------------------------------------------------------------------- package ast import "zettelstore.de/client.fossil/attrs" Index: ast/inline.go ================================================================== --- ast/inline.go +++ ast/inline.go @@ -4,10 +4,13 @@ // 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. +// +// SPDX-License-Identifier: EUPL-1.2 +// SPDX-FileCopyrightText: 2020-present Detlef Stern //----------------------------------------------------------------------------- package ast import ( Index: ast/ref.go ================================================================== --- ast/ref.go +++ ast/ref.go @@ -4,23 +4,27 @@ // 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. +// +// SPDX-License-Identifier: EUPL-1.2 +// SPDX-FileCopyrightText: 2020-present Detlef Stern //----------------------------------------------------------------------------- package ast import ( "net/url" "strings" + "zettelstore.de/client.fossil/api" "zettelstore.de/z/zettel/id" ) // QueryPrefix is the prefix that denotes a query expression. -const QueryPrefix = "query:" +const QueryPrefix = api.QueryPrefix // ParseReference parses a string and returns a reference. func ParseReference(s string) *Reference { if invalidReference(s) { return &Reference{URL: nil, Value: s, State: RefStateInvalid} Index: ast/ref_test.go ================================================================== --- ast/ref_test.go +++ ast/ref_test.go @@ -4,10 +4,13 @@ // 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. +// +// SPDX-License-Identifier: EUPL-1.2 +// SPDX-FileCopyrightText: 2020-present Detlef Stern //----------------------------------------------------------------------------- package ast_test import ( Index: ast/walk.go ================================================================== --- ast/walk.go +++ ast/walk.go @@ -4,10 +4,13 @@ // 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. +// +// SPDX-License-Identifier: EUPL-1.2 +// SPDX-FileCopyrightText: 2021-present Detlef Stern //----------------------------------------------------------------------------- package ast // Visitor is a visitor for walking the AST. Index: ast/walk_test.go ================================================================== --- ast/walk_test.go +++ ast/walk_test.go @@ -4,10 +4,13 @@ // 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. +// +// SPDX-License-Identifier: EUPL-1.2 +// SPDX-FileCopyrightText: 2021-present Detlef Stern //----------------------------------------------------------------------------- package ast_test import ( @@ -59,13 +62,13 @@ }, ), } v := benchVisitor{} b.ResetTimer() - for n := 0; n < b.N; n++ { + for range b.N { ast.Walk(&v, &root) } } type benchVisitor struct{} func (bv *benchVisitor) Visit(ast.Node) ast.Visitor { return bv } Index: auth/auth.go ================================================================== --- auth/auth.go +++ auth/auth.go @@ -4,10 +4,13 @@ // 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. +// +// SPDX-License-Identifier: EUPL-1.2 +// SPDX-FileCopyrightText: 2021-present Detlef Stern //----------------------------------------------------------------------------- // Package auth provides services for authentification / authorization. package auth Index: auth/cred/cred.go ================================================================== --- auth/cred/cred.go +++ auth/cred/cred.go @@ -4,10 +4,13 @@ // 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. +// +// SPDX-License-Identifier: EUPL-1.2 +// SPDX-FileCopyrightText: 2020-present Detlef Stern //----------------------------------------------------------------------------- // Package cred provides some function for handling credentials. package cred Index: auth/impl/digest.go ================================================================== --- auth/impl/digest.go +++ auth/impl/digest.go @@ -4,10 +4,13 @@ // 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. +// +// SPDX-License-Identifier: EUPL-1.2 +// SPDX-FileCopyrightText: 2023-present Detlef Stern //----------------------------------------------------------------------------- package impl import ( @@ -24,16 +27,19 @@ const digestAlg = crypto.SHA384 func sign(claim sx.Object, secret []byte) ([]byte, error) { var buf bytes.Buffer - sx.Print(&buf, claim) + _, err := sx.Print(&buf, claim) + if err != nil { + return nil, err + } token := make([]byte, encoding.EncodedLen(buf.Len())) encoding.Encode(token, buf.Bytes()) digest := hmac.New(digestAlg.New, secret) - _, err := digest.Write(buf.Bytes()) + _, err = digest.Write(buf.Bytes()) if err != nil { return nil, err } dig := digest.Sum(nil) encDig := make([]byte, encoding.EncodedLen(len(dig))) Index: auth/impl/impl.go ================================================================== --- auth/impl/impl.go +++ auth/impl/impl.go @@ -4,10 +4,13 @@ // 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. +// +// SPDX-License-Identifier: EUPL-1.2 +// SPDX-FileCopyrightText: 2021-present Detlef Stern //----------------------------------------------------------------------------- // Package impl provides services for authentification / authorization. package impl @@ -135,11 +138,11 @@ zid := id.Zid(vals[4].(sx.Int64)) if !zid.IsValid() { return ErrNoZid } - tokenData.Ident = ident.String() + tokenData.Ident = string(ident) tokenData.Issued = issued tokenData.Now = now tokenData.Expires = expires tokenData.Zid = zid return nil Index: auth/policy/anon.go ================================================================== --- auth/policy/anon.go +++ auth/policy/anon.go @@ -4,10 +4,13 @@ // 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. +// +// SPDX-License-Identifier: EUPL-1.2 +// SPDX-FileCopyrightText: 2020-present Detlef Stern //----------------------------------------------------------------------------- package policy import ( Index: auth/policy/box.go ================================================================== --- auth/policy/box.go +++ auth/policy/box.go @@ -4,10 +4,13 @@ // 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. +// +// SPDX-License-Identifier: EUPL-1.2 +// SPDX-FileCopyrightText: 2020-present Detlef Stern //----------------------------------------------------------------------------- package policy import ( Index: auth/policy/default.go ================================================================== --- auth/policy/default.go +++ auth/policy/default.go @@ -4,10 +4,13 @@ // 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. +// +// SPDX-License-Identifier: EUPL-1.2 +// SPDX-FileCopyrightText: 2020-present Detlef Stern //----------------------------------------------------------------------------- package policy import ( Index: auth/policy/owner.go ================================================================== --- auth/policy/owner.go +++ auth/policy/owner.go @@ -4,10 +4,13 @@ // 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. +// +// SPDX-License-Identifier: EUPL-1.2 +// SPDX-FileCopyrightText: 2020-present Detlef Stern //----------------------------------------------------------------------------- package policy import ( Index: auth/policy/policy.go ================================================================== --- auth/policy/policy.go +++ auth/policy/policy.go @@ -4,10 +4,13 @@ // 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. +// +// SPDX-License-Identifier: EUPL-1.2 +// SPDX-FileCopyrightText: 2020-present Detlef Stern //----------------------------------------------------------------------------- // Package policy provides some interfaces and implementation for authorizsation policies. package policy Index: auth/policy/policy_test.go ================================================================== --- auth/policy/policy_test.go +++ auth/policy/policy_test.go @@ -4,10 +4,13 @@ // 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. +// +// SPDX-License-Identifier: EUPL-1.2 +// SPDX-FileCopyrightText: 2020-present Detlef Stern //----------------------------------------------------------------------------- package policy import ( Index: auth/policy/readonly.go ================================================================== --- auth/policy/readonly.go +++ auth/policy/readonly.go @@ -4,10 +4,13 @@ // 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. +// +// SPDX-License-Identifier: EUPL-1.2 +// SPDX-FileCopyrightText: 2020-present Detlef Stern //----------------------------------------------------------------------------- package policy import "zettelstore.de/z/zettel/meta" Index: box/box.go ================================================================== --- box/box.go +++ box/box.go @@ -4,10 +4,13 @@ // 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. +// +// SPDX-License-Identifier: EUPL-1.2 +// SPDX-FileCopyrightText: 2020-present Detlef Stern //----------------------------------------------------------------------------- // Package box provides a generic interface to zettel boxes. package box Index: box/compbox/compbox.go ================================================================== --- box/compbox/compbox.go +++ box/compbox/compbox.go @@ -4,10 +4,13 @@ // 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. +// +// SPDX-License-Identifier: EUPL-1.2 +// SPDX-FileCopyrightText: 2020-present Detlef Stern //----------------------------------------------------------------------------- // Package compbox provides zettel that have computed content. package compbox Index: box/compbox/config.go ================================================================== --- box/compbox/config.go +++ box/compbox/config.go @@ -4,10 +4,13 @@ // 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. +// +// SPDX-License-Identifier: EUPL-1.2 +// SPDX-FileCopyrightText: 2020-present Detlef Stern //----------------------------------------------------------------------------- package compbox import ( Index: box/compbox/keys.go ================================================================== --- box/compbox/keys.go +++ box/compbox/keys.go @@ -4,10 +4,13 @@ // 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. +// +// SPDX-License-Identifier: EUPL-1.2 +// SPDX-FileCopyrightText: 2020-present Detlef Stern //----------------------------------------------------------------------------- package compbox import ( Index: box/compbox/log.go ================================================================== --- box/compbox/log.go +++ box/compbox/log.go @@ -4,10 +4,13 @@ // 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. +// +// SPDX-License-Identifier: EUPL-1.2 +// SPDX-FileCopyrightText: 2021-present Detlef Stern //----------------------------------------------------------------------------- package compbox import ( Index: box/compbox/manager.go ================================================================== --- box/compbox/manager.go +++ box/compbox/manager.go @@ -4,10 +4,13 @@ // 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. +// +// SPDX-License-Identifier: EUPL-1.2 +// SPDX-FileCopyrightText: 2021-present Detlef Stern //----------------------------------------------------------------------------- package compbox import ( Index: box/compbox/parser.go ================================================================== --- box/compbox/parser.go +++ box/compbox/parser.go @@ -4,10 +4,13 @@ // 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. +// +// SPDX-License-Identifier: EUPL-1.2 +// SPDX-FileCopyrightText: 2021-present Detlef Stern //----------------------------------------------------------------------------- package compbox import ( Index: box/compbox/version.go ================================================================== --- box/compbox/version.go +++ box/compbox/version.go @@ -4,10 +4,13 @@ // 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. +// +// SPDX-License-Identifier: EUPL-1.2 +// SPDX-FileCopyrightText: 2020-present Detlef Stern //----------------------------------------------------------------------------- package compbox import ( Index: box/constbox/base.css ================================================================== --- box/constbox/base.css +++ box/constbox/base.css @@ -1,5 +1,19 @@ +/*----------------------------------------------------------------------------- + * Copyright (c) 2020-present 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. + * + * SPDX-License-Identifier: EUPL-1.2 + * SPDX-FileCopyrightText: 2020-present Detlef Stern + *----------------------------------------------------------------------------- + */ + *,*::before,*::after { box-sizing: border-box; } html { font-size: 1rem; Index: box/constbox/base.sxn ================================================================== --- box/constbox/base.sxn +++ box/constbox/base.sxn @@ -1,5 +1,18 @@ +;;;---------------------------------------------------------------------------- +;;; Copyright (c) 2023-present 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. +;;; +;;; SPDX-License-Identifier: EUPL-1.2 +;;; SPDX-FileCopyrightText: 2023-present Detlef Stern +;;;---------------------------------------------------------------------------- + `(@@@@ (html ,@(if lang `((@ (lang ,lang)))) (head (meta (@ (charset "utf-8"))) (meta (@ (name "viewport") (content "width=device-width, initial-scale=1.0"))) @@ -6,11 +19,11 @@ (meta (@ (name "generator") (content "Zettelstore"))) (meta (@ (name "format-detection") (content "telephone=no"))) ,@META-HEADER (link (@ (rel "stylesheet") (href ,css-base-url))) (link (@ (rel "stylesheet") (href ,css-user-url))) - ,@(ROLE-DEFAULT-meta (current-environment)) + ,@(ROLE-DEFAULT-meta (current-binding)) (title ,title)) (body (nav (@ (class "zs-menu")) (a (@ (href ,home-url)) "Home") ,@(if with-auth @@ -37,12 +50,14 @@ (button "New") (nav (@ (class "zs-dropdown-content")) ,@(map wui-link new-zettel-links) ))) ) - (form (@ (action ,search-url)) - (input (@ (type "text") (placeholder "Search..") (name ,query-key-query) (dir "auto")))) + (search (form (@ (action ,search-url)) + (input (@ (type "search") (inputmode "search") (name ,query-key-query) + (title "General search field, with same behaviour as search field in search result list") + (placeholder "Search..") (dir "auto"))))) ) (main (@ (class "content")) ,DETAIL) ,@(if FOOTER `((footer (hr) ,@FOOTER))) ,@(if debug-mode '((div (b "WARNING: Debug mode is enabled. DO NOT USE IN PRODUCTION!")))) ))) Index: box/constbox/constbox.go ================================================================== --- box/constbox/constbox.go +++ box/constbox/constbox.go @@ -4,10 +4,13 @@ // 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. +// +// SPDX-License-Identifier: EUPL-1.2 +// SPDX-FileCopyrightText: 2020-present Detlef Stern //----------------------------------------------------------------------------- // Package constbox puts zettel inside the executable. package constbox @@ -176,100 +179,101 @@ constHeader{ api.KeyTitle: "Zettelstore Base HTML Template", api.KeyRole: api.ValueRoleConfiguration, api.KeySyntax: meta.SyntaxSxn, api.KeyCreated: "20230510155100", - api.KeyModified: "20230827212200", + api.KeyModified: "20240219145300", api.KeyVisibility: api.ValueVisibilityExpert, }, zettel.NewContent(contentBaseSxn)}, id.LoginTemplateZid: { constHeader{ api.KeyTitle: "Zettelstore Login Form HTML Template", api.KeyRole: api.ValueRoleConfiguration, api.KeySyntax: meta.SyntaxSxn, api.KeyCreated: "20200804111624", - api.KeyModified: "20230527144100", + api.KeyModified: "20240219145200", api.KeyVisibility: api.ValueVisibilityExpert, }, zettel.NewContent(contentLoginSxn)}, id.ZettelTemplateZid: { constHeader{ api.KeyTitle: "Zettelstore Zettel HTML Template", api.KeyRole: api.ValueRoleConfiguration, api.KeySyntax: meta.SyntaxSxn, api.KeyCreated: "20230510155300", - api.KeyModified: "20231126180500", + api.KeyModified: "20240219145100", api.KeyVisibility: api.ValueVisibilityExpert, }, zettel.NewContent(contentZettelSxn)}, id.InfoTemplateZid: { constHeader{ api.KeyTitle: "Zettelstore Info HTML Template", api.KeyRole: api.ValueRoleConfiguration, api.KeySyntax: meta.SyntaxSxn, api.KeyCreated: "20200804111624", - api.KeyModified: "20231023152000", + api.KeyModified: "20240219145200", api.KeyVisibility: api.ValueVisibilityExpert, }, zettel.NewContent(contentInfoSxn)}, id.FormTemplateZid: { constHeader{ api.KeyTitle: "Zettelstore Form HTML Template", api.KeyRole: api.ValueRoleConfiguration, api.KeySyntax: meta.SyntaxSxn, api.KeyCreated: "20200804111624", - api.KeyModified: "20230621132600", + api.KeyModified: "20240219145200", api.KeyVisibility: api.ValueVisibilityExpert, }, zettel.NewContent(contentFormSxn)}, id.RenameTemplateZid: { constHeader{ api.KeyTitle: "Zettelstore Rename Form HTML Template", api.KeyRole: api.ValueRoleConfiguration, api.KeySyntax: meta.SyntaxSxn, api.KeyCreated: "20200804111624", - api.KeyModified: "20230707190246", + api.KeyModified: "20240219145200", api.KeyVisibility: api.ValueVisibilityExpert, }, zettel.NewContent(contentRenameSxn)}, id.DeleteTemplateZid: { constHeader{ api.KeyTitle: "Zettelstore Delete HTML Template", api.KeyRole: api.ValueRoleConfiguration, api.KeySyntax: meta.SyntaxSxn, api.KeyCreated: "20200804111624", - api.KeyModified: "20230621133100", + api.KeyModified: "20240219145200", api.KeyVisibility: api.ValueVisibilityExpert, }, zettel.NewContent(contentDeleteSxn)}, id.ListTemplateZid: { constHeader{ api.KeyTitle: "Zettelstore List Zettel HTML Template", api.KeyRole: api.ValueRoleConfiguration, api.KeySyntax: meta.SyntaxSxn, api.KeyCreated: "20230704122100", - api.KeyModified: "20231129112800", + api.KeyModified: "20240219145200", api.KeyVisibility: api.ValueVisibilityExpert, }, zettel.NewContent(contentListZettelSxn)}, id.ErrorTemplateZid: { constHeader{ api.KeyTitle: "Zettelstore Error HTML Template", api.KeyRole: api.ValueRoleConfiguration, api.KeySyntax: meta.SyntaxSxn, api.KeyCreated: "20210305133215", - api.KeyModified: "20230527224800", + api.KeyModified: "20240219145200", api.KeyVisibility: api.ValueVisibilityExpert, }, zettel.NewContent(contentErrorSxn)}, id.StartSxnZid: { constHeader{ api.KeyTitle: "Zettelstore Sxn Start Code", api.KeyRole: api.ValueRoleConfiguration, api.KeySyntax: meta.SyntaxSxn, api.KeyCreated: "20230824160700", + api.KeyModified: "20240219145200", api.KeyVisibility: api.ValueVisibilityExpert, api.KeyPrecursor: string(api.ZidSxnBase), }, zettel.NewContent(contentStartCodeSxn)}, id.BaseSxnZid: { @@ -276,11 +280,11 @@ constHeader{ api.KeyTitle: "Zettelstore Sxn Base Code", api.KeyRole: api.ValueRoleConfiguration, api.KeySyntax: meta.SyntaxSxn, api.KeyCreated: "20230619132800", - api.KeyModified: "20231012154500", + api.KeyModified: "20240219144600", api.KeyReadOnly: api.ValueTrue, api.KeyVisibility: api.ValueVisibilityExpert, api.KeyPrecursor: string(api.ZidSxnPrelude), }, zettel.NewContent(contentBaseCodeSxn)}, @@ -288,11 +292,11 @@ constHeader{ api.KeyTitle: "Zettelstore Sxn Prelude", api.KeyRole: api.ValueRoleConfiguration, api.KeySyntax: meta.SyntaxSxn, api.KeyCreated: "20231006181700", - api.KeyModified: "20231019140400", + api.KeyModified: "20240222121200", api.KeyReadOnly: api.ValueTrue, api.KeyVisibility: api.ValueVisibilityExpert, }, zettel.NewContent(contentPreludeSxn)}, id.MustParse(api.ZidBaseCSS): { Index: box/constbox/delete.sxn ================================================================== --- box/constbox/delete.sxn +++ box/constbox/delete.sxn @@ -1,5 +1,18 @@ +;;;---------------------------------------------------------------------------- +;;; Copyright (c) 2023-present 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. +;;; +;;; SPDX-License-Identifier: EUPL-1.2 +;;; SPDX-FileCopyrightText: 2023-present Detlef Stern +;;;---------------------------------------------------------------------------- + `(article (header (h1 "Delete Zettel " ,zid)) (p "Do you really want to delete this zettel?") ,@(if shadowed-box `((div (@ (class "zs-info")) Index: box/constbox/error.sxn ================================================================== --- box/constbox/error.sxn +++ box/constbox/error.sxn @@ -1,4 +1,17 @@ +;;;---------------------------------------------------------------------------- +;;; Copyright (c) 2023-present 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. +;;; +;;; SPDX-License-Identifier: EUPL-1.2 +;;; SPDX-FileCopyrightText: 2023-present Detlef Stern +;;;---------------------------------------------------------------------------- + `(article (header (h1 ,heading)) ,message ) Index: box/constbox/form.sxn ================================================================== --- box/constbox/form.sxn +++ box/constbox/form.sxn @@ -1,35 +1,60 @@ +;;;---------------------------------------------------------------------------- +;;; Copyright (c) 2023-present 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. +;;; +;;; SPDX-License-Identifier: EUPL-1.2 +;;; SPDX-FileCopyrightText: 2023-present Detlef Stern +;;;---------------------------------------------------------------------------- + `(article (header (h1 ,heading)) (form (@ (action ,form-action-url) (method "POST") (enctype "multipart/form-data")) (div (label (@ (for "zs-title")) "Title " (a (@ (title "Main heading of this zettel.")) (@H "ⓘ"))) - (input (@ (class "zs-input") (type "text") (id "zs-title") (name "title") (placeholder "Title..") (value ,meta-title) (dir "auto") (autofocus)))) + (input (@ (class "zs-input") (type "text") (id "zs-title") (name "title") + (title "Title of this zettel") + (placeholder "Title..") (value ,meta-title) (dir "auto") (autofocus)))) (div (label (@ (for "zs-role")) "Role " (a (@ (title "One word, without spaces, to set the main role of this zettel.")) (@H "ⓘ"))) - (input (@ (class "zs-input") (type "text") (id "zs-role") (name "role") (placeholder "role..") (value ,meta-role) (dir "auto") + (input (@ (class "zs-input") (type "text") (pattern "\\w*") (id "zs-role") (name "role") + (title "One word, letters and digits, but no spaces, to set the main role of the zettel.") + (placeholder "role..") (value ,meta-role) (dir "auto") ,@(if role-data '((list "zs-role-data"))) )) ,@(wui-datalist "zs-role-data" role-data) ) (div (label (@ (for "zs-tags")) "Tags " (a (@ (title "Tags must begin with an '#' sign. They are separated by spaces.")) (@H "ⓘ"))) - (input (@ (class "zs-input") (type "text") (id "zs-tags") (name "tags") (placeholder "#tag") (value ,meta-tags) (dir "auto")))) + (input (@ (class "zs-input") (type "text") (id "zs-tags") (name "tags") + (title "Tags/keywords to categorize the zettel. Each tags is a word that begins with a '#' character; they are separated by spaces") + (placeholder "#tag") (value ,meta-tags) (dir "auto")))) (div (label (@ (for "zs-meta")) "Metadata " (a (@ (title "Other metadata for this zettel. Each line contains a key/value pair, separated by a colon ':'.")) (@H "ⓘ"))) - (textarea (@ (class "zs-input") (id "zs-meta") (name "meta") (rows "4") (placeholder "metakey: metavalue") (dir "auto")) ,meta)) + (textarea (@ (class "zs-input") (id "zs-meta") (name "meta") (rows "4") + (title "Additional metadata about the zettel") + (placeholder "metakey: metavalue") (dir "auto")) ,meta)) (div (label (@ (for "zs-syntax")) "Syntax " (a (@ (title "Syntax of zettel content below, one word. Typically 'zmk' (for zettelmarkup).")) (@H "ⓘ"))) - (input (@ (class "zs-input") (type "text") (id "zs-syntax") (name "syntax") (placeholder "syntax..") (value ,meta-syntax) (dir "auto") + (input (@ (class "zs-input") (type "text") (pattern "\\w*") (id "zs-syntax") (name "syntax") + (title "Syntax/format of zettel content below, one word, letters and digits, no spaces.") + (placeholder "syntax..") (value ,meta-syntax) (dir "auto") ,@(if syntax-data '((list "zs-syntax-data"))) )) ,@(wui-datalist "zs-syntax-data" syntax-data) ) ,@(if (bound? 'content) `((div (label (@ (for "zs-content")) "Content " (a (@ (title "Content for this zettel, according to above syntax.")) (@H "ⓘ"))) - (textarea (@ (class "zs-input zs-content") (id "zs-content") (name "content") (rows "20") (placeholder "Zettel content..") (dir "auto")) ,content) + (textarea (@ (class "zs-input zs-content") (id "zs-content") (name "content") (rows "20") + (title "Zettel content, according to the given syntax") + (placeholder "Zettel content..") (dir "auto")) ,content) )) ) (div (input (@ (class "zs-primary") (type "submit") (value "Submit"))) (input (@ (class "zs-secondary") (type "submit") (value "Save") (formaction "?save"))) Index: box/constbox/info.sxn ================================================================== --- box/constbox/info.sxn +++ box/constbox/info.sxn @@ -1,19 +1,33 @@ +;;;---------------------------------------------------------------------------- +;;; Copyright (c) 2023-present 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. +;;; +;;; SPDX-License-Identifier: EUPL-1.2 +;;; SPDX-FileCopyrightText: 2023-present Detlef Stern +;;;---------------------------------------------------------------------------- + `(article (header (h1 "Information for Zettel " ,zid) (p (a (@ (href ,web-url)) "Web") (@H " · ") (a (@ (href ,context-url)) "Context") + (@H " / ") (a (@ (href ,context-full-url)) "Full") ,@(if (bound? 'edit-url) `((@H " · ") (a (@ (href ,edit-url)) "Edit"))) - ,@(ROLE-DEFAULT-actions (current-environment)) + ,@(ROLE-DEFAULT-actions (current-binding)) ,@(if (bound? 'reindex-url) `((@H " · ") (a (@ (href ,reindex-url)) "Reindex"))) ,@(if (bound? 'rename-url) `((@H " · ") (a (@ (href ,rename-url)) "Rename"))) ,@(if (bound? 'delete-url) `((@H " · ") (a (@ (href ,delete-url)) "Delete"))) ) ) (h2 "Interpreted Metadata") - (table ,@(map wui-table-row metadata)) + (table ,@(map wui-info-meta-table-row metadata)) (h2 "References") ,@(if local-links `((h3 "Local") (ul ,@(map wui-valid-link local-links)))) ,@(if query-links `((h3 "Queries") (ul ,@(map wui-item-link query-links)))) ,@(if ext-links `((h3 "External") (ul ,@(map wui-item-popup-link ext-links)))) (h3 "Unlinked") Index: box/constbox/listzettel.sxn ================================================================== --- box/constbox/listzettel.sxn +++ box/constbox/listzettel.sxn @@ -1,9 +1,24 @@ +;;;---------------------------------------------------------------------------- +;;; Copyright (c) 2023-present 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. +;;; +;;; SPDX-License-Identifier: EUPL-1.2 +;;; SPDX-FileCopyrightText: 2023-present Detlef Stern +;;;---------------------------------------------------------------------------- + `(article (header (h1 ,heading)) - (form (@ (action ,search-url)) - (input (@ (class "zs-input") (type "text") (placeholder "Search..") (name ,query-key-query) (value ,query-value) (dir "auto")))) + (search (form (@ (action ,search-url)) + (input (@ (class "zs-input") (type "search") (inputmode "search") (name ,query-key-query) + (title "Contains the search that leads to the list below. You're allowed to modify it") + (placeholder "Search..") (value ,query-value) (dir "auto"))))) ,@(if (bound? 'tag-zettel) `((p (@ (class "zs-meta-zettel")) "Tag zettel: " ,@tag-zettel)) ) ,@(if (bound? 'create-tag-zettel) `((p (@ (class "zs-meta-zettel")) "Create tag zettel: " ,@create-tag-zettel)) @@ -15,17 +30,21 @@ `((p (@ (class "zs-meta-zettel")) "Create role zettel: " ,@create-role-zettel)) ) ,@content ,@endnotes (form (@ (action ,(if (bound? 'create-url) create-url))) - "Other encodings: " - (a (@ (href ,data-url)) "data") - ", " - (a (@ (href ,plain-url)) "plain") + ,(if (bound? 'data-url) + `(@L "Other encodings" + ,(if (> num-entries 3) `(@L " of these " ,num-entries " entries: ") ": ") + (a (@ (href ,data-url)) "data") + ", " + (a (@ (href ,plain-url)) "plain") + ) + ) ,@(if (bound? 'create-url) `((input (@ (type "hidden") (name ,query-key-query) (value ,query-value))) (input (@ (type "hidden") (name ,query-key-seed) (value ,seed))) (input (@ (class "zs-primary") (type "submit") (value "Save As Zettel"))) ) ) ) ) Index: box/constbox/login.sxn ================================================================== --- box/constbox/login.sxn +++ box/constbox/login.sxn @@ -1,5 +1,18 @@ +;;;---------------------------------------------------------------------------- +;;; Copyright (c) 2023-present 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. +;;; +;;; SPDX-License-Identifier: EUPL-1.2 +;;; SPDX-FileCopyrightText: 2023-present Detlef Stern +;;;---------------------------------------------------------------------------- + `(article (header (h1 "Login")) ,@(if retry '((div (@ (class "zs-indication zs-error")) "Wrong user name / password. Try again."))) (form (@ (method "POST") (action "")) (div Index: box/constbox/prelude.sxn ================================================================== --- box/constbox/prelude.sxn +++ box/constbox/prelude.sxn @@ -4,30 +4,35 @@ ;;; 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. +;;; +;;; SPDX-License-Identifier: EUPL-1.2 +;;; SPDX-FileCopyrightText: 2023-present Detlef Stern ;;;---------------------------------------------------------------------------- -;;; This zettel contains all sxn definitions that are independent of specific +;;; This zettel contains sxn definitions that are independent of specific ;;; subsystems, such as WebUI, API, or other. It just contains generic code to -;;; be used elsewhere. +;;; be used in all places. ;; Constants NIL and T (defconst NIL ()) (defconst T 'T) -;; Function not -(defun not (x) (if x NIL T)) -(defconst not not) - ;; defunconst macro to define functions that are bound as a constant. ;; ;; (defunconst NAME ARGS EXPR ...) (defmacro defunconst (name args . body) `(begin (defun ,name ,args ,@body) (defconst ,name ,name))) +;; not macro +(defmacro not (x) `(if ,x NIL T)) + +;; not= macro, to negate an equivalence +(defmacro not= args `(not (= ,@args))) + ;; let macro ;; ;; (let (BINDING ...) EXPR ...), where BINDING is a list of two elements ;; (SYMBOL EXPR) (defmacro let (bindings . body) @@ -41,10 +46,24 @@ `((lambda () ,@body)) `((lambda (,(caar bindings)) (let* ,(cdr bindings) ,@body)) ,(cadar bindings)))) +;; cond macro +;; +;; (cond ((COND EXPR) ...)) +(defmacro cond clauses + (if (null? clauses) + () + (let* ((clause (car clauses)) + (the-cond (car clause))) + (if (= the-cond T) + (cadr clause) + `(if ,the-cond + ,(cadr clause) + (cond ,@(cdr clauses))))))) + ;; and macro ;; ;; (and EXPR ...) (defmacro and args (cond ((null? args) T) Index: box/constbox/rename.sxn ================================================================== --- box/constbox/rename.sxn +++ box/constbox/rename.sxn @@ -1,5 +1,18 @@ +;;;---------------------------------------------------------------------------- +;;; Copyright (c) 2023-present 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. +;;; +;;; SPDX-License-Identifier: EUPL-1.2 +;;; SPDX-FileCopyrightText: 2023-present Detlef Stern +;;;---------------------------------------------------------------------------- + `(article (header (h1 "Rename Zettel " ,zid)) (p "Do you really want to rename this zettel?") ,@(if incoming `((div (@ (class "zs-warning")) @@ -17,10 +30,13 @@ ) (form (@ (method "POST")) (input (@ (type "hidden") (id "curzid") (name "curzid") (value ,zid))) (div (label (@ (for "newzid")) "New zettel id") - (input (@ (class "zs-input") (type "text") (id "newzid") (name "newzid") (placeholder "ZID..") (value ,zid) (autofocus)))) + (input (@ (class "zs-input") (type "text") (inputmode "numeric") (id "newzid") (name "newzid") + (pattern "\\d{14}") + (title "New zettel identifier, must be unique") + (placeholder "ZID..") (value ,zid) (autofocus)))) (div (input (@ (class "zs-primary") (type "submit") (value "Rename")))) ) ,(wui-meta-desc metapairs) ) Index: box/constbox/start.sxn ================================================================== --- box/constbox/start.sxn +++ box/constbox/start.sxn @@ -4,10 +4,13 @@ ;;; 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. +;;; +;;; SPDX-License-Identifier: EUPL-1.2 +;;; SPDX-FileCopyrightText: 2023-present Detlef Stern ;;;---------------------------------------------------------------------------- ;;; This zettel is the start of the loading sequence for Sx code used in the ;;; Zettelstore. Via the precursor metadata, dependend zettel are evaluated ;;; before this zettel. You must always depend, directly or indirectly on the Index: box/constbox/wuicode.sxn ================================================================== --- box/constbox/wuicode.sxn +++ box/constbox/wuicode.sxn @@ -4,21 +4,24 @@ ;;; 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. +;;; +;;; SPDX-License-Identifier: EUPL-1.2 +;;; SPDX-FileCopyrightText: 2023-present Detlef Stern ;;;---------------------------------------------------------------------------- ;; Contains WebUI specific code, but not related to a specific template. ;; wui-list-item returns the argument as a HTML list item. (defunconst wui-item (s) `(li ,s)) -;; wui-table-row takes a pair and translates it into a HTML table row with -;; two columns. -(defunconst wui-table-row (p) - `(tr (td ,(car p)) (td ,(cdr p)))) +;; wui-info-meta-table-row takes a pair and translates it into a HTML table row +;; with two columns. +(defunconst wui-info-meta-table-row (p) + `(tr (td (@ (class zs-info-meta-key)) ,(car p)) (td (@ (class zs-info-meta-value)) ,(cdr p)))) ;; wui-valid-link translates a local link into a HTML link. A link is a pair ;; (valid . url). If valid is not truish, only the invalid url is returned. (defunconst wui-valid-link (l) (if (car l) @@ -73,52 +76,68 @@ ;; Referenced in function "ROLE-DEFAULT-meta". (defvar CSS-ROLE-map '()) ;; ROLE-DEFAULT-meta returns some metadata for the base template. Any role ;; specific code should include the returned list of this function. -(defunconst ROLE-DEFAULT-meta (env) - `(,@(let* ((meta-role (environment-lookup 'meta-role env)) +(defun ROLE-DEFAULT-meta (binding) + `(,@(let* ((meta-role (binding-lookup 'meta-role binding)) (entry (assoc CSS-ROLE-map meta-role))) (if (pair? entry) `((link (@ (rel "stylesheet") (href ,(zid-content-path (cdr entry)))))) ) ) ) ) -;;; ACTION-SEPARATOR defines a HTML value that separates actions links. +;; ACTION-SEPARATOR defines a HTML value that separates actions links. (defvar ACTION-SEPARATOR '(@H " · ")) -;;; ROLE-DEFAULT-actions returns the default text for actions. -(defunconst ROLE-DEFAULT-actions (env) - `(,@(let ((copy-url (environment-lookup 'copy-url env))) +;; ROLE-DEFAULT-actions returns the default text for actions. +(defun ROLE-DEFAULT-actions (binding) + `(,@(let ((copy-url (binding-lookup 'copy-url binding))) (if (defined? copy-url) `((@H " · ") (a (@ (href ,copy-url)) "Copy")))) - ,@(let ((version-url (environment-lookup 'version-url env))) + ,@(let ((version-url (binding-lookup 'version-url binding))) (if (defined? version-url) `((@H " · ") (a (@ (href ,version-url)) "Version")))) - ,@(let ((child-url (environment-lookup 'child-url env))) + ,@(let ((child-url (binding-lookup 'child-url binding))) (if (defined? child-url) `((@H " · ") (a (@ (href ,child-url)) "Child")))) - ,@(let ((folge-url (environment-lookup 'folge-url env))) + ,@(let ((folge-url (binding-lookup 'folge-url binding))) (if (defined? folge-url) `((@H " · ") (a (@ (href ,folge-url)) "Folge")))) ) ) -;;; ROLE-tag-actions returns an additional action "Zettel" for zettel with role "tag". -(defunconst ROLE-tag-actions (env) - `(,@(ROLE-DEFAULT-actions env) - ,@(let ((title (environment-lookup 'title env))) +;; ROLE-tag-actions returns an additional action "Zettel" for zettel with role "tag". +(defun ROLE-tag-actions (binding) + `(,@(ROLE-DEFAULT-actions binding) + ,@(let ((title (binding-lookup 'title binding))) + (if (and (defined? title) title) + `(,ACTION-SEPARATOR (a (@ (href ,(query->url (concat "tags:" title)))) "Zettel")) + ) + ) + ) +) + +;; ROLE-role-actions returns an additional action "Zettel" for zettel with role "role". +(defun ROLE-role-actions (binding) + `(,@(ROLE-DEFAULT-actions binding) + ,@(let ((title (binding-lookup 'title binding))) (if (and (defined? title) title) - `(,ACTION-SEPARATOR (a (@ (href ,(query->url (string-append "tags:" title)))) "Zettel")) + `(,ACTION-SEPARATOR (a (@ (href ,(query->url (concat "role:" title)))) "Zettel")) ) ) ) ) -;;; ROLE-DEFAULT-heading returns the default text for headings, below the -;;; references of a zettel. In most cases it should be called from an -;;; overwriting function. -(defunconst ROLE-DEFAULT-heading (env) - `(,@(let ((meta-url (environment-lookup 'meta-url env))) +;; ROLE-DEFAULT-heading returns the default text for headings, below the +;; references of a zettel. In most cases it should be called from an +;; overwriting function. +(defun ROLE-DEFAULT-heading (binding) + `(,@(let ((meta-url (binding-lookup 'meta-url binding))) (if (defined? meta-url) `((br) "URL: " ,(url-to-html meta-url)))) - ,@(let ((meta-author (environment-lookup 'meta-author env))) + ,@(let ((urls (binding-lookup 'urls binding))) + (if (defined? urls) + (map (lambda (u) `(@L (br) ,(car u) ": " ,(url-to-html (cdr u)))) urls) + ) + ) + ,@(let ((meta-author (binding-lookup 'meta-author binding))) (if (and (defined? meta-author) meta-author) `((br) "By " ,meta-author))) ) ) Index: box/constbox/zettel.sxn ================================================================== --- box/constbox/zettel.sxn +++ box/constbox/zettel.sxn @@ -1,5 +1,18 @@ +;;;---------------------------------------------------------------------------- +;;; Copyright (c) 2023-present 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. +;;; +;;; SPDX-License-Identifier: EUPL-1.2 +;;; SPDX-FileCopyrightText: 2023-present Detlef Stern +;;;---------------------------------------------------------------------------- + `(article (header (h1 ,heading) (div (@ (class "zs-meta")) ,@(if (bound? 'edit-url) `((a (@ (href ,edit-url)) "Edit") (@H " · "))) @@ -8,15 +21,15 @@ "(" ,@(if (bound? 'role-url) `((a (@ (href ,role-url)) ,meta-role))) ,@(if (and (bound? 'folge-role-url) (bound? 'meta-folge-role)) `((@H " → ") (a (@ (href ,folge-role-url)) ,meta-folge-role))) ")" ,@(if tag-refs `((@H " · ") ,@tag-refs)) - ,@(ROLE-DEFAULT-actions (current-environment)) + ,@(ROLE-DEFAULT-actions (current-binding)) ,@(if predecessor-refs `((br) "Predecessor: " ,predecessor-refs)) ,@(if precursor-refs `((br) "Precursor: " ,precursor-refs)) ,@(if superior-refs `((br) "Superior: " ,superior-refs)) - ,@(ROLE-DEFAULT-heading (current-environment)) + ,@(ROLE-DEFAULT-heading (current-binding)) ) ) ,@content ,endnotes ,@(if (or folge-links subordinate-links back-links successor-links) Index: box/dirbox/dirbox.go ================================================================== --- box/dirbox/dirbox.go +++ box/dirbox/dirbox.go @@ -4,10 +4,13 @@ // 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. +// +// SPDX-License-Identifier: EUPL-1.2 +// SPDX-FileCopyrightText: 2020-present Detlef Stern //----------------------------------------------------------------------------- // Package dirbox provides a directory-based zettel box. package dirbox @@ -87,11 +90,11 @@ dirNotifySimple dirNotifyFS ) func getDirSrvInfo(log *logger.Logger, notifyType string) notifyTypeSpec { - for count := 0; count < 2; count++ { + for range 2 { switch notifyType { case kernel.BoxDirTypeNotify: return dirNotifyFS case kernel.BoxDirTypeSimple: return dirNotifySimple @@ -149,11 +152,11 @@ func (dp *dirBox) Start(context.Context) error { dp.mxCmds.Lock() defer dp.mxCmds.Unlock() dp.fCmds = make([]chan fileCmd, 0, dp.fSrvs) - for i := uint32(0); i < dp.fSrvs; i++ { + for i := range dp.fSrvs { cc := make(chan fileCmd) go fileService(i, dp.log.Clone().Str("sub", "file").Uint("fn", uint64(i)).Child(), dp.dir, cc) dp.fCmds = append(dp.fCmds, cc) } @@ -164,11 +167,11 @@ notifier, err = notify.NewSimpleDirNotifier(dp.log.Clone().Str("notify", "simple").Child(), dp.dir) default: notifier, err = notify.NewFSDirNotifier(dp.log.Clone().Str("notify", "fs").Child(), dp.dir) } if err != nil { - dp.log.Fatal().Err(err).Msg("Unable to create directory supervisor") + dp.log.Error().Err(err).Msg("Unable to create directory supervisor") dp.stopFileServices() return err } dp.dirSrv = notify.NewDirService( dp, Index: box/dirbox/dirbox_test.go ================================================================== --- box/dirbox/dirbox_test.go +++ box/dirbox/dirbox_test.go @@ -4,10 +4,13 @@ // 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. +// +// SPDX-License-Identifier: EUPL-1.2 +// SPDX-FileCopyrightText: 2021-present Detlef Stern //----------------------------------------------------------------------------- package dirbox import "testing" @@ -30,11 +33,11 @@ } } } func TestMakePrime(t *testing.T) { - for i := uint32(0); i < 1500; i++ { + for i := range uint32(1500) { np := makePrime(i) if np < i { t.Errorf("makePrime(%d) < %d", i, np) continue } Index: box/dirbox/service.go ================================================================== --- box/dirbox/service.go +++ box/dirbox/service.go @@ -4,24 +4,28 @@ // 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. +// +// SPDX-License-Identifier: EUPL-1.2 +// SPDX-FileCopyrightText: 2020-present Detlef Stern //----------------------------------------------------------------------------- package dirbox import ( "context" + "fmt" "io" "os" "path/filepath" "time" + "zettelstore.de/client.fossil/input" "zettelstore.de/z/box/filebox" "zettelstore.de/z/box/notify" - "zettelstore.de/z/input" "zettelstore.de/z/kernel" "zettelstore.de/z/logger" "zettelstore.de/z/zettel" "zettelstore.de/z/zettel/id" "zettelstore.de/z/zettel/meta" @@ -34,19 +38,19 @@ kernel.Main.LogRecover("FileService", ri) go fileService(i, log, dirPath, cmds) } }() - log.Trace().Uint("i", uint64(i)).Str("dirpath", dirPath).Msg("File service started") + log.Debug().Uint("i", uint64(i)).Str("dirpath", dirPath).Msg("File service started") for cmd := range cmds { - cmd.run(log, dirPath) + cmd.run(dirPath) } - log.Trace().Uint("i", uint64(i)).Str("dirpath", dirPath).Msg("File service stopped") + log.Debug().Uint("i", uint64(i)).Str("dirpath", dirPath).Msg("File service stopped") } type fileCmd interface { - run(*logger.Logger, string) + run(string) } const serviceTimeout = 5 * time.Second // must be shorter than the web servers timeout values for reading+writing. // COMMAND: srvGetMeta ---------------------------------------- @@ -73,23 +77,22 @@ type resGetMeta struct { meta *meta.Meta err error } -func (cmd *fileGetMeta) run(log *logger.Logger, dirPath string) { +func (cmd *fileGetMeta) run(dirPath string) { var m *meta.Meta var err error entry := cmd.entry zid := entry.Zid if metaName := entry.MetaName; metaName == "" { contentName := entry.ContentName contentExt := entry.ContentExt if contentName == "" || contentExt == "" { - log.Panic().Zid(zid).Msg("No meta, no content in getMeta") - } - if entry.HasMetaInContent() { + err = fmt.Errorf("no meta, no content in getMeta, zid=%v", zid) + } else if entry.HasMetaInContent() { m, _, err = parseMetaContentFile(zid, filepath.Join(dirPath, contentName)) } else { m = filebox.CalcDefaultMeta(zid, contentExt) } } else { @@ -126,11 +129,11 @@ meta *meta.Meta content []byte err error } -func (cmd *fileGetMetaContent) run(log *logger.Logger, dirPath string) { +func (cmd *fileGetMetaContent) run(dirPath string) { var m *meta.Meta var content []byte var err error entry := cmd.entry @@ -138,13 +141,12 @@ contentName := entry.ContentName contentExt := entry.ContentExt contentPath := filepath.Join(dirPath, contentName) if metaName := entry.MetaName; metaName == "" { if contentName == "" || contentExt == "" { - log.Panic().Zid(zid).Msg("No meta, no content in getMetaContent") - } - if entry.HasMetaInContent() { + err = fmt.Errorf("no meta, no content in getMetaContent, zid=%v", zid) + } else if entry.HasMetaInContent() { m, content, err = parseMetaContentFile(zid, contentPath) } else { m = filebox.CalcDefaultMeta(zid, contentExt) content, err = os.ReadFile(contentPath) } @@ -186,33 +188,35 @@ zettel zettel.Zettel rc chan<- resSetZettel } type resSetZettel = error -func (cmd *fileSetZettel) run(log *logger.Logger, dirPath string) { +func (cmd *fileSetZettel) run(dirPath string) { + var err error entry := cmd.entry zid := entry.Zid contentName := entry.ContentName m := cmd.zettel.Meta content := cmd.zettel.Content.AsBytes() metaName := entry.MetaName if metaName == "" { if contentName == "" { - log.Panic().Zid(zid).Msg("No meta, no content in setZettel") - } - contentPath := filepath.Join(dirPath, contentName) - if entry.HasMetaInContent() { - err := writeZettelFile(contentPath, m, content) - cmd.rc <- err - return - } - err := writeFileContent(contentPath, content) + err = fmt.Errorf("no meta, no content in setZettel, zid=%v", zid) + } else { + contentPath := filepath.Join(dirPath, contentName) + if entry.HasMetaInContent() { + err = writeZettelFile(contentPath, m, content) + cmd.rc <- err + return + } + err = writeFileContent(contentPath, content) + } cmd.rc <- err return } - err := writeMetaFile(filepath.Join(dirPath, metaName), m) + err = writeMetaFile(filepath.Join(dirPath, metaName), m) if err == nil && contentName != "" { err = writeFileContent(filepath.Join(dirPath, contentName), content) } cmd.rc <- err } @@ -235,13 +239,11 @@ func writeZettelFile(contentPath string, m *meta.Meta, content []byte) error { zettelFile, err := openFileWrite(contentPath) if err != nil { return err } - if err == nil { - err = writeMetaHeader(zettelFile, m) - } + err = writeMetaHeader(zettelFile, m) if err == nil { _, err = zettelFile.Write(content) } if err1 := zettelFile.Close(); err == nil { err = err1 @@ -298,21 +300,22 @@ entry *notify.DirEntry rc chan<- resDeleteZettel } type resDeleteZettel = error -func (cmd *fileDeleteZettel) run(log *logger.Logger, dirPath string) { +func (cmd *fileDeleteZettel) run(dirPath string) { var err error entry := cmd.entry contentName := entry.ContentName contentPath := filepath.Join(dirPath, contentName) if metaName := entry.MetaName; metaName == "" { if contentName == "" { - log.Panic().Zid(entry.Zid).Msg("No meta, no content in getMetaContent") + err = fmt.Errorf("no meta, no content in deleteZettel, zid=%v", entry.Zid) + } else { + err = os.Remove(contentPath) } - err = os.Remove(contentPath) } else { if contentName != "" { err = os.Remove(contentPath) } err1 := os.Remove(filepath.Join(dirPath, metaName)) Index: box/filebox/filebox.go ================================================================== --- box/filebox/filebox.go +++ box/filebox/filebox.go @@ -4,10 +4,13 @@ // 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. +// +// SPDX-License-Identifier: EUPL-1.2 +// SPDX-FileCopyrightText: 2021-present Detlef Stern //----------------------------------------------------------------------------- // Package filebox provides boxes that are stored in a file. package filebox Index: box/filebox/zipbox.go ================================================================== --- box/filebox/zipbox.go +++ box/filebox/zipbox.go @@ -4,23 +4,27 @@ // 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. +// +// SPDX-License-Identifier: EUPL-1.2 +// SPDX-FileCopyrightText: 2021-present Detlef Stern //----------------------------------------------------------------------------- package filebox import ( "archive/zip" "context" + "fmt" "io" "strings" + "zettelstore.de/client.fossil/input" "zettelstore.de/z/box" "zettelstore.de/z/box/notify" - "zettelstore.de/z/input" "zettelstore.de/z/logger" "zettelstore.de/z/query" "zettelstore.de/z/zettel" "zettelstore.de/z/zettel/id" "zettelstore.de/z/zettel/meta" @@ -98,11 +102,12 @@ var inMeta bool contentName := entry.ContentName if metaName := entry.MetaName; metaName == "" { if contentName == "" { - zb.log.Panic().Zid(zid).Msg("No meta, no content in zipBox.GetZettel") + err = fmt.Errorf("no meta, no content in getZettel, zid=%v", zid) + return zettel.Zettel{}, err } src, err = readZipFileContent(reader, entry.ContentName) if err != nil { return zettel.Zettel{}, err } @@ -207,13 +212,12 @@ var inMeta bool if metaName := entry.MetaName; metaName == "" { contentName := entry.ContentName contentExt := entry.ContentExt if contentName == "" || contentExt == "" { - zb.log.Panic().Zid(zid).Msg("No meta, no content in getMeta") - } - if entry.HasMetaInContent() { + err = fmt.Errorf("no meta, no content in getMeta, zid=%v", zid) + } else if entry.HasMetaInContent() { m, err = readZipMetaFile(reader, zid, contentName) } else { m = CalcDefaultMeta(zid, contentExt) } } else { Index: box/helper.go ================================================================== --- box/helper.go +++ box/helper.go @@ -4,10 +4,13 @@ // 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. +// +// SPDX-License-Identifier: EUPL-1.2 +// SPDX-FileCopyrightText: 2021-present Detlef Stern //----------------------------------------------------------------------------- package box import ( @@ -19,11 +22,11 @@ ) // GetNewZid calculates a new and unused zettel identifier, based on the current date and time. func GetNewZid(testZid func(id.Zid) (bool, error)) (id.Zid, error) { withSeconds := false - for i := 0; i < 90; i++ { // Must be completed within 9 seconds (less than web/server.writeTimeout) + for range 90 { // Must be completed within 9 seconds (less than web/server.writeTimeout) zid := id.New(withSeconds) found, err := testZid(zid) if err != nil { return id.Invalid, err } Index: box/manager/anteroom.go ================================================================== --- box/manager/anteroom.go +++ box/manager/anteroom.go @@ -4,10 +4,13 @@ // 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. +// +// SPDX-License-Identifier: EUPL-1.2 +// SPDX-FileCopyrightText: 2021-present Detlef Stern //----------------------------------------------------------------------------- package manager import ( Index: box/manager/anteroom_test.go ================================================================== --- box/manager/anteroom_test.go +++ box/manager/anteroom_test.go @@ -4,10 +4,13 @@ // 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. +// +// SPDX-License-Identifier: EUPL-1.2 +// SPDX-FileCopyrightText: 2021-present Detlef Stern //----------------------------------------------------------------------------- package manager import ( Index: box/manager/box.go ================================================================== --- box/manager/box.go +++ box/manager/box.go @@ -4,10 +4,13 @@ // 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. +// +// SPDX-License-Identifier: EUPL-1.2 +// SPDX-FileCopyrightText: 2021-present Detlef Stern //----------------------------------------------------------------------------- package manager import ( @@ -28,11 +31,11 @@ func (mgr *Manager) Location() string { if len(mgr.boxes) <= 2 { return "NONE" } var sb strings.Builder - for i := 0; i < len(mgr.boxes)-2; i++ { + for i := range len(mgr.boxes) - 2 { if i > 0 { sb.WriteString(", ") } sb.WriteString(mgr.boxes[i].Location()) } @@ -265,11 +268,11 @@ defer mgr.mgrMx.RUnlock() for i, p := range mgr.boxes { err := p.RenameZettel(ctx, curZid, newZid) var errZNF box.ErrZettelNotFound if err != nil && !errors.As(err, &errZNF) { - for j := 0; j < i; j++ { + for j := range i { mgr.boxes[j].RenameZettel(ctx, newZid, curZid) } return err } } Index: box/manager/collect.go ================================================================== --- box/manager/collect.go +++ box/manager/collect.go @@ -4,10 +4,13 @@ // 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. +// +// SPDX-License-Identifier: EUPL-1.2 +// SPDX-FileCopyrightText: 2021-present Detlef Stern //----------------------------------------------------------------------------- package manager import ( Index: box/manager/enrich.go ================================================================== --- box/manager/enrich.go +++ box/manager/enrich.go @@ -4,10 +4,13 @@ // 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. +// +// SPDX-License-Identifier: EUPL-1.2 +// SPDX-FileCopyrightText: 2021-present Detlef Stern //----------------------------------------------------------------------------- package manager import ( Index: box/manager/indexer.go ================================================================== --- box/manager/indexer.go +++ box/manager/indexer.go @@ -4,10 +4,13 @@ // 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. +// +// SPDX-License-Identifier: EUPL-1.2 +// SPDX-FileCopyrightText: 2021-present Detlef Stern //----------------------------------------------------------------------------- package manager import ( Index: box/manager/manager.go ================================================================== --- box/manager/manager.go +++ box/manager/manager.go @@ -4,10 +4,13 @@ // 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. +// +// SPDX-License-Identifier: EUPL-1.2 +// SPDX-FileCopyrightText: 2021-present Detlef Stern //----------------------------------------------------------------------------- // Package manager coordinates the various boxes and indexes of a Zettelstore. package manager @@ -16,14 +19,13 @@ "io" "net/url" "sync" "time" - "zettelstore.de/client.fossil/maps" "zettelstore.de/z/auth" "zettelstore.de/z/box" - "zettelstore.de/z/box/manager/memstore" + "zettelstore.de/z/box/manager/mapstore" "zettelstore.de/z/box/manager/store" "zettelstore.de/z/config" "zettelstore.de/z/kernel" "zettelstore.de/z/logger" "zettelstore.de/z/strfun" @@ -77,13 +79,10 @@ panic(scheme) } registry[scheme] = create } -// GetSchemes returns all registered scheme, ordered by scheme string. -func GetSchemes() []string { return maps.Keys(registry) } - // Manager is a coordinating box. type Manager struct { mgrLog *logger.Logger stateMx sync.RWMutex state box.StartState @@ -137,11 +136,11 @@ rtConfig: rtConfig, infos: make(chan box.UpdateInfo, len(boxURIs)*10), propertyKeys: propertyKeys, idxLog: boxLog.Clone().Str("box", "index").Child(), - idxStore: memstore.New(), + idxStore: createIdxStore(rtConfig), idxAr: newAnteroomQueue(1000), idxReady: make(chan struct{}, 1), } cdata := ConnectData{Number: 1, Config: rtConfig, Enricher: mgr, Notify: mgr.infos} boxes := make([]box.ManagedBox, 0, len(boxURIs)+2) @@ -167,10 +166,14 @@ cdata.Number++ boxes = append(boxes, constbox, compbox) mgr.boxes = boxes return mgr, nil } + +func createIdxStore(_ config.Config) store.Store { + return mapstore.New() +} // RegisterObserver registers an observer that will be notified // if a zettel was found to be changed. func (mgr *Manager) RegisterObserver(f box.UpdateFunc) { if f != nil { @@ -250,11 +253,11 @@ case box.OnReload: mgr.idxAr.Reset() case box.OnZettel: mgr.idxAr.EnqueueZettel(zid) default: - mgr.mgrLog.Warn().Uint("reason", uint64(reason)).Zid(zid).Msg("Unknown notification reason") + mgr.mgrLog.Error().Uint("reason", uint64(reason)).Zid(zid).Msg("Unknown notification reason") return } select { case mgr.idxReady <- struct{}{}: default: @@ -313,11 +316,11 @@ const waitTime = 10 * time.Millisecond const waitLoop = int(1 * time.Second / waitTime) for i := 1; !mgr.allBoxesStarted(); i++ { if i%waitLoop == 0 { if time.Duration(i)*waitTime > time.Minute { - mgr.mgrLog.Warn().Msg("Waiting for more than one minute to start") + mgr.mgrLog.Info().Msg("Waiting for more than one minute to start") } else { mgr.mgrLog.Trace().Msg("Wait for boxes to start") } } time.Sleep(waitTime) @@ -371,11 +374,11 @@ func (mgr *Manager) ReIndex(_ context.Context, zid id.Zid) error { mgr.mgrLog.Debug().Msg("ReIndex") if mgr.State() != box.StartStateStarted { return box.ErrStopped } - mgr.infos <- box.UpdateInfo{Reason: box.OnReload, Zid: zid} + mgr.infos <- box.UpdateInfo{Reason: box.OnZettel, Zid: zid} return nil } // ReadStats populates st with box statistics. func (mgr *Manager) ReadStats(st *box.Stats) { ADDED box/manager/mapstore/mapstore.go Index: box/manager/mapstore/mapstore.go ================================================================== --- /dev/null +++ box/manager/mapstore/mapstore.go @@ -0,0 +1,717 @@ +//----------------------------------------------------------------------------- +// Copyright (c) 2021-present 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. +// +// SPDX-License-Identifier: EUPL-1.2 +// SPDX-FileCopyrightText: 2021-present Detlef Stern +//----------------------------------------------------------------------------- + +// Package mapstore stored the index in main memory via a Go map. +package mapstore + +import ( + "context" + "fmt" + "io" + "sort" + "strings" + "sync" + + "zettelstore.de/client.fossil/api" + "zettelstore.de/client.fossil/maps" + "zettelstore.de/z/box" + "zettelstore.de/z/box/manager/store" + "zettelstore.de/z/zettel/id" + "zettelstore.de/z/zettel/meta" +) + +type zettelData struct { + meta *meta.Meta // a local copy of the metadata, without computed keys + dead id.Slice // list of dead references in this zettel + forward id.Slice // list of forward references in this zettel + backward id.Slice // list of zettel that reference with zettel + otherRefs map[string]bidiRefs + words []string // list of words of this zettel + urls []string // list of urls of this zettel +} + +type bidiRefs struct { + forward id.Slice + backward id.Slice +} + +type stringRefs map[string]id.Slice + +type memStore struct { + mx sync.RWMutex + intern map[string]string // map to intern strings + idx map[id.Zid]*zettelData + dead map[id.Zid]id.Slice // map dead refs where they occur + words stringRefs + urls stringRefs + + // Stats + mxStats sync.Mutex + updates uint64 +} + +// New returns a new memory-based index store. +func New() store.Store { + return &memStore{ + intern: make(map[string]string, 1024), + idx: make(map[id.Zid]*zettelData), + dead: make(map[id.Zid]id.Slice), + words: make(stringRefs), + urls: make(stringRefs), + } +} + +func (ms *memStore) GetMeta(_ context.Context, zid id.Zid) (*meta.Meta, error) { + ms.mx.RLock() + defer ms.mx.RUnlock() + if zi, found := ms.idx[zid]; found && zi.meta != nil { + // zi.meta is nil, if zettel was referenced, but is not indexed yet. + return zi.meta.Clone(), nil + } + return nil, box.ErrZettelNotFound{Zid: zid} +} + +func (ms *memStore) Enrich(_ context.Context, m *meta.Meta) { + if ms.doEnrich(m) { + ms.mxStats.Lock() + ms.updates++ + ms.mxStats.Unlock() + } +} + +func (ms *memStore) doEnrich(m *meta.Meta) bool { + ms.mx.RLock() + defer ms.mx.RUnlock() + zi, ok := ms.idx[m.Zid] + if !ok { + return false + } + var updated bool + if len(zi.dead) > 0 { + m.Set(api.KeyDead, zi.dead.String()) + updated = true + } + back := removeOtherMetaRefs(m, zi.backward.Clone()) + if len(zi.backward) > 0 { + m.Set(api.KeyBackward, zi.backward.String()) + updated = true + } + if len(zi.forward) > 0 { + m.Set(api.KeyForward, zi.forward.String()) + back = remRefs(back, zi.forward) + updated = true + } + for k, refs := range zi.otherRefs { + if len(refs.backward) > 0 { + m.Set(k, refs.backward.String()) + back = remRefs(back, refs.backward) + updated = true + } + } + if len(back) > 0 { + m.Set(api.KeyBack, back.String()) + updated = true + } + return updated +} + +// SearchEqual returns all zettel that contains the given exact word. +// The word must be normalized through Unicode NKFD, trimmed and not empty. +func (ms *memStore) SearchEqual(word string) id.Set { + ms.mx.RLock() + defer ms.mx.RUnlock() + result := id.NewSet() + if refs, ok := ms.words[word]; ok { + result.CopySlice(refs) + } + if refs, ok := ms.urls[word]; ok { + result.CopySlice(refs) + } + zid, err := id.Parse(word) + if err != nil { + return result + } + zi, ok := ms.idx[zid] + if !ok { + return result + } + + addBackwardZids(result, zid, zi) + return result +} + +// SearchPrefix returns all zettel that have a word with the given prefix. +// The prefix must be normalized through Unicode NKFD, trimmed and not empty. +func (ms *memStore) SearchPrefix(prefix string) id.Set { + ms.mx.RLock() + defer ms.mx.RUnlock() + result := ms.selectWithPred(prefix, strings.HasPrefix) + l := len(prefix) + if l > 14 { + 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) + } + } + return result +} + +// SearchSuffix returns all zettel that have a word with the given suffix. +// The suffix must be normalized through Unicode NKFD, trimmed and not empty. +func (ms *memStore) SearchSuffix(suffix string) id.Set { + ms.mx.RLock() + defer ms.mx.RUnlock() + result := ms.selectWithPred(suffix, strings.HasSuffix) + l := len(suffix) + if l > 14 { + return result + } + val, err := id.ParseUint(suffix) + if err != nil { + return result + } + modulo := uint64(1) + for range l { + modulo *= 10 + } + for zid, zi := range ms.idx { + if uint64(zid)%modulo == val { + addBackwardZids(result, zid, zi) + } + } + return result +} + +// SearchContains returns all zettel that contains the given string. +// The string must be normalized through Unicode NKFD, trimmed and not empty. +func (ms *memStore) SearchContains(s string) id.Set { + ms.mx.RLock() + defer ms.mx.RUnlock() + result := ms.selectWithPred(s, strings.Contains) + if len(s) > 14 { + return result + } + if _, err := id.ParseUint(s); err != nil { + return result + } + for zid, zi := range ms.idx { + if strings.Contains(zid.String(), s) { + addBackwardZids(result, zid, zi) + } + } + return result +} + +func (ms *memStore) selectWithPred(s string, pred func(string, string) bool) id.Set { + // Must only be called if ms.mx is read-locked! + result := id.NewSet() + for word, refs := range ms.words { + if !pred(word, s) { + continue + } + result.CopySlice(refs) + } + for u, refs := range ms.urls { + if !pred(u, s) { + continue + } + result.CopySlice(refs) + } + return result +} + +func addBackwardZids(result id.Set, zid id.Zid, zi *zettelData) { + // Must only be called if ms.mx is read-locked! + result.Add(zid) + result.CopySlice(zi.backward) + for _, mref := range zi.otherRefs { + result.CopySlice(mref.backward) + } +} + +func removeOtherMetaRefs(m *meta.Meta, back id.Slice) id.Slice { + for _, p := range m.PairsRest() { + switch meta.Type(p.Key) { + case meta.TypeID: + if zid, err := id.Parse(p.Value); err == nil { + back = remRef(back, zid) + } + case meta.TypeIDSet: + for _, val := range meta.ListFromValue(p.Value) { + if zid, err := id.Parse(val); err == nil { + back = remRef(back, zid) + } + } + } + } + return back +} + +func (ms *memStore) UpdateReferences(_ context.Context, zidx *store.ZettelIndex) id.Set { + ms.mx.Lock() + defer ms.mx.Unlock() + m := ms.makeMeta(zidx) + zi, ziExist := ms.idx[zidx.Zid] + if !ziExist || zi == nil { + zi = &zettelData{} + ziExist = false + } + + // Is this zettel an old dead reference mentioned in other zettel? + var toCheck id.Set + if refs, ok := ms.dead[zidx.Zid]; ok { + // These must be checked later again + toCheck = id.NewSet(refs...) + delete(ms.dead, zidx.Zid) + } + + zi.meta = m + ms.updateDeadReferences(zidx, zi) + ids := ms.updateForwardBackwardReferences(zidx, zi) + toCheck = toCheck.Copy(ids) + ids = ms.updateMetadataReferences(zidx, zi) + toCheck = toCheck.Copy(ids) + zi.words = updateStrings(zidx.Zid, ms.words, zi.words, zidx.GetWords()) + zi.urls = updateStrings(zidx.Zid, ms.urls, zi.urls, zidx.GetUrls()) + + // Check if zi must be inserted into ms.idx + if !ziExist { + ms.idx[zidx.Zid] = zi + } + + return toCheck +} + +var internableKeys = map[string]bool{ + api.KeyRole: true, + api.KeySyntax: true, + api.KeyFolgeRole: true, + api.KeyLang: true, + api.KeyReadOnly: true, +} + +func isInternableValue(key string) bool { + if internableKeys[key] { + return true + } + return strings.HasSuffix(key, meta.SuffixKeyRole) +} + +func (ms *memStore) internString(s string) string { + if is, found := ms.intern[s]; found { + return is + } + ms.intern[s] = s + return s +} + +func (ms *memStore) makeMeta(zidx *store.ZettelIndex) *meta.Meta { + origM := zidx.GetMeta() + copyM := meta.New(origM.Zid) + for _, p := range origM.Pairs() { + key := ms.internString(p.Key) + if isInternableValue(key) { + copyM.Set(key, ms.internString(p.Value)) + } else if key == api.KeyBoxNumber || !meta.IsComputed(key) { + copyM.Set(key, p.Value) + } + } + return copyM +} + +func (ms *memStore) updateDeadReferences(zidx *store.ZettelIndex, zi *zettelData) { + // Must only be called if ms.mx is write-locked! + drefs := zidx.GetDeadRefs() + newRefs, remRefs := refsDiff(drefs, zi.dead) + zi.dead = drefs + for _, ref := range remRefs { + ms.dead[ref] = remRef(ms.dead[ref], zidx.Zid) + } + for _, ref := range newRefs { + ms.dead[ref] = addRef(ms.dead[ref], zidx.Zid) + } +} + +func (ms *memStore) updateForwardBackwardReferences(zidx *store.ZettelIndex, zi *zettelData) id.Set { + // Must only be called if ms.mx is write-locked! + brefs := zidx.GetBackRefs() + newRefs, remRefs := refsDiff(brefs, zi.forward) + zi.forward = brefs + + var toCheck id.Set + for _, ref := range remRefs { + bzi := ms.getOrCreateEntry(ref) + bzi.backward = remRef(bzi.backward, zidx.Zid) + if bzi.meta == nil { + toCheck = toCheck.Add(ref) + } + } + for _, ref := range newRefs { + bzi := ms.getOrCreateEntry(ref) + bzi.backward = addRef(bzi.backward, zidx.Zid) + if bzi.meta == nil { + toCheck = toCheck.Add(ref) + } + } + return toCheck +} + +func (ms *memStore) updateMetadataReferences(zidx *store.ZettelIndex, zi *zettelData) id.Set { + // Must only be called if ms.mx is write-locked! + inverseRefs := zidx.GetInverseRefs() + for key, mr := range zi.otherRefs { + if _, ok := inverseRefs[key]; ok { + continue + } + ms.removeInverseMeta(zidx.Zid, key, mr.forward) + } + if zi.otherRefs == nil { + zi.otherRefs = make(map[string]bidiRefs) + } + var toCheck id.Set + for key, mrefs := range inverseRefs { + mr := zi.otherRefs[key] + newRefs, remRefs := refsDiff(mrefs, mr.forward) + mr.forward = mrefs + zi.otherRefs[key] = mr + + for _, ref := range newRefs { + bzi := ms.getOrCreateEntry(ref) + if bzi.otherRefs == nil { + bzi.otherRefs = make(map[string]bidiRefs) + } + bmr := bzi.otherRefs[key] + bmr.backward = addRef(bmr.backward, zidx.Zid) + bzi.otherRefs[key] = bmr + if bzi.meta == nil { + toCheck = toCheck.Add(ref) + } + } + ms.removeInverseMeta(zidx.Zid, key, remRefs) + } + return toCheck +} + +func updateStrings(zid id.Zid, srefs stringRefs, prev []string, next store.WordSet) []string { + newWords, removeWords := next.Diff(prev) + for _, word := range newWords { + if refs, ok := srefs[word]; ok { + srefs[word] = addRef(refs, zid) + continue + } + srefs[word] = id.Slice{zid} + } + for _, word := range removeWords { + refs, ok := srefs[word] + if !ok { + continue + } + refs2 := remRef(refs, zid) + if len(refs2) == 0 { + delete(srefs, word) + continue + } + srefs[word] = refs2 + } + return next.Words() +} + +func (ms *memStore) getOrCreateEntry(zid id.Zid) *zettelData { + // Must only be called if ms.mx is write-locked! + if zi, ok := ms.idx[zid]; ok { + return zi + } + zi := &zettelData{} + ms.idx[zid] = zi + return zi +} + +func (ms *memStore) RenameZettel(_ context.Context, curZid, newZid id.Zid) id.Set { + ms.mx.Lock() + defer ms.mx.Unlock() + + curZi, curFound := ms.idx[curZid] + _, newFound := ms.idx[newZid] + if !curFound || newFound { + return nil + } + newZi := &zettelData{ + meta: copyMeta(curZi.meta, newZid), + dead: ms.copyDeadReferences(curZi.dead), + forward: ms.copyForward(curZi.forward, newZid), + backward: nil, // will be done through tocheck + otherRefs: nil, // TODO: check if this will be done through toCheck + words: copyStrings(ms.words, curZi.words, newZid), + urls: copyStrings(ms.urls, curZi.urls, newZid), + } + + ms.idx[newZid] = newZi + toCheck := ms.doDeleteZettel(curZid) + toCheck = toCheck.CopySlice(ms.dead[newZid]) + delete(ms.dead, newZid) + toCheck = toCheck.Add(newZid) // should update otherRefs + return toCheck +} +func copyMeta(m *meta.Meta, newZid id.Zid) *meta.Meta { + result := m.Clone() + result.Zid = newZid + return result +} +func (ms *memStore) copyDeadReferences(curDead id.Slice) id.Slice { + // Must only be called if ms.mx is write-locked! + if l := len(curDead); l > 0 { + result := make(id.Slice, l) + for i, ref := range curDead { + result[i] = ref + ms.dead[ref] = addRef(ms.dead[ref], ref) + } + return result + } + return nil +} +func (ms *memStore) copyForward(curForward id.Slice, newZid id.Zid) id.Slice { + // Must only be called if ms.mx is write-locked! + if l := len(curForward); l > 0 { + result := make(id.Slice, l) + for i, ref := range curForward { + result[i] = ref + if fzi, found := ms.idx[ref]; found { + fzi.backward = addRef(fzi.backward, newZid) + } + } + return result + } + return nil +} +func copyStrings(msStringMap stringRefs, curStrings []string, newZid id.Zid) []string { + // Must only be called if ms.mx is write-locked! + if l := len(curStrings); l > 0 { + result := make([]string, l) + for i, s := range curStrings { + result[i] = s + msStringMap[s] = addRef(msStringMap[s], newZid) + } + return result + } + return nil +} + +func (ms *memStore) DeleteZettel(_ context.Context, zid id.Zid) id.Set { + ms.mx.Lock() + defer ms.mx.Unlock() + return ms.doDeleteZettel(zid) +} + +func (ms *memStore) doDeleteZettel(zid id.Zid) id.Set { + // Must only be called if ms.mx is write-locked! + zi, ok := ms.idx[zid] + if !ok { + return nil + } + + ms.deleteDeadSources(zid, zi) + toCheck := ms.deleteForwardBackward(zid, zi) + for key, mrefs := range zi.otherRefs { + ms.removeInverseMeta(zid, key, mrefs.forward) + } + deleteStrings(ms.words, zi.words, zid) + deleteStrings(ms.urls, zi.urls, zid) + delete(ms.idx, zid) + return toCheck +} + +func (ms *memStore) deleteDeadSources(zid id.Zid, zi *zettelData) { + // Must only be called if ms.mx is write-locked! + for _, ref := range zi.dead { + if drefs, ok := ms.dead[ref]; ok { + drefs = remRef(drefs, zid) + if len(drefs) > 0 { + ms.dead[ref] = drefs + } else { + delete(ms.dead, ref) + } + } + } +} + +func (ms *memStore) deleteForwardBackward(zid id.Zid, zi *zettelData) id.Set { + // Must only be called if ms.mx is write-locked! + for _, ref := range zi.forward { + if fzi, ok := ms.idx[ref]; ok { + fzi.backward = remRef(fzi.backward, zid) + } + } + var toCheck id.Set + for _, ref := range zi.backward { + if bzi, ok := ms.idx[ref]; ok { + bzi.forward = remRef(bzi.forward, zid) + toCheck = toCheck.Add(ref) + } + } + return toCheck +} + +func (ms *memStore) removeInverseMeta(zid id.Zid, key string, forward id.Slice) { + // Must only be called if ms.mx is write-locked! + for _, ref := range forward { + bzi, ok := ms.idx[ref] + if !ok || bzi.otherRefs == nil { + continue + } + bmr, ok := bzi.otherRefs[key] + if !ok { + continue + } + bmr.backward = remRef(bmr.backward, zid) + if len(bmr.backward) > 0 || len(bmr.forward) > 0 { + bzi.otherRefs[key] = bmr + } else { + delete(bzi.otherRefs, key) + if len(bzi.otherRefs) == 0 { + bzi.otherRefs = nil + } + } + } +} + +func deleteStrings(msStringMap stringRefs, curStrings []string, zid id.Zid) { + // Must only be called if ms.mx is write-locked! + for _, word := range curStrings { + refs, ok := msStringMap[word] + if !ok { + continue + } + refs2 := remRef(refs, zid) + if len(refs2) == 0 { + delete(msStringMap, word) + continue + } + msStringMap[word] = refs2 + } +} + +func (ms *memStore) ReadStats(st *store.Stats) { + ms.mx.RLock() + st.Zettel = len(ms.idx) + st.Words = uint64(len(ms.words)) + st.Urls = uint64(len(ms.urls)) + ms.mx.RUnlock() + ms.mxStats.Lock() + st.Updates = ms.updates + ms.mxStats.Unlock() +} + +func (ms *memStore) Dump(w io.Writer) { + ms.mx.RLock() + defer ms.mx.RUnlock() + + io.WriteString(w, "=== Dump\n") + ms.dumpIndex(w) + ms.dumpDead(w) + dumpStringRefs(w, "Words", "", "", ms.words) + dumpStringRefs(w, "URLs", "[[", "]]", ms.urls) +} + +func (ms *memStore) dumpIndex(w io.Writer) { + if len(ms.idx) == 0 { + return + } + io.WriteString(w, "==== Zettel Index\n") + zids := make(id.Slice, 0, len(ms.idx)) + for id := range ms.idx { + zids = append(zids, id) + } + zids.Sort() + for _, id := range zids { + fmt.Fprintln(w, "=====", id) + zi := ms.idx[id] + if len(zi.dead) > 0 { + fmt.Fprintln(w, "* Dead:", zi.dead) + } + dumpZids(w, "* Forward:", zi.forward) + dumpZids(w, "* Backward:", zi.backward) + for k, fb := range zi.otherRefs { + fmt.Fprintln(w, "* Meta", k) + dumpZids(w, "** Forward:", fb.forward) + dumpZids(w, "** Backward:", fb.backward) + } + dumpStrings(w, "* Words", "", "", zi.words) + dumpStrings(w, "* URLs", "[[", "]]", zi.urls) + } +} + +func (ms *memStore) dumpDead(w io.Writer) { + if len(ms.dead) == 0 { + return + } + fmt.Fprintf(w, "==== Dead References\n") + zids := make(id.Slice, 0, len(ms.dead)) + for id := range ms.dead { + zids = append(zids, id) + } + zids.Sort() + for _, id := range zids { + fmt.Fprintln(w, ";", id) + fmt.Fprintln(w, ":", ms.dead[id]) + } +} + +func dumpZids(w io.Writer, prefix string, zids id.Slice) { + if len(zids) > 0 { + io.WriteString(w, prefix) + for _, zid := range zids { + io.WriteString(w, " ") + w.Write(zid.Bytes()) + } + fmt.Fprintln(w) + } +} + +func dumpStrings(w io.Writer, title, preString, postString string, slice []string) { + if len(slice) > 0 { + sl := make([]string, len(slice)) + copy(sl, slice) + sort.Strings(sl) + fmt.Fprintln(w, title) + for _, s := range sl { + fmt.Fprintf(w, "** %s%s%s\n", preString, s, postString) + } + } + +} + +func dumpStringRefs(w io.Writer, title, preString, postString string, srefs stringRefs) { + if len(srefs) == 0 { + return + } + fmt.Fprintln(w, "====", title) + for _, s := range maps.Keys(srefs) { + fmt.Fprintf(w, "; %s%s%s\n", preString, s, postString) + fmt.Fprintln(w, ":", srefs[s]) + } +} ADDED box/manager/mapstore/refs.go Index: box/manager/mapstore/refs.go ================================================================== --- /dev/null +++ box/manager/mapstore/refs.go @@ -0,0 +1,105 @@ +//----------------------------------------------------------------------------- +// Copyright (c) 2021-present 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. +// +// SPDX-License-Identifier: EUPL-1.2 +// SPDX-FileCopyrightText: 2021-present Detlef Stern +//----------------------------------------------------------------------------- + +package mapstore + +import ( + "slices" + + "zettelstore.de/z/zettel/id" +) + +func refsDiff(refsN, refsO id.Slice) (newRefs, remRefs id.Slice) { + npos, opos := 0, 0 + for npos < len(refsN) && opos < len(refsO) { + rn, ro := refsN[npos], refsO[opos] + if rn == ro { + npos++ + opos++ + continue + } + if rn < ro { + newRefs = append(newRefs, rn) + npos++ + continue + } + remRefs = append(remRefs, ro) + opos++ + } + if npos < len(refsN) { + newRefs = append(newRefs, refsN[npos:]...) + } + if opos < len(refsO) { + remRefs = append(remRefs, refsO[opos:]...) + } + return newRefs, remRefs +} + +func addRef(refs id.Slice, ref id.Zid) id.Slice { + hi := len(refs) + for lo := 0; lo < hi; { + m := lo + (hi-lo)/2 + if r := refs[m]; r == ref { + return refs + } else if r < ref { + lo = m + 1 + } else { + hi = m + } + } + refs = slices.Insert(refs, hi, ref) + return refs +} + +func remRefs(refs, rem id.Slice) id.Slice { + if len(refs) == 0 || len(rem) == 0 { + return refs + } + result := make(id.Slice, 0, len(refs)) + rpos, dpos := 0, 0 + for rpos < len(refs) && dpos < len(rem) { + rr, dr := refs[rpos], rem[dpos] + if rr < dr { + result = append(result, rr) + rpos++ + continue + } + if dr < rr { + dpos++ + continue + } + rpos++ + dpos++ + } + if rpos < len(refs) { + result = append(result, refs[rpos:]...) + } + return result +} + +func remRef(refs id.Slice, ref id.Zid) id.Slice { + hi := len(refs) + for lo := 0; lo < hi; { + m := lo + (hi-lo)/2 + if r := refs[m]; r == ref { + copy(refs[m:], refs[m+1:]) + refs = refs[:len(refs)-1] + return refs + } else if r < ref { + lo = m + 1 + } else { + hi = m + } + } + return refs +} ADDED box/manager/mapstore/refs_test.go Index: box/manager/mapstore/refs_test.go ================================================================== --- /dev/null +++ box/manager/mapstore/refs_test.go @@ -0,0 +1,140 @@ +//----------------------------------------------------------------------------- +// Copyright (c) 2021-present 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. +// +// SPDX-License-Identifier: EUPL-1.2 +// SPDX-FileCopyrightText: 2021-present Detlef Stern +//----------------------------------------------------------------------------- + +package mapstore + +import ( + "testing" + + "zettelstore.de/z/zettel/id" +) + +func assertRefs(t *testing.T, i int, got, exp id.Slice) { + t.Helper() + if got == nil && exp != nil { + t.Errorf("%d: got nil, but expected %v", i, exp) + return + } + if got != nil && exp == nil { + t.Errorf("%d: expected nil, but got %v", i, got) + return + } + if len(got) != len(exp) { + t.Errorf("%d: expected len(%v)==%d, but got len(%v)==%d", i, exp, len(exp), got, len(got)) + return + } + for p, n := range exp { + if got := got[p]; got != id.Zid(n) { + t.Errorf("%d: pos %d: expected %d, but got %d", i, p, n, got) + } + } +} + +func TestRefsDiff(t *testing.T) { + t.Parallel() + testcases := []struct { + in1, in2 id.Slice + exp1, exp2 id.Slice + }{ + {nil, nil, nil, nil}, + {id.Slice{1}, nil, id.Slice{1}, nil}, + {nil, id.Slice{1}, nil, id.Slice{1}}, + {id.Slice{1}, id.Slice{1}, nil, nil}, + {id.Slice{1, 2}, id.Slice{1}, id.Slice{2}, nil}, + {id.Slice{1, 2}, id.Slice{1, 3}, id.Slice{2}, id.Slice{3}}, + {id.Slice{1, 4}, id.Slice{1, 3}, id.Slice{4}, id.Slice{3}}, + } + for i, tc := range testcases { + got1, got2 := refsDiff(tc.in1, tc.in2) + assertRefs(t, i, got1, tc.exp1) + assertRefs(t, i, got2, tc.exp2) + } +} + +func TestAddRef(t *testing.T) { + t.Parallel() + testcases := []struct { + ref id.Slice + zid uint + exp id.Slice + }{ + {nil, 5, id.Slice{5}}, + {id.Slice{1}, 5, id.Slice{1, 5}}, + {id.Slice{10}, 5, id.Slice{5, 10}}, + {id.Slice{5}, 5, id.Slice{5}}, + {id.Slice{1, 10}, 5, id.Slice{1, 5, 10}}, + {id.Slice{1, 5, 10}, 5, id.Slice{1, 5, 10}}, + } + for i, tc := range testcases { + got := addRef(tc.ref, id.Zid(tc.zid)) + assertRefs(t, i, got, tc.exp) + } +} + +func TestRemRefs(t *testing.T) { + t.Parallel() + testcases := []struct { + in1, in2 id.Slice + exp id.Slice + }{ + {nil, nil, nil}, + {nil, id.Slice{}, nil}, + {id.Slice{}, nil, id.Slice{}}, + {id.Slice{}, id.Slice{}, id.Slice{}}, + {id.Slice{1}, id.Slice{5}, id.Slice{1}}, + {id.Slice{10}, id.Slice{5}, id.Slice{10}}, + {id.Slice{1, 5}, id.Slice{5}, id.Slice{1}}, + {id.Slice{5, 10}, id.Slice{5}, id.Slice{10}}, + {id.Slice{1, 10}, id.Slice{5}, id.Slice{1, 10}}, + {id.Slice{1}, id.Slice{2, 5}, id.Slice{1}}, + {id.Slice{10}, id.Slice{2, 5}, id.Slice{10}}, + {id.Slice{1, 5}, id.Slice{2, 5}, id.Slice{1}}, + {id.Slice{5, 10}, id.Slice{2, 5}, id.Slice{10}}, + {id.Slice{1, 2, 5}, id.Slice{2, 5}, id.Slice{1}}, + {id.Slice{2, 5, 10}, id.Slice{2, 5}, id.Slice{10}}, + {id.Slice{1, 10}, id.Slice{2, 5}, id.Slice{1, 10}}, + {id.Slice{1}, id.Slice{5, 9}, id.Slice{1}}, + {id.Slice{10}, id.Slice{5, 9}, id.Slice{10}}, + {id.Slice{1, 5}, id.Slice{5, 9}, id.Slice{1}}, + {id.Slice{5, 10}, id.Slice{5, 9}, id.Slice{10}}, + {id.Slice{1, 5, 9}, id.Slice{5, 9}, id.Slice{1}}, + {id.Slice{5, 9, 10}, id.Slice{5, 9}, id.Slice{10}}, + {id.Slice{1, 10}, id.Slice{5, 9}, id.Slice{1, 10}}, + } + for i, tc := range testcases { + got := remRefs(tc.in1, tc.in2) + assertRefs(t, i, got, tc.exp) + } +} + +func TestRemRef(t *testing.T) { + t.Parallel() + testcases := []struct { + ref id.Slice + zid uint + exp id.Slice + }{ + {nil, 5, nil}, + {id.Slice{}, 5, id.Slice{}}, + {id.Slice{5}, 5, id.Slice{}}, + {id.Slice{1}, 5, id.Slice{1}}, + {id.Slice{10}, 5, id.Slice{10}}, + {id.Slice{1, 5}, 5, id.Slice{1}}, + {id.Slice{5, 10}, 5, id.Slice{10}}, + {id.Slice{1, 5, 10}, 5, id.Slice{1, 10}}, + } + for i, tc := range testcases { + got := remRef(tc.ref, id.Zid(tc.zid)) + assertRefs(t, i, got, tc.exp) + } +} DELETED box/manager/memstore/memstore.go Index: box/manager/memstore/memstore.go ================================================================== --- box/manager/memstore/memstore.go +++ /dev/null @@ -1,714 +0,0 @@ -//----------------------------------------------------------------------------- -// Copyright (c) 2021-present 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 memstore stored the index in main memory. -package memstore - -import ( - "context" - "fmt" - "io" - "sort" - "strings" - "sync" - - "zettelstore.de/client.fossil/api" - "zettelstore.de/client.fossil/maps" - "zettelstore.de/z/box" - "zettelstore.de/z/box/manager/store" - "zettelstore.de/z/zettel/id" - "zettelstore.de/z/zettel/meta" -) - -type bidiRefs struct { - forward id.Slice - backward id.Slice -} - -type zettelData struct { - meta *meta.Meta // a local copy of the metadata, without computed keys - dead id.Slice // list of dead references in this zettel - forward id.Slice // list of forward references in this zettel - backward id.Slice // list of zettel that reference with zettel - otherRefs map[string]bidiRefs - words []string // list of words of this zettel - urls []string // list of urls of this zettel -} - -type stringRefs map[string]id.Slice - -type memStore struct { - mx sync.RWMutex - intern map[string]string // map to intern strings - idx map[id.Zid]*zettelData - dead map[id.Zid]id.Slice // map dead refs where they occur - words stringRefs - urls stringRefs - - // Stats - mxStats sync.Mutex - updates uint64 -} - -// New returns a new memory-based index store. -func New() store.Store { - return &memStore{ - intern: make(map[string]string, 1024), - idx: make(map[id.Zid]*zettelData), - dead: make(map[id.Zid]id.Slice), - words: make(stringRefs), - urls: make(stringRefs), - } -} - -func (ms *memStore) GetMeta(_ context.Context, zid id.Zid) (*meta.Meta, error) { - ms.mx.RLock() - defer ms.mx.RUnlock() - if zi, found := ms.idx[zid]; found && zi.meta != nil { - // zi.meta is nil, if zettel was referenced, but is not indexed yet. - return zi.meta.Clone(), nil - } - return nil, box.ErrZettelNotFound{Zid: zid} -} - -func (ms *memStore) Enrich(_ context.Context, m *meta.Meta) { - if ms.doEnrich(m) { - ms.mxStats.Lock() - ms.updates++ - ms.mxStats.Unlock() - } -} - -func (ms *memStore) doEnrich(m *meta.Meta) bool { - ms.mx.RLock() - defer ms.mx.RUnlock() - zi, ok := ms.idx[m.Zid] - if !ok { - return false - } - var updated bool - if len(zi.dead) > 0 { - m.Set(api.KeyDead, zi.dead.String()) - updated = true - } - back := removeOtherMetaRefs(m, zi.backward.Clone()) - if len(zi.backward) > 0 { - m.Set(api.KeyBackward, zi.backward.String()) - updated = true - } - if len(zi.forward) > 0 { - m.Set(api.KeyForward, zi.forward.String()) - back = remRefs(back, zi.forward) - updated = true - } - for k, refs := range zi.otherRefs { - if len(refs.backward) > 0 { - m.Set(k, refs.backward.String()) - back = remRefs(back, refs.backward) - updated = true - } - } - if len(back) > 0 { - m.Set(api.KeyBack, back.String()) - updated = true - } - return updated -} - -// SearchEqual returns all zettel that contains the given exact word. -// The word must be normalized through Unicode NKFD, trimmed and not empty. -func (ms *memStore) SearchEqual(word string) id.Set { - ms.mx.RLock() - defer ms.mx.RUnlock() - result := id.NewSet() - if refs, ok := ms.words[word]; ok { - result.CopySlice(refs) - } - if refs, ok := ms.urls[word]; ok { - result.CopySlice(refs) - } - zid, err := id.Parse(word) - if err != nil { - return result - } - zi, ok := ms.idx[zid] - if !ok { - return result - } - - addBackwardZids(result, zid, zi) - return result -} - -// SearchPrefix returns all zettel that have a word with the given prefix. -// The prefix must be normalized through Unicode NKFD, trimmed and not empty. -func (ms *memStore) SearchPrefix(prefix string) id.Set { - ms.mx.RLock() - defer ms.mx.RUnlock() - result := ms.selectWithPred(prefix, strings.HasPrefix) - l := len(prefix) - if l > 14 { - 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) - } - } - return result -} - -// SearchSuffix returns all zettel that have a word with the given suffix. -// The suffix must be normalized through Unicode NKFD, trimmed and not empty. -func (ms *memStore) SearchSuffix(suffix string) id.Set { - ms.mx.RLock() - defer ms.mx.RUnlock() - result := ms.selectWithPred(suffix, strings.HasSuffix) - l := len(suffix) - if l > 14 { - return result - } - val, err := id.ParseUint(suffix) - if err != nil { - return result - } - modulo := uint64(1) - for i := 0; i < l; i++ { - modulo *= 10 - } - for zid, zi := range ms.idx { - if uint64(zid)%modulo == val { - addBackwardZids(result, zid, zi) - } - } - return result -} - -// SearchContains returns all zettel that contains the given string. -// The string must be normalized through Unicode NKFD, trimmed and not empty. -func (ms *memStore) SearchContains(s string) id.Set { - ms.mx.RLock() - defer ms.mx.RUnlock() - result := ms.selectWithPred(s, strings.Contains) - if len(s) > 14 { - return result - } - if _, err := id.ParseUint(s); err != nil { - return result - } - for zid, zi := range ms.idx { - if strings.Contains(zid.String(), s) { - addBackwardZids(result, zid, zi) - } - } - return result -} - -func (ms *memStore) selectWithPred(s string, pred func(string, string) bool) id.Set { - // Must only be called if ms.mx is read-locked! - result := id.NewSet() - for word, refs := range ms.words { - if !pred(word, s) { - continue - } - result.CopySlice(refs) - } - for u, refs := range ms.urls { - if !pred(u, s) { - continue - } - result.CopySlice(refs) - } - return result -} - -func addBackwardZids(result id.Set, zid id.Zid, zi *zettelData) { - // Must only be called if ms.mx is read-locked! - result.Add(zid) - result.CopySlice(zi.backward) - for _, mref := range zi.otherRefs { - result.CopySlice(mref.backward) - } -} - -func removeOtherMetaRefs(m *meta.Meta, back id.Slice) id.Slice { - for _, p := range m.PairsRest() { - switch meta.Type(p.Key) { - case meta.TypeID: - if zid, err := id.Parse(p.Value); err == nil { - back = remRef(back, zid) - } - case meta.TypeIDSet: - for _, val := range meta.ListFromValue(p.Value) { - if zid, err := id.Parse(val); err == nil { - back = remRef(back, zid) - } - } - } - } - return back -} - -func (ms *memStore) UpdateReferences(_ context.Context, zidx *store.ZettelIndex) id.Set { - ms.mx.Lock() - defer ms.mx.Unlock() - m := ms.makeMeta(zidx) - zi, ziExist := ms.idx[zidx.Zid] - if !ziExist || zi == nil { - zi = &zettelData{} - ziExist = false - } - - // Is this zettel an old dead reference mentioned in other zettel? - var toCheck id.Set - if refs, ok := ms.dead[zidx.Zid]; ok { - // These must be checked later again - toCheck = id.NewSet(refs...) - delete(ms.dead, zidx.Zid) - } - - zi.meta = m - ms.updateDeadReferences(zidx, zi) - ids := ms.updateForwardBackwardReferences(zidx, zi) - toCheck = toCheck.Copy(ids) - ids = ms.updateMetadataReferences(zidx, zi) - toCheck = toCheck.Copy(ids) - zi.words = updateStrings(zidx.Zid, ms.words, zi.words, zidx.GetWords()) - zi.urls = updateStrings(zidx.Zid, ms.urls, zi.urls, zidx.GetUrls()) - - // Check if zi must be inserted into ms.idx - if !ziExist { - ms.idx[zidx.Zid] = zi - } - - return toCheck -} - -var internableKeys = map[string]bool{ - api.KeyRole: true, - api.KeySyntax: true, - api.KeyFolgeRole: true, - api.KeyLang: true, - api.KeyReadOnly: true, -} - -func isInternableValue(key string) bool { - if internableKeys[key] { - return true - } - return strings.HasSuffix(key, "-role") -} - -func (ms *memStore) internString(s string) string { - if is, found := ms.intern[s]; found { - return is - } - ms.intern[s] = s - return s -} - -func (ms *memStore) makeMeta(zidx *store.ZettelIndex) *meta.Meta { - origM := zidx.GetMeta() - copyM := meta.New(origM.Zid) - for _, p := range origM.Pairs() { - key := ms.internString(p.Key) - if isInternableValue(key) { - copyM.Set(key, ms.internString(p.Value)) - } else if key == api.KeyBoxNumber || !meta.IsComputed(key) { - copyM.Set(key, p.Value) - } - } - return copyM -} - -func (ms *memStore) updateDeadReferences(zidx *store.ZettelIndex, zi *zettelData) { - // Must only be called if ms.mx is write-locked! - drefs := zidx.GetDeadRefs() - newRefs, remRefs := refsDiff(drefs, zi.dead) - zi.dead = drefs - for _, ref := range remRefs { - ms.dead[ref] = remRef(ms.dead[ref], zidx.Zid) - } - for _, ref := range newRefs { - ms.dead[ref] = addRef(ms.dead[ref], zidx.Zid) - } -} - -func (ms *memStore) updateForwardBackwardReferences(zidx *store.ZettelIndex, zi *zettelData) id.Set { - // Must only be called if ms.mx is write-locked! - brefs := zidx.GetBackRefs() - newRefs, remRefs := refsDiff(brefs, zi.forward) - zi.forward = brefs - - var toCheck id.Set - for _, ref := range remRefs { - bzi := ms.getOrCreateEntry(ref) - bzi.backward = remRef(bzi.backward, zidx.Zid) - if bzi.meta == nil { - toCheck = toCheck.Add(ref) - } - } - for _, ref := range newRefs { - bzi := ms.getOrCreateEntry(ref) - bzi.backward = addRef(bzi.backward, zidx.Zid) - if bzi.meta == nil { - toCheck = toCheck.Add(ref) - } - } - return toCheck -} - -func (ms *memStore) updateMetadataReferences(zidx *store.ZettelIndex, zi *zettelData) id.Set { - // Must only be called if ms.mx is write-locked! - inverseRefs := zidx.GetInverseRefs() - for key, mr := range zi.otherRefs { - if _, ok := inverseRefs[key]; ok { - continue - } - ms.removeInverseMeta(zidx.Zid, key, mr.forward) - } - if zi.otherRefs == nil { - zi.otherRefs = make(map[string]bidiRefs) - } - var toCheck id.Set - for key, mrefs := range inverseRefs { - mr := zi.otherRefs[key] - newRefs, remRefs := refsDiff(mrefs, mr.forward) - mr.forward = mrefs - zi.otherRefs[key] = mr - - for _, ref := range newRefs { - bzi := ms.getOrCreateEntry(ref) - if bzi.otherRefs == nil { - bzi.otherRefs = make(map[string]bidiRefs) - } - bmr := bzi.otherRefs[key] - bmr.backward = addRef(bmr.backward, zidx.Zid) - bzi.otherRefs[key] = bmr - if bzi.meta == nil { - toCheck = toCheck.Add(ref) - } - } - ms.removeInverseMeta(zidx.Zid, key, remRefs) - } - return toCheck -} - -func updateStrings(zid id.Zid, srefs stringRefs, prev []string, next store.WordSet) []string { - newWords, removeWords := next.Diff(prev) - for _, word := range newWords { - if refs, ok := srefs[word]; ok { - srefs[word] = addRef(refs, zid) - continue - } - srefs[word] = id.Slice{zid} - } - for _, word := range removeWords { - refs, ok := srefs[word] - if !ok { - continue - } - refs2 := remRef(refs, zid) - if len(refs2) == 0 { - delete(srefs, word) - continue - } - srefs[word] = refs2 - } - return next.Words() -} - -func (ms *memStore) getOrCreateEntry(zid id.Zid) *zettelData { - // Must only be called if ms.mx is write-locked! - if zi, ok := ms.idx[zid]; ok { - return zi - } - zi := &zettelData{} - ms.idx[zid] = zi - return zi -} - -func (ms *memStore) RenameZettel(_ context.Context, curZid, newZid id.Zid) id.Set { - ms.mx.Lock() - defer ms.mx.Unlock() - - curZi, curFound := ms.idx[curZid] - _, newFound := ms.idx[newZid] - if !curFound || newFound { - return nil - } - newZi := &zettelData{ - meta: copyMeta(curZi.meta, newZid), - dead: ms.copyDeadReferences(curZi.dead), - forward: ms.copyForward(curZi.forward, newZid), - backward: nil, // will be done through tocheck - otherRefs: nil, // TODO: check if this will be done through toCheck - words: copyStrings(ms.words, curZi.words, newZid), - urls: copyStrings(ms.urls, curZi.urls, newZid), - } - - ms.idx[newZid] = newZi - toCheck := ms.doDeleteZettel(curZid) - toCheck = toCheck.CopySlice(ms.dead[newZid]) - delete(ms.dead, newZid) - toCheck = toCheck.Add(newZid) // should update otherRefs - return toCheck -} -func copyMeta(m *meta.Meta, newZid id.Zid) *meta.Meta { - result := m.Clone() - result.Zid = newZid - return result -} -func (ms *memStore) copyDeadReferences(curDead id.Slice) id.Slice { - // Must only be called if ms.mx is write-locked! - if l := len(curDead); l > 0 { - result := make(id.Slice, l) - for i, ref := range curDead { - result[i] = ref - ms.dead[ref] = addRef(ms.dead[ref], ref) - } - return result - } - return nil -} -func (ms *memStore) copyForward(curForward id.Slice, newZid id.Zid) id.Slice { - // Must only be called if ms.mx is write-locked! - if l := len(curForward); l > 0 { - result := make(id.Slice, l) - for i, ref := range curForward { - result[i] = ref - if fzi, found := ms.idx[ref]; found { - fzi.backward = addRef(fzi.backward, newZid) - } - } - return result - } - return nil -} -func copyStrings(msStringMap stringRefs, curStrings []string, newZid id.Zid) []string { - // Must only be called if ms.mx is write-locked! - if l := len(curStrings); l > 0 { - result := make([]string, l) - for i, s := range curStrings { - result[i] = s - msStringMap[s] = addRef(msStringMap[s], newZid) - } - return result - } - return nil -} - -func (ms *memStore) DeleteZettel(_ context.Context, zid id.Zid) id.Set { - ms.mx.Lock() - defer ms.mx.Unlock() - return ms.doDeleteZettel(zid) -} - -func (ms *memStore) doDeleteZettel(zid id.Zid) id.Set { - // Must only be called if ms.mx is write-locked! - zi, ok := ms.idx[zid] - if !ok { - return nil - } - - ms.deleteDeadSources(zid, zi) - toCheck := ms.deleteForwardBackward(zid, zi) - for key, mrefs := range zi.otherRefs { - ms.removeInverseMeta(zid, key, mrefs.forward) - } - deleteStrings(ms.words, zi.words, zid) - deleteStrings(ms.urls, zi.urls, zid) - delete(ms.idx, zid) - return toCheck -} - -func (ms *memStore) deleteDeadSources(zid id.Zid, zi *zettelData) { - // Must only be called if ms.mx is write-locked! - for _, ref := range zi.dead { - if drefs, ok := ms.dead[ref]; ok { - drefs = remRef(drefs, zid) - if len(drefs) > 0 { - ms.dead[ref] = drefs - } else { - delete(ms.dead, ref) - } - } - } -} - -func (ms *memStore) deleteForwardBackward(zid id.Zid, zi *zettelData) id.Set { - // Must only be called if ms.mx is write-locked! - for _, ref := range zi.forward { - if fzi, ok := ms.idx[ref]; ok { - fzi.backward = remRef(fzi.backward, zid) - } - } - var toCheck id.Set - for _, ref := range zi.backward { - if bzi, ok := ms.idx[ref]; ok { - bzi.forward = remRef(bzi.forward, zid) - toCheck = toCheck.Add(ref) - } - } - return toCheck -} - -func (ms *memStore) removeInverseMeta(zid id.Zid, key string, forward id.Slice) { - // Must only be called if ms.mx is write-locked! - for _, ref := range forward { - bzi, ok := ms.idx[ref] - if !ok || bzi.otherRefs == nil { - continue - } - bmr, ok := bzi.otherRefs[key] - if !ok { - continue - } - bmr.backward = remRef(bmr.backward, zid) - if len(bmr.backward) > 0 || len(bmr.forward) > 0 { - bzi.otherRefs[key] = bmr - } else { - delete(bzi.otherRefs, key) - if len(bzi.otherRefs) == 0 { - bzi.otherRefs = nil - } - } - } -} - -func deleteStrings(msStringMap stringRefs, curStrings []string, zid id.Zid) { - // Must only be called if ms.mx is write-locked! - for _, word := range curStrings { - refs, ok := msStringMap[word] - if !ok { - continue - } - refs2 := remRef(refs, zid) - if len(refs2) == 0 { - delete(msStringMap, word) - continue - } - msStringMap[word] = refs2 - } -} - -func (ms *memStore) ReadStats(st *store.Stats) { - ms.mx.RLock() - st.Zettel = len(ms.idx) - st.Words = uint64(len(ms.words)) - st.Urls = uint64(len(ms.urls)) - ms.mx.RUnlock() - ms.mxStats.Lock() - st.Updates = ms.updates - ms.mxStats.Unlock() -} - -func (ms *memStore) Dump(w io.Writer) { - ms.mx.RLock() - defer ms.mx.RUnlock() - - io.WriteString(w, "=== Dump\n") - ms.dumpIndex(w) - ms.dumpDead(w) - dumpStringRefs(w, "Words", "", "", ms.words) - dumpStringRefs(w, "URLs", "[[", "]]", ms.urls) -} - -func (ms *memStore) dumpIndex(w io.Writer) { - if len(ms.idx) == 0 { - return - } - io.WriteString(w, "==== Zettel Index\n") - zids := make(id.Slice, 0, len(ms.idx)) - for id := range ms.idx { - zids = append(zids, id) - } - zids.Sort() - for _, id := range zids { - fmt.Fprintln(w, "=====", id) - zi := ms.idx[id] - if len(zi.dead) > 0 { - fmt.Fprintln(w, "* Dead:", zi.dead) - } - dumpZids(w, "* Forward:", zi.forward) - dumpZids(w, "* Backward:", zi.backward) - for k, fb := range zi.otherRefs { - fmt.Fprintln(w, "* Meta", k) - dumpZids(w, "** Forward:", fb.forward) - dumpZids(w, "** Backward:", fb.backward) - } - dumpStrings(w, "* Words", "", "", zi.words) - dumpStrings(w, "* URLs", "[[", "]]", zi.urls) - } -} - -func (ms *memStore) dumpDead(w io.Writer) { - if len(ms.dead) == 0 { - return - } - fmt.Fprintf(w, "==== Dead References\n") - zids := make(id.Slice, 0, len(ms.dead)) - for id := range ms.dead { - zids = append(zids, id) - } - zids.Sort() - for _, id := range zids { - fmt.Fprintln(w, ";", id) - fmt.Fprintln(w, ":", ms.dead[id]) - } -} - -func dumpZids(w io.Writer, prefix string, zids id.Slice) { - if len(zids) > 0 { - io.WriteString(w, prefix) - for _, zid := range zids { - io.WriteString(w, " ") - w.Write(zid.Bytes()) - } - fmt.Fprintln(w) - } -} - -func dumpStrings(w io.Writer, title, preString, postString string, slice []string) { - if len(slice) > 0 { - sl := make([]string, len(slice)) - copy(sl, slice) - sort.Strings(sl) - fmt.Fprintln(w, title) - for _, s := range sl { - fmt.Fprintf(w, "** %s%s%s\n", preString, s, postString) - } - } - -} - -func dumpStringRefs(w io.Writer, title, preString, postString string, srefs stringRefs) { - if len(srefs) == 0 { - return - } - fmt.Fprintln(w, "====", title) - for _, s := range maps.Keys(srefs) { - fmt.Fprintf(w, "; %s%s%s\n", preString, s, postString) - fmt.Fprintln(w, ":", srefs[s]) - } -} DELETED box/manager/memstore/refs.go Index: box/manager/memstore/refs.go ================================================================== --- box/manager/memstore/refs.go +++ /dev/null @@ -1,102 +0,0 @@ -//----------------------------------------------------------------------------- -// Copyright (c) 2021-present 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 memstore - -import ( - "slices" - - "zettelstore.de/z/zettel/id" -) - -func refsDiff(refsN, refsO id.Slice) (newRefs, remRefs id.Slice) { - npos, opos := 0, 0 - for npos < len(refsN) && opos < len(refsO) { - rn, ro := refsN[npos], refsO[opos] - if rn == ro { - npos++ - opos++ - continue - } - if rn < ro { - newRefs = append(newRefs, rn) - npos++ - continue - } - remRefs = append(remRefs, ro) - opos++ - } - if npos < len(refsN) { - newRefs = append(newRefs, refsN[npos:]...) - } - if opos < len(refsO) { - remRefs = append(remRefs, refsO[opos:]...) - } - return newRefs, remRefs -} - -func addRef(refs id.Slice, ref id.Zid) id.Slice { - hi := len(refs) - for lo := 0; lo < hi; { - m := lo + (hi-lo)/2 - if r := refs[m]; r == ref { - return refs - } else if r < ref { - lo = m + 1 - } else { - hi = m - } - } - refs = slices.Insert(refs, hi, ref) - return refs -} - -func remRefs(refs, rem id.Slice) id.Slice { - if len(refs) == 0 || len(rem) == 0 { - return refs - } - result := make(id.Slice, 0, len(refs)) - rpos, dpos := 0, 0 - for rpos < len(refs) && dpos < len(rem) { - rr, dr := refs[rpos], rem[dpos] - if rr < dr { - result = append(result, rr) - rpos++ - continue - } - if dr < rr { - dpos++ - continue - } - rpos++ - dpos++ - } - if rpos < len(refs) { - result = append(result, refs[rpos:]...) - } - return result -} - -func remRef(refs id.Slice, ref id.Zid) id.Slice { - hi := len(refs) - for lo := 0; lo < hi; { - m := lo + (hi-lo)/2 - if r := refs[m]; r == ref { - copy(refs[m:], refs[m+1:]) - refs = refs[:len(refs)-1] - return refs - } else if r < ref { - lo = m + 1 - } else { - hi = m - } - } - return refs -} DELETED box/manager/memstore/refs_test.go Index: box/manager/memstore/refs_test.go ================================================================== --- box/manager/memstore/refs_test.go +++ /dev/null @@ -1,137 +0,0 @@ -//----------------------------------------------------------------------------- -// Copyright (c) 2021-present 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 memstore - -import ( - "testing" - - "zettelstore.de/z/zettel/id" -) - -func assertRefs(t *testing.T, i int, got, exp id.Slice) { - t.Helper() - if got == nil && exp != nil { - t.Errorf("%d: got nil, but expected %v", i, exp) - return - } - if got != nil && exp == nil { - t.Errorf("%d: expected nil, but got %v", i, got) - return - } - if len(got) != len(exp) { - t.Errorf("%d: expected len(%v)==%d, but got len(%v)==%d", i, exp, len(exp), got, len(got)) - return - } - for p, n := range exp { - if got := got[p]; got != id.Zid(n) { - t.Errorf("%d: pos %d: expected %d, but got %d", i, p, n, got) - } - } -} - -func TestRefsDiff(t *testing.T) { - t.Parallel() - testcases := []struct { - in1, in2 id.Slice - exp1, exp2 id.Slice - }{ - {nil, nil, nil, nil}, - {id.Slice{1}, nil, id.Slice{1}, nil}, - {nil, id.Slice{1}, nil, id.Slice{1}}, - {id.Slice{1}, id.Slice{1}, nil, nil}, - {id.Slice{1, 2}, id.Slice{1}, id.Slice{2}, nil}, - {id.Slice{1, 2}, id.Slice{1, 3}, id.Slice{2}, id.Slice{3}}, - {id.Slice{1, 4}, id.Slice{1, 3}, id.Slice{4}, id.Slice{3}}, - } - for i, tc := range testcases { - got1, got2 := refsDiff(tc.in1, tc.in2) - assertRefs(t, i, got1, tc.exp1) - assertRefs(t, i, got2, tc.exp2) - } -} - -func TestAddRef(t *testing.T) { - t.Parallel() - testcases := []struct { - ref id.Slice - zid uint - exp id.Slice - }{ - {nil, 5, id.Slice{5}}, - {id.Slice{1}, 5, id.Slice{1, 5}}, - {id.Slice{10}, 5, id.Slice{5, 10}}, - {id.Slice{5}, 5, id.Slice{5}}, - {id.Slice{1, 10}, 5, id.Slice{1, 5, 10}}, - {id.Slice{1, 5, 10}, 5, id.Slice{1, 5, 10}}, - } - for i, tc := range testcases { - got := addRef(tc.ref, id.Zid(tc.zid)) - assertRefs(t, i, got, tc.exp) - } -} - -func TestRemRefs(t *testing.T) { - t.Parallel() - testcases := []struct { - in1, in2 id.Slice - exp id.Slice - }{ - {nil, nil, nil}, - {nil, id.Slice{}, nil}, - {id.Slice{}, nil, id.Slice{}}, - {id.Slice{}, id.Slice{}, id.Slice{}}, - {id.Slice{1}, id.Slice{5}, id.Slice{1}}, - {id.Slice{10}, id.Slice{5}, id.Slice{10}}, - {id.Slice{1, 5}, id.Slice{5}, id.Slice{1}}, - {id.Slice{5, 10}, id.Slice{5}, id.Slice{10}}, - {id.Slice{1, 10}, id.Slice{5}, id.Slice{1, 10}}, - {id.Slice{1}, id.Slice{2, 5}, id.Slice{1}}, - {id.Slice{10}, id.Slice{2, 5}, id.Slice{10}}, - {id.Slice{1, 5}, id.Slice{2, 5}, id.Slice{1}}, - {id.Slice{5, 10}, id.Slice{2, 5}, id.Slice{10}}, - {id.Slice{1, 2, 5}, id.Slice{2, 5}, id.Slice{1}}, - {id.Slice{2, 5, 10}, id.Slice{2, 5}, id.Slice{10}}, - {id.Slice{1, 10}, id.Slice{2, 5}, id.Slice{1, 10}}, - {id.Slice{1}, id.Slice{5, 9}, id.Slice{1}}, - {id.Slice{10}, id.Slice{5, 9}, id.Slice{10}}, - {id.Slice{1, 5}, id.Slice{5, 9}, id.Slice{1}}, - {id.Slice{5, 10}, id.Slice{5, 9}, id.Slice{10}}, - {id.Slice{1, 5, 9}, id.Slice{5, 9}, id.Slice{1}}, - {id.Slice{5, 9, 10}, id.Slice{5, 9}, id.Slice{10}}, - {id.Slice{1, 10}, id.Slice{5, 9}, id.Slice{1, 10}}, - } - for i, tc := range testcases { - got := remRefs(tc.in1, tc.in2) - assertRefs(t, i, got, tc.exp) - } -} - -func TestRemRef(t *testing.T) { - t.Parallel() - testcases := []struct { - ref id.Slice - zid uint - exp id.Slice - }{ - {nil, 5, nil}, - {id.Slice{}, 5, id.Slice{}}, - {id.Slice{5}, 5, id.Slice{}}, - {id.Slice{1}, 5, id.Slice{1}}, - {id.Slice{10}, 5, id.Slice{10}}, - {id.Slice{1, 5}, 5, id.Slice{1}}, - {id.Slice{5, 10}, 5, id.Slice{10}}, - {id.Slice{1, 5, 10}, 5, id.Slice{1, 10}}, - } - for i, tc := range testcases { - got := remRef(tc.ref, id.Zid(tc.zid)) - assertRefs(t, i, got, tc.exp) - } -} Index: box/manager/store/store.go ================================================================== --- box/manager/store/store.go +++ box/manager/store/store.go @@ -4,10 +4,13 @@ // 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. +// +// SPDX-License-Identifier: EUPL-1.2 +// SPDX-FileCopyrightText: 2021-present Detlef Stern //----------------------------------------------------------------------------- // Package store contains general index data for storing a zettel index. package store Index: box/manager/store/wordset.go ================================================================== --- box/manager/store/wordset.go +++ box/manager/store/wordset.go @@ -4,10 +4,13 @@ // 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. +// +// SPDX-License-Identifier: EUPL-1.2 +// SPDX-FileCopyrightText: 2021-present Detlef Stern //----------------------------------------------------------------------------- package store // WordSet contains the set of all words, with the count of their occurrences. Index: box/manager/store/wordset_test.go ================================================================== --- box/manager/store/wordset_test.go +++ box/manager/store/wordset_test.go @@ -4,10 +4,13 @@ // 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. +// +// SPDX-License-Identifier: EUPL-1.2 +// SPDX-FileCopyrightText: 2021-present Detlef Stern //----------------------------------------------------------------------------- package store_test import ( Index: box/manager/store/zettel.go ================================================================== --- box/manager/store/zettel.go +++ box/manager/store/zettel.go @@ -4,10 +4,13 @@ // 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. +// +// SPDX-License-Identifier: EUPL-1.2 +// SPDX-FileCopyrightText: 2021-present Detlef Stern //----------------------------------------------------------------------------- package store import ( Index: box/membox/membox.go ================================================================== --- box/membox/membox.go +++ box/membox/membox.go @@ -4,10 +4,13 @@ // 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. +// +// SPDX-License-Identifier: EUPL-1.2 +// SPDX-FileCopyrightText: 2020-present Detlef Stern //----------------------------------------------------------------------------- // Package membox stores zettel volatile in main memory. package membox Index: box/notify/directory.go ================================================================== --- box/notify/directory.go +++ box/notify/directory.go @@ -4,10 +4,13 @@ // 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. +// +// SPDX-License-Identifier: EUPL-1.2 +// SPDX-FileCopyrightText: 2020-present Detlef Stern //----------------------------------------------------------------------------- package notify import ( @@ -255,11 +258,11 @@ switch ev.Op { case Error: newEntries = nil if state != DsMissing { - ds.log.Warn().Err(ev.Err).Msg("Notifier confused") + ds.log.Error().Err(ev.Err).Msg("Notifier confused") } case Make: newEntries = make(entrySet) case List: if ev.Name == "" { @@ -296,11 +299,11 @@ ds.mx.Unlock() if zid != id.Invalid { ds.notifyChange(zid) } default: - ds.log.Warn().Str("event", fmt.Sprintf("%v", ev)).Msg("Unknown zettel notification event") + ds.log.Error().Str("event", fmt.Sprintf("%v", ev)).Msg("Unknown zettel notification event") } return newEntries, true } func getNewZids(entries entrySet) id.Slice { @@ -371,13 +374,13 @@ return id.Invalid } entry := fetchdirEntry(entries, zid) dupName1, dupName2 := ds.updateEntry(entry, name) if dupName1 != "" { - ds.log.Warn().Str("name", dupName1).Msg("Duplicate content (is ignored)") + ds.log.Info().Str("name", dupName1).Msg("Duplicate content (is ignored)") if dupName2 != "" { - ds.log.Warn().Str("name", dupName2).Msg("Duplicate content (is ignored)") + ds.log.Info().Str("name", dupName2).Msg("Duplicate content (is ignored)") } return id.Invalid } return zid } Index: box/notify/directory_test.go ================================================================== --- box/notify/directory_test.go +++ box/notify/directory_test.go @@ -4,10 +4,13 @@ // 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. +// +// SPDX-License-Identifier: EUPL-1.2 +// SPDX-FileCopyrightText: 2022-present Detlef Stern //----------------------------------------------------------------------------- package notify import ( Index: box/notify/entry.go ================================================================== --- box/notify/entry.go +++ box/notify/entry.go @@ -4,10 +4,13 @@ // 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. +// +// SPDX-License-Identifier: EUPL-1.2 +// SPDX-FileCopyrightText: 2020-present Detlef Stern //----------------------------------------------------------------------------- package notify import ( @@ -57,11 +60,11 @@ e.MetaName = e.calcBaseName(contentName) } return } - syntax := m.GetDefault(api.KeySyntax, "") + syntax := m.GetDefault(api.KeySyntax, meta.DefaultSyntax) ext := calcContentExt(syntax, m.YamlSep, getZettelFileSyntax) metaName := e.MetaName eimc := extIsMetaAndContent(ext) if eimc { if metaName != "" { Index: box/notify/fsdir.go ================================================================== --- box/notify/fsdir.go +++ box/notify/fsdir.go @@ -4,10 +4,13 @@ // 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. +// +// SPDX-License-Identifier: EUPL-1.2 +// SPDX-FileCopyrightText: 2021-present Detlef Stern //----------------------------------------------------------------------------- package notify import ( @@ -53,18 +56,17 @@ Str("path", absPath).Err(err). Msg("Unable to access Zettel directory and its parent directory") watcher.Close() return nil, err } - log.Warn(). - Str("parentDir", absParentDir).Err(errParent). + log.Info().Str("parentDir", absParentDir).Err(errParent). Msg("Parent of Zettel directory cannot be supervised") - log.Warn().Str("path", absPath). + log.Info().Str("path", absPath). Msg("Zettelstore might not detect a deletion or movement of the Zettel directory") } else if err != nil { // Not a problem, if container is not available. It might become available later. - log.Warn().Err(err).Str("path", absPath).Msg("Zettel directory not available") + log.Info().Err(err).Str("path", absPath).Msg("Zettel directory currently not available") } fsdn := &fsdirNotifier{ log: log, events: make(chan Event), Index: box/notify/helper.go ================================================================== --- box/notify/helper.go +++ box/notify/helper.go @@ -4,10 +4,13 @@ // 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. +// +// SPDX-License-Identifier: EUPL-1.2 +// SPDX-FileCopyrightText: 2021-present Detlef Stern //----------------------------------------------------------------------------- package notify import ( @@ -15,15 +18,10 @@ "os" "zettelstore.de/z/logger" ) -// MakeMetaFilename builds the name of the file containing metadata. -func MakeMetaFilename(basename string) string { - return basename //+ ".meta" -} - // EntryFetcher return a list of (file) names of an directory. type EntryFetcher interface { Fetch() ([]string, error) } Index: box/notify/notify.go ================================================================== --- box/notify/notify.go +++ box/notify/notify.go @@ -4,10 +4,13 @@ // 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. +// +// SPDX-License-Identifier: EUPL-1.2 +// SPDX-FileCopyrightText: 2021-present Detlef Stern //----------------------------------------------------------------------------- // Package notify provides some notification services to be used by box services. package notify Index: box/notify/simpledir.go ================================================================== --- box/notify/simpledir.go +++ box/notify/simpledir.go @@ -4,10 +4,13 @@ // 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. +// +// SPDX-License-Identifier: EUPL-1.2 +// SPDX-FileCopyrightText: 2021-present Detlef Stern //----------------------------------------------------------------------------- package notify import ( Index: cmd/cmd_file.go ================================================================== --- cmd/cmd_file.go +++ cmd/cmd_file.go @@ -4,10 +4,13 @@ // 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. +// +// SPDX-License-Identifier: EUPL-1.2 +// SPDX-FileCopyrightText: 2020-present Detlef Stern //----------------------------------------------------------------------------- package cmd import ( @@ -16,12 +19,12 @@ "fmt" "io" "os" "zettelstore.de/client.fossil/api" + "zettelstore.de/client.fossil/input" "zettelstore.de/z/encoder" - "zettelstore.de/z/input" "zettelstore.de/z/parser" "zettelstore.de/z/zettel" "zettelstore.de/z/zettel/id" "zettelstore.de/z/zettel/meta" ) @@ -38,11 +41,11 @@ context.Background(), zettel.Zettel{ Meta: m, Content: zettel.NewContent(inp.Src[inp.Pos:]), }, - m.GetDefault(api.KeySyntax, meta.SyntaxZmk), + m.GetDefault(api.KeySyntax, meta.DefaultSyntax), nil, ) encdr := encoder.Create(api.Encoder(enc), &encoder.CreateParameter{Lang: m.GetDefault(api.KeyLang, api.ValueLangEN)}) if encdr == nil { fmt.Fprintf(os.Stderr, "Unknown format %q\n", enc) Index: cmd/cmd_password.go ================================================================== --- cmd/cmd_password.go +++ cmd/cmd_password.go @@ -4,10 +4,13 @@ // 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. +// +// SPDX-License-Identifier: EUPL-1.2 +// SPDX-FileCopyrightText: 2020-present Detlef Stern //----------------------------------------------------------------------------- package cmd import ( Index: cmd/cmd_run.go ================================================================== --- cmd/cmd_run.go +++ cmd/cmd_run.go @@ -4,10 +4,13 @@ // 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. +// +// SPDX-License-Identifier: EUPL-1.2 +// SPDX-FileCopyrightText: 2020-present Detlef Stern //----------------------------------------------------------------------------- package cmd import ( Index: cmd/command.go ================================================================== --- cmd/command.go +++ cmd/command.go @@ -4,10 +4,13 @@ // 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. +// +// SPDX-License-Identifier: EUPL-1.2 +// SPDX-FileCopyrightText: 2020-present Detlef Stern //----------------------------------------------------------------------------- package cmd import ( Index: cmd/main.go ================================================================== --- cmd/main.go +++ cmd/main.go @@ -4,10 +4,13 @@ // 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. +// +// SPDX-License-Identifier: EUPL-1.2 +// SPDX-FileCopyrightText: 2020-present Detlef Stern //----------------------------------------------------------------------------- package cmd import ( @@ -21,17 +24,17 @@ "strconv" "strings" "time" "zettelstore.de/client.fossil/api" + "zettelstore.de/client.fossil/input" "zettelstore.de/z/auth" "zettelstore.de/z/auth/impl" "zettelstore.de/z/box" "zettelstore.de/z/box/compbox" "zettelstore.de/z/box/manager" "zettelstore.de/z/config" - "zettelstore.de/z/input" "zettelstore.de/z/kernel" "zettelstore.de/z/logger" "zettelstore.de/z/web/server" "zettelstore.de/z/zettel/id" "zettelstore.de/z/zettel/meta" @@ -232,11 +235,11 @@ func setConfigValue(err error, subsys kernel.Service, key string, val any) error { if err == nil { err = kernel.Main.SetConfig(subsys, key, fmt.Sprint(val)) if err != nil { - kernel.Main.GetKernelLogger().Fatal().Str("key", key).Str("value", fmt.Sprint(val)).Err(err).Msg("Unable to set configuration") + kernel.Main.GetKernelLogger().Error().Str("key", key).Str("value", fmt.Sprint(val)).Err(err).Msg("Unable to set configuration") } } return err } Index: cmd/register.go ================================================================== --- cmd/register.go +++ cmd/register.go @@ -4,10 +4,13 @@ // 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. +// +// SPDX-License-Identifier: EUPL-1.2 +// SPDX-FileCopyrightText: 2020-present Detlef Stern //----------------------------------------------------------------------------- // Package cmd provides command generic functions. package cmd Index: cmd/zettelstore/main.go ================================================================== --- cmd/zettelstore/main.go +++ cmd/zettelstore/main.go @@ -4,10 +4,13 @@ // 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. +// +// SPDX-License-Identifier: EUPL-1.2 +// SPDX-FileCopyrightText: 2020-present Detlef Stern //----------------------------------------------------------------------------- // Package main is the starting point for the zettelstore command. package main Index: collect/collect.go ================================================================== --- collect/collect.go +++ collect/collect.go @@ -4,10 +4,13 @@ // 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. +// +// SPDX-License-Identifier: EUPL-1.2 +// SPDX-FileCopyrightText: 2020-present Detlef Stern //----------------------------------------------------------------------------- // Package collect provides functions to collect items from a syntax tree. package collect Index: collect/collect_test.go ================================================================== --- collect/collect_test.go +++ collect/collect_test.go @@ -4,10 +4,13 @@ // 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. +// +// SPDX-License-Identifier: EUPL-1.2 +// SPDX-FileCopyrightText: 2020-present Detlef Stern //----------------------------------------------------------------------------- // Package collect_test provides some unit test for collectors. package collect_test Index: collect/order.go ================================================================== --- collect/order.go +++ collect/order.go @@ -4,10 +4,13 @@ // 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. +// +// SPDX-License-Identifier: EUPL-1.2 +// SPDX-FileCopyrightText: 2021-present Detlef Stern //----------------------------------------------------------------------------- // Package collect provides functions to collect items from a syntax tree. package collect DELETED collect/split.go Index: collect/split.go ================================================================== --- collect/split.go +++ /dev/null @@ -1,50 +0,0 @@ -//----------------------------------------------------------------------------- -// Copyright (c) 2020-present 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 collect provides functions to collect items from a syntax tree. -package collect - -import ( - "zettelstore.de/z/ast" - "zettelstore.de/z/strfun" -) - -// DivideReferences divides the given list of rederences into zettel, local, and external References. -func DivideReferences(all []*ast.Reference) (zettel, local, external []*ast.Reference) { - if len(all) == 0 { - return nil, nil, nil - } - - mapZettel := make(strfun.Set) - mapLocal := make(strfun.Set) - mapExternal := make(strfun.Set) - for _, ref := range all { - if ref.State == ast.RefStateSelf { - continue - } - if ref.IsZettel() { - zettel = appendRefToList(zettel, mapZettel, ref) - } else if ref.IsExternal() { - external = appendRefToList(external, mapExternal, ref) - } else { - local = appendRefToList(local, mapLocal, ref) - } - } - return zettel, local, external -} - -func appendRefToList(reflist []*ast.Reference, refSet strfun.Set, ref *ast.Reference) []*ast.Reference { - s := ref.String() - if !refSet.Has(s) { - reflist = append(reflist, ref) - refSet.Set(s) - } - return reflist -} Index: config/config.go ================================================================== --- config/config.go +++ config/config.go @@ -4,10 +4,13 @@ // 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. +// +// SPDX-License-Identifier: EUPL-1.2 +// SPDX-FileCopyrightText: 2020-present Detlef Stern //----------------------------------------------------------------------------- // Package config provides functions to retrieve runtime configuration data. package config Index: docs/development/00010000000000.zettel ================================================================== --- docs/development/00010000000000.zettel +++ docs/development/00010000000000.zettel @@ -1,10 +1,11 @@ id: 00010000000000 title: Developments Notes role: zettel syntax: zmk created: 00010101000000 -modified: 20221026184905 +modified: 20231218182020 * [[Required Software|20210916193200]] * [[Fuzzing tests|20221026184300]] * [[Checklist for Release|20210916194900]] +* [[Development tools|20231218181900]] Index: docs/development/20210916193200.zettel ================================================================== --- docs/development/20210916193200.zettel +++ docs/development/20210916193200.zettel @@ -1,11 +1,11 @@ id: 20210916193200 title: Required Software role: zettel syntax: zmk created: 20210916193200 -modified: 20230405150541 +modified: 20231213194509 The following software must be installed: * A current, supported [[release of Go|https://go.dev/doc/devel/release]], * [[Fossil|https://fossil-scm.org/]], @@ -16,13 +16,13 @@ export PATH=$PATH:/usr/local/go/bin export PATH=$PATH:$(go env GOPATH)/bin ``` The internal build tool need the following software. -It can be installed / updated via the build tool itself: ``go run tools/build.go tools``. +It can be installed / updated via the build tool itself: ``go run tools/devtools/devtools.go``. Otherwise you can install the software by hand: * [[shadow|https://pkg.go.dev/golang.org/x/tools/go/analysis/passes/shadow]] via ``go install golang.org/x/tools/go/analysis/passes/shadow/cmd/shadow@latest``, * [[staticcheck|https://staticcheck.io/]] via ``go install honnef.co/go/tools/cmd/staticcheck@latest``, * [[unparam|https://mvdan.cc/unparam]][^[[GitHub|https://github.com/mvdan/unparam]]] via ``go install mvdan.cc/unparam@latest``, * [[govulncheck|https://golang.org/x/vuln/cmd/govulncheck]] via ``go install golang.org/x/vuln/cmd/govulncheck@latest``, Index: docs/development/20210916194900.zettel ================================================================== --- docs/development/20210916194900.zettel +++ docs/development/20210916194900.zettel @@ -1,24 +1,24 @@ id: 20210916194900 title: Checklist for Release role: zettel syntax: zmk created: 20210916194900 -modified: 20230402181229 +modified: 20231213194631 # Sync with the official repository #* ``fossil sync -u`` # Make sure that there is no workspace defined. #* ``ls ..`` must not have a file ''go.work'', in no parent folder. # Make sure that all dependencies are up-to-date. #* ``cat go.mod`` # Clean up your Go workspace: -#* ``go run tools/build.go clean`` (alternatively: ``make clean``). +#* ``go run tools/clean/clean.go`` (alternatively: ``make clean``). # All internal tests must succeed: -#* ``go run tools/build.go relcheck`` (alternatively: ``make relcheck``). +#* ``go run tools/check/check.go -r`` (alternatively: ``make relcheck``). # The API tests must succeed on every development platform: -#* ``go run tools/build.go testapi`` (alternatively: ``make api``). +#* ``go run tools/testapi/testapi.go`` (alternatively: ``make api``). # Run [[linkchecker|https://linkchecker.github.io/linkchecker/]] with the manual: #* ``go run -race cmd/zettelstore/main.go run -d docs/manual`` #* ``linkchecker http://127.0.0.1:23123 2>&1 | tee lc.txt`` #* Check all ""Error: 404 Not Found"" #* Check all ""Error: 403 Forbidden"": allowed for endpoint ''/p'' with encoding ''html'' for those zettel that are accessible only in ''expert-mode''. @@ -42,13 +42,13 @@ # Commit the new release version: #* ``fossil commit --tag release --tag vVERSION -m "Version VERSION"`` #* **Important:** the tag must follow the given pattern, e.g. ''v0.0.15''. Otherwise client will not be able to import ''zettelkasten.de/z''. # Clean up your Go workspace: -#* ``go run tools/build.go clean`` (alternatively: ``make clean``). +#* ``go run tools/clean/clean.go`` (alternatively: ``make clean``). # Create the release: -#* ``go run tools/build.go release`` (alternatively: ``make release``). +#* ``go run tools/build/build.go release`` (alternatively: ``make release``). # Remove previous executables: #* ``fossil uv remove --glob '*-PREVVERSION*'`` # Add executables for release: #* ``cd releases`` #* ``fossil uv add *.zip`` ADDED docs/development/20231218181900.zettel Index: docs/development/20231218181900.zettel ================================================================== --- /dev/null +++ docs/development/20231218181900.zettel @@ -0,0 +1,117 @@ +id: 20231218181900 +title: Development tools +role: zettel +syntax: zmk +created: 20231218181956 +modified: 20231218184500 + +The source code contains some tools to assist the development of Zettelstore. +These are located in the ''tools'' directory. + +Most tool support the generic option ``-v``, which log internal activities. + +Some of the tools can be called easier by using ``make``, that reads in a provided ''Makefile''. + +=== Check +The ""check"" tool automates some testing activities. +It is called via the command line: +``` +# go run tools/check/check.go +``` +There is an additional option ``-r`` to check in advance of a release. + +The following checks are executed: +* Execution of unit tests, like ``go test ./...`` +* Analyze the source code for general problems, as in ``go vet ./...`` +* Tries to find shadowed variable, via ``shadow ./...`` +* Performs some additional checks on the source code, via ``staticcheck ./...`` +* Checks the usage of function parameters and usage of return values, via ``unparam ./...``. + In case the option ''-r'' is set, the check includes exported functions and internal tests. +* In case option ''-r'' is set, the source code is checked against the vulnerability database, via ``govulncheck ./...`` + +Please note, that most of the tools above are not automatically installed in a standard Go distribution. +Use the command ""devtools"" to install them. + +=== Devtools +The following command installs all needed tools: +``` +# go run tooles/devtools/devtools.go +``` +It will also automatically update these tools. + +=== TestAPI +The following command will perform some high-level tests: +```sh +# go run tools/testapi/testapi.go +``` +Basically, a Zettelstore will be started and then API calls will be made to simulate some typical activities with the Zettelstore. + +If a Zettelstore is already running on port 23123, this Zettelstore will be used instead. +Even if the API test should clean up later, some zettel might stay created if a test fails. +This feature is used, if you want to have more control on the running Zettelstore. +You should start it with the following command: +```sh +# go run -race cmd/zettelstore/main.go run -c testdata/testbox/19700101000000.zettel +``` +This allows you to debug failing API tests. + +=== HTMLlint +The following command will check the generated HTML code for validity: +```sh +# go run tools/htmllint/htmllint.go +``` +In addition, you might specify the URL od a running Zettelstore. +Otherwise ''http://localhost:23123'' is used. + +This command fetches first the list of all zettel. +This list is used to check the generated HTML code (''ZID'' is the paceholder for the zettel identification): + +* Check all zettel HTML encodings, via the path ''/z/ZID?enc=html&part=zettel'' +* Check all zettel web views, via the path ''/h/ZID'' +* The info page of all zettel is checked, via path ''/i/ZID'' +* A subset of max. 100 zettel will be checked for the validity of their edit page, via ''/e/ZID'' +* 10 random zettel are checked for a valid create form, via ''/c/ZID'' +* The zettel rename form will be checked for 100 zettel, via ''/b/ZID'' +* A maximum of 200 random zettel are checked for a valid delete dialog, via ''/d/ZID'' + +Depending on the selected Zettelstore, the command might take a long time. + +You can shorten the time, if you disable any zettel query in the footer. + +=== Build +The ""build"" tool allows to build the software, either for tests or for a release. + +The following command will create a Zettelstore executable for the architecture of the current computer: +```sh +# go tools/build/build.go build +``` +You will find the executable in the ''bin'' directory. + +A full release will be build in the directory ''releases'', containing ZIP files for the computer architectures ""Linux/amd64"", ""Linux/arm"", ""MacOS/arm64"", ""MacOS/amd64"", and ""Windows/amd64"". +In addition, the manual is also build as a ZIP file: +```sh +# go run tools/build/build.go release +``` + +If you just want the ZIP file with the manual, please use: +```sh +# go run tools/build/build.go manual +``` + +In case you want to check the version of the Zettelstore to be build, use: +```sh +# go run tools/build/build.go version +``` + +=== Clean +To remove the directories ''bin'' and ''releases'', as well as all cached Go libraries used by Zettelstore, execute: +```sh +# go run tools/clean/clean.go +``` + +Internally, the following commands are executed +```sh +# rm -rf bin releases +# go clean ./... +# go clean -cache -modcache -testcache +``` Index: docs/manual/00001004010000.zettel ================================================================== --- docs/manual/00001004010000.zettel +++ docs/manual/00001004010000.zettel @@ -2,11 +2,11 @@ title: Zettelstore startup configuration role: manual tags: #configuration #manual #zettelstore syntax: zmk created: 20210126175322 -modified: 20221128155143 +modified: 20240220190138 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. @@ -90,13 +90,13 @@ Several specifications are separated by the semicolon character (""'';''"", U+003B). Each specification consists of an optional service name, together with the colon character (""'':''"", U+003A), followed by the logging level. Default: ""info"". - Examples: ""sense"" will produce sensing messages (e.g. a little more than ""info""); ""sense;web:debug"" will emit debugging messages for the web component of Zettelstore while still producing sensing messages for all other components. + Examples: ""error"" will produce just error messages (e.g. no ""info"" messages); ""error;web:debug"" will emit debugging messages for the web component of Zettelstore while still producing error messages for all other components. - When you are familiar to operate the Zettelstore, you might set the level to ""warn"" or ""error"" to receive less noisy messages from the Zettelstore. + When you are familiar to operate the Zettelstore, you might set the level to ""error"" to receive less noisy messages from the Zettelstore. ; [!max-request-size|''max-request-size''] : Limits the maximum byte size of a web request body to prevent clients from accidentally or maliciously sending a large request and wasting server resources. The minimum value is 1024. Default: 16777216 (16 MiB). Index: docs/manual/00001004059700.zettel ================================================================== --- docs/manual/00001004059700.zettel +++ docs/manual/00001004059700.zettel @@ -1,27 +1,24 @@ id: 00001004059700 title: List of supported logging levels role: manual tags: #configuration #manual #zettelstore syntax: zmk -modified: 20220113183606 +created: 20211204182643 +modified: 20240221134619 Zettelstore supports various levels of logging output. This allows you to see the inner workings of Zettelstore, or to avoid it. Each level has an associated name and number. A lower number signals more logging output. -|= Name | Number >| Description +|= Name | Number :| Description | Trace | 1 | Show most of the inner workings | Debug | 2 | Show many internal values that might be interesting for a [[Zettelstore developer|00000000000005]]. -| Sense | 3 | Display sensing events, which are not essential information. -| Info | 4 | Display information about an event. In most cases, there is no required action expected from you. -| Warn | 5 | Show a warning, i.e. an event that might become an error or more. Mostly invalid data. -| Error | 6 | Notify about an error, which was handled automatically. Something is broken. User intervention is not required, in most cases. Monitor the application. -| Fatal | 7 | Notify about a significant error that cannot be handled automatically. At least some important functionality is disabled. -| Panic | 8 | The application is in an uncertain state and notifies you about its panic. At least some part of the application is possibly restarted. -| Mandatory | 9 | Important message will be shown, e.g. the Zettelstore version at startup time. -| Disabled | 10 | No messages will be shown +| Info | 3 | Display information about an event. In most cases, there is no required action expected from you. +| Error | 4 | Notify about an error, which was handled automatically. Something is broken. User intervention may be required, some important functionality may be disabled. Monitor the application. +| Mandatory | 5 | Important message will be shown, e.g. the Zettelstore version at startup time. +| Disabled | 6 | No messages will be shown If you set the logging level to a certain value, only messages with the same or higher numerical value will be shown. -E.g. if you set the logging level to ""warn"", no ""trace"", ""debug"", ""sense", and ""info"" messages are shown, but ""warn"", ""error"", ""fatal"", ""panic"", and ""mandatory"" messages. +E.g. if you set the logging level to ""error"", no ""trace"", ""debug"", and ""info"" messages are shown, but ""error"" and ""mandatory"" messages. Index: docs/manual/00001006010000.zettel ================================================================== --- docs/manual/00001006010000.zettel +++ docs/manual/00001006010000.zettel @@ -1,24 +1,26 @@ id: 00001006010000 title: Syntax of Metadata role: manual tags: #manual #syntax #zettelstore syntax: zmk -modified: 20220218131923 +created: 20210126175322 +modified: 20240219193158 The metadata of a zettel is a collection of key-value pairs. The syntax roughly resembles the internal header of an email ([[RFC5322|https://tools.ietf.org/html/rfc5322]]). The key is a sequence of alphanumeric characters, a hyphen-minus character (""''-''"", U+002D) is also allowed. It begins at the first position of a new line. +Uppercase letters of a key are translated to their lowercase equivalence. A key is separated from its value either by * a colon character (""'':''""), * a non-empty sequence of space characters, * a sequence of space characters, followed by a colon, followed by a sequence of space characters. -A Value is a sequence of printable characters. +A value is a sequence of printable characters. If the value should be continued in the following line, that following line (""continuation line"") must begin with a non-empty sequence of space characters. The rest of the following line will be interpreted as the next part of the value. There can be more than one continuation line for a value. A non-continuation line that contains a possibly empty sequence of characters, followed by the percent sign character (""''%''"") is treated as a comment line. Index: docs/manual/00001006030000.zettel ================================================================== --- docs/manual/00001006030000.zettel +++ docs/manual/00001006030000.zettel @@ -2,21 +2,20 @@ title: Supported Key Types role: manual tags: #manual #meta #reference #zettel #zettelstore syntax: zmk created: 20210126175322 -modified: 20230612183742 +modified: 20240219161909 All [[supported metadata keys|00001006020000]] conform to a type. User-defined metadata keys conform also to a type, based on the suffix of the key. |=Suffix|Type | ''-date'' | [[Timestamp|00001006034500]] | ''-number'' | [[Number|00001006033000]] | ''-role'' | [[Word|00001006035500]] -| ''-set'' | [[WordSet|00001006036000]] | ''-time'' | [[Timestamp|00001006034500]] | ''-title'' | [[Zettelmarkup|00001006036500]] | ''-url'' | [[URL|00001006035000]] | ''-zettel'' | [[Identifier|00001006032000]] | ''-zid'' | [[Identifier|00001006032000]] @@ -37,7 +36,6 @@ * [[String|00001006033500]] * [[TagSet|00001006034000]] * [[Timestamp|00001006034500]] * [[URL|00001006035000]] * [[Word|00001006035500]] -* [[WordSet|00001006036000]] * [[Zettelmarkup|00001006036500]] DELETED docs/manual/00001006036000.zettel Index: docs/manual/00001006036000.zettel ================================================================== --- docs/manual/00001006036000.zettel +++ /dev/null @@ -1,20 +0,0 @@ -id: 00001006036000 -title: WordSet Key Type -role: manual -tags: #manual #meta #reference #zettel #zettelstore -syntax: zmk -created: 20210212135017 -modified: 20230419175745 - -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. - -=== Query comparison -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/00001007031140.zettel ================================================================== --- docs/manual/00001007031140.zettel +++ docs/manual/00001007031140.zettel @@ -2,11 +2,11 @@ title: Zettelmarkup: Query Transclusion role: manual tags: #manual #search #zettelmarkup #zettelstore syntax: zmk created: 20220809132350 -modified: 20231023163751 +modified: 20240219161800 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]]. @@ -47,20 +47,23 @@ ; ''RSS'' (aggregate) : Transform the zettel list into a [[RSS 2.0|https://www.rssboard.org/rss-specification]]-conformant document / feed. The document is embedded into the referencing zettel. ; ''KEYS'' (aggregate) : Emit a list of all metadata keys, together with the number of zettel having the key. -; ''REINDEX'' (aggregate) +; ''REDIRECT'', ''REINDEX'' (aggregate) : Will be ignored. - This action may have been copied from an existing [[API query call|00001012051400]] (or from a WebUI query), but is here superfluous (and possibly harmful). -; Any [[metadata key|00001006020000]] of type [[Word|00001006035500]], [[WordSet|00001006036000]], or [[TagSet|00001006034000]] (aggregates) + These actions may have been copied from an existing [[API query call|00001012051400]] (or from a WebUI query), but are here superfluous (and possibly harmful). +; Any [[metadata key|00001006020000]] of type [[Word|00001006035500]] or of type [[TagSet|00001006034000]] (aggregates) : Emit an aggregate of the given metadata key. The key can be given in any letter case[^Except if the key name collides with one of the above names. In this case use at least one lower case letter.]. + +To allow some kind of backward compatibility, an action written in uppercase letters that leads to an empty result list, will be ignored. +In this case the list of selected zettel is returned. Example: ```zmk {{{query:tags:#search | tags}}} ``` This is a tag cloud of all tags that are used together with the tag #search: :::example {{{query:tags:#search | tags}}} ::: Index: docs/manual/00001007040324.zettel ================================================================== --- docs/manual/00001007040324.zettel +++ docs/manual/00001007040324.zettel @@ -2,11 +2,11 @@ title: Zettelmarkup: Inline-mode Transclusion role: manual tags: #manual #zettelmarkup #zettelstore syntax: zmk created: 20210811154251 -modified: 20221116165428 +modified: 20231222164501 Inline-mode transclusion applies to all zettel that are parsed in a non-trivial way, e.g. as structured textual content. For example, textual content is assumed if the [[syntax|00001006020000#syntax]] of a zettel is ""zmk"" ([[Zettelmarkup|00001007000000]]), or ""markdown"" / ""md"" ([[Markdown|00001008010000]]). Since this type of transclusion is at the level of [[inline-structured elements|00001007040000]], the transclude specification must be replaced with some inline-structured elements. @@ -36,14 +36,14 @@ ** Just specifying the fragment identifier will reference something in the current page. This is not allowed, to prevent a possible endless recursion. * If the reference is a [[hosted or based|00001007040310#link-specifications]] link / URL to an image, that image will be rendered. - Example: ``{{//z/00000000040001}}`` is rendered as ::{{//z/00000000040001}}::{=example} + Example: ``{{//z/00000000040001}}{alt=Emoji}`` is rendered as ::{{//z/00000000040001}}{alt=Emoji}::{=example} If no inline-structured elements are found, the transclude specification is replaced by an error message. To avoid an exploding ""transclusion bomb"", a form of a [[billion laughs attack|https://en.wikipedia.org/wiki/Billion_laughs_attack]] (also known as ""XML bomb""), the total number of transclusions / expansions is limited. The limit can be controlled by setting the value [[''max-transclusions''|00001004020000#max-transclusions]] of the runtime configuration zettel. === See also [[Full transclusion|00001007031100]] does not work inside some text, but is used for [[block-structured elements|00001007030000]]. Index: docs/manual/00001007720300.zettel ================================================================== --- docs/manual/00001007720300.zettel +++ docs/manual/00001007720300.zettel @@ -2,36 +2,40 @@ title: Query: Context Directive role: manual tags: #manual #search #zettelstore syntax: zmk created: 20230707204706 -modified: 20230724153832 +modified: 20240209191045 A context directive calculates the __context__ of a list of zettel identifier. It starts with the keyword ''CONTEXT''. Optionally you may specify some context details, after the keyword ''CONTEXT'', separated by space characters. These are: +* ''FULL'': additionally search for zettel with the same tags, * ''BACKWARD'': search for context only though backward links, * ''FORWARD'': search for context only through forward links, -* ''COST'', one or more space characters, and a positive integer: set the maximum __cost__ (default: 17), -* ''MAX'', one or more space characters, and a positive integer: set the maximum number of context zettel (default: 200). +* ''COST'': one or more space characters, and a positive integer: set the maximum __cost__ (default: 17), +* ''MAX'': one or more space characters, and a positive integer: set the maximum number of context zettel (default: 200). If no ''BACKWARD'' and ''FORWARD'' is specified, a search for context zettel will be done though backward and forward links. The cost of a context zettel is calculated iteratively: * Each of the specified zettel hast a cost of one. * A zettel found as a single folge zettel or single precursor zettel has the cost of the originating zettel, plus one. -* A zettel found as a single successor zettel or single predecessor zettel has the cost of the originating zettel, plus two. -* A zettel found via another link without being part of a [[set of zettel identifier|00001006032500]], has the cost of the originating zettel, plus three. -* A zettel which is part of a set of zettel identifier, has the cost of the originating zettel, plus one of the three choices above and multiplied with roughly a logarithmic value based on the size of the set. -* A zettel with the same tag, has the cost of the originating zettel, plus the number of zettel with the same tag (if it is less than eight), or the cost of the originating zettel plus two, multiplied by number of zettel with the same tag divided by four. +* A zettel found as a single subordinate zettel or single superior zettel has the cost of the originating zettel, plus 1.2. +* A zettel found as a single successor zettel or single predecessor zettel has the cost of the originating zettel, plus seven. +* A zettel found via another link without being part of a [[set of zettel identifier|00001006032500]], has the cost of the originating zettel, plus two. +* A zettel which is part of a set of zettel identifier, has the cost of the originating zettel, plus one of the four choices above and multiplied with roughly a linear-logarithmic value based on the size of the set. +* A zettel with the same tag, has the cost of the originating zettel, plus a linear-logarithmic number based on the number of zettel with this tag. + If a zettel belongs to more than one tag compared with the current zettel, there is a discount of 90% per additional tag. + This only applies if the ''FULL'' directive was specified. The maximum cost is only checked for all zettel that are not directly reachable from the initial, specified list of zettel. This ensures that initial zettel that have only a highly used tag, will also produce some context zettel. Despite its possibly complicated structure, this algorithm ensures in practice that the zettel context is a list of zettel, where the first elements are ""near"" to the specified zettel and the last elements are more ""distant"" to the specified zettel. It also penalties zettel that acts as a ""hub"" to other zettel, to make it more likely that only relevant zettel appear on the context list. This directive may be specified only once as a query directive. A second occurence of ''CONTEXT'' is interpreted as a [[search expression|00001007701000]]. -In most cases it is easier to adjust the maximum cost than to perform another context search, which is relatively expensive in terms of retrieving power. +In most cases it is easier to adjust the maximum cost than to perform another context search, which is relatively expensive in terms of retrieving effort. Index: docs/manual/00001007770000.zettel ================================================================== --- docs/manual/00001007770000.zettel +++ docs/manual/00001007770000.zettel @@ -2,11 +2,11 @@ title: Query: Action List role: manual tags: #manual #search #zettelstore syntax: zmk created: 20230707205246 -modified: 20230707205532 +modified: 20240219161813 With a [[list of zettel identifier|00001007710000]], a [[query directives|00001007720000]], or a [[search expression|00001007701000]], a list of zettel is selected. __Actions__ allow to modify this list to a certain degree. Which actions are allowed depends on the context. @@ -13,6 +13,9 @@ 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]]. +In most contexts, valid actions include the name of metadata keys, at least of type [[Word|00001006035500]] or [[TagSet|00001006034000]]. + +To allow some kind of backward compatibility, an action written in uppercase letters that leads to an empty result list, will be ignored. +In this case the list of selected zettel is returned. Index: docs/manual/00001007780000.zettel ================================================================== --- docs/manual/00001007780000.zettel +++ docs/manual/00001007780000.zettel @@ -2,11 +2,11 @@ title: Formal syntax of query expressions role: manual tags: #manual #reference #search #zettelstore syntax: zmk created: 20220810144539 -modified: 20230731160413 +modified: 20240219155949 ``` QueryExpression := ZettelList? QueryDirective* SearchExpression ActionExpression? ZettelList := (ZID (SPACE+ ZID)*). ZID := '0'+ ('1' .. '9'') DIGIT* @@ -14,11 +14,12 @@ QueryDirective := ContextDirective | IdentDirective | ItemsDirective | UnlinkedDirective. ContextDirective := "CONTEXT" (SPACE+ ContextDetail)*. -ContextDetail := "BACKWARD" +ContextDetail := "FULL" + | "BACKWARD" | "FORWARD" | "COST" SPACE+ PosInt | "MAX" SPACE+ PosInt. IdentDirective := IDENT. ItemsDirective := ITEMS. @@ -40,7 +41,17 @@ ExistOperator := '?' | '!' '?'. PosInt := '0' | ('1' .. '9') DIGIT*. ActionExpression := '|' (Word (SPACE+ Word)*)? +Action := Word + | 'ATOM' + | 'KEYS' + | 'N' NO-SPACE* + | 'MAX' PosInt + | 'MIN' PosInt + | 'REDIRECT' + | 'REINDEX' + | 'RSS' + | 'TITLE' (SPACE Word)* . Word := NO-SPACE NO-SPACE* ``` Index: docs/manual/00001007790000.zettel ================================================================== --- docs/manual/00001007790000.zettel +++ docs/manual/00001007790000.zettel @@ -2,11 +2,11 @@ title: Useful query expressions role: manual tags: #example #manual #search #zettelstore syntax: zmk created: 20220810144539 -modified: 20230706155134 +modified: 20240216003702 |= 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 @@ -14,5 +14,6 @@ | [[query:dead?]] | Zettel with invalid / dead links | [[query:backward!? precursor!?]] | Zettel that are not referenced by other zettel | [[query:tags!?]] | Zettel without tags | [[query:expire? ORDER expire]] | Zettel with an expire date, ordered from the nearest to the latest | [[query:00001007700000 CONTEXT]] | Zettel within the context of the [[given zettel|00001007700000]] +| [[query:PICK 1 | REDIRECT]] | Redirect to a random zettel Index: docs/manual/00001007903000.zettel ================================================================== --- docs/manual/00001007903000.zettel +++ docs/manual/00001007903000.zettel @@ -2,11 +2,11 @@ title: Zettelmarkup: First Steps role: manual tags: #manual #tutorial #zettelmarkup #zettelstore syntax: zmk created: 20220810182917 -modified: 20220926183359 +modified: 20231201135849 [[Zettelmarkup|00001007000000]] allows you to leave your text as it is, at least in many situations. Some characters have a special meaning, but you have to enter them is a defined way to see a visible change. Zettelmarkup is designed to be used for zettel, which are relatively short. It allows to produce longer texts, but you should probably use a different tool, if you want to produce an scientific paper, to name an example. @@ -29,11 +29,11 @@ | ''He says: ""I love you!""'' | Her says: ""I love you!"" | Put two quotation mark characters before and after the text you want to quote. You probably see a principle. One nice thing about the quotation mark characters: they are rendered according to the current language. -Examples: ""english""{lang=en}, ""french""{lang=fr}, ""german""{lang=de}, ""finnish""{lang=fi}. +Examples: ""english""{lang=en}, ""french""{lang=fr}, ""german""{lang=de}. You will see later, how to change the current language. === Lists Quite often, text consists of lists. Zettelmarkup supports different types of lists. Index: docs/manual/00001007990000.zettel ================================================================== --- docs/manual/00001007990000.zettel +++ docs/manual/00001007990000.zettel @@ -2,19 +2,19 @@ title: Zettelmarkup: Cheat Sheet role: manual tags: #manual #reference #zettelmarkup syntax: zmk created: 20221209191905 -modified: 20221209193310 +modified: 20231201140000 === Overview This Zettelmarkup cheat sheet provides a quick overview of many Zettelmarkup elements. It can not cover any special case. If you need more information about any of these elements, please refer to the detailed description. === Basic Syntax -|[[Text formatting|00001007040100]]|''__italic text__'' → __italic text__, ''**bold text**'' → **bold text**, ''""quoted text""'' → ""quoted text"" +|[[Text formatting|00001007040100]]|''__italic text__'' → __italic text__, ''**bold text**'' → **bold text**, ''""quoted text""'' → ""quoted text"", ''##marked text##'' → ##marked text## |[[Text editing|00001007040100]]|''>>inserted text>>'' → >>inserted text>>, ''~~deleted text~~'' → ~~deleted text~~ |[[Text literal formatting|00001007040200]]|''\'\'entered text\'\''' → ''entered text'', ''``source code``'' → ``source code``, ''==text output=='' → ==text output== |[[Superscript, subscript|00001007040100]]|''m^^2^^'' → m^^2^^, ''H,,2,,O'' → H,,2,,O |[[Links to other zettel|00001007040310]]|''[[Link text|00001007990000]]'' → [[Link text|00001007990000]] |[[Links to external resources|00001007040310]]|''[[Zettelstore|https://zettelstore.de]]'' → [[Zettelstore|https://zettelstore.de]] Index: docs/manual/00001012051400.zettel ================================================================== --- docs/manual/00001012051400.zettel +++ docs/manual/00001012051400.zettel @@ -2,11 +2,11 @@ title: API: Query the list of all zettel role: manual tags: #api #manual #zettelstore syntax: zmk created: 20220912111111 -modified: 20231023162927 +modified: 20240219161831 precursor: 00001012051200 The [[endpoint|00001012920000]] ''/z'' also allows you to filter the list of all zettel[^If [[authentication is enabled|00001010040100]], you must include the a valid [[access token|00001012050200]] in the ''Authorization'' header] and optionally to provide some actions. A [[query|00001007700000]] is an optional [[search expression|00001007700000#search-expression]], together with an optional [[list of actions|00001007700000#action-list]] (described below). @@ -107,26 +107,34 @@ : 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. +; ''REDIRECT'' (aggregate) +: Performs a HTTP redirect to the first selected zettel, using HTTP status code 302. + The zettel identifier is in the body. ; ''REINDEX'' (aggregate) : Updates the internal search index for the selected zettel, roughly similar to the [[refresh|00001012080500]] API call. It is not really an aggregate, since it is used only for its side effect. It is allowed to specify another aggregate. -; Any [[metadata key|00001006020000]] of type [[Word|00001006035500]], [[WordSet|00001006036000]], or [[TagSet|00001006034000]] (aggregates) +; Any [[metadata key|00001006020000]] of type [[Word|00001006035500]] 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. +First, ''REINDEX'' actions are executed, then ''REDIRECT''. +If no ''REDIRECT'' was found the first other aggregate action will be executed. +To allow some kind of backward compatibility, an action written in uppercase letters that leads to an empty result list, will be ignored. +In this case the list of selected zettel is returned. === HTTP Status codes ; ''200'' : Query was successful. ; ''204'' : Query was successful, but results in no content. Most likely, you specified no appropriate aggregator. +; ''302'' +: Query was successful, redirect to first zettel in list. ; ''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. Index: docs/manual/00001012931000.zettel ================================================================== --- docs/manual/00001012931000.zettel +++ docs/manual/00001012931000.zettel @@ -2,11 +2,11 @@ title: Encoding of Sz role: manual tags: #api #manual #reference #zettelstore syntax: zmk created: 20230403153903 -modified: 20230405133249 +modified: 20240123120319 Zettel in a [[Sz encoding|00001012920516]] are represented as a [[symbolic expression|00001012930000]]. To process these symbolic expressions, you need to know, how a specific part of a zettel is represented by a symbolic expression. Basically, each part of a zettel is represented as a list, often a nested list. @@ -27,11 +27,11 @@ Following elements represent each metadatum[^""Metadatum"" is used as the singular form of metadata.] of a zettel in standard order. Standard order is: [[Title|00001006020000#title]], [[Role|00001006020000#role]], [[Tags|00001006020000#tags]], [[Syntax|00001006020000#syntax]], all other [[keys|00001006020000]] in alphabetic order. :::syntax -__Metadata__ **=** ''(META'' [[__Metadatum__|00001012931200]] __Metadatum__ … __Metadatum__ '')''. +__Metadata__ **=** ''(META'' [[__Metadatum__|00001012931200]] … '')''. ::: === Content Zettel content is represented by a block. :::syntax @@ -41,28 +41,27 @@ ==== Block A block is represented by a list with the symbol ''BLOCK'' as the first element. All following elements represent a nested [[block-structured element|00001007030000]]. :::syntax -[!block|__Block__] **=** ''(BLOCK'' [[__BlockElement__|00001012931400]] __BlockElement__ … __BlockElement__ '')''. +[!block|__Block__] **=** ''(BLOCK'' [[__BlockElement__|00001012931400]] … '')''. ::: ==== Inline Both block-structured elements and some metadata values may contain [[inline-structured elements|00001007040000]]. Similar, inline-structured elements are represented as follows: :::syntax -__Inline__ **=** ''(INLINE'' [[__InlineElement__|00001012931600]] __InlineElement__ … __InlineElement__ '')''. +__Inline__ **=** ''(INLINE'' [[__InlineElement__|00001012931600]] … '')''. ::: ==== Attribute [[Attributes|00001007050000]] may be specified for both block- and inline- structured elements. Attributes are represented by the following schema. -Please note, the the symbol ''quote'' is lower-case by intention. :::syntax -__Attribute__ **=** ''('' **[** ''quote'' ''('' [[__AttributeKeyValue__|00001012931800]] __AttributeKeyValue__ … __AttributeKeyValue__ '')'' **]** ')'. +__Attribute__ **=** ''('' **[** [[__AttributeKeyValue__|00001012931800]] … **]** ')'. ::: Either, there are no attributes. These are specified by the empty list ''()''. Or there are attributes. Index: docs/manual/00001012931200.zettel ================================================================== --- docs/manual/00001012931200.zettel +++ docs/manual/00001012931200.zettel @@ -2,40 +2,34 @@ title: Encoding of Sz Metadata role: manual tags: #api #manual #reference #zettelstore syntax: zmk created: 20230403161618 -modified: 20230405121932 +modified: 20240219161848 A single metadata (""metadatum"") is represented by a triple: a symbol representing the type, a symbol representing the key, and either a string or a list that represent the value. -The key symbol must be ""quoted"", i.e. for the key ""title"": ''(quote title)''. -This property may be relaxed in future versions of the Zettelstore. - The symbol depends on the [[metadata key type|00001006030000]]. The value also depends somehow on the key type: a set of values is represented as a list, all other values are represented by a string, even if it is a number. The following table maps key types to symbols and to the type of the value representation. |=Key Type<| Symbol<| Value< | [[Credential|00001006031000]] | ''CREDENTIAL'' | string | [[EString|00001006031500]] | ''EMPTY-STRING'' | string | [[Identifier|00001006032000]] | ''ZID'' | string -| [[IdentifierSet|00001006032500]] | ''ZID-SET'' | list +| [[IdentifierSet|00001006032500]] | ''ZID-SET'' | ListValue | [[Number|00001006033000]] | ''NUMBER'' | string | [[String|00001006033500]] | ''STRING'' | string -| [[TagSet|00001006034000]] | ''TAG-SET'' | list +| [[TagSet|00001006034000]] | ''TAG-SET'' | ListValue | [[Timestamp|00001006034500]] | ''TIMESTAMP'' | string | [[URL|00001006035000]] | ''URL'' | string | [[Word|00001006035500]] | ''WORD'' | string -| [[WordSet|00001006036000]] | ''WORD-SET'' | list | [[Zettelmarkup|00001006036500]] | ''ZETTELMARKUP'' | string -If the value is represented as a list, its first element is the symbol ''list'', and all other elements are strings with the appropriate values. - :::syntax -__ListValue__ **=** ''(list'' String,,1,, String,,2,, … String,,n,, '')''. +__ListValue__ **=** ''('' String,,1,, String,,2,, … String,,n,, '')''. ::: Examples: -* The title of this zettel is represented as: ''(EMPTY-STRING (quote title) "Encoding of Sz Metadata")'' -* The tags of this zettel are represented as: ''(TAG-SET (quote tags) (list "#api" "#manual" "#reference" "#zettelstore"))'' +* The title of this zettel is represented as: ''(EMPTY-STRING title "Encoding of Sz Metadata")'' +* The tags of this zettel are represented as: ''(TAG-SET tags ("#api" "#manual" "#reference" "#zettelstore"))'' Index: docs/manual/00001012931400.zettel ================================================================== --- docs/manual/00001012931400.zettel +++ docs/manual/00001012931400.zettel @@ -2,11 +2,11 @@ title: Encoding of Sz Block Elements role: manual tags: #api #manual #reference #zettelstore syntax: zmk created: 20230403161803 -modified: 20230405132916 +modified: 20240123120132 === ''PARA'' :::syntax __Paragraph__ **=** ''(PARA'' [[__InlineElement__|00001012931600]] … '')''. ::: @@ -48,11 +48,11 @@ __Description__ **=** ''(DESCRIPTION'' __DescriptionTerm__ __DescriptionValues__ __DescriptionTerm__ __DescriptionValues__ … '')''. ::: A description is a sequence of one ore more terms and values. :::syntax -__DescriptionTerm__ **=** [[__Inline__|00001012931000#inline]]. +__DescriptionTerm__ **=** ''('' [[__InlineElement__|00001012931600]] … '')''. ::: A description term is just an inline-structured value. :::syntax __DescriptionValues__ **=** ''(BLOCK'' [[__Block__|00001012931000#block]] … '')''. @@ -64,18 +64,18 @@ __Table__ **=** ''(TABLE'' __TableHeader__ __TableRow__ … '')''. ::: A table is a table header and a sequence of table rows. :::syntax -__TableHeader__ **=** ''()'' **|** ''(list'' __TableCell__ … '')''. +__TableHeader__ **=** ''()'' **|** ''('' __TableCell__ … '')''. ::: -A table header is either the empty list or a list of table cells stating with the ''list'' symbol. +A table header is either the empty list or a list of table cells. :::syntax -__TableRow__ **=** ''(list'' __TableCell__ … '')''. +__TableRow__ **=** ''('' __TableCell__ … '')''. ::: -A table row is a list with the initial symbol ''list'', followed by table cells. +A table row is a list of table cells. === ''CELL'', ''CELL-*'' There are four kinds of table cells, one for each possible cell alignment. The structure is the same for all kind. @@ -107,25 +107,25 @@ The following lists specifies different kinds of regions. A region treat a sequence of block elements to be belonging together in certain ways. The have a similar structure. :::syntax -__BlockRegion__ **=** ''(REGION-BLOCK'' [[__Attributes__|00001012931000#attribute]] [[__Block__|00001012931000#block]] [[__Inline__|00001012931000#inline]] '')''. +__BlockRegion__ **=** ''(REGION-BLOCK'' [[__Attributes__|00001012931000#attribute]] ''('' [[__BlockElement__|00001012931400]] … '')'' [[__InlineElement__|00001012931600]] … '')''. ::: A block region just treats the block to belong in an unspecified way. Typically, the reason is given in the attributes. The inline describes the block. :::syntax -__QuoteRegion__ **=** ''(REGION-QUOTE'' [[__Attributes__|00001012931000#attribute]] [[__Block__|00001012931000#block]] [[__Inline__|00001012931000#inline]] '')''. +__QuoteRegion__ **=** ''(REGION-QUOTE'' [[__Attributes__|00001012931000#attribute]] ''('' [[__BlockElement__|00001012931400]] … '')'' [[__InlineElement__|00001012931600]] … '')''. ::: A block region just treats the block to contain a longer quotation. Attributes may further specify the quotation. The inline typically describes author / source of the quotation. :::syntax -__VerseRegion__ **=** ''(REGION-VERSE'' [[__Attributes__|00001012931000#attribute]] [[__Block__|00001012931000#block]] [[__Inline__|00001012931000#inline]] '')''. +__VerseRegion__ **=** ''(REGION-VERSE'' [[__Attributes__|00001012931000#attribute]] ''('' [[__BlockElement__|00001012931400]] … '')'' [[__InlineElement__|00001012931600]] … '')''. ::: A block region just treats the block to contain a verse. Soft line break are transformed into hard line breaks to save the structure of the verse / poem. Attributes may further specify something. The inline typically describes author / source of the verse. @@ -165,14 +165,14 @@ ::: The string contains text that should be treated as (nested) zettel content. === ''BLOB'' :::syntax -__BLOB__ **=** ''(BLOB'' [[__Inline__|00001012931000#inline]] String,,1,, String,,2,, '')''. +__BLOB__ **=** ''(BLOB'' ''('' [[__InlineElement__|00001012931600]] … '')'' String,,1,, String,,2,, '')''. ::: A BLOB contains an image in block mode. -The inline states some description. +The inline elements states some description. The first string contains the syntax of the image. The second string contains the actual image. If the syntax is ""SVG"", then the second string contains the SVG code. Otherwise the (binary) image data is encoded with base64. Index: docs/manual/00001012931600.zettel ================================================================== --- docs/manual/00001012931600.zettel +++ docs/manual/00001012931600.zettel @@ -2,11 +2,11 @@ title: Encoding of Sz Inline Elements role: manual tags: #api #manual #reference #zettelstore syntax: zmk created: 20230403161845 -modified: 20231113191203 +modified: 20240122122448 === ''TEXT'' :::syntax __Text__ **=** ''(TEXT'' String '')''. ::: @@ -122,11 +122,11 @@ The third string is the slug string, but made unique for the whole zettel. Then follows the marked text as a sequence of __InlineElement__s. === ''ENDNOTE'' :::syntax -__Endnote__ **=** ''(ENDNOTE'' [[__Attributes__|00001012931000#attribute]] ''(quote'' [[__InlineElement__|00001012931600]] … '')'''')''. +__Endnote__ **=** ''(ENDNOTE'' [[__Attributes__|00001012931000#attribute]] [[__InlineElement__|00001012931600]] … '')''. ::: Specifies endnote / footnote text. === ''FORMAT-*'' The following lists specifies some inline text formatting. Index: docs/manual/00001012931800.zettel ================================================================== --- docs/manual/00001012931800.zettel +++ docs/manual/00001012931800.zettel @@ -2,11 +2,11 @@ title: Encoding of Sz Attribute Values role: manual tags: #api #manual #reference #zettelstore syntax: zmk created: 20230403161923 -modified: 20230403163701 +modified: 20240122115245 An attribute is represented by a single cell. The first element of the cell references the attribute key, the second value the corresponding value. :::syntax @@ -20,9 +20,8 @@ A key with the value ''"-"'' specifies the default attribute. In this case, the attribute value is not interpreted. Some examples: * ''()'' represents the absence of attributes, -* ''(quote (("-" . "")))'' represent the default attribute, -* ''(quote (("-" . "") ("" . "syntax")))'' adds the generic attrribute with the value ""syntax"", -* ''(quote ())'' will also represent the absence of attribute (in a more complicated way), -* ''(quote (("lang" . "en")))'' denote the attribute key ""lang"" with a value ""en"". +* ''(("-" . ""))'' represent the default attribute, +* ''(("-" . "") ("" . "syntax"))'' adds the generic attribute with the value ""syntax"", +* ''(("lang" . "en"))'' denotes the attribute key ""lang"" with a value ""en"". Index: docs/manual/00001012931900.zettel ================================================================== --- docs/manual/00001012931900.zettel +++ docs/manual/00001012931900.zettel @@ -2,18 +2,18 @@ title: Encoding of Sz Reference Values role: manual tags: #api #manual #reference #zettelstore syntax: zmk created: 20230405123046 -modified: 20230405124516 +modified: 20240122094720 A reference is encoded as the actual reference value, and a symbol describing the state of that actual reference value. :::syntax -__Reference__ **=** ''(quote'' __ReferenceState__ String '')''. +__Reference__ **=** ''('' __ReferenceState__ String '')''. ::: -The ''quote'' is needed for internal reasons, the string contains the actual reference value. +The string contains the actual reference value. :::syntax __ReferenceState__ **=** ''INVALID'' **|** ''ZETTEL'' **|** ''SELF'' **|** ''FOUND'' **|** ''BROKEN'' **|** ''HOSTED'' **|** ''BASED'' **|** ''QUERY'' **|** ''EXTERNAL''. ::: @@ -31,8 +31,12 @@ This value is only possible after evaluating the zettel. ; ''BROKEN'' : The reference value is a valid reference to an missing zettel. This value is only possible after evaluating the zettel. ; ''HOSTED'' +: The reference value starts with one slash character, denoting an absolute local reference. ; ''BASED'' +: The reference value starts with two slash characters, denoting a local reference interpreted relative to the Zettelstore base URL. ; ''QUERY'' +: The reference value contains a query expression. ; ''EXTERNAL'' +: The reference value contains a full URL, referencing a resource outside of the Zettelstore server. Index: docs/manual/00001018000000.zettel ================================================================== --- docs/manual/00001018000000.zettel +++ docs/manual/00001018000000.zettel @@ -1,12 +1,12 @@ id: 00001018000000 title: Troubleshooting role: manual tags: #manual #zettelstore syntax: zmk -created: 00010101000000 -modified: 20221020132617 +created: 20211027105921 +modified: 20240221134749 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. @@ -26,11 +26,11 @@ ** **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. + If you access the zettel via Zettelstore, an 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. Index: encoder/encoder.go ================================================================== --- encoder/encoder.go +++ encoder/encoder.go @@ -4,10 +4,13 @@ // 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. +// +// SPDX-License-Identifier: EUPL-1.2 +// SPDX-FileCopyrightText: 2020-present Detlef Stern //----------------------------------------------------------------------------- // Package encoder provides a generic interface to encode the abstract syntax // tree into some text form. package encoder Index: encoder/encoder_blob_test.go ================================================================== --- encoder/encoder_blob_test.go +++ encoder/encoder_blob_test.go @@ -4,20 +4,23 @@ // 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. +// +// SPDX-License-Identifier: EUPL-1.2 +// SPDX-FileCopyrightText: 2021-present Detlef Stern //----------------------------------------------------------------------------- package encoder_test import ( "testing" "zettelstore.de/client.fossil/api" + "zettelstore.de/client.fossil/input" "zettelstore.de/z/config" - "zettelstore.de/z/input" "zettelstore.de/z/parser" "zettelstore.de/z/zettel/id" "zettelstore.de/z/zettel/meta" _ "zettelstore.de/z/parser/blob" // Allow to use BLOB parser. @@ -39,11 +42,11 @@ 0x06, 0x00, 0x03, 0x36, 0x37, 0x7c, 0xa8, 0x00, 0x00, 0x00, 0x00, 0x49, 0x45, 0x4e, 0x44, 0xae, 0x42, 0x60, 0x82, }, expect: expectMap{ encoderHTML: `

PNG

`, - encoderSz: `(BLOCK (BLOB (INLINE (TEXT "PNG")) "png" "iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAAAAAA6fptVAAAACklEQVR4nGNiAAAABgADNjd8qAAAAABJRU5ErkJggg=="))`, + encoderSz: `(BLOCK (BLOB ((TEXT "PNG")) "png" "iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAAAAAA6fptVAAAACklEQVR4nGNiAAAABgADNjd8qAAAAABJRU5ErkJggg=="))`, encoderSHTML: `((p (img (@ (alt . "PNG") (src . "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAAAAAA6fptVAAAACklEQVR4nGNiAAAABgADNjd8qAAAAABJRU5ErkJggg==")))))`, encoderText: "", encoderZmk: `%% Unable to display BLOB with description 'PNG' and syntax 'png'.`, }, }, Index: encoder/encoder_block_test.go ================================================================== --- encoder/encoder_block_test.go +++ encoder/encoder_block_test.go @@ -4,10 +4,13 @@ // 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. +// +// SPDX-License-Identifier: EUPL-1.2 +// SPDX-FileCopyrightText: 2021-present Detlef Stern //----------------------------------------------------------------------------- package encoder_test var tcsBlock = []zmkTestCase{ @@ -51,25 +54,25 @@ descr: "Rendered block comment", zmk: "%%%{-}\nRender\n%%%", expect: expectMap{ encoderHTML: "\n", encoderMD: "", - encoderSz: `(BLOCK (VERBATIM-COMMENT (quote (("-" . ""))) "Render"))`, + encoderSz: `(BLOCK (VERBATIM-COMMENT (("-" . "")) "Render"))`, encoderSHTML: "((@@@ \"Render\"))", encoderText: ``, encoderZmk: useZmk, }, }, { descr: "Simple Heading", - zmk: `=== Top`, + zmk: `=== Top Job`, expect: expectMap{ - encoderHTML: "

Top

", - encoderMD: "# Top", - encoderSz: `(BLOCK (HEADING 1 () "top" "top" (INLINE (TEXT "Top"))))`, - encoderSHTML: `((h2 (@ (id . "top")) "Top"))`, - encoderText: `Top`, + encoderHTML: "

Top Job

", + encoderMD: "# Top Job", + encoderSz: `(BLOCK (HEADING 1 () "top-job" "top-job" (TEXT "Top") (SPACE) (TEXT "Job")))`, + encoderSHTML: `((h2 (@ (id . "top-job")) "Top" " " "Job"))`, + encoderText: `Top Job`, encoderZmk: useZmk, }, }, { descr: "Simple List", @@ -123,11 +126,11 @@ descr: "Thematic break with attribute", zmk: `---{lang="zmk"}`, expect: expectMap{ encoderHTML: `
`, encoderMD: "---", - encoderSz: `(BLOCK (THEMATIC (quote (("lang" . "zmk")))))`, + encoderSz: `(BLOCK (THEMATIC (("lang" . "zmk"))))`, encoderSHTML: `((hr (@ (lang . "zmk"))))`, encoderText: ``, encoderZmk: useZmk, }, }, @@ -157,37 +160,37 @@ }, { descr: "Simple List Quote", zmk: "> ToBeOrNotToBe", expect: expectMap{ - encoderHTML: "

ToBeOrNotToBe

", + encoderHTML: "
ToBeOrNotToBe
", encoderMD: "> ToBeOrNotToBe", encoderSz: `(BLOCK (QUOTATION (INLINE (TEXT "ToBeOrNotToBe"))))`, - encoderSHTML: `((blockquote (p "ToBeOrNotToBe")))`, + encoderSHTML: `((blockquote (@L "ToBeOrNotToBe")))`, encoderText: "ToBeOrNotToBe", encoderZmk: useZmk, }, }, { descr: "Simple Quote Block", - zmk: "<<<\nToBeOrNotToBe\n<<< Romeo", + zmk: "<<<\nToBeOrNotToBe\n<<< Romeo Julia", expect: expectMap{ - encoderHTML: "

ToBeOrNotToBe

Romeo
", + encoderHTML: "

ToBeOrNotToBe

Romeo Julia
", encoderMD: "> ToBeOrNotToBe", - encoderSz: `(BLOCK (REGION-QUOTE () (BLOCK (PARA (TEXT "ToBeOrNotToBe"))) (INLINE (TEXT "Romeo"))))`, - encoderSHTML: `((blockquote (p "ToBeOrNotToBe") (cite "Romeo")))`, - encoderText: "ToBeOrNotToBe\nRomeo", + encoderSz: `(BLOCK (REGION-QUOTE () ((PARA (TEXT "ToBeOrNotToBe"))) (TEXT "Romeo") (SPACE) (TEXT "Julia")))`, + encoderSHTML: `((blockquote (p "ToBeOrNotToBe") (cite "Romeo" " " "Julia")))`, + encoderText: "ToBeOrNotToBe\nRomeo Julia", encoderZmk: useZmk, }, }, { descr: "Quote Block with multiple paragraphs", zmk: "<<<\nToBeOr\n\nNotToBe\n<<< Romeo", expect: expectMap{ encoderHTML: "

ToBeOr

NotToBe

Romeo
", encoderMD: "> ToBeOr\n\n> NotToBe", - encoderSz: `(BLOCK (REGION-QUOTE () (BLOCK (PARA (TEXT "ToBeOr")) (PARA (TEXT "NotToBe"))) (INLINE (TEXT "Romeo"))))`, + encoderSz: `(BLOCK (REGION-QUOTE () ((PARA (TEXT "ToBeOr")) (PARA (TEXT "NotToBe"))) (TEXT "Romeo")))`, encoderSHTML: `((blockquote (p "ToBeOr") (p "NotToBe") (cite "Romeo")))`, encoderText: "ToBeOr\nNotToBe\nRomeo", encoderZmk: useZmk, }, }, @@ -203,11 +206,11 @@ Spacy Para """ Author`, expect: expectMap{ encoderHTML: "

A\u00a0line
\u00a0\u00a0another\u00a0line
Back

Paragraph

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

Author
", encoderMD: "", - encoderSz: "(BLOCK (REGION-VERSE () (BLOCK (PARA (TEXT \"A\") (SPACE \"\u00a0\") (TEXT \"line\") (HARD) (SPACE \"\u00a0\u00a0\") (TEXT \"another\") (SPACE \"\u00a0\") (TEXT \"line\") (HARD) (TEXT \"Back\")) (PARA (TEXT \"Paragraph\")) (PARA (SPACE \"\u00a0\u00a0\u00a0\u00a0\") (TEXT \"Spacy\") (SPACE \"\u00a0\u00a0\") (TEXT \"Para\"))) (INLINE (TEXT \"Author\"))))", + encoderSz: "(BLOCK (REGION-VERSE () ((PARA (TEXT \"A\") (SPACE \"\u00a0\") (TEXT \"line\") (HARD) (SPACE \"\u00a0\u00a0\") (TEXT \"another\") (SPACE \"\u00a0\") (TEXT \"line\") (HARD) (TEXT \"Back\")) (PARA (TEXT \"Paragraph\")) (PARA (SPACE \"\u00a0\u00a0\u00a0\u00a0\") (TEXT \"Spacy\") (SPACE \"\u00a0\u00a0\") (TEXT \"Para\"))) (TEXT \"Author\")))", encoderSHTML: "((div (p \"A\" \"\u00a0\" \"line\" (br) \"\u00a0\u00a0\" \"another\" \"\u00a0\" \"line\" (br) \"Back\") (p \"Paragraph\") (p \"\u00a0\u00a0\u00a0\u00a0\" \"Spacy\" \"\u00a0\u00a0\" \"Para\") (cite \"Author\")))", encoderText: "A line\n another line\nBack\nParagraph\n Spacy Para\nAuthor", encoderZmk: "\"\"\"\nA\u00a0line\\\n\u00a0\u00a0another\u00a0line\\\nBack\nParagraph\n\u00a0\u00a0\u00a0\u00a0Spacy\u00a0\u00a0Para\n\"\"\" Author", }, }, @@ -219,11 +222,11 @@ and much more :::`, expect: expectMap{ encoderHTML: "

A simple span and much more

", encoderMD: "", - encoderSz: `(BLOCK (REGION-BLOCK () (BLOCK (PARA (TEXT "A") (SPACE) (TEXT "simple") (SOFT) (SPACE) (TEXT "span") (SOFT) (TEXT "and") (SPACE) (TEXT "much") (SPACE) (TEXT "more"))) (INLINE)))`, + encoderSz: `(BLOCK (REGION-BLOCK () ((PARA (TEXT "A") (SPACE) (TEXT "simple") (SOFT) (SPACE) (TEXT "span") (SOFT) (TEXT "and") (SPACE) (TEXT "much") (SPACE) (TEXT "more")))))`, encoderSHTML: `((div (p "A" " " "simple" " " " " "span" " " "and" " " "much" " " "more")))`, encoderText: `A simple span and much more`, encoderZmk: useZmk, }, }, @@ -243,11 +246,11 @@ descr: "Simple Verbatim Code with visible spaces", zmk: "```{-}\nHello World\n```", expect: expectMap{ encoderHTML: "
Hello\u2423World
", encoderMD: " Hello World", - encoderSz: `(BLOCK (VERBATIM-CODE (quote (("-" . ""))) "Hello World"))`, + encoderSz: `(BLOCK (VERBATIM-CODE (("-" . "")) "Hello World"))`, encoderSHTML: "((pre (code \"Hello\u2423World\")))", encoderText: "Hello World", encoderZmk: useZmk, }, }, @@ -279,11 +282,11 @@ descr: "Simple Description List", zmk: "; Zettel\n: Paper\n: Note\n; Zettelkasten\n: Slip box", expect: expectMap{ encoderHTML: "
Zettel

Paper

Note

Zettelkasten

Slip box

", encoderMD: "", - encoderSz: `(BLOCK (DESCRIPTION (INLINE (TEXT "Zettel")) (BLOCK (BLOCK (PARA (TEXT "Paper"))) (BLOCK (PARA (TEXT "Note")))) (INLINE (TEXT "Zettelkasten")) (BLOCK (BLOCK (PARA (TEXT "Slip") (SPACE) (TEXT "box"))))))`, + encoderSz: `(BLOCK (DESCRIPTION ((TEXT "Zettel")) (BLOCK (BLOCK (PARA (TEXT "Paper"))) (BLOCK (PARA (TEXT "Note")))) ((TEXT "Zettelkasten")) (BLOCK (BLOCK (PARA (TEXT "Slip") (SPACE) (TEXT "box"))))))`, encoderSHTML: `((dl (dt "Zettel") (dd (p "Paper")) (dd (p "Note")) (dt "Zettelkasten") (dd (p "Slip" " " "box"))))`, encoderText: "Zettel\nPaper\nNote\nZettelkasten\nSlip box", encoderZmk: useZmk, }, }, @@ -291,11 +294,11 @@ descr: "Description List with paragraphs as item", zmk: "; Zettel\n: Paper\n\n Note\n; Zettelkasten\n: Slip box", expect: expectMap{ encoderHTML: "
Zettel

Paper

Note

Zettelkasten

Slip box

", encoderMD: "", - encoderSz: `(BLOCK (DESCRIPTION (INLINE (TEXT "Zettel")) (BLOCK (BLOCK (PARA (TEXT "Paper")) (PARA (TEXT "Note")))) (INLINE (TEXT "Zettelkasten")) (BLOCK (BLOCK (PARA (TEXT "Slip") (SPACE) (TEXT "box"))))))`, + encoderSz: `(BLOCK (DESCRIPTION ((TEXT "Zettel")) (BLOCK (BLOCK (PARA (TEXT "Paper")) (PARA (TEXT "Note")))) ((TEXT "Zettelkasten")) (BLOCK (BLOCK (PARA (TEXT "Slip") (SPACE) (TEXT "box"))))))`, encoderSHTML: `((dl (dt "Zettel") (dd (p "Paper") (p "Note")) (dt "Zettelkasten") (dd (p "Slip" " " "box"))))`, encoderText: "Zettel\nPaper\nNote\nZettelkasten\nSlip box", encoderZmk: useZmk, }, }, @@ -303,11 +306,11 @@ descr: "Description List with keys, but no descriptions", zmk: "; K1\n: D11\n: D12\n; K2\n; K3\n: D31", expect: expectMap{ encoderHTML: "
K1

D11

D12

K2
K3

D31

", encoderMD: "", - encoderSz: `(BLOCK (DESCRIPTION (INLINE (TEXT "K1")) (BLOCK (BLOCK (PARA (TEXT "D11"))) (BLOCK (PARA (TEXT "D12")))) (INLINE (TEXT "K2")) (BLOCK) (INLINE (TEXT "K3")) (BLOCK (BLOCK (PARA (TEXT "D31"))))))`, + encoderSz: `(BLOCK (DESCRIPTION ((TEXT "K1")) (BLOCK (BLOCK (PARA (TEXT "D11"))) (BLOCK (PARA (TEXT "D12")))) ((TEXT "K2")) (BLOCK) ((TEXT "K3")) (BLOCK (BLOCK (PARA (TEXT "D31"))))))`, encoderSHTML: `((dl (dt "K1") (dd (p "D11")) (dd (p "D12")) (dt "K2") (dt "K3") (dd (p "D31"))))`, encoderText: "K1\nD11\nD12\nK2\nK3\nD31", encoderZmk: useZmk, }, }, @@ -315,11 +318,11 @@ descr: "Simple Table", zmk: "|c1|c2|c3\n|d1||d3", expect: expectMap{ encoderHTML: `
c1c2c3
d1d3
`, encoderMD: "", - encoderSz: `(BLOCK (TABLE () (list (CELL (TEXT "c1")) (CELL (TEXT "c2")) (CELL (TEXT "c3"))) (list (CELL (TEXT "d1")) (CELL) (CELL (TEXT "d3")))))`, + encoderSz: `(BLOCK (TABLE () ((CELL (TEXT "c1")) (CELL (TEXT "c2")) (CELL (TEXT "c3"))) ((CELL (TEXT "d1")) (CELL) (CELL (TEXT "d3")))))`, encoderSHTML: `((table (tbody (tr (td "c1") (td "c2") (td "c3")) (tr (td "d1") (td) (td "d3")))))`, encoderText: "c1 c2 c3\nd1 d3", encoderZmk: useZmk, }, }, @@ -330,11 +333,11 @@ |h1h2h3c1c2c3f1f2=f3`, encoderMD: "", - encoderSz: `(BLOCK (TABLE (list (CELL-RIGHT (TEXT "h1")) (CELL (TEXT "h2")) (CELL-CENTER (TEXT "h3"))) (list (CELL-LEFT (TEXT "c1")) (CELL (TEXT "c2")) (CELL-CENTER (TEXT "c3"))) (list (CELL-RIGHT (TEXT "f1")) (CELL (TEXT "f2")) (CELL-CENTER (TEXT "=f3")))))`, + encoderSz: `(BLOCK (TABLE ((CELL-RIGHT (TEXT "h1")) (CELL (TEXT "h2")) (CELL-CENTER (TEXT "h3"))) ((CELL-LEFT (TEXT "c1")) (CELL (TEXT "c2")) (CELL-CENTER (TEXT "c3"))) ((CELL-RIGHT (TEXT "f1")) (CELL (TEXT "f2")) (CELL-CENTER (TEXT "=f3")))))`, encoderSHTML: `((table (thead (tr (td (@ (class . "right")) "h1") (td "h2") (td (@ (class . "center")) "h3"))) (tbody (tr (td (@ (class . "left")) "c1") (td "c2") (td (@ (class . "center")) "c3")) (tr (td (@ (class . "right")) "f1") (td "f2") (td (@ (class . "center")) "=f3")))))`, encoderText: "h1 h2 h3\nc1 c2 c3\nf1 f2 =f3", encoderZmk: `|=h1>|=h2|=h3: |Text1

  1. Endnote \u21a9\ufe0e
", encoderMD: "Text", - encoderSz: `(BLOCK (PARA (TEXT "Text") (ENDNOTE () (quote (INLINE (TEXT "Endnote"))))))`, + encoderSz: `(BLOCK (PARA (TEXT "Text") (ENDNOTE () (TEXT "Endnote"))))`, encoderSHTML: "((p \"Text\" (sup (@ (id . \"fnref:1\")) (a (@ (class . \"zs-noteref\") (href . \"#fn:1\") (role . \"doc-noteref\")) \"1\"))))", encoderText: "Text Endnote", encoderZmk: useZmk, }, }, @@ -356,11 +359,11 @@ descr: "Nested Endnotes", zmk: `Text[^Endnote[^Nested]]`, expect: expectMap{ encoderHTML: "

Text1

  1. Endnote2 \u21a9\ufe0e
  2. Nested \u21a9\ufe0e
", encoderMD: "Text", - encoderSz: `(BLOCK (PARA (TEXT "Text") (ENDNOTE () (quote (INLINE (TEXT "Endnote") (ENDNOTE () (quote (INLINE (TEXT "Nested")))))))))`, + encoderSz: `(BLOCK (PARA (TEXT "Text") (ENDNOTE () (TEXT "Endnote") (ENDNOTE () (TEXT "Nested")))))`, encoderSHTML: "((p \"Text\" (sup (@ (id . \"fnref:1\")) (a (@ (class . \"zs-noteref\") (href . \"#fn:1\") (role . \"doc-noteref\")) \"1\"))))", encoderText: "Text Endnote Nested", encoderZmk: useZmk, }, }, @@ -368,11 +371,11 @@ descr: "Transclusion", zmk: `{{{http://example.com/image}}}{width="100px"}`, expect: expectMap{ encoderHTML: `

`, encoderMD: "", - encoderSz: `(BLOCK (TRANSCLUDE (quote (("width" . "100px"))) (quote (EXTERNAL "http://example.com/image"))))`, + encoderSz: `(BLOCK (TRANSCLUDE (("width" . "100px")) (EXTERNAL "http://example.com/image")))`, encoderSHTML: `((p (img (@ (class . "external") (src . "http://example.com/image") (width . "100px")))))`, encoderText: "", encoderZmk: useZmk, }, }, Index: encoder/encoder_inline_test.go ================================================================== --- encoder/encoder_inline_test.go +++ encoder/encoder_inline_test.go @@ -4,10 +4,13 @@ // 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. +// +// SPDX-License-Identifier: EUPL-1.2 +// SPDX-FileCopyrightText: 2021-present Detlef Stern //----------------------------------------------------------------------------- package encoder_test var tcsInline = []zmkTestCase{ @@ -159,11 +162,11 @@ descr: "Quotes formatting (german)", zmk: `""quotes""{lang=de}`, expect: expectMap{ encoderHTML: `„quotes“`, encoderMD: "quotes", - encoderSz: `(INLINE (FORMAT-QUOTE (quote (("lang" . "de"))) (TEXT "quotes")))`, + encoderSz: `(INLINE (FORMAT-QUOTE (("lang" . "de")) (TEXT "quotes")))`, encoderSHTML: `((span (@ (lang . "de")) (@H "„") "quotes" (@H "“")))`, encoderText: `quotes`, encoderZmk: `""quotes""{lang="de"}`, }, }, @@ -183,11 +186,11 @@ descr: "Empty quotes (unknown)", zmk: `""""{lang=unknown}`, expect: expectMap{ encoderHTML: `""`, encoderMD: "", - encoderSz: `(INLINE (FORMAT-QUOTE (quote (("lang" . "unknown")))))`, + encoderSz: `(INLINE (FORMAT-QUOTE (("lang" . "unknown"))))`, encoderSHTML: `((span (@ (lang . "unknown")) (@H """ """)))`, encoderText: ``, encoderZmk: `""""{lang="unknown"}`, }, }, @@ -255,11 +258,11 @@ descr: "Code formatting with visible space", zmk: "``x y``{-}", expect: expectMap{ encoderHTML: "x\u2423y", encoderMD: "`x y`", - encoderSz: `(INLINE (LITERAL-CODE (quote (("-" . ""))) "x y"))`, + encoderSz: `(INLINE (LITERAL-CODE (("-" . "")) "x y"))`, encoderSHTML: "((code \"x\u2423y\"))", encoderText: `x y`, encoderZmk: useZmk, }, }, @@ -315,11 +318,11 @@ descr: "Nested Span Quote formatting", zmk: `::""abc""::{lang=fr}`, expect: expectMap{ encoderHTML: `« abc »`, encoderMD: "abc", - encoderSz: `(INLINE (FORMAT-SPAN (quote (("lang" . "fr"))) (FORMAT-QUOTE () (TEXT "abc"))))`, + encoderSz: `(INLINE (FORMAT-SPAN (("lang" . "fr")) (FORMAT-QUOTE () (TEXT "abc"))))`, encoderSHTML: `((span (@ (lang . "fr")) (@L (@H "«" " ") "abc" (@H " " "»"))))`, encoderText: `abc`, encoderZmk: `::""abc""::{lang="fr"}`, }, }, @@ -374,11 +377,11 @@ descr: "Line comment", zmk: `%%{-} line comment`, expect: expectMap{ encoderHTML: ``, encoderMD: "", - encoderSz: `(INLINE (LITERAL-COMMENT (quote (("-" . ""))) "line comment"))`, + encoderSz: `(INLINE (LITERAL-COMMENT (("-" . "")) "line comment"))`, encoderSHTML: `((@@ "line comment"))`, encoderText: ``, encoderZmk: useZmk, }, }, @@ -386,11 +389,11 @@ descr: "Comment after text", zmk: `Text %%{-} comment`, expect: expectMap{ encoderHTML: `Text`, encoderMD: "Text", - encoderSz: `(INLINE (TEXT "Text") (LITERAL-COMMENT (quote (("-" . ""))) "comment"))`, + encoderSz: `(INLINE (TEXT "Text") (LITERAL-COMMENT (("-" . "")) "comment"))`, encoderSHTML: `("Text" (@@ "comment"))`, encoderText: `Text`, encoderZmk: useZmk, }, }, @@ -398,11 +401,11 @@ descr: "Comment after text and with -->", zmk: `Text %%{-} comment --> end`, expect: expectMap{ encoderHTML: `Text`, encoderMD: "Text", - encoderSz: `(INLINE (TEXT "Text") (LITERAL-COMMENT (quote (("-" . ""))) "comment --> end"))`, + encoderSz: `(INLINE (TEXT "Text") (LITERAL-COMMENT (("-" . "")) "comment --> end"))`, encoderSHTML: `("Text" (@@ "comment --> end"))`, encoderText: `Text`, encoderZmk: useZmk, }, }, @@ -410,11 +413,11 @@ descr: "Simple inline endnote", zmk: `[^endnote]`, expect: expectMap{ encoderHTML: `1`, encoderMD: "", - encoderSz: `(INLINE (ENDNOTE () (quote (INLINE (TEXT "endnote")))))`, + encoderSz: `(INLINE (ENDNOTE () (TEXT "endnote")))`, encoderSHTML: `((sup (@ (id . "fnref:1")) (a (@ (class . "zs-noteref") (href . "#fn:1") (role . "doc-noteref")) "1")))`, encoderText: `endnote`, encoderZmk: useZmk, }, }, @@ -626,11 +629,11 @@ descr: "Dummy Embed", zmk: `{{abc}}`, expect: expectMap{ encoderHTML: ``, encoderMD: "![abc](abc)", - encoderSz: `(INLINE (EMBED () (quote (EXTERNAL "abc")) ""))`, + encoderSz: `(INLINE (EMBED () (EXTERNAL "abc") ""))`, encoderSHTML: `((img (@ (src . "abc"))))`, encoderText: ``, encoderZmk: useZmk, }, }, @@ -650,11 +653,11 @@ descr: "Inline Text Zettel", zmk: `@@
@@{="text"}`, expect: expectMap{ encoderHTML: ``, encoderMD: "
", - encoderSz: `(INLINE (LITERAL-ZETTEL (quote (("" . "text"))) "
"))`, + encoderSz: `(INLINE (LITERAL-ZETTEL (("" . "text")) "
"))`, encoderSHTML: `(())`, encoderText: `
`, encoderZmk: useZmk, }, }, Index: encoder/encoder_test.go ================================================================== --- encoder/encoder_test.go +++ encoder/encoder_test.go @@ -4,10 +4,13 @@ // 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. +// +// SPDX-License-Identifier: EUPL-1.2 +// SPDX-FileCopyrightText: 2021-present Detlef Stern //----------------------------------------------------------------------------- package encoder_test import ( @@ -14,15 +17,15 @@ "fmt" "strings" "testing" "zettelstore.de/client.fossil/api" + "zettelstore.de/client.fossil/input" "zettelstore.de/sx.fossil/sxreader" "zettelstore.de/z/ast" "zettelstore.de/z/config" "zettelstore.de/z/encoder" - "zettelstore.de/z/input" "zettelstore.de/z/parser" "zettelstore.de/z/zettel/meta" _ "zettelstore.de/z/encoder/htmlenc" // Allow to use HTML encoder. _ "zettelstore.de/z/encoder/mdenc" // Allow to use markdown encoder. @@ -114,11 +117,11 @@ val, err := sxreader.MakeReader(strings.NewReader(exp)).Read() if err != nil { t.Error(err) return } - got := val.Repr() + got := val.String() if exp != got { prefix := fmt.Sprintf("Test #%d", testNum) if d := descr; d != "" { prefix += "\nReason: " + d } Index: encoder/htmlenc/htmlenc.go ================================================================== --- encoder/htmlenc/htmlenc.go +++ encoder/htmlenc/htmlenc.go @@ -4,10 +4,13 @@ // 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. +// +// SPDX-License-Identifier: EUPL-1.2 +// SPDX-FileCopyrightText: 2022-present Detlef Stern //----------------------------------------------------------------------------- // Package htmlenc encodes the abstract syntax tree into HTML5 via zettelstore-client. package htmlenc @@ -35,11 +38,11 @@ func Create(params *encoder.CreateParameter) *Encoder { // We need a new transformer every time, because tx.inVerse must be unique. // If we can refactor it out, the transformer can be created only once. return &Encoder{ tx: szenc.NewTransformer(), - th: shtml.NewEvaluator(1, nil), + th: shtml.NewEvaluator(1), lang: params.Lang, textEnc: textenc.Create(), } } @@ -75,46 +78,39 @@ if err != nil { return 0, err } hen := he.th.Endnotes(&env) - sf := he.th.SymbolFactory() - symAttr := sf.MustMake(sxhtml.NameSymAttr) - - head := sx.MakeList(sf.MustMake("head")) - curr := head - curr = curr.AppendBang(sx.Nil().Cons(sx.Nil().Cons(sx.Cons(sf.MustMake("charset"), sx.String("utf-8"))).Cons(symAttr)).Cons(sf.MustMake("meta"))) - for elem := hm; elem != nil; elem = elem.Tail() { - curr = curr.AppendBang(elem.Car()) - } + var head sx.ListBuilder + head.Add(shtml.SymHead) + head.Add(sx.Nil().Cons(sx.Nil().Cons(sx.Cons(sx.MakeSymbol("charset"), sx.String("utf-8"))).Cons(sxhtml.SymAttr)).Cons(shtml.SymMeta)) + head.ExtendBang(hm) var sb strings.Builder if hasTitle { he.textEnc.WriteInlines(&sb, &isTitle) } else { sb.Write(zn.Meta.Zid.Bytes()) } - _ = curr.AppendBang(sx.Nil().Cons(sx.String(sb.String())).Cons(sf.MustMake("title"))) - - body := sx.MakeList(sf.MustMake("body")) - curr = body - if hasTitle { - curr = curr.AppendBang(htitle.Cons(sf.MustMake("h1"))) - } - for elem := hast; elem != nil; elem = elem.Tail() { - curr = curr.AppendBang(elem.Car()) - } - if hen != nil { - curr = curr.AppendBang(sx.Nil().Cons(sf.MustMake("hr"))) - _ = curr.AppendBang(hen) + head.Add(sx.MakeList(shtml.SymAttrTitle, sx.String(sb.String()))) + + var body sx.ListBuilder + body.Add(shtml.SymBody) + if hasTitle { + body.Add(htitle.Cons(shtml.SymH1)) + } + body.ExtendBang(hast) + if hen != nil { + body.Add(sx.Cons(shtml.SymHR, nil)) + body.Add(hen) } doc := sx.MakeList( - sf.MustMake(sxhtml.NameSymDoctype), - sx.MakeList(sf.MustMake("html"), head, body), + sxhtml.SymDoctype, + sx.MakeList(shtml.SymHtml, head.List(), body.List()), ) - gen := sxhtml.NewGenerator(sf, sxhtml.WithNewline) + gen := sxhtml.NewGenerator(sxhtml.WithNewline) return gen.WriteHTML(w, doc) } // WriteMeta encodes meta data as HTML5. func (he *Encoder) WriteMeta(w io.Writer, m *meta.Meta, evalMeta encoder.EvalMetaFunc) (int, error) { @@ -121,11 +117,11 @@ env := shtml.MakeEnvironment(he.lang) hm, err := he.th.Evaluate(he.tx.GetMeta(m, evalMeta), &env) if err != nil { return 0, err } - gen := sxhtml.NewGenerator(he.th.SymbolFactory(), sxhtml.WithNewline) + gen := sxhtml.NewGenerator(sxhtml.WithNewline) return gen.WriteListHTML(w, hm) } func (he *Encoder) WriteContent(w io.Writer, zn *ast.ZettelNode) (int, error) { return he.WriteBlocks(w, &zn.Ast) @@ -134,11 +130,11 @@ // WriteBlocks encodes a block slice. func (he *Encoder) WriteBlocks(w io.Writer, bs *ast.BlockSlice) (int, error) { env := shtml.MakeEnvironment(he.lang) hobj, err := he.th.Evaluate(he.tx.GetSz(bs), &env) if err == nil { - gen := sxhtml.NewGenerator(he.th.SymbolFactory()) + gen := sxhtml.NewGenerator() length, err2 := gen.WriteListHTML(w, hobj) if err2 != nil { return length, err2 } @@ -152,14 +148,14 @@ // WriteInlines writes an inline slice to the writer func (he *Encoder) WriteInlines(w io.Writer, is *ast.InlineSlice) (int, error) { env := shtml.MakeEnvironment(he.lang) hobj, err := he.th.Evaluate(he.tx.GetSz(is), &env) if err == nil { - gen := sxhtml.NewGenerator(sx.FindSymbolFactory(hobj)) + gen := sxhtml.NewGenerator() length, err2 := gen.WriteListHTML(w, hobj) if err2 != nil { return length, err2 } return length, nil } return 0, err } Index: encoder/mdenc/mdenc.go ================================================================== --- encoder/mdenc/mdenc.go +++ encoder/mdenc/mdenc.go @@ -4,10 +4,13 @@ // 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. +// +// SPDX-License-Identifier: EUPL-1.2 +// SPDX-FileCopyrightText: 2022-present Detlef Stern //----------------------------------------------------------------------------- // Package mdenc encodes the abstract syntax tree back into Markdown. package mdenc @@ -351,9 +354,9 @@ v.b.Write(ln.Content) } } func (v *visitor) writeSpaces(n int) { - for i := 0; i < n; i++ { + for range n { v.b.WriteByte(' ') } } Index: encoder/shtmlenc/shtmlenc.go ================================================================== --- encoder/shtmlenc/shtmlenc.go +++ encoder/shtmlenc/shtmlenc.go @@ -4,10 +4,13 @@ // 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. +// +// SPDX-License-Identifier: EUPL-1.2 +// SPDX-FileCopyrightText: 2023-present Detlef Stern //----------------------------------------------------------------------------- // Package shtmlenc encodes the abstract syntax tree into a s-expr which represents HTML. package shtmlenc @@ -31,11 +34,11 @@ func Create(params *encoder.CreateParameter) *Encoder { // We need a new transformer every time, because tx.inVerse must be unique. // If we can refactor it out, the transformer can be created only once. return &Encoder{ tx: szenc.NewTransformer(), - th: shtml.NewEvaluator(1, nil), + th: shtml.NewEvaluator(1), lang: params.Lang, } } type Encoder struct { Index: encoder/szenc/szenc.go ================================================================== --- encoder/szenc/szenc.go +++ encoder/szenc/szenc.go @@ -4,10 +4,13 @@ // 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. +// +// SPDX-License-Identifier: EUPL-1.2 +// SPDX-FileCopyrightText: 2022-present Detlef Stern //----------------------------------------------------------------------------- // Package szenc encodes the abstract syntax tree into a s-expr for zettel. package szenc Index: encoder/szenc/transform.go ================================================================== --- encoder/szenc/transform.go +++ encoder/szenc/transform.go @@ -4,10 +4,13 @@ // 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. +// +// SPDX-License-Identifier: EUPL-1.2 +// SPDX-FileCopyrightText: 2022-present Detlef Stern //----------------------------------------------------------------------------- package szenc import ( @@ -23,233 +26,177 @@ "zettelstore.de/z/zettel/meta" ) // NewTransformer returns a new transformer to create s-expressions from AST nodes. func NewTransformer() *Transformer { - sf := sx.MakeMappedFactory(1024) - t := Transformer{sf: sf} - t.zetSyms.InitializeZettelSymbols(sf) - - t.mapVerbatimKindS = map[ast.VerbatimKind]*sx.Symbol{ - ast.VerbatimZettel: t.zetSyms.SymVerbatimZettel, - ast.VerbatimProg: t.zetSyms.SymVerbatimProg, - ast.VerbatimEval: t.zetSyms.SymVerbatimEval, - ast.VerbatimMath: t.zetSyms.SymVerbatimMath, - ast.VerbatimComment: t.zetSyms.SymVerbatimComment, - ast.VerbatimHTML: t.zetSyms.SymVerbatimHTML, - } - - t.mapRegionKindS = map[ast.RegionKind]*sx.Symbol{ - ast.RegionSpan: t.zetSyms.SymRegionBlock, - ast.RegionQuote: t.zetSyms.SymRegionQuote, - ast.RegionVerse: t.zetSyms.SymRegionVerse, - } - t.mapNestedListKindS = map[ast.NestedListKind]*sx.Symbol{ - ast.NestedListOrdered: t.zetSyms.SymListOrdered, - ast.NestedListUnordered: t.zetSyms.SymListUnordered, - ast.NestedListQuote: t.zetSyms.SymListQuote, - } - t.alignmentSymbolS = map[ast.Alignment]*sx.Symbol{ - ast.AlignDefault: t.zetSyms.SymCell, - ast.AlignLeft: t.zetSyms.SymCellLeft, - ast.AlignCenter: t.zetSyms.SymCellCenter, - ast.AlignRight: t.zetSyms.SymCellRight, - } - t.mapRefStateLink = map[ast.RefState]*sx.Symbol{ - ast.RefStateInvalid: t.zetSyms.SymLinkInvalid, - ast.RefStateZettel: t.zetSyms.SymLinkZettel, - ast.RefStateSelf: t.zetSyms.SymLinkSelf, - ast.RefStateFound: t.zetSyms.SymLinkFound, - ast.RefStateBroken: t.zetSyms.SymLinkBroken, - ast.RefStateHosted: t.zetSyms.SymLinkHosted, - ast.RefStateBased: t.zetSyms.SymLinkBased, - ast.RefStateQuery: t.zetSyms.SymLinkQuery, - ast.RefStateExternal: t.zetSyms.SymLinkExternal, - } - t.mapFormatKindS = map[ast.FormatKind]*sx.Symbol{ - ast.FormatEmph: t.zetSyms.SymFormatEmph, - ast.FormatStrong: t.zetSyms.SymFormatStrong, - ast.FormatDelete: t.zetSyms.SymFormatDelete, - ast.FormatInsert: t.zetSyms.SymFormatInsert, - ast.FormatSuper: t.zetSyms.SymFormatSuper, - ast.FormatSub: t.zetSyms.SymFormatSub, - ast.FormatQuote: t.zetSyms.SymFormatQuote, - ast.FormatMark: t.zetSyms.SymFormatMark, - ast.FormatSpan: t.zetSyms.SymFormatSpan, - } - t.mapLiteralKindS = map[ast.LiteralKind]*sx.Symbol{ - ast.LiteralZettel: t.zetSyms.SymLiteralZettel, - ast.LiteralProg: t.zetSyms.SymLiteralProg, - ast.LiteralInput: t.zetSyms.SymLiteralInput, - ast.LiteralOutput: t.zetSyms.SymLiteralOutput, - ast.LiteralComment: t.zetSyms.SymLiteralComment, - ast.LiteralHTML: t.zetSyms.SymLiteralHTML, - ast.LiteralMath: t.zetSyms.SymLiteralMath, - } - t.mapRefStateS = map[ast.RefState]*sx.Symbol{ - ast.RefStateInvalid: t.zetSyms.SymRefStateInvalid, - ast.RefStateZettel: t.zetSyms.SymRefStateZettel, - ast.RefStateSelf: t.zetSyms.SymRefStateSelf, - ast.RefStateFound: t.zetSyms.SymRefStateFound, - ast.RefStateBroken: t.zetSyms.SymRefStateBroken, - ast.RefStateHosted: t.zetSyms.SymRefStateHosted, - ast.RefStateBased: t.zetSyms.SymRefStateBased, - ast.RefStateQuery: t.zetSyms.SymRefStateQuery, - ast.RefStateExternal: t.zetSyms.SymRefStateExternal, - } - t.mapMetaTypeS = map[*meta.DescriptionType]*sx.Symbol{ - meta.TypeCredential: t.zetSyms.SymTypeCredential, - meta.TypeEmpty: t.zetSyms.SymTypeEmpty, - meta.TypeID: t.zetSyms.SymTypeID, - meta.TypeIDSet: t.zetSyms.SymTypeIDSet, - meta.TypeNumber: t.zetSyms.SymTypeNumber, - meta.TypeString: t.zetSyms.SymTypeString, - meta.TypeTagSet: t.zetSyms.SymTypeTagSet, - meta.TypeTimestamp: t.zetSyms.SymTypeTimestamp, - meta.TypeURL: t.zetSyms.SymTypeURL, - meta.TypeWord: t.zetSyms.SymTypeWord, - meta.TypeWordSet: t.zetSyms.SymTypeWordSet, - meta.TypeZettelmarkup: t.zetSyms.SymTypeZettelmarkup, - } + t := Transformer{} return &t } type Transformer struct { - sf sx.SymbolFactory - zetSyms sz.ZettelSymbols - mapVerbatimKindS map[ast.VerbatimKind]*sx.Symbol - mapRegionKindS map[ast.RegionKind]*sx.Symbol - mapNestedListKindS map[ast.NestedListKind]*sx.Symbol - alignmentSymbolS map[ast.Alignment]*sx.Symbol - mapRefStateLink map[ast.RefState]*sx.Symbol - mapFormatKindS map[ast.FormatKind]*sx.Symbol - mapLiteralKindS map[ast.LiteralKind]*sx.Symbol - mapRefStateS map[ast.RefState]*sx.Symbol - mapMetaTypeS map[*meta.DescriptionType]*sx.Symbol - inVerse bool + inVerse bool } func (t *Transformer) GetSz(node ast.Node) *sx.Pair { switch n := node.(type) { case *ast.BlockSlice: - return t.getBlockSlice(n) + return t.getBlockList(n).Cons(sz.SymBlock) case *ast.InlineSlice: - return t.getInlineSlice(*n) + return t.getInlineList(*n).Cons(sz.SymInline) case *ast.ParaNode: - return t.getInlineSlice(n.Inlines).Tail().Cons(t.zetSyms.SymPara) + return t.getInlineList(n.Inlines).Cons(sz.SymPara) case *ast.VerbatimNode: return sx.MakeList( - mapGetS(t, t.mapVerbatimKindS, n.Kind), - t.getAttributes(n.Attrs), + mapGetS(mapVerbatimKindS, n.Kind), + getAttributes(n.Attrs), sx.String(string(n.Content)), ) case *ast.RegionNode: return t.getRegion(n) case *ast.HeadingNode: - return sx.MakeList( - t.zetSyms.SymHeading, - sx.Int64(int64(n.Level)), - t.getAttributes(n.Attrs), - sx.String(n.Slug), - sx.String(n.Fragment), - t.getInlineSlice(n.Inlines), - ) + return t.getInlineList(n.Inlines). + Cons(sx.String(n.Fragment)). + Cons(sx.String(n.Slug)). + Cons(getAttributes(n.Attrs)). + Cons(sx.Int64(int64(n.Level))). + Cons(sz.SymHeading) case *ast.HRuleNode: - return sx.MakeList(t.zetSyms.SymThematic, t.getAttributes(n.Attrs)) + return sx.MakeList(sz.SymThematic, getAttributes(n.Attrs)) case *ast.NestedListNode: return t.getNestedList(n) case *ast.DescriptionListNode: return t.getDescriptionList(n) case *ast.TableNode: return t.getTable(n) case *ast.TranscludeNode: - return sx.MakeList(t.zetSyms.SymTransclude, t.getAttributes(n.Attrs), t.getReference(n.Ref)) + return sx.MakeList(sz.SymTransclude, getAttributes(n.Attrs), getReference(n.Ref)) case *ast.BLOBNode: return t.getBLOB(n) case *ast.TextNode: - return sx.MakeList(t.zetSyms.SymText, sx.String(n.Text)) + return sx.MakeList(sz.SymText, sx.String(n.Text)) case *ast.SpaceNode: if t.inVerse { - return sx.MakeList(t.zetSyms.SymSpace, sx.String(n.Lexeme)) + return sx.MakeList(sz.SymSpace, sx.String(n.Lexeme)) } - return sx.MakeList(t.zetSyms.SymSpace) + return sx.MakeList(sz.SymSpace) case *ast.BreakNode: if n.Hard { - return sx.MakeList(t.zetSyms.SymHard) + return sx.MakeList(sz.SymHard) } - return sx.MakeList(t.zetSyms.SymSoft) + return sx.MakeList(sz.SymSoft) case *ast.LinkNode: return t.getLink(n) case *ast.EmbedRefNode: - return t.getInlineSlice(n.Inlines).Tail(). + return t.getInlineList(n.Inlines). Cons(sx.String(n.Syntax)). - Cons(t.getReference(n.Ref)). - Cons(t.getAttributes(n.Attrs)). - Cons(t.zetSyms.SymEmbed) + Cons(getReference(n.Ref)). + Cons(getAttributes(n.Attrs)). + Cons(sz.SymEmbed) case *ast.EmbedBLOBNode: return t.getEmbedBLOB(n) case *ast.CiteNode: - return t.getInlineSlice(n.Inlines).Tail(). + return t.getInlineList(n.Inlines). Cons(sx.String(n.Key)). - Cons(t.getAttributes(n.Attrs)). - Cons(t.zetSyms.SymCite) + Cons(getAttributes(n.Attrs)). + Cons(sz.SymCite) case *ast.FootnoteNode: - text := sx.Nil().Cons(sx.Nil().Cons(t.getInlineSlice(n.Inlines)).Cons(t.zetSyms.SymQuote)) - return text.Cons(t.getAttributes(n.Attrs)).Cons(t.zetSyms.SymEndnote) + // (ENDNODE attrs InlineElement ...) + return t.getInlineList(n.Inlines).Cons(getAttributes(n.Attrs)).Cons(sz.SymEndnote) case *ast.MarkNode: - return t.getInlineSlice(n.Inlines).Tail(). + return t.getInlineList(n.Inlines). Cons(sx.String(n.Fragment)). Cons(sx.String(n.Slug)). Cons(sx.String(n.Mark)). - Cons(t.zetSyms.SymMark) + Cons(sz.SymMark) case *ast.FormatNode: - return t.getInlineSlice(n.Inlines).Tail(). - Cons(t.getAttributes(n.Attrs)). - Cons(mapGetS(t, t.mapFormatKindS, n.Kind)) + return t.getInlineList(n.Inlines). + Cons(getAttributes(n.Attrs)). + Cons(mapGetS(mapFormatKindS, n.Kind)) case *ast.LiteralNode: return sx.MakeList( - mapGetS(t, t.mapLiteralKindS, n.Kind), - t.getAttributes(n.Attrs), + mapGetS(mapLiteralKindS, n.Kind), + getAttributes(n.Attrs), sx.String(string(n.Content)), ) } - return sx.MakeList(t.zetSyms.SymUnknown, sx.String(fmt.Sprintf("%T %v", node, node))) + return sx.MakeList(sz.SymUnknown, sx.String(fmt.Sprintf("%T %v", node, node))) +} + +var mapVerbatimKindS = map[ast.VerbatimKind]*sx.Symbol{ + ast.VerbatimZettel: sz.SymVerbatimZettel, + ast.VerbatimProg: sz.SymVerbatimProg, + ast.VerbatimEval: sz.SymVerbatimEval, + ast.VerbatimMath: sz.SymVerbatimMath, + ast.VerbatimComment: sz.SymVerbatimComment, + ast.VerbatimHTML: sz.SymVerbatimHTML, +} + +var mapFormatKindS = map[ast.FormatKind]*sx.Symbol{ + ast.FormatEmph: sz.SymFormatEmph, + ast.FormatStrong: sz.SymFormatStrong, + ast.FormatDelete: sz.SymFormatDelete, + ast.FormatInsert: sz.SymFormatInsert, + ast.FormatSuper: sz.SymFormatSuper, + ast.FormatSub: sz.SymFormatSub, + ast.FormatQuote: sz.SymFormatQuote, + ast.FormatMark: sz.SymFormatMark, + ast.FormatSpan: sz.SymFormatSpan, +} + +var mapLiteralKindS = map[ast.LiteralKind]*sx.Symbol{ + ast.LiteralZettel: sz.SymLiteralZettel, + ast.LiteralProg: sz.SymLiteralProg, + ast.LiteralInput: sz.SymLiteralInput, + ast.LiteralOutput: sz.SymLiteralOutput, + ast.LiteralComment: sz.SymLiteralComment, + ast.LiteralHTML: sz.SymLiteralHTML, + ast.LiteralMath: sz.SymLiteralMath, +} + +var mapRegionKindS = map[ast.RegionKind]*sx.Symbol{ + ast.RegionSpan: sz.SymRegionBlock, + ast.RegionQuote: sz.SymRegionQuote, + ast.RegionVerse: sz.SymRegionVerse, } func (t *Transformer) getRegion(rn *ast.RegionNode) *sx.Pair { saveInVerse := t.inVerse if rn.Kind == ast.RegionVerse { t.inVerse = true } - symBlocks := t.GetSz(&rn.Blocks) + symBlocks := t.getBlockList(&rn.Blocks) t.inVerse = saveInVerse - return sx.MakeList( - mapGetS(t, t.mapRegionKindS, rn.Kind), - t.getAttributes(rn.Attrs), - symBlocks, - t.GetSz(&rn.Inlines), - ) + return t.getInlineList(rn.Inlines). + Cons(symBlocks). + Cons(getAttributes(rn.Attrs)). + Cons(mapGetS(mapRegionKindS, rn.Kind)) +} + +var mapNestedListKindS = map[ast.NestedListKind]*sx.Symbol{ + ast.NestedListOrdered: sz.SymListOrdered, + ast.NestedListUnordered: sz.SymListUnordered, + ast.NestedListQuote: sz.SymListQuote, } func (t *Transformer) getNestedList(ln *ast.NestedListNode) *sx.Pair { - nlistObjs := make([]sx.Object, len(ln.Items)+1) - nlistObjs[0] = mapGetS(t, t.mapNestedListKindS, ln.Kind) + nlistObjs := make(sx.Vector, len(ln.Items)+1) + nlistObjs[0] = mapGetS(mapNestedListKindS, ln.Kind) isCompact := isCompactList(ln.Items) for i, item := range ln.Items { if isCompact && len(item) > 0 { paragraph := t.GetSz(item[0]) - nlistObjs[i+1] = paragraph.Tail().Cons(t.zetSyms.SymInline) + nlistObjs[i+1] = paragraph.Tail().Cons(sz.SymInline) continue } - itemObjs := make([]sx.Object, len(item)) + itemObjs := make(sx.Vector, len(item)) for j, in := range item { itemObjs[j] = t.GetSz(in) } if isCompact { - nlistObjs[i+1] = sx.MakeList(itemObjs...).Cons(t.zetSyms.SymInline) + nlistObjs[i+1] = sx.MakeList(itemObjs...).Cons(sz.SymInline) } else { - nlistObjs[i+1] = sx.MakeList(itemObjs...).Cons(t.zetSyms.SymBlock) + nlistObjs[i+1] = sx.MakeList(itemObjs...).Cons(sz.SymBlock) } } return sx.MakeList(nlistObjs...) } func isCompactList(itemSlice []ast.ItemSlice) bool { @@ -265,30 +212,30 @@ } return true } func (t *Transformer) getDescriptionList(dn *ast.DescriptionListNode) *sx.Pair { - dlObjs := make([]sx.Object, 2*len(dn.Descriptions)+1) - dlObjs[0] = t.zetSyms.SymDescription + dlObjs := make(sx.Vector, 2*len(dn.Descriptions)+1) + dlObjs[0] = sz.SymDescription for i, def := range dn.Descriptions { - dlObjs[2*i+1] = t.getInlineSlice(def.Term) - descObjs := make([]sx.Object, len(def.Descriptions)) + dlObjs[2*i+1] = t.getInlineList(def.Term) + descObjs := make(sx.Vector, len(def.Descriptions)) for j, b := range def.Descriptions { - dVal := make([]sx.Object, len(b)) + dVal := make(sx.Vector, len(b)) for k, dn := range b { dVal[k] = t.GetSz(dn) } - descObjs[j] = sx.MakeList(dVal...).Cons(t.zetSyms.SymBlock) + descObjs[j] = sx.MakeList(dVal...).Cons(sz.SymBlock) } - dlObjs[2*i+2] = sx.MakeList(descObjs...).Cons(t.zetSyms.SymBlock) + dlObjs[2*i+2] = sx.MakeList(descObjs...).Cons(sz.SymBlock) } return sx.MakeList(dlObjs...) } func (t *Transformer) getTable(tn *ast.TableNode) *sx.Pair { - tObjs := make([]sx.Object, len(tn.Rows)+2) - tObjs[0] = t.zetSyms.SymTable + tObjs := make(sx.Vector, len(tn.Rows)+2) + tObjs[0] = sz.SymTable tObjs[1] = t.getHeader(tn.Header) for i, row := range tn.Rows { tObjs[i+2] = t.getRow(row) } return sx.MakeList(tObjs...) @@ -298,19 +245,26 @@ return nil } return t.getRow(header) } func (t *Transformer) getRow(row ast.TableRow) *sx.Pair { - rObjs := make([]sx.Object, len(row)) + rObjs := make(sx.Vector, len(row)) for i, cell := range row { rObjs[i] = t.getCell(cell) } - return sx.MakeList(rObjs...).Cons(t.zetSyms.SymList) + return sx.MakeList(rObjs...) +} + +var alignmentSymbolS = map[ast.Alignment]*sx.Symbol{ + ast.AlignDefault: sz.SymCell, + ast.AlignLeft: sz.SymCellLeft, + ast.AlignCenter: sz.SymCellCenter, + ast.AlignRight: sz.SymCellRight, } func (t *Transformer) getCell(cell *ast.TableCell) *sx.Pair { - return t.getInlineSlice(cell.Inlines).Tail().Cons(mapGetS(t, t.alignmentSymbolS, cell.Align)) + return t.getInlineList(cell.Inlines).Cons(mapGetS(alignmentSymbolS, cell.Align)) } func (t *Transformer) getBLOB(bn *ast.BLOBNode) *sx.Pair { var lastObj sx.Object if bn.Syntax == meta.SyntaxSVG { @@ -317,103 +271,134 @@ lastObj = sx.String(string(bn.Blob)) } else { lastObj = getBase64String(bn.Blob) } return sx.MakeList( - t.zetSyms.SymBLOB, - t.getInlineSlice(bn.Description), + sz.SymBLOB, + t.getInlineList(bn.Description), sx.String(bn.Syntax), lastObj, ) } + +var mapRefStateLink = map[ast.RefState]*sx.Symbol{ + ast.RefStateInvalid: sz.SymLinkInvalid, + ast.RefStateZettel: sz.SymLinkZettel, + ast.RefStateSelf: sz.SymLinkSelf, + ast.RefStateFound: sz.SymLinkFound, + ast.RefStateBroken: sz.SymLinkBroken, + ast.RefStateHosted: sz.SymLinkHosted, + ast.RefStateBased: sz.SymLinkBased, + ast.RefStateQuery: sz.SymLinkQuery, + ast.RefStateExternal: sz.SymLinkExternal, +} func (t *Transformer) getLink(ln *ast.LinkNode) *sx.Pair { - return t.getInlineSlice(ln.Inlines).Tail(). + return t.getInlineList(ln.Inlines). Cons(sx.String(ln.Ref.Value)). - Cons(t.getAttributes(ln.Attrs)). - Cons(mapGetS(t, t.mapRefStateLink, ln.Ref.State)) + Cons(getAttributes(ln.Attrs)). + Cons(mapGetS(mapRefStateLink, ln.Ref.State)) } func (t *Transformer) getEmbedBLOB(en *ast.EmbedBLOBNode) *sx.Pair { - tail := t.getInlineSlice(en.Inlines).Tail() + tail := t.getInlineList(en.Inlines) if en.Syntax == meta.SyntaxSVG { tail = tail.Cons(sx.String(string(en.Blob))) } else { tail = tail.Cons(getBase64String(en.Blob)) } - return tail.Cons(sx.String(en.Syntax)).Cons(t.getAttributes(en.Attrs)).Cons(t.zetSyms.SymEmbedBLOB) + return tail.Cons(sx.String(en.Syntax)).Cons(getAttributes(en.Attrs)).Cons(sz.SymEmbedBLOB) } -func (t *Transformer) getBlockSlice(bs *ast.BlockSlice) *sx.Pair { - objs := make([]sx.Object, len(*bs)) +func (t *Transformer) getBlockList(bs *ast.BlockSlice) *sx.Pair { + objs := make(sx.Vector, len(*bs)) for i, n := range *bs { objs[i] = t.GetSz(n) } - return sx.MakeList(objs...).Cons(t.zetSyms.SymBlock) + return sx.MakeList(objs...) } -func (t *Transformer) getInlineSlice(is ast.InlineSlice) *sx.Pair { - objs := make([]sx.Object, len(is)) +func (t *Transformer) getInlineList(is ast.InlineSlice) *sx.Pair { + objs := make(sx.Vector, len(is)) for i, n := range is { objs[i] = t.GetSz(n) } - return sx.MakeList(objs...).Cons(t.zetSyms.SymInline) + return sx.MakeList(objs...) } -func (t *Transformer) getAttributes(a attrs.Attributes) sx.Object { +func getAttributes(a attrs.Attributes) sx.Object { if a.IsEmpty() { return sx.Nil() } keys := a.Keys() - objs := make([]sx.Object, 0, len(keys)) + objs := make(sx.Vector, 0, len(keys)) for _, k := range keys { objs = append(objs, sx.Cons(sx.String(k), sx.String(a[k]))) } - return sx.Nil().Cons(sx.MakeList(objs...)).Cons(t.zetSyms.SymQuote) + return sx.MakeList(objs...) +} + +var mapRefStateS = map[ast.RefState]*sx.Symbol{ + ast.RefStateInvalid: sz.SymRefStateInvalid, + ast.RefStateZettel: sz.SymRefStateZettel, + ast.RefStateSelf: sz.SymRefStateSelf, + ast.RefStateFound: sz.SymRefStateFound, + ast.RefStateBroken: sz.SymRefStateBroken, + ast.RefStateHosted: sz.SymRefStateHosted, + ast.RefStateBased: sz.SymRefStateBased, + ast.RefStateQuery: sz.SymRefStateQuery, + ast.RefStateExternal: sz.SymRefStateExternal, +} + +func getReference(ref *ast.Reference) *sx.Pair { + return sx.MakeList(mapGetS(mapRefStateS, ref.State), sx.String(ref.Value)) } -func (t *Transformer) getReference(ref *ast.Reference) *sx.Pair { - return sx.MakeList( - t.zetSyms.SymQuote, - sx.MakeList( - mapGetS(t, t.mapRefStateS, ref.State), - sx.String(ref.Value), - ), - ) +var mapMetaTypeS = map[*meta.DescriptionType]*sx.Symbol{ + meta.TypeCredential: sz.SymTypeCredential, + meta.TypeEmpty: sz.SymTypeEmpty, + meta.TypeID: sz.SymTypeID, + meta.TypeIDSet: sz.SymTypeIDSet, + meta.TypeNumber: sz.SymTypeNumber, + meta.TypeString: sz.SymTypeString, + meta.TypeTagSet: sz.SymTypeTagSet, + meta.TypeTimestamp: sz.SymTypeTimestamp, + meta.TypeURL: sz.SymTypeURL, + meta.TypeWord: sz.SymTypeWord, + meta.TypeZettelmarkup: sz.SymTypeZettelmarkup, } func (t *Transformer) GetMeta(m *meta.Meta, evalMeta encoder.EvalMetaFunc) *sx.Pair { pairs := m.ComputedPairs() - objs := make([]sx.Object, 0, len(pairs)) + objs := make(sx.Vector, 0, len(pairs)) for _, p := range pairs { key := p.Key ty := m.Type(key) - symType := mapGetS(t, t.mapMetaTypeS, ty) + symType := mapGetS(mapMetaTypeS, ty) var obj sx.Object if ty.IsSet { setList := meta.ListFromValue(p.Value) - setObjs := make([]sx.Object, len(setList)) + setObjs := make(sx.Vector, len(setList)) for i, val := range setList { setObjs[i] = sx.String(val) } - obj = sx.MakeList(setObjs...).Cons(t.zetSyms.SymList) + obj = sx.MakeList(setObjs...) } else if ty == meta.TypeZettelmarkup { is := evalMeta(p.Value) - obj = t.GetSz(&is) + obj = t.getInlineList(is) } else { obj = sx.String(p.Value) } - symKey := sx.MakeList(t.zetSyms.SymQuote, t.sf.MustMake(key)) - objs = append(objs, sx.Nil().Cons(obj).Cons(symKey).Cons(symType)) + objs = append(objs, sx.Nil().Cons(obj).Cons(sx.MakeSymbol(key)).Cons(symType)) } - return sx.MakeList(objs...).Cons(t.zetSyms.SymMeta) + return sx.MakeList(objs...).Cons(sz.SymMeta) } -func mapGetS[T comparable](t *Transformer, m map[T]*sx.Symbol, k T) *sx.Symbol { +func mapGetS[T comparable](m map[T]*sx.Symbol, k T) *sx.Symbol { if result, found := m[k]; found { return result } - return t.sf.MustMake(fmt.Sprintf("**%v:NOT-FOUND**", k)) + return sx.MakeSymbol(fmt.Sprintf("**%v:NOT-FOUND**", k)) } func getBase64String(data []byte) sx.String { var sb strings.Builder encoder := base64.NewEncoder(base64.StdEncoding, &sb) Index: encoder/textenc/textenc.go ================================================================== --- encoder/textenc/textenc.go +++ encoder/textenc/textenc.go @@ -4,10 +4,13 @@ // 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. +// +// SPDX-License-Identifier: EUPL-1.2 +// SPDX-FileCopyrightText: 2020-present Detlef Stern //----------------------------------------------------------------------------- // Package textenc encodes the abstract syntax tree into its text. package textenc Index: encoder/write.go ================================================================== --- encoder/write.go +++ encoder/write.go @@ -4,10 +4,13 @@ // 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. +// +// SPDX-License-Identifier: EUPL-1.2 +// SPDX-FileCopyrightText: 2020-present Detlef Stern //----------------------------------------------------------------------------- package encoder import ( Index: encoder/zmkenc/zmkenc.go ================================================================== --- encoder/zmkenc/zmkenc.go +++ encoder/zmkenc/zmkenc.go @@ -4,10 +4,13 @@ // 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. +// +// SPDX-License-Identifier: EUPL-1.2 +// SPDX-FileCopyrightText: 2020-present Detlef Stern //----------------------------------------------------------------------------- // Package zmkenc encodes the abstract syntax tree back into Zettelmarkup. package zmkenc @@ -370,11 +373,11 @@ } if i < len(tn.Text)-1 { s := tn.Text[i : i+2] if escapeSeqs.Has(s) { v.b.WriteString(tn.Text[last:i]) - for j := 0; j < len(s); j++ { + for j := range len(s) { v.b.WriteBytes('\\', s[j]) } i++ last = i + 1 continue @@ -525,11 +528,11 @@ v.b.WriteByte('}') } func (v *visitor) writeEscaped(s string, toEscape byte) { last := 0 - for i := 0; i < len(s); i++ { + for i := range len(s) { if b := s[i]; b == toEscape || b == '\\' { v.b.WriteString(s[last:i]) v.b.WriteBytes('\\', b) last = i + 1 } Index: encoding/atom/atom.go ================================================================== --- encoding/atom/atom.go +++ encoding/atom/atom.go @@ -4,10 +4,13 @@ // 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. +// +// SPDX-License-Identifier: EUPL-1.2 +// SPDX-FileCopyrightText: 2022-present Detlef Stern //----------------------------------------------------------------------------- // Package atom provides an Atom encoding. package atom @@ -79,11 +82,11 @@ if published, err := time.ParseInLocation(id.TimestampLayout, val, time.Local); err == nil { entryUpdated = published.UTC().Format(time.RFC3339) } } - link := c.NewURLBuilderAbs().SetZid(api.ZettelID(m.Zid.String())).String() + link := c.NewURLBuilderAbs().SetZid(m.Zid.ZettelID()).String() buf.WriteString(" \n") xml.WriteTag(buf, " ", "title", encoding.TitleAsText(m)) xml.WriteTag(buf, " ", "id", link) buf.WriteString(` \n") xml.WriteTag(buf, " ", "title", encoding.TitleAsText(m)) xml.WriteTag(buf, " ", "link", link) xml.WriteTag(buf, " ", "guid", link) Index: encoding/xml/xml.go ================================================================== --- encoding/xml/xml.go +++ encoding/xml/xml.go @@ -4,10 +4,13 @@ // 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. +// +// SPDX-License-Identifier: EUPL-1.2 +// SPDX-FileCopyrightText: 2022-present Detlef Stern //----------------------------------------------------------------------------- // Package xml provides helper for a XML-based encoding. package xml Index: evaluator/evaluator.go ================================================================== --- evaluator/evaluator.go +++ evaluator/evaluator.go @@ -4,29 +4,35 @@ // 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. +// +// SPDX-License-Identifier: EUPL-1.2 +// SPDX-FileCopyrightText: 2021-present Detlef Stern //----------------------------------------------------------------------------- // Package evaluator interprets and evaluates the AST. package evaluator import ( + "bytes" "context" "errors" "fmt" "path" "strconv" "strings" "zettelstore.de/client.fossil/api" "zettelstore.de/client.fossil/attrs" + "zettelstore.de/client.fossil/input" + "zettelstore.de/sx.fossil/sxbuiltins" + "zettelstore.de/sx.fossil/sxreader" "zettelstore.de/z/ast" "zettelstore.de/z/box" "zettelstore.de/z/config" - "zettelstore.de/z/input" "zettelstore.de/z/parser" "zettelstore.de/z/parser/cleaner" "zettelstore.de/z/parser/draw" "zettelstore.de/z/query" "zettelstore.de/z/zettel" @@ -41,16 +47,45 @@ } // 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) { - if zn.Syntax == meta.SyntaxNone { + switch zn.Syntax { + case meta.SyntaxNone: // AST is empty, evaluate to a description list of metadata. zn.Ast = evaluateMetadata(zn.Meta) - return + case meta.SyntaxSxn: + zn.Ast = evaluateSxn(zn.Ast) + default: + EvaluateBlock(ctx, port, rtConfig, &zn.Ast) + } +} + +func evaluateSxn(bs ast.BlockSlice) ast.BlockSlice { + // Check for structure made in parser/plain/plain.go:parseSxnBlocks + if len(bs) == 1 { + // If len(bs) > 1 --> an error was found during parsing + if vn, isVerbatim := bs[0].(*ast.VerbatimNode); isVerbatim && vn.Kind == ast.VerbatimProg { + if classAttr, hasClass := vn.Attrs.Get(""); hasClass && classAttr == meta.SyntaxSxn { + rd := sxreader.MakeReader(bytes.NewReader(vn.Content)) + if objs, err := rd.ReadAll(); err == nil { + result := make(ast.BlockSlice, len(objs)) + for i, obj := range objs { + var buf bytes.Buffer + sxbuiltins.Print(&buf, obj) + result[i] = &ast.VerbatimNode{ + Kind: ast.VerbatimProg, + Attrs: attrs.Attributes{"": classAttr}, + Content: buf.Bytes(), + } + } + return result + } + } + } } - EvaluateBlock(ctx, port, rtConfig, &zn.Ast) + return bs } // 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) { @@ -259,11 +294,11 @@ if errors.Is(err, &box.ErrNotAllowed{}) { return nil } return makeBlockNode(createInlineErrorText(nil, "Unable", "to", "search", "zettel")) } - result := QueryAction(e.ctx, q, ml, e.rtConfig) + result, _ := QueryAction(e.ctx, q, ml, e.rtConfig) if result != nil { ast.Walk(e, result) } return result } @@ -405,11 +440,11 @@ } e.transcludeCount++ return createInlineErrorImage(en) } - if syntax := zettel.Meta.GetDefault(api.KeySyntax, ""); parser.IsImageFormat(syntax) { + if syntax := zettel.Meta.GetDefault(api.KeySyntax, meta.DefaultSyntax); parser.IsImageFormat(syntax) { e.updateImageRefNode(en, zettel.Meta, syntax) return en } else if !parser.IsASTParser(syntax) { // Not embeddable. e.transcludeCount++ @@ -534,11 +569,11 @@ ast.Walk(e, &is) return is } func (e *evaluator) evaluateEmbeddedZettel(zettel zettel.Zettel) *ast.ZettelNode { - zn := parser.ParseZettel(e.ctx, zettel, zettel.Meta.GetDefault(api.KeySyntax, ""), e.rtConfig) + zn := parser.ParseZettel(e.ctx, zettel, zettel.Meta.GetDefault(api.KeySyntax, meta.DefaultSyntax), e.rtConfig) ast.Walk(e, &zn.Ast) return zn } func findInlineSlice(bs *ast.BlockSlice, fragment string) ast.InlineSlice { Index: evaluator/list.go ================================================================== --- evaluator/list.go +++ evaluator/list.go @@ -4,10 +4,13 @@ // 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. +// +// SPDX-License-Identifier: EUPL-1.2 +// SPDX-FileCopyrightText: 2022-present Detlef Stern //----------------------------------------------------------------------------- package evaluator import ( @@ -28,11 +31,11 @@ "zettelstore.de/z/query" "zettelstore.de/z/zettel/meta" ) // 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 { +func QueryAction(ctx context.Context, q *query.Query, ml []*meta.Meta, rtConfig config.Config) (ast.BlockNode, int) { ap := actionPara{ ctx: ctx, q: q, ml: ml, kind: ast.NestedListUnordered, @@ -45,57 +48,61 @@ return ap.createBlockNodeMeta("") } acts := make([]string, 0, len(actions)) for i, act := range actions { - if strings.HasPrefix(act, "N") { + if strings.HasPrefix(act, api.NumberedAction[0:1]) { ap.kind = ast.NestedListOrdered continue } - if strings.HasPrefix(act, "MIN") { + if strings.HasPrefix(act, api.MinAction) { if num, err := strconv.Atoi(act[3:]); err == nil && num > 0 { ap.min = num continue } } - if strings.HasPrefix(act, "MAX") { + if strings.HasPrefix(act, api.MaxAction) { if num, err := strconv.Atoi(act[3:]); err == nil && num > 0 { ap.max = num continue } } - if act == "TITLE" && i+1 < len(actions) { + if act == api.TitleAction && i+1 < len(actions) { ap.title = strings.Join(actions[i+1:], " ") break } - if act == "REINDEX" { + if act == api.ReIndexAction { continue } acts = append(acts, act) } - var firstUnknownKey string + var firstUnknowAct string for _, act := range acts { switch act { - case "ATOM": + case api.AtomAction: return ap.createBlockNodeAtom(rtConfig) - case "RSS": + case api.RSSAction: return ap.createBlockNodeRSS(rtConfig) - case "KEYS": + case api.KeysAction: return ap.createBlockNodeMetaKeys() } key := strings.ToLower(act) switch meta.Type(key) { case meta.TypeWord: return ap.createBlockNodeWord(key) case meta.TypeTagSet: return ap.createBlockNodeTagSet(key) } - if firstUnknownKey == "" { - firstUnknownKey = key + if firstUnknowAct == "" { + firstUnknowAct = act } } - return ap.createBlockNodeMeta(firstUnknownKey) + bn, numItems := ap.createBlockNodeMeta(strings.ToLower(firstUnknowAct)) + if bn != nil && numItems == 0 && firstUnknowAct == strings.ToUpper(firstUnknowAct) { + bn, numItems = ap.createBlockNodeMeta("") + } + return bn, numItems } type actionPara struct { ctx context.Context q *query.Query @@ -104,15 +111,15 @@ min int max int title string } -func (ap *actionPara) createBlockNodeWord(key string) ast.BlockNode { +func (ap *actionPara) createBlockNodeWord(key string) (ast.BlockNode, int) { var buf bytes.Buffer ccs, bufLen := ap.prepareCatAction(key, &buf) if len(ccs) == 0 { - return nil + return nil, 0 } items := make([]ast.ItemSlice, 0, len(ccs)) ccs.SortByName() for _, cat := range ccs { buf.WriteString(cat.Name) @@ -125,18 +132,18 @@ } return &ast.NestedListNode{ Kind: ap.kind, Items: items, Attrs: nil, - } + }, len(items) } -func (ap *actionPara) createBlockNodeTagSet(key string) ast.BlockNode { +func (ap *actionPara) createBlockNodeTagSet(key string) (ast.BlockNode, int) { var buf bytes.Buffer ccs, bufLen := ap.prepareCatAction(key, &buf) if len(ccs) == 0 { - return nil + return nil, 0 } ccs.SortByCount() ccs = ap.limitTags(ccs) countMap := ap.calcFontSizes(ccs) @@ -161,11 +168,11 @@ Inlines: ast.InlineSlice{&ast.TextNode{Text: strconv.Itoa(cat.Count)}}, }, ) buf.Truncate(bufLen) } - return &ast.ParaNode{Inlines: para} + return &ast.ParaNode{Inlines: para}, len(ccs) } func (ap *actionPara) limitTags(ccs meta.CountedCategories) meta.CountedCategories { if min, max := ap.min, ap.max; min > 0 || max > 0 { if min < 0 { @@ -185,19 +192,19 @@ } } return ccs } -func (ap *actionPara) createBlockNodeMetaKeys() ast.BlockNode { +func (ap *actionPara) createBlockNodeMetaKeys() (ast.BlockNode, int) { arr := make(meta.Arrangement, 128) for _, m := range ap.ml { for k := range m.Map() { arr[k] = append(arr[k], m) } } if len(arr) == 0 { - return nil + return nil, 0 } ccs := arr.Counted() ccs.SortByName() var buf bytes.Buffer @@ -231,16 +238,16 @@ } return &ast.NestedListNode{ Kind: ap.kind, Items: items, Attrs: nil, - } + }, len(items) } -func (ap *actionPara) createBlockNodeMeta(key string) ast.BlockNode { +func (ap *actionPara) createBlockNodeMeta(key string) (ast.BlockNode, int) { if len(ap.ml) == 0 { - return nil + return nil, 0 } items := make([]ast.ItemSlice, 0, len(ap.ml)) for _, m := range ap.ml { if key != "" { if _, found := m.Get(key); !found { @@ -255,11 +262,11 @@ } return &ast.NestedListNode{ Kind: ap.kind, Items: items, Attrs: nil, - } + }, len(items) } func (ap *actionPara) prepareCatAction(key string, buf *bytes.Buffer) (meta.CountedCategories, int) { if len(ap.ml) == 0 { return nil, 0 @@ -292,11 +299,11 @@ const fontSizes64 = float64(fontSizes) 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++ { + for i := range fontSizes { fsAttrs[i] = a.AddClass("zs-font-size-" + strconv.Itoa(i)) } countMap := make(map[int]int, len(ccs)) for _, cat := range ccs { @@ -342,30 +349,30 @@ return result } func calcBudget(total, curSize float64) float64 { return math.Round(total / (fontSizes64 - curSize)) } -func (ap *actionPara) createBlockNodeRSS(cfg config.Config) ast.BlockNode { +func (ap *actionPara) createBlockNodeRSS(cfg config.Config) (ast.BlockNode, int) { var rssConfig rss.Configuration rssConfig.Setup(ap.ctx, cfg) rssConfig.Title = ap.title data := rssConfig.Marshal(ap.q, ap.ml) return &ast.VerbatimNode{ Kind: ast.VerbatimProg, Attrs: attrs.Attributes{"lang": "xml"}, Content: data, - } + }, len(ap.ml) } -func (ap *actionPara) createBlockNodeAtom(cfg config.Config) ast.BlockNode { +func (ap *actionPara) createBlockNodeAtom(cfg config.Config) (ast.BlockNode, int) { var atomConfig atom.Configuration atomConfig.Setup(cfg) atomConfig.Title = ap.title data := atomConfig.Marshal(ap.q, ap.ml) return &ast.VerbatimNode{ Kind: ast.VerbatimProg, Attrs: attrs.Attributes{"lang": "xml"}, Content: data, - } + }, len(ap.ml) } Index: evaluator/metadata.go ================================================================== --- evaluator/metadata.go +++ evaluator/metadata.go @@ -4,10 +4,13 @@ // 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. +// +// SPDX-License-Identifier: EUPL-1.2 +// SPDX-FileCopyrightText: 2021-present Detlef Stern //----------------------------------------------------------------------------- package evaluator import ( Index: go.mod ================================================================== --- go.mod +++ go.mod @@ -1,15 +1,15 @@ module zettelstore.de/z -go 1.21 +go 1.22 require ( github.com/fsnotify/fsnotify v1.7.0 - github.com/yuin/goldmark v1.6.0 - golang.org/x/crypto v0.16.0 - golang.org/x/term v0.15.0 + github.com/yuin/goldmark v1.7.0 + golang.org/x/crypto v0.20.0 + golang.org/x/term v0.17.0 golang.org/x/text v0.14.0 - zettelstore.de/client.fossil v0.0.0-20231130151508-751754d40c73 - zettelstore.de/sx.fossil v0.0.0-20231130150648-05ef116ba207 + zettelstore.de/client.fossil v0.0.0-20240304164340-1f9d9b832cdd + zettelstore.de/sx.fossil v0.0.0-20240304124557-67e0a1799d1d ) -require golang.org/x/sys v0.15.0 // indirect +require golang.org/x/sys v0.17.0 // indirect Index: go.sum ================================================================== --- go.sum +++ go.sum @@ -1,16 +1,16 @@ github.com/fsnotify/fsnotify v1.7.0 h1:8JEhPFa5W2WU7YfeZzPNqzMP6Lwt7L2715Ggo0nosvA= github.com/fsnotify/fsnotify v1.7.0/go.mod h1:40Bi/Hjc2AVfZrqy+aj+yEI+/bRxZnMJyTJwOpGvigM= -github.com/yuin/goldmark v1.6.0 h1:boZcn2GTjpsynOsC0iJHnBWa4Bi0qzfJjthwauItG68= -github.com/yuin/goldmark v1.6.0/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY= -golang.org/x/crypto v0.16.0 h1:mMMrFzRSCF0GvB7Ne27XVtVAaXLrPmgPC7/v0tkwHaY= -golang.org/x/crypto v0.16.0/go.mod h1:gCAAfMLgwOJRpTjQ2zCCt2OcSfYMTeZVSRtQlPC7Nq4= -golang.org/x/sys v0.15.0 h1:h48lPFYpsTvQJZF4EKyI4aLHaev3CxivZmv7yZig9pc= -golang.org/x/sys v0.15.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= -golang.org/x/term v0.15.0 h1:y/Oo/a/q3IXu26lQgl04j/gjuBDOBlx7X6Om1j2CPW4= -golang.org/x/term v0.15.0/go.mod h1:BDl952bC7+uMoWR75FIrCDx79TPU9oHkTZ9yRbYOrX0= +github.com/yuin/goldmark v1.7.0 h1:EfOIvIMZIzHdB/R/zVrikYLPPwJlfMcNczJFMs1m6sA= +github.com/yuin/goldmark v1.7.0/go.mod h1:uzxRWxtg69N339t3louHJ7+O03ezfj6PlliRlaOzY1E= +golang.org/x/crypto v0.20.0 h1:jmAMJJZXr5KiCw05dfYK9QnqaqKLYXijU23lsEdcQqg= +golang.org/x/crypto v0.20.0/go.mod h1:Xwo95rrVNIoSMx9wa1JroENMToLWn3RNVrTBpLHgZPQ= +golang.org/x/sys v0.17.0 h1:25cE3gD+tdBA7lp7QfhuV+rJiE9YXTcS3VG1SqssI/Y= +golang.org/x/sys v0.17.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= +golang.org/x/term v0.17.0 h1:mkTF7LCd6WGJNL3K1Ad7kwxNfYAW6a8a8QqtMblp/4U= +golang.org/x/term v0.17.0/go.mod h1:lLRBjIVuehSbZlaOtGMbcMncT+aqLLLmKrsjNrUguwk= golang.org/x/text v0.14.0 h1:ScX5w1eTa3QqT8oi6+ziP7dTV1S2+ALU0bI+0zXKWiQ= golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU= -zettelstore.de/client.fossil v0.0.0-20231130151508-751754d40c73 h1:LPF8QWip3GRLBzgPdnsJnrvk1sGiMOSpqOwURiax9gg= -zettelstore.de/client.fossil v0.0.0-20231130151508-751754d40c73/go.mod h1:fN+1WxRorSbHduS0T0B4GI8o82EgFuUWBv6fwsAZF6o= -zettelstore.de/sx.fossil v0.0.0-20231130150648-05ef116ba207 h1:8ch54z0w53bps6a00NDofEqo3AJ1l7ITXyC3XyLmlY4= -zettelstore.de/sx.fossil v0.0.0-20231130150648-05ef116ba207/go.mod h1:Uw3OLM1ufOM4Xe0G51mvkTDUv2okd+HyDBMx+0ZG7ME= +zettelstore.de/client.fossil v0.0.0-20240304164340-1f9d9b832cdd h1:+LUJqi1mvXo/zM9Ii64hcGd1LD3oC8kh5yrmw2fFoco= +zettelstore.de/client.fossil v0.0.0-20240304164340-1f9d9b832cdd/go.mod h1:y5zhvVuDHJKFcySEe70537w+5RL50jpeZjqyQuBjfa0= +zettelstore.de/sx.fossil v0.0.0-20240304124557-67e0a1799d1d h1:Gl5ZmdNV5wJsNMIQYjAd/sWLq2ng4NP+eglWU7lQP+I= +zettelstore.de/sx.fossil v0.0.0-20240304124557-67e0a1799d1d/go.mod h1:/iGHxFXoo6GSV04PUkwaLuFrrCa5LMorxD73iLMAruI= DELETED input/entity.go Index: input/entity.go ================================================================== --- input/entity.go +++ /dev/null @@ -1,159 +0,0 @@ -//----------------------------------------------------------------------------- -// Copyright (c) 2022-present 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 input - -import ( - "html" - "unicode" -) - -// ScanEntity scans either a named or a numbered entity and returns it as a string. -// -// For numbered entities (like { or ģ) html.UnescapeString returns -// sometimes other values as expected, if the number is not well-formed. This -// may happen because of some strange HTML parsing rules. But these do not -// apply to Zettelmarkup. Therefore, I parse the number here in the code. -func (inp *Input) ScanEntity() (res string, success bool) { - if inp.Ch != '&' { - return "", false - } - pos := inp.Pos - inp.Next() - if inp.Ch == '#' { - inp.Next() - if inp.Ch == 'x' || inp.Ch == 'X' { - return inp.scanEntityBase16() - } - return inp.scanEntityBase10() - } - return inp.scanEntityNamed(pos) -} - -func (inp *Input) scanEntityBase16() (string, bool) { - inp.Next() - if inp.Ch == ';' { - return "", false - } - code := 0 - for { - switch ch := inp.Ch; ch { - case ';': - inp.Next() - if r := rune(code); isValidEntity(r) { - return string(r), true - } - return "", false - case '0', '1', '2', '3', '4', '5', '6', '7', '8', '9': - code = 16*code + int(ch-'0') - case 'a', 'b', 'c', 'd', 'e', 'f': - code = 16*code + int(ch-'a'+10) - case 'A', 'B', 'C', 'D', 'E', 'F': - code = 16*code + int(ch-'A'+10) - default: - return "", false - } - if code > unicode.MaxRune { - return "", false - } - inp.Next() - } -} - -func (inp *Input) scanEntityBase10() (string, bool) { - // Base 10 code - if inp.Ch == ';' { - return "", false - } - code := 0 - for { - switch ch := inp.Ch; ch { - case ';': - inp.Next() - if r := rune(code); isValidEntity(r) { - return string(r), true - } - return "", false - case '0', '1', '2', '3', '4', '5', '6', '7', '8', '9': - code = 10*code + int(ch-'0') - default: - return "", false - } - if code > unicode.MaxRune { - return "", false - } - inp.Next() - } -} -func (inp *Input) scanEntityNamed(pos int) (string, bool) { - for { - switch inp.Ch { - case EOS, '\n', '\r', '&': - return "", false - case ';': - inp.Next() - es := string(inp.Src[pos:inp.Pos]) - ues := html.UnescapeString(es) - if es == ues { - return "", false - } - return ues, true - default: - inp.Next() - } - } -} - -// isValidEntity checks if the given code is valid for an entity. -// -// According to https://html.spec.whatwg.org/multipage/syntax.html#character-references -// ""The numeric character reference forms described above are allowed to reference any code point -// excluding U+000D CR, noncharacters, and controls other than ASCII whitespace."" -func isValidEntity(r rune) bool { - // No C0 control and no "code point in the range U+007F DELETE to U+009F APPLICATION PROGRAM COMMAND, inclusive." - if r < ' ' || ('\u007f' <= r && r <= '\u009f') { - return false - } - - // If below any noncharacter code point, return true - // - // See: https://infra.spec.whatwg.org/#noncharacter - if r < '\ufdd0' { - return true - } - - // First range of noncharacter code points: "(...) in the range U+FDD0 to U+FDEF, inclusive" - if r <= '\ufdef' { - return false - } - - // Other noncharacter code points: - switch r { - case '\uFFFE', '\uFFFF', - '\U0001FFFE', '\U0001FFFF', - '\U0002FFFE', '\U0002FFFF', - '\U0003FFFE', '\U0003FFFF', - '\U0004FFFE', '\U0004FFFF', - '\U0005FFFE', '\U0005FFFF', - '\U0006FFFE', '\U0006FFFF', - '\U0007FFFE', '\U0007FFFF', - '\U0008FFFE', '\U0008FFFF', - '\U0009FFFE', '\U0009FFFF', - '\U000AFFFE', '\U000AFFFF', - '\U000BFFFE', '\U000BFFFF', - '\U000CFFFE', '\U000CFFFF', - '\U000DFFFE', '\U000DFFFF', - '\U000EFFFE', '\U000EFFFF', - '\U000FFFFE', '\U000FFFFF', - '\U0010FFFE', '\U0010FFFF': - return false - } - return true -} DELETED input/entity_test.go Index: input/entity_test.go ================================================================== --- input/entity_test.go +++ /dev/null @@ -1,61 +0,0 @@ -//----------------------------------------------------------------------------- -// Copyright (c) 2020-present 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 input_test - -import ( - "testing" - - "zettelstore.de/z/input" -) - -func TestScanEntity(t *testing.T) { - t.Parallel() - var testcases = []struct { - text string - exp string - }{ - {"", ""}, - {"a", ""}, - {"&", "&"}, - {"!", "!"}, - {"3", "3"}, - {""", "\""}, - } - for id, tc := range testcases { - inp := input.NewInput([]byte(tc.text)) - got, ok := inp.ScanEntity() - if !ok { - if tc.exp != "" { - t.Errorf("ID=%d, text=%q: expected error, but got %q", id, tc.text, got) - } - if inp.Pos != 0 { - t.Errorf("ID=%d, text=%q: input position advances to %d", id, tc.text, inp.Pos) - } - continue - } - if tc.exp != got { - t.Errorf("ID=%d, text=%q: expected %q, but got %q", id, tc.text, tc.exp, got) - } - } -} - -func TestScanIllegalEntity(t *testing.T) { - t.Parallel() - testcases := []string{"", "a", "& Input →", " ", ""} - for i, tc := range testcases { - inp := input.NewInput([]byte(tc)) - got, ok := inp.ScanEntity() - if ok { - t.Errorf("%d: scanning %q was unexpected successful, got %q", i, tc, got) - continue - } - } -} DELETED input/input.go Index: input/input.go ================================================================== --- input/input.go +++ /dev/null @@ -1,153 +0,0 @@ -//----------------------------------------------------------------------------- -// Copyright (c) 2020-present 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 input provides an abstraction for data to be read. -package input - -import "unicode/utf8" - -// Input is an abstract input source -type Input struct { - // Read-only, will never change - Src []byte // The source string - - // Read-only, will change - Ch rune // current character - Pos int // character position in src - readPos int // reading position (position after current character) -} - -// NewInput creates a new input source. -func NewInput(src []byte) *Input { - inp := &Input{Src: src} - inp.Next() - return inp -} - -// EOS = End of source -const EOS = rune(-1) - -// Next reads the next rune into inp.Ch and returns it too. -func (inp *Input) Next() rune { - if inp.readPos >= len(inp.Src) { - inp.Pos = len(inp.Src) - inp.Ch = EOS - return EOS - } - inp.Pos = inp.readPos - r, w := rune(inp.Src[inp.readPos]), 1 - if r >= utf8.RuneSelf { - r, w = utf8.DecodeRune(inp.Src[inp.readPos:]) - } - inp.readPos += w - inp.Ch = r - return r -} - -// Peek returns the rune following the most recently read rune without -// advancing. If end-of-source was already found peek returns EOS. -func (inp *Input) Peek() rune { - return inp.PeekN(0) -} - -// PeekN returns the n-th rune after the most recently read rune without -// advancing. If end-of-source was already found peek returns EOS. -func (inp *Input) PeekN(n int) rune { - pos := inp.readPos + n - if pos < len(inp.Src) { - r := rune(inp.Src[pos]) - if r >= utf8.RuneSelf { - r, _ = utf8.DecodeRune(inp.Src[pos:]) - } - if r == '\t' { - return ' ' - } - return r - } - return EOS -} - -// Accept checks if the given string is a prefix of the text to be parsed. -// If successful, advance position and current character. -// String must only contain bytes < 128. -// If not successful, everything remains as it is. -func (inp *Input) Accept(s string) bool { - pos := inp.Pos - remaining := len(inp.Src) - pos - if s == "" || len(s) > remaining { - return false - } - // According to internal documentation of bytes.Equal, the string() will not allocate any memory. - if readPos := pos + len(s); s == string(inp.Src[pos:readPos]) { - inp.readPos = readPos - inp.Next() - return true - } - return false -} - -// IsEOLEOS returns true if char is either EOS or EOL. -func IsEOLEOS(ch rune) bool { - switch ch { - case EOS, '\n', '\r': - return true - } - return false -} - -// EatEOL transforms both "\r" and "\r\n" into "\n". -func (inp *Input) EatEOL() { - switch inp.Ch { - case '\r': - if inp.Peek() == '\n' { - inp.Next() - } - inp.Ch = '\n' - inp.Next() - case '\n': - inp.Next() - } -} - -// SetPos allows to reset the read position. -func (inp *Input) SetPos(pos int) { - if inp.Pos != pos { - inp.readPos = pos - inp.Next() - } -} - -// SkipToEOL reads until the next end-of-line. -func (inp *Input) SkipToEOL() { - for { - switch inp.Ch { - case EOS, '\n', '\r': - return - } - inp.Next() - } -} - -// ScanLineContent reads the reaining input stream and interprets it as lines of text. -func (inp *Input) ScanLineContent() []byte { - result := make([]byte, 0, len(inp.Src)-inp.Pos+1) - for { - inp.EatEOL() - posL := inp.Pos - if inp.Ch == EOS { - return result - } - inp.SkipToEOL() - if len(result) > 0 { - result = append(result, '\n') - } - result = append(result, inp.Src[posL:inp.Pos]...) - } -} DELETED input/input_test.go Index: input/input_test.go ================================================================== --- input/input_test.go +++ /dev/null @@ -1,65 +0,0 @@ -//----------------------------------------------------------------------------- -// Copyright (c) 2020-present 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 input_test provides some unit-tests for reading data. -package input_test - -import ( - "testing" - - "zettelstore.de/z/input" -) - -func TestEatEOL(t *testing.T) { - t.Parallel() - inp := input.NewInput(nil) - inp.EatEOL() - if inp.Ch != input.EOS { - t.Errorf("No EOS found: %q", inp.Ch) - } - if inp.Pos != 0 { - t.Errorf("Pos != 0: %d", inp.Pos) - } - - inp = input.NewInput([]byte("ABC")) - if inp.Ch != 'A' { - t.Errorf("First ch != 'A', got %q", inp.Ch) - } - inp.EatEOL() - if inp.Ch != 'A' { - t.Errorf("First ch != 'A', got %q", inp.Ch) - } -} - -func TestAccept(t *testing.T) { - t.Parallel() - testcases := []struct { - accept string - src string - acc bool - exp rune - }{ - {"", "", false, input.EOS}, - {"AB", "abc", false, 'a'}, - {"AB", "ABC", true, 'C'}, - {"AB", "AB", true, input.EOS}, - {"AB", "A", false, 'A'}, - } - for i, tc := range testcases { - inp := input.NewInput([]byte(tc.src)) - acc := inp.Accept(tc.accept) - if acc != tc.acc { - t.Errorf("%d: %q.Accept(%q) == %v, but got %v", i, tc.src, tc.accept, tc.acc, acc) - } - if got := inp.Ch; tc.exp != got { - t.Errorf("%d: %q.Accept(%q) should result in run %v, but got %v", i, tc.src, tc.accept, tc.exp, got) - } - } -} DELETED input/runes.go Index: input/runes.go ================================================================== --- input/runes.go +++ /dev/null @@ -1,24 +0,0 @@ -//----------------------------------------------------------------------------- -// Copyright (c) 2020-present 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 input - -import "unicode" - -// IsSpace returns true if rune is a whitespace. -func IsSpace(ch rune) bool { - switch ch { - case ' ', '\t': - return true - case '\n', '\r', EOS: - return false - } - return unicode.IsSpace(ch) -} Index: kernel/impl/auth.go ================================================================== --- kernel/impl/auth.go +++ kernel/impl/auth.go @@ -4,10 +4,13 @@ // 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. +// +// SPDX-License-Identifier: EUPL-1.2 +// SPDX-FileCopyrightText: 2021-present Detlef Stern //----------------------------------------------------------------------------- package impl import ( @@ -70,11 +73,11 @@ defer as.mxService.Unlock() readonlyMode := as.GetNextConfig(kernel.AuthReadonly).(bool) owner := as.GetNextConfig(kernel.AuthOwner).(id.Zid) authMgr, err := as.createManager(readonlyMode, owner) if err != nil { - as.logger.Fatal().Err(err).Msg("Unable to create manager") + as.logger.Error().Err(err).Msg("Unable to create manager") return err } as.logger.Info().Msg("Start Manager") as.manager = authMgr return nil Index: kernel/impl/box.go ================================================================== --- kernel/impl/box.go +++ kernel/impl/box.go @@ -4,10 +4,13 @@ // 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. +// +// SPDX-License-Identifier: EUPL-1.2 +// SPDX-FileCopyrightText: 2021-present Detlef Stern //----------------------------------------------------------------------------- package impl import ( @@ -80,16 +83,16 @@ } ps.mxService.Lock() defer ps.mxService.Unlock() mgr, err := ps.createManager(boxURIs, kern.auth.manager, &kern.cfg) if err != nil { - ps.logger.Fatal().Err(err).Msg("Unable to create manager") + ps.logger.Error().Err(err).Msg("Unable to create manager") return err } ps.logger.Info().Str("location", mgr.Location()).Msg("Start Manager") if err = mgr.Start(context.Background()); err != nil { - ps.logger.Fatal().Err(err).Msg("Unable to start manager") + ps.logger.Error().Err(err).Msg("Unable to start manager") return err } kern.cfg.setBox(mgr) ps.manager = mgr return nil Index: kernel/impl/cfg.go ================================================================== --- kernel/impl/cfg.go +++ kernel/impl/cfg.go @@ -4,10 +4,13 @@ // 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. +// +// SPDX-License-Identifier: EUPL-1.2 +// SPDX-FileCopyrightText: 2021-present Detlef Stern //----------------------------------------------------------------------------- package impl import ( Index: kernel/impl/cmd.go ================================================================== --- kernel/impl/cmd.go +++ kernel/impl/cmd.go @@ -4,10 +4,13 @@ // 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. +// +// SPDX-License-Identifier: EUPL-1.2 +// SPDX-FileCopyrightText: 2021-present Detlef Stern //----------------------------------------------------------------------------- package impl import ( Index: kernel/impl/config.go ================================================================== --- kernel/impl/config.go +++ kernel/impl/config.go @@ -4,10 +4,13 @@ // 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. +// +// SPDX-License-Identifier: EUPL-1.2 +// SPDX-FileCopyrightText: 2021-present Detlef Stern //----------------------------------------------------------------------------- package impl import ( Index: kernel/impl/core.go ================================================================== --- kernel/impl/core.go +++ kernel/impl/core.go @@ -4,10 +4,13 @@ // 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. +// +// SPDX-License-Identifier: EUPL-1.2 +// SPDX-FileCopyrightText: 2021-present Detlef Stern //----------------------------------------------------------------------------- package impl import ( Index: kernel/impl/impl.go ================================================================== --- kernel/impl/impl.go +++ kernel/impl/impl.go @@ -4,10 +4,13 @@ // 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. +// +// SPDX-License-Identifier: EUPL-1.2 +// SPDX-FileCopyrightText: 2021-present Detlef Stern //----------------------------------------------------------------------------- // Package impl provides the kernel implementation. package impl @@ -69,11 +72,11 @@ } type serviceDependency map[kernel.Service][]kernel.Service const ( defaultNormalLogLevel = logger.InfoLevel - defaultSimpleLogLevel = logger.WarnLevel + defaultSimpleLogLevel = logger.ErrorLevel ) // create a new kernel. func init() { kernel.Main = createKernel() @@ -97,11 +100,11 @@ kernel.WebService: {&kern.web, "web", defaultNormalLogLevel}, } kern.srvNames = make(map[string]serviceData, len(kern.srvs)) for key, srvD := range kern.srvs { if _, ok := kern.srvNames[srvD.name]; ok { - kern.logger.Panic().Str("service", srvD.name).Msg("Service data already set") + kern.logger.Error().Str("service", srvD.name).Msg("Service data already set, ignore") } kern.srvNames[srvD.name] = serviceData{srvD.srv, key} l := logger.New(lw, strings.ToUpper(srvD.name)).SetLevel(srvD.logLevel) kern.logger.Debug().Str("service", srvD.name).Msg("Initialize") srvD.srv.Initialize(l) @@ -164,13 +167,13 @@ logger.Mandatory().Str("filename", configFilename).Msg("Configuration file found") } else { logger.Mandatory().Msg("No configuration file found / used") } if kern.core.GetCurConfig(kernel.CoreDebug).(bool) { - logger.Warn().Msg("----------------------------------------") - logger.Warn().Msg("DEBUG MODE, DO NO USE THIS IN PRODUCTION") - logger.Warn().Msg("----------------------------------------") + logger.Info().Msg("----------------------------------------") + logger.Info().Msg("DEBUG MODE, DO NO USE THIS IN PRODUCTION") + logger.Info().Msg("----------------------------------------") } if kern.auth.GetCurConfig(kernel.AuthReadonly).(bool) { logger.Info().Msg("Read-only mode") } } @@ -276,11 +279,11 @@ func (kern *myKernel) LogRecover(name string, recoverInfo interface{}) bool { return kern.doLogRecover(name, recoverInfo) } func (kern *myKernel) doLogRecover(name string, recoverInfo interface{}) bool { stack := debug.Stack() - kern.logger.Fatal().Str("recovered_from", fmt.Sprint(recoverInfo)).Bytes("stack", stack).Msg(name) + kern.logger.Error().Str("recovered_from", fmt.Sprint(recoverInfo)).Bytes("stack", stack).Msg(name) kern.core.updateRecoverInfo(name, recoverInfo, stack) return true } // --- Profiling --------------------------------------------------------- Index: kernel/impl/log.go ================================================================== --- kernel/impl/log.go +++ kernel/impl/log.go @@ -4,10 +4,13 @@ // 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. +// +// SPDX-License-Identifier: EUPL-1.2 +// SPDX-FileCopyrightText: 2021-present Detlef Stern //----------------------------------------------------------------------------- package impl import ( @@ -74,13 +77,10 @@ buf = append(buf, details...) buf = append(buf, '\n') _, err := os.Stdout.Write(buf) klw.mx.Unlock() - if level == logger.PanicLevel { - panic(err) - } return err } func addTimestamp(buf *[]byte, ts time.Time) { year, month, day := ts.Date() @@ -124,11 +124,11 @@ if !klw.full { if klw.writePos == 0 { return nil } result := make([]kernel.LogEntry, klw.writePos) - for i := 0; i < klw.writePos; i++ { + for i := range klw.writePos { copyE2E(&result[i], &klw.data[i]) } return result } result := make([]kernel.LogEntry, cap(klw.data)) @@ -135,11 +135,11 @@ pos := 0 for j := klw.writePos; j < cap(klw.data); j++ { copyE2E(&result[pos], &klw.data[j]) pos++ } - for j := 0; j < klw.writePos; j++ { + for j := range klw.writePos { copyE2E(&result[pos], &klw.data[j]) pos++ } return result } Index: kernel/impl/server.go ================================================================== --- kernel/impl/server.go +++ kernel/impl/server.go @@ -4,10 +4,13 @@ // 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. +// +// SPDX-License-Identifier: EUPL-1.2 +// SPDX-FileCopyrightText: 2021-present Detlef Stern //----------------------------------------------------------------------------- package impl import ( @@ -16,11 +19,11 @@ ) func startLineServer(kern *myKernel, listenAddr string) error { ln, err := net.Listen("tcp", listenAddr) if err != nil { - kern.logger.Fatal().Err(err).Msg("Unable to start administration console") + kern.logger.Error().Err(err).Msg("Unable to start administration console") return err } kern.logger.Mandatory().Str("listen", listenAddr).Msg("Start administration console") go func() { lineServer(ln, kern) }() return nil Index: kernel/impl/web.go ================================================================== --- kernel/impl/web.go +++ kernel/impl/web.go @@ -4,10 +4,13 @@ // 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. +// +// SPDX-License-Identifier: EUPL-1.2 +// SPDX-FileCopyrightText: 2021-present Detlef Stern //----------------------------------------------------------------------------- package impl import ( @@ -144,30 +147,30 @@ if maxRequestSize < 1024 { maxRequestSize = 1024 } if !strings.HasSuffix(baseURL, urlPrefix) { - ws.logger.Fatal().Str("base-url", baseURL).Str("url-prefix", urlPrefix).Msg( + ws.logger.Error().Str("base-url", baseURL).Str("url-prefix", urlPrefix).Msg( "url-prefix is not a suffix of base-url") return errWrongBasePrefix } if lap := netip.MustParseAddrPort(listenAddr); !kern.auth.manager.WithAuth() && !lap.Addr().IsLoopback() { - ws.logger.Warn().Str("listen", listenAddr).Msg("service may be reached from outside, but authentication is not enabled") + ws.logger.Info().Str("listen", listenAddr).Msg("service may be reached from outside, but authentication is not enabled") } 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") + ws.logger.Error().Err(err).Msg("Unable to create") return err } if kern.core.GetNextConfig(kernel.CoreDebug).(bool) { srvw.SetDebug() } if err = srvw.Run(); err != nil { - ws.logger.Fatal().Err(err).Msg("Unable to start") + ws.logger.Error().Err(err).Msg("Unable to start") return err } ws.logger.Info().Str("listen", listenAddr).Str("base-url", baseURL).Msg("Start Service") ws.mxService.Lock() ws.srvw = srvw Index: kernel/kernel.go ================================================================== --- kernel/kernel.go +++ kernel/kernel.go @@ -4,10 +4,13 @@ // 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. +// +// SPDX-License-Identifier: EUPL-1.2 +// SPDX-FileCopyrightText: 2021-present Detlef Stern //----------------------------------------------------------------------------- // Package kernel provides the main kernel service. package kernel Index: logger/logger.go ================================================================== --- logger/logger.go +++ logger/logger.go @@ -4,10 +4,13 @@ // 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. +// +// SPDX-License-Identifier: EUPL-1.2 +// SPDX-FileCopyrightText: 2021-present Detlef Stern //----------------------------------------------------------------------------- // Package logger implements a logging package for use in the Zettelstore. package logger @@ -27,44 +30,32 @@ // Constants for Level const ( NoLevel Level = iota // the absent log level TraceLevel // Log most internal activities DebugLevel // Log most data updates - SenseLevel // Log activities of minor interest InfoLevel // Log normal activities - WarnLevel // Log event that can be easily recovered ErrorLevel // Log (persistent) errors - FatalLevel // Log event that cannot be recovered within an internal acitivty - PanicLevel // Log event that must stop the software MandatoryLevel // Log only mandatory events NeverLevel // Logging is disabled ) var logLevel = [...]string{ " ", "TRACE", "DEBUG", - "SENSE", "INFO ", - "WARN ", "ERROR", - "FATAL", - "PANIC", ">>>>>", "NEVER", } var strLevel = [...]string{ "", "trace", "debug", - "sense", "info", - "warn", "error", - "fatal", - "panic", "mandatory", "disabled", } // IsValid returns true, if the level is a valid level @@ -169,28 +160,16 @@ func (l *Logger) Trace() *Message { return newMessage(l, TraceLevel) } // Debug creates a debug message. func (l *Logger) Debug() *Message { return newMessage(l, DebugLevel) } -// Sense creates a message suitable for sensing data. -func (l *Logger) Sense() *Message { return newMessage(l, SenseLevel) } - // Info creates a message suitable for information data. func (l *Logger) Info() *Message { return newMessage(l, InfoLevel) } -// Warn creates a message suitable for warning the user. -func (l *Logger) Warn() *Message { return newMessage(l, WarnLevel) } - // Error creates a message suitable for errors. func (l *Logger) Error() *Message { return newMessage(l, ErrorLevel) } -// Fatal creates a message suitable for fatal errors. -func (l *Logger) Fatal() *Message { return newMessage(l, FatalLevel) } - -// Panic creates a message suitable for panicing. -func (l *Logger) Panic() *Message { return newMessage(l, PanicLevel) } - // Mandatory creates a message that will always logged, except when logging // is disabled. func (l *Logger) Mandatory() *Message { return newMessage(l, MandatoryLevel) } // Clone creates a message to clone the logger. Index: logger/logger_test.go ================================================================== --- logger/logger_test.go +++ logger/logger_test.go @@ -4,10 +4,13 @@ // 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. +// +// SPDX-License-Identifier: EUPL-1.2 +// SPDX-FileCopyrightText: 2021-present Detlef Stern //----------------------------------------------------------------------------- package logger_test import ( @@ -25,14 +28,11 @@ exp logger.Level }{ {"tra", logger.TraceLevel}, {"deb", logger.DebugLevel}, {"info", logger.InfoLevel}, - {"warn", logger.WarnLevel}, {"err", logger.ErrorLevel}, - {"fata", logger.FatalLevel}, - {"pan", logger.PanicLevel}, {"manda", logger.MandatoryLevel}, {"dis", logger.NeverLevel}, {"d", logger.Level(0)}, } for i, tc := range testcases { @@ -43,11 +43,11 @@ } } func BenchmarkDisabled(b *testing.B) { log := logger.New(&stderrLogWriter{}, "").SetLevel(logger.NeverLevel) - for n := 0; n < b.N; n++ { + for range b.N { log.Info().Str("key", "val").Msg("Benchmark") } } type stderrLogWriter struct{} @@ -63,23 +63,23 @@ return nil } func BenchmarkStrMessage(b *testing.B) { log := logger.New(&testLogWriter{}, "") - for n := 0; n < b.N; n++ { + for range b.N { log.Info().Str("key", "val").Msg("Benchmark") } } func BenchmarkMessage(b *testing.B) { log := logger.New(&testLogWriter{}, "") - for n := 0; n < b.N; n++ { + for range b.N { log.Info().Msg("Benchmark") } } func BenchmarkCloneStrMessage(b *testing.B) { log := logger.New(&testLogWriter{}, "").Clone().Str("sss", "ttt").Child() - for n := 0; n < b.N; n++ { + for range b.N { log.Info().Msg("123456789") } } Index: logger/message.go ================================================================== --- logger/message.go +++ logger/message.go @@ -4,10 +4,13 @@ // 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. +// +// SPDX-License-Identifier: EUPL-1.2 +// SPDX-FileCopyrightText: 2022-present Detlef Stern //----------------------------------------------------------------------------- package logger import ( Index: parser/blob/blob.go ================================================================== --- parser/blob/blob.go +++ parser/blob/blob.go @@ -4,18 +4,21 @@ // 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. +// +// SPDX-License-Identifier: EUPL-1.2 +// SPDX-FileCopyrightText: 2020-present Detlef Stern //----------------------------------------------------------------------------- // Package blob provides a parser of binary data. package blob import ( + "zettelstore.de/client.fossil/input" "zettelstore.de/z/ast" - "zettelstore.de/z/input" "zettelstore.de/z/parser" "zettelstore.de/z/zettel/meta" ) func init() { Index: parser/cleaner/cleaner.go ================================================================== --- parser/cleaner/cleaner.go +++ parser/cleaner/cleaner.go @@ -4,10 +4,13 @@ // 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. +// +// SPDX-License-Identifier: EUPL-1.2 +// SPDX-FileCopyrightText: 2020-present Detlef Stern //----------------------------------------------------------------------------- // Package cleaner provides functions to clean up the parsed AST. package cleaner Index: parser/draw/canvas.go ================================================================== --- parser/draw/canvas.go +++ parser/draw/canvas.go @@ -13,10 +13,13 @@ // https://github.com/asciitosvg/asciitosvg (Commit: // ca82a5ce41e2190a05e07af6e8b3ea4e3256a283, 2020-11-20): // // Copyright 2012 - 2018 The ASCIIToSVG Contributors // All rights reserved. +// +// SPDX-License-Identifier: EUPL-1.2 +// SPDX-FileCopyrightText: 2022-present Detlef Stern //----------------------------------------------------------------------------- package draw import ( @@ -95,13 +98,13 @@ } // findPaths by starting with a point that wasn't yet visited, beginning at the top // left of the grid. func (c *canvas) findPaths() { - for y := 0; y < c.siz.Y; y++ { + for y := range c.siz.Y { p := point{y: y} - for x := 0; x < c.siz.X; x++ { + for x := range c.siz.X { p.x = x if c.isVisited(p) { continue } ch := c.at(p) @@ -125,14 +128,14 @@ } } // findTexts with a second pass through the grid attempts to identify any text within the grid. func (c *canvas) findTexts() { - for y := 0; y < c.siz.Y; y++ { + for y := range c.siz.Y { p := point{} p.y = y - for x := 0; x < c.siz.X; x++ { + for x := range c.siz.X { p.x = x if c.isVisited(p) { continue } ch := c.at(p) Index: parser/draw/canvas_test.go ================================================================== --- parser/draw/canvas_test.go +++ parser/draw/canvas_test.go @@ -13,10 +13,13 @@ // https://github.com/asciitosvg/asciitosvg (Commit: // ca82a5ce41e2190a05e07af6e8b3ea4e3256a283, 2020-11-20): // // Copyright 2012 - 2018 The ASCIIToSVG Contributors // All rights reserved. +// +// SPDX-License-Identifier: EUPL-1.2 +// SPDX-FileCopyrightText: 2022-present Detlef Stern //----------------------------------------------------------------------------- package draw import ( @@ -656,11 +659,11 @@ "", "", } chunk := []byte(strings.Join(data, "\n")) input := make([]byte, 0, len(chunk)*b.N) - for i := 0; i < b.N; i++ { + for range b.N { input = append(input, chunk...) } expected := 30 * b.N b.ResetTimer() c, err := newCanvas(input) Index: parser/draw/char.go ================================================================== --- parser/draw/char.go +++ parser/draw/char.go @@ -13,10 +13,13 @@ // https://github.com/asciitosvg/asciitosvg (Commit: // ca82a5ce41e2190a05e07af6e8b3ea4e3256a283, 2020-11-20): // // Copyright 2012 - 2018 The ASCIIToSVG Contributors // All rights reserved. +// +// SPDX-License-Identifier: EUPL-1.2 +// SPDX-FileCopyrightText: 2022-present Detlef Stern //----------------------------------------------------------------------------- package draw import "unicode" Index: parser/draw/draw.go ================================================================== --- parser/draw/draw.go +++ parser/draw/draw.go @@ -4,10 +4,13 @@ // 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. +// +// SPDX-License-Identifier: EUPL-1.2 +// SPDX-FileCopyrightText: 2022-present Detlef Stern //----------------------------------------------------------------------------- // Package draw provides a parser to create SVG from ASCII drawing. // // It is not a parser registered by the general parser framework (directed by @@ -16,12 +19,12 @@ import ( "strconv" "zettelstore.de/client.fossil/attrs" + "zettelstore.de/client.fossil/input" "zettelstore.de/z/ast" - "zettelstore.de/z/input" "zettelstore.de/z/parser" "zettelstore.de/z/zettel/meta" ) func init() { Index: parser/draw/draw_test.go ================================================================== --- parser/draw/draw_test.go +++ parser/draw/draw_test.go @@ -4,19 +4,22 @@ // 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. +// +// SPDX-License-Identifier: EUPL-1.2 +// SPDX-FileCopyrightText: 2022-present Detlef Stern //----------------------------------------------------------------------------- package draw_test import ( "testing" + "zettelstore.de/client.fossil/input" "zettelstore.de/z/config" - "zettelstore.de/z/input" "zettelstore.de/z/parser" "zettelstore.de/z/zettel/meta" ) func FuzzParseBlocks(f *testing.F) { Index: parser/draw/object.go ================================================================== --- parser/draw/object.go +++ parser/draw/object.go @@ -13,10 +13,13 @@ // https://github.com/asciitosvg/asciitosvg (Commit: // ca82a5ce41e2190a05e07af6e8b3ea4e3256a283, 2020-11-20): // // Copyright 2012 - 2018 The ASCIIToSVG Contributors // All rights reserved. +// +// SPDX-License-Identifier: EUPL-1.2 +// SPDX-FileCopyrightText: 2022-present Detlef Stern //----------------------------------------------------------------------------- package draw import "fmt" Index: parser/draw/point.go ================================================================== --- parser/draw/point.go +++ parser/draw/point.go @@ -13,10 +13,13 @@ // https://github.com/asciitosvg/asciitosvg (Commit: // ca82a5ce41e2190a05e07af6e8b3ea4e3256a283, 2020-11-20): // // Copyright 2012 - 2018 The ASCIIToSVG Contributors // All rights reserved. +// +// SPDX-License-Identifier: EUPL-1.2 +// SPDX-FileCopyrightText: 2022-present Detlef Stern //----------------------------------------------------------------------------- package draw import "fmt" Index: parser/draw/svg.go ================================================================== --- parser/draw/svg.go +++ parser/draw/svg.go @@ -13,10 +13,13 @@ // https://github.com/asciitosvg/asciitosvg (Commit: // ca82a5ce41e2190a05e07af6e8b3ea4e3256a283, 2020-11-20): // // Copyright 2012 - 2018 The ASCIIToSVG Contributors // All rights reserved. +// +// SPDX-License-Identifier: EUPL-1.2 +// SPDX-FileCopyrightText: 2022-present Detlef Stern //----------------------------------------------------------------------------- package draw import ( Index: parser/draw/svg_test.go ================================================================== --- parser/draw/svg_test.go +++ parser/draw/svg_test.go @@ -13,10 +13,13 @@ // https://github.com/asciitosvg/asciitosvg (Commit: // ca82a5ce41e2190a05e07af6e8b3ea4e3256a283, 2020-11-20): // // Copyright 2012 - 2018 The ASCIIToSVG Contributors // All rights reserved. +// +// SPDX-License-Identifier: EUPL-1.2 +// SPDX-FileCopyrightText: 2022-present Detlef Stern //----------------------------------------------------------------------------- package draw import ( Index: parser/markdown/markdown.go ================================================================== --- parser/markdown/markdown.go +++ parser/markdown/markdown.go @@ -4,10 +4,13 @@ // 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. +// +// SPDX-License-Identifier: EUPL-1.2 +// SPDX-FileCopyrightText: 2020-present Detlef Stern //----------------------------------------------------------------------------- // Package markdown provides a parser for markdown. package markdown @@ -20,13 +23,13 @@ gm "github.com/yuin/goldmark" gmAst "github.com/yuin/goldmark/ast" gmText "github.com/yuin/goldmark/text" "zettelstore.de/client.fossil/attrs" + "zettelstore.de/client.fossil/input" "zettelstore.de/z/ast" "zettelstore.de/z/encoder/textenc" - "zettelstore.de/z/input" "zettelstore.de/z/parser" "zettelstore.de/z/zettel/meta" ) func init() { @@ -147,11 +150,11 @@ } func (p *mdP) acceptRawText(node gmAst.Node) []byte { lines := node.Lines() result := make([]byte, 0, 512) - for i := 0; i < lines.Len(); i++ { + for i := range lines.Len() { s := lines.At(i) line := s.Value(p.source) if l := len(line); l > 0 { if l > 1 && line[l-2] == '\r' && line[l-1] == '\n' { line = line[0 : l-2] @@ -463,11 +466,11 @@ } } func (p *mdP) acceptRawHTML(node *gmAst.RawHTML) ast.InlineSlice { segs := make([][]byte, 0, node.Segments.Len()) - for i := 0; i < node.Segments.Len(); i++ { + for i := range node.Segments.Len() { segment := node.Segments.At(i) segs = append(segs, segment.Value(p.source)) } return ast.InlineSlice{ &ast.LiteralNode{ Index: parser/markdown/markdown_test.go ================================================================== --- parser/markdown/markdown_test.go +++ parser/markdown/markdown_test.go @@ -4,10 +4,13 @@ // 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. +// +// SPDX-License-Identifier: EUPL-1.2 +// SPDX-FileCopyrightText: 2020-present Detlef Stern //----------------------------------------------------------------------------- package markdown import ( Index: parser/none/none.go ================================================================== --- parser/none/none.go +++ parser/none/none.go @@ -4,18 +4,21 @@ // 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. +// +// SPDX-License-Identifier: EUPL-1.2 +// SPDX-FileCopyrightText: 2020-present Detlef Stern //----------------------------------------------------------------------------- // Package none provides a none-parser, e.g. for zettel with just metadata. package none import ( + "zettelstore.de/client.fossil/input" "zettelstore.de/z/ast" - "zettelstore.de/z/input" "zettelstore.de/z/parser" "zettelstore.de/z/zettel/meta" ) func init() { Index: parser/parser.go ================================================================== --- parser/parser.go +++ parser/parser.go @@ -4,10 +4,13 @@ // 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. +// +// SPDX-License-Identifier: EUPL-1.2 +// SPDX-FileCopyrightText: 2020-present Detlef Stern //----------------------------------------------------------------------------- // Package parser provides a generic interface to a range of different parsers. package parser @@ -15,13 +18,13 @@ "context" "fmt" "strings" "zettelstore.de/client.fossil/api" + "zettelstore.de/client.fossil/input" "zettelstore.de/z/ast" "zettelstore.de/z/config" - "zettelstore.de/z/input" "zettelstore.de/z/parser/cleaner" "zettelstore.de/z/zettel" "zettelstore.de/z/zettel/meta" ) @@ -83,19 +86,10 @@ return false } return pi.IsASTParser } -// IsTextFormat returns whether the given syntax is known to be a text format. -func IsTextFormat(syntax string) bool { - pi, ok := registry[syntax] - if !ok { - return false - } - return pi.IsTextFormat -} - // IsImageFormat returns whether the given syntax is known to be an image format. func IsImageFormat(syntax string) bool { pi, ok := registry[syntax] if !ok { return false @@ -103,13 +97,10 @@ return pi.IsImageFormat } // ParseBlocks parses some input and returns a slice of block nodes. func ParseBlocks(inp *input.Input, m *meta.Meta, syntax string, hi config.HTMLInsecurity) ast.BlockSlice { - return parseBlocksAndClean(inp, m, syntax, hi) -} -func parseBlocksAndClean(inp *input.Input, m *meta.Meta, syntax string, hi config.HTMLInsecurity) ast.BlockSlice { bs := Get(syntax).ParseBlocks(inp, m, syntax) cleaner.CleanBlockSlice(&bs, hi.AllowHTML(syntax)) return bs } @@ -157,11 +148,11 @@ inhMeta := m if rtConfig != nil { inhMeta = rtConfig.AddDefaultValues(ctx, inhMeta) } if syntax == "" { - syntax, _ = inhMeta.Get(api.KeySyntax) + syntax = inhMeta.GetDefault(api.KeySyntax, meta.DefaultSyntax) } parseMeta := inhMeta if syntax == meta.SyntaxNone { parseMeta = m } @@ -173,9 +164,9 @@ return &ast.ZettelNode{ Meta: m, Content: zettel.Content, Zid: m.Zid, InhMeta: inhMeta, - Ast: parseBlocksAndClean(input.NewInput(zettel.Content.AsBytes()), parseMeta, syntax, hi), + Ast: ParseBlocks(input.NewInput(zettel.Content.AsBytes()), parseMeta, syntax, hi), Syntax: syntax, } } Index: parser/parser_test.go ================================================================== --- parser/parser_test.go +++ parser/parser_test.go @@ -4,10 +4,13 @@ // 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. +// +// SPDX-License-Identifier: EUPL-1.2 +// SPDX-FileCopyrightText: 2021-present Detlef Stern //----------------------------------------------------------------------------- package parser_test import ( @@ -28,42 +31,38 @@ func TestParserType(t *testing.T) { syntaxSet := strfun.NewSet(parser.GetSyntaxes()...) testCases := []struct { syntax string ast bool - text bool image bool }{ - {meta.SyntaxHTML, false, true, false}, - {meta.SyntaxCSS, false, true, false}, - {meta.SyntaxDraw, true, true, false}, - {meta.SyntaxGif, false, false, true}, - {meta.SyntaxJPEG, false, false, true}, - {meta.SyntaxJPG, false, false, true}, - {meta.SyntaxMarkdown, true, true, false}, - {meta.SyntaxMD, true, true, false}, - {meta.SyntaxNone, false, false, false}, - {meta.SyntaxPlain, false, true, false}, - {meta.SyntaxPNG, false, false, true}, - {meta.SyntaxSVG, false, true, true}, - {meta.SyntaxSxn, false, true, false}, - {meta.SyntaxText, false, true, false}, - {meta.SyntaxTxt, false, true, false}, - {meta.SyntaxWebp, false, false, true}, - {meta.SyntaxZmk, true, true, false}, + {meta.SyntaxHTML, false, false}, + {meta.SyntaxCSS, false, false}, + {meta.SyntaxDraw, true, false}, + {meta.SyntaxGif, false, true}, + {meta.SyntaxJPEG, false, true}, + {meta.SyntaxJPG, false, true}, + {meta.SyntaxMarkdown, true, false}, + {meta.SyntaxMD, true, false}, + {meta.SyntaxNone, false, false}, + {meta.SyntaxPlain, false, false}, + {meta.SyntaxPNG, false, true}, + {meta.SyntaxSVG, false, true}, + {meta.SyntaxSxn, false, false}, + {meta.SyntaxText, false, false}, + {meta.SyntaxTxt, false, false}, + {meta.SyntaxWebp, false, true}, + {meta.SyntaxZmk, true, false}, } for _, tc := range testCases { delete(syntaxSet, tc.syntax) if got := parser.IsASTParser(tc.syntax); got != tc.ast { t.Errorf("Syntax %q is AST: %v, but got %v", tc.syntax, tc.ast, got) } - if got := parser.IsTextFormat(tc.syntax); got != tc.text { - t.Errorf("Syntax %q is text: %v, but got %v", tc.syntax, tc.text, got) - } if got := parser.IsImageFormat(tc.syntax); got != tc.image { t.Errorf("Syntax %q is image: %v, but got %v", tc.syntax, tc.image, got) } } for syntax := range syntaxSet { t.Errorf("Forgot to test syntax %q", syntax) } } Index: parser/plain/plain.go ================================================================== --- parser/plain/plain.go +++ parser/plain/plain.go @@ -4,10 +4,13 @@ // 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. +// +// SPDX-License-Identifier: EUPL-1.2 +// SPDX-FileCopyrightText: 2020-present Detlef Stern //----------------------------------------------------------------------------- // Package plain provides a parser for plain text data. package plain @@ -14,14 +17,13 @@ import ( "bytes" "strings" "zettelstore.de/client.fossil/attrs" - "zettelstore.de/sx.fossil/sxbuiltins" + "zettelstore.de/client.fossil/input" "zettelstore.de/sx.fossil/sxreader" "zettelstore.de/z/ast" - "zettelstore.de/z/input" "zettelstore.de/z/parser" "zettelstore.de/z/zettel/meta" ) func init() { @@ -134,32 +136,22 @@ return svgSrc } func parseSxnBlocks(inp *input.Input, _ *meta.Meta, syntax string) ast.BlockSlice { rd := sxreader.MakeReader(bytes.NewReader(inp.Src)) - objs, err := rd.ReadAll() - if err != nil { - return ast.BlockSlice{ - &ast.VerbatimNode{ - Kind: ast.VerbatimProg, - Attrs: attrs.Attributes{"": syntax}, - Content: inp.ScanLineContent(), - }, - ast.CreateParaNode(&ast.TextNode{ - Text: err.Error(), - }), - } - } - result := make(ast.BlockSlice, len(objs)) - for i, obj := range objs { - var buf bytes.Buffer - sxbuiltins.Print(&buf, obj) - result[i] = &ast.VerbatimNode{ - Kind: ast.VerbatimProg, - Attrs: attrs.Attributes{"": syntax}, - Content: buf.Bytes(), - } + _, err := rd.ReadAll() + result := ast.BlockSlice{ + &ast.VerbatimNode{ + Kind: ast.VerbatimProg, + Attrs: attrs.Attributes{"": syntax}, + Content: inp.ScanLineContent(), + }, + } + if err != nil { + result = append(result, ast.CreateParaNode(&ast.TextNode{ + Text: err.Error(), + })) } return result } func parseSxnInlines(inp *input.Input, syntax string) ast.InlineSlice { Index: parser/zettelmark/block.go ================================================================== --- parser/zettelmark/block.go +++ parser/zettelmark/block.go @@ -4,19 +4,22 @@ // 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. +// +// SPDX-License-Identifier: EUPL-1.2 +// SPDX-FileCopyrightText: 2020-present Detlef Stern //----------------------------------------------------------------------------- package zettelmark import ( "fmt" + "zettelstore.de/client.fossil/input" "zettelstore.de/z/ast" - "zettelstore.de/z/input" ) // parseBlockSlice parses a sequence of blocks. func (cp *zmkP) parseBlockSlice() ast.BlockSlice { inp := cp.inp @@ -406,11 +409,11 @@ return ln, newLnCount } func (cp *zmkP) cleanupParsedNestedList(newLnCount int) (res ast.BlockNode, success bool) { listDepth := len(cp.lists) - for i := 0; i < newLnCount; i++ { + for i := range newLnCount { childPos := listDepth - i - 1 parentPos := childPos - 1 if parentPos < 0 { return cp.lists[0], true } Index: parser/zettelmark/inline.go ================================================================== --- parser/zettelmark/inline.go +++ parser/zettelmark/inline.go @@ -4,10 +4,13 @@ // 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. +// +// SPDX-License-Identifier: EUPL-1.2 +// SPDX-FileCopyrightText: 2020-present Detlef Stern //----------------------------------------------------------------------------- package zettelmark import ( @@ -14,12 +17,12 @@ "bytes" "fmt" "strings" "zettelstore.de/client.fossil/attrs" + "zettelstore.de/client.fossil/input" "zettelstore.de/z/ast" - "zettelstore.de/z/input" "zettelstore.de/z/zettel/meta" ) // parseInlineSlice parses a sequence of Inlines until EOS. func (cp *zmkP) parseInlineSlice() (ins ast.InlineSlice) { Index: parser/zettelmark/node.go ================================================================== --- parser/zettelmark/node.go +++ parser/zettelmark/node.go @@ -4,10 +4,13 @@ // 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. +// +// SPDX-License-Identifier: EUPL-1.2 +// SPDX-FileCopyrightText: 2020-present Detlef Stern //----------------------------------------------------------------------------- package zettelmark import "zettelstore.de/z/ast" Index: parser/zettelmark/post-processor.go ================================================================== --- parser/zettelmark/post-processor.go +++ parser/zettelmark/post-processor.go @@ -4,10 +4,13 @@ // 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. +// +// SPDX-License-Identifier: EUPL-1.2 +// SPDX-FileCopyrightText: 2020-present Detlef Stern //----------------------------------------------------------------------------- package zettelmark import ( @@ -129,11 +132,11 @@ } func (pp *postProcessor) visitTable(tn *ast.TableNode) { width := tableWidth(tn) tn.Align = make([]ast.Alignment, width) - for i := 0; i < width; i++ { + for i := range width { tn.Align[i] = ast.AlignDefault } if len(tn.Rows) > 0 && isHeaderRow(tn.Rows[0]) { tn.Header = tn.Rows[0] tn.Rows = tn.Rows[1:] Index: parser/zettelmark/zettelmark.go ================================================================== --- parser/zettelmark/zettelmark.go +++ parser/zettelmark/zettelmark.go @@ -4,10 +4,13 @@ // 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. +// +// SPDX-License-Identifier: EUPL-1.2 +// SPDX-FileCopyrightText: 2020-present Detlef Stern //----------------------------------------------------------------------------- // Package zettelmark provides a parser for zettelmarkup. package zettelmark @@ -14,12 +17,12 @@ import ( "strings" "unicode" "zettelstore.de/client.fossil/attrs" + "zettelstore.de/client.fossil/input" "zettelstore.de/z/ast" - "zettelstore.de/z/input" "zettelstore.de/z/parser" "zettelstore.de/z/zettel/meta" ) func init() { Index: parser/zettelmark/zettelmark_fuzz_test.go ================================================================== --- parser/zettelmark/zettelmark_fuzz_test.go +++ parser/zettelmark/zettelmark_fuzz_test.go @@ -4,19 +4,22 @@ // 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. +// +// SPDX-License-Identifier: EUPL-1.2 +// SPDX-FileCopyrightText: 2022-present Detlef Stern //----------------------------------------------------------------------------- package zettelmark_test import ( "testing" + "zettelstore.de/client.fossil/input" "zettelstore.de/z/config" - "zettelstore.de/z/input" "zettelstore.de/z/parser" "zettelstore.de/z/zettel/meta" ) func FuzzParseBlocks(f *testing.F) { Index: parser/zettelmark/zettelmark_test.go ================================================================== --- parser/zettelmark/zettelmark_test.go +++ parser/zettelmark/zettelmark_test.go @@ -4,10 +4,13 @@ // 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. +// +// SPDX-License-Identifier: EUPL-1.2 +// SPDX-FileCopyrightText: 2020-present Detlef Stern //----------------------------------------------------------------------------- // Package zettelmark_test provides some tests for the zettelmarkup parser. package zettelmark_test @@ -15,13 +18,13 @@ "fmt" "strings" "testing" "zettelstore.de/client.fossil/attrs" + "zettelstore.de/client.fossil/input" "zettelstore.de/z/ast" "zettelstore.de/z/config" - "zettelstore.de/z/input" "zettelstore.de/z/parser" "zettelstore.de/z/zettel/meta" ) type TestCase struct{ source, want string } Index: query/compiled.go ================================================================== --- query/compiled.go +++ query/compiled.go @@ -4,16 +4,19 @@ // 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. +// +// SPDX-License-Identifier: EUPL-1.2 +// SPDX-FileCopyrightText: 2023-present Detlef Stern //----------------------------------------------------------------------------- package query import ( - "math/rand" + "math/rand/v2" "sort" "zettelstore.de/z/zettel/id" "zettelstore.de/z/zettel/meta" ) @@ -137,18 +140,18 @@ count = limit c.limit = 0 } order := make([]int, len(metaList)) - for i := 0; i < len(metaList); i++ { + for i := range len(metaList) { order[i] = i } rnd := c.newRandom() picked := make([]int, count) - for i := 0; i < count; i++ { + for i := range count { last := len(order) - i - n := rnd.Intn(last) + n := rnd.IntN(last) picked[i] = order[n] order[n] = order[last-1] } order = nil sort.Ints(picked) @@ -169,13 +172,13 @@ } func (c *Compiled) newRandom() *rand.Rand { seed := c.seed if seed <= 0 { - seed = rand.Intn(10000) + 10001 + seed = rand.IntN(10000) + 10001 } - return rand.New(rand.NewSource(int64(seed))) + return rand.New(rand.NewPCG(uint64(seed), uint64(seed))) } func limitElements(metaList []*meta.Meta, limit int) []*meta.Meta { if limit > 0 && limit < len(metaList) { return metaList[:limit] Index: query/context.go ================================================================== --- query/context.go +++ query/context.go @@ -4,17 +4,21 @@ // 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. +// +// SPDX-License-Identifier: EUPL-1.2 +// SPDX-FileCopyrightText: 2023-present Detlef Stern //----------------------------------------------------------------------------- package query import ( "container/heap" "context" + "math" "zettelstore.de/client.fossil/api" "zettelstore.de/z/zettel/id" "zettelstore.de/z/zettel/meta" ) @@ -22,10 +26,11 @@ // ContextSpec contains all specification values for calculating a context. type ContextSpec struct { Direction ContextDirection MaxCost int MaxCount int + Full bool } // ContextDirection specifies the direction a context should be calculated. type ContextDirection uint8 @@ -42,10 +47,14 @@ } func (spec *ContextSpec) Print(pe *PrintEnv) { pe.printSpace() pe.writeString(api.ContextDirective) + if spec.Full { + pe.printSpace() + pe.writeString(api.FullDirective) + } switch spec.Direction { case ContextDirBackward: pe.printSpace() pe.writeString(api.BackwardDirective) case ContextDirForward: @@ -55,11 +64,11 @@ pe.printPosInt(api.CostDirective, spec.MaxCost) pe.printPosInt(api.MaxDirective, spec.MaxCount) } func (spec *ContextSpec) Execute(ctx context.Context, startSeq []*meta.Meta, port ContextPort) []*meta.Meta { - maxCost := spec.MaxCost + maxCost := float64(spec.MaxCost) if maxCost <= 0 { maxCost = 17 } maxCount := spec.MaxCount if maxCount <= 0 { @@ -76,22 +85,23 @@ } result = append(result, m) for _, p := range m.ComputedPairsRest() { tasks.addPair(ctx, p.Key, p.Value, cost, isBackward, isForward) + } + if !spec.Full { + continue } if tags, found := m.GetList(api.KeyTags); found { - for _, tag := range tags { - tasks.addSameTag(ctx, tag, cost) - } + tasks.addTags(ctx, tags, cost) } } return result } type ztlCtxItem struct { - cost int + cost float64 meta *meta.Meta } type ztlCtxQueue []ztlCtxItem func (q ztlCtxQueue) Len() int { return len(q) } @@ -106,25 +116,29 @@ *q = old[0 : n-1] return item } type contextTask struct { - port ContextPort - seen id.Set - queue ztlCtxQueue - maxCost int - limit int - tagCost map[string][]*meta.Meta + port ContextPort + seen id.Set + queue ztlCtxQueue + maxCost float64 + limit int + tagMetas map[string][]*meta.Meta + tagZids map[string]id.Set // just the zids of tagMetas + metaZid map[id.Zid]*meta.Meta // maps zid to meta for all meta retrieved with tags } -func newQueue(startSeq []*meta.Meta, maxCost, limit int, port ContextPort) *contextTask { +func newQueue(startSeq []*meta.Meta, maxCost float64, limit int, port ContextPort) *contextTask { result := &contextTask{ - port: port, - seen: id.NewSet(), - maxCost: maxCost, - limit: limit, - tagCost: make(map[string][]*meta.Meta, 1024), + port: port, + seen: id.NewSet(), + maxCost: maxCost, + limit: limit, + tagMetas: make(map[string][]*meta.Meta), + tagZids: make(map[string]id.Set), + metaZid: make(map[id.Zid]*meta.Meta), } queue := make(ztlCtxQueue, 0, len(startSeq)) for _, m := range startSeq { queue = append(queue, ztlCtxItem{cost: 1, meta: m}) @@ -132,11 +146,11 @@ heap.Init(&queue) result.queue = queue return result } -func (ct *contextTask) addPair(ctx context.Context, key, value string, curCost int, isBackward, isForward bool) { +func (ct *contextTask) addPair(ctx context.Context, key, value string, curCost float64, isBackward, isForward bool) { if key == api.KeyBack { return } newCost := curCost + contextCost(key) if key == api.KeyBackward { @@ -160,96 +174,105 @@ } else if t == meta.TypeIDSet { ct.addIDSet(ctx, newCost, value) } } -func contextCost(key string) int { +func contextCost(key string) float64 { switch key { case api.KeyFolge, api.KeyPrecursor: return 1 - case api.KeySuccessors, api.KeyPredecessor, - api.KeySubordinates, api.KeySuperior: - return 2 + case api.KeySubordinates, api.KeySuperior: + return 1.5 + case api.KeySuccessors, api.KeyPredecessor: + return 7 } - return 3 + return 2 } -func (ct *contextTask) addID(ctx context.Context, newCost int, value string) { - if ct.costMaxed(newCost) { - return - } +func (ct *contextTask) addID(ctx context.Context, newCost float64, value string) { if zid, errParse := id.Parse(value); errParse == nil { if m, errGetMeta := ct.port.GetMeta(ctx, zid); errGetMeta == nil { ct.addMeta(m, newCost) } } } -func (ct *contextTask) addMeta(m *meta.Meta, newCost int) { - if _, found := ct.seen[m.Zid]; !found { - heap.Push(&ct.queue, ztlCtxItem{cost: newCost, meta: m}) - } -} -func (ct *contextTask) costMaxed(newCost int) bool { +func (ct *contextTask) addMeta(m *meta.Meta, newCost float64) { // If len(zc.seen) <= 1, the initial zettel is processed. In this case allow all // other zettel that are directly reachable, without taking the cost into account. // Of course, the limit ist still relevant. - return (len(ct.seen) > 1 && ct.maxCost > 0 && newCost > ct.maxCost) || ct.hasLimit() + if !ct.hasLimit() && (len(ct.seen) <= 1 || ct.maxCost == 0 || newCost <= ct.maxCost) { + if _, found := ct.seen[m.Zid]; !found { + heap.Push(&ct.queue, ztlCtxItem{cost: newCost, meta: m}) + } + } } -func (ct *contextTask) addIDSet(ctx context.Context, newCost int, value string) { +func (ct *contextTask) addIDSet(ctx context.Context, newCost float64, value string) { elems := meta.ListFromValue(value) refCost := referenceCost(newCost, len(elems)) for _, val := range elems { ct.addID(ctx, refCost, val) } } -func referenceCost(baseCost int, numReferences int) int { - switch { - case numReferences < 5: - return baseCost + 1 - case numReferences < 9: - return baseCost * 2 - case numReferences < 17: - return baseCost * 3 - case numReferences < 33: - return baseCost * 4 - case numReferences < 65: - return baseCost * 5 - } - return baseCost * numReferences / 8 -} - -func (ct *contextTask) addSameTag(ctx context.Context, tag string, baseCost int) { - tagMetas, found := ct.tagCost[tag] - if !found { - q := Parse(api.KeyTags + api.SearchOperatorHas + tag + " ORDER REVERSE " + api.KeyID) - ml, err := ct.port.SelectMeta(ctx, nil, q) - if err != nil { - return - } - tagMetas = ml - ct.tagCost[tag] = ml - } - cost := tagCost(baseCost, len(tagMetas)) - if ct.costMaxed(cost) { - return - } - for _, m := range tagMetas { - ct.addMeta(m, cost) - } -} - -func tagCost(baseCost, numTags int) int { - if numTags < 8 { - return baseCost + numTags/2 - } - return (baseCost + 2) * (numTags / 4) -} - -func (ct *contextTask) next() (*meta.Meta, int) { +func referenceCost(baseCost float64, numReferences int) float64 { + nRefs := float64(numReferences) + return nRefs*math.Log2(nRefs+1) + baseCost +} + +func (ct *contextTask) addTags(ctx context.Context, tags []string, baseCost float64) { + var zidSet id.Set + for _, tag := range tags { + zs := ct.updateTagData(ctx, tag) + zidSet = zidSet.Copy(zs) + } + for _, zid := range zidSet.Sorted() { // .Sorted() to stay deterministic + minCost := math.MaxFloat64 + costFactor := 1.1 + for _, tag := range tags { + tagZids := ct.tagZids[tag] + if tagZids.Contains(zid) { + cost := tagCost(baseCost, len(tagZids)) + if cost < minCost { + minCost = cost + } + costFactor /= 1.1 + } + } + ct.addMeta(ct.metaZid[zid], minCost*costFactor) + } +} + +func (ct *contextTask) updateTagData(ctx context.Context, tag string) id.Set { + if _, found := ct.tagMetas[tag]; found { + return ct.tagZids[tag] + } + q := Parse(api.KeyTags + api.SearchOperatorHas + tag + " ORDER REVERSE " + api.KeyID) + ml, err := ct.port.SelectMeta(ctx, nil, q) + if err != nil { + ml = nil + } + ct.tagMetas[tag] = ml + zids := id.NewSetCap(len(ml)) + for _, m := range ml { + zid := m.Zid + zids = zids.Add(zid) + if _, found := ct.metaZid[zid]; !found { + ct.metaZid[zid] = m + } + } + ct.tagZids[tag] = zids + return zids +} + +func tagCost(baseCost float64, numTags int) float64 { + nTags := float64(numTags) + return nTags*math.Log2(nTags+1) + baseCost +} + +func (ct *contextTask) next() (*meta.Meta, float64) { if ct.hasLimit() { return nil, -1 } for len(ct.queue) > 0 { item := heap.Pop(&ct.queue).(ztlCtxItem) Index: query/parser.go ================================================================== --- query/parser.go +++ query/parser.go @@ -4,19 +4,22 @@ // 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. +// +// SPDX-License-Identifier: EUPL-1.2 +// SPDX-FileCopyrightText: 2022-present Detlef Stern //----------------------------------------------------------------------------- package query import ( "strconv" "zettelstore.de/client.fossil/api" - "zettelstore.de/z/input" + "zettelstore.de/client.fossil/input" "zettelstore.de/z/zettel/id" "zettelstore.de/z/zettel/meta" ) // Parse the query specification and return a Query object. @@ -203,10 +206,15 @@ ps.skipSpace() if ps.mustStop() { break } pos := inp.Pos + if ps.acceptSingleKw(api.FullDirective) { + spec.Full = true + continue + } + inp.SetPos(pos) if ps.acceptSingleKw(api.BackwardDirective) { spec.Direction = ContextDirBackward continue } inp.SetPos(pos) Index: query/parser_test.go ================================================================== --- query/parser_test.go +++ query/parser_test.go @@ -4,10 +4,13 @@ // 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. +// +// SPDX-License-Identifier: EUPL-1.2 +// SPDX-FileCopyrightText: 2022-present Detlef Stern //----------------------------------------------------------------------------- package query_test import ( @@ -33,10 +36,11 @@ {"CONTEXT", "CONTEXT"}, {"CONTEXT a", "CONTEXT a"}, {"0 CONTEXT", "0 CONTEXT"}, {"1 CONTEXT", "00000000000001 CONTEXT"}, {"00000000000001 CONTEXT", "00000000000001 CONTEXT"}, {"100000000000001 CONTEXT", "100000000000001 CONTEXT"}, + {"1 CONTEXT FULL", "00000000000001 CONTEXT FULL"}, {"1 CONTEXT BACKWARD", "00000000000001 CONTEXT BACKWARD"}, {"1 CONTEXT FORWARD", "00000000000001 CONTEXT FORWARD"}, {"1 CONTEXT COST ", "00000000000001 CONTEXT COST"}, {"1 CONTEXT COST 3", "00000000000001 CONTEXT COST 3"}, {"1 CONTEXT COST x", "00000000000001 CONTEXT COST x"}, {"1 CONTEXT MAX 5", "00000000000001 CONTEXT MAX 5"}, {"1 CONTEXT MAX y", "00000000000001 CONTEXT MAX y"}, Index: query/print.go ================================================================== --- query/print.go +++ query/print.go @@ -4,10 +4,13 @@ // 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. +// +// SPDX-License-Identifier: EUPL-1.2 +// SPDX-FileCopyrightText: 2020-present Detlef Stern //----------------------------------------------------------------------------- package query import ( Index: query/query.go ================================================================== --- query/query.go +++ query/query.go @@ -4,18 +4,21 @@ // 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. +// +// SPDX-License-Identifier: EUPL-1.2 +// SPDX-FileCopyrightText: 2020-present Detlef Stern //----------------------------------------------------------------------------- // Package query provides a query for zettel. package query import ( "context" - "math/rand" + "math/rand/v2" "slices" "zettelstore.de/z/zettel/id" "zettelstore.de/z/zettel/meta" ) @@ -245,18 +248,30 @@ q = createIfNeeded(q) q.terms[len(q.terms)-1].addKey(key, op) return q } -func (q *Query) GetMetaValues(key string) (vals []string) { +var missingMap = map[compareOp]bool{ + cmpNotExist: true, + cmpNotEqual: true, + cmpHasNot: true, + cmpNoMatch: true, +} + +// GetMetaValues returns the slice of all values specified for a given metadata key. +// If `withMissing` is true, all values are returned. Otherwise only those, +// where the comparison operator will positively search for a value. +func (q *Query) GetMetaValues(key string, withMissing bool) (vals []string) { if q == nil { return nil } for _, term := range q.terms { if mvs, hasMv := term.mvals[key]; hasMv { for _, ev := range mvs { - vals = append(vals, ev.value) + if withMissing || !missingMap[ev.op] { + vals = append(vals, ev.value) + } } } } slices.Sort(vals) return slices.Compact(vals) @@ -289,11 +304,11 @@ // SetDeterministic signals that the result should be the same if the seed is the same. func (q *Query) SetDeterministic() *Query { q = createIfNeeded(q) if q.seed <= 0 { - q.seed = int(rand.Intn(10000) + 1) + q.seed = int(rand.IntN(10000) + 1) } return q } // Actions returns the slice of action specifications Index: query/retrieve.go ================================================================== --- query/retrieve.go +++ query/retrieve.go @@ -4,10 +4,13 @@ // 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. +// +// SPDX-License-Identifier: EUPL-1.2 +// SPDX-FileCopyrightText: 2021-present Detlef Stern //----------------------------------------------------------------------------- package query // This file contains helper functions to search within the index. Index: query/select.go ================================================================== --- query/select.go +++ query/select.go @@ -4,10 +4,13 @@ // 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. +// +// SPDX-License-Identifier: EUPL-1.2 +// SPDX-FileCopyrightText: 2020-present Detlef Stern //----------------------------------------------------------------------------- package query import ( @@ -119,12 +122,10 @@ return createMatchNumberFunc(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) } @@ -140,11 +141,11 @@ return true } } func createMatchIDSetFunc(values []expValue, addSearch addSearchFunc) matchValueFunc { - predList := valuesToWordSetPredicates(preprocessSet(values), addSearch) + predList := valuesToSetPredicates(preprocessSet(values), addSearch) return func(value string) bool { ids := meta.ListFromValue(value) for _, preds := range predList { for _, pred := range preds { if !pred(ids) { @@ -179,11 +180,11 @@ return true } } func createMatchTagSetFunc(values []expValue, addSearch addSearchFunc) matchValueFunc { - predList := valuesToWordSetPredicates(processTagSet(preprocessSet(sliceToLower(values))), addSearch) + predList := valuesToSetPredicates(processTagSet(preprocessSet(sliceToLower(values))), addSearch) return func(value string) bool { tags := meta.TagsFromValue(value) for _, preds := range predList { for _, pred := range preds { if !pred(tags) { @@ -232,25 +233,10 @@ for _, pred := range preds { if !pred(value) { return false } } - return true - } -} - -func createMatchWordSetFunc(values []expValue, addSearch addSearchFunc) matchValueFunc { - predsList := valuesToWordSetPredicates(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 sliceToLower(sl []expValue) []expValue { @@ -309,11 +295,11 @@ return true } func zmk2text(zmk string) string { isASCII, hasUpper, needParse := true, false, false - for i := 0; i < len(zmk); i++ { + for i := range len(zmk) { ch := zmk[i] if ch >= utf8.RuneSelf { isASCII = false break } @@ -384,11 +370,11 @@ } return result } func isDigits(s string) bool { - for i := 0; i < len(s); i++ { + for i := range len(s) { if ch := s[i]; ch < '0' || '9' < ch { return false } } return true @@ -564,11 +550,11 @@ } } type stringSetPredicate func(value []string) bool -func valuesToWordSetPredicates(values [][]expValue, addSearch addSearchFunc) [][]stringSetPredicate { +func valuesToSetPredicates(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 Index: query/select_test.go ================================================================== --- query/select_test.go +++ query/select_test.go @@ -4,10 +4,13 @@ // 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. +// +// SPDX-License-Identifier: EUPL-1.2 +// SPDX-FileCopyrightText: 2021-present Detlef Stern //----------------------------------------------------------------------------- package query_test import ( Index: query/sorter.go ================================================================== --- query/sorter.go +++ query/sorter.go @@ -4,10 +4,13 @@ // 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. +// +// SPDX-License-Identifier: EUPL-1.2 +// SPDX-FileCopyrightText: 2020-present Detlef Stern //----------------------------------------------------------------------------- package query import ( Index: query/specs.go ================================================================== --- query/specs.go +++ query/specs.go @@ -4,10 +4,13 @@ // 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. +// +// SPDX-License-Identifier: EUPL-1.2 +// SPDX-FileCopyrightText: 2023-present Detlef Stern //----------------------------------------------------------------------------- package query import "zettelstore.de/client.fossil/api" Index: query/unlinked.go ================================================================== --- query/unlinked.go +++ query/unlinked.go @@ -4,10 +4,13 @@ // 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. +// +// SPDX-License-Identifier: EUPL-1.2 +// SPDX-FileCopyrightText: 2023-present Detlef Stern //----------------------------------------------------------------------------- package query import ( Index: strfun/escape.go ================================================================== --- strfun/escape.go +++ strfun/escape.go @@ -4,10 +4,13 @@ // 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. +// +// SPDX-License-Identifier: EUPL-1.2 +// SPDX-FileCopyrightText: 2020-present Detlef Stern //----------------------------------------------------------------------------- package strfun import "io" Index: strfun/set.go ================================================================== --- strfun/set.go +++ strfun/set.go @@ -4,10 +4,13 @@ // 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. +// +// SPDX-License-Identifier: EUPL-1.2 +// SPDX-FileCopyrightText: 2022-present Detlef Stern //----------------------------------------------------------------------------- package strfun // Set ist a set of strings. Index: strfun/slugify.go ================================================================== --- strfun/slugify.go +++ strfun/slugify.go @@ -4,10 +4,13 @@ // 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. +// +// SPDX-License-Identifier: EUPL-1.2 +// SPDX-FileCopyrightText: 2020-present Detlef Stern //----------------------------------------------------------------------------- package strfun import ( Index: strfun/slugify_test.go ================================================================== --- strfun/slugify_test.go +++ strfun/slugify_test.go @@ -4,10 +4,13 @@ // 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. +// +// SPDX-License-Identifier: EUPL-1.2 +// SPDX-FileCopyrightText: 2020-present Detlef Stern //----------------------------------------------------------------------------- package strfun_test import ( Index: strfun/strfun.go ================================================================== --- strfun/strfun.go +++ strfun/strfun.go @@ -4,10 +4,13 @@ // 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. +// +// SPDX-License-Identifier: EUPL-1.2 +// SPDX-FileCopyrightText: 2021-present Detlef Stern //----------------------------------------------------------------------------- // Package strfun provides some string functions. package strfun @@ -38,11 +41,11 @@ var sb strings.Builder for _, r := range runes { sb.WriteRune(r) } - for i := 0; i < maxLen-len(runes); i++ { + for range maxLen - len(runes) { sb.WriteRune(pad) } return sb.String() } Index: strfun/strfun_test.go ================================================================== --- strfun/strfun_test.go +++ strfun/strfun_test.go @@ -4,10 +4,13 @@ // 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. +// +// SPDX-License-Identifier: EUPL-1.2 +// SPDX-FileCopyrightText: 2021-present Detlef Stern //----------------------------------------------------------------------------- package strfun_test import ( Index: tests/client/client_test.go ================================================================== --- tests/client/client_test.go +++ tests/client/client_test.go @@ -4,21 +4,26 @@ // This file is part of Zettelstore. // // Zettelstore client is licensed under the latest version of the EUPL // (European Union Public License). Please see file LICENSE.txt for your rights // and obligations under this license. +// +// SPDX-License-Identifier: EUPL-1.2 +// SPDX-FileCopyrightText: 2021-present Detlef Stern //----------------------------------------------------------------------------- // Package client provides a client for accessing the Zettelstore via its API. package client_test import ( "context" "flag" "fmt" + "io" "net/http" "net/url" + "slices" "strconv" "testing" "zettelstore.de/client.fossil/api" "zettelstore.de/client.fossil/client" @@ -416,10 +421,45 @@ t.Error(err) } else if zid != exp { t.Errorf("role zettel for zettel should be %q, but got %q", exp, zid) } } + +func TestRedirect(t *testing.T) { + t.Parallel() + c := getClient() + search := api.OrderDirective + " " + api.ReverseDirective + " " + api.KeyID + api.ActionSeparator + api.RedirectAction + ub := c.NewURLBuilder('z').AppendQuery(search) + respRedirect, err := http.Get(ub.String()) + if err != nil { + t.Error(err) + return + } + defer respRedirect.Body.Close() + bodyRedirect, err := io.ReadAll(respRedirect.Body) + if err != nil { + t.Error(err) + return + } + ub.ClearQuery().SetZid(api.ZidEmoji) + respEmoji, err := http.Get(ub.String()) + if err != nil { + t.Error(err) + return + } + defer respEmoji.Body.Close() + bodyEmoji, err := io.ReadAll(respEmoji.Body) + if err != nil { + t.Error(err) + return + } + if !slices.Equal(bodyRedirect, bodyEmoji) { + t.Error("Wrong redirect") + t.Error("REDIRECT", respRedirect) + t.Error("EXPECTED", respEmoji) + } +} func TestVersion(t *testing.T) { t.Parallel() c := getClient() ver, err := c.GetVersionInfo(context.Background()) Index: tests/client/crud_test.go ================================================================== --- tests/client/crud_test.go +++ tests/client/crud_test.go @@ -4,10 +4,13 @@ // This file is part of Zettelstore. // // Zettelstore client is licensed under the latest version of the EUPL // (European Union Public License). Please see file LICENSE.txt for your rights // and obligations under this license. +// +// SPDX-License-Identifier: EUPL-1.2 +// SPDX-FileCopyrightText: 2021-present Detlef Stern //----------------------------------------------------------------------------- package client_test import ( Index: tests/client/embed_test.go ================================================================== --- tests/client/embed_test.go +++ tests/client/embed_test.go @@ -4,10 +4,13 @@ // 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. +// +// SPDX-License-Identifier: EUPL-1.2 +// SPDX-FileCopyrightText: 2021-present Detlef Stern //----------------------------------------------------------------------------- package client_test import ( Index: tests/markdown_test.go ================================================================== --- tests/markdown_test.go +++ tests/markdown_test.go @@ -4,10 +4,13 @@ // 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. +// +// SPDX-License-Identifier: EUPL-1.2 +// SPDX-FileCopyrightText: 2020-present Detlef Stern //----------------------------------------------------------------------------- package tests import ( @@ -17,20 +20,20 @@ "os" "strings" "testing" "zettelstore.de/client.fossil/api" + "zettelstore.de/client.fossil/input" "zettelstore.de/z/ast" "zettelstore.de/z/config" "zettelstore.de/z/encoder" _ "zettelstore.de/z/encoder/htmlenc" _ "zettelstore.de/z/encoder/mdenc" _ "zettelstore.de/z/encoder/shtmlenc" _ "zettelstore.de/z/encoder/szenc" _ "zettelstore.de/z/encoder/textenc" _ "zettelstore.de/z/encoder/zmkenc" - "zettelstore.de/z/input" "zettelstore.de/z/parser" _ "zettelstore.de/z/parser/markdown" _ "zettelstore.de/z/parser/zettelmark" "zettelstore.de/z/zettel/meta" ) Index: tests/naughtystrings_test.go ================================================================== --- tests/naughtystrings_test.go +++ tests/naughtystrings_test.go @@ -4,10 +4,13 @@ // 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. +// +// SPDX-License-Identifier: EUPL-1.2 +// SPDX-FileCopyrightText: 2020-present Detlef Stern //----------------------------------------------------------------------------- package tests import ( @@ -16,13 +19,13 @@ "os" "path/filepath" "testing" "zettelstore.de/client.fossil/api" + "zettelstore.de/client.fossil/input" _ "zettelstore.de/z/cmd" "zettelstore.de/z/encoder" - "zettelstore.de/z/input" "zettelstore.de/z/parser" "zettelstore.de/z/zettel/meta" ) // Test all parser / encoder with a list of "naughty strings", i.e. unusual strings Index: tests/regression_test.go ================================================================== --- tests/regression_test.go +++ tests/regression_test.go @@ -4,10 +4,13 @@ // 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. +// +// SPDX-License-Identifier: EUPL-1.2 +// SPDX-FileCopyrightText: 2020-present Detlef Stern //----------------------------------------------------------------------------- // Package tests provides some higher-level tests. package tests DELETED tools/build.go Index: tools/build.go ================================================================== --- tools/build.go +++ /dev/null @@ -1,644 +0,0 @@ -//----------------------------------------------------------------------------- -// Copyright (c) 2021-present 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 main provides a command to build and run the software. -package main - -import ( - "archive/zip" - "bytes" - "errors" - "flag" - "fmt" - "io" - "io/fs" - "net" - "os" - "os/exec" - "path/filepath" - "strings" - "time" - - "zettelstore.de/client.fossil/api" - "zettelstore.de/z/input" - "zettelstore.de/z/strfun" - "zettelstore.de/z/zettel/id" - "zettelstore.de/z/zettel/meta" -) - -var envDirectProxy = []string{"GOPROXY=direct"} -var envGoVCS = []string{"GOVCS=zettelstore.de:fossil"} - -func executeCommand(env []string, name string, arg ...string) (string, error) { - logCommand("EXEC", env, name, arg) - var out strings.Builder - cmd := prepareCommand(env, name, arg, &out) - err := cmd.Run() - return out.String(), err -} - -func prepareCommand(env []string, name string, arg []string, out io.Writer) *exec.Cmd { - if len(env) > 0 { - env = append(env, os.Environ()...) - } - cmd := exec.Command(name, arg...) - cmd.Env = env - cmd.Stdin = nil - cmd.Stdout = out - cmd.Stderr = os.Stderr - return cmd -} - -func logCommand(exec string, env []string, name string, arg []string) { - if verbose { - if len(env) > 0 { - for i, e := range env { - fmt.Fprintf(os.Stderr, "ENV%d %v\n", i+1, e) - } - } - fmt.Fprintln(os.Stderr, exec, name, arg) - } -} - -func readVersionFile() (string, error) { - content, err := os.ReadFile("VERSION") - if err != nil { - return "", err - } - return strings.TrimFunc(string(content), func(r rune) bool { - return r <= ' ' - }), nil -} - -func getVersion() string { - base, err := readVersionFile() - if err != nil { - base = "dev" - } - return base -} - -var dirtyPrefixes = []string{ - "DELETED ", "ADDED ", "UPDATED ", "CONFLICT ", "EDITED ", "RENAMED ", "EXTRA "} - -const dirtySuffix = "-dirty" - -func readFossilDirty() (string, error) { - s, err := executeCommand(nil, "fossil", "status", "--differ") - if err != nil { - return "", err - } - for _, line := range strfun.SplitLines(s) { - for _, prefix := range dirtyPrefixes { - if strings.HasPrefix(line, prefix) { - return dirtySuffix, nil - } - } - } - return "", nil -} - -func getFossilDirty() string { - fossil, err := readFossilDirty() - if err != nil { - return "" - } - return fossil -} - -func findExec(cmd string) string { - if path, err := executeCommand(nil, "which", cmd); err == nil && path != "" { - return strings.TrimSpace(path) - } - return "" -} - -func cmdCheck(forRelease bool) error { - if err := checkGoTest("./..."); err != nil { - return err - } - if err := checkGoVet(); err != nil { - return err - } - if err := checkShadow(forRelease); err != nil { - return err - } - if err := checkStaticcheck(); err != nil { - return err - } - if err := checkUnparam(forRelease); err != nil { - return err - } - if forRelease { - if err := checkGoVulncheck(); err != nil { - return err - } - } - return checkFossilExtra() -} - -func checkGoTest(pkg string, testParams ...string) error { - var env []string - env = append(env, envDirectProxy...) - env = append(env, envGoVCS...) - args := []string{"test", pkg} - args = append(args, testParams...) - out, err := executeCommand(env, "go", args...) - if err != nil { - for _, line := range strfun.SplitLines(out) { - if strings.HasPrefix(line, "ok") || strings.HasPrefix(line, "?") { - continue - } - fmt.Fprintln(os.Stderr, line) - } - } - return err -} - -func checkGoVet() error { - out, err := executeCommand(envGoVCS, "go", "vet", "./...") - if err != nil { - fmt.Fprintln(os.Stderr, "Some checks failed") - if len(out) > 0 { - fmt.Fprintln(os.Stderr, out) - } - } - return err -} - -func checkShadow(forRelease bool) error { - path, err := findExecStrict("shadow", forRelease) - if path == "" { - return err - } - out, err := executeCommand(envGoVCS, path, "-strict", "./...") - if err != nil { - fmt.Fprintln(os.Stderr, "Some shadowed variables found") - if len(out) > 0 { - fmt.Fprintln(os.Stderr, out) - } - } - return err -} - -func checkStaticcheck() error { - out, err := executeCommand(envGoVCS, "staticcheck", "./...") - if err != nil { - fmt.Fprintln(os.Stderr, "Some staticcheck problems found") - if len(out) > 0 { - fmt.Fprintln(os.Stderr, out) - } - } - return err -} - -func checkUnparam(forRelease bool) error { - path, err := findExecStrict("unparam", forRelease) - if path == "" { - return err - } - out, err := executeCommand(envGoVCS, path, "./...") - if err != nil { - fmt.Fprintln(os.Stderr, "Some unparam problems found") - if len(out) > 0 { - fmt.Fprintln(os.Stderr, out) - } - } - if forRelease { - if out2, err2 := executeCommand(nil, path, "-exported", "-tests", "./..."); err2 != nil { - fmt.Fprintln(os.Stderr, "Some optional unparam problems found") - if len(out2) > 0 { - fmt.Fprintln(os.Stderr, out2) - } - } - } - return err -} - -func checkGoVulncheck() error { - out, err := executeCommand(envGoVCS, "govulncheck", "./...") - if err != nil { - fmt.Fprintln(os.Stderr, "Some checks failed") - if len(out) > 0 { - fmt.Fprintln(os.Stderr, out) - } - } - return err -} - -func findExecStrict(cmd string, forRelease bool) (string, error) { - path := findExec(cmd) - if path != "" || !forRelease { - return path, nil - } - return "", errors.New("Command '" + cmd + "' not installed, but required for release") -} - -func checkFossilExtra() error { - out, err := executeCommand(nil, "fossil", "extra") - if err != nil { - fmt.Fprintln(os.Stderr, "Unable to execute 'fossil extra'") - return err - } - if len(out) > 0 { - fmt.Fprint(os.Stderr, "Warning: unversioned file(s):") - for i, extra := range strfun.SplitLines(out) { - if i > 0 { - fmt.Fprint(os.Stderr, ",") - } - fmt.Fprintf(os.Stderr, " %q", extra) - } - fmt.Fprintln(os.Stderr) - } - return nil -} - -type zsInfo struct { - cmd *exec.Cmd - out strings.Builder - adminAddress string -} - -func cmdTestAPI() error { - var err error - var info zsInfo - needServer := !addressInUse(":23123") - if needServer { - err = startZettelstore(&info) - } - if err != nil { - return err - } - err = checkGoTest("zettelstore.de/z/tests/client", "-base-url", "http://127.0.0.1:23123") - if needServer { - err1 := stopZettelstore(&info) - if err == nil { - err = err1 - } - } - return err -} - -func startZettelstore(info *zsInfo) error { - info.adminAddress = ":2323" - name, arg := "go", []string{ - "run", "cmd/zettelstore/main.go", "run", - "-c", "./testdata/testbox/19700101000000.zettel", "-a", info.adminAddress[1:]} - logCommand("FORK", nil, name, arg) - cmd := prepareCommand(envGoVCS, name, arg, &info.out) - if !verbose { - cmd.Stderr = nil - } - err := cmd.Start() - time.Sleep(2 * time.Second) - for i := 0; i < 100; i++ { - time.Sleep(time.Millisecond * 100) - if addressInUse(info.adminAddress) { - info.cmd = cmd - return err - } - } - time.Sleep(4 * time.Second) // Wait for all zettel to be indexed. - return errors.New("zettelstore did not start") -} - -func stopZettelstore(i *zsInfo) error { - conn, err := net.Dial("tcp", i.adminAddress) - if err != nil { - fmt.Println("Unable to stop Zettelstore") - return err - } - io.WriteString(conn, "shutdown\n") - conn.Close() - err = i.cmd.Wait() - return err -} - -func addressInUse(address string) bool { - conn, err := net.Dial("tcp", address) - if err != nil { - return false - } - conn.Close() - return true -} - -func cmdBuild() error { - return doBuild(envDirectProxy, getVersion(), "bin/zettelstore") -} - -func doBuild(env []string, version, target string) error { - env = append(env, "CGO_ENABLED=0") - env = append(env, envGoVCS...) - out, err := executeCommand( - env, - "go", "build", - "-tags", "osusergo,netgo", - "-trimpath", - "-ldflags", fmt.Sprintf("-X main.version=%v -w", version), - "-o", target, - "zettelstore.de/z/cmd/zettelstore", - ) - if err != nil { - return err - } - if len(out) > 0 { - fmt.Println(out) - } - return nil -} - -func cmdManual() error { - base := getReleaseVersionData() - return createManualZip(".", base) -} - -func createManualZip(path, base string) error { - manualPath := filepath.Join("docs", "manual") - entries, err := os.ReadDir(manualPath) - if err != nil { - return err - } - zipName := filepath.Join(path, "manual-"+base+".zip") - zipFile, err := os.OpenFile(zipName, os.O_RDWR|os.O_CREATE, 0600) - if err != nil { - return err - } - defer zipFile.Close() - zipWriter := zip.NewWriter(zipFile) - defer zipWriter.Close() - - for _, entry := range entries { - if err = createManualZipEntry(manualPath, entry, zipWriter); err != nil { - return err - } - } - return nil -} - -const versionZid = "00001000000001" - -func createManualZipEntry(path string, entry fs.DirEntry, zipWriter *zip.Writer) error { - info, err := entry.Info() - if err != nil { - return err - } - fh, err := zip.FileInfoHeader(info) - if err != nil { - return err - } - name := entry.Name() - fh.Name = name - fh.Method = zip.Deflate - w, err := zipWriter.CreateHeader(fh) - if err != nil { - return err - } - manualFile, err := os.Open(filepath.Join(path, name)) - if err != nil { - return err - } - defer manualFile.Close() - - if name != versionZid+".zettel" { - _, err = io.Copy(w, manualFile) - return err - } - - data, err := io.ReadAll(manualFile) - if err != nil { - return err - } - inp := input.NewInput(data) - m := meta.NewFromInput(id.MustParse(versionZid), inp) - m.SetNow(api.KeyModified) - - var buf bytes.Buffer - if _, err = fmt.Fprintf(&buf, "id: %s\n", versionZid); err != nil { - return err - } - if _, err = m.WriteComputed(&buf); err != nil { - return err - } - version := getVersion() - if _, err = fmt.Fprintf(&buf, "\n%s", version); err != nil { - return err - } - _, err = io.Copy(w, &buf) - return err -} - -func getReleaseVersionData() string { - 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().Local().Format("20060102") - } - return base -} - -func cmdRelease() error { - if err := cmdCheck(true); err != nil { - return err - } - base := getReleaseVersionData() - releases := []struct { - arch string - os string - env []string - name string - }{ - {"amd64", "linux", nil, "zettelstore"}, - {"arm", "linux", []string{"GOARM=6"}, "zettelstore"}, - {"amd64", "darwin", nil, "zettelstore"}, - {"arm64", "darwin", nil, "zettelstore"}, - {"amd64", "windows", nil, "zettelstore.exe"}, - } - for _, rel := range releases { - env := append([]string{}, rel.env...) - env = append(env, "GOARCH="+rel.arch, "GOOS="+rel.os) - env = append(env, envDirectProxy...) - env = append(env, envGoVCS...) - zsName := filepath.Join("releases", rel.name) - if err := doBuild(env, base, zsName); err != nil { - return err - } - zipName := fmt.Sprintf("zettelstore-%v-%v-%v.zip", base, rel.os, rel.arch) - if err := createReleaseZip(zsName, zipName, rel.name); err != nil { - return err - } - if err := os.Remove(zsName); err != nil { - return err - } - } - return createManualZip("releases", base) -} - -func createReleaseZip(zsName, zipName, fileName string) error { - zipFile, err := os.OpenFile(filepath.Join("releases", zipName), os.O_RDWR|os.O_CREATE, 0600) - if err != nil { - return err - } - defer zipFile.Close() - zw := zip.NewWriter(zipFile) - defer zw.Close() - err = addFileToZip(zw, zsName, fileName) - if err != nil { - return err - } - err = addFileToZip(zw, "LICENSE.txt", "LICENSE.txt") - if err != nil { - return err - } - err = addFileToZip(zw, "docs/readmezip.txt", "README.txt") - return err -} - -func addFileToZip(zipFile *zip.Writer, filepath, filename string) error { - zsFile, err := os.Open(filepath) - if err != nil { - return err - } - defer zsFile.Close() - stat, err := zsFile.Stat() - if err != nil { - return err - } - fh, err := zip.FileInfoHeader(stat) - if err != nil { - return err - } - fh.Name = filename - fh.Method = zip.Deflate - w, err := zipFile.CreateHeader(fh) - if err != nil { - return err - } - _, err = io.Copy(w, zsFile) - return err -} - -func cmdTools() error { - tools := []struct{ name, pack string }{ - {"shadow", "golang.org/x/tools/go/analysis/passes/shadow/cmd/shadow@latest"}, - {"unparam", "mvdan.cc/unparam@latest"}, - {"staticcheck", "honnef.co/go/tools/cmd/staticcheck@latest"}, - {"govulncheck", "golang.org/x/vuln/cmd/govulncheck@latest"}, - } - for _, tool := range tools { - err := doGoInstall(tool.pack) - if err != nil { - return err - } - } - return nil -} -func doGoInstall(pack string) error { - out, err := executeCommand(nil, "go", "install", pack) - if err != nil { - fmt.Fprintln(os.Stderr, "Unable to install package", pack) - if len(out) > 0 { - fmt.Fprintln(os.Stderr, out) - } - } - return err -} - -func cmdClean() error { - for _, dir := range []string{"bin", "releases"} { - err := os.RemoveAll(dir) - if err != nil { - return err - } - } - out, err := executeCommand(nil, "go", "clean", "./...") - if err != nil { - return err - } - if len(out) > 0 { - fmt.Println(out) - } - out, err = executeCommand(nil, "go", "clean", "-cache", "-modcache", "-testcache") - if err != nil { - return err - } - if len(out) > 0 { - fmt.Println(out) - } - return nil -} - -func cmdHelp() { - fmt.Println(`Usage: go run tools/build.go [-v] COMMAND - -Options: - -v Verbose output. - -Commands: - build Build the software for local computer. - check Check current working state: execute tests, - static analysis tools, extra files, ... - Is automatically done when releasing the software. - clean Remove all build and release directories. - help Output this text. - manual Create a ZIP file with all manual zettel - relcheck Check current working state for release. - release Create the software for various platforms and put them in - appropriate named ZIP files. - testapi Start a Zettelstore and execute API tests. - tools Install/update tools needed for building Zettelstore. - version Print the current version of the software. - -All commands can be abbreviated as long as they remain unique.`) -} - -var verbose bool - -func main() { - flag.BoolVar(&verbose, "v", false, "Verbose output") - flag.Parse() - var err error - args := flag.Args() - if len(args) < 1 { - cmdHelp() - } else { - switch args[0] { - case "b", "bu", "bui", "buil", "build": - err = cmdBuild() - case "m", "ma", "man", "manu", "manua", "manual": - err = cmdManual() - case "r", "re", "rel", "rele", "relea", "releas", "release": - err = cmdRelease() - case "cl", "cle", "clea", "clean": - err = cmdClean() - case "v", "ve", "ver", "vers", "versi", "versio", "version": - fmt.Print(getVersion()) - case "ch", "che", "chec", "check": - err = cmdCheck(false) - case "relc", "relch", "relche", "relchec", "relcheck": - err = cmdCheck(true) - case "te", "tes", "test", "testa", "testap", "testapi": - cmdTestAPI() - case "to", "too", "tool", "tools": - err = cmdTools() - case "h", "he", "hel", "help": - cmdHelp() - default: - fmt.Fprintf(os.Stderr, "Unknown command %q\n", args[0]) - cmdHelp() - os.Exit(1) - } - } - if err != nil { - fmt.Fprintln(os.Stderr, err) - } -} ADDED tools/build/build.go Index: tools/build/build.go ================================================================== --- /dev/null +++ tools/build/build.go @@ -0,0 +1,329 @@ +//----------------------------------------------------------------------------- +// Copyright (c) 2021-present 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. +// +// SPDX-License-Identifier: EUPL-1.2 +// SPDX-FileCopyrightText: 2021-present Detlef Stern +//----------------------------------------------------------------------------- + +// Package main provides a command to build and run the software. +package main + +import ( + "archive/zip" + "bytes" + "flag" + "fmt" + "io" + "io/fs" + "os" + "path/filepath" + "strings" + "time" + + "zettelstore.de/client.fossil/api" + "zettelstore.de/client.fossil/input" + "zettelstore.de/z/strfun" + "zettelstore.de/z/tools" + "zettelstore.de/z/zettel/id" + "zettelstore.de/z/zettel/meta" +) + +func readVersionFile() (string, error) { + content, err := os.ReadFile("VERSION") + if err != nil { + return "", err + } + return strings.TrimFunc(string(content), func(r rune) bool { + return r <= ' ' + }), nil +} + +func getVersion() string { + base, err := readVersionFile() + if err != nil { + base = "dev" + } + return base +} + +var dirtyPrefixes = []string{ + "DELETED ", "ADDED ", "UPDATED ", "CONFLICT ", "EDITED ", "RENAMED ", "EXTRA "} + +const dirtySuffix = "-dirty" + +func readFossilDirty() (string, error) { + s, err := tools.ExecuteCommand(nil, "fossil", "status", "--differ") + if err != nil { + return "", err + } + for _, line := range strfun.SplitLines(s) { + for _, prefix := range dirtyPrefixes { + if strings.HasPrefix(line, prefix) { + return dirtySuffix, nil + } + } + } + return "", nil +} + +func getFossilDirty() string { + fossil, err := readFossilDirty() + if err != nil { + return "" + } + return fossil +} + +func cmdBuild() error { + return doBuild(tools.EnvDirectProxy, getVersion(), "bin/zettelstore") +} + +func doBuild(env []string, version, target string) error { + env = append(env, "CGO_ENABLED=0") + env = append(env, tools.EnvGoVCS...) + out, err := tools.ExecuteCommand( + env, + "go", "build", + "-tags", "osusergo,netgo", + "-trimpath", + "-ldflags", fmt.Sprintf("-X main.version=%v -w", version), + "-o", target, + "zettelstore.de/z/cmd/zettelstore", + ) + if err != nil { + return err + } + if len(out) > 0 { + fmt.Println(out) + } + return nil +} + +func cmdHelp() { + fmt.Println(`Usage: go run tools/build/build.go [-v] COMMAND + +Options: + -v Verbose output. + +Commands: + build Build the software for local computer. + help Output this text. + manual Create a ZIP file with all manual zettel + release Create the software for various platforms and put them in + appropriate named ZIP files. + version Print the current version of the software. + +All commands can be abbreviated as long as they remain unique.`) +} + +func main() { + flag.BoolVar(&tools.Verbose, "v", false, "Verbose output") + flag.Parse() + var err error + args := flag.Args() + if len(args) < 1 { + cmdHelp() + } else { + switch args[0] { + case "b", "bu", "bui", "buil", "build": + err = cmdBuild() + case "m", "ma", "man", "manu", "manua", "manual": + err = cmdManual() + case "r", "re", "rel", "rele", "relea", "releas", "release": + err = cmdRelease() + case "v", "ve", "ver", "vers", "versi", "versio", "version": + fmt.Print(getVersion()) + case "h", "he", "hel", "help": + cmdHelp() + default: + fmt.Fprintf(os.Stderr, "Unknown command %q\n", args[0]) + cmdHelp() + os.Exit(1) + } + } + if err != nil { + fmt.Fprintln(os.Stderr, err) + } +} + +// --- manual + +func cmdManual() error { + base := getReleaseVersionData() + return createManualZip(".", base) +} + +func createManualZip(path, base string) error { + manualPath := filepath.Join("docs", "manual") + entries, err := os.ReadDir(manualPath) + if err != nil { + return err + } + zipName := filepath.Join(path, "manual-"+base+".zip") + zipFile, err := os.OpenFile(zipName, os.O_RDWR|os.O_CREATE, 0600) + if err != nil { + return err + } + defer zipFile.Close() + zipWriter := zip.NewWriter(zipFile) + defer zipWriter.Close() + + for _, entry := range entries { + if err = createManualZipEntry(manualPath, entry, zipWriter); err != nil { + return err + } + } + return nil +} + +const versionZid = "00001000000001" + +func createManualZipEntry(path string, entry fs.DirEntry, zipWriter *zip.Writer) error { + info, err := entry.Info() + if err != nil { + return err + } + fh, err := zip.FileInfoHeader(info) + if err != nil { + return err + } + name := entry.Name() + fh.Name = name + fh.Method = zip.Deflate + w, err := zipWriter.CreateHeader(fh) + if err != nil { + return err + } + manualFile, err := os.Open(filepath.Join(path, name)) + if err != nil { + return err + } + defer manualFile.Close() + + if name != versionZid+".zettel" { + _, err = io.Copy(w, manualFile) + return err + } + + data, err := io.ReadAll(manualFile) + if err != nil { + return err + } + inp := input.NewInput(data) + m := meta.NewFromInput(id.MustParse(versionZid), inp) + m.SetNow(api.KeyModified) + + var buf bytes.Buffer + if _, err = fmt.Fprintf(&buf, "id: %s\n", versionZid); err != nil { + return err + } + if _, err = m.WriteComputed(&buf); err != nil { + return err + } + version := getVersion() + if _, err = fmt.Fprintf(&buf, "\n%s", version); err != nil { + return err + } + _, err = io.Copy(w, &buf) + return err +} + +//--- release + +func cmdRelease() error { + if err := tools.Check(true); err != nil { + return err + } + base := getReleaseVersionData() + releases := []struct { + arch string + os string + env []string + name string + }{ + {"amd64", "linux", nil, "zettelstore"}, + {"arm", "linux", []string{"GOARM=6"}, "zettelstore"}, + {"amd64", "darwin", nil, "zettelstore"}, + {"arm64", "darwin", nil, "zettelstore"}, + {"amd64", "windows", nil, "zettelstore.exe"}, + } + for _, rel := range releases { + env := append([]string{}, rel.env...) + env = append(env, "GOARCH="+rel.arch, "GOOS="+rel.os) + env = append(env, tools.EnvDirectProxy...) + env = append(env, tools.EnvGoVCS...) + zsName := filepath.Join("releases", rel.name) + if err := doBuild(env, base, zsName); err != nil { + return err + } + zipName := fmt.Sprintf("zettelstore-%v-%v-%v.zip", base, rel.os, rel.arch) + if err := createReleaseZip(zsName, zipName, rel.name); err != nil { + return err + } + if err := os.Remove(zsName); err != nil { + return err + } + } + return createManualZip("releases", base) +} + +func getReleaseVersionData() string { + 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().Local().Format("20060102") + } + return base +} + +func createReleaseZip(zsName, zipName, fileName string) error { + zipFile, err := os.OpenFile(filepath.Join("releases", zipName), os.O_RDWR|os.O_CREATE, 0600) + if err != nil { + return err + } + defer zipFile.Close() + zw := zip.NewWriter(zipFile) + defer zw.Close() + err = addFileToZip(zw, zsName, fileName) + if err != nil { + return err + } + err = addFileToZip(zw, "LICENSE.txt", "LICENSE.txt") + if err != nil { + return err + } + err = addFileToZip(zw, "docs/readmezip.txt", "README.txt") + return err +} + +func addFileToZip(zipFile *zip.Writer, filepath, filename string) error { + zsFile, err := os.Open(filepath) + if err != nil { + return err + } + defer zsFile.Close() + stat, err := zsFile.Stat() + if err != nil { + return err + } + fh, err := zip.FileInfoHeader(stat) + if err != nil { + return err + } + fh.Name = filename + fh.Method = zip.Deflate + w, err := zipFile.CreateHeader(fh) + if err != nil { + return err + } + _, err = io.Copy(w, zsFile) + return err +} ADDED tools/check/check.go Index: tools/check/check.go ================================================================== --- /dev/null +++ tools/check/check.go @@ -0,0 +1,35 @@ +//----------------------------------------------------------------------------- +// Copyright (c) 2023-present 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. +// +// SPDX-License-Identifier: EUPL-1.2 +// SPDX-FileCopyrightText: 2023-present Detlef Stern +//----------------------------------------------------------------------------- + +// Package main provides a command to execute unit tests. +package main + +import ( + "flag" + "fmt" + "os" + + "zettelstore.de/z/tools" +) + +var release bool + +func main() { + flag.BoolVar(&release, "r", false, "Release check") + flag.BoolVar(&tools.Verbose, "v", false, "Verbose output") + flag.Parse() + + if err := tools.Check(release); err != nil { + fmt.Fprintln(os.Stderr, err) + } +} ADDED tools/clean/clean.go Index: tools/clean/clean.go ================================================================== --- /dev/null +++ tools/clean/clean.go @@ -0,0 +1,56 @@ +//----------------------------------------------------------------------------- +// Copyright (c) 2023-present 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. +// +// SPDX-License-Identifier: EUPL-1.2 +// SPDX-FileCopyrightText: 2023-present Detlef Stern +//----------------------------------------------------------------------------- + +// Package main provides a command to clean / remove development artifacts. +package main + +import ( + "flag" + "fmt" + "os" + + "zettelstore.de/z/tools" +) + +func main() { + flag.BoolVar(&tools.Verbose, "v", false, "Verbose output") + flag.Parse() + + if err := cmdClean(); err != nil { + fmt.Fprintln(os.Stderr, err) + } +} + +func cmdClean() error { + for _, dir := range []string{"bin", "releases"} { + err := os.RemoveAll(dir) + if err != nil { + return err + } + } + out, err := tools.ExecuteCommand(nil, "go", "clean", "./...") + if err != nil { + return err + } + if len(out) > 0 { + fmt.Println(out) + } + out, err = tools.ExecuteCommand(nil, "go", "clean", "-cache", "-modcache", "-testcache") + if err != nil { + return err + } + if len(out) > 0 { + fmt.Println(out) + } + return nil +} ADDED tools/devtools/devtools.go Index: tools/devtools/devtools.go ================================================================== --- /dev/null +++ tools/devtools/devtools.go @@ -0,0 +1,60 @@ +//----------------------------------------------------------------------------- +// Copyright (c) 2023-present 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. +// +// SPDX-License-Identifier: EUPL-1.2 +// SPDX-FileCopyrightText: 2023-present Detlef Stern +//----------------------------------------------------------------------------- + +// Package main provides a command to install development tools. +package main + +import ( + "flag" + "fmt" + "os" + + "zettelstore.de/z/tools" +) + +func main() { + flag.BoolVar(&tools.Verbose, "v", false, "Verbose output") + flag.Parse() + + if err := cmdTools(); err != nil { + fmt.Fprintln(os.Stderr, err) + } +} + +func cmdTools() error { + tools := []struct{ name, pack string }{ + {"shadow", "golang.org/x/tools/go/analysis/passes/shadow/cmd/shadow@latest"}, + {"unparam", "mvdan.cc/unparam@latest"}, + {"staticcheck", "honnef.co/go/tools/cmd/staticcheck@latest"}, + {"govulncheck", "golang.org/x/vuln/cmd/govulncheck@latest"}, + {"deadcode", "golang.org/x/tools/cmd/deadcode@latest"}, + {"errcheck", "github.com/kisielk/errcheck@latest"}, + } + for _, tool := range tools { + err := doGoInstall(tool.pack) + if err != nil { + return err + } + } + return nil +} +func doGoInstall(pack string) error { + out, err := tools.ExecuteCommand(nil, "go", "install", pack) + if err != nil { + fmt.Fprintln(os.Stderr, "Unable to install package", pack) + if len(out) > 0 { + fmt.Fprintln(os.Stderr, out) + } + } + return err +} ADDED tools/htmllint/htmllint.go Index: tools/htmllint/htmllint.go ================================================================== --- /dev/null +++ tools/htmllint/htmllint.go @@ -0,0 +1,205 @@ +//----------------------------------------------------------------------------- +// Copyright (c) 2023-present 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. +// +// SPDX-License-Identifier: EUPL-1.2 +// SPDX-FileCopyrightText: 2023-present Detlef Stern +//----------------------------------------------------------------------------- + +package main + +import ( + "context" + "flag" + "fmt" + "log" + "math/rand/v2" + "net/url" + "os" + "regexp" + "sort" + "strings" + + "zettelstore.de/client.fossil/api" + "zettelstore.de/client.fossil/client" + "zettelstore.de/z/tools" +) + +func main() { + flag.BoolVar(&tools.Verbose, "v", false, "Verbose output") + flag.Parse() + + if err := cmdValidateHTML(flag.Args()); err != nil { + fmt.Fprintln(os.Stderr, err) + } +} +func cmdValidateHTML(args []string) error { + rawURL := "http://localhost:23123" + if len(args) > 0 { + rawURL = args[0] + } + u, err := url.Parse(rawURL) + if err != nil { + return err + } + client := client.NewClient(u) + _, _, metaList, err := client.QueryZettelData(context.Background(), "") + if err != nil { + return err + } + zids, perm := calculateZids(metaList) + for _, kd := range keyDescr { + msgCount := 0 + fmt.Fprintf(os.Stderr, "Now checking: %s\n", kd.text) + for _, zid := range zidsToUse(zids, perm, kd.sampleSize) { + var nmsgs int + nmsgs, err = validateHTML(client, kd.uc, api.ZettelID(zid)) + if err != nil { + fmt.Fprintf(os.Stderr, "* error while validating zettel %v with: %v\n", zid, err) + msgCount += 1 + } else { + msgCount += nmsgs + } + } + if msgCount == 1 { + fmt.Fprintln(os.Stderr, "==> found 1 possible issue") + } else if msgCount > 1 { + fmt.Fprintf(os.Stderr, "==> found %v possible issues\n", msgCount) + } + } + return nil +} + +func calculateZids(metaList []api.ZidMetaRights) ([]string, []int) { + zids := make([]string, len(metaList)) + for i, m := range metaList { + zids[i] = string(m.ID) + } + sort.Strings(zids) + return zids, rand.Perm(len(metaList)) +} + +func zidsToUse(zids []string, perm []int, sampleSize int) []string { + if sampleSize < 0 || len(perm) <= sampleSize { + return zids + } + if sampleSize == 0 { + return nil + } + result := make([]string, sampleSize) + for i := range sampleSize { + result[i] = zids[perm[i]] + } + sort.Strings(result) + return result +} + +var keyDescr = []struct { + uc urlCreator + text string + sampleSize int +}{ + {getHTMLZettel, "zettel HTML encoding", -1}, + {createJustKey('h'), "zettel web view", -1}, + {createJustKey('i'), "zettel info view", -1}, + {createJustKey('e'), "zettel edit form", 100}, + {createJustKey('c'), "zettel create form", 10}, + {createJustKey('b'), "zettel rename form", 100}, + {createJustKey('d'), "zettel delete dialog", 200}, +} + +type urlCreator func(*client.Client, api.ZettelID) *api.URLBuilder + +func createJustKey(key byte) urlCreator { + return func(c *client.Client, zid api.ZettelID) *api.URLBuilder { + return c.NewURLBuilder(key).SetZid(zid) + } +} + +func getHTMLZettel(client *client.Client, zid api.ZettelID) *api.URLBuilder { + return client.NewURLBuilder('z').SetZid(zid). + AppendKVQuery(api.QueryKeyEncoding, api.EncodingHTML). + AppendKVQuery(api.QueryKeyPart, api.PartZettel) +} + +func validateHTML(client *client.Client, uc urlCreator, zid api.ZettelID) (int, error) { + ub := uc(client, zid) + if tools.Verbose { + fmt.Fprintf(os.Stderr, "GET %v\n", ub) + } + data, err := client.Get(context.Background(), ub) + if err != nil { + return 0, err + } + if len(data) == 0 { + return 0, nil + } + _, stderr, err := tools.ExecuteFilter(data, nil, "tidy", "-e", "-q", "-lang", "en") + if err != nil { + switch err.Error() { + case "exit status 1": + case "exit status 2": + default: + log.Println("SERR", stderr) + return 0, err + } + } + if stderr == "" { + return 0, nil + } + if msgs := filterTidyMessages(strings.Split(stderr, "\n")); len(msgs) > 0 { + fmt.Fprintln(os.Stderr, zid) + for _, msg := range msgs { + fmt.Fprintln(os.Stderr, "-", msg) + } + return len(msgs), nil + } + return 0, nil +} + +var reLine = regexp.MustCompile(`line \d+ column \d+ - (.+): (.+)`) + +func filterTidyMessages(lines []string) []string { + result := make([]string, 0, len(lines)) + for _, line := range lines { + line = strings.TrimSpace(line) + if line == "" { + continue + } + matches := reLine.FindStringSubmatch(line) + if len(matches) <= 1 { + if line == "This document has errors that must be fixed before" || + line == "using HTML Tidy to generate a tidied up version." { + continue + } + result = append(result, "!!!"+line) + continue + } + if matches[1] == "Error" { + if len(matches) > 2 { + if matches[2] == " is not recognized!" { + continue + } + } + } + if matches[1] != "Warning" { + result = append(result, "???"+line) + continue + } + if len(matches) > 2 { + switch matches[2] { + case "discarding unexpected ", + "discarding unexpected ", + ` proprietary attribute "inputmode"`: + continue + } + } + result = append(result, line) + } + return result +} ADDED tools/testapi/testapi.go Index: tools/testapi/testapi.go ================================================================== --- /dev/null +++ tools/testapi/testapi.go @@ -0,0 +1,108 @@ +//----------------------------------------------------------------------------- +// Copyright (c) 2023-present 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. +// +// SPDX-License-Identifier: EUPL-1.2 +// SPDX-FileCopyrightText: 2023-present Detlef Stern +//----------------------------------------------------------------------------- + +// Package main provides a command to test the API +package main + +import ( + "errors" + "flag" + "fmt" + "io" + "net" + "os" + "os/exec" + "strings" + "time" + + "zettelstore.de/z/tools" +) + +func main() { + flag.BoolVar(&tools.Verbose, "v", false, "Verbose output") + flag.Parse() + + if err := cmdTestAPI(); err != nil { + fmt.Fprintln(os.Stderr, err) + } +} + +type zsInfo struct { + cmd *exec.Cmd + out strings.Builder + adminAddress string +} + +func cmdTestAPI() error { + var err error + var info zsInfo + needServer := !addressInUse(":23123") + if needServer { + err = startZettelstore(&info) + } + if err != nil { + return err + } + err = tools.CheckGoTest("zettelstore.de/z/tests/client", "-base-url", "http://127.0.0.1:23123") + if needServer { + err1 := stopZettelstore(&info) + if err == nil { + err = err1 + } + } + return err +} + +func startZettelstore(info *zsInfo) error { + info.adminAddress = ":2323" + name, arg := "go", []string{ + "run", "cmd/zettelstore/main.go", "run", + "-c", "./testdata/testbox/19700101000000.zettel", "-a", info.adminAddress[1:]} + tools.LogCommand("FORK", nil, name, arg) + cmd := tools.PrepareCommand(tools.EnvGoVCS, name, arg, nil, &info.out, os.Stderr) + if !tools.Verbose { + cmd.Stderr = nil + } + err := cmd.Start() + time.Sleep(2 * time.Second) + for range 100 { + time.Sleep(time.Millisecond * 100) + if addressInUse(info.adminAddress) { + info.cmd = cmd + return err + } + } + time.Sleep(4 * time.Second) // Wait for all zettel to be indexed. + return errors.New("zettelstore did not start") +} + +func stopZettelstore(i *zsInfo) error { + conn, err := net.Dial("tcp", i.adminAddress) + if err != nil { + fmt.Println("Unable to stop Zettelstore") + return err + } + io.WriteString(conn, "shutdown\n") + conn.Close() + err = i.cmd.Wait() + return err +} + +func addressInUse(address string) bool { + conn, err := net.Dial("tcp", address) + if err != nil { + return false + } + conn.Close() + return true +} ADDED tools/tools.go Index: tools/tools.go ================================================================== --- /dev/null +++ tools/tools.go @@ -0,0 +1,214 @@ +//----------------------------------------------------------------------------- +// Copyright (c) 2023-present 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. +// +// SPDX-License-Identifier: EUPL-1.2 +// SPDX-FileCopyrightText: 2023-present Detlef Stern +//----------------------------------------------------------------------------- + +// Package tools provides a collection of functions to build needed tools. +package tools + +import ( + "bytes" + "errors" + "fmt" + "io" + "os" + "os/exec" + "strings" + + "zettelstore.de/z/strfun" +) + +var EnvDirectProxy = []string{"GOPROXY=direct"} +var EnvGoVCS = []string{"GOVCS=zettelstore.de:fossil"} +var Verbose bool + +func ExecuteCommand(env []string, name string, arg ...string) (string, error) { + LogCommand("EXEC", env, name, arg) + var out strings.Builder + cmd := PrepareCommand(env, name, arg, nil, &out, os.Stderr) + err := cmd.Run() + return out.String(), err +} + +func ExecuteFilter(data []byte, env []string, name string, arg ...string) (string, string, error) { + LogCommand("EXEC", env, name, arg) + var stdout, stderr strings.Builder + cmd := PrepareCommand(env, name, arg, bytes.NewReader(data), &stdout, &stderr) + err := cmd.Run() + return stdout.String(), stderr.String(), err +} + +func PrepareCommand(env []string, name string, arg []string, in io.Reader, stdout, stderr io.Writer) *exec.Cmd { + if len(env) > 0 { + env = append(env, os.Environ()...) + } + cmd := exec.Command(name, arg...) + cmd.Env = env + cmd.Stdin = in + cmd.Stdout = stdout + cmd.Stderr = stderr + return cmd +} +func LogCommand(exec string, env []string, name string, arg []string) { + if Verbose { + if len(env) > 0 { + for i, e := range env { + fmt.Fprintf(os.Stderr, "ENV%d %v\n", i+1, e) + } + } + fmt.Fprintln(os.Stderr, exec, name, arg) + } +} + +func Check(forRelease bool) error { + if err := CheckGoTest("./..."); err != nil { + return err + } + if err := checkGoVet(); err != nil { + return err + } + if err := checkShadow(forRelease); err != nil { + return err + } + if err := checkStaticcheck(); err != nil { + return err + } + if err := checkUnparam(forRelease); err != nil { + return err + } + if forRelease { + if err := checkGoVulncheck(); err != nil { + return err + } + } + return checkFossilExtra() +} + +func CheckGoTest(pkg string, testParams ...string) error { + var env []string + env = append(env, EnvDirectProxy...) + env = append(env, EnvGoVCS...) + args := []string{"test", pkg} + args = append(args, testParams...) + out, err := ExecuteCommand(env, "go", args...) + if err != nil { + for _, line := range strfun.SplitLines(out) { + if strings.HasPrefix(line, "ok") || strings.HasPrefix(line, "?") { + continue + } + fmt.Fprintln(os.Stderr, line) + } + } + return err +} +func checkGoVet() error { + out, err := ExecuteCommand(EnvGoVCS, "go", "vet", "./...") + if err != nil { + fmt.Fprintln(os.Stderr, "Some checks failed") + if len(out) > 0 { + fmt.Fprintln(os.Stderr, out) + } + } + return err +} + +func checkShadow(forRelease bool) error { + path, err := findExecStrict("shadow", forRelease) + if path == "" { + return err + } + out, err := ExecuteCommand(EnvGoVCS, path, "-strict", "./...") + if err != nil { + fmt.Fprintln(os.Stderr, "Some shadowed variables found") + if len(out) > 0 { + fmt.Fprintln(os.Stderr, out) + } + } + return err +} + +func checkStaticcheck() error { + out, err := ExecuteCommand(EnvGoVCS, "staticcheck", "./...") + if err != nil { + fmt.Fprintln(os.Stderr, "Some staticcheck problems found") + if len(out) > 0 { + fmt.Fprintln(os.Stderr, out) + } + } + return err +} + +func checkUnparam(forRelease bool) error { + path, err := findExecStrict("unparam", forRelease) + if path == "" { + return err + } + out, err := ExecuteCommand(EnvGoVCS, path, "./...") + if err != nil { + fmt.Fprintln(os.Stderr, "Some unparam problems found") + if len(out) > 0 { + fmt.Fprintln(os.Stderr, out) + } + } + if forRelease { + if out2, err2 := ExecuteCommand(nil, path, "-exported", "-tests", "./..."); err2 != nil { + fmt.Fprintln(os.Stderr, "Some optional unparam problems found") + if len(out2) > 0 { + fmt.Fprintln(os.Stderr, out2) + } + } + } + return err +} + +func checkGoVulncheck() error { + out, err := ExecuteCommand(EnvGoVCS, "govulncheck", "./...") + if err != nil { + fmt.Fprintln(os.Stderr, "Some checks failed") + if len(out) > 0 { + fmt.Fprintln(os.Stderr, out) + } + } + return err +} +func findExec(cmd string) string { + if path, err := ExecuteCommand(nil, "which", cmd); err == nil && path != "" { + return strings.TrimSpace(path) + } + return "" +} + +func findExecStrict(cmd string, forRelease bool) (string, error) { + path := findExec(cmd) + if path != "" || !forRelease { + return path, nil + } + return "", errors.New("Command '" + cmd + "' not installed, but required for release") +} + +func checkFossilExtra() error { + out, err := ExecuteCommand(nil, "fossil", "extra") + if err != nil { + fmt.Fprintln(os.Stderr, "Unable to execute 'fossil extra'") + return err + } + if len(out) > 0 { + fmt.Fprint(os.Stderr, "Warning: unversioned file(s):") + for i, extra := range strfun.SplitLines(out) { + if i > 0 { + fmt.Fprint(os.Stderr, ",") + } + fmt.Fprintf(os.Stderr, " %q", extra) + } + fmt.Fprintln(os.Stderr) + } + return nil +} Index: usecase/authenticate.go ================================================================== --- usecase/authenticate.go +++ usecase/authenticate.go @@ -4,17 +4,20 @@ // 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. +// +// SPDX-License-Identifier: EUPL-1.2 +// SPDX-FileCopyrightText: 2020-present Detlef Stern //----------------------------------------------------------------------------- package usecase import ( "context" - "math/rand" + "math/rand/v2" "net/http" "time" "zettelstore.de/client.fossil/api" "zettelstore.de/z/auth" @@ -85,11 +88,11 @@ // addDelay after credential checking to allow some CPU time for other tasks. // durDelay is the normal delay, if time spend for checking is smaller than // the minimum delay minDelay. In addition some jitter (+/- 50 ms) is added. func addDelay(start time.Time, durDelay, minDelay time.Duration) { - jitter := time.Duration(rand.Intn(100)-50) * time.Millisecond + jitter := time.Duration(rand.IntN(100)-50) * time.Millisecond if elapsed := time.Since(start); elapsed+minDelay < durDelay { time.Sleep(durDelay - elapsed + jitter) } else { time.Sleep(minDelay + jitter) } @@ -128,15 +131,15 @@ ) // Run executes the use case. func (uc *IsAuthenticated) Run(ctx context.Context) IsAuthenticatedResult { if !uc.authz.WithAuth() { - uc.log.Sense().Str("auth", "disabled").Msg("IsAuthenticated") + uc.log.Info().Str("auth", "disabled").Msg("IsAuthenticated") return IsAuthenticatedDisabled } if uc.port.GetUser(ctx) == nil { - uc.log.Sense().Msg("IsAuthenticated is false") + uc.log.Info().Msg("IsAuthenticated is false") return IsAuthenticatedAndInvalid } - uc.log.Sense().Msg("IsAuthenticated is true") + uc.log.Info().Msg("IsAuthenticated is true") return IsAuthenticatedAndValid } Index: usecase/create_zettel.go ================================================================== --- usecase/create_zettel.go +++ usecase/create_zettel.go @@ -4,10 +4,13 @@ // 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. +// +// SPDX-License-Identifier: EUPL-1.2 +// SPDX-FileCopyrightText: 2020-present Detlef Stern //----------------------------------------------------------------------------- package usecase import ( @@ -114,11 +117,11 @@ } func updateMetaRoleTagsSyntax(m, orig *meta.Meta) { m.SetNonEmpty(api.KeyRole, orig.GetDefault(api.KeyRole, "")) m.SetNonEmpty(api.KeyTags, orig.GetDefault(api.KeyTags, "")) - m.SetNonEmpty(api.KeySyntax, orig.GetDefault(api.KeySyntax, "")) + m.SetNonEmpty(api.KeySyntax, orig.GetDefault(api.KeySyntax, meta.DefaultSyntax)) } func prependTitle(title, s0, s1 string) string { if len(title) > 0 { return s1 + title Index: usecase/delete_zettel.go ================================================================== --- usecase/delete_zettel.go +++ usecase/delete_zettel.go @@ -4,10 +4,13 @@ // 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. +// +// SPDX-License-Identifier: EUPL-1.2 +// SPDX-FileCopyrightText: 2020-present Detlef Stern //----------------------------------------------------------------------------- package usecase import ( Index: usecase/evaluate.go ================================================================== --- usecase/evaluate.go +++ usecase/evaluate.go @@ -4,10 +4,13 @@ // 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. +// +// SPDX-License-Identifier: EUPL-1.2 +// SPDX-FileCopyrightText: 2021-present Detlef Stern //----------------------------------------------------------------------------- package usecase import ( Index: usecase/get_all_zettel.go ================================================================== --- usecase/get_all_zettel.go +++ usecase/get_all_zettel.go @@ -4,10 +4,13 @@ // 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. +// +// SPDX-License-Identifier: EUPL-1.2 +// SPDX-FileCopyrightText: 2021-present Detlef Stern //----------------------------------------------------------------------------- package usecase import ( Index: usecase/get_special_zettel.go ================================================================== --- usecase/get_special_zettel.go +++ usecase/get_special_zettel.go @@ -4,10 +4,13 @@ // 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. +// +// SPDX-License-Identifier: EUPL-1.2 +// SPDX-FileCopyrightText: 2023-present Detlef Stern //----------------------------------------------------------------------------- package usecase import ( Index: usecase/get_user.go ================================================================== --- usecase/get_user.go +++ usecase/get_user.go @@ -4,10 +4,13 @@ // 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. +// +// SPDX-License-Identifier: EUPL-1.2 +// SPDX-FileCopyrightText: 2020-present Detlef Stern //----------------------------------------------------------------------------- package usecase import ( Index: usecase/get_zettel.go ================================================================== --- usecase/get_zettel.go +++ usecase/get_zettel.go @@ -4,10 +4,13 @@ // 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. +// +// SPDX-License-Identifier: EUPL-1.2 +// SPDX-FileCopyrightText: 2020-present Detlef Stern //----------------------------------------------------------------------------- package usecase import ( Index: usecase/lists.go ================================================================== --- usecase/lists.go +++ usecase/lists.go @@ -4,10 +4,13 @@ // 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. +// +// SPDX-License-Identifier: EUPL-1.2 +// SPDX-FileCopyrightText: 2020-present Detlef Stern //----------------------------------------------------------------------------- package usecase import ( Index: usecase/parse_zettel.go ================================================================== --- usecase/parse_zettel.go +++ usecase/parse_zettel.go @@ -4,10 +4,13 @@ // 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. +// +// SPDX-License-Identifier: EUPL-1.2 +// SPDX-FileCopyrightText: 2020-present Detlef Stern //----------------------------------------------------------------------------- package usecase import ( Index: usecase/query.go ================================================================== --- usecase/query.go +++ usecase/query.go @@ -4,10 +4,13 @@ // 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. +// +// SPDX-License-Identifier: EUPL-1.2 +// SPDX-FileCopyrightText: 2020-present Detlef Stern //----------------------------------------------------------------------------- package usecase import ( @@ -115,11 +118,11 @@ } func (uc *Query) processItemsDirective(ctx context.Context, _ *query.ItemsSpec, metaSeq []*meta.Meta) []*meta.Meta { result := make([]*meta.Meta, 0, len(metaSeq)) for _, m := range metaSeq { - zn, err := uc.ucEvaluate.Run(ctx, m.Zid, m.GetDefault(api.KeySyntax, "")) + zn, err := uc.ucEvaluate.Run(ctx, m.Zid, m.GetDefault(api.KeySyntax, meta.DefaultSyntax)) if err != nil { continue } for _, ref := range collect.Order(zn) { if collectedZid, err2 := id.Parse(ref.URL.Path); err2 == nil { @@ -205,11 +208,11 @@ result = append(result, cand) continue candLoop } } - syntax := zettel.Meta.GetDefault(api.KeySyntax, "") + syntax := zettel.Meta.GetDefault(api.KeySyntax, meta.DefaultSyntax) if !parser.IsASTParser(syntax) { continue } zn := uc.ucEvaluate.RunZettel(ctx, zettel, syntax) ast.Walk(&v, &zn.Ast) Index: usecase/refresh.go ================================================================== --- usecase/refresh.go +++ usecase/refresh.go @@ -4,10 +4,13 @@ // 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. +// +// SPDX-License-Identifier: EUPL-1.2 +// SPDX-FileCopyrightText: 2021-present Detlef Stern //----------------------------------------------------------------------------- package usecase import ( Index: usecase/reindex.go ================================================================== --- usecase/reindex.go +++ usecase/reindex.go @@ -4,10 +4,13 @@ // 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. +// +// SPDX-License-Identifier: EUPL-1.2 +// SPDX-FileCopyrightText: 2023-present Detlef Stern //----------------------------------------------------------------------------- package usecase import ( @@ -34,8 +37,8 @@ } // Run executes the use case. func (uc *ReIndex) Run(ctx context.Context, zid id.Zid) error { err := uc.port.ReIndex(ctx, zid) - uc.log.Sense().User(ctx).Err(err).Zid(zid).Msg("ReIndex zettel") + uc.log.Info().User(ctx).Err(err).Zid(zid).Msg("ReIndex zettel") return err } Index: usecase/rename_zettel.go ================================================================== --- usecase/rename_zettel.go +++ usecase/rename_zettel.go @@ -4,10 +4,13 @@ // 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. +// +// SPDX-License-Identifier: EUPL-1.2 +// SPDX-FileCopyrightText: 2020-present Detlef Stern //----------------------------------------------------------------------------- package usecase import ( Index: usecase/update_zettel.go ================================================================== --- usecase/update_zettel.go +++ usecase/update_zettel.go @@ -4,10 +4,13 @@ // 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. +// +// SPDX-License-Identifier: EUPL-1.2 +// SPDX-FileCopyrightText: 2020-present Detlef Stern //----------------------------------------------------------------------------- package usecase import ( @@ -68,8 +71,8 @@ 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") + uc.log.Info().User(ctx).Zid(m.Zid).Err(err).Msg("Update zettel") return err } Index: usecase/usecase.go ================================================================== --- usecase/usecase.go +++ usecase/usecase.go @@ -4,9 +4,12 @@ // 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. +// +// SPDX-License-Identifier: EUPL-1.2 +// SPDX-FileCopyrightText: 2021-present Detlef Stern //----------------------------------------------------------------------------- // Package usecase provides (business) use cases for the zettelstore. package usecase Index: usecase/version.go ================================================================== --- usecase/version.go +++ usecase/version.go @@ -4,10 +4,13 @@ // 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. +// +// SPDX-License-Identifier: EUPL-1.2 +// SPDX-FileCopyrightText: 2022-present Detlef Stern //----------------------------------------------------------------------------- package usecase import ( ADDED web/adapter/adapter.go Index: web/adapter/adapter.go ================================================================== --- /dev/null +++ web/adapter/adapter.go @@ -0,0 +1,49 @@ +//----------------------------------------------------------------------------- +// Copyright (c) 2024-present 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. +// +// SPDX-License-Identifier: EUPL-1.2 +// SPDX-FileCopyrightText: 2024-present Detlef Stern +//----------------------------------------------------------------------------- + +// Package adapter provides handlers for web requests, and some helper tools. +package adapter + +import ( + "context" + + "zettelstore.de/client.fossil/api" + "zettelstore.de/z/usecase" + "zettelstore.de/z/zettel/meta" +) + +// TryReIndex executes a re-index if the appropriate query action is given. +func TryReIndex(ctx context.Context, actions []string, metaSeq []*meta.Meta, reIndex *usecase.ReIndex) ([]string, error) { + if lenActions := len(actions); lenActions > 0 { + tempActions := make([]string, 0, lenActions) + hasReIndex := false + for _, act := range actions { + if !hasReIndex && act == api.ReIndexAction { + hasReIndex = true + var errAction error + for _, m := range metaSeq { + if err := reIndex.Run(ctx, m.Zid); err != nil { + errAction = err + } + } + if errAction != nil { + return nil, errAction + } + continue + } + tempActions = append(tempActions, act) + } + return tempActions, nil + } + return nil, nil +} Index: web/adapter/api/api.go ================================================================== --- web/adapter/api/api.go +++ web/adapter/api/api.go @@ -4,10 +4,13 @@ // 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. +// +// SPDX-License-Identifier: EUPL-1.2 +// SPDX-FileCopyrightText: 2021-present Detlef Stern //----------------------------------------------------------------------------- // Package api provides api handlers for web requests. package api @@ -53,13 +56,10 @@ tokenLifetime: kernel.Main.GetConfig(kernel.WebService, kernel.WebTokenLifetimeAPI).(time.Duration), } return a } -// GetURLPrefix returns the configured URL prefix of the web server. -func (a *API) GetURLPrefix() string { return a.b.GetURLPrefix() } - // 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 server.GetAuthData(ctx) Index: web/adapter/api/command.go ================================================================== --- web/adapter/api/command.go +++ web/adapter/api/command.go @@ -4,10 +4,13 @@ // 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. +// +// SPDX-License-Identifier: EUPL-1.2 +// SPDX-FileCopyrightText: 2021-present Detlef Stern //----------------------------------------------------------------------------- package api import ( Index: web/adapter/api/create_zettel.go ================================================================== --- web/adapter/api/create_zettel.go +++ web/adapter/api/create_zettel.go @@ -4,10 +4,13 @@ // 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. +// +// SPDX-License-Identifier: EUPL-1.2 +// SPDX-FileCopyrightText: 2021-present Detlef Stern //----------------------------------------------------------------------------- package api import ( @@ -51,17 +54,17 @@ return } var result []byte var contentType string - location := a.NewURLBuilder('z').SetZid(api.ZettelID(newZid.String())) + location := a.NewURLBuilder('z').SetZid(newZid.ZettelID()) switch enc { case api.EncoderPlain: result = newZid.Bytes() contentType = content.PlainText case api.EncoderData: - result = []byte(sx.Int64(newZid).Repr()) + result = []byte(sx.Int64(newZid).String()) contentType = content.SXPF default: panic(encStr) } Index: web/adapter/api/delete_zettel.go ================================================================== --- web/adapter/api/delete_zettel.go +++ web/adapter/api/delete_zettel.go @@ -4,10 +4,13 @@ // 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. +// +// SPDX-License-Identifier: EUPL-1.2 +// SPDX-FileCopyrightText: 2021-present Detlef Stern //----------------------------------------------------------------------------- package api import ( Index: web/adapter/api/get_data.go ================================================================== --- web/adapter/api/get_data.go +++ web/adapter/api/get_data.go @@ -4,10 +4,13 @@ // 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. +// +// SPDX-License-Identifier: EUPL-1.2 +// SPDX-FileCopyrightText: 2022-present Detlef Stern //----------------------------------------------------------------------------- package api import ( Index: web/adapter/api/get_zettel.go ================================================================== --- web/adapter/api/get_zettel.go +++ web/adapter/api/get_zettel.go @@ -4,10 +4,13 @@ // 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. +// +// SPDX-License-Identifier: EUPL-1.2 +// SPDX-FileCopyrightText: 2020-present Detlef Stern //----------------------------------------------------------------------------- package api import ( @@ -25,10 +28,11 @@ "zettelstore.de/z/parser" "zettelstore.de/z/usecase" "zettelstore.de/z/web/adapter" "zettelstore.de/z/web/content" "zettelstore.de/z/zettel/id" + "zettelstore.de/z/zettel/meta" ) // MakeGetZettelHandler creates a new HTTP handler to return a zettel in various encodings. func (a *API) MakeGetZettelHandler(getZettel usecase.GetZettel, parseZettel usecase.ParseZettel, evaluate usecase.Evaluate) http.HandlerFunc { return func(w http.ResponseWriter, r *http.Request) { @@ -93,16 +97,16 @@ case partMeta: contentType = content.PlainText _, err = z.Meta.Write(&buf) case partContent: - contentType = content.MIMEFromSyntax(z.Meta.GetDefault(api.KeySyntax, "")) + contentType = content.MIMEFromSyntax(z.Meta.GetDefault(api.KeySyntax, meta.DefaultSyntax)) _, err = z.Content.Write(&buf) } if err != nil { - a.log.Fatal().Err(err).Zid(zid).Msg("Unable to store plain zettel/part in buffer") + a.log.Error().Err(err).Zid(zid).Msg("Unable to store plain zettel/part in buffer") http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError) return } if err = writeBuffer(w, &buf, contentType); err != nil { a.log.Error().Err(err).Zid(zid).Msg("Write Plain data") @@ -161,11 +165,11 @@ _, err = encdr.WriteMeta(&buf, zn.InhMeta, evalMeta) case partContent: _, err = encdr.WriteContent(&buf, zn) } if err != nil { - a.log.Fatal().Err(err).Zid(zn.Zid).Msg("Unable to store data in buffer") + a.log.Error().Err(err).Zid(zn.Zid).Msg("Unable to store data in buffer") http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError) return } if buf.Len() == 0 { w.WriteHeader(http.StatusNoContent) Index: web/adapter/api/login.go ================================================================== --- web/adapter/api/login.go +++ web/adapter/api/login.go @@ -4,10 +4,13 @@ // 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. +// +// SPDX-License-Identifier: EUPL-1.2 +// SPDX-FileCopyrightText: 2020-present Detlef Stern //----------------------------------------------------------------------------- package api import ( Index: web/adapter/api/query.go ================================================================== --- web/adapter/api/query.go +++ web/adapter/api/query.go @@ -4,10 +4,13 @@ // 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. +// +// SPDX-License-Identifier: EUPL-1.2 +// SPDX-FileCopyrightText: 2022-present Detlef Stern //----------------------------------------------------------------------------- package api import ( @@ -33,24 +36,38 @@ // MakeQueryHandler creates a new HTTP handler to perform a query. func (a *API) MakeQueryHandler(queryMeta *usecase.Query, tagZettel *usecase.TagZettel, roleZettel *usecase.RoleZettel, reIndex *usecase.ReIndex) http.HandlerFunc { return func(w http.ResponseWriter, r *http.Request) { ctx := r.Context() urlQuery := r.URL.Query() - if a.handleTagZettel(w, r, tagZettel, urlQuery) { - return - } - if a.handleRoleZettel(w, r, roleZettel, urlQuery) { + if a.handleTagZettel(w, r, tagZettel, urlQuery) || a.handleRoleZettel(w, r, roleZettel, urlQuery) { return } sq := adapter.GetQuery(urlQuery) - metaSeq, err := queryMeta.Run(ctx, sq) if err != nil { a.reportUsecaseError(w, err) return } + + actions, err := adapter.TryReIndex(ctx, sq.Actions(), metaSeq, reIndex) + if err != nil { + a.reportUsecaseError(w, err) + return + } + if len(actions) > 0 { + if len(metaSeq) > 0 { + for _, act := range actions { + if act == api.RedirectAction { + zid := metaSeq[0].Zid + ub := a.NewURLBuilder('z').SetZid(zid.ZettelID()) + a.redirectFound(w, r, ub, zid) + return + } + } + } + } var encoder zettelEncoder var contentType string switch enc, _ := getEncoding(r, urlQuery); enc { case api.EncoderPlain: @@ -57,11 +74,10 @@ encoder = &plainZettelEncoder{} contentType = content.PlainText case api.EncoderData: encoder = &dataZettelEncoder{ - sf: sx.MakeMappedFactory(256), sq: sq, getRights: func(m *meta.Meta) api.ZettelRights { return a.getRights(ctx, m) }, } contentType = content.SXPF @@ -69,11 +85,11 @@ http.Error(w, http.StatusText(http.StatusBadRequest), http.StatusBadRequest) return } var buf bytes.Buffer - err = queryAction(&buf, encoder, metaSeq, sq, func(zid id.Zid) error { return reIndex.Run(ctx, zid) }) + err = queryAction(&buf, encoder, metaSeq, actions) if err != nil { a.log.Error().Err(err).Str("query", sq.String()).Msg("execute query action") http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError) } @@ -80,39 +96,32 @@ if err = writeBuffer(w, &buf, contentType); err != nil { a.log.Error().Err(err).Msg("write result buffer") } } } -func queryAction(w io.Writer, enc zettelEncoder, ml []*meta.Meta, sq *query.Query, reindex func(id.Zid) error) error { +func queryAction(w io.Writer, enc zettelEncoder, ml []*meta.Meta, actions []string) error { min, max := -1, -1 - if actions := sq.Actions(); len(actions) > 0 { + if len(actions) > 0 { acts := make([]string, 0, len(actions)) for _, act := range actions { - if strings.HasPrefix(act, "MIN") { + if strings.HasPrefix(act, api.MinAction) { if num, err := strconv.Atoi(act[3:]); err == nil && num > 0 { min = num continue } } - if strings.HasPrefix(act, "MAX") { + if strings.HasPrefix(act, api.MaxAction) { if num, err := strconv.Atoi(act[3:]); err == nil && num > 0 { max = num continue } } acts = append(acts, act) } for _, act := range acts { - switch act { - case "KEYS": + if act == api.KeysAction { return encodeKeysArrangement(w, enc, ml, act) - case "REINDEX": - for _, m := range ml { - if err := reindex(m.Zid); err != nil { - return err - } - } } switch key := strings.ToLower(act); meta.Type(key) { case meta.TypeWord, meta.TypeTagSet: return encodeMetaKeyArrangement(w, enc, ml, key, min, max) } @@ -186,20 +195,18 @@ } return nil } type dataZettelEncoder struct { - sf sx.SymbolFactory sq *query.Query getRights func(*meta.Meta) api.ZettelRights } func (dze *dataZettelEncoder) writeMetaList(w io.Writer, ml []*meta.Meta) error { - sf := dze.sf - result := make([]sx.Object, len(ml)+1) - result[0] = sf.MustMake("list") - symID, symZettel := sf.MustMake("id"), sf.MustMake("zettel") + result := make(sx.Vector, len(ml)+1) + result[0] = sx.SymbolList + symID, symZettel := sx.MakeSymbol("id"), sx.MakeSymbol("zettel") for i, m := range ml { msz := sexp.EncodeMetaRights(api.MetaRights{ Meta: m.Map(), Rights: dze.getRights(m), }) @@ -206,19 +213,18 @@ msz = sx.Cons(sx.MakeList(symID, sx.Int64(m.Zid)), msz.Cdr()).Cons(symZettel) result[i+1] = msz } _, err := sx.Print(w, sx.MakeList( - sf.MustMake("meta-list"), - sx.MakeList(sf.MustMake("query"), sx.String(dze.sq.String())), - sx.MakeList(sf.MustMake("human"), sx.String(dze.sq.Human())), + sx.MakeSymbol("meta-list"), + sx.MakeList(sx.MakeSymbol("query"), sx.String(dze.sq.String())), + sx.MakeList(sx.MakeSymbol("human"), sx.String(dze.sq.Human())), sx.MakeList(result...), )) return err } func (dze *dataZettelEncoder) writeArrangement(w io.Writer, act string, arr meta.Arrangement) error { - sf := dze.sf result := sx.Nil() for aggKey, metaList := range arr { sxMeta := sx.Nil() for i := len(metaList) - 1; i >= 0; i-- { sxMeta = sxMeta.Cons(sx.Int64(metaList[i].Zid)) @@ -225,15 +231,15 @@ } sxMeta = sxMeta.Cons(sx.String(aggKey)) result = result.Cons(sxMeta) } _, err := sx.Print(w, sx.MakeList( - sf.MustMake("aggregate"), + sx.MakeSymbol("aggregate"), sx.String(act), - sx.MakeList(sf.MustMake("query"), sx.String(dze.sq.String())), - sx.MakeList(sf.MustMake("human"), sx.String(dze.sq.Human())), - result.Cons(sf.MustMake("list")), + sx.MakeList(sx.MakeSymbol("query"), sx.String(dze.sq.String())), + sx.MakeList(sx.MakeSymbol("human"), sx.String(dze.sq.Human())), + result.Cons(sx.SymbolList), )) return err } func (a *API) handleTagZettel(w http.ResponseWriter, r *http.Request, tagZettel *usecase.TagZettel, vals url.Values) bool { @@ -245,25 +251,21 @@ z, err := tagZettel.Run(ctx, tag) if err != nil { a.reportUsecaseError(w, err) return true } - zid := z.Meta.Zid.String() - w.Header().Set(api.HeaderContentType, content.PlainText) - newURL := a.NewURLBuilder('z').SetZid(api.ZettelID(zid)) + zid := z.Meta.Zid + newURL := a.NewURLBuilder('z').SetZid(zid.ZettelID()) for key, slVals := range vals { if key == api.QueryKeyTag { continue } for _, val := range slVals { newURL.AppendKVQuery(key, val) } } - http.Redirect(w, r, newURL.String(), http.StatusFound) - if _, err = io.WriteString(w, zid); err != nil { - a.log.Error().Err(err).Msg("redirect body") - } + a.redirectFound(w, r, newURL, zid) return true } func (a *API) handleRoleZettel(w http.ResponseWriter, r *http.Request, roleZettel *usecase.RoleZettel, vals url.Values) bool { role := vals.Get(api.QueryKeyRole) @@ -274,22 +276,26 @@ z, err := roleZettel.Run(ctx, role) if err != nil { a.reportUsecaseError(w, err) return true } - zid := z.Meta.Zid.String() - w.Header().Set(api.HeaderContentType, content.PlainText) - newURL := a.NewURLBuilder('z').SetZid(api.ZettelID(zid)) + zid := z.Meta.Zid + newURL := a.NewURLBuilder('z').SetZid(zid.ZettelID()) for key, slVals := range vals { if key == api.QueryKeyRole { continue } for _, val := range slVals { newURL.AppendKVQuery(key, val) } } - http.Redirect(w, r, newURL.String(), http.StatusFound) - if _, err = io.WriteString(w, zid); err != nil { + a.redirectFound(w, r, newURL, zid) + return true +} + +func (a *API) redirectFound(w http.ResponseWriter, r *http.Request, ub *api.URLBuilder, zid id.Zid) { + w.Header().Set(api.HeaderContentType, content.PlainText) + http.Redirect(w, r, ub.String(), http.StatusFound) + if _, err := io.WriteString(w, zid.String()); err != nil { a.log.Error().Err(err).Msg("redirect body") } - return true } Index: web/adapter/api/rename_zettel.go ================================================================== --- web/adapter/api/rename_zettel.go +++ web/adapter/api/rename_zettel.go @@ -4,10 +4,13 @@ // 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. +// +// SPDX-License-Identifier: EUPL-1.2 +// SPDX-FileCopyrightText: 2021-present Detlef Stern //----------------------------------------------------------------------------- package api import ( Index: web/adapter/api/request.go ================================================================== --- web/adapter/api/request.go +++ web/adapter/api/request.go @@ -4,10 +4,13 @@ // 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. +// +// SPDX-License-Identifier: EUPL-1.2 +// SPDX-FileCopyrightText: 2020-present Detlef Stern //----------------------------------------------------------------------------- package api import ( @@ -14,13 +17,13 @@ "io" "net/http" "net/url" "zettelstore.de/client.fossil/api" + "zettelstore.de/client.fossil/input" "zettelstore.de/client.fossil/sexp" "zettelstore.de/sx.fossil/sxreader" - "zettelstore.de/z/input" "zettelstore.de/z/zettel" "zettelstore.de/z/zettel/id" "zettelstore.de/z/zettel/meta" ) Index: web/adapter/api/response.go ================================================================== --- web/adapter/api/response.go +++ web/adapter/api/response.go @@ -4,10 +4,13 @@ // 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. +// +// SPDX-License-Identifier: EUPL-1.2 +// SPDX-FileCopyrightText: 2020-present Detlef Stern //----------------------------------------------------------------------------- package api import ( @@ -20,11 +23,11 @@ ) func (a *API) writeObject(w http.ResponseWriter, zid id.Zid, obj sx.Object) error { var buf bytes.Buffer if _, err := sx.Print(&buf, obj); err != nil { - msg := a.log.Fatal().Err(err) + msg := a.log.Error().Err(err) if msg != nil { if zid.IsValid() { msg = msg.Zid(zid) } msg.Msg("Unable to store object in buffer") Index: web/adapter/api/update_zettel.go ================================================================== --- web/adapter/api/update_zettel.go +++ web/adapter/api/update_zettel.go @@ -4,10 +4,13 @@ // 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. +// +// SPDX-License-Identifier: EUPL-1.2 +// SPDX-FileCopyrightText: 2021-present Detlef Stern //----------------------------------------------------------------------------- package api import ( Index: web/adapter/errors.go ================================================================== --- web/adapter/errors.go +++ web/adapter/errors.go @@ -4,31 +4,23 @@ // 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. +// +// SPDX-License-Identifier: EUPL-1.2 +// SPDX-FileCopyrightText: 2020-present Detlef Stern //----------------------------------------------------------------------------- -// Package adapter provides handlers for web requests. package adapter import "net/http" // BadRequest signals HTTP status code 400. func BadRequest(w http.ResponseWriter, text string) { http.Error(w, text, http.StatusBadRequest) } -// Forbidden signals HTTP status code 403. -func Forbidden(w http.ResponseWriter, text string) { - http.Error(w, text, http.StatusForbidden) -} - -// NotFound signals HTTP status code 404. -func NotFound(w http.ResponseWriter, text string) { - http.Error(w, text, http.StatusNotFound) -} - // ErrResourceNotFound is signalled when a web resource was not found. type ErrResourceNotFound struct{ Path string } func (ernf ErrResourceNotFound) Error() string { return "resource not found: " + ernf.Path } Index: web/adapter/request.go ================================================================== --- web/adapter/request.go +++ web/adapter/request.go @@ -4,10 +4,13 @@ // 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. +// +// SPDX-License-Identifier: EUPL-1.2 +// SPDX-FileCopyrightText: 2020-present Detlef Stern //----------------------------------------------------------------------------- package adapter import ( Index: web/adapter/response.go ================================================================== --- web/adapter/response.go +++ web/adapter/response.go @@ -4,10 +4,13 @@ // 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. +// +// SPDX-License-Identifier: EUPL-1.2 +// SPDX-FileCopyrightText: 2020-present Detlef Stern //----------------------------------------------------------------------------- package adapter import ( Index: web/adapter/webui/const.go ================================================================== --- web/adapter/webui/const.go +++ web/adapter/webui/const.go @@ -4,10 +4,13 @@ // 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. +// +// SPDX-License-Identifier: EUPL-1.2 +// SPDX-FileCopyrightText: 2022-present Detlef Stern //----------------------------------------------------------------------------- package webui // WebUI related constants. Index: web/adapter/webui/create_zettel.go ================================================================== --- web/adapter/webui/create_zettel.go +++ web/adapter/webui/create_zettel.go @@ -4,10 +4,13 @@ // 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. +// +// SPDX-License-Identifier: EUPL-1.2 +// SPDX-FileCopyrightText: 2020-present Detlef Stern //----------------------------------------------------------------------------- package webui import ( @@ -142,13 +145,13 @@ if err != nil { wui.reportError(ctx, w, err) return } if reEdit { - wui.redirectFound(w, r, wui.NewURLBuilder('e').SetZid(api.ZettelID(newZid.String()))) + wui.redirectFound(w, r, wui.NewURLBuilder('e').SetZid(newZid.ZettelID())) } else { - wui.redirectFound(w, r, wui.NewURLBuilder('h').SetZid(api.ZettelID(newZid.String()))) + wui.redirectFound(w, r, wui.NewURLBuilder('h').SetZid(newZid.ZettelID())) } } } // MakeGetZettelFromListHandler creates a new HTTP handler to store content of @@ -163,11 +166,12 @@ metaSeq, err := queryMeta.Run(box.NoEnrichQuery(ctx, q), q) if err != nil { wui.reportError(ctx, w, err) return } - bns := evaluate.RunBlockNode(ctx, evaluator.QueryAction(ctx, q, metaSeq, wui.rtConfig)) + entries, _ := evaluator.QueryAction(ctx, q, metaSeq, wui.rtConfig) + bns := evaluate.RunBlockNode(ctx, entries) enc := zmkenc.Create() var zmkContent bytes.Buffer _, err = enc.WriteBlocks(&zmkContent, &bns) if err != nil { wui.reportError(ctx, w, err) Index: web/adapter/webui/delete_zettel.go ================================================================== --- web/adapter/webui/delete_zettel.go +++ web/adapter/webui/delete_zettel.go @@ -4,10 +4,13 @@ // 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. +// +// SPDX-License-Identifier: EUPL-1.2 +// SPDX-FileCopyrightText: 2020-present Detlef Stern //----------------------------------------------------------------------------- package webui import ( Index: web/adapter/webui/edit_zettel.go ================================================================== --- web/adapter/webui/edit_zettel.go +++ web/adapter/webui/edit_zettel.go @@ -4,18 +4,20 @@ // 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. +// +// SPDX-License-Identifier: EUPL-1.2 +// SPDX-FileCopyrightText: 2020-present Detlef Stern //----------------------------------------------------------------------------- package webui import ( "net/http" - "zettelstore.de/client.fossil/api" "zettelstore.de/z/box" "zettelstore.de/z/usecase" "zettelstore.de/z/web/adapter" "zettelstore.de/z/zettel/id" ) @@ -70,11 +72,11 @@ wui.reportError(ctx, w, err) return } if reEdit { - wui.redirectFound(w, r, wui.NewURLBuilder('e').SetZid(api.ZettelID(zid.String()))) + wui.redirectFound(w, r, wui.NewURLBuilder('e').SetZid(zid.ZettelID())) } else { - wui.redirectFound(w, r, wui.NewURLBuilder('h').SetZid(api.ZettelID(zid.String()))) + wui.redirectFound(w, r, wui.NewURLBuilder('h').SetZid(zid.ZettelID())) } } } Index: web/adapter/webui/favicon.go ================================================================== --- web/adapter/webui/favicon.go +++ web/adapter/webui/favicon.go @@ -4,10 +4,13 @@ // 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. +// +// SPDX-License-Identifier: EUPL-1.2 +// SPDX-FileCopyrightText: 2022-present Detlef Stern //----------------------------------------------------------------------------- package webui import ( @@ -22,23 +25,23 @@ func (wui *WebUI) MakeFaviconHandler(baseDir string) http.HandlerFunc { return func(w http.ResponseWriter, r *http.Request) { filename := filepath.Join(baseDir, "favicon.ico") f, err := os.Open(filename) if err != nil { - wui.log.Sense().Err(err).Msg("Favicon not found") + wui.log.Debug().Err(err).Msg("Favicon not found") http.Error(w, http.StatusText(http.StatusNotFound), http.StatusNotFound) return } defer f.Close() data, err := io.ReadAll(f) if err != nil { - wui.log.Info().Err(err).Msg("Unable to read favicon data") + wui.log.Error().Err(err).Msg("Unable to read favicon data") http.Error(w, http.StatusText(http.StatusNotFound), http.StatusNotFound) return } if err = adapter.WriteData(w, data, ""); err != nil { wui.log.Error().Err(err).Msg("Write favicon") } } } Index: web/adapter/webui/forms.go ================================================================== --- web/adapter/webui/forms.go +++ web/adapter/webui/forms.go @@ -4,10 +4,13 @@ // 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. +// +// SPDX-License-Identifier: EUPL-1.2 +// SPDX-FileCopyrightText: 2020-present Detlef Stern //----------------------------------------------------------------------------- package webui import ( @@ -18,11 +21,11 @@ "regexp" "strings" "unicode" "zettelstore.de/client.fossil/api" - "zettelstore.de/z/input" + "zettelstore.de/client.fossil/input" "zettelstore.de/z/kernel" "zettelstore.de/z/parser" "zettelstore.de/z/web/content" "zettelstore.de/z/zettel" "zettelstore.de/z/zettel/id" Index: web/adapter/webui/forms_test.go ================================================================== --- web/adapter/webui/forms_test.go +++ web/adapter/webui/forms_test.go @@ -4,10 +4,13 @@ // 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. +// +// SPDX-License-Identifier: EUPL-1.2 +// SPDX-FileCopyrightText: 2020-present Detlef Stern //----------------------------------------------------------------------------- package webui import "testing" Index: web/adapter/webui/get_info.go ================================================================== --- web/adapter/webui/get_info.go +++ web/adapter/webui/get_info.go @@ -4,10 +4,13 @@ // 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. +// +// SPDX-License-Identifier: EUPL-1.2 +// SPDX-FileCopyrightText: 2020-present Detlef Stern //----------------------------------------------------------------------------- package webui import ( @@ -81,11 +84,12 @@ if err != nil { wui.reportError(ctx, w, err) return } - bns := ucEvaluate.RunBlockNode(ctx, evaluator.QueryAction(ctx, nil, unlinkedMeta, wui.rtConfig)) + entries, _ := evaluator.QueryAction(ctx, nil, unlinkedMeta, wui.rtConfig) + bns := ucEvaluate.RunBlockNode(ctx, entries) unlinkedContent, _, err := enc.BlocksSxn(&bns) if err != nil { wui.reportError(ctx, w, err) return } @@ -168,11 +172,11 @@ var apiParts = []string{api.PartZettel, api.PartMeta, api.PartContent} func (wui *WebUI) infoAPIMatrix(zid id.Zid, parseOnly bool, encTexts []string) *sx.Pair { matrix := sx.Nil() - u := wui.NewURLBuilder('z').SetZid(api.ZettelID(zid.String())) + u := wui.NewURLBuilder('z').SetZid(zid.ZettelID()) for ip := len(apiParts) - 1; ip >= 0; ip-- { part := apiParts[ip] row := sx.Nil() for je := len(encTexts) - 1; je >= 0; je-- { enc := encTexts[je] @@ -189,11 +193,11 @@ return matrix } func (wui *WebUI) infoAPIMatrixParsed(zid id.Zid, encTexts []string) *sx.Pair { matrix := wui.infoAPIMatrix(zid, true, encTexts) - u := wui.NewURLBuilder('z').SetZid(api.ZettelID(zid.String())) + u := wui.NewURLBuilder('z').SetZid(zid.ZettelID()) for i, row := 0, matrix; i < len(apiParts) && row != nil; row = row.Tail() { line, isLine := sx.GetPair(row.Car()) if !isLine || line == nil { continue Index: web/adapter/webui/get_zettel.go ================================================================== --- web/adapter/webui/get_zettel.go +++ web/adapter/webui/get_zettel.go @@ -4,19 +4,24 @@ // 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. +// +// SPDX-License-Identifier: EUPL-1.2 +// SPDX-FileCopyrightText: 2020-present Detlef Stern //----------------------------------------------------------------------------- package webui import ( "context" "net/http" + "strings" "zettelstore.de/client.fossil/api" + "zettelstore.de/client.fossil/shtml" "zettelstore.de/sx.fossil" "zettelstore.de/z/box" "zettelstore.de/z/config" "zettelstore.de/z/parser" "zettelstore.de/z/usecase" @@ -54,11 +59,11 @@ user := server.GetUser(ctx) getTextTitle := wui.makeGetTextTitle(ctx, getZettel) title := parser.NormalizedSpacedText(zn.InhMeta.GetTitle()) env, rb := wui.createRenderEnv(ctx, "zettel", wui.rtConfig.Get(ctx, zn.InhMeta, api.KeyLang), title, user) - rb.bindSymbol(wui.symMetaHeader, metaObj) + rb.bindSymbol(symMetaHeader, metaObj) rb.bindString("heading", sx.String(title)) if role, found := zn.InhMeta.Get(api.KeyRole); found && role != "" { rb.bindString("role-url", sx.String(wui.NewURLBuilder('h').AppendQuery(api.KeyRole+api.SearchOperatorHas+role).String())) } if folgeRole, found := zn.InhMeta.Get(api.KeyFolgeRole); found && folgeRole != "" { @@ -66,10 +71,11 @@ } rb.bindString("tag-refs", wui.transformTagSet(api.KeyTags, meta.ListFromValue(zn.InhMeta.GetDefault(api.KeyTags, "")))) rb.bindString("predecessor-refs", wui.identifierSetAsLinks(zn.InhMeta, api.KeyPredecessor, getTextTitle)) rb.bindString("precursor-refs", wui.identifierSetAsLinks(zn.InhMeta, api.KeyPrecursor, getTextTitle)) rb.bindString("superior-refs", wui.identifierSetAsLinks(zn.InhMeta, api.KeySuperior, getTextTitle)) + rb.bindString("urls", metaURLAssoc(zn.InhMeta)) rb.bindString("content", content) rb.bindString("endnotes", endnotes) wui.bindLinks(ctx, &rb, "folge", zn.InhMeta, api.KeyFolge, config.KeyShowFolgeLinks, getTextTitle) wui.bindLinks(ctx, &rb, "subordinate", zn.InhMeta, api.KeySubordinates, config.KeyShowSubordinateLinks, getTextTitle) wui.bindLinks(ctx, &rb, "back", zn.InhMeta, api.KeyBack, config.KeyShowBackLinks, getTextTitle) @@ -95,10 +101,22 @@ if values, ok := m.GetList(key); ok { return wui.transformIdentifierSet(values, getTextTitle) } return nil } + +func metaURLAssoc(m *meta.Meta) *sx.Pair { + var result sx.ListBuilder + for _, p := range m.PairsRest() { + if key := p.Key; strings.HasSuffix(key, meta.SuffixKeyURL) { + if val := p.Value; val != "" { + result.Add(sx.Cons(sx.String(capitalizeMetaKey(key)), sx.String(val))) + } + } + } + return result.List() +} func (wui *WebUI) bindLinks(ctx context.Context, rb *renderBinder, varPrefix string, m *meta.Meta, key, configKey string, getTextTitle getTextTitleFunc) { varLinks := varPrefix + "-links" var symOpen *sx.Symbol switch wui.rtConfig.Get(ctx, m, configKey) { @@ -105,11 +123,11 @@ case "false": rb.bindString(varLinks, sx.Nil()) return case "close": default: - symOpen = wui.symAttrOpen + symOpen = shtml.SymAttrOpen } lstLinks := wui.zettelLinksSxn(m, key, getTextTitle) rb.bindString(varLinks, lstLinks) if sx.IsNil(lstLinks) { return @@ -131,15 +149,15 @@ zid, err := id.Parse(val) if err != nil { continue } if title, found := getTextTitle(zid); found > 0 { - url := sx.String(wui.NewURLBuilder('h').SetZid(api.ZettelID(zid.String())).String()) + url := sx.String(wui.NewURLBuilder('h').SetZid(zid.ZettelID()).String()) if title == "" { lst = lst.Cons(sx.Cons(sx.String(val), url)) } else { lst = lst.Cons(sx.Cons(sx.String(title), url)) } } } return lst } Index: web/adapter/webui/goaction.go ================================================================== --- web/adapter/webui/goaction.go +++ web/adapter/webui/goaction.go @@ -4,10 +4,13 @@ // 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. +// +// SPDX-License-Identifier: EUPL-1.2 +// SPDX-FileCopyrightText: 2020-present Detlef Stern //----------------------------------------------------------------------------- package webui import ( Index: web/adapter/webui/home.go ================================================================== --- web/adapter/webui/home.go +++ web/adapter/webui/home.go @@ -4,20 +4,22 @@ // 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. +// +// SPDX-License-Identifier: EUPL-1.2 +// SPDX-FileCopyrightText: 2020-present Detlef Stern //----------------------------------------------------------------------------- package webui import ( "context" "errors" "net/http" - "zettelstore.de/client.fossil/api" "zettelstore.de/z/box" "zettelstore.de/z/config" "zettelstore.de/z/web/adapter" "zettelstore.de/z/web/server" "zettelstore.de/z/zettel" @@ -35,11 +37,11 @@ if p := r.URL.Path; p != "/" { wui.reportError(ctx, w, adapter.ErrResourceNotFound{Path: p}) return } homeZid, _ := id.Parse(wui.rtConfig.Get(ctx, nil, config.KeyHomeZettel)) - apiHomeZid := api.ZettelID(homeZid.String()) + apiHomeZid := homeZid.ZettelID() if homeZid != id.DefaultHomeZid { if _, err := s.GetZettel(ctx, homeZid); err == nil { wui.redirectFound(w, r, wui.NewURLBuilder('h').SetZid(apiHomeZid)) return } Index: web/adapter/webui/htmlgen.go ================================================================== --- web/adapter/webui/htmlgen.go +++ web/adapter/webui/htmlgen.go @@ -4,10 +4,13 @@ // 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. +// +// SPDX-License-Identifier: EUPL-1.2 +// SPDX-FileCopyrightText: 2022-present Detlef Stern //----------------------------------------------------------------------------- package webui import ( @@ -18,10 +21,11 @@ "zettelstore.de/client.fossil/attrs" "zettelstore.de/client.fossil/maps" "zettelstore.de/client.fossil/shtml" "zettelstore.de/client.fossil/sz" "zettelstore.de/sx.fossil" + "zettelstore.de/sx.fossil/sxhtml" "zettelstore.de/z/ast" "zettelstore.de/z/encoder" "zettelstore.de/z/encoder/szenc" "zettelstore.de/z/strfun" "zettelstore.de/z/zettel/meta" @@ -39,32 +43,24 @@ lang string symAt *sx.Symbol } func (wui *WebUI) createGenerator(builder urlBuilder, lang string) *htmlGenerator { - th := shtml.NewEvaluator(1, wui.sf) - symA := wui.symA - symImg := th.Make("img") - symAttr := wui.symAttr - - symHref := wui.symHref - symClass := th.Make("class") - symTarget := th.Make("target") - symRel := th.Make("rel") + th := shtml.NewEvaluator(1) findA := func(obj sx.Object) (attr, assoc, rest *sx.Pair) { pair, isPair := sx.GetPair(obj) - if !isPair || !symA.IsEqual(pair.Car()) { + if !isPair || !shtml.SymA.IsEqual(pair.Car()) { return nil, nil, nil } rest = pair.Tail() if rest == nil { return nil, nil, nil } objA := rest.Car() attr, isPair = sx.GetPair(objA) - if !isPair || !symAttr.IsEqual(attr.Car()) { + if !isPair || !sxhtml.SymAttr.IsEqual(attr.Car()) { return nil, nil, nil } return attr, attr.Tail(), rest.Tail() } linkZettel := func(obj sx.Object) sx.Object { @@ -71,92 +67,91 @@ attr, assoc, rest := findA(obj) if attr == nil { return obj } - hrefP := assoc.Assoc(symHref) + hrefP := assoc.Assoc(shtml.SymAttrHref) + if hrefP == nil { + return obj + } + href, ok := sx.GetString(hrefP.Cdr()) + if !ok { + return obj + } + zid, fragment, hasFragment := strings.Cut(string(href), "#") + u := builder.NewURLBuilder('h').SetZid(api.ZettelID(zid)) + if hasFragment { + u = u.SetFragment(fragment) + } + assoc = assoc.Cons(sx.Cons(shtml.SymAttrHref, sx.String(u.String()))) + return rest.Cons(assoc.Cons(sxhtml.SymAttr)).Cons(shtml.SymA) + } + + rebind(th, sz.SymLinkZettel, linkZettel) + rebind(th, sz.SymLinkFound, linkZettel) + rebind(th, sz.SymLinkBased, func(obj sx.Object) sx.Object { + attr, assoc, rest := findA(obj) + if attr == nil { + return obj + } + hrefP := assoc.Assoc(shtml.SymAttrHref) if hrefP == nil { return obj } href, ok := sx.GetString(hrefP.Cdr()) if !ok { return obj } - zid, fragment, hasFragment := strings.Cut(href.String(), "#") - u := builder.NewURLBuilder('h').SetZid(api.ZettelID(zid)) - if hasFragment { - u = u.SetFragment(fragment) - } - assoc = assoc.Cons(sx.Cons(symHref, sx.String(u.String()))) - return rest.Cons(assoc.Cons(symAttr)).Cons(symA) - } - - rebind(th, sz.NameSymLinkZettel, linkZettel) - rebind(th, sz.NameSymLinkFound, linkZettel) - rebind(th, sz.NameSymLinkBased, func(obj sx.Object) sx.Object { + u := builder.NewURLBuilder('/').SetRawLocal(string(href)) + assoc = assoc.Cons(sx.Cons(shtml.SymAttrHref, sx.String(u.String()))) + return rest.Cons(assoc.Cons(sxhtml.SymAttr)).Cons(shtml.SymA) + }) + rebind(th, sz.SymLinkQuery, func(obj sx.Object) sx.Object { attr, assoc, rest := findA(obj) if attr == nil { return obj } - hrefP := assoc.Assoc(symHref) + hrefP := assoc.Assoc(shtml.SymAttrHref) if hrefP == nil { return obj } href, ok := sx.GetString(hrefP.Cdr()) if !ok { return obj } - u := builder.NewURLBuilder('/').SetRawLocal(href.String()) - assoc = assoc.Cons(sx.Cons(symHref, sx.String(u.String()))) - return rest.Cons(assoc.Cons(symAttr)).Cons(symA) - }) - rebind(th, sz.NameSymLinkQuery, func(obj sx.Object) sx.Object { - attr, assoc, rest := findA(obj) - if attr == nil { - return obj - } - hrefP := assoc.Assoc(symHref) - if hrefP == nil { - return obj - } - href, ok := sx.GetString(hrefP.Cdr()) - if !ok { - return obj - } - ur, err := url.Parse(href.String()) + ur, err := url.Parse(string(href)) if err != nil { return obj } q := ur.Query().Get(api.QueryKeyQuery) if q == "" { return obj } u := builder.NewURLBuilder('h').AppendQuery(q) - assoc = assoc.Cons(sx.Cons(symHref, sx.String(u.String()))) - return rest.Cons(assoc.Cons(symAttr)).Cons(symA) + assoc = assoc.Cons(sx.Cons(shtml.SymAttrHref, sx.String(u.String()))) + return rest.Cons(assoc.Cons(sxhtml.SymAttr)).Cons(shtml.SymA) }) - rebind(th, sz.NameSymLinkExternal, func(obj sx.Object) sx.Object { + rebind(th, sz.SymLinkExternal, func(obj sx.Object) sx.Object { attr, assoc, rest := findA(obj) if attr == nil { return obj } - assoc = assoc.Cons(sx.Cons(symClass, sx.String("external"))). - Cons(sx.Cons(symTarget, sx.String("_blank"))). - Cons(sx.Cons(symRel, sx.String("noopener noreferrer"))) - return rest.Cons(assoc.Cons(symAttr)).Cons(symA) + assoc = assoc.Cons(sx.Cons(shtml.SymAttrClass, sx.String("external"))). + Cons(sx.Cons(shtml.SymAttrTarget, sx.String("_blank"))). + Cons(sx.Cons(shtml.SymAttrRel, sx.String("noopener noreferrer"))) + return rest.Cons(assoc.Cons(sxhtml.SymAttr)).Cons(shtml.SymA) }) - rebind(th, sz.NameSymEmbed, func(obj sx.Object) sx.Object { + rebind(th, sz.SymEmbed, func(obj sx.Object) sx.Object { pair, isPair := sx.GetPair(obj) - if !isPair || !symImg.IsEqual(pair.Car()) { + if !isPair || !shtml.SymIMG.IsEqual(pair.Car()) { return obj } attr, isPair := sx.GetPair(pair.Tail().Car()) - if !isPair || !symAttr.IsEqual(attr.Car()) { + if !isPair || !sxhtml.SymAttr.IsEqual(attr.Car()) { return obj } - symSrc := th.Make("src") - srcP := attr.Tail().Assoc(symSrc) + srcP := attr.Tail().Assoc(shtml.SymAttrSrc) if srcP == nil { return obj } src, isString := sx.GetString(srcP.Cdr()) if !isString { @@ -165,25 +160,24 @@ zid := api.ZettelID(src) if !zid.IsValid() { return obj } u := builder.NewURLBuilder('z').SetZid(zid) - imgAttr := attr.Tail().Cons(sx.Cons(symSrc, sx.String(u.String()))).Cons(symAttr) - return pair.Tail().Tail().Cons(imgAttr).Cons(symImg) + imgAttr := attr.Tail().Cons(sx.Cons(shtml.SymAttrSrc, sx.String(u.String()))).Cons(sxhtml.SymAttr) + return pair.Tail().Tail().Cons(imgAttr).Cons(shtml.SymIMG) }) return &htmlGenerator{ - tx: szenc.NewTransformer(), - th: th, - lang: lang, - symAt: symAttr, + tx: szenc.NewTransformer(), + th: th, + lang: lang, } } -func rebind(ev *shtml.Evaluator, name string, fn func(sx.Object) sx.Object) { - prevFn := ev.ResolveBinding(name) - ev.Rebind(name, func(args []sx.Object, env *shtml.Environment) sx.Object { +func rebind(ev *shtml.Evaluator, sym *sx.Symbol, fn func(sx.Object) sx.Object) { + prevFn := ev.ResolveBinding(sym) + ev.Rebind(sym, func(args sx.Vector, env *shtml.Environment) sx.Object { obj := prevFn(args, env) if env.GetError() == nil { return fn(obj) } return sx.Nil() @@ -231,11 +225,11 @@ key := p.Car() val := p.Cdr() if tail, isTail := sx.GetPair(val); isTail { val = tail.Car() } - a = a.Set(key.String(), val.String()) + a = a.Set(sz.GoValue(key), sz.GoValue(val)) } } name, found := a.Get("name") if !found || ignore.Has(name) { continue Index: web/adapter/webui/htmlmeta.go ================================================================== --- web/adapter/webui/htmlmeta.go +++ web/adapter/webui/htmlmeta.go @@ -4,19 +4,23 @@ // 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. +// +// SPDX-License-Identifier: EUPL-1.2 +// SPDX-FileCopyrightText: 2020-present Detlef Stern //----------------------------------------------------------------------------- package webui import ( "context" "errors" "zettelstore.de/client.fossil/api" + "zettelstore.de/client.fossil/shtml" "zettelstore.de/sx.fossil" "zettelstore.de/sx.fossil/sxhtml" "zettelstore.de/z/ast" "zettelstore.de/z/box" "zettelstore.de/z/parser" @@ -39,37 +43,35 @@ case meta.TypeID: return wui.transformIdentifier(value, getTextTitle) case meta.TypeIDSet: return wui.transformIdentifierSet(meta.ListFromValue(value), getTextTitle) case meta.TypeNumber: - return wui.transformLink(key, value, value) + return wui.transformKeyValueText(key, value, value) case meta.TypeString: return sx.String(value) case meta.TypeTagSet: return wui.transformTagSet(key, meta.ListFromValue(value)) case meta.TypeTimestamp: if ts, ok := meta.TimeValue(value); ok { return sx.MakeList( - wui.sf.MustMake("time"), + sx.MakeSymbol("time"), sx.MakeList( - wui.symAttr, - sx.Cons(wui.sf.MustMake("datetime"), sx.String(ts.Format("2006-01-02T15:04:05"))), + sxhtml.SymAttr, + sx.Cons(sx.MakeSymbol("datetime"), sx.String(ts.Format("2006-01-02T15:04:05"))), ), - sx.MakeList(wui.sf.MustMake(sxhtml.NameSymNoEscape), sx.String(ts.Format("2006-01-02 15:04:05"))), + sx.MakeList(sxhtml.SymNoEscape, sx.String(ts.Format("2006-01-02 15:04:05"))), ) } return sx.Nil() case meta.TypeURL: return wui.url2html(sx.String(value)) case meta.TypeWord: - return wui.transformLink(key, value, value) - case meta.TypeWordSet: - return wui.transformWordSet(key, meta.ListFromValue(value)) + return wui.transformKeyValueText(key, value, value) case meta.TypeZettelmarkup: return wui.transformZmkMetadata(value, evalMetadata, gen) default: - return sx.MakeList(wui.sf.MustMake("b"), sx.String("Unhandled type: "), sx.String(kt.Name)) + return sx.MakeList(shtml.SymSTRONG, sx.String("Unhandled type: "), sx.String(kt.Name)) } } func (wui *WebUI) transformIdentifier(val string, getTextTitle getTextTitleFunc) sx.Object { text := sx.String(val) @@ -78,66 +80,70 @@ return text } title, found := getTextTitle(zid) switch { case found > 0: - ub := wui.NewURLBuilder('h').SetZid(api.ZettelID(zid.String())) + ub := wui.NewURLBuilder('h').SetZid(zid.ZettelID()) attrs := sx.Nil() if title != "" { - attrs = attrs.Cons(sx.Cons(wui.sf.MustMake("title"), sx.String(title))) + attrs = attrs.Cons(sx.Cons(shtml.SymAttrTitle, sx.String(title))) } - attrs = attrs.Cons(sx.Cons(wui.symHref, sx.String(ub.String()))).Cons(wui.symAttr) - return sx.Nil().Cons(sx.String(zid.String())).Cons(attrs).Cons(wui.symA) + attrs = attrs.Cons(sx.Cons(shtml.SymAttrHref, sx.String(ub.String()))).Cons(sxhtml.SymAttr) + return sx.Nil().Cons(sx.String(zid.String())).Cons(attrs).Cons(shtml.SymA) case found == 0: - return sx.MakeList(wui.sf.MustMake("s"), text) + return sx.MakeList(sx.MakeSymbol("s"), text) default: // case found < 0: return text } } func (wui *WebUI) transformIdentifierSet(vals []string, getTextTitle getTextTitleFunc) *sx.Pair { if len(vals) == 0 { return nil } - space := sx.String(" ") - text := make([]sx.Object, 0, 2*len(vals)) + const space = sx.String(" ") + text := make(sx.Vector, 0, 2*len(vals)) for _, val := range vals { text = append(text, space, wui.transformIdentifier(val, getTextTitle)) } - return sx.MakeList(text[1:]...).Cons(wui.symSpan) + return sx.MakeList(text[1:]...).Cons(shtml.SymSPAN) } func (wui *WebUI) transformTagSet(key string, tags []string) *sx.Pair { if len(tags) == 0 { return nil } - space := sx.String(" ") - text := make([]sx.Object, 0, 2*len(tags)) - for _, tag := range tags { - text = append(text, space, wui.transformLink(key, tag, tag)) - } - return sx.MakeList(text[1:]...).Cons(wui.symSpan) -} - -func (wui *WebUI) transformWordSet(key string, words []string) sx.Object { - if len(words) == 0 { - return sx.Nil() - } - space := sx.String(" ") - text := make([]sx.Object, 0, 2*len(words)) - for _, word := range words { - text = append(text, space, wui.transformLink(key, word, word)) - } - return sx.MakeList(text[1:]...).Cons(wui.symSpan) -} - -func (wui *WebUI) transformLink(key, value, text string) *sx.Pair { - return sx.MakeList( - wui.symA, - sx.MakeList( - wui.symAttr, - sx.Cons(wui.symHref, sx.String(wui.NewURLBuilder('h').AppendQuery(key+api.SearchOperatorHas+value).String())), + const space = sx.String(" ") + text := make(sx.Vector, 0, 2*len(tags)+2) + for _, tag := range tags { + text = append(text, space, wui.transformKeyValueText(key, tag, tag)) + } + if len(tags) > 1 { + text = append(text, space, wui.transformKeyValuesText(key, tags, "(all)")) + } + return sx.MakeList(text[1:]...).Cons(shtml.SymSPAN) +} + +func (wui *WebUI) transformKeyValueText(key, value, text string) *sx.Pair { + ub := wui.NewURLBuilder('h').AppendQuery(key + api.SearchOperatorHas + value) + return buildHref(ub, text) +} + +func (wui *WebUI) transformKeyValuesText(key string, values []string, text string) *sx.Pair { + ub := wui.NewURLBuilder('h') + for _, val := range values { + ub = ub.AppendQuery(key + api.SearchOperatorHas + val) + } + return buildHref(ub, text) +} + +func buildHref(ub *api.URLBuilder, text string) *sx.Pair { + return sx.MakeList( + shtml.SymA, + sx.MakeList( + sxhtml.SymAttr, + sx.Cons(shtml.SymAttrHref, sx.String(ub.String())), ), sx.String(text), ) } @@ -162,7 +168,7 @@ } } func (wui *WebUI) transformZmkMetadata(value string, evalMetadata evalMetadataFunc, gen *htmlGenerator) sx.Object { is := evalMetadata(value) - return gen.InlinesSxHTML(&is).Cons(wui.symSpan) + return gen.InlinesSxHTML(&is).Cons(shtml.SymSPAN) } Index: web/adapter/webui/lists.go ================================================================== --- web/adapter/webui/lists.go +++ web/adapter/webui/lists.go @@ -4,10 +4,13 @@ // 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. +// +// SPDX-License-Identifier: EUPL-1.2 +// SPDX-FileCopyrightText: 2020-present Detlef Stern //----------------------------------------------------------------------------- package webui import ( @@ -18,11 +21,13 @@ "slices" "strconv" "strings" "zettelstore.de/client.fossil/api" + "zettelstore.de/client.fossil/shtml" "zettelstore.de/sx.fossil" + "zettelstore.de/sx.fossil/sxhtml" "zettelstore.de/z/ast" "zettelstore.de/z/encoding/atom" "zettelstore.de/z/encoding/rss" "zettelstore.de/z/encoding/xml" "zettelstore.de/z/evaluator" @@ -36,14 +41,12 @@ // MakeListHTMLMetaHandler creates a HTTP handler for rendering the list of zettel as HTML. func (wui *WebUI) MakeListHTMLMetaHandler(queryMeta *usecase.Query, tagZettel *usecase.TagZettel, roleZettel *usecase.RoleZettel, reIndex *usecase.ReIndex) http.HandlerFunc { return func(w http.ResponseWriter, r *http.Request) { urlQuery := r.URL.Query() - if wui.handleTagZettel(w, r, tagZettel, urlQuery) { - return - } - if wui.handleRoleZettel(w, r, roleZettel, urlQuery) { + if wui.handleTagZettel(w, r, tagZettel, urlQuery) || + wui.handleRoleZettel(w, r, roleZettel, urlQuery) { return } q := adapter.GetQuery(urlQuery) q = q.SetDeterministic() ctx := r.Context() @@ -50,44 +53,45 @@ metaSeq, err := queryMeta.Run(ctx, q) if err != nil { wui.reportError(ctx, w, err) return } - if actions := q.Actions(); len(actions) > 0 { - var tempActions []string - for _, act := range actions { - if act == "REINDEX" { - for _, m := range metaSeq { - if err = reIndex.Run(ctx, m.Zid); err != nil { - wui.reportError(ctx, w, err) - return - } - } - continue - } - tempActions = append(tempActions, act) - } - actions = tempActions - if len(actions) > 0 { - switch actions[0] { - case "ATOM": - wui.renderAtom(w, q, metaSeq) - return - case "RSS": - wui.renderRSS(ctx, w, q, metaSeq) - return - } - } - } - var content, endnotes *sx.Pair - if bn := evaluator.QueryAction(ctx, q, metaSeq, wui.rtConfig); bn != nil { + actions, err := adapter.TryReIndex(ctx, q.Actions(), metaSeq, reIndex) + if err != nil { + wui.reportError(ctx, w, err) + return + } + if len(actions) > 0 { + if len(metaSeq) > 0 { + for _, act := range actions { + if act == api.RedirectAction { + ub := wui.NewURLBuilder('h').SetZid(metaSeq[0].Zid.ZettelID()) + wui.redirectFound(w, r, ub) + return + } + } + } + switch actions[0] { + case api.AtomAction: + wui.renderAtom(w, q, metaSeq) + return + case api.RSSAction: + wui.renderRSS(ctx, w, q, metaSeq) + return + } + } + + var content, endnotes *sx.Pair + numEntries := 0 + if bn, cnt := evaluator.QueryAction(ctx, q, metaSeq, wui.rtConfig); bn != nil { enc := wui.getSimpleHTMLEncoder(wui.rtConfig.Get(ctx, nil, api.KeyLang)) content, endnotes, err = enc.BlocksSxn(&ast.BlockSlice{bn}) if err != nil { wui.reportError(ctx, w, err) return } + numEntries = cnt } user := server.GetUser(ctx) env, rb := wui.createRenderEnv( ctx, "list", @@ -99,20 +103,20 @@ var sb strings.Builder q.PrintHuman(&sb) rb.bindString("heading", sx.String(sb.String())) } rb.bindString("query-value", sx.String(q.String())) - if tzl := q.GetMetaValues(api.KeyTags); len(tzl) > 0 { + if tzl := q.GetMetaValues(api.KeyTags, false); len(tzl) > 0 { sxTzl, sxNoTzl := wui.transformTagZettelList(ctx, tagZettel, tzl) if !sx.IsNil(sxTzl) { rb.bindString("tag-zettel", sxTzl) } if !sx.IsNil(sxNoTzl) && wui.canCreate(ctx, user) { rb.bindString("create-tag-zettel", sxNoTzl) } } - if rzl := q.GetMetaValues(api.KeyRole); len(rzl) > 0 { + if rzl := q.GetMetaValues(api.KeyRole, false); len(rzl) > 0 { sxRzl, sxNoRzl := wui.transformRoleZettelList(ctx, roleZettel, rzl) if !sx.IsNil(sxRzl) { rb.bindString("role-zettel", sxRzl) } if !sx.IsNil(sxNoRzl) && wui.canCreate(ctx, user) { @@ -119,22 +123,26 @@ rb.bindString("create-role-zettel", sxNoRzl) } } rb.bindString("content", content) rb.bindString("endnotes", endnotes) + rb.bindString("num-entries", sx.Int64(numEntries)) + rb.bindString("num-meta", sx.Int64(len(metaSeq))) apiURL := wui.NewURLBuilder('z').AppendQuery(q.String()) seed, found := q.GetSeed() if found { apiURL = apiURL.AppendKVQuery(api.QueryKeySeed, strconv.Itoa(seed)) } else { seed = 0 } - rb.bindString("plain-url", sx.String(apiURL.String())) - rb.bindString("data-url", sx.String(apiURL.AppendKVQuery(api.QueryKeyEncoding, api.EncodingData).String())) - if wui.canCreate(ctx, user) { - rb.bindString("create-url", sx.String(wui.createNewURL)) - rb.bindString("seed", sx.Int64(seed)) + if len(metaSeq) > 0 { + rb.bindString("plain-url", sx.String(apiURL.String())) + rb.bindString("data-url", sx.String(apiURL.AppendKVQuery(api.QueryKeyEncoding, api.EncodingData).String())) + if wui.canCreate(ctx, user) { + rb.bindString("create-url", sx.String(wui.createNewURL)) + rb.bindString("seed", sx.Int64(seed)) + } } if rb.err == nil { err = wui.renderSxnTemplate(ctx, w, id.ListTemplateZid, env) } else { err = rb.err @@ -174,14 +182,14 @@ return withZettel, withoutZettel } func (wui *WebUI) prependZettelLink(sxZtl *sx.Pair, name string, u *api.URLBuilder) *sx.Pair { link := sx.MakeList( - wui.symA, + shtml.SymA, sx.MakeList( - wui.symAttr, - sx.Cons(wui.symHref, sx.String(u.String())), + sxhtml.SymAttr, + sx.Cons(shtml.SymAttrHref, sx.String(u.String())), ), sx.String(name), ) if sxZtl != nil { sxZtl = sxZtl.Cons(sx.String(", ")) @@ -190,11 +198,11 @@ } 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" { + if actions := q.Actions(); len(actions) > 2 && actions[1] == api.TitleAction { rssConfig.Title = strings.Join(actions[2:], " ") } data := rssConfig.Marshal(q, ml) adapter.PrepareHeader(w, rss.ContentType) @@ -209,11 +217,11 @@ } func (wui *WebUI) renderAtom(w http.ResponseWriter, q *query.Query, ml []*meta.Meta) { var atomConfig atom.Configuration atomConfig.Setup(wui.rtConfig) - if actions := q.Actions(); len(actions) > 2 && actions[1] == "TITLE" { + if actions := q.Actions(); len(actions) > 2 && actions[1] == api.TitleAction { atomConfig.Title = strings.Join(actions[2:], " ") } data := atomConfig.Marshal(q, ml) adapter.PrepareHeader(w, atom.ContentType) @@ -236,11 +244,11 @@ z, err := tagZettel.Run(ctx, tag) if err != nil { wui.reportError(ctx, w, err) return true } - wui.redirectFound(w, r, wui.NewURLBuilder('h').SetZid(api.ZettelID(z.Meta.Zid.String()))) + wui.redirectFound(w, r, wui.NewURLBuilder('h').SetZid(z.Meta.Zid.ZettelID())) return true } func (wui *WebUI) handleRoleZettel(w http.ResponseWriter, r *http.Request, roleZettel *usecase.RoleZettel, vals url.Values) bool { role := vals.Get(api.QueryKeyRole) @@ -251,8 +259,8 @@ z, err := roleZettel.Run(ctx, role) if err != nil { wui.reportError(ctx, w, err) return true } - wui.redirectFound(w, r, wui.NewURLBuilder('h').SetZid(api.ZettelID(z.Meta.Zid.String()))) + wui.redirectFound(w, r, wui.NewURLBuilder('h').SetZid(z.Meta.Zid.ZettelID())) return true } Index: web/adapter/webui/login.go ================================================================== --- web/adapter/webui/login.go +++ web/adapter/webui/login.go @@ -4,10 +4,13 @@ // 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. +// +// SPDX-License-Identifier: EUPL-1.2 +// SPDX-FileCopyrightText: 2020-present Detlef Stern //----------------------------------------------------------------------------- package webui import ( ADDED web/adapter/webui/meta.go Index: web/adapter/webui/meta.go ================================================================== --- /dev/null +++ web/adapter/webui/meta.go @@ -0,0 +1,56 @@ +//----------------------------------------------------------------------------- +// Copyright (c) 2024-present 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. +// +// SPDX-License-Identifier: EUPL-1.2 +// SPDX-FileCopyrightText: 2024-present Detlef Stern +//----------------------------------------------------------------------------- + +package webui + +import ( + "strings" + "unicode" + "unicode/utf8" +) + +func capitalizeMetaKey(key string) string { + var sb strings.Builder + for i, word := range strings.Split(key, "-") { + if i > 0 { + sb.WriteByte(' ') + } + if newWord, isSpecial := specialWords[word]; isSpecial { + if newWord == "" { + sb.WriteString(strings.ToTitle(word)) + } else { + sb.WriteString(newWord) + } + continue + } + r, size := utf8.DecodeRuneInString(word) + if r == utf8.RuneError { + sb.WriteString(word) + continue + } + sb.WriteRune(unicode.ToTitle(r)) + sb.WriteString(word[size:]) + } + return sb.String() +} + +var specialWords = map[string]string{ + "css": "", + "html": "", + "github": "GitHub", + "http": "", + "https": "", + "pdf": "", + "svg": "", + "url": "", +} ADDED web/adapter/webui/meta_test.go Index: web/adapter/webui/meta_test.go ================================================================== --- /dev/null +++ web/adapter/webui/meta_test.go @@ -0,0 +1,45 @@ +//----------------------------------------------------------------------------- +// Copyright (c) 2024-present 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. +// +// SPDX-License-Identifier: EUPL-1.2 +// SPDX-FileCopyrightText: 2024-present Detlef Stern +//----------------------------------------------------------------------------- + +package webui + +import "testing" + +func TestCapitalizeMetaKey(t *testing.T) { + var testcases = []struct { + key string + exp string + }{ + {"", ""}, + {"alt-url", "Alt URL"}, + {"author", "Author"}, + {"back", "Back"}, + {"box-number", "Box Number"}, + {"cite-key", "Cite Key"}, + {"fedi-url", "Fedi URL"}, + {"github-url", "GitHub URL"}, + {"hshn-bib", "Hshn Bib"}, + {"job-url", "Job URL"}, + {"new-user-id", "New User Id"}, + {"origin-zid", "Origin Zid"}, + {"site-url", "Site URL"}, + } + for _, tc := range testcases { + t.Run(tc.key, func(t *testing.T) { + got := capitalizeMetaKey(tc.key) + if got != tc.exp { + t.Errorf("capitalize(%q) == %q, but got %q", tc.key, tc.exp, got) + } + }) + } +} Index: web/adapter/webui/rename_zettel.go ================================================================== --- web/adapter/webui/rename_zettel.go +++ web/adapter/webui/rename_zettel.go @@ -4,10 +4,13 @@ // 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. +// +// SPDX-License-Identifier: EUPL-1.2 +// SPDX-FileCopyrightText: 2020-present Detlef Stern //----------------------------------------------------------------------------- package webui import ( @@ -95,8 +98,8 @@ if err = renameZettel.Run(r.Context(), curZid, newZid); err != nil { wui.reportError(ctx, w, err) return } - wui.redirectFound(w, r, wui.NewURLBuilder('h').SetZid(api.ZettelID(newZid.String()))) + wui.redirectFound(w, r, wui.NewURLBuilder('h').SetZid(newZid.ZettelID())) } } Index: web/adapter/webui/response.go ================================================================== --- web/adapter/webui/response.go +++ web/adapter/webui/response.go @@ -4,10 +4,13 @@ // 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. +// +// SPDX-License-Identifier: EUPL-1.2 +// SPDX-FileCopyrightText: 2021-present Detlef Stern //----------------------------------------------------------------------------- package webui import ( Index: web/adapter/webui/sxn_code.go ================================================================== --- web/adapter/webui/sxn_code.go +++ web/adapter/webui/sxn_code.go @@ -4,10 +4,13 @@ // 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. +// +// SPDX-License-Identifier: EUPL-1.2 +// SPDX-FileCopyrightText: 2023-present Detlef Stern //----------------------------------------------------------------------------- package webui import ( @@ -19,11 +22,11 @@ "zettelstore.de/sx.fossil/sxeval" "zettelstore.de/z/zettel/id" "zettelstore.de/z/zettel/meta" ) -func (wui *WebUI) loadAllSxnCodeZettel(ctx context.Context) (id.Digraph, sxeval.Environment, error) { +func (wui *WebUI) loadAllSxnCodeZettel(ctx context.Context) (id.Digraph, *sxeval.Binding, error) { // getMeta MUST currently use GetZettel, because GetMeta just uses the // Index, which might not be current. getMeta := func(ctx context.Context, zid id.Zid) (*meta.Meta, error) { z, err := wui.box.GetZettel(ctx, zid) if err != nil { @@ -31,26 +34,26 @@ } return z.Meta, nil } dg := buildSxnCodeDigraph(ctx, id.StartSxnZid, getMeta) if dg == nil { - return nil, wui.engine.RootEnvironment(), nil + return nil, wui.rootBinding, nil } dg = dg.AddVertex(id.BaseSxnZid).AddEdge(id.StartSxnZid, id.BaseSxnZid) dg = dg.AddVertex(id.PreludeSxnZid).AddEdge(id.BaseSxnZid, id.PreludeSxnZid) dg = dg.TransitiveClosure(id.StartSxnZid) if zid, isDAG := dg.IsDAG(); !isDAG { return nil, nil, fmt.Errorf("zettel %v is part of a dependency cycle", zid) } - env := sxeval.MakeChildEnvironment(wui.engine.RootEnvironment(), "zettel", 128) + bind := wui.rootBinding.MakeChildBinding("zettel", 128) for _, zid := range dg.SortReverse() { - if err := wui.loadSxnCodeZettel(ctx, zid, env); err != nil { + if err := wui.loadSxnCodeZettel(ctx, zid, bind); err != nil { return nil, nil, err } } - return dg, env, nil + return dg, bind, nil } type getMetaFunc func(context.Context, id.Zid) (*meta.Meta, error) func buildSxnCodeDigraph(ctx context.Context, startZid id.Zid, getMeta getMetaFunc) id.Digraph { @@ -83,25 +86,26 @@ } } return dg } -func (wui *WebUI) loadSxnCodeZettel(ctx context.Context, zid id.Zid, env sxeval.Environment) error { +func (wui *WebUI) loadSxnCodeZettel(ctx context.Context, zid id.Zid, bind *sxeval.Binding) error { rdr, err := wui.makeZettelReader(ctx, zid) if err != nil { return err } + env := sxeval.MakeExecutionEnvironment(bind) for { form, err2 := rdr.Read() if err2 != nil { if err2 == io.EOF { return nil } return err2 } - wui.log.Debug().Zid(zid).Str("form", form.Repr()).Msg("Loaded sxn code") + wui.log.Debug().Zid(zid).Str("form", form.String()).Msg("Loaded sxn code") - if _, err2 = wui.engine.Eval(form, env, nil); err2 != nil { + if _, err2 = env.Eval(form); err2 != nil { return err2 } } } Index: web/adapter/webui/template.go ================================================================== --- web/adapter/webui/template.go +++ web/adapter/webui/template.go @@ -4,10 +4,13 @@ // 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. +// +// SPDX-License-Identifier: EUPL-1.2 +// SPDX-FileCopyrightText: 2020-present Detlef Stern //----------------------------------------------------------------------------- package webui import ( @@ -16,10 +19,11 @@ "fmt" "net/http" "net/url" "zettelstore.de/client.fossil/api" + "zettelstore.de/client.fossil/shtml" "zettelstore.de/sx.fossil" "zettelstore.de/sx.fossil/sxbuiltins" "zettelstore.de/sx.fossil/sxeval" "zettelstore.de/sx.fossil/sxhtml" "zettelstore.de/sx.fossil/sxreader" @@ -32,140 +36,139 @@ "zettelstore.de/z/zettel" "zettelstore.de/z/zettel/id" "zettelstore.de/z/zettel/meta" ) -func (wui *WebUI) createRenderEngine() *sxeval.Engine { - root := sxeval.MakeRootEnvironment(len(specials) + len(builtins) + 3) - engine := sxeval.MakeEngine(wui.sf, root) +func (wui *WebUI) createRenderBinding() *sxeval.Binding { + root := sxeval.MakeRootBinding(len(specials) + len(builtins) + 3) for _, syntax := range specials { - engine.BindSpecial(syntax) + root.BindSpecial(syntax) } for _, b := range builtins { - engine.BindBuiltin(b) + root.BindBuiltin(b) } - engine.BindBuiltin(&sxeval.Builtin{ + root.BindBuiltin(&sxeval.Builtin{ Name: "url-to-html", MinArity: 1, MaxArity: 1, TestPure: sxeval.AssertPure, - Fn: func(_ *sxeval.Frame, args []sx.Object) (sx.Object, error) { + Fn: func(_ *sxeval.Environment, args sx.Vector) (sx.Object, error) { text, err := sxbuiltins.GetString(args, 0) if err != nil { return nil, err } return wui.url2html(text), nil }, }) - engine.BindBuiltin(&sxeval.Builtin{ + root.BindBuiltin(&sxeval.Builtin{ Name: "zid-content-path", MinArity: 1, MaxArity: 1, TestPure: sxeval.AssertPure, - Fn: func(_ *sxeval.Frame, args []sx.Object) (sx.Object, error) { + Fn: func(_ *sxeval.Environment, args sx.Vector) (sx.Object, error) { s, err := sxbuiltins.GetString(args, 0) if err != nil { return nil, err } - zid, err := id.Parse(s.String()) + zid, err := id.Parse(string(s)) if err != nil { - return nil, fmt.Errorf("parsing zettel identfier %q: %w", s, err) + return nil, fmt.Errorf("parsing zettel identifier %q: %w", s, err) } - ub := wui.NewURLBuilder('z').SetZid(api.ZettelID(zid.String())) + ub := wui.NewURLBuilder('z').SetZid(zid.ZettelID()) return sx.String(ub.String()), nil }, }) - engine.BindBuiltin(&sxeval.Builtin{ + root.BindBuiltin(&sxeval.Builtin{ Name: "query->url", MinArity: 1, MaxArity: 1, TestPure: sxeval.AssertPure, - Fn: func(_ *sxeval.Frame, args []sx.Object) (sx.Object, error) { + Fn: func(_ *sxeval.Environment, args sx.Vector) (sx.Object, error) { qs, err := sxbuiltins.GetString(args, 0) if err != nil { return nil, err } - u := wui.NewURLBuilder('h').AppendQuery(qs.String()) + u := wui.NewURLBuilder('h').AppendQuery(string(qs)) return sx.String(u.String()), nil }, }) root.Freeze() - return engine + return root } var ( specials = []*sxeval.Special{ &sxbuiltins.QuoteS, &sxbuiltins.QuasiquoteS, // quote, quasiquote &sxbuiltins.UnquoteS, &sxbuiltins.UnquoteSplicingS, // unquote, unquote-splicing &sxbuiltins.DefVarS, &sxbuiltins.DefConstS, // defvar, defconst &sxbuiltins.DefunS, &sxbuiltins.LambdaS, // defun, lambda &sxbuiltins.SetXS, // set! - &sxbuiltins.CondS, // cond &sxbuiltins.IfS, // if &sxbuiltins.BeginS, // begin &sxbuiltins.DefMacroS, // defmacro } builtins = []*sxeval.Builtin{ - &sxbuiltins.Identical, // == + &sxbuiltins.Equal, // = + &sxbuiltins.NumGreater, // > &sxbuiltins.NullP, // null? &sxbuiltins.PairP, // pair? &sxbuiltins.Car, &sxbuiltins.Cdr, // car, cdr &sxbuiltins.Caar, &sxbuiltins.Cadr, &sxbuiltins.Cdar, &sxbuiltins.Cddr, &sxbuiltins.Caaar, &sxbuiltins.Caadr, &sxbuiltins.Cadar, &sxbuiltins.Caddr, &sxbuiltins.Cdaar, &sxbuiltins.Cdadr, &sxbuiltins.Cddar, &sxbuiltins.Cdddr, - &sxbuiltins.List, // list - &sxbuiltins.Append, // append - &sxbuiltins.Assoc, // assoc - &sxbuiltins.Map, // map - &sxbuiltins.Apply, // apply - &sxbuiltins.StringAppend, // string-append - &sxbuiltins.BoundP, // bound? - &sxbuiltins.Defined, // defined? - &sxbuiltins.CurrentEnv, // current-environment - &sxbuiltins.EnvLookup, // environment-lookup + &sxbuiltins.List, // list + &sxbuiltins.Append, // append + &sxbuiltins.Assoc, // assoc + &sxbuiltins.Map, // map + &sxbuiltins.Apply, // apply + &sxbuiltins.Concat, // concat + &sxbuiltins.BoundP, // bound? + &sxbuiltins.Defined, // defined? + &sxbuiltins.CurrentBinding, // current-binding + &sxbuiltins.BindingLookup, // binding-lookup } ) func (wui *WebUI) url2html(text sx.String) sx.Object { - if u, errURL := url.Parse(text.String()); errURL == nil { + if u, errURL := url.Parse(string(text)); errURL == nil { if us := u.String(); us != "" { return sx.MakeList( - wui.symA, + shtml.SymA, sx.MakeList( - wui.symAttr, - sx.Cons(wui.symHref, sx.String(us)), - sx.Cons(wui.sf.MustMake("target"), sx.String("_blank")), - sx.Cons(wui.sf.MustMake("rel"), sx.String("noopener noreferrer")), + sxhtml.SymAttr, + sx.Cons(shtml.SymAttrHref, sx.String(us)), + sx.Cons(shtml.SymAttrTarget, sx.String("_blank")), + sx.Cons(shtml.SymAttrRel, sx.String("noopener noreferrer")), ), text) } } return text } -func (wui *WebUI) getParentEnv(ctx context.Context) (sxeval.Environment, error) { - wui.mxZettelEnv.Lock() - defer wui.mxZettelEnv.Unlock() - if parentEnv := wui.zettelEnv; parentEnv != nil { +func (wui *WebUI) getParentEnv(ctx context.Context) (*sxeval.Binding, error) { + wui.mxZettelBinding.Lock() + defer wui.mxZettelBinding.Unlock() + if parentEnv := wui.zettelBinding; parentEnv != nil { return parentEnv, nil } dag, zettelEnv, err := wui.loadAllSxnCodeZettel(ctx) if err != nil { wui.log.Error().Err(err).Msg("loading zettel sxn") return nil, err } wui.dag = dag - wui.zettelEnv = zettelEnv + wui.zettelBinding = zettelEnv return zettelEnv, nil } // createRenderEnv creates a new environment and populates it with all relevant data for the base template. -func (wui *WebUI) createRenderEnv(ctx context.Context, name, lang, title string, user *meta.Meta) (sxeval.Environment, renderBinder) { +func (wui *WebUI) createRenderEnv(ctx context.Context, name, lang, title string, user *meta.Meta) (*sxeval.Binding, renderBinder) { userIsValid, userZettelURL, userIdent := wui.getUserRenderData(user) parentEnv, err := wui.getParentEnv(ctx) - env := sxeval.MakeChildEnvironment(parentEnv, name, 128) - rb := makeRenderBinder(wui.sf, env, err) + bind := parentEnv.MakeChildBinding(name, 128) + rb := makeRenderBinder(bind, err) rb.bindString("lang", sx.String(lang)) rb.bindString("css-base-url", sx.String(wui.cssBaseURL)) rb.bindString("css-user-url", sx.String(wui.cssUserURL)) rb.bindString("title", sx.String(title)) rb.bindString("home-url", sx.String(wui.homeURL)) @@ -185,44 +188,38 @@ rb.bindString("search-url", sx.String(wui.searchURL)) rb.bindString("query-key-query", sx.String(api.QueryKeyQuery)) rb.bindString("query-key-seed", sx.String(api.QueryKeySeed)) rb.bindString("FOOTER", wui.calculateFooterSxn(ctx)) // TODO: use real footer rb.bindString("debug-mode", sx.MakeBoolean(wui.debug)) - rb.bindSymbol(wui.symMetaHeader, sx.Nil()) - rb.bindSymbol(wui.symDetail, sx.Nil()) - return env, rb + rb.bindSymbol(symMetaHeader, sx.Nil()) + rb.bindSymbol(symDetail, sx.Nil()) + return bind, rb } func (wui *WebUI) getUserRenderData(user *meta.Meta) (bool, string, string) { if user == nil { return false, "", "" } - return true, wui.NewURLBuilder('h').SetZid(api.ZettelID(user.Zid.String())).String(), user.GetDefault(api.KeyUserID, "") + return true, wui.NewURLBuilder('h').SetZid(user.Zid.ZettelID()).String(), user.GetDefault(api.KeyUserID, "") } type renderBinder struct { - err error - make func(string) (*sx.Symbol, error) - env sxeval.Environment + err error + binding *sxeval.Binding } -func makeRenderBinder(sf sx.SymbolFactory, env sxeval.Environment, err error) renderBinder { - return renderBinder{make: sf.Make, env: env, err: err} +func makeRenderBinder(bind *sxeval.Binding, err error) renderBinder { + return renderBinder{binding: bind, err: err} } func (rb *renderBinder) bindString(key string, obj sx.Object) { if rb.err == nil { - sym, err := rb.make(key) - if err == nil { - rb.err = rb.env.Bind(sym, obj) - return - } - rb.err = err + rb.err = rb.binding.Bind(sx.MakeSymbol(key), obj) } } func (rb *renderBinder) bindSymbol(sym *sx.Symbol, obj sx.Object) { if rb.err == nil { - rb.err = rb.env.Bind(sym, obj) + rb.err = rb.binding.Bind(sym, obj) } } func (rb *renderBinder) bindKeyValue(key string, value string) { rb.bindString("meta-"+key, sx.String(value)) if kt := meta.Type(key); kt.IsSet { @@ -229,19 +226,13 @@ rb.bindString("set-meta-"+key, makeStringList(meta.ListFromValue(value))) } } func (rb *renderBinder) rebindResolved(key, defKey string) { if rb.err == nil { - sym, err := rb.make(key) - if err == nil { - if obj, found := sxeval.Resolve(rb.env, sym); found { - rb.bindString(defKey, obj) - return - } - return - } - rb.err = err + if obj, found := rb.binding.Resolve(sx.MakeSymbol(key)); found { + rb.bindString(defKey, obj) + } } } func (wui *WebUI) bindCommonZettelData(ctx context.Context, rb *renderBinder, user, m *meta.Meta, content *zettel.Content) { strZid := m.Zid.String() @@ -269,29 +260,31 @@ rb.bindString("delete-url", sx.String(newURLBuilder('d').SetZid(apiZid).String())) } if val, found := m.Get(api.KeyUselessFiles); found { rb.bindString("useless", sx.Cons(sx.String(val), nil)) } - rb.bindString("context-url", sx.String(newURLBuilder('h').AppendQuery(strZid+" "+api.ContextDirective).String())) + queryContext := strZid + " " + api.ContextDirective + rb.bindString("context-url", sx.String(newURLBuilder('h').AppendQuery(queryContext).String())) + queryContext += " " + api.FullDirective + rb.bindString("context-full-url", sx.String(newURLBuilder('h').AppendQuery(queryContext).String())) if wui.canRefresh(user) { - rb.bindString("reindex-url", sx.String(newURLBuilder('h').AppendQuery(strZid+" "+api.IdentDirective+api.ActionSeparator+"REINDEX").String())) + rb.bindString("reindex-url", sx.String(newURLBuilder('h').AppendQuery( + strZid+" "+api.IdentDirective+api.ActionSeparator+api.ReIndexAction).String())) } // Ensure to have title, role, tags, and syntax included as "meta-*" rb.bindKeyValue(api.KeyTitle, m.GetDefault(api.KeyTitle, "")) rb.bindKeyValue(api.KeyRole, m.GetDefault(api.KeyRole, "")) rb.bindKeyValue(api.KeyTags, m.GetDefault(api.KeyTags, "")) - rb.bindKeyValue(api.KeySyntax, m.GetDefault(api.KeySyntax, "")) - sentinel := sx.Cons(nil, nil) - curr := sentinel + rb.bindKeyValue(api.KeySyntax, m.GetDefault(api.KeySyntax, meta.DefaultSyntax)) + var metaPairs sx.ListBuilder for _, p := range m.ComputedPairs() { key, value := p.Key, p.Value - curr = curr.AppendBang(sx.Cons(sx.String(key), sx.String(value))) - + metaPairs.Add(sx.Cons(sx.String(key), sx.String(value))) rb.bindKeyValue(key, value) } - rb.bindString("metapairs", sentinel.Tail()) + rb.bindString("metapairs", metaPairs.List()) } func (wui *WebUI) fetchNewTemplatesSxn(ctx context.Context, user *meta.Meta) (lst *sx.Pair) { if !wui.canCreate(ctx, user) { return nil @@ -313,11 +306,11 @@ } if !wui.policy.CanRead(user, z.Meta) { continue } text := sx.String(parser.NormalizedSpacedText(z.Meta.GetTitle())) - link := sx.String(wui.NewURLBuilder('c').SetZid(api.ZettelID(zid.String())). + link := sx.String(wui.NewURLBuilder('c').SetZid(zid.ZettelID()). AppendKVQuery(queryKeyAction, valueActionNew).String()) lst = lst.Cons(sx.Cons(text, link)) } return lst @@ -335,11 +328,11 @@ } } return nil } -func (wui *WebUI) getSxnTemplate(ctx context.Context, zid id.Zid, env sxeval.Environment) (sxeval.Expr, error) { +func (wui *WebUI) getSxnTemplate(ctx context.Context, zid id.Zid, bind *sxeval.Binding) (sxeval.Expr, error) { if t := wui.getSxnCache(zid); t != nil { return t, nil } reader, err := wui.makeZettelReader(ctx, zid) @@ -353,53 +346,58 @@ return nil, err } if len(objs) != 1 { return nil, fmt.Errorf("expected 1 expression in template, but got %d", len(objs)) } - t, err := wui.engine.Parse(objs[0], env) + env := sxeval.MakeExecutionEnvironment(bind) + t, err := env.Compile(objs[0]) if err != nil { return nil, err } - wui.setSxnCache(zid, wui.engine.Rework(t, env)) + wui.setSxnCache(zid, t) return t, nil } func (wui *WebUI) makeZettelReader(ctx context.Context, zid id.Zid) (*sxreader.Reader, error) { ztl, err := wui.box.GetZettel(ctx, zid) if err != nil { return nil, err } - reader := sxreader.MakeReader(bytes.NewReader(ztl.Content.AsBytes()), sxreader.WithSymbolFactory(wui.sf)) + reader := sxreader.MakeReader(bytes.NewReader(ztl.Content.AsBytes())) return reader, nil } -func (wui *WebUI) evalSxnTemplate(ctx context.Context, zid id.Zid, env sxeval.Environment) (sx.Object, error) { - templateExpr, err := wui.getSxnTemplate(ctx, zid, env) +func (wui *WebUI) evalSxnTemplate(ctx context.Context, zid id.Zid, bind *sxeval.Binding) (sx.Object, error) { + templateExpr, err := wui.getSxnTemplate(ctx, zid, bind) if err != nil { return nil, err } - return wui.engine.Execute(templateExpr, env, nil) + env := sxeval.MakeExecutionEnvironment(bind) + return env.Run(templateExpr) } -func (wui *WebUI) renderSxnTemplate(ctx context.Context, w http.ResponseWriter, templateID id.Zid, env sxeval.Environment) error { - return wui.renderSxnTemplateStatus(ctx, w, http.StatusOK, templateID, env) +func (wui *WebUI) renderSxnTemplate(ctx context.Context, w http.ResponseWriter, templateID id.Zid, bind *sxeval.Binding) error { + return wui.renderSxnTemplateStatus(ctx, w, http.StatusOK, templateID, bind) } -func (wui *WebUI) renderSxnTemplateStatus(ctx context.Context, w http.ResponseWriter, code int, templateID id.Zid, env sxeval.Environment) error { - detailObj, err := wui.evalSxnTemplate(ctx, templateID, env) +func (wui *WebUI) renderSxnTemplateStatus(ctx context.Context, w http.ResponseWriter, code int, templateID id.Zid, bind *sxeval.Binding) error { + detailObj, err := wui.evalSxnTemplate(ctx, templateID, bind) if err != nil { return err } - env.Bind(wui.symDetail, detailObj) + bind.Bind(symDetail, detailObj) - pageObj, err := wui.evalSxnTemplate(ctx, id.BaseTemplateZid, env) + pageObj, err := wui.evalSxnTemplate(ctx, id.BaseTemplateZid, bind) if err != nil { return err } - wui.log.Debug().Str("page", pageObj.Repr()).Msg("render") + if msg := wui.log.Debug(); msg != nil { + // pageObj.String() can be expensive to calculate. + msg.Str("page", pageObj.String()).Msg("render") + } - gen := sxhtml.NewGenerator(wui.sf, sxhtml.WithNewline) + gen := sxhtml.NewGenerator(sxhtml.WithNewline) var sb bytes.Buffer _, err = gen.WriteHTML(&sb, pageObj) if err != nil { return err } Index: web/adapter/webui/webui.go ================================================================== --- web/adapter/webui/webui.go +++ web/adapter/webui/webui.go @@ -4,10 +4,13 @@ // 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. +// +// SPDX-License-Identifier: EUPL-1.2 +// SPDX-FileCopyrightText: 2021-present Detlef Stern //----------------------------------------------------------------------------- // Package webui provides web-UI handlers for web requests. package webui @@ -62,23 +65,15 @@ loginURL string logoutURL string searchURL string createNewURL string - sf sx.SymbolFactory - engine *sxeval.Engine - mxZettelEnv sync.Mutex - zettelEnv sxeval.Environment - dag id.Digraph - genHTML *sxhtml.Generator - - symMetaHeader *sx.Symbol - symDetail *sx.Symbol - symA, symHref *sx.Symbol - symSpan *sx.Symbol - symAttr *sx.Symbol - symAttrOpen *sx.Symbol + rootBinding *sxeval.Binding + mxZettelBinding sync.Mutex + zettelBinding *sxeval.Binding + dag id.Digraph + genHTML *sxhtml.Generator } // webuiBox contains all box methods that are needed for WebUI operation. // // Note: these function must not do auth checking. @@ -93,11 +88,10 @@ // New creates a new WebUI struct. func New(log *logger.Logger, ab server.AuthBuilder, authz auth.AuthzManager, rtConfig config.Config, token auth.TokenManager, mgr box.Manager, pol auth.Policy, evalZettel *usecase.Evaluate) *WebUI { loginoutBase := ab.NewURLBuilder('i') - sf := sx.MakeMappedFactory(256) wui := &WebUI{ log: log, debug: kernel.Main.GetConfig(kernel.CoreService, kernel.CoreDebug).(bool), ab: ab, @@ -123,26 +117,23 @@ loginURL: loginoutBase.String(), logoutURL: loginoutBase.AppendKVQuery("logout", "").String(), searchURL: ab.NewURLBuilder('h').String(), createNewURL: ab.NewURLBuilder('c').String(), - sf: sf, - zettelEnv: nil, - genHTML: sxhtml.NewGenerator(sf, sxhtml.WithNewline), - symDetail: sf.MustMake("DETAIL"), - symMetaHeader: sf.MustMake("META-HEADER"), - symA: sf.MustMake("a"), - symHref: sf.MustMake("href"), - symSpan: sf.MustMake("span"), - symAttr: sf.MustMake(sxhtml.NameSymAttr), - symAttrOpen: sf.MustMake("open"), - } - wui.engine = wui.createRenderEngine() + zettelBinding: nil, + genHTML: sxhtml.NewGenerator(sxhtml.WithNewline), + } + wui.rootBinding = wui.createRenderBinding() wui.observe(box.UpdateInfo{Box: mgr, Reason: box.OnReload, Zid: id.Invalid}) mgr.RegisterObserver(wui.observe) return wui } + +var ( + symDetail = sx.MakeSymbol("DETAIL") + symMetaHeader = sx.MakeSymbol("META-HEADER") +) func (wui *WebUI) observe(ci box.UpdateInfo) { wui.mxCache.Lock() if ci.Reason == box.OnReload { clear(wui.templateCache) @@ -149,16 +140,16 @@ } else { delete(wui.templateCache, ci.Zid) } wui.mxCache.Unlock() - wui.mxZettelEnv.Lock() + wui.mxZettelBinding.Lock() if ci.Reason == box.OnReload || wui.dag.HasVertex(ci.Zid) { - wui.zettelEnv = nil + wui.zettelBinding = nil wui.dag = nil } - wui.mxZettelEnv.Unlock() + wui.mxZettelBinding.Unlock() } func (wui *WebUI) setSxnCache(zid id.Zid, expr sxeval.Expr) { wui.mxCache.Lock() wui.templateCache[zid] = expr Index: web/content/content.go ================================================================== --- web/content/content.go +++ web/content/content.go @@ -4,10 +4,13 @@ // 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. +// +// SPDX-License-Identifier: EUPL-1.2 +// SPDX-FileCopyrightText: 2022-present Detlef Stern //----------------------------------------------------------------------------- // Package content manages content handling within the web package. // It translates syntax values into content types, and vice versa. package content Index: web/content/content_test.go ================================================================== --- web/content/content_test.go +++ web/content/content_test.go @@ -4,10 +4,13 @@ // 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. +// +// SPDX-License-Identifier: EUPL-1.2 +// SPDX-FileCopyrightText: 2022-present Detlef Stern //----------------------------------------------------------------------------- package content_test import ( Index: web/server/impl/http.go ================================================================== --- web/server/impl/http.go +++ web/server/impl/http.go @@ -4,10 +4,13 @@ // 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. +// +// SPDX-License-Identifier: EUPL-1.2 +// SPDX-FileCopyrightText: 2020-present Detlef Stern //----------------------------------------------------------------------------- package impl import ( @@ -26,11 +29,10 @@ ) // httpServer is a HTTP server. type httpServer struct { http.Server - waitStop chan struct{} } // initializeHTTPServer creates a new HTTP server object. func (srv *httpServer) initializeHTTPServer(addr string, handler http.Handler) { if addr == "" { @@ -43,11 +45,10 @@ // See: https://blog.cloudflare.com/exposing-go-on-the-internet/ ReadTimeout: readTimeout, WriteTimeout: writeTimeout, IdleTimeout: idleTimeout, } - srv.waitStop = make(chan struct{}) } // SetDebug enables debugging goroutines that are started by the server. // Basically, just the timeout values are reset. This method should be called // before running the server. Index: web/server/impl/impl.go ================================================================== --- web/server/impl/impl.go +++ web/server/impl/impl.go @@ -4,10 +4,13 @@ // 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. +// +// SPDX-License-Identifier: EUPL-1.2 +// SPDX-FileCopyrightText: 2021-present Detlef Stern //----------------------------------------------------------------------------- // Package impl provides the Zettelstore web service. package impl Index: web/server/impl/router.go ================================================================== --- web/server/impl/router.go +++ web/server/impl/router.go @@ -4,10 +4,13 @@ // 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. +// +// SPDX-License-Identifier: EUPL-1.2 +// SPDX-FileCopyrightText: 2020-present Detlef Stern //----------------------------------------------------------------------------- package impl import ( @@ -184,17 +187,17 @@ rt.log.Debug().Msg("no auth token found in request") // IP already logged: ServeHTTP return r } tokenData, err := rt.auth.CheckToken(t, k) if err != nil { - rt.log.Sense().Err(err).HTTPIP(r).Msg("invalid auth token") + rt.log.Info().Err(err).HTTPIP(r).Msg("invalid auth token") return r } ctx := r.Context() user, err := rt.ur.GetUser(ctx, tokenData.Zid, tokenData.Ident) if err != nil { - rt.log.Sense().Zid(tokenData.Zid).Str("ident", tokenData.Ident).Err(err).HTTPIP(r).Msg("auth user not found") + rt.log.Info().Zid(tokenData.Zid).Str("ident", tokenData.Ident).Err(err).HTTPIP(r).Msg("auth user not found") return r } return r.WithContext(updateContext(ctx, user, &tokenData)) } Index: web/server/server.go ================================================================== --- web/server/server.go +++ web/server/server.go @@ -4,10 +4,13 @@ // 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. +// +// SPDX-License-Identifier: EUPL-1.2 +// SPDX-FileCopyrightText: 2020-present Detlef Stern //----------------------------------------------------------------------------- // Package server provides the Zettelstore web service. package server Index: www/build.md ================================================================== --- www/build.md +++ www/build.md @@ -22,18 +22,18 @@ 1. Create a working directory. Let's assume, you have created `$HOME/zettelstore`. 1. Change into this directory: `cd $HOME/zettelstore`. 1. Open development: `fossil open $HOME/fossils/zettelstore.fossil`. -## The build tool -In the directory `tools` there is a Go file called `build.go`. -It automates most aspects, (hopefully) platform-independent. +## Tools to build, test, and manage +In the directory `tools` there are some Go files to automate most aspects of +building and testing, (hopefully) platform-independent. -The script is called as: +The build script is called as: ``` -go run tools/build.go [-v] COMMAND +go run tools/build/build.go [-v] COMMAND ``` The flag `-v` enables the verbose mode. It outputs all commands called by the tool. @@ -41,28 +41,37 @@ * `build`: builds the software with correct version information and puts it into a freshly created directory `bin`. * `check`: checks the current state of the working directory to be ready for release (or commit). -* `clean`: removes the build directories and cleans the Go cache. * `version`: prints the current version information. -* `tools`: installs / updates the tools described above: staticcheck, shadow, - unparam, govulncheck. Therefore, the easiest way to build your own version of the Zettelstore software is to execute the command ``` -go run tools/build.go build +go run tools/build/build.go build ``` In case of errors, please send the output of the verbose execution: ``` -go run tools/build.go -v build +go run tools/build/build.go -v build ``` +Other tools are: + +* `go run tools/clean/clean.go` cleans your Go development worspace. +* `go run tools/check/check.go` executes all linters and unit tests. + If you add the option `-r` linters are more strict, to be used for a + release version. +* `go run tools/devtools/devtools.go` install all needed software (see above). +* `go run tools/htmllint/htmllint.go [URL]` checks all generated HTML of a + Zettelstore accessible at the given URL (default: http://localhost:23123). +* `go run tools/testapi/testapi.go` tests the API against a running + Zettelstore, which is started automatically. + ## A note on the use of Fossil Zettelstore is managed by the Fossil version control system. Fossil is an alternative to the ubiquitous Git version control system. However, Go seems to prefer Git and popular platforms that just support Git. Index: www/changes.wiki ================================================================== --- www/changes.wiki +++ www/changes.wiki @@ -1,11 +1,62 @@ Change Log + +

Changes for Version 0.18.0 (pending)

+ -

Changes for Version 0.17.0 (pending)

+

Changes for Version 0.17.0 (2024-03-04)

+ * Context search operates only on explicit references. Add the directive + FULL to follow zettel tags additionally. + (breaking) + * Context cost calculation has been changed. Prepare to retrieve different + result. + (breaking) + * Remove metadata type WordSet. It was never implemented completely, and + nobody complained about this. + (breaking) + * Remove logging level “sense”, “warn”, + “fatal”, and “panic”. + (breaking) + * Add query action REDIRECT which redirects to zettel that is + the first in the query result list. + (minor: api, webui) + * Add link to CONTEXT FULL in the zettel info page. + (minor: webui) + * When generating HTML code to query set based metadata (esp. tags), also + generate a query that matches all values. + (minor: webui) + * Show all metadata with key ending “-url” on zettel view. + (minor: webui) + * Make WebUI form elements a little bit more accessible by using HTML + search tag and inputmode attribute. + (minor: webui) + * Add UI action for role zettel, similar to tag zettel. Obviously forgotten + in release 0.16.0, but thanks to the bug fix v0.16.1 detected. + (minor: webui) + * If an action, which is written in uppercase letters, results in an empty + list, the list of selected zettel is returned instead. This allows some + backward compatibility if a new action is introduced. + (minor) + * Only when query list is not empty, allow to show data and plain encoding, + an optionally show the “Save As Zettel” button. + (minor: webui) + * If query list is greater than three elements, show the number of elements + at bottom (before other encodings). + (minor: webui) + * Zettel with syntax “sxn” are pretty-printed during evaluation. + This allows to retrieve parsed zettel content, which checked for syntax, + but is not pretty-printed. + (minor) + * Some smaller bug fixes and improvements, to the software and to the + documentation. +

Changes for Version 0.16.1 (2023-12-28)

+ * Fix some Sxn definitions to allow role-based UI customizations. + (minor: webui) +

Changes for Version 0.16.0 (2023-11-30)

* Sx function define is removed, as announced for version 0.15.0. Use defvar (to define variables) or defun (to define functions) instead. In addition defunconst defines a constant function, which ensures a fixed @@ -27,11 +78,11 @@ * SHTML encoder fixed w.r.t inline quoting. Previously, an <q> tag was used, which is inappropriate. Restored smart quotes from version 0.3, but with new SxHTML infrastructure. This affect the html encoder and the WebUI too. Now, an empty quote should not result in a warning by HTML linters. (minor: api, webui) - * Add new zettelmarkup inline formatting: ##Text## will mark / + * Add new zettelmarkup inline formatting: ##Text## will mark / highlight the given Text. It is typically used to highlight some text, which is important for you, but not for the original author. When rendered as HTML, the <mark> tag is used. (minor: zettelmarkup) * Add configuration keys to show, not to show, or show the closed list of @@ -43,27 +94,27 @@ * Some smaller bug fixes and improvements, to the software and to the documentation.

Changes for Version 0.15.0 (2023-10-26)

- * Sx function define is now deprecated. It will be removed in - version 0.16. Use defvar or defun instead. Otherwise - the WebUI will not work in version 0.16. + * Sx function define is now deprecated. It will be removed in + version 0.16. Use defvar or defun instead. + Otherwise the WebUI will not work in version 0.16. (major: webui, deprecated) - * Zettel can be re-indexed via WebUI or API query action REINDEX. - The info page of a zettel contains a link to re-index the zettel. In - a query transclusion, this action is ignored. + * Zettel can be re-indexed via WebUI or API query action + REINDEX. The info page of a zettel contains a link to + re-index the zettel. In a query transclusion, this action is ignored. (major: api, webui). * Allow to determine a tag zettel for a given tag. (major: api, webui) * Present user the option to create a (missing) tag zettel (in list view). Results in a new predefined zettel with identifier 00000000090003, which is a template for new tag zettel. (minor: webui) * ZIP file with manual now contains a zettel 00001000000000 that contains - its build date (metadata key created) and version (in the zettel - content) + its build date (metadata key created) and version (in the + zettel content) (minor) * If an error page cannot be created due to template errors (or similar), a plain text error page is delivered instead. It shows the original error and the error that occured durng rendering the original error page. (minor: webui) @@ -71,23 +122,23 @@ documentation.

Changes for Version 0.14.0 (2023-09-22)

* Remove support for JSON. This was marked deprecated in version 0.12.0. Use - the data encoding instead, a form of symbolic expressions. + the data encoding instead, a form of symbolic expressions. (breaking: api; minor: webui) - * Remove deprecated syntax for a context list: CONTEXT zid. Use - zid CONTEXT instead. It was deprecated in version 0.13.0. + * Remove deprecated syntax for a context list: CONTEXT zid. Use + zid CONTEXT instead. It was deprecated in version 0.13.0. (breaking: api, webui, zettelmarkup) * Replace CSS-role-map mechanism with a more general Sx-based one: user specific code may generates parts of resulting HTML document. (breaking: webui) * Allow meta-tags, i.e. zettel for a specific tag. Meta-tags have the tag name as a title and specify the role "tag". (major: webui) * Allow to load sx code from multiple zettel; dependencies are specified - using precursor metadata. + using precursor metadata. (major: webui) * Allow sx code to change WebUI for zettel with specified role. (major: webui) * Some minor usability improvements. (minor: webui) @@ -96,46 +147,47 @@

Changes for Version 0.13.0 (2023-08-07)

* There are for new search operators: less, not less, greater, not greater. These use the same syntax as the operators prefix, not prefix, suffix, not - suffix. The latter are now denoted as [, ![, ], - and !]. The first may operate numerically for metadata like - numbers, timestamps, and zettel identifier. They are not supported for - full-test search. + suffix. The latter are now denoted as [, ![, + ], and !]. The first may operate numerically for + metadata like numbers, timestamps, and zettel identifier. They are not + supported for full-text search. (breaking: api, webui) - * The API endpoint /o/{ID} (order of zettel ID) is no longer - available. Please use the query expression {ID} ITEMS instead. - (breaking: api) - * The API endpoint /u/{ID} (unlinked references of zettel ID) is no - longer available. Please use the query expression {ID} UNLINKED + * The API endpoint /o/{ID} (order of zettel ID) is no longer + available. Please use the query expression {ID} ITEMS instead. (breaking: api) - * All API endpoints allow to encode zettel data with the data + * The API endpoint /u/{ID} (unlinked references of zettel ID) + is no longer available. Please use the query expression {ID} + UNLINKED instead. + (breaking: api) + * All API endpoints allow to encode zettel data with the data encodings, incl. creating, updating, retrieving, and querying zettel. (major: api) - * Change syntax for context query to zid ... CONTEXT. This will - allow to add more directives that operate on zettel identifier. Old syntax - CONTEXT zid will be removed in 0.14. - (major, deprecated) - * Add query directive ITEMS that will produce a list of metadata - of all zettel that are referenced by the originating zettel in a top-level - list. It replaces the API endpoint /o/{ID} (and makes it more - useful). - (major: api, webui) - * Add query directive UNLINKED that will produce a list of metadata - of all zettel that are mentioning the originating zettel in a top-level, - but do not mention them. It replaces the API endpoint /u/{ID} - (and makes it more useful). - (major: api, webui) - * Add query directive IDENT to distinguish a search for a zettel - identifier (“{ID}”), that will list all metadata of zettel - containing that zettel identifier, and a request to just list the metadata - of given zettel (“{ID} IDENT”). The latter could be filtered - further. - (minor: api, webui) - * Add support for metadata key folge-role. + * Change syntax for context query to zid ... CONTEXT. This will + allow to add more directives that operate on zettel identifier. Old syntax + CONTEXT zid will be removed in 0.14. + (major, deprecated) + * Add query directive ITEMS that will produce a list of + metadata of all zettel that are referenced by the originating zettel in + a top-level list. It replaces the API endpoint /o/{ID} (and + makes it more useful). + (major: api, webui) + * Add query directive UNLINKED that will produce a list of + metadata of all zettel that are mentioning the originating zettel in + a top-level, but do not mention them. It replaces the API endpoint + /u/{ID} (and makes it more useful). + (major: api, webui) + * Add query directive IDENT to distinguish a search for + a zettel identifier (“{ID}”), that will list all metadata of + zettel containing that zettel identifier, and a request to just list the + metadata of given zettel (“{ID} IDENT”). The latter could be + filtered further. + (minor: api, webui) + * Add support for metadata key folge-role. (minor) * Allow to create a child from a given zettel. (minor: webui) * Make zettel entry/edit form a little friendlier: auto-prepend missing '#' to tags; ensure that role and syntax receive just a word. @@ -144,13 +196,13 @@ (minor: webui) * Add links to retrieve result of a query in other formats. (minor: webui) * Always log the found configuration file. (minor: server) - * The use of the json zettel encoding is deprecated (since version - 0.12.0). Support for this encoding will be removed in version 0.14.0. - Please use the new data encoding instead. + * The use of the json zettel encoding is deprecated (since + version 0.12.0). Support for this encoding will be removed in version + 0.14.0. Please use the new data encoding instead. (deprecated: api) * Some smaller bug fixes and improvements, to the software and to the documentation. @@ -172,48 +224,50 @@ * Render footer zettel on all WebUI pages. (fix: webui) * Query search operator "=" now compares for equality, ":" compares depending on the value type. (minor: api, webui) - * Search term PICK now respects the original sort order. This makes - it more useful and orthogonal to RANDOM and LIMIT. As - a side effect, zettel lists retrieved via the API are no longer sorted. In - case you want a specific order, you must specify it explicit. + * Search term PICK now respects the original sort order. This + makes it more useful and orthogonal to RANDOM and + LIMIT. As a side effect, zettel lists retrieved via the API + are no longer sorted. In case you want a specific order, you must specify + it explicit. (minor: api, webui) - * New metadata key expire records a timestamp when a zettel should - be treated as, well, expired. + * New metadata key expire records a timestamp when a zettel + should be treated as, well, expired. (minor) - * New metadata keys superior and subordinate (calculated - from superior) allow to specify a hierarchy between zettel. + * New metadata keys superior and subordinate + (calculated from superior) allow to specify a hierarchy + between zettel. (minor) - * Metadata keys with suffix -date and -time are treated as + * Metadata keys with suffix -date and -time are + treated as timestamp values. (minor) - * sexpr zettel encoding is now documented in the manual. + * sexpr zettel encoding is now documented in the manual. (minor: manual) * Build tool allows to install / update external Go tools needed to build the software. (minor) * Show only useful metadata on WebUI, not the internal metadata. (minor: webui) - * The use of the json zettel encoding is deprecated. Support for - this encoding may be removed in future versions. Please use the new - data encoding instead. + * The use of the json zettel encoding is deprecated. Support + for this encoding may be removed in future versions. Please use the new + data encoding instead. (deprecated: api) * Some smaller bug fixes and improvements, to the software and to the documentation.

Changes for Version 0.11.2 (2023-04-16)

- * Render footer zettel on all WebUI pages. - Backported from 0.12.0. - Many thanks to HK for reporting it! + * Render footer zettel on all WebUI pages. Backported from 0.12.0. Many + thanks to HK for reporting it! (fix: webui)

Changes for Version 0.11.1 (2023-03-28)

- * Make PICK search term a little bit more deterministic so that the - “Save As Zettel” button produces the same list. + * Make PICK search term a little bit more deterministic so that + the “Save As Zettel” button produces the same list. (fix: webui)

Changes for Version 0.11.0 (2023-03-27)

* Remove ZJSON encoding. It was announced in version 0.10.0. Use Sexpr encoding instead. @@ -223,27 +277,27 @@ bold text), no links, no footnotes, no citations (the latter made rendering the title often questionable, in some contexts). If you used special entities, please use the unicode characters directly. However, as a good practice, it is often the best to printable ASCII characters. (breaking) - * Remove runtime configuration marker-external. It was added in + * Remove runtime configuration marker-external. It was added in version [#0_0_6|0.0.6] and updated in [#0_0_10|0.0.10]. If you want to change the marker for an external URL, you could modify zettel 00000000020001 (Zettelstore Base CSS) or zettel 00000000025001 (Zettelstore User CSS, preferred) by changing / adding a rule to add some - content after an external tag. + content after an external tag. (breaking: webui) * Add SHTML encoding. This allows to ensure the quality of generated HTML code. In addition, clients might use it, because it is easier to parse and manipulate than ordinary HTML. In the future, HTML template zettel will probably also use SHTML, deprecating the current Mustache syntax (which was added in [#0_0_9|0.0.9]). (major) - * Search term PICK n, where n is an integer value greater - zero, will pick randomly n elements from the search result list. - Somehow similar (and faster) as RANDOM LIMIT n, but allows also - later ordering of the resulting list. + * Search term PICK n, where n is an integer value + greater zero, will pick randomly n elements from the search + result list. Somehow similar (and faster) as RANDOM LIMIT n, + but allows also later ordering of the resulting list. (minor) * Changed cost model for zettel context: a zettel with more outgoing/incoming references has higher cost than a zettel with less references. Also added support for traversing tags, with a similar cost model. As an effect, zettel hubs (in many cases your home zettel) will @@ -259,16 +313,17 @@ * Show button to save a query into a zettel only when the current user has authorization to do it. (fix: webui)

Changes for Version 0.10.0 (2023-01-24)

- * Remove support for endpoints /j, /m, /q, /p, /v. Their functions - are merged into endpoint /z. This was announced in version 0.9.0. - Please use only client library with at least version 0.10.0 too. + * Remove support for endpoints /j, /m, /q, /p, /v. Their + functions are merged into endpoint /z. This was announced in + version 0.9.0. Please use only client library with at least version 0.10.0 + too. (breaking: api) - * Remove support for runtime configuration key footer-html. Use - footer-zettel instead. Deprecated in version 0.9.0. + * Remove support for runtime configuration key footer-html. Use + footer-zettel instead. Deprecated in version 0.9.0. (breaking: webui) * Save a query into a zettel to freeze it. (major: webui) * Allow to show all used metadata keys, linked with their occurrences and their values. @@ -279,11 +334,11 @@ * Some smaller bug fixes and improvements, to the software and to the documentation.

Changes for Version 0.9.0 (2022-12-12)

- * Remove support syntax pikchr. Although it was a nice idea to + * Remove support syntax pikchr. Although it was a nice idea to include it into Zettelstore, the implementation is too brittle (w.r.t. the expected long lifetime of Zettelstore). There should be other ways to support SVG front-ends. (breaking) * Allow to upload content when creating / updating a zettel. @@ -294,61 +349,63 @@ a zettel can be encoded in Markdown. Those aspects will be ignored. (minor: api) * Enhance zettel context by raising the importance of folge zettel (and similar). (minor: api, webui) - * Interpret zettel files with extension .webp as an binary image - file format. + * Interpret zettel files with extension .webp as an binary + image file format. (minor) * Allow to specify service specific log level via statup configuration and via command line. (minor) * Allow to specify a zettel to serve footer content via runtime - comfiguration footer-zettel. Can be overwritten by user zettel. + comfiguration footer-zettel. Can be overwritten by user + zettel. (minor: webui) * Footer data is automatically separated by a thematic break / horizontal rule. If you do not like it, you have to update the base template. (minor: webui) - * Allow to set runtime configuration home-zettel in the user zettel - to make it user-specific. + * Allow to set runtime configuration home-zettel in the user + zettel to make it user-specific. (minor: webui) * Serve favicon.ico from the asset directory. (minor: webui) * Zettelmarkup cheat sheet (minor: manual) - * Runtime configuration key footer-html will be removed in Version - 0.10.0. Please use footer-zettel instead. + * Runtime configuration key footer-html will be removed in + Version 0.10.0. Please use footer-zettel instead. (deprecated: webui) - * In the next version 0.10.0, the API endpoints for a zettel (/j, - /p, /v) will be merged with endpoint /z. Basically, - the previous endpoint will be refactored as query parameter of endpoint - /z. To reduce errors, there will be no version, where the previous - endpoint are still available and the new funnctionality is still there. - This is a warning to prepare for some breaking changes in v0.10.0. This - also affects the API client implementation. + * In the next version 0.10.0, the API endpoints for a zettel + (/j, /p, /v) will be merged with + endpoint /z. Basically, the previous endpoint will be + refactored as query parameter of endpoint /z. To reduce + errors, there will be no version, where the previous endpoint are still + available and the new funnctionality is still there. This is a warning to + prepare for some breaking changes in v0.10.0. This also affects the API + client implementation. (warning: api) * Some smaller bug fixes and improvements, to the software and to the documentation.

Changes for Version 0.8.0 (2022-10-20)

* Remove support for tags within zettel content. Removes also property - metadata keys all-tags and computed-tags. Deprecated in - version 0.7.0. + metadata keys all-tags and computed-tags. + Deprecated in version 0.7.0. (breaking: zettelmarkup, api, webui) - * Remove API endpoint /m, which retrieve aggregated (tags, roles) - zettel identifier. Deprecated in version 0.7.0. + * Remove API endpoint /m, which retrieve aggregated (tags, + roles) zettel identifier. Deprecated in version 0.7.0. (breaking: api) * Remove support for URL query parameter starting with an underscore. Deprecated in version 0.7.0. (breaking: api, webui) * Ignore HTML content by default, and allow HTML gradually by setting - startup value insecure-html. + startup value insecure-html. (breaking: markup) - * Endpoint /q returns list of full metadata, if no query action is - specified. A HTTP call GET /z (retrieving metadata of all or some - zettel) is now an alias for GET /q. + * Endpoint /q returns list of full metadata, if no query action + is specified. A HTTP call GET /z (retrieving metadata of all + or some zettel) is now an alias for GET /q. (major: api) * Allow to create a zettel that acts as the new version of an existing zettel. Useful if you want to have access to older, outdated content. (minor: webui) * Allow transclusion to reference local image via URL. @@ -374,82 +431,84 @@ * 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. + 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. + 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: + 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. + * 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. + * 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 + * 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. + * 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/. + /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. + * 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. + * 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. + * 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. + 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) @@ -517,14 +576,14 @@ (breaking) * “Sexpr” encoding replaces “Native” encoding. Sexpr encoding is much easier to parse, compared with native and ZJSON encoding. In most cases it is smaller than ZJSON. (breaking: api) - * Endpoint /r is changed to /m?_key=role and returns now - a map of role names to the list of zettel having this role. Endpoint - /t is changed to /m?_key=tags. It already returned - mapping described before. + * Endpoint /r is changed to /m?_key=role and + returns now a map of role names to the list of zettel having this role. + Endpoint /t is changed to /m?_key=tags. It + already returned mapping described before. (breaking: api) * Remove support for a default value for metadata key title, role, and syntax. Title and role are now allowed to be empty, an empty syntax value defaults to “plain”. (breaking) @@ -565,12 +624,12 @@

Changes for Version 0.4 (2022-03-08)

* Encoding “djson” renamed to “zjson” (zettel json). (breaking: api; minor: webui) - * Remove inline quotation syntax <<...<<. Now, - ""..."" generates the equivalent code. + * Remove inline quotation syntax <<...<<. Now, + ""..."" generates the equivalent code. Typographical quotes are generated by the browser, not by Zettelstore. (breaking: Zettelmarkup) * Remove inline formatting for monospace. Its syntax is now used by the similar syntax element of literal computer input. Monospace was just a visual element with no semantic association. Now, the syntax @@ -597,11 +656,11 @@ identifier. (minor: api, webui) * Change generated URLs for zettel-creation forms. If you have bookmarked them, e.g. to create a new zettel, you should update. (minor: webui) - * Remove support for metadata key no-index to suppress indexing + * Remove support for metadata key no-index to suppress indexing selected zettel. It was introduced in v0.0.11, but disallows some future optimizations for searching zettel. (minor: api, webui) * Make some metadata-based searches a little bit faster by executing a (in-memory-based) full-text search first. Now only those zettel are @@ -618,11 +677,11 @@ * 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 + * Zettel files with extension .meta are now treated as content files. Previoulsy, they were interpreted as metadata files. The interpretation as metadata files was deprecated in version 0.2. (breaking: directory and file/zip box) * Add syntax “draw” to produce some graphical representations. (major) @@ -641,50 +700,51 @@ documentation.

Changes for Version 0.2 (2022-01-19)

* v0.2.1 (2021-02-01) updates the license year in some documents - * Remove support for ;;small text;; Zettelmarkup. + * Remove support for ;;small text;; Zettelmarkup. (breaking: Zettelmarkup) * On macOS, the downloadable executable program is now called “zettelstore”, as on all other Unix-like platforms. (possibly breaking: macOS) * External metadata (e.g. for zettel with file extension other than - .zettel) are stored in files without an extension. Metadata files - with extension .meta are still recognized, but result in - a warning message. In a future version (probably v0.3), .meta - files will be treated as ordinary content files, possibly resulting in - duplicate content. In other words: usage of .meta files for - storing metadata is deprecated. + .zettel) are stored in files without an extension. Metadata + files with extension .meta are still recognized, but result + in a warning message. In a future version (probably v0.3), + .meta files will be treated as ordinary content files, + possibly resulting in duplicate content. In other words: usage of + .meta files for storing metadata is deprecated. (possibly breaking: directory and file box) * Show unlinked references in info page of each zettel. Unlinked references are phrases within zettel content that might reference another zettel with the same title as the phase. (major: webui) - * Add endpoint /u/{ID} to retrieve unlinked references. + * Add endpoint /u/{ID} to retrieve unlinked references. (major: api) * Provide a logging facility. Log messages are written to standard output. Messages with level “information” are also written to a circular buffer (of length 8192) which can be retrieved via a computed zettel. There is a command - line flag -l LEVEL to specify an application global logging level - on startup (default: “information”). Logging level can also be - changed via the administrator console, even for specific (sub-) services. + line flag -l LEVEL to specify an application global logging + level on startup (default: “information”). Logging level can + also be changed via the administrator console, even for specific (sub-) + services. (major) * The internal handling of zettel files is rewritten. This allows less reloads ands detects when the directory containing the zettel files is removed. The API, WebUI, and the admin console allow to manually refresh the internal state on demand. (major: box, webui) - * .zettel files with YAML header are now correctly written. + * .zettel files with YAML header are now correctly written. (bug) * Selecting zettel based on their metadata allows the same syntax as searching for zettel content. For example, you can list all zettel that - have an identifier not ending with 00 by using the query - id=!<00. + have an identifier not ending with 00 by using the query + id=!<00. (minor: api, webui) - * Remove support for //deprecated emphasized// Zettelmarkup. + * Remove support for //deprecated emphasized// Zettelmarkup. (minor: Zettelmarkup) * Add options to profile the software. Profiling can be enabled at the command line or via the administrator console. (minor) * Add computed zettel that lists all supported parser / recognized zettel @@ -694,14 +754,14 @@ (minor: api) * Renewing an API access token works even if authentication is not enabled. This corresponds to the behaviour of optaining an access token. (minor: api) * If there is nothing to return, use HTTP status code 204, instead of 200 + - Content-Length: 0. + Content-Length: 0. (minor: api) - * Metadata key duplicates stores the duplicate file names, instead - of just a boolean value that there were duplicate file names. + * 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 improvements, to the software and to the documentation. @@ -711,23 +771,24 @@ * v0.1.3 (2021-12-15) fixes a bug where the modification date could be set when a new zettel is created. * v0.1.2 (2021-11-18) fixes a bug when selecting zettel from a list when more than one comparison is negated. * v0.1.1 (2021-11-12) updates the documentation, mostly related to the - deprecation of the // markup. + deprecation of the // markup. * Remove visual Zettelmarkup (italic, underline). Semantic Zettelmarkup (emphasize, insert) is still allowed, but got a different syntax. The new - syntax for inserted text is >>inserted>>, - while its previous syntax now denotes emphasized text: - __emphasized__. The previous syntax for emphasized text is now - deprecated: //deprecated emphasized//. Starting with - Version 0.2.0, the deprecated syntax will not be supported. The - reason is the collision with URLs that also contain the characters - //. The ZMK encoding of a zettel may help with the transition - (/v/{ZettelID}?_part=zettel&_enc=zmk, on the Info page of + syntax for inserted text is + >>inserted>>, while its previous syntax now + denotes emphasized text: __emphasized__. The + previous syntax for emphasized text is now deprecated: //deprecated + emphasized//. Starting with Version 0.2.0, the deprecated + syntax will not be supported. The reason is the collision with URLs that + also contain the characters //. The ZMK encoding of a zettel + may help with the transition + (/v/{ZettelID}?_part=zettel&_enc=zmk, on the Info page of each zettel in the WebUI). Additionally, all deprecated uses of - // will be rendered with a dashed box within the WebUI. + // will be rendered with a dashed box within the WebUI. (breaking: Zettelmarkup). * API client software is now a [https://zettelstore.de/client/|separate] project. (breaking) * Initial support for HTTP security headers (Content-Security-Policy, @@ -737,20 +798,21 @@ * Remove visual Zettelmarkup (bold, striketrough). Semantic Zettelmarkup (strong, delete) is still allowed and replaces the visual elements syntactically. The visual appearance should not change (depends on your changes / additions to CSS zettel). (possibly breaking: Zettelmarkup). - * Add API endpoint POST /v to retrieve HTMl and text encoded + * Add API endpoint POST /v to retrieve HTMl and text encoded strings from given ZettelMarkup encoded values. This will be used to render a HTML page from a given zettel: in many cases the title of a zettel must be treated separately. (minor: api) - * Add API endpoint /m to retrieve only the metadata of a zettel. + * Add API endpoint /m to retrieve only the metadata of + a zettel. (minor: api) - * New metadata value content-tags contains the tags that were given - in the zettel content. To put it simply, all-tags = tags - + content-tags. + * New metadata value content-tags contains the tags that were + given in the zettel content. To put it simply, all-tags + = tags + content-tags. (minor) * Calculating the context of a zettel stops at the home zettel. (minor: api, webui) * When renaming or deleting a zettel, a warning will be given, if other zettel references the given zettel, or when “deleting” will @@ -772,98 +834,99 @@ documentation.

Changes for Version 0.0.15 (2021-09-17)

* Move again endpoint characters for authentication to make room for future - features. WebUI authentication moves from /a to /i - (login) and /i?logout (logout). API authentication moves from - /v to /a. JSON-based basic zettel handling moves from - /z to /j and /z/{ID} to /j/{ID}. Since - the API client is updated too, this should not be a breaking change for - most users. + features. WebUI authentication moves from /a to + /i (login) and /i?logout (logout). API + authentication moves from /v to /a. JSON-based + basic zettel handling moves from /z to /j and + /z/{ID} to /j/{ID}. Since the API client is + updated too, this should not be a breaking change for most users. (minor: api, webui; possibly breaking) - * Add API endpoint /v/{ID} to retrieve an evaluated zettel in - various encodings. Mostly replaces endpoint /z/{ID} for other + * Add API endpoint /v/{ID} to retrieve an evaluated zettel in + various encodings. Mostly replaces endpoint /z/{ID} for other encodings except “json” and “raw”. Endpoint - /j/{ID} now only returns JSON data, endpoint /z/{ID} is - used to retrieve plain zettel data (previously called “raw”). - See documentation for details. + /j/{ID} now only returns JSON data, endpoint + /z/{ID} is used to retrieve plain zettel data (previously + called “raw”). See documentation for details. (major: api; breaking) * Metadata values of type tag set (the metadata with key - tags is its most prominent example), are now compared in + tags is its most prominent example), are now compared in a case-insensitive manner. Tags that only differ in upper / lower case character are now treated identical. This might break your workflow, if you depend on case-sensitive comparison of tag values. Tag values are translated to their lower case equivalent before comparing them and when you edit a zettel through Zettelstore. If you just modify the zettel files, your tag values remain unchanged. (major; breaking) - * Endpoint /z/{ID} allows the same methods as endpoint - /j/{ID}: GET retrieves zettel (see above), PUT - updates a zettel, DELETE deletes a zettel, MOVE renames - a zettel. In addtion, POST /z will create a new zettel. When - zettel data must be given, the format is plain text, with metadata - separated from content by an empty line. See documentation for more - details. + * Endpoint /z/{ID} allows the same methods as endpoint + /j/{ID}: GET retrieves zettel (see above), + PUT updates a zettel, DELETE deletes a zettel, + MOVE renames a zettel. In addtion, POST /z will + create a new zettel. When zettel data must be given, the format is plain + text, with metadata separated from content by an empty line. See + documentation for more details. (major: api (plus WebUI for some details)) * Allows to transclude / expand the content of another zettel into a target zettel when the zettel is rendered. By using the syntax of embedding an image (which is some kind of expansion too), the first top-level paragraph of a zettel may be transcluded into the target zettel. Endless recursion is checked, as well as a possible “transclusion bomb ” (similar to a XML bomb). See manual for details. (major: zettelmarkup) - * The endpoint /z allows to list zettel in a simpler format than - endpoint /j: one line per zettel, and only zettel identifier plus - zettel title. + * The endpoint /z allows to list zettel in a simpler format + than endpoint /j: one line per zettel, and only zettel + identifier plus zettel title. (minor: api) * Folgezettel are now displayed with full title at the bottom of a page. (minor: webui) - * Add API endpoint /p/{ID} to retrieve a parsed, but not evaluated - zettel in various encodings. + * 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 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 - configuration keys to specify boxes box-uriX (previously - place-uri-X. Older changes documented here are renamed - too. + configuration keys to specify boxes box-uriX + (previously place-uri-X. Older changes documented + here are renamed too. (breaking) * Add API for creating, updating, renaming, and deleting zettel. (major: api) * Initial API client for Go. (major: api) * Remove support for paging of WebUI list. Runtime configuration key - list-page-size is removed. If you still specify it, it will be - ignored. + list-page-size is removed. If you still specify it, it will + be ignored. (major: webui) - * Use endpoint /v for user authentication via API. Endpoint - /a is now used for the web user interface only. Similar, endpoint - /y (“zettel context”) is renamed to /x. + * Use endpoint /v for user authentication via API. Endpoint + /a is now used for the web user interface only. Similar, + endpoint /y (“zettel context”) is renamed to + /x. (minor, possibly breaking) * Type of used-defined metadata is determined by suffix of key: - -number, -url, -zid will result the values to - be interpreted as a number, an URL, or a zettel identifier. + -number, -url, -zid will result the + values to be interpreted as a number, an URL, or a zettel identifier. (minor, but possibly breaking if you already used a metadata key with above suffixes, but as a string type) - * New user-role “creator”, which is only allowed to + * New user-role “creator”, which is only allowed to create new zettel (except user zettel). This role may only read and update public zettel or its own user zettel. Added to support future client software (e.g. on a mobile device) that automatically creates new zettel but, in case of a password loss, should not allow to read existing zettel. (minor, possibly breaking, because new zettel template zettel must always - prepend the string new- before metdata keys that should be + prepend the string new- before metdata keys that should be transferred to the new zettel) - * New suported metadata key box-number, which gives an indication - from which box the zettel was loaded. + * New suported metadata key box-number, which gives an + indication from which box the zettel was loaded. (minor) - * New supported syntax html. + * New supported syntax html. (minor) * New predefined zettel “User CSS” that can be used to redefine some predefined CSS (without modifying the base CSS zettel). (minor: webui) * When a user moves a zettel file with additional characters into the box @@ -875,16 +938,16 @@ * 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 - number greater than zero) has been renamed to - box-uri-X. + * Startup configuration box-X-uri (where X is + a number greater than zero) has been renamed to + box-uri-X. (breaking) - * Web server processes startup configuration url-prefix. There is - no need for stripping the prefix by a front-end web server any more. + * Web server processes startup configuration url-prefix. There + is no need for stripping the prefix by a front-end web server any more. (breaking: webui, api) * Administrator console (only optional accessible locally). Enable it only on systems with a single user or with trusted users. It is disabled by default. (major: core) @@ -891,12 +954,13 @@ * Remove visibility value “simple-expert” introduced in [#0_0_8|version 0.0.8]. It was too complicated, esp. authorization. There was a name collision with the “simple” directory box sub-type. (major) * For security reasons, HTML blocks are not encoded as HTML if they contain - certain snippets, such as <script or <iframe. - These may be caused by using CommonMark as a zettel syntax. + certain snippets, such as <script or + <iframe. These may be caused by using CommonMark as + a zettel syntax. (major) * Full-text search can be a prefix search or a search for equal words, in addition to the search whether a word just contains word of the search term. (minor: api, webui) @@ -911,12 +975,12 @@ * Local images that cannot be read (not found or no access rights) are substituted with the new default image, a spinning emoji. See [/file?name=box/constbox/emoji_spin.gif]. (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. + |%. This allows to paste output of the administrator console + into a zettel. (minor: zmk) * Many smaller bug fixes and improvements, to the software and to the documentation. @@ -929,13 +993,13 @@ directory boxes. The original directory box type is now called "notify" (the default value). There is a new type called "simple". This new type does not notify Zettelstore when some of the underlying Zettel files change. (major) - * Add new startup configuration default-dir-box-type, which gives - the default value for specifying a directory box type. The default value - is “notify”. On macOS, the default value may be changed + * Add new startup configuration default-dir-box-type, which + gives the default value for specifying a directory box type. The default + value is “notify”. On macOS, the default value may be changed “simple” if some errors occur while raising the per-process limit of open files. (minor) @@ -948,11 +1012,11 @@ or a number will be ignored for the search. It is sufficient if the words to be searched are part of words inside a zettel, both content and metadata. (major: api, webui) * A zettel can be excluded from being indexed (and excluded from being found - in a search) if it contains the metadata no-index: true. + in a search) if it contains the metadata no-index: true. (minor: api, webui) * Menu bar is shown when displaying error messages. (minor: webui) * When selecting zettel, it can be specified that a given value should not match. Previously, only the whole select criteria could be @@ -968,106 +1032,118 @@ * 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 improvements, 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. - # You setup a virtualization environment to run Zettelstore on Linux or Windows. + # You setup a virtualization environment to run Zettelstore on Linux or + Windows. # You wait for version 0.0.12 which addresses this issue.

Changes for Version 0.0.10 (2021-02-26)

* Menu item “Home” now redirects to a home zettel. - Its default identifier is 000100000000. - The identifier can be changed with configuration key home-zettel, which supersedes key start. - The default home zettel contains some welcoming information for the new user. + Its default identifier is 000100000000. The identifier can be + changed with configuration key home-zettel, which supersedes + key start. The default home zettel contains some welcoming + information for the new user. (major: webui) - * Show context of a zettel by following all backward and/or forward reference - up to a defined depth and list the resulting zettel. Additionally, some zettel - with similar tags as the initial zettel are also taken into account. + * Show context of a zettel by following all backward and/or forward + reference up to a defined depth and list the resulting zettel. + Additionally, some zettel with similar tags as the initial zettel are also + taken into account. (major: api, webui) - * A zettel that references other zettel within first-level list items, can act - as a “table of contents” zettel. - The API endpoint /o/{ID} allows to retrieve the referenced zettel in - the same order as they occur in the zettel. + * A zettel that references other zettel within first-level list items, can + act as a “table of contents” zettel. The API endpoint + /o/{ID} allows to retrieve the referenced zettel in the same + order as they occur in the zettel. (major: api) - * The zettel “New Menu” with identifier 00000000090000 contains - a list of all zettel that should act as a template for new zettel. - They are listed in the WebUIs ”New“ menu. - This is an application of the previous item. - It supersedes the usage of a role new-template introduced in [#0_0_6|version 0.0.6]. - Please update your zettel if you make use of the now deprecated feature. + * The zettel “New Menu” with identifier + 00000000090000 contains a list of all zettel that should act + as a template for new zettel. They are listed in the WebUIs + ”New“ menu. This is an application of the previous item. It + supersedes the usage of a role new-template introduced in + [#0_0_6|version 0.0.6]. Please update your zettel if you make use of + the now deprecated feature. (major: webui) - * A reference that starts with two slash characters (“//”) - it will be interpreted relative to the value of url-prefix. - For example, if url-prefix has the value /manual/, - the reference [[Zettel list|//h]] will render as - <a href="/manual/h">Zettel list</a>. (minor: syntax) + * A reference that starts with two slash characters + (“//”) it will be interpreted relative to the + value of url-prefix. For example, if url-prefix + has the value /manual/, the reference + [[Zettel list|//h]] will render as <a + href="/manual/h">Zettel list</a>. + (minor: syntax) * Searching/selecting ignores the leading '#' character of tags. (minor: api, webui) - * When result of selecting or searching is presented, the query is written as the page heading. + * When result of selecting or searching is presented, the query is written + as the page heading. (minor: webui) - * A reference to a zettel that contains a URL fragment, will now be processed by the indexer. + * A reference to a zettel that contains a URL fragment, will now be + processed by the indexer. (bug: server) - * Runtime configuration key marker-external now defaults to + * Runtime configuration key marker-external now defaults to “&#10138;” (“➚”). It is more beautiful than the previous “&#8599;&#xfe0e;” (“↗︎”), which also needed the additional - “&#xfe0e;” to disable the conversion to an emoji on iPadOS. + “&#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. + * A pre-build binary for macOS ARM64 (also known as Apple silicon) is + available. (minor: infrastructure) - * Many smaller bug fixes and improvements, 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 [https://github.com/zettelstore/zettelstore-github|zettelstore-github].

Server / API

* (major) Support for property metadata. - Metadata key published is the first example of such + Metadata key published is the first example of such a property. * (major) A background activity (called indexer) continuously monitors zettel changes to establish the reverse direction of found internal links. This affects the new metadata keys - precursor and folge. A user specifies the - precursor of a zettel and the indexer computes the property + precursor and folge. A user specifies + the precursor of a zettel and the indexer computes the property metadata for [https://forum.zettelkasten.de/discussion/996/definition-folgezettel|Folgezettel]. Metadata keys with type “Identifier” or “IdentifierSet” that have no inverse key (like - precursor and folge with add to the key - forward that also collects all internal links within the - content. The computed inverse is backward, which provides - all backlinks. The key back is computed as the value of - backward, but without forward links. Therefore, - back is something like the list of “smart - backlinks”. + precursor and folge with add to the key + forward that also collects all internal links within + the content. The computed inverse is backward, which + provides all backlinks. The key back is computed as + the value of backward, but without forward links. + Therefore, back is something like the list of + “smart backlinks”. * (minor) If Zettelstore is being stopped, an appropriate message is written in the console log. * (minor) New computed zettel with environmental data, the list of supported meta data keys, and statistics about all configured zettel boxes. Some other computed zettel got a new identifier (to make room for other variant). - * (minor) Remove zettel 00000000000004, which contained the Go + * (minor) Remove zettel 00000000000004, which contained the Go version that produced the Zettelstore executable. It was too specific to the current implementation. This information is now - included in zettel 00000000000006 (Zettelstore + included in zettel 00000000000006 (Zettelstore Environment Values). * (minor) Predefined templates for new zettel do not contain any value for - attribute visibility any more. + attribute visibility any more. * (minor) Add a new metadata key type called “Zettelmarkup”. It is a non-empty string, that will be formatted with - Zettelmarkup. title and default-title have this - type. + Zettelmarkup. title and default-title + have this type. * (major) Rename zettel syntax “meta” to “none”. Please update the Zettelstore Runtime Configuration and all other zettel that previously used the value “meta”. Other zettel are typically user zettel, used for authentication. However, there is no real harm, if you do not update these zettel. @@ -1074,12 +1150,13 @@ In this case, the metadata is just not presented when rendered. Zettelstore will still work. * (minor) Login will take at least 500 milliseconds to mitigate login attacks. This affects both the API and the WebUI. * (minor) Add a sort option “_random” to produce a zettel list - in random order. _order / order are now an - aliases for the query parameters _sort / sort. + in random order. _order / order are now + an aliases for the query parameters _sort + / sort.

WebUI

* (major) HTML template zettel for WebUI now use [https://mustache.github.io/|Mustache] syntax instead of previously used [https://golang.org/pkg/html/template/|Go @@ -1093,70 +1170,71 @@ header of a rendered zettel. If a zettel has real backlinks, they are shown at the botton of the page (“Additional links to this zettel”). * (minor) All property metadata, even computed metadata is shown in the info page of a zettel. - * (minor) Rendering of metadata keys title and - default-title in info page changed to a full HTML output - for these Zettelmarkup encoded values. + * (minor) Rendering of metadata keys title and + default-title in info page changed to a full HTML + output for these Zettelmarkup encoded values. * (minor) Always show the zettel identifier on the zettel detail view. Previously, the identifier was not shown if the zettel was not editable. * (minor) Do not show computed metadata in edit forms anymore.

Changes for Version 0.0.8 (2020-12-23)

Server / API

- * (bug) Zettel files with extension .jpg and without metadata will - get a syntax value “jpg”. The internal data - structure got the same value internally, instead of + * (bug) Zettel files with extension .jpg and without metadata + will get a syntax value “jpg”. The internal + data structure got the same value internally, instead of “jpeg”. This has been fixed for all possible alternative syntax values. - * (bug) If a file, e.g. an image file like 20201130190200.jpg, is - added to the directory box, its metadata are just calculated from + * (bug) If a file, e.g. an image file like 20201130190200.jpg, + is added to the directory box, its metadata are just calculated from the information available. Updated metadata did not find its way - into the zettel box, because the .meta file was not + into the zettel box, because the .meta file was not written. - * (bug) If just the .meta file was deleted manually, the zettel was - assumed to be missing. A workaround is to restart the software. If - the .meta file is deleted, metadata is now calculated in - the same way when the .meta file is non-existing at the - start of the software. + * (bug) If just the .meta file was deleted manually, the zettel + was assumed to be missing. A workaround is to restart the software. + If the .meta file is deleted, metadata is now + calculated in the same way when the .meta file is + non-existing at the start of the software. * (bug) A link to the current zettel, only using a fragment (e.g. [[Title|#title]]) is now handled correctly as a zettel link (and not as a link to external material). * (minor) Allow zettel to be marked as “read only”. - This is done through the metadata key read-only. + This is done through the metadata key read-only. * (bug) When renaming a zettel, check all boxes for the new zettel identifier, not just the first one. Otherwise it will be possible to shadow a read-only zettel from a next box, effectively modifying it. * (minor) Add support for a configurable default value for metadata key - visibility. - * (bug) If list-page-size is set to a relatively small value and - the authenticated user is not the owner, some zettel were not - shown in the list of zettel or were not returned by the API. + visibility. + * (bug) If list-page-size is set to a relatively small value + and the authenticated user is not the owner, some zettel were + not shown in the list of zettel or were not returned by the API. * (minor) Add support for new visibility “expert”. An owner becomes an expert, if the runtime configuration key - expert-mode is set to true. + expert-mode is set to true. * (major) Add support for computed zettel. - These zettel have an identifier less than 0000000000100. - Most of them are only visible, if expert-mode is enabled. + These zettel have an identifier less than + 0000000000100. Most of them are only visible, if + expert-mode is enabled. * (bug) Fixes a memory leak that results in too many open files after approx. 125 reload operations. * (major) Predefined templates for new zettel got an explicit value for visibility: “login”. Please update these zettel if you modified them. - * (major) Rename key readonly of Zettelstore Startup - Configuration to read-only-mode. This was done to + * (major) Rename key readonly of Zettelstore Startup + Configuration to read-only-mode. This was done to avoid some confusion with the the zettel metadata key - read-only. Please adapt your startup configuration. + read-only. Please adapt your startup configuration. Otherwise your Zettelstore will be accidentally writable. * (minor) References starting with “./” and “../” are treated as a local reference. Previously, only the prefix “/” was treated as a local reference. - * (major) Metadata key modified will be set automatically to the - current local time if a zettel is updated through Zettelstore. + * (major) Metadata key modified will be set automatically to + the current local time if a zettel is updated through Zettelstore. If you used that key previously for your own, you should rename it before you upgrade. * (minor) The new visibility value “simple-expert” ensures that many computed zettel are shown for new users. This is to enable them to send useful bug reports. @@ -1172,16 +1250,16 @@ * (minor) Move zettel field "role" above "tags" and move "syntax" more to "content". * (minor) Rename zettel operation “clone” to “copy”. * (major) All predefined HTML templates have now a visibility value “expert”. If you want to see them as an non-expert - owner, you must temporary enable expert-mode and change - the visibility metadata value. + owner, you must temporary enable expert-mode and + change the visibility metadata value. * (minor) Initial support for [https://zettelkasten.de/posts/tags/folgezettel/|Folgezettel]. If you click on “Folge” (detail view or info view), a new - zettel is created with a reference (precursor) to the + zettel is created with a reference (precursor) to the original zettel. Title, role, tags, and syntax are copied from the original zettel. * (major) Most predefined zettel have a title prefix of “Zettelstore”. * (minor) If started in simple mode, e.g. via double click or without any @@ -1201,20 +1279,21 @@

Changes for Version 0.0.6 (2020-11-23)

Server

* (major) Rename identifier of Zettelstore Runtime Configuration to - 00000000000100 (previously 00000000000001). This - is done to gain some free identifier with smaller number to be - used internally. If you customized this zettel, please make - sure to rename it to the new identifier. + 00000000000100 (previously + 00000000000001). This is done to gain some free + identifier with smaller number to be used internally. If you + customized this zettel, please make sure to rename it to the new + identifier. * (major) Rename the two essential metadata keys of a user zettel to - credential and user-id. The previous values were - cred and ident. If you enabled user - authentication and added some user zettel, make sure to change - them accordingly. Otherwise these users will not authenticated any - more. + credential and user-id. The previous + values were cred and ident. If you + enabled user authentication and added some user zettel, make sure + to change them accordingly. Otherwise these users will not + authenticated any more. * (minor) Rename the scheme of the box URL where predefined zettel are stored to “const”. The previous value was “globals”.

Zettelmarkup

@@ -1227,11 +1306,11 @@ in valid JSON content. * (bug) All query parameters of selecting zettel must be true, regardless if a specific key occurs more than one or not. * (minor) Encode all inherited meta values in all formats except “raw”. A meta value is called inherited if - there is a key starting with default- in the + there is a key starting with default- in the Zettelstore Runtime Configuration. Applies to WebUI also. * (minor) Automatic calculated identifier for headings (only for “html”, “djson”, “native” format and for the Web user interface). You can use this to provide a zettel reference that links to the heading, without @@ -1252,39 +1331,40 @@ on the WebUI, it will not opened in a new window/tab. They will receive a local marker, when encoded as “djson” or “native”. Local references are listed on the Info page of each zettel. * (minor) Change the default value for some visual sugar put after an - external URL to &\#8599;&\#xfe0e; + external URL to &\#8599;&\#xfe0e; (“↗︎”). This affects the former key - icon-material of the Zettelstore Runtime - Configuration, which is renamed to marker-external. + icon-material of the Zettelstore Runtime + Configuration, which is renamed to + marker-external. * (major) Allow multiple zettel to act as templates for creating new zettel. All zettel with a role value “new-template” act as a template to create a new zettel. The WebUI menu item “New” changed to a drop-down list with all those zettel, ordered by their identifier. All metadata keys with the - prefix new- will be translated to a new or updated + prefix new- will be translated to a new or updated keys/value without that prefix. You can use this mechanism to specify a role for the new zettel, or a different title. The title of the template zettel is used in the drop-down list. The initial template zettel “New Zettel” has now a different - zettel identifier (now: 00000000091001, was: - 00000000040001). Please update it, if you changed that - zettel. + zettel identifier (now: 00000000091001, was: + 00000000040001). Please update it, if you changed + that zettel.
Note: this feature was superseded in [#0_0_10|version 0.0.10] by the “New Menu” zettel. * (minor) When a page should be opened in a new windows (e.g. for external references), the web browser is instructed to decouple the new page from the previous one for privacy and security reasons. In detail, the web browser is instructed to omit referrer information and to omit a JS object linking to the page that contained the external link. * (minor) If the value of the Zettelstore Runtime Configuration key - list-page-size is greater than zero, the number of WebUI - list elements will be restricted and it is possible to change to - the next/previous page to list more elements. + list-page-size is greater than zero, the number of + WebUI list elements will be restricted and it is possible to + change to the next/previous page to list more elements. * (minor) Change CSS to enhance reading: make line-height a little smaller (previous: 1.6, now 1.4) and move list items to the left. @@ -1295,28 +1375,30 @@ * Add support for a custom footer.

Changes for Version 0.0.4 (2020-09-11)

* Optional user authentication/authorization. - * New sub-commands file (use Zettelstore as a command line filter), - password (for authentication), and config. + * New sub-commands file (use Zettelstore as a command line + filter), password (for authentication), and + config.

Changes for Version 0.0.3 (2020-08-31)

* Starting Zettelstore has been changed by introducing sub-commands. This change is also reflected on the server installation procedures. * Limitations on renaming zettel has been relaxed.

Changes for Version 0.0.2 (2020-08-28)

- * Configuration zettel now has ID 00000000000001 (previously: - 00000000000000). - * The zettel with ID 00000000000000 is no longer shown in any + * Configuration zettel now has ID 00000000000001 (previously: + 00000000000000). + * The zettel with ID 00000000000000 is no longer shown in any zettel list. If you changed the configuration zettel, you should rename it manually in its file directory. * Creating a new zettel is now done by cloning an existing zettel. - To mimic the previous behaviour, a zettel with ID 00000000040001 - is introduced. You can change it if you need a different template zettel. + To mimic the previous behaviour, a zettel with ID + 00000000040001 is introduced. You can change it if you need + a different template zettel.

Changes for Version 0.0.1 (2020-08-21)

* Initial public release. 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.16.0 (2023-11-30). +Build: v0.17.0 (2024-03-04). - * [/uv/zettelstore-0.16.0-linux-amd64.zip|Linux] (amd64) - * [/uv/zettelstore-0.16.0-linux-arm.zip|Linux] (arm6, e.g. Raspberry Pi) - * [/uv/zettelstore-0.16.0-darwin-arm64.zip|macOS] (arm64) - * [/uv/zettelstore-0.16.0-darwin-amd64.zip|macOS] (amd64) - * [/uv/zettelstore-0.16.0-windows-amd64.zip|Windows] (amd64) + * [/uv/zettelstore-0.17.0-linux-amd64.zip|Linux] (amd64) + * [/uv/zettelstore-0.17.0-linux-arm.zip|Linux] (arm6, e.g. Raspberry Pi) + * [/uv/zettelstore-0.17.0-darwin-arm64.zip|macOS] (arm64) + * [/uv/zettelstore-0.17.0-darwin-amd64.zip|macOS] (amd64) + * [/uv/zettelstore-0.17.0-windows-amd64.zip|Windows] (amd64) 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.16.0.zip|here]. +[/uv/manual-0.17.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 @@ -24,17 +24,17 @@ * [https://zettelstore.de/sx|Sx] provides an evaluator for symbolic expressions, which is unsed for HTML templates and more. [https://mastodon.social/tags/Zettelstore|Stay tuned] …
-

Latest Release: 0.16.0 (2023-11-30)

+

Latest Release: 0.17.0 (2024-03-04)

* [./download.wiki|Download] - * [./changes.wiki#0_16|Change summary] - * [/timeline?p=v0.16.0&bt=v0.15.0&y=ci|Check-ins for version 0.16.0], - [/vdiff?to=v0.16.0&from=v0.15.0|content diff] - * [/timeline?df=v0.16.0&y=ci|Check-ins derived from the 0.16.0 release], - [/vdiff?from=v0.16.0&to=trunk|content diff] + * [./changes.wiki#0_17|Change summary] + * [/timeline?p=v0.17.0&bt=v0.16.0&y=ci|Check-ins for version 0.17.0], + [/vdiff?to=v0.17.0&from=v0.16.0|content diff] + * [/timeline?df=v0.17.0&y=ci|Check-ins derived from the 0.17.0 release], + [/vdiff?from=v0.17.0&to=trunk|content diff] * [./plan.wiki|Limitations and planned improvements] * [/timeline?t=release|Timeline of all past releases]

Build instructions

Index: zettel/content.go ================================================================== --- zettel/content.go +++ zettel/content.go @@ -4,10 +4,13 @@ // 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. +// +// SPDX-License-Identifier: EUPL-1.2 +// SPDX-FileCopyrightText: 2020-present Detlef Stern //----------------------------------------------------------------------------- package zettel import ( @@ -16,11 +19,11 @@ "errors" "io" "unicode" "unicode/utf8" - "zettelstore.de/z/input" + "zettelstore.de/client.fossil/input" ) // Content is just the content of a zettel. type Content struct { data []byte @@ -111,13 +114,12 @@ // IsBinary returns true if the given data appears to be non-text data. func IsBinary(data []byte) bool { if !utf8.Valid(data) { return true } - l := len(data) - for i := 0; i < l; i++ { + for i := range len(data) { if data[i] == 0 { return true } } return false } Index: zettel/content_test.go ================================================================== --- zettel/content_test.go +++ zettel/content_test.go @@ -4,10 +4,13 @@ // 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. +// +// SPDX-License-Identifier: EUPL-1.2 +// SPDX-FileCopyrightText: 2020-present Detlef Stern //----------------------------------------------------------------------------- package zettel_test import ( Index: zettel/id/digraph.go ================================================================== --- zettel/id/digraph.go +++ zettel/id/digraph.go @@ -4,10 +4,13 @@ // 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. +// +// SPDX-License-Identifier: EUPL-1.2 +// SPDX-FileCopyrightText: 2023-present Detlef Stern //----------------------------------------------------------------------------- package id import ( Index: zettel/id/digraph_test.go ================================================================== --- zettel/id/digraph_test.go +++ zettel/id/digraph_test.go @@ -4,10 +4,13 @@ // 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. +// +// SPDX-License-Identifier: EUPL-1.2 +// SPDX-FileCopyrightText: 2023-present Detlef Stern //----------------------------------------------------------------------------- package id_test import ( Index: zettel/id/edge.go ================================================================== --- zettel/id/edge.go +++ zettel/id/edge.go @@ -4,10 +4,13 @@ // 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. +// +// SPDX-License-Identifier: EUPL-1.2 +// SPDX-FileCopyrightText: 2023-present Detlef Stern //----------------------------------------------------------------------------- package id import "slices" Index: zettel/id/id.go ================================================================== --- zettel/id/id.go +++ zettel/id/id.go @@ -4,10 +4,13 @@ // 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. +// +// SPDX-License-Identifier: EUPL-1.2 +// SPDX-FileCopyrightText: 2020-present Detlef Stern //----------------------------------------------------------------------------- // Package id provides zettel specific types, constants, and functions about // zettel identifier. package id @@ -98,10 +101,13 @@ var result [14]byte zid.toByteArray(&result) return string(result[:]) } +// ZettelID return the zettel identification as a api.ZettelID. +func (zid Zid) ZettelID() api.ZettelID { return api.ZettelID(zid.String()) } + // Bytes converts the zettel identification to a byte slice of 14 digits. // Only defined for valid ids. func (zid Zid) Bytes() []byte { var result [14]byte zid.toByteArray(&result) Index: zettel/id/id_test.go ================================================================== --- zettel/id/id_test.go +++ zettel/id/id_test.go @@ -4,10 +4,13 @@ // 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. +// +// SPDX-License-Identifier: EUPL-1.2 +// SPDX-FileCopyrightText: 2020-present Detlef Stern //----------------------------------------------------------------------------- // Package id_test provides unit tests for testing zettel id specific functions. package id_test @@ -70,20 +73,20 @@ var sResult string // to disable compiler optimization in loop below func BenchmarkString(b *testing.B) { var s string - for n := 0; n < b.N; n++ { + for range b.N { s = id.Zid(12345678901200).String() } sResult = s } var bResult []byte // to disable compiler optimization in loop below func BenchmarkBytes(b *testing.B) { var bs []byte - for n := 0; n < b.N; n++ { + for range b.N { bs = id.Zid(12345678901200).Bytes() } bResult = bs } Index: zettel/id/set.go ================================================================== --- zettel/id/set.go +++ zettel/id/set.go @@ -4,10 +4,13 @@ // 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. +// +// SPDX-License-Identifier: EUPL-1.2 +// SPDX-FileCopyrightText: 2021-present Detlef Stern //----------------------------------------------------------------------------- package id import ( Index: zettel/id/set_test.go ================================================================== --- zettel/id/set_test.go +++ zettel/id/set_test.go @@ -4,10 +4,13 @@ // 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. +// +// SPDX-License-Identifier: EUPL-1.2 +// SPDX-FileCopyrightText: 2021-present Detlef Stern //----------------------------------------------------------------------------- package id_test import ( @@ -135,15 +138,15 @@ } } // func BenchmarkSet(b *testing.B) { // s := id.Set{} -// for i := 0; i < b.N; i++ { +// for range b.N { // s[id.Zid(i)] = true // } // } func BenchmarkSet(b *testing.B) { s := id.Set{} - for i := 0; i < b.N; i++ { + for i := range b.N { s[id.Zid(i)] = struct{}{} } } Index: zettel/id/slice.go ================================================================== --- zettel/id/slice.go +++ zettel/id/slice.go @@ -4,10 +4,13 @@ // 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. +// +// SPDX-License-Identifier: EUPL-1.2 +// SPDX-FileCopyrightText: 2021-present Detlef Stern //----------------------------------------------------------------------------- package id import ( Index: zettel/id/slice_test.go ================================================================== --- zettel/id/slice_test.go +++ zettel/id/slice_test.go @@ -4,10 +4,13 @@ // 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. +// +// SPDX-License-Identifier: EUPL-1.2 +// SPDX-FileCopyrightText: 2021-present Detlef Stern //----------------------------------------------------------------------------- package id_test import ( Index: zettel/meta/collection.go ================================================================== --- zettel/meta/collection.go +++ zettel/meta/collection.go @@ -4,10 +4,13 @@ // 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. +// +// SPDX-License-Identifier: EUPL-1.2 +// SPDX-FileCopyrightText: 2022-present Detlef Stern //----------------------------------------------------------------------------- package meta import "sort" Index: zettel/meta/meta.go ================================================================== --- zettel/meta/meta.go +++ zettel/meta/meta.go @@ -4,10 +4,13 @@ // 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. +// +// SPDX-License-Identifier: EUPL-1.2 +// SPDX-FileCopyrightText: 2020-present Detlef Stern //----------------------------------------------------------------------------- // Package meta provides the zettel specific type 'meta'. package meta @@ -17,12 +20,12 @@ "strings" "unicode" "unicode/utf8" "zettelstore.de/client.fossil/api" + "zettelstore.de/client.fossil/input" "zettelstore.de/client.fossil/maps" - "zettelstore.de/z/input" "zettelstore.de/z/strfun" "zettelstore.de/z/zettel/id" ) type keyUsage int @@ -46,13 +49,10 @@ 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") Index: zettel/meta/meta_test.go ================================================================== --- zettel/meta/meta_test.go +++ zettel/meta/meta_test.go @@ -4,10 +4,13 @@ // 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. +// +// SPDX-License-Identifier: EUPL-1.2 +// SPDX-FileCopyrightText: 2020-present Detlef Stern //----------------------------------------------------------------------------- package meta import ( @@ -229,11 +232,11 @@ } } func pairs2meta(pairs []string) *Meta { m := New(testID) - for i := 0; i < len(pairs); i = i + 2 { + for i := 0; i < len(pairs); i += 2 { m.Set(pairs[i], pairs[i+1]) } return m } Index: zettel/meta/parse.go ================================================================== --- zettel/meta/parse.go +++ zettel/meta/parse.go @@ -4,20 +4,23 @@ // 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. +// +// SPDX-License-Identifier: EUPL-1.2 +// SPDX-FileCopyrightText: 2020-present Detlef Stern //----------------------------------------------------------------------------- package meta import ( "strings" "zettelstore.de/client.fossil/api" + "zettelstore.de/client.fossil/input" "zettelstore.de/client.fossil/maps" - "zettelstore.de/z/input" "zettelstore.de/z/strfun" "zettelstore.de/z/zettel/id" ) // NewFromInput parses the meta data of a zettel. @@ -154,12 +157,10 @@ switch Type(key) { case TypeTagSet: 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: if _, err := id.Parse(v); err == nil { m.Set(key, v) } case TypeIDSet: Index: zettel/meta/parse_test.go ================================================================== --- zettel/meta/parse_test.go +++ zettel/meta/parse_test.go @@ -4,20 +4,23 @@ // 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. +// +// SPDX-License-Identifier: EUPL-1.2 +// SPDX-FileCopyrightText: 2020-present Detlef Stern //----------------------------------------------------------------------------- package meta_test import ( "strings" "testing" "zettelstore.de/client.fossil/api" - "zettelstore.de/z/input" + "zettelstore.de/client.fossil/input" "zettelstore.de/z/zettel/meta" ) func parseMetaStr(src string) *meta.Meta { return meta.NewFromInput(testID, input.NewInput([]byte(src))) @@ -135,11 +138,11 @@ func equalPairs(one, two []meta.Pair) bool { if len(one) != len(two) { return false } - for i := 0; i < len(one); i++ { + for i := range len(one) { if one[i].Key != two[i].Key || one[i].Value != two[i].Value { return false } } return true Index: zettel/meta/type.go ================================================================== --- zettel/meta/type.go +++ zettel/meta/type.go @@ -4,10 +4,13 @@ // 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. +// +// SPDX-License-Identifier: EUPL-1.2 +// SPDX-FileCopyrightText: 2020-present Detlef Stern //----------------------------------------------------------------------------- package meta import ( @@ -50,34 +53,38 @@ TypeString = registerType(api.MetaString, false) TypeTagSet = registerType(api.MetaTagSet, true) TypeTimestamp = registerType(api.MetaTimestamp, false) TypeURL = registerType(api.MetaURL, false) TypeWord = registerType(api.MetaWord, false) - TypeWordSet = registerType(api.MetaWordSet, true) TypeZettelmarkup = registerType(api.MetaZettelmarkup, false) ) // Type returns a type hint for the given key. If no type hint is specified, // TypeUnknown is returned. func (*Meta) Type(key string) *DescriptionType { return Type(key) } + +// Some constants for key suffixes that determine a type. +const ( + SuffixKeyRole = "-role" + SuffixKeyURL = "-url" +) var ( cachedTypedKeys = make(map[string]*DescriptionType) mxTypedKey sync.RWMutex suffixTypes = map[string]*DescriptionType{ - "-date": TypeTimestamp, - "-number": TypeNumber, - "-role": TypeWord, - "-set": TypeWordSet, - "-time": TypeTimestamp, - "-title": TypeZettelmarkup, - "-url": TypeURL, - "-zettel": TypeID, - "-zid": TypeID, - "-zids": TypeIDSet, + "-date": TypeTimestamp, + "-number": TypeNumber, + SuffixKeyRole: TypeWord, + "-time": TypeTimestamp, + "-title": TypeZettelmarkup, + SuffixKeyURL: TypeURL, + "-zettel": TypeID, + "-zid": TypeID, + "-zids": TypeIDSet, } ) // Type returns a type hint for the given key. If no type hint is specified, // TypeEmpty is returned. Index: zettel/meta/type_test.go ================================================================== --- zettel/meta/type_test.go +++ zettel/meta/type_test.go @@ -4,10 +4,13 @@ // 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. +// +// SPDX-License-Identifier: EUPL-1.2 +// SPDX-FileCopyrightText: 2020-present Detlef Stern //----------------------------------------------------------------------------- package meta_test import ( Index: zettel/meta/values.go ================================================================== --- zettel/meta/values.go +++ zettel/meta/values.go @@ -4,10 +4,13 @@ // 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. +// +// SPDX-License-Identifier: EUPL-1.2 +// SPDX-FileCopyrightText: 2020-present Detlef Stern //----------------------------------------------------------------------------- package meta import ( @@ -33,10 +36,12 @@ SyntaxSxn = api.ValueSyntaxSxn SyntaxText = api.ValueSyntaxText SyntaxTxt = "txt" SyntaxWebp = "webp" SyntaxZmk = api.ValueSyntaxZmk + + DefaultSyntax = SyntaxPlain ) // Visibility enumerates the variations of the 'visibility' meta key. type Visibility int Index: zettel/meta/write.go ================================================================== --- zettel/meta/write.go +++ zettel/meta/write.go @@ -4,10 +4,13 @@ // 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. +// +// SPDX-License-Identifier: EUPL-1.2 +// SPDX-FileCopyrightText: 2020-present Detlef Stern //----------------------------------------------------------------------------- package meta import "io" Index: zettel/meta/write_test.go ================================================================== --- zettel/meta/write_test.go +++ zettel/meta/write_test.go @@ -4,10 +4,13 @@ // 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. +// +// SPDX-License-Identifier: EUPL-1.2 +// SPDX-FileCopyrightText: 2020-present Detlef Stern //----------------------------------------------------------------------------- package meta_test import ( Index: zettel/zettel.go ================================================================== --- zettel/zettel.go +++ zettel/zettel.go @@ -4,10 +4,13 @@ // 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. +// +// SPDX-License-Identifier: EUPL-1.2 +// SPDX-FileCopyrightText: 2020-present Detlef Stern //----------------------------------------------------------------------------- // Package zettel provides specific types, constants, and functions for zettel. package zettel