Many hyperlinks are disabled.
Use anonymous login
to enable hyperlinks.
Difference From version-0.0.13 To version-0.0.14
2021-07-23
| ||
16:43 | Increase version to 0.0.15-dev to begin next development cycle ... (check-in: ba65777850 user: stern tags: trunk) | |
11:02 | Version 0.0.14 ... (check-in: 6fe53d5db2 user: stern tags: trunk, release, version-0.0.14) | |
2021-07-22
| ||
13:50 | Add client API for retrieving zettel links ... (check-in: e43ff68174 user: stern tags: trunk) | |
2021-06-07
| ||
09:11 | Increase version to 0.0.14-dev to begin next development cycle ... (check-in: 7dd6f4dd5c user: stern tags: trunk) | |
2021-06-01
| ||
12:35 | Version 0.0.13 ... (check-in: 11d9b6da63 user: stern tags: trunk, release, version-0.0.13) | |
10:14 | Log output while starting Command Line Server ... (check-in: 968a91bbaa user: stern tags: trunk) | |
Changes to Makefile.
1 2 3 4 5 6 7 8 9 | ## 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. | | > > > | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 | ## 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. .PHONY: check api 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 release: go run tools/build.go release clean: go run tools/build.go clean |
Changes to VERSION.
|
| | | 1 | 0.0.14 |
Added api/api.go.
> > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 | //----------------------------------------------------------------------------- // Copyright (c) 2021 Detlef Stern // // This file is part of zettelstore. // // Zettelstore is licensed under the latest version of the EUPL (European Union // Public License). Please see file LICENSE.txt for your rights and obligations // under this license. //----------------------------------------------------------------------------- // Package api contains common definition used for client and server. package api // AuthJSON contains the result of an authentication call. type AuthJSON struct { Token string `json:"token"` Type string `json:"token_type"` Expires int `json:"expires_in"` } // ZidJSON contains the identifier data of a zettel. type ZidJSON struct { ID string `json:"id"` URL string `json:"url"` } // ZidMetaJSON contains the identifier and the metadata of a zettel. type ZidMetaJSON struct { ID string `json:"id"` URL string `json:"url"` Meta map[string]string `json:"meta"` } // ZidMetaRelatedList contains identifier/metadata of a zettel and the same for related zettel type ZidMetaRelatedList struct { ID string `json:"id"` URL string `json:"url"` Meta map[string]string `json:"meta"` List []ZidMetaJSON `json:"list"` } // ZettelLinksJSON store all links / connections from one zettel to other. type ZettelLinksJSON struct { ID string `json:"id"` URL string `json:"url"` Links struct { Incoming []ZidJSON `json:"incoming,omitempty"` Outgoing []ZidJSON `json:"outgoing,omitempty"` Local []string `json:"local,omitempty"` External []string `json:"external,omitempty"` Meta []string `json:"meta,omitempty"` } `json:"links"` Images struct { Outgoing []ZidJSON `json:"outgoing,omitempty"` Local []string `json:"local,omitempty"` External []string `json:"external,omitempty"` } `json:"images,omitempty"` Cites []string `json:"cites,omitempty"` } // ZettelDataJSON contains all data for a zettel. type ZettelDataJSON struct { Meta map[string]string `json:"meta"` Encoding string `json:"encoding"` Content string `json:"content"` } // ZettelJSON contains all data for a zettel, the identifier, the metadata, and the content. type ZettelJSON struct { ID string `json:"id"` URL string `json:"url"` Meta map[string]string `json:"meta"` Encoding string `json:"encoding"` Content string `json:"content"` } // ZettelListJSON contains data for a zettel list. type ZettelListJSON struct { List []ZettelJSON `json:"list"` } // TagListJSON specifies the list/map of tags type TagListJSON struct { Tags map[string][]string `json:"tags"` } // RoleListJSON specifies the list of roles. type RoleListJSON struct { Roles []string `json:"role-list"` } |
Added api/const.go.
> > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 | //----------------------------------------------------------------------------- // Copyright (c) 2021 Detlef Stern // // This file is part of zettelstore. // // Zettelstore is licensed under the latest version of the EUPL (European Union // Public License). Please see file LICENSE.txt for your rights and obligations // under this license. //----------------------------------------------------------------------------- // Package api contains common definition used for client and server. package api import ( "fmt" ) // Additional HTTP constants used. const ( MethodMove = "MOVE" // HTTP method for renaming a zettel HeaderAccept = "Accept" HeaderContentType = "Content-Type" HeaderDestination = "Destination" HeaderLocation = "Location" ) // Values for HTTP query parameter. const ( QueryKeyDepth = "depth" QueryKeyDir = "dir" QueryKeyFormat = "_format" QueryKeyLimit = "limit" QueryKeyPart = "_part" ) // Supported dir values. const ( DirBackward = "backward" DirForward = "forward" ) // Supported format values. const ( FormatDJSON = "djson" FormatHTML = "html" FormatJSON = "json" FormatNative = "native" FormatRaw = "raw" FormatText = "text" FormatZMK = "zmk" ) var formatEncoder = map[string]EncodingEnum{ FormatDJSON: EncoderDJSON, FormatHTML: EncoderHTML, FormatJSON: EncoderJSON, FormatNative: EncoderNative, FormatRaw: EncoderRaw, FormatText: EncoderText, FormatZMK: EncoderZmk, } var encoderFormat = map[EncodingEnum]string{} func init() { for k, v := range formatEncoder { encoderFormat[v] = k } } // Encoder returns the internal encoder code for the given format string. func Encoder(format string) EncodingEnum { if e, ok := formatEncoder[format]; ok { return e } return EncoderUnknown } // EncodingEnum lists all valid encoder keys. type EncodingEnum uint8 // Values for EncoderEnum const ( EncoderUnknown EncodingEnum = iota EncoderDJSON EncoderHTML EncoderJSON EncoderNative EncoderRaw EncoderText EncoderZmk ) // String representation of an encoder key. func (e EncodingEnum) String() string { if f, ok := encoderFormat[e]; ok { return f } return fmt.Sprintf("*Unknown*(%d)", e) } // Supported part values. const ( PartID = "id" PartMeta = "meta" PartContent = "content" PartZettel = "zettel" ) |
Added api/urlbuilder.go.
> > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 | //----------------------------------------------------------------------------- // Copyright (c) 2020-2021 Detlef Stern // // This file is part of zettelstore. // // Zettelstore is licensed under the latest version of the EUPL (European Union // Public License). Please see file LICENSE.txt for your rights and obligations // under this license. //----------------------------------------------------------------------------- // Package api contains common definition used for client and server. package api import ( "net/url" "strings" "zettelstore.de/z/domain/id" ) type urlQuery struct{ key, val string } // URLBuilder should be used to create zettelstore URLs. type URLBuilder struct { prefix string key byte path []string query []urlQuery fragment string } // NewURLBuilder creates a new URL builder with the given prefix and key. func NewURLBuilder(prefix string, key byte) *URLBuilder { return &URLBuilder{prefix: prefix, key: key} } // Clone an URLBuilder func (ub *URLBuilder) Clone() *URLBuilder { cpy := new(URLBuilder) cpy.key = ub.key if len(ub.path) > 0 { cpy.path = make([]string, 0, len(ub.path)) cpy.path = append(cpy.path, ub.path...) } if len(ub.query) > 0 { cpy.query = make([]urlQuery, 0, len(ub.query)) cpy.query = append(cpy.query, ub.query...) } cpy.fragment = ub.fragment return cpy } // SetZid sets the zettel identifier. func (ub *URLBuilder) SetZid(zid id.Zid) *URLBuilder { if len(ub.path) > 0 { panic("Cannot add Zid") } ub.path = append(ub.path, zid.String()) return ub } // AppendPath adds a new path element func (ub *URLBuilder) AppendPath(p string) *URLBuilder { ub.path = append(ub.path, p) return ub } // AppendQuery adds a new query parameter func (ub *URLBuilder) AppendQuery(key, value string) *URLBuilder { ub.query = append(ub.query, urlQuery{key, value}) return ub } // ClearQuery removes all query parameters. func (ub *URLBuilder) ClearQuery() *URLBuilder { ub.query = nil ub.fragment = "" return ub } // SetFragment stores the fragment func (ub *URLBuilder) SetFragment(s string) *URLBuilder { ub.fragment = s return ub } // String produces a string value. func (ub *URLBuilder) String() string { var sb strings.Builder sb.WriteString(ub.prefix) if ub.key != '/' { sb.WriteByte(ub.key) } for _, p := range ub.path { sb.WriteByte('/') sb.WriteString(url.PathEscape(p)) } if len(ub.fragment) > 0 { sb.WriteByte('#') sb.WriteString(ub.fragment) } for i, q := range ub.query { if i == 0 { sb.WriteByte('?') } else { sb.WriteByte('&') } sb.WriteString(q.key) sb.WriteByte('=') sb.WriteString(url.QueryEscape(q.val)) } return sb.String() } |
Changes to ast/ast.go.
︙ | ︙ | |||
28 29 30 31 32 33 34 | Zid id.Zid // Zettel identification. InhMeta *meta.Meta // Metadata of the zettel, with inherited values. Ast BlockSlice // Zettel abstract syntax tree is a sequence of block nodes. } // Node is the interface, all nodes must implement. type Node interface { | | | 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 | Zid id.Zid // Zettel identification. InhMeta *meta.Meta // Metadata of the zettel, with inherited values. Ast BlockSlice // Zettel abstract syntax tree is a sequence of block nodes. } // Node is the interface, all nodes must implement. type Node interface { WalkChildren(v Visitor) } // BlockNode is the interface that all block nodes must implement. type BlockNode interface { Node blockNode() } |
︙ | ︙ |
Changes to ast/attr_test.go.
1 | //----------------------------------------------------------------------------- | | > > | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 | //----------------------------------------------------------------------------- // Copyright (c) 2020-2021 Detlef Stern // // This file is part of zettelstore. // // Zettelstore is licensed under the latest version of the EUPL (European Union // Public License). Please see file LICENSE.txt for your rights and obligations // under this license. //----------------------------------------------------------------------------- // Package ast provides the abstract syntax tree. package ast_test import ( "testing" "zettelstore.de/z/ast" ) func TestHasDefault(t *testing.T) { t.Parallel() attr := &ast.Attributes{} if attr.HasDefault() { t.Error("Should not have default attr") } attr = &ast.Attributes{Attrs: map[string]string{"-": "value"}} if !attr.HasDefault() { t.Error("Should have default attr") } } func TestAttrClone(t *testing.T) { t.Parallel() orig := &ast.Attributes{} clone := orig.Clone() if len(clone.Attrs) > 0 { t.Error("Attrs must be empty") } orig = &ast.Attributes{Attrs: map[string]string{"": "0", "-": "1", "a": "b"}} |
︙ | ︙ |
Changes to ast/block.go.
︙ | ︙ | |||
15 16 17 18 19 20 21 | // ParaNode contains just a sequence of inline elements. // Another name is "paragraph". type ParaNode struct { Inlines InlineSlice } | | | | | | > > | | | | | | | | | | | | | | | | > > > | | | | > > | | | | | | | | | | | | > > > > | | > > > > > > > | 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 | // ParaNode contains just a sequence of inline elements. // 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) } //-------------------------------------------------------------------------- // VerbatimNode contains lines of uninterpreted text type VerbatimNode struct { Kind VerbatimKind Attrs *Attributes Lines []string } // VerbatimKind specifies the format that is applied to code inline nodes. type VerbatimKind uint8 // Constants for VerbatimCode const ( _ VerbatimKind = 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 */ } // WalkChildren does nothing. func (vn *VerbatimNode) WalkChildren(v Visitor) { /* No children*/ } //-------------------------------------------------------------------------- // RegionNode encapsulates a region of block nodes. type RegionNode struct { Kind RegionKind Attrs *Attributes Blocks BlockSlice Inlines InlineSlice // Additional text at the end of the region } // RegionKind specifies the actual region type. type RegionKind uint8 // Values for RegionCode const ( _ RegionKind = 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) } //-------------------------------------------------------------------------- // HeadingNode stores the heading text and level. type HeadingNode struct { Level int 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 */ } // WalkChildren walks the heading text. func (hn *HeadingNode) WalkChildren(v Visitor) { WalkInlineSlice(v, hn.Inlines) } //-------------------------------------------------------------------------- // HRuleNode specifies a horizontal rule. type HRuleNode struct { Attrs *Attributes } func (hn *HRuleNode) blockNode() { /* Just a marker */ } func (hn *HRuleNode) itemNode() { /* Just a marker */ } // WalkChildren does nothing. func (hn *HRuleNode) WalkChildren(v Visitor) { /* No children*/ } //-------------------------------------------------------------------------- // NestedListNode specifies a nestable list, either ordered or unordered. type NestedListNode struct { Kind NestedListKind Items []ItemSlice Attrs *Attributes } // NestedListKind specifies the actual list type. type NestedListKind uint8 // Values for ListCode const ( _ NestedListKind = 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) } } //-------------------------------------------------------------------------- // DescriptionListNode specifies a description list. type DescriptionListNode struct { Descriptions []Description } // Description is one element of a description list. type Description struct { Term InlineSlice 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) } } } //-------------------------------------------------------------------------- // TableNode specifies a full table type TableNode struct { Header TableRow // The header row Align []Alignment // Default column alignment |
︙ | ︙ | |||
179 180 181 182 183 184 185 | _ Alignment = iota AlignDefault // Default alignment, inherited AlignLeft // Left alignment AlignCenter // Center the content AlignRight // Right alignment ) | | | | > > > > > > > > > | | | | 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 | _ Alignment = iota AlignDefault // Default alignment, inherited 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) } } } //-------------------------------------------------------------------------- // BLOBNode contains just binary data that must be interpreted according to // a syntax. type BLOBNode struct { Title string Syntax string Blob []byte } func (bn *BLOBNode) blockNode() { /* Just a marker */ } // WalkChildren does nothing. func (bn *BLOBNode) WalkChildren(v Visitor) { /* No children*/ } |
Changes to ast/inline.go.
︙ | ︙ | |||
14 15 16 17 18 19 20 | // Definitions of inline nodes. // TextNode just contains some text. type TextNode struct { Text string // The text itself. } | | | | | | | | | | | | | | | | > > | | | > > | | | > > | | | | | | > > | | | | | | | > > | | | | | | | | 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 | // Definitions of inline nodes. // TextNode just contains some text. type TextNode struct { Text string // The text itself. } func (tn *TextNode) inlineNode() { /* Just a marker */ } // WalkChildren does nothing. func (tn *TextNode) WalkChildren(v Visitor) { /* No children*/ } // -------------------------------------------------------------------------- // TagNode contains a tag. type TagNode struct { Tag string // The text itself. } func (tn *TagNode) inlineNode() { /* Just a marker */ } // WalkChildren does nothing. func (tn *TagNode) WalkChildren(v Visitor) { /* No children*/ } // -------------------------------------------------------------------------- // SpaceNode tracks inter-word space characters. type SpaceNode struct { Lexeme string } func (sn *SpaceNode) inlineNode() { /* Just a marker */ } // WalkChildren does nothing. func (sn *SpaceNode) WalkChildren(v Visitor) { /* No children*/ } // -------------------------------------------------------------------------- // BreakNode signals a new line that must / should be interpreted as a new line break. type BreakNode struct { Hard bool // Hard line break? } func (bn *BreakNode) inlineNode() { /* Just a marker */ } // WalkChildren does nothing. func (bn *BreakNode) WalkChildren(v Visitor) { /* No children*/ } // -------------------------------------------------------------------------- // LinkNode contains the specified link. type LinkNode struct { Ref *Reference Inlines InlineSlice // The text associated with the link. OnlyRef bool // True if no text was specified. Attrs *Attributes // Optional attributes } func (ln *LinkNode) inlineNode() { /* Just a marker */ } // WalkChildren walks to the link text. func (ln *LinkNode) WalkChildren(v Visitor) { WalkInlineSlice(v, ln.Inlines) } // -------------------------------------------------------------------------- // ImageNode contains the specified image reference. type ImageNode struct { Ref *Reference // Reference to image Blob []byte // BLOB data of the image, as an alternative to Ref. Syntax string // Syntax of Blob Inlines InlineSlice // The text associated with the image. Attrs *Attributes // Optional attributes } func (in *ImageNode) inlineNode() { /* Just a marker */ } // WalkChildren walks to the image text. func (in *ImageNode) WalkChildren(v Visitor) { WalkInlineSlice(v, in.Inlines) } // -------------------------------------------------------------------------- // CiteNode contains the specified citation. type CiteNode struct { Key string // The citation key Inlines InlineSlice // The text associated with the citation. Attrs *Attributes // Optional attributes } func (cn *CiteNode) inlineNode() { /* Just a marker */ } // WalkChildren walks to the cite text. func (cn *CiteNode) WalkChildren(v Visitor) { WalkInlineSlice(v, cn.Inlines) } // -------------------------------------------------------------------------- // MarkNode contains the specified merked position. // It is a BlockNode too, because although it is typically parsed during inline // mode, it is moved into block mode afterwards. type MarkNode struct { Text string } func (mn *MarkNode) inlineNode() { /* Just a marker */ } // WalkChildren does nothing. func (mn *MarkNode) WalkChildren(v Visitor) { /* No children*/ } // -------------------------------------------------------------------------- // FootnoteNode contains the specified footnote. type FootnoteNode struct { Inlines InlineSlice // The footnote text. Attrs *Attributes // Optional attributes } func (fn *FootnoteNode) inlineNode() { /* Just a marker */ } // WalkChildren walks to the footnote text. func (fn *FootnoteNode) WalkChildren(v Visitor) { WalkInlineSlice(v, fn.Inlines) } // -------------------------------------------------------------------------- // FormatNode specifies some inline formatting. type FormatNode struct { Kind FormatKind Attrs *Attributes // Optional attributes. Inlines InlineSlice } // FormatKind specifies the format that is applied to the inline nodes. type FormatKind uint8 // Constants for FormatCode const ( _ FormatKind = iota FormatItalic // Italic text. FormatEmph // Semantically emphasized text. FormatBold // Bold text. FormatStrong // Semantically strongly emphasized text. FormatUnder // Underlined text. FormatInsert // Inserted text. FormatStrike // Text that is no longer relevant or no longer accurate. FormatDelete // Deleted text. FormatSuper // Superscripted text. FormatSub // SubscriptedText. FormatQuote // Quoted text. FormatQuotation // Quotation text. FormatSmall // Smaller text. FormatSpan // Generic inline container. FormatMonospace // Monospaced text. ) func (fn *FormatNode) inlineNode() { /* Just a marker */ } // WalkChildren walks to the formatted text. func (fn *FormatNode) WalkChildren(v Visitor) { WalkInlineSlice(v, fn.Inlines) } // -------------------------------------------------------------------------- // LiteralNode specifies some uninterpreted text. type LiteralNode struct { Kind LiteralKind Attrs *Attributes // Optional attributes. Text string } // LiteralKind specifies the format that is applied to code inline nodes. type LiteralKind uint8 // Constants for LiteralCode const ( _ LiteralKind = 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 */ } // WalkChildren does nothing. func (ln *LiteralNode) WalkChildren(v Visitor) { /* No children*/ } |
Changes to ast/ref_test.go.
︙ | ︙ | |||
14 15 16 17 18 19 20 21 22 23 24 25 26 27 | import ( "testing" "zettelstore.de/z/ast" ) func TestParseReference(t *testing.T) { testcases := []struct { link string err bool exp string }{ {"", true, ""}, {"123", false, "123"}, | > | 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 | import ( "testing" "zettelstore.de/z/ast" ) func TestParseReference(t *testing.T) { t.Parallel() testcases := []struct { link string err bool exp string }{ {"", true, ""}, {"123", false, "123"}, |
︙ | ︙ | |||
37 38 39 40 41 42 43 44 45 46 47 48 49 50 | if got.IsValid() && got.String() != tc.exp { t.Errorf("TC=%d, Reference of %q is %q, but got %q", i, tc.link, tc.exp, got) } } } func TestReferenceIsZettelMaterial(t *testing.T) { testcases := []struct { link string isZettel bool isExternal bool isLocal bool }{ {"", false, false, false}, | > | 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 | if got.IsValid() && got.String() != tc.exp { t.Errorf("TC=%d, Reference of %q is %q, but got %q", i, tc.link, tc.exp, got) } } } func TestReferenceIsZettelMaterial(t *testing.T) { t.Parallel() testcases := []struct { link string isZettel bool isExternal bool isLocal bool }{ {"", false, false, false}, |
︙ | ︙ |
Deleted ast/traverser.go.
|
| < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < |
Deleted ast/visitor.go.
|
| < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < |
Added ast/walk.go.
> > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 | //----------------------------------------------------------------------------- // 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) } } |
Changes to auth/auth.go.
︙ | ︙ | |||
10 11 12 13 14 15 16 17 18 19 | // Package auth provides services for authentification / authorization. package auth import ( "time" "zettelstore.de/z/config" "zettelstore.de/z/domain/id" "zettelstore.de/z/domain/meta" | > < | 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 | // Package auth provides services for authentification / authorization. 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/web/server" ) // BaseManager allows to check some base auth modes. type BaseManager interface { // IsReadonly returns true, if the systems is configured to run in read-only-mode. IsReadonly() bool |
︙ | ︙ | |||
75 76 77 78 79 80 81 | } // Manager is the main interface for providing the service. type Manager interface { TokenManager AuthzManager | | | 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 | } // 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) } // Policy is an interface for checking access authorization. type Policy interface { // User is allowed to create a new zettel. CanCreate(user, newMeta *meta.Meta) bool |
︙ | ︙ |
Changes to auth/impl/impl.go.
︙ | ︙ | |||
17 18 19 20 21 22 23 24 25 26 27 | "io" "time" "github.com/pascaldekloe/jwt" "zettelstore.de/z/auth" "zettelstore.de/z/auth/policy" "zettelstore.de/z/config" "zettelstore.de/z/domain/id" "zettelstore.de/z/domain/meta" "zettelstore.de/z/kernel" | > < | 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 | "io" "time" "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/web/server" ) type myAuth struct { readonly bool owner id.Zid secret []byte |
︙ | ︙ | |||
175 176 177 178 179 180 181 | if ur := meta.GetUserRole(val); ur != meta.UserRoleUnknown { return ur } } return meta.UserRoleReader } | | | | 175 176 177 178 179 180 181 182 183 184 | if ur := meta.GetUserRole(val); ur != meta.UserRoleUnknown { return ur } } 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) } |
Added auth/policy/box.go.
> > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 | //----------------------------------------------------------------------------- // 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) } |
Changes to auth/policy/owner.go.
︙ | ︙ | |||
60 61 62 63 64 65 66 | if user == nil { return false } if role, ok := m.Get(meta.KeyRole); ok && role == meta.ValueRoleUser { // Only the user can read its own zettel return user.Zid == m.Zid } | > > | > > > > > | 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 | if user == nil { return false } 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 } } var noChangeUser = []string{ meta.KeyID, meta.KeyRole, meta.KeyUserID, meta.KeyUserRole, |
︙ | ︙ | |||
94 95 96 97 98 99 100 | for _, key := range noChangeUser { if oldMeta.GetDefault(key, "") != newMeta.GetDefault(key, "") { return false } } return true } | | > | 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 | for _, key := range noChangeUser { if oldMeta.GetDefault(key, "") != newMeta.GetDefault(key, "") { return false } } return true } switch userRole := o.manager.GetUserRole(user); userRole { case meta.UserRoleReader, meta.UserRoleCreator: return false } return o.userCanCreate(user, newMeta) } func (o *ownerPolicy) CanRename(user, m *meta.Meta) bool { if user == nil || !o.pre.CanRename(user, m) { |
︙ | ︙ |
Deleted auth/policy/place.go.
|
| < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < |
Changes to auth/policy/policy_test.go.
︙ | ︙ | |||
17 18 19 20 21 22 23 24 25 26 27 28 29 30 | "zettelstore.de/z/auth" "zettelstore.de/z/domain/id" "zettelstore.de/z/domain/meta" ) func TestPolicies(t *testing.T) { testScene := []struct { readonly bool withAuth bool expert bool }{ {true, true, true}, {true, true, false}, | > | 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 | "zettelstore.de/z/auth" "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 }{ {true, true, true}, {true, true, false}, |
︙ | ︙ | |||
84 85 86 87 88 89 90 | type authConfig struct{ expert bool } 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 { | < < < < | < < < > > > > > > > > > > > > > > > > > > > > > > | 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 | type authConfig struct{ expert bool } 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) } 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() userZettel := newUserZettel() 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}, // 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}, } for _, tc := range testCases { t.Run("Create", func(tt *testing.T) { got := pol.CanCreate(tc.user, tc.meta) if tc.exp != got { tt.Errorf("exp=%v, but got=%v", tc.exp, got) } }) } } 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 { 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}, // 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}, {owner2, owner2, true}, } for _, tc := range testCases { t.Run("Read", func(tt *testing.T) { got := pol.CanRead(tc.user, tc.meta) if tc.exp != got { tt.Errorf("exp=%v, but got=%v", tc.exp, got) } }) } } 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() publicZettel := newPublicZettel() loginZettel := newLoginZettel() |
︙ | ︙ | |||
246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 326 327 328 329 330 331 332 333 334 335 336 337 338 339 340 341 342 343 344 345 346 347 348 349 350 351 352 353 354 355 356 357 358 359 360 361 362 363 364 365 366 367 368 369 370 371 372 373 374 375 376 377 378 379 380 381 382 383 384 385 386 387 388 389 390 391 392 393 394 395 396 397 398 399 400 401 402 403 404 405 406 407 408 409 410 411 412 413 414 415 416 417 418 419 420 421 422 423 424 425 426 427 428 429 430 431 432 433 434 435 436 437 438 439 440 441 442 443 444 445 446 447 448 449 450 451 452 453 454 455 456 457 458 459 460 461 462 463 464 465 466 467 468 469 470 471 472 473 474 475 476 477 478 479 480 481 482 483 484 485 486 487 488 489 490 491 492 493 494 495 496 497 498 499 500 501 502 503 504 505 506 507 508 509 510 511 512 513 514 515 516 517 | user *meta.Meta old *meta.Meta new *meta.Meta exp bool }{ // No old and new meta {anonUser, 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}, {reader, nil, zettel, false}, {writer, nil, zettel, false}, {owner, nil, zettel, false}, {owner2, nil, zettel, false}, // No new meta {anonUser, 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}, {reader, zettel, publicZettel, false}, {writer, zettel, publicZettel, false}, {owner, zettel, publicZettel, false}, {owner2, zettel, publicZettel, false}, // Overwrite a normal zettel {anonUser, 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}, {reader, publicZettel, publicZettel, notAuthNotReadonly}, {writer, publicZettel, publicZettel, !readonly}, {owner, publicZettel, publicZettel, !readonly}, {owner2, publicZettel, publicZettel, !readonly}, // Login zettel {anonUser, 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}, {reader, ownerZettel, ownerZettel, notAuthNotReadonly}, {writer, ownerZettel, ownerZettel, notAuthNotReadonly}, {owner, ownerZettel, ownerZettel, !readonly}, {owner2, ownerZettel, ownerZettel, !readonly}, // Expert zettel {anonUser, 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}, {reader, userZettel, userZettel, notAuthNotReadonly}, {writer, userZettel, userZettel, notAuthNotReadonly}, {owner, userZettel, userZettel, !readonly}, {owner2, userZettel, userZettel, !readonly}, // Own user zettel {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}, {reader, roFalse, roFalse, notAuthNotReadonly}, {writer, roFalse, roFalse, !readonly}, {owner, roFalse, roFalse, !readonly}, {owner2, roFalse, roFalse, !readonly}, // Reader r/o zettel {anonUser, 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}, {reader, roWriter, roWriter, false}, {writer, roWriter, roWriter, false}, {owner, roWriter, roWriter, !readonly}, {owner2, roWriter, roWriter, !readonly}, // Owner r/o zettel {anonUser, 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}, {reader, roTrue, roTrue, false}, {writer, roTrue, roTrue, false}, {owner, roTrue, roTrue, false}, {owner2, roTrue, roTrue, false}, } for _, tc := range testCases { t.Run("Write", func(tt *testing.T) { got := pol.CanWrite(tc.user, tc.old, tc.new) if tc.exp != got { tt.Errorf("exp=%v, but got=%v", tc.exp, got) } }) } } func testRename(t *testing.T, pol auth.Policy, withAuth, readonly, expert bool) { t.Helper() anonUser := newAnon() 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() 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("Delete", func(tt *testing.T) { got := pol.CanDelete(tc.user, tc.meta) if tc.exp != got { tt.Errorf("exp=%v, but got=%v", tc.exp, got) } }) } } const ( | > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > | | | > | | | | > > > > > > > | 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 326 327 328 329 330 331 332 333 334 335 336 337 338 339 340 341 342 343 344 345 346 347 348 349 350 351 352 353 354 355 356 357 358 359 360 361 362 363 364 365 366 367 368 369 370 371 372 373 374 375 376 377 378 379 380 381 382 383 384 385 386 387 388 389 390 391 392 393 394 395 396 397 398 399 400 401 402 403 404 405 406 407 408 409 410 411 412 413 414 415 416 417 418 419 420 421 422 423 424 425 426 427 428 429 430 431 432 433 434 435 436 437 438 439 440 441 442 443 444 445 446 447 448 449 450 451 452 453 454 455 456 457 458 459 460 461 462 463 464 465 466 467 468 469 470 471 472 473 474 475 476 477 478 479 480 481 482 483 484 485 486 487 488 489 490 491 492 493 494 495 496 497 498 499 500 501 502 503 504 505 506 507 508 509 510 511 512 513 514 515 516 517 518 519 520 521 522 523 524 525 526 527 528 529 530 531 532 533 534 535 536 537 538 539 540 541 542 543 544 545 546 547 548 549 550 551 552 553 554 555 556 557 558 559 560 561 562 563 564 565 566 567 568 569 570 571 572 573 574 575 576 577 578 579 580 581 582 583 584 585 586 587 588 589 590 591 592 | user *meta.Meta old *meta.Meta 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}, } for _, tc := range testCases { t.Run("Write", func(tt *testing.T) { got := pol.CanWrite(tc.user, tc.old, tc.new) if tc.exp != got { tt.Errorf("exp=%v, but got=%v", tc.exp, got) } }) } } 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}, {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}, } for _, tc := range testCases { t.Run("Delete", func(tt *testing.T) { got := pol.CanDelete(tc.user, tc.meta) if tc.exp != got { tt.Errorf("exp=%v, but got=%v", tc.exp, got) } }) } } const ( creatorZid = id.Zid(1013) readerZid = id.Zid(1013) writerZid = id.Zid(1015) ownerZid = id.Zid(1017) owner2Zid = id.Zid(1019) zettelZid = id.Zid(1021) visZid = id.Zid(1023) userZid = id.Zid(1025) ) 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) return user } |
︙ | ︙ | |||
559 560 561 562 563 564 565 566 567 568 569 570 571 572 | return m } func newPublicZettel() *meta.Meta { m := meta.New(visZid) m.Set(meta.KeyTitle, "Public Zettel") m.Set(meta.KeyVisibility, meta.ValueVisibilityPublic) return m } func newLoginZettel() *meta.Meta { m := meta.New(visZid) m.Set(meta.KeyTitle, "Login Zettel") m.Set(meta.KeyVisibility, meta.ValueVisibilityLogin) return m } | > > > > > > | 617 618 619 620 621 622 623 624 625 626 627 628 629 630 631 632 633 634 635 636 | return m } func newPublicZettel() *meta.Meta { 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 } |
︙ | ︙ |
Added box/box.go.
> > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 | //----------------------------------------------------------------------------- // 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() } |
Added box/compbox/compbox.go.
> > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 | //----------------------------------------------------------------------------- // 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) } } |
Added box/compbox/config.go.
> > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 | //----------------------------------------------------------------------------- // Copyright (c) 2020-2021 Detlef Stern // // This file is part of zettelstore. // // Zettelstore is licensed under the latest version of the EUPL (European Union // Public License). Please see file LICENSE.txt for your rights and obligations // under this license. //----------------------------------------------------------------------------- // Package 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() } |
Added box/compbox/keys.go.
> > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 | //----------------------------------------------------------------------------- // 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() } |
Added box/compbox/manager.go.
> > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 | //----------------------------------------------------------------------------- // Copyright (c) 2021 Detlef Stern // // This file is part of zettelstore. // // Zettelstore is licensed under the latest version of the EUPL (European Union // Public License). Please see file LICENSE.txt for your rights and obligations // under this license. //----------------------------------------------------------------------------- // Package 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() } |
Added box/compbox/version.go.
> > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 | //----------------------------------------------------------------------------- // 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), ) } |
Added box/constbox/base.css.
> > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 | *,*::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; } } |
Added box/constbox/base.mustache.
> > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 | <!DOCTYPE html> <html{{#Lang}} lang="{{Lang}}"{{/Lang}}> <head> <meta charset="utf-8"> <meta name="referrer" content="no-referrer"> <meta name="viewport" content="width=device-width, initial-scale=1.0"> <meta name="generator" content="Zettelstore"> <meta name="format-detection" content="telephone=no"> {{{MetaHeader}}} <link rel="stylesheet" href="{{{CSSBaseURL}}}"> <link rel="stylesheet" href="{{{CSSUserURL}}}"> <title>{{Title}}</title> </head> <body> <nav class="zs-menu"> <a href="{{{HomeURL}}}">Home</a> {{#WithUser}} <div class="zs-dropdown"> <button>User</button> <nav class="zs-dropdown-content"> {{#WithAuth}} {{#UserIsValid}} <a href="{{{UserZettelURL}}}">{{UserIdent}}</a> {{/UserIsValid}} {{^UserIsValid}} <a href="{{{LoginURL}}}">Login</a> {{/UserIsValid}} {{#UserIsValid}} <a href="{{{UserLogoutURL}}}">Logout</a> {{/UserIsValid}} {{/WithAuth}} </nav> </div> {{/WithUser}} <div class="zs-dropdown"> <button>Lists</button> <nav class="zs-dropdown-content"> <a href="{{{ListZettelURL}}}">List Zettel</a> <a href="{{{ListRolesURL}}}">List Roles</a> <a href="{{{ListTagsURL}}}">List Tags</a> </nav> </div> {{#HasNewZettelLinks}} <div class="zs-dropdown"> <button>New</button> <nav class="zs-dropdown-content"> {{#NewZettelLinks}} <a href="{{{URL}}}">{{Text}}</a> {{/NewZettelLinks}} </nav> </div> {{/HasNewZettelLinks}} <form action="{{{SearchURL}}}"> <input type="text" placeholder="Search.." name="s"> </form> </nav> <main class="content"> {{{Content}}} </main> {{#FooterHTML}} <footer> {{{FooterHTML}}} </footer> {{/FooterHTML}} </body> </html> |
Added box/constbox/constbox.go.
> > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 326 327 328 329 330 331 332 333 334 335 336 337 338 339 340 341 342 343 344 345 346 347 348 349 350 351 352 353 354 355 356 357 358 359 360 361 362 363 364 365 366 367 368 369 370 371 372 373 374 375 376 377 378 379 380 381 382 383 384 385 386 387 388 389 390 391 392 393 394 395 396 397 398 399 400 401 402 403 404 405 | //----------------------------------------------------------------------------- // 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 |
Added box/constbox/context.mustache.
> > > > > > > > > > > > > > > > | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 | <nav> <header> <h1>{{Title}}</h1> <div class="zs-meta"> <a href="{{{InfoURL}}}">Info</a> · <a href="?dir=backward">Backward</a> · <a href="?dir=both">Both</a> · <a href="?dir=forward">Forward</a> · Depth:{{#Depths}} <a href="{{{URL}}}">{{{Text}}}</a>{{/Depths}} </div> </header> <p><a href="{{{Start.URL}}}">{{{Start.Text}}}</a></p> <ul> {{#Metas}}<li><a href="{{{URL}}}">{{{Text}}}</a></li> {{/Metas}}</ul> </nav> |
Added box/constbox/contributors.zettel.
> > > > > > > > | 1 2 3 4 5 6 7 8 | Zettelstore is a software for humans made from humans. === Licensor(s) * Detlef Stern [[mailto:ds@zettelstore.de]] ** Main author ** Maintainer === Contributors |
Added box/constbox/delete.mustache.
> > > > > > > > > > > > > > > | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 | <article> <header> <h1>Delete Zettel {{Zid}}</h1> </header> <p>Do you really want to delete this zettel?</p> <dl> {{#MetaPairs}} <dt>{{Key}}:</dt><dd>{{Value}}</dd> {{/MetaPairs}} </dl> <form method="POST"> <input class="zs-button" type="submit" value="Delete"> </form> </article> {{end}} |
Added box/constbox/dependencies.zettel.
> > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 | 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. ``` |
Added box/constbox/emoji_spin.gif.
cannot compute difference between binary files
Added box/constbox/error.mustache.
> > > > > > | 1 2 3 4 5 6 | <article> <header> <h1>{{ErrorTitle}}</h1> </header> {{ErrorText}} </article> |
Added box/constbox/form.mustache.
> > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 | <article> <header> <h1>{{Heading}}</h1> </header> <form method="POST"> <div> <label for="title">Title</label> <input class="zs-input" type="text" id="title" name="title" placeholder="Title.." value="{{MetaTitle}}" autofocus> </div> <div> <div> <label for="role">Role</label> <input class="zs-input" type="text" id="role" name="role" placeholder="role.." value="{{MetaRole}}"> </div> <label for="tags">Tags</label> <input class="zs-input" type="text" id="tags" name="tags" placeholder="#tag" value="{{MetaTags}}"> </div> <div> <label for="meta">Metadata</label> <textarea class="zs-input" id="meta" name="meta" rows="4" placeholder="metakey: metavalue"> {{#MetaPairsRest}} {{Key}}: {{Value}} {{/MetaPairsRest}} </textarea> </div> <div> <label for="syntax">Syntax</label> <input class="zs-input" type="text" id="syntax" name="syntax" placeholder="syntax.." value="{{MetaSyntax}}"> </div> <div> {{#IsTextContent}} <label for="content">Content</label> <textarea class="zs-input zs-content" id="meta" name="content" rows="20" placeholder="Your content..">{{Content}}</textarea> {{/IsTextContent}} </div> <input class="zs-button" type="submit" value="Submit"> </form> </article> |
Added box/constbox/home.zettel.
> > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 | === 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. |
Added box/constbox/info.mustache.
> > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 | <article> <header> <h1>Information for Zettel {{Zid}}</h1> <a href="{{{WebURL}}}">Web</a> · <a href="{{{ContextURL}}}">Context</a> {{#CanWrite}} · <a href="{{{EditURL}}}">Edit</a>{{/CanWrite}} {{#CanFolge}} · <a href="{{{FolgeURL}}}">Folge</a>{{/CanFolge}} {{#CanCopy}} · <a href="{{{CopyURL}}}">Copy</a>{{/CanCopy}} {{#CanRename}}· <a href="{{{RenameURL}}}">Rename</a>{{/CanRename}} {{#CanDelete}}· <a href="{{{DeleteURL}}}">Delete</a>{{/CanDelete}} </header> <h2>Interpreted Metadata</h2> <table>{{#MetaData}}<tr><td>{{Key}}</td><td>{{{Value}}}</td></tr>{{/MetaData}}</table> {{#HasLinks}} <h2>References</h2> {{#HasLocLinks}} <h3>Local</h3> <ul> {{#LocLinks}} {{#Valid}}<li><a href="{{{Zid}}}">{{Zid}}</a></li>{{/Valid}} {{^Valid}}<li>{{Zid}}</li>{{/Valid}} {{/LocLinks}} </ul> {{/HasLocLinks}} {{#HasExtLinks}} <h3>External</h3> <ul> {{#ExtLinks}} <li><a href="{{{.}}}"{{{ExtNewWindow}}}>{{.}}</a></li> {{/ExtLinks}} </ul> {{/HasExtLinks}} {{/HasLinks}} <h2>Parts and format</h3> <table> {{#Matrix}} <tr> {{#Elements}}{{#HasURL}}<td><a href="{{{URL}}}">{{Text}}</td>{{/HasURL}}{{^HasURL}}<th>{{Text}}</th>{{/HasURL}} {{/Elements}} </tr> {{/Matrix}} </table> {{#HasShadowLinks}} <h2>Shadowed Boxes</h2> <ul>{{#ShadowLinks}}<li>{{.}}</li>{{/ShadowLinks}}</ul> {{/HasShadowLinks}} {{#Endnotes}}{{{Endnotes}}}{{/Endnotes}} </article> |
Added box/constbox/license.txt.
> > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 | 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. |
Added box/constbox/listroles.mustache.
> > > > > > > > | 1 2 3 4 5 6 7 8 | <nav> <header> <h1>Currently used roles</h1> </header> <ul> {{#Roles}}<li><a href="{{{URL}}}">{{Text}}</a></li> {{/Roles}}</ul> </nav> |
Added box/constbox/listtags.mustache.
> > > > > > > > > > | 1 2 3 4 5 6 7 8 9 10 | <nav> <header> <h1>Currently used tags</h1> <div class="zs-meta"> <a href="{{{ListTagsURL}}}">All</a>{{#MinCounts}}, <a href="{{{URL}}}">{{Count}}</a>{{/MinCounts}} </div> </header> {{#Tags}} <a href="{{{URL}}}" style="font-size:{{Size}}%">{{Name}}</a><sup>{{Count}}</sup> {{/Tags}} </nav> |
Added box/constbox/listzettel.mustache.
> > > > > > | 1 2 3 4 5 6 | <header> <h1>{{Title}}</h1> </header> <ul> {{#Metas}}<li><a href="{{{URL}}}">{{{Text}}}</a></li> {{/Metas}}</ul> |
Added box/constbox/login.mustache.
> > > > > > > > > > > > > > > > > > > | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 | <article> <header> <h1>{{Title}}</h1> </header> {{#Retry}} <div class="zs-indication zs-error">Wrong user name / password. Try again.</div> {{/Retry}} <form method="POST" action="?_format=html"> <div> <label for="username">User name</label> <input class="zs-input" type="text" id="username" name="username" placeholder="Your user name.." autofocus> </div> <div> <label for="password">Password</label> <input class="zs-input" type="password" id="password" name="password" placeholder="Your password.."> </div> <input class="zs-button" type="submit" value="Login"> </form> </article> |
Added box/constbox/newtoc.zettel.
> > > > | 1 2 3 4 | This zettel lists all zettel that should act as a template for new zettel. These zettel will be included in the ""New"" menu of the WebUI. * [[New Zettel|00000000090001]] * [[New User|00000000090002]] |
Added box/constbox/rename.mustache.
> > > > > > > > > > > > > > > > > > > | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 | <article> <header> <h1>Rename Zettel {{.Zid}}</h1> </header> <p>Do you really want to rename this zettel?</p> <form method="POST"> <div> <label for="newid">New zettel id</label> <input class="zs-input" type="text" id="newzid" name="newzid" placeholder="ZID.." value="{{Zid}}" autofocus> </div> <input type="hidden" id="curzid" name="curzid" value="{{Zid}}"> <input class="zs-button" type="submit" value="Rename"> </form> <dl> {{#MetaPairs}} <dt>{{Key}}:</dt><dd>{{Value}}</dd> {{/MetaPairs}} </dl> </article> |
Added box/constbox/zettel.mustache.
> > > > > > > > > > > > > > > > > > > > > > > > > > > > | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 | <article> <header> <h1>{{{HTMLTitle}}}</h1> <div class="zs-meta"> {{#CanWrite}}<a href="{{{EditURL}}}">Edit</a> ·{{/CanWrite}} {{Zid}} · <a href="{{{InfoURL}}}">Info</a> · (<a href="{{{RoleURL}}}">{{RoleText}}</a>) {{#HasTags}}· {{#Tags}} <a href="{{{URL}}}">{{Text}}</a>{{/Tags}}{{/HasTags}} {{#CanCopy}}· <a href="{{{CopyURL}}}">Copy</a>{{/CanCopy}} {{#CanFolge}}· <a href="{{{FolgeURL}}}">Folge</a>{{/CanFolge}} {{#FolgeRefs}}<br>Folge: {{{FolgeRefs}}}{{/FolgeRefs}} {{#PrecursorRefs}}<br>Precursor: {{{PrecursorRefs}}}{{/PrecursorRefs}} {{#HasExtURL}}<br>URL: <a href="{{{ExtURL}}}"{{{ExtNewWindow}}}>{{ExtURL}}</a>{{/HasExtURL}} </div> </header> {{{Content}}} {{#HasBackLinks}} <details> <summary>Additional links to this zettel</summary> <ul> {{#BackLinks}} <li><a href="{{{URL}}}">{{Text}}</a></li> {{/BackLinks}} </ul> </details> {{/HasBackLinks}} </article> |
Added box/dirbox/dirbox.go.
> > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 326 327 328 329 330 331 332 333 334 335 336 337 338 339 340 341 342 343 344 345 346 347 348 349 350 351 352 353 354 355 356 357 358 359 360 361 362 363 364 365 366 367 368 369 370 371 372 373 374 375 376 377 378 379 380 381 382 383 384 385 386 387 388 389 390 391 392 393 394 395 396 397 398 399 400 401 402 403 404 405 406 407 408 409 410 411 412 413 414 415 416 417 418 419 420 | //----------------------------------------------------------------------------- // 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 } |
Added box/dirbox/directory/directory.go.
> > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 | //----------------------------------------------------------------------------- // Copyright (c) 2020-2021 Detlef Stern // // This file is part of zettelstore. // // Zettelstore is licensed under the latest version of the EUPL (European Union // Public License). Please see file LICENSE.txt for your rights and obligations // under this license. //----------------------------------------------------------------------------- // Package directory manages the directory interface of a dirstore. package directory import "zettelstore.de/z/domain/id" // Service is the interface of a directory service. type Service interface { Start() error Stop() error NumEntries() (int, error) GetEntries() ([]*Entry, error) GetEntry(zid id.Zid) (*Entry, error) GetNew() (*Entry, error) UpdateEntry(entry *Entry) error RenameEntry(curEntry, newEntry *Entry) error DeleteEntry(zid id.Zid) error } // MetaSpec defines all possibilities where meta data can be stored. type MetaSpec int // Constants for MetaSpec const ( _ MetaSpec = iota MetaSpecNone // no meta information MetaSpecFile // meta information is in meta file MetaSpecHeader // meta information is in header ) // Entry stores everything for a directory entry. type Entry struct { Zid id.Zid MetaSpec MetaSpec // location of meta information MetaPath string // file path of meta information ContentPath string // file path of zettel content ContentExt string // (normalized) file extension of zettel content Duplicates bool // multiple content files } // IsValid checks whether the entry is valid. func (e *Entry) IsValid() bool { return e != nil && e.Zid.IsValid() } |
Added box/dirbox/makedir.go.
> > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 | //----------------------------------------------------------------------------- // Copyright (c) 2021 Detlef Stern // // This file is part of zettelstore. // // Zettelstore is licensed under the latest version of the EUPL (European Union // Public License). Please see file LICENSE.txt for your rights and obligations // under this license. //----------------------------------------------------------------------------- // Package dirbox provides a directory-based zettel box. package dirbox import ( "zettelstore.de/z/box/dirbox/notifydir" "zettelstore.de/z/box/dirbox/simpledir" "zettelstore.de/z/kernel" ) func getDirSrvInfo(dirType string) (directoryServiceSpec, int, int) { for count := 0; count < 2; count++ { switch dirType { case kernel.BoxDirTypeNotify: return dirSrvNotify, 7, 1499 case kernel.BoxDirTypeSimple: return dirSrvSimple, 1, 1 default: dirType = kernel.Main.GetConfig(kernel.BoxService, kernel.BoxDefaultDirType).(string) } } panic("unable to set default dir box type: " + dirType) } func (dp *dirBox) setupDirService() { switch dp.dirSrvSpec { case dirSrvSimple: dp.dirSrv = simpledir.NewService(dp.dir) dp.mustNotify = true default: dp.dirSrv = notifydir.NewService(dp.dir, dp.dirRescan, dp.cdata.Notify) dp.mustNotify = false } } |
Added box/dirbox/notifydir/notifydir.go.
> > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 | //----------------------------------------------------------------------------- // Copyright (c) 2020-2021 Detlef Stern // // This file is part of zettelstore. // // Zettelstore is licensed under the latest version of the EUPL (European Union // Public License). Please see file LICENSE.txt for your rights and obligations // under this license. //----------------------------------------------------------------------------- // Package notifydir manages the notified directory part of a dirstore. package notifydir import ( "time" "zettelstore.de/z/box" "zettelstore.de/z/box/dirbox/directory" "zettelstore.de/z/domain/id" ) // notifyService specifies a directory scan service. type notifyService struct { dirPath string rescanTime time.Duration done chan struct{} cmds chan dirCmd infos chan<- box.UpdateInfo } // NewService creates a new directory service. func NewService(directoryPath string, rescanTime time.Duration, chci chan<- box.UpdateInfo) directory.Service { srv := ¬ifyService{ dirPath: directoryPath, rescanTime: rescanTime, cmds: make(chan dirCmd), infos: chci, } return srv } // Start makes the directory service operational. func (srv *notifyService) Start() error { tick := make(chan struct{}) rawEvents := make(chan *fileEvent) events := make(chan *fileEvent) ready := make(chan int) go srv.directoryService(events, ready) go collectEvents(events, rawEvents) go watchDirectory(srv.dirPath, rawEvents, tick) if srv.done != nil { panic("src.done already set") } srv.done = make(chan struct{}) go ping(tick, srv.rescanTime, srv.done) <-ready return nil } // Stop stops the directory service. func (srv *notifyService) Stop() error { close(srv.done) srv.done = nil return nil } func (srv *notifyService) notifyChange(reason box.UpdateReason, zid id.Zid) { if chci := srv.infos; chci != nil { chci <- box.UpdateInfo{Reason: reason, Zid: zid} } } // NumEntries returns the number of managed zettel. func (srv *notifyService) NumEntries() (int, error) { resChan := make(chan resNumEntries) srv.cmds <- &cmdNumEntries{resChan} return <-resChan, nil } // GetEntries returns an unsorted list of all current directory entries. func (srv *notifyService) GetEntries() ([]*directory.Entry, error) { resChan := make(chan resGetEntries) srv.cmds <- &cmdGetEntries{resChan} return <-resChan, nil } // GetEntry returns the entry with the specified zettel id. If there is no such // zettel id, an empty entry is returned. func (srv *notifyService) GetEntry(zid id.Zid) (*directory.Entry, error) { resChan := make(chan resGetEntry) srv.cmds <- &cmdGetEntry{zid, resChan} return <-resChan, nil } // GetNew returns an entry with a new zettel id. func (srv *notifyService) GetNew() (*directory.Entry, error) { resChan := make(chan resNewEntry) srv.cmds <- &cmdNewEntry{resChan} result := <-resChan return result.entry, result.err } // UpdateEntry notifies the directory of an updated entry. func (srv *notifyService) UpdateEntry(entry *directory.Entry) error { resChan := make(chan struct{}) srv.cmds <- &cmdUpdateEntry{entry, resChan} <-resChan return nil } // RenameEntry notifies the directory of an renamed entry. func (srv *notifyService) RenameEntry(curEntry, newEntry *directory.Entry) error { resChan := make(chan resRenameEntry) srv.cmds <- &cmdRenameEntry{curEntry, newEntry, resChan} return <-resChan } // DeleteEntry removes a zettel id from the directory of entries. func (srv *notifyService) DeleteEntry(zid id.Zid) error { resChan := make(chan struct{}) srv.cmds <- &cmdDeleteEntry{zid, resChan} <-resChan return nil } |
Added box/dirbox/notifydir/service.go.
> > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 | //----------------------------------------------------------------------------- // Copyright (c) 2020-2021 Detlef Stern // // This file is part of zettelstore. // // Zettelstore is licensed under the latest version of the EUPL (European Union // Public License). Please see file LICENSE.txt for your rights and obligations // under this license. //----------------------------------------------------------------------------- // Package notifydir manages the notified directory part of a dirstore. package notifydir import ( "log" "time" "zettelstore.de/z/box" "zettelstore.de/z/box/dirbox/directory" "zettelstore.de/z/domain/id" ) // ping sends every tick a signal to reload the directory list func ping(tick chan<- struct{}, rescanTime time.Duration, done <-chan struct{}) { ticker := time.NewTicker(rescanTime) defer close(tick) for { select { case _, ok := <-ticker.C: if !ok { return } tick <- struct{}{} case _, ok := <-done: if !ok { ticker.Stop() return } } } } func newEntry(ev *fileEvent) *directory.Entry { de := new(directory.Entry) de.Zid = ev.zid updateEntry(de, ev) return de } func updateEntry(de *directory.Entry, ev *fileEvent) { if ev.ext == "meta" { de.MetaSpec = directory.MetaSpecFile de.MetaPath = ev.path return } if de.ContentExt != "" && de.ContentExt != ev.ext { de.Duplicates = true return } if de.MetaSpec != directory.MetaSpecFile { if ev.ext == "zettel" { de.MetaSpec = directory.MetaSpecHeader } else { de.MetaSpec = directory.MetaSpecNone } } de.ContentPath = ev.path de.ContentExt = ev.ext } type dirMap map[id.Zid]*directory.Entry func dirMapUpdate(dm dirMap, ev *fileEvent) { de := dm[ev.zid] if de == nil { dm[ev.zid] = newEntry(ev) return } updateEntry(de, ev) } func deleteFromMap(dm dirMap, ev *fileEvent) { if ev.ext == "meta" { if entry, ok := dm[ev.zid]; ok { if entry.MetaSpec == directory.MetaSpecFile { entry.MetaSpec = directory.MetaSpecNone return } } } delete(dm, ev.zid) } // directoryService is the main service. func (srv *notifyService) directoryService(events <-chan *fileEvent, ready chan<- int) { curMap := make(dirMap) var newMap dirMap for { select { case ev, ok := <-events: if !ok { return } switch ev.status { case fileStatusReloadStart: newMap = make(dirMap) case fileStatusReloadEnd: curMap = newMap newMap = nil if ready != nil { ready <- len(curMap) close(ready) ready = nil } srv.notifyChange(box.OnReload, id.Invalid) case fileStatusError: log.Println("DIRBOX", "ERROR", ev.err) case fileStatusUpdate: srv.processFileUpdateEvent(ev, curMap, newMap) case fileStatusDelete: srv.processFileDeleteEvent(ev, curMap, newMap) } case cmd, ok := <-srv.cmds: if ok { cmd.run(curMap) } } } } func (srv *notifyService) processFileUpdateEvent(ev *fileEvent, curMap, newMap dirMap) { if newMap != nil { dirMapUpdate(newMap, ev) } else { dirMapUpdate(curMap, ev) srv.notifyChange(box.OnUpdate, ev.zid) } } func (srv *notifyService) processFileDeleteEvent(ev *fileEvent, curMap, newMap dirMap) { if newMap != nil { deleteFromMap(newMap, ev) } else { deleteFromMap(curMap, ev) srv.notifyChange(box.OnDelete, ev.zid) } } type dirCmd interface { run(m dirMap) } type cmdNumEntries struct { result chan<- resNumEntries } type resNumEntries = int func (cmd *cmdNumEntries) run(m dirMap) { cmd.result <- len(m) } type cmdGetEntries struct { result chan<- resGetEntries } type resGetEntries []*directory.Entry func (cmd *cmdGetEntries) run(m dirMap) { res := make([]*directory.Entry, len(m)) i := 0 for _, de := range m { entry := *de res[i] = &entry i++ } cmd.result <- res } type cmdGetEntry struct { zid id.Zid result chan<- resGetEntry } type resGetEntry = *directory.Entry func (cmd *cmdGetEntry) run(m dirMap) { entry := m[cmd.zid] if entry == nil { cmd.result <- nil } else { result := *entry cmd.result <- &result } } type cmdNewEntry struct { result chan<- resNewEntry } type resNewEntry struct { entry *directory.Entry err error } func (cmd *cmdNewEntry) run(m dirMap) { zid, err := box.GetNewZid(func(zid id.Zid) (bool, error) { _, ok := m[zid] return !ok, nil }) if err != nil { cmd.result <- resNewEntry{nil, err} return } entry := &directory.Entry{Zid: zid} m[zid] = entry cmd.result <- resNewEntry{&directory.Entry{Zid: zid}, nil} } type cmdUpdateEntry struct { entry *directory.Entry result chan<- struct{} } func (cmd *cmdUpdateEntry) run(m dirMap) { entry := *cmd.entry m[entry.Zid] = &entry cmd.result <- struct{}{} } type cmdRenameEntry struct { curEntry *directory.Entry newEntry *directory.Entry result chan<- resRenameEntry } type resRenameEntry = error func (cmd *cmdRenameEntry) run(m dirMap) { newEntry := *cmd.newEntry newZid := newEntry.Zid if _, found := m[newZid]; found { cmd.result <- &box.ErrInvalidID{Zid: newZid} return } delete(m, cmd.curEntry.Zid) m[newZid] = &newEntry cmd.result <- nil } type cmdDeleteEntry struct { zid id.Zid result chan<- struct{} } func (cmd *cmdDeleteEntry) run(m dirMap) { delete(m, cmd.zid) cmd.result <- struct{}{} } |
Added box/dirbox/notifydir/watch.go.
> > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 | //----------------------------------------------------------------------------- // Copyright (c) 2020-2021 Detlef Stern // // This file is part of zettelstore. // // Zettelstore is licensed under the latest version of the EUPL (European Union // Public License). Please see file LICENSE.txt for your rights and obligations // under this license. //----------------------------------------------------------------------------- // Package notifydir manages the notified directory part of a dirstore. package notifydir import ( "os" "path/filepath" "regexp" "time" "github.com/fsnotify/fsnotify" "zettelstore.de/z/domain/id" ) var validFileName = regexp.MustCompile(`^(\d{14}).*(\.(.+))$`) func matchValidFileName(name string) []string { return validFileName.FindStringSubmatch(name) } type fileStatus int const ( fileStatusNone fileStatus = iota fileStatusReloadStart fileStatusReloadEnd fileStatusError fileStatusUpdate fileStatusDelete ) type fileEvent struct { status fileStatus path string // Full file path zid id.Zid ext string // File extension err error // Error if Status == fileStatusError } type sendResult int const ( sendDone sendResult = iota sendReload sendExit ) func watchDirectory(directory string, events chan<- *fileEvent, tick <-chan struct{}) { defer close(events) var watcher *fsnotify.Watcher defer func() { if watcher != nil { watcher.Close() } }() sendEvent := func(ev *fileEvent) sendResult { select { case events <- ev: case _, ok := <-tick: if ok { return sendReload } return sendExit } return sendDone } sendError := func(err error) sendResult { return sendEvent(&fileEvent{status: fileStatusError, err: err}) } sendFileEvent := func(status fileStatus, path string, match []string) sendResult { zid, err := id.Parse(match[1]) if err != nil { return sendDone } event := &fileEvent{ status: status, path: path, zid: zid, ext: match[3], } return sendEvent(event) } reloadStartEvent := &fileEvent{status: fileStatusReloadStart} reloadEndEvent := &fileEvent{status: fileStatusReloadEnd} reloadFiles := func() bool { entries, err := os.ReadDir(directory) if err != nil { if res := sendError(err); res != sendDone { return res == sendReload } return true } if res := sendEvent(reloadStartEvent); res != sendDone { return res == sendReload } if watcher != nil { watcher.Close() } watcher, err = fsnotify.NewWatcher() if err != nil { if res := sendError(err); res != sendDone { return res == sendReload } } for _, entry := range entries { if entry.IsDir() { continue } if info, err1 := entry.Info(); err1 != nil || !info.Mode().IsRegular() { continue } name := entry.Name() match := matchValidFileName(name) if len(match) > 0 { path := filepath.Join(directory, name) if res := sendFileEvent(fileStatusUpdate, path, match); res != sendDone { return res == sendReload } } } if watcher != nil { err = watcher.Add(directory) if err != nil { if res := sendError(err); res != sendDone { return res == sendReload } } } if res := sendEvent(reloadEndEvent); res != sendDone { return res == sendReload } return true } handleEvents := func() bool { const createOps = fsnotify.Create | fsnotify.Write const deleteOps = fsnotify.Remove | fsnotify.Rename for { select { case wevent, ok := <-watcher.Events: if !ok { return false } path := filepath.Clean(wevent.Name) match := matchValidFileName(filepath.Base(path)) if len(match) == 0 { continue } if wevent.Op&createOps != 0 { if fi, err := os.Lstat(path); err != nil || !fi.Mode().IsRegular() { continue } if res := sendFileEvent( fileStatusUpdate, path, match); res != sendDone { return res == sendReload } } if wevent.Op&deleteOps != 0 { if res := sendFileEvent( fileStatusDelete, path, match); res != sendDone { return res == sendReload } } case err, ok := <-watcher.Errors: if !ok { return false } if res := sendError(err); res != sendDone { return res == sendReload } case _, ok := <-tick: return ok } } } for { if !reloadFiles() { return } if watcher == nil { if _, ok := <-tick; !ok { return } } else { if !handleEvents() { return } } } } func sendCollectedEvents(out chan<- *fileEvent, events []*fileEvent) { for _, ev := range events { if ev.status != fileStatusNone { out <- ev } } } func addEvent(events []*fileEvent, ev *fileEvent) []*fileEvent { switch ev.status { case fileStatusNone: return events case fileStatusReloadStart: events = events[0:0] case fileStatusUpdate, fileStatusDelete: if len(events) > 0 && mergeEvents(events, ev) { return events } } return append(events, ev) } func mergeEvents(events []*fileEvent, ev *fileEvent) bool { for i := len(events) - 1; i >= 0; i-- { oev := events[i] switch oev.status { case fileStatusReloadStart, fileStatusReloadEnd: return false case fileStatusUpdate, fileStatusDelete: if ev.path == oev.path { if ev.status == oev.status { return true } oev.status = fileStatusNone return false } } } return false } func collectEvents(out chan<- *fileEvent, in <-chan *fileEvent) { defer close(out) var sendTime time.Time sendTimeSet := false ticker := time.NewTicker(500 * time.Millisecond) defer ticker.Stop() events := make([]*fileEvent, 0, 32) buffer := false for { select { case ev, ok := <-in: if !ok { sendCollectedEvents(out, events) return } if ev.status == fileStatusReloadStart { buffer = false events = events[0:0] } if buffer { if !sendTimeSet { sendTime = time.Now().Add(1500 * time.Millisecond) sendTimeSet = true } events = addEvent(events, ev) if len(events) > 1024 { sendCollectedEvents(out, events) events = events[0:0] sendTimeSet = false } continue } out <- ev if ev.status == fileStatusReloadEnd { buffer = true } case now := <-ticker.C: if sendTimeSet && now.After(sendTime) { sendCollectedEvents(out, events) events = events[0:0] sendTimeSet = false } } } } |
Added box/dirbox/notifydir/watch_test.go.
> > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 | //----------------------------------------------------------------------------- // Copyright (c) 2020-2021 Detlef Stern // // This file is part of zettelstore. // // Zettelstore is licensed under the latest version of the EUPL (European Union // Public License). Please see file LICENSE.txt for your rights and obligations // under this license. //----------------------------------------------------------------------------- // Package notifydir manages the notified directory part of a dirstore. package notifydir import "testing" func sameStringSlices(sl1, sl2 []string) bool { if len(sl1) != len(sl2) { return false } for i := 0; i < len(sl1); i++ { if sl1[i] != sl2[i] { return false } } return true } func TestMatchValidFileName(t *testing.T) { t.Parallel() testcases := []struct { name string exp []string }{ {"", []string{}}, {".txt", []string{}}, {"12345678901234.txt", []string{"12345678901234", ".txt", "txt"}}, {"12345678901234abc.txt", []string{"12345678901234", ".txt", "txt"}}, {"12345678901234.abc.txt", []string{"12345678901234", ".txt", "txt"}}, } for i, tc := range testcases { got := matchValidFileName(tc.name) if len(got) == 0 { if len(tc.exp) > 0 { t.Errorf("TC=%d, name=%q, exp=%v, got=%v", i, tc.name, tc.exp, got) } } else { if got[0] != tc.name { t.Errorf("TC=%d, name=%q, got=%v", i, tc.name, got) } if !sameStringSlices(got[1:], tc.exp) { t.Errorf("TC=%d, name=%q, exp=%v, got=%v", i, tc.name, tc.exp, got) } } } } |
Added box/dirbox/service.go.
> > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 | //----------------------------------------------------------------------------- // 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 } |
Added box/dirbox/simpledir/simpledir.go.
> > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 | //----------------------------------------------------------------------------- // Copyright (c) 2021 Detlef Stern // // This file is part of zettelstore. // // Zettelstore is licensed under the latest version of the EUPL (European Union // Public License). Please see file LICENSE.txt for your rights and obligations // under this license. //----------------------------------------------------------------------------- // Package simpledir manages the directory part of a dirstore. package simpledir import ( "os" "path/filepath" "regexp" "sync" "zettelstore.de/z/box" "zettelstore.de/z/box/dirbox/directory" "zettelstore.de/z/domain/id" ) // simpleService specifies a directory service without scanning. type simpleService struct { dirPath string mx sync.Mutex } // NewService creates a new directory service. func NewService(directoryPath string) directory.Service { return &simpleService{ dirPath: directoryPath, } } func (ss *simpleService) Start() error { ss.mx.Lock() defer ss.mx.Unlock() _, err := os.ReadDir(ss.dirPath) return err } func (ss *simpleService) Stop() error { return nil } func (ss *simpleService) NumEntries() (int, error) { ss.mx.Lock() defer ss.mx.Unlock() entries, err := ss.getEntries() if err == nil { return len(entries), nil } return 0, err } func (ss *simpleService) GetEntries() ([]*directory.Entry, error) { ss.mx.Lock() defer ss.mx.Unlock() entrySet, err := ss.getEntries() if err != nil { return nil, err } result := make([]*directory.Entry, 0, len(entrySet)) for _, entry := range entrySet { result = append(result, entry) } return result, nil } func (ss *simpleService) getEntries() (map[id.Zid]*directory.Entry, error) { dirEntries, err := os.ReadDir(ss.dirPath) if err != nil { return nil, err } entrySet := make(map[id.Zid]*directory.Entry) for _, dirEntry := range dirEntries { if dirEntry.IsDir() { continue } if info, err1 := dirEntry.Info(); err1 != nil || !info.Mode().IsRegular() { continue } name := dirEntry.Name() match := matchValidFileName(name) if len(match) == 0 { continue } zid, err := id.Parse(match[1]) if err != nil { continue } var entry *directory.Entry if e, ok := entrySet[zid]; ok { entry = e } else { entry = &directory.Entry{Zid: zid} entrySet[zid] = entry } updateEntry(entry, filepath.Join(ss.dirPath, name), match[3]) } return entrySet, nil } var validFileName = regexp.MustCompile(`^(\d{14}).*(\.(.+))$`) func matchValidFileName(name string) []string { return validFileName.FindStringSubmatch(name) } func updateEntry(entry *directory.Entry, path, ext string) { if ext == "meta" { entry.MetaSpec = directory.MetaSpecFile entry.MetaPath = path } else if entry.ContentExt != "" && entry.ContentExt != ext { entry.Duplicates = true } else { if entry.MetaSpec != directory.MetaSpecFile { if ext == "zettel" { entry.MetaSpec = directory.MetaSpecHeader } else { entry.MetaSpec = directory.MetaSpecNone } } entry.ContentPath = path entry.ContentExt = ext } } func (ss *simpleService) GetEntry(zid id.Zid) (*directory.Entry, error) { ss.mx.Lock() defer ss.mx.Unlock() return ss.getEntry(zid) } func (ss *simpleService) getEntry(zid id.Zid) (*directory.Entry, error) { pattern := filepath.Join(ss.dirPath, zid.String()) + "*.*" paths, err := filepath.Glob(pattern) if err != nil { return nil, err } if len(paths) == 0 { return nil, nil } entry := &directory.Entry{Zid: zid} for _, path := range paths { ext := filepath.Ext(path) if len(ext) > 0 && ext[0] == '.' { ext = ext[1:] } updateEntry(entry, path, ext) } return entry, nil } func (ss *simpleService) GetNew() (*directory.Entry, error) { ss.mx.Lock() defer ss.mx.Unlock() zid, err := box.GetNewZid(func(zid id.Zid) (bool, error) { entry, err := ss.getEntry(zid) if err != nil { return false, nil } return !entry.IsValid(), nil }) if err != nil { return nil, err } return &directory.Entry{Zid: zid}, nil } func (ss *simpleService) UpdateEntry(entry *directory.Entry) error { // Nothing to to, since the actual file update is done by dirbox. return nil } func (ss *simpleService) RenameEntry(curEntry, newEntry *directory.Entry) error { // Nothing to to, since the actual file rename is done by dirbox. return nil } func (ss *simpleService) DeleteEntry(zid id.Zid) error { // Nothing to to, since the actual file delete is done by dirbox. return nil } |
Added box/filebox/filebox.go.
> > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 | //----------------------------------------------------------------------------- // 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) } } |
Added box/filebox/zipbox.go.
> > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 | //----------------------------------------------------------------------------- // 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 } |
Added box/helper.go.
> > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 | //----------------------------------------------------------------------------- // 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 } |
Added box/manager/anteroom.go.
> > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 | //----------------------------------------------------------------------------- // 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 } |
Added box/manager/anteroom_test.go.
> > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 | //----------------------------------------------------------------------------- // 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) } } |
Added box/manager/box.go.
> > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 | //----------------------------------------------------------------------------- // 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 } |
Added box/manager/collect.go.
> > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 | //----------------------------------------------------------------------------- // 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 } } |
Added box/manager/enrich.go.
> > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 | //----------------------------------------------------------------------------- // 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. } |
Added box/manager/indexer.go.
> > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 | //----------------------------------------------------------------------------- // 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) } } |
Added box/manager/manager.go.
> > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 | //----------------------------------------------------------------------------- // 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) } |
Added box/manager/memstore/memstore.go.
> > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 326 327 328 329 330 331 332 333 334 335 336 337 338 339 340 341 342 343 344 345 346 347 348 349 350 351 352 353 354 355 356 357 358 359 360 361 362 363 364 365 366 367 368 369 370 371 372 373 374 375 376 377 378 379 380 381 382 383 384 385 386 387 388 389 390 391 392 393 394 395 396 397 398 399 400 401 402 403 404 405 406 407 408 409 410 411 412 413 414 415 416 417 418 419 420 421 422 423 424 425 426 427 428 429 430 431 432 433 434 435 436 437 438 439 440 441 442 443 444 445 446 447 448 449 450 451 452 453 454 455 456 457 458 459 460 461 462 463 464 465 466 467 468 469 470 471 472 473 474 475 476 477 478 479 480 481 482 483 484 485 486 487 488 489 490 491 492 493 494 495 496 497 498 499 500 501 502 503 504 505 506 507 508 509 510 511 512 513 514 515 516 517 518 519 520 521 522 523 524 525 526 527 528 529 530 531 532 533 534 535 536 537 538 539 540 541 542 543 544 545 546 547 548 549 550 551 552 553 554 555 556 557 558 559 560 561 562 563 564 565 566 567 568 569 570 571 572 573 574 575 576 577 578 579 580 | //----------------------------------------------------------------------------- // Copyright (c) 2021 Detlef Stern // // This file is part of zettelstore. // // Zettelstore is licensed under the latest version of the EUPL (European Union // Public License). Please see file LICENSE.txt for your rights and obligations // under this license. //----------------------------------------------------------------------------- // Package memstore stored the index in main memory. package memstore import ( "context" "fmt" "io" "sort" "strings" "sync" "zettelstore.de/z/box/manager/store" "zettelstore.de/z/domain/id" "zettelstore.de/z/domain/meta" ) type metaRefs struct { forward id.Slice backward id.Slice } type zettelIndex struct { dead id.Slice forward id.Slice backward id.Slice meta map[string]metaRefs words []string urls []string } func (zi *zettelIndex) isEmpty() bool { if len(zi.forward) > 0 || len(zi.backward) > 0 || len(zi.dead) > 0 || len(zi.words) > 0 { return false } return zi.meta == nil || len(zi.meta) == 0 } type stringRefs map[string]id.Slice type memStore struct { mx sync.RWMutex idx map[id.Zid]*zettelIndex dead map[id.Zid]id.Slice // map dead refs where they occur words stringRefs urls stringRefs // Stats updates uint64 } // New returns a new memory-based index store. func New() store.Store { return &memStore{ idx: make(map[id.Zid]*zettelIndex), dead: make(map[id.Zid]id.Slice), words: make(stringRefs), urls: make(stringRefs), } } func (ms *memStore) Enrich(ctx context.Context, m *meta.Meta) { if ms.doEnrich(ctx, m) { ms.mx.Lock() ms.updates++ ms.mx.Unlock() } } func (ms *memStore) doEnrich(ctx context.Context, m *meta.Meta) bool { ms.mx.RLock() defer ms.mx.RUnlock() zi, ok := ms.idx[m.Zid] if !ok { return false } var updated bool if len(zi.dead) > 0 { m.Set(meta.KeyDead, zi.dead.String()) updated = true } back := removeOtherMetaRefs(m, zi.backward.Copy()) if len(zi.backward) > 0 { m.Set(meta.KeyBackward, zi.backward.String()) updated = true } if len(zi.forward) > 0 { m.Set(meta.KeyForward, zi.forward.String()) back = remRefs(back, zi.forward) updated = true } if len(zi.meta) > 0 { for k, refs := range zi.meta { if len(refs.backward) > 0 { m.Set(k, refs.backward.String()) back = remRefs(back, refs.backward) updated = true } } } if len(back) > 0 { m.Set(meta.KeyBack, back.String()) updated = true } return updated } // SearchEqual returns all zettel that contains the given exact word. // The word must be normalized through Unicode NKFD, trimmed and not empty. func (ms *memStore) SearchEqual(word string) id.Set { ms.mx.RLock() defer ms.mx.RUnlock() result := id.NewSet() if refs, ok := ms.words[word]; ok { result.AddSlice(refs) } if refs, ok := ms.urls[word]; ok { result.AddSlice(refs) } zid, err := id.Parse(word) if err != nil { return result } zi, ok := ms.idx[zid] if !ok { return result } addBackwardZids(result, zid, zi) return result } // SearchPrefix returns all zettel that have a word with the given prefix. // The prefix must be normalized through Unicode NKFD, trimmed and not empty. func (ms *memStore) SearchPrefix(prefix string) id.Set { ms.mx.RLock() defer ms.mx.RUnlock() result := ms.selectWithPred(prefix, strings.HasPrefix) l := len(prefix) if l > 14 { return result } minZid, err := id.Parse(prefix + "00000000000000"[:14-l]) if err != nil { return result } maxZid, err := id.Parse(prefix + "99999999999999"[:14-l]) if err != nil { return result } for zid, zi := range ms.idx { if minZid <= zid && zid <= maxZid { addBackwardZids(result, zid, zi) } } return result } // SearchSuffix returns all zettel that have a word with the given suffix. // The suffix must be normalized through Unicode NKFD, trimmed and not empty. func (ms *memStore) SearchSuffix(suffix string) id.Set { ms.mx.RLock() defer ms.mx.RUnlock() result := ms.selectWithPred(suffix, strings.HasSuffix) l := len(suffix) if l > 14 { return result } val, err := id.ParseUint(suffix) if err != nil { return result } modulo := uint64(1) for i := 0; i < l; i++ { modulo *= 10 } for zid, zi := range ms.idx { if uint64(zid)%modulo == val { addBackwardZids(result, zid, zi) } } return result } // SearchContains returns all zettel that contains the given string. // The string must be normalized through Unicode NKFD, trimmed and not empty. func (ms *memStore) SearchContains(s string) id.Set { ms.mx.RLock() defer ms.mx.RUnlock() result := ms.selectWithPred(s, strings.Contains) if len(s) > 14 { return result } if _, err := id.ParseUint(s); err != nil { return result } for zid, zi := range ms.idx { if strings.Contains(zid.String(), s) { addBackwardZids(result, zid, zi) } } return result } func (ms *memStore) selectWithPred(s string, pred func(string, string) bool) id.Set { // Must only be called if ms.mx is read-locked! result := id.NewSet() for word, refs := range ms.words { if !pred(word, s) { continue } result.AddSlice(refs) } for u, refs := range ms.urls { if !pred(u, s) { continue } result.AddSlice(refs) } return result } func addBackwardZids(result id.Set, zid id.Zid, zi *zettelIndex) { // Must only be called if ms.mx is read-locked! result[zid] = true result.AddSlice(zi.backward) for _, mref := range zi.meta { result.AddSlice(mref.backward) } } func removeOtherMetaRefs(m *meta.Meta, back id.Slice) id.Slice { for _, p := range m.PairsRest(false) { switch meta.Type(p.Key) { case meta.TypeID: if zid, err := id.Parse(p.Value); err == nil { back = remRef(back, zid) } case meta.TypeIDSet: for _, val := range meta.ListFromValue(p.Value) { if zid, err := id.Parse(val); err == nil { back = remRef(back, zid) } } } } return back } func (ms *memStore) UpdateReferences(ctx context.Context, zidx *store.ZettelIndex) id.Set { ms.mx.Lock() defer ms.mx.Unlock() zi, ziExist := ms.idx[zidx.Zid] if !ziExist || zi == nil { zi = &zettelIndex{} ziExist = false } // Is this zettel an old dead reference mentioned in other zettel? var toCheck id.Set if refs, ok := ms.dead[zidx.Zid]; ok { // These must be checked later again toCheck = id.NewSet(refs...) delete(ms.dead, zidx.Zid) } ms.updateDeadReferences(zidx, zi) ms.updateForwardBackwardReferences(zidx, zi) ms.updateMetadataReferences(zidx, zi) zi.words = updateWordSet(zidx.Zid, ms.words, zi.words, zidx.GetWords()) zi.urls = updateWordSet(zidx.Zid, ms.urls, zi.urls, zidx.GetUrls()) // Check if zi must be inserted into ms.idx if !ziExist && !zi.isEmpty() { ms.idx[zidx.Zid] = zi } return toCheck } func (ms *memStore) updateDeadReferences(zidx *store.ZettelIndex, zi *zettelIndex) { // Must only be called if ms.mx is write-locked! drefs := zidx.GetDeadRefs() newRefs, remRefs := refsDiff(drefs, zi.dead) zi.dead = drefs for _, ref := range remRefs { ms.dead[ref] = remRef(ms.dead[ref], zidx.Zid) } for _, ref := range newRefs { ms.dead[ref] = addRef(ms.dead[ref], zidx.Zid) } } func (ms *memStore) updateForwardBackwardReferences(zidx *store.ZettelIndex, zi *zettelIndex) { // Must only be called if ms.mx is write-locked! brefs := zidx.GetBackRefs() newRefs, remRefs := refsDiff(brefs, zi.forward) zi.forward = brefs for _, ref := range remRefs { bzi := ms.getEntry(ref) bzi.backward = remRef(bzi.backward, zidx.Zid) } for _, ref := range newRefs { bzi := ms.getEntry(ref) bzi.backward = addRef(bzi.backward, zidx.Zid) } } func (ms *memStore) updateMetadataReferences(zidx *store.ZettelIndex, zi *zettelIndex) { // Must only be called if ms.mx is write-locked! metarefs := zidx.GetMetaRefs() for key, mr := range zi.meta { if _, ok := metarefs[key]; ok { continue } ms.removeInverseMeta(zidx.Zid, key, mr.forward) } if zi.meta == nil { zi.meta = make(map[string]metaRefs) } for key, mrefs := range metarefs { mr := zi.meta[key] newRefs, remRefs := refsDiff(mrefs, mr.forward) mr.forward = mrefs zi.meta[key] = mr for _, ref := range newRefs { bzi := ms.getEntry(ref) if bzi.meta == nil { bzi.meta = make(map[string]metaRefs) } bmr := bzi.meta[key] bmr.backward = addRef(bmr.backward, zidx.Zid) bzi.meta[key] = bmr } ms.removeInverseMeta(zidx.Zid, key, remRefs) } } func updateWordSet(zid id.Zid, srefs stringRefs, prev []string, next store.WordSet) []string { // Must only be called if ms.mx is write-locked! newWords, removeWords := next.Diff(prev) for _, word := range newWords { if refs, ok := srefs[word]; ok { srefs[word] = addRef(refs, zid) continue } srefs[word] = id.Slice{zid} } for _, word := range removeWords { refs, ok := srefs[word] if !ok { continue } refs2 := remRef(refs, zid) if len(refs2) == 0 { delete(srefs, word) continue } srefs[word] = refs2 } return next.Words() } func (ms *memStore) getEntry(zid id.Zid) *zettelIndex { // Must only be called if ms.mx is write-locked! if zi, ok := ms.idx[zid]; ok { return zi } zi := &zettelIndex{} ms.idx[zid] = zi return zi } func (ms *memStore) DeleteZettel(ctx context.Context, zid id.Zid) id.Set { ms.mx.Lock() defer ms.mx.Unlock() zi, ok := ms.idx[zid] if !ok { return nil } ms.deleteDeadSources(zid, zi) toCheck := ms.deleteForwardBackward(zid, zi) if len(zi.meta) > 0 { for key, mrefs := range zi.meta { ms.removeInverseMeta(zid, key, mrefs.forward) } } ms.deleteWords(zid, zi.words) delete(ms.idx, zid) return toCheck } func (ms *memStore) deleteDeadSources(zid id.Zid, zi *zettelIndex) { // Must only be called if ms.mx is write-locked! for _, ref := range zi.dead { if drefs, ok := ms.dead[ref]; ok { drefs = remRef(drefs, zid) if len(drefs) > 0 { ms.dead[ref] = drefs } else { delete(ms.dead, ref) } } } } func (ms *memStore) deleteForwardBackward(zid id.Zid, zi *zettelIndex) id.Set { // Must only be called if ms.mx is write-locked! var toCheck id.Set for _, ref := range zi.forward { if fzi, ok := ms.idx[ref]; ok { fzi.backward = remRef(fzi.backward, zid) } } for _, ref := range zi.backward { if bzi, ok := ms.idx[ref]; ok { bzi.forward = remRef(bzi.forward, zid) if toCheck == nil { toCheck = id.NewSet() } toCheck[ref] = true } } return toCheck } func (ms *memStore) removeInverseMeta(zid id.Zid, key string, forward id.Slice) { // Must only be called if ms.mx is write-locked! for _, ref := range forward { bzi, ok := ms.idx[ref] if !ok || bzi.meta == nil { continue } bmr, ok := bzi.meta[key] if !ok { continue } bmr.backward = remRef(bmr.backward, zid) if len(bmr.backward) > 0 || len(bmr.forward) > 0 { bzi.meta[key] = bmr } else { delete(bzi.meta, key) if len(bzi.meta) == 0 { bzi.meta = nil } } } } func (ms *memStore) deleteWords(zid id.Zid, words []string) { // Must only be called if ms.mx is write-locked! for _, word := range words { refs, ok := ms.words[word] if !ok { continue } refs2 := remRef(refs, zid) if len(refs2) == 0 { delete(ms.words, word) continue } ms.words[word] = refs2 } } func (ms *memStore) ReadStats(st *store.Stats) { ms.mx.RLock() st.Zettel = len(ms.idx) st.Updates = ms.updates st.Words = uint64(len(ms.words)) st.Urls = uint64(len(ms.urls)) ms.mx.RUnlock() } func (ms *memStore) Dump(w io.Writer) { ms.mx.RLock() defer ms.mx.RUnlock() io.WriteString(w, "=== Dump\n") ms.dumpIndex(w) ms.dumpDead(w) dumpStringRefs(w, "Words", "", "", ms.words) dumpStringRefs(w, "URLs", "[[", "]]", ms.urls) } func (ms *memStore) dumpIndex(w io.Writer) { if len(ms.idx) == 0 { return } io.WriteString(w, "==== Zettel Index\n") zids := make(id.Slice, 0, len(ms.idx)) for id := range ms.idx { zids = append(zids, id) } zids.Sort() for _, id := range zids { fmt.Fprintln(w, "=====", id) zi := ms.idx[id] if len(zi.dead) > 0 { fmt.Fprintln(w, "* Dead:", zi.dead) } dumpZids(w, "* Forward:", zi.forward) dumpZids(w, "* Backward:", zi.backward) for k, fb := range zi.meta { fmt.Fprintln(w, "* Meta", k) dumpZids(w, "** Forward:", fb.forward) dumpZids(w, "** Backward:", fb.backward) } dumpStrings(w, "* Words", "", "", zi.words) dumpStrings(w, "* URLs", "[[", "]]", zi.urls) } } func (ms *memStore) dumpDead(w io.Writer) { if len(ms.dead) == 0 { return } fmt.Fprintf(w, "==== Dead References\n") zids := make(id.Slice, 0, len(ms.dead)) for id := range ms.dead { zids = append(zids, id) } zids.Sort() for _, id := range zids { fmt.Fprintln(w, ";", id) fmt.Fprintln(w, ":", ms.dead[id]) } } func dumpZids(w io.Writer, prefix string, zids id.Slice) { if len(zids) > 0 { io.WriteString(w, prefix) for _, zid := range zids { io.WriteString(w, " ") w.Write(zid.Bytes()) } fmt.Fprintln(w) } } func dumpStrings(w io.Writer, title, preString, postString string, slice []string) { if len(slice) > 0 { sl := make([]string, len(slice)) copy(sl, slice) sort.Strings(sl) fmt.Fprintln(w, title) for _, s := range sl { fmt.Fprintf(w, "** %s%s%s\n", preString, s, postString) } } } func dumpStringRefs(w io.Writer, title, preString, postString string, srefs stringRefs) { if len(srefs) == 0 { return } fmt.Fprintln(w, "====", title) slice := make([]string, 0, len(srefs)) for s := range srefs { slice = append(slice, s) } sort.Strings(slice) for _, s := range slice { fmt.Fprintf(w, "; %s%s%s\n", preString, s, postString) fmt.Fprintln(w, ":", srefs[s]) } } |
Added box/manager/memstore/refs.go.
> > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 | //----------------------------------------------------------------------------- // Copyright (c) 2021 Detlef Stern // // This file is part of zettelstore. // // Zettelstore is licensed under the latest version of the EUPL (European Union // Public License). Please see file LICENSE.txt for your rights and obligations // under this license. //----------------------------------------------------------------------------- // Package memstore stored the index in main memory. package memstore import "zettelstore.de/z/domain/id" func refsDiff(refsN, refsO id.Slice) (newRefs, remRefs id.Slice) { npos, opos := 0, 0 for npos < len(refsN) && opos < len(refsO) { rn, ro := refsN[npos], refsO[opos] if rn == ro { npos++ opos++ continue } if rn < ro { newRefs = append(newRefs, rn) npos++ continue } remRefs = append(remRefs, ro) opos++ } if npos < len(refsN) { newRefs = append(newRefs, refsN[npos:]...) } if opos < len(refsO) { remRefs = append(remRefs, refsO[opos:]...) } return newRefs, remRefs } func addRef(refs id.Slice, ref id.Zid) id.Slice { hi := len(refs) for lo := 0; lo < hi; { m := lo + (hi-lo)/2 if r := refs[m]; r == ref { return refs } else if r < ref { lo = m + 1 } else { hi = m } } refs = append(refs, id.Invalid) copy(refs[hi+1:], refs[hi:]) refs[hi] = ref return refs } func remRefs(refs, rem id.Slice) id.Slice { if len(refs) == 0 || len(rem) == 0 { return refs } result := make(id.Slice, 0, len(refs)) rpos, dpos := 0, 0 for rpos < len(refs) && dpos < len(rem) { rr, dr := refs[rpos], rem[dpos] if rr < dr { result = append(result, rr) rpos++ continue } if dr < rr { dpos++ continue } rpos++ dpos++ } if rpos < len(refs) { result = append(result, refs[rpos:]...) } return result } func remRef(refs id.Slice, ref id.Zid) id.Slice { hi := len(refs) for lo := 0; lo < hi; { m := lo + (hi-lo)/2 if r := refs[m]; r == ref { copy(refs[m:], refs[m+1:]) refs = refs[:len(refs)-1] return refs } else if r < ref { lo = m + 1 } else { hi = m } } return refs } |
Added box/manager/memstore/refs_test.go.
> > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 | //----------------------------------------------------------------------------- // Copyright (c) 2021 Detlef Stern // // This file is part of zettelstore. // // Zettelstore is licensed under the latest version of the EUPL (European Union // Public License). Please see file LICENSE.txt for your rights and obligations // under this license. //----------------------------------------------------------------------------- // Package memstore stored the index in main memory. package memstore import ( "testing" "zettelstore.de/z/domain/id" ) func assertRefs(t *testing.T, i int, got, exp id.Slice) { t.Helper() if got == nil && exp != nil { t.Errorf("%d: got nil, but expected %v", i, exp) return } if got != nil && exp == nil { t.Errorf("%d: expected nil, but got %v", i, got) return } if len(got) != len(exp) { t.Errorf("%d: expected len(%v)==%d, but got len(%v)==%d", i, exp, len(exp), got, len(got)) return } for p, n := range exp { if got := got[p]; got != id.Zid(n) { t.Errorf("%d: pos %d: expected %d, but got %d", i, p, n, got) } } } func TestRefsDiff(t *testing.T) { t.Parallel() testcases := []struct { in1, in2 id.Slice exp1, exp2 id.Slice }{ {nil, nil, nil, nil}, {id.Slice{1}, nil, id.Slice{1}, nil}, {nil, id.Slice{1}, nil, id.Slice{1}}, {id.Slice{1}, id.Slice{1}, nil, nil}, {id.Slice{1, 2}, id.Slice{1}, id.Slice{2}, nil}, {id.Slice{1, 2}, id.Slice{1, 3}, id.Slice{2}, id.Slice{3}}, {id.Slice{1, 4}, id.Slice{1, 3}, id.Slice{4}, id.Slice{3}}, } for i, tc := range testcases { got1, got2 := refsDiff(tc.in1, tc.in2) assertRefs(t, i, got1, tc.exp1) assertRefs(t, i, got2, tc.exp2) } } func TestAddRef(t *testing.T) { t.Parallel() testcases := []struct { ref id.Slice zid uint exp id.Slice }{ {nil, 5, id.Slice{5}}, {id.Slice{1}, 5, id.Slice{1, 5}}, {id.Slice{10}, 5, id.Slice{5, 10}}, {id.Slice{5}, 5, id.Slice{5}}, {id.Slice{1, 10}, 5, id.Slice{1, 5, 10}}, {id.Slice{1, 5, 10}, 5, id.Slice{1, 5, 10}}, } for i, tc := range testcases { got := addRef(tc.ref, id.Zid(tc.zid)) assertRefs(t, i, got, tc.exp) } } func TestRemRefs(t *testing.T) { t.Parallel() testcases := []struct { in1, in2 id.Slice exp id.Slice }{ {nil, nil, nil}, {nil, id.Slice{}, nil}, {id.Slice{}, nil, id.Slice{}}, {id.Slice{}, id.Slice{}, id.Slice{}}, {id.Slice{1}, id.Slice{5}, id.Slice{1}}, {id.Slice{10}, id.Slice{5}, id.Slice{10}}, {id.Slice{1, 5}, id.Slice{5}, id.Slice{1}}, {id.Slice{5, 10}, id.Slice{5}, id.Slice{10}}, {id.Slice{1, 10}, id.Slice{5}, id.Slice{1, 10}}, {id.Slice{1}, id.Slice{2, 5}, id.Slice{1}}, {id.Slice{10}, id.Slice{2, 5}, id.Slice{10}}, {id.Slice{1, 5}, id.Slice{2, 5}, id.Slice{1}}, {id.Slice{5, 10}, id.Slice{2, 5}, id.Slice{10}}, {id.Slice{1, 2, 5}, id.Slice{2, 5}, id.Slice{1}}, {id.Slice{2, 5, 10}, id.Slice{2, 5}, id.Slice{10}}, {id.Slice{1, 10}, id.Slice{2, 5}, id.Slice{1, 10}}, {id.Slice{1}, id.Slice{5, 9}, id.Slice{1}}, {id.Slice{10}, id.Slice{5, 9}, id.Slice{10}}, {id.Slice{1, 5}, id.Slice{5, 9}, id.Slice{1}}, {id.Slice{5, 10}, id.Slice{5, 9}, id.Slice{10}}, {id.Slice{1, 5, 9}, id.Slice{5, 9}, id.Slice{1}}, {id.Slice{5, 9, 10}, id.Slice{5, 9}, id.Slice{10}}, {id.Slice{1, 10}, id.Slice{5, 9}, id.Slice{1, 10}}, } for i, tc := range testcases { got := remRefs(tc.in1, tc.in2) assertRefs(t, i, got, tc.exp) } } func TestRemRef(t *testing.T) { t.Parallel() testcases := []struct { ref id.Slice zid uint exp id.Slice }{ {nil, 5, nil}, {id.Slice{}, 5, id.Slice{}}, {id.Slice{5}, 5, id.Slice{}}, {id.Slice{1}, 5, id.Slice{1}}, {id.Slice{10}, 5, id.Slice{10}}, {id.Slice{1, 5}, 5, id.Slice{1}}, {id.Slice{5, 10}, 5, id.Slice{10}}, {id.Slice{1, 5, 10}, 5, id.Slice{1, 10}}, } for i, tc := range testcases { got := remRef(tc.ref, id.Zid(tc.zid)) assertRefs(t, i, got, tc.exp) } } |
Added box/manager/store/store.go.
> > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 | //----------------------------------------------------------------------------- // 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) } |
Added box/manager/store/wordset.go.
> > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 | //----------------------------------------------------------------------------- // 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 } |
Added box/manager/store/wordset_test.go.
> > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 | //----------------------------------------------------------------------------- // 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) } } } |
Added box/manager/store/zettel.go.
> > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 | //----------------------------------------------------------------------------- // 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 } |
Added box/membox/membox.go.
> > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 | //----------------------------------------------------------------------------- // 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() } |
Added box/merge.go.
> > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 | //----------------------------------------------------------------------------- // Copyright (c) 2020-2021 Detlef Stern // // This file is part of zettelstore. // // Zettelstore is licensed under the latest version of the EUPL (European Union // Public License). Please see file LICENSE.txt for your rights and obligations // under this license. //----------------------------------------------------------------------------- // Package box provides a generic interface to zettel boxes. package box import "zettelstore.de/z/domain/meta" // MergeSorted returns a merged sequence of metadata, sorted by Zid. // The lists first and second must be sorted descending by Zid. func MergeSorted(first, second []*meta.Meta) []*meta.Meta { lenFirst := len(first) lenSecond := len(second) result := make([]*meta.Meta, 0, lenFirst+lenSecond) iFirst := 0 iSecond := 0 for iFirst < lenFirst && iSecond < lenSecond { zidFirst := first[iFirst].Zid zidSecond := second[iSecond].Zid if zidFirst > zidSecond { result = append(result, first[iFirst]) iFirst++ } else if zidFirst < zidSecond { result = append(result, second[iSecond]) iSecond++ } else { // zidFirst == zidSecond result = append(result, first[iFirst]) iFirst++ iSecond++ } } if iFirst < lenFirst { result = append(result, first[iFirst:]...) } else { result = append(result, second[iSecond:]...) } return result } |
Added client/client.go.
> > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 326 327 328 329 330 331 332 333 334 335 336 337 338 339 340 341 342 343 344 345 346 347 348 349 350 351 352 353 354 355 356 357 358 359 360 361 362 363 364 365 366 367 368 369 370 371 372 373 374 375 376 377 378 379 380 381 382 383 384 385 386 387 388 389 390 391 392 393 394 395 396 397 398 399 400 401 402 403 404 405 406 407 408 409 410 411 412 413 414 415 416 417 418 419 420 421 422 423 424 425 426 427 428 429 430 431 432 433 434 435 436 437 438 439 440 441 442 443 | //----------------------------------------------------------------------------- // Copyright (c) 2021 Detlef Stern // // This file is part of zettelstore. // // Zettelstore is licensed under the latest version of the EUPL (European Union // Public License). Please see file LICENSE.txt for your rights and obligations // under this license. //----------------------------------------------------------------------------- // Package client provides a client for accessing the Zettelstore via its API. package client import ( "bytes" "context" "encoding/json" "errors" "io" "net/http" "net/url" "strconv" "strings" "time" "zettelstore.de/z/api" "zettelstore.de/z/domain/id" ) // Client contains all data to execute requests. type Client struct { baseURL string username string password string token string tokenType string expires time.Time } // NewClient create a new client. func NewClient(baseURL string) *Client { if !strings.HasSuffix(baseURL, "/") { baseURL += "/" } c := Client{baseURL: baseURL} return &c } func (c *Client) newURLBuilder(key byte) *api.URLBuilder { return api.NewURLBuilder(c.baseURL, key) } func (c *Client) newRequest(ctx context.Context, method string, ub *api.URLBuilder, body io.Reader) (*http.Request, error) { return http.NewRequestWithContext(ctx, method, ub.String(), body) } func (c *Client) executeRequest(req *http.Request) (*http.Response, error) { if c.token != "" { req.Header.Add("Authorization", c.tokenType+" "+c.token) } client := http.Client{} resp, err := client.Do(req) if err != nil { if resp != nil && resp.Body != nil { resp.Body.Close() } return nil, err } return resp, err } func (c *Client) buildAndExecuteRequest( ctx context.Context, method string, ub *api.URLBuilder, body io.Reader, h http.Header) (*http.Response, error) { req, err := c.newRequest(ctx, method, ub, body) if err != nil { return nil, err } err = c.updateToken(ctx) if err != nil { return nil, err } for key, val := range h { req.Header[key] = append(req.Header[key], val...) } return c.executeRequest(req) } // SetAuth sets authentication data. func (c *Client) SetAuth(username, password string) { c.username = username c.password = password c.token = "" c.tokenType = "" c.expires = time.Time{} } func (c *Client) executeAuthRequest(req *http.Request) error { resp, err := c.executeRequest(req) if err != nil { return err } defer resp.Body.Close() if resp.StatusCode != http.StatusOK { return errors.New(resp.Status) } dec := json.NewDecoder(resp.Body) var tinfo api.AuthJSON err = dec.Decode(&tinfo) if err != nil { return err } c.token = tinfo.Token c.tokenType = tinfo.Type c.expires = time.Now().Add(time.Duration(tinfo.Expires*10/9) * time.Second) return nil } func (c *Client) updateToken(ctx context.Context) error { if c.username == "" { return nil } if time.Now().After(c.expires) { return c.Authenticate(ctx) } return c.RefreshToken(ctx) } // Authenticate sets a new token by sending user name and password. func (c *Client) Authenticate(ctx context.Context) error { authData := url.Values{"username": {c.username}, "password": {c.password}} req, err := c.newRequest(ctx, http.MethodPost, c.newURLBuilder('v'), strings.NewReader(authData.Encode())) if err != nil { return err } req.Header.Set("Content-Type", "application/x-www-form-urlencoded") return c.executeAuthRequest(req) } // RefreshToken updates the access token func (c *Client) RefreshToken(ctx context.Context) error { req, err := c.newRequest(ctx, http.MethodPut, c.newURLBuilder('v'), nil) if err != nil { return err } return c.executeAuthRequest(req) } // CreateZettel creates a new zettel and returns its URL. func (c *Client) CreateZettel(ctx context.Context, data *api.ZettelDataJSON) (id.Zid, error) { var buf bytes.Buffer if err := encodeZettelData(&buf, data); err != nil { return id.Invalid, err } ub := c.jsonZettelURLBuilder(nil) resp, err := c.buildAndExecuteRequest(ctx, http.MethodPost, ub, &buf, nil) if err != nil { return id.Invalid, err } defer resp.Body.Close() if resp.StatusCode != http.StatusCreated { return id.Invalid, errors.New(resp.Status) } dec := json.NewDecoder(resp.Body) var newZid api.ZidJSON err = dec.Decode(&newZid) if err != nil { return id.Invalid, err } zid, err := id.Parse(newZid.ID) if err != nil { return id.Invalid, err } return zid, nil } func encodeZettelData(buf *bytes.Buffer, data *api.ZettelDataJSON) error { enc := json.NewEncoder(buf) enc.SetEscapeHTML(false) return enc.Encode(&data) } // ListZettel returns a list of all Zettel. func (c *Client) ListZettel(ctx context.Context, query url.Values) ([]api.ZettelJSON, error) { ub := c.jsonZettelURLBuilder(query) resp, err := c.buildAndExecuteRequest(ctx, http.MethodGet, ub, nil, nil) if err != nil { return nil, err } defer resp.Body.Close() if resp.StatusCode != http.StatusOK { return nil, errors.New(resp.Status) } dec := json.NewDecoder(resp.Body) var zl api.ZettelListJSON err = dec.Decode(&zl) if err != nil { return nil, err } return zl.List, nil } // GetZettelJSON returns a zettel as a JSON struct. func (c *Client) GetZettelJSON(ctx context.Context, zid id.Zid, query url.Values) (*api.ZettelDataJSON, error) { ub := c.jsonZettelURLBuilder(query).SetZid(zid) resp, err := c.buildAndExecuteRequest(ctx, http.MethodGet, ub, nil, nil) if err != nil { return nil, err } defer resp.Body.Close() if resp.StatusCode != http.StatusOK { return nil, errors.New(resp.Status) } dec := json.NewDecoder(resp.Body) var out api.ZettelDataJSON err = dec.Decode(&out) if err != nil { return nil, err } return &out, nil } // GetEvaluatedZettel return a zettel in a defined encoding. func (c *Client) GetEvaluatedZettel(ctx context.Context, zid id.Zid, enc api.EncodingEnum) (string, error) { ub := c.jsonZettelURLBuilder(nil).SetZid(zid) ub.AppendQuery(api.QueryKeyFormat, enc.String()) ub.AppendQuery(api.QueryKeyPart, api.PartContent) resp, err := c.buildAndExecuteRequest(ctx, http.MethodGet, ub, nil, nil) if err != nil { return "", err } defer resp.Body.Close() if resp.StatusCode != http.StatusOK { return "", errors.New(resp.Status) } content, err := io.ReadAll(resp.Body) if err != nil { return "", err } return string(content), nil } // GetZettelOrder returns metadata of the given zettel and, more important, // metadata of zettel that are referenced in a list within the first zettel. func (c *Client) GetZettelOrder(ctx context.Context, zid id.Zid) (*api.ZidMetaRelatedList, error) { ub := c.newURLBuilder('o').SetZid(zid) resp, err := c.buildAndExecuteRequest(ctx, http.MethodGet, ub, nil, nil) if err != nil { return nil, err } defer resp.Body.Close() if resp.StatusCode != http.StatusOK { return nil, errors.New(resp.Status) } dec := json.NewDecoder(resp.Body) var out api.ZidMetaRelatedList err = dec.Decode(&out) if err != nil { return nil, err } return &out, nil } // ContextDirection specifies how the context should be calculated. type ContextDirection uint8 // Allowed values for ContextDirection const ( _ ContextDirection = iota DirBoth DirBackward DirForward ) // GetZettelContext returns metadata of the given zettel and, more important, // metadata of zettel that for the context of the first zettel. func (c *Client) GetZettelContext( ctx context.Context, zid id.Zid, dir ContextDirection, depth, limit int) ( *api.ZidMetaRelatedList, error, ) { ub := c.newURLBuilder('x').SetZid(zid) switch dir { case DirBackward: ub.AppendQuery(api.QueryKeyDir, api.DirBackward) case DirForward: ub.AppendQuery(api.QueryKeyDir, api.DirForward) } if depth > 0 { ub.AppendQuery(api.QueryKeyDepth, strconv.Itoa(depth)) } if limit > 0 { ub.AppendQuery(api.QueryKeyLimit, strconv.Itoa(limit)) } resp, err := c.buildAndExecuteRequest(ctx, http.MethodGet, ub, nil, nil) if err != nil { return nil, err } defer resp.Body.Close() if resp.StatusCode != http.StatusOK { return nil, errors.New(resp.Status) } dec := json.NewDecoder(resp.Body) var out api.ZidMetaRelatedList err = dec.Decode(&out) if err != nil { return nil, err } return &out, nil } // GetZettelLinks returns connections to ohter zettel, images, externals URLs. func (c *Client) GetZettelLinks(ctx context.Context, zid id.Zid) (*api.ZettelLinksJSON, error) { ub := c.newURLBuilder('l').SetZid(zid) resp, err := c.buildAndExecuteRequest(ctx, http.MethodGet, ub, nil, nil) if err != nil { return nil, err } defer resp.Body.Close() if resp.StatusCode != http.StatusOK { return nil, errors.New(resp.Status) } dec := json.NewDecoder(resp.Body) var out api.ZettelLinksJSON err = dec.Decode(&out) if err != nil { return nil, err } return &out, nil } // UpdateZettel updates an existing zettel. func (c *Client) UpdateZettel(ctx context.Context, zid id.Zid, data *api.ZettelDataJSON) error { var buf bytes.Buffer if err := encodeZettelData(&buf, data); err != nil { return err } ub := c.jsonZettelURLBuilder(nil).SetZid(zid) resp, err := c.buildAndExecuteRequest(ctx, http.MethodPut, ub, &buf, nil) if err != nil { return err } defer resp.Body.Close() if resp.StatusCode != http.StatusNoContent { return errors.New(resp.Status) } return nil } // RenameZettel renames a zettel. func (c *Client) RenameZettel(ctx context.Context, oldZid, newZid id.Zid) error { ub := c.jsonZettelURLBuilder(nil).SetZid(oldZid) h := http.Header{ api.HeaderDestination: {c.jsonZettelURLBuilder(nil).SetZid(newZid).String()}, } resp, err := c.buildAndExecuteRequest(ctx, api.MethodMove, ub, nil, h) if err != nil { return err } defer resp.Body.Close() if resp.StatusCode != http.StatusNoContent { return errors.New(resp.Status) } return nil } // DeleteZettel deletes a zettel with the given identifier. func (c *Client) DeleteZettel(ctx context.Context, zid id.Zid) error { ub := c.jsonZettelURLBuilder(nil).SetZid(zid) resp, err := c.buildAndExecuteRequest(ctx, http.MethodDelete, ub, nil, nil) if err != nil { return err } defer resp.Body.Close() if resp.StatusCode != http.StatusNoContent { return errors.New(resp.Status) } return nil } func (c *Client) jsonZettelURLBuilder(query url.Values) *api.URLBuilder { ub := c.newURLBuilder('z') for key, values := range query { if key == api.QueryKeyFormat { continue } for _, val := range values { ub.AppendQuery(key, val) } } return ub } // ListTags returns a map of all tags, together with the associated zettel containing this tag. func (c *Client) ListTags(ctx context.Context) (map[string][]string, error) { err := c.updateToken(ctx) if err != nil { return nil, err } req, err := c.newRequest(ctx, http.MethodGet, c.newURLBuilder('t'), nil) if err != nil { return nil, err } resp, err := c.executeRequest(req) if err != nil { return nil, err } defer resp.Body.Close() if resp.StatusCode != http.StatusOK { return nil, errors.New(resp.Status) } dec := json.NewDecoder(resp.Body) var tl api.TagListJSON err = dec.Decode(&tl) if err != nil { return nil, err } return tl.Tags, nil } // ListRoles returns a list of all roles. func (c *Client) ListRoles(ctx context.Context) ([]string, error) { err := c.updateToken(ctx) if err != nil { return nil, err } req, err := c.newRequest(ctx, http.MethodGet, c.newURLBuilder('r'), nil) if err != nil { return nil, err } resp, err := c.executeRequest(req) if err != nil { return nil, err } defer resp.Body.Close() if resp.StatusCode != http.StatusOK { return nil, errors.New(resp.Status) } dec := json.NewDecoder(resp.Body) var rl api.RoleListJSON err = dec.Decode(&rl) if err != nil { return nil, err } return rl.Roles, nil } |
Added client/client_test.go.
> > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 326 327 328 329 330 331 332 333 334 | //----------------------------------------------------------------------------- // Copyright (c) 2021 Detlef Stern // // This file is part of zettelstore. // // Zettelstore is licensed under the latest version of the EUPL (European Union // Public License). Please see file LICENSE.txt for your rights and obligations // under this license. //----------------------------------------------------------------------------- // Package client provides a client for accessing the Zettelstore via its API. package client_test import ( "context" "flag" "fmt" "net/url" "testing" "zettelstore.de/z/api" "zettelstore.de/z/client" "zettelstore.de/z/domain/id" "zettelstore.de/z/domain/meta" ) func TestCreateRenameDeleteZettel(t *testing.T) { // Is not to be allowed to run in parallel with other tests. c := getClient() c.SetAuth("creator", "creator") zid, err := c.CreateZettel(context.Background(), &api.ZettelDataJSON{ Meta: nil, Encoding: "", Content: "Example", }) if err != nil { t.Error("Cannot create zettel:", err) return } if !zid.IsValid() { t.Error("Invalid zettel ID", zid) return } newZid := zid + 1 c.SetAuth("owner", "owner") err = c.RenameZettel(context.Background(), zid, newZid) if err != nil { t.Error("Cannot rename", zid, ":", err) newZid = zid } err = c.DeleteZettel(context.Background(), newZid) if err != nil { t.Error("Cannot delete", zid, ":", err) return } } func TestUpdateZettel(t *testing.T) { t.Parallel() c := getClient() c.SetAuth("writer", "writer") z, err := c.GetZettelJSON(context.Background(), id.DefaultHomeZid, nil) if err != nil { t.Error(err) return } if got := z.Meta[meta.KeyTitle]; got != "Home" { t.Errorf("Title of zettel is not \"Home\", but %q", got) return } newTitle := "New Home" z.Meta[meta.KeyTitle] = newTitle err = c.UpdateZettel(context.Background(), id.DefaultHomeZid, z) if err != nil { t.Error(err) return } zt, err := c.GetZettelJSON(context.Background(), id.DefaultHomeZid, nil) if err != nil { t.Error(err) return } if got := zt.Meta[meta.KeyTitle]; got != newTitle { t.Errorf("Title of zettel is not %q, but %q", newTitle, got) } } func TestList(t *testing.T) { testdata := []struct { user string exp int }{ {"", 7}, {"creator", 10}, {"reader", 12}, {"writer", 12}, {"owner", 34}, } t.Parallel() c := getClient() query := url.Values{api.QueryKeyFormat: {"html"}} // Client must remove "html" for i, tc := range testdata { t.Run(fmt.Sprintf("User %d/%q", i, tc.user), func(tt *testing.T) { c.SetAuth(tc.user, tc.user) l, err := c.ListZettel(context.Background(), query) if err != nil { tt.Error(err) return } got := len(l) if got != tc.exp { tt.Errorf("List of length %d expected, but got %d\n%v", tc.exp, got, l) } }) } l, err := c.ListZettel(context.Background(), url.Values{meta.KeyRole: {meta.ValueRoleConfiguration}}) if err != nil { t.Error(err) return } got := len(l) if got != 27 { t.Errorf("List of length %d expected, but got %d\n%v", 27, got, l) } } func TestGetZettel(t *testing.T) { t.Parallel() c := getClient() c.SetAuth("owner", "owner") z, err := c.GetZettelJSON(context.Background(), id.DefaultHomeZid, url.Values{api.QueryKeyPart: {api.PartContent}}) if err != nil { t.Error(err) return } if m := z.Meta; len(m) > 0 { t.Errorf("Exptected empty meta, but got %v", z.Meta) } if z.Content == "" || z.Encoding != "" { t.Errorf("Expect non-empty content, but empty encoding (got %q)", z.Encoding) } } func TestGetEvaluatedZettel(t *testing.T) { t.Parallel() c := getClient() c.SetAuth("owner", "owner") encodings := []api.EncodingEnum{ api.EncoderDJSON, api.EncoderHTML, api.EncoderNative, api.EncoderText, } for _, enc := range encodings { content, err := c.GetEvaluatedZettel(context.Background(), id.DefaultHomeZid, enc) if err != nil { t.Error(err) continue } if len(content) == 0 { t.Errorf("Empty content for encoding %v", enc) } } } func TestGetZettelOrder(t *testing.T) { t.Parallel() c := getClient() c.SetAuth("owner", "owner") rl, err := c.GetZettelOrder(context.Background(), id.TOCNewTemplateZid) if err != nil { t.Error(err) return } if rl.ID != id.TOCNewTemplateZid.String() { t.Errorf("Expected an Zid %v, but got %v", id.TOCNewTemplateZid, rl.ID) return } l := rl.List if got := len(l); got != 2 { t.Errorf("Expected list fo length 2, got %d", got) return } if got := l[0].ID; got != id.TemplateNewZettelZid.String() { t.Errorf("Expected result[0]=%v, but got %v", id.TemplateNewZettelZid, got) } if got := l[1].ID; got != id.TemplateNewUserZid.String() { t.Errorf("Expected result[1]=%v, but got %v", id.TemplateNewUserZid, got) } } func TestGetZettelContext(t *testing.T) { t.Parallel() c := getClient() c.SetAuth("owner", "owner") rl, err := c.GetZettelContext(context.Background(), id.VersionZid, client.DirBoth, 0, 3) if err != nil { t.Error(err) return } if rl.ID != id.VersionZid.String() { t.Errorf("Expected an Zid %v, but got %v", id.VersionZid, rl.ID) return } l := rl.List if got := len(l); got != 3 { t.Errorf("Expected list fo length 3, got %d", got) return } if got := l[0].ID; got != id.DefaultHomeZid.String() { t.Errorf("Expected result[0]=%v, but got %v", id.DefaultHomeZid, got) } if got := l[1].ID; got != id.OperatingSystemZid.String() { t.Errorf("Expected result[1]=%v, but got %v", id.OperatingSystemZid, got) } if got := l[2].ID; got != id.StartupConfigurationZid.String() { t.Errorf("Expected result[2]=%v, but got %v", id.StartupConfigurationZid, got) } rl, err = c.GetZettelContext(context.Background(), id.VersionZid, client.DirBackward, 0, 0) if err != nil { t.Error(err) return } if rl.ID != id.VersionZid.String() { t.Errorf("Expected an Zid %v, but got %v", id.VersionZid, rl.ID) return } l = rl.List if got := len(l); got != 1 { t.Errorf("Expected list fo length 1, got %d", got) return } if got := l[0].ID; got != id.DefaultHomeZid.String() { t.Errorf("Expected result[0]=%v, but got %v", id.DefaultHomeZid, got) } } func TestGetZettelLinks(t *testing.T) { t.Parallel() c := getClient() c.SetAuth("owner", "owner") zl, err := c.GetZettelLinks(context.Background(), id.DefaultHomeZid) if err != nil { t.Error(err) return } if zl.ID != id.DefaultHomeZid.String() { t.Errorf("Expected an Zid %v, but got %v", id.DefaultHomeZid, zl.ID) return } if len(zl.Links.Incoming) != 0 { t.Error("No incomings expected", zl.Links.Incoming) } if got := len(zl.Links.Outgoing); got != 4 { t.Errorf("Expected 4 outgoing links, got %d", got) } if got := len(zl.Links.Local); got != 1 { t.Errorf("Expected 1 local link, got %d", got) } if got := len(zl.Links.External); got != 4 { t.Errorf("Expected 4 external link, got %d", got) } } func TestListTags(t *testing.T) { t.Parallel() c := getClient() c.SetAuth("owner", "owner") tm, err := c.ListTags(context.Background()) if err != nil { t.Error(err) return } tags := []struct { key string size int }{ {"#invisible", 1}, {"#user", 4}, {"#test", 4}, } if len(tm) != len(tags) { t.Errorf("Expected %d different tags, but got only %d (%v)", len(tags), len(tm), tm) } for _, tag := range tags { if zl, ok := tm[tag.key]; !ok { t.Errorf("No tag %v: %v", tag.key, tm) } else if len(zl) != tag.size { t.Errorf("Expected %d zettel with tag %v, but got %v", tag.size, tag.key, zl) } } for i, id := range tm["#user"] { if id != tm["#test"][i] { t.Errorf("Tags #user and #test have different content: %v vs %v", tm["#user"], tm["#test"]) } } } func TestListRoles(t *testing.T) { t.Parallel() c := getClient() c.SetAuth("owner", "owner") rl, err := c.ListRoles(context.Background()) if err != nil { t.Error(err) return } exp := []string{"configuration", "user", "zettel"} if len(rl) != len(exp) { t.Errorf("Expected %d different tags, but got only %d (%v)", len(exp), len(rl), rl) } for i, id := range exp { if id != rl[i] { t.Errorf("Role list pos %d: expected %q, got %q", i, id, rl[i]) } } } var baseURL string func init() { flag.StringVar(&baseURL, "base-url", "", "Base URL") } func getClient() *client.Client { return client.NewClient(baseURL) } // TestMain controls whether client API tests should run or not. func TestMain(m *testing.M) { flag.Parse() if baseURL != "" { m.Run() } } |
Changes to cmd/cmd_file.go.
︙ | ︙ | |||
12 13 14 15 16 17 18 19 20 21 22 23 24 25 | import ( "flag" "fmt" "io" "os" "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" ) | > | 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 | import ( "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" "zettelstore.de/z/parser" ) |
︙ | ︙ | |||
36 37 38 39 40 41 42 | domain.Zettel{ Meta: m, Content: domain.NewContent(inp.Src[inp.Pos:]), }, m.GetDefault(meta.KeySyntax, meta.ValueSyntaxZmk), nil, ) | | | 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 | domain.Zettel{ Meta: m, 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)}) if enc == nil { fmt.Fprintf(os.Stderr, "Unknown format %q\n", format) return 2, nil } _, err = enc.WriteZettel(os.Stdout, z, format != "raw") if err != nil { return 2, err |
︙ | ︙ |
Changes to cmd/cmd_run.go.
︙ | ︙ | |||
10 11 12 13 14 15 16 17 18 19 20 | package cmd import ( "flag" "net/http" "zettelstore.de/z/auth" "zettelstore.de/z/config" "zettelstore.de/z/domain/meta" "zettelstore.de/z/kernel" | > > < < | 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 | package cmd 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/usecase" "zettelstore.de/z/web/adapter/api" "zettelstore.de/z/web/adapter/webui" "zettelstore.de/z/web/server" ) // ---------- Subcommand: run ------------------------------------------------ |
︙ | ︙ | |||
54 55 56 57 58 59 60 | kern.SetDebug(debug) if err := kern.StartService(kernel.WebService); err != nil { return 1, err } return 0, nil } | | | | | | | > | | | | | > > > | > > | < < < | < | < | < | | > > | > > | | > > > > > | > | | 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 | kern.SetDebug(debug) if err := kern.StartService(kernel.WebService); err != nil { return 1, err } return 0, nil } func setupRouting(webSrv server.Server, boxManager box.Manager, authManager auth.Manager, rtConfig config.Config) { protectedBoxManager, authPolicy := authManager.BoxWithPolicy(webSrv, boxManager, rtConfig) api := api.New(webSrv, authManager, authManager, webSrv, rtConfig) wui := webui.New(webSrv, authManager, rtConfig, authManager, boxManager, authPolicy) ucAuthenticate := usecase.NewAuthenticate(authManager, authManager, boxManager) ucCreateZettel := usecase.NewCreateZettel(rtConfig, protectedBoxManager) ucGetMeta := usecase.NewGetMeta(protectedBoxManager) ucGetAllMeta := usecase.NewGetAllMeta(protectedBoxManager) ucGetZettel := usecase.NewGetZettel(protectedBoxManager) ucParseZettel := usecase.NewParseZettel(rtConfig, ucGetZettel) 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)) webSrv.AddZettelRoute('a', http.MethodGet, wui.MakeGetLogoutHandler()) if !authManager.IsReadonly() { webSrv.AddZettelRoute('b', http.MethodGet, wui.MakeGetRenameZettelHandler(ucGetMeta)) webSrv.AddZettelRoute('b', http.MethodPost, wui.MakePostRenameZettelHandler(ucRename)) webSrv.AddZettelRoute('c', http.MethodGet, wui.MakeGetCopyZettelHandler( ucGetZettel, usecase.NewCopyZettel())) webSrv.AddZettelRoute('c', http.MethodPost, wui.MakePostCreateZettelHandler(ucCreateZettel)) webSrv.AddZettelRoute('d', http.MethodGet, wui.MakeGetDeleteZettelHandler(ucGetZettel)) webSrv.AddZettelRoute('d', http.MethodPost, wui.MakePostDeleteZettelHandler(ucDelete)) webSrv.AddZettelRoute('e', http.MethodGet, wui.MakeEditGetZettelHandler(ucGetZettel)) webSrv.AddZettelRoute('e', http.MethodPost, wui.MakeEditSetZettelHandler(ucUpdate)) webSrv.AddZettelRoute('f', http.MethodGet, wui.MakeGetFolgeZettelHandler( ucGetZettel, usecase.NewFolgeZettel(rtConfig))) webSrv.AddZettelRoute('f', http.MethodPost, wui.MakePostCreateZettelHandler(ucCreateZettel)) webSrv.AddZettelRoute('g', http.MethodGet, wui.MakeGetNewZettelHandler( ucGetZettel, usecase.NewNewZettel())) webSrv.AddZettelRoute('g', http.MethodPost, wui.MakePostCreateZettelHandler(ucCreateZettel)) } webSrv.AddListRoute('f', http.MethodGet, wui.MakeSearchHandler( usecase.NewSearch(protectedBoxManager), ucGetMeta, ucGetZettel)) webSrv.AddListRoute('h', http.MethodGet, wui.MakeListHTMLMetaHandler( ucListMeta, ucListRoles, ucListTags)) webSrv.AddZettelRoute('h', http.MethodGet, wui.MakeGetHTMLZettelHandler( ucParseZettel, ucGetMeta)) webSrv.AddZettelRoute('i', http.MethodGet, wui.MakeGetInfoHandler( ucParseZettel, ucGetMeta, ucGetAllMeta)) webSrv.AddZettelRoute('j', http.MethodGet, wui.MakeZettelContextHandler(ucZettelContext)) // API webSrv.AddZettelRoute('l', http.MethodGet, api.MakeGetLinksHandler(ucParseZettel)) webSrv.AddZettelRoute('o', http.MethodGet, api.MakeGetOrderHandler( usecase.NewZettelOrder(protectedBoxManager, ucParseZettel))) webSrv.AddListRoute('r', http.MethodGet, api.MakeListRoleHandler(ucListRoles)) webSrv.AddListRoute('t', http.MethodGet, api.MakeListTagsHandler(ucListTags)) webSrv.AddListRoute('v', http.MethodPost, api.MakePostLoginHandler(ucAuthenticate)) webSrv.AddListRoute('v', http.MethodPut, api.MakeRenewAuthHandler()) webSrv.AddZettelRoute('x', http.MethodGet, api.MakeZettelContextHandler(ucZettelContext)) webSrv.AddListRoute('z', http.MethodGet, api.MakeListMetaHandler( usecase.NewListMeta(protectedBoxManager), ucGetMeta, ucParseZettel)) webSrv.AddZettelRoute('z', http.MethodGet, api.MakeGetZettelHandler( ucParseZettel, ucGetMeta)) if !authManager.IsReadonly() { webSrv.AddListRoute('z', 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)) } } |
Changes to cmd/command.go.
︙ | ︙ | |||
15 16 17 18 19 20 21 | "sort" "zettelstore.de/z/domain/meta" ) // Command stores information about commands / sub-commands. type Command struct { | | | | | > | | | 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 | "sort" "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 } // CommandFunc is the function that executes the command. // It accepts the parsed command line parameters. // It returns the exit code and an error. type CommandFunc func(*flag.FlagSet, *meta.Meta) (int, error) |
︙ | ︙ |
Changes to cmd/fd_limit_raise.go.
︙ | ︙ | |||
37 38 39 40 41 42 43 | return err } err = syscall.Getrlimit(syscall.RLIMIT_NOFILE, &rLimit) if err != nil { return err } if rLimit.Cur < minFiles { | | | 37 38 39 40 41 42 43 44 45 46 47 | return err } err = syscall.Getrlimit(syscall.RLIMIT_NOFILE, &rLimit) if err != nil { return err } if rLimit.Cur < minFiles { log.Printf("Make sure you have no more than %d files in all your boxes if you enabled notification\n", rLimit.Cur) } return nil } |
Changes to cmd/main.go.
︙ | ︙ | |||
18 19 20 21 22 23 24 25 26 27 28 29 | "net/url" "os" "strconv" "strings" "zettelstore.de/z/auth" "zettelstore.de/z/auth/impl" "zettelstore.de/z/config" "zettelstore.de/z/domain/id" "zettelstore.de/z/domain/meta" "zettelstore.de/z/input" "zettelstore.de/z/kernel" | > > > < < < | 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 | "net/url" "os" "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/web/server" ) const ( defConfigfile = ".zscfg" ) |
︙ | ︙ | |||
50 51 52 53 54 55 56 | }) RegisterCommand(Command{ Name: "version", Func: func(*flag.FlagSet, *meta.Meta) (int, error) { return 0, nil }, Header: true, }) RegisterCommand(Command{ | | | | | > | | | 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 | }) RegisterCommand(Command{ 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, }) RegisterCommand(Command{ Name: "run-simple", Func: runSimpleFunc, Boxes: true, Header: true, Flags: flgSimpleRun, }) RegisterCommand(Command{ Name: "file", Func: cmdFile, Flags: func(fs *flag.FlagSet) { |
︙ | ︙ | |||
109 110 111 112 113 114 115 | case "d": val := flg.Value.String() if strings.HasPrefix(val, "/") { val = "dir://" + val } else { val = "dir:" + val } | | | | | | | | | | | | | | | | | | | | 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 | case "d": val := flg.Value.String() if strings.HasPrefix(val, "/") { val = "dir://" + val } else { val = "dir:" + val } cfg.Set(keyBoxOneURI, val) case "r": cfg.Set(keyReadOnly, flg.Value.String()) case "v": cfg.Set(keyVerbose, flg.Value.String()) } }) return cfg } func parsePort(s string) (string, error) { port, err := net.LookupPort("tcp", s) if err != nil { fmt.Fprintf(os.Stderr, "Wrong port specification: %q", s) return "", err } return strconv.Itoa(port), nil } const ( keyAdminPort = "admin-port" 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" ) func setServiceConfig(cfg *meta.Meta) error { ok := setConfigValue(true, kernel.CoreService, kernel.CoreVerbose, cfg.GetBool(keyVerbose)) if val, found := cfg.Get(keyAdminPort); found { ok = setConfigValue(ok, kernel.CoreService, kernel.CorePort, val) } ok = setConfigValue(ok, kernel.AuthService, kernel.AuthOwner, cfg.GetDefault(keyOwner, "")) ok = setConfigValue(ok, kernel.AuthService, kernel.AuthReadonly, cfg.GetBool(keyReadOnly)) ok = setConfigValue( ok, kernel.BoxService, kernel.BoxDefaultDirType, cfg.GetDefault(keyDefaultDirBoxType, kernel.BoxDirTypeNotify)) ok = setConfigValue(ok, kernel.BoxService, kernel.BoxURIs+"1", "dir:./zettel") format := kernel.BoxURIs + "%v" for i := 1; ; i++ { key := fmt.Sprintf(format, i) val, found := cfg.Get(key) if !found { break } ok = setConfigValue(ok, kernel.BoxService, key, val) } ok = setConfigValue( ok, kernel.WebService, kernel.WebListenAddress, cfg.GetDefault(keyListenAddr, "127.0.0.1:23123")) ok = setConfigValue(ok, kernel.WebService, kernel.WebURLPrefix, cfg.GetDefault(keyURLPrefix, "/")) ok = setConfigValue(ok, kernel.WebService, kernel.WebSecureCookie, !cfg.GetBool(keyInsecureCookie)) |
︙ | ︙ | |||
191 192 193 194 195 196 197 | done := kernel.Main.SetConfig(subsys, key, fmt.Sprintf("%v", val)) if !done { kernel.Main.Log("unable to set configuration:", key, val) } return ok && done } | | | | | | | | | | | 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 | done := kernel.Main.SetConfig(subsys, key, fmt.Sprintf("%v", val)) if !done { kernel.Main.Log("unable to set configuration:", key, val) } return ok && done } func setupOperations(cfg *meta.Meta, withBoxes bool) { var createManager kernel.CreateBoxManagerFunc if withBoxes { err := raiseFdLimit() if err != nil { srvm := kernel.Main srvm.Log("Raising some limitions did not work:", err) srvm.Log("Prepare to encounter errors. Most of them can be mitigated. See the manual for details") srvm.SetConfig(kernel.BoxService, kernel.BoxDefaultDirType, kernel.BoxDirTypeSimple) } createManager = func(boxURIs []*url.URL, authManager auth.Manager, rtConfig config.Config) (box.Manager, error) { compbox.Setup(cfg) return manager.New(boxURIs, authManager, rtConfig) } } else { createManager = func([]*url.URL, auth.Manager, config.Config) (box.Manager, error) { return nil, nil } } kernel.Main.SetCreators( func(readonly bool, owner id.Zid) (auth.Manager, error) { return impl.New(readonly, owner, cfg.GetDefault("secret", "")), nil }, createManager, func(srv server.Server, plMgr box.Manager, authMgr auth.Manager, rtConfig config.Config) error { setupRouting(srv, plMgr, authMgr, rtConfig) return nil }, ) } func executeCommand(name string, args ...string) int { |
︙ | ︙ | |||
237 238 239 240 241 242 243 | return 1 } cfg := getConfig(fs) if err := setServiceConfig(cfg); err != nil { fmt.Fprintf(os.Stderr, "%s: %v\n", name, err) return 2 } | | | | 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 | return 1 } cfg := getConfig(fs) if err := setServiceConfig(cfg); err != nil { fmt.Fprintf(os.Stderr, "%s: %v\n", name, err) return 2 } setupOperations(cfg, command.Boxes) kernel.Main.Start(command.Header, command.LineServer) exitCode, err := command.Func(fs, cfg) if err != nil { fmt.Fprintf(os.Stderr, "%s: %v\n", name, err) } kernel.Main.Shutdown(true) return exitCode } |
︙ | ︙ |
Changes to cmd/register.go.
︙ | ︙ | |||
9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 | //----------------------------------------------------------------------------- // Package cmd provides command generic functions. package cmd // Mention all needed encoders, parsers and stores to have them registered. import ( _ "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. _ "zettelstore.de/z/encoder/zmkenc" // Allow to use zmk encoder. _ "zettelstore.de/z/kernel/impl" // Allow kernel implementation to create itself _ "zettelstore.de/z/parser/blob" // Allow to use BLOB parser. _ "zettelstore.de/z/parser/markdown" // Allow to use markdown parser. _ "zettelstore.de/z/parser/none" // Allow to use none parser. _ "zettelstore.de/z/parser/plain" // Allow to use plain parser. _ "zettelstore.de/z/parser/zettelmark" // Allow to use zettelmark parser. | > > > > > < < < < < | 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 | //----------------------------------------------------------------------------- // 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. _ "zettelstore.de/z/encoder/zmkenc" // Allow to use zmk encoder. _ "zettelstore.de/z/kernel/impl" // Allow kernel implementation to create itself _ "zettelstore.de/z/parser/blob" // Allow to use BLOB parser. _ "zettelstore.de/z/parser/markdown" // Allow to use markdown parser. _ "zettelstore.de/z/parser/none" // Allow to use none parser. _ "zettelstore.de/z/parser/plain" // Allow to use plain parser. _ "zettelstore.de/z/parser/zettelmark" // Allow to use zettelmark parser. ) |
Changes to collect/collect.go.
1 2 3 4 5 6 7 8 9 10 11 12 13 | //----------------------------------------------------------------------------- // Copyright (c) 2020-2021 Detlef Stern // // This file is part of zettelstore. // // Zettelstore is licensed under the latest version of the EUPL (European Union // Public License). Please see file LICENSE.txt for your rights and obligations // under this license. //----------------------------------------------------------------------------- // Package collect provides functions to collect items from a syntax tree. package collect | < | < | < | | < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < | < < | < < | < | | < < < | | | | < < < | | | | < < | < < < < < < < < | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 | //----------------------------------------------------------------------------- // Copyright (c) 2020-2021 Detlef Stern // // This file is part of zettelstore. // // Zettelstore is licensed under the latest version of the EUPL (European Union // Public License). Please see file LICENSE.txt for your rights and obligations // under this license. //----------------------------------------------------------------------------- // Package collect provides functions to collect items from a syntax tree. package collect import "zettelstore.de/z/ast" // Summary stores the relevant parts of the syntax tree type Summary struct { Links []*ast.Reference // list of all referenced links Images []*ast.Reference // list of all referenced images Cites []*ast.CiteNode // list of all referenced citations } // References returns all references mentioned in the given zettel. This also // includes references to images. func References(zn *ast.ZettelNode) (s Summary) { ast.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 } |
Changes to collect/collect_test.go.
︙ | ︙ | |||
23 24 25 26 27 28 29 30 31 32 33 34 35 36 | if !r.IsValid() { panic(s) } return r } func TestLinks(t *testing.T) { 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) } intNode := &ast.LinkNode{Ref: parseRef("01234567890123")} | > | 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 | if !r.IsValid() { panic(s) } 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) } intNode := &ast.LinkNode{Ref: parseRef("01234567890123")} |
︙ | ︙ | |||
50 51 52 53 54 55 56 57 58 59 60 61 62 63 | summary = collect.References(zn) if cnt := len(summary.Links); cnt != 3 { t.Error("Link count does not work. Expected: 3, got", summary.Links) } } func TestImage(t *testing.T) { zn := &ast.ZettelNode{ Ast: ast.BlockSlice{ &ast.ParaNode{ Inlines: ast.InlineSlice{ &ast.ImageNode{Ref: parseRef("12345678901234")}, }, }, | > | 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 | summary = collect.References(zn) if cnt := len(summary.Links); cnt != 3 { 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")}, }, }, |
︙ | ︙ |
Changes to collect/order.go.
︙ | ︙ | |||
13 14 15 16 17 18 19 | import "zettelstore.de/z/ast" // Order of internal reference within the given zettel. func Order(zn *ast.ZettelNode) (result []*ast.Reference) { for _, bn := range zn.Ast { if ln, ok := bn.(*ast.NestedListNode); ok { | | | 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 | import "zettelstore.de/z/ast" // Order of internal reference within the given zettel. func Order(zn *ast.ZettelNode) (result []*ast.Reference) { for _, bn := range zn.Ast { if ln, ok := bn.(*ast.NestedListNode); ok { switch ln.Kind { case ast.NestedListOrdered, ast.NestedListUnordered: for _, is := range ln.Items { if ref := firstItemZettelReference(is); ref != nil { result = append(result, ref) } } } |
︙ | ︙ |
Changes to config/config.go.
︙ | ︙ | |||
52 53 54 55 56 57 58 | // GetMarkerExternal returns the current value of the "marker-external" key. GetMarkerExternal() string // GetFooterHTML returns HTML code that should be embedded into the footer // of each WebUI page. GetFooterHTML() string | < < < < | 52 53 54 55 56 57 58 59 60 61 62 63 64 65 | // GetMarkerExternal returns the current value of the "marker-external" key. GetMarkerExternal() string // GetFooterHTML returns HTML code that should be embedded into the footer // of each WebUI page. GetFooterHTML() string } // AuthConfig are relevant configuration values for authentication. type AuthConfig interface { // GetExpertMode returns the current value of the "expert-mode" key GetExpertMode() bool |
︙ | ︙ |
Changes to docs/manual/00001002000000.zettel.
︙ | ︙ | |||
11 12 13 14 15 16 17 | : It should be not hard to write other software that works with your zettel. ; Single user : 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 | | | | 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 | : It should be not hard to write other software that works with your zettel. ; Single user : 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. : 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. : If you decide to use multiple directories, you are free to configure Zettelstore appropriately. ; Multiple modes of operation : You can use Zettelstore as a standalone software on your device, but you are not restricted to it. : You can install the software on a central server, or you can install it on all your devices with no restrictions how to synchronize your zettel. ; Multiple user interfaces : Zettelstore provides a default web-based user interface. Anybody can provide alternative user interfaces, e.g. for special purposes. ; Simple service : The purpose of Zettelstore is to safely store your zettel and to provide some initial relations between them. : External software can be written to deeply analyze your zettel and the structures they form. |
Changes to docs/manual/00001003000000.zettel.
1 2 3 4 5 6 7 8 9 10 11 | id: 00001003000000 title: Installation of the Zettelstore software role: manual tags: #installation #manual #zettelstore syntax: zmk === 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 | | | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 | id: 00001003000000 title: Installation of the Zettelstore software role: manual tags: #installation #manual #zettelstore syntax: zmk === 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. 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. * If you restart your device, please make sure to start your Zettelstore again. |
︙ | ︙ | |||
39 40 41 42 43 44 45 | ```sh # sudo useradd --system --gid zettelstore \ --create-home --home-dir /var/lib/zettelstore \ --shell /usr/sbin/nologin \ --comment "Zettelstore server" \ zettelstore ``` | | | 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 | ```sh # sudo useradd --system --gid zettelstore \ --create-home --home-dir /var/lib/zettelstore \ --shell /usr/sbin/nologin \ --comment "Zettelstore server" \ zettelstore ``` Create a systemd service file and store it into ''/etc/systemd/system/zettelstore.service'': ```ini [Unit] Description=Zettelstore After=network.target [Service] Type=simple |
︙ | ︙ |
Changes to docs/manual/00001004010000.zettel.
1 2 3 4 5 | id: 00001004010000 title: Zettelstore startup configuration role: manual tags: #configuration #manual #zettelstore syntax: zmk | | | | > > > > > > > > | | | | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 | id: 00001004010000 title: Zettelstore startup configuration role: manual tags: #configuration #manual #zettelstore syntax: zmk modified: 20210712234656 The configuration file, as specified by the ''-c CONFIGFILE'' [[command line option|00001004051000]], allows you to specify some startup options. These options cannot be stored in a [[configuration zettel|00001004020000]] because either they are needed before Zettelstore can start or because of security reasons. For example, Zettelstore need to know in advance, on which network address is must listen or where zettel are stored. An attacker that is able to change the owner can do anything. 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. 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]]. 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: ''notify'' ; [!insecure-cookie]''insecure-cookie'' : Must be set to ''true'', if authentication is enabled and Zettelstore is not accessible not via HTTPS (but via HTTP). Otherwise web browser are free to ignore the authentication cookie. Default: ''false'' |
︙ | ︙ | |||
48 49 50 51 52 53 54 | On these devices, the operating system is free to stop the web browser and to remove temporary cookies. Therefore, an authenticated user will be logged off. 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'' | < < < < < < < | 56 57 58 59 60 61 62 63 64 65 66 67 68 69 | On these devices, the operating system is free to stop the web browser and to remove temporary cookies. Therefore, an authenticated user will be logged off. 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'' ; [!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'' : Define lifetime of access tokens in minutes. Values are only valid if authentication is enabled, i.e. key ''owner'' is set. |
︙ | ︙ | |||
78 79 80 81 82 83 84 | Must begin and end with a slash character (""''/''"", ''U+002F''). Default: ''"/"''. This allows to use a forwarding proxy [[server|00001010090100]] in front of the Zettelstore. ; ''verbose'' : Be more verbose inf logging data. Default: false | < < | 79 80 81 82 83 84 85 | Must begin and end with a slash character (""''/''"", ''U+002F''). Default: ''"/"''. This allows to use a forwarding proxy [[server|00001010090100]] in front of the Zettelstore. ; ''verbose'' : Be more verbose inf logging data. Default: false |
Changes to docs/manual/00001004011200.zettel.
1 | id: 00001004011200 | | | | | | | | | | | | | | | | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 | id: 00001004011200 title: Zettelstore boxes 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. 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. The following box 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. ; ''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. ; ''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. 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. You must make sure that your computer has enough RAM to store all zettel. |
Changes to docs/manual/00001004011400.zettel.
1 | id: 00001004011400 | | | | | | | | | | | | | | | | | | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 | id: 00001004011400 title: Configure file directory boxes 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''. 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]]'') |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. To cope with this uncertainty, Zettelstore provides various internal implementations of a directory box. 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. Works on all platforms. Is a little slower than other implementations (up to three times slower). ; notify : Automatically detect external changes. Tries to optimize performance, at a little cost of main memory (RAM). === Rescan When the parameter ''type'' is set to ""notify"", Zettelstore automatically detects changes to zettel files that originates from other software. It is done on a ""best-effort"" basis. 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 ``` 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. This value is ignored for other directory box types, 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. 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 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. ``` box-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. |
Changes to docs/manual/00001004020000.zettel.
1 2 3 4 5 6 7 8 9 10 11 12 | id: 00001004020000 title: Configure the running Zettelstore role: manual tags: #configuration #manual #zettelstore syntax: zmk 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: ; [!default-copyright]''default-copyright'' : Copyright value to be used when rendering content. | > | 1 2 3 4 5 6 7 8 9 10 11 12 13 | 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: ; [!default-copyright]''default-copyright'' : Copyright value to be used when rendering content. |
︙ | ︙ | |||
50 51 52 53 54 55 56 | Default: (the empty string). ; [!home-zettel]''home-zettel'' : 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. | < < < < | 51 52 53 54 55 56 57 58 59 60 61 62 63 64 | Default: (the empty string). ; [!home-zettel]''home-zettel'' : 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. ; [!site-name]''site-name'' : Name of the Zettelstore instance. Will be used when displaying some lists. Default: ''Zettelstore''. ; [!yaml-header]''yaml-header'' : If true, metadata and content will be separated by ''-\--\\n'' instead of an empty line (''\\n\\n''). Default: ''false''. |
︙ | ︙ |
Changes to docs/manual/00001004050200.zettel.
1 2 3 4 5 | id: 00001004050200 title: The ''help'' sub-command role: manual tags: #command #configuration #manual #zettelstore syntax: zmk | | < | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 | id: 00001004050200 title: The ''help'' sub-command role: manual tags: #command #configuration #manual #zettelstore syntax: zmk modified: 20210712233414 Lists all implemented sub-commands. Example: ``` # zettelstore help Available commands: - "file" - "help" - "password" - "run" - "run-simple" - "version" ``` |
Changes to docs/manual/00001004050400.zettel.
1 2 3 4 5 | id: 00001004050400 title: The ''version'' sub-command role: manual tags: #command #configuration #manual #zettelstore syntax: zmk | | | | | < | | | > | < | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 | id: 00001004050400 title: The ''version'' sub-command role: manual tags: #command #configuration #manual #zettelstore syntax: zmk modified: 20210712234031 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 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. Everything after the release version is optional, eg. ""1.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) ``` 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]]. The software was build for running under a Linux operating system with an ""amd64"" processor. |
Changes to docs/manual/00001004051000.zettel.
1 2 3 4 5 | id: 00001004051000 title: The ''run'' sub-command role: manual tags: #command #configuration #manual #zettelstore syntax: zmk | | < | | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 | id: 00001004051000 title: The ''run'' sub-command role: manual tags: #command #configuration #manual #zettelstore syntax: zmk modified: 20210712234419 === ``zettelstore run`` This starts the web service. ``` zettelstore run [-a PORT] [-c CONFIGFILE] [-d DIR] [-debug] [-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. ; [!c]''-c CONFIGFILE'' : Specifies ''CONFIGFILE'' as a file, where [[startup configuration data|00001004010000]] is read. |
︙ | ︙ |
Changes to docs/manual/00001004051100.zettel.
1 2 3 4 5 | id: 00001004051100 title: The ''run-simple'' sub-command role: manual tags: #command #configuration #manual #zettelstore syntax: zmk | | | > | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 | id: 00001004051100 title: The ''run-simple'' sub-command role: manual tags: #command #configuration #manual #zettelstore syntax: zmk modified: 20210712234203 === ``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. ``` 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"". |
Changes to docs/manual/00001004051200.zettel.
1 2 3 4 5 | id: 00001004051200 title: The ''file'' sub-command role: manual tags: #command #configuration #manual #zettelstore syntax: zmk | | | 1 2 3 4 5 6 7 8 9 10 11 12 13 | id: 00001004051200 title: The ''file'' sub-command role: manual tags: #command #configuration #manual #zettelstore syntax: zmk modified: 20210712234222 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]] ``` |
︙ | ︙ |
Changes to docs/manual/00001004051400.zettel.
1 2 3 4 5 | id: 00001004051400 title: The ''password'' sub-command role: manual tags: #command #configuration #manual #zettelstore syntax: zmk | | | | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 | id: 00001004051400 title: The ''password'' sub-command role: manual tags: #command #configuration #manual #zettelstore syntax: zmk modified: 20210712234305 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. The general usage is: ``` zettelstore password IDENT ZETTEL-ID ``` ``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. An example: ``` # zettelstore password bob 20200911115600 Password: Again: |
︙ | ︙ |
Changes to docs/manual/00001004101000.zettel.
︙ | ︙ | |||
56 57 58 59 60 61 62 | : Displays s list of all available services and their current status. ; ''set-config SERVICE KEY VALUE'' : 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. | | | 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 | : Displays s list of all available services and their current status. ; ''set-config SERVICE KEY VALUE'' : 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-//. ; ''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. |
Changes to docs/manual/00001005000000.zettel.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 | id: 00001005000000 title: Structure of Zettelstore role: manual tags: #design #manual #zettelstore syntax: zmk 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. Zettelstore provides additional services to the user. Via a builtin web interface you can work with zettel in various ways. For example, you are able to list zettel, to create new zettel, to edit them, or to delete them. You can view zettel details and relations between zettel. In addition, Zettelstore provides an ""application programming interface"" (API) that allows other software to communicate with the Zettelstore. 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 | > | | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 | 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. Zettelstore provides additional services to the user. Via a builtin web interface you can work with zettel in various ways. For example, you are able to list zettel, to create new zettel, to edit them, or to delete them. You can view zettel details and relations between zettel. In addition, Zettelstore provides an ""application programming interface"" (API) that allows other software to communicate with the Zettelstore. 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. 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]]. If you create a new zettel via the web interface or the API, the zettel identifier will be the timestamp of the current date and time (format is ''YYYYMMDDhhmmss''). This allows zettel to be sorted naturally by creation time. |
︙ | ︙ | |||
44 45 46 47 48 49 50 | For example, you want to store an important figure in the Zettelstore that is encoded as a ''.png'' file. Since each zettel contains some metadata, e.g. the title of the figure, the question arises where these data should be stores. 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. | | | | > > > > > > > > > > > > | 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 | For example, you want to store an important figure in the Zettelstore that is encoded as a ''.png'' file. Since each zettel contains some metadata, e.g. the title of the figure, the question arises where these data should be stores. 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""). === 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. 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. 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. If such a file was not found, the internal zettel store is searched secondly. 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""? |
Changes to docs/manual/00001005090000.zettel.
1 2 3 4 5 | id: 00001005090000 title: List of predefined zettel role: manual tags: #manual #reference #zettelstore syntax: zmk | | | | > | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 | id: 00001005090000 title: List of predefined zettel role: manual tags: #manual #reference #zettelstore syntax: zmk modified: 20210622124647 The following table lists all predefined zettel with their purpose. |= Identifier :|= Title | Purpose | [[00000000000001]] | Zettelstore Version | Contains the version string of the running Zettelstore | [[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 | [[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]] | [[00000000010300]] | Zettelstore List Meta HTML Template | Used when displaying a list of zettel | [[00000000010401]] | Zettelstore Detail HTML Template | Layout for the HTML detail view of one zettel | [[00000000010402]] | Zettelstore Info HTML Templöate | Layout for the information view of a specific zettel | [[00000000010403]] | Zettelstore Form HTML Template | Form that is used to create a new or to change an existing zettel that contains text | [[00000000010404]] | Zettelstore Rename Form HTML Template | View that is displayed to change the [[zettel identifier|00001006050000]] | [[00000000010405]] | Zettelstore Delete HTML Template | View to confirm the deletion of a zettel | [[00000000010500]] | Zettelstore List Roles HTML Template | Layout for listing all roles | [[00000000010600]] | Zettelstore List Tags HTML Template | Layout of tags lists | [[00000000020001]] | Zettelstore Base CSS | System-defined CSS file that is included by the [[Base HTML Template|00000000010100]] | [[00000000025001]] | Zettelstore User CSS | User-defined CSS file that is included by the [[Base HTML Template|00000000010100]] | [[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. |
Changes to docs/manual/00001006020000.zettel.
1 2 3 4 5 6 7 8 9 10 11 12 | id: 00001006020000 title: Supported Metadata Keys role: manual tags: #manual #meta #reference #zettel #zettelstore syntax: zmk 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]]. ; [!back]''back'' | > | 1 2 3 4 5 6 7 8 9 10 11 12 13 | 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]]. ; [!back]''back'' |
︙ | ︙ | |||
44 45 46 47 48 49 50 51 52 53 54 55 | : Date and time when a zettel was modified through Zettelstore. If you edit a zettel with an editor software outside Zettelstore, you should set it manually to an appropriate value. This is a computed value. There is no need to set it via Zettelstore. ; [!no-index]''no-index'' : If set to true, the zettel will not be indexed and therefore not be found in full-text searches. ; [!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. | > > > | | | 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 | : Date and time when a zettel was modified through Zettelstore. If you edit a zettel with an editor software outside Zettelstore, you should set it manually to an appropriate value. This is a computed value. There is no need to set it via Zettelstore. ; [!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. 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 is a computed value. There is no need to set it via Zettelstore. ; [!read-only]''read-only'' : Marks a zettel as read-only. The interpretation of [[supported values|00001006020400]] for this key depends, whether authentication is [[enabled|00001010040100]] or not. ; [!role]''role'' |
︙ | ︙ |
Changes to docs/manual/00001006020400.zettel.
︙ | ︙ | |||
12 13 14 15 16 17 18 | === No authentication 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. | | | | 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 | === No authentication 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]], 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. If the metadata value is the same as an explicit [[user role|00001010070300]], users with that role (or a role with lower rights) are not allowed to modify the zettel. ; ""reader"" : Neither an unauthenticated user nor a user with role ""reader"" is allowed to modify the zettel. Users with role ""writer"" or the owner itself still can modify the zettel. ; ""writer"" : 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]], the zettel could be modified using an external editor. Typically the owner of a Zettelstore have such an access. |
Changes to docs/manual/00001006030000.zettel.
1 2 3 4 5 6 | id: 00001006030000 title: Supported Key Types role: manual tags: #manual #meta #reference #zettel #zettelstore syntax: zmk | > | < | > > > > > > | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 | 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]] 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. * [[Boolean|00001006030500]] |
︙ | ︙ |
Changes to docs/manual/00001006050000.zettel.
1 2 3 4 | id: 00001006050000 title: Zettel identifier tags: #design #manual #zettelstore syntax: zmk | > | | > | | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 | id: 00001006050000 title: Zettel identifier role: manual tags: #design #manual #zettelstore syntax: zmk modified: 20210721123222 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. Every zettel identifier consists of 14 digits. They resemble a timestamp: the first four digits could represent the year, the next two represent the month, following by day, hour, minute, and second. This allows to order zettel chronologically in a canonical way. 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''. 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. |
Added docs/manual/00001006055000.zettel.
> > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 | 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 |
Changes to docs/manual/00001007010000.zettel.
︙ | ︙ | |||
20 21 22 23 24 25 26 | Inline elements mostly begins with two non-space, often identical characters. 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. | | | | | 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 | Inline elements mostly begins with two non-space, often identical characters. 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.]. 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}. 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}. 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. * The most important exception to this rule is the specification of lists. * If no block element is found, a paragraph with inline elements is assumed. |
︙ | ︙ |
Changes to docs/manual/00001007030200.zettel.
︙ | ︙ | |||
86 87 88 89 90 91 92 | *# A.3 * B * C ::: Please note that two lists cannot be separated by an empty line. | | | 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 | *# A.3 * B * C ::: Please note that two lists cannot be separated by an empty line. Instead you should put 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] # Uno # Due |
︙ | ︙ |
Changes to docs/manual/00001007031000.zettel.
︙ | ︙ | |||
89 90 91 92 93 94 95 | |123|123|123|123 ::: === 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]]. | | | | | | | 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 | |123|123|123|123 ::: === 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 ``` |=Key | Value | Description |%-----------+--------+--------------------------- | defdirtype | notify | Default directory box type ``` This is rendered in HTML as: :::example |=Key | Value | Description |%-----------+--------+--------------------------- | defdirtype | notify | Default directory box type ::: |
Changes to docs/manual/00001007040000.zettel.
︙ | ︙ | |||
43 44 45 46 47 48 49 | They will be considered equivalent to tags in metadata. ==== 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''). | | | | 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 | They will be considered equivalent to tags in metadata. ==== 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. 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. 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. It is typically used in numeric ranges. ``pages 4--7`` will be rendered in HTML as: ::pages 4--7::{=example}. Alternative specifications are: ``–``, ``&x8211``, and ``–``. * Three consecutive full stop characters (""''.''"", ''U+002E'') after a space result in an horizontal ellipsis character. ``to be continued ... later`` will be rendered in HTML as: ::to be continued, ... later::{=example}. Alternative specifications are: ``…``, ``&x8230``, and ``…``. |
Changes to docs/manual/00001008000000.zettel.
1 2 3 4 5 | id: 00001008000000 title: Other Markup Languages role: manual tags: #manual #zettelstore syntax: zmk | | | 1 2 3 4 5 6 7 8 9 10 11 12 13 | id: 00001008000000 title: Other Markup Languages role: manual tags: #manual #zettelstore syntax: zmk modified: 20210705111758 [[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: * CSS |
︙ | ︙ | |||
21 22 23 24 25 26 27 28 29 30 31 32 | The following syntax values are supported: ; [!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. ; [!markdown]''markdown'', [!md]''md'' : For those who desperately need [[Markdown|https://daringfireball.net/projects/markdown/]]. Since the world of Markdown is so diverse, a [[CommonMark|https://commonmark.org/]] parser is used. See [[Use Markdown within Zettelstore|00001008010000]]. ; [!mustache]''mustache'' | > > > > > > | | 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 | The following syntax values are supported: ; [!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 ``<script ...`` tag. See [[security aspects of Markdown|00001008010000#security-aspects]] for some details. ; [!markdown]''markdown'', [!md]''md'' : For those who desperately need [[Markdown|https://daringfireball.net/projects/markdown/]]. Since the world of Markdown is so diverse, a [[CommonMark|https://commonmark.org/]] parser is used. See [[Use Markdown within Zettelstore|00001008010000]]. ; [!mustache]''mustache'' : A [[Mustache template|https://mustache.github.io/]], used when rendering a zettel as HTML for the [[web user interface|00001014000000]]. ; [!none]''none'' : Only the metadata of a zettel is ""parsed"". Useful for displaying the full metadata. The [[runtime configuration zettel|00000000000100]] uses this syntax. The zettel content is ignored. ; [!svg]''svg'' : A [[Scalable Vector Graphics|https://www.w3.org/TR/SVG2/]]. |
︙ | ︙ |
Changes to docs/manual/00001010070300.zettel.
1 2 3 4 5 6 7 8 9 10 | id: 00001010070300 title: User roles role: manual tags: #authorization #configuration #manual #security #zettelstore syntax: zmk Every user is associated with some basic privileges. These are specified in the user zettel with the key ''user-role''. The following values are supported: | > | | > > > | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 | id: 00001010070300 title: User roles role: manual tags: #authorization #configuration #manual #security #zettelstore syntax: zmk modified: 20210702165501 Every user is associated with some basic privileges. These are specified in the user zettel with the key ''user-role''. The following values are supported: ; [!reader]""reader"" : The user is allowed to read zettel. This is the default value for any user except the owner of the Zettelstore. ; [!writer]""writer"" : The user is allowed to create new zettel and to change existing zettel. ; [!creator]""creator"" : The user is only allowed to create new zettel. It is also allowed to change its own user zettel. There are two other user roles, implicitly defined: ; The anonymous user : This role is assigned to any user that is not authenticated. Can only read zettel with visibility [[public|00001010070200]], but cannot change them. ; The owner : The user that is configured to be the owner of the Zettelstore. Does not need to specify a user role in its user zettel. Is not restricted in the use of Zettelstore, except when a zettel is marked as [[read-only|00001006020400]]. |
Changes to docs/manual/00001010070600.zettel.
1 2 3 4 | id: 00001010070600 title: Access rules tags: #authorization #configuration #manual #security #zettelstore syntax: zmk | > | > | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 | id: 00001010070600 title: Access rules role: manual tags: #authorization #configuration #manual #security #zettelstore syntax: zmk modified: 20210702165416 Whether an operation of the Zettelstore is allowed or rejected, depends on various factors. The following rules are checked first, in this order: # In read-only mode, every operation except the ""Read"" operation is rejected. # If there is no owner, authentication is disabled and every operation is allowed for everybody. # If the user is authenticated and it is the owner, then the operation is allowed. In the second step, when authentication is enabled and the requesting user is not the owner, everything depends on the requested operation. * Read a zettel: ** If the visibility is ""public"", the access is granted. ** If the visibility is ""owner"", the access is rejected. ** If the user is not authenticated, access is rejected. ** If the zettel requested is an user zettel, reject the access if the users identification is not the same as of the ''ident'' meta key in the zettel. In other words: only the requesting user is allowed to access its own user zettel. ** If the ''user-role'' of the user is ""creator"", reject the access. ** Otherwise the user is authenticated, no sensitive zettel is requested. Allow to read the zettel. * Create a new zettel ** If the user is not authenticated, reject the access. ** If the ''user-role'' of the user is ""reader"", reject the access. ** If the user tries to create an user zettel, the access is rejected. |
︙ | ︙ |
Changes to docs/manual/00001012000000.zettel.
1 2 3 4 5 6 7 8 9 10 11 12 | id: 00001012000000 title: API role: manual tags: #api #manual #zettelstore syntax: zmk The API (short for ""**A**pplication **P**rogramming **I**nterface"") is the primary way to communicate with a running Zettelstore. Most integration with other systems and services is done through the API. The [[web user interface|00001014000000]] is just an alternative, secondary way of interacting with a Zettelstore. === Background The API is HTTP-based and uses JSON as its main encoding format for exchanging messages between a Zettelstore and its client software. | > | 1 2 3 4 5 6 7 8 9 10 11 12 13 | id: 00001012000000 title: API role: manual tags: #api #manual #zettelstore syntax: zmk modified: 20210721120820 The API (short for ""**A**pplication **P**rogramming **I**nterface"") is the primary way to communicate with a running Zettelstore. Most integration with other systems and services is done through the API. The [[web user interface|00001014000000]] is just an alternative, secondary way of interacting with a Zettelstore. === Background The API is HTTP-based and uses JSON as its main encoding format for exchanging messages between a Zettelstore and its client software. |
︙ | ︙ | |||
26 27 28 29 30 31 32 | * [[Renew an access token|00001012050400]] without costly re-authentication * [[Provide an access token|00001012050600]] when doing an API call === Zettel lists * [[List metadata of all zettel|00001012051200]] * [[List all zettel, but in different encoding formats|00001012051400]] * [[List all zettel, but include different parts of a zettel|00001012051600]] | | > > > > | | | | | | 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 | * [[Renew an access token|00001012050400]] without costly re-authentication * [[Provide an access token|00001012050600]] when doing an API call === Zettel lists * [[List metadata of all zettel|00001012051200]] * [[List all zettel, but in different encoding formats|00001012051400]] * [[List all zettel, but include different parts of a zettel|00001012051600]] * [[Shape the list of zettel metadata|00001012051800]] ** [[Selection of zettel|00001012051810]] ** [[Zettel parts|00001012051820]] ** [[Limit the list length|00001012051830]] ** [[Content search|00001012051840]] ** [[Sort the list of zettel metadata|00001012052000]] * [[List all tags|00001012052200]] * [[List all roles|00001012052400]] === Working with zettel * [[Create a new zettel|00001012053200]] * [[Retrieve metadata and content of an existing zettel|00001012053400]] * [[Retrieve references of an existing zettel|00001012053600]] * [[Retrieve context of an existing zettel|00001012053800]] * [[Retrieve zettel order within an existing zettel|00001012054000]] * [[Update metadata and content of a zettel|00001012054200]] * [[Rename a zettel|00001012054400]] * [[Delete a zettel|00001012054600]] |
Changes to docs/manual/00001012050200.zettel.
1 2 3 4 5 6 7 8 9 10 | id: 00001012050200 title: API: Authenticate a client role: manual tags: #api #manual #zettelstore syntax: zmk Authentication for future API calls is done by sending a [[user identification|00001010040200]] and a password to the Zettelstore to obtain an [[access token|00001010040700]]. This token has to be used for other API calls. It is valid for a relatively short amount of time, as configured with the key ''token-timeout-api'' of the [[startup configuration|00001004010000]] (typically 10 minutes). | > | | | | | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 | id: 00001012050200 title: API: Authenticate a client role: manual tags: #api #manual #zettelstore syntax: zmk modified: 20210712221945 Authentication for future API calls is done by sending a [[user identification|00001010040200]] and a password to the Zettelstore to obtain an [[access token|00001010040700]]. This token has to be used for other API calls. It is valid for a relatively short amount of time, as configured with the key ''token-timeout-api'' of the [[startup configuration|00001004010000]] (typically 10 minutes). The simplest way is to send user identification (''IDENT'') and password (''PASSWORD'') via [[HTTP Basic Authentication|https://tools.ietf.org/html/rfc7617]] and send them to the [[endpoint|00001012920000]] ''/v''[^The endpoint ''/a'' is already taken for the [[web user interface|00001014000000]], ""v"" stands for the German word ""verbürgen""] with a POST request: ```sh # curl -X POST -u IDENT:PASSWORD http://127.0.0.1:23123/v {"access_token":"eyJhbGciOiJIUzUxMiJ9.eyJfdGsiOjEsImV4cCI6MTYwMTczMTI3NSwiaWF0IjoxNjAxNzMwNjc1LCJzdWIiOiJhYmMiLCJ6aWQiOiIyMDIwMTAwMzE1MDEwMCJ9.ekhXkvn146P2bMKFQcU-bNlvgbeO6sS39hs6U5EKfjIqnSInkuHYjYAIfUqf_clYRfr6YBlX5izii8XfxV8jhg","token_type":"Bearer","expires_in":600} ``` Some tools, like [[curl|https://curl.haxx.se/]], also allow to specify user identification and password as part of the URL: ```sh # curl -X POST http://IDENT:PASSWORD@127.0.0.1:23123/v {"access_token":"eyJhbGciOiJIUzUxMiJ9.eyJfdGsiOjEsImV4cCI6MTYwMTczMTI3NSwiaWF0IjoxNjAxNzMwNjc1LCJzdWIiOiJhYmMiLCJ6aWQiOiIyMDIwMTAwMzE1MDEwMCJ9.ekhXkvn146P2bMKFQcU-bNlvgbeO6sS39hs6U5EKfjIqnSInkuHYjYAIfUqf_clYRfr6YBlX5izii8XfxV8jhg","token_type":"Bearer","expires_in":600} ``` If you do not want to use Basic Authentication, you can also send user identification and password as HTML form data: ```sh # curl -X POST -d 'username=IDENT&password=PASSWORD' http://127.0.0.1:23123/v {"access_token":"eyJhbGciOiJIUzUxMiJ9.eyJfdGsiOjEsImV4cCI6MTYwMTczMTI3NSwiaWF0IjoxNjAxNzMwNjc1LCJzdWIiOiJhYmMiLCJ6aWQiOiIyMDIwMTAwMzE1MDEwMCJ9.ekhXkvn146P2bMKFQcU-bNlvgbeO6sS39hs6U5EKfjIqnSInkuHYjYAIfUqf_clYRfr6YBlX5izii8XfxV8jhg","token_type":"Bearer","expires_in":600} ``` In all cases, you will receive an JSON object will all [[relevant data|00001012921000]] to be used for further API calls. **Important:** obtaining a token is a time-intensive process. Zettelstore will delay every request to obtain a token for a certain amount of time. |
︙ | ︙ |
Changes to docs/manual/00001012050400.zettel.
1 2 3 4 5 6 7 8 9 | id: 00001012050400 title: API: Renew an access token role: manual tags: #api #manual #zettelstore syntax: zmk An access token is only valid for a certain duration. Since the [[authentication process|00001012050200]] will need some processing time, there is a way to renew the token without providing full authentication data. | > | | | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 | id: 00001012050400 title: API: Renew an access token role: manual tags: #api #manual #zettelstore syntax: zmk modified: 20210712222135 An access token is only valid for a certain duration. Since the [[authentication process|00001012050200]] will need some processing time, there is a way to renew the token without providing full authentication data. Send a HTTP PUT request to the [[endpoint|00001012920000]] ''/v'' and include the current access token in the ''Authorization'' header: ```sh # curl -X PUT -H 'Authorization: Bearer TOKEN' http://127.0.0.1:23123/v {"access_token":"eyJhbGciOiJIUzUxMiJ9.eyJfdGsiOjEsImV4cCI6MTYwMTczMTI3NSwiaWF0IjoxNjAxNzMwNjc1LCJzdWIiOiJhYmMiLCJ6aWQiOiIyMDIwMTAwMzE1MDEwMCJ9.ekhXkvn146P2bMKFQcU-bNlvgbeO6sS39hs6U5EKfjIqnSInkuHYjYAIfUqf_clYRfr6YBlX5izii8XfxV8jhg","token_type":"Bearer","expires_in":456} ``` You may receive a new access token, or the current one if it was obtained not a long time ago. However, the lifetime of the returned [[access token|00001012921000]] is accurate. === HTTP Status codes ; ''200'' |
︙ | ︙ |
Changes to docs/manual/00001012051800.zettel.
1 | id: 00001012051800 | | | < < < < < | < < < < < | < < < < < < < < < < | < < < < < < < < < < < < < < < < | < < < < < < < < < < < | < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 | id: 00001012051800 title: API: Shape the list of zettel metadata role: manual tags: #api #manual #zettelstore syntax: zmk modified: 20210721120658 In most cases, it is not essential to list //all// zettel. Typically, you are interested only in a subset of the zettel maintained by your Zettelstore. This is done by adding some query parameters to the general ''GET /z'' request. * [[Select|00001012051810]] just some zettel, based on metadata. * You can specify which [[parts of a zettel|00001012051820]] must be returned. * Only a specific amount of zettel will be selected by specifying [[a length and/or an offset|00001012051830]]. * [[Searching for specific content|00001012051840]], not just the metadata, is another way of selecting some zettel. * The resulting list can be [[sorted|00001012052000]] according to various criteria. |
Added docs/manual/00001012051810.zettel.
> > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 | id: 00001012051810 title: API: Select zettel based on their metadata role: manual tags: #api #manual #zettelstore syntax: zmk modified: 20210721121038 Every query parameter that does //not// begin with the low line character (""_"", ''U+005F'') is treated as the name of a [[metadata|00001006010000]] key. According to the [[type|00001006030000]] of a metadata key, zettel are possibly selected. All [[supported|00001006020000]] metadata keys have a well-defined type. User-defined keys have the type ''e'' (string, possibly empty). For example, if you want to retrieve all zettel that contain the string ""API"" in its title, your request will be: ```sh # curl 'http://127.0.0.1:23123/z?title=API' {"list":[{"id":"00001012921000","url":"/z/00001012921000","meta":{"title":"API: JSON structure of an access token","tags":"#api #manual #reference #zettelstore","syntax":"zmk","role":"manual"}},{"id":"00001012920500","url":"/z/00001012920500","meta":{"title":"Formats available by the API","tags":"#api #manual #reference #zettelstore","syntax":"zmk","role":"manual"}},{"id":"00001012920000","url":"/z/00001012920000","meta":{"title":"Endpoints used by the API","tags":"#api #manual #reference #zettelstore","syntax":"zmk","role":"manual"}}, ... ``` However, if you want all zettel that does //not// match a given value, you must prefix the value with the exclamation mark character (""!"", ''U+0021''). For example, if you want to retrieve all zettel that do not contain the string ""API"" in their title, your request will be: ```sh # curl 'http://127.0.0.1:23123/z?title=!API' {"list":[{"id":"00010000000000","url":"/z/00010000000000","meta":{"back":"00001003000000 00001005090000","backward":"00001003000000 00001005090000","copyright":"(c) 2020-2021 by Detlef Stern <ds@zettelstore.de>","forward":"00000000000001 00000000000003 00000000000096 00000000000100","lang":"en","license":"EUPL-1.2-or-later","role":"zettel","syntax":"zmk","title":"Home"}},{"id":"00001014000000","url":"/z/00001014000000","meta":{"back":"00001000000000 00001004020000 00001012920510","backward":"00001000000000 00001004020000 00001012000000 00001012920510","copyright":"(c) 2020-2021 by Detlef Stern <ds@zettelstore.de>","forward":"00001012000000","lang":"en","license":"EUPL-1.2-or-later","published":"00001014000000","role":"manual","syntax":"zmk","tags":"#manual #webui #zettelstore","title":"Web user interface"}}, ... ``` In both cases, an implicit precondition is that the zettel must contain the given metadata key. For a metadata key like [[''title''|00001006020000#title]], which has a default value, this precondition should always be true. But the situation is different for a key like [[''url''|00001006020000#url]]. Both ``curl 'http://localhost:23123/z?url='`` and ``curl 'http://localhost:23123/z?url=!'`` may result in an empty list. The empty query parameter values matches all zettel that contain the given metadata key. Similar, if you specify just the exclamation mark character as a query parameter value, only those zettel match that does //not// contain the given metadata key. This is in contrast to above rule that the metadata value must exist before a match is done. For example ``curl 'http://localhost:23123/z?back=!&backward='`` returns all zettel that are reachable via other zettel, but also references these zettel. Above example shows that all sub-expressions of a select specification must be true so that no zettel is rejected from the final list. If you specify the query parameter ''_negate'', either with or without a value, the whole selection will be negated. Because of the precondition described above, ``curl 'http://127.0.0.1:23123/z?url=!com'`` and ``curl 'http://127.0.0.1:23123/z?url=com&_negate'`` may produce different lists. The first query produces a zettel list, where each zettel does have a ''url'' metadata value, which does not contain the characters ""com"". The second query produces a zettel list, that excludes any zettel containing a ''url'' metadata value that contains the characters ""com""; this also includes all zettel that do not contain the metadata key ''url''. |
Added docs/manual/00001012051820.zettel.
> > > > > > > > > > > > > > > > > > > > > > > > > | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 | id: 00001012051820 title: API: Shape the list of zettel metadata by returning specific parts of a zettel role: manual tags: #api #manual #zettelstore syntax: zmk modified: 20210721120743 === Basic usage If you are just interested in the zettel identifier, you should add the [[''_part''|00001012920800]] query parameter: ```sh # curl 'http://127.0.0.1:23123/z?title=API&_part=id' {"list":[{"id":"00001012921000","url":"/z/00001012921000"},{"id":"00001012920500","url":"/z/00001012920500"},{"id":"00001012920000","url":"/z/00001012920000"},{"id":"00001012051800","url":"/z/00001012051800"},{"id":"00001012051600","url":"/z/00001012051600"},{"id":"00001012051400","url":"/z/00001012051400"},{"id":"00001012051200","url":"/z/00001012051200"},{"id":"00001012050600","url":"/z/00001012050600"},{"id":"00001012050400","url":"/z/00001012050400"},{"id":"00001012050200","url":"/z/00001012050200"},{"id":"00001012000000","url":"/z/00001012000000"}]} ``` === Combined usage with select options If you want only those zettel that additionally must contain the string ""JSON"", you have to add an additional query parameter: ```sh # curl 'http://127.0.0.1:23123/z?title=API&_part=id&title=JSON' {"list":[{"id":"00001012921000","url":"/z/00001012921000"}]} ``` Similarly, if you add another query parameter, the intersection of both results is returned: ```sh # curl 'http://127.0.0.1:23123/z?title=API&_part=id&id=00001012050' {"list":[{"id":"00001012050600","url":"/z/00001012050600"},{"id":"00001012050400","url":"/z/00001012050400"},{"id":"00001012050200","url":"/z/00001012050200"}]} ``` |
Added docs/manual/00001012051830.zettel.
> > > > > > > > > > > > > > > > > > > | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 | id: 00001012051830 title: API: Shape the list of zettel metadata by limiting its length role: manual tags: #api #manual #zettelstore syntax: zmk === Limit By using the query parameter ""''_limit''"", which must have an integer value, you specifying an upper limit of list elements: ```sh # curl 'http://192.168.17.7:23121/z?title=API&_part=id&_sort=id&_limit=2' {"list":[{"id":"00001012000000","url":"/z/00001012000000"},{"id":"00001012050200","url":"/z/00001012050200"}]} ``` === Offset The query parameter ""''_offset''"" allows to list not only the first elements, but to begin at a specific element: ```sh # curl 'http://192.168.17.7:23121/z?title=API&_part=id&_sort=id&_limit=2&_offset=1' {"list":[{"id":"00001012050200","url":"/z/00001012050200"},{"id":"00001012050400","url":"/z/00001012050400"}]} ``` |
Added docs/manual/00001012051840.zettel.
> > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 | id: 00001012051840 title: API: Shape the list of zettel metadata by searching the content role: manual tags: #api #manual #zettelstore syntax: zmk The query parameter ""''_s''"" allows to provide a string for a full-text search of all zettel. The search string will be normalized according to Unicode NKFD, ignoring everything except letters and numbers. If the search string starts with the character ""''!''"", it will be removed and the query matches all zettel that **do not match** the search string. In the next step, the first character of the search string will be inspected. If it contains one of the characters ""'':''"", ""''=''"", ""''>''"", or ""''~''"", this will modify how the search will be performed. The character will be removed from the start of the search string. For example, assume the search string is ""def"": ; ""'':''"", ""''~''"" (or none of these characters)[^""'':''"" is always the character for specifying the default comparison. In this case, it is equal to ""''~''"". If you omit a comparison character, the default comparison is used.] : The zettel must contain a word that contains the search string. ""def"", ""defghi"", and ""abcdefghi"" are matching the search string. ; ""''=''"" : The zettel must contain a word that is equal to the search string. Only the word ""def"" matches the search string. ; ""''>''"" : The zettel must contain a word with the search string as a prefix. A word like ""def"" or ""defghi"" matches the search string. If you want to include an initial ""''!''"" into the search string, you must prefix that with the escape character ""''\\''"". For example ""\\!abc"" will search for zettel that contains the string ""!abc"". A similar rule applies to the characters that specify the way how the search will be done. For example, ""!\\=abc"" will search for zettel that do not contains the string ""=abc"". You are allowed to specify this query parameter more than once. All results will be intersected, i.e. a zettel will be included into the list if all of the provided values match. This parameter loosely resembles the search box of the web user interface. |
Changes to docs/manual/00001012052000.zettel.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 | id: 00001012052000 title: API: Sort the list of zettel metadata role: manual tags: #api #manual #zettelstore syntax: zmk If not specified, the list of zettel is sorted descending by the value of the zettel identifier. The highest zettel identifier, which is a number, comes first. You change that with the ""''_sort''"" query parameter. Alternatively, you can also use the ""''_order''"" query parameter. It is an alias. Its value is the name of a metadata key, optionally prefixed with a hyphen-minus character (""-"", ''U+002D''). According to the [[type|00001006030000]] of a metadata key, the list of zettel is sorted. If hyphen-minus is given, the order is descending, else ascending. If you want a random list of zettel, specify the value ""_random"" in place of the metadata key. ""''_sort=_random''"" (or ""''_order=_random''"") is the query parameter in this case. | > | | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 | id: 00001012052000 title: API: Sort the list of zettel metadata role: manual tags: #api #manual #zettelstore syntax: zmk modified: 20210709162242 If not specified, the list of zettel is sorted descending by the value of the zettel identifier. The highest zettel identifier, which is a number, comes first. You change that with the ""''_sort''"" query parameter. Alternatively, you can also use the ""''_order''"" query parameter. It is an alias. Its value is the name of a metadata key, optionally prefixed with a hyphen-minus character (""-"", ''U+002D''). According to the [[type|00001006030000]] of a metadata key, the list of zettel is sorted. If hyphen-minus is given, the order is descending, else ascending. If you want a random list of zettel, specify the value ""_random"" in place of the metadata key. ""''_sort=_random''"" (or ""''_order=_random''"") is the query parameter in this case. If can be combined with ""[[''_limit=1''|00001012051830]]"" to obtain just one random zettel. Currently, only the first occurrence of ''_sort'' is recognized. In the future it will be possible to specify a combined sort key. |
Added docs/manual/00001012053200.zettel.
> > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 | id: 00001012053200 title: API: Create a new zettel role: manual tags: #api #manual #zettelstore syntax: zmk modified: 20210713163927 A zettel is created by adding it to the [[list of zettel|00001012000000#zettel-lists]]. Therefore, the [[endpoint|00001012920000]] to create a new zettel is also ''/z'', but you must send the data of the new zettel via a HTTP POST request. The body of the POST request must contain a JSON object that specifies metadata and content of the zettel to be created. The following keys of the JSON object are used: ; ''"meta"'' : References an embedded JSON object with only string values. The name/value pairs of this objects are interpreted as the metadata of the new zettel. Please consider the [[list of supported metadata keys|00001006020000]] (and their value types). ; ''"encoding"'' : States how the content is encoded. Currently, only two values are allowed: the empty string (''""'') that specifies an empty encoding, and the string ''"base64"'' that specifies the [[standard Base64 encoding|https://www.rfc-editor.org/rfc/rfc4648.txt]]. Other values will result in a HTTP response status code ''400''. ; ''"content"'' : Is a string value that contains the content of the zettel to be created. Typically, text content is not encoded, and binary content is encoded via Base64. Other keys will be ignored. Even these three keys are just optional. The body of the HTTP POST request must not be empty and it must contain a JSON object. Therefore, a body containing just ''{}'' is perfectly valid. The new zettel will have no content, its title will be set to the value of [[''default-title''|00001004020000#default-title]] (default: ""Untitled""), its role is set to the value of [[''default-role''|00001004020000#default-role]] (default: ""zettel""), and its syntax is set to the value of [[''default-syntax''|00001004020000#default-syntax]] (default: ""zmk""). ``` # curl -X POST --data '{}' http://127.0.0.1:23123/z {"id":"20210713161000","url":"/z/20210713161000"} ``` If creating the zettel was successful, the HTTP response will contain a JSON object with two keys: ; ''"id"'' : Contains the zettel identifier of the created zettel for further usage. ; ''"url"'' : The URL for [[reading metadata and content|00001012053400]] of the new zettel. In most cases, the URL is a relative one. A client must prepend the HTTP protocol scheme, the host name, and (optional, but often needed) the post number to make it an absolute URL. In addition, the HTTP response header contains a key ''Location'' with the same value of the relative URL. As an example, a zettel with title ""Note"" and content ""Important content."" can be created by issuing: ``` # curl -X POST --data '{"meta":{"title":"Note"},"content":"Important content."}' http://127.0.0.1:23123/z {"id":"20210713163100","url":"/z/20210713163100"} ``` === HTTP Status codes ; ''201'' : Zettel creation was successful, the body contains a JSON object that contains its zettel identifier. ; ''400'' : Request was not valid. There are several reasons for this. Most likely, the JSON was not formed according to above rules. ; ''403'' : You are not allowed to create a new zettel. |
Changes to docs/manual/00001012053600.zettel.
1 2 3 4 | id: 00001012053600 title: API: Retrieve references of an existing zettel tags: #api #manual #zettelstore syntax: zmk | > | | < < < | < < < < < | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 | id: 00001012053600 title: API: Retrieve references of an existing zettel role: manual tags: #api #manual #zettelstore syntax: zmk modified: 20210702183357 The web of zettel is one important value of a Zettelstore. Many zettel references other zettel, images, external/local material or, via citations, external literature. By using the [[endpoint|00001012920000]] ''/l/{ID}'' you are able to retrieve these references. ```` # curl http://127.0.0.1:23123/l/00001012053600 {"id":"00001012053600","url":"/z/00001012053600","links":{"outgoing":[{"id":"00001012920000","url":"/z/00001012920000"},{"id":"00001007040300","url":"/z/00001007040300#links"},{"id":"00001007040300","url":"/z/00001007040300#images"},{"id":"00001007040300","url":"/z/00001007040300#citation-key"}]},"images":{}} ```` Formatted, this translates into: ````json { "id": "00001012053600", "url": "/z/00001012053600", "links": { "outgoing": [ { "id": "00001012920000", "url": "/z/00001012920000" }, { "id": "00001007040300", "url": "/z/00001007040300#links" }, { "id": "00001007040300", "url": "/z/00001007040300#images" }, { "id": "00001007040300", "url": "/z/00001007040300#citation-key" } ], }, "images": {}, } ```` === Kind The following to-level JSON keys are returned: ; ''id'' : The zettel identifier for which the references were requested. ; ''url'' |
︙ | ︙ | |||
80 81 82 83 84 85 86 87 | This is controlled by the value of the query parameter ''matter'': |= ''matter''| Number >| Returned reference list | (nothing) | (30) | incoming, outgoing, local, and external references | ''incoming'' | 2|incoming reference, not allowed for images (aka ""backlinks"", not yet implemented) | ''outgoing'' | 4|outgoing references | ''local'' | 8|local references, i.e. local, non-zettel material | ''external'' |16| external references, i.e. on the web | ''zettel'' | (6)|incoming and outgoing references | > | | | 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 | This is controlled by the value of the query parameter ''matter'': |= ''matter''| Number >| Returned reference list | (nothing) | (30) | incoming, outgoing, local, and external references | ''incoming'' | 2|incoming reference, not allowed for images (aka ""backlinks"", not yet implemented) | ''outgoing'' | 4|outgoing references | ''local'' | 8|local references, i.e. local, non-zettel material | ''external'' |16| external references, i.e. on the web | ''meta'' |32| external reference, stored in metadata | ''zettel'' | (6)|incoming and outgoing references | ''material'' |(56)| local and external references | ''all'' | (62)| incoming, outgoing, local, and external references Incoming and outgoing references are basically zettel. Therefore the list elements are JSON objects with keys ''id'' and ''url''. Local and external references are strings. Similar to the ''kind'' query parameter, each matter is associated with a number. To retrieve a combination of matter values that does not have a name, just add the numbers. |
︙ | ︙ |
Changes to docs/manual/00001012053800.zettel.
1 2 3 4 5 6 7 8 9 10 | id: 00001012053800 title: API: Retrieve context of an existing zettel role: manual tags: #api #manual #zettelstore syntax: zmk The context of an origin zettel consists of those zettel that are somehow connected to the origin zettel. Direct connections of an origin zettel to other zettel are visible via [[metadata values|00001006020000]], such as ''backward'', ''forward'' or other values with type [[identifier|00001006032000]] or [[set of identifier|00001006032500]]. Zettel are also connected by using same [[tags|00001006020000#tags]]. | > | | | | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 | id: 00001012053800 title: API: Retrieve context of an existing zettel role: manual tags: #api #manual #zettelstore syntax: zmk modified: 20210712223623 The context of an origin zettel consists of those zettel that are somehow connected to the origin zettel. Direct connections of an origin zettel to other zettel are visible via [[metadata values|00001006020000]], such as ''backward'', ''forward'' or other values with type [[identifier|00001006032000]] or [[set of identifier|00001006032500]]. Zettel are also connected by using same [[tags|00001006020000#tags]]. The context is defined by a //direction//, a //depth//, and a //limit//: * Direction: connections are directed. For example, the metadata value of ''backward'' lists all zettel that link to the current zettel, while ''formward'' list all zettel to which the current zettel links. When you are only interested in one direction, set the parameter ''dir'' either to the value ""backward"" or ""forward"". All other values, including a missing value, is interpreted as ""both"". * Depth: a direct connection has depth 1, an indirect connection is the length of the shortest path between two zettel. You should limit the depth by using the parameter ''depth''. Its default value is ""5"". A value of ""0"" does disable any depth check. * Limit: to set an upper bound for the returned context, you should use the parameter ''limit''. Its default value is ""200"". A value of ""0"" disables does not limit the number of elements returned. Zettel with same tags as the origin zettel are considered depth 1. Only for the origin zettel, tags are used to calculate a connection. Currently, only some of the newest zettel with a given tag are considered a connection.[^The number of zettel is given by the value of parameter ''depth''.] Otherwise the context would become too big and therefore unusable. To retrieve the context of an existing zettel, use the [[endpoint|00001012920000]] ''/x/{ID}''[^Mnemonic: conte**X**t]. ```` # curl 'http://127.0.0.1:23123/x/00001012053800?limit=3&dir=forward&depth=2' {"id": "00001012053800","url": "/z/00001012053800","meta": {...},"list": [{"id": "00001012921000","url": "/z/00001012921000","meta": {...}},{"id": "00001012920800","url": "/z/00001012920800","meta": {...}},{"id": "00010000000000","url": "/z/00010000000000","meta": {...}}]} ```` Formatted, this translates into:[^Metadata (key ''meta'') are hidden to make the overall structure easier to read.] ````json { "id": "00001012053800", "url": "/z/00001012053800", |
︙ | ︙ |
Changes to docs/manual/00001012054000.zettel.
1 2 3 4 5 6 7 8 9 10 11 12 | id: 00001012054000 title: API: Retrieve zettel order within an existing zettel role: manual tags: #api #manual #zettelstore syntax: zmk Some zettel act as a ""table of contents"" for other zettel. The [[initial zettel|00001000000000]] of this manual is one example, the [[general API description|00001012000000]] is another. Every zettel with a certain internal structure can act as the ""table of contents"" for others. What is a ""table of contents""? Basically, it is just a list of references to other zettel. | > | 1 2 3 4 5 6 7 8 9 10 11 12 13 | id: 00001012054000 title: API: Retrieve zettel order within an existing zettel role: manual tags: #api #manual #zettelstore syntax: zmk modified: 20210721184434 Some zettel act as a ""table of contents"" for other zettel. The [[initial zettel|00001000000000]] of this manual is one example, the [[general API description|00001012000000]] is another. Every zettel with a certain internal structure can act as the ""table of contents"" for others. What is a ""table of contents""? Basically, it is just a list of references to other zettel. |
︙ | ︙ | |||
25 26 27 28 29 30 31 | {"id":"00001000000000","url":"/z/00001000000000","meta":{...},"list":[{"id":"00001001000000","url":"/z/00001001000000","meta":{...}},{"id":"00001002000000","url":"/z/00001002000000","meta":{...}},{"id":"00001003000000","url":"/z/00001003000000","meta":{...}},{"id":"00001004000000","url":"/z/00001004000000","meta":{...}},...,{"id":"00001014000000","url":"/z/00001014000000","meta":{...}}]} ```` Formatted, this translates into:[^Metadata (key ''meta'') are hidden to make the overall structure easier to read.] ````json { "id": "00001000000000", "url": "/z/00001000000000", | | | 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 | {"id":"00001000000000","url":"/z/00001000000000","meta":{...},"list":[{"id":"00001001000000","url":"/z/00001001000000","meta":{...}},{"id":"00001002000000","url":"/z/00001002000000","meta":{...}},{"id":"00001003000000","url":"/z/00001003000000","meta":{...}},{"id":"00001004000000","url":"/z/00001004000000","meta":{...}},...,{"id":"00001014000000","url":"/z/00001014000000","meta":{...}}]} ```` Formatted, this translates into:[^Metadata (key ''meta'') are hidden to make the overall structure easier to read.] ````json { "id": "00001000000000", "url": "/z/00001000000000", "list": [ { "id": "00001001000000", "url": "/z/00001001000000", "meta": {...} }, { "id": "00001002000000", |
︙ | ︙ |
Added docs/manual/00001012054200.zettel.
> > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 | id: 00001012054200 title: API: Update a zettel role: manual tags: #api #manual #zettelstore syntax: zmk modified: 20210713163606 Updating metadata and content of a zettel is technically quite similar to [[creating a new zettel|00001012053200]]. In both cases you must provide the data for the new or updated zettel in the body of the HTTP request. One difference is the endpoint. The [[endpoint|00001012920000]] to update a zettel is ''/z/{ID}'', where ''{ID}'' is a placeholder for the zettel identifier (14 digits). You must send a HTTP PUT request to that endpoint: ``` # curl -X PUT --data '{}' http://127.0.0.1:23123/z/00001012054200 ``` This will put some empty content and metadata to the zettel you are currently reading. As usual, some metadata will be calculated if it is empty. The body of the HTTP response is empty, if the request was successful. === HTTP Status codes ; ''204'' : Update was successful, there is no body in the response. ; ''400'' : Request was not valid. For example, the request body was not valid. ; ''403'' : You are not allowed to delete the given zettel. ; ''404'' : Zettel not found. You probably used a zettel identifier that is not used in the Zettelstore. |
Added docs/manual/00001012054400.zettel.
> > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 | id: 00001012054400 title: API: Rename a zettel role: manual tags: #api #manual #zettelstore syntax: zmk modified: 20210713163708 Renaming a zettel is effectively just specifying a new identifier for the zettel. Since more than one [[box|00001004011200]] might contain a zettel with the old identifier, the rename operation must success in every relevant box to be overall successful. If the rename operation fails in one box, Zettelstore tries to rollback previous successful operations. As a consequence, you cannot rename a zettel when its identifier is used in a read-only box. This applies to all predefined zettel, for example. The [[endpoint|00001012920000]] to rename a zettel is ''/z/{ID}'', where ''{ID}'' is a placeholder for the zettel identifier (14 digits). You must send a HTTP MOVE request to this endpoint, and you must specify the new zettel identifier as an URL, placed under the HTTP request header key ''Destination''. ``` # curl -X MOVE -H "Destination: 10000000000001" http://127.0.0.1:23123/z/00001000000000 ``` Only the last 14 characters of the value of ''Destination'' are taken into account and those must form an unused zettel identifier. If the value contains less than 14 characters that do not form an unused zettel identifier, the response will contain a HTTP status code ''400''. All other characters, besides those 14 digits, are effectively ignored. However, the value should form a valid URL that could be used later to [[read the content|00001012053400]] of the freshly renamed zettel. === HTTP Status codes ; ''204'' : Rename was successful, there is no body in the response. ; ''400'' : Request was not valid. For example, the HTTP header did not contain a valid ''Destination'' key, or the new identifier is already in use. ; ''403'' : You are not allowed to delete the given zettel. In most cases you have either not enough [[access rights|00001010070600]] or at least one box containing the given identifier operates in read-only mode. ; ''404'' : Zettel not found. You probably used a zettel identifier that is not used in the Zettelstore. === Rationale for the MOVE method HTTP [[standardizes|https://www.rfc-editor.org/rfc/rfc7231.txt]] seven methods. None of them is conceptually close to a rename operation. Everyone is free to ""invent"" some new method to be used in HTTP. To avoid a divergency, there is a [[methods registry|https://www.iana.org/assignments/http-methods/]] that tracks those extensions. The [[HTTP Extensions for Web Distributed Authoring and Versioning (WebDAV)|https://www.rfc-editor.org/rfc/rfc4918.txt]] defines the method MOVE that is quite close to the desired rename operation. In fact, some command line tools use a ""move"" method for renaming files. Therefore, Zettelstore adopts somehow WebDAV's MOVE method and its use of the ''Destination'' HTTP header key. |
Added docs/manual/00001012054600.zettel.
> > > > > > > > > > > > > > > > > > > > > > > > > > | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 | id: 00001012054600 title: API: Delete a zettel role: manual tags: #api #manual #zettelstore syntax: zmk modified: 20210713163722 Deleting a zettel within the Zettelstore is executed on the first [[box|00001004011200]] that contains that zettel. Zettel with the same identifier, but in subsequent boxes remain. If the first box containing the zettel is read-only, deleting that zettel will fail, as well for a Zettelstore in [[read-only mode|00001004010000#read-only-mode]] or if authentication is enabled and the user has no [[access right|00001010070600]] to do so. The [[endpoint|00001012920000]] to delete a zettel is ''/z/{ID}'', where ''{ID}'' is a placeholder for the zettel identifier (14 digits). You must send a HTTP DELETE request to this endpoint: ``` # curl -X DELETE http://127.0.0.1:23123/z/00001000000000 ``` === HTTP Status codes ; ''204'' : Delete was successful, there is no body in the response. ; ''403'' : You are not allowed to delete the given zettel. Maybe you do not have enough access rights, or either the box or Zettelstore itself operate in read-only mode. ; ''404'' : Zettel not found. You probably specified a zettel identifier that is not used in the Zettelstore. |
Changes to docs/manual/00001012920000.zettel.
1 2 3 4 5 | id: 00001012920000 title: Endpoints used by the API role: manual tags: #api #manual #reference #zettelstore syntax: zmk | | | < < | | | | > > | | | | > | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 | id: 00001012920000 title: Endpoints used by the API role: manual tags: #api #manual #reference #zettelstore syntax: zmk modified: 20210712225257 All API endpoints conform to the pattern ''[PREFIX]LETTER[/ZETTEL-ID]'', where: ; ''PREFIX'' : is the URL prefix (default: ''/''), configured via the ''url-prefix'' [[startup configuration|00001004010000]], ; ''LETTER'' : is a single letter that specifies the ressource type, ; ''ZETTEL-ID'' : is an optional 14 digits string that uniquely [[identify a zettel|00001006050000]]. The following letters are currently in use: |= Letter:| Without zettel identifier | With [[zettel identifier|00001006050000]] | Mnemonic | ''l'' | | GET: [[list references|00001012053600]] | **L**inks | ''o'' | | GET: [[list zettel order|00001012054000]] | **O**rder | ''r'' | GET: [[list roles|00001012052400]] | | **R**oles | ''t'' | GET: [[list tags|00001012052200]] || **T**ags | ''v'' | POST: [[client authentication|00001012050200]] | | **V**erbürgen[^German translation for ""authentication"", since ''/a'' is already in use by the [[web user interface|00001014000000]].] | | PUT: [[renew access token|00001012050400]] | | ''x'' | | GET: [[list zettel context|00001012053800]] | Conte**x**t | ''z'' | GET: [[list zettel|00001012051200]] | GET: [[retrieve zettel|00001012053400]] | **Z**ettel | | POST: [[create new zettel|00001012053200]] | PUT: [[update a zettel|00001012054200]] | | | DELETE: [[delete the zettel|00001012054600]] | | | MOVE: [[rename the zettel|00001012054400]] The full URL will contain either the ''http'' oder ''https'' scheme, a host name, and an optional port number. The API examples will assume the ''http'' schema, the local host ''127.0.0.1'', the default port ''23123'', and the default empty ''PREFIX''. Therefore, all URLs in the API documentation will begin with ''http://127.0.0.1:23123''. |
Changes to domain/content.go.
1 2 3 4 5 6 7 8 9 10 11 12 13 | //----------------------------------------------------------------------------- // 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 domain provides domain specific types, constants, and functions. package domain | > > > | > > > | | > > | > | > | > > > > > > > | | | > > > > > > > > > > > > > > > > > > > > > > > > > > > | > > > > > > > > | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 | //----------------------------------------------------------------------------- // 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 domain provides domain specific types, constants, and functions. package domain import ( "encoding/base64" "errors" "unicode/utf8" "zettelstore.de/z/strfun" ) // Content is just the content of a zettel. type Content struct { data string isBinary bool } // NewContent creates a new content from a string. func NewContent(s string) Content { return Content{data: s, isBinary: calcIsBinary(s)} } // Set content to new string value. func (zc *Content) Set(s string) { zc.data = s zc.isBinary = calcIsBinary(s) } // AsString returns the content itself is a string. func (zc *Content) AsString() string { return zc.data } // AsBytes returns the content itself is a byte slice. func (zc *Content) AsBytes() []byte { return []byte(zc.data) } // IsBinary returns true if the content contains non-unicode values or is, // interpreted a text, with a high probability binary content. func (zc *Content) IsBinary() bool { return zc.isBinary } // TrimSpace remove some space character in content, if it is not binary content. func (zc *Content) TrimSpace() { if zc.isBinary { return } zc.data = strfun.TrimSpaceRight(zc.data) } // Encode content for future transmission. func (zc *Content) Encode() (data, encoding string) { if !zc.isBinary { return zc.data, "" } return base64.StdEncoding.EncodeToString([]byte(zc.data)), "base64" } // SetDecoded content to the decoded value of the given string. func (zc *Content) SetDecoded(data, encoding string) error { switch encoding { case "": zc.data = data case "base64": decoded, err := base64.StdEncoding.DecodeString(data) if err != nil { return err } zc.data = string(decoded) default: return errors.New("unknown encoding " + encoding) } zc.isBinary = calcIsBinary(zc.data) return nil } func calcIsBinary(s string) bool { if !utf8.ValidString(s) { return true } l := len(s) for i := 0; i < l; i++ { if s[i] == 0 { return true } } return false } |
Changes to domain/content_test.go.
1 | //----------------------------------------------------------------------------- | | > | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 | //----------------------------------------------------------------------------- // 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 domain_test import ( "testing" "zettelstore.de/z/domain" ) func TestContentIsBinary(t *testing.T) { t.Parallel() td := []struct { s string exp bool }{ {"abc", false}, {"äöü", false}, {"", false}, |
︙ | ︙ |
Changes to domain/id/id.go.
︙ | ︙ | |||
21 22 23 24 25 26 27 | // time stamp of the form "YYYYMMDDHHmmSS" converted to an unsigned integer. // A zettelstore implementation should try to set the last two digits to zero, // e.g. the seconds should be zero, type Zid uint64 // Some important ZettelIDs. // Note: if you change some values, ensure that you also change them in the | | | > | 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 | // time stamp of the form "YYYYMMDDHHmmSS" converted to an unsigned integer. // A zettelstore implementation should try to set the last two digits to zero, // e.g. the seconds should be zero, type Zid uint64 // Some important ZettelIDs. // Note: if you change some values, ensure that you also change them in the // constant box. They are mentioned there literally, because these // constants are not available there. const ( Invalid = Zid(0) // Invalid is a Zid that will never be valid // System zettel VersionZid = Zid(1) HostZid = Zid(2) OperatingSystemZid = Zid(3) LicenseZid = Zid(4) AuthorsZid = Zid(5) DependenciesZid = Zid(6) BoxManagerZid = Zid(20) MetadataKeyZid = Zid(90) StartupConfigurationZid = Zid(96) ConfigurationZid = Zid(100) // WebUI HTML templates are in the range 10000..19999 BaseTemplateZid = Zid(10100) LoginTemplateZid = Zid(10200) ListTemplateZid = Zid(10300) ZettelTemplateZid = Zid(10401) InfoTemplateZid = Zid(10402) FormTemplateZid = Zid(10403) RenameTemplateZid = Zid(10404) DeleteTemplateZid = Zid(10405) ContextTemplateZid = Zid(10406) RolesTemplateZid = Zid(10500) TagsTemplateZid = Zid(10600) ErrorTemplateZid = Zid(10700) // WebUI CSS zettel are in the range 20000..29999 BaseCSSZid = Zid(20001) UserCSSZid = Zid(25001) // WebUI JS zettel are in the range 30000..39999 // WebUI image zettel are in the range 40000..49999 EmojiZid = Zid(40001) // Range 90000...99999 is reserved for zettel templates |
︙ | ︙ |
Changes to domain/id/id_test.go.
︙ | ︙ | |||
14 15 16 17 18 19 20 21 22 23 24 25 26 27 | import ( "testing" "zettelstore.de/z/domain/id" ) func TestIsValid(t *testing.T) { validIDs := []string{ "00000000000001", "00000000000020", "00000000000300", "00000000004000", "00000000050000", "00000000600000", | > | 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 | import ( "testing" "zettelstore.de/z/domain/id" ) func TestIsValid(t *testing.T) { t.Parallel() validIDs := []string{ "00000000000001", "00000000000020", "00000000000300", "00000000004000", "00000000050000", "00000000600000", |
︙ | ︙ |
Changes to domain/id/set_test.go.
︙ | ︙ | |||
15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 | import ( "testing" "zettelstore.de/z/domain/id" ) func TestSetSorted(t *testing.T) { testcases := []struct { set id.Set exp id.Slice }{ {nil, nil}, {id.NewSet(), nil}, {id.NewSet(9, 4, 6, 1, 7), id.Slice{1, 4, 6, 7, 9}}, } for i, tc := range testcases { got := tc.set.Sorted() if !got.Equal(tc.exp) { t.Errorf("%d: %v.Sorted() should be %v, but got %v", i, tc.set, tc.exp, got) } } } func TestSetIntersection(t *testing.T) { testcases := []struct { s1, s2 id.Set exp id.Slice }{ {nil, nil, nil}, {id.NewSet(), nil, nil}, {id.NewSet(), id.NewSet(), nil}, | > > | 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 | import ( "testing" "zettelstore.de/z/domain/id" ) func TestSetSorted(t *testing.T) { t.Parallel() testcases := []struct { set id.Set exp id.Slice }{ {nil, nil}, {id.NewSet(), nil}, {id.NewSet(9, 4, 6, 1, 7), id.Slice{1, 4, 6, 7, 9}}, } for i, tc := range testcases { got := tc.set.Sorted() if !got.Equal(tc.exp) { t.Errorf("%d: %v.Sorted() should be %v, but got %v", i, tc.set, tc.exp, got) } } } func TestSetIntersection(t *testing.T) { t.Parallel() testcases := []struct { s1, s2 id.Set exp id.Slice }{ {nil, nil, nil}, {id.NewSet(), nil, nil}, {id.NewSet(), id.NewSet(), nil}, |
︙ | ︙ | |||
59 60 61 62 63 64 65 66 67 68 69 70 71 72 | if !got.Equal(tc.exp) { t.Errorf("%d: %v.Intersect(%v) should be %v, but got %v", i, sl2, sl1, tc.exp, got) } } } func TestSetRemove(t *testing.T) { testcases := []struct { s1, s2 id.Set exp id.Slice }{ {nil, nil, nil}, {id.NewSet(), nil, nil}, {id.NewSet(), id.NewSet(), nil}, | > | 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 | if !got.Equal(tc.exp) { t.Errorf("%d: %v.Intersect(%v) should be %v, but got %v", i, sl2, sl1, tc.exp, got) } } } func TestSetRemove(t *testing.T) { t.Parallel() testcases := []struct { s1, s2 id.Set exp id.Slice }{ {nil, nil, nil}, {id.NewSet(), nil, nil}, {id.NewSet(), id.NewSet(), nil}, |
︙ | ︙ |
Changes to domain/id/slice_test.go.
︙ | ︙ | |||
15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 | import ( "testing" "zettelstore.de/z/domain/id" ) func TestSliceSort(t *testing.T) { zs := id.Slice{9, 4, 6, 1, 7} zs.Sort() exp := id.Slice{1, 4, 6, 7, 9} if !zs.Equal(exp) { t.Errorf("Slice.Sort did not work. Expected %v, got %v", exp, zs) } } func TestCopy(t *testing.T) { var orig id.Slice got := orig.Copy() if got != nil { t.Errorf("Nil copy resulted in %v", got) } orig = id.Slice{9, 4, 6, 1, 7} got = orig.Copy() if !orig.Equal(got) { t.Errorf("Slice.Copy did not work. Expected %v, got %v", orig, got) } } func TestSliceEqual(t *testing.T) { testcases := []struct { s1, s2 id.Slice exp bool }{ {nil, nil, true}, {nil, id.Slice{}, true}, {nil, id.Slice{1}, false}, | > > > | 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 | import ( "testing" "zettelstore.de/z/domain/id" ) func TestSliceSort(t *testing.T) { t.Parallel() zs := id.Slice{9, 4, 6, 1, 7} zs.Sort() exp := id.Slice{1, 4, 6, 7, 9} if !zs.Equal(exp) { t.Errorf("Slice.Sort did not work. Expected %v, got %v", exp, zs) } } func TestCopy(t *testing.T) { t.Parallel() var orig id.Slice got := orig.Copy() if got != nil { t.Errorf("Nil copy resulted in %v", got) } orig = id.Slice{9, 4, 6, 1, 7} got = orig.Copy() if !orig.Equal(got) { t.Errorf("Slice.Copy did not work. Expected %v, got %v", orig, got) } } func TestSliceEqual(t *testing.T) { t.Parallel() testcases := []struct { s1, s2 id.Slice exp bool }{ {nil, nil, true}, {nil, id.Slice{}, true}, {nil, id.Slice{1}, false}, |
︙ | ︙ | |||
62 63 64 65 66 67 68 69 70 71 72 73 74 75 | if got != tc.exp { t.Errorf("%d/%v.Equal(%v)==%v, but got %v", i, tc.s2, tc.s1, tc.exp, got) } } } func TestSliceString(t *testing.T) { testcases := []struct { in id.Slice exp string }{ {nil, ""}, {id.Slice{}, ""}, {id.Slice{1}, "00000000000001"}, | > | 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 | if got != tc.exp { t.Errorf("%d/%v.Equal(%v)==%v, but got %v", i, tc.s2, tc.s1, tc.exp, got) } } } func TestSliceString(t *testing.T) { t.Parallel() testcases := []struct { in id.Slice exp string }{ {nil, ""}, {id.Slice{}, ""}, {id.Slice{1}, "00000000000001"}, |
︙ | ︙ |
Changes to domain/meta/meta.go.
︙ | ︙ | |||
13 14 15 16 17 18 19 | import ( "regexp" "sort" "strings" "zettelstore.de/z/domain/id" | | | 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 | import ( "regexp" "sort" "strings" "zettelstore.de/z/domain/id" "zettelstore.de/z/input" ) type keyUsage int const ( _ keyUsage = iota usageUser // Key will be manipulated by the user |
︙ | ︙ | |||
85 86 87 88 89 90 91 | } // GetDescription returns the key description object of the given key name. func GetDescription(name string) DescriptionKey { if d, ok := registeredKeys[name]; ok { return *d } | | | 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 | } // GetDescription returns the key description object of the given key name. func GetDescription(name string) DescriptionKey { if d, ok := registeredKeys[name]; ok { return *d } return DescriptionKey{Type: Type(name)} } // GetSortedKeyDescriptions delivers all metadata key descriptions as a slice, sorted by name. func GetSortedKeyDescriptions() []*DescriptionKey { names := make([]string, 0, len(registeredKeys)) for n := range registeredKeys { names = append(names, n) |
︙ | ︙ | |||
111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 | KeyID = registerKey("id", TypeID, usageComputed, "") KeyTitle = registerKey("title", TypeZettelmarkup, usageUser, "") KeyRole = registerKey("role", TypeWord, usageUser, "") KeyTags = registerKey("tags", TypeTagSet, usageUser, "") KeySyntax = registerKey("syntax", TypeWord, usageUser, "") KeyBack = registerKey("back", TypeIDSet, usageProperty, "") KeyBackward = registerKey("backward", TypeIDSet, usageProperty, "") KeyCopyright = registerKey("copyright", TypeString, usageUser, "") KeyCredential = registerKey("credential", TypeCredential, usageUser, "") KeyDead = registerKey("dead", TypeIDSet, usageProperty, "") KeyDefaultCopyright = registerKey("default-copyright", TypeString, usageUser, "") KeyDefaultLang = registerKey("default-lang", TypeWord, usageUser, "") KeyDefaultLicense = registerKey("default-license", TypeEmpty, usageUser, "") KeyDefaultRole = registerKey("default-role", TypeWord, usageUser, "") KeyDefaultSyntax = registerKey("default-syntax", TypeWord, usageUser, "") KeyDefaultTitle = registerKey("default-title", TypeZettelmarkup, usageUser, "") KeyDefaultVisibility = registerKey("default-visibility", TypeWord, usageUser, "") KeyDuplicates = registerKey("duplicates", TypeBool, usageUser, "") KeyExpertMode = registerKey("expert-mode", TypeBool, usageUser, "") KeyFolge = registerKey("folge", TypeIDSet, usageProperty, "") KeyFooterHTML = registerKey("footer-html", TypeString, usageUser, "") KeyForward = registerKey("forward", TypeIDSet, usageProperty, "") KeyHomeZettel = registerKey("home-zettel", TypeID, usageUser, "") KeyLang = registerKey("lang", TypeWord, usageUser, "") KeyLicense = registerKey("license", TypeEmpty, usageUser, "") | > < > > > > > | 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 | KeyID = registerKey("id", TypeID, usageComputed, "") KeyTitle = registerKey("title", TypeZettelmarkup, usageUser, "") KeyRole = registerKey("role", TypeWord, usageUser, "") KeyTags = registerKey("tags", TypeTagSet, usageUser, "") KeySyntax = registerKey("syntax", TypeWord, usageUser, "") KeyBack = registerKey("back", TypeIDSet, usageProperty, "") KeyBackward = registerKey("backward", TypeIDSet, usageProperty, "") KeyBoxNumber = registerKey("box-number", TypeNumber, usageComputed, "") KeyCopyright = registerKey("copyright", TypeString, usageUser, "") KeyCredential = registerKey("credential", TypeCredential, usageUser, "") KeyDead = registerKey("dead", TypeIDSet, usageProperty, "") KeyDefaultCopyright = registerKey("default-copyright", TypeString, usageUser, "") KeyDefaultLang = registerKey("default-lang", TypeWord, usageUser, "") KeyDefaultLicense = registerKey("default-license", TypeEmpty, usageUser, "") KeyDefaultRole = registerKey("default-role", TypeWord, usageUser, "") KeyDefaultSyntax = registerKey("default-syntax", TypeWord, usageUser, "") KeyDefaultTitle = registerKey("default-title", TypeZettelmarkup, usageUser, "") KeyDefaultVisibility = registerKey("default-visibility", TypeWord, usageUser, "") KeyDuplicates = registerKey("duplicates", TypeBool, usageUser, "") KeyExpertMode = registerKey("expert-mode", TypeBool, usageUser, "") KeyFolge = registerKey("folge", TypeIDSet, usageProperty, "") KeyFooterHTML = registerKey("footer-html", TypeString, usageUser, "") KeyForward = registerKey("forward", TypeIDSet, usageProperty, "") KeyHomeZettel = registerKey("home-zettel", TypeID, usageUser, "") KeyLang = registerKey("lang", TypeWord, usageUser, "") KeyLicense = registerKey("license", TypeEmpty, usageUser, "") KeyMarkerExternal = registerKey("marker-external", TypeEmpty, usageUser, "") KeyModified = registerKey("modified", TypeTimestamp, usageComputed, "") KeyNoIndex = registerKey("no-index", TypeBool, usageUser, "") KeyPrecursor = registerKey("precursor", TypeIDSet, usageUser, KeyFolge) KeyPublished = registerKey("published", TypeTimestamp, usageProperty, "") KeyReadOnly = registerKey("read-only", TypeWord, usageUser, "") KeySiteName = registerKey("site-name", TypeString, usageUser, "") KeyURL = registerKey("url", TypeURL, usageUser, "") KeyUserID = registerKey("user-id", TypeWord, usageUser, "") KeyUserRole = registerKey("user-role", TypeWord, usageUser, "") KeyVisibility = registerKey("visibility", TypeWord, usageUser, "") KeyYAMLHeader = registerKey("yaml-header", TypeBool, usageUser, "") KeyZettelFileSyntax = registerKey("zettel-file-syntax", TypeWordSet, usageUser, "") ) // NewPrefix is the prefix for metadata key in template zettel for creating new zettel. const NewPrefix = "new-" // Important values for some keys. const ( ValueRoleConfiguration = "configuration" ValueRoleUser = "user" ValueRoleZettel = "zettel" ValueSyntaxNone = "none" ValueSyntaxGif = "gif" ValueSyntaxText = "text" ValueSyntaxZmk = "zmk" ValueTrue = "true" ValueFalse = "false" ValueLangEN = "en" ValueUserRoleCreator = "creator" ValueUserRoleReader = "reader" ValueUserRoleWriter = "writer" ValueUserRoleOwner = "owner" ValueVisibilityCreator = "creator" ValueVisibilityExpert = "expert" ValueVisibilityOwner = "owner" ValueVisibilityLogin = "login" ValueVisibilityPublic = "public" ) // Meta contains all meta-data of a zettel. |
︙ | ︙ | |||
227 228 229 230 231 232 233 | func (m *Meta) Set(key, value string) { if key != KeyID { m.pairs[key] = trimValue(value) } } func trimValue(value string) string { | | | 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 | func (m *Meta) Set(key, value string) { if key != KeyID { m.pairs[key] = trimValue(value) } } func trimValue(value string) string { return strings.TrimFunc(value, input.IsSpace) } // Get retrieves the string value of a given key. The bool value signals, // whether there was a value stored or not. func (m *Meta) Get(key string) (string, bool) { if key == KeyID { return m.Zid.String(), true |
︙ | ︙ |
Changes to domain/meta/meta_test.go.
1 | //----------------------------------------------------------------------------- | | | 1 2 3 4 5 6 7 8 9 | //----------------------------------------------------------------------------- // 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. //----------------------------------------------------------------------------- |
︙ | ︙ | |||
17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 | "zettelstore.de/z/domain/id" ) const testID = id.Zid(98765432101234) func TestKeyIsValid(t *testing.T) { validKeys := []string{"0", "a", "0-", "title", "title-----", strings.Repeat("r", 255)} for _, key := range validKeys { if !KeyIsValid(key) { t.Errorf("Key %q wrongly identified as invalid key", key) } } invalidKeys := []string{"", "-", "-a", "Title", "a_b", strings.Repeat("e", 256)} for _, key := range invalidKeys { if KeyIsValid(key) { t.Errorf("Key %q wrongly identified as valid key", key) } } } func TestTitleHeader(t *testing.T) { m := New(testID) if got, ok := m.Get(KeyTitle); ok || got != "" { t.Errorf("Title is not empty, but %q", got) } addToMeta(m, KeyTitle, " ") if got, ok := m.Get(KeyTitle); ok || got != "" { t.Errorf("Title is not empty, but %q", got) | > > | 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 | "zettelstore.de/z/domain/id" ) const testID = id.Zid(98765432101234) func TestKeyIsValid(t *testing.T) { t.Parallel() validKeys := []string{"0", "a", "0-", "title", "title-----", strings.Repeat("r", 255)} for _, key := range validKeys { if !KeyIsValid(key) { t.Errorf("Key %q wrongly identified as invalid key", key) } } invalidKeys := []string{"", "-", "-a", "Title", "a_b", strings.Repeat("e", 256)} for _, key := range invalidKeys { if KeyIsValid(key) { t.Errorf("Key %q wrongly identified as valid key", key) } } } func TestTitleHeader(t *testing.T) { t.Parallel() m := New(testID) if got, ok := m.Get(KeyTitle); ok || got != "" { t.Errorf("Title is not empty, but %q", got) } addToMeta(m, KeyTitle, " ") if got, ok := m.Get(KeyTitle); ok || got != "" { t.Errorf("Title is not empty, but %q", got) |
︙ | ︙ | |||
78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 | } if len(exp) < len(got) { t.Errorf("Extra tags: %q", got[len(exp):]) } } func TestTagsHeader(t *testing.T) { m := New(testID) checkSet(t, []string{}, m, KeyTags) addToMeta(m, KeyTags, "") checkSet(t, []string{}, m, KeyTags) addToMeta(m, KeyTags, " #t1 #t2 #t3 #t4 ") checkSet(t, []string{"#t1", "#t2", "#t3", "#t4"}, m, KeyTags) addToMeta(m, KeyTags, "#t5") checkSet(t, []string{"#t1", "#t2", "#t3", "#t4", "#t5"}, m, KeyTags) addToMeta(m, KeyTags, "t6") checkSet(t, []string{"#t1", "#t2", "#t3", "#t4", "#t5"}, m, KeyTags) } func TestSyntax(t *testing.T) { m := New(testID) if got, ok := m.Get(KeySyntax); ok || got != "" { t.Errorf("Syntax is not %q, but %q", "", got) } addToMeta(m, KeySyntax, " ") if got, _ := m.Get(KeySyntax); got != "" { t.Errorf("Syntax is not %q, but %q", "", got) | > > | 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 | } if len(exp) < len(got) { t.Errorf("Extra tags: %q", got[len(exp):]) } } func TestTagsHeader(t *testing.T) { t.Parallel() m := New(testID) checkSet(t, []string{}, m, KeyTags) addToMeta(m, KeyTags, "") checkSet(t, []string{}, m, KeyTags) addToMeta(m, KeyTags, " #t1 #t2 #t3 #t4 ") checkSet(t, []string{"#t1", "#t2", "#t3", "#t4"}, m, KeyTags) addToMeta(m, KeyTags, "#t5") checkSet(t, []string{"#t1", "#t2", "#t3", "#t4", "#t5"}, m, KeyTags) addToMeta(m, KeyTags, "t6") checkSet(t, []string{"#t1", "#t2", "#t3", "#t4", "#t5"}, m, KeyTags) } func TestSyntax(t *testing.T) { t.Parallel() m := New(testID) if got, ok := m.Get(KeySyntax); ok || got != "" { t.Errorf("Syntax is not %q, but %q", "", got) } addToMeta(m, KeySyntax, " ") if got, _ := m.Get(KeySyntax); got != "" { t.Errorf("Syntax is not %q, but %q", "", got) |
︙ | ︙ | |||
135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 | t.Errorf("Key %q missing, should have value %q", k, v) } } } } func TestDefaultHeader(t *testing.T) { m := New(testID) addToMeta(m, "h1", "d1") addToMeta(m, "H2", "D2") addToMeta(m, "H1", "D1.1") exp := map[string]string{"h1": "d1 D1.1", "h2": "D2"} checkHeader(t, exp, m.Pairs(true)) addToMeta(m, "", "d0") checkHeader(t, exp, m.Pairs(true)) addToMeta(m, "h3", "") exp["h3"] = "" checkHeader(t, exp, m.Pairs(true)) addToMeta(m, "h3", " ") checkHeader(t, exp, m.Pairs(true)) addToMeta(m, "h4", " ") exp["h4"] = "" checkHeader(t, exp, m.Pairs(true)) } func TestDelete(t *testing.T) { m := New(testID) m.Set("key", "val") if got, ok := m.Get("key"); !ok || got != "val" { t.Errorf("Value != %q, got: %v/%q", "val", ok, got) } m.Set("key", "") if got, ok := m.Get("key"); !ok || got != "" { t.Errorf("Value != %q, got: %v/%q", "", ok, got) } m.Delete("key") if got, ok := m.Get("key"); ok || got != "" { t.Errorf("Value != %q, got: %v/%q", "", ok, got) } } func TestEqual(t *testing.T) { testcases := []struct { pairs1, pairs2 []string allowComputed bool exp bool }{ {nil, nil, true, true}, {nil, nil, false, true}, | > > > | 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 | t.Errorf("Key %q missing, should have value %q", k, v) } } } } func TestDefaultHeader(t *testing.T) { t.Parallel() m := New(testID) addToMeta(m, "h1", "d1") addToMeta(m, "H2", "D2") addToMeta(m, "H1", "D1.1") exp := map[string]string{"h1": "d1 D1.1", "h2": "D2"} checkHeader(t, exp, m.Pairs(true)) addToMeta(m, "", "d0") checkHeader(t, exp, m.Pairs(true)) addToMeta(m, "h3", "") exp["h3"] = "" checkHeader(t, exp, m.Pairs(true)) addToMeta(m, "h3", " ") checkHeader(t, exp, m.Pairs(true)) addToMeta(m, "h4", " ") exp["h4"] = "" checkHeader(t, exp, m.Pairs(true)) } func TestDelete(t *testing.T) { t.Parallel() m := New(testID) m.Set("key", "val") if got, ok := m.Get("key"); !ok || got != "val" { t.Errorf("Value != %q, got: %v/%q", "val", ok, got) } m.Set("key", "") if got, ok := m.Get("key"); !ok || got != "" { t.Errorf("Value != %q, got: %v/%q", "", ok, got) } m.Delete("key") if got, ok := m.Get("key"); ok || got != "" { t.Errorf("Value != %q, got: %v/%q", "", ok, got) } } func TestEqual(t *testing.T) { t.Parallel() testcases := []struct { pairs1, pairs2 []string allowComputed bool exp bool }{ {nil, nil, true, true}, {nil, nil, false, true}, |
︙ | ︙ |
Changes to domain/meta/parse.go.
︙ | ︙ | |||
13 14 15 16 17 18 19 | import ( "sort" "strings" "zettelstore.de/z/domain/id" "zettelstore.de/z/input" | < | 13 14 15 16 17 18 19 20 21 22 23 24 25 26 | import ( "sort" "strings" "zettelstore.de/z/domain/id" "zettelstore.de/z/input" ) // NewFromInput parses the meta data of a zettel. func NewFromInput(zid id.Zid, inp *input.Input) *Meta { if inp.Ch == '-' && inp.PeekN(0) == '-' && inp.PeekN(1) == '-' { skipToEOL(inp) inp.EatEOL() |
︙ | ︙ | |||
68 69 70 71 72 73 74 | var val string for { skipSpace(inp) pos = inp.Pos skipToEOL(inp) val += inp.Src[pos:inp.Pos] inp.EatEOL() | | | | 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 | var val string for { skipSpace(inp) pos = inp.Pos skipToEOL(inp) val += inp.Src[pos:inp.Pos] inp.EatEOL() if !input.IsSpace(inp.Ch) { break } val += " " } addToMeta(m, key, val) } func skipSpace(inp *input.Input) { for input.IsSpace(inp.Ch) { inp.Next() } } func skipToEOL(inp *input.Input) { for { switch inp.Ch { |
︙ | ︙ | |||
120 121 122 123 124 125 126 | if !ok { oldElems = nil } set := make(map[string]bool, len(newElems)+len(oldElems)) addToSet(set, newElems, useElem) if len(set) == 0 { | | | 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 | if !ok { oldElems = nil } set := make(map[string]bool, len(newElems)+len(oldElems)) addToSet(set, newElems, useElem) if len(set) == 0 { // Nothing to add. Maybe because of rejected elements. return } addToSet(set, oldElems, useElem) resultList := make([]string, 0, len(set)) for tag := range set { resultList = append(resultList, tag) |
︙ | ︙ |
Changes to domain/meta/parse_test.go.
︙ | ︙ | |||
19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 | ) func parseMetaStr(src string) *meta.Meta { return meta.NewFromInput(testID, input.NewInput(src)) } func TestEmpty(t *testing.T) { m := parseMetaStr("") if got, ok := m.Get(meta.KeySyntax); ok || got != "" { t.Errorf("Syntax is not %q, but %q", "", got) } if got, ok := m.GetList(meta.KeyTags); ok || len(got) > 0 { t.Errorf("Tags are not nil, but %v", got) } } func TestTitle(t *testing.T) { td := []struct{ s, e string }{ {meta.KeyTitle + ": a title", "a title"}, {meta.KeyTitle + ": a\n\t title", "a title"}, {meta.KeyTitle + ": a\n\t title\r\n x", "a title x"}, {meta.KeyTitle + " AbC", "AbC"}, {meta.KeyTitle + " AbC\n ded", "AbC ded"}, {meta.KeyTitle + ": o\ntitle: p", "o p"}, | > > | 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 | ) func parseMetaStr(src string) *meta.Meta { return meta.NewFromInput(testID, input.NewInput(src)) } func TestEmpty(t *testing.T) { t.Parallel() m := parseMetaStr("") if got, ok := m.Get(meta.KeySyntax); ok || got != "" { t.Errorf("Syntax is not %q, but %q", "", got) } if got, ok := m.GetList(meta.KeyTags); ok || len(got) > 0 { t.Errorf("Tags are not nil, but %v", got) } } func TestTitle(t *testing.T) { t.Parallel() td := []struct{ s, e string }{ {meta.KeyTitle + ": a title", "a title"}, {meta.KeyTitle + ": a\n\t title", "a title"}, {meta.KeyTitle + ": a\n\t title\r\n x", "a title x"}, {meta.KeyTitle + " AbC", "AbC"}, {meta.KeyTitle + " AbC\n ded", "AbC ded"}, {meta.KeyTitle + ": o\ntitle: p", "o p"}, |
︙ | ︙ | |||
57 58 59 60 61 62 63 64 65 66 67 68 69 70 | m := parseMetaStr(meta.KeyTitle + ": ") if title, ok := m.Get(meta.KeyTitle); ok { t.Errorf("Expected a missing title key, but got %q (meta=%v)", title, m) } } func TestNewFromInput(t *testing.T) { testcases := []struct { input string exp []meta.Pair }{ {"", []meta.Pair{}}, {" a:b", []meta.Pair{{"a", "b"}}}, {"%a:b", []meta.Pair{}}, | > | 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 | m := parseMetaStr(meta.KeyTitle + ": ") if title, ok := m.Get(meta.KeyTitle); ok { t.Errorf("Expected a missing title key, but got %q (meta=%v)", title, m) } } func TestNewFromInput(t *testing.T) { t.Parallel() testcases := []struct { input string exp []meta.Pair }{ {"", []meta.Pair{}}, {" a:b", []meta.Pair{{"a", "b"}}}, {"%a:b", []meta.Pair{}}, |
︙ | ︙ | |||
105 106 107 108 109 110 111 112 113 114 115 116 117 118 | return false } } return true } func TestPrecursorIDSet(t *testing.T) { var testdata = []struct { inp string exp string }{ {"", ""}, {"123", ""}, {"12345678901234", "12345678901234"}, | > | 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 | return false } } return true } func TestPrecursorIDSet(t *testing.T) { t.Parallel() var testdata = []struct { inp string exp string }{ {"", ""}, {"123", ""}, {"12345678901234", "12345678901234"}, |
︙ | ︙ |
Changes to domain/meta/type.go.
︙ | ︙ | |||
10 11 12 13 14 15 16 17 18 19 20 21 22 23 | // Package meta provides the domain specific type 'meta'. package meta import ( "strconv" "strings" "time" ) // DescriptionType is a description of a specific key type. type DescriptionType struct { Name string IsSet bool | > | 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 | // Package meta provides the domain specific type 'meta'. package meta import ( "strconv" "strings" "sync" "time" ) // DescriptionType is a description of a specific key type. type DescriptionType struct { Name string IsSet bool |
︙ | ︙ | |||
45 46 47 48 49 50 51 | TypeID = registerType("Identifier", false) TypeIDSet = registerType("IdentifierSet", true) TypeNumber = registerType("Number", false) TypeString = registerType("String", false) TypeTagSet = registerType("TagSet", true) TypeTimestamp = registerType("Timestamp", false) TypeURL = registerType("URL", false) | < > > > > > > > > > > > > > > > > > > > > > > > > > > > | | 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 | TypeID = registerType("Identifier", false) TypeIDSet = registerType("IdentifierSet", true) TypeNumber = registerType("Number", false) TypeString = registerType("String", false) TypeTagSet = registerType("TagSet", true) TypeTimestamp = registerType("Timestamp", false) TypeURL = registerType("URL", false) TypeWord = registerType("Word", false) TypeWordSet = registerType("WordSet", true) TypeZettelmarkup = registerType("Zettelmarkup", false) ) // Type returns a type hint for the given key. If no type hint is specified, // TypeUnknown is returned. func (m *Meta) Type(key string) *DescriptionType { return Type(key) } var ( cachedTypedKeys = make(map[string]*DescriptionType) mxTypedKey sync.RWMutex ) func typedKey(key string, t *DescriptionType) *DescriptionType { mxTypedKey.Lock() defer mxTypedKey.Unlock() cachedTypedKeys[key] = t return t } // Type returns a type hint for the given key. If no type hint is specified, // TypeUnknown is returned. func Type(key string) *DescriptionType { if k, ok := registeredKeys[key]; ok { return k.Type } mxTypedKey.RLock() k, ok := cachedTypedKeys[key] mxTypedKey.RUnlock() if ok { return k } if strings.HasSuffix(key, "-url") { return typedKey(key, TypeURL) } if strings.HasSuffix(key, "-number") { return typedKey(key, TypeNumber) } if strings.HasSuffix(key, "-zid") { return typedKey(key, TypeID) } return TypeEmpty } // SetList stores the given string list value under the given key. func (m *Meta) SetList(key string, values []string) { if key != KeyID { for i, val := range values { values[i] = trimValue(val) |
︙ | ︙ |
Changes to domain/meta/type_test.go.
1 | //----------------------------------------------------------------------------- | | | 1 2 3 4 5 6 7 8 9 | //----------------------------------------------------------------------------- // 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. //----------------------------------------------------------------------------- |
︙ | ︙ | |||
17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 | "time" "zettelstore.de/z/domain/id" "zettelstore.de/z/domain/meta" ) func TestNow(t *testing.T) { m := meta.New(id.Invalid) m.SetNow("key") val, ok := m.Get("key") if !ok { t.Error("Unable to get value of key") } if len(val) != 14 { t.Errorf("Value is not 14 digits long: %q", val) } if _, err := strconv.ParseInt(val, 10, 64); err != nil { t.Errorf("Unable to parse %q as an int64: %v", val, err) } if _, ok := m.GetTime("key"); !ok { t.Errorf("Unable to get time from value %q", val) } } func TestGetTime(t *testing.T) { testCases := []struct { value string valid bool exp time.Time }{ {"", false, time.Time{}}, {"1", false, time.Time{}}, | > > | 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 | "time" "zettelstore.de/z/domain/id" "zettelstore.de/z/domain/meta" ) func TestNow(t *testing.T) { t.Parallel() m := meta.New(id.Invalid) m.SetNow("key") val, ok := m.Get("key") if !ok { t.Error("Unable to get value of key") } if len(val) != 14 { t.Errorf("Value is not 14 digits long: %q", val) } if _, err := strconv.ParseInt(val, 10, 64); err != nil { t.Errorf("Unable to parse %q as an int64: %v", val, err) } if _, ok := m.GetTime("key"); !ok { t.Errorf("Unable to get time from value %q", val) } } func TestGetTime(t *testing.T) { t.Parallel() testCases := []struct { value string valid bool exp time.Time }{ {"", false, time.Time{}}, {"1", false, time.Time{}}, |
︙ | ︙ |
Changes to domain/meta/values.go.
︙ | ︙ | |||
15 16 17 18 19 20 21 22 23 24 25 26 27 | type Visibility int // Supported values for visibility. const ( _ Visibility = iota VisibilityUnknown VisibilityPublic VisibilityLogin VisibilityOwner VisibilityExpert ) var visMap = map[string]Visibility{ | > | > | | | > > | | | | 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 | type Visibility int // Supported values for visibility. const ( _ Visibility = iota VisibilityUnknown VisibilityPublic VisibilityCreator VisibilityLogin VisibilityOwner VisibilityExpert ) var visMap = map[string]Visibility{ ValueVisibilityPublic: VisibilityPublic, ValueVisibilityCreator: VisibilityCreator, ValueVisibilityLogin: VisibilityLogin, ValueVisibilityOwner: VisibilityOwner, ValueVisibilityExpert: VisibilityExpert, } // GetVisibility returns the visibility value of the given string func GetVisibility(val string) Visibility { if vis, ok := visMap[val]; ok { return vis } return VisibilityUnknown } // UserRole enumerates the supported values of meta key 'user-role'. type UserRole int // Supported values for user roles. const ( _ UserRole = iota UserRoleUnknown UserRoleCreator UserRoleReader UserRoleWriter UserRoleOwner ) var urMap = map[string]UserRole{ ValueUserRoleCreator: UserRoleCreator, ValueUserRoleReader: UserRoleReader, ValueUserRoleWriter: UserRoleWriter, ValueUserRoleOwner: UserRoleOwner, } // GetUserRole role returns the user role of the given string. func GetUserRole(val string) UserRole { if ur, ok := urMap[val]; ok { return ur } return UserRoleUnknown } |
Changes to domain/meta/write_test.go.
1 | //----------------------------------------------------------------------------- | | | 1 2 3 4 5 6 7 8 9 | //----------------------------------------------------------------------------- // 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. //----------------------------------------------------------------------------- |
︙ | ︙ | |||
40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 | m.Write(&sb, true) if got := sb.String(); got != expected { t.Errorf("\nExp: %q\ngot: %q", expected, got) } } func TestWriteMeta(t *testing.T) { assertWriteMeta(t, newMeta("", nil, ""), "") m := newMeta("TITLE", []string{"#t1", "#t2"}, "syntax") assertWriteMeta(t, m, "title: TITLE\ntags: #t1 #t2\nsyntax: syntax\n") m = newMeta("TITLE", nil, "") m.Set("user", "zettel") m.Set("auth", "basic") assertWriteMeta(t, m, "title: TITLE\nauth: basic\nuser: zettel\n") } | > | 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 | m.Write(&sb, true) if got := sb.String(); got != expected { t.Errorf("\nExp: %q\ngot: %q", expected, got) } } func TestWriteMeta(t *testing.T) { t.Parallel() assertWriteMeta(t, newMeta("", nil, ""), "") m := newMeta("TITLE", []string{"#t1", "#t2"}, "syntax") assertWriteMeta(t, m, "title: TITLE\ntags: #t1 #t2\nsyntax: syntax\n") m = newMeta("TITLE", nil, "") m.Set("user", "zettel") m.Set("auth", "basic") assertWriteMeta(t, m, "title: TITLE\nauth: basic\nuser: zettel\n") } |
Changes to encoder/buffer.go.
1 | //----------------------------------------------------------------------------- | | | 1 2 3 4 5 6 7 8 9 | //----------------------------------------------------------------------------- // 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. //----------------------------------------------------------------------------- |
︙ | ︙ | |||
28 29 30 31 32 33 34 | // NewBufWriter creates a new BufWriter func NewBufWriter(w io.Writer) BufWriter { return BufWriter{w: w, buf: make([]byte, 0, 4096)} } // Write writes the contents of p into the buffer. func (w *BufWriter) Write(p []byte) (int, error) { | | > > | | | | | | | | | < | < | > | > > > > > | 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 | // NewBufWriter creates a new BufWriter func NewBufWriter(w io.Writer) BufWriter { return BufWriter{w: w, buf: make([]byte, 0, 4096)} } // Write writes the contents of p into the buffer. func (w *BufWriter) Write(p []byte) (int, error) { if w.err != nil { return 0, w.err } w.buf = append(w.buf, p...) if len(w.buf) > 2048 { w.flush() if w.err != nil { return 0, w.err } } return len(p), nil } // WriteString writes the contents of s into the buffer. func (w *BufWriter) WriteString(s string) { if w.err != nil { return } w.buf = append(w.buf, s...) if len(w.buf) > 2048 { w.flush() } } // WriteStrings writes the contents of sl into the buffer. func (w *BufWriter) WriteStrings(sl ...string) { for _, s := range sl { w.WriteString(s) } |
︙ | ︙ |
Changes to encoder/encfun/encfun.go.
︙ | ︙ | |||
10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 | // Package encfun provides some helper function to work with encodings. package encfun import ( "strings" "zettelstore.de/z/ast" "zettelstore.de/z/domain/meta" "zettelstore.de/z/encoder" "zettelstore.de/z/parser" ) // MetaAsInlineSlice returns the value of the given metadata key as an inlince slice. func MetaAsInlineSlice(m *meta.Meta, key string) ast.InlineSlice { return parser.ParseMetadata(m.GetDefault(key, "")) } // MetaAsText returns the value of given metadata as text. func MetaAsText(m *meta.Meta, key string) string { | > | | 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 | // Package encfun provides some helper function to work with encodings. package encfun import ( "strings" "zettelstore.de/z/api" "zettelstore.de/z/ast" "zettelstore.de/z/domain/meta" "zettelstore.de/z/encoder" "zettelstore.de/z/parser" ) // MetaAsInlineSlice returns the value of the given metadata key as an inlince slice. func MetaAsInlineSlice(m *meta.Meta, key string) ast.InlineSlice { return parser.ParseMetadata(m.GetDefault(key, "")) } // MetaAsText returns the value of given metadata as text. func MetaAsText(m *meta.Meta, key string) string { textEncoder := encoder.Create(api.EncoderText, nil) var sb strings.Builder _, err := textEncoder.WriteInlines(&sb, MetaAsInlineSlice(m, key)) if err == nil { return sb.String() } return "" } |
Changes to encoder/encoder.go.
︙ | ︙ | |||
12 13 14 15 16 17 18 | // tree into some text form. package encoder import ( "errors" "io" "log" | < > | 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 | // tree into some text form. package encoder import ( "errors" "io" "log" "zettelstore.de/z/api" "zettelstore.de/z/ast" "zettelstore.de/z/domain/meta" ) // Encoder is an interface that allows to encode different parts of a zettel. type Encoder interface { WriteZettel(io.Writer, *ast.ZettelNode, bool) (int, error) |
︙ | ︙ | |||
37 38 39 40 41 42 43 | ErrNoWriteMeta = errors.New("method WriteMeta is not implemented") ErrNoWriteContent = errors.New("method WriteContent is not implemented") ErrNoWriteBlocks = errors.New("method WriteBlocks is not implemented") ErrNoWriteInlines = errors.New("method WriteInlines is not implemented") ) // Create builds a new encoder with the given options. | | | | | | | | | < | | | | | | 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 | ErrNoWriteMeta = errors.New("method WriteMeta is not implemented") ErrNoWriteContent = errors.New("method WriteContent is not implemented") ErrNoWriteBlocks = errors.New("method WriteBlocks is not implemented") ErrNoWriteInlines = errors.New("method WriteInlines is not implemented") ) // Create builds a new encoder with the given options. func Create(format api.EncodingEnum, env *Environment) Encoder { if info, ok := registry[format]; ok { return info.Create(env) } return nil } // Info stores some data about an encoder. type Info struct { Create func(*Environment) Encoder Default bool } var registry = map[api.EncodingEnum]Info{} var defFormat api.EncodingEnum // Register the encoder for later retrieval. func Register(format api.EncodingEnum, info Info) { if _, ok := registry[format]; ok { log.Fatalf("Writer with format %q already registered", format) } if info.Default { if defFormat != api.EncoderUnknown && defFormat != format { log.Fatalf("Default format already set: %q, new format: %q", defFormat, format) } defFormat = format } registry[format] = info } // GetFormats returns all registered formats, ordered by format code. func GetFormats() []api.EncodingEnum { result := make([]api.EncodingEnum, 0, len(registry)) for format := range registry { result = append(result, format) } return result } // GetDefaultFormat returns the format that should be used as default. func GetDefaultFormat() api.EncodingEnum { if defFormat != api.EncoderUnknown { return defFormat } if _, ok := registry[api.EncoderJSON]; ok { return api.EncoderJSON } log.Fatalf("No default format given") return api.EncoderUnknown } |
Changes to encoder/htmlenc/block.go.
︙ | ︙ | |||
15 16 17 18 19 20 21 | "fmt" "strconv" "strings" "zettelstore.de/z/ast" ) | < < < < < < < < | | | 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 | "fmt" "strconv" "strings" "zettelstore.de/z/ast" ) func (v *visitor) visitVerbatim(vn *ast.VerbatimNode) { switch vn.Kind { case ast.VerbatimProg: oldVisible := v.visibleSpace if vn.Attrs != nil { v.visibleSpace = vn.Attrs.HasDefault() } v.b.WriteString("<pre><code") v.visitAttributes(vn.Attrs) |
︙ | ︙ | |||
57 58 59 60 61 62 63 | case ast.VerbatimHTML: for _, line := range vn.Lines { if !ignoreHTMLText(line) { v.b.WriteStrings(line, "\n") } } default: | | | 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 | case ast.VerbatimHTML: for _, line := range vn.Lines { if !ignoreHTMLText(line) { v.b.WriteStrings(line, "\n") } } default: panic(fmt.Sprintf("Unknown verbatim kind %v", vn.Kind)) } } var htmlSnippetsIgnore = []string{ "<script", "</script", "<iframe", |
︙ | ︙ | |||
99 100 101 102 103 104 105 | attrs.Remove("") attrs = attrs.AddClass("zs-indication").AddClass("zs-" + attrVal) } } return attrs } | < | | | | | < | | < < < < < < < < < < < | < | | | | | 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 | attrs.Remove("") attrs = attrs.AddClass("zs-indication").AddClass("zs-" + attrVal) } } return attrs } func (v *visitor) visitRegion(rn *ast.RegionNode) { var code string attrs := rn.Attrs oldVerse := v.inVerse switch rn.Kind { case ast.RegionSpan: code = "div" attrs = processSpanAttributes(attrs) case ast.RegionVerse: v.inVerse = true code = "div" case ast.RegionQuote: code = "blockquote" default: panic(fmt.Sprintf("Unknown region kind %v", rn.Kind)) } v.lang.push(attrs) defer v.lang.pop() v.b.WriteStrings("<", code) v.visitAttributes(attrs) v.b.WriteString(">\n") ast.WalkBlockSlice(v, rn.Blocks) if len(rn.Inlines) > 0 { v.b.WriteString("<cite>") ast.WalkInlineSlice(v, rn.Inlines) v.b.WriteString("</cite>\n") } v.b.WriteStrings("</", code, ">\n") v.inVerse = oldVerse } func (v *visitor) visitHeading(hn *ast.HeadingNode) { v.lang.push(hn.Attrs) defer v.lang.pop() lvl := hn.Level if lvl > 6 { lvl = 6 // HTML has H1..H6 } strLvl := strconv.Itoa(lvl) v.b.WriteStrings("<h", strLvl) v.visitAttributes(hn.Attrs) if slug := hn.Slug; len(slug) > 0 { v.b.WriteStrings(" id=\"", slug, "\"") } v.b.WriteByte('>') ast.WalkInlineSlice(v, hn.Inlines) v.b.WriteStrings("</h", strLvl, ">\n") } var mapNestedListKind = map[ast.NestedListKind]string{ ast.NestedListOrdered: "ol", ast.NestedListUnordered: "ul", } func (v *visitor) visitNestedList(ln *ast.NestedListNode) { v.lang.push(ln.Attrs) defer v.lang.pop() if ln.Kind == ast.NestedListQuote { // NestedListQuote -> HTML <blockquote> doesn't use <li>...</li> v.writeQuotationList(ln) return } code, ok := mapNestedListKind[ln.Kind] if !ok { panic(fmt.Sprintf("Invalid list kind %v", ln.Kind)) } compact := isCompactList(ln.Items) v.b.WriteStrings("<", code) v.visitAttributes(ln.Attrs) v.b.WriteString(">\n") for _, item := range ln.Items { |
︙ | ︙ | |||
208 209 210 211 212 213 214 | if pn := getParaItem(item); pn != nil { if inPara { v.b.WriteByte('\n') } else { v.b.WriteString("<p>") inPara = true } | | | | 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 | if pn := getParaItem(item); pn != nil { if inPara { v.b.WriteByte('\n') } else { v.b.WriteString("<p>") inPara = true } ast.WalkInlineSlice(v, pn.Inlines) } else { if inPara { v.writeEndPara() inPara = false } ast.WalkItemSlice(v, item) } } if inPara { v.writeEndPara() } v.b.WriteString("</blockquote>\n") } |
︙ | ︙ | |||
263 264 265 266 267 268 269 | // writeItemSliceOrPara emits the content of a paragraph if the paragraph is // the only element of the block slice and if compact mode is true. Otherwise, // the item slice is emitted normally. func (v *visitor) writeItemSliceOrPara(ins ast.ItemSlice, compact bool) { if compact && len(ins) == 1 { if para, ok := ins[0].(*ast.ParaNode); ok { | | | | | < | | < < | | < | | 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 | // writeItemSliceOrPara emits the content of a paragraph if the paragraph is // the only element of the block slice and if compact mode is true. Otherwise, // the item slice is emitted normally. func (v *visitor) writeItemSliceOrPara(ins ast.ItemSlice, compact bool) { if compact && len(ins) == 1 { if para, ok := ins[0].(*ast.ParaNode); ok { ast.WalkInlineSlice(v, para.Inlines) return } } ast.WalkItemSlice(v, ins) } func (v *visitor) writeDescriptionsSlice(ds ast.DescriptionSlice) { if len(ds) == 1 { if para, ok := ds[0].(*ast.ParaNode); ok { ast.WalkInlineSlice(v, para.Inlines) return } } ast.WalkDescriptionSlice(v, ds) } func (v *visitor) visitDescriptionList(dn *ast.DescriptionListNode) { v.b.WriteString("<dl>\n") for _, descr := range dn.Descriptions { v.b.WriteString("<dt>") ast.WalkInlineSlice(v, descr.Term) v.b.WriteString("</dt>\n") for _, b := range descr.Descriptions { v.b.WriteString("<dd>") v.writeDescriptionsSlice(b) v.b.WriteString("</dd>\n") } } v.b.WriteString("</dl>\n") } func (v *visitor) visitTable(tn *ast.TableNode) { v.b.WriteString("<table>\n") if len(tn.Header) > 0 { v.b.WriteString("<thead>\n") v.writeRow(tn.Header, "<th", "</th>") v.b.WriteString("</thead>\n") } if len(tn.Rows) > 0 { |
︙ | ︙ | |||
332 333 334 335 336 337 338 | v.b.WriteString("<tr>") for _, cell := range row { v.b.WriteString(cellStart) if len(cell.Inlines) == 0 { v.b.WriteByte('>') } else { v.b.WriteString(alignStyle[cell.Align]) | | < | | 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 326 327 | v.b.WriteString("<tr>") for _, cell := range row { v.b.WriteString(cellStart) if len(cell.Inlines) == 0 { v.b.WriteByte('>') } else { v.b.WriteString(alignStyle[cell.Align]) ast.WalkInlineSlice(v, cell.Inlines) } v.b.WriteString(cellEnd) } v.b.WriteString("</tr>\n") } func (v *visitor) visitBLOB(bn *ast.BLOBNode) { switch bn.Syntax { case "gif", "jpeg", "png": v.b.WriteStrings("<img src=\"data:image/", bn.Syntax, ";base64,") v.b.WriteBase64(bn.Blob) v.b.WriteString("\" title=\"") v.writeQuotedEscaped(bn.Title) v.b.WriteString("\">\n") |
︙ | ︙ |
Changes to encoder/htmlenc/htmlenc.go.
︙ | ︙ | |||
11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 | // Package htmlenc encodes the abstract syntax tree into HTML5. package htmlenc import ( "io" "strings" "zettelstore.de/z/ast" "zettelstore.de/z/domain/meta" "zettelstore.de/z/encoder" "zettelstore.de/z/encoder/encfun" "zettelstore.de/z/parser" ) func init() { | > | | 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 | // Package htmlenc encodes the abstract syntax tree into HTML5. package htmlenc import ( "io" "strings" "zettelstore.de/z/api" "zettelstore.de/z/ast" "zettelstore.de/z/domain/meta" "zettelstore.de/z/encoder" "zettelstore.de/z/encoder/encfun" "zettelstore.de/z/parser" ) func init() { encoder.Register(api.EncoderHTML, encoder.Info{ Create: func(env *encoder.Environment) encoder.Encoder { return &htmlEncoder{env: env} }, }) } type htmlEncoder struct { env *encoder.Environment } |
︙ | ︙ | |||
47 48 49 50 51 52 53 | v.b.WriteStrings("<title>", encfun.MetaAsText(zn.InhMeta, meta.KeyTitle), "</title>") if inhMeta { v.acceptMeta(zn.InhMeta) } else { v.acceptMeta(zn.Meta) } v.b.WriteString("\n</head>\n<body>\n") | | | | | | 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 | v.b.WriteStrings("<title>", encfun.MetaAsText(zn.InhMeta, meta.KeyTitle), "</title>") if inhMeta { v.acceptMeta(zn.InhMeta) } else { v.acceptMeta(zn.Meta) } v.b.WriteString("\n</head>\n<body>\n") ast.WalkBlockSlice(v, zn.Ast) v.writeEndnotes() v.b.WriteString("</body>\n</html>") length, err := v.b.Flush() return length, err } // WriteMeta encodes meta data as HTML5. func (he *htmlEncoder) WriteMeta(w io.Writer, m *meta.Meta) (int, error) { v := newVisitor(he, w) // Write title if title, ok := m.Get(meta.KeyTitle); ok { textEnc := encoder.Create(api.EncoderText, nil) var sb strings.Builder textEnc.WriteInlines(&sb, parser.ParseMetadata(title)) v.b.WriteStrings("<meta name=\"zs-", meta.KeyTitle, "\" content=\"") v.writeQuotedEscaped(sb.String()) v.b.WriteString("\">") } // Write other metadata v.acceptMeta(m) length, err := v.b.Flush() return length, err } func (he *htmlEncoder) WriteContent(w io.Writer, zn *ast.ZettelNode) (int, error) { return he.WriteBlocks(w, zn.Ast) } // WriteBlocks encodes a block slice. func (he *htmlEncoder) WriteBlocks(w io.Writer, bs ast.BlockSlice) (int, error) { v := newVisitor(he, w) ast.WalkBlockSlice(v, bs) v.writeEndnotes() length, err := v.b.Flush() return length, err } // WriteInlines writes an inline slice to the writer func (he *htmlEncoder) WriteInlines(w io.Writer, is ast.InlineSlice) (int, error) { v := newVisitor(he, w) if env := he.env; env != nil { v.inInteractive = env.Interactive } ast.WalkInlineSlice(v, is) length, err := v.b.Flush() return length, err } |
Changes to encoder/htmlenc/inline.go.
︙ | ︙ | |||
16 17 18 19 20 21 22 | "strconv" "strings" "zettelstore.de/z/ast" "zettelstore.de/z/domain/meta" ) | < < < < < < < < < < < < < < < < < < < < < < < | < | | | 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 | "strconv" "strings" "zettelstore.de/z/ast" "zettelstore.de/z/domain/meta" ) func (v *visitor) visitBreak(bn *ast.BreakNode) { if bn.Hard { if v.env.IsXHTML() { v.b.WriteString("<br />\n") } else { v.b.WriteString("<br>\n") } } else { v.b.WriteByte('\n') } } func (v *visitor) visitLink(ln *ast.LinkNode) { ln, n := v.env.AdaptLink(ln) if n != nil { ast.Walk(v, n) return } v.lang.push(ln.Attrs) defer v.lang.pop() switch ln.Ref.State { case ast.RefStateSelf, ast.RefStateFound, ast.RefStateHosted, ast.RefStateBased: |
︙ | ︙ | |||
90 91 92 93 94 95 96 | } v.b.WriteString("<a href=\"") v.writeQuotedEscaped(ln.Ref.Value) v.b.WriteByte('"') v.visitAttributes(ln.Attrs) v.b.WriteByte('>') v.inInteractive = true | | | < | | | < | | | < | < | < | | | 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 | } v.b.WriteString("<a href=\"") v.writeQuotedEscaped(ln.Ref.Value) v.b.WriteByte('"') v.visitAttributes(ln.Attrs) v.b.WriteByte('>') v.inInteractive = true ast.WalkInlineSlice(v, ln.Inlines) v.inInteractive = false v.b.WriteString("</a>") } } func (v *visitor) writeAHref(ref *ast.Reference, attrs *ast.Attributes, ins ast.InlineSlice) { if v.env.IsInteractive(v.inInteractive) { v.writeSpan(ins, attrs) return } v.b.WriteString("<a href=\"") v.writeReference(ref) v.b.WriteByte('"') v.visitAttributes(attrs) v.b.WriteByte('>') v.inInteractive = true ast.WalkInlineSlice(v, ins) v.inInteractive = false v.b.WriteString("</a>") } func (v *visitor) visitImage(in *ast.ImageNode) { in, n := v.env.AdaptImage(in) if n != nil { ast.Walk(v, n) return } v.lang.push(in.Attrs) defer v.lang.pop() if in.Ref == nil { v.b.WriteString("<img src=\"data:image/") switch in.Syntax { case "svg": v.b.WriteString("svg+xml;utf8,") v.writeQuotedEscaped(string(in.Blob)) default: v.b.WriteStrings(in.Syntax, ";base64,") v.b.WriteBase64(in.Blob) } } else { v.b.WriteString("<img src=\"") v.writeReference(in.Ref) } v.b.WriteString("\" alt=\"") ast.WalkInlineSlice(v, in.Inlines) v.b.WriteByte('"') v.visitAttributes(in.Attrs) if v.env.IsXHTML() { v.b.WriteString(" />") } else { v.b.WriteByte('>') } } func (v *visitor) visitCite(cn *ast.CiteNode) { cn, n := v.env.AdaptCite(cn) if n != nil { ast.Walk(v, n) return } if cn == nil { return } v.lang.push(cn.Attrs) defer v.lang.pop() v.b.WriteString(cn.Key) if len(cn.Inlines) > 0 { v.b.WriteString(", ") ast.WalkInlineSlice(v, cn.Inlines) } } func (v *visitor) visitFootnote(fn *ast.FootnoteNode) { v.lang.push(fn.Attrs) defer v.lang.pop() if v.env.IsInteractive(v.inInteractive) { return } n := strconv.Itoa(v.env.AddFootnote(fn)) v.b.WriteStrings("<sup id=\"fnref:", n, "\"><a href=\"#fn:", n, "\" class=\"zs-footnote-ref\" role=\"doc-noteref\">", n, "</a></sup>") // TODO: what to do with Attrs? } func (v *visitor) visitMark(mn *ast.MarkNode) { if v.env.IsInteractive(v.inInteractive) { return } if len(mn.Text) > 0 { v.b.WriteStrings("<a id=\"", mn.Text, "\"></a>") } } func (v *visitor) visitFormat(fn *ast.FormatNode) { v.lang.push(fn.Attrs) defer v.lang.pop() var code string attrs := fn.Attrs switch fn.Kind { case ast.FormatItalic: code = "i" case ast.FormatEmph: code = "em" case ast.FormatBold: code = "b" case ast.FormatStrong: |
︙ | ︙ | |||
231 232 233 234 235 236 237 | case ast.FormatMonospace: code = "span" attrs = attrs.Set("style", "font-family:monospace") case ast.FormatQuote: v.visitQuotes(fn) return default: | | | | | 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 | case ast.FormatMonospace: code = "span" attrs = attrs.Set("style", "font-family:monospace") case ast.FormatQuote: v.visitQuotes(fn) return default: panic(fmt.Sprintf("Unknown format kind %v", fn.Kind)) } v.b.WriteStrings("<", code) v.visitAttributes(attrs) v.b.WriteByte('>') ast.WalkInlineSlice(v, fn.Inlines) v.b.WriteStrings("</", code, ">") } func (v *visitor) writeSpan(ins ast.InlineSlice, attrs *ast.Attributes) { v.b.WriteString("<span") v.visitAttributes(attrs) v.b.WriteByte('>') ast.WalkInlineSlice(v, ins) v.b.WriteString("</span>") } var langQuotes = map[string][2]string{ meta.ValueLangEN: {"“", "”"}, "de": {"„", "“"}, |
︙ | ︙ | |||
277 278 279 280 281 282 283 | if withSpan { v.b.WriteString("<span") v.visitAttributes(fn.Attrs) v.b.WriteByte('>') } openingQ, closingQ := getQuotes(v.lang.top()) v.b.WriteString(openingQ) | | < | | | | 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 | if withSpan { v.b.WriteString("<span") v.visitAttributes(fn.Attrs) v.b.WriteByte('>') } openingQ, closingQ := getQuotes(v.lang.top()) v.b.WriteString(openingQ) ast.WalkInlineSlice(v, fn.Inlines) v.b.WriteString(closingQ) if withSpan { v.b.WriteString("</span>") } } func (v *visitor) visitLiteral(ln *ast.LiteralNode) { switch ln.Kind { case ast.LiteralProg: v.writeLiteral("<code", "</code>", ln.Attrs, ln.Text) case ast.LiteralKeyb: v.writeLiteral("<kbd", "</kbd>", ln.Attrs, ln.Text) case ast.LiteralOutput: v.writeLiteral("<samp", "</samp>", ln.Attrs, ln.Text) case ast.LiteralComment: v.b.WriteString("<!-- ") v.writeHTMLEscaped(ln.Text) // writeCommentEscaped v.b.WriteString(" -->") case ast.LiteralHTML: if !ignoreHTMLText(ln.Text) { v.b.WriteString(ln.Text) } default: panic(fmt.Sprintf("Unknown literal kind %v", ln.Kind)) } } func (v *visitor) writeLiteral(codeS, codeE string, attrs *ast.Attributes, text string) { oldVisible := v.visibleSpace if attrs != nil { v.visibleSpace = attrs.HasDefault() |
︙ | ︙ |
Changes to encoder/htmlenc/langstack_test.go.
1 | //----------------------------------------------------------------------------- | | > | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 | //----------------------------------------------------------------------------- // Copyright (c) 2020-2021 Detlef Stern // // This file is part of zettelstore. // // Zettelstore is licensed under the latest version of the EUPL (European Union // Public License). Please see file LICENSE.txt for your rights and obligations // under this license. //----------------------------------------------------------------------------- // Package htmlenc encodes the abstract syntax tree into HTML5. package htmlenc import ( "testing" "zettelstore.de/z/ast" ) func TestStackSimple(t *testing.T) { t.Parallel() exp := "de" s := newLangStack(exp) if got := s.top(); got != exp { t.Errorf("Init: expected %q, but got %q", exp, got) return } |
︙ | ︙ |
Changes to encoder/htmlenc/visitor.go.
︙ | ︙ | |||
40 41 42 43 44 45 46 47 48 49 50 51 52 53 | } return &visitor{ env: he.env, b: encoder.NewBufWriter(w), lang: newLangStack(lang), } } var mapMetaKey = map[string]string{ meta.KeyCopyright: "copyright", meta.KeyLicense: "license", } func (v *visitor) acceptMeta(m *meta.Meta) { | > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > | 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 | } return &visitor{ env: he.env, b: encoder.NewBufWriter(w), lang: newLangStack(lang), } } func (v *visitor) Visit(node ast.Node) ast.Visitor { switch n := node.(type) { case *ast.ParaNode: v.b.WriteString("<p>") ast.WalkInlineSlice(v, n.Inlines) v.writeEndPara() case *ast.VerbatimNode: v.visitVerbatim(n) case *ast.RegionNode: v.visitRegion(n) case *ast.HeadingNode: v.visitHeading(n) case *ast.HRuleNode: v.b.WriteString("<hr") v.visitAttributes(n.Attrs) if v.env.IsXHTML() { v.b.WriteString(" />\n") } else { v.b.WriteString(">\n") } case *ast.NestedListNode: v.visitNestedList(n) case *ast.DescriptionListNode: v.visitDescriptionList(n) case *ast.TableNode: v.visitTable(n) case *ast.BLOBNode: v.visitBLOB(n) case *ast.TextNode: v.writeHTMLEscaped(n.Text) case *ast.TagNode: // TODO: erst mal als span. Link wäre gut, muss man vermutlich via Callback lösen. v.b.WriteString("<span class=\"zettel-tag\">#") v.writeHTMLEscaped(n.Tag) v.b.WriteString("</span>") case *ast.SpaceNode: if v.inVerse || v.env.IsXHTML() { v.b.WriteString(n.Lexeme) } else { v.b.WriteByte(' ') } case *ast.BreakNode: v.visitBreak(n) case *ast.LinkNode: v.visitLink(n) case *ast.ImageNode: v.visitImage(n) case *ast.CiteNode: v.visitCite(n) case *ast.FootnoteNode: v.visitFootnote(n) case *ast.MarkNode: v.visitMark(n) case *ast.FormatNode: v.visitFormat(n) case *ast.LiteralNode: v.visitLiteral(n) default: return v } return nil } var mapMetaKey = map[string]string{ meta.KeyCopyright: "copyright", meta.KeyLicense: "license", } func (v *visitor) acceptMeta(m *meta.Meta) { |
︙ | ︙ | |||
81 82 83 84 85 86 87 | func (v *visitor) writeMeta(prefix, key, value string) { v.b.WriteStrings("\n<meta name=\"", prefix, key, "\" content=\"") v.writeQuotedEscaped(value) v.b.WriteString("\">") } | < < < < < < < < < < < < < < < < | | 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 | func (v *visitor) writeMeta(prefix, key, value string) { v.b.WriteStrings("\n<meta name=\"", prefix, key, "\" content=\"") v.writeQuotedEscaped(value) v.b.WriteString("\">") } func (v *visitor) writeEndnotes() { footnotes := v.env.GetCleanFootnotes() if len(footnotes) > 0 { v.b.WriteString("<ol class=\"zs-endnotes\">\n") for i := 0; i < len(footnotes); i++ { // Do not use a range loop above, because a footnote may contain // a footnote. Therefore v.enc.footnote may grow during the loop. fn := footnotes[i] n := strconv.Itoa(i + 1) v.b.WriteStrings("<li id=\"fn:", n, "\" role=\"doc-endnote\">") ast.WalkInlineSlice(v, fn.Inlines) v.b.WriteStrings( " <a href=\"#fnref:", n, "\" class=\"zs-footnote-backref\" role=\"doc-backlink\">↩︎</a></li>\n") } v.b.WriteString("</ol>\n") } |
︙ | ︙ |
Changes to encoder/jsonenc/djsonenc.go.
︙ | ︙ | |||
13 14 15 16 17 18 19 20 21 22 23 24 25 26 | import ( "fmt" "io" "sort" "strconv" "zettelstore.de/z/ast" "zettelstore.de/z/domain/meta" "zettelstore.de/z/encoder" "zettelstore.de/z/encoder/encfun" ) func init() { | > | | | | | | < | > > | | | > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > | < | | | | < | < | | < | | | | | | < < | | < < < < < < < < | < | | < > | > > > | | < | < | < | > > > > > | < | < | < | < < | < < | < | | < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < | | | 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 326 327 328 329 330 331 332 333 334 335 336 337 338 339 340 341 342 343 344 345 346 347 348 349 350 351 352 353 354 355 356 357 358 359 360 361 362 | import ( "fmt" "io" "sort" "strconv" "zettelstore.de/z/api" "zettelstore.de/z/ast" "zettelstore.de/z/domain/meta" "zettelstore.de/z/encoder" "zettelstore.de/z/encoder/encfun" ) func init() { encoder.Register(api.EncoderDJSON, encoder.Info{ Create: func(env *encoder.Environment) encoder.Encoder { return &jsonDetailEncoder{env: env} }, }) } type jsonDetailEncoder struct { env *encoder.Environment } // WriteZettel writes the encoded zettel to the writer. func (je *jsonDetailEncoder) WriteZettel(w io.Writer, zn *ast.ZettelNode, inhMeta bool) (int, error) { v := newDetailVisitor(w, je) v.b.WriteString("{\"meta\":{\"title\":") v.walkInlineSlice(encfun.MetaAsInlineSlice(zn.InhMeta, meta.KeyTitle)) if inhMeta { v.writeMeta(zn.InhMeta) } else { v.writeMeta(zn.Meta) } v.b.WriteByte('}') v.b.WriteString(",\"content\":") v.walkBlockSlice(zn.Ast) v.b.WriteByte('}') length, err := v.b.Flush() return length, err } // WriteMeta encodes meta data as JSON. func (je *jsonDetailEncoder) WriteMeta(w io.Writer, m *meta.Meta) (int, error) { v := newDetailVisitor(w, je) v.b.WriteString("{\"title\":") v.walkInlineSlice(encfun.MetaAsInlineSlice(m, meta.KeyTitle)) v.writeMeta(m) v.b.WriteByte('}') length, err := v.b.Flush() return length, err } func (je *jsonDetailEncoder) WriteContent(w io.Writer, zn *ast.ZettelNode) (int, error) { return je.WriteBlocks(w, zn.Ast) } // WriteBlocks writes a block slice to the writer func (je *jsonDetailEncoder) WriteBlocks(w io.Writer, bs ast.BlockSlice) (int, error) { v := newDetailVisitor(w, je) v.walkBlockSlice(bs) length, err := v.b.Flush() return length, err } // WriteInlines writes an inline slice to the writer func (je *jsonDetailEncoder) WriteInlines(w io.Writer, is ast.InlineSlice) (int, error) { v := newDetailVisitor(w, je) v.walkInlineSlice(is) length, err := v.b.Flush() return length, err } // detailVisitor writes the abstract syntax tree to an io.Writer. type detailVisitor struct { b encoder.BufWriter env *encoder.Environment } func newDetailVisitor(w io.Writer, je *jsonDetailEncoder) *detailVisitor { return &detailVisitor{b: encoder.NewBufWriter(w), env: je.env} } func (v *detailVisitor) Visit(node ast.Node) ast.Visitor { switch n := node.(type) { case *ast.ParaNode: v.writeNodeStart("Para") v.writeContentStart('i') v.walkInlineSlice(n.Inlines) case *ast.VerbatimNode: v.visitVerbatim(n) case *ast.RegionNode: v.visitRegion(n) case *ast.HeadingNode: v.visitHeading(n) case *ast.HRuleNode: v.writeNodeStart("Hrule") v.visitAttributes(n.Attrs) case *ast.NestedListNode: v.visitNestedList(n) case *ast.DescriptionListNode: v.visitDescriptionList(n) case *ast.TableNode: v.visitTable(n) case *ast.BLOBNode: v.writeNodeStart("Blob") v.writeContentStart('q') writeEscaped(&v.b, n.Title) v.writeContentStart('s') writeEscaped(&v.b, n.Syntax) v.writeContentStart('o') v.b.WriteBase64(n.Blob) v.b.WriteByte('"') case *ast.TextNode: v.writeNodeStart("Text") v.writeContentStart('s') writeEscaped(&v.b, n.Text) case *ast.TagNode: v.writeNodeStart("Tag") v.writeContentStart('s') writeEscaped(&v.b, n.Tag) case *ast.SpaceNode: v.writeNodeStart("Space") if l := len(n.Lexeme); l > 1 { v.writeContentStart('n') v.b.WriteString(strconv.Itoa(l)) } case *ast.BreakNode: if n.Hard { v.writeNodeStart("Hard") } else { v.writeNodeStart("Soft") } case *ast.LinkNode: n, n2 := v.env.AdaptLink(n) if n2 != nil { ast.Walk(v, n2) return nil } v.writeNodeStart("Link") v.visitAttributes(n.Attrs) v.writeContentStart('q') writeEscaped(&v.b, mapRefState[n.Ref.State]) v.writeContentStart('s') writeEscaped(&v.b, n.Ref.String()) v.writeContentStart('i') v.walkInlineSlice(n.Inlines) case *ast.ImageNode: v.visitImage(n) case *ast.CiteNode: v.writeNodeStart("Cite") v.visitAttributes(n.Attrs) v.writeContentStart('s') writeEscaped(&v.b, n.Key) if len(n.Inlines) > 0 { v.writeContentStart('i') v.walkInlineSlice(n.Inlines) } case *ast.FootnoteNode: v.writeNodeStart("Footnote") v.visitAttributes(n.Attrs) v.writeContentStart('i') v.walkInlineSlice(n.Inlines) case *ast.MarkNode: v.writeNodeStart("Mark") if len(n.Text) > 0 { v.writeContentStart('s') writeEscaped(&v.b, n.Text) } case *ast.FormatNode: v.writeNodeStart(mapFormatKind[n.Kind]) v.visitAttributes(n.Attrs) v.writeContentStart('i') v.walkInlineSlice(n.Inlines) case *ast.LiteralNode: kind, ok := mapLiteralKind[n.Kind] if !ok { panic(fmt.Sprintf("Unknown literal kind %v", n.Kind)) } v.writeNodeStart(kind) v.visitAttributes(n.Attrs) v.writeContentStart('s') writeEscaped(&v.b, n.Text) default: return v } v.b.WriteByte('}') return nil } var mapVerbatimKind = map[ast.VerbatimKind]string{ ast.VerbatimProg: "CodeBlock", ast.VerbatimComment: "CommentBlock", ast.VerbatimHTML: "HTMLBlock", } func (v *detailVisitor) visitVerbatim(vn *ast.VerbatimNode) { kind, ok := mapVerbatimKind[vn.Kind] if !ok { panic(fmt.Sprintf("Unknown verbatim kind %v", vn.Kind)) } v.writeNodeStart(kind) v.visitAttributes(vn.Attrs) v.writeContentStart('l') for i, line := range vn.Lines { v.writeComma(i) writeEscaped(&v.b, line) } v.b.WriteByte(']') } var mapRegionKind = map[ast.RegionKind]string{ ast.RegionSpan: "SpanBlock", ast.RegionQuote: "QuoteBlock", ast.RegionVerse: "VerseBlock", } func (v *detailVisitor) visitRegion(rn *ast.RegionNode) { kind, ok := mapRegionKind[rn.Kind] if !ok { panic(fmt.Sprintf("Unknown region kind %v", rn.Kind)) } v.writeNodeStart(kind) v.visitAttributes(rn.Attrs) v.writeContentStart('b') v.walkBlockSlice(rn.Blocks) if len(rn.Inlines) > 0 { v.writeContentStart('i') v.walkInlineSlice(rn.Inlines) } } func (v *detailVisitor) visitHeading(hn *ast.HeadingNode) { v.writeNodeStart("Heading") v.visitAttributes(hn.Attrs) v.writeContentStart('n') v.b.WriteString(strconv.Itoa(hn.Level)) if slug := hn.Slug; len(slug) > 0 { v.writeContentStart('s') v.b.WriteStrings("\"", slug, "\"") } v.writeContentStart('i') v.walkInlineSlice(hn.Inlines) } var mapNestedListKind = map[ast.NestedListKind]string{ ast.NestedListOrdered: "OrderedList", ast.NestedListUnordered: "BulletList", ast.NestedListQuote: "QuoteList", } func (v *detailVisitor) visitNestedList(ln *ast.NestedListNode) { v.writeNodeStart(mapNestedListKind[ln.Kind]) v.writeContentStart('c') for i, item := range ln.Items { v.writeComma(i) v.b.WriteByte('[') for j, in := range item { v.writeComma(j) ast.Walk(v, in) } v.b.WriteByte(']') } v.b.WriteByte(']') } func (v *detailVisitor) visitDescriptionList(dn *ast.DescriptionListNode) { v.writeNodeStart("DescriptionList") v.writeContentStart('g') for i, def := range dn.Descriptions { v.writeComma(i) v.b.WriteByte('[') v.walkInlineSlice(def.Term) if len(def.Descriptions) > 0 { for _, b := range def.Descriptions { v.b.WriteString(",[") for j, dn := range b { v.writeComma(j) ast.Walk(v, dn) } v.b.WriteByte(']') } } v.b.WriteByte(']') } v.b.WriteByte(']') } func (v *detailVisitor) visitTable(tn *ast.TableNode) { v.writeNodeStart("Table") v.writeContentStart('p') // Table header v.b.WriteByte('[') for i, cell := range tn.Header { v.writeComma(i) v.writeCell(cell) } v.b.WriteString("],") // Table rows v.b.WriteByte('[') for i, row := range tn.Rows { v.writeComma(i) v.b.WriteByte('[') for j, cell := range row { v.writeComma(j) v.writeCell(cell) } v.b.WriteByte(']') } v.b.WriteString("]]") } var alignmentCode = map[ast.Alignment]string{ ast.AlignDefault: "[\"\",", ast.AlignLeft: "[\"<\",", ast.AlignCenter: "[\":\",", ast.AlignRight: "[\">\",", } func (v *detailVisitor) writeCell(cell *ast.TableCell) { v.b.WriteString(alignmentCode[cell.Align]) v.walkInlineSlice(cell.Inlines) v.b.WriteByte(']') } var mapRefState = map[ast.RefState]string{ ast.RefStateInvalid: "invalid", ast.RefStateZettel: "zettel", ast.RefStateSelf: "self", ast.RefStateFound: "zettel", ast.RefStateBroken: "broken", ast.RefStateHosted: "local", ast.RefStateBased: "based", ast.RefStateExternal: "external", } func (v *detailVisitor) visitImage(in *ast.ImageNode) { in, n := v.env.AdaptImage(in) if n != nil { ast.Walk(v, n) return } v.writeNodeStart("Image") v.visitAttributes(in.Attrs) if in.Ref == nil { v.writeContentStart('j') v.b.WriteString("\"s\":") |
︙ | ︙ | |||
358 359 360 361 362 363 364 | v.b.WriteByte('}') } else { v.writeContentStart('s') writeEscaped(&v.b, in.Ref.String()) } if len(in.Inlines) > 0 { v.writeContentStart('i') | | < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < | < < < < < < < < < | < < < < < < < < < < < < < | < < < < < < < | < < < < < < < < < < | < < < < < | < < | < | < | | 373 374 375 376 377 378 379 380 381 382 383 384 385 386 387 388 389 390 391 392 393 394 395 396 397 398 399 400 401 402 403 404 405 406 407 408 409 410 411 412 413 414 415 416 417 418 419 420 421 422 423 424 425 426 427 428 429 430 | v.b.WriteByte('}') } else { v.writeContentStart('s') writeEscaped(&v.b, in.Ref.String()) } if len(in.Inlines) > 0 { v.writeContentStart('i') v.walkInlineSlice(in.Inlines) } } var mapFormatKind = map[ast.FormatKind]string{ ast.FormatItalic: "Italic", ast.FormatEmph: "Emph", ast.FormatBold: "Bold", ast.FormatStrong: "Strong", ast.FormatMonospace: "Mono", ast.FormatStrike: "Strikethrough", ast.FormatDelete: "Delete", ast.FormatUnder: "Underline", ast.FormatInsert: "Insert", ast.FormatSuper: "Super", ast.FormatSub: "Sub", ast.FormatQuote: "Quote", ast.FormatQuotation: "Quotation", ast.FormatSmall: "Small", ast.FormatSpan: "Span", } var mapLiteralKind = map[ast.LiteralKind]string{ ast.LiteralProg: "Code", ast.LiteralKeyb: "Input", ast.LiteralOutput: "Output", ast.LiteralComment: "Comment", ast.LiteralHTML: "HTML", } func (v *detailVisitor) walkBlockSlice(bns ast.BlockSlice) { v.b.WriteByte('[') for i, bn := range bns { v.writeComma(i) ast.Walk(v, bn) } v.b.WriteByte(']') } func (v *detailVisitor) walkInlineSlice(ins ast.InlineSlice) { v.b.WriteByte('[') for i, in := range ins { v.writeComma(i) ast.Walk(v, in) } v.b.WriteByte(']') } // visitAttributes write JSON attributes func (v *detailVisitor) visitAttributes(a *ast.Attributes) { if a == nil || len(a.Attrs) == 0 { |
︙ | ︙ | |||
559 560 561 562 563 564 565 | } } } func (v *detailVisitor) writeSetValue(value string) { v.b.WriteByte('[') for i, val := range meta.ListFromValue(value) { | < | < > > > > > > | 493 494 495 496 497 498 499 500 501 502 503 504 505 506 507 508 509 510 511 512 | } } } func (v *detailVisitor) writeSetValue(value string) { v.b.WriteByte('[') for i, val := range meta.ListFromValue(value) { v.writeComma(i) v.b.WriteByte('"') v.b.Write(Escape(val)) v.b.WriteByte('"') } v.b.WriteByte(']') } func (v *detailVisitor) writeComma(pos int) { if pos > 0 { v.b.WriteByte(',') } } |
Changes to encoder/jsonenc/jsonenc.go.
︙ | ︙ | |||
11 12 13 14 15 16 17 18 19 20 21 22 23 | // Package jsonenc encodes the abstract syntax tree into some JSON formats. package jsonenc import ( "bytes" "io" "zettelstore.de/z/ast" "zettelstore.de/z/domain/meta" "zettelstore.de/z/encoder" ) func init() { | > | | 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 | // Package jsonenc encodes the abstract syntax tree into some JSON formats. package jsonenc import ( "bytes" "io" "zettelstore.de/z/api" "zettelstore.de/z/ast" "zettelstore.de/z/domain/meta" "zettelstore.de/z/encoder" ) func init() { encoder.Register(api.EncoderJSON, encoder.Info{ Create: func(*encoder.Environment) encoder.Encoder { return &jsonEncoder{} }, Default: true, }) } // jsonEncoder is just a stub. It is not implemented. The real implementation // is in file web/adapter/json.go |
︙ | ︙ |
Changes to encoder/nativeenc/nativeenc.go.
︙ | ︙ | |||
13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 | import ( "fmt" "io" "sort" "strconv" "zettelstore.de/z/ast" "zettelstore.de/z/domain/meta" "zettelstore.de/z/encoder" "zettelstore.de/z/encoder/encfun" "zettelstore.de/z/parser" ) func init() { | > | | | | | > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > | < | < | 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 | import ( "fmt" "io" "sort" "strconv" "zettelstore.de/z/api" "zettelstore.de/z/ast" "zettelstore.de/z/domain/meta" "zettelstore.de/z/encoder" "zettelstore.de/z/encoder/encfun" "zettelstore.de/z/parser" ) func init() { encoder.Register(api.EncoderNative, encoder.Info{ Create: func(env *encoder.Environment) encoder.Encoder { return &nativeEncoder{env: env} }, }) } type nativeEncoder struct { env *encoder.Environment } // WriteZettel encodes the zettel to the writer. func (ne *nativeEncoder) WriteZettel( w io.Writer, zn *ast.ZettelNode, inhMeta bool) (int, error) { v := newVisitor(w, ne) v.b.WriteString("[Title ") v.walkInlineSlice(encfun.MetaAsInlineSlice(zn.InhMeta, meta.KeyTitle)) v.b.WriteByte(']') if inhMeta { v.acceptMeta(zn.InhMeta, false) } else { v.acceptMeta(zn.Meta, false) } v.b.WriteByte('\n') v.walkBlockSlice(zn.Ast) length, err := v.b.Flush() return length, err } // WriteMeta encodes meta data in native format. func (ne *nativeEncoder) WriteMeta(w io.Writer, m *meta.Meta) (int, error) { v := newVisitor(w, ne) v.acceptMeta(m, true) length, err := v.b.Flush() return length, err } func (ne *nativeEncoder) WriteContent(w io.Writer, zn *ast.ZettelNode) (int, error) { return ne.WriteBlocks(w, zn.Ast) } // WriteBlocks writes a block slice to the writer func (ne *nativeEncoder) WriteBlocks(w io.Writer, bs ast.BlockSlice) (int, error) { v := newVisitor(w, ne) v.walkBlockSlice(bs) length, err := v.b.Flush() return length, err } // WriteInlines writes an inline slice to the writer func (ne *nativeEncoder) WriteInlines(w io.Writer, is ast.InlineSlice) (int, error) { v := newVisitor(w, ne) v.walkInlineSlice(is) length, err := v.b.Flush() return length, err } // visitor writes the abstract syntax tree to an io.Writer. type visitor struct { b encoder.BufWriter level int env *encoder.Environment } func newVisitor(w io.Writer, enc *nativeEncoder) *visitor { return &visitor{b: encoder.NewBufWriter(w), env: enc.env} } func (v *visitor) Visit(node ast.Node) ast.Visitor { switch n := node.(type) { case *ast.ParaNode: v.b.WriteString("[Para ") v.walkInlineSlice(n.Inlines) v.b.WriteByte(']') case *ast.VerbatimNode: v.visitVerbatim(n) case *ast.RegionNode: v.visitRegion(n) case *ast.HeadingNode: v.b.WriteStrings("[Heading ", strconv.Itoa(n.Level), " \"", n.Slug, "\"") v.visitAttributes(n.Attrs) v.b.WriteByte(' ') v.walkInlineSlice(n.Inlines) v.b.WriteByte(']') case *ast.HRuleNode: v.b.WriteString("[Hrule") v.visitAttributes(n.Attrs) v.b.WriteByte(']') case *ast.NestedListNode: v.visitNestedList(n) case *ast.DescriptionListNode: v.visitDescriptionList(n) case *ast.TableNode: v.visitTable(n) case *ast.BLOBNode: v.b.WriteString("[BLOB \"") v.writeEscaped(n.Title) v.b.WriteString("\" \"") v.writeEscaped(n.Syntax) v.b.WriteString("\" \"") v.b.WriteBase64(n.Blob) v.b.WriteString("\"]") case *ast.TextNode: v.b.WriteString("Text \"") v.writeEscaped(n.Text) v.b.WriteByte('"') case *ast.TagNode: v.b.WriteString("Tag \"") v.writeEscaped(n.Tag) v.b.WriteByte('"') case *ast.SpaceNode: v.b.WriteString("Space") if l := len(n.Lexeme); l > 1 { v.b.WriteByte(' ') v.b.WriteString(strconv.Itoa(l)) } case *ast.BreakNode: if n.Hard { v.b.WriteString("Break") } else { v.b.WriteString("Space") } case *ast.LinkNode: v.visitLink(n) case *ast.ImageNode: v.visitImage(n) case *ast.CiteNode: v.b.WriteString("Cite") v.visitAttributes(n.Attrs) v.b.WriteString(" \"") v.writeEscaped(n.Key) v.b.WriteByte('"') if len(n.Inlines) > 0 { v.b.WriteString(" [") v.walkInlineSlice(n.Inlines) v.b.WriteByte(']') } case *ast.FootnoteNode: v.b.WriteString("Footnote") v.visitAttributes(n.Attrs) v.b.WriteString(" [") v.walkInlineSlice(n.Inlines) v.b.WriteByte(']') case *ast.MarkNode: v.b.WriteString("Mark") if len(n.Text) > 0 { v.b.WriteString(" \"") v.writeEscaped(n.Text) v.b.WriteByte('"') } case *ast.FormatNode: v.b.Write(mapFormatKind[n.Kind]) v.visitAttributes(n.Attrs) v.b.WriteString(" [") v.walkInlineSlice(n.Inlines) v.b.WriteByte(']') case *ast.LiteralNode: kind, ok := mapLiteralKind[n.Kind] if !ok { panic(fmt.Sprintf("Unknown literal kind %v", n.Kind)) } v.b.Write(kind) v.visitAttributes(n.Attrs) v.b.WriteString(" \"") v.writeEscaped(n.Text) v.b.WriteByte('"') default: return v } return nil } var ( rawBackslash = []byte{'\\', '\\'} rawDoubleQuote = []byte{'\\', '"'} rawNewline = []byte{'\\', 'n'} ) func (v *visitor) acceptMeta(m *meta.Meta, withTitle bool) { if withTitle { v.b.WriteString("[Title ") v.walkInlineSlice(parser.ParseMetadata(m.GetDefault(meta.KeyTitle, ""))) v.b.WriteByte(']') } v.writeMetaString(m, meta.KeyRole, "Role") v.writeMetaList(m, meta.KeyTags, "Tags") v.writeMetaString(m, meta.KeySyntax, "Syntax") pairs := m.PairsRest(true) if len(pairs) == 0 { return } v.b.WriteString("\n[Header") v.level++ for i, p := range pairs { v.writeComma(i) v.writeNewLine() v.b.WriteByte('[') v.b.WriteStrings(p.Key, " \"") v.writeEscaped(p.Value) v.b.WriteString("\"]") } v.level-- |
︙ | ︙ | |||
139 140 141 142 143 144 145 | v.b.WriteByte(' ') v.b.WriteString(val) } v.b.WriteByte(']') } } |