ADDED .deepsource.toml Index: .deepsource.toml ================================================================== --- .deepsource.toml +++ .deepsource.toml @@ -0,0 +1,8 @@ +version = 1 + +[[analyzers]] +name = "go" +enabled = true + + [analyzers.meta] +import_paths = ["github.com/zettelstore/zettelstore"] Index: LICENSE.txt ================================================================== --- LICENSE.txt +++ LICENSE.txt @@ -1,6 +1,6 @@ -Copyright (c) 2020-present Detlef Stern +Copyright (c) 2020-2021 Detlef Stern Licensed under the EUPL Zettelstore is licensed under the European Union Public License, version 1.2 or later (EUPL v. 1.2). The license is available in the official languages of the Index: Makefile ================================================================== --- Makefile +++ Makefile @@ -1,31 +1,28 @@ -## Copyright (c) 2020-present Detlef Stern +## Copyright (c) 2020-2021 Detlef Stern ## -## This file is part of Zettelstore. +## This file is part of zettelstore. ## ## Zettelstore is licensed under the latest version of the EUPL (European Union ## Public License). Please see file LICENSE.txt for your rights and obligations ## under this license. -.PHONY: check relcheck api version build release clean +.PHONY: check api build release clean check: - go run tools/check/check.go - -relcheck: - go run tools/check/check.go -r + go run tools/build.go check api: - go run tools/testapi/testapi.go + go run tools/build.go testapi version: - @echo $(shell go run tools/build/build.go version) + @echo $(shell go run tools/build.go version) build: - go run tools/build/build.go build + go run tools/build.go build release: - go run tools/build/build.go release + go run tools/build.go release clean: - go run tools/clean/clean.go + go run tools/build.go clean Index: README.md ================================================================== --- README.md +++ README.md @@ -11,16 +11,10 @@ To get an initial impression, take a look at the [manual](https://zettelstore.de/manual/). It is a live example of the zettelstore software, running in read-only mode. -[Zettelstore Client](https://t73f.de/r/zsc) provides client software to access -Zettelstore via its API more easily, [Zettelstore -Contrib](https://zettelstore.de/contrib) contains contributed software, which -often connects to Zettelstore via its API. Some of the software packages may be -experimental. - The software, including the manual, is licensed under the [European Union Public License 1.2 (or later)](https://zettelstore.de/home/file?name=LICENSE.txt&ci=trunk). -[Stay tuned](https://mastodon.social/tags/Zettelstore) … +[Stay tuned](https://twitter.com/zettelstore)… Index: VERSION ================================================================== --- VERSION +++ VERSION @@ -1,1 +1,1 @@ -0.18.0-dev +0.0.14 ADDED api/api.go Index: api/api.go ================================================================== --- api/api.go +++ api/api.go @@ -0,0 +1,90 @@ +//----------------------------------------------------------------------------- +// Copyright (c) 2021 Detlef Stern +// +// This file is part of zettelstore. +// +// Zettelstore is licensed under the latest version of the EUPL (European Union +// Public License). Please see file LICENSE.txt for your rights and obligations +// under this license. +//----------------------------------------------------------------------------- + +// Package api contains common definition used for client and server. +package api + +// AuthJSON contains the result of an authentication call. +type AuthJSON struct { + Token string `json:"token"` + Type string `json:"token_type"` + Expires int `json:"expires_in"` +} + +// ZidJSON contains the identifier data of a zettel. +type ZidJSON struct { + ID string `json:"id"` + URL string `json:"url"` +} + +// ZidMetaJSON contains the identifier and the metadata of a zettel. +type ZidMetaJSON struct { + ID string `json:"id"` + URL string `json:"url"` + Meta map[string]string `json:"meta"` +} + +// ZidMetaRelatedList contains identifier/metadata of a zettel and the same for related zettel +type ZidMetaRelatedList struct { + ID string `json:"id"` + URL string `json:"url"` + Meta map[string]string `json:"meta"` + List []ZidMetaJSON `json:"list"` +} + +// ZettelLinksJSON store all links / connections from one zettel to other. +type ZettelLinksJSON struct { + ID string `json:"id"` + URL string `json:"url"` + Links struct { + Incoming []ZidJSON `json:"incoming,omitempty"` + Outgoing []ZidJSON `json:"outgoing,omitempty"` + Local []string `json:"local,omitempty"` + External []string `json:"external,omitempty"` + Meta []string `json:"meta,omitempty"` + } `json:"links"` + Images struct { + Outgoing []ZidJSON `json:"outgoing,omitempty"` + Local []string `json:"local,omitempty"` + External []string `json:"external,omitempty"` + } `json:"images,omitempty"` + Cites []string `json:"cites,omitempty"` +} + +// ZettelDataJSON contains all data for a zettel. +type ZettelDataJSON struct { + Meta map[string]string `json:"meta"` + Encoding string `json:"encoding"` + Content string `json:"content"` +} + +// ZettelJSON contains all data for a zettel, the identifier, the metadata, and the content. +type ZettelJSON struct { + ID string `json:"id"` + URL string `json:"url"` + Meta map[string]string `json:"meta"` + Encoding string `json:"encoding"` + Content string `json:"content"` +} + +// ZettelListJSON contains data for a zettel list. +type ZettelListJSON struct { + List []ZettelJSON `json:"list"` +} + +// TagListJSON specifies the list/map of tags +type TagListJSON struct { + Tags map[string][]string `json:"tags"` +} + +// RoleListJSON specifies the list of roles. +type RoleListJSON struct { + Roles []string `json:"role-list"` +} ADDED api/const.go Index: api/const.go ================================================================== --- api/const.go +++ api/const.go @@ -0,0 +1,108 @@ +//----------------------------------------------------------------------------- +// Copyright (c) 2021 Detlef Stern +// +// This file is part of zettelstore. +// +// Zettelstore is licensed under the latest version of the EUPL (European Union +// Public License). Please see file LICENSE.txt for your rights and obligations +// under this license. +//----------------------------------------------------------------------------- + +// Package api contains common definition used for client and server. +package api + +import ( + "fmt" +) + +// Additional HTTP constants used. +const ( + MethodMove = "MOVE" // HTTP method for renaming a zettel + + HeaderAccept = "Accept" + HeaderContentType = "Content-Type" + HeaderDestination = "Destination" + HeaderLocation = "Location" +) + +// Values for HTTP query parameter. +const ( + QueryKeyDepth = "depth" + QueryKeyDir = "dir" + QueryKeyFormat = "_format" + QueryKeyLimit = "limit" + QueryKeyPart = "_part" +) + +// Supported dir values. +const ( + DirBackward = "backward" + DirForward = "forward" +) + +// Supported format values. +const ( + FormatDJSON = "djson" + FormatHTML = "html" + FormatJSON = "json" + FormatNative = "native" + FormatRaw = "raw" + FormatText = "text" + FormatZMK = "zmk" +) + +var formatEncoder = map[string]EncodingEnum{ + FormatDJSON: EncoderDJSON, + FormatHTML: EncoderHTML, + FormatJSON: EncoderJSON, + FormatNative: EncoderNative, + FormatRaw: EncoderRaw, + FormatText: EncoderText, + FormatZMK: EncoderZmk, +} +var encoderFormat = map[EncodingEnum]string{} + +func init() { + for k, v := range formatEncoder { + encoderFormat[v] = k + } +} + +// Encoder returns the internal encoder code for the given format string. +func Encoder(format string) EncodingEnum { + if e, ok := formatEncoder[format]; ok { + return e + } + return EncoderUnknown +} + +// EncodingEnum lists all valid encoder keys. +type EncodingEnum uint8 + +// Values for EncoderEnum +const ( + EncoderUnknown EncodingEnum = iota + EncoderDJSON + EncoderHTML + EncoderJSON + EncoderNative + EncoderRaw + EncoderText + EncoderZmk +) + +// String representation of an encoder key. +func (e EncodingEnum) String() string { + if f, ok := encoderFormat[e]; ok { + return f + } + return fmt.Sprintf("*Unknown*(%d)", e) +} + +// Supported part values. +const ( + PartID = "id" + PartMeta = "meta" + PartContent = "content" + PartZettel = "zettel" +) ADDED api/urlbuilder.go Index: api/urlbuilder.go ================================================================== --- api/urlbuilder.go +++ api/urlbuilder.go @@ -0,0 +1,114 @@ +//----------------------------------------------------------------------------- +// Copyright (c) 2020-2021 Detlef Stern +// +// This file is part of zettelstore. +// +// Zettelstore is licensed under the latest version of the EUPL (European Union +// Public License). Please see file LICENSE.txt for your rights and obligations +// under this license. +//----------------------------------------------------------------------------- + +// Package api contains common definition used for client and server. +package api + +import ( + "net/url" + "strings" + + "zettelstore.de/z/domain/id" +) + +type urlQuery struct{ key, val string } + +// URLBuilder should be used to create zettelstore URLs. +type URLBuilder struct { + prefix string + key byte + path []string + query []urlQuery + fragment string +} + +// NewURLBuilder creates a new URL builder with the given prefix and key. +func NewURLBuilder(prefix string, key byte) *URLBuilder { + return &URLBuilder{prefix: prefix, key: key} +} + +// Clone an URLBuilder +func (ub *URLBuilder) Clone() *URLBuilder { + cpy := new(URLBuilder) + cpy.key = ub.key + if len(ub.path) > 0 { + cpy.path = make([]string, 0, len(ub.path)) + cpy.path = append(cpy.path, ub.path...) + } + if len(ub.query) > 0 { + cpy.query = make([]urlQuery, 0, len(ub.query)) + cpy.query = append(cpy.query, ub.query...) + } + cpy.fragment = ub.fragment + return cpy +} + +// SetZid sets the zettel identifier. +func (ub *URLBuilder) SetZid(zid id.Zid) *URLBuilder { + if len(ub.path) > 0 { + panic("Cannot add Zid") + } + ub.path = append(ub.path, zid.String()) + return ub +} + +// AppendPath adds a new path element +func (ub *URLBuilder) AppendPath(p string) *URLBuilder { + ub.path = append(ub.path, p) + return ub +} + +// AppendQuery adds a new query parameter +func (ub *URLBuilder) AppendQuery(key, value string) *URLBuilder { + ub.query = append(ub.query, urlQuery{key, value}) + return ub +} + +// ClearQuery removes all query parameters. +func (ub *URLBuilder) ClearQuery() *URLBuilder { + ub.query = nil + ub.fragment = "" + return ub +} + +// SetFragment stores the fragment +func (ub *URLBuilder) SetFragment(s string) *URLBuilder { + ub.fragment = s + return ub +} + +// String produces a string value. +func (ub *URLBuilder) String() string { + var sb strings.Builder + + sb.WriteString(ub.prefix) + if ub.key != '/' { + sb.WriteByte(ub.key) + } + for _, p := range ub.path { + sb.WriteByte('/') + sb.WriteString(url.PathEscape(p)) + } + if len(ub.fragment) > 0 { + sb.WriteByte('#') + sb.WriteString(ub.fragment) + } + for i, q := range ub.query { + if i == 0 { + sb.WriteByte('?') + } else { + sb.WriteByte('&') + } + sb.WriteString(q.key) + sb.WriteByte('=') + sb.WriteString(url.QueryEscape(q.val)) + } + return sb.String() +} Index: ast/ast.go ================================================================== --- ast/ast.go +++ ast/ast.go @@ -1,38 +1,35 @@ //----------------------------------------------------------------------------- -// Copyright (c) 2020-present Detlef Stern +// Copyright (c) 2020-2021 Detlef Stern // -// This file is part of Zettelstore. +// This file is part of zettelstore. // // Zettelstore is licensed under the latest version of the EUPL (European Union // Public License). Please see file LICENSE.txt for your rights and obligations // under this license. -// -// 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 provides the abstract syntax tree. package ast import ( "net/url" - "zettelstore.de/z/zettel" - "zettelstore.de/z/zettel/id" - "zettelstore.de/z/zettel/meta" + "zettelstore.de/z/domain" + "zettelstore.de/z/domain/id" + "zettelstore.de/z/domain/meta" ) // ZettelNode is the root node of the abstract syntax tree. // It is *not* part of the visitor pattern. type ZettelNode struct { + // Zettel domain.Zettel Meta *meta.Meta // Original metadata - Content zettel.Content // Original content + Content domain.Content // Original content Zid id.Zid // Zettel identification. InhMeta *meta.Meta // Metadata of the zettel, with inherited values. Ast BlockSlice // Zettel abstract syntax tree is a sequence of block nodes. - Syntax string // Syntax / parser that produced the Ast } // Node is the interface, all nodes must implement. type Node interface { WalkChildren(v Visitor) @@ -41,10 +38,13 @@ // BlockNode is the interface that all block nodes must implement. type BlockNode interface { Node blockNode() } + +// BlockSlice is a slice of BlockNodes. +type BlockSlice []BlockNode // ItemNode is a node that can occur as a list item. type ItemNode interface { BlockNode itemNode() @@ -65,10 +65,13 @@ // InlineNode is the interface that all inline nodes must implement. type InlineNode interface { Node inlineNode() } + +// InlineSlice is a slice of InlineNodes. +type InlineSlice []InlineNode // Reference is a reference to external or internal material. type Reference struct { URL *url.URL Value string @@ -81,12 +84,11 @@ // Constants for RefState const ( RefStateInvalid RefState = iota // Invalid Reference RefStateZettel // Reference to an internal zettel RefStateSelf // Reference to same zettel with a fragment - RefStateFound // Reference to an existing internal zettel, URL is ajusted + RefStateFound // Reference to an existing internal zettel RefStateBroken // Reference to a non-existing internal zettel RefStateHosted // Reference to local hosted non-Zettel, without URL change RefStateBased // Reference to local non-Zettel, to be prefixed - RefStateQuery // Reference to a zettel query RefStateExternal // Reference to external material ) ADDED ast/attr.go Index: ast/attr.go ================================================================== --- ast/attr.go +++ ast/attr.go @@ -0,0 +1,103 @@ +//----------------------------------------------------------------------------- +// Copyright (c) 2020-2021 Detlef Stern +// +// This file is part of zettelstore. +// +// Zettelstore is licensed under the latest version of the EUPL (European Union +// Public License). Please see file LICENSE.txt for your rights and obligations +// under this license. +//----------------------------------------------------------------------------- + +// Package ast provides the abstract syntax tree. +package ast + +import ( + "strings" +) + +// Attributes store additional information about some node types. +type Attributes struct { + Attrs map[string]string +} + +// HasDefault returns true, if the default attribute "-" has been set. +func (a *Attributes) HasDefault() bool { + if a != nil { + _, ok := a.Attrs["-"] + return ok + } + return false +} + +// RemoveDefault removes the default attribute +func (a *Attributes) RemoveDefault() { + a.Remove("-") +} + +// Get returns the attribute value of the given key and a succes value. +func (a *Attributes) Get(key string) (string, bool) { + if a != nil { + value, ok := a.Attrs[key] + return value, ok + } + return "", false +} + +// Clone returns a duplicate of the attribute. +func (a *Attributes) Clone() *Attributes { + if a == nil { + return nil + } + attrs := make(map[string]string, len(a.Attrs)) + for k, v := range a.Attrs { + attrs[k] = v + } + return &Attributes{attrs} +} + +// Set changes the attribute that a given key has now a given value. +func (a *Attributes) Set(key, value string) *Attributes { + if a == nil { + return &Attributes{map[string]string{key: value}} + } + if a.Attrs == nil { + a.Attrs = make(map[string]string) + } + a.Attrs[key] = value + return a +} + +// Remove the key from the attributes. +func (a *Attributes) Remove(key string) { + if a != nil { + delete(a.Attrs, key) + } +} + +// AddClass adds a value to the class attribute. +func (a *Attributes) AddClass(class string) *Attributes { + if a == nil { + return &Attributes{map[string]string{"class": class}} + } + classes := a.GetClasses() + for _, cls := range classes { + if cls == class { + return a + } + } + classes = append(classes, class) + a.Attrs["class"] = strings.Join(classes, " ") + return a +} + +// GetClasses returns the class values as a string slice +func (a *Attributes) GetClasses() []string { + if a == nil { + return nil + } + classes, ok := a.Attrs["class"] + if !ok { + return nil + } + return strings.Fields(classes) +} ADDED ast/attr_test.go Index: ast/attr_test.go ================================================================== --- ast/attr_test.go +++ ast/attr_test.go @@ -0,0 +1,50 @@ +//----------------------------------------------------------------------------- +// Copyright (c) 2020-2021 Detlef Stern +// +// This file is part of zettelstore. +// +// Zettelstore is licensed under the latest version of the EUPL (European Union +// Public License). Please see file LICENSE.txt for your rights and obligations +// under this license. +//----------------------------------------------------------------------------- + +// Package ast provides the abstract syntax tree. +package ast_test + +import ( + "testing" + + "zettelstore.de/z/ast" +) + +func TestHasDefault(t *testing.T) { + t.Parallel() + attr := &ast.Attributes{} + if attr.HasDefault() { + t.Error("Should not have default attr") + } + attr = &ast.Attributes{Attrs: map[string]string{"-": "value"}} + if !attr.HasDefault() { + t.Error("Should have default attr") + } +} + +func TestAttrClone(t *testing.T) { + t.Parallel() + orig := &ast.Attributes{} + clone := orig.Clone() + if len(clone.Attrs) > 0 { + t.Error("Attrs must be empty") + } + + orig = &ast.Attributes{Attrs: map[string]string{"": "0", "-": "1", "a": "b"}} + clone = orig.Clone() + m := clone.Attrs + if m[""] != "0" || m["-"] != "1" || m["a"] != "b" || len(m) != len(orig.Attrs) { + t.Error("Wrong cloned map") + } + m["a"] = "c" + if orig.Attrs["a"] != "b" { + t.Error("Aliased map") + } +} Index: ast/block.go ================================================================== --- ast/block.go +++ ast/block.go @@ -1,167 +1,129 @@ //----------------------------------------------------------------------------- -// Copyright (c) 2020-present Detlef Stern +// Copyright (c) 2020-2021 Detlef Stern // -// This file is part of Zettelstore. +// This file is part of zettelstore. // // Zettelstore is licensed under the latest version of the EUPL (European Union // Public License). Please see file LICENSE.txt for your rights and obligations // under this license. -// -// SPDX-License-Identifier: EUPL-1.2 -// SPDX-FileCopyrightText: 2020-present Detlef Stern //----------------------------------------------------------------------------- +// Package ast provides the abstract syntax tree. package ast -import "t73f.de/r/zsc/attrs" - // Definition of Block nodes. -// BlockSlice is a slice of BlockNodes. -type BlockSlice []BlockNode - -func (*BlockSlice) blockNode() { /* Just a marker */ } - -// WalkChildren walks down to the descriptions. -func (bs *BlockSlice) WalkChildren(v Visitor) { - if bs != nil { - for _, bn := range *bs { - Walk(v, bn) - } - } -} - -// FirstParagraphInlines returns the inline list of the first paragraph that -// contains a inline list. -func (bs BlockSlice) FirstParagraphInlines() InlineSlice { - for _, bn := range bs { - pn, ok := bn.(*ParaNode) - if !ok { - continue - } - if inl := pn.Inlines; len(inl) > 0 { - return inl - } - } - return nil -} - -//-------------------------------------------------------------------------- - // ParaNode contains just a sequence of inline elements. // Another name is "paragraph". type ParaNode struct { Inlines InlineSlice } -func (*ParaNode) blockNode() { /* Just a marker */ } -func (*ParaNode) itemNode() { /* Just a marker */ } -func (*ParaNode) descriptionNode() { /* Just a marker */ } - -// CreateParaNode creates a parameter block from inline nodes. -func CreateParaNode(nodes ...InlineNode) *ParaNode { return &ParaNode{Inlines: nodes} } +func (pn *ParaNode) blockNode() { /* Just a marker */ } +func (pn *ParaNode) itemNode() { /* Just a marker */ } +func (pn *ParaNode) descriptionNode() { /* Just a marker */ } // WalkChildren walks down the inline elements. -func (pn *ParaNode) WalkChildren(v Visitor) { Walk(v, &pn.Inlines) } +func (pn *ParaNode) WalkChildren(v Visitor) { + WalkInlineSlice(v, pn.Inlines) +} //-------------------------------------------------------------------------- -// VerbatimNode contains uninterpreted text +// VerbatimNode contains lines of uninterpreted text type VerbatimNode struct { - Kind VerbatimKind - Attrs attrs.Attributes - Content []byte + Kind VerbatimKind + Attrs *Attributes + Lines []string } // VerbatimKind specifies the format that is applied to code inline nodes. -type VerbatimKind int +type VerbatimKind uint8 // Constants for VerbatimCode const ( _ VerbatimKind = iota - VerbatimZettel // Zettel content - VerbatimProg // Program code - VerbatimEval // Code to be externally interpreted. Syntax is stored in default attribute. + VerbatimProg // Program code. VerbatimComment // Block comment VerbatimHTML // Block HTML, e.g. for Markdown - VerbatimMath // Block math mode ) -func (*VerbatimNode) blockNode() { /* Just a marker */ } -func (*VerbatimNode) itemNode() { /* Just a marker */ } +func (vn *VerbatimNode) blockNode() { /* Just a marker */ } +func (vn *VerbatimNode) itemNode() { /* Just a marker */ } // WalkChildren does nothing. -func (*VerbatimNode) WalkChildren(Visitor) { /* No children*/ } +func (vn *VerbatimNode) WalkChildren(v Visitor) { /* No children*/ } //-------------------------------------------------------------------------- // RegionNode encapsulates a region of block nodes. type RegionNode struct { Kind RegionKind - Attrs attrs.Attributes + Attrs *Attributes Blocks BlockSlice - Inlines InlineSlice // Optional text at the end of the region + Inlines InlineSlice // Additional text at the end of the region } // RegionKind specifies the actual region type. -type RegionKind int +type RegionKind uint8 // Values for RegionCode const ( _ RegionKind = iota RegionSpan // Just a span of blocks RegionQuote // A longer quotation RegionVerse // Line breaks matter ) -func (*RegionNode) blockNode() { /* Just a marker */ } -func (*RegionNode) itemNode() { /* Just a marker */ } +func (rn *RegionNode) blockNode() { /* Just a marker */ } +func (rn *RegionNode) itemNode() { /* Just a marker */ } // WalkChildren walks down the blocks and the text. func (rn *RegionNode) WalkChildren(v Visitor) { - Walk(v, &rn.Blocks) - Walk(v, &rn.Inlines) + WalkBlockSlice(v, rn.Blocks) + WalkInlineSlice(v, rn.Inlines) } //-------------------------------------------------------------------------- // HeadingNode stores the heading text and level. type HeadingNode struct { - Level int - Attrs attrs.Attributes - Slug string // Heading text, normalized - Fragment string // Heading text, suitable to be used as an unique URL fragment - Inlines InlineSlice // Heading text, possibly formatted + Level int + Inlines InlineSlice // Heading text, possibly formatted + Slug string // Heading text, suitable to be used as an URL fragment + Attrs *Attributes } -func (*HeadingNode) blockNode() { /* Just a marker */ } -func (*HeadingNode) itemNode() { /* Just a marker */ } +func (hn *HeadingNode) blockNode() { /* Just a marker */ } +func (hn *HeadingNode) itemNode() { /* Just a marker */ } // WalkChildren walks the heading text. -func (hn *HeadingNode) WalkChildren(v Visitor) { Walk(v, &hn.Inlines) } +func (hn *HeadingNode) WalkChildren(v Visitor) { + WalkInlineSlice(v, hn.Inlines) +} //-------------------------------------------------------------------------- // HRuleNode specifies a horizontal rule. type HRuleNode struct { - Attrs attrs.Attributes + Attrs *Attributes } -func (*HRuleNode) blockNode() { /* Just a marker */ } -func (*HRuleNode) itemNode() { /* Just a marker */ } +func (hn *HRuleNode) blockNode() { /* Just a marker */ } +func (hn *HRuleNode) itemNode() { /* Just a marker */ } // WalkChildren does nothing. -func (*HRuleNode) WalkChildren(Visitor) { /* No children*/ } +func (hn *HRuleNode) WalkChildren(v Visitor) { /* No children*/ } //-------------------------------------------------------------------------- // NestedListNode specifies a nestable list, either ordered or unordered. type NestedListNode struct { Kind NestedListKind Items []ItemSlice - Attrs attrs.Attributes + Attrs *Attributes } // NestedListKind specifies the actual list type. type NestedListKind uint8 @@ -171,19 +133,17 @@ NestedListOrdered // Ordered list. NestedListUnordered // Unordered list. NestedListQuote // Quote list. ) -func (*NestedListNode) blockNode() { /* Just a marker */ } -func (*NestedListNode) itemNode() { /* Just a marker */ } +func (ln *NestedListNode) blockNode() { /* Just a marker */ } +func (ln *NestedListNode) itemNode() { /* Just a marker */ } // WalkChildren walks down the items. func (ln *NestedListNode) WalkChildren(v Visitor) { - if items := ln.Items; items != nil { - for _, item := range items { - WalkItemSlice(v, item) - } + for _, item := range ln.Items { + WalkItemSlice(v, item) } } //-------------------------------------------------------------------------- @@ -196,24 +156,18 @@ type Description struct { Term InlineSlice Descriptions []DescriptionSlice } -func (*DescriptionListNode) blockNode() { /* Just a marker */ } +func (dn *DescriptionListNode) blockNode() {} // WalkChildren walks down to the descriptions. func (dn *DescriptionListNode) WalkChildren(v Visitor) { - if descrs := dn.Descriptions; descrs != nil { - for i, desc := range descrs { - if len(desc.Term) > 0 { - Walk(v, &descrs[i].Term) // Otherwise, changes in desc.Term will not go back into AST - } - if dss := desc.Descriptions; dss != nil { - for _, dns := range dss { - WalkDescriptionSlice(v, dns) - } - } + for _, desc := range dn.Descriptions { + WalkInlineSlice(v, desc.Term) + for _, dns := range desc.Descriptions { + WalkDescriptionSlice(v, dns) } } } //-------------------------------------------------------------------------- @@ -245,51 +199,33 @@ AlignLeft // Left alignment AlignCenter // Center the content AlignRight // Right alignment ) -func (*TableNode) blockNode() { /* Just a marker */ } +func (tn *TableNode) blockNode() { /* Just a marker */ } // WalkChildren walks down to the cells. func (tn *TableNode) WalkChildren(v Visitor) { - if header := tn.Header; header != nil { - for i := range header { - Walk(v, &header[i].Inlines) // Otherwise changes will not go back - } - } - if rows := tn.Rows; rows != nil { - for _, row := range rows { - for i := range row { - Walk(v, &row[i].Inlines) // Otherwise changes will not go back - } + for _, cell := range tn.Header { + WalkInlineSlice(v, cell.Inlines) + } + for _, row := range tn.Rows { + for _, cell := range row { + WalkInlineSlice(v, cell.Inlines) } } } //-------------------------------------------------------------------------- -// TranscludeNode specifies block content from other zettel to embedded in -// current zettel -type TranscludeNode struct { - Attrs attrs.Attributes - Ref *Reference -} - -func (*TranscludeNode) blockNode() { /* Just a marker */ } - -// WalkChildren does nothing. -func (*TranscludeNode) WalkChildren(Visitor) { /* No children*/ } - -//-------------------------------------------------------------------------- - // BLOBNode contains just binary data that must be interpreted according to // a syntax. type BLOBNode struct { - Description InlineSlice - Syntax string - Blob []byte + Title string + Syntax string + Blob []byte } -func (*BLOBNode) blockNode() { /* Just a marker */ } +func (bn *BLOBNode) blockNode() { /* Just a marker */ } // WalkChildren does nothing. -func (*BLOBNode) WalkChildren(Visitor) { /* No children*/ } +func (bn *BLOBNode) WalkChildren(v Visitor) { /* No children*/ } Index: ast/inline.go ================================================================== --- ast/inline.go +++ ast/inline.go @@ -1,244 +1,206 @@ //----------------------------------------------------------------------------- -// Copyright (c) 2020-present Detlef Stern +// Copyright (c) 2020-2021 Detlef Stern // -// This file is part of Zettelstore. +// This file is part of zettelstore. // // Zettelstore is licensed under the latest version of the EUPL (European Union // Public License). Please see file LICENSE.txt for your rights and obligations // under this license. -// -// SPDX-License-Identifier: EUPL-1.2 -// SPDX-FileCopyrightText: 2020-present Detlef Stern //----------------------------------------------------------------------------- +// Package ast provides the abstract syntax tree. package ast -import ( - "unicode/utf8" - - "t73f.de/r/zsc/attrs" -) - // Definitions of inline nodes. -// InlineSlice is a list of BlockNodes. -type InlineSlice []InlineNode - -func (*InlineSlice) inlineNode() { /* Just a marker */ } - -// CreateInlineSliceFromWords makes a new inline list from words, -// that will be space-separated. -func CreateInlineSliceFromWords(words ...string) InlineSlice { - inl := make(InlineSlice, 0, 2*len(words)-1) - for i, word := range words { - if i > 0 { - inl = append(inl, &SpaceNode{Lexeme: " "}) - } - inl = append(inl, &TextNode{Text: word}) - } - return inl -} - -// WalkChildren walks down to the list. -func (is *InlineSlice) WalkChildren(v Visitor) { - for _, in := range *is { - Walk(v, in) - } -} - -// -------------------------------------------------------------------------- - // TextNode just contains some text. type TextNode struct { Text string // The text itself. } -func (*TextNode) inlineNode() { /* Just a marker */ } +func (tn *TextNode) inlineNode() { /* Just a marker */ } + +// WalkChildren does nothing. +func (tn *TextNode) WalkChildren(v Visitor) { /* No children*/ } + +// -------------------------------------------------------------------------- + +// TagNode contains a tag. +type TagNode struct { + Tag string // The text itself. +} + +func (tn *TagNode) inlineNode() { /* Just a marker */ } // WalkChildren does nothing. -func (*TextNode) WalkChildren(Visitor) { /* No children*/ } +func (tn *TagNode) WalkChildren(v Visitor) { /* No children*/ } // -------------------------------------------------------------------------- // SpaceNode tracks inter-word space characters. type SpaceNode struct { Lexeme string } -func (*SpaceNode) inlineNode() { /* Just a marker */ } +func (sn *SpaceNode) inlineNode() { /* Just a marker */ } // WalkChildren does nothing. -func (*SpaceNode) WalkChildren(Visitor) { /* No children*/ } - -// Count returns the number of space runes. -func (sn *SpaceNode) Count() int { - return utf8.RuneCountInString(sn.Lexeme) -} +func (sn *SpaceNode) WalkChildren(v Visitor) { /* No children*/ } // -------------------------------------------------------------------------- // BreakNode signals a new line that must / should be interpreted as a new line break. type BreakNode struct { Hard bool // Hard line break? } -func (*BreakNode) inlineNode() { /* Just a marker */ } +func (bn *BreakNode) inlineNode() { /* Just a marker */ } // WalkChildren does nothing. -func (*BreakNode) WalkChildren(Visitor) { /* No children*/ } +func (bn *BreakNode) WalkChildren(v Visitor) { /* No children*/ } // -------------------------------------------------------------------------- // LinkNode contains the specified link. type LinkNode struct { - Attrs attrs.Attributes // Optional attributes - Ref *Reference - Inlines InlineSlice // The text associated with the link. -} - -func (*LinkNode) inlineNode() { /* Just a marker */ } - -// WalkChildren walks to the link text. -func (ln *LinkNode) WalkChildren(v Visitor) { - if len(ln.Inlines) > 0 { - Walk(v, &ln.Inlines) - } -} - -// -------------------------------------------------------------------------- - -// EmbedRefNode contains the specified embedded reference material. -type EmbedRefNode struct { - Attrs attrs.Attributes // Optional attributes - Ref *Reference // The reference to be embedded. - Syntax string // Syntax of referenced material, if known - Inlines InlineSlice // Optional text associated with the image. -} - -func (*EmbedRefNode) inlineNode() { /* Just a marker */ } - -// WalkChildren walks to the text that describes the embedded material. -func (en *EmbedRefNode) WalkChildren(v Visitor) { Walk(v, &en.Inlines) } - -// -------------------------------------------------------------------------- - -// EmbedBLOBNode contains the specified embedded BLOB material. -type EmbedBLOBNode struct { - Attrs attrs.Attributes // Optional attributes - Syntax string // Syntax of Blob - Blob []byte // BLOB data itself. - Inlines InlineSlice // Optional text associated with the image. -} - -func (*EmbedBLOBNode) inlineNode() { /* Just a marker */ } - -// WalkChildren walks to the text that describes the embedded material. -func (en *EmbedBLOBNode) WalkChildren(v Visitor) { Walk(v, &en.Inlines) } + Ref *Reference + Inlines InlineSlice // The text associated with the link. + OnlyRef bool // True if no text was specified. + Attrs *Attributes // Optional attributes +} + +func (ln *LinkNode) inlineNode() { /* Just a marker */ } + +// WalkChildren walks to the link text. +func (ln *LinkNode) WalkChildren(v Visitor) { + WalkInlineSlice(v, ln.Inlines) +} + +// -------------------------------------------------------------------------- + +// ImageNode contains the specified image reference. +type ImageNode struct { + Ref *Reference // Reference to image + Blob []byte // BLOB data of the image, as an alternative to Ref. + Syntax string // Syntax of Blob + Inlines InlineSlice // The text associated with the image. + Attrs *Attributes // Optional attributes +} + +func (in *ImageNode) inlineNode() { /* Just a marker */ } + +// WalkChildren walks to the image text. +func (in *ImageNode) WalkChildren(v Visitor) { + WalkInlineSlice(v, in.Inlines) +} // -------------------------------------------------------------------------- // CiteNode contains the specified citation. type CiteNode struct { - Attrs attrs.Attributes // Optional attributes - Key string // The citation key - Inlines InlineSlice // Optional text associated with the citation. + Key string // The citation key + Inlines InlineSlice // The text associated with the citation. + Attrs *Attributes // Optional attributes } -func (*CiteNode) inlineNode() { /* Just a marker */ } +func (cn *CiteNode) inlineNode() { /* Just a marker */ } // WalkChildren walks to the cite text. -func (cn *CiteNode) WalkChildren(v Visitor) { Walk(v, &cn.Inlines) } +func (cn *CiteNode) WalkChildren(v Visitor) { + WalkInlineSlice(v, cn.Inlines) +} // -------------------------------------------------------------------------- // MarkNode contains the specified merked position. // It is a BlockNode too, because although it is typically parsed during inline // mode, it is moved into block mode afterwards. type MarkNode struct { - Mark string // The mark text itself - Slug string // Slugified form of Mark - Fragment string // Unique form of Slug - Inlines InlineSlice // Marked inline content + Text string } -func (*MarkNode) inlineNode() { /* Just a marker */ } +func (mn *MarkNode) inlineNode() { /* Just a marker */ } // WalkChildren does nothing. -func (mn *MarkNode) WalkChildren(v Visitor) { - if len(mn.Inlines) > 0 { - Walk(v, &mn.Inlines) - } -} +func (mn *MarkNode) WalkChildren(v Visitor) { /* No children*/ } // -------------------------------------------------------------------------- // FootnoteNode contains the specified footnote. type FootnoteNode struct { - Attrs attrs.Attributes // Optional attributes - Inlines InlineSlice // The footnote text. + Inlines InlineSlice // The footnote text. + Attrs *Attributes // Optional attributes } -func (*FootnoteNode) inlineNode() { /* Just a marker */ } +func (fn *FootnoteNode) inlineNode() { /* Just a marker */ } // WalkChildren walks to the footnote text. -func (fn *FootnoteNode) WalkChildren(v Visitor) { Walk(v, &fn.Inlines) } +func (fn *FootnoteNode) WalkChildren(v Visitor) { + WalkInlineSlice(v, fn.Inlines) +} // -------------------------------------------------------------------------- // FormatNode specifies some inline formatting. type FormatNode struct { Kind FormatKind - Attrs attrs.Attributes // Optional attributes. + Attrs *Attributes // Optional attributes. Inlines InlineSlice } // FormatKind specifies the format that is applied to the inline nodes. -type FormatKind int +type FormatKind uint8 // Constants for FormatCode const ( - _ FormatKind = iota - FormatEmph // Emphasized text - FormatStrong // Strongly emphasized text - FormatInsert // Inserted text - FormatDelete // Deleted text - FormatSuper // Superscripted text - FormatSub // SubscriptedText - FormatQuote // Quoted text - FormatMark // Marked text - FormatSpan // Generic inline container -) - -func (*FormatNode) inlineNode() { /* Just a marker */ } - -// WalkChildren walks to the formatted text. -func (fn *FormatNode) WalkChildren(v Visitor) { Walk(v, &fn.Inlines) } + _ FormatKind = iota + FormatItalic // Italic text. + FormatEmph // Semantically emphasized text. + FormatBold // Bold text. + FormatStrong // Semantically strongly emphasized text. + FormatUnder // Underlined text. + FormatInsert // Inserted text. + FormatStrike // Text that is no longer relevant or no longer accurate. + FormatDelete // Deleted text. + FormatSuper // Superscripted text. + FormatSub // SubscriptedText. + FormatQuote // Quoted text. + FormatQuotation // Quotation text. + FormatSmall // Smaller text. + FormatSpan // Generic inline container. + FormatMonospace // Monospaced text. +) + +func (fn *FormatNode) inlineNode() { /* Just a marker */ } + +// WalkChildren walks to the formatted text. +func (fn *FormatNode) WalkChildren(v Visitor) { + WalkInlineSlice(v, fn.Inlines) +} // -------------------------------------------------------------------------- // LiteralNode specifies some uninterpreted text. type LiteralNode struct { - Kind LiteralKind - Attrs attrs.Attributes // Optional attributes. - Content []byte + Kind LiteralKind + Attrs *Attributes // Optional attributes. + Text string } // LiteralKind specifies the format that is applied to code inline nodes. -type LiteralKind int +type LiteralKind uint8 // Constants for LiteralCode const ( _ LiteralKind = iota - LiteralZettel // Zettel content - LiteralProg // Inline program code - LiteralInput // Computer input, e.g. Keyboard strokes - LiteralOutput // Computer output + LiteralProg // Inline program code. + LiteralKeyb // Keyboard strokes. + LiteralOutput // Sample output. LiteralComment // Inline comment LiteralHTML // Inline HTML, e.g. for Markdown - LiteralMath // Inline math mode ) -func (*LiteralNode) inlineNode() { /* Just a marker */ } +func (ln *LiteralNode) inlineNode() { /* Just a marker */ } // WalkChildren does nothing. -func (*LiteralNode) WalkChildren(Visitor) { /* No children*/ } +func (ln *LiteralNode) WalkChildren(v Visitor) { /* No children*/ } Index: ast/ref.go ================================================================== --- ast/ref.go +++ ast/ref.go @@ -1,39 +1,30 @@ //----------------------------------------------------------------------------- -// Copyright (c) 2020-present Detlef Stern +// Copyright (c) 2020-2021 Detlef Stern // -// This file is part of Zettelstore. +// This file is part of zettelstore. // // Zettelstore is licensed under the latest version of the EUPL (European Union // Public License). Please see file LICENSE.txt for your rights and obligations // under this license. -// -// SPDX-License-Identifier: EUPL-1.2 -// SPDX-FileCopyrightText: 2020-present Detlef Stern //----------------------------------------------------------------------------- +// Package ast provides the abstract syntax tree. package ast import ( "net/url" - "strings" - - "t73f.de/r/zsc/api" - "zettelstore.de/z/zettel/id" -) - -// QueryPrefix is the prefix that denotes a query expression. -const QueryPrefix = api.QueryPrefix + + "zettelstore.de/z/domain/id" +) // ParseReference parses a string and returns a reference. func ParseReference(s string) *Reference { - if invalidReference(s) { + switch s { + case "", "00000000000000": return &Reference{URL: nil, Value: s, State: RefStateInvalid} } - if strings.HasPrefix(s, QueryPrefix) { - return &Reference{URL: nil, Value: s[len(QueryPrefix):], State: RefStateQuery} - } if state, ok := localState(s); ok { if state == RefStateBased { s = s[1:] } u, err := url.Parse(s) @@ -43,26 +34,21 @@ } u, err := url.Parse(s) if err != nil { return &Reference{URL: nil, Value: s, State: RefStateInvalid} } - if !externalURL(u) { - if _, err = id.Parse(u.Path); err == nil { + if len(u.Scheme)+len(u.Opaque)+len(u.Host) == 0 && u.User == nil { + if _, err := id.Parse(u.Path); err == nil { return &Reference{URL: u, Value: s, State: RefStateZettel} } if u.Path == "" && u.Fragment != "" { return &Reference{URL: u, Value: s, State: RefStateSelf} } } return &Reference{URL: u, Value: s, State: RefStateExternal} } -func invalidReference(s string) bool { return s == "" || s == "00000000000000" } -func externalURL(u *url.URL) bool { - return u.Scheme != "" || u.Opaque != "" || u.Host != "" || u.User != nil -} - func localState(path string) (RefState, bool) { if len(path) > 0 && path[0] == '/' { if len(path) > 1 && path[1] == '/' { return RefStateBased, true } @@ -80,13 +66,10 @@ // String returns the string representation of a reference. func (r Reference) String() string { if r.URL != nil { return r.URL.String() } - if r.State == RefStateQuery { - return QueryPrefix + r.Value - } return r.Value } // IsValid returns true if reference is valid func (r *Reference) IsValid() bool { return r.State != RefStateInvalid } Index: ast/ref_test.go ================================================================== --- ast/ref_test.go +++ ast/ref_test.go @@ -1,18 +1,16 @@ //----------------------------------------------------------------------------- -// Copyright (c) 2020-present Detlef Stern +// Copyright (c) 2020-2021 Detlef Stern // -// This file is part of Zettelstore. +// This file is part of zettelstore. // // Zettelstore is licensed under the latest version of the EUPL (European Union // Public License). Please see file LICENSE.txt for your rights and obligations // under this license. -// -// SPDX-License-Identifier: EUPL-1.2 -// SPDX-FileCopyrightText: 2020-present Detlef Stern //----------------------------------------------------------------------------- +// Package ast_test provides the tests for the abstract syntax tree. package ast_test import ( "testing" Index: ast/walk.go ================================================================== --- ast/walk.go +++ ast/walk.go @@ -1,18 +1,16 @@ //----------------------------------------------------------------------------- -// Copyright (c) 2021-present Detlef Stern +// Copyright (c) 2021 Detlef Stern // -// This file is part of Zettelstore. +// This file is part of zettelstore. // // Zettelstore is licensed under the latest version of the EUPL (European Union // Public License). Please see file LICENSE.txt for your rights and obligations // under this license. -// -// SPDX-License-Identifier: EUPL-1.2 -// SPDX-FileCopyrightText: 2021-present Detlef Stern //----------------------------------------------------------------------------- +// Package ast provides the abstract syntax tree. package ast // Visitor is a visitor for walking the AST. type Visitor interface { Visit(node Node) Visitor @@ -21,19 +19,27 @@ // Walk traverses the AST. func Walk(v Visitor, node Node) { if v = v.Visit(node); v == nil { return } - - // Implementation note: - // It is much faster to use interface dispatching than to use a switch statement. - // On my "cpu: Intel(R) Core(TM) i7-6820HQ CPU @ 2.70GHz", a switch statement - // implementation tooks approx 940-980 ns/op. Interface dispatching is in the - // range of 900-930 ns/op. node.WalkChildren(v) v.Visit(nil) } + +// WalkBlockSlice traverse a block slice. +func WalkBlockSlice(v Visitor, bns BlockSlice) { + for _, bn := range bns { + Walk(v, bn) + } +} + +// WalkInlineSlice traverses an inline slice. +func WalkInlineSlice(v Visitor, ins InlineSlice) { + for _, in := range ins { + Walk(v, in) + } +} // WalkItemSlice traverses an item slice. func WalkItemSlice(v Visitor, ins ItemSlice) { for _, in := range ins { Walk(v, in) DELETED ast/walk_test.go Index: ast/walk_test.go ================================================================== --- ast/walk_test.go +++ ast/walk_test.go @@ -1,74 +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. -// -// SPDX-License-Identifier: EUPL-1.2 -// SPDX-FileCopyrightText: 2021-present Detlef Stern -//----------------------------------------------------------------------------- - -package ast_test - -import ( - "testing" - - "t73f.de/r/zsc/attrs" - "zettelstore.de/z/ast" -) - -func BenchmarkWalk(b *testing.B) { - root := ast.BlockSlice{ - &ast.HeadingNode{ - Inlines: ast.CreateInlineSliceFromWords("A", "Simple", "Heading"), - }, - &ast.ParaNode{ - Inlines: ast.CreateInlineSliceFromWords("This", "is", "the", "introduction."), - }, - &ast.NestedListNode{ - Kind: ast.NestedListUnordered, - Items: []ast.ItemSlice{ - []ast.ItemNode{ - &ast.ParaNode{ - Inlines: ast.CreateInlineSliceFromWords("Item", "1"), - }, - }, - []ast.ItemNode{ - &ast.ParaNode{ - Inlines: ast.CreateInlineSliceFromWords("Item", "2"), - }, - }, - }, - }, - &ast.ParaNode{ - Inlines: ast.CreateInlineSliceFromWords("This", "is", "some", "intermediate", "text."), - }, - ast.CreateParaNode( - &ast.FormatNode{ - Kind: ast.FormatEmph, - Attrs: attrs.Attributes(map[string]string{ - "": "class", - "color": "green", - }), - Inlines: ast.CreateInlineSliceFromWords("This", "is", "some", "emphasized", "text."), - }, - &ast.SpaceNode{Lexeme: " "}, - &ast.LinkNode{ - Ref: &ast.Reference{Value: "http://zettelstore.de"}, - Inlines: ast.CreateInlineSliceFromWords("URL", "text."), - }, - ), - } - v := benchVisitor{} - b.ResetTimer() - 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 @@ -1,16 +1,13 @@ //----------------------------------------------------------------------------- -// Copyright (c) 2021-present Detlef Stern +// Copyright (c) 2021 Detlef Stern // -// This file is part of Zettelstore. +// This file is part of zettelstore. // // Zettelstore is licensed under the latest version of the EUPL (European Union // Public License). Please see file LICENSE.txt for your rights and obligations // under this license. -// -// SPDX-License-Identifier: EUPL-1.2 -// SPDX-FileCopyrightText: 2021-present Detlef Stern //----------------------------------------------------------------------------- // Package auth provides services for authentification / authorization. package auth @@ -17,12 +14,13 @@ import ( "time" "zettelstore.de/z/box" "zettelstore.de/z/config" - "zettelstore.de/z/zettel/id" - "zettelstore.de/z/zettel/meta" + "zettelstore.de/z/domain/id" + "zettelstore.de/z/domain/meta" + "zettelstore.de/z/web/server" ) // BaseManager allows to check some base auth modes. type BaseManager interface { // IsReadonly returns true, if the systems is configured to run in read-only-mode. @@ -43,12 +41,12 @@ type TokenKind int // Allowed values of token kind const ( _ TokenKind = iota - KindAPI - KindwebUI + KindJSON + KindHTML ) // TokenData contains some important elements from a token. type TokenData struct { Token []byte @@ -79,11 +77,11 @@ // Manager is the main interface for providing the service. type Manager interface { TokenManager AuthzManager - BoxWithPolicy(unprotectedBox box.Box, rtConfig config.Config) (box.Box, Policy) + BoxWithPolicy(auth server.Auth, unprotectedBox box.Box, rtConfig config.Config) (box.Box, Policy) } // Policy is an interface for checking access authorization. type Policy interface { // User is allowed to create a new zettel. @@ -96,11 +94,8 @@ CanWrite(user, oldMeta, newMeta *meta.Meta) bool // User is allowed to rename zettel CanRename(user, m *meta.Meta) bool - // User is allowed to delete zettel. + // User is allowed to delete zettel CanDelete(user, m *meta.Meta) bool - - // User is allowed to refresh box data. - CanRefresh(user *meta.Meta) bool } Index: auth/cred/cred.go ================================================================== --- auth/cred/cred.go +++ auth/cred/cred.go @@ -1,26 +1,23 @@ //----------------------------------------------------------------------------- -// Copyright (c) 2020-present Detlef Stern +// Copyright (c) 2020-2021 Detlef Stern // -// This file is part of Zettelstore. +// This file is part of zettelstore. // // Zettelstore is licensed under the latest version of the EUPL (European Union // Public License). Please see file LICENSE.txt for your rights and obligations // under this license. -// -// SPDX-License-Identifier: EUPL-1.2 -// SPDX-FileCopyrightText: 2020-present Detlef Stern //----------------------------------------------------------------------------- // Package cred provides some function for handling credentials. package cred import ( "bytes" "golang.org/x/crypto/bcrypt" - "zettelstore.de/z/zettel/id" + "zettelstore.de/z/domain/id" ) // HashCredential returns a hashed vesion of the given credential func HashCredential(zid id.Zid, ident, credential string) (string, error) { fullCredential := createFullCredential(zid, ident, credential) @@ -45,12 +42,12 @@ return false, err } func createFullCredential(zid id.Zid, ident, credential string) []byte { var buf bytes.Buffer - buf.Write(zid.Bytes()) + buf.WriteString(zid.String()) buf.WriteByte(' ') buf.WriteString(ident) buf.WriteByte(' ') buf.WriteString(credential) return buf.Bytes() } DELETED auth/impl/digest.go Index: auth/impl/digest.go ================================================================== --- auth/impl/digest.go +++ auth/impl/digest.go @@ -1,89 +0,0 @@ -//----------------------------------------------------------------------------- -// 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 impl - -import ( - "bytes" - "crypto" - "crypto/hmac" - "encoding/base64" - - "t73f.de/r/sx" - "t73f.de/r/sx/sxreader" -) - -var encoding = base64.RawURLEncoding - -const digestAlg = crypto.SHA384 - -func sign(claim sx.Object, secret []byte) ([]byte, error) { - var buf bytes.Buffer - _, 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()) - if err != nil { - return nil, err - } - dig := digest.Sum(nil) - encDig := make([]byte, encoding.EncodedLen(len(dig))) - encoding.Encode(encDig, dig) - - token = append(token, '.') - token = append(token, encDig...) - return token, nil -} - -func check(token []byte, secret []byte) (sx.Object, error) { - i := bytes.IndexByte(token, '.') - if i <= 0 || 1024 < i { - return nil, ErrMalformedToken - } - buf := make([]byte, len(token)) - n, err := encoding.Decode(buf, token[:i]) - if err != nil { - return nil, err - } - rdr := sxreader.MakeReader(bytes.NewReader(buf[:n])) - obj, err := rdr.Read() - if err != nil { - return nil, err - } - - var objBuf bytes.Buffer - _, err = sx.Print(&objBuf, obj) - if err != nil { - return nil, err - } - - digest := hmac.New(digestAlg.New, secret) - _, err = digest.Write(objBuf.Bytes()) - if err != nil { - return nil, err - } - - n, err = encoding.Decode(buf, token[i+1:]) - if err != nil { - return nil, err - } - if !hmac.Equal(buf[:n], digest.Sum(nil)) { - return nil, ErrMalformedToken - } - return obj, nil -} Index: auth/impl/impl.go ================================================================== --- auth/impl/impl.go +++ auth/impl/impl.go @@ -1,16 +1,13 @@ //----------------------------------------------------------------------------- -// Copyright (c) 2021-present Detlef Stern +// Copyright (c) 2021 Detlef Stern // -// This file is part of Zettelstore. +// This file is part of zettelstore. // // Zettelstore is licensed under the latest version of the EUPL (European Union // Public License). Please see file LICENSE.txt for your rights and obligations // under this license. -// -// SPDX-License-Identifier: EUPL-1.2 -// SPDX-FileCopyrightText: 2021-present Detlef Stern //----------------------------------------------------------------------------- // Package impl provides services for authentification / authorization. package impl @@ -18,20 +15,20 @@ "errors" "hash/fnv" "io" "time" - "t73f.de/r/sx" - "t73f.de/r/zsc/api" - "t73f.de/r/zsc/sexp" + "github.com/pascaldekloe/jwt" + "zettelstore.de/z/auth" "zettelstore.de/z/auth/policy" "zettelstore.de/z/box" "zettelstore.de/z/config" + "zettelstore.de/z/domain/id" + "zettelstore.de/z/domain/meta" "zettelstore.de/z/kernel" - "zettelstore.de/z/zettel/id" - "zettelstore.de/z/zettel/meta" + "zettelstore.de/z/web/server" ) type myAuth struct { readonly bool owner id.Zid @@ -68,12 +65,14 @@ } // IsReadonly returns true, if the systems is configured to run in read-only-mode. func (a *myAuth) IsReadonly() bool { return a.readonly } -// ErrMalformedToken signals a broken token. -var ErrMalformedToken = errors.New("auth: malformed token") +const reqHash = jwt.HS512 + +// ErrNoUser signals that the meta data has no role value 'user'. +var ErrNoUser = errors.New("auth: meta is no user") // ErrNoIdent signals that the 'ident' key is missing. var ErrNoIdent = errors.New("auth: missing ident") // ErrOtherKind signals that the token was defined for another token kind. @@ -82,72 +81,77 @@ // ErrNoZid signals that the 'zid' key is missing. var ErrNoZid = errors.New("auth: missing zettel id") // GetToken returns a token to be used for authentification. func (a *myAuth) GetToken(ident *meta.Meta, d time.Duration, kind auth.TokenKind) ([]byte, error) { - subject, ok := ident.Get(api.KeyUserID) + if role, ok := ident.Get(meta.KeyRole); !ok || role != meta.ValueRoleUser { + return nil, ErrNoUser + } + subject, ok := ident.Get(meta.KeyUserID) if !ok || subject == "" { return nil, ErrNoIdent } now := time.Now().Round(time.Second) - sClaim := sx.MakeList( - sx.Int64(kind), - sx.MakeString(subject), - sx.Int64(now.Unix()), - sx.Int64(now.Add(d).Unix()), - sx.Int64(ident.Zid), - ) - return sign(sClaim, a.secret) + claims := jwt.Claims{ + Registered: jwt.Registered{ + Subject: subject, + Expires: jwt.NewNumericTime(now.Add(d)), + Issued: jwt.NewNumericTime(now), + }, + Set: map[string]interface{}{ + "zid": ident.Zid.String(), + "_tk": int(kind), + }, + } + token, err := claims.HMACSign(reqHash, a.secret) + if err != nil { + return nil, err + } + return token, nil } // ErrTokenExpired signals an exired token var ErrTokenExpired = errors.New("auth: token expired") // CheckToken checks the validity of the token and returns relevant data. -func (a *myAuth) CheckToken(tok []byte, k auth.TokenKind) (auth.TokenData, error) { - var tokenData auth.TokenData - - obj, err := check(tok, a.secret) - if err != nil { - return tokenData, err - } - - tokenData.Token = tok - err = setupTokenData(obj, k, &tokenData) - return tokenData, err -} - -func setupTokenData(obj sx.Object, k auth.TokenKind, tokenData *auth.TokenData) error { - vals, err := sexp.ParseList(obj, "isiii") - if err != nil { - return ErrMalformedToken - } - if auth.TokenKind(vals[0].(sx.Int64)) != k { - return ErrOtherKind - } - ident := vals[1].(sx.String).GetValue() +func (a *myAuth) CheckToken(token []byte, k auth.TokenKind) (auth.TokenData, error) { + h, err := jwt.NewHMAC(reqHash, a.secret) + if err != nil { + return auth.TokenData{}, err + } + claims, err := h.Check(token) + if err != nil { + return auth.TokenData{}, err + } + now := time.Now().Round(time.Second) + expires := claims.Expires.Time() + if expires.Before(now) { + return auth.TokenData{}, ErrTokenExpired + } + ident := claims.Subject if ident == "" { - return ErrNoIdent - } - issued := time.Unix(int64(vals[2].(sx.Int64)), 0) - expires := time.Unix(int64(vals[3].(sx.Int64)), 0) - now := time.Now().Round(time.Second) - if expires.Before(now) { - return ErrTokenExpired - } - zid := id.Zid(vals[4].(sx.Int64)) - if !zid.IsValid() { - return ErrNoZid - } - - tokenData.Ident = string(ident) - tokenData.Issued = issued - tokenData.Now = now - tokenData.Expires = expires - tokenData.Zid = zid - return nil + return auth.TokenData{}, ErrNoIdent + } + if zidS, ok := claims.Set["zid"].(string); ok { + if zid, err := id.Parse(zidS); err == nil { + if kind, ok := claims.Set["_tk"].(float64); ok { + if auth.TokenKind(kind) == k { + return auth.TokenData{ + Token: token, + Now: now, + Issued: claims.Issued.Time(), + Expires: expires, + Ident: ident, + Zid: zid, + }, nil + } + } + return auth.TokenData{}, ErrOtherKind + } + } + return auth.TokenData{}, ErrNoZid } func (a *myAuth) Owner() id.Zid { return a.owner } func (a *myAuth) IsOwner(zid id.Zid) bool { @@ -165,16 +169,16 @@ return meta.UserRoleOwner } if a.IsOwner(user.Zid) { return meta.UserRoleOwner } - if val, ok := user.Get(api.KeyUserRole); ok { + if val, ok := user.Get(meta.KeyUserRole); ok { if ur := meta.GetUserRole(val); ur != meta.UserRoleUnknown { return ur } } return meta.UserRoleReader } -func (a *myAuth) BoxWithPolicy(unprotectedBox box.Box, rtConfig config.Config) (box.Box, auth.Policy) { - return policy.BoxWithPolicy(a, unprotectedBox, rtConfig) +func (a *myAuth) BoxWithPolicy(auth server.Auth, unprotectedBox box.Box, rtConfig config.Config) (box.Box, auth.Policy) { + return policy.BoxWithPolicy(auth, a, unprotectedBox, rtConfig) } Index: auth/policy/anon.go ================================================================== --- auth/policy/anon.go +++ auth/policy/anon.go @@ -1,24 +1,22 @@ //----------------------------------------------------------------------------- -// Copyright (c) 2020-present Detlef Stern +// Copyright (c) 2020-2021 Detlef Stern // -// This file is part of Zettelstore. +// This file is part of zettelstore. // // Zettelstore is licensed under the latest version of the EUPL (European Union // Public License). Please see file LICENSE.txt for your rights and obligations // under this license. -// -// SPDX-License-Identifier: EUPL-1.2 -// SPDX-FileCopyrightText: 2020-present Detlef Stern //----------------------------------------------------------------------------- +// Package policy provides some interfaces and implementation for authorization policies. package policy import ( "zettelstore.de/z/auth" "zettelstore.de/z/config" - "zettelstore.de/z/zettel/meta" + "zettelstore.de/z/domain/meta" ) type anonPolicy struct { authConfig config.AuthConfig pre auth.Policy @@ -42,18 +40,11 @@ func (ap *anonPolicy) CanDelete(user, m *meta.Meta) bool { return ap.pre.CanDelete(user, m) && ap.checkVisibility(m) } -func (ap *anonPolicy) CanRefresh(user *meta.Meta) bool { - if ap.authConfig.GetExpertMode() || ap.authConfig.GetSimpleMode() { - return true - } - return ap.pre.CanRefresh(user) -} - func (ap *anonPolicy) checkVisibility(m *meta.Meta) bool { if ap.authConfig.GetVisibility(m) == meta.VisibilityExpert { return ap.authConfig.GetExpertMode() } return true } Index: auth/policy/box.go ================================================================== --- auth/policy/box.go +++ auth/policy/box.go @@ -1,48 +1,53 @@ //----------------------------------------------------------------------------- -// Copyright (c) 2020-present Detlef Stern +// Copyright (c) 2020-2021 Detlef Stern // -// This file is part of Zettelstore. +// This file is part of zettelstore. // // Zettelstore is licensed under the latest version of the EUPL (European Union // Public License). Please see file LICENSE.txt for your rights and obligations // under this license. -// -// SPDX-License-Identifier: EUPL-1.2 -// SPDX-FileCopyrightText: 2020-present Detlef Stern //----------------------------------------------------------------------------- +// Package policy provides some interfaces and implementation for authorizsation policies. package policy import ( "context" "zettelstore.de/z/auth" "zettelstore.de/z/box" "zettelstore.de/z/config" - "zettelstore.de/z/query" + "zettelstore.de/z/domain" + "zettelstore.de/z/domain/id" + "zettelstore.de/z/domain/meta" + "zettelstore.de/z/search" "zettelstore.de/z/web/server" - "zettelstore.de/z/zettel" - "zettelstore.de/z/zettel/id" - "zettelstore.de/z/zettel/meta" ) // BoxWithPolicy wraps the given box inside a policy box. -func BoxWithPolicy(manager auth.AuthzManager, box box.Box, authConfig config.AuthConfig) (box.Box, auth.Policy) { +func BoxWithPolicy( + auth server.Auth, + manager auth.AuthzManager, + box box.Box, + authConfig config.AuthConfig, +) (box.Box, auth.Policy) { pol := newPolicy(manager, authConfig) - return newBox(box, pol), pol + return newBox(auth, box, pol), pol } // polBox implements a policy box. type polBox struct { + auth server.Auth box box.Box policy auth.Policy } // newBox creates a new policy box. -func newBox(box box.Box, policy auth.Policy) box.Box { +func newBox(auth server.Auth, box box.Box, policy auth.Policy) box.Box { return &polBox{ + auth: auth, box: box, policy: policy, } } @@ -52,73 +57,77 @@ func (pp *polBox) CanCreateZettel(ctx context.Context) bool { return pp.box.CanCreateZettel(ctx) } -func (pp *polBox) CreateZettel(ctx context.Context, zettel zettel.Zettel) (id.Zid, error) { - user := server.GetUser(ctx) +func (pp *polBox) CreateZettel(ctx context.Context, zettel domain.Zettel) (id.Zid, error) { + user := pp.auth.GetUser(ctx) if pp.policy.CanCreate(user, zettel.Meta) { return pp.box.CreateZettel(ctx, zettel) } return id.Invalid, box.NewErrNotAllowed("Create", user, id.Invalid) } -func (pp *polBox) GetZettel(ctx context.Context, zid id.Zid) (zettel.Zettel, error) { - z, err := pp.box.GetZettel(ctx, zid) +func (pp *polBox) GetZettel(ctx context.Context, zid id.Zid) (domain.Zettel, error) { + zettel, err := pp.box.GetZettel(ctx, zid) if err != nil { - return zettel.Zettel{}, err - } - user := server.GetUser(ctx) - if pp.policy.CanRead(user, z.Meta) { - return z, nil - } - return zettel.Zettel{}, box.NewErrNotAllowed("GetZettel", user, zid) -} - -func (pp *polBox) GetAllZettel(ctx context.Context, zid id.Zid) ([]zettel.Zettel, error) { - return pp.box.GetAllZettel(ctx, zid) -} - -func (pp *polBox) FetchZids(ctx context.Context) (id.Set, error) { - return nil, box.NewErrNotAllowed("fetch-zids", server.GetUser(ctx), id.Invalid) + return domain.Zettel{}, err + } + user := pp.auth.GetUser(ctx) + if pp.policy.CanRead(user, zettel.Meta) { + return zettel, nil + } + return domain.Zettel{}, box.NewErrNotAllowed("GetZettel", user, zid) +} + +func (pp *polBox) GetAllZettel(ctx context.Context, zid id.Zid) ([]domain.Zettel, error) { + return pp.box.GetAllZettel(ctx, zid) } func (pp *polBox) GetMeta(ctx context.Context, zid id.Zid) (*meta.Meta, error) { m, err := pp.box.GetMeta(ctx, zid) if err != nil { return nil, err } - user := server.GetUser(ctx) + user := pp.auth.GetUser(ctx) if pp.policy.CanRead(user, m) { return m, nil } return nil, box.NewErrNotAllowed("GetMeta", user, zid) } -func (pp *polBox) SelectMeta(ctx context.Context, metaSeq []*meta.Meta, q *query.Query) ([]*meta.Meta, error) { - user := server.GetUser(ctx) +func (pp *polBox) GetAllMeta(ctx context.Context, zid id.Zid) ([]*meta.Meta, error) { + return pp.box.GetAllMeta(ctx, zid) +} + +func (pp *polBox) FetchZids(ctx context.Context) (id.Set, error) { + return nil, box.NewErrNotAllowed("fetch-zids", pp.auth.GetUser(ctx), id.Invalid) +} + +func (pp *polBox) SelectMeta(ctx context.Context, s *search.Search) ([]*meta.Meta, error) { + user := pp.auth.GetUser(ctx) canRead := pp.policy.CanRead - q = q.SetPreMatch(func(m *meta.Meta) bool { return canRead(user, m) }) - return pp.box.SelectMeta(ctx, metaSeq, q) + s = s.AddPreMatch(func(m *meta.Meta) bool { return canRead(user, m) }) + return pp.box.SelectMeta(ctx, s) } -func (pp *polBox) CanUpdateZettel(ctx context.Context, zettel zettel.Zettel) bool { +func (pp *polBox) CanUpdateZettel(ctx context.Context, zettel domain.Zettel) bool { return pp.box.CanUpdateZettel(ctx, zettel) } -func (pp *polBox) UpdateZettel(ctx context.Context, zettel zettel.Zettel) error { +func (pp *polBox) UpdateZettel(ctx context.Context, zettel domain.Zettel) error { zid := zettel.Meta.Zid - user := server.GetUser(ctx) + user := pp.auth.GetUser(ctx) if !zid.IsValid() { - return box.ErrInvalidZid{Zid: zid.String()} + return &box.ErrInvalidID{Zid: zid} } // Write existing zettel - oldZettel, err := pp.box.GetZettel(ctx, zid) + oldMeta, err := pp.box.GetMeta(ctx, zid) if err != nil { return err } - if pp.policy.CanWrite(user, oldZettel.Meta, zettel.Meta) { + if pp.policy.CanWrite(user, oldMeta, zettel.Meta) { return pp.box.UpdateZettel(ctx, zettel) } return box.NewErrNotAllowed("Write", user, zid) } @@ -125,16 +134,16 @@ func (pp *polBox) AllowRenameZettel(ctx context.Context, zid id.Zid) bool { return pp.box.AllowRenameZettel(ctx, zid) } func (pp *polBox) RenameZettel(ctx context.Context, curZid, newZid id.Zid) error { - z, err := pp.box.GetZettel(ctx, curZid) + meta, err := pp.box.GetMeta(ctx, curZid) if err != nil { return err } - user := server.GetUser(ctx) - if pp.policy.CanRename(user, z.Meta) { + user := pp.auth.GetUser(ctx) + if pp.policy.CanRename(user, meta) { return pp.box.RenameZettel(ctx, curZid, newZid) } return box.NewErrNotAllowed("Rename", user, curZid) } @@ -141,31 +150,15 @@ func (pp *polBox) CanDeleteZettel(ctx context.Context, zid id.Zid) bool { return pp.box.CanDeleteZettel(ctx, zid) } func (pp *polBox) DeleteZettel(ctx context.Context, zid id.Zid) error { - z, err := pp.box.GetZettel(ctx, zid) + meta, err := pp.box.GetMeta(ctx, zid) if err != nil { return err } - user := server.GetUser(ctx) - if pp.policy.CanDelete(user, z.Meta) { + user := pp.auth.GetUser(ctx) + if pp.policy.CanDelete(user, meta) { return pp.box.DeleteZettel(ctx, zid) } return box.NewErrNotAllowed("Delete", user, zid) } - -func (pp *polBox) Refresh(ctx context.Context) error { - user := server.GetUser(ctx) - if pp.policy.CanRefresh(user) { - return pp.box.Refresh(ctx) - } - return box.NewErrNotAllowed("Refresh", user, id.Invalid) -} -func (pp *polBox) ReIndex(ctx context.Context, zid id.Zid) error { - user := server.GetUser(ctx) - if pp.policy.CanRefresh(user) { - // If a user is allowed to refresh all data, it it also allowed to re-index a zettel. - return pp.box.ReIndex(ctx, zid) - } - return box.NewErrNotAllowed("ReIndex", user, zid) -} Index: auth/policy/default.go ================================================================== --- auth/policy/default.go +++ auth/policy/default.go @@ -1,60 +1,55 @@ //----------------------------------------------------------------------------- -// Copyright (c) 2020-present Detlef Stern +// Copyright (c) 2020-2021 Detlef Stern // -// This file is part of Zettelstore. +// This file is part of zettelstore. // // Zettelstore is licensed under the latest version of the EUPL (European Union // Public License). Please see file LICENSE.txt for your rights and obligations // under this license. -// -// SPDX-License-Identifier: EUPL-1.2 -// SPDX-FileCopyrightText: 2020-present Detlef Stern //----------------------------------------------------------------------------- +// Package policy provides some interfaces and implementation for authorizsation policies. package policy import ( - "t73f.de/r/zsc/api" "zettelstore.de/z/auth" - "zettelstore.de/z/zettel/meta" + "zettelstore.de/z/domain/meta" ) type defaultPolicy struct { manager auth.AuthzManager } -func (*defaultPolicy) CanCreate(_, _ *meta.Meta) bool { return true } -func (*defaultPolicy) CanRead(_, _ *meta.Meta) bool { return true } -func (d *defaultPolicy) CanWrite(user, oldMeta, _ *meta.Meta) bool { +func (d *defaultPolicy) CanCreate(user, newMeta *meta.Meta) bool { return true } +func (d *defaultPolicy) CanRead(user, m *meta.Meta) bool { return true } +func (d *defaultPolicy) CanWrite(user, oldMeta, newMeta *meta.Meta) bool { return d.canChange(user, oldMeta) } func (d *defaultPolicy) CanRename(user, m *meta.Meta) bool { return d.canChange(user, m) } func (d *defaultPolicy) CanDelete(user, m *meta.Meta) bool { return d.canChange(user, m) } -func (*defaultPolicy) CanRefresh(user *meta.Meta) bool { return user != nil } - func (d *defaultPolicy) canChange(user, m *meta.Meta) bool { - metaRo, ok := m.Get(api.KeyReadOnly) + metaRo, ok := m.Get(meta.KeyReadOnly) if !ok { return true } if user == nil { // If we are here, there is no authentication. // See owner.go:CanWrite. // No authentication: check for owner-like restriction, because the user // acts as an owner - return metaRo != api.ValueUserRoleOwner && !meta.BoolValue(metaRo) + return metaRo != meta.ValueUserRoleOwner && !meta.BoolValue(metaRo) } userRole := d.manager.GetUserRole(user) switch metaRo { - case api.ValueUserRoleReader: + case meta.ValueUserRoleReader: return userRole > meta.UserRoleReader - case api.ValueUserRoleWriter: + case meta.ValueUserRoleWriter: return userRole > meta.UserRoleWriter - case api.ValueUserRoleOwner: + case meta.ValueUserRoleOwner: return userRole > meta.UserRoleOwner } return !meta.BoolValue(metaRo) } Index: auth/policy/owner.go ================================================================== --- auth/policy/owner.go +++ auth/policy/owner.go @@ -1,25 +1,22 @@ //----------------------------------------------------------------------------- -// Copyright (c) 2020-present Detlef Stern +// Copyright (c) 2020-2021 Detlef Stern // -// This file is part of Zettelstore. +// This file is part of zettelstore. // // Zettelstore is licensed under the latest version of the EUPL (European Union // Public License). Please see file LICENSE.txt for your rights and obligations // under this license. -// -// SPDX-License-Identifier: EUPL-1.2 -// SPDX-FileCopyrightText: 2020-present Detlef Stern //----------------------------------------------------------------------------- +// Package policy provides some interfaces and implementation for authorizsation policies. package policy import ( - "t73f.de/r/zsc/api" "zettelstore.de/z/auth" "zettelstore.de/z/config" - "zettelstore.de/z/zettel/meta" + "zettelstore.de/z/domain/meta" ) type ownerPolicy struct { manager auth.AuthzManager authConfig config.AuthConfig @@ -35,11 +32,11 @@ func (o *ownerPolicy) userCanCreate(user, newMeta *meta.Meta) bool { if o.manager.GetUserRole(user) == meta.UserRoleReader { return false } - if _, ok := newMeta.Get(api.KeyUserID); ok { + if role, ok := newMeta.Get(meta.KeyRole); ok && role == meta.ValueRoleUser { return false } return true } @@ -61,11 +58,11 @@ return true } if user == nil { return false } - if _, ok := m.Get(api.KeyUserID); ok { + if role, ok := m.Get(meta.KeyRole); ok && role == meta.ValueRoleUser { // Only the user can read its own zettel return user.Zid == m.Zid } switch o.manager.GetUserRole(user) { case meta.UserRoleReader, meta.UserRoleWriter, meta.UserRoleOwner: @@ -76,14 +73,14 @@ return false } } var noChangeUser = []string{ - api.KeyID, - api.KeyRole, - api.KeyUserID, - api.KeyUserRole, + meta.KeyID, + meta.KeyRole, + meta.KeyUserID, + meta.KeyUserRole, } func (o *ownerPolicy) CanWrite(user, oldMeta, newMeta *meta.Meta) bool { if user == nil || !o.pre.CanWrite(user, oldMeta, newMeta) { return false @@ -96,11 +93,11 @@ return true } if !o.userCanRead(user, oldMeta, vis) { return false } - if _, ok := oldMeta.Get(api.KeyUserID); ok { + if role, ok := oldMeta.Get(meta.KeyRole); ok && role == meta.ValueRoleUser { // Here we know, that user.Zid == newMeta.Zid (because of userCanRead) and // user.Zid == newMeta.Zid (because oldMeta.Zid == newMeta.Zid) for _, key := range noChangeUser { if oldMeta.GetDefault(key, "") != newMeta.GetDefault(key, "") { return false @@ -133,20 +130,10 @@ return res } return o.userIsOwner(user) } -func (o *ownerPolicy) CanRefresh(user *meta.Meta) bool { - switch userRole := o.manager.GetUserRole(user); userRole { - case meta.UserRoleUnknown: - return o.authConfig.GetSimpleMode() - case meta.UserRoleCreator: - return o.authConfig.GetExpertMode() || o.authConfig.GetSimpleMode() - } - return true -} - func (o *ownerPolicy) checkVisibility(user *meta.Meta, vis meta.Visibility) (bool, bool) { if vis == meta.VisibilityExpert { return o.userIsOwner(user) && o.authConfig.GetExpertMode(), true } return false, false @@ -157,10 +144,10 @@ return false } if o.manager.IsOwner(user.Zid) { return true } - if val, ok := user.Get(api.KeyUserRole); ok && val == api.ValueUserRoleOwner { + if val, ok := user.Get(meta.KeyUserRole); ok && val == meta.ValueUserRoleOwner { return true } return false } Index: auth/policy/policy.go ================================================================== --- auth/policy/policy.go +++ auth/policy/policy.go @@ -1,25 +1,22 @@ //----------------------------------------------------------------------------- -// Copyright (c) 2020-present Detlef Stern +// Copyright (c) 2020-2021 Detlef Stern // -// This file is part of Zettelstore. +// This file is part of zettelstore. // // Zettelstore is licensed under the latest version of the EUPL (European Union // Public License). Please see file LICENSE.txt for your rights and obligations // under this license. -// -// SPDX-License-Identifier: EUPL-1.2 -// SPDX-FileCopyrightText: 2020-present Detlef Stern //----------------------------------------------------------------------------- // Package policy provides some interfaces and implementation for authorizsation policies. package policy import ( "zettelstore.de/z/auth" "zettelstore.de/z/config" - "zettelstore.de/z/zettel/meta" + "zettelstore.de/z/domain/meta" ) // newPolicy creates a policy based on given constraints. func newPolicy(manager auth.AuthzManager, authConfig config.AuthConfig) auth.Policy { var pol auth.Policy @@ -65,9 +62,5 @@ } func (p *prePolicy) CanDelete(user, m *meta.Meta) bool { return m != nil && p.post.CanDelete(user, m) } - -func (p *prePolicy) CanRefresh(user *meta.Meta) bool { - return p.post.CanRefresh(user) -} Index: auth/policy/policy_test.go ================================================================== --- auth/policy/policy_test.go +++ auth/policy/policy_test.go @@ -1,81 +1,69 @@ //----------------------------------------------------------------------------- -// Copyright (c) 2020-present Detlef Stern +// Copyright (c) 2020-2021 Detlef Stern // -// This file is part of Zettelstore. +// This file is part of zettelstore. // // Zettelstore is licensed under the latest version of the EUPL (European Union // Public License). Please see file LICENSE.txt for your rights and obligations // under this license. -// -// SPDX-License-Identifier: EUPL-1.2 -// SPDX-FileCopyrightText: 2020-present Detlef Stern //----------------------------------------------------------------------------- +// Package policy provides some interfaces and implementation for authorizsation policies. package policy import ( "fmt" "testing" - "t73f.de/r/zsc/api" "zettelstore.de/z/auth" - "zettelstore.de/z/zettel/id" - "zettelstore.de/z/zettel/meta" + "zettelstore.de/z/domain/id" + "zettelstore.de/z/domain/meta" ) func TestPolicies(t *testing.T) { t.Parallel() testScene := []struct { readonly bool withAuth bool expert bool - simple bool }{ - {true, true, true, true}, - {true, true, true, false}, - {true, true, false, true}, - {true, true, false, false}, - {true, false, true, true}, - {true, false, true, false}, - {true, false, false, true}, - {true, false, false, false}, - {false, true, true, true}, - {false, true, true, false}, - {false, true, false, true}, - {false, true, false, false}, - {false, false, true, true}, - {false, false, true, false}, - {false, false, false, true}, - {false, false, false, false}, + {true, true, true}, + {true, true, false}, + {true, false, true}, + {true, false, false}, + {false, true, true}, + {false, true, false}, + {false, false, true}, + {false, false, false}, } for _, ts := range testScene { - pol := newPolicy( - &testAuthzManager{readOnly: ts.readonly, withAuth: ts.withAuth}, - &authConfig{simple: ts.simple, expert: ts.expert}, - ) - name := fmt.Sprintf("readonly=%v/withauth=%v/expert=%v/simple=%v", - ts.readonly, ts.withAuth, ts.expert, ts.simple) + authzManager := &testAuthzManager{ + readOnly: ts.readonly, + withAuth: ts.withAuth, + } + pol := newPolicy(authzManager, &authConfig{ts.expert}) + name := fmt.Sprintf("readonly=%v/withauth=%v/expert=%v", + ts.readonly, ts.withAuth, ts.expert) t.Run(name, func(tt *testing.T) { - testCreate(tt, pol, ts.withAuth, ts.readonly) - testRead(tt, pol, ts.withAuth, ts.expert) + testCreate(tt, pol, ts.withAuth, ts.readonly, ts.expert) + testRead(tt, pol, ts.withAuth, ts.readonly, ts.expert) testWrite(tt, pol, ts.withAuth, ts.readonly, ts.expert) testRename(tt, pol, ts.withAuth, ts.readonly, ts.expert) testDelete(tt, pol, ts.withAuth, ts.readonly, ts.expert) - testRefresh(tt, pol, ts.withAuth, ts.expert, ts.simple) }) } } type testAuthzManager struct { readOnly bool withAuth bool } -func (a *testAuthzManager) IsReadonly() bool { return a.readOnly } -func (*testAuthzManager) Owner() id.Zid { return ownerZid } -func (*testAuthzManager) IsOwner(zid id.Zid) bool { return zid == ownerZid } +func (a *testAuthzManager) IsReadonly() bool { return a.readOnly } +func (a *testAuthzManager) Owner() id.Zid { return ownerZid } +func (a *testAuthzManager) IsOwner(zid id.Zid) bool { return zid == ownerZid } func (a *testAuthzManager) WithAuth() bool { return a.withAuth } func (a *testAuthzManager) GetUserRole(user *meta.Meta) meta.UserRole { if user == nil { @@ -85,31 +73,30 @@ return meta.UserRoleOwner } if a.IsOwner(user.Zid) { return meta.UserRoleOwner } - if val, ok := user.Get(api.KeyUserRole); ok { + if val, ok := user.Get(meta.KeyUserRole); ok { if ur := meta.GetUserRole(val); ur != meta.UserRoleUnknown { return ur } } return meta.UserRoleReader } -type authConfig struct{ simple, expert bool } +type authConfig struct{ expert bool } -func (ac *authConfig) GetSimpleMode() bool { return ac.simple } func (ac *authConfig) GetExpertMode() bool { return ac.expert } -func (*authConfig) GetVisibility(m *meta.Meta) meta.Visibility { - if vis, ok := m.Get(api.KeyVisibility); ok { +func (ac *authConfig) GetVisibility(m *meta.Meta) meta.Visibility { + if vis, ok := m.Get(meta.KeyVisibility); ok { return meta.GetVisibility(vis) } return meta.VisibilityLogin } -func testCreate(t *testing.T, pol auth.Policy, withAuth, readonly bool) { +func testCreate(t *testing.T, pol auth.Policy, withAuth, readonly, isExpert bool) { t.Helper() anonUser := newAnon() creator := newCreator() reader := newReader() writer := newWriter() @@ -152,11 +139,11 @@ } }) } } -func testRead(t *testing.T, pol auth.Policy, withAuth, expert bool) { +func testRead(t *testing.T, pol auth.Policy, withAuth, readonly, expert bool) { t.Helper() anonUser := newAnon() creator := newCreator() reader := newReader() writer := newWriter() @@ -262,11 +249,11 @@ loginZettel := newLoginZettel() ownerZettel := newOwnerZettel() expertZettel := newExpertZettel() userZettel := newUserZettel() writerNew := writer.Clone() - writerNew.Set(api.KeyUserRole, owner.GetDefault(api.KeyUserRole, "")) + writerNew.Set(meta.KeyUserRole, owner.GetDefault(meta.KeyUserRole, "")) roFalse := newRoFalseZettel() roTrue := newRoTrueZettel() roReader := newRoReaderZettel() roWriter := newRoWriterZettel() roOwner := newRoOwnerZettel() @@ -575,33 +562,10 @@ } }) } } -func testRefresh(t *testing.T, pol auth.Policy, withAuth, expert, simple bool) { - t.Helper() - testCases := []struct { - user *meta.Meta - exp bool - }{ - {newAnon(), (!withAuth && expert) || simple}, - {newCreator(), !withAuth || expert || simple}, - {newReader(), true}, - {newWriter(), true}, - {newOwner(), true}, - {newOwner2(), true}, - } - for _, tc := range testCases { - t.Run("Refresh", func(tt *testing.T) { - got := pol.CanRefresh(tc.user) - if tc.exp != got { - tt.Errorf("exp=%v, but got=%v", tc.exp, got) - } - }) - } -} - const ( creatorZid = id.Zid(1013) readerZid = id.Zid(1013) writerZid = id.Zid(1015) ownerZid = id.Zid(1017) @@ -612,109 +576,109 @@ ) func newAnon() *meta.Meta { return nil } func newCreator() *meta.Meta { user := meta.New(creatorZid) - user.Set(api.KeyTitle, "Creator") - user.Set(api.KeyUserID, "ceator") - user.Set(api.KeyUserRole, api.ValueUserRoleCreator) + user.Set(meta.KeyTitle, "Creator") + user.Set(meta.KeyRole, meta.ValueRoleUser) + user.Set(meta.KeyUserRole, meta.ValueUserRoleCreator) return user } func newReader() *meta.Meta { user := meta.New(readerZid) - user.Set(api.KeyTitle, "Reader") - user.Set(api.KeyUserID, "reader") - user.Set(api.KeyUserRole, api.ValueUserRoleReader) + user.Set(meta.KeyTitle, "Reader") + user.Set(meta.KeyRole, meta.ValueRoleUser) + user.Set(meta.KeyUserRole, meta.ValueUserRoleReader) return user } func newWriter() *meta.Meta { user := meta.New(writerZid) - user.Set(api.KeyTitle, "Writer") - user.Set(api.KeyUserID, "writer") - user.Set(api.KeyUserRole, api.ValueUserRoleWriter) + user.Set(meta.KeyTitle, "Writer") + user.Set(meta.KeyRole, meta.ValueRoleUser) + user.Set(meta.KeyUserRole, meta.ValueUserRoleWriter) return user } func newOwner() *meta.Meta { user := meta.New(ownerZid) - user.Set(api.KeyTitle, "Owner") - user.Set(api.KeyUserID, "owner") - user.Set(api.KeyUserRole, api.ValueUserRoleOwner) + user.Set(meta.KeyTitle, "Owner") + user.Set(meta.KeyRole, meta.ValueRoleUser) + user.Set(meta.KeyUserRole, meta.ValueUserRoleOwner) return user } func newOwner2() *meta.Meta { user := meta.New(owner2Zid) - user.Set(api.KeyTitle, "Owner 2") - user.Set(api.KeyUserID, "owner-2") - user.Set(api.KeyUserRole, api.ValueUserRoleOwner) + user.Set(meta.KeyTitle, "Owner 2") + user.Set(meta.KeyRole, meta.ValueRoleUser) + user.Set(meta.KeyUserRole, meta.ValueUserRoleOwner) return user } func newZettel() *meta.Meta { m := meta.New(zettelZid) - m.Set(api.KeyTitle, "Any Zettel") + m.Set(meta.KeyTitle, "Any Zettel") return m } func newPublicZettel() *meta.Meta { m := meta.New(visZid) - m.Set(api.KeyTitle, "Public Zettel") - m.Set(api.KeyVisibility, api.ValueVisibilityPublic) + m.Set(meta.KeyTitle, "Public Zettel") + m.Set(meta.KeyVisibility, meta.ValueVisibilityPublic) return m } func newCreatorZettel() *meta.Meta { m := meta.New(visZid) - m.Set(api.KeyTitle, "Creator Zettel") - m.Set(api.KeyVisibility, api.ValueVisibilityCreator) + m.Set(meta.KeyTitle, "Creator Zettel") + m.Set(meta.KeyVisibility, meta.ValueVisibilityCreator) return m } func newLoginZettel() *meta.Meta { m := meta.New(visZid) - m.Set(api.KeyTitle, "Login Zettel") - m.Set(api.KeyVisibility, api.ValueVisibilityLogin) + m.Set(meta.KeyTitle, "Login Zettel") + m.Set(meta.KeyVisibility, meta.ValueVisibilityLogin) return m } func newOwnerZettel() *meta.Meta { m := meta.New(visZid) - m.Set(api.KeyTitle, "Owner Zettel") - m.Set(api.KeyVisibility, api.ValueVisibilityOwner) + m.Set(meta.KeyTitle, "Owner Zettel") + m.Set(meta.KeyVisibility, meta.ValueVisibilityOwner) return m } func newExpertZettel() *meta.Meta { m := meta.New(visZid) - m.Set(api.KeyTitle, "Expert Zettel") - m.Set(api.KeyVisibility, api.ValueVisibilityExpert) + m.Set(meta.KeyTitle, "Expert Zettel") + m.Set(meta.KeyVisibility, meta.ValueVisibilityExpert) return m } func newRoFalseZettel() *meta.Meta { m := meta.New(zettelZid) - m.Set(api.KeyTitle, "No r/o Zettel") - m.Set(api.KeyReadOnly, api.ValueFalse) + m.Set(meta.KeyTitle, "No r/o Zettel") + m.Set(meta.KeyReadOnly, "false") return m } func newRoTrueZettel() *meta.Meta { m := meta.New(zettelZid) - m.Set(api.KeyTitle, "A r/o Zettel") - m.Set(api.KeyReadOnly, api.ValueTrue) + m.Set(meta.KeyTitle, "A r/o Zettel") + m.Set(meta.KeyReadOnly, "true") return m } func newRoReaderZettel() *meta.Meta { m := meta.New(zettelZid) - m.Set(api.KeyTitle, "Reader r/o Zettel") - m.Set(api.KeyReadOnly, api.ValueUserRoleReader) + m.Set(meta.KeyTitle, "Reader r/o Zettel") + m.Set(meta.KeyReadOnly, meta.ValueUserRoleReader) return m } func newRoWriterZettel() *meta.Meta { m := meta.New(zettelZid) - m.Set(api.KeyTitle, "Writer r/o Zettel") - m.Set(api.KeyReadOnly, api.ValueUserRoleWriter) + m.Set(meta.KeyTitle, "Writer r/o Zettel") + m.Set(meta.KeyReadOnly, meta.ValueUserRoleWriter) return m } func newRoOwnerZettel() *meta.Meta { m := meta.New(zettelZid) - m.Set(api.KeyTitle, "Owner r/o Zettel") - m.Set(api.KeyReadOnly, api.ValueUserRoleOwner) + m.Set(meta.KeyTitle, "Owner r/o Zettel") + m.Set(meta.KeyReadOnly, meta.ValueUserRoleOwner) return m } func newUserZettel() *meta.Meta { m := meta.New(userZid) - m.Set(api.KeyTitle, "Any User") - m.Set(api.KeyUserID, "any") + m.Set(meta.KeyTitle, "Any User") + m.Set(meta.KeyRole, meta.ValueRoleUser) return m } Index: auth/policy/readonly.go ================================================================== --- auth/policy/readonly.go +++ auth/policy/readonly.go @@ -1,25 +1,22 @@ //----------------------------------------------------------------------------- -// Copyright (c) 2020-present Detlef Stern +// Copyright (c) 2020-2021 Detlef Stern // -// This file is part of Zettelstore. +// This file is part of zettelstore. // // Zettelstore is licensed under the latest version of the EUPL (European Union // Public License). Please see file LICENSE.txt for your rights and obligations // under this license. -// -// SPDX-License-Identifier: EUPL-1.2 -// SPDX-FileCopyrightText: 2020-present Detlef Stern //----------------------------------------------------------------------------- +// Package policy provides some interfaces and implementation for authorization policies. package policy -import "zettelstore.de/z/zettel/meta" +import "zettelstore.de/z/domain/meta" type roPolicy struct{} -func (*roPolicy) CanCreate(_, _ *meta.Meta) bool { return false } -func (*roPolicy) CanRead(_, _ *meta.Meta) bool { return true } -func (*roPolicy) CanWrite(_, _, _ *meta.Meta) bool { return false } -func (*roPolicy) CanRename(_, _ *meta.Meta) bool { return false } -func (*roPolicy) CanDelete(_, _ *meta.Meta) bool { return false } -func (*roPolicy) CanRefresh(user *meta.Meta) bool { return user != nil } +func (p *roPolicy) CanCreate(user, newMeta *meta.Meta) bool { return false } +func (p *roPolicy) CanRead(user, m *meta.Meta) bool { return true } +func (p *roPolicy) CanWrite(user, oldMeta, newMeta *meta.Meta) bool { return false } +func (p *roPolicy) CanRename(user, m *meta.Meta) bool { return false } +func (p *roPolicy) CanDelete(user, m *meta.Meta) bool { return false } Index: box/box.go ================================================================== --- box/box.go +++ box/box.go @@ -1,16 +1,13 @@ //----------------------------------------------------------------------------- -// Copyright (c) 2020-present Detlef Stern +// Copyright (c) 2020-2021 Detlef Stern // -// This file is part of Zettelstore. +// This file is part of zettelstore. // // Zettelstore is licensed under the latest version of the EUPL (European Union // Public License). Please see file LICENSE.txt for your rights and obligations // under this license. -// -// SPDX-License-Identifier: EUPL-1.2 -// SPDX-FileCopyrightText: 2020-present Detlef Stern //----------------------------------------------------------------------------- // Package box provides a generic interface to zettel boxes. package box @@ -19,25 +16,43 @@ "errors" "fmt" "io" "time" - "t73f.de/r/zsc/api" - "zettelstore.de/z/query" - "zettelstore.de/z/zettel" - "zettelstore.de/z/zettel/id" - "zettelstore.de/z/zettel/meta" + "zettelstore.de/z/domain" + "zettelstore.de/z/domain/id" + "zettelstore.de/z/domain/meta" + "zettelstore.de/z/search" ) // BaseBox is implemented by all Zettel boxes. type BaseBox interface { // Location returns some information where the box is located. // Format is dependent of the box. Location() string + // CanCreateZettel returns true, if box could possibly create a new zettel. + CanCreateZettel(ctx context.Context) bool + + // CreateZettel creates a new zettel. + // Returns the new zettel id (and an error indication). + CreateZettel(ctx context.Context, zettel domain.Zettel) (id.Zid, error) + // GetZettel retrieves a specific zettel. - GetZettel(ctx context.Context, zid id.Zid) (zettel.Zettel, error) + GetZettel(ctx context.Context, zid id.Zid) (domain.Zettel, error) + + // GetMeta retrieves just the meta data of a specific zettel. + GetMeta(ctx context.Context, zid id.Zid) (*meta.Meta, error) + + // FetchZids returns the set of all zettel identifer managed by the box. + FetchZids(ctx context.Context) (id.Set, error) + + // CanUpdateZettel returns true, if box could possibly update the given zettel. + CanUpdateZettel(ctx context.Context, zettel domain.Zettel) bool + + // UpdateZettel updates an existing zettel. + UpdateZettel(ctx context.Context, zettel domain.Zettel) error // AllowRenameZettel returns true, if box will not disallow renaming the zettel. AllowRenameZettel(ctx context.Context, zid id.Zid) bool // RenameZettel changes the current Zid to a new Zid. @@ -48,44 +63,16 @@ // DeleteZettel removes the zettel from the box. DeleteZettel(ctx context.Context, zid id.Zid) error } -// WriteBox is a box that can create / update zettel content. -type WriteBox interface { - // CanCreateZettel returns true, if box could possibly create a new zettel. - CanCreateZettel(ctx context.Context) bool - - // CreateZettel creates a new zettel. - // Returns the new zettel id (and an error indication). - CreateZettel(ctx context.Context, zettel zettel.Zettel) (id.Zid, error) - - // CanUpdateZettel returns true, if box could possibly update the given zettel. - CanUpdateZettel(ctx context.Context, zettel zettel.Zettel) bool - - // UpdateZettel updates an existing zettel. - UpdateZettel(ctx context.Context, zettel zettel.Zettel) error -} - -// ZidFunc is a function that processes identifier of a zettel. -type ZidFunc func(id.Zid) - -// MetaFunc is a function that processes metadata of a zettel. -type MetaFunc func(*meta.Meta) - // ManagedBox is the interface of managed boxes. type ManagedBox interface { BaseBox - // HasZettel returns true, if box conains zettel with given identifier. - HasZettel(context.Context, id.Zid) bool - - // Apply identifier of every zettel to the given function, if predicate returns true. - ApplyZid(context.Context, ZidFunc, query.RetrievePredicate) error - - // Apply metadata of every zettel to the given function, if predicate returns true. - ApplyMeta(context.Context, MetaFunc, query.RetrievePredicate) error + // SelectMeta returns all zettel meta data that match the selection criteria. + SelectMeta(ctx context.Context, match search.MetaMatchFunc) ([]*meta.Meta, error) // ReadStats populates st with box statistics ReadStats(st *ManagedBoxStats) } @@ -96,67 +83,32 @@ // Zettel is the number of zettel managed by the box. Zettel int } -// StartState enumerates the possible states of starting and stopping a box. -// -// StartStateStopped -> StartStateStarting -> StartStateStarted -> StateStateStopping -> StartStateStopped. -// -// Other transitions are also possible. -type StartState uint8 - -// Constant values of StartState -const ( - StartStateStopped StartState = iota - StartStateStarting - StartStateStarted - StartStateStopping -) - // StartStopper performs simple lifecycle management. type StartStopper interface { - // State the current status of the box. - State() StartState - // Start the box. Now all other functions of the box are allowed. - // Starting a box, which is not in state StartStateStopped is not allowed. + // Starting an already started box is not allowed. Start(ctx context.Context) error // Stop the started box. Now only the Start() function is allowed. - Stop(ctx context.Context) -} - -// Refresher allow to refresh their internal data. -type Refresher interface { - // Refresh the box data. - Refresh(context.Context) + Stop(ctx context.Context) error } // Box is to be used outside the box package and its descendants. type Box interface { BaseBox - WriteBox - - // FetchZids returns the set of all zettel identifer managed by the box. - FetchZids(ctx context.Context) (id.Set, error) - - // GetMeta returns the metadata of the zettel with the given identifier. - GetMeta(context.Context, id.Zid) (*meta.Meta, error) // SelectMeta returns a list of metadata that comply to the given selection criteria. - // If `metaSeq` is `nil`, the box assumes metadata of all available zettel. - SelectMeta(ctx context.Context, metaSeq []*meta.Meta, q *query.Query) ([]*meta.Meta, error) + SelectMeta(ctx context.Context, s *search.Search) ([]*meta.Meta, error) // GetAllZettel retrieves a specific zettel from all managed boxes. - GetAllZettel(ctx context.Context, zid id.Zid) ([]zettel.Zettel, error) + GetAllZettel(ctx context.Context, zid id.Zid) ([]domain.Zettel, error) - // Refresh the data from the box and from its managed sub-boxes. - Refresh(context.Context) error - - // ReIndex one zettel to update its index data. - ReIndex(context.Context, id.Zid) error + // GetAllMeta retrieves the meta data of a specific zettel from all managed boxes. + GetAllMeta(ctx context.Context, zid id.Zid) ([]*meta.Meta, error) } // Stats record stattistics about a box. type Stats struct { // ReadOnly indicates that boxes cannot be modified. @@ -208,18 +160,18 @@ type UpdateReason uint8 // Values for Reason const ( _ UpdateReason = iota - OnReady // Box is started and fully operational OnReload // Box was reloaded - OnZettel // Something with a zettel happened + OnUpdate // A zettel was created or changed + OnDelete // A zettel was removed ) // UpdateInfo contains all the data about a changed zettel. type UpdateInfo struct { - Box BaseBox + Box Box Reason UpdateReason Zid id.Zid } // UpdateFunc is a function to be called when a change is detected. @@ -254,18 +206,10 @@ func DoNotEnrich(ctx context.Context) bool { _, ok := ctx.Value(ctxNoEnrichKey).(*ctxNoEnrichType) return ok } -// NoEnrichQuery provides a context that signals not to enrich, if the query does not need this. -func NoEnrichQuery(ctx context.Context, q *query.Query) context.Context { - if q.EnrichNeeded() { - return ctx - } - return NoEnrichContext(ctx) -} - // ErrNotAllowed is returned if the caller is not allowed to perform the operation. type ErrNotAllowed struct { Op string User *meta.Meta Zid id.Zid @@ -283,26 +227,32 @@ func (err *ErrNotAllowed) Error() string { if err.User == nil { if err.Zid.IsValid() { return fmt.Sprintf( "operation %q on zettel %v not allowed for not authorized user", - err.Op, err.Zid) + err.Op, + err.Zid.String()) } return fmt.Sprintf("operation %q not allowed for not authorized user", err.Op) } if err.Zid.IsValid() { return fmt.Sprintf( "operation %q on zettel %v not allowed for user %v/%v", - err.Op, err.Zid, err.User.GetDefault(api.KeyUserID, "?"), err.User.Zid) + err.Op, + err.Zid.String(), + err.User.GetDefault(meta.KeyUserID, "?"), + err.User.Zid.String()) } return fmt.Sprintf( "operation %q not allowed for user %v/%v", - err.Op, err.User.GetDefault(api.KeyUserID, "?"), err.User.Zid) + err.Op, + err.User.GetDefault(meta.KeyUserID, "?"), + err.User.Zid.String()) } // Is return true, if the error is of type ErrNotAllowed. -func (*ErrNotAllowed) Is(error) bool { return true } +func (err *ErrNotAllowed) Is(target error) bool { return true } // ErrStarted is returned when trying to start an already started box. var ErrStarted = errors.New("box is already started") // ErrStopped is returned if calling methods on a box that was not started. @@ -309,21 +259,16 @@ var ErrStopped = errors.New("box is stopped") // ErrReadOnly is returned if there is an attepmt to write to a read-only box. var ErrReadOnly = errors.New("read-only box") -// ErrZettelNotFound is returned if a zettel was not found in the box. -type ErrZettelNotFound struct{ Zid id.Zid } - -func (eznf ErrZettelNotFound) Error() string { return "zettel not found: " + eznf.Zid.String() } +// ErrNotFound is returned if a zettel was not found in the box. +var ErrNotFound = errors.New("zettel not found") // ErrConflict is returned if a box operation detected a conflict.. // One example: if calculating a new zettel identifier takes too long. var ErrConflict = errors.New("conflict") -// ErrCapacity is returned if a box has reached its capacity. -var ErrCapacity = errors.New("capacity exceeded") +// ErrInvalidID is returned if the zettel id is not appropriate for the box operation. +type ErrInvalidID struct{ Zid id.Zid } -// ErrInvalidZid is returned if the zettel id is not appropriate for the box operation. -type ErrInvalidZid struct{ Zid string } - -func (err ErrInvalidZid) Error() string { return "invalid Zettel id: " + err.Zid } +func (err *ErrInvalidID) Error() string { return "invalid Zettel id: " + err.Zid.String() } Index: box/compbox/compbox.go ================================================================== --- box/compbox/compbox.go +++ box/compbox/compbox.go @@ -1,34 +1,28 @@ //----------------------------------------------------------------------------- -// Copyright (c) 2020-present Detlef Stern +// Copyright (c) 2020-2021 Detlef Stern // -// This file is part of Zettelstore. +// This file is part of zettelstore. // // Zettelstore is licensed under the latest version of the EUPL (European Union // Public License). Please see file LICENSE.txt for your rights and obligations // under this license. -// -// SPDX-License-Identifier: EUPL-1.2 -// SPDX-FileCopyrightText: 2020-present Detlef Stern //----------------------------------------------------------------------------- // Package compbox provides zettel that have computed content. package compbox import ( "context" "net/url" - "t73f.de/r/zsc/api" "zettelstore.de/z/box" "zettelstore.de/z/box/manager" - "zettelstore.de/z/kernel" - "zettelstore.de/z/logger" - "zettelstore.de/z/query" - "zettelstore.de/z/zettel" - "zettelstore.de/z/zettel/id" - "zettelstore.de/z/zettel/meta" + "zettelstore.de/z/domain" + "zettelstore.de/z/domain/id" + "zettelstore.de/z/domain/meta" + "zettelstore.de/z/search" ) func init() { manager.Register( " comp", @@ -36,142 +30,138 @@ return getCompBox(cdata.Number, cdata.Enricher), nil }) } type compBox struct { - log *logger.Logger number int enricher box.Enricher } var myConfig *meta.Meta var myZettel = map[id.Zid]struct { meta func(id.Zid) *meta.Meta - content func(*meta.Meta) []byte + content func(*meta.Meta) string }{ - id.MustParse(api.ZidVersion): {genVersionBuildM, genVersionBuildC}, - id.MustParse(api.ZidHost): {genVersionHostM, genVersionHostC}, - id.MustParse(api.ZidOperatingSystem): {genVersionOSM, genVersionOSC}, - id.MustParse(api.ZidLog): {genLogM, genLogC}, - id.MustParse(api.ZidMemory): {genMemoryM, genMemoryC}, - id.MustParse(api.ZidBoxManager): {genManagerM, genManagerC}, - id.MustParse(api.ZidMetadataKey): {genKeysM, genKeysC}, - id.MustParse(api.ZidParser): {genParserM, genParserC}, - id.MustParse(api.ZidStartupConfiguration): {genConfigZettelM, genConfigZettelC}, + id.VersionZid: {genVersionBuildM, genVersionBuildC}, + id.HostZid: {genVersionHostM, genVersionHostC}, + id.OperatingSystemZid: {genVersionOSM, genVersionOSC}, + id.BoxManagerZid: {genManagerM, genManagerC}, + id.MetadataKeyZid: {genKeysM, genKeysC}, + id.StartupConfigurationZid: {genConfigZettelM, genConfigZettelC}, } // Get returns the one program box. func getCompBox(boxNumber int, mf box.Enricher) box.ManagedBox { - return &compBox{ - log: kernel.Main.GetLogger(kernel.BoxService).Clone(). - Str("box", "comp").Int("boxnum", int64(boxNumber)).Child(), - number: boxNumber, - enricher: mf, - } + return &compBox{number: boxNumber, enricher: mf} } // Setup remembers important values. func Setup(cfg *meta.Meta) { myConfig = cfg.Clone() } -func (*compBox) Location() string { return "" } +func (pp *compBox) Location() string { return "" } + +func (pp *compBox) CanCreateZettel(ctx context.Context) bool { return false } + +func (pp *compBox) CreateZettel( + ctx context.Context, zettel domain.Zettel) (id.Zid, error) { + return id.Invalid, box.ErrReadOnly +} -func (cb *compBox) GetZettel(_ context.Context, zid id.Zid) (zettel.Zettel, error) { +func (pp *compBox) GetZettel(ctx context.Context, zid id.Zid) (domain.Zettel, error) { if gen, ok := myZettel[zid]; ok && gen.meta != nil { if m := gen.meta(zid); m != nil { updateMeta(m) if genContent := gen.content; genContent != nil { - cb.log.Trace().Msg("GetZettel/Content") - return zettel.Zettel{ - Meta: m, - Content: zettel.NewContent(genContent(m)), - }, nil - } - cb.log.Trace().Msg("GetZettel/NoContent") - return zettel.Zettel{Meta: m}, nil - } - } - err := box.ErrZettelNotFound{Zid: zid} - cb.log.Trace().Err(err).Msg("GetZettel/Err") - return zettel.Zettel{}, err -} - -func (*compBox) HasZettel(_ context.Context, zid id.Zid) bool { - _, found := myZettel[zid] - return found -} - -func (cb *compBox) ApplyZid(_ context.Context, handle box.ZidFunc, constraint query.RetrievePredicate) error { - cb.log.Trace().Int("entries", int64(len(myZettel))).Msg("ApplyZid") - for zid, gen := range myZettel { - if !constraint(zid) { - continue - } - if genMeta := gen.meta; genMeta != nil { - if genMeta(zid) != nil { - handle(zid) - } - } - } - return nil -} - -func (cb *compBox) ApplyMeta(ctx context.Context, handle box.MetaFunc, constraint query.RetrievePredicate) error { - cb.log.Trace().Int("entries", int64(len(myZettel))).Msg("ApplyMeta") - for zid, gen := range myZettel { - if !constraint(zid) { - continue - } - if genMeta := gen.meta; genMeta != nil { - if m := genMeta(zid); m != nil { - updateMeta(m) - cb.enricher.Enrich(ctx, m, cb.number) - handle(m) - } - } - } - return nil -} - -func (*compBox) AllowRenameZettel(_ context.Context, zid id.Zid) bool { + return domain.Zettel{ + Meta: m, + Content: domain.NewContent(genContent(m)), + }, nil + } + return domain.Zettel{Meta: m}, nil + } + } + return domain.Zettel{}, box.ErrNotFound +} + +func (pp *compBox) GetMeta(ctx context.Context, zid id.Zid) (*meta.Meta, error) { + if gen, ok := myZettel[zid]; ok { + if genMeta := gen.meta; genMeta != nil { + if m := genMeta(zid); m != nil { + updateMeta(m) + return m, nil + } + } + } + return nil, box.ErrNotFound +} + +func (pp *compBox) FetchZids(ctx context.Context) (id.Set, error) { + result := id.NewSetCap(len(myZettel)) + for zid, gen := range myZettel { + if genMeta := gen.meta; genMeta != nil { + if genMeta(zid) != nil { + result[zid] = true + } + } + } + return result, nil +} + +func (pp *compBox) SelectMeta(ctx context.Context, match search.MetaMatchFunc) (res []*meta.Meta, err error) { + for zid, gen := range myZettel { + if genMeta := gen.meta; genMeta != nil { + if m := genMeta(zid); m != nil { + updateMeta(m) + pp.enricher.Enrich(ctx, m, pp.number) + if match(m) { + res = append(res, m) + } + } + } + } + return res, nil +} + +func (pp *compBox) CanUpdateZettel(ctx context.Context, zettel domain.Zettel) bool { + return false +} + +func (pp *compBox) UpdateZettel(ctx context.Context, zettel domain.Zettel) error { + return box.ErrReadOnly +} + +func (pp *compBox) AllowRenameZettel(ctx context.Context, zid id.Zid) bool { _, ok := myZettel[zid] return !ok } -func (cb *compBox) RenameZettel(_ context.Context, curZid, _ id.Zid) (err error) { - if _, ok := myZettel[curZid]; ok { - err = box.ErrReadOnly - } else { - err = box.ErrZettelNotFound{Zid: curZid} - } - cb.log.Trace().Err(err).Msg("RenameZettel") - return err -} - -func (*compBox) CanDeleteZettel(context.Context, id.Zid) bool { return false } - -func (cb *compBox) DeleteZettel(_ context.Context, zid id.Zid) (err error) { - if _, ok := myZettel[zid]; ok { - err = box.ErrReadOnly - } else { - err = box.ErrZettelNotFound{Zid: zid} - } - cb.log.Trace().Err(err).Msg("DeleteZettel") - return err -} - -func (cb *compBox) ReadStats(st *box.ManagedBoxStats) { - st.ReadOnly = true - st.Zettel = len(myZettel) - cb.log.Trace().Int("zettel", int64(st.Zettel)).Msg("ReadStats") +func (pp *compBox) RenameZettel(ctx context.Context, curZid, newZid id.Zid) error { + if _, ok := myZettel[curZid]; ok { + return box.ErrReadOnly + } + return box.ErrNotFound +} + +func (pp *compBox) CanDeleteZettel(ctx context.Context, zid id.Zid) bool { return false } + +func (pp *compBox) DeleteZettel(ctx context.Context, zid id.Zid) error { + if _, ok := myZettel[zid]; ok { + return box.ErrReadOnly + } + return box.ErrNotFound +} + +func (pp *compBox) ReadStats(st *box.ManagedBoxStats) { + st.ReadOnly = true + st.Zettel = len(myZettel) } func updateMeta(m *meta.Meta) { - if _, ok := m.Get(api.KeySyntax); !ok { - m.Set(api.KeySyntax, meta.SyntaxZmk) - } - m.Set(api.KeyRole, api.ValueRoleConfiguration) - m.Set(api.KeyLang, api.ValueLangEN) - m.Set(api.KeyReadOnly, api.ValueTrue) - if _, ok := m.Get(api.KeyVisibility); !ok { - m.Set(api.KeyVisibility, api.ValueVisibilityExpert) + m.Set(meta.KeyNoIndex, meta.ValueTrue) + m.Set(meta.KeySyntax, meta.ValueSyntaxZmk) + m.Set(meta.KeyRole, meta.ValueRoleConfiguration) + m.Set(meta.KeyLang, meta.ValueLangEN) + m.Set(meta.KeyReadOnly, meta.ValueTrue) + if _, ok := m.Get(meta.KeyVisibility); !ok { + m.Set(meta.KeyVisibility, meta.ValueVisibilityExpert) } } Index: box/compbox/config.go ================================================================== --- box/compbox/config.go +++ box/compbox/config.go @@ -1,57 +1,52 @@ //----------------------------------------------------------------------------- -// Copyright (c) 2020-present Detlef Stern +// Copyright (c) 2020-2021 Detlef Stern // -// This file is part of Zettelstore. +// This file is part of zettelstore. // // Zettelstore is licensed under the latest version of the EUPL (European Union // Public License). Please see file LICENSE.txt for your rights and obligations // under this license. -// -// SPDX-License-Identifier: EUPL-1.2 -// SPDX-FileCopyrightText: 2020-present Detlef Stern //----------------------------------------------------------------------------- +// Package compbox provides zettel that have computed content. package compbox import ( - "bytes" + "strings" - "t73f.de/r/zsc/api" - "zettelstore.de/z/kernel" - "zettelstore.de/z/zettel/id" - "zettelstore.de/z/zettel/meta" + "zettelstore.de/z/domain/id" + "zettelstore.de/z/domain/meta" ) func genConfigZettelM(zid id.Zid) *meta.Meta { if myConfig == nil { return nil } m := meta.New(zid) - m.Set(api.KeyTitle, "Zettelstore Startup Configuration") - m.Set(api.KeyCreated, kernel.Main.GetConfig(kernel.CoreService, kernel.CoreStarted).(string)) - m.Set(api.KeyVisibility, api.ValueVisibilityExpert) + m.Set(meta.KeyTitle, "Zettelstore Startup Configuration") + m.Set(meta.KeyVisibility, meta.ValueVisibilityExpert) return m } -func genConfigZettelC(*meta.Meta) []byte { - var buf bytes.Buffer - for i, p := range myConfig.Pairs() { - if i > 0 { - buf.WriteByte('\n') - } - buf.WriteString("; ''") - buf.WriteString(p.Key) - buf.WriteString("''") - if p.Value != "" { - buf.WriteString("\n: ``") +func genConfigZettelC(m *meta.Meta) string { + var sb strings.Builder + for i, p := range myConfig.Pairs(false) { + if i > 0 { + sb.WriteByte('\n') + } + sb.WriteString("; ''") + sb.WriteString(p.Key) + sb.WriteString("''") + if p.Value != "" { + sb.WriteString("\n: ``") for _, r := range p.Value { if r == '`' { - buf.WriteByte('\\') + sb.WriteByte('\\') } - buf.WriteRune(r) + sb.WriteRune(r) } - buf.WriteString("``") + sb.WriteString("``") } } - return buf.Bytes() + return sb.String() } Index: box/compbox/keys.go ================================================================== --- box/compbox/keys.go +++ box/compbox/keys.go @@ -1,43 +1,38 @@ //----------------------------------------------------------------------------- -// Copyright (c) 2020-present Detlef Stern +// Copyright (c) 2020-2021 Detlef Stern // -// This file is part of Zettelstore. +// This file is part of zettelstore. // // Zettelstore is licensed under the latest version of the EUPL (European Union // Public License). Please see file LICENSE.txt for your rights and obligations // under this license. -// -// SPDX-License-Identifier: EUPL-1.2 -// SPDX-FileCopyrightText: 2020-present Detlef Stern //----------------------------------------------------------------------------- +// Package compbox provides zettel that have computed content. package compbox import ( - "bytes" "fmt" + "strings" - "t73f.de/r/zsc/api" - "zettelstore.de/z/kernel" - "zettelstore.de/z/zettel/id" - "zettelstore.de/z/zettel/meta" + "zettelstore.de/z/domain/id" + "zettelstore.de/z/domain/meta" ) func genKeysM(zid id.Zid) *meta.Meta { m := meta.New(zid) - m.Set(api.KeyTitle, "Zettelstore Supported Metadata Keys") - m.Set(api.KeyCreated, kernel.Main.GetConfig(kernel.CoreService, kernel.CoreVTime).(string)) - m.Set(api.KeyVisibility, api.ValueVisibilityLogin) + m.Set(meta.KeyTitle, "Zettelstore Supported Metadata Keys") + m.Set(meta.KeyVisibility, meta.ValueVisibilityLogin) return m } -func genKeysC(*meta.Meta) []byte { +func genKeysC(*meta.Meta) string { keys := meta.GetSortedKeyDescriptions() - var buf bytes.Buffer - buf.WriteString("|=Name<|=Type<|=Computed?:|=Property?:\n") + var sb strings.Builder + sb.WriteString("|=Name<|=Type<|=Computed?:|=Property?:\n") for _, kd := range keys { - fmt.Fprintf(&buf, - "|[[%v|query:%v?]]|%v|%v|%v\n", kd.Name, kd.Name, kd.Type.Name, kd.IsComputed(), kd.IsProperty()) + fmt.Fprintf(&sb, + "|%v|%v|%v|%v\n", kd.Name, kd.Type.Name, kd.IsComputed(), kd.IsProperty()) } - return buf.Bytes() + return sb.String() } DELETED box/compbox/log.go Index: box/compbox/log.go ================================================================== --- box/compbox/log.go +++ box/compbox/log.go @@ -1,53 +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. -// -// SPDX-License-Identifier: EUPL-1.2 -// SPDX-FileCopyrightText: 2021-present Detlef Stern -//----------------------------------------------------------------------------- - -package compbox - -import ( - "bytes" - - "t73f.de/r/zsc/api" - "zettelstore.de/z/kernel" - "zettelstore.de/z/zettel/id" - "zettelstore.de/z/zettel/meta" -) - -func genLogM(zid id.Zid) *meta.Meta { - m := meta.New(zid) - m.Set(api.KeyTitle, "Zettelstore Log") - m.Set(api.KeySyntax, meta.SyntaxText) - m.Set(api.KeyCreated, kernel.Main.GetConfig(kernel.CoreService, kernel.CoreStarted).(string)) - m.Set(api.KeyModified, kernel.Main.GetLastLogTime().Local().Format(id.TimestampLayout)) - return m -} - -func genLogC(*meta.Meta) []byte { - const tsFormat = "2006-01-02 15:04:05.999999" - entries := kernel.Main.RetrieveLogEntries() - var buf bytes.Buffer - for _, entry := range entries { - ts := entry.TS.Format(tsFormat) - buf.WriteString(ts) - for j := len(ts); j < len(tsFormat); j++ { - buf.WriteByte('0') - } - buf.WriteByte(' ') - buf.WriteString(entry.Level.Format()) - buf.WriteByte(' ') - buf.WriteString(entry.Prefix) - buf.WriteByte(' ') - buf.WriteString(entry.Message) - buf.WriteByte('\n') - } - return buf.Bytes() -} Index: box/compbox/manager.go ================================================================== --- box/compbox/manager.go +++ box/compbox/manager.go @@ -1,44 +1,40 @@ //----------------------------------------------------------------------------- -// Copyright (c) 2021-present Detlef Stern +// Copyright (c) 2021 Detlef Stern // -// This file is part of Zettelstore. +// This file is part of zettelstore. // // Zettelstore is licensed under the latest version of the EUPL (European Union // Public License). Please see file LICENSE.txt for your rights and obligations // under this license. -// -// SPDX-License-Identifier: EUPL-1.2 -// SPDX-FileCopyrightText: 2021-present Detlef Stern //----------------------------------------------------------------------------- +// Package compbox provides zettel that have computed content. package compbox import ( - "bytes" "fmt" + "strings" - "t73f.de/r/zsc/api" + "zettelstore.de/z/domain/id" + "zettelstore.de/z/domain/meta" "zettelstore.de/z/kernel" - "zettelstore.de/z/zettel/id" - "zettelstore.de/z/zettel/meta" ) func genManagerM(zid id.Zid) *meta.Meta { m := meta.New(zid) - m.Set(api.KeyTitle, "Zettelstore Box Manager") - m.Set(api.KeyCreated, kernel.Main.GetConfig(kernel.CoreService, kernel.CoreStarted).(string)) + m.Set(meta.KeyTitle, "Zettelstore Box Manager") return m } -func genManagerC(*meta.Meta) []byte { +func genManagerC(*meta.Meta) string { kvl := kernel.Main.GetServiceStatistics(kernel.BoxService) if len(kvl) == 0 { - return nil + return "No statistics available" } - var buf bytes.Buffer - buf.WriteString("|=Name|=Value>\n") + var sb strings.Builder + sb.WriteString("|=Name|=Value>\n") for _, kv := range kvl { - fmt.Fprintf(&buf, "| %v | %v\n", kv.Key, kv.Value) + fmt.Fprintf(&sb, "| %v | %v\n", kv.Key, kv.Value) } - return buf.Bytes() + return sb.String() } DELETED box/compbox/memory.go Index: box/compbox/memory.go ================================================================== --- box/compbox/memory.go +++ box/compbox/memory.go @@ -1,60 +0,0 @@ -//----------------------------------------------------------------------------- -// 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 compbox - -import ( - "bytes" - "fmt" - "os" - "runtime" - - "t73f.de/r/zsc/api" - "zettelstore.de/z/kernel" - "zettelstore.de/z/zettel/id" - "zettelstore.de/z/zettel/meta" -) - -func genMemoryM(zid id.Zid) *meta.Meta { - if myConfig == nil { - return nil - } - m := meta.New(zid) - m.Set(api.KeyTitle, "Zettelstore Memory") - m.Set(api.KeyCreated, kernel.Main.GetConfig(kernel.CoreService, kernel.CoreStarted).(string)) - m.Set(api.KeyVisibility, api.ValueVisibilityExpert) - return m -} - -func genMemoryC(*meta.Meta) []byte { - pageSize := os.Getpagesize() - var m runtime.MemStats - runtime.GC() - runtime.ReadMemStats(&m) - - var buf bytes.Buffer - buf.WriteString("|=Name|=Value>\n") - fmt.Fprintf(&buf, "|Page Size|%d\n", pageSize) - fmt.Fprintf(&buf, "|Pages|%d\n", m.HeapSys/uint64(pageSize)) - fmt.Fprintf(&buf, "|Heap Objects|%d\n", m.HeapObjects) - fmt.Fprintf(&buf, "|Heap Sys (KiB)|%d\n", m.HeapSys/1024) - fmt.Fprintf(&buf, "|Heap Inuse (KiB)|%d\n", m.HeapInuse/1024) - debug := kernel.Main.GetConfig(kernel.CoreService, kernel.CoreDebug).(bool) - if debug { - for i, bysize := range m.BySize { - fmt.Fprintf(&buf, "|Size %2d: %d|%d - %d → %d\n", - i, bysize.Size, bysize.Mallocs, bysize.Frees, bysize.Mallocs-bysize.Frees) - } - } - return buf.Bytes() -} DELETED box/compbox/parser.go Index: box/compbox/parser.go ================================================================== --- box/compbox/parser.go +++ box/compbox/parser.go @@ -1,54 +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. -// -// SPDX-License-Identifier: EUPL-1.2 -// SPDX-FileCopyrightText: 2021-present Detlef Stern -//----------------------------------------------------------------------------- - -package compbox - -import ( - "bytes" - "fmt" - "sort" - "strings" - - "t73f.de/r/zsc/api" - "zettelstore.de/z/kernel" - "zettelstore.de/z/parser" - "zettelstore.de/z/zettel/id" - "zettelstore.de/z/zettel/meta" -) - -func genParserM(zid id.Zid) *meta.Meta { - m := meta.New(zid) - m.Set(api.KeyTitle, "Zettelstore Supported Parser") - m.Set(api.KeyCreated, kernel.Main.GetConfig(kernel.CoreService, kernel.CoreVTime).(string)) - m.Set(api.KeyVisibility, api.ValueVisibilityLogin) - return m -} - -func genParserC(*meta.Meta) []byte { - var buf bytes.Buffer - buf.WriteString("|=Syntax<|=Alt. Value(s):|=Text Parser?:|=Text Format?:|=Image Format?:\n") - syntaxes := parser.GetSyntaxes() - sort.Strings(syntaxes) - for _, syntax := range syntaxes { - info := parser.Get(syntax) - if info.Name != syntax { - continue - } - altNames := info.AltNames - sort.Strings(altNames) - fmt.Fprintf( - &buf, "|%v|%v|%v|%v|%v\n", - syntax, strings.Join(altNames, ", "), info.IsASTParser, info.IsTextFormat, info.IsImageFormat) - } - return buf.Bytes() -} Index: box/compbox/version.go ================================================================== --- box/compbox/version.go +++ box/compbox/version.go @@ -1,61 +1,54 @@ //----------------------------------------------------------------------------- -// Copyright (c) 2020-present Detlef Stern +// Copyright (c) 2020-2021 Detlef Stern // -// This file is part of Zettelstore. +// This file is part of zettelstore. // // Zettelstore is licensed under the latest version of the EUPL (European Union // Public License). Please see file LICENSE.txt for your rights and obligations // under this license. -// -// SPDX-License-Identifier: EUPL-1.2 -// SPDX-FileCopyrightText: 2020-present Detlef Stern //----------------------------------------------------------------------------- +// Package compbox provides zettel that have computed content. package compbox import ( - "t73f.de/r/zsc/api" + "fmt" + + "zettelstore.de/z/domain/id" + "zettelstore.de/z/domain/meta" "zettelstore.de/z/kernel" - "zettelstore.de/z/zettel/id" - "zettelstore.de/z/zettel/meta" ) func getVersionMeta(zid id.Zid, title string) *meta.Meta { m := meta.New(zid) - m.Set(api.KeyTitle, title) - m.Set(api.KeyVisibility, api.ValueVisibilityExpert) + m.Set(meta.KeyTitle, title) + m.Set(meta.KeyVisibility, meta.ValueVisibilityExpert) return m } func genVersionBuildM(zid id.Zid) *meta.Meta { m := getVersionMeta(zid, "Zettelstore Version") - m.Set(api.KeyCreated, kernel.Main.GetConfig(kernel.CoreService, kernel.CoreVTime).(string)) - m.Set(api.KeyVisibility, api.ValueVisibilityLogin) + m.Set(meta.KeyVisibility, meta.ValueVisibilityPublic) return m } -func genVersionBuildC(*meta.Meta) []byte { - return []byte(kernel.Main.GetConfig(kernel.CoreService, kernel.CoreVersion).(string)) +func genVersionBuildC(*meta.Meta) string { + return kernel.Main.GetConfig(kernel.CoreService, kernel.CoreVersion).(string) } func genVersionHostM(zid id.Zid) *meta.Meta { - m := getVersionMeta(zid, "Zettelstore Host") - m.Set(api.KeyCreated, kernel.Main.GetConfig(kernel.CoreService, kernel.CoreStarted).(string)) - return m + return getVersionMeta(zid, "Zettelstore Host") } -func genVersionHostC(*meta.Meta) []byte { - return []byte(kernel.Main.GetConfig(kernel.CoreService, kernel.CoreHostname).(string)) +func genVersionHostC(*meta.Meta) string { + return kernel.Main.GetConfig(kernel.CoreService, kernel.CoreHostname).(string) } func genVersionOSM(zid id.Zid) *meta.Meta { - m := getVersionMeta(zid, "Zettelstore Operating System") - m.Set(api.KeyCreated, kernel.Main.GetConfig(kernel.CoreService, kernel.CoreStarted).(string)) - return m -} -func genVersionOSC(*meta.Meta) []byte { - goOS := kernel.Main.GetConfig(kernel.CoreService, kernel.CoreGoOS).(string) - goArch := kernel.Main.GetConfig(kernel.CoreService, kernel.CoreGoArch).(string) - result := make([]byte, 0, len(goOS)+len(goArch)+1) - result = append(result, goOS...) - result = append(result, '/') - return append(result, goArch...) + return getVersionMeta(zid, "Zettelstore Operating System") +} +func genVersionOSC(*meta.Meta) string { + return fmt.Sprintf( + "%v/%v", + kernel.Main.GetConfig(kernel.CoreService, kernel.CoreGoOS).(string), + kernel.Main.GetConfig(kernel.CoreService, kernel.CoreGoArch).(string), + ) } Index: box/constbox/base.css ================================================================== --- box/constbox/base.css +++ box/constbox/base.css @@ -1,19 +1,5 @@ -/*----------------------------------------------------------------------------- - * 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; @@ -22,11 +8,13 @@ height: 100%; } body { margin: 0; min-height: 100vh; + text-rendering: optimizeSpeed; line-height: 1.4; + overflow-x: hidden; background-color: #f8f8f8 ; height: 100%; } nav.zs-menu { background-color: hsl(210, 28%, 90%); @@ -41,12 +29,16 @@ text-align: center; padding:.41rem .5rem; text-decoration: none; color:black; } - nav.zs-menu > a:hover, .zs-dropdown:hover button { background-color: hsl(210, 28%, 80%) } - nav.zs-menu form { float: right } + nav.zs-menu > a:hover, .zs-dropdown:hover button { + background-color: hsl(210, 28%, 80%); + } + nav.zs-menu form { + float: right; + } nav.zs-menu form input[type=text] { padding: .12rem; border: none; margin-top: .25rem; margin-right: .5rem; @@ -79,14 +71,22 @@ padding:.41rem .5rem; text-decoration: none; display: block; text-align: left; } - .zs-dropdown-content > a:hover { background-color: hsl(210, 28%, 75%) } - .zs-dropdown:hover > .zs-dropdown-content { display: block } - main { padding: 0 1rem } - article > * + * { margin-top: .5rem } + .zs-dropdown-content > a:hover { + background-color: hsl(210, 28%, 75%); + } + .zs-dropdown:hover > .zs-dropdown-content { + display: block; + } + main { + padding: 0 1rem; + } + article > * + * { + margin-top: .5rem; + } article header { padding: 0; margin: 0; } h1,h2,h3,h4,h5,h6 { font-family:sans-serif; font-weight:normal } @@ -94,38 +94,56 @@ h2 { font-size:1.25rem; margin:.70rem 0 } h3 { font-size:1.15rem; margin:.75rem 0 } h4 { font-size:1.05rem; margin:.8rem 0; font-weight: bold } h5 { font-size:1.05rem; margin:.8rem 0 } h6 { font-size:1.05rem; margin:.8rem 0; font-weight: lighter } - p { margin: .5rem 0 0 0 } - p.zs-meta-zettel { margin-top: .5rem; margin-left: 0.5rem } - li,figure,figcaption,dl { margin: 0 } - dt { margin: .5rem 0 0 0 } - dt+dd { margin-top: 0 } - dd { margin: .5rem 0 0 2rem } - dd > p:first-child { margin: 0 0 0 0 } + p { + margin: .5rem 0 0 0; + } + ol,ul { + padding-left: 1.1rem; + } + li,figure,figcaption,dl { + margin: 0; + } + dt { + margin: .5rem 0 0 0; + } + dt+dd { + margin-top: 0; + } + dd { + margin: .5rem 0 0 2rem; + } + dd > p:first-child { + margin: 0 0 0 0; + } blockquote { border-left: 0.5rem solid lightgray; padding-left: 1rem; margin-left: 1rem; margin-right: 2rem; font-style: italic; } - blockquote p { margin-bottom: .5rem } - blockquote cite { font-style: normal } + blockquote p { + margin-bottom: .5rem; + } + blockquote cite { + font-style: normal; + } table { border-collapse: collapse; border-spacing: 0; max-width: 100%; } - thead>tr>td { border-bottom: 2px solid hsl(0, 0%, 70%); font-weight: bold } - tfoot>tr>td { border-top: 2px solid hsl(0, 0%, 70%); font-weight: bold } - td { + th,td { text-align: left; padding: .25rem .5rem; - border-bottom: 1px solid hsl(0, 0%, 85%) } + td { border-bottom: 1px solid hsl(0, 0%, 85%); } + thead th { border-bottom: 2px solid hsl(0, 0%, 70%); } + tfoot th { border-top: 2px solid hsl(0, 0%, 70%); } main form { padding: 0 .5em; margin: .5em 0 0 0; } main form:after { @@ -133,14 +151,21 @@ display: block; height: 0; clear: both; visibility: hidden; } - main form div { margin: .5em 0 0 0 } - input { font-family: monospace } - input[type="submit"],button,select { font: inherit } + main form div { + margin: .5em 0 0 0 + } + input { + font-family: monospace; + } + input[type="submit"],button,select { + font: inherit; + } label { font-family: sans-serif; font-size:.9rem } + label::after { content:":" } textarea { font-family: monospace; resize: vertical; width: 100%; } @@ -149,27 +174,28 @@ display:block; border:none; border-bottom:1px solid #ccc; width:100%; } - input.zs-primary { float:right } - input.zs-secondary { float:left } - input.zs-upload { - padding-left: 1em; - padding-right: 1em; - } - a:not([class]) { text-decoration-skip-ink: auto } - a.broken { text-decoration: line-through } - a.external::after { content: "➚"; display: inline-block } - img { max-width: 100% } - img.right { float: right } - ol.zs-endnotes { + .zs-button { + float:right; + margin: .5em 0 .5em 1em; + } + a:not([class]) { + text-decoration-skip-ink: auto; + } + .zs-broken { + text-decoration: line-through; + } + img { + max-width: 100%; + } + .zs-endnotes { padding-top: .5rem; border-top: 1px solid; } - kbd { font-family:monospace } - code,pre { + code,pre,kbd { font-family: monospace; font-size: 85%; } code { padding: .1rem .2rem; @@ -195,48 +221,43 @@ padding: .5rem .7rem; max-width: 100%; border-radius: .5rem; border: 1px solid black; } - div.zs-indication p:first-child { margin-top: 0 } + div.zs-indication p:first-child { + margin-top: 0; + } span.zs-indication { border: 1px solid black; border-radius: .25rem; padding: .1rem .2rem; font-size: 95%; } - .zs-info { - background-color: lightblue; - padding: .5rem 1rem; - } - .zs-warning { - background-color: lightyellow; - padding: .5rem 1rem; - } + .zs-example { border-style: dotted !important } .zs-error { background-color: lightpink; border-style: none !important; font-weight: bold; } - td.left { text-align:left } - td.center { text-align:center } - td.right { text-align:right } - .zs-font-size-0 { font-size:75% } - .zs-font-size-1 { font-size:83% } - .zs-font-size-2 { font-size:100% } - .zs-font-size-3 { font-size:117% } - .zs-font-size-4 { font-size:150% } - .zs-font-size-5 { font-size:200% } - .zs-deprecated { border-style: dashed; padding: .2rem } + kbd { + background: hsl(210, 5%, 100%); + border: 1px solid hsl(210, 5%, 70%); + border-radius: .25rem; + padding: .1rem .2rem; + font-size: 75%; + } .zs-meta { font-size:.75rem; color:#444; margin-bottom:1rem; } - .zs-meta a { color:#444 } - h1+.zs-meta { margin-top:-1rem } - nav > details { margin-top:1rem } + .zs-meta a { + color:#444; + } + h1+.zs-meta { + margin-top:-1rem; + } details > summary { width: 100%; background-color: #eee; font-family:sans-serif; } @@ -243,14 +264,16 @@ details > ul { margin-top:0; padding-left:2rem; background-color: #eee; } - footer { padding: 0 1rem } + footer { + padding: 0 1rem; + } @media (prefers-reduced-motion: reduce) { * { animation-duration: 0.01ms !important; animation-iteration-count: 1 !important; transition-duration: 0.01ms !important; scroll-behavior: auto !important; } } ADDED box/constbox/base.mustache Index: box/constbox/base.mustache ================================================================== --- box/constbox/base.mustache +++ box/constbox/base.mustache @@ -0,0 +1,66 @@ + + + + + + + + +{{{MetaHeader}}} + + +{{Title}} + + + +
+{{{Content}}} +
+{{#FooterHTML}} + +{{/FooterHTML}} + + DELETED box/constbox/base.sxn Index: box/constbox/base.sxn ================================================================== --- box/constbox/base.sxn +++ box/constbox/base.sxn @@ -1,63 +0,0 @@ -;;;---------------------------------------------------------------------------- -;;; 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"))) - (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-binding)) - (title ,title)) -(body - (nav (@ (class "zs-menu")) - (a (@ (href ,home-url)) "Home") - ,@(if with-auth - `((div (@ (class "zs-dropdown")) - (button "User") - (nav (@ (class "zs-dropdown-content")) - ,@(if user-is-valid - `((a (@ (href ,user-zettel-url)) ,user-ident) - (a (@ (href ,logout-url)) "Logout")) - `((a (@ (href ,login-url)) "Login")) - ) - ))) - ) - (div (@ (class "zs-dropdown")) - (button "Lists") - (nav (@ (class "zs-dropdown-content")) - (a (@ (href ,list-zettel-url)) "List Zettel") - (a (@ (href ,list-roles-url)) "List Roles") - (a (@ (href ,list-tags-url)) "List Tags") - ,@(if (bound? 'refresh-url) `((a (@ (href ,refresh-url)) "Refresh"))) - )) - ,@(if new-zettel-links - `((div (@ (class "zs-dropdown")) - (button "New") - (nav (@ (class "zs-dropdown-content")) - ,@(map wui-link new-zettel-links) - ))) - ) - (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 @@ -1,16 +1,13 @@ //----------------------------------------------------------------------------- -// Copyright (c) 2020-present Detlef Stern +// Copyright (c) 2020-2021 Detlef Stern // -// This file is part of Zettelstore. +// This file is part of zettelstore. // // Zettelstore is licensed under the latest version of the EUPL (European Union // Public License). Please see file LICENSE.txt for your rights and obligations // under this license. -// -// SPDX-License-Identifier: EUPL-1.2 -// SPDX-FileCopyrightText: 2020-present Detlef Stern //----------------------------------------------------------------------------- // Package constbox puts zettel inside the executable. package constbox @@ -17,489 +14,392 @@ import ( "context" _ "embed" // Allow to embed file content "net/url" - "t73f.de/r/zsc/api" "zettelstore.de/z/box" "zettelstore.de/z/box/manager" - "zettelstore.de/z/kernel" - "zettelstore.de/z/logger" - "zettelstore.de/z/query" - "zettelstore.de/z/zettel" - "zettelstore.de/z/zettel/id" - "zettelstore.de/z/zettel/meta" + "zettelstore.de/z/domain" + "zettelstore.de/z/domain/id" + "zettelstore.de/z/domain/meta" + "zettelstore.de/z/search" ) func init() { manager.Register( " const", func(u *url.URL, cdata *manager.ConnectData) (box.ManagedBox, error) { return &constBox{ - log: kernel.Main.GetLogger(kernel.BoxService).Clone(). - Str("box", "const").Int("boxnum", int64(cdata.Number)).Child(), number: cdata.Number, zettel: constZettelMap, enricher: cdata.Enricher, }, nil }) } type constHeader map[string]string + +func makeMeta(zid id.Zid, h constHeader) *meta.Meta { + m := meta.New(zid) + for k, v := range h { + m.Set(k, v) + } + return m +} type constZettel struct { header constHeader - content zettel.Content + content domain.Content } type constBox struct { - log *logger.Logger number int zettel map[id.Zid]constZettel enricher box.Enricher } -func (*constBox) Location() string { return "const:" } - -func (cb *constBox) GetZettel(_ context.Context, zid id.Zid) (zettel.Zettel, error) { - if z, ok := cb.zettel[zid]; ok { - cb.log.Trace().Msg("GetZettel") - return zettel.Zettel{Meta: meta.NewWithData(zid, z.header), Content: z.content}, nil - } - err := box.ErrZettelNotFound{Zid: zid} - cb.log.Trace().Err(err).Msg("GetZettel/Err") - return zettel.Zettel{}, err -} - -func (cb *constBox) HasZettel(_ context.Context, zid id.Zid) bool { - _, found := cb.zettel[zid] - return found -} - -func (cb *constBox) ApplyZid(_ context.Context, handle box.ZidFunc, constraint query.RetrievePredicate) error { - cb.log.Trace().Int("entries", int64(len(cb.zettel))).Msg("ApplyZid") - for zid := range cb.zettel { - if constraint(zid) { - handle(zid) - } - } - return nil -} - -func (cb *constBox) ApplyMeta(ctx context.Context, handle box.MetaFunc, constraint query.RetrievePredicate) error { - cb.log.Trace().Int("entries", int64(len(cb.zettel))).Msg("ApplyMeta") - for zid, zettel := range cb.zettel { - if constraint(zid) { - m := meta.NewWithData(zid, zettel.header) - cb.enricher.Enrich(ctx, m, cb.number) - handle(m) - } - } - return nil -} - -func (cb *constBox) AllowRenameZettel(_ context.Context, zid id.Zid) bool { - _, ok := cb.zettel[zid] +func (cp *constBox) Location() string { + return "const:" +} + +func (cp *constBox) CanCreateZettel(ctx context.Context) bool { return false } + +func (cp *constBox) CreateZettel(ctx context.Context, zettel domain.Zettel) (id.Zid, error) { + return id.Invalid, box.ErrReadOnly +} + +func (cp *constBox) GetZettel(ctx context.Context, zid id.Zid) (domain.Zettel, error) { + if z, ok := cp.zettel[zid]; ok { + return domain.Zettel{Meta: makeMeta(zid, z.header), Content: z.content}, nil + } + return domain.Zettel{}, box.ErrNotFound +} + +func (cp *constBox) GetMeta(ctx context.Context, zid id.Zid) (*meta.Meta, error) { + if z, ok := cp.zettel[zid]; ok { + return makeMeta(zid, z.header), nil + } + return nil, box.ErrNotFound +} + +func (cp *constBox) FetchZids(ctx context.Context) (id.Set, error) { + result := id.NewSetCap(len(cp.zettel)) + for zid := range cp.zettel { + result[zid] = true + } + return result, nil +} + +func (cp *constBox) SelectMeta(ctx context.Context, match search.MetaMatchFunc) (res []*meta.Meta, err error) { + for zid, zettel := range cp.zettel { + m := makeMeta(zid, zettel.header) + cp.enricher.Enrich(ctx, m, cp.number) + if match(m) { + res = append(res, m) + } + } + return res, nil +} + +func (cp *constBox) CanUpdateZettel(ctx context.Context, zettel domain.Zettel) bool { + return false +} + +func (cp *constBox) UpdateZettel(ctx context.Context, zettel domain.Zettel) error { + return box.ErrReadOnly +} + +func (cp *constBox) AllowRenameZettel(ctx context.Context, zid id.Zid) bool { + _, ok := cp.zettel[zid] return !ok } -func (cb *constBox) RenameZettel(_ context.Context, curZid, _ id.Zid) (err error) { - if _, ok := cb.zettel[curZid]; ok { - err = box.ErrReadOnly - } else { - err = box.ErrZettelNotFound{Zid: curZid} - } - cb.log.Trace().Err(err).Msg("RenameZettel") - return err -} - -func (*constBox) CanDeleteZettel(context.Context, id.Zid) bool { return false } - -func (cb *constBox) DeleteZettel(_ context.Context, zid id.Zid) (err error) { - if _, ok := cb.zettel[zid]; ok { - err = box.ErrReadOnly - } else { - err = box.ErrZettelNotFound{Zid: zid} - } - cb.log.Trace().Err(err).Msg("DeleteZettel") - return err -} - -func (cb *constBox) ReadStats(st *box.ManagedBoxStats) { - st.ReadOnly = true - st.Zettel = len(cb.zettel) - cb.log.Trace().Int("zettel", int64(st.Zettel)).Msg("ReadStats") -} +func (cp *constBox) RenameZettel(ctx context.Context, curZid, newZid id.Zid) error { + if _, ok := cp.zettel[curZid]; ok { + return box.ErrReadOnly + } + return box.ErrNotFound +} +func (cp *constBox) CanDeleteZettel(ctx context.Context, zid id.Zid) bool { return false } + +func (cp *constBox) DeleteZettel(ctx context.Context, zid id.Zid) error { + if _, ok := cp.zettel[zid]; ok { + return box.ErrReadOnly + } + return box.ErrNotFound +} + +func (cp *constBox) ReadStats(st *box.ManagedBoxStats) { + st.ReadOnly = true + st.Zettel = len(cp.zettel) +} + +const syntaxTemplate = "mustache" var constZettelMap = map[id.Zid]constZettel{ id.ConfigurationZid: { constHeader{ - api.KeyTitle: "Zettelstore Runtime Configuration", - api.KeyRole: api.ValueRoleConfiguration, - api.KeySyntax: meta.SyntaxNone, - api.KeyCreated: "20200804111624", - api.KeyVisibility: api.ValueVisibilityOwner, - }, - zettel.NewContent(nil)}, - id.MustParse(api.ZidLicense): { - constHeader{ - api.KeyTitle: "Zettelstore License", - api.KeyRole: api.ValueRoleConfiguration, - api.KeySyntax: meta.SyntaxText, - api.KeyCreated: "20210504135842", - api.KeyLang: api.ValueLangEN, - api.KeyModified: "20220131153422", - api.KeyReadOnly: api.ValueTrue, - api.KeyVisibility: api.ValueVisibilityPublic, - }, - zettel.NewContent(contentLicense)}, - id.MustParse(api.ZidAuthors): { - constHeader{ - api.KeyTitle: "Zettelstore Contributors", - api.KeyRole: api.ValueRoleConfiguration, - api.KeySyntax: meta.SyntaxZmk, - api.KeyCreated: "20210504135842", - api.KeyLang: api.ValueLangEN, - api.KeyReadOnly: api.ValueTrue, - api.KeyVisibility: api.ValueVisibilityLogin, - }, - zettel.NewContent(contentContributors)}, - id.MustParse(api.ZidDependencies): { - constHeader{ - api.KeyTitle: "Zettelstore Dependencies", - api.KeyRole: api.ValueRoleConfiguration, - api.KeySyntax: meta.SyntaxZmk, - api.KeyLang: api.ValueLangEN, - api.KeyReadOnly: api.ValueTrue, - api.KeyVisibility: api.ValueVisibilityPublic, - api.KeyCreated: "20210504135842", - api.KeyModified: "20240418095500", - }, - zettel.NewContent(contentDependencies)}, + meta.KeyTitle: "Zettelstore Runtime Configuration", + meta.KeyRole: meta.ValueRoleConfiguration, + meta.KeySyntax: meta.ValueSyntaxNone, + meta.KeyNoIndex: meta.ValueTrue, + meta.KeyVisibility: meta.ValueVisibilityOwner, + }, + domain.NewContent("")}, + id.LicenseZid: { + constHeader{ + meta.KeyTitle: "Zettelstore License", + meta.KeyRole: meta.ValueRoleConfiguration, + meta.KeySyntax: meta.ValueSyntaxText, + meta.KeyLang: meta.ValueLangEN, + meta.KeyReadOnly: meta.ValueTrue, + meta.KeyVisibility: meta.ValueVisibilityPublic, + }, + domain.NewContent(contentLicense)}, + id.AuthorsZid: { + constHeader{ + meta.KeyTitle: "Zettelstore Contributors", + meta.KeyRole: meta.ValueRoleConfiguration, + meta.KeySyntax: meta.ValueSyntaxZmk, + meta.KeyLang: meta.ValueLangEN, + meta.KeyReadOnly: meta.ValueTrue, + meta.KeyVisibility: meta.ValueVisibilityPublic, + }, + domain.NewContent(contentContributors)}, + id.DependenciesZid: { + constHeader{ + meta.KeyTitle: "Zettelstore Dependencies", + meta.KeyRole: meta.ValueRoleConfiguration, + meta.KeySyntax: meta.ValueSyntaxZmk, + meta.KeyLang: meta.ValueLangEN, + meta.KeyReadOnly: meta.ValueTrue, + meta.KeyVisibility: meta.ValueVisibilityPublic, + }, + domain.NewContent(contentDependencies)}, id.BaseTemplateZid: { constHeader{ - api.KeyTitle: "Zettelstore Base HTML Template", - api.KeyRole: api.ValueRoleConfiguration, - api.KeySyntax: meta.SyntaxSxn, - api.KeyCreated: "20230510155100", - api.KeyModified: "20240219145300", - api.KeyVisibility: api.ValueVisibilityExpert, + meta.KeyTitle: "Zettelstore Base HTML Template", + meta.KeyRole: meta.ValueRoleConfiguration, + meta.KeySyntax: syntaxTemplate, + meta.KeyNoIndex: meta.ValueTrue, + meta.KeyVisibility: meta.ValueVisibilityExpert, }, - zettel.NewContent(contentBaseSxn)}, + domain.NewContent(contentBaseMustache)}, id.LoginTemplateZid: { constHeader{ - api.KeyTitle: "Zettelstore Login Form HTML Template", - api.KeyRole: api.ValueRoleConfiguration, - api.KeySyntax: meta.SyntaxSxn, - api.KeyCreated: "20200804111624", - api.KeyModified: "20240219145200", - api.KeyVisibility: api.ValueVisibilityExpert, + meta.KeyTitle: "Zettelstore Login Form HTML Template", + meta.KeyRole: meta.ValueRoleConfiguration, + meta.KeySyntax: syntaxTemplate, + meta.KeyNoIndex: meta.ValueTrue, + meta.KeyVisibility: meta.ValueVisibilityExpert, }, - zettel.NewContent(contentLoginSxn)}, + domain.NewContent(contentLoginMustache)}, id.ZettelTemplateZid: { constHeader{ - api.KeyTitle: "Zettelstore Zettel HTML Template", - api.KeyRole: api.ValueRoleConfiguration, - api.KeySyntax: meta.SyntaxSxn, - api.KeyCreated: "20230510155300", - api.KeyModified: "20240219145100", - api.KeyVisibility: api.ValueVisibilityExpert, + meta.KeyTitle: "Zettelstore Zettel HTML Template", + meta.KeyRole: meta.ValueRoleConfiguration, + meta.KeySyntax: syntaxTemplate, + meta.KeyNoIndex: meta.ValueTrue, + meta.KeyVisibility: meta.ValueVisibilityExpert, }, - zettel.NewContent(contentZettelSxn)}, + domain.NewContent(contentZettelMustache)}, id.InfoTemplateZid: { constHeader{ - api.KeyTitle: "Zettelstore Info HTML Template", - api.KeyRole: api.ValueRoleConfiguration, - api.KeySyntax: meta.SyntaxSxn, - api.KeyCreated: "20200804111624", - api.KeyModified: "20240219145200", - api.KeyVisibility: api.ValueVisibilityExpert, + meta.KeyTitle: "Zettelstore Info HTML Template", + meta.KeyRole: meta.ValueRoleConfiguration, + meta.KeySyntax: syntaxTemplate, + meta.KeyNoIndex: meta.ValueTrue, + meta.KeyVisibility: meta.ValueVisibilityExpert, + }, + domain.NewContent(contentInfoMustache)}, + id.ContextTemplateZid: { + constHeader{ + meta.KeyTitle: "Zettelstore Context HTML Template", + meta.KeyRole: meta.ValueRoleConfiguration, + meta.KeySyntax: syntaxTemplate, + meta.KeyNoIndex: meta.ValueTrue, + meta.KeyVisibility: meta.ValueVisibilityExpert, }, - zettel.NewContent(contentInfoSxn)}, + domain.NewContent(contentContextMustache)}, id.FormTemplateZid: { constHeader{ - api.KeyTitle: "Zettelstore Form HTML Template", - api.KeyRole: api.ValueRoleConfiguration, - api.KeySyntax: meta.SyntaxSxn, - api.KeyCreated: "20200804111624", - api.KeyModified: "20240219145200", - api.KeyVisibility: api.ValueVisibilityExpert, + meta.KeyTitle: "Zettelstore Form HTML Template", + meta.KeyRole: meta.ValueRoleConfiguration, + meta.KeySyntax: syntaxTemplate, + meta.KeyNoIndex: meta.ValueTrue, + meta.KeyVisibility: meta.ValueVisibilityExpert, }, - zettel.NewContent(contentFormSxn)}, + domain.NewContent(contentFormMustache)}, id.RenameTemplateZid: { constHeader{ - api.KeyTitle: "Zettelstore Rename Form HTML Template", - api.KeyRole: api.ValueRoleConfiguration, - api.KeySyntax: meta.SyntaxSxn, - api.KeyCreated: "20200804111624", - api.KeyModified: "20240219145200", - api.KeyVisibility: api.ValueVisibilityExpert, + meta.KeyTitle: "Zettelstore Rename Form HTML Template", + meta.KeyRole: meta.ValueRoleConfiguration, + meta.KeySyntax: syntaxTemplate, + meta.KeyNoIndex: meta.ValueTrue, + meta.KeyVisibility: meta.ValueVisibilityExpert, }, - zettel.NewContent(contentRenameSxn)}, + domain.NewContent(contentRenameMustache)}, id.DeleteTemplateZid: { constHeader{ - api.KeyTitle: "Zettelstore Delete HTML Template", - api.KeyRole: api.ValueRoleConfiguration, - api.KeySyntax: meta.SyntaxSxn, - api.KeyCreated: "20200804111624", - api.KeyModified: "20240219145200", - api.KeyVisibility: api.ValueVisibilityExpert, + meta.KeyTitle: "Zettelstore Delete HTML Template", + meta.KeyRole: meta.ValueRoleConfiguration, + meta.KeySyntax: syntaxTemplate, + meta.KeyNoIndex: meta.ValueTrue, + meta.KeyVisibility: meta.ValueVisibilityExpert, }, - zettel.NewContent(contentDeleteSxn)}, + domain.NewContent(contentDeleteMustache)}, id.ListTemplateZid: { constHeader{ - api.KeyTitle: "Zettelstore List Zettel HTML Template", - api.KeyRole: api.ValueRoleConfiguration, - api.KeySyntax: meta.SyntaxSxn, - api.KeyCreated: "20230704122100", - api.KeyModified: "20240219145200", - api.KeyVisibility: api.ValueVisibilityExpert, + meta.KeyTitle: "Zettelstore List Zettel HTML Template", + meta.KeyRole: meta.ValueRoleConfiguration, + meta.KeySyntax: syntaxTemplate, + meta.KeyNoIndex: meta.ValueTrue, + meta.KeyVisibility: meta.ValueVisibilityExpert, + }, + domain.NewContent(contentListZettelMustache)}, + id.RolesTemplateZid: { + constHeader{ + meta.KeyTitle: "Zettelstore List Roles HTML Template", + meta.KeyRole: meta.ValueRoleConfiguration, + meta.KeySyntax: syntaxTemplate, + meta.KeyNoIndex: meta.ValueTrue, + meta.KeyVisibility: meta.ValueVisibilityExpert, + }, + domain.NewContent(contentListRolesMustache)}, + id.TagsTemplateZid: { + constHeader{ + meta.KeyTitle: "Zettelstore List Tags HTML Template", + meta.KeyRole: meta.ValueRoleConfiguration, + meta.KeySyntax: syntaxTemplate, + meta.KeyNoIndex: meta.ValueTrue, + meta.KeyVisibility: meta.ValueVisibilityExpert, }, - zettel.NewContent(contentListZettelSxn)}, + domain.NewContent(contentListTagsMustache)}, id.ErrorTemplateZid: { constHeader{ - api.KeyTitle: "Zettelstore Error HTML Template", - api.KeyRole: api.ValueRoleConfiguration, - api.KeySyntax: meta.SyntaxSxn, - api.KeyCreated: "20210305133215", - 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: { - constHeader{ - api.KeyTitle: "Zettelstore Sxn Base Code", - api.KeyRole: api.ValueRoleConfiguration, - api.KeySyntax: meta.SyntaxSxn, - api.KeyCreated: "20230619132800", - api.KeyModified: "20240219144600", - api.KeyReadOnly: api.ValueTrue, - api.KeyVisibility: api.ValueVisibilityExpert, - api.KeyPrecursor: string(api.ZidSxnPrelude), - }, - zettel.NewContent(contentBaseCodeSxn)}, - id.PreludeSxnZid: { - constHeader{ - api.KeyTitle: "Zettelstore Sxn Prelude", - api.KeyRole: api.ValueRoleConfiguration, - api.KeySyntax: meta.SyntaxSxn, - api.KeyCreated: "20231006181700", - api.KeyModified: "20240222121200", - api.KeyReadOnly: api.ValueTrue, - api.KeyVisibility: api.ValueVisibilityExpert, - }, - zettel.NewContent(contentPreludeSxn)}, - id.MustParse(api.ZidBaseCSS): { - constHeader{ - api.KeyTitle: "Zettelstore Base CSS", - api.KeyRole: api.ValueRoleConfiguration, - api.KeySyntax: meta.SyntaxCSS, - api.KeyCreated: "20200804111624", - api.KeyModified: "20231129112800", - api.KeyVisibility: api.ValueVisibilityPublic, - }, - zettel.NewContent(contentBaseCSS)}, - id.MustParse(api.ZidUserCSS): { - constHeader{ - api.KeyTitle: "Zettelstore User CSS", - api.KeyRole: api.ValueRoleConfiguration, - api.KeySyntax: meta.SyntaxCSS, - api.KeyCreated: "20210622110143", - api.KeyVisibility: api.ValueVisibilityPublic, - }, - zettel.NewContent([]byte("/* User-defined CSS */"))}, + meta.KeyTitle: "Zettelstore Error HTML Template", + meta.KeyRole: meta.ValueRoleConfiguration, + meta.KeySyntax: syntaxTemplate, + meta.KeyNoIndex: meta.ValueTrue, + meta.KeyVisibility: meta.ValueVisibilityExpert, + }, + domain.NewContent(contentErrorMustache)}, + id.BaseCSSZid: { + constHeader{ + meta.KeyTitle: "Zettelstore Base CSS", + meta.KeyRole: meta.ValueRoleConfiguration, + meta.KeySyntax: "css", + meta.KeyNoIndex: meta.ValueTrue, + meta.KeyVisibility: meta.ValueVisibilityPublic, + }, + domain.NewContent(contentBaseCSS)}, + id.UserCSSZid: { + constHeader{ + meta.KeyTitle: "Zettelstore User CSS", + meta.KeyRole: meta.ValueRoleConfiguration, + meta.KeySyntax: "css", + meta.KeyVisibility: meta.ValueVisibilityPublic, + }, + domain.NewContent("/* User-defined CSS */")}, id.EmojiZid: { constHeader{ - api.KeyTitle: "Zettelstore Generic Emoji", - api.KeyRole: api.ValueRoleConfiguration, - api.KeySyntax: meta.SyntaxGif, - api.KeyReadOnly: api.ValueTrue, - api.KeyCreated: "20210504175807", - api.KeyVisibility: api.ValueVisibilityPublic, + meta.KeyTitle: "Generic Emoji", + meta.KeyRole: meta.ValueRoleConfiguration, + meta.KeySyntax: meta.ValueSyntaxGif, + meta.KeyReadOnly: meta.ValueTrue, + meta.KeyVisibility: meta.ValueVisibilityPublic, }, - zettel.NewContent(contentEmoji)}, + domain.NewContent(contentEmoji)}, id.TOCNewTemplateZid: { constHeader{ - api.KeyTitle: "New Menu", - api.KeyRole: api.ValueRoleConfiguration, - api.KeySyntax: meta.SyntaxZmk, - api.KeyLang: api.ValueLangEN, - api.KeyCreated: "20210217161829", - api.KeyModified: "20231129111800", - api.KeyVisibility: api.ValueVisibilityCreator, - }, - zettel.NewContent(contentNewTOCZettel)}, - id.MustParse(api.ZidTemplateNewZettel): { - constHeader{ - api.KeyTitle: "New Zettel", - api.KeyRole: api.ValueRoleConfiguration, - api.KeySyntax: meta.SyntaxZmk, - api.KeyCreated: "20201028185209", - api.KeyModified: "20230929132900", - meta.NewPrefix + api.KeyRole: api.ValueRoleZettel, - api.KeyVisibility: api.ValueVisibilityCreator, - }, - zettel.NewContent(nil)}, - id.MustParse(api.ZidTemplateNewRole): { - constHeader{ - api.KeyTitle: "New Role", - api.KeyRole: api.ValueRoleConfiguration, - api.KeySyntax: meta.SyntaxZmk, - api.KeyCreated: "20231129110800", - meta.NewPrefix + api.KeyRole: api.ValueRoleRole, - meta.NewPrefix + api.KeyTitle: "", - api.KeyVisibility: api.ValueVisibilityCreator, - }, - zettel.NewContent(nil)}, - id.MustParse(api.ZidTemplateNewTag): { - constHeader{ - api.KeyTitle: "New Tag", - api.KeyRole: api.ValueRoleConfiguration, - api.KeySyntax: meta.SyntaxZmk, - api.KeyCreated: "20230929132400", - meta.NewPrefix + api.KeyRole: api.ValueRoleTag, - meta.NewPrefix + api.KeyTitle: "#", - api.KeyVisibility: api.ValueVisibilityCreator, - }, - zettel.NewContent(nil)}, - id.MustParse(api.ZidTemplateNewUser): { - constHeader{ - api.KeyTitle: "New User", - api.KeyRole: api.ValueRoleConfiguration, - api.KeySyntax: meta.SyntaxNone, - api.KeyCreated: "20201028185209", - meta.NewPrefix + api.KeyCredential: "", - meta.NewPrefix + api.KeyUserID: "", - meta.NewPrefix + api.KeyUserRole: api.ValueUserRoleReader, - api.KeyVisibility: api.ValueVisibilityOwner, - }, - zettel.NewContent(nil)}, - id.MustParse(api.ZidRoleZettelZettel): { - constHeader{ - api.KeyTitle: api.ValueRoleZettel, - api.KeyRole: api.ValueRoleRole, - api.KeySyntax: meta.SyntaxZmk, - api.KeyCreated: "20231129161400", - api.KeyLang: api.ValueLangEN, - api.KeyVisibility: api.ValueVisibilityLogin, - }, - zettel.NewContent(contentRoleZettel)}, - id.MustParse(api.ZidRoleConfigurationZettel): { - constHeader{ - api.KeyTitle: api.ValueRoleConfiguration, - api.KeyRole: api.ValueRoleRole, - api.KeySyntax: meta.SyntaxZmk, - api.KeyCreated: "20231129162800", - api.KeyLang: api.ValueLangEN, - api.KeyVisibility: api.ValueVisibilityLogin, - }, - zettel.NewContent(contentRoleConfiguration)}, - id.MustParse(api.ZidRoleRoleZettel): { - constHeader{ - api.KeyTitle: api.ValueRoleRole, - api.KeyRole: api.ValueRoleRole, - api.KeySyntax: meta.SyntaxZmk, - api.KeyCreated: "20231129162900", - api.KeyLang: api.ValueLangEN, - api.KeyVisibility: api.ValueVisibilityLogin, - }, - zettel.NewContent(contentRoleRole)}, - id.MustParse(api.ZidRoleTagZettel): { - constHeader{ - api.KeyTitle: api.ValueRoleTag, - api.KeyRole: api.ValueRoleRole, - api.KeySyntax: meta.SyntaxZmk, - api.KeyCreated: "20231129162000", - api.KeyLang: api.ValueLangEN, - api.KeyVisibility: api.ValueVisibilityLogin, - }, - zettel.NewContent(contentRoleTag)}, + meta.KeyTitle: "New Menu", + meta.KeyRole: meta.ValueRoleConfiguration, + meta.KeySyntax: meta.ValueSyntaxZmk, + meta.KeyLang: meta.ValueLangEN, + meta.KeyVisibility: meta.ValueVisibilityCreator, + }, + domain.NewContent(contentNewTOCZettel)}, + id.TemplateNewZettelZid: { + constHeader{ + meta.KeyTitle: "New Zettel", + meta.KeyRole: meta.ValueRoleZettel, + meta.KeySyntax: meta.ValueSyntaxZmk, + meta.KeyVisibility: meta.ValueVisibilityCreator, + }, + domain.NewContent("")}, + id.TemplateNewUserZid: { + constHeader{ + meta.KeyTitle: "New User", + meta.KeyRole: meta.ValueRoleUser, + meta.KeySyntax: meta.ValueSyntaxNone, + meta.NewPrefix + meta.KeyCredential: "", + meta.NewPrefix + meta.KeyUserID: "", + meta.NewPrefix + meta.KeyUserRole: meta.ValueUserRoleReader, + meta.KeyVisibility: meta.ValueVisibilityOwner, + }, + domain.NewContent("")}, id.DefaultHomeZid: { constHeader{ - api.KeyTitle: "Home", - api.KeyRole: api.ValueRoleZettel, - api.KeySyntax: meta.SyntaxZmk, - api.KeyLang: api.ValueLangEN, - api.KeyCreated: "20210210190757", + meta.KeyTitle: "Home", + meta.KeyRole: meta.ValueRoleZettel, + meta.KeySyntax: meta.ValueSyntaxZmk, + meta.KeyLang: meta.ValueLangEN, }, - zettel.NewContent(contentHomeZettel)}, + domain.NewContent(contentHomeZettel)}, } //go:embed license.txt -var contentLicense []byte +var contentLicense string //go:embed contributors.zettel -var contentContributors []byte +var contentContributors string //go:embed dependencies.zettel -var contentDependencies []byte - -//go:embed base.sxn -var contentBaseSxn []byte - -//go:embed login.sxn -var contentLoginSxn []byte - -//go:embed zettel.sxn -var contentZettelSxn []byte - -//go:embed info.sxn -var contentInfoSxn []byte - -//go:embed form.sxn -var contentFormSxn []byte - -//go:embed rename.sxn -var contentRenameSxn []byte - -//go:embed delete.sxn -var contentDeleteSxn []byte - -//go:embed listzettel.sxn -var contentListZettelSxn []byte - -//go:embed error.sxn -var contentErrorSxn []byte - -//go:embed start.sxn -var contentStartCodeSxn []byte - -//go:embed wuicode.sxn -var contentBaseCodeSxn []byte - -//go:embed prelude.sxn -var contentPreludeSxn []byte +var contentDependencies string + +//go:embed base.mustache +var contentBaseMustache string + +//go:embed login.mustache +var contentLoginMustache string + +//go:embed zettel.mustache +var contentZettelMustache string + +//go:embed info.mustache +var contentInfoMustache string + +//go:embed context.mustache +var contentContextMustache string + +//go:embed form.mustache +var contentFormMustache string + +//go:embed rename.mustache +var contentRenameMustache string + +//go:embed delete.mustache +var contentDeleteMustache string + +//go:embed listzettel.mustache +var contentListZettelMustache string + +//go:embed listroles.mustache +var contentListRolesMustache string + +//go:embed listtags.mustache +var contentListTagsMustache string + +//go:embed error.mustache +var contentErrorMustache string //go:embed base.css -var contentBaseCSS []byte +var contentBaseCSS string //go:embed emoji_spin.gif -var contentEmoji []byte +var contentEmoji string //go:embed newtoc.zettel -var contentNewTOCZettel []byte - -//go:embed rolezettel.zettel -var contentRoleZettel []byte - -//go:embed roleconfiguration.zettel -var contentRoleConfiguration []byte - -//go:embed rolerole.zettel -var contentRoleRole []byte - -//go:embed roletag.zettel -var contentRoleTag []byte +var contentNewTOCZettel string //go:embed home.zettel -var contentHomeZettel []byte +var contentHomeZettel string ADDED box/constbox/context.mustache Index: box/constbox/context.mustache ================================================================== --- box/constbox/context.mustache +++ box/constbox/context.mustache @@ -0,0 +1,16 @@ + ADDED box/constbox/delete.mustache Index: box/constbox/delete.mustache ================================================================== --- box/constbox/delete.mustache +++ box/constbox/delete.mustache @@ -0,0 +1,15 @@ +
+
+

Delete Zettel {{Zid}}

+
+

Do you really want to delete this zettel?

+
+{{#MetaPairs}} +
{{Key}}:
{{Value}}
+{{/MetaPairs}} +
+
+ +
+
+{{end}} DELETED box/constbox/delete.sxn Index: box/constbox/delete.sxn ================================================================== --- box/constbox/delete.sxn +++ box/constbox/delete.sxn @@ -1,39 +0,0 @@ -;;;---------------------------------------------------------------------------- -;;; 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")) - (h2 "Information") - (p "If you delete this zettel, the previously shadowed zettel from overlayed box " ,shadowed-box " becomes available.") - )) - ) - ,@(if incoming - `((div (@ (class "zs-warning")) - (h2 "Warning!") - (p "If you delete this zettel, incoming references from the following zettel will become invalid.") - (ul ,@(map wui-item-link incoming)) - )) - ) - ,@(if (and (bound? 'useless) useless) - `((div (@ (class "zs-warning")) - (h2 "Warning!") - (p "Deleting this zettel will also delete the following files, so that they will not be interpreted as content for this zettel.") - (ul ,@(map wui-item useless)) - )) - ) - ,(wui-meta-desc metapairs) - (form (@ (method "POST")) (input (@ (class "zs-primary") (type "submit") (value "Delete")))) -) Index: box/constbox/dependencies.zettel ================================================================== --- box/constbox/dependencies.zettel +++ box/constbox/dependencies.zettel @@ -1,9 +1,9 @@ Zettelstore is made with the help of other software and other artifacts. Thank you very much! -This zettel lists all of them, together with their licenses. +This zettel lists all of them, together with their license. === Go runtime and associated libraries ; License : BSD 3-Clause "New" or "Revised" License ``` @@ -34,73 +34,91 @@ THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. ``` -=== ASCIIToSVG -; URL -: [[https://github.com/asciitosvg/asciitosvg]] -; License -: MIT -; Remarks -: ASCIIToSVG was incorporated into the source code of Zettelstore, moving it into package ''zettelstore.de/z/parser/draw''. - Later, the source code was changed substantially to adapt it to the needs of Zettelstore. -``` -Copyright (c) 2015 The ASCIIToSVG Contributors - -Permission is hereby granted, free of charge, to any person obtaining a copy -of this software and associated documentation files (the "Software"), to deal -in the Software without restriction, including without limitation the rights -to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -copies of the Software, and to permit persons to whom the Software is -furnished to do so, subject to the following conditions: - -The above copyright notice and this permission notice shall be included in all -copies or substantial portions of the Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE -SOFTWARE. -``` - === Fsnotify ; URL : [[https://fsnotify.org/]] ; License : BSD 3-Clause "New" or "Revised" License ; Source : [[https://github.com/fsnotify/fsnotify]] ``` -Copyright © 2012 The Go Authors. All rights reserved. -Copyright © fsnotify Authors. All rights reserved. - -Redistribution and use in source and binary forms, with or without modification, -are permitted provided that the following conditions are met: - -* Redistributions of source code must retain the above copyright notice, this - list of conditions and the following disclaimer. -* Redistributions in binary form must reproduce the above copyright notice, this - list of conditions and the following disclaimer in the documentation and/or - other materials provided with the distribution. -* Neither the name of Google Inc. nor the names of its contributors may be used - to endorse or promote products derived from this software without specific - prior written permission. - -THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND -ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED -WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE -DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR -ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES -(INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; -LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON -ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT -(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS -SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. +Copyright (c) 2012 The Go Authors. All rights reserved. +Copyright (c) 2012-2019 fsnotify Authors. All rights reserved. + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are +met: + + * Redistributions of source code must retain the above copyright +notice, this list of conditions and the following disclaimer. + * Redistributions in binary form must reproduce the above +copyright notice, this list of conditions and the following disclaimer +in the documentation and/or other materials provided with the +distribution. + * Neither the name of Google Inc. nor the names of its +contributors may be used to endorse or promote products derived from +this software without specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. +``` + +=== hoisie/mustache / cbroglie/mustache +; URL & Source +: [[https://github.com/hoisie/mustache]] / [[https://github.com/cbroglie/mustache]] +; License +: MIT License +; Remarks +: cbroglie/mustache is a fork from hoisie/mustache (starting with commit [f9b4cbf]). + cbroglie/mustache does not claim a copyright and includes just the license file from hoisie/mustache. + cbroglie/mustache obviously continues with the original license. + +``` +Copyright (c) 2009 Michael Hoisie + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. +``` + +=== pascaldekloe/jwt +; URL & Source +: [[https://github.com/pascaldekloe/jwt]] +; License +: [[CC0 1.0 Universal|https://creativecommons.org/publicdomain/zero/1.0/legalcode]] +``` +To the extent possible under law, Pascal S. de Kloe has waived all +copyright and related or neighboring rights to JWT. This work is +published from The Netherlands. + +https://creativecommons.org/publicdomain/zero/1.0/legalcode ``` === yuin/goldmark ; URL & Source : [[https://github.com/yuin/goldmark]] @@ -127,18 +145,5 @@ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. ``` - -=== Sx, SxHTML, Zettelstore-Client -These are companion projects, written by the main developer of Zettelstore. -They are published under the same license, [[EUPL v1.2, or later|00000000000004]]. - -; URL & Source Sx -: [[https://t73f.de/r/sx]] -; URL & Source SxHTML -: [[https://t73f.de/r/sxhtml]] -; URL & Source Zettelstore-Client -: [[https://t73f.de/r/zsc]] -; License: -: European Union Public License, version 1.2 (EUPL v1.2), or later. ADDED box/constbox/error.mustache Index: box/constbox/error.mustache ================================================================== --- box/constbox/error.mustache +++ box/constbox/error.mustache @@ -0,0 +1,6 @@ +
+
+

{{ErrorTitle}}

+
+{{ErrorText}} +
DELETED box/constbox/error.sxn Index: box/constbox/error.sxn ================================================================== --- box/constbox/error.sxn +++ box/constbox/error.sxn @@ -1,17 +0,0 @@ -;;;---------------------------------------------------------------------------- -;;; 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 -) ADDED box/constbox/form.mustache Index: box/constbox/form.mustache ================================================================== --- box/constbox/form.mustache +++ box/constbox/form.mustache @@ -0,0 +1,38 @@ +
+
+

{{Heading}}

+
+
+
+ + +
+
+
+ + +
+ + +
+
+ + +
+
+ + +
+
+{{#IsTextContent}} + + +{{/IsTextContent}} +
+ +
+
DELETED box/constbox/form.sxn Index: box/constbox/form.sxn ================================================================== --- box/constbox/form.sxn +++ box/constbox/form.sxn @@ -1,63 +0,0 @@ -;;;---------------------------------------------------------------------------- -;;; 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") - (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") (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") - (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") - (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") (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") - (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"))) - (input (@ (class "zs-upload") (type "file") (id "zs-file") (name "file"))) - )) -) Index: box/constbox/home.zettel ================================================================== --- box/constbox/home.zettel +++ box/constbox/home.zettel @@ -3,17 +3,18 @@ You will find the lastest information about Zettelstore at [[https://zettelstore.de]]. Check that website regulary for [[upgrades|https://zettelstore.de/home/doc/trunk/www/download.wiki]] to the latest version. You should consult the [[change log|https://zettelstore.de/home/doc/trunk/www/changes.wiki]] before upgrading. Sometimes, you have to edit some of your Zettelstore-related zettel before upgrading. Since Zettelstore is currently in a development state, every upgrade might fix some of your problems. +To check for versions, there is a zettel with the [[current version|00000000000001]] of your Zettelstore. If you have problems concerning Zettelstore, do not hesitate to get in [[contact with the main developer|mailto:ds@zettelstore.de]]. === Reporting errors If you have encountered an error, please include the content of the following zettel in your mail (if possible): -* [[Zettelstore Version|00000000000001]]: {{00000000000001}} +* [[Zettelstore Version|00000000000001]] * [[Zettelstore Operating System|00000000000003]] * [[Zettelstore Startup Configuration|00000000000096]] * [[Zettelstore Runtime Configuration|00000000000100]] Additionally, you have to describe, what you have done before that error occurs ADDED box/constbox/info.mustache Index: box/constbox/info.mustache ================================================================== --- box/constbox/info.mustache +++ box/constbox/info.mustache @@ -0,0 +1,48 @@ +
+
+

Information for Zettel {{Zid}}

+WebContext +{{#CanWrite}} · Edit{{/CanWrite}} +{{#CanFolge}} · Folge{{/CanFolge}} +{{#CanCopy}} · Copy{{/CanCopy}} +{{#CanRename}}· Rename{{/CanRename}} +{{#CanDelete}}· Delete{{/CanDelete}} +
+

Interpreted Metadata

+{{#MetaData}}{{/MetaData}}
{{Key}}{{{Value}}}
+{{#HasLinks}} +

References

+{{#HasLocLinks}} +

Local

+ +{{/HasLocLinks}} +{{#HasExtLinks}} +

External

+ +{{/HasExtLinks}} +{{/HasLinks}} +

Parts and format

+ +{{#Matrix}} + +{{#Elements}}{{#HasURL}}{{/HasURL}}{{^HasURL}}{{/HasURL}} +{{/Elements}} + +{{/Matrix}} +
{{Text}}{{Text}}
+{{#HasShadowLinks}} +

Shadowed Boxes

+ +{{/HasShadowLinks}} +{{#Endnotes}}{{{Endnotes}}}{{/Endnotes}} +
DELETED box/constbox/info.sxn Index: box/constbox/info.sxn ================================================================== --- box/constbox/info.sxn +++ box/constbox/info.sxn @@ -1,48 +0,0 @@ -;;;---------------------------------------------------------------------------- -;;; 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-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-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") - ,@unlinked-content - (form - (label (@ (for "phrase")) "Search Phrase") - (input (@ (class "zs-input") (type "text") (id "phrase") (name ,query-key-phrase) (placeholder "Phrase..") (value ,phrase))) - ) - (h2 "Parts and encodings") - ,(wui-enc-matrix enc-eval) - (h3 "Parsed (not evaluated)") - ,(wui-enc-matrix enc-parsed) - ,@(if shadow-links - `((h2 "Shadowed Boxes") - (ul ,@(map wui-item shadow-links)) - ) - ) -) Index: box/constbox/license.txt ================================================================== --- box/constbox/license.txt +++ box/constbox/license.txt @@ -1,6 +1,6 @@ -Copyright (c) 2020-present Detlef Stern +Copyright (c) 2020-2021 Detlef Stern Licensed under the EUPL Zettelstore is licensed under the European Union Public License, version 1.2 or later (EUPL v. 1.2). The license is available in the official languages of the ADDED box/constbox/listroles.mustache Index: box/constbox/listroles.mustache ================================================================== --- box/constbox/listroles.mustache +++ box/constbox/listroles.mustache @@ -0,0 +1,8 @@ + ADDED box/constbox/listtags.mustache Index: box/constbox/listtags.mustache ================================================================== --- box/constbox/listtags.mustache +++ box/constbox/listtags.mustache @@ -0,0 +1,10 @@ + ADDED box/constbox/listzettel.mustache Index: box/constbox/listzettel.mustache ================================================================== --- box/constbox/listzettel.mustache +++ box/constbox/listzettel.mustache @@ -0,0 +1,6 @@ +
+

{{Title}}

+
+ DELETED box/constbox/listzettel.sxn Index: box/constbox/listzettel.sxn ================================================================== --- box/constbox/listzettel.sxn +++ box/constbox/listzettel.sxn @@ -1,50 +0,0 @@ -;;;---------------------------------------------------------------------------- -;;; 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)) - (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)) - ) - ,@(if (bound? 'role-zettel) - `((p (@ (class "zs-meta-zettel")) "Role zettel: " ,@role-zettel)) - ) - ,@(if (bound? 'create-role-zettel) - `((p (@ (class "zs-meta-zettel")) "Create role zettel: " ,@create-role-zettel)) - ) - ,@content - ,@endnotes - (form (@ (action ,(if (bound? 'create-url) create-url))) - ,(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"))) - ) - ) - ) -) ADDED box/constbox/login.mustache Index: box/constbox/login.mustache ================================================================== --- box/constbox/login.mustache +++ box/constbox/login.mustache @@ -0,0 +1,19 @@ +
+
+

{{Title}}

+
+{{#Retry}} +
Wrong user name / password. Try again.
+{{/Retry}} +
+
+ + +
+
+ + +
+ +
+
DELETED box/constbox/login.sxn Index: box/constbox/login.sxn ================================================================== --- box/constbox/login.sxn +++ box/constbox/login.sxn @@ -1,27 +0,0 @@ -;;;---------------------------------------------------------------------------- -;;; 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 - (label (@ (for "username")) "User name:") - (input (@ (class "zs-input") (type "text") (id "username") (name "username") (placeholder "Your user name..") (autofocus)))) - (div - (label (@ (for "password")) "Password:") - (input (@ (class "zs-input") (type "password") (id "password") (name "password") (placeholder "Your password..")))) - (div - (input (@ (class "zs-primary") (type "submit") (value "Login")))) - ) -) Index: box/constbox/newtoc.zettel ================================================================== --- box/constbox/newtoc.zettel +++ box/constbox/newtoc.zettel @@ -1,6 +1,4 @@ This zettel lists all zettel that should act as a template for new zettel. These zettel will be included in the ""New"" menu of the WebUI. * [[New Zettel|00000000090001]] -* [[New Role|00000000090004]] -* [[New Tag|00000000090003]] * [[New User|00000000090002]] DELETED box/constbox/prelude.sxn Index: box/constbox/prelude.sxn ================================================================== --- box/constbox/prelude.sxn +++ box/constbox/prelude.sxn @@ -1,62 +0,0 @@ -;;;---------------------------------------------------------------------------- -;;; 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 -;;;---------------------------------------------------------------------------- - -;;; 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 in all places. It asumes that the symbols NIL and T are defined. - -;; 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 SYMBOL may occur in later bindings. -(defmacro let* (bindings . body) - (if (null? bindings) - `(begin ,@body) - `(let ((,(caar bindings) ,(cadar bindings))) - (let* ,(cdr bindings) ,@body)))) - -;; cond macro -;; -;; (cond ((COND EXPR) ...)) -(defmacro cond clauses - (if (null? clauses) - () - (let* ((clause (car clauses)) - (the-cond (car clause))) - (if (= the-cond T) - `(begin ,@(cdr clause)) - `(if ,the-cond - (begin ,@(cdr clause)) - (cond ,@(cdr clauses))))))) - -;; and macro -;; -;; (and EXPR ...) -(defmacro and args - (cond ((null? args) T) - ((null? (cdr args)) (car args)) - (T `(if ,(car args) (and ,@(cdr args)))))) - - -;; or macro -;; -;; (or EXPR ...) -(defmacro or args - (cond ((null? args) NIL) - ((null? (cdr args)) (car args)) - (T `(if ,(car args) T (or ,@(cdr args)))))) ADDED box/constbox/rename.mustache Index: box/constbox/rename.mustache ================================================================== --- box/constbox/rename.mustache +++ box/constbox/rename.mustache @@ -0,0 +1,19 @@ +
+
+

Rename Zettel {{.Zid}}

+
+

Do you really want to rename this zettel?

+
+
+ + +
+ + +
+
+{{#MetaPairs}} +
{{Key}}:
{{Value}}
+{{/MetaPairs}} +
+
DELETED box/constbox/rename.sxn Index: box/constbox/rename.sxn ================================================================== --- box/constbox/rename.sxn +++ box/constbox/rename.sxn @@ -1,42 +0,0 @@ -;;;---------------------------------------------------------------------------- -;;; 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")) - (h2 "Warning!") - (p "If you rename this zettel, incoming references from the following zettel will become invalid.") - (ul ,@(map wui-item-link incoming)) - )) - ) - ,@(if (and (bound? 'useless) useless) - `((div (@ (class "zs-warning")) - (h2 "Warning!") - (p "Renaming this zettel will also delete the following files, so that they will not be interpreted as content for this zettel.") - (ul ,@(map wui-item useless)) - )) - ) - (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") (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) -) DELETED box/constbox/roleconfiguration.zettel Index: box/constbox/roleconfiguration.zettel ================================================================== --- box/constbox/roleconfiguration.zettel +++ box/constbox/roleconfiguration.zettel @@ -1,20 +0,0 @@ -Zettel with role ""configuration"" are used within Zettelstore to manage and to show the current configuration of the software. - -Typically, there are some public zettel that show the license of this software, its dependencies, some CSS code to make the default web user interface a litte bit nicer, and the defult image to singal a broken image. - -Other zettel are only visible if an user has authenticated itself, or if there is no authentication enabled. -In this case, one additional configuration zettel is the zettel containing the version number of this software. -Other zettel are showing the supported metadata keys and supported syntax values. -Zettel that allow to configure the menu of template to create new zettel are also using the role ""configuration"". - -Most important is the zettel that contains the runtime configuration. -You may change its metadata value to change the behaviour of the software. - -One configuration is the ""expert mode"". -If enabled, and if you are authorized so see them, you will discover some more zettel. -For example, HTML templates to customize the default web user interface, to show the application log, to see statistics about zettel boxes, to show the host name and it operating system, and many more. - -You are allowed to add your own configuration zettel, for example if you want to customize the look and feel of zettel by placing relevant data into your own zettel. - -By default, user zettel (for authentification) use also the role ""configuration"". -However, you are allowed to change this. DELETED box/constbox/rolerole.zettel Index: box/constbox/rolerole.zettel ================================================================== --- box/constbox/rolerole.zettel +++ box/constbox/rolerole.zettel @@ -1,10 +0,0 @@ -A zettel with the role ""role"" describes a specific role. -The described role must be the title of such a zettel. - -This zettel is such a zettel, as it describes the meaning of the role ""role"". -Therefore it has the title ""role"" too. -If you like, this zettel is a meta-role. - -You are free to create your own role-describing zettel. -For example, you want to document the intended meaning of the role. -You might also be interested to describe needed metadata so that some software is enabled to analyse or to process your zettel. DELETED box/constbox/roletag.zettel Index: box/constbox/roletag.zettel ================================================================== --- box/constbox/roletag.zettel +++ box/constbox/roletag.zettel @@ -1,6 +0,0 @@ -A zettel with role ""tag"" is a zettel that describes specific tag. -The tag name must be the title of such a zettel. - -Such zettel are similar to this specific zettel: this zettel describes zettel with a role ""tag"". -These zettel with the role ""tag"" describe specific tags. -These might form a hierarchy of meta-tags (and meta-roles). DELETED box/constbox/rolezettel.zettel Index: box/constbox/rolezettel.zettel ================================================================== --- box/constbox/rolezettel.zettel +++ box/constbox/rolezettel.zettel @@ -1,7 +0,0 @@ -A zettel with the role ""zettel"" is typically used to document your own thoughts. -Such zettel are the main reason to use the software Zettelstore. - -The only predefined zettel with the role ""zettel"" is the [[default home zettel|00010000000000]], which contains some welcome information. - -You are free to change this. -In this case you should modify this zettel too, so that it reflects your own use of zettel with the role ""zettel"". DELETED box/constbox/start.sxn Index: box/constbox/start.sxn ================================================================== --- box/constbox/start.sxn +++ box/constbox/start.sxn @@ -1,17 +0,0 @@ -;;;---------------------------------------------------------------------------- -;;; 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 -;;;---------------------------------------------------------------------------- - -;;; 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 -;;; "Zettelstore Sxn Base Code" zettel. It provides the base definitions. DELETED box/constbox/wuicode.sxn Index: box/constbox/wuicode.sxn ================================================================== --- box/constbox/wuicode.sxn +++ box/constbox/wuicode.sxn @@ -1,143 +0,0 @@ -;;;---------------------------------------------------------------------------- -;;; 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 -;;;---------------------------------------------------------------------------- - -;; Contains WebUI specific code, but not related to a specific template. - -;; wui-list-item returns the argument as a HTML list item. -(defun wui-item (s) `(li ,s)) - -;; wui-info-meta-table-row takes a pair and translates it into a HTML table row -;; with two columns. -(defun 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. -(defun wui-valid-link (l) - (if (car l) - `(li (a (@ (href ,(cdr l))) ,(cdr l))) - `(li ,(cdr l)))) - -;; wui-link takes a link (title . url) and returns a HTML reference. -(defun wui-link (q) - `(a (@ (href ,(cdr q))) ,(car q))) - -;; wui-item-link taks a pair (text . url) and returns a HTML link inside -;; a list item. -(defun wui-item-link (q) `(li ,(wui-link q))) - -;; wui-tdata-link taks a pair (text . url) and returns a HTML link inside -;; a table data item. -(defun wui-tdata-link (q) `(td ,(wui-link q))) - -;; wui-item-popup-link is like 'wui-item-link, but the HTML link will open -;; a new tab / window. -(defun wui-item-popup-link (e) - `(li (a (@ (href ,e) (target "_blank") (rel "noopener noreferrer")) ,e))) - -;; wui-option-value returns a value for an HTML option element. -(defun wui-option-value (v) `(option (@ (value ,v)))) - -;; wui-datalist returns a HTML datalist with the given HTML identifier and a -;; list of values. -(defun wui-datalist (id lst) - (if lst - `((datalist (@ (id ,id)) ,@(map wui-option-value lst))))) - -;; wui-pair-desc-item takes a pair '(term . text) and returns a list with -;; a HTML description term and a HTML description data. -(defun wui-pair-desc-item (p) `((dt ,(car p)) (dd ,(cdr p)))) - -;; wui-meta-desc returns a HTML description list made from the list of pairs -;; given. -(defun wui-meta-desc (l) - `(dl ,@(apply append (map wui-pair-desc-item l)))) - -;; wui-enc-matrix returns the HTML table of all encodings and parts. -(defun wui-enc-matrix (matrix) - `(table - ,@(map - (lambda (row) `(tr (th ,(car row)) ,@(map wui-tdata-link (cdr row)))) - matrix))) - -;; CSS-ROLE-map is a mapping (pair list, assoc list) of role names to zettel -;; identifier. It is used in the base template to update the metadata of the -;; HTML page to include some role specific CSS code. -;; 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. -(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. -(defvar ACTION-SEPARATOR '(@H " · ")) - -;; 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 (binding-lookup 'version-url binding))) - (if (defined? version-url) `((@H " · ") (a (@ (href ,version-url)) "Version")))) - ,@(let ((child-url (binding-lookup 'child-url binding))) - (if (defined? child-url) `((@H " · ") (a (@ (href ,child-url)) "Child")))) - ,@(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". -(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 (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. -(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 ((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))) - ) -) ADDED box/constbox/zettel.mustache Index: box/constbox/zettel.mustache ================================================================== --- box/constbox/zettel.mustache +++ box/constbox/zettel.mustache @@ -0,0 +1,28 @@ +
+
+

{{{HTMLTitle}}}

+
+{{#CanWrite}}Edit ·{{/CanWrite}} +{{Zid}} · +Info · +({{RoleText}}) +{{#HasTags}}· {{#Tags}} {{Text}}{{/Tags}}{{/HasTags}} +{{#CanCopy}}· Copy{{/CanCopy}} +{{#CanFolge}}· Folge{{/CanFolge}} +{{#FolgeRefs}}
Folge: {{{FolgeRefs}}}{{/FolgeRefs}} +{{#PrecursorRefs}}
Precursor: {{{PrecursorRefs}}}{{/PrecursorRefs}} +{{#HasExtURL}}
URL: {{ExtURL}}{{/HasExtURL}} +
+
+{{{Content}}} +{{#HasBackLinks}} +
+Additional links to this zettel +
    +{{#BackLinks}} +
  • {{Text}}
  • +{{/BackLinks}} +
+
+{{/HasBackLinks}} +
DELETED box/constbox/zettel.sxn Index: box/constbox/zettel.sxn ================================================================== --- box/constbox/zettel.sxn +++ box/constbox/zettel.sxn @@ -1,43 +0,0 @@ -;;;---------------------------------------------------------------------------- -;;; 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 " · "))) - ,zid (@H " · ") - (a (@ (href ,info-url)) "Info") (@H " · ") - "(" ,@(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-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-binding)) - ) - ) - ,@content - ,endnotes - ,@(if (or folge-links subordinate-links back-links successor-links) - `((nav - ,@(if folge-links `((details (@ (,folge-open)) (summary "Folgezettel") (ul ,@(map wui-item-link folge-links))))) - ,@(if subordinate-links `((details (@ (,subordinate-open)) (summary "Subordinates") (ul ,@(map wui-item-link subordinate-links))))) - ,@(if back-links `((details (@ (,back-open)) (summary "Incoming") (ul ,@(map wui-item-link back-links))))) - ,@(if successor-links `((details (@ (,successor-open)) (summary "Successors") (ul ,@(map wui-item-link successor-links))))) - )) - ) -) Index: box/dirbox/dirbox.go ================================================================== --- box/dirbox/dirbox.go +++ box/dirbox/dirbox.go @@ -1,16 +1,13 @@ //----------------------------------------------------------------------------- -// Copyright (c) 2020-present Detlef Stern +// Copyright (c) 2020-2021 Detlef Stern // -// This file is part of Zettelstore. +// This file is part of zettelstore. // // Zettelstore is licensed under the latest version of the EUPL (European Union // Public License). Please see file LICENSE.txt for your rights and obligations // under this license. -// -// SPDX-License-Identifier: EUPL-1.2 -// SPDX-FileCopyrightText: 2020-present Detlef Stern //----------------------------------------------------------------------------- // Package dirbox provides a directory-based zettel box. package dirbox @@ -18,197 +15,136 @@ "context" "errors" "net/url" "os" "path/filepath" + "strconv" + "strings" "sync" + "time" "zettelstore.de/z/box" + "zettelstore.de/z/box/dirbox/directory" + "zettelstore.de/z/box/filebox" "zettelstore.de/z/box/manager" - "zettelstore.de/z/box/notify" - "zettelstore.de/z/kernel" - "zettelstore.de/z/logger" - "zettelstore.de/z/query" - "zettelstore.de/z/zettel" - "zettelstore.de/z/zettel/id" - "zettelstore.de/z/zettel/meta" + "zettelstore.de/z/domain" + "zettelstore.de/z/domain/id" + "zettelstore.de/z/domain/meta" + "zettelstore.de/z/search" ) func init() { manager.Register("dir", func(u *url.URL, cdata *manager.ConnectData) (box.ManagedBox, error) { - var log *logger.Logger - if krnl := kernel.Main; krnl != nil { - log = krnl.GetLogger(kernel.BoxService).Clone().Str("box", "dir").Int("boxnum", int64(cdata.Number)).Child() - } path := getDirPath(u) if _, err := os.Stat(path); errors.Is(err, os.ErrNotExist) { return nil, err } + dirSrvSpec, defWorker, maxWorker := getDirSrvInfo(u.Query().Get("type")) dp := dirBox{ - log: log, number: cdata.Number, location: u.String(), - readonly: box.GetQueryBool(u, "readonly"), + readonly: getQueryBool(u, "readonly"), cdata: *cdata, dir: path, - notifySpec: getDirSrvInfo(log, u.Query().Get("type")), - fSrvs: makePrime(uint32(box.GetQueryInt(u, "worker", 1, 7, 1499))), + dirRescan: time.Duration(getQueryInt(u, "rescan", 60, 3600, 30*24*60*60)) * time.Second, + dirSrvSpec: dirSrvSpec, + fSrvs: uint32(getQueryInt(u, "worker", 1, defWorker, maxWorker)), } return &dp, nil }) } -func makePrime(n uint32) uint32 { - for !isPrime(n) { - n++ - } - return n -} - -func isPrime(n uint32) bool { - if n == 0 { - return false - } - if n <= 3 { - return true - } - if n%2 == 0 { - return false - } - for i := uint32(3); i*i <= n; i += 2 { - if n%i == 0 { - return false - } - } - return true -} - -type notifyTypeSpec int +type directoryServiceSpec int const ( - _ notifyTypeSpec = iota - dirNotifyAny - dirNotifySimple - dirNotifyFS -) - -func getDirSrvInfo(log *logger.Logger, notifyType string) notifyTypeSpec { - for range 2 { - switch notifyType { - case kernel.BoxDirTypeNotify: - return dirNotifyFS - case kernel.BoxDirTypeSimple: - return dirNotifySimple - default: - notifyType = kernel.Main.GetConfig(kernel.BoxService, kernel.BoxDefaultDirType).(string) - } - } - log.Error().Str("notifyType", notifyType).Msg("Unable to set notify type, using a default") - return dirNotifySimple -} + _ directoryServiceSpec = iota + dirSrvAny + dirSrvSimple + dirSrvNotify +) func getDirPath(u *url.URL) string { if u.Opaque != "" { return filepath.Clean(u.Opaque) } return filepath.Clean(u.Path) } + +func getQueryBool(u *url.URL, key string) bool { + _, ok := u.Query()[key] + return ok +} + +func getQueryInt(u *url.URL, key string, min, def, max int) int { + sVal := u.Query().Get(key) + if sVal == "" { + return def + } + iVal, err := strconv.Atoi(sVal) + if err != nil { + return def + } + if iVal < min { + return min + } + if iVal > max { + return max + } + return iVal +} // dirBox uses a directory to store zettel as files. type dirBox struct { - log *logger.Logger number int location string readonly bool cdata manager.ConnectData dir string - notifySpec notifyTypeSpec - dirSrv *notify.DirService + dirRescan time.Duration + dirSrvSpec directoryServiceSpec + dirSrv directory.Service + mustNotify bool fSrvs uint32 fCmds []chan fileCmd mxCmds sync.RWMutex } func (dp *dirBox) Location() string { return dp.location } -func (dp *dirBox) State() box.StartState { - if ds := dp.dirSrv; ds != nil { - switch ds.State() { - case notify.DsCreated: - return box.StartStateStopped - case notify.DsStarting: - return box.StartStateStarting - case notify.DsWorking: - return box.StartStateStarted - case notify.DsMissing: - return box.StartStateStarted - case notify.DsStopping: - return box.StartStateStopping - } - } - return box.StartStateStopped -} - -func (dp *dirBox) Start(context.Context) error { +func (dp *dirBox) Start(ctx context.Context) error { dp.mxCmds.Lock() - defer dp.mxCmds.Unlock() dp.fCmds = make([]chan fileCmd, 0, dp.fSrvs) - for i := range dp.fSrvs { + for i := uint32(0); i < dp.fSrvs; i++ { cc := make(chan fileCmd) - go fileService(i, dp.log.Clone().Str("sub", "file").Uint("fn", uint64(i)).Child(), dp.dir, cc) + go fileService(i, cc) dp.fCmds = append(dp.fCmds, cc) } - - var notifier notify.Notifier - var err error - switch dp.notifySpec { - case dirNotifySimple: - 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.Error().Err(err).Msg("Unable to create directory supervisor") - dp.stopFileServices() - return err - } - dp.dirSrv = notify.NewDirService( - dp, - dp.log.Clone().Str("sub", "dirsrv").Child(), - notifier, - dp.cdata.Notify, - ) - dp.dirSrv.Start() - return nil -} - -func (dp *dirBox) Refresh(_ context.Context) { - dp.dirSrv.Refresh() - dp.log.Trace().Msg("Refresh") -} - -func (dp *dirBox) Stop(_ context.Context) { + dp.setupDirService() + dp.mxCmds.Unlock() + if dp.dirSrv == nil { + panic("No directory service") + } + return dp.dirSrv.Start() +} + +func (dp *dirBox) Stop(ctx context.Context) error { dirSrv := dp.dirSrv dp.dirSrv = nil - if dirSrv != nil { - dirSrv.Stop() - } - dp.stopFileServices() -} - -func (dp *dirBox) stopFileServices() { + err := dirSrv.Stop() for _, c := range dp.fCmds { close(c) } + return err } -func (dp *dirBox) notifyChanged(zid id.Zid) { - if chci := dp.cdata.Notify; chci != nil { - dp.log.Trace().Zid(zid).Msg("notifyChanged") - chci <- box.UpdateInfo{Reason: box.OnZettel, Zid: zid} +func (dp *dirBox) notifyChanged(reason box.UpdateReason, zid id.Zid) { + if dp.mustNotify { + if chci := dp.cdata.Notify; chci != nil { + chci <- box.UpdateInfo{Reason: reason, Zid: zid} + } } } func (dp *dirBox) getFileChan(zid id.Zid) chan fileCmd { // Based on https://en.wikipedia.org/wiki/Fowler%E2%80%93Noll%E2%80%93Vo_hash_function @@ -220,189 +156,265 @@ dp.mxCmds.RLock() defer dp.mxCmds.RUnlock() return dp.fCmds[sum%dp.fSrvs] } -func (dp *dirBox) CanCreateZettel(_ context.Context) bool { +func (dp *dirBox) CanCreateZettel(ctx context.Context) bool { return !dp.readonly } -func (dp *dirBox) CreateZettel(ctx context.Context, zettel zettel.Zettel) (id.Zid, error) { +func (dp *dirBox) CreateZettel(ctx context.Context, zettel domain.Zettel) (id.Zid, error) { if dp.readonly { return id.Invalid, box.ErrReadOnly } - newZid, err := dp.dirSrv.SetNewDirEntry() + entry, err := dp.dirSrv.GetNew() if err != nil { return id.Invalid, err } meta := zettel.Meta - meta.Zid = newZid - entry := notify.DirEntry{Zid: newZid} - dp.updateEntryFromMetaContent(&entry, meta, zettel.Content) + meta.Zid = entry.Zid + dp.updateEntryFromMeta(entry, meta) - err = dp.srvSetZettel(ctx, &entry, zettel) + err = setZettel(dp, entry, zettel) if err == nil { - err = dp.dirSrv.UpdateDirEntry(&entry) + dp.dirSrv.UpdateEntry(entry) } - dp.notifyChanged(meta.Zid) - dp.log.Trace().Err(err).Zid(meta.Zid).Msg("CreateZettel") + dp.notifyChanged(box.OnUpdate, meta.Zid) return meta.Zid, err } -func (dp *dirBox) GetZettel(ctx context.Context, zid id.Zid) (zettel.Zettel, error) { - entry := dp.dirSrv.GetDirEntry(zid) - if !entry.IsValid() { - return zettel.Zettel{}, box.ErrZettelNotFound{Zid: zid} +func (dp *dirBox) GetZettel(ctx context.Context, zid id.Zid) (domain.Zettel, error) { + entry, err := dp.dirSrv.GetEntry(zid) + if err != nil || !entry.IsValid() { + return domain.Zettel{}, box.ErrNotFound } - m, c, err := dp.srvGetMetaContent(ctx, entry, zid) + m, c, err := getMetaContent(dp, entry, zid) if err != nil { - return zettel.Zettel{}, err + return domain.Zettel{}, err } - zettel := zettel.Zettel{Meta: m, Content: zettel.NewContent(c)} - dp.log.Trace().Zid(zid).Msg("GetZettel") + dp.cleanupMeta(ctx, m) + zettel := domain.Zettel{Meta: m, Content: domain.NewContent(c)} return zettel, nil } -func (dp *dirBox) HasZettel(_ context.Context, zid id.Zid) bool { - return dp.dirSrv.GetDirEntry(zid).IsValid() +func (dp *dirBox) GetMeta(ctx context.Context, zid id.Zid) (*meta.Meta, error) { + entry, err := dp.dirSrv.GetEntry(zid) + if err != nil || !entry.IsValid() { + return nil, box.ErrNotFound + } + m, err := getMeta(dp, entry, zid) + if err != nil { + return nil, err + } + dp.cleanupMeta(ctx, m) + return m, nil } -func (dp *dirBox) ApplyZid(_ context.Context, handle box.ZidFunc, constraint query.RetrievePredicate) error { - entries := dp.dirSrv.GetDirEntries(constraint) - dp.log.Trace().Int("entries", int64(len(entries))).Msg("ApplyZid") +func (dp *dirBox) FetchZids(ctx context.Context) (id.Set, error) { + entries, err := dp.dirSrv.GetEntries() + if err != nil { + return nil, err + } + result := id.NewSetCap(len(entries)) for _, entry := range entries { - handle(entry.Zid) + result[entry.Zid] = true } - return nil + return result, nil } -func (dp *dirBox) ApplyMeta(ctx context.Context, handle box.MetaFunc, constraint query.RetrievePredicate) error { - entries := dp.dirSrv.GetDirEntries(constraint) - dp.log.Trace().Int("entries", int64(len(entries))).Msg("ApplyMeta") - +func (dp *dirBox) SelectMeta(ctx context.Context, match search.MetaMatchFunc) (res []*meta.Meta, err error) { + entries, err := dp.dirSrv.GetEntries() + if err != nil { + return nil, err + } + res = make([]*meta.Meta, 0, len(entries)) // The following loop could be parallelized if needed for performance. for _, entry := range entries { - m, err := dp.srvGetMeta(ctx, entry, entry.Zid) + m, err1 := getMeta(dp, entry, entry.Zid) + err = err1 if err != nil { - dp.log.Trace().Err(err).Msg("ApplyMeta/getMeta") - return err + continue } + dp.cleanupMeta(ctx, m) dp.cdata.Enricher.Enrich(ctx, m, dp.number) - handle(m) + + if match(m) { + res = append(res, m) + } + } + if err != nil { + return nil, err } - return nil + return res, nil } -func (dp *dirBox) CanUpdateZettel(context.Context, zettel.Zettel) bool { +func (dp *dirBox) CanUpdateZettel(ctx context.Context, zettel domain.Zettel) bool { return !dp.readonly } -func (dp *dirBox) UpdateZettel(ctx context.Context, zettel zettel.Zettel) error { +func (dp *dirBox) UpdateZettel(ctx context.Context, zettel domain.Zettel) error { if dp.readonly { return box.ErrReadOnly } meta := zettel.Meta - zid := meta.Zid - if !zid.IsValid() { - return box.ErrInvalidZid{Zid: zid.String()} + if !meta.Zid.IsValid() { + return &box.ErrInvalidID{Zid: meta.Zid} } - entry := dp.dirSrv.GetDirEntry(zid) + entry, err := dp.dirSrv.GetEntry(meta.Zid) + if err != nil { + return err + } if !entry.IsValid() { // Existing zettel, but new in this box. - entry = ¬ify.DirEntry{Zid: zid} + entry = &directory.Entry{Zid: meta.Zid} + dp.updateEntryFromMeta(entry, meta) + } else if entry.MetaSpec == directory.MetaSpecNone { + defaultMeta := filebox.CalcDefaultMeta(entry.Zid, entry.ContentExt) + if !meta.Equal(defaultMeta, true) { + dp.updateEntryFromMeta(entry, meta) + dp.dirSrv.UpdateEntry(entry) + } } - dp.updateEntryFromMetaContent(entry, meta, zettel.Content) - dp.dirSrv.UpdateDirEntry(entry) - err := dp.srvSetZettel(ctx, entry, zettel) + err = setZettel(dp, entry, zettel) if err == nil { - dp.notifyChanged(zid) + dp.notifyChanged(box.OnUpdate, meta.Zid) } - dp.log.Trace().Zid(zid).Err(err).Msg("UpdateZettel") return err } -func (dp *dirBox) updateEntryFromMetaContent(entry *notify.DirEntry, m *meta.Meta, content zettel.Content) { - entry.SetupFromMetaContent(m, content, dp.cdata.Config.GetZettelFileSyntax) +func (dp *dirBox) updateEntryFromMeta(entry *directory.Entry, meta *meta.Meta) { + entry.MetaSpec, entry.ContentExt = dp.calcSpecExt(meta) + basePath := dp.calcBasePath(entry) + if entry.MetaSpec == directory.MetaSpecFile { + entry.MetaPath = basePath + ".meta" + } + entry.ContentPath = basePath + "." + entry.ContentExt + entry.Duplicates = false +} + +func (dp *dirBox) calcBasePath(entry *directory.Entry) string { + p := entry.ContentPath + if p == "" { + return filepath.Join(dp.dir, entry.Zid.String()) + } + // ContentPath w/o the file extension + return p[0 : len(p)-len(filepath.Ext(p))] +} + +func (dp *dirBox) calcSpecExt(m *meta.Meta) (directory.MetaSpec, string) { + if m.YamlSep { + return directory.MetaSpecHeader, "zettel" + } + syntax := m.GetDefault(meta.KeySyntax, "bin") + switch syntax { + case meta.ValueSyntaxNone, meta.ValueSyntaxZmk: + return directory.MetaSpecHeader, "zettel" + } + for _, s := range dp.cdata.Config.GetZettelFileSyntax() { + if s == syntax { + return directory.MetaSpecHeader, "zettel" + } + } + return directory.MetaSpecFile, syntax } -func (dp *dirBox) AllowRenameZettel(context.Context, id.Zid) bool { +func (dp *dirBox) AllowRenameZettel(ctx context.Context, zid id.Zid) bool { return !dp.readonly } func (dp *dirBox) RenameZettel(ctx context.Context, curZid, newZid id.Zid) error { if curZid == newZid { return nil } - curEntry := dp.dirSrv.GetDirEntry(curZid) - if !curEntry.IsValid() { - return box.ErrZettelNotFound{Zid: curZid} + curEntry, err := dp.dirSrv.GetEntry(curZid) + if err != nil || !curEntry.IsValid() { + return box.ErrNotFound } if dp.readonly { return box.ErrReadOnly } // Check whether zettel with new ID already exists in this box. - if dp.HasZettel(ctx, newZid) { - return box.ErrInvalidZid{Zid: newZid.String()} - } - - oldMeta, oldContent, err := dp.srvGetMetaContent(ctx, curEntry, curZid) - if err != nil { - return err - } - - newEntry, err := dp.dirSrv.RenameDirEntry(curEntry, newZid) - if err != nil { - return err - } - oldMeta.Zid = newZid - newZettel := zettel.Zettel{Meta: oldMeta, Content: zettel.NewContent(oldContent)} - if err = dp.srvSetZettel(ctx, &newEntry, newZettel); err != nil { - // "Rollback" rename. No error checking... - dp.dirSrv.RenameDirEntry(&newEntry, curZid) - return err - } - err = dp.srvDeleteZettel(ctx, curEntry, curZid) - if err == nil { - dp.notifyChanged(curZid) - dp.notifyChanged(newZid) - } - dp.log.Trace().Zid(curZid).Zid(newZid).Err(err).Msg("RenameZettel") - return err -} - -func (dp *dirBox) CanDeleteZettel(_ context.Context, zid id.Zid) bool { - if dp.readonly { - return false - } - entry := dp.dirSrv.GetDirEntry(zid) - return entry.IsValid() + if _, err = dp.GetMeta(ctx, newZid); err == nil { + return &box.ErrInvalidID{Zid: newZid} + } + + oldMeta, oldContent, err := getMetaContent(dp, curEntry, curZid) + if err != nil { + return err + } + + newEntry := directory.Entry{ + Zid: newZid, + MetaSpec: curEntry.MetaSpec, + MetaPath: renamePath(curEntry.MetaPath, curZid, newZid), + ContentPath: renamePath(curEntry.ContentPath, curZid, newZid), + ContentExt: curEntry.ContentExt, + } + + if err = dp.dirSrv.RenameEntry(curEntry, &newEntry); err != nil { + return err + } + oldMeta.Zid = newZid + newZettel := domain.Zettel{Meta: oldMeta, Content: domain.NewContent(oldContent)} + if err = setZettel(dp, &newEntry, newZettel); err != nil { + // "Rollback" rename. No error checking... + dp.dirSrv.RenameEntry(&newEntry, curEntry) + return err + } + err = deleteZettel(dp, curEntry, curZid) + if err == nil { + dp.notifyChanged(box.OnDelete, curZid) + dp.notifyChanged(box.OnUpdate, newZid) + } + return err +} + +func (dp *dirBox) CanDeleteZettel(ctx context.Context, zid id.Zid) bool { + if dp.readonly { + return false + } + entry, err := dp.dirSrv.GetEntry(zid) + return err == nil && entry.IsValid() } func (dp *dirBox) DeleteZettel(ctx context.Context, zid id.Zid) error { if dp.readonly { return box.ErrReadOnly } - entry := dp.dirSrv.GetDirEntry(zid) - if !entry.IsValid() { - return box.ErrZettelNotFound{Zid: zid} - } - err := dp.dirSrv.DeleteDirEntry(zid) - if err != nil { - return nil - } - err = dp.srvDeleteZettel(ctx, entry, zid) - if err == nil { - dp.notifyChanged(zid) - } - dp.log.Trace().Zid(zid).Err(err).Msg("DeleteZettel") + entry, err := dp.dirSrv.GetEntry(zid) + if err != nil || !entry.IsValid() { + return box.ErrNotFound + } + dp.dirSrv.DeleteEntry(zid) + err = deleteZettel(dp, entry, zid) + if err == nil { + dp.notifyChanged(box.OnDelete, zid) + } return err } func (dp *dirBox) ReadStats(st *box.ManagedBoxStats) { st.ReadOnly = dp.readonly - st.Zettel = dp.dirSrv.NumDirEntries() - dp.log.Trace().Int("zettel", int64(st.Zettel)).Msg("ReadStats") + st.Zettel, _ = dp.dirSrv.NumEntries() +} + +func (dp *dirBox) cleanupMeta(ctx context.Context, m *meta.Meta) { + if role, ok := m.Get(meta.KeyRole); !ok || role == "" { + m.Set(meta.KeyRole, dp.cdata.Config.GetDefaultRole()) + } + if syntax, ok := m.Get(meta.KeySyntax); !ok || syntax == "" { + m.Set(meta.KeySyntax, dp.cdata.Config.GetDefaultSyntax()) + } +} + +func renamePath(path string, curID, newID id.Zid) string { + dir, file := filepath.Split(path) + if cur := curID.String(); strings.HasPrefix(file, cur) { + file = newID.String() + file[len(cur):] + return filepath.Join(dir, file) + } + return path } DELETED box/dirbox/dirbox_test.go Index: box/dirbox/dirbox_test.go ================================================================== --- box/dirbox/dirbox_test.go +++ box/dirbox/dirbox_test.go @@ -1,53 +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. -// -// SPDX-License-Identifier: EUPL-1.2 -// SPDX-FileCopyrightText: 2021-present Detlef Stern -//----------------------------------------------------------------------------- - -package dirbox - -import "testing" - -func TestIsPrime(t *testing.T) { - testcases := []struct { - n uint32 - exp bool - }{ - {0, false}, {1, true}, {2, true}, {3, true}, {4, false}, {5, true}, - {6, false}, {7, true}, {8, false}, {9, false}, {10, false}, - {11, true}, {12, false}, {13, true}, {14, false}, {15, false}, - {17, true}, {19, true}, {21, false}, {23, true}, {25, false}, - {27, false}, {29, true}, {31, true}, {33, false}, {35, false}, - } - for _, tc := range testcases { - got := isPrime(tc.n) - if got != tc.exp { - t.Errorf("isPrime(%d)=%v, but got %v", tc.n, tc.exp, got) - } - } -} - -func TestMakePrime(t *testing.T) { - for i := range uint32(1500) { - np := makePrime(i) - if np < i { - t.Errorf("makePrime(%d) < %d", i, np) - continue - } - if !isPrime(np) { - t.Errorf("makePrime(%d) == %d is not prime", i, np) - continue - } - if isPrime(i) && i != np { - t.Errorf("%d is already prime, but got %d as next prime", i, np) - continue - } - } -} ADDED box/dirbox/directory/directory.go Index: box/dirbox/directory/directory.go ================================================================== --- box/dirbox/directory/directory.go +++ box/dirbox/directory/directory.go @@ -0,0 +1,53 @@ +//----------------------------------------------------------------------------- +// Copyright (c) 2020-2021 Detlef Stern +// +// This file is part of zettelstore. +// +// Zettelstore is licensed under the latest version of the EUPL (European Union +// Public License). Please see file LICENSE.txt for your rights and obligations +// under this license. +//----------------------------------------------------------------------------- + +// Package directory manages the directory interface of a dirstore. +package directory + +import "zettelstore.de/z/domain/id" + +// Service is the interface of a directory service. +type Service interface { + Start() error + Stop() error + NumEntries() (int, error) + GetEntries() ([]*Entry, error) + GetEntry(zid id.Zid) (*Entry, error) + GetNew() (*Entry, error) + UpdateEntry(entry *Entry) error + RenameEntry(curEntry, newEntry *Entry) error + DeleteEntry(zid id.Zid) error +} + +// MetaSpec defines all possibilities where meta data can be stored. +type MetaSpec int + +// Constants for MetaSpec +const ( + _ MetaSpec = iota + MetaSpecNone // no meta information + MetaSpecFile // meta information is in meta file + MetaSpecHeader // meta information is in header +) + +// Entry stores everything for a directory entry. +type Entry struct { + Zid id.Zid + MetaSpec MetaSpec // location of meta information + MetaPath string // file path of meta information + ContentPath string // file path of zettel content + ContentExt string // (normalized) file extension of zettel content + Duplicates bool // multiple content files +} + +// IsValid checks whether the entry is valid. +func (e *Entry) IsValid() bool { + return e != nil && e.Zid.IsValid() +} ADDED box/dirbox/makedir.go Index: box/dirbox/makedir.go ================================================================== --- box/dirbox/makedir.go +++ box/dirbox/makedir.go @@ -0,0 +1,43 @@ +//----------------------------------------------------------------------------- +// Copyright (c) 2021 Detlef Stern +// +// This file is part of zettelstore. +// +// Zettelstore is licensed under the latest version of the EUPL (European Union +// Public License). Please see file LICENSE.txt for your rights and obligations +// under this license. +//----------------------------------------------------------------------------- + +// Package dirbox provides a directory-based zettel box. +package dirbox + +import ( + "zettelstore.de/z/box/dirbox/notifydir" + "zettelstore.de/z/box/dirbox/simpledir" + "zettelstore.de/z/kernel" +) + +func getDirSrvInfo(dirType string) (directoryServiceSpec, int, int) { + for count := 0; count < 2; count++ { + switch dirType { + case kernel.BoxDirTypeNotify: + return dirSrvNotify, 7, 1499 + case kernel.BoxDirTypeSimple: + return dirSrvSimple, 1, 1 + default: + dirType = kernel.Main.GetConfig(kernel.BoxService, kernel.BoxDefaultDirType).(string) + } + } + panic("unable to set default dir box type: " + dirType) +} + +func (dp *dirBox) setupDirService() { + switch dp.dirSrvSpec { + case dirSrvSimple: + dp.dirSrv = simpledir.NewService(dp.dir) + dp.mustNotify = true + default: + dp.dirSrv = notifydir.NewService(dp.dir, dp.dirRescan, dp.cdata.Notify) + dp.mustNotify = false + } +} ADDED box/dirbox/notifydir/notifydir.go Index: box/dirbox/notifydir/notifydir.go ================================================================== --- box/dirbox/notifydir/notifydir.go +++ box/dirbox/notifydir/notifydir.go @@ -0,0 +1,126 @@ +//----------------------------------------------------------------------------- +// Copyright (c) 2020-2021 Detlef Stern +// +// This file is part of zettelstore. +// +// Zettelstore is licensed under the latest version of the EUPL (European Union +// Public License). Please see file LICENSE.txt for your rights and obligations +// under this license. +//----------------------------------------------------------------------------- + +// Package notifydir manages the notified directory part of a dirstore. +package notifydir + +import ( + "time" + + "zettelstore.de/z/box" + "zettelstore.de/z/box/dirbox/directory" + "zettelstore.de/z/domain/id" +) + +// notifyService specifies a directory scan service. +type notifyService struct { + dirPath string + rescanTime time.Duration + done chan struct{} + cmds chan dirCmd + infos chan<- box.UpdateInfo +} + +// NewService creates a new directory service. +func NewService(directoryPath string, rescanTime time.Duration, chci chan<- box.UpdateInfo) directory.Service { + srv := ¬ifyService{ + dirPath: directoryPath, + rescanTime: rescanTime, + cmds: make(chan dirCmd), + infos: chci, + } + return srv +} + +// Start makes the directory service operational. +func (srv *notifyService) Start() error { + tick := make(chan struct{}) + rawEvents := make(chan *fileEvent) + events := make(chan *fileEvent) + + ready := make(chan int) + go srv.directoryService(events, ready) + go collectEvents(events, rawEvents) + go watchDirectory(srv.dirPath, rawEvents, tick) + + if srv.done != nil { + panic("src.done already set") + } + srv.done = make(chan struct{}) + go ping(tick, srv.rescanTime, srv.done) + <-ready + return nil +} + +// Stop stops the directory service. +func (srv *notifyService) Stop() error { + close(srv.done) + srv.done = nil + return nil +} + +func (srv *notifyService) notifyChange(reason box.UpdateReason, zid id.Zid) { + if chci := srv.infos; chci != nil { + chci <- box.UpdateInfo{Reason: reason, Zid: zid} + } +} + +// NumEntries returns the number of managed zettel. +func (srv *notifyService) NumEntries() (int, error) { + resChan := make(chan resNumEntries) + srv.cmds <- &cmdNumEntries{resChan} + return <-resChan, nil +} + +// GetEntries returns an unsorted list of all current directory entries. +func (srv *notifyService) GetEntries() ([]*directory.Entry, error) { + resChan := make(chan resGetEntries) + srv.cmds <- &cmdGetEntries{resChan} + return <-resChan, nil +} + +// GetEntry returns the entry with the specified zettel id. If there is no such +// zettel id, an empty entry is returned. +func (srv *notifyService) GetEntry(zid id.Zid) (*directory.Entry, error) { + resChan := make(chan resGetEntry) + srv.cmds <- &cmdGetEntry{zid, resChan} + return <-resChan, nil +} + +// GetNew returns an entry with a new zettel id. +func (srv *notifyService) GetNew() (*directory.Entry, error) { + resChan := make(chan resNewEntry) + srv.cmds <- &cmdNewEntry{resChan} + result := <-resChan + return result.entry, result.err +} + +// UpdateEntry notifies the directory of an updated entry. +func (srv *notifyService) UpdateEntry(entry *directory.Entry) error { + resChan := make(chan struct{}) + srv.cmds <- &cmdUpdateEntry{entry, resChan} + <-resChan + return nil +} + +// RenameEntry notifies the directory of an renamed entry. +func (srv *notifyService) RenameEntry(curEntry, newEntry *directory.Entry) error { + resChan := make(chan resRenameEntry) + srv.cmds <- &cmdRenameEntry{curEntry, newEntry, resChan} + return <-resChan +} + +// DeleteEntry removes a zettel id from the directory of entries. +func (srv *notifyService) DeleteEntry(zid id.Zid) error { + resChan := make(chan struct{}) + srv.cmds <- &cmdDeleteEntry{zid, resChan} + <-resChan + return nil +} ADDED box/dirbox/notifydir/service.go Index: box/dirbox/notifydir/service.go ================================================================== --- box/dirbox/notifydir/service.go +++ box/dirbox/notifydir/service.go @@ -0,0 +1,255 @@ +//----------------------------------------------------------------------------- +// Copyright (c) 2020-2021 Detlef Stern +// +// This file is part of zettelstore. +// +// Zettelstore is licensed under the latest version of the EUPL (European Union +// Public License). Please see file LICENSE.txt for your rights and obligations +// under this license. +//----------------------------------------------------------------------------- + +// Package notifydir manages the notified directory part of a dirstore. +package notifydir + +import ( + "log" + "time" + + "zettelstore.de/z/box" + "zettelstore.de/z/box/dirbox/directory" + "zettelstore.de/z/domain/id" +) + +// ping sends every tick a signal to reload the directory list +func ping(tick chan<- struct{}, rescanTime time.Duration, done <-chan struct{}) { + ticker := time.NewTicker(rescanTime) + defer close(tick) + for { + select { + case _, ok := <-ticker.C: + if !ok { + return + } + tick <- struct{}{} + case _, ok := <-done: + if !ok { + ticker.Stop() + return + } + } + } +} + +func newEntry(ev *fileEvent) *directory.Entry { + de := new(directory.Entry) + de.Zid = ev.zid + updateEntry(de, ev) + return de +} + +func updateEntry(de *directory.Entry, ev *fileEvent) { + if ev.ext == "meta" { + de.MetaSpec = directory.MetaSpecFile + de.MetaPath = ev.path + return + } + if de.ContentExt != "" && de.ContentExt != ev.ext { + de.Duplicates = true + return + } + if de.MetaSpec != directory.MetaSpecFile { + if ev.ext == "zettel" { + de.MetaSpec = directory.MetaSpecHeader + } else { + de.MetaSpec = directory.MetaSpecNone + } + } + de.ContentPath = ev.path + de.ContentExt = ev.ext +} + +type dirMap map[id.Zid]*directory.Entry + +func dirMapUpdate(dm dirMap, ev *fileEvent) { + de := dm[ev.zid] + if de == nil { + dm[ev.zid] = newEntry(ev) + return + } + updateEntry(de, ev) +} + +func deleteFromMap(dm dirMap, ev *fileEvent) { + if ev.ext == "meta" { + if entry, ok := dm[ev.zid]; ok { + if entry.MetaSpec == directory.MetaSpecFile { + entry.MetaSpec = directory.MetaSpecNone + return + } + } + } + delete(dm, ev.zid) +} + +// directoryService is the main service. +func (srv *notifyService) directoryService(events <-chan *fileEvent, ready chan<- int) { + curMap := make(dirMap) + var newMap dirMap + for { + select { + case ev, ok := <-events: + if !ok { + return + } + switch ev.status { + case fileStatusReloadStart: + newMap = make(dirMap) + case fileStatusReloadEnd: + curMap = newMap + newMap = nil + if ready != nil { + ready <- len(curMap) + close(ready) + ready = nil + } + srv.notifyChange(box.OnReload, id.Invalid) + case fileStatusError: + log.Println("DIRBOX", "ERROR", ev.err) + case fileStatusUpdate: + srv.processFileUpdateEvent(ev, curMap, newMap) + case fileStatusDelete: + srv.processFileDeleteEvent(ev, curMap, newMap) + } + case cmd, ok := <-srv.cmds: + if ok { + cmd.run(curMap) + } + } + } +} + +func (srv *notifyService) processFileUpdateEvent(ev *fileEvent, curMap, newMap dirMap) { + if newMap != nil { + dirMapUpdate(newMap, ev) + } else { + dirMapUpdate(curMap, ev) + srv.notifyChange(box.OnUpdate, ev.zid) + } +} + +func (srv *notifyService) processFileDeleteEvent(ev *fileEvent, curMap, newMap dirMap) { + if newMap != nil { + deleteFromMap(newMap, ev) + } else { + deleteFromMap(curMap, ev) + srv.notifyChange(box.OnDelete, ev.zid) + } +} + +type dirCmd interface { + run(m dirMap) +} + +type cmdNumEntries struct { + result chan<- resNumEntries +} +type resNumEntries = int + +func (cmd *cmdNumEntries) run(m dirMap) { + cmd.result <- len(m) +} + +type cmdGetEntries struct { + result chan<- resGetEntries +} +type resGetEntries []*directory.Entry + +func (cmd *cmdGetEntries) run(m dirMap) { + res := make([]*directory.Entry, len(m)) + i := 0 + for _, de := range m { + entry := *de + res[i] = &entry + i++ + } + cmd.result <- res +} + +type cmdGetEntry struct { + zid id.Zid + result chan<- resGetEntry +} +type resGetEntry = *directory.Entry + +func (cmd *cmdGetEntry) run(m dirMap) { + entry := m[cmd.zid] + if entry == nil { + cmd.result <- nil + } else { + result := *entry + cmd.result <- &result + } +} + +type cmdNewEntry struct { + result chan<- resNewEntry +} +type resNewEntry struct { + entry *directory.Entry + err error +} + +func (cmd *cmdNewEntry) run(m dirMap) { + zid, err := box.GetNewZid(func(zid id.Zid) (bool, error) { + _, ok := m[zid] + return !ok, nil + }) + if err != nil { + cmd.result <- resNewEntry{nil, err} + return + } + entry := &directory.Entry{Zid: zid} + m[zid] = entry + cmd.result <- resNewEntry{&directory.Entry{Zid: zid}, nil} +} + +type cmdUpdateEntry struct { + entry *directory.Entry + result chan<- struct{} +} + +func (cmd *cmdUpdateEntry) run(m dirMap) { + entry := *cmd.entry + m[entry.Zid] = &entry + cmd.result <- struct{}{} +} + +type cmdRenameEntry struct { + curEntry *directory.Entry + newEntry *directory.Entry + result chan<- resRenameEntry +} + +type resRenameEntry = error + +func (cmd *cmdRenameEntry) run(m dirMap) { + newEntry := *cmd.newEntry + newZid := newEntry.Zid + if _, found := m[newZid]; found { + cmd.result <- &box.ErrInvalidID{Zid: newZid} + return + } + delete(m, cmd.curEntry.Zid) + m[newZid] = &newEntry + cmd.result <- nil +} + +type cmdDeleteEntry struct { + zid id.Zid + result chan<- struct{} +} + +func (cmd *cmdDeleteEntry) run(m dirMap) { + delete(m, cmd.zid) + cmd.result <- struct{}{} +} ADDED box/dirbox/notifydir/watch.go Index: box/dirbox/notifydir/watch.go ================================================================== --- box/dirbox/notifydir/watch.go +++ box/dirbox/notifydir/watch.go @@ -0,0 +1,300 @@ +//----------------------------------------------------------------------------- +// Copyright (c) 2020-2021 Detlef Stern +// +// This file is part of zettelstore. +// +// Zettelstore is licensed under the latest version of the EUPL (European Union +// Public License). Please see file LICENSE.txt for your rights and obligations +// under this license. +//----------------------------------------------------------------------------- + +// Package notifydir manages the notified directory part of a dirstore. +package notifydir + +import ( + "os" + "path/filepath" + "regexp" + "time" + + "github.com/fsnotify/fsnotify" + + "zettelstore.de/z/domain/id" +) + +var validFileName = regexp.MustCompile(`^(\d{14}).*(\.(.+))$`) + +func matchValidFileName(name string) []string { + return validFileName.FindStringSubmatch(name) +} + +type fileStatus int + +const ( + fileStatusNone fileStatus = iota + fileStatusReloadStart + fileStatusReloadEnd + fileStatusError + fileStatusUpdate + fileStatusDelete +) + +type fileEvent struct { + status fileStatus + path string // Full file path + zid id.Zid + ext string // File extension + err error // Error if Status == fileStatusError +} + +type sendResult int + +const ( + sendDone sendResult = iota + sendReload + sendExit +) + +func watchDirectory(directory string, events chan<- *fileEvent, tick <-chan struct{}) { + defer close(events) + + var watcher *fsnotify.Watcher + defer func() { + if watcher != nil { + watcher.Close() + } + }() + + sendEvent := func(ev *fileEvent) sendResult { + select { + case events <- ev: + case _, ok := <-tick: + if ok { + return sendReload + } + return sendExit + } + return sendDone + } + + sendError := func(err error) sendResult { + return sendEvent(&fileEvent{status: fileStatusError, err: err}) + } + + sendFileEvent := func(status fileStatus, path string, match []string) sendResult { + zid, err := id.Parse(match[1]) + if err != nil { + return sendDone + } + event := &fileEvent{ + status: status, + path: path, + zid: zid, + ext: match[3], + } + return sendEvent(event) + } + + reloadStartEvent := &fileEvent{status: fileStatusReloadStart} + reloadEndEvent := &fileEvent{status: fileStatusReloadEnd} + reloadFiles := func() bool { + entries, err := os.ReadDir(directory) + if err != nil { + if res := sendError(err); res != sendDone { + return res == sendReload + } + return true + } + + if res := sendEvent(reloadStartEvent); res != sendDone { + return res == sendReload + } + + if watcher != nil { + watcher.Close() + } + watcher, err = fsnotify.NewWatcher() + if err != nil { + if res := sendError(err); res != sendDone { + return res == sendReload + } + } + + for _, entry := range entries { + if entry.IsDir() { + continue + } + if info, err1 := entry.Info(); err1 != nil || !info.Mode().IsRegular() { + continue + } + name := entry.Name() + match := matchValidFileName(name) + if len(match) > 0 { + path := filepath.Join(directory, name) + if res := sendFileEvent(fileStatusUpdate, path, match); res != sendDone { + return res == sendReload + } + } + } + + if watcher != nil { + err = watcher.Add(directory) + if err != nil { + if res := sendError(err); res != sendDone { + return res == sendReload + } + } + } + if res := sendEvent(reloadEndEvent); res != sendDone { + return res == sendReload + } + return true + } + + handleEvents := func() bool { + const createOps = fsnotify.Create | fsnotify.Write + const deleteOps = fsnotify.Remove | fsnotify.Rename + + for { + select { + case wevent, ok := <-watcher.Events: + if !ok { + return false + } + path := filepath.Clean(wevent.Name) + match := matchValidFileName(filepath.Base(path)) + if len(match) == 0 { + continue + } + if wevent.Op&createOps != 0 { + if fi, err := os.Lstat(path); err != nil || !fi.Mode().IsRegular() { + continue + } + if res := sendFileEvent( + fileStatusUpdate, path, match); res != sendDone { + return res == sendReload + } + } + if wevent.Op&deleteOps != 0 { + if res := sendFileEvent( + fileStatusDelete, path, match); res != sendDone { + return res == sendReload + } + } + case err, ok := <-watcher.Errors: + if !ok { + return false + } + if res := sendError(err); res != sendDone { + return res == sendReload + } + case _, ok := <-tick: + return ok + } + } + } + + for { + if !reloadFiles() { + return + } + if watcher == nil { + if _, ok := <-tick; !ok { + return + } + } else { + if !handleEvents() { + return + } + } + } +} + +func sendCollectedEvents(out chan<- *fileEvent, events []*fileEvent) { + for _, ev := range events { + if ev.status != fileStatusNone { + out <- ev + } + } +} + +func addEvent(events []*fileEvent, ev *fileEvent) []*fileEvent { + switch ev.status { + case fileStatusNone: + return events + case fileStatusReloadStart: + events = events[0:0] + case fileStatusUpdate, fileStatusDelete: + if len(events) > 0 && mergeEvents(events, ev) { + return events + } + } + return append(events, ev) +} + +func mergeEvents(events []*fileEvent, ev *fileEvent) bool { + for i := len(events) - 1; i >= 0; i-- { + oev := events[i] + switch oev.status { + case fileStatusReloadStart, fileStatusReloadEnd: + return false + case fileStatusUpdate, fileStatusDelete: + if ev.path == oev.path { + if ev.status == oev.status { + return true + } + oev.status = fileStatusNone + return false + } + } + } + return false +} + +func collectEvents(out chan<- *fileEvent, in <-chan *fileEvent) { + defer close(out) + + var sendTime time.Time + sendTimeSet := false + ticker := time.NewTicker(500 * time.Millisecond) + defer ticker.Stop() + + events := make([]*fileEvent, 0, 32) + buffer := false + for { + select { + case ev, ok := <-in: + if !ok { + sendCollectedEvents(out, events) + return + } + if ev.status == fileStatusReloadStart { + buffer = false + events = events[0:0] + } + if buffer { + if !sendTimeSet { + sendTime = time.Now().Add(1500 * time.Millisecond) + sendTimeSet = true + } + events = addEvent(events, ev) + if len(events) > 1024 { + sendCollectedEvents(out, events) + events = events[0:0] + sendTimeSet = false + } + continue + } + out <- ev + if ev.status == fileStatusReloadEnd { + buffer = true + } + case now := <-ticker.C: + if sendTimeSet && now.After(sendTime) { + sendCollectedEvents(out, events) + events = events[0:0] + sendTimeSet = false + } + } + } +} ADDED box/dirbox/notifydir/watch_test.go Index: box/dirbox/notifydir/watch_test.go ================================================================== --- box/dirbox/notifydir/watch_test.go +++ box/dirbox/notifydir/watch_test.go @@ -0,0 +1,56 @@ +//----------------------------------------------------------------------------- +// Copyright (c) 2020-2021 Detlef Stern +// +// This file is part of zettelstore. +// +// Zettelstore is licensed under the latest version of the EUPL (European Union +// Public License). Please see file LICENSE.txt for your rights and obligations +// under this license. +//----------------------------------------------------------------------------- + +// Package notifydir manages the notified directory part of a dirstore. +package notifydir + +import "testing" + +func sameStringSlices(sl1, sl2 []string) bool { + if len(sl1) != len(sl2) { + return false + } + for i := 0; i < len(sl1); i++ { + if sl1[i] != sl2[i] { + return false + } + } + return true +} + +func TestMatchValidFileName(t *testing.T) { + t.Parallel() + testcases := []struct { + name string + exp []string + }{ + {"", []string{}}, + {".txt", []string{}}, + {"12345678901234.txt", []string{"12345678901234", ".txt", "txt"}}, + {"12345678901234abc.txt", []string{"12345678901234", ".txt", "txt"}}, + {"12345678901234.abc.txt", []string{"12345678901234", ".txt", "txt"}}, + } + + for i, tc := range testcases { + got := matchValidFileName(tc.name) + if len(got) == 0 { + if len(tc.exp) > 0 { + t.Errorf("TC=%d, name=%q, exp=%v, got=%v", i, tc.name, tc.exp, got) + } + } else { + if got[0] != tc.name { + t.Errorf("TC=%d, name=%q, got=%v", i, tc.name, got) + } + if !sameStringSlices(got[1:], tc.exp) { + t.Errorf("TC=%d, name=%q, exp=%v, got=%v", i, tc.name, tc.exp, got) + } + } + } +} Index: box/dirbox/service.go ================================================================== --- box/dirbox/service.go +++ box/dirbox/service.go @@ -1,390 +1,290 @@ //----------------------------------------------------------------------------- -// Copyright (c) 2020-present Detlef Stern +// Copyright (c) 2020-2021 Detlef Stern // -// This file is part of Zettelstore. +// This file is part of zettelstore. // // Zettelstore is licensed under the latest version of the EUPL (European Union // Public License). Please see file LICENSE.txt for your rights and obligations // under this license. -// -// SPDX-License-Identifier: EUPL-1.2 -// SPDX-FileCopyrightText: 2020-present Detlef Stern //----------------------------------------------------------------------------- +// Package dirbox provides a directory-based zettel box. package dirbox import ( - "context" - "fmt" - "io" - "os" - "path/filepath" - "time" - - "t73f.de/r/zsc/input" - "zettelstore.de/z/box/filebox" - "zettelstore.de/z/box/notify" - "zettelstore.de/z/kernel" - "zettelstore.de/z/logger" - "zettelstore.de/z/zettel" - "zettelstore.de/z/zettel/id" - "zettelstore.de/z/zettel/meta" -) - -func fileService(i uint32, log *logger.Logger, dirPath string, cmds <-chan fileCmd) { - // Something may panic. Ensure a running service. - defer func() { - if ri := recover(); ri != nil { - kernel.Main.LogRecover("FileService", ri) - go fileService(i, log, dirPath, cmds) - } - }() - - log.Debug().Uint("i", uint64(i)).Str("dirpath", dirPath).Msg("File service started") - for cmd := range cmds { - cmd.run(dirPath) - } - log.Debug().Uint("i", uint64(i)).Str("dirpath", dirPath).Msg("File service stopped") + "os" + + "zettelstore.de/z/box/dirbox/directory" + "zettelstore.de/z/box/filebox" + "zettelstore.de/z/domain" + "zettelstore.de/z/domain/id" + "zettelstore.de/z/domain/meta" + "zettelstore.de/z/input" +) + +func fileService(num uint32, cmds <-chan fileCmd) { + for cmd := range cmds { + cmd.run() + } } type fileCmd interface { - run(string) + run() } -const serviceTimeout = 5 * time.Second // must be shorter than the web servers timeout values for reading+writing. - -// COMMAND: srvGetMeta ---------------------------------------- +// COMMAND: getMeta ---------------------------------------- // // Retrieves the meta data from a zettel. -func (dp *dirBox) srvGetMeta(ctx context.Context, entry *notify.DirEntry, zid id.Zid) (*meta.Meta, error) { - rc := make(chan resGetMeta, 1) +func getMeta(dp *dirBox, entry *directory.Entry, zid id.Zid) (*meta.Meta, error) { + rc := make(chan resGetMeta) dp.getFileChan(zid) <- &fileGetMeta{entry, rc} - ctx, cancel := context.WithTimeout(ctx, serviceTimeout) - defer cancel() - select { - case res := <-rc: - return res.meta, res.err - case <-ctx.Done(): - return nil, ctx.Err() - } + res := <-rc + close(rc) + return res.meta, res.err } type fileGetMeta struct { - entry *notify.DirEntry + entry *directory.Entry rc chan<- resGetMeta } type resGetMeta struct { meta *meta.Meta err error } -func (cmd *fileGetMeta) run(dirPath string) { +func (cmd *fileGetMeta) run() { + entry := cmd.entry 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 == "" { - 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 { - m, err = parseMetaFile(zid, filepath.Join(dirPath, metaName)) + switch entry.MetaSpec { + case directory.MetaSpecFile: + m, err = parseMetaFile(entry.Zid, entry.MetaPath) + case directory.MetaSpecHeader: + m, _, err = parseMetaContentFile(entry.Zid, entry.ContentPath) + default: + m = filebox.CalcDefaultMeta(entry.Zid, entry.ContentExt) } if err == nil { cmdCleanupMeta(m, entry) } cmd.rc <- resGetMeta{m, err} } -// COMMAND: srvGetMetaContent ---------------------------------------- +// COMMAND: getMetaContent ---------------------------------------- // // Retrieves the meta data and the content of a zettel. -func (dp *dirBox) srvGetMetaContent(ctx context.Context, entry *notify.DirEntry, zid id.Zid) (*meta.Meta, []byte, error) { - rc := make(chan resGetMetaContent, 1) +func getMetaContent(dp *dirBox, entry *directory.Entry, zid id.Zid) (*meta.Meta, string, error) { + rc := make(chan resGetMetaContent) dp.getFileChan(zid) <- &fileGetMetaContent{entry, rc} - ctx, cancel := context.WithTimeout(ctx, serviceTimeout) - defer cancel() - select { - case res := <-rc: - return res.meta, res.content, res.err - case <-ctx.Done(): - return nil, nil, ctx.Err() - } + res := <-rc + close(rc) + return res.meta, res.content, res.err } type fileGetMetaContent struct { - entry *notify.DirEntry + entry *directory.Entry rc chan<- resGetMetaContent } type resGetMetaContent struct { meta *meta.Meta - content []byte + content string err error } -func (cmd *fileGetMetaContent) run(dirPath string) { +func (cmd *fileGetMetaContent) run() { var m *meta.Meta - var content []byte + var content string var err error entry := cmd.entry - zid := entry.Zid - contentName := entry.ContentName - contentExt := entry.ContentExt - contentPath := filepath.Join(dirPath, contentName) - if metaName := entry.MetaName; metaName == "" { - if contentName == "" || contentExt == "" { - 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) - } - } else { - m, err = parseMetaFile(zid, filepath.Join(dirPath, metaName)) - if contentName != "" { - var err1 error - content, err1 = os.ReadFile(contentPath) - if err == nil { - err = err1 - } - } + switch entry.MetaSpec { + case directory.MetaSpecFile: + m, err = parseMetaFile(entry.Zid, entry.MetaPath) + if err == nil { + content, err = readFileContent(entry.ContentPath) + } + case directory.MetaSpecHeader: + m, content, err = parseMetaContentFile(entry.Zid, entry.ContentPath) + default: + m = filebox.CalcDefaultMeta(entry.Zid, entry.ContentExt) + content, err = readFileContent(entry.ContentPath) } if err == nil { cmdCleanupMeta(m, entry) } cmd.rc <- resGetMetaContent{m, content, err} } -// COMMAND: srvSetZettel ---------------------------------------- -// -// Writes a new or exsting zettel. - -func (dp *dirBox) srvSetZettel(ctx context.Context, entry *notify.DirEntry, zettel zettel.Zettel) error { - rc := make(chan resSetZettel, 1) - dp.getFileChan(zettel.Meta.Zid) <- &fileSetZettel{entry, zettel, rc} - ctx, cancel := context.WithTimeout(ctx, serviceTimeout) - defer cancel() - select { - case err := <-rc: - return err - case <-ctx.Done(): - return ctx.Err() - } -} - -type fileSetZettel struct { - entry *notify.DirEntry - zettel zettel.Zettel - rc chan<- resSetZettel -} -type resSetZettel = error - -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 == "" { - 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) - if err == nil && contentName != "" { - err = writeFileContent(filepath.Join(dirPath, contentName), content) - } - cmd.rc <- err -} - -func writeMetaFile(metaPath string, m *meta.Meta) error { - metaFile, err := openFileWrite(metaPath) - if err != nil { - return err - } - err = writeFileZid(metaFile, m.Zid) - if err == nil { - _, err = m.WriteComputed(metaFile) - } - if err1 := metaFile.Close(); err == nil { - err = err1 - } - return err -} - -func writeZettelFile(contentPath string, m *meta.Meta, content []byte) error { - zettelFile, err := openFileWrite(contentPath) - if err != nil { - return err - } - err = writeMetaHeader(zettelFile, m) - if err == nil { - _, err = zettelFile.Write(content) - } - if err1 := zettelFile.Close(); err == nil { - err = err1 - } - return err -} - -var ( - newline = []byte{'\n'} - yamlSep = []byte{'-', '-', '-', '\n'} -) - -func writeMetaHeader(w io.Writer, m *meta.Meta) (err error) { - if m.YamlSep { - _, err = w.Write(yamlSep) - if err != nil { - return err - } - } - err = writeFileZid(w, m.Zid) - if err != nil { - return err - } - _, err = m.WriteComputed(w) - if err != nil { - return err - } - if m.YamlSep { - _, err = w.Write(yamlSep) - } else { - _, err = w.Write(newline) - } - return err -} - -// COMMAND: srvDeleteZettel ---------------------------------------- -// -// Deletes an existing zettel. - -func (dp *dirBox) srvDeleteZettel(ctx context.Context, entry *notify.DirEntry, zid id.Zid) error { - rc := make(chan resDeleteZettel, 1) - dp.getFileChan(zid) <- &fileDeleteZettel{entry, rc} - ctx, cancel := context.WithTimeout(ctx, serviceTimeout) - defer cancel() - select { - case err := <-rc: - return err - case <-ctx.Done(): - return ctx.Err() - } -} - -type fileDeleteZettel struct { - entry *notify.DirEntry - rc chan<- resDeleteZettel -} -type resDeleteZettel = error - -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 == "" { - err = fmt.Errorf("no meta, no content in deleteZettel, zid=%v", entry.Zid) - } else { - err = os.Remove(contentPath) - } - } else { - if contentName != "" { - err = os.Remove(contentPath) - } - err1 := os.Remove(filepath.Join(dirPath, metaName)) - if err == nil { - err = err1 - } - } - for _, dupName := range entry.UselessFiles { - err1 := os.Remove(filepath.Join(dirPath, dupName)) - if err == nil { - err = err1 - } +// COMMAND: setZettel ---------------------------------------- +// +// Writes a new or exsting zettel. + +func setZettel(dp *dirBox, entry *directory.Entry, zettel domain.Zettel) error { + rc := make(chan resSetZettel) + dp.getFileChan(zettel.Meta.Zid) <- &fileSetZettel{entry, zettel, rc} + err := <-rc + close(rc) + return err +} + +type fileSetZettel struct { + entry *directory.Entry + zettel domain.Zettel + rc chan<- resSetZettel +} +type resSetZettel = error + +func (cmd *fileSetZettel) run() { + var err error + switch cmd.entry.MetaSpec { + case directory.MetaSpecFile: + err = cmd.runMetaSpecFile() + case directory.MetaSpecHeader: + err = cmd.runMetaSpecHeader() + case directory.MetaSpecNone: + // TODO: if meta has some additional infos: write meta to new .meta; + // update entry in dir + err = writeFileContent(cmd.entry.ContentPath, cmd.zettel.Content.AsString()) + default: + panic("TODO: ???") + } + cmd.rc <- err +} + +func (cmd *fileSetZettel) runMetaSpecFile() error { + f, err := openFileWrite(cmd.entry.MetaPath) + if err == nil { + err = writeFileZid(f, cmd.zettel.Meta.Zid) + if err == nil { + _, err = cmd.zettel.Meta.Write(f, true) + if err1 := f.Close(); err == nil { + err = err1 + } + if err == nil { + err = writeFileContent(cmd.entry.ContentPath, cmd.zettel.Content.AsString()) + } + } + } + return err +} + +func (cmd *fileSetZettel) runMetaSpecHeader() error { + f, err := openFileWrite(cmd.entry.ContentPath) + if err == nil { + err = writeFileZid(f, cmd.zettel.Meta.Zid) + if err == nil { + _, err = cmd.zettel.Meta.WriteAsHeader(f, true) + if err == nil { + _, err = f.WriteString(cmd.zettel.Content.AsString()) + if err1 := f.Close(); err == nil { + err = err1 + } + } + } + } + return err +} + +// COMMAND: deleteZettel ---------------------------------------- +// +// Deletes an existing zettel. + +func deleteZettel(dp *dirBox, entry *directory.Entry, zid id.Zid) error { + rc := make(chan resDeleteZettel) + dp.getFileChan(zid) <- &fileDeleteZettel{entry, rc} + err := <-rc + close(rc) + return err +} + +type fileDeleteZettel struct { + entry *directory.Entry + rc chan<- resDeleteZettel +} +type resDeleteZettel = error + +func (cmd *fileDeleteZettel) run() { + var err error + + switch cmd.entry.MetaSpec { + case directory.MetaSpecFile: + err1 := os.Remove(cmd.entry.MetaPath) + err = os.Remove(cmd.entry.ContentPath) + if err == nil { + err = err1 + } + case directory.MetaSpecHeader: + err = os.Remove(cmd.entry.ContentPath) + case directory.MetaSpecNone: + err = os.Remove(cmd.entry.ContentPath) + default: + panic("TODO: ???") } cmd.rc <- err } // Utility functions ---------------------------------------- + +func readFileContent(path string) (string, error) { + data, err := os.ReadFile(path) + if err != nil { + return "", err + } + return string(data), nil +} func parseMetaFile(zid id.Zid, path string) (*meta.Meta, error) { - src, err := os.ReadFile(path) + src, err := readFileContent(path) if err != nil { return nil, err } inp := input.NewInput(src) return meta.NewFromInput(zid, inp), nil } -func parseMetaContentFile(zid id.Zid, path string) (*meta.Meta, []byte, error) { - src, err := os.ReadFile(path) +func parseMetaContentFile(zid id.Zid, path string) (*meta.Meta, string, error) { + src, err := readFileContent(path) if err != nil { - return nil, nil, err + return nil, "", err } inp := input.NewInput(src) meta := meta.NewFromInput(zid, inp) return meta, src[inp.Pos:], nil } -func cmdCleanupMeta(m *meta.Meta, entry *notify.DirEntry) { +func cmdCleanupMeta(m *meta.Meta, entry *directory.Entry) { filebox.CleanupMeta( m, - entry.Zid, - entry.ContentExt, - entry.MetaName != "", - entry.UselessFiles, + entry.Zid, entry.ContentExt, + entry.MetaSpec == directory.MetaSpecFile, + entry.Duplicates, ) } func openFileWrite(path string) (*os.File, error) { return os.OpenFile(path, os.O_WRONLY|os.O_CREATE|os.O_TRUNC, 0600) } -func writeFileZid(w io.Writer, zid id.Zid) error { - _, err := io.WriteString(w, "id: ") +func writeFileZid(f *os.File, zid id.Zid) error { + _, err := f.WriteString("id: ") if err == nil { - _, err = w.Write(zid.Bytes()) + _, err = f.Write(zid.Bytes()) if err == nil { - _, err = io.WriteString(w, "\n") + _, err = f.WriteString("\n") } } return err } -func writeFileContent(path string, content []byte) error { +func writeFileContent(path, content string) error { f, err := openFileWrite(path) if err == nil { - _, err = f.Write(content) + _, err = f.WriteString(content) if err1 := f.Close(); err == nil { err = err1 } } return err } ADDED box/dirbox/simpledir/simpledir.go Index: box/dirbox/simpledir/simpledir.go ================================================================== --- box/dirbox/simpledir/simpledir.go +++ box/dirbox/simpledir/simpledir.go @@ -0,0 +1,185 @@ +//----------------------------------------------------------------------------- +// Copyright (c) 2021 Detlef Stern +// +// This file is part of zettelstore. +// +// Zettelstore is licensed under the latest version of the EUPL (European Union +// Public License). Please see file LICENSE.txt for your rights and obligations +// under this license. +//----------------------------------------------------------------------------- + +// Package simpledir manages the directory part of a dirstore. +package simpledir + +import ( + "os" + "path/filepath" + "regexp" + "sync" + + "zettelstore.de/z/box" + "zettelstore.de/z/box/dirbox/directory" + "zettelstore.de/z/domain/id" +) + +// simpleService specifies a directory service without scanning. +type simpleService struct { + dirPath string + mx sync.Mutex +} + +// NewService creates a new directory service. +func NewService(directoryPath string) directory.Service { + return &simpleService{ + dirPath: directoryPath, + } +} + +func (ss *simpleService) Start() error { + ss.mx.Lock() + defer ss.mx.Unlock() + _, err := os.ReadDir(ss.dirPath) + return err +} + +func (ss *simpleService) Stop() error { + return nil +} + +func (ss *simpleService) NumEntries() (int, error) { + ss.mx.Lock() + defer ss.mx.Unlock() + entries, err := ss.getEntries() + if err == nil { + return len(entries), nil + } + return 0, err +} + +func (ss *simpleService) GetEntries() ([]*directory.Entry, error) { + ss.mx.Lock() + defer ss.mx.Unlock() + entrySet, err := ss.getEntries() + if err != nil { + return nil, err + } + result := make([]*directory.Entry, 0, len(entrySet)) + for _, entry := range entrySet { + result = append(result, entry) + } + return result, nil +} +func (ss *simpleService) getEntries() (map[id.Zid]*directory.Entry, error) { + dirEntries, err := os.ReadDir(ss.dirPath) + if err != nil { + return nil, err + } + entrySet := make(map[id.Zid]*directory.Entry) + for _, dirEntry := range dirEntries { + if dirEntry.IsDir() { + continue + } + if info, err1 := dirEntry.Info(); err1 != nil || !info.Mode().IsRegular() { + continue + } + name := dirEntry.Name() + match := matchValidFileName(name) + if len(match) == 0 { + continue + } + zid, err := id.Parse(match[1]) + if err != nil { + continue + } + var entry *directory.Entry + if e, ok := entrySet[zid]; ok { + entry = e + } else { + entry = &directory.Entry{Zid: zid} + entrySet[zid] = entry + } + updateEntry(entry, filepath.Join(ss.dirPath, name), match[3]) + } + return entrySet, nil +} + +var validFileName = regexp.MustCompile(`^(\d{14}).*(\.(.+))$`) + +func matchValidFileName(name string) []string { + return validFileName.FindStringSubmatch(name) +} + +func updateEntry(entry *directory.Entry, path, ext string) { + if ext == "meta" { + entry.MetaSpec = directory.MetaSpecFile + entry.MetaPath = path + } else if entry.ContentExt != "" && entry.ContentExt != ext { + entry.Duplicates = true + } else { + if entry.MetaSpec != directory.MetaSpecFile { + if ext == "zettel" { + entry.MetaSpec = directory.MetaSpecHeader + } else { + entry.MetaSpec = directory.MetaSpecNone + } + } + entry.ContentPath = path + entry.ContentExt = ext + } +} + +func (ss *simpleService) GetEntry(zid id.Zid) (*directory.Entry, error) { + ss.mx.Lock() + defer ss.mx.Unlock() + return ss.getEntry(zid) +} +func (ss *simpleService) getEntry(zid id.Zid) (*directory.Entry, error) { + pattern := filepath.Join(ss.dirPath, zid.String()) + "*.*" + paths, err := filepath.Glob(pattern) + if err != nil { + return nil, err + } + if len(paths) == 0 { + return nil, nil + } + entry := &directory.Entry{Zid: zid} + for _, path := range paths { + ext := filepath.Ext(path) + if len(ext) > 0 && ext[0] == '.' { + ext = ext[1:] + } + updateEntry(entry, path, ext) + } + return entry, nil +} + +func (ss *simpleService) GetNew() (*directory.Entry, error) { + ss.mx.Lock() + defer ss.mx.Unlock() + zid, err := box.GetNewZid(func(zid id.Zid) (bool, error) { + entry, err := ss.getEntry(zid) + if err != nil { + return false, nil + } + return !entry.IsValid(), nil + }) + if err != nil { + return nil, err + } + return &directory.Entry{Zid: zid}, nil +} + +func (ss *simpleService) UpdateEntry(entry *directory.Entry) error { + // Nothing to to, since the actual file update is done by dirbox. + return nil +} + +func (ss *simpleService) RenameEntry(curEntry, newEntry *directory.Entry) error { + // Nothing to to, since the actual file rename is done by dirbox. + return nil +} + +func (ss *simpleService) DeleteEntry(zid id.Zid) error { + // Nothing to to, since the actual file delete is done by dirbox. + return nil +} Index: box/filebox/filebox.go ================================================================== --- box/filebox/filebox.go +++ box/filebox/filebox.go @@ -1,16 +1,13 @@ //----------------------------------------------------------------------------- -// Copyright (c) 2021-present Detlef Stern +// Copyright (c) 2021 Detlef Stern // -// This file is part of Zettelstore. +// This file is part of zettelstore. // // Zettelstore is licensed under the latest version of the EUPL (European Union // Public License). Please see file LICENSE.txt for your rights and obligations // under this license. -// -// SPDX-License-Identifier: EUPL-1.2 -// SPDX-FileCopyrightText: 2021-present Detlef Stern //----------------------------------------------------------------------------- // Package filebox provides boxes that are stored in a file. package filebox @@ -18,16 +15,14 @@ "errors" "net/url" "path/filepath" "strings" - "t73f.de/r/zsc/api" "zettelstore.de/z/box" "zettelstore.de/z/box/manager" - "zettelstore.de/z/kernel" - "zettelstore.de/z/zettel/id" - "zettelstore.de/z/zettel/meta" + "zettelstore.de/z/domain/id" + "zettelstore.de/z/domain/meta" ) func init() { manager.Register("file", func(u *url.URL, cdata *manager.ConnectData) (box.ManagedBox, error) { path := getFilepathFromURL(u) @@ -34,16 +29,13 @@ ext := strings.ToLower(filepath.Ext(path)) if ext != ".zip" { return nil, errors.New("unknown extension '" + ext + "' in box URL: " + u.String()) } return &zipBox{ - log: kernel.Main.GetLogger(kernel.BoxService).Clone(). - Str("box", "zip").Int("boxnum", int64(cdata.Number)).Child(), number: cdata.Number, name: path, enricher: cdata.Enricher, - notify: cdata.Notify, }, nil }) } func getFilepathFromURL(u *url.URL) string { @@ -72,26 +64,31 @@ } // CalcDefaultMeta returns metadata with default values for the given entry. func CalcDefaultMeta(zid id.Zid, ext string) *meta.Meta { m := meta.New(zid) - m.Set(api.KeySyntax, calculateSyntax(ext)) + m.Set(meta.KeyTitle, zid.String()) + m.Set(meta.KeySyntax, calculateSyntax(ext)) return m } // CleanupMeta enhances the given metadata. -func CleanupMeta(m *meta.Meta, zid id.Zid, ext string, inMeta bool, uselessFiles []string) { +func CleanupMeta(m *meta.Meta, zid id.Zid, ext string, inMeta, duplicates bool) { + if title, ok := m.Get(meta.KeyTitle); !ok || title == "" { + m.Set(meta.KeyTitle, zid.String()) + } + if inMeta { - if syntax, ok := m.Get(api.KeySyntax); !ok || syntax == "" { + if syntax, ok := m.Get(meta.KeySyntax); !ok || syntax == "" { dm := CalcDefaultMeta(zid, ext) - syntax, ok = dm.Get(api.KeySyntax) + syntax, ok = dm.Get(meta.KeySyntax) if !ok { panic("Default meta must contain syntax") } - m.Set(api.KeySyntax, syntax) + m.Set(meta.KeySyntax, syntax) } } - if len(uselessFiles) > 0 { - m.Set(api.KeyUselessFiles, strings.Join(uselessFiles, " ")) + if duplicates { + m.Set(meta.KeyDuplicates, meta.ValueTrue) } } Index: box/filebox/zipbox.go ================================================================== --- box/filebox/zipbox.go +++ box/filebox/zipbox.go @@ -1,232 +1,241 @@ //----------------------------------------------------------------------------- -// Copyright (c) 2021-present Detlef Stern +// Copyright (c) 2021 Detlef Stern // -// This file is part of Zettelstore. +// This file is part of zettelstore. // // Zettelstore is licensed under the latest version of the EUPL (European Union // Public License). Please see file LICENSE.txt for your rights and obligations // under this license. -// -// SPDX-License-Identifier: EUPL-1.2 -// SPDX-FileCopyrightText: 2021-present Detlef Stern //----------------------------------------------------------------------------- +// Package filebox provides boxes that are stored in a file. package filebox import ( "archive/zip" "context" - "fmt" "io" + "regexp" "strings" - "t73f.de/r/zsc/input" "zettelstore.de/z/box" - "zettelstore.de/z/box/notify" - "zettelstore.de/z/logger" - "zettelstore.de/z/query" - "zettelstore.de/z/zettel" - "zettelstore.de/z/zettel/id" - "zettelstore.de/z/zettel/meta" + "zettelstore.de/z/domain" + "zettelstore.de/z/domain/id" + "zettelstore.de/z/domain/meta" + "zettelstore.de/z/input" + "zettelstore.de/z/search" ) + +var validFileName = regexp.MustCompile(`^(\d{14}).*(\.(.+))$`) + +func matchValidFileName(name string) []string { + return validFileName.FindStringSubmatch(name) +} + +type zipEntry struct { + metaName string + contentName string + contentExt string // (normalized) file extension of zettel content + metaInHeader bool +} type zipBox struct { - log *logger.Logger number int name string enricher box.Enricher - notify chan<- box.UpdateInfo - dirSrv *notify.DirService -} - -func (zb *zipBox) Location() string { - if strings.HasPrefix(zb.name, "/") { - return "file://" + zb.name - } - return "file:" + zb.name -} - -func (zb *zipBox) State() box.StartState { - if ds := zb.dirSrv; ds != nil { - switch ds.State() { - case notify.DsCreated: - return box.StartStateStopped - case notify.DsStarting: - return box.StartStateStarting - case notify.DsWorking: - return box.StartStateStarted - case notify.DsMissing: - return box.StartStateStarted - case notify.DsStopping: - return box.StartStateStopping - } - } - return box.StartStateStopped -} - -func (zb *zipBox) Start(context.Context) error { - reader, err := zip.OpenReader(zb.name) + zettel map[id.Zid]*zipEntry // no lock needed, because read-only after creation +} + +func (zp *zipBox) Location() string { + if strings.HasPrefix(zp.name, "/") { + return "file://" + zp.name + } + return "file:" + zp.name +} + +func (zp *zipBox) Start(ctx context.Context) error { + reader, err := zip.OpenReader(zp.name) if err != nil { return err } - reader.Close() - zipNotifier := notify.NewSimpleZipNotifier(zb.log, zb.name) - zb.dirSrv = notify.NewDirService(zb, zb.log, zipNotifier, zb.notify) - zb.dirSrv.Start() + defer reader.Close() + zp.zettel = make(map[id.Zid]*zipEntry) + for _, f := range reader.File { + match := matchValidFileName(f.Name) + if len(match) < 1 { + continue + } + zid, err := id.Parse(match[1]) + if err != nil { + continue + } + zp.addFile(zid, f.Name, match[3]) + } + return nil +} + +func (zp *zipBox) addFile(zid id.Zid, name, ext string) { + entry := zp.zettel[zid] + if entry == nil { + entry = &zipEntry{} + zp.zettel[zid] = entry + } + switch ext { + case "zettel": + if entry.contentExt == "" { + entry.contentName = name + entry.contentExt = ext + entry.metaInHeader = true + } + case "meta": + entry.metaName = name + entry.metaInHeader = false + default: + if entry.contentExt == "" { + entry.contentExt = ext + entry.contentName = name + } + } +} + +func (zp *zipBox) Stop(ctx context.Context) error { + zp.zettel = nil return nil } -func (zb *zipBox) Refresh(_ context.Context) { - zb.dirSrv.Refresh() - zb.log.Trace().Msg("Refresh") -} - -func (zb *zipBox) Stop(context.Context) { - zb.dirSrv.Stop() - zb.dirSrv = nil -} - -func (zb *zipBox) GetZettel(_ context.Context, zid id.Zid) (zettel.Zettel, error) { - entry := zb.dirSrv.GetDirEntry(zid) - if !entry.IsValid() { - return zettel.Zettel{}, box.ErrZettelNotFound{Zid: zid} - } - reader, err := zip.OpenReader(zb.name) - if err != nil { - return zettel.Zettel{}, err +func (zp *zipBox) CanCreateZettel(ctx context.Context) bool { return false } + +func (zp *zipBox) CreateZettel(ctx context.Context, zettel domain.Zettel) (id.Zid, error) { + return id.Invalid, box.ErrReadOnly +} + +func (zp *zipBox) GetZettel(ctx context.Context, zid id.Zid) (domain.Zettel, error) { + entry, ok := zp.zettel[zid] + if !ok { + return domain.Zettel{}, box.ErrNotFound + } + reader, err := zip.OpenReader(zp.name) + if err != nil { + return domain.Zettel{}, err } defer reader.Close() var m *meta.Meta - var src []byte - var inMeta bool - - contentName := entry.ContentName - if metaName := entry.MetaName; metaName == "" { - if contentName == "" { - 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 - } - if entry.HasMetaInContent() { - inp := input.NewInput(src) - m = meta.NewFromInput(zid, inp) - src = src[inp.Pos:] - } else { - m = CalcDefaultMeta(zid, entry.ContentExt) - } - } else { - m, err = readZipMetaFile(reader, zid, metaName) - if err != nil { - return zettel.Zettel{}, err - } - inMeta = true - if contentName != "" { - src, err = readZipFileContent(reader, entry.ContentName) - if err != nil { - return zettel.Zettel{}, err - } - } - } - - CleanupMeta(m, zid, entry.ContentExt, inMeta, entry.UselessFiles) - zb.log.Trace().Zid(zid).Msg("GetZettel") - return zettel.Zettel{Meta: m, Content: zettel.NewContent(src)}, nil -} - -func (zb *zipBox) HasZettel(_ context.Context, zid id.Zid) bool { - return zb.dirSrv.GetDirEntry(zid).IsValid() -} - -func (zb *zipBox) ApplyZid(_ context.Context, handle box.ZidFunc, constraint query.RetrievePredicate) error { - entries := zb.dirSrv.GetDirEntries(constraint) - zb.log.Trace().Int("entries", int64(len(entries))).Msg("ApplyZid") - for _, entry := range entries { - handle(entry.Zid) - } - return nil -} - -func (zb *zipBox) ApplyMeta(ctx context.Context, handle box.MetaFunc, constraint query.RetrievePredicate) error { - reader, err := zip.OpenReader(zb.name) - if err != nil { - return err - } - defer reader.Close() - entries := zb.dirSrv.GetDirEntries(constraint) - zb.log.Trace().Int("entries", int64(len(entries))).Msg("ApplyMeta") - for _, entry := range entries { - if !constraint(entry.Zid) { - continue - } - m, err2 := zb.readZipMeta(reader, entry.Zid, entry) - if err2 != nil { - continue - } - zb.enricher.Enrich(ctx, m, zb.number) - handle(m) - } - return nil -} - -func (zb *zipBox) AllowRenameZettel(_ context.Context, zid id.Zid) bool { - entry := zb.dirSrv.GetDirEntry(zid) - return !entry.IsValid() -} - -func (zb *zipBox) RenameZettel(_ context.Context, curZid, newZid id.Zid) error { - err := box.ErrReadOnly - if curZid == newZid { - err = nil - } - curEntry := zb.dirSrv.GetDirEntry(curZid) - if !curEntry.IsValid() { - err = box.ErrZettelNotFound{Zid: curZid} - } - zb.log.Trace().Err(err).Msg("RenameZettel") - return err -} - -func (*zipBox) CanDeleteZettel(context.Context, id.Zid) bool { return false } - -func (zb *zipBox) DeleteZettel(_ context.Context, zid id.Zid) error { - err := box.ErrReadOnly - entry := zb.dirSrv.GetDirEntry(zid) - if !entry.IsValid() { - err = box.ErrZettelNotFound{Zid: zid} - } - zb.log.Trace().Err(err).Msg("DeleteZettel") - return err -} - -func (zb *zipBox) ReadStats(st *box.ManagedBoxStats) { - st.ReadOnly = true - st.Zettel = zb.dirSrv.NumDirEntries() - zb.log.Trace().Int("zettel", int64(st.Zettel)).Msg("ReadStats") -} - -func (zb *zipBox) readZipMeta(reader *zip.ReadCloser, zid id.Zid, entry *notify.DirEntry) (m *meta.Meta, err error) { - var inMeta bool - if metaName := entry.MetaName; metaName == "" { - contentName := entry.ContentName - contentExt := entry.ContentExt - if contentName == "" || contentExt == "" { - 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 { - m, err = readZipMetaFile(reader, zid, metaName) - } - if err == nil { - CleanupMeta(m, zid, entry.ContentExt, inMeta, entry.UselessFiles) + var src string + var inMeta bool + if entry.metaInHeader { + src, err = readZipFileContent(reader, entry.contentName) + if err != nil { + return domain.Zettel{}, err + } + inp := input.NewInput(src) + m = meta.NewFromInput(zid, inp) + src = src[inp.Pos:] + } else if metaName := entry.metaName; metaName != "" { + m, err = readZipMetaFile(reader, zid, metaName) + if err != nil { + return domain.Zettel{}, err + } + src, err = readZipFileContent(reader, entry.contentName) + if err != nil { + return domain.Zettel{}, err + } + inMeta = true + } else { + m = CalcDefaultMeta(zid, entry.contentExt) + } + CleanupMeta(m, zid, entry.contentExt, inMeta, false) + return domain.Zettel{Meta: m, Content: domain.NewContent(src)}, nil +} + +func (zp *zipBox) GetMeta(ctx context.Context, zid id.Zid) (*meta.Meta, error) { + entry, ok := zp.zettel[zid] + if !ok { + return nil, box.ErrNotFound + } + reader, err := zip.OpenReader(zp.name) + if err != nil { + return nil, err + } + defer reader.Close() + return readZipMeta(reader, zid, entry) +} + +func (zp *zipBox) FetchZids(ctx context.Context) (id.Set, error) { + result := id.NewSetCap(len(zp.zettel)) + for zid := range zp.zettel { + result[zid] = true + } + return result, nil +} + +func (zp *zipBox) SelectMeta(ctx context.Context, match search.MetaMatchFunc) (res []*meta.Meta, err error) { + reader, err := zip.OpenReader(zp.name) + if err != nil { + return nil, err + } + defer reader.Close() + for zid, entry := range zp.zettel { + m, err := readZipMeta(reader, zid, entry) + if err != nil { + continue + } + zp.enricher.Enrich(ctx, m, zp.number) + if match(m) { + res = append(res, m) + } + } + return res, nil +} + +func (zp *zipBox) CanUpdateZettel(ctx context.Context, zettel domain.Zettel) bool { + return false +} + +func (zp *zipBox) UpdateZettel(ctx context.Context, zettel domain.Zettel) error { + return box.ErrReadOnly +} + +func (zp *zipBox) AllowRenameZettel(ctx context.Context, zid id.Zid) bool { + _, ok := zp.zettel[zid] + return !ok +} + +func (zp *zipBox) RenameZettel(ctx context.Context, curZid, newZid id.Zid) error { + if _, ok := zp.zettel[curZid]; ok { + return box.ErrReadOnly + } + return box.ErrNotFound +} + +func (zp *zipBox) CanDeleteZettel(ctx context.Context, zid id.Zid) bool { return false } + +func (zp *zipBox) DeleteZettel(ctx context.Context, zid id.Zid) error { + if _, ok := zp.zettel[zid]; ok { + return box.ErrReadOnly + } + return box.ErrNotFound +} + +func (zp *zipBox) ReadStats(st *box.ManagedBoxStats) { + st.ReadOnly = true + st.Zettel = len(zp.zettel) +} + +func readZipMeta(reader *zip.ReadCloser, zid id.Zid, entry *zipEntry) (m *meta.Meta, err error) { + var inMeta bool + if entry.metaInHeader { + m, err = readZipMetaFile(reader, zid, entry.contentName) + } else if metaName := entry.metaName; metaName != "" { + m, err = readZipMetaFile(reader, zid, entry.metaName) + inMeta = true + } else { + m = CalcDefaultMeta(zid, entry.contentExt) + } + if err == nil { + CleanupMeta(m, zid, entry.contentExt, inMeta, false) } return m, err } func readZipMetaFile(reader *zip.ReadCloser, zid id.Zid, name string) (*meta.Meta, error) { @@ -236,13 +245,17 @@ } inp := input.NewInput(src) return meta.NewFromInput(zid, inp), nil } -func readZipFileContent(reader *zip.ReadCloser, name string) ([]byte, error) { +func readZipFileContent(reader *zip.ReadCloser, name string) (string, error) { f, err := reader.Open(name) if err != nil { - return nil, err + return "", err } defer f.Close() - return io.ReadAll(f) + buf, err := io.ReadAll(f) + if err != nil { + return "", err + } + return string(buf), nil } Index: box/helper.go ================================================================== --- box/helper.go +++ box/helper.go @@ -1,32 +1,28 @@ //----------------------------------------------------------------------------- -// Copyright (c) 2021-present Detlef Stern +// Copyright (c) 2021 Detlef Stern // -// This file is part of Zettelstore. +// This file is part of zettelstore. // // Zettelstore is licensed under the latest version of the EUPL (European Union // Public License). Please see file LICENSE.txt for your rights and obligations // under this license. -// -// SPDX-License-Identifier: EUPL-1.2 -// SPDX-FileCopyrightText: 2021-present Detlef Stern //----------------------------------------------------------------------------- +// Package box provides a generic interface to zettel boxes. package box import ( - "net/url" - "strconv" "time" - "zettelstore.de/z/zettel/id" + "zettelstore.de/z/domain/id" ) // 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 range 90 { // Must be completed within 9 seconds (less than web/server.writeTimeout) + for i := 0; i < 90; i++ { // 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 } @@ -37,30 +33,5 @@ time.Sleep(100 * time.Millisecond) withSeconds = true } return id.Invalid, ErrConflict } - -// GetQueryBool is a helper function to extract bool values from a box URI. -func GetQueryBool(u *url.URL, key string) bool { - _, ok := u.Query()[key] - return ok -} - -// GetQueryInt is a helper function to extract int values of a specified range from a box URI. -func GetQueryInt(u *url.URL, key string, min, def, max int) int { - sVal := u.Query().Get(key) - if sVal == "" { - return def - } - iVal, err := strconv.Atoi(sVal) - if err != nil { - return def - } - if iVal < min { - return min - } - if iVal > max { - return max - } - return iVal -} Index: box/manager/anteroom.go ================================================================== --- box/manager/anteroom.go +++ box/manager/anteroom.go @@ -1,112 +1,141 @@ //----------------------------------------------------------------------------- -// Copyright (c) 2021-present Detlef Stern +// Copyright (c) 2021 Detlef Stern // -// This file is part of Zettelstore. +// This file is part of zettelstore. // // Zettelstore is licensed under the latest version of the EUPL (European Union // Public License). Please see file LICENSE.txt for your rights and obligations // under this license. -// -// 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 import ( "sync" - "zettelstore.de/z/zettel/id" + "zettelstore.de/z/domain/id" ) type arAction int const ( arNothing arAction = iota arReload - arZettel + arUpdate + arDelete ) type anteroom struct { + num uint64 next *anteroom - waiting id.Set + waiting map[id.Zid]arAction curLoad int reload bool } -type anteroomQueue struct { +type anterooms struct { mx sync.Mutex + nextNum uint64 first *anteroom last *anteroom maxLoad int } -func newAnteroomQueue(maxLoad int) *anteroomQueue { return &anteroomQueue{maxLoad: maxLoad} } +func newAnterooms(maxLoad int) *anterooms { + return &anterooms{maxLoad: maxLoad} +} -func (ar *anteroomQueue) EnqueueZettel(zid id.Zid) { - if !zid.IsValid() { +func (ar *anterooms) Enqueue(zid id.Zid, action arAction) { + if !zid.IsValid() || action == arNothing || action == arReload { return } ar.mx.Lock() defer ar.mx.Unlock() if ar.first == nil { - ar.first = ar.makeAnteroom(zid) + ar.first = ar.makeAnteroom(zid, action) ar.last = ar.first return } for room := ar.first; room != nil; room = room.next { if room.reload { continue // Do not put zettel in reload room } - if _, ok := room.waiting[zid]; ok { - // Zettel is already waiting. Nothing to do. + a, ok := room.waiting[zid] + if !ok { + continue + } + switch action { + case a: return + case arUpdate: + room.waiting[zid] = action + case arDelete: + room.waiting[zid] = action } + return } if room := ar.last; !room.reload && (ar.maxLoad == 0 || room.curLoad < ar.maxLoad) { - room.waiting.Add(zid) + room.waiting[zid] = action room.curLoad++ return } - room := ar.makeAnteroom(zid) + room := ar.makeAnteroom(zid, action) ar.last.next = room ar.last = room } -func (ar *anteroomQueue) makeAnteroom(zid id.Zid) *anteroom { - if zid == id.Invalid { - panic(zid) - } - waiting := id.NewSetCap(max(ar.maxLoad, 100), zid) - return &anteroom{next: nil, waiting: waiting, curLoad: 1, reload: false} -} - -func (ar *anteroomQueue) Reset() { - ar.mx.Lock() - defer ar.mx.Unlock() - ar.first = &anteroom{next: nil, waiting: nil, curLoad: 0, reload: true} - ar.last = ar.first -} - -func (ar *anteroomQueue) Reload(allZids id.Set) { - ar.mx.Lock() - defer ar.mx.Unlock() - ar.deleteReloadedRooms() - - if ns := len(allZids); ns > 0 { - ar.first = &anteroom{next: ar.first, waiting: allZids, curLoad: ns, reload: true} - if ar.first.next == nil { - ar.last = ar.first - } - } else { - ar.first = nil - ar.last = nil - } -} - -func (ar *anteroomQueue) deleteReloadedRooms() { +func (ar *anterooms) makeAnteroom(zid id.Zid, action arAction) *anteroom { + c := ar.maxLoad + if c == 0 { + c = 100 + } + waiting := make(map[id.Zid]arAction, c) + waiting[zid] = action + ar.nextNum++ + return &anteroom{num: ar.nextNum, next: nil, waiting: waiting, curLoad: 1, reload: false} +} + +func (ar *anterooms) Reset() { + ar.mx.Lock() + defer ar.mx.Unlock() + ar.first = ar.makeAnteroom(id.Invalid, arReload) + ar.last = ar.first +} + +func (ar *anterooms) Reload(newZids id.Set) uint64 { + ar.mx.Lock() + defer ar.mx.Unlock() + newWaiting := createWaitingSet(newZids, arUpdate) + ar.deleteReloadedRooms() + + if ns := len(newWaiting); ns > 0 { + ar.nextNum++ + ar.first = &anteroom{num: ar.nextNum, next: ar.first, waiting: newWaiting, curLoad: ns} + if ar.first.next == nil { + ar.last = ar.first + } + return ar.nextNum + } + + ar.first = nil + ar.last = nil + return 0 +} + +func createWaitingSet(zids id.Set, action arAction) map[id.Zid]arAction { + waitingSet := make(map[id.Zid]arAction, len(zids)) + for zid := range zids { + if zid.IsValid() { + waitingSet[zid] = action + } + } + return waitingSet +} + +func (ar *anterooms) deleteReloadedRooms() { room := ar.first for room != nil && room.reload { room = room.next } ar.first = room @@ -113,32 +142,24 @@ if room == nil { ar.last = nil } } -func (ar *anteroomQueue) Dequeue() (arAction, id.Zid, bool) { +func (ar *anterooms) Dequeue() (arAction, id.Zid, uint64) { ar.mx.Lock() defer ar.mx.Unlock() - first := ar.first - if first != nil { - if first.waiting == nil && first.reload { - ar.removeFirst() - return arReload, id.Invalid, false - } - for zid := range first.waiting { - delete(first.waiting, zid) - if len(first.waiting) == 0 { - ar.removeFirst() - } - return arZettel, zid, first.reload - } - ar.removeFirst() - } - return arNothing, id.Invalid, false -} - -func (ar *anteroomQueue) removeFirst() { - ar.first = ar.first.next - if ar.first == nil { - ar.last = nil - } + if ar.first == nil { + return arNothing, id.Invalid, 0 + } + for zid, action := range ar.first.waiting { + roomNo := ar.first.num + delete(ar.first.waiting, zid) + if len(ar.first.waiting) == 0 { + ar.first = ar.first.next + if ar.first == nil { + ar.last = nil + } + } + return action, zid, roomNo + } + return arNothing, id.Invalid, 0 } Index: box/manager/anteroom_test.go ================================================================== --- box/manager/anteroom_test.go +++ box/manager/anteroom_test.go @@ -1,51 +1,49 @@ //----------------------------------------------------------------------------- -// Copyright (c) 2021-present Detlef Stern +// Copyright (c) 2021 Detlef Stern // -// This file is part of Zettelstore. +// This file is part of zettelstore. // // Zettelstore is licensed under the latest version of the EUPL (European Union // Public License). Please see file LICENSE.txt for your rights and obligations // under this license. -// -// 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 import ( "testing" - "zettelstore.de/z/zettel/id" + "zettelstore.de/z/domain/id" ) func TestSimple(t *testing.T) { t.Parallel() - ar := newAnteroomQueue(2) - ar.EnqueueZettel(id.Zid(1)) - action, zid, lastReload := ar.Dequeue() - if zid != id.Zid(1) || action != arZettel || lastReload { - t.Errorf("Expected arZettel/1/false, but got %v/%v/%v", action, zid, lastReload) - } - _, zid, _ = ar.Dequeue() - if zid != id.Invalid { + ar := newAnterooms(2) + ar.Enqueue(id.Zid(1), arUpdate) + action, zid, rno := ar.Dequeue() + if zid != id.Zid(1) || action != arUpdate || rno != 1 { + t.Errorf("Expected arUpdate/1/1, but got %v/%v/%v", action, zid, rno) + } + action, zid, _ = ar.Dequeue() + if zid != id.Invalid && action != arDelete { t.Errorf("Expected invalid Zid, but got %v", zid) } - ar.EnqueueZettel(id.Zid(1)) - ar.EnqueueZettel(id.Zid(2)) + ar.Enqueue(id.Zid(1), arUpdate) + ar.Enqueue(id.Zid(2), arUpdate) if ar.first != ar.last { t.Errorf("Expected one room, but got more") } - ar.EnqueueZettel(id.Zid(3)) + ar.Enqueue(id.Zid(3), arUpdate) if ar.first == ar.last { t.Errorf("Expected more than one room, but got only one") } count := 0 for ; count < 1000; count++ { - action, _, _ = ar.Dequeue() + action, _, _ := ar.Dequeue() if action == arNothing { break } } if count != 3 { @@ -53,57 +51,59 @@ } } func TestReset(t *testing.T) { t.Parallel() - ar := newAnteroomQueue(1) - ar.EnqueueZettel(id.Zid(1)) + ar := newAnterooms(1) + ar.Enqueue(id.Zid(1), arUpdate) ar.Reset() action, zid, _ := ar.Dequeue() if action != arReload || zid != id.Invalid { t.Errorf("Expected reload & invalid Zid, but got %v/%v", action, zid) } ar.Reload(id.NewSet(3, 4)) - ar.EnqueueZettel(id.Zid(5)) - ar.EnqueueZettel(id.Zid(5)) + ar.Enqueue(id.Zid(5), arUpdate) + ar.Enqueue(id.Zid(5), arDelete) + ar.Enqueue(id.Zid(5), arDelete) + ar.Enqueue(id.Zid(5), arUpdate) if ar.first == ar.last || ar.first.next != ar.last /*|| ar.first.next.next != ar.last*/ { t.Errorf("Expected 2 rooms") } action, zid1, _ := ar.Dequeue() - if action != arZettel { - t.Errorf("Expected arZettel, but got %v", action) + if action != arUpdate { + t.Errorf("Expected arUpdate, but got %v", action) } action, zid2, _ := ar.Dequeue() - if action != arZettel { - t.Errorf("Expected arZettel, but got %v", action) + if action != arUpdate { + t.Errorf("Expected arUpdate, but got %v", action) } if !(zid1 == id.Zid(3) && zid2 == id.Zid(4) || zid1 == id.Zid(4) && zid2 == id.Zid(3)) { t.Errorf("Zids must be 3 or 4, but got %v/%v", zid1, zid2) } action, zid, _ = ar.Dequeue() - if zid != id.Zid(5) || action != arZettel { - t.Errorf("Expected 5/arZettel, but got %v/%v", zid, action) + if zid != id.Zid(5) || action != arUpdate { + t.Errorf("Expected 5/arUpdate, but got %v/%v", zid, action) + } + action, zid, _ = ar.Dequeue() + if action != arNothing || zid != id.Invalid { + t.Errorf("Expected nothing & invalid Zid, but got %v/%v", action, zid) + } + + ar = newAnterooms(1) + ar.Reload(id.NewSet(id.Zid(6))) + action, zid, _ = ar.Dequeue() + if zid != id.Zid(6) || action != arUpdate { + t.Errorf("Expected 6/arUpdate, but got %v/%v", zid, action) } action, zid, _ = ar.Dequeue() if action != arNothing || zid != id.Invalid { t.Errorf("Expected nothing & invalid Zid, but got %v/%v", action, zid) } - ar = newAnteroomQueue(1) - ar.Reload(id.NewSet(id.Zid(6))) - action, zid, _ = ar.Dequeue() - if zid != id.Zid(6) || action != arZettel { - t.Errorf("Expected 6/arZettel, but got %v/%v", zid, action) - } - action, zid, _ = ar.Dequeue() - if action != arNothing || zid != id.Invalid { - t.Errorf("Expected nothing & invalid Zid, but got %v/%v", action, zid) - } - - ar = newAnteroomQueue(1) - ar.EnqueueZettel(id.Zid(8)) + ar = newAnterooms(1) + ar.Enqueue(id.Zid(8), arUpdate) ar.Reload(nil) action, zid, _ = ar.Dequeue() if action != arNothing || zid != id.Invalid { t.Errorf("Expected nothing & invalid Zid, but got %v/%v", action, zid) } } Index: box/manager/box.go ================================================================== --- box/manager/box.go +++ box/manager/box.go @@ -1,30 +1,29 @@ //----------------------------------------------------------------------------- -// Copyright (c) 2021-present Detlef Stern +// Copyright (c) 2021 Detlef Stern // -// This file is part of Zettelstore. +// This file is part of zettelstore. // // Zettelstore is licensed under the latest version of the EUPL (European Union // Public License). Please see file LICENSE.txt for your rights and obligations // under this license. -// -// 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 import ( "context" "errors" + "sort" "strings" "zettelstore.de/z/box" - "zettelstore.de/z/query" - "zettelstore.de/z/zettel" - "zettelstore.de/z/zettel/id" - "zettelstore.de/z/zettel/meta" + "zettelstore.de/z/domain" + "zettelstore.de/z/domain/id" + "zettelstore.de/z/domain/meta" + "zettelstore.de/z/search" ) // Conatains all box.Box related functions // Location returns some information where the box is located. @@ -31,11 +30,11 @@ func (mgr *Manager) Location() string { if len(mgr.boxes) <= 2 { return "NONE" } var sb strings.Builder - for i := range len(mgr.boxes) - 2 { + for i := 0; i < len(mgr.boxes)-2; i++ { if i > 0 { sb.WriteString(", ") } sb.WriteString(mgr.boxes[i].Location()) } @@ -42,216 +41,182 @@ return sb.String() } // CanCreateZettel returns true, if box could possibly create a new zettel. func (mgr *Manager) CanCreateZettel(ctx context.Context) bool { - if mgr.State() != box.StartStateStarted { - return false - } mgr.mgrMx.RLock() defer mgr.mgrMx.RUnlock() - if box, isWriteBox := mgr.boxes[0].(box.WriteBox); isWriteBox { - return box.CanCreateZettel(ctx) - } - return false + return mgr.started && mgr.boxes[0].CanCreateZettel(ctx) } // CreateZettel creates a new zettel. -func (mgr *Manager) CreateZettel(ctx context.Context, zettel zettel.Zettel) (id.Zid, error) { - mgr.mgrLog.Debug().Msg("CreateZettel") - if mgr.State() != box.StartStateStarted { +func (mgr *Manager) CreateZettel(ctx context.Context, zettel domain.Zettel) (id.Zid, error) { + mgr.mgrMx.RLock() + defer mgr.mgrMx.RUnlock() + if !mgr.started { return id.Invalid, box.ErrStopped } - mgr.mgrMx.RLock() - defer mgr.mgrMx.RUnlock() - if box, isWriteBox := mgr.boxes[0].(box.WriteBox); isWriteBox { - zettel.Meta = mgr.cleanMetaProperties(zettel.Meta) - zid, err := box.CreateZettel(ctx, zettel) - if err == nil { - mgr.idxUpdateZettel(ctx, zettel) - } - return zid, err - } - return id.Invalid, box.ErrReadOnly + return mgr.boxes[0].CreateZettel(ctx, zettel) } // GetZettel retrieves a specific zettel. -func (mgr *Manager) GetZettel(ctx context.Context, zid id.Zid) (zettel.Zettel, error) { - mgr.mgrLog.Debug().Zid(zid).Msg("GetZettel") - if mgr.State() != box.StartStateStarted { - return zettel.Zettel{}, box.ErrStopped - } +func (mgr *Manager) GetZettel(ctx context.Context, zid id.Zid) (domain.Zettel, error) { mgr.mgrMx.RLock() defer mgr.mgrMx.RUnlock() + if !mgr.started { + return domain.Zettel{}, box.ErrStopped + } for i, p := range mgr.boxes { - var errZNF box.ErrZettelNotFound - if z, err := p.GetZettel(ctx, zid); !errors.As(err, &errZNF) { + if z, err := p.GetZettel(ctx, zid); err != box.ErrNotFound { if err == nil { mgr.Enrich(ctx, z.Meta, i+1) } return z, err } } - return zettel.Zettel{}, box.ErrZettelNotFound{Zid: zid} + return domain.Zettel{}, box.ErrNotFound } // GetAllZettel retrieves a specific zettel from all managed boxes. -func (mgr *Manager) GetAllZettel(ctx context.Context, zid id.Zid) ([]zettel.Zettel, error) { - mgr.mgrLog.Debug().Zid(zid).Msg("GetAllZettel") - if mgr.State() != box.StartStateStarted { +func (mgr *Manager) GetAllZettel(ctx context.Context, zid id.Zid) ([]domain.Zettel, error) { + mgr.mgrMx.RLock() + defer mgr.mgrMx.RUnlock() + if !mgr.started { return nil, box.ErrStopped } - mgr.mgrMx.RLock() - defer mgr.mgrMx.RUnlock() - var result []zettel.Zettel + var result []domain.Zettel for i, p := range mgr.boxes { if z, err := p.GetZettel(ctx, zid); err == nil { mgr.Enrich(ctx, z.Meta, i+1) result = append(result, z) } } return result, nil } -// FetchZids returns the set of all zettel identifer managed by the box. -func (mgr *Manager) FetchZids(ctx context.Context) (id.Set, error) { - mgr.mgrLog.Debug().Msg("FetchZids") - if mgr.State() != box.StartStateStarted { - return nil, box.ErrStopped - } - result := id.Set{} - mgr.mgrMx.RLock() - defer mgr.mgrMx.RUnlock() - for _, p := range mgr.boxes { - err := p.ApplyZid(ctx, func(zid id.Zid) { result.Add(zid) }, func(id.Zid) bool { return true }) - if err != nil { - return nil, err - } - } - return result, nil -} - -func (mgr *Manager) HasZettel(ctx context.Context, zid id.Zid) bool { - mgr.mgrLog.Debug().Zid(zid).Msg("HasZettel") - if mgr.State() != box.StartStateStarted { - return false - } - mgr.mgrMx.RLock() - defer mgr.mgrMx.RUnlock() - for _, bx := range mgr.boxes { - if bx.HasZettel(ctx, zid) { - return true - } - } - return false -} - -func (mgr *Manager) GetMeta(ctx context.Context, zid id.Zid) (*meta.Meta, error) { - mgr.mgrLog.Debug().Zid(zid).Msg("GetMeta") - if mgr.State() != box.StartStateStarted { - return nil, box.ErrStopped - } - - m, err := mgr.idxStore.GetMeta(ctx, zid) - if err != nil { - return nil, err - } - mgr.Enrich(ctx, m, 0) - return m, nil -} - -// SelectMeta returns all zettel meta data that match the selection -// criteria. The result is ordered by descending zettel id. -func (mgr *Manager) SelectMeta(ctx context.Context, metaSeq []*meta.Meta, q *query.Query) ([]*meta.Meta, error) { - if msg := mgr.mgrLog.Debug(); msg.Enabled() { - msg.Str("query", q.String()).Msg("SelectMeta") - } - if mgr.State() != box.StartStateStarted { - return nil, box.ErrStopped - } - mgr.mgrMx.RLock() - defer mgr.mgrMx.RUnlock() - - compSearch := q.RetrieveAndCompile(ctx, mgr, metaSeq) - if result := compSearch.Result(); result != nil { - mgr.mgrLog.Trace().Int("count", int64(len(result))).Msg("found without ApplyMeta") - return result, nil - } - selected := map[id.Zid]*meta.Meta{} - for _, term := range compSearch.Terms { - rejected := id.Set{} - handleMeta := func(m *meta.Meta) { - zid := m.Zid - if rejected.ContainsOrNil(zid) { - mgr.mgrLog.Trace().Zid(zid).Msg("SelectMeta/alreadyRejected") - return - } - if _, ok := selected[zid]; ok { - mgr.mgrLog.Trace().Zid(zid).Msg("SelectMeta/alreadySelected") - return - } - if compSearch.PreMatch(m) && term.Match(m) { - selected[zid] = m - mgr.mgrLog.Trace().Zid(zid).Msg("SelectMeta/match") - } else { - rejected.Add(zid) - mgr.mgrLog.Trace().Zid(zid).Msg("SelectMeta/reject") - } - } - for _, p := range mgr.boxes { - if err2 := p.ApplyMeta(ctx, handleMeta, term.Retrieve); err2 != nil { - return nil, err2 - } - } - } - result := make([]*meta.Meta, 0, len(selected)) - for _, m := range selected { - result = append(result, m) - } - result = compSearch.AfterSearch(result) - mgr.mgrLog.Trace().Int("count", int64(len(result))).Msg("found with ApplyMeta") - return result, nil -} - -// CanUpdateZettel returns true, if box could possibly update the given zettel. -func (mgr *Manager) CanUpdateZettel(ctx context.Context, zettel zettel.Zettel) bool { - if mgr.State() != box.StartStateStarted { - return false - } - mgr.mgrMx.RLock() - defer mgr.mgrMx.RUnlock() - if box, isWriteBox := mgr.boxes[0].(box.WriteBox); isWriteBox { - return box.CanUpdateZettel(ctx, zettel) - } - return false - -} - -// UpdateZettel updates an existing zettel. -func (mgr *Manager) UpdateZettel(ctx context.Context, zettel zettel.Zettel) error { - mgr.mgrLog.Debug().Zid(zettel.Meta.Zid).Msg("UpdateZettel") - if mgr.State() != box.StartStateStarted { - return box.ErrStopped - } - if box, isWriteBox := mgr.boxes[0].(box.WriteBox); isWriteBox { - zettel.Meta = mgr.cleanMetaProperties(zettel.Meta) - if err := box.UpdateZettel(ctx, zettel); err != nil { - return err - } - mgr.idxUpdateZettel(ctx, zettel) - return nil - } - return box.ErrReadOnly -} - -// AllowRenameZettel returns true, if box will not disallow renaming the zettel. -func (mgr *Manager) AllowRenameZettel(ctx context.Context, zid id.Zid) bool { - if mgr.State() != box.StartStateStarted { - return false - } - mgr.mgrMx.RLock() - defer mgr.mgrMx.RUnlock() +// GetMeta retrieves just the meta data of a specific zettel. +func (mgr *Manager) GetMeta(ctx context.Context, zid id.Zid) (*meta.Meta, error) { + mgr.mgrMx.RLock() + defer mgr.mgrMx.RUnlock() + if !mgr.started { + return nil, box.ErrStopped + } + for i, p := range mgr.boxes { + if m, err := p.GetMeta(ctx, zid); err != box.ErrNotFound { + if err == nil { + mgr.Enrich(ctx, m, i+1) + } + return m, err + } + } + return nil, box.ErrNotFound +} + +// GetAllMeta retrieves the meta data of a specific zettel from all managed boxes. +func (mgr *Manager) GetAllMeta(ctx context.Context, zid id.Zid) ([]*meta.Meta, error) { + mgr.mgrMx.RLock() + defer mgr.mgrMx.RUnlock() + if !mgr.started { + return nil, box.ErrStopped + } + var result []*meta.Meta + for i, p := range mgr.boxes { + if m, err := p.GetMeta(ctx, zid); err == nil { + mgr.Enrich(ctx, m, i+1) + result = append(result, m) + } + } + return result, nil +} + +// FetchZids returns the set of all zettel identifer managed by the box. +func (mgr *Manager) FetchZids(ctx context.Context) (result id.Set, err error) { + mgr.mgrMx.RLock() + defer mgr.mgrMx.RUnlock() + if !mgr.started { + return nil, box.ErrStopped + } + for _, p := range mgr.boxes { + zids, err := p.FetchZids(ctx) + if err != nil { + return nil, err + } + if result == nil { + result = zids + } else if len(result) <= len(zids) { + for zid := range result { + zids[zid] = true + } + result = zids + } else { + for zid := range zids { + result[zid] = true + } + } + } + return result, nil +} + +// SelectMeta returns all zettel meta data that match the selection +// criteria. The result is ordered by descending zettel id. +func (mgr *Manager) SelectMeta(ctx context.Context, s *search.Search) ([]*meta.Meta, error) { + mgr.mgrMx.RLock() + defer mgr.mgrMx.RUnlock() + if !mgr.started { + return nil, box.ErrStopped + } + var result []*meta.Meta + match := s.CompileMatch(mgr) + for _, p := range mgr.boxes { + selected, err := p.SelectMeta(ctx, match) + if err != nil { + return nil, err + } + sort.Slice(selected, func(i, j int) bool { return selected[i].Zid > selected[j].Zid }) + if len(result) == 0 { + result = selected + } else { + result = box.MergeSorted(result, selected) + } + } + if s == nil { + return result, nil + } + return s.Sort(result), nil +} + +// CanUpdateZettel returns true, if box could possibly update the given zettel. +func (mgr *Manager) CanUpdateZettel(ctx context.Context, zettel domain.Zettel) bool { + mgr.mgrMx.RLock() + defer mgr.mgrMx.RUnlock() + return mgr.started && mgr.boxes[0].CanUpdateZettel(ctx, zettel) +} + +// UpdateZettel updates an existing zettel. +func (mgr *Manager) UpdateZettel(ctx context.Context, zettel domain.Zettel) error { + mgr.mgrMx.RLock() + defer mgr.mgrMx.RUnlock() + if !mgr.started { + return box.ErrStopped + } + // Remove all (computed) properties from metadata before storing the zettel. + zettel.Meta = zettel.Meta.Clone() + for _, p := range zettel.Meta.PairsRest(true) { + if mgr.propertyKeys[p.Key] { + zettel.Meta.Delete(p.Key) + } + } + return mgr.boxes[0].UpdateZettel(ctx, zettel) +} + +// AllowRenameZettel returns true, if box will not disallow renaming the zettel. +func (mgr *Manager) AllowRenameZettel(ctx context.Context, zid id.Zid) bool { + mgr.mgrMx.RLock() + defer mgr.mgrMx.RUnlock() + if !mgr.started { + return false + } for _, p := range mgr.boxes { if !p.AllowRenameZettel(ctx, zid) { return false } } @@ -258,37 +223,34 @@ return true } // RenameZettel changes the current zid to a new zid. func (mgr *Manager) RenameZettel(ctx context.Context, curZid, newZid id.Zid) error { - mgr.mgrLog.Debug().Zid(curZid).Zid(newZid).Msg("RenameZettel") - if mgr.State() != box.StartStateStarted { + mgr.mgrMx.RLock() + defer mgr.mgrMx.RUnlock() + if !mgr.started { return box.ErrStopped } - mgr.mgrMx.RLock() - 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 := range i { + if err != nil && !errors.Is(err, box.ErrNotFound) { + for j := 0; j < i; j++ { mgr.boxes[j].RenameZettel(ctx, newZid, curZid) } return err } } - mgr.idxRenameZettel(ctx, curZid, newZid) return nil } // CanDeleteZettel returns true, if box could possibly delete the given zettel. func (mgr *Manager) CanDeleteZettel(ctx context.Context, zid id.Zid) bool { - if mgr.State() != box.StartStateStarted { + mgr.mgrMx.RLock() + defer mgr.mgrMx.RUnlock() + if !mgr.started { return false } - mgr.mgrMx.RLock() - defer mgr.mgrMx.RUnlock() for _, p := range mgr.boxes { if p.CanDeleteZettel(ctx, zid) { return true } } @@ -295,35 +257,21 @@ return false } // DeleteZettel removes the zettel from the box. func (mgr *Manager) DeleteZettel(ctx context.Context, zid id.Zid) error { - mgr.mgrLog.Debug().Zid(zid).Msg("DeleteZettel") - if mgr.State() != box.StartStateStarted { + mgr.mgrMx.RLock() + defer mgr.mgrMx.RUnlock() + if !mgr.started { return box.ErrStopped } - mgr.mgrMx.RLock() - defer mgr.mgrMx.RUnlock() for _, p := range mgr.boxes { err := p.DeleteZettel(ctx, zid) if err == nil { - mgr.idxDeleteZettel(ctx, zid) return nil } - var errZNF box.ErrZettelNotFound - if !errors.As(err, &errZNF) && !errors.Is(err, box.ErrReadOnly) { + if !errors.Is(err, box.ErrNotFound) && !errors.Is(err, box.ErrReadOnly) { return err } } - return box.ErrZettelNotFound{Zid: zid} -} - -// Remove all (computed) properties from metadata before storing the zettel. -func (mgr *Manager) cleanMetaProperties(m *meta.Meta) *meta.Meta { - result := m.Clone() - for _, p := range result.ComputedPairsRest() { - if mgr.propertyKeys.Has(p.Key) { - result.Delete(p.Key) - } - } - return result + return box.ErrNotFound } Index: box/manager/collect.go ================================================================== --- box/manager/collect.go +++ box/manager/collect.go @@ -1,27 +1,25 @@ //----------------------------------------------------------------------------- -// Copyright (c) 2021-present Detlef Stern +// Copyright (c) 2021 Detlef Stern // -// This file is part of Zettelstore. +// This file is part of zettelstore. // // Zettelstore is licensed under the latest version of the EUPL (European Union // Public License). Please see file LICENSE.txt for your rights and obligations // under this license. -// -// 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 import ( "strings" "zettelstore.de/z/ast" "zettelstore.de/z/box/manager/store" + "zettelstore.de/z/domain/id" "zettelstore.de/z/strfun" - "zettelstore.de/z/zettel/id" ) type collectData struct { refs id.Set words store.WordSet @@ -33,33 +31,33 @@ data.words = store.NewWordSet() data.urls = store.NewWordSet() } func collectZettelIndexData(zn *ast.ZettelNode, data *collectData) { - ast.Walk(data, &zn.Ast) + ast.WalkBlockSlice(data, zn.Ast) } -func collectInlineIndexData(is *ast.InlineSlice, data *collectData) { - ast.Walk(data, is) +func collectInlineIndexData(ins ast.InlineSlice, data *collectData) { + ast.WalkInlineSlice(data, ins) } func (data *collectData) Visit(node ast.Node) ast.Visitor { switch n := node.(type) { case *ast.VerbatimNode: - data.addText(string(n.Content)) - case *ast.TranscludeNode: - data.addRef(n.Ref) + for _, line := range n.Lines { + data.addText(line) + } case *ast.TextNode: data.addText(n.Text) + case *ast.TagNode: + data.addText(n.Tag) case *ast.LinkNode: data.addRef(n.Ref) - case *ast.EmbedRefNode: + case *ast.ImageNode: data.addRef(n.Ref) - case *ast.CiteNode: - data.addText(n.Key) case *ast.LiteralNode: - data.addText(string(n.Content)) + data.addText(n.Text) } return data } func (data *collectData) addText(s string) { @@ -77,8 +75,8 @@ } if !ref.IsZettel() { return } if zid, err := id.Parse(ref.URL.Path); err == nil { - data.refs.Add(zid) + data.refs[zid] = true } } Index: box/manager/enrich.go ================================================================== --- box/manager/enrich.go +++ box/manager/enrich.go @@ -1,135 +1,52 @@ //----------------------------------------------------------------------------- -// Copyright (c) 2021-present Detlef Stern +// Copyright (c) 2021 Detlef Stern // -// This file is part of Zettelstore. +// This file is part of zettelstore. // // Zettelstore is licensed under the latest version of the EUPL (European Union // Public License). Please see file LICENSE.txt for your rights and obligations // under this license. -// -// 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 import ( "context" "strconv" - "t73f.de/r/zsc/api" "zettelstore.de/z/box" - "zettelstore.de/z/zettel/id" - "zettelstore.de/z/zettel/meta" + "zettelstore.de/z/domain/meta" ) // Enrich computes additional properties and updates the given metadata. func (mgr *Manager) Enrich(ctx context.Context, m *meta.Meta, boxNumber int) { - - // Calculate computed, but stored values. - if _, ok := m.Get(api.KeyCreated); !ok { - m.Set(api.KeyCreated, computeCreated(m.Zid)) - } - if box.DoNotEnrich(ctx) { // Enrich is called indirectly via indexer or enrichment is not requested - // because of other reasons -> ignore this call, do not update metadata + // because of other reasons -> ignore this call, do not update meta data return } + m.Set(meta.KeyBoxNumber, strconv.Itoa(boxNumber)) computePublished(m) - if boxNumber > 0 { - m.Set(api.KeyBoxNumber, strconv.Itoa(boxNumber)) - } mgr.idxStore.Enrich(ctx, m) } -func computeCreated(zid id.Zid) string { - if zid <= 10101000000 { - // A year 0000 is not allowed and therefore an artificaial Zid. - // In the year 0001, the month must be > 0. - // In the month 000101, the day must be > 0. - return "00010101000000" - } - seconds := zid % 100 - if seconds > 59 { - seconds = 59 - } - zid /= 100 - minutes := zid % 100 - if minutes > 59 { - minutes = 59 - } - zid /= 100 - hours := zid % 100 - if hours > 23 { - hours = 23 - } - zid /= 100 - day := zid % 100 - zid /= 100 - month := zid % 100 - year := zid / 100 - month, day = sanitizeMonthDay(year, month, day) - created := ((((year*100+month)*100+day)*100+hours)*100+minutes)*100 + seconds - return created.String() -} - -func sanitizeMonthDay(year, month, day id.Zid) (id.Zid, id.Zid) { - if day < 1 { - day = 1 - } - if month < 1 { - month = 1 - } - if month > 12 { - month = 12 - } - - switch month { - case 1, 3, 5, 7, 8, 10, 12: - if day > 31 { - day = 31 - } - case 4, 6, 9, 11: - if day > 30 { - day = 30 - } - case 2: - if year%4 != 0 || (year%100 == 0 && year%400 != 0) { - if day > 28 { - day = 28 - } - } else { - if day > 29 { - day = 29 - } - } - } - return month, day -} - -func computePublished(m *meta.Meta) { - if _, ok := m.Get(api.KeyPublished); ok { - return - } - if modified, ok := m.Get(api.KeyModified); ok { - if _, ok = meta.TimeValue(modified); ok { - m.Set(api.KeyPublished, modified) - return - } - } - if created, ok := m.Get(api.KeyCreated); ok { - if _, ok = meta.TimeValue(created); ok { - m.Set(api.KeyPublished, created) +func computePublished(m *meta.Meta) { + if _, ok := m.Get(meta.KeyPublished); ok { + return + } + if modified, ok := m.Get(meta.KeyModified); ok { + if _, ok = meta.TimeValue(modified); ok { + m.Set(meta.KeyPublished, modified) return } } zid := m.Zid.String() if _, ok := meta.TimeValue(zid); ok { - m.Set(api.KeyPublished, zid) + m.Set(meta.KeyPublished, zid) return } // Neither the zettel was modified nor the zettel identifer contains a valid // timestamp. In this case do not set the "published" property. } Index: box/manager/indexer.go ================================================================== --- box/manager/indexer.go +++ box/manager/indexer.go @@ -1,87 +1,64 @@ //----------------------------------------------------------------------------- -// Copyright (c) 2021-present Detlef Stern +// Copyright (c) 2021 Detlef Stern // -// This file is part of Zettelstore. +// This file is part of zettelstore. // // Zettelstore is licensed under the latest version of the EUPL (European Union // Public License). Please see file LICENSE.txt for your rights and obligations // under this license. -// -// 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 import ( "context" - "fmt" "net/url" "time" "zettelstore.de/z/box" "zettelstore.de/z/box/manager/store" + "zettelstore.de/z/domain" + "zettelstore.de/z/domain/id" + "zettelstore.de/z/domain/meta" "zettelstore.de/z/kernel" "zettelstore.de/z/parser" "zettelstore.de/z/strfun" - "zettelstore.de/z/zettel" - "zettelstore.de/z/zettel/id" - "zettelstore.de/z/zettel/meta" ) // SearchEqual returns all zettel that contains the given exact word. // The word must be normalized through Unicode NKFD, trimmed and not empty. func (mgr *Manager) SearchEqual(word string) id.Set { - found := mgr.idxStore.SearchEqual(word) - mgr.idxLog.Debug().Str("word", word).Int("found", int64(len(found))).Msg("SearchEqual") - if msg := mgr.idxLog.Trace(); msg.Enabled() { - msg.Str("ids", fmt.Sprint(found)).Msg("IDs") - } - return found + return mgr.idxStore.SearchEqual(word) } // 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 (mgr *Manager) SearchPrefix(prefix string) id.Set { - found := mgr.idxStore.SearchPrefix(prefix) - mgr.idxLog.Debug().Str("prefix", prefix).Int("found", int64(len(found))).Msg("SearchPrefix") - if msg := mgr.idxLog.Trace(); msg.Enabled() { - msg.Str("ids", fmt.Sprint(found)).Msg("IDs") - } - return found + return mgr.idxStore.SearchPrefix(prefix) } // 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 (mgr *Manager) SearchSuffix(suffix string) id.Set { - found := mgr.idxStore.SearchSuffix(suffix) - mgr.idxLog.Debug().Str("suffix", suffix).Int("found", int64(len(found))).Msg("SearchSuffix") - if msg := mgr.idxLog.Trace(); msg.Enabled() { - msg.Str("ids", fmt.Sprint(found)).Msg("IDs") - } - return found + return mgr.idxStore.SearchSuffix(suffix) } // SearchContains returns all zettel that contains the given string. // The string must be normalized through Unicode NKFD, trimmed and not empty. func (mgr *Manager) SearchContains(s string) id.Set { - found := mgr.idxStore.SearchContains(s) - mgr.idxLog.Debug().Str("s", s).Int("found", int64(len(found))).Msg("SearchContains") - if msg := mgr.idxLog.Trace(); msg.Enabled() { - msg.Str("ids", fmt.Sprint(found)).Msg("IDs") - } - return found + return mgr.idxStore.SearchContains(s) } // idxIndexer runs in the background and updates the index data structures. // This is the main service of the idxIndexer. func (mgr *Manager) idxIndexer() { // Something may panic. Ensure a running indexer. defer func() { - if ri := recover(); ri != nil { - kernel.Main.LogRecover("Indexer", ri) + if r := recover(); r != nil { + kernel.Main.LogRecover("Indexer", r) go mgr.idxIndexer() } }() timerDuration := 15 * time.Second @@ -94,43 +71,54 @@ } } } func (mgr *Manager) idxWorkService(ctx context.Context) { + var roomNum uint64 var start time.Time for { - switch action, zid, lastReload := mgr.idxAr.Dequeue(); action { + switch action, zid, arRoomNum := mgr.idxAr.Dequeue(); action { case arNothing: return case arReload: - mgr.idxLog.Debug().Msg("reload") + roomNum = 0 zids, err := mgr.FetchZids(ctx) if err == nil { start = time.Now() - mgr.idxAr.Reload(zids) + if rno := mgr.idxAr.Reload(zids); rno > 0 { + roomNum = rno + } mgr.idxMx.Lock() - mgr.idxLastReload = time.Now().Local() + mgr.idxLastReload = time.Now() mgr.idxSinceReload = 0 mgr.idxMx.Unlock() } - case arZettel: - mgr.idxLog.Debug().Zid(zid).Msg("zettel") + case arUpdate: zettel, err := mgr.GetZettel(ctx, zid) if err != nil { - // Zettel was deleted or is not accessible b/c of other reasons - mgr.idxLog.Trace().Zid(zid).Msg("delete") - mgr.idxDeleteZettel(ctx, zid) + // TODO: on some errors put the zid into a "try later" set continue } - mgr.idxLog.Trace().Zid(zid).Msg("update") - mgr.idxUpdateZettel(ctx, zettel) mgr.idxMx.Lock() - if lastReload { + if arRoomNum == roomNum { mgr.idxDurReload = time.Since(start) } mgr.idxSinceReload++ mgr.idxMx.Unlock() + mgr.idxUpdateZettel(ctx, zettel) + case arDelete: + if _, err := mgr.GetMeta(ctx, zid); err == nil { + // Zettel was not deleted. This might occur, if zettel was + // deleted in secondary dirbox, but is still present in + // first dirbox (or vice versa). Re-index zettel in case + // a hidden zettel was recovered + mgr.idxAr.Enqueue(zid, arUpdate) + } + mgr.idxMx.Lock() + mgr.idxSinceReload++ + mgr.idxMx.Unlock() + mgr.idxDeleteZettel(zid) } } } func (mgr *Manager) idxSleepService(timer *time.Timer, timerDuration time.Duration) bool { @@ -151,27 +139,33 @@ return false } return true } -func (mgr *Manager) idxUpdateZettel(ctx context.Context, zettel zettel.Zettel) { +func (mgr *Manager) idxUpdateZettel(ctx context.Context, zettel domain.Zettel) { + m := zettel.Meta + if m.GetBool(meta.KeyNoIndex) { + // Zettel maybe in index + toCheck := mgr.idxStore.DeleteZettel(ctx, m.Zid) + mgr.idxCheckZettel(toCheck) + return + } + var cData collectData cData.initialize() - collectZettelIndexData(parser.ParseZettel(ctx, zettel, "", mgr.rtConfig), &cData) - - m := zettel.Meta - zi := store.NewZettelIndex(m) + collectZettelIndexData(parser.ParseZettel(zettel, "", mgr.rtConfig), &cData) + zi := store.NewZettelIndex(m.Zid) mgr.idxCollectFromMeta(ctx, m, zi, &cData) mgr.idxProcessData(ctx, zi, &cData) toCheck := mgr.idxStore.UpdateReferences(ctx, zi) mgr.idxCheckZettel(toCheck) } func (mgr *Manager) idxCollectFromMeta(ctx context.Context, m *meta.Meta, zi *store.ZettelIndex, cData *collectData) { - for _, pair := range m.ComputedPairs() { + for _, pair := range m.Pairs(false) { descr := meta.GetDescription(pair.Key) - if descr.IsProperty() { + if descr.IsComputed() { continue } switch descr.Type { case meta.TypeID: mgr.idxUpdateValue(ctx, descr.Inverse, pair.Value, zi) @@ -178,41 +172,26 @@ case meta.TypeIDSet: for _, val := range meta.ListFromValue(pair.Value) { mgr.idxUpdateValue(ctx, descr.Inverse, val, zi) } case meta.TypeZettelmarkup: - is := parser.ParseMetadata(pair.Value) - collectInlineIndexData(&is, cData) + collectInlineIndexData(parser.ParseMetadata(pair.Value), cData) case meta.TypeURL: if _, err := url.Parse(pair.Value); err == nil { cData.urls.Add(pair.Value) } default: - if descr.Type.IsSet { - for _, val := range meta.ListFromValue(pair.Value) { - idxCollectMetaValue(cData.words, val) - } - } else { - idxCollectMetaValue(cData.words, pair.Value) + for _, word := range strfun.NormalizeWords(pair.Value) { + cData.words.Add(word) } } } } -func idxCollectMetaValue(stWords store.WordSet, value string) { - if words := strfun.NormalizeWords(value); len(words) > 0 { - for _, word := range words { - stWords.Add(word) - } - } else { - stWords.Add(value) - } -} - func (mgr *Manager) idxProcessData(ctx context.Context, zi *store.ZettelIndex, cData *collectData) { for ref := range cData.refs { - if mgr.HasZettel(ctx, ref) { + if _, err := mgr.GetMeta(ctx, ref); err == nil { zi.AddBackRef(ref) } else { zi.AddDeadRef(ref) } } @@ -223,31 +202,26 @@ func (mgr *Manager) idxUpdateValue(ctx context.Context, inverseKey, value string, zi *store.ZettelIndex) { zid, err := id.Parse(value) if err != nil { return } - if !mgr.HasZettel(ctx, zid) { + if _, err := mgr.GetMeta(ctx, zid); err != nil { zi.AddDeadRef(zid) return } if inverseKey == "" { zi.AddBackRef(zid) return } - zi.AddInverseRef(inverseKey, zid) + zi.AddMetaRef(inverseKey, zid) } -func (mgr *Manager) idxRenameZettel(ctx context.Context, curZid, newZid id.Zid) { - toCheck := mgr.idxStore.RenameZettel(ctx, curZid, newZid) - mgr.idxCheckZettel(toCheck) -} - -func (mgr *Manager) idxDeleteZettel(ctx context.Context, zid id.Zid) { - toCheck := mgr.idxStore.DeleteZettel(ctx, zid) +func (mgr *Manager) idxDeleteZettel(zid id.Zid) { + toCheck := mgr.idxStore.DeleteZettel(context.Background(), zid) mgr.idxCheckZettel(toCheck) } func (mgr *Manager) idxCheckZettel(s id.Set) { for zid := range s { - mgr.idxAr.EnqueueZettel(zid) + mgr.idxAr.Enqueue(zid, arUpdate) } } Index: box/manager/manager.go ================================================================== --- box/manager/manager.go +++ box/manager/manager.go @@ -1,38 +1,35 @@ //----------------------------------------------------------------------------- -// Copyright (c) 2021-present Detlef Stern +// Copyright (c) 2021 Detlef Stern // -// This file is part of Zettelstore. +// This file is part of zettelstore. // // Zettelstore is licensed under the latest version of the EUPL (European Union // Public License). Please see file LICENSE.txt for your rights and obligations // under this license. -// -// 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 import ( "context" "io" + "log" "net/url" + "sort" "sync" "time" "zettelstore.de/z/auth" "zettelstore.de/z/box" - "zettelstore.de/z/box/manager/mapstore" + "zettelstore.de/z/box/manager/memstore" "zettelstore.de/z/box/manager/store" "zettelstore.de/z/config" + "zettelstore.de/z/domain/id" + "zettelstore.de/z/domain/meta" "zettelstore.de/z/kernel" - "zettelstore.de/z/logger" - "zettelstore.de/z/strfun" - "zettelstore.de/z/zettel/id" - "zettelstore.de/z/zettel/meta" ) // ConnectData contains all administration related values. type ConnectData struct { Number int // number of the box, starting with 1. @@ -74,74 +71,64 @@ var registry = map[string]createFunc{} // Register the encoder for later retrieval. func Register(scheme string, create createFunc) { if _, ok := registry[scheme]; ok { - panic(scheme) + log.Fatalf("Box with scheme %q already registered", scheme) } registry[scheme] = create } + +// GetSchemes returns all registered scheme, ordered by scheme string. +func GetSchemes() []string { + result := make([]string, 0, len(registry)) + for scheme := range registry { + result = append(result, scheme) + } + sort.Strings(result) + return result +} // Manager is a coordinating box. type Manager struct { - mgrLog *logger.Logger - stateMx sync.RWMutex - state box.StartState mgrMx sync.RWMutex + started bool rtConfig config.Config boxes []box.ManagedBox observers []box.UpdateFunc mxObserver sync.RWMutex done chan struct{} infos chan box.UpdateInfo - propertyKeys strfun.Set // Set of property key names + propertyKeys map[string]bool // Set of property key names // Indexer data - idxLog *logger.Logger idxStore store.Store - idxAr *anteroomQueue + idxAr *anterooms idxReady chan struct{} // Signal a non-empty anteroom to background task // Indexer stats data idxMx sync.RWMutex idxLastReload time.Time idxDurReload time.Duration idxSinceReload uint64 } -func (mgr *Manager) setState(newState box.StartState) { - mgr.stateMx.Lock() - mgr.state = newState - mgr.stateMx.Unlock() -} - -func (mgr *Manager) State() box.StartState { - mgr.stateMx.RLock() - state := mgr.state - mgr.stateMx.RUnlock() - return state -} - // New creates a new managing box. func New(boxURIs []*url.URL, authManager auth.BaseManager, rtConfig config.Config) (*Manager, error) { - descrs := meta.GetSortedKeyDescriptions() - propertyKeys := make(strfun.Set, len(descrs)) - for _, kd := range descrs { + propertyKeys := make(map[string]bool) + for _, kd := range meta.GetSortedKeyDescriptions() { if kd.IsProperty() { - propertyKeys.Set(kd.Name) + propertyKeys[kd.Name] = true } } - boxLog := kernel.Main.GetLogger(kernel.BoxService) mgr := &Manager{ - mgrLog: boxLog.Clone().Str("box", "manager").Child(), rtConfig: rtConfig, infos: make(chan box.UpdateInfo, len(boxURIs)*10), propertyKeys: propertyKeys, - idxLog: boxLog.Clone().Str("box", "index").Child(), - idxStore: createIdxStore(rtConfig), - idxAr: newAnteroomQueue(1000), + idxStore: memstore.New(), + idxAr: newAnterooms(10), idxReady: make(chan struct{}, 1), } cdata := ConnectData{Number: 1, Config: rtConfig, Enricher: mgr, Notify: mgr.infos} boxes := make([]box.ManagedBox, 0, len(boxURIs)+2) for _, uri := range boxURIs { @@ -167,14 +154,10 @@ 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 { mgr.mxObserver.Lock() @@ -181,210 +164,120 @@ mgr.observers = append(mgr.observers, f) mgr.mxObserver.Unlock() } } -func (mgr *Manager) notifier() { - // The call to notify may panic. Ensure a running notifier. - defer func() { - if ri := recover(); ri != nil { - kernel.Main.LogRecover("Notifier", ri) - go mgr.notifier() - } - }() - - tsLastEvent := time.Now() - cache := destutterCache{} - for { - select { - case ci, ok := <-mgr.infos: - if ok { - now := time.Now() - if len(cache) > 1 && tsLastEvent.Add(10*time.Second).Before(now) { - // Cache contains entries and is definitely outdated - mgr.mgrLog.Trace().Msg("clean destutter cache") - cache = destutterCache{} - } - tsLastEvent = now - - reason, zid := ci.Reason, ci.Zid - mgr.mgrLog.Debug().Uint("reason", uint64(reason)).Zid(zid).Msg("notifier") - if ignoreUpdate(cache, now, reason, zid) { - mgr.mgrLog.Trace().Uint("reason", uint64(reason)).Zid(zid).Msg("notifier ignored") - continue - } - - mgr.idxEnqueue(reason, zid) - if ci.Box == nil { - ci.Box = mgr - } - if mgr.State() == box.StartStateStarted { - mgr.notifyObserver(&ci) - } - } - case <-mgr.done: - return - } - } -} - -type destutterData struct { - deadAt time.Time - reason box.UpdateReason -} -type destutterCache = map[id.Zid]destutterData - -func ignoreUpdate(cache destutterCache, now time.Time, reason box.UpdateReason, zid id.Zid) bool { - if dsd, found := cache[zid]; found { - if dsd.reason == reason && dsd.deadAt.After(now) { - return true - } - } - cache[zid] = destutterData{ - deadAt: now.Add(500 * time.Millisecond), - reason: reason, - } - return false -} - -func (mgr *Manager) idxEnqueue(reason box.UpdateReason, zid id.Zid) { - switch reason { - case box.OnReady: - return - case box.OnReload: - mgr.idxAr.Reset() - case box.OnZettel: - mgr.idxAr.EnqueueZettel(zid) - default: - mgr.mgrLog.Error().Uint("reason", uint64(reason)).Zid(zid).Msg("Unknown notification reason") - return - } - select { - case mgr.idxReady <- struct{}{}: - default: - } -} - func (mgr *Manager) notifyObserver(ci *box.UpdateInfo) { mgr.mxObserver.RLock() observers := mgr.observers mgr.mxObserver.RUnlock() for _, ob := range observers { ob(*ci) } } + +func (mgr *Manager) notifier() { + // The call to notify may panic. Ensure a running notifier. + defer func() { + if r := recover(); r != nil { + kernel.Main.LogRecover("Notifier", r) + go mgr.notifier() + } + }() + + for { + select { + case ci, ok := <-mgr.infos: + if ok { + mgr.idxEnqueue(ci.Reason, ci.Zid) + if ci.Box == nil { + ci.Box = mgr + } + mgr.notifyObserver(&ci) + } + case <-mgr.done: + return + } + } +} + +func (mgr *Manager) idxEnqueue(reason box.UpdateReason, zid id.Zid) { + switch reason { + case box.OnReload: + mgr.idxAr.Reset() + case box.OnUpdate: + mgr.idxAr.Enqueue(zid, arUpdate) + case box.OnDelete: + mgr.idxAr.Enqueue(zid, arDelete) + default: + return + } + select { + case mgr.idxReady <- struct{}{}: + default: + } +} // Start the box. Now all other functions of the box are allowed. // Starting an already started box is not allowed. func (mgr *Manager) Start(ctx context.Context) error { mgr.mgrMx.Lock() - defer mgr.mgrMx.Unlock() - if mgr.State() != box.StartStateStopped { + if mgr.started { + mgr.mgrMx.Unlock() return box.ErrStarted } - mgr.setState(box.StartStateStarting) for i := len(mgr.boxes) - 1; i >= 0; i-- { ssi, ok := mgr.boxes[i].(box.StartStopper) if !ok { continue } err := ssi.Start(ctx) if err == nil { continue } - mgr.setState(box.StartStateStopping) for j := i + 1; j < len(mgr.boxes); j++ { - if ssj, ok2 := mgr.boxes[j].(box.StartStopper); ok2 { + if ssj, ok := mgr.boxes[j].(box.StartStopper); ok { ssj.Stop(ctx) } } - mgr.setState(box.StartStateStopped) + mgr.mgrMx.Unlock() return err } mgr.idxAr.Reset() // Ensure an initial index run mgr.done = make(chan struct{}) go mgr.notifier() - - mgr.waitBoxesAreStarted() - mgr.setState(box.StartStateStarted) - mgr.notifyObserver(&box.UpdateInfo{Box: mgr, Reason: box.OnReady}) - go mgr.idxIndexer() - return nil -} - -func (mgr *Manager) waitBoxesAreStarted() { - 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.Info().Msg("Waiting for more than one minute to start") - } else { - mgr.mgrLog.Trace().Msg("Wait for boxes to start") - } - } - time.Sleep(waitTime) - } -} - -func (mgr *Manager) allBoxesStarted() bool { - for _, bx := range mgr.boxes { - if b, ok := bx.(box.StartStopper); ok && b.State() != box.StartStateStarted { - return false - } - } - return true + + // mgr.startIndexer(mgr) + mgr.started = true + mgr.mgrMx.Unlock() + mgr.infos <- box.UpdateInfo{Reason: box.OnReload, Zid: id.Invalid} + return nil } // Stop the started box. Now only the Start() function is allowed. -func (mgr *Manager) Stop(ctx context.Context) { - mgr.mgrMx.Lock() - defer mgr.mgrMx.Unlock() - if mgr.State() != box.StartStateStarted { - return - } - mgr.setState(box.StartStateStopping) - close(mgr.done) - for _, p := range mgr.boxes { - if ss, ok := p.(box.StartStopper); ok { - ss.Stop(ctx) - } - } - mgr.setState(box.StartStateStopped) -} - -// Refresh internal box data. -func (mgr *Manager) Refresh(ctx context.Context) error { - mgr.mgrLog.Debug().Msg("Refresh") - if mgr.State() != box.StartStateStarted { - return box.ErrStopped - } - mgr.infos <- box.UpdateInfo{Reason: box.OnReload, Zid: id.Invalid} - mgr.mgrMx.Lock() - defer mgr.mgrMx.Unlock() - for _, bx := range mgr.boxes { - if rb, ok := bx.(box.Refresher); ok { - rb.Refresh(ctx) - } - } - return nil -} - -// ReIndex data of the given zettel. -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.OnZettel, Zid: zid} - return nil +func (mgr *Manager) Stop(ctx context.Context) error { + mgr.mgrMx.Lock() + defer mgr.mgrMx.Unlock() + if !mgr.started { + return box.ErrStopped + } + close(mgr.done) + var err error + for _, p := range mgr.boxes { + if ss, ok := p.(box.StartStopper); ok { + if err1 := ss.Stop(ctx); err1 != nil && err == nil { + err = err1 + } + } + } + mgr.started = false + return err } // ReadStats populates st with box statistics. func (mgr *Manager) ReadStats(st *box.Stats) { - mgr.mgrLog.Debug().Msg("ReadStats") mgr.mgrMx.RLock() defer mgr.mgrMx.RUnlock() subStats := make([]box.ManagedBoxStats, len(mgr.boxes)) for i, p := range mgr.boxes { p.ReadStats(&subStats[i]) DELETED box/manager/mapstore/mapstore.go Index: box/manager/mapstore/mapstore.go ================================================================== --- box/manager/mapstore/mapstore.go +++ box/manager/mapstore/mapstore.go @@ -1,717 +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. -// -// 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" - - "t73f.de/r/zsc/api" - "t73f.de/r/zsc/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]) - } -} DELETED box/manager/mapstore/refs.go Index: box/manager/mapstore/refs.go ================================================================== --- box/manager/mapstore/refs.go +++ box/manager/mapstore/refs.go @@ -1,105 +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. -// -// 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 -} DELETED box/manager/mapstore/refs_test.go Index: box/manager/mapstore/refs_test.go ================================================================== --- box/manager/mapstore/refs_test.go +++ box/manager/mapstore/refs_test.go @@ -1,140 +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. -// -// 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) - } -} ADDED box/manager/memstore/memstore.go Index: box/manager/memstore/memstore.go ================================================================== --- box/manager/memstore/memstore.go +++ box/manager/memstore/memstore.go @@ -0,0 +1,580 @@ +//----------------------------------------------------------------------------- +// Copyright (c) 2021 Detlef Stern +// +// This file is part of zettelstore. +// +// Zettelstore is licensed under the latest version of the EUPL (European Union +// Public License). Please see file LICENSE.txt for your rights and obligations +// under this license. +//----------------------------------------------------------------------------- + +// Package memstore stored the index in main memory. +package memstore + +import ( + "context" + "fmt" + "io" + "sort" + "strings" + "sync" + + "zettelstore.de/z/box/manager/store" + "zettelstore.de/z/domain/id" + "zettelstore.de/z/domain/meta" +) + +type metaRefs struct { + forward id.Slice + backward id.Slice +} + +type zettelIndex struct { + dead id.Slice + forward id.Slice + backward id.Slice + meta map[string]metaRefs + words []string + urls []string +} + +func (zi *zettelIndex) isEmpty() bool { + if len(zi.forward) > 0 || len(zi.backward) > 0 || len(zi.dead) > 0 || len(zi.words) > 0 { + return false + } + return zi.meta == nil || len(zi.meta) == 0 +} + +type stringRefs map[string]id.Slice + +type memStore struct { + mx sync.RWMutex + idx map[id.Zid]*zettelIndex + dead map[id.Zid]id.Slice // map dead refs where they occur + words stringRefs + urls stringRefs + + // Stats + updates uint64 +} + +// New returns a new memory-based index store. +func New() store.Store { + return &memStore{ + idx: make(map[id.Zid]*zettelIndex), + dead: make(map[id.Zid]id.Slice), + words: make(stringRefs), + urls: make(stringRefs), + } +} + +func (ms *memStore) Enrich(ctx context.Context, m *meta.Meta) { + if ms.doEnrich(ctx, m) { + ms.mx.Lock() + ms.updates++ + ms.mx.Unlock() + } +} + +func (ms *memStore) doEnrich(ctx context.Context, 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(meta.KeyDead, zi.dead.String()) + updated = true + } + back := removeOtherMetaRefs(m, zi.backward.Copy()) + if len(zi.backward) > 0 { + m.Set(meta.KeyBackward, zi.backward.String()) + updated = true + } + if len(zi.forward) > 0 { + m.Set(meta.KeyForward, zi.forward.String()) + back = remRefs(back, zi.forward) + updated = true + } + if len(zi.meta) > 0 { + for k, refs := range zi.meta { + if len(refs.backward) > 0 { + m.Set(k, refs.backward.String()) + back = remRefs(back, refs.backward) + updated = true + } + } + } + if len(back) > 0 { + m.Set(meta.KeyBack, back.String()) + updated = true + } + 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.AddSlice(refs) + } + if refs, ok := ms.urls[word]; ok { + result.AddSlice(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 + } + minZid, err := id.Parse(prefix + "00000000000000"[:14-l]) + if err != nil { + return result + } + maxZid, err := id.Parse(prefix + "99999999999999"[: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.AddSlice(refs) + } + for u, refs := range ms.urls { + if !pred(u, s) { + continue + } + result.AddSlice(refs) + } + return result +} + +func addBackwardZids(result id.Set, zid id.Zid, zi *zettelIndex) { + // Must only be called if ms.mx is read-locked! + result[zid] = true + result.AddSlice(zi.backward) + for _, mref := range zi.meta { + result.AddSlice(mref.backward) + } +} + +func removeOtherMetaRefs(m *meta.Meta, back id.Slice) id.Slice { + for _, p := range m.PairsRest(false) { + 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(ctx context.Context, zidx *store.ZettelIndex) id.Set { + ms.mx.Lock() + defer ms.mx.Unlock() + zi, ziExist := ms.idx[zidx.Zid] + if !ziExist || zi == nil { + zi = &zettelIndex{} + 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) + } + + ms.updateDeadReferences(zidx, zi) + ms.updateForwardBackwardReferences(zidx, zi) + ms.updateMetadataReferences(zidx, zi) + zi.words = updateWordSet(zidx.Zid, ms.words, zi.words, zidx.GetWords()) + zi.urls = updateWordSet(zidx.Zid, ms.urls, zi.urls, zidx.GetUrls()) + + // Check if zi must be inserted into ms.idx + if !ziExist && !zi.isEmpty() { + ms.idx[zidx.Zid] = zi + } + + return toCheck +} + +func (ms *memStore) updateDeadReferences(zidx *store.ZettelIndex, zi *zettelIndex) { + // 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 *zettelIndex) { + // Must only be called if ms.mx is write-locked! + brefs := zidx.GetBackRefs() + newRefs, remRefs := refsDiff(brefs, zi.forward) + zi.forward = brefs + for _, ref := range remRefs { + bzi := ms.getEntry(ref) + bzi.backward = remRef(bzi.backward, zidx.Zid) + } + for _, ref := range newRefs { + bzi := ms.getEntry(ref) + bzi.backward = addRef(bzi.backward, zidx.Zid) + } +} + +func (ms *memStore) updateMetadataReferences(zidx *store.ZettelIndex, zi *zettelIndex) { + // Must only be called if ms.mx is write-locked! + metarefs := zidx.GetMetaRefs() + for key, mr := range zi.meta { + if _, ok := metarefs[key]; ok { + continue + } + ms.removeInverseMeta(zidx.Zid, key, mr.forward) + } + if zi.meta == nil { + zi.meta = make(map[string]metaRefs) + } + for key, mrefs := range metarefs { + mr := zi.meta[key] + newRefs, remRefs := refsDiff(mrefs, mr.forward) + mr.forward = mrefs + zi.meta[key] = mr + + for _, ref := range newRefs { + bzi := ms.getEntry(ref) + if bzi.meta == nil { + bzi.meta = make(map[string]metaRefs) + } + bmr := bzi.meta[key] + bmr.backward = addRef(bmr.backward, zidx.Zid) + bzi.meta[key] = bmr + } + ms.removeInverseMeta(zidx.Zid, key, remRefs) + } +} + +func updateWordSet(zid id.Zid, srefs stringRefs, prev []string, next store.WordSet) []string { + // Must only be called if ms.mx is write-locked! + 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) getEntry(zid id.Zid) *zettelIndex { + // Must only be called if ms.mx is write-locked! + if zi, ok := ms.idx[zid]; ok { + return zi + } + zi := &zettelIndex{} + ms.idx[zid] = zi + return zi +} + +func (ms *memStore) DeleteZettel(ctx context.Context, zid id.Zid) id.Set { + ms.mx.Lock() + defer ms.mx.Unlock() + + zi, ok := ms.idx[zid] + if !ok { + return nil + } + + ms.deleteDeadSources(zid, zi) + toCheck := ms.deleteForwardBackward(zid, zi) + if len(zi.meta) > 0 { + for key, mrefs := range zi.meta { + ms.removeInverseMeta(zid, key, mrefs.forward) + } + } + ms.deleteWords(zid, zi.words) + delete(ms.idx, zid) + return toCheck +} + +func (ms *memStore) deleteDeadSources(zid id.Zid, zi *zettelIndex) { + // 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 *zettelIndex) id.Set { + // Must only be called if ms.mx is write-locked! + var toCheck id.Set + for _, ref := range zi.forward { + if fzi, ok := ms.idx[ref]; ok { + fzi.backward = remRef(fzi.backward, zid) + } + } + for _, ref := range zi.backward { + if bzi, ok := ms.idx[ref]; ok { + bzi.forward = remRef(bzi.forward, zid) + if toCheck == nil { + toCheck = id.NewSet() + } + toCheck[ref] = true + } + } + 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.meta == nil { + continue + } + bmr, ok := bzi.meta[key] + if !ok { + continue + } + bmr.backward = remRef(bmr.backward, zid) + if len(bmr.backward) > 0 || len(bmr.forward) > 0 { + bzi.meta[key] = bmr + } else { + delete(bzi.meta, key) + if len(bzi.meta) == 0 { + bzi.meta = nil + } + } + } +} + +func (ms *memStore) deleteWords(zid id.Zid, words []string) { + // Must only be called if ms.mx is write-locked! + for _, word := range words { + refs, ok := ms.words[word] + if !ok { + continue + } + refs2 := remRef(refs, zid) + if len(refs2) == 0 { + delete(ms.words, word) + continue + } + ms.words[word] = refs2 + } +} + +func (ms *memStore) ReadStats(st *store.Stats) { + ms.mx.RLock() + st.Zettel = len(ms.idx) + st.Updates = ms.updates + st.Words = uint64(len(ms.words)) + st.Urls = uint64(len(ms.urls)) + ms.mx.RUnlock() +} + +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.meta { + 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) + slice := make([]string, 0, len(srefs)) + for s := range srefs { + slice = append(slice, s) + } + sort.Strings(slice) + for _, s := range slice { + fmt.Fprintf(w, "; %s%s%s\n", preString, s, postString) + fmt.Fprintln(w, ":", srefs[s]) + } +} ADDED box/manager/memstore/refs.go Index: box/manager/memstore/refs.go ================================================================== --- box/manager/memstore/refs.go +++ box/manager/memstore/refs.go @@ -0,0 +1,101 @@ +//----------------------------------------------------------------------------- +// Copyright (c) 2021 Detlef Stern +// +// This file is part of zettelstore. +// +// Zettelstore is licensed under the latest version of the EUPL (European Union +// Public License). Please see file LICENSE.txt for your rights and obligations +// under this license. +//----------------------------------------------------------------------------- + +// Package memstore stored the index in main memory. +package memstore + +import "zettelstore.de/z/domain/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 = append(refs, id.Invalid) + copy(refs[hi+1:], refs[hi:]) + 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/memstore/refs_test.go Index: box/manager/memstore/refs_test.go ================================================================== --- box/manager/memstore/refs_test.go +++ box/manager/memstore/refs_test.go @@ -0,0 +1,138 @@ +//----------------------------------------------------------------------------- +// Copyright (c) 2021 Detlef Stern +// +// This file is part of zettelstore. +// +// Zettelstore is licensed under the latest version of the EUPL (European Union +// Public License). Please see file LICENSE.txt for your rights and obligations +// under this license. +//----------------------------------------------------------------------------- + +// Package memstore stored the index in main memory. +package memstore + +import ( + "testing" + + "zettelstore.de/z/domain/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 @@ -1,28 +1,25 @@ //----------------------------------------------------------------------------- -// Copyright (c) 2021-present Detlef Stern +// Copyright (c) 2021 Detlef Stern // -// This file is part of Zettelstore. +// This file is part of zettelstore. // // Zettelstore is licensed under the latest version of the EUPL (European Union // Public License). Please see file LICENSE.txt for your rights and obligations // under this license. -// -// 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 import ( "context" "io" - "zettelstore.de/z/query" - "zettelstore.de/z/zettel/id" - "zettelstore.de/z/zettel/meta" + "zettelstore.de/z/domain/id" + "zettelstore.de/z/domain/meta" + "zettelstore.de/z/search" ) // Stats records statistics about the store. type Stats struct { // Zettel is the number of zettel managed by the indexer. @@ -39,26 +36,19 @@ } // Store all relevant zettel data. There may be multiple implementations, i.e. // memory-based, file-based, based on SQLite, ... type Store interface { - query.Searcher - - // GetMeta returns the metadata of the zettel with the given identifier. - GetMeta(context.Context, id.Zid) (*meta.Meta, error) + search.Searcher // Entrich metadata with data from store. Enrich(ctx context.Context, m *meta.Meta) // UpdateReferences for a specific zettel. // Returns set of zettel identifier that must also be checked for changes. UpdateReferences(context.Context, *ZettelIndex) id.Set - // RenameZettel changes all references of current zettel identifier to new - // zettel identifier. - RenameZettel(_ context.Context, curZid, newZid id.Zid) id.Set - // DeleteZettel removes index data for given zettel. // Returns set of zettel identifier that must also be checked for changes. DeleteZettel(context.Context, id.Zid) id.Set // ReadStats populates st with store statistics. Index: box/manager/store/wordset.go ================================================================== --- box/manager/store/wordset.go +++ box/manager/store/wordset.go @@ -1,18 +1,16 @@ //----------------------------------------------------------------------------- -// Copyright (c) 2021-present Detlef Stern +// Copyright (c) 2021 Detlef Stern // -// This file is part of Zettelstore. +// This file is part of zettelstore. // // Zettelstore is licensed under the latest version of the EUPL (European Union // Public License). Please see file LICENSE.txt for your rights and obligations // under this license. -// -// 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 // WordSet contains the set of all words, with the count of their occurrences. type WordSet map[string]int Index: box/manager/store/wordset_test.go ================================================================== --- box/manager/store/wordset_test.go +++ box/manager/store/wordset_test.go @@ -1,18 +1,16 @@ //----------------------------------------------------------------------------- -// Copyright (c) 2021-present Detlef Stern +// Copyright (c) 2021 Detlef Stern // -// This file is part of Zettelstore. +// This file is part of zettelstore. // // Zettelstore is licensed under the latest version of the EUPL (European Union // Public License). Please see file LICENSE.txt for your rights and obligations // under this license. -// -// 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_test import ( "sort" "testing" Index: box/manager/store/zettel.go ================================================================== --- box/manager/store/zettel.go +++ box/manager/store/zettel.go @@ -1,90 +1,84 @@ //----------------------------------------------------------------------------- -// Copyright (c) 2021-present Detlef Stern +// Copyright (c) 2021 Detlef Stern // -// This file is part of Zettelstore. +// This file is part of zettelstore. // // Zettelstore is licensed under the latest version of the EUPL (European Union // Public License). Please see file LICENSE.txt for your rights and obligations // under this license. -// -// 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 -import ( - "zettelstore.de/z/zettel/id" - "zettelstore.de/z/zettel/meta" -) +import "zettelstore.de/z/domain/id" // ZettelIndex contains all index data of a zettel. type ZettelIndex struct { - Zid id.Zid // zid of the indexed zettel - meta *meta.Meta // full metadata - backrefs id.Set // set of back references - inverseRefs map[string]id.Set // references of inverse keys - deadrefs id.Set // set of dead references - words WordSet - urls WordSet + Zid id.Zid // zid of the indexed zettel + backrefs id.Set // set of back references + metarefs map[string]id.Set // references to inverse keys + deadrefs id.Set // set of dead references + words WordSet + urls WordSet } // NewZettelIndex creates a new zettel index. -func NewZettelIndex(m *meta.Meta) *ZettelIndex { +func NewZettelIndex(zid id.Zid) *ZettelIndex { return &ZettelIndex{ - Zid: m.Zid, - meta: m, - backrefs: id.NewSet(), - inverseRefs: make(map[string]id.Set), - deadrefs: id.NewSet(), + Zid: zid, + backrefs: id.NewSet(), + metarefs: make(map[string]id.Set), + deadrefs: id.NewSet(), } } // AddBackRef adds a reference to a zettel where the current zettel links to // without any more information. func (zi *ZettelIndex) AddBackRef(zid id.Zid) { - zi.backrefs.Add(zid) + zi.backrefs[zid] = true } -// AddInverseRef adds a named reference to a zettel. On that zettel, the given +// AddMetaRef adds a named reference to a zettel. On that zettel, the given // metadata key should point back to the current zettel. -func (zi *ZettelIndex) AddInverseRef(key string, zid id.Zid) { - if zids, ok := zi.inverseRefs[key]; ok { - zids.Add(zid) +func (zi *ZettelIndex) AddMetaRef(key string, zid id.Zid) { + if zids, ok := zi.metarefs[key]; ok { + zids[zid] = true return } - zi.inverseRefs[key] = id.NewSet(zid) + zi.metarefs[key] = id.NewSet(zid) } // AddDeadRef adds a dead reference to a zettel. func (zi *ZettelIndex) AddDeadRef(zid id.Zid) { - zi.deadrefs.Add(zid) + zi.deadrefs[zid] = true } // SetWords sets the words to the given value. func (zi *ZettelIndex) SetWords(words WordSet) { zi.words = words } // SetUrls sets the words to the given value. func (zi *ZettelIndex) SetUrls(urls WordSet) { zi.urls = urls } // GetDeadRefs returns all dead references as a sorted list. -func (zi *ZettelIndex) GetDeadRefs() id.Slice { return zi.deadrefs.Sorted() } - -// GetMeta return just the raw metadata. -func (zi *ZettelIndex) GetMeta() *meta.Meta { return zi.meta } +func (zi *ZettelIndex) GetDeadRefs() id.Slice { + return zi.deadrefs.Sorted() +} // GetBackRefs returns all back references as a sorted list. -func (zi *ZettelIndex) GetBackRefs() id.Slice { return zi.backrefs.Sorted() } +func (zi *ZettelIndex) GetBackRefs() id.Slice { + return zi.backrefs.Sorted() +} -// GetInverseRefs returns all inverse meta references as a map of strings to a sorted list of references -func (zi *ZettelIndex) GetInverseRefs() map[string]id.Slice { - if len(zi.inverseRefs) == 0 { +// GetMetaRefs returns all meta references as a map of strings to a sorted list of references +func (zi *ZettelIndex) GetMetaRefs() map[string]id.Slice { + if len(zi.metarefs) == 0 { return nil } - result := make(map[string]id.Slice, len(zi.inverseRefs)) - for key, refs := range zi.inverseRefs { + result := make(map[string]id.Slice, len(zi.metarefs)) + for key, refs := range zi.metarefs { result[key] = refs.Sorted() } return result } Index: box/membox/membox.go ================================================================== --- box/membox/membox.go +++ box/membox/membox.go @@ -1,16 +1,13 @@ //----------------------------------------------------------------------------- -// Copyright (c) 2020-present Detlef Stern +// Copyright (c) 2020-2021 Detlef Stern // -// This file is part of Zettelstore. +// This file is part of zettelstore. // // Zettelstore is licensed under the latest version of the EUPL (European Union // Public License). Please see file LICENSE.txt for your rights and obligations // under this license. -// -// SPDX-License-Identifier: EUPL-1.2 -// SPDX-FileCopyrightText: 2020-present Detlef Stern //----------------------------------------------------------------------------- // Package membox stores zettel volatile in main memory. package membox @@ -19,246 +16,185 @@ "net/url" "sync" "zettelstore.de/z/box" "zettelstore.de/z/box/manager" - "zettelstore.de/z/kernel" - "zettelstore.de/z/logger" - "zettelstore.de/z/query" - "zettelstore.de/z/zettel" - "zettelstore.de/z/zettel/id" + "zettelstore.de/z/domain" + "zettelstore.de/z/domain/id" + "zettelstore.de/z/domain/meta" + "zettelstore.de/z/search" ) func init() { manager.Register( "mem", func(u *url.URL, cdata *manager.ConnectData) (box.ManagedBox, error) { - return &memBox{ - log: kernel.Main.GetLogger(kernel.BoxService).Clone(). - Str("box", "mem").Int("boxnum", int64(cdata.Number)).Child(), - u: u, - cdata: *cdata, - maxZettel: box.GetQueryInt(u, "max-zettel", 0, 127, 65535), - maxBytes: box.GetQueryInt(u, "max-bytes", 0, 65535, (1024*1024*1024)-1), - }, nil + return &memBox{u: u, cdata: *cdata}, nil }) } type memBox struct { - log *logger.Logger - u *url.URL - cdata manager.ConnectData - maxZettel int - maxBytes int - mx sync.RWMutex // Protects the following fields - zettel map[id.Zid]zettel.Zettel - curBytes int -} - -func (mb *memBox) notifyChanged(zid id.Zid) { - if chci := mb.cdata.Notify; chci != nil { - chci <- box.UpdateInfo{Box: mb, Reason: box.OnZettel, Zid: zid} - } -} - -func (mb *memBox) Location() string { - return mb.u.String() -} - -func (mb *memBox) State() box.StartState { - mb.mx.RLock() - defer mb.mx.RUnlock() - if mb.zettel == nil { - return box.StartStateStopped - } - return box.StartStateStarted -} - -func (mb *memBox) Start(context.Context) error { - mb.mx.Lock() - mb.zettel = make(map[id.Zid]zettel.Zettel) - mb.curBytes = 0 - mb.mx.Unlock() - mb.log.Trace().Int("max-zettel", int64(mb.maxZettel)).Int("max-bytes", int64(mb.maxBytes)).Msg("Start Box") + u *url.URL + cdata manager.ConnectData + zettel map[id.Zid]domain.Zettel + mx sync.RWMutex +} + +func (mp *memBox) notifyChanged(reason box.UpdateReason, zid id.Zid) { + if chci := mp.cdata.Notify; chci != nil { + chci <- box.UpdateInfo{Reason: reason, Zid: zid} + } +} + +func (mp *memBox) Location() string { + return mp.u.String() +} + +func (mp *memBox) Start(ctx context.Context) error { + mp.mx.Lock() + mp.zettel = make(map[id.Zid]domain.Zettel) + mp.mx.Unlock() + return nil +} + +func (mp *memBox) Stop(ctx context.Context) error { + mp.mx.Lock() + mp.zettel = nil + mp.mx.Unlock() return nil } -func (mb *memBox) Stop(context.Context) { - mb.mx.Lock() - mb.zettel = nil - mb.mx.Unlock() -} - -func (mb *memBox) CanCreateZettel(context.Context) bool { - mb.mx.RLock() - defer mb.mx.RUnlock() - return len(mb.zettel) < mb.maxZettel -} - -func (mb *memBox) CreateZettel(_ context.Context, zettel zettel.Zettel) (id.Zid, error) { - mb.mx.Lock() - newBytes := mb.curBytes + zettel.Length() - if mb.maxZettel < len(mb.zettel) || mb.maxBytes < newBytes { - mb.mx.Unlock() - return id.Invalid, box.ErrCapacity - } +func (mp *memBox) CanCreateZettel(ctx context.Context) bool { return true } + +func (mp *memBox) CreateZettel(ctx context.Context, zettel domain.Zettel) (id.Zid, error) { + mp.mx.Lock() zid, err := box.GetNewZid(func(zid id.Zid) (bool, error) { - _, ok := mb.zettel[zid] + _, ok := mp.zettel[zid] return !ok, nil }) if err != nil { - mb.mx.Unlock() + mp.mx.Unlock() return id.Invalid, err } meta := zettel.Meta.Clone() meta.Zid = zid zettel.Meta = meta - mb.zettel[zid] = zettel - mb.curBytes = newBytes - mb.mx.Unlock() - mb.notifyChanged(zid) - mb.log.Trace().Zid(zid).Msg("CreateZettel") - return zid, nil -} - -func (mb *memBox) GetZettel(_ context.Context, zid id.Zid) (zettel.Zettel, error) { - mb.mx.RLock() - z, ok := mb.zettel[zid] - mb.mx.RUnlock() - if !ok { - return zettel.Zettel{}, box.ErrZettelNotFound{Zid: zid} - } - z.Meta = z.Meta.Clone() - mb.log.Trace().Msg("GetZettel") - return z, nil -} - -func (mb *memBox) HasZettel(_ context.Context, zid id.Zid) bool { - mb.mx.RLock() - _, found := mb.zettel[zid] - mb.mx.RUnlock() - return found -} - -func (mb *memBox) ApplyZid(_ context.Context, handle box.ZidFunc, constraint query.RetrievePredicate) error { - mb.mx.RLock() - defer mb.mx.RUnlock() - mb.log.Trace().Int("entries", int64(len(mb.zettel))).Msg("ApplyZid") - for zid := range mb.zettel { - if constraint(zid) { - handle(zid) - } - } - return nil -} - -func (mb *memBox) ApplyMeta(ctx context.Context, handle box.MetaFunc, constraint query.RetrievePredicate) error { - mb.mx.RLock() - defer mb.mx.RUnlock() - mb.log.Trace().Int("entries", int64(len(mb.zettel))).Msg("ApplyMeta") - for zid, zettel := range mb.zettel { - if constraint(zid) { - m := zettel.Meta.Clone() - mb.cdata.Enricher.Enrich(ctx, m, mb.cdata.Number) - handle(m) - } - } - return nil -} - -func (mb *memBox) CanUpdateZettel(_ context.Context, zettel zettel.Zettel) bool { - mb.mx.RLock() - defer mb.mx.RUnlock() - zid := zettel.Meta.Zid - if !zid.IsValid() { - return false - } - - newBytes := mb.curBytes + zettel.Length() - if prevZettel, found := mb.zettel[zid]; found { - newBytes -= prevZettel.Length() - } - return newBytes < mb.maxBytes -} - -func (mb *memBox) UpdateZettel(_ context.Context, zettel zettel.Zettel) error { - m := zettel.Meta.Clone() - if !m.Zid.IsValid() { - return box.ErrInvalidZid{Zid: m.Zid.String()} - } - - mb.mx.Lock() - newBytes := mb.curBytes + zettel.Length() - if prevZettel, found := mb.zettel[m.Zid]; found { - newBytes -= prevZettel.Length() - } - if mb.maxBytes < newBytes { - mb.mx.Unlock() - return box.ErrCapacity - } - - zettel.Meta = m - mb.zettel[m.Zid] = zettel - mb.curBytes = newBytes - mb.mx.Unlock() - mb.notifyChanged(m.Zid) - mb.log.Trace().Msg("UpdateZettel") - return nil -} - -func (*memBox) AllowRenameZettel(context.Context, id.Zid) bool { return true } - -func (mb *memBox) RenameZettel(_ context.Context, curZid, newZid id.Zid) error { - mb.mx.Lock() - zettel, ok := mb.zettel[curZid] - if !ok { - mb.mx.Unlock() - return box.ErrZettelNotFound{Zid: curZid} - } - - // Check that there is no zettel with newZid - if _, ok = mb.zettel[newZid]; ok { - mb.mx.Unlock() - return box.ErrInvalidZid{Zid: newZid.String()} + mp.zettel[zid] = zettel + mp.mx.Unlock() + mp.notifyChanged(box.OnUpdate, zid) + return zid, nil +} + +func (mp *memBox) GetZettel(ctx context.Context, zid id.Zid) (domain.Zettel, error) { + mp.mx.RLock() + zettel, ok := mp.zettel[zid] + mp.mx.RUnlock() + if !ok { + return domain.Zettel{}, box.ErrNotFound + } + zettel.Meta = zettel.Meta.Clone() + return zettel, nil +} + +func (mp *memBox) GetMeta(ctx context.Context, zid id.Zid) (*meta.Meta, error) { + mp.mx.RLock() + zettel, ok := mp.zettel[zid] + mp.mx.RUnlock() + if !ok { + return nil, box.ErrNotFound + } + return zettel.Meta.Clone(), nil +} + +func (mp *memBox) FetchZids(ctx context.Context) (id.Set, error) { + mp.mx.RLock() + result := id.NewSetCap(len(mp.zettel)) + for zid := range mp.zettel { + result[zid] = true + } + mp.mx.RUnlock() + return result, nil +} + +func (mp *memBox) SelectMeta(ctx context.Context, match search.MetaMatchFunc) ([]*meta.Meta, error) { + result := make([]*meta.Meta, 0, len(mp.zettel)) + mp.mx.RLock() + for _, zettel := range mp.zettel { + m := zettel.Meta.Clone() + mp.cdata.Enricher.Enrich(ctx, m, mp.cdata.Number) + if match(m) { + result = append(result, m) + } + } + mp.mx.RUnlock() + return result, nil +} + +func (mp *memBox) CanUpdateZettel(ctx context.Context, zettel domain.Zettel) bool { + return true +} + +func (mp *memBox) UpdateZettel(ctx context.Context, zettel domain.Zettel) error { + mp.mx.Lock() + meta := zettel.Meta.Clone() + if !meta.Zid.IsValid() { + return &box.ErrInvalidID{Zid: meta.Zid} + } + zettel.Meta = meta + mp.zettel[meta.Zid] = zettel + mp.mx.Unlock() + mp.notifyChanged(box.OnUpdate, meta.Zid) + return nil +} + +func (mp *memBox) AllowRenameZettel(ctx context.Context, zid id.Zid) bool { return true } + +func (mp *memBox) RenameZettel(ctx context.Context, curZid, newZid id.Zid) error { + mp.mx.Lock() + zettel, ok := mp.zettel[curZid] + if !ok { + mp.mx.Unlock() + return box.ErrNotFound + } + + // Check that there is no zettel with newZid + if _, ok = mp.zettel[newZid]; ok { + mp.mx.Unlock() + return &box.ErrInvalidID{Zid: newZid} } meta := zettel.Meta.Clone() meta.Zid = newZid zettel.Meta = meta - mb.zettel[newZid] = zettel - delete(mb.zettel, curZid) - mb.mx.Unlock() - mb.notifyChanged(curZid) - mb.notifyChanged(newZid) - mb.log.Trace().Msg("RenameZettel") + mp.zettel[newZid] = zettel + delete(mp.zettel, curZid) + mp.mx.Unlock() + mp.notifyChanged(box.OnDelete, curZid) + mp.notifyChanged(box.OnUpdate, newZid) return nil } -func (mb *memBox) CanDeleteZettel(_ context.Context, zid id.Zid) bool { - mb.mx.RLock() - _, ok := mb.zettel[zid] - mb.mx.RUnlock() +func (mp *memBox) CanDeleteZettel(ctx context.Context, zid id.Zid) bool { + mp.mx.RLock() + _, ok := mp.zettel[zid] + mp.mx.RUnlock() return ok } -func (mb *memBox) DeleteZettel(_ context.Context, zid id.Zid) error { - mb.mx.Lock() - oldZettel, found := mb.zettel[zid] - if !found { - mb.mx.Unlock() - return box.ErrZettelNotFound{Zid: zid} - } - delete(mb.zettel, zid) - mb.curBytes -= oldZettel.Length() - mb.mx.Unlock() - mb.notifyChanged(zid) - mb.log.Trace().Msg("DeleteZettel") +func (mp *memBox) DeleteZettel(ctx context.Context, zid id.Zid) error { + mp.mx.Lock() + if _, ok := mp.zettel[zid]; !ok { + mp.mx.Unlock() + return box.ErrNotFound + } + delete(mp.zettel, zid) + mp.mx.Unlock() + mp.notifyChanged(box.OnDelete, zid) return nil } -func (mb *memBox) ReadStats(st *box.ManagedBoxStats) { +func (mp *memBox) ReadStats(st *box.ManagedBoxStats) { st.ReadOnly = false - mb.mx.RLock() - st.Zettel = len(mb.zettel) - mb.mx.RUnlock() - mb.log.Trace().Int("zettel", int64(st.Zettel)).Msg("ReadStats") + mp.mx.RLock() + st.Zettel = len(mp.zettel) + mp.mx.RUnlock() } ADDED box/merge.go Index: box/merge.go ================================================================== --- box/merge.go +++ box/merge.go @@ -0,0 +1,46 @@ +//----------------------------------------------------------------------------- +// Copyright (c) 2020-2021 Detlef Stern +// +// This file is part of zettelstore. +// +// Zettelstore is licensed under the latest version of the EUPL (European Union +// Public License). Please see file LICENSE.txt for your rights and obligations +// under this license. +//----------------------------------------------------------------------------- + +// Package box provides a generic interface to zettel boxes. +package box + +import "zettelstore.de/z/domain/meta" + +// MergeSorted returns a merged sequence of metadata, sorted by Zid. +// The lists first and second must be sorted descending by Zid. +func MergeSorted(first, second []*meta.Meta) []*meta.Meta { + lenFirst := len(first) + lenSecond := len(second) + result := make([]*meta.Meta, 0, lenFirst+lenSecond) + iFirst := 0 + iSecond := 0 + for iFirst < lenFirst && iSecond < lenSecond { + zidFirst := first[iFirst].Zid + zidSecond := second[iSecond].Zid + if zidFirst > zidSecond { + result = append(result, first[iFirst]) + iFirst++ + } else if zidFirst < zidSecond { + result = append(result, second[iSecond]) + iSecond++ + } else { // zidFirst == zidSecond + result = append(result, first[iFirst]) + iFirst++ + iSecond++ + } + } + if iFirst < lenFirst { + result = append(result, first[iFirst:]...) + } else { + result = append(result, second[iSecond:]...) + } + + return result +} DELETED box/notify/directory.go Index: box/notify/directory.go ================================================================== --- box/notify/directory.go +++ box/notify/directory.go @@ -1,613 +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. -// -// SPDX-License-Identifier: EUPL-1.2 -// SPDX-FileCopyrightText: 2020-present Detlef Stern -//----------------------------------------------------------------------------- - -package notify - -import ( - "errors" - "fmt" - "path/filepath" - "regexp" - "strings" - "sync" - - "zettelstore.de/z/box" - "zettelstore.de/z/kernel" - "zettelstore.de/z/logger" - "zettelstore.de/z/parser" - "zettelstore.de/z/query" - "zettelstore.de/z/strfun" - "zettelstore.de/z/zettel/id" -) - -type entrySet map[id.Zid]*DirEntry - -// DirServiceState signal the internal state of the service. -// -// The following state transitions are possible: -// --newDirService--> dsCreated -// dsCreated --Start--> dsStarting -// dsStarting --last list notification--> dsWorking -// dsWorking --directory missing--> dsMissing -// dsMissing --last list notification--> dsWorking -// --Stop--> dsStopping -type DirServiceState uint8 - -const ( - DsCreated DirServiceState = iota - DsStarting // Reading inital scan - DsWorking // Initial scan complete, fully operational - DsMissing // Directory is missing - DsStopping // Service is shut down -) - -// DirService specifies a directory service for file based zettel. -type DirService struct { - box box.ManagedBox - log *logger.Logger - dirPath string - notifier Notifier - infos chan<- box.UpdateInfo - mx sync.RWMutex // protects status, entries - state DirServiceState - entries entrySet -} - -// ErrNoDirectory signals missing directory data. -var ErrNoDirectory = errors.New("unable to retrieve zettel directory information") - -// NewDirService creates a new directory service. -func NewDirService(box box.ManagedBox, log *logger.Logger, notifier Notifier, chci chan<- box.UpdateInfo) *DirService { - return &DirService{ - box: box, - log: log, - notifier: notifier, - infos: chci, - state: DsCreated, - } -} - -// State the current service state. -func (ds *DirService) State() DirServiceState { - ds.mx.RLock() - state := ds.state - ds.mx.RUnlock() - return state -} - -// Start the directory service. -func (ds *DirService) Start() { - ds.mx.Lock() - ds.state = DsStarting - ds.mx.Unlock() - var newEntries entrySet - go ds.updateEvents(newEntries) -} - -// Refresh the directory entries. -func (ds *DirService) Refresh() { - ds.notifier.Refresh() -} - -// Stop the directory service. -func (ds *DirService) Stop() { - ds.mx.Lock() - ds.state = DsStopping - ds.mx.Unlock() - ds.notifier.Close() -} - -func (ds *DirService) logMissingEntry(action string) error { - err := ErrNoDirectory - ds.log.Info().Err(err).Str("action", action).Msg("Unable to get directory information") - return err -} - -// NumDirEntries returns the number of entries in the directory. -func (ds *DirService) NumDirEntries() int { - ds.mx.RLock() - defer ds.mx.RUnlock() - if ds.entries == nil { - return 0 - } - return len(ds.entries) -} - -// GetDirEntries returns a list of directory entries, which satisfy the given constraint. -func (ds *DirService) GetDirEntries(constraint query.RetrievePredicate) []*DirEntry { - ds.mx.RLock() - defer ds.mx.RUnlock() - if ds.entries == nil { - return nil - } - result := make([]*DirEntry, 0, len(ds.entries)) - for zid, entry := range ds.entries { - if constraint(zid) { - copiedEntry := *entry - result = append(result, &copiedEntry) - } - } - return result -} - -// GetDirEntry returns a directory entry with the given zid, or nil if not found. -func (ds *DirService) GetDirEntry(zid id.Zid) *DirEntry { - ds.mx.RLock() - defer ds.mx.RUnlock() - if ds.entries == nil { - return nil - } - foundEntry := ds.entries[zid] - if foundEntry == nil { - return nil - } - result := *foundEntry - return &result -} - -// SetNewDirEntry calculates an empty directory entry with an unused identifier and -// stores it in the directory. -func (ds *DirService) SetNewDirEntry() (id.Zid, error) { - ds.mx.Lock() - defer ds.mx.Unlock() - if ds.entries == nil { - return id.Invalid, ds.logMissingEntry("new") - } - zid, err := box.GetNewZid(func(zid id.Zid) (bool, error) { - _, found := ds.entries[zid] - return !found, nil - }) - if err != nil { - return id.Invalid, err - } - ds.entries[zid] = &DirEntry{Zid: zid} - return zid, nil -} - -// UpdateDirEntry updates an directory entry in place. -func (ds *DirService) UpdateDirEntry(updatedEntry *DirEntry) error { - entry := *updatedEntry - ds.mx.Lock() - defer ds.mx.Unlock() - if ds.entries == nil { - return ds.logMissingEntry("update") - } - ds.entries[entry.Zid] = &entry - return nil -} - -// RenameDirEntry replaces an existing directory entry with a new one. -func (ds *DirService) RenameDirEntry(oldEntry *DirEntry, newZid id.Zid) (DirEntry, error) { - ds.mx.Lock() - defer ds.mx.Unlock() - if ds.entries == nil { - return DirEntry{}, ds.logMissingEntry("rename") - } - if _, found := ds.entries[newZid]; found { - return DirEntry{}, box.ErrInvalidZid{Zid: newZid.String()} - } - oldZid := oldEntry.Zid - newEntry := DirEntry{ - Zid: newZid, - MetaName: renameFilename(oldEntry.MetaName, oldZid, newZid), - ContentName: renameFilename(oldEntry.ContentName, oldZid, newZid), - ContentExt: oldEntry.ContentExt, - // Duplicates must not be set, because duplicates will be deleted - } - delete(ds.entries, oldZid) - ds.entries[newZid] = &newEntry - return newEntry, nil -} - -func renameFilename(name string, curID, newID id.Zid) string { - if cur := curID.String(); strings.HasPrefix(name, cur) { - name = newID.String() + name[len(cur):] - } - return name -} - -// DeleteDirEntry removes a entry from the directory. -func (ds *DirService) DeleteDirEntry(zid id.Zid) error { - ds.mx.Lock() - defer ds.mx.Unlock() - if ds.entries == nil { - return ds.logMissingEntry("delete") - } - delete(ds.entries, zid) - return nil -} - -func (ds *DirService) updateEvents(newEntries entrySet) { - // Something may panic. Ensure a running service. - defer func() { - if ri := recover(); ri != nil { - kernel.Main.LogRecover("DirectoryService", ri) - go ds.updateEvents(newEntries) - } - }() - - for ev := range ds.notifier.Events() { - e, ok := ds.handleEvent(ev, newEntries) - if !ok { - break - } - newEntries = e - } -} -func (ds *DirService) handleEvent(ev Event, newEntries entrySet) (entrySet, bool) { - ds.mx.RLock() - state := ds.state - ds.mx.RUnlock() - - if msg := ds.log.Trace(); msg.Enabled() { - msg.Uint("state", uint64(state)).Str("op", ev.Op.String()).Str("name", ev.Name).Msg("notifyEvent") - } - if state == DsStopping { - return nil, false - } - - switch ev.Op { - case Error: - newEntries = nil - if state != DsMissing { - ds.log.Error().Err(ev.Err).Msg("Notifier confused") - } - case Make: - newEntries = make(entrySet) - case List: - if ev.Name == "" { - zids := getNewZids(newEntries) - ds.mx.Lock() - fromMissing := ds.state == DsMissing - prevEntries := ds.entries - ds.entries = newEntries - ds.state = DsWorking - ds.mx.Unlock() - ds.onCreateDirectory(zids, prevEntries) - if fromMissing { - ds.log.Info().Str("path", ds.dirPath).Msg("Zettel directory found") - } - return nil, true - } - if newEntries != nil { - ds.onUpdateFileEvent(newEntries, ev.Name) - } - case Destroy: - ds.onDestroyDirectory() - ds.log.Error().Str("path", ds.dirPath).Msg("Zettel directory missing") - return nil, true - case Update: - ds.mx.Lock() - zid := ds.onUpdateFileEvent(ds.entries, ev.Name) - ds.mx.Unlock() - if zid != id.Invalid { - ds.notifyChange(zid) - } - case Delete: - ds.mx.Lock() - zid := ds.onDeleteFileEvent(ds.entries, ev.Name) - ds.mx.Unlock() - if zid != id.Invalid { - ds.notifyChange(zid) - } - default: - ds.log.Error().Str("event", fmt.Sprintf("%v", ev)).Msg("Unknown zettel notification event") - } - return newEntries, true -} - -func getNewZids(entries entrySet) id.Slice { - zids := make(id.Slice, 0, len(entries)) - for zid := range entries { - zids = append(zids, zid) - } - return zids -} - -func (ds *DirService) onCreateDirectory(zids id.Slice, prevEntries entrySet) { - for _, zid := range zids { - ds.notifyChange(zid) - delete(prevEntries, zid) - } - - // These were previously stored, by are not found now. - // Notify system that these were deleted, e.g. for updating the index. - for zid := range prevEntries { - ds.notifyChange(zid) - } -} - -func (ds *DirService) onDestroyDirectory() { - ds.mx.Lock() - entries := ds.entries - ds.entries = nil - ds.state = DsMissing - ds.mx.Unlock() - for zid := range entries { - ds.notifyChange(zid) - } -} - -var validFileName = regexp.MustCompile(`^(\d{14})`) - -func matchValidFileName(name string) []string { - return validFileName.FindStringSubmatch(name) -} - -func seekZid(name string) id.Zid { - match := matchValidFileName(name) - if len(match) == 0 { - return id.Invalid - } - zid, err := id.Parse(match[1]) - if err != nil { - return id.Invalid - } - return zid -} - -func fetchdirEntry(entries entrySet, zid id.Zid) *DirEntry { - if entry, found := entries[zid]; found { - return entry - } - entry := &DirEntry{Zid: zid} - entries[zid] = entry - return entry -} - -func (ds *DirService) onUpdateFileEvent(entries entrySet, name string) id.Zid { - if entries == nil { - return id.Invalid - } - zid := seekZid(name) - if zid == id.Invalid { - return id.Invalid - } - entry := fetchdirEntry(entries, zid) - dupName1, dupName2 := ds.updateEntry(entry, name) - if dupName1 != "" { - ds.log.Info().Str("name", dupName1).Msg("Duplicate content (is ignored)") - if dupName2 != "" { - ds.log.Info().Str("name", dupName2).Msg("Duplicate content (is ignored)") - } - return id.Invalid - } - return zid -} - -func (ds *DirService) onDeleteFileEvent(entries entrySet, name string) id.Zid { - if entries == nil { - return id.Invalid - } - zid := seekZid(name) - if zid == id.Invalid { - return id.Invalid - } - entry, found := entries[zid] - if !found { - return zid - } - for i, dupName := range entry.UselessFiles { - if dupName == name { - removeDuplicate(entry, i) - return zid - } - } - if name == entry.ContentName { - entry.ContentName = "" - entry.ContentExt = "" - ds.replayUpdateUselessFiles(entry) - } else if name == entry.MetaName { - entry.MetaName = "" - ds.replayUpdateUselessFiles(entry) - } - if entry.ContentName == "" && entry.MetaName == "" { - delete(entries, zid) - } - return zid -} - -func removeDuplicate(entry *DirEntry, i int) { - if len(entry.UselessFiles) == 1 { - entry.UselessFiles = nil - return - } - entry.UselessFiles = entry.UselessFiles[:i+copy(entry.UselessFiles[i:], entry.UselessFiles[i+1:])] -} - -func (ds *DirService) replayUpdateUselessFiles(entry *DirEntry) { - uselessFiles := entry.UselessFiles - if len(uselessFiles) == 0 { - return - } - entry.UselessFiles = make([]string, 0, len(uselessFiles)) - for _, name := range uselessFiles { - ds.updateEntry(entry, name) - } - if len(uselessFiles) == len(entry.UselessFiles) { - return - } -loop: - for _, prevName := range uselessFiles { - for _, newName := range entry.UselessFiles { - if prevName == newName { - continue loop - } - } - ds.log.Info().Str("name", prevName).Msg("Previous duplicate file becomes useful") - } -} - -func (ds *DirService) updateEntry(entry *DirEntry, name string) (string, string) { - ext := onlyExt(name) - if !extIsMetaAndContent(entry.ContentExt) { - if ext == "" { - return updateEntryMeta(entry, name), "" - } - if entry.MetaName == "" { - if nameWithoutExt(name, ext) == entry.ContentName { - // We have marked a file as content file, but it is a metadata file, - // because it is the same as the new file without extension. - entry.MetaName = entry.ContentName - entry.ContentName = "" - entry.ContentExt = "" - ds.replayUpdateUselessFiles(entry) - } else if entry.ContentName != "" && nameWithoutExt(entry.ContentName, entry.ContentExt) == name { - // We have already a valid content file, and new file should serve as metadata file, - // because it is the same as the content file without extension. - entry.MetaName = name - return "", "" - } - } - } - return updateEntryContent(entry, name, ext) -} - -func nameWithoutExt(name, ext string) string { - return name[0 : len(name)-len(ext)-1] -} - -func updateEntryMeta(entry *DirEntry, name string) string { - metaName := entry.MetaName - if metaName == "" { - entry.MetaName = name - return "" - } - if metaName == name { - return "" - } - if newNameIsBetter(metaName, name) { - entry.MetaName = name - return addUselessFile(entry, metaName) - } - return addUselessFile(entry, name) -} - -func updateEntryContent(entry *DirEntry, name, ext string) (string, string) { - contentName := entry.ContentName - if contentName == "" { - entry.ContentName = name - entry.ContentExt = ext - return "", "" - } - if contentName == name { - return "", "" - } - contentExt := entry.ContentExt - if contentExt == ext { - if newNameIsBetter(contentName, name) { - entry.ContentName = name - return addUselessFile(entry, contentName), "" - } - return addUselessFile(entry, name), "" - } - if contentExt == extZettel { - return addUselessFile(entry, name), "" - } - if ext == extZettel { - entry.ContentName = name - entry.ContentExt = ext - contentName = addUselessFile(entry, contentName) - if metaName := entry.MetaName; metaName != "" { - metaName = addUselessFile(entry, metaName) - entry.MetaName = "" - return contentName, metaName - } - return contentName, "" - } - if newExtIsBetter(contentExt, ext) { - entry.ContentName = name - entry.ContentExt = ext - return addUselessFile(entry, contentName), "" - } - return addUselessFile(entry, name), "" -} -func addUselessFile(entry *DirEntry, name string) string { - for _, dupName := range entry.UselessFiles { - if name == dupName { - return "" - } - } - entry.UselessFiles = append(entry.UselessFiles, name) - return name -} - -func onlyExt(name string) string { - ext := filepath.Ext(name) - if ext == "" || ext[0] != '.' { - return ext - } - return ext[1:] -} - -func newNameIsBetter(oldName, newName string) bool { - if len(oldName) < len(newName) { - return false - } - return oldName > newName -} - -var supportedSyntax, primarySyntax strfun.Set - -func init() { - syntaxList := parser.GetSyntaxes() - supportedSyntax = strfun.NewSet(syntaxList...) - primarySyntax = make(map[string]struct{}, len(syntaxList)) - for _, syntax := range syntaxList { - if parser.Get(syntax).Name == syntax { - primarySyntax.Set(syntax) - } - } -} -func newExtIsBetter(oldExt, newExt string) bool { - oldSyntax := supportedSyntax.Has(oldExt) - if oldSyntax != supportedSyntax.Has(newExt) { - return !oldSyntax - } - if oldSyntax { - if oldExt == "zmk" { - return false - } - if newExt == "zmk" { - return true - } - oldInfo := parser.Get(oldExt) - newInfo := parser.Get(newExt) - if oldASTParser := oldInfo.IsASTParser; oldASTParser != newInfo.IsASTParser { - return !oldASTParser - } - if oldTextFormat := oldInfo.IsTextFormat; oldTextFormat != newInfo.IsTextFormat { - return !oldTextFormat - } - if oldImageFormat := oldInfo.IsImageFormat; oldImageFormat != newInfo.IsImageFormat { - return oldImageFormat - } - if oldPrimary := primarySyntax.Has(oldExt); oldPrimary != primarySyntax.Has(newExt) { - return !oldPrimary - } - } - - oldLen := len(oldExt) - newLen := len(newExt) - if oldLen != newLen { - return newLen < oldLen - } - return newExt < oldExt -} - -func (ds *DirService) notifyChange(zid id.Zid) { - if chci := ds.infos; chci != nil { - ds.log.Trace().Zid(zid).Msg("notifyChange") - chci <- box.UpdateInfo{Box: ds.box, Reason: box.OnZettel, Zid: zid} - } -} DELETED box/notify/directory_test.go Index: box/notify/directory_test.go ================================================================== --- box/notify/directory_test.go +++ box/notify/directory_test.go @@ -1,81 +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. -// -// SPDX-License-Identifier: EUPL-1.2 -// SPDX-FileCopyrightText: 2022-present Detlef Stern -//----------------------------------------------------------------------------- - -package notify - -import ( - "testing" - - _ "zettelstore.de/z/parser/blob" // Allow to use BLOB parser. - _ "zettelstore.de/z/parser/draw" // Allow to use draw parser. - _ "zettelstore.de/z/parser/markdown" // Allow to use markdown parser. - _ "zettelstore.de/z/parser/none" // Allow to use none parser. - _ "zettelstore.de/z/parser/plain" // Allow to use plain parser. - _ "zettelstore.de/z/parser/zettelmark" // Allow to use zettelmark parser. - "zettelstore.de/z/zettel/id" - "zettelstore.de/z/zettel/meta" -) - -func TestSeekZid(t *testing.T) { - testcases := []struct { - name string - zid id.Zid - }{ - {"", id.Invalid}, - {"1", id.Invalid}, - {"1234567890123", id.Invalid}, - {" 12345678901234", id.Invalid}, - {"12345678901234", id.Zid(12345678901234)}, - {"12345678901234.ext", id.Zid(12345678901234)}, - {"12345678901234 abc.ext", id.Zid(12345678901234)}, - {"12345678901234.abc.ext", id.Zid(12345678901234)}, - {"12345678901234 def", id.Zid(12345678901234)}, - } - for _, tc := range testcases { - gotZid := seekZid(tc.name) - if gotZid != tc.zid { - t.Errorf("seekZid(%q) == %v, but got %v", tc.name, tc.zid, gotZid) - } - } -} - -func TestNewExtIsBetter(t *testing.T) { - extVals := []string{ - // Main Formats - meta.SyntaxZmk, meta.SyntaxDraw, meta.SyntaxMarkdown, meta.SyntaxMD, - // Other supported text formats - meta.SyntaxCSS, meta.SyntaxSxn, meta.SyntaxTxt, meta.SyntaxHTML, - meta.SyntaxText, meta.SyntaxPlain, - // Supported text graphics formats - meta.SyntaxSVG, - meta.SyntaxNone, - // Supported binary graphic formats - meta.SyntaxGif, meta.SyntaxPNG, meta.SyntaxJPEG, meta.SyntaxWebp, meta.SyntaxJPG, - - // Unsupported syntax values - "gz", "cpp", "tar", "cppc", - } - for oldI, oldExt := range extVals { - for newI, newExt := range extVals { - if oldI <= newI { - continue - } - if !newExtIsBetter(oldExt, newExt) { - t.Errorf("newExtIsBetter(%q, %q) == true, but got false", oldExt, newExt) - } - if newExtIsBetter(newExt, oldExt) { - t.Errorf("newExtIsBetter(%q, %q) == false, but got true", newExt, oldExt) - } - } - } -} DELETED box/notify/entry.go Index: box/notify/entry.go ================================================================== --- box/notify/entry.go +++ box/notify/entry.go @@ -1,123 +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. -// -// SPDX-License-Identifier: EUPL-1.2 -// SPDX-FileCopyrightText: 2020-present Detlef Stern -//----------------------------------------------------------------------------- - -package notify - -import ( - "path/filepath" - - "t73f.de/r/zsc/api" - "zettelstore.de/z/parser" - "zettelstore.de/z/zettel" - "zettelstore.de/z/zettel/id" - "zettelstore.de/z/zettel/meta" -) - -const ( - extZettel = "zettel" // file contains metadata and content - extBin = "bin" // file contains binary content - extTxt = "txt" // file contains non-binary content -) - -func extIsMetaAndContent(ext string) bool { return ext == extZettel } - -// DirEntry stores everything for a directory entry. -type DirEntry struct { - Zid id.Zid - MetaName string // file name of meta information - ContentName string // file name of zettel content - ContentExt string // (normalized) file extension of zettel content - UselessFiles []string // list of other content files -} - -// IsValid checks whether the entry is valid. -func (e *DirEntry) IsValid() bool { - return e != nil && e.Zid.IsValid() -} - -// HasMetaInContent returns true, if metadata will be stored in the content file. -func (e *DirEntry) HasMetaInContent() bool { - return e.IsValid() && extIsMetaAndContent(e.ContentExt) -} - -// SetupFromMetaContent fills entry data based on metadata and zettel content. -func (e *DirEntry) SetupFromMetaContent(m *meta.Meta, content zettel.Content, getZettelFileSyntax func() []string) { - if e.Zid != m.Zid { - panic("Zid differ") - } - if contentName := e.ContentName; contentName != "" { - if !extIsMetaAndContent(e.ContentExt) && e.MetaName == "" { - e.MetaName = e.calcBaseName(contentName) - } - return - } - - syntax := m.GetDefault(api.KeySyntax, meta.DefaultSyntax) - ext := calcContentExt(syntax, m.YamlSep, getZettelFileSyntax) - metaName := e.MetaName - eimc := extIsMetaAndContent(ext) - if eimc { - if metaName != "" { - ext = contentExtWithMeta(syntax, content) - } - e.ContentName = e.calcBaseName(metaName) + "." + ext - e.ContentExt = ext - } else { - if len(content.AsBytes()) > 0 { - e.ContentName = e.calcBaseName(metaName) + "." + ext - e.ContentExt = ext - } - if metaName == "" { - e.MetaName = e.calcBaseName(e.ContentName) - } - } -} - -func contentExtWithMeta(syntax string, content zettel.Content) string { - p := parser.Get(syntax) - if content.IsBinary() { - if p.IsImageFormat { - return syntax - } - return extBin - } - if p.IsImageFormat { - return extTxt - } - return syntax -} - -func calcContentExt(syntax string, yamlSep bool, getZettelFileSyntax func() []string) string { - if yamlSep { - return extZettel - } - switch syntax { - case meta.SyntaxNone, meta.SyntaxZmk: - return extZettel - } - for _, s := range getZettelFileSyntax() { - if s == syntax { - return extZettel - } - } - return syntax - -} - -func (e *DirEntry) calcBaseName(name string) string { - if name == "" { - return e.Zid.String() - } - return name[0 : len(name)-len(filepath.Ext(name))] - -} DELETED box/notify/fsdir.go Index: box/notify/fsdir.go ================================================================== --- box/notify/fsdir.go +++ box/notify/fsdir.go @@ -1,234 +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. -// -// SPDX-License-Identifier: EUPL-1.2 -// SPDX-FileCopyrightText: 2021-present Detlef Stern -//----------------------------------------------------------------------------- - -package notify - -import ( - "os" - "path/filepath" - "strings" - - "github.com/fsnotify/fsnotify" - "zettelstore.de/z/logger" -) - -type fsdirNotifier struct { - log *logger.Logger - events chan Event - done chan struct{} - refresh chan struct{} - base *fsnotify.Watcher - path string - fetcher EntryFetcher - parent string -} - -// NewFSDirNotifier creates a directory based notifier that receives notifications -// from the file system. -func NewFSDirNotifier(log *logger.Logger, path string) (Notifier, error) { - absPath, err := filepath.Abs(path) - if err != nil { - log.Debug().Err(err).Str("path", path).Msg("Unable to create absolute path") - return nil, err - } - watcher, err := fsnotify.NewWatcher() - if err != nil { - log.Debug().Err(err).Str("absPath", absPath).Msg("Unable to create watcher") - return nil, err - } - absParentDir := filepath.Dir(absPath) - errParent := watcher.Add(absParentDir) - err = watcher.Add(absPath) - if errParent != nil { - if err != nil { - log.Error(). - Str("parentDir", absParentDir).Err(errParent). - Str("path", absPath).Err(err). - Msg("Unable to access Zettel directory and its parent directory") - watcher.Close() - return nil, err - } - log.Info().Str("parentDir", absParentDir).Err(errParent). - Msg("Parent of Zettel directory cannot be supervised") - 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.Info().Err(err).Str("path", absPath).Msg("Zettel directory currently not available") - } - - fsdn := &fsdirNotifier{ - log: log, - events: make(chan Event), - refresh: make(chan struct{}), - done: make(chan struct{}), - base: watcher, - path: absPath, - fetcher: newDirPathFetcher(absPath), - parent: absParentDir, - } - go fsdn.eventLoop() - return fsdn, nil -} - -func (fsdn *fsdirNotifier) Events() <-chan Event { - return fsdn.events -} - -func (fsdn *fsdirNotifier) Refresh() { - fsdn.refresh <- struct{}{} -} - -func (fsdn *fsdirNotifier) eventLoop() { - defer fsdn.base.Close() - defer close(fsdn.events) - defer close(fsdn.refresh) - if !listDirElements(fsdn.log, fsdn.fetcher, fsdn.events, fsdn.done) { - return - } - - for fsdn.readAndProcessEvent() { - } -} - -func (fsdn *fsdirNotifier) readAndProcessEvent() bool { - select { - case <-fsdn.done: - fsdn.traceDone(1) - return false - default: - } - select { - case <-fsdn.done: - fsdn.traceDone(2) - return false - case <-fsdn.refresh: - fsdn.log.Trace().Msg("refresh") - listDirElements(fsdn.log, fsdn.fetcher, fsdn.events, fsdn.done) - case err, ok := <-fsdn.base.Errors: - fsdn.log.Trace().Err(err).Bool("ok", ok).Msg("got errors") - if !ok { - return false - } - select { - case fsdn.events <- Event{Op: Error, Err: err}: - case <-fsdn.done: - fsdn.traceDone(3) - return false - } - case ev, ok := <-fsdn.base.Events: - fsdn.log.Trace().Str("name", ev.Name).Str("op", ev.Op.String()).Bool("ok", ok).Msg("file event") - if !ok { - return false - } - if !fsdn.processEvent(&ev) { - return false - } - } - return true -} - -func (fsdn *fsdirNotifier) traceDone(pos int64) { - fsdn.log.Trace().Int("i", pos).Msg("done with read and process events") -} - -func (fsdn *fsdirNotifier) processEvent(ev *fsnotify.Event) bool { - if strings.HasPrefix(ev.Name, fsdn.path) { - if len(ev.Name) == len(fsdn.path) { - return fsdn.processDirEvent(ev) - } - return fsdn.processFileEvent(ev) - } - fsdn.log.Trace().Str("path", fsdn.path).Str("name", ev.Name).Str("op", ev.Op.String()).Msg("event does not match") - return true -} - -func (fsdn *fsdirNotifier) processDirEvent(ev *fsnotify.Event) bool { - if ev.Has(fsnotify.Remove) || ev.Has(fsnotify.Rename) { - fsdn.log.Debug().Str("name", fsdn.path).Msg("Directory removed") - fsdn.base.Remove(fsdn.path) - select { - case fsdn.events <- Event{Op: Destroy}: - case <-fsdn.done: - fsdn.log.Trace().Int("i", 1).Msg("done dir event processing") - return false - } - return true - } - - if ev.Has(fsnotify.Create) { - err := fsdn.base.Add(fsdn.path) - if err != nil { - fsdn.log.Error().Err(err).Str("name", fsdn.path).Msg("Unable to add directory") - select { - case fsdn.events <- Event{Op: Error, Err: err}: - case <-fsdn.done: - fsdn.log.Trace().Int("i", 2).Msg("done dir event processing") - return false - } - } - fsdn.log.Debug().Str("name", fsdn.path).Msg("Directory added") - return listDirElements(fsdn.log, fsdn.fetcher, fsdn.events, fsdn.done) - } - - fsdn.log.Trace().Str("name", ev.Name).Str("op", ev.Op.String()).Msg("Directory processed") - return true -} - -func (fsdn *fsdirNotifier) processFileEvent(ev *fsnotify.Event) bool { - if ev.Has(fsnotify.Create) || ev.Has(fsnotify.Write) { - if fi, err := os.Lstat(ev.Name); err != nil || !fi.Mode().IsRegular() { - regular := err == nil && fi.Mode().IsRegular() - fsdn.log.Trace().Str("name", ev.Name).Str("op", ev.Op.String()).Err(err).Bool("regular", regular).Msg("error with file") - return true - } - fsdn.log.Trace().Str("name", ev.Name).Str("op", ev.Op.String()).Msg("File updated") - return fsdn.sendEvent(Update, filepath.Base(ev.Name)) - } - - if ev.Has(fsnotify.Rename) { - fi, err := os.Lstat(ev.Name) - if err != nil { - fsdn.log.Trace().Str("name", ev.Name).Str("op", ev.Op.String()).Msg("File deleted") - return fsdn.sendEvent(Delete, filepath.Base(ev.Name)) - } - if fi.Mode().IsRegular() { - fsdn.log.Trace().Str("name", ev.Name).Str("op", ev.Op.String()).Msg("File updated") - return fsdn.sendEvent(Update, filepath.Base(ev.Name)) - } - fsdn.log.Trace().Str("name", ev.Name).Msg("File not regular") - return true - } - - if ev.Has(fsnotify.Remove) { - fsdn.log.Trace().Str("name", ev.Name).Str("op", ev.Op.String()).Msg("File deleted") - return fsdn.sendEvent(Delete, filepath.Base(ev.Name)) - } - - fsdn.log.Trace().Str("name", ev.Name).Str("op", ev.Op.String()).Msg("File processed") - return true -} - -func (fsdn *fsdirNotifier) sendEvent(op EventOp, filename string) bool { - select { - case fsdn.events <- Event{Op: op, Name: filename}: - case <-fsdn.done: - fsdn.log.Trace().Msg("done file event processing") - return false - } - return true -} - -func (fsdn *fsdirNotifier) Close() { - close(fsdn.done) -} DELETED box/notify/helper.go Index: box/notify/helper.go ================================================================== --- box/notify/helper.go +++ box/notify/helper.go @@ -1,98 +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. -// -// SPDX-License-Identifier: EUPL-1.2 -// SPDX-FileCopyrightText: 2021-present Detlef Stern -//----------------------------------------------------------------------------- - -package notify - -import ( - "archive/zip" - "os" - - "zettelstore.de/z/logger" -) - -// EntryFetcher return a list of (file) names of an directory. -type EntryFetcher interface { - Fetch() ([]string, error) -} - -type dirPathFetcher struct { - dirPath string -} - -func newDirPathFetcher(dirPath string) EntryFetcher { return &dirPathFetcher{dirPath} } - -func (dpf *dirPathFetcher) Fetch() ([]string, error) { - entries, err := os.ReadDir(dpf.dirPath) - if err != nil { - return nil, err - } - result := make([]string, 0, len(entries)) - for _, entry := range entries { - if info, err1 := entry.Info(); err1 != nil || !info.Mode().IsRegular() { - continue - } - result = append(result, entry.Name()) - } - return result, nil -} - -type zipPathFetcher struct { - zipPath string -} - -func newZipPathFetcher(zipPath string) EntryFetcher { return &zipPathFetcher{zipPath} } - -func (zpf *zipPathFetcher) Fetch() ([]string, error) { - reader, err := zip.OpenReader(zpf.zipPath) - if err != nil { - return nil, err - } - defer reader.Close() - result := make([]string, 0, len(reader.File)) - for _, f := range reader.File { - result = append(result, f.Name) - } - return result, nil -} - -// listDirElements write all files within the directory path as events. -func listDirElements(log *logger.Logger, fetcher EntryFetcher, events chan<- Event, done <-chan struct{}) bool { - select { - case events <- Event{Op: Make}: - case <-done: - return false - } - entries, err := fetcher.Fetch() - if err != nil { - select { - case events <- Event{Op: Error, Err: err}: - case <-done: - return false - } - } - for _, name := range entries { - log.Trace().Str("name", name).Msg("File listed") - select { - case events <- Event{Op: List, Name: name}: - case <-done: - return false - } - } - - select { - case events <- Event{Op: List}: - case <-done: - return false - } - return true -} DELETED box/notify/notify.go Index: box/notify/notify.go ================================================================== --- box/notify/notify.go +++ box/notify/notify.go @@ -1,85 +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. -// -// 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 - -import "fmt" - -// Notifier send events about their container and content. -type Notifier interface { - // Return the channel - Events() <-chan Event - - // Signal a refresh of the container. This will result in some events. - Refresh() - - // Close the notifier (and eventually the channel) - Close() -} - -// EventOp describe a notification operation. -type EventOp uint8 - -// Valid constants for event operations. -// -// Error signals a detected error. Details are in Event.Err. -// -// Make signals that the container is detected. List events will follow. -// -// List signals a found file, if Event.Name is not empty. Otherwise it signals -// the end of files within the container. -// -// Destroy signals that the container is not there any more. It might me Make later again. -// -// Update signals that file Event.Name was created/updated. -// File name is relative to the container. -// -// Delete signals that file Event.Name was removed. -// File name is relative to the container's name. -const ( - _ EventOp = iota - Error // Error while operating - Make // Make container - List // List container - Destroy // Destroy container - Update // Update element - Delete // Delete element -) - -// String representation of operation code. -func (c EventOp) String() string { - switch c { - case Error: - return "ERROR" - case Make: - return "MAKE" - case List: - return "LIST" - case Destroy: - return "DESTROY" - case Update: - return "UPDATE" - case Delete: - return "DELETE" - default: - return fmt.Sprintf("UNKNOWN(%d)", c) - } -} - -// Event represents a single container / element event. -type Event struct { - Op EventOp - Name string - Err error // Valid iff Op == Error -} DELETED box/notify/simpledir.go Index: box/notify/simpledir.go ================================================================== --- box/notify/simpledir.go +++ box/notify/simpledir.go @@ -1,88 +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. -// -// SPDX-License-Identifier: EUPL-1.2 -// SPDX-FileCopyrightText: 2021-present Detlef Stern -//----------------------------------------------------------------------------- - -package notify - -import ( - "path/filepath" - - "zettelstore.de/z/logger" -) - -type simpleDirNotifier struct { - log *logger.Logger - events chan Event - done chan struct{} - refresh chan struct{} - fetcher EntryFetcher -} - -// NewSimpleDirNotifier creates a directory based notifier that will not receive -// any notifications from the operating system. -func NewSimpleDirNotifier(log *logger.Logger, path string) (Notifier, error) { - absPath, err := filepath.Abs(path) - if err != nil { - return nil, err - } - sdn := &simpleDirNotifier{ - log: log, - events: make(chan Event), - done: make(chan struct{}), - refresh: make(chan struct{}), - fetcher: newDirPathFetcher(absPath), - } - go sdn.eventLoop() - return sdn, nil -} - -// NewSimpleZipNotifier creates a zip-file based notifier that will not receive -// any notifications from the operating system. -func NewSimpleZipNotifier(log *logger.Logger, zipPath string) Notifier { - sdn := &simpleDirNotifier{ - log: log, - events: make(chan Event), - done: make(chan struct{}), - refresh: make(chan struct{}), - fetcher: newZipPathFetcher(zipPath), - } - go sdn.eventLoop() - return sdn -} - -func (sdn *simpleDirNotifier) Events() <-chan Event { - return sdn.events -} - -func (sdn *simpleDirNotifier) Refresh() { - sdn.refresh <- struct{}{} -} - -func (sdn *simpleDirNotifier) eventLoop() { - defer close(sdn.events) - defer close(sdn.refresh) - if !listDirElements(sdn.log, sdn.fetcher, sdn.events, sdn.done) { - return - } - for { - select { - case <-sdn.done: - return - case <-sdn.refresh: - listDirElements(sdn.log, sdn.fetcher, sdn.events, sdn.done) - } - } -} - -func (sdn *simpleDirNotifier) Close() { - close(sdn.done) -} ADDED client/client.go Index: client/client.go ================================================================== --- client/client.go +++ client/client.go @@ -0,0 +1,443 @@ +//----------------------------------------------------------------------------- +// Copyright (c) 2021 Detlef Stern +// +// This file is part of zettelstore. +// +// Zettelstore is licensed under the latest version of the EUPL (European Union +// Public License). Please see file LICENSE.txt for your rights and obligations +// under this license. +//----------------------------------------------------------------------------- + +// Package client provides a client for accessing the Zettelstore via its API. +package client + +import ( + "bytes" + "context" + "encoding/json" + "errors" + "io" + "net/http" + "net/url" + "strconv" + "strings" + "time" + + "zettelstore.de/z/api" + "zettelstore.de/z/domain/id" +) + +// Client contains all data to execute requests. +type Client struct { + baseURL string + username string + password string + token string + tokenType string + expires time.Time +} + +// NewClient create a new client. +func NewClient(baseURL string) *Client { + if !strings.HasSuffix(baseURL, "/") { + baseURL += "/" + } + c := Client{baseURL: baseURL} + return &c +} + +func (c *Client) newURLBuilder(key byte) *api.URLBuilder { + return api.NewURLBuilder(c.baseURL, key) +} +func (c *Client) newRequest(ctx context.Context, method string, ub *api.URLBuilder, body io.Reader) (*http.Request, error) { + return http.NewRequestWithContext(ctx, method, ub.String(), body) +} + +func (c *Client) executeRequest(req *http.Request) (*http.Response, error) { + if c.token != "" { + req.Header.Add("Authorization", c.tokenType+" "+c.token) + } + client := http.Client{} + resp, err := client.Do(req) + if err != nil { + if resp != nil && resp.Body != nil { + resp.Body.Close() + } + return nil, err + } + return resp, err +} + +func (c *Client) buildAndExecuteRequest( + ctx context.Context, method string, ub *api.URLBuilder, body io.Reader, h http.Header) (*http.Response, error) { + req, err := c.newRequest(ctx, method, ub, body) + if err != nil { + return nil, err + } + err = c.updateToken(ctx) + if err != nil { + return nil, err + } + for key, val := range h { + req.Header[key] = append(req.Header[key], val...) + } + return c.executeRequest(req) +} + +// SetAuth sets authentication data. +func (c *Client) SetAuth(username, password string) { + c.username = username + c.password = password + c.token = "" + c.tokenType = "" + c.expires = time.Time{} +} + +func (c *Client) executeAuthRequest(req *http.Request) error { + resp, err := c.executeRequest(req) + if err != nil { + return err + } + defer resp.Body.Close() + if resp.StatusCode != http.StatusOK { + return errors.New(resp.Status) + } + dec := json.NewDecoder(resp.Body) + var tinfo api.AuthJSON + err = dec.Decode(&tinfo) + if err != nil { + return err + } + c.token = tinfo.Token + c.tokenType = tinfo.Type + c.expires = time.Now().Add(time.Duration(tinfo.Expires*10/9) * time.Second) + return nil +} + +func (c *Client) updateToken(ctx context.Context) error { + if c.username == "" { + return nil + } + if time.Now().After(c.expires) { + return c.Authenticate(ctx) + } + return c.RefreshToken(ctx) +} + +// Authenticate sets a new token by sending user name and password. +func (c *Client) Authenticate(ctx context.Context) error { + authData := url.Values{"username": {c.username}, "password": {c.password}} + req, err := c.newRequest(ctx, http.MethodPost, c.newURLBuilder('v'), strings.NewReader(authData.Encode())) + if err != nil { + return err + } + req.Header.Set("Content-Type", "application/x-www-form-urlencoded") + return c.executeAuthRequest(req) +} + +// RefreshToken updates the access token +func (c *Client) RefreshToken(ctx context.Context) error { + req, err := c.newRequest(ctx, http.MethodPut, c.newURLBuilder('v'), nil) + if err != nil { + return err + } + return c.executeAuthRequest(req) +} + +// CreateZettel creates a new zettel and returns its URL. +func (c *Client) CreateZettel(ctx context.Context, data *api.ZettelDataJSON) (id.Zid, error) { + var buf bytes.Buffer + if err := encodeZettelData(&buf, data); err != nil { + return id.Invalid, err + } + ub := c.jsonZettelURLBuilder(nil) + resp, err := c.buildAndExecuteRequest(ctx, http.MethodPost, ub, &buf, nil) + if err != nil { + return id.Invalid, err + } + defer resp.Body.Close() + if resp.StatusCode != http.StatusCreated { + return id.Invalid, errors.New(resp.Status) + } + dec := json.NewDecoder(resp.Body) + var newZid api.ZidJSON + err = dec.Decode(&newZid) + if err != nil { + return id.Invalid, err + } + zid, err := id.Parse(newZid.ID) + if err != nil { + return id.Invalid, err + } + return zid, nil +} + +func encodeZettelData(buf *bytes.Buffer, data *api.ZettelDataJSON) error { + enc := json.NewEncoder(buf) + enc.SetEscapeHTML(false) + return enc.Encode(&data) +} + +// ListZettel returns a list of all Zettel. +func (c *Client) ListZettel(ctx context.Context, query url.Values) ([]api.ZettelJSON, error) { + ub := c.jsonZettelURLBuilder(query) + resp, err := c.buildAndExecuteRequest(ctx, http.MethodGet, ub, nil, nil) + if err != nil { + return nil, err + } + defer resp.Body.Close() + if resp.StatusCode != http.StatusOK { + return nil, errors.New(resp.Status) + } + dec := json.NewDecoder(resp.Body) + var zl api.ZettelListJSON + err = dec.Decode(&zl) + if err != nil { + return nil, err + } + return zl.List, nil +} + +// GetZettelJSON returns a zettel as a JSON struct. +func (c *Client) GetZettelJSON(ctx context.Context, zid id.Zid, query url.Values) (*api.ZettelDataJSON, error) { + ub := c.jsonZettelURLBuilder(query).SetZid(zid) + resp, err := c.buildAndExecuteRequest(ctx, http.MethodGet, ub, nil, nil) + if err != nil { + return nil, err + } + defer resp.Body.Close() + if resp.StatusCode != http.StatusOK { + return nil, errors.New(resp.Status) + } + dec := json.NewDecoder(resp.Body) + var out api.ZettelDataJSON + err = dec.Decode(&out) + if err != nil { + return nil, err + } + return &out, nil +} + +// GetEvaluatedZettel return a zettel in a defined encoding. +func (c *Client) GetEvaluatedZettel(ctx context.Context, zid id.Zid, enc api.EncodingEnum) (string, error) { + ub := c.jsonZettelURLBuilder(nil).SetZid(zid) + ub.AppendQuery(api.QueryKeyFormat, enc.String()) + ub.AppendQuery(api.QueryKeyPart, api.PartContent) + resp, err := c.buildAndExecuteRequest(ctx, http.MethodGet, ub, nil, nil) + if err != nil { + return "", err + } + defer resp.Body.Close() + if resp.StatusCode != http.StatusOK { + return "", errors.New(resp.Status) + } + content, err := io.ReadAll(resp.Body) + if err != nil { + return "", err + } + return string(content), nil +} + +// GetZettelOrder returns metadata of the given zettel and, more important, +// metadata of zettel that are referenced in a list within the first zettel. +func (c *Client) GetZettelOrder(ctx context.Context, zid id.Zid) (*api.ZidMetaRelatedList, error) { + ub := c.newURLBuilder('o').SetZid(zid) + resp, err := c.buildAndExecuteRequest(ctx, http.MethodGet, ub, nil, nil) + if err != nil { + return nil, err + } + defer resp.Body.Close() + if resp.StatusCode != http.StatusOK { + return nil, errors.New(resp.Status) + } + dec := json.NewDecoder(resp.Body) + var out api.ZidMetaRelatedList + err = dec.Decode(&out) + if err != nil { + return nil, err + } + return &out, nil +} + +// ContextDirection specifies how the context should be calculated. +type ContextDirection uint8 + +// Allowed values for ContextDirection +const ( + _ ContextDirection = iota + DirBoth + DirBackward + DirForward +) + +// GetZettelContext returns metadata of the given zettel and, more important, +// metadata of zettel that for the context of the first zettel. +func (c *Client) GetZettelContext( + ctx context.Context, zid id.Zid, dir ContextDirection, depth, limit int) ( + *api.ZidMetaRelatedList, error, +) { + ub := c.newURLBuilder('x').SetZid(zid) + switch dir { + case DirBackward: + ub.AppendQuery(api.QueryKeyDir, api.DirBackward) + case DirForward: + ub.AppendQuery(api.QueryKeyDir, api.DirForward) + } + if depth > 0 { + ub.AppendQuery(api.QueryKeyDepth, strconv.Itoa(depth)) + } + if limit > 0 { + ub.AppendQuery(api.QueryKeyLimit, strconv.Itoa(limit)) + } + resp, err := c.buildAndExecuteRequest(ctx, http.MethodGet, ub, nil, nil) + if err != nil { + return nil, err + } + defer resp.Body.Close() + if resp.StatusCode != http.StatusOK { + return nil, errors.New(resp.Status) + } + dec := json.NewDecoder(resp.Body) + var out api.ZidMetaRelatedList + err = dec.Decode(&out) + if err != nil { + return nil, err + } + return &out, nil +} + +// GetZettelLinks returns connections to ohter zettel, images, externals URLs. +func (c *Client) GetZettelLinks(ctx context.Context, zid id.Zid) (*api.ZettelLinksJSON, error) { + ub := c.newURLBuilder('l').SetZid(zid) + resp, err := c.buildAndExecuteRequest(ctx, http.MethodGet, ub, nil, nil) + if err != nil { + return nil, err + } + defer resp.Body.Close() + if resp.StatusCode != http.StatusOK { + return nil, errors.New(resp.Status) + } + dec := json.NewDecoder(resp.Body) + var out api.ZettelLinksJSON + err = dec.Decode(&out) + if err != nil { + return nil, err + } + return &out, nil +} + +// UpdateZettel updates an existing zettel. +func (c *Client) UpdateZettel(ctx context.Context, zid id.Zid, data *api.ZettelDataJSON) error { + var buf bytes.Buffer + if err := encodeZettelData(&buf, data); err != nil { + return err + } + ub := c.jsonZettelURLBuilder(nil).SetZid(zid) + resp, err := c.buildAndExecuteRequest(ctx, http.MethodPut, ub, &buf, nil) + if err != nil { + return err + } + defer resp.Body.Close() + if resp.StatusCode != http.StatusNoContent { + return errors.New(resp.Status) + } + return nil +} + +// RenameZettel renames a zettel. +func (c *Client) RenameZettel(ctx context.Context, oldZid, newZid id.Zid) error { + ub := c.jsonZettelURLBuilder(nil).SetZid(oldZid) + h := http.Header{ + api.HeaderDestination: {c.jsonZettelURLBuilder(nil).SetZid(newZid).String()}, + } + resp, err := c.buildAndExecuteRequest(ctx, api.MethodMove, ub, nil, h) + if err != nil { + return err + } + defer resp.Body.Close() + if resp.StatusCode != http.StatusNoContent { + return errors.New(resp.Status) + } + return nil +} + +// DeleteZettel deletes a zettel with the given identifier. +func (c *Client) DeleteZettel(ctx context.Context, zid id.Zid) error { + ub := c.jsonZettelURLBuilder(nil).SetZid(zid) + resp, err := c.buildAndExecuteRequest(ctx, http.MethodDelete, ub, nil, nil) + if err != nil { + return err + } + defer resp.Body.Close() + if resp.StatusCode != http.StatusNoContent { + return errors.New(resp.Status) + } + return nil +} + +func (c *Client) jsonZettelURLBuilder(query url.Values) *api.URLBuilder { + ub := c.newURLBuilder('z') + for key, values := range query { + if key == api.QueryKeyFormat { + continue + } + for _, val := range values { + ub.AppendQuery(key, val) + } + } + return ub +} + +// ListTags returns a map of all tags, together with the associated zettel containing this tag. +func (c *Client) ListTags(ctx context.Context) (map[string][]string, error) { + err := c.updateToken(ctx) + if err != nil { + return nil, err + } + req, err := c.newRequest(ctx, http.MethodGet, c.newURLBuilder('t'), nil) + if err != nil { + return nil, err + } + resp, err := c.executeRequest(req) + if err != nil { + return nil, err + } + defer resp.Body.Close() + if resp.StatusCode != http.StatusOK { + return nil, errors.New(resp.Status) + } + dec := json.NewDecoder(resp.Body) + var tl api.TagListJSON + err = dec.Decode(&tl) + if err != nil { + return nil, err + } + return tl.Tags, nil +} + +// ListRoles returns a list of all roles. +func (c *Client) ListRoles(ctx context.Context) ([]string, error) { + err := c.updateToken(ctx) + if err != nil { + return nil, err + } + req, err := c.newRequest(ctx, http.MethodGet, c.newURLBuilder('r'), nil) + if err != nil { + return nil, err + } + resp, err := c.executeRequest(req) + if err != nil { + return nil, err + } + defer resp.Body.Close() + if resp.StatusCode != http.StatusOK { + return nil, errors.New(resp.Status) + } + dec := json.NewDecoder(resp.Body) + var rl api.RoleListJSON + err = dec.Decode(&rl) + if err != nil { + return nil, err + } + return rl.Roles, nil +} ADDED client/client_test.go Index: client/client_test.go ================================================================== --- client/client_test.go +++ client/client_test.go @@ -0,0 +1,334 @@ +//----------------------------------------------------------------------------- +// Copyright (c) 2021 Detlef Stern +// +// This file is part of zettelstore. +// +// Zettelstore is licensed under the latest version of the EUPL (European Union +// Public License). Please see file LICENSE.txt for your rights and obligations +// under this license. +//----------------------------------------------------------------------------- + +// Package client provides a client for accessing the Zettelstore via its API. +package client_test + +import ( + "context" + "flag" + "fmt" + "net/url" + "testing" + + "zettelstore.de/z/api" + "zettelstore.de/z/client" + "zettelstore.de/z/domain/id" + "zettelstore.de/z/domain/meta" +) + +func TestCreateRenameDeleteZettel(t *testing.T) { + // Is not to be allowed to run in parallel with other tests. + c := getClient() + c.SetAuth("creator", "creator") + zid, err := c.CreateZettel(context.Background(), &api.ZettelDataJSON{ + Meta: nil, + Encoding: "", + Content: "Example", + }) + if err != nil { + t.Error("Cannot create zettel:", err) + return + } + if !zid.IsValid() { + t.Error("Invalid zettel ID", zid) + return + } + newZid := zid + 1 + c.SetAuth("owner", "owner") + err = c.RenameZettel(context.Background(), zid, newZid) + if err != nil { + t.Error("Cannot rename", zid, ":", err) + newZid = zid + } + err = c.DeleteZettel(context.Background(), newZid) + if err != nil { + t.Error("Cannot delete", zid, ":", err) + return + } +} + +func TestUpdateZettel(t *testing.T) { + t.Parallel() + c := getClient() + c.SetAuth("writer", "writer") + z, err := c.GetZettelJSON(context.Background(), id.DefaultHomeZid, nil) + if err != nil { + t.Error(err) + return + } + if got := z.Meta[meta.KeyTitle]; got != "Home" { + t.Errorf("Title of zettel is not \"Home\", but %q", got) + return + } + newTitle := "New Home" + z.Meta[meta.KeyTitle] = newTitle + err = c.UpdateZettel(context.Background(), id.DefaultHomeZid, z) + if err != nil { + t.Error(err) + return + } + zt, err := c.GetZettelJSON(context.Background(), id.DefaultHomeZid, nil) + if err != nil { + t.Error(err) + return + } + if got := zt.Meta[meta.KeyTitle]; got != newTitle { + t.Errorf("Title of zettel is not %q, but %q", newTitle, got) + } +} + +func TestList(t *testing.T) { + testdata := []struct { + user string + exp int + }{ + {"", 7}, + {"creator", 10}, + {"reader", 12}, + {"writer", 12}, + {"owner", 34}, + } + + t.Parallel() + c := getClient() + query := url.Values{api.QueryKeyFormat: {"html"}} // Client must remove "html" + for i, tc := range testdata { + t.Run(fmt.Sprintf("User %d/%q", i, tc.user), func(tt *testing.T) { + c.SetAuth(tc.user, tc.user) + l, err := c.ListZettel(context.Background(), query) + if err != nil { + tt.Error(err) + return + } + got := len(l) + if got != tc.exp { + tt.Errorf("List of length %d expected, but got %d\n%v", tc.exp, got, l) + } + }) + } + l, err := c.ListZettel(context.Background(), url.Values{meta.KeyRole: {meta.ValueRoleConfiguration}}) + if err != nil { + t.Error(err) + return + } + got := len(l) + if got != 27 { + t.Errorf("List of length %d expected, but got %d\n%v", 27, got, l) + } +} +func TestGetZettel(t *testing.T) { + t.Parallel() + c := getClient() + c.SetAuth("owner", "owner") + z, err := c.GetZettelJSON(context.Background(), id.DefaultHomeZid, url.Values{api.QueryKeyPart: {api.PartContent}}) + if err != nil { + t.Error(err) + return + } + if m := z.Meta; len(m) > 0 { + t.Errorf("Exptected empty meta, but got %v", z.Meta) + } + if z.Content == "" || z.Encoding != "" { + t.Errorf("Expect non-empty content, but empty encoding (got %q)", z.Encoding) + } +} + +func TestGetEvaluatedZettel(t *testing.T) { + t.Parallel() + c := getClient() + c.SetAuth("owner", "owner") + encodings := []api.EncodingEnum{ + api.EncoderDJSON, + api.EncoderHTML, + api.EncoderNative, + api.EncoderText, + } + for _, enc := range encodings { + content, err := c.GetEvaluatedZettel(context.Background(), id.DefaultHomeZid, enc) + if err != nil { + t.Error(err) + continue + } + if len(content) == 0 { + t.Errorf("Empty content for encoding %v", enc) + } + } +} + +func TestGetZettelOrder(t *testing.T) { + t.Parallel() + c := getClient() + c.SetAuth("owner", "owner") + rl, err := c.GetZettelOrder(context.Background(), id.TOCNewTemplateZid) + if err != nil { + t.Error(err) + return + } + if rl.ID != id.TOCNewTemplateZid.String() { + t.Errorf("Expected an Zid %v, but got %v", id.TOCNewTemplateZid, rl.ID) + return + } + l := rl.List + if got := len(l); got != 2 { + t.Errorf("Expected list fo length 2, got %d", got) + return + } + if got := l[0].ID; got != id.TemplateNewZettelZid.String() { + t.Errorf("Expected result[0]=%v, but got %v", id.TemplateNewZettelZid, got) + } + if got := l[1].ID; got != id.TemplateNewUserZid.String() { + t.Errorf("Expected result[1]=%v, but got %v", id.TemplateNewUserZid, got) + } +} + +func TestGetZettelContext(t *testing.T) { + t.Parallel() + c := getClient() + c.SetAuth("owner", "owner") + rl, err := c.GetZettelContext(context.Background(), id.VersionZid, client.DirBoth, 0, 3) + if err != nil { + t.Error(err) + return + } + if rl.ID != id.VersionZid.String() { + t.Errorf("Expected an Zid %v, but got %v", id.VersionZid, rl.ID) + return + } + l := rl.List + if got := len(l); got != 3 { + t.Errorf("Expected list fo length 3, got %d", got) + return + } + if got := l[0].ID; got != id.DefaultHomeZid.String() { + t.Errorf("Expected result[0]=%v, but got %v", id.DefaultHomeZid, got) + } + if got := l[1].ID; got != id.OperatingSystemZid.String() { + t.Errorf("Expected result[1]=%v, but got %v", id.OperatingSystemZid, got) + } + if got := l[2].ID; got != id.StartupConfigurationZid.String() { + t.Errorf("Expected result[2]=%v, but got %v", id.StartupConfigurationZid, got) + } + + rl, err = c.GetZettelContext(context.Background(), id.VersionZid, client.DirBackward, 0, 0) + if err != nil { + t.Error(err) + return + } + if rl.ID != id.VersionZid.String() { + t.Errorf("Expected an Zid %v, but got %v", id.VersionZid, rl.ID) + return + } + l = rl.List + if got := len(l); got != 1 { + t.Errorf("Expected list fo length 1, got %d", got) + return + } + if got := l[0].ID; got != id.DefaultHomeZid.String() { + t.Errorf("Expected result[0]=%v, but got %v", id.DefaultHomeZid, got) + } +} + +func TestGetZettelLinks(t *testing.T) { + t.Parallel() + c := getClient() + c.SetAuth("owner", "owner") + zl, err := c.GetZettelLinks(context.Background(), id.DefaultHomeZid) + if err != nil { + t.Error(err) + return + } + if zl.ID != id.DefaultHomeZid.String() { + t.Errorf("Expected an Zid %v, but got %v", id.DefaultHomeZid, zl.ID) + return + } + if len(zl.Links.Incoming) != 0 { + t.Error("No incomings expected", zl.Links.Incoming) + } + if got := len(zl.Links.Outgoing); got != 4 { + t.Errorf("Expected 4 outgoing links, got %d", got) + } + if got := len(zl.Links.Local); got != 1 { + t.Errorf("Expected 1 local link, got %d", got) + } + if got := len(zl.Links.External); got != 4 { + t.Errorf("Expected 4 external link, got %d", got) + } +} + +func TestListTags(t *testing.T) { + t.Parallel() + c := getClient() + c.SetAuth("owner", "owner") + tm, err := c.ListTags(context.Background()) + if err != nil { + t.Error(err) + return + } + tags := []struct { + key string + size int + }{ + {"#invisible", 1}, + {"#user", 4}, + {"#test", 4}, + } + if len(tm) != len(tags) { + t.Errorf("Expected %d different tags, but got only %d (%v)", len(tags), len(tm), tm) + } + for _, tag := range tags { + if zl, ok := tm[tag.key]; !ok { + t.Errorf("No tag %v: %v", tag.key, tm) + } else if len(zl) != tag.size { + t.Errorf("Expected %d zettel with tag %v, but got %v", tag.size, tag.key, zl) + } + } + for i, id := range tm["#user"] { + if id != tm["#test"][i] { + t.Errorf("Tags #user and #test have different content: %v vs %v", tm["#user"], tm["#test"]) + } + } +} + +func TestListRoles(t *testing.T) { + t.Parallel() + c := getClient() + c.SetAuth("owner", "owner") + rl, err := c.ListRoles(context.Background()) + if err != nil { + t.Error(err) + return + } + exp := []string{"configuration", "user", "zettel"} + if len(rl) != len(exp) { + t.Errorf("Expected %d different tags, but got only %d (%v)", len(exp), len(rl), rl) + } + for i, id := range exp { + if id != rl[i] { + t.Errorf("Role list pos %d: expected %q, got %q", i, id, rl[i]) + } + } +} + +var baseURL string + +func init() { + flag.StringVar(&baseURL, "base-url", "", "Base URL") +} + +func getClient() *client.Client { return client.NewClient(baseURL) } + +// TestMain controls whether client API tests should run or not. +func TestMain(m *testing.M) { + flag.Parse() + if baseURL != "" { + m.Run() + } +} Index: cmd/cmd_file.go ================================================================== --- cmd/cmd_file.go +++ cmd/cmd_file.go @@ -1,59 +1,54 @@ //----------------------------------------------------------------------------- -// Copyright (c) 2020-present Detlef Stern +// Copyright (c) 2020-2021 Detlef Stern // -// This file is part of Zettelstore. +// This file is part of zettelstore. // // Zettelstore is licensed under the latest version of the EUPL (European Union // Public License). Please see file LICENSE.txt for your rights and obligations // under this license. -// -// SPDX-License-Identifier: EUPL-1.2 -// SPDX-FileCopyrightText: 2020-present Detlef Stern //----------------------------------------------------------------------------- package cmd import ( - "context" "flag" "fmt" "io" "os" - "t73f.de/r/zsc/api" - "t73f.de/r/zsc/input" + "zettelstore.de/z/api" + "zettelstore.de/z/domain" + "zettelstore.de/z/domain/id" + "zettelstore.de/z/domain/meta" "zettelstore.de/z/encoder" + "zettelstore.de/z/input" "zettelstore.de/z/parser" - "zettelstore.de/z/zettel" - "zettelstore.de/z/zettel/id" - "zettelstore.de/z/zettel/meta" ) // ---------- Subcommand: file ----------------------------------------------- -func cmdFile(fs *flag.FlagSet) (int, error) { - enc := fs.Lookup("t").Value.String() +func cmdFile(fs *flag.FlagSet, cfg *meta.Meta) (int, error) { + format := fs.Lookup("t").Value.String() m, inp, err := getInput(fs.Args()) if m == nil { return 2, err } z := parser.ParseZettel( - context.Background(), - zettel.Zettel{ + domain.Zettel{ Meta: m, - Content: zettel.NewContent(inp.Src[inp.Pos:]), + Content: domain.NewContent(inp.Src[inp.Pos:]), }, - m.GetDefault(api.KeySyntax, meta.DefaultSyntax), + m.GetDefault(meta.KeySyntax, meta.ValueSyntaxZmk), 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) + enc := encoder.Create(api.Encoder(format), &encoder.Environment{Lang: m.GetDefault(meta.KeyLang, meta.ValueLangEN)}) + if enc == nil { + fmt.Fprintf(os.Stderr, "Unknown format %q\n", format) return 2, nil } - _, err = encdr.WriteZettel(os.Stdout, z, parser.ParseMetadata) + _, err = enc.WriteZettel(os.Stdout, z, format != "raw") if err != nil { return 2, err } fmt.Println() @@ -64,26 +59,26 @@ if len(args) < 1 { src, err := io.ReadAll(os.Stdin) if err != nil { return nil, nil, err } - inp := input.NewInput(src) + inp := input.NewInput(string(src)) m := meta.NewFromInput(id.New(true), inp) return m, inp, nil } src, err := os.ReadFile(args[0]) if err != nil { return nil, nil, err } - inp := input.NewInput(src) + inp := input.NewInput(string(src)) m := meta.NewFromInput(id.New(true), inp) if len(args) > 1 { - src, err = os.ReadFile(args[1]) + src, err := os.ReadFile(args[1]) if err != nil { return nil, nil, err } - inp = input.NewInput(src) + inp = input.NewInput(string(src)) } return m, inp, nil } Index: cmd/cmd_password.go ================================================================== --- cmd/cmd_password.go +++ cmd/cmd_password.go @@ -1,16 +1,13 @@ //----------------------------------------------------------------------------- -// Copyright (c) 2020-present Detlef Stern +// Copyright (c) 2020 Detlef Stern // -// This file is part of Zettelstore. +// This file is part of zettelstore. // // Zettelstore is licensed under the latest version of the EUPL (European Union // Public License). Please see file LICENSE.txt for your rights and obligations // under this license. -// -// SPDX-License-Identifier: EUPL-1.2 -// SPDX-FileCopyrightText: 2020-present Detlef Stern //----------------------------------------------------------------------------- package cmd import ( @@ -18,18 +15,18 @@ "fmt" "os" "golang.org/x/term" - "t73f.de/r/zsc/api" "zettelstore.de/z/auth/cred" - "zettelstore.de/z/zettel/id" + "zettelstore.de/z/domain/id" + "zettelstore.de/z/domain/meta" ) // ---------- Subcommand: password ------------------------------------------- -func cmdPassword(fs *flag.FlagSet) (int, error) { +func cmdPassword(fs *flag.FlagSet, cfg *meta.Meta) (int, error) { if fs.NArg() == 0 { fmt.Fprintln(os.Stderr, "User name and user zettel identification missing") return 2, nil } if fs.NArg() == 1 { @@ -61,12 +58,12 @@ hashedPassword, err := cred.HashCredential(zid, ident, password) if err != nil { return 2, err } fmt.Printf("%v: %s\n%v: %s\n", - api.KeyCredential, hashedPassword, - api.KeyUserID, ident, + meta.KeyCredential, hashedPassword, + meta.KeyUserID, ident, ) return 0, nil } func getPassword(prompt string) (string, error) { Index: cmd/cmd_run.go ================================================================== --- cmd/cmd_run.go +++ cmd/cmd_run.go @@ -1,140 +1,136 @@ //----------------------------------------------------------------------------- -// Copyright (c) 2020-present Detlef Stern +// Copyright (c) 2020-2021 Detlef Stern // -// This file is part of Zettelstore. +// This file is part of zettelstore. // // Zettelstore is licensed under the latest version of the EUPL (European Union // Public License). Please see file LICENSE.txt for your rights and obligations // under this license. -// -// SPDX-License-Identifier: EUPL-1.2 -// SPDX-FileCopyrightText: 2020-present Detlef Stern //----------------------------------------------------------------------------- package cmd import ( - "context" "flag" "net/http" + zsapi "zettelstore.de/z/api" "zettelstore.de/z/auth" "zettelstore.de/z/box" "zettelstore.de/z/config" + "zettelstore.de/z/domain/meta" "zettelstore.de/z/kernel" "zettelstore.de/z/usecase" "zettelstore.de/z/web/adapter/api" "zettelstore.de/z/web/adapter/webui" "zettelstore.de/z/web/server" - "zettelstore.de/z/zettel/meta" ) // ---------- Subcommand: run ------------------------------------------------ func flgRun(fs *flag.FlagSet) { - fs.String("c", "", "configuration file") + fs.String("c", defConfigfile, "configuration file") fs.Uint("a", 0, "port number kernel service (0=disable)") fs.Uint("p", 23123, "port number web service") fs.String("d", "", "zettel directory") fs.Bool("r", false, "system-wide read-only mode") fs.Bool("v", false, "verbose mode") fs.Bool("debug", false, "debug mode") } -func runFunc(*flag.FlagSet) (int, error) { - var exitCode int - err := kernel.Main.StartService(kernel.WebService) - if err != nil { - exitCode = 1 - } +func withDebug(fs *flag.FlagSet) bool { + dbg := fs.Lookup("debug") + return dbg != nil && dbg.Value.String() == "true" +} + +func runFunc(fs *flag.FlagSet, cfg *meta.Meta) (int, error) { + exitCode, err := doRun(withDebug(fs)) kernel.Main.WaitForShutdown() return exitCode, err } -func setupRouting(webSrv server.Server, boxManager box.Manager, authManager auth.Manager, rtConfig config.Config) { - protectedBoxManager, authPolicy := authManager.BoxWithPolicy(boxManager, rtConfig) +func doRun(debug bool) (int, error) { kern := kernel.Main - webLog := kern.GetLogger(kernel.WebService) - - var getUser getUserImpl - logAuth := kern.GetLogger(kernel.AuthService) - logUc := kern.GetLogger(kernel.CoreService).WithUser(&getUser) - ucGetUser := usecase.NewGetUser(authManager, boxManager) - ucAuthenticate := usecase.NewAuthenticate(logAuth, authManager, &ucGetUser) - ucIsAuth := usecase.NewIsAuthenticated(logUc, &getUser, authManager) - ucCreateZettel := usecase.NewCreateZettel(logUc, rtConfig, protectedBoxManager) - ucGetAllZettel := usecase.NewGetAllZettel(protectedBoxManager) + kern.SetDebug(debug) + if err := kern.StartService(kernel.WebService); err != nil { + return 1, err + } + return 0, nil +} + +func setupRouting(webSrv server.Server, boxManager box.Manager, authManager auth.Manager, rtConfig config.Config) { + protectedBoxManager, authPolicy := authManager.BoxWithPolicy(webSrv, boxManager, rtConfig) + api := api.New(webSrv, authManager, authManager, webSrv, rtConfig) + wui := webui.New(webSrv, authManager, rtConfig, authManager, boxManager, authPolicy) + + ucAuthenticate := usecase.NewAuthenticate(authManager, authManager, boxManager) + ucCreateZettel := usecase.NewCreateZettel(rtConfig, protectedBoxManager) + ucGetMeta := usecase.NewGetMeta(protectedBoxManager) + ucGetAllMeta := usecase.NewGetAllMeta(protectedBoxManager) ucGetZettel := usecase.NewGetZettel(protectedBoxManager) ucParseZettel := usecase.NewParseZettel(rtConfig, ucGetZettel) - ucQuery := usecase.NewQuery(protectedBoxManager) - ucEvaluate := usecase.NewEvaluate(rtConfig, &ucGetZettel, &ucQuery) - ucQuery.SetEvaluate(&ucEvaluate) - ucTagZettel := usecase.NewTagZettel(protectedBoxManager, &ucQuery) - ucRoleZettel := usecase.NewRoleZettel(protectedBoxManager, &ucQuery) - ucListSyntax := usecase.NewListSyntax(protectedBoxManager) - ucListRoles := usecase.NewListRoles(protectedBoxManager) - ucDelete := usecase.NewDeleteZettel(logUc, protectedBoxManager) - ucUpdate := usecase.NewUpdateZettel(logUc, protectedBoxManager) - ucRename := usecase.NewRenameZettel(logUc, protectedBoxManager) - ucRefresh := usecase.NewRefresh(logUc, protectedBoxManager) - ucReIndex := usecase.NewReIndex(logUc, protectedBoxManager) - ucVersion := usecase.NewVersion(kernel.Main.GetConfig(kernel.CoreService, kernel.CoreVersion).(string)) - - a := api.New( - webLog.Clone().Str("adapter", "api").Child(), - webSrv, authManager, authManager, rtConfig, authPolicy) - wui := webui.New( - webLog.Clone().Str("adapter", "wui").Child(), - webSrv, authManager, rtConfig, authManager, boxManager, authPolicy, &ucEvaluate) + ucListMeta := usecase.NewListMeta(protectedBoxManager) + ucListRoles := usecase.NewListRole(protectedBoxManager) + ucListTags := usecase.NewListTags(protectedBoxManager) + ucZettelContext := usecase.NewZettelContext(protectedBoxManager) + ucDelete := usecase.NewDeleteZettel(protectedBoxManager) + ucUpdate := usecase.NewUpdateZettel(protectedBoxManager) + ucRename := usecase.NewRenameZettel(protectedBoxManager) webSrv.Handle("/", wui.MakeGetRootHandler(protectedBoxManager)) - if assetDir := kern.GetConfig(kernel.WebService, kernel.WebAssetDir).(string); assetDir != "" { - const assetPrefix = "/assets/" - webSrv.Handle(assetPrefix, http.StripPrefix(assetPrefix, http.FileServer(http.Dir(assetDir)))) - webSrv.Handle("/favicon.ico", wui.MakeFaviconHandler(assetDir)) - } // Web user interface - if !authManager.IsReadonly() { - webSrv.AddZettelRoute('b', server.MethodGet, wui.MakeGetRenameZettelHandler(ucGetZettel)) - webSrv.AddZettelRoute('b', server.MethodPost, wui.MakePostRenameZettelHandler(&ucRename)) - webSrv.AddListRoute('c', server.MethodGet, wui.MakeGetZettelFromListHandler(&ucQuery, &ucEvaluate, ucListRoles, ucListSyntax)) - webSrv.AddListRoute('c', server.MethodPost, wui.MakePostCreateZettelHandler(&ucCreateZettel)) - webSrv.AddZettelRoute('c', server.MethodGet, wui.MakeGetCreateZettelHandler( - ucGetZettel, &ucCreateZettel, ucListRoles, ucListSyntax)) - webSrv.AddZettelRoute('c', server.MethodPost, wui.MakePostCreateZettelHandler(&ucCreateZettel)) - webSrv.AddZettelRoute('d', server.MethodGet, wui.MakeGetDeleteZettelHandler(ucGetZettel, ucGetAllZettel)) - webSrv.AddZettelRoute('d', server.MethodPost, wui.MakePostDeleteZettelHandler(&ucDelete)) - webSrv.AddZettelRoute('e', server.MethodGet, wui.MakeEditGetZettelHandler(ucGetZettel, ucListRoles, ucListSyntax)) - webSrv.AddZettelRoute('e', server.MethodPost, wui.MakeEditSetZettelHandler(&ucUpdate)) - } - webSrv.AddListRoute('g', server.MethodGet, wui.MakeGetGoActionHandler(&ucRefresh)) - webSrv.AddListRoute('h', server.MethodGet, wui.MakeListHTMLMetaHandler(&ucQuery, &ucTagZettel, &ucRoleZettel, &ucReIndex)) - webSrv.AddZettelRoute('h', server.MethodGet, wui.MakeGetHTMLZettelHandler(&ucEvaluate, ucGetZettel)) - webSrv.AddListRoute('i', server.MethodGet, wui.MakeGetLoginOutHandler()) - webSrv.AddListRoute('i', server.MethodPost, wui.MakePostLoginHandler(&ucAuthenticate)) - webSrv.AddZettelRoute('i', server.MethodGet, wui.MakeGetInfoHandler( - ucParseZettel, &ucEvaluate, ucGetZettel, ucGetAllZettel, &ucQuery)) + webSrv.AddListRoute('a', http.MethodGet, wui.MakeGetLoginHandler()) + webSrv.AddListRoute('a', http.MethodPost, wui.MakePostLoginHandler(ucAuthenticate)) + webSrv.AddZettelRoute('a', http.MethodGet, wui.MakeGetLogoutHandler()) + if !authManager.IsReadonly() { + webSrv.AddZettelRoute('b', http.MethodGet, wui.MakeGetRenameZettelHandler(ucGetMeta)) + webSrv.AddZettelRoute('b', http.MethodPost, wui.MakePostRenameZettelHandler(ucRename)) + webSrv.AddZettelRoute('c', http.MethodGet, wui.MakeGetCopyZettelHandler( + ucGetZettel, usecase.NewCopyZettel())) + webSrv.AddZettelRoute('c', http.MethodPost, wui.MakePostCreateZettelHandler(ucCreateZettel)) + webSrv.AddZettelRoute('d', http.MethodGet, wui.MakeGetDeleteZettelHandler(ucGetZettel)) + webSrv.AddZettelRoute('d', http.MethodPost, wui.MakePostDeleteZettelHandler(ucDelete)) + webSrv.AddZettelRoute('e', http.MethodGet, wui.MakeEditGetZettelHandler(ucGetZettel)) + webSrv.AddZettelRoute('e', http.MethodPost, wui.MakeEditSetZettelHandler(ucUpdate)) + webSrv.AddZettelRoute('f', http.MethodGet, wui.MakeGetFolgeZettelHandler( + ucGetZettel, usecase.NewFolgeZettel(rtConfig))) + webSrv.AddZettelRoute('f', http.MethodPost, wui.MakePostCreateZettelHandler(ucCreateZettel)) + webSrv.AddZettelRoute('g', http.MethodGet, wui.MakeGetNewZettelHandler( + ucGetZettel, usecase.NewNewZettel())) + webSrv.AddZettelRoute('g', http.MethodPost, wui.MakePostCreateZettelHandler(ucCreateZettel)) + } + webSrv.AddListRoute('f', http.MethodGet, wui.MakeSearchHandler( + usecase.NewSearch(protectedBoxManager), ucGetMeta, ucGetZettel)) + webSrv.AddListRoute('h', http.MethodGet, wui.MakeListHTMLMetaHandler( + ucListMeta, ucListRoles, ucListTags)) + webSrv.AddZettelRoute('h', http.MethodGet, wui.MakeGetHTMLZettelHandler( + ucParseZettel, ucGetMeta)) + webSrv.AddZettelRoute('i', http.MethodGet, wui.MakeGetInfoHandler( + ucParseZettel, ucGetMeta, ucGetAllMeta)) + webSrv.AddZettelRoute('j', http.MethodGet, wui.MakeZettelContextHandler(ucZettelContext)) // API - webSrv.AddListRoute('a', server.MethodPost, a.MakePostLoginHandler(&ucAuthenticate)) - webSrv.AddListRoute('a', server.MethodPut, a.MakeRenewAuthHandler()) - webSrv.AddListRoute('x', server.MethodGet, a.MakeGetDataHandler(ucVersion)) - webSrv.AddListRoute('x', server.MethodPost, a.MakePostCommandHandler(&ucIsAuth, &ucRefresh)) - webSrv.AddListRoute('z', server.MethodGet, a.MakeQueryHandler(&ucQuery, &ucTagZettel, &ucRoleZettel, &ucReIndex)) - webSrv.AddZettelRoute('z', server.MethodGet, a.MakeGetZettelHandler(ucGetZettel, ucParseZettel, ucEvaluate)) + webSrv.AddZettelRoute('l', http.MethodGet, api.MakeGetLinksHandler(ucParseZettel)) + webSrv.AddZettelRoute('o', http.MethodGet, api.MakeGetOrderHandler( + usecase.NewZettelOrder(protectedBoxManager, ucParseZettel))) + webSrv.AddListRoute('r', http.MethodGet, api.MakeListRoleHandler(ucListRoles)) + webSrv.AddListRoute('t', http.MethodGet, api.MakeListTagsHandler(ucListTags)) + webSrv.AddListRoute('v', http.MethodPost, api.MakePostLoginHandler(ucAuthenticate)) + webSrv.AddListRoute('v', http.MethodPut, api.MakeRenewAuthHandler()) + webSrv.AddZettelRoute('x', http.MethodGet, api.MakeZettelContextHandler(ucZettelContext)) + webSrv.AddListRoute('z', http.MethodGet, api.MakeListMetaHandler( + usecase.NewListMeta(protectedBoxManager), ucGetMeta, ucParseZettel)) + webSrv.AddZettelRoute('z', http.MethodGet, api.MakeGetZettelHandler( + ucParseZettel, ucGetMeta)) if !authManager.IsReadonly() { - webSrv.AddListRoute('z', server.MethodPost, a.MakePostCreateZettelHandler(&ucCreateZettel)) - webSrv.AddZettelRoute('z', server.MethodPut, a.MakeUpdateZettelHandler(&ucUpdate)) - webSrv.AddZettelRoute('z', server.MethodDelete, a.MakeDeleteZettelHandler(&ucDelete)) - webSrv.AddZettelRoute('z', server.MethodMove, a.MakeRenameZettelHandler(&ucRename)) + webSrv.AddListRoute('z', http.MethodPost, api.MakePostCreateZettelHandler(ucCreateZettel)) + webSrv.AddZettelRoute('z', http.MethodDelete, api.MakeDeleteZettelHandler(ucDelete)) + webSrv.AddZettelRoute('z', http.MethodPut, api.MakeUpdateZettelHandler(ucUpdate)) + webSrv.AddZettelRoute('z', zsapi.MethodMove, api.MakeRenameZettelHandler(ucRename)) } if authManager.WithAuth() { webSrv.SetUserRetriever(usecase.NewGetUserByZid(boxManager)) } } - -type getUserImpl struct{} - -func (*getUserImpl) GetUser(ctx context.Context) *meta.Meta { return server.GetUser(ctx) } ADDED cmd/cmd_run_simple.go Index: cmd/cmd_run_simple.go ================================================================== --- cmd/cmd_run_simple.go +++ cmd/cmd_run_simple.go @@ -0,0 +1,52 @@ +//----------------------------------------------------------------------------- +// Copyright (c) 2020-2021 Detlef Stern +// +// This file is part of zettelstore. +// +// Zettelstore is licensed under the latest version of the EUPL (European Union +// Public License). Please see file LICENSE.txt for your rights and obligations +// under this license. +//----------------------------------------------------------------------------- + +package cmd + +import ( + "flag" + "fmt" + "os" + "strings" + + "zettelstore.de/z/domain/meta" + "zettelstore.de/z/kernel" +) + +func flgSimpleRun(fs *flag.FlagSet) { + fs.String("d", "", "zettel directory") +} + +func runSimpleFunc(fs *flag.FlagSet, cfg *meta.Meta) (int, error) { + kern := kernel.Main + listenAddr := kern.GetConfig(kernel.WebService, kernel.WebListenAddress).(string) + exitCode, err := doRun(false) + if idx := strings.LastIndexByte(listenAddr, ':'); idx >= 0 { + kern.Log() + kern.Log("--------------------------") + kern.Log("Open your browser and enter the following URL:") + kern.Log() + kern.Log(fmt.Sprintf(" http://localhost%v", listenAddr[idx:])) + kern.Log() + } + kern.WaitForShutdown() + return exitCode, err +} + +// runSimple is called, when the user just starts the software via a double click +// or via a simple call ``./zettelstore`` on the command line. +func runSimple() int { + dir := "./zettel" + if err := os.MkdirAll(dir, 0750); err != nil { + fmt.Fprintf(os.Stderr, "Unable to create zettel directory %q (%s)\n", dir, err) + os.Exit(1) + } + return executeCommand("run-simple", "-d", dir) +} Index: cmd/command.go ================================================================== --- cmd/command.go +++ cmd/command.go @@ -1,43 +1,40 @@ //----------------------------------------------------------------------------- -// Copyright (c) 2020-present Detlef Stern +// Copyright (c) 2020-2021 Detlef Stern // -// This file is part of Zettelstore. +// This file is part of zettelstore. // // Zettelstore is licensed under the latest version of the EUPL (European Union // Public License). Please see file LICENSE.txt for your rights and obligations // under this license. -// -// SPDX-License-Identifier: EUPL-1.2 -// SPDX-FileCopyrightText: 2020-present Detlef Stern //----------------------------------------------------------------------------- package cmd import ( "flag" + "sort" - "t73f.de/r/zsc/maps" - "zettelstore.de/z/logger" + "zettelstore.de/z/domain/meta" ) // Command stores information about commands / sub-commands. type Command struct { Name string // command name as it appears on the command line Func CommandFunc // function that executes a command - Simple bool // Operate in simple-mode Boxes bool // if true then boxes will be set up Header bool // Print a heading on startup LineServer bool // Start admin line server - SetFlags func(*flag.FlagSet) // function to set up flag.FlagSet + Flags func(*flag.FlagSet) // function to set up flag.FlagSet flags *flag.FlagSet // flags that belong to the command + } // CommandFunc is the function that executes the command. // It accepts the parsed command line parameters. // It returns the exit code and an error. -type CommandFunc func(*flag.FlagSet) (int, error) +type CommandFunc func(*flag.FlagSet, *meta.Meta) (int, error) // GetFlags return the flag.FlagSet defined for the command. func (c *Command) GetFlags() *flag.FlagSet { return c.flags } var commands = make(map[string]Command) @@ -49,14 +46,12 @@ } if _, ok := commands[cmd.Name]; ok { panic("Command already registered: " + cmd.Name) } cmd.flags = flag.NewFlagSet(cmd.Name, flag.ExitOnError) - cmd.flags.String("l", logger.InfoLevel.String(), "log level specification") - - if cmd.SetFlags != nil { - cmd.SetFlags(cmd.flags) + if cmd.Flags != nil { + cmd.Flags(cmd.flags) } commands[cmd.Name] = cmd } // Get returns the command identified by the given name and a bool to signal success. @@ -64,6 +59,13 @@ cmd, ok := commands[name] return cmd, ok } // List returns a sorted list of all registered command names. -func List() []string { return maps.Keys(commands) } +func List() []string { + result := make([]string, 0, len(commands)) + for name := range commands { + result = append(result, name) + } + sort.Strings(result) + return result +} ADDED cmd/fd_limit.go Index: cmd/fd_limit.go ================================================================== --- cmd/fd_limit.go +++ cmd/fd_limit.go @@ -0,0 +1,15 @@ +//----------------------------------------------------------------------------- +// Copyright (c) 2021 Detlef Stern +// +// This file is part of zettelstore. +// +// Zettelstore is licensed under the latest version of the EUPL (European Union +// Public License). Please see file LICENSE.txt for your rights and obligations +// under this license. +//----------------------------------------------------------------------------- + +// +build !darwin + +package cmd + +func raiseFdLimit() error { return nil } ADDED cmd/fd_limit_raise.go Index: cmd/fd_limit_raise.go ================================================================== --- cmd/fd_limit_raise.go +++ cmd/fd_limit_raise.go @@ -0,0 +1,47 @@ +//----------------------------------------------------------------------------- +// Copyright (c) 2021 Detlef Stern +// +// This file is part of zettelstore. +// +// Zettelstore is licensed under the latest version of the EUPL (European Union +// Public License). Please see file LICENSE.txt for your rights and obligations +// under this license. +//----------------------------------------------------------------------------- + +// +build darwin + +package cmd + +import ( + "log" + "syscall" +) + +const minFiles = 1048576 + +func raiseFdLimit() error { + var rLimit syscall.Rlimit + err := syscall.Getrlimit(syscall.RLIMIT_NOFILE, &rLimit) + if err != nil { + return err + } + if rLimit.Cur >= minFiles { + return nil + } + rLimit.Cur = minFiles + if rLimit.Cur > rLimit.Max { + rLimit.Cur = rLimit.Max + } + err = syscall.Setrlimit(syscall.RLIMIT_NOFILE, &rLimit) + if err != nil { + return err + } + err = syscall.Getrlimit(syscall.RLIMIT_NOFILE, &rLimit) + if err != nil { + return err + } + if rLimit.Cur < minFiles { + log.Printf("Make sure you have no more than %d files in all your boxes if you enabled notification\n", rLimit.Cur) + } + return nil +} Index: cmd/main.go ================================================================== --- cmd/main.go +++ cmd/main.go @@ -1,248 +1,231 @@ //----------------------------------------------------------------------------- -// Copyright (c) 2020-present Detlef Stern +// Copyright (c) 2020-2021 Detlef Stern // -// This file is part of Zettelstore. +// This file is part of zettelstore. // // Zettelstore is licensed under the latest version of the EUPL (European Union // Public License). Please see file LICENSE.txt for your rights and obligations // under this license. -// -// SPDX-License-Identifier: EUPL-1.2 -// SPDX-FileCopyrightText: 2020-present Detlef Stern //----------------------------------------------------------------------------- package cmd import ( - "crypto/sha256" + "errors" "flag" "fmt" "net" "net/url" "os" - "runtime/debug" "strconv" "strings" - "time" - "t73f.de/r/zsc/api" - "t73f.de/r/zsc/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/domain/id" + "zettelstore.de/z/domain/meta" + "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" ) -const strRunSimple = "run-simple" +const ( + defConfigfile = ".zscfg" +) func init() { RegisterCommand(Command{ Name: "help", - Func: func(*flag.FlagSet) (int, error) { + Func: func(*flag.FlagSet, *meta.Meta) (int, error) { fmt.Println("Available commands:") for _, name := range List() { fmt.Printf("- %q\n", name) } return 0, nil }, }) RegisterCommand(Command{ Name: "version", - Func: func(*flag.FlagSet) (int, error) { return 0, nil }, + Func: func(*flag.FlagSet, *meta.Meta) (int, error) { return 0, nil }, Header: true, }) RegisterCommand(Command{ Name: "run", Func: runFunc, Boxes: true, Header: true, LineServer: true, - SetFlags: flgRun, + Flags: flgRun, }) RegisterCommand(Command{ - Name: strRunSimple, - Func: runFunc, - Simple: true, + Name: "run-simple", + Func: runSimpleFunc, Boxes: true, Header: true, - // LineServer: true, - SetFlags: func(fs *flag.FlagSet) { - // fs.Uint("a", 0, "port number kernel service (0=disable)") - fs.String("d", "", "zettel directory") - }, + Flags: flgSimpleRun, }) RegisterCommand(Command{ Name: "file", Func: cmdFile, - SetFlags: func(fs *flag.FlagSet) { - fs.String("t", api.EncoderHTML.String(), "target output encoding") + Flags: func(fs *flag.FlagSet) { + fs.String("t", "html", "target output format") }, }) RegisterCommand(Command{ Name: "password", Func: cmdPassword, }) } -func fetchStartupConfiguration(fs *flag.FlagSet) (string, *meta.Meta) { +func readConfig(fs *flag.FlagSet) (cfg *meta.Meta) { + var configFile string if configFlag := fs.Lookup("c"); configFlag != nil { - if filename := configFlag.Value.String(); filename != "" { - content, err := readConfiguration(filename) - return filename, createConfiguration(content, err) - } - } - filename, content, err := searchAndReadConfiguration() - return filename, createConfiguration(content, err) -} - -func createConfiguration(content []byte, err error) *meta.Meta { + configFile = configFlag.Value.String() + } else { + configFile = defConfigfile + } + content, err := os.ReadFile(configFile) if err != nil { return meta.New(id.Invalid) } - return meta.NewFromInput(id.Invalid, input.NewInput(content)) -} - -func readConfiguration(filename string) ([]byte, error) { return os.ReadFile(filename) } - -func searchAndReadConfiguration() (string, []byte, error) { - for _, filename := range []string{"zettelstore.cfg", "zsconfig.txt", "zscfg.txt", "_zscfg", ".zscfg"} { - if content, err := readConfiguration(filename); err == nil { - return filename, content, nil - } - } - return "", nil, os.ErrNotExist -} - -func getConfig(fs *flag.FlagSet) (string, *meta.Meta) { - filename, cfg := fetchStartupConfiguration(fs) + return meta.NewFromInput(id.Invalid, input.NewInput(string(content))) +} + +func getConfig(fs *flag.FlagSet) *meta.Meta { + cfg := readConfig(fs) fs.Visit(func(flg *flag.Flag) { switch flg.Name { case "p": - cfg.Set(keyListenAddr, net.JoinHostPort("127.0.0.1", flg.Value.String())) + if portStr, err := parsePort(flg.Value.String()); err == nil { + cfg.Set(keyListenAddr, net.JoinHostPort("127.0.0.1", portStr)) + } case "a": - cfg.Set(keyAdminPort, flg.Value.String()) + if portStr, err := parsePort(flg.Value.String()); err == nil { + cfg.Set(keyAdminPort, portStr) + } case "d": val := flg.Value.String() if strings.HasPrefix(val, "/") { val = "dir://" + val } else { val = "dir:" + val } - deleteConfiguredBoxes(cfg) cfg.Set(keyBoxOneURI, val) - case "l": - cfg.Set(keyLogLevel, flg.Value.String()) - case "debug": - cfg.Set(keyDebug, flg.Value.String()) case "r": cfg.Set(keyReadOnly, flg.Value.String()) case "v": cfg.Set(keyVerbose, flg.Value.String()) } }) - return filename, cfg + return cfg } -func deleteConfiguredBoxes(cfg *meta.Meta) { - for _, p := range cfg.PairsRest() { - if key := p.Key; strings.HasPrefix(key, kernel.BoxURIs) { - cfg.Delete(key) - } +func parsePort(s string) (string, error) { + port, err := net.LookupPort("tcp", s) + if err != nil { + fmt.Fprintf(os.Stderr, "Wrong port specification: %q", s) + return "", err } + return strconv.Itoa(port), nil } const ( keyAdminPort = "admin-port" - keyAssetDir = "asset-dir" - keyBaseURL = "base-url" - keyDebug = "debug-mode" keyDefaultDirBoxType = "default-dir-box-type" keyInsecureCookie = "insecure-cookie" - keyInsecureHTML = "insecure-html" keyListenAddr = "listen-addr" - keyLogLevel = "log-level" - keyMaxRequestSize = "max-request-size" keyOwner = "owner" keyPersistentCookie = "persistent-cookie" keyBoxOneURI = kernel.BoxURIs + "1" keyReadOnly = "read-only-mode" keyTokenLifetimeHTML = "token-lifetime-html" keyTokenLifetimeAPI = "token-lifetime-api" keyURLPrefix = "url-prefix" - keyVerbose = "verbose-mode" -) - -func setServiceConfig(cfg *meta.Meta) bool { - debugMode := cfg.GetBool(keyDebug) - if debugMode && kernel.Main.GetKernelLogger().Level() > logger.DebugLevel { - kernel.Main.SetLogLevel(logger.DebugLevel.String()) - } - if logLevel, found := cfg.Get(keyLogLevel); found { - kernel.Main.SetLogLevel(logLevel) - } - err := setConfigValue(nil, kernel.CoreService, kernel.CoreDebug, debugMode) - err = setConfigValue(err, kernel.CoreService, kernel.CoreVerbose, cfg.GetBool(keyVerbose)) - if val, found := cfg.Get(keyAdminPort); found { - err = setConfigValue(err, kernel.CoreService, kernel.CorePort, val) - } - - err = setConfigValue(err, kernel.AuthService, kernel.AuthOwner, cfg.GetDefault(keyOwner, "")) - err = setConfigValue(err, kernel.AuthService, kernel.AuthReadonly, cfg.GetBool(keyReadOnly)) - - err = setConfigValue( - err, kernel.BoxService, kernel.BoxDefaultDirType, - cfg.GetDefault(keyDefaultDirBoxType, kernel.BoxDirTypeNotify)) - err = setConfigValue(err, kernel.BoxService, kernel.BoxURIs+"1", "dir:./zettel") - for i := 1; ; i++ { - key := kernel.BoxURIs + strconv.Itoa(i) + keyVerbose = "verbose" +) + +func setServiceConfig(cfg *meta.Meta) error { + ok := setConfigValue(true, kernel.CoreService, kernel.CoreVerbose, cfg.GetBool(keyVerbose)) + if val, found := cfg.Get(keyAdminPort); found { + ok = setConfigValue(ok, kernel.CoreService, kernel.CorePort, val) + } + + ok = setConfigValue(ok, kernel.AuthService, kernel.AuthOwner, cfg.GetDefault(keyOwner, "")) + ok = setConfigValue(ok, kernel.AuthService, kernel.AuthReadonly, cfg.GetBool(keyReadOnly)) + + ok = setConfigValue( + ok, kernel.BoxService, kernel.BoxDefaultDirType, + cfg.GetDefault(keyDefaultDirBoxType, kernel.BoxDirTypeNotify)) + ok = setConfigValue(ok, kernel.BoxService, kernel.BoxURIs+"1", "dir:./zettel") + format := kernel.BoxURIs + "%v" + for i := 1; ; i++ { + key := fmt.Sprintf(format, i) val, found := cfg.Get(key) if !found { break } - err = setConfigValue(err, kernel.BoxService, key, val) - } - - err = setConfigValue(err, kernel.ConfigService, kernel.ConfigInsecureHTML, cfg.GetDefault(keyInsecureHTML, kernel.ConfigSecureHTML)) - - err = setConfigValue(err, kernel.WebService, kernel.WebListenAddress, cfg.GetDefault(keyListenAddr, "127.0.0.1:23123")) - if val, found := cfg.Get(keyBaseURL); found { - err = setConfigValue(err, kernel.WebService, kernel.WebBaseURL, val) - } - if val, found := cfg.Get(keyURLPrefix); found { - err = setConfigValue(err, kernel.WebService, kernel.WebURLPrefix, val) - } - err = setConfigValue(err, kernel.WebService, kernel.WebSecureCookie, !cfg.GetBool(keyInsecureCookie)) - err = setConfigValue(err, kernel.WebService, kernel.WebPersistentCookie, cfg.GetBool(keyPersistentCookie)) - if val, found := cfg.Get(keyMaxRequestSize); found { - err = setConfigValue(err, kernel.WebService, kernel.WebMaxRequestSize, val) - } - err = setConfigValue( - err, kernel.WebService, kernel.WebTokenLifetimeAPI, cfg.GetDefault(keyTokenLifetimeAPI, "")) - err = setConfigValue( - err, kernel.WebService, kernel.WebTokenLifetimeHTML, cfg.GetDefault(keyTokenLifetimeHTML, "")) - if val, found := cfg.Get(keyAssetDir); found { - err = setConfigValue(err, kernel.WebService, kernel.WebAssetDir, val) - } - return err == nil -} - -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().Error().Str("key", key).Str("value", fmt.Sprint(val)).Err(err).Msg("Unable to set configuration") - } - } - return err + ok = setConfigValue(ok, kernel.BoxService, key, val) + } + + ok = setConfigValue( + ok, kernel.WebService, kernel.WebListenAddress, + cfg.GetDefault(keyListenAddr, "127.0.0.1:23123")) + ok = setConfigValue(ok, kernel.WebService, kernel.WebURLPrefix, cfg.GetDefault(keyURLPrefix, "/")) + ok = setConfigValue(ok, kernel.WebService, kernel.WebSecureCookie, !cfg.GetBool(keyInsecureCookie)) + ok = setConfigValue(ok, kernel.WebService, kernel.WebPersistentCookie, cfg.GetBool(keyPersistentCookie)) + ok = setConfigValue( + ok, kernel.WebService, kernel.WebTokenLifetimeAPI, cfg.GetDefault(keyTokenLifetimeAPI, "")) + ok = setConfigValue( + ok, kernel.WebService, kernel.WebTokenLifetimeHTML, cfg.GetDefault(keyTokenLifetimeHTML, "")) + + if !ok { + return errors.New("unable to set configuration") + } + return nil +} + +func setConfigValue(ok bool, subsys kernel.Service, key string, val interface{}) bool { + done := kernel.Main.SetConfig(subsys, key, fmt.Sprintf("%v", val)) + if !done { + kernel.Main.Log("unable to set configuration:", key, val) + } + return ok && done +} + +func setupOperations(cfg *meta.Meta, withBoxes bool) { + var createManager kernel.CreateBoxManagerFunc + if withBoxes { + err := raiseFdLimit() + if err != nil { + srvm := kernel.Main + srvm.Log("Raising some limitions did not work:", err) + srvm.Log("Prepare to encounter errors. Most of them can be mitigated. See the manual for details") + srvm.SetConfig(kernel.BoxService, kernel.BoxDefaultDirType, kernel.BoxDirTypeSimple) + } + createManager = func(boxURIs []*url.URL, authManager auth.Manager, rtConfig config.Config) (box.Manager, error) { + compbox.Setup(cfg) + return manager.New(boxURIs, authManager, rtConfig) + } + } else { + createManager = func([]*url.URL, auth.Manager, config.Config) (box.Manager, error) { return nil, nil } + } + + kernel.Main.SetCreators( + func(readonly bool, owner id.Zid) (auth.Manager, error) { + return impl.New(readonly, owner, cfg.GetDefault("secret", "")), nil + }, + createManager, + func(srv server.Server, plMgr box.Manager, authMgr auth.Manager, rtConfig config.Config) error { + setupRouting(srv, plMgr, authMgr, rtConfig) + return nil + }, + ) } func executeCommand(name string, args ...string) int { command, ok := Get(name) if !ok { @@ -252,127 +235,34 @@ fs := command.GetFlags() if err := fs.Parse(args); err != nil { fmt.Fprintf(os.Stderr, "%s: unable to parse flags: %v %v\n", name, args, err) return 1 } - filename, cfg := getConfig(fs) - if !setServiceConfig(cfg) { - fs.Usage() - return 2 - } - - kern := kernel.Main - var createManager kernel.CreateBoxManagerFunc - if command.Boxes { - createManager = func(boxURIs []*url.URL, authManager auth.Manager, rtConfig config.Config) (box.Manager, error) { - compbox.Setup(cfg) - return manager.New(boxURIs, authManager, rtConfig) - } - } else { - createManager = func([]*url.URL, auth.Manager, config.Config) (box.Manager, error) { return nil, nil } - } - - secret := cfg.GetDefault("secret", "") - if len(secret) < 16 && cfg.GetDefault(keyOwner, "") != "" { - fmt.Fprintf(os.Stderr, "secret must have at least length 16 when authentication is enabled, but is %q\n", secret) - return 2 - } - cfg.Delete("secret") - secret = fmt.Sprintf("%x", sha256.Sum256([]byte(secret))) - - kern.SetCreators( - func(readonly bool, owner id.Zid) (auth.Manager, error) { - return impl.New(readonly, owner, secret), nil - }, - createManager, - func(srv server.Server, plMgr box.Manager, authMgr auth.Manager, rtConfig config.Config) error { - setupRouting(srv, plMgr, authMgr, rtConfig) - return nil - }, - ) - - if command.Simple { - kern.SetConfig(kernel.ConfigService, kernel.ConfigSimpleMode, "true") - } - kern.Start(command.Header, command.LineServer, filename) - exitCode, err := command.Func(fs) + cfg := getConfig(fs) + if err := setServiceConfig(cfg); err != nil { + fmt.Fprintf(os.Stderr, "%s: %v\n", name, err) + return 2 + } + setupOperations(cfg, command.Boxes) + kernel.Main.Start(command.Header, command.LineServer) + exitCode, err := command.Func(fs, cfg) if err != nil { fmt.Fprintf(os.Stderr, "%s: %v\n", name, err) } - kern.Shutdown(true) + kernel.Main.Shutdown(true) return exitCode } -// runSimple is called, when the user just starts the software via a double click -// or via a simple call “./zettelstore“ on the command line. -func runSimple() int { - if _, _, err := searchAndReadConfiguration(); err == nil { - return executeCommand(strRunSimple) - } - dir := "./zettel" - if err := os.MkdirAll(dir, 0750); err != nil { - fmt.Fprintf(os.Stderr, "Unable to create zettel directory %q (%s)\n", dir, err) - return 1 - } - return executeCommand(strRunSimple, "-d", dir) -} - -var cpuprofile = flag.String("cpuprofile", "", "write cpu profile to `file`") -var memprofile = flag.String("memprofile", "", "write memory profile to `file`") - -// Main is the real entrypoint of the zettelstore. -func Main(progName, buildVersion string) int { - info := retrieveVCSInfo(buildVersion) - fullVersion := info.revision - if info.dirty { - fullVersion += "-dirty" - } - kernel.Main.Setup(progName, fullVersion, info.time) - flag.Parse() - if *cpuprofile != "" || *memprofile != "" { - if *cpuprofile != "" { - kernel.Main.StartProfiling(kernel.ProfileCPU, *cpuprofile) - } else { - kernel.Main.StartProfiling(kernel.ProfileHead, *memprofile) - } - defer kernel.Main.StopProfiling() - } - args := flag.Args() - if len(args) == 0 { - return runSimple() - } - return executeCommand(args[0], args[1:]...) -} - -type vcsInfo struct { - revision string - dirty bool - time time.Time -} - -func retrieveVCSInfo(version string) vcsInfo { - buildTime := time.Date(1970, 1, 1, 0, 0, 0, 0, time.UTC) - info, ok := debug.ReadBuildInfo() - if !ok { - return vcsInfo{revision: version, dirty: false, time: buildTime} - } - result := vcsInfo{time: buildTime} - for _, kv := range info.Settings { - switch kv.Key { - case "vcs.revision": - revision := "+" + kv.Value - if len(revision) > 11 { - revision = revision[:11] - } - result.revision = version + revision - case "vcs.modified": - if kv.Value == "true" { - result.dirty = true - } - case "vcs.time": - if t, err := time.Parse(time.RFC3339, kv.Value); err == nil { - result.time = t - } - } - } - return result +// Main is the real entrypoint of the zettelstore. +func Main(progName, buildVersion string) { + kernel.Main.SetConfig(kernel.CoreService, kernel.CoreProgname, progName) + kernel.Main.SetConfig(kernel.CoreService, kernel.CoreVersion, buildVersion) + var exitCode int + if len(os.Args) <= 1 { + exitCode = runSimple() + } else { + exitCode = executeCommand(os.Args[1], os.Args[2:]...) + } + if exitCode != 0 { + os.Exit(exitCode) + } } Index: cmd/register.go ================================================================== --- cmd/register.go +++ cmd/register.go @@ -1,16 +1,13 @@ //----------------------------------------------------------------------------- -// Copyright (c) 2020-present Detlef Stern +// Copyright (c) 2020-2021 Detlef Stern // -// This file is part of Zettelstore. +// This file is part of zettelstore. // // Zettelstore is licensed under the latest version of the EUPL (European Union // Public License). Please see file LICENSE.txt for your rights and obligations // under this license. -// -// SPDX-License-Identifier: EUPL-1.2 -// SPDX-FileCopyrightText: 2020-present Detlef Stern //----------------------------------------------------------------------------- // Package cmd provides command generic functions. package cmd @@ -20,18 +17,17 @@ _ "zettelstore.de/z/box/constbox" // Allow to use global internal box. _ "zettelstore.de/z/box/dirbox" // Allow to use directory box. _ "zettelstore.de/z/box/filebox" // Allow to use file box. _ "zettelstore.de/z/box/membox" // Allow to use in-memory box. _ "zettelstore.de/z/encoder/htmlenc" // Allow to use HTML encoder. - _ "zettelstore.de/z/encoder/mdenc" // Allow to use markdown encoder. - _ "zettelstore.de/z/encoder/shtmlenc" // Allow to use SHTML encoder. - _ "zettelstore.de/z/encoder/szenc" // Allow to use Sz encoder. + _ "zettelstore.de/z/encoder/jsonenc" // Allow to use JSON encoder. + _ "zettelstore.de/z/encoder/nativeenc" // Allow to use native encoder. + _ "zettelstore.de/z/encoder/rawenc" // Allow to use raw encoder. _ "zettelstore.de/z/encoder/textenc" // Allow to use text encoder. _ "zettelstore.de/z/encoder/zmkenc" // Allow to use zmk encoder. _ "zettelstore.de/z/kernel/impl" // Allow kernel implementation to create itself _ "zettelstore.de/z/parser/blob" // Allow to use BLOB parser. - _ "zettelstore.de/z/parser/draw" // Allow to use draw parser. _ "zettelstore.de/z/parser/markdown" // Allow to use markdown parser. _ "zettelstore.de/z/parser/none" // Allow to use none parser. _ "zettelstore.de/z/parser/plain" // Allow to use plain parser. _ "zettelstore.de/z/parser/zettelmark" // Allow to use zettelmark parser. ) Index: cmd/zettelstore/main.go ================================================================== --- cmd/zettelstore/main.go +++ cmd/zettelstore/main.go @@ -1,29 +1,21 @@ //----------------------------------------------------------------------------- -// Copyright (c) 2020-present Detlef Stern +// Copyright (c) 2020-2021 Detlef Stern // -// This file is part of Zettelstore. +// This file is part of zettelstore. // // Zettelstore is licensed under the latest version of the EUPL (European Union // Public License). Please see file LICENSE.txt for your rights and obligations // under this license. -// -// SPDX-License-Identifier: EUPL-1.2 -// SPDX-FileCopyrightText: 2020-present Detlef Stern //----------------------------------------------------------------------------- // Package main is the starting point for the zettelstore command. package main -import ( - "os" - - "zettelstore.de/z/cmd" -) +import "zettelstore.de/z/cmd" // Version variable. Will be filled by build process. var version string = "" func main() { - exitCode := cmd.Main("Zettelstore", version) - os.Exit(exitCode) + cmd.Main("Zettelstore", version) } Index: collect/collect.go ================================================================== --- collect/collect.go +++ collect/collect.go @@ -1,46 +1,43 @@ //----------------------------------------------------------------------------- -// Copyright (c) 2020-present Detlef Stern +// Copyright (c) 2020-2021 Detlef Stern // -// This file is part of Zettelstore. +// This file is part of zettelstore. // // Zettelstore is licensed under the latest version of the EUPL (European Union // Public License). Please see file LICENSE.txt for your rights and obligations // under this license. -// -// 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 import "zettelstore.de/z/ast" // Summary stores the relevant parts of the syntax tree type Summary struct { - Links []*ast.Reference // list of all linked material - Embeds []*ast.Reference // list of all embedded material + Links []*ast.Reference // list of all referenced links + Images []*ast.Reference // list of all referenced images Cites []*ast.CiteNode // list of all referenced citations } // References returns all references mentioned in the given zettel. This also // includes references to images. func References(zn *ast.ZettelNode) (s Summary) { - ast.Walk(&s, &zn.Ast) + ast.WalkBlockSlice(&s, zn.Ast) return s } // Visit all node to collect data for the summary. func (s *Summary) Visit(node ast.Node) ast.Visitor { switch n := node.(type) { - case *ast.TranscludeNode: - s.Embeds = append(s.Embeds, n.Ref) case *ast.LinkNode: s.Links = append(s.Links, n.Ref) - case *ast.EmbedRefNode: - s.Embeds = append(s.Embeds, n.Ref) + case *ast.ImageNode: + if n.Ref != nil { + s.Images = append(s.Images, n.Ref) + } case *ast.CiteNode: s.Cites = append(s.Cites, n) } return s } Index: collect/collect_test.go ================================================================== --- collect/collect_test.go +++ collect/collect_test.go @@ -1,16 +1,13 @@ //----------------------------------------------------------------------------- -// Copyright (c) 2020-present Detlef Stern +// Copyright (c) 2020-2021 Detlef Stern // -// This file is part of Zettelstore. +// This file is part of zettelstore. // // Zettelstore is licensed under the latest version of the EUPL (European Union // Public License). Please see file LICENSE.txt for your rights and obligations // under this license. -// -// SPDX-License-Identifier: EUPL-1.2 -// SPDX-FileCopyrightText: 2020-present Detlef Stern //----------------------------------------------------------------------------- // Package collect_test provides some unit test for collectors. package collect_test @@ -31,34 +28,45 @@ func TestLinks(t *testing.T) { t.Parallel() zn := &ast.ZettelNode{} summary := collect.References(zn) - if summary.Links != nil || summary.Embeds != nil { - t.Error("No links/images expected, but got:", summary.Links, "and", summary.Embeds) + if summary.Links != nil || summary.Images != nil { + t.Error("No links/images expected, but got:", summary.Links, "and", summary.Images) } intNode := &ast.LinkNode{Ref: parseRef("01234567890123")} - para := ast.CreateParaNode(intNode, &ast.LinkNode{Ref: parseRef("https://zettelstore.de/z")}) + para := &ast.ParaNode{ + Inlines: ast.InlineSlice{ + intNode, + &ast.LinkNode{Ref: parseRef("https://zettelstore.de/z")}, + }, + } zn.Ast = ast.BlockSlice{para} summary = collect.References(zn) - if summary.Links == nil || summary.Embeds != nil { - t.Error("Links expected, and no images, but got:", summary.Links, "and", summary.Embeds) + if summary.Links == nil || summary.Images != nil { + t.Error("Links expected, and no images, but got:", summary.Links, "and", summary.Images) } para.Inlines = append(para.Inlines, intNode) summary = collect.References(zn) if cnt := len(summary.Links); cnt != 3 { t.Error("Link count does not work. Expected: 3, got", summary.Links) } } -func TestEmbed(t *testing.T) { +func TestImage(t *testing.T) { t.Parallel() zn := &ast.ZettelNode{ - Ast: ast.BlockSlice{ast.CreateParaNode(&ast.EmbedRefNode{Ref: parseRef("12345678901234")})}, + Ast: ast.BlockSlice{ + &ast.ParaNode{ + Inlines: ast.InlineSlice{ + &ast.ImageNode{Ref: parseRef("12345678901234")}, + }, + }, + }, } summary := collect.References(zn) - if summary.Embeds == nil { - t.Error("Only image expected, but got: ", summary.Embeds) + if summary.Images == nil { + t.Error("Only image expected, but got: ", summary.Images) } } Index: collect/order.go ================================================================== --- collect/order.go +++ collect/order.go @@ -1,16 +1,13 @@ //----------------------------------------------------------------------------- -// Copyright (c) 2021-present Detlef Stern +// Copyright (c) 2021 Detlef Stern // -// This file is part of Zettelstore. +// This file is part of zettelstore. // // Zettelstore is licensed under the latest version of the EUPL (European Union // Public License). Please see file LICENSE.txt for your rights and obligations // under this license. -// -// 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 @@ -17,19 +14,17 @@ import "zettelstore.de/z/ast" // Order of internal reference within the given zettel. func Order(zn *ast.ZettelNode) (result []*ast.Reference) { for _, bn := range zn.Ast { - ln, ok := bn.(*ast.NestedListNode) - if !ok { - continue - } - switch ln.Kind { - case ast.NestedListOrdered, ast.NestedListUnordered: - for _, is := range ln.Items { - if ref := firstItemZettelReference(is); ref != nil { - result = append(result, ref) + if ln, ok := bn.(*ast.NestedListNode); ok { + switch ln.Kind { + case ast.NestedListOrdered, ast.NestedListUnordered: + for _, is := range ln.Items { + if ref := firstItemZettelReference(is); ref != nil { + result = append(result, ref) + } } } } } return result @@ -44,21 +39,19 @@ } } return nil } -func firstInlineZettelReference(is ast.InlineSlice) (result *ast.Reference) { - for _, inl := range is { +func firstInlineZettelReference(ins ast.InlineSlice) (result *ast.Reference) { + for _, inl := range ins { switch in := inl.(type) { case *ast.LinkNode: if ref := in.Ref; ref.IsZettel() { return ref } result = firstInlineZettelReference(in.Inlines) - case *ast.EmbedRefNode: - result = firstInlineZettelReference(in.Inlines) - case *ast.EmbedBLOBNode: + case *ast.ImageNode: result = firstInlineZettelReference(in.Inlines) case *ast.CiteNode: result = firstInlineZettelReference(in.Inlines) case *ast.FootnoteNode: // Ignore references in footnotes ADDED collect/split.go Index: collect/split.go ================================================================== --- collect/split.go +++ collect/split.go @@ -0,0 +1,47 @@ +//----------------------------------------------------------------------------- +// Copyright (c) 2020-2021 Detlef Stern +// +// This file is part of zettelstore. +// +// Zettelstore is licensed under the latest version of the EUPL (European Union +// Public License). Please see file LICENSE.txt for your rights and obligations +// under this license. +//----------------------------------------------------------------------------- + +// Package collect provides functions to collect items from a syntax tree. +package collect + +import "zettelstore.de/z/ast" + +// 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(map[string]bool) + mapLocal := make(map[string]bool) + mapExternal := make(map[string]bool) + 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 map[string]bool, ref *ast.Reference) []*ast.Reference { + s := ref.String() + if _, ok := refSet[s]; !ok { + reflist = append(reflist, ref) + refSet[s] = true + } + return reflist +} Index: config/config.go ================================================================== --- config/config.go +++ config/config.go @@ -1,109 +1,104 @@ //----------------------------------------------------------------------------- -// Copyright (c) 2020-present Detlef Stern +// Copyright (c) 2020-2021 Detlef Stern // -// This file is part of Zettelstore. +// This file is part of zettelstore. // // Zettelstore is licensed under the latest version of the EUPL (European Union // Public License). Please see file LICENSE.txt for your rights and obligations // under this license. -// -// SPDX-License-Identifier: EUPL-1.2 -// SPDX-FileCopyrightText: 2020-present Detlef Stern //----------------------------------------------------------------------------- // Package config provides functions to retrieve runtime configuration data. package config import ( - "context" - - "zettelstore.de/z/zettel/meta" -) - -// Key values that are supported by Config.Get -const ( - KeyFooterZettel = "footer-zettel" - KeyHomeZettel = "home-zettel" - KeyShowBackLinks = "show-back-links" - KeyShowFolgeLinks = "show-folge-links" - KeyShowSubordinateLinks = "show-subordinate-links" - KeyShowSuccessorLinks = "show-successor-links" - // api.KeyLang + "zettelstore.de/z/domain/id" + "zettelstore.de/z/domain/meta" ) // Config allows to retrieve all defined configuration values that can be changed during runtime. type Config interface { AuthConfig - // Get returns the value of the given key. It searches first in the given metadata, - // then in the data of the current user, and at last in the system-wide data. - Get(ctx context.Context, m *meta.Meta, key string) string - // AddDefaultValues enriches the given meta data with its default values. - AddDefaultValues(context.Context, *meta.Meta) *meta.Meta + AddDefaultValues(m *meta.Meta) *meta.Meta + + // GetDefaultTitle returns the current value of the "default-title" key. + GetDefaultTitle() string + + // GetDefaultRole returns the current value of the "default-role" key. + GetDefaultRole() string + + // GetDefaultSyntax returns the current value of the "default-syntax" key. + GetDefaultSyntax() string + + // GetDefaultLang returns the current value of the "default-lang" key. + GetDefaultLang() string // GetSiteName returns the current value of the "site-name" key. GetSiteName() string - // GetHTMLInsecurity returns the current - GetHTMLInsecurity() HTMLInsecurity + // GetHomeZettel returns the value of the "home-zettel" key. + GetHomeZettel() id.Zid - // GetMaxTransclusions returns the maximum number of indirect transclusions. - GetMaxTransclusions() int + // GetDefaultVisibility returns the default value for zettel visibility. + GetDefaultVisibility() meta.Visibility // GetYAMLHeader returns the current value of the "yaml-header" key. GetYAMLHeader() bool // GetZettelFileSyntax returns the current value of the "zettel-file-syntax" key. GetZettelFileSyntax() []string + + // GetMarkerExternal returns the current value of the "marker-external" key. + GetMarkerExternal() string + + // GetFooterHTML returns HTML code that should be embedded into the footer + // of each WebUI page. + GetFooterHTML() string } // AuthConfig are relevant configuration values for authentication. type AuthConfig interface { - // GetSimpleMode returns true if system tuns in simple-mode. - GetSimpleMode() bool - - // GetExpertMode returns the current value of the "expert-mode" key. + // GetExpertMode returns the current value of the "expert-mode" key GetExpertMode() bool // GetVisibility returns the visibility value of the metadata. GetVisibility(m *meta.Meta) meta.Visibility } -// HTMLInsecurity states what kind of insecure HTML is allowed. -// The lowest value is the most secure one (disallowing any HTML) -type HTMLInsecurity uint8 - -// Constant values for HTMLInsecurity: -const ( - NoHTML HTMLInsecurity = iota - SyntaxHTML - MarkdownHTML - ZettelmarkupHTML -) - -func (hi HTMLInsecurity) String() string { - switch hi { - case SyntaxHTML: - return "html" - case MarkdownHTML: - return "markdown" - case ZettelmarkupHTML: - return "zettelmarkup" - } - return "secure" -} - -// AllowHTML returns true, if the given HTML insecurity level matches the given syntax value. -func (hi HTMLInsecurity) AllowHTML(syntax string) bool { - switch hi { - case SyntaxHTML: - return syntax == meta.SyntaxHTML - case MarkdownHTML: - return syntax == meta.SyntaxHTML || syntax == meta.SyntaxMarkdown || syntax == meta.SyntaxMD - case ZettelmarkupHTML: - return syntax == meta.SyntaxZmk || syntax == meta.SyntaxHTML || - syntax == meta.SyntaxMarkdown || syntax == meta.SyntaxMD - } - return false +// GetTitle returns the value of the "title" key of the given meta. If there +// is no such value, GetDefaultTitle is returned. +func GetTitle(m *meta.Meta, cfg Config) string { + if val, ok := m.Get(meta.KeyTitle); ok { + return val + } + return cfg.GetDefaultTitle() +} + +// GetRole returns the value of the "role" key of the given meta. If there +// is no such value, GetDefaultRole is returned. +func GetRole(m *meta.Meta, cfg Config) string { + if val, ok := m.Get(meta.KeyRole); ok { + return val + } + return cfg.GetDefaultRole() +} + +// GetSyntax returns the value of the "syntax" key of the given meta. If there +// is no such value, GetDefaultSyntax is returned. +func GetSyntax(m *meta.Meta, cfg Config) string { + if val, ok := m.Get(meta.KeySyntax); ok { + return val + } + return cfg.GetDefaultSyntax() +} + +// GetLang returns the value of the "lang" key of the given meta. If there is +// no such value, GetDefaultLang is returned. +func GetLang(m *meta.Meta, cfg Config) string { + if val, ok := m.Get(meta.KeyLang); ok { + return val + } + return cfg.GetDefaultLang() } DELETED docs/development/00010000000000.zettel Index: docs/development/00010000000000.zettel ================================================================== --- docs/development/00010000000000.zettel +++ docs/development/00010000000000.zettel @@ -1,11 +0,0 @@ -id: 00010000000000 -title: Developments Notes -role: zettel -syntax: zmk -created: 00010101000000 -modified: 20231218182020 - -* [[Required Software|20210916193200]] -* [[Fuzzing tests|20221026184300]] -* [[Checklist for Release|20210916194900]] -* [[Development tools|20231218181900]] DELETED docs/development/20210916193200.zettel Index: docs/development/20210916193200.zettel ================================================================== --- docs/development/20210916193200.zettel +++ docs/development/20210916193200.zettel @@ -1,28 +0,0 @@ -id: 20210916193200 -title: Required Software -role: zettel -syntax: zmk -created: 20210916193200 -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/]], -* [[Git|https://git-scm.org/]] (most dependencies are accessible via Git only). - -Make sure that the software is in your path, e.g. via: -```sh -export PATH=$PATH:/usr/local/go/bin -export PATH=$PATH:$(go env GOPATH)/bin -``` - -The internal build tool need the following software. -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``, DELETED docs/development/20210916194900.zettel Index: docs/development/20210916194900.zettel ================================================================== --- docs/development/20210916194900.zettel +++ docs/development/20210916194900.zettel @@ -1,59 +0,0 @@ -id: 20210916194900 -title: Checklist for Release -role: zettel -syntax: zmk -created: 20210916194900 -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/clean/clean.go`` (alternatively: ``make clean``). -# All internal tests must succeed: -#* ``go run tools/check/check.go -r`` (alternatively: ``make relcheck``). -# The API tests must succeed on every development platform: -#* ``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''. -#* Try to resolve other error messages and warnings -#* Warnings about empty content can be ignored -# On every development platform, the box with 10.000 zettel must run, with ''-race'' enabled: -#* ``go run -race cmd/zettelstore/main.go run -d DIR``. -# Create a development release: -#* ``go run tools/build.go release`` (alternatively: ``make release``). -# On every platform (esp. macOS), the box with 10.000 zettel must run properly: -#* ``./zettelstore -d DIR`` -# Update files in directory ''www'' -#* index.wiki -#* download.wiki -#* changes.wiki -#* plan.wiki -# Set file ''VERSION'' to the new release version. - It _must_ consist of three digits: MAJOR.MINOR.PATCH, even if PATCH is zero -# Disable Fossil autosync mode: -#* ``fossil setting autosync off`` -# Commit the new release version: -#* ``fossil commit --tag release --tag vVERSION -m "Version VERSION"`` -#* **Important:** the tag must follow the given pattern, e.g. ''v0.0.15''. - Otherwise client will not be able to import ''zettelkasten.de/z''. -# Clean up your Go workspace: -#* ``go run tools/clean/clean.go`` (alternatively: ``make clean``). -# Create the 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`` -#* ``cd ..`` -#* Synchronize with main repository: -#* ``fossil sync -u`` -# Enable autosync: -#* ``fossil setting autosync on`` DELETED docs/development/20221026184300.zettel Index: docs/development/20221026184300.zettel ================================================================== --- docs/development/20221026184300.zettel +++ docs/development/20221026184300.zettel @@ -1,14 +0,0 @@ -id: 20221026184300 -title: Fuzzing Tests -role: zettel -syntax: zmk -created: 20221026184320 -modified: 20221102140156 - -The source code contains some simple [[fuzzing tests|https://go.dev/security/fuzz/]]. -You should call them regularly to make sure that the software will cope with unusual input. - -```sh -go test -fuzz=FuzzParseBlocks zettelstore.de/z/parser/draw -go test -fuzz=FuzzParseBlocks zettelstore.de/z/parser/zettelmark -``` DELETED docs/development/20231218181900.zettel Index: docs/development/20231218181900.zettel ================================================================== --- docs/development/20231218181900.zettel +++ docs/development/20231218181900.zettel @@ -1,117 +0,0 @@ -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/00000000000100.zettel ================================================================== --- docs/manual/00000000000100.zettel +++ docs/manual/00000000000100.zettel @@ -1,14 +1,13 @@ id: 00000000000100 title: Zettelstore Runtime Configuration role: configuration syntax: none -created: 00010101000000 -default-copyright: (c) 2020-present by Detlef Stern +default-copyright: (c) 2020-2021 by Detlef Stern default-license: EUPL-1.2-or-later default-visibility: public -footer-zettel: 00001000000100 +footer-html:

Imprint / Privacy

home-zettel: 00001000000000 -modified: 20221205173642 +no-index: true site-name: Zettelstore Manual visibility: owner DELETED docs/manual/00000000025001 Index: docs/manual/00000000025001 ================================================================== --- docs/manual/00000000025001 +++ docs/manual/00000000025001 @@ -1,7 +0,0 @@ -id: 00000000025001 -title: Zettelstore User CSS -role: configuration -syntax: css -created: 20210622110143 -modified: 20220926183101 -visibility: public DELETED docs/manual/00000000025001.css Index: docs/manual/00000000025001.css ================================================================== --- docs/manual/00000000025001.css +++ docs/manual/00000000025001.css @@ -1,2 +0,0 @@ -/* User-defined CSS */ -.example { border-style: dotted !important } Index: docs/manual/00001000000000.zettel ================================================================== --- docs/manual/00001000000000.zettel +++ docs/manual/00001000000000.zettel @@ -1,13 +1,10 @@ id: 00001000000000 title: Zettelstore Manual role: manual tags: #manual #zettelstore syntax: zmk -created: 20210301190630 -modified: 20231125185455 -show-back-links: false * [[Introduction|00001001000000]] * [[Design goals|00001002000000]] * [[Installation|00001003000000]] * [[Configuration|00001004000000]] @@ -16,12 +13,9 @@ * [[Zettelmarkup|00001007000000]] * [[Other markup languages|00001008000000]] * [[Security|00001010000000]] * [[API|00001012000000]] * [[Web user interface|00001014000000]] -* [[Tips and Tricks|00001017000000]] -* [[Troubleshooting|00001018000000]] +* Troubleshooting * Frequently asked questions -Version: {{00001000000001}}. - Licensed under the EUPL-1.2-or-later. DELETED docs/manual/00001000000001.zettel Index: docs/manual/00001000000001.zettel ================================================================== --- docs/manual/00001000000001.zettel +++ docs/manual/00001000000001.zettel @@ -1,8 +0,0 @@ -id: 00001000000001 -title: Manual Version -role: configuration -syntax: zmk -created: 20231002142915 -modified: 20231002142948 - -To be set by build tool. DELETED docs/manual/00001000000100.zettel Index: docs/manual/00001000000100.zettel ================================================================== --- docs/manual/00001000000100.zettel +++ docs/manual/00001000000100.zettel @@ -1,8 +0,0 @@ -id: 00001000000100 -title: Footer Zettel -role: configuration -syntax: zmk -created: 20221205173520 -modified: 20221207175927 - -[[Imprint / Privacy|/home/doc/trunk/www/impri.wiki]] Index: docs/manual/00001002000000.zettel ================================================================== --- docs/manual/00001002000000.zettel +++ docs/manual/00001002000000.zettel @@ -1,26 +1,20 @@ id: 00001002000000 title: Design goals for the Zettelstore -role: manual tags: #design #goal #manual #zettelstore syntax: zmk -created: 20210126175322 -modified: 20230624171152 +role: manual Zettelstore supports the following design goals: ; Longevity of stored notes / zettel : Every zettel you create should be readable without the help of any tool, even without Zettelstore. : It should be not hard to write other software that works with your zettel. -: Normal zettel should be stored in a single file. - If this is not possible: at most in two files: one for the metadata, one for the content. - The only exception are [[predefined zettel|00001005090000]] stored in the Zettelstore executable. -: There is no additional database. ; Single user : All zettel belong to you, only to you. Zettelstore provides its services only to one person: you. - If the computer running Zettelstore is securely configured, there should be no risk that others are able to read or update your zettel. + If your device is securely configured, there should be no risk that others are able to read or update your zettel. : If you want, you can customize Zettelstore in a way that some specific or all persons are able to read some of your zettel. ; Ease of installation : If you want to use the Zettelstore software, all you need is to copy the executable to an appropriate file directory and start working. : Upgrading the software is done just by replacing the executable with a newer one. ; Ease of operation @@ -28,16 +22,10 @@ : If you decide to use multiple directories, you are free to configure Zettelstore appropriately. ; Multiple modes of operation : You can use Zettelstore as a standalone software on your device, but you are not restricted to it. : You can install the software on a central server, or you can install it on all your devices with no restrictions how to synchronize your zettel. ; Multiple user interfaces -: Zettelstore provides a default [[web-based user interface|00001014000000]]. +: Zettelstore provides a default web-based user interface. Anybody can provide alternative user interfaces, e.g. for special purposes. ; Simple service : The purpose of Zettelstore is to safely store your zettel and to provide some initial relations between them. : External software can be written to deeply analyze your zettel and the structures they form. -; Security by default -: Without any customization, Zettelstore provides its services in a safe and secure manner and does not expose you (or other users) to security risks. -: If you know what use are doing, Zettelstore allows you to relax some security-related preferences. - However, even in this case, the more secure way is chosen. -: The Zettelstore software uses a minimal design and uses other software dependencies only is essential needed. -: There will be no plugin mechanism, which allows external software to control the inner workings of the Zettelstore software. Index: docs/manual/00001003000000.zettel ================================================================== --- docs/manual/00001003000000.zettel +++ docs/manual/00001003000000.zettel @@ -1,31 +1,72 @@ id: 00001003000000 title: Installation of the Zettelstore software role: manual tags: #installation #manual #zettelstore syntax: zmk -modified: 20220119145756 === The curious user You just want to check out the Zettelstore software * Grab the appropriate executable and copy it into any directory -* Start the Zettelstore software, e.g. with a double click[^On Windows and macOS, the operating system tries to protect you from possible malicious software. If you encounter problem, please take a look on the [[Troubleshooting|00001018000000]] page.] +* Start the Zettelstore software, e.g. with a double click * A sub-directory ""zettel"" will be created in the directory where you put the executable. It will contain your future zettel. * Open the URI [[http://localhost:23123]] with your web browser. It will present you a mostly empty Zettelstore. There will be a zettel titled ""[[Home|00010000000000]]"" that contains some helpful information. -* Please read the instructions for the [[web-based user interface|00001014000000]] and learn about the various ways to write zettel. +* Please read the instructions for the web-based user interface and learn about the various ways to write zettel. * If you restart your device, please make sure to start your Zettelstore again. === The intermediate user You already tried the Zettelstore software and now you want to use it permanently. -Zettelstore should start automatically when you log into your computer. -Please follow [[these instructions|00001003300000]]. +* Grab the appropriate executable and copy it into the appropriate directory +* ... === The server administrator You want to provide a shared Zettelstore that can be used from your various devices. Installing Zettelstore as a Linux service is not that hard. -Please follow [[these instructions|00001003600000]]. +Grab the appropriate executable and copy it into the appropriate directory: +```sh +# sudo mv zettelstore /usr/local/bin/zettelstore +``` +Create a group named ''zettelstore'': +```sh +# sudo groupadd --system zettelstore +``` +Create a system user of that group, named ''zettelstore'', with a home folder: +```sh +# sudo useradd --system --gid zettelstore \ + --create-home --home-dir /var/lib/zettelstore \ + --shell /usr/sbin/nologin \ + --comment "Zettelstore server" \ + zettelstore +``` +Create a systemd service file and store it into ''/etc/systemd/system/zettelstore.service'': +```ini +[Unit] +Description=Zettelstore +After=network.target + +[Service] +Type=simple +User=zettelstore +Group=zettelstore +ExecStart=/usr/local/bin/zettelstore run -d /var/lib/zettelstore +WorkingDirectory=/var/lib/zettelstore + +[Install] +WantedBy=multi-user.target +``` +Double-check everything. Now you can enable and start the zettelstore as a service: +```sh +# sudo systemctl daemon-reload +# sudo systemctl enable zettelstore +# sudo systemctl start zettelstore +``` +Use the commands ``systemctl``{=sh} and ``journalctl``{=sh} to manage the service, e.g.: +```sh +# sudo systemctl status zettelstore # verify that it is running +# sudo journalctl -u zettelstore # obtain the output of the running zettelstore +``` DELETED docs/manual/00001003300000.zettel Index: docs/manual/00001003300000.zettel ================================================================== --- docs/manual/00001003300000.zettel +++ docs/manual/00001003300000.zettel @@ -1,26 +0,0 @@ -id: 00001003300000 -title: Zettelstore installation for the intermediate user -role: manual -tags: #installation #manual #zettelstore -syntax: zmk -modified: 20220114175754 - -You already tried the Zettelstore software and now you want to use it permanently. -Zettelstore should start automatically when you log into your computer. - -* Grab the appropriate executable and copy it into the appropriate directory -* If you want to place your zettel into another directory, or if you want more than one [[Zettelstore box|00001004011200]], or if you want to [[enable authentication|00001010040100]], or if you want to tweak your Zettelstore in some other way, create an appropriate [[startup configuration file|00001004010000]]. -* If you created a startup configuration file, you need to test it: -** Start a command line prompt for your operating system. -** Navigate to the directory, where you placed the Zettelstore executable. - In most cases, this is done by the command ``cd DIR``, where ''DIR'' denotes the directory, where you placed the executable. -** Start the Zettelstore: -*** On Windows execute the command ``zettelstore.exe run -c CONFIG_FILE`` -*** On macOS execute the command ``./zettelstore run -c CONFIG_FILE`` -*** On Linux execute the command ``./zettelstore run -c CONFIG_FILE`` -** In all cases ''CONFIG_FILE'' must be substituted by file name where you wrote the startup configuration. -** If you encounter some error messages, update the startup configuration, and try again. -* Depending on your operating system, there are different ways to register Zettelstore to start automatically: -** [[Windows|00001003305000]] -** [[macOS|00001003310000]] -** [[Linux|00001003315000]] DELETED docs/manual/00001003305000.zettel Index: docs/manual/00001003305000.zettel ================================================================== --- docs/manual/00001003305000.zettel +++ docs/manual/00001003305000.zettel @@ -1,119 +0,0 @@ -id: 00001003305000 -title: Enable Zettelstore to start automatically on Windows -role: manual -tags: #installation #manual #zettelstore -syntax: zmk -modified: 20220218125541 - -Windows is a complicated beast. There are several ways to automatically start Zettelstore. - -=== Startup folder - -One way is to use the [[autostart folder|https://support.microsoft.com/en-us/windows/add-an-app-to-run-automatically-at-startup-in-windows-10-150da165-dcd9-7230-517b-cf3c295d89dd]]. -Open the folder where you have placed in the Explorer. -Create a shortcut file for the Zettelstore executable. -There are some ways to do this: -* Execute a right-click on the executable, and choose the menu entry ""Create shortcut"", -* Execute a right-click on the executable, and then click Send To > Desktop (Create shortcut). -* Drag the executable to your Desktop with pressing the ''Alt''-Key. - -If you have created the shortcut file, you must move it into the Startup folder. -Press the Windows logo key and the key ''R'', type ''shell:startup''. -Select the OK button. -This will open the Startup folder. -Move the shortcut file into this folder. - -The next time you log into your computer, Zettelstore will be started automatically. -However, it remains visible, at least in the task bar. - -You can modify the behavior by changing some properties of the shortcut file. - -=== Task scheduler - -The Windows Task scheduler allows you to start Zettelstore as an background task. - -This is both an advantage and a disadvantage. - -On the plus side, Zettelstore runs in the background, and it does not disturbs you. -All you have to do is to open your web browser, enter the appropriate URL, and there you go. - -On the negative side, you will not be notified when you enter the wrong data in the Task scheduler and Zettelstore fails to start. -This can be mitigated by first using the command line prompt to start Zettelstore with the appropriate options. -Once everything works, you can register Zettelstore to be automatically started by the task scheduler. -There you should make sure that you have followed the first steps as described on the [[parent page|00001003300000]]. - -To start the Task scheduler management console, press the Windows logo key and the key ''R'', type ''taskschd.msc''. -Select the OK button. - -{{00001003305102}} - -This will start the ""Task Scheduler"". - -Now, create a new task with ""Create Task ..."" - -{{00001003305104}} - -Enter a name for the task, e.g. ""Zettelstore"" and select the options ""Run whether user is logged in or not"" and ""Do not store password."" - -{{00001003305106}} - -Create a new trigger. - -{{00001003305108}} - -Select the option ""At startup"". - -{{00001003305110}} - -Create a new action. - -{{00001003305112}} - -The next steps are the trickiest. - -If you did not created a startup configuration file, then create an action that starts a program. -Enter the file path where you placed the Zettelstore executable. -The ""Browse ..."" button helps you with that.[^I store my Zettelstore executable in the sub-directory ''bin'' of my home directory.] - -It is essential that you also enter a directory, which serves as the environment for your zettelstore. -The (sub-) directory ''zettel'', which will contain your zettel, will be placed in this directory. -If you leave the field ""Start in (optional)"" empty, the directory will be an internal Windows system directory (most likely: ''C:\\Windows\\System32''). - -If you press the OK button, the ""Create Task"" tab shows up as on the right image. - -{{00001003305114}}\ {{00001003305116}} - -If you have created a startup configuration file, you must enter something into the field ""Add arguments (optional)"". -Unfortunately, the text box is too narrow to fully see its content. - -I have entered the string ''run -c "C:\\Users\\Detlef Stern\\bin\\zsconfig.txt"'', because my startup configuration file has the name ''zsconfig.txt'' and I placed it into the same folder that also contains the Zettelstore executable. -Maybe you have to adapt to this. - -You must also enter appropriate data for the other form fields. -If you press the OK button, the ""Create Task"" tab shows up as on the right image. - -{{00001003305118}}\ {{00001003305120}} - -You should disable any additional conditions, since you typically want to use Zettelstore unconditionally. -Especially, make sure that ""Start the task only if the computer is on AC power"" is disabled. -Otherwise Zettelstore will not start if you run on battery power. - -{{00001003305122}} - -On the ""Settings"" tab, you should disable the option ""Stop the task if it runs longer than:"". - -{{00001003305124}} - -After entering the data, press the OK button. -Under some circumstances, Windows asks for permission and you have to enter your password. - -As the last step, you could run the freshly created task manually. - -Open your browser, enter the appropriate URL and use your Zettelstore. -In case of errors, the task will most likely stop immediately. -Make sure that all data you have entered is valid. -To not forget to check the content of the startup configuration file. -Use the command prompt to debug your configuration. - -Sometimes, for example when your computer was in stand-by and it wakes up, these tasks are not started. -In this case execute the task scheduler and run the task manually. DELETED docs/manual/00001003305102.png Index: docs/manual/00001003305102.png ================================================================== --- docs/manual/00001003305102.png +++ docs/manual/00001003305102.png cannot compute difference between binary files DELETED docs/manual/00001003305104.png Index: docs/manual/00001003305104.png ================================================================== --- docs/manual/00001003305104.png +++ docs/manual/00001003305104.png cannot compute difference between binary files DELETED docs/manual/00001003305106.png Index: docs/manual/00001003305106.png ================================================================== --- docs/manual/00001003305106.png +++ docs/manual/00001003305106.png cannot compute difference between binary files DELETED docs/manual/00001003305108.png Index: docs/manual/00001003305108.png ================================================================== --- docs/manual/00001003305108.png +++ docs/manual/00001003305108.png cannot compute difference between binary files DELETED docs/manual/00001003305110.png Index: docs/manual/00001003305110.png ================================================================== --- docs/manual/00001003305110.png +++ docs/manual/00001003305110.png cannot compute difference between binary files DELETED docs/manual/00001003305112.png Index: docs/manual/00001003305112.png ================================================================== --- docs/manual/00001003305112.png +++ docs/manual/00001003305112.png cannot compute difference between binary files DELETED docs/manual/00001003305114.png Index: docs/manual/00001003305114.png ================================================================== --- docs/manual/00001003305114.png +++ docs/manual/00001003305114.png cannot compute difference between binary files DELETED docs/manual/00001003305116.png Index: docs/manual/00001003305116.png ================================================================== --- docs/manual/00001003305116.png +++ docs/manual/00001003305116.png cannot compute difference between binary files DELETED docs/manual/00001003305118.png Index: docs/manual/00001003305118.png ================================================================== --- docs/manual/00001003305118.png +++ docs/manual/00001003305118.png cannot compute difference between binary files DELETED docs/manual/00001003305120.png Index: docs/manual/00001003305120.png ================================================================== --- docs/manual/00001003305120.png +++ docs/manual/00001003305120.png cannot compute difference between binary files DELETED docs/manual/00001003305122.png Index: docs/manual/00001003305122.png ================================================================== --- docs/manual/00001003305122.png +++ docs/manual/00001003305122.png cannot compute difference between binary files DELETED docs/manual/00001003305124.png Index: docs/manual/00001003305124.png ================================================================== --- docs/manual/00001003305124.png +++ docs/manual/00001003305124.png cannot compute difference between binary files DELETED docs/manual/00001003310000.zettel Index: docs/manual/00001003310000.zettel ================================================================== --- docs/manual/00001003310000.zettel +++ docs/manual/00001003310000.zettel @@ -1,94 +0,0 @@ -id: 00001003310000 -title: Enable Zettelstore to start automatically on macOS -role: manual -tags: #installation #manual #zettelstore -syntax: zmk -modified: 20220119124635 - -There are several ways to automatically start Zettelstore. - -* [[Login Items|#login-items]] -* [[Launch Agent|#launch-agent]] - -=== Login Items - -Via macOS's system preferences, everybody is able to specify executables that are started when a user is logged in. -To do this, start system preferences and select ""Users & Groups"". - -{{00001003310104}} - -In the next screen, select the current user and then click on ""Login Items"". - -{{00001003310106}} - -Click on the plus sign at the bottom and select the Zettelstore executable. - -{{00001003310108}} - -Optionally select the ""Hide"" check box. - -{{00001003310110}} - -The next time you log into your macOS computer, Zettelstore will be started automatically. - -Unfortunately, hiding the Zettelstore windows does not always work. -Therefore, this method is just a way to automate navigating to the directory where the Zettelstore executable is placed and to click on that icon. - -If you don't want the Zettelstore window, you should try the next method. - -=== Launch Agent - -If you want to execute Zettelstore automatically and less visible, and if you know a little bit about working in the terminal application, then you could try to run Zettelstore under the control of the [[Launchd system|https://developer.apple.com/library/archive/documentation/MacOSX/Conceptual/BPSystemStartup/Chapters/Introduction.html]]. - -First, you have to create a description for ""Launchd"". -This is a text file named ''zettelstore.plist'' with the following content. -It assumes that you have copied the Zettelstore executable in a local folder called ''~/bin'' and have created a file for [[startup configuration|00001004010000]] called ''zettelstore.cfg'', which is placed in the same folder[^If you are not using a configuration file, just remove the lines ``-c`` and ``/Users/USERNAME/bin/zettelstore.cfg``.]: - -``` - - - - - Label - de.zettelstore - - ProgramArguments - - /Users/USERNAME/bin/zettelstore - run - -c - /Users/USERNAME/bin/zettelstore.cfg - - - WorkingDirectory - /Users/USERNAME - - EnvironmentVariables - - HOME - /Users/USERNAME - - - KeepAlive - - - LowPriorityIO - - - ProcessType - Background - - StandardOutPath - /Users/USERNAME/Library/Logs/Zettelstore.log - - StandardErrorPath - /Users/USERNAME/Library/Logs/Zettelstore-Errors.log - - -``` - -You must substitute all occurrences of ''USERNAME'' with your user name. - -Place this file into the user specific folder ''~/Library/LaunchAgents''. - -Log out and in again, or execute the command ``launchctl load ~/Library/LaunchAgents/zettelstore.plist``. DELETED docs/manual/00001003310104.png Index: docs/manual/00001003310104.png ================================================================== --- docs/manual/00001003310104.png +++ docs/manual/00001003310104.png cannot compute difference between binary files DELETED docs/manual/00001003310106.png Index: docs/manual/00001003310106.png ================================================================== --- docs/manual/00001003310106.png +++ docs/manual/00001003310106.png cannot compute difference between binary files DELETED docs/manual/00001003310108.png Index: docs/manual/00001003310108.png ================================================================== --- docs/manual/00001003310108.png +++ docs/manual/00001003310108.png cannot compute difference between binary files DELETED docs/manual/00001003310110.png Index: docs/manual/00001003310110.png ================================================================== --- docs/manual/00001003310110.png +++ docs/manual/00001003310110.png cannot compute difference between binary files DELETED docs/manual/00001003315000.zettel Index: docs/manual/00001003315000.zettel ================================================================== --- docs/manual/00001003315000.zettel +++ docs/manual/00001003315000.zettel @@ -1,42 +0,0 @@ -id: 00001003315000 -title: Enable Zettelstore to start automatically on Linux -role: manual -tags: #installation #manual #zettelstore -syntax: zmk -modified: 20220307104944 - -Since there is no such thing as the one Linux, there are too many different ways to automatically start Zettelstore. - -* One way is to interpret your Linux desktop system as a server and use the [[recipe to install Zettelstore on a server|00001003600000]]. -** See below for a lighter alternative. -* If you are using the [[Gnome Desktop|https://www.gnome.org/]], you could use the tool [[Tweak|https://wiki.gnome.org/action/show/Apps/Tweaks]] (formerly known as ""GNOME Tweak Tool"" or just ""Tweak Tool""). - It allows to specify application that should run on startup / login. -* [[KDE|https://kde.org/]] provides a system setting to [[autostart|https://docs.kde.org/stable5/en/plasma-workspace/kcontrol/autostart/]] applications. -* [[Xfce|https://xfce.org/]] allows to specify [[autostart applications|https://docs.xfce.org/xfce/xfce4-session/preferences#application_autostart]]. -* [[LXDE|https://www.lxde.org/]] uses [[LXSession Edit|https://wiki.lxde.org/en/LXSession_Edit]] to allow users to specify autostart applications. - -If you use a different desktop environment, it often helps to to provide its name and the string ""autostart"" to google for it with the search engine of your choice. - -Yet another way is to make use of the middleware that is provided. -Many Linux distributions make use of [[systemd|https://systemd.io/]], which allows to start processes on behalf of an user. -On the command line, adapt the following script to your own needs and execute it: -``` -# mkdir -p "$HOME/.config/systemd/user" -# cd "$HOME/.config/systemd/user" -# cat <<__EOF__ > zettelstore.service -[Unit] -Description=Zettelstore -After=network.target home.mount - -[Service] -ExecStart=/usr/local/bin/zettelstore run -d zettel - -[Install] -WantedBy=default.target -__EOF__ -# systemctl --user daemon-reload -# systemctl --user enable zettelstore.service -# systemctl --user start zettelstore.service -# systemctl --user status zettelstore.service -``` -The last command should output some lines to indicate success. DELETED docs/manual/00001003600000.zettel Index: docs/manual/00001003600000.zettel ================================================================== --- docs/manual/00001003600000.zettel +++ docs/manual/00001003600000.zettel @@ -1,53 +0,0 @@ -id: 00001003600000 -title: Installation of Zettelstore on a server -role: manual -tags: #installation #manual #zettelstore -syntax: zmk -modified: 20211125185833 - -You want to provide a shared Zettelstore that can be used from your various devices. -Installing Zettelstore as a Linux service is not that hard. - -Grab the appropriate executable and copy it into the appropriate directory: -```sh -# sudo mv zettelstore /usr/local/bin/zettelstore -``` -Create a group named ''zettelstore'': -```sh -# sudo groupadd --system zettelstore -``` -Create a system user of that group, named ''zettelstore'', with a home folder: -```sh -# sudo useradd --system --gid zettelstore \ - --create-home --home-dir /var/lib/zettelstore \ - --shell /usr/sbin/nologin \ - --comment "Zettelstore server" \ - zettelstore -``` -Create a systemd service file and store it into ''/etc/systemd/system/zettelstore.service'': -```ini -[Unit] -Description=Zettelstore -After=network.target - -[Service] -Type=simple -User=zettelstore -Group=zettelstore -ExecStart=/usr/local/bin/zettelstore run -d /var/lib/zettelstore -WorkingDirectory=/var/lib/zettelstore - -[Install] -WantedBy=multi-user.target -``` -Double-check everything. Now you can enable and start the zettelstore as a service: -```sh -# sudo systemctl daemon-reload -# sudo systemctl enable zettelstore -# sudo systemctl start zettelstore -``` -Use the commands ``systemctl``{=sh} and ``journalctl``{=sh} to manage the service, e.g.: -```sh -# sudo systemctl status zettelstore # verify that it is running -# sudo journalctl -u zettelstore # obtain the output of the running zettelstore -``` Index: docs/manual/00001004010000.zettel ================================================================== --- docs/manual/00001004010000.zettel +++ docs/manual/00001004010000.zettel @@ -1,12 +1,11 @@ id: 00001004010000 title: Zettelstore startup configuration role: manual tags: #configuration #manual #zettelstore syntax: zmk -created: 20210126175322 -modified: 20240220190138 +modified: 20210712234656 The configuration file, as specified by the ''-c CONFIGFILE'' [[command line option|00001004051000]], allows you to specify some startup options. These options cannot be stored in a [[configuration zettel|00001004020000]] because either they are needed before Zettelstore can start or because of security reasons. For example, Zettelstore need to know in advance, on which network address is must listen or where zettel are stored. An attacker that is able to change the owner can do anything. @@ -15,138 +14,72 @@ The file for startup configuration must be created via a text editor in advance. The syntax of the configuration file is the same as for any zettel metadata. The following keys are supported: -; [!admin-port|''admin-port''] +; [!admin-port]''admin-port'' : Specifies the TCP port through which you can reach the [[administrator console|00001004100000]]. - A value of ""0"" (the default) disables the administrator console. + A value of ''0'' (the default) disables the administrator console. The administrator console will only be enabled if Zettelstore is started with the [[''run'' sub-command|00001004051000]]. - On most operating systems, the value must be greater than ""1024"" unless you start Zettelstore with the full privileges of a system administrator (which is not recommended). - - Default: ""0"" -; [!asset-dir|''asset-dir''] -: Allows to specify a directory whose files are allowed be transferred directly with the help of the web server. - The URL prefix for these files is ''/assets/''. - You can use this if you want to transfer files that are too large for a note to users. - Examples would be presentation files, PDF files, music files or video files. - - Files within the given directory will not be managed by Zettelstore.[^They will be managed by Zettelstore just in the case that the directory is one of the configured [[boxes|#box-uri-x]].] - - If you specify only the URL prefix, then the contents of the directory are listed to the user. - To avoid this, create an empty file in the directory named ""index.html"". - - Default: """", no asset directory is set, the URL prefix ''/assets/'' is invalid. -; [!base-url|''base-url''] -: Sets the absolute base URL for the service. - - Note: [[''url-prefix''|#url-prefix]] must be the suffix of ''base-url'', otherwise the web service will not start. - - Default: ""http://127.0.0.1:23123/"". -; [!box-uri-x|''box-uri-X''], where __X__ is a number greater or equal to one + On most operating systems, the value must be greater than ''1024'' unless you start Zettelstore with the full privileges of a system administrator (which is not recommended). + + Default: ''0'' +; [!box-uri-x]''box-uri-//X//'', where //X// is a number greater or equal to one : Specifies a [[box|00001004011200]] where zettel are stored. - During startup __X__ is counted up, starting with one, until no key is found. + During startup //X// is counted up, starting with one, until no key is found. This allows to configure more than one box. - If no ''box-uri-1'' key is given, the overall effect will be the same as if only ''box-uri-1'' was specified with the value ""dir://.zettel"". + If no ''box-uri-1'' key is given, the overall effect will be the same as if only ''box-uri-1'' was specified with the value ''dir://.zettel''. In this case, even a key ''box-uri-2'' will be ignored. -; [!debug-mode|''debug-mode''] -: Allows to debug the Zettelstore software (mostly used by the developers) if set to [[true|00001006030500]] - Disables any timeout values of the internal web server and does not send some security-related data. - Sets [[''log-level''|#log-level]] to ""debug"". - - Do not enable it for a production server. - - Default: ""false"" -; [!default-dir-box-type|''default-dir-box-type''] +; [!default-dir-box-type]''default-dir-box-type'' : Specifies the default value for the (sub-) type of [[directory boxes|00001004011400#type]]. Zettel are typically stored in such boxes. - Default: ""notify"" -; [!insecure-cookie|''insecure-cookie''] -: Must be set to [[true|00001006030500]], if authentication is enabled and Zettelstore is not accessible not via HTTPS (but via HTTP). + Default: ''notify'' +; [!insecure-cookie]''insecure-cookie'' +: Must be set to ''true'', if authentication is enabled and Zettelstore is not accessible not via HTTPS (but via HTTP). Otherwise web browser are free to ignore the authentication cookie. - Default: ""false"" -; [!insecure-html|''insecure-html''] -: Allows to use HTML, e.g. within supported markup languages, even if this might introduce security-related problems. - However, HTML containing the `` -<script>alert('1');</script> - - -"> -'> -> - -< / script >< script >alert(8)< / script > - onfocus=JaVaSCript:alert(9) autofocus -" onfocus=JaVaSCript:alert(10) autofocus -' onfocus=JaVaSCript:alert(11) autofocus -<script>alert(12)</script> -ript>alert(13)ript> ---> -";alert(15);t=" -';alert(16);t=' -JavaSCript:alert(17) -;alert(18); -src=JaVaSCript:prompt(19) -">javascript:alert(25); -javascript:alert(26); -javascript:alert(27); -javascript:alert(28); -javascript:alert(29); -javascript:alert(30); -javascript:alert(31); -'`"><\x3Cscript>javascript:alert(32) -'`"><\x00script>javascript:alert(33) -ABC
DEF -ABC
DEF -ABC
DEF -ABC
DEF -ABC
DEF -ABC
DEF -ABC
DEF -ABC
DEF -ABC
DEF -ABC
DEF -ABC
DEF -ABC
DEF -ABC
DEF -ABC
DEF -ABC
DEF -ABC
DEF -ABC
DEF -ABC
DEF -ABC
DEF -ABC
DEF -ABC
DEF -ABC
DEF -ABC
DEF -ABC
DEF -ABC
DEF -ABC
DEF -ABC
DEF -test -test -test -test -test -test -test -test -test -test -test -test -test -test -test -test -test -test -test -test -test -test -test -test -test -test -test -test -test -test -test -test -test -test -test -test -test -test -test -test -test -test -test -test -test -test -test -test -test -test -test -test -test -test -test -test -test -`"'> -`"'> -`"'> -`"'> -`"'> -`"'> -`"'> -`"'> -`"'> -`"'> -"`'> -"`'> -"`'> -"`'> -"`'> -"`'> -"`'> -"`'> -"`'> -"`'> -"`'> -"`'> -"`'> -"`'> -"`'> -"`'> -"`'> -"`'> -"`'> -"`'> -"`'> -"`'> -"`'> -"`'> -"`'> -"`'> -"`'> -"`'> -"`'> -"`'> -"`'> -"`'> -"`'> -"`'> -"`'> -"`'> -"`'> - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -XXX - - - -<a href=http://foo.bar/#x=`y></a><img alt="`><img src=x:x onerror=javascript:alert(203)></a>"> -<!--[if]><script>javascript:alert(204)</script --> -<!--[if<img src=x onerror=javascript:alert(205)//]> --> -<script src="/\%(jscript)s"></script> -<script src="\\%(jscript)s"></script> -<IMG """><SCRIPT>alert("206")</SCRIPT>"> -<IMG SRC=javascript:alert(String.fromCharCode(50,48,55))> -<IMG SRC=# onmouseover="alert('208')"> -<IMG SRC= onmouseover="alert('209')"> -<IMG onmouseover="alert('210')"> -<IMG SRC=javascript:alert('211')> -<IMG SRC=javascript:alert('212')> -<IMG SRC=javascript:alert('213')> -<IMG SRC="jav   ascript:alert('214');"> -<IMG SRC="jav ascript:alert('215');"> -<IMG SRC="jav ascript:alert('216');"> -<IMG SRC="jav ascript:alert('217');"> -perl -e 'print "<IMG SRC=java\0script:alert(\"218\")>";' > out -<IMG SRC="   javascript:alert('219');"> -<SCRIPT/XSS SRC="http://ha.ckers.org/xss.js"></SCRIPT> -<BODY onload!#$%&()*~+-_.,:;?@[/|\]^`=alert("220")> -<SCRIPT/SRC="http://ha.ckers.org/xss.js"></SCRIPT> -<<SCRIPT>alert("221");//<</SCRIPT> -<SCRIPT SRC=http://ha.ckers.org/xss.js?< B > -<SCRIPT SRC=//ha.ckers.org/.j> -<IMG SRC="javascript:alert('222')" -<iframe src=http://ha.ckers.org/scriptlet.html < -\";alert('223');// -<u oncopy=alert()> Copy me</u> -<i onwheel=alert(224)> Scroll over me </i> -<plaintext> -http://a/%%30%30 -</textarea><script>alert(225)</script> - -# SQL Injection -# -# Strings which can cause a SQL injection if inputs are not sanitized - -1;DROP TABLE users -1'; DROP TABLE users-- 1 -' OR 1=1 -- 1 -' OR '1'='1 -'; EXEC sp_MSForEachTable 'DROP TABLE ?'; -- - -% -_ - -# Server Code Injection -# -# Strings which can cause user to run code on server as a privileged user (c.f. https://news.ycombinator.com/item?id=7665153) - -- --- ---version ---help -$USER -/dev/null; touch /tmp/blns.fail ; echo -`touch /tmp/blns.fail` -$(touch /tmp/blns.fail) -@{[system "touch /tmp/blns.fail"]} - -# Command Injection (Ruby) -# -# Strings which can call system commands within Ruby/Rails applications - -eval("puts 'hello world'") -System("ls -al /") -`ls -al /` -Kernel.exec("ls -al /") -Kernel.exit(1) -%x('ls -al /') - -# XXE Injection (XML) -# -# String which can reveal system files when parsed by a badly configured XML parser - -<?xml version="1.0" encoding="ISO-8859-1"?><!DOCTYPE foo [ <!ELEMENT foo ANY ><!ENTITY xxe SYSTEM "file:///etc/passwd" >]><foo>&xxe;</foo> - -# Unwanted Interpolation -# -# Strings which can be accidentally expanded into different strings if evaluated in the wrong context, e.g. used as a printf format string or via Perl or shell eval. Might expose sensitive data from the program doing the interpolation, or might just represent the wrong string. - -$HOME -$ENV{'HOME'} -%d -%s%s%s%s%s -{0} -%*.*s -%@ -%n -File:/// - -# File Inclusion -# -# Strings which can cause user to pull in files that should not be a part of a web server - -../../../../../../../../../../../etc/passwd%00 -../../../../../../../../../../../etc/hosts - -# Known CVEs and Vulnerabilities -# -# Strings that test for known vulnerabilities - -() { 0; }; touch /tmp/blns.shellshock1.fail; -() { _; } >_[$($())] { touch /tmp/blns.shellshock2.fail; } -<<< %s(un='%s') = %u -+++ATH0 - -# MSDOS/Windows Special Filenames -# -# Strings which are reserved characters in MSDOS/Windows - -CON -PRN -AUX -CLOCK$ -NUL -A: -ZZ: -COM1 -LPT1 -LPT2 -LPT3 -COM2 -COM3 -COM4 - -# IRC specific strings -# -# Strings that may occur on IRC clients that make security products freak out - -DCC SEND STARTKEYLOGGER 0 0 0 - -# Scunthorpe Problem -# -# Innocuous strings which may be blocked by profanity filters (https://en.wikipedia.org/wiki/Scunthorpe_problem) - -Scunthorpe General Hospital -Penistone Community Church -Lightwater Country Park -Jimmy Clitheroe -Horniman Museum -shitake mushrooms -RomansInSussex.co.uk -http://www.cum.qc.ca/ -Craig Cockburn, Software Specialist -Linda Callahan -Dr. Herman I. Libshitz -magna cum laude -Super Bowl XXX -medieval erection of parapets -evaluate -mocha -expression -Arsenal canal -classic -Tyson Gay -Dick Van Dyke -basement - -# Human injection -# -# Strings which may cause human to reinterpret worldview - -If you're reading this, you've been in a coma for almost 20 years now. We're trying a new technique. We don't know where this message will end up in your dream, but we hope it works. Please wake up, we miss you. - -# Terminal escape codes -# -# Strings which punish the fools who use cat/type on this file - -Roses are red, violets are blue. Hope you enjoy terminal hue -But now...for my greatest trick... -The quick brown fox... [Beeeep] - -# iOS Vulnerabilities -# -# Strings which crashed iMessage in various versions of iOS - -Powerلُلُصّبُلُلصّبُررً ॣ ॣh ॣ ॣ冗 -🏳0🌈️ -జ్ఞ‌ా - -# Persian special characters -# -# This is a four characters string which includes Persian special characters (گچپژ) - -گچپژ - -# jinja2 injection -# -# first one is supposed to raise "MemoryError" exception -# second, obviously, prints contents of /etc/passwd - -{% print 'x' * 64 * 1024**3 %} -{{ "".__class__.__mro__[2].__subclasses__()[40]("/etc/passwd").read() }} Index: testdata/testbox/00000000000100.zettel ================================================================== --- testdata/testbox/00000000000100.zettel +++ testdata/testbox/00000000000100.zettel @@ -1,8 +1,9 @@ id: 00000000000100 title: Zettelstore Runtime Configuration role: configuration syntax: none expert-mode: true -modified: 20220215171142 +modified: 20210629174242 +no-index: true visibility: owner Index: testdata/testbox/19700101000000.zettel ================================================================== --- testdata/testbox/19700101000000.zettel +++ testdata/testbox/19700101000000.zettel @@ -5,8 +5,7 @@ syntax: none box-uri-1: mem: box-uri-2: dir:testdata/testbox?readonly modified: 20210629174022 owner: 20210629163300 -secret: 1234567890123456 token-lifetime-api: 1 visibility: owner Index: testdata/testbox/20210629163300.zettel ================================================================== --- testdata/testbox/20210629163300.zettel +++ testdata/testbox/20210629163300.zettel @@ -1,9 +1,8 @@ id: 20210629163300 -title: Owena +title: owner role: user tags: #test #user syntax: none credential: $2a$10$gcKyVmQ50fwgpOjyiiCm4eba/ILrNXoxTUCopgTEnYTa4yuceHMC6 -modified: 20211220131749 +modified: 20210629173617 user-id: owner - Index: testdata/testbox/20210629165000.zettel ================================================================== --- testdata/testbox/20210629165000.zettel +++ testdata/testbox/20210629165000.zettel @@ -1,10 +1,9 @@ id: 20210629165000 -title: Woody +title: writer role: user tags: #test #user syntax: none credential: $2a$10$VmHPyXa0Bm8DE4MJ.pQnbuuQmweWtyGya0L/bFA4nIuCn1EvPQflK -modified: 20211220132007 +modified: 20210629173536 user-id: writer user-role: writer - Index: testdata/testbox/20210629165024.zettel ================================================================== --- testdata/testbox/20210629165024.zettel +++ testdata/testbox/20210629165024.zettel @@ -1,10 +1,9 @@ id: 20210629165024 -title: Reanna +title: reader role: user tags: #test #user syntax: none credential: $2a$10$uC7LV2JdFhasw2HqSWZbSOihvFpwtaEXjXp98yzGfE3FHudq.vg.u -modified: 20211220131906 +modified: 20210629173459 user-id: reader user-role: reader - Index: testdata/testbox/20210629165050.zettel ================================================================== --- testdata/testbox/20210629165050.zettel +++ testdata/testbox/20210629165050.zettel @@ -1,10 +1,9 @@ id: 20210629165050 -title: Creighton +title: creator role: user tags: #test #user syntax: none credential: $2a$10$z85253tqhbHlXPZpt0hJpughLR4WXY8iYJbm1LlBhrKsL1YfkRy2q -modified: 20211220131520 +modified: 20210629173424 user-id: creator user-role: creator - DELETED testdata/testbox/20211019200500.zettel Index: testdata/testbox/20211019200500.zettel ================================================================== --- testdata/testbox/20211019200500.zettel +++ testdata/testbox/20211019200500.zettel @@ -1,10 +0,0 @@ -id: 20211019200500 -title: Collection of all users -role: zettel -syntax: zmk -modified: 20211220132017 - -* [[Owena|20210629163300]] -* [[Woody|20210629165000]] -* [[Reanna|20210629165024]] -* [[Creighton|20210629165050]] DELETED testdata/testbox/20211020121000.zettel Index: testdata/testbox/20211020121000.zettel ================================================================== --- testdata/testbox/20211020121000.zettel +++ testdata/testbox/20211020121000.zettel @@ -1,8 +0,0 @@ -id: 20211020121000 -title: ABC -role: zettel -syntax: zmk -modified: 20211020191331 -visibility: owner - -Ab C DELETED testdata/testbox/20211020121100.zettel Index: testdata/testbox/20211020121100.zettel ================================================================== --- testdata/testbox/20211020121100.zettel +++ testdata/testbox/20211020121100.zettel @@ -1,6 +0,0 @@ -id: 20211020121100 -title: 10*ABC -role: zettel -syntax: zmk - -{{20211020121000}}{{20211020121000}}{{20211020121000}}{{20211020121000}}{{20211020121000}}{{20211020121000}}{{20211020121000}}{{20211020121000}}{{20211020121000}}{{20211020121000}} DELETED testdata/testbox/20211020121145.zettel Index: testdata/testbox/20211020121145.zettel ================================================================== --- testdata/testbox/20211020121145.zettel +++ testdata/testbox/20211020121145.zettel @@ -1,6 +0,0 @@ -id: 20211020121145 -title: 10*10*ABC -role: zettel -syntax: zmk - -{{20211020121100}}{{20211020121100}}{{20211020121100}}{{20211020121100}}{{20211020121100}}{{20211020121100}}{{20211020121100}}{{20211020121100}}{{20211020121100}}{{20211020121100}} DELETED testdata/testbox/20211020121300.zettel Index: testdata/testbox/20211020121300.zettel ================================================================== --- testdata/testbox/20211020121300.zettel +++ testdata/testbox/20211020121300.zettel @@ -1,6 +0,0 @@ -id: 20211020121300 -title: 10*10*10*ABC -role: zettel -syntax: zmk - -{{20211020121145}}{{20211020121145}}{{20211020121145}}{{20211020121145}}{{20211020121145}}{{20211020121145}}{{20211020121145}}{{20211020121145}}{{20211020121145}}{{20211020121145}} DELETED testdata/testbox/20211020121400.zettel Index: testdata/testbox/20211020121400.zettel ================================================================== --- testdata/testbox/20211020121400.zettel +++ testdata/testbox/20211020121400.zettel @@ -1,6 +0,0 @@ -id: 20211020121400 -title: 10*10*10*10*ABC -role: zettel -syntax: zmk - -{{20211020121300}}{{20211020121300}}{{20211020121300}}{{20211020121300}}{{20211020121300}}{{20211020121300}}{{20211020121300}}{{20211020121300}}{{20211020121300}}{{20211020121300}} DELETED testdata/testbox/20211020182600.zettel Index: testdata/testbox/20211020182600.zettel ================================================================== --- testdata/testbox/20211020182600.zettel +++ testdata/testbox/20211020182600.zettel @@ -1,7 +0,0 @@ -id: 20211020182600 -title: Self-recursive transclusion -role: zettel -syntax: zmk -modified: 20211020182712 - -{{20211020182600}} DELETED testdata/testbox/20211020183700.zettel Index: testdata/testbox/20211020183700.zettel ================================================================== --- testdata/testbox/20211020183700.zettel +++ testdata/testbox/20211020183700.zettel @@ -1,7 +0,0 @@ -id: 20211020183700 -title: Indirect Recursive 1 -role: zettel -syntax: zmk -modified: 20211020183932 - -{{20211020183800}} DELETED testdata/testbox/20211020183800.zettel Index: testdata/testbox/20211020183800.zettel ================================================================== --- testdata/testbox/20211020183800.zettel +++ testdata/testbox/20211020183800.zettel @@ -1,6 +0,0 @@ -id: 20211020183800 -title: Indirect Recursive 2 -role: zettel -syntax: zmk - -{{20211020183700}} DELETED testdata/testbox/20211020184300.zettel Index: testdata/testbox/20211020184300.zettel ================================================================== --- testdata/testbox/20211020184300.zettel +++ testdata/testbox/20211020184300.zettel @@ -1,5 +0,0 @@ -id: 20211020184300 -title: Empty Zettel -role: zettel -syntax: zmk - DELETED testdata/testbox/20211020184342.zettel Index: testdata/testbox/20211020184342.zettel ================================================================== --- testdata/testbox/20211020184342.zettel +++ testdata/testbox/20211020184342.zettel @@ -1,6 +0,0 @@ -id: 20211020184342 -title: Transclude empty Zettel -role: zettel -syntax: zmk - -{{20211020184300}} DELETED testdata/testbox/20211020185400.zettel Index: testdata/testbox/20211020185400.zettel ================================================================== --- testdata/testbox/20211020185400.zettel +++ testdata/testbox/20211020185400.zettel @@ -1,8 +0,0 @@ -id: 20211020185400 -title: Self embed zettel -role: zettel -syntax: zmk -modified: 20211119150218 - -{{#mark}} -[!mark] Home DELETED testdata/testbox/20230929102100.zettel Index: testdata/testbox/20230929102100.zettel ================================================================== --- testdata/testbox/20230929102100.zettel +++ testdata/testbox/20230929102100.zettel @@ -1,7 +0,0 @@ -id: 20230929102100 -title: #test -role: tag -syntax: zmk -created: 20230929102125 - -Zettel with this tag are testing the Zettelstore. DELETED tests/client/client_test.go Index: tests/client/client_test.go ================================================================== --- tests/client/client_test.go +++ tests/client/client_test.go @@ -1,495 +0,0 @@ -//----------------------------------------------------------------------------- -// Copyright (c) 2021-present Detlef Stern -// -// 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" - - "t73f.de/r/zsc/api" - "t73f.de/r/zsc/client" - "zettelstore.de/z/kernel" -) - -func nextZid(zid api.ZettelID) api.ZettelID { - numVal, err := strconv.ParseUint(string(zid), 10, 64) - if err != nil { - panic(err) - } - return api.ZettelID(fmt.Sprintf("%014d", numVal+1)) -} - -func TestNextZid(t *testing.T) { - testCases := []struct { - zid, exp api.ZettelID - }{ - {api.ZettelID("00000000000000"), api.ZettelID("00000000000001")}, - } - for i, tc := range testCases { - if got := nextZid(tc.zid); got != tc.exp { - t.Errorf("%d: zid=%q, exp=%q, got=%q", i, tc.zid, tc.exp, got) - } - - } -} - -func TestListZettel(t *testing.T) { - const ( - ownerZettel = 56 - configRoleZettel = 34 - writerZettel = ownerZettel - 25 - readerZettel = ownerZettel - 25 - creatorZettel = 10 - publicZettel = 5 - ) - - testdata := []struct { - user string - exp int - }{ - {"", publicZettel}, - {"creator", creatorZettel}, - {"reader", readerZettel}, - {"writer", writerZettel}, - {"owner", ownerZettel}, - } - - t.Parallel() - c := getClient() - for i, tc := range testdata { - t.Run(fmt.Sprintf("User %d/%q", i, tc.user), func(tt *testing.T) { - c.SetAuth(tc.user, tc.user) - q, h, l, err := c.QueryZettelData(context.Background(), "") - if err != nil { - tt.Error(err) - return - } - if q != "" { - tt.Errorf("Query should be empty, but is %q", q) - } - if h != "" { - tt.Errorf("Human should be empty, but is %q", q) - } - got := len(l) - if got != tc.exp { - tt.Errorf("List of length %d expected, but got %d\n%v", tc.exp, got, l) - } - }) - } - search := api.KeyRole + api.SearchOperatorHas + api.ValueRoleConfiguration + " ORDER id" - q, h, l, err := c.QueryZettelData(context.Background(), search) - if err != nil { - t.Error(err) - return - } - expQ := "role:configuration ORDER id" - if q != expQ { - t.Errorf("Query should be %q, but is %q", expQ, q) - } - expH := "role HAS configuration ORDER id" - if h != expH { - t.Errorf("Human should be %q, but is %q", expH, h) - } - got := len(l) - if got != configRoleZettel { - t.Errorf("List of length %d expected, but got %d\n%v", configRoleZettel, got, l) - } - - pl, err := c.QueryZettel(context.Background(), search) - if err != nil { - t.Error(err) - return - } - compareZettelList(t, pl, l) -} - -func compareZettelList(t *testing.T, pl [][]byte, l []api.ZidMetaRights) { - t.Helper() - if len(pl) != len(l) { - t.Errorf("Different list lenght: Plain=%d, Data=%d", len(pl), len(l)) - } else { - for i, line := range pl { - if got := api.ZettelID(line[:14]); got != l[i].ID { - t.Errorf("%d: Data=%q, got=%q", i, l[i].ID, got) - } - } - } -} - -func TestGetZettelData(t *testing.T) { - t.Parallel() - c := getClient() - c.SetAuth("owner", "owner") - z, err := c.GetZettelData(context.Background(), api.ZidDefaultHome) - if err != nil { - t.Error(err) - return - } - if m := z.Meta; len(m) == 0 { - t.Errorf("Exptected non-empty meta, but got %v", z.Meta) - } - if z.Content == "" || z.Encoding != "" { - t.Errorf("Expect non-empty content, but empty encoding (got %q)", z.Encoding) - } - - mr, err := c.GetMetaData(context.Background(), api.ZidDefaultHome) - if err != nil { - t.Error(err) - return - } - if mr.Rights == api.ZettelCanNone { - t.Error("rights must be greater zero") - } - if len(mr.Meta) != len(z.Meta) { - t.Errorf("Pure meta differs from zettel meta: %s vs %s", mr.Meta, z.Meta) - return - } - for k, v := range z.Meta { - got, ok := mr.Meta[k] - if !ok { - t.Errorf("Pure meta has no key %q", k) - continue - } - if got != v { - t.Errorf("Pure meta has different value for key %q: %q vs %q", k, got, v) - } - } -} - -func TestGetParsedEvaluatedZettel(t *testing.T) { - t.Parallel() - c := getClient() - c.SetAuth("owner", "owner") - encodings := []api.EncodingEnum{ - api.EncoderHTML, - api.EncoderSz, - api.EncoderText, - } - for _, enc := range encodings { - content, err := c.GetParsedZettel(context.Background(), api.ZidDefaultHome, enc) - if err != nil { - t.Error(err) - continue - } - if len(content) == 0 { - t.Errorf("Empty content for parsed encoding %v", enc) - } - content, err = c.GetEvaluatedZettel(context.Background(), api.ZidDefaultHome, enc) - if err != nil { - t.Error(err) - continue - } - if len(content) == 0 { - t.Errorf("Empty content for evaluated encoding %v", enc) - } - } -} - -func checkListZid(t *testing.T, l []api.ZidMetaRights, pos int, expected api.ZettelID) { - t.Helper() - if got := api.ZettelID(l[pos].ID); got != expected { - t.Errorf("Expected result[%d]=%v, but got %v", pos, expected, got) - } -} - -func TestGetZettelOrder(t *testing.T) { - t.Parallel() - c := getClient() - c.SetAuth("owner", "owner") - _, _, metaSeq, err := c.QueryZettelData(context.Background(), string(api.ZidTOCNewTemplate)+" "+api.ItemsDirective) - if err != nil { - t.Error(err) - return - } - if got := len(metaSeq); got != 4 { - t.Errorf("Expected list of length 4, got %d", got) - return - } - checkListZid(t, metaSeq, 0, api.ZidTemplateNewZettel) - checkListZid(t, metaSeq, 1, api.ZidTemplateNewRole) - checkListZid(t, metaSeq, 2, api.ZidTemplateNewTag) - checkListZid(t, metaSeq, 3, api.ZidTemplateNewUser) -} - -// func TestGetZettelContext(t *testing.T) { -// const ( -// allUserZid = api.ZettelID("20211019200500") -// ownerZid = api.ZettelID("20210629163300") -// writerZid = api.ZettelID("20210629165000") -// readerZid = api.ZettelID("20210629165024") -// creatorZid = api.ZettelID("20210629165050") -// limitAll = 3 -// ) -// t.Parallel() -// c := getClient() -// c.SetAuth("owner", "owner") -// rl, err := c.GetZettelContext(context.Background(), ownerZid, client.DirBoth, 0, limitAll) -// if err != nil { -// t.Error(err) -// return -// } -// if !checkZid(t, ownerZid, rl.ID) { -// return -// } -// l := rl.List -// if got := len(l); got != limitAll { -// t.Errorf("Expected list of length %d, got %d", limitAll, got) -// t.Error(rl) -// return -// } -// checkListZid(t, l, 0, allUserZid) -// // checkListZid(t, l, 1, writerZid) -// // checkListZid(t, l, 2, readerZid) -// checkListZid(t, l, 1, creatorZid) - -// rl, err = c.GetZettelContext(context.Background(), ownerZid, client.DirBackward, 0, 0) -// if err != nil { -// t.Error(err) -// return -// } -// if !checkZid(t, ownerZid, rl.ID) { -// return -// } -// l = rl.List -// if got, exp := len(l), 4; got != exp { -// t.Errorf("Expected list of length %d, got %d", exp, got) -// return -// } -// checkListZid(t, l, 0, allUserZid) -// } - -func TestGetUnlinkedReferences(t *testing.T) { - t.Parallel() - c := getClient() - c.SetAuth("owner", "owner") - _, _, metaSeq, err := c.QueryZettelData(context.Background(), string(api.ZidDefaultHome)+" "+api.UnlinkedDirective) - if err != nil { - t.Error(err) - return - } - if got := len(metaSeq); got != 1 { - t.Errorf("Expected list of length 1, got %d:\n%v", got, metaSeq) - return - } -} - -func failNoErrorOrNoCode(t *testing.T, err error, goodCode int) bool { - if err != nil { - if cErr, ok := err.(*client.Error); ok { - if cErr.StatusCode == goodCode { - return false - } - t.Errorf("Expect status code %d, but got client error %v", goodCode, cErr) - } else { - t.Errorf("Expect status code %d, but got non-client error %v", goodCode, err) - } - } else { - t.Errorf("No error returned, but status code %d expected", goodCode) - } - return true -} - -func TestExecuteCommand(t *testing.T) { - c := getClient() - err := c.ExecuteCommand(context.Background(), api.Command("xyz")) - failNoErrorOrNoCode(t, err, http.StatusBadRequest) - err = c.ExecuteCommand(context.Background(), api.CommandAuthenticated) - failNoErrorOrNoCode(t, err, http.StatusUnauthorized) - err = c.ExecuteCommand(context.Background(), api.CommandRefresh) - failNoErrorOrNoCode(t, err, http.StatusForbidden) - - c.SetAuth("owner", "owner") - err = c.ExecuteCommand(context.Background(), api.CommandAuthenticated) - if err != nil { - t.Error(err) - } - err = c.ExecuteCommand(context.Background(), api.CommandRefresh) - if err != nil { - t.Error(err) - } -} - -func TestListTags(t *testing.T) { - t.Parallel() - c := getClient() - c.SetAuth("owner", "owner") - agg, err := c.QueryAggregate(context.Background(), api.ActionSeparator+api.KeyTags) - if err != nil { - t.Error(err) - return - } - tags := []struct { - key string - size int - }{ - {"#invisible", 1}, - {"#user", 4}, - {"#test", 4}, - } - if len(agg) != len(tags) { - t.Errorf("Expected %d different tags, but got %d (%v)", len(tags), len(agg), agg) - } - for _, tag := range tags { - if zl, ok := agg[tag.key]; !ok { - t.Errorf("No tag %v: %v", tag.key, agg) - } else if len(zl) != tag.size { - t.Errorf("Expected %d zettel with tag %v, but got %v", tag.size, tag.key, zl) - } - } - for i, id := range agg["#user"] { - if id != agg["#test"][i] { - t.Errorf("Tags #user and #test have different content: %v vs %v", agg["#user"], agg["#test"]) - } - } -} - -func TestTagZettel(t *testing.T) { - t.Parallel() - c := getClient() - c.AllowRedirect(true) - c.SetAuth("owner", "owner") - ctx := context.Background() - zid, err := c.TagZettel(ctx, "nosuchtag") - if err != nil { - t.Error(err) - } else if zid != "" { - t.Errorf("no zid expected, but got %q", zid) - } - zid, err = c.TagZettel(ctx, "#test") - exp := api.ZettelID("20230929102100") - if err != nil { - t.Error(err) - } else if zid != exp { - t.Errorf("tag zettel for #test should be %q, but got %q", exp, zid) - } -} - -func TestListRoles(t *testing.T) { - t.Parallel() - c := getClient() - c.SetAuth("owner", "owner") - agg, err := c.QueryAggregate(context.Background(), api.ActionSeparator+api.KeyRole) - if err != nil { - t.Error(err) - return - } - exp := []string{"configuration", "role", "user", "tag", "zettel"} - if len(agg) != len(exp) { - t.Errorf("Expected %d different roles, but got %d (%v)", len(exp), len(agg), agg) - } - for _, id := range exp { - if _, found := agg[id]; !found { - t.Errorf("Role map expected key %q", id) - } - } -} - -func TestRoleZettel(t *testing.T) { - t.Parallel() - c := getClient() - c.AllowRedirect(true) - c.SetAuth("owner", "owner") - ctx := context.Background() - zid, err := c.RoleZettel(ctx, "nosuchrole") - if err != nil { - t.Error("AAA", err) - } else if zid != "" { - t.Errorf("no zid expected, but got %q", zid) - } - zid, err = c.RoleZettel(ctx, "zettel") - exp := api.ZettelID("00000000060010") - if err != nil { - 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()) - if err != nil { - t.Error(err) - return - } - if ver.Major != -1 || ver.Minor != -1 || ver.Patch != -1 || ver.Info != kernel.CoreDefaultVersion || ver.Hash != "" { - t.Error(ver) - } -} - -var baseURL string - -func init() { - flag.StringVar(&baseURL, "base-url", "", "Base URL") -} - -func getClient() *client.Client { - u, err := url.Parse(baseURL) - if err != nil { - panic(err) - } - return client.NewClient(u) -} - -// TestMain controls whether client API tests should run or not. -func TestMain(m *testing.M) { - flag.Parse() - if baseURL != "" { - m.Run() - } -} DELETED tests/client/crud_test.go Index: tests/client/crud_test.go ================================================================== --- tests/client/crud_test.go +++ tests/client/crud_test.go @@ -1,202 +0,0 @@ -//----------------------------------------------------------------------------- -// Copyright (c) 2021-present Detlef Stern -// -// 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 ( - "context" - "strings" - "testing" - - "t73f.de/r/zsc/api" - "t73f.de/r/zsc/client" -) - -// --------------------------------------------------------------------------- -// Tests that change the Zettelstore must nor run parallel to other tests. - -func TestCreateGetRenameDeleteZettel(t *testing.T) { - // Is not to be allowed to run in parallel with other tests. - zettel := `title: A Test - -Example content.` - c := getClient() - c.SetAuth("owner", "owner") - zid, err := c.CreateZettel(context.Background(), []byte(zettel)) - if err != nil { - t.Error("Cannot create zettel:", err) - return - } - if !zid.IsValid() { - t.Error("Invalid zettel ID", zid) - return - } - data, err := c.GetZettel(context.Background(), zid, api.PartZettel) - if err != nil { - t.Error("Cannot read zettel", zid, err) - return - } - exp := `title: A Test - -Example content.` - if string(data) != exp { - t.Errorf("Expected zettel data: %q, but got %q", exp, data) - } - newZid := nextZid(zid) - err = c.RenameZettel(context.Background(), zid, newZid) - if err != nil { - t.Error("Cannot rename", zid, ":", err) - newZid = zid - } - - doDelete(t, c, newZid) -} - -func TestCreateGetRenameDeleteZettelData(t *testing.T) { - // Is not to be allowed to run in parallel with other tests. - c := getClient() - c.SetAuth("creator", "creator") - zid, err := c.CreateZettelData(context.Background(), api.ZettelData{ - Meta: nil, - Encoding: "", - Content: "Example", - }) - if err != nil { - t.Error("Cannot create zettel:", err) - return - } - if !zid.IsValid() { - t.Error("Invalid zettel ID", zid) - return - } - newZid := nextZid(zid) - c.SetAuth("owner", "owner") - err = c.RenameZettel(context.Background(), zid, newZid) - if err != nil { - t.Error("Cannot rename", zid, ":", err) - newZid = zid - } - - c.SetAuth("owner", "owner") - doDelete(t, c, newZid) -} - -func TestCreateGetDeleteZettelData(t *testing.T) { - // Is not to be allowed to run in parallel with other tests. - c := getClient() - c.SetAuth("owner", "owner") - wrongModified := "19691231115959" - zid, err := c.CreateZettelData(context.Background(), api.ZettelData{ - Meta: api.ZettelMeta{ - api.KeyTitle: "A\nTitle", // \n must be converted into a space - api.KeyModified: wrongModified, - }, - }) - if err != nil { - t.Error("Cannot create zettel:", err) - return - } - z, err := c.GetZettelData(context.Background(), zid) - if err != nil { - t.Error("Cannot get zettel:", zid, err) - } else { - exp := "A Title" - if got := z.Meta[api.KeyTitle]; got != exp { - t.Errorf("Expected title %q, but got %q", exp, got) - } - if got := z.Meta[api.KeyModified]; got != "" { - t.Errorf("Create allowed to set the modified key: %q", got) - } - } - doDelete(t, c, zid) -} - -func TestUpdateZettel(t *testing.T) { - c := getClient() - c.SetAuth("owner", "owner") - z, err := c.GetZettel(context.Background(), api.ZidDefaultHome, api.PartZettel) - if err != nil { - t.Error(err) - return - } - if !strings.HasPrefix(string(z), "title: Home\n") { - t.Error("Got unexpected zettel", z) - return - } - newZettel := `title: Empty Home -role: zettel -syntax: zmk - -Empty` - err = c.UpdateZettel(context.Background(), api.ZidDefaultHome, []byte(newZettel)) - if err != nil { - t.Error(err) - return - } - zt, err := c.GetZettel(context.Background(), api.ZidDefaultHome, api.PartZettel) - if err != nil { - t.Error(err) - return - } - if string(zt) != newZettel { - t.Errorf("Expected zettel %q, got %q", newZettel, zt) - } - // Must delete to clean up for next tests - doDelete(t, c, api.ZidDefaultHome) -} - -func TestUpdateZettelData(t *testing.T) { - c := getClient() - c.SetAuth("writer", "writer") - z, err := c.GetZettelData(context.Background(), api.ZidDefaultHome) - if err != nil { - t.Error(err) - return - } - if got := z.Meta[api.KeyTitle]; got != "Home" { - t.Errorf("Title of zettel is not \"Home\", but %q", got) - return - } - newTitle := "New Home" - z.Meta[api.KeyTitle] = newTitle - wrongModified := "19691231235959" - z.Meta[api.KeyModified] = wrongModified - err = c.UpdateZettelData(context.Background(), api.ZidDefaultHome, z) - if err != nil { - t.Error(err) - return - } - zt, err := c.GetZettelData(context.Background(), api.ZidDefaultHome) - if err != nil { - t.Error(err) - return - } - if got := zt.Meta[api.KeyTitle]; got != newTitle { - t.Errorf("Title of zettel is not %q, but %q", newTitle, got) - } - if got := zt.Meta[api.KeyModified]; got == wrongModified { - t.Errorf("Update did not change the modified key: %q", got) - } - - // Must delete to clean up for next tests - c.SetAuth("owner", "owner") - doDelete(t, c, api.ZidDefaultHome) -} - -func doDelete(t *testing.T, c *client.Client, zid api.ZettelID) { - err := c.DeleteZettel(context.Background(), zid) - if err != nil { - t.Helper() - t.Error("Cannot delete", zid, ":", err) - } -} DELETED tests/client/embed_test.go Index: tests/client/embed_test.go ================================================================== --- tests/client/embed_test.go +++ tests/client/embed_test.go @@ -1,183 +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. -// -// SPDX-License-Identifier: EUPL-1.2 -// SPDX-FileCopyrightText: 2021-present Detlef Stern -//----------------------------------------------------------------------------- - -package client_test - -import ( - "context" - "strings" - "testing" - - "t73f.de/r/zsc/api" -) - -const ( - abcZid = api.ZettelID("20211020121000") - abc10Zid = api.ZettelID("20211020121100") -) - -func TestZettelTransclusion(t *testing.T) { - t.Parallel() - c := getClient() - c.SetAuth("owner", "owner") - - const abc10000Zid = api.ZettelID("20211020121400") - contentMap := map[api.ZettelID]int{ - abcZid: 1, - abc10Zid: 10, - api.ZettelID("20211020121145"): 100, - api.ZettelID("20211020121300"): 1000, - } - content, err := c.GetZettel(context.Background(), abcZid, api.PartContent) - if err != nil { - t.Error(err) - return - } - baseContent := string(content) - for zid, siz := range contentMap { - content, err = c.GetEvaluatedZettel(context.Background(), zid, api.EncoderHTML) - if err != nil { - t.Error(err) - continue - } - sContent := string(content) - prefix := "<p>" - if !strings.HasPrefix(sContent, prefix) { - t.Errorf("Content of zettel %q does not start with %q: %q", zid, prefix, stringHead(sContent)) - continue - } - suffix := "</p>" - if !strings.HasSuffix(sContent, suffix) { - t.Errorf("Content of zettel %q does not end with %q: %q", zid, suffix, stringTail(sContent)) - continue - } - got := sContent[len(prefix) : len(content)-len(suffix)] - if expect := strings.Repeat(baseContent, siz); expect != got { - t.Errorf("Unexpected content for zettel %q\nExpect: %q\nGot: %q", zid, expect, got) - } - } - - content, err = c.GetEvaluatedZettel(context.Background(), abc10000Zid, api.EncoderHTML) - if err != nil { - t.Error(err) - return - } - checkContentContains(t, abc10000Zid, string(content), "Too many transclusions") -} - -func TestZettelTransclusionNoPrivilegeEscalation(t *testing.T) { - t.Parallel() - c := getClient() - c.SetAuth("reader", "reader") - - zettelData, err := c.GetZettelData(context.Background(), api.ZidEmoji) - if err != nil { - t.Error(err) - return - } - expectedEnc := "base64" - if got := zettelData.Encoding; expectedEnc != got { - t.Errorf("Zettel %q: encoding %q expected, but got %q", abcZid, expectedEnc, got) - } - - content, err := c.GetEvaluatedZettel(context.Background(), abc10Zid, api.EncoderHTML) - if err != nil { - t.Error(err) - return - } - if exp, got := "", string(content); exp != got { - t.Errorf("Zettel %q must contain %q, but got %q", abc10Zid, exp, got) - } -} - -func stringHead(s string) string { - const maxLen = 40 - if len(s) <= maxLen { - return s - } - return s[:maxLen-3] + "..." -} - -func stringTail(s string) string { - const maxLen = 40 - if len(s) <= maxLen { - return s - } - return "..." + s[len(s)-maxLen-3:] -} - -func TestRecursiveTransclusion(t *testing.T) { - t.Parallel() - c := getClient() - c.SetAuth("owner", "owner") - - const ( - selfRecursiveZid = api.ZettelID("20211020182600") - indirectRecursive1Zid = api.ZettelID("20211020183700") - indirectRecursive2Zid = api.ZettelID("20211020183800") - ) - recursiveZettel := map[api.ZettelID]api.ZettelID{ - selfRecursiveZid: selfRecursiveZid, - indirectRecursive1Zid: indirectRecursive2Zid, - indirectRecursive2Zid: indirectRecursive1Zid, - } - for zid, errZid := range recursiveZettel { - content, err := c.GetEvaluatedZettel(context.Background(), zid, api.EncoderHTML) - if err != nil { - t.Error(err) - continue - } - sContent := string(content) - checkContentContains(t, zid, sContent, "Recursive transclusion") - checkContentContains(t, zid, sContent, string(errZid)) - } -} -func TestNothingToTransclude(t *testing.T) { - t.Parallel() - c := getClient() - c.SetAuth("owner", "owner") - - const ( - transZid = api.ZettelID("20211020184342") - emptyZid = api.ZettelID("20211020184300") - ) - content, err := c.GetEvaluatedZettel(context.Background(), transZid, api.EncoderHTML) - if err != nil { - t.Error(err) - return - } - sContent := string(content) - checkContentContains(t, transZid, sContent, "<!-- Nothing to transclude") - checkContentContains(t, transZid, sContent, string(emptyZid)) -} - -func TestSelfEmbedRef(t *testing.T) { - t.Parallel() - c := getClient() - c.SetAuth("owner", "owner") - - const selfEmbedZid = api.ZettelID("20211020185400") - content, err := c.GetEvaluatedZettel(context.Background(), selfEmbedZid, api.EncoderHTML) - if err != nil { - t.Error(err) - return - } - checkContentContains(t, selfEmbedZid, string(content), "Self embed reference") -} - -func checkContentContains(t *testing.T, zid api.ZettelID, content, expected string) { - if !strings.Contains(content, expected) { - t.Helper() - t.Errorf("Zettel %q should contain %q, but does not: %q", zid, expected, content) - } -} Index: tests/markdown_test.go ================================================================== --- tests/markdown_test.go +++ tests/markdown_test.go @@ -1,43 +1,38 @@ //----------------------------------------------------------------------------- -// Copyright (c) 2020-present Detlef Stern +// Copyright (c) 2020-2021 Detlef Stern // -// This file is part of Zettelstore. +// This file is part of zettelstore. // // Zettelstore is licensed under the latest version of the EUPL (European Union // Public License). Please see file LICENSE.txt for your rights and obligations // under this license. -// -// SPDX-License-Identifier: EUPL-1.2 -// SPDX-FileCopyrightText: 2020-present Detlef Stern //----------------------------------------------------------------------------- +// Package tests provides some higher-level tests. package tests import ( - "bytes" "encoding/json" "fmt" "os" + "regexp" "strings" "testing" - "t73f.de/r/zsc/api" - "t73f.de/r/zsc/input" + "zettelstore.de/z/api" "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/jsonenc" + _ "zettelstore.de/z/encoder/nativeenc" _ "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" ) type markdownTestCase struct { Markdown string `json:"markdown"` HTML string `json:"html"` @@ -45,17 +40,45 @@ StartLine int `json:"start_line"` EndLine int `json:"end_line"` Section string `json:"section"` } +// exceptions lists all CommonMark tests that should not be tested for identical HTML output +var exceptions = []string{ + " - foo\n - bar\n\t - baz\n", // 9 + "<script type=\"text/javascript\">\n// JavaScript example\n\ndocument.getElementById(\"demo\").innerHTML = \"Hello JavaScript!\";\n</script>\nokay\n", // 170 + "<script>\nfoo\n</script>1. *bar*\n", // 178 + "- foo\n - bar\n - baz\n - boo\n", // 294 + "10) foo\n - bar\n", // 296 + "- # Foo\n- Bar\n ---\n baz\n", // 300 + "- foo\n\n- bar\n\n\n- baz\n", // 306 + "- foo\n - bar\n - baz\n\n\n bim\n", // 307 + "1. a\n\n 2. b\n\n 3. c\n", // 311 + "1. a\n\n 2. b\n\n 3. c\n", // 313 + "- a\n- b\n\n- c\n", // 314 + "* a\n*\n\n* c\n", // 315 + "- a\n- b\n\n [ref]: /url\n- d\n", // 317 + "- a\n - b\n\n c\n- d\n", // 319 + "* a\n > b\n >\n* c\n", // 320 + "- a\n > b\n ```\n c\n ```\n- d\n", // 321 + "- a\n - b\n", // 323 + "<http://foo.bar.`baz>`\n", // 345 + "[foo<http://example.com/?search=](uri)>\n", // 525 + "[foo<http://example.com/?search=][ref]>\n\n[ref]: /uri\n", // 537 + "<http://example.com?find=\\*>\n", // 581 + "<http://foo.bar.baz/test?q=hello&id=22&boolean>\n", // 594 +} + +var reHeadingID = regexp.MustCompile(` id="[^"]*"`) + func TestEncoderAvailability(t *testing.T) { t.Parallel() encoderMissing := false - for _, enc := range encodings { - enc := encoder.Create(enc, &encoder.CreateParameter{Lang: api.ValueLangEN}) + for _, format := range formats { + enc := encoder.Create(format, nil) if enc == nil { - t.Errorf("No encoder for %q found", enc) + t.Errorf("No encoder for %q found", format) encoderMissing = true } } if encoderMissing { panic("At least one encoder is missing. See test log") @@ -70,78 +93,86 @@ } var testcases []markdownTestCase if err = json.Unmarshal(content, &testcases); err != nil { panic(err) } - + excMap := make(map[string]bool, len(exceptions)) + for _, exc := range exceptions { + excMap[exc] = true + } for _, tc := range testcases { - ast := createMDBlockSlice(tc.Markdown, config.NoHTML) - testAllEncodings(t, tc, &ast) - testZmkEncoding(t, tc, &ast) + ast := parser.ParseBlocks(input.NewInput(tc.Markdown), nil, "markdown") + testAllEncodings(t, tc, ast) + if _, found := excMap[tc.Markdown]; !found { + testHTMLEncoding(t, tc, ast) + } + testZmkEncoding(t, tc, ast) } } -func createMDBlockSlice(markdown string, hi config.HTMLInsecurity) ast.BlockSlice { - return parser.ParseBlocks(input.NewInput([]byte(markdown)), nil, meta.SyntaxMarkdown, hi) -} - -func testAllEncodings(t *testing.T, tc markdownTestCase, ast *ast.BlockSlice) { +func testAllEncodings(t *testing.T, tc markdownTestCase, ast ast.BlockSlice) { var sb strings.Builder testID := tc.Example*100 + 1 - for _, enc := range encodings { - t.Run(fmt.Sprintf("Encode %v %v", enc, testID), func(st *testing.T) { - encoder.Create(enc, &encoder.CreateParameter{Lang: api.ValueLangEN}).WriteBlocks(&sb, ast) + for _, format := range formats { + t.Run(fmt.Sprintf("Encode %v %v", format, testID), func(st *testing.T) { + encoder.Create(format, nil).WriteBlocks(&sb, ast) sb.Reset() }) } } -func testZmkEncoding(t *testing.T, tc markdownTestCase, ast *ast.BlockSlice) { +func testHTMLEncoding(t *testing.T, tc markdownTestCase, ast ast.BlockSlice) { + htmlEncoder := encoder.Create(api.EncoderHTML, &encoder.Environment{Xhtml: true}) + var sb strings.Builder + testID := tc.Example*100 + 1 + t.Run(fmt.Sprintf("Encode md html %v", testID), func(st *testing.T) { + htmlEncoder.WriteBlocks(&sb, ast) + gotHTML := sb.String() + sb.Reset() + + mdHTML := tc.HTML + mdHTML = strings.ReplaceAll(mdHTML, "\"MAILTO:", "\"mailto:") + gotHTML = strings.ReplaceAll(gotHTML, " class=\"zs-external\"", "") + gotHTML = strings.ReplaceAll(gotHTML, "%2A", "*") // url.QueryEscape + if strings.Count(gotHTML, "<h") > 0 { + gotHTML = reHeadingID.ReplaceAllString(gotHTML, "") + } + if gotHTML != mdHTML { + mdHTML = strings.ReplaceAll(mdHTML, "<li>\n", "<li>") + if gotHTML != mdHTML { + st.Errorf("\nCMD: %q\nExp: %q\nGot: %q", tc.Markdown, mdHTML, gotHTML) + } + } + }) +} + +func testZmkEncoding(t *testing.T, tc markdownTestCase, ast ast.BlockSlice) { zmkEncoder := encoder.Create(api.EncoderZmk, nil) - var buf bytes.Buffer + var sb strings.Builder testID := tc.Example*100 + 1 t.Run(fmt.Sprintf("Encode zmk %14d", testID), func(st *testing.T) { - buf.Reset() - zmkEncoder.WriteBlocks(&buf, ast) - // gotFirst := buf.String() + zmkEncoder.WriteBlocks(&sb, ast) + gotFirst := sb.String() + sb.Reset() testID = tc.Example*100 + 2 - secondAst := parser.ParseBlocks(input.NewInput(buf.Bytes()), nil, meta.SyntaxZmk, config.NoHTML) - buf.Reset() - zmkEncoder.WriteBlocks(&buf, &secondAst) - gotSecond := buf.String() + secondAst := parser.ParseBlocks(input.NewInput(gotFirst), nil, "zmk") + zmkEncoder.WriteBlocks(&sb, secondAst) + gotSecond := sb.String() + sb.Reset() // if gotFirst != gotSecond { // st.Errorf("\nCMD: %q\n1st: %q\n2nd: %q", tc.Markdown, gotFirst, gotSecond) // } testID = tc.Example*100 + 3 - thirdAst := parser.ParseBlocks(input.NewInput(buf.Bytes()), nil, meta.SyntaxZmk, config.NoHTML) - buf.Reset() - zmkEncoder.WriteBlocks(&buf, &thirdAst) - gotThird := buf.String() + thirdAst := parser.ParseBlocks(input.NewInput(gotFirst), nil, "zmk") + zmkEncoder.WriteBlocks(&sb, thirdAst) + gotThird := sb.String() + sb.Reset() if gotSecond != gotThird { st.Errorf("\n1st: %q\n2nd: %q", gotSecond, gotThird) } }) -} - -func TestAdditionalMarkdown(t *testing.T) { - testcases := []struct { - md string - exp string - }{ - {`abc<br>def`, `abc@@<br>@@{="html"}def`}, - } - zmkEncoder := encoder.Create(api.EncoderZmk, nil) - var sb strings.Builder - for i, tc := range testcases { - ast := createMDBlockSlice(tc.md, config.MarkdownHTML) - sb.Reset() - zmkEncoder.WriteBlocks(&sb, &ast) - got := sb.String() - if got != tc.exp { - t.Errorf("%d: %q -> %q, but got %q", i, tc.md, tc.exp, got) - } - } + } DELETED tests/naughtystrings_test.go Index: tests/naughtystrings_test.go ================================================================== --- tests/naughtystrings_test.go +++ tests/naughtystrings_test.go @@ -1,100 +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. -// -// SPDX-License-Identifier: EUPL-1.2 -// SPDX-FileCopyrightText: 2020-present Detlef Stern -//----------------------------------------------------------------------------- - -package tests - -import ( - "bufio" - "io" - "os" - "path/filepath" - "testing" - - "t73f.de/r/zsc/api" - "t73f.de/r/zsc/input" - _ "zettelstore.de/z/cmd" - "zettelstore.de/z/encoder" - "zettelstore.de/z/parser" - "zettelstore.de/z/zettel/meta" -) - -// Test all parser / encoder with a list of "naughty strings", i.e. unusual strings -// that often crash software. - -func getNaughtyStrings() (result []string, err error) { - fpath := filepath.Join("..", "testdata", "naughty", "blns.txt") - file, err := os.Open(fpath) - if err != nil { - return nil, err - } - defer file.Close() - scanner := bufio.NewScanner(file) - for scanner.Scan() { - if text := scanner.Text(); text != "" && text[0] != '#' { - result = append(result, text) - } - } - return result, scanner.Err() -} - -func getAllParser() (result []*parser.Info) { - for _, pname := range parser.GetSyntaxes() { - pinfo := parser.Get(pname) - if pname == pinfo.Name { - result = append(result, pinfo) - } - } - return result -} - -func getAllEncoder() (result []encoder.Encoder) { - for _, enc := range encoder.GetEncodings() { - e := encoder.Create(enc, &encoder.CreateParameter{Lang: api.ValueLangEN}) - result = append(result, e) - } - return result -} - -func TestNaughtyStringParser(t *testing.T) { - blns, err := getNaughtyStrings() - if err != nil { - t.Fatal(err) - } - if len(blns) == 0 { - t.Fatal("no naughty strings found") - } - pinfos := getAllParser() - if len(pinfos) == 0 { - t.Fatal("no parser found") - } - encs := getAllEncoder() - if len(encs) == 0 { - t.Fatal("no encoder found") - } - for _, s := range blns { - for _, pinfo := range pinfos { - bs := pinfo.ParseBlocks(input.NewInput([]byte(s)), &meta.Meta{}, pinfo.Name) - is := pinfo.ParseInlines(input.NewInput([]byte(s)), pinfo.Name) - for _, enc := range encs { - _, err = enc.WriteBlocks(io.Discard, &bs) - if err != nil { - t.Error(err) - } - _, err = enc.WriteInlines(io.Discard, &is) - if err != nil { - t.Error(err) - } - } - } - } -} Index: tests/regression_test.go ================================================================== --- tests/regression_test.go +++ tests/regression_test.go @@ -1,16 +1,13 @@ //----------------------------------------------------------------------------- -// Copyright (c) 2020-present Detlef Stern +// Copyright (c) 2020-2021 Detlef Stern // -// This file is part of Zettelstore. +// This file is part of zettelstore. // // Zettelstore is licensed under the latest version of the EUPL (European Union // Public License). Please see file LICENSE.txt for your rights and obligations // under this license. -// -// SPDX-License-Identifier: EUPL-1.2 -// SPDX-FileCopyrightText: 2020-present Detlef Stern //----------------------------------------------------------------------------- // Package tests provides some higher-level tests. package tests @@ -22,27 +19,35 @@ "os" "path/filepath" "strings" "testing" - "t73f.de/r/zsc/api" + "zettelstore.de/z/api" "zettelstore.de/z/ast" "zettelstore.de/z/box" "zettelstore.de/z/box/manager" - "zettelstore.de/z/config" + "zettelstore.de/z/domain" + "zettelstore.de/z/domain/id" + "zettelstore.de/z/domain/meta" "zettelstore.de/z/encoder" "zettelstore.de/z/kernel" "zettelstore.de/z/parser" - "zettelstore.de/z/query" - "zettelstore.de/z/zettel/meta" _ "zettelstore.de/z/box/dirbox" + _ "zettelstore.de/z/encoder/htmlenc" + _ "zettelstore.de/z/encoder/jsonenc" + _ "zettelstore.de/z/encoder/nativeenc" + _ "zettelstore.de/z/encoder/textenc" + _ "zettelstore.de/z/encoder/zmkenc" + _ "zettelstore.de/z/parser/blob" + _ "zettelstore.de/z/parser/zettelmark" ) -var encodings = []api.EncodingEnum{ +var formats = []api.EncodingEnum{ api.EncoderHTML, - api.EncoderSz, + api.EncoderDJSON, + api.EncoderNative, api.EncoderText, } func getFileBoxes(wd, kind string) (root string, boxes []box.ManagedBox) { root = filepath.Clean(filepath.Join(wd, "..", "testdata", kind)) @@ -49,40 +54,35 @@ entries, err := os.ReadDir(root) if err != nil { panic(err) } - cdata := manager.ConnectData{ - Number: 0, - Config: testConfig, - Enricher: &noEnrich{}, - Notify: nil, - } + cdata := manager.ConnectData{Config: testConfig, Enricher: &noEnrich{}, Notify: nil} for _, entry := range entries { if entry.IsDir() { - u, err2 := url.Parse("dir://" + filepath.Join(root, entry.Name()) + "?type=" + kernel.BoxDirTypeSimple) - if err2 != nil { - panic(err2) + u, err := url.Parse("dir://" + filepath.Join(root, entry.Name()) + "?type=" + kernel.BoxDirTypeSimple) + if err != nil { + panic(err) } - box, err2 := manager.Connect(u, &noAuth{}, &cdata) - if err2 != nil { - panic(err2) + box, err := manager.Connect(u, &noAuth{}, &cdata) + if err != nil { + panic(err) } boxes = append(boxes, box) } } return root, boxes } type noEnrich struct{} -func (*noEnrich) Enrich(context.Context, *meta.Meta, int) {} -func (*noEnrich) Remove(context.Context, *meta.Meta) {} +func (nf *noEnrich) Enrich(context.Context, *meta.Meta, int) {} +func (nf *noEnrich) Remove(context.Context, *meta.Meta) {} type noAuth struct{} -func (*noAuth) IsReadonly() bool { return false } +func (na *noAuth) IsReadonly() bool { return false } func trimLastEOL(s string) string { if lastPos := len(s) - 1; lastPos >= 0 && s[lastPos] == '\n' { return s[:lastPos] } @@ -110,74 +110,150 @@ wantContent = trimLastEOL(wantContent) if gotContent != wantContent { t.Errorf("\nWant: %q\nGot: %q", wantContent, gotContent) } } + +func checkBlocksFile(t *testing.T, resultName string, zn *ast.ZettelNode, format api.EncodingEnum) { + t.Helper() + var env encoder.Environment + if enc := encoder.Create(format, &env); enc != nil { + var sb strings.Builder + enc.WriteBlocks(&sb, zn.Ast) + checkFileContent(t, resultName, sb.String()) + return + } + panic(fmt.Sprintf("Unknown writer format %q", format)) +} + +func checkZmkEncoder(t *testing.T, zn *ast.ZettelNode) { + zmkEncoder := encoder.Create(api.EncoderZmk, nil) + var sb strings.Builder + zmkEncoder.WriteBlocks(&sb, zn.Ast) + gotFirst := sb.String() + sb.Reset() + + newZettel := parser.ParseZettel(domain.Zettel{ + Meta: zn.Meta, Content: domain.NewContent("\n" + gotFirst)}, "", testConfig) + zmkEncoder.WriteBlocks(&sb, newZettel.Ast) + gotSecond := sb.String() + sb.Reset() + + if gotFirst != gotSecond { + t.Errorf("\n1st: %q\n2nd: %q", gotFirst, gotSecond) + } +} func getBoxName(p box.ManagedBox, root string) string { u, err := url.Parse(p.Location()) if err != nil { panic("Unable to parse URL '" + p.Location() + "': " + err.Error()) } return u.Path[len(root):] } -func checkMetaFile(t *testing.T, resultName string, zn *ast.ZettelNode, enc api.EncodingEnum) { +func match(*meta.Meta) bool { return true } + +func checkContentBox(t *testing.T, p box.ManagedBox, wd, boxName string) { + ss := p.(box.StartStopper) + if err := ss.Start(context.Background()); err != nil { + panic(err) + } + metaList, err := p.SelectMeta(context.Background(), match) + if err != nil { + panic(err) + } + for _, meta := range metaList { + zettel, err := p.GetZettel(context.Background(), meta.Zid) + if err != nil { + panic(err) + } + z := parser.ParseZettel(zettel, "", testConfig) + for _, format := range formats { + t.Run(fmt.Sprintf("%s::%d(%s)", p.Location(), meta.Zid, format), func(st *testing.T) { + resultName := filepath.Join(wd, "result", "content", boxName, z.Zid.String()+"."+format.String()) + checkBlocksFile(st, resultName, z, format) + }) + } + t.Run(fmt.Sprintf("%s::%d", p.Location(), meta.Zid), func(st *testing.T) { + checkZmkEncoder(st, z) + }) + } + if err := ss.Stop(context.Background()); err != nil { + panic(err) + } +} + +func TestContentRegression(t *testing.T) { + t.Parallel() + wd, err := os.Getwd() + if err != nil { + panic(err) + } + root, boxes := getFileBoxes(wd, "content") + for _, p := range boxes { + checkContentBox(t, p, wd, getBoxName(p, root)) + } +} + +func checkMetaFile(t *testing.T, resultName string, zn *ast.ZettelNode, format api.EncodingEnum) { t.Helper() - if enc := encoder.Create(enc, &encoder.CreateParameter{Lang: api.ValueLangEN}); enc != nil { - var sf strings.Builder - enc.WriteMeta(&sf, zn.Meta, parser.ParseMetadata) - checkFileContent(t, resultName, sf.String()) + if enc := encoder.Create(format, nil); enc != nil { + var sb strings.Builder + enc.WriteMeta(&sb, zn.Meta) + checkFileContent(t, resultName, sb.String()) return } - panic(fmt.Sprintf("Unknown writer encoding %q", enc)) + panic(fmt.Sprintf("Unknown writer format %q", format)) } func checkMetaBox(t *testing.T, p box.ManagedBox, wd, boxName string) { ss := p.(box.StartStopper) if err := ss.Start(context.Background()); err != nil { panic(err) } - metaList := []*meta.Meta{} - if err := p.ApplyMeta(context.Background(), - func(m *meta.Meta) { metaList = append(metaList, m) }, - query.AlwaysIncluded); err != nil { + metaList, err := p.SelectMeta(context.Background(), match) + if err != nil { panic(err) } for _, meta := range metaList { zettel, err := p.GetZettel(context.Background(), meta.Zid) if err != nil { panic(err) } - z := parser.ParseZettel(context.Background(), zettel, "", testConfig) - for _, enc := range encodings { - t.Run(fmt.Sprintf("%s::%d(%s)", p.Location(), meta.Zid, enc), func(st *testing.T) { - resultName := filepath.Join(wd, "result", "meta", boxName, z.Zid.String()+"."+enc.String()) - checkMetaFile(st, resultName, z, enc) + z := parser.ParseZettel(zettel, "", testConfig) + for _, format := range formats { + t.Run(fmt.Sprintf("%s::%d(%s)", p.Location(), meta.Zid, format), func(st *testing.T) { + resultName := filepath.Join(wd, "result", "meta", boxName, z.Zid.String()+"."+format.String()) + checkMetaFile(st, resultName, z, format) }) } } - ss.Stop(context.Background()) + if err := ss.Stop(context.Background()); err != nil { + panic(err) + } } type myConfig struct{} -func (*myConfig) Get(context.Context, *meta.Meta, string) string { return "" } -func (*myConfig) AddDefaultValues(_ context.Context, m *meta.Meta) *meta.Meta { - return m -} -func (*myConfig) GetHTMLInsecurity() config.HTMLInsecurity { return config.NoHTML } -func (*myConfig) GetListPageSize() int { return 0 } -func (*myConfig) GetSiteName() string { return "" } -func (*myConfig) GetYAMLHeader() bool { return false } -func (*myConfig) GetZettelFileSyntax() []string { return nil } - -func (*myConfig) GetSimpleMode() bool { return false } -func (*myConfig) GetExpertMode() bool { return false } -func (*myConfig) GetVisibility(*meta.Meta) meta.Visibility { return meta.VisibilityPublic } -func (*myConfig) GetMaxTransclusions() int { return 1024 } +func (cfg *myConfig) AddDefaultValues(m *meta.Meta) *meta.Meta { return m } +func (cfg *myConfig) GetDefaultTitle() string { return "" } +func (cfg *myConfig) GetDefaultRole() string { return meta.ValueRoleZettel } +func (cfg *myConfig) GetDefaultSyntax() string { return meta.ValueSyntaxZmk } +func (cfg *myConfig) GetDefaultLang() string { return "" } +func (cfg *myConfig) GetDefaultVisibility() meta.Visibility { return meta.VisibilityPublic } +func (cfg *myConfig) GetFooterHTML() string { return "" } +func (cfg *myConfig) GetHomeZettel() id.Zid { return id.Invalid } +func (cfg *myConfig) GetListPageSize() int { return 0 } +func (cfg *myConfig) GetMarkerExternal() string { return "" } +func (cfg *myConfig) GetSiteName() string { return "" } +func (cfg *myConfig) GetYAMLHeader() bool { return false } +func (cfg *myConfig) GetZettelFileSyntax() []string { return nil } + +func (cfg *myConfig) GetExpertMode() bool { return false } +func (cfg *myConfig) GetVisibility(*meta.Meta) meta.Visibility { return cfg.GetDefaultVisibility() } var testConfig = &myConfig{} func TestMetaRegression(t *testing.T) { t.Parallel() ADDED tests/result/content/blockcomment/20200215204700.djson Index: tests/result/content/blockcomment/20200215204700.djson ================================================================== --- tests/result/content/blockcomment/20200215204700.djson +++ tests/result/content/blockcomment/20200215204700.djson @@ -0,0 +1,1 @@ +[{"t":"CommentBlock","l":["No render"]},{"t":"CommentBlock","a":{"-":""},"l":["Render"]}] ADDED tests/result/content/blockcomment/20200215204700.html Index: tests/result/content/blockcomment/20200215204700.html ================================================================== --- tests/result/content/blockcomment/20200215204700.html +++ tests/result/content/blockcomment/20200215204700.html @@ -0,0 +1,3 @@ +<!-- +Render +--> ADDED tests/result/content/blockcomment/20200215204700.native Index: tests/result/content/blockcomment/20200215204700.native ================================================================== --- tests/result/content/blockcomment/20200215204700.native +++ tests/result/content/blockcomment/20200215204700.native @@ -0,0 +1,2 @@ +[CommentBlock "No render"], +[CommentBlock ("",[-]) "Render"] ADDED tests/result/content/blockcomment/20200215204700.text Index: tests/result/content/blockcomment/20200215204700.text ================================================================== --- tests/result/content/blockcomment/20200215204700.text +++ tests/result/content/blockcomment/20200215204700.text ADDED tests/result/content/cite/20200215204700.djson Index: tests/result/content/cite/20200215204700.djson ================================================================== --- tests/result/content/cite/20200215204700.djson +++ tests/result/content/cite/20200215204700.djson @@ -0,0 +1,1 @@ +[{"t":"Para","i":[{"t":"Cite","a":{"-":""},"s":"Stern18"}]}] ADDED tests/result/content/cite/20200215204700.html Index: tests/result/content/cite/20200215204700.html ================================================================== --- tests/result/content/cite/20200215204700.html +++ tests/result/content/cite/20200215204700.html @@ -0,0 +1,1 @@ +<p>Stern18</p> ADDED tests/result/content/cite/20200215204700.native Index: tests/result/content/cite/20200215204700.native ================================================================== --- tests/result/content/cite/20200215204700.native +++ tests/result/content/cite/20200215204700.native @@ -0,0 +1,1 @@ +[Para Cite ("",[-]) "Stern18"] ADDED tests/result/content/cite/20200215204700.text Index: tests/result/content/cite/20200215204700.text ================================================================== --- tests/result/content/cite/20200215204700.text +++ tests/result/content/cite/20200215204700.text ADDED tests/result/content/comment/20200215204700.djson Index: tests/result/content/comment/20200215204700.djson ================================================================== --- tests/result/content/comment/20200215204700.djson +++ tests/result/content/comment/20200215204700.djson @@ -0,0 +1,1 @@ +[{"t":"Para","i":[{"t":"Text","s":"%"},{"t":"Space"},{"t":"Text","s":"No"},{"t":"Space"},{"t":"Text","s":"comment"},{"t":"Soft"},{"t":"Comment","s":"Comment"}]}] ADDED tests/result/content/comment/20200215204700.html Index: tests/result/content/comment/20200215204700.html ================================================================== --- tests/result/content/comment/20200215204700.html +++ tests/result/content/comment/20200215204700.html @@ -0,0 +1,2 @@ +<p>% No comment +<!-- Comment --></p> ADDED tests/result/content/comment/20200215204700.native Index: tests/result/content/comment/20200215204700.native ================================================================== --- tests/result/content/comment/20200215204700.native +++ tests/result/content/comment/20200215204700.native @@ -0,0 +1,1 @@ +[Para Text "%",Space,Text "No",Space,Text "comment",Space,Comment "Comment"] ADDED tests/result/content/comment/20200215204700.text Index: tests/result/content/comment/20200215204700.text ================================================================== --- tests/result/content/comment/20200215204700.text +++ tests/result/content/comment/20200215204700.text @@ -0,0 +1,1 @@ +% No comment ADDED tests/result/content/descrlist/20200226122100.djson Index: tests/result/content/descrlist/20200226122100.djson ================================================================== --- tests/result/content/descrlist/20200226122100.djson +++ tests/result/content/descrlist/20200226122100.djson @@ -0,0 +1,1 @@ +[{"t":"DescriptionList","g":[[[{"t":"Text","s":"Zettel"}],[{"t":"Para","i":[{"t":"Text","s":"Paper"}]}],[{"t":"Para","i":[{"t":"Text","s":"Note"}]}]],[[{"t":"Text","s":"Zettelkasten"}],[{"t":"Para","i":[{"t":"Text","s":"Slip"},{"t":"Space"},{"t":"Text","s":"box"}]}]]]}] ADDED tests/result/content/descrlist/20200226122100.html Index: tests/result/content/descrlist/20200226122100.html ================================================================== --- tests/result/content/descrlist/20200226122100.html +++ tests/result/content/descrlist/20200226122100.html @@ -0,0 +1,7 @@ +<dl> +<dt>Zettel</dt> +<dd>Paper</dd> +<dd>Note</dd> +<dt>Zettelkasten</dt> +<dd>Slip box</dd> +</dl> ADDED tests/result/content/descrlist/20200226122100.native Index: tests/result/content/descrlist/20200226122100.native ================================================================== --- tests/result/content/descrlist/20200226122100.native +++ tests/result/content/descrlist/20200226122100.native @@ -0,0 +1,9 @@ +[DescriptionList + [Term [Text "Zettel"], + [Description + [Para Text "Paper"]], + [Description + [Para Text "Note"]]], + [Term [Text "Zettelkasten"], + [Description + [Para Text "Slip",Space,Text "box"]]]] ADDED tests/result/content/descrlist/20200226122100.text Index: tests/result/content/descrlist/20200226122100.text ================================================================== --- tests/result/content/descrlist/20200226122100.text +++ tests/result/content/descrlist/20200226122100.text @@ -0,0 +1,5 @@ +Zettel +Paper +Note +Zettelkasten +Slip box ADDED tests/result/content/edit/20200215204700.djson Index: tests/result/content/edit/20200215204700.djson ================================================================== --- tests/result/content/edit/20200215204700.djson +++ tests/result/content/edit/20200215204700.djson @@ -0,0 +1,1 @@ +[{"t":"Para","i":[{"t":"Delete","i":[{"t":"Text","s":"delete"}]},{"t":"Soft"},{"t":"Insert","i":[{"t":"Text","s":"insert"}]},{"t":"Soft"},{"t":"Delete","i":[{"t":"Text","s":"kill"}]},{"t":"Insert","i":[{"t":"Text","s":"create"}]}]}] ADDED tests/result/content/edit/20200215204700.html Index: tests/result/content/edit/20200215204700.html ================================================================== --- tests/result/content/edit/20200215204700.html +++ tests/result/content/edit/20200215204700.html @@ -0,0 +1,3 @@ +<p><del>delete</del> +<ins>insert</ins> +<del>kill</del><ins>create</ins></p> ADDED tests/result/content/edit/20200215204700.native Index: tests/result/content/edit/20200215204700.native ================================================================== --- tests/result/content/edit/20200215204700.native +++ tests/result/content/edit/20200215204700.native @@ -0,0 +1,1 @@ +[Para Delete [Text "delete"],Space,Insert [Text "insert"],Space,Delete [Text "kill"],Insert [Text "create"]] ADDED tests/result/content/edit/20200215204700.text Index: tests/result/content/edit/20200215204700.text ================================================================== --- tests/result/content/edit/20200215204700.text +++ tests/result/content/edit/20200215204700.text @@ -0,0 +1,1 @@ +delete insert killcreate ADDED tests/result/content/footnote/20200215204700.djson Index: tests/result/content/footnote/20200215204700.djson ================================================================== --- tests/result/content/footnote/20200215204700.djson +++ tests/result/content/footnote/20200215204700.djson @@ -0,0 +1,1 @@ +[{"t":"Para","i":[{"t":"Text","s":"Text"},{"t":"Footnote","a":{"":"sidebar"},"i":[{"t":"Text","s":"foot"}]}]}] ADDED tests/result/content/footnote/20200215204700.html Index: tests/result/content/footnote/20200215204700.html ================================================================== --- tests/result/content/footnote/20200215204700.html +++ tests/result/content/footnote/20200215204700.html @@ -0,0 +1,4 @@ +<p>Text<sup id="fnref:1"><a href="#fn:1" class="zs-footnote-ref" role="doc-noteref">1</a></sup></p> +<ol class="zs-endnotes"> +<li id="fn:1" role="doc-endnote">foot <a href="#fnref:1" class="zs-footnote-backref" role="doc-backlink">↩︎</a></li> +</ol> ADDED tests/result/content/footnote/20200215204700.native Index: tests/result/content/footnote/20200215204700.native ================================================================== --- tests/result/content/footnote/20200215204700.native +++ tests/result/content/footnote/20200215204700.native @@ -0,0 +1,1 @@ +[Para Text "Text",Footnote ("sidebar",[]) [Text "foot"]] ADDED tests/result/content/footnote/20200215204700.text Index: tests/result/content/footnote/20200215204700.text ================================================================== --- tests/result/content/footnote/20200215204700.text +++ tests/result/content/footnote/20200215204700.text @@ -0,0 +1,1 @@ +Text foot ADDED tests/result/content/format/20200215204700.djson Index: tests/result/content/format/20200215204700.djson ================================================================== --- tests/result/content/format/20200215204700.djson +++ tests/result/content/format/20200215204700.djson @@ -0,0 +1,1 @@ +[{"t":"Para","i":[{"t":"Italic","i":[{"t":"Text","s":"italic"}]},{"t":"Soft"},{"t":"Emph","i":[{"t":"Text","s":"emph"}]},{"t":"Soft"},{"t":"Bold","i":[{"t":"Text","s":"bold"}]},{"t":"Soft"},{"t":"Strong","i":[{"t":"Text","s":"strong"}]},{"t":"Soft"},{"t":"Underline","i":[{"t":"Text","s":"unterline"}]},{"t":"Soft"},{"t":"Strikethrough","i":[{"t":"Text","s":"strike"}]},{"t":"Soft"},{"t":"Mono","i":[{"t":"Text","s":"monospace"}]},{"t":"Soft"},{"t":"Super","i":[{"t":"Text","s":"superscript"}]},{"t":"Soft"},{"t":"Sub","i":[{"t":"Text","s":"subscript"}]},{"t":"Soft"},{"t":"Quote","i":[{"t":"Text","s":"Quotes"}]},{"t":"Soft"},{"t":"Quotation","i":[{"t":"Text","s":"Quotation"}]},{"t":"Soft"},{"t":"Small","i":[{"t":"Text","s":"small"}]},{"t":"Soft"},{"t":"Span","i":[{"t":"Text","s":"span"}]},{"t":"Soft"},{"t":"Code","s":"code"},{"t":"Soft"},{"t":"Input","s":"input"},{"t":"Soft"},{"t":"Output","s":"output"}]}] ADDED tests/result/content/format/20200215204700.html Index: tests/result/content/format/20200215204700.html ================================================================== --- tests/result/content/format/20200215204700.html +++ tests/result/content/format/20200215204700.html @@ -0,0 +1,16 @@ +<p><i>italic</i> +<em>emph</em> +<b>bold</b> +<strong>strong</strong> +<u>unterline</u> +<s>strike</s> +<span style="font-family:monospace">monospace</span> +<sup>superscript</sup> +<sub>subscript</sub> +"Quotes" +<q>Quotation</q> +<small>small</small> +<span>span</span> +<code>code</code> +<kbd>input</kbd> +<samp>output</samp></p> ADDED tests/result/content/format/20200215204700.native Index: tests/result/content/format/20200215204700.native ================================================================== --- tests/result/content/format/20200215204700.native +++ tests/result/content/format/20200215204700.native @@ -0,0 +1,1 @@ +[Para Italic [Text "italic"],Space,Emph [Text "emph"],Space,Bold [Text "bold"],Space,Strong [Text "strong"],Space,Underline [Text "unterline"],Space,Strikethrough [Text "strike"],Space,Mono [Text "monospace"],Space,Super [Text "superscript"],Space,Sub [Text "subscript"],Space,Quote [Text "Quotes"],Space,Quotation [Text "Quotation"],Space,Small [Text "small"],Space,Span [Text "span"],Space,Code "code",Space,Input "input",Space,Output "output"] ADDED tests/result/content/format/20200215204700.text Index: tests/result/content/format/20200215204700.text ================================================================== --- tests/result/content/format/20200215204700.text +++ tests/result/content/format/20200215204700.text @@ -0,0 +1,1 @@ +italic emph bold strong unterline strike monospace superscript subscript Quotes Quotation small span code input output ADDED tests/result/content/format/20201107164400.djson Index: tests/result/content/format/20201107164400.djson ================================================================== --- tests/result/content/format/20201107164400.djson +++ tests/result/content/format/20201107164400.djson @@ -0,0 +1,1 @@ +[{"t":"Para","i":[{"t":"Span","a":{"lang":"fr"},"i":[{"t":"Quote","i":[{"t":"Text","s":"abc"}]}]}]}] ADDED tests/result/content/format/20201107164400.html Index: tests/result/content/format/20201107164400.html ================================================================== --- tests/result/content/format/20201107164400.html +++ tests/result/content/format/20201107164400.html @@ -0,0 +1,1 @@ +<p><span lang="fr">« abc »</span></p> ADDED tests/result/content/format/20201107164400.native Index: tests/result/content/format/20201107164400.native ================================================================== --- tests/result/content/format/20201107164400.native +++ tests/result/content/format/20201107164400.native @@ -0,0 +1,1 @@ +[Para Span ("",[lang="fr"]) [Quote [Text "abc"]]] ADDED tests/result/content/format/20201107164400.text Index: tests/result/content/format/20201107164400.text ================================================================== --- tests/result/content/format/20201107164400.text +++ tests/result/content/format/20201107164400.text @@ -0,0 +1,1 @@ +abc ADDED tests/result/content/heading/20200215204700.djson Index: tests/result/content/heading/20200215204700.djson ================================================================== --- tests/result/content/heading/20200215204700.djson +++ tests/result/content/heading/20200215204700.djson @@ -0,0 +1,1 @@ +[{"t":"Heading","n":2,"s":"first","i":[{"t":"Text","s":"First"}]}] ADDED tests/result/content/heading/20200215204700.html Index: tests/result/content/heading/20200215204700.html ================================================================== --- tests/result/content/heading/20200215204700.html +++ tests/result/content/heading/20200215204700.html @@ -0,0 +1,1 @@ +<h2 id="first">First</h2> ADDED tests/result/content/heading/20200215204700.native Index: tests/result/content/heading/20200215204700.native ================================================================== --- tests/result/content/heading/20200215204700.native +++ tests/result/content/heading/20200215204700.native @@ -0,0 +1,1 @@ +[Heading 2 "first" Text "First"] ADDED tests/result/content/heading/20200215204700.text Index: tests/result/content/heading/20200215204700.text ================================================================== --- tests/result/content/heading/20200215204700.text +++ tests/result/content/heading/20200215204700.text @@ -0,0 +1,1 @@ +First ADDED tests/result/content/hrule/20200215204700.djson Index: tests/result/content/hrule/20200215204700.djson ================================================================== --- tests/result/content/hrule/20200215204700.djson +++ tests/result/content/hrule/20200215204700.djson @@ -0,0 +1,1 @@ +[{"t":"Hrule"}] ADDED tests/result/content/hrule/20200215204700.html Index: tests/result/content/hrule/20200215204700.html ================================================================== --- tests/result/content/hrule/20200215204700.html +++ tests/result/content/hrule/20200215204700.html @@ -0,0 +1,1 @@ +<hr> ADDED tests/result/content/hrule/20200215204700.native Index: tests/result/content/hrule/20200215204700.native ================================================================== --- tests/result/content/hrule/20200215204700.native +++ tests/result/content/hrule/20200215204700.native @@ -0,0 +1,1 @@ +[Hrule] ADDED tests/result/content/hrule/20200215204700.text Index: tests/result/content/hrule/20200215204700.text ================================================================== --- tests/result/content/hrule/20200215204700.text +++ tests/result/content/hrule/20200215204700.text ADDED tests/result/content/image/20200215204700.djson Index: tests/result/content/image/20200215204700.djson ================================================================== --- tests/result/content/image/20200215204700.djson +++ tests/result/content/image/20200215204700.djson @@ -0,0 +1,1 @@ +[{"t":"Para","i":[{"t":"Image","s":"abc"}]}] ADDED tests/result/content/image/20200215204700.html Index: tests/result/content/image/20200215204700.html ================================================================== --- tests/result/content/image/20200215204700.html +++ tests/result/content/image/20200215204700.html @@ -0,0 +1,1 @@ +<p><img src="abc" alt=""></p> ADDED tests/result/content/image/20200215204700.native Index: tests/result/content/image/20200215204700.native ================================================================== --- tests/result/content/image/20200215204700.native +++ tests/result/content/image/20200215204700.native @@ -0,0 +1,1 @@ +[Para Image "abc"] ADDED tests/result/content/image/20200215204700.text Index: tests/result/content/image/20200215204700.text ================================================================== --- tests/result/content/image/20200215204700.text +++ tests/result/content/image/20200215204700.text ADDED tests/result/content/link/20200215204700.djson Index: tests/result/content/link/20200215204700.djson ================================================================== --- tests/result/content/link/20200215204700.djson +++ tests/result/content/link/20200215204700.djson @@ -0,0 +1,1 @@ +[{"t":"Para","i":[{"t":"Link","q":"external","s":"https://zettelstore.de/z","i":[{"t":"Text","s":"Home"}]},{"t":"Soft"},{"t":"Link","q":"external","s":"https://zettelstore.de","i":[{"t":"Text","s":"https://zettelstore.de"}]},{"t":"Soft"},{"t":"Link","q":"zettel","s":"00000000000100","i":[{"t":"Text","s":"Config"}]},{"t":"Soft"},{"t":"Link","q":"zettel","s":"00000000000100","i":[{"t":"Text","s":"00000000000100"}]},{"t":"Soft"},{"t":"Link","q":"self","s":"#frag","i":[{"t":"Text","s":"Frag"}]},{"t":"Soft"},{"t":"Link","q":"self","s":"#frag","i":[{"t":"Text","s":"#frag"}]},{"t":"Soft"},{"t":"Link","q":"local","s":"/hosted","i":[{"t":"Text","s":"H"}]},{"t":"Soft"},{"t":"Link","q":"based","s":"/based","i":[{"t":"Text","s":"B"}]},{"t":"Soft"},{"t":"Link","q":"local","s":"../rel","i":[{"t":"Text","s":"R"}]}]}] ADDED tests/result/content/link/20200215204700.html Index: tests/result/content/link/20200215204700.html ================================================================== --- tests/result/content/link/20200215204700.html +++ tests/result/content/link/20200215204700.html @@ -0,0 +1,9 @@ +<p><a href="https://zettelstore.de/z" class="zs-external">Home</a> +<a href="https://zettelstore.de" class="zs-external">https://zettelstore.de</a> +<a href="00000000000100">Config</a> +<a href="00000000000100">00000000000100</a> +<a href="#frag">Frag</a> +<a href="#frag">#frag</a> +<a href="/hosted">H</a> +<a href="/based">B</a> +<a href="../rel">R</a></p> ADDED tests/result/content/link/20200215204700.native Index: tests/result/content/link/20200215204700.native ================================================================== --- tests/result/content/link/20200215204700.native +++ tests/result/content/link/20200215204700.native @@ -0,0 +1,1 @@ +[Para Link EXTERNAL "https://zettelstore.de/z" [Text "Home"],Space,Link EXTERNAL "https://zettelstore.de" [],Space,Link ZETTEL "00000000000100" [Text "Config"],Space,Link ZETTEL "00000000000100" [],Space,Link SELF "#frag" [Text "Frag"],Space,Link SELF "#frag" [],Space,Link LOCAL "/hosted" [Text "H"],Space,Link BASED "/based" [Text "B"],Space,Link LOCAL "../rel" [Text "R"]] ADDED tests/result/content/link/20200215204700.text Index: tests/result/content/link/20200215204700.text ================================================================== --- tests/result/content/link/20200215204700.text +++ tests/result/content/link/20200215204700.text @@ -0,0 +1,1 @@ +Home Config Frag H B R ADDED tests/result/content/list/20200215204700.djson Index: tests/result/content/list/20200215204700.djson ================================================================== --- tests/result/content/list/20200215204700.djson +++ tests/result/content/list/20200215204700.djson @@ -0,0 +1,1 @@ +[{"t":"BulletList","c":[[{"t":"Para","i":[{"t":"Text","s":"Item"},{"t":"Space"},{"t":"Text","s":"1"}]}],[{"t":"Para","i":[{"t":"Text","s":"Item"},{"t":"Space"},{"t":"Text","s":"2"}]}],[{"t":"Para","i":[{"t":"Text","s":"Item"},{"t":"Space"},{"t":"Text","s":"3"}]}]]}] ADDED tests/result/content/list/20200215204700.html Index: tests/result/content/list/20200215204700.html ================================================================== --- tests/result/content/list/20200215204700.html +++ tests/result/content/list/20200215204700.html @@ -0,0 +1,5 @@ +<ul> +<li>Item 1</li> +<li>Item 2</li> +<li>Item 3</li> +</ul> ADDED tests/result/content/list/20200215204700.native Index: tests/result/content/list/20200215204700.native ================================================================== --- tests/result/content/list/20200215204700.native +++ tests/result/content/list/20200215204700.native @@ -0,0 +1,4 @@ +[BulletList + [[Para Text "Item",Space,Text "1"]], + [[Para Text "Item",Space,Text "2"]], + [[Para Text "Item",Space,Text "3"]]] ADDED tests/result/content/list/20200215204700.text Index: tests/result/content/list/20200215204700.text ================================================================== --- tests/result/content/list/20200215204700.text +++ tests/result/content/list/20200215204700.text @@ -0,0 +1,3 @@ +Item 1 +Item 2 +Item 3 ADDED tests/result/content/list/20200217194800.djson Index: tests/result/content/list/20200217194800.djson ================================================================== --- tests/result/content/list/20200217194800.djson +++ tests/result/content/list/20200217194800.djson @@ -0,0 +1,1 @@ +[{"t":"BulletList","c":[[{"t":"Para","i":[{"t":"Text","s":"Item1.1"}]}],[{"t":"Para","i":[{"t":"Text","s":"Item1.2"}]}],[{"t":"Para","i":[{"t":"Text","s":"Item1.3"}]}],[{"t":"Para","i":[{"t":"Text","s":"Item2.1"}]}],[{"t":"Para","i":[{"t":"Text","s":"Item2.2"}]}]]}] ADDED tests/result/content/list/20200217194800.html Index: tests/result/content/list/20200217194800.html ================================================================== --- tests/result/content/list/20200217194800.html +++ tests/result/content/list/20200217194800.html @@ -0,0 +1,7 @@ +<ul> +<li>Item1.1</li> +<li>Item1.2</li> +<li>Item1.3</li> +<li>Item2.1</li> +<li>Item2.2</li> +</ul> ADDED tests/result/content/list/20200217194800.native Index: tests/result/content/list/20200217194800.native ================================================================== --- tests/result/content/list/20200217194800.native +++ tests/result/content/list/20200217194800.native @@ -0,0 +1,6 @@ +[BulletList + [[Para Text "Item1.1"]], + [[Para Text "Item1.2"]], + [[Para Text "Item1.3"]], + [[Para Text "Item2.1"]], + [[Para Text "Item2.2"]]] ADDED tests/result/content/list/20200217194800.text Index: tests/result/content/list/20200217194800.text ================================================================== --- tests/result/content/list/20200217194800.text +++ tests/result/content/list/20200217194800.text @@ -0,0 +1,5 @@ +Item1.1 +Item1.2 +Item1.3 +Item2.1 +Item2.2 ADDED tests/result/content/list/20200516105700.djson Index: tests/result/content/list/20200516105700.djson ================================================================== --- tests/result/content/list/20200516105700.djson +++ tests/result/content/list/20200516105700.djson @@ -0,0 +1,1 @@ +[{"t":"BulletList","c":[[{"t":"Para","i":[{"t":"Text","s":"T1"}]},{"t":"BulletList","c":[[{"t":"Para","i":[{"t":"Text","s":"T2"}]}]]}],[{"t":"Para","i":[{"t":"Text","s":"T3"}]},{"t":"BulletList","c":[[{"t":"Para","i":[{"t":"Text","s":"T4"}]}]]}],[{"t":"Para","i":[{"t":"Text","s":"T5"}]}]]}] ADDED tests/result/content/list/20200516105700.html Index: tests/result/content/list/20200516105700.html ================================================================== --- tests/result/content/list/20200516105700.html +++ tests/result/content/list/20200516105700.html @@ -0,0 +1,14 @@ +<ul> +<li><p>T1</p> +<ul> +<li>T2</li> +</ul> +</li> +<li><p>T3</p> +<ul> +<li>T4</li> +</ul> +</li> +<li><p>T5</p> +</li> +</ul> ADDED tests/result/content/list/20200516105700.native Index: tests/result/content/list/20200516105700.native ================================================================== --- tests/result/content/list/20200516105700.native +++ tests/result/content/list/20200516105700.native @@ -0,0 +1,8 @@ +[BulletList + [[Para Text "T1"], + [BulletList + [[Para Text "T2"]]]], + [[Para Text "T3"], + [BulletList + [[Para Text "T4"]]]], + [[Para Text "T5"]]] ADDED tests/result/content/list/20200516105700.text Index: tests/result/content/list/20200516105700.text ================================================================== --- tests/result/content/list/20200516105700.text +++ tests/result/content/list/20200516105700.text @@ -0,0 +1,5 @@ +T1 +T2 +T3 +T4 +T5 ADDED tests/result/content/literal/20200215204700.djson Index: tests/result/content/literal/20200215204700.djson ================================================================== --- tests/result/content/literal/20200215204700.djson +++ tests/result/content/literal/20200215204700.djson @@ -0,0 +1,1 @@ +[{"t":"Para","i":[{"t":"Input","s":"input"},{"t":"Soft"},{"t":"Code","s":"program"},{"t":"Soft"},{"t":"Output","s":"output"}]}] ADDED tests/result/content/literal/20200215204700.html Index: tests/result/content/literal/20200215204700.html ================================================================== --- tests/result/content/literal/20200215204700.html +++ tests/result/content/literal/20200215204700.html @@ -0,0 +1,3 @@ +<p><kbd>input</kbd> +<code>program</code> +<samp>output</samp></p> ADDED tests/result/content/literal/20200215204700.native Index: tests/result/content/literal/20200215204700.native ================================================================== --- tests/result/content/literal/20200215204700.native +++ tests/result/content/literal/20200215204700.native @@ -0,0 +1,1 @@ +[Para Input "input",Space,Code "program",Space,Output "output"] ADDED tests/result/content/literal/20200215204700.text Index: tests/result/content/literal/20200215204700.text ================================================================== --- tests/result/content/literal/20200215204700.text +++ tests/result/content/literal/20200215204700.text @@ -0,0 +1,1 @@ +input program output ADDED tests/result/content/mark/20200215204700.djson Index: tests/result/content/mark/20200215204700.djson ================================================================== --- tests/result/content/mark/20200215204700.djson +++ tests/result/content/mark/20200215204700.djson @@ -0,0 +1,1 @@ +[{"t":"Para","i":[{"t":"Mark","s":"mark"}]}] ADDED tests/result/content/mark/20200215204700.html Index: tests/result/content/mark/20200215204700.html ================================================================== --- tests/result/content/mark/20200215204700.html +++ tests/result/content/mark/20200215204700.html @@ -0,0 +1,1 @@ +<p><a id="mark"></a></p> ADDED tests/result/content/mark/20200215204700.native Index: tests/result/content/mark/20200215204700.native ================================================================== --- tests/result/content/mark/20200215204700.native +++ tests/result/content/mark/20200215204700.native @@ -0,0 +1,1 @@ +[Para Mark "mark"] ADDED tests/result/content/mark/20200215204700.text Index: tests/result/content/mark/20200215204700.text ================================================================== --- tests/result/content/mark/20200215204700.text +++ tests/result/content/mark/20200215204700.text ADDED tests/result/content/paragraph/20200215185900.djson Index: tests/result/content/paragraph/20200215185900.djson ================================================================== --- tests/result/content/paragraph/20200215185900.djson +++ tests/result/content/paragraph/20200215185900.djson @@ -0,0 +1,1 @@ +[{"t":"Para","i":[{"t":"Text","s":"This"},{"t":"Space"},{"t":"Text","s":"is"},{"t":"Space"},{"t":"Text","s":"a"},{"t":"Space"},{"t":"Text","s":"zettel"},{"t":"Space"},{"t":"Text","s":"for"},{"t":"Space"},{"t":"Text","s":"testing."}]}] ADDED tests/result/content/paragraph/20200215185900.html Index: tests/result/content/paragraph/20200215185900.html ================================================================== --- tests/result/content/paragraph/20200215185900.html +++ tests/result/content/paragraph/20200215185900.html @@ -0,0 +1,1 @@ +<p>This is a zettel for testing.</p> ADDED tests/result/content/paragraph/20200215185900.native Index: tests/result/content/paragraph/20200215185900.native ================================================================== --- tests/result/content/paragraph/20200215185900.native +++ tests/result/content/paragraph/20200215185900.native @@ -0,0 +1,1 @@ +[Para Text "This",Space,Text "is",Space,Text "a",Space,Text "zettel",Space,Text "for",Space,Text "testing."] ADDED tests/result/content/paragraph/20200215185900.text Index: tests/result/content/paragraph/20200215185900.text ================================================================== --- tests/result/content/paragraph/20200215185900.text +++ tests/result/content/paragraph/20200215185900.text @@ -0,0 +1,1 @@ +This is a zettel for testing. ADDED tests/result/content/paragraph/20200217151800.djson Index: tests/result/content/paragraph/20200217151800.djson ================================================================== --- tests/result/content/paragraph/20200217151800.djson +++ tests/result/content/paragraph/20200217151800.djson @@ -0,0 +1,1 @@ +[{"t":"Para","i":[{"t":"Text","s":"Text"},{"t":"Space"},{"t":"Text","s":"Text"},{"t":"Soft"},{"t":"Text","s":"*abc"}]},{"t":"Para","i":[{"t":"Text","s":"Text"},{"t":"Space"},{"t":"Text","s":"Text"}]},{"t":"BulletList","c":[[{"t":"Para","i":[{"t":"Text","s":"abc"}]}]]}] ADDED tests/result/content/paragraph/20200217151800.html Index: tests/result/content/paragraph/20200217151800.html ================================================================== --- tests/result/content/paragraph/20200217151800.html +++ tests/result/content/paragraph/20200217151800.html @@ -0,0 +1,6 @@ +<p>Text Text +*abc</p> +<p>Text Text</p> +<ul> +<li>abc</li> +</ul> ADDED tests/result/content/paragraph/20200217151800.native Index: tests/result/content/paragraph/20200217151800.native ================================================================== --- tests/result/content/paragraph/20200217151800.native +++ tests/result/content/paragraph/20200217151800.native @@ -0,0 +1,4 @@ +[Para Text "Text",Space,Text "Text",Space,Text "*abc"], +[Para Text "Text",Space,Text "Text"], +[BulletList + [[Para Text "abc"]]] ADDED tests/result/content/paragraph/20200217151800.text Index: tests/result/content/paragraph/20200217151800.text ================================================================== --- tests/result/content/paragraph/20200217151800.text +++ tests/result/content/paragraph/20200217151800.text @@ -0,0 +1,3 @@ +Text Text *abc +Text Text +abc ADDED tests/result/content/png/20200512180900.djson Index: tests/result/content/png/20200512180900.djson ================================================================== --- tests/result/content/png/20200512180900.djson +++ tests/result/content/png/20200512180900.djson @@ -0,0 +1,1 @@ +[{"t":"Blob","q":"20200512180900","s":"png","o":"iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAAAAAA6fptVAAAACklEQVR4nGNiAAAABgADNjd8qAAAAABJRU5ErkJggg=="}] ADDED tests/result/content/png/20200512180900.html Index: tests/result/content/png/20200512180900.html ================================================================== --- tests/result/content/png/20200512180900.html +++ tests/result/content/png/20200512180900.html @@ -0,0 +1,1 @@ +<img src="data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAAAAAA6fptVAAAACklEQVR4nGNiAAAABgADNjd8qAAAAABJRU5ErkJggg==" title="20200512180900"> ADDED tests/result/content/png/20200512180900.native Index: tests/result/content/png/20200512180900.native ================================================================== --- tests/result/content/png/20200512180900.native +++ tests/result/content/png/20200512180900.native @@ -0,0 +1,1 @@ +[BLOB "20200512180900" "png" "iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAAAAAA6fptVAAAACklEQVR4nGNiAAAABgADNjd8qAAAAABJRU5ErkJggg=="] ADDED tests/result/content/png/20200512180900.text Index: tests/result/content/png/20200512180900.text ================================================================== --- tests/result/content/png/20200512180900.text +++ tests/result/content/png/20200512180900.text ADDED tests/result/content/quoteblock/20200215204700.djson Index: tests/result/content/quoteblock/20200215204700.djson ================================================================== --- tests/result/content/quoteblock/20200215204700.djson +++ tests/result/content/quoteblock/20200215204700.djson @@ -0,0 +1,1 @@ +[{"t":"QuoteBlock","b":[{"t":"Para","i":[{"t":"Text","s":"To"},{"t":"Space"},{"t":"Text","s":"be"},{"t":"Space"},{"t":"Text","s":"or"},{"t":"Space"},{"t":"Text","s":"not"},{"t":"Space"},{"t":"Text","s":"to"},{"t":"Space"},{"t":"Text","s":"be."}]}],"i":[{"t":"Text","s":"Romeo"}]}] ADDED tests/result/content/quoteblock/20200215204700.html Index: tests/result/content/quoteblock/20200215204700.html ================================================================== --- tests/result/content/quoteblock/20200215204700.html +++ tests/result/content/quoteblock/20200215204700.html @@ -0,0 +1,4 @@ +<blockquote> +<p>To be or not to be.</p> +<cite>Romeo</cite> +</blockquote> ADDED tests/result/content/quoteblock/20200215204700.native Index: tests/result/content/quoteblock/20200215204700.native ================================================================== --- tests/result/content/quoteblock/20200215204700.native +++ tests/result/content/quoteblock/20200215204700.native @@ -0,0 +1,3 @@ +[QuoteBlock + [[Para Text "To",Space,Text "be",Space,Text "or",Space,Text "not",Space,Text "to",Space,Text "be."]], + [Cite Text "Romeo"]] ADDED tests/result/content/quoteblock/20200215204700.text Index: tests/result/content/quoteblock/20200215204700.text ================================================================== --- tests/result/content/quoteblock/20200215204700.text +++ tests/result/content/quoteblock/20200215204700.text @@ -0,0 +1,2 @@ +To be or not to be. +Romeo ADDED tests/result/content/spanblock/20200215204700.djson Index: tests/result/content/spanblock/20200215204700.djson ================================================================== --- tests/result/content/spanblock/20200215204700.djson +++ tests/result/content/spanblock/20200215204700.djson @@ -0,0 +1,1 @@ +[{"t":"SpanBlock","b":[{"t":"Para","i":[{"t":"Text","s":"A"},{"t":"Space"},{"t":"Text","s":"simple"},{"t":"Soft"},{"t":"Space","n":3},{"t":"Text","s":"span"},{"t":"Soft"},{"t":"Text","s":"and"},{"t":"Space"},{"t":"Text","s":"much"},{"t":"Space"},{"t":"Text","s":"more"}]}]}] ADDED tests/result/content/spanblock/20200215204700.html Index: tests/result/content/spanblock/20200215204700.html ================================================================== --- tests/result/content/spanblock/20200215204700.html +++ tests/result/content/spanblock/20200215204700.html @@ -0,0 +1,5 @@ +<div> +<p>A simple + span +and much more</p> +</div> ADDED tests/result/content/spanblock/20200215204700.native Index: tests/result/content/spanblock/20200215204700.native ================================================================== --- tests/result/content/spanblock/20200215204700.native +++ tests/result/content/spanblock/20200215204700.native @@ -0,0 +1,2 @@ +[SpanBlock + [[Para Text "A",Space,Text "simple",Space,Space 3,Text "span",Space,Text "and",Space,Text "much",Space,Text "more"]]] ADDED tests/result/content/spanblock/20200215204700.text Index: tests/result/content/spanblock/20200215204700.text ================================================================== --- tests/result/content/spanblock/20200215204700.text +++ tests/result/content/spanblock/20200215204700.text @@ -0,0 +1,1 @@ +A simple span and much more ADDED tests/result/content/table/20200215204700.djson Index: tests/result/content/table/20200215204700.djson ================================================================== --- tests/result/content/table/20200215204700.djson +++ tests/result/content/table/20200215204700.djson @@ -0,0 +1,1 @@ +[{"t":"Table","p":[[],[[["",[{"t":"Text","s":"c1"}]],["",[{"t":"Text","s":"c2"}]],["",[{"t":"Text","s":"c3"}]]]]]}] ADDED tests/result/content/table/20200215204700.html Index: tests/result/content/table/20200215204700.html ================================================================== --- tests/result/content/table/20200215204700.html +++ tests/result/content/table/20200215204700.html @@ -0,0 +1,5 @@ +<table> +<tbody> +<tr><td>c1</td><td>c2</td><td>c3</td></tr> +</tbody> +</table> ADDED tests/result/content/table/20200215204700.native Index: tests/result/content/table/20200215204700.native ================================================================== --- tests/result/content/table/20200215204700.native +++ tests/result/content/table/20200215204700.native @@ -0,0 +1,2 @@ +[Table + [Row [Cell Default Text "c1"],[Cell Default Text "c2"],[Cell Default Text "c3"]]] ADDED tests/result/content/table/20200215204700.text Index: tests/result/content/table/20200215204700.text ================================================================== --- tests/result/content/table/20200215204700.text +++ tests/result/content/table/20200215204700.text @@ -0,0 +1,1 @@ +c1 c2 c3 ADDED tests/result/content/table/20200618140700.djson Index: tests/result/content/table/20200618140700.djson ================================================================== --- tests/result/content/table/20200618140700.djson +++ tests/result/content/table/20200618140700.djson @@ -0,0 +1,1 @@ +[{"t":"Table","p":[[[">",[{"t":"Text","s":"h1"}]],["",[{"t":"Text","s":"h2"}]],[":",[{"t":"Text","s":"h3"}]]],[[["<",[{"t":"Text","s":"c1"}]],["",[{"t":"Text","s":"c2"}]],[":",[{"t":"Text","s":"c3"}]]],[[">",[{"t":"Text","s":"f1"}]],["",[{"t":"Text","s":"f2"}]],[":",[{"t":"Text","s":"=f3"}]]]]]}] ADDED tests/result/content/table/20200618140700.html Index: tests/result/content/table/20200618140700.html ================================================================== --- tests/result/content/table/20200618140700.html +++ tests/result/content/table/20200618140700.html @@ -0,0 +1,9 @@ +<table> +<thead> +<tr><th style="text-align:right">h1</th><th>h2</th><th style="text-align:center">h3</th></tr> +</thead> +<tbody> +<tr><td style="text-align:left">c1</td><td>c2</td><td style="text-align:center">c3</td></tr> +<tr><td style="text-align:right">f1</td><td>f2</td><td style="text-align:center">=f3</td></tr> +</tbody> +</table> ADDED tests/result/content/table/20200618140700.native Index: tests/result/content/table/20200618140700.native ================================================================== --- tests/result/content/table/20200618140700.native +++ tests/result/content/table/20200618140700.native @@ -0,0 +1,4 @@ +[Table + [Header [Cell Right Text "h1"],[Cell Default Text "h2"],[Cell Center Text "h3"]], + [Row [Cell Left Text "c1"],[Cell Default Text "c2"],[Cell Center Text "c3"]], + [Row [Cell Right Text "f1"],[Cell Default Text "f2"],[Cell Center Text "=f3"]]] ADDED tests/result/content/table/20200618140700.text Index: tests/result/content/table/20200618140700.text ================================================================== --- tests/result/content/table/20200618140700.text +++ tests/result/content/table/20200618140700.text @@ -0,0 +1,3 @@ +h1 h2 h3 +c1 c2 c3 +f1 f2 =f3 ADDED tests/result/content/verbatim/20200215204700.djson Index: tests/result/content/verbatim/20200215204700.djson ================================================================== --- tests/result/content/verbatim/20200215204700.djson +++ tests/result/content/verbatim/20200215204700.djson @@ -0,0 +1,1 @@ +[{"t":"CodeBlock","l":["if __name__ == \"main\":"," print(\"Hello, World\")","exit(0)"]}] ADDED tests/result/content/verbatim/20200215204700.html Index: tests/result/content/verbatim/20200215204700.html ================================================================== --- tests/result/content/verbatim/20200215204700.html +++ tests/result/content/verbatim/20200215204700.html @@ -0,0 +1,4 @@ +<pre><code>if __name__ == "main": + print("Hello, World") +exit(0) +</code></pre> ADDED tests/result/content/verbatim/20200215204700.native Index: tests/result/content/verbatim/20200215204700.native ================================================================== --- tests/result/content/verbatim/20200215204700.native +++ tests/result/content/verbatim/20200215204700.native @@ -0,0 +1,1 @@ +[CodeBlock "if __name__ == \"main\":\n print(\"Hello, World\")\nexit(0)"] ADDED tests/result/content/verbatim/20200215204700.text Index: tests/result/content/verbatim/20200215204700.text ================================================================== --- tests/result/content/verbatim/20200215204700.text +++ tests/result/content/verbatim/20200215204700.text @@ -0,0 +1,3 @@ +if __name__ == "main": + print("Hello, World") +exit(0) ADDED tests/result/content/verseblock/20200215204700.djson Index: tests/result/content/verseblock/20200215204700.djson ================================================================== --- tests/result/content/verseblock/20200215204700.djson +++ tests/result/content/verseblock/20200215204700.djson @@ -0,0 +1,1 @@ +[{"t":"VerseBlock","b":[{"t":"Para","i":[{"t":"Text","s":"A line"},{"t":"Hard"},{"t":"Text","s":"  another line"},{"t":"Hard"},{"t":"Text","s":"Back"}]},{"t":"Para","i":[{"t":"Text","s":"Paragraph"}]},{"t":"Para","i":[{"t":"Text","s":"    Spacy  Para"}]}],"i":[{"t":"Text","s":"Author"}]}] ADDED tests/result/content/verseblock/20200215204700.html Index: tests/result/content/verseblock/20200215204700.html ================================================================== --- tests/result/content/verseblock/20200215204700.html +++ tests/result/content/verseblock/20200215204700.html @@ -0,0 +1,8 @@ +<div> +<p>A line<br> +  another line<br> +Back</p> +<p>Paragraph</p> +<p>    Spacy  Para</p> +<cite>Author</cite> +</div> ADDED tests/result/content/verseblock/20200215204700.native Index: tests/result/content/verseblock/20200215204700.native ================================================================== --- tests/result/content/verseblock/20200215204700.native +++ tests/result/content/verseblock/20200215204700.native @@ -0,0 +1,5 @@ +[VerseBlock + [[Para Text "A line",Break,Text "  another line",Break,Text "Back"], + [Para Text "Paragraph"], + [Para Text "    Spacy  Para"]], + [Cite Text "Author"]] ADDED tests/result/content/verseblock/20200215204700.text Index: tests/result/content/verseblock/20200215204700.text ================================================================== --- tests/result/content/verseblock/20200215204700.text +++ tests/result/content/verseblock/20200215204700.text @@ -0,0 +1,6 @@ +A line +  another line +Back +Paragraph +    Spacy  Para +Author ADDED tests/result/meta/copyright/20200310125800.djson Index: tests/result/meta/copyright/20200310125800.djson ================================================================== --- tests/result/meta/copyright/20200310125800.djson +++ tests/result/meta/copyright/20200310125800.djson @@ -0,0 +1,1 @@ +{"title":[{"t":"Text","s":"Header"},{"t":"Space"},{"t":"Text","s":"Test"}],"role":"zettel","syntax":"zmk","copyright":"(c) 2020 Detlef Stern","license":"CC BY-SA 4.0"} DELETED tests/result/meta/copyright/20200310125800.zjson Index: tests/result/meta/copyright/20200310125800.zjson ================================================================== --- tests/result/meta/copyright/20200310125800.zjson +++ tests/result/meta/copyright/20200310125800.zjson @@ -1,1 +0,0 @@ -{"title":[{"t":"Text","s":"Header"},{"t":"Space"},{"t":"Text","s":"Test"}],"role":"zettel","syntax":"zmk","copyright":"(c) 2020 Detlef Stern","license":"CC BY-SA 4.0"} ADDED tests/result/meta/header/20200310125800.djson Index: tests/result/meta/header/20200310125800.djson ================================================================== --- tests/result/meta/header/20200310125800.djson +++ tests/result/meta/header/20200310125800.djson @@ -0,0 +1,1 @@ +{"title":[{"t":"Text","s":"Header"},{"t":"Space"},{"t":"Text","s":"Test"}],"role":"zettel","syntax":"zmk","x-no":"00000000000000"} DELETED tests/result/meta/header/20200310125800.zjson Index: tests/result/meta/header/20200310125800.zjson ================================================================== --- tests/result/meta/header/20200310125800.zjson +++ tests/result/meta/header/20200310125800.zjson @@ -1,1 +0,0 @@ -{"title":[{"t":"Text","s":"Header"},{"t":"Space"},{"t":"Text","s":"Test"}],"role":"zettel","syntax":"zmk","x-no":"00000000000000"} ADDED tests/result/meta/title/20200310110300.djson Index: tests/result/meta/title/20200310110300.djson ================================================================== --- tests/result/meta/title/20200310110300.djson +++ tests/result/meta/title/20200310110300.djson @@ -0,0 +1,1 @@ +{"title":[{"t":"Text","s":"A"},{"t":"Space"},{"t":"Quote","i":[{"t":"Text","s":"Title"}]},{"t":"Space"},{"t":"Text","s":"with"},{"t":"Space"},{"t":"Italic","i":[{"t":"Text","s":"Markup"}]},{"t":"Text","s":","},{"t":"Space"},{"t":"Code","a":{"":"zmk"},"s":"Zettelmarkup"}],"role":"zettel","syntax":"zmk"} Index: tests/result/meta/title/20200310110300.native ================================================================== --- tests/result/meta/title/20200310110300.native +++ tests/result/meta/title/20200310110300.native @@ -1,3 +1,3 @@ -[Title Text "A",Space,Quote [Text "Title"],Space,Text "with",Space,Emph [Text "Markup"],Text ",",Space,Code ("zmk",[]) "Zettelmarkup"] +[Title Text "A",Space,Quote [Text "Title"],Space,Text "with",Space,Italic [Text "Markup"],Text ",",Space,Code ("zmk",[]) "Zettelmarkup"] [Role "zettel"] [Syntax "zmk"] DELETED tests/result/meta/title/20200310110300.zjson Index: tests/result/meta/title/20200310110300.zjson ================================================================== --- tests/result/meta/title/20200310110300.zjson +++ tests/result/meta/title/20200310110300.zjson @@ -1,1 +0,0 @@ -{"title":[{"t":"Text","s":"A"},{"t":"Space"},{"t":"Quote","i":[{"t":"Text","s":"Title"}]},{"t":"Space"},{"t":"Text","s":"with"},{"t":"Space"},{"t":"Emph","i":[{"t":"Text","s":"Markup"}]},{"t":"Text","s":","},{"t":"Space"},{"t":"Code","a":{"":"zmk"},"s":"Zettelmarkup"}],"role":"zettel","syntax":"zmk"} ADDED tools/build.go Index: tools/build.go ================================================================== --- tools/build.go +++ tools/build.go @@ -0,0 +1,538 @@ +//----------------------------------------------------------------------------- +// Copyright (c) 2021 Detlef Stern +// +// This file is part of zettelstore. +// +// Zettelstore is licensed under the latest version of the EUPL (European Union +// Public License). Please see file LICENSE.txt for your rights and obligations +// under this license. +//----------------------------------------------------------------------------- + +// Package 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" + "regexp" + "strings" + "time" + + "zettelstore.de/z/strfun" +) + +func executeCommand(env []string, name string, arg ...string) (string, error) { + logCommand("EXEC", env, name, arg) + var out bytes.Buffer + 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 +} + +var fossilCheckout = regexp.MustCompile(`^checkout:\s+([0-9a-f]+)\s`) +var dirtyPrefixes = []string{ + "DELETED ", "ADDED ", "UPDATED ", "CONFLICT ", "EDITED ", "RENAMED ", "EXTRA "} + +const dirtySuffix = "-dirty" + +func readFossilVersion() (string, error) { + s, err := executeCommand(nil, "fossil", "status", "--differ") + if err != nil { + return "", err + } + var hash, suffix string + for _, line := range strfun.SplitLines(s) { + if hash == "" { + if m := fossilCheckout.FindStringSubmatch(line); len(m) > 0 { + hash = m[1][:10] + if suffix != "" { + return hash + suffix, nil + } + continue + } + } + if suffix == "" { + for _, prefix := range dirtyPrefixes { + if strings.HasPrefix(line, prefix) { + suffix = dirtySuffix + if hash != "" { + return hash + suffix, nil + } + break + } + } + } + } + return hash, nil +} + +func getVersionData() (string, string) { + base, err := readVersionFile() + if err != nil { + base = "dev" + } + fossil, err := readFossilVersion() + if err != nil { + return base, "" + } + return base, fossil +} + +func calcVersion(base, vcs string) string { return base + "+" + vcs } + +func getVersion() string { + base, vcs := getVersionData() + return calcVersion(base, vcs) +} + +func findExec(cmd string) string { + if path, err := executeCommand(nil, "which", "shadow"); err == nil && path != "" { + return path + } + return "" +} + +func cmdCheck() error { + if err := checkGoTest("./..."); err != nil { + return err + } + if err := checkGoVet(); err != nil { + return err + } + if err := checkGoLint(); err != nil { + return err + } + if err := checkGoVetShadow(); err != nil { + return err + } + if err := checkStaticcheck(); err != nil { + return err + } + return checkFossilExtra() +} + +func checkGoTest(pkg string, testParams ...string) error { + args := []string{"test", pkg} + args = append(args, testParams...) + out, err := executeCommand(nil, "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(nil, "go", "vet", "./...") + if err != nil { + fmt.Fprintln(os.Stderr, "Some checks failed") + if len(out) > 0 { + fmt.Fprintln(os.Stderr, out) + } + } + return err +} + +func checkGoLint() error { + out, err := executeCommand(nil, "golint", "./...") + if out != "" { + fmt.Fprintln(os.Stderr, "Some lints failed") + fmt.Fprint(os.Stderr, out) + } + return err +} + +func checkGoVetShadow() error { + path := findExec("shadow") + if path == "" { + return nil + } + out, err := executeCommand(nil, "go", "vet", "-vettool", strings.TrimSpace(path), "./...") + if err != nil { + fmt.Fprintln(os.Stderr, "Some shadowed variables found") + if len(out) > 0 { + fmt.Fprintln(os.Stderr, out) + } + } + return err +} +func checkStaticcheck() error { + out, err := executeCommand(nil, "staticcheck", "./...") + if err != nil { + fmt.Fprintln(os.Stderr, "Some staticcheck problems found") + if len(out) > 0 { + fmt.Fprintln(os.Stderr, out) + } + } + return err +} + +func checkFossilExtra() error { + out, err := executeCommand(nil, "fossil", "extra") + if err != nil { + fmt.Fprintln(os.Stderr, "Unable to execute 'fossil extra'") + return err + } + if len(out) > 0 { + 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 bytes.Buffer + 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/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(nil, name, arg, &info.out) + if !verbose { + cmd.Stderr = nil + } + err := cmd.Start() + for i := 0; i < 100; i++ { + time.Sleep(time.Millisecond * 100) + if addressInUse(info.adminAddress) { + info.cmd = cmd + return err + } + } + 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(nil, getVersion(), "bin/zettelstore") +} + +func doBuild(env []string, version, target string) error { + 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 +} + +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 + } + fh.Name = entry.Name() + fh.Method = zip.Deflate + w, err := zipWriter.CreateHeader(fh) + if err != nil { + return err + } + manualFile, err := os.Open(filepath.Join(path, entry.Name())) + if err != nil { + return err + } + defer manualFile.Close() + _, err = io.Copy(w, manualFile) + return err +} + +func getReleaseVersionData() (string, string) { + base, fossil := getVersionData() + if strings.HasSuffix(base, "dev") { + base = base[:len(base)-3] + "preview-" + time.Now().Format("20060102") + } + if strings.HasSuffix(fossil, dirtySuffix) { + fmt.Fprintf(os.Stderr, "Warning: releasing a dirty version %v\n", fossil) + base = base + dirtySuffix + } + return base, fossil +} + +func cmdRelease() error { + if err := cmdCheck(); err != nil { + return err + } + base, fossil := getReleaseVersionData() + releases := []struct { + arch string + os string + env []string + name string + }{ + {"amd64", "linux", nil, "zettelstore"}, + {"arm", "linux", []string{"GOARM=6"}, "zettelstore"}, + {"amd64", "darwin", nil, "iZettelstore"}, + {"arm64", "darwin", nil, "iZettelstore"}, + {"amd64", "windows", nil, "zettelstore.exe"}, + } + for _, rel := range releases { + env := append(rel.env, "GOARCH="+rel.arch, "GOOS="+rel.os) + zsName := filepath.Join("releases", rel.name) + if err := doBuild(env, calcVersion(base, fossil), 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 cmdClean() error { + for _, dir := range []string{"bin", "releases"} { + err := os.RemoveAll(dir) + if err != nil { + return err + } + } + 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 Outputs this text. + manual Create a ZIP file with all manual zettel + release Create the software for various platforms and put them in + appropriate named ZIP files. + testapi Starts a Zettelstore and execute API tests. + version Print the current version of the software. + +All commands can be abbreviated as long as they remain unique.`) +} + +var ( + verbose bool +) + +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() + case "t", "te", "tes", "test", "testa", "testap", "testapi": + cmdTestAPI() + case "h", "he", "hel", "help": + cmdHelp() + default: + fmt.Fprintf(os.Stderr, "Unknown command %q\n", args[0]) + cmdHelp() + os.Exit(1) + } + } + if err != nil { + fmt.Fprintln(os.Stderr, err) + } +} DELETED tools/build/build.go Index: tools/build/build.go ================================================================== --- tools/build/build.go +++ tools/build/build.go @@ -1,329 +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. -// -// 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" - - "t73f.de/r/zsc/api" - "t73f.de/r/zsc/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 -} DELETED tools/check/check.go Index: tools/check/check.go ================================================================== --- tools/check/check.go +++ tools/check/check.go @@ -1,35 +0,0 @@ -//----------------------------------------------------------------------------- -// 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) - } -} DELETED tools/clean/clean.go Index: tools/clean/clean.go ================================================================== --- tools/clean/clean.go +++ tools/clean/clean.go @@ -1,56 +0,0 @@ -//----------------------------------------------------------------------------- -// 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 -} DELETED tools/devtools/devtools.go Index: tools/devtools/devtools.go ================================================================== --- tools/devtools/devtools.go +++ tools/devtools/devtools.go @@ -1,60 +0,0 @@ -//----------------------------------------------------------------------------- -// 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 -} DELETED tools/htmllint/htmllint.go Index: tools/htmllint/htmllint.go ================================================================== --- tools/htmllint/htmllint.go +++ tools/htmllint/htmllint.go @@ -1,205 +0,0 @@ -//----------------------------------------------------------------------------- -// 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" - - "t73f.de/r/zsc/api" - "t73f.de/r/zsc/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] == "<search> is not recognized!" { - continue - } - } - } - if matches[1] != "Warning" { - result = append(result, "???"+line) - continue - } - if len(matches) > 2 { - switch matches[2] { - case "discarding unexpected <search>", - "discarding unexpected </search>", - `<input> proprietary attribute "inputmode"`: - continue - } - } - result = append(result, line) - } - return result -} DELETED tools/testapi/testapi.go Index: tools/testapi/testapi.go ================================================================== --- tools/testapi/testapi.go +++ tools/testapi/testapi.go @@ -1,108 +0,0 @@ -//----------------------------------------------------------------------------- -// 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 -} DELETED tools/tools.go Index: tools/tools.go ================================================================== --- tools/tools.go +++ tools/tools.go @@ -1,214 +0,0 @@ -//----------------------------------------------------------------------------- -// 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,t73f.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 @@ -1,83 +1,76 @@ //----------------------------------------------------------------------------- -// Copyright (c) 2020-present Detlef Stern +// Copyright (c) 2020-2021 Detlef Stern // -// This file is part of Zettelstore. +// This file is part of zettelstore. // // Zettelstore is licensed under the latest version of the EUPL (European Union // Public License). Please see file LICENSE.txt for your rights and obligations // under this license. -// -// SPDX-License-Identifier: EUPL-1.2 -// SPDX-FileCopyrightText: 2020-present Detlef Stern //----------------------------------------------------------------------------- +// Package usecase provides (business) use cases for the zettelstore. package usecase import ( "context" - "math/rand/v2" - "net/http" + "math/rand" "time" - "t73f.de/r/zsc/api" "zettelstore.de/z/auth" "zettelstore.de/z/auth/cred" - "zettelstore.de/z/logger" - "zettelstore.de/z/zettel/id" - "zettelstore.de/z/zettel/meta" + "zettelstore.de/z/domain/id" + "zettelstore.de/z/domain/meta" + "zettelstore.de/z/search" ) + +// AuthenticatePort is the interface used by this use case. +type AuthenticatePort interface { + GetMeta(ctx context.Context, zid id.Zid) (*meta.Meta, error) + SelectMeta(ctx context.Context, s *search.Search) ([]*meta.Meta, error) +} // Authenticate is the data for this use case. type Authenticate struct { - log *logger.Logger token auth.TokenManager - ucGetUser *GetUser + port AuthenticatePort + ucGetUser GetUser } // NewAuthenticate creates a new use case. -func NewAuthenticate(log *logger.Logger, token auth.TokenManager, ucGetUser *GetUser) Authenticate { +func NewAuthenticate(token auth.TokenManager, authz auth.AuthzManager, port AuthenticatePort) Authenticate { return Authenticate{ - log: log, token: token, - ucGetUser: ucGetUser, + port: port, + ucGetUser: NewGetUser(authz, port), } } // Run executes the use case. -// -// Parameter "r" is just included to produce better logging messages. It may be nil. Do not use it -// for other purposes. -func (uc *Authenticate) Run(ctx context.Context, r *http.Request, ident, credential string, d time.Duration, k auth.TokenKind) ([]byte, error) { +func (uc Authenticate) Run(ctx context.Context, ident, credential string, d time.Duration, k auth.TokenKind) ([]byte, error) { identMeta, err := uc.ucGetUser.Run(ctx, ident) defer addDelay(time.Now(), 500*time.Millisecond, 100*time.Millisecond) if identMeta == nil || err != nil { - uc.log.Info().Str("ident", ident).Err(err).HTTPIP(r).Msg("No user with given ident found") compensateCompare() return nil, err } - if hashCred, ok := identMeta.Get(api.KeyCredential); ok { - ok, err = cred.CompareHashAndCredential(hashCred, identMeta.Zid, ident, credential) + if hashCred, ok := identMeta.Get(meta.KeyCredential); ok { + ok, err := cred.CompareHashAndCredential(hashCred, identMeta.Zid, ident, credential) if err != nil { - uc.log.Info().Str("ident", ident).Err(err).HTTPIP(r).Msg("Error while comparing credentials") return nil, err } if ok { - token, err2 := uc.token.GetToken(identMeta, d, k) - if err2 != nil { - uc.log.Info().Str("ident", ident).Err(err).Msg("Unable to produce authentication token") - return nil, err2 + token, err := uc.token.GetToken(identMeta, d, k) + if err != nil { + return nil, err } - uc.log.Info().Str("user", ident).Msg("Successful") return token, nil } - uc.log.Info().Str("ident", ident).HTTPIP(r).Msg("Credentials don't match") return nil, nil } - uc.log.Info().Str("ident", ident).Msg("No credential stored") compensateCompare() return nil, nil } // compensateCompare if normal comapare is not possible, to avoid timing hints. @@ -88,58 +81,12 @@ // 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) } } - -// IsAuthenticatedPort contains method for this usecase. -type IsAuthenticatedPort interface { - GetUser(context.Context) *meta.Meta -} - -// IsAuthenticated cheks if the caller is alrwady authenticated. -type IsAuthenticated struct { - log *logger.Logger - port IsAuthenticatedPort - authz auth.AuthzManager -} - -// NewIsAuthenticated creates a new use case object. -func NewIsAuthenticated(log *logger.Logger, port IsAuthenticatedPort, authz auth.AuthzManager) IsAuthenticated { - return IsAuthenticated{ - log: log, - port: port, - authz: authz, - } -} - -// IsAuthenticatedResult is an enumeration. -type IsAuthenticatedResult uint8 - -// Values for IsAuthenticatedResult. -const ( - _ IsAuthenticatedResult = iota - IsAuthenticatedDisabled - IsAuthenticatedAndValid - IsAuthenticatedAndInvalid -) - -// Run executes the use case. -func (uc *IsAuthenticated) Run(ctx context.Context) IsAuthenticatedResult { - if !uc.authz.WithAuth() { - uc.log.Info().Str("auth", "disabled").Msg("IsAuthenticated") - return IsAuthenticatedDisabled - } - if uc.port.GetUser(ctx) == nil { - uc.log.Info().Msg("IsAuthenticated is false") - return IsAuthenticatedAndInvalid - } - uc.log.Info().Msg("IsAuthenticated is true") - return IsAuthenticatedAndValid -} ADDED usecase/context.go Index: usecase/context.go ================================================================== --- usecase/context.go +++ usecase/context.go @@ -0,0 +1,157 @@ +//----------------------------------------------------------------------------- +// Copyright (c) 2021 Detlef Stern +// +// This file is part of zettelstore. +// +// Zettelstore is licensed under the latest version of the EUPL (European Union +// Public License). Please see file LICENSE.txt for your rights and obligations +// under this license. +//----------------------------------------------------------------------------- + +// Package usecase provides (business) use cases for the Zettelstore. +package usecase + +import ( + "context" + + "zettelstore.de/z/domain/id" + "zettelstore.de/z/domain/meta" +) + +// ZettelContextPort is the interface used by this use case. +type ZettelContextPort interface { + // GetMeta retrieves just the meta data of a specific zettel. + GetMeta(ctx context.Context, zid id.Zid) (*meta.Meta, error) +} + +// ZettelContext is the data for this use case. +type ZettelContext struct { + port ZettelContextPort +} + +// NewZettelContext creates a new use case. +func NewZettelContext(port ZettelContextPort) ZettelContext { + return ZettelContext{port: port} +} + +// ZettelContextDirection determines the way, the context is calculated. +type ZettelContextDirection int + +// Constant values for ZettelContextDirection +const ( + _ ZettelContextDirection = iota + ZettelContextForward // Traverse all forwarding links + ZettelContextBackward // Traverse all backwaring links + ZettelContextBoth // Traverse both directions +) + +// Run executes the use case. +func (uc ZettelContext) Run(ctx context.Context, zid id.Zid, dir ZettelContextDirection, depth, limit int) (result []*meta.Meta, err error) { + start, err := uc.port.GetMeta(ctx, zid) + if err != nil { + return nil, err + } + tasks := ztlCtx{depth: depth} + uc.addInitialTasks(ctx, &tasks, start) + visited := id.NewSet() + isBackward := dir == ZettelContextBoth || dir == ZettelContextBackward + isForward := dir == ZettelContextBoth || dir == ZettelContextForward + for !tasks.empty() { + m, curDepth := tasks.pop() + if _, ok := visited[m.Zid]; ok { + continue + } + visited[m.Zid] = true + result = append(result, m) + if limit > 0 && len(result) > limit { // start is the first element of result + break + } + curDepth++ + for _, p := range m.PairsRest(true) { + if p.Key == meta.KeyBackward { + if isBackward { + uc.addIDSet(ctx, &tasks, curDepth, p.Value) + } + continue + } + if p.Key == meta.KeyForward { + if isForward { + uc.addIDSet(ctx, &tasks, curDepth, p.Value) + } + continue + } + if p.Key != meta.KeyBack { + hasInverse := meta.Inverse(p.Key) != "" + if (!hasInverse || !isBackward) && (hasInverse || !isForward) { + continue + } + if t := meta.Type(p.Key); t == meta.TypeID { + uc.addID(ctx, &tasks, curDepth, p.Value) + } else if t == meta.TypeIDSet { + uc.addIDSet(ctx, &tasks, curDepth, p.Value) + } + } + } + } + return result, nil +} + +func (uc ZettelContext) addInitialTasks(ctx context.Context, tasks *ztlCtx, start *meta.Meta) { + tasks.add(start, 0) +} + +func (uc ZettelContext) addID(ctx context.Context, tasks *ztlCtx, depth int, value string) { + if zid, err := id.Parse(value); err == nil { + if m, err := uc.port.GetMeta(ctx, zid); err == nil { + tasks.add(m, depth) + } + } +} + +func (uc ZettelContext) addIDSet(ctx context.Context, tasks *ztlCtx, depth int, value string) { + for _, val := range meta.ListFromValue(value) { + uc.addID(ctx, tasks, depth, val) + } +} + +type ztlCtxTask struct { + next *ztlCtxTask + meta *meta.Meta + depth int +} + +type ztlCtx struct { + first *ztlCtxTask + last *ztlCtxTask + depth int +} + +func (zc *ztlCtx) add(m *meta.Meta, depth int) { + if zc.depth > 0 && depth > zc.depth { + return + } + task := &ztlCtxTask{next: nil, meta: m, depth: depth} + if zc.first == nil { + zc.first = task + zc.last = task + } else { + zc.last.next = task + zc.last = task + } +} + +func (zc *ztlCtx) empty() bool { + return zc.first == nil +} + +func (zc *ztlCtx) pop() (*meta.Meta, int) { + task := zc.first + if task == nil { + return nil, -1 + } + zc.first = task.next + if zc.first == nil { + zc.last = nil + } + return task.meta, task.depth +} ADDED usecase/copy_zettel.go Index: usecase/copy_zettel.go ================================================================== --- usecase/copy_zettel.go +++ usecase/copy_zettel.go @@ -0,0 +1,41 @@ +//----------------------------------------------------------------------------- +// Copyright (c) 2020-2021 Detlef Stern +// +// This file is part of zettelstore. +// +// Zettelstore is licensed under the latest version of the EUPL (European Union +// Public License). Please see file LICENSE.txt for your rights and obligations +// under this license. +//----------------------------------------------------------------------------- + +// Package usecase provides (business) use cases for the zettelstore. +package usecase + +import ( + "zettelstore.de/z/domain" + "zettelstore.de/z/domain/meta" +) + +// CopyZettel is the data for this use case. +type CopyZettel struct{} + +// NewCopyZettel creates a new use case. +func NewCopyZettel() CopyZettel { + return CopyZettel{} +} + +// Run executes the use case. +func (uc CopyZettel) Run(origZettel domain.Zettel) domain.Zettel { + m := origZettel.Meta.Clone() + if title, ok := m.Get(meta.KeyTitle); ok { + if len(title) > 0 { + title = "Copy of " + title + } else { + title = "Copy" + } + m.Set(meta.KeyTitle, title) + } + content := origZettel.Content + content.TrimSpace() + return domain.Zettel{Meta: m, Content: content} +} Index: usecase/create_zettel.go ================================================================== --- usecase/create_zettel.go +++ usecase/create_zettel.go @@ -1,158 +1,63 @@ //----------------------------------------------------------------------------- -// Copyright (c) 2020-present Detlef Stern +// Copyright (c) 2020-2021 Detlef Stern // -// This file is part of Zettelstore. +// This file is part of zettelstore. // // Zettelstore is licensed under the latest version of the EUPL (European Union // Public License). Please see file LICENSE.txt for your rights and obligations // under this license. -// -// SPDX-License-Identifier: EUPL-1.2 -// SPDX-FileCopyrightText: 2020-present Detlef Stern //----------------------------------------------------------------------------- +// Package usecase provides (business) use cases for the zettelstore. package usecase import ( "context" - "time" - "t73f.de/r/zsc/api" "zettelstore.de/z/config" - "zettelstore.de/z/logger" - "zettelstore.de/z/zettel" - "zettelstore.de/z/zettel/id" - "zettelstore.de/z/zettel/meta" + "zettelstore.de/z/domain" + "zettelstore.de/z/domain/id" + "zettelstore.de/z/domain/meta" ) // CreateZettelPort is the interface used by this use case. type CreateZettelPort interface { // CreateZettel creates a new zettel. - CreateZettel(ctx context.Context, zettel zettel.Zettel) (id.Zid, error) + CreateZettel(ctx context.Context, zettel domain.Zettel) (id.Zid, error) } // CreateZettel is the data for this use case. type CreateZettel struct { - log *logger.Logger rtConfig config.Config port CreateZettelPort } // NewCreateZettel creates a new use case. -func NewCreateZettel(log *logger.Logger, rtConfig config.Config, port CreateZettelPort) CreateZettel { +func NewCreateZettel(rtConfig config.Config, port CreateZettelPort) CreateZettel { return CreateZettel{ - log: log, rtConfig: rtConfig, port: port, } } -// PrepareCopy the zettel for further modification. -func (*CreateZettel) PrepareCopy(origZettel zettel.Zettel) zettel.Zettel { - origMeta := origZettel.Meta - m := origMeta.Clone() - if title, found := origMeta.Get(api.KeyTitle); found { - m.Set(api.KeyTitle, prependTitle(title, "Copy", "Copy of ")) - } - setReadonly(m) - content := origZettel.Content - content.TrimSpace() - return zettel.Zettel{Meta: m, Content: content} -} - -// PrepareVersion the zettel for further modification. -func (*CreateZettel) PrepareVersion(origZettel zettel.Zettel) zettel.Zettel { - origMeta := origZettel.Meta - m := origMeta.Clone() - m.Set(api.KeyPredecessor, origMeta.Zid.String()) - setReadonly(m) - content := origZettel.Content - content.TrimSpace() - return zettel.Zettel{Meta: m, Content: content} -} - -// PrepareFolge the zettel for further modification. -func (*CreateZettel) PrepareFolge(origZettel zettel.Zettel) zettel.Zettel { - origMeta := origZettel.Meta - m := meta.New(id.Invalid) - if title, found := origMeta.Get(api.KeyTitle); found { - m.Set(api.KeyTitle, prependTitle(title, "Folge", "Folge of ")) - } - updateMetaRoleTagsSyntax(m, origMeta) - m.Set(api.KeyPrecursor, origMeta.Zid.String()) - return zettel.Zettel{Meta: m, Content: zettel.NewContent(nil)} -} - -// PrepareChild the zettel for further modification. -func (*CreateZettel) PrepareChild(origZettel zettel.Zettel) zettel.Zettel { - origMeta := origZettel.Meta - m := meta.New(id.Invalid) - if title, found := origMeta.Get(api.KeyTitle); found { - m.Set(api.KeyTitle, prependTitle(title, "Child", "Child of ")) - } - updateMetaRoleTagsSyntax(m, origMeta) - m.Set(api.KeySuperior, origMeta.Zid.String()) - return zettel.Zettel{Meta: m, Content: zettel.NewContent(nil)} -} - -// PrepareNew the zettel for further modification. -func (*CreateZettel) PrepareNew(origZettel zettel.Zettel, newTitle string) zettel.Zettel { - m := meta.New(id.Invalid) - om := origZettel.Meta - m.SetNonEmpty(api.KeyTitle, om.GetDefault(api.KeyTitle, "")) - updateMetaRoleTagsSyntax(m, om) - - const prefixLen = len(meta.NewPrefix) - for _, pair := range om.PairsRest() { - if key := pair.Key; len(key) > prefixLen && key[0:prefixLen] == meta.NewPrefix { - m.Set(key[prefixLen:], pair.Value) - } - } - if newTitle != "" { - m.Set(api.KeyTitle, newTitle) - } - content := origZettel.Content - content.TrimSpace() - return zettel.Zettel{Meta: m, Content: content} -} - -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, meta.DefaultSyntax)) -} - -func prependTitle(title, s0, s1 string) string { - if len(title) > 0 { - return s1 + title - } - return s0 -} - -func setReadonly(m *meta.Meta) { - if _, found := m.Get(api.KeyReadOnly); found { - // Currently, "false" is a safe value. - // - // If the current user and its role is known, a more elaborative calculation - // could be done: set it to a value, so that the current user will be able - // to modify it later. - m.Set(api.KeyReadOnly, api.ValueFalse) - } -} - // Run executes the use case. -func (uc *CreateZettel) Run(ctx context.Context, zettel zettel.Zettel) (id.Zid, error) { +func (uc CreateZettel) Run(ctx context.Context, zettel domain.Zettel) (id.Zid, error) { m := zettel.Meta if m.Zid.IsValid() { return m.Zid, nil // TODO: new error: already exists } - m.Set(api.KeyCreated, time.Now().Local().Format(id.TimestampLayout)) - m.Delete(api.KeyModified) + if title, ok := m.Get(meta.KeyTitle); !ok || title == "" { + m.Set(meta.KeyTitle, uc.rtConfig.GetDefaultTitle()) + } + if role, ok := m.Get(meta.KeyRole); !ok || role == "" { + m.Set(meta.KeyRole, uc.rtConfig.GetDefaultRole()) + } + if syntax, ok := m.Get(meta.KeySyntax); !ok || syntax == "" { + m.Set(meta.KeySyntax, uc.rtConfig.GetDefaultSyntax()) + } m.YamlSep = uc.rtConfig.GetYAMLHeader() zettel.Content.TrimSpace() - zid, err := uc.port.CreateZettel(ctx, zettel) - uc.log.Info().User(ctx).Zid(zid).Err(err).Msg("Create zettel") - return zid, err + return uc.port.CreateZettel(ctx, zettel) } Index: usecase/delete_zettel.go ================================================================== --- usecase/delete_zettel.go +++ usecase/delete_zettel.go @@ -1,25 +1,22 @@ //----------------------------------------------------------------------------- -// Copyright (c) 2020-present Detlef Stern +// Copyright (c) 2020 Detlef Stern // -// This file is part of Zettelstore. +// This file is part of zettelstore. // // Zettelstore is licensed under the latest version of the EUPL (European Union // Public License). Please see file LICENSE.txt for your rights and obligations // under this license. -// -// SPDX-License-Identifier: EUPL-1.2 -// SPDX-FileCopyrightText: 2020-present Detlef Stern //----------------------------------------------------------------------------- +// Package usecase provides (business) use cases for the zettelstore. package usecase import ( "context" - "zettelstore.de/z/logger" - "zettelstore.de/z/zettel/id" + "zettelstore.de/z/domain/id" ) // DeleteZettelPort is the interface used by this use case. type DeleteZettelPort interface { // DeleteZettel removes the zettel from the box. @@ -26,20 +23,17 @@ DeleteZettel(ctx context.Context, zid id.Zid) error } // DeleteZettel is the data for this use case. type DeleteZettel struct { - log *logger.Logger port DeleteZettelPort } // NewDeleteZettel creates a new use case. -func NewDeleteZettel(log *logger.Logger, port DeleteZettelPort) DeleteZettel { - return DeleteZettel{log: log, port: port} +func NewDeleteZettel(port DeleteZettelPort) DeleteZettel { + return DeleteZettel{port: port} } // Run executes the use case. -func (uc *DeleteZettel) Run(ctx context.Context, zid id.Zid) error { - err := uc.port.DeleteZettel(ctx, zid) - uc.log.Info().User(ctx).Zid(zid).Err(err).Msg("Delete zettel") - return err +func (uc DeleteZettel) Run(ctx context.Context, zid id.Zid) error { + return uc.port.DeleteZettel(ctx, zid) } DELETED usecase/evaluate.go Index: usecase/evaluate.go ================================================================== --- usecase/evaluate.go +++ usecase/evaluate.go @@ -1,86 +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. -// -// SPDX-License-Identifier: EUPL-1.2 -// SPDX-FileCopyrightText: 2021-present Detlef Stern -//----------------------------------------------------------------------------- - -package usecase - -import ( - "context" - - "zettelstore.de/z/ast" - "zettelstore.de/z/config" - "zettelstore.de/z/evaluator" - "zettelstore.de/z/parser" - "zettelstore.de/z/query" - "zettelstore.de/z/zettel" - "zettelstore.de/z/zettel/id" - "zettelstore.de/z/zettel/meta" -) - -// Evaluate is the data for this use case. -type Evaluate struct { - rtConfig config.Config - ucGetZettel *GetZettel - ucQuery *Query -} - -// NewEvaluate creates a new use case. -func NewEvaluate(rtConfig config.Config, ucGetZettel *GetZettel, ucQuery *Query) Evaluate { - return Evaluate{ - rtConfig: rtConfig, - ucGetZettel: ucGetZettel, - ucQuery: ucQuery, - } -} - -// Run executes the use case. -func (uc *Evaluate) Run(ctx context.Context, zid id.Zid, syntax string) (*ast.ZettelNode, error) { - zettel, err := uc.ucGetZettel.Run(ctx, zid) - if err != nil { - return nil, err - } - return uc.RunZettel(ctx, zettel, syntax), nil -} - -// RunZettel executes the use case for a given zettel. -func (uc *Evaluate) RunZettel(ctx context.Context, zettel zettel.Zettel, syntax string) *ast.ZettelNode { - zn := parser.ParseZettel(ctx, zettel, syntax, uc.rtConfig) - evaluator.EvaluateZettel(ctx, uc, uc.rtConfig, zn) - return zn -} - -// RunBlockNode executes the use case for a metadata list. -func (uc *Evaluate) RunBlockNode(ctx context.Context, bn ast.BlockNode) ast.BlockSlice { - if bn == nil { - return nil - } - bns := ast.BlockSlice{bn} - evaluator.EvaluateBlock(ctx, uc, uc.rtConfig, &bns) - return bns -} - -// RunMetadata executes the use case for a metadata value. -func (uc *Evaluate) RunMetadata(ctx context.Context, value string) ast.InlineSlice { - is := parser.ParseMetadata(value) - evaluator.EvaluateInline(ctx, uc, uc.rtConfig, &is) - return is -} - -// GetZettel retrieves the full zettel of a given zettel identifier. -func (uc *Evaluate) GetZettel(ctx context.Context, zid id.Zid) (zettel.Zettel, error) { - return uc.ucGetZettel.Run(ctx, zid) -} - -// QueryMeta returns a list of metadata that comply to the given selection criteria. -func (uc *Evaluate) QueryMeta(ctx context.Context, q *query.Query) ([]*meta.Meta, error) { - return uc.ucQuery.Run(ctx, q) -} ADDED usecase/folge_zettel.go Index: usecase/folge_zettel.go ================================================================== --- usecase/folge_zettel.go +++ usecase/folge_zettel.go @@ -0,0 +1,48 @@ +//----------------------------------------------------------------------------- +// Copyright (c) 2020-2021 Detlef Stern +// +// This file is part of zettelstore. +// +// Zettelstore is licensed under the latest version of the EUPL (European Union +// Public License). Please see file LICENSE.txt for your rights and obligations +// under this license. +//----------------------------------------------------------------------------- + +// Package usecase provides (business) use cases for the zettelstore. +package usecase + +import ( + "zettelstore.de/z/config" + "zettelstore.de/z/domain" + "zettelstore.de/z/domain/id" + "zettelstore.de/z/domain/meta" +) + +// FolgeZettel is the data for this use case. +type FolgeZettel struct { + rtConfig config.Config +} + +// NewFolgeZettel creates a new use case. +func NewFolgeZettel(rtConfig config.Config) FolgeZettel { + return FolgeZettel{rtConfig} +} + +// Run executes the use case. +func (uc FolgeZettel) Run(origZettel domain.Zettel) domain.Zettel { + origMeta := origZettel.Meta + m := meta.New(id.Invalid) + if title, ok := origMeta.Get(meta.KeyTitle); ok { + if len(title) > 0 { + title = "Folge of " + title + } else { + title = "Folge" + } + m.Set(meta.KeyTitle, title) + } + m.Set(meta.KeyRole, config.GetRole(origMeta, uc.rtConfig)) + m.Set(meta.KeyTags, origMeta.GetDefault(meta.KeyTags, "")) + m.Set(meta.KeySyntax, uc.rtConfig.GetDefaultSyntax()) + m.Set(meta.KeyPrecursor, origMeta.Zid.String()) + return domain.Zettel{Meta: m, Content: domain.NewContent("")} +} ADDED usecase/get_all_meta.go Index: usecase/get_all_meta.go ================================================================== --- usecase/get_all_meta.go +++ usecase/get_all_meta.go @@ -0,0 +1,40 @@ +//----------------------------------------------------------------------------- +// Copyright (c) 2021 Detlef Stern +// +// This file is part of zettelstore. +// +// Zettelstore is licensed under the latest version of the EUPL (European Union +// Public License). Please see file LICENSE.txt for your rights and obligations +// under this license. +//----------------------------------------------------------------------------- + +// Package usecase provides (business) use cases for the zettelstore. +package usecase + +import ( + "context" + + "zettelstore.de/z/domain/id" + "zettelstore.de/z/domain/meta" +) + +// GetAllMetaPort is the interface used by this use case. +type GetAllMetaPort interface { + // GetAllMeta retrieves just the meta data of a specific zettel. + GetAllMeta(ctx context.Context, zid id.Zid) ([]*meta.Meta, error) +} + +// GetAllMeta is the data for this use case. +type GetAllMeta struct { + port GetAllMetaPort +} + +// NewGetAllMeta creates a new use case. +func NewGetAllMeta(port GetAllMetaPort) GetAllMeta { + return GetAllMeta{port: port} +} + +// Run executes the use case. +func (uc GetAllMeta) Run(ctx context.Context, zid id.Zid) ([]*meta.Meta, error) { + return uc.port.GetAllMeta(ctx, zid) +} DELETED usecase/get_all_zettel.go Index: usecase/get_all_zettel.go ================================================================== --- usecase/get_all_zettel.go +++ usecase/get_all_zettel.go @@ -1,41 +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. -// -// SPDX-License-Identifier: EUPL-1.2 -// SPDX-FileCopyrightText: 2021-present Detlef Stern -//----------------------------------------------------------------------------- - -package usecase - -import ( - "context" - - "zettelstore.de/z/zettel" - "zettelstore.de/z/zettel/id" -) - -// GetAllZettelPort is the interface used by this use case. -type GetAllZettelPort interface { - GetAllZettel(ctx context.Context, zid id.Zid) ([]zettel.Zettel, error) -} - -// GetAllZettel is the data for this use case. -type GetAllZettel struct { - port GetAllZettelPort -} - -// NewGetAllZettel creates a new use case. -func NewGetAllZettel(port GetAllZettelPort) GetAllZettel { - return GetAllZettel{port: port} -} - -// Run executes the use case. -func (uc GetAllZettel) Run(ctx context.Context, zid id.Zid) ([]zettel.Zettel, error) { - return uc.port.GetAllZettel(ctx, zid) -} ADDED usecase/get_meta.go Index: usecase/get_meta.go ================================================================== --- usecase/get_meta.go +++ usecase/get_meta.go @@ -0,0 +1,40 @@ +//----------------------------------------------------------------------------- +// Copyright (c) 2020 Detlef Stern +// +// This file is part of zettelstore. +// +// Zettelstore is licensed under the latest version of the EUPL (European Union +// Public License). Please see file LICENSE.txt for your rights and obligations +// under this license. +//----------------------------------------------------------------------------- + +// Package usecase provides (business) use cases for the zettelstore. +package usecase + +import ( + "context" + + "zettelstore.de/z/domain/id" + "zettelstore.de/z/domain/meta" +) + +// GetMetaPort is the interface used by this use case. +type GetMetaPort interface { + // GetMeta retrieves just the meta data of a specific zettel. + GetMeta(ctx context.Context, zid id.Zid) (*meta.Meta, error) +} + +// GetMeta is the data for this use case. +type GetMeta struct { + port GetMetaPort +} + +// NewGetMeta creates a new use case. +func NewGetMeta(port GetMetaPort) GetMeta { + return GetMeta{port: port} +} + +// Run executes the use case. +func (uc GetMeta) Run(ctx context.Context, zid id.Zid) (*meta.Meta, error) { + return uc.port.GetMeta(ctx, zid) +} DELETED usecase/get_special_zettel.go Index: usecase/get_special_zettel.go ================================================================== --- usecase/get_special_zettel.go +++ usecase/get_special_zettel.go @@ -1,113 +0,0 @@ -//----------------------------------------------------------------------------- -// 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 usecase - -import ( - "context" - - "t73f.de/r/zsc/api" - "zettelstore.de/z/query" - "zettelstore.de/z/zettel" - "zettelstore.de/z/zettel/id" - "zettelstore.de/z/zettel/meta" -) - -// TagZettel is the usecase of retrieving a "tag zettel", i.e. a zettel that -// describes a given tag. A tag zettel must have the tag's name in its title -// and must have a role=tag. - -// TagZettelPort is the interface used by this use case. -type TagZettelPort interface { - // GetZettel retrieves a specific zettel. - GetZettel(ctx context.Context, zid id.Zid) (zettel.Zettel, error) -} - -// TagZettel is the data for this use case. -type TagZettel struct { - port GetZettelPort - query *Query -} - -// NewTagZettel creates a new use case. -func NewTagZettel(port GetZettelPort, query *Query) TagZettel { - return TagZettel{port: port, query: query} -} - -// Run executes the use case. -func (uc TagZettel) Run(ctx context.Context, tag string) (zettel.Zettel, error) { - tag = meta.NormalizeTag(tag) - q := query.Parse( - api.KeyTitle + api.SearchOperatorEqual + tag + " " + - api.KeyRole + api.SearchOperatorHas + api.ValueRoleTag) - ml, err := uc.query.Run(ctx, q) - if err != nil { - return zettel.Zettel{}, err - } - for _, m := range ml { - z, errZ := uc.port.GetZettel(ctx, m.Zid) - if errZ == nil { - return z, nil - } - } - return zettel.Zettel{}, ErrTagZettelNotFound{Tag: tag} -} - -// ErrTagZettelNotFound is returned if a tag zettel was not found. -type ErrTagZettelNotFound struct{ Tag string } - -func (etznf ErrTagZettelNotFound) Error() string { return "tag zettel not found: " + etznf.Tag } - -// RoleZettel is the usecase of retrieving a "role zettel", i.e. a zettel that -// describes a given role. A role zettel must have the role's name in its title -// and must have a role=role. - -// RoleZettelPort is the interface used by this use case. -type RoleZettelPort interface { - // GetZettel retrieves a specific zettel. - GetZettel(ctx context.Context, zid id.Zid) (zettel.Zettel, error) -} - -// RoleZettel is the data for this use case. -type RoleZettel struct { - port GetZettelPort - query *Query -} - -// NewRoleZettel creates a new use case. -func NewRoleZettel(port GetZettelPort, query *Query) RoleZettel { - return RoleZettel{port: port, query: query} -} - -// Run executes the use case. -func (uc RoleZettel) Run(ctx context.Context, role string) (zettel.Zettel, error) { - q := query.Parse( - api.KeyTitle + api.SearchOperatorEqual + role + " " + - api.KeyRole + api.SearchOperatorHas + api.ValueRoleRole) - ml, err := uc.query.Run(ctx, q) - if err != nil { - return zettel.Zettel{}, err - } - for _, m := range ml { - z, errZ := uc.port.GetZettel(ctx, m.Zid) - if errZ == nil { - return z, nil - } - } - return zettel.Zettel{}, ErrRoleZettelNotFound{Role: role} -} - -// ErrRoleZettelNotFound is returned if a role zettel was not found. -type ErrRoleZettelNotFound struct{ Role string } - -func (etznf ErrRoleZettelNotFound) Error() string { return "role zettel not found: " + etznf.Role } Index: usecase/get_user.go ================================================================== --- usecase/get_user.go +++ usecase/get_user.go @@ -1,39 +1,35 @@ //----------------------------------------------------------------------------- -// Copyright (c) 2020-present Detlef Stern +// Copyright (c) 2020-2021 Detlef Stern // -// This file is part of Zettelstore. +// This file is part of zettelstore. // // Zettelstore is licensed under the latest version of the EUPL (European Union // Public License). Please see file LICENSE.txt for your rights and obligations // under this license. -// -// SPDX-License-Identifier: EUPL-1.2 -// SPDX-FileCopyrightText: 2020-present Detlef Stern //----------------------------------------------------------------------------- +// Package usecase provides (business) use cases for the zettelstore. package usecase import ( "context" - "t73f.de/r/zsc/api" "zettelstore.de/z/auth" "zettelstore.de/z/box" - "zettelstore.de/z/query" - "zettelstore.de/z/zettel" - "zettelstore.de/z/zettel/id" - "zettelstore.de/z/zettel/meta" + "zettelstore.de/z/domain/id" + "zettelstore.de/z/domain/meta" + "zettelstore.de/z/search" ) // Use case: return user identified by meta key ident. // --------------------------------------------------- // GetUserPort is the interface used by this use case. type GetUserPort interface { - GetZettel(ctx context.Context, zid id.Zid) (zettel.Zettel, error) - SelectMeta(ctx context.Context, metaSeq []*meta.Meta, q *query.Query) ([]*meta.Meta, error) + GetMeta(ctx context.Context, zid id.Zid) (*meta.Meta, error) + SelectMeta(ctx context.Context, s *search.Search) ([]*meta.Meta, error) } // GetUser is the data for this use case. type GetUser struct { authz auth.AuthzManager @@ -50,17 +46,23 @@ ctx = box.NoEnrichContext(ctx) // It is important to try first with the owner. First, because another user // could give herself the same ''ident''. Second, in most cases the owner // will authenticate. - identZettel, err := uc.port.GetZettel(ctx, uc.authz.Owner()) - if err == nil && identZettel.Meta.GetDefault(api.KeyUserID, "") == ident { - return identZettel.Meta, nil + identMeta, err := uc.port.GetMeta(ctx, uc.authz.Owner()) + if err == nil && identMeta.GetDefault(meta.KeyUserID, "") == ident { + if role, ok := identMeta.Get(meta.KeyRole); !ok || + role != meta.ValueRoleUser { + return nil, nil + } + return identMeta, nil } // Owner was not found or has another ident. Try via list search. - q := query.Parse(api.KeyUserID + api.SearchOperatorHas + ident + " " + api.SearchOperatorHas + ident) - metaList, err := uc.port.SelectMeta(ctx, nil, q) + var s *search.Search + s = s.AddExpr(meta.KeyRole, meta.ValueRoleUser) + s = s.AddExpr(meta.KeyUserID, ident) + metaList, err := uc.port.SelectMeta(ctx, s) if err != nil { return nil, err } if len(metaList) < 1 { return nil, nil @@ -71,11 +73,11 @@ // Use case: return an user identified by zettel id and assert given ident value. // ------------------------------------------------------------------------------ // GetUserByZidPort is the interface used by this use case. type GetUserByZidPort interface { - GetZettel(ctx context.Context, zid id.Zid) (zettel.Zettel, error) + GetMeta(ctx context.Context, zid id.Zid) (*meta.Meta, error) } // GetUserByZid is the data for this use case. type GetUserByZid struct { port GetUserByZidPort @@ -86,16 +88,15 @@ return GetUserByZid{port: port} } // GetUser executes the use case. func (uc GetUserByZid) GetUser(ctx context.Context, zid id.Zid, ident string) (*meta.Meta, error) { - userZettel, err := uc.port.GetZettel(box.NoEnrichContext(ctx), zid) + userMeta, err := uc.port.GetMeta(box.NoEnrichContext(ctx), zid) if err != nil { return nil, err } - userMeta := userZettel.Meta - if val, ok := userMeta.Get(api.KeyUserID); !ok || val != ident { + if val, ok := userMeta.Get(meta.KeyUserID); !ok || val != ident { return nil, nil } return userMeta, nil } Index: usecase/get_zettel.go ================================================================== --- usecase/get_zettel.go +++ usecase/get_zettel.go @@ -1,31 +1,29 @@ //----------------------------------------------------------------------------- -// Copyright (c) 2020-present Detlef Stern +// Copyright (c) 2020 Detlef Stern // -// This file is part of Zettelstore. +// This file is part of zettelstore. // // Zettelstore is licensed under the latest version of the EUPL (European Union // Public License). Please see file LICENSE.txt for your rights and obligations // under this license. -// -// SPDX-License-Identifier: EUPL-1.2 -// SPDX-FileCopyrightText: 2020-present Detlef Stern //----------------------------------------------------------------------------- +// Package usecase provides (business) use cases for the zettelstore. package usecase import ( "context" - "zettelstore.de/z/zettel" - "zettelstore.de/z/zettel/id" + "zettelstore.de/z/domain" + "zettelstore.de/z/domain/id" ) // GetZettelPort is the interface used by this use case. type GetZettelPort interface { // GetZettel retrieves a specific zettel. - GetZettel(ctx context.Context, zid id.Zid) (zettel.Zettel, error) + GetZettel(ctx context.Context, zid id.Zid) (domain.Zettel, error) } // GetZettel is the data for this use case. type GetZettel struct { port GetZettelPort @@ -35,8 +33,8 @@ func NewGetZettel(port GetZettelPort) GetZettel { return GetZettel{port: port} } // Run executes the use case. -func (uc GetZettel) Run(ctx context.Context, zid id.Zid) (zettel.Zettel, error) { +func (uc GetZettel) Run(ctx context.Context, zid id.Zid) (domain.Zettel, error) { return uc.port.GetZettel(ctx, zid) } ADDED usecase/list_meta.go Index: usecase/list_meta.go ================================================================== --- usecase/list_meta.go +++ usecase/list_meta.go @@ -0,0 +1,40 @@ +//----------------------------------------------------------------------------- +// Copyright (c) 2020-2021 Detlef Stern +// +// This file is part of zettelstore. +// +// Zettelstore is licensed under the latest version of the EUPL (European Union +// Public License). Please see file LICENSE.txt for your rights and obligations +// under this license. +//----------------------------------------------------------------------------- + +// Package usecase provides (business) use cases for the zettelstore. +package usecase + +import ( + "context" + + "zettelstore.de/z/domain/meta" + "zettelstore.de/z/search" +) + +// ListMetaPort is the interface used by this use case. +type ListMetaPort interface { + // SelectMeta returns all zettel meta data that match the selection criteria. + SelectMeta(ctx context.Context, s *search.Search) ([]*meta.Meta, error) +} + +// ListMeta is the data for this use case. +type ListMeta struct { + port ListMetaPort +} + +// NewListMeta creates a new use case. +func NewListMeta(port ListMetaPort) ListMeta { + return ListMeta{port: port} +} + +// Run executes the use case. +func (uc ListMeta) Run(ctx context.Context, s *search.Search) ([]*meta.Meta, error) { + return uc.port.SelectMeta(ctx, s) +} ADDED usecase/list_role.go Index: usecase/list_role.go ================================================================== --- usecase/list_role.go +++ usecase/list_role.go @@ -0,0 +1,57 @@ +//----------------------------------------------------------------------------- +// Copyright (c) 2020-2021 Detlef Stern +// +// This file is part of zettelstore. +// +// Zettelstore is licensed under the latest version of the EUPL (European Union +// Public License). Please see file LICENSE.txt for your rights and obligations +// under this license. +//----------------------------------------------------------------------------- + +// Package usecase provides (business) use cases for the zettelstore. +package usecase + +import ( + "context" + "sort" + + "zettelstore.de/z/box" + "zettelstore.de/z/domain/meta" + "zettelstore.de/z/search" +) + +// ListRolePort is the interface used by this use case. +type ListRolePort interface { + // SelectMeta returns all zettel meta data that match the selection criteria. + SelectMeta(ctx context.Context, s *search.Search) ([]*meta.Meta, error) +} + +// ListRole is the data for this use case. +type ListRole struct { + port ListRolePort +} + +// NewListRole creates a new use case. +func NewListRole(port ListRolePort) ListRole { + return ListRole{port: port} +} + +// Run executes the use case. +func (uc ListRole) Run(ctx context.Context) ([]string, error) { + metas, err := uc.port.SelectMeta(box.NoEnrichContext(ctx), nil) + if err != nil { + return nil, err + } + roles := make(map[string]bool, 8) + for _, m := range metas { + if role, ok := m.Get(meta.KeyRole); ok && role != "" { + roles[role] = true + } + } + result := make([]string, 0, len(roles)) + for role := range roles { + result = append(result, role) + } + sort.Strings(result) + return result, nil +} ADDED usecase/list_tags.go Index: usecase/list_tags.go ================================================================== --- usecase/list_tags.go +++ usecase/list_tags.go @@ -0,0 +1,63 @@ +//----------------------------------------------------------------------------- +// Copyright (c) 2020-2021 Detlef Stern +// +// This file is part of zettelstore. +// +// Zettelstore is licensed under the latest version of the EUPL (European Union +// Public License). Please see file LICENSE.txt for your rights and obligations +// under this license. +//----------------------------------------------------------------------------- + +// Package usecase provides (business) use cases for the zettelstore. +package usecase + +import ( + "context" + + "zettelstore.de/z/box" + "zettelstore.de/z/domain/meta" + "zettelstore.de/z/search" +) + +// ListTagsPort is the interface used by this use case. +type ListTagsPort interface { + // SelectMeta returns all zettel meta data that match the selection criteria. + SelectMeta(ctx context.Context, s *search.Search) ([]*meta.Meta, error) +} + +// ListTags is the data for this use case. +type ListTags struct { + port ListTagsPort +} + +// NewListTags creates a new use case. +func NewListTags(port ListTagsPort) ListTags { + return ListTags{port: port} +} + +// TagData associates tags with a list of all zettel meta that use this tag +type TagData map[string][]*meta.Meta + +// Run executes the use case. +func (uc ListTags) Run(ctx context.Context, minCount int) (TagData, error) { + metas, err := uc.port.SelectMeta(box.NoEnrichContext(ctx), nil) + if err != nil { + return nil, err + } + result := make(TagData) + for _, m := range metas { + if tl, ok := m.GetList(meta.KeyTags); ok && len(tl) > 0 { + for _, t := range tl { + result[t] = append(result[t], m) + } + } + } + if minCount > 1 { + for t, ms := range result { + if len(ms) < minCount { + delete(result, t) + } + } + } + return result, nil +} DELETED usecase/lists.go Index: usecase/lists.go ================================================================== --- usecase/lists.go +++ usecase/lists.go @@ -1,84 +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. -// -// SPDX-License-Identifier: EUPL-1.2 -// SPDX-FileCopyrightText: 2020-present Detlef Stern -//----------------------------------------------------------------------------- - -package usecase - -import ( - "context" - - "t73f.de/r/zsc/api" - "zettelstore.de/z/box" - "zettelstore.de/z/parser" - "zettelstore.de/z/query" - "zettelstore.de/z/zettel/meta" -) - -// -------- List syntax ------------------------------------------------------ - -// ListSyntaxPort is the interface used by this use case. -type ListSyntaxPort interface { - SelectMeta(ctx context.Context, metaSeq []*meta.Meta, q *query.Query) ([]*meta.Meta, error) -} - -// ListSyntax is the data for this use case. -type ListSyntax struct { - port ListSyntaxPort -} - -// NewListSyntax creates a new use case. -func NewListSyntax(port ListSyntaxPort) ListSyntax { - return ListSyntax{port: port} -} - -// Run executes the use case. -func (uc ListSyntax) Run(ctx context.Context) (meta.Arrangement, error) { - q := query.Parse(api.KeySyntax + api.ExistOperator) // We look for all metadata with a syntax key - metas, err := uc.port.SelectMeta(box.NoEnrichContext(ctx), nil, q) - if err != nil { - return nil, err - } - result := meta.CreateArrangement(metas, api.KeySyntax) - for _, syn := range parser.GetSyntaxes() { - if _, found := result[syn]; !found { - delete(result, syn) - } - } - return result, nil -} - -// -------- List roles ------------------------------------------------------- - -// ListRolesPort is the interface used by this use case. -type ListRolesPort interface { - SelectMeta(ctx context.Context, metaSeq []*meta.Meta, q *query.Query) ([]*meta.Meta, error) -} - -// ListRoles is the data for this use case. -type ListRoles struct { - port ListRolesPort -} - -// NewListRoles creates a new use case. -func NewListRoles(port ListRolesPort) ListRoles { - return ListRoles{port: port} -} - -// Run executes the use case. -func (uc ListRoles) Run(ctx context.Context) (meta.Arrangement, error) { - q := query.Parse(api.KeyRole + api.ExistOperator) // We look for all metadata with an existing role key - metas, err := uc.port.SelectMeta(box.NoEnrichContext(ctx), nil, q) - if err != nil { - return nil, err - } - return meta.CreateArrangement(metas, api.KeyRole), nil -} ADDED usecase/new_zettel.go Index: usecase/new_zettel.go ================================================================== --- usecase/new_zettel.go +++ usecase/new_zettel.go @@ -0,0 +1,46 @@ +//----------------------------------------------------------------------------- +// Copyright (c) 2020-2021 Detlef Stern +// +// This file is part of zettelstore. +// +// Zettelstore is licensed under the latest version of the EUPL (European Union +// Public License). Please see file LICENSE.txt for your rights and obligations +// under this license. +//----------------------------------------------------------------------------- + +// Package usecase provides (business) use cases for the zettelstore. +package usecase + +import ( + "zettelstore.de/z/domain" + "zettelstore.de/z/domain/id" + "zettelstore.de/z/domain/meta" +) + +// NewZettel is the data for this use case. +type NewZettel struct{} + +// NewNewZettel creates a new use case. +func NewNewZettel() NewZettel { + return NewZettel{} +} + +// Run executes the use case. +func (uc NewZettel) Run(origZettel domain.Zettel) domain.Zettel { + m := meta.New(id.Invalid) + om := origZettel.Meta + m.Set(meta.KeyTitle, om.GetDefault(meta.KeyTitle, "")) + m.Set(meta.KeyRole, om.GetDefault(meta.KeyRole, "")) + m.Set(meta.KeyTags, om.GetDefault(meta.KeyTags, "")) + m.Set(meta.KeySyntax, om.GetDefault(meta.KeySyntax, "")) + + const prefixLen = len(meta.NewPrefix) + for _, pair := range om.PairsRest(false) { + if key := pair.Key; len(key) > prefixLen && key[0:prefixLen] == meta.NewPrefix { + m.Set(key[prefixLen:], pair.Value) + } + } + content := origZettel.Content + content.TrimSpace() + return domain.Zettel{Meta: m, Content: content} +} ADDED usecase/order.go Index: usecase/order.go ================================================================== --- usecase/order.go +++ usecase/order.go @@ -0,0 +1,55 @@ +//----------------------------------------------------------------------------- +// Copyright (c) 2021 Detlef Stern +// +// This file is part of zettelstore. +// +// Zettelstore is licensed under the latest version of the EUPL (European Union +// Public License). Please see file LICENSE.txt for your rights and obligations +// under this license. +//----------------------------------------------------------------------------- + +// Package usecase provides (business) use cases for the Zettelstore. +package usecase + +import ( + "context" + + "zettelstore.de/z/collect" + "zettelstore.de/z/domain/id" + "zettelstore.de/z/domain/meta" +) + +// ZettelOrderPort is the interface used by this use case. +type ZettelOrderPort interface { + // GetMeta retrieves just the meta data of a specific zettel. + GetMeta(ctx context.Context, zid id.Zid) (*meta.Meta, error) +} + +// ZettelOrder is the data for this use case. +type ZettelOrder struct { + port ZettelOrderPort + parseZettel ParseZettel +} + +// NewZettelOrder creates a new use case. +func NewZettelOrder(port ZettelOrderPort, parseZettel ParseZettel) ZettelOrder { + return ZettelOrder{port: port, parseZettel: parseZettel} +} + +// Run executes the use case. +func (uc ZettelOrder) Run(ctx context.Context, zid id.Zid, syntax string) ( + start *meta.Meta, result []*meta.Meta, err error, +) { + zn, err := uc.parseZettel.Run(ctx, zid, syntax) + if err != nil { + return nil, nil, err + } + for _, ref := range collect.Order(zn) { + if zid, err := id.Parse(ref.URL.Path); err == nil { + if m, err := uc.port.GetMeta(ctx, zid); err == nil { + result = append(result, m) + } + } + } + return zn.Meta, result, nil +} Index: usecase/parse_zettel.go ================================================================== --- usecase/parse_zettel.go +++ usecase/parse_zettel.go @@ -1,27 +1,25 @@ //----------------------------------------------------------------------------- -// Copyright (c) 2020-present Detlef Stern +// Copyright (c) 2020 Detlef Stern // -// This file is part of Zettelstore. +// This file is part of zettelstore. // // Zettelstore is licensed under the latest version of the EUPL (European Union // Public License). Please see file LICENSE.txt for your rights and obligations // under this license. -// -// SPDX-License-Identifier: EUPL-1.2 -// SPDX-FileCopyrightText: 2020-present Detlef Stern //----------------------------------------------------------------------------- +// Package usecase provides (business) use cases for the zettelstore. package usecase import ( "context" "zettelstore.de/z/ast" "zettelstore.de/z/config" + "zettelstore.de/z/domain/id" "zettelstore.de/z/parser" - "zettelstore.de/z/zettel/id" ) // ParseZettel is the data for this use case. type ParseZettel struct { rtConfig config.Config @@ -32,13 +30,14 @@ func NewParseZettel(rtConfig config.Config, getZettel GetZettel) ParseZettel { return ParseZettel{rtConfig: rtConfig, getZettel: getZettel} } // Run executes the use case. -func (uc ParseZettel) Run(ctx context.Context, zid id.Zid, syntax string) (*ast.ZettelNode, error) { +func (uc ParseZettel) Run( + ctx context.Context, zid id.Zid, syntax string) (*ast.ZettelNode, error) { zettel, err := uc.getZettel.Run(ctx, zid) if err != nil { return nil, err } - return parser.ParseZettel(ctx, zettel, syntax, uc.rtConfig), nil + return parser.ParseZettel(zettel, syntax, uc.rtConfig), nil } DELETED usecase/query.go Index: usecase/query.go ================================================================== --- usecase/query.go +++ usecase/query.go @@ -1,279 +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. -// -// SPDX-License-Identifier: EUPL-1.2 -// SPDX-FileCopyrightText: 2020-present Detlef Stern -//----------------------------------------------------------------------------- - -package usecase - -import ( - "context" - "errors" - "fmt" - "strings" - - "t73f.de/r/zsc/api" - "zettelstore.de/z/ast" - "zettelstore.de/z/box" - "zettelstore.de/z/collect" - "zettelstore.de/z/parser" - "zettelstore.de/z/query" - "zettelstore.de/z/strfun" - "zettelstore.de/z/zettel" - "zettelstore.de/z/zettel/id" - "zettelstore.de/z/zettel/meta" -) - -// QueryPort is the interface used by this use case. -type QueryPort interface { - GetZettel(ctx context.Context, zid id.Zid) (zettel.Zettel, error) - GetMeta(ctx context.Context, zid id.Zid) (*meta.Meta, error) - SelectMeta(ctx context.Context, metaSeq []*meta.Meta, q *query.Query) ([]*meta.Meta, error) -} - -// Query is the data for this use case. -type Query struct { - port QueryPort - ucEvaluate Evaluate -} - -// NewQuery creates a new use case. -func NewQuery(port QueryPort) Query { - return Query{port: port} -} - -// SetEvaluate sets the usecase Evaluate, because of circular dependencies. -func (uc *Query) SetEvaluate(ucEvaluate *Evaluate) { uc.ucEvaluate = *ucEvaluate } - -// Run executes the use case. -func (uc *Query) Run(ctx context.Context, q *query.Query) ([]*meta.Meta, error) { - zids := q.GetZids() - if zids == nil { - return uc.port.SelectMeta(ctx, nil, q) - } - if len(zids) == 0 { - return nil, nil - } - metaSeq, err := uc.getMetaZid(ctx, zids) - if err != nil { - return metaSeq, err - } - if metaSeq = uc.processDirectives(ctx, metaSeq, q.GetDirectives()); len(metaSeq) > 0 { - return uc.port.SelectMeta(ctx, metaSeq, q) - } - return nil, nil -} - -func (uc *Query) getMetaZid(ctx context.Context, zids []id.Zid) ([]*meta.Meta, error) { - metaSeq := make([]*meta.Meta, 0, len(zids)) - for _, zid := range zids { - m, err := uc.port.GetMeta(ctx, zid) - if err == nil { - metaSeq = append(metaSeq, m) - continue - } - if errors.Is(err, &box.ErrNotAllowed{}) { - continue - } - return metaSeq, err - } - return metaSeq, nil -} - -func (uc *Query) processDirectives(ctx context.Context, metaSeq []*meta.Meta, directives []query.Directive) []*meta.Meta { - if len(directives) == 0 { - return metaSeq - } - for _, dir := range directives { - if len(metaSeq) == 0 { - return nil - } - switch ds := dir.(type) { - case *query.ContextSpec: - metaSeq = uc.processContextDirective(ctx, ds, metaSeq) - case *query.IdentSpec: - // Nothing to do. - case *query.ItemsSpec: - metaSeq = uc.processItemsDirective(ctx, ds, metaSeq) - case *query.UnlinkedSpec: - metaSeq = uc.processUnlinkedDirective(ctx, ds, metaSeq) - default: - panic(fmt.Sprintf("Unknown directive %T", ds)) - } - } - if len(metaSeq) == 0 { - return nil - } - return metaSeq -} -func (uc *Query) processContextDirective(ctx context.Context, spec *query.ContextSpec, metaSeq []*meta.Meta) []*meta.Meta { - return spec.Execute(ctx, metaSeq, uc.port) -} - -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, meta.DefaultSyntax)) - if err != nil { - continue - } - for _, ref := range collect.Order(zn) { - if collectedZid, err2 := id.Parse(ref.URL.Path); err2 == nil { - if z, err3 := uc.port.GetZettel(ctx, collectedZid); err3 == nil { - result = append(result, z.Meta) - } - } - } - } - return result -} - -func (uc *Query) processUnlinkedDirective(ctx context.Context, spec *query.UnlinkedSpec, metaSeq []*meta.Meta) []*meta.Meta { - words := spec.GetWords(metaSeq) - if len(words) == 0 { - return metaSeq - } - var sb strings.Builder - for _, word := range words { - sb.WriteString(" :") - sb.WriteString(word) - } - q := (*query.Query)(nil).Parse(sb.String()) - candidates, err := uc.port.SelectMeta(ctx, nil, q) - if err != nil { - return nil - } - metaZids := id.NewSetCap(len(metaSeq)) - refZids := id.NewSetCap(len(metaSeq) * 4) // Assumption: there are four zids per zettel - for _, m := range metaSeq { - metaZids.Add(m.Zid) - refZids.Add(m.Zid) - for _, pair := range m.ComputedPairsRest() { - switch meta.Type(pair.Key) { - case meta.TypeID: - if zid, errParse := id.Parse(pair.Value); errParse == nil { - refZids.Add(zid) - } - case meta.TypeIDSet: - for _, value := range meta.ListFromValue(pair.Value) { - if zid, errParse := id.Parse(value); errParse == nil { - refZids.Add(zid) - } - } - } - } - } - candidates = filterByZid(candidates, refZids) - return uc.filterCandidates(ctx, candidates, words) -} - -func filterByZid(candidates []*meta.Meta, ignoreSeq id.Set) []*meta.Meta { - result := make([]*meta.Meta, 0, len(candidates)) - for _, m := range candidates { - if !ignoreSeq.ContainsOrNil(m.Zid) { - result = append(result, m) - } - } - return result -} - -func (uc *Query) filterCandidates(ctx context.Context, candidates []*meta.Meta, words []string) []*meta.Meta { - result := make([]*meta.Meta, 0, len(candidates)) -candLoop: - for _, cand := range candidates { - zettel, err := uc.port.GetZettel(ctx, cand.Zid) - if err != nil { - continue - } - v := unlinkedVisitor{ - words: words, - found: false, - } - v.text = v.joinWords(words) - - for _, pair := range zettel.Meta.Pairs() { - if meta.Type(pair.Key) != meta.TypeZettelmarkup { - continue - } - is := uc.ucEvaluate.RunMetadata(ctx, pair.Value) - ast.Walk(&v, &is) - if v.found { - result = append(result, cand) - continue candLoop - } - } - - 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) - if v.found { - result = append(result, cand) - } - } - return result -} - -func (*unlinkedVisitor) joinWords(words []string) string { - return " " + strings.ToLower(strings.Join(words, " ")) + " " -} - -type unlinkedVisitor struct { - words []string - text string - found bool -} - -func (v *unlinkedVisitor) Visit(node ast.Node) ast.Visitor { - switch n := node.(type) { - case *ast.InlineSlice: - v.checkWords(n) - return nil - case *ast.HeadingNode: - return nil - case *ast.LinkNode, *ast.EmbedRefNode, *ast.EmbedBLOBNode, *ast.CiteNode: - return nil - } - return v -} - -func (v *unlinkedVisitor) checkWords(is *ast.InlineSlice) { - if len(*is) < 2*len(v.words)-1 { - return - } - for _, text := range v.splitInlineTextList(is) { - if strings.Contains(text, v.text) { - v.found = true - } - } -} - -func (v *unlinkedVisitor) splitInlineTextList(is *ast.InlineSlice) []string { - var result []string - var curList []string - for _, in := range *is { - switch n := in.(type) { - case *ast.TextNode: - curList = append(curList, strfun.MakeWords(n.Text)...) - case *ast.SpaceNode: - default: - if curList != nil { - result = append(result, v.joinWords(curList)) - curList = nil - } - } - } - if curList != nil { - result = append(result, v.joinWords(curList)) - } - return result -} DELETED usecase/refresh.go Index: usecase/refresh.go ================================================================== --- usecase/refresh.go +++ usecase/refresh.go @@ -1,43 +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. -// -// SPDX-License-Identifier: EUPL-1.2 -// SPDX-FileCopyrightText: 2021-present Detlef Stern -//----------------------------------------------------------------------------- - -package usecase - -import ( - "context" - - "zettelstore.de/z/logger" -) - -// RefreshPort is the interface used by this use case. -type RefreshPort interface { - Refresh(context.Context) error -} - -// Refresh is the data for this use case. -type Refresh struct { - log *logger.Logger - port RefreshPort -} - -// NewRefresh creates a new use case. -func NewRefresh(log *logger.Logger, port RefreshPort) Refresh { - return Refresh{log: log, port: port} -} - -// Run executes the use case. -func (uc *Refresh) Run(ctx context.Context) error { - err := uc.port.Refresh(ctx) - uc.log.Info().User(ctx).Err(err).Msg("Refresh internal data") - return err -} DELETED usecase/reindex.go Index: usecase/reindex.go ================================================================== --- usecase/reindex.go +++ usecase/reindex.go @@ -1,44 +0,0 @@ -//----------------------------------------------------------------------------- -// 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 usecase - -import ( - "context" - - "zettelstore.de/z/logger" - "zettelstore.de/z/zettel/id" -) - -// ReIndexPort is the interface used by this use case. -type ReIndexPort interface { - ReIndex(context.Context, id.Zid) error -} - -// ReIndex is the data for this use case. -type ReIndex struct { - log *logger.Logger - port ReIndexPort -} - -// NewReIndex creates a new use case. -func NewReIndex(log *logger.Logger, port ReIndexPort) ReIndex { - return ReIndex{log: log, port: port} -} - -// Run executes the use case. -func (uc *ReIndex) Run(ctx context.Context, zid id.Zid) error { - err := uc.port.ReIndex(ctx, zid) - 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 @@ -1,65 +1,62 @@ //----------------------------------------------------------------------------- -// Copyright (c) 2020-present Detlef Stern +// Copyright (c) 2020-2021 Detlef Stern // -// This file is part of Zettelstore. +// This file is part of zettelstore. // // Zettelstore is licensed under the latest version of the EUPL (European Union // Public License). Please see file LICENSE.txt for your rights and obligations // under this license. -// -// SPDX-License-Identifier: EUPL-1.2 -// SPDX-FileCopyrightText: 2020-present Detlef Stern //----------------------------------------------------------------------------- +// Package usecase provides (business) use cases for the zettelstore. package usecase import ( "context" "zettelstore.de/z/box" - "zettelstore.de/z/logger" - "zettelstore.de/z/zettel" - "zettelstore.de/z/zettel/id" + "zettelstore.de/z/domain/id" + "zettelstore.de/z/domain/meta" ) // RenameZettelPort is the interface used by this use case. type RenameZettelPort interface { - GetZettel(ctx context.Context, zid id.Zid) (zettel.Zettel, error) + // GetMeta retrieves just the meta data of a specific zettel. + GetMeta(ctx context.Context, zid id.Zid) (*meta.Meta, error) + + // Rename changes the current id to a new id. RenameZettel(ctx context.Context, curZid, newZid id.Zid) error } // RenameZettel is the data for this use case. type RenameZettel struct { - log *logger.Logger port RenameZettelPort } // ErrZidInUse is returned if the zettel id is not appropriate for the box operation. type ErrZidInUse struct{ Zid id.Zid } -func (err ErrZidInUse) Error() string { +func (err *ErrZidInUse) Error() string { return "Zettel id already in use: " + err.Zid.String() } // NewRenameZettel creates a new use case. -func NewRenameZettel(log *logger.Logger, port RenameZettelPort) RenameZettel { - return RenameZettel{log: log, port: port} +func NewRenameZettel(port RenameZettelPort) RenameZettel { + return RenameZettel{port: port} } // Run executes the use case. -func (uc *RenameZettel) Run(ctx context.Context, curZid, newZid id.Zid) error { +func (uc RenameZettel) Run(ctx context.Context, curZid, newZid id.Zid) error { noEnrichCtx := box.NoEnrichContext(ctx) - if _, err := uc.port.GetZettel(noEnrichCtx, curZid); err != nil { + if _, err := uc.port.GetMeta(noEnrichCtx, curZid); err != nil { return err } if newZid == curZid { // Nothing to do return nil } - if _, err := uc.port.GetZettel(noEnrichCtx, newZid); err == nil { - return ErrZidInUse{Zid: newZid} + if _, err := uc.port.GetMeta(noEnrichCtx, newZid); err == nil { + return &ErrZidInUse{Zid: newZid} } - err := uc.port.RenameZettel(ctx, curZid, newZid) - uc.log.Info().User(ctx).Zid(curZid).Err(err).Zid(newZid).Msg("Rename zettel") - return err + return uc.port.RenameZettel(ctx, curZid, newZid) } ADDED usecase/search.go Index: usecase/search.go ================================================================== --- usecase/search.go +++ usecase/search.go @@ -0,0 +1,44 @@ +//----------------------------------------------------------------------------- +// Copyright (c) 2020-2021 Detlef Stern +// +// This file is part of zettelstore. +// +// Zettelstore is licensed under the latest version of the EUPL (European Union +// Public License). Please see file LICENSE.txt for your rights and obligations +// under this license. +//----------------------------------------------------------------------------- + +// Package usecase provides (business) use cases for the zettelstore. +package usecase + +import ( + "context" + + "zettelstore.de/z/box" + "zettelstore.de/z/domain/meta" + "zettelstore.de/z/search" +) + +// SearchPort is the interface used by this use case. +type SearchPort interface { + // SelectMeta returns all zettel meta data that match the selection criteria. + SelectMeta(ctx context.Context, s *search.Search) ([]*meta.Meta, error) +} + +// Search is the data for this use case. +type Search struct { + port SearchPort +} + +// NewSearch creates a new use case. +func NewSearch(port SearchPort) Search { + return Search{port: port} +} + +// Run executes the use case. +func (uc Search) Run(ctx context.Context, s *search.Search) ([]*meta.Meta, error) { + if !s.HasComputedMetaKey() { + ctx = box.NoEnrichContext(ctx) + } + return uc.port.SelectMeta(ctx, s) +} Index: usecase/update_zettel.go ================================================================== --- usecase/update_zettel.go +++ usecase/update_zettel.go @@ -1,78 +1,62 @@ //----------------------------------------------------------------------------- -// Copyright (c) 2020-present Detlef Stern +// Copyright (c) 2020-2021 Detlef Stern // -// This file is part of Zettelstore. +// This file is part of zettelstore. // // Zettelstore is licensed under the latest version of the EUPL (European Union // Public License). Please see file LICENSE.txt for your rights and obligations // under this license. -// -// SPDX-License-Identifier: EUPL-1.2 -// SPDX-FileCopyrightText: 2020-present Detlef Stern //----------------------------------------------------------------------------- +// Package usecase provides (business) use cases for the zettelstore. package usecase import ( "context" - "t73f.de/r/zsc/api" "zettelstore.de/z/box" - "zettelstore.de/z/logger" - "zettelstore.de/z/zettel" - "zettelstore.de/z/zettel/id" - "zettelstore.de/z/zettel/meta" + "zettelstore.de/z/domain" + "zettelstore.de/z/domain/id" + "zettelstore.de/z/domain/meta" ) // UpdateZettelPort is the interface used by this use case. type UpdateZettelPort interface { // GetZettel retrieves a specific zettel. - GetZettel(ctx context.Context, zid id.Zid) (zettel.Zettel, error) + GetZettel(ctx context.Context, zid id.Zid) (domain.Zettel, error) // UpdateZettel updates an existing zettel. - UpdateZettel(ctx context.Context, zettel zettel.Zettel) error + UpdateZettel(ctx context.Context, zettel domain.Zettel) error } // UpdateZettel is the data for this use case. type UpdateZettel struct { - log *logger.Logger port UpdateZettelPort } // NewUpdateZettel creates a new use case. -func NewUpdateZettel(log *logger.Logger, port UpdateZettelPort) UpdateZettel { - return UpdateZettel{log: log, port: port} +func NewUpdateZettel(port UpdateZettelPort) UpdateZettel { + return UpdateZettel{port: port} } // Run executes the use case. -func (uc *UpdateZettel) Run(ctx context.Context, zettel zettel.Zettel, hasContent bool) error { +func (uc UpdateZettel) Run(ctx context.Context, zettel domain.Zettel, hasContent bool) error { m := zettel.Meta oldZettel, err := uc.port.GetZettel(box.NoEnrichContext(ctx), m.Zid) if err != nil { return err } if zettel.Equal(oldZettel, false) { return nil } - - // Update relevant computed, but stored values. - if _, found := m.Get(api.KeyCreated); !found { - if val, crFound := oldZettel.Meta.Get(api.KeyCreated); crFound { - m.Set(api.KeyCreated, val) - } - } - m.SetNow(api.KeyModified) - + m.SetNow(meta.KeyModified) m.YamlSep = oldZettel.Meta.YamlSep if m.Zid == id.ConfigurationZid { - m.Set(api.KeySyntax, meta.SyntaxNone) + m.Set(meta.KeySyntax, meta.ValueSyntaxNone) } - if !hasContent { zettel.Content = oldZettel.Content + zettel.Content.TrimSpace() } - zettel.Content.TrimSpace() - err = uc.port.UpdateZettel(ctx, zettel) - uc.log.Info().User(ctx).Zid(m.Zid).Err(err).Msg("Update zettel") - return err + return uc.port.UpdateZettel(ctx, zettel) } DELETED usecase/usecase.go Index: usecase/usecase.go ================================================================== --- usecase/usecase.go +++ usecase/usecase.go @@ -1,15 +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. -// -// SPDX-License-Identifier: EUPL-1.2 -// SPDX-FileCopyrightText: 2021-present Detlef Stern -//----------------------------------------------------------------------------- - -// Package usecase provides (business) use cases for the zettelstore. -package usecase DELETED usecase/version.go Index: usecase/version.go ================================================================== --- usecase/version.go +++ usecase/version.go @@ -1,79 +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. -// -// SPDX-License-Identifier: EUPL-1.2 -// SPDX-FileCopyrightText: 2022-present Detlef Stern -//----------------------------------------------------------------------------- - -package usecase - -import ( - "regexp" - "strconv" - - "zettelstore.de/z/kernel" -) - -// Version is the data for this use case. -type Version struct { - vr VersionResult -} - -// NewVersion creates a new use case. -func NewVersion(version string) Version { - return Version{calculateVersionResult(version)} -} - -// VersionResult is the data structure returned by this usecase. -type VersionResult struct { - Major int - Minor int - Patch int - Info string - Hash string -} - -var invalidVersion = VersionResult{ - Major: -1, - Minor: -1, - Patch: -1, - Info: kernel.CoreDefaultVersion, - Hash: "", -} - -var reVersion = regexp.MustCompile(`^(\d+)\.(\d+)(\.(\d+))?(-(([[:alnum:]]|-)+))?(\+(([[:alnum:]])+(-[[:alnum:]]+)?))?`) - -func calculateVersionResult(version string) VersionResult { - match := reVersion.FindStringSubmatch(version) - if len(match) < 12 { - return invalidVersion - } - major, err := strconv.Atoi(match[1]) - if err != nil { - return invalidVersion - } - minor, err := strconv.Atoi(match[2]) - if err != nil { - return invalidVersion - } - patch, err := strconv.Atoi(match[4]) - if err != nil { - patch = 0 - } - return VersionResult{ - Major: major, - Minor: minor, - Patch: patch, - Info: match[6], - Hash: match[9], - } -} - -// Run executes the use case. -func (uc Version) Run() VersionResult { return uc.vr } DELETED web/adapter/adapter.go Index: web/adapter/adapter.go ================================================================== --- web/adapter/adapter.go +++ web/adapter/adapter.go @@ -1,49 +0,0 @@ -//----------------------------------------------------------------------------- -// 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" - - "t73f.de/r/zsc/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 @@ -1,109 +1,63 @@ //----------------------------------------------------------------------------- -// Copyright (c) 2021-present Detlef Stern +// Copyright (c) 2021 Detlef Stern // -// This file is part of Zettelstore. +// This file is part of zettelstore. // // Zettelstore is licensed under the latest version of the EUPL (European Union // Public License). Please see file LICENSE.txt for your rights and obligations // under this license. -// -// SPDX-License-Identifier: EUPL-1.2 -// SPDX-FileCopyrightText: 2021-present Detlef Stern //----------------------------------------------------------------------------- // Package api provides api handlers for web requests. package api import ( - "bytes" "context" - "net/http" "time" - "t73f.de/r/zsc/api" + "zettelstore.de/z/api" "zettelstore.de/z/auth" "zettelstore.de/z/config" + "zettelstore.de/z/domain/meta" "zettelstore.de/z/kernel" - "zettelstore.de/z/logger" - "zettelstore.de/z/web/adapter" "zettelstore.de/z/web/server" - "zettelstore.de/z/zettel/meta" ) // API holds all data and methods for delivering API call results. type API struct { - log *logger.Logger b server.Builder + rtConfig config.Config authz auth.AuthzManager token auth.TokenManager - rtConfig config.Config - policy auth.Policy + auth server.Auth tokenLifetime time.Duration } // New creates a new API object. -func New(log *logger.Logger, b server.Builder, authz auth.AuthzManager, token auth.TokenManager, - rtConfig config.Config, pol auth.Policy) *API { - a := &API{ - log: log, +func New(b server.Builder, authz auth.AuthzManager, token auth.TokenManager, auth server.Auth, rtConfig config.Config) *API { + api := &API{ b: b, authz: authz, token: token, + auth: auth, rtConfig: rtConfig, - policy: pol, tokenLifetime: kernel.Main.GetConfig(kernel.WebService, kernel.WebTokenLifetimeAPI).(time.Duration), } - return a + return api } +// GetURLPrefix returns the configured URL prefix of the web server. +func (api *API) GetURLPrefix() string { return api.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) -} -func (a *API) withAuth() bool { return a.authz.WithAuth() } -func (a *API) getToken(ident *meta.Meta) ([]byte, error) { - return a.token.GetToken(ident, a.tokenLifetime, auth.KindAPI) -} - -func (a *API) reportUsecaseError(w http.ResponseWriter, err error) { - code, text := adapter.CodeMessageFromError(err) - if code == http.StatusInternalServerError { - a.log.Error().Err(err).Msg(text) - http.Error(w, http.StatusText(code), code) - return - } - // TODO: must call PrepareHeader somehow - http.Error(w, text, code) -} - -func writeBuffer(w http.ResponseWriter, buf *bytes.Buffer, contentType string) error { - return adapter.WriteData(w, buf.Bytes(), contentType) -} - -func (a *API) getRights(ctx context.Context, m *meta.Meta) (result api.ZettelRights) { - pol := a.policy - user := server.GetUser(ctx) - if pol.CanCreate(user, m) { - result |= api.ZettelCanCreate - } - if pol.CanRead(user, m) { - result |= api.ZettelCanRead - } - if pol.CanWrite(user, m, m) { - result |= api.ZettelCanWrite - } - if pol.CanRename(user, m) { - result |= api.ZettelCanRename - } - if pol.CanDelete(user, m) { - result |= api.ZettelCanDelete - } - if result == 0 { - return api.ZettelCanNone - } - return result +func (api *API) NewURLBuilder(key byte) *api.URLBuilder { return api.b.NewURLBuilder(key) } + +func (api *API) getAuthData(ctx context.Context) *server.AuthData { + return api.auth.GetAuthData(ctx) +} +func (api *API) withAuth() bool { return api.authz.WithAuth() } +func (api *API) getToken(ident *meta.Meta) ([]byte, error) { + return api.token.GetToken(ident, api.tokenLifetime, auth.KindJSON) } DELETED web/adapter/api/command.go Index: web/adapter/api/command.go ================================================================== --- web/adapter/api/command.go +++ web/adapter/api/command.go @@ -1,59 +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. -// -// SPDX-License-Identifier: EUPL-1.2 -// SPDX-FileCopyrightText: 2021-present Detlef Stern -//----------------------------------------------------------------------------- - -package api - -import ( - "context" - "net/http" - - "t73f.de/r/zsc/api" - "zettelstore.de/z/usecase" -) - -// MakePostCommandHandler creates a new HTTP handler to execute certain commands. -func (a *API) MakePostCommandHandler( - ucIsAuth *usecase.IsAuthenticated, - ucRefresh *usecase.Refresh, -) http.HandlerFunc { - return func(w http.ResponseWriter, r *http.Request) { - ctx := r.Context() - switch api.Command(r.URL.Query().Get(api.QueryKeyCommand)) { - case api.CommandAuthenticated: - handleIsAuthenticated(ctx, w, ucIsAuth) - return - case api.CommandRefresh: - err := ucRefresh.Run(ctx) - if err != nil { - a.reportUsecaseError(w, err) - return - } - w.WriteHeader(http.StatusNoContent) - return - } - http.Error(w, "Unknown command", http.StatusBadRequest) - } -} - -func handleIsAuthenticated(ctx context.Context, w http.ResponseWriter, ucIsAuth *usecase.IsAuthenticated) { - switch ucIsAuth.Run(ctx) { - case usecase.IsAuthenticatedDisabled: - w.WriteHeader(http.StatusOK) - case usecase.IsAuthenticatedAndValid: - w.WriteHeader(http.StatusNoContent) - case usecase.IsAuthenticatedAndInvalid: - w.WriteHeader(http.StatusUnauthorized) - default: - http.Error(w, "Unexpected result value", http.StatusInternalServerError) - } -} ADDED web/adapter/api/content_type.go Index: web/adapter/api/content_type.go ================================================================== --- web/adapter/api/content_type.go +++ web/adapter/api/content_type.go @@ -0,0 +1,59 @@ +//----------------------------------------------------------------------------- +// Copyright (c) 2020-2021 Detlef Stern +// +// This file is part of zettelstore. +// +// Zettelstore is licensed under the latest version of the EUPL (European Union +// Public License). Please see file LICENSE.txt for your rights and obligations +// under this license. +//----------------------------------------------------------------------------- + +// Package api provides api handlers for web requests. +package api + +import "zettelstore.de/z/api" + +const plainText = "text/plain; charset=utf-8" + +var mapFormat2CT = map[api.EncodingEnum]string{ + api.EncoderHTML: "text/html; charset=utf-8", + api.EncoderNative: plainText, + api.EncoderJSON: "application/json", + api.EncoderDJSON: "application/json", + api.EncoderText: plainText, + api.EncoderZmk: plainText, + api.EncoderRaw: plainText, // In some cases... +} + +func format2ContentType(format api.EncodingEnum) string { + ct, ok := mapFormat2CT[format] + if !ok { + return "application/octet-stream" + } + return ct +} + +var mapSyntax2CT = map[string]string{ + "css": "text/css; charset=utf-8", + "gif": "image/gif", + "html": "text/html; charset=utf-8", + "jpeg": "image/jpeg", + "jpg": "image/jpeg", + "js": "text/javascript; charset=utf-8", + "pdf": "application/pdf", + "png": "image/png", + "svg": "image/svg+xml", + "xml": "text/xml; charset=utf-8", + "zmk": "text/x-zmk; charset=utf-8", + "plain": plainText, + "text": plainText, + "markdown": "text/markdown; charset=utf-8", + "md": "text/markdown; charset=utf-8", + "mustache": plainText, + //"graphviz": "text/vnd.graphviz; charset=utf-8", +} + +func syntax2contentType(syntax string) (string, bool) { + contentType, ok := mapSyntax2CT[syntax] + return contentType, ok +} Index: web/adapter/api/create_zettel.go ================================================================== --- web/adapter/api/create_zettel.go +++ web/adapter/api/create_zettel.go @@ -1,78 +1,48 @@ //----------------------------------------------------------------------------- -// Copyright (c) 2021-present Detlef Stern +// Copyright (c) 2021 Detlef Stern // -// This file is part of Zettelstore. +// This file is part of zettelstore. // // Zettelstore is licensed under the latest version of the EUPL (European Union // Public License). Please see file LICENSE.txt for your rights and obligations // under this license. -// -// SPDX-License-Identifier: EUPL-1.2 -// SPDX-FileCopyrightText: 2021-present Detlef Stern //----------------------------------------------------------------------------- +// Package api provides api handlers for web requests. package api import ( "net/http" - "t73f.de/r/sx" - "t73f.de/r/zsc/api" + zsapi "zettelstore.de/z/api" + "zettelstore.de/z/domain/id" "zettelstore.de/z/usecase" "zettelstore.de/z/web/adapter" - "zettelstore.de/z/web/content" - "zettelstore.de/z/zettel" - "zettelstore.de/z/zettel/id" ) // MakePostCreateZettelHandler creates a new HTTP handler to store content of // an existing zettel. -func (a *API) MakePostCreateZettelHandler(createZettel *usecase.CreateZettel) http.HandlerFunc { - return func(w http.ResponseWriter, r *http.Request) { - q := r.URL.Query() - enc, encStr := getEncoding(r, q) - var zettel zettel.Zettel - var err error - switch enc { - case api.EncoderPlain: - zettel, err = buildZettelFromPlainData(r, id.Invalid) - case api.EncoderData: - zettel, err = buildZettelFromData(r, id.Invalid) - default: - http.Error(w, http.StatusText(http.StatusBadRequest), http.StatusBadRequest) - return - } - if err != nil { - a.reportUsecaseError(w, adapter.NewErrBadRequest(err.Error())) - return - } - - ctx := r.Context() - newZid, err := createZettel.Run(ctx, zettel) - if err != nil { - a.reportUsecaseError(w, err) - return - } - - var result []byte - var contentType 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).String()) - contentType = content.SXPF - default: - panic(encStr) - } - - h := adapter.PrepareHeader(w, contentType) - h.Set(api.HeaderLocation, location.String()) - w.WriteHeader(http.StatusCreated) - if _, err = w.Write(result); err != nil { - a.log.Error().Err(err).Zid(newZid).Msg("Create Zettel") +func (api *API) MakePostCreateZettelHandler(createZettel usecase.CreateZettel) http.HandlerFunc { + return func(w http.ResponseWriter, r *http.Request) { + ctx := r.Context() + zettel, err := buildZettelFromData(r, id.Invalid) + if err != nil { + adapter.ReportUsecaseError(w, adapter.NewErrBadRequest(err.Error())) + return + } + + newZid, err := createZettel.Run(ctx, zettel) + if err != nil { + adapter.ReportUsecaseError(w, err) + return + } + u := api.NewURLBuilder('z').SetZid(newZid).String() + h := w.Header() + h.Set(zsapi.HeaderContentType, format2ContentType(zsapi.EncoderJSON)) + h.Set(zsapi.HeaderLocation, u) + w.WriteHeader(http.StatusCreated) + if err = encodeJSONData(w, zsapi.ZidJSON{ID: newZid.String(), URL: u}); err != nil { + adapter.InternalServerError(w, "Write JSON", err) } } } Index: web/adapter/api/delete_zettel.go ================================================================== --- web/adapter/api/delete_zettel.go +++ web/adapter/api/delete_zettel.go @@ -1,38 +1,37 @@ //----------------------------------------------------------------------------- -// Copyright (c) 2021-present Detlef Stern +// Copyright (c) 2021 Detlef Stern // -// This file is part of Zettelstore. +// This file is part of zettelstore. // // Zettelstore is licensed under the latest version of the EUPL (European Union // Public License). Please see file LICENSE.txt for your rights and obligations // under this license. -// -// SPDX-License-Identifier: EUPL-1.2 -// SPDX-FileCopyrightText: 2021-present Detlef Stern //----------------------------------------------------------------------------- +// Package api provides api handlers for web requests. package api import ( "net/http" + "zettelstore.de/z/domain/id" "zettelstore.de/z/usecase" - "zettelstore.de/z/zettel/id" + "zettelstore.de/z/web/adapter" ) // MakeDeleteZettelHandler creates a new HTTP handler to delete a zettel. -func (a *API) MakeDeleteZettelHandler(deleteZettel *usecase.DeleteZettel) http.HandlerFunc { +func (api *API) MakeDeleteZettelHandler(deleteZettel usecase.DeleteZettel) http.HandlerFunc { return func(w http.ResponseWriter, r *http.Request) { zid, err := id.Parse(r.URL.Path[1:]) if err != nil { http.NotFound(w, r) return } - if err = deleteZettel.Run(r.Context(), zid); err != nil { - a.reportUsecaseError(w, err) + if err := deleteZettel.Run(r.Context(), zid); err != nil { + adapter.ReportUsecaseError(w, err) return } w.WriteHeader(http.StatusNoContent) } } DELETED web/adapter/api/get_data.go Index: web/adapter/api/get_data.go ================================================================== --- web/adapter/api/get_data.go +++ web/adapter/api/get_data.go @@ -1,39 +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. -// -// SPDX-License-Identifier: EUPL-1.2 -// SPDX-FileCopyrightText: 2022-present Detlef Stern -//----------------------------------------------------------------------------- - -package api - -import ( - "net/http" - - "t73f.de/r/sx" - "zettelstore.de/z/usecase" - "zettelstore.de/z/zettel/id" -) - -// MakeGetDataHandler creates a new HTTP handler to return zettelstore data. -func (a *API) MakeGetDataHandler(ucVersion usecase.Version) http.HandlerFunc { - return func(w http.ResponseWriter, r *http.Request) { - version := ucVersion.Run() - err := a.writeObject(w, id.Invalid, sx.MakeList( - sx.Int64(version.Major), - sx.Int64(version.Minor), - sx.Int64(version.Patch), - sx.MakeString(version.Info), - sx.MakeString(version.Hash), - )) - if err != nil { - a.log.Error().Err(err).Msg("Write Version Info") - } - } -} ADDED web/adapter/api/get_links.go Index: web/adapter/api/get_links.go ================================================================== --- web/adapter/api/get_links.go +++ web/adapter/api/get_links.go @@ -0,0 +1,218 @@ +//----------------------------------------------------------------------------- +// Copyright (c) 2020-2021 Detlef Stern +// +// This file is part of zettelstore. +// +// Zettelstore is licensed under the latest version of the EUPL (European Union +// Public License). Please see file LICENSE.txt for your rights and obligations +// under this license. +//----------------------------------------------------------------------------- + +// Package api provides api handlers for web requests. +package api + +import ( + "net/http" + "strconv" + + zsapi "zettelstore.de/z/api" + "zettelstore.de/z/ast" + "zettelstore.de/z/collect" + "zettelstore.de/z/domain/id" + "zettelstore.de/z/domain/meta" + "zettelstore.de/z/usecase" + "zettelstore.de/z/web/adapter" +) + +// MakeGetLinksHandler creates a new API handler to return links to other material. +func (api *API) MakeGetLinksHandler(parseZettel usecase.ParseZettel) http.HandlerFunc { + return func(w http.ResponseWriter, r *http.Request) { + zid, err := id.Parse(r.URL.Path[1:]) + if err != nil { + http.NotFound(w, r) + return + } + ctx := r.Context() + q := r.URL.Query() + zn, err := parseZettel.Run(ctx, zid, q.Get(meta.KeySyntax)) + if err != nil { + adapter.ReportUsecaseError(w, err) + return + } + summary := collect.References(zn) + + kind := getKindFromValue(q.Get("kind")) + matter := getMatterFromValue(q.Get("matter")) + if !validKindMatter(kind, matter) { + adapter.BadRequest(w, "Invalid kind/matter") + return + } + + outData := zsapi.ZettelLinksJSON{ + ID: zid.String(), + URL: api.NewURLBuilder('z').SetZid(zid).String(), + } + if kind&kindLink != 0 { + api.setupLinkJSONRefs(summary, matter, &outData) + if matter&matterMeta != 0 { + for _, p := range zn.Meta.PairsRest(false) { + if meta.Type(p.Key) == meta.TypeURL { + outData.Links.Meta = append(outData.Links.Meta, p.Value) + } + } + } + } + if kind&kindImage != 0 { + api.setupImageJSONRefs(summary, matter, &outData) + } + if kind&kindCite != 0 { + outData.Cites = stringCites(summary.Cites) + } + + w.Header().Set(zsapi.HeaderContentType, format2ContentType(zsapi.EncoderJSON)) + encodeJSONData(w, outData) + } +} + +func (api *API) setupLinkJSONRefs(summary collect.Summary, matter matterType, outData *zsapi.ZettelLinksJSON) { + if matter&matterIncoming != 0 { + // TODO: calculate incoming links from other zettel (via "backward" metadata?) + outData.Links.Incoming = []zsapi.ZidJSON{} + } + zetRefs, locRefs, extRefs := collect.DivideReferences(summary.Links) + if matter&matterOutgoing != 0 { + outData.Links.Outgoing = api.idURLRefs(zetRefs) + } + if matter&matterLocal != 0 { + outData.Links.Local = stringRefs(locRefs) + } + if matter&matterExternal != 0 { + outData.Links.External = stringRefs(extRefs) + } +} + +func (api *API) setupImageJSONRefs(summary collect.Summary, matter matterType, outData *zsapi.ZettelLinksJSON) { + zetRefs, locRefs, extRefs := collect.DivideReferences(summary.Images) + if matter&matterOutgoing != 0 { + outData.Images.Outgoing = api.idURLRefs(zetRefs) + } + if matter&matterLocal != 0 { + outData.Images.Local = stringRefs(locRefs) + } + if matter&matterExternal != 0 { + outData.Images.External = stringRefs(extRefs) + } +} + +func (api *API) idURLRefs(refs []*ast.Reference) []zsapi.ZidJSON { + result := make([]zsapi.ZidJSON, 0, len(refs)) + for _, ref := range refs { + path := ref.URL.Path + ub := api.NewURLBuilder('z').AppendPath(path) + if fragment := ref.URL.Fragment; len(fragment) > 0 { + ub.SetFragment(fragment) + } + result = append(result, zsapi.ZidJSON{ID: path, URL: ub.String()}) + } + return result +} + +func stringRefs(refs []*ast.Reference) []string { + result := make([]string, 0, len(refs)) + for _, ref := range refs { + result = append(result, ref.String()) + } + return result +} + +func stringCites(cites []*ast.CiteNode) []string { + mapKey := make(map[string]bool) + result := make([]string, 0, len(cites)) + for _, cn := range cites { + if _, ok := mapKey[cn.Key]; !ok { + mapKey[cn.Key] = true + result = append(result, cn.Key) + } + } + return result +} + +type kindType int + +const ( + _ kindType = 1 << iota + kindLink + kindImage + kindCite +) + +var mapKind = map[string]kindType{ + "": kindLink | kindImage | kindCite, + "link": kindLink, + "image": kindImage, + "cite": kindCite, + "both": kindLink | kindImage, + "all": kindLink | kindImage | kindCite, +} + +func getKindFromValue(value string) kindType { + if k, ok := mapKind[value]; ok { + return k + } + if n, err := strconv.Atoi(value); err == nil && n > 0 { + return kindType(n) + } + return 0 +} + +type matterType int + +const ( + _ matterType = 1 << iota + matterIncoming + matterOutgoing + matterLocal + matterExternal + matterMeta +) + +var mapMatter = map[string]matterType{ + "": matterIncoming | matterOutgoing | matterLocal | matterExternal | matterMeta, + "incoming": matterIncoming, + "outgoing": matterOutgoing, + "local": matterLocal, + "external": matterExternal, + "meta": matterMeta, + "zettel": matterIncoming | matterOutgoing, + "material": matterLocal | matterExternal | matterMeta, + "all": matterIncoming | matterOutgoing | matterLocal | matterExternal | matterMeta, +} + +func getMatterFromValue(value string) matterType { + if m, ok := mapMatter[value]; ok { + return m + } + if n, err := strconv.Atoi(value); err == nil && n > 0 { + return matterType(n) + } + return 0 +} + +func validKindMatter(kind kindType, matter matterType) bool { + if kind == 0 { + return false + } + if kind&kindLink != 0 { + return matter != 0 + } + if kind&kindImage != 0 { + if matter == 0 || matter == matterIncoming { + return false + } + return true + } + if kind&kindCite != 0 { + return matter == matterOutgoing + } + return false +} ADDED web/adapter/api/get_order.go Index: web/adapter/api/get_order.go ================================================================== --- web/adapter/api/get_order.go +++ web/adapter/api/get_order.go @@ -0,0 +1,41 @@ +//----------------------------------------------------------------------------- +// Copyright (c) 2021 Detlef Stern +// +// This file is part of zettelstore. +// +// Zettelstore is licensed under the latest version of the EUPL (European Union +// Public License). Please see file LICENSE.txt for your rights and obligations +// under this license. +//----------------------------------------------------------------------------- + +// Package api provides api handlers for web requests. +package api + +import ( + "net/http" + + "zettelstore.de/z/domain/id" + "zettelstore.de/z/domain/meta" + "zettelstore.de/z/usecase" + "zettelstore.de/z/web/adapter" +) + +// MakeGetOrderHandler creates a new API handler to return zettel references +// of a given zettel. +func (api *API) MakeGetOrderHandler(zettelOrder usecase.ZettelOrder) http.HandlerFunc { + return func(w http.ResponseWriter, r *http.Request) { + zid, err := id.Parse(r.URL.Path[1:]) + if err != nil { + http.NotFound(w, r) + return + } + ctx := r.Context() + q := r.URL.Query() + start, metas, err := zettelOrder.Run(ctx, zid, q.Get(meta.KeySyntax)) + if err != nil { + adapter.ReportUsecaseError(w, err) + return + } + api.writeMetaList(w, start, metas) + } +} ADDED web/adapter/api/get_role_list.go Index: web/adapter/api/get_role_list.go ================================================================== --- web/adapter/api/get_role_list.go +++ web/adapter/api/get_role_list.go @@ -0,0 +1,43 @@ +//----------------------------------------------------------------------------- +// Copyright (c) 2020-2021 Detlef Stern +// +// This file is part of zettelstore. +// +// Zettelstore is licensed under the latest version of the EUPL (European Union +// Public License). Please see file LICENSE.txt for your rights and obligations +// under this license. +//----------------------------------------------------------------------------- + +// Package api provides api handlers for web requests. +package api + +import ( + "fmt" + "net/http" + + zsapi "zettelstore.de/z/api" + "zettelstore.de/z/encoder" + "zettelstore.de/z/usecase" + "zettelstore.de/z/web/adapter" +) + +// MakeListRoleHandler creates a new HTTP handler for the use case "list some zettel". +func (api *API) MakeListRoleHandler(listRole usecase.ListRole) http.HandlerFunc { + return func(w http.ResponseWriter, r *http.Request) { + roleList, err := listRole.Run(r.Context()) + if err != nil { + adapter.ReportUsecaseError(w, err) + return + } + + format, formatText := adapter.GetFormat(r, r.URL.Query(), encoder.GetDefaultFormat()) + switch format { + case zsapi.EncoderJSON: + w.Header().Set(zsapi.HeaderContentType, format2ContentType(format)) + encodeJSONData(w, zsapi.RoleListJSON{Roles: roleList}) + default: + adapter.BadRequest(w, fmt.Sprintf("Role list not available in format %q", formatText)) + } + + } +} ADDED web/adapter/api/get_tags_list.go Index: web/adapter/api/get_tags_list.go ================================================================== --- web/adapter/api/get_tags_list.go +++ web/adapter/api/get_tags_list.go @@ -0,0 +1,52 @@ +//----------------------------------------------------------------------------- +// Copyright (c) 2020-2021 Detlef Stern +// +// This file is part of zettelstore. +// +// Zettelstore is licensed under the latest version of the EUPL (European Union +// Public License). Please see file LICENSE.txt for your rights and obligations +// under this license. +//----------------------------------------------------------------------------- + +// Package api provides api handlers for web requests. +package api + +import ( + "fmt" + "net/http" + "strconv" + + zsapi "zettelstore.de/z/api" + "zettelstore.de/z/encoder" + "zettelstore.de/z/usecase" + "zettelstore.de/z/web/adapter" +) + +// MakeListTagsHandler creates a new HTTP handler for the use case "list some zettel". +func (api *API) MakeListTagsHandler(listTags usecase.ListTags) http.HandlerFunc { + return func(w http.ResponseWriter, r *http.Request) { + iMinCount, _ := strconv.Atoi(r.URL.Query().Get("min")) + tagData, err := listTags.Run(r.Context(), iMinCount) + if err != nil { + adapter.ReportUsecaseError(w, err) + return + } + + format, formatText := adapter.GetFormat(r, r.URL.Query(), encoder.GetDefaultFormat()) + switch format { + case zsapi.EncoderJSON: + w.Header().Set(zsapi.HeaderContentType, format2ContentType(format)) + tagMap := make(map[string][]string, len(tagData)) + for tag, metaList := range tagData { + zidList := make([]string, 0, len(metaList)) + for _, m := range metaList { + zidList = append(zidList, m.Zid.String()) + } + tagMap[tag] = zidList + } + encodeJSONData(w, zsapi.TagListJSON{Tags: tagMap}) + default: + adapter.BadRequest(w, fmt.Sprintf("Tags list not available in format %q", formatText)) + } + } +} Index: web/adapter/api/get_zettel.go ================================================================== --- web/adapter/api/get_zettel.go +++ web/adapter/api/get_zettel.go @@ -1,182 +1,130 @@ //----------------------------------------------------------------------------- -// Copyright (c) 2020-present Detlef Stern +// Copyright (c) 2020-2021 Detlef Stern // -// This file is part of Zettelstore. +// This file is part of zettelstore. // // Zettelstore is licensed under the latest version of the EUPL (European Union // Public License). Please see file LICENSE.txt for your rights and obligations // under this license. -// -// SPDX-License-Identifier: EUPL-1.2 -// SPDX-FileCopyrightText: 2020-present Detlef Stern //----------------------------------------------------------------------------- +// Package api provides api handlers for web requests. package api import ( - "bytes" - "context" + "errors" "fmt" "net/http" - "t73f.de/r/sx" - "t73f.de/r/zsc/api" - "t73f.de/r/zsc/sexp" + zsapi "zettelstore.de/z/api" "zettelstore.de/z/ast" "zettelstore.de/z/box" + "zettelstore.de/z/config" + "zettelstore.de/z/domain/id" + "zettelstore.de/z/domain/meta" "zettelstore.de/z/encoder" - "zettelstore.de/z/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 { +// MakeGetZettelHandler creates a new HTTP handler to return a rendered zettel. +func (api *API) MakeGetZettelHandler(parseZettel usecase.ParseZettel, getMeta usecase.GetMeta) http.HandlerFunc { return func(w http.ResponseWriter, r *http.Request) { zid, err := id.Parse(r.URL.Path[1:]) if err != nil { http.NotFound(w, r) return } - q := r.URL.Query() - part := getPart(q, partContent) - ctx := r.Context() - switch enc, encStr := getEncoding(r, q); enc { - case api.EncoderPlain: - a.writePlainData(w, ctx, zid, part, getZettel) - - case api.EncoderData: - a.writeSzData(w, ctx, zid, part, getZettel) - - default: - var zn *ast.ZettelNode - var em func(value string) ast.InlineSlice - if q.Has(api.QueryKeyParseOnly) { - zn, err = parseZettel.Run(ctx, zid, q.Get(api.KeySyntax)) - em = parser.ParseMetadata - } else { - zn, err = evaluate.Run(ctx, zid, q.Get(api.KeySyntax)) - em = func(value string) ast.InlineSlice { - return evaluate.RunMetadata(ctx, value) - } - } - if err != nil { - a.reportUsecaseError(w, err) - return - } - a.writeEncodedZettelPart(ctx, w, zn, em, enc, encStr, part) - } - } -} - -func (a *API) writePlainData(w http.ResponseWriter, ctx context.Context, zid id.Zid, part partType, getZettel usecase.GetZettel) { - var buf bytes.Buffer - var contentType string - var err error - - z, err := getZettel.Run(box.NoEnrichContext(ctx), zid) - if err != nil { - a.reportUsecaseError(w, err) - return - } - - switch part { - case partZettel: - _, err = z.Meta.Write(&buf) - if err == nil { - err = buf.WriteByte('\n') - } - if err == nil { - _, err = z.Content.Write(&buf) - } - - case partMeta: - contentType = content.PlainText - _, err = z.Meta.Write(&buf) - - case partContent: - contentType = content.MIMEFromSyntax(z.Meta.GetDefault(api.KeySyntax, meta.DefaultSyntax)) - _, err = z.Content.Write(&buf) - } - - if err != nil { - 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") - } -} - -func (a *API) writeSzData(w http.ResponseWriter, ctx context.Context, zid id.Zid, part partType, getZettel usecase.GetZettel) { - z, err := getZettel.Run(ctx, zid) - if err != nil { - a.reportUsecaseError(w, err) - return - } - var obj sx.Object - switch part { - case partZettel: - zContent, zEncoding := z.Content.Encode() - obj = sexp.EncodeZettel(api.ZettelData{ - Meta: z.Meta.Map(), - Rights: a.getRights(ctx, z.Meta), - Encoding: zEncoding, - Content: zContent, - }) - - case partMeta: - obj = sexp.EncodeMetaRights(api.MetaRights{ - Meta: z.Meta.Map(), - Rights: a.getRights(ctx, z.Meta), - }) - } - if err = a.writeObject(w, zid, obj); err != nil { - a.log.Error().Err(err).Zid(zid).Msg("write sx data") - } -} - -func (a *API) writeEncodedZettelPart( - ctx context.Context, - w http.ResponseWriter, zn *ast.ZettelNode, - evalMeta encoder.EvalMetaFunc, - enc api.EncodingEnum, encStr string, part partType, -) { - encdr := encoder.Create( - enc, - &encoder.CreateParameter{ - Lang: a.rtConfig.Get(ctx, zn.InhMeta, api.KeyLang), - }) - if encdr == nil { - adapter.BadRequest(w, fmt.Sprintf("Zettel %q not available in encoding %q", zn.Meta.Zid, encStr)) - return - } - var err error - var buf bytes.Buffer - switch part { - case partZettel: - _, err = encdr.WriteZettel(&buf, zn, evalMeta) - case partMeta: - _, err = encdr.WriteMeta(&buf, zn.InhMeta, evalMeta) - case partContent: - _, err = encdr.WriteContent(&buf, zn) - } - if err != nil { - 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) - return - } - - if err = writeBuffer(w, &buf, content.MIMEFromEncoding(enc)); err != nil { - a.log.Error().Err(err).Zid(zn.Zid).Msg("Write Encoded Zettel") - } + ctx := r.Context() + q := r.URL.Query() + format, _ := adapter.GetFormat(r, q, encoder.GetDefaultFormat()) + if format == zsapi.EncoderRaw { + ctx = box.NoEnrichContext(ctx) + } + zn, err := parseZettel.Run(ctx, zid, q.Get(meta.KeySyntax)) + if err != nil { + adapter.ReportUsecaseError(w, err) + return + } + + part := getPart(q, partZettel) + if part == partUnknown { + adapter.BadRequest(w, "Unknown _part parameter") + return + } + switch format { + case zsapi.EncoderJSON, zsapi.EncoderDJSON: + w.Header().Set(zsapi.HeaderContentType, format2ContentType(format)) + err = api.getWriteMetaZettelFunc(ctx, format, part, partZettel, getMeta)(w, zn) + if err != nil { + adapter.InternalServerError(w, "Write D/JSON", err) + } + return + } + + env := encoder.Environment{ + LinkAdapter: adapter.MakeLinkAdapter(ctx, api, 'z', getMeta, part.DefString(partZettel), format), + ImageAdapter: adapter.MakeImageAdapter(ctx, api, getMeta), + CiteAdapter: nil, + Lang: config.GetLang(zn.InhMeta, api.rtConfig), + Xhtml: false, + MarkerExternal: "", + NewWindow: false, + IgnoreMeta: map[string]bool{meta.KeyLang: true}, + } + switch part { + case partZettel: + err = writeZettelPartZettel(w, zn, format, env) + case partMeta: + err = writeZettelPartMeta(w, zn, format) + case partContent: + err = api.writeZettelPartContent(w, zn, format, env) + } + if err != nil { + if errors.Is(err, adapter.ErrNoSuchFormat) { + adapter.BadRequest(w, fmt.Sprintf("Zettel %q not available in format %q", zid.String(), format)) + return + } + adapter.InternalServerError(w, "Get zettel", err) + } + } +} + +func writeZettelPartZettel(w http.ResponseWriter, zn *ast.ZettelNode, format zsapi.EncodingEnum, env encoder.Environment) error { + enc := encoder.Create(format, &env) + if enc == nil { + return adapter.ErrNoSuchFormat + } + inhMeta := false + if format != zsapi.EncoderRaw { + w.Header().Set(zsapi.HeaderContentType, format2ContentType(format)) + inhMeta = true + } + _, err := enc.WriteZettel(w, zn, inhMeta) + return err +} + +func writeZettelPartMeta(w http.ResponseWriter, zn *ast.ZettelNode, format zsapi.EncodingEnum) error { + w.Header().Set(zsapi.HeaderContentType, format2ContentType(format)) + if enc := encoder.Create(format, nil); enc != nil { + if format == zsapi.EncoderRaw { + _, err := enc.WriteMeta(w, zn.Meta) + return err + } + _, err := enc.WriteMeta(w, zn.InhMeta) + return err + } + return adapter.ErrNoSuchFormat +} + +func (api *API) writeZettelPartContent(w http.ResponseWriter, zn *ast.ZettelNode, format zsapi.EncodingEnum, env encoder.Environment) error { + if format == zsapi.EncoderRaw { + if ct, ok := syntax2contentType(config.GetSyntax(zn.Meta, api.rtConfig)); ok { + w.Header().Add(zsapi.HeaderContentType, ct) + } + } else { + w.Header().Set(zsapi.HeaderContentType, format2ContentType(format)) + } + return writeContent(w, zn, format, &env) } ADDED web/adapter/api/get_zettel_context.go Index: web/adapter/api/get_zettel_context.go ================================================================== --- web/adapter/api/get_zettel_context.go +++ web/adapter/api/get_zettel_context.go @@ -0,0 +1,49 @@ +//----------------------------------------------------------------------------- +// Copyright (c) 2020-2021 Detlef Stern +// +// This file is part of zettelstore. +// +// Zettelstore is licensed under the latest version of the EUPL (European Union +// Public License). Please see file LICENSE.txt for your rights and obligations +// under this license. +//----------------------------------------------------------------------------- + +// Package api provides api handlers for web requests. +package api + +import ( + "net/http" + + zsapi "zettelstore.de/z/api" + "zettelstore.de/z/domain/id" + "zettelstore.de/z/usecase" + "zettelstore.de/z/web/adapter" +) + +// MakeZettelContextHandler creates a new HTTP handler for the use case "zettel context". +func (api *API) MakeZettelContextHandler(getContext usecase.ZettelContext) http.HandlerFunc { + return func(w http.ResponseWriter, r *http.Request) { + zid, err := id.Parse(r.URL.Path[1:]) + if err != nil { + http.NotFound(w, r) + return + } + q := r.URL.Query() + dir := adapter.GetZCDirection(q.Get(zsapi.QueryKeyDir)) + depth, ok := adapter.GetInteger(q, zsapi.QueryKeyDepth) + if !ok || depth < 0 { + depth = 5 + } + limit, ok := adapter.GetInteger(q, zsapi.QueryKeyLimit) + if !ok || limit < 0 { + limit = 200 + } + ctx := r.Context() + metaList, err := getContext.Run(ctx, zid, dir, depth, limit) + if err != nil { + adapter.ReportUsecaseError(w, err) + return + } + api.writeMetaList(w, metaList[0], metaList[1:]) + } +} ADDED web/adapter/api/get_zettel_list.go Index: web/adapter/api/get_zettel_list.go ================================================================== --- web/adapter/api/get_zettel_list.go +++ web/adapter/api/get_zettel_list.go @@ -0,0 +1,87 @@ +//----------------------------------------------------------------------------- +// Copyright (c) 2020-2021 Detlef Stern +// +// This file is part of zettelstore. +// +// Zettelstore is licensed under the latest version of the EUPL (European Union +// Public License). Please see file LICENSE.txt for your rights and obligations +// under this license. +//----------------------------------------------------------------------------- + +// Package api provides api handlers for web requests. +package api + +import ( + "fmt" + "net/http" + + zsapi "zettelstore.de/z/api" + "zettelstore.de/z/box" + "zettelstore.de/z/domain/meta" + "zettelstore.de/z/encoder" + "zettelstore.de/z/parser" + "zettelstore.de/z/usecase" + "zettelstore.de/z/web/adapter" +) + +// MakeListMetaHandler creates a new HTTP handler for the use case "list some zettel". +func (api *API) MakeListMetaHandler( + listMeta usecase.ListMeta, + getMeta usecase.GetMeta, + parseZettel usecase.ParseZettel, +) http.HandlerFunc { + return func(w http.ResponseWriter, r *http.Request) { + ctx := r.Context() + q := r.URL.Query() + s := adapter.GetSearch(q, false) + format, formatText := adapter.GetFormat(r, q, encoder.GetDefaultFormat()) + part := getPart(q, partMeta) + if part == partUnknown { + adapter.BadRequest(w, "Unknown _part parameter") + return + } + ctx1 := ctx + if format == zsapi.EncoderHTML || (!s.HasComputedMetaKey() && (part == partID || part == partContent)) { + ctx1 = box.NoEnrichContext(ctx1) + } + metaList, err := listMeta.Run(ctx1, s) + if err != nil { + adapter.ReportUsecaseError(w, err) + return + } + + w.Header().Set(zsapi.HeaderContentType, format2ContentType(format)) + switch format { + case zsapi.EncoderHTML: + api.renderListMetaHTML(w, metaList) + case zsapi.EncoderJSON, zsapi.EncoderDJSON: + api.renderListMetaXJSON(ctx, w, metaList, format, part, partMeta, getMeta, parseZettel) + case zsapi.EncoderNative, zsapi.EncoderRaw, zsapi.EncoderText, zsapi.EncoderZmk: + adapter.NotImplemented(w, fmt.Sprintf("Zettel list in format %q not yet implemented", formatText)) + default: + adapter.BadRequest(w, fmt.Sprintf("Zettel list not available in format %q", formatText)) + } + } +} + +func (api *API) renderListMetaHTML(w http.ResponseWriter, metaList []*meta.Meta) { + env := encoder.Environment{Interactive: true} + buf := encoder.NewBufWriter(w) + buf.WriteStrings("<html lang=\"", api.rtConfig.GetDefaultLang(), "\">\n<body>\n<ul>\n") + for _, m := range metaList { + title := m.GetDefault(meta.KeyTitle, "") + htmlTitle, err := adapter.FormatInlines(parser.ParseMetadata(title), zsapi.EncoderHTML, &env) + if err != nil { + adapter.InternalServerError(w, "Format HTML inlines", err) + return + } + buf.WriteStrings( + "<li><a href=\"", + api.NewURLBuilder('z').SetZid(m.Zid).AppendQuery(zsapi.QueryKeyFormat, zsapi.FormatHTML).String(), + "\">", + htmlTitle, + "</a></li>\n") + } + buf.WriteString("</ul>\n</body>\n</html>") + buf.Flush() +} ADDED web/adapter/api/json.go Index: web/adapter/api/json.go ================================================================== --- web/adapter/api/json.go +++ web/adapter/api/json.go @@ -0,0 +1,334 @@ +//----------------------------------------------------------------------------- +// Copyright (c) 2020-2021 Detlef Stern +// +// This file is part of zettelstore. +// +// Zettelstore is licensed under the latest version of the EUPL (European Union +// Public License). Please see file LICENSE.txt for your rights and obligations +// under this license. +//----------------------------------------------------------------------------- + +// Package api provides api handlers for web requests. +package api + +import ( + "context" + "encoding/json" + "io" + "net/http" + + zsapi "zettelstore.de/z/api" + "zettelstore.de/z/ast" + "zettelstore.de/z/domain" + "zettelstore.de/z/domain/id" + "zettelstore.de/z/domain/meta" + "zettelstore.de/z/encoder" + "zettelstore.de/z/usecase" + "zettelstore.de/z/web/adapter" +) + +type jsonContent struct { + ID string `json:"id"` + URL string `json:"url"` + Encoding string `json:"encoding"` + Content string `json:"content"` +} + +var ( + djsonMetaHeader = []byte(",\"meta\":") + djsonContentHeader = []byte(",\"content\":") + djsonHeader1 = []byte("{\"id\":\"") + djsonHeader2 = []byte("\",\"url\":\"") + djsonHeader3 = []byte("?_format=") + djsonHeader4 = []byte("\"") + djsonFooter = []byte("}") +) + +func (api *API) writeDJSONHeader(w io.Writer, zid id.Zid) error { + _, err := w.Write(djsonHeader1) + if err == nil { + _, err = w.Write(zid.Bytes()) + } + if err == nil { + _, err = w.Write(djsonHeader2) + } + if err == nil { + _, err = io.WriteString(w, api.NewURLBuilder('z').SetZid(zid).String()) + } + if err == nil { + _, err = w.Write(djsonHeader3) + if err == nil { + _, err = io.WriteString(w, zsapi.FormatDJSON) + } + } + if err == nil { + _, err = w.Write(djsonHeader4) + } + return err +} + +func (api *API) renderListMetaXJSON( + ctx context.Context, + w http.ResponseWriter, + metaList []*meta.Meta, + format zsapi.EncodingEnum, + part, defPart partType, + getMeta usecase.GetMeta, + parseZettel usecase.ParseZettel, +) { + prepareZettel := api.getPrepareZettelFunc(ctx, parseZettel, part) + writeZettel := api.getWriteMetaZettelFunc(ctx, format, part, defPart, getMeta) + err := writeListXJSON(w, metaList, prepareZettel, writeZettel) + if err != nil { + adapter.InternalServerError(w, "Get list", err) + } +} + +type prepareZettelFunc func(m *meta.Meta) (*ast.ZettelNode, error) + +func (api *API) getPrepareZettelFunc(ctx context.Context, parseZettel usecase.ParseZettel, part partType) prepareZettelFunc { + switch part { + case partZettel, partContent: + return func(m *meta.Meta) (*ast.ZettelNode, error) { + return parseZettel.Run(ctx, m.Zid, "") + } + case partMeta, partID: + return func(m *meta.Meta) (*ast.ZettelNode, error) { + return &ast.ZettelNode{ + Meta: m, + Content: domain.NewContent(""), + Zid: m.Zid, + InhMeta: api.rtConfig.AddDefaultValues(m), + Ast: nil, + }, nil + } + } + return nil +} + +type writeZettelFunc func(io.Writer, *ast.ZettelNode) error + +func (api *API) getWriteMetaZettelFunc(ctx context.Context, format zsapi.EncodingEnum, + part, defPart partType, getMeta usecase.GetMeta) writeZettelFunc { + switch part { + case partZettel: + return api.getWriteZettelFunc(ctx, format, defPart, getMeta) + case partMeta: + return api.getWriteMetaFunc(ctx, format) + case partContent: + return api.getWriteContentFunc(ctx, format, defPart, getMeta) + case partID: + return api.getWriteIDFunc(ctx, format) + default: + panic(part) + } +} + +func (api *API) getWriteZettelFunc(ctx context.Context, format zsapi.EncodingEnum, + defPart partType, getMeta usecase.GetMeta) writeZettelFunc { + if format == zsapi.EncoderJSON { + return func(w io.Writer, zn *ast.ZettelNode) error { + content, encoding := zn.Content.Encode() + return encodeJSONData(w, zsapi.ZettelJSON{ + ID: zn.Zid.String(), + URL: api.NewURLBuilder('z').SetZid(zn.Zid).String(), + Meta: zn.InhMeta.Map(), + Encoding: encoding, + Content: content, + }) + } + } + enc := encoder.Create(zsapi.EncoderDJSON, nil) + if enc == nil { + panic("no DJSON encoder found") + } + return func(w io.Writer, zn *ast.ZettelNode) error { + err := api.writeDJSONHeader(w, zn.Zid) + if err != nil { + return err + } + _, err = w.Write(djsonMetaHeader) + if err != nil { + return err + } + _, err = enc.WriteMeta(w, zn.InhMeta) + if err != nil { + return err + } + _, err = w.Write(djsonContentHeader) + if err != nil { + return err + } + err = writeContent(w, zn, zsapi.EncoderDJSON, &encoder.Environment{ + LinkAdapter: adapter.MakeLinkAdapter( + ctx, api, 'z', getMeta, partZettel.DefString(defPart), zsapi.EncoderDJSON), + ImageAdapter: adapter.MakeImageAdapter(ctx, api, getMeta)}) + if err != nil { + return err + } + _, err = w.Write(djsonFooter) + return err + } +} +func (api *API) getWriteMetaFunc(ctx context.Context, format zsapi.EncodingEnum) writeZettelFunc { + if format == zsapi.EncoderJSON { + return func(w io.Writer, zn *ast.ZettelNode) error { + return encodeJSONData(w, zsapi.ZidMetaJSON{ + ID: zn.Zid.String(), + URL: api.NewURLBuilder('z').SetZid(zn.Zid).String(), + Meta: zn.InhMeta.Map(), + }) + } + } + enc := encoder.Create(zsapi.EncoderDJSON, nil) + if enc == nil { + panic("no DJSON encoder found") + } + return func(w io.Writer, zn *ast.ZettelNode) error { + err := api.writeDJSONHeader(w, zn.Zid) + if err != nil { + return err + } + _, err = w.Write(djsonMetaHeader) + if err != nil { + return err + } + _, err = enc.WriteMeta(w, zn.InhMeta) + if err != nil { + return err + } + _, err = w.Write(djsonFooter) + return err + } +} +func (api *API) getWriteContentFunc(ctx context.Context, format zsapi.EncodingEnum, + defPart partType, getMeta usecase.GetMeta) writeZettelFunc { + if format == zsapi.EncoderJSON { + return func(w io.Writer, zn *ast.ZettelNode) error { + content, encoding := zn.Content.Encode() + return encodeJSONData(w, jsonContent{ + ID: zn.Zid.String(), + URL: api.NewURLBuilder('z').SetZid(zn.Zid).String(), + Encoding: encoding, + Content: content, + }) + } + } + return func(w io.Writer, zn *ast.ZettelNode) error { + err := api.writeDJSONHeader(w, zn.Zid) + if err != nil { + return err + } + _, err = w.Write(djsonContentHeader) + if err != nil { + return err + } + err = writeContent(w, zn, zsapi.EncoderDJSON, &encoder.Environment{ + LinkAdapter: adapter.MakeLinkAdapter( + ctx, api, 'z', getMeta, partContent.DefString(defPart), zsapi.EncoderDJSON), + ImageAdapter: adapter.MakeImageAdapter(ctx, api, getMeta)}) + if err != nil { + return err + } + _, err = w.Write(djsonFooter) + return err + } +} +func (api *API) getWriteIDFunc(ctx context.Context, format zsapi.EncodingEnum) writeZettelFunc { + if format == zsapi.EncoderJSON { + return func(w io.Writer, zn *ast.ZettelNode) error { + return encodeJSONData(w, zsapi.ZidJSON{ + ID: zn.Zid.String(), + URL: api.NewURLBuilder('z').SetZid(zn.Zid).String(), + }) + } + } + return func(w io.Writer, zn *ast.ZettelNode) error { + err := api.writeDJSONHeader(w, zn.Zid) + if err != nil { + return err + } + _, err = w.Write(djsonFooter) + return err + } +} + +var ( + jsonListHeader = []byte("{\"list\":[") + jsonListSep = []byte{','} + jsonListFooter = []byte("]}") +) + +func writeListXJSON(w http.ResponseWriter, metaList []*meta.Meta, prepareZettel prepareZettelFunc, writeZettel writeZettelFunc) error { + _, err := w.Write(jsonListHeader) + for i, m := range metaList { + if err != nil { + return err + } + if i > 0 { + _, err = w.Write(jsonListSep) + if err != nil { + return err + } + } + zn, err1 := prepareZettel(m) + if err1 != nil { + return err1 + } + err = writeZettel(w, zn) + } + if err == nil { + _, err = w.Write(jsonListFooter) + } + return err +} + +func writeContent(w io.Writer, zn *ast.ZettelNode, format zsapi.EncodingEnum, env *encoder.Environment) error { + enc := encoder.Create(format, env) + if enc == nil { + return adapter.ErrNoSuchFormat + } + + _, err := enc.WriteContent(w, zn) + return err +} + +func encodeJSONData(w io.Writer, data interface{}) error { + enc := json.NewEncoder(w) + enc.SetEscapeHTML(false) + return enc.Encode(data) +} + +func (api *API) writeMetaList(w http.ResponseWriter, m *meta.Meta, metaList []*meta.Meta) error { + outList := make([]zsapi.ZidMetaJSON, len(metaList)) + for i, m := range metaList { + outList[i].ID = m.Zid.String() + outList[i].URL = api.NewURLBuilder('z').SetZid(m.Zid).String() + outList[i].Meta = m.Map() + } + w.Header().Set(zsapi.HeaderContentType, format2ContentType(zsapi.EncoderJSON)) + return encodeJSONData(w, zsapi.ZidMetaRelatedList{ + ID: m.Zid.String(), + URL: api.NewURLBuilder('z').SetZid(m.Zid).String(), + Meta: m.Map(), + List: outList, + }) +} + +func buildZettelFromData(r *http.Request, zid id.Zid) (domain.Zettel, error) { + var zettel domain.Zettel + dec := json.NewDecoder(r.Body) + var zettelData zsapi.ZettelDataJSON + if err := dec.Decode(&zettelData); err != nil { + return zettel, err + } + m := meta.New(zid) + for k, v := range zettelData.Meta { + m.Set(k, v) + } + zettel.Meta = m + if err := zettel.Content.SetDecoded(zettelData.Content, zettelData.Encoding); err != nil { + return zettel, err + } + return zettel, nil +} Index: web/adapter/api/login.go ================================================================== --- web/adapter/api/login.go +++ web/adapter/api/login.go @@ -1,58 +1,54 @@ //----------------------------------------------------------------------------- -// Copyright (c) 2020-present Detlef Stern +// Copyright (c) 2020-2021 Detlef Stern // -// This file is part of Zettelstore. +// This file is part of zettelstore. // // Zettelstore is licensed under the latest version of the EUPL (European Union // Public License). Please see file LICENSE.txt for your rights and obligations // under this license. -// -// SPDX-License-Identifier: EUPL-1.2 -// SPDX-FileCopyrightText: 2020-present Detlef Stern //----------------------------------------------------------------------------- +// Package api provides api handlers for web requests. package api import ( + "encoding/json" "net/http" "time" - "t73f.de/r/sx" + zsapi "zettelstore.de/z/api" "zettelstore.de/z/auth" "zettelstore.de/z/usecase" "zettelstore.de/z/web/adapter" - "zettelstore.de/z/zettel/id" ) // MakePostLoginHandler creates a new HTTP handler to authenticate the given user via API. -func (a *API) MakePostLoginHandler(ucAuth *usecase.Authenticate) http.HandlerFunc { +func (api *API) MakePostLoginHandler(ucAuth usecase.Authenticate) http.HandlerFunc { return func(w http.ResponseWriter, r *http.Request) { - if !a.withAuth() { - if err := a.writeToken(w, "freeaccess", 24*366*10*time.Hour); err != nil { - a.log.Error().Err(err).Msg("Login/free") - } + if !api.withAuth() { + w.Header().Set(zsapi.HeaderContentType, format2ContentType(zsapi.EncoderJSON)) + writeJSONToken(w, "freeaccess", 24*366*10*time.Hour) return } var token []byte if ident, cred := retrieveIdentCred(r); ident != "" { var err error - token, err = ucAuth.Run(r.Context(), r, ident, cred, a.tokenLifetime, auth.KindAPI) + token, err = ucAuth.Run(r.Context(), ident, cred, api.tokenLifetime, auth.KindJSON) if err != nil { - a.reportUsecaseError(w, err) + adapter.ReportUsecaseError(w, err) return } } if len(token) == 0 { w.Header().Set("WWW-Authenticate", `Bearer realm="Default"`) http.Error(w, "Authentication failed", http.StatusUnauthorized) return } - if err := a.writeToken(w, string(token), a.tokenLifetime); err != nil { - a.log.Error().Err(err).Msg("Login") - } + w.Header().Set(zsapi.HeaderContentType, format2ContentType(zsapi.EncoderJSON)) + writeJSONToken(w, string(token), api.tokenLifetime) } } func retrieveIdentCred(r *http.Request) (string, string) { if ident, cred, ok := adapter.GetCredentialsViaForm(r); ok { @@ -61,50 +57,43 @@ if ident, cred, ok := r.BasicAuth(); ok { return ident, cred } return "", "" } + +func writeJSONToken(w http.ResponseWriter, token string, lifetime time.Duration) { + je := json.NewEncoder(w) + je.Encode(zsapi.AuthJSON{ + Token: token, + Type: "Bearer", + Expires: int(lifetime / time.Second), + }) +} // MakeRenewAuthHandler creates a new HTTP handler to renew the authenticate of a user. -func (a *API) MakeRenewAuthHandler() http.HandlerFunc { +func (api *API) MakeRenewAuthHandler() http.HandlerFunc { return func(w http.ResponseWriter, r *http.Request) { ctx := r.Context() - if !a.withAuth() { - if err := a.writeToken(w, "freeaccess", 24*366*10*time.Hour); err != nil { - a.log.Error().Err(err).Msg("Refresh/free") - } - return - } - authData := a.getAuthData(ctx) + authData := api.getAuthData(ctx) if authData == nil || len(authData.Token) == 0 || authData.User == nil { adapter.BadRequest(w, "Not authenticated") return } totalLifetime := authData.Expires.Sub(authData.Issued) currentLifetime := authData.Now.Sub(authData.Issued) // If we are in the first quarter of the tokens lifetime, return the token if currentLifetime*4 < totalLifetime { - if err := a.writeToken(w, string(authData.Token), totalLifetime-currentLifetime); err != nil { - a.log.Error().Err(err).Msg("Write old token") - } + w.Header().Set(zsapi.HeaderContentType, format2ContentType(zsapi.EncoderJSON)) + writeJSONToken(w, string(authData.Token), totalLifetime-currentLifetime) return } // Token is a little bit aged. Create a new one - token, err := a.getToken(authData.User) + token, err := api.getToken(authData.User) if err != nil { - a.reportUsecaseError(w, err) + adapter.ReportUsecaseError(w, err) return } - if err = a.writeToken(w, string(token), a.tokenLifetime); err != nil { - a.log.Error().Err(err).Msg("Write renewed token") - } - } -} - -func (a *API) writeToken(w http.ResponseWriter, token string, lifetime time.Duration) error { - return a.writeObject(w, id.Invalid, sx.MakeList( - sx.MakeString("Bearer"), - sx.MakeString(token), - sx.Int64(int64(lifetime/time.Second)), - )) + w.Header().Set(zsapi.HeaderContentType, format2ContentType(zsapi.EncoderJSON)) + writeJSONToken(w, string(token), api.tokenLifetime) + } } DELETED web/adapter/api/query.go Index: web/adapter/api/query.go ================================================================== --- web/adapter/api/query.go +++ web/adapter/api/query.go @@ -1,301 +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. -// -// SPDX-License-Identifier: EUPL-1.2 -// SPDX-FileCopyrightText: 2022-present Detlef Stern -//----------------------------------------------------------------------------- - -package api - -import ( - "bytes" - "fmt" - "io" - "net/http" - "net/url" - "strconv" - "strings" - - "t73f.de/r/sx" - "t73f.de/r/zsc/api" - "t73f.de/r/zsc/sexp" - "zettelstore.de/z/query" - "zettelstore.de/z/usecase" - "zettelstore.de/z/web/adapter" - "zettelstore.de/z/web/content" - "zettelstore.de/z/zettel/id" - "zettelstore.de/z/zettel/meta" -) - -// 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) || 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: - encoder = &plainZettelEncoder{} - contentType = content.PlainText - - case api.EncoderData: - encoder = &dataZettelEncoder{ - sq: sq, - getRights: func(m *meta.Meta) api.ZettelRights { return a.getRights(ctx, m) }, - } - contentType = content.SXPF - - default: - http.Error(w, http.StatusText(http.StatusBadRequest), http.StatusBadRequest) - return - } - - var buf bytes.Buffer - 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) - } - - 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, actions []string) error { - min, max := -1, -1 - if len(actions) > 0 { - acts := make([]string, 0, len(actions)) - for _, act := range actions { - if strings.HasPrefix(act, api.MinAction) { - if num, err := strconv.Atoi(act[3:]); err == nil && num > 0 { - min = num - continue - } - } - 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 { - if act == api.KeysAction { - return encodeKeysArrangement(w, enc, ml, act) - } - switch key := strings.ToLower(act); meta.Type(key) { - case meta.TypeWord, meta.TypeTagSet: - return encodeMetaKeyArrangement(w, enc, ml, key, min, max) - } - } - } - return enc.writeMetaList(w, ml) -} - -func encodeKeysArrangement(w io.Writer, enc zettelEncoder, ml []*meta.Meta, act string) error { - arr := make(meta.Arrangement, 128) - for _, m := range ml { - for k := range m.Map() { - arr[k] = append(arr[k], m) - } - } - return enc.writeArrangement(w, act, arr) -} - -func encodeMetaKeyArrangement(w io.Writer, enc zettelEncoder, ml []*meta.Meta, key string, min, max int) error { - arr0 := meta.CreateArrangement(ml, key) - arr := make(meta.Arrangement, len(arr0)) - for k0, ml0 := range arr0 { - if len(ml0) < min || (max > 0 && len(ml0) > max) { - continue - } - arr[k0] = ml0 - } - return enc.writeArrangement(w, key, arr) -} - -type zettelEncoder interface { - writeMetaList(w io.Writer, ml []*meta.Meta) error - writeArrangement(w io.Writer, act string, arr meta.Arrangement) error -} - -type plainZettelEncoder struct{} - -func (*plainZettelEncoder) writeMetaList(w io.Writer, ml []*meta.Meta) error { - for _, m := range ml { - _, err := fmt.Fprintln(w, m.Zid.String(), m.GetTitle()) - if err != nil { - return err - } - } - return nil -} -func (*plainZettelEncoder) writeArrangement(w io.Writer, _ string, arr meta.Arrangement) error { - for key, ml := range arr { - _, err := io.WriteString(w, key) - if err != nil { - return err - } - for i, m := range ml { - if i == 0 { - _, err = io.WriteString(w, "\t") - } else { - _, err = io.WriteString(w, " ") - } - if err != nil { - return err - } - _, err = io.WriteString(w, m.Zid.String()) - if err != nil { - return err - } - } - _, err = io.WriteString(w, "\n") - if err != nil { - return err - } - } - return nil -} - -type dataZettelEncoder struct { - sq *query.Query - getRights func(*meta.Meta) api.ZettelRights -} - -func (dze *dataZettelEncoder) writeMetaList(w io.Writer, ml []*meta.Meta) error { - 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), - }) - msz = sx.Cons(sx.MakeList(symID, sx.Int64(m.Zid)), msz.Cdr()).Cons(symZettel) - result[i+1] = msz - } - - _, err := sx.Print(w, sx.MakeList( - sx.MakeSymbol("meta-list"), - sx.MakeList(sx.MakeSymbol("query"), sx.MakeString(dze.sq.String())), - sx.MakeList(sx.MakeSymbol("human"), sx.MakeString(dze.sq.Human())), - sx.MakeList(result...), - )) - return err -} -func (dze *dataZettelEncoder) writeArrangement(w io.Writer, act string, arr meta.Arrangement) error { - 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)) - } - sxMeta = sxMeta.Cons(sx.MakeString(aggKey)) - result = result.Cons(sxMeta) - } - _, err := sx.Print(w, sx.MakeList( - sx.MakeSymbol("aggregate"), - sx.MakeString(act), - sx.MakeList(sx.MakeSymbol("query"), sx.MakeString(dze.sq.String())), - sx.MakeList(sx.MakeSymbol("human"), sx.MakeString(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 { - tag := vals.Get(api.QueryKeyTag) - if tag == "" { - return false - } - ctx := r.Context() - z, err := tagZettel.Run(ctx, tag) - if err != nil { - a.reportUsecaseError(w, err) - return true - } - 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) - } - } - 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) - if role == "" { - return false - } - ctx := r.Context() - z, err := roleZettel.Run(ctx, role) - if err != nil { - a.reportUsecaseError(w, err) - return true - } - 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) - } - } - 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") - } -} Index: web/adapter/api/rename_zettel.go ================================================================== --- web/adapter/api/rename_zettel.go +++ web/adapter/api/rename_zettel.go @@ -1,31 +1,30 @@ //----------------------------------------------------------------------------- -// Copyright (c) 2021-present Detlef Stern +// Copyright (c) 2021 Detlef Stern // -// This file is part of Zettelstore. +// This file is part of zettelstore. // // Zettelstore is licensed under the latest version of the EUPL (European Union // Public License). Please see file LICENSE.txt for your rights and obligations // under this license. -// -// SPDX-License-Identifier: EUPL-1.2 -// SPDX-FileCopyrightText: 2021-present Detlef Stern //----------------------------------------------------------------------------- +// Package api provides api handlers for web requests. package api import ( "net/http" "net/url" - "t73f.de/r/zsc/api" + "zettelstore.de/z/api" + "zettelstore.de/z/domain/id" "zettelstore.de/z/usecase" - "zettelstore.de/z/zettel/id" + "zettelstore.de/z/web/adapter" ) // MakeRenameZettelHandler creates a new HTTP handler to update a zettel. -func (a *API) MakeRenameZettelHandler(renameZettel *usecase.RenameZettel) http.HandlerFunc { +func (api *API) MakeRenameZettelHandler(renameZettel usecase.RenameZettel) http.HandlerFunc { return func(w http.ResponseWriter, r *http.Request) { zid, err := id.Parse(r.URL.Path[1:]) if err != nil { http.NotFound(w, r) return @@ -33,38 +32,40 @@ newZid, found := getDestinationZid(r) if !found { http.Error(w, http.StatusText(http.StatusBadRequest), http.StatusBadRequest) return } - if err = renameZettel.Run(r.Context(), zid, newZid); err != nil { - a.reportUsecaseError(w, err) + if err := renameZettel.Run(r.Context(), zid, newZid); err != nil { + adapter.ReportUsecaseError(w, err) return } w.WriteHeader(http.StatusNoContent) } } func getDestinationZid(r *http.Request) (id.Zid, bool) { if values, ok := r.Header[api.HeaderDestination]; ok { for _, value := range values { - if zid, ok2 := getZidFromURL(value); ok2 { + if zid, ok := getZidFromURL(value); ok { return zid, true } } } return id.Invalid, false } + +var zidLength = len(id.VersionZid.Bytes()) func getZidFromURL(val string) (id.Zid, bool) { u, err := url.Parse(val) if err != nil { return id.Invalid, false } - if len(u.Path) < len(api.ZidVersion) { + if len(u.Path) < zidLength { return id.Invalid, false } - zid, err := id.Parse(u.Path[len(u.Path)-len(api.ZidVersion):]) + zid, err := id.Parse(u.Path[len(u.Path)-zidLength:]) if err != nil { return id.Invalid, false } return zid, true } Index: web/adapter/api/request.go ================================================================== --- web/adapter/api/request.go +++ web/adapter/api/request.go @@ -1,100 +1,64 @@ //----------------------------------------------------------------------------- -// Copyright (c) 2020-present Detlef Stern +// Copyright (c) 2020-2021 Detlef Stern // -// This file is part of Zettelstore. +// This file is part of zettelstore. // // Zettelstore is licensed under the latest version of the EUPL (European Union // Public License). Please see file LICENSE.txt for your rights and obligations // under this license. -// -// SPDX-License-Identifier: EUPL-1.2 -// SPDX-FileCopyrightText: 2020-present Detlef Stern //----------------------------------------------------------------------------- +// Package api provides api handlers for web requests. package api import ( - "io" - "net/http" "net/url" - "t73f.de/r/sx/sxreader" - "t73f.de/r/zsc/api" - "t73f.de/r/zsc/input" - "t73f.de/r/zsc/sexp" - "zettelstore.de/z/zettel" - "zettelstore.de/z/zettel/id" - "zettelstore.de/z/zettel/meta" -) - -// getEncoding returns the data encoding selected by the caller. -func getEncoding(r *http.Request, q url.Values) (api.EncodingEnum, string) { - encoding := q.Get(api.QueryKeyEncoding) - if encoding != "" { - return api.Encoder(encoding), encoding - } - if enc, ok := getOneEncoding(r, api.HeaderAccept); ok { - return api.Encoder(enc), enc - } - if enc, ok := getOneEncoding(r, api.HeaderContentType); ok { - return api.Encoder(enc), enc - } - return api.EncoderPlain, api.EncoderPlain.String() -} - -func getOneEncoding(r *http.Request, key string) (string, bool) { - if values, ok := r.Header[key]; ok { - for _, value := range values { - if enc, ok2 := contentType2encoding(value); ok2 { - return enc, true - } - } - } - return "", false -} - -var mapCT2encoding = map[string]string{ - "text/html": api.EncodingHTML, -} - -func contentType2encoding(contentType string) (string, bool) { - // TODO: only check before first ';' - enc, ok := mapCT2encoding[contentType] - return enc, ok -} + "zettelstore.de/z/api" +) type partType int const ( - _ partType = iota + partUnknown partType = iota + partID partMeta partContent partZettel ) var partMap = map[string]partType{ + api.PartID: partID, api.PartMeta: partMeta, api.PartContent: partContent, api.PartZettel: partZettel, } func getPart(q url.Values, defPart partType) partType { - if part, ok := partMap[q.Get(api.QueryKeyPart)]; ok { + p := q.Get(api.QueryKeyPart) + if p == "" { + return defPart + } + if part, ok := partMap[p]; ok { return part } - return defPart + return partUnknown } func (p partType) String() string { switch p { + case partID: + return "id" case partMeta: return "meta" case partContent: return "content" case partZettel: return "zettel" + case partUnknown: + return "unknown" } return "" } func (p partType) DefString(defPart partType) string { @@ -101,49 +65,5 @@ if p == defPart { return "" } return p.String() } - -func buildZettelFromPlainData(r *http.Request, zid id.Zid) (zettel.Zettel, error) { - defer r.Body.Close() - b, err := io.ReadAll(r.Body) - if err != nil { - return zettel.Zettel{}, err - } - inp := input.NewInput(b) - m := meta.NewFromInput(zid, inp) - return zettel.Zettel{ - Meta: m, - Content: zettel.NewContent(inp.Src[inp.Pos:]), - }, nil -} - -func buildZettelFromData(r *http.Request, zid id.Zid) (zettel.Zettel, error) { - defer r.Body.Close() - rdr := sxreader.MakeReader(r.Body) - obj, err := rdr.Read() - if err != nil { - return zettel.Zettel{}, err - } - zd, err := sexp.ParseZettel(obj) - if err != nil { - return zettel.Zettel{}, err - } - - m := meta.New(zid) - for k, v := range zd.Meta { - if !meta.IsComputed(k) { - m.Set(meta.RemoveNonGraphic(k), meta.RemoveNonGraphic(v)) - } - } - - var content zettel.Content - if err = content.SetDecoded(zd.Content, zd.Encoding); err != nil { - return zettel.Zettel{}, err - } - - return zettel.Zettel{ - Meta: m, - Content: content, - }, nil -} DELETED web/adapter/api/response.go Index: web/adapter/api/response.go ================================================================== --- web/adapter/api/response.go +++ web/adapter/api/response.go @@ -1,39 +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. -// -// SPDX-License-Identifier: EUPL-1.2 -// SPDX-FileCopyrightText: 2020-present Detlef Stern -//----------------------------------------------------------------------------- - -package api - -import ( - "bytes" - "net/http" - - "t73f.de/r/sx" - "zettelstore.de/z/web/content" - "zettelstore.de/z/zettel/id" -) - -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.Error().Err(err) - if msg != nil { - if zid.IsValid() { - msg = msg.Zid(zid) - } - msg.Msg("Unable to store object in buffer") - } - http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError) - return nil - } - return writeBuffer(w, &buf, content.SXPF) -} Index: web/adapter/api/update_zettel.go ================================================================== --- web/adapter/api/update_zettel.go +++ web/adapter/api/update_zettel.go @@ -1,57 +1,41 @@ //----------------------------------------------------------------------------- -// Copyright (c) 2021-present Detlef Stern +// Copyright (c) 2021 Detlef Stern // -// This file is part of Zettelstore. +// This file is part of zettelstore. // // Zettelstore is licensed under the latest version of the EUPL (European Union // Public License). Please see file LICENSE.txt for your rights and obligations // under this license. -// -// SPDX-License-Identifier: EUPL-1.2 -// SPDX-FileCopyrightText: 2021-present Detlef Stern //----------------------------------------------------------------------------- +// Package api provides api handlers for web requests. package api import ( "net/http" - "t73f.de/r/zsc/api" + "zettelstore.de/z/domain/id" "zettelstore.de/z/usecase" "zettelstore.de/z/web/adapter" - "zettelstore.de/z/zettel" - "zettelstore.de/z/zettel/id" ) // MakeUpdateZettelHandler creates a new HTTP handler to update a zettel. -func (a *API) MakeUpdateZettelHandler(updateZettel *usecase.UpdateZettel) http.HandlerFunc { +func (api *API) MakeUpdateZettelHandler(updateZettel usecase.UpdateZettel) http.HandlerFunc { return func(w http.ResponseWriter, r *http.Request) { zid, err := id.Parse(r.URL.Path[1:]) if err != nil { http.NotFound(w, r) return } - - q := r.URL.Query() - var zettel zettel.Zettel - switch enc, _ := getEncoding(r, q); enc { - case api.EncoderPlain: - zettel, err = buildZettelFromPlainData(r, zid) - case api.EncoderData: - zettel, err = buildZettelFromData(r, zid) - default: - http.Error(w, http.StatusText(http.StatusBadRequest), http.StatusBadRequest) - return - } - - if err != nil { - a.reportUsecaseError(w, adapter.NewErrBadRequest(err.Error())) - return - } - if err = updateZettel.Run(r.Context(), zettel, true); err != nil { - a.reportUsecaseError(w, err) + zettel, err := buildZettelFromData(r, zid) + if err != nil { + adapter.ReportUsecaseError(w, adapter.NewErrBadRequest(err.Error())) + return + } + if err := updateZettel.Run(r.Context(), zettel, true); err != nil { + adapter.ReportUsecaseError(w, err) return } w.WriteHeader(http.StatusNoContent) } } ADDED web/adapter/encoding.go Index: web/adapter/encoding.go ================================================================== --- web/adapter/encoding.go +++ web/adapter/encoding.go @@ -0,0 +1,138 @@ +//----------------------------------------------------------------------------- +// Copyright (c) 2020-2021 Detlef Stern +// +// This file is part of zettelstore. +// +// Zettelstore is licensed under the latest version of the EUPL (European Union +// Public License). Please see file LICENSE.txt for your rights and obligations +// under this license. +//----------------------------------------------------------------------------- + +// Package adapter provides handlers for web requests. +package adapter + +import ( + "context" + "errors" + "strings" + + "zettelstore.de/z/api" + "zettelstore.de/z/ast" + "zettelstore.de/z/box" + "zettelstore.de/z/domain/id" + "zettelstore.de/z/encoder" + "zettelstore.de/z/usecase" + "zettelstore.de/z/web/server" +) + +// ErrNoSuchFormat signals an unsupported encoding format +var ErrNoSuchFormat = errors.New("no such format") + +// FormatInlines returns a string representation of the inline slice. +func FormatInlines(is ast.InlineSlice, format api.EncodingEnum, env *encoder.Environment) (string, error) { + enc := encoder.Create(format, env) + if enc == nil { + return "", ErrNoSuchFormat + } + + var content strings.Builder + _, err := enc.WriteInlines(&content, is) + if err != nil { + return "", err + } + return content.String(), nil +} + +// MakeLinkAdapter creates an adapter to change a link node during encoding. +func MakeLinkAdapter( + ctx context.Context, + b server.Builder, + key byte, + getMeta usecase.GetMeta, + part string, + format api.EncodingEnum, +) func(*ast.LinkNode) ast.InlineNode { + return func(origLink *ast.LinkNode) ast.InlineNode { + origRef := origLink.Ref + if origRef == nil { + return origLink + } + if origRef.State == ast.RefStateBased { + newLink := *origLink + urlPrefix := b.GetURLPrefix() + newRef := ast.ParseReference(urlPrefix + origRef.Value[1:]) + newRef.State = ast.RefStateHosted + newLink.Ref = newRef + return &newLink + } + if origRef.State != ast.RefStateZettel { + return origLink + } + zid, err := id.Parse(origRef.URL.Path) + if err != nil { + panic(err) + } + _, err = getMeta.Run(box.NoEnrichContext(ctx), zid) + if errors.Is(err, &box.ErrNotAllowed{}) { + return &ast.FormatNode{ + Kind: ast.FormatSpan, + Attrs: origLink.Attrs, + Inlines: origLink.Inlines, + } + } + var newRef *ast.Reference + if err == nil { + ub := b.NewURLBuilder(key).SetZid(zid) + if part != "" { + ub.AppendQuery(api.QueryKeyPart, part) + } + if format != api.EncoderUnknown { + ub.AppendQuery(api.QueryKeyFormat, format.String()) + } + if fragment := origRef.URL.EscapedFragment(); fragment != "" { + ub.SetFragment(fragment) + } + + newRef = ast.ParseReference(ub.String()) + newRef.State = ast.RefStateFound + } else { + newRef = ast.ParseReference(origRef.Value) + newRef.State = ast.RefStateBroken + } + newLink := *origLink + newLink.Ref = newRef + return &newLink + } +} + +// MakeImageAdapter creates an adapter to change an image node during encoding. +func MakeImageAdapter(ctx context.Context, b server.Builder, getMeta usecase.GetMeta) func(*ast.ImageNode) ast.InlineNode { + return func(origImage *ast.ImageNode) ast.InlineNode { + if origImage.Ref == nil { + return origImage + } + switch origImage.Ref.State { + case ast.RefStateInvalid: + return createZettelImage(b, origImage, id.EmojiZid, ast.RefStateInvalid) + case ast.RefStateZettel: + zid, err := id.Parse(origImage.Ref.Value) + if err != nil { + panic(err) + } + _, err = getMeta.Run(box.NoEnrichContext(ctx), zid) + if err != nil { + return createZettelImage(b, origImage, id.EmojiZid, ast.RefStateBroken) + } + return createZettelImage(b, origImage, zid, ast.RefStateFound) + } + return origImage + } +} + +func createZettelImage(b server.Builder, origImage *ast.ImageNode, zid id.Zid, state ast.RefState) *ast.ImageNode { + newImage := *origImage + newImage.Ref = ast.ParseReference( + b.NewURLBuilder('z').SetZid(zid).AppendQuery("_part", "content").AppendQuery("_format", "raw").String()) + newImage.Ref.State = state + return &newImage +} Index: web/adapter/errors.go ================================================================== --- web/adapter/errors.go +++ web/adapter/errors.go @@ -1,26 +1,48 @@ //----------------------------------------------------------------------------- -// Copyright (c) 2020-present Detlef Stern +// Copyright (c) 2020 Detlef Stern // -// This file is part of Zettelstore. +// This file is part of zettelstore. // // Zettelstore is licensed under the latest version of the EUPL (European Union // Public License). Please see file LICENSE.txt for your rights and obligations // under this license. -// -// SPDX-License-Identifier: EUPL-1.2 -// SPDX-FileCopyrightText: 2020-present Detlef Stern //----------------------------------------------------------------------------- +// Package adapter provides handlers for web requests. package adapter -import "net/http" +import ( + "log" + "net/http" +) // BadRequest signals HTTP status code 400. func BadRequest(w http.ResponseWriter, text string) { http.Error(w, text, http.StatusBadRequest) } -// ErrResourceNotFound is signalled when a web resource was not found. -type ErrResourceNotFound struct{ Path string } +// 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) +} + +// InternalServerError signals HTTP status code 500. +func InternalServerError(w http.ResponseWriter, text string, err error) { + http.Error(w, "Internal Server Error", http.StatusInternalServerError) + if text == "" { + log.Println(err) + } else { + log.Printf("%v: %v", text, err) + } +} -func (ernf ErrResourceNotFound) Error() string { return "resource not found: " + ernf.Path } +// NotImplemented signals HTTP status code 501 +func NotImplemented(w http.ResponseWriter, text string) { + http.Error(w, text, http.StatusNotImplemented) + log.Println(text) +} Index: web/adapter/request.go ================================================================== --- web/adapter/request.go +++ web/adapter/request.go @@ -1,36 +1,36 @@ //----------------------------------------------------------------------------- -// Copyright (c) 2020-present Detlef Stern +// Copyright (c) 2020-2021 Detlef Stern // -// This file is part of Zettelstore. +// This file is part of zettelstore. // // Zettelstore is licensed under the latest version of the EUPL (European Union // Public License). Please see file LICENSE.txt for your rights and obligations // under this license. -// -// SPDX-License-Identifier: EUPL-1.2 -// SPDX-FileCopyrightText: 2020-present Detlef Stern //----------------------------------------------------------------------------- +// Package adapter provides handlers for web requests. package adapter import ( + "log" "net/http" "net/url" "strconv" "strings" - "t73f.de/r/zsc/api" - "zettelstore.de/z/kernel" - "zettelstore.de/z/query" + "zettelstore.de/z/api" + "zettelstore.de/z/domain/meta" + "zettelstore.de/z/search" + "zettelstore.de/z/usecase" ) // GetCredentialsViaForm retrieves the authentication credentions from a form. func GetCredentialsViaForm(r *http.Request) (ident, cred string, ok bool) { err := r.ParseForm() if err != nil { - kernel.Main.GetLogger(kernel.WebService).Info().Err(err).Msg("Unable to parse form") + log.Println(err) return "", "", false } ident = strings.TrimSpace(r.PostFormValue("username")) cred = r.PostFormValue("password") @@ -38,20 +38,134 @@ return "", "", false } return ident, cred, true } -// GetQuery retrieves the specified options from a query. -func GetQuery(vals url.Values) (result *query.Query) { - if exprs, found := vals[api.QueryKeyQuery]; found { - result = query.Parse(strings.Join(exprs, " ")) - } - if seeds, found := vals[api.QueryKeySeed]; found { - for _, seed := range seeds { - if si, err := strconv.ParseInt(seed, 10, 31); err == nil { - result = result.SetSeed(int(si)) - break +// GetInteger returns the integer value of the named query key. +func GetInteger(q url.Values, key string) (int, bool) { + s := q.Get(key) + if s != "" { + if val, err := strconv.Atoi(s); err == nil { + return val, true + } + } + return 0, false +} + +// GetFormat returns the data format selected by the caller. +func GetFormat(r *http.Request, q url.Values, defFormat api.EncodingEnum) (api.EncodingEnum, string) { + format := q.Get(api.QueryKeyFormat) + if len(format) > 0 { + return api.Encoder(format), format + } + if format, ok := getOneFormat(r, api.HeaderAccept); ok { + return api.Encoder(format), format + } + if format, ok := getOneFormat(r, api.HeaderContentType); ok { + return api.Encoder(format), format + } + return defFormat, "*default*" +} + +func getOneFormat(r *http.Request, key string) (string, bool) { + if values, ok := r.Header[key]; ok { + for _, value := range values { + if format, ok := contentType2format(value); ok { + return format, true + } + } + } + return "", false +} + +var mapCT2format = map[string]string{ + "application/json": api.FormatJSON, + "text/html": api.FormatHTML, +} + +func contentType2format(contentType string) (string, bool) { + // TODO: only check before first ';' + format, ok := mapCT2format[contentType] + return format, ok +} + +// GetSearch retrieves the specified search and sorting options from a query. +func GetSearch(q url.Values, forSearch bool) (s *search.Search) { + sortQKey, orderQKey, offsetQKey, limitQKey, negateQKey, sQKey := getQueryKeys(forSearch) + for key, values := range q { + switch key { + case sortQKey, orderQKey: + s = extractOrderFromQuery(values, s) + case offsetQKey: + s = extractOffsetFromQuery(values, s) + case limitQKey: + s = extractLimitFromQuery(values, s) + case negateQKey: + s = s.SetNegate() + case sQKey: + s = setCleanedQueryValues(s, "", values) + default: + if !forSearch && meta.KeyIsValid(key) { + s = setCleanedQueryValues(s, key, values) } } } - return result + return s +} + +func extractOrderFromQuery(values []string, s *search.Search) *search.Search { + if len(values) > 0 { + descending := false + sortkey := values[0] + if strings.HasPrefix(sortkey, "-") { + descending = true + sortkey = sortkey[1:] + } + if meta.KeyIsValid(sortkey) || sortkey == search.RandomOrder { + s = s.AddOrder(sortkey, descending) + } + } + return s +} + +func extractOffsetFromQuery(values []string, s *search.Search) *search.Search { + if len(values) > 0 { + if offset, err := strconv.Atoi(values[0]); err == nil { + s = s.SetOffset(offset) + } + } + return s +} + +func extractLimitFromQuery(values []string, s *search.Search) *search.Search { + if len(values) > 0 { + if limit, err := strconv.Atoi(values[0]); err == nil { + s = s.SetLimit(limit) + } + } + return s +} + +func getQueryKeys(forSearch bool) (string, string, string, string, string, string) { + if forSearch { + return "sort", "order", "offset", "limit", "negate", "s" + } + return "_sort", "_order", "_offset", "_limit", "_negate", "_s" +} + +func setCleanedQueryValues(s *search.Search, key string, values []string) *search.Search { + for _, val := range values { + s = s.AddExpr(key, val) + } + return s +} + +// GetZCDirection returns a direction value for a given string. +func GetZCDirection(s string) usecase.ZettelContextDirection { + switch s { + case api.DirBackward: + return usecase.ZettelContextBackward + case api.DirForward: + return usecase.ZettelContextForward + } + return usecase.ZettelContextBoth } Index: web/adapter/response.go ================================================================== --- web/adapter/response.go +++ web/adapter/response.go @@ -1,103 +1,67 @@ //----------------------------------------------------------------------------- -// Copyright (c) 2020-present Detlef Stern +// Copyright (c) 2020 Detlef Stern // -// This file is part of Zettelstore. +// This file is part of zettelstore. // // Zettelstore is licensed under the latest version of the EUPL (European Union // Public License). Please see file LICENSE.txt for your rights and obligations // under this license. -// -// SPDX-License-Identifier: EUPL-1.2 -// SPDX-FileCopyrightText: 2020-present Detlef Stern //----------------------------------------------------------------------------- +// Package adapter provides handlers for web requests. package adapter import ( "errors" "fmt" + "log" "net/http" - "strings" - "t73f.de/r/zsc/api" "zettelstore.de/z/box" "zettelstore.de/z/usecase" ) -// WriteData emits the given data to the response writer. -func WriteData(w http.ResponseWriter, data []byte, contentType string) error { - if len(data) == 0 { - w.WriteHeader(http.StatusNoContent) - return nil - } - PrepareHeader(w, contentType) - w.WriteHeader(http.StatusOK) - _, err := w.Write(data) - return err -} - -// PrepareHeader sets the HTTP header to defined values. -func PrepareHeader(w http.ResponseWriter, contentType string) http.Header { - h := w.Header() - if contentType != "" { - h.Set(api.HeaderContentType, contentType) - } - return h +// ReportUsecaseError returns an appropriate HTTP status code for errors in use cases. +func ReportUsecaseError(w http.ResponseWriter, err error) { + code, text := CodeMessageFromError(err) + if code == http.StatusInternalServerError { + log.Printf("%v: %v", text, err) + } + http.Error(w, text, code) } // ErrBadRequest is returned if the caller made an invalid HTTP request. type ErrBadRequest struct { Text string } // NewErrBadRequest creates an new bad request error. -func NewErrBadRequest(text string) error { return ErrBadRequest{Text: text} } +func NewErrBadRequest(text string) error { return &ErrBadRequest{Text: text} } -func (err ErrBadRequest) Error() string { return err.Text } +func (err *ErrBadRequest) Error() string { return err.Text } // CodeMessageFromError returns an appropriate HTTP status code and text from a given error. func CodeMessageFromError(err error) (int, string) { - var eznf box.ErrZettelNotFound - if errors.As(err, &eznf) { - return http.StatusNotFound, "Zettel not found: " + eznf.Zid.String() - } - var ena *box.ErrNotAllowed - if errors.As(err, &ena) { - msg := ena.Error() - return http.StatusForbidden, strings.ToUpper(msg[:1]) + msg[1:] - } - var eiz box.ErrInvalidZid - if errors.As(err, &eiz) { - return http.StatusBadRequest, fmt.Sprintf("Zettel-ID %q not appropriate in this context", eiz.Zid) - } - var ezin usecase.ErrZidInUse - if errors.As(err, &ezin) { - return http.StatusBadRequest, fmt.Sprintf("Zettel-ID %q already in use", ezin.Zid) - } - var etznf usecase.ErrTagZettelNotFound - if errors.As(err, &etznf) { - return http.StatusNotFound, "Tag zettel not found: " + etznf.Tag - } - var erznf usecase.ErrRoleZettelNotFound - if errors.As(err, &erznf) { - return http.StatusNotFound, "Role zettel not found: " + erznf.Role - } - var ebr ErrBadRequest - if errors.As(err, &ebr) { - return http.StatusBadRequest, ebr.Text + if err == box.ErrNotFound { + return http.StatusNotFound, http.StatusText(http.StatusNotFound) + } + if err1, ok := err.(*box.ErrNotAllowed); ok { + return http.StatusForbidden, err1.Error() + } + if err1, ok := err.(*box.ErrInvalidID); ok { + return http.StatusBadRequest, fmt.Sprintf("Zettel-ID %q not appropriate in this context", err1.Zid) + } + if err1, ok := err.(*usecase.ErrZidInUse); ok { + return http.StatusBadRequest, fmt.Sprintf("Zettel-ID %q already in use", err1.Zid) + } + if err1, ok := err.(*ErrBadRequest); ok { + return http.StatusBadRequest, err1.Text } if errors.Is(err, box.ErrStopped) { return http.StatusInternalServerError, fmt.Sprintf("Zettelstore not operational: %v", err) } if errors.Is(err, box.ErrConflict) { return http.StatusConflict, "Zettelstore operations conflicted" } - if errors.Is(err, box.ErrCapacity) { - return http.StatusInsufficientStorage, "Zettelstore reached one of its storage limits" - } - var ernf ErrResourceNotFound - if errors.As(err, &ernf) { - return http.StatusNotFound, "Resource not found: " + ernf.Path - } return http.StatusInternalServerError, err.Error() } DELETED web/adapter/webui/const.go Index: web/adapter/webui/const.go ================================================================== --- web/adapter/webui/const.go +++ web/adapter/webui/const.go @@ -1,53 +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. -// -// SPDX-License-Identifier: EUPL-1.2 -// SPDX-FileCopyrightText: 2022-present Detlef Stern -//----------------------------------------------------------------------------- - -package webui - -// WebUI related constants. - -const queryKeyAction = "_action" - -// Values for queryKeyAction -const ( - valueActionChild = "child" - valueActionCopy = "copy" - valueActionFolge = "folge" - valueActionNew = "new" - valueActionVersion = "version" -) - -// Enumeration for queryKeyAction -type createAction uint8 - -const ( - actionChild createAction = iota - actionCopy - actionFolge - actionNew - actionVersion -) - -var createActionMap = map[string]createAction{ - valueActionChild: actionChild, - valueActionCopy: actionCopy, - valueActionFolge: actionFolge, - valueActionNew: actionNew, - valueActionVersion: actionVersion, -} - -func getCreateAction(s string) createAction { - if action, found := createActionMap[s]; found { - return action - } - return actionCopy -} Index: web/adapter/webui/create_zettel.go ================================================================== --- web/adapter/webui/create_zettel.go +++ web/adapter/webui/create_zettel.go @@ -1,191 +1,154 @@ //----------------------------------------------------------------------------- -// Copyright (c) 2020-present Detlef Stern +// Copyright (c) 2020-2021 Detlef Stern // -// This file is part of Zettelstore. +// This file is part of zettelstore. // // Zettelstore is licensed under the latest version of the EUPL (European Union // Public License). Please see file LICENSE.txt for your rights and obligations // under this license. -// -// SPDX-License-Identifier: EUPL-1.2 -// SPDX-FileCopyrightText: 2020-present Detlef Stern //----------------------------------------------------------------------------- +// Package webui provides web-UI handlers for web requests. package webui import ( - "bytes" "context" + "fmt" "net/http" - "strings" - "t73f.de/r/sx" - "t73f.de/r/zsc/api" + "zettelstore.de/z/api" "zettelstore.de/z/box" - "zettelstore.de/z/encoder/zmkenc" - "zettelstore.de/z/evaluator" + "zettelstore.de/z/config" + "zettelstore.de/z/domain" + "zettelstore.de/z/domain/id" + "zettelstore.de/z/domain/meta" + "zettelstore.de/z/encoder" + "zettelstore.de/z/input" "zettelstore.de/z/parser" "zettelstore.de/z/usecase" "zettelstore.de/z/web/adapter" - "zettelstore.de/z/web/server" - "zettelstore.de/z/zettel" - "zettelstore.de/z/zettel/id" - "zettelstore.de/z/zettel/meta" -) - -// MakeGetCreateZettelHandler creates a new HTTP handler to display the -// HTML edit view for the various zettel creation methods. -func (wui *WebUI) MakeGetCreateZettelHandler( - getZettel usecase.GetZettel, createZettel *usecase.CreateZettel, - ucListRoles usecase.ListRoles, ucListSyntax usecase.ListSyntax) http.HandlerFunc { - return func(w http.ResponseWriter, r *http.Request) { - ctx := r.Context() - q := r.URL.Query() - op := getCreateAction(q.Get(queryKeyAction)) - path := r.URL.Path[1:] - zid, err := id.Parse(path) - if err != nil { - wui.reportError(ctx, w, box.ErrInvalidZid{Zid: path}) - return - } - origZettel, err := getZettel.Run(box.NoEnrichContext(ctx), zid) - if err != nil { - wui.reportError(ctx, w, box.ErrZettelNotFound{Zid: zid}) - return - } - - roleData, syntaxData := retrieveDataLists(ctx, ucListRoles, ucListSyntax) - switch op { - case actionChild: - wui.renderZettelForm(ctx, w, createZettel.PrepareChild(origZettel), "Child Zettel", "", roleData, syntaxData) - case actionCopy: - wui.renderZettelForm(ctx, w, createZettel.PrepareCopy(origZettel), "Copy Zettel", "", roleData, syntaxData) - case actionFolge: - wui.renderZettelForm(ctx, w, createZettel.PrepareFolge(origZettel), "Folge Zettel", "", roleData, syntaxData) - case actionNew: - title := parser.NormalizedSpacedText(origZettel.Meta.GetTitle()) - newTitle := parser.NormalizedSpacedText(q.Get(api.KeyTitle)) - wui.renderZettelForm(ctx, w, createZettel.PrepareNew(origZettel, newTitle), title, "", roleData, syntaxData) - case actionVersion: - wui.renderZettelForm(ctx, w, createZettel.PrepareVersion(origZettel), "Version Zettel", "", roleData, syntaxData) - } - } -} - -func retrieveDataLists(ctx context.Context, ucListRoles usecase.ListRoles, ucListSyntax usecase.ListSyntax) ([]string, []string) { - roleData := dataListFromArrangement(ucListRoles.Run(ctx)) - syntaxData := dataListFromArrangement(ucListSyntax.Run(ctx)) - return roleData, syntaxData -} - -func dataListFromArrangement(ar meta.Arrangement, err error) []string { - if err == nil { - l := ar.Counted() - l.SortByCount() - return l.Categories() - } - return nil +) + +// MakeGetCopyZettelHandler creates a new HTTP handler to display the +// HTML edit view of a copied zettel. +func (wui *WebUI) MakeGetCopyZettelHandler(getZettel usecase.GetZettel, copyZettel usecase.CopyZettel) http.HandlerFunc { + return func(w http.ResponseWriter, r *http.Request) { + ctx := r.Context() + origZettel, err := getOrigZettel(ctx, w, r, getZettel, "Copy") + if err != nil { + wui.reportError(ctx, w, err) + return + } + wui.renderZettelForm(w, r, copyZettel.Run(origZettel), "Copy Zettel", "Copy Zettel") + } +} + +// MakeGetFolgeZettelHandler creates a new HTTP handler to display the +// HTML edit view of a follow-up zettel. +func (wui *WebUI) MakeGetFolgeZettelHandler(getZettel usecase.GetZettel, folgeZettel usecase.FolgeZettel) http.HandlerFunc { + return func(w http.ResponseWriter, r *http.Request) { + ctx := r.Context() + origZettel, err := getOrigZettel(ctx, w, r, getZettel, "Folge") + if err != nil { + wui.reportError(ctx, w, err) + return + } + wui.renderZettelForm(w, r, folgeZettel.Run(origZettel), "Folge Zettel", "Folgezettel") + } +} + +// MakeGetNewZettelHandler creates a new HTTP handler to display the +// HTML edit view of a zettel. +func (wui *WebUI) MakeGetNewZettelHandler(getZettel usecase.GetZettel, newZettel usecase.NewZettel) http.HandlerFunc { + return func(w http.ResponseWriter, r *http.Request) { + ctx := r.Context() + origZettel, err := getOrigZettel(ctx, w, r, getZettel, "New") + if err != nil { + wui.reportError(ctx, w, err) + return + } + m := origZettel.Meta + title := parser.ParseInlines(input.NewInput(config.GetTitle(m, wui.rtConfig)), meta.ValueSyntaxZmk) + textTitle, err := adapter.FormatInlines(title, api.EncoderText, nil) + if err != nil { + wui.reportError(ctx, w, err) + return + } + env := encoder.Environment{Lang: config.GetLang(m, wui.rtConfig)} + htmlTitle, err := adapter.FormatInlines(title, api.EncoderHTML, &env) + if err != nil { + wui.reportError(ctx, w, err) + return + } + wui.renderZettelForm(w, r, newZettel.Run(origZettel), textTitle, htmlTitle) + } +} + +func getOrigZettel( + ctx context.Context, + w http.ResponseWriter, + r *http.Request, + getZettel usecase.GetZettel, + op string, +) (domain.Zettel, error) { + if format, formatText := adapter.GetFormat(r, r.URL.Query(), api.EncoderHTML); format != api.EncoderHTML { + return domain.Zettel{}, adapter.NewErrBadRequest( + fmt.Sprintf("%v zettel not possible in format %q", op, formatText)) + } + zid, err := id.Parse(r.URL.Path[1:]) + if err != nil { + return domain.Zettel{}, box.ErrNotFound + } + origZettel, err := getZettel.Run(box.NoEnrichContext(ctx), zid) + if err != nil { + return domain.Zettel{}, box.ErrNotFound + } + return origZettel, nil } func (wui *WebUI) renderZettelForm( - ctx context.Context, w http.ResponseWriter, - ztl zettel.Zettel, - title string, - formActionURL string, - roleData []string, - syntaxData []string, + r *http.Request, + zettel domain.Zettel, + title, heading string, ) { - user := server.GetUser(ctx) - m := ztl.Meta - - var sb strings.Builder - for _, p := range m.PairsRest() { - sb.WriteString(p.Key) - sb.WriteString(": ") - sb.WriteString(p.Value) - sb.WriteByte('\n') - } - env, rb := wui.createRenderEnv(ctx, "form", wui.rtConfig.Get(ctx, nil, api.KeyLang), title, user) - rb.bindString("heading", sx.MakeString(title)) - rb.bindString("form-action-url", sx.MakeString(formActionURL)) - rb.bindString("role-data", makeStringList(roleData)) - rb.bindString("syntax-data", makeStringList(syntaxData)) - rb.bindString("meta", sx.MakeString(sb.String())) - if !ztl.Content.IsBinary() { - rb.bindString("content", sx.MakeString(ztl.Content.AsString())) - } - wui.bindCommonZettelData(ctx, &rb, user, m, &ztl.Content) - if rb.err == nil { - rb.err = wui.renderSxnTemplate(ctx, w, id.FormTemplateZid, env) - } - if err := rb.err; err != nil { - wui.reportError(ctx, w, err) - } + ctx := r.Context() + user := wui.getUser(ctx) + m := zettel.Meta + var base baseData + wui.makeBaseData(ctx, config.GetLang(m, wui.rtConfig), title, user, &base) + wui.renderTemplate(ctx, w, id.FormTemplateZid, &base, formZettelData{ + Heading: heading, + MetaTitle: m.GetDefault(meta.KeyTitle, ""), + MetaTags: m.GetDefault(meta.KeyTags, ""), + MetaRole: config.GetRole(m, wui.rtConfig), + MetaSyntax: config.GetSyntax(m, wui.rtConfig), + MetaPairsRest: m.PairsRest(false), + IsTextContent: !zettel.Content.IsBinary(), + Content: zettel.Content.AsString(), + }) } // MakePostCreateZettelHandler creates a new HTTP handler to store content of // an existing zettel. -func (wui *WebUI) MakePostCreateZettelHandler(createZettel *usecase.CreateZettel) http.HandlerFunc { +func (wui *WebUI) MakePostCreateZettelHandler(createZettel usecase.CreateZettel) http.HandlerFunc { return func(w http.ResponseWriter, r *http.Request) { ctx := r.Context() - reEdit, zettel, err := parseZettelForm(r, id.Invalid) - if err == errMissingContent { - wui.reportError(ctx, w, adapter.NewErrBadRequest("Content is missing")) + zettel, hasContent, err := parseZettelForm(r, id.Invalid) + if err != nil { + wui.reportError(ctx, w, adapter.NewErrBadRequest("Unable to read form data")) return } - if err != nil { - const msg = "Unable to read form data" - wui.log.Info().Err(err).Msg(msg) - wui.reportError(ctx, w, adapter.NewErrBadRequest(msg)) + if !hasContent { + wui.reportError(ctx, w, adapter.NewErrBadRequest("Content is missing")) return } newZid, err := createZettel.Run(ctx, zettel) if err != nil { wui.reportError(ctx, w, err) return } - if reEdit { - wui.redirectFound(w, r, wui.NewURLBuilder('e').SetZid(newZid.ZettelID())) - } else { - wui.redirectFound(w, r, wui.NewURLBuilder('h').SetZid(newZid.ZettelID())) - } - } -} - -// MakeGetZettelFromListHandler creates a new HTTP handler to store content of -// an existing zettel. -func (wui *WebUI) MakeGetZettelFromListHandler( - queryMeta *usecase.Query, evaluate *usecase.Evaluate, - ucListRoles usecase.ListRoles, ucListSyntax usecase.ListSyntax) http.HandlerFunc { - - return func(w http.ResponseWriter, r *http.Request) { - q := adapter.GetQuery(r.URL.Query()) - ctx := r.Context() - metaSeq, err := queryMeta.Run(box.NoEnrichQuery(ctx, q), q) - if err != nil { - wui.reportError(ctx, w, err) - return - } - 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) - return - } - - m := meta.New(id.Invalid) - m.Set(api.KeyTitle, q.Human()) - m.Set(api.KeySyntax, api.ValueSyntaxZmk) - if qval := q.String(); qval != "" { - m.Set(api.KeyQuery, qval) - } - zettel := zettel.Zettel{Meta: m, Content: zettel.NewContent(zmkContent.Bytes())} - roleData, syntaxData := retrieveDataLists(ctx, ucListRoles, ucListSyntax) - wui.renderZettelForm(ctx, w, zettel, "Zettel from list", wui.createNewURL, roleData, syntaxData) + redirectFound(w, r, wui.NewURLBuilder('h').SetZid(newZid)) } } Index: web/adapter/webui/delete_zettel.go ================================================================== --- web/adapter/webui/delete_zettel.go +++ web/adapter/webui/delete_zettel.go @@ -1,119 +1,80 @@ //----------------------------------------------------------------------------- -// Copyright (c) 2020-present Detlef Stern +// Copyright (c) 2020-2021 Detlef Stern // -// This file is part of Zettelstore. +// This file is part of zettelstore. // // Zettelstore is licensed under the latest version of the EUPL (European Union // Public License). Please see file LICENSE.txt for your rights and obligations // under this license. -// -// SPDX-License-Identifier: EUPL-1.2 -// SPDX-FileCopyrightText: 2020-present Detlef Stern //----------------------------------------------------------------------------- +// Package webui provides web-UI handlers for web requests. package webui import ( + "fmt" "net/http" - "t73f.de/r/sx" - "t73f.de/r/zsc/api" - "t73f.de/r/zsc/maps" + "zettelstore.de/z/api" "zettelstore.de/z/box" - "zettelstore.de/z/strfun" + "zettelstore.de/z/config" + "zettelstore.de/z/domain/id" + "zettelstore.de/z/domain/meta" "zettelstore.de/z/usecase" - "zettelstore.de/z/web/server" - "zettelstore.de/z/zettel/id" - "zettelstore.de/z/zettel/meta" + "zettelstore.de/z/web/adapter" ) // MakeGetDeleteZettelHandler creates a new HTTP handler to display the // HTML delete view of a zettel. -func (wui *WebUI) MakeGetDeleteZettelHandler(getZettel usecase.GetZettel, getAllZettel usecase.GetAllZettel) http.HandlerFunc { - return func(w http.ResponseWriter, r *http.Request) { - ctx := r.Context() - path := r.URL.Path[1:] - zid, err := id.Parse(path) - if err != nil { - wui.reportError(ctx, w, box.ErrInvalidZid{Zid: path}) - return - } - - zs, err := getAllZettel.Run(ctx, zid) - if err != nil { - wui.reportError(ctx, w, err) - return - } - m := zs[0].Meta - - user := server.GetUser(ctx) - env, rb := wui.createRenderEnv( - ctx, "delete", - wui.rtConfig.Get(ctx, nil, api.KeyLang), "Delete Zettel "+m.Zid.String(), user) - if len(zs) > 1 { - rb.bindString("shadowed-box", sx.MakeString(zs[1].Meta.GetDefault(api.KeyBoxNumber, "???"))) - rb.bindString("incoming", nil) - } else { - rb.bindString("shadowed-box", nil) - rb.bindString("incoming", wui.encodeIncoming(m, wui.makeGetTextTitle(ctx, getZettel))) - } - wui.bindCommonZettelData(ctx, &rb, user, m, nil) - - if rb.err == nil { - err = wui.renderSxnTemplate(ctx, w, id.DeleteTemplateZid, env) - } else { - err = rb.err - } - if err != nil { - wui.reportError(ctx, w, err) - } - } -} - -func (wui *WebUI) encodeIncoming(m *meta.Meta, getTextTitle getTextTitleFunc) *sx.Pair { - zidMap := make(strfun.Set) - addListValues(zidMap, m, api.KeyBackward) - for _, kd := range meta.GetSortedKeyDescriptions() { - inverseKey := kd.Inverse - if inverseKey == "" { - continue - } - ikd := meta.GetDescription(inverseKey) - switch ikd.Type { - case meta.TypeID: - if val, ok := m.Get(inverseKey); ok { - zidMap.Set(val) - } - case meta.TypeIDSet: - addListValues(zidMap, m, inverseKey) - } - } - return wui.zidLinksSxn(maps.Keys(zidMap), getTextTitle) -} - -func addListValues(zidMap strfun.Set, m *meta.Meta, key string) { - if values, ok := m.GetList(key); ok { - for _, val := range values { - zidMap.Set(val) - } +func (wui *WebUI) MakeGetDeleteZettelHandler(getZettel usecase.GetZettel) http.HandlerFunc { + return func(w http.ResponseWriter, r *http.Request) { + ctx := r.Context() + if format, formatText := adapter.GetFormat(r, r.URL.Query(), api.EncoderHTML); format != api.EncoderHTML { + wui.reportError(ctx, w, adapter.NewErrBadRequest( + fmt.Sprintf("Delete zettel not possible in format %q", formatText))) + return + } + + zid, err := id.Parse(r.URL.Path[1:]) + if err != nil { + wui.reportError(ctx, w, box.ErrNotFound) + return + } + + zettel, err := getZettel.Run(ctx, zid) + if err != nil { + wui.reportError(ctx, w, err) + return + } + + user := wui.getUser(ctx) + m := zettel.Meta + var base baseData + wui.makeBaseData(ctx, config.GetLang(m, wui.rtConfig), "Delete Zettel "+m.Zid.String(), user, &base) + wui.renderTemplate(ctx, w, id.DeleteTemplateZid, &base, struct { + Zid string + MetaPairs []meta.Pair + }{ + Zid: zid.String(), + MetaPairs: m.Pairs(true), + }) } } // MakePostDeleteZettelHandler creates a new HTTP handler to delete a zettel. -func (wui *WebUI) MakePostDeleteZettelHandler(deleteZettel *usecase.DeleteZettel) http.HandlerFunc { +func (wui *WebUI) MakePostDeleteZettelHandler(deleteZettel usecase.DeleteZettel) http.HandlerFunc { return func(w http.ResponseWriter, r *http.Request) { ctx := r.Context() - path := r.URL.Path[1:] - zid, err := id.Parse(path) + zid, err := id.Parse(r.URL.Path[1:]) if err != nil { - wui.reportError(ctx, w, box.ErrInvalidZid{Zid: path}) + wui.reportError(ctx, w, box.ErrNotFound) return } - if err = deleteZettel.Run(r.Context(), zid); err != nil { + if err := deleteZettel.Run(r.Context(), zid); err != nil { wui.reportError(ctx, w, err) return } - wui.redirectFound(w, r, wui.NewURLBuilder('/')) + redirectFound(w, r, wui.NewURLBuilder('/')) } } Index: web/adapter/webui/edit_zettel.go ================================================================== --- web/adapter/webui/edit_zettel.go +++ web/adapter/webui/edit_zettel.go @@ -1,82 +1,90 @@ //----------------------------------------------------------------------------- -// Copyright (c) 2020-present Detlef Stern +// Copyright (c) 2020-2021 Detlef Stern // -// This file is part of Zettelstore. +// This file is part of zettelstore. // // Zettelstore is licensed under the latest version of the EUPL (European Union // Public License). Please see file LICENSE.txt for your rights and obligations // under this license. -// -// SPDX-License-Identifier: EUPL-1.2 -// SPDX-FileCopyrightText: 2020-present Detlef Stern //----------------------------------------------------------------------------- +// Package webui provides web-UI handlers for web requests. package webui import ( + "fmt" "net/http" + "zettelstore.de/z/api" "zettelstore.de/z/box" + "zettelstore.de/z/config" + "zettelstore.de/z/domain/id" + "zettelstore.de/z/domain/meta" "zettelstore.de/z/usecase" "zettelstore.de/z/web/adapter" - "zettelstore.de/z/zettel/id" ) // MakeEditGetZettelHandler creates a new HTTP handler to display the // HTML edit view of a zettel. -func (wui *WebUI) MakeEditGetZettelHandler(getZettel usecase.GetZettel, ucListRoles usecase.ListRoles, ucListSyntax usecase.ListSyntax) http.HandlerFunc { +func (wui *WebUI) MakeEditGetZettelHandler(getZettel usecase.GetZettel) http.HandlerFunc { return func(w http.ResponseWriter, r *http.Request) { ctx := r.Context() - path := r.URL.Path[1:] - zid, err := id.Parse(path) + zid, err := id.Parse(r.URL.Path[1:]) if err != nil { - wui.reportError(ctx, w, box.ErrInvalidZid{Zid: path}) + wui.reportError(ctx, w, box.ErrNotFound) return } zettel, err := getZettel.Run(box.NoEnrichContext(ctx), zid) if err != nil { wui.reportError(ctx, w, err) return } - roleData, syntaxData := retrieveDataLists(ctx, ucListRoles, ucListSyntax) - wui.renderZettelForm(ctx, w, zettel, "Edit Zettel", "", roleData, syntaxData) + if format, formatText := adapter.GetFormat(r, r.URL.Query(), api.EncoderHTML); format != api.EncoderHTML { + wui.reportError(ctx, w, adapter.NewErrBadRequest( + fmt.Sprintf("Edit zettel %q not possible in format %q", zid, formatText))) + return + } + + user := wui.getUser(ctx) + m := zettel.Meta + var base baseData + wui.makeBaseData(ctx, config.GetLang(m, wui.rtConfig), "Edit Zettel", user, &base) + wui.renderTemplate(ctx, w, id.FormTemplateZid, &base, formZettelData{ + Heading: base.Title, + MetaTitle: m.GetDefault(meta.KeyTitle, ""), + MetaRole: m.GetDefault(meta.KeyRole, ""), + MetaTags: m.GetDefault(meta.KeyTags, ""), + MetaSyntax: m.GetDefault(meta.KeySyntax, ""), + MetaPairsRest: m.PairsRest(false), + IsTextContent: !zettel.Content.IsBinary(), + Content: zettel.Content.AsString(), + }) } } // MakeEditSetZettelHandler creates a new HTTP handler to store content of // an existing zettel. -func (wui *WebUI) MakeEditSetZettelHandler(updateZettel *usecase.UpdateZettel) http.HandlerFunc { - return func(w http.ResponseWriter, r *http.Request) { - ctx := r.Context() - path := r.URL.Path[1:] - zid, err := id.Parse(path) - if err != nil { - wui.reportError(ctx, w, box.ErrInvalidZid{Zid: path}) - return - } - - reEdit, zettel, err := parseZettelForm(r, zid) - hasContent := true - if err != nil { - if err != errMissingContent { - const msg = "Unable to read zettel form" - wui.log.Info().Err(err).Msg(msg) - wui.reportError(ctx, w, adapter.NewErrBadRequest(msg)) - return - } - hasContent = false - } - if err = updateZettel.Run(r.Context(), zettel, hasContent); err != nil { - wui.reportError(ctx, w, err) - return - } - - if reEdit { - wui.redirectFound(w, r, wui.NewURLBuilder('e').SetZid(zid.ZettelID())) - } else { - wui.redirectFound(w, r, wui.NewURLBuilder('h').SetZid(zid.ZettelID())) - } +func (wui *WebUI) MakeEditSetZettelHandler(updateZettel usecase.UpdateZettel) http.HandlerFunc { + return func(w http.ResponseWriter, r *http.Request) { + ctx := r.Context() + zid, err := id.Parse(r.URL.Path[1:]) + if err != nil { + wui.reportError(ctx, w, box.ErrNotFound) + return + } + + zettel, hasContent, err := parseZettelForm(r, zid) + if err != nil { + wui.reportError(ctx, w, adapter.NewErrBadRequest("Unable to read zettel form")) + return + } + + if err := updateZettel.Run(r.Context(), zettel, hasContent); err != nil { + wui.reportError(ctx, w, err) + return + } + redirectFound(w, r, wui.NewURLBuilder('h').SetZid(zid)) } } DELETED web/adapter/webui/favicon.go Index: web/adapter/webui/favicon.go ================================================================== --- web/adapter/webui/favicon.go +++ web/adapter/webui/favicon.go @@ -1,47 +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. -// -// SPDX-License-Identifier: EUPL-1.2 -// SPDX-FileCopyrightText: 2022-present Detlef Stern -//----------------------------------------------------------------------------- - -package webui - -import ( - "io" - "net/http" - "os" - "path/filepath" - - "zettelstore.de/z/web/adapter" -) - -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.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.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 @@ -1,90 +1,76 @@ //----------------------------------------------------------------------------- -// Copyright (c) 2020-present Detlef Stern +// Copyright (c) 2020-2021 Detlef Stern // -// This file is part of Zettelstore. +// This file is part of zettelstore. // // Zettelstore is licensed under the latest version of the EUPL (European Union // Public License). Please see file LICENSE.txt for your rights and obligations // under this license. -// -// SPDX-License-Identifier: EUPL-1.2 -// SPDX-FileCopyrightText: 2020-present Detlef Stern //----------------------------------------------------------------------------- +// Package webui provides web-UI handlers for web requests. package webui import ( - "bytes" - "errors" - "io" - "net/http" - "regexp" - "strings" - "unicode" - - "t73f.de/r/zsc/api" - "t73f.de/r/zsc/input" - "zettelstore.de/z/kernel" - "zettelstore.de/z/parser" - "zettelstore.de/z/web/content" - "zettelstore.de/z/zettel" - "zettelstore.de/z/zettel/id" - "zettelstore.de/z/zettel/meta" -) - -var ( - bsCRLF = []byte{'\r', '\n'} - bsLF = []byte{'\n'} -) - -var errMissingContent = errors.New("missing zettel content") - -func parseZettelForm(r *http.Request, zid id.Zid) (bool, zettel.Zettel, error) { - maxRequestSize := kernel.Main.GetConfig(kernel.WebService, kernel.WebMaxRequestSize).(int64) - err := r.ParseMultipartForm(maxRequestSize) - if err != nil { - return false, zettel.Zettel{}, err - } - _, doSave := r.Form["save"] + "net/http" + "strings" + + "zettelstore.de/z/domain" + "zettelstore.de/z/domain/id" + "zettelstore.de/z/domain/meta" + "zettelstore.de/z/input" +) + +type formZettelData struct { + Heading string + MetaTitle string + MetaRole string + MetaTags string + MetaSyntax string + MetaPairsRest []meta.Pair + IsTextContent bool + Content string +} + +func parseZettelForm(r *http.Request, zid id.Zid) (domain.Zettel, bool, error) { + err := r.ParseForm() + if err != nil { + return domain.Zettel{}, false, err + } var m *meta.Meta if postMeta, ok := trimmedFormValue(r, "meta"); ok { - m = meta.NewFromInput(zid, input.NewInput(removeEmptyLines([]byte(postMeta)))) - m.Sanitize() + m = meta.NewFromInput(zid, input.NewInput(postMeta)) } else { m = meta.New(zid) } if postTitle, ok := trimmedFormValue(r, "title"); ok { - m.Set(api.KeyTitle, meta.RemoveNonGraphic(postTitle)) + m.Set(meta.KeyTitle, postTitle) } if postTags, ok := trimmedFormValue(r, "tags"); ok { - if tags := meta.ListFromValue(meta.RemoveNonGraphic(postTags)); len(tags) > 0 { - for i, tag := range tags { - tags[i] = meta.NormalizeTag(tag) - } - m.SetList(api.KeyTags, tags) + if tags := strings.Fields(postTags); len(tags) > 0 { + m.SetList(meta.KeyTags, tags) } } if postRole, ok := trimmedFormValue(r, "role"); ok { - m.SetWord(api.KeyRole, meta.RemoveNonGraphic(postRole)) + m.Set(meta.KeyRole, postRole) } if postSyntax, ok := trimmedFormValue(r, "syntax"); ok { - m.SetWord(api.KeySyntax, meta.RemoveNonGraphic(postSyntax)) - } - - if data := textContent(r); data != nil { - return doSave, zettel.Zettel{Meta: m, Content: zettel.NewContent(data)}, nil - } - if data, m2 := uploadedContent(r, m); data != nil { - return doSave, zettel.Zettel{Meta: m2, Content: zettel.NewContent(data)}, nil - } - - if allowEmptyContent(m) { - return doSave, zettel.Zettel{Meta: m, Content: zettel.NewContent(nil)}, nil - } - return doSave, zettel.Zettel{Meta: m, Content: zettel.NewContent(nil)}, errMissingContent + m.Set(meta.KeySyntax, postSyntax) + } + if values, ok := r.PostForm["content"]; ok && len(values) > 0 { + return domain.Zettel{ + Meta: m, + Content: domain.NewContent( + strings.ReplaceAll(strings.TrimSpace(values[0]), "\r\n", "\n")), + }, true, nil + } + return domain.Zettel{ + Meta: m, + Content: domain.NewContent(""), + }, false, nil } func trimmedFormValue(r *http.Request, key string) (string, bool) { if values, ok := r.PostForm[key]; ok && len(values) > 0 { value := strings.TrimSpace(values[0]) @@ -92,56 +78,5 @@ return value, true } } return "", false } - -func textContent(r *http.Request) []byte { - if values, found := r.PostForm["content"]; found && len(values) > 0 { - result := bytes.ReplaceAll([]byte(values[0]), bsCRLF, bsLF) - if bytes.IndexFunc(result, func(ch rune) bool { return !unicode.IsSpace(ch) }) >= 0 { - return result - } - } - return nil -} - -func uploadedContent(r *http.Request, m *meta.Meta) ([]byte, *meta.Meta) { - file, fh, err := r.FormFile("file") - if file != nil { - defer file.Close() - if err == nil { - data, err2 := io.ReadAll(file) - if err2 != nil { - return nil, m - } - if cts, found := fh.Header["Content-Type"]; found && len(cts) > 0 { - ct := cts[0] - if fileSyntax := content.SyntaxFromMIME(ct, data); fileSyntax != "" { - m = m.Clone() - m.Set(api.KeySyntax, fileSyntax) - } - } - return data, m - } - } - return nil, m -} - -func allowEmptyContent(m *meta.Meta) bool { - if syntax, found := m.Get(api.KeySyntax); found { - if syntax == api.ValueSyntaxNone { - return true - } - if pinfo := parser.Get(syntax); pinfo != nil { - return pinfo.IsTextFormat - } - } - return true -} - -var reEmptyLines = regexp.MustCompile(`(\n|\r)+\s*(\n|\r)+`) - -func removeEmptyLines(s []byte) []byte { - b := bytes.TrimSpace(s) - return reEmptyLines.ReplaceAllLiteral(b, []byte{'\n'}) -} DELETED web/adapter/webui/forms_test.go Index: web/adapter/webui/forms_test.go ================================================================== --- web/adapter/webui/forms_test.go +++ web/adapter/webui/forms_test.go @@ -1,38 +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. -// -// SPDX-License-Identifier: EUPL-1.2 -// SPDX-FileCopyrightText: 2020-present Detlef Stern -//----------------------------------------------------------------------------- - -package webui - -import "testing" - -func TestRemoveEmptyLines(t *testing.T) { - t.Parallel() - testcases := []struct { - in string - exp string - }{ - {"", ""}, - {"a", "a"}, - {"\na", "a"}, - {"a\n", "a"}, - {"a\nb", "a\nb"}, - {"a\n\nb", "a\nb"}, - {"a\n \nb", "a\nb"}, - } - for i, tc := range testcases { - got := string(removeEmptyLines([]byte(tc.in))) - if got != tc.exp { - t.Errorf("%d/%q: expected=%q, got=%q", i, tc.in, tc.exp, got) - } - } -} Index: web/adapter/webui/get_info.go ================================================================== --- web/adapter/webui/get_info.go +++ web/adapter/webui/get_info.go @@ -1,231 +1,217 @@ //----------------------------------------------------------------------------- -// 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 -//----------------------------------------------------------------------------- - -package webui - -import ( - "context" - "net/http" - "sort" - "strings" - - "t73f.de/r/sx" - "t73f.de/r/zsc/api" - "zettelstore.de/z/ast" - "zettelstore.de/z/box" - "zettelstore.de/z/collect" - "zettelstore.de/z/encoder" - "zettelstore.de/z/evaluator" - "zettelstore.de/z/parser" - "zettelstore.de/z/query" - "zettelstore.de/z/strfun" - "zettelstore.de/z/usecase" - "zettelstore.de/z/web/server" - "zettelstore.de/z/zettel/id" -) - -// MakeGetInfoHandler creates a new HTTP handler for the use case "get zettel". -func (wui *WebUI) MakeGetInfoHandler( - ucParseZettel usecase.ParseZettel, - ucEvaluate *usecase.Evaluate, - ucGetZettel usecase.GetZettel, - ucGetAllMeta usecase.GetAllZettel, - ucQuery *usecase.Query, -) http.HandlerFunc { - return func(w http.ResponseWriter, r *http.Request) { - ctx := r.Context() - q := r.URL.Query() - - path := r.URL.Path[1:] - zid, err := id.Parse(path) - if err != nil { - wui.reportError(ctx, w, box.ErrInvalidZid{Zid: path}) - return - } - - zn, err := ucParseZettel.Run(ctx, zid, q.Get(api.KeySyntax)) - if err != nil { - wui.reportError(ctx, w, err) - return - } - - enc := wui.getSimpleHTMLEncoder(wui.rtConfig.Get(ctx, zn.InhMeta, api.KeyLang)) - getTextTitle := wui.makeGetTextTitle(ctx, ucGetZettel) - evalMeta := func(val string) ast.InlineSlice { - return ucEvaluate.RunMetadata(ctx, val) - } - pairs := zn.Meta.ComputedPairs() - metadata := sx.Nil() - for i := len(pairs) - 1; i >= 0; i-- { - key := pairs[i].Key - sxval := wui.writeHTMLMetaValue(key, pairs[i].Value, getTextTitle, evalMeta, enc) - metadata = metadata.Cons(sx.Cons(sx.MakeString(key), sxval)) - } - - summary := collect.References(zn) - locLinks, queryLinks, extLinks := wui.splitLocSeaExtLinks(append(summary.Links, summary.Embeds...)) - - title := parser.NormalizedSpacedText(zn.InhMeta.GetTitle()) - phrase := q.Get(api.QueryKeyPhrase) - if phrase == "" { - phrase = title - } - unlinkedMeta, err := ucQuery.Run(ctx, createUnlinkedQuery(zid, phrase)) - if err != nil { - wui.reportError(ctx, w, err) - return - } - - 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 - } - encTexts := encodingTexts() - shadowLinks := getShadowLinks(ctx, zid, ucGetAllMeta) - - user := server.GetUser(ctx) - env, rb := wui.createRenderEnv(ctx, "info", wui.rtConfig.Get(ctx, nil, api.KeyLang), title, user) - rb.bindString("metadata", metadata) - rb.bindString("local-links", locLinks) - rb.bindString("query-links", queryLinks) - rb.bindString("ext-links", extLinks) - rb.bindString("unlinked-content", unlinkedContent) - rb.bindString("phrase", sx.MakeString(phrase)) - rb.bindString("query-key-phrase", sx.MakeString(api.QueryKeyPhrase)) - rb.bindString("enc-eval", wui.infoAPIMatrix(zid, false, encTexts)) - rb.bindString("enc-parsed", wui.infoAPIMatrixParsed(zid, encTexts)) - rb.bindString("shadow-links", shadowLinks) - wui.bindCommonZettelData(ctx, &rb, user, zn.InhMeta, &zn.Content) - if rb.err == nil { - err = wui.renderSxnTemplate(ctx, w, id.InfoTemplateZid, env) - } else { - err = rb.err - } - if err != nil { - wui.reportError(ctx, w, err) - } - } -} - -func (wui *WebUI) splitLocSeaExtLinks(links []*ast.Reference) (locLinks, queries, extLinks *sx.Pair) { - for i := len(links) - 1; i >= 0; i-- { - ref := links[i] - if ref.State == ast.RefStateSelf || ref.IsZettel() { - continue - } - if ref.State == ast.RefStateQuery { - queries = queries.Cons( - sx.Cons( - sx.MakeString(ref.Value), - sx.MakeString(wui.NewURLBuilder('h').AppendQuery(ref.Value).String()))) - continue - } - if ref.IsExternal() { - extLinks = extLinks.Cons(sx.MakeString(ref.String())) - continue - } - locLinks = locLinks.Cons(sx.Cons(sx.MakeBoolean(ref.IsValid()), sx.MakeString(ref.String()))) - } - return locLinks, queries, extLinks -} - -func createUnlinkedQuery(zid id.Zid, phrase string) *query.Query { - var sb strings.Builder - sb.Write(zid.Bytes()) - sb.WriteByte(' ') - sb.WriteString(api.UnlinkedDirective) - for _, word := range strfun.MakeWords(phrase) { - sb.WriteByte(' ') - sb.WriteString(api.PhraseDirective) - sb.WriteByte(' ') - sb.WriteString(word) - } - sb.WriteByte(' ') - sb.WriteString(api.OrderDirective) - sb.WriteByte(' ') - sb.WriteString(api.KeyID) - return query.Parse(sb.String()) -} - -func encodingTexts() []string { - encodings := encoder.GetEncodings() - encTexts := make([]string, 0, len(encodings)) - for _, f := range encodings { - encTexts = append(encTexts, f.String()) - } - sort.Strings(encTexts) - return encTexts -} - -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(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] - if parseOnly { - u.AppendKVQuery(api.QueryKeyParseOnly, "") - } - u.AppendKVQuery(api.QueryKeyPart, part) - u.AppendKVQuery(api.QueryKeyEncoding, enc) - row = row.Cons(sx.Cons(sx.MakeString(enc), sx.MakeString(u.String()))) - u.ClearQuery() - } - matrix = matrix.Cons(sx.Cons(sx.MakeString(part), row)) - } - return matrix -} - -func (wui *WebUI) infoAPIMatrixParsed(zid id.Zid, encTexts []string) *sx.Pair { - matrix := wui.infoAPIMatrix(zid, true, encTexts) - 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 - } - last := line.LastPair() - part := apiParts[i] - u.AppendKVQuery(api.QueryKeyPart, part) - last = last.AppendBang(sx.Cons(sx.MakeString("plain"), sx.MakeString(u.String()))) - u.ClearQuery() - if i < 2 { - u.AppendKVQuery(api.QueryKeyEncoding, api.EncodingData) - u.AppendKVQuery(api.QueryKeyPart, part) - last.AppendBang(sx.Cons(sx.MakeString("data"), sx.MakeString(u.String()))) - u.ClearQuery() - } - i++ - } - return matrix -} - -func getShadowLinks(ctx context.Context, zid id.Zid, getAllZettel usecase.GetAllZettel) *sx.Pair { - result := sx.Nil() - if zl, err := getAllZettel.Run(ctx, zid); err == nil { - for i := len(zl) - 1; i >= 1; i-- { - if boxNo, ok := zl[i].Meta.Get(api.KeyBoxNumber); ok { - result = result.Cons(sx.MakeString(boxNo)) - } +// Copyright (c) 2020-2021 Detlef Stern +// +// This file is part of zettelstore. +// +// Zettelstore is licensed under the latest version of the EUPL (European Union +// Public License). Please see file LICENSE.txt for your rights and obligations +// under this license. +//----------------------------------------------------------------------------- + +// Package webui provides web-UI handlers for web requests. +package webui + +import ( + "context" + "fmt" + "net/http" + "sort" + "strings" + + "zettelstore.de/z/api" + "zettelstore.de/z/ast" + "zettelstore.de/z/box" + "zettelstore.de/z/collect" + "zettelstore.de/z/config" + "zettelstore.de/z/domain/id" + "zettelstore.de/z/domain/meta" + "zettelstore.de/z/encoder" + "zettelstore.de/z/encoder/encfun" + "zettelstore.de/z/usecase" + "zettelstore.de/z/web/adapter" +) + +type metaDataInfo struct { + Key string + Value string +} + +type matrixElement struct { + Text string + HasURL bool + URL string +} +type matrixLine struct { + Elements []matrixElement +} + +// MakeGetInfoHandler creates a new HTTP handler for the use case "get zettel". +func (wui *WebUI) MakeGetInfoHandler( + parseZettel usecase.ParseZettel, + getMeta usecase.GetMeta, + getAllMeta usecase.GetAllMeta, +) http.HandlerFunc { + return func(w http.ResponseWriter, r *http.Request) { + ctx := r.Context() + q := r.URL.Query() + if format, formatText := adapter.GetFormat(r, q, api.EncoderHTML); format != api.EncoderHTML { + wui.reportError(ctx, w, adapter.NewErrBadRequest( + fmt.Sprintf("Zettel info not available in format %q", formatText))) + return + } + + zid, err := id.Parse(r.URL.Path[1:]) + if err != nil { + wui.reportError(ctx, w, box.ErrNotFound) + return + } + + zn, err := parseZettel.Run(ctx, zid, q.Get("syntax")) + if err != nil { + wui.reportError(ctx, w, err) + return + } + + summary := collect.References(zn) + locLinks, extLinks := splitLocExtLinks(append(summary.Links, summary.Images...)) + + lang := config.GetLang(zn.InhMeta, wui.rtConfig) + env := encoder.Environment{Lang: lang} + pairs := zn.Meta.Pairs(true) + metaData := make([]metaDataInfo, len(pairs)) + getTitle := makeGetTitle(ctx, getMeta, &env) + for i, p := range pairs { + var html strings.Builder + wui.writeHTMLMetaValue(&html, zn.Meta, p.Key, getTitle, &env) + metaData[i] = metaDataInfo{p.Key, html.String()} + } + shadowLinks := getShadowLinks(ctx, zid, getAllMeta) + endnotes, err := formatBlocks(nil, api.EncoderHTML, &env) + if err != nil { + endnotes = "" + } + + textTitle := encfun.MetaAsText(zn.InhMeta, meta.KeyTitle) + user := wui.getUser(ctx) + canCreate := wui.canCreate(ctx, user) + var base baseData + wui.makeBaseData(ctx, lang, textTitle, user, &base) + wui.renderTemplate(ctx, w, id.InfoTemplateZid, &base, struct { + Zid string + WebURL string + ContextURL string + CanWrite bool + EditURL string + CanFolge bool + FolgeURL string + CanCopy bool + CopyURL string + CanRename bool + RenameURL string + CanDelete bool + DeleteURL string + MetaData []metaDataInfo + HasLinks bool + HasLocLinks bool + LocLinks []localLink + HasExtLinks bool + ExtLinks []string + ExtNewWindow string + Matrix []matrixLine + HasShadowLinks bool + ShadowLinks []string + Endnotes string + }{ + Zid: zid.String(), + WebURL: wui.NewURLBuilder('h').SetZid(zid).String(), + ContextURL: wui.NewURLBuilder('j').SetZid(zid).String(), + CanWrite: wui.canWrite(ctx, user, zn.Meta, zn.Content), + EditURL: wui.NewURLBuilder('e').SetZid(zid).String(), + CanFolge: canCreate, + FolgeURL: wui.NewURLBuilder('f').SetZid(zid).String(), + CanCopy: canCreate && !zn.Content.IsBinary(), + CopyURL: wui.NewURLBuilder('c').SetZid(zid).String(), + CanRename: wui.canRename(ctx, user, zn.Meta), + RenameURL: wui.NewURLBuilder('b').SetZid(zid).String(), + CanDelete: wui.canDelete(ctx, user, zn.Meta), + DeleteURL: wui.NewURLBuilder('d').SetZid(zid).String(), + MetaData: metaData, + HasLinks: len(extLinks)+len(locLinks) > 0, + HasLocLinks: len(locLinks) > 0, + LocLinks: locLinks, + HasExtLinks: len(extLinks) > 0, + ExtLinks: extLinks, + ExtNewWindow: htmlAttrNewWindow(len(extLinks) > 0), + Matrix: wui.infoAPIMatrix(zid), + HasShadowLinks: len(shadowLinks) > 0, + ShadowLinks: shadowLinks, + Endnotes: endnotes, + }) + } +} + +type localLink struct { + Valid bool + Zid string +} + +func splitLocExtLinks(links []*ast.Reference) (locLinks []localLink, extLinks []string) { + if len(links) == 0 { + return nil, nil + } + for _, ref := range links { + if ref.State == ast.RefStateSelf { + continue + } + if ref.IsZettel() { + continue + } + if ref.IsExternal() { + extLinks = append(extLinks, ref.String()) + continue + } + locLinks = append(locLinks, localLink{ref.IsValid(), ref.String()}) + } + return locLinks, extLinks +} + +func (wui *WebUI) infoAPIMatrix(zid id.Zid) []matrixLine { + formats := encoder.GetFormats() + formatTexts := make([]string, 0, len(formats)) + for _, f := range formats { + formatTexts = append(formatTexts, f.String()) + } + sort.Strings(formatTexts) + defFormat := encoder.GetDefaultFormat().String() + parts := []string{"zettel", "meta", "content"} + matrix := make([]matrixLine, 0, len(parts)) + u := wui.NewURLBuilder('z').SetZid(zid) + for _, part := range parts { + row := make([]matrixElement, 0, len(formatTexts)+1) + row = append(row, matrixElement{part, false, ""}) + for _, format := range formatTexts { + u.AppendQuery(api.QueryKeyPart, part) + if format != defFormat { + u.AppendQuery(api.QueryKeyFormat, format) + } + row = append(row, matrixElement{format, true, u.String()}) + u.ClearQuery() + } + matrix = append(matrix, matrixLine{row}) + } + return matrix +} + +func getShadowLinks(ctx context.Context, zid id.Zid, getAllMeta usecase.GetAllMeta) []string { + ml, err := getAllMeta.Run(ctx, zid) + if err != nil || len(ml) < 2 { + return nil + } + result := make([]string, 0, len(ml)-1) + for _, m := range ml[1:] { + if boxNo, ok := m.Get(meta.KeyBoxNumber); ok { + result = append(result, boxNo) } } return result } Index: web/adapter/webui/get_zettel.go ================================================================== --- web/adapter/webui/get_zettel.go +++ web/adapter/webui/get_zettel.go @@ -1,163 +1,205 @@ //----------------------------------------------------------------------------- -// 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 -//----------------------------------------------------------------------------- - -package webui - -import ( - "context" - "net/http" - "strings" - - "t73f.de/r/sx" - "t73f.de/r/zsc/api" - "t73f.de/r/zsc/shtml" - "zettelstore.de/z/box" - "zettelstore.de/z/config" - "zettelstore.de/z/parser" - "zettelstore.de/z/usecase" - "zettelstore.de/z/web/server" - "zettelstore.de/z/zettel/id" - "zettelstore.de/z/zettel/meta" -) - -// MakeGetHTMLZettelHandler creates a new HTTP handler for the use case "get zettel". -func (wui *WebUI) MakeGetHTMLZettelHandler(evaluate *usecase.Evaluate, getZettel usecase.GetZettel) http.HandlerFunc { - return func(w http.ResponseWriter, r *http.Request) { - ctx := r.Context() - path := r.URL.Path[1:] - zid, err := id.Parse(path) - if err != nil { - wui.reportError(ctx, w, box.ErrInvalidZid{Zid: path}) - return - } - - q := r.URL.Query() - zn, err := evaluate.Run(ctx, zid, q.Get(api.KeySyntax)) - if err != nil { - wui.reportError(ctx, w, err) - return - } - - enc := wui.getSimpleHTMLEncoder(wui.rtConfig.Get(ctx, zn.InhMeta, api.KeyLang)) - metaObj := enc.MetaSxn(zn.InhMeta, createEvalMetadataFunc(ctx, evaluate)) - content, endnotes, err := enc.BlocksSxn(&zn.Ast) - if err != nil { - wui.reportError(ctx, w, err) - return - } - - 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(symMetaHeader, metaObj) - rb.bindString("heading", sx.MakeString(title)) - if role, found := zn.InhMeta.Get(api.KeyRole); found && role != "" { - rb.bindString("role-url", sx.MakeString(wui.NewURLBuilder('h').AppendQuery(api.KeyRole+api.SearchOperatorHas+role).String())) - } - if folgeRole, found := zn.InhMeta.Get(api.KeyFolgeRole); found && folgeRole != "" { - rb.bindString("folge-role-url", sx.MakeString(wui.NewURLBuilder('h').AppendQuery(api.KeyRole+api.SearchOperatorHas+folgeRole).String())) - } - 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) - wui.bindLinks(ctx, &rb, "successor", zn.InhMeta, api.KeySuccessors, config.KeyShowSuccessorLinks, getTextTitle) - if role, found := zn.InhMeta.Get(api.KeyRole); found && role != "" { - for _, part := range []string{"meta", "actions", "heading"} { - rb.rebindResolved("ROLE-"+role+"-"+part, "ROLE-DEFAULT-"+part) - } - } - wui.bindCommonZettelData(ctx, &rb, user, zn.InhMeta, &zn.Content) - if rb.err == nil { - err = wui.renderSxnTemplate(ctx, w, id.ZettelTemplateZid, env) - } else { - err = rb.err - } - if err != nil { - wui.reportError(ctx, w, err) - } - } -} - -func (wui *WebUI) identifierSetAsLinks(m *meta.Meta, key string, getTextTitle getTextTitleFunc) *sx.Pair { - 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.MakeString(capitalizeMetaKey(key)), sx.MakeString(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) { - case "false": - rb.bindString(varLinks, sx.Nil()) - return - case "close": - default: - symOpen = shtml.SymAttrOpen - } - lstLinks := wui.zettelLinksSxn(m, key, getTextTitle) - rb.bindString(varLinks, lstLinks) - if sx.IsNil(lstLinks) { - return - } - rb.bindString(varPrefix+"-open", symOpen) -} - -func (wui *WebUI) zettelLinksSxn(m *meta.Meta, key string, getTextTitle getTextTitleFunc) *sx.Pair { - values, ok := m.GetList(key) - if !ok || len(values) == 0 { - return nil - } - return wui.zidLinksSxn(values, getTextTitle) -} - -func (wui *WebUI) zidLinksSxn(values []string, getTextTitle getTextTitleFunc) (lst *sx.Pair) { - for i := len(values) - 1; i >= 0; i-- { - val := values[i] - zid, err := id.Parse(val) - if err != nil { - continue - } - if title, found := getTextTitle(zid); found > 0 { - url := sx.MakeString(wui.NewURLBuilder('h').SetZid(zid.ZettelID()).String()) - if title == "" { - lst = lst.Cons(sx.Cons(sx.MakeString(val), url)) - } else { - lst = lst.Cons(sx.Cons(sx.MakeString(title), url)) - } - } - } - return lst +// Copyright (c) 2020-2021 Detlef Stern +// +// This file is part of zettelstore. +// +// Zettelstore is licensed under the latest version of the EUPL (European Union +// Public License). Please see file LICENSE.txt for your rights and obligations +// under this license. +//----------------------------------------------------------------------------- + +// Package webui provides web-UI handlers for web requests. +package webui + +import ( + "bytes" + "net/http" + "strings" + + "zettelstore.de/z/api" + "zettelstore.de/z/ast" + "zettelstore.de/z/box" + "zettelstore.de/z/config" + "zettelstore.de/z/domain/id" + "zettelstore.de/z/domain/meta" + "zettelstore.de/z/encoder" + "zettelstore.de/z/encoder/encfun" + "zettelstore.de/z/usecase" + "zettelstore.de/z/web/adapter" +) + +// MakeGetHTMLZettelHandler creates a new HTTP handler for the use case "get zettel". +func (wui *WebUI) MakeGetHTMLZettelHandler(parseZettel usecase.ParseZettel, getMeta usecase.GetMeta) http.HandlerFunc { + return func(w http.ResponseWriter, r *http.Request) { + ctx := r.Context() + zid, err := id.Parse(r.URL.Path[1:]) + if err != nil { + wui.reportError(ctx, w, box.ErrNotFound) + return + } + + syntax := r.URL.Query().Get("syntax") + zn, err := parseZettel.Run(ctx, zid, syntax) + if err != nil { + wui.reportError(ctx, w, err) + return + } + + lang := config.GetLang(zn.InhMeta, wui.rtConfig) + envHTML := encoder.Environment{ + LinkAdapter: adapter.MakeLinkAdapter(ctx, wui, 'h', getMeta, "", api.EncoderUnknown), + ImageAdapter: adapter.MakeImageAdapter(ctx, wui, getMeta), + CiteAdapter: nil, + Lang: lang, + Xhtml: false, + MarkerExternal: wui.rtConfig.GetMarkerExternal(), + NewWindow: true, + IgnoreMeta: map[string]bool{meta.KeyTitle: true, meta.KeyLang: true}, + } + metaHeader, err := formatMeta(zn.InhMeta, api.EncoderHTML, &envHTML) + if err != nil { + wui.reportError(ctx, w, err) + return + } + htmlTitle, err := adapter.FormatInlines( + encfun.MetaAsInlineSlice(zn.InhMeta, meta.KeyTitle), api.EncoderHTML, &envHTML) + if err != nil { + wui.reportError(ctx, w, err) + return + } + htmlContent, err := formatBlocks(zn.Ast, api.EncoderHTML, &envHTML) + if err != nil { + wui.reportError(ctx, w, err) + return + } + textTitle := encfun.MetaAsText(zn.InhMeta, meta.KeyTitle) + user := wui.getUser(ctx) + roleText := zn.Meta.GetDefault(meta.KeyRole, "*") + tags := wui.buildTagInfos(zn.Meta) + canCreate := wui.canCreate(ctx, user) + getTitle := makeGetTitle(ctx, getMeta, &encoder.Environment{Lang: lang}) + extURL, hasExtURL := zn.Meta.Get(meta.KeyURL) + backLinks := wui.formatBackLinks(zn.InhMeta, getTitle) + var base baseData + wui.makeBaseData(ctx, lang, textTitle, user, &base) + base.MetaHeader = metaHeader + wui.renderTemplate(ctx, w, id.ZettelTemplateZid, &base, struct { + HTMLTitle string + CanWrite bool + EditURL string + Zid string + InfoURL string + RoleText string + RoleURL string + HasTags bool + Tags []simpleLink + CanCopy bool + CopyURL string + CanFolge bool + FolgeURL string + FolgeRefs string + PrecursorRefs string + HasExtURL bool + ExtURL string + ExtNewWindow string + Content string + HasBackLinks bool + BackLinks []simpleLink + }{ + HTMLTitle: htmlTitle, + CanWrite: wui.canWrite(ctx, user, zn.Meta, zn.Content), + EditURL: wui.NewURLBuilder('e').SetZid(zid).String(), + Zid: zid.String(), + InfoURL: wui.NewURLBuilder('i').SetZid(zid).String(), + RoleText: roleText, + RoleURL: wui.NewURLBuilder('h').AppendQuery("role", roleText).String(), + HasTags: len(tags) > 0, + Tags: tags, + CanCopy: canCreate && !zn.Content.IsBinary(), + CopyURL: wui.NewURLBuilder('c').SetZid(zid).String(), + CanFolge: canCreate, + FolgeURL: wui.NewURLBuilder('f').SetZid(zid).String(), + FolgeRefs: wui.formatMetaKey(zn.InhMeta, meta.KeyFolge, getTitle), + PrecursorRefs: wui.formatMetaKey(zn.InhMeta, meta.KeyPrecursor, getTitle), + ExtURL: extURL, + HasExtURL: hasExtURL, + ExtNewWindow: htmlAttrNewWindow(envHTML.NewWindow && hasExtURL), + Content: htmlContent, + HasBackLinks: len(backLinks) > 0, + BackLinks: backLinks, + }) + } +} + +func formatBlocks(bs ast.BlockSlice, format api.EncodingEnum, env *encoder.Environment) (string, error) { + enc := encoder.Create(format, env) + if enc == nil { + return "", adapter.ErrNoSuchFormat + } + + var content strings.Builder + _, err := enc.WriteBlocks(&content, bs) + if err != nil { + return "", err + } + return content.String(), nil +} + +func formatMeta(m *meta.Meta, format api.EncodingEnum, env *encoder.Environment) (string, error) { + enc := encoder.Create(format, env) + if enc == nil { + return "", adapter.ErrNoSuchFormat + } + + var content strings.Builder + _, err := enc.WriteMeta(&content, m) + if err != nil { + return "", err + } + return content.String(), nil +} + +func (wui *WebUI) buildTagInfos(m *meta.Meta) []simpleLink { + var tagInfos []simpleLink + if tags, ok := m.GetList(meta.KeyTags); ok { + ub := wui.NewURLBuilder('h') + tagInfos = make([]simpleLink, len(tags)) + for i, tag := range tags { + tagInfos[i] = simpleLink{Text: tag, URL: ub.AppendQuery("tags", tag).String()} + ub.ClearQuery() + } + } + return tagInfos +} + +func (wui *WebUI) formatMetaKey(m *meta.Meta, key string, getTitle getTitleFunc) string { + if _, ok := m.Get(key); ok { + var buf bytes.Buffer + wui.writeHTMLMetaValue(&buf, m, key, getTitle, nil) + return buf.String() + } + return "" +} + +func (wui *WebUI) formatBackLinks(m *meta.Meta, getTitle getTitleFunc) []simpleLink { + values, ok := m.GetList(meta.KeyBack) + if !ok || len(values) == 0 { + return nil + } + result := make([]simpleLink, 0, len(values)) + for _, val := range values { + zid, err := id.Parse(val) + if err != nil { + continue + } + if title, found := getTitle(zid, api.EncoderText); found > 0 { + url := wui.NewURLBuilder('h').SetZid(zid).String() + if title == "" { + result = append(result, simpleLink{Text: val, URL: url}) + } else { + result = append(result, simpleLink{Text: title, URL: url}) + } + } + } + return result } DELETED web/adapter/webui/goaction.go Index: web/adapter/webui/goaction.go ================================================================== --- web/adapter/webui/goaction.go +++ web/adapter/webui/goaction.go @@ -1,35 +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. -// -// SPDX-License-Identifier: EUPL-1.2 -// SPDX-FileCopyrightText: 2020-present Detlef Stern -//----------------------------------------------------------------------------- - -package webui - -import ( - "net/http" - - "zettelstore.de/z/usecase" -) - -// MakeGetGoActionHandler creates a new HTTP handler to execute certain commands. -func (wui *WebUI) MakeGetGoActionHandler(ucRefresh *usecase.Refresh) http.HandlerFunc { - return func(w http.ResponseWriter, r *http.Request) { - ctx := r.Context() - - // Currently, command "refresh" is the only command to be executed. - err := ucRefresh.Run(ctx) - if err != nil { - wui.reportError(ctx, w, err) - return - } - wui.redirectFound(w, r, wui.NewURLBuilder('/')) - } -} Index: web/adapter/webui/home.go ================================================================== --- web/adapter/webui/home.go +++ web/adapter/webui/home.go @@ -1,61 +1,56 @@ //----------------------------------------------------------------------------- -// Copyright (c) 2020-present Detlef Stern +// Copyright (c) 2020-2021 Detlef Stern // -// This file is part of Zettelstore. +// This file is part of zettelstore. // // Zettelstore is licensed under the latest version of the EUPL (European Union // Public License). Please see file LICENSE.txt for your rights and obligations // under this license. -// -// SPDX-License-Identifier: EUPL-1.2 -// SPDX-FileCopyrightText: 2020-present Detlef Stern //----------------------------------------------------------------------------- +// Package webui provides web-UI handlers for web requests. package webui import ( "context" "errors" "net/http" "zettelstore.de/z/box" - "zettelstore.de/z/config" - "zettelstore.de/z/web/adapter" - "zettelstore.de/z/web/server" - "zettelstore.de/z/zettel" - "zettelstore.de/z/zettel/id" + "zettelstore.de/z/domain/id" + "zettelstore.de/z/domain/meta" ) type getRootStore interface { - GetZettel(ctx context.Context, zid id.Zid) (zettel.Zettel, error) + // GetMeta retrieves just the meta data of a specific zettel. + GetMeta(ctx context.Context, zid id.Zid) (*meta.Meta, error) } // MakeGetRootHandler creates a new HTTP handler to show the root URL. func (wui *WebUI) MakeGetRootHandler(s getRootStore) http.HandlerFunc { return func(w http.ResponseWriter, r *http.Request) { ctx := r.Context() - if p := r.URL.Path; p != "/" { - wui.reportError(ctx, w, adapter.ErrResourceNotFound{Path: p}) + if r.URL.Path != "/" { + wui.reportError(ctx, w, box.ErrNotFound) return } - homeZid, _ := id.Parse(wui.rtConfig.Get(ctx, nil, config.KeyHomeZettel)) - apiHomeZid := homeZid.ZettelID() + homeZid := wui.rtConfig.GetHomeZettel() if homeZid != id.DefaultHomeZid { - if _, err := s.GetZettel(ctx, homeZid); err == nil { - wui.redirectFound(w, r, wui.NewURLBuilder('h').SetZid(apiHomeZid)) + if _, err := s.GetMeta(ctx, homeZid); err == nil { + redirectFound(w, r, wui.NewURLBuilder('h').SetZid(homeZid)) return } homeZid = id.DefaultHomeZid } - _, err := s.GetZettel(ctx, homeZid) + _, err := s.GetMeta(ctx, homeZid) if err == nil { - wui.redirectFound(w, r, wui.NewURLBuilder('h').SetZid(apiHomeZid)) + redirectFound(w, r, wui.NewURLBuilder('h').SetZid(homeZid)) return } - if errors.Is(err, &box.ErrNotAllowed{}) && wui.authz.WithAuth() && server.GetUser(ctx) == nil { - wui.redirectFound(w, r, wui.NewURLBuilder('i')) + if errors.Is(err, &box.ErrNotAllowed{}) && wui.authz.WithAuth() && wui.getUser(ctx) == nil { + redirectFound(w, r, wui.NewURLBuilder('a')) return } - wui.redirectFound(w, r, wui.NewURLBuilder('h')) + redirectFound(w, r, wui.NewURLBuilder('h')) } } DELETED web/adapter/webui/htmlgen.go Index: web/adapter/webui/htmlgen.go ================================================================== --- web/adapter/webui/htmlgen.go +++ web/adapter/webui/htmlgen.go @@ -1,293 +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. -// -// SPDX-License-Identifier: EUPL-1.2 -// SPDX-FileCopyrightText: 2022-present Detlef Stern -//----------------------------------------------------------------------------- - -package webui - -import ( - "net/url" - "strings" - - "t73f.de/r/sx" - "t73f.de/r/sxhtml" - "t73f.de/r/zsc/api" - "t73f.de/r/zsc/attrs" - "t73f.de/r/zsc/maps" - "t73f.de/r/zsc/shtml" - "t73f.de/r/zsc/sz" - "zettelstore.de/z/ast" - "zettelstore.de/z/encoder" - "zettelstore.de/z/encoder/szenc" - "zettelstore.de/z/strfun" - "zettelstore.de/z/zettel/meta" -) - -// Builder allows to build new URLs for the web service. -type urlBuilder interface { - GetURLPrefix() string - NewURLBuilder(key byte) *api.URLBuilder -} - -type htmlGenerator struct { - tx *szenc.Transformer - th *shtml.Evaluator - lang string - symAt *sx.Symbol -} - -func (wui *WebUI) createGenerator(builder urlBuilder, lang string) *htmlGenerator { - th := shtml.NewEvaluator(1) - - findA := func(obj sx.Object) (attr, assoc, rest *sx.Pair) { - pair, isPair := sx.GetPair(obj) - 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 || !sxhtml.SymAttr.IsEqual(attr.Car()) { - return nil, nil, nil - } - return attr, attr.Tail(), rest.Tail() - } - linkZettel := 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.GetValue(), "#") - u := builder.NewURLBuilder('h').SetZid(api.ZettelID(zid)) - if hasFragment { - u = u.SetFragment(fragment) - } - assoc = assoc.Cons(sx.Cons(shtml.SymAttrHref, sx.MakeString(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 - } - u := builder.NewURLBuilder('/') - assoc = assoc.Cons(sx.Cons(shtml.SymAttrHref, sx.MakeString(u.String()+href.GetValue()[1:]))) - 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(shtml.SymAttrHref) - if hrefP == nil { - return obj - } - href, ok := sx.GetString(hrefP.Cdr()) - if !ok { - return obj - } - ur, err := url.Parse(href.GetValue()) - 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(shtml.SymAttrHref, sx.MakeString(u.String()))) - return rest.Cons(assoc.Cons(sxhtml.SymAttr)).Cons(shtml.SymA) - }) - 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(shtml.SymAttrClass, sx.MakeString("external"))). - Cons(sx.Cons(shtml.SymAttrTarget, sx.MakeString("_blank"))). - Cons(sx.Cons(shtml.SymAttrRel, sx.MakeString("noopener noreferrer"))) - return rest.Cons(assoc.Cons(sxhtml.SymAttr)).Cons(shtml.SymA) - }) - rebind(th, sz.SymEmbed, func(obj sx.Object) sx.Object { - pair, isPair := sx.GetPair(obj) - if !isPair || !shtml.SymIMG.IsEqual(pair.Car()) { - return obj - } - attr, isPair := sx.GetPair(pair.Tail().Car()) - if !isPair || !sxhtml.SymAttr.IsEqual(attr.Car()) { - return obj - } - srcP := attr.Tail().Assoc(shtml.SymAttrSrc) - if srcP == nil { - return obj - } - src, isString := sx.GetString(srcP.Cdr()) - if !isString { - return obj - } - zid := api.ZettelID(src.GetValue()) - if !zid.IsValid() { - return obj - } - u := builder.NewURLBuilder('z').SetZid(zid) - imgAttr := attr.Tail().Cons(sx.Cons(shtml.SymAttrSrc, sx.MakeString(u.String()))).Cons(sxhtml.SymAttr) - return pair.Tail().Tail().Cons(imgAttr).Cons(shtml.SymIMG) - }) - - return &htmlGenerator{ - tx: szenc.NewTransformer(), - th: th, - lang: lang, - } -} - -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() - }) -} - -// SetUnique sets a prefix to make several HTML ids unique. -func (g *htmlGenerator) SetUnique(s string) *htmlGenerator { g.th.SetUnique(s); return g } - -var mapMetaKey = map[string]string{ - api.KeyCopyright: "copyright", - api.KeyLicense: "license", -} - -func (g *htmlGenerator) MetaSxn(m *meta.Meta, evalMeta encoder.EvalMetaFunc) *sx.Pair { - tm := g.tx.GetMeta(m, evalMeta) - env := shtml.MakeEnvironment(g.lang) - hm, err := g.th.Evaluate(tm, &env) - if err != nil { - return nil - } - - ignore := strfun.NewSet(api.KeyTitle, api.KeyLang) - metaMap := make(map[string]*sx.Pair, m.Length()) - if tags, ok := m.Get(api.KeyTags); ok { - metaMap[api.KeyTags] = g.transformMetaTags(tags) - ignore.Set(api.KeyTags) - } - - for elem := hm; elem != nil; elem = elem.Tail() { - mlst, isPair := sx.GetPair(elem.Car()) - if !isPair { - continue - } - att, isPair := sx.GetPair(mlst.Tail().Car()) - if !isPair { - continue - } - if !att.Car().IsEqual(g.symAt) { - continue - } - a := make(attrs.Attributes, 32) - for aelem := att.Tail(); aelem != nil; aelem = aelem.Tail() { - if p, ok := sx.GetPair(aelem.Car()); ok { - key := p.Car() - val := p.Cdr() - if tail, isTail := sx.GetPair(val); isTail { - val = tail.Car() - } - a = a.Set(sz.GoValue(key), sz.GoValue(val)) - } - } - name, found := a.Get("name") - if !found || ignore.Has(name) { - continue - } - - newName, found := mapMetaKey[name] - if !found { - continue - } - a = a.Set("name", newName) - metaMap[newName] = g.th.EvaluateMeta(a) - } - result := sx.Nil() - keys := maps.Keys(metaMap) - for i := len(keys) - 1; i >= 0; i-- { - result = result.Cons(metaMap[keys[i]]) - } - return result -} - -func (g *htmlGenerator) transformMetaTags(tags string) *sx.Pair { - var sb strings.Builder - for i, val := range meta.ListFromValue(tags) { - if i > 0 { - sb.WriteString(", ") - } - sb.WriteString(strings.TrimPrefix(val, "#")) - } - metaTags := sb.String() - if len(metaTags) == 0 { - return nil - } - return g.th.EvaluateMeta(attrs.Attributes{"name": "keywords", "content": metaTags}) -} - -func (g *htmlGenerator) BlocksSxn(bs *ast.BlockSlice) (content, endnotes *sx.Pair, _ error) { - if bs == nil || len(*bs) == 0 { - return nil, nil, nil - } - sx := g.tx.GetSz(bs) - env := shtml.MakeEnvironment(g.lang) - sh, err := g.th.Evaluate(sx, &env) - if err != nil { - return nil, nil, err - } - return sh, g.th.Endnotes(&env), nil -} - -// InlinesSxHTML returns an inline slice, encoded as a SxHTML object. -func (g *htmlGenerator) InlinesSxHTML(is *ast.InlineSlice) *sx.Pair { - if is == nil || len(*is) == 0 { - return nil - } - sx := g.tx.GetSz(is) - env := shtml.MakeEnvironment(g.lang) - sh, err := g.th.Evaluate(sx, &env) - if err != nil { - return nil - } - return sh -} Index: web/adapter/webui/htmlmeta.go ================================================================== --- web/adapter/webui/htmlmeta.go +++ web/adapter/webui/htmlmeta.go @@ -1,174 +1,200 @@ //----------------------------------------------------------------------------- -// Copyright (c) 2020-present Detlef Stern +// Copyright (c) 2020-2021 Detlef Stern // -// This file is part of Zettelstore. +// This file is part of zettelstore. // // Zettelstore is licensed under the latest version of the EUPL (European Union // Public License). Please see file LICENSE.txt for your rights and obligations // under this license. -// -// SPDX-License-Identifier: EUPL-1.2 -// SPDX-FileCopyrightText: 2020-present Detlef Stern //----------------------------------------------------------------------------- +// Package webui provides web-UI handlers for web requests. package webui import ( "context" "errors" - - "t73f.de/r/sx" - "t73f.de/r/sxhtml" - "t73f.de/r/zsc/api" - "t73f.de/r/zsc/shtml" - "zettelstore.de/z/ast" - "zettelstore.de/z/box" - "zettelstore.de/z/parser" - "zettelstore.de/z/usecase" - "zettelstore.de/z/zettel/id" - "zettelstore.de/z/zettel/meta" -) - -func (wui *WebUI) writeHTMLMetaValue( - key, value string, - getTextTitle getTextTitleFunc, - evalMetadata evalMetadataFunc, - gen *htmlGenerator, -) sx.Object { - switch kt := meta.Type(key); kt { - case meta.TypeCredential: - return sx.MakeString(value) - case meta.TypeEmpty: - return sx.MakeString(value) - case meta.TypeID: - return wui.transformIdentifier(value, getTextTitle) - case meta.TypeIDSet: - return wui.transformIdentifierSet(meta.ListFromValue(value), getTextTitle) - case meta.TypeNumber: - return wui.transformKeyValueText(key, value, value) - case meta.TypeString: - return sx.MakeString(value) - case meta.TypeTagSet: - return wui.transformTagSet(key, meta.ListFromValue(value)) - case meta.TypeTimestamp: - if ts, ok := meta.TimeValue(value); ok { - return sx.MakeList( - sx.MakeSymbol("time"), - sx.MakeList( - sxhtml.SymAttr, - sx.Cons(sx.MakeSymbol("datetime"), sx.MakeString(ts.Format("2006-01-02T15:04:05"))), - ), - sx.MakeList(sxhtml.SymNoEscape, sx.MakeString(ts.Format("2006-01-02 15:04:05"))), - ) - } - return sx.Nil() - case meta.TypeURL: - return wui.url2html(sx.MakeString(value)) - case meta.TypeWord: - return wui.transformKeyValueText(key, value, value) - case meta.TypeZettelmarkup: - return wui.transformZmkMetadata(value, evalMetadata, gen) - default: - return sx.MakeList(shtml.SymSTRONG, sx.MakeString("Unhandled type: "), sx.MakeString(kt.Name)) - } -} - -func (wui *WebUI) transformIdentifier(val string, getTextTitle getTextTitleFunc) sx.Object { - text := sx.MakeString(val) - zid, err := id.Parse(val) - if err != nil { - return text - } - title, found := getTextTitle(zid) + "fmt" + "io" + "net/url" + "time" + + "zettelstore.de/z/api" + "zettelstore.de/z/box" + "zettelstore.de/z/domain/id" + "zettelstore.de/z/domain/meta" + "zettelstore.de/z/encoder" + "zettelstore.de/z/parser" + "zettelstore.de/z/strfun" + "zettelstore.de/z/usecase" + "zettelstore.de/z/web/adapter" +) + +var space = []byte{' '} + +func (wui *WebUI) writeHTMLMetaValue(w io.Writer, m *meta.Meta, key string, getTitle getTitleFunc, env *encoder.Environment) { + switch kt := m.Type(key); kt { + case meta.TypeBool: + wui.writeHTMLBool(w, key, m.GetBool(key)) + case meta.TypeCredential: + writeCredential(w, m.GetDefault(key, "???c")) + case meta.TypeEmpty: + writeEmpty(w, m.GetDefault(key, "???e")) + case meta.TypeID: + wui.writeIdentifier(w, m.GetDefault(key, "???i"), getTitle) + case meta.TypeIDSet: + if l, ok := m.GetList(key); ok { + wui.writeIdentifierSet(w, l, getTitle) + } + case meta.TypeNumber: + wui.writeNumber(w, key, m.GetDefault(key, "???n")) + case meta.TypeString: + writeString(w, m.GetDefault(key, "???s")) + case meta.TypeTagSet: + if l, ok := m.GetList(key); ok { + wui.writeTagSet(w, key, l) + } + case meta.TypeTimestamp: + if ts, ok := m.GetTime(key); ok { + writeTimestamp(w, ts) + } + case meta.TypeURL: + writeURL(w, m.GetDefault(key, "???u")) + case meta.TypeWord: + wui.writeWord(w, key, m.GetDefault(key, "???w")) + case meta.TypeWordSet: + if l, ok := m.GetList(key); ok { + wui.writeWordSet(w, key, l) + } + case meta.TypeZettelmarkup: + writeZettelmarkup(w, m.GetDefault(key, "???z"), env) + default: + strfun.HTMLEscape(w, m.GetDefault(key, "???w"), false) + fmt.Fprintf(w, " <b>(Unhandled type: %v, key: %v)</b>", kt, key) + } +} + +func (wui *WebUI) writeHTMLBool(w io.Writer, key string, val bool) { + if val { + wui.writeLink(w, key, "true", "True") + } else { + wui.writeLink(w, key, "false", "False") + } +} + +func writeCredential(w io.Writer, val string) { + strfun.HTMLEscape(w, val, false) +} + +func writeEmpty(w io.Writer, val string) { + strfun.HTMLEscape(w, val, false) +} + +func (wui *WebUI) writeIdentifier(w io.Writer, val string, getTitle getTitleFunc) { + zid, err := id.Parse(val) + if err != nil { + strfun.HTMLEscape(w, val, false) + return + } + title, found := getTitle(zid, api.EncoderText) switch { case found > 0: - ub := wui.NewURLBuilder('h').SetZid(zid.ZettelID()) - attrs := sx.Nil() - if title != "" { - attrs = attrs.Cons(sx.Cons(shtml.SymAttrTitle, sx.MakeString(title))) - } - attrs = attrs.Cons(sx.Cons(shtml.SymAttrHref, sx.MakeString(ub.String()))).Cons(sxhtml.SymAttr) - return sx.Nil().Cons(sx.MakeString(zid.String())).Cons(attrs).Cons(shtml.SymA) - case found == 0: - 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 - } - var space = sx.MakeString(" ") - 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(shtml.SymSPAN) -} - -func (wui *WebUI) transformTagSet(key string, tags []string) *sx.Pair { - if len(tags) == 0 { - return nil - } - var space = sx.MakeString(" ") - 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.MakeString(ub.String())), - ), - sx.MakeString(text), - ) -} - -type evalMetadataFunc = func(string) ast.InlineSlice - -func createEvalMetadataFunc(ctx context.Context, evaluate *usecase.Evaluate) evalMetadataFunc { - return func(value string) ast.InlineSlice { return evaluate.RunMetadata(ctx, value) } -} - -type getTextTitleFunc func(id.Zid) (string, int) - -func (wui *WebUI) makeGetTextTitle(ctx context.Context, getZettel usecase.GetZettel) getTextTitleFunc { - return func(zid id.Zid) (string, int) { - z, err := getZettel.Run(box.NoEnrichContext(ctx), zid) + if title == "" { + fmt.Fprintf(w, "<a href=\"%v\">%v</a>", wui.NewURLBuilder('h').SetZid(zid), zid) + } else { + fmt.Fprintf(w, "<a href=\"%v\" title=\"%v\">%v</a>", wui.NewURLBuilder('h').SetZid(zid), title, zid) + } + case found == 0: + fmt.Fprintf(w, "<s>%v</s>", val) + case found < 0: + io.WriteString(w, val) + } +} + +func (wui *WebUI) writeIdentifierSet(w io.Writer, vals []string, getTitle getTitleFunc) { + for i, val := range vals { + if i > 0 { + w.Write(space) + } + wui.writeIdentifier(w, val, getTitle) + } +} + +func (wui *WebUI) writeNumber(w io.Writer, key, val string) { + wui.writeLink(w, key, val, val) +} + +func writeString(w io.Writer, val string) { + strfun.HTMLEscape(w, val, false) +} + +func (wui *WebUI) writeTagSet(w io.Writer, key string, tags []string) { + for i, tag := range tags { + if i > 0 { + w.Write(space) + } + wui.writeLink(w, key, tag, tag) + } +} + +func writeTimestamp(w io.Writer, ts time.Time) { + io.WriteString(w, ts.Format("2006-01-02 15:04:05")) +} + +func writeURL(w io.Writer, val string) { + u, err := url.Parse(val) + if err != nil { + strfun.HTMLEscape(w, val, false) + return + } + fmt.Fprintf(w, "<a href=\"%v\"%v>", u, htmlAttrNewWindow(true)) + strfun.HTMLEscape(w, val, false) + io.WriteString(w, "</a>") +} + +func (wui *WebUI) writeWord(w io.Writer, key, word string) { + wui.writeLink(w, key, word, word) +} + +func (wui *WebUI) writeWordSet(w io.Writer, key string, words []string) { + for i, word := range words { + if i > 0 { + w.Write(space) + } + wui.writeWord(w, key, word) + } +} +func writeZettelmarkup(w io.Writer, val string, env *encoder.Environment) { + title, err := adapter.FormatInlines(parser.ParseMetadata(val), api.EncoderHTML, env) + if err != nil { + strfun.HTMLEscape(w, val, false) + return + } + io.WriteString(w, title) +} + +func (wui *WebUI) writeLink(w io.Writer, key, value, text string) { + fmt.Fprintf(w, "<a href=\"%v?%v=%v\">", wui.NewURLBuilder('h'), url.QueryEscape(key), url.QueryEscape(value)) + strfun.HTMLEscape(w, text, false) + io.WriteString(w, "</a>") +} + +type getTitleFunc func(id.Zid, api.EncodingEnum) (string, int) + +func makeGetTitle(ctx context.Context, getMeta usecase.GetMeta, env *encoder.Environment) getTitleFunc { + return func(zid id.Zid, format api.EncodingEnum) (string, int) { + m, err := getMeta.Run(box.NoEnrichContext(ctx), zid) if err != nil { if errors.Is(err, &box.ErrNotAllowed{}) { return "", -1 } return "", 0 } - return parser.NormalizedSpacedText(z.Meta.GetTitle()), 1 + astTitle := parser.ParseMetadata(m.GetDefault(meta.KeyTitle, "")) + title, err := adapter.FormatInlines(astTitle, format, env) + if err == nil { + return title, 1 + } + return "", 1 } } - -func (wui *WebUI) transformZmkMetadata(value string, evalMetadata evalMetadataFunc, gen *htmlGenerator) sx.Object { - is := evalMetadata(value) - return gen.InlinesSxHTML(&is).Cons(shtml.SymSPAN) -} Index: web/adapter/webui/lists.go ================================================================== --- web/adapter/webui/lists.go +++ web/adapter/webui/lists.go @@ -1,266 +1,348 @@ //----------------------------------------------------------------------------- -// Copyright (c) 2020-present Detlef Stern +// Copyright (c) 2020-2021 Detlef Stern // -// This file is part of Zettelstore. +// This file is part of zettelstore. // // Zettelstore is licensed under the latest version of the EUPL (European Union // Public License). Please see file LICENSE.txt for your rights and obligations // under this license. -// -// SPDX-License-Identifier: EUPL-1.2 -// SPDX-FileCopyrightText: 2020-present Detlef Stern //----------------------------------------------------------------------------- +// Package webui provides web-UI handlers for web requests. package webui import ( "context" - "io" - "net/http" - "net/url" - "slices" - "strconv" - "strings" - - "t73f.de/r/sx" - "t73f.de/r/sxhtml" - "t73f.de/r/zsc/api" - "t73f.de/r/zsc/shtml" - "zettelstore.de/z/ast" - "zettelstore.de/z/encoding/atom" - "zettelstore.de/z/encoding/rss" - "zettelstore.de/z/encoding/xml" - "zettelstore.de/z/evaluator" - "zettelstore.de/z/query" - "zettelstore.de/z/usecase" - "zettelstore.de/z/web/adapter" - "zettelstore.de/z/web/server" - "zettelstore.de/z/zettel/id" - "zettelstore.de/z/zettel/meta" -) - -// 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) || - wui.handleRoleZettel(w, r, roleZettel, urlQuery) { - return - } - q := adapter.GetQuery(urlQuery) - q = q.SetDeterministic() - ctx := r.Context() - metaSeq, err := queryMeta.Run(ctx, q) - if err != nil { - wui.reportError(ctx, w, err) - return - } - 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", - wui.rtConfig.Get(ctx, nil, api.KeyLang), - wui.rtConfig.GetSiteName(), user) - if q == nil { - rb.bindString("heading", sx.MakeString(wui.rtConfig.GetSiteName())) - } else { - var sb strings.Builder - q.PrintHuman(&sb) - rb.bindString("heading", sx.MakeString(sb.String())) - } - rb.bindString("query-value", sx.MakeString(q.String())) - 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, 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) { - 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 - } - if len(metaSeq) > 0 { - rb.bindString("plain-url", sx.MakeString(apiURL.String())) - rb.bindString("data-url", sx.MakeString(apiURL.AppendKVQuery(api.QueryKeyEncoding, api.EncodingData).String())) - if wui.canCreate(ctx, user) { - rb.bindString("create-url", sx.MakeString(wui.createNewURL)) - rb.bindString("seed", sx.Int64(seed)) - } - } - if rb.err == nil { - err = wui.renderSxnTemplate(ctx, w, id.ListTemplateZid, env) - } else { - err = rb.err - } - if err != nil { - wui.reportError(ctx, w, err) - } - } -} - -func (wui *WebUI) transformTagZettelList(ctx context.Context, tagZettel *usecase.TagZettel, tags []string) (withZettel, withoutZettel *sx.Pair) { - slices.Reverse(tags) - for _, tag := range tags { - tag = meta.NormalizeTag(tag) - if _, err := tagZettel.Run(ctx, tag); err == nil { - u := wui.NewURLBuilder('h').AppendKVQuery(api.QueryKeyTag, tag) - withZettel = wui.prependZettelLink(withZettel, tag, u) - } else { - u := wui.NewURLBuilder('c').SetZid(api.ZidTemplateNewTag).AppendKVQuery(queryKeyAction, valueActionNew).AppendKVQuery(api.KeyTitle, tag) - withoutZettel = wui.prependZettelLink(withoutZettel, tag, u) - } - } - return withZettel, withoutZettel -} - -func (wui *WebUI) transformRoleZettelList(ctx context.Context, roleZettel *usecase.RoleZettel, roles []string) (withZettel, withoutZettel *sx.Pair) { - slices.Reverse(roles) - for _, role := range roles { - if _, err := roleZettel.Run(ctx, role); err == nil { - u := wui.NewURLBuilder('h').AppendKVQuery(api.QueryKeyRole, role) - withZettel = wui.prependZettelLink(withZettel, role, u) - } else { - u := wui.NewURLBuilder('c').SetZid(api.ZidTemplateNewRole).AppendKVQuery(queryKeyAction, valueActionNew).AppendKVQuery(api.KeyTitle, role) - withoutZettel = wui.prependZettelLink(withoutZettel, role, u) - } - } - return withZettel, withoutZettel -} - -func (wui *WebUI) prependZettelLink(sxZtl *sx.Pair, name string, u *api.URLBuilder) *sx.Pair { - link := sx.MakeList( - shtml.SymA, - sx.MakeList( - sxhtml.SymAttr, - sx.Cons(shtml.SymAttrHref, sx.MakeString(u.String())), - ), - sx.MakeString(name), - ) - if sxZtl != nil { - sxZtl = sxZtl.Cons(sx.MakeString(", ")) - } - return sxZtl.Cons(link) -} - -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] == api.TitleAction { - rssConfig.Title = strings.Join(actions[2:], " ") - } - data := rssConfig.Marshal(q, ml) - - adapter.PrepareHeader(w, rss.ContentType) - w.WriteHeader(http.StatusOK) - var err error - if _, err = io.WriteString(w, xml.Header); err == nil { - _, err = w.Write(data) - } - if err != nil { - wui.log.Error().Err(err).Msg("unable to write RSS data") - } -} - -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] == api.TitleAction { - atomConfig.Title = strings.Join(actions[2:], " ") - } - data := atomConfig.Marshal(q, ml) - - adapter.PrepareHeader(w, atom.ContentType) - w.WriteHeader(http.StatusOK) - var err error - if _, err = io.WriteString(w, xml.Header); err == nil { - _, err = w.Write(data) - } - if err != nil { - wui.log.Error().Err(err).Msg("unable to write Atom data") - } -} - -func (wui *WebUI) handleTagZettel(w http.ResponseWriter, r *http.Request, tagZettel *usecase.TagZettel, vals url.Values) bool { - tag := vals.Get(api.QueryKeyTag) - if tag == "" { - return false - } - ctx := r.Context() - z, err := tagZettel.Run(ctx, tag) - if err != nil { - wui.reportError(ctx, w, err) - return true - } - 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) - if role == "" { - return false - } - ctx := r.Context() - z, err := roleZettel.Run(ctx, role) - if err != nil { - wui.reportError(ctx, w, err) - return true - } - wui.redirectFound(w, r, wui.NewURLBuilder('h').SetZid(z.Meta.Zid.ZettelID())) - return true + "net/http" + "net/url" + "sort" + "strconv" + "strings" + + "zettelstore.de/z/api" + "zettelstore.de/z/box" + "zettelstore.de/z/domain/id" + "zettelstore.de/z/domain/meta" + "zettelstore.de/z/encoder" + "zettelstore.de/z/parser" + "zettelstore.de/z/search" + "zettelstore.de/z/usecase" + "zettelstore.de/z/web/adapter" +) + +// MakeListHTMLMetaHandler creates a HTTP handler for rendering the list of +// zettel as HTML. +func (wui *WebUI) MakeListHTMLMetaHandler( + listMeta usecase.ListMeta, + listRole usecase.ListRole, + listTags usecase.ListTags, +) http.HandlerFunc { + return func(w http.ResponseWriter, r *http.Request) { + query := r.URL.Query() + switch query.Get("_l") { + case "r": + wui.renderRolesList(w, r, listRole) + case "t": + wui.renderTagsList(w, r, listTags) + default: + wui.renderZettelList(w, r, listMeta) + } + } +} + +func (wui *WebUI) renderZettelList(w http.ResponseWriter, r *http.Request, listMeta usecase.ListMeta) { + query := r.URL.Query() + s := adapter.GetSearch(query, false) + ctx := r.Context() + title := wui.listTitleSearch("Select", s) + wui.renderMetaList( + ctx, w, title, s, + func(s *search.Search) ([]*meta.Meta, error) { + if !s.HasComputedMetaKey() { + ctx = box.NoEnrichContext(ctx) + } + return listMeta.Run(ctx, s) + }, + func(offset int) string { + return wui.newPageURL('h', query, offset, "_offset", "_limit") + }) +} + +type roleInfo struct { + Text string + URL string +} + +func (wui *WebUI) renderRolesList(w http.ResponseWriter, r *http.Request, listRole usecase.ListRole) { + ctx := r.Context() + roleList, err := listRole.Run(ctx) + if err != nil { + adapter.ReportUsecaseError(w, err) + return + } + + roleInfos := make([]roleInfo, 0, len(roleList)) + for _, role := range roleList { + roleInfos = append( + roleInfos, + roleInfo{role, wui.NewURLBuilder('h').AppendQuery("role", role).String()}) + } + + user := wui.getUser(ctx) + var base baseData + wui.makeBaseData(ctx, wui.rtConfig.GetDefaultLang(), wui.rtConfig.GetSiteName(), user, &base) + wui.renderTemplate(ctx, w, id.RolesTemplateZid, &base, struct { + Roles []roleInfo + }{ + Roles: roleInfos, + }) +} + +type countInfo struct { + Count string + URL string +} + +type tagInfo struct { + Name string + URL string + count int + Count string + Size string +} + +var fontSizes = [...]int{75, 83, 100, 117, 150, 200} + +func (wui *WebUI) renderTagsList(w http.ResponseWriter, r *http.Request, listTags usecase.ListTags) { + ctx := r.Context() + iMinCount, _ := strconv.Atoi(r.URL.Query().Get("min")) + tagData, err := listTags.Run(ctx, iMinCount) + if err != nil { + wui.reportError(ctx, w, err) + return + } + + user := wui.getUser(ctx) + tagsList := make([]tagInfo, 0, len(tagData)) + countMap := make(map[int]int) + baseTagListURL := wui.NewURLBuilder('h') + for tag, ml := range tagData { + count := len(ml) + countMap[count]++ + tagsList = append( + tagsList, + tagInfo{tag, baseTagListURL.AppendQuery("tags", tag).String(), count, "", ""}) + baseTagListURL.ClearQuery() + } + sort.Slice(tagsList, func(i, j int) bool { return tagsList[i].Name < tagsList[j].Name }) + + countList := make([]int, 0, len(countMap)) + for count := range countMap { + countList = append(countList, count) + } + sort.Ints(countList) + for pos, count := range countList { + countMap[count] = fontSizes[(pos*len(fontSizes))/len(countList)] + } + for i := 0; i < len(tagsList); i++ { + count := tagsList[i].count + tagsList[i].Count = strconv.Itoa(count) + tagsList[i].Size = strconv.Itoa(countMap[count]) + } + + var base baseData + wui.makeBaseData(ctx, wui.rtConfig.GetDefaultLang(), wui.rtConfig.GetSiteName(), user, &base) + minCounts := make([]countInfo, 0, len(countList)) + for _, c := range countList { + sCount := strconv.Itoa(c) + minCounts = append(minCounts, countInfo{sCount, base.ListTagsURL + "&min=" + sCount}) + } + + wui.renderTemplate(ctx, w, id.TagsTemplateZid, &base, struct { + ListTagsURL string + MinCounts []countInfo + Tags []tagInfo + }{ + ListTagsURL: base.ListTagsURL, + MinCounts: minCounts, + Tags: tagsList, + }) +} + +// MakeSearchHandler creates a new HTTP handler for the use case "search". +func (wui *WebUI) MakeSearchHandler( + ucSearch usecase.Search, + getMeta usecase.GetMeta, + getZettel usecase.GetZettel, +) http.HandlerFunc { + return func(w http.ResponseWriter, r *http.Request) { + query := r.URL.Query() + ctx := r.Context() + s := adapter.GetSearch(query, true) + if s == nil { + redirectFound(w, r, wui.NewURLBuilder('h')) + return + } + + title := wui.listTitleSearch("Search", s) + wui.renderMetaList( + ctx, w, title, s, func(s *search.Search) ([]*meta.Meta, error) { + if !s.HasComputedMetaKey() { + ctx = box.NoEnrichContext(ctx) + } + return ucSearch.Run(ctx, s) + }, + func(offset int) string { + return wui.newPageURL('f', query, offset, "offset", "limit") + }) + } +} + +// MakeZettelContextHandler creates a new HTTP handler for the use case "zettel context". +func (wui *WebUI) MakeZettelContextHandler(getContext usecase.ZettelContext) http.HandlerFunc { + return func(w http.ResponseWriter, r *http.Request) { + ctx := r.Context() + zid, err := id.Parse(r.URL.Path[1:]) + if err != nil { + wui.reportError(ctx, w, box.ErrNotFound) + return + } + q := r.URL.Query() + dir := adapter.GetZCDirection(q.Get(api.QueryKeyDir)) + depth := getIntParameter(q, api.QueryKeyDepth, 5) + limit := getIntParameter(q, api.QueryKeyLimit, 200) + metaList, err := getContext.Run(ctx, zid, dir, depth, limit) + if err != nil { + wui.reportError(ctx, w, err) + return + } + metaLinks, err := wui.buildHTMLMetaList(metaList) + if err != nil { + adapter.InternalServerError(w, "Build HTML meta list", err) + return + } + + depths := []string{"2", "3", "4", "5", "6", "7", "8", "9", "10"} + depthLinks := make([]simpleLink, len(depths)) + depthURL := wui.NewURLBuilder('j').SetZid(zid) + for i, depth := range depths { + depthURL.ClearQuery() + switch dir { + case usecase.ZettelContextBackward: + depthURL.AppendQuery(api.QueryKeyDir, api.DirBackward) + case usecase.ZettelContextForward: + depthURL.AppendQuery(api.QueryKeyDir, api.DirForward) + } + depthURL.AppendQuery(api.QueryKeyDepth, depth) + depthLinks[i].Text = depth + depthLinks[i].URL = depthURL.String() + } + var base baseData + user := wui.getUser(ctx) + wui.makeBaseData(ctx, wui.rtConfig.GetDefaultLang(), wui.rtConfig.GetSiteName(), user, &base) + wui.renderTemplate(ctx, w, id.ContextTemplateZid, &base, struct { + Title string + InfoURL string + Depths []simpleLink + Start simpleLink + Metas []simpleLink + }{ + Title: "Zettel Context", + InfoURL: wui.NewURLBuilder('i').SetZid(zid).String(), + Depths: depthLinks, + Start: metaLinks[0], + Metas: metaLinks[1:], + }) + } +} + +func getIntParameter(q url.Values, key string, minValue int) int { + val, ok := adapter.GetInteger(q, key) + if !ok || val < 0 { + return minValue + } + return val +} + +func (wui *WebUI) renderMetaList( + ctx context.Context, + w http.ResponseWriter, + title string, + s *search.Search, + ucMetaList func(sorter *search.Search) ([]*meta.Meta, error), + pageURL func(int) string) { + + metaList, err := ucMetaList(s) + if err != nil { + wui.reportError(ctx, w, err) + return + } + user := wui.getUser(ctx) + metas, err := wui.buildHTMLMetaList(metaList) + if err != nil { + wui.reportError(ctx, w, err) + return + } + var base baseData + wui.makeBaseData(ctx, wui.rtConfig.GetDefaultLang(), wui.rtConfig.GetSiteName(), user, &base) + wui.renderTemplate(ctx, w, id.ListTemplateZid, &base, struct { + Title string + Metas []simpleLink + }{ + Title: title, + Metas: metas, + }) +} + +func (wui *WebUI) listTitleSearch(prefix string, s *search.Search) string { + if s == nil { + return wui.rtConfig.GetSiteName() + } + var sb strings.Builder + sb.WriteString(prefix) + if s != nil { + sb.WriteString(": ") + s.Print(&sb) + } + return sb.String() +} + +func (wui *WebUI) newPageURL(key byte, query url.Values, offset int, offsetKey, limitKey string) string { + ub := wui.NewURLBuilder(key) + for key, values := range query { + if key != offsetKey && key != limitKey { + for _, val := range values { + ub.AppendQuery(key, val) + } + } + } + if offset > 0 { + ub.AppendQuery(offsetKey, strconv.Itoa(offset)) + } + return ub.String() +} + +// buildHTMLMetaList builds a zettel list based on a meta list for HTML rendering. +func (wui *WebUI) buildHTMLMetaList(metaList []*meta.Meta) ([]simpleLink, error) { + defaultLang := wui.rtConfig.GetDefaultLang() + metas := make([]simpleLink, 0, len(metaList)) + for _, m := range metaList { + var lang string + if val, ok := m.Get(meta.KeyLang); ok { + lang = val + } else { + lang = defaultLang + } + title, _ := m.Get(meta.KeyTitle) + env := encoder.Environment{Lang: lang, Interactive: true} + htmlTitle, err := adapter.FormatInlines(parser.ParseMetadata(title), api.EncoderHTML, &env) + if err != nil { + return nil, err + } + metas = append(metas, simpleLink{ + Text: htmlTitle, + URL: wui.NewURLBuilder('h').SetZid(m.Zid).String(), + }) + } + return metas, nil } Index: web/adapter/webui/login.go ================================================================== --- web/adapter/webui/login.go +++ web/adapter/webui/login.go @@ -1,71 +1,61 @@ //----------------------------------------------------------------------------- -// Copyright (c) 2020-present Detlef Stern +// Copyright (c) 2020-2021 Detlef Stern // -// This file is part of Zettelstore. +// This file is part of zettelstore. // // Zettelstore is licensed under the latest version of the EUPL (European Union // Public License). Please see file LICENSE.txt for your rights and obligations // under this license. -// -// SPDX-License-Identifier: EUPL-1.2 -// SPDX-FileCopyrightText: 2020-present Detlef Stern //----------------------------------------------------------------------------- +// Package webui provides web-UI handlers for web requests. package webui import ( "context" "net/http" - "t73f.de/r/sx" - "t73f.de/r/zsc/api" - "zettelstore.de/z/auth" - "zettelstore.de/z/usecase" - "zettelstore.de/z/web/adapter" - "zettelstore.de/z/zettel/id" -) - -// MakeGetLoginOutHandler creates a new HTTP handler to display the HTML login view, -// or to execute a logout. -func (wui *WebUI) MakeGetLoginOutHandler() http.HandlerFunc { - return func(w http.ResponseWriter, r *http.Request) { - query := r.URL.Query() - if query.Has("logout") { - wui.clearToken(r.Context(), w) - wui.redirectFound(w, r, wui.NewURLBuilder('/')) - return - } + "zettelstore.de/z/auth" + "zettelstore.de/z/domain/id" + "zettelstore.de/z/usecase" + "zettelstore.de/z/web/adapter" +) + +// MakeGetLoginHandler creates a new HTTP handler to display the HTML login view. +func (wui *WebUI) MakeGetLoginHandler() http.HandlerFunc { + return func(w http.ResponseWriter, r *http.Request) { wui.renderLoginForm(wui.clearToken(r.Context(), w), w, false) } } func (wui *WebUI) renderLoginForm(ctx context.Context, w http.ResponseWriter, retry bool) { - env, rb := wui.createRenderEnv(ctx, "login", wui.rtConfig.Get(ctx, nil, api.KeyLang), "Login", nil) - rb.bindString("retry", sx.MakeBoolean(retry)) - if rb.err == nil { - rb.err = wui.renderSxnTemplate(ctx, w, id.LoginTemplateZid, env) - } - if err := rb.err; err != nil { - wui.reportError(ctx, w, err) - } + var base baseData + wui.makeBaseData(ctx, wui.rtConfig.GetDefaultLang(), "Login", nil, &base) + wui.renderTemplate(ctx, w, id.LoginTemplateZid, &base, struct { + Title string + Retry bool + }{ + Title: base.Title, + Retry: retry, + }) } // MakePostLoginHandler creates a new HTTP handler to authenticate the given user. -func (wui *WebUI) MakePostLoginHandler(ucAuth *usecase.Authenticate) http.HandlerFunc { +func (wui *WebUI) MakePostLoginHandler(ucAuth usecase.Authenticate) http.HandlerFunc { return func(w http.ResponseWriter, r *http.Request) { if !wui.authz.WithAuth() { - wui.redirectFound(w, r, wui.NewURLBuilder('/')) + redirectFound(w, r, wui.NewURLBuilder('/')) return } ctx := r.Context() ident, cred, ok := adapter.GetCredentialsViaForm(r) if !ok { wui.reportError(ctx, w, adapter.NewErrBadRequest("Unable to read login form")) return } - token, err := ucAuth.Run(ctx, r, ident, cred, wui.tokenLifetime, auth.KindwebUI) + token, err := ucAuth.Run(ctx, ident, cred, wui.tokenLifetime, auth.KindHTML) if err != nil { wui.reportError(ctx, w, err) return } if token == nil { @@ -72,8 +62,16 @@ wui.renderLoginForm(wui.clearToken(ctx, w), w, true) return } wui.setToken(w, token) - wui.redirectFound(w, r, wui.NewURLBuilder('/')) + redirectFound(w, r, wui.NewURLBuilder('/')) + } +} + +// MakeGetLogoutHandler creates a new HTTP handler to log out the current user +func (wui *WebUI) MakeGetLogoutHandler() http.HandlerFunc { + return func(w http.ResponseWriter, r *http.Request) { + wui.clearToken(r.Context(), w) + redirectFound(w, r, wui.NewURLBuilder('/')) } } DELETED web/adapter/webui/meta.go Index: web/adapter/webui/meta.go ================================================================== --- web/adapter/webui/meta.go +++ web/adapter/webui/meta.go @@ -1,56 +0,0 @@ -//----------------------------------------------------------------------------- -// 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": "", -} DELETED web/adapter/webui/meta_test.go Index: web/adapter/webui/meta_test.go ================================================================== --- web/adapter/webui/meta_test.go +++ web/adapter/webui/meta_test.go @@ -1,45 +0,0 @@ -//----------------------------------------------------------------------------- -// 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 @@ -1,105 +1,95 @@ //----------------------------------------------------------------------------- -// Copyright (c) 2020-present Detlef Stern +// Copyright (c) 2020-2021 Detlef Stern // -// This file is part of Zettelstore. +// This file is part of zettelstore. // // Zettelstore is licensed under the latest version of the EUPL (European Union // Public License). Please see file LICENSE.txt for your rights and obligations // under this license. -// -// SPDX-License-Identifier: EUPL-1.2 -// SPDX-FileCopyrightText: 2020-present Detlef Stern //----------------------------------------------------------------------------- +// Package webui provides web-UI handlers for web requests. package webui import ( "fmt" "net/http" "strings" - "t73f.de/r/zsc/api" - "zettelstore.de/z/box" - "zettelstore.de/z/usecase" - "zettelstore.de/z/web/adapter" - "zettelstore.de/z/web/server" - "zettelstore.de/z/zettel/id" -) - -// MakeGetRenameZettelHandler creates a new HTTP handler to display the -// HTML rename view of a zettel. -func (wui *WebUI) MakeGetRenameZettelHandler(getZettel usecase.GetZettel) http.HandlerFunc { - return func(w http.ResponseWriter, r *http.Request) { - ctx := r.Context() - path := r.URL.Path[1:] - zid, err := id.Parse(path) - if err != nil { - wui.reportError(ctx, w, box.ErrInvalidZid{Zid: path}) - return - } - - z, err := getZettel.Run(ctx, zid) - if err != nil { - wui.reportError(ctx, w, err) - return - } - m := z.Meta - - user := server.GetUser(ctx) - env, rb := wui.createRenderEnv( - ctx, "rename", - wui.rtConfig.Get(ctx, nil, api.KeyLang), "Rename Zettel "+m.Zid.String(), user) - rb.bindString("incoming", wui.encodeIncoming(m, wui.makeGetTextTitle(ctx, getZettel))) - wui.bindCommonZettelData(ctx, &rb, user, m, nil) - if rb.err == nil { - err = wui.renderSxnTemplate(ctx, w, id.RenameTemplateZid, env) - } else { - err = rb.err - } - if err != nil { - wui.reportError(ctx, w, err) - } + "zettelstore.de/z/api" + "zettelstore.de/z/box" + "zettelstore.de/z/config" + "zettelstore.de/z/domain/id" + "zettelstore.de/z/domain/meta" + "zettelstore.de/z/usecase" + "zettelstore.de/z/web/adapter" +) + +// MakeGetRenameZettelHandler creates a new HTTP handler to display the +// HTML rename view of a zettel. +func (wui *WebUI) MakeGetRenameZettelHandler(getMeta usecase.GetMeta) http.HandlerFunc { + return func(w http.ResponseWriter, r *http.Request) { + ctx := r.Context() + zid, err := id.Parse(r.URL.Path[1:]) + if err != nil { + wui.reportError(ctx, w, box.ErrNotFound) + return + } + + m, err := getMeta.Run(ctx, zid) + if err != nil { + wui.reportError(ctx, w, err) + return + } + + if format, formatText := adapter.GetFormat(r, r.URL.Query(), api.EncoderHTML); format != api.EncoderHTML { + wui.reportError(ctx, w, adapter.NewErrBadRequest( + fmt.Sprintf("Rename zettel %q not possible in format %q", zid.String(), formatText))) + return + } + + user := wui.getUser(ctx) + var base baseData + wui.makeBaseData(ctx, config.GetLang(m, wui.rtConfig), "Rename Zettel "+zid.String(), user, &base) + wui.renderTemplate(ctx, w, id.RenameTemplateZid, &base, struct { + Zid string + MetaPairs []meta.Pair + }{ + Zid: zid.String(), + MetaPairs: m.Pairs(true), + }) } } // MakePostRenameZettelHandler creates a new HTTP handler to rename an existing zettel. -func (wui *WebUI) MakePostRenameZettelHandler(renameZettel *usecase.RenameZettel) http.HandlerFunc { +func (wui *WebUI) MakePostRenameZettelHandler(renameZettel usecase.RenameZettel) http.HandlerFunc { return func(w http.ResponseWriter, r *http.Request) { ctx := r.Context() - path := r.URL.Path[1:] - curZid, err := id.Parse(path) + curZid, err := id.Parse(r.URL.Path[1:]) if err != nil { - wui.reportError(ctx, w, box.ErrInvalidZid{Zid: path}) + wui.reportError(ctx, w, box.ErrNotFound) return } if err = r.ParseForm(); err != nil { - wui.log.Trace().Err(err).Msg("unable to read rename zettel form") wui.reportError(ctx, w, adapter.NewErrBadRequest("Unable to read rename zettel form")) return } - formCurZidStr := r.PostFormValue("curzid") - if formCurZid, err1 := id.Parse(formCurZidStr); err1 != nil || formCurZid != curZid { - if err1 != nil { - wui.log.Trace().Str("formCurzid", formCurZidStr).Err(err1).Msg("unable to parse as zid") - } else if formCurZid != curZid { - wui.log.Trace().Zid(formCurZid).Zid(curZid).Msg("zid differ (form/url)") - } + if formCurZid, err1 := id.Parse( + r.PostFormValue("curzid")); err1 != nil || formCurZid != curZid { wui.reportError(ctx, w, adapter.NewErrBadRequest("Invalid value for current zettel id in form")) return } - formNewZid := strings.TrimSpace(r.PostFormValue("newzid")) - newZid, err := id.Parse(formNewZid) + newZid, err := id.Parse(strings.TrimSpace(r.PostFormValue("newzid"))) if err != nil { - wui.reportError( - ctx, w, adapter.NewErrBadRequest(fmt.Sprintf("Invalid new zettel id %q", formNewZid))) + wui.reportError(ctx, w, adapter.NewErrBadRequest(fmt.Sprintf("Invalid new zettel id %q", newZid))) return } - if err = renameZettel.Run(r.Context(), curZid, newZid); err != nil { + if err := renameZettel.Run(r.Context(), curZid, newZid); err != nil { wui.reportError(ctx, w, err) return } - wui.redirectFound(w, r, wui.NewURLBuilder('h').SetZid(newZid.ZettelID())) + redirectFound(w, r, wui.NewURLBuilder('h').SetZid(newZid)) } } Index: web/adapter/webui/response.go ================================================================== --- web/adapter/webui/response.go +++ web/adapter/webui/response.go @@ -1,26 +1,22 @@ //----------------------------------------------------------------------------- -// Copyright (c) 2021-present Detlef Stern +// Copyright (c) 2021 Detlef Stern // -// This file is part of Zettelstore. +// This file is part of zettelstore. // // Zettelstore is licensed under the latest version of the EUPL (European Union // Public License). Please see file LICENSE.txt for your rights and obligations // under this license. -// -// SPDX-License-Identifier: EUPL-1.2 -// SPDX-FileCopyrightText: 2021-present Detlef Stern //----------------------------------------------------------------------------- +// Package webui provides web-UI handlers for web requests. package webui import ( "net/http" - "t73f.de/r/zsc/api" + "zettelstore.de/z/api" ) -func (wui *WebUI) redirectFound(w http.ResponseWriter, r *http.Request, ub *api.URLBuilder) { - us := ub.String() - wui.log.Debug().Str("uri", us).Msg("redirect") - http.Redirect(w, r, us, http.StatusFound) +func redirectFound(w http.ResponseWriter, r *http.Request, ub *api.URLBuilder) { + http.Redirect(w, r, ub.String(), http.StatusFound) } DELETED web/adapter/webui/sxn_code.go Index: web/adapter/webui/sxn_code.go ================================================================== --- web/adapter/webui/sxn_code.go +++ web/adapter/webui/sxn_code.go @@ -1,111 +0,0 @@ -//----------------------------------------------------------------------------- -// 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 webui - -import ( - "context" - "fmt" - "io" - - "t73f.de/r/sx/sxeval" - "t73f.de/r/zsc/api" - "zettelstore.de/z/zettel/id" - "zettelstore.de/z/zettel/meta" -) - -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 { - return nil, err - } - return z.Meta, nil - } - dg := buildSxnCodeDigraph(ctx, id.StartSxnZid, getMeta) - if dg == 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) - } - bind := wui.rootBinding.MakeChildBinding("zettel", 128) - for _, zid := range dg.SortReverse() { - if err := wui.loadSxnCodeZettel(ctx, zid, bind); err != nil { - return nil, nil, err - } - } - 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 { - m, err := getMeta(ctx, startZid) - if err != nil { - return nil - } - var marked id.Set - stack := []*meta.Meta{m} - dg := id.Digraph(nil).AddVertex(startZid) - for pos := len(stack) - 1; pos >= 0; pos = len(stack) - 1 { - curr := stack[pos] - stack = stack[:pos] - if marked.Contains(curr.Zid) { - continue - } - marked = marked.Add(curr.Zid) - if precursors, hasPrecursor := curr.GetList(api.KeyPrecursor); hasPrecursor && len(precursors) > 0 { - for _, pre := range precursors { - if preZid, errParse := id.Parse(pre); errParse == nil { - m, err = getMeta(ctx, preZid) - if err != nil { - continue - } - stack = append(stack, m) - dg.AddVertex(preZid) - dg.AddEdge(curr.Zid, preZid) - } - } - } - } - return dg -} - -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.String()).Msg("Loaded sxn code") - - if _, err2 = env.Eval(form); err2 != nil { - return err2 - } - } -} DELETED web/adapter/webui/template.go Index: web/adapter/webui/template.go ================================================================== --- web/adapter/webui/template.go +++ web/adapter/webui/template.go @@ -1,457 +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. -// -// SPDX-License-Identifier: EUPL-1.2 -// SPDX-FileCopyrightText: 2020-present Detlef Stern -//----------------------------------------------------------------------------- - -package webui - -import ( - "bytes" - "context" - "fmt" - "net/http" - "net/url" - - "t73f.de/r/sx" - "t73f.de/r/sx/sxbuiltins" - "t73f.de/r/sx/sxeval" - "t73f.de/r/sx/sxreader" - "t73f.de/r/sxhtml" - "t73f.de/r/zsc/api" - "t73f.de/r/zsc/shtml" - "zettelstore.de/z/box" - "zettelstore.de/z/collect" - "zettelstore.de/z/config" - "zettelstore.de/z/parser" - "zettelstore.de/z/web/adapter" - "zettelstore.de/z/web/server" - "zettelstore.de/z/zettel" - "zettelstore.de/z/zettel/id" - "zettelstore.de/z/zettel/meta" -) - -func (wui *WebUI) createRenderBinding() *sxeval.Binding { - root := sxeval.MakeRootBinding(len(specials) + len(builtins) + 3) - for _, syntax := range specials { - root.BindSpecial(syntax) - } - for _, b := range builtins { - root.BindBuiltin(b) - } - _ = root.Bind(sx.MakeSymbol("NIL"), sx.Nil()) - _ = root.Bind(sx.MakeSymbol("T"), sx.MakeSymbol("T")) - root.BindBuiltin(&sxeval.Builtin{ - Name: "url-to-html", - MinArity: 1, - MaxArity: 1, - TestPure: sxeval.AssertPure, - Fn1: func(_ *sxeval.Environment, arg sx.Object) (sx.Object, error) { - text, err := sxbuiltins.GetString(arg, 0) - if err != nil { - return nil, err - } - return wui.url2html(text), nil - }, - }) - root.BindBuiltin(&sxeval.Builtin{ - Name: "zid-content-path", - MinArity: 1, - MaxArity: 1, - TestPure: sxeval.AssertPure, - Fn1: func(_ *sxeval.Environment, arg sx.Object) (sx.Object, error) { - s, err := sxbuiltins.GetString(arg, 0) - if err != nil { - return nil, err - } - zid, err := id.Parse(s.GetValue()) - if err != nil { - return nil, fmt.Errorf("parsing zettel identifier %q: %w", s.GetValue(), err) - } - ub := wui.NewURLBuilder('z').SetZid(zid.ZettelID()) - return sx.MakeString(ub.String()), nil - }, - }) - root.BindBuiltin(&sxeval.Builtin{ - Name: "query->url", - MinArity: 1, - MaxArity: 1, - TestPure: sxeval.AssertPure, - Fn1: func(_ *sxeval.Environment, arg sx.Object) (sx.Object, error) { - qs, err := sxbuiltins.GetString(arg, 0) - if err != nil { - return nil, err - } - u := wui.NewURLBuilder('h').AppendQuery(qs.GetValue()) - return sx.MakeString(u.String()), nil - }, - }) - root.Freeze() - return root -} - -var ( - specials = []*sxeval.Special{ - &sxbuiltins.QuoteS, &sxbuiltins.QuasiquoteS, // quote, quasiquote - &sxbuiltins.UnquoteS, &sxbuiltins.UnquoteSplicingS, // unquote, unquote-splicing - &sxbuiltins.DefVarS, // defvar - &sxbuiltins.DefunS, &sxbuiltins.LambdaS, // defun, lambda - &sxbuiltins.SetXS, // set! - &sxbuiltins.IfS, // if - &sxbuiltins.BeginS, // begin - &sxbuiltins.DefMacroS, // defmacro - &sxbuiltins.LetS, // let - } - builtins = []*sxeval.Builtin{ - &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.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.GetValue()); errURL == nil { - if us := u.String(); us != "" { - return sx.MakeList( - shtml.SymA, - sx.MakeList( - sxhtml.SymAttr, - sx.Cons(shtml.SymAttrHref, sx.MakeString(us)), - sx.Cons(shtml.SymAttrTarget, sx.MakeString("_blank")), - sx.Cons(shtml.SymAttrRel, sx.MakeString("noopener noreferrer")), - ), - text) - } - } - return text -} - -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.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.Binding, renderBinder) { - userIsValid, userZettelURL, userIdent := wui.getUserRenderData(user) - parentEnv, err := wui.getParentEnv(ctx) - bind := parentEnv.MakeChildBinding(name, 128) - rb := makeRenderBinder(bind, err) - rb.bindString("lang", sx.MakeString(lang)) - rb.bindString("css-base-url", sx.MakeString(wui.cssBaseURL)) - rb.bindString("css-user-url", sx.MakeString(wui.cssUserURL)) - rb.bindString("title", sx.MakeString(title)) - rb.bindString("home-url", sx.MakeString(wui.homeURL)) - rb.bindString("with-auth", sx.MakeBoolean(wui.withAuth)) - rb.bindString("user-is-valid", sx.MakeBoolean(userIsValid)) - rb.bindString("user-zettel-url", sx.MakeString(userZettelURL)) - rb.bindString("user-ident", sx.MakeString(userIdent)) - rb.bindString("login-url", sx.MakeString(wui.loginURL)) - rb.bindString("logout-url", sx.MakeString(wui.logoutURL)) - rb.bindString("list-zettel-url", sx.MakeString(wui.listZettelURL)) - rb.bindString("list-roles-url", sx.MakeString(wui.listRolesURL)) - rb.bindString("list-tags-url", sx.MakeString(wui.listTagsURL)) - if wui.canRefresh(user) { - rb.bindString("refresh-url", sx.MakeString(wui.refreshURL)) - } - rb.bindString("new-zettel-links", wui.fetchNewTemplatesSxn(ctx, user)) - rb.bindString("search-url", sx.MakeString(wui.searchURL)) - rb.bindString("query-key-query", sx.MakeString(api.QueryKeyQuery)) - rb.bindString("query-key-seed", sx.MakeString(api.QueryKeySeed)) - rb.bindString("FOOTER", wui.calculateFooterSxn(ctx)) // TODO: use real footer - rb.bindString("debug-mode", sx.MakeBoolean(wui.debug)) - 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(user.Zid.ZettelID()).String(), user.GetDefault(api.KeyUserID, "") -} - -type renderBinder struct { - err error - binding *sxeval.Binding -} - -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 { - 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.binding.Bind(sym, obj) - } -} -func (rb *renderBinder) bindKeyValue(key string, value string) { - rb.bindString("meta-"+key, sx.MakeString(value)) - if kt := meta.Type(key); kt.IsSet { - rb.bindString("set-meta-"+key, makeStringList(meta.ListFromValue(value))) - } -} -func (rb *renderBinder) rebindResolved(key, defKey string) { - if rb.err == nil { - 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() - apiZid := api.ZettelID(strZid) - newURLBuilder := wui.NewURLBuilder - - rb.bindString("zid", sx.MakeString(strZid)) - rb.bindString("web-url", sx.MakeString(newURLBuilder('h').SetZid(apiZid).String())) - if content != nil && wui.canWrite(ctx, user, m, *content) { - rb.bindString("edit-url", sx.MakeString(newURLBuilder('e').SetZid(apiZid).String())) - } - rb.bindString("info-url", sx.MakeString(newURLBuilder('i').SetZid(apiZid).String())) - if wui.canCreate(ctx, user) { - if content != nil && !content.IsBinary() { - rb.bindString("copy-url", sx.MakeString(newURLBuilder('c').SetZid(apiZid).AppendKVQuery(queryKeyAction, valueActionCopy).String())) - } - rb.bindString("version-url", sx.MakeString(newURLBuilder('c').SetZid(apiZid).AppendKVQuery(queryKeyAction, valueActionVersion).String())) - rb.bindString("child-url", sx.MakeString(newURLBuilder('c').SetZid(apiZid).AppendKVQuery(queryKeyAction, valueActionChild).String())) - rb.bindString("folge-url", sx.MakeString(newURLBuilder('c').SetZid(apiZid).AppendKVQuery(queryKeyAction, valueActionFolge).String())) - } - if wui.canRename(ctx, user, m) { - rb.bindString("rename-url", sx.MakeString(newURLBuilder('b').SetZid(apiZid).String())) - } - if wui.canDelete(ctx, user, m) { - rb.bindString("delete-url", sx.MakeString(newURLBuilder('d').SetZid(apiZid).String())) - } - if val, found := m.Get(api.KeyUselessFiles); found { - rb.bindString("useless", sx.Cons(sx.MakeString(val), nil)) - } - queryContext := strZid + " " + api.ContextDirective - rb.bindString("context-url", sx.MakeString(newURLBuilder('h').AppendQuery(queryContext).String())) - queryContext += " " + api.FullDirective - rb.bindString("context-full-url", sx.MakeString(newURLBuilder('h').AppendQuery(queryContext).String())) - if wui.canRefresh(user) { - rb.bindString("reindex-url", sx.MakeString(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, meta.DefaultSyntax)) - var metaPairs sx.ListBuilder - for _, p := range m.ComputedPairs() { - key, value := p.Key, p.Value - metaPairs.Add(sx.Cons(sx.MakeString(key), sx.MakeString(value))) - rb.bindKeyValue(key, value) - } - 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 - } - ctx = box.NoEnrichContext(ctx) - menu, err := wui.box.GetZettel(ctx, id.TOCNewTemplateZid) - if err != nil { - return nil - } - refs := collect.Order(parser.ParseZettel(ctx, menu, "", wui.rtConfig)) - for i := len(refs) - 1; i >= 0; i-- { - zid, err2 := id.Parse(refs[i].URL.Path) - if err2 != nil { - continue - } - z, err2 := wui.box.GetZettel(ctx, zid) - if err2 != nil { - continue - } - if !wui.policy.CanRead(user, z.Meta) { - continue - } - text := sx.MakeString(parser.NormalizedSpacedText(z.Meta.GetTitle())) - link := sx.MakeString(wui.NewURLBuilder('c').SetZid(zid.ZettelID()). - AppendKVQuery(queryKeyAction, valueActionNew).String()) - - lst = lst.Cons(sx.Cons(text, link)) - } - return lst -} -func (wui *WebUI) calculateFooterSxn(ctx context.Context) *sx.Pair { - if footerZid, err := id.Parse(wui.rtConfig.Get(ctx, nil, config.KeyFooterZettel)); err == nil { - if zn, err2 := wui.evalZettel.Run(ctx, footerZid, ""); err2 == nil { - htmlEnc := wui.getSimpleHTMLEncoder(wui.rtConfig.Get(ctx, zn.InhMeta, api.KeyLang)).SetUnique("footer-") - if content, endnotes, err3 := htmlEnc.BlocksSxn(&zn.Ast); err3 == nil { - if content != nil && endnotes != nil { - content.LastPair().SetCdr(sx.Cons(endnotes, nil)) - } - return content - } - } - } - return nil -} - -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) - if err != nil { - return nil, err - } - - objs, err := reader.ReadAll() - if err != nil { - wui.log.Error().Err(err).Zid(zid).Msg("reading sxn template") - return nil, err - } - if len(objs) != 1 { - return nil, fmt.Errorf("expected 1 expression in template, but got %d", len(objs)) - } - env := sxeval.MakeExecutionEnvironment(bind) - t, err := env.Compile(objs[0]) - if err != nil { - return nil, err - } - - 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())) - return reader, nil -} - -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 - } - env := sxeval.MakeExecutionEnvironment(bind) - return env.Run(templateExpr) -} - -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, bind *sxeval.Binding) error { - detailObj, err := wui.evalSxnTemplate(ctx, templateID, bind) - if err != nil { - return err - } - bind.Bind(symDetail, detailObj) - - pageObj, err := wui.evalSxnTemplate(ctx, id.BaseTemplateZid, bind) - if err != nil { - return err - } - if msg := wui.log.Debug(); msg != nil { - // pageObj.String() can be expensive to calculate. - msg.Str("page", pageObj.String()).Msg("render") - } - - gen := sxhtml.NewGenerator().SetNewline() - var sb bytes.Buffer - _, err = gen.WriteHTML(&sb, pageObj) - if err != nil { - return err - } - wui.prepareAndWriteHeader(w, code) - if _, err = w.Write(sb.Bytes()); err != nil { - wui.log.Error().Err(err).Msg("Unable to write HTML via template") - } - return nil // No error reporting, since we do not know what happended during write to client. -} - -func (wui *WebUI) reportError(ctx context.Context, w http.ResponseWriter, err error) { - code, text := adapter.CodeMessageFromError(err) - if code == http.StatusInternalServerError { - wui.log.Error().Msg(err.Error()) - } else { - wui.log.Debug().Err(err).Msg("reportError") - } - user := server.GetUser(ctx) - env, rb := wui.createRenderEnv(ctx, "error", api.ValueLangEN, "Error", user) - rb.bindString("heading", sx.MakeString(http.StatusText(code))) - rb.bindString("message", sx.MakeString(text)) - if rb.err == nil { - rb.err = wui.renderSxnTemplateStatus(ctx, w, code, id.ErrorTemplateZid, env) - } - errSx := rb.err - if errSx == nil { - return - } - wui.log.Error().Err(errSx).Msg("while rendering error message") - - // if errBind != nil, the HTTP header was not written - wui.prepareAndWriteHeader(w, http.StatusInternalServerError) - fmt.Fprintf( - w, - `<!DOCTYPE html> -<html> -<head><title>Internal server error - -

Internal server error

-

When generating error code %d with message:

%v

an error occured:

%v
- -`, code, text, errSx) -} - -func makeStringList(sl []string) *sx.Pair { - if len(sl) == 0 { - return nil - } - result := sx.Nil() - for i := len(sl) - 1; i >= 0; i-- { - result = result.Cons(sx.MakeString(sl[i])) - } - return result -} Index: web/adapter/webui/webui.go ================================================================== --- web/adapter/webui/webui.go +++ web/adapter/webui/webui.go @@ -1,181 +1,137 @@ //----------------------------------------------------------------------------- -// Copyright (c) 2021-present Detlef Stern +// Copyright (c) 2021 Detlef Stern // -// This file is part of Zettelstore. +// This file is part of zettelstore. // // Zettelstore is licensed under the latest version of the EUPL (European Union // Public License). Please see file LICENSE.txt for your rights and obligations // under this license. -// -// SPDX-License-Identifier: EUPL-1.2 -// SPDX-FileCopyrightText: 2021-present Detlef Stern //----------------------------------------------------------------------------- // Package webui provides web-UI handlers for web requests. package webui import ( + "bytes" "context" + "log" "net/http" "sync" "time" - "t73f.de/r/sx" - "t73f.de/r/sx/sxeval" - "t73f.de/r/sxhtml" - "t73f.de/r/zsc/api" + "zettelstore.de/z/api" "zettelstore.de/z/auth" "zettelstore.de/z/box" + "zettelstore.de/z/collect" "zettelstore.de/z/config" + "zettelstore.de/z/domain" + "zettelstore.de/z/domain/id" + "zettelstore.de/z/domain/meta" + "zettelstore.de/z/encoder" + "zettelstore.de/z/input" "zettelstore.de/z/kernel" - "zettelstore.de/z/logger" - "zettelstore.de/z/usecase" + "zettelstore.de/z/parser" + "zettelstore.de/z/template" "zettelstore.de/z/web/adapter" "zettelstore.de/z/web/server" - "zettelstore.de/z/zettel" - "zettelstore.de/z/zettel/id" - "zettelstore.de/z/zettel/meta" ) // WebUI holds all data for delivering the web ui. type WebUI struct { - log *logger.Logger - debug bool ab server.AuthBuilder authz auth.AuthzManager rtConfig config.Config token auth.TokenManager box webuiBox policy auth.Policy - evalZettel *usecase.Evaluate - + templateCache map[id.Zid]*template.Template mxCache sync.RWMutex - templateCache map[id.Zid]sxeval.Expr tokenLifetime time.Duration cssBaseURL string cssUserURL string homeURL string listZettelURL string listRolesURL string listTagsURL string - refreshURL string withAuth bool loginURL string - logoutURL string searchURL string - createNewURL string - - 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. type webuiBox interface { - CanCreateZettel(context.Context) bool - GetZettel(context.Context, id.Zid) (zettel.Zettel, error) - GetMeta(context.Context, id.Zid) (*meta.Meta, error) - CanUpdateZettel(context.Context, zettel.Zettel) bool - AllowRenameZettel(context.Context, id.Zid) bool - CanDeleteZettel(context.Context, id.Zid) bool + CanCreateZettel(ctx context.Context) bool + GetZettel(ctx context.Context, zid id.Zid) (domain.Zettel, error) + GetMeta(ctx context.Context, zid id.Zid) (*meta.Meta, error) + CanUpdateZettel(ctx context.Context, zettel domain.Zettel) bool + AllowRenameZettel(ctx context.Context, zid id.Zid) bool + CanDeleteZettel(ctx context.Context, zid id.Zid) bool } // 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') - +func New(ab server.AuthBuilder, authz auth.AuthzManager, rtConfig config.Config, token auth.TokenManager, + mgr box.Manager, pol auth.Policy) *WebUI { wui := &WebUI{ - log: log, - debug: kernel.Main.GetConfig(kernel.CoreService, kernel.CoreDebug).(bool), ab: ab, rtConfig: rtConfig, authz: authz, token: token, box: mgr, policy: pol, - evalZettel: evalZettel, - - templateCache: make(map[id.Zid]sxeval.Expr, 32), - tokenLifetime: kernel.Main.GetConfig(kernel.WebService, kernel.WebTokenLifetimeHTML).(time.Duration), - cssBaseURL: ab.NewURLBuilder('z').SetZid(api.ZidBaseCSS).String(), - cssUserURL: ab.NewURLBuilder('z').SetZid(api.ZidUserCSS).String(), + cssBaseURL: ab.NewURLBuilder('z').SetZid( + id.BaseCSSZid).AppendQuery("_format", "raw").AppendQuery("_part", "content").String(), + cssUserURL: ab.NewURLBuilder('z').SetZid( + id.UserCSSZid).AppendQuery("_format", "raw").AppendQuery("_part", "content").String(), homeURL: ab.NewURLBuilder('/').String(), listZettelURL: ab.NewURLBuilder('h').String(), - listRolesURL: ab.NewURLBuilder('h').AppendQuery(api.ActionSeparator + api.KeyRole).String(), - listTagsURL: ab.NewURLBuilder('h').AppendQuery(api.ActionSeparator + api.KeyTags).String(), - refreshURL: ab.NewURLBuilder('g').AppendKVQuery("_c", "r").String(), + listRolesURL: ab.NewURLBuilder('h').AppendQuery("_l", "r").String(), + listTagsURL: ab.NewURLBuilder('h').AppendQuery("_l", "t").String(), withAuth: authz.WithAuth(), - loginURL: loginoutBase.String(), - logoutURL: loginoutBase.AppendKVQuery("logout", "").String(), - searchURL: ab.NewURLBuilder('h').String(), - createNewURL: ab.NewURLBuilder('c').String(), - - zettelBinding: nil, - genHTML: sxhtml.NewGenerator().SetNewline(), - } - wui.rootBinding = wui.createRenderBinding() + loginURL: ab.NewURLBuilder('a').String(), + searchURL: ab.NewURLBuilder('f').String(), + } 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) + if ci.Reason == box.OnReload || ci.Zid == id.BaseTemplateZid { + wui.templateCache = make(map[id.Zid]*template.Template, len(wui.templateCache)) } else { delete(wui.templateCache, ci.Zid) } wui.mxCache.Unlock() - - wui.mxZettelBinding.Lock() - if ci.Reason == box.OnReload || wui.dag.HasVertex(ci.Zid) { - wui.zettelBinding = nil - wui.dag = nil - } - wui.mxZettelBinding.Unlock() } -func (wui *WebUI) setSxnCache(zid id.Zid, expr sxeval.Expr) { +func (wui *WebUI) cacheSetTemplate(zid id.Zid, t *template.Template) { wui.mxCache.Lock() - wui.templateCache[zid] = expr + wui.templateCache[zid] = t wui.mxCache.Unlock() } -func (wui *WebUI) getSxnCache(zid id.Zid) sxeval.Expr { + +func (wui *WebUI) cacheGetTemplate(zid id.Zid) (*template.Template, bool) { wui.mxCache.RLock() - expr, found := wui.templateCache[zid] + t, ok := wui.templateCache[zid] wui.mxCache.RUnlock() - if found { - return expr - } - return nil + return t, ok } func (wui *WebUI) canCreate(ctx context.Context, user *meta.Meta) bool { m := meta.New(id.Invalid) return wui.policy.CanCreate(user, m) && wui.box.CanCreateZettel(ctx) } func (wui *WebUI) canWrite( - ctx context.Context, user, meta *meta.Meta, content zettel.Content) bool { + ctx context.Context, user, meta *meta.Meta, content domain.Content) bool { return wui.policy.CanWrite(user, meta, meta) && - wui.box.CanUpdateZettel(ctx, zettel.Zettel{Meta: meta, Content: content}) + wui.box.CanUpdateZettel(ctx, domain.Zettel{Meta: meta, Content: content}) } func (wui *WebUI) canRename(ctx context.Context, user, m *meta.Meta) bool { return wui.policy.CanRename(user, m) && wui.box.AllowRenameZettel(ctx, m.Zid) } @@ -182,17 +138,204 @@ func (wui *WebUI) canDelete(ctx context.Context, user, m *meta.Meta) bool { return wui.policy.CanDelete(user, m) && wui.box.CanDeleteZettel(ctx, m.Zid) } -func (wui *WebUI) canRefresh(user *meta.Meta) bool { - return wui.policy.CanRefresh(user) +func (wui *WebUI) getTemplate( + ctx context.Context, templateID id.Zid) (*template.Template, error) { + if t, ok := wui.cacheGetTemplate(templateID); ok { + return t, nil + } + realTemplateZettel, err := wui.box.GetZettel(ctx, templateID) + if err != nil { + return nil, err + } + t, err := template.ParseString(realTemplateZettel.Content.AsString(), nil) + if err == nil { + // t.SetErrorOnMissing() + wui.cacheSetTemplate(templateID, t) + } + return t, err +} + +type simpleLink struct { + Text string + URL string +} + +type baseData struct { + Lang string + MetaHeader string + CSSBaseURL string + CSSUserURL string + Title string + HomeURL string + WithUser bool + WithAuth bool + UserIsValid bool + UserZettelURL string + UserIdent string + UserLogoutURL string + LoginURL string + ListZettelURL string + ListRolesURL string + ListTagsURL string + HasNewZettelLinks bool + NewZettelLinks []simpleLink + SearchURL string + Content string + FooterHTML string +} + +func (wui *WebUI) makeBaseData( + ctx context.Context, lang, title string, user *meta.Meta, data *baseData) { + var ( + userZettelURL string + userIdent string + userLogoutURL string + ) + userIsValid := user != nil + if userIsValid { + userZettelURL = wui.NewURLBuilder('h').SetZid(user.Zid).String() + userIdent = user.GetDefault(meta.KeyUserID, "") + userLogoutURL = wui.NewURLBuilder('a').SetZid(user.Zid).String() + } + newZettelLinks := wui.fetchNewTemplates(ctx, user) + + data.Lang = lang + data.CSSBaseURL = wui.cssBaseURL + data.CSSUserURL = wui.cssUserURL + data.Title = title + data.HomeURL = wui.homeURL + data.WithAuth = wui.withAuth + data.WithUser = data.WithAuth + data.UserIsValid = userIsValid + data.UserZettelURL = userZettelURL + data.UserIdent = userIdent + data.UserLogoutURL = userLogoutURL + data.LoginURL = wui.loginURL + data.ListZettelURL = wui.listZettelURL + data.ListRolesURL = wui.listRolesURL + data.ListTagsURL = wui.listTagsURL + data.HasNewZettelLinks = len(newZettelLinks) > 0 + data.NewZettelLinks = newZettelLinks + data.SearchURL = wui.searchURL + data.FooterHTML = wui.rtConfig.GetFooterHTML() +} + +// htmlAttrNewWindow returns HTML attribute string for opening a link in a new window. +// If hasURL is false an empty string is returned. +func htmlAttrNewWindow(hasURL bool) string { + if hasURL { + return " target=\"_blank\" ref=\"noopener noreferrer\"" + } + return "" +} + +func (wui *WebUI) fetchNewTemplates(ctx context.Context, user *meta.Meta) (result []simpleLink) { + ctx = box.NoEnrichContext(ctx) + if !wui.canCreate(ctx, user) { + return nil + } + menu, err := wui.box.GetZettel(ctx, id.TOCNewTemplateZid) + if err != nil { + return nil + } + refs := collect.Order(parser.ParseZettel(menu, "", wui.rtConfig)) + for _, ref := range refs { + zid, err := id.Parse(ref.URL.Path) + if err != nil { + continue + } + m, err := wui.box.GetMeta(ctx, zid) + if err != nil { + continue + } + if !wui.policy.CanRead(user, m) { + continue + } + title := config.GetTitle(m, wui.rtConfig) + astTitle := parser.ParseInlines(input.NewInput(title), meta.ValueSyntaxZmk) + env := encoder.Environment{Lang: config.GetLang(m, wui.rtConfig)} + menuTitle, err := adapter.FormatInlines(astTitle, api.EncoderHTML, &env) + if err != nil { + menuTitle, err = adapter.FormatInlines(astTitle, api.EncoderText, nil) + if err != nil { + menuTitle = title + } + } + result = append(result, simpleLink{ + Text: menuTitle, + URL: wui.NewURLBuilder('g').SetZid(m.Zid).String(), + }) + } + return result +} + +func (wui *WebUI) renderTemplate( + ctx context.Context, + w http.ResponseWriter, + templateID id.Zid, + base *baseData, + data interface{}) { + wui.renderTemplateStatus(ctx, w, http.StatusOK, templateID, base, data) +} + +func (wui *WebUI) reportError(ctx context.Context, w http.ResponseWriter, err error) { + code, text := adapter.CodeMessageFromError(err) + if code == http.StatusInternalServerError { + log.Printf("%v: %v", text, err) + } + user := wui.getUser(ctx) + var base baseData + wui.makeBaseData(ctx, meta.ValueLangEN, "Error", user, &base) + wui.renderTemplateStatus(ctx, w, code, id.ErrorTemplateZid, &base, struct { + ErrorTitle string + ErrorText string + }{ + ErrorTitle: http.StatusText(code), + ErrorText: text, + }) +} + +func (wui *WebUI) renderTemplateStatus( + ctx context.Context, + w http.ResponseWriter, + code int, + templateID id.Zid, + base *baseData, + data interface{}) { + + bt, err := wui.getTemplate(ctx, id.BaseTemplateZid) + if err != nil { + adapter.InternalServerError(w, "Unable to get base template", err) + return + } + t, err := wui.getTemplate(ctx, templateID) + if err != nil { + adapter.InternalServerError(w, "Unable to get template", err) + return + } + if user := wui.getUser(ctx); user != nil { + if tok, err1 := wui.token.GetToken(user, wui.tokenLifetime, auth.KindHTML); err1 == nil { + wui.setToken(w, tok) + } + } + var content bytes.Buffer + err = t.Render(&content, data) + if err == nil { + base.Content = content.String() + w.Header().Set(api.HeaderContentType, "text/html; charset=utf-8") + w.WriteHeader(code) + err = bt.Render(w, base) + } + if err != nil { + log.Println("Unable to render template", err) + } } -func (wui *WebUI) getSimpleHTMLEncoder(lang string) *htmlGenerator { - return wui.createGenerator(wui, lang) -} +func (wui *WebUI) getUser(ctx context.Context) *meta.Meta { return wui.ab.GetUser(ctx) } // GetURLPrefix returns the configured URL prefix of the web server. func (wui *WebUI) GetURLPrefix() string { return wui.ab.GetURLPrefix() } // NewURLBuilder creates a new URL builder object with the given key. @@ -199,21 +342,8 @@ func (wui *WebUI) NewURLBuilder(key byte) *api.URLBuilder { return wui.ab.NewURLBuilder(key) } func (wui *WebUI) clearToken(ctx context.Context, w http.ResponseWriter) context.Context { return wui.ab.ClearToken(ctx, w) } - func (wui *WebUI) setToken(w http.ResponseWriter, token []byte) { wui.ab.SetToken(w, token, wui.tokenLifetime) } - -func (wui *WebUI) prepareAndWriteHeader(w http.ResponseWriter, statusCode int) { - h := adapter.PrepareHeader(w, "text/html; charset=utf-8") - h.Set("Content-Security-Policy", "default-src 'self'; img-src * data:; style-src 'self' 'unsafe-inline'") - h.Set("Permissions-Policy", "payment=(), interest-cohort=()") - h.Set("Referrer-Policy", "no-referrer") - h.Set("X-Content-Type-Options", "nosniff") - if !wui.debug { - h.Set("X-Frame-Options", "sameorigin") - } - w.WriteHeader(statusCode) -} DELETED web/content/content.go Index: web/content/content.go ================================================================== --- web/content/content.go +++ web/content/content.go @@ -1,122 +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. -// -// 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 - -import ( - "mime" - "net/http" - - "t73f.de/r/zsc/api" - "zettelstore.de/z/zettel" - "zettelstore.de/z/zettel/meta" -) - -const ( - UnknownMIME = "application/octet-stream" - mimeGIF = "image/gif" - mimeHTML = "text/html; charset=utf-8" - mimeJPEG = "image/jpeg" - mimeMarkdown = "text/markdown; charset=utf-8" - PlainText = "text/plain; charset=utf-8" - mimePNG = "image/png" - SXPF = PlainText - mimeWEBP = "image/webp" -) - -var encoding2mime = map[api.EncodingEnum]string{ - api.EncoderHTML: mimeHTML, - api.EncoderMD: mimeMarkdown, - api.EncoderSz: SXPF, - api.EncoderSHTML: SXPF, - api.EncoderText: PlainText, - api.EncoderZmk: PlainText, -} - -// MIMEFromEncoding returns the MIME encoding for a given zettel encoding -func MIMEFromEncoding(enc api.EncodingEnum) string { - if m, found := encoding2mime[enc]; found { - return m - } - return UnknownMIME -} - -var syntax2mime = map[string]string{ - meta.SyntaxCSS: "text/css; charset=utf-8", - meta.SyntaxDraw: PlainText, - meta.SyntaxGif: mimeGIF, - meta.SyntaxHTML: mimeHTML, - meta.SyntaxJPEG: mimeJPEG, - meta.SyntaxJPG: mimeJPEG, - meta.SyntaxMarkdown: mimeMarkdown, - meta.SyntaxMD: mimeMarkdown, - meta.SyntaxNone: "", - meta.SyntaxPlain: PlainText, - meta.SyntaxPNG: mimePNG, - meta.SyntaxSVG: "image/svg+xml", - meta.SyntaxSxn: SXPF, - meta.SyntaxText: PlainText, - meta.SyntaxTxt: PlainText, - meta.SyntaxWebp: mimeWEBP, - meta.SyntaxZmk: "text/x-zmk; charset=utf-8", - - // Additional syntaxes that are parsed as plain text. - "js": "text/javascript; charset=utf-8", - "pdf": "application/pdf", - "xml": "text/xml; charset=utf-8", -} - -// MIMEFromSyntax returns a MIME encoding for a given syntax value. -func MIMEFromSyntax(syntax string) string { - if mt, found := syntax2mime[syntax]; found { - return mt - } - return UnknownMIME -} - -var mime2syntax = map[string]string{ - mimeGIF: meta.SyntaxGif, - mimeJPEG: meta.SyntaxJPEG, - mimePNG: meta.SyntaxPNG, - mimeWEBP: meta.SyntaxWebp, - "text/html": meta.SyntaxHTML, - "text/markdown": meta.SyntaxMarkdown, - "text/plain": meta.SyntaxText, - - // Additional syntaxes - "application/pdf": "pdf", - "text/javascript": "js", -} - -func SyntaxFromMIME(m string, data []byte) string { - mt, _, _ := mime.ParseMediaType(m) - if syntax, found := mime2syntax[mt]; found { - return syntax - } - if len(data) > 0 { - ct := http.DetectContentType(data) - mt, _, _ = mime.ParseMediaType(ct) - if syntax, found := mime2syntax[mt]; found { - return syntax - } - if ext, err := mime.ExtensionsByType(mt); err != nil && len(ext) > 0 { - return ext[0][1:] - } - if zettel.IsBinary(data) { - return "binary" - } - } - return "plain" -} DELETED web/content/content_test.go Index: web/content/content_test.go ================================================================== --- web/content/content_test.go +++ web/content/content_test.go @@ -1,45 +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. -// -// SPDX-License-Identifier: EUPL-1.2 -// SPDX-FileCopyrightText: 2022-present Detlef Stern -//----------------------------------------------------------------------------- - -package content_test - -import ( - "testing" - - "zettelstore.de/z/parser" - "zettelstore.de/z/web/content" - - _ "zettelstore.de/z/parser/blob" // Allow to use BLOB parser. - _ "zettelstore.de/z/parser/draw" // Allow to use draw parser. - _ "zettelstore.de/z/parser/markdown" // Allow to use markdown parser. - _ "zettelstore.de/z/parser/none" // Allow to use none parser. - _ "zettelstore.de/z/parser/plain" // Allow to use plain parser. - _ "zettelstore.de/z/parser/zettelmark" // Allow to use zettelmark parser. -) - -func TestSupportedSyntax(t *testing.T) { - for _, syntax := range parser.GetSyntaxes() { - mt := content.MIMEFromSyntax(syntax) - if mt == content.UnknownMIME { - t.Errorf("No MIME type registered for syntax %q", syntax) - continue - } - - newSyntax := content.SyntaxFromMIME(mt, nil) - pinfo := parser.Get(newSyntax) - if pinfo == nil { - t.Errorf("MIME type for syntax %q is %q, but this has no corresponding syntax", syntax, mt) - continue - } - } -} Index: web/server/impl/http.go ================================================================== --- web/server/impl/http.go +++ web/server/impl/http.go @@ -1,18 +1,16 @@ //----------------------------------------------------------------------------- -// Copyright (c) 2020-present Detlef Stern +// Copyright (c) 2020-2021 Detlef Stern // -// This file is part of Zettelstore. +// This file is part of zettelstore. // // Zettelstore is licensed under the latest version of the EUPL (European Union // Public License). Please see file LICENSE.txt for your rights and obligations // under this license. -// -// SPDX-License-Identifier: EUPL-1.2 -// SPDX-FileCopyrightText: 2020-present Detlef Stern //----------------------------------------------------------------------------- +// Package impl provides the Zettelstore web service. package impl import ( "context" "net" @@ -29,10 +27,11 @@ ) // 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 == "" { @@ -45,10 +44,11 @@ // 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. @@ -68,11 +68,11 @@ go func() { srv.Serve(ln) }() return nil } // Stop the web server. -func (srv *httpServer) Stop() { +func (srv *httpServer) Stop() error { ctx, cancel := context.WithTimeout(context.Background(), shutdownTimeout) defer cancel() - srv.Shutdown(ctx) + return srv.Shutdown(ctx) } Index: web/server/impl/impl.go ================================================================== --- web/server/impl/impl.go +++ web/server/impl/impl.go @@ -1,16 +1,13 @@ //----------------------------------------------------------------------------- -// Copyright (c) 2021-present Detlef Stern +// Copyright (c) 2021 Detlef Stern // -// This file is part of Zettelstore. +// This file is part of zettelstore. // // Zettelstore is licensed under the latest version of the EUPL (European Union // Public License). Please see file LICENSE.txt for your rights and obligations // under this license. -// -// SPDX-License-Identifier: EUPL-1.2 -// SPDX-FileCopyrightText: 2021-present Detlef Stern //----------------------------------------------------------------------------- // Package impl provides the Zettelstore web service. package impl @@ -17,60 +14,57 @@ import ( "context" "net/http" "time" - "t73f.de/r/zsc/api" + "zettelstore.de/z/api" "zettelstore.de/z/auth" - "zettelstore.de/z/logger" + "zettelstore.de/z/domain/meta" "zettelstore.de/z/web/server" - "zettelstore.de/z/zettel/meta" ) type myServer struct { - log *logger.Logger - baseURL string server httpServer router httpRouter persistentCookie bool secureCookie bool } // New creates a new web server. -func New(log *logger.Logger, listenAddr, baseURL, urlPrefix string, persistentCookie, secureCookie bool, maxRequestSize int64, auth auth.TokenManager) server.Server { +func New(listenAddr, urlPrefix string, persistentCookie, secureCookie bool, auth auth.TokenManager) server.Server { srv := myServer{ - log: log, - baseURL: baseURL, persistentCookie: persistentCookie, secureCookie: secureCookie, } - srv.router.initializeRouter(log, urlPrefix, maxRequestSize, auth) + srv.router.initializeRouter(urlPrefix, auth) srv.server.initializeHTTPServer(listenAddr, &srv.router) return &srv } func (srv *myServer) Handle(pattern string, handler http.Handler) { srv.router.Handle(pattern, handler) } -func (srv *myServer) AddListRoute(key byte, method server.Method, handler http.Handler) { - srv.router.addListRoute(key, method, handler) +func (srv *myServer) AddListRoute(key byte, httpMethod string, handler http.Handler) { + srv.router.addListRoute(key, httpMethod, handler) } -func (srv *myServer) AddZettelRoute(key byte, method server.Method, handler http.Handler) { - srv.router.addZettelRoute(key, method, handler) +func (srv *myServer) AddZettelRoute(key byte, httpMethod string, handler http.Handler) { + srv.router.addZettelRoute(key, httpMethod, handler) } func (srv *myServer) SetUserRetriever(ur server.UserRetriever) { srv.router.ur = ur } - -func (srv *myServer) GetURLPrefix() string { - return srv.router.urlPrefix +func (srv *myServer) GetUser(ctx context.Context) *meta.Meta { + if data := srv.GetAuthData(ctx); data != nil { + return data.User + } + return nil } func (srv *myServer) NewURLBuilder(key byte) *api.URLBuilder { return api.NewURLBuilder(srv.GetURLPrefix(), key) } -func (srv *myServer) NewURLBuilderAbs(key byte) *api.URLBuilder { - return api.NewURLBuilder(srv.baseURL, key) +func (srv *myServer) GetURLPrefix() string { + return srv.router.urlPrefix } const sessionName = "zsession" func (srv *myServer) SetToken(w http.ResponseWriter, token []byte, d time.Duration) { @@ -78,49 +72,53 @@ Name: sessionName, Value: string(token), Path: srv.GetURLPrefix(), Secure: srv.secureCookie, HttpOnly: true, - SameSite: http.SameSiteLaxMode, + SameSite: http.SameSiteStrictMode, } if srv.persistentCookie && d > 0 { cookie.Expires = time.Now().Add(d).Add(30 * time.Second).UTC() } - srv.log.Debug().Bytes("token", token).Msg("SetToken") - if v := cookie.String(); v != "" { - w.Header().Add("Set-Cookie", v) - w.Header().Add("Cache-Control", `no-cache="Set-Cookie"`) - w.Header().Add("Vary", "Cookie") - } + http.SetCookie(w, &cookie) } // ClearToken invalidates the session cookie by sending an empty one. func (srv *myServer) ClearToken(ctx context.Context, w http.ResponseWriter) context.Context { - if authData := server.GetAuthData(ctx); authData == nil { - // No authentication data stored in session, nothing to do. - return ctx - } if w != nil { srv.SetToken(w, nil, 0) } return updateContext(ctx, nil, nil) } + +// GetAuthData returns the full authentication data from the context. +func (srv *myServer) GetAuthData(ctx context.Context) *server.AuthData { + data, ok := ctx.Value(ctxKeySession).(*server.AuthData) + if ok { + return data + } + return nil +} + +type ctxKeyTypeSession struct{} + +var ctxKeySession ctxKeyTypeSession func updateContext(ctx context.Context, user *meta.Meta, data *auth.TokenData) context.Context { if data == nil { - return context.WithValue(ctx, server.CtxKeySession, &server.AuthData{User: user}) + return context.WithValue(ctx, ctxKeySession, &server.AuthData{User: user}) } return context.WithValue( ctx, - server.CtxKeySession, + ctxKeySession, &server.AuthData{ User: user, Token: data.Token, Now: data.Now, Issued: data.Issued, Expires: data.Expires, }) } -func (srv *myServer) SetDebug() { srv.server.SetDebug() } -func (srv *myServer) Run() error { return srv.server.Run() } -func (srv *myServer) Stop() { srv.server.Stop() } +func (srv *myServer) SetDebug() { srv.server.SetDebug() } +func (srv *myServer) Run() error { return srv.server.Run() } +func (srv *myServer) Stop() error { return srv.server.Stop() } Index: web/server/impl/router.go ================================================================== --- web/server/impl/router.go +++ web/server/impl/router.go @@ -1,75 +1,58 @@ //----------------------------------------------------------------------------- -// Copyright (c) 2020-present Detlef Stern +// Copyright (c) 2020-2021 Detlef Stern // -// This file is part of Zettelstore. +// This file is part of zettelstore. // // Zettelstore is licensed under the latest version of the EUPL (European Union // Public License). Please see file LICENSE.txt for your rights and obligations // under this license. -// -// SPDX-License-Identifier: EUPL-1.2 -// SPDX-FileCopyrightText: 2020-present Detlef Stern //----------------------------------------------------------------------------- +// Package impl provides the Zettelstore web service. package impl import ( - "io" "net/http" "regexp" "strings" - "t73f.de/r/zsc/api" "zettelstore.de/z/auth" - "zettelstore.de/z/kernel" - "zettelstore.de/z/logger" "zettelstore.de/z/web/server" ) type ( - methodHandler [server.MethodLAST]http.Handler - routingTable [256]*methodHandler -) - -var mapMethod = map[string]server.Method{ - http.MethodHead: server.MethodHead, - http.MethodGet: server.MethodGet, - http.MethodPost: server.MethodPost, - http.MethodPut: server.MethodPut, - http.MethodDelete: server.MethodDelete, - api.MethodMove: server.MethodMove, -} + methodHandler map[string]http.Handler + routingTable map[byte]methodHandler +) // httpRouter handles all routing for zettelstore. type httpRouter struct { - log *logger.Logger urlPrefix string auth auth.TokenManager minKey byte maxKey byte reURL *regexp.Regexp listTable routingTable zettelTable routingTable ur server.UserRetriever mux *http.ServeMux - maxReqSize int64 } // initializeRouter creates a new, empty router with the given root handler. -func (rt *httpRouter) initializeRouter(log *logger.Logger, urlPrefix string, maxRequestSize int64, auth auth.TokenManager) { - rt.log = log +func (rt *httpRouter) initializeRouter(urlPrefix string, auth auth.TokenManager) { rt.urlPrefix = urlPrefix rt.auth = auth rt.minKey = 255 rt.maxKey = 0 rt.reURL = regexp.MustCompile("^$") rt.mux = http.NewServeMux() - rt.maxReqSize = maxRequestSize + rt.listTable = make(routingTable) + rt.zettelTable = make(routingTable) } -func (rt *httpRouter) addRoute(key byte, method server.Method, handler http.Handler, table *routingTable) { +func (rt *httpRouter) addRoute(key byte, httpMethod string, handler http.Handler, table routingTable) { // Set minKey and maxKey; re-calculate regexp. if key < rt.minKey || rt.maxKey < key { if key < rt.minKey { rt.minKey = key } @@ -78,126 +61,87 @@ } rt.reURL = regexp.MustCompile( "^/(?:([" + string(rt.minKey) + "-" + string(rt.maxKey) + "])(?:/(?:([0-9]{14})/?)?)?)$") } - mh := table[key] - if mh == nil { - mh = new(methodHandler) + mh, hasKey := table[key] + if !hasKey { + mh = make(methodHandler) table[key] = mh } - mh[method] = handler - if method == server.MethodGet { - if prevHandler := mh[server.MethodHead]; prevHandler == nil { - mh[server.MethodHead] = handler + mh[httpMethod] = handler + if httpMethod == http.MethodGet { + if _, hasHead := table[key][http.MethodHead]; !hasHead { + table[key][http.MethodHead] = handler } } } // addListRoute adds a route for the given key and HTTP method to work with a list. -func (rt *httpRouter) addListRoute(key byte, method server.Method, handler http.Handler) { - rt.addRoute(key, method, handler, &rt.listTable) +func (rt *httpRouter) addListRoute(key byte, httpMethod string, handler http.Handler) { + rt.addRoute(key, httpMethod, handler, rt.listTable) } // addZettelRoute adds a route for the given key and HTTP method to work with a zettel. -func (rt *httpRouter) addZettelRoute(key byte, method server.Method, handler http.Handler) { - rt.addRoute(key, method, handler, &rt.zettelTable) +func (rt *httpRouter) addZettelRoute(key byte, httpMethod string, handler http.Handler) { + rt.addRoute(key, httpMethod, handler, rt.zettelTable) } // Handle registers the handler for the given pattern. If a handler already exists for pattern, Handle panics. func (rt *httpRouter) Handle(pattern string, handler http.Handler) { rt.mux.Handle(pattern, handler) } func (rt *httpRouter) ServeHTTP(w http.ResponseWriter, r *http.Request) { - // Something may panic. Ensure a kernel log. - defer func() { - if ri := recover(); ri != nil { - rt.log.Error().Str("Method", r.Method).Str("URL", r.URL.String()).HTTPIP(r).Msg("Recover context") - kernel.Main.LogRecover("Web", ri) - } - }() - - var withDebug bool - if msg := rt.log.Debug(); msg.Enabled() { - withDebug = true - w = &traceResponseWriter{original: w} - msg.Str("method", r.Method).Str("uri", r.RequestURI).HTTPIP(r).Msg("ServeHTTP") - } - if prefixLen := len(rt.urlPrefix); prefixLen > 1 { if len(r.URL.Path) < prefixLen || r.URL.Path[:prefixLen] != rt.urlPrefix { http.Error(w, http.StatusText(http.StatusNotFound), http.StatusNotFound) - if withDebug { - rt.log.Debug().Int("sc", int64(w.(*traceResponseWriter).statusCode)).Msg("/ServeHTTP/prefix") - } return } r.URL.Path = r.URL.Path[prefixLen-1:] } - r.Body = http.MaxBytesReader(w, r.Body, rt.maxReqSize) match := rt.reURL.FindStringSubmatch(r.URL.Path) if len(match) != 3 { rt.mux.ServeHTTP(w, rt.addUserContext(r)) - if withDebug { - rt.log.Debug().Int("sc", int64(w.(*traceResponseWriter).statusCode)).Msg("match other") - } return } - if withDebug { - rt.log.Debug().Str("key", match[1]).Str("zid", match[2]).Msg("path match") - } key := match[1][0] - var mh *methodHandler + table := rt.zettelTable if match[2] == "" { - mh = rt.listTable[key] - } else { - mh = rt.zettelTable[key] + table = rt.listTable } - method, ok := mapMethod[r.Method] - if ok && mh != nil { - if handler := mh[method]; handler != nil { + if mh, ok := table[key]; ok { + if handler, ok := mh[r.Method]; ok { r.URL.Path = "/" + match[2] handler.ServeHTTP(w, rt.addUserContext(r)) - if withDebug { - rt.log.Debug().Int("sc", int64(w.(*traceResponseWriter).statusCode)).Msg("/ServeHTTP") - } return } } http.Error(w, http.StatusText(http.StatusMethodNotAllowed), http.StatusMethodNotAllowed) - if withDebug { - rt.log.Debug().Int("sc", int64(w.(*traceResponseWriter).statusCode)).Msg("no match") - } } func (rt *httpRouter) addUserContext(r *http.Request) *http.Request { if rt.ur == nil { - // No auth needed return r } - k := auth.KindAPI + k := auth.KindJSON t := getHeaderToken(r) if len(t) == 0 { - rt.log.Debug().Msg("no jwt token found") // IP already logged: ServeHTTP - k = auth.KindwebUI + k = auth.KindHTML t = getSessionToken(r) } if len(t) == 0 { - 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.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.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)) } @@ -226,20 +170,5 @@ if len(auth) < len(prefix) || !strings.EqualFold(auth[:len(prefix)], prefix) { return nil } return []byte(auth[len(prefix):]) } - -type traceResponseWriter struct { - original http.ResponseWriter - statusCode int -} - -func (w *traceResponseWriter) Header() http.Header { return w.original.Header() } -func (w *traceResponseWriter) Write(p []byte) (int, error) { return w.original.Write(p) } -func (w *traceResponseWriter) WriteHeader(statusCode int) { - w.statusCode = statusCode - w.original.WriteHeader(statusCode) -} -func (w *traceResponseWriter) WriteString(s string) (int, error) { - return io.WriteString(w.original, s) -} Index: web/server/server.go ================================================================== --- web/server/server.go +++ web/server/server.go @@ -1,16 +1,13 @@ //----------------------------------------------------------------------------- -// Copyright (c) 2020-present Detlef Stern +// Copyright (c) 2020-2021 Detlef Stern // -// This file is part of Zettelstore. +// This file is part of zettelstore. // // Zettelstore is licensed under the latest version of the EUPL (European Union // Public License). Please see file LICENSE.txt for your rights and obligations // under this license. -// -// SPDX-License-Identifier: EUPL-1.2 -// SPDX-FileCopyrightText: 2020-present Detlef Stern //----------------------------------------------------------------------------- // Package server provides the Zettelstore web service. package server @@ -17,56 +14,44 @@ import ( "context" "net/http" "time" - "t73f.de/r/zsc/api" - "zettelstore.de/z/zettel/id" - "zettelstore.de/z/zettel/meta" + "zettelstore.de/z/api" + "zettelstore.de/z/domain/id" + "zettelstore.de/z/domain/meta" ) // UserRetriever allows to retrieve user data based on a given zettel identifier. type UserRetriever interface { GetUser(ctx context.Context, zid id.Zid, ident string) (*meta.Meta, error) } -// Method enumerates the allowed HTTP methods. -type Method uint8 - -// Values for method type -const ( - MethodGet Method = iota - MethodHead - MethodPost - MethodPut - MethodMove - MethodDelete - MethodLAST // must always be the last one -) - // Router allows to state routes for various URL paths. type Router interface { Handle(pattern string, handler http.Handler) - AddListRoute(key byte, method Method, handler http.Handler) - AddZettelRoute(key byte, method Method, handler http.Handler) + AddListRoute(key byte, httpMethod string, handler http.Handler) + AddZettelRoute(key byte, httpMethod string, handler http.Handler) SetUserRetriever(ur UserRetriever) } // Builder allows to build new URLs for the web service. type Builder interface { GetURLPrefix() string NewURLBuilder(key byte) *api.URLBuilder - NewURLBuilderAbs(key byte) *api.URLBuilder } // Auth is the authencation interface. type Auth interface { - // SetToken sends the token to the client. + GetUser(context.Context) *meta.Meta SetToken(w http.ResponseWriter, token []byte, d time.Duration) // ClearToken invalidates the session cookie by sending an empty one. ClearToken(ctx context.Context, w http.ResponseWriter) context.Context + + // GetAuthData returns the full authentication data from the context. + GetAuthData(ctx context.Context) *AuthData } // AuthData stores all relevant authentication data for a context. type AuthData struct { User *meta.Meta @@ -74,35 +59,10 @@ Now time.Time Issued time.Time Expires time.Time } -// GetAuthData returns the full authentication data from the context. -func GetAuthData(ctx context.Context) *AuthData { - if ctx != nil { - data, ok := ctx.Value(CtxKeySession).(*AuthData) - if ok { - return data - } - } - return nil -} - -// GetUser returns the metadata of the current user, or nil if there is no one. -func GetUser(ctx context.Context) *meta.Meta { - if data := GetAuthData(ctx); data != nil { - return data.User - } - return nil -} - -// CtxKeyTypeSession is just an additional type to make context value retrieval unambiguous. -type CtxKeyTypeSession struct{} - -// CtxKeySession is the key value to retrieve Authdata -var CtxKeySession CtxKeyTypeSession - // AuthBuilder is a Builder that also allows to execute authentication functions. type AuthBuilder interface { Auth Builder } @@ -113,7 +73,7 @@ Auth Builder SetDebug() Run() error - Stop() + Stop() error } Index: www/build.md ================================================================== --- www/build.md +++ www/build.md @@ -1,94 +1,59 @@ -# How to build Zettelstore +# How to build the Zettelstore ## Prerequisites You must install the following software: -* A current, supported [release of Go](https://go.dev/doc/devel/release), -* [staticcheck](https://staticcheck.io/), -* [shadow](https://pkg.go.dev/golang.org/x/tools/go/analysis/passes/shadow), -* [unparam](https://mvdan.cc/unparam), -* [govulncheck](https://golang.org/x/vuln/cmd/govulncheck), -* [Fossil](https://fossil-scm.org/), -* [Git](https://git-scm.org) (so that Go can download some dependencies). - -See folder `docs/development` (a zettel box) for details. +* A current, supported [release of Go](https://golang.org/doc/devel/release.html), +* [golint](https://github.com/golang/lint|golint), +* [Fossil](https://fossil-scm.org/). ## Clone the repository -Most of this is covered by the excellent Fossil -[documentation](https://fossil-scm.org/home/doc/trunk/www/quickstart.wiki). +Most of this is covered by the excellent Fossil documentation. 1. Create a directory to store your Fossil repositories. - Let's assume, you have created `$HOME/fossils`. -1. Clone the repository: `fossil clone https://zettelstore.de/ $HOME/fossils/zettelstore.fossil`. + Let's assume, you have created $HOME/fossil. +1. Clone the repository: `fossil clone https://zettelstore.de/ $HOME/fossil/zettelstore.fossil`. 1. Create a working directory. - Let's assume, you have created `$HOME/zettelstore`. + 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`. +1. Open development: `fossil open $HOME/fossil/zettelstore.fossil`. + +(If you are not able to use Fossil, you could try the Git mirror +.) -## 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 build tool +In directory tools there is a Go file called build.go. +It automates most aspects, (hopefully) platform-independent. -The build script is called as: +The script is called as: ``` -go run tools/build/build.go [-v] COMMAND +go run tools/build.go [-v] COMMAND ``` The flag `-v` enables the verbose mode. It outputs all commands called by the tool. -Some important `COMMAND`s are: +`COMMAND` is one of: * `build`: builds the software with correct version information and puts it - into a freshly created directory `bin`. + into a freshly created directory bin. * `check`: checks the current state of the working directory to be ready for release (or commit). +* `release`: executes `check` command and if this was successful, builds the + software for various platforms, and creates ZIP files for each executable. + Everything is put in the directory releases. +* `clean`: removes the directories bin and releases. * `version`: prints the current version information. Therefore, the easiest way to build your own version of the Zettelstore software is to execute the command ``` -go run tools/build/build.go build +go run tools/build.go build ``` In case of errors, please send the output of the verbose execution: ``` -go run tools/build/build.go -v build +go run tools/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. - -Some dependencies of Zettelstore, namely [Zettelstore -client](https://t73f.de/r/zsc) and [Sx](https://t73f.de/r/sx), are also -managed by Fossil. Depending on your development setup, some error messages -might occur. - -If the error message mentions an environment variable called `GOVCS` you should -set it to the value `GOVCS=zettelstore.de:fossil` (alternatively more generous -to `GOVCS=*:all`). Since the Go build system is coupled with Git and some -special platforms, you allow ot to download a Fossil repository from the host -`zettelstore.de`. The build tool set `GOVCS` to the right value, but you may -use other `go` commands that try to download a Fossil repository. - -On some operating systems, namely Termux on Android, an error message might -state that an user cannot be determined (`cannot determine user`). In this -case, Fossil is allowed to download the repository, but cannot associate it -with an user name. Set the environment variable `USER` to any user name, like: -`USER=nobody go run tools/build.go build`. Index: www/changes.wiki ================================================================== --- www/changes.wiki +++ www/changes.wiki @@ -1,942 +1,46 @@ Change Log - -

Changes for Version 0.18.0 (pending)

- * Remove Sx macro defunconst. Use defun instead. - (breaking: webui) - * Update Sx prelude: make macros more robust / more general. This might - break your code in the future. - (minor: webui) - * Add computed zettel “Zettelstore Memory” with zettel - identifier 00000000000008. It shows some statistics about - memory usage. - (minor: webui) - * Zettelstore client is now Go package t73f.de/r/zsc. - (minor) - - -

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 - binding of its name to its function body (performance optimization). - (breaking: webui) - * Allow to determine a role zettel for a given role. - (major: api, webui) - * Present user the option to create a (missing) role zettel (in list view). - Results in a new predefined zettel with identifier 00000000090004, which - is a template for new role zettel. - (minor: webui) - * Timestamp values can be abbrevated by omitting most of its components. - Previously, such values that are not in the format YYYYMMDDhhmmss were - ignored. Now the following formats are also allowed: YYYY, YYYYMM, - YYYYMMDD, YYYYMMDDhh, YYYYMMDDhhmm. Querying and sorting work accordingly. - Previously, only a sequences of zeroes were appended, resulting in illegal - timestamps, e.g. for YYYY or YYYYMM. - (minor) - * 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 / - 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 - referencing zettel in the web user interface. You can set these - configurations system-wide, per user, or per zettel. Often it is used to - ensure a “clean” home zettel. Affects the list of incoming - / back links, folge zettel, subordinate zettel, and successor zettel. - (minor: webui) - * 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. - (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. - (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) - (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) - * Some smaller bug fixes and improvements, to the software and to the - 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. - (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. - (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. - (major: webui) - * Allow sx code to change WebUI for zettel with specified role. - (major: webui) - * Some minor usability improvements. - (minor: webui) - * Some smaller bug fixes and improvements, to the software and to the - documentation. - - -

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-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 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. - (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. - (minor: webui) - * Use a zettel that defines builtins for evaluating WebUI templates. - (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. - (deprecated: api) - * Some smaller bug fixes and improvements, to the software and to the - documentation. - - -

Changes for Version 0.12.0 (2023-06-05)

- * Syntax of templates for the web user interface are changed from Mustache - to Sxn (S-Expressions). Mustache is no longer supported, nowhere in the - software. Mustache was marked deprecated in version 0.11.0. If you - modified the template zettel, you must adapt to the new syntax. - (breaking: webui) - * Query expression is allowed to search for the "context" of a zettel. - Previously, this was a separate call, without adding a search expression - / action expression. - (breaking) - * "sexpr" encoding is renamed to "sz" encoding. This will affect mostly the - API. Additionally, all string "sexpr" are renamed to "sz" also. "Sz" is - the short form for "symbolic expression for zettel", similar to "shtml" - that is the short form for "symbolic expression for HTML". - (breaking) - * 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. - (minor: api, webui) - * 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. - (minor) - * Metadata keys with suffix -date and -time are - treated as - timestamp values. - (minor) - * 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. - (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! - (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. - (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. - (breaking) - * Title of a zettel is no longer interpreted as Zettelmarkup text. Now it is - just a plain string, possibly empty. Therefore, no inline formatting (like - 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 - 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. - (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. - (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 - less likely add its references. Same for often used tags. The cost model - might change in some details in the future, but the idea of a penalty - applied to zettel / tags with many references will hold. - (minor) - * Some smaller bug fixes and improvements, to the software and to the - documentation. - - -

Changes for Version 0.10.1 (2023-01-30)

- * 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. - (breaking: api) - * 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. - (minor: webui) - * Mark ZJSON encoding as deprecated for v0.11.0. Please use Sexpr encoding - instead. - (deprecated) - * 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 - 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. - (major: webui) - * Add syntax “draw” (again) - (minor: zettelmarkup) - * Allow to encode zettel in Markdown. Please note: not every aspect of - 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. - (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. - (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. - (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. - (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. - (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. - (breaking: zettelmarkup, api, webui) - * 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. - (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. - (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. - (minor: zettelmarkup, webui) - * Add categories in RSS feed, based on zettel tags. - (minor: api, webui) - * Add support for creating an Atom 1.0 feed using a query action. - (minor: api, webui) - * Ignore entities with code point that is not allowed in HTML. - (minor: zettelmarkup) - * Enhance distribution of tag sizes when show a tag cloud. - (minor: webui) - * Warn user if zettelstore listens non-locally, but no authentication is - enabled. - (minor: server) - * Fix error that a manual zettel deletion was not always detected. - (bug: dirbox) - * Some smaller bug fixes and improvements, to the software and to the - documentation. - - -

Changes for Version 0.7.1 (2022-09-18)

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

Changes for Version 0.7.0 (2022-09-17)

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

Changes for Version 0.6.2 (2022-08-22)

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

Changes for Version 0.6.1 (2022-08-22)

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

Changes for Version 0.6.0 (2022-08-11)

- * Translating of "..." into horizontal ellipsis is no longer supported. Use - &hellip; instead. - (breaking: zettelmarkup) - * Allow to specify search expressions, which allow to specify search - criterias by using a simple syntax. Can be specified in WebUI's search box - and via the API by using query parameter "_s". - (major: api, webui) - * A link reference is allowed to be a search expression. The WebUI will - render this as a link to a list of zettel that satisfy the search - expression. - (major: zettelmarkup, webui) - * A block transclusion is allowed to specify a search expression. When - evaluated, the transclusion is replaced by a list of zettel that satisfy - the search expression. - (major: zettelmarkup) - * When presenting a zettel list, allow to change the search expression. - (minor: webui) - * When evaluating a zettel, ignore transclusions if current user is not - allowed to read transcluded zettel. - (minor) - * Added a small tutorial for Zettelmarkup. - (minor: manual) - * Using URL query parameter to search for metdata values, specify an - ordering, an offset, and a limit for the resulting list, will be removed - in version 0.7. Replace these with the more useable search expressions. - Please be aware that the = search operator is also deprecated. It was only - introduced to help the migration. - (deprecated: api, webui) - * Some smaller bug fixes and improvements, to the software and to the - documentation. - - -

Changes for Version 0.5.1 (2022-08-02)

- * Log missing authentication tokens in debug level (was: sense level) - (major) - * Allow to use empty metadata values of string and zmk types. - (minor) - * Add IP address to some log messages, esp. when authentication fails. - (minor) - -

Changes for Version 0.5.0 (2022-07-29)

- * Removed zettel syntax “draw”. The new default syntax for - inline zettel is now “text”. A drawing can now be made by - using the “evaluation block” syntax (see below) by setting - the generic attribute to “draw”. - (breaking: zettelmarkup, api, webui) - * If authentication is enabled, a secret of at least 16 bytes must be set in - the startup configuration. - (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. - (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) - * Add support for an “evaluation block” syntax in Zettelmarkup - to allow interpretation of content by external software. - (minor: zettelmarkup) - * Add initial support for a TeX-like math-mode to Zettelmarkup (both block- - and inline-structured elements). Currently, support only the syntax, - but WebUI does not render these elements in a special way. - (minor: zettelmarkup) - * For block-structured elements, attributes may now span more than one line. - If a line ending occurs within a quoted attribute value, the line ending - characters are part of the attributes value. - (minor: zettelmarkup) - * Zettel 00000000029000 acts as a map of zettel roles to identifier of - zettel that are included as additional CSS. Allows to display zettel - differently depending on their role. Use case: slides that are processed - by Zettel Presenter will use the same CSS if they are rendered by - Zettelstore WebUI. - (minor: webui) - * A zettel can be saved while creating / editing it. There is no need to - manually re-edit it by using the 'e' endpoint. - (minor: webui) - * Zettel role and zettel syntax are backed by a HTML5 data list element - which lists supported and used values to help to enter a valid value. - (mirnor: webui) - * Allow to use startup configuration, even if started in simple mode. - (minor) - * Log authentication issues in level "sense"; add caller IP address to some - web server log messages. - (minor: web server) - * New startup configuration key max-request-size to limit a web - request body to prevent client sending too large requests. - (minor: web server) - * Many smaller bug fixes and improvements, to the software and to the - documentation. - - -

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. - 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 - ++...++ is obsolete. - (breaking: Zettelmarkup). - * Remove API call to parse Zettelmarkup texts and encode it as text and - HTML. Was call “POST /v”. It was needed to separately encode - the titles of zettel. The same effect can be achieved by fetching the - ZJSON representation and encode it using the function in the Zettelstore - client software. - (breaking: api) - * Remove API call to retrieve all links of an zettel. This can be done more - easily on the client side by traversing the ZJSON encoding of a zettel. - (breaking: api) - * ZJSON will encode metadata value as pairs of a metadata type and metadata - value. This allows a client to decode the associated value more easily. - (minor: api) - * A sequence of inline-structured elements can be marked, not just a point - in the Zettelmarkup text. - (minor: zettelmarkup) - * Metadata keys with suffix -title force their value to be - interpreted as Zettelmarkup. Similar, the suffix -set denotes - a set/list of words and the suffix -zids a set/list of zettel - 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 - 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 - loaded from file that contain the metdata value. - (minor: api, webui) - * Add an API call to retrieve the version of the Zettelstore. - (minor: api) - * Limit the amount of zettel and bytes to be stored in a memory box. Allows - to use it with public access. - (minor: box) - * Disallow to cache the authentication cookie. Will remove most unexpected - log-outs when using a mobile device. - (minor: webui) - * Many smaller bug fixes and 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 - 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) - * Add Zettelmarkup syntax to specify full transclusion of other zettel. - (major: Zettelmarkup) - * Add Zettelmarkup syntax to specify inline-zettel, both for - block-structured and for inline-structured elements. - (major: Zettelmarkup) - * Metadata-returning API calls additionally return an indication about - access rights for the given zettel. - (minor: api) - * A previously duplicate file that is now useful (because another file was - deleted) is now logged as such. - (minor: directory and file/zip box) - * Many smaller bug fixes and improvements, to the software and to the - documentation. - - -

Changes for Version 0.2 (2022-01-19)

- * v0.2.1 (2021-02-01) updates the license year in some documents - * 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. - (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. - (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. - (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. - (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. - (minor: api, webui) - * 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 - syntaxes. - (minor) - * Add API call to check for enabled authentication. - (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. - (minor: api) - * 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. - - -

Changes for Version 0.1 (2021-11-11)

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

Changes for Version 0.0.15 (2021-09-17)

- * Move again endpoint characters for authentication to make room for future - 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 - 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. - (major: api; breaking) - * Metadata values of type tag set (the metadata with key - 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. - (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. - (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. - (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.15 (pending)

+ +

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 @@ -943,21 +47,21 @@ directory, these characters are preserved when zettel is updated. (bug) * The phase “filtering a zettel list” is more precise “selecting zettel” (documentation) - * Many smaller bug fixes and improvements, to the software and to the + * Many smaller bug fixes and inprovements, 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) @@ -964,13 +68,12 @@ * 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) @@ -985,17 +88,17 @@ * 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 + * Many smaller bug fixes and inprovements, to the software and to the documentation. - +

Changes for Version 0.0.12 (2021-04-16)

* Raise the per-process limit of open files on macOS to 1.048.576. This allows most macOS users to use at least 500.000 zettel. That should be enough for the near future. (major) @@ -1003,18 +106,18 @@ 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) - +

Changes for Version 0.0.11 (2021-04-05)

* New box schema "file" allows to read zettel from a ZIP file. A zettel collection can now be packaged and distributed easier. (major: server) * Non-restricted search is a full-text search. The search string will be @@ -1022,11 +125,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 @@ -1042,118 +145,106 @@ * 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 inprovements, 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 inprovements, 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. @@ -1160,13 +251,12 @@ 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 @@ -1180,71 +270,70 @@ 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. @@ -1260,16 +349,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 @@ -1277,33 +366,32 @@ terminal, there is a hint about opening the web browser and use a specific URL. A Welcome zettel is created, to give some more information. (This change also applies to the server itself, but it is more suited to the WebUI user.) - +

Changes for Version 0.0.7 (2020-11-24)

* With this version, Zettelstore and this manual got a new license, the [https://joinup.ec.europa.eu/collection/eupl|European Union Public Licence] (EUPL), version 1.2 or later. Nothing else changed. If you want to stay with the old licenses (AGPLv3+, CC BY-SA 4.0), you are free to fork from the previous version. - +

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

@@ -1316,11 +404,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 @@ -1340,75 +428,73 @@ references”). When a local reference is displayed as an URL 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; + * (minor) Change the default value for some visual sugar putd after an + 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. - +

Changes for Version 0.0.5 (2020-10-22)

* Application Programming Interface (API) to allow external software to retrieve zettel data from the Zettelstore. * Specify boxes, where zettel are stored, via an URL. * 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.17.0 (2024-03-04). +Build: v0.0.14 (2021-07-23). - * [/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) + * [/uv/zettelstore-0.0.14-linux-amd64.zip|Linux] (amd64) + * [/uv/zettelstore-0.0.14-linux-arm.zip|Linux] (arm6, e.g. Raspberry Pi) + * [/uv/zettelstore-0.0.14-windows-amd64.zip|Windows] (amd64) + * [/uv/zettelstore-0.0.14-darwin-amd64.zip|macOS] (amd64) + * [/uv/zettelstore-0.0.14-darwin-arm64.zip|macOS] (arm64, aka Apple silicon) Unzip the appropriate file, install and execute Zettelstore according to the manual.

Zettel for the manual

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

Latest Release: 0.17.0 (2024-03-04)

+

Latest Release: 0.0.14 (2021-07-23)

* [./download.wiki|Download] - * [./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] + * [./changes.wiki#0_0_14|Change summary] + * [/timeline?p=version-0.0.14&bt=version-0.0.13&y=ci|Check-ins for version 0.0.14], + [/vdiff?to=version-0.0.14&from=version-0.0.13|content diff] + * [/timeline?df=version-0.0.14&y=ci|Check-ins derived from the 0.0.14 release], + [/vdiff?from=version-0.0.14&to=trunk|content diff] * [./plan.wiki|Limitations and planned improvements] * [/timeline?t=release|Timeline of all past releases]

Build instructions

-Just install [https://go.dev/dl/|Go] and some Go-based tools. Please read +Just install [https://golang.org/dl/|Go] and some Go-based tools. Please read the [./build.md|instructions] for details. * [/dir?ci=trunk|Source code] * [/download|Download the source code] as a tarball or a ZIP file (you must [/login|login] as user "anonymous"). Index: www/plan.wiki ================================================================== --- www/plan.wiki +++ www/plan.wiki @@ -1,20 +1,32 @@ Limitations and planned improvements Here is a list of some shortcomings of Zettelstore. They are planned to be solved. - * Zettelstore must have indexed all zettel to make use of queries. - Otherwise not all zettel may be returned. +

Serious limitations

+ * Content with binary data (e.g. a GIF, PNG, or JPG file) cannot be created + nor modified via the standard web interface. As a workaround, you should + put your file into the directory where your zettel are stored. Make sure + that the file name starts with unique 14 digits that make up the zettel + identifier. + * Automatic lists and transclusions are not supported in Zettelmarkup. + * … + +

Smaller limitations

* Quoted attribute values are not yet supported in Zettelmarkup: {key="value with space"}. + * The file sub-command currently does not support output format + “json”. * The horizontal tab character (U+0009) is not supported. * Missing support for citation keys. * Changing the content syntax is not reflected in file extension. * File names with additional text besides the zettel identifier are not always preserved. - * Some file systems differentiate filenames with different cases (e.g. some - on Linux, sometimes on macOS), others do not (default on macOS, most on - Windows). Zettelstore is not able to detect these differences. Do not put - files in your directory boxes and in files boxes that differ only by upper - / lower case letters. + * … + +

Planned improvements

+ * Support for mathematical content is missing, e.g. $$F(x) &= + \\int^a_b \\frac{1}{3}x^3$$. + * Render zettel in [https://pandoc.org|pandoc's] JSON version of + their native AST to make pandoc an external renderer for Zettelstore. * … DELETED zettel/content.go Index: zettel/content.go ================================================================== --- zettel/content.go +++ zettel/content.go @@ -1,125 +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. -// -// SPDX-License-Identifier: EUPL-1.2 -// SPDX-FileCopyrightText: 2020-present Detlef Stern -//----------------------------------------------------------------------------- - -package zettel - -import ( - "bytes" - "encoding/base64" - "errors" - "io" - "unicode" - "unicode/utf8" - - "t73f.de/r/zsc/input" -) - -// Content is just the content of a zettel. -type Content struct { - data []byte - isBinary bool -} - -// NewContent creates a new content from a string. -func NewContent(data []byte) Content { - return Content{data: data, isBinary: IsBinary(data)} -} - -// Length returns the number of bytes stored. -func (zc *Content) Length() int { return len(zc.data) } - -// Equal compares two content values. -func (zc *Content) Equal(o *Content) bool { - if zc == nil { - return o == nil - } - if zc.isBinary != o.isBinary { - return false - } - return bytes.Equal(zc.data, o.data) -} - -// Write it to a Writer -func (zc *Content) Write(w io.Writer) (int, error) { - return w.Write(zc.data) -} - -// AsString returns the content itself is a string. -func (zc *Content) AsString() string { return string(zc.data) } - -// AsBytes returns the content itself is a byte slice. -func (zc *Content) AsBytes() []byte { return zc.data } - -// IsBinary returns true if the content contains non-unicode values or is, -// interpreted a text, with a high probability binary content. -func (zc *Content) IsBinary() bool { return zc.isBinary } - -// TrimSpace remove some space character in content, if it is not binary content. -func (zc *Content) TrimSpace() { - if zc.isBinary { - return - } - inp := input.NewInput(zc.data) - pos := inp.Pos - for inp.Ch != input.EOS { - if input.IsEOLEOS(inp.Ch) { - inp.Next() - pos = inp.Pos - continue - } - if !input.IsSpace(inp.Ch) { - break - } - inp.Next() - } - zc.data = bytes.TrimRightFunc(inp.Src[pos:], unicode.IsSpace) -} - -// Encode content for future transmission. -func (zc *Content) Encode() (data, encoding string) { - if !zc.isBinary { - return zc.AsString(), "" - } - return base64.StdEncoding.EncodeToString(zc.data), "base64" -} - -// SetDecoded content to the decoded value of the given string. -func (zc *Content) SetDecoded(data, encoding string) error { - switch encoding { - case "": - zc.data = []byte(data) - case "base64": - decoded, err := base64.StdEncoding.DecodeString(data) - if err != nil { - return err - } - zc.data = decoded - default: - return errors.New("unknown encoding " + encoding) - } - zc.isBinary = IsBinary(zc.data) - return nil -} - -// IsBinary returns true if the given data appears to be non-text data. -func IsBinary(data []byte) bool { - if !utf8.Valid(data) { - return true - } - for i := range len(data) { - if data[i] == 0 { - return true - } - } - return false -} DELETED zettel/content_test.go Index: zettel/content_test.go ================================================================== --- zettel/content_test.go +++ zettel/content_test.go @@ -1,69 +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. -// -// SPDX-License-Identifier: EUPL-1.2 -// SPDX-FileCopyrightText: 2020-present Detlef Stern -//----------------------------------------------------------------------------- - -package zettel_test - -import ( - "testing" - - "zettelstore.de/z/zettel" -) - -func TestContentIsBinary(t *testing.T) { - t.Parallel() - td := []struct { - s string - exp bool - }{ - {"abc", false}, - {"äöü", false}, - {"", false}, - {string([]byte{0}), true}, - } - for i, tc := range td { - content := zettel.NewContent([]byte(tc.s)) - got := content.IsBinary() - if got != tc.exp { - t.Errorf("TC=%d: expected %v, got %v", i, tc.exp, got) - } - } -} - -func TestTrimSpace(t *testing.T) { - t.Parallel() - testcases := []struct { - in, exp string - }{ - {"", ""}, - {" ", ""}, - {"abc", "abc"}, - {" abc", " abc"}, - {"abc ", "abc"}, - {"abc \n", "abc"}, - {"abc\n ", "abc"}, - {"\nabc", "abc"}, - {" \nabc", "abc"}, - {" \n abc", " abc"}, - {" \n\n abc", " abc"}, - {" \n \n abc", " abc"}, - {" \n \n abc \n \n ", " abc"}, - } - for _, tc := range testcases { - c := zettel.NewContent([]byte(tc.in)) - c.TrimSpace() - got := c.AsString() - if got != tc.exp { - t.Errorf("TrimSpace(%q) should be %q, but got %q", tc.in, tc.exp, got) - } - } -} DELETED zettel/id/digraph.go Index: zettel/id/digraph.go ================================================================== --- zettel/id/digraph.go +++ zettel/id/digraph.go @@ -1,239 +0,0 @@ -//----------------------------------------------------------------------------- -// 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 id - -import ( - "maps" - "slices" -) - -// Digraph relates zettel identifier in a directional way. -type Digraph map[Zid]Set - -// AddVertex adds an edge / vertex to the digraph. -func (dg Digraph) AddVertex(zid Zid) Digraph { - if dg == nil { - return Digraph{zid: nil} - } - if _, found := dg[zid]; !found { - dg[zid] = nil - } - return dg -} - -// RemoveVertex removes a vertex and all its edges from the digraph. -func (dg Digraph) RemoveVertex(zid Zid) { - if len(dg) > 0 { - delete(dg, zid) - for vertex, closure := range dg { - dg[vertex] = closure.Remove(zid) - } - } -} - -// AddEdge adds a connection from `zid1` to `zid2`. -// Both vertices must be added before. Otherwise the function may panic. -func (dg Digraph) AddEdge(fromZid, toZid Zid) Digraph { - if dg == nil { - return Digraph{fromZid: Set(nil).Add(toZid), toZid: nil} - } - dg[fromZid] = dg[fromZid].Add(toZid) - return dg -} - -// AddEgdes adds all given `Edge`s to the digraph. -// -// In contrast to `AddEdge` the vertices must not exist before. -func (dg Digraph) AddEgdes(edges EdgeSlice) Digraph { - if dg == nil { - if len(edges) == 0 { - return nil - } - dg = make(Digraph, len(edges)) - } - for _, edge := range edges { - dg = dg.AddVertex(edge.From) - dg = dg.AddVertex(edge.To) - dg = dg.AddEdge(edge.From, edge.To) - } - return dg -} - -// Equal returns true if both digraphs have the same vertices and edges. -func (dg Digraph) Equal(other Digraph) bool { - return maps.EqualFunc(dg, other, func(cg, co Set) bool { return cg.Equal(co) }) -} - -// Clone a digraph. -func (dg Digraph) Clone() Digraph { - if len(dg) == 0 { - return nil - } - copyDG := make(Digraph, len(dg)) - for vertex, closure := range dg { - copyDG[vertex] = closure.Clone() - } - return copyDG -} - -// HasVertex returns true, if `zid` is a vertex of the digraph. -func (dg Digraph) HasVertex(zid Zid) bool { - if len(dg) == 0 { - return false - } - _, found := dg[zid] - return found -} - -// Vertices returns the set of all vertices. -func (dg Digraph) Vertices() Set { - if len(dg) == 0 { - return nil - } - verts := NewSetCap(len(dg)) - for vert := range dg { - verts.Add(vert) - } - return verts -} - -// Edges returns an unsorted slice of the edges of the digraph. -func (dg Digraph) Edges() (es EdgeSlice) { - for vert, closure := range dg { - for next := range closure { - es = append(es, Edge{From: vert, To: next}) - } - } - return es -} - -// Originators will return the set of all vertices that are not referenced -// a the to-part of an edge. -func (dg Digraph) Originators() Set { - if len(dg) == 0 { - return nil - } - origs := dg.Vertices() - for _, closure := range dg { - origs.Substract(closure) - } - return origs -} - -// Terminators returns the set of all vertices that does not reference -// other vertices. -func (dg Digraph) Terminators() (terms Set) { - for vert, closure := range dg { - if len(closure) == 0 { - terms = terms.Add(vert) - } - } - return terms -} - -// TransitiveClosure calculates the sub-graph that is reachable from `zid`. -func (dg Digraph) TransitiveClosure(zid Zid) (tc Digraph) { - if len(dg) == 0 { - return nil - } - var marked Set - stack := Slice{zid} - for pos := len(stack) - 1; pos >= 0; pos = len(stack) - 1 { - curr := stack[pos] - stack = stack[:pos] - if marked.Contains(curr) { - continue - } - tc = tc.AddVertex(curr) - for next := range dg[curr] { - tc = tc.AddVertex(next) - tc = tc.AddEdge(curr, next) - stack = append(stack, next) - } - marked = marked.Add(curr) - } - return tc -} - -// ReachableVertices calculates the set of all vertices that are reachable -// from the given `zid`. -func (dg Digraph) ReachableVertices(zid Zid) (tc Set) { - if len(dg) == 0 { - return nil - } - stack := dg[zid].Sorted() - for last := len(stack) - 1; last >= 0; last = len(stack) - 1 { - curr := stack[last] - stack = stack[:last] - if tc.Contains(curr) { - continue - } - closure, found := dg[curr] - if !found { - continue - } - tc = tc.Add(curr) - for next := range closure { - stack = append(stack, next) - } - } - return tc -} - -// IsDAG returns a vertex and false, if the graph has a cycle containing the vertex. -func (dg Digraph) IsDAG() (Zid, bool) { - for vertex := range dg { - if dg.ReachableVertices(vertex).Contains(vertex) { - return vertex, false - } - } - return Invalid, true -} - -// Reverse returns a graph with reversed edges. -func (dg Digraph) Reverse() (revDg Digraph) { - for vertex, closure := range dg { - revDg = revDg.AddVertex(vertex) - for next := range closure { - revDg = revDg.AddVertex(next) - revDg = revDg.AddEdge(next, vertex) - } - } - return revDg -} - -// SortReverse returns a deterministic, topological, reverse sort of the -// digraph. -// -// Works only if digraph is a DAG. Otherwise the algorithm will not terminate -// or returns an arbitrary value. -func (dg Digraph) SortReverse() (sl Slice) { - if len(dg) == 0 { - return nil - } - tempDg := dg.Clone() - for len(tempDg) > 0 { - terms := tempDg.Terminators() - if len(terms) == 0 { - break - } - termSlice := terms.Sorted() - slices.Reverse(termSlice) - sl = append(sl, termSlice...) - for t := range terms { - tempDg.RemoveVertex(t) - } - } - return sl -} DELETED zettel/id/digraph_test.go Index: zettel/id/digraph_test.go ================================================================== --- zettel/id/digraph_test.go +++ zettel/id/digraph_test.go @@ -1,178 +0,0 @@ -//----------------------------------------------------------------------------- -// 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 id_test - -import ( - "testing" - - "zettelstore.de/z/zettel/id" -) - -type zps = id.EdgeSlice - -func createDigraph(pairs zps) (dg id.Digraph) { - return dg.AddEgdes(pairs) -} - -func TestDigraphOriginators(t *testing.T) { - t.Parallel() - testcases := []struct { - name string - dg id.EdgeSlice - orig id.Set - term id.Set - }{ - {"empty", nil, nil, nil}, - {"single", zps{{0, 1}}, id.NewSet(0), id.NewSet(1)}, - {"chain", zps{{0, 1}, {1, 2}, {2, 3}}, id.NewSet(0), id.NewSet(3)}, - } - for _, tc := range testcases { - t.Run(tc.name, func(t *testing.T) { - dg := createDigraph(tc.dg) - if got := dg.Originators(); !tc.orig.Equal(got) { - t.Errorf("Originators: expected:\n%v, but got:\n%v", tc.orig, got) - } - if got := dg.Terminators(); !tc.term.Equal(got) { - t.Errorf("Termintors: expected:\n%v, but got:\n%v", tc.orig, got) - } - }) - } -} - -func TestDigraphReachableVertices(t *testing.T) { - t.Parallel() - testcases := []struct { - name string - pairs id.EdgeSlice - start id.Zid - exp id.Set - }{ - {"nil", nil, 0, nil}, - {"0-2", zps{{1, 2}, {2, 3}}, 1, id.NewSet(2, 3)}, - {"1,2", zps{{1, 2}, {2, 3}}, 2, id.NewSet(3)}, - {"0-2,1-2", zps{{1, 2}, {2, 3}, {1, 3}}, 1, id.NewSet(2, 3)}, - {"0-2,1-2/1", zps{{1, 2}, {2, 3}, {1, 3}}, 2, id.NewSet(3)}, - {"0-2,1-2/2", zps{{1, 2}, {2, 3}, {1, 3}}, 3, nil}, - {"0-2,1-2,3*", zps{{1, 2}, {2, 3}, {1, 3}, {4, 4}}, 1, id.NewSet(2, 3)}, - } - - for _, tc := range testcases { - t.Run(tc.name, func(t *testing.T) { - dg := createDigraph(tc.pairs) - if got := dg.ReachableVertices(tc.start); !got.Equal(tc.exp) { - t.Errorf("\n%v, but got:\n%v", tc.exp, got) - } - - }) - } -} - -func TestDigraphTransitiveClosure(t *testing.T) { - t.Parallel() - testcases := []struct { - name string - pairs id.EdgeSlice - start id.Zid - exp id.EdgeSlice - }{ - {"nil", nil, 0, nil}, - {"1-3", zps{{1, 2}, {2, 3}}, 1, zps{{1, 2}, {2, 3}}}, - {"1,2", zps{{1, 1}, {2, 3}}, 2, zps{{2, 3}}}, - {"0-2,1-2", zps{{1, 2}, {2, 3}, {1, 3}}, 1, zps{{1, 2}, {1, 3}, {2, 3}}}, - {"0-2,1-2/1", zps{{1, 2}, {2, 3}, {1, 3}}, 1, zps{{1, 2}, {1, 3}, {2, 3}}}, - {"0-2,1-2/2", zps{{1, 2}, {2, 3}, {1, 3}}, 2, zps{{2, 3}}}, - {"0-2,1-2,3*", zps{{1, 2}, {2, 3}, {1, 3}, {4, 4}}, 1, zps{{1, 2}, {1, 3}, {2, 3}}}, - } - - for _, tc := range testcases { - t.Run(tc.name, func(t *testing.T) { - dg := createDigraph(tc.pairs) - if got := dg.TransitiveClosure(tc.start).Edges().Sort(); !got.Equal(tc.exp) { - t.Errorf("\n%v, but got:\n%v", tc.exp, got) - } - }) - } -} - -func TestIsDAG(t *testing.T) { - t.Parallel() - testcases := []struct { - name string - dg id.EdgeSlice - exp bool - }{ - {"empty", nil, true}, - {"single-edge", zps{{1, 2}}, true}, - {"single-loop", zps{{1, 1}}, false}, - {"long-loop", zps{{1, 2}, {2, 3}, {3, 4}, {4, 5}, {5, 2}}, false}, - } - for _, tc := range testcases { - t.Run(tc.name, func(t *testing.T) { - if zid, got := createDigraph(tc.dg).IsDAG(); got != tc.exp { - t.Errorf("expected %v, but got %v (%v)", tc.exp, got, zid) - } - }) - } -} - -func TestDigraphReverse(t *testing.T) { - t.Parallel() - testcases := []struct { - name string - dg id.EdgeSlice - exp id.EdgeSlice - }{ - {"empty", nil, nil}, - {"single-edge", zps{{1, 2}}, zps{{2, 1}}}, - {"single-loop", zps{{1, 1}}, zps{{1, 1}}}, - {"end-loop", zps{{1, 2}, {2, 2}}, zps{{2, 1}, {2, 2}}}, - {"long-loop", zps{{1, 2}, {2, 3}, {3, 4}, {4, 5}, {5, 2}}, zps{{2, 1}, {2, 5}, {3, 2}, {4, 3}, {5, 4}}}, - {"sect-loop", zps{{1, 2}, {2, 3}, {3, 4}, {4, 5}, {4, 2}}, zps{{2, 1}, {2, 4}, {3, 2}, {4, 3}, {5, 4}}}, - {"two-islands", zps{{1, 2}, {2, 3}, {4, 5}}, zps{{2, 1}, {3, 2}, {5, 4}}}, - {"direct-indirect", zps{{1, 2}, {1, 3}, {3, 2}}, zps{{2, 1}, {2, 3}, {3, 1}}}, - } - for _, tc := range testcases { - t.Run(tc.name, func(t *testing.T) { - dg := createDigraph(tc.dg) - if got := dg.Reverse().Edges().Sort(); !got.Equal(tc.exp) { - t.Errorf("\n%v, but got:\n%v", tc.exp, got) - } - }) - } -} - -func TestDigraphSortReverse(t *testing.T) { - t.Parallel() - testcases := []struct { - name string - dg id.EdgeSlice - exp id.Slice - }{ - {"empty", nil, nil}, - {"single-edge", zps{{1, 2}}, id.Slice{2, 1}}, - {"single-loop", zps{{1, 1}}, nil}, - {"end-loop", zps{{1, 2}, {2, 2}}, id.Slice{}}, - {"long-loop", zps{{1, 2}, {2, 3}, {3, 4}, {4, 5}, {5, 2}}, id.Slice{}}, - {"sect-loop", zps{{1, 2}, {2, 3}, {3, 4}, {4, 5}, {4, 2}}, id.Slice{5}}, - {"two-islands", zps{{1, 2}, {2, 3}, {4, 5}}, id.Slice{5, 3, 4, 2, 1}}, - {"direct-indirect", zps{{1, 2}, {1, 3}, {3, 2}}, id.Slice{2, 3, 1}}, - } - for _, tc := range testcases { - t.Run(tc.name, func(t *testing.T) { - if got := createDigraph(tc.dg).SortReverse(); !got.Equal(tc.exp) { - t.Errorf("expected:\n%v, but got:\n%v", tc.exp, got) - } - }) - } -} DELETED zettel/id/edge.go Index: zettel/id/edge.go ================================================================== --- zettel/id/edge.go +++ zettel/id/edge.go @@ -1,49 +0,0 @@ -//----------------------------------------------------------------------------- -// 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 id - -import "slices" - -// Edge is a pair of to vertices. -type Edge struct { - From, To Zid -} - -// EdgeSlice is a slice of Edges -type EdgeSlice []Edge - -// Equal return true if both slices are the same. -func (es EdgeSlice) Equal(other EdgeSlice) bool { - return slices.Equal(es, other) -} - -// Sort the slice. -func (es EdgeSlice) Sort() EdgeSlice { - slices.SortFunc(es, func(e1, e2 Edge) int { - if e1.From < e2.From { - return -1 - } - if e1.From > e2.From { - return 1 - } - if e1.To < e2.To { - return -1 - } - if e1.To > e2.To { - return 1 - } - return 0 - }) - return es -} DELETED zettel/id/id.go Index: zettel/id/id.go ================================================================== --- zettel/id/id.go +++ zettel/id/id.go @@ -1,167 +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. -// -// 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 - -import ( - "strconv" - "time" - - "t73f.de/r/zsc/api" -) - -// Zid is the internal identifier of a zettel. Typically, it is a -// time stamp of the form "YYYYMMDDHHmmSS" converted to an unsigned integer. -// A zettelstore implementation should try to set the last two digits to zero, -// e.g. the seconds should be zero, -type Zid uint64 - -// Some important ZettelIDs. -const ( - Invalid = Zid(0) // Invalid is a Zid that will never be valid -) - -// ZettelIDs that are used as Zid more than once. -// -// Note: if you change some values, ensure that you also change them in the -// Constant box. They are mentioned there literally, because these -// constants are not available there. -var ( - ConfigurationZid = MustParse(api.ZidConfiguration) - BaseTemplateZid = MustParse(api.ZidBaseTemplate) - LoginTemplateZid = MustParse(api.ZidLoginTemplate) - ListTemplateZid = MustParse(api.ZidListTemplate) - ZettelTemplateZid = MustParse(api.ZidZettelTemplate) - InfoTemplateZid = MustParse(api.ZidInfoTemplate) - FormTemplateZid = MustParse(api.ZidFormTemplate) - RenameTemplateZid = MustParse(api.ZidRenameTemplate) - DeleteTemplateZid = MustParse(api.ZidDeleteTemplate) - ErrorTemplateZid = MustParse(api.ZidErrorTemplate) - StartSxnZid = MustParse(api.ZidSxnStart) - BaseSxnZid = MustParse(api.ZidSxnBase) - PreludeSxnZid = MustParse(api.ZidSxnPrelude) - EmojiZid = MustParse(api.ZidEmoji) - TOCNewTemplateZid = MustParse(api.ZidTOCNewTemplate) - DefaultHomeZid = MustParse(api.ZidDefaultHome) -) - -const maxZid = 99999999999999 - -// ParseUint interprets a string as a possible zettel identifier -// and returns its integer value. -func ParseUint(s string) (uint64, error) { - res, err := strconv.ParseUint(s, 10, 47) - if err != nil { - return 0, err - } - if res == 0 || res > maxZid { - return res, strconv.ErrRange - } - return res, nil -} - -// Parse interprets a string as a zettel identification and -// returns its value. -func Parse(s string) (Zid, error) { - if len(s) != 14 { - return Invalid, strconv.ErrSyntax - } - res, err := ParseUint(s) - if err != nil { - return Invalid, err - } - return Zid(res), nil -} - -// MustParse tries to interpret a string as a zettel identifier and returns -// its value or panics otherwise. -func MustParse(s api.ZettelID) Zid { - zid, err := Parse(string(s)) - if err == nil { - return zid - } - panic(err) -} - -// String converts the zettel identification to a string of 14 digits. -// Only defined for valid ids. -func (zid Zid) String() string { - 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) - return result[:] -} - -// toByteArray converts the Zid into a fixed byte array, usable for printing. -// -// Based on idea by Daniel Lemire: "Converting integers to fix-digit representations quickly" -// https://lemire.me/blog/2021/11/18/converting-integers-to-fix-digit-representations-quickly/ -func (zid Zid) toByteArray(result *[14]byte) { - date := uint64(zid) / 1000000 - fullyear := date / 10000 - century, year := fullyear/100, fullyear%100 - monthday := date % 10000 - month, day := monthday/100, monthday%100 - time := uint64(zid) % 1000000 - hmtime, second := time/100, time%100 - hour, minute := hmtime/100, hmtime%100 - - result[0] = byte(century/10) + '0' - result[1] = byte(century%10) + '0' - result[2] = byte(year/10) + '0' - result[3] = byte(year%10) + '0' - result[4] = byte(month/10) + '0' - result[5] = byte(month%10) + '0' - result[6] = byte(day/10) + '0' - result[7] = byte(day%10) + '0' - result[8] = byte(hour/10) + '0' - result[9] = byte(hour%10) + '0' - result[10] = byte(minute/10) + '0' - result[11] = byte(minute%10) + '0' - result[12] = byte(second/10) + '0' - result[13] = byte(second%10) + '0' -} - -// IsValid determines if zettel id is a valid one, e.g. consists of max. 14 digits. -func (zid Zid) IsValid() bool { return 0 < zid && zid <= maxZid } - -// TimestampLayout to transform a date into a Zid and into other internal dates. -const TimestampLayout = "20060102150405" - -// New returns a new zettel id based on the current time. -func New(withSeconds bool) Zid { - now := time.Now().Local() - var s string - if withSeconds { - s = now.Format(TimestampLayout) - } else { - s = now.Format("20060102150400") - } - res, err := Parse(s) - if err != nil { - panic(err) - } - return res -} DELETED zettel/id/id_test.go Index: zettel/id/id_test.go ================================================================== --- zettel/id/id_test.go +++ zettel/id/id_test.go @@ -1,92 +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. -// -// 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 - -import ( - "testing" - - "zettelstore.de/z/zettel/id" -) - -func TestIsValid(t *testing.T) { - t.Parallel() - validIDs := []string{ - "00000000000001", - "00000000000020", - "00000000000300", - "00000000004000", - "00000000050000", - "00000000600000", - "00000007000000", - "00000080000000", - "00000900000000", - "00001000000000", - "00020000000000", - "00300000000000", - "04000000000000", - "50000000000000", - "99999999999999", - "00001007030200", - "20200310195100", - "12345678901234", - } - - for i, sid := range validIDs { - zid, err := id.Parse(sid) - if err != nil { - t.Errorf("i=%d: sid=%q is not valid, but should be. err=%v", i, sid, err) - } - s := zid.String() - if s != sid { - t.Errorf( - "i=%d: zid=%v does not format to %q, but to %q", i, zid, sid, s) - } - } - - invalidIDs := []string{ - "", "0", "a", - "00000000000000", - "0000000000000a", - "000000000000000", - "20200310T195100", - "+1234567890123", - } - - for i, sid := range invalidIDs { - if zid, err := id.Parse(sid); err == nil { - t.Errorf("i=%d: sid=%q is valid (zid=%s), but should not be", i, sid, zid) - } - } -} - -var sResult string // to disable compiler optimization in loop below - -func BenchmarkString(b *testing.B) { - var s string - 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 range b.N { - bs = id.Zid(12345678901200).Bytes() - } - bResult = bs -} DELETED zettel/id/set.go Index: zettel/id/set.go ================================================================== --- zettel/id/set.go +++ zettel/id/set.go @@ -1,181 +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. -// -// SPDX-License-Identifier: EUPL-1.2 -// SPDX-FileCopyrightText: 2021-present Detlef Stern -//----------------------------------------------------------------------------- - -package id - -import ( - "maps" - "strings" -) - -// Set is a set of zettel identifier -type Set map[Zid]struct{} - -// String returns a string representation of the map. -func (s Set) String() string { - if s == nil { - return "{}" - } - var sb strings.Builder - sb.WriteByte('{') - for i, zid := range s.Sorted() { - if i > 0 { - sb.WriteByte(' ') - } - sb.Write(zid.Bytes()) - } - sb.WriteByte('}') - return sb.String() -} - -// NewSet returns a new set of identifier with the given initial values. -func NewSet(zids ...Zid) Set { - l := len(zids) - if l < 8 { - l = 8 - } - result := make(Set, l) - result.CopySlice(zids) - return result -} - -// NewSetCap returns a new set of identifier with the given capacity and initial values. -func NewSetCap(c int, zids ...Zid) Set { - l := len(zids) - if c < l { - c = l - } - if c < 8 { - c = 8 - } - result := make(Set, c) - result.CopySlice(zids) - return result -} - -// Clone returns a copy of the given set. -func (s Set) Clone() Set { - if len(s) == 0 { - return nil - } - return maps.Clone(s) -} - -// Add adds a Add to the set. -func (s Set) Add(zid Zid) Set { - if s == nil { - return NewSet(zid) - } - s[zid] = struct{}{} - return s -} - -// Contains return true if the set is non-nil and the set contains the given Zettel identifier. -func (s Set) Contains(zid Zid) bool { - if s != nil { - _, found := s[zid] - return found - } - return false -} - -// ContainsOrNil return true if the set is nil or if the set contains the given Zettel identifier. -func (s Set) ContainsOrNil(zid Zid) bool { - if s != nil { - _, found := s[zid] - return found - } - return true -} - -// Copy adds all member from the other set. -func (s Set) Copy(other Set) Set { - if s == nil { - if len(other) == 0 { - return nil - } - s = NewSetCap(len(other)) - } - maps.Copy(s, other) - return s -} - -// CopySlice adds all identifier of the given slice to the set. -func (s Set) CopySlice(sl Slice) Set { - if s == nil { - s = NewSetCap(len(sl)) - } - for _, zid := range sl { - s[zid] = struct{}{} - } - return s -} - -// Sorted returns the set as a sorted slice of zettel identifier. -func (s Set) Sorted() Slice { - if l := len(s); l > 0 { - result := make(Slice, 0, l) - for zid := range s { - result = append(result, zid) - } - result.Sort() - return result - } - return nil -} - -// IntersectOrSet removes all zettel identifier that are not in the other set. -// Both sets can be modified by this method. One of them is the set returned. -// It contains the intersection of both, if s is not nil. -// -// If s == nil, then the other set is always returned. -func (s Set) IntersectOrSet(other Set) Set { - if s == nil { - return other - } - if len(s) > len(other) { - s, other = other, s - } - for zid := range s { - _, otherOk := other[zid] - if !otherOk { - delete(s, zid) - } - } - return s -} - -// Substract removes all zettel identifier from 's' that are in the set 'other'. -func (s Set) Substract(other Set) { - if s == nil || other == nil { - return - } - for zid := range other { - delete(s, zid) - } -} - -// Remove the identifier from the set. -func (s Set) Remove(zid Zid) Set { - if len(s) == 0 { - return nil - } - delete(s, zid) - if len(s) == 0 { - return nil - } - return s -} - -// Equal returns true if the other set is equal to the given set. -func (s Set) Equal(other Set) bool { return maps.Equal(s, other) } DELETED zettel/id/set_test.go Index: zettel/id/set_test.go ================================================================== --- zettel/id/set_test.go +++ zettel/id/set_test.go @@ -1,152 +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. -// -// SPDX-License-Identifier: EUPL-1.2 -// SPDX-FileCopyrightText: 2021-present Detlef Stern -//----------------------------------------------------------------------------- - -package id_test - -import ( - "testing" - - "zettelstore.de/z/zettel/id" -) - -func TestSetContains(t *testing.T) { - t.Parallel() - testcases := []struct { - s id.Set - zid id.Zid - exp bool - }{ - {nil, id.Invalid, true}, - {nil, 14, true}, - {id.NewSet(), id.Invalid, false}, - {id.NewSet(), 1, false}, - {id.NewSet(), id.Invalid, false}, - {id.NewSet(1), 1, true}, - } - for i, tc := range testcases { - got := tc.s.ContainsOrNil(tc.zid) - if got != tc.exp { - t.Errorf("%d: %v.Contains(%v) == %v, but got %v", i, tc.s, tc.zid, tc.exp, got) - } - } -} - -func TestSetAdd(t *testing.T) { - t.Parallel() - testcases := []struct { - s1, s2 id.Set - exp id.Slice - }{ - {nil, nil, nil}, - {id.NewSet(), nil, nil}, - {id.NewSet(), id.NewSet(), nil}, - {nil, id.NewSet(1), id.Slice{1}}, - {id.NewSet(1), nil, id.Slice{1}}, - {id.NewSet(1), id.NewSet(), id.Slice{1}}, - {id.NewSet(1), id.NewSet(2), id.Slice{1, 2}}, - {id.NewSet(1), id.NewSet(1), id.Slice{1}}, - } - for i, tc := range testcases { - sl1 := tc.s1.Sorted() - sl2 := tc.s2.Sorted() - got := tc.s1.Copy(tc.s2).Sorted() - if !got.Equal(tc.exp) { - t.Errorf("%d: %v.Add(%v) should be %v, but got %v", i, sl1, sl2, tc.exp, got) - } - } -} - -func TestSetSorted(t *testing.T) { - t.Parallel() - testcases := []struct { - set id.Set - exp id.Slice - }{ - {nil, nil}, - {id.NewSet(), nil}, - {id.NewSet(9, 4, 6, 1, 7), id.Slice{1, 4, 6, 7, 9}}, - } - for i, tc := range testcases { - got := tc.set.Sorted() - if !got.Equal(tc.exp) { - t.Errorf("%d: %v.Sorted() should be %v, but got %v", i, tc.set, tc.exp, got) - } - } -} - -func TestSetIntersectOrSet(t *testing.T) { - t.Parallel() - testcases := []struct { - s1, s2 id.Set - exp id.Slice - }{ - {nil, nil, nil}, - {id.NewSet(), nil, nil}, - {nil, id.NewSet(), nil}, - {id.NewSet(), id.NewSet(), nil}, - {id.NewSet(1), nil, nil}, - {nil, id.NewSet(1), id.Slice{1}}, - {id.NewSet(1), id.NewSet(), nil}, - {id.NewSet(), id.NewSet(1), nil}, - {id.NewSet(1), id.NewSet(2), nil}, - {id.NewSet(2), id.NewSet(1), nil}, - {id.NewSet(1), id.NewSet(1), id.Slice{1}}, - } - for i, tc := range testcases { - sl1 := tc.s1.Sorted() - sl2 := tc.s2.Sorted() - got := tc.s1.IntersectOrSet(tc.s2).Sorted() - if !got.Equal(tc.exp) { - t.Errorf("%d: %v.IntersectOrSet(%v) should be %v, but got %v", i, sl1, sl2, tc.exp, got) - } - } -} - -func TestSetRemove(t *testing.T) { - t.Parallel() - testcases := []struct { - s1, s2 id.Set - exp id.Slice - }{ - {nil, nil, nil}, - {id.NewSet(), nil, nil}, - {id.NewSet(), id.NewSet(), nil}, - {id.NewSet(1), nil, id.Slice{1}}, - {id.NewSet(1), id.NewSet(), id.Slice{1}}, - {id.NewSet(1), id.NewSet(2), id.Slice{1}}, - {id.NewSet(1), id.NewSet(1), id.Slice{}}, - } - for i, tc := range testcases { - sl1 := tc.s1.Sorted() - sl2 := tc.s2.Sorted() - newS1 := id.NewSet(sl1...) - newS1.Substract(tc.s2) - got := newS1.Sorted() - if !got.Equal(tc.exp) { - t.Errorf("%d: %v.Remove(%v) should be %v, but got %v", i, sl1, sl2, tc.exp, got) - } - } -} - -// func BenchmarkSet(b *testing.B) { -// s := id.Set{} -// for range b.N { -// s[id.Zid(i)] = true -// } -// } -func BenchmarkSet(b *testing.B) { - s := id.Set{} - for i := range b.N { - s[id.Zid(i)] = struct{}{} - } -} DELETED zettel/id/slice.go Index: zettel/id/slice.go ================================================================== --- zettel/id/slice.go +++ zettel/id/slice.go @@ -1,46 +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. -// -// SPDX-License-Identifier: EUPL-1.2 -// SPDX-FileCopyrightText: 2021-present Detlef Stern -//----------------------------------------------------------------------------- - -package id - -import ( - "slices" - "strings" -) - -// Slice is a sequence of zettel identifier. A special case is a sorted slice. -type Slice []Zid - -// Sort a slice of Zids. -func (zs Slice) Sort() { slices.Sort(zs) } - -// Clone a zettel identifier slice -func (zs Slice) Clone() Slice { return slices.Clone(zs) } - -// Equal reports whether zs and other are the same length and contain the samle zettel -// identifier. A nil argument is equivalent to an empty slice. -func (zs Slice) Equal(other Slice) bool { return slices.Equal(zs, other) } - -func (zs Slice) String() string { - if len(zs) == 0 { - return "" - } - var sb strings.Builder - for i, zid := range zs { - if i > 0 { - sb.WriteByte(' ') - } - sb.WriteString(zid.String()) - } - return sb.String() -} DELETED zettel/id/slice_test.go Index: zettel/id/slice_test.go ================================================================== --- zettel/id/slice_test.go +++ zettel/id/slice_test.go @@ -1,89 +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. -// -// SPDX-License-Identifier: EUPL-1.2 -// SPDX-FileCopyrightText: 2021-present Detlef Stern -//----------------------------------------------------------------------------- - -package id_test - -import ( - "testing" - - "zettelstore.de/z/zettel/id" -) - -func TestSliceSort(t *testing.T) { - t.Parallel() - zs := id.Slice{9, 4, 6, 1, 7} - zs.Sort() - exp := id.Slice{1, 4, 6, 7, 9} - if !zs.Equal(exp) { - t.Errorf("Slice.Sort did not work. Expected %v, got %v", exp, zs) - } -} - -func TestCopy(t *testing.T) { - t.Parallel() - var orig id.Slice - got := orig.Clone() - if got != nil { - t.Errorf("Nil copy resulted in %v", got) - } - orig = id.Slice{9, 4, 6, 1, 7} - got = orig.Clone() - if !orig.Equal(got) { - t.Errorf("Slice.Copy did not work. Expected %v, got %v", orig, got) - } -} - -func TestSliceEqual(t *testing.T) { - t.Parallel() - testcases := []struct { - s1, s2 id.Slice - exp bool - }{ - {nil, nil, true}, - {nil, id.Slice{}, true}, - {nil, id.Slice{1}, false}, - {id.Slice{1}, id.Slice{1}, true}, - {id.Slice{1}, id.Slice{2}, false}, - {id.Slice{1, 2}, id.Slice{2, 1}, false}, - {id.Slice{1, 2}, id.Slice{1, 2}, true}, - } - for i, tc := range testcases { - got := tc.s1.Equal(tc.s2) - if got != tc.exp { - t.Errorf("%d/%v.Equal(%v)==%v, but got %v", i, tc.s1, tc.s2, tc.exp, got) - } - got = tc.s2.Equal(tc.s1) - if got != tc.exp { - t.Errorf("%d/%v.Equal(%v)==%v, but got %v", i, tc.s2, tc.s1, tc.exp, got) - } - } -} - -func TestSliceString(t *testing.T) { - t.Parallel() - testcases := []struct { - in id.Slice - exp string - }{ - {nil, ""}, - {id.Slice{}, ""}, - {id.Slice{1}, "00000000000001"}, - {id.Slice{1, 2}, "00000000000001 00000000000002"}, - } - for i, tc := range testcases { - got := tc.in.String() - if got != tc.exp { - t.Errorf("%d/%v: expected %q, but got %q", i, tc.in, tc.exp, got) - } - } -} DELETED zettel/meta/collection.go Index: zettel/meta/collection.go ================================================================== --- zettel/meta/collection.go +++ zettel/meta/collection.go @@ -1,113 +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. -// -// SPDX-License-Identifier: EUPL-1.2 -// SPDX-FileCopyrightText: 2022-present Detlef Stern -//----------------------------------------------------------------------------- - -package meta - -import "sort" - -// Arrangement stores metadata within its categories. -// Typecally a category might be a tag name, a role name, a syntax value. -type Arrangement map[string][]*Meta - -// CreateArrangement by inspecting a given key and use the found -// value as a category. -func CreateArrangement(metaList []*Meta, key string) Arrangement { - if len(metaList) == 0 { - return nil - } - descr := Type(key) - if descr == nil { - return nil - } - if descr.IsSet { - return createSetArrangement(metaList, key) - } - return createSimplearrangement(metaList, key) -} - -func createSetArrangement(metaList []*Meta, key string) Arrangement { - a := make(Arrangement) - for _, m := range metaList { - if vals, ok := m.GetList(key); ok { - for _, val := range vals { - a[val] = append(a[val], m) - } - } - } - return a -} - -func createSimplearrangement(metaList []*Meta, key string) Arrangement { - a := make(Arrangement) - for _, m := range metaList { - if val, ok := m.Get(key); ok && val != "" { - a[val] = append(a[val], m) - } - } - return a -} - -// Counted returns the list of categories, together with the number of -// metadata for each category. -func (a Arrangement) Counted() CountedCategories { - if len(a) == 0 { - return nil - } - result := make(CountedCategories, 0, len(a)) - for cat, metas := range a { - result = append(result, CountedCategory{Name: cat, Count: len(metas)}) - } - return result -} - -// CountedCategory contains of a name and the number how much this name occured -// somewhere. -type CountedCategory struct { - Name string - Count int -} - -// CountedCategories is the list of CountedCategories. -// Every name must occur only once. -type CountedCategories []CountedCategory - -// SortByName sorts the list by the name attribute. -// Since each name must occur only once, two CountedCategories cannot have -// the same name. -func (ccs CountedCategories) SortByName() { - sort.Slice(ccs, func(i, j int) bool { return ccs[i].Name < ccs[j].Name }) -} - -// SortByCount sorts the list by the count attribute, descending. -// If two counts are equal, elements are sorted by name. -func (ccs CountedCategories) SortByCount() { - sort.Slice(ccs, func(i, j int) bool { - iCount, jCount := ccs[i].Count, ccs[j].Count - if iCount > jCount { - return true - } - if iCount == jCount { - return ccs[i].Name < ccs[j].Name - } - return false - }) -} - -// Categories returns just the category names. -func (ccs CountedCategories) Categories() []string { - result := make([]string, len(ccs)) - for i, cc := range ccs { - result[i] = cc.Name - } - return result -} DELETED zettel/meta/meta.go Index: zettel/meta/meta.go ================================================================== --- zettel/meta/meta.go +++ zettel/meta/meta.go @@ -1,418 +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. -// -// SPDX-License-Identifier: EUPL-1.2 -// SPDX-FileCopyrightText: 2020-present Detlef Stern -//----------------------------------------------------------------------------- - -// Package meta provides the zettel specific type 'meta'. -package meta - -import ( - "regexp" - "sort" - "strings" - "unicode" - "unicode/utf8" - - "t73f.de/r/zsc/api" - "t73f.de/r/zsc/input" - "t73f.de/r/zsc/maps" - "zettelstore.de/z/strfun" - "zettelstore.de/z/zettel/id" -) - -type keyUsage int - -const ( - _ keyUsage = iota - usageUser // Key will be manipulated by the user - usageComputed // Key is computed by zettelstore - usageProperty // Key is computed and not stored by zettelstore -) - -// DescriptionKey formally describes each supported metadata key. -type DescriptionKey struct { - Name string - Type *DescriptionType - usage keyUsage - Inverse string -} - -// IsComputed returns true, if metadata is computed and not set by the user. -func (kd *DescriptionKey) IsComputed() bool { return kd.usage >= usageComputed } - -// IsProperty returns true, if metadata is a computed property. -func (kd *DescriptionKey) IsProperty() bool { return kd.usage >= usageProperty } - -var registeredKeys = make(map[string]*DescriptionKey) - -func registerKey(name string, t *DescriptionType, usage keyUsage, inverse string) { - if _, ok := registeredKeys[name]; ok { - panic("Key '" + name + "' already defined") - } - if inverse != "" { - if t != TypeID && t != TypeIDSet { - panic("Inversable key '" + name + "' is not identifier type, but " + t.String()) - } - inv, ok := registeredKeys[inverse] - if !ok { - panic("Inverse Key '" + inverse + "' not found") - } - if !inv.IsComputed() { - panic("Inverse Key '" + inverse + "' is not computed.") - } - if inv.Type != TypeIDSet { - panic("Inverse Key '" + inverse + "' is not an identifier set, but " + inv.Type.String()) - } - } - registeredKeys[name] = &DescriptionKey{name, t, usage, inverse} -} - -// IsComputed returns true, if key denotes a computed metadata key. -func IsComputed(name string) bool { - if kd, ok := registeredKeys[name]; ok { - return kd.IsComputed() - } - return false -} - -// IsProperty returns true, if key denotes a property metadata value. -func IsProperty(name string) bool { - if kd, ok := registeredKeys[name]; ok { - return kd.IsProperty() - } - return false -} - -// Inverse returns the name of the inverse key. -func Inverse(name string) string { - if kd, ok := registeredKeys[name]; ok { - return kd.Inverse - } - return "" -} - -// GetDescription returns the key description object of the given key name. -func GetDescription(name string) DescriptionKey { - if d, ok := registeredKeys[name]; ok { - return *d - } - return DescriptionKey{Type: Type(name)} -} - -// GetSortedKeyDescriptions delivers all metadata key descriptions as a slice, sorted by name. -func GetSortedKeyDescriptions() []*DescriptionKey { - keys := maps.Keys(registeredKeys) - result := make([]*DescriptionKey, 0, len(keys)) - for _, n := range keys { - result = append(result, registeredKeys[n]) - } - return result -} - -// Supported keys. -func init() { - registerKey(api.KeyID, TypeID, usageComputed, "") - registerKey(api.KeyTitle, TypeEmpty, usageUser, "") - registerKey(api.KeyRole, TypeWord, usageUser, "") - registerKey(api.KeyTags, TypeTagSet, usageUser, "") - registerKey(api.KeySyntax, TypeWord, usageUser, "") - - // Properties that are inverse keys - registerKey(api.KeyFolge, TypeIDSet, usageProperty, "") - registerKey(api.KeySuccessors, TypeIDSet, usageProperty, "") - registerKey(api.KeySubordinates, TypeIDSet, usageProperty, "") - - // Non-inverse keys - registerKey(api.KeyAuthor, TypeString, usageUser, "") - registerKey(api.KeyBack, TypeIDSet, usageProperty, "") - registerKey(api.KeyBackward, TypeIDSet, usageProperty, "") - registerKey(api.KeyBoxNumber, TypeNumber, usageProperty, "") - registerKey(api.KeyCopyright, TypeString, usageUser, "") - registerKey(api.KeyCreated, TypeTimestamp, usageComputed, "") - registerKey(api.KeyCredential, TypeCredential, usageUser, "") - registerKey(api.KeyDead, TypeIDSet, usageProperty, "") - registerKey(api.KeyExpire, TypeTimestamp, usageUser, "") - registerKey(api.KeyFolgeRole, TypeWord, usageUser, "") - registerKey(api.KeyForward, TypeIDSet, usageProperty, "") - registerKey(api.KeyLang, TypeWord, usageUser, "") - registerKey(api.KeyLicense, TypeEmpty, usageUser, "") - registerKey(api.KeyModified, TypeTimestamp, usageComputed, "") - registerKey(api.KeyPrecursor, TypeIDSet, usageUser, api.KeyFolge) - registerKey(api.KeyPredecessor, TypeID, usageUser, api.KeySuccessors) - registerKey(api.KeyPublished, TypeTimestamp, usageProperty, "") - registerKey(api.KeyQuery, TypeEmpty, usageUser, "") - registerKey(api.KeyReadOnly, TypeWord, usageUser, "") - registerKey(api.KeySummary, TypeZettelmarkup, usageUser, "") - registerKey(api.KeySuperior, TypeIDSet, usageUser, api.KeySubordinates) - registerKey(api.KeyURL, TypeURL, usageUser, "") - registerKey(api.KeyUselessFiles, TypeString, usageProperty, "") - registerKey(api.KeyUserID, TypeWord, usageUser, "") - registerKey(api.KeyUserRole, TypeWord, usageUser, "") - registerKey(api.KeyVisibility, TypeWord, usageUser, "") -} - -// NewPrefix is the prefix for metadata key in template zettel for creating new zettel. -const NewPrefix = "new-" - -// Meta contains all meta-data of a zettel. -type Meta struct { - Zid id.Zid - pairs map[string]string - YamlSep bool -} - -// New creates a new chunk for storing metadata. -func New(zid id.Zid) *Meta { - return &Meta{Zid: zid, pairs: make(map[string]string, 5)} -} - -// NewWithData creates metadata object with given data. -func NewWithData(zid id.Zid, data map[string]string) *Meta { - pairs := make(map[string]string, len(data)) - for k, v := range data { - pairs[k] = v - } - return &Meta{Zid: zid, pairs: pairs} -} - -// Length returns the number of bytes stored for the metadata. -func (m *Meta) Length() int { - if m == nil { - return 0 - } - result := 6 // storage needed for Zid - for k, v := range m.pairs { - result += len(k) + len(v) + 1 // 1 because separator - } - return result -} - -// Clone returns a new copy of the metadata. -func (m *Meta) Clone() *Meta { - return &Meta{ - Zid: m.Zid, - pairs: m.Map(), - YamlSep: m.YamlSep, - } -} - -// Map returns a copy of the meta data as a string map. -func (m *Meta) Map() map[string]string { - pairs := make(map[string]string, len(m.pairs)) - for k, v := range m.pairs { - pairs[k] = v - } - return pairs -} - -var reKey = regexp.MustCompile("^[0-9a-z][-0-9a-z]{0,254}$") - -// KeyIsValid returns true, if the string is a valid metadata key. -func KeyIsValid(s string) bool { return reKey.MatchString(s) } - -// Pair is one key-value-pair of a Zettel meta. -type Pair struct { - Key string - Value string -} - -var firstKeys = []string{api.KeyTitle, api.KeyRole, api.KeyTags, api.KeySyntax} -var firstKeySet strfun.Set - -func init() { - firstKeySet = strfun.NewSet(firstKeys...) -} - -// Set stores the given string value under the given key. -func (m *Meta) Set(key, value string) { - if key != api.KeyID { - m.pairs[key] = trimValue(value) - } -} - -// SetNonEmpty stores the given value under the given key, if the value is non-empty. -// An empty value will delete the previous association. -func (m *Meta) SetNonEmpty(key, value string) { - if value == "" { - delete(m.pairs, key) - } else { - m.Set(key, trimValue(value)) - } -} - -func trimValue(value string) string { - return strings.TrimFunc(value, input.IsSpace) -} - -// Get retrieves the string value of a given key. The bool value signals, -// whether there was a value stored or not. -func (m *Meta) Get(key string) (string, bool) { - if m == nil { - return "", false - } - if key == api.KeyID { - return m.Zid.String(), true - } - value, ok := m.pairs[key] - return value, ok -} - -// GetDefault retrieves the string value of the given key. If no value was -// stored, the given default value is returned. -func (m *Meta) GetDefault(key, def string) string { - if value, found := m.Get(key); found { - return value - } - return def -} - -// GetTitle returns the title of the metadata. It is the only key that has a -// defined default value: the string representation of the zettel identifier. -func (m *Meta) GetTitle() string { - if title, found := m.Get(api.KeyTitle); found { - return title - } - return m.Zid.String() -} - -// Pairs returns not computed key/values pairs stored, in a specific order. -// First come the pairs with predefined keys: MetaTitleKey, MetaTagsKey, MetaSyntaxKey, -// MetaContextKey. Then all other pairs are append to the list, ordered by key. -func (m *Meta) Pairs() []Pair { - return m.doPairs(m.getFirstKeys(), notComputedKey) -} - -// ComputedPairs returns all key/values pairs stored, in a specific order. First come -// the pairs with predefined keys: MetaTitleKey, MetaTagsKey, MetaSyntaxKey, -// MetaContextKey. Then all other pairs are append to the list, ordered by key. -func (m *Meta) ComputedPairs() []Pair { - return m.doPairs(m.getFirstKeys(), anyKey) -} - -// PairsRest returns not computed key/values pairs stored, except the values with -// predefined keys. The pairs are ordered by key. -func (m *Meta) PairsRest() []Pair { - result := make([]Pair, 0, len(m.pairs)) - return m.doPairs(result, notComputedKey) -} - -// ComputedPairsRest returns all key/values pairs stored, except the values with -// predefined keys. The pairs are ordered by key. -func (m *Meta) ComputedPairsRest() []Pair { - result := make([]Pair, 0, len(m.pairs)) - return m.doPairs(result, anyKey) -} - -func notComputedKey(key string) bool { return !IsComputed(key) } -func anyKey(string) bool { return true } - -func (m *Meta) doPairs(firstKeys []Pair, addKeyPred func(string) bool) []Pair { - keys := m.getKeysRest(addKeyPred) - for _, k := range keys { - firstKeys = append(firstKeys, Pair{k, m.pairs[k]}) - } - return firstKeys -} - -func (m *Meta) getFirstKeys() []Pair { - result := make([]Pair, 0, len(m.pairs)) - for _, key := range firstKeys { - if value, ok := m.pairs[key]; ok { - result = append(result, Pair{key, value}) - } - } - return result -} - -func (m *Meta) getKeysRest(addKeyPred func(string) bool) []string { - keys := make([]string, 0, len(m.pairs)) - for k := range m.pairs { - if !firstKeySet.Has(k) && addKeyPred(k) { - keys = append(keys, k) - } - } - sort.Strings(keys) - return keys -} - -// Delete removes a key from the data. -func (m *Meta) Delete(key string) { - if key != api.KeyID { - delete(m.pairs, key) - } -} - -// Equal compares to metas for equality. -func (m *Meta) Equal(o *Meta, allowComputed bool) bool { - if m == nil && o == nil { - return true - } - if m == nil || o == nil || m.Zid != o.Zid { - return false - } - tested := make(strfun.Set, len(m.pairs)) - for k, v := range m.pairs { - tested.Set(k) - if !equalValue(k, v, o, allowComputed) { - return false - } - } - for k, v := range o.pairs { - if !tested.Has(k) && !equalValue(k, v, m, allowComputed) { - return false - } - } - return true -} - -func equalValue(key, val string, other *Meta, allowComputed bool) bool { - if allowComputed || !IsComputed(key) { - if valO, found := other.pairs[key]; !found || val != valO { - return false - } - } - return true -} - -// Sanitize all metadata keys and values, so that they can be written safely into a file. -func (m *Meta) Sanitize() { - if m == nil { - return - } - for k, v := range m.pairs { - m.pairs[RemoveNonGraphic(k)] = RemoveNonGraphic(v) - } -} - -// RemoveNonGraphic changes the given string not to include non-graphical characters. -// It is needed to sanitize meta data. -func RemoveNonGraphic(s string) string { - if s == "" { - return "" - } - pos := 0 - var sb strings.Builder - for pos < len(s) { - nextPos := strings.IndexFunc(s[pos:], func(r rune) bool { return !unicode.IsGraphic(r) }) - if nextPos < 0 { - break - } - sb.WriteString(s[pos:nextPos]) - sb.WriteByte(' ') - _, size := utf8.DecodeRuneInString(s[nextPos:]) - pos = nextPos + size - } - if pos == 0 { - return strings.TrimSpace(s) - } - sb.WriteString(s[pos:]) - return strings.TrimSpace(sb.String()) -} DELETED zettel/meta/meta_test.go Index: zettel/meta/meta_test.go ================================================================== --- zettel/meta/meta_test.go +++ zettel/meta/meta_test.go @@ -1,264 +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. -// -// SPDX-License-Identifier: EUPL-1.2 -// SPDX-FileCopyrightText: 2020-present Detlef Stern -//----------------------------------------------------------------------------- - -package meta - -import ( - "strings" - "testing" - - "t73f.de/r/zsc/api" - "zettelstore.de/z/zettel/id" -) - -const testID = id.Zid(98765432101234) - -func TestKeyIsValid(t *testing.T) { - t.Parallel() - validKeys := []string{"0", "a", "0-", "title", "title-----", strings.Repeat("r", 255)} - for _, key := range validKeys { - if !KeyIsValid(key) { - t.Errorf("Key %q wrongly identified as invalid key", key) - } - } - invalidKeys := []string{"", "-", "-a", "Title", "a_b", strings.Repeat("e", 256)} - for _, key := range invalidKeys { - if KeyIsValid(key) { - t.Errorf("Key %q wrongly identified as valid key", key) - } - } -} - -func TestTitleHeader(t *testing.T) { - t.Parallel() - m := New(testID) - if got, ok := m.Get(api.KeyTitle); ok && got != "" { - t.Errorf("Title is not empty, but %q", got) - } - addToMeta(m, api.KeyTitle, " ") - if got, ok := m.Get(api.KeyTitle); ok && got != "" { - t.Errorf("Title is not empty, but %q", got) - } - const st = "A simple text" - addToMeta(m, api.KeyTitle, " "+st+" ") - if got, ok := m.Get(api.KeyTitle); !ok || got != st { - t.Errorf("Title is not %q, but %q", st, got) - } - addToMeta(m, api.KeyTitle, " "+st+"\t") - const exp = st + " " + st - if got, ok := m.Get(api.KeyTitle); !ok || got != exp { - t.Errorf("Title is not %q, but %q", exp, got) - } - - m = New(testID) - const at = "A Title" - addToMeta(m, api.KeyTitle, at) - addToMeta(m, api.KeyTitle, " ") - if got, ok := m.Get(api.KeyTitle); !ok || got != at { - t.Errorf("Title is not %q, but %q", at, got) - } -} - -func checkTags(t *testing.T, exp []string, m *Meta) { - t.Helper() - got, _ := m.GetList(api.KeyTags) - for i, tag := range exp { - if i < len(got) { - if tag != got[i] { - t.Errorf("Pos=%d, expected %q, got %q", i, exp[i], got[i]) - } - } else { - t.Errorf("Expected %q, but is missing", exp[i]) - } - } - if len(exp) < len(got) { - t.Errorf("Extra tags: %q", got[len(exp):]) - } -} - -func TestTagsHeader(t *testing.T) { - t.Parallel() - m := New(testID) - checkTags(t, []string{}, m) - - addToMeta(m, api.KeyTags, "") - checkTags(t, []string{}, m) - - addToMeta(m, api.KeyTags, " #t1 #t2 #t3 #t4 ") - checkTags(t, []string{"#t1", "#t2", "#t3", "#t4"}, m) - - addToMeta(m, api.KeyTags, "#t5") - checkTags(t, []string{"#t1", "#t2", "#t3", "#t4", "#t5"}, m) - - addToMeta(m, api.KeyTags, "t6") - checkTags(t, []string{"#t1", "#t2", "#t3", "#t4", "#t5"}, m) -} - -func TestSyntax(t *testing.T) { - t.Parallel() - m := New(testID) - if got, ok := m.Get(api.KeySyntax); ok || got != "" { - t.Errorf("Syntax is not %q, but %q", "", got) - } - addToMeta(m, api.KeySyntax, " ") - if got, _ := m.Get(api.KeySyntax); got != "" { - t.Errorf("Syntax is not %q, but %q", "", got) - } - addToMeta(m, api.KeySyntax, "MarkDown") - const exp = "markdown" - if got, ok := m.Get(api.KeySyntax); !ok || got != exp { - t.Errorf("Syntax is not %q, but %q", exp, got) - } - addToMeta(m, api.KeySyntax, " ") - if got, _ := m.Get(api.KeySyntax); got != "" { - t.Errorf("Syntax is not %q, but %q", "", got) - } -} - -func checkHeader(t *testing.T, exp map[string]string, gotP []Pair) { - t.Helper() - got := make(map[string]string, len(gotP)) - for _, p := range gotP { - got[p.Key] = p.Value - if _, ok := exp[p.Key]; !ok { - t.Errorf("Key %q is not expected, but has value %q", p.Key, p.Value) - } - } - for k, v := range exp { - if gv, ok := got[k]; !ok || v != gv { - if ok { - t.Errorf("Key %q is not %q, but %q", k, v, got[k]) - } else { - t.Errorf("Key %q missing, should have value %q", k, v) - } - } - } -} - -func TestDefaultHeader(t *testing.T) { - t.Parallel() - m := New(testID) - addToMeta(m, "h1", "d1") - addToMeta(m, "H2", "D2") - addToMeta(m, "H1", "D1.1") - exp := map[string]string{"h1": "d1 D1.1", "h2": "D2"} - checkHeader(t, exp, m.Pairs()) - addToMeta(m, "", "d0") - checkHeader(t, exp, m.Pairs()) - addToMeta(m, "h3", "") - exp["h3"] = "" - checkHeader(t, exp, m.Pairs()) - addToMeta(m, "h3", " ") - checkHeader(t, exp, m.Pairs()) - addToMeta(m, "h4", " ") - exp["h4"] = "" - checkHeader(t, exp, m.Pairs()) -} - -func TestDelete(t *testing.T) { - t.Parallel() - m := New(testID) - m.Set("key", "val") - if got, ok := m.Get("key"); !ok || got != "val" { - t.Errorf("Value != %q, got: %v/%q", "val", ok, got) - } - m.Set("key", "") - if got, ok := m.Get("key"); !ok || got != "" { - t.Errorf("Value != %q, got: %v/%q", "", ok, got) - } - m.Delete("key") - if got, ok := m.Get("key"); ok || got != "" { - t.Errorf("Value != %q, got: %v/%q", "", ok, got) - } -} - -func TestEqual(t *testing.T) { - t.Parallel() - testcases := []struct { - pairs1, pairs2 []string - allowComputed bool - exp bool - }{ - {nil, nil, true, true}, - {nil, nil, false, true}, - {[]string{"a", "a"}, nil, false, false}, - {[]string{"a", "a"}, nil, true, false}, - {[]string{api.KeyFolge, "0"}, nil, true, false}, - {[]string{api.KeyFolge, "0"}, nil, false, true}, - {[]string{api.KeyFolge, "0"}, []string{api.KeyFolge, "0"}, true, true}, - {[]string{api.KeyFolge, "0"}, []string{api.KeyFolge, "0"}, false, true}, - } - for i, tc := range testcases { - m1 := pairs2meta(tc.pairs1) - m2 := pairs2meta(tc.pairs2) - got := m1.Equal(m2, tc.allowComputed) - if tc.exp != got { - t.Errorf("%d: %v =?= %v: expected=%v, but got=%v", i, tc.pairs1, tc.pairs2, tc.exp, got) - } - got = m2.Equal(m1, tc.allowComputed) - if tc.exp != got { - t.Errorf("%d: %v =!= %v: expected=%v, but got=%v", i, tc.pairs1, tc.pairs2, tc.exp, got) - } - } - - // Pathologic cases - var m1, m2 *Meta - if !m1.Equal(m2, true) { - t.Error("Nil metas should be treated equal") - } - m1 = New(testID) - if m1.Equal(m2, true) { - t.Error("Empty meta should not be equal to nil") - } - if m2.Equal(m1, true) { - t.Error("Nil meta should should not be equal to empty") - } - m2 = New(testID + 1) - if m1.Equal(m2, true) { - t.Error("Different ID should differentiate") - } - if m2.Equal(m1, true) { - t.Error("Different ID should differentiate") - } -} - -func pairs2meta(pairs []string) *Meta { - m := New(testID) - for i := 0; i < len(pairs); i += 2 { - m.Set(pairs[i], pairs[i+1]) - } - return m -} - -func TestRemoveNonGraphic(t *testing.T) { - testCases := []struct { - inp string - exp string - }{ - {"", ""}, - {" ", ""}, - {"a", "a"}, - {"a ", "a"}, - {"a b", "a b"}, - {"\n", ""}, - {"a\n", "a"}, - {"a\nb", "a b"}, - {"a\tb", "a b"}, - } - for i, tc := range testCases { - got := RemoveNonGraphic(tc.inp) - if tc.exp != got { - t.Errorf("%q/%d: expected %q, but got %q", tc.inp, i, tc.exp, got) - } - } -} DELETED zettel/meta/parse.go Index: zettel/meta/parse.go ================================================================== --- zettel/meta/parse.go +++ zettel/meta/parse.go @@ -1,178 +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. -// -// SPDX-License-Identifier: EUPL-1.2 -// SPDX-FileCopyrightText: 2020-present Detlef Stern -//----------------------------------------------------------------------------- - -package meta - -import ( - "strings" - - "t73f.de/r/zsc/api" - "t73f.de/r/zsc/input" - "t73f.de/r/zsc/maps" - "zettelstore.de/z/strfun" - "zettelstore.de/z/zettel/id" -) - -// NewFromInput parses the meta data of a zettel. -func NewFromInput(zid id.Zid, inp *input.Input) *Meta { - if inp.Ch == '-' && inp.PeekN(0) == '-' && inp.PeekN(1) == '-' { - skipToEOL(inp) - inp.EatEOL() - } - meta := New(zid) - for { - skipSpace(inp) - switch inp.Ch { - case '\r': - if inp.Peek() == '\n' { - inp.Next() - } - fallthrough - case '\n': - inp.Next() - return meta - case input.EOS: - return meta - case '%': - skipToEOL(inp) - inp.EatEOL() - continue - } - parseHeader(meta, inp) - if inp.Ch == '-' && inp.PeekN(0) == '-' && inp.PeekN(1) == '-' { - skipToEOL(inp) - inp.EatEOL() - meta.YamlSep = true - return meta - } - } -} - -func parseHeader(m *Meta, inp *input.Input) { - pos := inp.Pos - for isHeader(inp.Ch) { - inp.Next() - } - key := inp.Src[pos:inp.Pos] - skipSpace(inp) - if inp.Ch == ':' { - inp.Next() - } - var val []byte - for { - skipSpace(inp) - pos = inp.Pos - skipToEOL(inp) - val = append(val, inp.Src[pos:inp.Pos]...) - inp.EatEOL() - if !input.IsSpace(inp.Ch) { - break - } - val = append(val, ' ') - } - addToMeta(m, string(key), string(val)) -} - -func skipSpace(inp *input.Input) { - for input.IsSpace(inp.Ch) { - inp.Next() - } -} - -func skipToEOL(inp *input.Input) { - for { - switch inp.Ch { - case '\n', '\r', input.EOS: - return - } - inp.Next() - } -} - -// Return true iff rune is valid for header key. -func isHeader(ch rune) bool { - return ('a' <= ch && ch <= 'z') || - ('0' <= ch && ch <= '9') || - ch == '-' || - ('A' <= ch && ch <= 'Z') -} - -type predValidElem func(string) bool - -func addToSet(set strfun.Set, elems []string, useElem predValidElem) { - for _, s := range elems { - if len(s) > 0 && useElem(s) { - set.Set(s) - } - } -} - -func addSet(m *Meta, key, val string, useElem predValidElem) { - newElems := strings.Fields(val) - oldElems, ok := m.GetList(key) - if !ok { - oldElems = nil - } - - set := make(strfun.Set, len(newElems)+len(oldElems)) - addToSet(set, newElems, useElem) - if len(set) == 0 { - // Nothing to add. Maybe because of rejected elements. - return - } - addToSet(set, oldElems, useElem) - m.SetList(key, maps.Keys(set)) -} - -func addData(m *Meta, k, v string) { - if o, ok := m.Get(k); !ok || o == "" { - m.Set(k, v) - } else if v != "" { - m.Set(k, o+" "+v) - } -} - -func addToMeta(m *Meta, key, val string) { - v := trimValue(val) - key = strings.ToLower(key) - if !KeyIsValid(key) { - return - } - switch key { - case "", api.KeyID: - // Empty key and 'id' key will be ignored - return - } - - 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 TypeID: - if _, err := id.Parse(v); err == nil { - m.Set(key, v) - } - case TypeIDSet: - addSet(m, key, v, func(s string) bool { - _, err := id.Parse(s) - return err == nil - }) - case TypeTimestamp: - if _, ok := TimeValue(v); ok { - m.Set(key, v) - } - default: - addData(m, key, v) - } -} DELETED zettel/meta/parse_test.go Index: zettel/meta/parse_test.go ================================================================== --- zettel/meta/parse_test.go +++ zettel/meta/parse_test.go @@ -1,171 +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. -// -// SPDX-License-Identifier: EUPL-1.2 -// SPDX-FileCopyrightText: 2020-present Detlef Stern -//----------------------------------------------------------------------------- - -package meta_test - -import ( - "strings" - "testing" - - "t73f.de/r/zsc/api" - "t73f.de/r/zsc/input" - "zettelstore.de/z/zettel/meta" -) - -func parseMetaStr(src string) *meta.Meta { - return meta.NewFromInput(testID, input.NewInput([]byte(src))) -} - -func TestEmpty(t *testing.T) { - t.Parallel() - m := parseMetaStr("") - if got, ok := m.Get(api.KeySyntax); ok || got != "" { - t.Errorf("Syntax is not %q, but %q", "", got) - } - if got, ok := m.GetList(api.KeyTags); ok || len(got) > 0 { - t.Errorf("Tags are not nil, but %v", got) - } -} - -func TestTitle(t *testing.T) { - t.Parallel() - td := []struct{ s, e string }{ - {api.KeyTitle + ": a title", "a title"}, - {api.KeyTitle + ": a\n\t title", "a title"}, - {api.KeyTitle + ": a\n\t title\r\n x", "a title x"}, - {api.KeyTitle + " AbC", "AbC"}, - {api.KeyTitle + " AbC\n ded", "AbC ded"}, - {api.KeyTitle + ": o\ntitle: p", "o p"}, - {api.KeyTitle + ": O\n\ntitle: P", "O"}, - {api.KeyTitle + ": b\r\ntitle: c", "b c"}, - {api.KeyTitle + ": B\r\n\r\ntitle: C", "B"}, - {api.KeyTitle + ": r\rtitle: q", "r q"}, - {api.KeyTitle + ": R\r\rtitle: Q", "R"}, - } - for i, tc := range td { - m := parseMetaStr(tc.s) - if got, ok := m.Get(api.KeyTitle); !ok || got != tc.e { - t.Log(m) - t.Errorf("TC=%d: expected %q, got %q", i, tc.e, got) - } - } -} - -func TestTags(t *testing.T) { - t.Parallel() - testcases := []struct { - src string - exp string - }{ - {"", ""}, - {api.KeyTags + ":", ""}, - {api.KeyTags + ": c", ""}, - {api.KeyTags + ": #", ""}, - {api.KeyTags + ": #c", "c"}, - {api.KeyTags + ": #c #", "c"}, - {api.KeyTags + ": #c #b", "b c"}, - {api.KeyTags + ": #c # #", "c"}, - {api.KeyTags + ": #c # #b", "b c"}, - } - for i, tc := range testcases { - m := parseMetaStr(tc.src) - tagsString, found := m.Get(api.KeyTags) - if !found { - if tc.exp != "" { - t.Errorf("%d / %q: no %s found", i, tc.src, api.KeyTags) - } - continue - } - tags := meta.TagsFromValue(tagsString) - if tc.exp == "" && len(tags) > 0 { - t.Errorf("%d / %q: expected no %s, but got %v", i, tc.src, api.KeyTags, tags) - continue - } - got := strings.Join(tags, " ") - if tc.exp != got { - t.Errorf("%d / %q: expected %q, got: %q", i, tc.src, tc.exp, got) - } - } -} - -func TestNewFromInput(t *testing.T) { - t.Parallel() - testcases := []struct { - input string - exp []meta.Pair - }{ - {"", []meta.Pair{}}, - {" a:b", []meta.Pair{{"a", "b"}}}, - {"%a:b", []meta.Pair{}}, - {"a:b\r\n\r\nc:d", []meta.Pair{{"a", "b"}}}, - {"a:b\r\n%c:d", []meta.Pair{{"a", "b"}}}, - {"% a:b\r\n c:d", []meta.Pair{{"c", "d"}}}, - {"---\r\na:b\r\n", []meta.Pair{{"a", "b"}}}, - {"---\r\na:b\r\n--\r\nc:d", []meta.Pair{{"a", "b"}, {"c", "d"}}}, - {"---\r\na:b\r\n---\r\nc:d", []meta.Pair{{"a", "b"}}}, - {"---\r\na:b\r\n----\r\nc:d", []meta.Pair{{"a", "b"}}}, - {"new-title:\nnew-url:", []meta.Pair{{"new-title", ""}, {"new-url", ""}}}, - } - for i, tc := range testcases { - meta := parseMetaStr(tc.input) - if got := meta.Pairs(); !equalPairs(tc.exp, got) { - t.Errorf("TC=%d: expected=%v, got=%v", i, tc.exp, got) - } - } - - // Test, whether input position is correct. - inp := input.NewInput([]byte("---\na:b\n---\nX")) - m := meta.NewFromInput(testID, inp) - exp := []meta.Pair{{"a", "b"}} - if got := m.Pairs(); !equalPairs(exp, got) { - t.Errorf("Expected=%v, got=%v", exp, got) - } - expCh := 'X' - if gotCh := inp.Ch; gotCh != expCh { - t.Errorf("Expected=%v, got=%v", expCh, gotCh) - } -} - -func equalPairs(one, two []meta.Pair) bool { - if len(one) != len(two) { - return false - } - for i := range len(one) { - if one[i].Key != two[i].Key || one[i].Value != two[i].Value { - return false - } - } - return true -} - -func TestPrecursorIDSet(t *testing.T) { - t.Parallel() - var testdata = []struct { - inp string - exp string - }{ - {"", ""}, - {"123", ""}, - {"12345678901234", "12345678901234"}, - {"123 12345678901234", "12345678901234"}, - {"12345678901234 123", "12345678901234"}, - {"01234567890123 123 12345678901234", "01234567890123 12345678901234"}, - {"12345678901234 01234567890123", "01234567890123 12345678901234"}, - } - for i, tc := range testdata { - m := parseMetaStr(api.KeyPrecursor + ": " + tc.inp) - if got, ok := m.Get(api.KeyPrecursor); (!ok && tc.exp != "") || tc.exp != got { - t.Errorf("TC=%d: expected %q, but got %q when parsing %q", i, tc.exp, got, tc.inp) - } - } -} DELETED zettel/meta/type.go Index: zettel/meta/type.go ================================================================== --- zettel/meta/type.go +++ zettel/meta/type.go @@ -1,230 +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. -// -// SPDX-License-Identifier: EUPL-1.2 -// SPDX-FileCopyrightText: 2020-present Detlef Stern -//----------------------------------------------------------------------------- - -package meta - -import ( - "strconv" - "strings" - "sync" - "time" - - "t73f.de/r/zsc/api" - "zettelstore.de/z/zettel/id" -) - -// DescriptionType is a description of a specific key type. -type DescriptionType struct { - Name string - IsSet bool -} - -// String returns the string representation of the given type -func (t DescriptionType) String() string { return t.Name } - -var registeredTypes = make(map[string]*DescriptionType) - -func registerType(name string, isSet bool) *DescriptionType { - if _, ok := registeredTypes[name]; ok { - panic("Type '" + name + "' already registered") - } - t := &DescriptionType{name, isSet} - registeredTypes[name] = t - return t -} - -// Supported key types. -var ( - TypeCredential = registerType(api.MetaCredential, false) - TypeEmpty = registerType(api.MetaEmpty, false) - TypeID = registerType(api.MetaID, false) - TypeIDSet = registerType(api.MetaIDSet, true) - TypeNumber = registerType(api.MetaNumber, false) - 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) - 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, - 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. -func Type(key string) *DescriptionType { - if k, ok := registeredKeys[key]; ok { - return k.Type - } - mxTypedKey.RLock() - k, ok := cachedTypedKeys[key] - mxTypedKey.RUnlock() - if ok { - return k - } - for suffix, t := range suffixTypes { - if strings.HasSuffix(key, suffix) { - mxTypedKey.Lock() - defer mxTypedKey.Unlock() - cachedTypedKeys[key] = t - return t - } - } - return TypeEmpty -} - -// SetList stores the given string list value under the given key. -func (m *Meta) SetList(key string, values []string) { - if key != api.KeyID { - for i, val := range values { - values[i] = trimValue(val) - } - m.pairs[key] = strings.Join(values, " ") - } -} - -// SetWord stores the given word under the given key. -func (m *Meta) SetWord(key, word string) { - if slist := ListFromValue(word); len(slist) > 0 { - m.Set(key, slist[0]) - } -} - -// SetNow stores the current timestamp under the given key. -func (m *Meta) SetNow(key string) { - m.Set(key, time.Now().Local().Format(id.TimestampLayout)) -} - -// BoolValue returns the value interpreted as a bool. -func BoolValue(value string) bool { - if len(value) > 0 { - switch value[0] { - case '0', 'f', 'F', 'n', 'N': - return false - } - } - return true -} - -// GetBool returns the boolean value of the given key. -func (m *Meta) GetBool(key string) bool { - if value, ok := m.Get(key); ok { - return BoolValue(value) - } - return false -} - -// TimeValue returns the time value of the given value. -func TimeValue(value string) (time.Time, bool) { - if t, err := time.Parse(id.TimestampLayout, ExpandTimestamp(value)); err == nil { - return t, true - } - return time.Time{}, false -} - -// ExpandTimestamp makes a short-form timestamp larger. -func ExpandTimestamp(value string) string { - switch l := len(value); l { - case 4: // YYYY - return value + "0101000000" - case 6: // YYYYMM - return value + "01000000" - case 8, 10, 12: // YYYYMMDD, YYYYMMDDhh, YYYYMMDDhhmm - return value + "000000"[:14-l] - case 14: // YYYYMMDDhhmmss - return value - default: - if l > 14 { - return value[:14] - } - return value - } -} - -// ListFromValue transforms a string value into a list value. -func ListFromValue(value string) []string { - return strings.Fields(value) -} - -// GetList retrieves the string list value of a given key. The bool value -// signals, whether there was a value stored or not. -func (m *Meta) GetList(key string) ([]string, bool) { - value, ok := m.Get(key) - if !ok { - return nil, false - } - return ListFromValue(value), true -} - -// TagsFromValue returns the value as a sequence of normalized tags. -func TagsFromValue(value string) []string { - tags := ListFromValue(strings.ToLower(value)) - for i, tag := range tags { - if len(tag) > 1 && tag[0] == '#' { - tags[i] = tag[1:] - } - } - return tags -} - -// CleanTag removes the number character ('#') from a tag value and lowercases it. -func CleanTag(tag string) string { - if len(tag) > 1 && tag[0] == '#' { - return tag[1:] - } - return tag -} - -// NormalizeTag adds a missing prefix "#" to the tag -func NormalizeTag(tag string) string { - if len(tag) > 0 && tag[0] == '#' { - return tag - } - return "#" + tag -} - -// GetNumber retrieves the numeric value of a given key. -func (m *Meta) GetNumber(key string, def int64) int64 { - if value, ok := m.Get(key); ok { - if num, err := strconv.ParseInt(value, 10, 64); err == nil { - return num - } - } - return def -} DELETED zettel/meta/type_test.go Index: zettel/meta/type_test.go ================================================================== --- zettel/meta/type_test.go +++ zettel/meta/type_test.go @@ -1,79 +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. -// -// SPDX-License-Identifier: EUPL-1.2 -// SPDX-FileCopyrightText: 2020-present Detlef Stern -//----------------------------------------------------------------------------- - -package meta_test - -import ( - "strconv" - "testing" - "time" - - "zettelstore.de/z/zettel/id" - "zettelstore.de/z/zettel/meta" -) - -func TestNow(t *testing.T) { - t.Parallel() - m := meta.New(id.Invalid) - m.SetNow("key") - val, ok := m.Get("key") - if !ok { - t.Error("Unable to get value of key") - } - if len(val) != 14 { - t.Errorf("Value is not 14 digits long: %q", val) - } - if _, err := strconv.ParseInt(val, 10, 64); err != nil { - t.Errorf("Unable to parse %q as an int64: %v", val, err) - } - if _, ok = meta.TimeValue(val); !ok { - t.Errorf("Unable to get time from value %q", val) - } -} - -func TestTimeValue(t *testing.T) { - t.Parallel() - testCases := []struct { - value string - valid bool - exp time.Time - }{ - {"", false, time.Time{}}, - {"1", false, time.Time{}}, - {"00000000000000", false, time.Time{}}, - {"98765432109876", false, time.Time{}}, - {"20201221111905", true, time.Date(2020, time.December, 21, 11, 19, 5, 0, time.UTC)}, - {"2023", true, time.Date(2023, time.January, 1, 0, 0, 0, 0, time.UTC)}, - {"20231", false, time.Time{}}, - {"202310", true, time.Date(2023, time.October, 1, 0, 0, 0, 0, time.UTC)}, - {"2023103", false, time.Time{}}, - {"20231030", true, time.Date(2023, time.October, 30, 0, 0, 0, 0, time.UTC)}, - {"202310301", false, time.Time{}}, - {"2023103016", true, time.Date(2023, time.October, 30, 16, 0, 0, 0, time.UTC)}, - {"20231030165", false, time.Time{}}, - {"202310301654", true, time.Date(2023, time.October, 30, 16, 54, 0, 0, time.UTC)}, - {"2023103016541", false, time.Time{}}, - {"20231030165417", true, time.Date(2023, time.October, 30, 16, 54, 17, 0, time.UTC)}, - {"2023103916541700", false, time.Time{}}, - } - for i, tc := range testCases { - got, ok := meta.TimeValue(tc.value) - if ok != tc.valid { - t.Errorf("%d: parsing of %q should be %v, but got %v", i, tc.value, tc.valid, ok) - continue - } - if got != tc.exp { - t.Errorf("%d: parsing of %q should return %v, but got %v", i, tc.value, tc.exp, got) - } - } -} DELETED zettel/meta/values.go Index: zettel/meta/values.go ================================================================== --- zettel/meta/values.go +++ zettel/meta/values.go @@ -1,115 +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. -// -// SPDX-License-Identifier: EUPL-1.2 -// SPDX-FileCopyrightText: 2020-present Detlef Stern -//----------------------------------------------------------------------------- - -package meta - -import ( - "fmt" - - "t73f.de/r/zsc/api" -) - -// Supported syntax values. -const ( - SyntaxCSS = api.ValueSyntaxCSS - SyntaxDraw = api.ValueSyntaxDraw - SyntaxGif = api.ValueSyntaxGif - SyntaxHTML = api.ValueSyntaxHTML - SyntaxJPEG = "jpeg" - SyntaxJPG = "jpg" - SyntaxMarkdown = api.ValueSyntaxMarkdown - SyntaxMD = api.ValueSyntaxMD - SyntaxNone = api.ValueSyntaxNone - SyntaxPlain = "plain" - SyntaxPNG = "png" - SyntaxSVG = api.ValueSyntaxSVG - 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 - -// Supported values for visibility. -const ( - _ Visibility = iota - VisibilityUnknown - VisibilityPublic - VisibilityCreator - VisibilityLogin - VisibilityOwner - VisibilityExpert -) - -var visMap = map[string]Visibility{ - api.ValueVisibilityPublic: VisibilityPublic, - api.ValueVisibilityCreator: VisibilityCreator, - api.ValueVisibilityLogin: VisibilityLogin, - api.ValueVisibilityOwner: VisibilityOwner, - api.ValueVisibilityExpert: VisibilityExpert, -} -var revVisMap = map[Visibility]string{} - -func init() { - for k, v := range visMap { - revVisMap[v] = k - } -} - -// GetVisibility returns the visibility value of the given string -func GetVisibility(val string) Visibility { - if vis, ok := visMap[val]; ok { - return vis - } - return VisibilityUnknown -} - -func (v Visibility) String() string { - if s, ok := revVisMap[v]; ok { - return s - } - return fmt.Sprintf("Unknown (%d)", v) -} - -// UserRole enumerates the supported values of meta key 'user-role'. -type UserRole int - -// Supported values for user roles. -const ( - _ UserRole = iota - UserRoleUnknown - UserRoleCreator - UserRoleReader - UserRoleWriter - UserRoleOwner -) - -var urMap = map[string]UserRole{ - api.ValueUserRoleCreator: UserRoleCreator, - api.ValueUserRoleReader: UserRoleReader, - api.ValueUserRoleWriter: UserRoleWriter, - api.ValueUserRoleOwner: UserRoleOwner, -} - -// GetUserRole role returns the user role of the given string. -func GetUserRole(val string) UserRole { - if ur, ok := urMap[val]; ok { - return ur - } - return UserRoleUnknown -} DELETED zettel/meta/write.go Index: zettel/meta/write.go ================================================================== --- zettel/meta/write.go +++ zettel/meta/write.go @@ -1,60 +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. -// -// SPDX-License-Identifier: EUPL-1.2 -// SPDX-FileCopyrightText: 2020-present Detlef Stern -//----------------------------------------------------------------------------- - -package meta - -import "io" - -// Write writes metadata to a writer, excluding computed and propery values. -func (m *Meta) Write(w io.Writer) (int, error) { - return m.doWrite(w, IsComputed) -} - -// WriteComputed writes metadata to a writer, including computed values, -// but excluding property values. -func (m *Meta) WriteComputed(w io.Writer) (int, error) { - return m.doWrite(w, IsProperty) -} - -func (m *Meta) doWrite(w io.Writer, ignoreKeyPred func(string) bool) (length int, err error) { - for _, p := range m.ComputedPairs() { - key := p.Key - if ignoreKeyPred(key) { - continue - } - if err != nil { - break - } - var l int - l, err = io.WriteString(w, key) - length += l - if err == nil { - l, err = w.Write(colonSpace) - length += l - } - if err == nil { - l, err = io.WriteString(w, p.Value) - length += l - } - if err == nil { - l, err = w.Write(newline) - length += l - } - } - return length, err -} - -var ( - colonSpace = []byte{':', ' '} - newline = []byte{'\n'} -) DELETED zettel/meta/write_test.go Index: zettel/meta/write_test.go ================================================================== --- zettel/meta/write_test.go +++ zettel/meta/write_test.go @@ -1,60 +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. -// -// SPDX-License-Identifier: EUPL-1.2 -// SPDX-FileCopyrightText: 2020-present Detlef Stern -//----------------------------------------------------------------------------- - -package meta_test - -import ( - "strings" - "testing" - - "t73f.de/r/zsc/api" - "zettelstore.de/z/zettel/id" - "zettelstore.de/z/zettel/meta" -) - -const testID = id.Zid(98765432101234) - -func newMeta(title string, tags []string, syntax string) *meta.Meta { - m := meta.New(testID) - if title != "" { - m.Set(api.KeyTitle, title) - } - if tags != nil { - m.Set(api.KeyTags, strings.Join(tags, " ")) - } - if syntax != "" { - m.Set(api.KeySyntax, syntax) - } - return m -} -func assertWriteMeta(t *testing.T, m *meta.Meta, expected string) { - t.Helper() - var sb strings.Builder - m.Write(&sb) - if got := sb.String(); got != expected { - t.Errorf("\nExp: %q\ngot: %q", expected, got) - } -} - -func TestWriteMeta(t *testing.T) { - t.Parallel() - assertWriteMeta(t, newMeta("", nil, ""), "") - - m := newMeta("TITLE", []string{"#t1", "#t2"}, "syntax") - assertWriteMeta(t, m, "title: TITLE\ntags: #t1 #t2\nsyntax: syntax\n") - - m = newMeta("TITLE", nil, "") - m.Set("user", "zettel") - m.Set("auth", "basic") - assertWriteMeta(t, m, "title: TITLE\nauth: basic\nuser: zettel\n") -} DELETED zettel/zettel.go Index: zettel/zettel.go ================================================================== --- zettel/zettel.go +++ zettel/zettel.go @@ -1,32 +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. -// -// SPDX-License-Identifier: EUPL-1.2 -// SPDX-FileCopyrightText: 2020-present Detlef Stern -//----------------------------------------------------------------------------- - -// Package zettel provides specific types, constants, and functions for zettel. -package zettel - -import "zettelstore.de/z/zettel/meta" - -// Zettel is the main data object of a zettelstore. -type Zettel struct { - Meta *meta.Meta // Some additional meta-data. - Content Content // The content of the zettel itself. -} - -// Length returns the number of bytes to store the zettel (in a zettel view, -// not in a technical view). -func (z Zettel) Length() int { return z.Meta.Length() + z.Content.Length() } - -// Equal compares two zettel for equality. -func (z Zettel) Equal(o Zettel, allowComputed bool) bool { - return z.Meta.Equal(o.Meta, allowComputed) && z.Content.Equal(&o.Content) -}