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,25 @@
-## 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 build release clean
check:
- go run tools/check/check.go
-
-relcheck:
- go run tools/check/check.go -r
-
-api:
- go run tools/testapi/testapi.go
+ go run tools/build.go check
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.13
Index: ast/ast.go
==================================================================
--- ast/ast.go
+++ ast/ast.go
@@ -1,50 +1,50 @@
//-----------------------------------------------------------------------------
-// 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)
+ Accept(v Visitor)
}
// 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,48 @@
+//-----------------------------------------------------------------------------
+// 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 ast provides the abstract syntax tree.
+package ast_test
+
+import (
+ "testing"
+
+ "zettelstore.de/z/ast"
+)
+
+func TestHasDefault(t *testing.T) {
+ 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) {
+ 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,191 +1,140 @@
//-----------------------------------------------------------------------------
-// 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} }
-
-// WalkChildren walks down the inline elements.
-func (pn *ParaNode) WalkChildren(v Visitor) { Walk(v, &pn.Inlines) }
+func (pn *ParaNode) blockNode() {}
+func (pn *ParaNode) itemNode() {}
+func (pn *ParaNode) descriptionNode() {}
+
+// Accept a visitor and visit the node.
+func (pn *ParaNode) Accept(v Visitor) { v.VisitPara(pn) }
//--------------------------------------------------------------------------
-// VerbatimNode contains uninterpreted text
+// VerbatimNode contains lines of uninterpreted text
type VerbatimNode struct {
- Kind VerbatimKind
- Attrs attrs.Attributes
- Content []byte
+ Code VerbatimCode
+ Attrs *Attributes
+ Lines []string
}
-// VerbatimKind specifies the format that is applied to code inline nodes.
-type VerbatimKind int
+// VerbatimCode specifies the format that is applied to code inline nodes.
+type VerbatimCode int
// Constants for VerbatimCode
const (
- _ VerbatimKind = iota
- VerbatimZettel // Zettel content
- VerbatimProg // Program code
- VerbatimEval // Code to be externally interpreted. Syntax is stored in default attribute.
+ _ VerbatimCode = iota
+ 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() {}
+func (vn *VerbatimNode) itemNode() {}
-// WalkChildren does nothing.
-func (*VerbatimNode) WalkChildren(Visitor) { /* No children*/ }
+// Accept a visitor an visit the node.
+func (vn *VerbatimNode) Accept(v Visitor) { v.VisitVerbatim(vn) }
//--------------------------------------------------------------------------
// RegionNode encapsulates a region of block nodes.
type RegionNode struct {
- Kind RegionKind
- Attrs attrs.Attributes
+ Code RegionCode
+ 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
+// RegionCode specifies the actual region type.
+type RegionCode int
// Values for RegionCode
const (
- _ RegionKind = iota
+ _ RegionCode = 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 */ }
-
-// WalkChildren walks down the blocks and the text.
-func (rn *RegionNode) WalkChildren(v Visitor) {
- Walk(v, &rn.Blocks)
- Walk(v, &rn.Inlines)
-}
+func (rn *RegionNode) blockNode() {}
+func (rn *RegionNode) itemNode() {}
+
+// Accept a visitor and visit the node.
+func (rn *RegionNode) Accept(v Visitor) { v.VisitRegion(rn) }
//--------------------------------------------------------------------------
// 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() {}
+func (hn *HeadingNode) itemNode() {}
-// WalkChildren walks the heading text.
-func (hn *HeadingNode) WalkChildren(v Visitor) { Walk(v, &hn.Inlines) }
+// Accept a visitor and visit the node.
+func (hn *HeadingNode) Accept(v Visitor) { v.VisitHeading(hn) }
//--------------------------------------------------------------------------
// 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() {}
+func (hn *HRuleNode) itemNode() {}
-// WalkChildren does nothing.
-func (*HRuleNode) WalkChildren(Visitor) { /* No children*/ }
+// Accept a visitor and visit the node.
+func (hn *HRuleNode) Accept(v Visitor) { v.VisitHRule(hn) }
//--------------------------------------------------------------------------
// NestedListNode specifies a nestable list, either ordered or unordered.
type NestedListNode struct {
- Kind NestedListKind
+ Code NestedListCode
Items []ItemSlice
- Attrs attrs.Attributes
+ Attrs *Attributes
}
-// NestedListKind specifies the actual list type.
-type NestedListKind uint8
+// NestedListCode specifies the actual list type.
+type NestedListCode int
// Values for ListCode
const (
- _ NestedListKind = iota
+ _ NestedListCode = iota
NestedListOrdered // Ordered list.
NestedListUnordered // Unordered list.
NestedListQuote // Quote list.
)
-func (*NestedListNode) blockNode() { /* Just a marker */ }
-func (*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)
- }
- }
-}
+func (ln *NestedListNode) blockNode() {}
+func (ln *NestedListNode) itemNode() {}
+
+// Accept a visitor and visit the node.
+func (ln *NestedListNode) Accept(v Visitor) { v.VisitNestedList(ln) }
//--------------------------------------------------------------------------
// DescriptionListNode specifies a description list.
type DescriptionListNode struct {
@@ -196,27 +145,14 @@
type Description struct {
Term InlineSlice
Descriptions []DescriptionSlice
}
-func (*DescriptionListNode) blockNode() { /* Just a marker */ }
-
-// 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)
- }
- }
- }
- }
-}
+func (dn *DescriptionListNode) blockNode() {}
+
+// Accept a visitor and visit the node.
+func (dn *DescriptionListNode) Accept(v Visitor) { v.VisitDescriptionList(dn) }
//--------------------------------------------------------------------------
// TableNode specifies a full table
type TableNode struct {
@@ -245,51 +181,24 @@
AlignLeft // Left alignment
AlignCenter // Center the content
AlignRight // Right alignment
)
-func (*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
- }
- }
- }
-}
-
-//--------------------------------------------------------------------------
-
-// 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*/ }
+func (tn *TableNode) blockNode() {}
+
+// Accept a visitor and visit the node.
+func (tn *TableNode) Accept(v Visitor) { v.VisitTable(tn) }
//--------------------------------------------------------------------------
// 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() {}
-// WalkChildren does nothing.
-func (*BLOBNode) WalkChildren(Visitor) { /* No children*/ }
+// Accept a visitor and visit the node.
+func (bn *BLOBNode) Accept(v Visitor) { v.VisitBLOB(bn) }
Index: ast/inline.go
==================================================================
--- ast/inline.go
+++ ast/inline.go
@@ -1,244 +1,196 @@
//-----------------------------------------------------------------------------
-// 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() {}
+
+// Accept a visitor and visit the node.
+func (tn *TextNode) Accept(v Visitor) { v.VisitText(tn) }
+
+// --------------------------------------------------------------------------
+
+// TagNode contains a tag.
+type TagNode struct {
+ Tag string // The text itself.
+}
+
+func (tn *TagNode) inlineNode() {}
-// WalkChildren does nothing.
-func (*TextNode) WalkChildren(Visitor) { /* No children*/ }
+// Accept a visitor and visit the node.
+func (tn *TagNode) Accept(v Visitor) { v.VisitTag(tn) }
// --------------------------------------------------------------------------
// SpaceNode tracks inter-word space characters.
type SpaceNode struct {
Lexeme string
}
-func (*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) inlineNode() {}
+
+// Accept a visitor and visit the node.
+func (sn *SpaceNode) Accept(v Visitor) { v.VisitSpace(sn) }
// --------------------------------------------------------------------------
// 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() {}
-// WalkChildren does nothing.
-func (*BreakNode) WalkChildren(Visitor) { /* No children*/ }
+// Accept a visitor and visit the node.
+func (bn *BreakNode) Accept(v Visitor) { v.VisitBreak(bn) }
// --------------------------------------------------------------------------
// 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() {}
+
+// Accept a visitor and visit the node.
+func (ln *LinkNode) Accept(v Visitor) { v.VisitLink(ln) }
+
+// --------------------------------------------------------------------------
+
+// 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() {}
+
+// Accept a visitor and visit the node.
+func (in *ImageNode) Accept(v Visitor) { v.VisitImage(in) }
// --------------------------------------------------------------------------
// 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() {}
-// WalkChildren walks to the cite text.
-func (cn *CiteNode) WalkChildren(v Visitor) { Walk(v, &cn.Inlines) }
+// Accept a visitor and visit the node.
+func (cn *CiteNode) Accept(v Visitor) { v.VisitCite(cn) }
// --------------------------------------------------------------------------
// 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 */ }
-
-// WalkChildren does nothing.
-func (mn *MarkNode) WalkChildren(v Visitor) {
- if len(mn.Inlines) > 0 {
- Walk(v, &mn.Inlines)
- }
-}
+func (mn *MarkNode) inlineNode() {}
+
+// Accept a visitor and visit the node.
+func (mn *MarkNode) Accept(v Visitor) { v.VisitMark(mn) }
// --------------------------------------------------------------------------
// 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() {}
-// WalkChildren walks to the footnote text.
-func (fn *FootnoteNode) WalkChildren(v Visitor) { Walk(v, &fn.Inlines) }
+// Accept a visitor and visit the node.
+func (fn *FootnoteNode) Accept(v Visitor) { v.VisitFootnote(fn) }
// --------------------------------------------------------------------------
// FormatNode specifies some inline formatting.
type FormatNode struct {
- Kind FormatKind
- Attrs attrs.Attributes // Optional attributes.
+ Code FormatCode
+ Attrs *Attributes // Optional attributes.
Inlines InlineSlice
}
-// FormatKind specifies the format that is applied to the inline nodes.
-type FormatKind int
+// FormatCode specifies the format that is applied to the inline nodes.
+type FormatCode int
// 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) }
+ _ FormatCode = 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() {}
+
+// Accept a visitor and visit the node.
+func (fn *FormatNode) Accept(v Visitor) { v.VisitFormat(fn) }
// --------------------------------------------------------------------------
// LiteralNode specifies some uninterpreted text.
type LiteralNode struct {
- Kind LiteralKind
- Attrs attrs.Attributes // Optional attributes.
- Content []byte
+ Code LiteralCode
+ Attrs *Attributes // Optional attributes.
+ Text string
}
-// LiteralKind specifies the format that is applied to code inline nodes.
-type LiteralKind int
+// LiteralCode specifies the format that is applied to code inline nodes.
+type LiteralCode int
// Constants for LiteralCode
const (
- _ LiteralKind = iota
- LiteralZettel // Zettel content
- LiteralProg // Inline program code
- LiteralInput // Computer input, e.g. Keyboard strokes
- LiteralOutput // Computer output
+ _ LiteralCode = iota
+ 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 (rn *LiteralNode) inlineNode() {}
-// WalkChildren does nothing.
-func (*LiteralNode) WalkChildren(Visitor) { /* No children*/ }
+// Accept a visitor and visit the node.
+func (rn *LiteralNode) Accept(v Visitor) { v.VisitLiteral(rn) }
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,28 +1,25 @@
//-----------------------------------------------------------------------------
-// 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"
"zettelstore.de/z/ast"
)
func TestParseReference(t *testing.T) {
- t.Parallel()
testcases := []struct {
link string
err bool
exp string
}{
@@ -42,11 +39,10 @@
}
}
}
func TestReferenceIsZettelMaterial(t *testing.T) {
- t.Parallel()
testcases := []struct {
link string
isZettel bool
isExternal bool
isLocal bool
ADDED ast/traverser.go
Index: ast/traverser.go
==================================================================
--- ast/traverser.go
+++ ast/traverser.go
@@ -0,0 +1,161 @@
+//-----------------------------------------------------------------------------
+// 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
+
+// A traverser is a Visitor that just traverses the AST and delegates node
+// spacific actions to a Visitor. This Visitor should not traverse the AST.
+
+// TopDownTraverser visits first the node and then the children nodes.
+type TopDownTraverser struct {
+ v Visitor
+}
+
+// NewTopDownTraverser creates a new traverser.
+func NewTopDownTraverser(visitor Visitor) TopDownTraverser {
+ return TopDownTraverser{visitor}
+}
+
+// VisitVerbatim has nothing to traverse.
+func (t TopDownTraverser) VisitVerbatim(vn *VerbatimNode) { t.v.VisitVerbatim(vn) }
+
+// VisitRegion traverses the content and the additional text.
+func (t TopDownTraverser) VisitRegion(rn *RegionNode) {
+ t.v.VisitRegion(rn)
+ t.VisitBlockSlice(rn.Blocks)
+ t.VisitInlineSlice(rn.Inlines)
+}
+
+// VisitHeading traverses the heading.
+func (t TopDownTraverser) VisitHeading(hn *HeadingNode) {
+ t.v.VisitHeading(hn)
+ t.VisitInlineSlice(hn.Inlines)
+}
+
+// VisitHRule traverses nothing.
+func (t TopDownTraverser) VisitHRule(hn *HRuleNode) { t.v.VisitHRule(hn) }
+
+// VisitNestedList traverses all nested list elements.
+func (t TopDownTraverser) VisitNestedList(ln *NestedListNode) {
+ t.v.VisitNestedList(ln)
+ for _, item := range ln.Items {
+ t.visitItemSlice(item)
+ }
+}
+
+// VisitDescriptionList traverses all description terms and their associated
+// descriptions.
+func (t TopDownTraverser) VisitDescriptionList(dn *DescriptionListNode) {
+ t.v.VisitDescriptionList(dn)
+ for _, defs := range dn.Descriptions {
+ t.VisitInlineSlice(defs.Term)
+ for _, descr := range defs.Descriptions {
+ t.visitDescriptionSlice(descr)
+ }
+ }
+}
+
+// VisitPara traverses the inlines of a paragraph.
+func (t TopDownTraverser) VisitPara(pn *ParaNode) {
+ t.v.VisitPara(pn)
+ t.VisitInlineSlice(pn.Inlines)
+}
+
+// VisitTable traverses all cells of the header and then row-wise all cells of
+// the table body.
+func (t TopDownTraverser) VisitTable(tn *TableNode) {
+ t.v.VisitTable(tn)
+ for _, col := range tn.Header {
+ t.VisitInlineSlice(col.Inlines)
+ }
+ for _, row := range tn.Rows {
+ for _, col := range row {
+ t.VisitInlineSlice(col.Inlines)
+ }
+ }
+}
+
+// VisitBLOB traverses nothing.
+func (t TopDownTraverser) VisitBLOB(bn *BLOBNode) { t.v.VisitBLOB(bn) }
+
+// VisitText traverses nothing.
+func (t TopDownTraverser) VisitText(tn *TextNode) { t.v.VisitText(tn) }
+
+// VisitTag traverses nothing.
+func (t TopDownTraverser) VisitTag(tn *TagNode) { t.v.VisitTag(tn) }
+
+// VisitSpace traverses nothing.
+func (t TopDownTraverser) VisitSpace(sn *SpaceNode) { t.v.VisitSpace(sn) }
+
+// VisitBreak traverses nothing.
+func (t TopDownTraverser) VisitBreak(bn *BreakNode) { t.v.VisitBreak(bn) }
+
+// VisitLink traverses the link text.
+func (t TopDownTraverser) VisitLink(ln *LinkNode) {
+ t.v.VisitLink(ln)
+ t.VisitInlineSlice(ln.Inlines)
+}
+
+// VisitImage traverses the image text.
+func (t TopDownTraverser) VisitImage(in *ImageNode) {
+ t.v.VisitImage(in)
+ t.VisitInlineSlice(in.Inlines)
+}
+
+// VisitCite traverses the cite text.
+func (t TopDownTraverser) VisitCite(cn *CiteNode) {
+ t.v.VisitCite(cn)
+ t.VisitInlineSlice(cn.Inlines)
+}
+
+// VisitFootnote traverses the footnote text.
+func (t TopDownTraverser) VisitFootnote(fn *FootnoteNode) {
+ t.v.VisitFootnote(fn)
+ t.VisitInlineSlice(fn.Inlines)
+}
+
+// VisitMark traverses nothing.
+func (t TopDownTraverser) VisitMark(mn *MarkNode) { t.v.VisitMark(mn) }
+
+// VisitFormat traverses the formatted text.
+func (t TopDownTraverser) VisitFormat(fn *FormatNode) {
+ t.v.VisitFormat(fn)
+ t.VisitInlineSlice(fn.Inlines)
+}
+
+// VisitLiteral traverses nothing.
+func (t TopDownTraverser) VisitLiteral(ln *LiteralNode) { t.v.VisitLiteral(ln) }
+
+// VisitBlockSlice traverses a block slice.
+func (t TopDownTraverser) VisitBlockSlice(bns BlockSlice) {
+ for _, bn := range bns {
+ bn.Accept(t)
+ }
+}
+
+func (t TopDownTraverser) visitItemSlice(ins ItemSlice) {
+ for _, in := range ins {
+ in.Accept(t)
+ }
+}
+
+func (t TopDownTraverser) visitDescriptionSlice(dns DescriptionSlice) {
+ for _, dn := range dns {
+ dn.Accept(t)
+ }
+}
+
+// VisitInlineSlice traverses a block slice.
+func (t TopDownTraverser) VisitInlineSlice(ins InlineSlice) {
+ for _, in := range ins {
+ in.Accept(t)
+ }
+}
ADDED ast/visitor.go
Index: ast/visitor.go
==================================================================
--- ast/visitor.go
+++ ast/visitor.go
@@ -0,0 +1,39 @@
+//-----------------------------------------------------------------------------
+// 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 ast provides the abstract syntax tree.
+package ast
+
+// Visitor is the interface all visitors must implement.
+type Visitor interface {
+ // Block nodes
+ VisitVerbatim(vn *VerbatimNode)
+ VisitRegion(rn *RegionNode)
+ VisitHeading(hn *HeadingNode)
+ VisitHRule(hn *HRuleNode)
+ VisitNestedList(ln *NestedListNode)
+ VisitDescriptionList(dn *DescriptionListNode)
+ VisitPara(pn *ParaNode)
+ VisitTable(tn *TableNode)
+ VisitBLOB(bn *BLOBNode)
+
+ // Inline nodes
+ VisitText(tn *TextNode)
+ VisitTag(tn *TagNode)
+ VisitSpace(sn *SpaceNode)
+ VisitBreak(bn *BreakNode)
+ VisitLink(ln *LinkNode)
+ VisitImage(in *ImageNode)
+ VisitCite(cn *CiteNode)
+ VisitFootnote(fn *FootnoteNode)
+ VisitMark(mn *MarkNode)
+ VisitFormat(fn *FormatNode)
+ VisitLiteral(ln *LiteralNode)
+}
DELETED ast/walk.go
Index: ast/walk.go
==================================================================
--- ast/walk.go
+++ ast/walk.go
@@ -1,48 +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
-
-// Visitor is a visitor for walking the AST.
-type Visitor interface {
- Visit(node Node) Visitor
-}
-
-// 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)
-}
-
-// WalkItemSlice traverses an item slice.
-func WalkItemSlice(v Visitor, ins ItemSlice) {
- for _, in := range ins {
- Walk(v, in)
- }
-}
-
-// WalkDescriptionSlice traverses an item slice.
-func WalkDescriptionSlice(v Visitor, dns DescriptionSlice) {
- for _, dn := range dns {
- Walk(v, dn)
- }
-}
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,28 +1,26 @@
//-----------------------------------------------------------------------------
-// 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
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/place"
+ "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)
+ PlaceWithPolicy(auth server.Auth, unprotectedPlace place.Place, rtConfig config.Config) (place.Place, 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/place"
+ "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) PlaceWithPolicy(auth server.Auth, unprotectedPlace place.Place, rtConfig config.Config) (place.Place, auth.Policy) {
+ return policy.PlaceWithPolicy(auth, a, unprotectedPlace, 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
}
DELETED auth/policy/box.go
Index: auth/policy/box.go
==================================================================
--- auth/policy/box.go
+++ auth/policy/box.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 policy
-
-import (
- "context"
-
- "zettelstore.de/z/auth"
- "zettelstore.de/z/box"
- "zettelstore.de/z/config"
- "zettelstore.de/z/query"
- "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) {
- pol := newPolicy(manager, authConfig)
- return newBox(box, pol), pol
-}
-
-// polBox implements a policy box.
-type polBox struct {
- box box.Box
- policy auth.Policy
-}
-
-// newBox creates a new policy box.
-func newBox(box box.Box, policy auth.Policy) box.Box {
- return &polBox{
- box: box,
- policy: policy,
- }
-}
-
-func (pp *polBox) Location() string {
- return pp.box.Location()
-}
-
-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)
- 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)
- 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)
-}
-
-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)
- 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)
- canRead := pp.policy.CanRead
- q = q.SetPreMatch(func(m *meta.Meta) bool { return canRead(user, m) })
- return pp.box.SelectMeta(ctx, metaSeq, q)
-}
-
-func (pp *polBox) CanUpdateZettel(ctx context.Context, zettel zettel.Zettel) bool {
- return pp.box.CanUpdateZettel(ctx, zettel)
-}
-
-func (pp *polBox) UpdateZettel(ctx context.Context, zettel zettel.Zettel) error {
- zid := zettel.Meta.Zid
- user := server.GetUser(ctx)
- if !zid.IsValid() {
- return box.ErrInvalidZid{Zid: zid.String()}
- }
- // Write existing zettel
- oldZettel, err := pp.box.GetZettel(ctx, zid)
- if err != nil {
- return err
- }
- if pp.policy.CanWrite(user, oldZettel.Meta, zettel.Meta) {
- return pp.box.UpdateZettel(ctx, zettel)
- }
- return box.NewErrNotAllowed("Write", user, zid)
-}
-
-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)
- if err != nil {
- return err
- }
- user := server.GetUser(ctx)
- if pp.policy.CanRename(user, z.Meta) {
- return pp.box.RenameZettel(ctx, curZid, newZid)
- }
- return box.NewErrNotAllowed("Rename", user, curZid)
-}
-
-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)
- if err != nil {
- return err
- }
- user := server.GetUser(ctx)
- if pp.policy.CanDelete(user, z.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,29 +58,22 @@
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:
- return true
- case meta.UserRoleCreator:
- return vis == meta.VisibilityCreator
- default:
- return false
- }
+ return true
}
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,22 +86,21 @@
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
}
}
return true
}
- switch userRole := o.manager.GetUserRole(user); userRole {
- case meta.UserRoleReader, meta.UserRoleCreator:
+ if o.manager.GetUserRole(user) == meta.UserRoleReader {
return false
}
return o.userCanCreate(user, newMeta)
}
@@ -133,20 +122,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 +136,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
}
ADDED auth/policy/place.go
Index: auth/policy/place.go
==================================================================
--- auth/policy/place.go
+++ auth/policy/place.go
@@ -0,0 +1,165 @@
+//-----------------------------------------------------------------------------
+// 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 policy provides some interfaces and implementation for authorizsation policies.
+package policy
+
+import (
+ "context"
+ "io"
+
+ "zettelstore.de/z/auth"
+ "zettelstore.de/z/config"
+ "zettelstore.de/z/domain"
+ "zettelstore.de/z/domain/id"
+ "zettelstore.de/z/domain/meta"
+ "zettelstore.de/z/place"
+ "zettelstore.de/z/search"
+ "zettelstore.de/z/web/server"
+)
+
+// PlaceWithPolicy wraps the given place inside a policy place.
+func PlaceWithPolicy(
+ auth server.Auth,
+ manager auth.AuthzManager,
+ place place.Place,
+ authConfig config.AuthConfig,
+) (place.Place, auth.Policy) {
+ pol := newPolicy(manager, authConfig)
+ return newPlace(auth, place, pol), pol
+}
+
+// polPlace implements a policy place.
+type polPlace struct {
+ auth server.Auth
+ place place.Place
+ policy auth.Policy
+}
+
+// newPlace creates a new policy place.
+func newPlace(auth server.Auth, place place.Place, policy auth.Policy) place.Place {
+ return &polPlace{
+ auth: auth,
+ place: place,
+ policy: policy,
+ }
+}
+
+func (pp *polPlace) Location() string {
+ return pp.place.Location()
+}
+
+func (pp *polPlace) CanCreateZettel(ctx context.Context) bool {
+ return pp.place.CanCreateZettel(ctx)
+}
+
+func (pp *polPlace) CreateZettel(ctx context.Context, zettel domain.Zettel) (id.Zid, error) {
+ user := pp.auth.GetUser(ctx)
+ if pp.policy.CanCreate(user, zettel.Meta) {
+ return pp.place.CreateZettel(ctx, zettel)
+ }
+ return id.Invalid, place.NewErrNotAllowed("Create", user, id.Invalid)
+}
+
+func (pp *polPlace) GetZettel(ctx context.Context, zid id.Zid) (domain.Zettel, error) {
+ zettel, err := pp.place.GetZettel(ctx, zid)
+ if err != nil {
+ return domain.Zettel{}, err
+ }
+ user := pp.auth.GetUser(ctx)
+ if pp.policy.CanRead(user, zettel.Meta) {
+ return zettel, nil
+ }
+ return domain.Zettel{}, place.NewErrNotAllowed("GetZettel", user, zid)
+}
+
+func (pp *polPlace) GetMeta(ctx context.Context, zid id.Zid) (*meta.Meta, error) {
+ m, err := pp.place.GetMeta(ctx, zid)
+ if err != nil {
+ return nil, err
+ }
+ user := pp.auth.GetUser(ctx)
+ if pp.policy.CanRead(user, m) {
+ return m, nil
+ }
+ return nil, place.NewErrNotAllowed("GetMeta", user, zid)
+}
+
+func (pp *polPlace) FetchZids(ctx context.Context) (id.Set, error) {
+ return nil, place.NewErrNotAllowed("fetch-zids", pp.auth.GetUser(ctx), id.Invalid)
+}
+
+func (pp *polPlace) SelectMeta(ctx context.Context, s *search.Search) ([]*meta.Meta, error) {
+ user := pp.auth.GetUser(ctx)
+ canRead := pp.policy.CanRead
+ s = s.AddPreMatch(func(m *meta.Meta) bool { return canRead(user, m) })
+ return pp.place.SelectMeta(ctx, s)
+}
+
+func (pp *polPlace) CanUpdateZettel(ctx context.Context, zettel domain.Zettel) bool {
+ return pp.place.CanUpdateZettel(ctx, zettel)
+}
+
+func (pp *polPlace) UpdateZettel(ctx context.Context, zettel domain.Zettel) error {
+ zid := zettel.Meta.Zid
+ user := pp.auth.GetUser(ctx)
+ if !zid.IsValid() {
+ return &place.ErrInvalidID{Zid: zid}
+ }
+ // Write existing zettel
+ oldMeta, err := pp.place.GetMeta(ctx, zid)
+ if err != nil {
+ return err
+ }
+ if pp.policy.CanWrite(user, oldMeta, zettel.Meta) {
+ return pp.place.UpdateZettel(ctx, zettel)
+ }
+ return place.NewErrNotAllowed("Write", user, zid)
+}
+
+func (pp *polPlace) AllowRenameZettel(ctx context.Context, zid id.Zid) bool {
+ return pp.place.AllowRenameZettel(ctx, zid)
+}
+
+func (pp *polPlace) RenameZettel(ctx context.Context, curZid, newZid id.Zid) error {
+ meta, err := pp.place.GetMeta(ctx, curZid)
+ if err != nil {
+ return err
+ }
+ user := pp.auth.GetUser(ctx)
+ if pp.policy.CanRename(user, meta) {
+ return pp.place.RenameZettel(ctx, curZid, newZid)
+ }
+ return place.NewErrNotAllowed("Rename", user, curZid)
+}
+
+func (pp *polPlace) CanDeleteZettel(ctx context.Context, zid id.Zid) bool {
+ return pp.place.CanDeleteZettel(ctx, zid)
+}
+
+func (pp *polPlace) DeleteZettel(ctx context.Context, zid id.Zid) error {
+ meta, err := pp.place.GetMeta(ctx, zid)
+ if err != nil {
+ return err
+ }
+ user := pp.auth.GetUser(ctx)
+ if pp.policy.CanDelete(user, meta) {
+ return pp.place.DeleteZettel(ctx, zid)
+ }
+ return place.NewErrNotAllowed("Delete", user, zid)
+}
+
+func (pp *polPlace) ReadStats(st *place.Stats) {
+ pp.place.ReadStats(st)
+}
+
+func (pp *polPlace) Dump(w io.Writer) {
+ pp.place.Dump(w)
+}
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,68 @@
//-----------------------------------------------------------------------------
-// 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,34 +72,39 @@
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 {
- return meta.GetVisibility(vis)
+func (ac *authConfig) GetVisibility(m *meta.Meta) meta.Visibility {
+ if vis, ok := m.Get(meta.KeyVisibility); ok {
+ switch vis {
+ case meta.ValueVisibilityPublic:
+ return meta.VisibilityPublic
+ case meta.ValueVisibilityOwner:
+ return meta.VisibilityOwner
+ case meta.ValueVisibilityExpert:
+ return meta.VisibilityExpert
+ }
}
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()
owner := newOwner()
owner2 := newOwner2()
zettel := newZettel()
@@ -122,25 +114,22 @@
meta *meta.Meta
exp bool
}{
// No meta
{anonUser, nil, false},
- {creator, nil, false},
{reader, nil, false},
{writer, nil, false},
{owner, nil, false},
{owner2, nil, false},
// Ordinary zettel
{anonUser, zettel, !withAuth && !readonly},
- {creator, zettel, !readonly},
{reader, zettel, !withAuth && !readonly},
{writer, zettel, !readonly},
{owner, zettel, !readonly},
{owner2, zettel, !readonly},
// User zettel
{anonUser, userZettel, !withAuth && !readonly},
- {creator, userZettel, !withAuth && !readonly},
{reader, userZettel, !withAuth && !readonly},
{writer, userZettel, !withAuth && !readonly},
{owner, userZettel, !readonly},
{owner2, userZettel, !readonly},
}
@@ -152,21 +141,19 @@
}
})
}
}
-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()
owner := newOwner()
owner2 := newOwner2()
zettel := newZettel()
publicZettel := newPublicZettel()
- creatorZettel := newCreatorZettel()
loginZettel := newLoginZettel()
ownerZettel := newOwnerZettel()
expertZettel := newExpertZettel()
userZettel := newUserZettel()
testCases := []struct {
@@ -174,66 +161,51 @@
meta *meta.Meta
exp bool
}{
// No meta
{anonUser, nil, false},
- {creator, nil, false},
{reader, nil, false},
{writer, nil, false},
{owner, nil, false},
{owner2, nil, false},
// Ordinary zettel
{anonUser, zettel, !withAuth},
- {creator, zettel, !withAuth},
{reader, zettel, true},
{writer, zettel, true},
{owner, zettel, true},
{owner2, zettel, true},
// Public zettel
{anonUser, publicZettel, true},
- {creator, publicZettel, true},
{reader, publicZettel, true},
{writer, publicZettel, true},
{owner, publicZettel, true},
{owner2, publicZettel, true},
- // Creator zettel
- {anonUser, creatorZettel, !withAuth},
- {creator, creatorZettel, true},
- {reader, creatorZettel, true},
- {writer, creatorZettel, true},
- {owner, creatorZettel, true},
- {owner2, creatorZettel, true},
// Login zettel
{anonUser, loginZettel, !withAuth},
- {creator, loginZettel, !withAuth},
{reader, loginZettel, true},
{writer, loginZettel, true},
{owner, loginZettel, true},
{owner2, loginZettel, true},
// Owner zettel
{anonUser, ownerZettel, !withAuth},
- {creator, ownerZettel, !withAuth},
{reader, ownerZettel, !withAuth},
{writer, ownerZettel, !withAuth},
{owner, ownerZettel, true},
{owner2, ownerZettel, true},
// Expert zettel
{anonUser, expertZettel, !withAuth && expert},
- {creator, expertZettel, !withAuth && expert},
{reader, expertZettel, !withAuth && expert},
{writer, expertZettel, !withAuth && expert},
{owner, expertZettel, expert},
{owner2, expertZettel, expert},
// Other user zettel
{anonUser, userZettel, !withAuth},
- {creator, userZettel, !withAuth},
{reader, userZettel, !withAuth},
{writer, userZettel, !withAuth},
{owner, userZettel, true},
{owner2, userZettel, true},
// Own user zettel
- {creator, creator, true},
{reader, reader, true},
{writer, writer, true},
{owner, owner, true},
{owner, owner2, true},
{owner2, owner, true},
@@ -250,11 +222,10 @@
}
func testWrite(t *testing.T, pol auth.Policy, withAuth, readonly, expert bool) {
t.Helper()
anonUser := newAnon()
- creator := newCreator()
reader := newReader()
writer := newWriter()
owner := newOwner()
owner2 := newOwner2()
zettel := newZettel()
@@ -262,11 +233,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()
@@ -277,117 +248,101 @@
new *meta.Meta
exp bool
}{
// No old and new meta
{anonUser, nil, nil, false},
- {creator, nil, nil, false},
{reader, nil, nil, false},
{writer, nil, nil, false},
{owner, nil, nil, false},
{owner2, nil, nil, false},
// No old meta
{anonUser, nil, zettel, false},
- {creator, nil, zettel, false},
{reader, nil, zettel, false},
{writer, nil, zettel, false},
{owner, nil, zettel, false},
{owner2, nil, zettel, false},
// No new meta
{anonUser, zettel, nil, false},
- {creator, zettel, nil, false},
{reader, zettel, nil, false},
{writer, zettel, nil, false},
{owner, zettel, nil, false},
{owner2, zettel, nil, false},
// Old an new zettel have different zettel identifier
{anonUser, zettel, publicZettel, false},
- {creator, zettel, publicZettel, false},
{reader, zettel, publicZettel, false},
{writer, zettel, publicZettel, false},
{owner, zettel, publicZettel, false},
{owner2, zettel, publicZettel, false},
// Overwrite a normal zettel
{anonUser, zettel, zettel, notAuthNotReadonly},
- {creator, zettel, zettel, notAuthNotReadonly},
{reader, zettel, zettel, notAuthNotReadonly},
{writer, zettel, zettel, !readonly},
{owner, zettel, zettel, !readonly},
{owner2, zettel, zettel, !readonly},
// Public zettel
{anonUser, publicZettel, publicZettel, notAuthNotReadonly},
- {creator, publicZettel, publicZettel, notAuthNotReadonly},
{reader, publicZettel, publicZettel, notAuthNotReadonly},
{writer, publicZettel, publicZettel, !readonly},
{owner, publicZettel, publicZettel, !readonly},
{owner2, publicZettel, publicZettel, !readonly},
// Login zettel
{anonUser, loginZettel, loginZettel, notAuthNotReadonly},
- {creator, loginZettel, loginZettel, notAuthNotReadonly},
{reader, loginZettel, loginZettel, notAuthNotReadonly},
{writer, loginZettel, loginZettel, !readonly},
{owner, loginZettel, loginZettel, !readonly},
{owner2, loginZettel, loginZettel, !readonly},
// Owner zettel
{anonUser, ownerZettel, ownerZettel, notAuthNotReadonly},
- {creator, ownerZettel, ownerZettel, notAuthNotReadonly},
{reader, ownerZettel, ownerZettel, notAuthNotReadonly},
{writer, ownerZettel, ownerZettel, notAuthNotReadonly},
{owner, ownerZettel, ownerZettel, !readonly},
{owner2, ownerZettel, ownerZettel, !readonly},
// Expert zettel
{anonUser, expertZettel, expertZettel, notAuthNotReadonly && expert},
- {creator, expertZettel, expertZettel, notAuthNotReadonly && expert},
{reader, expertZettel, expertZettel, notAuthNotReadonly && expert},
{writer, expertZettel, expertZettel, notAuthNotReadonly && expert},
{owner, expertZettel, expertZettel, !readonly && expert},
{owner2, expertZettel, expertZettel, !readonly && expert},
// Other user zettel
{anonUser, userZettel, userZettel, notAuthNotReadonly},
- {creator, userZettel, userZettel, notAuthNotReadonly},
{reader, userZettel, userZettel, notAuthNotReadonly},
{writer, userZettel, userZettel, notAuthNotReadonly},
{owner, userZettel, userZettel, !readonly},
{owner2, userZettel, userZettel, !readonly},
// Own user zettel
- {creator, creator, creator, !readonly},
{reader, reader, reader, !readonly},
{writer, writer, writer, !readonly},
{owner, owner, owner, !readonly},
{owner2, owner2, owner2, !readonly},
// Writer cannot change importand metadata of its own user zettel
{writer, writer, writerNew, notAuthNotReadonly},
// No r/o zettel
{anonUser, roFalse, roFalse, notAuthNotReadonly},
- {creator, roFalse, roFalse, notAuthNotReadonly},
{reader, roFalse, roFalse, notAuthNotReadonly},
{writer, roFalse, roFalse, !readonly},
{owner, roFalse, roFalse, !readonly},
{owner2, roFalse, roFalse, !readonly},
// Reader r/o zettel
{anonUser, roReader, roReader, false},
- {creator, roReader, roReader, false},
{reader, roReader, roReader, false},
{writer, roReader, roReader, !readonly},
{owner, roReader, roReader, !readonly},
{owner2, roReader, roReader, !readonly},
// Writer r/o zettel
{anonUser, roWriter, roWriter, false},
- {creator, roWriter, roWriter, false},
{reader, roWriter, roWriter, false},
{writer, roWriter, roWriter, false},
{owner, roWriter, roWriter, !readonly},
{owner2, roWriter, roWriter, !readonly},
// Owner r/o zettel
{anonUser, roOwner, roOwner, false},
- {creator, roOwner, roOwner, false},
{reader, roOwner, roOwner, false},
{writer, roOwner, roOwner, false},
{owner, roOwner, roOwner, false},
{owner2, roOwner, roOwner, false},
// r/o = true zettel
{anonUser, roTrue, roTrue, false},
- {creator, roTrue, roTrue, false},
{reader, roTrue, roTrue, false},
{writer, roTrue, roTrue, false},
{owner, roTrue, roTrue, false},
{owner2, roTrue, roTrue, false},
}
@@ -402,11 +357,10 @@
}
func testRename(t *testing.T, pol auth.Policy, withAuth, readonly, expert bool) {
t.Helper()
anonUser := newAnon()
- creator := newCreator()
reader := newReader()
writer := newWriter()
owner := newOwner()
owner2 := newOwner2()
zettel := newZettel()
@@ -422,60 +376,52 @@
meta *meta.Meta
exp bool
}{
// No meta
{anonUser, nil, false},
- {creator, nil, false},
{reader, nil, false},
{writer, nil, false},
{owner, nil, false},
{owner2, nil, false},
// Any zettel
{anonUser, zettel, notAuthNotReadonly},
- {creator, zettel, notAuthNotReadonly},
{reader, zettel, notAuthNotReadonly},
{writer, zettel, notAuthNotReadonly},
{owner, zettel, !readonly},
{owner2, zettel, !readonly},
// Expert zettel
{anonUser, expertZettel, notAuthNotReadonly && expert},
- {creator, expertZettel, notAuthNotReadonly && expert},
{reader, expertZettel, notAuthNotReadonly && expert},
{writer, expertZettel, notAuthNotReadonly && expert},
{owner, expertZettel, !readonly && expert},
{owner2, expertZettel, !readonly && expert},
// No r/o zettel
{anonUser, roFalse, notAuthNotReadonly},
- {creator, roFalse, notAuthNotReadonly},
{reader, roFalse, notAuthNotReadonly},
{writer, roFalse, notAuthNotReadonly},
{owner, roFalse, !readonly},
{owner2, roFalse, !readonly},
// Reader r/o zettel
{anonUser, roReader, false},
- {creator, roReader, false},
{reader, roReader, false},
{writer, roReader, notAuthNotReadonly},
{owner, roReader, !readonly},
{owner2, roReader, !readonly},
// Writer r/o zettel
{anonUser, roWriter, false},
- {creator, roWriter, false},
{reader, roWriter, false},
{writer, roWriter, false},
{owner, roWriter, !readonly},
{owner2, roWriter, !readonly},
// Owner r/o zettel
{anonUser, roOwner, false},
- {creator, roOwner, false},
{reader, roOwner, false},
{writer, roOwner, false},
{owner, roOwner, false},
{owner2, roOwner, false},
// r/o = true zettel
{anonUser, roTrue, false},
- {creator, roTrue, false},
{reader, roTrue, false},
{writer, roTrue, false},
{owner, roTrue, false},
{owner2, roTrue, false},
}
@@ -490,11 +436,10 @@
}
func testDelete(t *testing.T, pol auth.Policy, withAuth, readonly, expert bool) {
t.Helper()
anonUser := newAnon()
- creator := newCreator()
reader := newReader()
writer := newWriter()
owner := newOwner()
owner2 := newOwner2()
zettel := newZettel()
@@ -510,60 +455,52 @@
meta *meta.Meta
exp bool
}{
// No meta
{anonUser, nil, false},
- {creator, nil, false},
{reader, nil, false},
{writer, nil, false},
{owner, nil, false},
{owner2, nil, false},
// Any zettel
{anonUser, zettel, notAuthNotReadonly},
- {creator, zettel, notAuthNotReadonly},
{reader, zettel, notAuthNotReadonly},
{writer, zettel, notAuthNotReadonly},
{owner, zettel, !readonly},
{owner2, zettel, !readonly},
// Expert zettel
{anonUser, expertZettel, notAuthNotReadonly && expert},
- {creator, expertZettel, notAuthNotReadonly && expert},
{reader, expertZettel, notAuthNotReadonly && expert},
{writer, expertZettel, notAuthNotReadonly && expert},
{owner, expertZettel, !readonly && expert},
{owner2, expertZettel, !readonly && expert},
// No r/o zettel
{anonUser, roFalse, notAuthNotReadonly},
- {creator, roFalse, notAuthNotReadonly},
{reader, roFalse, notAuthNotReadonly},
{writer, roFalse, notAuthNotReadonly},
{owner, roFalse, !readonly},
{owner2, roFalse, !readonly},
// Reader r/o zettel
{anonUser, roReader, false},
- {creator, roReader, false},
{reader, roReader, false},
{writer, roReader, notAuthNotReadonly},
{owner, roReader, !readonly},
{owner2, roReader, !readonly},
// Writer r/o zettel
{anonUser, roWriter, false},
- {creator, roWriter, false},
{reader, roWriter, false},
{writer, roWriter, false},
{owner, roWriter, !readonly},
{owner2, roWriter, !readonly},
// Owner r/o zettel
{anonUser, roOwner, false},
- {creator, roOwner, false},
{reader, roOwner, false},
{writer, roOwner, false},
{owner, roOwner, false},
{owner2, roOwner, false},
// r/o = true zettel
{anonUser, roTrue, false},
- {creator, roTrue, false},
{reader, roTrue, false},
{writer, roTrue, false},
{owner, roTrue, false},
{owner2, roTrue, false},
}
@@ -575,146 +512,109 @@
}
})
}
}
-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)
- owner2Zid = id.Zid(1019)
- zettelZid = id.Zid(1021)
- visZid = id.Zid(1023)
- userZid = id.Zid(1025)
+ readerZid = id.Zid(1013)
+ writerZid = id.Zid(1015)
+ ownerZid = id.Zid(1017)
+ owner2Zid = id.Zid(1019)
+ zettelZid = id.Zid(1021)
+ visZid = id.Zid(1023)
+ userZid = id.Zid(1025)
)
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)
- 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)
- 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, "Public Zettel")
+ m.Set(meta.KeyVisibility, meta.ValueVisibilityPublic)
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 }
DELETED box/box.go
Index: box/box.go
==================================================================
--- box/box.go
+++ box/box.go
@@ -1,329 +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 box provides a generic interface to zettel boxes.
-package box
-
-import (
- "context"
- "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"
-)
-
-// 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
-
- // GetZettel retrieves a specific zettel.
- GetZettel(ctx context.Context, zid id.Zid) (zettel.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.
- RenameZettel(ctx context.Context, curZid, newZid id.Zid) error
-
- // CanDeleteZettel returns true, if box could possibly delete the given zettel.
- CanDeleteZettel(ctx context.Context, zid id.Zid) bool
-
- // 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
-
- // ReadStats populates st with box statistics
- ReadStats(st *ManagedBoxStats)
-}
-
-// ManagedBoxStats records statistics about the box.
-type ManagedBoxStats struct {
- // ReadOnly indicates that the content of a box cannot change.
- ReadOnly bool
-
- // 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.
- 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)
-}
-
-// 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)
-
- // GetAllZettel retrieves a specific zettel from all managed boxes.
- GetAllZettel(ctx context.Context, zid id.Zid) ([]zettel.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
-}
-
-// Stats record stattistics about a box.
-type Stats struct {
- // ReadOnly indicates that boxes cannot be modified.
- ReadOnly bool
-
- // NumManagedBoxes is the number of boxes managed.
- NumManagedBoxes int
-
- // Zettel is the number of zettel managed by the box, including
- // duplicates across managed boxes.
- ZettelTotal int
-
- // LastReload stores the timestamp when a full re-index was done.
- LastReload time.Time
-
- // DurLastReload is the duration of the last full re-index run.
- DurLastReload time.Duration
-
- // IndexesSinceReload counts indexing a zettel since the full re-index.
- IndexesSinceReload uint64
-
- // ZettelIndexed is the number of zettel managed by the indexer.
- ZettelIndexed int
-
- // IndexUpdates count the number of metadata updates.
- IndexUpdates uint64
-
- // IndexedWords count the different words indexed.
- IndexedWords uint64
-
- // IndexedUrls count the different URLs indexed.
- IndexedUrls uint64
-}
-
-// Manager is a box-managing box.
-type Manager interface {
- Box
- StartStopper
- Subject
-
- // ReadStats populates st with box statistics
- ReadStats(st *Stats)
-
- // Dump internal data to a Writer.
- Dump(w io.Writer)
-}
-
-// UpdateReason gives an indication, why the ObserverFunc was called.
-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
-)
-
-// UpdateInfo contains all the data about a changed zettel.
-type UpdateInfo struct {
- Box BaseBox
- Reason UpdateReason
- Zid id.Zid
-}
-
-// UpdateFunc is a function to be called when a change is detected.
-type UpdateFunc func(UpdateInfo)
-
-// Subject is a box that notifies observers about changes.
-type Subject interface {
- // RegisterObserver registers an observer that will be notified
- // if one or all zettel are found to be changed.
- RegisterObserver(UpdateFunc)
-}
-
-// Enricher is used to update metadata by adding new properties.
-type Enricher interface {
- // Enrich computes additional properties and updates the given metadata.
- // It is typically called by zettel reading methods.
- Enrich(ctx context.Context, m *meta.Meta, boxNumber int)
-}
-
-// NoEnrichContext will signal an enricher that nothing has to be done.
-// This is useful for an Indexer, but also for some box.Box calls, when
-// just the plain metadata is needed.
-func NoEnrichContext(ctx context.Context) context.Context {
- return context.WithValue(ctx, ctxNoEnrichKey, &ctxNoEnrichKey)
-}
-
-type ctxNoEnrichType struct{}
-
-var ctxNoEnrichKey ctxNoEnrichType
-
-// DoNotEnrich determines if the context is marked to not enrich metadata.
-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
-}
-
-// NewErrNotAllowed creates an new authorization error.
-func NewErrNotAllowed(op string, user *meta.Meta, zid id.Zid) error {
- return &ErrNotAllowed{
- Op: op,
- User: user,
- Zid: zid,
- }
-}
-
-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)
- }
- 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)
- }
- return fmt.Sprintf(
- "operation %q not allowed for user %v/%v",
- err.Op, err.User.GetDefault(api.KeyUserID, "?"), err.User.Zid)
-}
-
-// Is return true, if the error is of type ErrNotAllowed.
-func (*ErrNotAllowed) Is(error) bool { return true }
-
-// ErrStarted is returned when trying to start an already started box.
-var ErrStarted = errors.New("box is already started")
-
-// ErrStopped is returned if calling methods on a box that was not started.
-var ErrStopped = errors.New("box is stopped")
-
-// 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() }
-
-// 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")
-
-// 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 }
DELETED box/compbox/compbox.go
Index: box/compbox/compbox.go
==================================================================
--- box/compbox/compbox.go
+++ box/compbox/compbox.go
@@ -1,177 +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 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"
-)
-
-func init() {
- manager.Register(
- " comp",
- func(u *url.URL, cdata *manager.ConnectData) (box.ManagedBox, error) {
- 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
-}{
- 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},
-}
-
-// 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,
- }
-}
-
-// Setup remembers important values.
-func Setup(cfg *meta.Meta) { myConfig = cfg.Clone() }
-
-func (*compBox) Location() string { return "" }
-
-func (cb *compBox) GetZettel(_ context.Context, zid id.Zid) (zettel.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 {
- _, 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 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)
- }
-}
DELETED box/compbox/config.go
Index: box/compbox/config.go
==================================================================
--- box/compbox/config.go
+++ box/compbox/config.go
@@ -1,57 +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 compbox
-
-import (
- "bytes"
-
- "t73f.de/r/zsc/api"
- "zettelstore.de/z/kernel"
- "zettelstore.de/z/zettel/id"
- "zettelstore.de/z/zettel/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)
- 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: ``")
- for _, r := range p.Value {
- if r == '`' {
- buf.WriteByte('\\')
- }
- buf.WriteRune(r)
- }
- buf.WriteString("``")
- }
- }
- return buf.Bytes()
-}
DELETED box/compbox/keys.go
Index: box/compbox/keys.go
==================================================================
--- box/compbox/keys.go
+++ box/compbox/keys.go
@@ -1,43 +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 compbox
-
-import (
- "bytes"
- "fmt"
-
- "t73f.de/r/zsc/api"
- "zettelstore.de/z/kernel"
- "zettelstore.de/z/zettel/id"
- "zettelstore.de/z/zettel/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)
- return m
-}
-
-func genKeysC(*meta.Meta) []byte {
- keys := meta.GetSortedKeyDescriptions()
- var buf bytes.Buffer
- buf.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())
- }
- return buf.Bytes()
-}
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()
-}
DELETED box/compbox/manager.go
Index: box/compbox/manager.go
==================================================================
--- box/compbox/manager.go
+++ box/compbox/manager.go
@@ -1,44 +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"
-
- "t73f.de/r/zsc/api"
- "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))
- return m
-}
-
-func genManagerC(*meta.Meta) []byte {
- kvl := kernel.Main.GetServiceStatistics(kernel.BoxService)
- if len(kvl) == 0 {
- return nil
- }
- var buf bytes.Buffer
- buf.WriteString("|=Name|=Value>\n")
- for _, kv := range kvl {
- fmt.Fprintf(&buf, "| %v | %v\n", kv.Key, kv.Value)
- }
- return buf.Bytes()
-}
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()
-}
DELETED box/compbox/version.go
Index: box/compbox/version.go
==================================================================
--- box/compbox/version.go
+++ box/compbox/version.go
@@ -1,61 +0,0 @@
-//-----------------------------------------------------------------------------
-// Copyright (c) 2020-present Detlef Stern
-//
-// This file is part of Zettelstore.
-//
-// Zettelstore is licensed under the latest version of the EUPL (European Union
-// Public License). Please see file LICENSE.txt for your rights and obligations
-// under this license.
-//
-// SPDX-License-Identifier: EUPL-1.2
-// SPDX-FileCopyrightText: 2020-present Detlef Stern
-//-----------------------------------------------------------------------------
-
-package compbox
-
-import (
- "t73f.de/r/zsc/api"
- "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)
- return m
-}
-
-func genVersionBuildM(zid id.Zid) *meta.Meta {
- m := getVersionMeta(zid, "Zettelstore Version")
- m.Set(api.KeyCreated, kernel.Main.GetConfig(kernel.CoreService, kernel.CoreVTime).(string))
- m.Set(api.KeyVisibility, api.ValueVisibilityLogin)
- return m
-}
-func genVersionBuildC(*meta.Meta) []byte {
- return []byte(kernel.Main.GetConfig(kernel.CoreService, kernel.CoreVersion).(string))
-}
-
-func genVersionHostM(zid id.Zid) *meta.Meta {
- m := getVersionMeta(zid, "Zettelstore Host")
- m.Set(api.KeyCreated, kernel.Main.GetConfig(kernel.CoreService, kernel.CoreStarted).(string))
- return m
-}
-func genVersionHostC(*meta.Meta) []byte {
- return []byte(kernel.Main.GetConfig(kernel.CoreService, kernel.CoreHostname).(string))
-}
-
-func genVersionOSM(zid id.Zid) *meta.Meta {
- 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...)
-}
DELETED box/constbox/base.css
Index: box/constbox/base.css
==================================================================
--- box/constbox/base.css
+++ box/constbox/base.css
@@ -1,256 +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
- *-----------------------------------------------------------------------------
- */
-
-*,*::before,*::after {
- box-sizing: border-box;
- }
- html {
- font-size: 1rem;
- font-family: serif;
- scroll-behavior: smooth;
- height: 100%;
- }
- body {
- margin: 0;
- min-height: 100vh;
- line-height: 1.4;
- background-color: #f8f8f8 ;
- height: 100%;
- }
- nav.zs-menu {
- background-color: hsl(210, 28%, 90%);
- overflow: auto;
- white-space: nowrap;
- font-family: sans-serif;
- padding-left: .5rem;
- }
- nav.zs-menu > a {
- float:left;
- display: block;
- 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 form input[type=text] {
- padding: .12rem;
- border: none;
- margin-top: .25rem;
- margin-right: .5rem;
- }
- .zs-dropdown {
- float: left;
- overflow: hidden;
- }
- .zs-dropdown > button {
- font-size: 16px;
- border: none;
- outline: none;
- color: black;
- padding:.41rem .5rem;
- background-color: inherit;
- font-family: inherit;
- margin: 0;
- }
- .zs-dropdown-content {
- display: none;
- position: absolute;
- background-color: #f9f9f9;
- min-width: 160px;
- box-shadow: 0px 8px 16px 0px rgba(0,0,0,0.2);
- z-index: 1;
- }
- .zs-dropdown-content > a {
- float: none;
- color: black;
- 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 }
- article header {
- padding: 0;
- margin: 0;
- }
- h1,h2,h3,h4,h5,h6 { font-family:sans-serif; font-weight:normal }
- h1 { font-size:1.5rem; margin:.65rem 0 }
- 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 }
- 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 }
- 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 {
- text-align: left;
- padding: .25rem .5rem;
- border-bottom: 1px solid hsl(0, 0%, 85%)
- }
- main form {
- padding: 0 .5em;
- margin: .5em 0 0 0;
- }
- main form:after {
- content: ".";
- 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 }
- label { font-family: sans-serif; font-size:.9rem }
- textarea {
- font-family: monospace;
- resize: vertical;
- width: 100%;
- }
- .zs-input {
- padding: .5em;
- 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 {
- padding-top: .5rem;
- border-top: 1px solid;
- }
- kbd { font-family:monospace }
- code,pre {
- font-family: monospace;
- font-size: 85%;
- }
- code {
- padding: .1rem .2rem;
- background: #f0f0f0;
- border: 1px solid #ccc;
- border-radius: .25rem;
- }
- pre {
- padding: .5rem .7rem;
- max-width: 100%;
- overflow: auto;
- border: 1px solid #ccc;
- border-radius: .5rem;
- background: #f0f0f0;
- }
- pre code {
- font-size: 95%;
- position: relative;
- padding: 0;
- border: none;
- }
- div.zs-indication {
- padding: .5rem .7rem;
- max-width: 100%;
- border-radius: .5rem;
- border: 1px solid black;
- }
- 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-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 }
- .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 }
- details > summary {
- width: 100%;
- background-color: #eee;
- font-family:sans-serif;
- }
- details > ul {
- margin-top:0;
- padding-left:2rem;
- background-color: #eee;
- }
- 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;
- }
- }
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!"))))
-)))
DELETED box/constbox/constbox.go
Index: box/constbox/constbox.go
==================================================================
--- box/constbox/constbox.go
+++ box/constbox/constbox.go
@@ -1,505 +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 constbox puts zettel inside the executable.
-package constbox
-
-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"
-)
-
-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
-
-type constZettel struct {
- header constHeader
- content zettel.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]
- 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")
-}
-
-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)},
- 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,
- },
- zettel.NewContent(contentBaseSxn)},
- id.LoginTemplateZid: {
- constHeader{
- api.KeyTitle: "Zettelstore Login Form HTML Template",
- api.KeyRole: api.ValueRoleConfiguration,
- api.KeySyntax: meta.SyntaxSxn,
- api.KeyCreated: "20200804111624",
- api.KeyModified: "20240219145200",
- api.KeyVisibility: api.ValueVisibilityExpert,
- },
- zettel.NewContent(contentLoginSxn)},
- id.ZettelTemplateZid: {
- constHeader{
- api.KeyTitle: "Zettelstore Zettel HTML Template",
- api.KeyRole: api.ValueRoleConfiguration,
- api.KeySyntax: meta.SyntaxSxn,
- api.KeyCreated: "20230510155300",
- api.KeyModified: "20240219145100",
- api.KeyVisibility: api.ValueVisibilityExpert,
- },
- zettel.NewContent(contentZettelSxn)},
- id.InfoTemplateZid: {
- constHeader{
- api.KeyTitle: "Zettelstore Info HTML Template",
- api.KeyRole: api.ValueRoleConfiguration,
- api.KeySyntax: meta.SyntaxSxn,
- api.KeyCreated: "20200804111624",
- api.KeyModified: "20240219145200",
- api.KeyVisibility: api.ValueVisibilityExpert,
- },
- zettel.NewContent(contentInfoSxn)},
- id.FormTemplateZid: {
- constHeader{
- api.KeyTitle: "Zettelstore Form HTML Template",
- api.KeyRole: api.ValueRoleConfiguration,
- api.KeySyntax: meta.SyntaxSxn,
- api.KeyCreated: "20200804111624",
- api.KeyModified: "20240219145200",
- api.KeyVisibility: api.ValueVisibilityExpert,
- },
- zettel.NewContent(contentFormSxn)},
- id.RenameTemplateZid: {
- constHeader{
- api.KeyTitle: "Zettelstore Rename Form HTML Template",
- api.KeyRole: api.ValueRoleConfiguration,
- api.KeySyntax: meta.SyntaxSxn,
- api.KeyCreated: "20200804111624",
- api.KeyModified: "20240219145200",
- api.KeyVisibility: api.ValueVisibilityExpert,
- },
- zettel.NewContent(contentRenameSxn)},
- id.DeleteTemplateZid: {
- constHeader{
- api.KeyTitle: "Zettelstore Delete HTML Template",
- api.KeyRole: api.ValueRoleConfiguration,
- api.KeySyntax: meta.SyntaxSxn,
- api.KeyCreated: "20200804111624",
- api.KeyModified: "20240219145200",
- api.KeyVisibility: api.ValueVisibilityExpert,
- },
- zettel.NewContent(contentDeleteSxn)},
- id.ListTemplateZid: {
- constHeader{
- api.KeyTitle: "Zettelstore List Zettel HTML Template",
- api.KeyRole: api.ValueRoleConfiguration,
- api.KeySyntax: meta.SyntaxSxn,
- api.KeyCreated: "20230704122100",
- api.KeyModified: "20240219145200",
- api.KeyVisibility: api.ValueVisibilityExpert,
- },
- zettel.NewContent(contentListZettelSxn)},
- id.ErrorTemplateZid: {
- constHeader{
- api.KeyTitle: "Zettelstore Error HTML Template",
- api.KeyRole: api.ValueRoleConfiguration,
- api.KeySyntax: meta.SyntaxSxn,
- api.KeyCreated: "20210305133215",
- api.KeyModified: "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 */"))},
- 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,
- },
- zettel.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)},
- id.DefaultHomeZid: {
- constHeader{
- api.KeyTitle: "Home",
- api.KeyRole: api.ValueRoleZettel,
- api.KeySyntax: meta.SyntaxZmk,
- api.KeyLang: api.ValueLangEN,
- api.KeyCreated: "20210210190757",
- },
- zettel.NewContent(contentHomeZettel)},
-}
-
-//go:embed license.txt
-var contentLicense []byte
-
-//go:embed contributors.zettel
-var contentContributors []byte
-
-//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
-
-//go:embed base.css
-var contentBaseCSS []byte
-
-//go:embed emoji_spin.gif
-var contentEmoji []byte
-
-//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
-
-//go:embed home.zettel
-var contentHomeZettel []byte
DELETED box/constbox/contributors.zettel
Index: box/constbox/contributors.zettel
==================================================================
--- box/constbox/contributors.zettel
+++ box/constbox/contributors.zettel
@@ -1,8 +0,0 @@
-Zettelstore is a software for humans made from humans.
-
-=== Licensor(s)
-* Detlef Stern [[mailto:ds@zettelstore.de]]
-** Main author
-** Maintainer
-
-=== Contributors
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"))))
-)
DELETED box/constbox/dependencies.zettel
Index: box/constbox/dependencies.zettel
==================================================================
--- box/constbox/dependencies.zettel
+++ box/constbox/dependencies.zettel
@@ -1,144 +0,0 @@
-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.
-
-=== Go runtime and associated libraries
-; License
-: BSD 3-Clause "New" or "Revised" License
-```
-Copyright (c) 2009 The Go 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.
-```
-
-=== 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.
-```
-
-=== yuin/goldmark
-; URL & Source
-: [[https://github.com/yuin/goldmark]]
-; License
-: MIT License
-```
-MIT License
-
-Copyright (c) 2019 Yusuke Inuzuka
-
-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.
-```
-
-=== 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.
DELETED box/constbox/emoji_spin.gif
Index: box/constbox/emoji_spin.gif
==================================================================
--- box/constbox/emoji_spin.gif
+++ box/constbox/emoji_spin.gif
cannot compute difference between binary files
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
-)
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")))
- ))
-)
DELETED box/constbox/home.zettel
Index: box/constbox/home.zettel
==================================================================
--- box/constbox/home.zettel
+++ box/constbox/home.zettel
@@ -1,43 +0,0 @@
-=== Thank you for using Zettelstore!
-
-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.
-
-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 Operating System|00000000000003]]
-* [[Zettelstore Startup Configuration|00000000000096]]
-* [[Zettelstore Runtime Configuration|00000000000100]]
-
-Additionally, you have to describe, what you have done before that error occurs
-and what you have expected instead.
-Please do not forget to include the error message, if there is one.
-
-Some of above Zettelstore zettel can only be retrieved if you enabled ""expert mode"".
-Otherwise, only some zettel are linked.
-To enable expert mode, edit the zettel [[Zettelstore Runtime Configuration|00000000000100]]:
-please set the metadata value of the key ''expert-mode'' to true.
-To do you, enter the string ''expert-mode:true'' inside the edit view of the metadata.
-
-=== Information about this zettel
-This zettel is your home zettel.
-It is part of the Zettelstore software itself.
-Every time you click on the [[Home|//]] link in the menu bar, you will be redirected to this zettel.
-
-You can change the content of this zettel by clicking on ""Edit"" above.
-This allows you to customize your home zettel.
-
-Alternatively, you can designate another zettel as your home zettel.
-Edit the [[Zettelstore Runtime Configuration|00000000000100]] by adding the metadata key ''home-zettel''.
-Its value is the identifier of the zettel that should act as the new home zettel.
-You will find the identifier of each zettel between their ""Edit"" and the ""Info"" link above.
-The identifier of this zettel is ''00010000000000''.
-If you provide a wrong identifier, this zettel will be shown as the home zettel.
-Take a look inside the manual for further details.
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))
- )
- )
-)
DELETED box/constbox/license.txt
Index: box/constbox/license.txt
==================================================================
--- box/constbox/license.txt
+++ box/constbox/license.txt
@@ -1,295 +0,0 @@
-Copyright (c) 2020-present 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
-EU. The English version is included here. Please see
-https://joinup.ec.europa.eu/community/eupl/og_page/eupl for official
-translations of the other languages.
-
-
--------------------------------------------------------------------------------
-
-
-EUROPEAN UNION PUBLIC LICENCE v. 1.2
-EUPL © the European Union 2007, 2016
-
-This European Union Public Licence (the ‘EUPL’) applies to the Work (as defined
-below) which is provided under the terms of this Licence. Any use of the Work,
-other than as authorised under this Licence is prohibited (to the extent such
-use is covered by a right of the copyright holder of the Work).
-
-The Work is provided under the terms of this Licence when the Licensor (as
-defined below) has placed the following notice immediately following the
-copyright notice for the Work:
-
- Licensed under the EUPL
-
-or has expressed by any other means his willingness to license under the EUPL.
-
-1. Definitions
-
-In this Licence, the following terms have the following meaning:
-
-— ‘The Licence’: this Licence.
-— ‘The Original Work’: the work or software distributed or communicated by the
- Licensor under this Licence, available as Source Code and also as Executable
- Code as the case may be.
-— ‘Derivative Works’: the works or software that could be created by the
- Licensee, based upon the Original Work or modifications thereof. This Licence
- does not define the extent of modification or dependence on the Original Work
- required in order to classify a work as a Derivative Work; this extent is
- determined by copyright law applicable in the country mentioned in Article
- 15.
-— ‘The Work’: the Original Work or its Derivative Works.
-— ‘The Source Code’: the human-readable form of the Work which is the most
- convenient for people to study and modify.
-— ‘The Executable Code’: any code which has generally been compiled and which
- is meant to be interpreted by a computer as a program.
-— ‘The Licensor’: the natural or legal person that distributes or communicates
- the Work under the Licence.
-— ‘Contributor(s)’: any natural or legal person who modifies the Work under the
- Licence, or otherwise contributes to the creation of a Derivative Work.
-— ‘The Licensee’ or ‘You’: any natural or legal person who makes any usage of
- the Work under the terms of the Licence.
-— ‘Distribution’ or ‘Communication’: any act of selling, giving, lending,
- renting, distributing, communicating, transmitting, or otherwise making
- available, online or offline, copies of the Work or providing access to its
- essential functionalities at the disposal of any other natural or legal
- person.
-
-2. Scope of the rights granted by the Licence
-
-The Licensor hereby grants You a worldwide, royalty-free, non-exclusive,
-sublicensable licence to do the following, for the duration of copyright vested
-in the Original Work:
-
-— use the Work in any circumstance and for all usage,
-— reproduce the Work,
-— modify the Work, and make Derivative Works based upon the Work,
-— communicate to the public, including the right to make available or display
- the Work or copies thereof to the public and perform publicly, as the case
- may be, the Work,
-— distribute the Work or copies thereof,
-— lend and rent the Work or copies thereof,
-— sublicense rights in the Work or copies thereof.
-
-Those rights can be exercised on any media, supports and formats, whether now
-known or later invented, as far as the applicable law permits so.
-
-In the countries where moral rights apply, the Licensor waives his right to
-exercise his moral right to the extent allowed by law in order to make
-effective the licence of the economic rights here above listed.
-
-The Licensor grants to the Licensee royalty-free, non-exclusive usage rights to
-any patents held by the Licensor, to the extent necessary to make use of the
-rights granted on the Work under this Licence.
-
-3. Communication of the Source Code
-
-The Licensor may provide the Work either in its Source Code form, or as
-Executable Code. If the Work is provided as Executable Code, the Licensor
-provides in addition a machine-readable copy of the Source Code of the Work
-along with each copy of the Work that the Licensor distributes or indicates, in
-a notice following the copyright notice attached to the Work, a repository
-where the Source Code is easily and freely accessible for as long as the
-Licensor continues to distribute or communicate the Work.
-
-4. Limitations on copyright
-
-Nothing in this Licence is intended to deprive the Licensee of the benefits
-from any exception or limitation to the exclusive rights of the rights owners
-in the Work, of the exhaustion of those rights or of other applicable
-limitations thereto.
-
-5. Obligations of the Licensee
-
-The grant of the rights mentioned above is subject to some restrictions and
-obligations imposed on the Licensee. Those obligations are the following:
-
-Attribution right: The Licensee shall keep intact all copyright, patent or
-trademarks notices and all notices that refer to the Licence and to the
-disclaimer of warranties. The Licensee must include a copy of such notices and
-a copy of the Licence with every copy of the Work he/she distributes or
-communicates. The Licensee must cause any Derivative Work to carry prominent
-notices stating that the Work has been modified and the date of modification.
-
-Copyleft clause: If the Licensee distributes or communicates copies of the
-Original Works or Derivative Works, this Distribution or Communication will be
-done under the terms of this Licence or of a later version of this Licence
-unless the Original Work is expressly distributed only under this version of
-the Licence — for example by communicating ‘EUPL v. 1.2 only’. The Licensee
-(becoming Licensor) cannot offer or impose any additional terms or conditions
-on the Work or Derivative Work that alter or restrict the terms of the Licence.
-
-Compatibility clause: If the Licensee Distributes or Communicates Derivative
-Works or copies thereof based upon both the Work and another work licensed
-under a Compatible Licence, this Distribution or Communication can be done
-under the terms of this Compatible Licence. For the sake of this clause,
-‘Compatible Licence’ refers to the licences listed in the appendix attached to
-this Licence. Should the Licensee's obligations under the Compatible Licence
-conflict with his/her obligations under this Licence, the obligations of the
-Compatible Licence shall prevail.
-
-Provision of Source Code: When distributing or communicating copies of the
-Work, the Licensee will provide a machine-readable copy of the Source Code or
-indicate a repository where this Source will be easily and freely available for
-as long as the Licensee continues to distribute or communicate the Work.
-
-Legal Protection: This Licence does not grant permission to use the trade
-names, trademarks, service marks, or names of the Licensor, except as required
-for reasonable and customary use in describing the origin of the Work and
-reproducing the content of the copyright notice.
-
-6. Chain of Authorship
-
-The original Licensor warrants that the copyright in the Original Work granted
-hereunder is owned by him/her or licensed to him/her and that he/she has the
-power and authority to grant the Licence.
-
-Each Contributor warrants that the copyright in the modifications he/she brings
-to the Work are owned by him/her or licensed to him/her and that he/she has the
-power and authority to grant the Licence.
-
-Each time You accept the Licence, the original Licensor and subsequent
-Contributors grant You a licence to their contributions to the Work, under the
-terms of this Licence.
-
-7. Disclaimer of Warranty
-
-The Work is a work in progress, which is continuously improved by numerous
-Contributors. It is not a finished work and may therefore contain defects or
-‘bugs’ inherent to this type of development.
-
-For the above reason, the Work is provided under the Licence on an ‘as is’
-basis and without warranties of any kind concerning the Work, including without
-limitation merchantability, fitness for a particular purpose, absence of
-defects or errors, accuracy, non-infringement of intellectual property rights
-other than copyright as stated in Article 6 of this Licence.
-
-This disclaimer of warranty is an essential part of the Licence and a condition
-for the grant of any rights to the Work.
-
-8. Disclaimer of Liability
-
-Except in the cases of wilful misconduct or damages directly caused to natural
-persons, the Licensor will in no event be liable for any direct or indirect,
-material or moral, damages of any kind, arising out of the Licence or of the
-use of the Work, including without limitation, damages for loss of goodwill,
-work stoppage, computer failure or malfunction, loss of data or any commercial
-damage, even if the Licensor has been advised of the possibility of such
-damage. However, the Licensor will be liable under statutory product liability
-laws as far such laws apply to the Work.
-
-9. Additional agreements
-
-While distributing the Work, You may choose to conclude an additional
-agreement, defining obligations or services consistent with this Licence.
-However, if accepting obligations, You may act only on your own behalf and on
-your sole responsibility, not on behalf of the original Licensor or any other
-Contributor, and only if You agree to indemnify, defend, and hold each
-Contributor harmless for any liability incurred by, or claims asserted against
-such Contributor by the fact You have accepted any warranty or additional
-liability.
-
-10. Acceptance of the Licence
-
-The provisions of this Licence can be accepted by clicking on an icon ‘I agree’
-placed under the bottom of a window displaying the text of this Licence or by
-affirming consent in any other similar way, in accordance with the rules of
-applicable law. Clicking on that icon indicates your clear and irrevocable
-acceptance of this Licence and all of its terms and conditions.
-
-Similarly, you irrevocably accept this Licence and all of its terms and
-conditions by exercising any rights granted to You by Article 2 of this
-Licence, such as the use of the Work, the creation by You of a Derivative Work
-or the Distribution or Communication by You of the Work or copies thereof.
-
-11. Information to the public
-
-In case of any Distribution or Communication of the Work by means of electronic
-communication by You (for example, by offering to download the Work from
-a remote location) the distribution channel or media (for example, a website)
-must at least provide to the public the information requested by the applicable
-law regarding the Licensor, the Licence and the way it may be accessible,
-concluded, stored and reproduced by the Licensee.
-
-12. Termination of the Licence
-
-The Licence and the rights granted hereunder will terminate automatically upon
-any breach by the Licensee of the terms of the Licence.
-
-Such a termination will not terminate the licences of any person who has
-received the Work from the Licensee under the Licence, provided such persons
-remain in full compliance with the Licence.
-
-13. Miscellaneous
-
-Without prejudice of Article 9 above, the Licence represents the complete
-agreement between the Parties as to the Work.
-
-If any provision of the Licence is invalid or unenforceable under applicable
-law, this will not affect the validity or enforceability of the Licence as
-a whole. Such provision will be construed or reformed so as necessary to make
-it valid and enforceable.
-
-The European Commission may publish other linguistic versions or new versions
-of this Licence or updated versions of the Appendix, so far this is required
-and reasonable, without reducing the scope of the rights granted by the
-Licence. New versions of the Licence will be published with a unique version
-number.
-
-All linguistic versions of this Licence, approved by the European Commission,
-have identical value. Parties can take advantage of the linguistic version of
-their choice.
-
-14. Jurisdiction
-
-Without prejudice to specific agreement between parties,
-
-— any litigation resulting from the interpretation of this License, arising
- between the European Union institutions, bodies, offices or agencies, as
- a Licensor, and any Licensee, will be subject to the jurisdiction of the
- Court of Justice of the European Union, as laid down in article 272 of the
- Treaty on the Functioning of the European Union,
-— any litigation arising between other parties and resulting from the
- interpretation of this License, will be subject to the exclusive jurisdiction
- of the competent court where the Licensor resides or conducts its primary
- business.
-
-15. Applicable Law
-
-Without prejudice to specific agreement between parties,
-
-— this Licence shall be governed by the law of the European Union Member State
- where the Licensor has his seat, resides or has his registered office,
-— this licence shall be governed by Belgian law if the Licensor has no seat,
- residence or registered office inside a European Union Member State.
-
-
- Appendix
-
-
-‘Compatible Licences’ according to Article 5 EUPL are:
-
-— GNU General Public License (GPL) v. 2, v. 3
-— GNU Affero General Public License (AGPL) v. 3
-— Open Software License (OSL) v. 2.1, v. 3.0
-— Eclipse Public License (EPL) v. 1.0
-— CeCILL v. 2.0, v. 2.1
-— Mozilla Public Licence (MPL) v. 2
-— GNU Lesser General Public Licence (LGPL) v. 2.1, v. 3
-— Creative Commons Attribution-ShareAlike v. 3.0 Unported (CC BY-SA 3.0) for
- works other than software
-— European Union Public Licence (EUPL) v. 1.1, v. 1.2
-— Québec Free and Open-Source Licence — Reciprocity (LiLiQ-R) or Strong
- Reciprocity (LiLiQ-R+)
-
-The European Commission may update this Appendix to later versions of the above
-licences without producing a new version of the EUPL, as long as they provide
-the rights granted in Article 2 of this Licence and protect the covered Source
-Code from exclusive appropriation.
-
-All other changes or additions to this Appendix require the production of a new
-EUPL version.
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")))
- )
- )
- )
-)
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"))))
- )
-)
DELETED box/constbox/newtoc.zettel
Index: box/constbox/newtoc.zettel
==================================================================
--- box/constbox/newtoc.zettel
+++ box/constbox/newtoc.zettel
@@ -1,6 +0,0 @@
-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))))))
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)))
- )
-)
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)))))
- ))
- )
-)
DELETED box/dirbox/dirbox.go
Index: box/dirbox/dirbox.go
==================================================================
--- box/dirbox/dirbox.go
+++ box/dirbox/dirbox.go
@@ -1,408 +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 dirbox provides a directory-based zettel box.
-package dirbox
-
-import (
- "context"
- "errors"
- "net/url"
- "os"
- "path/filepath"
- "sync"
-
- "zettelstore.de/z/box"
- "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"
-)
-
-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
- }
- dp := dirBox{
- log: log,
- number: cdata.Number,
- location: u.String(),
- readonly: box.GetQueryBool(u, "readonly"),
- cdata: *cdata,
- dir: path,
- notifySpec: getDirSrvInfo(log, u.Query().Get("type")),
- fSrvs: makePrime(uint32(box.GetQueryInt(u, "worker", 1, 7, 1499))),
- }
- 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
-
-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
-}
-
-func getDirPath(u *url.URL) string {
- if u.Opaque != "" {
- return filepath.Clean(u.Opaque)
- }
- return filepath.Clean(u.Path)
-}
-
-// 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
- 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 {
- dp.mxCmds.Lock()
- defer dp.mxCmds.Unlock()
- dp.fCmds = make([]chan fileCmd, 0, dp.fSrvs)
- for i := range dp.fSrvs {
- cc := make(chan fileCmd)
- go fileService(i, dp.log.Clone().Str("sub", "file").Uint("fn", uint64(i)).Child(), dp.dir, cc)
- dp.fCmds = append(dp.fCmds, cc)
- }
-
- 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) {
- dirSrv := dp.dirSrv
- dp.dirSrv = nil
- if dirSrv != nil {
- dirSrv.Stop()
- }
- dp.stopFileServices()
-}
-
-func (dp *dirBox) stopFileServices() {
- for _, c := range dp.fCmds {
- close(c)
- }
-}
-
-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) getFileChan(zid id.Zid) chan fileCmd {
- // Based on https://en.wikipedia.org/wiki/Fowler%E2%80%93Noll%E2%80%93Vo_hash_function
- sum := 2166136261 ^ uint32(zid)
- sum *= 16777619
- sum ^= uint32(zid >> 32)
- sum *= 16777619
-
- dp.mxCmds.RLock()
- defer dp.mxCmds.RUnlock()
- return dp.fCmds[sum%dp.fSrvs]
-}
-
-func (dp *dirBox) CanCreateZettel(_ context.Context) bool {
- return !dp.readonly
-}
-
-func (dp *dirBox) CreateZettel(ctx context.Context, zettel zettel.Zettel) (id.Zid, error) {
- if dp.readonly {
- return id.Invalid, box.ErrReadOnly
- }
-
- newZid, err := dp.dirSrv.SetNewDirEntry()
- if err != nil {
- return id.Invalid, err
- }
- meta := zettel.Meta
- meta.Zid = newZid
- entry := notify.DirEntry{Zid: newZid}
- dp.updateEntryFromMetaContent(&entry, meta, zettel.Content)
-
- err = dp.srvSetZettel(ctx, &entry, zettel)
- if err == nil {
- err = dp.dirSrv.UpdateDirEntry(&entry)
- }
- dp.notifyChanged(meta.Zid)
- dp.log.Trace().Err(err).Zid(meta.Zid).Msg("CreateZettel")
- return meta.Zid, err
-}
-
-func (dp *dirBox) GetZettel(ctx context.Context, zid id.Zid) (zettel.Zettel, error) {
- entry := dp.dirSrv.GetDirEntry(zid)
- if !entry.IsValid() {
- return zettel.Zettel{}, box.ErrZettelNotFound{Zid: zid}
- }
- m, c, err := dp.srvGetMetaContent(ctx, entry, zid)
- if err != nil {
- return zettel.Zettel{}, err
- }
- zettel := zettel.Zettel{Meta: m, Content: zettel.NewContent(c)}
- dp.log.Trace().Zid(zid).Msg("GetZettel")
- return zettel, nil
-}
-
-func (dp *dirBox) HasZettel(_ context.Context, zid id.Zid) bool {
- return dp.dirSrv.GetDirEntry(zid).IsValid()
-}
-
-func (dp *dirBox) ApplyZid(_ context.Context, handle box.ZidFunc, constraint query.RetrievePredicate) error {
- entries := dp.dirSrv.GetDirEntries(constraint)
- dp.log.Trace().Int("entries", int64(len(entries))).Msg("ApplyZid")
- for _, entry := range entries {
- handle(entry.Zid)
- }
- return nil
-}
-
-func (dp *dirBox) ApplyMeta(ctx context.Context, handle box.MetaFunc, constraint query.RetrievePredicate) error {
- entries := dp.dirSrv.GetDirEntries(constraint)
- dp.log.Trace().Int("entries", int64(len(entries))).Msg("ApplyMeta")
-
- // The following loop could be parallelized if needed for performance.
- for _, entry := range entries {
- m, err := dp.srvGetMeta(ctx, entry, entry.Zid)
- if err != nil {
- dp.log.Trace().Err(err).Msg("ApplyMeta/getMeta")
- return err
- }
- dp.cdata.Enricher.Enrich(ctx, m, dp.number)
- handle(m)
- }
- return nil
-}
-
-func (dp *dirBox) CanUpdateZettel(context.Context, zettel.Zettel) bool {
- return !dp.readonly
-}
-
-func (dp *dirBox) UpdateZettel(ctx context.Context, zettel zettel.Zettel) error {
- if dp.readonly {
- return box.ErrReadOnly
- }
-
- meta := zettel.Meta
- zid := meta.Zid
- if !zid.IsValid() {
- return box.ErrInvalidZid{Zid: zid.String()}
- }
- entry := dp.dirSrv.GetDirEntry(zid)
- if !entry.IsValid() {
- // Existing zettel, but new in this box.
- entry = ¬ify.DirEntry{Zid: zid}
- }
- dp.updateEntryFromMetaContent(entry, meta, zettel.Content)
- dp.dirSrv.UpdateDirEntry(entry)
- err := dp.srvSetZettel(ctx, entry, zettel)
- if err == nil {
- dp.notifyChanged(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) AllowRenameZettel(context.Context, 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}
- }
- 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()
-}
-
-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")
- 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")
-}
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
- }
- }
-}
DELETED box/dirbox/service.go
Index: box/dirbox/service.go
==================================================================
--- box/dirbox/service.go
+++ box/dirbox/service.go
@@ -1,390 +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 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")
-}
-
-type fileCmd interface {
- run(string)
-}
-
-const serviceTimeout = 5 * time.Second // must be shorter than the web servers timeout values for reading+writing.
-
-// COMMAND: srvGetMeta ----------------------------------------
-//
-// 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)
- 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()
- }
-}
-
-type fileGetMeta struct {
- entry *notify.DirEntry
- rc chan<- resGetMeta
-}
-type resGetMeta struct {
- meta *meta.Meta
- err error
-}
-
-func (cmd *fileGetMeta) run(dirPath string) {
- var m *meta.Meta
- var err error
-
- entry := cmd.entry
- zid := entry.Zid
- if metaName := entry.MetaName; metaName == "" {
- contentName := entry.ContentName
- contentExt := entry.ContentExt
- if contentName == "" || contentExt == "" {
- 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))
- }
- if err == nil {
- cmdCleanupMeta(m, entry)
- }
- cmd.rc <- resGetMeta{m, err}
-}
-
-// COMMAND: srvGetMetaContent ----------------------------------------
-//
-// 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)
- 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()
- }
-}
-
-type fileGetMetaContent struct {
- entry *notify.DirEntry
- rc chan<- resGetMetaContent
-}
-type resGetMetaContent struct {
- meta *meta.Meta
- content []byte
- err error
-}
-
-func (cmd *fileGetMetaContent) run(dirPath string) {
- var m *meta.Meta
- var content []byte
- 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
- }
- }
- }
- 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
- }
- }
- cmd.rc <- err
-}
-
-// Utility functions ----------------------------------------
-
-func parseMetaFile(zid id.Zid, path string) (*meta.Meta, error) {
- src, err := os.ReadFile(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)
- if err != nil {
- return nil, 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) {
- filebox.CleanupMeta(
- m,
- entry.Zid,
- entry.ContentExt,
- entry.MetaName != "",
- entry.UselessFiles,
- )
-}
-
-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: ")
- if err == nil {
- _, err = w.Write(zid.Bytes())
- if err == nil {
- _, err = io.WriteString(w, "\n")
- }
- }
- return err
-}
-
-func writeFileContent(path string, content []byte) error {
- f, err := openFileWrite(path)
- if err == nil {
- _, err = f.Write(content)
- if err1 := f.Close(); err == nil {
- err = err1
- }
- }
- return err
-}
DELETED box/filebox/filebox.go
Index: box/filebox/filebox.go
==================================================================
--- box/filebox/filebox.go
+++ box/filebox/filebox.go
@@ -1,97 +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 filebox provides boxes that are stored in a file.
-package filebox
-
-import (
- "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"
-)
-
-func init() {
- manager.Register("file", func(u *url.URL, cdata *manager.ConnectData) (box.ManagedBox, error) {
- path := getFilepathFromURL(u)
- 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 {
- name := u.Opaque
- if name == "" {
- name = u.Path
- }
- components := strings.Split(name, "/")
- fileName := filepath.Join(components...)
- if len(components) > 0 && components[0] == "" {
- return "/" + fileName
- }
- return fileName
-}
-
-var alternativeSyntax = map[string]string{
- "htm": "html",
-}
-
-func calculateSyntax(ext string) string {
- ext = strings.ToLower(ext)
- if syntax, ok := alternativeSyntax[ext]; ok {
- return syntax
- }
- return ext
-}
-
-// CalcDefaultMeta returns metadata with default values for the given entry.
-func CalcDefaultMeta(zid id.Zid, ext string) *meta.Meta {
- m := meta.New(zid)
- m.Set(api.KeySyntax, calculateSyntax(ext))
- return m
-}
-
-// CleanupMeta enhances the given metadata.
-func CleanupMeta(m *meta.Meta, zid id.Zid, ext string, inMeta bool, uselessFiles []string) {
- if inMeta {
- if syntax, ok := m.Get(api.KeySyntax); !ok || syntax == "" {
- dm := CalcDefaultMeta(zid, ext)
- syntax, ok = dm.Get(api.KeySyntax)
- if !ok {
- panic("Default meta must contain syntax")
- }
- m.Set(api.KeySyntax, syntax)
- }
- }
-
- if len(uselessFiles) > 0 {
- m.Set(api.KeyUselessFiles, strings.Join(uselessFiles, " "))
- }
-}
DELETED box/filebox/zipbox.go
Index: box/filebox/zipbox.go
==================================================================
--- box/filebox/zipbox.go
+++ box/filebox/zipbox.go
@@ -1,248 +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 filebox
-
-import (
- "archive/zip"
- "context"
- "fmt"
- "io"
- "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"
-)
-
-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)
- 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()
- 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
- }
- 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)
- }
- return m, err
-}
-
-func readZipMetaFile(reader *zip.ReadCloser, zid id.Zid, name string) (*meta.Meta, error) {
- src, err := readZipFileContent(reader, name)
- if err != nil {
- return nil, err
- }
- inp := input.NewInput(src)
- return meta.NewFromInput(zid, inp), nil
-}
-
-func readZipFileContent(reader *zip.ReadCloser, name string) ([]byte, error) {
- f, err := reader.Open(name)
- if err != nil {
- return nil, err
- }
- defer f.Close()
- return io.ReadAll(f)
-}
DELETED box/helper.go
Index: box/helper.go
==================================================================
--- box/helper.go
+++ box/helper.go
@@ -1,66 +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 box
-
-import (
- "net/url"
- "strconv"
- "time"
-
- "zettelstore.de/z/zettel/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)
- zid := id.New(withSeconds)
- found, err := testZid(zid)
- if err != nil {
- return id.Invalid, err
- }
- if found {
- return zid, nil
- }
- // TODO: do not wait here unconditionally.
- 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
-}
DELETED box/manager/anteroom.go
Index: box/manager/anteroom.go
==================================================================
--- box/manager/anteroom.go
+++ box/manager/anteroom.go
@@ -1,144 +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 manager
-
-import (
- "sync"
-
- "zettelstore.de/z/zettel/id"
-)
-
-type arAction int
-
-const (
- arNothing arAction = iota
- arReload
- arZettel
-)
-
-type anteroom struct {
- next *anteroom
- waiting id.Set
- curLoad int
- reload bool
-}
-
-type anteroomQueue struct {
- mx sync.Mutex
- first *anteroom
- last *anteroom
- maxLoad int
-}
-
-func newAnteroomQueue(maxLoad int) *anteroomQueue { return &anteroomQueue{maxLoad: maxLoad} }
-
-func (ar *anteroomQueue) EnqueueZettel(zid id.Zid) {
- if !zid.IsValid() {
- return
- }
- ar.mx.Lock()
- defer ar.mx.Unlock()
- if ar.first == nil {
- ar.first = ar.makeAnteroom(zid)
- 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.
- return
- }
- }
- if room := ar.last; !room.reload && (ar.maxLoad == 0 || room.curLoad < ar.maxLoad) {
- room.waiting.Add(zid)
- room.curLoad++
- return
- }
- room := ar.makeAnteroom(zid)
- 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() {
- room := ar.first
- for room != nil && room.reload {
- room = room.next
- }
- ar.first = room
- if room == nil {
- ar.last = nil
- }
-}
-
-func (ar *anteroomQueue) Dequeue() (arAction, id.Zid, bool) {
- 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
- }
-}
DELETED box/manager/anteroom_test.go
Index: box/manager/anteroom_test.go
==================================================================
--- box/manager/anteroom_test.go
+++ box/manager/anteroom_test.go
@@ -1,109 +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 manager
-
-import (
- "testing"
-
- "zettelstore.de/z/zettel/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 {
- t.Errorf("Expected invalid Zid, but got %v", zid)
- }
- ar.EnqueueZettel(id.Zid(1))
- ar.EnqueueZettel(id.Zid(2))
- if ar.first != ar.last {
- t.Errorf("Expected one room, but got more")
- }
- ar.EnqueueZettel(id.Zid(3))
- if ar.first == ar.last {
- t.Errorf("Expected more than one room, but got only one")
- }
-
- count := 0
- for ; count < 1000; count++ {
- action, _, _ = ar.Dequeue()
- if action == arNothing {
- break
- }
- }
- if count != 3 {
- t.Errorf("Expected 3 dequeues, but got %v", count)
- }
-}
-
-func TestReset(t *testing.T) {
- t.Parallel()
- ar := newAnteroomQueue(1)
- ar.EnqueueZettel(id.Zid(1))
- ar.Reset()
- action, zid, _ := ar.Dequeue()
- if action != arReload || zid != id.Invalid {
- t.Errorf("Expected reload & invalid Zid, but got %v/%v", action, zid)
- }
- ar.Reload(id.NewSet(3, 4))
- ar.EnqueueZettel(id.Zid(5))
- ar.EnqueueZettel(id.Zid(5))
- if ar.first == ar.last || ar.first.next != ar.last /*|| ar.first.next.next != ar.last*/ {
- t.Errorf("Expected 2 rooms")
- }
- action, zid1, _ := ar.Dequeue()
- if action != arZettel {
- t.Errorf("Expected arZettel, but got %v", action)
- }
- action, zid2, _ := ar.Dequeue()
- if action != arZettel {
- t.Errorf("Expected arZettel, but got %v", action)
- }
- if !(zid1 == id.Zid(3) && zid2 == id.Zid(4) || zid1 == id.Zid(4) && zid2 == id.Zid(3)) {
- t.Errorf("Zids must be 3 or 4, but got %v/%v", zid1, zid2)
- }
- action, zid, _ = ar.Dequeue()
- if zid != id.Zid(5) || action != arZettel {
- t.Errorf("Expected 5/arZettel, but got %v/%v", zid, action)
- }
- action, zid, _ = ar.Dequeue()
- if action != arNothing || zid != id.Invalid {
- t.Errorf("Expected nothing & invalid Zid, but got %v/%v", action, zid)
- }
-
- ar = 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.Reload(nil)
- action, zid, _ = ar.Dequeue()
- if action != arNothing || zid != id.Invalid {
- t.Errorf("Expected nothing & invalid Zid, but got %v/%v", action, zid)
- }
-}
DELETED box/manager/box.go
Index: box/manager/box.go
==================================================================
--- box/manager/box.go
+++ box/manager/box.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 manager
-
-import (
- "context"
- "errors"
- "strings"
-
- "zettelstore.de/z/box"
- "zettelstore.de/z/query"
- "zettelstore.de/z/zettel"
- "zettelstore.de/z/zettel/id"
- "zettelstore.de/z/zettel/meta"
-)
-
-// Conatains all box.Box related functions
-
-// Location returns some information where the box is located.
-func (mgr *Manager) Location() string {
- if len(mgr.boxes) <= 2 {
- return "NONE"
- }
- var sb strings.Builder
- for i := range len(mgr.boxes) - 2 {
- if i > 0 {
- sb.WriteString(", ")
- }
- sb.WriteString(mgr.boxes[i].Location())
- }
- return sb.String()
-}
-
-// CanCreateZettel returns true, if box could possibly create a new zettel.
-func (mgr *Manager) CanCreateZettel(ctx context.Context) bool {
- 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
-}
-
-// 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 {
- 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
-}
-
-// 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
- }
- mgr.mgrMx.RLock()
- defer mgr.mgrMx.RUnlock()
- for i, p := range mgr.boxes {
- var errZNF box.ErrZettelNotFound
- if z, err := p.GetZettel(ctx, zid); !errors.As(err, &errZNF) {
- if err == nil {
- mgr.Enrich(ctx, z.Meta, i+1)
- }
- return z, err
- }
- }
- return zettel.Zettel{}, box.ErrZettelNotFound{Zid: zid}
-}
-
-// 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 {
- return nil, box.ErrStopped
- }
- mgr.mgrMx.RLock()
- defer mgr.mgrMx.RUnlock()
- var result []zettel.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()
- for _, p := range mgr.boxes {
- if !p.AllowRenameZettel(ctx, zid) {
- return false
- }
- }
- 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 {
- 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 {
- 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 {
- return false
- }
- mgr.mgrMx.RLock()
- defer mgr.mgrMx.RUnlock()
- for _, p := range mgr.boxes {
- if p.CanDeleteZettel(ctx, zid) {
- return true
- }
- }
- 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 {
- 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) {
- 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
-}
DELETED box/manager/collect.go
Index: box/manager/collect.go
==================================================================
--- box/manager/collect.go
+++ box/manager/collect.go
@@ -1,84 +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 manager
-
-import (
- "strings"
-
- "zettelstore.de/z/ast"
- "zettelstore.de/z/box/manager/store"
- "zettelstore.de/z/strfun"
- "zettelstore.de/z/zettel/id"
-)
-
-type collectData struct {
- refs id.Set
- words store.WordSet
- urls store.WordSet
-}
-
-func (data *collectData) initialize() {
- data.refs = id.NewSet()
- data.words = store.NewWordSet()
- data.urls = store.NewWordSet()
-}
-
-func collectZettelIndexData(zn *ast.ZettelNode, data *collectData) {
- ast.Walk(data, &zn.Ast)
-}
-
-func collectInlineIndexData(is *ast.InlineSlice, data *collectData) {
- ast.Walk(data, is)
-}
-
-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)
- case *ast.TextNode:
- data.addText(n.Text)
- case *ast.LinkNode:
- data.addRef(n.Ref)
- case *ast.EmbedRefNode:
- data.addRef(n.Ref)
- case *ast.CiteNode:
- data.addText(n.Key)
- case *ast.LiteralNode:
- data.addText(string(n.Content))
- }
- return data
-}
-
-func (data *collectData) addText(s string) {
- for _, word := range strfun.NormalizeWords(s) {
- data.words.Add(word)
- }
-}
-
-func (data *collectData) addRef(ref *ast.Reference) {
- if ref == nil {
- return
- }
- if ref.IsExternal() {
- data.urls.Add(strings.ToLower(ref.Value))
- }
- if !ref.IsZettel() {
- return
- }
- if zid, err := id.Parse(ref.URL.Path); err == nil {
- data.refs.Add(zid)
- }
-}
DELETED box/manager/enrich.go
Index: box/manager/enrich.go
==================================================================
--- box/manager/enrich.go
+++ box/manager/enrich.go
@@ -1,135 +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 manager
-
-import (
- "context"
- "strconv"
-
- "t73f.de/r/zsc/api"
- "zettelstore.de/z/box"
- "zettelstore.de/z/zettel/id"
- "zettelstore.de/z/zettel/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
- return
- }
- 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)
- return
- }
- }
- zid := m.Zid.String()
- if _, ok := meta.TimeValue(zid); ok {
- m.Set(api.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.
-}
DELETED box/manager/indexer.go
Index: box/manager/indexer.go
==================================================================
--- box/manager/indexer.go
+++ box/manager/indexer.go
@@ -1,253 +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 manager
-
-import (
- "context"
- "fmt"
- "net/url"
- "time"
-
- "zettelstore.de/z/box"
- "zettelstore.de/z/box/manager/store"
- "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
-}
-
-// 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
-}
-
-// 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
-}
-
-// 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
-}
-
-// 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)
- go mgr.idxIndexer()
- }
- }()
-
- timerDuration := 15 * time.Second
- timer := time.NewTimer(timerDuration)
- ctx := box.NoEnrichContext(context.Background())
- for {
- mgr.idxWorkService(ctx)
- if !mgr.idxSleepService(timer, timerDuration) {
- return
- }
- }
-}
-
-func (mgr *Manager) idxWorkService(ctx context.Context) {
- var start time.Time
- for {
- switch action, zid, lastReload := mgr.idxAr.Dequeue(); action {
- case arNothing:
- return
- case arReload:
- mgr.idxLog.Debug().Msg("reload")
- zids, err := mgr.FetchZids(ctx)
- if err == nil {
- start = time.Now()
- mgr.idxAr.Reload(zids)
- mgr.idxMx.Lock()
- mgr.idxLastReload = time.Now().Local()
- mgr.idxSinceReload = 0
- mgr.idxMx.Unlock()
- }
- case arZettel:
- mgr.idxLog.Debug().Zid(zid).Msg("zettel")
- 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)
- continue
- }
- mgr.idxLog.Trace().Zid(zid).Msg("update")
- mgr.idxUpdateZettel(ctx, zettel)
- mgr.idxMx.Lock()
- if lastReload {
- mgr.idxDurReload = time.Since(start)
- }
- mgr.idxSinceReload++
- mgr.idxMx.Unlock()
- }
- }
-}
-
-func (mgr *Manager) idxSleepService(timer *time.Timer, timerDuration time.Duration) bool {
- select {
- case _, ok := <-mgr.idxReady:
- if !ok {
- return false
- }
- case _, ok := <-timer.C:
- if !ok {
- return false
- }
- timer.Reset(timerDuration)
- case <-mgr.done:
- if !timer.Stop() {
- <-timer.C
- }
- return false
- }
- return true
-}
-
-func (mgr *Manager) idxUpdateZettel(ctx context.Context, zettel zettel.Zettel) {
- var cData collectData
- cData.initialize()
- collectZettelIndexData(parser.ParseZettel(ctx, zettel, "", mgr.rtConfig), &cData)
-
- m := zettel.Meta
- zi := store.NewZettelIndex(m)
- 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() {
- descr := meta.GetDescription(pair.Key)
- if descr.IsProperty() {
- continue
- }
- switch descr.Type {
- case meta.TypeID:
- mgr.idxUpdateValue(ctx, descr.Inverse, pair.Value, zi)
- 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)
- 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)
- }
- }
- }
-}
-
-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) {
- zi.AddBackRef(ref)
- } else {
- zi.AddDeadRef(ref)
- }
- }
- zi.SetWords(cData.words)
- zi.SetUrls(cData.urls)
-}
-
-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) {
- zi.AddDeadRef(zid)
- return
- }
- if inverseKey == "" {
- zi.AddBackRef(zid)
- return
- }
- zi.AddInverseRef(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)
- mgr.idxCheckZettel(toCheck)
-}
-
-func (mgr *Manager) idxCheckZettel(s id.Set) {
- for zid := range s {
- mgr.idxAr.EnqueueZettel(zid)
- }
-}
DELETED box/manager/manager.go
Index: box/manager/manager.go
==================================================================
--- box/manager/manager.go
+++ box/manager/manager.go
@@ -1,421 +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 manager coordinates the various boxes and indexes of a Zettelstore.
-package manager
-
-import (
- "context"
- "io"
- "net/url"
- "sync"
- "time"
-
- "zettelstore.de/z/auth"
- "zettelstore.de/z/box"
- "zettelstore.de/z/box/manager/mapstore"
- "zettelstore.de/z/box/manager/store"
- "zettelstore.de/z/config"
- "zettelstore.de/z/kernel"
- "zettelstore.de/z/logger"
- "zettelstore.de/z/strfun"
- "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.
- Config config.Config
- Enricher box.Enricher
- Notify chan<- box.UpdateInfo
-}
-
-// Connect returns a handle to the specified box.
-func Connect(u *url.URL, authManager auth.BaseManager, cdata *ConnectData) (box.ManagedBox, error) {
- if authManager.IsReadonly() {
- rawURL := u.String()
- // TODO: the following is wrong under some circumstances:
- // 1. fragment is set
- if q := u.Query(); len(q) == 0 {
- rawURL += "?readonly"
- } else if _, ok := q["readonly"]; !ok {
- rawURL += "&readonly"
- }
- var err error
- if u, err = url.Parse(rawURL); err != nil {
- return nil, err
- }
- }
-
- if create, ok := registry[u.Scheme]; ok {
- return create(u, cdata)
- }
- return nil, &ErrInvalidScheme{u.Scheme}
-}
-
-// ErrInvalidScheme is returned if there is no box with the given scheme.
-type ErrInvalidScheme struct{ Scheme string }
-
-func (err *ErrInvalidScheme) Error() string { return "Invalid scheme: " + err.Scheme }
-
-type createFunc func(*url.URL, *ConnectData) (box.ManagedBox, error)
-
-var registry = map[string]createFunc{}
-
-// Register the encoder for later retrieval.
-func Register(scheme string, create createFunc) {
- if _, ok := registry[scheme]; ok {
- panic(scheme)
- }
- registry[scheme] = create
-}
-
-// Manager is a coordinating box.
-type Manager struct {
- mgrLog *logger.Logger
- stateMx sync.RWMutex
- state box.StartState
- mgrMx sync.RWMutex
- 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
-
- // Indexer data
- idxLog *logger.Logger
- idxStore store.Store
- idxAr *anteroomQueue
- 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 {
- if kd.IsProperty() {
- propertyKeys.Set(kd.Name)
- }
- }
- 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),
- 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 {
- p, err := Connect(uri, authManager, &cdata)
- if err != nil {
- return nil, err
- }
- if p != nil {
- boxes = append(boxes, p)
- cdata.Number++
- }
- }
- constbox, err := registry[" const"](nil, &cdata)
- if err != nil {
- return nil, err
- }
- cdata.Number++
- compbox, err := registry[" comp"](nil, &cdata)
- if err != nil {
- return nil, err
- }
- cdata.Number++
- boxes = append(boxes, constbox, compbox)
- mgr.boxes = boxes
- return mgr, nil
-}
-
-func createIdxStore(_ config.Config) store.Store {
- return mapstore.New()
-}
-
-// RegisterObserver registers an observer that will be notified
-// if a zettel was found to be changed.
-func (mgr *Manager) RegisterObserver(f box.UpdateFunc) {
- if f != nil {
- mgr.mxObserver.Lock()
- 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)
- }
-}
-
-// 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 {
- 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 {
- ssj.Stop(ctx)
- }
- }
- mgr.setState(box.StartStateStopped)
- 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
-}
-
-// 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
-}
-
-// 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])
- }
-
- st.ReadOnly = true
- sumZettel := 0
- for _, sst := range subStats {
- if !sst.ReadOnly {
- st.ReadOnly = false
- }
- sumZettel += sst.Zettel
- }
- st.NumManagedBoxes = len(mgr.boxes)
- st.ZettelTotal = sumZettel
-
- var storeSt store.Stats
- mgr.idxMx.RLock()
- defer mgr.idxMx.RUnlock()
- mgr.idxStore.ReadStats(&storeSt)
-
- st.LastReload = mgr.idxLastReload
- st.IndexesSinceReload = mgr.idxSinceReload
- st.DurLastReload = mgr.idxDurReload
- st.ZettelIndexed = storeSt.Zettel
- st.IndexUpdates = storeSt.Updates
- st.IndexedWords = storeSt.Words
- st.IndexedUrls = storeSt.Urls
-}
-
-// Dump internal data structures to a Writer.
-func (mgr *Manager) Dump(w io.Writer) {
- mgr.idxStore.Dump(w)
-}
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)
- }
-}
DELETED box/manager/store/store.go
Index: box/manager/store/store.go
==================================================================
--- box/manager/store/store.go
+++ box/manager/store/store.go
@@ -1,69 +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 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"
-)
-
-// Stats records statistics about the store.
-type Stats struct {
- // Zettel is the number of zettel managed by the indexer.
- Zettel int
-
- // Updates count the number of metadata updates.
- Updates uint64
-
- // Words count the different words stored in the store.
- Words uint64
-
- // Urls count the different URLs stored in the store.
- Urls uint64
-}
-
-// 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)
-
- // 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.
- ReadStats(st *Stats)
-
- // Dump the content to a Writer.
- Dump(io.Writer)
-}
DELETED box/manager/store/wordset.go
Index: box/manager/store/wordset.go
==================================================================
--- box/manager/store/wordset.go
+++ box/manager/store/wordset.go
@@ -1,63 +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 store
-
-// WordSet contains the set of all words, with the count of their occurrences.
-type WordSet map[string]int
-
-// NewWordSet returns a new WordSet.
-func NewWordSet() WordSet { return make(WordSet) }
-
-// Add one word to the set
-func (ws WordSet) Add(s string) {
- ws[s] = ws[s] + 1
-}
-
-// Words gives the slice of all words in the set.
-func (ws WordSet) Words() []string {
- if len(ws) == 0 {
- return nil
- }
- words := make([]string, 0, len(ws))
- for w := range ws {
- words = append(words, w)
- }
- return words
-}
-
-// Diff calculates the word slice to be added and to be removed from oldWords
-// to get the given word set.
-func (ws WordSet) Diff(oldWords []string) (newWords, removeWords []string) {
- if len(ws) == 0 {
- return nil, oldWords
- }
- if len(oldWords) == 0 {
- return ws.Words(), nil
- }
- oldSet := make(WordSet, len(oldWords))
- for _, ow := range oldWords {
- if _, ok := ws[ow]; ok {
- oldSet[ow] = 1
- continue
- }
- removeWords = append(removeWords, ow)
- }
- for w := range ws {
- if _, ok := oldSet[w]; ok {
- continue
- }
- newWords = append(newWords, w)
- }
- return newWords, removeWords
-}
DELETED box/manager/store/wordset_test.go
Index: box/manager/store/wordset_test.go
==================================================================
--- box/manager/store/wordset_test.go
+++ box/manager/store/wordset_test.go
@@ -1,80 +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 store_test
-
-import (
- "sort"
- "testing"
-
- "zettelstore.de/z/box/manager/store"
-)
-
-func equalWordList(exp, got []string) bool {
- if len(exp) != len(got) {
- return false
- }
- if len(got) == 0 {
- return len(exp) == 0
- }
- sort.Strings(got)
- for i, w := range exp {
- if w != got[i] {
- return false
- }
- }
- return true
-}
-
-func TestWordsWords(t *testing.T) {
- t.Parallel()
- testcases := []struct {
- words store.WordSet
- exp []string
- }{
- {nil, nil},
- {store.WordSet{}, nil},
- {store.WordSet{"a": 1, "b": 2}, []string{"a", "b"}},
- }
- for i, tc := range testcases {
- got := tc.words.Words()
- if !equalWordList(tc.exp, got) {
- t.Errorf("%d: %v.Words() == %v, but got %v", i, tc.words, tc.exp, got)
- }
- }
-}
-
-func TestWordsDiff(t *testing.T) {
- t.Parallel()
- testcases := []struct {
- cur store.WordSet
- old []string
- expN, expR []string
- }{
- {nil, nil, nil, nil},
- {store.WordSet{}, []string{}, nil, nil},
- {store.WordSet{"a": 1}, []string{}, []string{"a"}, nil},
- {store.WordSet{"a": 1}, []string{"b"}, []string{"a"}, []string{"b"}},
- {store.WordSet{}, []string{"b"}, nil, []string{"b"}},
- {store.WordSet{"a": 1}, []string{"a"}, nil, nil},
- }
- for i, tc := range testcases {
- gotN, gotR := tc.cur.Diff(tc.old)
- if !equalWordList(tc.expN, gotN) {
- t.Errorf("%d: %v.Diff(%v)->new %v, but got %v", i, tc.cur, tc.old, tc.expN, gotN)
- }
- if !equalWordList(tc.expR, gotR) {
- t.Errorf("%d: %v.Diff(%v)->rem %v, but got %v", i, tc.cur, tc.old, tc.expR, gotR)
- }
- }
-}
DELETED box/manager/store/zettel.go
Index: box/manager/store/zettel.go
==================================================================
--- box/manager/store/zettel.go
+++ box/manager/store/zettel.go
@@ -1,95 +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 store
-
-import (
- "zettelstore.de/z/zettel/id"
- "zettelstore.de/z/zettel/meta"
-)
-
-// 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
-}
-
-// NewZettelIndex creates a new zettel index.
-func NewZettelIndex(m *meta.Meta) *ZettelIndex {
- return &ZettelIndex{
- Zid: m.Zid,
- meta: m,
- backrefs: id.NewSet(),
- inverseRefs: 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)
-}
-
-// AddInverseRef 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)
- return
- }
- zi.inverseRefs[key] = id.NewSet(zid)
-}
-
-// AddDeadRef adds a dead reference to a zettel.
-func (zi *ZettelIndex) AddDeadRef(zid id.Zid) {
- zi.deadrefs.Add(zid)
-}
-
-// 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 }
-
-// GetBackRefs returns all back references as a sorted list.
-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 {
- return nil
- }
- result := make(map[string]id.Slice, len(zi.inverseRefs))
- for key, refs := range zi.inverseRefs {
- result[key] = refs.Sorted()
- }
- return result
-}
-
-// GetWords returns a reference to the set of words. It must not be modified.
-func (zi *ZettelIndex) GetWords() WordSet { return zi.words }
-
-// GetUrls returns a reference to the set of URLs. It must not be modified.
-func (zi *ZettelIndex) GetUrls() WordSet { return zi.urls }
DELETED box/membox/membox.go
Index: box/membox/membox.go
==================================================================
--- box/membox/membox.go
+++ box/membox/membox.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 membox stores zettel volatile in main memory.
-package membox
-
-import (
- "context"
- "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"
-)
-
-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
- })
-}
-
-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")
- 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
- }
- zid, err := box.GetNewZid(func(zid id.Zid) (bool, error) {
- _, ok := mb.zettel[zid]
- return !ok, nil
- })
- if err != nil {
- mb.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()}
- }
-
- 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")
- return nil
-}
-
-func (mb *memBox) CanDeleteZettel(_ context.Context, zid id.Zid) bool {
- mb.mx.RLock()
- _, ok := mb.zettel[zid]
- mb.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")
- return nil
-}
-
-func (mb *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")
-}
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)
-}
Index: cmd/cmd_file.go
==================================================================
--- cmd/cmd_file.go
+++ cmd/cmd_file.go
@@ -1,59 +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 cmd
import (
- "context"
"flag"
"fmt"
"io"
"os"
- "t73f.de/r/zsc/api"
- "t73f.de/r/zsc/input"
+ "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(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 +58,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,126 @@
//-----------------------------------------------------------------------------
-// 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"
"zettelstore.de/z/auth"
- "zettelstore.de/z/box"
"zettelstore.de/z/config"
+ "zettelstore.de/z/domain/meta"
"zettelstore.de/z/kernel"
+ "zettelstore.de/z/place"
"zettelstore.de/z/usecase"
+ "zettelstore.de/z/web/adapter"
"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)
- 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)
- 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)
-
- 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))
-
- // 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))
- 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))
- }
+func doRun(debug bool) (int, error) {
+ kern := kernel.Main
+ kern.SetDebug(debug)
+ if err := kern.StartService(kernel.WebService); err != nil {
+ return 1, err
+ }
+ return 0, nil
+}
+
+func setupRouting(webSrv server.Server, placeManager place.Manager, authManager auth.Manager, rtConfig config.Config) {
+ protectedPlaceManager, authPolicy := authManager.PlaceWithPolicy(webSrv, placeManager, rtConfig)
+ api := api.New(webSrv, authManager, authManager, webSrv, rtConfig)
+ wui := webui.New(webSrv, authManager, rtConfig, authManager, placeManager, authPolicy)
+
+ ucAuthenticate := usecase.NewAuthenticate(authManager, authManager, placeManager)
+ ucCreateZettel := usecase.NewCreateZettel(rtConfig, protectedPlaceManager)
+ ucGetMeta := usecase.NewGetMeta(protectedPlaceManager)
+ ucGetZettel := usecase.NewGetZettel(protectedPlaceManager)
+ ucParseZettel := usecase.NewParseZettel(rtConfig, ucGetZettel)
+ ucListMeta := usecase.NewListMeta(protectedPlaceManager)
+ ucListRoles := usecase.NewListRole(protectedPlaceManager)
+ ucListTags := usecase.NewListTags(protectedPlaceManager)
+ ucZettelContext := usecase.NewZettelContext(protectedPlaceManager)
+
+ webSrv.Handle("/", wui.MakeGetRootHandler(protectedPlaceManager))
+ webSrv.AddListRoute('a', http.MethodGet, wui.MakeGetLoginHandler())
+ webSrv.AddListRoute('a', http.MethodPost, adapter.MakePostLoginHandler(
+ api.MakePostLoginHandlerAPI(ucAuthenticate),
+ wui.MakePostLoginHandlerHTML(ucAuthenticate)))
+ webSrv.AddListRoute('a', http.MethodPut, api.MakeRenewAuthHandler())
+ 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(
+ usecase.NewRenameZettel(protectedPlaceManager)))
+ 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(
+ usecase.NewDeleteZettel(protectedPlaceManager)))
+ webSrv.AddZettelRoute('e', http.MethodGet, wui.MakeEditGetZettelHandler(ucGetZettel))
+ webSrv.AddZettelRoute('e', http.MethodPost, wui.MakeEditSetZettelHandler(
+ usecase.NewUpdateZettel(protectedPlaceManager)))
+ 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(protectedPlaceManager), 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))
+ webSrv.AddZettelRoute('j', http.MethodGet, wui.MakeZettelContextHandler(ucZettelContext))
+
+ webSrv.AddZettelRoute('l', http.MethodGet, api.MakeGetLinksHandler(ucParseZettel))
+ webSrv.AddZettelRoute('o', http.MethodGet, api.MakeGetOrderHandler(
+ usecase.NewZettelOrder(protectedPlaceManager, ucParseZettel)))
+ webSrv.AddListRoute('r', http.MethodGet, api.MakeListRoleHandler(ucListRoles))
+ webSrv.AddListRoute('t', http.MethodGet, api.MakeListTagsHandler(ucListTags))
+ webSrv.AddZettelRoute('y', http.MethodGet, api.MakeZettelContextHandler(ucZettelContext))
+ webSrv.AddListRoute('z', http.MethodGet, api.MakeListMetaHandler(
+ usecase.NewListMeta(protectedPlaceManager), ucGetMeta, ucParseZettel))
+ webSrv.AddZettelRoute('z', http.MethodGet, api.MakeGetZettelHandler(
+ ucParseZettel, ucGetMeta))
if authManager.WithAuth() {
- webSrv.SetUserRetriever(usecase.NewGetUserByZid(boxManager))
+ webSrv.SetUserRetriever(usecase.NewGetUserByZid(placeManager))
}
}
-
-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,39 @@
//-----------------------------------------------------------------------------
-// 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 *flag.FlagSet // flags that belong to the command
+ Name string // command name as it appears on the command line
+ Func CommandFunc // function that executes a command
+ Places bool // if true then places will be set up
+ Header bool // Print a heading on startup
+ 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 +45,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 +58,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 places if you enabled notification\n", rLimit.Cur)
+ }
+ return nil
+}
Index: cmd/main.go
==================================================================
--- cmd/main.go
+++ cmd/main.go
@@ -1,248 +1,230 @@
//-----------------------------------------------------------------------------
-// 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/place"
+ "zettelstore.de/z/place/manager"
+ "zettelstore.de/z/place/progplace"
"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 },
- Header: true,
- })
- RegisterCommand(Command{
- Name: "run",
- Func: runFunc,
- Boxes: true,
- Header: true,
- LineServer: true,
- SetFlags: flgRun,
- })
- RegisterCommand(Command{
- Name: strRunSimple,
- Func: runFunc,
- Simple: true,
- 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")
- },
+ Func: func(*flag.FlagSet, *meta.Meta) (int, error) { return 0, nil },
+ Header: true,
+ })
+ RegisterCommand(Command{
+ Name: "run",
+ Func: runFunc,
+ Places: true,
+ Header: true,
+ Flags: flgRun,
+ })
+ RegisterCommand(Command{
+ Name: "run-simple",
+ Func: runSimpleFunc,
+ Places: true,
+ Header: true,
+ 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())
+ cfg.Set(keyPlaceOneURI, val)
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)
+ keyAdminPort = "admin-port"
+ keyDefaultDirPlaceType = "default-dir-place-type"
+ keyInsecureCookie = "insecure-cookie"
+ keyListenAddr = "listen-addr"
+ keyOwner = "owner"
+ keyPersistentCookie = "persistent-cookie"
+ keyPlaceOneURI = kernel.PlaceURIs + "1"
+ keyReadOnly = "read-only-mode"
+ keyTokenLifetimeHTML = "token-lifetime-html"
+ keyTokenLifetimeAPI = "token-lifetime-api"
+ keyURLPrefix = "url-prefix"
+ keyVerbose = "verbose"
+)
+
+func setServiceConfig(cfg *meta.Meta) error {
+ ok := setConfigValue(true, kernel.CoreService, kernel.CoreVerbose, cfg.GetBool(keyVerbose))
+ if val, found := cfg.Get(keyAdminPort); found {
+ ok = setConfigValue(ok, kernel.CoreService, kernel.CorePort, val)
+ }
+
+ ok = setConfigValue(ok, kernel.AuthService, kernel.AuthOwner, cfg.GetDefault(keyOwner, ""))
+ ok = setConfigValue(ok, kernel.AuthService, kernel.AuthReadonly, cfg.GetBool(keyReadOnly))
+
+ ok = setConfigValue(
+ ok, kernel.PlaceService, kernel.PlaceDefaultDirType,
+ cfg.GetDefault(keyDefaultDirPlaceType, kernel.PlaceDirTypeNotify))
+ ok = setConfigValue(ok, kernel.PlaceService, kernel.PlaceURIs+"1", "dir:./zettel")
+ format := kernel.PlaceURIs + "%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.PlaceService, 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, withPlaces bool) {
+ var createManager kernel.CreatePlaceManagerFunc
+ if withPlaces {
+ 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.PlaceService, kernel.PlaceDefaultDirType, kernel.PlaceDirTypeSimple)
+ }
+ createManager = func(placeURIs []*url.URL, authManager auth.Manager, rtConfig config.Config) (place.Manager, error) {
+ progplace.Setup(cfg)
+ return manager.New(placeURIs, authManager, rtConfig)
+ }
+ } else {
+ createManager = func([]*url.URL, auth.Manager, config.Config) (place.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 place.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 +234,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.Places)
+ kernel.Main.Start(command.Header)
+ 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,37 +1,33 @@
//-----------------------------------------------------------------------------
-// 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
// Mention all needed encoders, parsers and stores to have them registered.
import (
- _ "zettelstore.de/z/box/compbox" // Allow to use computed box.
- _ "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.
+ _ "zettelstore.de/z/place/constplace" // Allow to use global internal place.
+ _ "zettelstore.de/z/place/dirplace" // Allow to use directory place.
+ _ "zettelstore.de/z/place/fileplace" // Allow to use file place.
+ _ "zettelstore.de/z/place/memplace" // Allow to use memory place.
+ _ "zettelstore.de/z/place/progplace" // Allow to use computed place.
)
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,103 @@
//-----------------------------------------------------------------------------
-// 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"
+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)
- 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.CiteNode:
- s.Cites = append(s.Cites, n)
- }
- return s
-}
+func References(zn *ast.ZettelNode) Summary {
+ lv := linkVisitor{}
+ ast.NewTopDownTraverser(&lv).VisitBlockSlice(zn.Ast)
+ return lv.summary
+}
+
+type linkVisitor struct {
+ summary Summary
+}
+
+// VisitVerbatim does nothing.
+func (lv *linkVisitor) VisitVerbatim(vn *ast.VerbatimNode) {}
+
+// VisitRegion does nothing.
+func (lv *linkVisitor) VisitRegion(rn *ast.RegionNode) {}
+
+// VisitHeading does nothing.
+func (lv *linkVisitor) VisitHeading(hn *ast.HeadingNode) {}
+
+// VisitHRule does nothing.
+func (lv *linkVisitor) VisitHRule(hn *ast.HRuleNode) {}
+
+// VisitList does nothing.
+func (lv *linkVisitor) VisitNestedList(ln *ast.NestedListNode) {}
+
+// VisitDescriptionList does nothing.
+func (lv *linkVisitor) VisitDescriptionList(dn *ast.DescriptionListNode) {}
+
+// VisitPara does nothing.
+func (lv *linkVisitor) VisitPara(pn *ast.ParaNode) {}
+
+// VisitTable does nothing.
+func (lv *linkVisitor) VisitTable(tn *ast.TableNode) {}
+
+// VisitBLOB does nothing.
+func (lv *linkVisitor) VisitBLOB(bn *ast.BLOBNode) {}
+
+// VisitText does nothing.
+func (lv *linkVisitor) VisitText(tn *ast.TextNode) {}
+
+// VisitTag does nothing.
+func (lv *linkVisitor) VisitTag(tn *ast.TagNode) {}
+
+// VisitSpace does nothing.
+func (lv *linkVisitor) VisitSpace(sn *ast.SpaceNode) {}
+
+// VisitBreak does nothing.
+func (lv *linkVisitor) VisitBreak(bn *ast.BreakNode) {}
+
+// VisitLink collects the given link as a reference.
+func (lv *linkVisitor) VisitLink(ln *ast.LinkNode) {
+ lv.summary.Links = append(lv.summary.Links, ln.Ref)
+}
+
+// VisitImage collects the image links as a reference.
+func (lv *linkVisitor) VisitImage(in *ast.ImageNode) {
+ if in.Ref != nil {
+ lv.summary.Images = append(lv.summary.Images, in.Ref)
+ }
+}
+
+// VisitCite collects the citation.
+func (lv *linkVisitor) VisitCite(cn *ast.CiteNode) {
+ lv.summary.Cites = append(lv.summary.Cites, cn)
+}
+
+// VisitFootnote does nothing.
+func (lv *linkVisitor) VisitFootnote(fn *ast.FootnoteNode) {}
+
+// VisitMark does nothing.
+func (lv *linkVisitor) VisitMark(mn *ast.MarkNode) {}
+
+// VisitFormat does nothing.
+func (lv *linkVisitor) VisitFormat(fn *ast.FormatNode) {}
+
+// VisitLiteral does nothing.
+func (lv *linkVisitor) VisitLiteral(ln *ast.LiteralNode) {}
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
@@ -28,37 +25,46 @@
}
return r
}
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) {
- t.Parallel()
+func TestImage(t *testing.T) {
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.Code {
+ 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,108 @@
//-----------------------------------------------------------------------------
-// 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
+
+ // GetListPageSize returns the maximum length of a list to be returned in WebUI.
+ // A value less or equal to zero signals no limit.
+ GetListPageSize() int
}
// 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 ")
+ inPara = true
+ }
+ v.acceptInlineSlice(pn.Inlines)
+ } else {
+ if inPara {
+ v.writeEndPara()
+ inPara = false
+ }
+ v.acceptItemSlice(item)
+ }
+ }
+ if inPara {
+ v.writeEndPara()
+ }
+ v.b.WriteString(" Unable to display BLOB with syntax '", bn.Syntax, "'.
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,43 +1,31 @@
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.
+: If you want to use the Zettelstore software, all you need is to copy the executable to an appropriate place and start working.
: Upgrading the software is done just by replacing the executable with a newer one.
; Ease of operation
-: There is only one executable for Zettelstore and one directory, where your zettel are stored.
+: There is only one executable for Zettelstore and one directory, where your zettel are placed.
: 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.]
-* A sub-directory ""zettel"" will be created in the directory where you put the executable.
+* Start the Zettelstore software, e.g. with a double click
+* A sub-directory ""zettel"" will be created in the directory where you placed 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 place 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 ``
.
+func (v *visitor) VisitHRule(hn *ast.HRuleNode) {
+ v.b.WriteString("
\n")
+ } else {
+ v.b.WriteString(">\n")
+ }
+}
+
+var listCode = map[ast.NestedListCode]string{
+ ast.NestedListOrdered: "ol",
+ ast.NestedListUnordered: "ul",
+}
+
+// VisitNestedList writes HTML code for lists and blockquotes.
+func (v *visitor) VisitNestedList(ln *ast.NestedListNode) {
+ v.lang.push(ln.Attrs)
+ defer v.lang.pop()
+
+ if ln.Code == ast.NestedListQuote {
+ // NestedListQuote -> HTML doesn't use
\n")
+ inPara := false
+ for _, item := range ln.Items {
+ if pn := getParaItem(item); pn != nil {
+ if inPara {
+ v.b.WriteByte('\n')
+ } else {
+ v.b.WriteString("
\n")
+}
+
+func getParaItem(its ast.ItemSlice) *ast.ParaNode {
+ if len(its) != 1 {
+ return nil
+ }
+ if pn, ok := its[0].(*ast.ParaNode); ok {
+ return pn
+ }
+ return nil
+}
+
+func isCompactList(insl []ast.ItemSlice) bool {
+ for _, ins := range insl {
+ if !isCompactSlice(ins) {
+ return false
+ }
+ }
+ return true
+}
+
+func isCompactSlice(ins ast.ItemSlice) bool {
+ if len(ins) < 1 {
+ return true
+ }
+ if len(ins) == 1 {
+ switch ins[0].(type) {
+ case *ast.ParaNode, *ast.VerbatimNode, *ast.HRuleNode:
+ return true
+ case *ast.NestedListNode:
+ return false
+ }
+ }
+ return false
+}
+
+// writeItemSliceOrPara emits the content of a paragraph if the paragraph is
+// the only element of the block slice and if compact mode is true. Otherwise,
+// the item slice is emitted normally.
+func (v *visitor) writeItemSliceOrPara(ins ast.ItemSlice, compact bool) {
+ if compact && len(ins) == 1 {
+ if para, ok := ins[0].(*ast.ParaNode); ok {
+ v.acceptInlineSlice(para.Inlines)
+ return
+ }
+ }
+ v.acceptItemSlice(ins)
+}
+
+func (v *visitor) writeDescriptionsSlice(ds ast.DescriptionSlice) {
+ if len(ds) == 1 {
+ if para, ok := ds[0].(*ast.ParaNode); ok {
+ v.acceptInlineSlice(para.Inlines)
+ return
+ }
+ }
+ for _, dn := range ds {
+ dn.Accept(v)
+ }
+}
+
+// VisitDescriptionList emits a HTML description list.
+func (v *visitor) VisitDescriptionList(dn *ast.DescriptionListNode) {
+ v.b.WriteString("\n")
+ for _, descr := range dn.Descriptions {
+ v.b.WriteString("
\n")
+}
+
+// VisitTable emits a HTML table.
+func (v *visitor) VisitTable(tn *ast.TableNode) {
+ v.b.WriteString("\n")
+ if len(tn.Header) > 0 {
+ v.b.WriteString("\n")
+ v.writeRow(tn.Header, "
\n")
+}
+
+var alignStyle = map[ast.Alignment]string{
+ ast.AlignDefault: ">",
+ ast.AlignLeft: " style=\"text-align:left\">",
+ ast.AlignCenter: " style=\"text-align:center\">",
+ ast.AlignRight: " style=\"text-align:right\">",
+}
+
+func (v *visitor) writeRow(row ast.TableRow, cellStart, cellEnd string) {
+ v.b.WriteString("")
+ v.b.WriteString(" \n")
+ }
+ if len(tn.Rows) > 0 {
+ v.b.WriteString("\n")
+ for _, row := range tn.Rows {
+ v.writeRow(row, "")
+ }
+ v.b.WriteString(" \n")
+ }
+ v.b.WriteString("")
+ for _, cell := range row {
+ v.b.WriteString(cellStart)
+ if len(cell.Inlines) == 0 {
+ v.b.WriteByte('>')
+ } else {
+ v.b.WriteString(alignStyle[cell.Align])
+ v.acceptInlineSlice(cell.Inlines)
+ }
+ v.b.WriteString(cellEnd)
+ }
+ v.b.WriteString(" \n")
+}
+
+// VisitBLOB writes the binary object as a value.
+func (v *visitor) VisitBLOB(bn *ast.BLOBNode) {
+ switch bn.Syntax {
+ case "gif", "jpeg", "png":
+ v.b.WriteStrings("\n")
+ default:
+ v.b.WriteStrings("
", ln.Attrs, ln.Text)
+ case ast.LiteralKeyb:
+ v.writeLiteral("", ln.Attrs, ln.Text)
+ case ast.LiteralOutput:
+ v.writeLiteral("", ln.Attrs, ln.Text)
+ case ast.LiteralComment:
+ v.b.WriteString("")
+ case ast.LiteralHTML:
+ if !ignoreHTMLText(ln.Text) {
+ v.b.WriteString(ln.Text)
+ }
+ default:
+ panic(fmt.Sprintf("Unknown literal code %v", ln.Code))
+ }
+}
+
+func (v *visitor) writeLiteral(codeS, codeE string, attrs *ast.Attributes, text string) {
+ oldVisible := v.visibleSpace
+ if attrs != nil {
+ v.visibleSpace = attrs.HasDefault()
+ }
+ v.b.WriteString(codeS)
+ v.visitAttributes(attrs)
+ v.b.WriteByte('>')
+ v.writeHTMLEscaped(text)
+ v.b.WriteString(codeE)
+ v.visibleSpace = oldVisible
+}
ADDED encoder/htmlenc/langstack.go
Index: encoder/htmlenc/langstack.go
==================================================================
--- encoder/htmlenc/langstack.go
+++ encoder/htmlenc/langstack.go
@@ -0,0 +1,36 @@
+//-----------------------------------------------------------------------------
+// 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 htmlenc encodes the abstract syntax tree into HTML5.
+package htmlenc
+
+import "zettelstore.de/z/ast"
+
+type langStack struct {
+ items []string
+}
+
+func newLangStack(lang string) langStack {
+ items := make([]string, 1, 16)
+ items[0] = lang
+ return langStack{items}
+}
+
+func (s langStack) top() string { return s.items[len(s.items)-1] }
+
+func (s *langStack) pop() { s.items = s.items[0 : len(s.items)-1] }
+
+func (s *langStack) push(attrs *ast.Attributes) {
+ if value, ok := attrs.Get("lang"); ok {
+ s.items = append(s.items, value)
+ } else {
+ s.items = append(s.items, s.top())
+ }
+}
ADDED encoder/htmlenc/langstack_test.go
Index: encoder/htmlenc/langstack_test.go
==================================================================
--- encoder/htmlenc/langstack_test.go
+++ encoder/htmlenc/langstack_test.go
@@ -0,0 +1,45 @@
+//-----------------------------------------------------------------------------
+// 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 htmlenc encodes the abstract syntax tree into HTML5.
+package htmlenc
+
+import (
+ "testing"
+
+ "zettelstore.de/z/ast"
+)
+
+func TestStackSimple(t *testing.T) {
+ exp := "de"
+ s := newLangStack(exp)
+ if got := s.top(); got != exp {
+ t.Errorf("Init: expected %q, but got %q", exp, got)
+ return
+ }
+
+ a := &ast.Attributes{}
+ s.push(a)
+ if got := s.top(); exp != got {
+ t.Errorf("Empty push: expected %q, but got %q", exp, got)
+ }
+
+ exp2 := "en"
+ a = a.Set("lang", exp2)
+ s.push(a)
+ if got := s.top(); exp2 != got {
+ t.Errorf("Full push: expected %q, but got %q", exp2, got)
+ }
+
+ s.pop()
+ if got := s.top(); exp != got {
+ t.Errorf("pop: expected %q, but got %q", exp, got)
+ }
+}
ADDED encoder/htmlenc/visitor.go
Index: encoder/htmlenc/visitor.go
==================================================================
--- encoder/htmlenc/visitor.go
+++ encoder/htmlenc/visitor.go
@@ -0,0 +1,165 @@
+//-----------------------------------------------------------------------------
+// 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 htmlenc encodes the abstract syntax tree into HTML5.
+package htmlenc
+
+import (
+ "io"
+ "sort"
+ "strconv"
+ "strings"
+
+ "zettelstore.de/z/ast"
+ "zettelstore.de/z/domain/meta"
+ "zettelstore.de/z/encoder"
+ "zettelstore.de/z/strfun"
+)
+
+// visitor writes the abstract syntax tree to an io.Writer.
+type visitor struct {
+ env *encoder.Environment
+ b encoder.BufWriter
+ visibleSpace bool // Show space character in raw text
+ inVerse bool // In verse block
+ inInteractive bool // Rendered interactive HTML code
+ lang langStack
+}
+
+func newVisitor(he *htmlEncoder, w io.Writer) *visitor {
+ var lang string
+ if he.env != nil {
+ lang = he.env.Lang
+ }
+ return &visitor{
+ env: he.env,
+ b: encoder.NewBufWriter(w),
+ lang: newLangStack(lang),
+ }
+}
+
+var mapMetaKey = map[string]string{
+ meta.KeyCopyright: "copyright",
+ meta.KeyLicense: "license",
+}
+
+func (v *visitor) acceptMeta(m *meta.Meta) {
+ for _, pair := range m.Pairs(true) {
+ if env := v.env; env != nil && env.IgnoreMeta[pair.Key] {
+ continue
+ }
+ if pair.Key == meta.KeyTitle {
+ continue
+ }
+ if pair.Key == meta.KeyTags {
+ v.writeTags(pair.Value)
+ } else if key, ok := mapMetaKey[pair.Key]; ok {
+ v.writeMeta("", key, pair.Value)
+ } else {
+ v.writeMeta("zs-", pair.Key, pair.Value)
+ }
+ }
+}
+
+func (v *visitor) writeTags(tags string) {
+ v.b.WriteString("\n 0 {
+ v.b.WriteString(", ")
+ }
+ v.writeQuotedEscaped(strings.TrimPrefix(val, "#"))
+ }
+ v.b.WriteString("\">")
+}
+
+func (v *visitor) writeMeta(prefix, key, value string) {
+ v.b.WriteStrings("\n")
+}
+
+func (v *visitor) acceptBlockSlice(bns ast.BlockSlice) {
+ for _, bn := range bns {
+ bn.Accept(v)
+ }
+}
+func (v *visitor) acceptItemSlice(ins ast.ItemSlice) {
+ for _, in := range ins {
+ in.Accept(v)
+ }
+}
+func (v *visitor) acceptInlineSlice(ins ast.InlineSlice) {
+ for _, in := range ins {
+ in.Accept(v)
+ }
+}
+
+func (v *visitor) writeEndnotes() {
+ footnotes := v.env.GetCleanFootnotes()
+ if len(footnotes) > 0 {
+ v.b.WriteString("\n")
+ for i := 0; i < len(footnotes); i++ {
+ // Do not use a range loop above, because a footnote may contain
+ // a footnote. Therefore v.enc.footnote may grow during the loop.
+ fn := footnotes[i]
+ n := strconv.Itoa(i + 1)
+ v.b.WriteStrings("- ")
+ v.acceptInlineSlice(fn.Inlines)
+ v.b.WriteStrings(
+ " ↩︎
\n")
+ }
+ v.b.WriteString("
\n")
+ }
+}
+
+// visitAttributes write HTML attributes
+func (v *visitor) visitAttributes(a *ast.Attributes) {
+ if a == nil || len(a.Attrs) == 0 {
+ return
+ }
+ keys := make([]string, 0, len(a.Attrs))
+ for k := range a.Attrs {
+ if k != "-" {
+ keys = append(keys, k)
+ }
+ }
+ sort.Strings(keys)
+
+ for _, k := range keys {
+ if k == "" || k == "-" {
+ continue
+ }
+ v.b.WriteStrings(" ", k)
+ vl := a.Attrs[k]
+ if len(vl) > 0 {
+ v.b.WriteString("=\"")
+ v.writeQuotedEscaped(vl)
+ v.b.WriteByte('"')
+ }
+ }
+}
+
+func (v *visitor) writeHTMLEscaped(s string) {
+ strfun.HTMLEscape(&v.b, s, v.visibleSpace)
+}
+
+func (v *visitor) writeQuotedEscaped(s string) {
+ strfun.HTMLAttrEscape(&v.b, s)
+}
+
+func (v *visitor) writeReference(ref *ast.Reference) {
+ if ref.URL == nil {
+ v.writeHTMLEscaped(ref.Value)
+ return
+ }
+ v.b.WriteString(ref.URL.String())
+}
ADDED encoder/jsonenc/djsonenc.go
Index: encoder/jsonenc/djsonenc.go
==================================================================
--- encoder/jsonenc/djsonenc.go
+++ encoder/jsonenc/djsonenc.go
@@ -0,0 +1,574 @@
+//-----------------------------------------------------------------------------
+// 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 jsonenc encodes the abstract syntax tree into JSON.
+package jsonenc
+
+import (
+ "fmt"
+ "io"
+ "sort"
+ "strconv"
+
+ "zettelstore.de/z/ast"
+ "zettelstore.de/z/domain/meta"
+ "zettelstore.de/z/encoder"
+ "zettelstore.de/z/encoder/encfun"
+)
+
+func init() {
+ encoder.Register("djson", encoder.Info{
+ Create: func(env *encoder.Environment) encoder.Encoder { return &jsonDetailEncoder{env: env} },
+ })
+}
+
+type jsonDetailEncoder struct {
+ env *encoder.Environment
+}
+
+// WriteZettel writes the encoded zettel to the writer.
+func (je *jsonDetailEncoder) WriteZettel(w io.Writer, zn *ast.ZettelNode, inhMeta bool) (int, error) {
+ v := newDetailVisitor(w, je)
+ v.b.WriteString("{\"meta\":{\"title\":")
+ v.acceptInlineSlice(encfun.MetaAsInlineSlice(zn.InhMeta, meta.KeyTitle))
+ if inhMeta {
+ v.writeMeta(zn.InhMeta)
+ } else {
+ v.writeMeta(zn.Meta)
+ }
+ v.b.WriteByte('}')
+ v.b.WriteString(",\"content\":")
+ v.acceptBlockSlice(zn.Ast)
+ v.b.WriteByte('}')
+ length, err := v.b.Flush()
+ return length, err
+}
+
+// WriteMeta encodes meta data as JSON.
+func (je *jsonDetailEncoder) WriteMeta(w io.Writer, m *meta.Meta) (int, error) {
+ v := newDetailVisitor(w, je)
+ v.b.WriteString("{\"title\":")
+ v.acceptInlineSlice(encfun.MetaAsInlineSlice(m, meta.KeyTitle))
+ v.writeMeta(m)
+ v.b.WriteByte('}')
+ length, err := v.b.Flush()
+ return length, err
+}
+
+func (je *jsonDetailEncoder) WriteContent(w io.Writer, zn *ast.ZettelNode) (int, error) {
+ return je.WriteBlocks(w, zn.Ast)
+}
+
+// WriteBlocks writes a block slice to the writer
+func (je *jsonDetailEncoder) WriteBlocks(w io.Writer, bs ast.BlockSlice) (int, error) {
+ v := newDetailVisitor(w, je)
+ v.acceptBlockSlice(bs)
+ length, err := v.b.Flush()
+ return length, err
+}
+
+// WriteInlines writes an inline slice to the writer
+func (je *jsonDetailEncoder) WriteInlines(w io.Writer, is ast.InlineSlice) (int, error) {
+ v := newDetailVisitor(w, je)
+ v.acceptInlineSlice(is)
+ length, err := v.b.Flush()
+ return length, err
+}
+
+// detailVisitor writes the abstract syntax tree to an io.Writer.
+type detailVisitor struct {
+ b encoder.BufWriter
+ env *encoder.Environment
+}
+
+func newDetailVisitor(w io.Writer, je *jsonDetailEncoder) *detailVisitor {
+ return &detailVisitor{b: encoder.NewBufWriter(w), env: je.env}
+}
+
+// VisitPara emits JSON code for a paragraph.
+func (v *detailVisitor) VisitPara(pn *ast.ParaNode) {
+ v.writeNodeStart("Para")
+ v.writeContentStart('i')
+ v.acceptInlineSlice(pn.Inlines)
+ v.b.WriteByte('}')
+}
+
+var verbatimCode = map[ast.VerbatimCode]string{
+ ast.VerbatimProg: "CodeBlock",
+ ast.VerbatimComment: "CommentBlock",
+ ast.VerbatimHTML: "HTMLBlock",
+}
+
+// VisitVerbatim emits JSON code for verbatim lines.
+func (v *detailVisitor) VisitVerbatim(vn *ast.VerbatimNode) {
+ code, ok := verbatimCode[vn.Code]
+ if !ok {
+ panic(fmt.Sprintf("Unknown verbatim code %v", vn.Code))
+ }
+ v.writeNodeStart(code)
+ v.visitAttributes(vn.Attrs)
+ v.writeContentStart('l')
+ for i, line := range vn.Lines {
+ if i > 0 {
+ v.b.WriteByte(',')
+ }
+ writeEscaped(&v.b, line)
+ }
+ v.b.WriteString("]}")
+}
+
+var regionCode = map[ast.RegionCode]string{
+ ast.RegionSpan: "SpanBlock",
+ ast.RegionQuote: "QuoteBlock",
+ ast.RegionVerse: "VerseBlock",
+}
+
+// VisitRegion writes JSON code for block regions.
+func (v *detailVisitor) VisitRegion(rn *ast.RegionNode) {
+ code, ok := regionCode[rn.Code]
+ if !ok {
+ panic(fmt.Sprintf("Unknown region code %v", rn.Code))
+ }
+ v.writeNodeStart(code)
+ v.visitAttributes(rn.Attrs)
+ v.writeContentStart('b')
+ v.acceptBlockSlice(rn.Blocks)
+ if len(rn.Inlines) > 0 {
+ v.writeContentStart('i')
+ v.acceptInlineSlice(rn.Inlines)
+ }
+ v.b.WriteByte('}')
+}
+
+// VisitHeading writes the JSON code for a heading.
+func (v *detailVisitor) VisitHeading(hn *ast.HeadingNode) {
+ v.writeNodeStart("Heading")
+ v.visitAttributes(hn.Attrs)
+ v.writeContentStart('n')
+ v.b.WriteString(strconv.Itoa(hn.Level))
+ if slug := hn.Slug; len(slug) > 0 {
+ v.writeContentStart('s')
+ v.b.WriteStrings("\"", slug, "\"")
+ }
+ v.writeContentStart('i')
+ v.acceptInlineSlice(hn.Inlines)
+ v.b.WriteByte('}')
+}
+
+// VisitHRule writes JSON code for a horizontal rule:
.
+func (v *detailVisitor) VisitHRule(hn *ast.HRuleNode) {
+ v.writeNodeStart("Hrule")
+ v.visitAttributes(hn.Attrs)
+ v.b.WriteByte('}')
+}
+
+var listCode = map[ast.NestedListCode]string{
+ ast.NestedListOrdered: "OrderedList",
+ ast.NestedListUnordered: "BulletList",
+ ast.NestedListQuote: "QuoteList",
+}
+
+// VisitNestedList writes JSON code for lists and blockquotes.
+func (v *detailVisitor) VisitNestedList(ln *ast.NestedListNode) {
+ v.writeNodeStart(listCode[ln.Code])
+ v.writeContentStart('c')
+ for i, item := range ln.Items {
+ if i > 0 {
+ v.b.WriteByte(',')
+ }
+ v.acceptItemSlice(item)
+ }
+ v.b.WriteString("]}")
+}
+
+// VisitDescriptionList emits a JSON description list.
+func (v *detailVisitor) VisitDescriptionList(dn *ast.DescriptionListNode) {
+ v.writeNodeStart("DescriptionList")
+ v.writeContentStart('g')
+ for i, def := range dn.Descriptions {
+ if i > 0 {
+ v.b.WriteByte(',')
+ }
+ v.b.WriteByte('[')
+ v.acceptInlineSlice(def.Term)
+
+ if len(def.Descriptions) > 0 {
+ for _, b := range def.Descriptions {
+ v.b.WriteByte(',')
+ v.acceptDescriptionSlice(b)
+ }
+ }
+ v.b.WriteByte(']')
+ }
+ v.b.WriteString("]}")
+}
+
+// VisitTable emits a JSON table.
+func (v *detailVisitor) VisitTable(tn *ast.TableNode) {
+ v.writeNodeStart("Table")
+ v.writeContentStart('p')
+
+ // Table header
+ v.b.WriteByte('[')
+ for i, cell := range tn.Header {
+ if i > 0 {
+ v.b.WriteByte(',')
+ }
+ v.writeCell(cell)
+ }
+ v.b.WriteString("],")
+
+ // Table rows
+ v.b.WriteByte('[')
+ for i, row := range tn.Rows {
+ if i > 0 {
+ v.b.WriteByte(',')
+ }
+ v.b.WriteByte('[')
+ for j, cell := range row {
+ if j > 0 {
+ v.b.WriteByte(',')
+ }
+ v.writeCell(cell)
+ }
+ v.b.WriteByte(']')
+ }
+ v.b.WriteString("]]}")
+}
+
+var alignmentCode = map[ast.Alignment]string{
+ ast.AlignDefault: "[\"\",",
+ ast.AlignLeft: "[\"<\",",
+ ast.AlignCenter: "[\":\",",
+ ast.AlignRight: "[\">\",",
+}
+
+func (v *detailVisitor) writeCell(cell *ast.TableCell) {
+ v.b.WriteString(alignmentCode[cell.Align])
+ v.acceptInlineSlice(cell.Inlines)
+ v.b.WriteByte(']')
+}
+
+// VisitBLOB writes the binary object as a value.
+func (v *detailVisitor) VisitBLOB(bn *ast.BLOBNode) {
+ v.writeNodeStart("Blob")
+ v.writeContentStart('q')
+ writeEscaped(&v.b, bn.Title)
+ v.writeContentStart('s')
+ writeEscaped(&v.b, bn.Syntax)
+ v.writeContentStart('o')
+ v.b.WriteBase64(bn.Blob)
+ v.b.WriteString("\"}")
+}
+
+// VisitText writes text content.
+func (v *detailVisitor) VisitText(tn *ast.TextNode) {
+ v.writeNodeStart("Text")
+ v.writeContentStart('s')
+ writeEscaped(&v.b, tn.Text)
+ v.b.WriteByte('}')
+}
+
+// VisitTag writes tag content.
+func (v *detailVisitor) VisitTag(tn *ast.TagNode) {
+ v.writeNodeStart("Tag")
+ v.writeContentStart('s')
+ writeEscaped(&v.b, tn.Tag)
+ v.b.WriteByte('}')
+}
+
+// VisitSpace emits a white space.
+func (v *detailVisitor) VisitSpace(sn *ast.SpaceNode) {
+ v.writeNodeStart("Space")
+ if l := len(sn.Lexeme); l > 1 {
+ v.writeContentStart('n')
+ v.b.WriteString(strconv.Itoa(l))
+ }
+ v.b.WriteByte('}')
+}
+
+// VisitBreak writes JSON code for line breaks.
+func (v *detailVisitor) VisitBreak(bn *ast.BreakNode) {
+ if bn.Hard {
+ v.writeNodeStart("Hard")
+ } else {
+ v.writeNodeStart("Soft")
+ }
+ v.b.WriteByte('}')
+}
+
+var mapRefState = map[ast.RefState]string{
+ ast.RefStateInvalid: "invalid",
+ ast.RefStateZettel: "zettel",
+ ast.RefStateSelf: "self",
+ ast.RefStateFound: "zettel",
+ ast.RefStateBroken: "broken",
+ ast.RefStateHosted: "local",
+ ast.RefStateBased: "based",
+ ast.RefStateExternal: "external",
+}
+
+// VisitLink writes JSON code for links.
+func (v *detailVisitor) VisitLink(ln *ast.LinkNode) {
+ ln, n := v.env.AdaptLink(ln)
+ if n != nil {
+ n.Accept(v)
+ return
+ }
+ v.writeNodeStart("Link")
+ v.visitAttributes(ln.Attrs)
+ v.writeContentStart('q')
+ writeEscaped(&v.b, mapRefState[ln.Ref.State])
+ v.writeContentStart('s')
+ writeEscaped(&v.b, ln.Ref.String())
+ v.writeContentStart('i')
+ v.acceptInlineSlice(ln.Inlines)
+ v.b.WriteByte('}')
+}
+
+// VisitImage writes JSON code for images.
+func (v *detailVisitor) VisitImage(in *ast.ImageNode) {
+ in, n := v.env.AdaptImage(in)
+ if n != nil {
+ n.Accept(v)
+ return
+ }
+ v.writeNodeStart("Image")
+ v.visitAttributes(in.Attrs)
+ if in.Ref == nil {
+ v.writeContentStart('j')
+ v.b.WriteString("\"s\":")
+ writeEscaped(&v.b, in.Syntax)
+ switch in.Syntax {
+ case "svg":
+ v.writeContentStart('q')
+ writeEscaped(&v.b, string(in.Blob))
+ default:
+ v.writeContentStart('o')
+ v.b.WriteBase64(in.Blob)
+ v.b.WriteByte('"')
+ }
+ v.b.WriteByte('}')
+ } else {
+ v.writeContentStart('s')
+ writeEscaped(&v.b, in.Ref.String())
+ }
+ if len(in.Inlines) > 0 {
+ v.writeContentStart('i')
+ v.acceptInlineSlice(in.Inlines)
+ }
+ v.b.WriteByte('}')
+}
+
+// VisitCite writes code for citations.
+func (v *detailVisitor) VisitCite(cn *ast.CiteNode) {
+ v.writeNodeStart("Cite")
+ v.visitAttributes(cn.Attrs)
+ v.writeContentStart('s')
+ writeEscaped(&v.b, cn.Key)
+ if len(cn.Inlines) > 0 {
+ v.writeContentStart('i')
+ v.acceptInlineSlice(cn.Inlines)
+ }
+ v.b.WriteByte('}')
+}
+
+// VisitFootnote write JSON code for a footnote.
+func (v *detailVisitor) VisitFootnote(fn *ast.FootnoteNode) {
+ v.writeNodeStart("Footnote")
+ v.visitAttributes(fn.Attrs)
+ v.writeContentStart('i')
+ v.acceptInlineSlice(fn.Inlines)
+ v.b.WriteByte('}')
+}
+
+// VisitMark writes JSON code to mark a position.
+func (v *detailVisitor) VisitMark(mn *ast.MarkNode) {
+ v.writeNodeStart("Mark")
+ if len(mn.Text) > 0 {
+ v.writeContentStart('s')
+ writeEscaped(&v.b, mn.Text)
+ }
+ v.b.WriteByte('}')
+}
+
+var formatCode = map[ast.FormatCode]string{
+ ast.FormatItalic: "Italic",
+ ast.FormatEmph: "Emph",
+ ast.FormatBold: "Bold",
+ ast.FormatStrong: "Strong",
+ ast.FormatMonospace: "Mono",
+ ast.FormatStrike: "Strikethrough",
+ ast.FormatDelete: "Delete",
+ ast.FormatUnder: "Underline",
+ ast.FormatInsert: "Insert",
+ ast.FormatSuper: "Super",
+ ast.FormatSub: "Sub",
+ ast.FormatQuote: "Quote",
+ ast.FormatQuotation: "Quotation",
+ ast.FormatSmall: "Small",
+ ast.FormatSpan: "Span",
+}
+
+// VisitFormat write JSON code for formatting text.
+func (v *detailVisitor) VisitFormat(fn *ast.FormatNode) {
+ v.writeNodeStart(formatCode[fn.Code])
+ v.visitAttributes(fn.Attrs)
+ v.writeContentStart('i')
+ v.acceptInlineSlice(fn.Inlines)
+ v.b.WriteByte('}')
+}
+
+var literalCode = map[ast.LiteralCode]string{
+ ast.LiteralProg: "Code",
+ ast.LiteralKeyb: "Input",
+ ast.LiteralOutput: "Output",
+ ast.LiteralComment: "Comment",
+ ast.LiteralHTML: "HTML",
+}
+
+// VisitLiteral write JSON code for literal inline text.
+func (v *detailVisitor) VisitLiteral(ln *ast.LiteralNode) {
+ code, ok := literalCode[ln.Code]
+ if !ok {
+ panic(fmt.Sprintf("Unknown literal code %v", ln.Code))
+ }
+ v.writeNodeStart(code)
+ v.visitAttributes(ln.Attrs)
+ v.writeContentStart('s')
+ writeEscaped(&v.b, ln.Text)
+ v.b.WriteByte('}')
+}
+
+func (v *detailVisitor) acceptBlockSlice(bns ast.BlockSlice) {
+ v.b.WriteByte('[')
+ for i, bn := range bns {
+ if i > 0 {
+ v.b.WriteByte(',')
+ }
+ bn.Accept(v)
+ }
+ v.b.WriteByte(']')
+}
+
+func (v *detailVisitor) acceptItemSlice(ins ast.ItemSlice) {
+ v.b.WriteByte('[')
+ for i, in := range ins {
+ if i > 0 {
+ v.b.WriteByte(',')
+ }
+ in.Accept(v)
+ }
+ v.b.WriteByte(']')
+}
+
+func (v *detailVisitor) acceptDescriptionSlice(dns ast.DescriptionSlice) {
+ v.b.WriteByte('[')
+ for i, dn := range dns {
+ if i > 0 {
+ v.b.WriteByte(',')
+ }
+ dn.Accept(v)
+ }
+ v.b.WriteByte(']')
+}
+
+func (v *detailVisitor) acceptInlineSlice(ins ast.InlineSlice) {
+ v.b.WriteByte('[')
+ for i, in := range ins {
+ if i > 0 {
+ v.b.WriteByte(',')
+ }
+ in.Accept(v)
+ }
+ v.b.WriteByte(']')
+}
+
+// visitAttributes write JSON attributes
+func (v *detailVisitor) visitAttributes(a *ast.Attributes) {
+ if a == nil || len(a.Attrs) == 0 {
+ return
+ }
+ keys := make([]string, 0, len(a.Attrs))
+ for k := range a.Attrs {
+ keys = append(keys, k)
+ }
+ sort.Strings(keys)
+
+ v.b.WriteString(",\"a\":{\"")
+ for i, k := range keys {
+ if i > 0 {
+ v.b.WriteString("\",\"")
+ }
+ v.b.Write(Escape(k))
+ v.b.WriteString("\":\"")
+ v.b.Write(Escape(a.Attrs[k]))
+ }
+ v.b.WriteString("\"}")
+}
+
+func (v *detailVisitor) writeNodeStart(t string) {
+ v.b.WriteStrings("{\"t\":\"", t, "\"")
+}
+
+var contentCode = map[rune][]byte{
+ 'b': []byte(",\"b\":"), // List of blocks
+ 'c': []byte(",\"c\":["), // List of list of blocks
+ 'g': []byte(",\"g\":["), // General list
+ 'i': []byte(",\"i\":"), // List of inlines
+ 'j': []byte(",\"j\":{"), // Embedded JSON object
+ 'l': []byte(",\"l\":["), // List of lines
+ 'n': []byte(",\"n\":"), // Number
+ 'o': []byte(",\"o\":\""), // Byte object
+ 'p': []byte(",\"p\":["), // Generic tuple
+ 'q': []byte(",\"q\":"), // String, if 's' is also needed
+ 's': []byte(",\"s\":"), // String
+ 't': []byte("Content code 't' is not allowed"),
+ 'y': []byte("Content code 'y' is not allowed"), // field after 'j'
+}
+
+func (v *detailVisitor) writeContentStart(code rune) {
+ if b, ok := contentCode[code]; ok {
+ v.b.Write(b)
+ return
+ }
+ panic("Unknown content code " + strconv.Itoa(int(code)))
+}
+
+func (v *detailVisitor) writeMeta(m *meta.Meta) {
+ for _, p := range m.Pairs(true) {
+ if p.Key == meta.KeyTitle {
+ continue
+ }
+ v.b.WriteString(",\"")
+ v.b.Write(Escape(p.Key))
+ v.b.WriteString("\":")
+ if m.Type(p.Key).IsSet {
+ v.writeSetValue(p.Value)
+ } else {
+ v.b.WriteByte('"')
+ v.b.Write(Escape(p.Value))
+ v.b.WriteByte('"')
+ }
+ }
+}
+
+func (v *detailVisitor) writeSetValue(value string) {
+ v.b.WriteByte('[')
+ for i, val := range meta.ListFromValue(value) {
+ if i > 0 {
+ v.b.WriteByte(',')
+ }
+ v.b.WriteByte('"')
+ v.b.Write(Escape(val))
+ v.b.WriteByte('"')
+ }
+ v.b.WriteByte(']')
+}
ADDED encoder/jsonenc/jsonenc.go
Index: encoder/jsonenc/jsonenc.go
==================================================================
--- encoder/jsonenc/jsonenc.go
+++ encoder/jsonenc/jsonenc.go
@@ -0,0 +1,111 @@
+//-----------------------------------------------------------------------------
+// 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 jsonenc encodes the abstract syntax tree into some JSON formats.
+package jsonenc
+
+import (
+ "bytes"
+ "io"
+
+ "zettelstore.de/z/ast"
+ "zettelstore.de/z/domain/meta"
+ "zettelstore.de/z/encoder"
+)
+
+func init() {
+ encoder.Register("json", encoder.Info{
+ Create: func(*encoder.Environment) encoder.Encoder { return &jsonEncoder{} },
+ Default: true,
+ })
+}
+
+// jsonEncoder is just a stub. It is not implemented. The real implementation
+// is in file web/adapter/json.go
+type jsonEncoder struct{}
+
+// WriteZettel writes the encoded zettel to the writer.
+func (je *jsonEncoder) WriteZettel(
+ w io.Writer, zn *ast.ZettelNode, inhMeta bool) (int, error) {
+ return 0, encoder.ErrNoWriteZettel
+}
+
+// WriteMeta encodes meta data as HTML5.
+func (je *jsonEncoder) WriteMeta(w io.Writer, meta *meta.Meta) (int, error) {
+ return 0, encoder.ErrNoWriteMeta
+}
+
+func (je *jsonEncoder) WriteContent(w io.Writer, zn *ast.ZettelNode) (int, error) {
+ return 0, encoder.ErrNoWriteContent
+}
+
+// WriteBlocks writes a block slice to the writer
+func (je *jsonEncoder) WriteBlocks(w io.Writer, bs ast.BlockSlice) (int, error) {
+ return 0, encoder.ErrNoWriteBlocks
+}
+
+// WriteInlines writes an inline slice to the writer
+func (je *jsonEncoder) WriteInlines(w io.Writer, is ast.InlineSlice) (int, error) {
+ return 0, encoder.ErrNoWriteInlines
+}
+
+var (
+ jsBackslash = []byte{'\\', '\\'}
+ jsDoubleQuote = []byte{'\\', '"'}
+ jsNewline = []byte{'\\', 'n'}
+ jsTab = []byte{'\\', 't'}
+ jsCr = []byte{'\\', 'r'}
+ jsUnicode = []byte{'\\', 'u', '0', '0', '0', '0'}
+ jsHex = []byte("0123456789ABCDEF")
+)
+
+// Escape returns the given string as a byte slice, where every non-printable
+// rune is made printable.
+func Escape(s string) []byte {
+ var buf bytes.Buffer
+
+ last := 0
+ for i, ch := range s {
+ var b []byte
+ switch ch {
+ case '\t':
+ b = jsTab
+ case '\r':
+ b = jsCr
+ case '\n':
+ b = jsNewline
+ case '"':
+ b = jsDoubleQuote
+ case '\\':
+ b = jsBackslash
+ default:
+ if ch < ' ' {
+ b = jsUnicode
+ b[2] = '0'
+ b[3] = '0'
+ b[4] = jsHex[ch>>4]
+ b[5] = jsHex[ch&0xF]
+ } else {
+ continue
+ }
+ }
+ buf.WriteString(s[last:i])
+ buf.Write(b)
+ last = i + 1
+ }
+ buf.WriteString(s[last:])
+ return buf.Bytes()
+}
+
+func writeEscaped(b *encoder.BufWriter, s string) {
+ b.WriteByte('"')
+ b.Write(Escape(s))
+ b.WriteByte('"')
+}
DELETED encoder/mdenc/mdenc.go
Index: encoder/mdenc/mdenc.go
==================================================================
--- encoder/mdenc/mdenc.go
+++ encoder/mdenc/mdenc.go
@@ -1,362 +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 mdenc encodes the abstract syntax tree back into Markdown.
-package mdenc
-
-import (
- "io"
-
- "t73f.de/r/zsc/api"
- "zettelstore.de/z/ast"
- "zettelstore.de/z/encoder"
- "zettelstore.de/z/zettel/meta"
-)
-
-func init() {
- encoder.Register(api.EncoderMD, func(*encoder.CreateParameter) encoder.Encoder { return Create() })
-}
-
-// Create an encoder.
-func Create() *Encoder { return &myME }
-
-type Encoder struct{}
-
-var myME Encoder
-
-// WriteZettel writes the encoded zettel to the writer.
-func (*Encoder) WriteZettel(w io.Writer, zn *ast.ZettelNode, evalMeta encoder.EvalMetaFunc) (int, error) {
- v := newVisitor(w)
- v.acceptMeta(zn.InhMeta, evalMeta)
- if zn.InhMeta.YamlSep {
- v.b.WriteString("---\n")
- } else {
- v.b.WriteByte('\n')
- }
- ast.Walk(v, &zn.Ast)
- length, err := v.b.Flush()
- return length, err
-}
-
-// WriteMeta encodes meta data as markdown.
-func (*Encoder) WriteMeta(w io.Writer, m *meta.Meta, evalMeta encoder.EvalMetaFunc) (int, error) {
- v := newVisitor(w)
- v.acceptMeta(m, evalMeta)
- length, err := v.b.Flush()
- return length, err
-}
-
-func (v *visitor) acceptMeta(m *meta.Meta, evalMeta encoder.EvalMetaFunc) {
- for _, p := range m.ComputedPairs() {
- key := p.Key
- v.b.WriteStrings(key, ": ")
- if meta.Type(key) == meta.TypeZettelmarkup {
- is := evalMeta(p.Value)
- ast.Walk(v, &is)
- } else {
- v.b.WriteString(p.Value)
- }
- v.b.WriteByte('\n')
- }
-}
-
-func (ze *Encoder) WriteContent(w io.Writer, zn *ast.ZettelNode) (int, error) {
- return ze.WriteBlocks(w, &zn.Ast)
-}
-
-// WriteBlocks writes the content of a block slice to the writer.
-func (*Encoder) WriteBlocks(w io.Writer, bs *ast.BlockSlice) (int, error) {
- v := newVisitor(w)
- ast.Walk(v, bs)
- length, err := v.b.Flush()
- return length, err
-}
-
-// WriteInlines writes an inline slice to the writer
-func (*Encoder) WriteInlines(w io.Writer, is *ast.InlineSlice) (int, error) {
- v := newVisitor(w)
- ast.Walk(v, is)
- length, err := v.b.Flush()
- return length, err
-}
-
-// visitor writes the abstract syntax tree to an EncWriter.
-type visitor struct {
- b encoder.EncWriter
- listInfo []int
- listPrefix string
-}
-
-func newVisitor(w io.Writer) *visitor {
- return &visitor{b: encoder.NewEncWriter(w)}
-}
-
-func (v *visitor) Visit(node ast.Node) ast.Visitor {
- switch n := node.(type) {
- case *ast.BlockSlice:
- v.visitBlockSlice(n)
- case *ast.VerbatimNode:
- v.visitVerbatim(n)
- case *ast.RegionNode:
- v.visitRegion(n)
- case *ast.HeadingNode:
- v.visitHeading(n)
- case *ast.HRuleNode:
- v.b.WriteString("---")
- case *ast.NestedListNode:
- v.visitNestedList(n)
- case *ast.DescriptionListNode:
- return nil // Should write no content
- case *ast.TableNode:
- return nil // Should write no content
- case *ast.TextNode:
- v.visitText(n)
- case *ast.SpaceNode:
- v.b.WriteString(n.Lexeme)
- case *ast.BreakNode:
- v.visitBreak(n)
- case *ast.LinkNode:
- v.visitLink(n)
- case *ast.EmbedRefNode:
- v.visitEmbedRef(n)
- case *ast.FootnoteNode:
- return nil // Should write no content
- case *ast.FormatNode:
- v.visitFormat(n)
- case *ast.LiteralNode:
- v.visitLiteral(n)
- default:
- return v
- }
- return nil
-}
-
-func (v *visitor) visitBlockSlice(bs *ast.BlockSlice) {
- for i, bn := range *bs {
- if i > 0 {
- v.b.WriteString("\n\n")
- }
- ast.Walk(v, bn)
- }
-}
-
-func (v *visitor) visitVerbatim(vn *ast.VerbatimNode) {
- lc := len(vn.Content)
- if vn.Kind != ast.VerbatimProg || lc == 0 {
- return
- }
- v.writeSpaces(4)
- lcm1 := lc - 1
- for i := 0; i < lc; i++ {
- b := vn.Content[i]
- if b != '\n' && b != '\r' {
- v.b.WriteByte(b)
- continue
- }
- j := i + 1
- for ; j < lc; j++ {
- c := vn.Content[j]
- if c != '\n' && c != '\r' {
- break
- }
- }
- if j >= lcm1 {
- break
- }
- v.b.WriteByte('\n')
- v.writeSpaces(4)
- i = j - 1
- }
-}
-
-func (v *visitor) visitRegion(rn *ast.RegionNode) {
- if rn.Kind != ast.RegionQuote {
- return
- }
- first := true
- for _, bn := range rn.Blocks {
- pn, ok := bn.(*ast.ParaNode)
- if !ok {
- continue
- }
- if !first {
- v.b.WriteString("\n\n")
- }
- first = false
- v.b.WriteString("> ")
- ast.Walk(v, &pn.Inlines)
- }
-}
-
-func (v *visitor) visitHeading(hn *ast.HeadingNode) {
- const headingSigns = "###### "
- v.b.WriteString(headingSigns[len(headingSigns)-hn.Level-1:])
- ast.Walk(v, &hn.Inlines)
-}
-
-func (v *visitor) visitNestedList(ln *ast.NestedListNode) {
- switch ln.Kind {
- case ast.NestedListOrdered:
- v.writeNestedList(ln, "1. ")
- case ast.NestedListUnordered:
- v.writeNestedList(ln, "* ")
- case ast.NestedListQuote:
- v.writeListQuote(ln)
- }
- v.listInfo = v.listInfo[:len(v.listInfo)-1]
-}
-
-func (v *visitor) writeNestedList(ln *ast.NestedListNode, enum string) {
- v.listInfo = append(v.listInfo, len(enum))
- regIndent := 4*len(v.listInfo) - 4
- paraIndent := regIndent + len(enum)
- for i, item := range ln.Items {
- if i > 0 {
- v.b.WriteByte('\n')
- }
- v.writeSpaces(regIndent)
- v.b.WriteString(enum)
- for j, in := range item {
- if j > 0 {
- v.b.WriteByte('\n')
- if _, ok := in.(*ast.ParaNode); ok {
- v.writeSpaces(paraIndent)
- }
- }
- ast.Walk(v, in)
- }
- }
-}
-
-func (v *visitor) writeListQuote(ln *ast.NestedListNode) {
- v.listInfo = append(v.listInfo, 0)
- if len(v.listInfo) > 1 {
- return
- }
-
- prefix := v.listPrefix
- v.listPrefix = "> "
-
- for i, item := range ln.Items {
- if i > 0 {
- v.b.WriteByte('\n')
- }
- v.b.WriteString(v.listPrefix)
- for j, in := range item {
- if j > 0 {
- v.b.WriteByte('\n')
- if _, ok := in.(*ast.ParaNode); ok {
- v.b.WriteString(v.listPrefix)
- }
- }
- ast.Walk(v, in)
- }
- }
-
- v.listPrefix = prefix
-}
-
-func (v *visitor) visitText(tn *ast.TextNode) {
- v.b.WriteString(tn.Text)
-}
-
-func (v *visitor) visitBreak(bn *ast.BreakNode) {
- if bn.Hard {
- v.b.WriteString("\\\n")
- } else {
- v.b.WriteByte('\n')
- }
- if l := len(v.listInfo); l > 0 {
- if v.listPrefix == "" {
- v.writeSpaces(4*l - 4 + v.listInfo[l-1])
- } else {
- v.writeSpaces(4*l - 4)
- v.b.WriteString(v.listPrefix)
- }
- }
-}
-
-func (v *visitor) visitLink(ln *ast.LinkNode) {
- v.writeReference(ln.Ref, ln.Inlines)
-}
-
-func (v *visitor) visitEmbedRef(en *ast.EmbedRefNode) {
- v.b.WriteByte('!')
- v.writeReference(en.Ref, en.Inlines)
-}
-
-func (v *visitor) writeReference(ref *ast.Reference, is ast.InlineSlice) {
- if ref.State == ast.RefStateQuery {
- ast.Walk(v, &is)
- } else if len(is) > 0 {
- v.b.WriteByte('[')
- ast.Walk(v, &is)
- v.b.WriteStrings("](", ref.String())
- v.b.WriteByte(')')
- } else if isAutoLinkable(ref) {
- v.b.WriteByte('<')
- v.b.WriteString(ref.String())
- v.b.WriteByte('>')
- } else {
- s := ref.String()
- v.b.WriteStrings("[", s, "](", s, ")")
- }
-}
-
-func isAutoLinkable(ref *ast.Reference) bool {
- if ref.State != ast.RefStateExternal || ref.URL == nil {
- return false
- }
- return ref.URL.Scheme != ""
-}
-
-func (v *visitor) visitFormat(fn *ast.FormatNode) {
- switch fn.Kind {
- case ast.FormatEmph:
- v.b.WriteByte('*')
- ast.Walk(v, &fn.Inlines)
- v.b.WriteByte('*')
- case ast.FormatStrong:
- v.b.WriteString("__")
- ast.Walk(v, &fn.Inlines)
- v.b.WriteString("__")
- case ast.FormatQuote:
- v.b.WriteString("")
- ast.Walk(v, &fn.Inlines)
- v.b.WriteString("
")
- case ast.FormatMark:
- v.b.WriteString("")
- ast.Walk(v, &fn.Inlines)
- v.b.WriteString("")
- default:
- ast.Walk(v, &fn.Inlines)
- }
-}
-
-func (v *visitor) visitLiteral(ln *ast.LiteralNode) {
- switch ln.Kind {
- case ast.LiteralProg:
- v.b.WriteByte('`')
- v.b.Write(ln.Content)
- v.b.WriteByte('`')
- case ast.LiteralComment, ast.LiteralHTML: // ignore everything
- default:
- v.b.Write(ln.Content)
- }
-}
-
-func (v *visitor) writeSpaces(n int) {
- for range n {
- v.b.WriteByte(' ')
- }
-}
ADDED encoder/nativeenc/nativeenc.go
Index: encoder/nativeenc/nativeenc.go
==================================================================
--- encoder/nativeenc/nativeenc.go
+++ encoder/nativeenc/nativeenc.go
@@ -0,0 +1,614 @@
+//-----------------------------------------------------------------------------
+// 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 nativeenc encodes the abstract syntax tree into native format.
+package nativeenc
+
+import (
+ "fmt"
+ "io"
+ "sort"
+ "strconv"
+
+ "zettelstore.de/z/ast"
+ "zettelstore.de/z/domain/meta"
+ "zettelstore.de/z/encoder"
+ "zettelstore.de/z/encoder/encfun"
+ "zettelstore.de/z/parser"
+)
+
+func init() {
+ encoder.Register("native", encoder.Info{
+ Create: func(env *encoder.Environment) encoder.Encoder { return &nativeEncoder{env: env} },
+ })
+}
+
+type nativeEncoder struct {
+ env *encoder.Environment
+}
+
+// WriteZettel encodes the zettel to the writer.
+func (ne *nativeEncoder) WriteZettel(
+ w io.Writer, zn *ast.ZettelNode, inhMeta bool) (int, error) {
+ v := newVisitor(w, ne)
+ v.b.WriteString("[Title ")
+ v.acceptInlineSlice(encfun.MetaAsInlineSlice(zn.InhMeta, meta.KeyTitle))
+ v.b.WriteByte(']')
+ if inhMeta {
+ v.acceptMeta(zn.InhMeta, false)
+ } else {
+ v.acceptMeta(zn.Meta, false)
+ }
+ v.b.WriteByte('\n')
+ v.acceptBlockSlice(zn.Ast)
+ length, err := v.b.Flush()
+ return length, err
+}
+
+// WriteMeta encodes meta data in native format.
+func (ne *nativeEncoder) WriteMeta(w io.Writer, m *meta.Meta) (int, error) {
+ v := newVisitor(w, ne)
+ v.acceptMeta(m, true)
+ length, err := v.b.Flush()
+ return length, err
+}
+
+func (ne *nativeEncoder) WriteContent(w io.Writer, zn *ast.ZettelNode) (int, error) {
+ return ne.WriteBlocks(w, zn.Ast)
+}
+
+// WriteBlocks writes a block slice to the writer
+func (ne *nativeEncoder) WriteBlocks(w io.Writer, bs ast.BlockSlice) (int, error) {
+ v := newVisitor(w, ne)
+ v.acceptBlockSlice(bs)
+ length, err := v.b.Flush()
+ return length, err
+}
+
+// WriteInlines writes an inline slice to the writer
+func (ne *nativeEncoder) WriteInlines(w io.Writer, is ast.InlineSlice) (int, error) {
+ v := newVisitor(w, ne)
+ v.acceptInlineSlice(is)
+ length, err := v.b.Flush()
+ return length, err
+}
+
+// visitor writes the abstract syntax tree to an io.Writer.
+type visitor struct {
+ b encoder.BufWriter
+ level int
+ env *encoder.Environment
+}
+
+func newVisitor(w io.Writer, enc *nativeEncoder) *visitor {
+ return &visitor{b: encoder.NewBufWriter(w), env: enc.env}
+}
+
+var (
+ rawBackslash = []byte{'\\', '\\'}
+ rawDoubleQuote = []byte{'\\', '"'}
+ rawNewline = []byte{'\\', 'n'}
+)
+
+func (v *visitor) acceptMeta(m *meta.Meta, withTitle bool) {
+ if withTitle {
+ v.b.WriteString("[Title ")
+ v.acceptInlineSlice(parser.ParseMetadata(m.GetDefault(meta.KeyTitle, "")))
+ v.b.WriteByte(']')
+ }
+ v.writeMetaString(m, meta.KeyRole, "Role")
+ v.writeMetaList(m, meta.KeyTags, "Tags")
+ v.writeMetaString(m, meta.KeySyntax, "Syntax")
+ pairs := m.PairsRest(true)
+ if len(pairs) == 0 {
+ return
+ }
+ v.b.WriteString("\n[Header")
+ v.level++
+ for i, p := range pairs {
+ if i > 0 {
+ v.b.WriteByte(',')
+ }
+ v.writeNewLine()
+ v.b.WriteByte('[')
+ v.b.WriteStrings(p.Key, " \"")
+ v.writeEscaped(p.Value)
+ v.b.WriteString("\"]")
+ }
+ v.level--
+ v.b.WriteByte(']')
+}
+
+func (v *visitor) writeMetaString(m *meta.Meta, key, native string) {
+ if val, ok := m.Get(key); ok && len(val) > 0 {
+ v.b.WriteStrings("\n[", native, " \"", val, "\"]")
+ }
+}
+
+func (v *visitor) writeMetaList(m *meta.Meta, key, native string) {
+ if vals, ok := m.GetList(key); ok && len(vals) > 0 {
+ v.b.WriteStrings("\n[", native)
+ for _, val := range vals {
+ v.b.WriteByte(' ')
+ v.b.WriteString(val)
+ }
+ v.b.WriteByte(']')
+ }
+}
+
+// VisitPara emits native code for a paragraph.
+func (v *visitor) VisitPara(pn *ast.ParaNode) {
+ v.b.WriteString("[Para ")
+ v.acceptInlineSlice(pn.Inlines)
+ v.b.WriteByte(']')
+}
+
+var verbatimCode = map[ast.VerbatimCode][]byte{
+ ast.VerbatimProg: []byte("[CodeBlock"),
+ ast.VerbatimComment: []byte("[CommentBlock"),
+ ast.VerbatimHTML: []byte("[HTMLBlock"),
+}
+
+// VisitVerbatim emits native code for verbatim lines.
+func (v *visitor) VisitVerbatim(vn *ast.VerbatimNode) {
+ code, ok := verbatimCode[vn.Code]
+ if !ok {
+ panic(fmt.Sprintf("Unknown verbatim code %v", vn.Code))
+ }
+ v.b.Write(code)
+ v.visitAttributes(vn.Attrs)
+ v.b.WriteString(" \"")
+ for i, line := range vn.Lines {
+ if i > 0 {
+ v.b.Write(rawNewline)
+ }
+ v.writeEscaped(line)
+ }
+ v.b.WriteString("\"]")
+}
+
+var regionCode = map[ast.RegionCode][]byte{
+ ast.RegionSpan: []byte("[SpanBlock"),
+ ast.RegionQuote: []byte("[QuoteBlock"),
+ ast.RegionVerse: []byte("[VerseBlock"),
+}
+
+// VisitRegion writes native code for block regions.
+func (v *visitor) VisitRegion(rn *ast.RegionNode) {
+ code, ok := regionCode[rn.Code]
+ if !ok {
+ panic(fmt.Sprintf("Unknown region code %v", rn.Code))
+ }
+ v.b.Write(code)
+ v.visitAttributes(rn.Attrs)
+ v.level++
+ v.writeNewLine()
+ v.b.WriteByte('[')
+ v.level++
+ v.acceptBlockSlice(rn.Blocks)
+ v.level--
+ v.b.WriteByte(']')
+ if len(rn.Inlines) > 0 {
+ v.b.WriteByte(',')
+ v.writeNewLine()
+ v.b.WriteString("[Cite ")
+ v.acceptInlineSlice(rn.Inlines)
+ v.b.WriteByte(']')
+ }
+ v.level--
+ v.b.WriteByte(']')
+}
+
+// VisitHeading writes the native code for a heading.
+func (v *visitor) VisitHeading(hn *ast.HeadingNode) {
+ v.b.WriteStrings("[Heading ", strconv.Itoa(hn.Level), " \"", hn.Slug, "\"")
+ v.visitAttributes(hn.Attrs)
+ v.b.WriteByte(' ')
+ v.acceptInlineSlice(hn.Inlines)
+ v.b.WriteByte(']')
+}
+
+// VisitHRule writes native code for a horizontal rule:
.
+func (v *visitor) VisitHRule(hn *ast.HRuleNode) {
+ v.b.WriteString("[Hrule")
+ v.visitAttributes(hn.Attrs)
+ v.b.WriteByte(']')
+}
+
+var listCode = map[ast.NestedListCode][]byte{
+ ast.NestedListOrdered: []byte("[OrderedList"),
+ ast.NestedListUnordered: []byte("[BulletList"),
+ ast.NestedListQuote: []byte("[QuoteList"),
+}
+
+// VisitNestedList writes native code for lists and blockquotes.
+func (v *visitor) VisitNestedList(ln *ast.NestedListNode) {
+ v.b.Write(listCode[ln.Code])
+ v.level++
+ for i, item := range ln.Items {
+ if i > 0 {
+ v.b.WriteByte(',')
+ }
+ v.writeNewLine()
+ v.level++
+ v.b.WriteByte('[')
+ v.acceptItemSlice(item)
+ v.b.WriteByte(']')
+ v.level--
+ }
+ v.level--
+ v.b.WriteByte(']')
+}
+
+// VisitDescriptionList emits a native description list.
+func (v *visitor) VisitDescriptionList(dn *ast.DescriptionListNode) {
+ v.b.WriteString("[DescriptionList")
+ v.level++
+ for i, descr := range dn.Descriptions {
+ if i > 0 {
+ v.b.WriteByte(',')
+ }
+ v.writeNewLine()
+ v.b.WriteString("[Term [")
+ v.acceptInlineSlice(descr.Term)
+ v.b.WriteByte(']')
+
+ if len(descr.Descriptions) > 0 {
+ v.level++
+ for _, b := range descr.Descriptions {
+ v.b.WriteByte(',')
+ v.writeNewLine()
+ v.b.WriteString("[Description")
+ v.level++
+ v.writeNewLine()
+ v.acceptDescriptionSlice(b)
+ v.b.WriteByte(']')
+ v.level--
+ }
+ v.level--
+ }
+ v.b.WriteByte(']')
+ }
+ v.level--
+ v.b.WriteByte(']')
+}
+
+// VisitTable emits a native table.
+func (v *visitor) VisitTable(tn *ast.TableNode) {
+ v.b.WriteString("[Table")
+ v.level++
+ if len(tn.Header) > 0 {
+ v.writeNewLine()
+ v.b.WriteString("[Header ")
+ for i, cell := range tn.Header {
+ if i > 0 {
+ v.b.WriteByte(',')
+ }
+ v.writeCell(cell)
+ }
+ v.b.WriteString("],")
+ }
+ for i, row := range tn.Rows {
+ if i > 0 {
+ v.b.WriteByte(',')
+ }
+ v.writeNewLine()
+ v.b.WriteString("[Row ")
+ for j, cell := range row {
+ if j > 0 {
+ v.b.WriteByte(',')
+ }
+ v.writeCell(cell)
+ }
+ v.b.WriteByte(']')
+ }
+ v.level--
+ v.b.WriteByte(']')
+}
+
+var alignString = map[ast.Alignment]string{
+ ast.AlignDefault: " Default",
+ ast.AlignLeft: " Left",
+ ast.AlignCenter: " Center",
+ ast.AlignRight: " Right",
+}
+
+func (v *visitor) writeCell(cell *ast.TableCell) {
+ v.b.WriteStrings("[Cell", alignString[cell.Align])
+ if len(cell.Inlines) > 0 {
+ v.b.WriteByte(' ')
+ v.acceptInlineSlice(cell.Inlines)
+ }
+ v.b.WriteByte(']')
+}
+
+// VisitBLOB writes the binary object as a value.
+func (v *visitor) VisitBLOB(bn *ast.BLOBNode) {
+ v.b.WriteString("[BLOB \"")
+ v.writeEscaped(bn.Title)
+ v.b.WriteString("\" \"")
+ v.writeEscaped(bn.Syntax)
+ v.b.WriteString("\" \"")
+ v.b.WriteBase64(bn.Blob)
+ v.b.WriteString("\"]")
+}
+
+// VisitText writes text content.
+func (v *visitor) VisitText(tn *ast.TextNode) {
+ v.b.WriteString("Text \"")
+ v.writeEscaped(tn.Text)
+ v.b.WriteByte('"')
+}
+
+// VisitTag writes tag content.
+func (v *visitor) VisitTag(tn *ast.TagNode) {
+ v.b.WriteString("Tag \"")
+ v.writeEscaped(tn.Tag)
+ v.b.WriteByte('"')
+}
+
+// VisitSpace emits a white space.
+func (v *visitor) VisitSpace(sn *ast.SpaceNode) {
+ v.b.WriteString("Space")
+ if l := len(sn.Lexeme); l > 1 {
+ v.b.WriteByte(' ')
+ v.b.WriteString(strconv.Itoa(l))
+ }
+}
+
+// VisitBreak writes native code for line breaks.
+func (v *visitor) VisitBreak(bn *ast.BreakNode) {
+ if bn.Hard {
+ v.b.WriteString("Break")
+ } else {
+ v.b.WriteString("Space")
+ }
+}
+
+var mapRefState = map[ast.RefState]string{
+ ast.RefStateInvalid: "INVALID",
+ ast.RefStateZettel: "ZETTEL",
+ ast.RefStateSelf: "SELF",
+ ast.RefStateFound: "ZETTEL",
+ ast.RefStateBroken: "BROKEN",
+ ast.RefStateHosted: "LOCAL",
+ ast.RefStateBased: "BASED",
+ ast.RefStateExternal: "EXTERNAL",
+}
+
+// VisitLink writes native code for links.
+func (v *visitor) VisitLink(ln *ast.LinkNode) {
+ ln, n := v.env.AdaptLink(ln)
+ if n != nil {
+ n.Accept(v)
+ return
+ }
+ v.b.WriteString("Link")
+ v.visitAttributes(ln.Attrs)
+ v.b.WriteByte(' ')
+ v.b.WriteString(mapRefState[ln.Ref.State])
+ v.b.WriteString(" \"")
+ v.writeEscaped(ln.Ref.String())
+ v.b.WriteString("\" [")
+ if !ln.OnlyRef {
+ v.acceptInlineSlice(ln.Inlines)
+ }
+ v.b.WriteByte(']')
+}
+
+// VisitImage writes native code for images.
+func (v *visitor) VisitImage(in *ast.ImageNode) {
+ in, n := v.env.AdaptImage(in)
+ if n != nil {
+ n.Accept(v)
+ return
+ }
+ v.b.WriteString("Image")
+ v.visitAttributes(in.Attrs)
+ if in.Ref == nil {
+ v.b.WriteStrings(" {\"", in.Syntax, "\" \"")
+ switch in.Syntax {
+ case "svg":
+ v.writeEscaped(string(in.Blob))
+ default:
+ v.b.WriteString("\" \"")
+ v.b.WriteBase64(in.Blob)
+ }
+ v.b.WriteString("\"}")
+ } else {
+ v.b.WriteStrings(" \"", in.Ref.String(), "\"")
+ }
+ if len(in.Inlines) > 0 {
+ v.b.WriteString(" [")
+ v.acceptInlineSlice(in.Inlines)
+ v.b.WriteByte(']')
+ }
+}
+
+// VisitCite writes code for citations.
+func (v *visitor) VisitCite(cn *ast.CiteNode) {
+ v.b.WriteString("Cite")
+ v.visitAttributes(cn.Attrs)
+ v.b.WriteString(" \"")
+ v.writeEscaped(cn.Key)
+ v.b.WriteByte('"')
+ if len(cn.Inlines) > 0 {
+ v.b.WriteString(" [")
+ v.acceptInlineSlice(cn.Inlines)
+ v.b.WriteByte(']')
+ }
+}
+
+// VisitFootnote write native code for a footnote.
+func (v *visitor) VisitFootnote(fn *ast.FootnoteNode) {
+ v.b.WriteString("Footnote")
+ v.visitAttributes(fn.Attrs)
+ v.b.WriteString(" [")
+ v.acceptInlineSlice(fn.Inlines)
+ v.b.WriteByte(']')
+}
+
+// VisitMark writes native code to mark a position.
+func (v *visitor) VisitMark(mn *ast.MarkNode) {
+ v.b.WriteString("Mark")
+ if len(mn.Text) > 0 {
+ v.b.WriteString(" \"")
+ v.writeEscaped(mn.Text)
+ v.b.WriteByte('"')
+ }
+}
+
+var formatCode = map[ast.FormatCode][]byte{
+ ast.FormatItalic: []byte("Italic"),
+ ast.FormatEmph: []byte("Emph"),
+ ast.FormatBold: []byte("Bold"),
+ ast.FormatStrong: []byte("Strong"),
+ ast.FormatUnder: []byte("Underline"),
+ ast.FormatInsert: []byte("Insert"),
+ ast.FormatMonospace: []byte("Mono"),
+ ast.FormatStrike: []byte("Strikethrough"),
+ ast.FormatDelete: []byte("Delete"),
+ ast.FormatSuper: []byte("Super"),
+ ast.FormatSub: []byte("Sub"),
+ ast.FormatQuote: []byte("Quote"),
+ ast.FormatQuotation: []byte("Quotation"),
+ ast.FormatSmall: []byte("Small"),
+ ast.FormatSpan: []byte("Span"),
+}
+
+// VisitFormat write native code for formatting text.
+func (v *visitor) VisitFormat(fn *ast.FormatNode) {
+ v.b.Write(formatCode[fn.Code])
+ v.visitAttributes(fn.Attrs)
+ v.b.WriteString(" [")
+ v.acceptInlineSlice(fn.Inlines)
+ v.b.WriteByte(']')
+}
+
+var literalCode = map[ast.LiteralCode][]byte{
+ ast.LiteralProg: []byte("Code"),
+ ast.LiteralKeyb: []byte("Input"),
+ ast.LiteralOutput: []byte("Output"),
+ ast.LiteralComment: []byte("Comment"),
+ ast.LiteralHTML: []byte("HTML"),
+}
+
+// VisitLiteral write native code for code inline text.
+func (v *visitor) VisitLiteral(ln *ast.LiteralNode) {
+ code, ok := literalCode[ln.Code]
+ if !ok {
+ panic(fmt.Sprintf("Unknown literal code %v", ln.Code))
+ }
+ v.b.Write(code)
+ v.visitAttributes(ln.Attrs)
+ v.b.WriteString(" \"")
+ v.writeEscaped(ln.Text)
+ v.b.WriteByte('"')
+}
+
+func (v *visitor) acceptBlockSlice(bns ast.BlockSlice) {
+ for i, bn := range bns {
+ if i > 0 {
+ v.b.WriteByte(',')
+ v.writeNewLine()
+ }
+ bn.Accept(v)
+ }
+}
+func (v *visitor) acceptItemSlice(ins ast.ItemSlice) {
+ for i, in := range ins {
+ if i > 0 {
+ v.b.WriteByte(',')
+ v.writeNewLine()
+ }
+ in.Accept(v)
+ }
+}
+func (v *visitor) acceptDescriptionSlice(dns ast.DescriptionSlice) {
+ for i, dn := range dns {
+ if i > 0 {
+ v.b.WriteByte(',')
+ v.writeNewLine()
+ }
+ dn.Accept(v)
+ }
+}
+func (v *visitor) acceptInlineSlice(ins ast.InlineSlice) {
+ for i, in := range ins {
+ if i > 0 {
+ v.b.WriteByte(',')
+ }
+ in.Accept(v)
+ }
+}
+
+// visitAttributes write native attributes
+func (v *visitor) visitAttributes(a *ast.Attributes) {
+ if a == nil || len(a.Attrs) == 0 {
+ return
+ }
+ keys := make([]string, 0, len(a.Attrs))
+ for k := range a.Attrs {
+ keys = append(keys, k)
+ }
+ sort.Strings(keys)
+
+ v.b.WriteString(" (\"")
+ if val, ok := a.Attrs[""]; ok {
+ v.writeEscaped(val)
+ }
+ v.b.WriteString("\",[")
+ first := true
+ for _, k := range keys {
+ if k == "" {
+ continue
+ }
+ if !first {
+ v.b.WriteByte(',')
+ }
+ v.b.WriteString(k)
+ val := a.Attrs[k]
+ if len(val) > 0 {
+ v.b.WriteString("=\"")
+ v.writeEscaped(val)
+ v.b.WriteByte('"')
+ }
+ first = false
+ }
+ v.b.WriteString("])")
+}
+
+func (v *visitor) writeNewLine() {
+ v.b.WriteByte('\n')
+ for i := 0; i < v.level; i++ {
+ v.b.WriteByte(' ')
+ }
+}
+
+func (v *visitor) writeEscaped(s string) {
+ last := 0
+ for i, ch := range s {
+ var b []byte
+ switch ch {
+ case '\n':
+ b = rawNewline
+ case '"':
+ b = rawDoubleQuote
+ case '\\':
+ b = rawBackslash
+ default:
+ continue
+ }
+ v.b.WriteString(s[last:i])
+ v.b.Write(b)
+ last = i + 1
+ }
+ v.b.WriteString(s[last:])
+}
ADDED encoder/rawenc/rawenc.go
Index: encoder/rawenc/rawenc.go
==================================================================
--- encoder/rawenc/rawenc.go
+++ encoder/rawenc/rawenc.go
@@ -0,0 +1,68 @@
+//-----------------------------------------------------------------------------
+// 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 rawenc encodes the abstract syntax tree as raw content.
+package rawenc
+
+import (
+ "io"
+
+ "zettelstore.de/z/ast"
+ "zettelstore.de/z/domain/meta"
+ "zettelstore.de/z/encoder"
+)
+
+func init() {
+ encoder.Register("raw", encoder.Info{
+ Create: func(*encoder.Environment) encoder.Encoder { return &rawEncoder{} },
+ })
+}
+
+type rawEncoder struct{}
+
+// WriteZettel writes the encoded zettel to the writer.
+func (re *rawEncoder) WriteZettel(
+ w io.Writer, zn *ast.ZettelNode, inhMeta bool) (int, error) {
+ b := encoder.NewBufWriter(w)
+ if inhMeta {
+ zn.InhMeta.Write(&b, true)
+ } else {
+ zn.Meta.Write(&b, true)
+ }
+ b.WriteByte('\n')
+ b.WriteString(zn.Content.AsString())
+ length, err := b.Flush()
+ return length, err
+}
+
+// WriteMeta encodes meta data as HTML5.
+func (re *rawEncoder) WriteMeta(w io.Writer, m *meta.Meta) (int, error) {
+ b := encoder.NewBufWriter(w)
+ m.Write(&b, true)
+ length, err := b.Flush()
+ return length, err
+}
+
+func (re *rawEncoder) WriteContent(w io.Writer, zn *ast.ZettelNode) (int, error) {
+ b := encoder.NewBufWriter(w)
+ b.WriteString(zn.Content.AsString())
+ length, err := b.Flush()
+ return length, err
+}
+
+// WriteBlocks writes a block slice to the writer
+func (re *rawEncoder) WriteBlocks(w io.Writer, bs ast.BlockSlice) (int, error) {
+ return 0, encoder.ErrNoWriteBlocks
+}
+
+// WriteInlines writes an inline slice to the writer
+func (re *rawEncoder) WriteInlines(w io.Writer, is ast.InlineSlice) (int, error) {
+ return 0, encoder.ErrNoWriteInlines
+}
DELETED encoder/shtmlenc/shtmlenc.go
Index: encoder/shtmlenc/shtmlenc.go
==================================================================
--- encoder/shtmlenc/shtmlenc.go
+++ encoder/shtmlenc/shtmlenc.go
@@ -1,97 +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 shtmlenc encodes the abstract syntax tree into a s-expr which represents HTML.
-package shtmlenc
-
-import (
- "io"
-
- "t73f.de/r/sx"
- "t73f.de/r/zsc/api"
- "t73f.de/r/zsc/shtml"
- "zettelstore.de/z/ast"
- "zettelstore.de/z/encoder"
- "zettelstore.de/z/encoder/szenc"
- "zettelstore.de/z/zettel/meta"
-)
-
-func init() {
- encoder.Register(api.EncoderSHTML, func(params *encoder.CreateParameter) encoder.Encoder { return Create(params) })
-}
-
-// Create a SHTML encoder
-func Create(params *encoder.CreateParameter) *Encoder {
- // We need a new transformer every time, because tx.inVerse must be unique.
- // If we can refactor it out, the transformer can be created only once.
- return &Encoder{
- tx: szenc.NewTransformer(),
- th: shtml.NewEvaluator(1),
- lang: params.Lang,
- }
-}
-
-type Encoder struct {
- tx *szenc.Transformer
- th *shtml.Evaluator
- lang string
-}
-
-// WriteZettel writes the encoded zettel to the writer.
-func (enc *Encoder) WriteZettel(w io.Writer, zn *ast.ZettelNode, evalMeta encoder.EvalMetaFunc) (int, error) {
- env := shtml.MakeEnvironment(enc.lang)
- metaSHTML, err := enc.th.Evaluate(enc.tx.GetMeta(zn.InhMeta, evalMeta), &env)
- if err != nil {
- return 0, err
- }
- contentSHTML, err := enc.th.Evaluate(enc.tx.GetSz(&zn.Ast), &env)
- if err != nil {
- return 0, err
- }
- result := sx.Cons(metaSHTML, contentSHTML)
- return result.Print(w)
-}
-
-// WriteMeta encodes meta data as s-expression.
-func (enc *Encoder) WriteMeta(w io.Writer, m *meta.Meta, evalMeta encoder.EvalMetaFunc) (int, error) {
- env := shtml.MakeEnvironment(enc.lang)
- metaSHTML, err := enc.th.Evaluate(enc.tx.GetMeta(m, evalMeta), &env)
- if err != nil {
- return 0, err
- }
- return sx.Print(w, metaSHTML)
-}
-
-func (enc *Encoder) WriteContent(w io.Writer, zn *ast.ZettelNode) (int, error) {
- return enc.WriteBlocks(w, &zn.Ast)
-}
-
-// WriteBlocks writes a block slice to the writer
-func (enc *Encoder) WriteBlocks(w io.Writer, bs *ast.BlockSlice) (int, error) {
- env := shtml.MakeEnvironment(enc.lang)
- hval, err := enc.th.Evaluate(enc.tx.GetSz(bs), &env)
- if err != nil {
- return 0, err
- }
- return sx.Print(w, hval)
-}
-
-// WriteInlines writes an inline slice to the writer
-func (enc *Encoder) WriteInlines(w io.Writer, is *ast.InlineSlice) (int, error) {
- env := shtml.MakeEnvironment(enc.lang)
- hval, err := enc.th.Evaluate(enc.tx.GetSz(is), &env)
- if err != nil {
- return 0, err
- }
- return sx.Print(w, hval)
-}
DELETED encoder/szenc/szenc.go
Index: encoder/szenc/szenc.go
==================================================================
--- encoder/szenc/szenc.go
+++ encoder/szenc/szenc.go
@@ -1,66 +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 szenc encodes the abstract syntax tree into a s-expr for zettel.
-package szenc
-
-import (
- "io"
-
- "t73f.de/r/sx"
- "t73f.de/r/zsc/api"
- "zettelstore.de/z/ast"
- "zettelstore.de/z/encoder"
- "zettelstore.de/z/zettel/meta"
-)
-
-func init() {
- encoder.Register(api.EncoderSz, func(*encoder.CreateParameter) encoder.Encoder { return Create() })
-}
-
-// Create a S-expr encoder
-func Create() *Encoder {
- // We need a new transformer every time, because trans.inVerse must be unique.
- // If we can refactor it out, the transformer can be created only once.
- return &Encoder{trans: NewTransformer()}
-}
-
-type Encoder struct {
- trans *Transformer
-}
-
-// WriteZettel writes the encoded zettel to the writer.
-func (enc *Encoder) WriteZettel(w io.Writer, zn *ast.ZettelNode, evalMeta encoder.EvalMetaFunc) (int, error) {
- content := enc.trans.GetSz(&zn.Ast)
- meta := enc.trans.GetMeta(zn.InhMeta, evalMeta)
- return sx.MakeList(meta, content).Print(w)
-}
-
-// WriteMeta encodes meta data as s-expression.
-func (enc *Encoder) WriteMeta(w io.Writer, m *meta.Meta, evalMeta encoder.EvalMetaFunc) (int, error) {
- return enc.trans.GetMeta(m, evalMeta).Print(w)
-}
-
-func (enc *Encoder) WriteContent(w io.Writer, zn *ast.ZettelNode) (int, error) {
- return enc.WriteBlocks(w, &zn.Ast)
-}
-
-// WriteBlocks writes a block slice to the writer
-func (enc *Encoder) WriteBlocks(w io.Writer, bs *ast.BlockSlice) (int, error) {
- return enc.trans.GetSz(bs).Print(w)
-}
-
-// WriteInlines writes an inline slice to the writer
-func (enc *Encoder) WriteInlines(w io.Writer, is *ast.InlineSlice) (int, error) {
- return enc.trans.GetSz(is).Print(w)
-}
DELETED encoder/szenc/transform.go
Index: encoder/szenc/transform.go
==================================================================
--- encoder/szenc/transform.go
+++ encoder/szenc/transform.go
@@ -1,413 +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 szenc
-
-import (
- "encoding/base64"
- "fmt"
- "strings"
-
- "t73f.de/r/sx"
- "t73f.de/r/zsc/attrs"
- "t73f.de/r/zsc/sz"
- "zettelstore.de/z/ast"
- "zettelstore.de/z/encoder"
- "zettelstore.de/z/zettel/meta"
-)
-
-// NewTransformer returns a new transformer to create s-expressions from AST nodes.
-func NewTransformer() *Transformer {
- t := Transformer{}
- return &t
-}
-
-type Transformer struct {
- inVerse bool
-}
-
-func (t *Transformer) GetSz(node ast.Node) *sx.Pair {
- switch n := node.(type) {
- case *ast.BlockSlice:
- return t.getBlockList(n).Cons(sz.SymBlock)
- case *ast.InlineSlice:
- return t.getInlineList(*n).Cons(sz.SymInline)
- case *ast.ParaNode:
- return t.getInlineList(n.Inlines).Cons(sz.SymPara)
- case *ast.VerbatimNode:
- return sx.MakeList(
- mapGetS(mapVerbatimKindS, n.Kind),
- getAttributes(n.Attrs),
- sx.MakeString(string(n.Content)),
- )
- case *ast.RegionNode:
- return t.getRegion(n)
- case *ast.HeadingNode:
- return t.getInlineList(n.Inlines).
- Cons(sx.MakeString(n.Fragment)).
- Cons(sx.MakeString(n.Slug)).
- Cons(getAttributes(n.Attrs)).
- Cons(sx.Int64(int64(n.Level))).
- Cons(sz.SymHeading)
- case *ast.HRuleNode:
- return sx.MakeList(sz.SymThematic, getAttributes(n.Attrs))
- case *ast.NestedListNode:
- return t.getNestedList(n)
- case *ast.DescriptionListNode:
- return t.getDescriptionList(n)
- case *ast.TableNode:
- return t.getTable(n)
- case *ast.TranscludeNode:
- return sx.MakeList(sz.SymTransclude, getAttributes(n.Attrs), getReference(n.Ref))
- case *ast.BLOBNode:
- return t.getBLOB(n)
- case *ast.TextNode:
- return sx.MakeList(sz.SymText, sx.MakeString(n.Text))
- case *ast.SpaceNode:
- if t.inVerse {
- return sx.MakeList(sz.SymSpace, sx.MakeString(n.Lexeme))
- }
- return sx.MakeList(sz.SymSpace)
- case *ast.BreakNode:
- if n.Hard {
- return sx.MakeList(sz.SymHard)
- }
- return sx.MakeList(sz.SymSoft)
- case *ast.LinkNode:
- return t.getLink(n)
- case *ast.EmbedRefNode:
- return t.getInlineList(n.Inlines).
- Cons(sx.MakeString(n.Syntax)).
- Cons(getReference(n.Ref)).
- Cons(getAttributes(n.Attrs)).
- Cons(sz.SymEmbed)
- case *ast.EmbedBLOBNode:
- return t.getEmbedBLOB(n)
- case *ast.CiteNode:
- return t.getInlineList(n.Inlines).
- Cons(sx.MakeString(n.Key)).
- Cons(getAttributes(n.Attrs)).
- Cons(sz.SymCite)
- case *ast.FootnoteNode:
- // (ENDNODE attrs InlineElement ...)
- return t.getInlineList(n.Inlines).Cons(getAttributes(n.Attrs)).Cons(sz.SymEndnote)
- case *ast.MarkNode:
- return t.getInlineList(n.Inlines).
- Cons(sx.MakeString(n.Fragment)).
- Cons(sx.MakeString(n.Slug)).
- Cons(sx.MakeString(n.Mark)).
- Cons(sz.SymMark)
- case *ast.FormatNode:
- return t.getInlineList(n.Inlines).
- Cons(getAttributes(n.Attrs)).
- Cons(mapGetS(mapFormatKindS, n.Kind))
- case *ast.LiteralNode:
- return sx.MakeList(
- mapGetS(mapLiteralKindS, n.Kind),
- getAttributes(n.Attrs),
- sx.MakeString(string(n.Content)),
- )
- }
- return sx.MakeList(sz.SymUnknown, sx.MakeString(fmt.Sprintf("%T %v", node, node)))
-}
-
-var mapVerbatimKindS = map[ast.VerbatimKind]*sx.Symbol{
- ast.VerbatimZettel: sz.SymVerbatimZettel,
- ast.VerbatimProg: sz.SymVerbatimProg,
- ast.VerbatimEval: sz.SymVerbatimEval,
- ast.VerbatimMath: sz.SymVerbatimMath,
- ast.VerbatimComment: sz.SymVerbatimComment,
- ast.VerbatimHTML: sz.SymVerbatimHTML,
-}
-
-var mapFormatKindS = map[ast.FormatKind]*sx.Symbol{
- ast.FormatEmph: sz.SymFormatEmph,
- ast.FormatStrong: sz.SymFormatStrong,
- ast.FormatDelete: sz.SymFormatDelete,
- ast.FormatInsert: sz.SymFormatInsert,
- ast.FormatSuper: sz.SymFormatSuper,
- ast.FormatSub: sz.SymFormatSub,
- ast.FormatQuote: sz.SymFormatQuote,
- ast.FormatMark: sz.SymFormatMark,
- ast.FormatSpan: sz.SymFormatSpan,
-}
-
-var mapLiteralKindS = map[ast.LiteralKind]*sx.Symbol{
- ast.LiteralZettel: sz.SymLiteralZettel,
- ast.LiteralProg: sz.SymLiteralProg,
- ast.LiteralInput: sz.SymLiteralInput,
- ast.LiteralOutput: sz.SymLiteralOutput,
- ast.LiteralComment: sz.SymLiteralComment,
- ast.LiteralHTML: sz.SymLiteralHTML,
- ast.LiteralMath: sz.SymLiteralMath,
-}
-
-var mapRegionKindS = map[ast.RegionKind]*sx.Symbol{
- ast.RegionSpan: sz.SymRegionBlock,
- ast.RegionQuote: sz.SymRegionQuote,
- ast.RegionVerse: sz.SymRegionVerse,
-}
-
-func (t *Transformer) getRegion(rn *ast.RegionNode) *sx.Pair {
- saveInVerse := t.inVerse
- if rn.Kind == ast.RegionVerse {
- t.inVerse = true
- }
- symBlocks := t.getBlockList(&rn.Blocks)
- t.inVerse = saveInVerse
- return t.getInlineList(rn.Inlines).
- Cons(symBlocks).
- Cons(getAttributes(rn.Attrs)).
- Cons(mapGetS(mapRegionKindS, rn.Kind))
-}
-
-var mapNestedListKindS = map[ast.NestedListKind]*sx.Symbol{
- ast.NestedListOrdered: sz.SymListOrdered,
- ast.NestedListUnordered: sz.SymListUnordered,
- ast.NestedListQuote: sz.SymListQuote,
-}
-
-func (t *Transformer) getNestedList(ln *ast.NestedListNode) *sx.Pair {
- nlistObjs := make(sx.Vector, len(ln.Items)+1)
- nlistObjs[0] = mapGetS(mapNestedListKindS, ln.Kind)
- isCompact := isCompactList(ln.Items)
- for i, item := range ln.Items {
- if isCompact && len(item) > 0 {
- paragraph := t.GetSz(item[0])
- nlistObjs[i+1] = paragraph.Tail().Cons(sz.SymInline)
- continue
- }
- itemObjs := make(sx.Vector, len(item))
- for j, in := range item {
- itemObjs[j] = t.GetSz(in)
- }
- if isCompact {
- nlistObjs[i+1] = sx.MakeList(itemObjs...).Cons(sz.SymInline)
- } else {
- nlistObjs[i+1] = sx.MakeList(itemObjs...).Cons(sz.SymBlock)
- }
- }
- return sx.MakeList(nlistObjs...)
-}
-func isCompactList(itemSlice []ast.ItemSlice) bool {
- for _, items := range itemSlice {
- if len(items) > 1 {
- return false
- }
- if len(items) == 1 {
- if _, ok := items[0].(*ast.ParaNode); !ok {
- return false
- }
- }
- }
- return true
-}
-
-func (t *Transformer) getDescriptionList(dn *ast.DescriptionListNode) *sx.Pair {
- dlObjs := make(sx.Vector, 2*len(dn.Descriptions)+1)
- dlObjs[0] = sz.SymDescription
- for i, def := range dn.Descriptions {
- dlObjs[2*i+1] = t.getInlineList(def.Term)
- descObjs := make(sx.Vector, len(def.Descriptions))
- for j, b := range def.Descriptions {
- dVal := make(sx.Vector, len(b))
- for k, dn := range b {
- dVal[k] = t.GetSz(dn)
- }
- descObjs[j] = sx.MakeList(dVal...).Cons(sz.SymBlock)
- }
- dlObjs[2*i+2] = sx.MakeList(descObjs...).Cons(sz.SymBlock)
- }
- return sx.MakeList(dlObjs...)
-}
-
-func (t *Transformer) getTable(tn *ast.TableNode) *sx.Pair {
- tObjs := make(sx.Vector, len(tn.Rows)+2)
- tObjs[0] = sz.SymTable
- tObjs[1] = t.getHeader(tn.Header)
- for i, row := range tn.Rows {
- tObjs[i+2] = t.getRow(row)
- }
- return sx.MakeList(tObjs...)
-}
-func (t *Transformer) getHeader(header ast.TableRow) *sx.Pair {
- if len(header) == 0 {
- return nil
- }
- return t.getRow(header)
-}
-func (t *Transformer) getRow(row ast.TableRow) *sx.Pair {
- rObjs := make(sx.Vector, len(row))
- for i, cell := range row {
- rObjs[i] = t.getCell(cell)
- }
- return sx.MakeList(rObjs...)
-}
-
-var alignmentSymbolS = map[ast.Alignment]*sx.Symbol{
- ast.AlignDefault: sz.SymCell,
- ast.AlignLeft: sz.SymCellLeft,
- ast.AlignCenter: sz.SymCellCenter,
- ast.AlignRight: sz.SymCellRight,
-}
-
-func (t *Transformer) getCell(cell *ast.TableCell) *sx.Pair {
- return t.getInlineList(cell.Inlines).Cons(mapGetS(alignmentSymbolS, cell.Align))
-}
-
-func (t *Transformer) getBLOB(bn *ast.BLOBNode) *sx.Pair {
- var lastObj sx.Object
- if bn.Syntax == meta.SyntaxSVG {
- lastObj = sx.MakeString(string(bn.Blob))
- } else {
- lastObj = getBase64String(bn.Blob)
- }
- return sx.MakeList(
- sz.SymBLOB,
- t.getInlineList(bn.Description),
- sx.MakeString(bn.Syntax),
- lastObj,
- )
-}
-
-var mapRefStateLink = map[ast.RefState]*sx.Symbol{
- ast.RefStateInvalid: sz.SymLinkInvalid,
- ast.RefStateZettel: sz.SymLinkZettel,
- ast.RefStateSelf: sz.SymLinkSelf,
- ast.RefStateFound: sz.SymLinkFound,
- ast.RefStateBroken: sz.SymLinkBroken,
- ast.RefStateHosted: sz.SymLinkHosted,
- ast.RefStateBased: sz.SymLinkBased,
- ast.RefStateQuery: sz.SymLinkQuery,
- ast.RefStateExternal: sz.SymLinkExternal,
-}
-
-func (t *Transformer) getLink(ln *ast.LinkNode) *sx.Pair {
- return t.getInlineList(ln.Inlines).
- Cons(sx.MakeString(ln.Ref.Value)).
- Cons(getAttributes(ln.Attrs)).
- Cons(mapGetS(mapRefStateLink, ln.Ref.State))
-}
-
-func (t *Transformer) getEmbedBLOB(en *ast.EmbedBLOBNode) *sx.Pair {
- tail := t.getInlineList(en.Inlines)
- if en.Syntax == meta.SyntaxSVG {
- tail = tail.Cons(sx.MakeString(string(en.Blob)))
- } else {
- tail = tail.Cons(getBase64String(en.Blob))
- }
- return tail.Cons(sx.MakeString(en.Syntax)).Cons(getAttributes(en.Attrs)).Cons(sz.SymEmbedBLOB)
-}
-
-func (t *Transformer) getBlockList(bs *ast.BlockSlice) *sx.Pair {
- objs := make(sx.Vector, len(*bs))
- for i, n := range *bs {
- objs[i] = t.GetSz(n)
- }
- return sx.MakeList(objs...)
-}
-func (t *Transformer) getInlineList(is ast.InlineSlice) *sx.Pair {
- objs := make(sx.Vector, len(is))
- for i, n := range is {
- objs[i] = t.GetSz(n)
- }
- return sx.MakeList(objs...)
-}
-
-func getAttributes(a attrs.Attributes) sx.Object {
- if a.IsEmpty() {
- return sx.Nil()
- }
- keys := a.Keys()
- objs := make(sx.Vector, 0, len(keys))
- for _, k := range keys {
- objs = append(objs, sx.Cons(sx.MakeString(k), sx.MakeString(a[k])))
- }
- return sx.MakeList(objs...)
-}
-
-var mapRefStateS = map[ast.RefState]*sx.Symbol{
- ast.RefStateInvalid: sz.SymRefStateInvalid,
- ast.RefStateZettel: sz.SymRefStateZettel,
- ast.RefStateSelf: sz.SymRefStateSelf,
- ast.RefStateFound: sz.SymRefStateFound,
- ast.RefStateBroken: sz.SymRefStateBroken,
- ast.RefStateHosted: sz.SymRefStateHosted,
- ast.RefStateBased: sz.SymRefStateBased,
- ast.RefStateQuery: sz.SymRefStateQuery,
- ast.RefStateExternal: sz.SymRefStateExternal,
-}
-
-func getReference(ref *ast.Reference) *sx.Pair {
- return sx.MakeList(mapGetS(mapRefStateS, ref.State), sx.MakeString(ref.Value))
-}
-
-var mapMetaTypeS = map[*meta.DescriptionType]*sx.Symbol{
- meta.TypeCredential: sz.SymTypeCredential,
- meta.TypeEmpty: sz.SymTypeEmpty,
- meta.TypeID: sz.SymTypeID,
- meta.TypeIDSet: sz.SymTypeIDSet,
- meta.TypeNumber: sz.SymTypeNumber,
- meta.TypeString: sz.SymTypeString,
- meta.TypeTagSet: sz.SymTypeTagSet,
- meta.TypeTimestamp: sz.SymTypeTimestamp,
- meta.TypeURL: sz.SymTypeURL,
- meta.TypeWord: sz.SymTypeWord,
- meta.TypeZettelmarkup: sz.SymTypeZettelmarkup,
-}
-
-func (t *Transformer) GetMeta(m *meta.Meta, evalMeta encoder.EvalMetaFunc) *sx.Pair {
- pairs := m.ComputedPairs()
- objs := make(sx.Vector, 0, len(pairs))
- for _, p := range pairs {
- key := p.Key
- ty := m.Type(key)
- symType := mapGetS(mapMetaTypeS, ty)
- var obj sx.Object
- if ty.IsSet {
- setList := meta.ListFromValue(p.Value)
- setObjs := make(sx.Vector, len(setList))
- for i, val := range setList {
- setObjs[i] = sx.MakeString(val)
- }
- obj = sx.MakeList(setObjs...)
- } else if ty == meta.TypeZettelmarkup {
- is := evalMeta(p.Value)
- obj = t.getInlineList(is)
- } else {
- obj = sx.MakeString(p.Value)
- }
- objs = append(objs, sx.Nil().Cons(obj).Cons(sx.MakeSymbol(key)).Cons(symType))
- }
- return sx.MakeList(objs...).Cons(sz.SymMeta)
-}
-
-func mapGetS[T comparable](m map[T]*sx.Symbol, k T) *sx.Symbol {
- if result, found := m[k]; found {
- return result
- }
- return sx.MakeSymbol(fmt.Sprintf("**%v:NOT-FOUND**", k))
-}
-
-func getBase64String(data []byte) sx.String {
- var sb strings.Builder
- encoder := base64.NewEncoder(base64.StdEncoding, &sb)
- _, err := encoder.Write(data)
- if err == nil {
- err = encoder.Close()
- }
- if err == nil {
- return sx.MakeString(sb.String())
- }
- return sx.MakeString("")
-}
Index: encoder/textenc/textenc.go
==================================================================
--- encoder/textenc/textenc.go
+++ encoder/textenc/textenc.go
@@ -1,238 +1,282 @@
//-----------------------------------------------------------------------------
-// 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 textenc encodes the abstract syntax tree into its text.
package textenc
import (
"io"
- "t73f.de/r/zsc/api"
"zettelstore.de/z/ast"
+ "zettelstore.de/z/domain/meta"
"zettelstore.de/z/encoder"
- "zettelstore.de/z/zettel/meta"
+ "zettelstore.de/z/parser"
)
func init() {
- encoder.Register(api.EncoderText, func(*encoder.CreateParameter) encoder.Encoder { return Create() })
+ encoder.Register("text", encoder.Info{
+ Create: func(*encoder.Environment) encoder.Encoder { return &textEncoder{} },
+ })
}
-// Create an encoder.
-func Create() *Encoder { return &myTE }
-
-type Encoder struct{}
-
-var myTE Encoder // Only a singleton is required.
+type textEncoder struct{}
// WriteZettel writes metadata and content.
-func (te *Encoder) WriteZettel(w io.Writer, zn *ast.ZettelNode, evalMeta encoder.EvalMetaFunc) (int, error) {
+func (te *textEncoder) WriteZettel(w io.Writer, zn *ast.ZettelNode, inhMeta bool) (int, error) {
v := newVisitor(w)
- te.WriteMeta(&v.b, zn.InhMeta, evalMeta)
- v.visitBlockSlice(&zn.Ast)
+ if inhMeta {
+ te.WriteMeta(&v.b, zn.InhMeta)
+ } else {
+ te.WriteMeta(&v.b, zn.Meta)
+ }
+ v.acceptBlockSlice(zn.Ast)
length, err := v.b.Flush()
return length, err
}
// WriteMeta encodes metadata as text.
-func (te *Encoder) WriteMeta(w io.Writer, m *meta.Meta, evalMeta encoder.EvalMetaFunc) (int, error) {
- buf := encoder.NewEncWriter(w)
- for _, pair := range m.ComputedPairs() {
+func (te *textEncoder) WriteMeta(w io.Writer, m *meta.Meta) (int, error) {
+ b := encoder.NewBufWriter(w)
+ for _, pair := range m.Pairs(true) {
switch meta.Type(pair.Key) {
+ case meta.TypeBool:
+ if meta.BoolValue(pair.Value) {
+ b.WriteString("true")
+ } else {
+ b.WriteString("false")
+ }
case meta.TypeTagSet:
- writeTagSet(&buf, meta.ListFromValue(pair.Value))
+ for i, tag := range meta.ListFromValue(pair.Value) {
+ if i > 0 {
+ b.WriteByte(' ')
+ }
+ b.WriteString(meta.CleanTag(tag))
+ }
case meta.TypeZettelmarkup:
- is := evalMeta(pair.Value)
- te.WriteInlines(&buf, &is)
+ te.WriteInlines(w, parser.ParseMetadata(pair.Value))
default:
- buf.WriteString(pair.Value)
+ b.WriteString(pair.Value)
}
- buf.WriteByte('\n')
+ b.WriteByte('\n')
}
- length, err := buf.Flush()
+ length, err := b.Flush()
return length, err
}
-func writeTagSet(buf *encoder.EncWriter, tags []string) {
- for i, tag := range tags {
- if i > 0 {
- buf.WriteByte(' ')
- }
- buf.WriteString(meta.CleanTag(tag))
- }
-
-}
-
-func (te *Encoder) WriteContent(w io.Writer, zn *ast.ZettelNode) (int, error) {
- return te.WriteBlocks(w, &zn.Ast)
+func (te *textEncoder) WriteContent(w io.Writer, zn *ast.ZettelNode) (int, error) {
+ return te.WriteBlocks(w, zn.Ast)
}
// WriteBlocks writes the content of a block slice to the writer.
-func (*Encoder) WriteBlocks(w io.Writer, bs *ast.BlockSlice) (int, error) {
+func (te *textEncoder) WriteBlocks(w io.Writer, bs ast.BlockSlice) (int, error) {
v := newVisitor(w)
- v.visitBlockSlice(bs)
+ v.acceptBlockSlice(bs)
length, err := v.b.Flush()
return length, err
}
// WriteInlines writes an inline slice to the writer
-func (*Encoder) WriteInlines(w io.Writer, is *ast.InlineSlice) (int, error) {
+func (te *textEncoder) WriteInlines(w io.Writer, is ast.InlineSlice) (int, error) {
v := newVisitor(w)
- ast.Walk(v, is)
+ v.acceptInlineSlice(is)
length, err := v.b.Flush()
return length, err
}
// visitor writes the abstract syntax tree to an io.Writer.
type visitor struct {
- b encoder.EncWriter
- inlinePos int
+ b encoder.BufWriter
}
func newVisitor(w io.Writer) *visitor {
- return &visitor{b: encoder.NewEncWriter(w)}
-}
-
-func (v *visitor) Visit(node ast.Node) ast.Visitor {
- switch n := node.(type) {
- case *ast.BlockSlice:
- v.visitBlockSlice(n)
- return nil
- case *ast.InlineSlice:
- v.visitInlineSlice(n)
- return nil
- case *ast.VerbatimNode:
- v.visitVerbatim(n)
- return nil
- case *ast.RegionNode:
- v.visitBlockSlice(&n.Blocks)
- if len(n.Inlines) > 0 {
- v.b.WriteByte('\n')
- ast.Walk(v, &n.Inlines)
- }
- return nil
- case *ast.NestedListNode:
- v.visitNestedList(n)
- return nil
- case *ast.DescriptionListNode:
- v.visitDescriptionList(n)
- return nil
- case *ast.TableNode:
- v.visitTable(n)
- return nil
- case *ast.TranscludeNode, *ast.BLOBNode:
- return nil
- case *ast.TextNode:
- v.b.WriteString(n.Text)
- return nil
- case *ast.SpaceNode:
- v.b.WriteByte(' ')
- return nil
- case *ast.BreakNode:
- if n.Hard {
- v.b.WriteByte('\n')
- } else {
- v.b.WriteByte(' ')
- }
- return nil
- case *ast.LinkNode:
- if len(n.Inlines) > 0 {
- ast.Walk(v, &n.Inlines)
- }
- return nil
- case *ast.MarkNode:
- if len(n.Inlines) > 0 {
- ast.Walk(v, &n.Inlines)
- }
- return nil
- case *ast.FootnoteNode:
- if v.inlinePos > 0 {
- v.b.WriteByte(' ')
- }
- // No 'return nil' to write text
- case *ast.LiteralNode:
- if n.Kind != ast.LiteralComment {
- v.b.Write(n.Content)
- }
- }
- return v
-}
-
-func (v *visitor) visitVerbatim(vn *ast.VerbatimNode) {
- if vn.Kind == ast.VerbatimComment {
+ return &visitor{b: encoder.NewBufWriter(w)}
+}
+
+// VisitPara emits text code for a paragraph
+func (v *visitor) VisitPara(pn *ast.ParaNode) {
+ v.acceptInlineSlice(pn.Inlines)
+}
+
+// VisitVerbatim emits text for verbatim lines.
+func (v *visitor) VisitVerbatim(vn *ast.VerbatimNode) {
+ if vn.Code == ast.VerbatimComment {
return
}
- v.b.Write(vn.Content)
+ for i, line := range vn.Lines {
+ if i > 0 {
+ v.b.WriteByte('\n')
+ }
+ v.b.WriteString(line)
+ }
+}
+
+// VisitRegion writes text code for block regions.
+func (v *visitor) VisitRegion(rn *ast.RegionNode) {
+ v.acceptBlockSlice(rn.Blocks)
+ if len(rn.Inlines) > 0 {
+ v.b.WriteByte('\n')
+ v.acceptInlineSlice(rn.Inlines)
+ }
+}
+
+// VisitHeading writes the text code for a heading.
+func (v *visitor) VisitHeading(hn *ast.HeadingNode) {
+ v.acceptInlineSlice(hn.Inlines)
}
+
+// VisitHRule writes nothing for a horizontal rule.
+func (v *visitor) VisitHRule(hn *ast.HRuleNode) {}
-func (v *visitor) visitNestedList(ln *ast.NestedListNode) {
+// VisitNestedList writes text code for lists and blockquotes.
+func (v *visitor) VisitNestedList(ln *ast.NestedListNode) {
for i, item := range ln.Items {
- v.writePosChar(i, '\n')
- for j, it := range item {
- v.writePosChar(j, '\n')
- ast.Walk(v, it)
+ if i > 0 {
+ v.b.WriteByte('\n')
}
+ v.acceptItemSlice(item)
}
}
-func (v *visitor) visitDescriptionList(dl *ast.DescriptionListNode) {
- for i, descr := range dl.Descriptions {
- v.writePosChar(i, '\n')
- ast.Walk(v, &descr.Term)
+// VisitDescriptionList emits a text for a description list.
+func (v *visitor) VisitDescriptionList(dn *ast.DescriptionListNode) {
+ for i, descr := range dn.Descriptions {
+ if i > 0 {
+ v.b.WriteByte('\n')
+ }
+ v.acceptInlineSlice(descr.Term)
+
for _, b := range descr.Descriptions {
v.b.WriteByte('\n')
- for k, d := range b {
- v.writePosChar(k, '\n')
- ast.Walk(v, d)
- }
+ v.acceptDescriptionSlice(b)
}
}
}
-func (v *visitor) visitTable(tn *ast.TableNode) {
+// VisitTable emits a text table.
+func (v *visitor) VisitTable(tn *ast.TableNode) {
if len(tn.Header) > 0 {
- v.writeRow(tn.Header)
+ for i, cell := range tn.Header {
+ if i > 0 {
+ v.b.WriteByte(' ')
+ }
+ v.acceptInlineSlice(cell.Inlines)
+ }
v.b.WriteByte('\n')
}
for i, row := range tn.Rows {
- v.writePosChar(i, '\n')
- v.writeRow(row)
- }
-}
-
-func (v *visitor) writeRow(row ast.TableRow) {
- for i, cell := range row {
- v.writePosChar(i, ' ')
- ast.Walk(v, &cell.Inlines)
- }
-}
-
-func (v *visitor) visitBlockSlice(bs *ast.BlockSlice) {
- for i, bn := range *bs {
- v.writePosChar(i, '\n')
- ast.Walk(v, bn)
- }
-}
-
-func (v *visitor) visitInlineSlice(is *ast.InlineSlice) {
- for i, in := range *is {
- v.inlinePos = i
- ast.Walk(v, in)
- }
- v.inlinePos = 0
-}
-
-func (v *visitor) writePosChar(pos int, ch byte) {
- if pos > 0 {
- v.b.WriteByte(ch)
+ if i > 0 {
+ v.b.WriteByte('\n')
+ }
+ for j, cell := range row {
+ if j > 0 {
+ v.b.WriteByte(' ')
+ }
+ v.acceptInlineSlice(cell.Inlines)
+ }
+ }
+}
+
+// VisitBLOB writes nothing, because it contains no text.
+func (v *visitor) VisitBLOB(bn *ast.BLOBNode) {}
+
+// VisitText writes text content.
+func (v *visitor) VisitText(tn *ast.TextNode) {
+ v.b.WriteString(tn.Text)
+}
+
+// VisitTag writes tag content.
+func (v *visitor) VisitTag(tn *ast.TagNode) {
+ v.b.WriteStrings("#", tn.Tag)
+}
+
+// VisitSpace emits a white space.
+func (v *visitor) VisitSpace(sn *ast.SpaceNode) {
+ v.b.WriteByte(' ')
+}
+
+// VisitBreak writes text code for line breaks.
+func (v *visitor) VisitBreak(bn *ast.BreakNode) {
+ if bn.Hard {
+ v.b.WriteByte('\n')
+ } else {
+ v.b.WriteByte(' ')
+ }
+}
+
+// VisitLink writes text code for links.
+func (v *visitor) VisitLink(ln *ast.LinkNode) {
+ if !ln.OnlyRef {
+ v.acceptInlineSlice(ln.Inlines)
+ }
+}
+
+// VisitImage writes text code for images.
+func (v *visitor) VisitImage(in *ast.ImageNode) {
+ v.acceptInlineSlice(in.Inlines)
+}
+
+// VisitCite writes code for citations.
+func (v *visitor) VisitCite(cn *ast.CiteNode) {
+ v.acceptInlineSlice(cn.Inlines)
+}
+
+// VisitFootnote write text code for a footnote.
+func (v *visitor) VisitFootnote(fn *ast.FootnoteNode) {
+ v.b.WriteByte(' ')
+ v.acceptInlineSlice(fn.Inlines)
+}
+
+// VisitMark writes nothing for a mark.
+func (v *visitor) VisitMark(mn *ast.MarkNode) {}
+
+// VisitFormat write text code for formatting text.
+func (v *visitor) VisitFormat(fn *ast.FormatNode) {
+ v.acceptInlineSlice(fn.Inlines)
+}
+
+// VisitLiteral write text code for literal inline text.
+func (v *visitor) VisitLiteral(ln *ast.LiteralNode) {
+ if ln.Code != ast.LiteralComment {
+ v.b.WriteString(ln.Text)
+ }
+}
+
+// VisitAttributes never writes any attribute data.
+func (v *visitor) VisitAttributes(a *ast.Attributes) {}
+
+func (v *visitor) acceptBlockSlice(bns ast.BlockSlice) {
+ for i, bn := range bns {
+ if i > 0 {
+ v.b.WriteByte('\n')
+ }
+ bn.Accept(v)
+ }
+}
+func (v *visitor) acceptItemSlice(ins ast.ItemSlice) {
+ for i, in := range ins {
+ if i > 0 {
+ v.b.WriteByte('\n')
+ }
+ in.Accept(v)
+ }
+}
+func (v *visitor) acceptDescriptionSlice(dns ast.DescriptionSlice) {
+ for i, dn := range dns {
+ if i > 0 {
+ v.b.WriteByte('\n')
+ }
+ dn.Accept(v)
+ }
+}
+func (v *visitor) acceptInlineSlice(ins ast.InlineSlice) {
+ for _, in := range ins {
+ in.Accept(v)
}
}
DELETED encoder/write.go
Index: encoder/write.go
==================================================================
--- encoder/write.go
+++ encoder/write.go
@@ -1,88 +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 encoder
-
-import (
- "encoding/base64"
- "io"
-)
-
-// EncWriter is a specialized writer for encoding zettel.
-type EncWriter struct {
- w io.Writer // The io.Writer to write to
- err error // Collect error
- length int // Collected length
-}
-
-// NewEncWriter creates a new EncWriter
-func NewEncWriter(w io.Writer) EncWriter {
- return EncWriter{w: w}
-}
-
-// Write writes the content of p.
-func (w *EncWriter) Write(p []byte) (l int, err error) {
- if w.err != nil {
- return 0, w.err
- }
- l, w.err = w.w.Write(p)
- w.length += l
- return l, w.err
-}
-
-// WriteString writes the content of s.
-func (w *EncWriter) WriteString(s string) {
- if w.err != nil {
- return
- }
- var l int
- l, w.err = io.WriteString(w.w, s)
- w.length += l
-}
-
-// WriteStrings writes the content of sl.
-func (w *EncWriter) WriteStrings(sl ...string) {
- for _, s := range sl {
- w.WriteString(s)
- }
-}
-
-// WriteByte writes the content of b.
-func (w *EncWriter) WriteByte(b byte) error {
- var l int
- l, w.err = w.Write([]byte{b})
- w.length += l
- return w.err
-}
-
-// WriteBytes writes the content of bs.
-func (w *EncWriter) WriteBytes(bs ...byte) {
- w.Write(bs)
-}
-
-// WriteBase64 writes the content of p, encoded with base64.
-func (w *EncWriter) WriteBase64(p []byte) {
- if w.err == nil {
- encoder := base64.NewEncoder(base64.StdEncoding, w.w)
- var l int
- l, w.err = encoder.Write(p)
- w.length += l
- err1 := encoder.Close()
- if w.err == nil {
- w.err = err1
- }
- }
-}
-
-// Flush returns the collected length and error.
-func (w *EncWriter) Flush() (int, error) { return w.length, w.err }
Index: encoder/zmkenc/zmkenc.go
==================================================================
--- encoder/zmkenc/zmkenc.go
+++ encoder/zmkenc/zmkenc.go
@@ -1,305 +1,194 @@
//-----------------------------------------------------------------------------
-// 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 zmkenc encodes the abstract syntax tree back into Zettelmarkup.
package zmkenc
import (
"fmt"
"io"
- "strings"
+ "sort"
- "t73f.de/r/zsc/api"
- "t73f.de/r/zsc/attrs"
"zettelstore.de/z/ast"
+ "zettelstore.de/z/domain/meta"
"zettelstore.de/z/encoder"
- "zettelstore.de/z/encoder/textenc"
- "zettelstore.de/z/strfun"
- "zettelstore.de/z/zettel/meta"
)
func init() {
- encoder.Register(api.EncoderZmk, func(*encoder.CreateParameter) encoder.Encoder { return Create() })
+ encoder.Register("zmk", encoder.Info{
+ Create: func(*encoder.Environment) encoder.Encoder { return &zmkEncoder{} },
+ })
}
-// Create an encoder.
-func Create() *Encoder { return &myZE }
-
-type Encoder struct{}
-
-var myZE Encoder
+type zmkEncoder struct{}
// WriteZettel writes the encoded zettel to the writer.
-func (*Encoder) WriteZettel(w io.Writer, zn *ast.ZettelNode, evalMeta encoder.EvalMetaFunc) (int, error) {
- v := newVisitor(w)
- v.acceptMeta(zn.InhMeta, evalMeta)
- if zn.InhMeta.YamlSep {
- v.b.WriteString("---\n")
+func (ze *zmkEncoder) WriteZettel(
+ w io.Writer, zn *ast.ZettelNode, inhMeta bool) (int, error) {
+ v := newVisitor(w, ze)
+ if inhMeta {
+ zn.InhMeta.WriteAsHeader(&v.b, true)
} else {
- v.b.WriteByte('\n')
+ zn.Meta.WriteAsHeader(&v.b, true)
}
- ast.Walk(v, &zn.Ast)
+ v.acceptBlockSlice(zn.Ast)
length, err := v.b.Flush()
return length, err
}
// WriteMeta encodes meta data as zmk.
-func (*Encoder) WriteMeta(w io.Writer, m *meta.Meta, evalMeta encoder.EvalMetaFunc) (int, error) {
- v := newVisitor(w)
- v.acceptMeta(m, evalMeta)
- length, err := v.b.Flush()
- return length, err
-}
-
-func (v *visitor) acceptMeta(m *meta.Meta, evalMeta encoder.EvalMetaFunc) {
- for _, p := range m.ComputedPairs() {
- key := p.Key
- v.b.WriteStrings(key, ": ")
- if meta.Type(key) == meta.TypeZettelmarkup {
- is := evalMeta(p.Value)
- ast.Walk(v, &is)
- } else {
- v.b.WriteString(p.Value)
- }
- v.b.WriteByte('\n')
- }
-}
-
-func (ze *Encoder) WriteContent(w io.Writer, zn *ast.ZettelNode) (int, error) {
- return ze.WriteBlocks(w, &zn.Ast)
+func (ze *zmkEncoder) WriteMeta(w io.Writer, m *meta.Meta) (int, error) {
+ return m.Write(w, true)
+}
+
+func (ze *zmkEncoder) WriteContent(w io.Writer, zn *ast.ZettelNode) (int, error) {
+ return ze.WriteBlocks(w, zn.Ast)
}
// WriteBlocks writes the content of a block slice to the writer.
-func (*Encoder) WriteBlocks(w io.Writer, bs *ast.BlockSlice) (int, error) {
- v := newVisitor(w)
- ast.Walk(v, bs)
+func (ze *zmkEncoder) WriteBlocks(w io.Writer, bs ast.BlockSlice) (int, error) {
+ v := newVisitor(w, ze)
+ v.acceptBlockSlice(bs)
length, err := v.b.Flush()
return length, err
}
// WriteInlines writes an inline slice to the writer
-func (*Encoder) WriteInlines(w io.Writer, is *ast.InlineSlice) (int, error) {
- v := newVisitor(w)
- ast.Walk(v, is)
+func (ze *zmkEncoder) WriteInlines(w io.Writer, is ast.InlineSlice) (int, error) {
+ v := newVisitor(w, ze)
+ v.acceptInlineSlice(is)
length, err := v.b.Flush()
return length, err
}
// visitor writes the abstract syntax tree to an io.Writer.
type visitor struct {
- b encoder.EncWriter
- textEnc encoder.Encoder
- prefix []byte
- inVerse bool
- inlinePos int
-}
-
-func newVisitor(w io.Writer) *visitor {
- return &visitor{b: encoder.NewEncWriter(w), textEnc: textenc.Create()}
-}
-
-func (v *visitor) Visit(node ast.Node) ast.Visitor {
- switch n := node.(type) {
- case *ast.BlockSlice:
- v.visitBlockSlice(n)
- case *ast.InlineSlice:
- for i, in := range *n {
- v.inlinePos = i
- ast.Walk(v, in)
- }
- v.inlinePos = 0
- case *ast.VerbatimNode:
- v.visitVerbatim(n)
- case *ast.RegionNode:
- v.visitRegion(n)
- case *ast.HeadingNode:
- v.visitHeading(n)
- case *ast.HRuleNode:
- v.b.WriteString("---")
- v.visitAttributes(n.Attrs)
- case *ast.NestedListNode:
- v.visitNestedList(n)
- case *ast.DescriptionListNode:
- v.visitDescriptionList(n)
- case *ast.TableNode:
- v.visitTable(n)
- case *ast.TranscludeNode:
- v.b.WriteStrings("{{{", n.Ref.String(), "}}}")
- v.visitAttributes(n.Attrs)
- case *ast.BLOBNode:
- v.visitBLOB(n)
- case *ast.TextNode:
- v.visitText(n)
- case *ast.SpaceNode:
- v.b.WriteString(n.Lexeme)
- case *ast.BreakNode:
- v.visitBreak(n)
- case *ast.LinkNode:
- v.visitLink(n)
- case *ast.EmbedRefNode:
- v.visitEmbedRef(n)
- case *ast.EmbedBLOBNode:
- v.visitEmbedBLOB(n)
- case *ast.CiteNode:
- v.visitCite(n)
- case *ast.FootnoteNode:
- v.b.WriteString("[^")
- ast.Walk(v, &n.Inlines)
- v.b.WriteByte(']')
- v.visitAttributes(n.Attrs)
- case *ast.MarkNode:
- v.visitMark(n)
- case *ast.FormatNode:
- v.visitFormat(n)
- case *ast.LiteralNode:
- v.visitLiteral(n)
- default:
- return v
- }
- return nil
-}
-
-func (v *visitor) visitBlockSlice(bs *ast.BlockSlice) {
- var lastWasParagraph bool
- for i, bn := range *bs {
- if i > 0 {
- v.b.WriteByte('\n')
- if lastWasParagraph && !v.inVerse {
- if _, ok := bn.(*ast.ParaNode); ok {
- v.b.WriteByte('\n')
- }
- }
- }
- ast.Walk(v, bn)
- _, lastWasParagraph = bn.(*ast.ParaNode)
- }
-}
-
-var mapVerbatimKind = map[ast.VerbatimKind]string{
- ast.VerbatimZettel: "@@@",
- ast.VerbatimComment: "%%%",
- ast.VerbatimHTML: "@@@", // Attribute is set to {="html"}
- ast.VerbatimProg: "```",
- ast.VerbatimEval: "~~~",
- ast.VerbatimMath: "$$$",
-}
-
-func (v *visitor) visitVerbatim(vn *ast.VerbatimNode) {
- kind, ok := mapVerbatimKind[vn.Kind]
- if !ok {
- panic(fmt.Sprintf("Unknown verbatim kind %d", vn.Kind))
- }
- attrs := vn.Attrs
- if vn.Kind == ast.VerbatimHTML {
- attrs = syntaxToHTML(attrs)
- }
-
- // TODO: scan cn.Lines to find embedded kind[0]s at beginning
- v.b.WriteString(kind)
- v.visitAttributes(attrs)
- v.b.WriteByte('\n')
- v.b.Write(vn.Content)
- v.b.WriteByte('\n')
- v.b.WriteString(kind)
-}
-
-var mapRegionKind = map[ast.RegionKind]string{
+ b encoder.BufWriter
+ prefix []byte
+ enc *zmkEncoder
+}
+
+func newVisitor(w io.Writer, enc *zmkEncoder) *visitor {
+ return &visitor{
+ b: encoder.NewBufWriter(w),
+ enc: enc,
+ }
+}
+
+// VisitPara emits HTML code for a paragraph: ...
+func (v *visitor) VisitPara(pn *ast.ParaNode) {
+ v.acceptInlineSlice(pn.Inlines)
+ v.b.WriteByte('\n')
+ if len(v.prefix) == 0 {
+ v.b.WriteByte('\n')
+ }
+}
+
+// VisitVerbatim emits HTML code for verbatim lines.
+func (v *visitor) VisitVerbatim(vn *ast.VerbatimNode) {
+ // TODO: scan cn.Lines to find embedded "`"s at beginning
+ v.b.WriteString("```")
+ v.visitAttributes(vn.Attrs)
+ v.b.WriteByte('\n')
+ for _, line := range vn.Lines {
+ v.b.WriteStrings(line, "\n")
+ }
+ v.b.WriteString("```\n")
+}
+
+var regionCode = map[ast.RegionCode]string{
ast.RegionSpan: ":::",
ast.RegionQuote: "<<<",
ast.RegionVerse: "\"\"\"",
}
-func (v *visitor) visitRegion(rn *ast.RegionNode) {
+// VisitRegion writes HTML code for block regions.
+func (v *visitor) VisitRegion(rn *ast.RegionNode) {
// Scan rn.Blocks for embedded regions to adjust length of regionCode
- kind, ok := mapRegionKind[rn.Kind]
+ code, ok := regionCode[rn.Code]
if !ok {
- panic(fmt.Sprintf("Unknown region kind %d", rn.Kind))
+ panic(fmt.Sprintf("Unknown region code %d", rn.Code))
}
- v.b.WriteString(kind)
+ v.b.WriteString(code)
v.visitAttributes(rn.Attrs)
v.b.WriteByte('\n')
- saveInVerse := v.inVerse
- v.inVerse = rn.Kind == ast.RegionVerse
- ast.Walk(v, &rn.Blocks)
- v.inVerse = saveInVerse
- v.b.WriteByte('\n')
- v.b.WriteString(kind)
+ v.acceptBlockSlice(rn.Blocks)
+ v.b.WriteString(code)
if len(rn.Inlines) > 0 {
v.b.WriteByte(' ')
- ast.Walk(v, &rn.Inlines)
+ v.acceptInlineSlice(rn.Inlines)
+ }
+ v.b.WriteByte('\n')
+}
+
+// VisitHeading writes the HTML code for a heading.
+func (v *visitor) VisitHeading(hn *ast.HeadingNode) {
+ for i := 0; i <= hn.Level; i++ {
+ v.b.WriteByte('=')
}
+ v.b.WriteByte(' ')
+ v.acceptInlineSlice(hn.Inlines)
+ v.visitAttributes(hn.Attrs)
+ v.b.WriteByte('\n')
}
-func (v *visitor) visitHeading(hn *ast.HeadingNode) {
- const headingSigns = "========= "
- v.b.WriteString(headingSigns[len(headingSigns)-hn.Level-3:])
- ast.Walk(v, &hn.Inlines)
+// VisitHRule writes HTML code for a horizontal rule:
.
+func (v *visitor) VisitHRule(hn *ast.HRuleNode) {
+ v.b.WriteString("---")
v.visitAttributes(hn.Attrs)
+ v.b.WriteByte('\n')
}
-var mapNestedListKind = map[ast.NestedListKind]byte{
+var listCode = map[ast.NestedListCode]byte{
ast.NestedListOrdered: '#',
ast.NestedListUnordered: '*',
ast.NestedListQuote: '>',
}
-func (v *visitor) visitNestedList(ln *ast.NestedListNode) {
- v.prefix = append(v.prefix, mapNestedListKind[ln.Kind])
- for i, item := range ln.Items {
- if i > 0 {
- v.b.WriteByte('\n')
- }
+// VisitNestedList writes HTML code for lists and blockquotes.
+func (v *visitor) VisitNestedList(ln *ast.NestedListNode) {
+ v.prefix = append(v.prefix, listCode[ln.Code])
+ for _, item := range ln.Items {
v.b.Write(v.prefix)
v.b.WriteByte(' ')
- for j, in := range item {
- if j > 0 {
- v.b.WriteByte('\n')
+ for i, in := range item {
+ if i > 0 {
if _, ok := in.(*ast.ParaNode); ok {
- v.writePrefixSpaces()
+ v.b.WriteByte('\n')
+ for j := 0; j <= len(v.prefix); j++ {
+ v.b.WriteByte(' ')
+ }
}
}
- ast.Walk(v, in)
+ in.Accept(v)
}
}
v.prefix = v.prefix[:len(v.prefix)-1]
-}
-
-func (v *visitor) writePrefixSpaces() {
- if prefixLen := len(v.prefix); prefixLen > 0 {
- for i := 0; i <= prefixLen; i++ {
- v.b.WriteByte(' ')
- }
- }
-}
-
-func (v *visitor) visitDescriptionList(dn *ast.DescriptionListNode) {
- for i, descr := range dn.Descriptions {
- if i > 0 {
- v.b.WriteByte('\n')
- }
+ v.b.WriteByte('\n')
+}
+
+// VisitDescriptionList emits a HTML description list.
+func (v *visitor) VisitDescriptionList(dn *ast.DescriptionListNode) {
+ for _, descr := range dn.Descriptions {
v.b.WriteString("; ")
- ast.Walk(v, &descr.Term)
+ v.acceptInlineSlice(descr.Term)
+ v.b.WriteByte('\n')
for _, b := range descr.Descriptions {
- v.b.WriteString("\n: ")
- for jj, dn := range b {
- if jj > 0 {
- v.b.WriteString("\n\n ")
- }
- ast.Walk(v, dn)
+ v.b.WriteString(": ")
+ for _, dn := range b {
+ dn.Accept(v)
}
+ v.b.WriteByte('\n')
}
}
}
var alignCode = map[ast.Alignment]string{
@@ -307,64 +196,69 @@
ast.AlignLeft: "<",
ast.AlignCenter: ":",
ast.AlignRight: ">",
}
-func (v *visitor) visitTable(tn *ast.TableNode) {
- if header := tn.Header; len(header) > 0 {
- v.writeTableHeader(header, tn.Align)
- v.b.WriteByte('\n')
- }
- for i, row := range tn.Rows {
- if i > 0 {
- v.b.WriteByte('\n')
- }
- v.writeTableRow(row, tn.Align)
- }
-}
-
-func (v *visitor) writeTableHeader(header ast.TableRow, align []ast.Alignment) {
- for pos, cell := range header {
- v.b.WriteString("|=")
- colAlign := align[pos]
- if cell.Align != colAlign {
- v.b.WriteString(alignCode[cell.Align])
- }
- ast.Walk(v, &cell.Inlines)
- if colAlign != ast.AlignDefault {
- v.b.WriteString(alignCode[colAlign])
- }
- }
-}
-
-func (v *visitor) writeTableRow(row ast.TableRow, align []ast.Alignment) {
- for pos, cell := range row {
- v.b.WriteByte('|')
- if cell.Align != align[pos] {
- v.b.WriteString(alignCode[cell.Align])
- }
- ast.Walk(v, &cell.Inlines)
- }
-}
-
-func (v *visitor) visitBLOB(bn *ast.BLOBNode) {
- if bn.Syntax == meta.SyntaxSVG {
- v.b.WriteStrings("@@@", bn.Syntax, "\n")
- v.b.Write(bn.Blob)
- v.b.WriteString("\n@@@\n")
- return
- }
- var sb strings.Builder
- v.textEnc.WriteInlines(&sb, &bn.Description)
- v.b.WriteStrings("%% Unable to display BLOB with description '", sb.String(), "' and syntax '", bn.Syntax, "'.")
-}
-
-var escapeSeqs = strfun.NewSet(
- "\\", "__", "**", "~~", "^^", ",,", ">>", `""`, "::", "''", "``", "++", "==", "##",
-)
-
-func (v *visitor) visitText(tn *ast.TextNode) {
+// VisitTable emits a HTML table.
+func (v *visitor) VisitTable(tn *ast.TableNode) {
+ if len(tn.Header) > 0 {
+ for pos, cell := range tn.Header {
+ v.b.WriteString("|=")
+ colAlign := tn.Align[pos]
+ if cell.Align != colAlign {
+ v.b.WriteString(alignCode[cell.Align])
+ }
+ v.acceptInlineSlice(cell.Inlines)
+ if colAlign != ast.AlignDefault {
+ v.b.WriteString(alignCode[colAlign])
+ }
+ }
+ v.b.WriteByte('\n')
+ }
+ for _, row := range tn.Rows {
+ for pos, cell := range row {
+ v.b.WriteByte('|')
+ if cell.Align != tn.Align[pos] {
+ v.b.WriteString(alignCode[cell.Align])
+ }
+ v.acceptInlineSlice(cell.Inlines)
+ }
+ v.b.WriteByte('\n')
+ }
+ v.b.WriteByte('\n')
+}
+
+// VisitBLOB writes the binary object as a value.
+func (v *visitor) VisitBLOB(bn *ast.BLOBNode) {
+ v.b.WriteStrings(
+ "%% Unable to display BLOB with title '",
+ bn.Title,
+ "' and syntax '",
+ bn.Syntax,
+ "'\n")
+}
+
+var escapeSeqs = map[string]bool{
+ "\\": true,
+ "//": true,
+ "**": true,
+ "__": true,
+ "~~": true,
+ "^^": true,
+ ",,": true,
+ "<<": true,
+ "\"\"": true,
+ ";;": true,
+ "::": true,
+ "''": true,
+ "``": true,
+ "++": true,
+ "==": true,
+}
+
+// VisitText writes text content.
+func (v *visitor) VisitText(tn *ast.TextNode) {
last := 0
for i := 0; i < len(tn.Text); i++ {
if b := tn.Text[i]; b == '\\' {
v.b.WriteString(tn.Text[last:i])
v.b.WriteBytes('\\', b)
@@ -371,13 +265,13 @@
last = i + 1
continue
}
if i < len(tn.Text)-1 {
s := tn.Text[i : i+2]
- if escapeSeqs.Has(s) {
+ if _, ok := escapeSeqs[s]; ok {
v.b.WriteString(tn.Text[last:i])
- for j := range len(s) {
+ for j := 0; j < len(s); j++ {
v.b.WriteBytes('\\', s[j])
}
i++
last = i + 1
continue
@@ -385,161 +279,190 @@
}
}
v.b.WriteString(tn.Text[last:])
}
-func (v *visitor) visitBreak(bn *ast.BreakNode) {
+// VisitTag writes tag content.
+func (v *visitor) VisitTag(tn *ast.TagNode) {
+ v.b.WriteStrings("#", tn.Tag)
+}
+
+// VisitSpace emits a white space.
+func (v *visitor) VisitSpace(sn *ast.SpaceNode) {
+ v.b.WriteString(sn.Lexeme)
+}
+
+// VisitBreak writes HTML code for line breaks.
+func (v *visitor) VisitBreak(bn *ast.BreakNode) {
if bn.Hard {
v.b.WriteString("\\\n")
} else {
v.b.WriteByte('\n')
}
- v.writePrefixSpaces()
+ if prefixLen := len(v.prefix); prefixLen > 0 {
+ for i := 0; i <= prefixLen; i++ {
+ v.b.WriteByte(' ')
+ }
+ }
}
-func (v *visitor) visitLink(ln *ast.LinkNode) {
+// VisitLink writes HTML code for links.
+func (v *visitor) VisitLink(ln *ast.LinkNode) {
v.b.WriteString("[[")
- if len(ln.Inlines) > 0 {
- ast.Walk(v, &ln.Inlines)
+ if !ln.OnlyRef {
+ v.acceptInlineSlice(ln.Inlines)
v.b.WriteByte('|')
}
- if ln.Ref.State == ast.RefStateBased {
- v.b.WriteByte('/')
- }
v.b.WriteStrings(ln.Ref.String(), "]]")
}
-func (v *visitor) visitEmbedRef(en *ast.EmbedRefNode) {
- v.b.WriteString("{{")
- if len(en.Inlines) > 0 {
- ast.Walk(v, &en.Inlines)
- v.b.WriteByte('|')
- }
- v.b.WriteStrings(en.Ref.String(), "}}")
-}
-
-func (v *visitor) visitEmbedBLOB(en *ast.EmbedBLOBNode) {
- if en.Syntax == meta.SyntaxSVG {
- v.b.WriteString("@@")
- v.b.Write(en.Blob)
- v.b.WriteStrings("@@{=", en.Syntax, "}")
- return
- }
- v.b.WriteString("{{TODO: display inline BLOB}}")
-}
-
-func (v *visitor) visitCite(cn *ast.CiteNode) {
+// VisitImage writes HTML code for images.
+func (v *visitor) VisitImage(in *ast.ImageNode) {
+ if in.Ref != nil {
+ v.b.WriteString("{{")
+ if len(in.Inlines) > 0 {
+ v.acceptInlineSlice(in.Inlines)
+ v.b.WriteByte('|')
+ }
+ v.b.WriteStrings(in.Ref.String(), "}}")
+ }
+}
+
+// VisitCite writes code for citations.
+func (v *visitor) VisitCite(cn *ast.CiteNode) {
v.b.WriteStrings("[@", cn.Key)
if len(cn.Inlines) > 0 {
- v.b.WriteByte(' ')
- ast.Walk(v, &cn.Inlines)
+ v.b.WriteString(", ")
+ v.acceptInlineSlice(cn.Inlines)
}
v.b.WriteByte(']')
v.visitAttributes(cn.Attrs)
}
-func (v *visitor) visitMark(mn *ast.MarkNode) {
- v.b.WriteStrings("[!", mn.Mark)
- if len(mn.Inlines) > 0 {
- v.b.WriteByte('|')
- ast.Walk(v, &mn.Inlines)
- }
+// VisitFootnote write HTML code for a footnote.
+func (v *visitor) VisitFootnote(fn *ast.FootnoteNode) {
+ v.b.WriteString("[^")
+ v.acceptInlineSlice(fn.Inlines)
v.b.WriteByte(']')
-
-}
-
-var mapFormatKind = map[ast.FormatKind][]byte{
- ast.FormatEmph: []byte("__"),
- ast.FormatStrong: []byte("**"),
- ast.FormatInsert: []byte(">>"),
- ast.FormatDelete: []byte("~~"),
- ast.FormatSuper: []byte("^^"),
- ast.FormatSub: []byte(",,"),
- ast.FormatQuote: []byte(`""`),
- ast.FormatMark: []byte("##"),
- ast.FormatSpan: []byte("::"),
-}
-
-func (v *visitor) visitFormat(fn *ast.FormatNode) {
- kind, ok := mapFormatKind[fn.Kind]
- if !ok {
- panic(fmt.Sprintf("Unknown format kind %d", fn.Kind))
- }
- v.b.Write(kind)
- ast.Walk(v, &fn.Inlines)
- v.b.Write(kind)
v.visitAttributes(fn.Attrs)
}
-func (v *visitor) visitLiteral(ln *ast.LiteralNode) {
- switch ln.Kind {
- case ast.LiteralZettel:
- v.writeLiteral('@', ln.Attrs, ln.Content)
- case ast.LiteralProg:
- v.writeLiteral('`', ln.Attrs, ln.Content)
- case ast.LiteralMath:
- v.b.WriteStrings("$$", string(ln.Content), "$$")
- v.visitAttributes(ln.Attrs)
- case ast.LiteralInput:
- v.writeLiteral('\'', ln.Attrs, ln.Content)
- case ast.LiteralOutput:
- v.writeLiteral('=', ln.Attrs, ln.Content)
- case ast.LiteralComment:
- if v.inlinePos > 0 {
- v.b.WriteByte(' ')
- }
- v.b.WriteString("%%")
- v.visitAttributes(ln.Attrs)
- v.b.WriteByte(' ')
- v.b.Write(ln.Content)
- case ast.LiteralHTML:
- v.writeLiteral('@', syntaxToHTML(ln.Attrs), ln.Content)
- default:
- panic(fmt.Sprintf("Unknown literal kind %v", ln.Kind))
- }
-}
-
-func (v *visitor) writeLiteral(code byte, a attrs.Attributes, content []byte) {
- v.b.WriteBytes(code, code)
- v.writeEscaped(string(content), code)
- v.b.WriteBytes(code, code)
- v.visitAttributes(a)
-}
-
-// visitAttributes write HTML attributes
-func (v *visitor) visitAttributes(a attrs.Attributes) {
- if a.IsEmpty() {
- return
- }
- v.b.WriteByte('{')
- for i, k := range a.Keys() {
+// VisitMark writes HTML code to mark a position.
+func (v *visitor) VisitMark(mn *ast.MarkNode) {
+ v.b.WriteStrings("[!", mn.Text, "]")
+}
+
+var formatCode = map[ast.FormatCode][]byte{
+ ast.FormatItalic: []byte("//"),
+ ast.FormatEmph: []byte("//"),
+ ast.FormatBold: []byte("**"),
+ ast.FormatStrong: []byte("**"),
+ ast.FormatUnder: []byte("__"),
+ ast.FormatInsert: []byte("__"),
+ ast.FormatStrike: []byte("~~"),
+ ast.FormatDelete: []byte("~~"),
+ ast.FormatSuper: []byte("^^"),
+ ast.FormatSub: []byte(",,"),
+ ast.FormatQuotation: []byte("<<"),
+ ast.FormatQuote: []byte("\"\""),
+ ast.FormatSmall: []byte(";;"),
+ ast.FormatSpan: []byte("::"),
+ ast.FormatMonospace: []byte("''"),
+}
+
+// VisitFormat write HTML code for formatting text.
+func (v *visitor) VisitFormat(fn *ast.FormatNode) {
+ code, ok := formatCode[fn.Code]
+ if !ok {
+ panic(fmt.Sprintf("Unknown format code %d", fn.Code))
+ }
+ attrs := fn.Attrs
+ switch fn.Code {
+ case ast.FormatEmph, ast.FormatStrong, ast.FormatInsert, ast.FormatDelete:
+ attrs = attrs.Clone()
+ attrs.Set("-", "")
+ }
+
+ v.b.Write(code)
+ v.acceptInlineSlice(fn.Inlines)
+ v.b.Write(code)
+ v.visitAttributes(attrs)
+}
+
+// VisitLiteral write Zettelmarkup for inline literal text.
+func (v *visitor) VisitLiteral(ln *ast.LiteralNode) {
+ switch ln.Code {
+ case ast.LiteralProg:
+ v.writeLiteral('`', ln.Attrs, ln.Text)
+ case ast.LiteralKeyb:
+ v.writeLiteral('+', ln.Attrs, ln.Text)
+ case ast.LiteralOutput:
+ v.writeLiteral('=', ln.Attrs, ln.Text)
+ case ast.LiteralComment:
+ v.b.WriteStrings("%% ", ln.Text)
+ case ast.LiteralHTML:
+ v.b.WriteString("``")
+ v.writeEscaped(ln.Text, '`')
+ v.b.WriteString("``{=html,.warning}")
+ default:
+ panic(fmt.Sprintf("Unknown literal code %v", ln.Code))
+ }
+}
+
+func (v *visitor) writeLiteral(code byte, attrs *ast.Attributes, text string) {
+ v.b.WriteBytes(code, code)
+ v.writeEscaped(text, code)
+ v.b.WriteBytes(code, code)
+ v.visitAttributes(attrs)
+}
+
+func (v *visitor) acceptBlockSlice(bns ast.BlockSlice) {
+ for _, bn := range bns {
+ bn.Accept(v)
+ }
+}
+func (v *visitor) acceptInlineSlice(ins ast.InlineSlice) {
+ for _, in := range ins {
+ in.Accept(v)
+ }
+}
+
+// visitAttributes write HTML attributes
+func (v *visitor) visitAttributes(a *ast.Attributes) {
+ if a == nil || len(a.Attrs) == 0 {
+ return
+ }
+ keys := make([]string, 0, len(a.Attrs))
+ for k := range a.Attrs {
+ keys = append(keys, k)
+ }
+ sort.Strings(keys)
+
+ v.b.WriteByte('{')
+ for i, k := range keys {
if i > 0 {
v.b.WriteByte(' ')
}
if k == "-" {
v.b.WriteByte('-')
continue
}
v.b.WriteString(k)
- if vl := a[k]; len(vl) > 0 {
+ if vl := a.Attrs[k]; len(vl) > 0 {
v.b.WriteStrings("=\"", vl)
v.b.WriteByte('"')
}
}
v.b.WriteByte('}')
}
func (v *visitor) writeEscaped(s string, toEscape byte) {
last := 0
- for i := range len(s) {
+ for i := 0; i < len(s); i++ {
if b := s[i]; b == toEscape || b == '\\' {
v.b.WriteString(s[last:i])
v.b.WriteBytes('\\', b)
last = i + 1
}
}
v.b.WriteString(s[last:])
}
-
-func syntaxToHTML(a attrs.Attributes) attrs.Attributes {
- return a.Clone().Set("", meta.SyntaxHTML).Remove(api.KeySyntax)
-}
DELETED encoding/atom/atom.go
Index: encoding/atom/atom.go
==================================================================
--- encoding/atom/atom.go
+++ encoding/atom/atom.go
@@ -1,119 +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 atom provides an Atom encoding.
-package atom
-
-import (
- "bytes"
- "time"
-
- "t73f.de/r/zsc/api"
- "zettelstore.de/z/config"
- "zettelstore.de/z/encoding"
- "zettelstore.de/z/encoding/xml"
- "zettelstore.de/z/kernel"
- "zettelstore.de/z/query"
- "zettelstore.de/z/strfun"
- "zettelstore.de/z/zettel/id"
- "zettelstore.de/z/zettel/meta"
-)
-
-const ContentType = "application/atom+xml"
-
-type Configuration struct {
- Title string
- Generator string
- NewURLBuilderAbs func() *api.URLBuilder
-}
-
-func (c *Configuration) Setup(cfg config.Config) {
- baseURL := kernel.Main.GetConfig(kernel.WebService, kernel.WebBaseURL).(string)
-
- c.Title = cfg.GetSiteName()
- c.Generator = (kernel.Main.GetConfig(kernel.CoreService, kernel.CoreProgname).(string) +
- " " +
- kernel.Main.GetConfig(kernel.CoreService, kernel.CoreVersion).(string))
- c.NewURLBuilderAbs = func() *api.URLBuilder { return api.NewURLBuilder(baseURL, 'h') }
-}
-
-func (c *Configuration) Marshal(q *query.Query, ml []*meta.Meta) []byte {
- atomUpdated := encoding.LastUpdated(ml, time.RFC3339)
- feedLink := c.NewURLBuilderAbs().String()
-
- var buf bytes.Buffer
- buf.WriteString(`` + "\n")
- xml.WriteTag(&buf, " ", "title", c.Title)
- xml.WriteTag(&buf, " ", "id", feedLink)
- buf.WriteString(` ` + "\n")
- if atomUpdated != "" {
- xml.WriteTag(&buf, " ", "updated", atomUpdated)
- }
- xml.WriteTag(&buf, " ", "generator", c.Generator)
- buf.WriteString(" Unknown \n")
-
- for _, m := range ml {
- c.marshalMeta(&buf, m)
- }
-
- buf.WriteString(" ")
- return buf.Bytes()
-}
-
-func (c *Configuration) marshalMeta(buf *bytes.Buffer, m *meta.Meta) {
- entryUpdated := ""
- if val, found := m.Get(api.KeyPublished); found {
- if published, err := time.ParseInLocation(id.TimestampLayout, val, time.Local); err == nil {
- entryUpdated = published.UTC().Format(time.RFC3339)
- }
- }
-
- link := c.NewURLBuilderAbs().SetZid(m.Zid.ZettelID()).String()
-
- buf.WriteString(" \n")
- xml.WriteTag(buf, " ", "title", encoding.TitleAsText(m))
- xml.WriteTag(buf, " ", "id", link)
- buf.WriteString(` ` + "\n")
- buf.WriteString(` ` + "\n")
-
- if entryUpdated != "" {
- xml.WriteTag(buf, " ", "updated", entryUpdated)
- }
- marshalTags(buf, m)
- buf.WriteString(" \n")
-}
-
-func marshalTags(buf *bytes.Buffer, m *meta.Meta) {
- if tags, found := m.GetList(api.KeyTags); found && len(tags) > 0 {
- for _, tag := range tags {
- for len(tag) > 0 && tag[0] == '#' {
- tag = tag[1:]
- }
- if tag != "" {
- buf.WriteString(` \n")
- }
- }
- }
-}
DELETED encoding/encoding.go
Index: encoding/encoding.go
==================================================================
--- encoding/encoding.go
+++ encoding/encoding.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 encoding provides helper functions for encodings.
-package encoding
-
-import (
- "time"
-
- "t73f.de/r/zsc/api"
- "zettelstore.de/z/parser"
- "zettelstore.de/z/zettel/id"
- "zettelstore.de/z/zettel/meta"
-)
-
-// LastUpdated returns the formated time of the zettel which was updated at the latest time.
-func LastUpdated(ml []*meta.Meta, timeFormat string) string {
- maxPublished := time.Date(1, time.January, 1, 0, 0, 0, 0, time.Local)
- for _, m := range ml {
- if val, found := m.Get(api.KeyPublished); found {
- if published, err := time.ParseInLocation(id.TimestampLayout, val, time.Local); err == nil {
- if maxPublished.Before(published) {
- maxPublished = published
- }
- }
- }
- }
- if maxPublished.Year() > 1 {
- return maxPublished.UTC().Format(timeFormat)
- }
- return ""
-}
-
-// TitleAsText returns the title of a zettel as plain text
-func TitleAsText(m *meta.Meta) string { return parser.NormalizedSpacedText(m.GetTitle()) }
DELETED encoding/rss/rss.go
Index: encoding/rss/rss.go
==================================================================
--- encoding/rss/rss.go
+++ encoding/rss/rss.go
@@ -1,121 +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 rss provides a RSS encoding.
-package rss
-
-import (
- "bytes"
- "context"
- "time"
-
- "t73f.de/r/zsc/api"
- "zettelstore.de/z/config"
- "zettelstore.de/z/encoding"
- "zettelstore.de/z/encoding/xml"
- "zettelstore.de/z/kernel"
- "zettelstore.de/z/query"
- "zettelstore.de/z/strfun"
- "zettelstore.de/z/zettel/id"
- "zettelstore.de/z/zettel/meta"
-)
-
-const ContentType = "application/rss+xml"
-
-type Configuration struct {
- Title string
- Language string
- Copyright string
- Generator string
- NewURLBuilderAbs func() *api.URLBuilder
-}
-
-func (c *Configuration) Setup(ctx context.Context, cfg config.Config) {
- baseURL := kernel.Main.GetConfig(kernel.WebService, kernel.WebBaseURL).(string)
- defVals := cfg.AddDefaultValues(ctx, &meta.Meta{})
-
- c.Title = cfg.GetSiteName()
- c.Language = defVals.GetDefault(api.KeyLang, "")
- c.Copyright = defVals.GetDefault(api.KeyCopyright, "")
- c.Generator = (kernel.Main.GetConfig(kernel.CoreService, kernel.CoreProgname).(string) +
- " " +
- kernel.Main.GetConfig(kernel.CoreService, kernel.CoreVersion).(string))
- c.NewURLBuilderAbs = func() *api.URLBuilder { return api.NewURLBuilder(baseURL, 'h') }
-}
-
-func (c *Configuration) Marshal(q *query.Query, ml []*meta.Meta) []byte {
- rssPublished := encoding.LastUpdated(ml, time.RFC1123Z)
-
- atomLink := ""
- if s := q.String(); s != "" {
- atomLink = c.NewURLBuilderAbs().AppendQuery(s).String()
- }
- var buf bytes.Buffer
- buf.WriteString(`` + "\n\n")
- xml.WriteTag(&buf, " ", "title", c.Title)
- xml.WriteTag(&buf, " ", "link", c.NewURLBuilderAbs().String())
- xml.WriteTag(&buf, " ", "description", "")
- xml.WriteTag(&buf, " ", "language", c.Language)
- xml.WriteTag(&buf, " ", "copyright", c.Copyright)
- if rssPublished != "" {
- xml.WriteTag(&buf, " ", "pubDate", rssPublished)
- xml.WriteTag(&buf, " ", "lastBuildDate", rssPublished)
- }
- xml.WriteTag(&buf, " ", "generator", c.Generator)
- buf.WriteString(" https://www.rssboard.org/rss-specification \n")
- if atomLink != "" {
- buf.WriteString(` ` + "\n")
- }
- for _, m := range ml {
- c.marshalMeta(&buf, m)
- }
-
- buf.WriteString(" \n ")
- return buf.Bytes()
-}
-
-func (c *Configuration) marshalMeta(buf *bytes.Buffer, m *meta.Meta) {
- itemPublished := ""
- if val, found := m.Get(api.KeyPublished); found {
- if published, err := time.ParseInLocation(id.TimestampLayout, val, time.Local); err == nil {
- itemPublished = published.UTC().Format(time.RFC1123Z)
- }
- }
-
- link := c.NewURLBuilderAbs().SetZid(m.Zid.ZettelID()).String()
-
- buf.WriteString(" - \n")
- xml.WriteTag(buf, " ", "title", encoding.TitleAsText(m))
- xml.WriteTag(buf, " ", "link", link)
- xml.WriteTag(buf, " ", "guid", link)
- if itemPublished != "" {
- xml.WriteTag(buf, " ", "pubDate", itemPublished)
- }
- marshalTags(buf, m)
- buf.WriteString("
\n")
-}
-
-func marshalTags(buf *bytes.Buffer, m *meta.Meta) {
- if tags, found := m.GetList(api.KeyTags); found && len(tags) > 0 {
- for _, tag := range tags {
- for len(tag) > 0 && tag[0] == '#' {
- tag = tag[1:]
- }
- if tag != "" {
- xml.WriteTag(buf, " ", "category", tag)
- }
- }
- }
-}
DELETED encoding/xml/xml.go
Index: encoding/xml/xml.go
==================================================================
--- encoding/xml/xml.go
+++ encoding/xml/xml.go
@@ -1,36 +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 xml provides helper for a XML-based encoding.
-package xml
-
-import (
- "bytes"
-
- "zettelstore.de/z/strfun"
-)
-
-// Header contains the string that should start all XML documents.
-const Header = `` + "\n"
-
-// WriteTag writes a simple XML tag with a given prefix and a specific value.
-func WriteTag(buf *bytes.Buffer, prefix, tag, value string) {
- buf.WriteString(prefix)
- buf.WriteByte('<')
- buf.WriteString(tag)
- buf.WriteByte('>')
- strfun.XMLEscape(buf, value)
- buf.WriteString("")
- buf.WriteString(tag)
- buf.WriteString(">\n")
-}
DELETED evaluator/evaluator.go
Index: evaluator/evaluator.go
==================================================================
--- evaluator/evaluator.go
+++ evaluator/evaluator.go
@@ -1,662 +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 evaluator interprets and evaluates the AST.
-package evaluator
-
-import (
- "bytes"
- "context"
- "errors"
- "fmt"
- "path"
- "strconv"
- "strings"
-
- "t73f.de/r/sx/sxbuiltins"
- "t73f.de/r/sx/sxreader"
- "t73f.de/r/zsc/api"
- "t73f.de/r/zsc/attrs"
- "t73f.de/r/zsc/input"
- "zettelstore.de/z/ast"
- "zettelstore.de/z/box"
- "zettelstore.de/z/config"
- "zettelstore.de/z/parser"
- "zettelstore.de/z/parser/cleaner"
- "zettelstore.de/z/parser/draw"
- "zettelstore.de/z/query"
- "zettelstore.de/z/zettel"
- "zettelstore.de/z/zettel/id"
- "zettelstore.de/z/zettel/meta"
-)
-
-// Port contains all methods to retrieve zettel (or part of it) to evaluate a zettel.
-type Port interface {
- GetZettel(context.Context, id.Zid) (zettel.Zettel, error)
- QueryMeta(ctx context.Context, q *query.Query) ([]*meta.Meta, error)
-}
-
-// EvaluateZettel evaluates the given zettel in the given context, with the
-// given ports, and the given environment.
-func EvaluateZettel(ctx context.Context, port Port, rtConfig config.Config, zn *ast.ZettelNode) {
- switch zn.Syntax {
- case meta.SyntaxNone:
- // AST is empty, evaluate to a description list of metadata.
- zn.Ast = evaluateMetadata(zn.Meta)
- case meta.SyntaxSxn:
- zn.Ast = evaluateSxn(zn.Ast)
- default:
- EvaluateBlock(ctx, port, rtConfig, &zn.Ast)
- }
-}
-
-func evaluateSxn(bs ast.BlockSlice) ast.BlockSlice {
- // Check for structure made in parser/plain/plain.go:parseSxnBlocks
- if len(bs) == 1 {
- // If len(bs) > 1 --> an error was found during parsing
- if vn, isVerbatim := bs[0].(*ast.VerbatimNode); isVerbatim && vn.Kind == ast.VerbatimProg {
- if classAttr, hasClass := vn.Attrs.Get(""); hasClass && classAttr == meta.SyntaxSxn {
- rd := sxreader.MakeReader(bytes.NewReader(vn.Content))
- if objs, err := rd.ReadAll(); err == nil {
- result := make(ast.BlockSlice, len(objs))
- for i, obj := range objs {
- var buf bytes.Buffer
- sxbuiltins.Print(&buf, obj)
- result[i] = &ast.VerbatimNode{
- Kind: ast.VerbatimProg,
- Attrs: attrs.Attributes{"": classAttr},
- Content: buf.Bytes(),
- }
- }
- return result
- }
- }
- }
- }
- return bs
-}
-
-// EvaluateBlock evaluates the given block list in the given context, with
-// the given ports, and the given environment.
-func EvaluateBlock(ctx context.Context, port Port, rtConfig config.Config, bns *ast.BlockSlice) {
- evaluateNode(ctx, port, rtConfig, bns)
- cleaner.CleanBlockSlice(bns, true)
-}
-
-// EvaluateInline evaluates the given inline list in the given context, with
-// the given ports, and the given environment.
-func EvaluateInline(ctx context.Context, port Port, rtConfig config.Config, is *ast.InlineSlice) {
- evaluateNode(ctx, port, rtConfig, is)
- cleaner.CleanInlineSlice(is)
-}
-
-func evaluateNode(ctx context.Context, port Port, rtConfig config.Config, n ast.Node) {
- e := evaluator{
- ctx: ctx,
- port: port,
- rtConfig: rtConfig,
- transcludeMax: rtConfig.GetMaxTransclusions(),
- transcludeCount: 0,
- costMap: map[id.Zid]transcludeCost{},
- embedMap: map[string]ast.InlineSlice{},
- marker: &ast.ZettelNode{},
- }
- ast.Walk(&e, n)
-}
-
-type evaluator struct {
- ctx context.Context
- port Port
- rtConfig config.Config
- transcludeMax int
- transcludeCount int
- costMap map[id.Zid]transcludeCost
- marker *ast.ZettelNode
- embedMap map[string]ast.InlineSlice
-}
-
-type transcludeCost struct {
- zn *ast.ZettelNode
- ec int
-}
-
-func (e *evaluator) Visit(node ast.Node) ast.Visitor {
- switch n := node.(type) {
- case *ast.BlockSlice:
- e.visitBlockSlice(n)
- case *ast.InlineSlice:
- e.visitInlineSlice(n)
- default:
- return e
- }
- return nil
-}
-
-func (e *evaluator) visitBlockSlice(bs *ast.BlockSlice) {
- for i := 0; i < len(*bs); i++ {
- bn := (*bs)[i]
- ast.Walk(e, bn)
- switch n := bn.(type) {
- case *ast.VerbatimNode:
- i += transcludeNode(bs, i, e.evalVerbatimNode(n))
- case *ast.TranscludeNode:
- i += transcludeNode(bs, i, e.evalTransclusionNode(n))
- }
- }
-}
-
-func transcludeNode(bln *ast.BlockSlice, i int, bn ast.BlockNode) int {
- if ln, ok := bn.(*ast.BlockSlice); ok {
- *bln = replaceWithBlockNodes(*bln, i, *ln)
- return len(*ln) - 1
- }
- if bn == nil {
- (*bln) = (*bln)[:i+copy((*bln)[i:], (*bln)[i+1:])]
- return -1
- }
- (*bln)[i] = bn
- return 0
-}
-
-func replaceWithBlockNodes(bns []ast.BlockNode, i int, replaceBns []ast.BlockNode) []ast.BlockNode {
- if len(replaceBns) == 1 {
- bns[i] = replaceBns[0]
- return bns
- }
- newIns := make([]ast.BlockNode, 0, len(bns)+len(replaceBns)-1)
- if i > 0 {
- newIns = append(newIns, bns[:i]...)
- }
- if len(replaceBns) > 0 {
- newIns = append(newIns, replaceBns...)
- }
- if i+1 < len(bns) {
- newIns = append(newIns, bns[i+1:]...)
- }
- return newIns
-}
-
-func (e *evaluator) evalVerbatimNode(vn *ast.VerbatimNode) ast.BlockNode {
- switch vn.Kind {
- case ast.VerbatimZettel:
- return e.evalVerbatimZettel(vn)
- case ast.VerbatimEval:
- if syntax, found := vn.Attrs.Get(""); found && syntax == meta.SyntaxDraw {
- return draw.ParseDrawBlock(vn)
- }
- }
- return vn
-}
-
-func (e *evaluator) evalVerbatimZettel(vn *ast.VerbatimNode) ast.BlockNode {
- m := meta.New(id.Invalid)
- m.Set(api.KeySyntax, getSyntax(vn.Attrs, meta.SyntaxText))
- zettel := zettel.Zettel{
- Meta: m,
- Content: zettel.NewContent(vn.Content),
- }
- e.transcludeCount++
- zn := e.evaluateEmbeddedZettel(zettel)
- return &zn.Ast
-}
-
-func getSyntax(a attrs.Attributes, defSyntax string) string {
- if a != nil {
- if val, ok := a.Get(api.KeySyntax); ok {
- return val
- }
- if val, ok := a.Get(""); ok {
- return val
- }
- }
- return defSyntax
-}
-
-func (e *evaluator) evalTransclusionNode(tn *ast.TranscludeNode) ast.BlockNode {
- ref := tn.Ref
-
- // To prevent e.embedCount from counting
- if errText := e.checkMaxTransclusions(ref); errText != nil {
- return makeBlockNode(errText)
- }
- switch ref.State {
- case ast.RefStateZettel:
- // Only zettel references will be evaluated.
- case ast.RefStateInvalid, ast.RefStateBroken:
- e.transcludeCount++
- return makeBlockNode(createInlineErrorText(ref, "Invalid", "or", "broken", "transclusion", "reference"))
- case ast.RefStateSelf:
- e.transcludeCount++
- return makeBlockNode(createInlineErrorText(ref, "Self", "transclusion", "reference"))
- case ast.RefStateFound, ast.RefStateExternal:
- return tn
- case ast.RefStateHosted, ast.RefStateBased:
- if n := createEmbeddedNodeLocal(ref); n != nil {
- n.Attrs = tn.Attrs
- return makeBlockNode(n)
- }
- return tn
- case ast.RefStateQuery:
- e.transcludeCount++
- return e.evalQueryTransclusion(tn.Ref.Value)
- default:
- return makeBlockNode(createInlineErrorText(ref, "Illegal", "block", "state", strconv.Itoa(int(ref.State))))
- }
-
- zid, err := id.Parse(ref.URL.Path)
- if err != nil {
- panic(err)
- }
-
- cost, ok := e.costMap[zid]
- zn := cost.zn
- if zn == e.marker {
- e.transcludeCount++
- return makeBlockNode(createInlineErrorText(ref, "Recursive", "transclusion"))
- }
- if !ok {
- zettel, err1 := e.port.GetZettel(box.NoEnrichContext(e.ctx), zid)
- if err1 != nil {
- if errors.Is(err1, &box.ErrNotAllowed{}) {
- return nil
- }
- e.transcludeCount++
- return makeBlockNode(createInlineErrorText(ref, "Unable", "to", "get", "zettel"))
- }
- setMetadataFromAttributes(zettel.Meta, tn.Attrs)
- ec := e.transcludeCount
- e.costMap[zid] = transcludeCost{zn: e.marker, ec: ec}
- zn = e.evaluateEmbeddedZettel(zettel)
- e.costMap[zid] = transcludeCost{zn: zn, ec: e.transcludeCount - ec}
- e.transcludeCount = 0 // No stack needed, because embedding is done left-recursive, depth-first.
- }
- e.transcludeCount++
- if ec := cost.ec; ec > 0 {
- e.transcludeCount += cost.ec
- }
- return &zn.Ast
-}
-
-func (e *evaluator) evalQueryTransclusion(expr string) ast.BlockNode {
- q := query.Parse(expr)
- ml, err := e.port.QueryMeta(e.ctx, q)
- if err != nil {
- if errors.Is(err, &box.ErrNotAllowed{}) {
- return nil
- }
- return makeBlockNode(createInlineErrorText(nil, "Unable", "to", "search", "zettel"))
- }
- result, _ := QueryAction(e.ctx, q, ml, e.rtConfig)
- if result != nil {
- ast.Walk(e, result)
- }
- return result
-}
-
-func (e *evaluator) checkMaxTransclusions(ref *ast.Reference) ast.InlineNode {
- if maxTrans := e.transcludeMax; e.transcludeCount > maxTrans {
- e.transcludeCount = maxTrans + 1
- return createInlineErrorText(ref,
- "Too", "many", "transclusions", "(must", "be", "at", "most", strconv.Itoa(maxTrans)+",",
- "see", "runtime", "configuration", "key", "max-transclusions)")
- }
- return nil
-}
-
-func makeBlockNode(in ast.InlineNode) ast.BlockNode { return ast.CreateParaNode(in) }
-
-func setMetadataFromAttributes(m *meta.Meta, a attrs.Attributes) {
- for aKey, aVal := range a {
- if meta.KeyIsValid(aKey) {
- m.Set(aKey, aVal)
- }
- }
-}
-
-func (e *evaluator) visitInlineSlice(is *ast.InlineSlice) {
- for i := 0; i < len(*is); i++ {
- in := (*is)[i]
- ast.Walk(e, in)
- switch n := in.(type) {
- case *ast.LinkNode:
- (*is)[i] = e.evalLinkNode(n)
- case *ast.EmbedRefNode:
- i += embedNode(is, i, e.evalEmbedRefNode(n))
- case *ast.LiteralNode:
- i += embedNode(is, i, e.evalLiteralNode(n))
- }
- }
-}
-
-func embedNode(is *ast.InlineSlice, i int, in ast.InlineNode) int {
- if ln, ok := in.(*ast.InlineSlice); ok {
- *is = replaceWithInlineNodes(*is, i, *ln)
- return len(*ln) - 1
- }
- if in == nil {
- (*is) = (*is)[:i+copy((*is)[i:], (*is)[i+1:])]
- return -1
- }
- (*is)[i] = in
- return 0
-}
-
-func replaceWithInlineNodes(ins ast.InlineSlice, i int, replaceIns ast.InlineSlice) ast.InlineSlice {
- if len(replaceIns) == 1 {
- ins[i] = replaceIns[0]
- return ins
- }
- newIns := make(ast.InlineSlice, 0, len(ins)+len(replaceIns)-1)
- if i > 0 {
- newIns = append(newIns, ins[:i]...)
- }
- if len(replaceIns) > 0 {
- newIns = append(newIns, replaceIns...)
- }
- if i+1 < len(ins) {
- newIns = append(newIns, ins[i+1:]...)
- }
- return newIns
-}
-
-func (e *evaluator) evalLinkNode(ln *ast.LinkNode) ast.InlineNode {
- if len(ln.Inlines) == 0 {
- ln.Inlines = ast.InlineSlice{&ast.TextNode{Text: ln.Ref.Value}}
- }
- ref := ln.Ref
- if ref == nil || ref.State != ast.RefStateZettel {
- return ln
- }
-
- zid := mustParseZid(ref)
- _, err := e.port.GetZettel(box.NoEnrichContext(e.ctx), zid)
- if errors.Is(err, &box.ErrNotAllowed{}) {
- return &ast.FormatNode{
- Kind: ast.FormatSpan,
- Attrs: ln.Attrs,
- Inlines: getLinkInline(ln),
- }
- } else if err != nil {
- ln.Ref.State = ast.RefStateBroken
- return ln
- }
-
- ln.Ref.State = ast.RefStateZettel
- return ln
-}
-
-func getLinkInline(ln *ast.LinkNode) ast.InlineSlice {
- if ln.Inlines != nil {
- return ln.Inlines
- }
- return ast.InlineSlice{&ast.TextNode{Text: ln.Ref.Value}}
-}
-
-func (e *evaluator) evalEmbedRefNode(en *ast.EmbedRefNode) ast.InlineNode {
- ref := en.Ref
-
- // To prevent e.embedCount from counting
- if errText := e.checkMaxTransclusions(ref); errText != nil {
- return errText
- }
-
- switch ref.State {
- case ast.RefStateZettel:
- // Only zettel references will be evaluated.
- case ast.RefStateInvalid, ast.RefStateBroken:
- e.transcludeCount++
- return createInlineErrorImage(en)
- case ast.RefStateSelf:
- e.transcludeCount++
- return createInlineErrorText(ref, "Self", "embed", "reference")
- case ast.RefStateFound, ast.RefStateExternal:
- return en
- case ast.RefStateHosted, ast.RefStateBased:
- if n := createEmbeddedNodeLocal(ref); n != nil {
- n.Attrs = en.Attrs
- n.Inlines = en.Inlines
- return n
- }
- return en
- default:
- return createInlineErrorText(ref, "Illegal", "inline", "state", strconv.Itoa(int(ref.State)))
- }
-
- zid := mustParseZid(ref)
- zettel, err := e.port.GetZettel(box.NoEnrichContext(e.ctx), zid)
- if err != nil {
- if errors.Is(err, &box.ErrNotAllowed{}) {
- return nil
- }
- e.transcludeCount++
- return createInlineErrorImage(en)
- }
-
- if syntax := zettel.Meta.GetDefault(api.KeySyntax, meta.DefaultSyntax); parser.IsImageFormat(syntax) {
- e.updateImageRefNode(en, zettel.Meta, syntax)
- return en
- } else if !parser.IsASTParser(syntax) {
- // Not embeddable.
- e.transcludeCount++
- return createInlineErrorText(ref, "Not", "embeddable (syntax="+syntax+")")
- }
-
- cost, ok := e.costMap[zid]
- zn := cost.zn
- if zn == e.marker {
- e.transcludeCount++
- return createInlineErrorText(ref, "Recursive", "transclusion")
- }
- if !ok {
- ec := e.transcludeCount
- e.costMap[zid] = transcludeCost{zn: e.marker, ec: ec}
- zn = e.evaluateEmbeddedZettel(zettel)
- e.costMap[zid] = transcludeCost{zn: zn, ec: e.transcludeCount - ec}
- e.transcludeCount = 0 // No stack needed, because embedding is done left-recursive, depth-first.
- }
- e.transcludeCount++
-
- result, ok := e.embedMap[ref.Value]
- if !ok {
- // Search for text to be embedded.
- result = findInlineSlice(&zn.Ast, ref.URL.Fragment)
- e.embedMap[ref.Value] = result
- }
- if len(result) == 0 {
- return &ast.LiteralNode{
- Kind: ast.LiteralComment,
- Attrs: map[string]string{"-": ""},
- Content: append([]byte("Nothing to transclude: "), ref.String()...),
- }
- }
-
- if ec := cost.ec; ec > 0 {
- e.transcludeCount += cost.ec
- }
- return &result
-}
-
-func mustParseZid(ref *ast.Reference) id.Zid {
- zid, err := id.Parse(ref.URL.Path)
- if err != nil {
- panic(fmt.Sprintf("%v: %q (state %v) -> %v", err, ref.URL.Path, ref.State, ref))
- }
- return zid
-}
-
-func (e *evaluator) updateImageRefNode(en *ast.EmbedRefNode, m *meta.Meta, syntax string) {
- en.Syntax = syntax
- if len(en.Inlines) == 0 {
- is := parser.ParseDescription(m)
- if len(is) > 0 {
- ast.Walk(e, &is)
- if len(is) > 0 {
- en.Inlines = is
- }
- }
- }
-}
-
-func (e *evaluator) evalLiteralNode(ln *ast.LiteralNode) ast.InlineNode {
- if ln.Kind != ast.LiteralZettel {
- return ln
- }
- e.transcludeCount++
- result := e.evaluateEmbeddedInline(ln.Content, getSyntax(ln.Attrs, meta.SyntaxText))
- if len(result) == 0 {
- return &ast.LiteralNode{
- Kind: ast.LiteralComment,
- Attrs: map[string]string{"-": ""},
- Content: []byte("Nothing to transclude"),
- }
- }
- return &result
-}
-
-func createInlineErrorImage(en *ast.EmbedRefNode) *ast.EmbedRefNode {
- errorZid := id.EmojiZid
- en.Ref = ast.ParseReference(errorZid.String())
- if len(en.Inlines) == 0 {
- en.Inlines = parser.ParseMetadata("Error placeholder")
- }
- return en
-}
-
-func createInlineErrorText(ref *ast.Reference, msgWords ...string) ast.InlineNode {
- text := strings.Join(msgWords, " ")
- if ref != nil {
- text += ": " + ref.String() + "."
- }
- ln := &ast.LiteralNode{
- Kind: ast.LiteralInput,
- Content: []byte(text),
- }
- fn := &ast.FormatNode{
- Kind: ast.FormatStrong,
- Inlines: ast.InlineSlice{ln},
- }
- fn.Attrs = fn.Attrs.AddClass("error")
- return fn
-}
-
-func createEmbeddedNodeLocal(ref *ast.Reference) *ast.EmbedRefNode {
- ext := path.Ext(ref.Value)
- if ext != "" && ext[0] == '.' {
- ext = ext[1:]
- }
- pinfo := parser.Get(ext)
- if pinfo == nil || !pinfo.IsImageFormat {
- return nil
- }
- return &ast.EmbedRefNode{
- Ref: ref,
- Syntax: ext,
- }
-}
-
-func (e *evaluator) evaluateEmbeddedInline(content []byte, syntax string) ast.InlineSlice {
- is := parser.ParseInlines(input.NewInput(content), syntax)
- ast.Walk(e, &is)
- return is
-}
-
-func (e *evaluator) evaluateEmbeddedZettel(zettel zettel.Zettel) *ast.ZettelNode {
- zn := parser.ParseZettel(e.ctx, zettel, zettel.Meta.GetDefault(api.KeySyntax, meta.DefaultSyntax), e.rtConfig)
- ast.Walk(e, &zn.Ast)
- return zn
-}
-
-func findInlineSlice(bs *ast.BlockSlice, fragment string) ast.InlineSlice {
- if fragment == "" {
- return firstInlinesToEmbed(*bs)
- }
- fs := fragmentSearcher{fragment: fragment}
- ast.Walk(&fs, bs)
- return fs.result
-}
-
-func firstInlinesToEmbed(bs ast.BlockSlice) ast.InlineSlice {
- if ins := bs.FirstParagraphInlines(); ins != nil {
- return ins
- }
- if len(bs) == 0 {
- return nil
- }
- if bn, ok := bs[0].(*ast.BLOBNode); ok {
- return ast.InlineSlice{&ast.EmbedBLOBNode{
- Blob: bn.Blob,
- Syntax: bn.Syntax,
- Inlines: bn.Description,
- }}
- }
- return nil
-}
-
-type fragmentSearcher struct {
- fragment string
- result ast.InlineSlice
-}
-
-func (fs *fragmentSearcher) Visit(node ast.Node) ast.Visitor {
- if len(fs.result) > 0 {
- return nil
- }
- switch n := node.(type) {
- case *ast.BlockSlice:
- fs.visitBlockSlice(n)
- case *ast.InlineSlice:
- fs.visitInlineSlice(n)
- default:
- return fs
- }
- return nil
-}
-
-func (fs *fragmentSearcher) visitBlockSlice(bs *ast.BlockSlice) {
- for i, bn := range *bs {
- if hn, ok := bn.(*ast.HeadingNode); ok && hn.Fragment == fs.fragment {
- fs.result = (*bs)[i+1:].FirstParagraphInlines()
- return
- }
- ast.Walk(fs, bn)
- }
-}
-
-func (fs *fragmentSearcher) visitInlineSlice(is *ast.InlineSlice) {
- for i, in := range *is {
- if mn, ok := in.(*ast.MarkNode); ok && mn.Fragment == fs.fragment {
- ris := skipSpaceNodes((*is)[i+1:])
- if len(mn.Inlines) > 0 {
- fs.result = append(ast.InlineSlice{}, mn.Inlines...)
- fs.result = append(fs.result, &ast.SpaceNode{Lexeme: " "})
- fs.result = append(fs.result, ris...)
- } else {
- fs.result = ris
- }
- return
- }
- ast.Walk(fs, in)
- }
-}
-
-func skipSpaceNodes(ins ast.InlineSlice) ast.InlineSlice {
- for i, in := range ins {
- switch in.(type) {
- case *ast.SpaceNode:
- case *ast.BreakNode:
- default:
- return ins[i:]
- }
- }
- return nil
-}
DELETED evaluator/list.go
Index: evaluator/list.go
==================================================================
--- evaluator/list.go
+++ evaluator/list.go
@@ -1,378 +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 evaluator
-
-import (
- "bytes"
- "context"
- "math"
- "sort"
- "strconv"
- "strings"
-
- "t73f.de/r/zsc/api"
- "t73f.de/r/zsc/attrs"
- "zettelstore.de/z/ast"
- "zettelstore.de/z/config"
- "zettelstore.de/z/encoding/atom"
- "zettelstore.de/z/encoding/rss"
- "zettelstore.de/z/parser"
- "zettelstore.de/z/query"
- "zettelstore.de/z/zettel/meta"
-)
-
-// QueryAction transforms a list of metadata according to query actions into a AST nested list.
-func QueryAction(ctx context.Context, q *query.Query, ml []*meta.Meta, rtConfig config.Config) (ast.BlockNode, int) {
- ap := actionPara{
- ctx: ctx,
- q: q,
- ml: ml,
- kind: ast.NestedListUnordered,
- min: -1,
- max: -1,
- title: rtConfig.GetSiteName(),
- }
- actions := q.Actions()
- if len(actions) == 0 {
- return ap.createBlockNodeMeta("")
- }
-
- acts := make([]string, 0, len(actions))
- for i, act := range actions {
- if strings.HasPrefix(act, api.NumberedAction[0:1]) {
- ap.kind = ast.NestedListOrdered
- continue
- }
- if strings.HasPrefix(act, api.MinAction) {
- if num, err := strconv.Atoi(act[3:]); err == nil && num > 0 {
- ap.min = num
- continue
- }
- }
- if strings.HasPrefix(act, api.MaxAction) {
- if num, err := strconv.Atoi(act[3:]); err == nil && num > 0 {
- ap.max = num
- continue
- }
- }
- if act == api.TitleAction && i+1 < len(actions) {
- ap.title = strings.Join(actions[i+1:], " ")
- break
- }
- if act == api.ReIndexAction {
- continue
- }
- acts = append(acts, act)
- }
- var firstUnknowAct string
- for _, act := range acts {
- switch act {
- case api.AtomAction:
- return ap.createBlockNodeAtom(rtConfig)
- case api.RSSAction:
- return ap.createBlockNodeRSS(rtConfig)
- case api.KeysAction:
- return ap.createBlockNodeMetaKeys()
- }
- key := strings.ToLower(act)
- switch meta.Type(key) {
- case meta.TypeWord:
- return ap.createBlockNodeWord(key)
- case meta.TypeTagSet:
- return ap.createBlockNodeTagSet(key)
- }
- if firstUnknowAct == "" {
- firstUnknowAct = act
- }
- }
- bn, numItems := ap.createBlockNodeMeta(strings.ToLower(firstUnknowAct))
- if bn != nil && numItems == 0 && firstUnknowAct == strings.ToUpper(firstUnknowAct) {
- bn, numItems = ap.createBlockNodeMeta("")
- }
- return bn, numItems
-}
-
-type actionPara struct {
- ctx context.Context
- q *query.Query
- ml []*meta.Meta
- kind ast.NestedListKind
- min int
- max int
- title string
-}
-
-func (ap *actionPara) createBlockNodeWord(key string) (ast.BlockNode, int) {
- var buf bytes.Buffer
- ccs, bufLen := ap.prepareCatAction(key, &buf)
- if len(ccs) == 0 {
- return nil, 0
- }
- items := make([]ast.ItemSlice, 0, len(ccs))
- ccs.SortByName()
- for _, cat := range ccs {
- buf.WriteString(cat.Name)
- items = append(items, ast.ItemSlice{ast.CreateParaNode(&ast.LinkNode{
- Attrs: nil,
- Ref: ast.ParseReference(buf.String()),
- Inlines: ast.InlineSlice{&ast.TextNode{Text: cat.Name}},
- })})
- buf.Truncate(bufLen)
- }
- return &ast.NestedListNode{
- Kind: ap.kind,
- Items: items,
- Attrs: nil,
- }, len(items)
-}
-
-func (ap *actionPara) createBlockNodeTagSet(key string) (ast.BlockNode, int) {
- var buf bytes.Buffer
- ccs, bufLen := ap.prepareCatAction(key, &buf)
- if len(ccs) == 0 {
- return nil, 0
- }
- ccs.SortByCount()
- ccs = ap.limitTags(ccs)
- countMap := ap.calcFontSizes(ccs)
-
- para := make(ast.InlineSlice, 0, len(ccs))
- ccs.SortByName()
- for i, cat := range ccs {
- if i > 0 {
- para = append(para, &ast.SpaceNode{Lexeme: " "})
- }
- buf.WriteString(cat.Name)
- para = append(para,
- &ast.LinkNode{
- Attrs: countMap[cat.Count],
- Ref: ast.ParseReference(buf.String()),
- Inlines: ast.InlineSlice{
- &ast.TextNode{Text: cat.Name},
- },
- },
- &ast.FormatNode{
- Kind: ast.FormatSuper,
- Attrs: nil,
- Inlines: ast.InlineSlice{&ast.TextNode{Text: strconv.Itoa(cat.Count)}},
- },
- )
- buf.Truncate(bufLen)
- }
- return &ast.ParaNode{Inlines: para}, len(ccs)
-}
-
-func (ap *actionPara) limitTags(ccs meta.CountedCategories) meta.CountedCategories {
- if min, max := ap.min, ap.max; min > 0 || max > 0 {
- if min < 0 {
- min = ccs[len(ccs)-1].Count
- }
- if max < 0 {
- max = ccs[0].Count
- }
- if ccs[len(ccs)-1].Count < min || max < ccs[0].Count {
- temp := make(meta.CountedCategories, 0, len(ccs))
- for _, cat := range ccs {
- if min <= cat.Count && cat.Count <= max {
- temp = append(temp, cat)
- }
- }
- return temp
- }
- }
- return ccs
-}
-
-func (ap *actionPara) createBlockNodeMetaKeys() (ast.BlockNode, int) {
- arr := make(meta.Arrangement, 128)
- for _, m := range ap.ml {
- for k := range m.Map() {
- arr[k] = append(arr[k], m)
- }
- }
- if len(arr) == 0 {
- return nil, 0
- }
- ccs := arr.Counted()
- ccs.SortByName()
-
- var buf bytes.Buffer
- bufLen := ap.prepareSimpleQuery(&buf)
- items := make([]ast.ItemSlice, 0, len(ccs))
- for _, cat := range ccs {
- buf.WriteString(cat.Name)
- buf.WriteString(api.ExistOperator)
- q1 := buf.String()
- buf.Truncate(bufLen)
- buf.WriteString(api.ActionSeparator)
- buf.WriteString(cat.Name)
- q2 := buf.String()
- buf.Truncate(bufLen)
-
- items = append(items, ast.ItemSlice{ast.CreateParaNode(
- &ast.LinkNode{
- Attrs: nil,
- Ref: ast.ParseReference(q1),
- Inlines: ast.InlineSlice{&ast.TextNode{Text: cat.Name}},
- },
- &ast.SpaceNode{Lexeme: " "},
- &ast.TextNode{Text: "(" + strconv.Itoa(cat.Count) + ", "},
- &ast.LinkNode{
- Attrs: nil,
- Ref: ast.ParseReference(q2),
- Inlines: ast.InlineSlice{&ast.TextNode{Text: "values"}},
- },
- &ast.TextNode{Text: ")"},
- )})
- }
- return &ast.NestedListNode{
- Kind: ap.kind,
- Items: items,
- Attrs: nil,
- }, len(items)
-}
-
-func (ap *actionPara) createBlockNodeMeta(key string) (ast.BlockNode, int) {
- if len(ap.ml) == 0 {
- return nil, 0
- }
- items := make([]ast.ItemSlice, 0, len(ap.ml))
- for _, m := range ap.ml {
- if key != "" {
- if _, found := m.Get(key); !found {
- continue
- }
- }
- items = append(items, ast.ItemSlice{ast.CreateParaNode(&ast.LinkNode{
- Attrs: nil,
- Ref: ast.ParseReference(m.Zid.String()),
- Inlines: parser.ParseSpacedText(m.GetTitle()),
- })})
- }
- return &ast.NestedListNode{
- Kind: ap.kind,
- Items: items,
- Attrs: nil,
- }, len(items)
-}
-
-func (ap *actionPara) prepareCatAction(key string, buf *bytes.Buffer) (meta.CountedCategories, int) {
- if len(ap.ml) == 0 {
- return nil, 0
- }
- ccs := meta.CreateArrangement(ap.ml, key).Counted()
- if len(ccs) == 0 {
- return nil, 0
- }
-
- ap.prepareSimpleQuery(buf)
- buf.WriteString(key)
- buf.WriteString(api.SearchOperatorHas)
- bufLen := buf.Len()
-
- return ccs, bufLen
-}
-
-func (ap *actionPara) prepareSimpleQuery(buf *bytes.Buffer) int {
- sea := ap.q.Clone()
- sea.RemoveActions()
- buf.WriteString(ast.QueryPrefix)
- sea.Print(buf)
- if buf.Len() > len(ast.QueryPrefix) {
- buf.WriteByte(' ')
- }
- return buf.Len()
-}
-
-const fontSizes = 6 // Must be the number of CSS classes zs-font-size-* in base.css
-const fontSizes64 = float64(fontSizes)
-
-func (*actionPara) calcFontSizes(ccs meta.CountedCategories) map[int]attrs.Attributes {
- var fsAttrs [fontSizes]attrs.Attributes
- var a attrs.Attributes
- for i := range fontSizes {
- fsAttrs[i] = a.AddClass("zs-font-size-" + strconv.Itoa(i))
- }
-
- countMap := make(map[int]int, len(ccs))
- for _, cat := range ccs {
- countMap[cat.Count]++
- }
-
- countList := make([]int, 0, len(countMap))
- for count := range countMap {
- countList = append(countList, count)
- }
- sort.Ints(countList)
-
- result := make(map[int]attrs.Attributes, len(countList))
- if len(countList) <= fontSizes {
- // If we have less different counts, center them inside the fsAttrs vector.
- curSize := (fontSizes - len(countList)) / 2
- for _, count := range countList {
- result[count] = fsAttrs[curSize]
- curSize++
- }
- return result
- }
-
- // Idea: the number of occurences for a specific count is substracted from a budget.
- total := float64(len(ccs))
- curSize := 0
- budget := calcBudget(total, 0.0)
- for _, count := range countList {
- result[count] = fsAttrs[curSize]
- cc := float64(countMap[count])
- total -= cc
- budget -= cc
- if budget < 1 {
- curSize++
- if curSize >= fontSizes {
- curSize = fontSizes
- budget = 0.0
- } else {
- budget = calcBudget(total, float64(curSize))
- }
- }
- }
- return result
-}
-
-func calcBudget(total, curSize float64) float64 { return math.Round(total / (fontSizes64 - curSize)) }
-
-func (ap *actionPara) createBlockNodeRSS(cfg config.Config) (ast.BlockNode, int) {
- var rssConfig rss.Configuration
- rssConfig.Setup(ap.ctx, cfg)
- rssConfig.Title = ap.title
- data := rssConfig.Marshal(ap.q, ap.ml)
-
- return &ast.VerbatimNode{
- Kind: ast.VerbatimProg,
- Attrs: attrs.Attributes{"lang": "xml"},
- Content: data,
- }, len(ap.ml)
-}
-
-func (ap *actionPara) createBlockNodeAtom(cfg config.Config) (ast.BlockNode, int) {
- var atomConfig atom.Configuration
- atomConfig.Setup(cfg)
- atomConfig.Title = ap.title
- data := atomConfig.Marshal(ap.q, ap.ml)
-
- return &ast.VerbatimNode{
- Kind: ast.VerbatimProg,
- Attrs: attrs.Attributes{"lang": "xml"},
- Content: data,
- }, len(ap.ml)
-}
DELETED evaluator/metadata.go
Index: evaluator/metadata.go
==================================================================
--- evaluator/metadata.go
+++ evaluator/metadata.go
@@ -1,66 +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 evaluator
-
-import (
- "zettelstore.de/z/ast"
- "zettelstore.de/z/zettel/meta"
-)
-
-func evaluateMetadata(m *meta.Meta) ast.BlockSlice {
- descrlist := &ast.DescriptionListNode{}
- for _, p := range m.Pairs() {
- descrlist.Descriptions = append(
- descrlist.Descriptions, getMetadataDescription(p.Key, p.Value))
- }
- return ast.BlockSlice{descrlist}
-}
-
-func getMetadataDescription(key, value string) ast.Description {
- is := convertMetavalueToInlineSlice(value, meta.Type(key))
- return ast.Description{
- Term: ast.InlineSlice{&ast.TextNode{Text: key}},
- Descriptions: []ast.DescriptionSlice{{&ast.ParaNode{Inlines: is}}},
- }
-}
-
-func convertMetavalueToInlineSlice(value string, dt *meta.DescriptionType) ast.InlineSlice {
- var sliceData []string
- if dt.IsSet {
- sliceData = meta.ListFromValue(value)
- if len(sliceData) == 0 {
- return nil
- }
- } else {
- sliceData = []string{value}
- }
- makeLink := dt == meta.TypeID || dt == meta.TypeIDSet
-
- result := make(ast.InlineSlice, 0, 2*len(sliceData)-1)
- for i, val := range sliceData {
- if i > 0 {
- result = append(result, &ast.SpaceNode{Lexeme: " "})
- }
- tn := &ast.TextNode{Text: val}
- if makeLink {
- result = append(result, &ast.LinkNode{
- Ref: ast.ParseReference(val),
- Inlines: ast.InlineSlice{tn},
- })
- } else {
- result = append(result, tn)
- }
- }
- return result
-}
Index: go.mod
==================================================================
--- go.mod
+++ go.mod
@@ -1,19 +1,12 @@
module zettelstore.de/z
-go 1.22
-
-require (
- github.com/fsnotify/fsnotify v1.7.0
- github.com/yuin/goldmark v1.7.1
- golang.org/x/crypto v0.22.0
- golang.org/x/term v0.19.0
- golang.org/x/text v0.14.0
- t73f.de/r/sx v0.0.0-20240418072254-b6eff7d787f9
- t73f.de/r/sxhtml v0.0.0-20240418073213-2d735b1e4353
- t73f.de/r/zsc v0.0.0-20240418132724-4623af27f112
-)
-
-require (
- golang.org/x/sys v0.19.0 // indirect
- t73f.de/r/webs v0.0.0-20240418131849-b90a59f7f704 // indirect
+go 1.16
+
+require (
+ github.com/fsnotify/fsnotify v1.4.9
+ github.com/pascaldekloe/jwt v1.10.0
+ github.com/yuin/goldmark v1.3.7
+ golang.org/x/crypto v0.0.0-20210220033148-5ea612d1eb83
+ golang.org/x/term v0.0.0-20201117132131-f5c789dd3221
+ golang.org/x/text v0.3.6
)
Index: go.sum
==================================================================
--- go.sum
+++ go.sum
@@ -1,20 +1,20 @@
-github.com/fsnotify/fsnotify v1.7.0 h1:8JEhPFa5W2WU7YfeZzPNqzMP6Lwt7L2715Ggo0nosvA=
-github.com/fsnotify/fsnotify v1.7.0/go.mod h1:40Bi/Hjc2AVfZrqy+aj+yEI+/bRxZnMJyTJwOpGvigM=
-github.com/yuin/goldmark v1.7.1 h1:3bajkSilaCbjdKVsKdZjZCLBNPL9pYzrCakKaf4U49U=
-github.com/yuin/goldmark v1.7.1/go.mod h1:uzxRWxtg69N339t3louHJ7+O03ezfj6PlliRlaOzY1E=
-golang.org/x/crypto v0.22.0 h1:g1v0xeRhjcugydODzvb3mEM9SQ0HGp9s/nh3COQ/C30=
-golang.org/x/crypto v0.22.0/go.mod h1:vr6Su+7cTlO45qkww3VDJlzDn0ctJvRgYbC2NvXHt+M=
-golang.org/x/sys v0.19.0 h1:q5f1RH2jigJ1MoAWp2KTp3gm5zAGFUTarQZ5U386+4o=
-golang.org/x/sys v0.19.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
-golang.org/x/term v0.19.0 h1:+ThwsDv+tYfnJFhF4L8jITxu1tdTWRTZpdsWgEgjL6Q=
-golang.org/x/term v0.19.0/go.mod h1:2CuTdWZ7KHSQwUzKva0cbMg6q2DMI3Mmxp+gKJbskEk=
-golang.org/x/text v0.14.0 h1:ScX5w1eTa3QqT8oi6+ziP7dTV1S2+ALU0bI+0zXKWiQ=
-golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU=
-t73f.de/r/sx v0.0.0-20240418072254-b6eff7d787f9 h1:lVPkYN8+J9f6JA9SmoF6icvpLxz4u3h1MCTuDYJYwdU=
-t73f.de/r/sx v0.0.0-20240418072254-b6eff7d787f9/go.mod h1:G9pD1j2R6y9ZkPBb81mSnmwaAvTOg7r6jKp/OF7WeFA=
-t73f.de/r/sxhtml v0.0.0-20240418073213-2d735b1e4353 h1:FYE8m1ewouQXG3uc4FoUcr+++qzX8OtIJ9ZhI8CSs1s=
-t73f.de/r/sxhtml v0.0.0-20240418073213-2d735b1e4353/go.mod h1:AGX5DjZ1x6agvQA8VVK8/bwae9Hcr9qBkt/kXgGQDjE=
-t73f.de/r/webs v0.0.0-20240418131849-b90a59f7f704 h1:UkPXoJC0DUczgvuQbptvjKJ6N9vnBTQ6gIflBp7tC/k=
-t73f.de/r/webs v0.0.0-20240418131849-b90a59f7f704/go.mod h1:UGAAtul0TK5ACeZ6zTS3SX6GqwMFXxlUpHiV8oqNq5w=
-t73f.de/r/zsc v0.0.0-20240418132724-4623af27f112 h1:AsyAn58gP3P/olHi0lNkm4fAlnjXaPzYMRO0iBp+KBw=
-t73f.de/r/zsc v0.0.0-20240418132724-4623af27f112/go.mod h1:6zqgiX3FXW7J9GyXBDOqOIciyaHZtZh9zp1yNFp/cus=
+github.com/fsnotify/fsnotify v1.4.9 h1:hsms1Qyu0jgnwNXIxa+/V/PDsU6CfLf6CNO8H7IWoS4=
+github.com/fsnotify/fsnotify v1.4.9/go.mod h1:znqG4EE+3YCdAaPaxE2ZRY/06pZUdp0tY4IgpuI1SZQ=
+github.com/pascaldekloe/jwt v1.10.0 h1:ktcIUV4TPvh404R5dIBEnPCsSwj0sqi3/0+XafE5gJs=
+github.com/pascaldekloe/jwt v1.10.0/go.mod h1:TKhllgThT7TOP5rGr2zMLKEDZRAgJfBbtKyVeRsNB9A=
+github.com/yuin/goldmark v1.3.7 h1:NSaHgaeJFCtWXCBkBKXw0rhgMuJ0VoE9FB5mWldcrQ4=
+github.com/yuin/goldmark v1.3.7/go.mod h1:mwnBkeHKe2W/ZEtQ+71ViKU8L12m81fl3OWwC1Zlc8k=
+golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
+golang.org/x/crypto v0.0.0-20210220033148-5ea612d1eb83 h1:/ZScEX8SfEmUGRHs0gxpqteO5nfNW6axyZbBdw9A12g=
+golang.org/x/crypto v0.0.0-20210220033148-5ea612d1eb83/go.mod h1:jdWPYTVW3xRLrWPugEBEK3UY2ZEsg3UU495nc5E+M+I=
+golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
+golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
+golang.org/x/sys v0.0.0-20191005200804-aed5e4c7ecf9/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20191026070338-33540a1f6037 h1:YyJpGZS1sBuBCzLAR1VEpK193GlqGZbnPFnPV/5Rsb4=
+golang.org/x/sys v0.0.0-20191026070338-33540a1f6037/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/term v0.0.0-20201117132131-f5c789dd3221 h1:/ZHdbVpdR/jk3g30/d4yUL0JU9kksj8+F/bnQUVLGDM=
+golang.org/x/term v0.0.0-20201117132131-f5c789dd3221/go.mod h1:Nr5EML6q2oocZ2LXRh80K7BxOlk5/8JxuGnuhpl+muw=
+golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
+golang.org/x/text v0.3.6 h1:aRYxNxv6iGQlyVaZmk6ZgYEDa+Jg18DxebPSrd6bg1M=
+golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
+golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
ADDED input/input.go
Index: input/input.go
==================================================================
--- input/input.go
+++ input/input.go
@@ -0,0 +1,207 @@
+//-----------------------------------------------------------------------------
+// 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 input provides an abstraction for data to be read.
+package input
+
+import (
+ "html"
+ "unicode"
+ "unicode/utf8"
+)
+
+// Input is an abstract input source
+type Input struct {
+ // Read-only, will never change
+ Src string // The source string
+
+ // Read-only, will change
+ Ch rune // current character
+ Pos int // character position in src
+ readPos int // reading position (position after current character)
+}
+
+// NewInput creates a new input source.
+func NewInput(src string) *Input {
+ inp := &Input{Src: src}
+ inp.Next()
+ return inp
+}
+
+// EOS = End of source
+const EOS = rune(-1)
+
+// Next reads the next rune into inp.Ch.
+func (inp *Input) Next() {
+ if inp.readPos < len(inp.Src) {
+ inp.Pos = inp.readPos
+ r, w := rune(inp.Src[inp.readPos]), 1
+ if r >= utf8.RuneSelf {
+ r, w = utf8.DecodeRuneInString(inp.Src[inp.readPos:])
+ }
+ inp.readPos += w
+ inp.Ch = r
+ } else {
+ inp.Pos = len(inp.Src)
+ inp.Ch = EOS
+ }
+}
+
+// Peek returns the rune following the most recently read rune without
+// advancing. If end-of-source was already found peek returns EOS.
+func (inp *Input) Peek() rune {
+ return inp.PeekN(0)
+}
+
+// PeekN returns the n-th rune after the most recently read rune without
+// advancing. If end-of-source was already found peek returns EOS.
+func (inp *Input) PeekN(n int) rune {
+ pos := inp.readPos + n
+ if pos < len(inp.Src) {
+ r := rune(inp.Src[pos])
+ if r >= utf8.RuneSelf {
+ r, _ = utf8.DecodeRuneInString(inp.Src[pos:])
+ }
+ if r == '\t' {
+ return ' '
+ }
+ return r
+ }
+ return EOS
+}
+
+// IsEOLEOS returns true if char is either EOS or EOL.
+func IsEOLEOS(ch rune) bool {
+ switch ch {
+ case EOS, '\n', '\r':
+ return true
+ }
+ return false
+}
+
+// EatEOL transforms both "\r" and "\r\n" into "\n".
+func (inp *Input) EatEOL() {
+ switch inp.Ch {
+ case '\r':
+ if inp.Peek() == '\n' {
+ inp.Next()
+ }
+ inp.Ch = '\n'
+ inp.Next()
+ case '\n':
+ inp.Next()
+ }
+}
+
+// SetPos allows to reset the read position.
+func (inp *Input) SetPos(pos int) {
+ inp.readPos = pos
+ inp.Next()
+}
+
+// SkipToEOL reads until the next end-of-line.
+func (inp *Input) SkipToEOL() {
+ for {
+ switch inp.Ch {
+ case EOS, '\n', '\r':
+ return
+ }
+ inp.Next()
+ }
+}
+
+// ScanEntity scans either a named or a numbered entity and returns it as a string.
+//
+// For numbered entities (like { or ģ) html.UnescapeString returns
+// sometimes other values as expected, if the number is not well-formed. This
+// may happen because of some strange HTML parsing rules. But these do not
+// apply to Zettelmarkup. Therefore, I parse the number here in the code.
+func (inp *Input) ScanEntity() (res string, success bool) {
+ if inp.Ch != '&' {
+ return "", false
+ }
+ pos := inp.Pos
+ inp.Next()
+ if inp.Ch == '#' {
+ inp.Next()
+ if inp.Ch == 'x' || inp.Ch == 'X' {
+ return inp.scanEntityBase16()
+ }
+ return inp.scanEntityBase10()
+ }
+ return inp.scanEntityNamed(pos)
+}
+
+func (inp *Input) scanEntityBase16() (string, bool) {
+ inp.Next()
+ if inp.Ch == ';' {
+ return "", false
+ }
+ code := 0
+ for {
+ switch ch := inp.Ch; ch {
+ case ';':
+ inp.Next()
+ return string(rune(code)), true
+ case '0', '1', '2', '3', '4', '5', '6', '7', '8', '9':
+ code = 16*code + int(ch-'0')
+ case 'a', 'b', 'c', 'd', 'e', 'f':
+ code = 16*code + int(ch-'a'+10)
+ case 'A', 'B', 'C', 'D', 'E', 'F':
+ code = 16*code + int(ch-'A'+10)
+ default:
+ return "", false
+ }
+ if code > unicode.MaxRune {
+ return "", false
+ }
+ inp.Next()
+ }
+}
+
+func (inp *Input) scanEntityBase10() (string, bool) {
+ // Base 10 code
+ if inp.Ch == ';' {
+ return "", false
+ }
+ code := 0
+ for {
+ switch ch := inp.Ch; ch {
+ case ';':
+ inp.Next()
+ return string(rune(code)), true
+ case '0', '1', '2', '3', '4', '5', '6', '7', '8', '9':
+ code = 10*code + int(ch-'0')
+ default:
+ return "", false
+ }
+ if code > unicode.MaxRune {
+ return "", false
+ }
+ inp.Next()
+ }
+}
+func (inp *Input) scanEntityNamed(pos int) (string, bool) {
+ for {
+ switch inp.Ch {
+ case EOS, '\n', '\r':
+ return "", false
+ case ';':
+ inp.Next()
+ es := inp.Src[pos:inp.Pos]
+ ues := html.UnescapeString(es)
+ if es == ues {
+ return "", false
+ }
+ return ues, true
+ }
+ inp.Next()
+ }
+}
ADDED input/input_test.go
Index: input/input_test.go
==================================================================
--- input/input_test.go
+++ input/input_test.go
@@ -0,0 +1,67 @@
+//-----------------------------------------------------------------------------
+// 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 input_test provides some unit-tests for reading data.
+package input_test
+
+import (
+ "testing"
+
+ "zettelstore.de/z/input"
+)
+
+func TestEatEOL(t *testing.T) {
+ inp := input.NewInput("")
+ inp.EatEOL()
+ if inp.Ch != input.EOS {
+ t.Errorf("No EOS found: %q", inp.Ch)
+ }
+ if inp.Pos != 0 {
+ t.Errorf("Pos != 0: %d", inp.Pos)
+ }
+
+ inp = input.NewInput("ABC")
+ if inp.Ch != 'A' {
+ t.Errorf("First ch != 'A', got %q", inp.Ch)
+ }
+ inp.EatEOL()
+ if inp.Ch != 'A' {
+ t.Errorf("First ch != 'A', got %q", inp.Ch)
+ }
+}
+
+func TestScanEntity(t *testing.T) {
+ var testcases = []struct {
+ text string
+ exp string
+ }{
+ {"", ""},
+ {"a", ""},
+ {"&", "&"},
+ {" ", "\t"},
+ {""", "\""},
+ }
+ for id, tc := range testcases {
+ inp := input.NewInput(tc.text)
+ got, ok := inp.ScanEntity()
+ if !ok {
+ if tc.exp != "" {
+ t.Errorf("ID=%d, text=%q: expected error, but got %q", id, tc.text, got)
+ }
+ if inp.Pos != 0 {
+ t.Errorf("ID=%d, text=%q: input position advances to %d", id, tc.text, inp.Pos)
+ }
+ continue
+ }
+ if tc.exp != got {
+ t.Errorf("ID=%d, text=%q: expected %q, but got %q", id, tc.text, tc.exp, got)
+ }
+ }
+}
Index: kernel/impl/auth.go
==================================================================
--- kernel/impl/auth.go
+++ kernel/impl/auth.go
@@ -1,61 +1,50 @@
//-----------------------------------------------------------------------------
-// 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 kernel implementation.
package impl
import (
- "errors"
"sync"
"zettelstore.de/z/auth"
+ "zettelstore.de/z/domain/id"
"zettelstore.de/z/kernel"
- "zettelstore.de/z/logger"
- "zettelstore.de/z/zettel/id"
)
type authService struct {
srvConfig
mxService sync.RWMutex
manager auth.Manager
createManager kernel.CreateAuthManagerFunc
}
-var errAlreadySetOwner = errors.New("changing an existing owner not allowed")
-var errAlreadyROMode = errors.New("system in readonly mode cannot change this mode")
-
-func (as *authService) Initialize(logger *logger.Logger) {
- as.logger = logger
+func (as *authService) Initialize() {
as.descr = descriptionMap{
kernel.AuthOwner: {
"Owner's zettel id",
- func(val string) (any, error) {
+ func(val string) interface{} {
if owner := as.cur[kernel.AuthOwner]; owner != nil && owner != id.Invalid {
- return nil, errAlreadySetOwner
- }
- if val == "" {
- return id.Invalid, nil
+ return nil
}
return parseZid(val)
},
false,
},
kernel.AuthReadonly: {
- "Readonly mode",
- func(val string) (any, error) {
+ "Read-only mode",
+ func(val string) interface{} {
if ro := as.cur[kernel.AuthReadonly]; ro == true {
- return nil, errAlreadyROMode
+ return nil
}
return parseBool(val)
},
true,
},
@@ -64,23 +53,21 @@
kernel.AuthOwner: id.Invalid,
kernel.AuthReadonly: false,
}
}
-func (as *authService) GetLogger() *logger.Logger { return as.logger }
-
-func (as *authService) Start(*myKernel) error {
+func (as *authService) Start(kern *myKernel) error {
as.mxService.Lock()
defer as.mxService.Unlock()
readonlyMode := as.GetNextConfig(kernel.AuthReadonly).(bool)
owner := as.GetNextConfig(kernel.AuthOwner).(id.Zid)
authMgr, err := as.createManager(readonlyMode, owner)
if err != nil {
- as.logger.Error().Err(err).Msg("Unable to create manager")
+ kern.doLog("Unable to create auth manager:", err)
return err
}
- as.logger.Info().Msg("Start Manager")
+ kern.doLog("Start Auth Manager")
as.manager = authMgr
return nil
}
func (as *authService) IsStarted() bool {
@@ -87,13 +74,16 @@
as.mxService.RLock()
defer as.mxService.RUnlock()
return as.manager != nil
}
-func (as *authService) Stop(*myKernel) {
- as.logger.Info().Msg("Stop Manager")
+func (as *authService) Stop(kern *myKernel) error {
+ kern.doLog("Stop Auth Manager")
as.mxService.Lock()
+ defer as.mxService.Unlock()
as.manager = nil
- as.mxService.Unlock()
+ return nil
}
-func (*authService) GetStatistics() []kernel.KeyValue { return nil }
+func (as *authService) GetStatistics() []kernel.KeyValue {
+ return nil
+}
DELETED kernel/impl/box.go
Index: kernel/impl/box.go
==================================================================
--- kernel/impl/box.go
+++ kernel/impl/box.go
@@ -1,148 +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 impl
-
-import (
- "context"
- "errors"
- "fmt"
- "io"
- "net/url"
- "strconv"
- "sync"
-
- "zettelstore.de/z/box"
- "zettelstore.de/z/kernel"
- "zettelstore.de/z/logger"
-)
-
-type boxService struct {
- srvConfig
- mxService sync.RWMutex
- manager box.Manager
- createManager kernel.CreateBoxManagerFunc
-}
-
-var errInvalidDirType = errors.New("invalid directory type")
-
-func (ps *boxService) Initialize(logger *logger.Logger) {
- ps.logger = logger
- ps.descr = descriptionMap{
- kernel.BoxDefaultDirType: {
- "Default directory box type",
- ps.noFrozen(func(val string) (any, error) {
- switch val {
- case kernel.BoxDirTypeNotify, kernel.BoxDirTypeSimple:
- return val, nil
- }
- return nil, errInvalidDirType
- }),
- true,
- },
- kernel.BoxURIs: {
- "Box URI",
- func(val string) (any, error) {
- uVal, err := url.Parse(val)
- if err != nil {
- return nil, err
- }
- if uVal.Scheme == "" {
- uVal.Scheme = "dir"
- }
- return uVal, nil
- },
- true,
- },
- }
- ps.next = interfaceMap{
- kernel.BoxDefaultDirType: kernel.BoxDirTypeNotify,
- }
-}
-
-func (ps *boxService) GetLogger() *logger.Logger { return ps.logger }
-
-func (ps *boxService) Start(kern *myKernel) error {
- boxURIs := make([]*url.URL, 0, 4)
- for i := 1; ; i++ {
- u := ps.GetNextConfig(kernel.BoxURIs + strconv.Itoa(i))
- if u == nil {
- break
- }
- boxURIs = append(boxURIs, u.(*url.URL))
- }
- ps.mxService.Lock()
- defer ps.mxService.Unlock()
- mgr, err := ps.createManager(boxURIs, kern.auth.manager, &kern.cfg)
- if err != nil {
- ps.logger.Error().Err(err).Msg("Unable to create manager")
- return err
- }
- ps.logger.Info().Str("location", mgr.Location()).Msg("Start Manager")
- if err = mgr.Start(context.Background()); err != nil {
- ps.logger.Error().Err(err).Msg("Unable to start manager")
- return err
- }
- kern.cfg.setBox(mgr)
- ps.manager = mgr
- return nil
-}
-
-func (ps *boxService) IsStarted() bool {
- ps.mxService.RLock()
- defer ps.mxService.RUnlock()
- return ps.manager != nil
-}
-
-func (ps *boxService) Stop(*myKernel) {
- ps.logger.Info().Msg("Stop Manager")
- ps.mxService.RLock()
- mgr := ps.manager
- ps.mxService.RUnlock()
- mgr.Stop(context.Background())
- ps.mxService.Lock()
- ps.manager = nil
- ps.mxService.Unlock()
-}
-
-func (ps *boxService) GetStatistics() []kernel.KeyValue {
- var st box.Stats
- ps.mxService.RLock()
- ps.manager.ReadStats(&st)
- ps.mxService.RUnlock()
- return []kernel.KeyValue{
- {Key: "Read-only", Value: strconv.FormatBool(st.ReadOnly)},
- {Key: "Managed boxes", Value: strconv.Itoa(st.NumManagedBoxes)},
- {Key: "Zettel (total)", Value: strconv.Itoa(st.ZettelTotal)},
- {Key: "Zettel (indexed)", Value: strconv.Itoa(st.ZettelIndexed)},
- {Key: "Last re-index", Value: st.LastReload.Format("2006-01-02 15:04:05 -0700 MST")},
- {Key: "Duration last re-index", Value: fmt.Sprintf("%vms", st.DurLastReload.Milliseconds())},
- {Key: "Indexes since last re-index", Value: strconv.FormatUint(st.IndexesSinceReload, 10)},
- {Key: "Indexed words", Value: strconv.FormatUint(st.IndexedWords, 10)},
- {Key: "Indexed URLs", Value: strconv.FormatUint(st.IndexedUrls, 10)},
- {Key: "Zettel enrichments", Value: strconv.FormatUint(st.IndexUpdates, 10)},
- }
-}
-
-func (ps *boxService) DumpIndex(w io.Writer) {
- ps.manager.Dump(w)
-}
-
-func (ps *boxService) Refresh() error {
- ps.mxService.RLock()
- defer ps.mxService.RUnlock()
- if ps.manager != nil {
- return ps.manager.Refresh(context.Background())
- }
- return nil
-}
Index: kernel/impl/cfg.go
==================================================================
--- kernel/impl/cfg.go
+++ kernel/impl/cfg.go
@@ -1,321 +1,296 @@
//-----------------------------------------------------------------------------
-// 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 kernel implementation.
package impl
import (
"context"
- "errors"
"fmt"
"strconv"
"strings"
"sync"
- "t73f.de/r/zsc/api"
- "zettelstore.de/z/box"
- "zettelstore.de/z/config"
+ "zettelstore.de/z/domain/id"
+ "zettelstore.de/z/domain/meta"
"zettelstore.de/z/kernel"
- "zettelstore.de/z/logger"
- "zettelstore.de/z/web/server"
- "zettelstore.de/z/zettel/id"
- "zettelstore.de/z/zettel/meta"
+ "zettelstore.de/z/place"
)
type configService struct {
srvConfig
mxService sync.RWMutex
- orig *meta.Meta
- manager box.Manager
-}
-
-// Predefined Metadata keys for runtime configuration
-// See: https://zettelstore.de/manual/h/00001004020000
-const (
- keyDefaultCopyright = "default-copyright"
- keyDefaultLicense = "default-license"
- keyDefaultVisibility = "default-visibility"
- keyExpertMode = "expert-mode"
- keyMaxTransclusions = "max-transclusions"
- keySiteName = "site-name"
- keyYAMLHeader = "yaml-header"
- keyZettelFileSyntax = "zettel-file-syntax"
-)
-
-var errUnknownVisibility = errors.New("unknown visibility")
-
-func (cs *configService) Initialize(logger *logger.Logger) {
- cs.logger = logger
- cs.descr = descriptionMap{
- keyDefaultCopyright: {"Default copyright", parseString, true},
- keyDefaultLicense: {"Default license", parseString, true},
- keyDefaultVisibility: {
- "Default zettel visibility",
- func(val string) (any, error) {
- vis := meta.GetVisibility(val)
- if vis == meta.VisibilityUnknown {
- return nil, errUnknownVisibility
- }
- return vis, nil
- },
- true,
- },
- keyExpertMode: {"Expert mode", parseBool, true},
- config.KeyFooterZettel: {"Footer Zettel", parseInvalidZid, true},
- config.KeyHomeZettel: {"Home zettel", parseZid, true},
- kernel.ConfigInsecureHTML: {
- "Insecure HTML",
- cs.noFrozen(func(val string) (any, error) {
- switch val {
- case kernel.ConfigSyntaxHTML:
- return config.SyntaxHTML, nil
- case kernel.ConfigMarkdownHTML:
- return config.MarkdownHTML, nil
- case kernel.ConfigZmkHTML:
- return config.ZettelmarkupHTML, nil
- }
- return config.NoHTML, nil
- }),
- true,
- },
- api.KeyLang: {"Language", parseString, true},
- keyMaxTransclusions: {"Maximum transclusions", parseInt64, true},
- keySiteName: {"Site name", parseString, true},
- keyYAMLHeader: {"YAML header", parseBool, true},
- keyZettelFileSyntax: {
- "Zettel file syntax",
- func(val string) (any, error) { return strings.Fields(val), nil },
- true,
- },
- kernel.ConfigSimpleMode: {"Simple mode", cs.noFrozen(parseBool), true},
- config.KeyShowBackLinks: {"Show back links", parseString, true},
- config.KeyShowFolgeLinks: {"Show folge links", parseString, true},
- config.KeyShowSubordinateLinks: {"Show subordinate links", parseString, true},
- config.KeyShowSuccessorLinks: {"Show successor links", parseString, true},
- }
- cs.next = interfaceMap{
- keyDefaultCopyright: "",
- keyDefaultLicense: "",
- keyDefaultVisibility: meta.VisibilityLogin,
- keyExpertMode: false,
- config.KeyFooterZettel: id.Invalid,
- config.KeyHomeZettel: id.DefaultHomeZid,
- kernel.ConfigInsecureHTML: config.NoHTML,
- api.KeyLang: api.ValueLangEN,
- keyMaxTransclusions: int64(1024),
- keySiteName: "Zettelstore",
- keyYAMLHeader: false,
- keyZettelFileSyntax: nil,
- kernel.ConfigSimpleMode: false,
- config.KeyShowBackLinks: "",
- config.KeyShowFolgeLinks: "",
- config.KeyShowSubordinateLinks: "",
- config.KeyShowSuccessorLinks: "",
- }
-}
-func (cs *configService) GetLogger() *logger.Logger { return cs.logger }
-
-func (cs *configService) Start(*myKernel) error {
- cs.logger.Info().Msg("Start Service")
- data := meta.New(id.ConfigurationZid)
- for _, kv := range cs.GetNextConfigList() {
- data.Set(kv.Key, kv.Value)
- }
- cs.mxService.Lock()
- cs.orig = data
+ rtConfig *myConfig
+}
+
+func (cs *configService) Initialize() {
+ cs.descr = descriptionMap{
+ meta.KeyDefaultCopyright: {"Default copyright", parseString, true},
+ meta.KeyDefaultLang: {"Default language", parseString, true},
+ meta.KeyDefaultRole: {"Default role", parseString, true},
+ meta.KeyDefaultSyntax: {"Default syntax", parseString, true},
+ meta.KeyDefaultTitle: {"Default title", parseString, true},
+ meta.KeyDefaultVisibility: {
+ "Default zettel visibility",
+ func(val string) interface{} {
+ vis := meta.GetVisibility(val)
+ if vis == meta.VisibilityUnknown {
+ return nil
+ }
+ return vis
+ },
+ true,
+ },
+ meta.KeyExpertMode: {"Expert mode", parseBool, true},
+ meta.KeyFooterHTML: {"Footer HTML", parseString, true},
+ meta.KeyHomeZettel: {"Home zettel", parseZid, true},
+ meta.KeyListPageSize: {
+ "List page size",
+ func(val string) interface{} {
+ iVal, err := strconv.Atoi(val)
+ if err != nil {
+ return nil
+ }
+ return iVal
+ },
+ true,
+ },
+ meta.KeyMarkerExternal: {"Marker external URL", parseString, true},
+ meta.KeySiteName: {"Site name", parseString, true},
+ meta.KeyYAMLHeader: {"YAML header", parseBool, true},
+ meta.KeyZettelFileSyntax: {
+ "Zettel file syntax",
+ func(val string) interface{} { return strings.Fields(val) },
+ true,
+ },
+ }
+ cs.next = interfaceMap{
+ meta.KeyDefaultCopyright: "",
+ meta.KeyDefaultLang: meta.ValueLangEN,
+ meta.KeyDefaultRole: meta.ValueRoleZettel,
+ meta.KeyDefaultSyntax: meta.ValueSyntaxZmk,
+ meta.KeyDefaultTitle: "Untitled",
+ meta.KeyDefaultVisibility: meta.VisibilityLogin,
+ meta.KeyExpertMode: false,
+ meta.KeyFooterHTML: "",
+ meta.KeyHomeZettel: id.DefaultHomeZid,
+ meta.KeyListPageSize: 0,
+ meta.KeyMarkerExternal: "➚",
+ meta.KeySiteName: "Zettelstore",
+ meta.KeyYAMLHeader: false,
+ meta.KeyZettelFileSyntax: nil,
+ }
+}
+
+func (cs *configService) Start(kern *myKernel) error {
+ kern.doLog("Start Config Service")
+ data := meta.New(id.ConfigurationZid)
+ for _, kv := range cs.GetNextConfigList() {
+ data.Set(kv.Key, fmt.Sprintf("%v", kv.Value))
+ }
+ cs.mxService.Lock()
+ cs.rtConfig = newConfig(data)
cs.mxService.Unlock()
return nil
}
func (cs *configService) IsStarted() bool {
cs.mxService.RLock()
defer cs.mxService.RUnlock()
- return cs.orig != nil
-}
-
-func (cs *configService) Stop(*myKernel) {
- cs.logger.Info().Msg("Stop Service")
- cs.mxService.Lock()
- cs.orig = nil
- cs.manager = nil
- cs.mxService.Unlock()
-}
-
-func (*configService) GetStatistics() []kernel.KeyValue {
- return nil
-}
-
-func (cs *configService) setBox(mgr box.Manager) {
- cs.mxService.Lock()
- cs.manager = mgr
- cs.mxService.Unlock()
- mgr.RegisterObserver(cs.observe)
- cs.observe(box.UpdateInfo{Box: mgr, Reason: box.OnZettel, Zid: id.ConfigurationZid})
-}
-
-func (cs *configService) doUpdate(p box.BaseBox) error {
- z, err := p.GetZettel(context.Background(), id.ConfigurationZid)
- cs.logger.Trace().Err(err).Msg("got config meta")
- if err != nil {
- return err
- }
- m := z.Meta
- cs.mxService.Lock()
- for _, pair := range cs.orig.Pairs() {
- key := pair.Key
- if val, ok := m.Get(key); ok {
- cs.SetConfig(key, val)
- } else if defVal, defFound := cs.orig.Get(key); defFound {
- cs.SetConfig(key, defVal)
- }
- }
- cs.mxService.Unlock()
- cs.SwitchNextToCur() // Poor man's restart
- return nil
-}
-
-func (cs *configService) observe(ci box.UpdateInfo) {
- if ci.Reason != box.OnZettel || ci.Zid == id.ConfigurationZid {
- cs.logger.Debug().Uint("reason", uint64(ci.Reason)).Zid(ci.Zid).Msg("observe")
- go func() {
- cs.mxService.RLock()
- mgr := cs.manager
- cs.mxService.RUnlock()
- if mgr != nil {
- cs.doUpdate(mgr)
- } else {
- cs.doUpdate(ci.Box)
- }
- }()
- }
-}
-
-// --- config.Config
-
-func (cs *configService) Get(ctx context.Context, m *meta.Meta, key string) string {
- if m != nil {
- if val, found := m.Get(key); found {
- return val
- }
- }
- if user := server.GetUser(ctx); user != nil {
- if val, found := user.Get(key); found {
- return val
- }
- }
- result := cs.GetCurConfig(key)
- if result == nil {
- return ""
- }
- switch val := result.(type) {
- case string:
- return val
- case bool:
- if val {
- return api.ValueTrue
- }
- return api.ValueFalse
- case id.Zid:
- return val.String()
- case int:
- return strconv.Itoa(val)
- case []string:
- return strings.Join(val, " ")
- case meta.Visibility:
- return val.String()
- case fmt.Stringer:
- return val.String()
- case fmt.GoStringer:
- return val.GoString()
- }
- return fmt.Sprintf("%v", result)
-}
-
-// AddDefaultValues enriches the given meta data with its default values.
-func (cs *configService) AddDefaultValues(ctx context.Context, m *meta.Meta) *meta.Meta {
- if cs == nil {
- return m
- }
- result := m
- cs.mxService.RLock()
- if _, found := m.Get(api.KeyCopyright); !found {
- result = updateMeta(result, m, api.KeyCopyright, cs.GetCurConfig(keyDefaultCopyright).(string))
- }
- if _, found := m.Get(api.KeyLang); !found {
- result = updateMeta(result, m, api.KeyLang, cs.Get(ctx, nil, api.KeyLang))
- }
- if _, found := m.Get(api.KeyLicense); !found {
- result = updateMeta(result, m, api.KeyLicense, cs.GetCurConfig(keyDefaultLicense).(string))
- }
- if _, found := m.Get(api.KeyVisibility); !found {
- result = updateMeta(result, m, api.KeyVisibility, cs.GetCurConfig(keyDefaultVisibility).(meta.Visibility).String())
- }
- cs.mxService.RUnlock()
- return result
-}
-func updateMeta(result, m *meta.Meta, key, val string) *meta.Meta {
- if result == m {
- result = m.Clone()
- }
- result.Set(key, val)
- return result
-}
-
-func (cs *configService) GetHTMLInsecurity() config.HTMLInsecurity {
- return cs.GetCurConfig(kernel.ConfigInsecureHTML).(config.HTMLInsecurity)
-}
-
-// GetSiteName returns the current value of the "site-name" key.
-func (cs *configService) GetSiteName() string { return cs.GetCurConfig(keySiteName).(string) }
-
-// GetMaxTransclusions return the maximum number of indirect transclusions.
-func (cs *configService) GetMaxTransclusions() int {
- return int(cs.GetCurConfig(keyMaxTransclusions).(int64))
-}
-
-// GetYAMLHeader returns the current value of the "yaml-header" key.
-func (cs *configService) GetYAMLHeader() bool { return cs.GetCurConfig(keyYAMLHeader).(bool) }
-
-// GetZettelFileSyntax returns the current value of the "zettel-file-syntax" key.
-func (cs *configService) GetZettelFileSyntax() []string {
- if zfs := cs.GetCurConfig(keyZettelFileSyntax); zfs != nil {
- return zfs.([]string)
- }
- return nil
-}
-
-// --- config.AuthConfig
-
-// GetSimpleMode returns true if system tuns in simple-mode.
-func (cs *configService) GetSimpleMode() bool { return cs.GetCurConfig(kernel.ConfigSimpleMode).(bool) }
-
-// GetExpertMode returns the current value of the "expert-mode" key.
-func (cs *configService) GetExpertMode() bool { return cs.GetCurConfig(keyExpertMode).(bool) }
-
-// GetVisibility returns the visibility value, or "login" if none is given.
-func (cs *configService) GetVisibility(m *meta.Meta) meta.Visibility {
- if val, ok := m.Get(api.KeyVisibility); ok {
+ return cs.rtConfig != nil
+}
+
+func (cs *configService) Stop(kern *myKernel) error {
+ kern.doLog("Stop Config Service")
+ cs.mxService.Lock()
+ cs.rtConfig = nil
+ cs.mxService.Unlock()
+ return nil
+}
+
+func (cs *configService) GetStatistics() []kernel.KeyValue {
+ return nil
+}
+
+func (cs *configService) setPlace(mgr place.Manager) {
+ cs.rtConfig.setPlace(mgr)
+}
+
+// myConfig contains all runtime configuration data relevant for the software.
+type myConfig struct {
+ mx sync.RWMutex
+ orig *meta.Meta
+ data *meta.Meta
+}
+
+// New creates a new Config value.
+func newConfig(orig *meta.Meta) *myConfig {
+ cfg := myConfig{
+ orig: orig,
+ data: orig.Clone(),
+ }
+ return &cfg
+}
+func (cfg *myConfig) setPlace(mgr place.Manager) {
+ mgr.RegisterObserver(cfg.observe)
+ cfg.doUpdate(mgr)
+}
+
+func (cfg *myConfig) doUpdate(p place.Place) error {
+ m, err := p.GetMeta(context.Background(), cfg.data.Zid)
+ if err != nil {
+ return err
+ }
+ cfg.mx.Lock()
+ for _, pair := range cfg.data.Pairs(false) {
+ if val, ok := m.Get(pair.Key); ok {
+ cfg.data.Set(pair.Key, val)
+ }
+ }
+ cfg.mx.Unlock()
+ return nil
+}
+
+func (cfg *myConfig) observe(ci place.UpdateInfo) {
+ if ci.Reason == place.OnReload || ci.Zid == id.ConfigurationZid {
+ go func() { cfg.doUpdate(ci.Place) }()
+ }
+}
+
+var defaultKeys = map[string]string{
+ meta.KeyCopyright: meta.KeyDefaultCopyright,
+ meta.KeyLang: meta.KeyDefaultLang,
+ meta.KeyLicense: meta.KeyDefaultLicense,
+ meta.KeyRole: meta.KeyDefaultRole,
+ meta.KeySyntax: meta.KeyDefaultSyntax,
+ meta.KeyTitle: meta.KeyDefaultTitle,
+}
+
+// AddDefaultValues enriches the given meta data with its default values.
+func (cfg *myConfig) AddDefaultValues(m *meta.Meta) *meta.Meta {
+ if cfg == nil {
+ return m
+ }
+ result := m
+ cfg.mx.RLock()
+ for k, d := range defaultKeys {
+ if _, ok := result.Get(k); !ok {
+ if result == m {
+ result = m.Clone()
+ }
+ if val, ok := cfg.data.Get(d); ok {
+ result.Set(k, val)
+ }
+ }
+ }
+ cfg.mx.RUnlock()
+ return result
+}
+
+func (cfg *myConfig) getString(key string) string {
+ cfg.mx.RLock()
+ val, _ := cfg.data.Get(key)
+ cfg.mx.RUnlock()
+ return val
+}
+func (cfg *myConfig) getBool(key string) bool {
+ cfg.mx.RLock()
+ val := cfg.data.GetBool(key)
+ cfg.mx.RUnlock()
+ return val
+}
+
+// GetDefaultTitle returns the current value of the "default-title" key.
+func (cfg *myConfig) GetDefaultTitle() string { return cfg.getString(meta.KeyDefaultTitle) }
+
+// GetDefaultRole returns the current value of the "default-role" key.
+func (cfg *myConfig) GetDefaultRole() string { return cfg.getString(meta.KeyDefaultRole) }
+
+// GetDefaultSyntax returns the current value of the "default-syntax" key.
+func (cfg *myConfig) GetDefaultSyntax() string { return cfg.getString(meta.KeyDefaultSyntax) }
+
+// GetDefaultLang returns the current value of the "default-lang" key.
+func (cfg *myConfig) GetDefaultLang() string { return cfg.getString(meta.KeyDefaultLang) }
+
+// GetSiteName returns the current value of the "site-name" key.
+func (cfg *myConfig) GetSiteName() string { return cfg.getString(meta.KeySiteName) }
+
+// GetHomeZettel returns the value of the "home-zettel" key.
+func (cfg *myConfig) GetHomeZettel() id.Zid {
+ val := cfg.getString(meta.KeyHomeZettel)
+ if homeZid, err := id.Parse(val); err == nil {
+ return homeZid
+ }
+ cfg.mx.RLock()
+ val, _ = cfg.orig.Get(meta.KeyHomeZettel)
+ homeZid, _ := id.Parse(val)
+ cfg.mx.RUnlock()
+ return homeZid
+}
+
+// GetDefaultVisibility returns the default value for zettel visibility.
+func (cfg *myConfig) GetDefaultVisibility() meta.Visibility {
+ val := cfg.getString(meta.KeyDefaultVisibility)
+ if vis := meta.GetVisibility(val); vis != meta.VisibilityUnknown {
+ return vis
+ }
+ cfg.mx.RLock()
+ val, _ = cfg.orig.Get(meta.KeyDefaultVisibility)
+ vis := meta.GetVisibility(val)
+ cfg.mx.RUnlock()
+ return vis
+}
+
+// GetYAMLHeader returns the current value of the "yaml-header" key.
+func (cfg *myConfig) GetYAMLHeader() bool { return cfg.getBool(meta.KeyYAMLHeader) }
+
+// GetMarkerExternal returns the current value of the "marker-external" key.
+func (cfg *myConfig) GetMarkerExternal() string {
+ return cfg.getString(meta.KeyMarkerExternal)
+}
+
+// GetFooterHTML returns HTML code that should be embedded into the footer
+// of each WebUI page.
+func (cfg *myConfig) GetFooterHTML() string { return cfg.getString(meta.KeyFooterHTML) }
+
+// GetListPageSize returns the maximum length of a list to be returned in WebUI.
+// A value less or equal to zero signals no limit.
+func (cfg *myConfig) GetListPageSize() int {
+ cfg.mx.RLock()
+ defer cfg.mx.RUnlock()
+
+ if value, ok := cfg.data.GetNumber(meta.KeyListPageSize); ok {
+ return value
+ }
+ value, _ := cfg.orig.GetNumber(meta.KeyListPageSize)
+ return value
+}
+
+// GetZettelFileSyntax returns the current value of the "zettel-file-syntax" key.
+func (cfg *myConfig) GetZettelFileSyntax() []string {
+ cfg.mx.RLock()
+ defer cfg.mx.RUnlock()
+ return cfg.data.GetListOrNil(meta.KeyZettelFileSyntax)
+}
+
+// --- AuthConfig
+
+// GetExpertMode returns the current value of the "expert-mode" key
+func (cfg *myConfig) GetExpertMode() bool { return cfg.getBool(meta.KeyExpertMode) }
+
+// GetVisibility returns the visibility value, or "login" if none is given.
+func (cfg *myConfig) GetVisibility(m *meta.Meta) meta.Visibility {
+ if val, ok := m.Get(meta.KeyVisibility); ok {
if vis := meta.GetVisibility(val); vis != meta.VisibilityUnknown {
return vis
}
}
-
- vis := cs.GetCurConfig(keyDefaultVisibility).(meta.Visibility)
- if vis != meta.VisibilityUnknown {
- return vis
- }
- cs.mxService.RLock()
- val, _ := cs.orig.Get(keyDefaultVisibility)
- vis = meta.GetVisibility(val)
- cs.mxService.RUnlock()
- return vis
+ return cfg.GetDefaultVisibility()
}
Index: kernel/impl/cmd.go
==================================================================
--- kernel/impl/cmd.go
+++ kernel/impl/cmd.go
@@ -1,32 +1,27 @@
//-----------------------------------------------------------------------------
-// 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 kernel implementation.
package impl
import (
"fmt"
"io"
"os"
"runtime/metrics"
"sort"
- "strconv"
"strings"
- "t73f.de/r/zsc/maps"
"zettelstore.de/z/kernel"
- "zettelstore.de/z/logger"
"zettelstore.de/z/strfun"
)
type cmdSession struct {
w io.Writer
@@ -70,14 +65,10 @@
}
}
sess.w.Write(sess.eol)
}
-func (sess *cmdSession) usage(cmd, val string) {
- sess.println("Usage:", cmd, val)
-}
-
func (sess *cmdSession) printTable(table [][]string) {
maxLen := sess.calcMaxLen(table)
if len(maxLen) == 0 {
return
}
@@ -167,13 +158,12 @@
sess.println("echo is off")
}
return true
},
},
- "end-profile": {"stop profiling", cmdEndProfile},
- "env": {"show environment values", cmdEnvironment},
- "get-config": {"show current configuration data", cmdGetConfig},
+ "env": {"show environment values", cmdEnvironment},
+ "get-config": {"show current configuration data", cmdGetConfig},
"header": {
"toggle table header",
func(sess *cmdSession, cmd string, args []string) bool {
sess.header = !sess.header
if sess.header {
@@ -182,15 +172,12 @@
sess.println("header are off")
}
return true
},
},
- "log-level": {"get/set log level", cmdLogLevel},
"metrics": {"show Go runtime metrics", cmdMetrics},
"next-config": {"show next configuration data", cmdNextConfig},
- "profile": {"start profiling", cmdProfile},
- "refresh": {"refresh box data", cmdRefresh},
"restart": {"restart service", cmdRestart},
"services": {"show available services", cmdServices},
"set-config": {"set next configuration data", cmdSetConfig},
"shutdown": {
"shutdown Zettelstore",
@@ -199,12 +186,19 @@
"start": {"start service", cmdStart},
"stat": {"show service statistics", cmdStat},
"stop": {"stop service", cmdStop},
}
-func cmdHelp(sess *cmdSession, _ string, _ []string) bool {
- cmds := maps.Keys(commands)
+func cmdHelp(sess *cmdSession, cmd string, args []string) bool {
+ cmds := make([]string, 0, len(commands))
+ for key := range commands {
+ if key == "" {
+ continue
+ }
+ cmds = append(cmds, key)
+ }
+ sort.Strings(cmds)
table := [][]string{{"Command", "Description"}}
for _, cmd := range cmds {
table = append(table, []string{cmd, commands[cmd].Text})
}
sess.printTable(table)
@@ -222,16 +216,16 @@
table = append(table, []string{kd.Key, kd.Descr})
}
sess.printTable(table)
return true
}
-func cmdGetConfig(sess *cmdSession, _ string, args []string) bool {
+func cmdGetConfig(sess *cmdSession, cmd string, args []string) bool {
showConfig(sess, args,
- listCurConfig, func(srv service, key string) interface{} { return srv.GetCurConfig(key) })
+ listCurConfig, func(srv service, key string) interface{} { return srv.GetConfig(key) })
return true
}
-func cmdNextConfig(sess *cmdSession, _ string, args []string) bool {
+func cmdNextConfig(sess *cmdSession, cmd string, args []string) bool {
showConfig(sess, args,
listNextConfig, func(srv service, key string) interface{} { return srv.GetNextConfig(key) })
return true
}
func showConfig(sess *cmdSession, args []string,
@@ -252,12 +246,13 @@
listConfig(sess, srvD.srv)
}
return
}
- srvD, found := getService(sess, args[0])
- if !found {
+ srvD, ok := sess.kern.srvNames[args[0]]
+ if !ok {
+ sess.println("Unknown service:", args[0])
return
}
if len(args) == 1 {
listConfig(sess, srvD.srv)
return
@@ -268,11 +263,11 @@
return
}
sess.println(fmt.Sprintf("%v", val))
}
func listCurConfig(sess *cmdSession, srv service) {
- listConfig(sess, func() []kernel.KeyDescrValue { return srv.GetCurConfigList(true) })
+ listConfig(sess, func() []kernel.KeyDescrValue { return srv.GetConfigList(true) })
}
func listNextConfig(sess *cmdSession, srv service) {
listConfig(sess, srv.GetNextConfigList)
}
func listConfig(sess *cmdSession, getConfigList func() []kernel.KeyDescrValue) {
@@ -284,30 +279,34 @@
sess.printTable(table)
}
func cmdSetConfig(sess *cmdSession, cmd string, args []string) bool {
if len(args) < 3 {
- sess.usage(cmd, "SERVICE KEY VALUE")
+ sess.println("Usage:", cmd, "SERIVCE KEY VALUE")
return true
}
- srvD, found := getService(sess, args[0])
- if !found {
+ srvD, ok := sess.kern.srvNames[args[0]]
+ if !ok {
+ sess.println("Unknown service:", args[0])
return true
}
- key := args[1]
newValue := strings.Join(args[2:], " ")
- if err := srvD.srv.SetConfig(key, newValue); err == nil {
- sess.kern.logger.Mandatory().Str("key", key).Str("value", newValue).Msg("Update system configuration")
- } else {
- sess.println("Unable to set key", args[1], "to value", newValue, "because:", err.Error())
+ if !srvD.srv.SetConfig(args[1], newValue) {
+ sess.println("Unable to set key", args[1], "to value", newValue)
}
return true
}
-func cmdServices(sess *cmdSession, _ string, _ []string) bool {
+func cmdServices(sess *cmdSession, cmd string, args []string) bool {
+ names := make([]string, 0, len(sess.kern.srvNames))
+ for name := range sess.kern.srvNames {
+ names = append(names, name)
+ }
+ sort.Strings(names)
+
table := [][]string{{"Service", "Status"}}
- for _, name := range sortedServiceNames(sess) {
+ for _, name := range names {
if sess.kern.srvNames[name].srv.IsStarted() {
table = append(table, []string{name, "started"})
} else {
table = append(table, []string{name, "stopped"})
}
@@ -352,15 +351,16 @@
return true
}
func cmdStat(sess *cmdSession, cmd string, args []string) bool {
if len(args) == 0 {
- sess.usage(cmd, "SERVICE")
+ sess.println("Usage:", cmd, "SERVICE")
return true
}
- srvD, ok := getService(sess, args[0])
+ srvD, ok := sess.kern.srvNames[args[0]]
if !ok {
+ sess.println("Unknown service", args[0])
return true
}
kvl := srvD.srv.GetStatistics()
if len(kvl) == 0 {
return true
@@ -371,103 +371,24 @@
}
sess.printTable(table)
return true
}
-func cmdLogLevel(sess *cmdSession, _ string, args []string) bool {
- kern := sess.kern
- if len(args) == 0 {
- // Write log levels
- level := kern.logger.Level()
- table := [][]string{
- {"Service", "Level", "Name"},
- {"kernel", strconv.Itoa(int(level)), level.String()},
- }
- for _, name := range sortedServiceNames(sess) {
- level = kern.srvNames[name].srv.GetLogger().Level()
- table = append(table, []string{name, strconv.Itoa(int(level)), level.String()})
- }
- sess.printTable(table)
- return true
- }
- var l *logger.Logger
- name := args[0]
- if name == "kernel" {
- l = kern.logger
- } else {
- srvD, ok := getService(sess, name)
- if !ok {
- return true
- }
- l = srvD.srv.GetLogger()
- }
-
- if len(args) == 1 {
- level := l.Level()
- sess.println(strconv.Itoa(int(level)), level.String())
- return true
- }
-
- level := args[1]
- uval, err := strconv.ParseUint(level, 10, 8)
- lv := logger.Level(uval)
- if err != nil || !lv.IsValid() {
- lv = logger.ParseLevel(level)
- }
- if !lv.IsValid() {
- sess.println("Invalid level:", level)
- return true
- }
- kern.logger.Mandatory().Str("name", name).Str("level", lv.String()).Msg("Update log level")
- l.SetLevel(lv)
- return true
-}
-
func lookupService(sess *cmdSession, cmd string, args []string) (kernel.Service, bool) {
if len(args) == 0 {
- sess.usage(cmd, "SERVICE")
+ sess.println("Usage:", cmd, "SERVICE")
return 0, false
}
- srvD, ok := getService(sess, args[0])
+ srvD, ok := sess.kern.srvNames[args[0]]
if !ok {
+ sess.println("Unknown service", args[0])
return 0, false
}
return srvD.srvnum, true
}
-func cmdProfile(sess *cmdSession, _ string, args []string) bool {
- var profileName string
- if len(args) < 1 {
- profileName = kernel.ProfileCPU
- } else {
- profileName = args[0]
- }
- var fileName string
- if len(args) < 2 {
- fileName = profileName + ".prof"
- } else {
- fileName = args[1]
- }
- kern := sess.kern
- if err := kern.doStartProfiling(profileName, fileName); err != nil {
- sess.println("Error:", err.Error())
- } else {
- kern.logger.Mandatory().Str("profile", profileName).Str("file", fileName).Msg("Start profiling")
- }
- return true
-}
-func cmdEndProfile(sess *cmdSession, _ string, _ []string) bool {
- kern := sess.kern
- err := kern.doStopProfiling()
- if err != nil {
- sess.println("Error:", err.Error())
- }
- kern.logger.Mandatory().Err(err).Msg("Stop profiling")
- return true
-}
-
-func cmdMetrics(sess *cmdSession, _ string, _ []string) bool {
+func cmdMetrics(sess *cmdSession, cmd string, args []string) bool {
var samples []metrics.Sample
all := metrics.All()
for _, d := range all {
if d.Kind == metrics.KindFloat64Histogram {
continue
@@ -489,11 +410,11 @@
value := samples[i].Value
i++
var sVal string
switch value.Kind() {
case metrics.KindUint64:
- sVal = strconv.FormatUint(value.Uint64(), 10)
+ sVal = fmt.Sprintf("%v", value.Uint64())
case metrics.KindFloat64:
sVal = fmt.Sprintf("%v", value.Float64())
case metrics.KindFloat64Histogram:
sVal = "(Histogramm)"
case metrics.KindBad:
@@ -505,25 +426,17 @@
}
sess.printTable(table)
return true
}
-func cmdDumpIndex(sess *cmdSession, _ string, _ []string) bool {
+func cmdDumpIndex(sess *cmdSession, cmd string, args []string) bool {
sess.kern.DumpIndex(sess.w)
return true
}
-
-func cmdRefresh(sess *cmdSession, _ string, _ []string) bool {
- kern := sess.kern
- kern.logger.Mandatory().Msg("Refresh")
- kern.box.Refresh()
- return true
-}
-
func cmdDumpRecover(sess *cmdSession, cmd string, args []string) bool {
if len(args) == 0 {
- sess.usage(cmd, "RECOVER")
+ sess.println("Usage:", cmd, "RECOVER")
sess.println("-- A valid value for RECOVER can be obtained via 'stat core'.")
return true
}
lines := sess.kern.core.RecoverLines(args[0])
if len(lines) == 0 {
@@ -533,11 +446,11 @@
sess.println(line)
}
return true
}
-func cmdEnvironment(sess *cmdSession, _ string, _ []string) bool {
+func cmdEnvironment(sess *cmdSession, cmd string, args []string) bool {
workDir, err := os.Getwd()
if err != nil {
workDir = err.Error()
}
execName, err := os.Executable()
@@ -558,15 +471,5 @@
}
}
sess.printTable(table)
return true
}
-
-func sortedServiceNames(sess *cmdSession) []string { return maps.Keys(sess.kern.srvNames) }
-
-func getService(sess *cmdSession, name string) (serviceData, bool) {
- srvD, found := sess.kern.srvNames[name]
- if !found {
- sess.println("Unknown service", name)
- }
- return srvD, found
-}
Index: kernel/impl/config.go
==================================================================
--- kernel/impl/config.go
+++ kernel/impl/config.go
@@ -1,35 +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 impl provides the kernel implementation.
package impl
import (
- "errors"
"fmt"
"sort"
"strconv"
"strings"
"sync"
- "t73f.de/r/zsc/maps"
+ "zettelstore.de/z/domain/id"
"zettelstore.de/z/kernel"
- "zettelstore.de/z/logger"
- "zettelstore.de/z/zettel/id"
)
-type parseFunc func(string) (any, error)
+type parseFunc func(string) interface{}
type configDescription struct {
text string
parse parseFunc
canList bool
}
@@ -46,11 +41,10 @@
}
return result
}
type srvConfig struct {
- logger *logger.Logger
mxConfig sync.RWMutex
frozen bool
descr descriptionMap
cur interfaceMap
next interfaceMap
@@ -57,11 +51,15 @@
}
func (cfg *srvConfig) ConfigDescriptions() []serviceConfigDescription {
cfg.mxConfig.RLock()
defer cfg.mxConfig.RUnlock()
- keys := maps.Keys(cfg.descr)
+ keys := make([]string, 0, len(cfg.descr))
+ for k := range cfg.descr {
+ keys = append(keys, k)
+ }
+ sort.Strings(keys)
result := make([]serviceConfigDescription, 0, len(keys))
for _, k := range keys {
text := cfg.descr[k].text
if strings.HasSuffix(k, "-") {
text = text + " (list)"
@@ -69,58 +67,55 @@
result = append(result, serviceConfigDescription{Key: k, Descr: text})
}
return result
}
-var errAlreadyFrozen = errors.New("value not allowed to be set")
-
func (cfg *srvConfig) noFrozen(parse parseFunc) parseFunc {
- return func(val string) (any, error) {
+ return func(val string) interface{} {
if cfg.frozen {
- return nil, errAlreadyFrozen
+ return nil
}
return parse(val)
}
}
-var errListKeyNotFound = errors.New("no list key found")
-
-func (cfg *srvConfig) SetConfig(key, value string) error {
+func (cfg *srvConfig) SetConfig(key, value string) bool {
cfg.mxConfig.Lock()
defer cfg.mxConfig.Unlock()
descr, ok := cfg.descr[key]
if !ok {
d, baseKey, num := cfg.getListDescription(key)
if num < 0 {
- return errListKeyNotFound
+ return false
}
+ format := baseKey + "%d"
for i := num + 1; ; i++ {
- k := baseKey + strconv.Itoa(i)
+ k := fmt.Sprintf(format, i)
if _, ok = cfg.next[k]; !ok {
break
}
delete(cfg.next, k)
}
if num == 0 {
- return nil
+ return true
}
descr = d
}
parse := descr.parse
if parse == nil {
if cfg.frozen {
- return errAlreadyFrozen
+ return false
}
cfg.next[key] = value
- return nil
+ return true
}
- iVal, err := parse(value)
- if err != nil {
- return err
+ iVal := parse(value)
+ if iVal == nil {
+ return false
}
cfg.next[key] = iVal
- return nil
+ return true
}
func (cfg *srvConfig) getListDescription(key string) (configDescription, string, int) {
for k, d := range cfg.descr {
if !strings.HasSuffix(k, "-") {
@@ -136,11 +131,11 @@
return d, k, num
}
return configDescription{}, "", -1
}
-func (cfg *srvConfig) GetCurConfig(key string) interface{} {
+func (cfg *srvConfig) GetConfig(key string) interface{} {
cfg.mxConfig.RLock()
defer cfg.mxConfig.RUnlock()
if cfg.cur == nil {
return cfg.next[key]
}
@@ -151,21 +146,39 @@
cfg.mxConfig.RLock()
defer cfg.mxConfig.RUnlock()
return cfg.next[key]
}
-func (cfg *srvConfig) GetCurConfigList(all bool) []kernel.KeyDescrValue {
- return cfg.getOneConfigList(all, cfg.GetCurConfig)
+func (cfg *srvConfig) GetConfigList(all bool) []kernel.KeyDescrValue {
+ return cfg.getConfigList(all, cfg.GetConfig)
}
func (cfg *srvConfig) GetNextConfigList() []kernel.KeyDescrValue {
- return cfg.getOneConfigList(true, cfg.GetNextConfig)
+ return cfg.getConfigList(true, cfg.GetNextConfig)
}
-func (cfg *srvConfig) getOneConfigList(all bool, getConfig func(string) interface{}) []kernel.KeyDescrValue {
+func (cfg *srvConfig) getConfigList(all bool, getConfig func(string) interface{}) []kernel.KeyDescrValue {
if len(cfg.descr) == 0 {
return nil
}
- keys := cfg.getSortedConfigKeys(all, getConfig)
+ keys := make([]string, 0, len(cfg.descr))
+ for k, descr := range cfg.descr {
+ if all || descr.canList {
+ if !strings.HasSuffix(k, "-") {
+ keys = append(keys, k)
+ continue
+ }
+ format := k + "%d"
+ for i := 1; ; i++ {
+ key := fmt.Sprintf(format, i)
+ val := getConfig(key)
+ if val == nil {
+ break
+ }
+ keys = append(keys, key)
+ }
+ }
+ }
+ sort.Strings(keys)
result := make([]kernel.KeyDescrValue, 0, len(keys))
for _, k := range keys {
val := getConfig(k)
if val == nil {
continue
@@ -181,32 +194,10 @@
})
}
return result
}
-func (cfg *srvConfig) getSortedConfigKeys(all bool, getConfig func(string) interface{}) []string {
- keys := make([]string, 0, len(cfg.descr))
- for k, descr := range cfg.descr {
- if all || descr.canList {
- if !strings.HasSuffix(k, "-") {
- keys = append(keys, k)
- continue
- }
- for i := 1; ; i++ {
- key := k + strconv.Itoa(i)
- val := getConfig(key)
- if val == nil {
- break
- }
- keys = append(keys, key)
- }
- }
- }
- sort.Strings(keys)
- return keys
-}
-
func (cfg *srvConfig) Freeze() {
cfg.mxConfig.Lock()
cfg.frozen = true
cfg.mxConfig.Unlock()
}
@@ -215,40 +206,24 @@
cfg.mxConfig.Lock()
defer cfg.mxConfig.Unlock()
cfg.cur = cfg.next.Clone()
}
-func parseString(val string) (any, error) { return val, nil }
+func parseString(val string) interface{} { return val }
-var errNoBoolean = errors.New("no boolean value")
-
-func parseBool(val string) (any, error) {
+func parseBool(val string) interface{} {
if val == "" {
- return false, errNoBoolean
+ return false
}
switch val[0] {
case '0', 'f', 'F', 'n', 'N':
- return false, nil
- }
- return true, nil
-}
-
-func parseInt64(val string) (any, error) {
- if u64, err := strconv.ParseInt(val, 10, 64); err == nil {
- return u64, nil
- } else {
- return nil, err
- }
-}
-
-func parseZid(val string) (any, error) {
- if zid, err := id.Parse(val); err == nil {
- return zid, nil
- } else {
- return id.Invalid, err
- }
-}
-
-func parseInvalidZid(val string) (any, error) {
- zid, _ := id.Parse(val)
- return zid, nil
+ return false
+ }
+ return true
+}
+
+func parseZid(val string) interface{} {
+ if zid, err := id.Parse(val); err == nil {
+ return zid
+ }
+ return id.Invalid
}
Index: kernel/impl/core.go
==================================================================
--- kernel/impl/core.go
+++ kernel/impl/core.go
@@ -1,33 +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 impl provides the kernel implementation.
package impl
import (
"fmt"
"net"
"os"
"runtime"
+ "sort"
"sync"
"time"
- "t73f.de/r/zsc/maps"
"zettelstore.de/z/kernel"
- "zettelstore.de/z/logger"
"zettelstore.de/z/strfun"
- "zettelstore.de/z/zettel/id"
)
type coreService struct {
srvConfig
started bool
@@ -40,75 +36,72 @@
ts time.Time
info interface{}
stack []byte
}
-func (cs *coreService) Initialize(logger *logger.Logger) {
- cs.logger = logger
+func (cs *coreService) Initialize() {
cs.mapRecover = make(map[string]recoverInfo)
cs.descr = descriptionMap{
- kernel.CoreDebug: {"Debug mode", parseBool, false},
kernel.CoreGoArch: {"Go processor architecture", nil, false},
kernel.CoreGoOS: {"Go Operating System", nil, false},
kernel.CoreGoVersion: {"Go Version", nil, false},
kernel.CoreHostname: {"Host name", nil, false},
kernel.CorePort: {
"Port of command line server",
- cs.noFrozen(func(val string) (any, error) {
+ cs.noFrozen(func(val string) interface{} {
port, err := net.LookupPort("tcp", val)
if err != nil {
- return nil, err
+ return nil
}
- return port, nil
+ return port
}),
true,
},
kernel.CoreProgname: {"Program name", nil, false},
- kernel.CoreStarted: {"Start time", nil, false},
kernel.CoreVerbose: {"Verbose output", parseBool, true},
kernel.CoreVersion: {
"Version",
- cs.noFrozen(func(val string) (any, error) {
+ cs.noFrozen(func(val string) interface{} {
if val == "" {
- return kernel.CoreDefaultVersion, nil
+ return "unknown"
}
- return val, nil
+ return val
}),
false,
},
- kernel.CoreVTime: {"Version time", nil, false},
}
cs.next = interfaceMap{
- kernel.CoreDebug: false,
kernel.CoreGoArch: runtime.GOARCH,
kernel.CoreGoOS: runtime.GOOS,
kernel.CoreGoVersion: runtime.Version(),
kernel.CoreHostname: "*unknown host*",
kernel.CorePort: 0,
- kernel.CoreStarted: time.Now().Local().Format(id.TimestampLayout),
kernel.CoreVerbose: false,
}
if hn, err := os.Hostname(); err == nil {
cs.next[kernel.CoreHostname] = hn
}
}
-func (cs *coreService) GetLogger() *logger.Logger { return cs.logger }
-
-func (cs *coreService) Start(*myKernel) error {
+func (cs *coreService) Start(kern *myKernel) error {
cs.started = true
return nil
}
func (cs *coreService) IsStarted() bool { return cs.started }
-func (cs *coreService) Stop(*myKernel) {
+func (cs *coreService) Stop(*myKernel) error {
cs.started = false
+ return nil
}
func (cs *coreService) GetStatistics() []kernel.KeyValue {
cs.mxRecover.RLock()
defer cs.mxRecover.RUnlock()
- names := maps.Keys(cs.mapRecover)
+ names := make([]string, 0, len(cs.mapRecover))
+ for n := range cs.mapRecover {
+ names = append(names, n)
+ }
+ sort.Strings(names)
result := make([]kernel.KeyValue, 0, 3*len(names))
for _, n := range names {
ri := cs.mapRecover[n]
result = append(
result,
@@ -148,11 +141,11 @@
func (cs *coreService) updateRecoverInfo(name string, recoverInfo interface{}, stack []byte) {
cs.mxRecover.Lock()
ri := cs.mapRecover[name]
ri.count++
- ri.ts = time.Now().Local()
+ ri.ts = time.Now()
ri.info = recoverInfo
ri.stack = stack
cs.mapRecover[name] = ri
cs.mxRecover.Unlock()
}
Index: kernel/impl/impl.go
==================================================================
--- kernel/impl/impl.go
+++ kernel/impl/impl.go
@@ -1,123 +1,94 @@
//-----------------------------------------------------------------------------
-// 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 kernel implementation.
package impl
import (
- "errors"
"fmt"
"io"
+ "log"
"net"
"os"
"os/signal"
- "runtime"
"runtime/debug"
- "runtime/pprof"
"strconv"
- "strings"
"sync"
"syscall"
- "time"
"zettelstore.de/z/kernel"
- "zettelstore.de/z/logger"
- "zettelstore.de/z/zettel/id"
)
// myKernel is the main internal kernel.
type myKernel struct {
- logWriter *kernelLogWriter
- logger *logger.Logger
+ // started bool
wg sync.WaitGroup
mx sync.RWMutex
interrupt chan os.Signal
-
- profileName string
- fileName string
- profileFile *os.File
- profile *pprof.Profile
-
- self kernelService
- core coreService
- cfg configService
- auth authService
- box boxService
- web webService
+ debug bool
+
+ core coreService
+ cfg configService
+ auth authService
+ place placeService
+ web webService
srvs map[kernel.Service]serviceDescr
srvNames map[string]serviceData
depStart serviceDependency
depStop serviceDependency // reverse of depStart
}
type serviceDescr struct {
- srv service
- name string
- logLevel logger.Level
+ srv service
+ name string
}
type serviceData struct {
srv service
srvnum kernel.Service
}
type serviceDependency map[kernel.Service][]kernel.Service
-const (
- defaultNormalLogLevel = logger.InfoLevel
- defaultSimpleLogLevel = logger.ErrorLevel
-)
-
-// create a new kernel.
+// create and start a new kernel.
func init() {
- kernel.Main = createKernel()
+ kernel.Main = createAndStart()
}
-// create a new kernel.
-func createKernel() kernel.Kernel {
- lw := newKernelLogWriter(8192)
+// create and start a new kernel.
+func createAndStart() kernel.Kernel {
kern := &myKernel{
- logWriter: lw,
- logger: logger.New(lw, "").SetLevel(defaultNormalLogLevel),
interrupt: make(chan os.Signal, 5),
}
- kern.self.kernel = kern
kern.srvs = map[kernel.Service]serviceDescr{
- kernel.KernelService: {&kern.self, "kernel", defaultNormalLogLevel},
- kernel.CoreService: {&kern.core, "core", defaultNormalLogLevel},
- kernel.ConfigService: {&kern.cfg, "config", defaultNormalLogLevel},
- kernel.AuthService: {&kern.auth, "auth", defaultNormalLogLevel},
- kernel.BoxService: {&kern.box, "box", defaultNormalLogLevel},
- kernel.WebService: {&kern.web, "web", defaultNormalLogLevel},
+ kernel.CoreService: {&kern.core, "core"},
+ kernel.ConfigService: {&kern.cfg, "config"},
+ kernel.AuthService: {&kern.auth, "auth"},
+ kernel.PlaceService: {&kern.place, "place"},
+ kernel.WebService: {&kern.web, "web"},
}
kern.srvNames = make(map[string]serviceData, len(kern.srvs))
for key, srvD := range kern.srvs {
if _, ok := kern.srvNames[srvD.name]; ok {
- kern.logger.Error().Str("service", srvD.name).Msg("Service data already set, ignore")
+ panic(fmt.Sprintf("Key %q already given for service %v", key, srvD.name))
}
kern.srvNames[srvD.name] = serviceData{srvD.srv, key}
- l := logger.New(lw, strings.ToUpper(srvD.name)).SetLevel(srvD.logLevel)
- kern.logger.Debug().Str("service", srvD.name).Msg("Initialize")
- srvD.srv.Initialize(l)
+ srvD.srv.Initialize()
}
kern.depStart = serviceDependency{
- kernel.KernelService: nil,
- kernel.CoreService: {kernel.KernelService},
+ kernel.CoreService: nil,
kernel.ConfigService: {kernel.CoreService},
kernel.AuthService: {kernel.CoreService},
- kernel.BoxService: {kernel.CoreService, kernel.ConfigService, kernel.AuthService},
- kernel.WebService: {kernel.ConfigService, kernel.AuthService, kernel.BoxService},
+ kernel.PlaceService: {kernel.CoreService, kernel.ConfigService, kernel.AuthService},
+ kernel.WebService: {kernel.ConfigService, kernel.AuthService, kernel.PlaceService},
}
kern.depStop = make(serviceDependency, len(kern.depStart))
for srv, deps := range kern.depStart {
for _, dep := range deps {
kern.depStop[dep] = append(kern.depStop[dep], srv)
@@ -124,84 +95,68 @@
}
}
return kern
}
-func (kern *myKernel) Setup(progname, version string, versionTime time.Time) {
- kern.SetConfig(kernel.CoreService, kernel.CoreProgname, progname)
- kern.SetConfig(kernel.CoreService, kernel.CoreVersion, version)
- kern.SetConfig(kernel.CoreService, kernel.CoreVTime, versionTime.Local().Format(id.TimestampLayout))
-}
-
-func (kern *myKernel) Start(headline, lineServer bool, configFilename string) {
+func (kern *myKernel) Start(headline bool) {
for _, srvD := range kern.srvs {
srvD.srv.Freeze()
}
- if kern.cfg.GetCurConfig(kernel.ConfigSimpleMode).(bool) {
- kern.SetLogLevel(defaultSimpleLogLevel.String())
- }
kern.wg.Add(1)
signal.Notify(kern.interrupt, os.Interrupt, syscall.SIGTERM)
go func() {
// Wait for interrupt.
sig := <-kern.interrupt
if strSig := sig.String(); strSig != "" {
- kern.logger.Info().Str("signal", strSig).Msg("Shut down Zettelstore")
+ kern.doLog("Shut down Zettelstore:", strSig)
}
- kern.doShutdown()
+ kern.shutdown()
kern.wg.Done()
}()
- kern.StartService(kernel.KernelService)
- if headline {
- logger := kern.logger
- logger.Mandatory().Msg(fmt.Sprintf(
- "%v %v (%v@%v/%v)",
- kern.core.GetCurConfig(kernel.CoreProgname),
- kern.core.GetCurConfig(kernel.CoreVersion),
- kern.core.GetCurConfig(kernel.CoreGoVersion),
- kern.core.GetCurConfig(kernel.CoreGoOS),
- kern.core.GetCurConfig(kernel.CoreGoArch),
- ))
- logger.Mandatory().Msg("Licensed under the latest version of the EUPL (European Union Public License)")
- if configFilename != "" {
- logger.Mandatory().Str("filename", configFilename).Msg("Configuration file found")
- } else {
- logger.Mandatory().Msg("No configuration file found / used")
- }
- if kern.core.GetCurConfig(kernel.CoreDebug).(bool) {
- logger.Info().Msg("----------------------------------------")
- logger.Info().Msg("DEBUG MODE, DO NO USE THIS IN PRODUCTION")
- logger.Info().Msg("----------------------------------------")
- }
- if kern.auth.GetCurConfig(kernel.AuthReadonly).(bool) {
- logger.Info().Msg("Read-only mode")
- }
- }
- if lineServer {
- port := kern.core.GetNextConfig(kernel.CorePort).(int)
- if port > 0 {
- listenAddr := net.JoinHostPort("127.0.0.1", strconv.Itoa(port))
- startLineServer(kern, listenAddr)
- }
- }
-}
-
-func (kern *myKernel) doShutdown() {
- kern.StopService(kernel.KernelService) // Will stop all other services.
+ kern.StartService(kernel.CoreService)
+ if headline {
+ kern.doLog(fmt.Sprintf(
+ "%v %v (%v@%v/%v)",
+ kern.core.GetConfig(kernel.CoreProgname),
+ kern.core.GetConfig(kernel.CoreVersion),
+ kern.core.GetConfig(kernel.CoreGoVersion),
+ kern.core.GetConfig(kernel.CoreGoOS),
+ kern.core.GetConfig(kernel.CoreGoArch),
+ ))
+ kern.doLog("Licensed under the latest version of the EUPL (European Union Public License)")
+ if kern.auth.GetConfig(kernel.AuthReadonly).(bool) {
+ kern.doLog("Read-only mode")
+ }
+ }
+ port := kern.core.GetNextConfig(kernel.CorePort).(int)
+ if port > 0 {
+ listenAddr := net.JoinHostPort("127.0.0.1", strconv.Itoa(port))
+ startLineServer(kern, listenAddr)
+ }
+}
+
+func (kern *myKernel) shutdown() {
+ kern.StopService(kernel.CoreService) // Will stop all other services.
}
func (kern *myKernel) WaitForShutdown() {
kern.wg.Wait()
- kern.doStopProfiling()
+}
+
+func (kern *myKernel) SetDebug(enable bool) bool {
+ kern.mx.Lock()
+ prevDebug := kern.debug
+ kern.debug = enable
+ kern.mx.Unlock()
+ return prevDebug
}
// --- Shutdown operation ----------------------------------------------------
// Shutdown the service. Waits for all concurrent activity to stop.
func (kern *myKernel) Shutdown(silent bool) {
- kern.logger.Trace().Msg("Shutdown")
kern.interrupt <- &shutdownSignal{silent: silent}
}
type shutdownSignal struct{ silent bool }
@@ -209,210 +164,73 @@
if s.silent {
return ""
}
return "shutdown"
}
-func (*shutdownSignal) Signal() { /* Just a signal */ }
+func (s *shutdownSignal) Signal() { /* Just a signal */ }
// --- Log operation ---------------------------------------------------------
-func (kern *myKernel) GetKernelLogger() *logger.Logger {
- return kern.logger
-}
-
-func (kern *myKernel) SetLogLevel(logLevel string) {
- defaultLevel, srvLevel := kern.parseLogLevel(logLevel)
-
- kern.mx.RLock()
- defer kern.mx.RUnlock()
- for srvN, srvD := range kern.srvs {
- if lvl, found := srvLevel[srvN]; found {
- srvD.srv.GetLogger().SetLevel(lvl)
- } else if defaultLevel != logger.NoLevel {
- srvD.srv.GetLogger().SetLevel(defaultLevel)
- }
- }
-}
-
-func (kern *myKernel) parseLogLevel(logLevel string) (logger.Level, map[kernel.Service]logger.Level) {
- defaultLevel := logger.NoLevel
- srvLevel := map[kernel.Service]logger.Level{}
- for _, spec := range strings.Split(logLevel, ";") {
- vals := cleanLogSpec(strings.Split(spec, ":"))
- switch len(vals) {
- case 0:
- case 1:
- if lvl := logger.ParseLevel(vals[0]); lvl.IsValid() {
- defaultLevel = lvl
- }
- default:
- serviceText, levelText := vals[0], vals[1]
- if srv, found := kern.srvNames[serviceText]; found {
- if lvl := logger.ParseLevel(levelText); lvl.IsValid() {
- srvLevel[srv.srvnum] = lvl
- }
- }
- }
- }
- return defaultLevel, srvLevel
-}
-
-func cleanLogSpec(rawVals []string) []string {
- vals := make([]string, 0, len(rawVals))
- for _, rVal := range rawVals {
- val := strings.TrimSpace(rVal)
- if val != "" {
- vals = append(vals, val)
- }
- }
- return vals
-}
-
-func (kern *myKernel) RetrieveLogEntries() []kernel.LogEntry {
- return kern.logWriter.retrieveLogEntries()
-}
-
-func (kern *myKernel) GetLastLogTime() time.Time {
- return kern.logWriter.getLastLogTime()
+// Log some activity.
+func (kern *myKernel) Log(args ...interface{}) {
+ kern.mx.Lock()
+ defer kern.mx.Unlock()
+ kern.doLog(args...)
+}
+func (kern *myKernel) doLog(args ...interface{}) {
+ log.Println(args...)
}
// LogRecover outputs some information about the previous panic.
func (kern *myKernel) LogRecover(name string, recoverInfo interface{}) bool {
return kern.doLogRecover(name, recoverInfo)
}
func (kern *myKernel) doLogRecover(name string, recoverInfo interface{}) bool {
+ kern.Log(name, "recovered from:", recoverInfo)
stack := debug.Stack()
- kern.logger.Error().Str("recovered_from", fmt.Sprint(recoverInfo)).Bytes("stack", stack).Msg(name)
+ os.Stderr.Write(stack)
kern.core.updateRecoverInfo(name, recoverInfo, stack)
return true
}
-// --- Profiling ---------------------------------------------------------
-
-var errProfileInWork = errors.New("already profiling")
-var errProfileNotFound = errors.New("profile not found")
-
-func (kern *myKernel) StartProfiling(profileName, fileName string) error {
- kern.mx.Lock()
- defer kern.mx.Unlock()
- return kern.doStartProfiling(profileName, fileName)
-}
-func (kern *myKernel) doStartProfiling(profileName, fileName string) error {
- if kern.profileName != "" {
- return errProfileInWork
- }
- if profileName == kernel.ProfileCPU {
- f, err := os.Create(fileName)
- if err != nil {
- return err
- }
- err = pprof.StartCPUProfile(f)
- if err != nil {
- f.Close()
- return err
- }
- kern.profileName = profileName
- kern.fileName = fileName
- kern.profileFile = f
- return nil
- }
- profile := pprof.Lookup(profileName)
- if profile == nil {
- return errProfileNotFound
- }
- f, err := os.Create(fileName)
- if err != nil {
- return err
- }
- kern.profileName = profileName
- kern.fileName = fileName
- kern.profile = profile
- kern.profileFile = f
- runtime.GC() // get up-to-date statistics
- profile.WriteTo(f, 0)
- return nil
-}
-
-func (kern *myKernel) StopProfiling() error {
- kern.mx.Lock()
- defer kern.mx.Unlock()
- return kern.doStopProfiling()
-}
-func (kern *myKernel) doStopProfiling() error {
- if kern.profileName == "" {
- return nil // No profile started
- }
- if kern.profileName == kernel.ProfileCPU {
- pprof.StopCPUProfile()
- }
- err := kern.profileFile.Close()
- kern.profileName = ""
- kern.fileName = ""
- kern.profile = nil
- kern.profileFile = nil
- return err
-}
-
// --- Service handling --------------------------------------------------
-var errUnknownService = errors.New("unknown service")
-
-func (kern *myKernel) SetConfig(srvnum kernel.Service, key, value string) error {
+func (kern *myKernel) SetConfig(srvnum kernel.Service, key, value string) bool {
kern.mx.Lock()
defer kern.mx.Unlock()
if srvD, ok := kern.srvs[srvnum]; ok {
return srvD.srv.SetConfig(key, value)
}
- return errUnknownService
+ return false
}
func (kern *myKernel) GetConfig(srvnum kernel.Service, key string) interface{} {
kern.mx.RLock()
defer kern.mx.RUnlock()
if srvD, ok := kern.srvs[srvnum]; ok {
- return srvD.srv.GetCurConfig(key)
+ return srvD.srv.GetConfig(key)
}
return nil
}
func (kern *myKernel) GetConfigList(srvnum kernel.Service) []kernel.KeyDescrValue {
kern.mx.RLock()
defer kern.mx.RUnlock()
if srvD, ok := kern.srvs[srvnum]; ok {
- return srvD.srv.GetCurConfigList(false)
+ return srvD.srv.GetConfigList(false)
}
return nil
}
-
func (kern *myKernel) GetServiceStatistics(srvnum kernel.Service) []kernel.KeyValue {
kern.mx.RLock()
defer kern.mx.RUnlock()
if srvD, ok := kern.srvs[srvnum]; ok {
return srvD.srv.GetStatistics()
}
return nil
}
-func (kern *myKernel) GetLogger(srvnum kernel.Service) *logger.Logger {
- kern.mx.RLock()
- defer kern.mx.RUnlock()
- if srvD, ok := kern.srvs[srvnum]; ok {
- return srvD.srv.GetLogger()
- }
- return kern.GetKernelLogger()
-}
-
-func (kern *myKernel) SetLevel(srvnum kernel.Service, level logger.Level) {
- if level.IsValid() {
- kern.mx.RLock()
- if srvD, ok := kern.srvs[srvnum]; ok {
- srvD.srv.GetLogger().SetLevel(level)
- }
- kern.mx.RUnlock()
- }
-}
-
func (kern *myKernel) StartService(srvnum kernel.Service) error {
kern.mx.RLock()
defer kern.mx.RUnlock()
return kern.doStartService(srvnum)
}
@@ -432,11 +250,13 @@
return kern.doRestartService(srvnum)
}
func (kern *myKernel) doRestartService(srvnum kernel.Service) error {
deps := kern.sortDependency(srvnum, kern.depStop, false)
for _, srv := range deps {
- srv.Stop(kern)
+ if err := srv.Stop(kern); err != nil {
+ return err
+ }
}
for i := len(deps) - 1; i >= 0; i-- {
srv := deps[i]
if err := srv.Start(kern); err != nil {
return err
@@ -445,18 +265,19 @@
}
return nil
}
func (kern *myKernel) StopService(srvnum kernel.Service) error {
- kern.mx.Lock()
- defer kern.mx.Unlock()
+ kern.mx.RLock()
+ defer kern.mx.RUnlock()
return kern.doStopService(srvnum)
}
-
func (kern *myKernel) doStopService(srvnum kernel.Service) error {
for _, srv := range kern.sortDependency(srvnum, kern.depStop, false) {
- srv.Stop(kern)
+ if err := srv.Stop(kern); err != nil {
+ return err
+ }
}
return nil
}
func (kern *myKernel) sortDependency(
@@ -470,12 +291,12 @@
}
if srvD.srv.IsStarted() == isStarted {
return nil
}
deps := srvdeps[srvnum]
- found := make(map[service]bool, 8)
- result := make([]service, 0, len(found))
+ found := make(map[service]bool, 4)
+ result := make([]service, 0, 4)
for _, dep := range deps {
srvDeps := kern.sortDependency(dep, srvdeps, isStarted)
for _, depSrv := range srvDeps {
if !found[depSrv] {
result = append(result, depSrv)
@@ -483,36 +304,32 @@
}
}
}
return append(result, srvD.srv)
}
-
func (kern *myKernel) DumpIndex(w io.Writer) {
- kern.box.DumpIndex(w)
+ kern.place.DumpIndex(w)
}
type service interface {
// Initialize the data for the service.
- Initialize(*logger.Logger)
-
- // Get service logger.
- GetLogger() *logger.Logger
+ Initialize()
// ConfigDescriptions returns a sorted list of configuration descriptions.
ConfigDescriptions() []serviceConfigDescription
// SetConfig stores a configuration value.
- SetConfig(key, value string) error
+ SetConfig(key, value string) bool
- // GetCurConfig returns the current configuration value.
- GetCurConfig(key string) interface{}
+ // GetConfig returns the current configuration value.
+ GetConfig(key string) interface{}
// GetNextConfig returns the next configuration value.
GetNextConfig(key string) interface{}
- // GetCurConfigList returns a sorted list of current configuration data.
- GetCurConfigList(all bool) []kernel.KeyDescrValue
+ // GetConfigList returns a sorted list of current configuration data.
+ GetConfigList(all bool) []kernel.KeyDescrValue
// GetNextConfigList returns a sorted list of next configuration data.
GetNextConfigList() []kernel.KeyDescrValue
// GetStatistics returns a key/value list of statistical data.
@@ -529,40 +346,19 @@
// IsStarted returns true if the service was started successfully.
IsStarted() bool
// Stop the service.
- Stop(*myKernel)
+ Stop(*myKernel) error
}
type serviceConfigDescription struct{ Key, Descr string }
func (kern *myKernel) SetCreators(
createAuthManager kernel.CreateAuthManagerFunc,
- createBoxManager kernel.CreateBoxManagerFunc,
+ createPlaceManager kernel.CreatePlaceManagerFunc,
setupWebServer kernel.SetupWebServerFunc,
) {
kern.auth.createManager = createAuthManager
- kern.box.createManager = createBoxManager
+ kern.place.createManager = createPlaceManager
kern.web.setupServer = setupWebServer
}
-
-// --- The kernel as a service -------------------------------------------
-
-type kernelService struct {
- kernel *myKernel
-}
-
-func (*kernelService) Initialize(*logger.Logger) {}
-func (ks *kernelService) GetLogger() *logger.Logger { return ks.kernel.logger }
-func (*kernelService) ConfigDescriptions() []serviceConfigDescription { return nil }
-func (*kernelService) SetConfig(key, value string) error { return errAlreadyFrozen }
-func (*kernelService) GetCurConfig(key string) interface{} { return nil }
-func (*kernelService) GetNextConfig(key string) interface{} { return nil }
-func (*kernelService) GetCurConfigList(all bool) []kernel.KeyDescrValue { return nil }
-func (*kernelService) GetNextConfigList() []kernel.KeyDescrValue { return nil }
-func (*kernelService) GetStatistics() []kernel.KeyValue { return nil }
-func (*kernelService) Freeze() {}
-func (*kernelService) Start(*myKernel) error { return nil }
-func (*kernelService) SwitchNextToCur() {}
-func (*kernelService) IsStarted() bool { return true }
-func (*kernelService) Stop(*myKernel) {}
DELETED kernel/impl/log.go
Index: kernel/impl/log.go
==================================================================
--- kernel/impl/log.go
+++ kernel/impl/log.go
@@ -1,158 +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 impl
-
-import (
- "os"
- "sync"
- "time"
-
- "zettelstore.de/z/kernel"
- "zettelstore.de/z/logger"
-)
-
-// kernelLogWriter adapts an io.Writer to a LogWriter
-type kernelLogWriter struct {
- mx sync.RWMutex // protects buf, serializes w.Write and retrieveLogEntries
- lastLog time.Time
- buf []byte
- writePos int
- data []logEntry
- full bool
-}
-
-// newKernelLogWriter creates a new LogWriter for kernel logging.
-func newKernelLogWriter(capacity int) *kernelLogWriter {
- if capacity < 1 {
- capacity = 1
- }
- return &kernelLogWriter{
- lastLog: time.Now(),
- buf: make([]byte, 0, 500),
- data: make([]logEntry, capacity),
- }
-}
-
-func (klw *kernelLogWriter) WriteMessage(level logger.Level, ts time.Time, prefix, msg string, details []byte) error {
- klw.mx.Lock()
-
- if level > logger.DebugLevel {
- klw.lastLog = ts
- klw.data[klw.writePos] = logEntry{
- level: level,
- ts: ts,
- prefix: prefix,
- msg: msg,
- details: append([]byte(nil), details...),
- }
- klw.writePos++
- if klw.writePos >= cap(klw.data) {
- klw.writePos = 0
- klw.full = true
- }
- }
-
- klw.buf = klw.buf[:0]
- buf := klw.buf
- addTimestamp(&buf, ts)
- buf = append(buf, ' ')
- buf = append(buf, level.Format()...)
- buf = append(buf, ' ')
- if prefix != "" {
- buf = append(buf, prefix...)
- buf = append(buf, ' ')
- }
- buf = append(buf, msg...)
- buf = append(buf, details...)
- buf = append(buf, '\n')
- _, err := os.Stdout.Write(buf)
-
- klw.mx.Unlock()
- return err
-}
-
-func addTimestamp(buf *[]byte, ts time.Time) {
- year, month, day := ts.Date()
- itoa(buf, year, 4)
- *buf = append(*buf, '-')
- itoa(buf, int(month), 2)
- *buf = append(*buf, '-')
- itoa(buf, day, 2)
- *buf = append(*buf, ' ')
- hour, minute, second := ts.Clock()
- itoa(buf, hour, 2)
- *buf = append(*buf, ':')
- itoa(buf, minute, 2)
- *buf = append(*buf, ':')
- itoa(buf, second, 2)
-
-}
-
-func itoa(buf *[]byte, i, wid int) {
- var b [20]byte
- for bp := wid - 1; bp >= 0; bp-- {
- q := i / 10
- b[bp] = byte('0' + i - q*10)
- i = q
- }
- *buf = append(*buf, b[:wid]...)
-}
-
-type logEntry struct {
- level logger.Level
- ts time.Time
- prefix string
- msg string
- details []byte
-}
-
-func (klw *kernelLogWriter) retrieveLogEntries() []kernel.LogEntry {
- klw.mx.RLock()
- defer klw.mx.RUnlock()
-
- if !klw.full {
- if klw.writePos == 0 {
- return nil
- }
- result := make([]kernel.LogEntry, klw.writePos)
- for i := range klw.writePos {
- copyE2E(&result[i], &klw.data[i])
- }
- return result
- }
- result := make([]kernel.LogEntry, cap(klw.data))
- pos := 0
- for j := klw.writePos; j < cap(klw.data); j++ {
- copyE2E(&result[pos], &klw.data[j])
- pos++
- }
- for j := range klw.writePos {
- copyE2E(&result[pos], &klw.data[j])
- pos++
- }
- return result
-}
-
-func (klw *kernelLogWriter) getLastLogTime() time.Time {
- klw.mx.RLock()
- defer klw.mx.RUnlock()
- return klw.lastLog
-}
-
-func copyE2E(result *kernel.LogEntry, origin *logEntry) {
- result.Level = origin.level
- result.TS = origin.ts
- result.Prefix = origin.prefix
- result.Message = origin.msg + string(origin.details)
-}
ADDED kernel/impl/place.go
Index: kernel/impl/place.go
==================================================================
--- kernel/impl/place.go
+++ kernel/impl/place.go
@@ -0,0 +1,130 @@
+//-----------------------------------------------------------------------------
+// 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 impl provides the kernel implementation.
+package impl
+
+import (
+ "context"
+ "fmt"
+ "io"
+ "net/url"
+ "sync"
+
+ "zettelstore.de/z/kernel"
+ "zettelstore.de/z/place"
+)
+
+type placeService struct {
+ srvConfig
+ mxService sync.RWMutex
+ manager place.Manager
+ createManager kernel.CreatePlaceManagerFunc
+}
+
+func (ps *placeService) Initialize() {
+ ps.descr = descriptionMap{
+ kernel.PlaceDefaultDirType: {
+ "Default directory place type",
+ ps.noFrozen(func(val string) interface{} {
+ switch val {
+ case kernel.PlaceDirTypeNotify, kernel.PlaceDirTypeSimple:
+ return val
+ }
+ return nil
+ }),
+ true,
+ },
+ kernel.PlaceURIs: {
+ "Place URI",
+ func(val string) interface{} {
+ uVal, err := url.Parse(val)
+ if err != nil {
+ return nil
+ }
+ if uVal.Scheme == "" {
+ uVal.Scheme = "dir"
+ }
+ return uVal
+ },
+ true,
+ },
+ }
+ ps.next = interfaceMap{
+ kernel.PlaceDefaultDirType: kernel.PlaceDirTypeNotify,
+ }
+}
+
+func (ps *placeService) Start(kern *myKernel) error {
+ placeURIs := make([]*url.URL, 0, 4)
+ format := kernel.PlaceURIs + "%d"
+ for i := 1; ; i++ {
+ u := ps.GetNextConfig(fmt.Sprintf(format, i))
+ if u == nil {
+ break
+ }
+ placeURIs = append(placeURIs, u.(*url.URL))
+ }
+ ps.mxService.Lock()
+ defer ps.mxService.Unlock()
+ mgr, err := ps.createManager(placeURIs, kern.auth.manager, kern.cfg.rtConfig)
+ if err != nil {
+ kern.doLog("Unable to create place manager:", err)
+ return err
+ }
+ kern.doLog("Start Place Manager:", mgr.Location())
+ if err := mgr.Start(context.Background()); err != nil {
+ kern.doLog("Unable to start place manager:", err)
+ }
+ kern.cfg.setPlace(mgr)
+ ps.manager = mgr
+ return nil
+}
+
+func (ps *placeService) IsStarted() bool {
+ ps.mxService.RLock()
+ defer ps.mxService.RUnlock()
+ return ps.manager != nil
+}
+
+func (ps *placeService) Stop(kern *myKernel) error {
+ kern.doLog("Stop Place Manager")
+ ps.mxService.RLock()
+ mgr := ps.manager
+ ps.mxService.RUnlock()
+ err := mgr.Stop(context.Background())
+ ps.mxService.Lock()
+ ps.manager = nil
+ ps.mxService.Unlock()
+ return err
+}
+
+func (ps *placeService) GetStatistics() []kernel.KeyValue {
+ var st place.Stats
+ ps.mxService.RLock()
+ ps.manager.ReadStats(&st)
+ ps.mxService.RUnlock()
+ return []kernel.KeyValue{
+ {Key: "Read-only", Value: fmt.Sprintf("%v", st.ReadOnly)},
+ {Key: "Sub-places", Value: fmt.Sprintf("%v", st.NumManagedPlaces)},
+ {Key: "Zettel (total)", Value: fmt.Sprintf("%v", st.ZettelTotal)},
+ {Key: "Zettel (indexed)", Value: fmt.Sprintf("%v", st.ZettelIndexed)},
+ {Key: "Last re-index", Value: st.LastReload.Format("2006-01-02 15:04:05 -0700 MST")},
+ {Key: "Indexes since last re-index", Value: fmt.Sprintf("%v", st.IndexesSinceReload)},
+ {Key: "Duration last index", Value: fmt.Sprintf("%vms", st.DurLastIndex.Milliseconds())},
+ {Key: "Indexed words", Value: fmt.Sprintf("%v", st.IndexedWords)},
+ {Key: "Indexed URLs", Value: fmt.Sprintf("%v", st.IndexedUrls)},
+ {Key: "Zettel enrichments", Value: fmt.Sprintf("%v", st.IndexUpdates)},
+ }
+}
+
+func (ps *placeService) DumpIndex(w io.Writer) {
+ ps.manager.Dump(w)
+}
Index: kernel/impl/server.go
==================================================================
--- kernel/impl/server.go
+++ kernel/impl/server.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 impl provides the kernel implementation.
package impl
import (
"bufio"
"net"
@@ -19,32 +17,32 @@
)
func startLineServer(kern *myKernel, listenAddr string) error {
ln, err := net.Listen("tcp", listenAddr)
if err != nil {
- kern.logger.Error().Err(err).Msg("Unable to start administration console")
+ kern.doLog("Unable to start Line Command Server:", err)
return err
}
- kern.logger.Mandatory().Str("listen", listenAddr).Msg("Start administration console")
+ kern.doLog("Start Line Command Server:", listenAddr)
go func() { lineServer(ln, kern) }()
return nil
}
func lineServer(ln net.Listener, kern *myKernel) {
// Something may panic. Ensure a running line service.
defer func() {
- if ri := recover(); ri != nil {
- kern.doLogRecover("Line", ri)
+ if r := recover(); r != nil {
+ kern.doLogRecover("Line", r)
go lineServer(ln, kern)
}
}()
for {
conn, err := ln.Accept()
if err != nil {
// handle error
- kern.logger.Error().Err(err).Msg("Unable to accept connection")
+ kern.doLog("Unable to accept connection:", err)
break
}
go handleLineConnection(conn, kern)
}
ln.Close()
@@ -51,17 +49,16 @@
}
func handleLineConnection(conn net.Conn, kern *myKernel) {
// Something may panic. Ensure a running connection.
defer func() {
- if ri := recover(); ri != nil {
- kern.doLogRecover("LineConn", ri)
+ if r := recover(); r != nil {
+ kern.doLogRecover("LineConn", r)
go handleLineConnection(conn, kern)
}
}()
- kern.logger.Mandatory().Str("from", conn.RemoteAddr().String()).Msg("Start session on administration console")
cmds := cmdSession{}
cmds.initialize(conn, kern)
s := bufio.NewScanner(conn)
for s.Scan() {
line := s.Text()
Index: kernel/impl/web.go
==================================================================
--- kernel/impl/web.go
+++ kernel/impl/web.go
@@ -1,34 +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 impl provides the kernel implementation.
package impl
import (
- "errors"
"net"
- "net/netip"
- "net/url"
- "os"
- "path/filepath"
"strconv"
- "strings"
"sync"
"time"
"zettelstore.de/z/kernel"
- "zettelstore.de/z/logger"
"zettelstore.de/z/web/server"
"zettelstore.de/z/web/server/impl"
)
type webService struct {
@@ -36,53 +27,35 @@
mxService sync.RWMutex
srvw server.Server
setupServer kernel.SetupWebServerFunc
}
-var errURLPrefixSyntax = errors.New("must not be empty and must start with '//'")
-
-func (ws *webService) Initialize(logger *logger.Logger) {
- ws.logger = logger
- ws.descr = descriptionMap{
- kernel.WebAssetDir: {
- "Asset file directory",
- func(val string) (any, error) {
- val = filepath.Clean(val)
- if finfo, err := os.Stat(val); err == nil && finfo.IsDir() {
- return val, nil
- } else {
- return nil, err
- }
- },
- true,
- },
- kernel.WebBaseURL: {
- "Base URL",
- func(val string) (any, error) {
- if _, err := url.Parse(val); err != nil {
- return nil, err
- }
- return val, nil
- },
- true,
- },
+// Constants for web service keys.
+const (
+ WebSecureCookie = "secure"
+ WebListenAddress = "listen"
+ WebPersistentCookie = "persistent"
+ WebTokenLifetimeAPI = "api-lifetime"
+ WebTokenLifetimeHTML = "html-lifetime"
+ WebURLPrefix = "prefix"
+)
+
+func (ws *webService) Initialize() {
+ ws.descr = descriptionMap{
kernel.WebListenAddress: {
"Listen address",
- func(val string) (any, error) {
- // If there is no host, prepend 127.0.0.1 as host.
- host, _, err := net.SplitHostPort(val)
- if err == nil && host == "" {
- val = "127.0.0.1" + val
- }
- ap, err := netip.ParseAddrPort(val)
- if err != nil {
- return "", err
- }
- return ap.String(), nil
+ func(val string) interface{} {
+ host, port, err := net.SplitHostPort(val)
+ if err != nil {
+ return nil
+ }
+ if _, err := net.LookupPort("tcp", port); err != nil {
+ return nil
+ }
+ return net.JoinHostPort(host, port)
},
true},
- kernel.WebMaxRequestSize: {"Max Request Size", parseInt64, true},
kernel.WebPersistentCookie: {"Persistent cookie", parseBool, true},
kernel.WebSecureCookie: {"Secure cookie", parseBool, true},
kernel.WebTokenLifetimeAPI: {
"Token lifetime API",
makeDurationParser(10*time.Minute, 0, 1*time.Hour),
@@ -93,118 +66,84 @@
makeDurationParser(1*time.Hour, 1*time.Minute, 30*24*time.Hour),
true,
},
kernel.WebURLPrefix: {
"URL prefix under which the web server runs",
- func(val string) (any, error) {
+ func(val string) interface{} {
if val != "" && val[0] == '/' && val[len(val)-1] == '/' {
- return val, nil
+ return val
}
- return nil, errURLPrefixSyntax
+ return nil
},
true,
},
}
ws.next = interfaceMap{
- kernel.WebAssetDir: "",
- kernel.WebBaseURL: "http://127.0.0.1:23123/",
kernel.WebListenAddress: "127.0.0.1:23123",
- kernel.WebMaxRequestSize: int64(16 * 1024 * 1024),
kernel.WebPersistentCookie: false,
kernel.WebSecureCookie: true,
kernel.WebTokenLifetimeAPI: 1 * time.Hour,
kernel.WebTokenLifetimeHTML: 10 * time.Minute,
kernel.WebURLPrefix: "/",
}
}
func makeDurationParser(defDur, minDur, maxDur time.Duration) parseFunc {
- return func(val string) (any, error) {
+ return func(val string) interface{} {
if d, err := strconv.ParseUint(val, 10, 64); err == nil {
secs := time.Duration(d) * time.Minute
if secs < minDur {
- return minDur, nil
+ return minDur
}
if secs > maxDur {
- return maxDur, nil
+ return maxDur
}
- return secs, nil
+ return secs
}
- return defDur, nil
+ return defDur
}
}
-var errWrongBasePrefix = errors.New(kernel.WebURLPrefix + " does not match " + kernel.WebBaseURL)
-
-func (ws *webService) GetLogger() *logger.Logger { return ws.logger }
-
func (ws *webService) Start(kern *myKernel) error {
- baseURL := ws.GetNextConfig(kernel.WebBaseURL).(string)
listenAddr := ws.GetNextConfig(kernel.WebListenAddress).(string)
urlPrefix := ws.GetNextConfig(kernel.WebURLPrefix).(string)
persistentCookie := ws.GetNextConfig(kernel.WebPersistentCookie).(bool)
secureCookie := ws.GetNextConfig(kernel.WebSecureCookie).(bool)
- maxRequestSize := ws.GetNextConfig(kernel.WebMaxRequestSize).(int64)
- if maxRequestSize < 1024 {
- maxRequestSize = 1024
- }
-
- if !strings.HasSuffix(baseURL, urlPrefix) {
- ws.logger.Error().Str("base-url", baseURL).Str("url-prefix", urlPrefix).Msg(
- "url-prefix is not a suffix of base-url")
- return errWrongBasePrefix
- }
-
- if lap := netip.MustParseAddrPort(listenAddr); !kern.auth.manager.WithAuth() && !lap.Addr().IsLoopback() {
- ws.logger.Info().Str("listen", listenAddr).Msg("service may be reached from outside, but authentication is not enabled")
- }
-
- srvw := impl.New(ws.logger, listenAddr, baseURL, urlPrefix, persistentCookie, secureCookie, maxRequestSize, kern.auth.manager)
- err := kern.web.setupServer(srvw, kern.box.manager, kern.auth.manager, &kern.cfg)
- if err != nil {
- ws.logger.Error().Err(err).Msg("Unable to create")
- return err
- }
- if kern.core.GetNextConfig(kernel.CoreDebug).(bool) {
- srvw.SetDebug()
- }
- if err = srvw.Run(); err != nil {
- ws.logger.Error().Err(err).Msg("Unable to start")
- return err
- }
- ws.logger.Info().Str("listen", listenAddr).Str("base-url", baseURL).Msg("Start Service")
+
+ srvw := impl.New(listenAddr, urlPrefix, persistentCookie, secureCookie, kern.auth.manager)
+ err := kern.web.setupServer(srvw, kern.place.manager, kern.auth.manager, kern.cfg.rtConfig)
+ if err != nil {
+ kern.doLog("Unable to create Web Server:", err)
+ return err
+ }
+ if kern.debug {
+ srvw.SetDebug()
+ }
+ if err := srvw.Run(); err != nil {
+ kern.doLog("Unable to start Web Service:", err)
+ return err
+ }
+ kern.doLog("Start Web Service:", listenAddr)
ws.mxService.Lock()
ws.srvw = srvw
ws.mxService.Unlock()
-
- if kern.cfg.GetCurConfig(kernel.ConfigSimpleMode).(bool) {
- listenAddr := ws.GetNextConfig(kernel.WebListenAddress).(string)
- if idx := strings.LastIndexByte(listenAddr, ':'); idx >= 0 {
- ws.logger.Mandatory().Msg(strings.Repeat("--------------------", 3))
- ws.logger.Mandatory().Msg("Open your browser and enter the following URL:")
- ws.logger.Mandatory().Msg(" http://localhost" + listenAddr[idx:])
- ws.logger.Mandatory().Msg("")
- ws.logger.Mandatory().Msg("If this does not work, try:")
- ws.logger.Mandatory().Msg(" http://127.0.0.1" + listenAddr[idx:])
- }
- }
-
return nil
}
func (ws *webService) IsStarted() bool {
ws.mxService.RLock()
defer ws.mxService.RUnlock()
return ws.srvw != nil
}
-func (ws *webService) Stop(*myKernel) {
- ws.logger.Info().Msg("Stop Service")
- ws.srvw.Stop()
+func (ws *webService) Stop(kern *myKernel) error {
+ kern.doLog("Stop Web Service")
+ err := ws.srvw.Stop()
ws.mxService.Lock()
ws.srvw = nil
ws.mxService.Unlock()
+ return err
}
-func (*webService) GetStatistics() []kernel.KeyValue {
+func (ws *webService) GetStatistics() []kernel.KeyValue {
return nil
}
Index: kernel/kernel.go
==================================================================
--- kernel/kernel.go
+++ kernel/kernel.go
@@ -1,94 +1,58 @@
//-----------------------------------------------------------------------------
-// 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 kernel provides the main kernel service.
package kernel
import (
"io"
"net/url"
- "time"
"zettelstore.de/z/auth"
- "zettelstore.de/z/box"
"zettelstore.de/z/config"
- "zettelstore.de/z/logger"
+ "zettelstore.de/z/domain/id"
+ "zettelstore.de/z/place"
"zettelstore.de/z/web/server"
- "zettelstore.de/z/zettel/id"
)
// Kernel is the main internal service.
type Kernel interface {
- // Setup sets the most basic data of a software: its name, its version,
- // and when the version was created.
- Setup(progname, version string, versionTime time.Time)
-
// Start the service.
- Start(headline bool, lineServer bool, configFile string)
+ Start(headline bool)
// WaitForShutdown blocks the call until Shutdown is called.
WaitForShutdown()
+
+ // SetDebug to enable/disable debug mode
+ SetDebug(enable bool) bool
// Shutdown the service. Waits for all concurrent activities to stop.
Shutdown(silent bool)
- // GetKernelLogger returns the kernel logger.
- GetKernelLogger() *logger.Logger
-
- // SetLogLevel sets the logging level for logger maintained by the kernel.
- //
- // Its syntax is: (SERVICE ":")? LEVEL (";" (SERICE ":")? LEVEL)*.
- SetLogLevel(string)
+ // Log some activity.
+ Log(args ...interface{})
// LogRecover outputs some information about the previous panic.
LogRecover(name string, recoverInfo interface{}) bool
- // StartProfiling starts profiling the software according to a profile.
- // It is an error to start more than one profile.
- //
- // profileName is a valid profile (see runtime/pprof/Lookup()), or the
- // value "cpu" for profiling the CPI.
- // fileName is the name of the file where the results are written to.
- StartProfiling(profileName, fileName string) error
-
- // StopProfiling stops the current profiling and writes the result to
- // the file, which was named during StartProfiling().
- // It will always be called before the software stops its operations.
- StopProfiling() error
-
// SetConfig stores a configuration value.
- SetConfig(srv Service, key, value string) error
+ SetConfig(srv Service, key, value string) bool
// GetConfig returns a configuration value.
GetConfig(srv Service, key string) interface{}
// GetConfigList returns a sorted list of configuration data.
GetConfigList(Service) []KeyDescrValue
- // GetLogger returns a logger for the given service.
- GetLogger(Service) *logger.Logger
-
- // SetLevel sets the logging level for the given service.
- SetLevel(Service, logger.Level)
-
- // RetrieveLogEntries returns all buffered log entries.
- RetrieveLogEntries() []LogEntry
-
- // GetLastLogTime returns the time when the last logging with level > DEBUG happened.
- GetLastLogTime() time.Time
-
// StartService start the given service.
StartService(Service) error
// RestartService stops and restarts the given service, while maintaining service dependencies.
RestartService(Service) error
@@ -101,11 +65,11 @@
// DumpIndex writes some data about the internal index into a writer.
DumpIndex(io.Writer)
// SetCreators store functions to be called when a service has to be created.
- SetCreators(CreateAuthManagerFunc, CreateBoxManagerFunc, SetupWebServerFunc)
+ SetCreators(CreateAuthManagerFunc, CreatePlaceManagerFunc, SetupWebServerFunc)
}
// Main references the main kernel.
var Main Kernel
@@ -113,89 +77,57 @@
type Unit struct{}
// ShutdownChan is a channel used to signal a system shutdown.
type ShutdownChan <-chan Unit
-// Constants for profile names.
-const (
- ProfileCPU = "CPU"
- ProfileHead = "heap"
-)
-
// Service specifies a service, e.g. web, ...
type Service uint8
// Constants for type Service.
const (
- _ Service = iota
- KernelService // The Kernel itself is also a sevice
- CoreService // Manages startup specific functionality
- ConfigService // Provides access to runtime configuration
- AuthService // Manages authentication
- BoxService // Boxes provide zettel
- WebService // Access to Zettelstore through Web-based API and WebUI
+ _ Service = iota
+ CoreService
+ ConfigService
+ AuthService
+ PlaceService
+ WebService
)
// Constants for core service system keys.
const (
- CoreDebug = "debug"
CoreGoArch = "go-arch"
CoreGoOS = "go-os"
CoreGoVersion = "go-version"
CoreHostname = "hostname"
CorePort = "port"
CoreProgname = "progname"
- CoreStarted = "started"
CoreVerbose = "verbose"
CoreVersion = "version"
- CoreVTime = "vtime"
-)
-
-// Defined values for core service.
-const (
- CoreDefaultVersion = "unknown"
-)
-
-// Constants for config service keys.
-const (
- ConfigSimpleMode = "simple-mode"
- ConfigInsecureHTML = "insecure-html"
)
// Constants for authentication service keys.
const (
AuthOwner = "owner"
AuthReadonly = "readonly"
)
-// Constants for box service keys.
-const (
- BoxDefaultDirType = "defdirtype"
- BoxURIs = "box-uri-"
-)
-
-// Allowed values for BoxDefaultDirType
-const (
- BoxDirTypeNotify = "notify"
- BoxDirTypeSimple = "simple"
-)
-
-// Constants for config service keys.
-const (
- ConfigSecureHTML = "secure"
- ConfigSyntaxHTML = "html"
- ConfigMarkdownHTML = "markdown"
- ConfigZmkHTML = "zettelmarkup"
+// Constants for place service keys.
+const (
+ PlaceDefaultDirType = "defdirtype"
+ PlaceURIs = "place-uri-"
+)
+
+// Allowed values for PlaceDefaultDirType
+const (
+ PlaceDirTypeNotify = "notify"
+ PlaceDirTypeSimple = "simple"
)
// Constants for web service keys.
const (
- WebAssetDir = "asset-dir"
- WebBaseURL = "base-url"
WebListenAddress = "listen"
WebPersistentCookie = "persistent"
- WebMaxRequestSize = "max-request-size"
WebSecureCookie = "secure"
WebTokenLifetimeAPI = "api-lifetime"
WebTokenLifetimeHTML = "html-lifetime"
WebURLPrefix = "prefix"
)
@@ -204,30 +136,22 @@
type KeyDescrValue struct{ Key, Descr, Value string }
// KeyValue is a pair of key and value.
type KeyValue struct{ Key, Value string }
-// LogEntry stores values of one log line written by a logger.Logger
-type LogEntry struct {
- Level logger.Level
- TS time.Time
- Prefix string
- Message string
-}
-
// CreateAuthManagerFunc is called to create a new auth manager.
type CreateAuthManagerFunc func(readonly bool, owner id.Zid) (auth.Manager, error)
-// CreateBoxManagerFunc is called to create a new box manager.
-type CreateBoxManagerFunc func(
- boxURIs []*url.URL,
+// CreatePlaceManagerFunc is called to create a new place manager.
+type CreatePlaceManagerFunc func(
+ placeURIs []*url.URL,
authManager auth.Manager,
rtConfig config.Config,
-) (box.Manager, error)
+) (place.Manager, error)
// SetupWebServerFunc is called to create a new web service handler.
type SetupWebServerFunc func(
webServer server.Server,
- boxManager box.Manager,
+ placeManager place.Manager,
authManager auth.Manager,
rtConfig config.Config,
) error
DELETED logger/logger.go
Index: logger/logger.go
==================================================================
--- logger/logger.go
+++ logger/logger.go
@@ -1,203 +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 logger implements a logging package for use in the Zettelstore.
-package logger
-
-import (
- "context"
- "strconv"
- "strings"
- "sync/atomic"
- "time"
-
- "zettelstore.de/z/zettel/meta"
-)
-
-// Level defines the possible log levels
-type Level uint8
-
-// Constants for Level
-const (
- NoLevel Level = iota // the absent log level
- TraceLevel // Log most internal activities
- DebugLevel // Log most data updates
- InfoLevel // Log normal activities
- ErrorLevel // Log (persistent) errors
- MandatoryLevel // Log only mandatory events
- NeverLevel // Logging is disabled
-)
-
-var logLevel = [...]string{
- " ",
- "TRACE",
- "DEBUG",
- "INFO ",
- "ERROR",
- ">>>>>",
- "NEVER",
-}
-
-var strLevel = [...]string{
- "",
- "trace",
- "debug",
- "info",
- "error",
- "mandatory",
- "disabled",
-}
-
-// IsValid returns true, if the level is a valid level
-func (l Level) IsValid() bool { return TraceLevel <= l && l <= NeverLevel }
-
-func (l Level) String() string {
- if l.IsValid() {
- return strLevel[l]
- }
- return strconv.Itoa(int(l))
-}
-
-// Format returns a string representation suitable for logging.
-func (l Level) Format() string {
- if l.IsValid() {
- return logLevel[l]
- }
- return strconv.Itoa(int(l))
-}
-
-// ParseLevel returns the recognized level.
-func ParseLevel(text string) Level {
- for lv := TraceLevel; lv <= NeverLevel; lv++ {
- if len(text) > 2 && strings.HasPrefix(strLevel[lv], text) {
- return lv
- }
- }
- return NoLevel
-}
-
-// Logger represents an objects that emits logging messages.
-type Logger struct {
- lw LogWriter
- levelVal uint32
- prefix string
- context []byte
- topParent *Logger
- uProvider UserProvider
-}
-
-// LogWriter writes log messages to their specified destinations.
-type LogWriter interface {
- WriteMessage(level Level, ts time.Time, prefix, msg string, details []byte) error
-}
-
-// New creates a new logger for the given service.
-//
-// This function must only be called from a kernel implementation, not from
-// code that tries to log something.
-func New(lw LogWriter, prefix string) *Logger {
- if prefix != "" && len(prefix) < 6 {
- prefix = (prefix + " ")[:6]
- }
- result := &Logger{
- lw: lw,
- levelVal: uint32(InfoLevel),
- prefix: prefix,
- context: nil,
- uProvider: nil,
- }
- result.topParent = result
- return result
-}
-
-func newFromMessage(msg *Message) *Logger {
- if msg == nil {
- return nil
- }
- logger := msg.logger
- context := make([]byte, 0, len(msg.buf))
- context = append(context, msg.buf...)
- return &Logger{
- lw: nil,
- levelVal: 0,
- prefix: logger.prefix,
- context: context,
- topParent: logger.topParent,
- uProvider: nil,
- }
-}
-
-// SetLevel sets the level of the logger.
-func (l *Logger) SetLevel(newLevel Level) *Logger {
- if l != nil {
- if l.topParent != l {
- panic("try to set level for child logger")
- }
- atomic.StoreUint32(&l.levelVal, uint32(newLevel))
- }
- return l
-}
-
-// Level returns the current level of the given logger
-func (l *Logger) Level() Level {
- if l != nil {
- return Level(atomic.LoadUint32(&l.levelVal))
- }
- return NeverLevel
-}
-
-// Trace creates a tracing message.
-func (l *Logger) Trace() *Message { return newMessage(l, TraceLevel) }
-
-// Debug creates a debug message.
-func (l *Logger) Debug() *Message { return newMessage(l, DebugLevel) }
-
-// Info creates a message suitable for information data.
-func (l *Logger) Info() *Message { return newMessage(l, InfoLevel) }
-
-// Error creates a message suitable for errors.
-func (l *Logger) Error() *Message { return newMessage(l, ErrorLevel) }
-
-// Mandatory creates a message that will always logged, except when logging
-// is disabled.
-func (l *Logger) Mandatory() *Message { return newMessage(l, MandatoryLevel) }
-
-// Clone creates a message to clone the logger.
-func (l *Logger) Clone() *Message {
- msg := newMessage(l, NeverLevel)
- if msg != nil {
- msg.level = NoLevel
- }
- return msg
-}
-
-// UserProvider allows to retrieve an user metadata from a context.
-type UserProvider interface {
- GetUser(ctx context.Context) *meta.Meta
-}
-
-// WithUser creates a derivied logger that allows to retrieve and log user identifer.
-func (l *Logger) WithUser(up UserProvider) *Logger {
- return &Logger{
- lw: nil,
- levelVal: 0,
- prefix: l.prefix,
- context: l.context,
- topParent: l.topParent,
- uProvider: up,
- }
-}
-
-func (l *Logger) writeMessage(level Level, msg string, details []byte) error {
- return l.topParent.lw.WriteMessage(level, time.Now().Local(), l.prefix, msg, details)
-}
DELETED logger/logger_test.go
Index: logger/logger_test.go
==================================================================
--- logger/logger_test.go
+++ logger/logger_test.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 logger_test
-
-import (
- "fmt"
- "os"
- "testing"
- "time"
-
- "zettelstore.de/z/logger"
-)
-
-func TestParseLevel(t *testing.T) {
- testcases := []struct {
- text string
- exp logger.Level
- }{
- {"tra", logger.TraceLevel},
- {"deb", logger.DebugLevel},
- {"info", logger.InfoLevel},
- {"err", logger.ErrorLevel},
- {"manda", logger.MandatoryLevel},
- {"dis", logger.NeverLevel},
- {"d", logger.Level(0)},
- }
- for i, tc := range testcases {
- got := logger.ParseLevel(tc.text)
- if got != tc.exp {
- t.Errorf("%d: ParseLevel(%q) == %q, but got %q", i, tc.text, tc.exp, got)
- }
- }
-}
-
-func BenchmarkDisabled(b *testing.B) {
- log := logger.New(&stderrLogWriter{}, "").SetLevel(logger.NeverLevel)
- for range b.N {
- log.Info().Str("key", "val").Msg("Benchmark")
- }
-}
-
-type stderrLogWriter struct{}
-
-func (*stderrLogWriter) WriteMessage(level logger.Level, ts time.Time, prefix, msg string, details []byte) error {
- fmt.Fprintf(os.Stderr, "%v %v %v %v %v\n", level.Format(), ts, prefix, msg, string(details))
- return nil
-}
-
-type testLogWriter struct{}
-
-func (*testLogWriter) WriteMessage(logger.Level, time.Time, string, string, []byte) error {
- return nil
-}
-
-func BenchmarkStrMessage(b *testing.B) {
- log := logger.New(&testLogWriter{}, "")
- for range b.N {
- log.Info().Str("key", "val").Msg("Benchmark")
- }
-}
-
-func BenchmarkMessage(b *testing.B) {
- log := logger.New(&testLogWriter{}, "")
- for range b.N {
- log.Info().Msg("Benchmark")
- }
-}
-
-func BenchmarkCloneStrMessage(b *testing.B) {
- log := logger.New(&testLogWriter{}, "").Clone().Str("sss", "ttt").Child()
- for range b.N {
- log.Info().Msg("123456789")
- }
-}
DELETED logger/message.go
Index: logger/message.go
==================================================================
--- logger/message.go
+++ logger/message.go
@@ -1,157 +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 logger
-
-import (
- "context"
- "net/http"
- "strconv"
- "sync"
-
- "t73f.de/r/zsc/api"
- "zettelstore.de/z/zettel/id"
-)
-
-// Message presents a message to log.
-type Message struct {
- logger *Logger
- level Level
- buf []byte
-}
-
-func newMessage(logger *Logger, level Level) *Message {
- if logger != nil {
- if logger.topParent.Level() <= level {
- m := messagePool.Get().(*Message)
- m.logger = logger
- m.level = level
- m.buf = append(m.buf[:0], logger.context...)
- return m
- }
- }
- return nil
-}
-
-func recycleMessage(m *Message) {
- messagePool.Put(m)
-}
-
-var messagePool = &sync.Pool{
- New: func() interface{} {
- return &Message{
- buf: make([]byte, 0, 500),
- }
- },
-}
-
-// Enabled returns whether the message will log or not.
-func (m *Message) Enabled() bool {
- return m != nil && m.level != NeverLevel
-}
-
-// Str adds a string value to the full message
-func (m *Message) Str(text, val string) *Message {
- if m.Enabled() {
- buf := append(m.buf, ',', ' ')
- buf = append(buf, text...)
- buf = append(buf, '=')
- m.buf = append(buf, val...)
- }
- return m
-}
-
-// Bool adds a boolean value to the full message
-func (m *Message) Bool(text string, val bool) *Message {
- if val {
- m.Str(text, "true")
- } else {
- m.Str(text, "false")
- }
- return m
-}
-
-// Bytes adds a byte slice value to the full message
-func (m *Message) Bytes(text string, val []byte) *Message {
- if m.Enabled() {
- buf := append(m.buf, ',', ' ')
- buf = append(buf, text...)
- buf = append(buf, '=')
- m.buf = append(buf, val...)
- }
- return m
-}
-
-// Err adds an error value to the full message
-func (m *Message) Err(err error) *Message {
- if err != nil {
- return m.Str("error", err.Error())
- }
- return m
-}
-
-// Int adds an integer to the full message
-func (m *Message) Int(text string, i int64) *Message {
- return m.Str(text, strconv.FormatInt(i, 10))
-}
-
-// Uint adds an unsigned integer to the full message
-func (m *Message) Uint(text string, u uint64) *Message {
- return m.Str(text, strconv.FormatUint(u, 10))
-}
-
-// User adds the user-id field of the given user to the message.
-func (m *Message) User(ctx context.Context) *Message {
- if m.Enabled() {
- if up := m.logger.uProvider; up != nil {
- if user := up.GetUser(ctx); user != nil {
- m.buf = append(m.buf, ", user="...)
- if userID, found := user.Get(api.KeyUserID); found {
- m.buf = append(m.buf, userID...)
- } else {
- m.buf = append(m.buf, user.Zid.Bytes()...)
- }
- }
- }
- }
- return m
-}
-
-// HTTPIP adds the IP address of a HTTP request to the message.
-func (m *Message) HTTPIP(r *http.Request) *Message {
- if r == nil {
- return m
- }
- if from := r.Header.Get("X-Forwarded-For"); from != "" {
- return m.Str("ip", from)
- }
- return m.Str("IP", r.RemoteAddr)
-}
-
-// Zid adds a zettel identifier to the full message
-func (m *Message) Zid(zid id.Zid) *Message {
- return m.Bytes("zid", zid.Bytes())
-}
-
-// Msg add the given text to the message and writes it to the log.
-func (m *Message) Msg(text string) {
- if m.Enabled() {
- m.logger.writeMessage(m.level, text, m.buf)
- recycleMessage(m)
- }
-}
-
-// Child creates a child logger with context of this message.
-func (m *Message) Child() *Logger {
- return newFromMessage(m)
-}
Index: parser/blob/blob.go
==================================================================
--- parser/blob/blob.go
+++ parser/blob/blob.go
@@ -1,74 +1,58 @@
//-----------------------------------------------------------------------------
-// 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 blob provides a parser of binary data.
package blob
import (
- "t73f.de/r/zsc/input"
"zettelstore.de/z/ast"
+ "zettelstore.de/z/domain/meta"
+ "zettelstore.de/z/input"
"zettelstore.de/z/parser"
- "zettelstore.de/z/zettel/meta"
)
func init() {
parser.Register(&parser.Info{
- Name: meta.SyntaxGif,
- AltNames: nil,
- IsASTParser: false,
- IsTextFormat: false,
- IsImageFormat: true,
- ParseBlocks: parseBlocks,
- ParseInlines: parseInlines,
- })
- parser.Register(&parser.Info{
- Name: meta.SyntaxJPEG,
- AltNames: []string{meta.SyntaxJPG},
- IsASTParser: false,
- IsTextFormat: false,
- IsImageFormat: true,
- ParseBlocks: parseBlocks,
- ParseInlines: parseInlines,
- })
- parser.Register(&parser.Info{
- Name: meta.SyntaxPNG,
- AltNames: nil,
- IsASTParser: false,
- IsTextFormat: false,
- IsImageFormat: true,
- ParseBlocks: parseBlocks,
- ParseInlines: parseInlines,
- })
- parser.Register(&parser.Info{
- Name: meta.SyntaxWebp,
- AltNames: nil,
- IsASTParser: false,
- IsTextFormat: false,
- IsImageFormat: true,
- ParseBlocks: parseBlocks,
- ParseInlines: parseInlines,
+ Name: "gif",
+ AltNames: nil,
+ ParseBlocks: parseBlocks,
+ ParseInlines: parseInlines,
+ })
+ parser.Register(&parser.Info{
+ Name: "jpeg",
+ AltNames: []string{"jpg"},
+ ParseBlocks: parseBlocks,
+ ParseInlines: parseInlines,
+ })
+ parser.Register(&parser.Info{
+ Name: "png",
+ AltNames: nil,
+ ParseBlocks: parseBlocks,
+ ParseInlines: parseInlines,
})
}
func parseBlocks(inp *input.Input, m *meta.Meta, syntax string) ast.BlockSlice {
if p := parser.Get(syntax); p != nil {
syntax = p.Name
}
- return ast.BlockSlice{&ast.BLOBNode{
- Description: parser.ParseDescription(m),
- Syntax: syntax,
- Blob: []byte(inp.Src),
- }}
+ title, _ := m.Get(meta.KeyTitle)
+ return ast.BlockSlice{
+ &ast.BLOBNode{
+ Title: title,
+ Syntax: syntax,
+ Blob: []byte(inp.Src),
+ },
+ }
}
-func parseInlines(*input.Input, string) ast.InlineSlice { return nil }
+func parseInlines(inp *input.Input, syntax string) ast.InlineSlice {
+ return ast.InlineSlice{}
+}
Index: parser/cleaner/cleaner.go
==================================================================
--- parser/cleaner/cleaner.go
+++ parser/cleaner/cleaner.go
@@ -1,229 +1,147 @@
//-----------------------------------------------------------------------------
-// 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 cleaner provides functions to clean up the parsed AST.
+// Package cleaner provides funxtions to clean up the parsed AST.
package cleaner
import (
"strconv"
"strings"
"zettelstore.de/z/ast"
- "zettelstore.de/z/encoder/textenc"
+ "zettelstore.de/z/encoder"
"zettelstore.de/z/strfun"
)
-// CleanBlockSlice cleans the given block list.
-func CleanBlockSlice(bs *ast.BlockSlice, allowHTML bool) { cleanNode(bs, allowHTML) }
-
-// CleanInlineSlice cleans the given inline list.
-func CleanInlineSlice(is *ast.InlineSlice) { cleanNode(is, false) }
-
-func cleanNode(n ast.Node, allowHTML bool) {
- cv := cleanVisitor{
- textEnc: textenc.Create(),
- allowHTML: allowHTML,
- hasMark: false,
- doMark: false,
- }
- ast.Walk(&cv, n)
- if cv.hasMark {
- cv.doMark = true
- ast.Walk(&cv, n)
- }
-}
-
-type cleanVisitor struct {
- textEnc *textenc.Encoder
- ids map[string]ast.Node
- allowHTML bool
- hasMark bool
- doMark bool
-}
-
-func (cv *cleanVisitor) Visit(node ast.Node) ast.Visitor {
- switch n := node.(type) {
- case *ast.BlockSlice:
- if !cv.allowHTML {
- cv.visitBlockSlice(n)
- return nil
- }
- case *ast.InlineSlice:
- if !cv.allowHTML {
- cv.visitInlineSlice(n)
- return nil
- }
- case *ast.HeadingNode:
- cv.visitHeading(n)
- return nil
- case *ast.MarkNode:
- cv.visitMark(n)
- return nil
- }
- return cv
-}
-
-func (cv *cleanVisitor) visitBlockSlice(bs *ast.BlockSlice) {
- if bs == nil {
- return
- }
- if len(*bs) == 0 {
- *bs = nil
- return
- }
- for _, bn := range *bs {
- ast.Walk(cv, bn)
- }
-
- fromPos, toPos := 0, 0
- for fromPos < len(*bs) {
- (*bs)[toPos] = (*bs)[fromPos]
- fromPos++
- switch bn := (*bs)[toPos].(type) {
- case *ast.VerbatimNode:
- if bn.Kind != ast.VerbatimHTML {
- toPos++
- }
- default:
- toPos++
- }
- }
- for pos := toPos; pos < len(*bs); pos++ {
- (*bs)[pos] = nil // Allow excess nodes to be garbage collected.
- }
- *bs = (*bs)[:toPos:toPos]
-}
-
-func (cv *cleanVisitor) visitInlineSlice(is *ast.InlineSlice) {
- if is == nil {
- return
- }
- if len(*is) == 0 {
- *is = nil
- return
- }
- for _, bn := range *is {
- ast.Walk(cv, bn)
- }
-
- fromPos, toPos := 0, 0
- for fromPos < len(*is) {
- (*is)[toPos] = (*is)[fromPos]
- fromPos++
- switch in := (*is)[toPos].(type) {
- case *ast.LiteralNode:
- if in.Kind != ast.LiteralHTML {
- toPos++
- }
- default:
- toPos++
- }
- }
- for pos := toPos; pos < len(*is); pos++ {
- (*is)[pos] = nil // Allow excess nodes to be garbage collected.
- }
- *is = (*is)[:toPos:toPos]
-}
-
-func (cv *cleanVisitor) visitHeading(hn *ast.HeadingNode) {
- if cv.doMark || hn == nil || len(hn.Inlines) == 0 {
- return
- }
- if hn.Slug == "" {
- var sb strings.Builder
- _, err := cv.textEnc.WriteInlines(&sb, &hn.Inlines)
- if err != nil {
- return
- }
- hn.Slug = strfun.Slugify(sb.String())
- }
- if hn.Slug != "" {
- hn.Fragment = cv.addIdentifier(hn.Slug, hn)
- }
-}
-
-func (cv *cleanVisitor) visitMark(mn *ast.MarkNode) {
+// CleanupBlockSlice cleans the given block slice.
+func CleanupBlockSlice(bs ast.BlockSlice) {
+ cv := &cleanupVisitor{
+ textEnc: encoder.Create("text", nil),
+ doMark: false,
+ }
+ t := ast.NewTopDownTraverser(cv)
+ t.VisitBlockSlice(bs)
+ if cv.hasMark {
+ cv.doMark = true
+ t.VisitBlockSlice(bs)
+ }
+}
+
+type cleanupVisitor struct {
+ textEnc encoder.Encoder
+ ids map[string]ast.Node
+ hasMark bool
+ doMark bool
+}
+
+// VisitVerbatim does nothing.
+func (cv *cleanupVisitor) VisitVerbatim(vn *ast.VerbatimNode) {}
+
+// VisitRegion does nothing.
+func (cv *cleanupVisitor) VisitRegion(rn *ast.RegionNode) {}
+
+// VisitHeading calculates the heading slug.
+func (cv *cleanupVisitor) VisitHeading(hn *ast.HeadingNode) {
+ if cv.doMark || hn == nil || hn.Inlines == nil {
+ return
+ }
+ var sb strings.Builder
+ _, err := cv.textEnc.WriteInlines(&sb, hn.Inlines)
+ if err != nil {
+ return
+ }
+ s := strfun.Slugify(sb.String())
+ if len(s) > 0 {
+ hn.Slug = cv.addIdentifier(s, hn)
+ }
+}
+
+// VisitHRule does nothing.
+func (cv *cleanupVisitor) VisitHRule(hn *ast.HRuleNode) {}
+
+// VisitList does nothing.
+func (cv *cleanupVisitor) VisitNestedList(ln *ast.NestedListNode) {}
+
+// VisitDescriptionList does nothing.
+func (cv *cleanupVisitor) VisitDescriptionList(dn *ast.DescriptionListNode) {}
+
+// VisitPara does nothing.
+func (cv *cleanupVisitor) VisitPara(pn *ast.ParaNode) {}
+
+// VisitTable does nothing.
+func (cv *cleanupVisitor) VisitTable(tn *ast.TableNode) {}
+
+// VisitBLOB does nothing.
+func (cv *cleanupVisitor) VisitBLOB(bn *ast.BLOBNode) {}
+
+// VisitText does nothing.
+func (cv *cleanupVisitor) VisitText(tn *ast.TextNode) {}
+
+// VisitTag does nothing.
+func (cv *cleanupVisitor) VisitTag(tn *ast.TagNode) {}
+
+// VisitSpace does nothing.
+func (cv *cleanupVisitor) VisitSpace(sn *ast.SpaceNode) {}
+
+// VisitBreak does nothing.
+func (cv *cleanupVisitor) VisitBreak(bn *ast.BreakNode) {}
+
+// VisitLink collects the given link as a reference.
+func (cv *cleanupVisitor) VisitLink(ln *ast.LinkNode) {}
+
+// VisitImage collects the image links as a reference.
+func (cv *cleanupVisitor) VisitImage(in *ast.ImageNode) {}
+
+// VisitCite does nothing.
+func (cv *cleanupVisitor) VisitCite(cn *ast.CiteNode) {}
+
+// VisitFootnote does nothing.
+func (cv *cleanupVisitor) VisitFootnote(fn *ast.FootnoteNode) {}
+
+// VisitMark checks for duplicate marks and changes them.
+func (cv *cleanupVisitor) VisitMark(mn *ast.MarkNode) {
+ if mn == nil {
+ return
+ }
if !cv.doMark {
cv.hasMark = true
return
}
- if mn.Mark == "" {
- mn.Slug = ""
- mn.Fragment = cv.addIdentifier("*", mn)
+ if mn.Text == "" {
+ mn.Text = cv.addIdentifier("*", mn)
return
}
- if mn.Slug == "" {
- mn.Slug = strfun.Slugify(mn.Mark)
- }
- mn.Fragment = cv.addIdentifier(mn.Slug, mn)
+ mn.Text = cv.addIdentifier(mn.Text, mn)
}
-func (cv *cleanVisitor) addIdentifier(id string, node ast.Node) string {
+// VisitFormat does nothing.
+func (cv *cleanupVisitor) VisitFormat(fn *ast.FormatNode) {}
+
+// VisitLiteral does nothing.
+func (cv *cleanupVisitor) VisitLiteral(ln *ast.LiteralNode) {}
+
+func (cv *cleanupVisitor) addIdentifier(id string, node ast.Node) string {
if cv.ids == nil {
cv.ids = map[string]ast.Node{id: node}
return id
}
if n, ok := cv.ids[id]; ok && n != node {
prefix := id + "-"
for count := 1; ; count++ {
newID := prefix + strconv.Itoa(count)
- if n2, ok2 := cv.ids[newID]; !ok2 || n2 == node {
+ if n, ok := cv.ids[newID]; !ok || n == node {
cv.ids[newID] = node
return newID
}
}
}
cv.ids[id] = node
return id
}
-
-// CleanInlineLinks removes all links and footnote node from the given inline slice.
-func CleanInlineLinks(is *ast.InlineSlice) { ast.Walk(&cleanLinks{}, is) }
-
-type cleanLinks struct{}
-
-func (cl *cleanLinks) Visit(node ast.Node) ast.Visitor {
- ins, ok := node.(*ast.InlineSlice)
- if !ok {
- return cl
- }
- for _, in := range *ins {
- ast.Walk(cl, in)
- }
- if hasNoLinks(*ins) {
- return nil
- }
-
- result := make(ast.InlineSlice, 0, len(*ins))
- for _, in := range *ins {
- switch n := in.(type) {
- case *ast.LinkNode:
- result = append(result, n.Inlines...)
- case *ast.FootnoteNode: // Do nothing
- default:
- result = append(result, n)
- }
- }
- *ins = result
- return nil
-}
-
-func hasNoLinks(ins ast.InlineSlice) bool {
- for _, in := range ins {
- switch in.(type) {
- case *ast.LinkNode, *ast.FootnoteNode:
- return false
- }
- }
- return true
-}
DELETED parser/draw/ORIG_CONTRIBUTORS
Index: parser/draw/ORIG_CONTRIBUTORS
==================================================================
--- parser/draw/ORIG_CONTRIBUTORS
+++ parser/draw/ORIG_CONTRIBUTORS
@@ -1,3 +0,0 @@
-Devon H. O'Dell
-Marc-Antoine Ruel
-Mateusz Czaplinski
DELETED parser/draw/ORIG_LICENSE
Index: parser/draw/ORIG_LICENSE
==================================================================
--- parser/draw/ORIG_LICENSE
+++ parser/draw/ORIG_LICENSE
@@ -1,22 +0,0 @@
-The MIT License (MIT)
-
-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.
-
DELETED parser/draw/canvas.go
Index: parser/draw/canvas.go
==================================================================
--- parser/draw/canvas.go
+++ parser/draw/canvas.go
@@ -1,361 +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.
-//
-// This file was originally created by the ASCIIToSVG contributors under an MIT
-// license, but later changed to fulfil the needs of Zettelstore. The following
-// statements affects the original code as found on
-// https://github.com/asciitosvg/asciitosvg (Commit:
-// ca82a5ce41e2190a05e07af6e8b3ea4e3256a283, 2020-11-20):
-//
-// Copyright 2012 - 2018 The ASCIIToSVG Contributors
-// All rights reserved.
-//
-// SPDX-License-Identifier: EUPL-1.2
-// SPDX-FileCopyrightText: 2022-present Detlef Stern
-//-----------------------------------------------------------------------------
-
-package draw
-
-import (
- "bytes"
- "fmt"
- "image"
- "sort"
- "unicode/utf8"
-)
-
-// newCanvas returns a new Canvas, initialized from the provided data. If tabWidth is set to a non-negative
-// value, that value will be used to convert tabs to spaces within the grid. Creation of the Canvas
-// can fail if the diagram contains invalid UTF-8 sequences.
-func newCanvas(data []byte) (*canvas, error) {
- c := &canvas{}
-
- lines := bytes.Split(data, []byte("\n"))
- c.siz.Y = len(lines)
-
- // Diagrams will often not be padded to a uniform width. To overcome this, we scan over
- // each line and figure out which is the longest. This becomes the width of the canvas.
- for i, line := range lines {
- if ok := utf8.Valid(line); !ok {
- return nil, fmt.Errorf("invalid UTF-8 encoding on line %d", i)
- }
- if i1 := utf8.RuneCount(line); i1 > c.siz.X {
- c.siz.X = i1
- }
- }
-
- c.grid = make([]char, c.siz.X*c.siz.Y)
- c.visited = make([]bool, c.siz.X*c.siz.Y)
- for y, line := range lines {
- x := 0
- for len(line) > 0 {
- r, l := utf8.DecodeRune(line)
- c.grid[y*c.siz.X+x] = char(r)
- x++
- line = line[l:]
- }
-
- for ; x < c.siz.X; x++ {
- c.grid[y*c.siz.X+x] = ' '
- }
- }
-
- c.findObjects()
- return c, nil
-}
-
-// canvas is the parsed source data.
-type canvas struct {
- // (0,0) is top left.
- grid []char
- visited []bool
- objs objects
- siz image.Point
- hasStartMarker bool
- hasEndMarker bool
-}
-
-// String provides a view into the underlying grid.
-func (c *canvas) String() string { return fmt.Sprintf("%+v", c.grid) }
-
-// objects returns all the objects found in the underlying grid.
-func (c *canvas) objects() objects { return c.objs }
-
-// size returns the visual dimensions of the Canvas.
-func (c *canvas) size() image.Point { return c.siz }
-
-// findObjects finds all objects (lines, polygons, and text) within the underlying grid.
-func (c *canvas) findObjects() {
- c.findPaths()
- c.findTexts()
- sort.Sort(c.objs)
-}
-
-// findPaths by starting with a point that wasn't yet visited, beginning at the top
-// left of the grid.
-func (c *canvas) findPaths() {
- for y := range c.siz.Y {
- p := point{y: y}
- for x := range c.siz.X {
- p.x = x
- if c.isVisited(p) {
- continue
- }
- ch := c.at(p)
- if !ch.isPathStart() {
- continue
- }
-
- // Found the start of a one or multiple connected paths. Traverse all
- // connecting points. This will generate multiple objects if multiple
- // paths (either open or closed) are found.
- c.visit(p)
- objs := c.scanPath([]point{p})
- for _, obj := range objs {
- // For all points in all objects found, mark the points as visited.
- for _, p := range obj.Points() {
- c.visit(p)
- }
- }
- c.objs = append(c.objs, objs...)
- }
- }
-}
-
-// findTexts with a second pass through the grid attempts to identify any text within the grid.
-func (c *canvas) findTexts() {
- for y := range c.siz.Y {
- p := point{}
- p.y = y
- for x := range c.siz.X {
- p.x = x
- if c.isVisited(p) {
- continue
- }
- ch := c.at(p)
- if !ch.isTextStart() {
- continue
- }
-
- // scanText will return nil if the text at this area is simply
- // setting options on a container object.
- obj := c.scanText(p)
- if obj == nil {
- continue
- }
- for _, p := range obj.Points() {
- c.visit(p)
- }
- c.objs = append(c.objs, obj)
- }
- }
-}
-
-// scanPath tries to complete a total path (for lines or polygons) starting with some partial path.
-// It recurses when it finds multiple unvisited outgoing paths.
-func (c *canvas) scanPath(points []point) objects {
- cur := points[len(points)-1]
- next := c.next(cur)
-
- // If there are no points that can progress traversal of the path, finalize the one we're
- // working on, and return it. This is the terminal condition in the passive flow.
- if len(next) == 0 {
- if len(points) == 1 {
- // Discard 'path' of 1 point. Do not mark point as visited.
- c.unvisit(cur)
- return nil
- }
-
- // TODO(dhobsd): Determine if path is sharing the line with another path. If so,
- // we may want to join the objects such that we don't get weird rendering artifacts.
- o := &object{points: points}
- o.seal(c)
- return objects{o}
- }
-
- // If we have hit a point that can create a closed path, create an object and close
- // the path. Additionally, recurse to other progress directions in case e.g. an open
- // path spawns from this point. Paths are always closed vertically.
- if cur.x == points[0].x && cur.y == points[0].y+1 {
- o := &object{points: points}
- o.seal(c)
- r := objects{o}
- return append(r, c.scanPath([]point{cur})...)
- }
-
- // We scan depth-first instead of breadth-first, making it possible to find a
- // closed path.
- var objs objects
- for _, n := range next {
- if c.isVisited(n) {
- continue
- }
- c.visit(n)
- p2 := make([]point, len(points)+1)
- copy(p2, points)
- p2[len(p2)-1] = n
- objs = append(objs, c.scanPath(p2)...)
- }
- return objs
-}
-
-// The next returns the points that can be used to make progress, scanning (in order) horizontal
-// progress to the left or right, vertical progress above or below, or diagonal progress to NW,
-// NE, SW, and SE. It skips any points already visited, and returns all of the possible progress
-// points.
-func (c *canvas) next(pos point) []point {
- // Our caller must have called c.visit prior to calling this function.
- if !c.isVisited(pos) {
- panic(fmt.Errorf("internal error; revisiting %s", pos))
- }
-
- var out []point
-
- nextHorizontal := func(p point) {
- if !c.isVisited(p) && c.at(p).canHorizontal() {
- out = append(out, p)
- }
- }
- nextVertical := func(p point) {
- if !c.isVisited(p) && c.at(p).canVertical() {
- out = append(out, p)
- }
- }
- nextDiagonal := func(from, to point) {
- if !c.isVisited(to) && c.at(to).canDiagonalFrom(c.at(from)) {
- out = append(out, to)
- }
- }
-
- ch := c.at(pos)
- if ch.canHorizontal() {
- if c.canLeft(pos) {
- n := pos
- n.x--
- nextHorizontal(n)
- }
- if c.canRight(pos) {
- n := pos
- n.x++
- nextHorizontal(n)
- }
- }
- if ch.canVertical() {
- if c.canUp(pos) {
- n := pos
- n.y--
- nextVertical(n)
- }
- if c.canDown(pos) {
- n := pos
- n.y++
- nextVertical(n)
- }
- }
- if c.canDiagonal(pos) {
- if c.canUp(pos) {
- if c.canLeft(pos) {
- n := pos
- n.x--
- n.y--
- nextDiagonal(pos, n)
- }
- if c.canRight(pos) {
- n := pos
- n.x++
- n.y--
- nextDiagonal(pos, n)
- }
- }
- if c.canDown(pos) {
- if c.canLeft(pos) {
- n := pos
- n.x--
- n.y++
- nextDiagonal(pos, n)
- }
- if c.canRight(pos) {
- n := pos
- n.x++
- n.y++
- nextDiagonal(pos, n)
- }
- }
- }
-
- return out
-}
-
-// scanText extracts a line of text.
-func (c *canvas) scanText(start point) *object {
- obj := &object{points: []point{start}, isText: true}
- whiteSpaceStreak := 0
- cur := start
-
- for c.canRight(cur) {
- cur.x++
- if c.isVisited(cur) {
- // If the point is already visited, we hit a polygon or a line.
- break
- }
- ch := c.at(cur)
- if !ch.isTextCont() {
- break
- }
- if ch.isSpace() {
- whiteSpaceStreak++
- // Stop when we see 3 consecutive whitespace points.
- if whiteSpaceStreak > 2 {
- break
- }
- } else {
- whiteSpaceStreak = 0
- }
- obj.points = append(obj.points, cur)
- }
-
- // Trim the right side of the text object.
- for len(obj.points) != 0 && c.at(obj.points[len(obj.points)-1]).isSpace() {
- obj.points = obj.points[:len(obj.points)-1]
- }
-
- obj.seal(c)
- return obj
-}
-
-func (c *canvas) at(p point) char {
- return c.grid[p.y*c.siz.X+p.x]
-}
-
-func (c *canvas) isVisited(p point) bool {
- return c.visited[p.y*c.siz.X+p.x]
-}
-
-func (c *canvas) visit(p point) {
- // TODO(dhobsd): Change code to ensure that visit() is called once and only
- // once per point.
- c.visited[p.y*c.siz.X+p.x] = true
-}
-
-func (c *canvas) unvisit(p point) {
- o := p.y*c.siz.X + p.x
- if !c.visited[o] {
- panic(fmt.Errorf("internal error: point %+v never visited", p))
- }
- c.visited[o] = false
-}
-
-func (*canvas) canLeft(p point) bool { return p.x > 0 }
-func (c *canvas) canRight(p point) bool { return p.x < c.siz.X-1 }
-func (*canvas) canUp(p point) bool { return p.y > 0 }
-func (c *canvas) canDown(p point) bool { return p.y < c.siz.Y-1 }
-
-func (c *canvas) canDiagonal(p point) bool {
- return (c.canLeft(p) || c.canRight(p)) && (c.canUp(p) || c.canDown(p))
-}
DELETED parser/draw/canvas_test.go
Index: parser/draw/canvas_test.go
==================================================================
--- parser/draw/canvas_test.go
+++ parser/draw/canvas_test.go
@@ -1,719 +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.
-//
-// This file was originally created by the ASCIIToSVG contributors under an MIT
-// license, but later changed to fulfil the needs of Zettelstore. The following
-// statements affects the original code as found on
-// https://github.com/asciitosvg/asciitosvg (Commit:
-// ca82a5ce41e2190a05e07af6e8b3ea4e3256a283, 2020-11-20):
-//
-// Copyright 2012 - 2018 The ASCIIToSVG Contributors
-// All rights reserved.
-//
-// SPDX-License-Identifier: EUPL-1.2
-// SPDX-FileCopyrightText: 2022-present Detlef Stern
-//-----------------------------------------------------------------------------
-
-package draw
-
-import (
- "reflect"
- "strings"
- "testing"
-)
-
-func TestNewCanvas(t *testing.T) {
- t.Parallel()
- data := []struct {
- input []string
- strings []string
- texts []string
- points [][]point
- allPoints bool
- }{
- // 0 Small box
- {
- []string{
- "+-+",
- "| |",
- "+-+",
- },
- []string{"Path{[(0,0) (1,0) (2,0) (2,1) (2,2) (1,2) (0,2) (0,1)]}"},
- []string{""},
- [][]point{{{x: 0, y: 0}, {x: 2, y: 0}, {x: 2, y: 2}, {x: 0, y: 2}}},
- false,
- },
-
- // 1 Tight box
- {
- []string{
- "++",
- "++",
- },
- []string{"Path{[(0,0) (1,0) (1,1) (0,1)]}"},
- []string{""},
- [][]point{
- {
- {x: 0, y: 0}, {x: 1, y: 0}, {x: 1, y: 1}, {x: 0, y: 1},
- },
- },
- false,
- },
-
- // 2 Indented box
- {
- []string{
- "",
- " +-+",
- " | |",
- " +-+",
- },
- []string{"Path{[(1,1) (2,1) (3,1) (3,2) (3,3) (2,3) (1,3) (1,2)]}"},
- []string{""},
- [][]point{{{x: 1, y: 1}, {x: 3, y: 1}, {x: 3, y: 3}, {x: 1, y: 3}}},
- false,
- },
-
- // 3 Free flow text
- {
- []string{
- "",
- " foo bar ",
- "b baz bee",
- },
- []string{"Text{(1,1) \"foo bar\"}", "Text{(0,2) \"b baz\"}", "Text{(9,2) \"bee\"}"},
- []string{"foo bar", "b baz", "bee"},
- [][]point{
- {{x: 1, y: 1}, {x: 7, y: 1}},
- {{x: 0, y: 2}, {x: 5, y: 2}},
- {{x: 9, y: 2}, {x: 11, y: 2}},
- },
- false,
- },
-
- // 4 Text in a box
- {
- []string{
- "+--+",
- "|Hi|",
- "+--+",
- },
- []string{"Path{[(0,0) (1,0) (2,0) (3,0) (3,1) (3,2) (2,2) (1,2) (0,2) (0,1)]}", "Text{(1,1) \"Hi\"}"},
- []string{"", "Hi"},
- [][]point{
- {{x: 0, y: 0}, {x: 3, y: 0}, {x: 3, y: 2}, {x: 0, y: 2}},
- {{x: 1, y: 1}, {x: 2, y: 1}},
- },
- false,
- },
-
- // 5 Concave pieces
- {
- []string{
- " +----+",
- " | |",
- "+---+ +----+",
- "| |",
- "+-------------+",
- "", // 5
- "+----+",
- "| |",
- "| +---+",
- "| |",
- "| +---+", // 10
- "| |",
- "+----+",
- "",
- " +----+",
- " | |", // 15
- "+---+ |",
- "| |",
- "+---+ |",
- " | |",
- " +----+",
- },
- []string{
- "Path{[(4,0) (5,0) (6,0) (7,0) (8,0) (9,0) (9,1) (9,2) (10,2) (11,2) (12,2) (13,2) (14,2) (14,3) (14,4) (13,4) (12,4) (11,4) (10,4) (9,4) (8,4) (7,4) (6,4) (5,4) (4,4) (3,4) (2,4) (1,4) (0,4) (0,3) (0,2) (1,2) (2,2) (3,2) (4,2) (4,1)]}",
- "Path{[(0,6) (1,6) (2,6) (3,6) (4,6) (5,6) (5,7) (5,8) (6,8) (7,8) (8,8) (9,8) (9,9) (9,10) (8,10) (7,10) (6,10) (5,10) (5,11) (5,12) (4,12) (3,12) (2,12) (1,12) (0,12) (0,11) (0,10) (0,9) (0,8) (0,7)]}",
- "Path{[(4,14) (5,14) (6,14) (7,14) (8,14) (9,14) (9,15) (9,16) (9,17) (9,18) (9,19) (9,20) (8,20) (7,20) (6,20) (5,20) (4,20) (4,19) (4,18) (3,18) (2,18) (1,18) (0,18) (0,17) (0,16) (1,16) (2,16) (3,16) (4,16) (4,15)]}",
- },
- []string{"", "", ""},
- [][]point{
- {
- {x: 4, y: 0}, {x: 9, y: 0}, {x: 9, y: 2}, {x: 14, y: 2},
- {x: 14, y: 4}, {x: 0, y: 4}, {x: 0, y: 2}, {x: 4, y: 2},
- },
- {
- {x: 0, y: 6}, {x: 5, y: 6}, {x: 5, y: 8}, {x: 9, y: 8},
- {x: 9, y: 10}, {x: 5, y: 10}, {x: 5, y: 12}, {x: 0, y: 12},
- },
- {
- {x: 4, y: 14}, {x: 9, y: 14}, {x: 9, y: 20}, {x: 4, y: 20},
- {x: 4, y: 18}, {x: 0, y: 18}, {x: 0, y: 16}, {x: 4, y: 16},
- },
- },
- false,
- },
-
- // 6 Inner boxes
- {
- []string{
- "+-----+",
- "| |",
- "| +-+ |",
- "| | | |",
- "| +-+ |",
- "| |",
- "+-----+",
- },
- []string{
- "Path{[(0,0) (1,0) (2,0) (3,0) (4,0) (5,0) (6,0) (6,1) (6,2) (6,3) (6,4) (6,5) (6,6) (5,6) (4,6) (3,6) (2,6) (1,6) (0,6) (0,5) (0,4) (0,3) (0,2) (0,1)]}",
- "Path{[(2,2) (3,2) (4,2) (4,3) (4,4) (3,4) (2,4) (2,3)]}",
- },
- []string{"", ""},
- [][]point{
- {{x: 0, y: 0}, {x: 6, y: 0}, {x: 6, y: 6}, {x: 0, y: 6}},
- {{x: 2, y: 2}, {x: 4, y: 2}, {x: 4, y: 4}, {x: 2, y: 4}},
- },
- false,
- },
-
- // 7 Real world diagram example
- {
- []string{
- // 1 2 3
- " +------+",
- " |Editor|-------------+--------+",
- " +------+ | |",
- " | | v",
- " v | +--------+",
- " +------+ | |Document|", // 5
- " |Window| | +--------+",
- " +------+ |",
- " | |",
- " +-----+-------+ |",
- " | | |", // 10
- " v v |",
- "+------+ +------+ |",
- "|Window| |Window| |",
- "+------+ +------+ |",
- " | |", // 15
- " v |",
- " +----+ |",
- " |View| |",
- " +----+ |",
- " | |", // 20
- " v |",
- " +--------+ |",
- " |Document|<----+",
- " +--------+",
- },
- []string{
- "Path{[(6,0) (7,0) (8,0) (9,0) (10,0) (11,0) (12,0) (13,0) (13,1) (13,2) (12,2) (11,2) (10,2) (9,2) (8,2) (7,2) (6,2) (6,1)]}",
- "Path{[(14,1) (15,1) (16,1) (17,1) (18,1) (19,1) (20,1) (21,1) (22,1) (23,1) (24,1) (25,1) (26,1) (27,1) (28,1) (29,1) (30,1) (31,1) (32,1) (33,1) (34,1) (35,1) (36,1) (36,2) (36,3)]}",
- "Path{[(14,1) (15,1) (16,1) (17,1) (18,1) (19,1) (20,1) (21,1) (22,1) (23,1) (24,1) (25,1) (26,1) (27,1) (27,2) (27,3) (27,4) (27,5) (27,6) (27,7) (27,8) (27,9) (27,10) (27,11) (27,12) (27,13) (27,14) (27,15) (27,16) (27,17) (27,18) (27,19) (27,20) (27,21) (27,22) (27,23) (26,23) (25,23) (24,23) (23,23) (22,23)]}",
- "Path{[(10,3) (10,4)]}",
- "Path{[(31,4) (32,4) (33,4) (34,4) (35,4) (36,4) (37,4) (38,4) (39,4) (40,4) (40,5) (40,6) (39,6) (38,6) (37,6) (36,6) (35,6) (34,6) (33,6) (32,6) (31,6) (31,5)]}",
- "Path{[(6,5) (7,5) (8,5) (9,5) (10,5) (11,5) (12,5) (13,5) (13,6) (13,7) (12,7) (11,7) (10,7) (9,7) (8,7) (7,7) (6,7) (6,6)]}",
- "Path{[(9,8) (9,9)]}",
- "Path{[(9,9) (8,9) (7,9) (6,9) (5,9) (4,9) (3,9) (3,10) (3,11)]}",
- "Path{[(9,9) (10,9) (11,9) (12,9) (13,9) (14,9) (15,9) (16,9) (17,9) (17,10) (17,11)]}",
- "Path{[(0,12) (1,12) (2,12) (3,12) (4,12) (5,12) (6,12) (7,12) (7,13) (7,14) (6,14) (5,14) (4,14) (3,14) (2,14) (1,14) (0,14) (0,13)]}",
- "Path{[(13,12) (14,12) (15,12) (16,12) (17,12) (18,12) (19,12) (20,12) (20,13) (20,14) (19,14) (18,14) (17,14) (16,14) (15,14) (14,14) (13,14) (13,13)]}",
- "Path{[(16,15) (16,16)]}",
- "Path{[(14,17) (15,17) (16,17) (17,17) (18,17) (19,17) (19,18) (19,19) (18,19) (17,19) (16,19) (15,19) (14,19) (14,18)]}",
- "Path{[(16,20) (16,21)]}",
- "Path{[(12,22) (13,22) (14,22) (15,22) (16,22) (17,22) (18,22) (19,22) (20,22) (21,22) (21,23) (21,24) (20,24) (19,24) (18,24) (17,24) (16,24) (15,24) (14,24) (13,24) (12,24) (12,23)]}",
- "Text{(7,1) \"Editor\"}",
- "Text{(32,5) \"Document\"}",
- "Text{(7,6) \"Window\"}",
- "Text{(1,13) \"Window\"}",
- "Text{(14,13) \"Window\"}",
- "Text{(15,18) \"View\"}",
- "Text{(13,23) \"Document\"}",
- },
- []string{
- "", "", "", "", "", "", "", "", "", "", "", "", "", "", "",
- "Editor", "Document", "Window", "Window", "Window", "View", "Document",
- },
- [][]point{
- {{x: 6, y: 0}, {x: 13, y: 0}, {x: 13, y: 2}, {x: 6, y: 2}},
- {{x: 14, y: 1}, {x: 36, y: 1}, {x: 36, y: 3, hint: 3}},
- {{x: 14, y: 1}, {x: 27, y: 1}, {x: 27, y: 23}, {x: 22, y: 23, hint: 3}},
- {{x: 10, y: 3}, {x: 10, y: 4, hint: 3}},
- {{x: 31, y: 4}, {x: 40, y: 4}, {x: 40, y: 6}, {x: 31, y: 6}},
- {{x: 6, y: 5}, {x: 13, y: 5}, {x: 13, y: 7}, {x: 6, y: 7}},
- {{x: 9, y: 8}, {x: 9, y: 9}},
- {{x: 9, y: 9}, {x: 3, y: 9}, {x: 3, y: 11, hint: 3}},
- {{x: 9, y: 9}, {x: 17, y: 9}, {x: 17, y: 11, hint: 3}},
- {{x: 0, y: 12}, {x: 7, y: 12}, {x: 7, y: 14}, {x: 0, y: 14}},
- {{x: 13, y: 12}, {x: 20, y: 12}, {x: 20, y: 14}, {x: 13, y: 14}},
- {{x: 16, y: 15}, {x: 16, y: 16, hint: 3}},
- {{x: 14, y: 17}, {x: 19, y: 17}, {x: 19, y: 19}, {x: 14, y: 19}},
- {{x: 16, y: 20}, {x: 16, y: 21, hint: 3}},
- {{x: 12, y: 22}, {x: 21, y: 22}, {x: 21, y: 24}, {x: 12, y: 24}},
- {{x: 7, y: 1}, {x: 12, y: 1}},
- {{x: 32, y: 5}, {x: 39, y: 5}},
- {{x: 7, y: 6}, {x: 12, y: 6}},
- {{x: 1, y: 13}, {x: 6, y: 13}},
- {{x: 14, y: 13}, {x: 19, y: 13}},
- {{x: 15, y: 18}, {x: 18, y: 18}},
- {{x: 13, y: 23}, {x: 20, y: 23}},
- },
- false,
- },
-
- // 8 Interwined lines.
- {
- []string{
- " +-----+-------+",
- " | | |",
- " | | |",
- " +----+-----+---- |",
- "--------+----+-----+-------+---+",
- " | | | | |",
- " | | | | | | |",
- " | | | | | | |",
- " | | | | | | |",
- "--------+----+-----+-------+---+-----+---+--+",
- " | | | | | | | |",
- " | | | | | | | |",
- " | -+-----+-------+---+-----+ | |",
- " | | | | | | | |",
- " | | | | +-----+---+--+",
- " | | | | |",
- " | | | | |",
- " --------+-----+-------+---------+---+-----",
- " | | | | |",
- " +-----+-------+---------+---+",
- },
- // TODO(dhobsd): it's a tad overwhelming.
- nil,
- nil,
- nil,
- false,
- },
-
- // 9 Indented box
- {
- []string{
- "",
- " +-+",
- " | |",
- " +-+",
- },
- []string{"Path{[(1,1) (2,1) (3,1) (3,2) (3,3) (2,3) (1,3) (1,2)]}"},
- []string{""},
- [][]point{{{x: 1, y: 1}, {x: 3, y: 1}, {x: 3, y: 3}, {x: 1, y: 3}}},
- false,
- },
-
- // 10 Diagonal lines with arrows
- {
- []string{
- "^ ^",
- " \\ /",
- " \\ /",
- " \\ /",
- " v v",
- },
- []string{"Path{[(0,0) (1,1) (2,2) (3,3) (4,4)]}", "Path{[(11,0) (10,1) (9,2) (8,3) (7,4)]}"},
- []string{"", ""},
- [][]point{
- {{x: 0, y: 0, hint: 2}, {x: 4, y: 4, hint: 3}},
- {{x: 11, y: 0, hint: 2}, {x: 7, y: 4, hint: 3}},
- },
- false,
- },
-
- // 11 Diagonal lines forming an object
- {
- []string{
- " .-----.",
- " / \\",
- " / \\",
- "+ +",
- "| |",
- "| |",
- "+ +",
- " \\ /",
- " \\ /",
- " '-----'",
- },
- []string{"Path{[(3,0) (4,0) (5,0) (6,0) (7,0) (8,0) (9,0) (10,1) (11,2) (12,3) (12,4) (12,5) (12,6) (11,7) (10,8) (9,9) (8,9) (7,9) (6,9) (5,9) (4,9) (3,9) (2,8) (1,7) (0,6) (0,5) (0,4) (0,3) (1,2) (2,1)]}"},
- []string{""},
- [][]point{{
- {x: 3, y: 0},
- {x: 9, y: 0},
- {x: 12, y: 3},
- {x: 12, y: 6},
- {x: 9, y: 9},
- {x: 3, y: 9},
- {x: 0, y: 6},
- {x: 0, y: 3},
- }},
- false,
- },
-
- // 12 A2S logo
- {
- []string{
- ".-------------------------.",
- "| |",
- "| .---.-. .-----. .-----. |",
- "| | .-. | +--> | | <--+ |",
- "| | '-' | | <--+ +--> | |",
- "| '---'-' '-----' '-----' |",
- "| ascii 2 svg |",
- "| |",
- "'-------------------------'",
- },
- []string{
- "Path{[(0,0) (1,0) (2,0) (3,0) (4,0) (5,0) (6,0) (7,0) (8,0) (9,0) (10,0) (11,0) (12,0) (13,0) (14,0) (15,0) (16,0) (17,0) (18,0) (19,0) (20,0) (21,0) (22,0) (23,0) (24,0) (25,0) (26,0) (26,1) (26,2) (26,3) (26,4) (26,5) (26,6) (26,7) (26,8) (25,8) (24,8) (23,8) (22,8) (21,8) (20,8) (19,8) (18,8) (17,8) (16,8) (15,8) (14,8) (13,8) (12,8) (11,8) (10,8) (9,8) (8,8) (7,8) (6,8) (5,8) (4,8) (3,8) (2,8) (1,8) (0,8) (0,7) (0,6) (0,5) (0,4) (0,3) (0,2) (0,1)]}",
- "Path{[(2,2) (3,2) (4,2) (5,2) (6,2) (7,2) (8,2) (8,3) (8,4) (8,5) (7,5) (6,5) (5,5) (4,5) (3,5) (2,5) (2,4) (2,3)]}",
- "Path{[(2,2) (3,2) (4,2) (5,2) (6,2) (7,2) (8,2) (8,3) (8,4) (8,5) (7,5) (6,5) (6,4) (5,4) (4,4) (4,3) (5,3) (6,3)]}",
- "Path{[(10,2) (11,2) (12,2) (13,2) (14,2) (15,2) (16,2) (16,3) (16,4) (15,4) (14,4) (13,4)]}",
- "Path{[(10,2) (11,2) (12,2) (13,2) (14,2) (15,2) (16,2) (16,3) (16,4) (16,5) (15,5) (14,5) (13,5) (12,5) (11,5) (10,5) (10,4) (10,3)]}",
- "Path{[(18,2) (19,2) (20,2) (21,2) (22,2) (23,2) (24,2) (24,3) (23,3) (22,3) (21,3)]}",
- "Path{[(18,2) (19,2) (20,2) (21,2) (22,2) (23,2) (24,2) (24,3) (24,4) (24,5) (23,5) (22,5) (21,5) (20,5) (19,5) (18,5) (18,4) (19,4) (20,4) (21,4)]}",
- "Path{[(18,2) (19,2) (20,2) (21,2) (22,2) (23,2) (24,2) (24,3) (24,4) (24,5) (23,5) (22,5) (21,5) (20,5) (19,5) (18,5) (18,4) (18,3)]}",
- "Path{[(10,3) (11,3) (12,3) (13,3)]}",
- "Text{(3,6) \"ascii\"}",
- "Text{(13,6) \"2\"}",
- "Text{(20,6) \"svg\"}",
- },
- []string{"", "", "", "", "", "", "", "", "", "ascii", "2", "svg"},
- [][]point{
- {{x: 0, y: 0}, {x: 26, y: 0}, {x: 26, y: 8}, {x: 0, y: 8}},
- {{x: 2, y: 2}, {x: 8, y: 2}, {x: 8, y: 5}, {x: 2, y: 5}},
- {{x: 2, y: 2}, {x: 8, y: 2}, {x: 8, y: 5}, {x: 6, y: 5}, {x: 6, y: 4}, {x: 4, y: 4}, {x: 4, y: 3}, {x: 6, y: 3}},
- {{x: 10, y: 2}, {x: 16, y: 2}, {x: 16, y: 4}, {x: 13, y: 4, hint: 3}},
- {{x: 10, y: 2}, {x: 16, y: 2}, {x: 16, y: 5}, {x: 10, y: 5}},
- {{x: 18, y: 2}, {x: 24, y: 2}, {x: 24, y: 3}, {x: 21, y: 3, hint: 3}},
- {{x: 18, y: 2}, {x: 24, y: 2}, {x: 24, y: 5}, {x: 18, y: 5}, {x: 18, y: 4}, {x: 21, y: 4, hint: 3}},
- {{x: 18, y: 2}, {x: 24, y: 2}, {x: 24, y: 5}, {x: 18, y: 5}},
- {{x: 10, y: 3}, {x: 13, y: 3, hint: 3}},
- {{x: 3, y: 6}, {x: 7, y: 6}},
- {{x: 13, y: 6}},
- {{x: 20, y: 6}, {x: 22, y: 6}},
- },
- false,
- },
-
- // 13 Ticks and dots in lines.
- {
- []string{
- " ------x----->",
- "",
- " <-----*------",
- },
- []string{"Path{[(1,0) (2,0) (3,0) (4,0) (5,0) (6,0) (7,0) (8,0) (9,0) (10,0) (11,0) (12,0) (13,0)]}", "Path{[(1,2) (2,2) (3,2) (4,2) (5,2) (6,2) (7,2) (8,2) (9,2) (10,2) (11,2) (12,2) (13,2)]}"},
- []string{"", ""},
- [][]point{
- {
- {x: 1, y: 0, hint: 0},
- {x: 2, y: 0, hint: 0},
- {x: 3, y: 0, hint: 0},
- {x: 4, y: 0, hint: 0},
- {x: 5, y: 0, hint: 0},
- {x: 6, y: 0, hint: 0},
- {x: 7, y: 0, hint: 4},
- {x: 8, y: 0, hint: 0},
- {x: 9, y: 0, hint: 0},
- {x: 10, y: 0, hint: 0},
- {x: 11, y: 0, hint: 0},
- {x: 12, y: 0, hint: 0},
- {x: 13, y: 0, hint: 3},
- },
- {
- {x: 1, y: 2, hint: 2},
- {x: 2, y: 2, hint: 0},
- {x: 3, y: 2, hint: 0},
- {x: 4, y: 2, hint: 0},
- {x: 5, y: 2, hint: 0},
- {x: 6, y: 2, hint: 0},
- {x: 7, y: 2, hint: 5},
- {x: 8, y: 2, hint: 0},
- {x: 9, y: 2, hint: 0},
- {x: 10, y: 2, hint: 0},
- {x: 11, y: 2, hint: 0},
- {x: 12, y: 2, hint: 0},
- {x: 13, y: 2, hint: 0},
- },
- },
- true,
- },
-
- // 14 Multiple closed path on one object
- {
- []string{
- "+-+-+",
- "| | |",
- "+-+-+",
- },
- []string{
- "Path{[(0,0) (1,0) (2,0) (3,0) (4,0) (4,1) (4,2) (3,2) (2,2) (1,2) (0,2) (0,1)]}",
- "Path{[(0,0) (1,0) (2,0) (3,0) (4,0) (4,1) (4,2) (3,2) (2,2) (2,1)]}", // TODO (2,0)
- },
- []string{"", ""},
- [][]point{
- {
- {0, 0, 0}, {1, 0, 0}, {2, 0, 0}, {3, 0, 0}, {4, 0, 0}, {4, 1, 0}, {4, 2, 0},
- {3, 2, 0}, {2, 2, 0}, {1, 2, 0}, {0, 2, 0}, {0, 1, 0},
- },
- {
- {0, 0, 0}, {1, 0, 0}, {2, 0, 0}, {3, 0, 0}, {4, 0, 0}, {4, 1, 0}, {4, 2, 0},
- {3, 2, 0}, {2, 2, 0}, {2, 1, 0}, // TODO: {2, 0, 0}
- },
- },
- true,
- },
- }
- for i, line := range data {
- c, err := newCanvas([]byte(strings.Join(line.input, "\n")))
- if err != nil {
- t.Fatalf("Test %d: error creating canvas: %s", i, err)
- }
- objs := c.objects()
- if line.strings != nil {
- if got := getStrings(objs); !reflect.DeepEqual(line.strings, got) {
- t.Errorf("%d: expected %q, but got %q", i, line.strings, got)
- }
- }
- if line.texts != nil {
- if got := getTexts(objs); !reflect.DeepEqual(line.texts, got) {
- t.Errorf("%d: expected %q, but got %q", i, line.texts, got)
- }
- }
- if line.points != nil {
- if line.allPoints == false {
- if got := getCorners(objs); !reflect.DeepEqual(line.points, got) {
- t.Errorf("%d: expected %q, but got %q", i, line.points, got)
- }
- } else {
- if got := getPoints(objs); !reflect.DeepEqual(line.points, got) {
- t.Errorf("%d: expected %q, but got %q", i, line.points, got)
- }
- }
- }
- }
-}
-
-func TestNewCanvasBroken(t *testing.T) {
- // These are the ones that do not give the desired result.
- t.Parallel()
- data := []struct {
- input []string
- strings []string
- texts []string
- corners [][]point
- }{
- // 0 URL
- {
- []string{
- "github.com/foo/bar",
- },
- []string{"Text{(0,0) \"github.com/foo/bar\"}"},
- []string{"github.com/foo/bar"},
- [][]point{{{x: 0, y: 0}, {x: 17, y: 0}}},
- },
-
- // 1 Merged boxes
- {
- []string{
- "+-+-+",
- "| | |",
- "+-+-+",
- },
- []string{"Path{[(0,0) (1,0) (2,0) (3,0) (4,0) (4,1) (4,2) (3,2) (2,2) (1,2) (0,2) (0,1)]}", "Path{[(0,0) (1,0) (2,0) (3,0) (4,0) (4,1) (4,2) (3,2) (2,2) (2,1)]}"},
- []string{"", ""},
- // TODO(dhobsd): BROKEN.
- [][]point{
- {{x: 0, y: 0}, {x: 4, y: 0}, {x: 4, y: 2}, {x: 0, y: 2}},
- {{x: 0, y: 0}, {x: 4, y: 0}, {x: 4, y: 2}, {x: 2, y: 2}, {x: 2, y: 1}},
- },
- },
-
- // 2 Adjacent boxes
- {
- // TODO(dhobsd): BROKEN. This one is hard, as it can be seen as 3 boxes
- // but that is not what is desired.
- []string{
- "+-++-+",
- "| || |",
- "+-++-+",
- },
- []string{
- "Path{[(0,0) (1,0) (2,0) (3,0) (4,0) (5,0) (5,1) (5,2) (4,2) (3,2) (2,2) (1,2) (0,2) (0,1)]}",
- "Path{[(0,0) (1,0) (2,0) (3,0) (4,0) (5,0) (5,1) (5,2) (4,2) (3,2) (2,2) (2,1)]}",
- "Path{[(0,0) (1,0) (2,0) (3,0) (4,0) (5,0) (5,1) (5,2) (4,2) (3,2) (3,1)]}",
- },
- []string{"", "", ""},
- [][]point{
- {{x: 0, y: 0}, {x: 5, y: 0}, {x: 5, y: 2}, {x: 0, y: 2}},
- {{x: 0, y: 0}, {x: 5, y: 0}, {x: 5, y: 2}, {x: 2, y: 2}, {x: 2, y: 1}},
- {{x: 0, y: 0}, {x: 5, y: 0}, {x: 5, y: 2}, {x: 3, y: 2}, {x: 3, y: 1}},
- },
- },
- }
- for i, line := range data {
- c, err := newCanvas([]byte(strings.Join(line.input, "\n")))
- if err != nil {
- t.Fatalf("Test %d: error creating canvas: %s", i, err)
- }
- objs := c.objects()
- if line.strings != nil {
- if got := getStrings(objs); !reflect.DeepEqual(line.strings, got) {
- t.Errorf("%d: expected %q, but got %q", i, line.strings, got)
- }
- }
- if line.texts != nil {
- if got := getTexts(objs); !reflect.DeepEqual(line.texts, got) {
- t.Errorf("%d: expected %q, but got %q", i, line.texts, got)
- }
- }
- if line.corners != nil {
- if got := getCorners(objs); !reflect.DeepEqual(line.corners, got) {
- t.Errorf("%d: expected %q, but got %q", i, line.corners, got)
- }
- }
- }
-}
-
-func TestPointsToCorners(t *testing.T) {
- t.Parallel()
- data := []struct {
- in []point
- expected []point
- closed bool
- }{
- {
- []point{{x: 0, y: 0}, {x: 1, y: 0}},
- []point{{x: 0, y: 0}, {x: 1, y: 0}},
- false,
- },
- {
- []point{{x: 0, y: 0}, {x: 1, y: 0}, {x: 2, y: 0}},
- []point{{x: 0, y: 0}, {x: 2, y: 0}},
- false,
- },
- {
- []point{{x: 0, y: 0}, {x: 1, y: 0}, {x: 1, y: 1}},
- []point{{x: 0, y: 0}, {x: 1, y: 0}, {x: 1, y: 1}},
- false,
- },
- {
- []point{
- {x: 0, y: 0}, {x: 1, y: 0}, {x: 2, y: 0}, {x: 2, y: 1}, {x: 2, y: 2},
- {x: 1, y: 2}, {x: 0, y: 2}, {x: 0, y: 1},
- },
- []point{{x: 0, y: 0}, {x: 2, y: 0}, {x: 2, y: 2}, {x: 0, y: 2}},
- true,
- },
- {
- []point{{x: 0, y: 0}, {x: 1, y: 0}, {x: 1, y: 1}, {x: 0, y: 1}},
- []point{{x: 0, y: 0}, {x: 1, y: 0}, {x: 1, y: 1}, {x: 0, y: 1}},
- // TODO(dhobsd): Unexpected; broken.
- false,
- },
- }
- for i, line := range data {
- p, c := pointsToCorners(line.in)
- if !reflect.DeepEqual(line.expected, p) {
- t.Errorf("%d: expected %v, but got %v", i, line.expected, p)
- }
- if line.closed != c {
- t.Errorf("%d: expected close == %v, but got %v", i, line.closed, c)
- }
- }
-}
-
-func BenchmarkT(b *testing.B) {
- data := []string{
- " +-----+-------+",
- " | | |",
- " | | |",
- " +----+-----+---- |",
- "--------+----+-----+-------+---+",
- " | | | | |",
- " | | | | | | |",
- " | | | | | | |",
- " | | | | | | |",
- "--------+----+-----+-------+---+-----+---+--+",
- " | | | | | | | |",
- " | | | | | | | |",
- " | -+-----+-------+---+-----+ | |",
- " | | | | | | | |",
- " | | | | +-----+---+--+",
- " | | | | |",
- " | | | | |",
- " --------+-----+-------+---------+---+-----",
- " | | | | |",
- " +-----+-------+---------+---+",
- "",
- "",
- }
- chunk := []byte(strings.Join(data, "\n"))
- input := make([]byte, 0, len(chunk)*b.N)
- for range b.N {
- input = append(input, chunk...)
- }
- expected := 30 * b.N
- b.ResetTimer()
- c, err := newCanvas(input)
- if err != nil {
- b.Fatalf("Error creating canvas: %s", err)
- }
-
- objs := c.objects()
- if len(objs) != expected {
- b.Fatalf("%d != %d", len(objs), expected)
- }
-}
-
-// Private details.
-
-func getPoints(objs []*object) [][]point {
- out := [][]point{}
- for _, obj := range objs {
- out = append(out, obj.Points())
- }
- return out
-}
-
-func getTexts(objs []*object) []string {
- out := []string{}
- for _, obj := range objs {
- t := obj.Text()
- if !obj.isJustText() {
- out = append(out, "")
- } else if len(t) > 0 {
- out = append(out, string(t))
- } else {
- panic("failed")
- }
- }
- return out
-}
-
-func getStrings(objs []*object) []string {
- out := []string{}
- for _, obj := range objs {
- out = append(out, obj.String())
- }
- return out
-}
-
-func getCorners(objs []*object) [][]point {
- out := make([][]point, len(objs))
- for i, obj := range objs {
- out[i] = obj.Corners()
- }
- return out
-}
DELETED parser/draw/char.go
Index: parser/draw/char.go
==================================================================
--- parser/draw/char.go
+++ parser/draw/char.go
@@ -1,137 +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.
-//
-// This file was originally created by the ASCIIToSVG contributors under an MIT
-// license, but later changed to fulfil the needs of Zettelstore. The following
-// statements affects the original code as found on
-// https://github.com/asciitosvg/asciitosvg (Commit:
-// ca82a5ce41e2190a05e07af6e8b3ea4e3256a283, 2020-11-20):
-//
-// Copyright 2012 - 2018 The ASCIIToSVG Contributors
-// All rights reserved.
-//
-// SPDX-License-Identifier: EUPL-1.2
-// SPDX-FileCopyrightText: 2022-present Detlef Stern
-//-----------------------------------------------------------------------------
-
-package draw
-
-import "unicode"
-
-type char rune
-
-func (c char) isTextStart() bool {
- r := rune(c)
- return unicode.IsLetter(r) || unicode.IsNumber(r) || unicode.IsSymbol(r)
-}
-
-func (c char) isTextCont() bool {
- return unicode.IsPrint(rune(c))
-}
-
-func (c char) isSpace() bool {
- return unicode.IsSpace(rune(c))
-}
-
-// isPathStart returns true on any form of ascii art that can start a graph.
-func (c char) isPathStart() bool {
- return (c.isCorner() || c.isHorizontal() || c.isVertical() || c.isArrowHorizontalLeft() || c.isArrowVerticalUp() || c.isDiagonal()) &&
- !c.isTick() && !c.isDot()
-}
-
-func (c char) isCorner() bool {
- return c == '.' || c == '\'' || c == '+'
-}
-
-func (c char) isRoundedCorner() bool {
- return c == '.' || c == '\''
-}
-
-func (c char) isDashedHorizontal() bool {
- return c == '='
-}
-
-func (c char) isHorizontal() bool {
- return c.isDashedHorizontal() || c.isTick() || c.isDot() || c == '-'
-}
-
-func (c char) isDashedVertical() bool {
- return c == ':'
-}
-
-func (c char) isVertical() bool {
- return c.isDashedVertical() || c.isTick() || c.isDot() || c == '|'
-}
-
-func (c char) isDashed() bool {
- return c.isDashedHorizontal() || c.isDashedVertical()
-}
-
-func (c char) isArrowHorizontalLeft() bool {
- return c == '<'
-}
-
-func (c char) isArrowHorizontal() bool {
- return c.isArrowHorizontalLeft() || c == '>'
-}
-
-func (c char) isArrowVerticalUp() bool {
- return c == '^'
-}
-
-func (c char) isArrowVertical() bool {
- return c.isArrowVerticalUp() || c == 'v'
-}
-
-func (c char) isArrow() bool {
- return c.isArrowHorizontal() || c.isArrowVertical()
-}
-
-func (c char) isDiagonalNorthEast() bool {
- return c == '/'
-}
-
-func (c char) isDiagonalSouthEast() bool {
- return c == '\\'
-}
-
-func (c char) isDiagonal() bool {
- return c.isDiagonalNorthEast() || c.isDiagonalSouthEast()
-}
-
-func (c char) isTick() bool {
- return c == 'x'
-}
-
-func (c char) isDot() bool {
- return c == '*'
-}
-
-// Diagonal transitions are special: you can move lines diagonally, you can move diagonally from
-// corners to edges or lines, but you cannot move diagonally between corners.
-func (c char) canDiagonalFrom(from char) bool {
- if from.isArrowVertical() || from.isCorner() {
- return c.isDiagonal()
- }
- if from.isDiagonal() {
- return c.isDiagonal() || c.isCorner() || c.isArrowVertical() || c.isHorizontal() || c.isVertical()
- }
- if from.isHorizontal() || from.isVertical() {
- return c.isDiagonal()
- }
- return false
-}
-
-func (c char) canHorizontal() bool {
- return c.isHorizontal() || c.isCorner() || c.isArrowHorizontal()
-}
-
-func (c char) canVertical() bool {
- return c.isVertical() || c.isCorner() || c.isArrowVertical()
-}
DELETED parser/draw/draw.go
Index: parser/draw/draw.go
==================================================================
--- parser/draw/draw.go
+++ parser/draw/draw.go
@@ -1,136 +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 draw provides a parser to create SVG from ASCII drawing.
-//
-// It is not a parser registered by the general parser framework (directed by
-// metadata "syntax" of a zettel). It will be used when a zettel is evaluated.
-package draw
-
-import (
- "strconv"
-
- "t73f.de/r/zsc/attrs"
- "t73f.de/r/zsc/input"
- "zettelstore.de/z/ast"
- "zettelstore.de/z/parser"
- "zettelstore.de/z/zettel/meta"
-)
-
-func init() {
- parser.Register(&parser.Info{
- Name: meta.SyntaxDraw,
- AltNames: []string{},
- IsASTParser: true,
- IsTextFormat: true,
- IsImageFormat: false,
- ParseBlocks: parseBlocks,
- ParseInlines: parseInlines,
- })
-}
-
-const (
- defaultFont = ""
- defaultScaleX = 10
- defaultScaleY = 20
-)
-
-func parseBlocks(inp *input.Input, m *meta.Meta, _ string) ast.BlockSlice {
- font := m.GetDefault("font", defaultFont)
- scaleX := m.GetNumber("x-scale", defaultScaleX)
- scaleY := m.GetNumber("y-scale", defaultScaleY)
- if scaleX < 1 || 1000000 < scaleX {
- scaleX = defaultScaleX
- }
- if scaleY < 1 || 1000000 < scaleY {
- scaleY = defaultScaleY
- }
-
- canvas, err := newCanvas(inp.Src[inp.Pos:])
- if err != nil {
- return ast.BlockSlice{ast.CreateParaNode(canvasErrMsg(err)...)}
- }
- svg := canvasToSVG(canvas, font, int(scaleX), int(scaleY))
- if len(svg) == 0 {
- return ast.BlockSlice{ast.CreateParaNode(noSVGErrMsg()...)}
- }
- return ast.BlockSlice{&ast.BLOBNode{
- Description: parser.ParseDescription(m),
- Syntax: meta.SyntaxSVG,
- Blob: svg,
- }}
-}
-
-func parseInlines(inp *input.Input, _ string) ast.InlineSlice {
- canvas, err := newCanvas(inp.Src[inp.Pos:])
- if err != nil {
- return canvasErrMsg(err)
- }
- svg := canvasToSVG(canvas, defaultFont, defaultScaleX, defaultScaleY)
- if len(svg) == 0 {
- return noSVGErrMsg()
- }
- return ast.InlineSlice{&ast.EmbedBLOBNode{
- Attrs: nil,
- Syntax: meta.SyntaxSVG,
- Blob: svg,
- Inlines: nil,
- }}
-}
-
-// ParseDrawBlock parses the content of an eval verbatim node into an SVG image BLOB.
-func ParseDrawBlock(vn *ast.VerbatimNode) ast.BlockNode {
- font := defaultFont
- if val, found := vn.Attrs.Get("font"); found {
- font = val
- }
- scaleX := getScale(vn.Attrs, "x-scale", defaultScaleX)
- scaleY := getScale(vn.Attrs, "y-scale", defaultScaleY)
-
- canvas, err := newCanvas(vn.Content)
- if err != nil {
- return ast.CreateParaNode(canvasErrMsg(err)...)
- }
- if scaleX < 1 || 1000000 < scaleX {
- scaleX = defaultScaleX
- }
- if scaleY < 1 || 1000000 < scaleY {
- scaleY = defaultScaleY
- }
- svg := canvasToSVG(canvas, font, scaleX, scaleY)
- if len(svg) == 0 {
- return ast.CreateParaNode(noSVGErrMsg()...)
- }
- return &ast.BLOBNode{
- Description: nil, // TODO: look for attribute "summary" / "title"
- Syntax: meta.SyntaxSVG,
- Blob: svg,
- }
-}
-
-func getScale(a attrs.Attributes, key string, defVal int) int {
- if val, found := a.Get(key); found {
- if n, err := strconv.Atoi(val); err == nil && 0 < n && n < 100000 {
- return n
- }
- }
- return defVal
-}
-
-func canvasErrMsg(err error) ast.InlineSlice {
- return ast.CreateInlineSliceFromWords("Error:", err.Error())
-}
-
-func noSVGErrMsg() ast.InlineSlice {
- return ast.CreateInlineSliceFromWords("NO", "IMAGE")
-}
DELETED parser/draw/draw_test.go
Index: parser/draw/draw_test.go
==================================================================
--- parser/draw/draw_test.go
+++ parser/draw/draw_test.go
@@ -1,31 +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 draw_test
-
-import (
- "testing"
-
- "t73f.de/r/zsc/input"
- "zettelstore.de/z/config"
- "zettelstore.de/z/parser"
- "zettelstore.de/z/zettel/meta"
-)
-
-func FuzzParseBlocks(f *testing.F) {
- f.Fuzz(func(t *testing.T, src []byte) {
- t.Parallel()
- inp := input.NewInput(src)
- parser.ParseBlocks(inp, nil, meta.SyntaxDraw, config.NoHTML)
- })
-}
DELETED parser/draw/object.go
Index: parser/draw/object.go
==================================================================
--- parser/draw/object.go
+++ parser/draw/object.go
@@ -1,218 +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.
-//
-// This file was originally created by the ASCIIToSVG contributors under an MIT
-// license, but later changed to fulfil the needs of Zettelstore. The following
-// statements affects the original code as found on
-// https://github.com/asciitosvg/asciitosvg (Commit:
-// ca82a5ce41e2190a05e07af6e8b3ea4e3256a283, 2020-11-20):
-//
-// Copyright 2012 - 2018 The ASCIIToSVG Contributors
-// All rights reserved.
-//
-// SPDX-License-Identifier: EUPL-1.2
-// SPDX-FileCopyrightText: 2022-present Detlef Stern
-//-----------------------------------------------------------------------------
-
-package draw
-
-import "fmt"
-
-// object represents one of an open path, a closed path, or text.
-type object struct {
- // points always starts with the top most, then left most point, proceeding to the right.
- points []point
- text []rune
- corners []point
- isText bool
- isClosed bool
- isDashed bool
-}
-
-// Points returns all the points occupied by this Object. Every object has at least one point,
-// and all points are both in-order and contiguous.
-func (o *object) Points() []point { return o.points }
-
-// Corners returns all the corners (change of direction) along the path.
-func (o *object) Corners() []point { return o.corners }
-
-// IsClosed is true if the object is composed of a closed path.
-func (o *object) IsClosed() bool { return o.isClosed }
-
-// IsDashed is true if this object is a path object, and lines should be drawn dashed.
-func (o *object) IsDashed() bool { return o.isDashed }
-
-// Text returns the text associated with this object if textual, and nil otherwise.
-func (o *object) Text() []rune {
- return o.text
-}
-
-func (o *object) isOpenPath() bool { return !o.isClosed && !o.isText }
-func (o *object) isClosedPath() bool { return o.isClosed && !o.isText }
-func (o *object) isJustText() bool { return o.isText }
-
-func (o *object) String() string {
- if o.isJustText() {
- return fmt.Sprintf("Text{%s %q}", o.points[0], string(o.text))
- }
- return fmt.Sprintf("Path{%v}", o.points)
-}
-
-// seal finalizes the object, setting its text, its corners, and its various rendering hints.
-func (o *object) seal(c *canvas) {
- if c.at(o.points[0]).isArrow() {
- o.points[0].hint = startMarker
- c.hasStartMarker = true
- }
-
- if c.at(o.points[len(o.points)-1]).isArrow() {
- o.points[len(o.points)-1].hint = endMarker
- c.hasEndMarker = true
- }
-
- o.corners, o.isClosed = pointsToCorners(o.points)
- o.text = make([]rune, len(o.points))
-
- for i, p := range o.points {
- ch := c.at(p)
- if !o.isJustText() {
- if ch.isTick() {
- o.points[i].hint = tick
- } else if ch.isDot() {
- o.points[i].hint = dot
- }
- if ch.isDashed() {
- o.isDashed = true
- }
-
- for _, corner := range o.corners {
- if corner.x == p.x && corner.y == p.y && c.at(p).isRoundedCorner() {
- o.points[i].hint = roundedCorner
- }
- }
- }
- o.text[i] = rune(ch)
- }
-}
-
-// objects implements a sortable collection of Object interfaces.
-type objects []*object
-
-func (o objects) Len() int { return len(o) }
-func (o objects) Swap(i, j int) { o[i], o[j] = o[j], o[i] }
-
-// Less returns in order top most, then left most.
-func (o objects) Less(i, j int) bool {
- // TODO(dhobsd): This doesn't catch every z-index case we could possibly want. We should
- // support z-indexing of objects through an a2s tag.
- l := o[i]
- r := o[j]
- lt := l.isJustText()
- rt := r.isJustText()
- if lt != rt {
- return rt
- }
- lp := l.Points()[0]
- rp := r.Points()[0]
- if lp.y != rp.y {
- return lp.y < rp.y
- }
- return lp.x < rp.x
-}
-
-const (
- dirNone = iota // No directionality
- dirH // Horizontal
- dirV // Vertical
- dirSE // South-East
- dirSW // South-West
- dirNW // North-West
- dirNE // North-East
-)
-
-// pointsToCorners returns all the corners (points at which there is a change of directionality) for
-// a path. It additionally returns a truth value indicating whether the points supplied indicate a
-// closed path.
-func pointsToCorners(points []point) ([]point, bool) {
- l := len(points)
- // A path containing fewer than 3 points can neither be closed, nor change direction.
- if l < 3 {
- return points, false
- }
- out := []point{points[0]}
-
- dir := dirNone
- if isHorizontal(points[0], points[1]) {
- dir = dirH
- } else if isVertical(points[0], points[1]) {
- dir = dirV
- } else if isDiagonalSE(points[0], points[1]) {
- dir = dirSE
- } else if isDiagonalSW(points[0], points[1]) {
- dir = dirSW
- } else if isDiagonalNW(points[0], points[1]) {
- dir = dirNW
- } else if isDiagonalNE(points[0], points[1]) {
- dir = dirNE
- } else {
- panic(fmt.Errorf("discontiguous points: %+v", points))
- }
-
- cornerFunc := func(idx, newDir int) {
- if dir != newDir {
- out = append(out, points[idx-1])
- dir = newDir
- }
- }
-
- // Starting from the third point, check to see if the directionality between points P and
- // P-1 has changed.
- for i := 2; i < l; i++ {
- if isHorizontal(points[i-1], points[i]) {
- cornerFunc(i, dirH)
- } else if isVertical(points[i-1], points[i]) {
- cornerFunc(i, dirV)
- } else if isDiagonalSE(points[i-1], points[i]) {
- cornerFunc(i, dirSE)
- } else if isDiagonalSW(points[i-1], points[i]) {
- cornerFunc(i, dirSW)
- } else if isDiagonalNW(points[i-1], points[i]) {
- cornerFunc(i, dirNW)
- } else if isDiagonalNE(points[i-1], points[i]) {
- cornerFunc(i, dirNE)
- } else {
- panic(fmt.Errorf("discontiguous points: %+v", points))
- }
- }
-
- // Check if the points indicate a closed path. If not, append the last point.
- last := points[l-1]
- closed := true
- closedFunc := func(newDir int) {
- if dir != newDir {
- closed = false
- out = append(out, last)
- }
- }
- if isHorizontal(points[0], last) {
- closedFunc(dirH)
- } else if isVertical(points[0], last) {
- closedFunc(dirV)
- } else if isDiagonalNE(last, points[0]) {
- closedFunc(dirNE)
- } else {
- // Note: we'll always find any closed polygon from its top-left-most point. If it
- // is closed, it must be closed in the north-easterly direction, thus we don't test
- // for any other types of polygone closure.
- closed = false
- out = append(out, last)
- }
-
- return out, closed
-}
DELETED parser/draw/point.go
Index: parser/draw/point.go
==================================================================
--- parser/draw/point.go
+++ parser/draw/point.go
@@ -1,65 +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.
-//
-// This file was originally created by the ASCIIToSVG contributors under an MIT
-// license, but later changed to fulfil the needs of Zettelstore. The following
-// statements affects the original code as found on
-// https://github.com/asciitosvg/asciitosvg (Commit:
-// ca82a5ce41e2190a05e07af6e8b3ea4e3256a283, 2020-11-20):
-//
-// Copyright 2012 - 2018 The ASCIIToSVG Contributors
-// All rights reserved.
-//
-// SPDX-License-Identifier: EUPL-1.2
-// SPDX-FileCopyrightText: 2022-present Detlef Stern
-//-----------------------------------------------------------------------------
-
-package draw
-
-import "fmt"
-
-// A renderHint suggests ways the SVG renderer may appropriately represent this point.
-type renderHint uint8
-
-const (
- _ renderHint = iota
- roundedCorner // the renderer should smooth corners on this path.
- startMarker // this point should have an SVG marker-start attribute.
- endMarker // this point should have an SVG marker-end attribute.
- tick // the renderer should mark a tick in the path at this point.
- dot // the renderer should insert a filled dot in the path at this point.
-)
-
-// A point is an X,Y coordinate in the diagram's grid. The grid represents (0, 0) as the top-left
-// of the diagram. The point also provides hints to the renderer as to how it should be interpreted.
-type point struct {
- x, y int
- hint renderHint
-}
-
-// String implements fmt.Stringer on Point.
-func (p point) String() string { return fmt.Sprintf("(%d,%d)", p.x, p.y) }
-
-// isHorizontal returns true if p1 and p2 are horizontally aligned.
-func isHorizontal(p1, p2 point) bool {
- d := p1.x - p2.x
- return d <= 1 && d >= -1 && p1.y == p2.y
-}
-
-// isVertical returns true if p1 and p2 are vertically aligned.
-func isVertical(p1, p2 point) bool {
- d := p1.y - p2.y
- return d <= 1 && d >= -1 && p1.x == p2.x
-}
-
-// The following functions return true when the diagonals are connected in various compass directions.
-func isDiagonalSE(p1, p2 point) bool { return p1.x-p2.x == -1 && p1.y-p2.y == -1 }
-func isDiagonalSW(p1, p2 point) bool { return p1.x-p2.x == 1 && p1.y-p2.y == -1 }
-func isDiagonalNW(p1, p2 point) bool { return p1.x-p2.x == 1 && p1.y-p2.y == 1 }
-func isDiagonalNE(p1, p2 point) bool { return p1.x-p2.x == -1 && p1.y-p2.y == 1 }
DELETED parser/draw/svg.go
Index: parser/draw/svg.go
==================================================================
--- parser/draw/svg.go
+++ parser/draw/svg.go
@@ -1,273 +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.
-//
-// This file was originally created by the ASCIIToSVG contributors under an MIT
-// license, but later changed to fulfil the needs of Zettelstore. The following
-// statements affects the original code as found on
-// https://github.com/asciitosvg/asciitosvg (Commit:
-// ca82a5ce41e2190a05e07af6e8b3ea4e3256a283, 2020-11-20):
-//
-// Copyright 2012 - 2018 The ASCIIToSVG Contributors
-// All rights reserved.
-//
-// SPDX-License-Identifier: EUPL-1.2
-// SPDX-FileCopyrightText: 2022-present Detlef Stern
-//-----------------------------------------------------------------------------
-
-package draw
-
-import (
- "bytes"
- "fmt"
- "io"
- "strings"
-
- "zettelstore.de/z/strfun"
-)
-
-// canvasToSVG renders the supplied asciitosvg.Canvas to SVG, based on the supplied options.
-func canvasToSVG(c *canvas, font string, scaleX, scaleY int) []byte {
- if len(c.objects()) == 0 {
- return nil
- }
- if font == "" {
- font = "monospace"
- }
-
- var b bytes.Buffer
- fmt.Fprintf(&b,
- `")
- return b.Bytes()
-}
-
-const (
- nameStartMarker = "iPointer"
- nameEndMarker = "Pointer"
-)
-
-func writeMarkerDefs(w io.Writer, c *canvas, scaleX, scaleY int) {
- const markerTag = ` `
- x := float64(scaleX) / 2
- y := float64(scaleY) / 2
- if c.hasStartMarker {
- fmt.Fprintf(w, markerTag, nameStartMarker, x, y, "M 10 0 L 10 10 L 0 5 z")
- }
- if c.hasEndMarker {
- fmt.Fprintf(w, markerTag, nameEndMarker, x, y, "M 0 0 L 10 5 L 0 10 z")
- }
-}
-
-const pathTag = ` `
-
-func writeClosedPaths(w io.Writer, c *canvas, scaleX, scaleY int) {
- first := true
- for i, obj := range c.objects() {
- if !obj.isClosedPath() {
- continue
- }
- if first {
- io.WriteString(w, ``)
- first = false
- }
- opts := ""
- if obj.IsDashed() {
- opts = `stroke-dasharray="5 5" `
- }
-
- fmt.Fprintf(w, pathTag, "closed", i, opts, flatten(obj.Points(), scaleX, scaleY)+"Z")
- }
- if !first {
- io.WriteString(w, " ")
- }
-}
-
-func writeOpenPaths(w io.Writer, c *canvas, scaleX, scaleY int) {
- const optStartMarker = `marker-start="url(#` + nameStartMarker + `)" `
- const optEndMarker = `marker-end="url(#` + nameEndMarker + `)" `
-
- first := true
- for i, obj := range c.objects() {
- if !obj.isOpenPath() {
- continue
- }
- if first {
- io.WriteString(w, ``)
- first = false
- }
- points := obj.Points()
- for _, p := range points {
- switch p.hint {
- case dot:
- sp := scale(p, scaleX, scaleY)
- fmt.Fprintf(w, ` `, sp.X, sp.Y)
- case tick:
- sp := scale(p, scaleX, scaleY)
- const tickLine = ` `
- fmt.Fprintf(w, tickLine, sp.X-4, sp.Y-4, sp.X+4, sp.Y+4)
- fmt.Fprintf(w, tickLine, sp.X+4, sp.Y-4, sp.X-4, sp.Y+4)
- }
- }
-
- opts := ""
- if obj.IsDashed() {
- opts += `stroke-dasharray="5 5" `
- }
- if points[0].hint == startMarker {
- opts += optStartMarker
- }
- if points[len(points)-1].hint == endMarker {
- opts += optEndMarker
- }
- fmt.Fprintf(w, pathTag, "open", i, opts, flatten(points, scaleX, scaleY))
- }
- if !first {
- io.WriteString(w, " ")
- }
-}
-
-func writeTexts(w io.Writer, c *canvas, font string, scaleX, scaleY int) {
- fontSize := float64(scaleY) * 0.75
- deltaX := float64(scaleX) / 4
- deltaY := float64(scaleY) / 4
- first := true
- for i, obj := range c.objects() {
- if !obj.isJustText() {
- continue
- }
- if first {
- fmt.Fprintf(w, ``, font, fontSize)
- first = false
- }
-
- text := string(obj.Text())
- sp := scale(obj.Points()[0], scaleX, scaleY)
- fmt.Fprintf(w,
- `%s `,
- i, sp.X-deltaX, sp.Y+deltaY, escape(text))
- }
- if !first {
- io.WriteString(w, " ")
- }
-}
-
-func escape(s string) string {
- var sb strings.Builder
- strfun.XMLEscape(&sb, s)
- return sb.String()
-}
-
-type scaledPoint struct {
- X float64
- Y float64
- Hint renderHint
-}
-
-func scale(p point, scaleX, scaleY int) scaledPoint {
- return scaledPoint{
- X: (float64(p.x) + .5) * float64(scaleX),
- Y: (float64(p.y) + .5) * float64(scaleY),
- Hint: p.hint,
- }
-}
-
-func flatten(points []point, scaleX, scaleY int) string {
- var result strings.Builder
-
- // Scaled start point, and previous point (which is always initially the start point).
- sp := scale(points[0], scaleX, scaleY)
- pp := sp
-
- for i, cp := range points {
- p := scale(cp, scaleX, scaleY)
-
- // Our start point is represented by a single moveto command (unless the start point
- // is a rounded corner) as the shape will be closed with the Z command automatically
- // if we have a closed polygon. If our start point is a rounded corner, we have to go
- // ahead and draw that curve.
- if i == 0 {
- if cp.hint == roundedCorner {
- fmt.Fprintf(&result, "M %g %g Q %g %g %g %g ", p.X, p.Y+10, p.X, p.Y, p.X+10, p.Y)
- continue
- }
-
- fmt.Fprintf(&result, "M %g %g ", p.X, p.Y)
- continue
- }
-
- // If this point has a rounded corner, we need to calculate the curve. This algorithm
- // only works when the shapes are drawn in a clockwise manner.
- if cp.hint == roundedCorner {
- // The control point is always the original corner.
- cx := p.X
- cy := p.Y
-
- sx, sy, ex, ey := 0., 0., 0., 0.
-
- // We need to know the next point to determine which way to turn.
- var np scaledPoint
- if i == len(points)-1 {
- np = sp
- } else {
- np = scale(points[i+1], scaleX, scaleY)
- }
-
- if pp.X == p.X {
- // If we're on the same vertical axis, our starting X coordinate is
- // the same as the control point coordinate
- sx = p.X
-
- // Offset start point from control point in the proper direction.
- if pp.Y < p.Y {
- sy = p.Y - 10
- } else {
- sy = p.Y + 10
- }
-
- ey = p.Y
- // Offset endpoint from control point in the proper direction.
- if np.X < p.X {
- ex = p.X - 10
- } else {
- ex = p.X + 10
- }
- } else if pp.Y == p.Y {
- // Horizontal decisions mirror vertical's above.
- sy = p.Y
- if pp.X < p.X {
- sx = p.X - 10
- } else {
- sx = p.X + 10
- }
- ex = p.X
- if np.Y <= p.Y {
- ey = p.Y - 10
- } else {
- ey = p.Y + 10
- }
- }
-
- fmt.Fprintf(&result, "L %g %g Q %g %g %g %g ", sx, sy, cx, cy, ex, ey)
- } else {
- // Just draw a straight line.
- fmt.Fprintf(&result, "L %g %g ", p.X, p.Y)
- }
-
- pp = p
- }
- return result.String()
-}
DELETED parser/draw/svg_test.go
Index: parser/draw/svg_test.go
==================================================================
--- parser/draw/svg_test.go
+++ parser/draw/svg_test.go
@@ -1,76 +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.
-//
-// This file was originally created by the ASCIIToSVG contributors under an MIT
-// license, but later changed to fulfil the needs of Zettelstore. The following
-// statements affects the original code as found on
-// https://github.com/asciitosvg/asciitosvg (Commit:
-// ca82a5ce41e2190a05e07af6e8b3ea4e3256a283, 2020-11-20):
-//
-// Copyright 2012 - 2018 The ASCIIToSVG Contributors
-// All rights reserved.
-//
-// SPDX-License-Identifier: EUPL-1.2
-// SPDX-FileCopyrightText: 2022-present Detlef Stern
-//-----------------------------------------------------------------------------
-
-package draw
-
-import (
- "strings"
- "testing"
-)
-
-func TestCanvasToSVG(t *testing.T) {
- t.Parallel()
- data := []struct {
- input []string
- length int
- }{
- // 0 Box with dashed corners and text
- {
- []string{
- "+--.",
- "|Hi:",
- "+--+",
- },
- 482,
- },
-
- // 2 Ticks and dots in lines.
- {
- []string{
- " ------x----->",
- "",
- " <-----*------",
- },
- 1084,
- },
-
- // 3 Just text
- {
- []string{
- " foo",
- },
- 261,
- },
- }
- for i, line := range data {
- canvas, err := newCanvas([]byte(strings.Join(line.input, "\n")))
- if err != nil {
- t.Fatalf("Error creating canvas: %s", err)
- }
- actual := string(canvasToSVG(canvas, "", 9, 16))
- // TODO(dhobsd): Use golden file? Worth postponing once output is actually
- // nice.
- if line.length != len(actual) {
- t.Errorf("%d: expected length %d, but got %d\n%q", i, line.length, len(actual), actual)
- }
- }
-}
Index: parser/markdown/markdown.go
==================================================================
--- parser/markdown/markdown.go
+++ parser/markdown/markdown.go
@@ -1,76 +1,68 @@
//-----------------------------------------------------------------------------
-// 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 markdown provides a parser for markdown.
package markdown
import (
"bytes"
"fmt"
- "strconv"
"strings"
gm "github.com/yuin/goldmark"
gmAst "github.com/yuin/goldmark/ast"
gmText "github.com/yuin/goldmark/text"
- "t73f.de/r/zsc/attrs"
- "t73f.de/r/zsc/input"
"zettelstore.de/z/ast"
- "zettelstore.de/z/encoder/textenc"
+ "zettelstore.de/z/domain/meta"
+ "zettelstore.de/z/encoder"
+ "zettelstore.de/z/input"
"zettelstore.de/z/parser"
- "zettelstore.de/z/zettel/meta"
+ "zettelstore.de/z/runes"
)
func init() {
parser.Register(&parser.Info{
- Name: meta.SyntaxMarkdown,
- AltNames: []string{meta.SyntaxMD},
- IsASTParser: true,
- IsTextFormat: true,
- IsImageFormat: false,
- ParseBlocks: parseBlocks,
- ParseInlines: parseInlines,
+ Name: "markdown",
+ AltNames: []string{"md"},
+ ParseBlocks: parseBlocks,
+ ParseInlines: parseInlines,
})
}
-func parseBlocks(inp *input.Input, _ *meta.Meta, _ string) ast.BlockSlice {
+func parseBlocks(inp *input.Input, m *meta.Meta, syntax string) ast.BlockSlice {
p := parseMarkdown(inp)
- return p.acceptBlockChildren(p.docNode)
+ return p.acceptBlockSlice(p.docNode)
}
func parseInlines(inp *input.Input, syntax string) ast.InlineSlice {
- bs := parseBlocks(inp, nil, syntax)
- return bs.FirstParagraphInlines()
+ panic("markdown.parseInline not yet implemented")
}
func parseMarkdown(inp *input.Input) *mdP {
source := []byte(inp.Src[inp.Pos:])
parser := gm.DefaultParser()
node := parser.Parse(gmText.NewReader(source))
- textEnc := textenc.Create()
+ textEnc := encoder.Create("text", nil)
return &mdP{source: source, docNode: node, textEnc: textEnc}
}
type mdP struct {
source []byte
docNode gmAst.Node
- textEnc *textenc.Encoder
+ textEnc encoder.Encoder
}
-func (p *mdP) acceptBlockChildren(docNode gmAst.Node) ast.BlockSlice {
+func (p *mdP) acceptBlockSlice(docNode gmAst.Node) ast.BlockSlice {
if docNode.Type() != gmAst.TypeDocument {
panic(fmt.Sprintf("Expected document, but got node type %v", docNode.Type()))
}
result := make(ast.BlockSlice, 0, docNode.ChildCount())
for child := docNode.FirstChild(); child != nil; child = child.NextSibling() {
@@ -91,11 +83,11 @@
case *gmAst.TextBlock:
return p.acceptTextBlock(n)
case *gmAst.Heading:
return p.acceptHeading(n)
case *gmAst.ThematicBreak:
- return p.acceptThematicBreak()
+ return p.acceptThematicBreak(n)
case *gmAst.CodeBlock:
return p.acceptCodeBlock(n)
case *gmAst.FencedCodeBlock:
return p.acceptFencedCodeBlock(n)
case *gmAst.Blockquote:
@@ -107,87 +99,86 @@
}
panic(fmt.Sprintf("Unhandled block node of kind %v", node.Kind()))
}
func (p *mdP) acceptParagraph(node *gmAst.Paragraph) ast.ItemNode {
- if is := p.acceptInlineChildren(node); len(is) > 0 {
- return &ast.ParaNode{Inlines: is}
+ if ins := p.acceptInlineSlice(node); len(ins) > 0 {
+ return &ast.ParaNode{
+ Inlines: ins,
+ }
}
return nil
}
func (p *mdP) acceptHeading(node *gmAst.Heading) *ast.HeadingNode {
return &ast.HeadingNode{
Level: node.Level,
- Inlines: p.acceptInlineChildren(node),
+ Inlines: p.acceptInlineSlice(node),
Attrs: nil,
}
}
-func (*mdP) acceptThematicBreak() *ast.HRuleNode {
+func (p *mdP) acceptThematicBreak(node *gmAst.ThematicBreak) *ast.HRuleNode {
return &ast.HRuleNode{
Attrs: nil, //TODO
}
}
func (p *mdP) acceptCodeBlock(node *gmAst.CodeBlock) *ast.VerbatimNode {
return &ast.VerbatimNode{
- Kind: ast.VerbatimProg,
- Attrs: nil, //TODO
- Content: p.acceptRawText(node),
+ Code: ast.VerbatimProg,
+ Attrs: nil, //TODO
+ Lines: p.acceptRawText(node),
}
}
func (p *mdP) acceptFencedCodeBlock(node *gmAst.FencedCodeBlock) *ast.VerbatimNode {
- var a attrs.Attributes
+ var attrs *ast.Attributes
if language := node.Language(p.source); len(language) > 0 {
- a = a.Set("class", "language-"+cleanText(language, true))
+ attrs = attrs.Set("class", "language-"+cleanText(string(language), true))
}
return &ast.VerbatimNode{
- Kind: ast.VerbatimProg,
- Attrs: a,
- Content: p.acceptRawText(node),
+ Code: ast.VerbatimProg,
+ Attrs: attrs,
+ Lines: p.acceptRawText(node),
}
}
-func (p *mdP) acceptRawText(node gmAst.Node) []byte {
+func (p *mdP) acceptRawText(node gmAst.Node) []string {
lines := node.Lines()
- result := make([]byte, 0, 512)
- for i := range lines.Len() {
+ result := make([]string, 0, lines.Len())
+ for i := 0; i < lines.Len(); i++ {
s := lines.At(i)
line := s.Value(p.source)
if l := len(line); l > 0 {
if l > 1 && line[l-2] == '\r' && line[l-1] == '\n' {
line = line[0 : l-2]
} else if line[l-1] == '\n' || line[l-1] == '\r' {
line = line[0 : l-1]
}
}
- if i > 0 {
- result = append(result, '\n')
- }
- result = append(result, line...)
+ result = append(result, string(line))
}
return result
}
func (p *mdP) acceptBlockquote(node *gmAst.Blockquote) *ast.NestedListNode {
return &ast.NestedListNode{
- Kind: ast.NestedListQuote,
+ Code: ast.NestedListQuote,
Items: []ast.ItemSlice{
p.acceptItemSlice(node),
},
}
}
func (p *mdP) acceptList(node *gmAst.List) ast.ItemNode {
- kind := ast.NestedListUnordered
- var a attrs.Attributes
+ code := ast.NestedListUnordered
+ var attrs *ast.Attributes
if node.IsOrdered() {
- kind = ast.NestedListOrdered
+ code = ast.NestedListOrdered
if node.Start != 1 {
- a = a.Set("start", strconv.Itoa(node.Start))
+ attrs = attrs.Set("start", fmt.Sprintf("%d", node.Start))
}
}
items := make([]ast.ItemSlice, 0, node.ChildCount())
for child := node.FirstChild(); child != nil; child = child.NextSibling() {
item, ok := child.(*gmAst.ListItem)
@@ -195,13 +186,13 @@
panic(fmt.Sprintf("Expected list item node, but got %v", child.Kind()))
}
items = append(items, p.acceptItemSlice(item))
}
return &ast.NestedListNode{
- Kind: kind,
+ Code: code,
Items: items,
- Attrs: a,
+ Attrs: attrs,
}
}
func (p *mdP) acceptItemSlice(node gmAst.Node) ast.ItemSlice {
result := make(ast.ItemSlice, 0, node.ChildCount())
@@ -212,35 +203,34 @@
}
return result
}
func (p *mdP) acceptTextBlock(node *gmAst.TextBlock) ast.ItemNode {
- if is := p.acceptInlineChildren(node); len(is) > 0 {
- return &ast.ParaNode{Inlines: is}
+ if ins := p.acceptInlineSlice(node); len(ins) > 0 {
+ return &ast.ParaNode{
+ Inlines: ins,
+ }
}
return nil
}
func (p *mdP) acceptHTMLBlock(node *gmAst.HTMLBlock) *ast.VerbatimNode {
- content := p.acceptRawText(node)
+ lines := p.acceptRawText(node)
if node.HasClosure() {
- closure := node.ClosureLine.Value(p.source)
+ closure := string(node.ClosureLine.Value(p.source))
if l := len(closure); l > 1 && closure[l-1] == '\n' {
closure = closure[:l-1]
}
- if len(content) > 1 {
- content = append(content, '\n')
- }
- content = append(content, closure...)
+ lines = append(lines, closure)
}
return &ast.VerbatimNode{
- Kind: ast.VerbatimHTML,
- Content: content,
+ Code: ast.VerbatimHTML,
+ Lines: lines,
}
}
-func (p *mdP) acceptInlineChildren(node gmAst.Node) ast.InlineSlice {
+func (p *mdP) acceptInlineSlice(node gmAst.Node) ast.InlineSlice {
result := make(ast.InlineSlice, 0, node.ChildCount())
for child := node.FirstChild(); child != nil; child = child.NextSibling() {
if inlines := p.acceptInline(child); inlines != nil {
result = append(result, inlines...)
}
@@ -278,11 +268,11 @@
}
ins := splitText(string(segment.Value(p.source)))
result := make(ast.InlineSlice, 0, len(ins)+1)
for _, in := range ins {
if tn, ok := in.(*ast.TextNode); ok {
- tn.Text = cleanText([]byte(tn.Text), true)
+ tn.Text = cleanText(tn.Text, true)
}
result = append(result, in)
}
if node.HardLineBreak() {
result = append(result, &ast.BreakNode{Hard: true})
@@ -293,18 +283,18 @@
}
// splitText transform the text into a sequence of TextNode and SpaceNode
func splitText(text string) ast.InlineSlice {
if text == "" {
- return nil
+ return ast.InlineSlice{}
}
result := make(ast.InlineSlice, 0, 1)
state := 0 // 0=unknown,1=non-spaces,2=spaces
lastPos := 0
for pos, ch := range text {
- if input.IsSpace(ch) {
+ if runes.IsSpace(ch) {
if state == 1 {
result = append(result, &ast.TextNode{Text: text[lastPos:pos]})
lastPos = pos
}
state = 2
@@ -325,158 +315,176 @@
panic(fmt.Sprintf("Unexpected state %v", state))
}
return result
}
-var ignoreAfterBS = map[byte]struct{}{
- '!': {}, '"': {}, '#': {}, '$': {}, '%': {}, '&': {}, '\'': {}, '(': {},
- ')': {}, '*': {}, '+': {}, ',': {}, '-': {}, '.': {}, '/': {}, ':': {},
- ';': {}, '<': {}, '=': {}, '>': {}, '?': {}, '@': {}, '[': {}, '\\': {},
- ']': {}, '^': {}, '_': {}, '`': {}, '{': {}, '|': {}, '}': {}, '~': {},
+var ignoreAfterBS = map[byte]bool{
+ '!': true, '"': true, '#': true, '$': true, '%': true, '&': true,
+ '\'': true, '(': true, ')': true, '*': true, '+': true, ',': true,
+ '-': true, '.': true, '/': true, ':': true, ';': true, '<': true,
+ '=': true, '>': true, '?': true, '@': true, '[': true, '\\': true,
+ ']': true, '^': true, '_': true, '`': true, '{': true, '|': true,
+ '}': true, '~': true,
}
// cleanText removes backslashes from TextNodes and expands entities
-func cleanText(text []byte, cleanBS bool) string {
+func cleanText(text string, cleanBS bool) string {
lastPos := 0
var sb strings.Builder
for pos, ch := range text {
if pos < lastPos {
continue
}
if ch == '&' {
- inp := input.NewInput([]byte(text[pos:]))
+ inp := input.NewInput(text[pos:])
if s, ok := inp.ScanEntity(); ok {
- sb.Write(text[lastPos:pos])
+ sb.WriteString(text[lastPos:pos])
sb.WriteString(s)
lastPos = pos + inp.Pos
}
continue
}
- if cleanBS && ch == '\\' && pos < len(text)-1 {
- if _, found := ignoreAfterBS[text[pos+1]]; found {
- sb.Write(text[lastPos:pos])
- sb.WriteByte(text[pos+1])
- lastPos = pos + 2
- }
+ if cleanBS && ch == '\\' && pos < len(text)-1 && ignoreAfterBS[text[pos+1]] {
+ sb.WriteString(text[lastPos:pos])
+ sb.WriteByte(text[pos+1])
+ lastPos = pos + 2
}
+ }
+ if lastPos == 0 {
+ return text
}
if lastPos < len(text) {
- sb.Write(text[lastPos:])
+ sb.WriteString(text[lastPos:])
}
return sb.String()
}
func (p *mdP) acceptCodeSpan(node *gmAst.CodeSpan) ast.InlineSlice {
return ast.InlineSlice{
&ast.LiteralNode{
- Kind: ast.LiteralProg,
- Attrs: nil, //TODO
- Content: cleanCodeSpan(node.Text(p.source)),
+ Code: ast.LiteralProg,
+ Attrs: nil, //TODO
+ Text: cleanCodeSpan(string(node.Text(p.source))),
},
}
}
-func cleanCodeSpan(text []byte) []byte {
- if len(text) == 0 {
- return nil
+func cleanCodeSpan(text string) string {
+ if text == "" {
+ return ""
}
lastPos := 0
- var buf bytes.Buffer
+ var sb strings.Builder
for pos, ch := range text {
if ch == '\n' {
- buf.Write(text[lastPos:pos])
+ sb.WriteString(text[lastPos:pos])
if pos < len(text)-1 {
- buf.WriteByte(' ')
+ sb.WriteByte(' ')
}
lastPos = pos + 1
}
}
- buf.Write(text[lastPos:])
- return buf.Bytes()
+ if lastPos == 0 {
+ return text
+ }
+ sb.WriteString(text[lastPos:])
+ return sb.String()
}
func (p *mdP) acceptEmphasis(node *gmAst.Emphasis) ast.InlineSlice {
- kind := ast.FormatEmph
+ code := ast.FormatEmph
if node.Level == 2 {
- kind = ast.FormatStrong
+ code = ast.FormatStrong
}
return ast.InlineSlice{
&ast.FormatNode{
- Kind: kind,
+ Code: code,
Attrs: nil, //TODO
- Inlines: p.acceptInlineChildren(node),
+ Inlines: p.acceptInlineSlice(node),
},
}
}
func (p *mdP) acceptLink(node *gmAst.Link) ast.InlineSlice {
- ref := ast.ParseReference(cleanText(node.Destination, true))
- var a attrs.Attributes
- if title := node.Title; len(title) > 0 {
- a = a.Set("title", cleanText(title, true))
+ ref := ast.ParseReference(cleanText(string(node.Destination), true))
+ var attrs *ast.Attributes
+ if title := string(node.Title); len(title) > 0 {
+ attrs = attrs.Set("title", cleanText(title, true))
}
return ast.InlineSlice{
&ast.LinkNode{
Ref: ref,
- Inlines: p.acceptInlineChildren(node),
- Attrs: a,
+ Inlines: p.acceptInlineSlice(node),
+ OnlyRef: false,
+ Attrs: attrs,
},
}
}
func (p *mdP) acceptImage(node *gmAst.Image) ast.InlineSlice {
- ref := ast.ParseReference(cleanText(node.Destination, true))
- var a attrs.Attributes
- if title := node.Title; len(title) > 0 {
- a = a.Set("title", cleanText(title, true))
+ ref := ast.ParseReference(cleanText(string(node.Destination), true))
+ var attrs *ast.Attributes
+ if title := string(node.Title); len(title) > 0 {
+ attrs = attrs.Set("title", cleanText(title, true))
}
return ast.InlineSlice{
- &ast.EmbedRefNode{
+ &ast.ImageNode{
Ref: ref,
Inlines: p.flattenInlineSlice(node),
- Attrs: a,
+ Attrs: attrs,
},
}
}
func (p *mdP) flattenInlineSlice(node gmAst.Node) ast.InlineSlice {
- is := p.acceptInlineChildren(node)
+ ins := p.acceptInlineSlice(node)
var sb strings.Builder
- _, err := p.textEnc.WriteInlines(&sb, &is)
+ _, err := p.textEnc.WriteInlines(&sb, ins)
if err != nil {
panic(err)
}
- if sb.Len() == 0 {
+ text := sb.String()
+ if text == "" {
return nil
}
- return ast.InlineSlice{&ast.TextNode{Text: sb.String()}}
+ return ast.InlineSlice{
+ &ast.TextNode{
+ Text: text,
+ },
+ }
}
func (p *mdP) acceptAutoLink(node *gmAst.AutoLink) ast.InlineSlice {
- u := node.URL(p.source)
+ url := node.URL(p.source)
if node.AutoLinkType == gmAst.AutoLinkEmail &&
- !bytes.HasPrefix(bytes.ToLower(u), []byte("mailto:")) {
- u = append([]byte("mailto:"), u...)
+ !bytes.HasPrefix(bytes.ToLower(url), []byte("mailto:")) {
+ url = append([]byte("mailto:"), url...)
+ }
+ ref := ast.ParseReference(cleanText(string(url), false))
+ label := node.Label(p.source)
+ if len(label) == 0 {
+ label = url
}
return ast.InlineSlice{
&ast.LinkNode{
- Ref: ast.ParseReference(cleanText(u, false)),
- Inlines: nil,
- Attrs: nil, // TODO
+ Ref: ref,
+ Inlines: ast.InlineSlice{&ast.TextNode{Text: string(label)}},
+ OnlyRef: true,
+ Attrs: nil, //TODO
},
}
}
func (p *mdP) acceptRawHTML(node *gmAst.RawHTML) ast.InlineSlice {
- segs := make([][]byte, 0, node.Segments.Len())
- for i := range node.Segments.Len() {
+ segs := make([]string, 0, node.Segments.Len())
+ for i := 0; i < node.Segments.Len(); i++ {
segment := node.Segments.At(i)
- segs = append(segs, segment.Value(p.source))
+ segs = append(segs, string(segment.Value(p.source)))
}
return ast.InlineSlice{
&ast.LiteralNode{
- Kind: ast.LiteralHTML,
- Attrs: nil, // TODO: add HTML as language
- Content: bytes.Join(segs, nil),
+ Code: ast.LiteralHTML,
+ Attrs: nil, // TODO: add HTML as language
+ Text: strings.Join(segs, ""),
},
}
}
Index: parser/markdown/markdown_test.go
==================================================================
--- parser/markdown/markdown_test.go
+++ parser/markdown/markdown_test.go
@@ -1,18 +1,16 @@
//-----------------------------------------------------------------------------
-// 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 markdown provides a parser for markdown.
package markdown
import (
"strings"
"testing"
@@ -19,11 +17,10 @@
"zettelstore.de/z/ast"
)
func TestSplitText(t *testing.T) {
- t.Parallel()
var testcases = []struct {
text string
exp string
}{
{"", ""},
Index: parser/none/none.go
==================================================================
--- parser/none/none.go
+++ parser/none/none.go
@@ -1,41 +1,105 @@
//-----------------------------------------------------------------------------
-// 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 none provides a none-parser, e.g. for zettel with just metadata.
+// Package none provides a none-parser for meta data.
package none
import (
- "t73f.de/r/zsc/input"
"zettelstore.de/z/ast"
+ "zettelstore.de/z/domain/meta"
+ "zettelstore.de/z/input"
"zettelstore.de/z/parser"
- "zettelstore.de/z/zettel/meta"
)
func init() {
parser.Register(&parser.Info{
- Name: meta.SyntaxNone,
- AltNames: []string{},
- IsASTParser: false,
- IsTextFormat: false,
- IsImageFormat: false,
- ParseBlocks: parseBlocks,
- ParseInlines: parseInlines,
+ Name: meta.ValueSyntaxNone,
+ AltNames: []string{},
+ ParseBlocks: parseBlocks,
+ ParseInlines: parseInlines,
})
}
-func parseBlocks(*input.Input, *meta.Meta, string) ast.BlockSlice { return nil }
+func parseBlocks(inp *input.Input, m *meta.Meta, syntax string) ast.BlockSlice {
+ descrlist := &ast.DescriptionListNode{}
+ for _, p := range m.Pairs(true) {
+ descrlist.Descriptions = append(
+ descrlist.Descriptions, getDescription(p.Key, p.Value))
+ }
+ return ast.BlockSlice{descrlist}
+}
+
+func getDescription(key, value string) ast.Description {
+ return ast.Description{
+ Term: ast.InlineSlice{&ast.TextNode{Text: key}},
+ Descriptions: []ast.DescriptionSlice{
+ ast.DescriptionSlice{
+ &ast.ParaNode{
+ Inlines: convertToInlineSlice(value, meta.Type(key)),
+ },
+ },
+ },
+ }
+}
+
+func convertToInlineSlice(value string, dt *meta.DescriptionType) ast.InlineSlice {
+ var sliceData []string
+ if dt.IsSet {
+ sliceData = meta.ListFromValue(value)
+ if len(sliceData) == 0 {
+ return ast.InlineSlice{}
+ }
+ } else {
+ sliceData = []string{value}
+ }
+ var makeLink bool
+ switch dt {
+ case meta.TypeID, meta.TypeIDSet:
+ makeLink = true
+ }
+
+ result := make(ast.InlineSlice, 0, 2*len(sliceData)-1)
+ for i, val := range sliceData {
+ if i > 0 {
+ result = append(result, &ast.SpaceNode{Lexeme: " "})
+ }
+ tn := &ast.TextNode{Text: val}
+ if makeLink {
+ result = append(result, &ast.LinkNode{
+ Ref: ast.ParseReference(val),
+ Inlines: ast.InlineSlice{tn},
+ })
+ } else {
+ result = append(result, tn)
+ }
+ }
+ return result
+}
-func parseInlines(inp *input.Input, _ string) ast.InlineSlice {
+func parseInlines(inp *input.Input, syntax string) ast.InlineSlice {
inp.SkipToEOL()
- return nil
+ return ast.InlineSlice{
+ &ast.FormatNode{
+ Code: ast.FormatSpan,
+ Attrs: &ast.Attributes{Attrs: map[string]string{"class": "warning"}},
+ Inlines: ast.InlineSlice{
+ &ast.TextNode{Text: "parser.meta.ParseInlines:"},
+ &ast.SpaceNode{Lexeme: " "},
+ &ast.TextNode{Text: "not"},
+ &ast.SpaceNode{Lexeme: " "},
+ &ast.TextNode{Text: "possible"},
+ &ast.SpaceNode{Lexeme: " "},
+ &ast.TextNode{Text: "("},
+ &ast.TextNode{Text: inp.Src[0:inp.Pos]},
+ &ast.TextNode{Text: ")"},
+ },
+ },
+ }
}
Index: parser/parser.go
==================================================================
--- parser/parser.go
+++ parser/parser.go
@@ -1,73 +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 parser provides a generic interface to a range of different parsers.
package parser
import (
- "context"
- "fmt"
- "strings"
+ "log"
- "t73f.de/r/zsc/api"
- "t73f.de/r/zsc/input"
"zettelstore.de/z/ast"
"zettelstore.de/z/config"
+ "zettelstore.de/z/domain"
+ "zettelstore.de/z/domain/meta"
+ "zettelstore.de/z/input"
"zettelstore.de/z/parser/cleaner"
- "zettelstore.de/z/zettel"
- "zettelstore.de/z/zettel/meta"
)
// Info describes a single parser.
//
// Before ParseBlocks() or ParseInlines() is called, ensure the input stream to
// be valid. This can ce achieved on calling inp.Next() after the input stream
// was created.
type Info struct {
- Name string
- AltNames []string
- IsASTParser bool
- IsTextFormat bool
- IsImageFormat bool
- ParseBlocks func(*input.Input, *meta.Meta, string) ast.BlockSlice
- ParseInlines func(*input.Input, string) ast.InlineSlice
+ Name string
+ AltNames []string
+ ParseBlocks func(*input.Input, *meta.Meta, string) ast.BlockSlice
+ ParseInlines func(*input.Input, string) ast.InlineSlice
}
var registry = map[string]*Info{}
// Register the parser (info) for later retrieval.
-func Register(pi *Info) {
+func Register(pi *Info) *Info {
if _, ok := registry[pi.Name]; ok {
- panic(fmt.Sprintf("Parser %q already registered", pi.Name))
+ log.Fatalf("Parser %q already registered", pi.Name)
}
registry[pi.Name] = pi
for _, alt := range pi.AltNames {
if _, ok := registry[alt]; ok {
- panic(fmt.Sprintf("Parser %q already registered", alt))
+ log.Fatalf("Parser %q already registered", alt)
}
registry[alt] = pi
}
-}
-
-// GetSyntaxes returns a list of syntaxes implemented by all registered parsers.
-func GetSyntaxes() []string {
- result := make([]string, 0, len(registry))
- for syntax := range registry {
- result = append(result, syntax)
- }
- return result
+ return pi
}
// Get the parser (info) by name. If name not found, use a default parser.
func Get(name string) *Info {
if pi := registry[name]; pi != nil {
@@ -74,99 +57,46 @@
return pi
}
if pi := registry["plain"]; pi != nil {
return pi
}
- panic(fmt.Sprintf("No parser for %q found", name))
-}
-
-// IsASTParser returns whether the given syntax parses text into an AST or not.
-func IsASTParser(syntax string) bool {
- pi, ok := registry[syntax]
- if !ok {
- return false
- }
- return pi.IsASTParser
-}
-
-// IsImageFormat returns whether the given syntax is known to be an image format.
-func IsImageFormat(syntax string) bool {
- pi, ok := registry[syntax]
- if !ok {
- return false
- }
- return pi.IsImageFormat
+ log.Printf("No parser for %q found", name)
+ panic("No default parser registered")
}
// ParseBlocks parses some input and returns a slice of block nodes.
-func ParseBlocks(inp *input.Input, m *meta.Meta, syntax string, hi config.HTMLInsecurity) ast.BlockSlice {
+func ParseBlocks(inp *input.Input, m *meta.Meta, syntax string) ast.BlockSlice {
bs := Get(syntax).ParseBlocks(inp, m, syntax)
- cleaner.CleanBlockSlice(&bs, hi.AllowHTML(syntax))
+ cleaner.CleanupBlockSlice(bs)
return bs
}
// ParseInlines parses some input and returns a slice of inline nodes.
func ParseInlines(inp *input.Input, syntax string) ast.InlineSlice {
- // Do not clean, because we don't know the context where this function will be called.
return Get(syntax).ParseInlines(inp, syntax)
}
// ParseMetadata parses a string as Zettelmarkup, resulting in an inline slice.
// Typically used to parse the title or other metadata of type Zettelmarkup.
-func ParseMetadata(value string) ast.InlineSlice {
- return ParseInlines(input.NewInput([]byte(value)), meta.SyntaxZmk)
-}
-
-// ParseSpacedText returns an inline slice that consists just of test and space node.
-// No Zettelmarkup parsing is done. It is typically used to transform the zettel title into an inline slice.
-func ParseSpacedText(s string) ast.InlineSlice {
- return ast.CreateInlineSliceFromWords(meta.ListFromValue(s)...)
-}
-
-// NormalizedSpacedText returns the given string, but normalize multiple spaces to one space.
-func NormalizedSpacedText(s string) string { return strings.Join(meta.ListFromValue(s), " ") }
-
-// ParseDescription returns a suitable description stored in the metadata as an inline slice.
-// This is done for an image in most cases.
-func ParseDescription(m *meta.Meta) ast.InlineSlice {
- if m == nil {
- return nil
- }
- if descr, found := m.Get(api.KeySummary); found {
- in := ParseMetadata(descr)
- cleaner.CleanInlineLinks(&in)
- return in
- }
- if title, found := m.Get(api.KeyTitle); found {
- return ParseSpacedText(title)
- }
- return ast.CreateInlineSliceFromWords("Zettel", "without", "title:", m.Zid.String())
+func ParseMetadata(title string) ast.InlineSlice {
+ return ParseInlines(input.NewInput(title), meta.ValueSyntaxZmk)
}
// ParseZettel parses the zettel based on the syntax.
-func ParseZettel(ctx context.Context, zettel zettel.Zettel, syntax string, rtConfig config.Config) *ast.ZettelNode {
+func ParseZettel(zettel domain.Zettel, syntax string, rtConfig config.Config) *ast.ZettelNode {
m := zettel.Meta
- inhMeta := m
- if rtConfig != nil {
- inhMeta = rtConfig.AddDefaultValues(ctx, inhMeta)
- }
+ inhMeta := rtConfig.AddDefaultValues(m)
if syntax == "" {
- syntax = inhMeta.GetDefault(api.KeySyntax, meta.DefaultSyntax)
+ syntax, _ = inhMeta.Get(meta.KeySyntax)
}
parseMeta := inhMeta
- if syntax == meta.SyntaxNone {
+ if syntax == meta.ValueSyntaxNone {
parseMeta = m
}
-
- hi := config.NoHTML
- if rtConfig != nil {
- hi = rtConfig.GetHTMLInsecurity()
- }
return &ast.ZettelNode{
Meta: m,
Content: zettel.Content,
Zid: m.Zid,
InhMeta: inhMeta,
- Ast: ParseBlocks(input.NewInput(zettel.Content.AsBytes()), parseMeta, syntax, hi),
- Syntax: syntax,
+ Ast: ParseBlocks(input.NewInput(zettel.Content.AsString()), parseMeta, syntax),
}
}
DELETED parser/parser_test.go
Index: parser/parser_test.go
==================================================================
--- parser/parser_test.go
+++ parser/parser_test.go
@@ -1,68 +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 parser_test
-
-import (
- "testing"
-
- "zettelstore.de/z/parser"
- "zettelstore.de/z/strfun"
- "zettelstore.de/z/zettel/meta"
-
- _ "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 TestParserType(t *testing.T) {
- syntaxSet := strfun.NewSet(parser.GetSyntaxes()...)
- testCases := []struct {
- syntax string
- ast bool
- image bool
- }{
- {meta.SyntaxHTML, false, false},
- {meta.SyntaxCSS, false, false},
- {meta.SyntaxDraw, true, false},
- {meta.SyntaxGif, false, true},
- {meta.SyntaxJPEG, false, true},
- {meta.SyntaxJPG, false, true},
- {meta.SyntaxMarkdown, true, false},
- {meta.SyntaxMD, true, false},
- {meta.SyntaxNone, false, false},
- {meta.SyntaxPlain, false, false},
- {meta.SyntaxPNG, false, true},
- {meta.SyntaxSVG, false, true},
- {meta.SyntaxSxn, false, false},
- {meta.SyntaxText, false, false},
- {meta.SyntaxTxt, false, false},
- {meta.SyntaxWebp, false, true},
- {meta.SyntaxZmk, true, false},
- }
- for _, tc := range testCases {
- delete(syntaxSet, tc.syntax)
- if got := parser.IsASTParser(tc.syntax); got != tc.ast {
- t.Errorf("Syntax %q is AST: %v, but got %v", tc.syntax, tc.ast, got)
- }
- if got := parser.IsImageFormat(tc.syntax); got != tc.image {
- t.Errorf("Syntax %q is image: %v, but got %v", tc.syntax, tc.image, got)
- }
- }
- for syntax := range syntaxSet {
- t.Errorf("Forgot to test syntax %q", syntax)
- }
-}
Index: parser/plain/plain.go
==================================================================
--- parser/plain/plain.go
+++ parser/plain/plain.go
@@ -1,164 +1,116 @@
//-----------------------------------------------------------------------------
-// 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 plain provides a parser for plain text data.
package plain
import (
- "bytes"
"strings"
- "t73f.de/r/sx/sxreader"
- "t73f.de/r/zsc/attrs"
- "t73f.de/r/zsc/input"
"zettelstore.de/z/ast"
+ "zettelstore.de/z/domain/meta"
+ "zettelstore.de/z/input"
"zettelstore.de/z/parser"
- "zettelstore.de/z/zettel/meta"
+ "zettelstore.de/z/runes"
)
func init() {
parser.Register(&parser.Info{
- Name: meta.SyntaxTxt,
- AltNames: []string{meta.SyntaxPlain, meta.SyntaxText},
- IsASTParser: false,
- IsTextFormat: true,
- IsImageFormat: false,
- ParseBlocks: parseBlocks,
- ParseInlines: parseInlines,
- })
- parser.Register(&parser.Info{
- Name: meta.SyntaxHTML,
- AltNames: []string{},
- IsASTParser: false,
- IsTextFormat: true,
- IsImageFormat: false,
- ParseBlocks: parseBlocksHTML,
- ParseInlines: parseInlinesHTML,
- })
- parser.Register(&parser.Info{
- Name: meta.SyntaxCSS,
- AltNames: []string{},
- IsASTParser: false,
- IsTextFormat: true,
- IsImageFormat: false,
- ParseBlocks: parseBlocks,
- ParseInlines: parseInlines,
- })
- parser.Register(&parser.Info{
- Name: meta.SyntaxSVG,
- AltNames: []string{},
- IsASTParser: false,
- IsTextFormat: true,
- IsImageFormat: true,
- ParseBlocks: parseSVGBlocks,
- ParseInlines: parseSVGInlines,
- })
- parser.Register(&parser.Info{
- Name: meta.SyntaxSxn,
- AltNames: []string{},
- IsASTParser: false,
- IsTextFormat: true,
- IsImageFormat: false,
- ParseBlocks: parseSxnBlocks,
- ParseInlines: parseSxnInlines,
- })
-}
-
-func parseBlocks(inp *input.Input, _ *meta.Meta, syntax string) ast.BlockSlice {
- return doParseBlocks(inp, syntax, ast.VerbatimProg)
-}
-func parseBlocksHTML(inp *input.Input, _ *meta.Meta, syntax string) ast.BlockSlice {
- return doParseBlocks(inp, syntax, ast.VerbatimHTML)
-}
-func doParseBlocks(inp *input.Input, syntax string, kind ast.VerbatimKind) ast.BlockSlice {
- return ast.BlockSlice{
- &ast.VerbatimNode{
- Kind: kind,
- Attrs: attrs.Attributes{"": syntax},
- Content: inp.ScanLineContent(),
+ Name: "txt",
+ AltNames: []string{"plain", "text"},
+ ParseBlocks: parseBlocks,
+ ParseInlines: parseInlines,
+ })
+ parser.Register(&parser.Info{
+ Name: "css",
+ ParseBlocks: parseBlocks,
+ ParseInlines: parseInlines,
+ })
+ parser.Register(&parser.Info{
+ Name: "svg",
+ ParseBlocks: parseSVGBlocks,
+ ParseInlines: parseSVGInlines,
+ })
+ parser.Register(&parser.Info{
+ Name: "mustache",
+ ParseBlocks: parseBlocks,
+ ParseInlines: parseInlines,
+ })
+}
+
+func parseBlocks(inp *input.Input, m *meta.Meta, syntax string) ast.BlockSlice {
+ return ast.BlockSlice{
+ &ast.VerbatimNode{
+ Code: ast.VerbatimProg,
+ Attrs: &ast.Attributes{Attrs: map[string]string{"": syntax}},
+ Lines: readLines(inp),
+ },
+ }
+}
+
+func readLines(inp *input.Input) (lines []string) {
+ for {
+ inp.EatEOL()
+ posL := inp.Pos
+ if inp.Ch == input.EOS {
+ return lines
+ }
+ inp.SkipToEOL()
+ lines = append(lines, inp.Src[posL:inp.Pos])
+ }
+}
+
+func parseInlines(inp *input.Input, syntax string) ast.InlineSlice {
+ inp.SkipToEOL()
+ return ast.InlineSlice{
+ &ast.LiteralNode{
+ Code: ast.LiteralProg,
+ Attrs: &ast.Attributes{Attrs: map[string]string{"": syntax}},
+ Text: inp.Src[0:inp.Pos],
},
}
}
-func parseInlines(inp *input.Input, syntax string) ast.InlineSlice {
- return doParseInlines(inp, syntax, ast.LiteralProg)
-}
-func parseInlinesHTML(inp *input.Input, syntax string) ast.InlineSlice {
- return doParseInlines(inp, syntax, ast.LiteralHTML)
-}
-func doParseInlines(inp *input.Input, syntax string, kind ast.LiteralKind) ast.InlineSlice {
- inp.SkipToEOL()
- return ast.InlineSlice{&ast.LiteralNode{
- Kind: kind,
- Attrs: attrs.Attributes{"": syntax},
- Content: append([]byte(nil), inp.Src[0:inp.Pos]...),
- }}
-}
-
-func parseSVGBlocks(inp *input.Input, _ *meta.Meta, syntax string) ast.BlockSlice {
- is := parseSVGInlines(inp, syntax)
- if len(is) == 0 {
+func parseSVGBlocks(inp *input.Input, m *meta.Meta, syntax string) ast.BlockSlice {
+ ins := parseSVGInlines(inp, syntax)
+ if ins == nil {
return nil
}
- return ast.BlockSlice{ast.CreateParaNode(is...)}
+ return ast.BlockSlice{
+ &ast.ParaNode{
+ Inlines: ins,
+ },
+ }
}
func parseSVGInlines(inp *input.Input, syntax string) ast.InlineSlice {
svgSrc := scanSVG(inp)
if svgSrc == "" {
return nil
}
- return ast.InlineSlice{&ast.EmbedBLOBNode{
- Blob: []byte(svgSrc),
- Syntax: syntax,
- }}
+ return ast.InlineSlice{
+ &ast.ImageNode{
+ Blob: []byte(svgSrc),
+ Syntax: syntax,
+ },
+ }
}
func scanSVG(inp *input.Input) string {
- for input.IsSpace(inp.Ch) {
+ for runes.IsSpace(inp.Ch) {
inp.Next()
}
- svgSrc := string(inp.Src[inp.Pos:])
+ svgSrc := inp.Src[inp.Pos:]
if !strings.HasPrefix(svgSrc, "