Index: Makefile ================================================================== --- Makefile +++ Makefile @@ -5,18 +5,15 @@ ## ## Zettelstore is licensed 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 api build release clean +.PHONY: check build release clean check: go run tools/build.go check -api: - go run tools/build.go testapi - version: @echo $(shell go run tools/build.go version) build: go run tools/build.go build Index: VERSION ================================================================== --- VERSION +++ VERSION @@ -1,1 +1,1 @@ -0.0.14 +0.0.13 DELETED api/api.go Index: api/api.go ================================================================== --- api/api.go +++ api/api.go @@ -1,90 +0,0 @@ -//----------------------------------------------------------------------------- -// Copyright (c) 2021 Detlef Stern -// -// This file is part of zettelstore. -// -// Zettelstore is licensed under the latest version of the EUPL (European Union -// Public License). Please see file LICENSE.txt for your rights and obligations -// under this license. -//----------------------------------------------------------------------------- - -// Package api contains common definition used for client and server. -package api - -// AuthJSON contains the result of an authentication call. -type AuthJSON struct { - Token string `json:"token"` - Type string `json:"token_type"` - Expires int `json:"expires_in"` -} - -// ZidJSON contains the identifier data of a zettel. -type ZidJSON struct { - ID string `json:"id"` - URL string `json:"url"` -} - -// ZidMetaJSON contains the identifier and the metadata of a zettel. -type ZidMetaJSON struct { - ID string `json:"id"` - URL string `json:"url"` - Meta map[string]string `json:"meta"` -} - -// ZidMetaRelatedList contains identifier/metadata of a zettel and the same for related zettel -type ZidMetaRelatedList struct { - ID string `json:"id"` - URL string `json:"url"` - Meta map[string]string `json:"meta"` - List []ZidMetaJSON `json:"list"` -} - -// ZettelLinksJSON store all links / connections from one zettel to other. -type ZettelLinksJSON struct { - ID string `json:"id"` - URL string `json:"url"` - Links struct { - Incoming []ZidJSON `json:"incoming,omitempty"` - Outgoing []ZidJSON `json:"outgoing,omitempty"` - Local []string `json:"local,omitempty"` - External []string `json:"external,omitempty"` - Meta []string `json:"meta,omitempty"` - } `json:"links"` - Images struct { - Outgoing []ZidJSON `json:"outgoing,omitempty"` - Local []string `json:"local,omitempty"` - External []string `json:"external,omitempty"` - } `json:"images,omitempty"` - Cites []string `json:"cites,omitempty"` -} - -// ZettelDataJSON contains all data for a zettel. -type ZettelDataJSON struct { - Meta map[string]string `json:"meta"` - Encoding string `json:"encoding"` - Content string `json:"content"` -} - -// ZettelJSON contains all data for a zettel, the identifier, the metadata, and the content. -type ZettelJSON struct { - ID string `json:"id"` - URL string `json:"url"` - Meta map[string]string `json:"meta"` - Encoding string `json:"encoding"` - Content string `json:"content"` -} - -// ZettelListJSON contains data for a zettel list. -type ZettelListJSON struct { - List []ZettelJSON `json:"list"` -} - -// TagListJSON specifies the list/map of tags -type TagListJSON struct { - Tags map[string][]string `json:"tags"` -} - -// RoleListJSON specifies the list of roles. -type RoleListJSON struct { - Roles []string `json:"role-list"` -} DELETED api/const.go Index: api/const.go ================================================================== --- api/const.go +++ api/const.go @@ -1,108 +0,0 @@ -//----------------------------------------------------------------------------- -// Copyright (c) 2021 Detlef Stern -// -// This file is part of zettelstore. -// -// Zettelstore is licensed under the latest version of the EUPL (European Union -// Public License). Please see file LICENSE.txt for your rights and obligations -// under this license. -//----------------------------------------------------------------------------- - -// Package api contains common definition used for client and server. -package api - -import ( - "fmt" -) - -// Additional HTTP constants used. -const ( - MethodMove = "MOVE" // HTTP method for renaming a zettel - - HeaderAccept = "Accept" - HeaderContentType = "Content-Type" - HeaderDestination = "Destination" - HeaderLocation = "Location" -) - -// Values for HTTP query parameter. -const ( - QueryKeyDepth = "depth" - QueryKeyDir = "dir" - QueryKeyFormat = "_format" - QueryKeyLimit = "limit" - QueryKeyPart = "_part" -) - -// Supported dir values. -const ( - DirBackward = "backward" - DirForward = "forward" -) - -// Supported format values. -const ( - FormatDJSON = "djson" - FormatHTML = "html" - FormatJSON = "json" - FormatNative = "native" - FormatRaw = "raw" - FormatText = "text" - FormatZMK = "zmk" -) - -var formatEncoder = map[string]EncodingEnum{ - FormatDJSON: EncoderDJSON, - FormatHTML: EncoderHTML, - FormatJSON: EncoderJSON, - FormatNative: EncoderNative, - FormatRaw: EncoderRaw, - FormatText: EncoderText, - FormatZMK: EncoderZmk, -} -var encoderFormat = map[EncodingEnum]string{} - -func init() { - for k, v := range formatEncoder { - encoderFormat[v] = k - } -} - -// Encoder returns the internal encoder code for the given format string. -func Encoder(format string) EncodingEnum { - if e, ok := formatEncoder[format]; ok { - return e - } - return EncoderUnknown -} - -// EncodingEnum lists all valid encoder keys. -type EncodingEnum uint8 - -// Values for EncoderEnum -const ( - EncoderUnknown EncodingEnum = iota - EncoderDJSON - EncoderHTML - EncoderJSON - EncoderNative - EncoderRaw - EncoderText - EncoderZmk -) - -// String representation of an encoder key. -func (e EncodingEnum) String() string { - if f, ok := encoderFormat[e]; ok { - return f - } - return fmt.Sprintf("*Unknown*(%d)", e) -} - -// Supported part values. -const ( - PartID = "id" - PartMeta = "meta" - PartContent = "content" - PartZettel = "zettel" -) DELETED api/urlbuilder.go Index: api/urlbuilder.go ================================================================== --- api/urlbuilder.go +++ api/urlbuilder.go @@ -1,114 +0,0 @@ -//----------------------------------------------------------------------------- -// Copyright (c) 2020-2021 Detlef Stern -// -// This file is part of zettelstore. -// -// Zettelstore is licensed under the latest version of the EUPL (European Union -// Public License). Please see file LICENSE.txt for your rights and obligations -// under this license. -//----------------------------------------------------------------------------- - -// Package api contains common definition used for client and server. -package api - -import ( - "net/url" - "strings" - - "zettelstore.de/z/domain/id" -) - -type urlQuery struct{ key, val string } - -// URLBuilder should be used to create zettelstore URLs. -type URLBuilder struct { - prefix string - key byte - path []string - query []urlQuery - fragment string -} - -// NewURLBuilder creates a new URL builder with the given prefix and key. -func NewURLBuilder(prefix string, key byte) *URLBuilder { - return &URLBuilder{prefix: prefix, key: key} -} - -// Clone an URLBuilder -func (ub *URLBuilder) Clone() *URLBuilder { - cpy := new(URLBuilder) - cpy.key = ub.key - if len(ub.path) > 0 { - cpy.path = make([]string, 0, len(ub.path)) - cpy.path = append(cpy.path, ub.path...) - } - if len(ub.query) > 0 { - cpy.query = make([]urlQuery, 0, len(ub.query)) - cpy.query = append(cpy.query, ub.query...) - } - cpy.fragment = ub.fragment - return cpy -} - -// SetZid sets the zettel identifier. -func (ub *URLBuilder) SetZid(zid id.Zid) *URLBuilder { - if len(ub.path) > 0 { - panic("Cannot add Zid") - } - ub.path = append(ub.path, zid.String()) - return ub -} - -// AppendPath adds a new path element -func (ub *URLBuilder) AppendPath(p string) *URLBuilder { - ub.path = append(ub.path, p) - return ub -} - -// AppendQuery adds a new query parameter -func (ub *URLBuilder) AppendQuery(key, value string) *URLBuilder { - ub.query = append(ub.query, urlQuery{key, value}) - return ub -} - -// ClearQuery removes all query parameters. -func (ub *URLBuilder) ClearQuery() *URLBuilder { - ub.query = nil - ub.fragment = "" - return ub -} - -// SetFragment stores the fragment -func (ub *URLBuilder) SetFragment(s string) *URLBuilder { - ub.fragment = s - return ub -} - -// String produces a string value. -func (ub *URLBuilder) String() string { - var sb strings.Builder - - sb.WriteString(ub.prefix) - if ub.key != '/' { - sb.WriteByte(ub.key) - } - for _, p := range ub.path { - sb.WriteByte('/') - sb.WriteString(url.PathEscape(p)) - } - if len(ub.fragment) > 0 { - sb.WriteByte('#') - sb.WriteString(ub.fragment) - } - for i, q := range ub.query { - if i == 0 { - sb.WriteByte('?') - } else { - sb.WriteByte('&') - } - sb.WriteString(q.key) - sb.WriteByte('=') - sb.WriteString(url.QueryEscape(q.val)) - } - return sb.String() -} Index: ast/ast.go ================================================================== --- ast/ast.go +++ ast/ast.go @@ -30,11 +30,11 @@ Ast BlockSlice // Zettel abstract syntax tree is a sequence of block nodes. } // 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 Index: ast/attr_test.go ================================================================== --- ast/attr_test.go +++ ast/attr_test.go @@ -1,7 +1,7 @@ //----------------------------------------------------------------------------- -// Copyright (c) 2020-2021 Detlef Stern +// 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 @@ -16,11 +16,10 @@ "zettelstore.de/z/ast" ) func TestHasDefault(t *testing.T) { - t.Parallel() attr := &ast.Attributes{} if attr.HasDefault() { t.Error("Should not have default attr") } attr = &ast.Attributes{Attrs: map[string]string{"-": "value"}} @@ -28,11 +27,10 @@ t.Error("Should have default attr") } } func TestAttrClone(t *testing.T) { - t.Parallel() orig := &ast.Attributes{} clone := orig.Clone() if len(clone.Attrs) > 0 { t.Error("Attrs must be empty") } Index: ast/block.go ================================================================== --- ast/block.go +++ ast/block.go @@ -17,74 +17,69 @@ // Another name is "paragraph". type ParaNode struct { Inlines InlineSlice } -func (pn *ParaNode) blockNode() { /* Just a marker */ } -func (pn *ParaNode) itemNode() { /* Just a marker */ } -func (pn *ParaNode) descriptionNode() { /* Just a marker */ } - -// WalkChildren walks down the inline elements. -func (pn *ParaNode) WalkChildren(v Visitor) { - WalkInlineSlice(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 lines of uninterpreted text type VerbatimNode struct { - Kind VerbatimKind + Code VerbatimCode Attrs *Attributes Lines []string } -// VerbatimKind specifies the format that is applied to code inline nodes. -type VerbatimKind uint8 +// VerbatimCode specifies the format that is applied to code inline nodes. +type VerbatimCode int // Constants for VerbatimCode const ( - _ VerbatimKind = iota + _ VerbatimCode = iota VerbatimProg // Program code. VerbatimComment // Block comment VerbatimHTML // Block HTML, e.g. for Markdown ) -func (vn *VerbatimNode) blockNode() { /* Just a marker */ } -func (vn *VerbatimNode) itemNode() { /* Just a marker */ } +func (vn *VerbatimNode) blockNode() {} +func (vn *VerbatimNode) itemNode() {} -// WalkChildren does nothing. -func (vn *VerbatimNode) WalkChildren(v 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 + Code RegionCode Attrs *Attributes Blocks BlockSlice Inlines InlineSlice // Additional text at the end of the region } -// RegionKind specifies the actual region type. -type RegionKind uint8 +// 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 (rn *RegionNode) blockNode() { /* Just a marker */ } -func (rn *RegionNode) itemNode() { /* Just a marker */ } - -// WalkChildren walks down the blocks and the text. -func (rn *RegionNode) WalkChildren(v Visitor) { - WalkBlockSlice(v, rn.Blocks) - WalkInlineSlice(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 { @@ -92,60 +87,54 @@ Inlines InlineSlice // Heading text, possibly formatted Slug string // Heading text, suitable to be used as an URL fragment Attrs *Attributes } -func (hn *HeadingNode) blockNode() { /* Just a marker */ } -func (hn *HeadingNode) itemNode() { /* Just a marker */ } +func (hn *HeadingNode) blockNode() {} +func (hn *HeadingNode) itemNode() {} -// WalkChildren walks the heading text. -func (hn *HeadingNode) WalkChildren(v Visitor) { - WalkInlineSlice(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 *Attributes } -func (hn *HRuleNode) blockNode() { /* Just a marker */ } -func (hn *HRuleNode) itemNode() { /* Just a marker */ } +func (hn *HRuleNode) blockNode() {} +func (hn *HRuleNode) itemNode() {} -// WalkChildren does nothing. -func (hn *HRuleNode) WalkChildren(v 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 *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 (ln *NestedListNode) blockNode() { /* Just a marker */ } -func (ln *NestedListNode) itemNode() { /* Just a marker */ } - -// WalkChildren walks down the items. -func (ln *NestedListNode) WalkChildren(v Visitor) { - for _, item := range ln.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 { @@ -158,19 +147,12 @@ Descriptions []DescriptionSlice } func (dn *DescriptionListNode) blockNode() {} -// WalkChildren walks down to the descriptions. -func (dn *DescriptionListNode) WalkChildren(v Visitor) { - for _, desc := range dn.Descriptions { - WalkInlineSlice(v, desc.Term) - for _, dns := range desc.Descriptions { - WalkDescriptionSlice(v, dns) - } - } -} +// Accept a visitor and visit the node. +func (dn *DescriptionListNode) Accept(v Visitor) { v.VisitDescriptionList(dn) } //-------------------------------------------------------------------------- // TableNode specifies a full table type TableNode struct { @@ -199,23 +181,14 @@ AlignLeft // Left alignment AlignCenter // Center the content AlignRight // Right alignment ) -func (tn *TableNode) blockNode() { /* Just a marker */ } - -// WalkChildren walks down to the cells. -func (tn *TableNode) WalkChildren(v Visitor) { - for _, cell := range tn.Header { - WalkInlineSlice(v, cell.Inlines) - } - for _, row := range tn.Rows { - for _, cell := range row { - WalkInlineSlice(v, cell.Inlines) - } - } -} +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. @@ -223,9 +196,9 @@ Title string Syntax string Blob []byte } -func (bn *BLOBNode) blockNode() { /* Just a marker */ } +func (bn *BLOBNode) blockNode() {} -// WalkChildren does nothing. -func (bn *BLOBNode) WalkChildren(v 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 @@ -16,50 +16,50 @@ // TextNode just contains some text. type TextNode struct { Text string // The text itself. } -func (tn *TextNode) inlineNode() { /* Just a marker */ } +func (tn *TextNode) inlineNode() {} -// WalkChildren does nothing. -func (tn *TextNode) WalkChildren(v Visitor) { /* No children*/ } +// 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() { /* Just a marker */ } +func (tn *TagNode) inlineNode() {} -// WalkChildren does nothing. -func (tn *TagNode) WalkChildren(v 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 (sn *SpaceNode) inlineNode() { /* Just a marker */ } +func (sn *SpaceNode) inlineNode() {} -// WalkChildren does nothing. -func (sn *SpaceNode) WalkChildren(v Visitor) { /* No children*/ } +// 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 (bn *BreakNode) inlineNode() { /* Just a marker */ } +func (bn *BreakNode) inlineNode() {} -// WalkChildren does nothing. -func (bn *BreakNode) WalkChildren(v 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 { @@ -67,16 +67,14 @@ Inlines InlineSlice // The text associated with the link. OnlyRef bool // True if no text was specified. Attrs *Attributes // Optional attributes } -func (ln *LinkNode) inlineNode() { /* Just a marker */ } +func (ln *LinkNode) inlineNode() {} -// WalkChildren walks to the link text. -func (ln *LinkNode) WalkChildren(v Visitor) { - WalkInlineSlice(v, ln.Inlines) -} +// 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 { @@ -85,16 +83,14 @@ Syntax string // Syntax of Blob Inlines InlineSlice // The text associated with the image. Attrs *Attributes // Optional attributes } -func (in *ImageNode) inlineNode() { /* Just a marker */ } +func (in *ImageNode) inlineNode() {} -// WalkChildren walks to the image text. -func (in *ImageNode) WalkChildren(v Visitor) { - WalkInlineSlice(v, in.Inlines) -} +// Accept a visitor and visit the node. +func (in *ImageNode) Accept(v Visitor) { v.VisitImage(in) } // -------------------------------------------------------------------------- // CiteNode contains the specified citation. type CiteNode struct { @@ -101,16 +97,14 @@ Key string // The citation key Inlines InlineSlice // The text associated with the citation. Attrs *Attributes // Optional attributes } -func (cn *CiteNode) inlineNode() { /* Just a marker */ } +func (cn *CiteNode) inlineNode() {} -// WalkChildren walks to the cite text. -func (cn *CiteNode) WalkChildren(v Visitor) { - WalkInlineSlice(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 @@ -117,45 +111,43 @@ // mode, it is moved into block mode afterwards. type MarkNode struct { Text string } -func (mn *MarkNode) inlineNode() { /* Just a marker */ } +func (mn *MarkNode) inlineNode() {} -// WalkChildren does nothing. -func (mn *MarkNode) WalkChildren(v Visitor) { /* No children*/ } +// Accept a visitor and visit the node. +func (mn *MarkNode) Accept(v Visitor) { v.VisitMark(mn) } // -------------------------------------------------------------------------- // FootnoteNode contains the specified footnote. type FootnoteNode struct { Inlines InlineSlice // The footnote text. Attrs *Attributes // Optional attributes } -func (fn *FootnoteNode) inlineNode() { /* Just a marker */ } +func (fn *FootnoteNode) inlineNode() {} -// WalkChildren walks to the footnote text. -func (fn *FootnoteNode) WalkChildren(v Visitor) { - WalkInlineSlice(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 + Code FormatCode Attrs *Attributes // Optional attributes. Inlines InlineSlice } -// FormatKind specifies the format that is applied to the inline nodes. -type FormatKind uint8 +// FormatCode specifies the format that is applied to the inline nodes. +type FormatCode int // Constants for FormatCode const ( - _ FormatKind = iota + _ FormatCode = iota FormatItalic // Italic text. FormatEmph // Semantically emphasized text. FormatBold // Bold text. FormatStrong // Semantically strongly emphasized text. FormatUnder // Underlined text. @@ -169,38 +161,36 @@ FormatSmall // Smaller text. FormatSpan // Generic inline container. FormatMonospace // Monospaced text. ) -func (fn *FormatNode) inlineNode() { /* Just a marker */ } +func (fn *FormatNode) inlineNode() {} -// WalkChildren walks to the formatted text. -func (fn *FormatNode) WalkChildren(v Visitor) { - WalkInlineSlice(v, fn.Inlines) -} +// 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 + Code LiteralCode Attrs *Attributes // Optional attributes. Text string } -// LiteralKind specifies the format that is applied to code inline nodes. -type LiteralKind uint8 +// LiteralCode specifies the format that is applied to code inline nodes. +type LiteralCode int // Constants for LiteralCode const ( - _ LiteralKind = iota + _ LiteralCode = iota LiteralProg // Inline program code. LiteralKeyb // Keyboard strokes. LiteralOutput // Sample output. LiteralComment // Inline comment LiteralHTML // Inline HTML, e.g. for Markdown ) -func (ln *LiteralNode) inlineNode() { /* Just a marker */ } +func (rn *LiteralNode) inlineNode() {} -// WalkChildren does nothing. -func (ln *LiteralNode) WalkChildren(v Visitor) { /* No children*/ } +// Accept a visitor and visit the node. +func (rn *LiteralNode) Accept(v Visitor) { v.VisitLiteral(rn) } Index: ast/ref_test.go ================================================================== --- ast/ref_test.go +++ ast/ref_test.go @@ -16,11 +16,10 @@ "zettelstore.de/z/ast" ) func TestParseReference(t *testing.T) { - t.Parallel() testcases := []struct { link string err bool exp string }{ @@ -40,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,54 +0,0 @@ -//----------------------------------------------------------------------------- -// Copyright (c) 2021 Detlef Stern -// -// This file is part of zettelstore. -// -// Zettelstore is licensed under the latest version of the EUPL (European Union -// Public License). Please see file LICENSE.txt for your rights and obligations -// under this license. -//----------------------------------------------------------------------------- - -// Package ast provides the abstract syntax tree. -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 - } - node.WalkChildren(v) - v.Visit(nil) -} - -// WalkBlockSlice traverse a block slice. -func WalkBlockSlice(v Visitor, bns BlockSlice) { - for _, bn := range bns { - Walk(v, bn) - } -} - -// WalkInlineSlice traverses an inline slice. -func WalkInlineSlice(v Visitor, ins InlineSlice) { - for _, in := range ins { - Walk(v, in) - } -} - -// WalkItemSlice traverses an item slice. -func WalkItemSlice(v Visitor, ins ItemSlice) { - for _, in := range ins { - Walk(v, in) - } -} - -// WalkDescriptionSlice traverses an item slice. -func WalkDescriptionSlice(v Visitor, dns DescriptionSlice) { - for _, dn := range dns { - Walk(v, dn) - } -} Index: auth/auth.go ================================================================== --- auth/auth.go +++ auth/auth.go @@ -12,14 +12,14 @@ package auth import ( "time" - "zettelstore.de/z/box" "zettelstore.de/z/config" "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 { @@ -77,11 +77,11 @@ // Manager is the main interface for providing the service. type Manager interface { TokenManager AuthzManager - BoxWithPolicy(auth server.Auth, unprotectedBox box.Box, rtConfig config.Config) (box.Box, Policy) + 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. Index: auth/impl/impl.go ================================================================== --- auth/impl/impl.go +++ auth/impl/impl.go @@ -19,15 +19,15 @@ "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/place" "zettelstore.de/z/web/server" ) type myAuth struct { readonly bool @@ -177,8 +177,8 @@ } } return meta.UserRoleReader } -func (a *myAuth) BoxWithPolicy(auth server.Auth, unprotectedBox box.Box, rtConfig config.Config) (box.Box, auth.Policy) { - return policy.BoxWithPolicy(auth, a, unprotectedBox, rtConfig) +func (a *myAuth) PlaceWithPolicy(auth server.Auth, unprotectedPlace place.Place, rtConfig config.Config) (place.Place, auth.Policy) { + return policy.PlaceWithPolicy(auth, a, unprotectedPlace, rtConfig) } DELETED auth/policy/box.go Index: auth/policy/box.go ================================================================== --- auth/policy/box.go +++ auth/policy/box.go @@ -1,164 +0,0 @@ -//----------------------------------------------------------------------------- -// 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" - - "zettelstore.de/z/auth" - "zettelstore.de/z/box" - "zettelstore.de/z/config" - "zettelstore.de/z/domain" - "zettelstore.de/z/domain/id" - "zettelstore.de/z/domain/meta" - "zettelstore.de/z/search" - "zettelstore.de/z/web/server" -) - -// BoxWithPolicy wraps the given box inside a policy box. -func BoxWithPolicy( - auth server.Auth, - manager auth.AuthzManager, - box box.Box, - authConfig config.AuthConfig, -) (box.Box, auth.Policy) { - pol := newPolicy(manager, authConfig) - return newBox(auth, box, pol), pol -} - -// polBox implements a policy box. -type polBox struct { - auth server.Auth - box box.Box - policy auth.Policy -} - -// newBox creates a new policy box. -func newBox(auth server.Auth, box box.Box, policy auth.Policy) box.Box { - return &polBox{ - auth: auth, - 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 domain.Zettel) (id.Zid, error) { - user := pp.auth.GetUser(ctx) - if pp.policy.CanCreate(user, zettel.Meta) { - return pp.box.CreateZettel(ctx, zettel) - } - return id.Invalid, box.NewErrNotAllowed("Create", user, id.Invalid) -} - -func (pp *polBox) GetZettel(ctx context.Context, zid id.Zid) (domain.Zettel, error) { - zettel, err := pp.box.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{}, box.NewErrNotAllowed("GetZettel", user, zid) -} - -func (pp *polBox) GetAllZettel(ctx context.Context, zid id.Zid) ([]domain.Zettel, error) { - return pp.box.GetAllZettel(ctx, zid) -} - -func (pp *polBox) GetMeta(ctx context.Context, zid id.Zid) (*meta.Meta, error) { - m, err := pp.box.GetMeta(ctx, zid) - if err != nil { - return nil, err - } - user := pp.auth.GetUser(ctx) - if pp.policy.CanRead(user, m) { - return m, nil - } - return nil, box.NewErrNotAllowed("GetMeta", user, zid) -} - -func (pp *polBox) GetAllMeta(ctx context.Context, zid id.Zid) ([]*meta.Meta, error) { - return pp.box.GetAllMeta(ctx, zid) -} - -func (pp *polBox) FetchZids(ctx context.Context) (id.Set, error) { - return nil, box.NewErrNotAllowed("fetch-zids", pp.auth.GetUser(ctx), id.Invalid) -} - -func (pp *polBox) SelectMeta(ctx context.Context, s *search.Search) ([]*meta.Meta, error) { - user := pp.auth.GetUser(ctx) - canRead := pp.policy.CanRead - s = s.AddPreMatch(func(m *meta.Meta) bool { return canRead(user, m) }) - return pp.box.SelectMeta(ctx, s) -} - -func (pp *polBox) CanUpdateZettel(ctx context.Context, zettel domain.Zettel) bool { - return pp.box.CanUpdateZettel(ctx, zettel) -} - -func (pp *polBox) UpdateZettel(ctx context.Context, zettel domain.Zettel) error { - zid := zettel.Meta.Zid - user := pp.auth.GetUser(ctx) - if !zid.IsValid() { - return &box.ErrInvalidID{Zid: zid} - } - // Write existing zettel - oldMeta, err := pp.box.GetMeta(ctx, zid) - if err != nil { - return err - } - if pp.policy.CanWrite(user, oldMeta, 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 { - meta, err := pp.box.GetMeta(ctx, curZid) - if err != nil { - return err - } - user := pp.auth.GetUser(ctx) - if pp.policy.CanRename(user, 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 { - meta, err := pp.box.GetMeta(ctx, zid) - if err != nil { - return err - } - user := pp.auth.GetUser(ctx) - if pp.policy.CanDelete(user, meta) { - return pp.box.DeleteZettel(ctx, zid) - } - return box.NewErrNotAllowed("Delete", user, zid) -} Index: auth/policy/owner.go ================================================================== --- auth/policy/owner.go +++ auth/policy/owner.go @@ -62,18 +62,11 @@ } 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{ meta.KeyID, meta.KeyRole, @@ -103,12 +96,11 @@ 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) } 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_test.go ================================================================== --- auth/policy/policy_test.go +++ auth/policy/policy_test.go @@ -19,11 +19,10 @@ "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 }{ @@ -87,19 +86,25 @@ func (ac *authConfig) GetExpertMode() bool { return ac.expert } func (ac *authConfig) GetVisibility(m *meta.Meta) meta.Visibility { if vis, ok := m.Get(meta.KeyVisibility); ok { - return meta.GetVisibility(vis) + 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, isExpert bool) { t.Helper() anonUser := newAnon() - creator := newCreator() reader := newReader() writer := newWriter() owner := newOwner() owner2 := newOwner2() zettel := newZettel() @@ -109,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}, } @@ -142,18 +144,16 @@ } 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 { @@ -161,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}, @@ -237,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() @@ -264,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}, } @@ -389,11 +357,89 @@ } 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() + expertZettel := newExpertZettel() + roFalse := newRoFalseZettel() + roTrue := newRoTrueZettel() + roReader := newRoReaderZettel() + roWriter := newRoWriterZettel() + roOwner := newRoOwnerZettel() + notAuthNotReadonly := !withAuth && !readonly + testCases := []struct { + user *meta.Meta + meta *meta.Meta + exp bool + }{ + // No meta + {anonUser, nil, false}, + {reader, nil, false}, + {writer, nil, false}, + {owner, nil, false}, + {owner2, nil, false}, + // Any zettel + {anonUser, zettel, notAuthNotReadonly}, + {reader, zettel, notAuthNotReadonly}, + {writer, zettel, notAuthNotReadonly}, + {owner, zettel, !readonly}, + {owner2, zettel, !readonly}, + // Expert zettel + {anonUser, 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}, + {reader, roFalse, notAuthNotReadonly}, + {writer, roFalse, notAuthNotReadonly}, + {owner, roFalse, !readonly}, + {owner2, roFalse, !readonly}, + // Reader r/o zettel + {anonUser, roReader, false}, + {reader, roReader, false}, + {writer, roReader, notAuthNotReadonly}, + {owner, roReader, !readonly}, + {owner2, roReader, !readonly}, + // Writer r/o zettel + {anonUser, roWriter, false}, + {reader, roWriter, false}, + {writer, roWriter, false}, + {owner, roWriter, !readonly}, + {owner2, roWriter, !readonly}, + // Owner r/o zettel + {anonUser, roOwner, false}, + {reader, roOwner, false}, + {writer, roOwner, false}, + {owner, roOwner, false}, + {owner2, roOwner, false}, + // r/o = true zettel + {anonUser, roTrue, false}, + {reader, roTrue, false}, + {writer, roTrue, false}, + {owner, roTrue, false}, + {owner2, roTrue, false}, + } + for _, tc := range testCases { + t.Run("Rename", func(tt *testing.T) { + got := pol.CanRename(tc.user, tc.meta) + if tc.exp != got { + tt.Errorf("exp=%v, but got=%v", tc.exp, got) + } + }) + } +} + +func testDelete(t *testing.T, pol auth.Policy, withAuth, readonly, expert bool) { + t.Helper() + anonUser := newAnon() reader := newReader() writer := newWriter() owner := newOwner() owner2 := newOwner2() zettel := newZettel() @@ -409,148 +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}, - } - for _, tc := range testCases { - t.Run("Rename", func(tt *testing.T) { - got := pol.CanRename(tc.user, tc.meta) - if tc.exp != got { - tt.Errorf("exp=%v, but got=%v", tc.exp, got) - } - }) - } -} - -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() - expertZettel := newExpertZettel() - roFalse := newRoFalseZettel() - roTrue := newRoTrueZettel() - roReader := newRoReaderZettel() - roWriter := newRoWriterZettel() - roOwner := newRoOwnerZettel() - notAuthNotReadonly := !withAuth && !readonly - testCases := []struct { - user *meta.Meta - 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}, } @@ -563,28 +513,20 @@ }) } } 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(meta.KeyTitle, "Creator") - user.Set(meta.KeyRole, meta.ValueRoleUser) - user.Set(meta.KeyUserRole, meta.ValueUserRoleCreator) - return user -} func newReader() *meta.Meta { user := meta.New(readerZid) user.Set(meta.KeyTitle, "Reader") user.Set(meta.KeyRole, meta.ValueRoleUser) user.Set(meta.KeyUserRole, meta.ValueUserRoleReader) @@ -620,16 +562,10 @@ m := meta.New(visZid) m.Set(meta.KeyTitle, "Public Zettel") m.Set(meta.KeyVisibility, meta.ValueVisibilityPublic) return m } -func newCreatorZettel() *meta.Meta { - m := meta.New(visZid) - m.Set(meta.KeyTitle, "Creator Zettel") - m.Set(meta.KeyVisibility, meta.ValueVisibilityCreator) - return m -} func newLoginZettel() *meta.Meta { m := meta.New(visZid) m.Set(meta.KeyTitle, "Login Zettel") m.Set(meta.KeyVisibility, meta.ValueVisibilityLogin) return m DELETED box/box.go Index: box/box.go ================================================================== --- box/box.go +++ box/box.go @@ -1,274 +0,0 @@ -//----------------------------------------------------------------------------- -// Copyright (c) 2020-2021 Detlef Stern -// -// This file is part of zettelstore. -// -// Zettelstore is licensed under the latest version of the EUPL (European Union -// Public License). Please see file LICENSE.txt for your rights and obligations -// under this license. -//----------------------------------------------------------------------------- - -// Package box provides a generic interface to zettel boxes. -package box - -import ( - "context" - "errors" - "fmt" - "io" - "time" - - "zettelstore.de/z/domain" - "zettelstore.de/z/domain/id" - "zettelstore.de/z/domain/meta" - "zettelstore.de/z/search" -) - -// BaseBox is implemented by all Zettel boxes. -type BaseBox interface { - // Location returns some information where the box is located. - // Format is dependent of the box. - Location() string - - // CanCreateZettel returns true, if box could possibly create a new zettel. - CanCreateZettel(ctx context.Context) bool - - // CreateZettel creates a new zettel. - // Returns the new zettel id (and an error indication). - CreateZettel(ctx context.Context, zettel domain.Zettel) (id.Zid, error) - - // GetZettel retrieves a specific zettel. - GetZettel(ctx context.Context, zid id.Zid) (domain.Zettel, error) - - // GetMeta retrieves just the meta data of a specific zettel. - GetMeta(ctx context.Context, zid id.Zid) (*meta.Meta, error) - - // FetchZids returns the set of all zettel identifer managed by the box. - FetchZids(ctx context.Context) (id.Set, error) - - // CanUpdateZettel returns true, if box could possibly update the given zettel. - CanUpdateZettel(ctx context.Context, zettel domain.Zettel) bool - - // UpdateZettel updates an existing zettel. - UpdateZettel(ctx context.Context, zettel domain.Zettel) error - - // AllowRenameZettel returns true, if box will not disallow renaming the zettel. - AllowRenameZettel(ctx context.Context, zid id.Zid) bool - - // RenameZettel changes the current Zid to a new Zid. - 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 -} - -// ManagedBox is the interface of managed boxes. -type ManagedBox interface { - BaseBox - - // SelectMeta returns all zettel meta data that match the selection criteria. - SelectMeta(ctx context.Context, match search.MetaMatchFunc) ([]*meta.Meta, error) - - // ReadStats populates st with box statistics - ReadStats(st *ManagedBoxStats) -} - -// 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 -} - -// StartStopper performs simple lifecycle management. -type StartStopper interface { - // Start the box. Now all other functions of the box are allowed. - // Starting an already started box is not allowed. - Start(ctx context.Context) error - - // Stop the started box. Now only the Start() function is allowed. - Stop(ctx context.Context) error -} - -// Box is to be used outside the box package and its descendants. -type Box interface { - BaseBox - - // SelectMeta returns a list of metadata that comply to the given selection criteria. - SelectMeta(ctx context.Context, s *search.Search) ([]*meta.Meta, error) - - // GetAllZettel retrieves a specific zettel from all managed boxes. - GetAllZettel(ctx context.Context, zid id.Zid) ([]domain.Zettel, error) - - // GetAllMeta retrieves the meta data of a specific zettel from all managed boxes. - GetAllMeta(ctx context.Context, zid id.Zid) ([]*meta.Meta, error) -} - -// Stats record stattistics about a box. -type Stats struct { - // ReadOnly indicates that boxes cannot be modified. - 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 - OnReload // Box was reloaded - OnUpdate // A zettel was created or changed - OnDelete // A zettel was removed -) - -// UpdateInfo contains all the data about a changed zettel. -type UpdateInfo struct { - Box Box - 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 -} - -// 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.String()) - } - return fmt.Sprintf("operation %q not allowed for not authorized user", err.Op) - } - if err.Zid.IsValid() { - return fmt.Sprintf( - "operation %q on zettel %v not allowed for user %v/%v", - err.Op, - err.Zid.String(), - err.User.GetDefault(meta.KeyUserID, "?"), - err.User.Zid.String()) - } - return fmt.Sprintf( - "operation %q not allowed for user %v/%v", - err.Op, - err.User.GetDefault(meta.KeyUserID, "?"), - err.User.Zid.String()) -} - -// Is return true, if the error is of type ErrNotAllowed. -func (err *ErrNotAllowed) Is(target error) bool { return true } - -// ErrStarted is returned when trying to start an already started box. -var ErrStarted = errors.New("box is already started") - -// ErrStopped is returned if calling methods on a box that was not started. -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") - -// ErrNotFound is returned if a zettel was not found in the box. -var ErrNotFound = errors.New("zettel not found") - -// ErrConflict is returned if a box operation detected a conflict.. -// One example: if calculating a new zettel identifier takes too long. -var ErrConflict = errors.New("conflict") - -// ErrInvalidID is returned if the zettel id is not appropriate for the box operation. -type ErrInvalidID struct{ Zid id.Zid } - -func (err *ErrInvalidID) Error() string { return "invalid Zettel id: " + err.Zid.String() } DELETED box/compbox/compbox.go Index: box/compbox/compbox.go ================================================================== --- box/compbox/compbox.go +++ box/compbox/compbox.go @@ -1,167 +0,0 @@ -//----------------------------------------------------------------------------- -// 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 compbox provides zettel that have computed content. -package compbox - -import ( - "context" - "net/url" - - "zettelstore.de/z/box" - "zettelstore.de/z/box/manager" - "zettelstore.de/z/domain" - "zettelstore.de/z/domain/id" - "zettelstore.de/z/domain/meta" - "zettelstore.de/z/search" -) - -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 { - 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) string -}{ - id.VersionZid: {genVersionBuildM, genVersionBuildC}, - id.HostZid: {genVersionHostM, genVersionHostC}, - id.OperatingSystemZid: {genVersionOSM, genVersionOSC}, - id.BoxManagerZid: {genManagerM, genManagerC}, - id.MetadataKeyZid: {genKeysM, genKeysC}, - id.StartupConfigurationZid: {genConfigZettelM, genConfigZettelC}, -} - -// Get returns the one program box. -func getCompBox(boxNumber int, mf box.Enricher) box.ManagedBox { - return &compBox{number: boxNumber, enricher: mf} -} - -// Setup remembers important values. -func Setup(cfg *meta.Meta) { myConfig = cfg.Clone() } - -func (pp *compBox) Location() string { return "" } - -func (pp *compBox) CanCreateZettel(ctx context.Context) bool { return false } - -func (pp *compBox) CreateZettel( - ctx context.Context, zettel domain.Zettel) (id.Zid, error) { - return id.Invalid, box.ErrReadOnly -} - -func (pp *compBox) GetZettel(ctx context.Context, zid id.Zid) (domain.Zettel, error) { - if gen, ok := myZettel[zid]; ok && gen.meta != nil { - if m := gen.meta(zid); m != nil { - updateMeta(m) - if genContent := gen.content; genContent != nil { - return domain.Zettel{ - Meta: m, - Content: domain.NewContent(genContent(m)), - }, nil - } - return domain.Zettel{Meta: m}, nil - } - } - return domain.Zettel{}, box.ErrNotFound -} - -func (pp *compBox) GetMeta(ctx context.Context, zid id.Zid) (*meta.Meta, error) { - if gen, ok := myZettel[zid]; ok { - if genMeta := gen.meta; genMeta != nil { - if m := genMeta(zid); m != nil { - updateMeta(m) - return m, nil - } - } - } - return nil, box.ErrNotFound -} - -func (pp *compBox) FetchZids(ctx context.Context) (id.Set, error) { - result := id.NewSetCap(len(myZettel)) - for zid, gen := range myZettel { - if genMeta := gen.meta; genMeta != nil { - if genMeta(zid) != nil { - result[zid] = true - } - } - } - return result, nil -} - -func (pp *compBox) SelectMeta(ctx context.Context, match search.MetaMatchFunc) (res []*meta.Meta, err error) { - for zid, gen := range myZettel { - if genMeta := gen.meta; genMeta != nil { - if m := genMeta(zid); m != nil { - updateMeta(m) - pp.enricher.Enrich(ctx, m, pp.number) - if match(m) { - res = append(res, m) - } - } - } - } - return res, nil -} - -func (pp *compBox) CanUpdateZettel(ctx context.Context, zettel domain.Zettel) bool { - return false -} - -func (pp *compBox) UpdateZettel(ctx context.Context, zettel domain.Zettel) error { - return box.ErrReadOnly -} - -func (pp *compBox) AllowRenameZettel(ctx context.Context, zid id.Zid) bool { - _, ok := myZettel[zid] - return !ok -} - -func (pp *compBox) RenameZettel(ctx context.Context, curZid, newZid id.Zid) error { - if _, ok := myZettel[curZid]; ok { - return box.ErrReadOnly - } - return box.ErrNotFound -} - -func (pp *compBox) CanDeleteZettel(ctx context.Context, zid id.Zid) bool { return false } - -func (pp *compBox) DeleteZettel(ctx context.Context, zid id.Zid) error { - if _, ok := myZettel[zid]; ok { - return box.ErrReadOnly - } - return box.ErrNotFound -} - -func (pp *compBox) ReadStats(st *box.ManagedBoxStats) { - st.ReadOnly = true - st.Zettel = len(myZettel) -} - -func updateMeta(m *meta.Meta) { - m.Set(meta.KeyNoIndex, meta.ValueTrue) - m.Set(meta.KeySyntax, meta.ValueSyntaxZmk) - m.Set(meta.KeyRole, meta.ValueRoleConfiguration) - m.Set(meta.KeyLang, meta.ValueLangEN) - m.Set(meta.KeyReadOnly, meta.ValueTrue) - if _, ok := m.Get(meta.KeyVisibility); !ok { - m.Set(meta.KeyVisibility, meta.ValueVisibilityExpert) - } -} DELETED box/compbox/config.go Index: box/compbox/config.go ================================================================== --- box/compbox/config.go +++ box/compbox/config.go @@ -1,52 +0,0 @@ -//----------------------------------------------------------------------------- -// 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 compbox provides zettel that have computed content. -package compbox - -import ( - "strings" - - "zettelstore.de/z/domain/id" - "zettelstore.de/z/domain/meta" -) - -func genConfigZettelM(zid id.Zid) *meta.Meta { - if myConfig == nil { - return nil - } - m := meta.New(zid) - m.Set(meta.KeyTitle, "Zettelstore Startup Configuration") - m.Set(meta.KeyVisibility, meta.ValueVisibilityExpert) - return m -} - -func genConfigZettelC(m *meta.Meta) string { - var sb strings.Builder - for i, p := range myConfig.Pairs(false) { - if i > 0 { - sb.WriteByte('\n') - } - sb.WriteString("; ''") - sb.WriteString(p.Key) - sb.WriteString("''") - if p.Value != "" { - sb.WriteString("\n: ``") - for _, r := range p.Value { - if r == '`' { - sb.WriteByte('\\') - } - sb.WriteRune(r) - } - sb.WriteString("``") - } - } - return sb.String() -} DELETED box/compbox/keys.go Index: box/compbox/keys.go ================================================================== --- box/compbox/keys.go +++ box/compbox/keys.go @@ -1,38 +0,0 @@ -//----------------------------------------------------------------------------- -// 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 compbox provides zettel that have computed content. -package compbox - -import ( - "fmt" - "strings" - - "zettelstore.de/z/domain/id" - "zettelstore.de/z/domain/meta" -) - -func genKeysM(zid id.Zid) *meta.Meta { - m := meta.New(zid) - m.Set(meta.KeyTitle, "Zettelstore Supported Metadata Keys") - m.Set(meta.KeyVisibility, meta.ValueVisibilityLogin) - return m -} - -func genKeysC(*meta.Meta) string { - keys := meta.GetSortedKeyDescriptions() - var sb strings.Builder - sb.WriteString("|=Name<|=Type<|=Computed?:|=Property?:\n") - for _, kd := range keys { - fmt.Fprintf(&sb, - "|%v|%v|%v|%v\n", kd.Name, kd.Type.Name, kd.IsComputed(), kd.IsProperty()) - } - return sb.String() -} DELETED box/compbox/manager.go Index: box/compbox/manager.go ================================================================== --- box/compbox/manager.go +++ box/compbox/manager.go @@ -1,40 +0,0 @@ -//----------------------------------------------------------------------------- -// Copyright (c) 2021 Detlef Stern -// -// This file is part of zettelstore. -// -// Zettelstore is licensed under the latest version of the EUPL (European Union -// Public License). Please see file LICENSE.txt for your rights and obligations -// under this license. -//----------------------------------------------------------------------------- - -// Package compbox provides zettel that have computed content. -package compbox - -import ( - "fmt" - "strings" - - "zettelstore.de/z/domain/id" - "zettelstore.de/z/domain/meta" - "zettelstore.de/z/kernel" -) - -func genManagerM(zid id.Zid) *meta.Meta { - m := meta.New(zid) - m.Set(meta.KeyTitle, "Zettelstore Box Manager") - return m -} - -func genManagerC(*meta.Meta) string { - kvl := kernel.Main.GetServiceStatistics(kernel.BoxService) - if len(kvl) == 0 { - return "No statistics available" - } - var sb strings.Builder - sb.WriteString("|=Name|=Value>\n") - for _, kv := range kvl { - fmt.Fprintf(&sb, "| %v | %v\n", kv.Key, kv.Value) - } - return sb.String() -} DELETED box/compbox/version.go Index: box/compbox/version.go ================================================================== --- box/compbox/version.go +++ box/compbox/version.go @@ -1,54 +0,0 @@ -//----------------------------------------------------------------------------- -// 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 compbox provides zettel that have computed content. -package compbox - -import ( - "fmt" - - "zettelstore.de/z/domain/id" - "zettelstore.de/z/domain/meta" - "zettelstore.de/z/kernel" -) - -func getVersionMeta(zid id.Zid, title string) *meta.Meta { - m := meta.New(zid) - m.Set(meta.KeyTitle, title) - m.Set(meta.KeyVisibility, meta.ValueVisibilityExpert) - return m -} - -func genVersionBuildM(zid id.Zid) *meta.Meta { - m := getVersionMeta(zid, "Zettelstore Version") - m.Set(meta.KeyVisibility, meta.ValueVisibilityPublic) - return m -} -func genVersionBuildC(*meta.Meta) string { - return kernel.Main.GetConfig(kernel.CoreService, kernel.CoreVersion).(string) -} - -func genVersionHostM(zid id.Zid) *meta.Meta { - return getVersionMeta(zid, "Zettelstore Host") -} -func genVersionHostC(*meta.Meta) string { - return kernel.Main.GetConfig(kernel.CoreService, kernel.CoreHostname).(string) -} - -func genVersionOSM(zid id.Zid) *meta.Meta { - return getVersionMeta(zid, "Zettelstore Operating System") -} -func genVersionOSC(*meta.Meta) string { - return fmt.Sprintf( - "%v/%v", - kernel.Main.GetConfig(kernel.CoreService, kernel.CoreGoOS).(string), - kernel.Main.GetConfig(kernel.CoreService, kernel.CoreGoArch).(string), - ) -} DELETED box/constbox/base.css Index: box/constbox/base.css ================================================================== --- box/constbox/base.css +++ box/constbox/base.css @@ -1,279 +0,0 @@ -*,*::before,*::after { - box-sizing: border-box; - } - html { - font-size: 1rem; - font-family: serif; - scroll-behavior: smooth; - height: 100%; - } - body { - margin: 0; - min-height: 100vh; - text-rendering: optimizeSpeed; - line-height: 1.4; - overflow-x: hidden; - background-color: #f8f8f8 ; - height: 100%; - } - nav.zs-menu { - background-color: hsl(210, 28%, 90%); - 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; - } - ol,ul { - padding-left: 1.1rem; - } - li,figure,figcaption,dl { - margin: 0; - } - dt { - margin: .5rem 0 0 0; - } - dt+dd { - margin-top: 0; - } - dd { - margin: .5rem 0 0 2rem; - } - dd > p:first-child { - margin: 0 0 0 0; - } - blockquote { - border-left: 0.5rem solid lightgray; - padding-left: 1rem; - margin-left: 1rem; - margin-right: 2rem; - font-style: italic; - } - blockquote p { - margin-bottom: .5rem; - } - blockquote cite { - font-style: normal; - } - table { - border-collapse: collapse; - border-spacing: 0; - max-width: 100%; - } - th,td { - text-align: left; - padding: .25rem .5rem; - } - td { border-bottom: 1px solid hsl(0, 0%, 85%); } - thead th { border-bottom: 2px solid hsl(0, 0%, 70%); } - tfoot th { border-top: 2px solid hsl(0, 0%, 70%); } - main form { - padding: 0 .5em; - margin: .5em 0 0 0; - } - main form:after { - 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 } - label::after { content:":" } - textarea { - font-family: monospace; - resize: vertical; - width: 100%; - } - .zs-input { - padding: .5em; - display:block; - border:none; - border-bottom:1px solid #ccc; - width:100%; - } - .zs-button { - float:right; - margin: .5em 0 .5em 1em; - } - a:not([class]) { - text-decoration-skip-ink: auto; - } - .zs-broken { - text-decoration: line-through; - } - img { - max-width: 100%; - } - .zs-endnotes { - padding-top: .5rem; - border-top: 1px solid; - } - code,pre,kbd { - 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-example { border-style: dotted !important } - .zs-error { - background-color: lightpink; - border-style: none !important; - font-weight: bold; - } - kbd { - background: hsl(210, 5%, 100%); - border: 1px solid hsl(210, 5%, 70%); - border-radius: .25rem; - padding: .1rem .2rem; - font-size: 75%; - } - .zs-meta { - font-size:.75rem; - color:#444; - margin-bottom:1rem; - } - .zs-meta a { - color:#444; - } - h1+.zs-meta { - margin-top:-1rem; - } - 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.mustache Index: box/constbox/base.mustache ================================================================== --- box/constbox/base.mustache +++ box/constbox/base.mustache @@ -1,66 +0,0 @@ - - - - - - - - -{{{MetaHeader}}} - - -{{Title}} - - - -
-{{{Content}}} -
-{{#FooterHTML}} - -{{/FooterHTML}} - - DELETED box/constbox/constbox.go Index: box/constbox/constbox.go ================================================================== --- box/constbox/constbox.go +++ box/constbox/constbox.go @@ -1,405 +0,0 @@ -//----------------------------------------------------------------------------- -// 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 constbox puts zettel inside the executable. -package constbox - -import ( - "context" - _ "embed" // Allow to embed file content - "net/url" - - "zettelstore.de/z/box" - "zettelstore.de/z/box/manager" - "zettelstore.de/z/domain" - "zettelstore.de/z/domain/id" - "zettelstore.de/z/domain/meta" - "zettelstore.de/z/search" -) - -func init() { - manager.Register( - " const", - func(u *url.URL, cdata *manager.ConnectData) (box.ManagedBox, error) { - return &constBox{ - number: cdata.Number, - zettel: constZettelMap, - enricher: cdata.Enricher, - }, nil - }) -} - -type constHeader map[string]string - -func makeMeta(zid id.Zid, h constHeader) *meta.Meta { - m := meta.New(zid) - for k, v := range h { - m.Set(k, v) - } - return m -} - -type constZettel struct { - header constHeader - content domain.Content -} - -type constBox struct { - number int - zettel map[id.Zid]constZettel - enricher box.Enricher -} - -func (cp *constBox) Location() string { - return "const:" -} - -func (cp *constBox) CanCreateZettel(ctx context.Context) bool { return false } - -func (cp *constBox) CreateZettel(ctx context.Context, zettel domain.Zettel) (id.Zid, error) { - return id.Invalid, box.ErrReadOnly -} - -func (cp *constBox) GetZettel(ctx context.Context, zid id.Zid) (domain.Zettel, error) { - if z, ok := cp.zettel[zid]; ok { - return domain.Zettel{Meta: makeMeta(zid, z.header), Content: z.content}, nil - } - return domain.Zettel{}, box.ErrNotFound -} - -func (cp *constBox) GetMeta(ctx context.Context, zid id.Zid) (*meta.Meta, error) { - if z, ok := cp.zettel[zid]; ok { - return makeMeta(zid, z.header), nil - } - return nil, box.ErrNotFound -} - -func (cp *constBox) FetchZids(ctx context.Context) (id.Set, error) { - result := id.NewSetCap(len(cp.zettel)) - for zid := range cp.zettel { - result[zid] = true - } - return result, nil -} - -func (cp *constBox) SelectMeta(ctx context.Context, match search.MetaMatchFunc) (res []*meta.Meta, err error) { - for zid, zettel := range cp.zettel { - m := makeMeta(zid, zettel.header) - cp.enricher.Enrich(ctx, m, cp.number) - if match(m) { - res = append(res, m) - } - } - return res, nil -} - -func (cp *constBox) CanUpdateZettel(ctx context.Context, zettel domain.Zettel) bool { - return false -} - -func (cp *constBox) UpdateZettel(ctx context.Context, zettel domain.Zettel) error { - return box.ErrReadOnly -} - -func (cp *constBox) AllowRenameZettel(ctx context.Context, zid id.Zid) bool { - _, ok := cp.zettel[zid] - return !ok -} - -func (cp *constBox) RenameZettel(ctx context.Context, curZid, newZid id.Zid) error { - if _, ok := cp.zettel[curZid]; ok { - return box.ErrReadOnly - } - return box.ErrNotFound -} -func (cp *constBox) CanDeleteZettel(ctx context.Context, zid id.Zid) bool { return false } - -func (cp *constBox) DeleteZettel(ctx context.Context, zid id.Zid) error { - if _, ok := cp.zettel[zid]; ok { - return box.ErrReadOnly - } - return box.ErrNotFound -} - -func (cp *constBox) ReadStats(st *box.ManagedBoxStats) { - st.ReadOnly = true - st.Zettel = len(cp.zettel) -} - -const syntaxTemplate = "mustache" - -var constZettelMap = map[id.Zid]constZettel{ - id.ConfigurationZid: { - constHeader{ - meta.KeyTitle: "Zettelstore Runtime Configuration", - meta.KeyRole: meta.ValueRoleConfiguration, - meta.KeySyntax: meta.ValueSyntaxNone, - meta.KeyNoIndex: meta.ValueTrue, - meta.KeyVisibility: meta.ValueVisibilityOwner, - }, - domain.NewContent("")}, - id.LicenseZid: { - constHeader{ - meta.KeyTitle: "Zettelstore License", - meta.KeyRole: meta.ValueRoleConfiguration, - meta.KeySyntax: meta.ValueSyntaxText, - meta.KeyLang: meta.ValueLangEN, - meta.KeyReadOnly: meta.ValueTrue, - meta.KeyVisibility: meta.ValueVisibilityPublic, - }, - domain.NewContent(contentLicense)}, - id.AuthorsZid: { - constHeader{ - meta.KeyTitle: "Zettelstore Contributors", - meta.KeyRole: meta.ValueRoleConfiguration, - meta.KeySyntax: meta.ValueSyntaxZmk, - meta.KeyLang: meta.ValueLangEN, - meta.KeyReadOnly: meta.ValueTrue, - meta.KeyVisibility: meta.ValueVisibilityPublic, - }, - domain.NewContent(contentContributors)}, - id.DependenciesZid: { - constHeader{ - meta.KeyTitle: "Zettelstore Dependencies", - meta.KeyRole: meta.ValueRoleConfiguration, - meta.KeySyntax: meta.ValueSyntaxZmk, - meta.KeyLang: meta.ValueLangEN, - meta.KeyReadOnly: meta.ValueTrue, - meta.KeyVisibility: meta.ValueVisibilityPublic, - }, - domain.NewContent(contentDependencies)}, - id.BaseTemplateZid: { - constHeader{ - meta.KeyTitle: "Zettelstore Base HTML Template", - meta.KeyRole: meta.ValueRoleConfiguration, - meta.KeySyntax: syntaxTemplate, - meta.KeyNoIndex: meta.ValueTrue, - meta.KeyVisibility: meta.ValueVisibilityExpert, - }, - domain.NewContent(contentBaseMustache)}, - id.LoginTemplateZid: { - constHeader{ - meta.KeyTitle: "Zettelstore Login Form HTML Template", - meta.KeyRole: meta.ValueRoleConfiguration, - meta.KeySyntax: syntaxTemplate, - meta.KeyNoIndex: meta.ValueTrue, - meta.KeyVisibility: meta.ValueVisibilityExpert, - }, - domain.NewContent(contentLoginMustache)}, - id.ZettelTemplateZid: { - constHeader{ - meta.KeyTitle: "Zettelstore Zettel HTML Template", - meta.KeyRole: meta.ValueRoleConfiguration, - meta.KeySyntax: syntaxTemplate, - meta.KeyNoIndex: meta.ValueTrue, - meta.KeyVisibility: meta.ValueVisibilityExpert, - }, - domain.NewContent(contentZettelMustache)}, - id.InfoTemplateZid: { - constHeader{ - meta.KeyTitle: "Zettelstore Info HTML Template", - meta.KeyRole: meta.ValueRoleConfiguration, - meta.KeySyntax: syntaxTemplate, - meta.KeyNoIndex: meta.ValueTrue, - meta.KeyVisibility: meta.ValueVisibilityExpert, - }, - domain.NewContent(contentInfoMustache)}, - id.ContextTemplateZid: { - constHeader{ - meta.KeyTitle: "Zettelstore Context HTML Template", - meta.KeyRole: meta.ValueRoleConfiguration, - meta.KeySyntax: syntaxTemplate, - meta.KeyNoIndex: meta.ValueTrue, - meta.KeyVisibility: meta.ValueVisibilityExpert, - }, - domain.NewContent(contentContextMustache)}, - id.FormTemplateZid: { - constHeader{ - meta.KeyTitle: "Zettelstore Form HTML Template", - meta.KeyRole: meta.ValueRoleConfiguration, - meta.KeySyntax: syntaxTemplate, - meta.KeyNoIndex: meta.ValueTrue, - meta.KeyVisibility: meta.ValueVisibilityExpert, - }, - domain.NewContent(contentFormMustache)}, - id.RenameTemplateZid: { - constHeader{ - meta.KeyTitle: "Zettelstore Rename Form HTML Template", - meta.KeyRole: meta.ValueRoleConfiguration, - meta.KeySyntax: syntaxTemplate, - meta.KeyNoIndex: meta.ValueTrue, - meta.KeyVisibility: meta.ValueVisibilityExpert, - }, - domain.NewContent(contentRenameMustache)}, - id.DeleteTemplateZid: { - constHeader{ - meta.KeyTitle: "Zettelstore Delete HTML Template", - meta.KeyRole: meta.ValueRoleConfiguration, - meta.KeySyntax: syntaxTemplate, - meta.KeyNoIndex: meta.ValueTrue, - meta.KeyVisibility: meta.ValueVisibilityExpert, - }, - domain.NewContent(contentDeleteMustache)}, - id.ListTemplateZid: { - constHeader{ - meta.KeyTitle: "Zettelstore List Zettel HTML Template", - meta.KeyRole: meta.ValueRoleConfiguration, - meta.KeySyntax: syntaxTemplate, - meta.KeyNoIndex: meta.ValueTrue, - meta.KeyVisibility: meta.ValueVisibilityExpert, - }, - domain.NewContent(contentListZettelMustache)}, - id.RolesTemplateZid: { - constHeader{ - meta.KeyTitle: "Zettelstore List Roles HTML Template", - meta.KeyRole: meta.ValueRoleConfiguration, - meta.KeySyntax: syntaxTemplate, - meta.KeyNoIndex: meta.ValueTrue, - meta.KeyVisibility: meta.ValueVisibilityExpert, - }, - domain.NewContent(contentListRolesMustache)}, - id.TagsTemplateZid: { - constHeader{ - meta.KeyTitle: "Zettelstore List Tags HTML Template", - meta.KeyRole: meta.ValueRoleConfiguration, - meta.KeySyntax: syntaxTemplate, - meta.KeyNoIndex: meta.ValueTrue, - meta.KeyVisibility: meta.ValueVisibilityExpert, - }, - domain.NewContent(contentListTagsMustache)}, - id.ErrorTemplateZid: { - constHeader{ - meta.KeyTitle: "Zettelstore Error HTML Template", - meta.KeyRole: meta.ValueRoleConfiguration, - meta.KeySyntax: syntaxTemplate, - meta.KeyNoIndex: meta.ValueTrue, - meta.KeyVisibility: meta.ValueVisibilityExpert, - }, - domain.NewContent(contentErrorMustache)}, - id.BaseCSSZid: { - constHeader{ - meta.KeyTitle: "Zettelstore Base CSS", - meta.KeyRole: meta.ValueRoleConfiguration, - meta.KeySyntax: "css", - meta.KeyNoIndex: meta.ValueTrue, - meta.KeyVisibility: meta.ValueVisibilityPublic, - }, - domain.NewContent(contentBaseCSS)}, - id.UserCSSZid: { - constHeader{ - meta.KeyTitle: "Zettelstore User CSS", - meta.KeyRole: meta.ValueRoleConfiguration, - meta.KeySyntax: "css", - meta.KeyVisibility: meta.ValueVisibilityPublic, - }, - domain.NewContent("/* User-defined CSS */")}, - id.EmojiZid: { - constHeader{ - meta.KeyTitle: "Generic Emoji", - meta.KeyRole: meta.ValueRoleConfiguration, - meta.KeySyntax: meta.ValueSyntaxGif, - meta.KeyReadOnly: meta.ValueTrue, - meta.KeyVisibility: meta.ValueVisibilityPublic, - }, - domain.NewContent(contentEmoji)}, - id.TOCNewTemplateZid: { - constHeader{ - meta.KeyTitle: "New Menu", - meta.KeyRole: meta.ValueRoleConfiguration, - meta.KeySyntax: meta.ValueSyntaxZmk, - meta.KeyLang: meta.ValueLangEN, - meta.KeyVisibility: meta.ValueVisibilityCreator, - }, - domain.NewContent(contentNewTOCZettel)}, - id.TemplateNewZettelZid: { - constHeader{ - meta.KeyTitle: "New Zettel", - meta.KeyRole: meta.ValueRoleZettel, - meta.KeySyntax: meta.ValueSyntaxZmk, - meta.KeyVisibility: meta.ValueVisibilityCreator, - }, - domain.NewContent("")}, - id.TemplateNewUserZid: { - constHeader{ - meta.KeyTitle: "New User", - meta.KeyRole: meta.ValueRoleUser, - meta.KeySyntax: meta.ValueSyntaxNone, - meta.NewPrefix + meta.KeyCredential: "", - meta.NewPrefix + meta.KeyUserID: "", - meta.NewPrefix + meta.KeyUserRole: meta.ValueUserRoleReader, - meta.KeyVisibility: meta.ValueVisibilityOwner, - }, - domain.NewContent("")}, - id.DefaultHomeZid: { - constHeader{ - meta.KeyTitle: "Home", - meta.KeyRole: meta.ValueRoleZettel, - meta.KeySyntax: meta.ValueSyntaxZmk, - meta.KeyLang: meta.ValueLangEN, - }, - domain.NewContent(contentHomeZettel)}, -} - -//go:embed license.txt -var contentLicense string - -//go:embed contributors.zettel -var contentContributors string - -//go:embed dependencies.zettel -var contentDependencies string - -//go:embed base.mustache -var contentBaseMustache string - -//go:embed login.mustache -var contentLoginMustache string - -//go:embed zettel.mustache -var contentZettelMustache string - -//go:embed info.mustache -var contentInfoMustache string - -//go:embed context.mustache -var contentContextMustache string - -//go:embed form.mustache -var contentFormMustache string - -//go:embed rename.mustache -var contentRenameMustache string - -//go:embed delete.mustache -var contentDeleteMustache string - -//go:embed listzettel.mustache -var contentListZettelMustache string - -//go:embed listroles.mustache -var contentListRolesMustache string - -//go:embed listtags.mustache -var contentListTagsMustache string - -//go:embed error.mustache -var contentErrorMustache string - -//go:embed base.css -var contentBaseCSS string - -//go:embed emoji_spin.gif -var contentEmoji string - -//go:embed newtoc.zettel -var contentNewTOCZettel string - -//go:embed home.zettel -var contentHomeZettel string DELETED box/constbox/context.mustache Index: box/constbox/context.mustache ================================================================== --- box/constbox/context.mustache +++ box/constbox/context.mustache @@ -1,16 +0,0 @@ - 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.mustache Index: box/constbox/delete.mustache ================================================================== --- box/constbox/delete.mustache +++ box/constbox/delete.mustache @@ -1,15 +0,0 @@ -
-
-

Delete Zettel {{Zid}}

-
-

Do you really want to delete this zettel?

-
-{{#MetaPairs}} -
{{Key}}:
{{Value}}
-{{/MetaPairs}} -
-
- -
-
-{{end}} DELETED box/constbox/dependencies.zettel Index: box/constbox/dependencies.zettel ================================================================== --- box/constbox/dependencies.zettel +++ box/constbox/dependencies.zettel @@ -1,149 +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 license. - -=== 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. -``` - -=== Fsnotify -; URL -: [[https://fsnotify.org/]] -; License -: BSD 3-Clause "New" or "Revised" License -; Source -: [[https://github.com/fsnotify/fsnotify]] -``` -Copyright (c) 2012 The Go Authors. All rights reserved. -Copyright (c) 2012-2019 fsnotify Authors. All rights reserved. - -Redistribution and use in source and binary forms, with or without -modification, are permitted provided that the following conditions are -met: - - * Redistributions of source code must retain the above copyright -notice, this list of conditions and the following disclaimer. - * Redistributions in binary form must reproduce the above -copyright notice, this list of conditions and the following disclaimer -in the documentation and/or other materials provided with the -distribution. - * Neither the name of Google Inc. nor the names of its -contributors may be used to endorse or promote products derived from -this software without specific prior written permission. - -THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS -"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT -LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR -A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT -OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, -SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT -LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, -DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY -THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT -(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE -OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. -``` - -=== hoisie/mustache / cbroglie/mustache -; URL & Source -: [[https://github.com/hoisie/mustache]] / [[https://github.com/cbroglie/mustache]] -; License -: MIT License -; Remarks -: cbroglie/mustache is a fork from hoisie/mustache (starting with commit [f9b4cbf]). - cbroglie/mustache does not claim a copyright and includes just the license file from hoisie/mustache. - cbroglie/mustache obviously continues with the original license. - -``` -Copyright (c) 2009 Michael Hoisie - -Permission is hereby granted, free of charge, to any person obtaining a copy -of this software and associated documentation files (the "Software"), to deal -in the Software without restriction, including without limitation the rights -to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -copies of the Software, and to permit persons to whom the Software is -furnished to do so, subject to the following conditions: - -The above copyright notice and this permission notice shall be included in -all copies or substantial portions of the Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN -THE SOFTWARE. -``` - -=== pascaldekloe/jwt -; URL & Source -: [[https://github.com/pascaldekloe/jwt]] -; License -: [[CC0 1.0 Universal|https://creativecommons.org/publicdomain/zero/1.0/legalcode]] -``` -To the extent possible under law, Pascal S. de Kloe has waived all -copyright and related or neighboring rights to JWT. This work is -published from The Netherlands. - -https://creativecommons.org/publicdomain/zero/1.0/legalcode -``` - -=== yuin/goldmark -; URL & Source -: [[https://github.com/yuin/goldmark]] -; 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. -``` 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.mustache Index: box/constbox/error.mustache ================================================================== --- box/constbox/error.mustache +++ box/constbox/error.mustache @@ -1,6 +0,0 @@ -
-
-

{{ErrorTitle}}

-
-{{ErrorText}} -
DELETED box/constbox/form.mustache Index: box/constbox/form.mustache ================================================================== --- box/constbox/form.mustache +++ box/constbox/form.mustache @@ -1,38 +0,0 @@ -
-
-

{{Heading}}

-
-
-
- - -
-
-
- - -
- - -
-
- - -
-
- - -
-
-{{#IsTextContent}} - - -{{/IsTextContent}} -
- -
-
DELETED box/constbox/home.zettel Index: box/constbox/home.zettel ================================================================== --- box/constbox/home.zettel +++ box/constbox/home.zettel @@ -1,44 +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. -To check for versions, there is a zettel with the [[current version|00000000000001]] of your Zettelstore. - -If you have problems concerning Zettelstore, -do not hesitate to get in [[contact with the main developer|mailto:ds@zettelstore.de]]. - -=== Reporting errors -If you have encountered an error, please include the content of the following zettel in your mail (if possible): -* [[Zettelstore Version|00000000000001]] -* [[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.mustache Index: box/constbox/info.mustache ================================================================== --- box/constbox/info.mustache +++ box/constbox/info.mustache @@ -1,48 +0,0 @@ -
-
-

Information for Zettel {{Zid}}

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

Interpreted Metadata

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

References

-{{#HasLocLinks}} -

Local

- -{{/HasLocLinks}} -{{#HasExtLinks}} -

External

- -{{/HasExtLinks}} -{{/HasLinks}} -

Parts and format

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

Shadowed Boxes

- -{{/HasShadowLinks}} -{{#Endnotes}}{{{Endnotes}}}{{/Endnotes}} -
DELETED box/constbox/license.txt Index: box/constbox/license.txt ================================================================== --- box/constbox/license.txt +++ box/constbox/license.txt @@ -1,295 +0,0 @@ -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 -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/listroles.mustache Index: box/constbox/listroles.mustache ================================================================== --- box/constbox/listroles.mustache +++ box/constbox/listroles.mustache @@ -1,8 +0,0 @@ - DELETED box/constbox/listtags.mustache Index: box/constbox/listtags.mustache ================================================================== --- box/constbox/listtags.mustache +++ box/constbox/listtags.mustache @@ -1,10 +0,0 @@ - DELETED box/constbox/listzettel.mustache Index: box/constbox/listzettel.mustache ================================================================== --- box/constbox/listzettel.mustache +++ box/constbox/listzettel.mustache @@ -1,6 +0,0 @@ -
-

{{Title}}

-
- DELETED box/constbox/login.mustache Index: box/constbox/login.mustache ================================================================== --- box/constbox/login.mustache +++ box/constbox/login.mustache @@ -1,19 +0,0 @@ -
-
-

{{Title}}

-
-{{#Retry}} -
Wrong user name / password. Try again.
-{{/Retry}} -
-
- - -
-
- - -
- -
-
DELETED box/constbox/newtoc.zettel Index: box/constbox/newtoc.zettel ================================================================== --- box/constbox/newtoc.zettel +++ box/constbox/newtoc.zettel @@ -1,4 +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 User|00000000090002]] DELETED box/constbox/rename.mustache Index: box/constbox/rename.mustache ================================================================== --- box/constbox/rename.mustache +++ box/constbox/rename.mustache @@ -1,19 +0,0 @@ -
-
-

Rename Zettel {{.Zid}}

-
-

Do you really want to rename this zettel?

-
-
- - -
- - -
-
-{{#MetaPairs}} -
{{Key}}:
{{Value}}
-{{/MetaPairs}} -
-
DELETED box/constbox/zettel.mustache Index: box/constbox/zettel.mustache ================================================================== --- box/constbox/zettel.mustache +++ box/constbox/zettel.mustache @@ -1,28 +0,0 @@ -
-
-

{{{HTMLTitle}}}

-
-{{#CanWrite}}Edit ·{{/CanWrite}} -{{Zid}} · -Info · -({{RoleText}}) -{{#HasTags}}· {{#Tags}} {{Text}}{{/Tags}}{{/HasTags}} -{{#CanCopy}}· Copy{{/CanCopy}} -{{#CanFolge}}· Folge{{/CanFolge}} -{{#FolgeRefs}}
Folge: {{{FolgeRefs}}}{{/FolgeRefs}} -{{#PrecursorRefs}}
Precursor: {{{PrecursorRefs}}}{{/PrecursorRefs}} -{{#HasExtURL}}
URL: {{ExtURL}}{{/HasExtURL}} -
-
-{{{Content}}} -{{#HasBackLinks}} -
-Additional links to this zettel -
    -{{#BackLinks}} -
  • {{Text}}
  • -{{/BackLinks}} -
-
-{{/HasBackLinks}} -
DELETED box/dirbox/dirbox.go Index: box/dirbox/dirbox.go ================================================================== --- box/dirbox/dirbox.go +++ box/dirbox/dirbox.go @@ -1,420 +0,0 @@ -//----------------------------------------------------------------------------- -// 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 dirbox provides a directory-based zettel box. -package dirbox - -import ( - "context" - "errors" - "net/url" - "os" - "path/filepath" - "strconv" - "strings" - "sync" - "time" - - "zettelstore.de/z/box" - "zettelstore.de/z/box/dirbox/directory" - "zettelstore.de/z/box/filebox" - "zettelstore.de/z/box/manager" - "zettelstore.de/z/domain" - "zettelstore.de/z/domain/id" - "zettelstore.de/z/domain/meta" - "zettelstore.de/z/search" -) - -func init() { - manager.Register("dir", func(u *url.URL, cdata *manager.ConnectData) (box.ManagedBox, error) { - path := getDirPath(u) - if _, err := os.Stat(path); errors.Is(err, os.ErrNotExist) { - return nil, err - } - dirSrvSpec, defWorker, maxWorker := getDirSrvInfo(u.Query().Get("type")) - dp := dirBox{ - number: cdata.Number, - location: u.String(), - readonly: getQueryBool(u, "readonly"), - cdata: *cdata, - dir: path, - dirRescan: time.Duration(getQueryInt(u, "rescan", 60, 3600, 30*24*60*60)) * time.Second, - dirSrvSpec: dirSrvSpec, - fSrvs: uint32(getQueryInt(u, "worker", 1, defWorker, maxWorker)), - } - return &dp, nil - }) -} - -type directoryServiceSpec int - -const ( - _ directoryServiceSpec = iota - dirSrvAny - dirSrvSimple - dirSrvNotify -) - -func getDirPath(u *url.URL) string { - if u.Opaque != "" { - return filepath.Clean(u.Opaque) - } - return filepath.Clean(u.Path) -} - -func getQueryBool(u *url.URL, key string) bool { - _, ok := u.Query()[key] - return ok -} - -func getQueryInt(u *url.URL, key string, min, def, max int) int { - sVal := u.Query().Get(key) - if sVal == "" { - return def - } - iVal, err := strconv.Atoi(sVal) - if err != nil { - return def - } - if iVal < min { - return min - } - if iVal > max { - return max - } - return iVal -} - -// dirBox uses a directory to store zettel as files. -type dirBox struct { - number int - location string - readonly bool - cdata manager.ConnectData - dir string - dirRescan time.Duration - dirSrvSpec directoryServiceSpec - dirSrv directory.Service - mustNotify bool - fSrvs uint32 - fCmds []chan fileCmd - mxCmds sync.RWMutex -} - -func (dp *dirBox) Location() string { - return dp.location -} - -func (dp *dirBox) Start(ctx context.Context) error { - dp.mxCmds.Lock() - dp.fCmds = make([]chan fileCmd, 0, dp.fSrvs) - for i := uint32(0); i < dp.fSrvs; i++ { - cc := make(chan fileCmd) - go fileService(i, cc) - dp.fCmds = append(dp.fCmds, cc) - } - dp.setupDirService() - dp.mxCmds.Unlock() - if dp.dirSrv == nil { - panic("No directory service") - } - return dp.dirSrv.Start() -} - -func (dp *dirBox) Stop(ctx context.Context) error { - dirSrv := dp.dirSrv - dp.dirSrv = nil - err := dirSrv.Stop() - for _, c := range dp.fCmds { - close(c) - } - return err -} - -func (dp *dirBox) notifyChanged(reason box.UpdateReason, zid id.Zid) { - if dp.mustNotify { - if chci := dp.cdata.Notify; chci != nil { - chci <- box.UpdateInfo{Reason: reason, Zid: zid} - } - } -} - -func (dp *dirBox) getFileChan(zid id.Zid) chan fileCmd { - // Based on https://en.wikipedia.org/wiki/Fowler%E2%80%93Noll%E2%80%93Vo_hash_function - 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(ctx context.Context) bool { - return !dp.readonly -} - -func (dp *dirBox) CreateZettel(ctx context.Context, zettel domain.Zettel) (id.Zid, error) { - if dp.readonly { - return id.Invalid, box.ErrReadOnly - } - - entry, err := dp.dirSrv.GetNew() - if err != nil { - return id.Invalid, err - } - meta := zettel.Meta - meta.Zid = entry.Zid - dp.updateEntryFromMeta(entry, meta) - - err = setZettel(dp, entry, zettel) - if err == nil { - dp.dirSrv.UpdateEntry(entry) - } - dp.notifyChanged(box.OnUpdate, meta.Zid) - return meta.Zid, err -} - -func (dp *dirBox) GetZettel(ctx context.Context, zid id.Zid) (domain.Zettel, error) { - entry, err := dp.dirSrv.GetEntry(zid) - if err != nil || !entry.IsValid() { - return domain.Zettel{}, box.ErrNotFound - } - m, c, err := getMetaContent(dp, entry, zid) - if err != nil { - return domain.Zettel{}, err - } - dp.cleanupMeta(ctx, m) - zettel := domain.Zettel{Meta: m, Content: domain.NewContent(c)} - return zettel, nil -} - -func (dp *dirBox) GetMeta(ctx context.Context, zid id.Zid) (*meta.Meta, error) { - entry, err := dp.dirSrv.GetEntry(zid) - if err != nil || !entry.IsValid() { - return nil, box.ErrNotFound - } - m, err := getMeta(dp, entry, zid) - if err != nil { - return nil, err - } - dp.cleanupMeta(ctx, m) - return m, nil -} - -func (dp *dirBox) FetchZids(ctx context.Context) (id.Set, error) { - entries, err := dp.dirSrv.GetEntries() - if err != nil { - return nil, err - } - result := id.NewSetCap(len(entries)) - for _, entry := range entries { - result[entry.Zid] = true - } - return result, nil -} - -func (dp *dirBox) SelectMeta(ctx context.Context, match search.MetaMatchFunc) (res []*meta.Meta, err error) { - entries, err := dp.dirSrv.GetEntries() - if err != nil { - return nil, err - } - res = make([]*meta.Meta, 0, len(entries)) - // The following loop could be parallelized if needed for performance. - for _, entry := range entries { - m, err1 := getMeta(dp, entry, entry.Zid) - err = err1 - if err != nil { - continue - } - dp.cleanupMeta(ctx, m) - dp.cdata.Enricher.Enrich(ctx, m, dp.number) - - if match(m) { - res = append(res, m) - } - } - if err != nil { - return nil, err - } - return res, nil -} - -func (dp *dirBox) CanUpdateZettel(ctx context.Context, zettel domain.Zettel) bool { - return !dp.readonly -} - -func (dp *dirBox) UpdateZettel(ctx context.Context, zettel domain.Zettel) error { - if dp.readonly { - return box.ErrReadOnly - } - - meta := zettel.Meta - if !meta.Zid.IsValid() { - return &box.ErrInvalidID{Zid: meta.Zid} - } - entry, err := dp.dirSrv.GetEntry(meta.Zid) - if err != nil { - return err - } - if !entry.IsValid() { - // Existing zettel, but new in this box. - entry = &directory.Entry{Zid: meta.Zid} - dp.updateEntryFromMeta(entry, meta) - } else if entry.MetaSpec == directory.MetaSpecNone { - defaultMeta := filebox.CalcDefaultMeta(entry.Zid, entry.ContentExt) - if !meta.Equal(defaultMeta, true) { - dp.updateEntryFromMeta(entry, meta) - dp.dirSrv.UpdateEntry(entry) - } - } - err = setZettel(dp, entry, zettel) - if err == nil { - dp.notifyChanged(box.OnUpdate, meta.Zid) - } - return err -} - -func (dp *dirBox) updateEntryFromMeta(entry *directory.Entry, meta *meta.Meta) { - entry.MetaSpec, entry.ContentExt = dp.calcSpecExt(meta) - basePath := dp.calcBasePath(entry) - if entry.MetaSpec == directory.MetaSpecFile { - entry.MetaPath = basePath + ".meta" - } - entry.ContentPath = basePath + "." + entry.ContentExt - entry.Duplicates = false -} - -func (dp *dirBox) calcBasePath(entry *directory.Entry) string { - p := entry.ContentPath - if p == "" { - return filepath.Join(dp.dir, entry.Zid.String()) - } - // ContentPath w/o the file extension - return p[0 : len(p)-len(filepath.Ext(p))] -} - -func (dp *dirBox) calcSpecExt(m *meta.Meta) (directory.MetaSpec, string) { - if m.YamlSep { - return directory.MetaSpecHeader, "zettel" - } - syntax := m.GetDefault(meta.KeySyntax, "bin") - switch syntax { - case meta.ValueSyntaxNone, meta.ValueSyntaxZmk: - return directory.MetaSpecHeader, "zettel" - } - for _, s := range dp.cdata.Config.GetZettelFileSyntax() { - if s == syntax { - return directory.MetaSpecHeader, "zettel" - } - } - return directory.MetaSpecFile, syntax -} - -func (dp *dirBox) AllowRenameZettel(ctx context.Context, zid id.Zid) bool { - return !dp.readonly -} - -func (dp *dirBox) RenameZettel(ctx context.Context, curZid, newZid id.Zid) error { - if curZid == newZid { - return nil - } - curEntry, err := dp.dirSrv.GetEntry(curZid) - if err != nil || !curEntry.IsValid() { - return box.ErrNotFound - } - if dp.readonly { - return box.ErrReadOnly - } - - // Check whether zettel with new ID already exists in this box. - if _, err = dp.GetMeta(ctx, newZid); err == nil { - return &box.ErrInvalidID{Zid: newZid} - } - - oldMeta, oldContent, err := getMetaContent(dp, curEntry, curZid) - if err != nil { - return err - } - - newEntry := directory.Entry{ - Zid: newZid, - MetaSpec: curEntry.MetaSpec, - MetaPath: renamePath(curEntry.MetaPath, curZid, newZid), - ContentPath: renamePath(curEntry.ContentPath, curZid, newZid), - ContentExt: curEntry.ContentExt, - } - - if err = dp.dirSrv.RenameEntry(curEntry, &newEntry); err != nil { - return err - } - oldMeta.Zid = newZid - newZettel := domain.Zettel{Meta: oldMeta, Content: domain.NewContent(oldContent)} - if err = setZettel(dp, &newEntry, newZettel); err != nil { - // "Rollback" rename. No error checking... - dp.dirSrv.RenameEntry(&newEntry, curEntry) - return err - } - err = deleteZettel(dp, curEntry, curZid) - if err == nil { - dp.notifyChanged(box.OnDelete, curZid) - dp.notifyChanged(box.OnUpdate, newZid) - } - return err -} - -func (dp *dirBox) CanDeleteZettel(ctx context.Context, zid id.Zid) bool { - if dp.readonly { - return false - } - entry, err := dp.dirSrv.GetEntry(zid) - return err == nil && entry.IsValid() -} - -func (dp *dirBox) DeleteZettel(ctx context.Context, zid id.Zid) error { - if dp.readonly { - return box.ErrReadOnly - } - - entry, err := dp.dirSrv.GetEntry(zid) - if err != nil || !entry.IsValid() { - return box.ErrNotFound - } - dp.dirSrv.DeleteEntry(zid) - err = deleteZettel(dp, entry, zid) - if err == nil { - dp.notifyChanged(box.OnDelete, zid) - } - return err -} - -func (dp *dirBox) ReadStats(st *box.ManagedBoxStats) { - st.ReadOnly = dp.readonly - st.Zettel, _ = dp.dirSrv.NumEntries() -} - -func (dp *dirBox) cleanupMeta(ctx context.Context, m *meta.Meta) { - if role, ok := m.Get(meta.KeyRole); !ok || role == "" { - m.Set(meta.KeyRole, dp.cdata.Config.GetDefaultRole()) - } - if syntax, ok := m.Get(meta.KeySyntax); !ok || syntax == "" { - m.Set(meta.KeySyntax, dp.cdata.Config.GetDefaultSyntax()) - } -} - -func renamePath(path string, curID, newID id.Zid) string { - dir, file := filepath.Split(path) - if cur := curID.String(); strings.HasPrefix(file, cur) { - file = newID.String() + file[len(cur):] - return filepath.Join(dir, file) - } - return path -} DELETED box/dirbox/directory/directory.go Index: box/dirbox/directory/directory.go ================================================================== --- box/dirbox/directory/directory.go +++ box/dirbox/directory/directory.go @@ -1,53 +0,0 @@ -//----------------------------------------------------------------------------- -// Copyright (c) 2020-2021 Detlef Stern -// -// This file is part of zettelstore. -// -// Zettelstore is licensed under the latest version of the EUPL (European Union -// Public License). Please see file LICENSE.txt for your rights and obligations -// under this license. -//----------------------------------------------------------------------------- - -// Package directory manages the directory interface of a dirstore. -package directory - -import "zettelstore.de/z/domain/id" - -// Service is the interface of a directory service. -type Service interface { - Start() error - Stop() error - NumEntries() (int, error) - GetEntries() ([]*Entry, error) - GetEntry(zid id.Zid) (*Entry, error) - GetNew() (*Entry, error) - UpdateEntry(entry *Entry) error - RenameEntry(curEntry, newEntry *Entry) error - DeleteEntry(zid id.Zid) error -} - -// MetaSpec defines all possibilities where meta data can be stored. -type MetaSpec int - -// Constants for MetaSpec -const ( - _ MetaSpec = iota - MetaSpecNone // no meta information - MetaSpecFile // meta information is in meta file - MetaSpecHeader // meta information is in header -) - -// Entry stores everything for a directory entry. -type Entry struct { - Zid id.Zid - MetaSpec MetaSpec // location of meta information - MetaPath string // file path of meta information - ContentPath string // file path of zettel content - ContentExt string // (normalized) file extension of zettel content - Duplicates bool // multiple content files -} - -// IsValid checks whether the entry is valid. -func (e *Entry) IsValid() bool { - return e != nil && e.Zid.IsValid() -} DELETED box/dirbox/makedir.go Index: box/dirbox/makedir.go ================================================================== --- box/dirbox/makedir.go +++ box/dirbox/makedir.go @@ -1,43 +0,0 @@ -//----------------------------------------------------------------------------- -// Copyright (c) 2021 Detlef Stern -// -// This file is part of zettelstore. -// -// Zettelstore is licensed under the latest version of the EUPL (European Union -// Public License). Please see file LICENSE.txt for your rights and obligations -// under this license. -//----------------------------------------------------------------------------- - -// Package dirbox provides a directory-based zettel box. -package dirbox - -import ( - "zettelstore.de/z/box/dirbox/notifydir" - "zettelstore.de/z/box/dirbox/simpledir" - "zettelstore.de/z/kernel" -) - -func getDirSrvInfo(dirType string) (directoryServiceSpec, int, int) { - for count := 0; count < 2; count++ { - switch dirType { - case kernel.BoxDirTypeNotify: - return dirSrvNotify, 7, 1499 - case kernel.BoxDirTypeSimple: - return dirSrvSimple, 1, 1 - default: - dirType = kernel.Main.GetConfig(kernel.BoxService, kernel.BoxDefaultDirType).(string) - } - } - panic("unable to set default dir box type: " + dirType) -} - -func (dp *dirBox) setupDirService() { - switch dp.dirSrvSpec { - case dirSrvSimple: - dp.dirSrv = simpledir.NewService(dp.dir) - dp.mustNotify = true - default: - dp.dirSrv = notifydir.NewService(dp.dir, dp.dirRescan, dp.cdata.Notify) - dp.mustNotify = false - } -} DELETED box/dirbox/notifydir/notifydir.go Index: box/dirbox/notifydir/notifydir.go ================================================================== --- box/dirbox/notifydir/notifydir.go +++ box/dirbox/notifydir/notifydir.go @@ -1,126 +0,0 @@ -//----------------------------------------------------------------------------- -// Copyright (c) 2020-2021 Detlef Stern -// -// This file is part of zettelstore. -// -// Zettelstore is licensed under the latest version of the EUPL (European Union -// Public License). Please see file LICENSE.txt for your rights and obligations -// under this license. -//----------------------------------------------------------------------------- - -// Package notifydir manages the notified directory part of a dirstore. -package notifydir - -import ( - "time" - - "zettelstore.de/z/box" - "zettelstore.de/z/box/dirbox/directory" - "zettelstore.de/z/domain/id" -) - -// notifyService specifies a directory scan service. -type notifyService struct { - dirPath string - rescanTime time.Duration - done chan struct{} - cmds chan dirCmd - infos chan<- box.UpdateInfo -} - -// NewService creates a new directory service. -func NewService(directoryPath string, rescanTime time.Duration, chci chan<- box.UpdateInfo) directory.Service { - srv := ¬ifyService{ - dirPath: directoryPath, - rescanTime: rescanTime, - cmds: make(chan dirCmd), - infos: chci, - } - return srv -} - -// Start makes the directory service operational. -func (srv *notifyService) Start() error { - tick := make(chan struct{}) - rawEvents := make(chan *fileEvent) - events := make(chan *fileEvent) - - ready := make(chan int) - go srv.directoryService(events, ready) - go collectEvents(events, rawEvents) - go watchDirectory(srv.dirPath, rawEvents, tick) - - if srv.done != nil { - panic("src.done already set") - } - srv.done = make(chan struct{}) - go ping(tick, srv.rescanTime, srv.done) - <-ready - return nil -} - -// Stop stops the directory service. -func (srv *notifyService) Stop() error { - close(srv.done) - srv.done = nil - return nil -} - -func (srv *notifyService) notifyChange(reason box.UpdateReason, zid id.Zid) { - if chci := srv.infos; chci != nil { - chci <- box.UpdateInfo{Reason: reason, Zid: zid} - } -} - -// NumEntries returns the number of managed zettel. -func (srv *notifyService) NumEntries() (int, error) { - resChan := make(chan resNumEntries) - srv.cmds <- &cmdNumEntries{resChan} - return <-resChan, nil -} - -// GetEntries returns an unsorted list of all current directory entries. -func (srv *notifyService) GetEntries() ([]*directory.Entry, error) { - resChan := make(chan resGetEntries) - srv.cmds <- &cmdGetEntries{resChan} - return <-resChan, nil -} - -// GetEntry returns the entry with the specified zettel id. If there is no such -// zettel id, an empty entry is returned. -func (srv *notifyService) GetEntry(zid id.Zid) (*directory.Entry, error) { - resChan := make(chan resGetEntry) - srv.cmds <- &cmdGetEntry{zid, resChan} - return <-resChan, nil -} - -// GetNew returns an entry with a new zettel id. -func (srv *notifyService) GetNew() (*directory.Entry, error) { - resChan := make(chan resNewEntry) - srv.cmds <- &cmdNewEntry{resChan} - result := <-resChan - return result.entry, result.err -} - -// UpdateEntry notifies the directory of an updated entry. -func (srv *notifyService) UpdateEntry(entry *directory.Entry) error { - resChan := make(chan struct{}) - srv.cmds <- &cmdUpdateEntry{entry, resChan} - <-resChan - return nil -} - -// RenameEntry notifies the directory of an renamed entry. -func (srv *notifyService) RenameEntry(curEntry, newEntry *directory.Entry) error { - resChan := make(chan resRenameEntry) - srv.cmds <- &cmdRenameEntry{curEntry, newEntry, resChan} - return <-resChan -} - -// DeleteEntry removes a zettel id from the directory of entries. -func (srv *notifyService) DeleteEntry(zid id.Zid) error { - resChan := make(chan struct{}) - srv.cmds <- &cmdDeleteEntry{zid, resChan} - <-resChan - return nil -} DELETED box/dirbox/notifydir/service.go Index: box/dirbox/notifydir/service.go ================================================================== --- box/dirbox/notifydir/service.go +++ box/dirbox/notifydir/service.go @@ -1,255 +0,0 @@ -//----------------------------------------------------------------------------- -// Copyright (c) 2020-2021 Detlef Stern -// -// This file is part of zettelstore. -// -// Zettelstore is licensed under the latest version of the EUPL (European Union -// Public License). Please see file LICENSE.txt for your rights and obligations -// under this license. -//----------------------------------------------------------------------------- - -// Package notifydir manages the notified directory part of a dirstore. -package notifydir - -import ( - "log" - "time" - - "zettelstore.de/z/box" - "zettelstore.de/z/box/dirbox/directory" - "zettelstore.de/z/domain/id" -) - -// ping sends every tick a signal to reload the directory list -func ping(tick chan<- struct{}, rescanTime time.Duration, done <-chan struct{}) { - ticker := time.NewTicker(rescanTime) - defer close(tick) - for { - select { - case _, ok := <-ticker.C: - if !ok { - return - } - tick <- struct{}{} - case _, ok := <-done: - if !ok { - ticker.Stop() - return - } - } - } -} - -func newEntry(ev *fileEvent) *directory.Entry { - de := new(directory.Entry) - de.Zid = ev.zid - updateEntry(de, ev) - return de -} - -func updateEntry(de *directory.Entry, ev *fileEvent) { - if ev.ext == "meta" { - de.MetaSpec = directory.MetaSpecFile - de.MetaPath = ev.path - return - } - if de.ContentExt != "" && de.ContentExt != ev.ext { - de.Duplicates = true - return - } - if de.MetaSpec != directory.MetaSpecFile { - if ev.ext == "zettel" { - de.MetaSpec = directory.MetaSpecHeader - } else { - de.MetaSpec = directory.MetaSpecNone - } - } - de.ContentPath = ev.path - de.ContentExt = ev.ext -} - -type dirMap map[id.Zid]*directory.Entry - -func dirMapUpdate(dm dirMap, ev *fileEvent) { - de := dm[ev.zid] - if de == nil { - dm[ev.zid] = newEntry(ev) - return - } - updateEntry(de, ev) -} - -func deleteFromMap(dm dirMap, ev *fileEvent) { - if ev.ext == "meta" { - if entry, ok := dm[ev.zid]; ok { - if entry.MetaSpec == directory.MetaSpecFile { - entry.MetaSpec = directory.MetaSpecNone - return - } - } - } - delete(dm, ev.zid) -} - -// directoryService is the main service. -func (srv *notifyService) directoryService(events <-chan *fileEvent, ready chan<- int) { - curMap := make(dirMap) - var newMap dirMap - for { - select { - case ev, ok := <-events: - if !ok { - return - } - switch ev.status { - case fileStatusReloadStart: - newMap = make(dirMap) - case fileStatusReloadEnd: - curMap = newMap - newMap = nil - if ready != nil { - ready <- len(curMap) - close(ready) - ready = nil - } - srv.notifyChange(box.OnReload, id.Invalid) - case fileStatusError: - log.Println("DIRBOX", "ERROR", ev.err) - case fileStatusUpdate: - srv.processFileUpdateEvent(ev, curMap, newMap) - case fileStatusDelete: - srv.processFileDeleteEvent(ev, curMap, newMap) - } - case cmd, ok := <-srv.cmds: - if ok { - cmd.run(curMap) - } - } - } -} - -func (srv *notifyService) processFileUpdateEvent(ev *fileEvent, curMap, newMap dirMap) { - if newMap != nil { - dirMapUpdate(newMap, ev) - } else { - dirMapUpdate(curMap, ev) - srv.notifyChange(box.OnUpdate, ev.zid) - } -} - -func (srv *notifyService) processFileDeleteEvent(ev *fileEvent, curMap, newMap dirMap) { - if newMap != nil { - deleteFromMap(newMap, ev) - } else { - deleteFromMap(curMap, ev) - srv.notifyChange(box.OnDelete, ev.zid) - } -} - -type dirCmd interface { - run(m dirMap) -} - -type cmdNumEntries struct { - result chan<- resNumEntries -} -type resNumEntries = int - -func (cmd *cmdNumEntries) run(m dirMap) { - cmd.result <- len(m) -} - -type cmdGetEntries struct { - result chan<- resGetEntries -} -type resGetEntries []*directory.Entry - -func (cmd *cmdGetEntries) run(m dirMap) { - res := make([]*directory.Entry, len(m)) - i := 0 - for _, de := range m { - entry := *de - res[i] = &entry - i++ - } - cmd.result <- res -} - -type cmdGetEntry struct { - zid id.Zid - result chan<- resGetEntry -} -type resGetEntry = *directory.Entry - -func (cmd *cmdGetEntry) run(m dirMap) { - entry := m[cmd.zid] - if entry == nil { - cmd.result <- nil - } else { - result := *entry - cmd.result <- &result - } -} - -type cmdNewEntry struct { - result chan<- resNewEntry -} -type resNewEntry struct { - entry *directory.Entry - err error -} - -func (cmd *cmdNewEntry) run(m dirMap) { - zid, err := box.GetNewZid(func(zid id.Zid) (bool, error) { - _, ok := m[zid] - return !ok, nil - }) - if err != nil { - cmd.result <- resNewEntry{nil, err} - return - } - entry := &directory.Entry{Zid: zid} - m[zid] = entry - cmd.result <- resNewEntry{&directory.Entry{Zid: zid}, nil} -} - -type cmdUpdateEntry struct { - entry *directory.Entry - result chan<- struct{} -} - -func (cmd *cmdUpdateEntry) run(m dirMap) { - entry := *cmd.entry - m[entry.Zid] = &entry - cmd.result <- struct{}{} -} - -type cmdRenameEntry struct { - curEntry *directory.Entry - newEntry *directory.Entry - result chan<- resRenameEntry -} - -type resRenameEntry = error - -func (cmd *cmdRenameEntry) run(m dirMap) { - newEntry := *cmd.newEntry - newZid := newEntry.Zid - if _, found := m[newZid]; found { - cmd.result <- &box.ErrInvalidID{Zid: newZid} - return - } - delete(m, cmd.curEntry.Zid) - m[newZid] = &newEntry - cmd.result <- nil -} - -type cmdDeleteEntry struct { - zid id.Zid - result chan<- struct{} -} - -func (cmd *cmdDeleteEntry) run(m dirMap) { - delete(m, cmd.zid) - cmd.result <- struct{}{} -} DELETED box/dirbox/notifydir/watch.go Index: box/dirbox/notifydir/watch.go ================================================================== --- box/dirbox/notifydir/watch.go +++ box/dirbox/notifydir/watch.go @@ -1,300 +0,0 @@ -//----------------------------------------------------------------------------- -// Copyright (c) 2020-2021 Detlef Stern -// -// This file is part of zettelstore. -// -// Zettelstore is licensed under the latest version of the EUPL (European Union -// Public License). Please see file LICENSE.txt for your rights and obligations -// under this license. -//----------------------------------------------------------------------------- - -// Package notifydir manages the notified directory part of a dirstore. -package notifydir - -import ( - "os" - "path/filepath" - "regexp" - "time" - - "github.com/fsnotify/fsnotify" - - "zettelstore.de/z/domain/id" -) - -var validFileName = regexp.MustCompile(`^(\d{14}).*(\.(.+))$`) - -func matchValidFileName(name string) []string { - return validFileName.FindStringSubmatch(name) -} - -type fileStatus int - -const ( - fileStatusNone fileStatus = iota - fileStatusReloadStart - fileStatusReloadEnd - fileStatusError - fileStatusUpdate - fileStatusDelete -) - -type fileEvent struct { - status fileStatus - path string // Full file path - zid id.Zid - ext string // File extension - err error // Error if Status == fileStatusError -} - -type sendResult int - -const ( - sendDone sendResult = iota - sendReload - sendExit -) - -func watchDirectory(directory string, events chan<- *fileEvent, tick <-chan struct{}) { - defer close(events) - - var watcher *fsnotify.Watcher - defer func() { - if watcher != nil { - watcher.Close() - } - }() - - sendEvent := func(ev *fileEvent) sendResult { - select { - case events <- ev: - case _, ok := <-tick: - if ok { - return sendReload - } - return sendExit - } - return sendDone - } - - sendError := func(err error) sendResult { - return sendEvent(&fileEvent{status: fileStatusError, err: err}) - } - - sendFileEvent := func(status fileStatus, path string, match []string) sendResult { - zid, err := id.Parse(match[1]) - if err != nil { - return sendDone - } - event := &fileEvent{ - status: status, - path: path, - zid: zid, - ext: match[3], - } - return sendEvent(event) - } - - reloadStartEvent := &fileEvent{status: fileStatusReloadStart} - reloadEndEvent := &fileEvent{status: fileStatusReloadEnd} - reloadFiles := func() bool { - entries, err := os.ReadDir(directory) - if err != nil { - if res := sendError(err); res != sendDone { - return res == sendReload - } - return true - } - - if res := sendEvent(reloadStartEvent); res != sendDone { - return res == sendReload - } - - if watcher != nil { - watcher.Close() - } - watcher, err = fsnotify.NewWatcher() - if err != nil { - if res := sendError(err); res != sendDone { - return res == sendReload - } - } - - for _, entry := range entries { - if entry.IsDir() { - continue - } - if info, err1 := entry.Info(); err1 != nil || !info.Mode().IsRegular() { - continue - } - name := entry.Name() - match := matchValidFileName(name) - if len(match) > 0 { - path := filepath.Join(directory, name) - if res := sendFileEvent(fileStatusUpdate, path, match); res != sendDone { - return res == sendReload - } - } - } - - if watcher != nil { - err = watcher.Add(directory) - if err != nil { - if res := sendError(err); res != sendDone { - return res == sendReload - } - } - } - if res := sendEvent(reloadEndEvent); res != sendDone { - return res == sendReload - } - return true - } - - handleEvents := func() bool { - const createOps = fsnotify.Create | fsnotify.Write - const deleteOps = fsnotify.Remove | fsnotify.Rename - - for { - select { - case wevent, ok := <-watcher.Events: - if !ok { - return false - } - path := filepath.Clean(wevent.Name) - match := matchValidFileName(filepath.Base(path)) - if len(match) == 0 { - continue - } - if wevent.Op&createOps != 0 { - if fi, err := os.Lstat(path); err != nil || !fi.Mode().IsRegular() { - continue - } - if res := sendFileEvent( - fileStatusUpdate, path, match); res != sendDone { - return res == sendReload - } - } - if wevent.Op&deleteOps != 0 { - if res := sendFileEvent( - fileStatusDelete, path, match); res != sendDone { - return res == sendReload - } - } - case err, ok := <-watcher.Errors: - if !ok { - return false - } - if res := sendError(err); res != sendDone { - return res == sendReload - } - case _, ok := <-tick: - return ok - } - } - } - - for { - if !reloadFiles() { - return - } - if watcher == nil { - if _, ok := <-tick; !ok { - return - } - } else { - if !handleEvents() { - return - } - } - } -} - -func sendCollectedEvents(out chan<- *fileEvent, events []*fileEvent) { - for _, ev := range events { - if ev.status != fileStatusNone { - out <- ev - } - } -} - -func addEvent(events []*fileEvent, ev *fileEvent) []*fileEvent { - switch ev.status { - case fileStatusNone: - return events - case fileStatusReloadStart: - events = events[0:0] - case fileStatusUpdate, fileStatusDelete: - if len(events) > 0 && mergeEvents(events, ev) { - return events - } - } - return append(events, ev) -} - -func mergeEvents(events []*fileEvent, ev *fileEvent) bool { - for i := len(events) - 1; i >= 0; i-- { - oev := events[i] - switch oev.status { - case fileStatusReloadStart, fileStatusReloadEnd: - return false - case fileStatusUpdate, fileStatusDelete: - if ev.path == oev.path { - if ev.status == oev.status { - return true - } - oev.status = fileStatusNone - return false - } - } - } - return false -} - -func collectEvents(out chan<- *fileEvent, in <-chan *fileEvent) { - defer close(out) - - var sendTime time.Time - sendTimeSet := false - ticker := time.NewTicker(500 * time.Millisecond) - defer ticker.Stop() - - events := make([]*fileEvent, 0, 32) - buffer := false - for { - select { - case ev, ok := <-in: - if !ok { - sendCollectedEvents(out, events) - return - } - if ev.status == fileStatusReloadStart { - buffer = false - events = events[0:0] - } - if buffer { - if !sendTimeSet { - sendTime = time.Now().Add(1500 * time.Millisecond) - sendTimeSet = true - } - events = addEvent(events, ev) - if len(events) > 1024 { - sendCollectedEvents(out, events) - events = events[0:0] - sendTimeSet = false - } - continue - } - out <- ev - if ev.status == fileStatusReloadEnd { - buffer = true - } - case now := <-ticker.C: - if sendTimeSet && now.After(sendTime) { - sendCollectedEvents(out, events) - events = events[0:0] - sendTimeSet = false - } - } - } -} DELETED box/dirbox/notifydir/watch_test.go Index: box/dirbox/notifydir/watch_test.go ================================================================== --- box/dirbox/notifydir/watch_test.go +++ box/dirbox/notifydir/watch_test.go @@ -1,56 +0,0 @@ -//----------------------------------------------------------------------------- -// Copyright (c) 2020-2021 Detlef Stern -// -// This file is part of zettelstore. -// -// Zettelstore is licensed under the latest version of the EUPL (European Union -// Public License). Please see file LICENSE.txt for your rights and obligations -// under this license. -//----------------------------------------------------------------------------- - -// Package notifydir manages the notified directory part of a dirstore. -package notifydir - -import "testing" - -func sameStringSlices(sl1, sl2 []string) bool { - if len(sl1) != len(sl2) { - return false - } - for i := 0; i < len(sl1); i++ { - if sl1[i] != sl2[i] { - return false - } - } - return true -} - -func TestMatchValidFileName(t *testing.T) { - t.Parallel() - testcases := []struct { - name string - exp []string - }{ - {"", []string{}}, - {".txt", []string{}}, - {"12345678901234.txt", []string{"12345678901234", ".txt", "txt"}}, - {"12345678901234abc.txt", []string{"12345678901234", ".txt", "txt"}}, - {"12345678901234.abc.txt", []string{"12345678901234", ".txt", "txt"}}, - } - - for i, tc := range testcases { - got := matchValidFileName(tc.name) - if len(got) == 0 { - if len(tc.exp) > 0 { - t.Errorf("TC=%d, name=%q, exp=%v, got=%v", i, tc.name, tc.exp, got) - } - } else { - if got[0] != tc.name { - t.Errorf("TC=%d, name=%q, got=%v", i, tc.name, got) - } - if !sameStringSlices(got[1:], tc.exp) { - t.Errorf("TC=%d, name=%q, exp=%v, got=%v", i, tc.name, tc.exp, got) - } - } - } -} DELETED box/dirbox/service.go Index: box/dirbox/service.go ================================================================== --- box/dirbox/service.go +++ box/dirbox/service.go @@ -1,290 +0,0 @@ -//----------------------------------------------------------------------------- -// 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 dirbox provides a directory-based zettel box. -package dirbox - -import ( - "os" - - "zettelstore.de/z/box/dirbox/directory" - "zettelstore.de/z/box/filebox" - "zettelstore.de/z/domain" - "zettelstore.de/z/domain/id" - "zettelstore.de/z/domain/meta" - "zettelstore.de/z/input" -) - -func fileService(num uint32, cmds <-chan fileCmd) { - for cmd := range cmds { - cmd.run() - } -} - -type fileCmd interface { - run() -} - -// COMMAND: getMeta ---------------------------------------- -// -// Retrieves the meta data from a zettel. - -func getMeta(dp *dirBox, entry *directory.Entry, zid id.Zid) (*meta.Meta, error) { - rc := make(chan resGetMeta) - dp.getFileChan(zid) <- &fileGetMeta{entry, rc} - res := <-rc - close(rc) - return res.meta, res.err -} - -type fileGetMeta struct { - entry *directory.Entry - rc chan<- resGetMeta -} -type resGetMeta struct { - meta *meta.Meta - err error -} - -func (cmd *fileGetMeta) run() { - entry := cmd.entry - var m *meta.Meta - var err error - switch entry.MetaSpec { - case directory.MetaSpecFile: - m, err = parseMetaFile(entry.Zid, entry.MetaPath) - case directory.MetaSpecHeader: - m, _, err = parseMetaContentFile(entry.Zid, entry.ContentPath) - default: - m = filebox.CalcDefaultMeta(entry.Zid, entry.ContentExt) - } - if err == nil { - cmdCleanupMeta(m, entry) - } - cmd.rc <- resGetMeta{m, err} -} - -// COMMAND: getMetaContent ---------------------------------------- -// -// Retrieves the meta data and the content of a zettel. - -func getMetaContent(dp *dirBox, entry *directory.Entry, zid id.Zid) (*meta.Meta, string, error) { - rc := make(chan resGetMetaContent) - dp.getFileChan(zid) <- &fileGetMetaContent{entry, rc} - res := <-rc - close(rc) - return res.meta, res.content, res.err -} - -type fileGetMetaContent struct { - entry *directory.Entry - rc chan<- resGetMetaContent -} -type resGetMetaContent struct { - meta *meta.Meta - content string - err error -} - -func (cmd *fileGetMetaContent) run() { - var m *meta.Meta - var content string - var err error - - entry := cmd.entry - switch entry.MetaSpec { - case directory.MetaSpecFile: - m, err = parseMetaFile(entry.Zid, entry.MetaPath) - if err == nil { - content, err = readFileContent(entry.ContentPath) - } - case directory.MetaSpecHeader: - m, content, err = parseMetaContentFile(entry.Zid, entry.ContentPath) - default: - m = filebox.CalcDefaultMeta(entry.Zid, entry.ContentExt) - content, err = readFileContent(entry.ContentPath) - } - if err == nil { - cmdCleanupMeta(m, entry) - } - cmd.rc <- resGetMetaContent{m, content, err} -} - -// COMMAND: setZettel ---------------------------------------- -// -// Writes a new or exsting zettel. - -func setZettel(dp *dirBox, entry *directory.Entry, zettel domain.Zettel) error { - rc := make(chan resSetZettel) - dp.getFileChan(zettel.Meta.Zid) <- &fileSetZettel{entry, zettel, rc} - err := <-rc - close(rc) - return err -} - -type fileSetZettel struct { - entry *directory.Entry - zettel domain.Zettel - rc chan<- resSetZettel -} -type resSetZettel = error - -func (cmd *fileSetZettel) run() { - var err error - switch cmd.entry.MetaSpec { - case directory.MetaSpecFile: - err = cmd.runMetaSpecFile() - case directory.MetaSpecHeader: - err = cmd.runMetaSpecHeader() - case directory.MetaSpecNone: - // TODO: if meta has some additional infos: write meta to new .meta; - // update entry in dir - err = writeFileContent(cmd.entry.ContentPath, cmd.zettel.Content.AsString()) - default: - panic("TODO: ???") - } - cmd.rc <- err -} - -func (cmd *fileSetZettel) runMetaSpecFile() error { - f, err := openFileWrite(cmd.entry.MetaPath) - if err == nil { - err = writeFileZid(f, cmd.zettel.Meta.Zid) - if err == nil { - _, err = cmd.zettel.Meta.Write(f, true) - if err1 := f.Close(); err == nil { - err = err1 - } - if err == nil { - err = writeFileContent(cmd.entry.ContentPath, cmd.zettel.Content.AsString()) - } - } - } - return err -} - -func (cmd *fileSetZettel) runMetaSpecHeader() error { - f, err := openFileWrite(cmd.entry.ContentPath) - if err == nil { - err = writeFileZid(f, cmd.zettel.Meta.Zid) - if err == nil { - _, err = cmd.zettel.Meta.WriteAsHeader(f, true) - if err == nil { - _, err = f.WriteString(cmd.zettel.Content.AsString()) - if err1 := f.Close(); err == nil { - err = err1 - } - } - } - } - return err -} - -// COMMAND: deleteZettel ---------------------------------------- -// -// Deletes an existing zettel. - -func deleteZettel(dp *dirBox, entry *directory.Entry, zid id.Zid) error { - rc := make(chan resDeleteZettel) - dp.getFileChan(zid) <- &fileDeleteZettel{entry, rc} - err := <-rc - close(rc) - return err -} - -type fileDeleteZettel struct { - entry *directory.Entry - rc chan<- resDeleteZettel -} -type resDeleteZettel = error - -func (cmd *fileDeleteZettel) run() { - var err error - - switch cmd.entry.MetaSpec { - case directory.MetaSpecFile: - err1 := os.Remove(cmd.entry.MetaPath) - err = os.Remove(cmd.entry.ContentPath) - if err == nil { - err = err1 - } - case directory.MetaSpecHeader: - err = os.Remove(cmd.entry.ContentPath) - case directory.MetaSpecNone: - err = os.Remove(cmd.entry.ContentPath) - default: - panic("TODO: ???") - } - cmd.rc <- err -} - -// Utility functions ---------------------------------------- - -func readFileContent(path string) (string, error) { - data, err := os.ReadFile(path) - if err != nil { - return "", err - } - return string(data), nil -} - -func parseMetaFile(zid id.Zid, path string) (*meta.Meta, error) { - src, err := readFileContent(path) - if err != nil { - return nil, err - } - inp := input.NewInput(src) - return meta.NewFromInput(zid, inp), nil -} - -func parseMetaContentFile(zid id.Zid, path string) (*meta.Meta, string, error) { - src, err := readFileContent(path) - if err != nil { - return nil, "", err - } - inp := input.NewInput(src) - meta := meta.NewFromInput(zid, inp) - return meta, src[inp.Pos:], nil -} - -func cmdCleanupMeta(m *meta.Meta, entry *directory.Entry) { - filebox.CleanupMeta( - m, - entry.Zid, entry.ContentExt, - entry.MetaSpec == directory.MetaSpecFile, - entry.Duplicates, - ) -} - -func openFileWrite(path string) (*os.File, error) { - return os.OpenFile(path, os.O_WRONLY|os.O_CREATE|os.O_TRUNC, 0600) -} - -func writeFileZid(f *os.File, zid id.Zid) error { - _, err := f.WriteString("id: ") - if err == nil { - _, err = f.Write(zid.Bytes()) - if err == nil { - _, err = f.WriteString("\n") - } - } - return err -} - -func writeFileContent(path, content string) error { - f, err := openFileWrite(path) - if err == nil { - _, err = f.WriteString(content) - if err1 := f.Close(); err == nil { - err = err1 - } - } - return err -} DELETED box/dirbox/simpledir/simpledir.go Index: box/dirbox/simpledir/simpledir.go ================================================================== --- box/dirbox/simpledir/simpledir.go +++ box/dirbox/simpledir/simpledir.go @@ -1,185 +0,0 @@ -//----------------------------------------------------------------------------- -// Copyright (c) 2021 Detlef Stern -// -// This file is part of zettelstore. -// -// Zettelstore is licensed under the latest version of the EUPL (European Union -// Public License). Please see file LICENSE.txt for your rights and obligations -// under this license. -//----------------------------------------------------------------------------- - -// Package simpledir manages the directory part of a dirstore. -package simpledir - -import ( - "os" - "path/filepath" - "regexp" - "sync" - - "zettelstore.de/z/box" - "zettelstore.de/z/box/dirbox/directory" - "zettelstore.de/z/domain/id" -) - -// simpleService specifies a directory service without scanning. -type simpleService struct { - dirPath string - mx sync.Mutex -} - -// NewService creates a new directory service. -func NewService(directoryPath string) directory.Service { - return &simpleService{ - dirPath: directoryPath, - } -} - -func (ss *simpleService) Start() error { - ss.mx.Lock() - defer ss.mx.Unlock() - _, err := os.ReadDir(ss.dirPath) - return err -} - -func (ss *simpleService) Stop() error { - return nil -} - -func (ss *simpleService) NumEntries() (int, error) { - ss.mx.Lock() - defer ss.mx.Unlock() - entries, err := ss.getEntries() - if err == nil { - return len(entries), nil - } - return 0, err -} - -func (ss *simpleService) GetEntries() ([]*directory.Entry, error) { - ss.mx.Lock() - defer ss.mx.Unlock() - entrySet, err := ss.getEntries() - if err != nil { - return nil, err - } - result := make([]*directory.Entry, 0, len(entrySet)) - for _, entry := range entrySet { - result = append(result, entry) - } - return result, nil -} -func (ss *simpleService) getEntries() (map[id.Zid]*directory.Entry, error) { - dirEntries, err := os.ReadDir(ss.dirPath) - if err != nil { - return nil, err - } - entrySet := make(map[id.Zid]*directory.Entry) - for _, dirEntry := range dirEntries { - if dirEntry.IsDir() { - continue - } - if info, err1 := dirEntry.Info(); err1 != nil || !info.Mode().IsRegular() { - continue - } - name := dirEntry.Name() - match := matchValidFileName(name) - if len(match) == 0 { - continue - } - zid, err := id.Parse(match[1]) - if err != nil { - continue - } - var entry *directory.Entry - if e, ok := entrySet[zid]; ok { - entry = e - } else { - entry = &directory.Entry{Zid: zid} - entrySet[zid] = entry - } - updateEntry(entry, filepath.Join(ss.dirPath, name), match[3]) - } - return entrySet, nil -} - -var validFileName = regexp.MustCompile(`^(\d{14}).*(\.(.+))$`) - -func matchValidFileName(name string) []string { - return validFileName.FindStringSubmatch(name) -} - -func updateEntry(entry *directory.Entry, path, ext string) { - if ext == "meta" { - entry.MetaSpec = directory.MetaSpecFile - entry.MetaPath = path - } else if entry.ContentExt != "" && entry.ContentExt != ext { - entry.Duplicates = true - } else { - if entry.MetaSpec != directory.MetaSpecFile { - if ext == "zettel" { - entry.MetaSpec = directory.MetaSpecHeader - } else { - entry.MetaSpec = directory.MetaSpecNone - } - } - entry.ContentPath = path - entry.ContentExt = ext - } -} - -func (ss *simpleService) GetEntry(zid id.Zid) (*directory.Entry, error) { - ss.mx.Lock() - defer ss.mx.Unlock() - return ss.getEntry(zid) -} -func (ss *simpleService) getEntry(zid id.Zid) (*directory.Entry, error) { - pattern := filepath.Join(ss.dirPath, zid.String()) + "*.*" - paths, err := filepath.Glob(pattern) - if err != nil { - return nil, err - } - if len(paths) == 0 { - return nil, nil - } - entry := &directory.Entry{Zid: zid} - for _, path := range paths { - ext := filepath.Ext(path) - if len(ext) > 0 && ext[0] == '.' { - ext = ext[1:] - } - updateEntry(entry, path, ext) - } - return entry, nil -} - -func (ss *simpleService) GetNew() (*directory.Entry, error) { - ss.mx.Lock() - defer ss.mx.Unlock() - zid, err := box.GetNewZid(func(zid id.Zid) (bool, error) { - entry, err := ss.getEntry(zid) - if err != nil { - return false, nil - } - return !entry.IsValid(), nil - }) - if err != nil { - return nil, err - } - return &directory.Entry{Zid: zid}, nil -} - -func (ss *simpleService) UpdateEntry(entry *directory.Entry) error { - // Nothing to to, since the actual file update is done by dirbox. - return nil -} - -func (ss *simpleService) RenameEntry(curEntry, newEntry *directory.Entry) error { - // Nothing to to, since the actual file rename is done by dirbox. - return nil -} - -func (ss *simpleService) DeleteEntry(zid id.Zid) error { - // Nothing to to, since the actual file delete is done by dirbox. - return nil -} DELETED box/filebox/filebox.go Index: box/filebox/filebox.go ================================================================== --- box/filebox/filebox.go +++ box/filebox/filebox.go @@ -1,94 +0,0 @@ -//----------------------------------------------------------------------------- -// Copyright (c) 2021 Detlef Stern -// -// This file is part of zettelstore. -// -// Zettelstore is licensed under the latest version of the EUPL (European Union -// Public License). Please see file LICENSE.txt for your rights and obligations -// under this license. -//----------------------------------------------------------------------------- - -// Package filebox provides boxes that are stored in a file. -package filebox - -import ( - "errors" - "net/url" - "path/filepath" - "strings" - - "zettelstore.de/z/box" - "zettelstore.de/z/box/manager" - "zettelstore.de/z/domain/id" - "zettelstore.de/z/domain/meta" -) - -func init() { - manager.Register("file", func(u *url.URL, cdata *manager.ConnectData) (box.ManagedBox, error) { - path := getFilepathFromURL(u) - ext := strings.ToLower(filepath.Ext(path)) - if ext != ".zip" { - return nil, errors.New("unknown extension '" + ext + "' in box URL: " + u.String()) - } - return &zipBox{ - number: cdata.Number, - name: path, - enricher: cdata.Enricher, - }, 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(meta.KeyTitle, zid.String()) - m.Set(meta.KeySyntax, calculateSyntax(ext)) - return m -} - -// CleanupMeta enhances the given metadata. -func CleanupMeta(m *meta.Meta, zid id.Zid, ext string, inMeta, duplicates bool) { - if title, ok := m.Get(meta.KeyTitle); !ok || title == "" { - m.Set(meta.KeyTitle, zid.String()) - } - - if inMeta { - if syntax, ok := m.Get(meta.KeySyntax); !ok || syntax == "" { - dm := CalcDefaultMeta(zid, ext) - syntax, ok = dm.Get(meta.KeySyntax) - if !ok { - panic("Default meta must contain syntax") - } - m.Set(meta.KeySyntax, syntax) - } - } - - if duplicates { - m.Set(meta.KeyDuplicates, meta.ValueTrue) - } -} DELETED box/filebox/zipbox.go Index: box/filebox/zipbox.go ================================================================== --- box/filebox/zipbox.go +++ box/filebox/zipbox.go @@ -1,261 +0,0 @@ -//----------------------------------------------------------------------------- -// Copyright (c) 2021 Detlef Stern -// -// This file is part of zettelstore. -// -// Zettelstore is licensed under the latest version of the EUPL (European Union -// Public License). Please see file LICENSE.txt for your rights and obligations -// under this license. -//----------------------------------------------------------------------------- - -// Package filebox provides boxes that are stored in a file. -package filebox - -import ( - "archive/zip" - "context" - "io" - "regexp" - "strings" - - "zettelstore.de/z/box" - "zettelstore.de/z/domain" - "zettelstore.de/z/domain/id" - "zettelstore.de/z/domain/meta" - "zettelstore.de/z/input" - "zettelstore.de/z/search" -) - -var validFileName = regexp.MustCompile(`^(\d{14}).*(\.(.+))$`) - -func matchValidFileName(name string) []string { - return validFileName.FindStringSubmatch(name) -} - -type zipEntry struct { - metaName string - contentName string - contentExt string // (normalized) file extension of zettel content - metaInHeader bool -} - -type zipBox struct { - number int - name string - enricher box.Enricher - zettel map[id.Zid]*zipEntry // no lock needed, because read-only after creation -} - -func (zp *zipBox) Location() string { - if strings.HasPrefix(zp.name, "/") { - return "file://" + zp.name - } - return "file:" + zp.name -} - -func (zp *zipBox) Start(ctx context.Context) error { - reader, err := zip.OpenReader(zp.name) - if err != nil { - return err - } - defer reader.Close() - zp.zettel = make(map[id.Zid]*zipEntry) - for _, f := range reader.File { - match := matchValidFileName(f.Name) - if len(match) < 1 { - continue - } - zid, err := id.Parse(match[1]) - if err != nil { - continue - } - zp.addFile(zid, f.Name, match[3]) - } - return nil -} - -func (zp *zipBox) addFile(zid id.Zid, name, ext string) { - entry := zp.zettel[zid] - if entry == nil { - entry = &zipEntry{} - zp.zettel[zid] = entry - } - switch ext { - case "zettel": - if entry.contentExt == "" { - entry.contentName = name - entry.contentExt = ext - entry.metaInHeader = true - } - case "meta": - entry.metaName = name - entry.metaInHeader = false - default: - if entry.contentExt == "" { - entry.contentExt = ext - entry.contentName = name - } - } -} - -func (zp *zipBox) Stop(ctx context.Context) error { - zp.zettel = nil - return nil -} - -func (zp *zipBox) CanCreateZettel(ctx context.Context) bool { return false } - -func (zp *zipBox) CreateZettel(ctx context.Context, zettel domain.Zettel) (id.Zid, error) { - return id.Invalid, box.ErrReadOnly -} - -func (zp *zipBox) GetZettel(ctx context.Context, zid id.Zid) (domain.Zettel, error) { - entry, ok := zp.zettel[zid] - if !ok { - return domain.Zettel{}, box.ErrNotFound - } - reader, err := zip.OpenReader(zp.name) - if err != nil { - return domain.Zettel{}, err - } - defer reader.Close() - - var m *meta.Meta - var src string - var inMeta bool - if entry.metaInHeader { - src, err = readZipFileContent(reader, entry.contentName) - if err != nil { - return domain.Zettel{}, err - } - inp := input.NewInput(src) - m = meta.NewFromInput(zid, inp) - src = src[inp.Pos:] - } else if metaName := entry.metaName; metaName != "" { - m, err = readZipMetaFile(reader, zid, metaName) - if err != nil { - return domain.Zettel{}, err - } - src, err = readZipFileContent(reader, entry.contentName) - if err != nil { - return domain.Zettel{}, err - } - inMeta = true - } else { - m = CalcDefaultMeta(zid, entry.contentExt) - } - CleanupMeta(m, zid, entry.contentExt, inMeta, false) - return domain.Zettel{Meta: m, Content: domain.NewContent(src)}, nil -} - -func (zp *zipBox) GetMeta(ctx context.Context, zid id.Zid) (*meta.Meta, error) { - entry, ok := zp.zettel[zid] - if !ok { - return nil, box.ErrNotFound - } - reader, err := zip.OpenReader(zp.name) - if err != nil { - return nil, err - } - defer reader.Close() - return readZipMeta(reader, zid, entry) -} - -func (zp *zipBox) FetchZids(ctx context.Context) (id.Set, error) { - result := id.NewSetCap(len(zp.zettel)) - for zid := range zp.zettel { - result[zid] = true - } - return result, nil -} - -func (zp *zipBox) SelectMeta(ctx context.Context, match search.MetaMatchFunc) (res []*meta.Meta, err error) { - reader, err := zip.OpenReader(zp.name) - if err != nil { - return nil, err - } - defer reader.Close() - for zid, entry := range zp.zettel { - m, err := readZipMeta(reader, zid, entry) - if err != nil { - continue - } - zp.enricher.Enrich(ctx, m, zp.number) - if match(m) { - res = append(res, m) - } - } - return res, nil -} - -func (zp *zipBox) CanUpdateZettel(ctx context.Context, zettel domain.Zettel) bool { - return false -} - -func (zp *zipBox) UpdateZettel(ctx context.Context, zettel domain.Zettel) error { - return box.ErrReadOnly -} - -func (zp *zipBox) AllowRenameZettel(ctx context.Context, zid id.Zid) bool { - _, ok := zp.zettel[zid] - return !ok -} - -func (zp *zipBox) RenameZettel(ctx context.Context, curZid, newZid id.Zid) error { - if _, ok := zp.zettel[curZid]; ok { - return box.ErrReadOnly - } - return box.ErrNotFound -} - -func (zp *zipBox) CanDeleteZettel(ctx context.Context, zid id.Zid) bool { return false } - -func (zp *zipBox) DeleteZettel(ctx context.Context, zid id.Zid) error { - if _, ok := zp.zettel[zid]; ok { - return box.ErrReadOnly - } - return box.ErrNotFound -} - -func (zp *zipBox) ReadStats(st *box.ManagedBoxStats) { - st.ReadOnly = true - st.Zettel = len(zp.zettel) -} - -func readZipMeta(reader *zip.ReadCloser, zid id.Zid, entry *zipEntry) (m *meta.Meta, err error) { - var inMeta bool - if entry.metaInHeader { - m, err = readZipMetaFile(reader, zid, entry.contentName) - } else if metaName := entry.metaName; metaName != "" { - m, err = readZipMetaFile(reader, zid, entry.metaName) - inMeta = true - } else { - m = CalcDefaultMeta(zid, entry.contentExt) - } - if err == nil { - CleanupMeta(m, zid, entry.contentExt, inMeta, false) - } - return m, err -} - -func readZipMetaFile(reader *zip.ReadCloser, zid id.Zid, name string) (*meta.Meta, error) { - 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) (string, error) { - f, err := reader.Open(name) - if err != nil { - return "", err - } - defer f.Close() - buf, err := io.ReadAll(f) - if err != nil { - return "", err - } - return string(buf), nil -} DELETED box/helper.go Index: box/helper.go ================================================================== --- box/helper.go +++ box/helper.go @@ -1,37 +0,0 @@ -//----------------------------------------------------------------------------- -// Copyright (c) 2021 Detlef Stern -// -// This file is part of zettelstore. -// -// Zettelstore is licensed under the latest version of the EUPL (European Union -// Public License). Please see file LICENSE.txt for your rights and obligations -// under this license. -//----------------------------------------------------------------------------- - -// Package box provides a generic interface to zettel boxes. -package box - -import ( - "time" - - "zettelstore.de/z/domain/id" -) - -// GetNewZid calculates a new and unused zettel identifier, based on the current date and time. -func GetNewZid(testZid func(id.Zid) (bool, error)) (id.Zid, error) { - withSeconds := false - for i := 0; i < 90; i++ { // Must be completed within 9 seconds (less than web/server.writeTimeout) - zid := id.New(withSeconds) - found, err := testZid(zid) - if err != nil { - return id.Invalid, err - } - if found { - return zid, nil - } - // TODO: do not wait here unconditionally. - time.Sleep(100 * time.Millisecond) - withSeconds = true - } - return id.Invalid, ErrConflict -} DELETED box/manager/anteroom.go Index: box/manager/anteroom.go ================================================================== --- box/manager/anteroom.go +++ box/manager/anteroom.go @@ -1,165 +0,0 @@ -//----------------------------------------------------------------------------- -// Copyright (c) 2021 Detlef Stern -// -// This file is part of zettelstore. -// -// Zettelstore is licensed under the latest version of the EUPL (European Union -// Public License). Please see file LICENSE.txt for your rights and obligations -// under this license. -//----------------------------------------------------------------------------- - -// Package manager coordinates the various boxes and indexes of a Zettelstore. -package manager - -import ( - "sync" - - "zettelstore.de/z/domain/id" -) - -type arAction int - -const ( - arNothing arAction = iota - arReload - arUpdate - arDelete -) - -type anteroom struct { - num uint64 - next *anteroom - waiting map[id.Zid]arAction - curLoad int - reload bool -} - -type anterooms struct { - mx sync.Mutex - nextNum uint64 - first *anteroom - last *anteroom - maxLoad int -} - -func newAnterooms(maxLoad int) *anterooms { - return &anterooms{maxLoad: maxLoad} -} - -func (ar *anterooms) Enqueue(zid id.Zid, action arAction) { - if !zid.IsValid() || action == arNothing || action == arReload { - return - } - ar.mx.Lock() - defer ar.mx.Unlock() - if ar.first == nil { - ar.first = ar.makeAnteroom(zid, action) - ar.last = ar.first - return - } - for room := ar.first; room != nil; room = room.next { - if room.reload { - continue // Do not put zettel in reload room - } - a, ok := room.waiting[zid] - if !ok { - continue - } - switch action { - case a: - return - case arUpdate: - room.waiting[zid] = action - case arDelete: - room.waiting[zid] = action - } - return - } - if room := ar.last; !room.reload && (ar.maxLoad == 0 || room.curLoad < ar.maxLoad) { - room.waiting[zid] = action - room.curLoad++ - return - } - room := ar.makeAnteroom(zid, action) - ar.last.next = room - ar.last = room -} - -func (ar *anterooms) makeAnteroom(zid id.Zid, action arAction) *anteroom { - c := ar.maxLoad - if c == 0 { - c = 100 - } - waiting := make(map[id.Zid]arAction, c) - waiting[zid] = action - ar.nextNum++ - return &anteroom{num: ar.nextNum, next: nil, waiting: waiting, curLoad: 1, reload: false} -} - -func (ar *anterooms) Reset() { - ar.mx.Lock() - defer ar.mx.Unlock() - ar.first = ar.makeAnteroom(id.Invalid, arReload) - ar.last = ar.first -} - -func (ar *anterooms) Reload(newZids id.Set) uint64 { - ar.mx.Lock() - defer ar.mx.Unlock() - newWaiting := createWaitingSet(newZids, arUpdate) - ar.deleteReloadedRooms() - - if ns := len(newWaiting); ns > 0 { - ar.nextNum++ - ar.first = &anteroom{num: ar.nextNum, next: ar.first, waiting: newWaiting, curLoad: ns} - if ar.first.next == nil { - ar.last = ar.first - } - return ar.nextNum - } - - ar.first = nil - ar.last = nil - return 0 -} - -func createWaitingSet(zids id.Set, action arAction) map[id.Zid]arAction { - waitingSet := make(map[id.Zid]arAction, len(zids)) - for zid := range zids { - if zid.IsValid() { - waitingSet[zid] = action - } - } - return waitingSet -} - -func (ar *anterooms) deleteReloadedRooms() { - room := ar.first - for room != nil && room.reload { - room = room.next - } - ar.first = room - if room == nil { - ar.last = nil - } -} - -func (ar *anterooms) Dequeue() (arAction, id.Zid, uint64) { - ar.mx.Lock() - defer ar.mx.Unlock() - if ar.first == nil { - return arNothing, id.Invalid, 0 - } - for zid, action := range ar.first.waiting { - roomNo := ar.first.num - delete(ar.first.waiting, zid) - if len(ar.first.waiting) == 0 { - ar.first = ar.first.next - if ar.first == nil { - ar.last = nil - } - } - return action, zid, roomNo - } - return arNothing, id.Invalid, 0 -} 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 Detlef Stern -// -// This file is part of zettelstore. -// -// Zettelstore is licensed 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 manager coordinates the various boxes and indexes of a Zettelstore. -package manager - -import ( - "testing" - - "zettelstore.de/z/domain/id" -) - -func TestSimple(t *testing.T) { - t.Parallel() - ar := newAnterooms(2) - ar.Enqueue(id.Zid(1), arUpdate) - action, zid, rno := ar.Dequeue() - if zid != id.Zid(1) || action != arUpdate || rno != 1 { - t.Errorf("Expected arUpdate/1/1, but got %v/%v/%v", action, zid, rno) - } - action, zid, _ = ar.Dequeue() - if zid != id.Invalid && action != arDelete { - t.Errorf("Expected invalid Zid, but got %v", zid) - } - ar.Enqueue(id.Zid(1), arUpdate) - ar.Enqueue(id.Zid(2), arUpdate) - if ar.first != ar.last { - t.Errorf("Expected one room, but got more") - } - ar.Enqueue(id.Zid(3), arUpdate) - if ar.first == ar.last { - t.Errorf("Expected more than one room, but got only one") - } - - count := 0 - for ; count < 1000; count++ { - action, _, _ := ar.Dequeue() - if action == arNothing { - break - } - } - if count != 3 { - t.Errorf("Expected 3 dequeues, but got %v", count) - } -} - -func TestReset(t *testing.T) { - t.Parallel() - ar := newAnterooms(1) - ar.Enqueue(id.Zid(1), arUpdate) - ar.Reset() - action, zid, _ := ar.Dequeue() - if action != arReload || zid != id.Invalid { - t.Errorf("Expected reload & invalid Zid, but got %v/%v", action, zid) - } - ar.Reload(id.NewSet(3, 4)) - ar.Enqueue(id.Zid(5), arUpdate) - ar.Enqueue(id.Zid(5), arDelete) - ar.Enqueue(id.Zid(5), arDelete) - ar.Enqueue(id.Zid(5), arUpdate) - if ar.first == ar.last || ar.first.next != ar.last /*|| ar.first.next.next != ar.last*/ { - t.Errorf("Expected 2 rooms") - } - action, zid1, _ := ar.Dequeue() - if action != arUpdate { - t.Errorf("Expected arUpdate, but got %v", action) - } - action, zid2, _ := ar.Dequeue() - if action != arUpdate { - t.Errorf("Expected arUpdate, but got %v", action) - } - if !(zid1 == id.Zid(3) && zid2 == id.Zid(4) || zid1 == id.Zid(4) && zid2 == id.Zid(3)) { - t.Errorf("Zids must be 3 or 4, but got %v/%v", zid1, zid2) - } - action, zid, _ = ar.Dequeue() - if zid != id.Zid(5) || action != arUpdate { - t.Errorf("Expected 5/arUpdate, but got %v/%v", zid, action) - } - action, zid, _ = ar.Dequeue() - if action != arNothing || zid != id.Invalid { - t.Errorf("Expected nothing & invalid Zid, but got %v/%v", action, zid) - } - - ar = newAnterooms(1) - ar.Reload(id.NewSet(id.Zid(6))) - action, zid, _ = ar.Dequeue() - if zid != id.Zid(6) || action != arUpdate { - t.Errorf("Expected 6/arUpdate, but got %v/%v", zid, action) - } - action, zid, _ = ar.Dequeue() - if action != arNothing || zid != id.Invalid { - t.Errorf("Expected nothing & invalid Zid, but got %v/%v", action, zid) - } - - ar = newAnterooms(1) - ar.Enqueue(id.Zid(8), arUpdate) - ar.Reload(nil) - action, zid, _ = ar.Dequeue() - if action != arNothing || zid != id.Invalid { - t.Errorf("Expected nothing & invalid Zid, but got %v/%v", action, zid) - } -} DELETED box/manager/box.go Index: box/manager/box.go ================================================================== --- box/manager/box.go +++ box/manager/box.go @@ -1,277 +0,0 @@ -//----------------------------------------------------------------------------- -// Copyright (c) 2021 Detlef Stern -// -// This file is part of zettelstore. -// -// Zettelstore is licensed under the latest version of the EUPL (European Union -// Public License). Please see file LICENSE.txt for your rights and obligations -// under this license. -//----------------------------------------------------------------------------- - -// Package manager coordinates the various boxes and indexes of a Zettelstore. -package manager - -import ( - "context" - "errors" - "sort" - "strings" - - "zettelstore.de/z/box" - "zettelstore.de/z/domain" - "zettelstore.de/z/domain/id" - "zettelstore.de/z/domain/meta" - "zettelstore.de/z/search" -) - -// Conatains all box.Box related functions - -// Location returns some information where the box is located. -func (mgr *Manager) Location() string { - if len(mgr.boxes) <= 2 { - return "NONE" - } - var sb strings.Builder - for i := 0; i < len(mgr.boxes)-2; i++ { - 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 { - mgr.mgrMx.RLock() - defer mgr.mgrMx.RUnlock() - return mgr.started && mgr.boxes[0].CanCreateZettel(ctx) -} - -// CreateZettel creates a new zettel. -func (mgr *Manager) CreateZettel(ctx context.Context, zettel domain.Zettel) (id.Zid, error) { - mgr.mgrMx.RLock() - defer mgr.mgrMx.RUnlock() - if !mgr.started { - return id.Invalid, box.ErrStopped - } - return mgr.boxes[0].CreateZettel(ctx, zettel) -} - -// GetZettel retrieves a specific zettel. -func (mgr *Manager) GetZettel(ctx context.Context, zid id.Zid) (domain.Zettel, error) { - mgr.mgrMx.RLock() - defer mgr.mgrMx.RUnlock() - if !mgr.started { - return domain.Zettel{}, box.ErrStopped - } - for i, p := range mgr.boxes { - if z, err := p.GetZettel(ctx, zid); err != box.ErrNotFound { - if err == nil { - mgr.Enrich(ctx, z.Meta, i+1) - } - return z, err - } - } - return domain.Zettel{}, box.ErrNotFound -} - -// GetAllZettel retrieves a specific zettel from all managed boxes. -func (mgr *Manager) GetAllZettel(ctx context.Context, zid id.Zid) ([]domain.Zettel, error) { - mgr.mgrMx.RLock() - defer mgr.mgrMx.RUnlock() - if !mgr.started { - return nil, box.ErrStopped - } - var result []domain.Zettel - for i, p := range mgr.boxes { - if z, err := p.GetZettel(ctx, zid); err == nil { - mgr.Enrich(ctx, z.Meta, i+1) - result = append(result, z) - } - } - return result, nil -} - -// GetMeta retrieves just the meta data of a specific zettel. -func (mgr *Manager) GetMeta(ctx context.Context, zid id.Zid) (*meta.Meta, error) { - mgr.mgrMx.RLock() - defer mgr.mgrMx.RUnlock() - if !mgr.started { - return nil, box.ErrStopped - } - for i, p := range mgr.boxes { - if m, err := p.GetMeta(ctx, zid); err != box.ErrNotFound { - if err == nil { - mgr.Enrich(ctx, m, i+1) - } - return m, err - } - } - return nil, box.ErrNotFound -} - -// GetAllMeta retrieves the meta data of a specific zettel from all managed boxes. -func (mgr *Manager) GetAllMeta(ctx context.Context, zid id.Zid) ([]*meta.Meta, error) { - mgr.mgrMx.RLock() - defer mgr.mgrMx.RUnlock() - if !mgr.started { - return nil, box.ErrStopped - } - var result []*meta.Meta - for i, p := range mgr.boxes { - if m, err := p.GetMeta(ctx, zid); err == nil { - mgr.Enrich(ctx, m, i+1) - result = append(result, m) - } - } - return result, nil -} - -// FetchZids returns the set of all zettel identifer managed by the box. -func (mgr *Manager) FetchZids(ctx context.Context) (result id.Set, err error) { - mgr.mgrMx.RLock() - defer mgr.mgrMx.RUnlock() - if !mgr.started { - return nil, box.ErrStopped - } - for _, p := range mgr.boxes { - zids, err := p.FetchZids(ctx) - if err != nil { - return nil, err - } - if result == nil { - result = zids - } else if len(result) <= len(zids) { - for zid := range result { - zids[zid] = true - } - result = zids - } else { - for zid := range zids { - result[zid] = true - } - } - } - return result, nil -} - -// SelectMeta returns all zettel meta data that match the selection -// criteria. The result is ordered by descending zettel id. -func (mgr *Manager) SelectMeta(ctx context.Context, s *search.Search) ([]*meta.Meta, error) { - mgr.mgrMx.RLock() - defer mgr.mgrMx.RUnlock() - if !mgr.started { - return nil, box.ErrStopped - } - var result []*meta.Meta - match := s.CompileMatch(mgr) - for _, p := range mgr.boxes { - selected, err := p.SelectMeta(ctx, match) - if err != nil { - return nil, err - } - sort.Slice(selected, func(i, j int) bool { return selected[i].Zid > selected[j].Zid }) - if len(result) == 0 { - result = selected - } else { - result = box.MergeSorted(result, selected) - } - } - if s == nil { - return result, nil - } - return s.Sort(result), nil -} - -// CanUpdateZettel returns true, if box could possibly update the given zettel. -func (mgr *Manager) CanUpdateZettel(ctx context.Context, zettel domain.Zettel) bool { - mgr.mgrMx.RLock() - defer mgr.mgrMx.RUnlock() - return mgr.started && mgr.boxes[0].CanUpdateZettel(ctx, zettel) -} - -// UpdateZettel updates an existing zettel. -func (mgr *Manager) UpdateZettel(ctx context.Context, zettel domain.Zettel) error { - mgr.mgrMx.RLock() - defer mgr.mgrMx.RUnlock() - if !mgr.started { - return box.ErrStopped - } - // Remove all (computed) properties from metadata before storing the zettel. - zettel.Meta = zettel.Meta.Clone() - for _, p := range zettel.Meta.PairsRest(true) { - if mgr.propertyKeys[p.Key] { - zettel.Meta.Delete(p.Key) - } - } - return mgr.boxes[0].UpdateZettel(ctx, zettel) -} - -// AllowRenameZettel returns true, if box will not disallow renaming the zettel. -func (mgr *Manager) AllowRenameZettel(ctx context.Context, zid id.Zid) bool { - mgr.mgrMx.RLock() - defer mgr.mgrMx.RUnlock() - if !mgr.started { - return false - } - for _, p := range mgr.boxes { - if !p.AllowRenameZettel(ctx, zid) { - return false - } - } - return true -} - -// RenameZettel changes the current zid to a new zid. -func (mgr *Manager) RenameZettel(ctx context.Context, curZid, newZid id.Zid) error { - mgr.mgrMx.RLock() - defer mgr.mgrMx.RUnlock() - if !mgr.started { - return box.ErrStopped - } - for i, p := range mgr.boxes { - err := p.RenameZettel(ctx, curZid, newZid) - if err != nil && !errors.Is(err, box.ErrNotFound) { - for j := 0; j < i; j++ { - mgr.boxes[j].RenameZettel(ctx, newZid, curZid) - } - return err - } - } - return nil -} - -// CanDeleteZettel returns true, if box could possibly delete the given zettel. -func (mgr *Manager) CanDeleteZettel(ctx context.Context, zid id.Zid) bool { - mgr.mgrMx.RLock() - defer mgr.mgrMx.RUnlock() - if !mgr.started { - return false - } - for _, p := range mgr.boxes { - if p.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.mgrMx.RLock() - defer mgr.mgrMx.RUnlock() - if !mgr.started { - return box.ErrStopped - } - for _, p := range mgr.boxes { - err := p.DeleteZettel(ctx, zid) - if err == nil { - return nil - } - if !errors.Is(err, box.ErrNotFound) && !errors.Is(err, box.ErrReadOnly) { - return err - } - } - return box.ErrNotFound -} DELETED box/manager/collect.go Index: box/manager/collect.go ================================================================== --- box/manager/collect.go +++ box/manager/collect.go @@ -1,82 +0,0 @@ -//----------------------------------------------------------------------------- -// Copyright (c) 2021 Detlef Stern -// -// This file is part of zettelstore. -// -// Zettelstore is licensed under the latest version of the EUPL (European Union -// Public License). Please see file LICENSE.txt for your rights and obligations -// under this license. -//----------------------------------------------------------------------------- - -// Package manager coordinates the various boxes and indexes of a Zettelstore. -package manager - -import ( - "strings" - - "zettelstore.de/z/ast" - "zettelstore.de/z/box/manager/store" - "zettelstore.de/z/domain/id" - "zettelstore.de/z/strfun" -) - -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.WalkBlockSlice(data, zn.Ast) -} - -func collectInlineIndexData(ins ast.InlineSlice, data *collectData) { - ast.WalkInlineSlice(data, ins) -} - -func (data *collectData) Visit(node ast.Node) ast.Visitor { - switch n := node.(type) { - case *ast.VerbatimNode: - for _, line := range n.Lines { - data.addText(line) - } - case *ast.TextNode: - data.addText(n.Text) - case *ast.TagNode: - data.addText(n.Tag) - case *ast.LinkNode: - data.addRef(n.Ref) - case *ast.ImageNode: - data.addRef(n.Ref) - case *ast.LiteralNode: - data.addText(n.Text) - } - 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[zid] = true - } -} DELETED box/manager/enrich.go Index: box/manager/enrich.go ================================================================== --- box/manager/enrich.go +++ box/manager/enrich.go @@ -1,52 +0,0 @@ -//----------------------------------------------------------------------------- -// Copyright (c) 2021 Detlef Stern -// -// This file is part of zettelstore. -// -// Zettelstore is licensed under the latest version of the EUPL (European Union -// Public License). Please see file LICENSE.txt for your rights and obligations -// under this license. -//----------------------------------------------------------------------------- - -// Package manager coordinates the various boxes and indexes of a Zettelstore. -package manager - -import ( - "context" - "strconv" - - "zettelstore.de/z/box" - "zettelstore.de/z/domain/meta" -) - -// Enrich computes additional properties and updates the given metadata. -func (mgr *Manager) Enrich(ctx context.Context, m *meta.Meta, boxNumber int) { - if box.DoNotEnrich(ctx) { - // Enrich is called indirectly via indexer or enrichment is not requested - // because of other reasons -> ignore this call, do not update meta data - return - } - m.Set(meta.KeyBoxNumber, strconv.Itoa(boxNumber)) - computePublished(m) - mgr.idxStore.Enrich(ctx, m) -} - -func computePublished(m *meta.Meta) { - if _, ok := m.Get(meta.KeyPublished); ok { - return - } - if modified, ok := m.Get(meta.KeyModified); ok { - if _, ok = meta.TimeValue(modified); ok { - m.Set(meta.KeyPublished, modified) - return - } - } - zid := m.Zid.String() - if _, ok := meta.TimeValue(zid); ok { - m.Set(meta.KeyPublished, zid) - return - } - - // Neither the zettel was modified nor the zettel identifer contains a valid - // timestamp. In this case do not set the "published" property. -} DELETED box/manager/indexer.go Index: box/manager/indexer.go ================================================================== --- box/manager/indexer.go +++ box/manager/indexer.go @@ -1,227 +0,0 @@ -//----------------------------------------------------------------------------- -// Copyright (c) 2021 Detlef Stern -// -// This file is part of zettelstore. -// -// Zettelstore is licensed under the latest version of the EUPL (European Union -// Public License). Please see file LICENSE.txt for your rights and obligations -// under this license. -//----------------------------------------------------------------------------- - -// Package manager coordinates the various boxes and indexes of a Zettelstore. -package manager - -import ( - "context" - "net/url" - "time" - - "zettelstore.de/z/box" - "zettelstore.de/z/box/manager/store" - "zettelstore.de/z/domain" - "zettelstore.de/z/domain/id" - "zettelstore.de/z/domain/meta" - "zettelstore.de/z/kernel" - "zettelstore.de/z/parser" - "zettelstore.de/z/strfun" -) - -// 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 { - return mgr.idxStore.SearchEqual(word) -} - -// SearchPrefix returns all zettel that have a word with the given prefix. -// The prefix must be normalized through Unicode NKFD, trimmed and not empty. -func (mgr *Manager) SearchPrefix(prefix string) id.Set { - return mgr.idxStore.SearchPrefix(prefix) -} - -// SearchSuffix returns all zettel that have a word with the given suffix. -// The suffix must be normalized through Unicode NKFD, trimmed and not empty. -func (mgr *Manager) SearchSuffix(suffix string) id.Set { - return mgr.idxStore.SearchSuffix(suffix) -} - -// SearchContains returns all zettel that contains the given string. -// The string must be normalized through Unicode NKFD, trimmed and not empty. -func (mgr *Manager) SearchContains(s string) id.Set { - return mgr.idxStore.SearchContains(s) -} - -// idxIndexer runs in the background and updates the index data structures. -// This is the main service of the idxIndexer. -func (mgr *Manager) idxIndexer() { - // Something may panic. Ensure a running indexer. - defer func() { - if r := recover(); r != nil { - kernel.Main.LogRecover("Indexer", r) - 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 roomNum uint64 - var start time.Time - for { - switch action, zid, arRoomNum := mgr.idxAr.Dequeue(); action { - case arNothing: - return - case arReload: - roomNum = 0 - zids, err := mgr.FetchZids(ctx) - if err == nil { - start = time.Now() - if rno := mgr.idxAr.Reload(zids); rno > 0 { - roomNum = rno - } - mgr.idxMx.Lock() - mgr.idxLastReload = time.Now() - mgr.idxSinceReload = 0 - mgr.idxMx.Unlock() - } - case arUpdate: - zettel, err := mgr.GetZettel(ctx, zid) - if err != nil { - // TODO: on some errors put the zid into a "try later" set - continue - } - mgr.idxMx.Lock() - if arRoomNum == roomNum { - mgr.idxDurReload = time.Since(start) - } - mgr.idxSinceReload++ - mgr.idxMx.Unlock() - mgr.idxUpdateZettel(ctx, zettel) - case arDelete: - if _, err := mgr.GetMeta(ctx, zid); err == nil { - // Zettel was not deleted. This might occur, if zettel was - // deleted in secondary dirbox, but is still present in - // first dirbox (or vice versa). Re-index zettel in case - // a hidden zettel was recovered - mgr.idxAr.Enqueue(zid, arUpdate) - } - mgr.idxMx.Lock() - mgr.idxSinceReload++ - mgr.idxMx.Unlock() - mgr.idxDeleteZettel(zid) - } - } -} - -func (mgr *Manager) idxSleepService(timer *time.Timer, timerDuration time.Duration) bool { - 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 domain.Zettel) { - m := zettel.Meta - if m.GetBool(meta.KeyNoIndex) { - // Zettel maybe in index - toCheck := mgr.idxStore.DeleteZettel(ctx, m.Zid) - mgr.idxCheckZettel(toCheck) - return - } - - var cData collectData - cData.initialize() - collectZettelIndexData(parser.ParseZettel(zettel, "", mgr.rtConfig), &cData) - zi := store.NewZettelIndex(m.Zid) - mgr.idxCollectFromMeta(ctx, m, zi, &cData) - mgr.idxProcessData(ctx, zi, &cData) - toCheck := mgr.idxStore.UpdateReferences(ctx, zi) - mgr.idxCheckZettel(toCheck) -} - -func (mgr *Manager) idxCollectFromMeta(ctx context.Context, m *meta.Meta, zi *store.ZettelIndex, cData *collectData) { - for _, pair := range m.Pairs(false) { - descr := meta.GetDescription(pair.Key) - if descr.IsComputed() { - 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: - collectInlineIndexData(parser.ParseMetadata(pair.Value), cData) - case meta.TypeURL: - if _, err := url.Parse(pair.Value); err == nil { - cData.urls.Add(pair.Value) - } - default: - for _, word := range strfun.NormalizeWords(pair.Value) { - cData.words.Add(word) - } - } - } -} - -func (mgr *Manager) idxProcessData(ctx context.Context, zi *store.ZettelIndex, cData *collectData) { - for ref := range cData.refs { - if _, err := mgr.GetMeta(ctx, ref); err == nil { - 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 _, err := mgr.GetMeta(ctx, zid); err != nil { - zi.AddDeadRef(zid) - return - } - if inverseKey == "" { - zi.AddBackRef(zid) - return - } - zi.AddMetaRef(inverseKey, zid) -} - -func (mgr *Manager) idxDeleteZettel(zid id.Zid) { - toCheck := mgr.idxStore.DeleteZettel(context.Background(), zid) - mgr.idxCheckZettel(toCheck) -} - -func (mgr *Manager) idxCheckZettel(s id.Set) { - for zid := range s { - mgr.idxAr.Enqueue(zid, arUpdate) - } -} DELETED box/manager/manager.go Index: box/manager/manager.go ================================================================== --- box/manager/manager.go +++ box/manager/manager.go @@ -1,314 +0,0 @@ -//----------------------------------------------------------------------------- -// Copyright (c) 2021 Detlef Stern -// -// This file is part of zettelstore. -// -// Zettelstore is licensed under the latest version of the EUPL (European Union -// Public License). Please see file LICENSE.txt for your rights and obligations -// under this license. -//----------------------------------------------------------------------------- - -// Package manager coordinates the various boxes and indexes of a Zettelstore. -package manager - -import ( - "context" - "io" - "log" - "net/url" - "sort" - "sync" - "time" - - "zettelstore.de/z/auth" - "zettelstore.de/z/box" - "zettelstore.de/z/box/manager/memstore" - "zettelstore.de/z/box/manager/store" - "zettelstore.de/z/config" - "zettelstore.de/z/domain/id" - "zettelstore.de/z/domain/meta" - "zettelstore.de/z/kernel" -) - -// 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 { - log.Fatalf("Box with scheme %q already registered", scheme) - } - registry[scheme] = create -} - -// GetSchemes returns all registered scheme, ordered by scheme string. -func GetSchemes() []string { - result := make([]string, 0, len(registry)) - for scheme := range registry { - result = append(result, scheme) - } - sort.Strings(result) - return result -} - -// Manager is a coordinating box. -type Manager struct { - mgrMx sync.RWMutex - started bool - rtConfig config.Config - boxes []box.ManagedBox - observers []box.UpdateFunc - mxObserver sync.RWMutex - done chan struct{} - infos chan box.UpdateInfo - propertyKeys map[string]bool // Set of property key names - - // Indexer data - idxStore store.Store - idxAr *anterooms - idxReady chan struct{} // Signal a non-empty anteroom to background task - - // Indexer stats data - idxMx sync.RWMutex - idxLastReload time.Time - idxDurReload time.Duration - idxSinceReload uint64 -} - -// New creates a new managing box. -func New(boxURIs []*url.URL, authManager auth.BaseManager, rtConfig config.Config) (*Manager, error) { - propertyKeys := make(map[string]bool) - for _, kd := range meta.GetSortedKeyDescriptions() { - if kd.IsProperty() { - propertyKeys[kd.Name] = true - } - } - mgr := &Manager{ - rtConfig: rtConfig, - infos: make(chan box.UpdateInfo, len(boxURIs)*10), - propertyKeys: propertyKeys, - - idxStore: memstore.New(), - idxAr: newAnterooms(10), - idxReady: make(chan struct{}, 1), - } - cdata := ConnectData{Number: 1, Config: rtConfig, Enricher: mgr, Notify: mgr.infos} - boxes := make([]box.ManagedBox, 0, len(boxURIs)+2) - for _, uri := range boxURIs { - 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 -} - -// 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) notifyObserver(ci *box.UpdateInfo) { - mgr.mxObserver.RLock() - observers := mgr.observers - mgr.mxObserver.RUnlock() - for _, ob := range observers { - ob(*ci) - } -} - -func (mgr *Manager) notifier() { - // The call to notify may panic. Ensure a running notifier. - defer func() { - if r := recover(); r != nil { - kernel.Main.LogRecover("Notifier", r) - go mgr.notifier() - } - }() - - for { - select { - case ci, ok := <-mgr.infos: - if ok { - mgr.idxEnqueue(ci.Reason, ci.Zid) - if ci.Box == nil { - ci.Box = mgr - } - mgr.notifyObserver(&ci) - } - case <-mgr.done: - return - } - } -} - -func (mgr *Manager) idxEnqueue(reason box.UpdateReason, zid id.Zid) { - switch reason { - case box.OnReload: - mgr.idxAr.Reset() - case box.OnUpdate: - mgr.idxAr.Enqueue(zid, arUpdate) - case box.OnDelete: - mgr.idxAr.Enqueue(zid, arDelete) - default: - return - } - select { - case mgr.idxReady <- struct{}{}: - default: - } -} - -// Start the box. Now all other functions of the box are allowed. -// Starting an already started box is not allowed. -func (mgr *Manager) Start(ctx context.Context) error { - mgr.mgrMx.Lock() - if mgr.started { - mgr.mgrMx.Unlock() - return box.ErrStarted - } - 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 - } - for j := i + 1; j < len(mgr.boxes); j++ { - if ssj, ok := mgr.boxes[j].(box.StartStopper); ok { - ssj.Stop(ctx) - } - } - mgr.mgrMx.Unlock() - return err - } - mgr.idxAr.Reset() // Ensure an initial index run - mgr.done = make(chan struct{}) - go mgr.notifier() - go mgr.idxIndexer() - - // mgr.startIndexer(mgr) - mgr.started = true - mgr.mgrMx.Unlock() - mgr.infos <- box.UpdateInfo{Reason: box.OnReload, Zid: id.Invalid} - return nil -} - -// Stop the started box. Now only the Start() function is allowed. -func (mgr *Manager) Stop(ctx context.Context) error { - mgr.mgrMx.Lock() - defer mgr.mgrMx.Unlock() - if !mgr.started { - return box.ErrStopped - } - close(mgr.done) - var err error - for _, p := range mgr.boxes { - if ss, ok := p.(box.StartStopper); ok { - if err1 := ss.Stop(ctx); err1 != nil && err == nil { - err = err1 - } - } - } - mgr.started = false - return err -} - -// ReadStats populates st with box statistics. -func (mgr *Manager) ReadStats(st *box.Stats) { - mgr.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/memstore/memstore.go Index: box/manager/memstore/memstore.go ================================================================== --- box/manager/memstore/memstore.go +++ box/manager/memstore/memstore.go @@ -1,580 +0,0 @@ -//----------------------------------------------------------------------------- -// Copyright (c) 2021 Detlef Stern -// -// This file is part of zettelstore. -// -// Zettelstore is licensed under the latest version of the EUPL (European Union -// Public License). Please see file LICENSE.txt for your rights and obligations -// under this license. -//----------------------------------------------------------------------------- - -// Package memstore stored the index in main memory. -package memstore - -import ( - "context" - "fmt" - "io" - "sort" - "strings" - "sync" - - "zettelstore.de/z/box/manager/store" - "zettelstore.de/z/domain/id" - "zettelstore.de/z/domain/meta" -) - -type metaRefs struct { - forward id.Slice - backward id.Slice -} - -type zettelIndex struct { - dead id.Slice - forward id.Slice - backward id.Slice - meta map[string]metaRefs - words []string - urls []string -} - -func (zi *zettelIndex) isEmpty() bool { - if len(zi.forward) > 0 || len(zi.backward) > 0 || len(zi.dead) > 0 || len(zi.words) > 0 { - return false - } - return zi.meta == nil || len(zi.meta) == 0 -} - -type stringRefs map[string]id.Slice - -type memStore struct { - mx sync.RWMutex - idx map[id.Zid]*zettelIndex - dead map[id.Zid]id.Slice // map dead refs where they occur - words stringRefs - urls stringRefs - - // Stats - updates uint64 -} - -// New returns a new memory-based index store. -func New() store.Store { - return &memStore{ - idx: make(map[id.Zid]*zettelIndex), - dead: make(map[id.Zid]id.Slice), - words: make(stringRefs), - urls: make(stringRefs), - } -} - -func (ms *memStore) Enrich(ctx context.Context, m *meta.Meta) { - if ms.doEnrich(ctx, m) { - ms.mx.Lock() - ms.updates++ - ms.mx.Unlock() - } -} - -func (ms *memStore) doEnrich(ctx context.Context, m *meta.Meta) bool { - ms.mx.RLock() - defer ms.mx.RUnlock() - zi, ok := ms.idx[m.Zid] - if !ok { - return false - } - var updated bool - if len(zi.dead) > 0 { - m.Set(meta.KeyDead, zi.dead.String()) - updated = true - } - back := removeOtherMetaRefs(m, zi.backward.Copy()) - if len(zi.backward) > 0 { - m.Set(meta.KeyBackward, zi.backward.String()) - updated = true - } - if len(zi.forward) > 0 { - m.Set(meta.KeyForward, zi.forward.String()) - back = remRefs(back, zi.forward) - updated = true - } - if len(zi.meta) > 0 { - for k, refs := range zi.meta { - if len(refs.backward) > 0 { - m.Set(k, refs.backward.String()) - back = remRefs(back, refs.backward) - updated = true - } - } - } - if len(back) > 0 { - m.Set(meta.KeyBack, back.String()) - updated = true - } - return updated -} - -// SearchEqual returns all zettel that contains the given exact word. -// The word must be normalized through Unicode NKFD, trimmed and not empty. -func (ms *memStore) SearchEqual(word string) id.Set { - ms.mx.RLock() - defer ms.mx.RUnlock() - result := id.NewSet() - if refs, ok := ms.words[word]; ok { - result.AddSlice(refs) - } - if refs, ok := ms.urls[word]; ok { - result.AddSlice(refs) - } - zid, err := id.Parse(word) - if err != nil { - return result - } - zi, ok := ms.idx[zid] - if !ok { - return result - } - - addBackwardZids(result, zid, zi) - return result -} - -// SearchPrefix returns all zettel that have a word with the given prefix. -// The prefix must be normalized through Unicode NKFD, trimmed and not empty. -func (ms *memStore) SearchPrefix(prefix string) id.Set { - ms.mx.RLock() - defer ms.mx.RUnlock() - result := ms.selectWithPred(prefix, strings.HasPrefix) - l := len(prefix) - if l > 14 { - return result - } - minZid, err := id.Parse(prefix + "00000000000000"[:14-l]) - if err != nil { - return result - } - maxZid, err := id.Parse(prefix + "99999999999999"[:14-l]) - if err != nil { - return result - } - for zid, zi := range ms.idx { - if minZid <= zid && zid <= maxZid { - addBackwardZids(result, zid, zi) - } - } - return result -} - -// SearchSuffix returns all zettel that have a word with the given suffix. -// The suffix must be normalized through Unicode NKFD, trimmed and not empty. -func (ms *memStore) SearchSuffix(suffix string) id.Set { - ms.mx.RLock() - defer ms.mx.RUnlock() - result := ms.selectWithPred(suffix, strings.HasSuffix) - l := len(suffix) - if l > 14 { - return result - } - val, err := id.ParseUint(suffix) - if err != nil { - return result - } - modulo := uint64(1) - for i := 0; i < l; i++ { - modulo *= 10 - } - for zid, zi := range ms.idx { - if uint64(zid)%modulo == val { - addBackwardZids(result, zid, zi) - } - } - return result -} - -// SearchContains returns all zettel that contains the given string. -// The string must be normalized through Unicode NKFD, trimmed and not empty. -func (ms *memStore) SearchContains(s string) id.Set { - ms.mx.RLock() - defer ms.mx.RUnlock() - result := ms.selectWithPred(s, strings.Contains) - if len(s) > 14 { - return result - } - if _, err := id.ParseUint(s); err != nil { - return result - } - for zid, zi := range ms.idx { - if strings.Contains(zid.String(), s) { - addBackwardZids(result, zid, zi) - } - } - return result -} - -func (ms *memStore) selectWithPred(s string, pred func(string, string) bool) id.Set { - // Must only be called if ms.mx is read-locked! - result := id.NewSet() - for word, refs := range ms.words { - if !pred(word, s) { - continue - } - result.AddSlice(refs) - } - for u, refs := range ms.urls { - if !pred(u, s) { - continue - } - result.AddSlice(refs) - } - return result -} - -func addBackwardZids(result id.Set, zid id.Zid, zi *zettelIndex) { - // Must only be called if ms.mx is read-locked! - result[zid] = true - result.AddSlice(zi.backward) - for _, mref := range zi.meta { - result.AddSlice(mref.backward) - } -} - -func removeOtherMetaRefs(m *meta.Meta, back id.Slice) id.Slice { - for _, p := range m.PairsRest(false) { - switch meta.Type(p.Key) { - case meta.TypeID: - if zid, err := id.Parse(p.Value); err == nil { - back = remRef(back, zid) - } - case meta.TypeIDSet: - for _, val := range meta.ListFromValue(p.Value) { - if zid, err := id.Parse(val); err == nil { - back = remRef(back, zid) - } - } - } - } - return back -} - -func (ms *memStore) UpdateReferences(ctx context.Context, zidx *store.ZettelIndex) id.Set { - ms.mx.Lock() - defer ms.mx.Unlock() - zi, ziExist := ms.idx[zidx.Zid] - if !ziExist || zi == nil { - zi = &zettelIndex{} - ziExist = false - } - - // Is this zettel an old dead reference mentioned in other zettel? - var toCheck id.Set - if refs, ok := ms.dead[zidx.Zid]; ok { - // These must be checked later again - toCheck = id.NewSet(refs...) - delete(ms.dead, zidx.Zid) - } - - ms.updateDeadReferences(zidx, zi) - ms.updateForwardBackwardReferences(zidx, zi) - ms.updateMetadataReferences(zidx, zi) - zi.words = updateWordSet(zidx.Zid, ms.words, zi.words, zidx.GetWords()) - zi.urls = updateWordSet(zidx.Zid, ms.urls, zi.urls, zidx.GetUrls()) - - // Check if zi must be inserted into ms.idx - if !ziExist && !zi.isEmpty() { - ms.idx[zidx.Zid] = zi - } - - return toCheck -} - -func (ms *memStore) updateDeadReferences(zidx *store.ZettelIndex, zi *zettelIndex) { - // Must only be called if ms.mx is write-locked! - drefs := zidx.GetDeadRefs() - newRefs, remRefs := refsDiff(drefs, zi.dead) - zi.dead = drefs - for _, ref := range remRefs { - ms.dead[ref] = remRef(ms.dead[ref], zidx.Zid) - } - for _, ref := range newRefs { - ms.dead[ref] = addRef(ms.dead[ref], zidx.Zid) - } -} - -func (ms *memStore) updateForwardBackwardReferences(zidx *store.ZettelIndex, zi *zettelIndex) { - // Must only be called if ms.mx is write-locked! - brefs := zidx.GetBackRefs() - newRefs, remRefs := refsDiff(brefs, zi.forward) - zi.forward = brefs - for _, ref := range remRefs { - bzi := ms.getEntry(ref) - bzi.backward = remRef(bzi.backward, zidx.Zid) - } - for _, ref := range newRefs { - bzi := ms.getEntry(ref) - bzi.backward = addRef(bzi.backward, zidx.Zid) - } -} - -func (ms *memStore) updateMetadataReferences(zidx *store.ZettelIndex, zi *zettelIndex) { - // Must only be called if ms.mx is write-locked! - metarefs := zidx.GetMetaRefs() - for key, mr := range zi.meta { - if _, ok := metarefs[key]; ok { - continue - } - ms.removeInverseMeta(zidx.Zid, key, mr.forward) - } - if zi.meta == nil { - zi.meta = make(map[string]metaRefs) - } - for key, mrefs := range metarefs { - mr := zi.meta[key] - newRefs, remRefs := refsDiff(mrefs, mr.forward) - mr.forward = mrefs - zi.meta[key] = mr - - for _, ref := range newRefs { - bzi := ms.getEntry(ref) - if bzi.meta == nil { - bzi.meta = make(map[string]metaRefs) - } - bmr := bzi.meta[key] - bmr.backward = addRef(bmr.backward, zidx.Zid) - bzi.meta[key] = bmr - } - ms.removeInverseMeta(zidx.Zid, key, remRefs) - } -} - -func updateWordSet(zid id.Zid, srefs stringRefs, prev []string, next store.WordSet) []string { - // Must only be called if ms.mx is write-locked! - newWords, removeWords := next.Diff(prev) - for _, word := range newWords { - if refs, ok := srefs[word]; ok { - srefs[word] = addRef(refs, zid) - continue - } - srefs[word] = id.Slice{zid} - } - for _, word := range removeWords { - refs, ok := srefs[word] - if !ok { - continue - } - refs2 := remRef(refs, zid) - if len(refs2) == 0 { - delete(srefs, word) - continue - } - srefs[word] = refs2 - } - return next.Words() -} - -func (ms *memStore) getEntry(zid id.Zid) *zettelIndex { - // Must only be called if ms.mx is write-locked! - if zi, ok := ms.idx[zid]; ok { - return zi - } - zi := &zettelIndex{} - ms.idx[zid] = zi - return zi -} - -func (ms *memStore) DeleteZettel(ctx context.Context, zid id.Zid) id.Set { - ms.mx.Lock() - defer ms.mx.Unlock() - - zi, ok := ms.idx[zid] - if !ok { - return nil - } - - ms.deleteDeadSources(zid, zi) - toCheck := ms.deleteForwardBackward(zid, zi) - if len(zi.meta) > 0 { - for key, mrefs := range zi.meta { - ms.removeInverseMeta(zid, key, mrefs.forward) - } - } - ms.deleteWords(zid, zi.words) - delete(ms.idx, zid) - return toCheck -} - -func (ms *memStore) deleteDeadSources(zid id.Zid, zi *zettelIndex) { - // Must only be called if ms.mx is write-locked! - for _, ref := range zi.dead { - if drefs, ok := ms.dead[ref]; ok { - drefs = remRef(drefs, zid) - if len(drefs) > 0 { - ms.dead[ref] = drefs - } else { - delete(ms.dead, ref) - } - } - } -} - -func (ms *memStore) deleteForwardBackward(zid id.Zid, zi *zettelIndex) id.Set { - // Must only be called if ms.mx is write-locked! - var toCheck id.Set - for _, ref := range zi.forward { - if fzi, ok := ms.idx[ref]; ok { - fzi.backward = remRef(fzi.backward, zid) - } - } - for _, ref := range zi.backward { - if bzi, ok := ms.idx[ref]; ok { - bzi.forward = remRef(bzi.forward, zid) - if toCheck == nil { - toCheck = id.NewSet() - } - toCheck[ref] = true - } - } - return toCheck -} - -func (ms *memStore) removeInverseMeta(zid id.Zid, key string, forward id.Slice) { - // Must only be called if ms.mx is write-locked! - for _, ref := range forward { - bzi, ok := ms.idx[ref] - if !ok || bzi.meta == nil { - continue - } - bmr, ok := bzi.meta[key] - if !ok { - continue - } - bmr.backward = remRef(bmr.backward, zid) - if len(bmr.backward) > 0 || len(bmr.forward) > 0 { - bzi.meta[key] = bmr - } else { - delete(bzi.meta, key) - if len(bzi.meta) == 0 { - bzi.meta = nil - } - } - } -} - -func (ms *memStore) deleteWords(zid id.Zid, words []string) { - // Must only be called if ms.mx is write-locked! - for _, word := range words { - refs, ok := ms.words[word] - if !ok { - continue - } - refs2 := remRef(refs, zid) - if len(refs2) == 0 { - delete(ms.words, word) - continue - } - ms.words[word] = refs2 - } -} - -func (ms *memStore) ReadStats(st *store.Stats) { - ms.mx.RLock() - st.Zettel = len(ms.idx) - st.Updates = ms.updates - st.Words = uint64(len(ms.words)) - st.Urls = uint64(len(ms.urls)) - ms.mx.RUnlock() -} - -func (ms *memStore) Dump(w io.Writer) { - ms.mx.RLock() - defer ms.mx.RUnlock() - - io.WriteString(w, "=== Dump\n") - ms.dumpIndex(w) - ms.dumpDead(w) - dumpStringRefs(w, "Words", "", "", ms.words) - dumpStringRefs(w, "URLs", "[[", "]]", ms.urls) -} - -func (ms *memStore) dumpIndex(w io.Writer) { - if len(ms.idx) == 0 { - return - } - io.WriteString(w, "==== Zettel Index\n") - zids := make(id.Slice, 0, len(ms.idx)) - for id := range ms.idx { - zids = append(zids, id) - } - zids.Sort() - for _, id := range zids { - fmt.Fprintln(w, "=====", id) - zi := ms.idx[id] - if len(zi.dead) > 0 { - fmt.Fprintln(w, "* Dead:", zi.dead) - } - dumpZids(w, "* Forward:", zi.forward) - dumpZids(w, "* Backward:", zi.backward) - for k, fb := range zi.meta { - fmt.Fprintln(w, "* Meta", k) - dumpZids(w, "** Forward:", fb.forward) - dumpZids(w, "** Backward:", fb.backward) - } - dumpStrings(w, "* Words", "", "", zi.words) - dumpStrings(w, "* URLs", "[[", "]]", zi.urls) - } -} - -func (ms *memStore) dumpDead(w io.Writer) { - if len(ms.dead) == 0 { - return - } - fmt.Fprintf(w, "==== Dead References\n") - zids := make(id.Slice, 0, len(ms.dead)) - for id := range ms.dead { - zids = append(zids, id) - } - zids.Sort() - for _, id := range zids { - fmt.Fprintln(w, ";", id) - fmt.Fprintln(w, ":", ms.dead[id]) - } -} - -func dumpZids(w io.Writer, prefix string, zids id.Slice) { - if len(zids) > 0 { - io.WriteString(w, prefix) - for _, zid := range zids { - io.WriteString(w, " ") - w.Write(zid.Bytes()) - } - fmt.Fprintln(w) - } -} - -func dumpStrings(w io.Writer, title, preString, postString string, slice []string) { - if len(slice) > 0 { - sl := make([]string, len(slice)) - copy(sl, slice) - sort.Strings(sl) - fmt.Fprintln(w, title) - for _, s := range sl { - fmt.Fprintf(w, "** %s%s%s\n", preString, s, postString) - } - } - -} - -func dumpStringRefs(w io.Writer, title, preString, postString string, srefs stringRefs) { - if len(srefs) == 0 { - return - } - fmt.Fprintln(w, "====", title) - slice := make([]string, 0, len(srefs)) - for s := range srefs { - slice = append(slice, s) - } - sort.Strings(slice) - for _, s := range slice { - fmt.Fprintf(w, "; %s%s%s\n", preString, s, postString) - fmt.Fprintln(w, ":", srefs[s]) - } -} DELETED box/manager/memstore/refs.go Index: box/manager/memstore/refs.go ================================================================== --- box/manager/memstore/refs.go +++ box/manager/memstore/refs.go @@ -1,101 +0,0 @@ -//----------------------------------------------------------------------------- -// Copyright (c) 2021 Detlef Stern -// -// This file is part of zettelstore. -// -// Zettelstore is licensed under the latest version of the EUPL (European Union -// Public License). Please see file LICENSE.txt for your rights and obligations -// under this license. -//----------------------------------------------------------------------------- - -// Package memstore stored the index in main memory. -package memstore - -import "zettelstore.de/z/domain/id" - -func refsDiff(refsN, refsO id.Slice) (newRefs, remRefs id.Slice) { - npos, opos := 0, 0 - for npos < len(refsN) && opos < len(refsO) { - rn, ro := refsN[npos], refsO[opos] - if rn == ro { - npos++ - opos++ - continue - } - if rn < ro { - newRefs = append(newRefs, rn) - npos++ - continue - } - remRefs = append(remRefs, ro) - opos++ - } - if npos < len(refsN) { - newRefs = append(newRefs, refsN[npos:]...) - } - if opos < len(refsO) { - remRefs = append(remRefs, refsO[opos:]...) - } - return newRefs, remRefs -} - -func addRef(refs id.Slice, ref id.Zid) id.Slice { - hi := len(refs) - for lo := 0; lo < hi; { - m := lo + (hi-lo)/2 - if r := refs[m]; r == ref { - return refs - } else if r < ref { - lo = m + 1 - } else { - hi = m - } - } - refs = append(refs, id.Invalid) - copy(refs[hi+1:], refs[hi:]) - refs[hi] = ref - return refs -} - -func remRefs(refs, rem id.Slice) id.Slice { - if len(refs) == 0 || len(rem) == 0 { - return refs - } - result := make(id.Slice, 0, len(refs)) - rpos, dpos := 0, 0 - for rpos < len(refs) && dpos < len(rem) { - rr, dr := refs[rpos], rem[dpos] - if rr < dr { - result = append(result, rr) - rpos++ - continue - } - if dr < rr { - dpos++ - continue - } - rpos++ - dpos++ - } - if rpos < len(refs) { - result = append(result, refs[rpos:]...) - } - return result -} - -func remRef(refs id.Slice, ref id.Zid) id.Slice { - hi := len(refs) - for lo := 0; lo < hi; { - m := lo + (hi-lo)/2 - if r := refs[m]; r == ref { - copy(refs[m:], refs[m+1:]) - refs = refs[:len(refs)-1] - return refs - } else if r < ref { - lo = m + 1 - } else { - hi = m - } - } - return refs -} DELETED box/manager/memstore/refs_test.go Index: box/manager/memstore/refs_test.go ================================================================== --- box/manager/memstore/refs_test.go +++ box/manager/memstore/refs_test.go @@ -1,138 +0,0 @@ -//----------------------------------------------------------------------------- -// Copyright (c) 2021 Detlef Stern -// -// This file is part of zettelstore. -// -// Zettelstore is licensed under the latest version of the EUPL (European Union -// Public License). Please see file LICENSE.txt for your rights and obligations -// under this license. -//----------------------------------------------------------------------------- - -// Package memstore stored the index in main memory. -package memstore - -import ( - "testing" - - "zettelstore.de/z/domain/id" -) - -func assertRefs(t *testing.T, i int, got, exp id.Slice) { - t.Helper() - if got == nil && exp != nil { - t.Errorf("%d: got nil, but expected %v", i, exp) - return - } - if got != nil && exp == nil { - t.Errorf("%d: expected nil, but got %v", i, got) - return - } - if len(got) != len(exp) { - t.Errorf("%d: expected len(%v)==%d, but got len(%v)==%d", i, exp, len(exp), got, len(got)) - return - } - for p, n := range exp { - if got := got[p]; got != id.Zid(n) { - t.Errorf("%d: pos %d: expected %d, but got %d", i, p, n, got) - } - } -} - -func TestRefsDiff(t *testing.T) { - t.Parallel() - testcases := []struct { - in1, in2 id.Slice - exp1, exp2 id.Slice - }{ - {nil, nil, nil, nil}, - {id.Slice{1}, nil, id.Slice{1}, nil}, - {nil, id.Slice{1}, nil, id.Slice{1}}, - {id.Slice{1}, id.Slice{1}, nil, nil}, - {id.Slice{1, 2}, id.Slice{1}, id.Slice{2}, nil}, - {id.Slice{1, 2}, id.Slice{1, 3}, id.Slice{2}, id.Slice{3}}, - {id.Slice{1, 4}, id.Slice{1, 3}, id.Slice{4}, id.Slice{3}}, - } - for i, tc := range testcases { - got1, got2 := refsDiff(tc.in1, tc.in2) - assertRefs(t, i, got1, tc.exp1) - assertRefs(t, i, got2, tc.exp2) - } -} - -func TestAddRef(t *testing.T) { - t.Parallel() - testcases := []struct { - ref id.Slice - zid uint - exp id.Slice - }{ - {nil, 5, id.Slice{5}}, - {id.Slice{1}, 5, id.Slice{1, 5}}, - {id.Slice{10}, 5, id.Slice{5, 10}}, - {id.Slice{5}, 5, id.Slice{5}}, - {id.Slice{1, 10}, 5, id.Slice{1, 5, 10}}, - {id.Slice{1, 5, 10}, 5, id.Slice{1, 5, 10}}, - } - for i, tc := range testcases { - got := addRef(tc.ref, id.Zid(tc.zid)) - assertRefs(t, i, got, tc.exp) - } -} - -func TestRemRefs(t *testing.T) { - t.Parallel() - testcases := []struct { - in1, in2 id.Slice - exp id.Slice - }{ - {nil, nil, nil}, - {nil, id.Slice{}, nil}, - {id.Slice{}, nil, id.Slice{}}, - {id.Slice{}, id.Slice{}, id.Slice{}}, - {id.Slice{1}, id.Slice{5}, id.Slice{1}}, - {id.Slice{10}, id.Slice{5}, id.Slice{10}}, - {id.Slice{1, 5}, id.Slice{5}, id.Slice{1}}, - {id.Slice{5, 10}, id.Slice{5}, id.Slice{10}}, - {id.Slice{1, 10}, id.Slice{5}, id.Slice{1, 10}}, - {id.Slice{1}, id.Slice{2, 5}, id.Slice{1}}, - {id.Slice{10}, id.Slice{2, 5}, id.Slice{10}}, - {id.Slice{1, 5}, id.Slice{2, 5}, id.Slice{1}}, - {id.Slice{5, 10}, id.Slice{2, 5}, id.Slice{10}}, - {id.Slice{1, 2, 5}, id.Slice{2, 5}, id.Slice{1}}, - {id.Slice{2, 5, 10}, id.Slice{2, 5}, id.Slice{10}}, - {id.Slice{1, 10}, id.Slice{2, 5}, id.Slice{1, 10}}, - {id.Slice{1}, id.Slice{5, 9}, id.Slice{1}}, - {id.Slice{10}, id.Slice{5, 9}, id.Slice{10}}, - {id.Slice{1, 5}, id.Slice{5, 9}, id.Slice{1}}, - {id.Slice{5, 10}, id.Slice{5, 9}, id.Slice{10}}, - {id.Slice{1, 5, 9}, id.Slice{5, 9}, id.Slice{1}}, - {id.Slice{5, 9, 10}, id.Slice{5, 9}, id.Slice{10}}, - {id.Slice{1, 10}, id.Slice{5, 9}, id.Slice{1, 10}}, - } - for i, tc := range testcases { - got := remRefs(tc.in1, tc.in2) - assertRefs(t, i, got, tc.exp) - } -} - -func TestRemRef(t *testing.T) { - t.Parallel() - testcases := []struct { - ref id.Slice - zid uint - exp id.Slice - }{ - {nil, 5, nil}, - {id.Slice{}, 5, id.Slice{}}, - {id.Slice{5}, 5, id.Slice{}}, - {id.Slice{1}, 5, id.Slice{1}}, - {id.Slice{10}, 5, id.Slice{10}}, - {id.Slice{1, 5}, 5, id.Slice{1}}, - {id.Slice{5, 10}, 5, id.Slice{10}}, - {id.Slice{1, 5, 10}, 5, id.Slice{1, 10}}, - } - for i, tc := range testcases { - got := remRef(tc.ref, id.Zid(tc.zid)) - assertRefs(t, i, got, tc.exp) - } -} DELETED box/manager/store/store.go Index: box/manager/store/store.go ================================================================== --- box/manager/store/store.go +++ box/manager/store/store.go @@ -1,59 +0,0 @@ -//----------------------------------------------------------------------------- -// Copyright (c) 2021 Detlef Stern -// -// This file is part of zettelstore. -// -// Zettelstore is licensed under the latest version of the EUPL (European Union -// Public License). Please see file LICENSE.txt for your rights and obligations -// under this license. -//----------------------------------------------------------------------------- - -// Package store contains general index data for storing a zettel index. -package store - -import ( - "context" - "io" - - "zettelstore.de/z/domain/id" - "zettelstore.de/z/domain/meta" - "zettelstore.de/z/search" -) - -// Stats records statistics about the store. -type Stats struct { - // Zettel is the number of zettel managed by the indexer. - 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 { - search.Searcher - - // Entrich metadata with data from store. - Enrich(ctx context.Context, m *meta.Meta) - - // UpdateReferences for a specific zettel. - // Returns set of zettel identifier that must also be checked for changes. - UpdateReferences(context.Context, *ZettelIndex) id.Set - - // 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,61 +0,0 @@ -//----------------------------------------------------------------------------- -// Copyright (c) 2021 Detlef Stern -// -// This file is part of zettelstore. -// -// Zettelstore is licensed under the latest version of the EUPL (European Union -// Public License). Please see file LICENSE.txt for your rights and obligations -// under this license. -//----------------------------------------------------------------------------- - -// Package store contains general index data for storing a zettel index. -package store - -// WordSet contains the set of all words, with the count of their occurrences. -type WordSet map[string]int - -// 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,78 +0,0 @@ -//----------------------------------------------------------------------------- -// Copyright (c) 2021 Detlef Stern -// -// This file is part of zettelstore. -// -// Zettelstore is licensed under the latest version of the EUPL (European Union -// Public License). Please see file LICENSE.txt for your rights and obligations -// under this license. -//----------------------------------------------------------------------------- - -// Package store contains general index data for storing a zettel index. -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,89 +0,0 @@ -//----------------------------------------------------------------------------- -// Copyright (c) 2021 Detlef Stern -// -// This file is part of zettelstore. -// -// Zettelstore is licensed under the latest version of the EUPL (European Union -// Public License). Please see file LICENSE.txt for your rights and obligations -// under this license. -//----------------------------------------------------------------------------- - -// Package store contains general index data for storing a zettel index. -package store - -import "zettelstore.de/z/domain/id" - -// ZettelIndex contains all index data of a zettel. -type ZettelIndex struct { - Zid id.Zid // zid of the indexed zettel - backrefs id.Set // set of back references - metarefs map[string]id.Set // references to inverse keys - deadrefs id.Set // set of dead references - words WordSet - urls WordSet -} - -// NewZettelIndex creates a new zettel index. -func NewZettelIndex(zid id.Zid) *ZettelIndex { - return &ZettelIndex{ - Zid: zid, - backrefs: id.NewSet(), - metarefs: make(map[string]id.Set), - deadrefs: id.NewSet(), - } -} - -// AddBackRef adds a reference to a zettel where the current zettel links to -// without any more information. -func (zi *ZettelIndex) AddBackRef(zid id.Zid) { - zi.backrefs[zid] = true -} - -// AddMetaRef adds a named reference to a zettel. On that zettel, the given -// metadata key should point back to the current zettel. -func (zi *ZettelIndex) AddMetaRef(key string, zid id.Zid) { - if zids, ok := zi.metarefs[key]; ok { - zids[zid] = true - return - } - zi.metarefs[key] = id.NewSet(zid) -} - -// AddDeadRef adds a dead reference to a zettel. -func (zi *ZettelIndex) AddDeadRef(zid id.Zid) { - zi.deadrefs[zid] = true -} - -// SetWords sets the words to the given value. -func (zi *ZettelIndex) SetWords(words WordSet) { zi.words = words } - -// SetUrls sets the words to the given value. -func (zi *ZettelIndex) SetUrls(urls WordSet) { zi.urls = urls } - -// GetDeadRefs returns all dead references as a sorted list. -func (zi *ZettelIndex) GetDeadRefs() id.Slice { - return zi.deadrefs.Sorted() -} - -// GetBackRefs returns all back references as a sorted list. -func (zi *ZettelIndex) GetBackRefs() id.Slice { - return zi.backrefs.Sorted() -} - -// GetMetaRefs returns all meta references as a map of strings to a sorted list of references -func (zi *ZettelIndex) GetMetaRefs() map[string]id.Slice { - if len(zi.metarefs) == 0 { - return nil - } - result := make(map[string]id.Slice, len(zi.metarefs)) - for key, refs := range zi.metarefs { - 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,200 +0,0 @@ -//----------------------------------------------------------------------------- -// 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 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/domain" - "zettelstore.de/z/domain/id" - "zettelstore.de/z/domain/meta" - "zettelstore.de/z/search" -) - -func init() { - manager.Register( - "mem", - func(u *url.URL, cdata *manager.ConnectData) (box.ManagedBox, error) { - return &memBox{u: u, cdata: *cdata}, nil - }) -} - -type memBox struct { - u *url.URL - cdata manager.ConnectData - zettel map[id.Zid]domain.Zettel - mx sync.RWMutex -} - -func (mp *memBox) notifyChanged(reason box.UpdateReason, zid id.Zid) { - if chci := mp.cdata.Notify; chci != nil { - chci <- box.UpdateInfo{Reason: reason, Zid: zid} - } -} - -func (mp *memBox) Location() string { - return mp.u.String() -} - -func (mp *memBox) Start(ctx context.Context) error { - mp.mx.Lock() - mp.zettel = make(map[id.Zid]domain.Zettel) - mp.mx.Unlock() - return nil -} - -func (mp *memBox) Stop(ctx context.Context) error { - mp.mx.Lock() - mp.zettel = nil - mp.mx.Unlock() - return nil -} - -func (mp *memBox) CanCreateZettel(ctx context.Context) bool { return true } - -func (mp *memBox) CreateZettel(ctx context.Context, zettel domain.Zettel) (id.Zid, error) { - mp.mx.Lock() - zid, err := box.GetNewZid(func(zid id.Zid) (bool, error) { - _, ok := mp.zettel[zid] - return !ok, nil - }) - if err != nil { - mp.mx.Unlock() - return id.Invalid, err - } - meta := zettel.Meta.Clone() - meta.Zid = zid - zettel.Meta = meta - mp.zettel[zid] = zettel - mp.mx.Unlock() - mp.notifyChanged(box.OnUpdate, zid) - return zid, nil -} - -func (mp *memBox) GetZettel(ctx context.Context, zid id.Zid) (domain.Zettel, error) { - mp.mx.RLock() - zettel, ok := mp.zettel[zid] - mp.mx.RUnlock() - if !ok { - return domain.Zettel{}, box.ErrNotFound - } - zettel.Meta = zettel.Meta.Clone() - return zettel, nil -} - -func (mp *memBox) GetMeta(ctx context.Context, zid id.Zid) (*meta.Meta, error) { - mp.mx.RLock() - zettel, ok := mp.zettel[zid] - mp.mx.RUnlock() - if !ok { - return nil, box.ErrNotFound - } - return zettel.Meta.Clone(), nil -} - -func (mp *memBox) FetchZids(ctx context.Context) (id.Set, error) { - mp.mx.RLock() - result := id.NewSetCap(len(mp.zettel)) - for zid := range mp.zettel { - result[zid] = true - } - mp.mx.RUnlock() - return result, nil -} - -func (mp *memBox) SelectMeta(ctx context.Context, match search.MetaMatchFunc) ([]*meta.Meta, error) { - result := make([]*meta.Meta, 0, len(mp.zettel)) - mp.mx.RLock() - for _, zettel := range mp.zettel { - m := zettel.Meta.Clone() - mp.cdata.Enricher.Enrich(ctx, m, mp.cdata.Number) - if match(m) { - result = append(result, m) - } - } - mp.mx.RUnlock() - return result, nil -} - -func (mp *memBox) CanUpdateZettel(ctx context.Context, zettel domain.Zettel) bool { - return true -} - -func (mp *memBox) UpdateZettel(ctx context.Context, zettel domain.Zettel) error { - mp.mx.Lock() - meta := zettel.Meta.Clone() - if !meta.Zid.IsValid() { - return &box.ErrInvalidID{Zid: meta.Zid} - } - zettel.Meta = meta - mp.zettel[meta.Zid] = zettel - mp.mx.Unlock() - mp.notifyChanged(box.OnUpdate, meta.Zid) - return nil -} - -func (mp *memBox) AllowRenameZettel(ctx context.Context, zid id.Zid) bool { return true } - -func (mp *memBox) RenameZettel(ctx context.Context, curZid, newZid id.Zid) error { - mp.mx.Lock() - zettel, ok := mp.zettel[curZid] - if !ok { - mp.mx.Unlock() - return box.ErrNotFound - } - - // Check that there is no zettel with newZid - if _, ok = mp.zettel[newZid]; ok { - mp.mx.Unlock() - return &box.ErrInvalidID{Zid: newZid} - } - - meta := zettel.Meta.Clone() - meta.Zid = newZid - zettel.Meta = meta - mp.zettel[newZid] = zettel - delete(mp.zettel, curZid) - mp.mx.Unlock() - mp.notifyChanged(box.OnDelete, curZid) - mp.notifyChanged(box.OnUpdate, newZid) - return nil -} - -func (mp *memBox) CanDeleteZettel(ctx context.Context, zid id.Zid) bool { - mp.mx.RLock() - _, ok := mp.zettel[zid] - mp.mx.RUnlock() - return ok -} - -func (mp *memBox) DeleteZettel(ctx context.Context, zid id.Zid) error { - mp.mx.Lock() - if _, ok := mp.zettel[zid]; !ok { - mp.mx.Unlock() - return box.ErrNotFound - } - delete(mp.zettel, zid) - mp.mx.Unlock() - mp.notifyChanged(box.OnDelete, zid) - return nil -} - -func (mp *memBox) ReadStats(st *box.ManagedBoxStats) { - st.ReadOnly = false - mp.mx.RLock() - st.Zettel = len(mp.zettel) - mp.mx.RUnlock() -} DELETED box/merge.go Index: box/merge.go ================================================================== --- box/merge.go +++ box/merge.go @@ -1,46 +0,0 @@ -//----------------------------------------------------------------------------- -// Copyright (c) 2020-2021 Detlef Stern -// -// This file is part of zettelstore. -// -// Zettelstore is licensed under the latest version of the EUPL (European Union -// Public License). Please see file LICENSE.txt for your rights and obligations -// under this license. -//----------------------------------------------------------------------------- - -// Package box provides a generic interface to zettel boxes. -package box - -import "zettelstore.de/z/domain/meta" - -// MergeSorted returns a merged sequence of metadata, sorted by Zid. -// The lists first and second must be sorted descending by Zid. -func MergeSorted(first, second []*meta.Meta) []*meta.Meta { - lenFirst := len(first) - lenSecond := len(second) - result := make([]*meta.Meta, 0, lenFirst+lenSecond) - iFirst := 0 - iSecond := 0 - for iFirst < lenFirst && iSecond < lenSecond { - zidFirst := first[iFirst].Zid - zidSecond := second[iSecond].Zid - if zidFirst > zidSecond { - result = append(result, first[iFirst]) - iFirst++ - } else if zidFirst < zidSecond { - result = append(result, second[iSecond]) - iSecond++ - } else { // zidFirst == zidSecond - result = append(result, first[iFirst]) - iFirst++ - iSecond++ - } - } - if iFirst < lenFirst { - result = append(result, first[iFirst:]...) - } else { - result = append(result, second[iSecond:]...) - } - - return result -} DELETED client/client.go Index: client/client.go ================================================================== --- client/client.go +++ client/client.go @@ -1,443 +0,0 @@ -//----------------------------------------------------------------------------- -// Copyright (c) 2021 Detlef Stern -// -// This file is part of zettelstore. -// -// Zettelstore is licensed under the latest version of the EUPL (European Union -// Public License). Please see file LICENSE.txt for your rights and obligations -// under this license. -//----------------------------------------------------------------------------- - -// Package client provides a client for accessing the Zettelstore via its API. -package client - -import ( - "bytes" - "context" - "encoding/json" - "errors" - "io" - "net/http" - "net/url" - "strconv" - "strings" - "time" - - "zettelstore.de/z/api" - "zettelstore.de/z/domain/id" -) - -// Client contains all data to execute requests. -type Client struct { - baseURL string - username string - password string - token string - tokenType string - expires time.Time -} - -// NewClient create a new client. -func NewClient(baseURL string) *Client { - if !strings.HasSuffix(baseURL, "/") { - baseURL += "/" - } - c := Client{baseURL: baseURL} - return &c -} - -func (c *Client) newURLBuilder(key byte) *api.URLBuilder { - return api.NewURLBuilder(c.baseURL, key) -} -func (c *Client) newRequest(ctx context.Context, method string, ub *api.URLBuilder, body io.Reader) (*http.Request, error) { - return http.NewRequestWithContext(ctx, method, ub.String(), body) -} - -func (c *Client) executeRequest(req *http.Request) (*http.Response, error) { - if c.token != "" { - req.Header.Add("Authorization", c.tokenType+" "+c.token) - } - client := http.Client{} - resp, err := client.Do(req) - if err != nil { - if resp != nil && resp.Body != nil { - resp.Body.Close() - } - return nil, err - } - return resp, err -} - -func (c *Client) buildAndExecuteRequest( - ctx context.Context, method string, ub *api.URLBuilder, body io.Reader, h http.Header) (*http.Response, error) { - req, err := c.newRequest(ctx, method, ub, body) - if err != nil { - return nil, err - } - err = c.updateToken(ctx) - if err != nil { - return nil, err - } - for key, val := range h { - req.Header[key] = append(req.Header[key], val...) - } - return c.executeRequest(req) -} - -// SetAuth sets authentication data. -func (c *Client) SetAuth(username, password string) { - c.username = username - c.password = password - c.token = "" - c.tokenType = "" - c.expires = time.Time{} -} - -func (c *Client) executeAuthRequest(req *http.Request) error { - resp, err := c.executeRequest(req) - if err != nil { - return err - } - defer resp.Body.Close() - if resp.StatusCode != http.StatusOK { - return errors.New(resp.Status) - } - dec := json.NewDecoder(resp.Body) - var tinfo api.AuthJSON - err = dec.Decode(&tinfo) - if err != nil { - return err - } - c.token = tinfo.Token - c.tokenType = tinfo.Type - c.expires = time.Now().Add(time.Duration(tinfo.Expires*10/9) * time.Second) - return nil -} - -func (c *Client) updateToken(ctx context.Context) error { - if c.username == "" { - return nil - } - if time.Now().After(c.expires) { - return c.Authenticate(ctx) - } - return c.RefreshToken(ctx) -} - -// Authenticate sets a new token by sending user name and password. -func (c *Client) Authenticate(ctx context.Context) error { - authData := url.Values{"username": {c.username}, "password": {c.password}} - req, err := c.newRequest(ctx, http.MethodPost, c.newURLBuilder('v'), strings.NewReader(authData.Encode())) - if err != nil { - return err - } - req.Header.Set("Content-Type", "application/x-www-form-urlencoded") - return c.executeAuthRequest(req) -} - -// RefreshToken updates the access token -func (c *Client) RefreshToken(ctx context.Context) error { - req, err := c.newRequest(ctx, http.MethodPut, c.newURLBuilder('v'), nil) - if err != nil { - return err - } - return c.executeAuthRequest(req) -} - -// CreateZettel creates a new zettel and returns its URL. -func (c *Client) CreateZettel(ctx context.Context, data *api.ZettelDataJSON) (id.Zid, error) { - var buf bytes.Buffer - if err := encodeZettelData(&buf, data); err != nil { - return id.Invalid, err - } - ub := c.jsonZettelURLBuilder(nil) - resp, err := c.buildAndExecuteRequest(ctx, http.MethodPost, ub, &buf, nil) - if err != nil { - return id.Invalid, err - } - defer resp.Body.Close() - if resp.StatusCode != http.StatusCreated { - return id.Invalid, errors.New(resp.Status) - } - dec := json.NewDecoder(resp.Body) - var newZid api.ZidJSON - err = dec.Decode(&newZid) - if err != nil { - return id.Invalid, err - } - zid, err := id.Parse(newZid.ID) - if err != nil { - return id.Invalid, err - } - return zid, nil -} - -func encodeZettelData(buf *bytes.Buffer, data *api.ZettelDataJSON) error { - enc := json.NewEncoder(buf) - enc.SetEscapeHTML(false) - return enc.Encode(&data) -} - -// ListZettel returns a list of all Zettel. -func (c *Client) ListZettel(ctx context.Context, query url.Values) ([]api.ZettelJSON, error) { - ub := c.jsonZettelURLBuilder(query) - resp, err := c.buildAndExecuteRequest(ctx, http.MethodGet, ub, nil, nil) - if err != nil { - return nil, err - } - defer resp.Body.Close() - if resp.StatusCode != http.StatusOK { - return nil, errors.New(resp.Status) - } - dec := json.NewDecoder(resp.Body) - var zl api.ZettelListJSON - err = dec.Decode(&zl) - if err != nil { - return nil, err - } - return zl.List, nil -} - -// GetZettelJSON returns a zettel as a JSON struct. -func (c *Client) GetZettelJSON(ctx context.Context, zid id.Zid, query url.Values) (*api.ZettelDataJSON, error) { - ub := c.jsonZettelURLBuilder(query).SetZid(zid) - resp, err := c.buildAndExecuteRequest(ctx, http.MethodGet, ub, nil, nil) - if err != nil { - return nil, err - } - defer resp.Body.Close() - if resp.StatusCode != http.StatusOK { - return nil, errors.New(resp.Status) - } - dec := json.NewDecoder(resp.Body) - var out api.ZettelDataJSON - err = dec.Decode(&out) - if err != nil { - return nil, err - } - return &out, nil -} - -// GetEvaluatedZettel return a zettel in a defined encoding. -func (c *Client) GetEvaluatedZettel(ctx context.Context, zid id.Zid, enc api.EncodingEnum) (string, error) { - ub := c.jsonZettelURLBuilder(nil).SetZid(zid) - ub.AppendQuery(api.QueryKeyFormat, enc.String()) - ub.AppendQuery(api.QueryKeyPart, api.PartContent) - resp, err := c.buildAndExecuteRequest(ctx, http.MethodGet, ub, nil, nil) - if err != nil { - return "", err - } - defer resp.Body.Close() - if resp.StatusCode != http.StatusOK { - return "", errors.New(resp.Status) - } - content, err := io.ReadAll(resp.Body) - if err != nil { - return "", err - } - return string(content), nil -} - -// GetZettelOrder returns metadata of the given zettel and, more important, -// metadata of zettel that are referenced in a list within the first zettel. -func (c *Client) GetZettelOrder(ctx context.Context, zid id.Zid) (*api.ZidMetaRelatedList, error) { - ub := c.newURLBuilder('o').SetZid(zid) - resp, err := c.buildAndExecuteRequest(ctx, http.MethodGet, ub, nil, nil) - if err != nil { - return nil, err - } - defer resp.Body.Close() - if resp.StatusCode != http.StatusOK { - return nil, errors.New(resp.Status) - } - dec := json.NewDecoder(resp.Body) - var out api.ZidMetaRelatedList - err = dec.Decode(&out) - if err != nil { - return nil, err - } - return &out, nil -} - -// ContextDirection specifies how the context should be calculated. -type ContextDirection uint8 - -// Allowed values for ContextDirection -const ( - _ ContextDirection = iota - DirBoth - DirBackward - DirForward -) - -// GetZettelContext returns metadata of the given zettel and, more important, -// metadata of zettel that for the context of the first zettel. -func (c *Client) GetZettelContext( - ctx context.Context, zid id.Zid, dir ContextDirection, depth, limit int) ( - *api.ZidMetaRelatedList, error, -) { - ub := c.newURLBuilder('x').SetZid(zid) - switch dir { - case DirBackward: - ub.AppendQuery(api.QueryKeyDir, api.DirBackward) - case DirForward: - ub.AppendQuery(api.QueryKeyDir, api.DirForward) - } - if depth > 0 { - ub.AppendQuery(api.QueryKeyDepth, strconv.Itoa(depth)) - } - if limit > 0 { - ub.AppendQuery(api.QueryKeyLimit, strconv.Itoa(limit)) - } - resp, err := c.buildAndExecuteRequest(ctx, http.MethodGet, ub, nil, nil) - if err != nil { - return nil, err - } - defer resp.Body.Close() - if resp.StatusCode != http.StatusOK { - return nil, errors.New(resp.Status) - } - dec := json.NewDecoder(resp.Body) - var out api.ZidMetaRelatedList - err = dec.Decode(&out) - if err != nil { - return nil, err - } - return &out, nil -} - -// GetZettelLinks returns connections to ohter zettel, images, externals URLs. -func (c *Client) GetZettelLinks(ctx context.Context, zid id.Zid) (*api.ZettelLinksJSON, error) { - ub := c.newURLBuilder('l').SetZid(zid) - resp, err := c.buildAndExecuteRequest(ctx, http.MethodGet, ub, nil, nil) - if err != nil { - return nil, err - } - defer resp.Body.Close() - if resp.StatusCode != http.StatusOK { - return nil, errors.New(resp.Status) - } - dec := json.NewDecoder(resp.Body) - var out api.ZettelLinksJSON - err = dec.Decode(&out) - if err != nil { - return nil, err - } - return &out, nil -} - -// UpdateZettel updates an existing zettel. -func (c *Client) UpdateZettel(ctx context.Context, zid id.Zid, data *api.ZettelDataJSON) error { - var buf bytes.Buffer - if err := encodeZettelData(&buf, data); err != nil { - return err - } - ub := c.jsonZettelURLBuilder(nil).SetZid(zid) - resp, err := c.buildAndExecuteRequest(ctx, http.MethodPut, ub, &buf, nil) - if err != nil { - return err - } - defer resp.Body.Close() - if resp.StatusCode != http.StatusNoContent { - return errors.New(resp.Status) - } - return nil -} - -// RenameZettel renames a zettel. -func (c *Client) RenameZettel(ctx context.Context, oldZid, newZid id.Zid) error { - ub := c.jsonZettelURLBuilder(nil).SetZid(oldZid) - h := http.Header{ - api.HeaderDestination: {c.jsonZettelURLBuilder(nil).SetZid(newZid).String()}, - } - resp, err := c.buildAndExecuteRequest(ctx, api.MethodMove, ub, nil, h) - if err != nil { - return err - } - defer resp.Body.Close() - if resp.StatusCode != http.StatusNoContent { - return errors.New(resp.Status) - } - return nil -} - -// DeleteZettel deletes a zettel with the given identifier. -func (c *Client) DeleteZettel(ctx context.Context, zid id.Zid) error { - ub := c.jsonZettelURLBuilder(nil).SetZid(zid) - resp, err := c.buildAndExecuteRequest(ctx, http.MethodDelete, ub, nil, nil) - if err != nil { - return err - } - defer resp.Body.Close() - if resp.StatusCode != http.StatusNoContent { - return errors.New(resp.Status) - } - return nil -} - -func (c *Client) jsonZettelURLBuilder(query url.Values) *api.URLBuilder { - ub := c.newURLBuilder('z') - for key, values := range query { - if key == api.QueryKeyFormat { - continue - } - for _, val := range values { - ub.AppendQuery(key, val) - } - } - return ub -} - -// ListTags returns a map of all tags, together with the associated zettel containing this tag. -func (c *Client) ListTags(ctx context.Context) (map[string][]string, error) { - err := c.updateToken(ctx) - if err != nil { - return nil, err - } - req, err := c.newRequest(ctx, http.MethodGet, c.newURLBuilder('t'), nil) - if err != nil { - return nil, err - } - resp, err := c.executeRequest(req) - if err != nil { - return nil, err - } - defer resp.Body.Close() - if resp.StatusCode != http.StatusOK { - return nil, errors.New(resp.Status) - } - dec := json.NewDecoder(resp.Body) - var tl api.TagListJSON - err = dec.Decode(&tl) - if err != nil { - return nil, err - } - return tl.Tags, nil -} - -// ListRoles returns a list of all roles. -func (c *Client) ListRoles(ctx context.Context) ([]string, error) { - err := c.updateToken(ctx) - if err != nil { - return nil, err - } - req, err := c.newRequest(ctx, http.MethodGet, c.newURLBuilder('r'), nil) - if err != nil { - return nil, err - } - resp, err := c.executeRequest(req) - if err != nil { - return nil, err - } - defer resp.Body.Close() - if resp.StatusCode != http.StatusOK { - return nil, errors.New(resp.Status) - } - dec := json.NewDecoder(resp.Body) - var rl api.RoleListJSON - err = dec.Decode(&rl) - if err != nil { - return nil, err - } - return rl.Roles, nil -} DELETED client/client_test.go Index: client/client_test.go ================================================================== --- client/client_test.go +++ client/client_test.go @@ -1,334 +0,0 @@ -//----------------------------------------------------------------------------- -// Copyright (c) 2021 Detlef Stern -// -// This file is part of zettelstore. -// -// Zettelstore is licensed under the latest version of the EUPL (European Union -// Public License). Please see file LICENSE.txt for your rights and obligations -// under this license. -//----------------------------------------------------------------------------- - -// Package client provides a client for accessing the Zettelstore via its API. -package client_test - -import ( - "context" - "flag" - "fmt" - "net/url" - "testing" - - "zettelstore.de/z/api" - "zettelstore.de/z/client" - "zettelstore.de/z/domain/id" - "zettelstore.de/z/domain/meta" -) - -func TestCreateRenameDeleteZettel(t *testing.T) { - // Is not to be allowed to run in parallel with other tests. - c := getClient() - c.SetAuth("creator", "creator") - zid, err := c.CreateZettel(context.Background(), &api.ZettelDataJSON{ - Meta: nil, - Encoding: "", - Content: "Example", - }) - if err != nil { - t.Error("Cannot create zettel:", err) - return - } - if !zid.IsValid() { - t.Error("Invalid zettel ID", zid) - return - } - newZid := zid + 1 - c.SetAuth("owner", "owner") - err = c.RenameZettel(context.Background(), zid, newZid) - if err != nil { - t.Error("Cannot rename", zid, ":", err) - newZid = zid - } - err = c.DeleteZettel(context.Background(), newZid) - if err != nil { - t.Error("Cannot delete", zid, ":", err) - return - } -} - -func TestUpdateZettel(t *testing.T) { - t.Parallel() - c := getClient() - c.SetAuth("writer", "writer") - z, err := c.GetZettelJSON(context.Background(), id.DefaultHomeZid, nil) - if err != nil { - t.Error(err) - return - } - if got := z.Meta[meta.KeyTitle]; got != "Home" { - t.Errorf("Title of zettel is not \"Home\", but %q", got) - return - } - newTitle := "New Home" - z.Meta[meta.KeyTitle] = newTitle - err = c.UpdateZettel(context.Background(), id.DefaultHomeZid, z) - if err != nil { - t.Error(err) - return - } - zt, err := c.GetZettelJSON(context.Background(), id.DefaultHomeZid, nil) - if err != nil { - t.Error(err) - return - } - if got := zt.Meta[meta.KeyTitle]; got != newTitle { - t.Errorf("Title of zettel is not %q, but %q", newTitle, got) - } -} - -func TestList(t *testing.T) { - testdata := []struct { - user string - exp int - }{ - {"", 7}, - {"creator", 10}, - {"reader", 12}, - {"writer", 12}, - {"owner", 34}, - } - - t.Parallel() - c := getClient() - query := url.Values{api.QueryKeyFormat: {"html"}} // Client must remove "html" - for i, tc := range testdata { - t.Run(fmt.Sprintf("User %d/%q", i, tc.user), func(tt *testing.T) { - c.SetAuth(tc.user, tc.user) - l, err := c.ListZettel(context.Background(), query) - if err != nil { - tt.Error(err) - return - } - got := len(l) - if got != tc.exp { - tt.Errorf("List of length %d expected, but got %d\n%v", tc.exp, got, l) - } - }) - } - l, err := c.ListZettel(context.Background(), url.Values{meta.KeyRole: {meta.ValueRoleConfiguration}}) - if err != nil { - t.Error(err) - return - } - got := len(l) - if got != 27 { - t.Errorf("List of length %d expected, but got %d\n%v", 27, got, l) - } -} -func TestGetZettel(t *testing.T) { - t.Parallel() - c := getClient() - c.SetAuth("owner", "owner") - z, err := c.GetZettelJSON(context.Background(), id.DefaultHomeZid, url.Values{api.QueryKeyPart: {api.PartContent}}) - if err != nil { - t.Error(err) - return - } - if m := z.Meta; len(m) > 0 { - t.Errorf("Exptected empty meta, but got %v", z.Meta) - } - if z.Content == "" || z.Encoding != "" { - t.Errorf("Expect non-empty content, but empty encoding (got %q)", z.Encoding) - } -} - -func TestGetEvaluatedZettel(t *testing.T) { - t.Parallel() - c := getClient() - c.SetAuth("owner", "owner") - encodings := []api.EncodingEnum{ - api.EncoderDJSON, - api.EncoderHTML, - api.EncoderNative, - api.EncoderText, - } - for _, enc := range encodings { - content, err := c.GetEvaluatedZettel(context.Background(), id.DefaultHomeZid, enc) - if err != nil { - t.Error(err) - continue - } - if len(content) == 0 { - t.Errorf("Empty content for encoding %v", enc) - } - } -} - -func TestGetZettelOrder(t *testing.T) { - t.Parallel() - c := getClient() - c.SetAuth("owner", "owner") - rl, err := c.GetZettelOrder(context.Background(), id.TOCNewTemplateZid) - if err != nil { - t.Error(err) - return - } - if rl.ID != id.TOCNewTemplateZid.String() { - t.Errorf("Expected an Zid %v, but got %v", id.TOCNewTemplateZid, rl.ID) - return - } - l := rl.List - if got := len(l); got != 2 { - t.Errorf("Expected list fo length 2, got %d", got) - return - } - if got := l[0].ID; got != id.TemplateNewZettelZid.String() { - t.Errorf("Expected result[0]=%v, but got %v", id.TemplateNewZettelZid, got) - } - if got := l[1].ID; got != id.TemplateNewUserZid.String() { - t.Errorf("Expected result[1]=%v, but got %v", id.TemplateNewUserZid, got) - } -} - -func TestGetZettelContext(t *testing.T) { - t.Parallel() - c := getClient() - c.SetAuth("owner", "owner") - rl, err := c.GetZettelContext(context.Background(), id.VersionZid, client.DirBoth, 0, 3) - if err != nil { - t.Error(err) - return - } - if rl.ID != id.VersionZid.String() { - t.Errorf("Expected an Zid %v, but got %v", id.VersionZid, rl.ID) - return - } - l := rl.List - if got := len(l); got != 3 { - t.Errorf("Expected list fo length 3, got %d", got) - return - } - if got := l[0].ID; got != id.DefaultHomeZid.String() { - t.Errorf("Expected result[0]=%v, but got %v", id.DefaultHomeZid, got) - } - if got := l[1].ID; got != id.OperatingSystemZid.String() { - t.Errorf("Expected result[1]=%v, but got %v", id.OperatingSystemZid, got) - } - if got := l[2].ID; got != id.StartupConfigurationZid.String() { - t.Errorf("Expected result[2]=%v, but got %v", id.StartupConfigurationZid, got) - } - - rl, err = c.GetZettelContext(context.Background(), id.VersionZid, client.DirBackward, 0, 0) - if err != nil { - t.Error(err) - return - } - if rl.ID != id.VersionZid.String() { - t.Errorf("Expected an Zid %v, but got %v", id.VersionZid, rl.ID) - return - } - l = rl.List - if got := len(l); got != 1 { - t.Errorf("Expected list fo length 1, got %d", got) - return - } - if got := l[0].ID; got != id.DefaultHomeZid.String() { - t.Errorf("Expected result[0]=%v, but got %v", id.DefaultHomeZid, got) - } -} - -func TestGetZettelLinks(t *testing.T) { - t.Parallel() - c := getClient() - c.SetAuth("owner", "owner") - zl, err := c.GetZettelLinks(context.Background(), id.DefaultHomeZid) - if err != nil { - t.Error(err) - return - } - if zl.ID != id.DefaultHomeZid.String() { - t.Errorf("Expected an Zid %v, but got %v", id.DefaultHomeZid, zl.ID) - return - } - if len(zl.Links.Incoming) != 0 { - t.Error("No incomings expected", zl.Links.Incoming) - } - if got := len(zl.Links.Outgoing); got != 4 { - t.Errorf("Expected 4 outgoing links, got %d", got) - } - if got := len(zl.Links.Local); got != 1 { - t.Errorf("Expected 1 local link, got %d", got) - } - if got := len(zl.Links.External); got != 4 { - t.Errorf("Expected 4 external link, got %d", got) - } -} - -func TestListTags(t *testing.T) { - t.Parallel() - c := getClient() - c.SetAuth("owner", "owner") - tm, err := c.ListTags(context.Background()) - if err != nil { - t.Error(err) - return - } - tags := []struct { - key string - size int - }{ - {"#invisible", 1}, - {"#user", 4}, - {"#test", 4}, - } - if len(tm) != len(tags) { - t.Errorf("Expected %d different tags, but got only %d (%v)", len(tags), len(tm), tm) - } - for _, tag := range tags { - if zl, ok := tm[tag.key]; !ok { - t.Errorf("No tag %v: %v", tag.key, tm) - } else if len(zl) != tag.size { - t.Errorf("Expected %d zettel with tag %v, but got %v", tag.size, tag.key, zl) - } - } - for i, id := range tm["#user"] { - if id != tm["#test"][i] { - t.Errorf("Tags #user and #test have different content: %v vs %v", tm["#user"], tm["#test"]) - } - } -} - -func TestListRoles(t *testing.T) { - t.Parallel() - c := getClient() - c.SetAuth("owner", "owner") - rl, err := c.ListRoles(context.Background()) - if err != nil { - t.Error(err) - return - } - exp := []string{"configuration", "user", "zettel"} - if len(rl) != len(exp) { - t.Errorf("Expected %d different tags, but got only %d (%v)", len(exp), len(rl), rl) - } - for i, id := range exp { - if id != rl[i] { - t.Errorf("Role list pos %d: expected %q, got %q", i, id, rl[i]) - } - } -} - -var baseURL string - -func init() { - flag.StringVar(&baseURL, "base-url", "", "Base URL") -} - -func getClient() *client.Client { return client.NewClient(baseURL) } - -// TestMain controls whether client API tests should run or not. -func TestMain(m *testing.M) { - flag.Parse() - if baseURL != "" { - m.Run() - } -} Index: cmd/cmd_file.go ================================================================== --- cmd/cmd_file.go +++ cmd/cmd_file.go @@ -14,11 +14,10 @@ "flag" "fmt" "io" "os" - "zettelstore.de/z/api" "zettelstore.de/z/domain" "zettelstore.de/z/domain/id" "zettelstore.de/z/domain/meta" "zettelstore.de/z/encoder" "zettelstore.de/z/input" @@ -39,11 +38,11 @@ Content: domain.NewContent(inp.Src[inp.Pos:]), }, m.GetDefault(meta.KeySyntax, meta.ValueSyntaxZmk), nil, ) - enc := encoder.Create(api.Encoder(format), &encoder.Environment{Lang: m.GetDefault(meta.KeyLang, meta.ValueLangEN)}) + 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 = enc.WriteZettel(os.Stdout, z, format != "raw") Index: cmd/cmd_run.go ================================================================== --- cmd/cmd_run.go +++ cmd/cmd_run.go @@ -12,17 +12,17 @@ import ( "flag" "net/http" - zsapi "zettelstore.de/z/api" "zettelstore.de/z/auth" - "zettelstore.de/z/box" "zettelstore.de/z/config" "zettelstore.de/z/domain/meta" "zettelstore.de/z/kernel" + "zettelstore.de/z/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" ) @@ -56,81 +56,71 @@ return 1, err } return 0, nil } -func setupRouting(webSrv server.Server, boxManager box.Manager, authManager auth.Manager, rtConfig config.Config) { - protectedBoxManager, authPolicy := authManager.BoxWithPolicy(webSrv, boxManager, rtConfig) - api := api.New(webSrv, authManager, authManager, webSrv, rtConfig) - wui := webui.New(webSrv, authManager, rtConfig, authManager, boxManager, authPolicy) - - ucAuthenticate := usecase.NewAuthenticate(authManager, authManager, boxManager) - ucCreateZettel := usecase.NewCreateZettel(rtConfig, protectedBoxManager) - ucGetMeta := usecase.NewGetMeta(protectedBoxManager) - ucGetAllMeta := usecase.NewGetAllMeta(protectedBoxManager) - ucGetZettel := usecase.NewGetZettel(protectedBoxManager) +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(protectedBoxManager) - ucListRoles := usecase.NewListRole(protectedBoxManager) - ucListTags := usecase.NewListTags(protectedBoxManager) - ucZettelContext := usecase.NewZettelContext(protectedBoxManager) - ucDelete := usecase.NewDeleteZettel(protectedBoxManager) - ucUpdate := usecase.NewUpdateZettel(protectedBoxManager) - ucRename := usecase.NewRenameZettel(protectedBoxManager) - - webSrv.Handle("/", wui.MakeGetRootHandler(protectedBoxManager)) - - // Web user interface - webSrv.AddListRoute('a', http.MethodGet, wui.MakeGetLoginHandler()) - webSrv.AddListRoute('a', http.MethodPost, wui.MakePostLoginHandler(ucAuthenticate)) + 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(ucRename)) + 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(ucDelete)) + 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(ucUpdate)) + 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(protectedBoxManager), ucGetMeta, ucGetZettel)) + 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, ucGetAllMeta)) + webSrv.AddZettelRoute('i', http.MethodGet, wui.MakeGetInfoHandler(ucParseZettel, ucGetMeta)) webSrv.AddZettelRoute('j', http.MethodGet, wui.MakeZettelContextHandler(ucZettelContext)) - // API webSrv.AddZettelRoute('l', http.MethodGet, api.MakeGetLinksHandler(ucParseZettel)) webSrv.AddZettelRoute('o', http.MethodGet, api.MakeGetOrderHandler( - usecase.NewZettelOrder(protectedBoxManager, ucParseZettel))) + usecase.NewZettelOrder(protectedPlaceManager, ucParseZettel))) webSrv.AddListRoute('r', http.MethodGet, api.MakeListRoleHandler(ucListRoles)) webSrv.AddListRoute('t', http.MethodGet, api.MakeListTagsHandler(ucListTags)) - webSrv.AddListRoute('v', http.MethodPost, api.MakePostLoginHandler(ucAuthenticate)) - webSrv.AddListRoute('v', http.MethodPut, api.MakeRenewAuthHandler()) - webSrv.AddZettelRoute('x', http.MethodGet, api.MakeZettelContextHandler(ucZettelContext)) + webSrv.AddZettelRoute('y', http.MethodGet, api.MakeZettelContextHandler(ucZettelContext)) webSrv.AddListRoute('z', http.MethodGet, api.MakeListMetaHandler( - usecase.NewListMeta(protectedBoxManager), ucGetMeta, ucParseZettel)) + usecase.NewListMeta(protectedPlaceManager), ucGetMeta, ucParseZettel)) webSrv.AddZettelRoute('z', http.MethodGet, api.MakeGetZettelHandler( ucParseZettel, ucGetMeta)) - if !authManager.IsReadonly() { - webSrv.AddListRoute('z', http.MethodPost, api.MakePostCreateZettelHandler(ucCreateZettel)) - webSrv.AddZettelRoute('z', http.MethodDelete, api.MakeDeleteZettelHandler(ucDelete)) - webSrv.AddZettelRoute('z', http.MethodPut, api.MakeUpdateZettelHandler(ucUpdate)) - webSrv.AddZettelRoute('z', zsapi.MethodMove, api.MakeRenameZettelHandler(ucRename)) - } if authManager.WithAuth() { - webSrv.SetUserRetriever(usecase.NewGetUserByZid(boxManager)) + webSrv.SetUserRetriever(usecase.NewGetUserByZid(placeManager)) } } Index: cmd/command.go ================================================================== --- cmd/command.go +++ cmd/command.go @@ -17,17 +17,16 @@ "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 - Boxes bool // if true then boxes will be set up - Header bool // Print a heading on startup - LineServer bool // Start admin line server - Flags 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. Index: cmd/fd_limit_raise.go ================================================================== --- cmd/fd_limit_raise.go +++ cmd/fd_limit_raise.go @@ -39,9 +39,9 @@ err = syscall.Getrlimit(syscall.RLIMIT_NOFILE, &rLimit) if err != nil { return err } if rLimit.Cur < minFiles { - log.Printf("Make sure you have no more than %d files in all your boxes if you enabled notification\n", rLimit.Cur) + 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 @@ -20,18 +20,18 @@ "strconv" "strings" "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/place" + "zettelstore.de/z/place/manager" + "zettelstore.de/z/place/progplace" "zettelstore.de/z/web/server" ) const ( defConfigfile = ".zscfg" @@ -52,21 +52,20 @@ Name: "version", Func: func(*flag.FlagSet, *meta.Meta) (int, error) { return 0, nil }, Header: true, }) RegisterCommand(Command{ - Name: "run", - Func: runFunc, - Boxes: true, - Header: true, - LineServer: true, - Flags: flgRun, + Name: "run", + Func: runFunc, + Places: true, + Header: true, + Flags: flgRun, }) RegisterCommand(Command{ Name: "run-simple", Func: runSimpleFunc, - Boxes: true, + Places: true, Header: true, Flags: flgSimpleRun, }) RegisterCommand(Command{ Name: "file", @@ -112,11 +111,11 @@ if strings.HasPrefix(val, "/") { val = "dir://" + val } else { val = "dir:" + val } - cfg.Set(keyBoxOneURI, val) + cfg.Set(keyPlaceOneURI, val) case "r": cfg.Set(keyReadOnly, flg.Value.String()) case "v": cfg.Set(keyVerbose, flg.Value.String()) } @@ -132,22 +131,22 @@ } return strconv.Itoa(port), nil } const ( - keyAdminPort = "admin-port" - keyDefaultDirBoxType = "default-dir-box-type" - keyInsecureCookie = "insecure-cookie" - keyListenAddr = "listen-addr" - 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" + 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 { @@ -156,21 +155,21 @@ ok = setConfigValue(ok, kernel.AuthService, kernel.AuthOwner, cfg.GetDefault(keyOwner, "")) ok = setConfigValue(ok, kernel.AuthService, kernel.AuthReadonly, cfg.GetBool(keyReadOnly)) ok = setConfigValue( - ok, kernel.BoxService, kernel.BoxDefaultDirType, - cfg.GetDefault(keyDefaultDirBoxType, kernel.BoxDirTypeNotify)) - ok = setConfigValue(ok, kernel.BoxService, kernel.BoxURIs+"1", "dir:./zettel") - format := kernel.BoxURIs + "%v" + 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 } - ok = setConfigValue(ok, kernel.BoxService, key, val) + ok = setConfigValue(ok, kernel.PlaceService, key, val) } ok = setConfigValue( ok, kernel.WebService, kernel.WebListenAddress, cfg.GetDefault(keyListenAddr, "127.0.0.1:23123")) @@ -194,34 +193,34 @@ kernel.Main.Log("unable to set configuration:", key, val) } return ok && done } -func setupOperations(cfg *meta.Meta, withBoxes bool) { - var createManager kernel.CreateBoxManagerFunc - if withBoxes { +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.BoxService, kernel.BoxDefaultDirType, kernel.BoxDirTypeSimple) + srvm.SetConfig(kernel.PlaceService, kernel.PlaceDefaultDirType, kernel.PlaceDirTypeSimple) } - createManager = func(boxURIs []*url.URL, authManager auth.Manager, rtConfig config.Config) (box.Manager, error) { - compbox.Setup(cfg) - return manager.New(boxURIs, authManager, rtConfig) + 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) (box.Manager, error) { return nil, nil } + 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 box.Manager, authMgr auth.Manager, rtConfig config.Config) error { + func(srv server.Server, plMgr place.Manager, authMgr auth.Manager, rtConfig config.Config) error { setupRouting(srv, plMgr, authMgr, rtConfig) return nil }, ) } @@ -240,12 +239,12 @@ cfg := getConfig(fs) if err := setServiceConfig(cfg); err != nil { fmt.Fprintf(os.Stderr, "%s: %v\n", name, err) return 2 } - setupOperations(cfg, command.Boxes) - kernel.Main.Start(command.Header, command.LineServer) + 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) } kernel.Main.Shutdown(true) Index: cmd/register.go ================================================================== --- cmd/register.go +++ cmd/register.go @@ -11,15 +11,10 @@ // 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/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. @@ -28,6 +23,11 @@ _ "zettelstore.de/z/parser/blob" // Allow to use BLOB parser. _ "zettelstore.de/z/parser/markdown" // Allow to use markdown parser. _ "zettelstore.de/z/parser/none" // Allow to use none parser. _ "zettelstore.de/z/parser/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: collect/collect.go ================================================================== --- collect/collect.go +++ collect/collect.go @@ -9,11 +9,13 @@ //----------------------------------------------------------------------------- // 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 referenced links Images []*ast.Reference // list of all referenced images @@ -20,24 +22,82 @@ 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.WalkBlockSlice(&s, zn.Ast) - return s -} - -// Visit all node to collect data for the summary. -func (s *Summary) Visit(node ast.Node) ast.Visitor { - switch n := node.(type) { - case *ast.LinkNode: - s.Links = append(s.Links, n.Ref) - case *ast.ImageNode: - if n.Ref != nil { - s.Images = append(s.Images, n.Ref) - } - case *ast.CiteNode: - s.Cites = append(s.Cites, n) - } - return s -} +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 @@ -25,11 +25,10 @@ } return r } func TestLinks(t *testing.T) { - t.Parallel() zn := &ast.ZettelNode{} summary := collect.References(zn) if summary.Links != nil || summary.Images != nil { t.Error("No links/images expected, but got:", summary.Links, "and", summary.Images) } @@ -53,11 +52,10 @@ t.Error("Link count does not work. Expected: 3, got", summary.Links) } } func TestImage(t *testing.T) { - t.Parallel() zn := &ast.ZettelNode{ Ast: ast.BlockSlice{ &ast.ParaNode{ Inlines: ast.InlineSlice{ &ast.ImageNode{Ref: parseRef("12345678901234")}, Index: collect/order.go ================================================================== --- collect/order.go +++ collect/order.go @@ -15,11 +15,11 @@ // Order of internal reference within the given zettel. func Order(zn *ast.ZettelNode) (result []*ast.Reference) { for _, bn := range zn.Ast { if ln, ok := bn.(*ast.NestedListNode); ok { - switch ln.Kind { + switch ln.Code { case ast.NestedListOrdered, ast.NestedListUnordered: for _, is := range ln.Items { if ref := firstItemZettelReference(is); ref != nil { result = append(result, ref) } Index: config/config.go ================================================================== --- config/config.go +++ config/config.go @@ -54,10 +54,14 @@ 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 { // GetExpertMode returns the current value of the "expert-mode" key Index: docs/manual/00001002000000.zettel ================================================================== --- docs/manual/00001002000000.zettel +++ docs/manual/00001002000000.zettel @@ -13,14 +13,14 @@ : All zettel belong to you, only to you. Zettelstore provides its services only to one person: you. 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 Index: docs/manual/00001003000000.zettel ================================================================== --- docs/manual/00001003000000.zettel +++ docs/manual/00001003000000.zettel @@ -7,11 +7,11 @@ === 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 -* A sub-directory ""zettel"" will be created in the directory where you put the executable. +* 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 and learn about the various ways to write zettel. @@ -41,11 +41,11 @@ --create-home --home-dir /var/lib/zettelstore \ --shell /usr/sbin/nologin \ --comment "Zettelstore server" \ zettelstore ``` -Create a systemd service file and store it into ''/etc/systemd/system/zettelstore.service'': +Create a systemd service file and place it into ''/etc/systemd/system/zettelstore.service'': ```ini [Unit] Description=Zettelstore After=network.target Index: docs/manual/00001004010000.zettel ================================================================== --- docs/manual/00001004010000.zettel +++ docs/manual/00001004010000.zettel @@ -1,15 +1,15 @@ id: 00001004010000 title: Zettelstore startup configuration role: manual tags: #configuration #manual #zettelstore syntax: zmk -modified: 20210712234656 +modified: 20210525121644 The configuration file, as specified by the ''-c CONFIGFILE'' [[command line option|00001004051000]], allows you to specify some startup options. These options cannot be stored in a [[configuration zettel|00001004020000]] because either they are needed before Zettelstore can start or because of security reasons. -For example, Zettelstore need to know in advance, on which network address is must listen or where zettel are stored. +For example, Zettelstore need to know in advance, on which network address is must listen or where zettel are placed. An attacker that is able to change the owner can do anything. Therefore only the owner of the computer on which Zettelstore runs can change this information. The file for startup configuration must be created via a text editor in advance. @@ -16,26 +16,18 @@ The syntax of the configuration file is the same as for any zettel metadata. The following keys are supported: ; [!admin-port]''admin-port'' : Specifies the TCP port through which you can reach the [[administrator console|00001004100000]]. - A value of ''0'' (the default) disables the administrator console. - The administrator console will only be enabled if Zettelstore is started with the [[''run'' sub-command|00001004051000]]. + A value of ''0'' (the default) disables the administrators console. On most operating systems, the value must be greater than ''1024'' unless you start Zettelstore with the full privileges of a system administrator (which is not recommended). Default: ''0'' -; [!box-uri-x]''box-uri-//X//'', where //X// is a number greater or equal to one -: Specifies a [[box|00001004011200]] where zettel are stored. - During startup //X// is counted up, starting with one, until no key is found. - This allows to configure more than one box. - - If no ''box-uri-1'' key is given, the overall effect will be the same as if only ''box-uri-1'' was specified with the value ''dir://.zettel''. - In this case, even a key ''box-uri-2'' will be ignored. -; [!default-dir-box-type]''default-dir-box-type'' -: Specifies the default value for the (sub-) type of [[directory boxes|00001004011400#type]]. - Zettel are typically stored in such boxes. +; [!default-dir-place-type]''default-dir-place-type'' +: Specifies the default value for the (sub-) type of [[directory places|00001004011400#type]]. + Zettel are typically stored in such places. Default: ''notify'' ; [!insecure-cookie]''insecure-cookie'' : Must be set to ''true'', if authentication is enabled and Zettelstore is not accessible not via HTTPS (but via HTTP). Otherwise web browser are free to ignore the authentication cookie. @@ -58,10 +50,17 @@ If ''true'', a persistent cookie is used. Its lifetime exceeds the lifetime of the authentication token (see option ''token-lifetime-html'') by 30 seconds. Default: ''false'' +; [!place-uri-X]''place-uri-//X//'', where //X// is a number greater or equal to one +: Specifies a [[place|00001004011200]] where zettel are stored. + During startup //X// is counted up, starting with one, until no key is found. + This allows to configure more than one place. + + If no ''place-uri-1'' key is given, the overall effect will be the same as if only ''place-uri-1'' was specified with the value ''dir://.zettel''. + In this case, even a key ''place-uri-2'' will be ignored. ; [!read-only-mode]''read-only-mode'' : Puts the Zettelstore web service into a read-only mode. No changes are possible. Default: false. ; [!token-lifetime-api]''token-lifetime-api'', [!token-lifetime-html]''token-lifetime-html'' @@ -81,5 +80,7 @@ This allows to use a forwarding proxy [[server|00001010090100]] in front of the Zettelstore. ; ''verbose'' : Be more verbose inf logging data. Default: false + +Other keys will be ignored. Index: docs/manual/00001004011200.zettel ================================================================== --- docs/manual/00001004011200.zettel +++ docs/manual/00001004011200.zettel @@ -1,46 +1,46 @@ id: 00001004011200 -title: Zettelstore boxes +title: Zettelstore places role: manual tags: #configuration #manual #zettelstore syntax: zmk modified: 20210525121452 A Zettelstore must store its zettel somehow and somewhere. In most cases you want to store your zettel as files in a directory. -Under certain circumstances you may want to store your zettel elsewhere. +Under certain circumstances you may want to store your zettel in other places. An example are the [[predefined zettel|00001005090000]] that come with a Zettelstore. They are stored within the software itself. In another situation you may want to store your zettel volatile, e.g. if you want to provide a sandbox for experimenting. -To cope with these (and more) situations, you configure Zettelstore to use one or more //boxes//{-}. -This is done via the ''box-uri-X'' keys of the [[startup configuration|00001004010000#box-uri-X]] (X is a number). -Boxes are specified using special [[URIs|https://en.wikipedia.org/wiki/Uniform_Resource_Identifier]], somehow similar to web addresses. +To cope with these (and more) situations, you configure Zettelstore to use one or more places. +This is done via the ''place-uri-X'' keys of the [[startup configuration|00001004010000#place-uri-X]] (X is a number). +Places are specified using special [[URIs|https://en.wikipedia.org/wiki/Uniform_Resource_Identifier]], somehow similar to web addresses. -The following box URIs are supported: +The following place URIs are supported: ; ''dir:\//DIR'' : Specifies a directory where zettel files are stored. ''DIR'' is the file path. Although it is possible to use relative file paths, such as ''./zettel'' (→ URI is ''dir:\//.zettel''), it is preferable to use absolute file paths, e.g. ''/home/user/zettel''. The directory must exist before starting the Zettelstore[^There is one exception: when Zettelstore is [[started without any parameter|00001004050000]], e.g. via double-clicking its icon, an directory called ''./zettel'' will be created.]. - It is possible to [[configure|00001004011400]] a directory box. + It is possible to [[configure|00001004011400]] a directory place. ; ''file:FILE.zip'' oder ''file:/\//path/to/file.zip'' : Specifies a ZIP file which contains files that store zettel. You can create such a ZIP file, if you zip a directory full of zettel files. - This box is always read-only. + This place is always read-only. ; ''mem:'' : Stores all its zettel in volatile memory. If you stop the Zettelstore, all changes are lost. -All boxes that you configure via the ''box-uri-X'' keys form a chain of boxes. -If a zettel should be retrieved, a search starts in the box specified with the ''box-uri-2'' key, then ''box-uri-3'' and so on. -If a zettel is created or changed, it is always stored in the box specified with the ''box-uri-1'' key. -This allows to overwrite zettel from other boxes, e.g. the predefined zettel. +All places that you configure via the ''store-uri-X'' keys form a chain of places. +If a zettel should be retrieved, a search starts in the place specified with the ''place-uri-2'' key, then ''place-uri-3'' and so on. +If a zettel is created or changed, it is always stored in the place specified with the ''place-uri-1'' key. +This allows to overwrite zettel from other places, e.g. the predefined zettel. -If you use the ''mem:'' box, where zettel are stored in volatile memory, it makes only sense if you configure it as ''box-uri-1''. -Such a box will be empty when Zettelstore starts and only the first box will receive updates. +If you use the ''mem:'' place, where zettel are stored in volatile memory, it makes only sense if you configure it as ''place-uri-1''. +Such a place will be empty when Zettelstore starts and only the first place will receive updates. You must make sure that your computer has enough RAM to store all zettel. Index: docs/manual/00001004011400.zettel ================================================================== --- docs/manual/00001004011400.zettel +++ docs/manual/00001004011400.zettel @@ -1,29 +1,29 @@ id: 00001004011400 -title: Configure file directory boxes +title: Configure file directory places role: manual tags: #configuration #manual #zettelstore syntax: zmk modified: 20210525121232 -Under certain circumstances, it is preferable to further configure a file directory box. -This is done by appending query parameters after the base box URI ''dir:\//DIR''. +Under certain circumstances, it is preferable to further configure a file directory place. +This is done by appending query parameters after the base place URI ''dir:\//DIR''. The following parameters are supported: |= Parameter:|Description|Default value:| -|type|(Sub-) Type of the directory service|(value of ''[[default-dir-box-type|00001004010000#default-dir-box-type]]'') +|type|(Sub-) Type of the directory service|(value of ''[[default-dir-place-type|00001004010000#default-dir-place-type]]'') |rescan|Time (in seconds) after which the directory should be scanned fully|600 |worker|Number of worker that can access the directory in parallel|(depends on type) |readonly|Allow only operations that do not change a zettel or create a new zettel|n/a === Type On some operating systems, Zettelstore tries to detect changes to zettel files outside of Zettelstore's control[^This includes Linux, Windows, and macOS.]. On other operating systems, this may be not possible, due to technical limitations. -Automatic detection of external changes is also not possible, if zettel files are put on an external service, such as a file server accessed via SMD/CIFS or NFS. +Automatic detection of external changes is also not possible, if zettel files are placed on an external service, such as a file server accessed via SMD/CIFS or NFS. -To cope with this uncertainty, Zettelstore provides various internal implementations of a directory box. +To cope with this uncertainty, Zettelstore provides various internal implementations of a directory place. The default values should match the needs of different users, as explained in the [[installation part|00001003000000]] of this manual. The following values are supported: ; simple : Is not able to detect external changes. @@ -39,44 +39,44 @@ Under certain circumstances it is possible that Zettelstore does not detect a change done by another software. To cope with this unlikely, but still possible event, Zettelstore re-scans periodically the file directory. The time interval is configured by the ''rescan'' parameter, e.g. ``` -box-uri-1: dir:///home/zettel?rescan=300 +place-uri-1: dir:///home/zettel?rescan=300 ``` This makes Zettelstore to re-scan the directory ''/home/zettel/'' every 300 seconds, i.e. 5 minutes. For a Zettelstore with many zettel, re-scanning the directory may take a while, especially if it is stored on a remote computer (e.g. via CIFS/SMB or NFS). In this case, you should adjust the parameter value. Please note that a directory re-scan invalidates all internal data of a Zettelstore. It might trigger a re-build of the backlink database (and other internal databases). -Therefore a large value is preferred. +Therefore a large value if preferred. -This value is ignored for other directory box types, such as ""simple"". +This value is ignored for other directory place type, such as ""simple"". === Worker Internally, Zettelstore parallels concurrent requests for a zettel or its metadata. The number of parallel activities is configured by the ''worker'' parameter. A computer contains a limited number of internal processing units (CPU). Its number ranges from 1 to (currently) 128, e.g. in bigger server environments. Zettelstore typically runs on a system with 1 to 8 CPUs. Access to zettel file is ultimately managed by the underlying operating system. -Depending on the hardware and on the type of the directory box, only a limited number of parallel accesses are desirable. +Depending on the hardware and on the type of the directory place, only a limited number of parallel accesses are desirable. On smaller hardware[^In comparison to a normal desktop or laptop computer], such as the [[Raspberry Zero|https://www.raspberrypi.org/products/raspberry-pi-zero/]], a smaller value might be appropriate. Every worker needs some amount of main memory (RAM) and some amount of processing power. On bigger hardware, with some fast file services, a bigger value could result in higher performance, if needed. -For a directory box of type ""notify"", the default value is: 7. -The directory box type ""simple"" limits the value to a maximum of 1, i.e. no concurrency is possible with this type of directory box. +For a directory place of type ""notify"", the default value is: 7. +The directory place type ""simple"" limits the value to a maximum of 1, i.e. no concurrency is possible with this type of directory place. For various reasons, the value should be a prime number, with a maximum value of 1499. === Readonly -Sometimes you may want to provide zettel from a file directory box, but you want to disallow any changes. -If you provide the query parameter ''readonly'' (with or without a corresponding value), the box will disallow any changes. +Sometimes you may want to provide zettel from a file directory place, but you want to disallow any changes. +If you provide the query parameter ''readonly'' (with or without a corresponding value), the place will disallow any changes. ``` -box-uri-1: dir:///home/zettel?readonly +place-uri-1: dir:///home/zettel?readonly ``` -If you put the whole Zettelstore in [[read-only|00001004010000]] [[mode|00001004051000]], all configured file directory boxes will be in read-only mode too, even if not explicitly configured. +If you put the whole Zettelstore in [[read-only|00001004010000]] [[mode|00001004051000]], all configured file directory places will be in read-only mode too, even if not explicitly configured. Index: docs/manual/00001004020000.zettel ================================================================== --- docs/manual/00001004020000.zettel +++ docs/manual/00001004020000.zettel @@ -1,11 +1,10 @@ id: 00001004020000 title: Configure the running Zettelstore role: manual tags: #configuration #manual #zettelstore syntax: zmk -modified: 20210611213730 You can configure a running Zettelstore by modifying the special zettel with the ID [[00000000000100]]. This zettel is called ""configuration zettel"". The following metadata keys change the appearance / behavior of Zettelstore: @@ -53,10 +52,14 @@ : Specifies the identifier of the zettel, that should be presented for the default view / home view. If not given or if the identifier does not identify a zettel, the zettel with the identifier ''00010000000000'' is shown. ; [!marker-external]''marker-external'' : Some HTML code that is displayed after a reference to external material. Default: ''&\#10138;'', to display a ""➚"" sign. +; [!list-page-size]''list-page-size'' +: If set to a value greater than zero, specifies the number of items shown in WebUI lists. + Basically, this is the list of all zettel (possibly restricted) and the list of search results. + Default: ''0''. ; [!site-name]''site-name'' : Name of the Zettelstore instance. Will be used when displaying some lists. Default: ''Zettelstore''. ; [!yaml-header]''yaml-header'' Index: docs/manual/00001004050200.zettel ================================================================== --- docs/manual/00001004050200.zettel +++ docs/manual/00001004050200.zettel @@ -1,20 +1,21 @@ id: 00001004050200 title: The ''help'' sub-command role: manual tags: #command #configuration #manual #zettelstore syntax: zmk -modified: 20210712233414 +precursor: 00001004050000 Lists all implemented sub-commands. Example: ``` # zettelstore help Available commands: +- "config" - "file" - "help" - "password" - "run" - "run-simple" - "version" ``` Index: docs/manual/00001004050400.zettel ================================================================== --- docs/manual/00001004050400.zettel +++ docs/manual/00001004050400.zettel @@ -1,27 +1,28 @@ id: 00001004050400 title: The ''version'' sub-command role: manual tags: #command #configuration #manual #zettelstore syntax: zmk -modified: 20210712234031 +precursor: 00001004050000 Emits some information about the Zettelstore's version. This allows you to check, whether your installed Zettelstore is -The name of the software (""Zettelstore"") and the build version information is given, as well as the compiler version, and an indication about the operating system and the processor architecture of that computer. +The name of the software (""Zettelstore"") and the build version information is given, as well as the compiler version, the name of the computer running the Zettelstore, and an indication about the operating system and the processor architecture of that computer. -The build version information is a string like ''1.0.2+351ae138b4''. -The part ""1.0.2"" is the release version. -""+351ae138b4"" is a code uniquely identifying the version to the developer. +The build version information is a string like ''v1.0.2-34-gf567a3''. +The part ""v1.0.2"" is the release version. +The string ""34"" specifies the number of internal patches, after the release was published. +""gf567a3"" is a code uniquely identify the version to the developer. -Everything after the release version is optional, eg. ""1.4.3"" is a valid build version information too. +Everything after the release version is optional, eg. ""v1.4.3"" is a valid build version information too. Example: ``` # zettelstore version -Zettelstore 1.0.2+351ae138b4 (go1.16.5@linux/amd64) -Licensed under the latest version of the EUPL (European Union Public License) +Zettelstore (v0.0.4/go1.15) running on mycomputer (linux/amd64) ``` -In this example, Zettelstore is running in the released version ""1.0.2"" and was compiled using [[Go, version 1.16.5|https://golang.org/doc/go1.16]]. +In this example, Zettelstore is running in the released version ""v.0.0.4"" and was compiled using [[Go, version 1.15|https://golang.org/doc/go1.15]]. +It runs on a computer named ""mycomputer"". The software was build for running under a Linux operating system with an ""amd64"" processor. Index: docs/manual/00001004051000.zettel ================================================================== --- docs/manual/00001004051000.zettel +++ docs/manual/00001004051000.zettel @@ -1,17 +1,18 @@ id: 00001004051000 title: The ''run'' sub-command role: manual tags: #command #configuration #manual #zettelstore syntax: zmk -modified: 20210712234419 +modified: 20210510153318 +precursor: 00001004050000 === ``zettelstore run`` This starts the web service. ``` -zettelstore run [-a PORT] [-c CONFIGFILE] [-d DIR] [-debug] [-p PORT] [-r] [-v] +zettelstore run [-c CONFIGFILE] [-d DIR] [-p PORT] [-r] [-v] ``` ; [!a]''-a PORT'' : Specifies the TCP port through which you can reach the [[administrator console|00001004100000]]. See the explanation of [[''admin-port''|00001004010000#admin-port]] for more details. Index: docs/manual/00001004051100.zettel ================================================================== --- docs/manual/00001004051100.zettel +++ docs/manual/00001004051100.zettel @@ -1,24 +1,23 @@ id: 00001004051100 title: The ''run-simple'' sub-command role: manual tags: #command #configuration #manual #zettelstore syntax: zmk -modified: 20210712234203 +precursor: 00001004050000 === ``zettelstore run-simple`` This sub-command is implicitly called, when an user starts Zettelstore by double-clicking on its GUI icon. It is s simplified variant of the [[''run'' sub-command|00001004051000]]. It allows only to specify a zettel directory. The directory will be created automatically, if it does not exist. -This is a difference to the ''run'' sub-command, where the directory must exists. -In contrast to the ''run'' sub-command, other command line parameter are not allowed. +This is the only difference to the ''run'' sub-command, where the directory must exists. ``` zettelstore run-simple [-d DIR] ``` ; [!d]''-d DIR'' : Specifies ''DIR'' as the directory that contains all zettel. Default is ''./zettel'' (''.\\zettel'' on Windows), where ''.'' denotes the ""current directory"". Index: docs/manual/00001004051200.zettel ================================================================== --- docs/manual/00001004051200.zettel +++ docs/manual/00001004051200.zettel @@ -1,11 +1,11 @@ id: 00001004051200 title: The ''file'' sub-command role: manual tags: #command #configuration #manual #zettelstore syntax: zmk -modified: 20210712234222 +precursor: 00001004050000 Reads zettel data from a file (or from standard input / stdin) and renders it to standard output / stdout. This allows Zettelstore to render files manually. ``` zettelstore file [-t FORMAT] [file-1 [file-2]] Index: docs/manual/00001004051400.zettel ================================================================== --- docs/manual/00001004051400.zettel +++ docs/manual/00001004051400.zettel @@ -1,11 +1,11 @@ id: 00001004051400 title: The ''password'' sub-command role: manual tags: #command #configuration #manual #zettelstore syntax: zmk -modified: 20210712234305 +precursor: 00001004050000 This sub-command is used to create a hashed password for to be authenticated users. It reads a password from standard input (two times, both must be equal) and writes the hashed password to standard output. @@ -15,11 +15,11 @@ ``` ``IDENT`` is the identification for the user that should be authenticated. ``ZETTEL-ID`` is the [[identifier of the zettel|00001006050000]] that later acts as a user zettel. -See [[Creating an user zettel|00001010040200]] for some background information. +See [[Creating an user zettel|00001010040200]] for background. An example: ``` # zettelstore password bob 20200911115600 Index: docs/manual/00001004101000.zettel ================================================================== --- docs/manual/00001004101000.zettel +++ docs/manual/00001004101000.zettel @@ -58,14 +58,14 @@ : Sets a single configuration value for the next configuration of a given service. It will become effective if the service is restarted. If the key specifies a list value, all other list values with a number greater than the given key are deleted. You can use the special number ""0"" to delete all values. - E.g. ``set-config box box-uri-0 any_text`` will remove all values of the list //box-uri-//. + E.g. ``set-config place place-uri-0 any_text`` will remove all values of the list //place-uri-//. ; ''shutdown'' : Terminate the Zettelstore itself (and closes the connection to the administrator console). ; ''start SERVICE'' : Start the given bservice and all dependent services. ; ''stat SERVICE'' : Display some statistical values for the given service. ; ''stop SERVICE'' : Stop the given service and all other that depend on this. Index: docs/manual/00001005000000.zettel ================================================================== --- docs/manual/00001005000000.zettel +++ docs/manual/00001005000000.zettel @@ -1,11 +1,10 @@ id: 00001005000000 title: Structure of Zettelstore role: manual tags: #design #manual #zettelstore syntax: zmk -modified: 20210614165848 Zettelstore is a software that manages your zettel. Since every zettel must be readable without any special tool, most zettel has to be stored as ordinary files within specific directories. Typically, file names and file content must comply to specific rules so that Zettelstore can manage them. If you add, delete, or change zettel files with other tools, e.g. a text editor, Zettelstore will monitor these actions. @@ -19,11 +18,11 @@ Zettelstore becomes extensible by external software. For example, a more sophisticated web interface could be build, or an application for your mobile device that allows you to send content to your Zettelstore as new zettel. === Where zettel are stored -Your zettel are stored typically as files in a specific directory. +Your zettel are stored as files in a specific directory. If you have not explicitly specified the directory, a default directory will be used. The directory has to be specified at [[startup time|00001004010000]]. Nested directories are not supported (yet). Every file in this directory that should be monitored by Zettelstore must have a file name that begins with 14 digits (0-9), the [[zettel identifier|00001006050000]]. @@ -47,24 +46,24 @@ The solution is a ''.meta'' file with the same zettel identifier. Zettelstore recognizes this situation and reads in both files for the one zettel containing the figure. It maintains this relationship as long as theses files exists. In case of some textual zettel content you do not want to store the metadata and the zettel content in two different files. -Here the ''.zettel'' extension will signal that the metadata and the zettel content will be put in the same file, separated by an empty line or a line with three dashes (""''-\-\-''"", also known as ""YAML separator""). +Here the ''.zettel'' extension will signal that the metadata and the zettel content will be placed in the same file, separated by an empty line or a line with three dashes (""''-\-\-''"", also known as ""YAML separator""). === Predefined zettel Zettelstore contains some [[predefined zettel|00001005090000]] to work properly. The [[configuration zettel|00001004020000]] is one example. To render the builtin web interface, some templates are used, as well as a layout specification in CSS. -The icon that visualizes a broken image is a predefined GIF image. +The icon that visualizes an external link is a predefined SVG image. All of these are visible to the Zettelstore as zettel. One reason for this is to allow you to modify these zettel to adapt Zettelstore to your needs and visual preferences. Where are these zettel stored? -They are stored within the Zettelstore software itself, because one design goal was to have just one executable file to use Zettelstore. +They are stored within the Zettelstore software itself, because one design goal was to have just one file to use Zettelstore. But data stored within an executable programm cannot be changed later[^Well, it can, but it is a very bad idea to allow this. Mostly for security reasons.]. To allow changing predefined zettel, both the file store and the internal zettel store are internally chained together. If you change a zettel, it will be always stored as a file. If a zettel is requested, Zettelstore will first try to read that zettel from a file. @@ -73,17 +72,5 @@ Therefore, the file store ""shadows"" the internal zettel store. If you want to read the original zettel, you either have to delete the zettel (which removes it from the file directory), or you have to rename it to another zettel identifier. Now we have two places where zettel are stored: in the specific directory and within the Zettelstore software. * [[List of predefined zettel|00001005090000]] - -=== Boxes: other ways to store zettel -As described above, a zettel may be stored as a file inside a directory or inside the Zettelstore software itself. -Zettelstore allows other ways to store zettel by providing an abstraction called //box//.[^Formerly, zettel were stored physically in boxes, often made of wood.] - -A file directory which stores zettel is called a ""directory box"". -But zettel may be also stored in a ZIP file, which is called ""file box"". -For testing purposes, zettel may be stored in volatile memeory (called //RAM//). -This way is called ""memory box"". - -Other types of boxes could be added to Zettelstore. -What about a ""remote Zettelstore box""? Index: docs/manual/00001005090000.zettel ================================================================== --- docs/manual/00001005090000.zettel +++ docs/manual/00001005090000.zettel @@ -1,11 +1,11 @@ id: 00001005090000 title: List of predefined zettel role: manual tags: #manual #reference #zettelstore syntax: zmk -modified: 20210622124647 +modified: 20210511180816 The following table lists all predefined zettel with their purpose. |= Identifier :|= Title | Purpose | [[00000000000001]] | Zettelstore Version | Contains the version string of the running Zettelstore @@ -12,11 +12,11 @@ | [[00000000000002]] | Zettelstore Host | Contains the name of the computer running the Zettelstore | [[00000000000003]] | Zettelstore Operating System | Contains the operating system and CPU architecture of the computer running the Zettelstore | [[00000000000004]] | Zettelstore License | Lists the license of Zettelstore | [[00000000000005]] | Zettelstore Contributors | Lists all contributors of Zettelstore | [[00000000000006]] | Zettelstore Dependencies | Lists all licensed content -| [[00000000000020]] | Zettelstore Box Manager | Contains some statistics about zettel boxes and the the index process +| [[00000000000020]] | Zettelstore Place Manager | Contains some statistics about zettel places and the the index process | [[00000000000090]] | Zettelstore Supported Metadata Keys | Contains all supported metadata keys, their [[types|00001006030000]], and more | [[00000000000096]] | Zettelstore Startup Configuration | Contains the effective values of the [[startup configuration|00001004010000]] | [[00000000000100]] | Zettelstore Runtime Configuration | Allows to [[configure Zettelstore at runtime|00001004020000]] | [[00000000010100]] | Zettelstore Base HTML Template | Contains the general layout of the HTML view | [[00000000010200]] | Zettelstore Login Form HTML Template | Layout of the login form, when authentication is [[enabled|00001010040100]] @@ -26,16 +26,15 @@ | [[00000000010403]] | Zettelstore Form HTML Template | Form that is used to create a new or to change an existing zettel that contains text | [[00000000010404]] | Zettelstore Rename Form HTML Template | View that is displayed to change the [[zettel identifier|00001006050000]] | [[00000000010405]] | Zettelstore Delete HTML Template | View to confirm the deletion of a zettel | [[00000000010500]] | Zettelstore List Roles HTML Template | Layout for listing all roles | [[00000000010600]] | Zettelstore List Tags HTML Template | Layout of tags lists -| [[00000000020001]] | Zettelstore Base CSS | System-defined CSS file that is included by the [[Base HTML Template|00000000010100]] -| [[00000000025001]] | Zettelstore User CSS | User-defined CSS file that is included by the [[Base HTML Template|00000000010100]] +| [[00000000020001]] | Zettelstore Base CSS | CSS file that is included by the [[Base HTML Template|00000000010100]] | [[00000000040001]] | Generic Emoji | Image that is shown if original image reference is invalid | [[00000000090000]] | New Menu | Contains items that should contain in the zettel template menu | [[00000000090001]] | New Zettel | Template for a new zettel with role ""[[zettel|00001006020100]]"" | [[00000000090002]] | New User | Template for a new zettel with role ""[[user|00001006020100#user]]"" | [[00010000000000]] | Home | Default home zettel, contains some welcome information If a zettel is not linked, it is not accessible for the current user. **Important:** All identifier may change until a stable version of the software is released. Index: docs/manual/00001006020000.zettel ================================================================== --- docs/manual/00001006020000.zettel +++ docs/manual/00001006020000.zettel @@ -1,11 +1,10 @@ id: 00001006020000 title: Supported Metadata Keys role: manual tags: #manual #meta #reference #zettel #zettelstore syntax: zmk -modified: 20210709162756 Although you are free to define your own metadata, by using any key (according to the [[syntax|00001006010000]]), some keys have a special meaning that is enforced by Zettelstore. See the [[computed list of supported metadata keys|00000000000090]] for details. Most keys conform to a [[type|00001006030000]]. @@ -47,23 +46,20 @@ This is a computed value. There is no need to set it via Zettelstore. ; [!no-index]''no-index'' : If set to true, the zettel will not be indexed and therefore not be found in full-text searches. -; [!box-number]''box-number'' -: Is a computed value and contains the number of the box where the zettel was found. - For all but the [[predefined zettel|00001005090000]], this number is equal to the number //X// specified in startup configuration key [[''box-uri-//X//''|00001004010000#box-uri-x]]. ; [!precursor]''precursor'' : References zettel for which this zettel is a ""Folgezettel"" / follow-up zettel. Basically the inverse of key [[''folge''|#folge]]. ; [!published]''published'' : This property contains the timestamp of the mast modification / creation of the zettel. - If [[''modified''|#modified]] is set, it contains the same value. + If [[''modified''|#modified]]is set, it contains the same value. Otherwise, if the zettel identifier contains a valid timestamp, the identifier is used. In all other cases, this property is not set. - It can be used for [[sorting|00001012052000]] zettel based on their publication date. + It can be used for [[sorting|00001012051800#sort]] zettel based on their publication date. It is a computed value. There is no need to set it via Zettelstore. ; [!read-only]''read-only'' : Marks a zettel as read-only. Index: docs/manual/00001006020400.zettel ================================================================== --- docs/manual/00001006020400.zettel +++ docs/manual/00001006020400.zettel @@ -14,11 +14,11 @@ If there is no metadata value for key ''read-only'' or if its [[boolean value|00001006030500]] is interpreted as ""false"", anybody can modify the zettel. If the metadata value is something else (the value ""true"" is recommended), the user cannot modify the zettel through the web interface. -However, if the zettel is stored as a file in a [[directory box|00001004011400]], +However, if the zettel is stored as a file in a [[directory place|00001004011400]], the zettel could be modified using an external editor. === Authentication enabled If there is no metadata value for key ''read-only'' or if its [[boolean value|00001006030500]] is interpreted as ""false"", anybody can modify the zettel. @@ -33,8 +33,8 @@ : Neither an unauthenticated user, nor users with roles ""reader"" or ""writer"" are allowed to modify the zettel. Only the owner of the Zettelstore can modify the zettel. If the metadata value is something else (one of the values ""true"" or ""owner"" is recommended), no user is allowed modify the zettel through the web interface. -However, if the zettel is accessible as a file in a [[directory box|00001004011400]], +However, if the zettel is accessible as a file in a [[directory place|00001004011400]], the zettel could be modified using an external editor. Typically the owner of a Zettelstore have such an access. Index: docs/manual/00001006030000.zettel ================================================================== --- docs/manual/00001006030000.zettel +++ docs/manual/00001006030000.zettel @@ -1,21 +1,15 @@ id: 00001006030000 title: Supported Key Types role: manual tags: #manual #meta #reference #zettel #zettelstore syntax: zmk -modified: 20210627170437 - -All [[supported metadata keys|00001006020000]] conform to a type. - -User-defined metadata keys conform also to a type, based on the suffix of the key. -|=Suffix|Type -| ''-number'' | [[Number|00001006033000]] -| ''-url'' | [[URL|00001006035000]] -| ''-zid'' | [[Identifier|00001006032000]] -| any other suffix | [[EString|00001006031500]] - + +Most [[supported metadata keys|00001006020000]] conform to a type. + +Every metadata key should conform to a type. +User-defined metadata keys are of type EString. The name of the metadata key is bound to the key type Every key type has an associated validation rule to check values of the given type. There is also a rule how values are matched, e.g. against a search term when selecting some zettel. And there is a rule, how values compare for sorting. Index: docs/manual/00001006050000.zettel ================================================================== --- docs/manual/00001006050000.zettel +++ docs/manual/00001006050000.zettel @@ -1,11 +1,10 @@ id: 00001006050000 title: Zettel identifier -role: manual tags: #design #manual #zettelstore syntax: zmk -modified: 20210721123222 +role: manual Each zettel is given a unique identifier. To some degree, the zettel identifier is part of the metadata. Basically, the identifier is given by the [[Zettelstore|00001005000000]] software. @@ -18,12 +17,11 @@ In most cases the zettel identifier is the timestamp when the zettel was created. However, the Zettelstore software just checks for exactly 14 digits. Anybody is free to assign a ""non-timestamp"" identifier to a zettel, e.g. with a month part of ""35"" or with ""99"" as the last two digits. - -Some zettel identifier are [[reserved|00001006055000]] and should not be used otherwise. -All identifiers of zettel initially provided by an empty Zettelstore begin with ""000000"", except the home zettel ''00010000000000''. +In fact, all identifiers of zettel initially provided by an empty Zettelstore +begin with ""000000"", except the home zettel ''00010000000000''. Zettel identifier of this manual have be chosen to begin with ""000010"". A zettel can have any identifier that contains 14 digits and that is not in use by another zettel managed by the same Zettelstore. DELETED docs/manual/00001006055000.zettel Index: docs/manual/00001006055000.zettel ================================================================== --- docs/manual/00001006055000.zettel +++ docs/manual/00001006055000.zettel @@ -1,43 +0,0 @@ -id: 00001006055000 -title: Reserved zettel identifier -role: manual -tags: #design #manual #zettelstore -syntax: zmk -modified: 20210721125518 - -[[Zettel identifier|00001006050000]] are typically created by examine the current date and time. -By renaming a zettel, you are able to provide any sequence of 14 digits. -If no other zettel has the same identifier, you are allowed to rename a zettel. - -To make things easier, you normally should not use zettel identifier that begin with four zeroes (''0000''). - -All zettel provided by an empty zettelstore begin with six zeroes[^Exception: the predefined home zettel ''00010000000000''. But you can [[configure|00001004020000#home-zettel]] another zettel with another identifier as the new home zettel.]. -Zettel identifier of this manual have be chosen to begin with ''000010''. - -However, some external applications may need a range of specific zettel identifier to work properly. -Identifier that begin with ''00009'' can be used for such purpose. -To request a reservation, please send an email to the maintainer of Zettelstore. -The request must include the following data: -; Title -: Title of you application -; Description -: A brief description what the application is used for and why you need to reserve some zettel identifier -; Number -: Specify the amount of zettel identifier you are planning to use. - Minimum size is 100. - If you need more than 10.000, your justification will contain more words. - -=== Reserved Zettel Identifier - -|= From | To | Description -| 00000000000000 | 0000000000000 | This is an invalid zettel identifier -| 00000000000001 | 0000009999999 | [[Predefined zettel|00001005090000]] -| 00000100000000 | 0000019999999 | Zettelstore manual -| 00000200000000 | 0000899999999 | Reserved for future use -| 00009000000000 | 0000999999999 | Reserved for applications - -This list may change in the future. - -==== External Applications -|= From | To | Description -| 00009000001000 | 00009000001000 | ZS Slides, an application to display zettel as a HTML-based slideshow Index: docs/manual/00001007010000.zettel ================================================================== --- docs/manual/00001007010000.zettel +++ docs/manual/00001007010000.zettel @@ -22,28 +22,28 @@ With some exceptions, two identical non-space characters begins a formatting range that is ended with the same two characters. Exceptions are: links, images, edits, comments, and both the ""en-dash"" and the ""horizontal ellipsis"". A link is given with ``[[...]]``{=zmk}, an images with ``{{...}}``{=zmk}, and an edit formatting with ``((...))``{=zmk}. An inline comment, beginning with the sequence ``%%``{=zmk}, always ends at the end of the line where it begins. -The ""en-dash"" (""--"") is specified as ``--``{=zmk}, the ""horizontal ellipsis"" (""..."") as ``...``{=zmk}[^If put at the end of non-space text.]. +The ""en-dash"" (""--"") is specified as ``--``{=zmk}, the ""horizontal ellipsis"" (""..."") as ``...``{=zmk}[^If placed at the end of non-space text.]. Some inline elements do not follow the rule of two identical character, especially to specify footnotes, citation keys, and local marks. These elements begin with one opening square bracket (""``[``""), use a character for specifying the kind of the inline, typically allow to specify some content, and end with one closing square bracket (""``]``""). One inline element that does not begin with two characters is the ""entity"". It allows to specify any Unicode character. -The specification of that character is put between an ampersand character and a semicolon: ``&...;``{=zmk}. +The specification of that character is placed between an ampersand character and a semicolon: ``&...;``{=zmk}. For exmple, an ""n-dash"" could also be specified as ``–``{==zmk}. The backslash character (""``\\``"") possibly gives the next character a special meaning. This allows to resolve some left ambiguities. For example, a list of depth 2 will begin a line with ``** Item 2.2``{=zmk}. An inline element to strongly emphasize some text begin with a space will be specified as ``** Text**``{=zmk}. To force the inline element formatting at the beginning of a line, ``**\\ Text**``{=zmk} should better be specified. Many block and inline elements can be refined by additional attributes. -Attributes resemble roughly HTML attributes and are put near the corresponding elements by using the syntax ``{...}``{=zmk}. +Attributes resemble roughly HTML attributes and are placed near the corresponding elements by using the syntax ``{...}``{=zmk}. One example is to make space characters visible inside a inline literal element: ``1 + 2 = 3``{-} was specified by using the default attribute: ``\`\`1 + 2 = 3\`\`{-}``. To summarize: * With some exceptions, blocks-structural elements begins at the for position of a line with three identical characters. Index: docs/manual/00001007030200.zettel ================================================================== --- docs/manual/00001007030200.zettel +++ docs/manual/00001007030200.zettel @@ -88,11 +88,11 @@ * C ::: Please note that two lists cannot be separated by an empty line. -Instead you should put a horizonal rule (""thematic break"") between them. +Instead you should place a horizonal rule (""thematic break"") between them. You could also use a mark element or a hard line break to separate the two lists: ```zmk # One # Two [!sep] Index: docs/manual/00001007031000.zettel ================================================================== --- docs/manual/00001007031000.zettel +++ docs/manual/00001007031000.zettel @@ -91,17 +91,17 @@ === Rows to be ignored A line that begins with the sequence ''|%'' (vertical bar character (""''|''"", ''U+007C''), followed by a percent sign character (“%”, U+0025)) will be ignored. This allows to specify a horizontal rule that is not rendered. Such tables are emitted by some commands of the [[administrator console|00001004100000]]. -For example, the command ``get-config box`` will emit +For example, the command ``get-config place`` will emit ``` |=Key | Value | Description -|%-----------+--------+--------------------------- -| defdirtype | notify | Default directory box type +|%-----------+--------+----------------------------- +| defdirtype | notify | Default directory place type ``` This is rendered in HTML as: :::example |=Key | Value | Description -|%-----------+--------+--------------------------- -| defdirtype | notify | Default directory box type +|%-----------+--------+----------------------------- +| defdirtype | notify | Default directory place type ::: Index: docs/manual/00001007040000.zettel ================================================================== --- docs/manual/00001007040000.zettel +++ docs/manual/00001007040000.zettel @@ -45,17 +45,17 @@ ==== Entities & more Sometimes it is not easy to enter special characters. If you know the Unicode code point of that character, or its name according to the [[HTML standard|https://html.spec.whatwg.org/multipage/named-characters.html]], you can enter it by number or by name. Regardless which method you use, an entity always begins with an ampersand character (""''&''"", ''U+0026'') and ends with a semicolon character (""'';''"", ''U+003B''). -If you know the HTML name of the character you want to enter, put it between these two character. +If you know the HTML name of the character you want to enter, place it between these two character. Example: ``&`` is rendered as ::&::{=example}. If you want to enter its numeric code point, a number sign character must follow the ampersand character, followed by digits to base 10. Example: ``&`` is rendered in HTML as ::&::{=example}. -You also can enter its numeric code point as a hex number, if you put the letter ""x"" after the numeric sign character. +You also can enter its numeric code point as a hex number, if you place the letter ""x"" after the numeric sign character. Example: ``&`` is rendered in HTML as ::&::{=example}. Since some Unicode character are used quite often, a special notation is introduced for them: * Two consecutive hyphen-minus characters result in an //en-dash// character. Index: docs/manual/00001008000000.zettel ================================================================== --- docs/manual/00001008000000.zettel +++ docs/manual/00001008000000.zettel @@ -1,11 +1,11 @@ id: 00001008000000 title: Other Markup Languages role: manual tags: #manual #zettelstore syntax: zmk -modified: 20210705111758 +modified: 20210523194915 [[Zettelmarkup|00001007000000]] is not the only markup language you can use to define your content. Zettelstore is quite agnostic with respect to markup languages. Of course, Zettelmarkup plays an important role. However, with the exception of zettel titles, you can use any (markup) language that is supported: @@ -23,22 +23,16 @@ ; [!css]''css'' : A [[Cascading Style Sheet|https://www.w3.org/Style/CSS/]], to be used when rendering a zettel as HTML. ; [!gif]''gif''; [!jpeg]''jpeg''; [!jpg]''jpg''; [!png]''png'' : The formats for pixel graphics. Typically the data is stored in a separate file and the syntax is given in the ''.meta'' file. -; [!html]''html'' -: Hypertext Markup Language, will not be parsed further. - Instead, it is treated as [[text|#text]], but will be encoded differently for [[HTML format|00001012920510]] (same for the [[web user interface|00001014000000]]). - - For security reasons, equivocal elements will not be encoded in the HTML format / web user interface, e.g. the ``\nokay\n", "html": "\n

okay

\n", - "example": 170, - "start_line": 2756, - "end_line": 2770, - "section": "HTML blocks" - }, - { - "markdown": "\n", - "html": "\n", - "example": 171, - "start_line": 2775, - "end_line": 2791, + "example": 140, + "start_line": 2411, + "end_line": 2425, "section": "HTML blocks" }, { "markdown": "\nh1 {color:red;}\n\np {color:blue;}\n\nokay\n", "html": "\nh1 {color:red;}\n\np {color:blue;}\n\n

okay

\n", - "example": 172, - "start_line": 2795, - "end_line": 2811, + "example": 141, + "start_line": 2430, + "end_line": 2446, "section": "HTML blocks" }, { "markdown": "\n\nfoo\n", "html": "\n\nfoo\n", - "example": 173, - "start_line": 2818, - "end_line": 2828, + "example": 142, + "start_line": 2453, + "end_line": 2463, "section": "HTML blocks" }, { "markdown": ">
\n> foo\n\nbar\n", "html": "
\n
\nfoo\n
\n

bar

\n", - "example": 174, - "start_line": 2831, - "end_line": 2842, + "example": 143, + "start_line": 2466, + "end_line": 2477, "section": "HTML blocks" }, { "markdown": "-
\n- foo\n", "html": "
    \n
  • \n
    \n
  • \n
  • foo
  • \n
\n", - "example": 175, - "start_line": 2845, - "end_line": 2855, + "example": 144, + "start_line": 2480, + "end_line": 2490, "section": "HTML blocks" }, { "markdown": "\n*foo*\n", "html": "\n

foo

\n", - "example": 176, - "start_line": 2860, - "end_line": 2866, + "example": 145, + "start_line": 2495, + "end_line": 2501, "section": "HTML blocks" }, { "markdown": "*bar*\n*baz*\n", "html": "*bar*\n

baz

\n", - "example": 177, - "start_line": 2869, - "end_line": 2875, + "example": 146, + "start_line": 2504, + "end_line": 2510, "section": "HTML blocks" }, { "markdown": "1. *bar*\n", "html": "1. *bar*\n", - "example": 178, - "start_line": 2881, - "end_line": 2889, + "example": 147, + "start_line": 2516, + "end_line": 2524, "section": "HTML blocks" }, { "markdown": "\nokay\n", "html": "\n

okay

\n", - "example": 179, - "start_line": 2894, - "end_line": 2906, + "example": 148, + "start_line": 2529, + "end_line": 2541, "section": "HTML blocks" }, { "markdown": "';\n\n?>\nokay\n", "html": "';\n\n?>\n

okay

\n", - "example": 180, - "start_line": 2912, - "end_line": 2926, + "example": 149, + "start_line": 2547, + "end_line": 2561, "section": "HTML blocks" }, { "markdown": "\n", "html": "\n", - "example": 181, - "start_line": 2931, - "end_line": 2935, + "example": 150, + "start_line": 2566, + "end_line": 2570, "section": "HTML blocks" }, { "markdown": "\nokay\n", "html": "\n

okay

\n", - "example": 182, - "start_line": 2940, - "end_line": 2968, + "example": 151, + "start_line": 2575, + "end_line": 2603, "section": "HTML blocks" }, { "markdown": " \n\n \n", "html": " \n
<!-- foo -->\n
\n", - "example": 183, - "start_line": 2974, - "end_line": 2982, + "example": 152, + "start_line": 2608, + "end_line": 2616, "section": "HTML blocks" }, { "markdown": "
\n\n
\n", "html": "
\n
<div>\n
\n", - "example": 184, - "start_line": 2985, - "end_line": 2993, + "example": 153, + "start_line": 2619, + "end_line": 2627, "section": "HTML blocks" }, { "markdown": "Foo\n
\nbar\n
\n", "html": "

Foo

\n
\nbar\n
\n", - "example": 185, - "start_line": 2999, - "end_line": 3009, + "example": 154, + "start_line": 2633, + "end_line": 2643, "section": "HTML blocks" }, { "markdown": "
\nbar\n
\n*foo*\n", "html": "
\nbar\n
\n*foo*\n", - "example": 186, - "start_line": 3016, - "end_line": 3026, + "example": 155, + "start_line": 2650, + "end_line": 2660, "section": "HTML blocks" }, { "markdown": "Foo\n\nbaz\n", "html": "

Foo\n\nbaz

\n", - "example": 187, - "start_line": 3031, - "end_line": 3039, + "example": 156, + "start_line": 2665, + "end_line": 2673, "section": "HTML blocks" }, { "markdown": "
\n\n*Emphasized* text.\n\n
\n", "html": "
\n

Emphasized text.

\n
\n", - "example": 188, - "start_line": 3072, - "end_line": 3082, + "example": 157, + "start_line": 2706, + "end_line": 2716, "section": "HTML blocks" }, { "markdown": "
\n*Emphasized* text.\n
\n", "html": "
\n*Emphasized* text.\n
\n", - "example": 189, - "start_line": 3085, - "end_line": 3093, + "example": 158, + "start_line": 2719, + "end_line": 2727, "section": "HTML blocks" }, { "markdown": "\n\n\n\n\n\n\n\n
\nHi\n
\n", "html": "\n\n\n\n
\nHi\n
\n", - "example": 190, - "start_line": 3107, - "end_line": 3127, + "example": 159, + "start_line": 2741, + "end_line": 2761, "section": "HTML blocks" }, { "markdown": "\n\n \n\n \n\n \n\n
\n Hi\n
\n", "html": "\n \n
<td>\n  Hi\n</td>\n
\n \n
\n", - "example": 191, - "start_line": 3134, - "end_line": 3155, + "example": 160, + "start_line": 2768, + "end_line": 2789, "section": "HTML blocks" }, { "markdown": "[foo]: /url \"title\"\n\n[foo]\n", "html": "

foo

\n", - "example": 192, - "start_line": 3183, - "end_line": 3189, + "example": 161, + "start_line": 2816, + "end_line": 2822, "section": "Link reference definitions" }, { "markdown": " [foo]: \n /url \n 'the title' \n\n[foo]\n", "html": "

foo

\n", - "example": 193, - "start_line": 3192, - "end_line": 3200, + "example": 162, + "start_line": 2825, + "end_line": 2833, "section": "Link reference definitions" }, { "markdown": "[Foo*bar\\]]:my_(url) 'title (with parens)'\n\n[Foo*bar\\]]\n", "html": "

Foo*bar]

\n", - "example": 194, - "start_line": 3203, - "end_line": 3209, + "example": 163, + "start_line": 2836, + "end_line": 2842, "section": "Link reference definitions" }, { "markdown": "[Foo bar]:\n\n'title'\n\n[Foo bar]\n", "html": "

Foo bar

\n", - "example": 195, - "start_line": 3212, - "end_line": 3220, + "example": 164, + "start_line": 2845, + "end_line": 2853, "section": "Link reference definitions" }, { "markdown": "[foo]: /url '\ntitle\nline1\nline2\n'\n\n[foo]\n", "html": "

foo

\n", - "example": 196, - "start_line": 3225, - "end_line": 3239, + "example": 165, + "start_line": 2858, + "end_line": 2872, "section": "Link reference definitions" }, { "markdown": "[foo]: /url 'title\n\nwith blank line'\n\n[foo]\n", "html": "

[foo]: /url 'title

\n

with blank line'

\n

[foo]

\n", - "example": 197, - "start_line": 3244, - "end_line": 3254, + "example": 166, + "start_line": 2877, + "end_line": 2887, "section": "Link reference definitions" }, { "markdown": "[foo]:\n/url\n\n[foo]\n", "html": "

foo

\n", - "example": 198, - "start_line": 3259, - "end_line": 3266, + "example": 167, + "start_line": 2892, + "end_line": 2899, "section": "Link reference definitions" }, { "markdown": "[foo]:\n\n[foo]\n", "html": "

[foo]:

\n

[foo]

\n", - "example": 199, - "start_line": 3271, - "end_line": 3278, + "example": 168, + "start_line": 2904, + "end_line": 2911, "section": "Link reference definitions" }, { "markdown": "[foo]: <>\n\n[foo]\n", "html": "

foo

\n", - "example": 200, - "start_line": 3283, - "end_line": 3289, + "example": 169, + "start_line": 2916, + "end_line": 2922, "section": "Link reference definitions" }, { "markdown": "[foo]: (baz)\n\n[foo]\n", "html": "

[foo]: (baz)

\n

[foo]

\n", - "example": 201, - "start_line": 3294, - "end_line": 3301, + "example": 170, + "start_line": 2927, + "end_line": 2934, "section": "Link reference definitions" }, { "markdown": "[foo]: /url\\bar\\*baz \"foo\\\"bar\\baz\"\n\n[foo]\n", "html": "

foo

\n", - "example": 202, - "start_line": 3307, - "end_line": 3313, + "example": 171, + "start_line": 2940, + "end_line": 2946, "section": "Link reference definitions" }, { "markdown": "[foo]\n\n[foo]: url\n", "html": "

foo

\n", - "example": 203, - "start_line": 3318, - "end_line": 3324, + "example": 172, + "start_line": 2951, + "end_line": 2957, "section": "Link reference definitions" }, { "markdown": "[foo]\n\n[foo]: first\n[foo]: second\n", "html": "

foo

\n", - "example": 204, - "start_line": 3330, - "end_line": 3337, + "example": 173, + "start_line": 2963, + "end_line": 2970, "section": "Link reference definitions" }, { "markdown": "[FOO]: /url\n\n[Foo]\n", "html": "

Foo

\n", - "example": 205, - "start_line": 3343, - "end_line": 3349, + "example": 174, + "start_line": 2976, + "end_line": 2982, "section": "Link reference definitions" }, { "markdown": "[ΑΓΩ]: /φου\n\n[αγω]\n", "html": "

αγω

\n", - "example": 206, - "start_line": 3352, - "end_line": 3358, + "example": 175, + "start_line": 2985, + "end_line": 2991, "section": "Link reference definitions" }, { "markdown": "[foo]: /url\n", "html": "", - "example": 207, - "start_line": 3367, - "end_line": 3370, + "example": 176, + "start_line": 2997, + "end_line": 3000, "section": "Link reference definitions" }, { "markdown": "[\nfoo\n]: /url\nbar\n", "html": "

bar

\n", - "example": 208, - "start_line": 3375, - "end_line": 3382, + "example": 177, + "start_line": 3005, + "end_line": 3012, "section": "Link reference definitions" }, { "markdown": "[foo]: /url \"title\" ok\n", "html": "

[foo]: /url "title" ok

\n", - "example": 209, - "start_line": 3388, - "end_line": 3392, + "example": 178, + "start_line": 3018, + "end_line": 3022, "section": "Link reference definitions" }, { "markdown": "[foo]: /url\n\"title\" ok\n", "html": "

"title" ok

\n", - "example": 210, - "start_line": 3397, - "end_line": 3402, + "example": 179, + "start_line": 3027, + "end_line": 3032, "section": "Link reference definitions" }, { "markdown": " [foo]: /url \"title\"\n\n[foo]\n", "html": "
[foo]: /url "title"\n
\n

[foo]

\n", - "example": 211, - "start_line": 3408, - "end_line": 3416, + "example": 180, + "start_line": 3038, + "end_line": 3046, "section": "Link reference definitions" }, { "markdown": "```\n[foo]: /url\n```\n\n[foo]\n", "html": "
[foo]: /url\n
\n

[foo]

\n", - "example": 212, - "start_line": 3422, - "end_line": 3432, + "example": 181, + "start_line": 3052, + "end_line": 3062, "section": "Link reference definitions" }, { "markdown": "Foo\n[bar]: /baz\n\n[bar]\n", "html": "

Foo\n[bar]: /baz

\n

[bar]

\n", - "example": 213, - "start_line": 3437, - "end_line": 3446, + "example": 182, + "start_line": 3067, + "end_line": 3076, "section": "Link reference definitions" }, { "markdown": "# [Foo]\n[foo]: /url\n> bar\n", "html": "

Foo

\n
\n

bar

\n
\n", - "example": 214, - "start_line": 3452, - "end_line": 3461, + "example": 183, + "start_line": 3082, + "end_line": 3091, "section": "Link reference definitions" }, { "markdown": "[foo]: /url\nbar\n===\n[foo]\n", "html": "

bar

\n

foo

\n", - "example": 215, - "start_line": 3463, - "end_line": 3471, + "example": 184, + "start_line": 3093, + "end_line": 3101, "section": "Link reference definitions" }, { "markdown": "[foo]: /url\n===\n[foo]\n", "html": "

===\nfoo

\n", - "example": 216, - "start_line": 3473, - "end_line": 3480, + "example": 185, + "start_line": 3103, + "end_line": 3110, "section": "Link reference definitions" }, { "markdown": "[foo]: /foo-url \"foo\"\n[bar]: /bar-url\n \"bar\"\n[baz]: /baz-url\n\n[foo],\n[bar],\n[baz]\n", "html": "

foo,\nbar,\nbaz

\n", - "example": 217, - "start_line": 3486, - "end_line": 3499, + "example": 186, + "start_line": 3116, + "end_line": 3129, "section": "Link reference definitions" }, { "markdown": "[foo]\n\n> [foo]: /url\n", "html": "

foo

\n
\n
\n", - "example": 218, - "start_line": 3507, - "end_line": 3515, + "example": 187, + "start_line": 3137, + "end_line": 3145, + "section": "Link reference definitions" + }, + { + "markdown": "[foo]: /url\n", + "html": "", + "example": 188, + "start_line": 3154, + "end_line": 3157, "section": "Link reference definitions" }, { "markdown": "aaa\n\nbbb\n", "html": "

aaa

\n

bbb

\n", - "example": 219, - "start_line": 3529, - "end_line": 3536, + "example": 189, + "start_line": 3171, + "end_line": 3178, "section": "Paragraphs" }, { "markdown": "aaa\nbbb\n\nccc\nddd\n", "html": "

aaa\nbbb

\n

ccc\nddd

\n", - "example": 220, - "start_line": 3541, - "end_line": 3552, + "example": 190, + "start_line": 3183, + "end_line": 3194, "section": "Paragraphs" }, { "markdown": "aaa\n\n\nbbb\n", "html": "

aaa

\n

bbb

\n", - "example": 221, - "start_line": 3557, - "end_line": 3565, + "example": 191, + "start_line": 3199, + "end_line": 3207, "section": "Paragraphs" }, { "markdown": " aaa\n bbb\n", "html": "

aaa\nbbb

\n", - "example": 222, - "start_line": 3570, - "end_line": 3576, + "example": 192, + "start_line": 3212, + "end_line": 3218, "section": "Paragraphs" }, { "markdown": "aaa\n bbb\n ccc\n", "html": "

aaa\nbbb\nccc

\n", - "example": 223, - "start_line": 3582, - "end_line": 3590, + "example": 193, + "start_line": 3224, + "end_line": 3232, "section": "Paragraphs" }, { "markdown": " aaa\nbbb\n", "html": "

aaa\nbbb

\n", - "example": 224, - "start_line": 3596, - "end_line": 3602, + "example": 194, + "start_line": 3238, + "end_line": 3244, "section": "Paragraphs" }, { "markdown": " aaa\nbbb\n", "html": "
aaa\n
\n

bbb

\n", - "example": 225, - "start_line": 3605, - "end_line": 3612, + "example": 195, + "start_line": 3247, + "end_line": 3254, "section": "Paragraphs" }, { "markdown": "aaa \nbbb \n", "html": "

aaa
\nbbb

\n", - "example": 226, - "start_line": 3619, - "end_line": 3625, + "example": 196, + "start_line": 3261, + "end_line": 3267, "section": "Paragraphs" }, { "markdown": " \n\naaa\n \n\n# aaa\n\n \n", "html": "

aaa

\n

aaa

\n", - "example": 227, - "start_line": 3636, - "end_line": 3648, + "example": 197, + "start_line": 3278, + "end_line": 3290, "section": "Blank lines" }, { "markdown": "> # Foo\n> bar\n> baz\n", "html": "
\n

Foo

\n

bar\nbaz

\n
\n", - "example": 228, - "start_line": 3704, - "end_line": 3714, + "example": 198, + "start_line": 3344, + "end_line": 3354, "section": "Block quotes" }, { "markdown": "># Foo\n>bar\n> baz\n", "html": "
\n

Foo

\n

bar\nbaz

\n
\n", - "example": 229, - "start_line": 3719, - "end_line": 3729, + "example": 199, + "start_line": 3359, + "end_line": 3369, "section": "Block quotes" }, { "markdown": " > # Foo\n > bar\n > baz\n", "html": "
\n

Foo

\n

bar\nbaz

\n
\n", - "example": 230, - "start_line": 3734, - "end_line": 3744, + "example": 200, + "start_line": 3374, + "end_line": 3384, "section": "Block quotes" }, { "markdown": " > # Foo\n > bar\n > baz\n", "html": "
> # Foo\n> bar\n> baz\n
\n", - "example": 231, - "start_line": 3749, - "end_line": 3758, + "example": 201, + "start_line": 3389, + "end_line": 3398, "section": "Block quotes" }, { "markdown": "> # Foo\n> bar\nbaz\n", "html": "
\n

Foo

\n

bar\nbaz

\n
\n", - "example": 232, - "start_line": 3764, - "end_line": 3774, + "example": 202, + "start_line": 3404, + "end_line": 3414, "section": "Block quotes" }, { "markdown": "> bar\nbaz\n> foo\n", "html": "
\n

bar\nbaz\nfoo

\n
\n", - "example": 233, - "start_line": 3780, - "end_line": 3790, + "example": 203, + "start_line": 3420, + "end_line": 3430, "section": "Block quotes" }, { "markdown": "> foo\n---\n", "html": "
\n

foo

\n
\n
\n", - "example": 234, - "start_line": 3804, - "end_line": 3812, + "example": 204, + "start_line": 3444, + "end_line": 3452, "section": "Block quotes" }, { "markdown": "> - foo\n- bar\n", "html": "
\n
    \n
  • foo
  • \n
\n
\n
    \n
  • bar
  • \n
\n", - "example": 235, - "start_line": 3824, - "end_line": 3836, + "example": 205, + "start_line": 3464, + "end_line": 3476, "section": "Block quotes" }, { "markdown": "> foo\n bar\n", "html": "
\n
foo\n
\n
\n
bar\n
\n", - "example": 236, - "start_line": 3842, - "end_line": 3852, + "example": 206, + "start_line": 3482, + "end_line": 3492, "section": "Block quotes" }, { "markdown": "> ```\nfoo\n```\n", "html": "
\n
\n
\n

foo

\n
\n", - "example": 237, - "start_line": 3855, - "end_line": 3865, + "example": 207, + "start_line": 3495, + "end_line": 3505, "section": "Block quotes" }, { "markdown": "> foo\n - bar\n", "html": "
\n

foo\n- bar

\n
\n", - "example": 238, - "start_line": 3871, - "end_line": 3879, + "example": 208, + "start_line": 3511, + "end_line": 3519, "section": "Block quotes" }, { "markdown": ">\n", "html": "
\n
\n", - "example": 239, - "start_line": 3895, - "end_line": 3900, + "example": 209, + "start_line": 3535, + "end_line": 3540, "section": "Block quotes" }, { "markdown": ">\n> \n> \n", "html": "
\n
\n", - "example": 240, - "start_line": 3903, - "end_line": 3910, + "example": 210, + "start_line": 3543, + "end_line": 3550, "section": "Block quotes" }, { "markdown": ">\n> foo\n> \n", "html": "
\n

foo

\n
\n", - "example": 241, - "start_line": 3915, - "end_line": 3923, + "example": 211, + "start_line": 3555, + "end_line": 3563, "section": "Block quotes" }, { "markdown": "> foo\n\n> bar\n", "html": "
\n

foo

\n
\n
\n

bar

\n
\n", - "example": 242, - "start_line": 3928, - "end_line": 3939, + "example": 212, + "start_line": 3568, + "end_line": 3579, "section": "Block quotes" }, { "markdown": "> foo\n> bar\n", "html": "
\n

foo\nbar

\n
\n", - "example": 243, - "start_line": 3950, - "end_line": 3958, + "example": 213, + "start_line": 3590, + "end_line": 3598, "section": "Block quotes" }, { "markdown": "> foo\n>\n> bar\n", "html": "
\n

foo

\n

bar

\n
\n", - "example": 244, - "start_line": 3963, - "end_line": 3972, + "example": 214, + "start_line": 3603, + "end_line": 3612, "section": "Block quotes" }, { "markdown": "foo\n> bar\n", "html": "

foo

\n
\n

bar

\n
\n", - "example": 245, - "start_line": 3977, - "end_line": 3985, + "example": 215, + "start_line": 3617, + "end_line": 3625, "section": "Block quotes" }, { "markdown": "> aaa\n***\n> bbb\n", "html": "
\n

aaa

\n
\n
\n
\n

bbb

\n
\n", - "example": 246, - "start_line": 3991, - "end_line": 4003, + "example": 216, + "start_line": 3631, + "end_line": 3643, "section": "Block quotes" }, { "markdown": "> bar\nbaz\n", "html": "
\n

bar\nbaz

\n
\n", - "example": 247, - "start_line": 4009, - "end_line": 4017, + "example": 217, + "start_line": 3649, + "end_line": 3657, "section": "Block quotes" }, { "markdown": "> bar\n\nbaz\n", "html": "
\n

bar

\n
\n

baz

\n", - "example": 248, - "start_line": 4020, - "end_line": 4029, + "example": 218, + "start_line": 3660, + "end_line": 3669, "section": "Block quotes" }, { "markdown": "> bar\n>\nbaz\n", "html": "
\n

bar

\n
\n

baz

\n", - "example": 249, - "start_line": 4032, - "end_line": 4041, + "example": 219, + "start_line": 3672, + "end_line": 3681, "section": "Block quotes" }, { "markdown": "> > > foo\nbar\n", "html": "
\n
\n
\n

foo\nbar

\n
\n
\n
\n", - "example": 250, - "start_line": 4048, - "end_line": 4060, + "example": 220, + "start_line": 3688, + "end_line": 3700, "section": "Block quotes" }, { "markdown": ">>> foo\n> bar\n>>baz\n", "html": "
\n
\n
\n

foo\nbar\nbaz

\n
\n
\n
\n", - "example": 251, - "start_line": 4063, - "end_line": 4077, + "example": 221, + "start_line": 3703, + "end_line": 3717, "section": "Block quotes" }, { "markdown": "> code\n\n> not code\n", "html": "
\n
code\n
\n
\n
\n

not code

\n
\n", - "example": 252, - "start_line": 4085, - "end_line": 4097, + "example": 222, + "start_line": 3725, + "end_line": 3737, "section": "Block quotes" }, { "markdown": "A paragraph\nwith two lines.\n\n indented code\n\n> A block quote.\n", "html": "

A paragraph\nwith two lines.

\n
indented code\n
\n
\n

A block quote.

\n
\n", - "example": 253, - "start_line": 4139, - "end_line": 4154, + "example": 223, + "start_line": 3779, + "end_line": 3794, "section": "List items" }, { "markdown": "1. A paragraph\n with two lines.\n\n indented code\n\n > A block quote.\n", "html": "
    \n
  1. \n

    A paragraph\nwith two lines.

    \n
    indented code\n
    \n
    \n

    A block quote.

    \n
    \n
  2. \n
\n", - "example": 254, - "start_line": 4161, - "end_line": 4180, + "example": 224, + "start_line": 3801, + "end_line": 3820, "section": "List items" }, { "markdown": "- one\n\n two\n", "html": "
    \n
  • one
  • \n
\n

two

\n", - "example": 255, - "start_line": 4194, - "end_line": 4203, + "example": 225, + "start_line": 3834, + "end_line": 3843, "section": "List items" }, { "markdown": "- one\n\n two\n", "html": "
    \n
  • \n

    one

    \n

    two

    \n
  • \n
\n", - "example": 256, - "start_line": 4206, - "end_line": 4217, + "example": 226, + "start_line": 3846, + "end_line": 3857, "section": "List items" }, { "markdown": " - one\n\n two\n", "html": "
    \n
  • one
  • \n
\n
 two\n
\n", - "example": 257, - "start_line": 4220, - "end_line": 4230, + "example": 227, + "start_line": 3860, + "end_line": 3870, "section": "List items" }, { "markdown": " - one\n\n two\n", "html": "
    \n
  • \n

    one

    \n

    two

    \n
  • \n
\n", - "example": 258, - "start_line": 4233, - "end_line": 4244, + "example": 228, + "start_line": 3873, + "end_line": 3884, "section": "List items" }, { "markdown": " > > 1. one\n>>\n>> two\n", "html": "
\n
\n
    \n
  1. \n

    one

    \n

    two

    \n
  2. \n
\n
\n
\n", - "example": 259, - "start_line": 4255, - "end_line": 4270, + "example": 229, + "start_line": 3895, + "end_line": 3910, "section": "List items" }, { "markdown": ">>- one\n>>\n > > two\n", "html": "
\n
\n
    \n
  • one
  • \n
\n

two

\n
\n
\n", - "example": 260, - "start_line": 4282, - "end_line": 4295, + "example": 230, + "start_line": 3922, + "end_line": 3935, "section": "List items" }, { "markdown": "-one\n\n2.two\n", "html": "

-one

\n

2.two

\n", - "example": 261, - "start_line": 4301, - "end_line": 4308, + "example": 231, + "start_line": 3941, + "end_line": 3948, "section": "List items" }, { "markdown": "- foo\n\n\n bar\n", "html": "
    \n
  • \n

    foo

    \n

    bar

    \n
  • \n
\n", - "example": 262, - "start_line": 4314, - "end_line": 4326, + "example": 232, + "start_line": 3954, + "end_line": 3966, "section": "List items" }, { "markdown": "1. foo\n\n ```\n bar\n ```\n\n baz\n\n > bam\n", "html": "
    \n
  1. \n

    foo

    \n
    bar\n
    \n

    baz

    \n
    \n

    bam

    \n
    \n
  2. \n
\n", - "example": 263, - "start_line": 4331, - "end_line": 4353, + "example": 233, + "start_line": 3971, + "end_line": 3993, "section": "List items" }, { "markdown": "- Foo\n\n bar\n\n\n baz\n", "html": "
    \n
  • \n

    Foo

    \n
    bar\n\n\nbaz\n
    \n
  • \n
\n", - "example": 264, - "start_line": 4359, - "end_line": 4377, + "example": 234, + "start_line": 3999, + "end_line": 4017, "section": "List items" }, { "markdown": "123456789. ok\n", "html": "
    \n
  1. ok
  2. \n
\n", - "example": 265, - "start_line": 4381, - "end_line": 4387, + "example": 235, + "start_line": 4021, + "end_line": 4027, "section": "List items" }, { "markdown": "1234567890. not ok\n", "html": "

1234567890. not ok

\n", - "example": 266, - "start_line": 4390, - "end_line": 4394, + "example": 236, + "start_line": 4030, + "end_line": 4034, "section": "List items" }, { "markdown": "0. ok\n", "html": "
    \n
  1. ok
  2. \n
\n", - "example": 267, - "start_line": 4399, - "end_line": 4405, + "example": 237, + "start_line": 4039, + "end_line": 4045, "section": "List items" }, { "markdown": "003. ok\n", "html": "
    \n
  1. ok
  2. \n
\n", - "example": 268, - "start_line": 4408, - "end_line": 4414, + "example": 238, + "start_line": 4048, + "end_line": 4054, "section": "List items" }, { "markdown": "-1. not ok\n", "html": "

-1. not ok

\n", - "example": 269, - "start_line": 4419, - "end_line": 4423, + "example": 239, + "start_line": 4059, + "end_line": 4063, "section": "List items" }, { "markdown": "- foo\n\n bar\n", "html": "
    \n
  • \n

    foo

    \n
    bar\n
    \n
  • \n
\n", - "example": 270, - "start_line": 4442, - "end_line": 4454, + "example": 240, + "start_line": 4082, + "end_line": 4094, "section": "List items" }, { "markdown": " 10. foo\n\n bar\n", "html": "
    \n
  1. \n

    foo

    \n
    bar\n
    \n
  2. \n
\n", - "example": 271, - "start_line": 4459, - "end_line": 4471, + "example": 241, + "start_line": 4099, + "end_line": 4111, "section": "List items" }, { "markdown": " indented code\n\nparagraph\n\n more code\n", "html": "
indented code\n
\n

paragraph

\n
more code\n
\n", - "example": 272, - "start_line": 4478, - "end_line": 4490, + "example": 242, + "start_line": 4118, + "end_line": 4130, "section": "List items" }, { "markdown": "1. indented code\n\n paragraph\n\n more code\n", "html": "
    \n
  1. \n
    indented code\n
    \n

    paragraph

    \n
    more code\n
    \n
  2. \n
\n", - "example": 273, - "start_line": 4493, - "end_line": 4509, + "example": 243, + "start_line": 4133, + "end_line": 4149, "section": "List items" }, { "markdown": "1. indented code\n\n paragraph\n\n more code\n", "html": "
    \n
  1. \n
     indented code\n
    \n

    paragraph

    \n
    more code\n
    \n
  2. \n
\n", - "example": 274, - "start_line": 4515, - "end_line": 4531, + "example": 244, + "start_line": 4155, + "end_line": 4171, "section": "List items" }, { "markdown": " foo\n\nbar\n", "html": "

foo

\n

bar

\n", - "example": 275, - "start_line": 4542, - "end_line": 4549, + "example": 245, + "start_line": 4182, + "end_line": 4189, "section": "List items" }, { "markdown": "- foo\n\n bar\n", "html": "
    \n
  • foo
  • \n
\n

bar

\n", - "example": 276, - "start_line": 4552, - "end_line": 4561, + "example": 246, + "start_line": 4192, + "end_line": 4201, "section": "List items" }, { "markdown": "- foo\n\n bar\n", "html": "
    \n
  • \n

    foo

    \n

    bar

    \n
  • \n
\n", - "example": 277, - "start_line": 4569, - "end_line": 4580, + "example": 247, + "start_line": 4209, + "end_line": 4220, "section": "List items" }, { "markdown": "-\n foo\n-\n ```\n bar\n ```\n-\n baz\n", "html": "
    \n
  • foo
  • \n
  • \n
    bar\n
    \n
  • \n
  • \n
    baz\n
    \n
  • \n
\n", - "example": 278, - "start_line": 4596, - "end_line": 4617, + "example": 248, + "start_line": 4237, + "end_line": 4258, "section": "List items" }, { "markdown": "- \n foo\n", "html": "
    \n
  • foo
  • \n
\n", - "example": 279, - "start_line": 4622, - "end_line": 4629, + "example": 249, + "start_line": 4263, + "end_line": 4270, "section": "List items" }, { "markdown": "-\n\n foo\n", "html": "
    \n
  • \n
\n

foo

\n", - "example": 280, - "start_line": 4636, - "end_line": 4645, + "example": 250, + "start_line": 4277, + "end_line": 4286, "section": "List items" }, { "markdown": "- foo\n-\n- bar\n", "html": "
    \n
  • foo
  • \n
  • \n
  • bar
  • \n
\n", - "example": 281, - "start_line": 4650, - "end_line": 4660, + "example": 251, + "start_line": 4291, + "end_line": 4301, "section": "List items" }, { "markdown": "- foo\n- \n- bar\n", "html": "
    \n
  • foo
  • \n
  • \n
  • bar
  • \n
\n", - "example": 282, - "start_line": 4665, - "end_line": 4675, + "example": 252, + "start_line": 4306, + "end_line": 4316, "section": "List items" }, { "markdown": "1. foo\n2.\n3. bar\n", "html": "
    \n
  1. foo
  2. \n
  3. \n
  4. bar
  5. \n
\n", - "example": 283, - "start_line": 4680, - "end_line": 4690, + "example": 253, + "start_line": 4321, + "end_line": 4331, "section": "List items" }, { "markdown": "*\n", "html": "
    \n
  • \n
\n", - "example": 284, - "start_line": 4695, - "end_line": 4701, + "example": 254, + "start_line": 4336, + "end_line": 4342, "section": "List items" }, { "markdown": "foo\n*\n\nfoo\n1.\n", "html": "

foo\n*

\n

foo\n1.

\n", - "example": 285, - "start_line": 4705, - "end_line": 4716, + "example": 255, + "start_line": 4346, + "end_line": 4357, "section": "List items" }, { "markdown": " 1. A paragraph\n with two lines.\n\n indented code\n\n > A block quote.\n", "html": "
    \n
  1. \n

    A paragraph\nwith two lines.

    \n
    indented code\n
    \n
    \n

    A block quote.

    \n
    \n
  2. \n
\n", - "example": 286, - "start_line": 4727, - "end_line": 4746, + "example": 256, + "start_line": 4368, + "end_line": 4387, "section": "List items" }, { "markdown": " 1. A paragraph\n with two lines.\n\n indented code\n\n > A block quote.\n", "html": "
    \n
  1. \n

    A paragraph\nwith two lines.

    \n
    indented code\n
    \n
    \n

    A block quote.

    \n
    \n
  2. \n
\n", - "example": 287, - "start_line": 4751, - "end_line": 4770, + "example": 257, + "start_line": 4392, + "end_line": 4411, "section": "List items" }, { "markdown": " 1. A paragraph\n with two lines.\n\n indented code\n\n > A block quote.\n", "html": "
    \n
  1. \n

    A paragraph\nwith two lines.

    \n
    indented code\n
    \n
    \n

    A block quote.

    \n
    \n
  2. \n
\n", - "example": 288, - "start_line": 4775, - "end_line": 4794, + "example": 258, + "start_line": 4416, + "end_line": 4435, "section": "List items" }, { "markdown": " 1. A paragraph\n with two lines.\n\n indented code\n\n > A block quote.\n", "html": "
1.  A paragraph\n    with two lines.\n\n        indented code\n\n    > A block quote.\n
\n", - "example": 289, - "start_line": 4799, - "end_line": 4814, + "example": 259, + "start_line": 4440, + "end_line": 4455, "section": "List items" }, { "markdown": " 1. A paragraph\nwith two lines.\n\n indented code\n\n > A block quote.\n", "html": "
    \n
  1. \n

    A paragraph\nwith two lines.

    \n
    indented code\n
    \n
    \n

    A block quote.

    \n
    \n
  2. \n
\n", - "example": 290, - "start_line": 4829, - "end_line": 4848, + "example": 260, + "start_line": 4470, + "end_line": 4489, "section": "List items" }, { "markdown": " 1. A paragraph\n with two lines.\n", "html": "
    \n
  1. A paragraph\nwith two lines.
  2. \n
\n", - "example": 291, - "start_line": 4853, - "end_line": 4861, + "example": 261, + "start_line": 4494, + "end_line": 4502, "section": "List items" }, { "markdown": "> 1. > Blockquote\ncontinued here.\n", "html": "
\n
    \n
  1. \n
    \n

    Blockquote\ncontinued here.

    \n
    \n
  2. \n
\n
\n", - "example": 292, - "start_line": 4866, - "end_line": 4880, + "example": 262, + "start_line": 4507, + "end_line": 4521, "section": "List items" }, { "markdown": "> 1. > Blockquote\n> continued here.\n", "html": "
\n
    \n
  1. \n
    \n

    Blockquote\ncontinued here.

    \n
    \n
  2. \n
\n
\n", - "example": 293, - "start_line": 4883, - "end_line": 4897, + "example": 263, + "start_line": 4524, + "end_line": 4538, "section": "List items" }, { "markdown": "- foo\n - bar\n - baz\n - boo\n", "html": "
    \n
  • foo\n
      \n
    • bar\n
        \n
      • baz\n
          \n
        • boo
        • \n
        \n
      • \n
      \n
    • \n
    \n
  • \n
\n", - "example": 294, - "start_line": 4911, - "end_line": 4932, + "example": 264, + "start_line": 4552, + "end_line": 4573, "section": "List items" }, { "markdown": "- foo\n - bar\n - baz\n - boo\n", "html": "
    \n
  • foo
  • \n
  • bar
  • \n
  • baz
  • \n
  • boo
  • \n
\n", - "example": 295, - "start_line": 4937, - "end_line": 4949, + "example": 265, + "start_line": 4578, + "end_line": 4590, "section": "List items" }, { "markdown": "10) foo\n - bar\n", "html": "
    \n
  1. foo\n
      \n
    • bar
    • \n
    \n
  2. \n
\n", - "example": 296, - "start_line": 4954, - "end_line": 4965, + "example": 266, + "start_line": 4595, + "end_line": 4606, "section": "List items" }, { "markdown": "10) foo\n - bar\n", "html": "
    \n
  1. foo
  2. \n
\n
    \n
  • bar
  • \n
\n", - "example": 297, - "start_line": 4970, - "end_line": 4980, + "example": 267, + "start_line": 4611, + "end_line": 4621, "section": "List items" }, { "markdown": "- - foo\n", "html": "
    \n
  • \n
      \n
    • foo
    • \n
    \n
  • \n
\n", - "example": 298, - "start_line": 4985, - "end_line": 4995, + "example": 268, + "start_line": 4626, + "end_line": 4636, "section": "List items" }, { "markdown": "1. - 2. foo\n", "html": "
    \n
  1. \n
      \n
    • \n
        \n
      1. foo
      2. \n
      \n
    • \n
    \n
  2. \n
\n", - "example": 299, - "start_line": 4998, - "end_line": 5012, + "example": 269, + "start_line": 4639, + "end_line": 4653, "section": "List items" }, { "markdown": "- # Foo\n- Bar\n ---\n baz\n", "html": "
    \n
  • \n

    Foo

    \n
  • \n
  • \n

    Bar

    \nbaz
  • \n
\n", - "example": 300, - "start_line": 5017, - "end_line": 5031, + "example": 270, + "start_line": 4658, + "end_line": 4672, "section": "List items" }, { "markdown": "- foo\n- bar\n+ baz\n", "html": "
    \n
  • foo
  • \n
  • bar
  • \n
\n
    \n
  • baz
  • \n
\n", - "example": 301, - "start_line": 5253, - "end_line": 5265, + "example": 271, + "start_line": 4894, + "end_line": 4906, "section": "Lists" }, { "markdown": "1. foo\n2. bar\n3) baz\n", "html": "
    \n
  1. foo
  2. \n
  3. bar
  4. \n
\n
    \n
  1. baz
  2. \n
\n", - "example": 302, - "start_line": 5268, - "end_line": 5280, + "example": 272, + "start_line": 4909, + "end_line": 4921, "section": "Lists" }, { "markdown": "Foo\n- bar\n- baz\n", "html": "

Foo

\n
    \n
  • bar
  • \n
  • baz
  • \n
\n", - "example": 303, - "start_line": 5287, - "end_line": 5297, + "example": 273, + "start_line": 4928, + "end_line": 4938, "section": "Lists" }, { "markdown": "The number of windows in my house is\n14. The number of doors is 6.\n", "html": "

The number of windows in my house is\n14. The number of doors is 6.

\n", - "example": 304, - "start_line": 5364, - "end_line": 5370, + "example": 274, + "start_line": 5005, + "end_line": 5011, "section": "Lists" }, { "markdown": "The number of windows in my house is\n1. The number of doors is 6.\n", "html": "

The number of windows in my house is

\n
    \n
  1. The number of doors is 6.
  2. \n
\n", - "example": 305, - "start_line": 5374, - "end_line": 5382, + "example": 275, + "start_line": 5015, + "end_line": 5023, "section": "Lists" }, { "markdown": "- foo\n\n- bar\n\n\n- baz\n", "html": "
    \n
  • \n

    foo

    \n
  • \n
  • \n

    bar

    \n
  • \n
  • \n

    baz

    \n
  • \n
\n", - "example": 306, - "start_line": 5388, - "end_line": 5407, + "example": 276, + "start_line": 5029, + "end_line": 5048, "section": "Lists" }, { "markdown": "- foo\n - bar\n - baz\n\n\n bim\n", "html": "
    \n
  • foo\n
      \n
    • bar\n
        \n
      • \n

        baz

        \n

        bim

        \n
      • \n
      \n
    • \n
    \n
  • \n
\n", - "example": 307, - "start_line": 5409, - "end_line": 5431, + "example": 277, + "start_line": 5050, + "end_line": 5072, "section": "Lists" }, { "markdown": "- foo\n- bar\n\n\n\n- baz\n- bim\n", "html": "
    \n
  • foo
  • \n
  • bar
  • \n
\n\n
    \n
  • baz
  • \n
  • bim
  • \n
\n", - "example": 308, - "start_line": 5439, - "end_line": 5457, + "example": 278, + "start_line": 5080, + "end_line": 5098, "section": "Lists" }, { "markdown": "- foo\n\n notcode\n\n- foo\n\n\n\n code\n", "html": "
    \n
  • \n

    foo

    \n

    notcode

    \n
  • \n
  • \n

    foo

    \n
  • \n
\n\n
code\n
\n", - "example": 309, - "start_line": 5460, - "end_line": 5483, + "example": 279, + "start_line": 5101, + "end_line": 5124, "section": "Lists" }, { "markdown": "- a\n - b\n - c\n - d\n - e\n - f\n- g\n", "html": "
    \n
  • a
  • \n
  • b
  • \n
  • c
  • \n
  • d
  • \n
  • e
  • \n
  • f
  • \n
  • g
  • \n
\n", - "example": 310, - "start_line": 5491, - "end_line": 5509, + "example": 280, + "start_line": 5132, + "end_line": 5150, "section": "Lists" }, { "markdown": "1. a\n\n 2. b\n\n 3. c\n", "html": "
    \n
  1. \n

    a

    \n
  2. \n
  3. \n

    b

    \n
  4. \n
  5. \n

    c

    \n
  6. \n
\n", - "example": 311, - "start_line": 5512, - "end_line": 5530, + "example": 281, + "start_line": 5153, + "end_line": 5171, "section": "Lists" }, { "markdown": "- a\n - b\n - c\n - d\n - e\n", "html": "
    \n
  • a
  • \n
  • b
  • \n
  • c
  • \n
  • d\n- e
  • \n
\n", - "example": 312, - "start_line": 5536, - "end_line": 5550, + "example": 282, + "start_line": 5177, + "end_line": 5191, "section": "Lists" }, { "markdown": "1. a\n\n 2. b\n\n 3. c\n", "html": "
    \n
  1. \n

    a

    \n
  2. \n
  3. \n

    b

    \n
  4. \n
\n
3. c\n
\n", - "example": 313, - "start_line": 5556, - "end_line": 5573, + "example": 283, + "start_line": 5197, + "end_line": 5214, "section": "Lists" }, { "markdown": "- a\n- b\n\n- c\n", "html": "
    \n
  • \n

    a

    \n
  • \n
  • \n

    b

    \n
  • \n
  • \n

    c

    \n
  • \n
\n", - "example": 314, - "start_line": 5579, - "end_line": 5596, + "example": 284, + "start_line": 5220, + "end_line": 5237, "section": "Lists" }, { "markdown": "* a\n*\n\n* c\n", "html": "
    \n
  • \n

    a

    \n
  • \n
  • \n
  • \n

    c

    \n
  • \n
\n", - "example": 315, - "start_line": 5601, - "end_line": 5616, + "example": 285, + "start_line": 5242, + "end_line": 5257, "section": "Lists" }, { "markdown": "- a\n- b\n\n c\n- d\n", "html": "
    \n
  • \n

    a

    \n
  • \n
  • \n

    b

    \n

    c

    \n
  • \n
  • \n

    d

    \n
  • \n
\n", - "example": 316, - "start_line": 5623, - "end_line": 5642, + "example": 286, + "start_line": 5264, + "end_line": 5283, "section": "Lists" }, { "markdown": "- a\n- b\n\n [ref]: /url\n- d\n", "html": "
    \n
  • \n

    a

    \n
  • \n
  • \n

    b

    \n
  • \n
  • \n

    d

    \n
  • \n
\n", - "example": 317, - "start_line": 5645, - "end_line": 5663, + "example": 287, + "start_line": 5286, + "end_line": 5304, "section": "Lists" }, { "markdown": "- a\n- ```\n b\n\n\n ```\n- c\n", "html": "
    \n
  • a
  • \n
  • \n
    b\n\n\n
    \n
  • \n
  • c
  • \n
\n", - "example": 318, - "start_line": 5668, - "end_line": 5687, + "example": 288, + "start_line": 5309, + "end_line": 5328, "section": "Lists" }, { "markdown": "- a\n - b\n\n c\n- d\n", "html": "
    \n
  • a\n
      \n
    • \n

      b

      \n

      c

      \n
    • \n
    \n
  • \n
  • d
  • \n
\n", - "example": 319, - "start_line": 5694, - "end_line": 5712, + "example": 289, + "start_line": 5335, + "end_line": 5353, "section": "Lists" }, { "markdown": "* a\n > b\n >\n* c\n", "html": "
    \n
  • a\n
    \n

    b

    \n
    \n
  • \n
  • c
  • \n
\n", - "example": 320, - "start_line": 5718, - "end_line": 5732, + "example": 290, + "start_line": 5359, + "end_line": 5373, "section": "Lists" }, { "markdown": "- a\n > b\n ```\n c\n ```\n- d\n", "html": "
    \n
  • a\n
    \n

    b

    \n
    \n
    c\n
    \n
  • \n
  • d
  • \n
\n", - "example": 321, - "start_line": 5738, - "end_line": 5756, + "example": 291, + "start_line": 5379, + "end_line": 5397, "section": "Lists" }, { "markdown": "- a\n", "html": "
    \n
  • a
  • \n
\n", - "example": 322, - "start_line": 5761, - "end_line": 5767, + "example": 292, + "start_line": 5402, + "end_line": 5408, "section": "Lists" }, { "markdown": "- a\n - b\n", "html": "
    \n
  • a\n
      \n
    • b
    • \n
    \n
  • \n
\n", - "example": 323, - "start_line": 5770, - "end_line": 5781, + "example": 293, + "start_line": 5411, + "end_line": 5422, "section": "Lists" }, { "markdown": "1. ```\n foo\n ```\n\n bar\n", "html": "
    \n
  1. \n
    foo\n
    \n

    bar

    \n
  2. \n
\n", - "example": 324, - "start_line": 5787, - "end_line": 5801, + "example": 294, + "start_line": 5428, + "end_line": 5442, "section": "Lists" }, { "markdown": "* foo\n * bar\n\n baz\n", "html": "
    \n
  • \n

    foo

    \n
      \n
    • bar
    • \n
    \n

    baz

    \n
  • \n
\n", - "example": 325, - "start_line": 5806, - "end_line": 5821, + "example": 295, + "start_line": 5447, + "end_line": 5462, "section": "Lists" }, { "markdown": "- a\n - b\n - c\n\n- d\n - e\n - f\n", "html": "
    \n
  • \n

    a

    \n
      \n
    • b
    • \n
    • c
    • \n
    \n
  • \n
  • \n

    d

    \n
      \n
    • e
    • \n
    • f
    • \n
    \n
  • \n
\n", - "example": 326, - "start_line": 5824, - "end_line": 5849, + "example": 296, + "start_line": 5465, + "end_line": 5490, "section": "Lists" }, { "markdown": "`hi`lo`\n", "html": "

hilo`

\n", - "example": 327, - "start_line": 5858, - "end_line": 5862, + "example": 297, + "start_line": 5499, + "end_line": 5503, "section": "Inlines" }, + { + "markdown": "\\!\\\"\\#\\$\\%\\&\\'\\(\\)\\*\\+\\,\\-\\.\\/\\:\\;\\<\\=\\>\\?\\@\\[\\\\\\]\\^\\_\\`\\{\\|\\}\\~\n", + "html": "

!"#$%&'()*+,-./:;<=>?@[\\]^_`{|}~

\n", + "example": 298, + "start_line": 5513, + "end_line": 5517, + "section": "Backslash escapes" + }, + { + "markdown": "\\\t\\A\\a\\ \\3\\φ\\«\n", + "html": "

\\\t\\A\\a\\ \\3\\φ\\«

\n", + "example": 299, + "start_line": 5523, + "end_line": 5527, + "section": "Backslash escapes" + }, + { + "markdown": "\\*not emphasized*\n\\
not a tag\n\\[not a link](/foo)\n\\`not code`\n1\\. not a list\n\\* not a list\n\\# not a heading\n\\[foo]: /url \"not a reference\"\n\\ö not a character entity\n", + "html": "

*not emphasized*\n<br/> not a tag\n[not a link](/foo)\n`not code`\n1. not a list\n* not a list\n# not a heading\n[foo]: /url "not a reference"\n&ouml; not a character entity

\n", + "example": 300, + "start_line": 5533, + "end_line": 5553, + "section": "Backslash escapes" + }, + { + "markdown": "\\\\*emphasis*\n", + "html": "

\\emphasis

\n", + "example": 301, + "start_line": 5558, + "end_line": 5562, + "section": "Backslash escapes" + }, + { + "markdown": "foo\\\nbar\n", + "html": "

foo
\nbar

\n", + "example": 302, + "start_line": 5567, + "end_line": 5573, + "section": "Backslash escapes" + }, + { + "markdown": "`` \\[\\` ``\n", + "html": "

\\[\\`

\n", + "example": 303, + "start_line": 5579, + "end_line": 5583, + "section": "Backslash escapes" + }, + { + "markdown": " \\[\\]\n", + "html": "
\\[\\]\n
\n", + "example": 304, + "start_line": 5586, + "end_line": 5591, + "section": "Backslash escapes" + }, + { + "markdown": "~~~\n\\[\\]\n~~~\n", + "html": "
\\[\\]\n
\n", + "example": 305, + "start_line": 5594, + "end_line": 5601, + "section": "Backslash escapes" + }, + { + "markdown": "\n", + "html": "

http://example.com?find=\\*

\n", + "example": 306, + "start_line": 5604, + "end_line": 5608, + "section": "Backslash escapes" + }, + { + "markdown": "\n", + "html": "\n", + "example": 307, + "start_line": 5611, + "end_line": 5615, + "section": "Backslash escapes" + }, + { + "markdown": "[foo](/bar\\* \"ti\\*tle\")\n", + "html": "

foo

\n", + "example": 308, + "start_line": 5621, + "end_line": 5625, + "section": "Backslash escapes" + }, + { + "markdown": "[foo]\n\n[foo]: /bar\\* \"ti\\*tle\"\n", + "html": "

foo

\n", + "example": 309, + "start_line": 5628, + "end_line": 5634, + "section": "Backslash escapes" + }, + { + "markdown": "``` foo\\+bar\nfoo\n```\n", + "html": "
foo\n
\n", + "example": 310, + "start_line": 5637, + "end_line": 5644, + "section": "Backslash escapes" + }, + { + "markdown": "  & © Æ Ď\n¾ ℋ ⅆ\n∲ ≧̸\n", + "html": "

  & © Æ Ď\n¾ ℋ ⅆ\n∲ ≧̸

\n", + "example": 311, + "start_line": 5674, + "end_line": 5682, + "section": "Entity and numeric character references" + }, + { + "markdown": "# Ӓ Ϡ �\n", + "html": "

# Ӓ Ϡ �

\n", + "example": 312, + "start_line": 5693, + "end_line": 5697, + "section": "Entity and numeric character references" + }, + { + "markdown": "" ആ ಫ\n", + "html": "

" ആ ಫ

\n", + "example": 313, + "start_line": 5706, + "end_line": 5710, + "section": "Entity and numeric character references" + }, + { + "markdown": "  &x; &#; &#x;\n�\n&#abcdef0;\n&ThisIsNotDefined; &hi?;\n", + "html": "

&nbsp &x; &#; &#x;\n&#987654321;\n&#abcdef0;\n&ThisIsNotDefined; &hi?;

\n", + "example": 314, + "start_line": 5715, + "end_line": 5725, + "section": "Entity and numeric character references" + }, + { + "markdown": "©\n", + "html": "

&copy

\n", + "example": 315, + "start_line": 5732, + "end_line": 5736, + "section": "Entity and numeric character references" + }, + { + "markdown": "&MadeUpEntity;\n", + "html": "

&MadeUpEntity;

\n", + "example": 316, + "start_line": 5742, + "end_line": 5746, + "section": "Entity and numeric character references" + }, + { + "markdown": "\n", + "html": "\n", + "example": 317, + "start_line": 5753, + "end_line": 5757, + "section": "Entity and numeric character references" + }, + { + "markdown": "[foo](/föö \"föö\")\n", + "html": "

foo

\n", + "example": 318, + "start_line": 5760, + "end_line": 5764, + "section": "Entity and numeric character references" + }, + { + "markdown": "[foo]\n\n[foo]: /föö \"föö\"\n", + "html": "

foo

\n", + "example": 319, + "start_line": 5767, + "end_line": 5773, + "section": "Entity and numeric character references" + }, + { + "markdown": "``` föö\nfoo\n```\n", + "html": "
foo\n
\n", + "example": 320, + "start_line": 5776, + "end_line": 5783, + "section": "Entity and numeric character references" + }, + { + "markdown": "`föö`\n", + "html": "

f&ouml;&ouml;

\n", + "example": 321, + "start_line": 5789, + "end_line": 5793, + "section": "Entity and numeric character references" + }, + { + "markdown": " föfö\n", + "html": "
f&ouml;f&ouml;\n
\n", + "example": 322, + "start_line": 5796, + "end_line": 5801, + "section": "Entity and numeric character references" + }, + { + "markdown": "*foo*\n*foo*\n", + "html": "

*foo*\nfoo

\n", + "example": 323, + "start_line": 5808, + "end_line": 5814, + "section": "Entity and numeric character references" + }, + { + "markdown": "* foo\n\n* foo\n", + "html": "

* foo

\n
    \n
  • foo
  • \n
\n", + "example": 324, + "start_line": 5816, + "end_line": 5825, + "section": "Entity and numeric character references" + }, + { + "markdown": "foo bar\n", + "html": "

foo\n\nbar

\n", + "example": 325, + "start_line": 5827, + "end_line": 5833, + "section": "Entity and numeric character references" + }, + { + "markdown": " foo\n", + "html": "

\tfoo

\n", + "example": 326, + "start_line": 5835, + "end_line": 5839, + "section": "Entity and numeric character references" + }, + { + "markdown": "[a](url "tit")\n", + "html": "

[a](url "tit")

\n", + "example": 327, + "start_line": 5842, + "end_line": 5846, + "section": "Entity and numeric character references" + }, { "markdown": "`foo`\n", "html": "

foo

\n", "example": 328, - "start_line": 5890, - "end_line": 5894, + "start_line": 5870, + "end_line": 5874, "section": "Code spans" }, { "markdown": "`` foo ` bar ``\n", "html": "

foo ` bar

\n", "example": 329, - "start_line": 5901, - "end_line": 5905, + "start_line": 5881, + "end_line": 5885, "section": "Code spans" }, { "markdown": "` `` `\n", "html": "

``

\n", "example": 330, - "start_line": 5911, - "end_line": 5915, + "start_line": 5891, + "end_line": 5895, "section": "Code spans" }, { "markdown": "` `` `\n", "html": "

``

\n", "example": 331, - "start_line": 5919, - "end_line": 5923, + "start_line": 5899, + "end_line": 5903, "section": "Code spans" }, { "markdown": "` a`\n", "html": "

a

\n", "example": 332, - "start_line": 5928, - "end_line": 5932, + "start_line": 5908, + "end_line": 5912, "section": "Code spans" }, { "markdown": "` b `\n", "html": "

 b 

\n", "example": 333, - "start_line": 5937, - "end_line": 5941, + "start_line": 5917, + "end_line": 5921, "section": "Code spans" }, { "markdown": "` `\n` `\n", "html": "

 \n

\n", "example": 334, - "start_line": 5945, - "end_line": 5951, + "start_line": 5925, + "end_line": 5931, "section": "Code spans" }, { "markdown": "``\nfoo\nbar \nbaz\n``\n", "html": "

foo bar baz

\n", "example": 335, - "start_line": 5956, - "end_line": 5964, + "start_line": 5936, + "end_line": 5944, "section": "Code spans" }, { "markdown": "``\nfoo \n``\n", "html": "

foo

\n", "example": 336, - "start_line": 5966, - "end_line": 5972, + "start_line": 5946, + "end_line": 5952, "section": "Code spans" }, { "markdown": "`foo bar \nbaz`\n", "html": "

foo bar baz

\n", "example": 337, - "start_line": 5977, - "end_line": 5982, + "start_line": 5957, + "end_line": 5962, "section": "Code spans" }, { "markdown": "`foo\\`bar`\n", "html": "

foo\\bar`

\n", "example": 338, - "start_line": 5994, - "end_line": 5998, + "start_line": 5974, + "end_line": 5978, "section": "Code spans" }, { "markdown": "``foo`bar``\n", "html": "

foo`bar

\n", "example": 339, - "start_line": 6005, - "end_line": 6009, + "start_line": 5985, + "end_line": 5989, "section": "Code spans" }, { "markdown": "` foo `` bar `\n", "html": "

foo `` bar

\n", "example": 340, - "start_line": 6011, - "end_line": 6015, + "start_line": 5991, + "end_line": 5995, "section": "Code spans" }, { "markdown": "*foo`*`\n", "html": "

*foo*

\n", "example": 341, - "start_line": 6023, - "end_line": 6027, + "start_line": 6003, + "end_line": 6007, "section": "Code spans" }, { "markdown": "[not a `link](/foo`)\n", "html": "

[not a link](/foo)

\n", "example": 342, - "start_line": 6032, - "end_line": 6036, + "start_line": 6012, + "end_line": 6016, "section": "Code spans" }, { "markdown": "``\n", "html": "

<a href="">`

\n", "example": 343, - "start_line": 6042, - "end_line": 6046, + "start_line": 6022, + "end_line": 6026, "section": "Code spans" }, { "markdown": "
`\n", "html": "

`

\n", "example": 344, - "start_line": 6051, - "end_line": 6055, + "start_line": 6031, + "end_line": 6035, "section": "Code spans" }, { "markdown": "``\n", "html": "

<http://foo.bar.baz>`

\n", "example": 345, - "start_line": 6060, - "end_line": 6064, + "start_line": 6040, + "end_line": 6044, "section": "Code spans" }, { "markdown": "`\n", "html": "

http://foo.bar.`baz`

\n", "example": 346, - "start_line": 6069, - "end_line": 6073, + "start_line": 6049, + "end_line": 6053, "section": "Code spans" }, { "markdown": "```foo``\n", "html": "

```foo``

\n", "example": 347, - "start_line": 6079, - "end_line": 6083, + "start_line": 6059, + "end_line": 6063, "section": "Code spans" }, { "markdown": "`foo\n", "html": "

`foo

\n", "example": 348, - "start_line": 6086, - "end_line": 6090, + "start_line": 6066, + "end_line": 6070, "section": "Code spans" }, { "markdown": "`foo``bar``\n", "html": "

`foobar

\n", "example": 349, - "start_line": 6095, - "end_line": 6099, + "start_line": 6075, + "end_line": 6079, "section": "Code spans" }, { "markdown": "*foo bar*\n", "html": "

foo bar

\n", "example": 350, - "start_line": 6312, - "end_line": 6316, + "start_line": 6292, + "end_line": 6296, "section": "Emphasis and strong emphasis" }, { "markdown": "a * foo bar*\n", "html": "

a * foo bar*

\n", "example": 351, - "start_line": 6322, - "end_line": 6326, + "start_line": 6302, + "end_line": 6306, "section": "Emphasis and strong emphasis" }, { "markdown": "a*\"foo\"*\n", "html": "

a*"foo"*

\n", "example": 352, - "start_line": 6333, - "end_line": 6337, + "start_line": 6313, + "end_line": 6317, "section": "Emphasis and strong emphasis" }, { "markdown": "* a *\n", "html": "

* a *

\n", "example": 353, - "start_line": 6342, - "end_line": 6346, + "start_line": 6322, + "end_line": 6326, "section": "Emphasis and strong emphasis" }, { "markdown": "foo*bar*\n", "html": "

foobar

\n", "example": 354, - "start_line": 6351, - "end_line": 6355, + "start_line": 6331, + "end_line": 6335, "section": "Emphasis and strong emphasis" }, { "markdown": "5*6*78\n", "html": "

5678

\n", "example": 355, - "start_line": 6358, - "end_line": 6362, + "start_line": 6338, + "end_line": 6342, "section": "Emphasis and strong emphasis" }, { "markdown": "_foo bar_\n", "html": "

foo bar

\n", "example": 356, - "start_line": 6367, - "end_line": 6371, + "start_line": 6347, + "end_line": 6351, "section": "Emphasis and strong emphasis" }, { "markdown": "_ foo bar_\n", "html": "

_ foo bar_

\n", "example": 357, - "start_line": 6377, - "end_line": 6381, + "start_line": 6357, + "end_line": 6361, "section": "Emphasis and strong emphasis" }, { "markdown": "a_\"foo\"_\n", "html": "

a_"foo"_

\n", "example": 358, - "start_line": 6387, - "end_line": 6391, + "start_line": 6367, + "end_line": 6371, "section": "Emphasis and strong emphasis" }, { "markdown": "foo_bar_\n", "html": "

foo_bar_

\n", "example": 359, - "start_line": 6396, - "end_line": 6400, + "start_line": 6376, + "end_line": 6380, "section": "Emphasis and strong emphasis" }, { "markdown": "5_6_78\n", "html": "

5_6_78

\n", "example": 360, - "start_line": 6403, - "end_line": 6407, + "start_line": 6383, + "end_line": 6387, "section": "Emphasis and strong emphasis" }, { "markdown": "пристаням_стремятся_\n", "html": "

пристаням_стремятся_

\n", "example": 361, - "start_line": 6410, - "end_line": 6414, + "start_line": 6390, + "end_line": 6394, "section": "Emphasis and strong emphasis" }, { "markdown": "aa_\"bb\"_cc\n", "html": "

aa_"bb"_cc

\n", "example": 362, - "start_line": 6420, - "end_line": 6424, + "start_line": 6400, + "end_line": 6404, "section": "Emphasis and strong emphasis" }, { "markdown": "foo-_(bar)_\n", "html": "

foo-(bar)

\n", "example": 363, - "start_line": 6431, - "end_line": 6435, + "start_line": 6411, + "end_line": 6415, "section": "Emphasis and strong emphasis" }, { "markdown": "_foo*\n", "html": "

_foo*

\n", "example": 364, - "start_line": 6443, - "end_line": 6447, + "start_line": 6423, + "end_line": 6427, "section": "Emphasis and strong emphasis" }, { "markdown": "*foo bar *\n", "html": "

*foo bar *

\n", "example": 365, - "start_line": 6453, - "end_line": 6457, + "start_line": 6433, + "end_line": 6437, "section": "Emphasis and strong emphasis" }, { "markdown": "*foo bar\n*\n", "html": "

*foo bar\n*

\n", "example": 366, - "start_line": 6462, - "end_line": 6468, + "start_line": 6442, + "end_line": 6448, "section": "Emphasis and strong emphasis" }, { "markdown": "*(*foo)\n", "html": "

*(*foo)

\n", "example": 367, - "start_line": 6475, - "end_line": 6479, + "start_line": 6455, + "end_line": 6459, "section": "Emphasis and strong emphasis" }, { "markdown": "*(*foo*)*\n", "html": "

(foo)

\n", "example": 368, - "start_line": 6485, - "end_line": 6489, + "start_line": 6465, + "end_line": 6469, "section": "Emphasis and strong emphasis" }, { "markdown": "*foo*bar\n", "html": "

foobar

\n", "example": 369, - "start_line": 6494, - "end_line": 6498, + "start_line": 6474, + "end_line": 6478, "section": "Emphasis and strong emphasis" }, { "markdown": "_foo bar _\n", "html": "

_foo bar _

\n", "example": 370, - "start_line": 6507, - "end_line": 6511, + "start_line": 6487, + "end_line": 6491, "section": "Emphasis and strong emphasis" }, { "markdown": "_(_foo)\n", "html": "

_(_foo)

\n", "example": 371, - "start_line": 6517, - "end_line": 6521, + "start_line": 6497, + "end_line": 6501, "section": "Emphasis and strong emphasis" }, { "markdown": "_(_foo_)_\n", "html": "

(foo)

\n", "example": 372, - "start_line": 6526, - "end_line": 6530, + "start_line": 6506, + "end_line": 6510, "section": "Emphasis and strong emphasis" }, { "markdown": "_foo_bar\n", "html": "

_foo_bar

\n", "example": 373, - "start_line": 6535, - "end_line": 6539, + "start_line": 6515, + "end_line": 6519, "section": "Emphasis and strong emphasis" }, { "markdown": "_пристаням_стремятся\n", "html": "

_пристаням_стремятся

\n", "example": 374, - "start_line": 6542, - "end_line": 6546, + "start_line": 6522, + "end_line": 6526, "section": "Emphasis and strong emphasis" }, { "markdown": "_foo_bar_baz_\n", "html": "

foo_bar_baz

\n", "example": 375, - "start_line": 6549, - "end_line": 6553, + "start_line": 6529, + "end_line": 6533, "section": "Emphasis and strong emphasis" }, { "markdown": "_(bar)_.\n", "html": "

(bar).

\n", "example": 376, - "start_line": 6560, - "end_line": 6564, + "start_line": 6540, + "end_line": 6544, "section": "Emphasis and strong emphasis" }, { "markdown": "**foo bar**\n", "html": "

foo bar

\n", "example": 377, - "start_line": 6569, - "end_line": 6573, + "start_line": 6549, + "end_line": 6553, "section": "Emphasis and strong emphasis" }, { "markdown": "** foo bar**\n", "html": "

** foo bar**

\n", "example": 378, - "start_line": 6579, - "end_line": 6583, + "start_line": 6559, + "end_line": 6563, "section": "Emphasis and strong emphasis" }, { "markdown": "a**\"foo\"**\n", "html": "

a**"foo"**

\n", "example": 379, - "start_line": 6590, - "end_line": 6594, + "start_line": 6570, + "end_line": 6574, "section": "Emphasis and strong emphasis" }, { "markdown": "foo**bar**\n", "html": "

foobar

\n", "example": 380, - "start_line": 6599, - "end_line": 6603, + "start_line": 6579, + "end_line": 6583, "section": "Emphasis and strong emphasis" }, { "markdown": "__foo bar__\n", "html": "

foo bar

\n", "example": 381, - "start_line": 6608, - "end_line": 6612, + "start_line": 6588, + "end_line": 6592, "section": "Emphasis and strong emphasis" }, { "markdown": "__ foo bar__\n", "html": "

__ foo bar__

\n", "example": 382, - "start_line": 6618, - "end_line": 6622, + "start_line": 6598, + "end_line": 6602, "section": "Emphasis and strong emphasis" }, { "markdown": "__\nfoo bar__\n", "html": "

__\nfoo bar__

\n", "example": 383, - "start_line": 6626, - "end_line": 6632, + "start_line": 6606, + "end_line": 6612, "section": "Emphasis and strong emphasis" }, { "markdown": "a__\"foo\"__\n", "html": "

a__"foo"__

\n", "example": 384, - "start_line": 6638, - "end_line": 6642, + "start_line": 6618, + "end_line": 6622, "section": "Emphasis and strong emphasis" }, { "markdown": "foo__bar__\n", "html": "

foo__bar__

\n", "example": 385, - "start_line": 6647, - "end_line": 6651, + "start_line": 6627, + "end_line": 6631, "section": "Emphasis and strong emphasis" }, { "markdown": "5__6__78\n", "html": "

5__6__78

\n", "example": 386, - "start_line": 6654, - "end_line": 6658, + "start_line": 6634, + "end_line": 6638, "section": "Emphasis and strong emphasis" }, { "markdown": "пристаням__стремятся__\n", "html": "

пристаням__стремятся__

\n", "example": 387, - "start_line": 6661, - "end_line": 6665, + "start_line": 6641, + "end_line": 6645, "section": "Emphasis and strong emphasis" }, { "markdown": "__foo, __bar__, baz__\n", "html": "

foo, bar, baz

\n", "example": 388, - "start_line": 6668, - "end_line": 6672, + "start_line": 6648, + "end_line": 6652, "section": "Emphasis and strong emphasis" }, { "markdown": "foo-__(bar)__\n", "html": "

foo-(bar)

\n", "example": 389, - "start_line": 6679, - "end_line": 6683, + "start_line": 6659, + "end_line": 6663, "section": "Emphasis and strong emphasis" }, { "markdown": "**foo bar **\n", "html": "

**foo bar **

\n", "example": 390, - "start_line": 6692, - "end_line": 6696, + "start_line": 6672, + "end_line": 6676, "section": "Emphasis and strong emphasis" }, { "markdown": "**(**foo)\n", "html": "

**(**foo)

\n", "example": 391, - "start_line": 6705, - "end_line": 6709, + "start_line": 6685, + "end_line": 6689, "section": "Emphasis and strong emphasis" }, { "markdown": "*(**foo**)*\n", "html": "

(foo)

\n", "example": 392, - "start_line": 6715, - "end_line": 6719, + "start_line": 6695, + "end_line": 6699, "section": "Emphasis and strong emphasis" }, { "markdown": "**Gomphocarpus (*Gomphocarpus physocarpus*, syn.\n*Asclepias physocarpa*)**\n", "html": "

Gomphocarpus (Gomphocarpus physocarpus, syn.\nAsclepias physocarpa)

\n", "example": 393, - "start_line": 6722, - "end_line": 6728, + "start_line": 6702, + "end_line": 6708, "section": "Emphasis and strong emphasis" }, { "markdown": "**foo \"*bar*\" foo**\n", "html": "

foo "bar" foo

\n", "example": 394, - "start_line": 6731, - "end_line": 6735, + "start_line": 6711, + "end_line": 6715, "section": "Emphasis and strong emphasis" }, { "markdown": "**foo**bar\n", "html": "

foobar

\n", "example": 395, - "start_line": 6740, - "end_line": 6744, + "start_line": 6720, + "end_line": 6724, "section": "Emphasis and strong emphasis" }, { "markdown": "__foo bar __\n", "html": "

__foo bar __

\n", "example": 396, - "start_line": 6752, - "end_line": 6756, + "start_line": 6732, + "end_line": 6736, "section": "Emphasis and strong emphasis" }, { "markdown": "__(__foo)\n", "html": "

__(__foo)

\n", "example": 397, - "start_line": 6762, - "end_line": 6766, + "start_line": 6742, + "end_line": 6746, "section": "Emphasis and strong emphasis" }, { "markdown": "_(__foo__)_\n", "html": "

(foo)

\n", "example": 398, - "start_line": 6772, - "end_line": 6776, + "start_line": 6752, + "end_line": 6756, "section": "Emphasis and strong emphasis" }, { "markdown": "__foo__bar\n", "html": "

__foo__bar

\n", "example": 399, - "start_line": 6781, - "end_line": 6785, + "start_line": 6761, + "end_line": 6765, "section": "Emphasis and strong emphasis" }, { "markdown": "__пристаням__стремятся\n", "html": "

__пристаням__стремятся

\n", "example": 400, - "start_line": 6788, - "end_line": 6792, + "start_line": 6768, + "end_line": 6772, "section": "Emphasis and strong emphasis" }, { "markdown": "__foo__bar__baz__\n", "html": "

foo__bar__baz

\n", "example": 401, - "start_line": 6795, - "end_line": 6799, + "start_line": 6775, + "end_line": 6779, "section": "Emphasis and strong emphasis" }, { "markdown": "__(bar)__.\n", "html": "

(bar).

\n", "example": 402, - "start_line": 6806, - "end_line": 6810, + "start_line": 6786, + "end_line": 6790, "section": "Emphasis and strong emphasis" }, { "markdown": "*foo [bar](/url)*\n", "html": "

foo bar

\n", "example": 403, - "start_line": 6818, - "end_line": 6822, + "start_line": 6798, + "end_line": 6802, "section": "Emphasis and strong emphasis" }, { "markdown": "*foo\nbar*\n", "html": "

foo\nbar

\n", "example": 404, - "start_line": 6825, - "end_line": 6831, + "start_line": 6805, + "end_line": 6811, "section": "Emphasis and strong emphasis" }, { "markdown": "_foo __bar__ baz_\n", "html": "

foo bar baz

\n", "example": 405, - "start_line": 6837, - "end_line": 6841, + "start_line": 6817, + "end_line": 6821, "section": "Emphasis and strong emphasis" }, { "markdown": "_foo _bar_ baz_\n", "html": "

foo bar baz

\n", "example": 406, - "start_line": 6844, - "end_line": 6848, + "start_line": 6824, + "end_line": 6828, "section": "Emphasis and strong emphasis" }, { "markdown": "__foo_ bar_\n", "html": "

foo bar

\n", "example": 407, - "start_line": 6851, - "end_line": 6855, + "start_line": 6831, + "end_line": 6835, "section": "Emphasis and strong emphasis" }, { "markdown": "*foo *bar**\n", "html": "

foo bar

\n", "example": 408, - "start_line": 6858, - "end_line": 6862, + "start_line": 6838, + "end_line": 6842, "section": "Emphasis and strong emphasis" }, { "markdown": "*foo **bar** baz*\n", "html": "

foo bar baz

\n", "example": 409, - "start_line": 6865, - "end_line": 6869, + "start_line": 6845, + "end_line": 6849, "section": "Emphasis and strong emphasis" }, { "markdown": "*foo**bar**baz*\n", "html": "

foobarbaz

\n", "example": 410, - "start_line": 6871, - "end_line": 6875, + "start_line": 6851, + "end_line": 6855, "section": "Emphasis and strong emphasis" }, { "markdown": "*foo**bar*\n", "html": "

foo**bar

\n", "example": 411, - "start_line": 6895, - "end_line": 6899, + "start_line": 6875, + "end_line": 6879, "section": "Emphasis and strong emphasis" }, { "markdown": "***foo** bar*\n", "html": "

foo bar

\n", "example": 412, - "start_line": 6908, - "end_line": 6912, + "start_line": 6888, + "end_line": 6892, "section": "Emphasis and strong emphasis" }, { "markdown": "*foo **bar***\n", "html": "

foo bar

\n", "example": 413, - "start_line": 6915, - "end_line": 6919, + "start_line": 6895, + "end_line": 6899, "section": "Emphasis and strong emphasis" }, { "markdown": "*foo**bar***\n", "html": "

foobar

\n", "example": 414, - "start_line": 6922, - "end_line": 6926, + "start_line": 6902, + "end_line": 6906, "section": "Emphasis and strong emphasis" }, { "markdown": "foo***bar***baz\n", "html": "

foobarbaz

\n", "example": 415, - "start_line": 6933, - "end_line": 6937, + "start_line": 6913, + "end_line": 6917, "section": "Emphasis and strong emphasis" }, { "markdown": "foo******bar*********baz\n", "html": "

foobar***baz

\n", "example": 416, - "start_line": 6939, - "end_line": 6943, + "start_line": 6919, + "end_line": 6923, "section": "Emphasis and strong emphasis" }, { "markdown": "*foo **bar *baz* bim** bop*\n", "html": "

foo bar baz bim bop

\n", "example": 417, - "start_line": 6948, - "end_line": 6952, + "start_line": 6928, + "end_line": 6932, "section": "Emphasis and strong emphasis" }, { "markdown": "*foo [*bar*](/url)*\n", "html": "

foo bar

\n", "example": 418, - "start_line": 6955, - "end_line": 6959, + "start_line": 6935, + "end_line": 6939, "section": "Emphasis and strong emphasis" }, { "markdown": "** is not an empty emphasis\n", "html": "

** is not an empty emphasis

\n", "example": 419, - "start_line": 6964, - "end_line": 6968, + "start_line": 6944, + "end_line": 6948, "section": "Emphasis and strong emphasis" }, { "markdown": "**** is not an empty strong emphasis\n", "html": "

**** is not an empty strong emphasis

\n", "example": 420, - "start_line": 6971, - "end_line": 6975, + "start_line": 6951, + "end_line": 6955, "section": "Emphasis and strong emphasis" }, { "markdown": "**foo [bar](/url)**\n", "html": "

foo bar

\n", "example": 421, - "start_line": 6984, - "end_line": 6988, + "start_line": 6964, + "end_line": 6968, "section": "Emphasis and strong emphasis" }, { "markdown": "**foo\nbar**\n", "html": "

foo\nbar

\n", "example": 422, - "start_line": 6991, - "end_line": 6997, + "start_line": 6971, + "end_line": 6977, "section": "Emphasis and strong emphasis" }, { "markdown": "__foo _bar_ baz__\n", "html": "

foo bar baz

\n", "example": 423, - "start_line": 7003, - "end_line": 7007, + "start_line": 6983, + "end_line": 6987, "section": "Emphasis and strong emphasis" }, { "markdown": "__foo __bar__ baz__\n", "html": "

foo bar baz

\n", "example": 424, - "start_line": 7010, - "end_line": 7014, + "start_line": 6990, + "end_line": 6994, "section": "Emphasis and strong emphasis" }, { "markdown": "____foo__ bar__\n", "html": "

foo bar

\n", "example": 425, - "start_line": 7017, - "end_line": 7021, + "start_line": 6997, + "end_line": 7001, "section": "Emphasis and strong emphasis" }, { "markdown": "**foo **bar****\n", "html": "

foo bar

\n", "example": 426, - "start_line": 7024, - "end_line": 7028, + "start_line": 7004, + "end_line": 7008, "section": "Emphasis and strong emphasis" }, { "markdown": "**foo *bar* baz**\n", "html": "

foo bar baz

\n", "example": 427, - "start_line": 7031, - "end_line": 7035, + "start_line": 7011, + "end_line": 7015, "section": "Emphasis and strong emphasis" }, { "markdown": "**foo*bar*baz**\n", "html": "

foobarbaz

\n", "example": 428, - "start_line": 7038, - "end_line": 7042, + "start_line": 7018, + "end_line": 7022, "section": "Emphasis and strong emphasis" }, { "markdown": "***foo* bar**\n", "html": "

foo bar

\n", "example": 429, - "start_line": 7045, - "end_line": 7049, + "start_line": 7025, + "end_line": 7029, "section": "Emphasis and strong emphasis" }, { "markdown": "**foo *bar***\n", "html": "

foo bar

\n", "example": 430, - "start_line": 7052, - "end_line": 7056, + "start_line": 7032, + "end_line": 7036, "section": "Emphasis and strong emphasis" }, { "markdown": "**foo *bar **baz**\nbim* bop**\n", "html": "

foo bar baz\nbim bop

\n", "example": 431, - "start_line": 7061, - "end_line": 7067, + "start_line": 7041, + "end_line": 7047, "section": "Emphasis and strong emphasis" }, { "markdown": "**foo [*bar*](/url)**\n", "html": "

foo bar

\n", "example": 432, - "start_line": 7070, - "end_line": 7074, + "start_line": 7050, + "end_line": 7054, "section": "Emphasis and strong emphasis" }, { "markdown": "__ is not an empty emphasis\n", "html": "

__ is not an empty emphasis

\n", "example": 433, - "start_line": 7079, - "end_line": 7083, + "start_line": 7059, + "end_line": 7063, "section": "Emphasis and strong emphasis" }, { "markdown": "____ is not an empty strong emphasis\n", "html": "

____ is not an empty strong emphasis

\n", "example": 434, - "start_line": 7086, - "end_line": 7090, + "start_line": 7066, + "end_line": 7070, "section": "Emphasis and strong emphasis" }, { "markdown": "foo ***\n", "html": "

foo ***

\n", "example": 435, - "start_line": 7096, - "end_line": 7100, + "start_line": 7076, + "end_line": 7080, "section": "Emphasis and strong emphasis" }, { "markdown": "foo *\\**\n", "html": "

foo *

\n", "example": 436, - "start_line": 7103, - "end_line": 7107, + "start_line": 7083, + "end_line": 7087, "section": "Emphasis and strong emphasis" }, { "markdown": "foo *_*\n", "html": "

foo _

\n", "example": 437, - "start_line": 7110, - "end_line": 7114, + "start_line": 7090, + "end_line": 7094, "section": "Emphasis and strong emphasis" }, { "markdown": "foo *****\n", "html": "

foo *****

\n", "example": 438, - "start_line": 7117, - "end_line": 7121, + "start_line": 7097, + "end_line": 7101, "section": "Emphasis and strong emphasis" }, { "markdown": "foo **\\***\n", "html": "

foo *

\n", "example": 439, - "start_line": 7124, - "end_line": 7128, + "start_line": 7104, + "end_line": 7108, "section": "Emphasis and strong emphasis" }, { "markdown": "foo **_**\n", "html": "

foo _

\n", "example": 440, - "start_line": 7131, - "end_line": 7135, + "start_line": 7111, + "end_line": 7115, "section": "Emphasis and strong emphasis" }, { "markdown": "**foo*\n", "html": "

*foo

\n", "example": 441, - "start_line": 7142, - "end_line": 7146, + "start_line": 7122, + "end_line": 7126, "section": "Emphasis and strong emphasis" }, { "markdown": "*foo**\n", "html": "

foo*

\n", "example": 442, - "start_line": 7149, - "end_line": 7153, + "start_line": 7129, + "end_line": 7133, "section": "Emphasis and strong emphasis" }, { "markdown": "***foo**\n", "html": "

*foo

\n", "example": 443, - "start_line": 7156, - "end_line": 7160, + "start_line": 7136, + "end_line": 7140, "section": "Emphasis and strong emphasis" }, { "markdown": "****foo*\n", "html": "

***foo

\n", "example": 444, - "start_line": 7163, - "end_line": 7167, + "start_line": 7143, + "end_line": 7147, "section": "Emphasis and strong emphasis" }, { "markdown": "**foo***\n", "html": "

foo*

\n", "example": 445, - "start_line": 7170, - "end_line": 7174, + "start_line": 7150, + "end_line": 7154, "section": "Emphasis and strong emphasis" }, { "markdown": "*foo****\n", "html": "

foo***

\n", "example": 446, - "start_line": 7177, - "end_line": 7181, + "start_line": 7157, + "end_line": 7161, "section": "Emphasis and strong emphasis" }, { "markdown": "foo ___\n", "html": "

foo ___

\n", "example": 447, - "start_line": 7187, - "end_line": 7191, + "start_line": 7167, + "end_line": 7171, "section": "Emphasis and strong emphasis" }, { "markdown": "foo _\\__\n", "html": "

foo _

\n", "example": 448, - "start_line": 7194, - "end_line": 7198, + "start_line": 7174, + "end_line": 7178, "section": "Emphasis and strong emphasis" }, { "markdown": "foo _*_\n", "html": "

foo *

\n", "example": 449, - "start_line": 7201, - "end_line": 7205, + "start_line": 7181, + "end_line": 7185, "section": "Emphasis and strong emphasis" }, { "markdown": "foo _____\n", "html": "

foo _____

\n", "example": 450, - "start_line": 7208, - "end_line": 7212, + "start_line": 7188, + "end_line": 7192, "section": "Emphasis and strong emphasis" }, { "markdown": "foo __\\___\n", "html": "

foo _

\n", "example": 451, - "start_line": 7215, - "end_line": 7219, + "start_line": 7195, + "end_line": 7199, "section": "Emphasis and strong emphasis" }, { "markdown": "foo __*__\n", "html": "

foo *

\n", "example": 452, - "start_line": 7222, - "end_line": 7226, + "start_line": 7202, + "end_line": 7206, "section": "Emphasis and strong emphasis" }, { "markdown": "__foo_\n", "html": "

_foo

\n", "example": 453, - "start_line": 7229, - "end_line": 7233, + "start_line": 7209, + "end_line": 7213, "section": "Emphasis and strong emphasis" }, { "markdown": "_foo__\n", "html": "

foo_

\n", "example": 454, - "start_line": 7240, - "end_line": 7244, + "start_line": 7220, + "end_line": 7224, "section": "Emphasis and strong emphasis" }, { "markdown": "___foo__\n", "html": "

_foo

\n", "example": 455, - "start_line": 7247, - "end_line": 7251, + "start_line": 7227, + "end_line": 7231, "section": "Emphasis and strong emphasis" }, { "markdown": "____foo_\n", "html": "

___foo

\n", "example": 456, - "start_line": 7254, - "end_line": 7258, + "start_line": 7234, + "end_line": 7238, "section": "Emphasis and strong emphasis" }, { "markdown": "__foo___\n", "html": "

foo_

\n", "example": 457, - "start_line": 7261, - "end_line": 7265, + "start_line": 7241, + "end_line": 7245, "section": "Emphasis and strong emphasis" }, { "markdown": "_foo____\n", "html": "

foo___

\n", "example": 458, - "start_line": 7268, - "end_line": 7272, + "start_line": 7248, + "end_line": 7252, "section": "Emphasis and strong emphasis" }, { "markdown": "**foo**\n", "html": "

foo

\n", "example": 459, - "start_line": 7278, - "end_line": 7282, + "start_line": 7258, + "end_line": 7262, "section": "Emphasis and strong emphasis" }, { "markdown": "*_foo_*\n", "html": "

foo

\n", "example": 460, - "start_line": 7285, - "end_line": 7289, + "start_line": 7265, + "end_line": 7269, "section": "Emphasis and strong emphasis" }, { "markdown": "__foo__\n", "html": "

foo

\n", "example": 461, - "start_line": 7292, - "end_line": 7296, + "start_line": 7272, + "end_line": 7276, "section": "Emphasis and strong emphasis" }, { "markdown": "_*foo*_\n", "html": "

foo

\n", "example": 462, - "start_line": 7299, - "end_line": 7303, + "start_line": 7279, + "end_line": 7283, "section": "Emphasis and strong emphasis" }, { "markdown": "****foo****\n", "html": "

foo

\n", "example": 463, - "start_line": 7309, - "end_line": 7313, + "start_line": 7289, + "end_line": 7293, "section": "Emphasis and strong emphasis" }, { "markdown": "____foo____\n", "html": "

foo

\n", "example": 464, - "start_line": 7316, - "end_line": 7320, + "start_line": 7296, + "end_line": 7300, "section": "Emphasis and strong emphasis" }, { "markdown": "******foo******\n", "html": "

foo

\n", "example": 465, - "start_line": 7327, - "end_line": 7331, + "start_line": 7307, + "end_line": 7311, "section": "Emphasis and strong emphasis" }, { "markdown": "***foo***\n", "html": "

foo

\n", "example": 466, - "start_line": 7336, - "end_line": 7340, + "start_line": 7316, + "end_line": 7320, "section": "Emphasis and strong emphasis" }, { "markdown": "_____foo_____\n", "html": "

foo

\n", "example": 467, - "start_line": 7343, - "end_line": 7347, + "start_line": 7323, + "end_line": 7327, "section": "Emphasis and strong emphasis" }, { "markdown": "*foo _bar* baz_\n", "html": "

foo _bar baz_

\n", "example": 468, - "start_line": 7352, - "end_line": 7356, + "start_line": 7332, + "end_line": 7336, "section": "Emphasis and strong emphasis" }, { "markdown": "*foo __bar *baz bim__ bam*\n", "html": "

foo bar *baz bim bam

\n", "example": 469, - "start_line": 7359, - "end_line": 7363, + "start_line": 7339, + "end_line": 7343, "section": "Emphasis and strong emphasis" }, { "markdown": "**foo **bar baz**\n", "html": "

**foo bar baz

\n", "example": 470, - "start_line": 7368, - "end_line": 7372, + "start_line": 7348, + "end_line": 7352, "section": "Emphasis and strong emphasis" }, { "markdown": "*foo *bar baz*\n", "html": "

*foo bar baz

\n", "example": 471, - "start_line": 7375, - "end_line": 7379, + "start_line": 7355, + "end_line": 7359, "section": "Emphasis and strong emphasis" }, { "markdown": "*[bar*](/url)\n", "html": "

*bar*

\n", "example": 472, - "start_line": 7384, - "end_line": 7388, + "start_line": 7364, + "end_line": 7368, "section": "Emphasis and strong emphasis" }, { "markdown": "_foo [bar_](/url)\n", "html": "

_foo bar_

\n", "example": 473, - "start_line": 7391, - "end_line": 7395, + "start_line": 7371, + "end_line": 7375, "section": "Emphasis and strong emphasis" }, { "markdown": "*\n", "html": "

*

\n", "example": 474, - "start_line": 7398, - "end_line": 7402, + "start_line": 7378, + "end_line": 7382, "section": "Emphasis and strong emphasis" }, { "markdown": "**\n", "html": "

**

\n", "example": 475, - "start_line": 7405, - "end_line": 7409, + "start_line": 7385, + "end_line": 7389, "section": "Emphasis and strong emphasis" }, { "markdown": "__\n", "html": "

__

\n", "example": 476, - "start_line": 7412, - "end_line": 7416, + "start_line": 7392, + "end_line": 7396, "section": "Emphasis and strong emphasis" }, { "markdown": "*a `*`*\n", "html": "

a *

\n", "example": 477, - "start_line": 7419, - "end_line": 7423, + "start_line": 7399, + "end_line": 7403, "section": "Emphasis and strong emphasis" }, { "markdown": "_a `_`_\n", "html": "

a _

\n", "example": 478, - "start_line": 7426, - "end_line": 7430, + "start_line": 7406, + "end_line": 7410, "section": "Emphasis and strong emphasis" }, { "markdown": "**a\n", "html": "

**ahttp://foo.bar/?q=**

\n", "example": 479, - "start_line": 7433, - "end_line": 7437, + "start_line": 7413, + "end_line": 7417, "section": "Emphasis and strong emphasis" }, { "markdown": "__a\n", "html": "

__ahttp://foo.bar/?q=__

\n", "example": 480, - "start_line": 7440, - "end_line": 7444, + "start_line": 7420, + "end_line": 7424, "section": "Emphasis and strong emphasis" }, { "markdown": "[link](/uri \"title\")\n", "html": "

link

\n", "example": 481, - "start_line": 7528, - "end_line": 7532, + "start_line": 7503, + "end_line": 7507, "section": "Links" }, { "markdown": "[link](/uri)\n", "html": "

link

\n", "example": 482, - "start_line": 7538, - "end_line": 7542, - "section": "Links" - }, - { - "markdown": "[](./target.md)\n", - "html": "

\n", - "example": 483, - "start_line": 7544, - "end_line": 7548, + "start_line": 7512, + "end_line": 7516, "section": "Links" }, { "markdown": "[link]()\n", "html": "

link

\n", - "example": 484, - "start_line": 7551, - "end_line": 7555, + "example": 483, + "start_line": 7521, + "end_line": 7525, "section": "Links" }, { "markdown": "[link](<>)\n", "html": "

link

\n", - "example": 485, - "start_line": 7558, - "end_line": 7562, - "section": "Links" - }, - { - "markdown": "[]()\n", - "html": "

\n", - "example": 486, - "start_line": 7565, - "end_line": 7569, + "example": 484, + "start_line": 7528, + "end_line": 7532, "section": "Links" }, { "markdown": "[link](/my uri)\n", "html": "

[link](/my uri)

\n", - "example": 487, - "start_line": 7574, - "end_line": 7578, + "example": 485, + "start_line": 7537, + "end_line": 7541, "section": "Links" }, { "markdown": "[link](
)\n", "html": "

link

\n", - "example": 488, - "start_line": 7580, - "end_line": 7584, + "example": 486, + "start_line": 7543, + "end_line": 7547, "section": "Links" }, { "markdown": "[link](foo\nbar)\n", "html": "

[link](foo\nbar)

\n", - "example": 489, - "start_line": 7589, - "end_line": 7595, + "example": 487, + "start_line": 7552, + "end_line": 7558, "section": "Links" }, { "markdown": "[link]()\n", "html": "

[link]()

\n", - "example": 490, - "start_line": 7597, - "end_line": 7603, + "example": 488, + "start_line": 7560, + "end_line": 7566, "section": "Links" }, { "markdown": "[a]()\n", "html": "

a

\n", - "example": 491, - "start_line": 7608, - "end_line": 7612, + "example": 489, + "start_line": 7571, + "end_line": 7575, "section": "Links" }, { "markdown": "[link]()\n", "html": "

[link](<foo>)

\n", - "example": 492, - "start_line": 7616, - "end_line": 7620, + "example": 490, + "start_line": 7579, + "end_line": 7583, "section": "Links" }, { "markdown": "[a](\n[a](c)\n", "html": "

[a](<b)c\n[a](<b)c>\n[a](c)

\n", - "example": 493, - "start_line": 7625, - "end_line": 7633, + "example": 491, + "start_line": 7588, + "end_line": 7596, "section": "Links" }, { "markdown": "[link](\\(foo\\))\n", "html": "

link

\n", - "example": 494, - "start_line": 7637, - "end_line": 7641, + "example": 492, + "start_line": 7600, + "end_line": 7604, "section": "Links" }, { "markdown": "[link](foo(and(bar)))\n", "html": "

link

\n", - "example": 495, - "start_line": 7646, - "end_line": 7650, - "section": "Links" - }, - { - "markdown": "[link](foo(and(bar))\n", - "html": "

[link](foo(and(bar))

\n", - "example": 496, - "start_line": 7655, - "end_line": 7659, + "example": 493, + "start_line": 7609, + "end_line": 7613, "section": "Links" }, { "markdown": "[link](foo\\(and\\(bar\\))\n", "html": "

link

\n", - "example": 497, - "start_line": 7662, - "end_line": 7666, + "example": 494, + "start_line": 7618, + "end_line": 7622, "section": "Links" }, { "markdown": "[link]()\n", "html": "

link

\n", - "example": 498, - "start_line": 7669, - "end_line": 7673, + "example": 495, + "start_line": 7625, + "end_line": 7629, "section": "Links" }, { "markdown": "[link](foo\\)\\:)\n", "html": "

link

\n", - "example": 499, - "start_line": 7679, - "end_line": 7683, + "example": 496, + "start_line": 7635, + "end_line": 7639, "section": "Links" }, { "markdown": "[link](#fragment)\n\n[link](http://example.com#fragment)\n\n[link](http://example.com?foo=3#frag)\n", "html": "

link

\n

link

\n

link

\n", - "example": 500, - "start_line": 7688, - "end_line": 7698, + "example": 497, + "start_line": 7644, + "end_line": 7654, "section": "Links" }, { "markdown": "[link](foo\\bar)\n", "html": "

link

\n", - "example": 501, - "start_line": 7704, - "end_line": 7708, + "example": 498, + "start_line": 7660, + "end_line": 7664, "section": "Links" }, { "markdown": "[link](foo%20bä)\n", "html": "

link

\n", - "example": 502, - "start_line": 7720, - "end_line": 7724, + "example": 499, + "start_line": 7676, + "end_line": 7680, "section": "Links" }, { "markdown": "[link](\"title\")\n", "html": "

link

\n", - "example": 503, - "start_line": 7731, - "end_line": 7735, + "example": 500, + "start_line": 7687, + "end_line": 7691, "section": "Links" }, { "markdown": "[link](/url \"title\")\n[link](/url 'title')\n[link](/url (title))\n", "html": "

link\nlink\nlink

\n", - "example": 504, - "start_line": 7740, - "end_line": 7748, + "example": 501, + "start_line": 7696, + "end_line": 7704, "section": "Links" }, { "markdown": "[link](/url \"title \\\""\")\n", "html": "

link

\n", - "example": 505, - "start_line": 7754, - "end_line": 7758, + "example": 502, + "start_line": 7710, + "end_line": 7714, "section": "Links" }, { "markdown": "[link](/url \"title\")\n", "html": "

link

\n", - "example": 506, - "start_line": 7765, - "end_line": 7769, + "example": 503, + "start_line": 7720, + "end_line": 7724, "section": "Links" }, { "markdown": "[link](/url \"title \"and\" title\")\n", "html": "

[link](/url "title "and" title")

\n", - "example": 507, - "start_line": 7774, - "end_line": 7778, + "example": 504, + "start_line": 7729, + "end_line": 7733, "section": "Links" }, { "markdown": "[link](/url 'title \"and\" title')\n", "html": "

link

\n", + "example": 505, + "start_line": 7738, + "end_line": 7742, + "section": "Links" + }, + { + "markdown": "[link]( /uri\n \"title\" )\n", + "html": "

link

\n", + "example": 506, + "start_line": 7762, + "end_line": 7767, + "section": "Links" + }, + { + "markdown": "[link] (/uri)\n", + "html": "

[link] (/uri)

\n", + "example": 507, + "start_line": 7773, + "end_line": 7777, + "section": "Links" + }, + { + "markdown": "[link [foo [bar]]](/uri)\n", + "html": "

link [foo [bar]]

\n", "example": 508, "start_line": 7783, "end_line": 7787, "section": "Links" }, { - "markdown": "[link]( /uri\n \"title\" )\n", - "html": "

link

\n", - "example": 509, - "start_line": 7808, - "end_line": 7813, - "section": "Links" - }, - { - "markdown": "[link] (/uri)\n", - "html": "

[link] (/uri)

\n", - "example": 510, - "start_line": 7819, - "end_line": 7823, - "section": "Links" - }, - { - "markdown": "[link [foo [bar]]](/uri)\n", - "html": "

link [foo [bar]]

\n", - "example": 511, - "start_line": 7829, - "end_line": 7833, - "section": "Links" - }, - { "markdown": "[link] bar](/uri)\n", "html": "

[link] bar](/uri)

\n", - "example": 512, - "start_line": 7836, - "end_line": 7840, + "example": 509, + "start_line": 7790, + "end_line": 7794, "section": "Links" }, { "markdown": "[link [bar](/uri)\n", "html": "

[link bar

\n", - "example": 513, - "start_line": 7843, - "end_line": 7847, + "example": 510, + "start_line": 7797, + "end_line": 7801, "section": "Links" }, { "markdown": "[link \\[bar](/uri)\n", "html": "

link [bar

\n", - "example": 514, - "start_line": 7850, - "end_line": 7854, + "example": 511, + "start_line": 7804, + "end_line": 7808, "section": "Links" }, { "markdown": "[link *foo **bar** `#`*](/uri)\n", "html": "

link foo bar #

\n", - "example": 515, - "start_line": 7859, - "end_line": 7863, + "example": 512, + "start_line": 7813, + "end_line": 7817, "section": "Links" }, { "markdown": "[![moon](moon.jpg)](/uri)\n", "html": "

\"moon\"

\n", - "example": 516, - "start_line": 7866, - "end_line": 7870, + "example": 513, + "start_line": 7820, + "end_line": 7824, "section": "Links" }, { "markdown": "[foo [bar](/uri)](/uri)\n", "html": "

[foo bar](/uri)

\n", - "example": 517, - "start_line": 7875, - "end_line": 7879, + "example": 514, + "start_line": 7829, + "end_line": 7833, "section": "Links" }, { "markdown": "[foo *[bar [baz](/uri)](/uri)*](/uri)\n", "html": "

[foo [bar baz](/uri)](/uri)

\n", - "example": 518, - "start_line": 7882, - "end_line": 7886, + "example": 515, + "start_line": 7836, + "end_line": 7840, "section": "Links" }, { "markdown": "![[[foo](uri1)](uri2)](uri3)\n", "html": "

\"[foo](uri2)\"

\n", - "example": 519, - "start_line": 7889, - "end_line": 7893, + "example": 516, + "start_line": 7843, + "end_line": 7847, "section": "Links" }, { "markdown": "*[foo*](/uri)\n", "html": "

*foo*

\n", - "example": 520, - "start_line": 7899, - "end_line": 7903, + "example": 517, + "start_line": 7853, + "end_line": 7857, "section": "Links" }, { "markdown": "[foo *bar](baz*)\n", "html": "

foo *bar

\n", - "example": 521, - "start_line": 7906, - "end_line": 7910, + "example": 518, + "start_line": 7860, + "end_line": 7864, "section": "Links" }, { "markdown": "*foo [bar* baz]\n", "html": "

foo [bar baz]

\n", - "example": 522, - "start_line": 7916, - "end_line": 7920, + "example": 519, + "start_line": 7870, + "end_line": 7874, "section": "Links" }, { "markdown": "[foo \n", "html": "

[foo

\n", - "example": 523, - "start_line": 7926, - "end_line": 7930, + "example": 520, + "start_line": 7880, + "end_line": 7884, "section": "Links" }, { "markdown": "[foo`](/uri)`\n", "html": "

[foo](/uri)

\n", - "example": 524, - "start_line": 7933, - "end_line": 7937, + "example": 521, + "start_line": 7887, + "end_line": 7891, "section": "Links" }, { "markdown": "[foo\n", "html": "

[foohttp://example.com/?search=](uri)

\n", - "example": 525, - "start_line": 7940, - "end_line": 7944, + "example": 522, + "start_line": 7894, + "end_line": 7898, "section": "Links" }, { "markdown": "[foo][bar]\n\n[bar]: /url \"title\"\n", "html": "

foo

\n", - "example": 526, - "start_line": 7978, - "end_line": 7984, + "example": 523, + "start_line": 7932, + "end_line": 7938, "section": "Links" }, { "markdown": "[link [foo [bar]]][ref]\n\n[ref]: /uri\n", "html": "

link [foo [bar]]

\n", - "example": 527, - "start_line": 7993, - "end_line": 7999, + "example": 524, + "start_line": 7947, + "end_line": 7953, "section": "Links" }, { "markdown": "[link \\[bar][ref]\n\n[ref]: /uri\n", "html": "

link [bar

\n", - "example": 528, - "start_line": 8002, - "end_line": 8008, + "example": 525, + "start_line": 7956, + "end_line": 7962, "section": "Links" }, { "markdown": "[link *foo **bar** `#`*][ref]\n\n[ref]: /uri\n", "html": "

link foo bar #

\n", - "example": 529, - "start_line": 8013, - "end_line": 8019, + "example": 526, + "start_line": 7967, + "end_line": 7973, "section": "Links" }, { "markdown": "[![moon](moon.jpg)][ref]\n\n[ref]: /uri\n", "html": "

\"moon\"

\n", - "example": 530, - "start_line": 8022, - "end_line": 8028, + "example": 527, + "start_line": 7976, + "end_line": 7982, "section": "Links" }, { "markdown": "[foo [bar](/uri)][ref]\n\n[ref]: /uri\n", "html": "

[foo bar]ref

\n", - "example": 531, - "start_line": 8033, - "end_line": 8039, + "example": 528, + "start_line": 7987, + "end_line": 7993, "section": "Links" }, { "markdown": "[foo *bar [baz][ref]*][ref]\n\n[ref]: /uri\n", "html": "

[foo bar baz]ref

\n", - "example": 532, - "start_line": 8042, - "end_line": 8048, + "example": 529, + "start_line": 7996, + "end_line": 8002, "section": "Links" }, { "markdown": "*[foo*][ref]\n\n[ref]: /uri\n", "html": "

*foo*

\n", - "example": 533, - "start_line": 8057, - "end_line": 8063, + "example": 530, + "start_line": 8011, + "end_line": 8017, "section": "Links" }, { - "markdown": "[foo *bar][ref]*\n\n[ref]: /uri\n", - "html": "

foo *bar*

\n", - "example": 534, - "start_line": 8066, - "end_line": 8072, + "markdown": "[foo *bar][ref]\n\n[ref]: /uri\n", + "html": "

foo *bar

\n", + "example": 531, + "start_line": 8020, + "end_line": 8026, "section": "Links" }, { "markdown": "[foo \n\n[ref]: /uri\n", "html": "

[foo

\n", - "example": 535, - "start_line": 8078, - "end_line": 8084, + "example": 532, + "start_line": 8032, + "end_line": 8038, "section": "Links" }, { "markdown": "[foo`][ref]`\n\n[ref]: /uri\n", "html": "

[foo][ref]

\n", - "example": 536, - "start_line": 8087, - "end_line": 8093, + "example": 533, + "start_line": 8041, + "end_line": 8047, "section": "Links" }, { "markdown": "[foo\n\n[ref]: /uri\n", "html": "

[foohttp://example.com/?search=][ref]

\n", - "example": 537, - "start_line": 8096, - "end_line": 8102, + "example": 534, + "start_line": 8050, + "end_line": 8056, "section": "Links" }, { "markdown": "[foo][BaR]\n\n[bar]: /url \"title\"\n", "html": "

foo

\n", - "example": 538, - "start_line": 8107, - "end_line": 8113, + "example": 535, + "start_line": 8061, + "end_line": 8067, "section": "Links" }, { - "markdown": "[ẞ]\n\n[SS]: /url\n", - "html": "

\n", - "example": 539, - "start_line": 8118, - "end_line": 8124, + "markdown": "[Толпой][Толпой] is a Russian word.\n\n[ТОЛПОЙ]: /url\n", + "html": "

Толпой is a Russian word.

\n", + "example": 536, + "start_line": 8072, + "end_line": 8078, "section": "Links" }, { "markdown": "[Foo\n bar]: /url\n\n[Baz][Foo bar]\n", "html": "

Baz

\n", - "example": 540, - "start_line": 8130, - "end_line": 8137, + "example": 537, + "start_line": 8084, + "end_line": 8091, "section": "Links" }, { "markdown": "[foo] [bar]\n\n[bar]: /url \"title\"\n", "html": "

[foo] bar

\n", - "example": 541, - "start_line": 8143, - "end_line": 8149, + "example": 538, + "start_line": 8097, + "end_line": 8103, "section": "Links" }, { "markdown": "[foo]\n[bar]\n\n[bar]: /url \"title\"\n", "html": "

[foo]\nbar

\n", - "example": 542, - "start_line": 8152, - "end_line": 8160, + "example": 539, + "start_line": 8106, + "end_line": 8114, "section": "Links" }, { "markdown": "[foo]: /url1\n\n[foo]: /url2\n\n[bar][foo]\n", "html": "

bar

\n", - "example": 543, - "start_line": 8193, - "end_line": 8201, + "example": 540, + "start_line": 8147, + "end_line": 8155, "section": "Links" }, { "markdown": "[bar][foo\\!]\n\n[foo!]: /url\n", "html": "

[bar][foo!]

\n", - "example": 544, - "start_line": 8208, - "end_line": 8214, + "example": 541, + "start_line": 8162, + "end_line": 8168, "section": "Links" }, { "markdown": "[foo][ref[]\n\n[ref[]: /uri\n", "html": "

[foo][ref[]

\n

[ref[]: /uri

\n", - "example": 545, - "start_line": 8220, - "end_line": 8227, + "example": 542, + "start_line": 8174, + "end_line": 8181, "section": "Links" }, { "markdown": "[foo][ref[bar]]\n\n[ref[bar]]: /uri\n", "html": "

[foo][ref[bar]]

\n

[ref[bar]]: /uri

\n", - "example": 546, - "start_line": 8230, - "end_line": 8237, + "example": 543, + "start_line": 8184, + "end_line": 8191, "section": "Links" }, { "markdown": "[[[foo]]]\n\n[[[foo]]]: /url\n", "html": "

[[[foo]]]

\n

[[[foo]]]: /url

\n", - "example": 547, - "start_line": 8240, - "end_line": 8247, + "example": 544, + "start_line": 8194, + "end_line": 8201, "section": "Links" }, { "markdown": "[foo][ref\\[]\n\n[ref\\[]: /uri\n", "html": "

foo

\n", - "example": 548, - "start_line": 8250, - "end_line": 8256, + "example": 545, + "start_line": 8204, + "end_line": 8210, "section": "Links" }, { "markdown": "[bar\\\\]: /uri\n\n[bar\\\\]\n", "html": "

bar\\

\n", - "example": 549, - "start_line": 8261, - "end_line": 8267, + "example": 546, + "start_line": 8215, + "end_line": 8221, "section": "Links" }, { "markdown": "[]\n\n[]: /uri\n", "html": "

[]

\n

[]: /uri

\n", - "example": 550, - "start_line": 8273, - "end_line": 8280, + "example": 547, + "start_line": 8226, + "end_line": 8233, "section": "Links" }, { "markdown": "[\n ]\n\n[\n ]: /uri\n", "html": "

[\n]

\n

[\n]: /uri

\n", - "example": 551, - "start_line": 8283, - "end_line": 8294, + "example": 548, + "start_line": 8236, + "end_line": 8247, "section": "Links" }, { "markdown": "[foo][]\n\n[foo]: /url \"title\"\n", "html": "

foo

\n", - "example": 552, - "start_line": 8306, - "end_line": 8312, + "example": 549, + "start_line": 8259, + "end_line": 8265, "section": "Links" }, { "markdown": "[*foo* bar][]\n\n[*foo* bar]: /url \"title\"\n", "html": "

foo bar

\n", - "example": 553, - "start_line": 8315, - "end_line": 8321, + "example": 550, + "start_line": 8268, + "end_line": 8274, "section": "Links" }, { "markdown": "[Foo][]\n\n[foo]: /url \"title\"\n", "html": "

Foo

\n", - "example": 554, - "start_line": 8326, - "end_line": 8332, + "example": 551, + "start_line": 8279, + "end_line": 8285, "section": "Links" }, { "markdown": "[foo] \n[]\n\n[foo]: /url \"title\"\n", "html": "

foo\n[]

\n", - "example": 555, - "start_line": 8339, - "end_line": 8347, + "example": 552, + "start_line": 8292, + "end_line": 8300, "section": "Links" }, { "markdown": "[foo]\n\n[foo]: /url \"title\"\n", "html": "

foo

\n", - "example": 556, - "start_line": 8359, - "end_line": 8365, + "example": 553, + "start_line": 8312, + "end_line": 8318, "section": "Links" }, { "markdown": "[*foo* bar]\n\n[*foo* bar]: /url \"title\"\n", "html": "

foo bar

\n", - "example": 557, - "start_line": 8368, - "end_line": 8374, + "example": 554, + "start_line": 8321, + "end_line": 8327, "section": "Links" }, { "markdown": "[[*foo* bar]]\n\n[*foo* bar]: /url \"title\"\n", "html": "

[foo bar]

\n", - "example": 558, - "start_line": 8377, - "end_line": 8383, + "example": 555, + "start_line": 8330, + "end_line": 8336, "section": "Links" }, { "markdown": "[[bar [foo]\n\n[foo]: /url\n", "html": "

[[bar foo

\n", - "example": 559, - "start_line": 8386, - "end_line": 8392, + "example": 556, + "start_line": 8339, + "end_line": 8345, "section": "Links" }, { "markdown": "[Foo]\n\n[foo]: /url \"title\"\n", "html": "

Foo

\n", - "example": 560, - "start_line": 8397, - "end_line": 8403, + "example": 557, + "start_line": 8350, + "end_line": 8356, "section": "Links" }, { "markdown": "[foo] bar\n\n[foo]: /url\n", "html": "

foo bar

\n", - "example": 561, - "start_line": 8408, - "end_line": 8414, + "example": 558, + "start_line": 8361, + "end_line": 8367, "section": "Links" }, { "markdown": "\\[foo]\n\n[foo]: /url \"title\"\n", "html": "

[foo]

\n", - "example": 562, - "start_line": 8420, - "end_line": 8426, + "example": 559, + "start_line": 8373, + "end_line": 8379, "section": "Links" }, { "markdown": "[foo*]: /url\n\n*[foo*]\n", "html": "

*foo*

\n", - "example": 563, - "start_line": 8432, - "end_line": 8438, + "example": 560, + "start_line": 8385, + "end_line": 8391, "section": "Links" }, { "markdown": "[foo][bar]\n\n[foo]: /url1\n[bar]: /url2\n", "html": "

foo

\n", - "example": 564, - "start_line": 8444, - "end_line": 8451, + "example": 561, + "start_line": 8397, + "end_line": 8404, "section": "Links" }, { "markdown": "[foo][]\n\n[foo]: /url1\n", "html": "

foo

\n", - "example": 565, - "start_line": 8453, - "end_line": 8459, + "example": 562, + "start_line": 8406, + "end_line": 8412, "section": "Links" }, { "markdown": "[foo]()\n\n[foo]: /url1\n", "html": "

foo

\n", - "example": 566, - "start_line": 8463, - "end_line": 8469, + "example": 563, + "start_line": 8416, + "end_line": 8422, "section": "Links" }, { "markdown": "[foo](not a link)\n\n[foo]: /url1\n", "html": "

foo(not a link)

\n", - "example": 567, - "start_line": 8471, - "end_line": 8477, + "example": 564, + "start_line": 8424, + "end_line": 8430, "section": "Links" }, { "markdown": "[foo][bar][baz]\n\n[baz]: /url\n", "html": "

[foo]bar

\n", - "example": 568, - "start_line": 8482, - "end_line": 8488, + "example": 565, + "start_line": 8435, + "end_line": 8441, "section": "Links" }, { "markdown": "[foo][bar][baz]\n\n[baz]: /url1\n[bar]: /url2\n", "html": "

foobaz

\n", - "example": 569, - "start_line": 8494, - "end_line": 8501, + "example": 566, + "start_line": 8447, + "end_line": 8454, "section": "Links" }, { "markdown": "[foo][bar][baz]\n\n[baz]: /url1\n[foo]: /url2\n", "html": "

[foo]bar

\n", - "example": 570, - "start_line": 8507, - "end_line": 8514, + "example": 567, + "start_line": 8460, + "end_line": 8467, "section": "Links" }, { "markdown": "![foo](/url \"title\")\n", "html": "

\"foo\"

\n", - "example": 571, - "start_line": 8530, - "end_line": 8534, + "example": 568, + "start_line": 8483, + "end_line": 8487, "section": "Images" }, { "markdown": "![foo *bar*]\n\n[foo *bar*]: train.jpg \"train & tracks\"\n", "html": "

\"foo

\n", - "example": 572, - "start_line": 8537, - "end_line": 8543, + "example": 569, + "start_line": 8490, + "end_line": 8496, "section": "Images" }, { "markdown": "![foo ![bar](/url)](/url2)\n", "html": "

\"foo

\n", - "example": 573, - "start_line": 8546, - "end_line": 8550, + "example": 570, + "start_line": 8499, + "end_line": 8503, "section": "Images" }, { "markdown": "![foo [bar](/url)](/url2)\n", "html": "

\"foo

\n", - "example": 574, - "start_line": 8553, - "end_line": 8557, + "example": 571, + "start_line": 8506, + "end_line": 8510, "section": "Images" }, { "markdown": "![foo *bar*][]\n\n[foo *bar*]: train.jpg \"train & tracks\"\n", "html": "

\"foo

\n", - "example": 575, - "start_line": 8567, - "end_line": 8573, + "example": 572, + "start_line": 8520, + "end_line": 8526, "section": "Images" }, { "markdown": "![foo *bar*][foobar]\n\n[FOOBAR]: train.jpg \"train & tracks\"\n", "html": "

\"foo

\n", - "example": 576, - "start_line": 8576, - "end_line": 8582, + "example": 573, + "start_line": 8529, + "end_line": 8535, "section": "Images" }, { "markdown": "![foo](train.jpg)\n", "html": "

\"foo\"

\n", - "example": 577, - "start_line": 8585, - "end_line": 8589, + "example": 574, + "start_line": 8538, + "end_line": 8542, "section": "Images" }, { "markdown": "My ![foo bar](/path/to/train.jpg \"title\" )\n", "html": "

My \"foo

\n", - "example": 578, - "start_line": 8592, - "end_line": 8596, + "example": 575, + "start_line": 8545, + "end_line": 8549, "section": "Images" }, { "markdown": "![foo]()\n", "html": "

\"foo\"

\n", - "example": 579, - "start_line": 8599, - "end_line": 8603, + "example": 576, + "start_line": 8552, + "end_line": 8556, "section": "Images" }, { "markdown": "![](/url)\n", "html": "

\"\"

\n", - "example": 580, - "start_line": 8606, - "end_line": 8610, + "example": 577, + "start_line": 8559, + "end_line": 8563, "section": "Images" }, { "markdown": "![foo][bar]\n\n[bar]: /url\n", "html": "

\"foo\"

\n", - "example": 581, - "start_line": 8615, - "end_line": 8621, + "example": 578, + "start_line": 8568, + "end_line": 8574, "section": "Images" }, { "markdown": "![foo][bar]\n\n[BAR]: /url\n", "html": "

\"foo\"

\n", - "example": 582, - "start_line": 8624, - "end_line": 8630, + "example": 579, + "start_line": 8577, + "end_line": 8583, "section": "Images" }, { "markdown": "![foo][]\n\n[foo]: /url \"title\"\n", "html": "

\"foo\"

\n", - "example": 583, - "start_line": 8635, - "end_line": 8641, + "example": 580, + "start_line": 8588, + "end_line": 8594, "section": "Images" }, { "markdown": "![*foo* bar][]\n\n[*foo* bar]: /url \"title\"\n", "html": "

\"foo

\n", - "example": 584, - "start_line": 8644, - "end_line": 8650, + "example": 581, + "start_line": 8597, + "end_line": 8603, "section": "Images" }, { "markdown": "![Foo][]\n\n[foo]: /url \"title\"\n", "html": "

\"Foo\"

\n", - "example": 585, - "start_line": 8655, - "end_line": 8661, + "example": 582, + "start_line": 8608, + "end_line": 8614, "section": "Images" }, { "markdown": "![foo] \n[]\n\n[foo]: /url \"title\"\n", "html": "

\"foo\"\n[]

\n", - "example": 586, - "start_line": 8667, - "end_line": 8675, + "example": 583, + "start_line": 8620, + "end_line": 8628, "section": "Images" }, { "markdown": "![foo]\n\n[foo]: /url \"title\"\n", "html": "

\"foo\"

\n", - "example": 587, - "start_line": 8680, - "end_line": 8686, + "example": 584, + "start_line": 8633, + "end_line": 8639, "section": "Images" }, { "markdown": "![*foo* bar]\n\n[*foo* bar]: /url \"title\"\n", "html": "

\"foo

\n", - "example": 588, - "start_line": 8689, - "end_line": 8695, + "example": 585, + "start_line": 8642, + "end_line": 8648, "section": "Images" }, { "markdown": "![[foo]]\n\n[[foo]]: /url \"title\"\n", "html": "

![[foo]]

\n

[[foo]]: /url "title"

\n", - "example": 589, - "start_line": 8700, - "end_line": 8707, + "example": 586, + "start_line": 8653, + "end_line": 8660, "section": "Images" }, { "markdown": "![Foo]\n\n[foo]: /url \"title\"\n", "html": "

\"Foo\"

\n", - "example": 590, - "start_line": 8712, - "end_line": 8718, + "example": 587, + "start_line": 8665, + "end_line": 8671, "section": "Images" }, { "markdown": "!\\[foo]\n\n[foo]: /url \"title\"\n", "html": "

![foo]

\n", - "example": 591, - "start_line": 8724, - "end_line": 8730, + "example": 588, + "start_line": 8677, + "end_line": 8683, "section": "Images" }, { "markdown": "\\![foo]\n\n[foo]: /url \"title\"\n", "html": "

!foo

\n", - "example": 592, - "start_line": 8736, - "end_line": 8742, + "example": 589, + "start_line": 8689, + "end_line": 8695, "section": "Images" }, { "markdown": "\n", "html": "

http://foo.bar.baz

\n", - "example": 593, - "start_line": 8769, - "end_line": 8773, + "example": 590, + "start_line": 8722, + "end_line": 8726, "section": "Autolinks" }, { "markdown": "\n", "html": "

http://foo.bar.baz/test?q=hello&id=22&boolean

\n", - "example": 594, - "start_line": 8776, - "end_line": 8780, + "example": 591, + "start_line": 8729, + "end_line": 8733, "section": "Autolinks" }, { "markdown": "\n", "html": "

irc://foo.bar:2233/baz

\n", - "example": 595, - "start_line": 8783, - "end_line": 8787, + "example": 592, + "start_line": 8736, + "end_line": 8740, "section": "Autolinks" }, { "markdown": "\n", "html": "

MAILTO:FOO@BAR.BAZ

\n", - "example": 596, - "start_line": 8792, - "end_line": 8796, + "example": 593, + "start_line": 8745, + "end_line": 8749, "section": "Autolinks" }, { "markdown": "\n", "html": "

a+b+c:d

\n", - "example": 597, - "start_line": 8804, - "end_line": 8808, + "example": 594, + "start_line": 8757, + "end_line": 8761, "section": "Autolinks" }, { "markdown": "\n", "html": "

made-up-scheme://foo,bar

\n", - "example": 598, - "start_line": 8811, - "end_line": 8815, + "example": 595, + "start_line": 8764, + "end_line": 8768, "section": "Autolinks" }, { "markdown": "\n", "html": "

http://../

\n", - "example": 599, - "start_line": 8818, - "end_line": 8822, + "example": 596, + "start_line": 8771, + "end_line": 8775, "section": "Autolinks" }, { "markdown": "\n", "html": "

localhost:5001/foo

\n", - "example": 600, - "start_line": 8825, - "end_line": 8829, + "example": 597, + "start_line": 8778, + "end_line": 8782, "section": "Autolinks" }, { "markdown": "\n", "html": "

<http://foo.bar/baz bim>

\n", - "example": 601, - "start_line": 8834, - "end_line": 8838, + "example": 598, + "start_line": 8787, + "end_line": 8791, "section": "Autolinks" }, { "markdown": "\n", "html": "

http://example.com/\\[\\

\n", - "example": 602, - "start_line": 8843, - "end_line": 8847, + "example": 599, + "start_line": 8796, + "end_line": 8800, "section": "Autolinks" }, { "markdown": "\n", "html": "

foo@bar.example.com

\n", - "example": 603, - "start_line": 8865, - "end_line": 8869, + "example": 600, + "start_line": 8818, + "end_line": 8822, "section": "Autolinks" }, { "markdown": "\n", "html": "

foo+special@Bar.baz-bar0.com

\n", - "example": 604, - "start_line": 8872, - "end_line": 8876, + "example": 601, + "start_line": 8825, + "end_line": 8829, "section": "Autolinks" }, { "markdown": "\n", "html": "

<foo+@bar.example.com>

\n", - "example": 605, - "start_line": 8881, - "end_line": 8885, + "example": 602, + "start_line": 8834, + "end_line": 8838, "section": "Autolinks" }, { "markdown": "<>\n", "html": "

<>

\n", - "example": 606, - "start_line": 8890, - "end_line": 8894, + "example": 603, + "start_line": 8843, + "end_line": 8847, "section": "Autolinks" }, { "markdown": "< http://foo.bar >\n", "html": "

< http://foo.bar >

\n", - "example": 607, - "start_line": 8897, - "end_line": 8901, + "example": 604, + "start_line": 8850, + "end_line": 8854, "section": "Autolinks" }, { "markdown": "\n", "html": "

<m:abc>

\n", - "example": 608, - "start_line": 8904, - "end_line": 8908, + "example": 605, + "start_line": 8857, + "end_line": 8861, "section": "Autolinks" }, { "markdown": "\n", "html": "

<foo.bar.baz>

\n", - "example": 609, - "start_line": 8911, - "end_line": 8915, + "example": 606, + "start_line": 8864, + "end_line": 8868, "section": "Autolinks" }, { "markdown": "http://example.com\n", "html": "

http://example.com

\n", - "example": 610, - "start_line": 8918, - "end_line": 8922, + "example": 607, + "start_line": 8871, + "end_line": 8875, "section": "Autolinks" }, { "markdown": "foo@bar.example.com\n", "html": "

foo@bar.example.com

\n", - "example": 611, - "start_line": 8925, - "end_line": 8929, + "example": 608, + "start_line": 8878, + "end_line": 8882, "section": "Autolinks" }, { "markdown": "\n", "html": "

\n", - "example": 612, - "start_line": 9006, - "end_line": 9010, + "example": 609, + "start_line": 8960, + "end_line": 8964, "section": "Raw HTML" }, { "markdown": "\n", "html": "

\n", - "example": 613, - "start_line": 9015, - "end_line": 9019, + "example": 610, + "start_line": 8969, + "end_line": 8973, "section": "Raw HTML" }, { "markdown": "\n", "html": "

\n", - "example": 614, - "start_line": 9024, - "end_line": 9030, + "example": 611, + "start_line": 8978, + "end_line": 8984, "section": "Raw HTML" }, { "markdown": "\n", "html": "

\n", - "example": 615, - "start_line": 9035, - "end_line": 9041, + "example": 612, + "start_line": 8989, + "end_line": 8995, "section": "Raw HTML" }, { "markdown": "Foo \n", "html": "

Foo

\n", - "example": 616, - "start_line": 9046, - "end_line": 9050, + "example": 613, + "start_line": 9000, + "end_line": 9004, "section": "Raw HTML" }, { "markdown": "<33> <__>\n", "html": "

<33> <__>

\n", - "example": 617, - "start_line": 9055, - "end_line": 9059, + "example": 614, + "start_line": 9009, + "end_line": 9013, "section": "Raw HTML" }, { "markdown": "
\n", "html": "

<a h*#ref="hi">

\n", - "example": 618, - "start_line": 9064, - "end_line": 9068, + "example": 615, + "start_line": 9018, + "end_line": 9022, "section": "Raw HTML" }, { "markdown": "
\n", "html": "

<a href="hi'> <a href=hi'>

\n", - "example": 619, - "start_line": 9073, - "end_line": 9077, + "example": 616, + "start_line": 9027, + "end_line": 9031, "section": "Raw HTML" }, { "markdown": "< a><\nfoo>\n\n", "html": "

< a><\nfoo><bar/ >\n<foo bar=baz\nbim!bop />

\n", - "example": 620, - "start_line": 9082, - "end_line": 9092, + "example": 617, + "start_line": 9036, + "end_line": 9046, "section": "Raw HTML" }, { "markdown": "
\n", "html": "

<a href='bar'title=title>

\n", - "example": 621, - "start_line": 9097, - "end_line": 9101, + "example": 618, + "start_line": 9051, + "end_line": 9055, "section": "Raw HTML" }, { "markdown": "
\n", "html": "

\n", - "example": 622, - "start_line": 9106, - "end_line": 9110, + "example": 619, + "start_line": 9060, + "end_line": 9064, "section": "Raw HTML" }, { "markdown": "\n", "html": "

</a href="foo">

\n", - "example": 623, - "start_line": 9115, - "end_line": 9119, + "example": 620, + "start_line": 9069, + "end_line": 9073, "section": "Raw HTML" }, { "markdown": "foo \n", "html": "

foo

\n", - "example": 624, - "start_line": 9124, - "end_line": 9130, + "example": 621, + "start_line": 9078, + "end_line": 9084, "section": "Raw HTML" }, { "markdown": "foo \n", "html": "

foo <!-- not a comment -- two hyphens -->

\n", - "example": 625, - "start_line": 9133, - "end_line": 9137, + "example": 622, + "start_line": 9087, + "end_line": 9091, "section": "Raw HTML" }, { "markdown": "foo foo -->\n\nfoo \n", "html": "

foo <!--> foo -->

\n

foo <!-- foo--->

\n", - "example": 626, - "start_line": 9142, - "end_line": 9149, + "example": 623, + "start_line": 9096, + "end_line": 9103, "section": "Raw HTML" }, { "markdown": "foo \n", "html": "

foo

\n", - "example": 627, - "start_line": 9154, - "end_line": 9158, + "example": 624, + "start_line": 9108, + "end_line": 9112, "section": "Raw HTML" }, { "markdown": "foo \n", "html": "

foo

\n", - "example": 628, - "start_line": 9163, - "end_line": 9167, + "example": 625, + "start_line": 9117, + "end_line": 9121, "section": "Raw HTML" }, { "markdown": "foo &<]]>\n", "html": "

foo &<]]>

\n", - "example": 629, - "start_line": 9172, - "end_line": 9176, + "example": 626, + "start_line": 9126, + "end_line": 9130, "section": "Raw HTML" }, { "markdown": "foo \n", "html": "

foo

\n", - "example": 630, - "start_line": 9182, - "end_line": 9186, + "example": 627, + "start_line": 9136, + "end_line": 9140, "section": "Raw HTML" }, { "markdown": "foo \n", "html": "

foo

\n", - "example": 631, - "start_line": 9191, - "end_line": 9195, + "example": 628, + "start_line": 9145, + "end_line": 9149, "section": "Raw HTML" }, { "markdown": "\n", "html": "

<a href=""">

\n", - "example": 632, - "start_line": 9198, - "end_line": 9202, + "example": 629, + "start_line": 9152, + "end_line": 9156, "section": "Raw HTML" }, { "markdown": "foo \nbaz\n", "html": "

foo
\nbaz

\n", - "example": 633, - "start_line": 9212, - "end_line": 9218, + "example": 630, + "start_line": 9166, + "end_line": 9172, "section": "Hard line breaks" }, { "markdown": "foo\\\nbaz\n", "html": "

foo
\nbaz

\n", - "example": 634, - "start_line": 9224, - "end_line": 9230, + "example": 631, + "start_line": 9178, + "end_line": 9184, "section": "Hard line breaks" }, { "markdown": "foo \nbaz\n", "html": "

foo
\nbaz

\n", - "example": 635, - "start_line": 9235, - "end_line": 9241, + "example": 632, + "start_line": 9189, + "end_line": 9195, "section": "Hard line breaks" }, { "markdown": "foo \n bar\n", "html": "

foo
\nbar

\n", - "example": 636, - "start_line": 9246, - "end_line": 9252, + "example": 633, + "start_line": 9200, + "end_line": 9206, "section": "Hard line breaks" }, { "markdown": "foo\\\n bar\n", "html": "

foo
\nbar

\n", - "example": 637, - "start_line": 9255, - "end_line": 9261, + "example": 634, + "start_line": 9209, + "end_line": 9215, "section": "Hard line breaks" }, { "markdown": "*foo \nbar*\n", "html": "

foo
\nbar

\n", - "example": 638, - "start_line": 9267, - "end_line": 9273, + "example": 635, + "start_line": 9221, + "end_line": 9227, "section": "Hard line breaks" }, { "markdown": "*foo\\\nbar*\n", "html": "

foo
\nbar

\n", - "example": 639, - "start_line": 9276, - "end_line": 9282, + "example": 636, + "start_line": 9230, + "end_line": 9236, "section": "Hard line breaks" }, { - "markdown": "`code \nspan`\n", - "html": "

code span

\n", - "example": 640, - "start_line": 9287, - "end_line": 9292, + "markdown": "`code \nspan`\n", + "html": "

code span

\n", + "example": 637, + "start_line": 9241, + "end_line": 9246, "section": "Hard line breaks" }, { "markdown": "`code\\\nspan`\n", "html": "

code\\ span

\n", - "example": 641, - "start_line": 9295, - "end_line": 9300, + "example": 638, + "start_line": 9249, + "end_line": 9254, "section": "Hard line breaks" }, { "markdown": "
\n", "html": "

\n", - "example": 642, - "start_line": 9305, - "end_line": 9311, + "example": 639, + "start_line": 9259, + "end_line": 9265, "section": "Hard line breaks" }, { "markdown": "\n", "html": "

\n", - "example": 643, - "start_line": 9314, - "end_line": 9320, + "example": 640, + "start_line": 9268, + "end_line": 9274, "section": "Hard line breaks" }, { "markdown": "foo\\\n", "html": "

foo\\

\n", - "example": 644, - "start_line": 9327, - "end_line": 9331, + "example": 641, + "start_line": 9281, + "end_line": 9285, "section": "Hard line breaks" }, { "markdown": "foo \n", "html": "

foo

\n", - "example": 645, - "start_line": 9334, - "end_line": 9338, + "example": 642, + "start_line": 9288, + "end_line": 9292, "section": "Hard line breaks" }, { "markdown": "### foo\\\n", "html": "

foo\\

\n", - "example": 646, - "start_line": 9341, - "end_line": 9345, + "example": 643, + "start_line": 9295, + "end_line": 9299, "section": "Hard line breaks" }, { "markdown": "### foo \n", "html": "

foo

\n", - "example": 647, - "start_line": 9348, - "end_line": 9352, + "example": 644, + "start_line": 9302, + "end_line": 9306, "section": "Hard line breaks" }, { "markdown": "foo\nbaz\n", "html": "

foo\nbaz

\n", - "example": 648, - "start_line": 9363, - "end_line": 9369, + "example": 645, + "start_line": 9317, + "end_line": 9323, "section": "Soft line breaks" }, { "markdown": "foo \n baz\n", "html": "

foo\nbaz

\n", - "example": 649, - "start_line": 9375, - "end_line": 9381, + "example": 646, + "start_line": 9329, + "end_line": 9335, "section": "Soft line breaks" }, { "markdown": "hello $.;'there\n", "html": "

hello $.;'there

\n", - "example": 650, - "start_line": 9395, - "end_line": 9399, + "example": 647, + "start_line": 9349, + "end_line": 9353, "section": "Textual content" }, { "markdown": "Foo χρῆν\n", "html": "

Foo χρῆν

\n", - "example": 651, - "start_line": 9402, - "end_line": 9406, + "example": 648, + "start_line": 9356, + "end_line": 9360, "section": "Textual content" }, { "markdown": "Multiple spaces\n", "html": "

Multiple spaces

\n", - "example": 652, - "start_line": 9411, - "end_line": 9415, + "example": 649, + "start_line": 9365, + "end_line": 9369, "section": "Textual content" } ] DELETED testdata/testbox/00000000000100.zettel Index: testdata/testbox/00000000000100.zettel ================================================================== --- testdata/testbox/00000000000100.zettel +++ testdata/testbox/00000000000100.zettel @@ -1,9 +0,0 @@ -id: 00000000000100 -title: Zettelstore Runtime Configuration -role: configuration -syntax: none -expert-mode: true -modified: 20210629174242 -no-index: true -visibility: owner - DELETED testdata/testbox/19700101000000.zettel Index: testdata/testbox/19700101000000.zettel ================================================================== --- testdata/testbox/19700101000000.zettel +++ testdata/testbox/19700101000000.zettel @@ -1,11 +0,0 @@ -id: 19700101000000 -title: Startup Configuration -role: configuration -tags: #invisible -syntax: none -box-uri-1: mem: -box-uri-2: dir:testdata/testbox?readonly -modified: 20210629174022 -owner: 20210629163300 -token-lifetime-api: 1 -visibility: owner DELETED testdata/testbox/20210629163300.zettel Index: testdata/testbox/20210629163300.zettel ================================================================== --- testdata/testbox/20210629163300.zettel +++ testdata/testbox/20210629163300.zettel @@ -1,8 +0,0 @@ -id: 20210629163300 -title: owner -role: user -tags: #test #user -syntax: none -credential: $2a$10$gcKyVmQ50fwgpOjyiiCm4eba/ILrNXoxTUCopgTEnYTa4yuceHMC6 -modified: 20210629173617 -user-id: owner DELETED testdata/testbox/20210629165000.zettel Index: testdata/testbox/20210629165000.zettel ================================================================== --- testdata/testbox/20210629165000.zettel +++ testdata/testbox/20210629165000.zettel @@ -1,9 +0,0 @@ -id: 20210629165000 -title: writer -role: user -tags: #test #user -syntax: none -credential: $2a$10$VmHPyXa0Bm8DE4MJ.pQnbuuQmweWtyGya0L/bFA4nIuCn1EvPQflK -modified: 20210629173536 -user-id: writer -user-role: writer DELETED testdata/testbox/20210629165024.zettel Index: testdata/testbox/20210629165024.zettel ================================================================== --- testdata/testbox/20210629165024.zettel +++ testdata/testbox/20210629165024.zettel @@ -1,9 +0,0 @@ -id: 20210629165024 -title: reader -role: user -tags: #test #user -syntax: none -credential: $2a$10$uC7LV2JdFhasw2HqSWZbSOihvFpwtaEXjXp98yzGfE3FHudq.vg.u -modified: 20210629173459 -user-id: reader -user-role: reader DELETED testdata/testbox/20210629165050.zettel Index: testdata/testbox/20210629165050.zettel ================================================================== --- testdata/testbox/20210629165050.zettel +++ testdata/testbox/20210629165050.zettel @@ -1,9 +0,0 @@ -id: 20210629165050 -title: creator -role: user -tags: #test #user -syntax: none -credential: $2a$10$z85253tqhbHlXPZpt0hJpughLR4WXY8iYJbm1LlBhrKsL1YfkRy2q -modified: 20210629173424 -user-id: creator -user-role: creator Index: tests/markdown_test.go ================================================================== --- tests/markdown_test.go +++ tests/markdown_test.go @@ -17,11 +17,10 @@ "os" "regexp" "strings" "testing" - "zettelstore.de/z/api" "zettelstore.de/z/ast" "zettelstore.de/z/encoder" _ "zettelstore.de/z/encoder/htmlenc" _ "zettelstore.de/z/encoder/jsonenc" _ "zettelstore.de/z/encoder/nativeenc" @@ -43,37 +42,36 @@ } // exceptions lists all CommonMark tests that should not be tested for identical HTML output var exceptions = []string{ " - foo\n - bar\n\t - baz\n", // 9 - "\nokay\n", // 170 - "1. *bar*\n", // 178 - "- foo\n - bar\n - baz\n - boo\n", // 294 - "10) foo\n - bar\n", // 296 - "- # Foo\n- Bar\n ---\n baz\n", // 300 - "- foo\n\n- bar\n\n\n- baz\n", // 306 - "- foo\n - bar\n - baz\n\n\n bim\n", // 307 - "1. a\n\n 2. b\n\n 3. c\n", // 311 - "1. a\n\n 2. b\n\n 3. c\n", // 313 - "- a\n- b\n\n- c\n", // 314 - "* a\n*\n\n* c\n", // 315 - "- a\n- b\n\n [ref]: /url\n- d\n", // 317 - "- a\n - b\n\n c\n- d\n", // 319 - "* a\n > b\n >\n* c\n", // 320 - "- a\n > b\n ```\n c\n ```\n- d\n", // 321 - "- a\n - b\n", // 323 - "`\n", // 345 - "[foo\n", // 525 - "[foo\n\n[ref]: /uri\n", // 537 - "\n", // 581 - "\n", // 594 + "\nokay\n", // 140 + "1. *bar*\n", // 147 + "- foo\n - bar\n - baz\n - boo\n", // 264 + "10) foo\n - bar\n", // 266 + "- # Foo\n- Bar\n ---\n baz\n", // 270 + "- foo\n\n- bar\n\n\n- baz\n", // 276 + "- foo\n - bar\n - baz\n\n\n bim\n", // 277 + "1. a\n\n 2. b\n\n 3. c\n", // 281 + "1. a\n\n 2. b\n\n 3. c\n", // 283 + "- a\n- b\n\n- c\n", // 284 + "* a\n*\n\n* c\n", // 285 + "- a\n- b\n\n [ref]: /url\n- d\n", // 287 + "- a\n - b\n\n c\n- d\n", // 289 + "* a\n > b\n >\n* c\n", // 290 + "- a\n > b\n ```\n c\n ```\n- d\n", // 291 + "- a\n - b\n", // 293 + "\n", // 306 + "`\n", // 346 + "[foo\n", // 522 + "[foo\n\n[ref]: /uri\n", // 534 + "\n", // 591 } var reHeadingID = regexp.MustCompile(` id="[^"]*"`) func TestEncoderAvailability(t *testing.T) { - t.Parallel() encoderMissing := false for _, format := range formats { enc := encoder.Create(format, nil) if enc == nil { t.Errorf("No encoder for %q found", format) @@ -84,11 +82,10 @@ panic("At least one encoder is missing. See test log") } } func TestMarkdownSpec(t *testing.T) { - t.Parallel() content, err := os.ReadFile("../testdata/markdown/spec.json") if err != nil { panic(err) } var testcases []markdownTestCase @@ -119,11 +116,11 @@ }) } } func testHTMLEncoding(t *testing.T, tc markdownTestCase, ast ast.BlockSlice) { - htmlEncoder := encoder.Create(api.EncoderHTML, &encoder.Environment{Xhtml: true}) + htmlEncoder := encoder.Create("html", &encoder.Environment{Xhtml: true}) var sb strings.Builder testID := tc.Example*100 + 1 t.Run(fmt.Sprintf("Encode md html %v", testID), func(st *testing.T) { htmlEncoder.WriteBlocks(&sb, ast) gotHTML := sb.String() @@ -144,11 +141,11 @@ } }) } func testZmkEncoding(t *testing.T, tc markdownTestCase, ast ast.BlockSlice) { - zmkEncoder := encoder.Create(api.EncoderZmk, nil) + zmkEncoder := encoder.Create("zmk", nil) var sb strings.Builder testID := tc.Example*100 + 1 t.Run(fmt.Sprintf("Encode zmk %14d", testID), func(st *testing.T) { zmkEncoder.WriteBlocks(&sb, ast) gotFirst := sb.String() Index: tests/regression_test.go ================================================================== --- tests/regression_test.go +++ tests/regression_test.go @@ -19,66 +19,60 @@ "os" "path/filepath" "strings" "testing" - "zettelstore.de/z/api" "zettelstore.de/z/ast" - "zettelstore.de/z/box" - "zettelstore.de/z/box/manager" "zettelstore.de/z/domain" "zettelstore.de/z/domain/id" "zettelstore.de/z/domain/meta" "zettelstore.de/z/encoder" "zettelstore.de/z/kernel" "zettelstore.de/z/parser" + "zettelstore.de/z/place" + "zettelstore.de/z/place/manager" - _ "zettelstore.de/z/box/dirbox" _ "zettelstore.de/z/encoder/htmlenc" _ "zettelstore.de/z/encoder/jsonenc" _ "zettelstore.de/z/encoder/nativeenc" _ "zettelstore.de/z/encoder/textenc" _ "zettelstore.de/z/encoder/zmkenc" _ "zettelstore.de/z/parser/blob" _ "zettelstore.de/z/parser/zettelmark" + _ "zettelstore.de/z/place/dirplace" ) -var formats = []api.EncodingEnum{ - api.EncoderHTML, - api.EncoderDJSON, - api.EncoderNative, - api.EncoderText, -} - -func getFileBoxes(wd, kind string) (root string, boxes []box.ManagedBox) { +var formats = []string{"html", "djson", "native", "text"} + +func getFilePlaces(wd string, kind string) (root string, places []place.ManagedPlace) { root = filepath.Clean(filepath.Join(wd, "..", "testdata", kind)) entries, err := os.ReadDir(root) if err != nil { panic(err) } cdata := manager.ConnectData{Config: testConfig, Enricher: &noEnrich{}, Notify: nil} for _, entry := range entries { if entry.IsDir() { - u, err := url.Parse("dir://" + filepath.Join(root, entry.Name()) + "?type=" + kernel.BoxDirTypeSimple) + u, err := url.Parse("dir://" + filepath.Join(root, entry.Name()) + "?type=" + kernel.PlaceDirTypeSimple) if err != nil { panic(err) } - box, err := manager.Connect(u, &noAuth{}, &cdata) + place, err := manager.Connect(u, &noAuth{}, &cdata) if err != nil { panic(err) } - boxes = append(boxes, box) + places = append(places, place) } } - return root, boxes + return root, places } type noEnrich struct{} -func (nf *noEnrich) Enrich(context.Context, *meta.Meta, int) {} -func (nf *noEnrich) Remove(context.Context, *meta.Meta) {} +func (nf *noEnrich) Enrich(ctx context.Context, m *meta.Meta) {} +func (nf *noEnrich) Remove(ctx context.Context, m *meta.Meta) {} type noAuth struct{} func (na *noAuth) IsReadonly() bool { return false } @@ -97,11 +91,11 @@ defer f.Close() src, err := io.ReadAll(f) return string(src), err } -func checkFileContent(t *testing.T, filename, gotContent string) { +func checkFileContent(t *testing.T, filename string, gotContent string) { t.Helper() wantContent, err := resultFile(filename) if err != nil { t.Error(err) return @@ -111,11 +105,11 @@ if gotContent != wantContent { t.Errorf("\nWant: %q\nGot: %q", wantContent, gotContent) } } -func checkBlocksFile(t *testing.T, resultName string, zn *ast.ZettelNode, format api.EncodingEnum) { +func checkBlocksFile(t *testing.T, resultName string, zn *ast.ZettelNode, format string) { t.Helper() var env encoder.Environment if enc := encoder.Create(format, &env); enc != nil { var sb strings.Builder enc.WriteBlocks(&sb, zn.Ast) @@ -124,11 +118,11 @@ } panic(fmt.Sprintf("Unknown writer format %q", format)) } func checkZmkEncoder(t *testing.T, zn *ast.ZettelNode) { - zmkEncoder := encoder.Create(api.EncoderZmk, nil) + zmkEncoder := encoder.Create("zmk", nil) var sb strings.Builder zmkEncoder.WriteBlocks(&sb, zn.Ast) gotFirst := sb.String() sb.Reset() @@ -141,22 +135,22 @@ if gotFirst != gotSecond { t.Errorf("\n1st: %q\n2nd: %q", gotFirst, gotSecond) } } -func getBoxName(p box.ManagedBox, root string) string { +func getPlaceName(p place.ManagedPlace, root string) string { u, err := url.Parse(p.Location()) if err != nil { panic("Unable to parse URL '" + p.Location() + "': " + err.Error()) } return u.Path[len(root):] } func match(*meta.Meta) bool { return true } -func checkContentBox(t *testing.T, p box.ManagedBox, wd, boxName string) { - ss := p.(box.StartStopper) +func checkContentPlace(t *testing.T, p place.ManagedPlace, wd, placeName string) { + ss := p.(place.StartStopper) if err := ss.Start(context.Background()); err != nil { panic(err) } metaList, err := p.SelectMeta(context.Background(), match) if err != nil { @@ -168,11 +162,11 @@ panic(err) } z := parser.ParseZettel(zettel, "", testConfig) for _, format := range formats { t.Run(fmt.Sprintf("%s::%d(%s)", p.Location(), meta.Zid, format), func(st *testing.T) { - resultName := filepath.Join(wd, "result", "content", boxName, z.Zid.String()+"."+format.String()) + resultName := filepath.Join(wd, "result", "content", placeName, z.Zid.String()+"."+format) checkBlocksFile(st, resultName, z, format) }) } t.Run(fmt.Sprintf("%s::%d", p.Location(), meta.Zid), func(st *testing.T) { checkZmkEncoder(st, z) @@ -182,22 +176,21 @@ panic(err) } } func TestContentRegression(t *testing.T) { - t.Parallel() wd, err := os.Getwd() if err != nil { panic(err) } - root, boxes := getFileBoxes(wd, "content") - for _, p := range boxes { - checkContentBox(t, p, wd, getBoxName(p, root)) + root, places := getFilePlaces(wd, "content") + for _, p := range places { + checkContentPlace(t, p, wd, getPlaceName(p, root)) } } -func checkMetaFile(t *testing.T, resultName string, zn *ast.ZettelNode, format api.EncodingEnum) { +func checkMetaFile(t *testing.T, resultName string, zn *ast.ZettelNode, format string) { t.Helper() if enc := encoder.Create(format, nil); enc != nil { var sb strings.Builder enc.WriteMeta(&sb, zn.Meta) @@ -205,12 +198,12 @@ return } panic(fmt.Sprintf("Unknown writer format %q", format)) } -func checkMetaBox(t *testing.T, p box.ManagedBox, wd, boxName string) { - ss := p.(box.StartStopper) +func checkMetaPlace(t *testing.T, p place.ManagedPlace, wd, placeName string) { + ss := p.(place.StartStopper) if err := ss.Start(context.Background()); err != nil { panic(err) } metaList, err := p.SelectMeta(context.Background(), match) if err != nil { @@ -222,11 +215,11 @@ panic(err) } z := parser.ParseZettel(zettel, "", testConfig) for _, format := range formats { t.Run(fmt.Sprintf("%s::%d(%s)", p.Location(), meta.Zid, format), func(st *testing.T) { - resultName := filepath.Join(wd, "result", "meta", boxName, z.Zid.String()+"."+format.String()) + resultName := filepath.Join(wd, "result", "meta", placeName, z.Zid.String()+"."+format) checkMetaFile(st, resultName, z, format) }) } } if err := ss.Stop(context.Background()); err != nil { @@ -254,15 +247,14 @@ func (cfg *myConfig) GetVisibility(*meta.Meta) meta.Visibility { return cfg.GetDefaultVisibility() } var testConfig = &myConfig{} func TestMetaRegression(t *testing.T) { - t.Parallel() wd, err := os.Getwd() if err != nil { panic(err) } - root, boxes := getFileBoxes(wd, "meta") - for _, p := range boxes { - checkMetaBox(t, p, wd, getBoxName(p, root)) + root, places := getFilePlaces(wd, "meta") + for _, p := range places { + checkMetaPlace(t, p, wd, getPlaceName(p, root)) } } Index: tools/build.go ================================================================== --- tools/build.go +++ tools/build.go @@ -12,16 +12,14 @@ package main import ( "archive/zip" "bytes" - "errors" "flag" "fmt" "io" "io/fs" - "net" "os" "os/exec" "path/filepath" "regexp" "strings" @@ -29,38 +27,29 @@ "zettelstore.de/z/strfun" ) func executeCommand(env []string, name string, arg ...string) (string, error) { - logCommand("EXEC", env, name, arg) - var out bytes.Buffer - cmd := prepareCommand(env, name, arg, &out) - err := cmd.Run() - return out.String(), err -} - -func prepareCommand(env []string, name string, arg []string, out io.Writer) *exec.Cmd { - if len(env) > 0 { - env = append(env, os.Environ()...) - } - cmd := exec.Command(name, arg...) - cmd.Env = env - cmd.Stdin = nil - cmd.Stdout = out - cmd.Stderr = os.Stderr - return cmd -} - -func logCommand(exec string, env []string, name string, arg []string) { if verbose { if len(env) > 0 { for i, e := range env { fmt.Fprintf(os.Stderr, "ENV%d %v\n", i+1, e) } } - fmt.Fprintln(os.Stderr, exec, name, arg) + fmt.Fprintln(os.Stderr, "EXEC", name, arg) + } + if len(env) > 0 { + env = append(env, os.Environ()...) } + var out bytes.Buffer + cmd := exec.Command(name, arg...) + cmd.Env = env + cmd.Stdin = nil + cmd.Stdout = &out + cmd.Stderr = os.Stderr + err := cmd.Run() + return out.String(), err } func readVersionFile() (string, error) { content, err := os.ReadFile("VERSION") if err != nil { @@ -133,11 +122,11 @@ } return "" } func cmdCheck() error { - if err := checkGoTest("./..."); err != nil { + if err := checkGoTest(); err != nil { return err } if err := checkGoVet(); err != nil { return err } @@ -151,14 +140,12 @@ return err } return checkFossilExtra() } -func checkGoTest(pkg string, testParams ...string) error { - args := []string{"test", pkg} - args = append(args, testParams...) - out, err := executeCommand(nil, "go", args...) +func checkGoTest() error { + out, err := executeCommand(nil, "go", "test", "./...") if err != nil { for _, line := range strfun.SplitLines(out) { if strings.HasPrefix(line, "ok") || strings.HasPrefix(line, "?") { continue } @@ -230,78 +217,10 @@ fmt.Fprintln(os.Stderr) } return nil } -type zsInfo struct { - cmd *exec.Cmd - out bytes.Buffer - adminAddress string -} - -func cmdTestAPI() error { - var err error - var info zsInfo - needServer := !addressInUse(":23123") - if needServer { - err = startZettelstore(&info) - } - if err != nil { - return err - } - err = checkGoTest("zettelstore.de/z/client", "-base-url", "http://127.0.0.1:23123") - if needServer { - err1 := stopZettelstore(&info) - if err == nil { - err = err1 - } - } - return err -} - -func startZettelstore(info *zsInfo) error { - info.adminAddress = ":2323" - name, arg := "go", []string{ - "run", "cmd/zettelstore/main.go", "run", - "-c", "./testdata/testbox/19700101000000.zettel", "-a", info.adminAddress[1:]} - logCommand("FORK", nil, name, arg) - cmd := prepareCommand(nil, name, arg, &info.out) - if !verbose { - cmd.Stderr = nil - } - err := cmd.Start() - for i := 0; i < 100; i++ { - time.Sleep(time.Millisecond * 100) - if addressInUse(info.adminAddress) { - info.cmd = cmd - return err - } - } - return errors.New("zettelstore did not start") -} - -func stopZettelstore(i *zsInfo) error { - conn, err := net.Dial("tcp", i.adminAddress) - if err != nil { - fmt.Println("Unable to stop Zettelstore") - return err - } - io.WriteString(conn, "shutdown\n") - conn.Close() - err = i.cmd.Wait() - return err -} - -func addressInUse(address string) bool { - conn, err := net.Dial("tcp", address) - if err != nil { - return false - } - conn.Close() - return true -} - func cmdBuild() error { return doBuild(nil, getVersion(), "bin/zettelstore") } func doBuild(env []string, version, target string) error { @@ -489,11 +408,10 @@ clean Remove all build and release directories. help Outputs this text. manual Create a ZIP file with all manual zettel release Create the software for various platforms and put them in appropriate named ZIP files. - testapi Starts a Zettelstore and execute API tests. version Print the current version of the software. All commands can be abbreviated as long as they remain unique.`) } @@ -520,12 +438,10 @@ err = cmdClean() case "v", "ve", "ver", "vers", "versi", "versio", "version": fmt.Print(getVersion()) case "ch", "che", "chec", "check": err = cmdCheck() - case "t", "te", "tes", "test", "testa", "testap", "testapi": - cmdTestAPI() case "h", "he", "hel", "help": cmdHelp() default: fmt.Fprintf(os.Stderr, "Unknown command %q\n", args[0]) cmdHelp() Index: usecase/context.go ================================================================== --- usecase/context.go +++ usecase/context.go @@ -42,10 +42,21 @@ _ ZettelContextDirection = iota ZettelContextForward // Traverse all forwarding links ZettelContextBackward // Traverse all backwaring links ZettelContextBoth // Traverse both directions ) + +// ParseZCDirection returns a direction value for a given string. +func ParseZCDirection(s string) ZettelContextDirection { + switch s { + case "backward": + return ZettelContextBackward + case "forward": + return ZettelContextForward + } + return ZettelContextBoth +} // Run executes the use case. func (uc ZettelContext) Run(ctx context.Context, zid id.Zid, dir ZettelContextDirection, depth, limit int) (result []*meta.Meta, err error) { start, err := uc.port.GetMeta(ctx, zid) if err != nil { Index: usecase/copy_zettel.go ================================================================== --- usecase/copy_zettel.go +++ usecase/copy_zettel.go @@ -12,10 +12,11 @@ package usecase import ( "zettelstore.de/z/domain" "zettelstore.de/z/domain/meta" + "zettelstore.de/z/strfun" ) // CopyZettel is the data for this use case. type CopyZettel struct{} @@ -33,9 +34,8 @@ } else { title = "Copy" } m.Set(meta.KeyTitle, title) } - content := origZettel.Content - content.TrimSpace() - return domain.Zettel{Meta: m, Content: content} + content := strfun.TrimSpaceRight(origZettel.Content.AsString()) + return domain.Zettel{Meta: m, Content: domain.Content(content)} } Index: usecase/create_zettel.go ================================================================== --- usecase/create_zettel.go +++ usecase/create_zettel.go @@ -16,10 +16,11 @@ "zettelstore.de/z/config" "zettelstore.de/z/domain" "zettelstore.de/z/domain/id" "zettelstore.de/z/domain/meta" + "zettelstore.de/z/strfun" ) // CreateZettelPort is the interface used by this use case. type CreateZettelPort interface { // CreateZettel creates a new zettel. @@ -56,8 +57,8 @@ if syntax, ok := m.Get(meta.KeySyntax); !ok || syntax == "" { m.Set(meta.KeySyntax, uc.rtConfig.GetDefaultSyntax()) } m.YamlSep = uc.rtConfig.GetYAMLHeader() - zettel.Content.TrimSpace() + zettel.Content = domain.Content(strfun.TrimSpaceRight(zettel.Content.AsString())) return uc.port.CreateZettel(ctx, zettel) } Index: usecase/delete_zettel.go ================================================================== --- usecase/delete_zettel.go +++ usecase/delete_zettel.go @@ -17,11 +17,11 @@ "zettelstore.de/z/domain/id" ) // DeleteZettelPort is the interface used by this use case. type DeleteZettelPort interface { - // DeleteZettel removes the zettel from the box. + // DeleteZettel removes the zettel from the place. DeleteZettel(ctx context.Context, zid id.Zid) error } // DeleteZettel is the data for this use case. type DeleteZettel struct { Index: usecase/folge_zettel.go ================================================================== --- usecase/folge_zettel.go +++ usecase/folge_zettel.go @@ -1,7 +1,7 @@ //----------------------------------------------------------------------------- -// Copyright (c) 2020-2021 Detlef Stern +// 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 @@ -42,7 +42,7 @@ } m.Set(meta.KeyRole, config.GetRole(origMeta, uc.rtConfig)) m.Set(meta.KeyTags, origMeta.GetDefault(meta.KeyTags, "")) m.Set(meta.KeySyntax, uc.rtConfig.GetDefaultSyntax()) m.Set(meta.KeyPrecursor, origMeta.Zid.String()) - return domain.Zettel{Meta: m, Content: domain.NewContent("")} + return domain.Zettel{Meta: m, Content: ""} } DELETED usecase/get_all_meta.go Index: usecase/get_all_meta.go ================================================================== --- usecase/get_all_meta.go +++ usecase/get_all_meta.go @@ -1,40 +0,0 @@ -//----------------------------------------------------------------------------- -// Copyright (c) 2021 Detlef Stern -// -// This file is part of zettelstore. -// -// Zettelstore is licensed under the latest version of the EUPL (European Union -// Public License). Please see file LICENSE.txt for your rights and obligations -// under this license. -//----------------------------------------------------------------------------- - -// Package usecase provides (business) use cases for the zettelstore. -package usecase - -import ( - "context" - - "zettelstore.de/z/domain/id" - "zettelstore.de/z/domain/meta" -) - -// GetAllMetaPort is the interface used by this use case. -type GetAllMetaPort interface { - // GetAllMeta retrieves just the meta data of a specific zettel. - GetAllMeta(ctx context.Context, zid id.Zid) ([]*meta.Meta, error) -} - -// GetAllMeta is the data for this use case. -type GetAllMeta struct { - port GetAllMetaPort -} - -// NewGetAllMeta creates a new use case. -func NewGetAllMeta(port GetAllMetaPort) GetAllMeta { - return GetAllMeta{port: port} -} - -// Run executes the use case. -func (uc GetAllMeta) Run(ctx context.Context, zid id.Zid) ([]*meta.Meta, error) { - return uc.port.GetAllMeta(ctx, zid) -} Index: usecase/get_user.go ================================================================== --- usecase/get_user.go +++ usecase/get_user.go @@ -13,13 +13,13 @@ import ( "context" "zettelstore.de/z/auth" - "zettelstore.de/z/box" "zettelstore.de/z/domain/id" "zettelstore.de/z/domain/meta" + "zettelstore.de/z/place" "zettelstore.de/z/search" ) // Use case: return user identified by meta key ident. // --------------------------------------------------- @@ -41,11 +41,11 @@ return GetUser{authz: authz, port: port} } // Run executes the use case. func (uc GetUser) Run(ctx context.Context, ident string) (*meta.Meta, error) { - ctx = box.NoEnrichContext(ctx) + ctx = place.NoEnrichContext(ctx) // It is important to try first with the owner. First, because another user // could give herself the same ''ident''. Second, in most cases the owner // will authenticate. identMeta, err := uc.port.GetMeta(ctx, uc.authz.Owner()) @@ -88,15 +88,15 @@ return GetUserByZid{port: port} } // GetUser executes the use case. func (uc GetUserByZid) GetUser(ctx context.Context, zid id.Zid, ident string) (*meta.Meta, error) { - userMeta, err := uc.port.GetMeta(box.NoEnrichContext(ctx), zid) + userMeta, err := uc.port.GetMeta(place.NoEnrichContext(ctx), zid) if err != nil { return nil, err } if val, ok := userMeta.Get(meta.KeyUserID); !ok || val != ident { return nil, nil } return userMeta, nil } Index: usecase/list_role.go ================================================================== --- usecase/list_role.go +++ usecase/list_role.go @@ -13,12 +13,12 @@ import ( "context" "sort" - "zettelstore.de/z/box" "zettelstore.de/z/domain/meta" + "zettelstore.de/z/place" "zettelstore.de/z/search" ) // ListRolePort is the interface used by this use case. type ListRolePort interface { @@ -36,11 +36,11 @@ return ListRole{port: port} } // Run executes the use case. func (uc ListRole) Run(ctx context.Context) ([]string, error) { - metas, err := uc.port.SelectMeta(box.NoEnrichContext(ctx), nil) + metas, err := uc.port.SelectMeta(place.NoEnrichContext(ctx), nil) if err != nil { return nil, err } roles := make(map[string]bool, 8) for _, m := range metas { Index: usecase/list_tags.go ================================================================== --- usecase/list_tags.go +++ usecase/list_tags.go @@ -12,12 +12,12 @@ package usecase import ( "context" - "zettelstore.de/z/box" "zettelstore.de/z/domain/meta" + "zettelstore.de/z/place" "zettelstore.de/z/search" ) // ListTagsPort is the interface used by this use case. type ListTagsPort interface { @@ -38,11 +38,11 @@ // TagData associates tags with a list of all zettel meta that use this tag type TagData map[string][]*meta.Meta // Run executes the use case. func (uc ListTags) Run(ctx context.Context, minCount int) (TagData, error) { - metas, err := uc.port.SelectMeta(box.NoEnrichContext(ctx), nil) + metas, err := uc.port.SelectMeta(place.NoEnrichContext(ctx), nil) if err != nil { return nil, err } result := make(TagData) for _, m := range metas { Index: usecase/new_zettel.go ================================================================== --- usecase/new_zettel.go +++ usecase/new_zettel.go @@ -11,12 +11,11 @@ // Package usecase provides (business) use cases for the zettelstore. package usecase import ( "zettelstore.de/z/domain" - "zettelstore.de/z/domain/id" - "zettelstore.de/z/domain/meta" + "zettelstore.de/z/strfun" ) // NewZettel is the data for this use case. type NewZettel struct{} @@ -25,22 +24,16 @@ return NewZettel{} } // Run executes the use case. func (uc NewZettel) Run(origZettel domain.Zettel) domain.Zettel { - m := meta.New(id.Invalid) - om := origZettel.Meta - m.Set(meta.KeyTitle, om.GetDefault(meta.KeyTitle, "")) - m.Set(meta.KeyRole, om.GetDefault(meta.KeyRole, "")) - m.Set(meta.KeyTags, om.GetDefault(meta.KeyTags, "")) - m.Set(meta.KeySyntax, om.GetDefault(meta.KeySyntax, "")) - - const prefixLen = len(meta.NewPrefix) - for _, pair := range om.PairsRest(false) { - if key := pair.Key; len(key) > prefixLen && key[0:prefixLen] == meta.NewPrefix { - m.Set(key[prefixLen:], pair.Value) - } - } - content := origZettel.Content - content.TrimSpace() - return domain.Zettel{Meta: m, Content: content} + m := origZettel.Meta.Clone() + const prefix = "new-" + for _, pair := range m.PairsRest(false) { + if key := pair.Key; len(key) > len(prefix) && key[0:len(prefix)] == prefix { + m.Set(key[len(prefix):], pair.Value) + m.Delete(key) + } + } + content := strfun.TrimSpaceRight(origZettel.Content.AsString()) + return domain.Zettel{Meta: m, Content: domain.Content(content)} } Index: usecase/order.go ================================================================== --- usecase/order.go +++ usecase/order.go @@ -35,13 +35,13 @@ func NewZettelOrder(port ZettelOrderPort, parseZettel ParseZettel) ZettelOrder { return ZettelOrder{port: port, parseZettel: parseZettel} } // Run executes the use case. -func (uc ZettelOrder) Run(ctx context.Context, zid id.Zid, syntax string) ( - start *meta.Meta, result []*meta.Meta, err error, -) { +func (uc ZettelOrder) Run( + ctx context.Context, zid id.Zid, syntax string, +) (start *meta.Meta, result []*meta.Meta, err error) { zn, err := uc.parseZettel.Run(ctx, zid, syntax) if err != nil { return nil, nil, err } for _, ref := range collect.Order(zn) { Index: usecase/rename_zettel.go ================================================================== --- usecase/rename_zettel.go +++ usecase/rename_zettel.go @@ -12,13 +12,13 @@ package usecase import ( "context" - "zettelstore.de/z/box" "zettelstore.de/z/domain/id" "zettelstore.de/z/domain/meta" + "zettelstore.de/z/place" ) // RenameZettelPort is the interface used by this use case. type RenameZettelPort interface { // GetMeta retrieves just the meta data of a specific zettel. @@ -31,11 +31,11 @@ // RenameZettel is the data for this use case. type RenameZettel struct { port RenameZettelPort } -// ErrZidInUse is returned if the zettel id is not appropriate for the box operation. +// ErrZidInUse is returned if the zettel id is not appropriate for the place operation. type ErrZidInUse struct{ Zid id.Zid } func (err *ErrZidInUse) Error() string { return "Zettel id already in use: " + err.Zid.String() } @@ -45,11 +45,11 @@ return RenameZettel{port: port} } // Run executes the use case. func (uc RenameZettel) Run(ctx context.Context, curZid, newZid id.Zid) error { - noEnrichCtx := box.NoEnrichContext(ctx) + noEnrichCtx := place.NoEnrichContext(ctx) if _, err := uc.port.GetMeta(noEnrichCtx, curZid); err != nil { return err } if newZid == curZid { // Nothing to do Index: usecase/search.go ================================================================== --- usecase/search.go +++ usecase/search.go @@ -12,12 +12,12 @@ package usecase import ( "context" - "zettelstore.de/z/box" "zettelstore.de/z/domain/meta" + "zettelstore.de/z/place" "zettelstore.de/z/search" ) // SearchPort is the interface used by this use case. type SearchPort interface { @@ -36,9 +36,9 @@ } // Run executes the use case. func (uc Search) Run(ctx context.Context, s *search.Search) ([]*meta.Meta, error) { if !s.HasComputedMetaKey() { - ctx = box.NoEnrichContext(ctx) + ctx = place.NoEnrichContext(ctx) } return uc.port.SelectMeta(ctx, s) } Index: usecase/update_zettel.go ================================================================== --- usecase/update_zettel.go +++ usecase/update_zettel.go @@ -12,14 +12,15 @@ package usecase import ( "context" - "zettelstore.de/z/box" "zettelstore.de/z/domain" "zettelstore.de/z/domain/id" "zettelstore.de/z/domain/meta" + "zettelstore.de/z/place" + "zettelstore.de/z/strfun" ) // UpdateZettelPort is the interface used by this use case. type UpdateZettelPort interface { // GetZettel retrieves a specific zettel. @@ -40,11 +41,11 @@ } // Run executes the use case. func (uc UpdateZettel) Run(ctx context.Context, zettel domain.Zettel, hasContent bool) error { m := zettel.Meta - oldZettel, err := uc.port.GetZettel(box.NoEnrichContext(ctx), m.Zid) + oldZettel, err := uc.port.GetZettel(place.NoEnrichContext(ctx), m.Zid) if err != nil { return err } if zettel.Equal(oldZettel, false) { return nil @@ -53,10 +54,9 @@ m.YamlSep = oldZettel.Meta.YamlSep if m.Zid == id.ConfigurationZid { m.Set(meta.KeySyntax, meta.ValueSyntaxNone) } if !hasContent { - zettel.Content = oldZettel.Content - zettel.Content.TrimSpace() + zettel.Content = domain.Content(strfun.TrimSpaceRight(oldZettel.Content.AsString())) } return uc.port.UpdateZettel(ctx, zettel) } Index: web/adapter/api/api.go ================================================================== --- web/adapter/api/api.go +++ web/adapter/api/api.go @@ -13,11 +13,10 @@ import ( "context" "time" - "zettelstore.de/z/api" "zettelstore.de/z/auth" "zettelstore.de/z/config" "zettelstore.de/z/domain/meta" "zettelstore.de/z/kernel" "zettelstore.de/z/web/server" @@ -50,14 +49,14 @@ // GetURLPrefix returns the configured URL prefix of the web server. func (api *API) GetURLPrefix() string { return api.b.GetURLPrefix() } // NewURLBuilder creates a new URL builder object with the given key. -func (api *API) NewURLBuilder(key byte) *api.URLBuilder { return api.b.NewURLBuilder(key) } +func (api *API) NewURLBuilder(key byte) server.URLBuilder { return api.b.NewURLBuilder(key) } func (api *API) getAuthData(ctx context.Context) *server.AuthData { return api.auth.GetAuthData(ctx) } func (api *API) withAuth() bool { return api.authz.WithAuth() } func (api *API) getToken(ident *meta.Meta) ([]byte, error) { return api.token.GetToken(ident, api.tokenLifetime, auth.KindJSON) } Index: web/adapter/api/content_type.go ================================================================== --- web/adapter/api/content_type.go +++ web/adapter/api/content_type.go @@ -1,7 +1,7 @@ //----------------------------------------------------------------------------- -// Copyright (c) 2020-2021 Detlef Stern +// 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 @@ -9,25 +9,23 @@ //----------------------------------------------------------------------------- // Package api provides api handlers for web requests. package api -import "zettelstore.de/z/api" - const plainText = "text/plain; charset=utf-8" -var mapFormat2CT = map[api.EncodingEnum]string{ - api.EncoderHTML: "text/html; charset=utf-8", - api.EncoderNative: plainText, - api.EncoderJSON: "application/json", - api.EncoderDJSON: "application/json", - api.EncoderText: plainText, - api.EncoderZmk: plainText, - api.EncoderRaw: plainText, // In some cases... +var mapFormat2CT = map[string]string{ + "html": "text/html; charset=utf-8", + "native": plainText, + "json": "application/json", + "djson": "application/json", + "text": plainText, + "zmk": plainText, + "raw": plainText, // In some cases... } -func format2ContentType(format api.EncodingEnum) string { +func format2ContentType(format string) string { ct, ok := mapFormat2CT[format] if !ok { return "application/octet-stream" } return ct DELETED web/adapter/api/create_zettel.go Index: web/adapter/api/create_zettel.go ================================================================== --- web/adapter/api/create_zettel.go +++ web/adapter/api/create_zettel.go @@ -1,48 +0,0 @@ -//----------------------------------------------------------------------------- -// Copyright (c) 2021 Detlef Stern -// -// This file is part of zettelstore. -// -// Zettelstore is licensed under the latest version of the EUPL (European Union -// Public License). Please see file LICENSE.txt for your rights and obligations -// under this license. -//----------------------------------------------------------------------------- - -// Package api provides api handlers for web requests. -package api - -import ( - "net/http" - - zsapi "zettelstore.de/z/api" - "zettelstore.de/z/domain/id" - "zettelstore.de/z/usecase" - "zettelstore.de/z/web/adapter" -) - -// MakePostCreateZettelHandler creates a new HTTP handler to store content of -// an existing zettel. -func (api *API) MakePostCreateZettelHandler(createZettel usecase.CreateZettel) http.HandlerFunc { - return func(w http.ResponseWriter, r *http.Request) { - ctx := r.Context() - zettel, err := buildZettelFromData(r, id.Invalid) - if err != nil { - adapter.ReportUsecaseError(w, adapter.NewErrBadRequest(err.Error())) - return - } - - newZid, err := createZettel.Run(ctx, zettel) - if err != nil { - adapter.ReportUsecaseError(w, err) - return - } - u := api.NewURLBuilder('z').SetZid(newZid).String() - h := w.Header() - h.Set(zsapi.HeaderContentType, format2ContentType(zsapi.EncoderJSON)) - h.Set(zsapi.HeaderLocation, u) - w.WriteHeader(http.StatusCreated) - if err = encodeJSONData(w, zsapi.ZidJSON{ID: newZid.String(), URL: u}); err != nil { - adapter.InternalServerError(w, "Write JSON", err) - } - } -} DELETED web/adapter/api/delete_zettel.go Index: web/adapter/api/delete_zettel.go ================================================================== --- web/adapter/api/delete_zettel.go +++ web/adapter/api/delete_zettel.go @@ -1,37 +0,0 @@ -//----------------------------------------------------------------------------- -// Copyright (c) 2021 Detlef Stern -// -// This file is part of zettelstore. -// -// Zettelstore is licensed under the latest version of the EUPL (European Union -// Public License). Please see file LICENSE.txt for your rights and obligations -// under this license. -//----------------------------------------------------------------------------- - -// Package api provides api handlers for web requests. -package api - -import ( - "net/http" - - "zettelstore.de/z/domain/id" - "zettelstore.de/z/usecase" - "zettelstore.de/z/web/adapter" -) - -// MakeDeleteZettelHandler creates a new HTTP handler to delete a zettel. -func (api *API) MakeDeleteZettelHandler(deleteZettel usecase.DeleteZettel) http.HandlerFunc { - return func(w http.ResponseWriter, r *http.Request) { - zid, err := id.Parse(r.URL.Path[1:]) - if err != nil { - http.NotFound(w, r) - return - } - - if err := deleteZettel.Run(r.Context(), zid); err != nil { - adapter.ReportUsecaseError(w, err) - return - } - w.WriteHeader(http.StatusNoContent) - } -} Index: web/adapter/api/get_links.go ================================================================== --- web/adapter/api/get_links.go +++ web/adapter/api/get_links.go @@ -10,21 +10,37 @@ // Package api provides api handlers for web requests. package api import ( + "encoding/json" "net/http" "strconv" - zsapi "zettelstore.de/z/api" "zettelstore.de/z/ast" "zettelstore.de/z/collect" "zettelstore.de/z/domain/id" - "zettelstore.de/z/domain/meta" "zettelstore.de/z/usecase" "zettelstore.de/z/web/adapter" ) + +type jsonGetLinks struct { + ID string `json:"id"` + URL string `json:"url"` + Links struct { + Incoming []jsonIDURL `json:"incoming"` + Outgoing []jsonIDURL `json:"outgoing"` + Local []string `json:"local"` + External []string `json:"external"` + } `json:"links"` + Images struct { + Outgoing []jsonIDURL `json:"outgoing"` + Local []string `json:"local"` + External []string `json:"external"` + } `json:"images"` + Cites []string `json:"cites"` +} // MakeGetLinksHandler creates a new API handler to return links to other material. func (api *API) MakeGetLinksHandler(parseZettel usecase.ParseZettel) http.HandlerFunc { return func(w http.ResponseWriter, r *http.Request) { zid, err := id.Parse(r.URL.Path[1:]) @@ -32,11 +48,11 @@ http.NotFound(w, r) return } ctx := r.Context() q := r.URL.Query() - zn, err := parseZettel.Run(ctx, zid, q.Get(meta.KeySyntax)) + zn, err := parseZettel.Run(ctx, zid, q.Get("syntax")) if err != nil { adapter.ReportUsecaseError(w, err) return } summary := collect.References(zn) @@ -46,40 +62,34 @@ if !validKindMatter(kind, matter) { adapter.BadRequest(w, "Invalid kind/matter") return } - outData := zsapi.ZettelLinksJSON{ + outData := jsonGetLinks{ ID: zid.String(), URL: api.NewURLBuilder('z').SetZid(zid).String(), } if kind&kindLink != 0 { api.setupLinkJSONRefs(summary, matter, &outData) - if matter&matterMeta != 0 { - for _, p := range zn.Meta.PairsRest(false) { - if meta.Type(p.Key) == meta.TypeURL { - outData.Links.Meta = append(outData.Links.Meta, p.Value) - } - } - } } if kind&kindImage != 0 { api.setupImageJSONRefs(summary, matter, &outData) } if kind&kindCite != 0 { outData.Cites = stringCites(summary.Cites) } - w.Header().Set(zsapi.HeaderContentType, format2ContentType(zsapi.EncoderJSON)) - encodeJSONData(w, outData) + w.Header().Set(adapter.ContentType, format2ContentType("json")) + enc := json.NewEncoder(w) + enc.SetEscapeHTML(false) + enc.Encode(&outData) } } -func (api *API) setupLinkJSONRefs(summary collect.Summary, matter matterType, outData *zsapi.ZettelLinksJSON) { +func (api *API) setupLinkJSONRefs(summary collect.Summary, matter matterType, outData *jsonGetLinks) { if matter&matterIncoming != 0 { - // TODO: calculate incoming links from other zettel (via "backward" metadata?) - outData.Links.Incoming = []zsapi.ZidJSON{} + outData.Links.Incoming = []jsonIDURL{} } zetRefs, locRefs, extRefs := collect.DivideReferences(summary.Links) if matter&matterOutgoing != 0 { outData.Links.Outgoing = api.idURLRefs(zetRefs) } @@ -89,11 +99,11 @@ if matter&matterExternal != 0 { outData.Links.External = stringRefs(extRefs) } } -func (api *API) setupImageJSONRefs(summary collect.Summary, matter matterType, outData *zsapi.ZettelLinksJSON) { +func (api *API) setupImageJSONRefs(summary collect.Summary, matter matterType, outData *jsonGetLinks) { zetRefs, locRefs, extRefs := collect.DivideReferences(summary.Images) if matter&matterOutgoing != 0 { outData.Images.Outgoing = api.idURLRefs(zetRefs) } if matter&matterLocal != 0 { @@ -102,19 +112,19 @@ if matter&matterExternal != 0 { outData.Images.External = stringRefs(extRefs) } } -func (api *API) idURLRefs(refs []*ast.Reference) []zsapi.ZidJSON { - result := make([]zsapi.ZidJSON, 0, len(refs)) +func (api *API) idURLRefs(refs []*ast.Reference) []jsonIDURL { + result := make([]jsonIDURL, 0, len(refs)) for _, ref := range refs { path := ref.URL.Path ub := api.NewURLBuilder('z').AppendPath(path) if fragment := ref.URL.Fragment; len(fragment) > 0 { ub.SetFragment(fragment) } - result = append(result, zsapi.ZidJSON{ID: path, URL: ub.String()}) + result = append(result, jsonIDURL{ID: path, URL: ub.String()}) } return result } func stringRefs(refs []*ast.Reference) []string { @@ -171,23 +181,21 @@ _ matterType = 1 << iota matterIncoming matterOutgoing matterLocal matterExternal - matterMeta ) var mapMatter = map[string]matterType{ - "": matterIncoming | matterOutgoing | matterLocal | matterExternal | matterMeta, + "": matterIncoming | matterOutgoing | matterLocal | matterExternal, "incoming": matterIncoming, "outgoing": matterOutgoing, "local": matterLocal, "external": matterExternal, - "meta": matterMeta, "zettel": matterIncoming | matterOutgoing, - "material": matterLocal | matterExternal | matterMeta, - "all": matterIncoming | matterOutgoing | matterLocal | matterExternal | matterMeta, + "material": matterLocal | matterExternal, + "all": matterIncoming | matterOutgoing | matterLocal | matterExternal, } func getMatterFromValue(value string) matterType { if m, ok := mapMatter[value]; ok { return m Index: web/adapter/api/get_order.go ================================================================== --- web/adapter/api/get_order.go +++ web/adapter/api/get_order.go @@ -13,11 +13,10 @@ import ( "net/http" "zettelstore.de/z/domain/id" - "zettelstore.de/z/domain/meta" "zettelstore.de/z/usecase" "zettelstore.de/z/web/adapter" ) // MakeGetOrderHandler creates a new API handler to return zettel references @@ -29,13 +28,13 @@ http.NotFound(w, r) return } ctx := r.Context() q := r.URL.Query() - start, metas, err := zettelOrder.Run(ctx, zid, q.Get(meta.KeySyntax)) + start, metas, err := zettelOrder.Run(ctx, zid, q.Get("syntax")) if err != nil { adapter.ReportUsecaseError(w, err) return } api.writeMetaList(w, start, metas) } } Index: web/adapter/api/get_role_list.go ================================================================== --- web/adapter/api/get_role_list.go +++ web/adapter/api/get_role_list.go @@ -13,12 +13,12 @@ import ( "fmt" "net/http" - zsapi "zettelstore.de/z/api" "zettelstore.de/z/encoder" + "zettelstore.de/z/encoder/jsonenc" "zettelstore.de/z/usecase" "zettelstore.de/z/web/adapter" ) // MakeListRoleHandler creates a new HTTP handler for the use case "list some zettel". @@ -28,14 +28,20 @@ if err != nil { adapter.ReportUsecaseError(w, err) return } - format, formatText := adapter.GetFormat(r, r.URL.Query(), encoder.GetDefaultFormat()) + format := adapter.GetFormat(r, r.URL.Query(), encoder.GetDefaultFormat()) switch format { - case zsapi.EncoderJSON: - w.Header().Set(zsapi.HeaderContentType, format2ContentType(format)) - encodeJSONData(w, zsapi.RoleListJSON{Roles: roleList}) + case "json": + w.Header().Set(adapter.ContentType, format2ContentType(format)) + renderListRoleJSON(w, roleList) default: - adapter.BadRequest(w, fmt.Sprintf("Role list not available in format %q", formatText)) + adapter.BadRequest(w, fmt.Sprintf("Role list not available in format %q", format)) } + } +} + +func renderListRoleJSON(w http.ResponseWriter, roleList []string) { + buf := encoder.NewBufWriter(w) + @@ -42,2 +48,17 @@ + buf.WriteString("{\"role-list\":[") + first := true + for _, role := range roleList { + if first { + buf.WriteByte('"') + first = false + } else { + buf.WriteString("\",\"") + } + buf.Write(jsonenc.Escape(role)) + } + if !first { + buf.WriteByte('"') } + buf.WriteString("]}") + buf.Flush() } Index: web/adapter/api/get_tags_list.go ================================================================== --- web/adapter/api/get_tags_list.go +++ web/adapter/api/get_tags_list.go @@ -12,14 +12,15 @@ package api import ( "fmt" "net/http" + "sort" "strconv" - zsapi "zettelstore.de/z/api" "zettelstore.de/z/encoder" + "zettelstore.de/z/encoder/jsonenc" "zettelstore.de/z/usecase" "zettelstore.de/z/web/adapter" ) // MakeListTagsHandler creates a new HTTP handler for the use case "list some zettel". @@ -30,23 +31,50 @@ if err != nil { adapter.ReportUsecaseError(w, err) return } - format, formatText := adapter.GetFormat(r, r.URL.Query(), encoder.GetDefaultFormat()) + format := adapter.GetFormat(r, r.URL.Query(), encoder.GetDefaultFormat()) switch format { - case zsapi.EncoderJSON: - w.Header().Set(zsapi.HeaderContentType, format2ContentType(format)) - tagMap := make(map[string][]string, len(tagData)) - for tag, metaList := range tagData { - zidList := make([]string, 0, len(metaList)) - for _, m := range metaList { - zidList = append(zidList, m.Zid.String()) - } - tagMap[tag] = zidList - } - encodeJSONData(w, zsapi.TagListJSON{Tags: tagMap}) + case "json": + w.Header().Set(adapter.ContentType, format2ContentType(format)) + renderListTagsJSON(w, tagData) default: - adapter.BadRequest(w, fmt.Sprintf("Tags list not available in format %q", formatText)) + adapter.BadRequest(w, fmt.Sprintf("Tags list not available in format %q", format)) + } + } +} + +func renderListTagsJSON(w http.ResponseWriter, tagData usecase.TagData) { + buf := encoder.NewBufWriter(w) + + tagList := make([]string, 0, len(tagData)) + for tag := range tagData { + tagList = append(tagList, tag) + } + sort.Strings(tagList) + + buf.WriteString("{\"tags\":{") + first := true + for _, tag := range tagList { + if first { + buf.WriteByte('"') + first = false + } else { + buf.WriteString(",\"") + } + buf.Write(jsonenc.Escape(tag)) + buf.WriteString("\":[") + for i, meta := range tagData[tag] { + if i > 0 { + buf.WriteByte(',') + } + buf.WriteByte('"') + buf.WriteString(meta.Zid.String()) + buf.WriteByte('"') } + buf.WriteString("]") + } + buf.WriteString("}}") + buf.Flush() } Index: web/adapter/api/get_zettel.go ================================================================== --- web/adapter/api/get_zettel.go +++ web/adapter/api/get_zettel.go @@ -14,17 +14,16 @@ import ( "errors" "fmt" "net/http" - zsapi "zettelstore.de/z/api" "zettelstore.de/z/ast" - "zettelstore.de/z/box" "zettelstore.de/z/config" "zettelstore.de/z/domain/id" "zettelstore.de/z/domain/meta" "zettelstore.de/z/encoder" + "zettelstore.de/z/place" "zettelstore.de/z/usecase" "zettelstore.de/z/web/adapter" ) // MakeGetZettelHandler creates a new HTTP handler to return a rendered zettel. @@ -36,15 +35,15 @@ return } ctx := r.Context() q := r.URL.Query() - format, _ := adapter.GetFormat(r, q, encoder.GetDefaultFormat()) - if format == zsapi.EncoderRaw { - ctx = box.NoEnrichContext(ctx) + format := adapter.GetFormat(r, q, encoder.GetDefaultFormat()) + if format == "raw" { + ctx = place.NoEnrichContext(ctx) } - zn, err := parseZettel.Run(ctx, zid, q.Get(meta.KeySyntax)) + zn, err := parseZettel.Run(ctx, zid, q.Get("syntax")) if err != nil { adapter.ReportUsecaseError(w, err) return } @@ -52,12 +51,12 @@ if part == partUnknown { adapter.BadRequest(w, "Unknown _part parameter") return } switch format { - case zsapi.EncoderJSON, zsapi.EncoderDJSON: - w.Header().Set(zsapi.HeaderContentType, format2ContentType(format)) + case "json", "djson": + w.Header().Set(adapter.ContentType, format2ContentType(format)) err = api.getWriteMetaZettelFunc(ctx, format, part, partZettel, getMeta)(w, zn) if err != nil { adapter.InternalServerError(w, "Write D/JSON", err) } return @@ -89,42 +88,42 @@ adapter.InternalServerError(w, "Get zettel", err) } } } -func writeZettelPartZettel(w http.ResponseWriter, zn *ast.ZettelNode, format zsapi.EncodingEnum, env encoder.Environment) error { +func writeZettelPartZettel(w http.ResponseWriter, zn *ast.ZettelNode, format string, env encoder.Environment) error { enc := encoder.Create(format, &env) if enc == nil { return adapter.ErrNoSuchFormat } inhMeta := false - if format != zsapi.EncoderRaw { - w.Header().Set(zsapi.HeaderContentType, format2ContentType(format)) + if format != "raw" { + w.Header().Set(adapter.ContentType, format2ContentType(format)) inhMeta = true } _, err := enc.WriteZettel(w, zn, inhMeta) return err } -func writeZettelPartMeta(w http.ResponseWriter, zn *ast.ZettelNode, format zsapi.EncodingEnum) error { - w.Header().Set(zsapi.HeaderContentType, format2ContentType(format)) +func writeZettelPartMeta(w http.ResponseWriter, zn *ast.ZettelNode, format string) error { + w.Header().Set(adapter.ContentType, format2ContentType(format)) if enc := encoder.Create(format, nil); enc != nil { - if format == zsapi.EncoderRaw { + if format == "raw" { _, err := enc.WriteMeta(w, zn.Meta) return err } _, err := enc.WriteMeta(w, zn.InhMeta) return err } return adapter.ErrNoSuchFormat } -func (api *API) writeZettelPartContent(w http.ResponseWriter, zn *ast.ZettelNode, format zsapi.EncodingEnum, env encoder.Environment) error { - if format == zsapi.EncoderRaw { +func (api *API) writeZettelPartContent(w http.ResponseWriter, zn *ast.ZettelNode, format string, env encoder.Environment) error { + if format == "raw" { if ct, ok := syntax2contentType(config.GetSyntax(zn.Meta, api.rtConfig)); ok { - w.Header().Add(zsapi.HeaderContentType, ct) + w.Header().Add(adapter.ContentType, ct) } } else { - w.Header().Set(zsapi.HeaderContentType, format2ContentType(format)) + w.Header().Set(adapter.ContentType, format2ContentType(format)) } return writeContent(w, zn, format, &env) } Index: web/adapter/api/get_zettel_context.go ================================================================== --- web/adapter/api/get_zettel_context.go +++ web/adapter/api/get_zettel_context.go @@ -12,11 +12,10 @@ package api import ( "net/http" - zsapi "zettelstore.de/z/api" "zettelstore.de/z/domain/id" "zettelstore.de/z/usecase" "zettelstore.de/z/web/adapter" ) @@ -27,16 +26,16 @@ if err != nil { http.NotFound(w, r) return } q := r.URL.Query() - dir := adapter.GetZCDirection(q.Get(zsapi.QueryKeyDir)) - depth, ok := adapter.GetInteger(q, zsapi.QueryKeyDepth) + dir := usecase.ParseZCDirection(q.Get("dir")) + depth, ok := adapter.GetInteger(q, "depth") if !ok || depth < 0 { depth = 5 } - limit, ok := adapter.GetInteger(q, zsapi.QueryKeyLimit) + limit, ok := adapter.GetInteger(q, "limit") if !ok || limit < 0 { limit = 200 } ctx := r.Context() metaList, err := getContext.Run(ctx, zid, dir, depth, limit) Index: web/adapter/api/get_zettel_list.go ================================================================== --- web/adapter/api/get_zettel_list.go +++ web/adapter/api/get_zettel_list.go @@ -13,15 +13,14 @@ import ( "fmt" "net/http" - zsapi "zettelstore.de/z/api" - "zettelstore.de/z/box" "zettelstore.de/z/domain/meta" "zettelstore.de/z/encoder" "zettelstore.de/z/parser" + "zettelstore.de/z/place" "zettelstore.de/z/usecase" "zettelstore.de/z/web/adapter" ) // MakeListMetaHandler creates a new HTTP handler for the use case "list some zettel". @@ -32,36 +31,36 @@ ) http.HandlerFunc { return func(w http.ResponseWriter, r *http.Request) { ctx := r.Context() q := r.URL.Query() s := adapter.GetSearch(q, false) - format, formatText := adapter.GetFormat(r, q, encoder.GetDefaultFormat()) + format := adapter.GetFormat(r, q, encoder.GetDefaultFormat()) part := getPart(q, partMeta) if part == partUnknown { adapter.BadRequest(w, "Unknown _part parameter") return } ctx1 := ctx - if format == zsapi.EncoderHTML || (!s.HasComputedMetaKey() && (part == partID || part == partContent)) { - ctx1 = box.NoEnrichContext(ctx1) + if format == "html" || (!s.HasComputedMetaKey() && (part == partID || part == partContent)) { + ctx1 = place.NoEnrichContext(ctx1) } metaList, err := listMeta.Run(ctx1, s) if err != nil { adapter.ReportUsecaseError(w, err) return } - w.Header().Set(zsapi.HeaderContentType, format2ContentType(format)) + w.Header().Set(adapter.ContentType, format2ContentType(format)) switch format { - case zsapi.EncoderHTML: + case "html": api.renderListMetaHTML(w, metaList) - case zsapi.EncoderJSON, zsapi.EncoderDJSON: + case "json", "djson": api.renderListMetaXJSON(ctx, w, metaList, format, part, partMeta, getMeta, parseZettel) - case zsapi.EncoderNative, zsapi.EncoderRaw, zsapi.EncoderText, zsapi.EncoderZmk: - adapter.NotImplemented(w, fmt.Sprintf("Zettel list in format %q not yet implemented", formatText)) + case "native", "raw", "text", "zmk": + adapter.NotImplemented(w, fmt.Sprintf("Zettel list in format %q not yet implemented", format)) default: - adapter.BadRequest(w, fmt.Sprintf("Zettel list not available in format %q", formatText)) + adapter.BadRequest(w, fmt.Sprintf("Zettel list not available in format %q", format)) } } } func (api *API) renderListMetaHTML(w http.ResponseWriter, metaList []*meta.Meta) { @@ -68,20 +67,20 @@ env := encoder.Environment{Interactive: true} buf := encoder.NewBufWriter(w) buf.WriteStrings("\n\n
\n\n") buf.Flush() } Index: web/adapter/api/json.go ================================================================== --- web/adapter/api/json.go +++ web/adapter/api/json.go @@ -15,25 +15,53 @@ "context" "encoding/json" "io" "net/http" - zsapi "zettelstore.de/z/api" "zettelstore.de/z/ast" "zettelstore.de/z/domain" "zettelstore.de/z/domain/id" "zettelstore.de/z/domain/meta" "zettelstore.de/z/encoder" "zettelstore.de/z/usecase" "zettelstore.de/z/web/adapter" ) +type jsonIDURL struct { + ID string `json:"id"` + URL string `json:"url"` +} +type jsonZettel struct { + ID string `json:"id"` + URL string `json:"url"` + Meta map[string]string `json:"meta"` + Encoding string `json:"encoding"` + Content interface{} `json:"content"` +} +type jsonMeta struct { + ID string `json:"id"` + URL string `json:"url"` + Meta map[string]string `json:"meta"` +} +type jsonMetaList struct { + ID string `json:"id"` + URL string `json:"url"` + Meta map[string]string `json:"meta"` + List []jsonMeta `json:"list"` +} type jsonContent struct { - ID string `json:"id"` - URL string `json:"url"` - Encoding string `json:"encoding"` - Content string `json:"content"` + ID string `json:"id"` + URL string `json:"url"` + Encoding string `json:"encoding"` + Content interface{} `json:"content"` +} + +func encodedContent(content domain.Content) (string, interface{}) { + if content.IsBinary() { + return "base64", content.AsBytes() + } + return "", content.AsString() } var ( djsonMetaHeader = []byte(",\"meta\":") djsonContentHeader = []byte(",\"content\":") @@ -56,11 +84,11 @@ _, err = io.WriteString(w, api.NewURLBuilder('z').SetZid(zid).String()) } if err == nil { _, err = w.Write(djsonHeader3) if err == nil { - _, err = io.WriteString(w, zsapi.FormatDJSON) + _, err = io.WriteString(w, "djson") } } if err == nil { _, err = w.Write(djsonHeader4) } @@ -69,11 +97,11 @@ func (api *API) renderListMetaXJSON( ctx context.Context, w http.ResponseWriter, metaList []*meta.Meta, - format zsapi.EncodingEnum, + format string, part, defPart partType, getMeta usecase.GetMeta, parseZettel usecase.ParseZettel, ) { prepareZettel := api.getPrepareZettelFunc(ctx, parseZettel, part) @@ -94,11 +122,11 @@ } case partMeta, partID: return func(m *meta.Meta) (*ast.ZettelNode, error) { return &ast.ZettelNode{ Meta: m, - Content: domain.NewContent(""), + Content: "", Zid: m.Zid, InhMeta: api.rtConfig.AddDefaultValues(m), Ast: nil, }, nil } @@ -106,11 +134,11 @@ return nil } type writeZettelFunc func(io.Writer, *ast.ZettelNode) error -func (api *API) getWriteMetaZettelFunc(ctx context.Context, format zsapi.EncodingEnum, +func (api *API) getWriteMetaZettelFunc(ctx context.Context, format string, part, defPart partType, getMeta usecase.GetMeta) writeZettelFunc { switch part { case partZettel: return api.getWriteZettelFunc(ctx, format, defPart, getMeta) case partMeta: @@ -122,25 +150,25 @@ default: panic(part) } } -func (api *API) getWriteZettelFunc(ctx context.Context, format zsapi.EncodingEnum, +func (api *API) getWriteZettelFunc(ctx context.Context, format string, defPart partType, getMeta usecase.GetMeta) writeZettelFunc { - if format == zsapi.EncoderJSON { + if format == "json" { return func(w io.Writer, zn *ast.ZettelNode) error { - content, encoding := zn.Content.Encode() - return encodeJSONData(w, zsapi.ZettelJSON{ + encoding, content := encodedContent(zn.Content) + return encodeJSONData(w, jsonZettel{ ID: zn.Zid.String(), URL: api.NewURLBuilder('z').SetZid(zn.Zid).String(), Meta: zn.InhMeta.Map(), Encoding: encoding, Content: content, }) } } - enc := encoder.Create(zsapi.EncoderDJSON, nil) + enc := encoder.Create("djson", nil) if enc == nil { panic("no DJSON encoder found") } return func(w io.Writer, zn *ast.ZettelNode) error { err := api.writeDJSONHeader(w, zn.Zid) @@ -157,32 +185,31 @@ } _, err = w.Write(djsonContentHeader) if err != nil { return err } - err = writeContent(w, zn, zsapi.EncoderDJSON, &encoder.Environment{ - LinkAdapter: adapter.MakeLinkAdapter( - ctx, api, 'z', getMeta, partZettel.DefString(defPart), zsapi.EncoderDJSON), + err = writeContent(w, zn, "djson", &encoder.Environment{ + LinkAdapter: adapter.MakeLinkAdapter(ctx, api, 'z', getMeta, partZettel.DefString(defPart), "djson"), ImageAdapter: adapter.MakeImageAdapter(ctx, api, getMeta)}) if err != nil { return err } _, err = w.Write(djsonFooter) return err } } -func (api *API) getWriteMetaFunc(ctx context.Context, format zsapi.EncodingEnum) writeZettelFunc { - if format == zsapi.EncoderJSON { +func (api *API) getWriteMetaFunc(ctx context.Context, format string) writeZettelFunc { + if format == "json" { return func(w io.Writer, zn *ast.ZettelNode) error { - return encodeJSONData(w, zsapi.ZidMetaJSON{ + return encodeJSONData(w, jsonMeta{ ID: zn.Zid.String(), URL: api.NewURLBuilder('z').SetZid(zn.Zid).String(), Meta: zn.InhMeta.Map(), }) } } - enc := encoder.Create(zsapi.EncoderDJSON, nil) + enc := encoder.Create("djson", nil) if enc == nil { panic("no DJSON encoder found") } return func(w io.Writer, zn *ast.ZettelNode) error { err := api.writeDJSONHeader(w, zn.Zid) @@ -199,15 +226,15 @@ } _, err = w.Write(djsonFooter) return err } } -func (api *API) getWriteContentFunc(ctx context.Context, format zsapi.EncodingEnum, +func (api *API) getWriteContentFunc(ctx context.Context, format string, defPart partType, getMeta usecase.GetMeta) writeZettelFunc { - if format == zsapi.EncoderJSON { + if format == "json" { return func(w io.Writer, zn *ast.ZettelNode) error { - content, encoding := zn.Content.Encode() + encoding, content := encodedContent(zn.Content) return encodeJSONData(w, jsonContent{ ID: zn.Zid.String(), URL: api.NewURLBuilder('z').SetZid(zn.Zid).String(), Encoding: encoding, Content: content, @@ -221,25 +248,24 @@ } _, err = w.Write(djsonContentHeader) if err != nil { return err } - err = writeContent(w, zn, zsapi.EncoderDJSON, &encoder.Environment{ - LinkAdapter: adapter.MakeLinkAdapter( - ctx, api, 'z', getMeta, partContent.DefString(defPart), zsapi.EncoderDJSON), + err = writeContent(w, zn, "djson", &encoder.Environment{ + LinkAdapter: adapter.MakeLinkAdapter(ctx, api, 'z', getMeta, partContent.DefString(defPart), "djson"), ImageAdapter: adapter.MakeImageAdapter(ctx, api, getMeta)}) if err != nil { return err } _, err = w.Write(djsonFooter) return err } } -func (api *API) getWriteIDFunc(ctx context.Context, format zsapi.EncodingEnum) writeZettelFunc { - if format == zsapi.EncoderJSON { +func (api *API) getWriteIDFunc(ctx context.Context, format string) writeZettelFunc { + if format == "json" { return func(w io.Writer, zn *ast.ZettelNode) error { - return encodeJSONData(w, zsapi.ZidJSON{ + return encodeJSONData(w, jsonIDURL{ ID: zn.Zid.String(), URL: api.NewURLBuilder('z').SetZid(zn.Zid).String(), }) } } @@ -281,11 +307,11 @@ _, err = w.Write(jsonListFooter) } return err } -func writeContent(w io.Writer, zn *ast.ZettelNode, format zsapi.EncodingEnum, env *encoder.Environment) error { +func writeContent(w io.Writer, zn *ast.ZettelNode, format string, env *encoder.Environment) error { enc := encoder.Create(format, env) if enc == nil { return adapter.ErrNoSuchFormat } @@ -298,37 +324,19 @@ enc.SetEscapeHTML(false) return enc.Encode(data) } func (api *API) writeMetaList(w http.ResponseWriter, m *meta.Meta, metaList []*meta.Meta) error { - outList := make([]zsapi.ZidMetaJSON, len(metaList)) - for i, m := range metaList { - outList[i].ID = m.Zid.String() - outList[i].URL = api.NewURLBuilder('z').SetZid(m.Zid).String() - outList[i].Meta = m.Map() - } - w.Header().Set(zsapi.HeaderContentType, format2ContentType(zsapi.EncoderJSON)) - return encodeJSONData(w, zsapi.ZidMetaRelatedList{ + outData := jsonMetaList{ ID: m.Zid.String(), URL: api.NewURLBuilder('z').SetZid(m.Zid).String(), Meta: m.Map(), - List: outList, - }) -} - -func buildZettelFromData(r *http.Request, zid id.Zid) (domain.Zettel, error) { - var zettel domain.Zettel - dec := json.NewDecoder(r.Body) - var zettelData zsapi.ZettelDataJSON - if err := dec.Decode(&zettelData); err != nil { - return zettel, err - } - m := meta.New(zid) - for k, v := range zettelData.Meta { - m.Set(k, v) - } - zettel.Meta = m - if err := zettel.Content.SetDecoded(zettelData.Content, zettelData.Encoding); err != nil { - return zettel, err - } - return zettel, nil + List: make([]jsonMeta, len(metaList)), + } + for i, m := range metaList { + outData.List[i].ID = m.Zid.String() + outData.List[i].URL = api.NewURLBuilder('z').SetZid(m.Zid).String() + outData.List[i].Meta = m.Map() + } + w.Header().Set(adapter.ContentType, format2ContentType("json")) + return encodeJSONData(w, outData) } Index: web/adapter/api/login.go ================================================================== --- web/adapter/api/login.go +++ web/adapter/api/login.go @@ -14,21 +14,20 @@ import ( "encoding/json" "net/http" "time" - zsapi "zettelstore.de/z/api" "zettelstore.de/z/auth" "zettelstore.de/z/usecase" "zettelstore.de/z/web/adapter" ) -// MakePostLoginHandler creates a new HTTP handler to authenticate the given user via API. -func (api *API) MakePostLoginHandler(ucAuth usecase.Authenticate) http.HandlerFunc { +// MakePostLoginHandlerAPI creates a new HTTP handler to authenticate the given user via API. +func (api *API) MakePostLoginHandlerAPI(ucAuth usecase.Authenticate) http.HandlerFunc { return func(w http.ResponseWriter, r *http.Request) { if !api.withAuth() { - w.Header().Set(zsapi.HeaderContentType, format2ContentType(zsapi.EncoderJSON)) + w.Header().Set(adapter.ContentType, format2ContentType("json")) writeJSONToken(w, "freeaccess", 24*366*10*time.Hour) return } var token []byte if ident, cred := retrieveIdentCred(r); ident != "" { @@ -43,11 +42,11 @@ w.Header().Set("WWW-Authenticate", `Bearer realm="Default"`) http.Error(w, "Authentication failed", http.StatusUnauthorized) return } - w.Header().Set(zsapi.HeaderContentType, format2ContentType(zsapi.EncoderJSON)) + w.Header().Set(adapter.ContentType, format2ContentType("json")) writeJSONToken(w, string(token), api.tokenLifetime) } } func retrieveIdentCred(r *http.Request) (string, string) { @@ -60,11 +59,15 @@ return "", "" } func writeJSONToken(w http.ResponseWriter, token string, lifetime time.Duration) { je := json.NewEncoder(w) - je.Encode(zsapi.AuthJSON{ + je.Encode(struct { + Token string `json:"access_token"` + Type string `json:"token_type"` + Expires int `json:"expires_in"` + }{ Token: token, Type: "Bearer", Expires: int(lifetime / time.Second), }) } @@ -80,11 +83,11 @@ } totalLifetime := authData.Expires.Sub(authData.Issued) currentLifetime := authData.Now.Sub(authData.Issued) // If we are in the first quarter of the tokens lifetime, return the token if currentLifetime*4 < totalLifetime { - w.Header().Set(zsapi.HeaderContentType, format2ContentType(zsapi.EncoderJSON)) + w.Header().Set(adapter.ContentType, format2ContentType("json")) writeJSONToken(w, string(authData.Token), totalLifetime-currentLifetime) return } // Token is a little bit aged. Create a new one @@ -91,9 +94,9 @@ token, err := api.getToken(authData.User) if err != nil { adapter.ReportUsecaseError(w, err) return } - w.Header().Set(zsapi.HeaderContentType, format2ContentType(zsapi.EncoderJSON)) + w.Header().Set(adapter.ContentType, format2ContentType("json")) writeJSONToken(w, string(token), api.tokenLifetime) } } DELETED web/adapter/api/rename_zettel.go Index: web/adapter/api/rename_zettel.go ================================================================== --- web/adapter/api/rename_zettel.go +++ web/adapter/api/rename_zettel.go @@ -1,71 +0,0 @@ -//----------------------------------------------------------------------------- -// Copyright (c) 2021 Detlef Stern -// -// This file is part of zettelstore. -// -// Zettelstore is licensed under the latest version of the EUPL (European Union -// Public License). Please see file LICENSE.txt for your rights and obligations -// under this license. -//----------------------------------------------------------------------------- - -// Package api provides api handlers for web requests. -package api - -import ( - "net/http" - "net/url" - - "zettelstore.de/z/api" - "zettelstore.de/z/domain/id" - "zettelstore.de/z/usecase" - "zettelstore.de/z/web/adapter" -) - -// MakeRenameZettelHandler creates a new HTTP handler to update a zettel. -func (api *API) MakeRenameZettelHandler(renameZettel usecase.RenameZettel) http.HandlerFunc { - return func(w http.ResponseWriter, r *http.Request) { - zid, err := id.Parse(r.URL.Path[1:]) - if err != nil { - http.NotFound(w, r) - return - } - newZid, found := getDestinationZid(r) - if !found { - http.Error(w, http.StatusText(http.StatusBadRequest), http.StatusBadRequest) - return - } - if err := renameZettel.Run(r.Context(), zid, newZid); err != nil { - adapter.ReportUsecaseError(w, err) - return - } - w.WriteHeader(http.StatusNoContent) - } -} - -func getDestinationZid(r *http.Request) (id.Zid, bool) { - if values, ok := r.Header[api.HeaderDestination]; ok { - for _, value := range values { - if zid, ok := getZidFromURL(value); ok { - return zid, true - } - } - } - return id.Invalid, false -} - -var zidLength = len(id.VersionZid.Bytes()) - -func getZidFromURL(val string) (id.Zid, bool) { - u, err := url.Parse(val) - if err != nil { - return id.Invalid, false - } - if len(u.Path) < zidLength { - return id.Invalid, false - } - zid, err := id.Parse(u.Path[len(u.Path)-zidLength:]) - if err != nil { - return id.Invalid, false - } - return zid, true -} Index: web/adapter/api/request.go ================================================================== --- web/adapter/api/request.go +++ web/adapter/api/request.go @@ -9,15 +9,11 @@ //----------------------------------------------------------------------------- // Package api provides api handlers for web requests. package api -import ( - "net/url" - - "zettelstore.de/z/api" -) +import "net/url" type partType int const ( partUnknown partType = iota @@ -26,18 +22,18 @@ partContent partZettel ) var partMap = map[string]partType{ - api.PartID: partID, - api.PartMeta: partMeta, - api.PartContent: partContent, - api.PartZettel: partZettel, + "id": partID, + "meta": partMeta, + "content": partContent, + "zettel": partZettel, } func getPart(q url.Values, defPart partType) partType { - p := q.Get(api.QueryKeyPart) + p := q.Get("_part") if p == "" { return defPart } if part, ok := partMap[p]; ok { return part DELETED web/adapter/api/update_zettel.go Index: web/adapter/api/update_zettel.go ================================================================== --- web/adapter/api/update_zettel.go +++ web/adapter/api/update_zettel.go @@ -1,41 +0,0 @@ -//----------------------------------------------------------------------------- -// Copyright (c) 2021 Detlef Stern -// -// This file is part of zettelstore. -// -// Zettelstore is licensed under the latest version of the EUPL (European Union -// Public License). Please see file LICENSE.txt for your rights and obligations -// under this license. -//----------------------------------------------------------------------------- - -// Package api provides api handlers for web requests. -package api - -import ( - "net/http" - - "zettelstore.de/z/domain/id" - "zettelstore.de/z/usecase" - "zettelstore.de/z/web/adapter" -) - -// MakeUpdateZettelHandler creates a new HTTP handler to update a zettel. -func (api *API) MakeUpdateZettelHandler(updateZettel usecase.UpdateZettel) http.HandlerFunc { - return func(w http.ResponseWriter, r *http.Request) { - zid, err := id.Parse(r.URL.Path[1:]) - if err != nil { - http.NotFound(w, r) - return - } - zettel, err := buildZettelFromData(r, zid) - if err != nil { - adapter.ReportUsecaseError(w, adapter.NewErrBadRequest(err.Error())) - return - } - if err := updateZettel.Run(r.Context(), zettel, true); err != nil { - adapter.ReportUsecaseError(w, err) - return - } - w.WriteHeader(http.StatusNoContent) - } -} Index: web/adapter/encoding.go ================================================================== --- web/adapter/encoding.go +++ web/adapter/encoding.go @@ -14,24 +14,23 @@ import ( "context" "errors" "strings" - "zettelstore.de/z/api" "zettelstore.de/z/ast" - "zettelstore.de/z/box" "zettelstore.de/z/domain/id" "zettelstore.de/z/encoder" + "zettelstore.de/z/place" "zettelstore.de/z/usecase" "zettelstore.de/z/web/server" ) // ErrNoSuchFormat signals an unsupported encoding format var ErrNoSuchFormat = errors.New("no such format") // FormatInlines returns a string representation of the inline slice. -func FormatInlines(is ast.InlineSlice, format api.EncodingEnum, env *encoder.Environment) (string, error) { +func FormatInlines(is ast.InlineSlice, format string, env *encoder.Environment) (string, error) { enc := encoder.Create(format, env) if enc == nil { return "", ErrNoSuchFormat } @@ -47,12 +46,11 @@ func MakeLinkAdapter( ctx context.Context, b server.Builder, key byte, getMeta usecase.GetMeta, - part string, - format api.EncodingEnum, + part, format string, ) func(*ast.LinkNode) ast.InlineNode { return func(origLink *ast.LinkNode) ast.InlineNode { origRef := origLink.Ref if origRef == nil { return origLink @@ -70,26 +68,26 @@ } zid, err := id.Parse(origRef.URL.Path) if err != nil { panic(err) } - _, err = getMeta.Run(box.NoEnrichContext(ctx), zid) - if errors.Is(err, &box.ErrNotAllowed{}) { + _, err = getMeta.Run(place.NoEnrichContext(ctx), zid) + if errors.Is(err, &place.ErrNotAllowed{}) { return &ast.FormatNode{ - Kind: ast.FormatSpan, + Code: ast.FormatSpan, Attrs: origLink.Attrs, Inlines: origLink.Inlines, } } var newRef *ast.Reference if err == nil { ub := b.NewURLBuilder(key).SetZid(zid) if part != "" { - ub.AppendQuery(api.QueryKeyPart, part) + ub.AppendQuery("_part", part) } - if format != api.EncoderUnknown { - ub.AppendQuery(api.QueryKeyFormat, format.String()) + if format != "" { + ub.AppendQuery("_format", format) } if fragment := origRef.URL.EscapedFragment(); fragment != "" { ub.SetFragment(fragment) } @@ -117,11 +115,11 @@ case ast.RefStateZettel: zid, err := id.Parse(origImage.Ref.Value) if err != nil { panic(err) } - _, err = getMeta.Run(box.NoEnrichContext(ctx), zid) + _, err = getMeta.Run(place.NoEnrichContext(ctx), zid) if err != nil { return createZettelImage(b, origImage, id.EmojiZid, ast.RefStateBroken) } return createZettelImage(b, origImage, zid, ast.RefStateFound) } ADDED web/adapter/login.go Index: web/adapter/login.go ================================================================== --- web/adapter/login.go +++ web/adapter/login.go @@ -0,0 +1,51 @@ +//----------------------------------------------------------------------------- +// 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 adapter provides handlers for web requests. +package adapter + +import ( + "fmt" + "log" + "net/http" + "strings" + + "zettelstore.de/z/encoder" +) + +// MakePostLoginHandler creates a new HTTP handler to authenticate the given user. +func MakePostLoginHandler(apiHandler, htmlHandler http.HandlerFunc) http.HandlerFunc { + return func(w http.ResponseWriter, r *http.Request) { + switch format := GetFormat(r, r.URL.Query(), encoder.GetDefaultFormat()); format { + case "json": + apiHandler(w, r) + case "html": + htmlHandler(w, r) + default: + BadRequest(w, fmt.Sprintf("Authentication not available in format %q", format)) + } + } +} + +// GetCredentialsViaForm retrieves the authentication credentions from a form. +func GetCredentialsViaForm(r *http.Request) (ident, cred string, ok bool) { + err := r.ParseForm() + if err != nil { + log.Println(err) + return "", "", false + } + + ident = strings.TrimSpace(r.PostFormValue("username")) + cred = r.PostFormValue("password") + if ident == "" { + return "", "", false + } + return ident, cred, true +} Index: web/adapter/request.go ================================================================== --- web/adapter/request.go +++ web/adapter/request.go @@ -10,37 +10,18 @@ // Package adapter provides handlers for web requests. package adapter import ( - "log" "net/http" "net/url" "strconv" "strings" - "zettelstore.de/z/api" "zettelstore.de/z/domain/meta" "zettelstore.de/z/search" - "zettelstore.de/z/usecase" -) - -// GetCredentialsViaForm retrieves the authentication credentions from a form. -func GetCredentialsViaForm(r *http.Request) (ident, cred string, ok bool) { - err := r.ParseForm() - if err != nil { - log.Println(err) - return "", "", false - } - - ident = strings.TrimSpace(r.PostFormValue("username")) - cred = r.PostFormValue("password") - if ident == "" { - return "", "", false - } - return ident, cred, true -} +) // GetInteger returns the integer value of the named query key. func GetInteger(q url.Values, key string) (int, bool) { s := q.Get(key) if s != "" { @@ -48,24 +29,27 @@ return val, true } } return 0, false } + +// ContentType defines the HTTP header value "Content-Type". +const ContentType = "Content-Type" // GetFormat returns the data format selected by the caller. -func GetFormat(r *http.Request, q url.Values, defFormat api.EncodingEnum) (api.EncodingEnum, string) { - format := q.Get(api.QueryKeyFormat) +func GetFormat(r *http.Request, q url.Values, defFormat string) string { + format := q.Get("_format") if len(format) > 0 { - return api.Encoder(format), format - } - if format, ok := getOneFormat(r, api.HeaderAccept); ok { - return api.Encoder(format), format - } - if format, ok := getOneFormat(r, api.HeaderContentType); ok { - return api.Encoder(format), format - } - return defFormat, "*default*" + return format + } + if format, ok := getOneFormat(r, "Accept"); ok { + return format + } + if format, ok := getOneFormat(r, ContentType); ok { + return format + } + return defFormat } func getOneFormat(r *http.Request, key string) (string, bool) { if values, ok := r.Header[key]; ok { for _, value := range values { @@ -76,21 +60,21 @@ } return "", false } var mapCT2format = map[string]string{ - "application/json": api.FormatJSON, - "text/html": api.FormatHTML, + "application/json": "json", + "text/html": "html", } func contentType2format(contentType string) (string, bool) { // TODO: only check before first ';' format, ok := mapCT2format[contentType] return format, ok } -// GetSearch retrieves the specified search and sorting options from a query. +// GetSearch retrieves the specified filter and sorting options from a query. func GetSearch(q url.Values, forSearch bool) (s *search.Search) { sortQKey, orderQKey, offsetQKey, limitQKey, negateQKey, sQKey := getQueryKeys(forSearch) for key, values := range q { switch key { case sortQKey, orderQKey: @@ -156,16 +140,5 @@ for _, val := range values { s = s.AddExpr(key, val) } return s } - -// GetZCDirection returns a direction value for a given string. -func GetZCDirection(s string) usecase.ZettelContextDirection { - switch s { - case api.DirBackward: - return usecase.ZettelContextBackward - case api.DirForward: - return usecase.ZettelContextForward - } - return usecase.ZettelContextBoth -} Index: web/adapter/response.go ================================================================== --- web/adapter/response.go +++ web/adapter/response.go @@ -15,11 +15,11 @@ "errors" "fmt" "log" "net/http" - "zettelstore.de/z/box" + "zettelstore.de/z/place" "zettelstore.de/z/usecase" ) // ReportUsecaseError returns an appropriate HTTP status code for errors in use cases. func ReportUsecaseError(w http.ResponseWriter, err error) { @@ -40,28 +40,28 @@ func (err *ErrBadRequest) Error() string { return err.Text } // CodeMessageFromError returns an appropriate HTTP status code and text from a given error. func CodeMessageFromError(err error) (int, string) { - if err == box.ErrNotFound { + if err == place.ErrNotFound { return http.StatusNotFound, http.StatusText(http.StatusNotFound) } - if err1, ok := err.(*box.ErrNotAllowed); ok { + if err1, ok := err.(*place.ErrNotAllowed); ok { return http.StatusForbidden, err1.Error() } - if err1, ok := err.(*box.ErrInvalidID); ok { + if err1, ok := err.(*place.ErrInvalidID); ok { return http.StatusBadRequest, fmt.Sprintf("Zettel-ID %q not appropriate in this context", err1.Zid) } if err1, ok := err.(*usecase.ErrZidInUse); ok { return http.StatusBadRequest, fmt.Sprintf("Zettel-ID %q already in use", err1.Zid) } if err1, ok := err.(*ErrBadRequest); ok { return http.StatusBadRequest, err1.Text } - if errors.Is(err, box.ErrStopped) { + if errors.Is(err, place.ErrStopped) { return http.StatusInternalServerError, fmt.Sprintf("Zettelstore not operational: %v", err) } - if errors.Is(err, box.ErrConflict) { + if errors.Is(err, place.ErrConflict) { return http.StatusConflict, "Zettelstore operations conflicted" } return http.StatusInternalServerError, err.Error() } Index: web/adapter/webui/create_zettel.go ================================================================== --- web/adapter/webui/create_zettel.go +++ web/adapter/webui/create_zettel.go @@ -14,19 +14,18 @@ import ( "context" "fmt" "net/http" - "zettelstore.de/z/api" - "zettelstore.de/z/box" "zettelstore.de/z/config" "zettelstore.de/z/domain" "zettelstore.de/z/domain/id" "zettelstore.de/z/domain/meta" "zettelstore.de/z/encoder" "zettelstore.de/z/input" "zettelstore.de/z/parser" + "zettelstore.de/z/place" "zettelstore.de/z/usecase" "zettelstore.de/z/web/adapter" ) // MakeGetCopyZettelHandler creates a new HTTP handler to display the @@ -67,17 +66,17 @@ wui.reportError(ctx, w, err) return } m := origZettel.Meta title := parser.ParseInlines(input.NewInput(config.GetTitle(m, wui.rtConfig)), meta.ValueSyntaxZmk) - textTitle, err := adapter.FormatInlines(title, api.EncoderText, nil) + textTitle, err := adapter.FormatInlines(title, "text", nil) if err != nil { wui.reportError(ctx, w, err) return } env := encoder.Environment{Lang: config.GetLang(m, wui.rtConfig)} - htmlTitle, err := adapter.FormatInlines(title, api.EncoderHTML, &env) + htmlTitle, err := adapter.FormatInlines(title, "html", &env) if err != nil { wui.reportError(ctx, w, err) return } wui.renderZettelForm(w, r, newZettel.Run(origZettel), textTitle, htmlTitle) @@ -89,21 +88,21 @@ w http.ResponseWriter, r *http.Request, getZettel usecase.GetZettel, op string, ) (domain.Zettel, error) { - if format, formatText := adapter.GetFormat(r, r.URL.Query(), api.EncoderHTML); format != api.EncoderHTML { + if format := adapter.GetFormat(r, r.URL.Query(), "html"); format != "html" { return domain.Zettel{}, adapter.NewErrBadRequest( - fmt.Sprintf("%v zettel not possible in format %q", op, formatText)) + fmt.Sprintf("%v zettel not possible in format %q", op, format)) } zid, err := id.Parse(r.URL.Path[1:]) if err != nil { - return domain.Zettel{}, box.ErrNotFound + return domain.Zettel{}, place.ErrNotFound } - origZettel, err := getZettel.Run(box.NoEnrichContext(ctx), zid) + origZettel, err := getZettel.Run(place.NoEnrichContext(ctx), zid) if err != nil { - return domain.Zettel{}, box.ErrNotFound + return domain.Zettel{}, place.ErrNotFound } return origZettel, nil } func (wui *WebUI) renderZettelForm( Index: web/adapter/webui/delete_zettel.go ================================================================== --- web/adapter/webui/delete_zettel.go +++ web/adapter/webui/delete_zettel.go @@ -13,33 +13,32 @@ import ( "fmt" "net/http" - "zettelstore.de/z/api" - "zettelstore.de/z/box" "zettelstore.de/z/config" "zettelstore.de/z/domain/id" "zettelstore.de/z/domain/meta" + "zettelstore.de/z/place" "zettelstore.de/z/usecase" "zettelstore.de/z/web/adapter" ) // MakeGetDeleteZettelHandler creates a new HTTP handler to display the // HTML delete view of a zettel. func (wui *WebUI) MakeGetDeleteZettelHandler(getZettel usecase.GetZettel) http.HandlerFunc { return func(w http.ResponseWriter, r *http.Request) { ctx := r.Context() - if format, formatText := adapter.GetFormat(r, r.URL.Query(), api.EncoderHTML); format != api.EncoderHTML { + if format := adapter.GetFormat(r, r.URL.Query(), "html"); format != "html" { wui.reportError(ctx, w, adapter.NewErrBadRequest( - fmt.Sprintf("Delete zettel not possible in format %q", formatText))) + fmt.Sprintf("Delete zettel not possible in format %q", format))) return } zid, err := id.Parse(r.URL.Path[1:]) if err != nil { - wui.reportError(ctx, w, box.ErrNotFound) + wui.reportError(ctx, w, place.ErrNotFound) return } zettel, err := getZettel.Run(ctx, zid) if err != nil { @@ -65,11 +64,11 @@ func (wui *WebUI) MakePostDeleteZettelHandler(deleteZettel usecase.DeleteZettel) http.HandlerFunc { return func(w http.ResponseWriter, r *http.Request) { ctx := r.Context() zid, err := id.Parse(r.URL.Path[1:]) if err != nil { - wui.reportError(ctx, w, box.ErrNotFound) + wui.reportError(ctx, w, place.ErrNotFound) return } if err := deleteZettel.Run(r.Context(), zid); err != nil { wui.reportError(ctx, w, err) Index: web/adapter/webui/edit_zettel.go ================================================================== --- web/adapter/webui/edit_zettel.go +++ web/adapter/webui/edit_zettel.go @@ -13,15 +13,14 @@ import ( "fmt" "net/http" - "zettelstore.de/z/api" - "zettelstore.de/z/box" "zettelstore.de/z/config" "zettelstore.de/z/domain/id" "zettelstore.de/z/domain/meta" + "zettelstore.de/z/place" "zettelstore.de/z/usecase" "zettelstore.de/z/web/adapter" ) // MakeEditGetZettelHandler creates a new HTTP handler to display the @@ -29,23 +28,23 @@ func (wui *WebUI) MakeEditGetZettelHandler(getZettel usecase.GetZettel) http.HandlerFunc { return func(w http.ResponseWriter, r *http.Request) { ctx := r.Context() zid, err := id.Parse(r.URL.Path[1:]) if err != nil { - wui.reportError(ctx, w, box.ErrNotFound) + wui.reportError(ctx, w, place.ErrNotFound) return } - zettel, err := getZettel.Run(box.NoEnrichContext(ctx), zid) + zettel, err := getZettel.Run(place.NoEnrichContext(ctx), zid) if err != nil { wui.reportError(ctx, w, err) return } - if format, formatText := adapter.GetFormat(r, r.URL.Query(), api.EncoderHTML); format != api.EncoderHTML { + if format := adapter.GetFormat(r, r.URL.Query(), "html"); format != "html" { wui.reportError(ctx, w, adapter.NewErrBadRequest( - fmt.Sprintf("Edit zettel %q not possible in format %q", zid, formatText))) + fmt.Sprintf("Edit zettel %q not possible in format %q", zid, format))) return } user := wui.getUser(ctx) m := zettel.Meta @@ -69,11 +68,11 @@ func (wui *WebUI) MakeEditSetZettelHandler(updateZettel usecase.UpdateZettel) http.HandlerFunc { return func(w http.ResponseWriter, r *http.Request) { ctx := r.Context() zid, err := id.Parse(r.URL.Path[1:]) if err != nil { - wui.reportError(ctx, w, box.ErrNotFound) + wui.reportError(ctx, w, place.ErrNotFound) return } zettel, hasContent, err := parseZettelForm(r, zid) if err != nil { Index: web/adapter/webui/get_info.go ================================================================== --- web/adapter/webui/get_info.go +++ web/adapter/webui/get_info.go @@ -10,25 +10,22 @@ // Package webui provides web-UI handlers for web requests. package webui import ( - "context" "fmt" "net/http" - "sort" "strings" - "zettelstore.de/z/api" "zettelstore.de/z/ast" - "zettelstore.de/z/box" "zettelstore.de/z/collect" "zettelstore.de/z/config" "zettelstore.de/z/domain/id" "zettelstore.de/z/domain/meta" "zettelstore.de/z/encoder" "zettelstore.de/z/encoder/encfun" + "zettelstore.de/z/place" "zettelstore.de/z/usecase" "zettelstore.de/z/web/adapter" ) type metaDataInfo struct { @@ -44,27 +41,23 @@ type matrixLine struct { Elements []matrixElement } // MakeGetInfoHandler creates a new HTTP handler for the use case "get zettel". -func (wui *WebUI) MakeGetInfoHandler( - parseZettel usecase.ParseZettel, - getMeta usecase.GetMeta, - getAllMeta usecase.GetAllMeta, -) http.HandlerFunc { +func (wui *WebUI) MakeGetInfoHandler(parseZettel usecase.ParseZettel, getMeta usecase.GetMeta) http.HandlerFunc { return func(w http.ResponseWriter, r *http.Request) { ctx := r.Context() q := r.URL.Query() - if format, formatText := adapter.GetFormat(r, q, api.EncoderHTML); format != api.EncoderHTML { + if format := adapter.GetFormat(r, q, "html"); format != "html" { wui.reportError(ctx, w, adapter.NewErrBadRequest( - fmt.Sprintf("Zettel info not available in format %q", formatText))) + fmt.Sprintf("Zettel info not available in format %q", format))) return } zid, err := id.Parse(r.URL.Path[1:]) if err != nil { - wui.reportError(ctx, w, box.ErrNotFound) + wui.reportError(ctx, w, place.ErrNotFound) return } zn, err := parseZettel.Run(ctx, zid, q.Get("syntax")) if err != nil { @@ -83,71 +76,65 @@ for i, p := range pairs { var html strings.Builder wui.writeHTMLMetaValue(&html, zn.Meta, p.Key, getTitle, &env) metaData[i] = metaDataInfo{p.Key, html.String()} } - shadowLinks := getShadowLinks(ctx, zid, getAllMeta) - endnotes, err := formatBlocks(nil, api.EncoderHTML, &env) + endnotes, err := formatBlocks(nil, "html", &env) if err != nil { endnotes = "" } textTitle := encfun.MetaAsText(zn.InhMeta, meta.KeyTitle) user := wui.getUser(ctx) - canCreate := wui.canCreate(ctx, user) var base baseData wui.makeBaseData(ctx, lang, textTitle, user, &base) wui.renderTemplate(ctx, w, id.InfoTemplateZid, &base, struct { - Zid string - WebURL string - ContextURL string - CanWrite bool - EditURL string - CanFolge bool - FolgeURL string - CanCopy bool - CopyURL string - CanRename bool - RenameURL string - CanDelete bool - DeleteURL string - MetaData []metaDataInfo - HasLinks bool - HasLocLinks bool - LocLinks []localLink - HasExtLinks bool - ExtLinks []string - ExtNewWindow string - Matrix []matrixLine - HasShadowLinks bool - ShadowLinks []string - Endnotes string + Zid string + WebURL string + ContextURL string + CanWrite bool + EditURL string + CanFolge bool + FolgeURL string + CanCopy bool + CopyURL string + CanRename bool + RenameURL string + CanDelete bool + DeleteURL string + MetaData []metaDataInfo + HasLinks bool + HasLocLinks bool + LocLinks []localLink + HasExtLinks bool + ExtLinks []string + ExtNewWindow string + Matrix []matrixLine + Endnotes string }{ - Zid: zid.String(), - WebURL: wui.NewURLBuilder('h').SetZid(zid).String(), - ContextURL: wui.NewURLBuilder('j').SetZid(zid).String(), - CanWrite: wui.canWrite(ctx, user, zn.Meta, zn.Content), - EditURL: wui.NewURLBuilder('e').SetZid(zid).String(), - CanFolge: canCreate, - FolgeURL: wui.NewURLBuilder('f').SetZid(zid).String(), - CanCopy: canCreate && !zn.Content.IsBinary(), - CopyURL: wui.NewURLBuilder('c').SetZid(zid).String(), - CanRename: wui.canRename(ctx, user, zn.Meta), - RenameURL: wui.NewURLBuilder('b').SetZid(zid).String(), - CanDelete: wui.canDelete(ctx, user, zn.Meta), - DeleteURL: wui.NewURLBuilder('d').SetZid(zid).String(), - MetaData: metaData, - HasLinks: len(extLinks)+len(locLinks) > 0, - HasLocLinks: len(locLinks) > 0, - LocLinks: locLinks, - HasExtLinks: len(extLinks) > 0, - ExtLinks: extLinks, - ExtNewWindow: htmlAttrNewWindow(len(extLinks) > 0), - Matrix: wui.infoAPIMatrix(zid), - HasShadowLinks: len(shadowLinks) > 0, - ShadowLinks: shadowLinks, - Endnotes: endnotes, + Zid: zid.String(), + WebURL: wui.NewURLBuilder('h').SetZid(zid).String(), + ContextURL: wui.NewURLBuilder('j').SetZid(zid).String(), + CanWrite: wui.canWrite(ctx, user, zn.Meta, zn.Content), + EditURL: wui.NewURLBuilder('e').SetZid(zid).String(), + CanFolge: base.CanCreate, + FolgeURL: wui.NewURLBuilder('f').SetZid(zid).String(), + CanCopy: base.CanCreate && !zn.Content.IsBinary(), + CopyURL: wui.NewURLBuilder('c').SetZid(zid).String(), + CanRename: wui.canRename(ctx, user, zn.Meta), + RenameURL: wui.NewURLBuilder('b').SetZid(zid).String(), + CanDelete: wui.canDelete(ctx, user, zn.Meta), + DeleteURL: wui.NewURLBuilder('d').SetZid(zid).String(), + MetaData: metaData, + HasLinks: len(extLinks)+len(locLinks) > 0, + HasLocLinks: len(locLinks) > 0, + LocLinks: locLinks, + HasExtLinks: len(extLinks) > 0, + ExtLinks: extLinks, + ExtNewWindow: htmlAttrNewWindow(len(extLinks) > 0), + Matrix: wui.infoAPIMatrix(zid), + Endnotes: endnotes, }) } } type localLink struct { @@ -175,43 +162,24 @@ return locLinks, extLinks } func (wui *WebUI) infoAPIMatrix(zid id.Zid) []matrixLine { formats := encoder.GetFormats() - formatTexts := make([]string, 0, len(formats)) - for _, f := range formats { - formatTexts = append(formatTexts, f.String()) - } - sort.Strings(formatTexts) - defFormat := encoder.GetDefaultFormat().String() + defFormat := encoder.GetDefaultFormat() parts := []string{"zettel", "meta", "content"} matrix := make([]matrixLine, 0, len(parts)) u := wui.NewURLBuilder('z').SetZid(zid) for _, part := range parts { - row := make([]matrixElement, 0, len(formatTexts)+1) + row := make([]matrixElement, 0, len(formats)+1) row = append(row, matrixElement{part, false, ""}) - for _, format := range formatTexts { - u.AppendQuery(api.QueryKeyPart, part) + for _, format := range formats { + u.AppendQuery("_part", part) if format != defFormat { - u.AppendQuery(api.QueryKeyFormat, format) + u.AppendQuery("_format", format) } row = append(row, matrixElement{format, true, u.String()}) u.ClearQuery() } matrix = append(matrix, matrixLine{row}) } return matrix } - -func getShadowLinks(ctx context.Context, zid id.Zid, getAllMeta usecase.GetAllMeta) []string { - ml, err := getAllMeta.Run(ctx, zid) - if err != nil || len(ml) < 2 { - return nil - } - result := make([]string, 0, len(ml)-1) - for _, m := range ml[1:] { - if boxNo, ok := m.Get(meta.KeyBoxNumber); ok { - result = append(result, boxNo) - } - } - return result -} Index: web/adapter/webui/get_zettel.go ================================================================== --- web/adapter/webui/get_zettel.go +++ web/adapter/webui/get_zettel.go @@ -14,18 +14,17 @@ import ( "bytes" "net/http" "strings" - "zettelstore.de/z/api" "zettelstore.de/z/ast" - "zettelstore.de/z/box" "zettelstore.de/z/config" "zettelstore.de/z/domain/id" "zettelstore.de/z/domain/meta" "zettelstore.de/z/encoder" "zettelstore.de/z/encoder/encfun" + "zettelstore.de/z/place" "zettelstore.de/z/usecase" "zettelstore.de/z/web/adapter" ) // MakeGetHTMLZettelHandler creates a new HTTP handler for the use case "get zettel". @@ -32,11 +31,11 @@ func (wui *WebUI) MakeGetHTMLZettelHandler(parseZettel usecase.ParseZettel, getMeta usecase.GetMeta) http.HandlerFunc { return func(w http.ResponseWriter, r *http.Request) { ctx := r.Context() zid, err := id.Parse(r.URL.Path[1:]) if err != nil { - wui.reportError(ctx, w, box.ErrNotFound) + wui.reportError(ctx, w, place.ErrNotFound) return } syntax := r.URL.Query().Get("syntax") zn, err := parseZettel.Run(ctx, zid, syntax) @@ -45,40 +44,39 @@ return } lang := config.GetLang(zn.InhMeta, wui.rtConfig) envHTML := encoder.Environment{ - LinkAdapter: adapter.MakeLinkAdapter(ctx, wui, 'h', getMeta, "", api.EncoderUnknown), + LinkAdapter: adapter.MakeLinkAdapter(ctx, wui, 'h', getMeta, "", ""), ImageAdapter: adapter.MakeImageAdapter(ctx, wui, getMeta), CiteAdapter: nil, Lang: lang, Xhtml: false, MarkerExternal: wui.rtConfig.GetMarkerExternal(), NewWindow: true, IgnoreMeta: map[string]bool{meta.KeyTitle: true, meta.KeyLang: true}, } - metaHeader, err := formatMeta(zn.InhMeta, api.EncoderHTML, &envHTML) + metaHeader, err := formatMeta(zn.InhMeta, "html", &envHTML) if err != nil { wui.reportError(ctx, w, err) return } htmlTitle, err := adapter.FormatInlines( - encfun.MetaAsInlineSlice(zn.InhMeta, meta.KeyTitle), api.EncoderHTML, &envHTML) + encfun.MetaAsInlineSlice(zn.InhMeta, meta.KeyTitle), "html", &envHTML) if err != nil { wui.reportError(ctx, w, err) return } - htmlContent, err := formatBlocks(zn.Ast, api.EncoderHTML, &envHTML) + htmlContent, err := formatBlocks(zn.Ast, "html", &envHTML) if err != nil { wui.reportError(ctx, w, err) return } textTitle := encfun.MetaAsText(zn.InhMeta, meta.KeyTitle) user := wui.getUser(ctx) roleText := zn.Meta.GetDefault(meta.KeyRole, "*") tags := wui.buildTagInfos(zn.Meta) - canCreate := wui.canCreate(ctx, user) getTitle := makeGetTitle(ctx, getMeta, &encoder.Environment{Lang: lang}) extURL, hasExtURL := zn.Meta.Get(meta.KeyURL) backLinks := wui.formatBackLinks(zn.InhMeta, getTitle) var base baseData wui.makeBaseData(ctx, lang, textTitle, user, &base) @@ -113,13 +111,13 @@ InfoURL: wui.NewURLBuilder('i').SetZid(zid).String(), RoleText: roleText, RoleURL: wui.NewURLBuilder('h').AppendQuery("role", roleText).String(), HasTags: len(tags) > 0, Tags: tags, - CanCopy: canCreate && !zn.Content.IsBinary(), + CanCopy: base.CanCreate && !zn.Content.IsBinary(), CopyURL: wui.NewURLBuilder('c').SetZid(zid).String(), - CanFolge: canCreate, + CanFolge: base.CanCreate, FolgeURL: wui.NewURLBuilder('f').SetZid(zid).String(), FolgeRefs: wui.formatMetaKey(zn.InhMeta, meta.KeyFolge, getTitle), PrecursorRefs: wui.formatMetaKey(zn.InhMeta, meta.KeyPrecursor, getTitle), ExtURL: extURL, HasExtURL: hasExtURL, @@ -129,11 +127,11 @@ BackLinks: backLinks, }) } } -func formatBlocks(bs ast.BlockSlice, format api.EncodingEnum, env *encoder.Environment) (string, error) { +func formatBlocks(bs ast.BlockSlice, format string, env *encoder.Environment) (string, error) { enc := encoder.Create(format, env) if enc == nil { return "", adapter.ErrNoSuchFormat } @@ -143,11 +141,11 @@ return "", err } return content.String(), nil } -func formatMeta(m *meta.Meta, format api.EncodingEnum, env *encoder.Environment) (string, error) { +func formatMeta(m *meta.Meta, format string, env *encoder.Environment) (string, error) { enc := encoder.Create(format, env) if enc == nil { return "", adapter.ErrNoSuchFormat } @@ -190,11 +188,11 @@ for _, val := range values { zid, err := id.Parse(val) if err != nil { continue } - if title, found := getTitle(zid, api.EncoderText); found > 0 { + if title, found := getTitle(zid, "text"); found > 0 { url := wui.NewURLBuilder('h').SetZid(zid).String() if title == "" { result = append(result, simpleLink{Text: val, URL: url}) } else { result = append(result, simpleLink{Text: title, URL: url}) Index: web/adapter/webui/home.go ================================================================== --- web/adapter/webui/home.go +++ web/adapter/webui/home.go @@ -14,13 +14,13 @@ import ( "context" "errors" "net/http" - "zettelstore.de/z/box" "zettelstore.de/z/domain/id" "zettelstore.de/z/domain/meta" + "zettelstore.de/z/place" ) type getRootStore interface { // GetMeta retrieves just the meta data of a specific zettel. GetMeta(ctx context.Context, zid id.Zid) (*meta.Meta, error) @@ -29,11 +29,11 @@ // MakeGetRootHandler creates a new HTTP handler to show the root URL. func (wui *WebUI) MakeGetRootHandler(s getRootStore) http.HandlerFunc { return func(w http.ResponseWriter, r *http.Request) { ctx := r.Context() if r.URL.Path != "/" { - wui.reportError(ctx, w, box.ErrNotFound) + wui.reportError(ctx, w, place.ErrNotFound) return } homeZid := wui.rtConfig.GetHomeZettel() if homeZid != id.DefaultHomeZid { if _, err := s.GetMeta(ctx, homeZid); err == nil { @@ -45,12 +45,12 @@ _, err := s.GetMeta(ctx, homeZid) if err == nil { redirectFound(w, r, wui.NewURLBuilder('h').SetZid(homeZid)) return } - if errors.Is(err, &box.ErrNotAllowed{}) && wui.authz.WithAuth() && wui.getUser(ctx) == nil { + if errors.Is(err, &place.ErrNotAllowed{}) && wui.authz.WithAuth() && wui.getUser(ctx) == nil { redirectFound(w, r, wui.NewURLBuilder('a')) return } redirectFound(w, r, wui.NewURLBuilder('h')) } } Index: web/adapter/webui/htmlmeta.go ================================================================== --- web/adapter/webui/htmlmeta.go +++ web/adapter/webui/htmlmeta.go @@ -17,16 +17,15 @@ "fmt" "io" "net/url" "time" - "zettelstore.de/z/api" - "zettelstore.de/z/box" "zettelstore.de/z/domain/id" "zettelstore.de/z/domain/meta" "zettelstore.de/z/encoder" "zettelstore.de/z/parser" + "zettelstore.de/z/place" "zettelstore.de/z/strfun" "zettelstore.de/z/usecase" "zettelstore.de/z/web/adapter" ) @@ -45,11 +44,11 @@ case meta.TypeIDSet: if l, ok := m.GetList(key); ok { wui.writeIdentifierSet(w, l, getTitle) } case meta.TypeNumber: - wui.writeNumber(w, key, m.GetDefault(key, "???n")) + writeNumber(w, m.GetDefault(key, "???n")) case meta.TypeString: writeString(w, m.GetDefault(key, "???s")) case meta.TypeTagSet: if l, ok := m.GetList(key); ok { wui.writeTagSet(w, key, l) @@ -66,10 +65,12 @@ if l, ok := m.GetList(key); ok { wui.writeWordSet(w, key, l) } case meta.TypeZettelmarkup: writeZettelmarkup(w, m.GetDefault(key, "???z"), env) + case meta.TypeUnknown: + writeUnknown(w, m.GetDefault(key, "???u")) default: strfun.HTMLEscape(w, m.GetDefault(key, "???w"), false) fmt.Fprintf(w, " (Unhandled type: %v, key: %v)", kt, key) } } @@ -88,17 +89,17 @@ func writeEmpty(w io.Writer, val string) { strfun.HTMLEscape(w, val, false) } -func (wui *WebUI) writeIdentifier(w io.Writer, val string, getTitle getTitleFunc) { +func (wui *WebUI) writeIdentifier(w io.Writer, val string, getTitle func(id.Zid, string) (string, int)) { zid, err := id.Parse(val) if err != nil { strfun.HTMLEscape(w, val, false) return } - title, found := getTitle(zid, api.EncoderText) + title, found := getTitle(zid, "text") switch { case found > 0: if title == "" { fmt.Fprintf(w, "%v", wui.NewURLBuilder('h').SetZid(zid), zid) } else { @@ -109,26 +110,30 @@ case found < 0: io.WriteString(w, val) } } -func (wui *WebUI) writeIdentifierSet(w io.Writer, vals []string, getTitle getTitleFunc) { +func (wui *WebUI) writeIdentifierSet(w io.Writer, vals []string, getTitle func(id.Zid, string) (string, int)) { for i, val := range vals { if i > 0 { w.Write(space) } wui.writeIdentifier(w, val, getTitle) } } -func (wui *WebUI) writeNumber(w io.Writer, key, val string) { - wui.writeLink(w, key, val, val) +func writeNumber(w io.Writer, val string) { + strfun.HTMLEscape(w, val, false) } func writeString(w io.Writer, val string) { strfun.HTMLEscape(w, val, false) } + +func writeUnknown(w io.Writer, val string) { + strfun.HTMLEscape(w, val, false) +} func (wui *WebUI) writeTagSet(w io.Writer, key string, tags []string) { for i, tag := range tags { if i > 0 { w.Write(space) @@ -145,11 +150,11 @@ u, err := url.Parse(val) if err != nil { strfun.HTMLEscape(w, val, false) return } - fmt.Fprintf(w, "", u, htmlAttrNewWindow(true)) + fmt.Fprintf(w, "", u) strfun.HTMLEscape(w, val, false) io.WriteString(w, "") } func (wui *WebUI) writeWord(w io.Writer, key, word string) { @@ -163,11 +168,11 @@ } wui.writeWord(w, key, word) } } func writeZettelmarkup(w io.Writer, val string, env *encoder.Environment) { - title, err := adapter.FormatInlines(parser.ParseMetadata(val), api.EncoderHTML, env) + title, err := adapter.FormatInlines(parser.ParseMetadata(val), "html", env) if err != nil { strfun.HTMLEscape(w, val, false) return } io.WriteString(w, title) @@ -177,17 +182,17 @@ fmt.Fprintf(w, "", wui.NewURLBuilder('h'), url.QueryEscape(key), url.QueryEscape(value)) strfun.HTMLEscape(w, text, false) io.WriteString(w, "") } -type getTitleFunc func(id.Zid, api.EncodingEnum) (string, int) +type getTitleFunc func(id.Zid, string) (string, int) func makeGetTitle(ctx context.Context, getMeta usecase.GetMeta, env *encoder.Environment) getTitleFunc { - return func(zid id.Zid, format api.EncodingEnum) (string, int) { - m, err := getMeta.Run(box.NoEnrichContext(ctx), zid) + return func(zid id.Zid, format string) (string, int) { + m, err := getMeta.Run(place.NoEnrichContext(ctx), zid) if err != nil { - if errors.Is(err, &box.ErrNotAllowed{}) { + if errors.Is(err, &place.ErrNotAllowed{}) { return "", -1 } return "", 0 } astTitle := parser.ParseMetadata(m.GetDefault(meta.KeyTitle, "")) Index: web/adapter/webui/lists.go ================================================================== --- web/adapter/webui/lists.go +++ web/adapter/webui/lists.go @@ -17,16 +17,15 @@ "net/url" "sort" "strconv" "strings" - "zettelstore.de/z/api" - "zettelstore.de/z/box" "zettelstore.de/z/domain/id" "zettelstore.de/z/domain/meta" "zettelstore.de/z/encoder" "zettelstore.de/z/parser" + "zettelstore.de/z/place" "zettelstore.de/z/search" "zettelstore.de/z/usecase" "zettelstore.de/z/web/adapter" ) @@ -52,16 +51,16 @@ func (wui *WebUI) renderZettelList(w http.ResponseWriter, r *http.Request, listMeta usecase.ListMeta) { query := r.URL.Query() s := adapter.GetSearch(query, false) ctx := r.Context() - title := wui.listTitleSearch("Select", s) + title := wui.listTitleSearch("Filter", s) wui.renderMetaList( ctx, w, title, s, func(s *search.Search) ([]*meta.Meta, error) { if !s.HasComputedMetaKey() { - ctx = box.NoEnrichContext(ctx) + ctx = place.NoEnrichContext(ctx) } return listMeta.Run(ctx, s) }, func(offset int) string { return wui.newPageURL('h', query, offset, "_offset", "_limit") @@ -186,11 +185,11 @@ title := wui.listTitleSearch("Search", s) wui.renderMetaList( ctx, w, title, s, func(s *search.Search) ([]*meta.Meta, error) { if !s.HasComputedMetaKey() { - ctx = box.NoEnrichContext(ctx) + ctx = place.NoEnrichContext(ctx) } return ucSearch.Run(ctx, s) }, func(offset int) string { return wui.newPageURL('f', query, offset, "offset", "limit") @@ -202,17 +201,17 @@ func (wui *WebUI) MakeZettelContextHandler(getContext usecase.ZettelContext) http.HandlerFunc { return func(w http.ResponseWriter, r *http.Request) { ctx := r.Context() zid, err := id.Parse(r.URL.Path[1:]) if err != nil { - wui.reportError(ctx, w, box.ErrNotFound) + wui.reportError(ctx, w, place.ErrNotFound) return } q := r.URL.Query() - dir := adapter.GetZCDirection(q.Get(api.QueryKeyDir)) - depth := getIntParameter(q, api.QueryKeyDepth, 5) - limit := getIntParameter(q, api.QueryKeyLimit, 200) + dir := usecase.ParseZCDirection(q.Get("dir")) + depth := getIntParameter(q, "depth", 5) + limit := getIntParameter(q, "limit", 200) metaList, err := getContext.Run(ctx, zid, dir, depth, limit) if err != nil { wui.reportError(ctx, w, err) return } @@ -227,15 +226,15 @@ depthURL := wui.NewURLBuilder('j').SetZid(zid) for i, depth := range depths { depthURL.ClearQuery() switch dir { case usecase.ZettelContextBackward: - depthURL.AppendQuery(api.QueryKeyDir, api.DirBackward) + depthURL.AppendQuery("dir", "backward") case usecase.ZettelContextForward: - depthURL.AppendQuery(api.QueryKeyDir, api.DirForward) + depthURL.AppendQuery("dir", "forward") } - depthURL.AppendQuery(api.QueryKeyDepth, depth) + depthURL.AppendQuery("depth", depth) depthLinks[i].Text = depth depthLinks[i].URL = depthURL.String() } var base baseData user := wui.getUser(ctx) @@ -270,14 +269,40 @@ title string, s *search.Search, ucMetaList func(sorter *search.Search) ([]*meta.Meta, error), pageURL func(int) string) { - metaList, err := ucMetaList(s) - if err != nil { - wui.reportError(ctx, w, err) - return + var metaList []*meta.Meta + var err error + var prevURL, nextURL string + if lps := wui.rtConfig.GetListPageSize(); lps > 0 { + if s.GetLimit() < lps { + s.SetLimit(lps + 1) + } + + metaList, err = ucMetaList(s) + if err != nil { + wui.reportError(ctx, w, err) + return + } + if offset := s.GetOffset(); offset > 0 { + offset -= lps + if offset < 0 { + offset = 0 + } + prevURL = pageURL(offset) + } + if len(metaList) >= s.GetLimit() { + nextURL = pageURL(s.GetOffset() + lps) + metaList = metaList[:len(metaList)-1] + } + } else { + metaList, err = ucMetaList(s) + if err != nil { + wui.reportError(ctx, w, err) + return + } } user := wui.getUser(ctx) metas, err := wui.buildHTMLMetaList(metaList) if err != nil { wui.reportError(ctx, w, err) @@ -284,15 +309,25 @@ return } var base baseData wui.makeBaseData(ctx, wui.rtConfig.GetDefaultLang(), wui.rtConfig.GetSiteName(), user, &base) wui.renderTemplate(ctx, w, id.ListTemplateZid, &base, struct { - Title string - Metas []simpleLink + Title string + Metas []simpleLink + HasPrevNext bool + HasPrev bool + PrevURL string + HasNext bool + NextURL string }{ - Title: title, - Metas: metas, + Title: title, + Metas: metas, + HasPrevNext: len(prevURL) > 0 || len(nextURL) > 0, + HasPrev: len(prevURL) > 0, + PrevURL: prevURL, + HasNext: len(nextURL) > 0, + NextURL: nextURL, }) } func (wui *WebUI) listTitleSearch(prefix string, s *search.Search) string { if s == nil { @@ -333,11 +368,11 @@ } else { lang = defaultLang } title, _ := m.Get(meta.KeyTitle) env := encoder.Environment{Lang: lang, Interactive: true} - htmlTitle, err := adapter.FormatInlines(parser.ParseMetadata(title), api.EncoderHTML, &env) + htmlTitle, err := adapter.FormatInlines(parser.ParseMetadata(title), "html", &env) if err != nil { return nil, err } metas = append(metas, simpleLink{ Text: htmlTitle, Index: web/adapter/webui/login.go ================================================================== --- web/adapter/webui/login.go +++ web/adapter/webui/login.go @@ -38,12 +38,12 @@ Title: base.Title, Retry: retry, }) } -// MakePostLoginHandler creates a new HTTP handler to authenticate the given user. -func (wui *WebUI) MakePostLoginHandler(ucAuth usecase.Authenticate) http.HandlerFunc { +// MakePostLoginHandlerHTML creates a new HTTP handler to authenticate the given user. +func (wui *WebUI) MakePostLoginHandlerHTML(ucAuth usecase.Authenticate) http.HandlerFunc { return func(w http.ResponseWriter, r *http.Request) { if !wui.authz.WithAuth() { redirectFound(w, r, wui.NewURLBuilder('/')) return } Index: web/adapter/webui/rename_zettel.go ================================================================== --- web/adapter/webui/rename_zettel.go +++ web/adapter/webui/rename_zettel.go @@ -14,15 +14,14 @@ import ( "fmt" "net/http" "strings" - "zettelstore.de/z/api" - "zettelstore.de/z/box" "zettelstore.de/z/config" "zettelstore.de/z/domain/id" "zettelstore.de/z/domain/meta" + "zettelstore.de/z/place" "zettelstore.de/z/usecase" "zettelstore.de/z/web/adapter" ) // MakeGetRenameZettelHandler creates a new HTTP handler to display the @@ -30,23 +29,23 @@ func (wui *WebUI) MakeGetRenameZettelHandler(getMeta usecase.GetMeta) http.HandlerFunc { return func(w http.ResponseWriter, r *http.Request) { ctx := r.Context() zid, err := id.Parse(r.URL.Path[1:]) if err != nil { - wui.reportError(ctx, w, box.ErrNotFound) + wui.reportError(ctx, w, place.ErrNotFound) return } m, err := getMeta.Run(ctx, zid) if err != nil { wui.reportError(ctx, w, err) return } - if format, formatText := adapter.GetFormat(r, r.URL.Query(), api.EncoderHTML); format != api.EncoderHTML { + if format := adapter.GetFormat(r, r.URL.Query(), "html"); format != "html" { wui.reportError(ctx, w, adapter.NewErrBadRequest( - fmt.Sprintf("Rename zettel %q not possible in format %q", zid.String(), formatText))) + fmt.Sprintf("Rename zettel %q not possible in format %q", zid.String(), format))) return } user := wui.getUser(ctx) var base baseData @@ -65,11 +64,11 @@ func (wui *WebUI) MakePostRenameZettelHandler(renameZettel usecase.RenameZettel) http.HandlerFunc { return func(w http.ResponseWriter, r *http.Request) { ctx := r.Context() curZid, err := id.Parse(r.URL.Path[1:]) if err != nil { - wui.reportError(ctx, w, box.ErrNotFound) + wui.reportError(ctx, w, place.ErrNotFound) return } if err = r.ParseForm(); err != nil { wui.reportError(ctx, w, adapter.NewErrBadRequest("Unable to read rename zettel form")) Index: web/adapter/webui/response.go ================================================================== --- web/adapter/webui/response.go +++ web/adapter/webui/response.go @@ -12,11 +12,11 @@ package webui import ( "net/http" - "zettelstore.de/z/api" + "zettelstore.de/z/web/server" ) -func redirectFound(w http.ResponseWriter, r *http.Request, ub *api.URLBuilder) { +func redirectFound(w http.ResponseWriter, r *http.Request, ub server.URLBuilder) { http.Redirect(w, r, ub.String(), http.StatusFound) } Index: web/adapter/webui/webui.go ================================================================== --- web/adapter/webui/webui.go +++ web/adapter/webui/webui.go @@ -17,22 +17,21 @@ "log" "net/http" "sync" "time" - "zettelstore.de/z/api" "zettelstore.de/z/auth" - "zettelstore.de/z/box" "zettelstore.de/z/collect" "zettelstore.de/z/config" "zettelstore.de/z/domain" "zettelstore.de/z/domain/id" "zettelstore.de/z/domain/meta" "zettelstore.de/z/encoder" "zettelstore.de/z/input" "zettelstore.de/z/kernel" "zettelstore.de/z/parser" + "zettelstore.de/z/place" "zettelstore.de/z/template" "zettelstore.de/z/web/adapter" "zettelstore.de/z/web/server" ) @@ -40,29 +39,28 @@ type WebUI struct { ab server.AuthBuilder authz auth.AuthzManager rtConfig config.Config token auth.TokenManager - box webuiBox + place webuiPlace policy auth.Policy templateCache map[id.Zid]*template.Template mxCache sync.RWMutex tokenLifetime time.Duration - cssBaseURL string - cssUserURL string + stylesheetURL string homeURL string listZettelURL string listRolesURL string listTagsURL string withAuth bool loginURL string searchURL string } -type webuiBox interface { +type webuiPlace interface { CanCreateZettel(ctx context.Context) bool GetZettel(ctx context.Context, zid id.Zid) (domain.Zettel, error) GetMeta(ctx context.Context, zid id.Zid) (*meta.Meta, error) CanUpdateZettel(ctx context.Context, zettel domain.Zettel) bool AllowRenameZettel(ctx context.Context, zid id.Zid) bool @@ -69,40 +67,39 @@ CanDeleteZettel(ctx context.Context, zid id.Zid) bool } // New creates a new WebUI struct. func New(ab server.AuthBuilder, authz auth.AuthzManager, rtConfig config.Config, token auth.TokenManager, - mgr box.Manager, pol auth.Policy) *WebUI { + mgr place.Manager, pol auth.Policy) *WebUI { wui := &WebUI{ ab: ab, rtConfig: rtConfig, authz: authz, token: token, - box: mgr, + place: mgr, policy: pol, tokenLifetime: kernel.Main.GetConfig(kernel.WebService, kernel.WebTokenLifetimeHTML).(time.Duration), - cssBaseURL: ab.NewURLBuilder('z').SetZid( - id.BaseCSSZid).AppendQuery("_format", "raw").AppendQuery("_part", "content").String(), - cssUserURL: ab.NewURLBuilder('z').SetZid( - id.UserCSSZid).AppendQuery("_format", "raw").AppendQuery("_part", "content").String(), + stylesheetURL: ab.NewURLBuilder('z').SetZid( + id.BaseCSSZid).AppendQuery("_format", "raw").AppendQuery( + "_part", "content").String(), homeURL: ab.NewURLBuilder('/').String(), listZettelURL: ab.NewURLBuilder('h').String(), listRolesURL: ab.NewURLBuilder('h').AppendQuery("_l", "r").String(), listTagsURL: ab.NewURLBuilder('h').AppendQuery("_l", "t").String(), withAuth: authz.WithAuth(), loginURL: ab.NewURLBuilder('a').String(), searchURL: ab.NewURLBuilder('f').String(), } - wui.observe(box.UpdateInfo{Box: mgr, Reason: box.OnReload, Zid: id.Invalid}) + wui.observe(place.UpdateInfo{Place: mgr, Reason: place.OnReload, Zid: id.Invalid}) mgr.RegisterObserver(wui.observe) return wui } -func (wui *WebUI) observe(ci box.UpdateInfo) { +func (wui *WebUI) observe(ci place.UpdateInfo) { wui.mxCache.Lock() - if ci.Reason == box.OnReload || ci.Zid == id.BaseTemplateZid { + if ci.Reason == place.OnReload || ci.Zid == id.BaseTemplateZid { wui.templateCache = make(map[id.Zid]*template.Template, len(wui.templateCache)) } else { delete(wui.templateCache, ci.Zid) } wui.mxCache.Unlock() @@ -121,33 +118,33 @@ return t, ok } func (wui *WebUI) canCreate(ctx context.Context, user *meta.Meta) bool { m := meta.New(id.Invalid) - return wui.policy.CanCreate(user, m) && wui.box.CanCreateZettel(ctx) + return wui.policy.CanCreate(user, m) && wui.place.CanCreateZettel(ctx) } func (wui *WebUI) canWrite( ctx context.Context, user, meta *meta.Meta, content domain.Content) bool { return wui.policy.CanWrite(user, meta, meta) && - wui.box.CanUpdateZettel(ctx, domain.Zettel{Meta: meta, Content: content}) + wui.place.CanUpdateZettel(ctx, domain.Zettel{Meta: meta, Content: content}) } func (wui *WebUI) canRename(ctx context.Context, user, m *meta.Meta) bool { - return wui.policy.CanRename(user, m) && wui.box.AllowRenameZettel(ctx, m.Zid) + return wui.policy.CanRename(user, m) && wui.place.AllowRenameZettel(ctx, m.Zid) } func (wui *WebUI) canDelete(ctx context.Context, user, m *meta.Meta) bool { - return wui.policy.CanDelete(user, m) && wui.box.CanDeleteZettel(ctx, m.Zid) + return wui.policy.CanDelete(user, m) && wui.place.CanDeleteZettel(ctx, m.Zid) } func (wui *WebUI) getTemplate( ctx context.Context, templateID id.Zid) (*template.Template, error) { if t, ok := wui.cacheGetTemplate(templateID); ok { return t, nil } - realTemplateZettel, err := wui.box.GetZettel(ctx, templateID) + realTemplateZettel, err := wui.place.GetZettel(ctx, templateID) if err != nil { return nil, err } t, err := template.ParseString(realTemplateZettel.Content.AsString(), nil) if err == nil { @@ -161,51 +158,54 @@ Text string URL string } type baseData struct { - Lang string - MetaHeader string - CSSBaseURL string - CSSUserURL string - Title string - HomeURL string - WithUser bool - WithAuth bool - UserIsValid bool - UserZettelURL string - UserIdent string - UserLogoutURL string - LoginURL string - ListZettelURL string - ListRolesURL string - ListTagsURL string - HasNewZettelLinks bool - NewZettelLinks []simpleLink - SearchURL string - Content string - FooterHTML string + Lang string + MetaHeader string + StylesheetURL string + Title string + HomeURL string + WithUser bool + WithAuth bool + UserIsValid bool + UserZettelURL string + UserIdent string + UserLogoutURL string + LoginURL string + ListZettelURL string + ListRolesURL string + ListTagsURL string + CanCreate bool + NewZettelURL string + NewZettelLinks []simpleLink + SearchURL string + Content string + FooterHTML string } func (wui *WebUI) makeBaseData( ctx context.Context, lang, title string, user *meta.Meta, data *baseData) { var ( - userZettelURL string - userIdent string - userLogoutURL string + newZettelLinks []simpleLink + userZettelURL string + userIdent string + userLogoutURL string ) + canCreate := wui.canCreate(ctx, user) + if canCreate { + newZettelLinks = wui.fetchNewTemplates(ctx, user) + } userIsValid := user != nil if userIsValid { userZettelURL = wui.NewURLBuilder('h').SetZid(user.Zid).String() userIdent = user.GetDefault(meta.KeyUserID, "") userLogoutURL = wui.NewURLBuilder('a').SetZid(user.Zid).String() } - newZettelLinks := wui.fetchNewTemplates(ctx, user) data.Lang = lang - data.CSSBaseURL = wui.cssBaseURL - data.CSSUserURL = wui.cssUserURL + data.StylesheetURL = wui.stylesheetURL data.Title = title data.HomeURL = wui.homeURL data.WithAuth = wui.withAuth data.WithUser = data.WithAuth data.UserIsValid = userIsValid @@ -214,53 +214,52 @@ data.UserLogoutURL = userLogoutURL data.LoginURL = wui.loginURL data.ListZettelURL = wui.listZettelURL data.ListRolesURL = wui.listRolesURL data.ListTagsURL = wui.listTagsURL - data.HasNewZettelLinks = len(newZettelLinks) > 0 + data.CanCreate = canCreate data.NewZettelLinks = newZettelLinks data.SearchURL = wui.searchURL data.FooterHTML = wui.rtConfig.GetFooterHTML() } -// htmlAttrNewWindow returns HTML attribute string for opening a link in a new window. +// htmlAttrNewWindow eturns HTML attribute string for opening a link in a new window. // If hasURL is false an empty string is returned. func htmlAttrNewWindow(hasURL bool) string { if hasURL { return " target=\"_blank\" ref=\"noopener noreferrer\"" } return "" } -func (wui *WebUI) fetchNewTemplates(ctx context.Context, user *meta.Meta) (result []simpleLink) { - ctx = box.NoEnrichContext(ctx) - if !wui.canCreate(ctx, user) { - return nil - } - menu, err := wui.box.GetZettel(ctx, id.TOCNewTemplateZid) +func (wui *WebUI) fetchNewTemplates(ctx context.Context, user *meta.Meta) []simpleLink { + ctx = place.NoEnrichContext(ctx) + menu, err := wui.place.GetZettel(ctx, id.TOCNewTemplateZid) if err != nil { return nil } - refs := collect.Order(parser.ParseZettel(menu, "", wui.rtConfig)) + zn := parser.ParseZettel(menu, "", wui.rtConfig) + refs := collect.Order(zn) + result := make([]simpleLink, 0, len(refs)) for _, ref := range refs { zid, err := id.Parse(ref.URL.Path) if err != nil { continue } - m, err := wui.box.GetMeta(ctx, zid) + m, err := wui.place.GetMeta(ctx, zid) if err != nil { continue } if !wui.policy.CanRead(user, m) { continue } title := config.GetTitle(m, wui.rtConfig) astTitle := parser.ParseInlines(input.NewInput(title), meta.ValueSyntaxZmk) env := encoder.Environment{Lang: config.GetLang(m, wui.rtConfig)} - menuTitle, err := adapter.FormatInlines(astTitle, api.EncoderHTML, &env) + menuTitle, err := adapter.FormatInlines(astTitle, "html", &env) if err != nil { - menuTitle, err = adapter.FormatInlines(astTitle, api.EncoderText, nil) + menuTitle, err = adapter.FormatInlines(astTitle, "text", nil) if err != nil { menuTitle = title } } result = append(result, simpleLink{ @@ -322,11 +321,11 @@ } var content bytes.Buffer err = t.Render(&content, data) if err == nil { base.Content = content.String() - w.Header().Set(api.HeaderContentType, "text/html; charset=utf-8") + w.Header().Set(adapter.ContentType, "text/html; charset=utf-8") w.WriteHeader(code) err = bt.Render(w, base) } if err != nil { log.Println("Unable to render template", err) @@ -337,13 +336,13 @@ // GetURLPrefix returns the configured URL prefix of the web server. func (wui *WebUI) GetURLPrefix() string { return wui.ab.GetURLPrefix() } // NewURLBuilder creates a new URL builder object with the given key. -func (wui *WebUI) NewURLBuilder(key byte) *api.URLBuilder { return wui.ab.NewURLBuilder(key) } +func (wui *WebUI) NewURLBuilder(key byte) server.URLBuilder { return wui.ab.NewURLBuilder(key) } func (wui *WebUI) clearToken(ctx context.Context, w http.ResponseWriter) context.Context { return wui.ab.ClearToken(ctx, w) } func (wui *WebUI) setToken(w http.ResponseWriter, token []byte) { wui.ab.SetToken(w, token, wui.tokenLifetime) } Index: web/server/impl/impl.go ================================================================== --- web/server/impl/impl.go +++ web/server/impl/impl.go @@ -14,11 +14,10 @@ import ( "context" "net/http" "time" - "zettelstore.de/z/api" "zettelstore.de/z/auth" "zettelstore.de/z/domain/meta" "zettelstore.de/z/web/server" ) @@ -56,12 +55,12 @@ if data := srv.GetAuthData(ctx); data != nil { return data.User } return nil } -func (srv *myServer) NewURLBuilder(key byte) *api.URLBuilder { - return api.NewURLBuilder(srv.GetURLPrefix(), key) +func (srv *myServer) NewURLBuilder(key byte) server.URLBuilder { + return &URLBuilder{router: &srv.router, key: key} } func (srv *myServer) GetURLPrefix() string { return srv.router.urlPrefix } Index: web/server/impl/router.go ================================================================== --- web/server/impl/router.go +++ web/server/impl/router.go @@ -98,28 +98,27 @@ return } r.URL.Path = r.URL.Path[prefixLen-1:] } match := rt.reURL.FindStringSubmatch(r.URL.Path) - if len(match) != 3 { - rt.mux.ServeHTTP(w, rt.addUserContext(r)) - return - } - - key := match[1][0] - table := rt.zettelTable - if match[2] == "" { - table = rt.listTable - } - if mh, ok := table[key]; ok { - if handler, ok := mh[r.Method]; ok { - r.URL.Path = "/" + match[2] - handler.ServeHTTP(w, rt.addUserContext(r)) + if len(match) == 3 { + key := match[1][0] + table := rt.zettelTable + if match[2] == "" { + table = rt.listTable + } + if mh, ok := table[key]; ok { + if handler, ok := mh[r.Method]; ok { + r.URL.Path = "/" + match[2] + handler.ServeHTTP(w, rt.addUserContext(r)) + return + } + http.Error(w, http.StatusText(http.StatusMethodNotAllowed), http.StatusMethodNotAllowed) return } } - http.Error(w, http.StatusText(http.StatusMethodNotAllowed), http.StatusMethodNotAllowed) + rt.mux.ServeHTTP(w, rt.addUserContext(r)) } func (rt *httpRouter) addUserContext(r *http.Request) *http.Request { if rt.ur == nil { return r ADDED web/server/impl/urlbuilder.go Index: web/server/impl/urlbuilder.go ================================================================== --- web/server/impl/urlbuilder.go +++ web/server/impl/urlbuilder.go @@ -0,0 +1,110 @@ +//----------------------------------------------------------------------------- +// 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 impl provides the Zettelstore web service. +package impl + +import ( + "net/url" + "strings" + + "zettelstore.de/z/domain/id" + "zettelstore.de/z/web/server" +) + +type urlQuery struct{ key, val string } + +// URLBuilder should be used to create zettelstore URLs. +type URLBuilder struct { + router *httpRouter + key byte + path []string + query []urlQuery + fragment string +} + +// Clone an URLBuilder +func (ub *URLBuilder) Clone() server.URLBuilder { + cpy := new(URLBuilder) + cpy.key = ub.key + if len(ub.path) > 0 { + cpy.path = make([]string, 0, len(ub.path)) + cpy.path = append(cpy.path, ub.path...) + } + if len(ub.query) > 0 { + cpy.query = make([]urlQuery, 0, len(ub.query)) + cpy.query = append(cpy.query, ub.query...) + } + cpy.fragment = ub.fragment + return cpy +} + +// SetZid sets the zettel identifier. +func (ub *URLBuilder) SetZid(zid id.Zid) server.URLBuilder { + if len(ub.path) > 0 { + panic("Cannot add Zid") + } + ub.path = append(ub.path, zid.String()) + return ub +} + +// AppendPath adds a new path element +func (ub *URLBuilder) AppendPath(p string) server.URLBuilder { + ub.path = append(ub.path, p) + return ub +} + +// AppendQuery adds a new query parameter +func (ub *URLBuilder) AppendQuery(key, value string) server.URLBuilder { + ub.query = append(ub.query, urlQuery{key, value}) + return ub +} + +// ClearQuery removes all query parameters. +func (ub *URLBuilder) ClearQuery() server.URLBuilder { + ub.query = nil + ub.fragment = "" + return ub +} + +// SetFragment stores the fragment +func (ub *URLBuilder) SetFragment(s string) server.URLBuilder { + ub.fragment = s + return ub +} + +// String produces a string value. +func (ub *URLBuilder) String() string { + var sb strings.Builder + + sb.WriteString(ub.router.urlPrefix) + if ub.key != '/' { + sb.WriteByte(ub.key) + } + for _, p := range ub.path { + sb.WriteByte('/') + sb.WriteString(url.PathEscape(p)) + } + if len(ub.fragment) > 0 { + sb.WriteByte('#') + sb.WriteString(ub.fragment) + } + for i, q := range ub.query { + if i == 0 { + sb.WriteByte('?') + } else { + sb.WriteByte('&') + } + sb.WriteString(q.key) + sb.WriteByte('=') + sb.WriteString(url.QueryEscape(q.val)) + } + return sb.String() +} Index: web/server/server.go ================================================================== --- web/server/server.go +++ web/server/server.go @@ -14,14 +14,37 @@ import ( "context" "net/http" "time" - "zettelstore.de/z/api" "zettelstore.de/z/domain/id" "zettelstore.de/z/domain/meta" ) + +// URLBuilder builds URLs. +type URLBuilder interface { + // Clone an URLBuilder + Clone() URLBuilder + + // SetZid sets the zettel identifier. + SetZid(zid id.Zid) URLBuilder + + // AppendPath adds a new path element + AppendPath(p string) URLBuilder + + // AppendQuery adds a new query parameter + AppendQuery(key, value string) URLBuilder + + // ClearQuery removes all query parameters. + ClearQuery() URLBuilder + + // SetFragment stores the fragment + SetFragment(s string) URLBuilder + + // String produces a string value. + String() string +} // UserRetriever allows to retrieve user data based on a given zettel identifier. type UserRetriever interface { GetUser(ctx context.Context, zid id.Zid, ident string) (*meta.Meta, error) } @@ -35,14 +58,14 @@ } // Builder allows to build new URLs for the web service. type Builder interface { GetURLPrefix() string - NewURLBuilder(key byte) *api.URLBuilder + NewURLBuilder(key byte) URLBuilder } -// Auth is the authencation interface. +// Auth is. type Auth interface { GetUser(context.Context) *meta.Meta SetToken(w http.ResponseWriter, token []byte, d time.Duration) // ClearToken invalidates the session cookie by sending an empty one. Index: www/build.md ================================================================== --- www/build.md +++ www/build.md @@ -33,17 +33,17 @@ The flag `-v` enables the verbose mode. It outputs all commands called by the tool. `COMMAND` is one of: -* `build`: builds the software with correct version information and puts it +* `build`: builds the software with correct version information and places it into a freshly created directory bin. * `check`: checks the current state of the working directory to be ready for release (or commit). * `release`: executes `check` command and if this was successful, builds the software for various platforms, and creates ZIP files for each executable. - Everything is put in the directory releases. + Everything is placed in the directory releases. * `clean`: removes the directories bin and releases. * `version`: prints the current version information. Therefore, the easiest way to build your own version of the Zettelstore software is to execute the command Index: www/changes.wiki ================================================================== --- www/changes.wiki +++ www/changes.wiki @@ -1,64 +1,15 @@ Change Log - -

Changes for Version 0.0.15 (pending)

- -

Changes for Version 0.0.14 (2021-07-23)

- * Rename “place” into “box”. This also affects the - configuration keys to specify boxes box-uriX (previously - place-uri-X. Older changes documented here are renamed - too. - (breaking) - * Add API for creating, updating, renaming, and deleting zettel. - (major: api) - * Initial API client for Go. - (major: api) - * Remove support for paging of WebUI list. Runtime configuration key - list-page-size is removed. If you still specify it, it will be - ignored. - (major: webui) - * Use endpoint /v for user authentication via API. Endpoint - /a is now used for the web user interface only. Similar, endpoint - /y (“zettel context”) is renamed to /x. - (minor, possibly breaking) - * Type of used-defined metadata is determined by suffix of key: - -number, -url, -zid will result the values to - be interpreted as a number, an URL, or a zettel identifier. - (minor, but possibly breaking if you already used a metadata key with - above suffixes, but as a string type) - * New user-role “creator”, which is only allowed to - create new zettel (except user zettel). This role may only read and update - public zettel or its own user zettel. Added to support future client - software (e.g. on a mobile device) that automatically creates new zettel - but, in case of a password loss, should not allow to read existing zettel. - (minor, possibly breaking, because new zettel template zettel must always - prepend the string new- before metdata keys that should be - transferred to the new zettel) - * New suported metadata key box-number, which gives an indication - from which box the zettel was loaded. - (minor) - * New supported syntax html. - (minor) - * New predefined zettel “User CSS” that can be used to redefine - some predefined CSS (without modifying the base CSS zettel). - (minor: webui) - * When a user moves a zettel file with additional characters into the box - directory, these characters are preserved when zettel is updated. - (bug) - * The phase “filtering a zettel list” is more precise - “selecting zettel” - (documentation) - * Many smaller bug fixes and inprovements, to the software and to the - documentation. +

Changes for Version 0.0.14 (pending)

Changes for Version 0.0.13 (2021-06-01)

- * Startup configuration box-X-uri (where X is a + * Startup configuration place-X-uri (where X is a number greater than zero) has been renamed to - box-uri-X. + place-uri-X. (breaking) * Web server processes startup configuration url-prefix. There is no need for stripping the prefix by a front-end web server any more. (breaking: webui, api) * Administrator console (only optional accessible locally). Enable it only @@ -65,11 +16,12 @@ on systems with a single user or with trusted users. It is disabled by default. (major: core) * Remove visibility value “simple-expert” introduced in [#0_0_8|version 0.0.8]. It was too complicated, esp. authorization. There - was a name collision with the “simple” directory box sub-type. + was a name collision with the “simple” directory place + sub-type. (major) * For security reasons, HTML blocks are not encoded as HTML if they contain certain snippets, such as <script or <iframe. These may be caused by using CommonMark as a zettel syntax. (major) @@ -85,11 +37,11 @@ values and about runtime metrics got new zettel identifier. This affects only user that referenced those zettel. (minor) * Local images that cannot be read (not found or no access rights) are substituted with the new default image, a spinning emoji. - See [/file?name=box/constbox/emoji_spin.gif]. + See [/file?name=place/constplace/emoji_spin.gif]. (minor: webui) * Add zettelmarkup syntax for a table row that should be ignored: |%. This allows to paste output of the administrator console into a zettel. (minor: zmk) @@ -101,59 +53,57 @@ * Raise the per-process limit of open files on macOS to 1.048.576. This allows most macOS users to use at least 500.000 zettel. That should be enough for the near future. (major) * Mitigate the shortcomings of the macOS version by introducing types of - directory boxes. The original directory box type is now called "notify" + directory places. The original directory place type is now called "notify" (the default value). There is a new type called "simple". This new type does not notify Zettelstore when some of the underlying Zettel files change. (major) - * Add new startup configuration default-dir-box-type, which gives - the default value for specifying a directory box type. The default value + * Add new startup configuration default-dir-place-type, which gives + the default value for specifying a directory place type. The default value is “notify”. On macOS, the default value may be changed “simple” if some errors occur while raising the per-process limit of open files. (minor)

Changes for Version 0.0.11 (2021-04-05)

- * New box schema "file" allows to read zettel from a ZIP file. + * New place schema "file" allows to read zettel from a ZIP file. A zettel collection can now be packaged and distributed easier. (major: server) - * Non-restricted search is a full-text search. The search string will be - normalized according to Unicode NFKD. Every character that is not a letter + * Non-restricted search is a full-text search. The search string will + be normalized according to Unicode NFKD. Every character that is not a letter or a number will be ignored for the search. It is sufficient if the words - to be searched are part of words inside a zettel, both content and - metadata. + to be searched are part of words inside a zettel, both content and metadata. (major: api, webui) * A zettel can be excluded from being indexed (and excluded from being found in a search) if it contains the metadata no-index: true. (minor: api, webui) * Menu bar is shown when displaying error messages. (minor: webui) - * When selecting zettel, it can be specified that a given value should - not match. Previously, only the whole select criteria could be + * When filtering a list of zettel, it can be specified that a given value should + not match. Previously, only the whole filter expression could be negated (which is still possible). (minor: api, webui) - * You can select a zettel by specifying that specific metadata keys must + * You can filter a zettel list by specifying that specific metadata keys must (or must not) be present. (minor: api, webui) - * Context of a zettel (introduced in version 0.0.10) does not take tags into - account any more. Using some tags for determining the context resulted - into erratic, non-deterministic context lists. + * Context of a zettel (introduced in version 0.0.10) does not take tags into account any more. + Using some tags for determining the context resulted into erratic, non-deterministic context lists. (minor: api, webui) - * Selecting zettel depending on tag values can be both by comparing only the - prefix or the whole string. If a search value begins with '#', only zettel - with the exact tag will be returned. Otherwise a zettel will be returned - if the search string just matches the prefix of only one of its tags. + * Filtering zettel depending on tag values can be both by comparing only the prefix + or the whole string. If a search value begins with '#', only zettel with the exact + tag will be returned. Otherwise a zettel will be returned if the search string + just matches the prefix of only one of its tags. (minor: api, webui) * Many smaller bug fixes and inprovements, to the software and to the documentation. -A note for users of macOS: in the current release and with macOS's default -values, a zettel directory must not contain more than approx. 250 files. There -are three options to mitigate this limitation temporarily: +A note for users of macOS: in the current release and with macOS's default values, +a zettel directory place must not contain more than approx. 250 files. There are three options +to mitigate this limitation temporarily: # You update the per-process limit of open files on macOS. # You setup a virtualization environment to run Zettelstore on Linux or Windows. # You wait for version 0.0.12 which addresses this issue. @@ -182,13 +132,13 @@ * A reference that starts with two slash characters (“//”) it will be interpreted relative to the value of url-prefix. For example, if url-prefix has the value /manual/, the reference [[Zettel list|//h]] will render as <a href="/manual/h">Zettel list</a>. (minor: syntax) - * Searching/selecting ignores the leading '#' character of tags. + * Searching/filtering ignores the leading '#' character of tags. (minor: api, webui) - * When result of selecting or searching is presented, the query is written as the page heading. + * When result of filtering or searching is presented, the query is written as the page heading. (minor: webui) * A reference to a zettel that contains a URL fragment, will now be processed by the indexer. (bug: server) * Runtime configuration key marker-external now defaults to “&#10138;” (“➚”). It is more beautiful @@ -200,301 +150,217 @@ (minor: infrastructure) * Many smaller bug fixes and inprovements, to the software and to the documentation.

Changes for Version 0.0.9 (2021-01-29)

-This is the first version that is managed by [https://fossil-scm.org|Fossil] -instead of GitHub. To access older versions, use the Git repository under +This is the first version that is managed by [https://fossil-scm.org|Fossil] instead +of GitHub. To access older versions, use the Git repository under [https://github.com/zettelstore/zettelstore-github|zettelstore-github].

Server / API

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

WebUI

- * (major) HTML template zettel for WebUI now use - [https://mustache.github.io/|Mustache] syntax instead of - previously used [https://golang.org/pkg/html/template/|Go - template] syntax. This allows these zettel to be used, even when - there is another Zettelstore implementation, in another - programming language. Mustache is available for approx. 48 - programming languages, instead of only one for Go templates. If - you modified your templates, you must adapt them to the new - syntax. Otherwise the WebUI will not work. - * (major) Show zettel identifier of folgezettel and precursor zettel in the - header of a rendered zettel. If a zettel has real backlinks, they - are shown at the botton of the page (“Additional links to - this zettel”). - * (minor) All property metadata, even computed metadata is shown in the info - page of a zettel. - * (minor) Rendering of metadata keys title and - default-title in info page changed to a full HTML output - for these Zettelmarkup encoded values. + * (major) HTML template zettel for WebUI now use [https://mustache.github.io/|Mustache] + syntax instead of previously used [https://golang.org/pkg/html/template/|Go template] syntax. + This allows these zettel to be used, even when there is another Zettelstore implementation, in another programming language. + Mustache is available for approx. 48 programming languages, instead of only one for Go templates. + If you modified your templates, you must adapt them to the new syntax. + Otherwise the WebUI will not work. + * (major) Show zettel identifier of folgezettel and precursor zettel in the header of a rendered zettel. + If a zettel has real backlinks, they are shown at the botton of the page + (“Additional links to this zettel”). + * (minor) All property metadata, even computed metadata is shown in the info page of a zettel. + * (minor) Rendering of metadata keys title and default-title in info page changed to a full HTML output for these Zettelmarkup encoded values. * (minor) Always show the zettel identifier on the zettel detail view. - Previously, the identifier was not shown if the zettel was not - editable. + Previously, the identifier was not shown if the zettel was not editable. * (minor) Do not show computed metadata in edit forms anymore.

Changes for Version 0.0.8 (2020-12-23)

Server / API

- * (bug) Zettel files with extension .jpg and without metadata will - get a syntax value “jpg”. The internal data - structure got the same value internally, instead of - “jpeg”. This has been fixed for all possible alternative - syntax values. - * (bug) If a file, e.g. an image file like 20201130190200.jpg, is - added to the directory box, its metadata are just calculated from - the information available. Updated metadata did not find its way - into the zettel box, because the .meta file was not - written. - * (bug) If just the .meta file was deleted manually, the zettel was - assumed to be missing. A workaround is to restart the software. If - the .meta file is deleted, metadata is now calculated in - the same way when the .meta file is non-existing at the - start of the software. - * (bug) A link to the current zettel, only using a fragment (e.g. - [[Title|#title]]) is now handled correctly as - a zettel link (and not as a link to external material). + * (bug) Zettel files with extension .jpg and without metadata will get a syntax value “jpg”. + The internal data structure got the same value internally, instead of “jpeg”. + This has been fixed for all possible alternative syntax values. + * (bug) If a file, e.g. an image file like 20201130190200.jpg, is added to the directory place, + * its metadata are just calculated from the information available. + Updated metadata did not find its way into the place, because the .meta file was not written. + This has been fixed. + * (bug) If just the .meta file was deleted manually, the zettel was assumed to be missing. + A workaround is to restart the software. + This has been fixed. + If the .meta file is deleted, metadata is now calculated in the same way when the .meta file is non-existing at the start of the software. + * (bug) A link to the current zettel, only using a fragment (e.g. [[Title|#title]]) is now handled correctly as a zettel link (and not as a link to external material). * (minor) Allow zettel to be marked as “read only”. This is done through the metadata key read-only. - * (bug) When renaming a zettel, check all boxes for the new zettel - identifier, not just the first one. Otherwise it will be possible to - shadow a read-only zettel from a next box, effectively modifying it. - * (minor) Add support for a configurable default value for metadata key - visibility. - * (bug) If list-page-size is set to a relatively small value and - the authenticated user is not the owner, some zettel were not - shown in the list of zettel or were not returned by the API. + * (bug) When renaming a zettel, check all places for the new zettel identifier, not just the first place. + Otherwise it will be possible to shadow a read-only zettel from a next place, effectively modifying it. + * (minor) Add support for a configurable default value for metadata key visibility. + * (bug) If list-page-size is set to a relatively small value and the authenticated user is not the owner, + * some zettel were not shown in the list of zettel or were not returned by the API. * (minor) Add support for new visibility “expert”. - An owner becomes an expert, if the runtime configuration key - expert-mode is set to true. + An owner becomes an expert, if the runtime configuration key expert-mode is set to true. * (major) Add support for computed zettel. These zettel have an identifier less than 0000000000100. Most of them are only visible, if expert-mode is enabled. - * (bug) Fixes a memory leak that results in too many open files after - approx. 125 reload operations. - * (major) Predefined templates for new zettel got an explicit value for - visibility: “login”. Please update these zettel if you - modified them. - * (major) Rename key readonly of Zettelstore Startup - Configuration to read-only-mode. This was done to - avoid some confusion with the the zettel metadata key - read-only. Please adapt your startup configuration. + * (bug) Fixes a memory leak that results in too many open files after approx. 125 reload operations. + * (major) Predefined templates for new zettel got an explicit value for visibility: “login”. + Please update these zettel if you modified them. + * (major) Rename key readonly of Zettelstore Startup Configuration to read-only-mode. + This was done to avoid some confusion with the the zettel metadata key read-only. + Please adapt your startup configuration. Otherwise your Zettelstore will be accidentally writable. - * (minor) References starting with “./” and “../” - are treated as a local reference. Previously, only the prefix - “/” was treated as a local reference. - * (major) Metadata key modified will be set automatically to the - current local time if a zettel is updated through Zettelstore. - If you used that key previously for your own, you should rename - it before you upgrade. - * (minor) The new visibility value “simple-expert” ensures that - many computed zettel are shown for new users. This is to enable - them to send useful bug reports. - * (minor) When a zettel is stored as a file, its identifier is additionally - stored within the metadata. This helps for better robustness in - case the file names were corrupted. In addition, there could be - a tool that compares the identifier with the file name. + * (minor) References starting with “./” and “../” are treated as a local reference. + Previously, only the prefix “/” was treated as a local reference. + * (major) Metadata key modified will be set automatically to the current local time if a zettel is updated through Zettelstore. + If you used that key previously for your own, you should rename it before you upgrade. + * (minor) The new visibility value “simple-expert” ensures that many computed zettel are shown for new users. + This is to enable them to send useful bug reports. + * (minor) When a zettel is stored as a file, its identifier is additionally stored within the metadata. + This helps for better robustness in case the file names were corrupted. + In addition, there could be a tool that compares the identifier with the file name.

WebUI

- * (minor) Remove list of tags in “List Zettel” and search - results. There was some feedback that the additional tags were not - helpful. - * (minor) Move zettel field "role" above "tags" and move "syntax" more to - "content". + * (minor) Remove list of tags in “List Zettel” and search results. + There was some feedback that the additional tags were not helpful. + * (minor) Move zettel field "role" above "tags" and move "syntax" more to "content". * (minor) Rename zettel operation “clone” to “copy”. - * (major) All predefined HTML templates have now a visibility value - “expert”. If you want to see them as an non-expert - owner, you must temporary enable expert-mode and change - the visibility metadata value. - * (minor) Initial support for - [https://zettelkasten.de/posts/tags/folgezettel/|Folgezettel]. If - you click on “Folge” (detail view or info view), a new - zettel is created with a reference (precursor) to the - original zettel. Title, role, tags, and syntax are copied from the - original zettel. - * (major) Most predefined zettel have a title prefix of - “Zettelstore”. - * (minor) If started in simple mode, e.g. via double click or without any - command, some information for the new user is presented. In the - terminal, there is a hint about opening the web browser and use - a specific URL. A Welcome zettel is created, to give some - more information. (This change also applies to the server itself, - but it is more suited to the WebUI user.) + * (major) All predefined HTML templates have now a visibility value “expert”. + If you want to see them as an non-expert owner, you must temporary enable expert-mode and change the visibility metadata value. + * (minor) Initial support for [https://zettelkasten.de/posts/tags/folgezettel/|Folgezettel]. + If you click on “Folge” (detail view or info view), a new zettel is created with a reference (precursor) to the original zettel. + Title, role, tags, and syntax are copied from the original zettel. + * (major) Most predefined zettel have a title prefix of “Zettelstore”. + * (minor) If started in simple mode, e.g. via double click or without any command, some information for the new user is presented. + In the terminal, there is a hint about opening the web browser and use a specific URL. + A Welcome zettel is created, to give some more information. + (This change also applies to the server itself, but it is more suited to the WebUI user.)

Changes for Version 0.0.7 (2020-11-24)

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

Changes for Version 0.0.6 (2020-11-23)

Server

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

Zettelmarkup

* (bug) Allow to specify a fragment in a reference to a zettel. Used to link to an internal position within a zettel. This applies to CommonMark too.

API

- * (bug) Encoding binary content in format “json” now results - in valid JSON content. - * (bug) All query parameters of selecting zettel must be true, regardless - if a specific key occurs more than one or not. - * (minor) Encode all inherited meta values in all formats except - “raw”. A meta value is called inherited if - there is a key starting with default- in the - Zettelstore Runtime Configuration. Applies to WebUI also. - * (minor) Automatic calculated identifier for headings (only for - “html”, “djson”, “native” - format and for the Web user interface). You can use this to - provide a zettel reference that links to the heading, without - specifying an explicit mark ([!mark]). + * (bug) Encoding binary content in format “json” now results in valid JSON content. + * (bug) All query parameter of filtering a list must be true, regardless if a specific key occurs more than one or not. + * (minor) Encode all inherited meta values in all formats except “raw”. + A meta value is called inherited if there is a key starting with default- in the Zettelstore Runtime Configuration. + Applies to WebUI also. + * (minor) Automatic calculated identifier for headings (only for “html”, “djson”, “native” format and for the Web user interface). + You can use this to provide a zettel reference that links to the heading, without specifying an explicit mark ([!mark]). * (major) Allow to retrieve all references of a given zettel.

Web user interface (WebUI)

- * (minor) Focus on the first text field on some forms (new zettel, edit - zettel, rename zettel, login) + * (minor) Focus on the first text field on some forms (new zettel, edit zettel, rename zettel, login) * (major) Adapt all HTML templates to a simpler structure. - * (bug) Rendered wrong URLs for internal links on info page. - * (bug) If a zettel contains binary content it cannot be cloned. - For such a zettel only the metadata can be changed. - * (minor) Non-zettel references that neither have an URL scheme, user info, - nor host name, are considered “local references” (in - contrast to “zettel references” and “external - references”). When a local reference is displayed as an URL - on the WebUI, it will not opened in a new window/tab. They will - receive a local marker, when encoded as “djson” - or “native”. Local references are listed on the - Info page of each zettel. - * (minor) Change the default value for some visual sugar putd after an - external URL to &\#8599;&\#xfe0e; - (“↗︎”). This affects the former key - icon-material of the Zettelstore Runtime - Configuration, which is renamed to marker-external. + * (bug) Rendered wrong URLs for internal links on info page. + * (bug) If a zettel contains binary content it cannot be cloned. + For such a zettel only the metadata can be changed. + * (minor) Non-zettel references that neither have an URL scheme, user info, nor host name, + are considered “local references” (in contrast to “zettel references” and “external references”). + When a local reference is displayed as an URL on the WebUI, it will not opened in a new window/tab. + They will receive a local marker, when encoded as “djson” or “native”. + Local references are listed on the Info page of each zettel. + * (minor) Change the default value for some visual sugar placed after an external URL to &\#8599;&\#xfe0e; (“↗︎”). + This affects the former key icon-material of the Zettelstore Runtime Configuration, which is renamed to marker-external. * (major) Allow multiple zettel to act as templates for creating new zettel. - All zettel with a role value “new-template” act as - a template to create a new zettel. The WebUI menu item - “New” changed to a drop-down list with all those - zettel, ordered by their identifier. All metadata keys with the - prefix new- will be translated to a new or updated - keys/value without that prefix. You can use this mechanism to - specify a role for the new zettel, or a different title. The title - of the template zettel is used in the drop-down list. The initial - template zettel “New Zettel” has now a different - zettel identifier (now: 00000000091001, was: - 00000000040001). Please update it, if you changed that - zettel. -
Note: this feature was superseded in [#0_0_10|version 0.0.10] - by the “New Menu” zettel. - * (minor) When a page should be opened in a new windows (e.g. for external - references), the web browser is instructed to decouple the new - page from the previous one for privacy and security reasons. In - detail, the web browser is instructed to omit referrer information - and to omit a JS object linking to the page that contained the - external link. - * (minor) If the value of the Zettelstore Runtime Configuration key - list-page-size is greater than zero, the number of WebUI - list elements will be restricted and it is possible to change to - the next/previous page to list more elements. - * (minor) Change CSS to enhance reading: make line-height - a little smaller (previous: 1.6, now 1.4) and move list items to - the left. + All zettel with a role value “new-template” act as a template to create a new zettel. + The WebUI menu item “New” changed to a drop-down list with all those zettel, ordered by their identifier. + All metadata keys with the prefix new- will be translated to a new or updated keys/value without that prefix. + You can use this mechanism to specify a role for the new zettel, or a different title. + The title of the template zettel is used in the drop-down list. + The initial template zettel “New Zettel” has now a different zettel identifier + (now: 00000000091001, was: 00000000040001). + Please update it, if you changed that zettel. +
Note: this feature was superseded in [#0_0_10|version 0.0.10] by the “New Menu” zettel. + * (minor) When a page should be opened in a new windows (e.g. for external references), + the web browser is instructed to decouple the new page from the previous one for privacy and security reasons. + In detail, the web browser is instructed to omit referrer information and to omit a JS object linking to the page that contained the external link. + * (minor) If the value of the Zettelstore Runtime Configuration key list-page-size is greater than zero, + the number of WebUI list elements will be restricted and it is possible to change to the next/previous page to list more elements. + * (minor) Change CSS to enhance reading: make line-height a little smaller (previous: 1.6, now 1.4) and move list items to the left.

Changes for Version 0.0.5 (2020-10-22)

- * Application Programming Interface (API) to allow external software to - retrieve zettel data from the Zettelstore. - * Specify boxes, where zettel are stored, via an URL. + * Application Programming Interface (API) to allow external software to retrieve zettel data from the Zettelstore. + * Specify places, where zettel are stored, via an URL. * Add support for a custom footer.

Changes for Version 0.0.4 (2020-09-11)

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

Changes for Version 0.0.3 (2020-08-31)

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

Changes for Version 0.0.2 (2020-08-28)

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

Changes for Version 0.0.1 (2020-08-21)

* Initial public release. Index: www/download.wiki ================================================================== --- www/download.wiki +++ www/download.wiki @@ -7,20 +7,19 @@ * However, it is in use by the main developer since March 2020 without any damage. * It may be useful for you. It is useful for me. * Take a look at the [https://zettelstore.de/manual/|manual] to know how to start and use it.

ZIP-ped Executables

-Build: v0.0.14 (2021-07-23). +Build: v0.0.13 (2021-06-01). - * [/uv/zettelstore-0.0.14-linux-amd64.zip|Linux] (amd64) - * [/uv/zettelstore-0.0.14-linux-arm.zip|Linux] (arm6, e.g. Raspberry Pi) - * [/uv/zettelstore-0.0.14-windows-amd64.zip|Windows] (amd64) - * [/uv/zettelstore-0.0.14-darwin-amd64.zip|macOS] (amd64) - * [/uv/zettelstore-0.0.14-darwin-arm64.zip|macOS] (arm64, aka Apple silicon) + * [/uv/zettelstore-0.0.13-linux-amd64.zip|Linux] (amd64) + * [/uv/zettelstore-0.0.13-linux-arm.zip|Linux] (arm6, e.g. Raspberry Pi) + * [/uv/zettelstore-0.0.13-windows-amd64.zip|Windows] (amd64) + * [/uv/zettelstore-0.0.13-darwin-amd64.zip|macOS] (amd64) + * [/uv/zettelstore-0.0.13-darwin-arm64.zip|macOS] (arm64, aka Apple silicon) Unzip the appropriate file, install and execute Zettelstore according to the manual.

Zettel for the manual

-As a starter, you can download the zettel for the manual -[/uv/manual-0.0.13.zip|here]. Just unzip the contained files and put them into -your zettel folder or configure a file box to read the zettel directly from the -ZIP file. +As a starter, you can download the zettel for the manual [/uv/manual-0.0.13.zip|here]. +Just unzip the contained files and put them into your zettel folder or configure +a file place to read the zettel directly from the ZIP file. Index: www/index.wiki ================================================================== --- www/index.wiki +++ www/index.wiki @@ -15,17 +15,17 @@ The software, including the manual, is licensed under the [/file?name=LICENSE.txt&ci=trunk|European Union Public License 1.2 (or later)]. [https://twitter.com/zettelstore|Stay tuned]…
-

Latest Release: 0.0.14 (2021-07-23)

+

Latest Release: 0.0.13 (2021-06-01)

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

Build instructions

Index: www/plan.wiki ================================================================== --- www/plan.wiki +++ www/plan.wiki @@ -4,11 +4,11 @@ They are planned to be solved.

Serious limitations

* Content with binary data (e.g. a GIF, PNG, or JPG file) cannot be created nor modified via the standard web interface. As a workaround, you should - put your file into the directory where your zettel are stored. Make sure + place your file into the directory where your zettel are stored. Make sure that the file name starts with unique 14 digits that make up the zettel identifier. * Automatic lists and transclusions are not supported in Zettelmarkup. * … @@ -20,13 +20,15 @@ * The horizontal tab character (U+0009) is not supported. * Missing support for citation keys. * Changing the content syntax is not reflected in file extension. * File names with additional text besides the zettel identifier are not always preserved. + * Backspace character in links does not always work, esp. for \| or + \]. * …

Planned improvements

* Support for mathematical content is missing, e.g. $$F(x) &= \\int^a_b \\frac{1}{3}x^3$$. * Render zettel in [https://pandoc.org|pandoc's] JSON version of their native AST to make pandoc an external renderer for Zettelstore. * …