Many hyperlinks are disabled.
Use anonymous login
to enable hyperlinks.
Difference From trunk To v0.11.0
2025-03-10
| ||
18:52 | Refactored strfun.XMLEscape into web/xml ... (Leaf check-in: 08d7e39dbc user: stern tags: trunk) | |
17:04 | Typos in Website; add missing download link to Android executable ... (check-in: 9e0f33d64a user: stern tags: trunk) | |
2023-03-28
| ||
16:28 | Version 0.11.1 ... (check-in: fede87bf67 user: stern tags: trunk, release, v0.11.1) | |
2023-03-27
| ||
14:53 | Version 0.11.0 ... (check-in: 30ca5d039f user: stern tags: trunk, release, v0.11.0) | |
10:58 | Add search term ''PICK n'' to replace ''RANDOM'' in most use cases ... (check-in: b131f5e588 user: stern tags: trunk) | |
Changes to .fossil-settings/ignore-glob.
1 2 | bin/* releases/* | > | 1 2 3 | bin/* releases/* parser/pikchr/*.out |
Changes to Makefile.
1 2 3 4 5 6 7 8 9 | ## Copyright (c) 2020-present Detlef Stern ## ## This file is part of Zettelstore. ## ## Zettelstore is licensed under the latest version of the EUPL (European Union ## Public License). Please see file LICENSE.txt for your rights and obligations ## under this license. | | | | | | | | | | 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 | ## Copyright (c) 2020-present Detlef Stern ## ## This file is part of Zettelstore. ## ## Zettelstore is licensed under the latest version of the EUPL (European Union ## Public License). Please see file LICENSE.txt for your rights and obligations ## under this license. .PHONY: check relcheck api build release clean check: go run tools/build.go check relcheck: go run tools/build.go relcheck 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 README.md.
︙ | ︙ | |||
9 10 11 12 13 14 15 | gradually, one major focus is a long-term store of these notes, hence the name “Zettelstore”. To get an initial impression, take a look at the [manual](https://zettelstore.de/manual/). It is a live example of the zettelstore software, running in read-only mode. | | | > | | 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 | gradually, one major focus is a long-term store of these notes, hence the name “Zettelstore”. To get an initial impression, take a look at the [manual](https://zettelstore.de/manual/). It is a live example of the zettelstore software, running in read-only mode. [Zettelstore Client](https://zettelstore.de/client) provides client software to access Zettelstore via its API more easily, [Zettelstore Contrib](https://zettelstore.de/contrib) contains contributed software, which often connects to Zettelstore via its API. Some of the software packages may be experimental. The software, including the manual, is licensed under the [European Union Public License 1.2 (or later)](https://zettelstore.de/home/file?name=LICENSE.txt&ci=trunk). [Stay](https://twitter.com/zettelstore) <a rel="me" href="https://nerdculture.de/@zettelstore">tuned</a> … |
Changes to VERSION.
|
| | | 1 | 0.11.0 |
Added ast/ast.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) 2020-present Detlef Stern // // This file is part of Zettelstore. // // Zettelstore is licensed under the latest version of the EUPL (European Union // Public License). Please see file LICENSE.txt for your rights and obligations // under this license. //----------------------------------------------------------------------------- // Package ast provides the abstract syntax tree for parsed zettel content. package ast import ( "net/url" "zettelstore.de/z/domain" "zettelstore.de/z/domain/id" "zettelstore.de/z/domain/meta" ) // ZettelNode is the root node of the abstract syntax tree. // It is *not* part of the visitor pattern. type ZettelNode struct { Meta *meta.Meta // Original metadata Content domain.Content // Original content Zid id.Zid // Zettel identification. InhMeta *meta.Meta // Metadata of the zettel, with inherited values. Ast BlockSlice // Zettel abstract syntax tree is a sequence of block nodes. Syntax string // Syntax / parser that produced the Ast } // Node is the interface, all nodes must implement. type Node interface { WalkChildren(v Visitor) } // BlockNode is the interface that all block nodes must implement. type BlockNode interface { Node blockNode() } // ItemNode is a node that can occur as a list item. type ItemNode interface { BlockNode itemNode() } // ItemSlice is a slice of ItemNodes. type ItemSlice []ItemNode // DescriptionNode is a node that contains just textual description. type DescriptionNode interface { ItemNode descriptionNode() } // DescriptionSlice is a slice of DescriptionNodes. type DescriptionSlice []DescriptionNode // InlineNode is the interface that all inline nodes must implement. type InlineNode interface { Node inlineNode() } // Reference is a reference to external or internal material. type Reference struct { URL *url.URL Value string State RefState } // RefState indicates the state of the reference. type RefState int // Constants for RefState const ( RefStateInvalid RefState = iota // Invalid Reference RefStateZettel // Reference to an internal zettel RefStateSelf // Reference to same zettel with a fragment RefStateFound // Reference to an existing internal zettel, URL is ajusted RefStateBroken // Reference to a non-existing internal zettel RefStateHosted // Reference to local hosted non-Zettel, without URL change RefStateBased // Reference to local non-Zettel, to be prefixed RefStateQuery // Reference to a zettel query RefStateExternal // Reference to external material ) |
Added ast/block.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 | //----------------------------------------------------------------------------- // Copyright (c) 2020-present Detlef Stern // // This file is part of Zettelstore. // // Zettelstore is licensed under the latest version of the EUPL (European Union // Public License). Please see file LICENSE.txt for your rights and obligations // under this license. //----------------------------------------------------------------------------- package ast import "zettelstore.de/c/attrs" // Definition of Block nodes. // BlockSlice is a slice of BlockNodes. type BlockSlice []BlockNode func (*BlockSlice) blockNode() { /* Just a marker */ } // WalkChildren walks down to the descriptions. func (bs *BlockSlice) WalkChildren(v Visitor) { if bs != nil { for _, bn := range *bs { Walk(v, bn) } } } // FirstParagraphInlines returns the inline list of the first paragraph that // contains a inline list. func (bs BlockSlice) FirstParagraphInlines() InlineSlice { for _, bn := range bs { pn, ok := bn.(*ParaNode) if !ok { continue } if inl := pn.Inlines; len(inl) > 0 { return inl } } return nil } //-------------------------------------------------------------------------- // ParaNode contains just a sequence of inline elements. // Another name is "paragraph". type ParaNode struct { Inlines InlineSlice } func (*ParaNode) blockNode() { /* Just a marker */ } func (*ParaNode) itemNode() { /* Just a marker */ } func (*ParaNode) descriptionNode() { /* Just a marker */ } // CreateParaNode creates a parameter block from inline nodes. func CreateParaNode(nodes ...InlineNode) *ParaNode { return &ParaNode{Inlines: nodes} } // WalkChildren walks down the inline elements. func (pn *ParaNode) WalkChildren(v Visitor) { Walk(v, &pn.Inlines) } //-------------------------------------------------------------------------- // VerbatimNode contains uninterpreted text type VerbatimNode struct { Kind VerbatimKind Attrs attrs.Attributes Content []byte } // VerbatimKind specifies the format that is applied to code inline nodes. type VerbatimKind int // Constants for VerbatimCode const ( _ VerbatimKind = iota VerbatimZettel // Zettel content VerbatimProg // Program code VerbatimEval // Code to be externally interpreted. Syntax is stored in default attribute. VerbatimComment // Block comment VerbatimHTML // Block HTML, e.g. for Markdown VerbatimMath // Block math mode ) func (*VerbatimNode) blockNode() { /* Just a marker */ } func (*VerbatimNode) itemNode() { /* Just a marker */ } // WalkChildren does nothing. func (*VerbatimNode) WalkChildren(Visitor) { /* No children*/ } //-------------------------------------------------------------------------- // RegionNode encapsulates a region of block nodes. type RegionNode struct { Kind RegionKind Attrs attrs.Attributes Blocks BlockSlice Inlines InlineSlice // Optional text at the end of the region } // RegionKind specifies the actual region type. type RegionKind int // Values for RegionCode const ( _ RegionKind = iota RegionSpan // Just a span of blocks RegionQuote // A longer quotation RegionVerse // Line breaks matter ) func (*RegionNode) blockNode() { /* Just a marker */ } func (*RegionNode) itemNode() { /* Just a marker */ } // WalkChildren walks down the blocks and the text. func (rn *RegionNode) WalkChildren(v Visitor) { Walk(v, &rn.Blocks) Walk(v, &rn.Inlines) } //-------------------------------------------------------------------------- // HeadingNode stores the heading text and level. type HeadingNode struct { Level int Attrs attrs.Attributes Slug string // Heading text, normalized Fragment string // Heading text, suitable to be used as an unique URL fragment Inlines InlineSlice // Heading text, possibly formatted } func (*HeadingNode) blockNode() { /* Just a marker */ } func (*HeadingNode) itemNode() { /* Just a marker */ } // WalkChildren walks the heading text. func (hn *HeadingNode) WalkChildren(v Visitor) { Walk(v, &hn.Inlines) } //-------------------------------------------------------------------------- // HRuleNode specifies a horizontal rule. type HRuleNode struct { Attrs attrs.Attributes } func (*HRuleNode) blockNode() { /* Just a marker */ } func (*HRuleNode) itemNode() { /* Just a marker */ } // WalkChildren does nothing. func (*HRuleNode) WalkChildren(Visitor) { /* No children*/ } //-------------------------------------------------------------------------- // NestedListNode specifies a nestable list, either ordered or unordered. type NestedListNode struct { Kind NestedListKind Items []ItemSlice Attrs 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 (*NestedListNode) blockNode() { /* Just a marker */ } func (*NestedListNode) itemNode() { /* Just a marker */ } // WalkChildren walks down the items. func (ln *NestedListNode) WalkChildren(v Visitor) { if items := ln.Items; items != nil { for _, item := range items { WalkItemSlice(v, item) } } } //-------------------------------------------------------------------------- // 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 (*DescriptionListNode) blockNode() { /* Just a marker */ } // WalkChildren walks down to the descriptions. func (dn *DescriptionListNode) WalkChildren(v Visitor) { if descrs := dn.Descriptions; descrs != nil { for i, desc := range descrs { if len(desc.Term) > 0 { Walk(v, &descrs[i].Term) // Otherwise, changes in desc.Term will not go back into AST } if dss := desc.Descriptions; dss != nil { for _, dns := range dss { WalkDescriptionSlice(v, dns) } } } } } //-------------------------------------------------------------------------- // TableNode specifies a full table type TableNode struct { Header TableRow // The header row Align []Alignment // Default column alignment Rows []TableRow // The slice of cell rows } // TableCell contains the data for one table cell type TableCell struct { Align Alignment // Cell alignment Inlines InlineSlice // Cell content } // TableRow is a slice of cells. type TableRow []*TableCell // Alignment specifies text alignment. // Currently only for tables. type Alignment int // Constants for Alignment. const ( _ Alignment = iota AlignDefault // Default alignment, inherited AlignLeft // Left alignment AlignCenter // Center the content AlignRight // Right alignment ) func (*TableNode) blockNode() { /* Just a marker */ } // WalkChildren walks down to the cells. func (tn *TableNode) WalkChildren(v Visitor) { if header := tn.Header; header != nil { for i := range header { Walk(v, &header[i].Inlines) // Otherwise changes will not go back } } if rows := tn.Rows; rows != nil { for _, row := range rows { for i := range row { Walk(v, &row[i].Inlines) // Otherwise changes will not go back } } } } //-------------------------------------------------------------------------- // TranscludeNode specifies block content from other zettel to embedded in // current zettel type TranscludeNode struct { Attrs attrs.Attributes Ref *Reference } func (*TranscludeNode) blockNode() { /* Just a marker */ } // WalkChildren does nothing. func (*TranscludeNode) WalkChildren(Visitor) { /* No children*/ } //-------------------------------------------------------------------------- // BLOBNode contains just binary data that must be interpreted according to // a syntax. type BLOBNode struct { Description InlineSlice Syntax string Blob []byte } func (*BLOBNode) blockNode() { /* Just a marker */ } // WalkChildren does nothing. func (*BLOBNode) WalkChildren(Visitor) { /* No children*/ } |
Added ast/inline.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 | //----------------------------------------------------------------------------- // Copyright (c) 2020-present Detlef Stern // // This file is part of Zettelstore. // // Zettelstore is licensed under the latest version of the EUPL (European Union // Public License). Please see file LICENSE.txt for your rights and obligations // under this license. //----------------------------------------------------------------------------- package ast import ( "unicode/utf8" "zettelstore.de/c/attrs" ) // Definitions of inline nodes. // InlineSlice is a list of BlockNodes. type InlineSlice []InlineNode func (*InlineSlice) inlineNode() { /* Just a marker */ } // CreateInlineSliceFromWords makes a new inline list from words, // that will be space-separated. func CreateInlineSliceFromWords(words ...string) InlineSlice { inl := make(InlineSlice, 0, 2*len(words)-1) for i, word := range words { if i > 0 { inl = append(inl, &SpaceNode{Lexeme: " "}) } inl = append(inl, &TextNode{Text: word}) } return inl } // WalkChildren walks down to the list. func (is *InlineSlice) WalkChildren(v Visitor) { for _, in := range *is { Walk(v, in) } } // -------------------------------------------------------------------------- // TextNode just contains some text. type TextNode struct { Text string // The text itself. } func (*TextNode) inlineNode() { /* Just a marker */ } // WalkChildren does nothing. func (*TextNode) WalkChildren(Visitor) { /* No children*/ } // -------------------------------------------------------------------------- // SpaceNode tracks inter-word space characters. type SpaceNode struct { Lexeme string } func (*SpaceNode) inlineNode() { /* Just a marker */ } // WalkChildren does nothing. func (*SpaceNode) WalkChildren(Visitor) { /* No children*/ } // Count returns the number of space runes. func (sn *SpaceNode) Count() int { return utf8.RuneCountInString(sn.Lexeme) } // -------------------------------------------------------------------------- // BreakNode signals a new line that must / should be interpreted as a new line break. type BreakNode struct { Hard bool // Hard line break? } func (*BreakNode) inlineNode() { /* Just a marker */ } // WalkChildren does nothing. func (*BreakNode) WalkChildren(Visitor) { /* No children*/ } // -------------------------------------------------------------------------- // LinkNode contains the specified link. type LinkNode struct { Attrs attrs.Attributes // Optional attributes Ref *Reference Inlines InlineSlice // The text associated with the link. } func (*LinkNode) inlineNode() { /* Just a marker */ } // WalkChildren walks to the link text. func (ln *LinkNode) WalkChildren(v Visitor) { if len(ln.Inlines) > 0 { Walk(v, &ln.Inlines) } } // -------------------------------------------------------------------------- // EmbedRefNode contains the specified embedded reference material. type EmbedRefNode struct { Attrs attrs.Attributes // Optional attributes Ref *Reference // The reference to be embedded. Syntax string // Syntax of referenced material, if known Inlines InlineSlice // Optional text associated with the image. } func (*EmbedRefNode) inlineNode() { /* Just a marker */ } // WalkChildren walks to the text that describes the embedded material. func (en *EmbedRefNode) WalkChildren(v Visitor) { Walk(v, &en.Inlines) } // -------------------------------------------------------------------------- // EmbedBLOBNode contains the specified embedded BLOB material. type EmbedBLOBNode struct { Attrs attrs.Attributes // Optional attributes Syntax string // Syntax of Blob Blob []byte // BLOB data itself. Inlines InlineSlice // Optional text associated with the image. } func (*EmbedBLOBNode) inlineNode() { /* Just a marker */ } // WalkChildren walks to the text that describes the embedded material. func (en *EmbedBLOBNode) WalkChildren(v Visitor) { Walk(v, &en.Inlines) } // -------------------------------------------------------------------------- // CiteNode contains the specified citation. type CiteNode struct { Attrs attrs.Attributes // Optional attributes Key string // The citation key Inlines InlineSlice // Optional text associated with the citation. } func (*CiteNode) inlineNode() { /* Just a marker */ } // WalkChildren walks to the cite text. func (cn *CiteNode) WalkChildren(v Visitor) { Walk(v, &cn.Inlines) } // -------------------------------------------------------------------------- // MarkNode contains the specified merked position. // It is a BlockNode too, because although it is typically parsed during inline // mode, it is moved into block mode afterwards. type MarkNode struct { Mark string // The mark text itself Slug string // Slugified form of Mark Fragment string // Unique form of Slug Inlines InlineSlice // Marked inline content } func (*MarkNode) inlineNode() { /* Just a marker */ } // WalkChildren does nothing. func (mn *MarkNode) WalkChildren(v Visitor) { if len(mn.Inlines) > 0 { Walk(v, &mn.Inlines) } } // -------------------------------------------------------------------------- // FootnoteNode contains the specified footnote. type FootnoteNode struct { Attrs attrs.Attributes // Optional attributes Inlines InlineSlice // The footnote text. } func (*FootnoteNode) inlineNode() { /* Just a marker */ } // WalkChildren walks to the footnote text. func (fn *FootnoteNode) WalkChildren(v Visitor) { Walk(v, &fn.Inlines) } // -------------------------------------------------------------------------- // FormatNode specifies some inline formatting. type FormatNode struct { Kind FormatKind Attrs attrs.Attributes // Optional attributes. Inlines InlineSlice } // FormatKind specifies the format that is applied to the inline nodes. type FormatKind int // Constants for FormatCode const ( _ FormatKind = iota FormatEmph // Emphasized text. FormatStrong // Strongly emphasized text. FormatInsert // Inserted text. FormatDelete // Deleted text. FormatSuper // Superscripted text. FormatSub // SubscriptedText. FormatQuote // Quoted text. FormatSpan // Generic inline container. ) func (*FormatNode) inlineNode() { /* Just a marker */ } // WalkChildren walks to the formatted text. func (fn *FormatNode) WalkChildren(v Visitor) { Walk(v, &fn.Inlines) } // -------------------------------------------------------------------------- // LiteralNode specifies some uninterpreted text. type LiteralNode struct { Kind LiteralKind Attrs attrs.Attributes // Optional attributes. Content []byte } // LiteralKind specifies the format that is applied to code inline nodes. type LiteralKind int // Constants for LiteralCode const ( _ LiteralKind = iota LiteralZettel // Zettel content LiteralProg // Inline program code LiteralInput // Computer input, e.g. Keyboard strokes LiteralOutput // Computer output LiteralComment // Inline comment LiteralHTML // Inline HTML, e.g. for Markdown LiteralMath // Inline math mode ) func (*LiteralNode) inlineNode() { /* Just a marker */ } // WalkChildren does nothing. func (*LiteralNode) WalkChildren(Visitor) { /* No children*/ } |
Added ast/ref.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 | //----------------------------------------------------------------------------- // Copyright (c) 2020-present Detlef Stern // // This file is part of Zettelstore. // // Zettelstore is licensed under the latest version of the EUPL (European Union // Public License). Please see file LICENSE.txt for your rights and obligations // under this license. //----------------------------------------------------------------------------- package ast import ( "net/url" "strings" "zettelstore.de/z/domain/id" ) // QueryPrefix is the prefix that denotes a query expression. const QueryPrefix = "query:" // ParseReference parses a string and returns a reference. func ParseReference(s string) *Reference { if invalidReference(s) { return &Reference{URL: nil, Value: s, State: RefStateInvalid} } if strings.HasPrefix(s, QueryPrefix) { return &Reference{URL: nil, Value: s[len(QueryPrefix):], State: RefStateQuery} } if state, ok := localState(s); ok { if state == RefStateBased { s = s[1:] } u, err := url.Parse(s) if err == nil { return &Reference{URL: u, Value: s, State: state} } } u, err := url.Parse(s) if err != nil { return &Reference{URL: nil, Value: s, State: RefStateInvalid} } if !externalURL(u) { if _, err = id.Parse(u.Path); err == nil { return &Reference{URL: u, Value: s, State: RefStateZettel} } if u.Path == "" && u.Fragment != "" { return &Reference{URL: u, Value: s, State: RefStateSelf} } } return &Reference{URL: u, Value: s, State: RefStateExternal} } func invalidReference(s string) bool { return s == "" || s == "00000000000000" } func externalURL(u *url.URL) bool { return u.Scheme != "" || u.Opaque != "" || u.Host != "" || u.User != nil } func localState(path string) (RefState, bool) { if len(path) > 0 && path[0] == '/' { if len(path) > 1 && path[1] == '/' { return RefStateBased, true } return RefStateHosted, true } if len(path) > 1 && path[0] == '.' { if len(path) > 2 && path[1] == '.' && path[2] == '/' { return RefStateHosted, true } return RefStateHosted, path[1] == '/' } return RefStateInvalid, false } // String returns the string representation of a reference. func (r Reference) String() string { if r.URL != nil { return r.URL.String() } if r.State == RefStateQuery { return QueryPrefix + r.Value } return r.Value } // IsValid returns true if reference is valid func (r *Reference) IsValid() bool { return r.State != RefStateInvalid } // IsZettel returns true if it is a referencen to a local zettel. func (r *Reference) IsZettel() bool { switch r.State { case RefStateZettel, RefStateSelf, RefStateFound, RefStateBroken: return true } return false } // IsLocal returns true if reference is local func (r *Reference) IsLocal() bool { return r.State == RefStateHosted || r.State == RefStateBased } // IsExternal returns true if it is a referencen to external material. func (r *Reference) IsExternal() bool { return r.State == RefStateExternal } |
Added ast/ref_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 | //----------------------------------------------------------------------------- // Copyright (c) 2020-present Detlef Stern // // This file is part of Zettelstore. // // Zettelstore is licensed under the latest version of the EUPL (European Union // Public License). Please see file LICENSE.txt for your rights and obligations // under this license. //----------------------------------------------------------------------------- package ast_test 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"}, {",://", true, ""}, } for i, tc := range testcases { got := ast.ParseReference(tc.link) if got.IsValid() == tc.err { t.Errorf( "TC=%d, expected parse error of %q: %v, but got %q", i, tc.link, tc.err, got) } 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}, {"00000000000000", false, false, false}, {"http://zettelstore.de/z/ast", false, true, false}, {"12345678901234", true, false, false}, {"12345678901234#local", true, false, false}, {"http://12345678901234", false, true, false}, {"http://zettelstore.de/z/12345678901234", false, true, false}, {"http://zettelstore.de/12345678901234", false, true, false}, {"/12345678901234", false, false, true}, {"//12345678901234", false, false, true}, {"./12345678901234", false, false, true}, {"../12345678901234", false, false, true}, {".../12345678901234", false, true, false}, } for i, tc := range testcases { ref := ast.ParseReference(tc.link) isZettel := ref.IsZettel() if isZettel != tc.isZettel { t.Errorf( "TC=%d, Reference %q isZettel=%v expected, but got %v", i, tc.link, tc.isZettel, isZettel) } isLocal := ref.IsLocal() if isLocal != tc.isLocal { t.Errorf( "TC=%d, Reference %q isLocal=%v expected, but got %v", i, tc.link, tc.isLocal, isLocal) } isExternal := ref.IsExternal() if isExternal != tc.isExternal { t.Errorf( "TC=%d, Reference %q isExternal=%v expected, but got %v", i, tc.link, tc.isExternal, isExternal) } } } |
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 | //----------------------------------------------------------------------------- // Copyright (c) 2021-present Detlef Stern // // This file is part of Zettelstore. // // Zettelstore is licensed under the latest version of the EUPL (European Union // Public License). Please see file LICENSE.txt for your rights and obligations // under this license. //----------------------------------------------------------------------------- package ast // Visitor is a visitor for walking the AST. type Visitor interface { Visit(node Node) Visitor } // Walk traverses the AST. func Walk(v Visitor, node Node) { if v = v.Visit(node); v == nil { return } // Implementation note: // It is much faster to use interface dispatching than to use a switch statement. // On my "cpu: Intel(R) Core(TM) i7-6820HQ CPU @ 2.70GHz", a switch statement // implementation tooks approx 940-980 ns/op. Interface dispatching is in the // range of 900-930 ns/op. node.WalkChildren(v) v.Visit(nil) } // WalkItemSlice traverses an item slice. func WalkItemSlice(v Visitor, ins ItemSlice) { for _, in := range ins { Walk(v, in) } } // WalkDescriptionSlice traverses an item slice. func WalkDescriptionSlice(v Visitor, dns DescriptionSlice) { for _, dn := range dns { Walk(v, dn) } } |
Added ast/walk_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 | //----------------------------------------------------------------------------- // Copyright (c) 2021-present Detlef Stern // // This file is part of Zettelstore. // // Zettelstore is licensed under the latest version of the EUPL (European Union // Public License). Please see file LICENSE.txt for your rights and obligations // under this license. //----------------------------------------------------------------------------- package ast_test import ( "testing" "zettelstore.de/c/attrs" "zettelstore.de/z/ast" ) func BenchmarkWalk(b *testing.B) { root := ast.BlockSlice{ &ast.HeadingNode{ Inlines: ast.CreateInlineSliceFromWords("A", "Simple", "Heading"), }, &ast.ParaNode{ Inlines: ast.CreateInlineSliceFromWords("This", "is", "the", "introduction."), }, &ast.NestedListNode{ Kind: ast.NestedListUnordered, Items: []ast.ItemSlice{ []ast.ItemNode{ &ast.ParaNode{ Inlines: ast.CreateInlineSliceFromWords("Item", "1"), }, }, []ast.ItemNode{ &ast.ParaNode{ Inlines: ast.CreateInlineSliceFromWords("Item", "2"), }, }, }, }, &ast.ParaNode{ Inlines: ast.CreateInlineSliceFromWords("This", "is", "some", "intermediate", "text."), }, ast.CreateParaNode( &ast.FormatNode{ Kind: ast.FormatEmph, Attrs: attrs.Attributes(map[string]string{ "": "class", "color": "green", }), Inlines: ast.CreateInlineSliceFromWords("This", "is", "some", "emphasized", "text."), }, &ast.SpaceNode{Lexeme: " "}, &ast.LinkNode{ Ref: &ast.Reference{Value: "http://zettelstore.de"}, Inlines: ast.CreateInlineSliceFromWords("URL", "text."), }, ), } v := benchVisitor{} b.ResetTimer() for n := 0; n < b.N; n++ { ast.Walk(&v, &root) } } type benchVisitor struct{} func (bv *benchVisitor) Visit(ast.Node) ast.Visitor { return bv } |
Added auth/auth.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 | //----------------------------------------------------------------------------- // Copyright (c) 2021-present Detlef Stern // // This file is part of Zettelstore. // // Zettelstore is licensed under the latest version of the EUPL (European Union // Public License). Please see file LICENSE.txt for your rights and obligations // under this license. //----------------------------------------------------------------------------- // Package 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" ) // 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 } // TokenManager provides methods to create authentication type TokenManager interface { // GetToken produces a authentication token. GetToken(ident *meta.Meta, d time.Duration, kind TokenKind) ([]byte, error) // CheckToken checks the validity of the token and returns relevant data. CheckToken(token []byte, k TokenKind) (TokenData, error) } // TokenKind specifies for which application / usage a token is/was requested. type TokenKind int // Allowed values of token kind const ( _ TokenKind = iota KindJSON KindHTML ) // TokenData contains some important elements from a token. type TokenData struct { Token []byte Now time.Time Issued time.Time Expires time.Time Ident string Zid id.Zid } // AuthzManager provides methods for authorization. type AuthzManager interface { BaseManager // Owner returns the zettel identifier of the owner. Owner() id.Zid // IsOwner returns true, if the given zettel identifier is that of the owner. IsOwner(zid id.Zid) bool // Returns true if authentication is enabled. WithAuth() bool // GetUserRole role returns the user role of the given user zettel. GetUserRole(user *meta.Meta) meta.UserRole } // Manager is the main interface for providing the service. type Manager interface { TokenManager AuthzManager BoxWithPolicy(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 // User is allowed to read zettel CanRead(user, m *meta.Meta) bool // User is allowed to write zettel. CanWrite(user, oldMeta, newMeta *meta.Meta) bool // User is allowed to rename zettel CanRename(user, m *meta.Meta) bool // User is allowed to delete zettel. CanDelete(user, m *meta.Meta) bool // User is allowed to refresh box data. CanRefresh(user *meta.Meta) bool } |
Added auth/cred/cred.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-present Detlef Stern // // This file is part of Zettelstore. // // Zettelstore is licensed under the latest version of the EUPL (European Union // Public License). Please see file LICENSE.txt for your rights and obligations // under this license. //----------------------------------------------------------------------------- // Package cred provides some function for handling credentials. package cred import ( "bytes" "golang.org/x/crypto/bcrypt" "zettelstore.de/z/domain/id" ) // HashCredential returns a hashed vesion of the given credential func HashCredential(zid id.Zid, ident, credential string) (string, error) { fullCredential := createFullCredential(zid, ident, credential) res, err := bcrypt.GenerateFromPassword(fullCredential, bcrypt.DefaultCost) if err != nil { return "", err } return string(res), nil } // CompareHashAndCredential checks, whether the hashed credential is a possible // value when hashing the credential. func CompareHashAndCredential(hashed string, zid id.Zid, ident, credential string) (bool, error) { fullCredential := createFullCredential(zid, ident, credential) err := bcrypt.CompareHashAndPassword([]byte(hashed), fullCredential) if err == nil { return true, nil } if err == bcrypt.ErrMismatchedHashAndPassword { return false, nil } return false, err } func createFullCredential(zid id.Zid, ident, credential string) []byte { var buf bytes.Buffer buf.WriteString(zid.String()) buf.WriteByte(' ') buf.WriteString(ident) buf.WriteByte(' ') buf.WriteString(credential) return buf.Bytes() } |
Added auth/impl/impl.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 | //----------------------------------------------------------------------------- // Copyright (c) 2021-present Detlef Stern // // This file is part of Zettelstore. // // Zettelstore is licensed under the latest version of the EUPL (European Union // Public License). Please see file LICENSE.txt for your rights and obligations // under this license. //----------------------------------------------------------------------------- // Package impl provides services for authentification / authorization. package impl import ( "errors" "hash/fnv" "io" "time" "github.com/pascaldekloe/jwt" "zettelstore.de/c/api" "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" ) type myAuth struct { readonly bool owner id.Zid secret []byte } // New creates a new auth object. func New(readonly bool, owner id.Zid, extSecret string) auth.Manager { return &myAuth{ readonly: readonly, owner: owner, secret: calcSecret(extSecret), } } var configKeys = []string{ kernel.CoreProgname, kernel.CoreGoVersion, kernel.CoreHostname, kernel.CoreGoOS, kernel.CoreGoArch, kernel.CoreVersion, } func calcSecret(extSecret string) []byte { h := fnv.New128() if extSecret != "" { io.WriteString(h, extSecret) } for _, key := range configKeys { io.WriteString(h, kernel.Main.GetConfig(kernel.CoreService, key).(string)) } return h.Sum(nil) } // IsReadonly returns true, if the systems is configured to run in read-only-mode. func (a *myAuth) IsReadonly() bool { return a.readonly } const reqHash = jwt.HS512 // ErrNoIdent signals that the 'ident' key is missing. var ErrNoIdent = errors.New("auth: missing ident") // ErrOtherKind signals that the token was defined for another token kind. var ErrOtherKind = errors.New("auth: wrong token kind") // ErrNoZid signals that the 'zid' key is missing. var ErrNoZid = errors.New("auth: missing zettel id") // GetToken returns a token to be used for authentification. func (a *myAuth) GetToken(ident *meta.Meta, d time.Duration, kind auth.TokenKind) ([]byte, error) { subject, ok := ident.Get(api.KeyUserID) if !ok || subject == "" { return nil, ErrNoIdent } now := time.Now().Round(time.Second) claims := jwt.Claims{ Registered: jwt.Registered{ Subject: subject, Expires: jwt.NewNumericTime(now.Add(d)), Issued: jwt.NewNumericTime(now), }, Set: map[string]interface{}{ "zid": ident.Zid.String(), "_tk": int(kind), }, } token, err := claims.HMACSign(reqHash, a.secret) if err != nil { return nil, err } return token, nil } // ErrTokenExpired signals an exired token var ErrTokenExpired = errors.New("auth: token expired") // CheckToken checks the validity of the token and returns relevant data. func (a *myAuth) CheckToken(token []byte, k auth.TokenKind) (auth.TokenData, error) { h, err := jwt.NewHMAC(reqHash, a.secret) if err != nil { return auth.TokenData{}, err } claims, err := h.Check(token) if err != nil { return auth.TokenData{}, err } now := time.Now().Round(time.Second) expires := claims.Expires.Time() if expires.Before(now) { return auth.TokenData{}, ErrTokenExpired } ident := claims.Subject if ident == "" { return auth.TokenData{}, ErrNoIdent } if zidS, ok := claims.Set["zid"].(string); ok { if zid, err2 := id.Parse(zidS); err2 == nil { if kind, ok2 := claims.Set["_tk"].(float64); ok2 { if auth.TokenKind(kind) == k { return auth.TokenData{ Token: token, Now: now, Issued: claims.Issued.Time(), Expires: expires, Ident: ident, Zid: zid, }, nil } } return auth.TokenData{}, ErrOtherKind } } return auth.TokenData{}, ErrNoZid } func (a *myAuth) Owner() id.Zid { return a.owner } func (a *myAuth) IsOwner(zid id.Zid) bool { return zid.IsValid() && zid == a.owner } func (a *myAuth) WithAuth() bool { return a.owner != id.Invalid } // GetUserRole role returns the user role of the given user zettel. func (a *myAuth) GetUserRole(user *meta.Meta) meta.UserRole { if user == nil { if a.WithAuth() { return meta.UserRoleUnknown } return meta.UserRoleOwner } if a.IsOwner(user.Zid) { return meta.UserRoleOwner } if val, ok := user.Get(api.KeyUserRole); ok { if ur := meta.GetUserRole(val); ur != meta.UserRoleUnknown { return ur } } return meta.UserRoleReader } func (a *myAuth) BoxWithPolicy(unprotectedBox box.Box, rtConfig config.Config) (box.Box, auth.Policy) { return policy.BoxWithPolicy(a, unprotectedBox, rtConfig) } |
Added auth/policy/anon.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-present Detlef Stern // // This file is part of Zettelstore. // // Zettelstore is licensed under the latest version of the EUPL (European Union // Public License). Please see file LICENSE.txt for your rights and obligations // under this license. //----------------------------------------------------------------------------- package policy import ( "zettelstore.de/z/auth" "zettelstore.de/z/config" "zettelstore.de/z/domain/meta" ) type anonPolicy struct { authConfig config.AuthConfig pre auth.Policy } func (ap *anonPolicy) CanCreate(user, newMeta *meta.Meta) bool { return ap.pre.CanCreate(user, newMeta) } func (ap *anonPolicy) CanRead(user, m *meta.Meta) bool { return ap.pre.CanRead(user, m) && ap.checkVisibility(m) } func (ap *anonPolicy) CanWrite(user, oldMeta, newMeta *meta.Meta) bool { return ap.pre.CanWrite(user, oldMeta, newMeta) && ap.checkVisibility(oldMeta) } func (ap *anonPolicy) CanRename(user, m *meta.Meta) bool { return ap.pre.CanRename(user, m) && ap.checkVisibility(m) } func (ap *anonPolicy) CanDelete(user, m *meta.Meta) bool { return ap.pre.CanDelete(user, m) && ap.checkVisibility(m) } func (ap *anonPolicy) CanRefresh(user *meta.Meta) bool { if ap.authConfig.GetExpertMode() || ap.authConfig.GetSimpleMode() { return true } return ap.pre.CanRefresh(user) } func (ap *anonPolicy) checkVisibility(m *meta.Meta) bool { if ap.authConfig.GetVisibility(m) == meta.VisibilityExpert { return ap.authConfig.GetExpertMode() } return true } |
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 165 166 167 168 | //----------------------------------------------------------------------------- // Copyright (c) 2020-present Detlef Stern // // This file is part of Zettelstore. // // Zettelstore is licensed under the latest version of the EUPL (European Union // Public License). Please see file LICENSE.txt for your rights and obligations // under this license. //----------------------------------------------------------------------------- package 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/query" "zettelstore.de/z/web/server" ) // BoxWithPolicy wraps the given box inside a policy box. func BoxWithPolicy( manager auth.AuthzManager, box box.Box, authConfig config.AuthConfig, ) (box.Box, auth.Policy) { pol := newPolicy(manager, authConfig) return newBox(box, pol), pol } // polBox implements a policy box. type polBox struct { box box.Box policy auth.Policy } // newBox creates a new policy box. func newBox(box box.Box, policy auth.Policy) box.Box { return &polBox{ box: box, policy: policy, } } func (pp *polBox) Location() string { return pp.box.Location() } func (pp *polBox) CanCreateZettel(ctx context.Context) bool { return pp.box.CanCreateZettel(ctx) } func (pp *polBox) CreateZettel(ctx context.Context, zettel domain.Zettel) (id.Zid, error) { user := server.GetUser(ctx) if pp.policy.CanCreate(user, zettel.Meta) { return pp.box.CreateZettel(ctx, zettel) } return id.Invalid, box.NewErrNotAllowed("Create", user, id.Invalid) } func (pp *polBox) GetZettel(ctx context.Context, zid id.Zid) (domain.Zettel, error) { zettel, err := pp.box.GetZettel(ctx, zid) if err != nil { return domain.Zettel{}, err } user := server.GetUser(ctx) if pp.policy.CanRead(user, zettel.Meta) { return zettel, nil } return domain.Zettel{}, box.NewErrNotAllowed("GetZettel", user, zid) } func (pp *polBox) GetAllZettel(ctx context.Context, zid id.Zid) ([]domain.Zettel, error) { return pp.box.GetAllZettel(ctx, zid) } func (pp *polBox) GetMeta(ctx context.Context, zid id.Zid) (*meta.Meta, error) { m, err := pp.box.GetMeta(ctx, zid) if err != nil { return nil, err } user := server.GetUser(ctx) 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", server.GetUser(ctx), id.Invalid) } func (pp *polBox) SelectMeta(ctx context.Context, q *query.Query) ([]*meta.Meta, error) { user := server.GetUser(ctx) canRead := pp.policy.CanRead q = q.SetPreMatch(func(m *meta.Meta) bool { return canRead(user, m) }) return pp.box.SelectMeta(ctx, q) } 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 := server.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 := server.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 := server.GetUser(ctx) if pp.policy.CanDelete(user, meta) { return pp.box.DeleteZettel(ctx, zid) } return box.NewErrNotAllowed("Delete", user, zid) } func (pp *polBox) Refresh(ctx context.Context) error { user := server.GetUser(ctx) if pp.policy.CanRefresh(user) { return pp.box.Refresh(ctx) } return box.NewErrNotAllowed("Refresh", user, id.Invalid) } |
Added auth/policy/default.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 | //----------------------------------------------------------------------------- // Copyright (c) 2020-present Detlef Stern // // This file is part of Zettelstore. // // Zettelstore is licensed under the latest version of the EUPL (European Union // Public License). Please see file LICENSE.txt for your rights and obligations // under this license. //----------------------------------------------------------------------------- package policy import ( "zettelstore.de/c/api" "zettelstore.de/z/auth" "zettelstore.de/z/domain/meta" ) type defaultPolicy struct { manager auth.AuthzManager } func (*defaultPolicy) CanCreate(_, _ *meta.Meta) bool { return true } func (*defaultPolicy) CanRead(_, _ *meta.Meta) bool { return true } func (d *defaultPolicy) CanWrite(user, oldMeta, _ *meta.Meta) bool { return d.canChange(user, oldMeta) } func (d *defaultPolicy) CanRename(user, m *meta.Meta) bool { return d.canChange(user, m) } func (d *defaultPolicy) CanDelete(user, m *meta.Meta) bool { return d.canChange(user, m) } func (*defaultPolicy) CanRefresh(user *meta.Meta) bool { return user != nil } func (d *defaultPolicy) canChange(user, m *meta.Meta) bool { metaRo, ok := m.Get(api.KeyReadOnly) if !ok { return true } if user == nil { // If we are here, there is no authentication. // See owner.go:CanWrite. // No authentication: check for owner-like restriction, because the user // acts as an owner return metaRo != api.ValueUserRoleOwner && !meta.BoolValue(metaRo) } userRole := d.manager.GetUserRole(user) switch metaRo { case api.ValueUserRoleReader: return userRole > meta.UserRoleReader case api.ValueUserRoleWriter: return userRole > meta.UserRoleWriter case api.ValueUserRoleOwner: return userRole > meta.UserRoleOwner } return !meta.BoolValue(metaRo) } |
Added auth/policy/owner.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 | //----------------------------------------------------------------------------- // Copyright (c) 2020-present Detlef Stern // // This file is part of Zettelstore. // // Zettelstore is licensed under the latest version of the EUPL (European Union // Public License). Please see file LICENSE.txt for your rights and obligations // under this license. //----------------------------------------------------------------------------- package policy import ( "zettelstore.de/c/api" "zettelstore.de/z/auth" "zettelstore.de/z/config" "zettelstore.de/z/domain/meta" ) type ownerPolicy struct { manager auth.AuthzManager authConfig config.AuthConfig pre auth.Policy } func (o *ownerPolicy) CanCreate(user, newMeta *meta.Meta) bool { if user == nil || !o.pre.CanCreate(user, newMeta) { return false } return o.userIsOwner(user) || o.userCanCreate(user, newMeta) } func (o *ownerPolicy) userCanCreate(user, newMeta *meta.Meta) bool { if o.manager.GetUserRole(user) == meta.UserRoleReader { return false } if _, ok := newMeta.Get(api.KeyUserID); ok { return false } return true } func (o *ownerPolicy) CanRead(user, m *meta.Meta) bool { // No need to call o.pre.CanRead(user, meta), because it will always return true. // Both the default and the readonly policy allow to read a zettel. vis := o.authConfig.GetVisibility(m) if res, ok := o.checkVisibility(user, vis); ok { return res } return o.userIsOwner(user) || o.userCanRead(user, m, vis) } func (o *ownerPolicy) userCanRead(user, m *meta.Meta, vis meta.Visibility) bool { switch vis { case meta.VisibilityOwner, meta.VisibilityExpert: return false case meta.VisibilityPublic: return true } if user == nil { return false } if _, ok := m.Get(api.KeyUserID); ok { // 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{ api.KeyID, api.KeyRole, api.KeyUserID, api.KeyUserRole, } func (o *ownerPolicy) CanWrite(user, oldMeta, newMeta *meta.Meta) bool { if user == nil || !o.pre.CanWrite(user, oldMeta, newMeta) { return false } vis := o.authConfig.GetVisibility(oldMeta) if res, ok := o.checkVisibility(user, vis); ok { return res } if o.userIsOwner(user) { return true } if !o.userCanRead(user, oldMeta, vis) { return false } if _, ok := oldMeta.Get(api.KeyUserID); ok { // Here we know, that user.Zid == newMeta.Zid (because of userCanRead) and // user.Zid == newMeta.Zid (because oldMeta.Zid == newMeta.Zid) for _, key := range noChangeUser { if oldMeta.GetDefault(key, "") != newMeta.GetDefault(key, "") { return false } } return true } switch userRole := o.manager.GetUserRole(user); userRole { case meta.UserRoleReader, meta.UserRoleCreator: return false } return o.userCanCreate(user, newMeta) } func (o *ownerPolicy) CanRename(user, m *meta.Meta) bool { if user == nil || !o.pre.CanRename(user, m) { return false } if res, ok := o.checkVisibility(user, o.authConfig.GetVisibility(m)); ok { return res } return o.userIsOwner(user) } func (o *ownerPolicy) CanDelete(user, m *meta.Meta) bool { if user == nil || !o.pre.CanDelete(user, m) { return false } if res, ok := o.checkVisibility(user, o.authConfig.GetVisibility(m)); ok { return res } return o.userIsOwner(user) } func (o *ownerPolicy) CanRefresh(user *meta.Meta) bool { switch userRole := o.manager.GetUserRole(user); userRole { case meta.UserRoleUnknown: return o.authConfig.GetSimpleMode() case meta.UserRoleCreator: return o.authConfig.GetExpertMode() || o.authConfig.GetSimpleMode() } return true } func (o *ownerPolicy) checkVisibility(user *meta.Meta, vis meta.Visibility) (bool, bool) { if vis == meta.VisibilityExpert { return o.userIsOwner(user) && o.authConfig.GetExpertMode(), true } return false, false } func (o *ownerPolicy) userIsOwner(user *meta.Meta) bool { if user == nil { return false } if o.manager.IsOwner(user.Zid) { return true } if val, ok := user.Get(api.KeyUserRole); ok && val == api.ValueUserRoleOwner { return true } return false } |
Added auth/policy/policy.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 | //----------------------------------------------------------------------------- // Copyright (c) 2020-present Detlef Stern // // This file is part of Zettelstore. // // Zettelstore is licensed under the latest version of the EUPL (European Union // Public License). Please see file LICENSE.txt for your rights and obligations // under this license. //----------------------------------------------------------------------------- // Package policy provides some interfaces and implementation for authorizsation policies. package policy import ( "zettelstore.de/z/auth" "zettelstore.de/z/config" "zettelstore.de/z/domain/meta" ) // newPolicy creates a policy based on given constraints. func newPolicy(manager auth.AuthzManager, authConfig config.AuthConfig) auth.Policy { var pol auth.Policy if manager.IsReadonly() { pol = &roPolicy{} } else { pol = &defaultPolicy{manager} } if manager.WithAuth() { pol = &ownerPolicy{ manager: manager, authConfig: authConfig, pre: pol, } } else { pol = &anonPolicy{ authConfig: authConfig, pre: pol, } } return &prePolicy{pol} } type prePolicy struct { post auth.Policy } func (p *prePolicy) CanCreate(user, newMeta *meta.Meta) bool { return newMeta != nil && p.post.CanCreate(user, newMeta) } func (p *prePolicy) CanRead(user, m *meta.Meta) bool { return m != nil && p.post.CanRead(user, m) } func (p *prePolicy) CanWrite(user, oldMeta, newMeta *meta.Meta) bool { return oldMeta != nil && newMeta != nil && oldMeta.Zid == newMeta.Zid && p.post.CanWrite(user, oldMeta, newMeta) } func (p *prePolicy) CanRename(user, m *meta.Meta) bool { return m != nil && p.post.CanRename(user, m) } func (p *prePolicy) CanDelete(user, m *meta.Meta) bool { return m != nil && p.post.CanDelete(user, m) } func (p *prePolicy) CanRefresh(user *meta.Meta) bool { return p.post.CanRefresh(user) } |
Added auth/policy/policy_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 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 593 594 595 596 597 598 599 600 601 602 603 604 605 606 607 608 609 610 611 612 613 614 615 616 617 618 619 620 621 622 623 624 625 626 627 628 629 630 631 632 633 634 635 636 637 638 639 640 641 642 643 644 645 646 647 648 649 650 651 652 653 654 655 656 657 658 659 660 661 662 663 664 665 666 667 668 669 670 671 672 673 674 675 676 677 678 679 680 681 682 683 684 685 686 687 688 689 690 691 692 693 694 695 696 697 698 699 700 701 702 703 704 705 706 707 708 709 710 711 712 713 714 715 716 717 | //----------------------------------------------------------------------------- // Copyright (c) 2020-present Detlef Stern // // This file is part of Zettelstore. // // Zettelstore is licensed under the latest version of the EUPL (European Union // Public License). Please see file LICENSE.txt for your rights and obligations // under this license. //----------------------------------------------------------------------------- package policy import ( "fmt" "testing" "zettelstore.de/c/api" "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 simple bool }{ {true, true, true, true}, {true, true, true, false}, {true, true, false, true}, {true, true, false, false}, {true, false, true, true}, {true, false, true, false}, {true, false, false, true}, {true, false, false, false}, {false, true, true, true}, {false, true, true, false}, {false, true, false, true}, {false, true, false, false}, {false, false, true, true}, {false, false, true, false}, {false, false, false, true}, {false, false, false, false}, } for _, ts := range testScene { pol := newPolicy( &testAuthzManager{readOnly: ts.readonly, withAuth: ts.withAuth}, &authConfig{simple: ts.simple, expert: ts.expert}, ) name := fmt.Sprintf("readonly=%v/withauth=%v/expert=%v/simple=%v", ts.readonly, ts.withAuth, ts.expert, ts.simple) t.Run(name, func(tt *testing.T) { testCreate(tt, pol, ts.withAuth, ts.readonly) testRead(tt, pol, ts.withAuth, ts.expert) testWrite(tt, pol, ts.withAuth, ts.readonly, ts.expert) testRename(tt, pol, ts.withAuth, ts.readonly, ts.expert) testDelete(tt, pol, ts.withAuth, ts.readonly, ts.expert) testRefresh(tt, pol, ts.withAuth, ts.expert, ts.simple) }) } } type testAuthzManager struct { readOnly bool withAuth bool } func (a *testAuthzManager) IsReadonly() bool { return a.readOnly } func (*testAuthzManager) Owner() id.Zid { return ownerZid } func (*testAuthzManager) IsOwner(zid id.Zid) bool { return zid == ownerZid } func (a *testAuthzManager) WithAuth() bool { return a.withAuth } func (a *testAuthzManager) GetUserRole(user *meta.Meta) meta.UserRole { if user == nil { if a.WithAuth() { return meta.UserRoleUnknown } return meta.UserRoleOwner } if a.IsOwner(user.Zid) { return meta.UserRoleOwner } if val, ok := user.Get(api.KeyUserRole); ok { if ur := meta.GetUserRole(val); ur != meta.UserRoleUnknown { return ur } } return meta.UserRoleReader } type authConfig struct{ simple, expert bool } func (ac *authConfig) GetSimpleMode() bool { return ac.simple } func (ac *authConfig) GetExpertMode() bool { return ac.expert } func (*authConfig) GetVisibility(m *meta.Meta) meta.Visibility { if vis, ok := m.Get(api.KeyVisibility); ok { return meta.GetVisibility(vis) } return meta.VisibilityLogin } func testCreate(t *testing.T, pol auth.Policy, withAuth, readonly 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, 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() ownerZettel := newOwnerZettel() expertZettel := newExpertZettel() userZettel := newUserZettel() writerNew := writer.Clone() writerNew.Set(api.KeyUserRole, owner.GetDefault(api.KeyUserRole, "")) roFalse := newRoFalseZettel() roTrue := newRoTrueZettel() roReader := newRoReaderZettel() roWriter := newRoWriterZettel() roOwner := newRoOwnerZettel() notAuthNotReadonly := !withAuth && !readonly testCases := []struct { 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) } }) } } func testRefresh(t *testing.T, pol auth.Policy, withAuth, expert, simple bool) { t.Helper() testCases := []struct { user *meta.Meta exp bool }{ {newAnon(), (!withAuth && expert) || simple}, {newCreator(), !withAuth || expert || simple}, {newReader(), true}, {newWriter(), true}, {newOwner(), true}, {newOwner2(), true}, } for _, tc := range testCases { t.Run("Refresh", func(tt *testing.T) { got := pol.CanRefresh(tc.user) if tc.exp != got { tt.Errorf("exp=%v, but got=%v", tc.exp, got) } }) } } const ( creatorZid = id.Zid(1013) readerZid = id.Zid(1013) writerZid = id.Zid(1015) ownerZid = id.Zid(1017) owner2Zid = id.Zid(1019) zettelZid = id.Zid(1021) visZid = id.Zid(1023) userZid = id.Zid(1025) ) func newAnon() *meta.Meta { return nil } func newCreator() *meta.Meta { user := meta.New(creatorZid) user.Set(api.KeyTitle, "Creator") user.Set(api.KeyUserID, "ceator") user.Set(api.KeyUserRole, api.ValueUserRoleCreator) return user } func newReader() *meta.Meta { user := meta.New(readerZid) user.Set(api.KeyTitle, "Reader") user.Set(api.KeyUserID, "reader") user.Set(api.KeyUserRole, api.ValueUserRoleReader) return user } func newWriter() *meta.Meta { user := meta.New(writerZid) user.Set(api.KeyTitle, "Writer") user.Set(api.KeyUserID, "writer") user.Set(api.KeyUserRole, api.ValueUserRoleWriter) return user } func newOwner() *meta.Meta { user := meta.New(ownerZid) user.Set(api.KeyTitle, "Owner") user.Set(api.KeyUserID, "owner") user.Set(api.KeyUserRole, api.ValueUserRoleOwner) return user } func newOwner2() *meta.Meta { user := meta.New(owner2Zid) user.Set(api.KeyTitle, "Owner 2") user.Set(api.KeyUserID, "owner-2") user.Set(api.KeyUserRole, api.ValueUserRoleOwner) return user } func newZettel() *meta.Meta { m := meta.New(zettelZid) m.Set(api.KeyTitle, "Any Zettel") return m } func newPublicZettel() *meta.Meta { m := meta.New(visZid) m.Set(api.KeyTitle, "Public Zettel") m.Set(api.KeyVisibility, api.ValueVisibilityPublic) return m } func newCreatorZettel() *meta.Meta { m := meta.New(visZid) m.Set(api.KeyTitle, "Creator Zettel") m.Set(api.KeyVisibility, api.ValueVisibilityCreator) return m } func newLoginZettel() *meta.Meta { m := meta.New(visZid) m.Set(api.KeyTitle, "Login Zettel") m.Set(api.KeyVisibility, api.ValueVisibilityLogin) return m } func newOwnerZettel() *meta.Meta { m := meta.New(visZid) m.Set(api.KeyTitle, "Owner Zettel") m.Set(api.KeyVisibility, api.ValueVisibilityOwner) return m } func newExpertZettel() *meta.Meta { m := meta.New(visZid) m.Set(api.KeyTitle, "Expert Zettel") m.Set(api.KeyVisibility, api.ValueVisibilityExpert) return m } func newRoFalseZettel() *meta.Meta { m := meta.New(zettelZid) m.Set(api.KeyTitle, "No r/o Zettel") m.Set(api.KeyReadOnly, api.ValueFalse) return m } func newRoTrueZettel() *meta.Meta { m := meta.New(zettelZid) m.Set(api.KeyTitle, "A r/o Zettel") m.Set(api.KeyReadOnly, api.ValueTrue) return m } func newRoReaderZettel() *meta.Meta { m := meta.New(zettelZid) m.Set(api.KeyTitle, "Reader r/o Zettel") m.Set(api.KeyReadOnly, api.ValueUserRoleReader) return m } func newRoWriterZettel() *meta.Meta { m := meta.New(zettelZid) m.Set(api.KeyTitle, "Writer r/o Zettel") m.Set(api.KeyReadOnly, api.ValueUserRoleWriter) return m } func newRoOwnerZettel() *meta.Meta { m := meta.New(zettelZid) m.Set(api.KeyTitle, "Owner r/o Zettel") m.Set(api.KeyReadOnly, api.ValueUserRoleOwner) return m } func newUserZettel() *meta.Meta { m := meta.New(userZid) m.Set(api.KeyTitle, "Any User") m.Set(api.KeyUserID, "any") return m } |
Added auth/policy/readonly.go.
> > > > > > > > > > > > > > > > > > > > > > | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 | //----------------------------------------------------------------------------- // Copyright (c) 2020-present Detlef Stern // // This file is part of Zettelstore. // // Zettelstore is licensed under the latest version of the EUPL (European Union // Public License). Please see file LICENSE.txt for your rights and obligations // under this license. //----------------------------------------------------------------------------- package policy import "zettelstore.de/z/domain/meta" type roPolicy struct{} func (*roPolicy) CanCreate(_, _ *meta.Meta) bool { return false } func (*roPolicy) CanRead(_, _ *meta.Meta) bool { return true } func (*roPolicy) CanWrite(_, _, _ *meta.Meta) bool { return false } func (*roPolicy) CanRename(_, _ *meta.Meta) bool { return false } func (*roPolicy) CanDelete(_, _ *meta.Meta) bool { return false } func (*roPolicy) CanRefresh(user *meta.Meta) bool { return user != nil } |
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 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 | //----------------------------------------------------------------------------- // Copyright (c) 2020-present Detlef Stern // // This file is part of Zettelstore. // // Zettelstore is licensed under the latest version of the EUPL (European Union // Public License). Please see file LICENSE.txt for your rights and obligations // under this license. //----------------------------------------------------------------------------- // Package box provides a generic interface to zettel boxes. package box import ( "context" "errors" "fmt" "io" "time" "zettelstore.de/c/api" "zettelstore.de/z/domain" "zettelstore.de/z/domain/id" "zettelstore.de/z/domain/meta" "zettelstore.de/z/query" ) // 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) // 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 } // ZidFunc is a function that processes identifier of a zettel. type ZidFunc func(id.Zid) // MetaFunc is a function that processes metadata of a zettel. type MetaFunc func(*meta.Meta) // ManagedBox is the interface of managed boxes. type ManagedBox interface { BaseBox // Apply identifier of every zettel to the given function, if predicate returns true. ApplyZid(context.Context, ZidFunc, query.RetrievePredicate) error // Apply metadata of every zettel to the given function, if predicate returns true. ApplyMeta(context.Context, MetaFunc, query.RetrievePredicate) error // ReadStats populates st with box statistics ReadStats(st *ManagedBoxStats) } // ManagedBoxStats records statistics about the box. type ManagedBoxStats struct { // ReadOnly indicates that the content of a box cannot change. ReadOnly bool // Zettel is the number of zettel managed by the box. Zettel int } // StartState enumerates the possible states of starting and stopping a box. // // StartStateStopped -> StartStateStarting -> StartStateStarted -> StateStateStopping -> StartStateStopped. // // Other transitions are also possible. type StartState uint8 // Constant values of StartState const ( StartStateStopped StartState = iota StartStateStarting StartStateStarted StartStateStopping ) // StartStopper performs simple lifecycle management. type StartStopper interface { // State the current status of the box. State() StartState // Start the box. Now all other functions of the box are allowed. // Starting a box, which is not in state StartStateStopped is not allowed. Start(ctx context.Context) error // Stop the started box. Now only the Start() function is allowed. Stop(ctx context.Context) } // Refresher allow to refresh their internal data. type Refresher interface { // Refresh the box data. Refresh(context.Context) } // Box is to be used outside the box package and its descendants. type Box interface { BaseBox // FetchZids returns the set of all zettel identifer managed by the box. FetchZids(ctx context.Context) (id.Set, error) // SelectMeta returns a list of metadata that comply to the given selection criteria. SelectMeta(ctx context.Context, q *query.Query) ([]*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) // Refresh the data from the box and from its managed sub-boxes. Refresh(context.Context) error } // Stats record stattistics about a box. type Stats struct { // ReadOnly indicates that boxes cannot be modified. ReadOnly bool // NumManagedBoxes is the number of boxes managed. NumManagedBoxes int // Zettel is the number of zettel managed by the box, including // duplicates across managed boxes. ZettelTotal int // LastReload stores the timestamp when a full re-index was done. LastReload time.Time // DurLastReload is the duration of the last full re-index run. DurLastReload time.Duration // IndexesSinceReload counts indexing a zettel since the full re-index. IndexesSinceReload uint64 // ZettelIndexed is the number of zettel managed by the indexer. ZettelIndexed int // IndexUpdates count the number of metadata updates. IndexUpdates uint64 // IndexedWords count the different words indexed. IndexedWords uint64 // IndexedUrls count the different URLs indexed. IndexedUrls uint64 } // Manager is a box-managing box. type Manager interface { Box StartStopper Subject // ReadStats populates st with box statistics ReadStats(st *Stats) // Dump internal data to a Writer. Dump(w io.Writer) } // UpdateReason gives an indication, why the ObserverFunc was called. type UpdateReason uint8 // Values for Reason const ( _ UpdateReason = iota OnReady // Box is started and fully operational OnReload // Box was reloaded OnZettel // Something with a zettel happened ) // UpdateInfo contains all the data about a changed zettel. type UpdateInfo struct { Box BaseBox Reason UpdateReason Zid id.Zid } // UpdateFunc is a function to be called when a change is detected. type UpdateFunc func(UpdateInfo) // Subject is a box that notifies observers about changes. type Subject interface { // RegisterObserver registers an observer that will be notified // if one or all zettel are found to be changed. RegisterObserver(UpdateFunc) } // Enricher is used to update metadata by adding new properties. type Enricher interface { // Enrich computes additional properties and updates the given metadata. // It is typically called by zettel reading methods. Enrich(ctx context.Context, m *meta.Meta, boxNumber int) } // NoEnrichContext will signal an enricher that nothing has to be done. // This is useful for an Indexer, but also for some box.Box calls, when // just the plain metadata is needed. func NoEnrichContext(ctx context.Context) context.Context { return context.WithValue(ctx, ctxNoEnrichKey, &ctxNoEnrichKey) } type ctxNoEnrichType struct{} var ctxNoEnrichKey ctxNoEnrichType // DoNotEnrich determines if the context is marked to not enrich metadata. func DoNotEnrich(ctx context.Context) bool { _, ok := ctx.Value(ctxNoEnrichKey).(*ctxNoEnrichType) return ok } // NoEnrichQuery provides a context that signals not to enrich, if the query does not need this. func NoEnrichQuery(ctx context.Context, q *query.Query) context.Context { if q.EnrichNeeded() { return ctx } return NoEnrichContext(ctx) } // ErrNotAllowed is returned if the caller is not allowed to perform the operation. type ErrNotAllowed struct { Op string User *meta.Meta Zid id.Zid } // NewErrNotAllowed creates an new authorization error. func NewErrNotAllowed(op string, user *meta.Meta, zid id.Zid) error { return &ErrNotAllowed{ Op: op, User: user, Zid: zid, } } func (err *ErrNotAllowed) Error() string { if err.User == nil { if err.Zid.IsValid() { return fmt.Sprintf( "operation %q on zettel %v not allowed for not authorized user", err.Op, err.Zid) } return fmt.Sprintf("operation %q not allowed for not authorized user", err.Op) } if err.Zid.IsValid() { return fmt.Sprintf( "operation %q on zettel %v not allowed for user %v/%v", err.Op, err.Zid, err.User.GetDefault(api.KeyUserID, "?"), err.User.Zid) } return fmt.Sprintf( "operation %q not allowed for user %v/%v", err.Op, err.User.GetDefault(api.KeyUserID, "?"), err.User.Zid) } // Is return true, if the error is of type ErrNotAllowed. func (*ErrNotAllowed) Is(error) bool { return true } // ErrStarted is returned when trying to start an already started box. var ErrStarted = errors.New("box is already started") // ErrStopped is returned if calling methods on a box that was not started. var ErrStopped = errors.New("box is stopped") // ErrReadOnly is returned if there is an attepmt to write to a read-only box. var ErrReadOnly = errors.New("read-only box") // ErrNotFound is returned if a zettel was not found in the box. var ErrNotFound = errors.New("zettel not found") // ErrConflict is returned if a box operation detected a conflict.. // One example: if calculating a new zettel identifier takes too long. var ErrConflict = errors.New("conflict") // ErrCapacity is returned if a box has reached its capacity. var ErrCapacity = errors.New("capacity exceeded") // ErrInvalidID is returned if the zettel id is not appropriate for the box operation. type ErrInvalidID struct{ Zid id.Zid } 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 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 | //----------------------------------------------------------------------------- // Copyright (c) 2020-present Detlef Stern // // This file is part of Zettelstore. // // Zettelstore is licensed under the latest version of the EUPL (European Union // Public License). Please see file LICENSE.txt for your rights and obligations // under this license. //----------------------------------------------------------------------------- // Package compbox provides zettel that have computed content. package compbox import ( "context" "net/url" "zettelstore.de/c/api" "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/kernel" "zettelstore.de/z/logger" "zettelstore.de/z/query" ) func init() { manager.Register( " comp", func(u *url.URL, cdata *manager.ConnectData) (box.ManagedBox, error) { return getCompBox(cdata.Number, cdata.Enricher), nil }) } type compBox struct { log *logger.Logger number int enricher box.Enricher } var myConfig *meta.Meta var myZettel = map[id.Zid]struct { meta func(id.Zid) *meta.Meta content func(*meta.Meta) []byte }{ id.MustParse(api.ZidVersion): {genVersionBuildM, genVersionBuildC}, id.MustParse(api.ZidHost): {genVersionHostM, genVersionHostC}, id.MustParse(api.ZidOperatingSystem): {genVersionOSM, genVersionOSC}, id.MustParse(api.ZidLog): {genLogM, genLogC}, id.MustParse(api.ZidBoxManager): {genManagerM, genManagerC}, id.MustParse(api.ZidMetadataKey): {genKeysM, genKeysC}, id.MustParse(api.ZidParser): {genParserM, genParserC}, id.MustParse(api.ZidStartupConfiguration): {genConfigZettelM, genConfigZettelC}, } // Get returns the one program box. func getCompBox(boxNumber int, mf box.Enricher) box.ManagedBox { return &compBox{ log: kernel.Main.GetLogger(kernel.BoxService).Clone(). Str("box", "comp").Int("boxnum", int64(boxNumber)).Child(), number: boxNumber, enricher: mf, } } // Setup remembers important values. func Setup(cfg *meta.Meta) { myConfig = cfg.Clone() } func (*compBox) Location() string { return "" } func (*compBox) CanCreateZettel(context.Context) bool { return false } func (cb *compBox) CreateZettel(context.Context, domain.Zettel) (id.Zid, error) { cb.log.Trace().Err(box.ErrReadOnly).Msg("CreateZettel") return id.Invalid, box.ErrReadOnly } func (cb *compBox) GetZettel(_ context.Context, zid id.Zid) (domain.Zettel, error) { if gen, ok := myZettel[zid]; ok && gen.meta != nil { if m := gen.meta(zid); m != nil { updateMeta(m) if genContent := gen.content; genContent != nil { cb.log.Trace().Msg("GetMeta/Content") return domain.Zettel{ Meta: m, Content: domain.NewContent(genContent(m)), }, nil } cb.log.Trace().Msg("GetMeta/NoContent") return domain.Zettel{Meta: m}, nil } } cb.log.Trace().Err(box.ErrNotFound).Msg("GetZettel/Err") return domain.Zettel{}, box.ErrNotFound } func (cb *compBox) GetMeta(_ 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) cb.log.Trace().Msg("GetMeta") return m, nil } } } cb.log.Trace().Err(box.ErrNotFound).Msg("GetMeta/Err") return nil, box.ErrNotFound } func (cb *compBox) ApplyZid(_ context.Context, handle box.ZidFunc, constraint query.RetrievePredicate) error { cb.log.Trace().Int("entries", int64(len(myZettel))).Msg("ApplyMeta") for zid, gen := range myZettel { if !constraint(zid) { continue } if genMeta := gen.meta; genMeta != nil { if genMeta(zid) != nil { handle(zid) } } } return nil } func (cb *compBox) ApplyMeta(ctx context.Context, handle box.MetaFunc, constraint query.RetrievePredicate) error { cb.log.Trace().Int("entries", int64(len(myZettel))).Msg("ApplyMeta") for zid, gen := range myZettel { if !constraint(zid) { continue } if genMeta := gen.meta; genMeta != nil { if m := genMeta(zid); m != nil { updateMeta(m) cb.enricher.Enrich(ctx, m, cb.number) handle(m) } } } return nil } func (*compBox) CanUpdateZettel(context.Context, domain.Zettel) bool { return false } func (cb *compBox) UpdateZettel(context.Context, domain.Zettel) error { cb.log.Trace().Err(box.ErrReadOnly).Msg("UpdateZettel") return box.ErrReadOnly } func (*compBox) AllowRenameZettel(_ context.Context, zid id.Zid) bool { _, ok := myZettel[zid] return !ok } func (cb *compBox) RenameZettel(_ context.Context, curZid, _ id.Zid) error { err := box.ErrNotFound if _, ok := myZettel[curZid]; ok { err = box.ErrReadOnly } cb.log.Trace().Err(err).Msg("RenameZettel") return err } func (*compBox) CanDeleteZettel(context.Context, id.Zid) bool { return false } func (cb *compBox) DeleteZettel(_ context.Context, zid id.Zid) error { err := box.ErrNotFound if _, ok := myZettel[zid]; ok { err = box.ErrReadOnly } cb.log.Trace().Err(err).Msg("DeleteZettel") return err } func (cb *compBox) ReadStats(st *box.ManagedBoxStats) { st.ReadOnly = true st.Zettel = len(myZettel) cb.log.Trace().Int("zettel", int64(st.Zettel)).Msg("ReadStats") } func updateMeta(m *meta.Meta) { if _, ok := m.Get(api.KeySyntax); !ok { m.Set(api.KeySyntax, meta.SyntaxZmk) } m.Set(api.KeyRole, api.ValueRoleConfiguration) m.Set(api.KeyLang, api.ValueLangEN) m.Set(api.KeyReadOnly, api.ValueTrue) if _, ok := m.Get(api.KeyVisibility); !ok { m.Set(api.KeyVisibility, api.ValueVisibilityExpert) } } |
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 53 54 | //----------------------------------------------------------------------------- // Copyright (c) 2020-present Detlef Stern // // This file is part of Zettelstore. // // Zettelstore is licensed under the latest version of the EUPL (European Union // Public License). Please see file LICENSE.txt for your rights and obligations // under this license. //----------------------------------------------------------------------------- package compbox import ( "bytes" "zettelstore.de/c/api" "zettelstore.de/z/domain/id" "zettelstore.de/z/domain/meta" "zettelstore.de/z/kernel" ) func genConfigZettelM(zid id.Zid) *meta.Meta { if myConfig == nil { return nil } m := meta.New(zid) m.Set(api.KeyTitle, "Zettelstore Startup Configuration") m.Set(api.KeyCreated, kernel.Main.GetConfig(kernel.CoreService, kernel.CoreStarted).(string)) m.Set(api.KeyVisibility, api.ValueVisibilityExpert) return m } func genConfigZettelC(*meta.Meta) []byte { var buf bytes.Buffer for i, p := range myConfig.Pairs() { if i > 0 { buf.WriteByte('\n') } buf.WriteString("; ''") buf.WriteString(p.Key) buf.WriteString("''") if p.Value != "" { buf.WriteString("\n: ``") for _, r := range p.Value { if r == '`' { buf.WriteByte('\\') } buf.WriteRune(r) } buf.WriteString("``") } } return buf.Bytes() } |
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 39 40 | //----------------------------------------------------------------------------- // Copyright (c) 2020-present Detlef Stern // // This file is part of Zettelstore. // // Zettelstore is licensed under the latest version of the EUPL (European Union // Public License). Please see file LICENSE.txt for your rights and obligations // under this license. //----------------------------------------------------------------------------- package compbox import ( "bytes" "fmt" "zettelstore.de/c/api" "zettelstore.de/z/domain/id" "zettelstore.de/z/domain/meta" "zettelstore.de/z/kernel" ) func genKeysM(zid id.Zid) *meta.Meta { m := meta.New(zid) m.Set(api.KeyTitle, "Zettelstore Supported Metadata Keys") m.Set(api.KeyCreated, kernel.Main.GetConfig(kernel.CoreService, kernel.CoreVTime).(string)) m.Set(api.KeyVisibility, api.ValueVisibilityLogin) return m } func genKeysC(*meta.Meta) []byte { keys := meta.GetSortedKeyDescriptions() var buf bytes.Buffer buf.WriteString("|=Name<|=Type<|=Computed?:|=Property?:\n") for _, kd := range keys { fmt.Fprintf(&buf, "|%v|%v|%v|%v\n", kd.Name, kd.Type.Name, kd.IsComputed(), kd.IsProperty()) } return buf.Bytes() } |
Added box/compbox/log.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 | //----------------------------------------------------------------------------- // Copyright (c) 2021-present Detlef Stern // // This file is part of Zettelstore. // // Zettelstore is licensed under the latest version of the EUPL (European Union // Public License). Please see file LICENSE.txt for your rights and obligations // under this license. //----------------------------------------------------------------------------- package compbox import ( "bytes" "zettelstore.de/c/api" "zettelstore.de/z/domain/id" "zettelstore.de/z/domain/meta" "zettelstore.de/z/kernel" ) func genLogM(zid id.Zid) *meta.Meta { m := meta.New(zid) m.Set(api.KeyTitle, "Zettelstore Log") m.Set(api.KeySyntax, meta.SyntaxText) m.Set(api.KeyCreated, kernel.Main.GetConfig(kernel.CoreService, kernel.CoreStarted).(string)) m.Set(api.KeyModified, kernel.Main.GetLastLogTime().Local().Format(id.ZidLayout)) return m } func genLogC(*meta.Meta) []byte { const tsFormat = "2006-01-02 15:04:05.999999" entries := kernel.Main.RetrieveLogEntries() var buf bytes.Buffer for _, entry := range entries { ts := entry.TS.Format(tsFormat) buf.WriteString(ts) for j := len(ts); j < len(tsFormat); j++ { buf.WriteByte('0') } buf.WriteByte(' ') buf.WriteString(entry.Level.Format()) buf.WriteByte(' ') buf.WriteString(entry.Prefix) buf.WriteByte(' ') buf.WriteString(entry.Message) buf.WriteByte('\n') } return buf.Bytes() } |
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 41 | //----------------------------------------------------------------------------- // Copyright (c) 2021-present Detlef Stern // // This file is part of Zettelstore. // // Zettelstore is licensed under the latest version of the EUPL (European Union // Public License). Please see file LICENSE.txt for your rights and obligations // under this license. //----------------------------------------------------------------------------- package compbox import ( "bytes" "fmt" "zettelstore.de/c/api" "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(api.KeyTitle, "Zettelstore Box Manager") m.Set(api.KeyCreated, kernel.Main.GetConfig(kernel.CoreService, kernel.CoreStarted).(string)) return m } func genManagerC(*meta.Meta) []byte { kvl := kernel.Main.GetServiceStatistics(kernel.BoxService) if len(kvl) == 0 { return nil } var buf bytes.Buffer buf.WriteString("|=Name|=Value>\n") for _, kv := range kvl { fmt.Fprintf(&buf, "| %v | %v\n", kv.Key, kv.Value) } return buf.Bytes() } |
Added box/compbox/parser.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 | //----------------------------------------------------------------------------- // Copyright (c) 2021-present Detlef Stern // // This file is part of Zettelstore. // // Zettelstore is licensed under the latest version of the EUPL (European Union // Public License). Please see file LICENSE.txt for your rights and obligations // under this license. //----------------------------------------------------------------------------- package compbox import ( "bytes" "fmt" "sort" "strings" "zettelstore.de/c/api" "zettelstore.de/z/domain/id" "zettelstore.de/z/domain/meta" "zettelstore.de/z/kernel" "zettelstore.de/z/parser" ) func genParserM(zid id.Zid) *meta.Meta { m := meta.New(zid) m.Set(api.KeyTitle, "Zettelstore Supported Parser") m.Set(api.KeyCreated, kernel.Main.GetConfig(kernel.CoreService, kernel.CoreVTime).(string)) m.Set(api.KeyVisibility, api.ValueVisibilityLogin) return m } func genParserC(*meta.Meta) []byte { var buf bytes.Buffer buf.WriteString("|=Syntax<|=Alt. Value(s):|=Text Parser?:|=Text Format?:|=Image Format?:\n") syntaxes := parser.GetSyntaxes() sort.Strings(syntaxes) for _, syntax := range syntaxes { info := parser.Get(syntax) if info.Name != syntax { continue } altNames := info.AltNames sort.Strings(altNames) fmt.Fprintf( &buf, "|%v|%v|%v|%v|%v\n", syntax, strings.Join(altNames, ", "), info.IsASTParser, info.IsTextFormat, info.IsImageFormat) } return buf.Bytes() } |
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 55 56 57 58 | //----------------------------------------------------------------------------- // Copyright (c) 2020-present Detlef Stern // // This file is part of Zettelstore. // // Zettelstore is licensed under the latest version of the EUPL (European Union // Public License). Please see file LICENSE.txt for your rights and obligations // under this license. //----------------------------------------------------------------------------- package compbox import ( "zettelstore.de/c/api" "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(api.KeyTitle, title) m.Set(api.KeyVisibility, api.ValueVisibilityExpert) return m } func genVersionBuildM(zid id.Zid) *meta.Meta { m := getVersionMeta(zid, "Zettelstore Version") m.Set(api.KeyCreated, kernel.Main.GetConfig(kernel.CoreService, kernel.CoreVTime).(string)) m.Set(api.KeyVisibility, api.ValueVisibilityLogin) return m } func genVersionBuildC(*meta.Meta) []byte { return []byte(kernel.Main.GetConfig(kernel.CoreService, kernel.CoreVersion).(string)) } func genVersionHostM(zid id.Zid) *meta.Meta { m := getVersionMeta(zid, "Zettelstore Host") m.Set(api.KeyCreated, kernel.Main.GetConfig(kernel.CoreService, kernel.CoreStarted).(string)) return m } func genVersionHostC(*meta.Meta) []byte { return []byte(kernel.Main.GetConfig(kernel.CoreService, kernel.CoreHostname).(string)) } func genVersionOSM(zid id.Zid) *meta.Meta { m := getVersionMeta(zid, "Zettelstore Operating System") m.Set(api.KeyCreated, kernel.Main.GetConfig(kernel.CoreService, kernel.CoreStarted).(string)) return m } func genVersionOSC(*meta.Meta) []byte { goOS := kernel.Main.GetConfig(kernel.CoreService, kernel.CoreGoOS).(string) goArch := kernel.Main.GetConfig(kernel.CoreService, kernel.CoreGoArch).(string) result := make([]byte, 0, len(goOS)+len(goArch)+1) result = append(result, goOS...) result = append(result, '/') return append(result, goArch...) } |
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 | *,*::before,*::after { box-sizing: border-box; } html { font-size: 1rem; font-family: serif; scroll-behavior: smooth; height: 100%; } body { margin: 0; min-height: 100vh; line-height: 1.4; background-color: #f8f8f8 ; height: 100%; } nav.zs-menu { background-color: hsl(210, 28%, 90%); overflow: auto; white-space: nowrap; font-family: sans-serif; padding-left: .5rem; } nav.zs-menu > a { float:left; display: block; text-align: center; padding:.41rem .5rem; text-decoration: none; color:black; } nav.zs-menu > a:hover, .zs-dropdown:hover button { background-color: hsl(210, 28%, 80%) } nav.zs-menu form { float: right } nav.zs-menu form input[type=text] { padding: .12rem; border: none; margin-top: .25rem; margin-right: .5rem; } .zs-dropdown { float: left; overflow: hidden; } .zs-dropdown > button { font-size: 16px; border: none; outline: none; color: black; padding:.41rem .5rem; background-color: inherit; font-family: inherit; margin: 0; } .zs-dropdown-content { display: none; position: absolute; background-color: #f9f9f9; min-width: 160px; box-shadow: 0px 8px 16px 0px rgba(0,0,0,0.2); z-index: 1; } .zs-dropdown-content > a { float: none; color: black; padding:.41rem .5rem; text-decoration: none; display: block; text-align: left; } .zs-dropdown-content > a:hover { background-color: hsl(210, 28%, 75%) } .zs-dropdown:hover > .zs-dropdown-content { display: block } main { padding: 0 1rem } article > * + * { margin-top: .5rem } article header { padding: 0; margin: 0; } h1,h2,h3,h4,h5,h6 { font-family:sans-serif; font-weight:normal } h1 { font-size:1.5rem; margin:.65rem 0 } h2 { font-size:1.25rem; margin:.70rem 0 } h3 { font-size:1.15rem; margin:.75rem 0 } h4 { font-size:1.05rem; margin:.8rem 0; font-weight: bold } h5 { font-size:1.05rem; margin:.8rem 0 } h6 { font-size:1.05rem; margin:.8rem 0; font-weight: lighter } p { margin: .5rem 0 0 0 } li,figure,figcaption,dl { margin: 0 } dt { margin: .5rem 0 0 0 } dt+dd { margin-top: 0 } dd { margin: .5rem 0 0 2rem } dd > p:first-child { margin: 0 0 0 0 } blockquote { border-left: 0.5rem solid lightgray; padding-left: 1rem; margin-left: 1rem; margin-right: 2rem; font-style: italic; } blockquote p { margin-bottom: .5rem } blockquote cite { font-style: normal } table { border-collapse: collapse; border-spacing: 0; max-width: 100%; } thead>tr>td { border-bottom: 2px solid hsl(0, 0%, 70%); font-weight: bold } tfoot>tr>td { border-top: 2px solid hsl(0, 0%, 70%); font-weight: bold } td { text-align: left; padding: .25rem .5rem; border-bottom: 1px solid hsl(0, 0%, 85%) } main form { padding: 0 .5em; margin: .5em 0 0 0; } main form:after { content: "."; display: block; height: 0; clear: both; visibility: hidden; } main form div { margin: .5em 0 0 0 } input { font-family: monospace } input[type="submit"],button,select { font: inherit } label { font-family: sans-serif; font-size:.9rem } textarea { font-family: monospace; resize: vertical; width: 100%; } .zs-input { padding: .5em; display:block; border:none; border-bottom:1px solid #ccc; width:100%; } input.zs-primary { float:right } input.zs-secondary { float:left } input.zs-upload { padding-left: 1em; padding-right: 1em; } a:not([class]) { text-decoration-skip-ink: auto } a.broken { text-decoration: line-through } a.external::after { content: "➚"; display: inline-block } img { max-width: 100% } img.right { float: right } ol.zs-endnotes { padding-top: .5rem; border-top: 1px solid; } kbd { font-family:monospace } code,pre { font-family: monospace; font-size: 85%; } code { padding: .1rem .2rem; background: #f0f0f0; border: 1px solid #ccc; border-radius: .25rem; } pre { padding: .5rem .7rem; max-width: 100%; overflow: auto; border: 1px solid #ccc; border-radius: .5rem; background: #f0f0f0; } pre code { font-size: 95%; position: relative; padding: 0; border: none; } div.zs-indication { padding: .5rem .7rem; max-width: 100%; border-radius: .5rem; border: 1px solid black; } div.zs-indication p:first-child { margin-top: 0 } span.zs-indication { border: 1px solid black; border-radius: .25rem; padding: .1rem .2rem; font-size: 95%; } .zs-info { background-color: lightblue; padding: .5rem 1rem; } .zs-warning { background-color: lightyellow; padding: .5rem 1rem; } .zs-error { background-color: lightpink; border-style: none !important; font-weight: bold; } td.left { text-align:left } td.center { text-align:center } td.right { text-align:right } .zs-font-size-0 { font-size:75% } .zs-font-size-1 { font-size:83% } .zs-font-size-2 { font-size:100% } .zs-font-size-3 { font-size:117% } .zs-font-size-4 { font-size:150% } .zs-font-size-5 { font-size:200% } .zs-deprecated { border-style: dashed; padding: .2rem } .zs-meta { font-size:.75rem; color:#444; margin-bottom:1rem; } .zs-meta a { color:#444 } h1+.zs-meta { margin-top:-1rem } nav > details { margin-top:1rem } details > summary { width: 100%; background-color: #eee; font-family:sans-serif; } details > ul { margin-top:0; padding-left:2rem; background-color: #eee; } footer { padding: 0 1rem } @media (prefers-reduced-motion: reduce) { * { animation-duration: 0.01ms !important; animation-iteration-count: 1 !important; transition-duration: 0.01ms !important; scroll-behavior: auto !important; } } |
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="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}}}"> {{#CSSRoleURL}}<link rel="stylesheet" href="{{{CSSRoleURL}}}">{{/CSSRoleURL}} <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="{{{LogoutURL}}}">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> {{#CanRefresh}} <a href="{{{RefreshURL}}}">Refresh</a> {{/CanRefresh}} </nav> </div> {{#NewZettelLinks.Has}} <div class="zs-dropdown"> <button>New</button> <nav class="zs-dropdown-content"> {{#NewZettelLinks.Links}} <a href="{{{URL}}}">{{Text}}</a> {{/NewZettelLinks.Links}} </nav> </div> {{/NewZettelLinks.Has}} <form action="{{{SearchURL}}}"> <input type="text" placeholder="Search.." name="{{QueryKeyQuery}}"> </form> </nav> <main class="content"> {{{Content}}} </main> {{#FooterHTML}}<footer><hr>{{{FooterHTML}}}</footer>{{/FooterHTML}} {{#DebugMode}}<div><b>WARNING: Debug mode is enabled. DO NOT USE IN PRODUCTION!</b></div>{{/DebugMode}} </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 406 407 408 | //----------------------------------------------------------------------------- // Copyright (c) 2020-present Detlef Stern // // This file is part of Zettelstore. // // Zettelstore is licensed under the latest version of the EUPL (European Union // Public License). Please see file LICENSE.txt for your rights and obligations // under this license. //----------------------------------------------------------------------------- // Package constbox puts zettel inside the executable. package constbox import ( "context" _ "embed" // Allow to embed file content "net/url" "zettelstore.de/c/api" "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/kernel" "zettelstore.de/z/logger" "zettelstore.de/z/query" ) func init() { manager.Register( " const", func(u *url.URL, cdata *manager.ConnectData) (box.ManagedBox, error) { return &constBox{ log: kernel.Main.GetLogger(kernel.BoxService).Clone(). Str("box", "const").Int("boxnum", int64(cdata.Number)).Child(), number: cdata.Number, zettel: constZettelMap, enricher: cdata.Enricher, }, nil }) } type constHeader map[string]string type constZettel struct { header constHeader content domain.Content } type constBox struct { log *logger.Logger number int zettel map[id.Zid]constZettel enricher box.Enricher } func (*constBox) Location() string { return "const:" } func (*constBox) CanCreateZettel(context.Context) bool { return false } func (cb *constBox) CreateZettel(context.Context, domain.Zettel) (id.Zid, error) { cb.log.Trace().Err(box.ErrReadOnly).Msg("CreateZettel") return id.Invalid, box.ErrReadOnly } func (cb *constBox) GetZettel(_ context.Context, zid id.Zid) (domain.Zettel, error) { if z, ok := cb.zettel[zid]; ok { cb.log.Trace().Msg("GetZettel") return domain.Zettel{Meta: meta.NewWithData(zid, z.header), Content: z.content}, nil } cb.log.Trace().Err(box.ErrNotFound).Msg("GetZettel") return domain.Zettel{}, box.ErrNotFound } func (cb *constBox) GetMeta(_ context.Context, zid id.Zid) (*meta.Meta, error) { if z, ok := cb.zettel[zid]; ok { cb.log.Trace().Msg("GetMeta") return meta.NewWithData(zid, z.header), nil } cb.log.Trace().Err(box.ErrNotFound).Msg("GetMeta") return nil, box.ErrNotFound } func (cb *constBox) ApplyZid(_ context.Context, handle box.ZidFunc, constraint query.RetrievePredicate) error { cb.log.Trace().Int("entries", int64(len(cb.zettel))).Msg("ApplyZid") for zid := range cb.zettel { if constraint(zid) { handle(zid) } } return nil } func (cb *constBox) ApplyMeta(ctx context.Context, handle box.MetaFunc, constraint query.RetrievePredicate) error { cb.log.Trace().Int("entries", int64(len(cb.zettel))).Msg("ApplyMeta") for zid, zettel := range cb.zettel { if constraint(zid) { m := meta.NewWithData(zid, zettel.header) cb.enricher.Enrich(ctx, m, cb.number) handle(m) } } return nil } func (*constBox) CanUpdateZettel(context.Context, domain.Zettel) bool { return false } func (cb *constBox) UpdateZettel(context.Context, domain.Zettel) error { cb.log.Trace().Err(box.ErrReadOnly).Msg("UpdateZettel") return box.ErrReadOnly } func (cb *constBox) AllowRenameZettel(_ context.Context, zid id.Zid) bool { _, ok := cb.zettel[zid] return !ok } func (cb *constBox) RenameZettel(_ context.Context, curZid, _ id.Zid) error { err := box.ErrNotFound if _, ok := cb.zettel[curZid]; ok { err = box.ErrReadOnly } cb.log.Trace().Err(err).Msg("RenameZettel") return err } func (*constBox) CanDeleteZettel(context.Context, id.Zid) bool { return false } func (cb *constBox) DeleteZettel(_ context.Context, zid id.Zid) error { err := box.ErrNotFound if _, ok := cb.zettel[zid]; ok { err = box.ErrReadOnly } cb.log.Trace().Err(err).Msg("DeleteZettel") return err } func (cb *constBox) ReadStats(st *box.ManagedBoxStats) { st.ReadOnly = true st.Zettel = len(cb.zettel) cb.log.Trace().Int("zettel", int64(st.Zettel)).Msg("ReadStats") } var constZettelMap = map[id.Zid]constZettel{ id.ConfigurationZid: { constHeader{ api.KeyTitle: "Zettelstore Runtime Configuration", api.KeyRole: api.ValueRoleConfiguration, api.KeySyntax: meta.SyntaxNone, api.KeyCreated: "20200804111624", api.KeyVisibility: api.ValueVisibilityOwner, }, domain.NewContent(nil)}, id.MustParse(api.ZidLicense): { constHeader{ api.KeyTitle: "Zettelstore License", api.KeyRole: api.ValueRoleConfiguration, api.KeySyntax: meta.SyntaxText, api.KeyCreated: "20210504135842", api.KeyLang: api.ValueLangEN, api.KeyModified: "20220131153422", api.KeyReadOnly: api.ValueTrue, api.KeyVisibility: api.ValueVisibilityPublic, }, domain.NewContent(contentLicense)}, id.MustParse(api.ZidAuthors): { constHeader{ api.KeyTitle: "Zettelstore Contributors", api.KeyRole: api.ValueRoleConfiguration, api.KeySyntax: meta.SyntaxZmk, api.KeyCreated: "20210504135842", api.KeyLang: api.ValueLangEN, api.KeyReadOnly: api.ValueTrue, api.KeyVisibility: api.ValueVisibilityLogin, }, domain.NewContent(contentContributors)}, id.MustParse(api.ZidDependencies): { constHeader{ api.KeyTitle: "Zettelstore Dependencies", api.KeyRole: api.ValueRoleConfiguration, api.KeySyntax: meta.SyntaxZmk, api.KeyLang: api.ValueLangEN, api.KeyReadOnly: api.ValueTrue, api.KeyVisibility: api.ValueVisibilityLogin, api.KeyCreated: "20210504135842", api.KeyModified: "20221013105100", }, domain.NewContent(contentDependencies)}, id.BaseTemplateZid: { constHeader{ api.KeyTitle: "Zettelstore Base HTML Template", api.KeyRole: api.ValueRoleConfiguration, api.KeySyntax: meta.SyntaxMustache, api.KeyCreated: "20210504135842", api.KeyVisibility: api.ValueVisibilityExpert, }, domain.NewContent(contentBaseMustache)}, id.LoginTemplateZid: { constHeader{ api.KeyTitle: "Zettelstore Login Form HTML Template", api.KeyRole: api.ValueRoleConfiguration, api.KeySyntax: meta.SyntaxMustache, api.KeyCreated: "20200804111624", api.KeyVisibility: api.ValueVisibilityExpert, }, domain.NewContent(contentLoginMustache)}, id.ZettelTemplateZid: { constHeader{ api.KeyTitle: "Zettelstore Zettel HTML Template", api.KeyRole: api.ValueRoleConfiguration, api.KeySyntax: meta.SyntaxMustache, api.KeyCreated: "20200804111624", api.KeyVisibility: api.ValueVisibilityExpert, }, domain.NewContent(contentZettelMustache)}, id.InfoTemplateZid: { constHeader{ api.KeyTitle: "Zettelstore Info HTML Template", api.KeyRole: api.ValueRoleConfiguration, api.KeySyntax: meta.SyntaxMustache, api.KeyCreated: "20200804111624", api.KeyVisibility: api.ValueVisibilityExpert, }, domain.NewContent(contentInfoMustache)}, id.ContextTemplateZid: { constHeader{ api.KeyTitle: "Zettelstore Context HTML Template", api.KeyRole: api.ValueRoleConfiguration, api.KeySyntax: meta.SyntaxMustache, api.KeyCreated: "20210218181140", api.KeyVisibility: api.ValueVisibilityExpert, }, domain.NewContent(contentContextMustache)}, id.FormTemplateZid: { constHeader{ api.KeyTitle: "Zettelstore Form HTML Template", api.KeyRole: api.ValueRoleConfiguration, api.KeySyntax: meta.SyntaxMustache, api.KeyCreated: "20200804111624", api.KeyVisibility: api.ValueVisibilityExpert, }, domain.NewContent(contentFormMustache)}, id.RenameTemplateZid: { constHeader{ api.KeyTitle: "Zettelstore Rename Form HTML Template", api.KeyRole: api.ValueRoleConfiguration, api.KeySyntax: meta.SyntaxMustache, api.KeyCreated: "20200804111624", api.KeyVisibility: api.ValueVisibilityExpert, }, domain.NewContent(contentRenameMustache)}, id.DeleteTemplateZid: { constHeader{ api.KeyTitle: "Zettelstore Delete HTML Template", api.KeyRole: api.ValueRoleConfiguration, api.KeySyntax: meta.SyntaxMustache, api.KeyCreated: "20200804111624", api.KeyVisibility: api.ValueVisibilityExpert, }, domain.NewContent(contentDeleteMustache)}, id.ListTemplateZid: { constHeader{ api.KeyTitle: "Zettelstore List Zettel HTML Template", api.KeyRole: api.ValueRoleConfiguration, api.KeySyntax: meta.SyntaxMustache, api.KeyCreated: "20200804111624", api.KeyVisibility: api.ValueVisibilityExpert, }, domain.NewContent(contentListZettelMustache)}, id.ErrorTemplateZid: { constHeader{ api.KeyTitle: "Zettelstore Error HTML Template", api.KeyRole: api.ValueRoleConfiguration, api.KeySyntax: meta.SyntaxMustache, api.KeyCreated: "20210305133215", api.KeyVisibility: api.ValueVisibilityExpert, }, domain.NewContent(contentErrorMustache)}, id.MustParse(api.ZidBaseCSS): { constHeader{ api.KeyTitle: "Zettelstore Base CSS", api.KeyRole: api.ValueRoleConfiguration, api.KeySyntax: meta.SyntaxCSS, api.KeyCreated: "20200804111624", api.KeyVisibility: api.ValueVisibilityPublic, }, domain.NewContent(contentBaseCSS)}, id.MustParse(api.ZidUserCSS): { constHeader{ api.KeyTitle: "Zettelstore User CSS", api.KeyRole: api.ValueRoleConfiguration, api.KeySyntax: meta.SyntaxCSS, api.KeyCreated: "20210622110143", api.KeyVisibility: api.ValueVisibilityPublic, }, domain.NewContent([]byte("/* User-defined CSS */"))}, id.RoleCSSMapZid: { constHeader{ api.KeyTitle: "Zettelstore Role to CSS Map", api.KeyRole: api.ValueRoleConfiguration, api.KeySyntax: meta.SyntaxNone, api.KeyCreated: "20220321183214", api.KeyVisibility: api.ValueVisibilityExpert, }, domain.NewContent(nil)}, id.EmojiZid: { constHeader{ api.KeyTitle: "Zettelstore Generic Emoji", api.KeyRole: api.ValueRoleConfiguration, api.KeySyntax: meta.SyntaxGif, api.KeyReadOnly: api.ValueTrue, api.KeyCreated: "20210504175807", api.KeyVisibility: api.ValueVisibilityPublic, }, domain.NewContent(contentEmoji)}, id.TOCNewTemplateZid: { constHeader{ api.KeyTitle: "New Menu", api.KeyRole: api.ValueRoleConfiguration, api.KeySyntax: meta.SyntaxZmk, api.KeyLang: api.ValueLangEN, api.KeyCreated: "20210217161829", api.KeyVisibility: api.ValueVisibilityCreator, }, domain.NewContent(contentNewTOCZettel)}, id.MustParse(api.ZidTemplateNewZettel): { constHeader{ api.KeyTitle: "New Zettel", api.KeyRole: api.ValueRoleZettel, api.KeySyntax: meta.SyntaxZmk, api.KeyCreated: "20201028185209", api.KeyVisibility: api.ValueVisibilityCreator, }, domain.NewContent(nil)}, id.MustParse(api.ZidTemplateNewUser): { constHeader{ api.KeyTitle: "New User", api.KeyRole: api.ValueRoleConfiguration, api.KeySyntax: meta.SyntaxNone, api.KeyCreated: "20201028185209", meta.NewPrefix + api.KeyCredential: "", meta.NewPrefix + api.KeyUserID: "", meta.NewPrefix + api.KeyUserRole: api.ValueUserRoleReader, api.KeyVisibility: api.ValueVisibilityOwner, }, domain.NewContent(nil)}, id.DefaultHomeZid: { constHeader{ api.KeyTitle: "Home", api.KeyRole: api.ValueRoleZettel, api.KeySyntax: meta.SyntaxZmk, api.KeyLang: api.ValueLangEN, api.KeyCreated: "20210210190757", }, domain.NewContent(contentHomeZettel)}, } //go:embed license.txt var contentLicense []byte //go:embed contributors.zettel var contentContributors []byte //go:embed dependencies.zettel var contentDependencies []byte //go:embed base.mustache var contentBaseMustache []byte //go:embed login.mustache var contentLoginMustache []byte //go:embed zettel.mustache var contentZettelMustache []byte //go:embed info.mustache var contentInfoMustache []byte //go:embed context.mustache var contentContextMustache []byte //go:embed form.mustache var contentFormMustache []byte //go:embed rename.mustache var contentRenameMustache []byte //go:embed delete.mustache var contentDeleteMustache []byte //go:embed listzettel.mustache var contentListZettelMustache []byte //go:embed error.mustache var contentErrorMustache []byte //go:embed base.css var contentBaseCSS []byte //go:embed emoji_spin.gif var contentEmoji []byte //go:embed newtoc.zettel var contentNewTOCZettel []byte //go:embed home.zettel var contentHomeZettel []byte |
Added box/constbox/context.mustache.
> > > > > > > > > > > > > | 1 2 3 4 5 6 7 8 9 10 11 12 13 | <article> <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> · Cost:{{#Costs}} <a href="{{{URL}}}">{{{Text}}}</a>{{/Costs}} </div> </header> {{{Content}}} </article> |
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 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 | <article> <header> <h1>Delete Zettel {{Zid}}</h1> </header> <p>Do you really want to delete this zettel?</p> {{#HasShadows}} <div class="zs-info"> <h2>Infomation</h2> <p>If you delete this zettel, the previoulsy shadowed zettel from overlayed box {{ShadowedBox}} becomes available.</p> </div> {{/HasShadows}} {{#Incoming.Has}} <div class="zs-warning"> <h2>Warning!</h2> <p>If you delete this zettel, incoming references from the following zettel will become invalid.</p> <ul> {{#Incoming.Links}} <li><a href="{{{URL}}}">{{Text}}</a></li> {{/Incoming.Links}} </ul> </div> {{/Incoming.Has}} {{#HasUselessFiles}} <div class="zs-warning"> <h2>Warning!</h2> <p>Deleting this zettel will also delete the following files, so that they will not be interpreted as content for this zettel.</p> <ul> {{#UselessFiles}} <li>{{{.}}}</li> {{/UselessFiles}} </ul> </div> {{/HasUselessFiles}} <dl> {{#MetaPairs}} <dt>{{Key}}:</dt><dd>{{Value}}</dd> {{/MetaPairs}} </dl> <form method="POST"> <input class="zs-primary" type="submit" value="Delete"> </form> </article> |
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 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 | Zettelstore is made with the help of other software and other artifacts. Thank you very much! This zettel lists all of them, together with their licenses. === Go runtime and associated libraries ; License : BSD 3-Clause "New" or "Revised" License ``` Copyright (c) 2009 The Go Authors. All rights reserved. Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met: * Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer. * Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution. * Neither the name of Google Inc. nor the names of its contributors may be used to endorse or promote products derived from this software without specific prior written permission. THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. ``` === ASCIIToSVG ; URL : [[https://github.com/asciitosvg/asciitosvg]] ; License : MIT ; Remarks : ASCIIToSVG was incorporated into the source code of Zettelstore, moving it into package ''zettelstore.de/z/parser/draw''. Later, the source code was changed substantially to adapt it to the needs of Zettelstore. ``` Copyright (c) 2015 The ASCIIToSVG Contributors Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. ``` === Fsnotify ; URL : [[https://fsnotify.org/]] ; License : BSD 3-Clause "New" or "Revised" License ; Source : [[https://github.com/fsnotify/fsnotify]] ``` Copyright © 2012 The Go Authors. All rights reserved. Copyright © fsnotify Authors. All rights reserved. Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met: * Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer. * Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution. * Neither the name of Google Inc. nor the names of its contributors may be used to endorse or promote products derived from this software without specific prior written permission. THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. ``` === 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 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 | <article> <header> <h1>{{Heading}}</h1> </header> <form action="{{FormActionURL}}" method="POST" enctype="multipart/form-data"> <div> <label for="zs-title">Title <a title="Main heading of this zettel. You can use inline zettelmarkup.">ⓘ</a></label> <input class="zs-input" type="text" id="zs-title" name="title" placeholder="Title.." value="{{MetaTitle}}" autofocus> </div> <div> <div> <label for="zs-role">Role <a title="One word, without spaces, to set the main role of this zettel.">ⓘ</a></label> <input class="zs-input" type="text" id="zs-role" {{#HasRoleData}}list="zs-role-data"{{/HasRoleData}} name="role" placeholder="role.." value="{{MetaRole}}"> {{#HasRoleData}} <datalist id="zs-role-data"> {{#RoleData}} <option value="{{.}}"> {{/RoleData}} </datalist> {{/HasRoleData}} </div> <label for="zs-tags">Tags <a title="Tags must begin with an '#' sign. They are separated by spaces.">ⓘ</a></label> <input class="zs-input" type="text" id="zs-tags" name="tags" placeholder="#tag" value="{{MetaTags}}"> </div> <div> <label for="zs-meta">Metadata <a title="Other metadata for this zettel. Each line contains a key/value pair, separated by a colon ':'.">ⓘ</a></label> <textarea class="zs-input" id="zs-meta" name="meta" rows="4" placeholder="metakey: metavalue"> {{#MetaPairsRest}} {{Key}}: {{Value}} {{/MetaPairsRest}} </textarea> </div> <div> <label for="zs-syntax">Syntax <a title="Syntax of zettel content below, one word. Typically 'zmk' (for zettelmarkup).">ⓘ</a></label> <input class="zs-input" type="text" id="zs-syntax" {{#HasSyntaxData}}list="zs-syntax-data"{{/HasSyntaxData}} name="syntax" placeholder="syntax.." value="{{MetaSyntax}}"> {{#HasSyntaxData}} <datalist id="zs-syntax-data"> {{#SyntaxData}} <option value="{{.}}"> {{/SyntaxData}} </datalist> {{/HasSyntaxData}}</div> <div> {{#IsTextContent}} <label for="zs-content">Content <a title="Content for this zettel, according to above syntax.">ⓘ</a></label> <textarea class="zs-input zs-content" id="zs-content" name="content" rows="20" placeholder="Your content..">{{Content}}</textarea> {{/IsTextContent}} </div> <div> <input class="zs-primary" type="submit" value="Submit"> <input class="zs-secondary" type="submit" value="Save" formaction="?save"> <input class="zs-upload" type="file" id="zs-file" name="file"> </div> </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 | === Thank you for using Zettelstore! You will find the lastest information about Zettelstore at [[https://zettelstore.de]]. Check that website regulary for [[upgrades|https://zettelstore.de/home/doc/trunk/www/download.wiki]] to the latest version. You should consult the [[change log|https://zettelstore.de/home/doc/trunk/www/changes.wiki]] before upgrading. Sometimes, you have to edit some of your Zettelstore-related zettel before upgrading. Since Zettelstore is currently in a development state, every upgrade might fix some of your problems. If you have problems concerning Zettelstore, do not hesitate to get in [[contact with the main developer|mailto:ds@zettelstore.de]]. === Reporting errors If you have encountered an error, please include the content of the following zettel in your mail (if possible): * [[Zettelstore Version|00000000000001]]: {{00000000000001}} * [[Zettelstore Operating System|00000000000003]] * [[Zettelstore Startup Configuration|00000000000096]] * [[Zettelstore Runtime Configuration|00000000000100]] Additionally, you have to describe, what you have done before that error occurs and what you have expected instead. Please do not forget to include the error message, if there is one. Some of above Zettelstore zettel can only be retrieved if you enabled ""expert mode"". Otherwise, only some zettel are linked. To enable expert mode, edit the zettel [[Zettelstore Runtime Configuration|00000000000100]]: please set the metadata value of the key ''expert-mode'' to true. To do you, enter the string ''expert-mode:true'' inside the edit view of the metadata. === Information about this zettel This zettel is your home zettel. It is part of the Zettelstore software itself. Every time you click on the [[Home|//]] link in the menu bar, you will be redirected to this zettel. You can change the content of this zettel by clicking on ""Edit"" above. This allows you to customize your home zettel. Alternatively, you can designate another zettel as your home zettel. Edit the [[Zettelstore Runtime Configuration|00000000000100]] by adding the metadata key ''home-zettel''. Its value is the identifier of the zettel that should act as the new home zettel. You will find the identifier of each zettel between their ""Edit"" and the ""Info"" link above. The identifier of this zettel is ''00010000000000''. If you provide a wrong identifier, this zettel will be shown as the home zettel. Take a look inside the manual for further details. |
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 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 | <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}} {{#CanCopy}} · <a href="{{{CopyURL}}}">Copy</a>{{/CanCopy}} {{#CanVersion}} · <a href="{{{VersionURL}}}">Version</a>{{/CanVersion}} {{#CanFolge}} · <a href="{{{FolgeURL}}}">Folge</a>{{/CanFolge}} {{#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> <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}} {{#QueryLinks.Has}} <h3>Queries</h3> <ul> {{#QueryLinks.Links}} <li><a href="{{{URL}}}">{{Text}}</a></li> {{/QueryLinks.Links}} </ul> {{/QueryLinks.Has}} {{#HasExtLinks}} <h3>External</h3> <ul> {{#ExtLinks}} <li><a href="{{{.}}}"{{{ExtNewWindow}}}>{{.}}</a></li> {{/ExtLinks}} </ul> {{/HasExtLinks}} <h3>Unlinked</h3> {{{UnLinksContent}}} <form> <label for="phrase">Search Phrase</label> <input class="zs-input" type="text" id="phrase" name="{{QueryKeyPhrase}}" placeholder="Phrase.." value="{{UnLinksPhrase}}"> </form> <h2>Parts and encodings</h2> <table> {{#EvalMatrix}} <tr> <th>{{Header}}</th> {{#Elements}}<td><a href="{{{URL}}}">{{Text}}</a></td> {{/Elements}} </tr> {{/EvalMatrix}} </table> <h3>Parsed (not evaluated)</h3> <table> {{#ParseMatrix}} <tr> <th>{{Header}}</th> {{#Elements}}<td><a href="{{{URL}}}">{{Text}}</a></td> {{/Elements}} </tr> {{/ParseMatrix}} </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-present Detlef Stern Licensed under the EUPL Zettelstore is licensed under the European Union Public License, version 1.2 or later (EUPL v. 1.2). The license is available in the official languages of the EU. The English version is included here. Please see https://joinup.ec.europa.eu/community/eupl/og_page/eupl for official translations of the other languages. ------------------------------------------------------------------------------- EUROPEAN UNION PUBLIC LICENCE v. 1.2 EUPL © the European Union 2007, 2016 This European Union Public Licence (the ‘EUPL’) applies to the Work (as defined below) which is provided under the terms of this Licence. Any use of the Work, other than as authorised under this Licence is prohibited (to the extent such use is covered by a right of the copyright holder of the Work). The Work is provided under the terms of this Licence when the Licensor (as defined below) has placed the following notice immediately following the copyright notice for the Work: Licensed under the EUPL or has expressed by any other means his willingness to license under the EUPL. 1. Definitions In this Licence, the following terms have the following meaning: — ‘The Licence’: this Licence. — ‘The Original Work’: the work or software distributed or communicated by the Licensor under this Licence, available as Source Code and also as Executable Code as the case may be. — ‘Derivative Works’: the works or software that could be created by the Licensee, based upon the Original Work or modifications thereof. This Licence does not define the extent of modification or dependence on the Original Work required in order to classify a work as a Derivative Work; this extent is determined by copyright law applicable in the country mentioned in Article 15. — ‘The Work’: the Original Work or its Derivative Works. — ‘The Source Code’: the human-readable form of the Work which is the most convenient for people to study and modify. — ‘The Executable Code’: any code which has generally been compiled and which is meant to be interpreted by a computer as a program. — ‘The Licensor’: the natural or legal person that distributes or communicates the Work under the Licence. — ‘Contributor(s)’: any natural or legal person who modifies the Work under the Licence, or otherwise contributes to the creation of a Derivative Work. — ‘The Licensee’ or ‘You’: any natural or legal person who makes any usage of the Work under the terms of the Licence. — ‘Distribution’ or ‘Communication’: any act of selling, giving, lending, renting, distributing, communicating, transmitting, or otherwise making available, online or offline, copies of the Work or providing access to its essential functionalities at the disposal of any other natural or legal person. 2. Scope of the rights granted by the Licence The Licensor hereby grants You a worldwide, royalty-free, non-exclusive, sublicensable licence to do the following, for the duration of copyright vested in the Original Work: — use the Work in any circumstance and for all usage, — reproduce the Work, — modify the Work, and make Derivative Works based upon the Work, — communicate to the public, including the right to make available or display the Work or copies thereof to the public and perform publicly, as the case may be, the Work, — distribute the Work or copies thereof, — lend and rent the Work or copies thereof, — sublicense rights in the Work or copies thereof. Those rights can be exercised on any media, supports and formats, whether now known or later invented, as far as the applicable law permits so. In the countries where moral rights apply, the Licensor waives his right to exercise his moral right to the extent allowed by law in order to make effective the licence of the economic rights here above listed. The Licensor grants to the Licensee royalty-free, non-exclusive usage rights to any patents held by the Licensor, to the extent necessary to make use of the rights granted on the Work under this Licence. 3. Communication of the Source Code The Licensor may provide the Work either in its Source Code form, or as Executable Code. If the Work is provided as Executable Code, the Licensor provides in addition a machine-readable copy of the Source Code of the Work along with each copy of the Work that the Licensor distributes or indicates, in a notice following the copyright notice attached to the Work, a repository where the Source Code is easily and freely accessible for as long as the Licensor continues to distribute or communicate the Work. 4. Limitations on copyright Nothing in this Licence is intended to deprive the Licensee of the benefits from any exception or limitation to the exclusive rights of the rights owners in the Work, of the exhaustion of those rights or of other applicable limitations thereto. 5. Obligations of the Licensee The grant of the rights mentioned above is subject to some restrictions and obligations imposed on the Licensee. Those obligations are the following: Attribution right: The Licensee shall keep intact all copyright, patent or trademarks notices and all notices that refer to the Licence and to the disclaimer of warranties. The Licensee must include a copy of such notices and a copy of the Licence with every copy of the Work he/she distributes or communicates. The Licensee must cause any Derivative Work to carry prominent notices stating that the Work has been modified and the date of modification. Copyleft clause: If the Licensee distributes or communicates copies of the Original Works or Derivative Works, this Distribution or Communication will be done under the terms of this Licence or of a later version of this Licence unless the Original Work is expressly distributed only under this version of the Licence — for example by communicating ‘EUPL v. 1.2 only’. The Licensee (becoming Licensor) cannot offer or impose any additional terms or conditions on the Work or Derivative Work that alter or restrict the terms of the Licence. Compatibility clause: If the Licensee Distributes or Communicates Derivative Works or copies thereof based upon both the Work and another work licensed under a Compatible Licence, this Distribution or Communication can be done under the terms of this Compatible Licence. For the sake of this clause, ‘Compatible Licence’ refers to the licences listed in the appendix attached to this Licence. Should the Licensee's obligations under the Compatible Licence conflict with his/her obligations under this Licence, the obligations of the Compatible Licence shall prevail. Provision of Source Code: When distributing or communicating copies of the Work, the Licensee will provide a machine-readable copy of the Source Code or indicate a repository where this Source will be easily and freely available for as long as the Licensee continues to distribute or communicate the Work. Legal Protection: This Licence does not grant permission to use the trade names, trademarks, service marks, or names of the Licensor, except as required for reasonable and customary use in describing the origin of the Work and reproducing the content of the copyright notice. 6. Chain of Authorship The original Licensor warrants that the copyright in the Original Work granted hereunder is owned by him/her or licensed to him/her and that he/she has the power and authority to grant the Licence. Each Contributor warrants that the copyright in the modifications he/she brings to the Work are owned by him/her or licensed to him/her and that he/she has the power and authority to grant the Licence. Each time You accept the Licence, the original Licensor and subsequent Contributors grant You a licence to their contributions to the Work, under the terms of this Licence. 7. Disclaimer of Warranty The Work is a work in progress, which is continuously improved by numerous Contributors. It is not a finished work and may therefore contain defects or ‘bugs’ inherent to this type of development. For the above reason, the Work is provided under the Licence on an ‘as is’ basis and without warranties of any kind concerning the Work, including without limitation merchantability, fitness for a particular purpose, absence of defects or errors, accuracy, non-infringement of intellectual property rights other than copyright as stated in Article 6 of this Licence. This disclaimer of warranty is an essential part of the Licence and a condition for the grant of any rights to the Work. 8. Disclaimer of Liability Except in the cases of wilful misconduct or damages directly caused to natural persons, the Licensor will in no event be liable for any direct or indirect, material or moral, damages of any kind, arising out of the Licence or of the use of the Work, including without limitation, damages for loss of goodwill, work stoppage, computer failure or malfunction, loss of data or any commercial damage, even if the Licensor has been advised of the possibility of such damage. However, the Licensor will be liable under statutory product liability laws as far such laws apply to the Work. 9. Additional agreements While distributing the Work, You may choose to conclude an additional agreement, defining obligations or services consistent with this Licence. However, if accepting obligations, You may act only on your own behalf and on your sole responsibility, not on behalf of the original Licensor or any other Contributor, and only if You agree to indemnify, defend, and hold each Contributor harmless for any liability incurred by, or claims asserted against such Contributor by the fact You have accepted any warranty or additional liability. 10. Acceptance of the Licence The provisions of this Licence can be accepted by clicking on an icon ‘I agree’ placed under the bottom of a window displaying the text of this Licence or by affirming consent in any other similar way, in accordance with the rules of applicable law. Clicking on that icon indicates your clear and irrevocable acceptance of this Licence and all of its terms and conditions. Similarly, you irrevocably accept this Licence and all of its terms and conditions by exercising any rights granted to You by Article 2 of this Licence, such as the use of the Work, the creation by You of a Derivative Work or the Distribution or Communication by You of the Work or copies thereof. 11. Information to the public In case of any Distribution or Communication of the Work by means of electronic communication by You (for example, by offering to download the Work from a remote location) the distribution channel or media (for example, a website) must at least provide to the public the information requested by the applicable law regarding the Licensor, the Licence and the way it may be accessible, concluded, stored and reproduced by the Licensee. 12. Termination of the Licence The Licence and the rights granted hereunder will terminate automatically upon any breach by the Licensee of the terms of the Licence. Such a termination will not terminate the licences of any person who has received the Work from the Licensee under the Licence, provided such persons remain in full compliance with the Licence. 13. Miscellaneous Without prejudice of Article 9 above, the Licence represents the complete agreement between the Parties as to the Work. If any provision of the Licence is invalid or unenforceable under applicable law, this will not affect the validity or enforceability of the Licence as a whole. Such provision will be construed or reformed so as necessary to make it valid and enforceable. The European Commission may publish other linguistic versions or new versions of this Licence or updated versions of the Appendix, so far this is required and reasonable, without reducing the scope of the rights granted by the Licence. New versions of the Licence will be published with a unique version number. All linguistic versions of this Licence, approved by the European Commission, have identical value. Parties can take advantage of the linguistic version of their choice. 14. Jurisdiction Without prejudice to specific agreement between parties, — any litigation resulting from the interpretation of this License, arising between the European Union institutions, bodies, offices or agencies, as a Licensor, and any Licensee, will be subject to the jurisdiction of the Court of Justice of the European Union, as laid down in article 272 of the Treaty on the Functioning of the European Union, — any litigation arising between other parties and resulting from the interpretation of this License, will be subject to the exclusive jurisdiction of the competent court where the Licensor resides or conducts its primary business. 15. Applicable Law Without prejudice to specific agreement between parties, — this Licence shall be governed by the law of the European Union Member State where the Licensor has his seat, resides or has his registered office, — this licence shall be governed by Belgian law if the Licensor has no seat, residence or registered office inside a European Union Member State. Appendix ‘Compatible Licences’ according to Article 5 EUPL are: — GNU General Public License (GPL) v. 2, v. 3 — GNU Affero General Public License (AGPL) v. 3 — Open Software License (OSL) v. 2.1, v. 3.0 — Eclipse Public License (EPL) v. 1.0 — CeCILL v. 2.0, v. 2.1 — Mozilla Public Licence (MPL) v. 2 — GNU Lesser General Public Licence (LGPL) v. 2.1, v. 3 — Creative Commons Attribution-ShareAlike v. 3.0 Unported (CC BY-SA 3.0) for works other than software — European Union Public Licence (EUPL) v. 1.1, v. 1.2 — Québec Free and Open-Source Licence — Reciprocity (LiLiQ-R) or Strong Reciprocity (LiLiQ-R+) The European Commission may update this Appendix to later versions of the above licences without producing a new version of the EUPL, as long as they provide the rights granted in Article 2 of this Licence and protect the covered Source Code from exclusive appropriation. All other changes or additions to this Appendix require the production of a new EUPL version. |
Added box/constbox/listzettel.mustache.
> > > > > > > > > > > > > > | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 | <article> <header> <h1>{{Title}}</h1> </header> <form action="{{{SearchURL}}}"> <input class="zs-input" type="text" placeholder="Search.." name="{{QueryKeyQuery}}" value="{{QueryValue}}"> </form> {{{Content}}} {{#CanCreate}}<form action="{{{CreateURL}}}"> <input type="hidden" name="{{QueryKeyQuery}}" value="{{QueryValue}}"> <input type="hidden" name="{{QueryKeySeed}}" value="{{Seed}}"> <input class="zs-primary" type="submit" value="Save As Zettel"> </form>{{/CanCreate}} </article> |
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=""> <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> <div><input class="zs-primary" type="submit" value="Login"></div> </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 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 | <article> <header> <h1>Rename Zettel {{Zid}}</h1> </header> <p>Do you really want to rename this zettel?</p> {{#Incoming.Has}} <div class="zs-warning"> <h2>Warning!</h2> <p>If you rename this zettel, incoming references from the following zettel will become invalid.</p> <ul> {{#Incoming.Links}} <li><a href="{{{URL}}}">{{Text}}</a></li> {{/Incoming.Links}} </ul> </div> {{/Incoming.Has}} {{#HasUselessFiles}} <div class="zs-warning"> <h2>Warning!</h2> <p>Renaming this zettel will also delete the following files, so that they will not be interpreted as content for a zettel with identifier {{Zid}}.</p> <ul> {{#UselessFiles}} <li>{{{.}}}</li> {{/UselessFiles}} </ul> </div> {{/HasUselessFiles}} <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}}"> <div><input class="zs-primary" type="submit" value="Rename"></div> </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 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 | <article> <header> <h1>{{Heading}}</h1> <div class="zs-meta"> {{#CanWrite}}<a href="{{{EditURL}}}">Edit</a> ·{{/CanWrite}} {{Zid}} · <a href="{{{InfoURL}}}">Info</a> · (<a href="{{{RoleURL}}}">{{RoleText}}</a>) {{#Tags.Has}}· {{#Tags.Links}} <a href="{{{URL}}}">{{Text}}</a>{{/Tags.Links}}{{/Tags.Has}} {{#CanCopy}}· <a href="{{{CopyURL}}}">Copy</a>{{/CanCopy}} {{#CanVersion}}· <a href="{{{VersionURL}}}">Version</a>{{/CanVersion}} {{#CanFolge}}· <a href="{{{FolgeURL}}}">Folge</a>{{/CanFolge}} {{#PredecessorRefs}}<br>Predecessor: {{{PredecessorRefs}}}{{/PredecessorRefs}} {{#PrecursorRefs}}<br>Precursor: {{{PrecursorRefs}}}{{/PrecursorRefs}} {{#HasExtURL}}<br>URL: {{{ExtURL}}}{{/HasExtURL}} {{#Author}}<br>By {{Author}}{{/Author}} </div> </header> {{{Content}}} </article> {{#NeedBottomNav}}<nav>{{/NeedBottomNav}} {{#FolgeLinks.Has}} <details open> <summary>Folgezettel</summary> <ul> {{#FolgeLinks.Links}} <li><a href="{{{URL}}}">{{Text}}</a></li> {{/FolgeLinks.Links}} </ul> </details> {{/FolgeLinks.Has}} {{#BackLinks.Has}} <details open> <summary>Incoming</summary> <ul> {{#BackLinks.Links}} <li><a href="{{{URL}}}">{{Text}}</a></li> {{/BackLinks.Links}} </ul> </details> {{/BackLinks.Has}} {{#SuccessorLinks.Has}} <details open> <summary>Successors</summary> <ul> {{#SuccessorLinks.Links}} <li><a href="{{{URL}}}">{{Text}}</a></li> {{/SuccessorLinks.Links}} </ul> </details> {{/SuccessorLinks.Has}} {{#NeedBottomNav}}</nav>{{/NeedBottomNav}} |
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 | //----------------------------------------------------------------------------- // Copyright (c) 2020-present Detlef Stern // // This file is part of Zettelstore. // // Zettelstore is licensed under the latest version of the EUPL (European Union // Public License). Please see file LICENSE.txt for your rights and obligations // under this license. //----------------------------------------------------------------------------- // Package dirbox provides a directory-based zettel box. package dirbox import ( "context" "errors" "net/url" "os" "path/filepath" "sync" "zettelstore.de/z/box" "zettelstore.de/z/box/manager" "zettelstore.de/z/box/notify" "zettelstore.de/z/domain" "zettelstore.de/z/domain/id" "zettelstore.de/z/domain/meta" "zettelstore.de/z/kernel" "zettelstore.de/z/logger" "zettelstore.de/z/query" ) func init() { manager.Register("dir", func(u *url.URL, cdata *manager.ConnectData) (box.ManagedBox, error) { var log *logger.Logger if krnl := kernel.Main; krnl != nil { log = krnl.GetLogger(kernel.BoxService).Clone().Str("box", "dir").Int("boxnum", int64(cdata.Number)).Child() } path := getDirPath(u) if _, err := os.Stat(path); errors.Is(err, os.ErrNotExist) { return nil, err } dp := dirBox{ log: log, number: cdata.Number, location: u.String(), readonly: box.GetQueryBool(u, "readonly"), cdata: *cdata, dir: path, notifySpec: getDirSrvInfo(log, u.Query().Get("type")), fSrvs: makePrime(uint32(box.GetQueryInt(u, "worker", 1, 7, 1499))), } return &dp, nil }) } func makePrime(n uint32) uint32 { for !isPrime(n) { n++ } return n } func isPrime(n uint32) bool { if n == 0 { return false } if n <= 3 { return true } if n%2 == 0 { return false } for i := uint32(3); i*i <= n; i += 2 { if n%i == 0 { return false } } return true } type notifyTypeSpec int const ( _ notifyTypeSpec = iota dirNotifyAny dirNotifySimple dirNotifyFS ) func getDirSrvInfo(log *logger.Logger, notifyType string) notifyTypeSpec { for count := 0; count < 2; count++ { switch notifyType { case kernel.BoxDirTypeNotify: return dirNotifyFS case kernel.BoxDirTypeSimple: return dirNotifySimple default: notifyType = kernel.Main.GetConfig(kernel.BoxService, kernel.BoxDefaultDirType).(string) } } log.Error().Str("notifyType", notifyType).Msg("Unable to set notify type, using a default") return dirNotifySimple } func getDirPath(u *url.URL) string { if u.Opaque != "" { return filepath.Clean(u.Opaque) } return filepath.Clean(u.Path) } // dirBox uses a directory to store zettel as files. type dirBox struct { log *logger.Logger number int location string readonly bool cdata manager.ConnectData dir string notifySpec notifyTypeSpec dirSrv *notify.DirService fSrvs uint32 fCmds []chan fileCmd mxCmds sync.RWMutex } func (dp *dirBox) Location() string { return dp.location } func (dp *dirBox) State() box.StartState { if ds := dp.dirSrv; ds != nil { switch ds.State() { case notify.DsCreated: return box.StartStateStopped case notify.DsStarting: return box.StartStateStarting case notify.DsWorking: return box.StartStateStarted case notify.DsMissing: return box.StartStateStarted case notify.DsStopping: return box.StartStateStopping } } return box.StartStateStopped } func (dp *dirBox) Start(context.Context) error { dp.mxCmds.Lock() defer dp.mxCmds.Unlock() dp.fCmds = make([]chan fileCmd, 0, dp.fSrvs) for i := uint32(0); i < dp.fSrvs; i++ { cc := make(chan fileCmd) go fileService(i, dp.log.Clone().Str("sub", "file").Uint("fn", uint64(i)).Child(), dp.dir, cc) dp.fCmds = append(dp.fCmds, cc) } var notifier notify.Notifier var err error switch dp.notifySpec { case dirNotifySimple: notifier, err = notify.NewSimpleDirNotifier(dp.log.Clone().Str("notify", "simple").Child(), dp.dir) default: notifier, err = notify.NewFSDirNotifier(dp.log.Clone().Str("notify", "fs").Child(), dp.dir) } if err != nil { dp.log.Fatal().Err(err).Msg("Unable to create directory supervisor") dp.stopFileServices() return err } dp.dirSrv = notify.NewDirService( dp, dp.log.Clone().Str("sub", "dirsrv").Child(), notifier, dp.cdata.Notify, ) dp.dirSrv.Start() return nil } func (dp *dirBox) Refresh(_ context.Context) { dp.dirSrv.Refresh() dp.log.Trace().Msg("Refresh") } func (dp *dirBox) Stop(_ context.Context) { dirSrv := dp.dirSrv dp.dirSrv = nil if dirSrv != nil { dirSrv.Stop() } dp.stopFileServices() } func (dp *dirBox) stopFileServices() { for _, c := range dp.fCmds { close(c) } } func (dp *dirBox) notifyChanged(zid id.Zid) { if chci := dp.cdata.Notify; chci != nil { dp.log.Trace().Zid(zid).Msg("notifyChanged") chci <- box.UpdateInfo{Reason: box.OnZettel, Zid: zid} } } func (dp *dirBox) getFileChan(zid id.Zid) chan fileCmd { // Based on https://en.wikipedia.org/wiki/Fowler%E2%80%93Noll%E2%80%93Vo_hash_function sum := 2166136261 ^ uint32(zid) sum *= 16777619 sum ^= uint32(zid >> 32) sum *= 16777619 dp.mxCmds.RLock() defer dp.mxCmds.RUnlock() return dp.fCmds[sum%dp.fSrvs] } func (dp *dirBox) CanCreateZettel(_ context.Context) bool { return !dp.readonly } func (dp *dirBox) CreateZettel(ctx context.Context, zettel domain.Zettel) (id.Zid, error) { if dp.readonly { return id.Invalid, box.ErrReadOnly } newZid, err := dp.dirSrv.SetNewDirEntry() if err != nil { return id.Invalid, err } meta := zettel.Meta meta.Zid = newZid entry := notify.DirEntry{Zid: newZid} dp.updateEntryFromMetaContent(&entry, meta, zettel.Content) err = dp.srvSetZettel(ctx, &entry, zettel) if err == nil { err = dp.dirSrv.UpdateDirEntry(&entry) } dp.notifyChanged(meta.Zid) dp.log.Trace().Err(err).Zid(meta.Zid).Msg("CreateZettel") return meta.Zid, err } func (dp *dirBox) GetZettel(ctx context.Context, zid id.Zid) (domain.Zettel, error) { entry := dp.dirSrv.GetDirEntry(zid) if !entry.IsValid() { return domain.Zettel{}, box.ErrNotFound } m, c, err := dp.srvGetMetaContent(ctx, entry, zid) if err != nil { return domain.Zettel{}, err } zettel := domain.Zettel{Meta: m, Content: domain.NewContent(c)} dp.log.Trace().Zid(zid).Msg("GetZettel") return zettel, nil } func (dp *dirBox) GetMeta(ctx context.Context, zid id.Zid) (*meta.Meta, error) { m, err := dp.doGetMeta(ctx, zid) dp.log.Trace().Zid(zid).Err(err).Msg("GetMeta") return m, err } func (dp *dirBox) doGetMeta(ctx context.Context, zid id.Zid) (*meta.Meta, error) { entry := dp.dirSrv.GetDirEntry(zid) if !entry.IsValid() { return nil, box.ErrNotFound } m, err := dp.srvGetMeta(ctx, entry, zid) if err != nil { return nil, err } return m, nil } func (dp *dirBox) ApplyZid(_ context.Context, handle box.ZidFunc, constraint query.RetrievePredicate) error { entries := dp.dirSrv.GetDirEntries(constraint) dp.log.Trace().Int("entries", int64(len(entries))).Msg("ApplyZid") for _, entry := range entries { handle(entry.Zid) } return nil } func (dp *dirBox) ApplyMeta(ctx context.Context, handle box.MetaFunc, constraint query.RetrievePredicate) error { entries := dp.dirSrv.GetDirEntries(constraint) dp.log.Trace().Int("entries", int64(len(entries))).Msg("ApplyMeta") // The following loop could be parallelized if needed for performance. for _, entry := range entries { m, err := dp.srvGetMeta(ctx, entry, entry.Zid) if err != nil { dp.log.Trace().Err(err).Msg("ApplyMeta/getMeta") return err } dp.cdata.Enricher.Enrich(ctx, m, dp.number) handle(m) } return nil } func (dp *dirBox) CanUpdateZettel(context.Context, 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 zid := meta.Zid if !zid.IsValid() { return &box.ErrInvalidID{Zid: zid} } entry := dp.dirSrv.GetDirEntry(zid) if !entry.IsValid() { // Existing zettel, but new in this box. entry = ¬ify.DirEntry{Zid: zid} } dp.updateEntryFromMetaContent(entry, meta, zettel.Content) dp.dirSrv.UpdateDirEntry(entry) err := dp.srvSetZettel(ctx, entry, zettel) if err == nil { dp.notifyChanged(zid) } dp.log.Trace().Zid(zid).Err(err).Msg("UpdateZettel") return err } func (dp *dirBox) updateEntryFromMetaContent(entry *notify.DirEntry, m *meta.Meta, content domain.Content) { entry.SetupFromMetaContent(m, content, dp.cdata.Config.GetZettelFileSyntax) } func (dp *dirBox) AllowRenameZettel(context.Context, id.Zid) bool { return !dp.readonly } func (dp *dirBox) RenameZettel(ctx context.Context, curZid, newZid id.Zid) error { if curZid == newZid { return nil } curEntry := dp.dirSrv.GetDirEntry(curZid) if !curEntry.IsValid() { return box.ErrNotFound } if dp.readonly { return box.ErrReadOnly } // Check whether zettel with new ID already exists in this box. if _, err := dp.doGetMeta(ctx, newZid); err == nil { return &box.ErrInvalidID{Zid: newZid} } oldMeta, oldContent, err := dp.srvGetMetaContent(ctx, curEntry, curZid) if err != nil { return err } newEntry, err := dp.dirSrv.RenameDirEntry(curEntry, newZid) if err != nil { return err } oldMeta.Zid = newZid newZettel := domain.Zettel{Meta: oldMeta, Content: domain.NewContent(oldContent)} if err = dp.srvSetZettel(ctx, &newEntry, newZettel); err != nil { // "Rollback" rename. No error checking... dp.dirSrv.RenameDirEntry(&newEntry, curZid) return err } err = dp.srvDeleteZettel(ctx, curEntry, curZid) if err == nil { dp.notifyChanged(curZid) dp.notifyChanged(newZid) } dp.log.Trace().Zid(curZid).Zid(newZid).Err(err).Msg("RenameZettel") return err } func (dp *dirBox) CanDeleteZettel(_ context.Context, zid id.Zid) bool { if dp.readonly { return false } entry := dp.dirSrv.GetDirEntry(zid) return entry.IsValid() } func (dp *dirBox) DeleteZettel(ctx context.Context, zid id.Zid) error { if dp.readonly { return box.ErrReadOnly } entry := dp.dirSrv.GetDirEntry(zid) if !entry.IsValid() { return box.ErrNotFound } err := dp.dirSrv.DeleteDirEntry(zid) if err != nil { return nil } err = dp.srvDeleteZettel(ctx, entry, zid) if err == nil { dp.notifyChanged(zid) } dp.log.Trace().Zid(zid).Err(err).Msg("DeleteZettel") return err } func (dp *dirBox) ReadStats(st *box.ManagedBoxStats) { st.ReadOnly = dp.readonly st.Zettel = dp.dirSrv.NumDirEntries() dp.log.Trace().Int("zettel", int64(st.Zettel)).Msg("ReadStats") } |
Added box/dirbox/dirbox_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 | //----------------------------------------------------------------------------- // Copyright (c) 2021-present Detlef Stern // // This file is part of Zettelstore. // // Zettelstore is licensed under the latest version of the EUPL (European Union // Public License). Please see file LICENSE.txt for your rights and obligations // under this license. //----------------------------------------------------------------------------- package dirbox import "testing" func TestIsPrime(t *testing.T) { testcases := []struct { n uint32 exp bool }{ {0, false}, {1, true}, {2, true}, {3, true}, {4, false}, {5, true}, {6, false}, {7, true}, {8, false}, {9, false}, {10, false}, {11, true}, {12, false}, {13, true}, {14, false}, {15, false}, {17, true}, {19, true}, {21, false}, {23, true}, {25, false}, {27, false}, {29, true}, {31, true}, {33, false}, {35, false}, } for _, tc := range testcases { got := isPrime(tc.n) if got != tc.exp { t.Errorf("isPrime(%d)=%v, but got %v", tc.n, tc.exp, got) } } } func TestMakePrime(t *testing.T) { for i := uint32(0); i < 1500; i++ { np := makePrime(i) if np < i { t.Errorf("makePrime(%d) < %d", i, np) continue } if !isPrime(np) { t.Errorf("makePrime(%d) == %d is not prime", i, np) continue } if isPrime(i) && i != np { t.Errorf("%d is already prime, but got %d as next prime", i, np) continue } } } |
Added box/dirbox/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 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 | //----------------------------------------------------------------------------- // Copyright (c) 2020-present Detlef Stern // // This file is part of Zettelstore. // // Zettelstore is licensed under the latest version of the EUPL (European Union // Public License). Please see file LICENSE.txt for your rights and obligations // under this license. //----------------------------------------------------------------------------- package dirbox import ( "context" "io" "os" "path/filepath" "time" "zettelstore.de/z/box/filebox" "zettelstore.de/z/box/notify" "zettelstore.de/z/domain" "zettelstore.de/z/domain/id" "zettelstore.de/z/domain/meta" "zettelstore.de/z/input" "zettelstore.de/z/kernel" "zettelstore.de/z/logger" ) func fileService(i uint32, log *logger.Logger, dirPath string, cmds <-chan fileCmd) { // Something may panic. Ensure a running service. defer func() { if r := recover(); r != nil { kernel.Main.LogRecover("FileService", r) go fileService(i, log, dirPath, cmds) } }() log.Trace().Uint("i", uint64(i)).Str("dirpath", dirPath).Msg("File service started") for cmd := range cmds { cmd.run(log, dirPath) } log.Trace().Uint("i", uint64(i)).Str("dirpath", dirPath).Msg("File service stopped") } type fileCmd interface { run(*logger.Logger, string) } const serviceTimeout = 5 * time.Second // must be shorter than the web servers timeout values for reading+writing. // COMMAND: srvGetMeta ---------------------------------------- // // Retrieves the meta data from a zettel. func (dp *dirBox) srvGetMeta(ctx context.Context, entry *notify.DirEntry, zid id.Zid) (*meta.Meta, error) { rc := make(chan resGetMeta, 1) dp.getFileChan(zid) <- &fileGetMeta{entry, rc} ctx, cancel := context.WithTimeout(ctx, serviceTimeout) defer cancel() select { case res := <-rc: return res.meta, res.err case <-ctx.Done(): return nil, ctx.Err() } } type fileGetMeta struct { entry *notify.DirEntry rc chan<- resGetMeta } type resGetMeta struct { meta *meta.Meta err error } func (cmd *fileGetMeta) run(log *logger.Logger, dirPath string) { var m *meta.Meta var err error entry := cmd.entry zid := entry.Zid if metaName := entry.MetaName; metaName == "" { contentName := entry.ContentName contentExt := entry.ContentExt if contentName == "" || contentExt == "" { log.Panic().Zid(zid).Msg("No meta, no content in getMeta") } if entry.HasMetaInContent() { m, _, err = parseMetaContentFile(zid, filepath.Join(dirPath, contentName)) } else { m = filebox.CalcDefaultMeta(zid, contentExt) } } else { m, err = parseMetaFile(zid, filepath.Join(dirPath, metaName)) } if err == nil { cmdCleanupMeta(m, entry) } cmd.rc <- resGetMeta{m, err} } // COMMAND: srvGetMetaContent ---------------------------------------- // // Retrieves the meta data and the content of a zettel. func (dp *dirBox) srvGetMetaContent(ctx context.Context, entry *notify.DirEntry, zid id.Zid) (*meta.Meta, []byte, error) { rc := make(chan resGetMetaContent, 1) dp.getFileChan(zid) <- &fileGetMetaContent{entry, rc} ctx, cancel := context.WithTimeout(ctx, serviceTimeout) defer cancel() select { case res := <-rc: return res.meta, res.content, res.err case <-ctx.Done(): return nil, nil, ctx.Err() } } type fileGetMetaContent struct { entry *notify.DirEntry rc chan<- resGetMetaContent } type resGetMetaContent struct { meta *meta.Meta content []byte err error } func (cmd *fileGetMetaContent) run(log *logger.Logger, dirPath string) { var m *meta.Meta var content []byte var err error entry := cmd.entry zid := entry.Zid contentName := entry.ContentName contentExt := entry.ContentExt contentPath := filepath.Join(dirPath, contentName) if metaName := entry.MetaName; metaName == "" { if contentName == "" || contentExt == "" { log.Panic().Zid(zid).Msg("No meta, no content in getMetaContent") } if entry.HasMetaInContent() { m, content, err = parseMetaContentFile(zid, contentPath) } else { m = filebox.CalcDefaultMeta(zid, contentExt) content, err = os.ReadFile(contentPath) } } else { m, err = parseMetaFile(zid, filepath.Join(dirPath, metaName)) if contentName != "" { var err1 error content, err1 = os.ReadFile(contentPath) if err == nil { err = err1 } } } if err == nil { cmdCleanupMeta(m, entry) } cmd.rc <- resGetMetaContent{m, content, err} } // COMMAND: srvSetZettel ---------------------------------------- // // Writes a new or exsting zettel. func (dp *dirBox) srvSetZettel(ctx context.Context, entry *notify.DirEntry, zettel domain.Zettel) error { rc := make(chan resSetZettel, 1) dp.getFileChan(zettel.Meta.Zid) <- &fileSetZettel{entry, zettel, rc} ctx, cancel := context.WithTimeout(ctx, serviceTimeout) defer cancel() select { case err := <-rc: return err case <-ctx.Done(): return ctx.Err() } } type fileSetZettel struct { entry *notify.DirEntry zettel domain.Zettel rc chan<- resSetZettel } type resSetZettel = error func (cmd *fileSetZettel) run(log *logger.Logger, dirPath string) { entry := cmd.entry zid := entry.Zid contentName := entry.ContentName m := cmd.zettel.Meta content := cmd.zettel.Content.AsBytes() metaName := entry.MetaName if metaName == "" { if contentName == "" { log.Panic().Zid(zid).Msg("No meta, no content in setZettel") } contentPath := filepath.Join(dirPath, contentName) if entry.HasMetaInContent() { err := writeZettelFile(contentPath, m, content) cmd.rc <- err return } err := writeFileContent(contentPath, content) cmd.rc <- err return } err := writeMetaFile(filepath.Join(dirPath, metaName), m) if err == nil && contentName != "" { err = writeFileContent(filepath.Join(dirPath, contentName), content) } cmd.rc <- err } func writeMetaFile(metaPath string, m *meta.Meta) error { metaFile, err := openFileWrite(metaPath) if err != nil { return err } err = writeFileZid(metaFile, m.Zid) if err == nil { _, err = m.WriteComputed(metaFile) } if err1 := metaFile.Close(); err == nil { err = err1 } return err } func writeZettelFile(contentPath string, m *meta.Meta, content []byte) error { zettelFile, err := openFileWrite(contentPath) if err != nil { return err } if err == nil { err = writeMetaHeader(zettelFile, m) } if err == nil { _, err = zettelFile.Write(content) } if err1 := zettelFile.Close(); err == nil { err = err1 } return err } var ( newline = []byte{'\n'} yamlSep = []byte{'-', '-', '-', '\n'} ) func writeMetaHeader(w io.Writer, m *meta.Meta) (err error) { if m.YamlSep { _, err = w.Write(yamlSep) if err != nil { return err } } err = writeFileZid(w, m.Zid) if err != nil { return err } _, err = m.WriteComputed(w) if err != nil { return err } if m.YamlSep { _, err = w.Write(yamlSep) } else { _, err = w.Write(newline) } return err } // COMMAND: srvDeleteZettel ---------------------------------------- // // Deletes an existing zettel. func (dp *dirBox) srvDeleteZettel(ctx context.Context, entry *notify.DirEntry, zid id.Zid) error { rc := make(chan resDeleteZettel, 1) dp.getFileChan(zid) <- &fileDeleteZettel{entry, rc} ctx, cancel := context.WithTimeout(ctx, serviceTimeout) defer cancel() select { case err := <-rc: return err case <-ctx.Done(): return ctx.Err() } } type fileDeleteZettel struct { entry *notify.DirEntry rc chan<- resDeleteZettel } type resDeleteZettel = error func (cmd *fileDeleteZettel) run(log *logger.Logger, dirPath string) { var err error entry := cmd.entry contentName := entry.ContentName contentPath := filepath.Join(dirPath, contentName) if metaName := entry.MetaName; metaName == "" { if contentName == "" { log.Panic().Zid(entry.Zid).Msg("No meta, no content in getMetaContent") } err = os.Remove(contentPath) } else { if contentName != "" { err = os.Remove(contentPath) } err1 := os.Remove(filepath.Join(dirPath, metaName)) if err == nil { err = err1 } } for _, dupName := range entry.UselessFiles { err1 := os.Remove(filepath.Join(dirPath, dupName)) if err == nil { err = err1 } } cmd.rc <- err } // Utility functions ---------------------------------------- func parseMetaFile(zid id.Zid, path string) (*meta.Meta, error) { src, err := os.ReadFile(path) if err != nil { return nil, err } inp := input.NewInput(src) return meta.NewFromInput(zid, inp), nil } func parseMetaContentFile(zid id.Zid, path string) (*meta.Meta, []byte, error) { src, err := os.ReadFile(path) if err != nil { return nil, nil, err } inp := input.NewInput(src) meta := meta.NewFromInput(zid, inp) return meta, src[inp.Pos:], nil } func cmdCleanupMeta(m *meta.Meta, entry *notify.DirEntry) { filebox.CleanupMeta( m, entry.Zid, entry.ContentExt, entry.MetaName != "", entry.UselessFiles, ) } func openFileWrite(path string) (*os.File, error) { return os.OpenFile(path, os.O_WRONLY|os.O_CREATE|os.O_TRUNC, 0600) } func writeFileZid(w io.Writer, zid id.Zid) error { _, err := io.WriteString(w, "id: ") if err == nil { _, err = w.Write(zid.Bytes()) if err == nil { _, err = io.WriteString(w, "\n") } } return err } func writeFileContent(path string, content []byte) error { f, err := openFileWrite(path) if err == nil { _, err = f.Write(content) if err1 := f.Close(); err == nil { err = err1 } } return err } |
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-present Detlef Stern // // This file is part of Zettelstore. // // Zettelstore is licensed under the latest version of the EUPL (European Union // Public License). Please see file LICENSE.txt for your rights and obligations // under this license. //----------------------------------------------------------------------------- // Package filebox provides boxes that are stored in a file. package filebox import ( "errors" "net/url" "path/filepath" "strings" "zettelstore.de/c/api" "zettelstore.de/z/box" "zettelstore.de/z/box/manager" "zettelstore.de/z/domain/id" "zettelstore.de/z/domain/meta" "zettelstore.de/z/kernel" ) func init() { manager.Register("file", func(u *url.URL, cdata *manager.ConnectData) (box.ManagedBox, error) { path := getFilepathFromURL(u) ext := strings.ToLower(filepath.Ext(path)) if ext != ".zip" { return nil, errors.New("unknown extension '" + ext + "' in box URL: " + u.String()) } return &zipBox{ log: kernel.Main.GetLogger(kernel.BoxService).Clone(). Str("box", "zip").Int("boxnum", int64(cdata.Number)).Child(), number: cdata.Number, name: path, enricher: cdata.Enricher, notify: cdata.Notify, }, nil }) } func getFilepathFromURL(u *url.URL) string { name := u.Opaque if name == "" { name = u.Path } components := strings.Split(name, "/") fileName := filepath.Join(components...) if len(components) > 0 && components[0] == "" { return "/" + fileName } return fileName } var alternativeSyntax = map[string]string{ "htm": "html", } func calculateSyntax(ext string) string { ext = strings.ToLower(ext) if syntax, ok := alternativeSyntax[ext]; ok { return syntax } return ext } // CalcDefaultMeta returns metadata with default values for the given entry. func CalcDefaultMeta(zid id.Zid, ext string) *meta.Meta { m := meta.New(zid) m.Set(api.KeySyntax, calculateSyntax(ext)) return m } // CleanupMeta enhances the given metadata. func CleanupMeta(m *meta.Meta, zid id.Zid, ext string, inMeta bool, uselessFiles []string) { if inMeta { if syntax, ok := m.Get(api.KeySyntax); !ok || syntax == "" { dm := CalcDefaultMeta(zid, ext) syntax, ok = dm.Get(api.KeySyntax) if !ok { panic("Default meta must contain syntax") } m.Set(api.KeySyntax, syntax) } } if len(uselessFiles) > 0 { m.Set(api.KeyUselessFiles, strings.Join(uselessFiles, " ")) } } |
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 262 263 264 265 266 267 268 269 270 271 | //----------------------------------------------------------------------------- // Copyright (c) 2021-present Detlef Stern // // This file is part of Zettelstore. // // Zettelstore is licensed under the latest version of the EUPL (European Union // Public License). Please see file LICENSE.txt for your rights and obligations // under this license. //----------------------------------------------------------------------------- package filebox import ( "archive/zip" "context" "io" "strings" "zettelstore.de/z/box" "zettelstore.de/z/box/notify" "zettelstore.de/z/domain" "zettelstore.de/z/domain/id" "zettelstore.de/z/domain/meta" "zettelstore.de/z/input" "zettelstore.de/z/logger" "zettelstore.de/z/query" ) type zipBox struct { log *logger.Logger number int name string enricher box.Enricher notify chan<- box.UpdateInfo dirSrv *notify.DirService } func (zb *zipBox) Location() string { if strings.HasPrefix(zb.name, "/") { return "file://" + zb.name } return "file:" + zb.name } func (zb *zipBox) State() box.StartState { if ds := zb.dirSrv; ds != nil { switch ds.State() { case notify.DsCreated: return box.StartStateStopped case notify.DsStarting: return box.StartStateStarting case notify.DsWorking: return box.StartStateStarted case notify.DsMissing: return box.StartStateStarted case notify.DsStopping: return box.StartStateStopping } } return box.StartStateStopped } func (zb *zipBox) Start(context.Context) error { reader, err := zip.OpenReader(zb.name) if err != nil { return err } reader.Close() zipNotifier := notify.NewSimpleZipNotifier(zb.log, zb.name) zb.dirSrv = notify.NewDirService(zb, zb.log, zipNotifier, zb.notify) zb.dirSrv.Start() return nil } func (zb *zipBox) Refresh(_ context.Context) { zb.dirSrv.Refresh() zb.log.Trace().Msg("Refresh") } func (zb *zipBox) Stop(context.Context) { zb.dirSrv.Stop() zb.dirSrv = nil } func (*zipBox) CanCreateZettel(context.Context) bool { return false } func (zb *zipBox) CreateZettel(context.Context, domain.Zettel) (id.Zid, error) { err := box.ErrReadOnly zb.log.Trace().Err(err).Msg("CreateZettel") return id.Invalid, err } func (zb *zipBox) GetZettel(_ context.Context, zid id.Zid) (domain.Zettel, error) { entry := zb.dirSrv.GetDirEntry(zid) if !entry.IsValid() { return domain.Zettel{}, box.ErrNotFound } reader, err := zip.OpenReader(zb.name) if err != nil { return domain.Zettel{}, err } defer reader.Close() var m *meta.Meta var src []byte var inMeta bool contentName := entry.ContentName if metaName := entry.MetaName; metaName == "" { if contentName == "" { zb.log.Panic().Zid(zid).Msg("No meta, no content in zipBox.GetZettel") } src, err = readZipFileContent(reader, entry.ContentName) if err != nil { return domain.Zettel{}, err } if entry.HasMetaInContent() { inp := input.NewInput(src) m = meta.NewFromInput(zid, inp) src = src[inp.Pos:] } else { m = CalcDefaultMeta(zid, entry.ContentExt) } } else { m, err = readZipMetaFile(reader, zid, metaName) if err != nil { return domain.Zettel{}, err } inMeta = true if contentName != "" { src, err = readZipFileContent(reader, entry.ContentName) if err != nil { return domain.Zettel{}, err } } } CleanupMeta(m, zid, entry.ContentExt, inMeta, entry.UselessFiles) zb.log.Trace().Zid(zid).Msg("GetZettel") return domain.Zettel{Meta: m, Content: domain.NewContent(src)}, nil } func (zb *zipBox) GetMeta(_ context.Context, zid id.Zid) (*meta.Meta, error) { entry := zb.dirSrv.GetDirEntry(zid) if !entry.IsValid() { return nil, box.ErrNotFound } reader, err := zip.OpenReader(zb.name) if err != nil { return nil, err } defer reader.Close() m, err := zb.readZipMeta(reader, zid, entry) zb.log.Trace().Err(err).Zid(zid).Msg("GetMeta") return m, err } func (zb *zipBox) ApplyZid(_ context.Context, handle box.ZidFunc, constraint query.RetrievePredicate) error { entries := zb.dirSrv.GetDirEntries(constraint) zb.log.Trace().Int("entries", int64(len(entries))).Msg("ApplyZid") for _, entry := range entries { handle(entry.Zid) } return nil } func (zb *zipBox) ApplyMeta(ctx context.Context, handle box.MetaFunc, constraint query.RetrievePredicate) error { reader, err := zip.OpenReader(zb.name) if err != nil { return err } defer reader.Close() entries := zb.dirSrv.GetDirEntries(constraint) zb.log.Trace().Int("entries", int64(len(entries))).Msg("ApplyMeta") for _, entry := range entries { if !constraint(entry.Zid) { continue } m, err2 := zb.readZipMeta(reader, entry.Zid, entry) if err2 != nil { continue } zb.enricher.Enrich(ctx, m, zb.number) handle(m) } return nil } func (*zipBox) CanUpdateZettel(context.Context, domain.Zettel) bool { return false } func (zb *zipBox) UpdateZettel(context.Context, domain.Zettel) error { err := box.ErrReadOnly zb.log.Trace().Err(err).Msg("UpdateZettel") return err } func (zb *zipBox) AllowRenameZettel(_ context.Context, zid id.Zid) bool { entry := zb.dirSrv.GetDirEntry(zid) return !entry.IsValid() } func (zb *zipBox) RenameZettel(_ context.Context, curZid, newZid id.Zid) error { err := box.ErrReadOnly if curZid == newZid { err = nil } curEntry := zb.dirSrv.GetDirEntry(curZid) if !curEntry.IsValid() { err = box.ErrNotFound } zb.log.Trace().Err(err).Msg("RenameZettel") return err } func (*zipBox) CanDeleteZettel(context.Context, id.Zid) bool { return false } func (zb *zipBox) DeleteZettel(_ context.Context, zid id.Zid) error { err := box.ErrReadOnly entry := zb.dirSrv.GetDirEntry(zid) if !entry.IsValid() { err = box.ErrNotFound } zb.log.Trace().Err(err).Msg("DeleteZettel") return err } func (zb *zipBox) ReadStats(st *box.ManagedBoxStats) { st.ReadOnly = true st.Zettel = zb.dirSrv.NumDirEntries() zb.log.Trace().Int("zettel", int64(st.Zettel)).Msg("ReadStats") } func (zb *zipBox) readZipMeta(reader *zip.ReadCloser, zid id.Zid, entry *notify.DirEntry) (m *meta.Meta, err error) { var inMeta bool if metaName := entry.MetaName; metaName == "" { contentName := entry.ContentName contentExt := entry.ContentExt if contentName == "" || contentExt == "" { zb.log.Panic().Zid(zid).Msg("No meta, no content in getMeta") } if entry.HasMetaInContent() { m, err = readZipMetaFile(reader, zid, contentName) } else { m = CalcDefaultMeta(zid, contentExt) } } else { m, err = readZipMetaFile(reader, zid, metaName) } if err == nil { CleanupMeta(m, zid, entry.ContentExt, inMeta, entry.UselessFiles) } return m, err } func readZipMetaFile(reader *zip.ReadCloser, zid id.Zid, name string) (*meta.Meta, error) { src, err := readZipFileContent(reader, name) if err != nil { return nil, err } inp := input.NewInput(src) return meta.NewFromInput(zid, inp), nil } func readZipFileContent(reader *zip.ReadCloser, name string) ([]byte, error) { f, err := reader.Open(name) if err != nil { return nil, err } defer f.Close() return io.ReadAll(f) } |
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 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 | //----------------------------------------------------------------------------- // Copyright (c) 2021-present Detlef Stern // // This file is part of Zettelstore. // // Zettelstore is licensed under the latest version of the EUPL (European Union // Public License). Please see file LICENSE.txt for your rights and obligations // under this license. //----------------------------------------------------------------------------- package box import ( "net/url" "strconv" "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 } // GetQueryBool is a helper function to extract bool values from a box URI. func GetQueryBool(u *url.URL, key string) bool { _, ok := u.Query()[key] return ok } // GetQueryInt is a helper function to extract int values of a specified range from a box URI. func GetQueryInt(u *url.URL, key string, min, def, max int) int { sVal := u.Query().Get(key) if sVal == "" { return def } iVal, err := strconv.Atoi(sVal) if err != nil { return def } if iVal < min { return min } if iVal > max { return max } return iVal } |
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 | //----------------------------------------------------------------------------- // Copyright (c) 2021-present Detlef Stern // // This file is part of Zettelstore. // // Zettelstore is licensed under the latest version of the EUPL (European Union // Public License). Please see file LICENSE.txt for your rights and obligations // under this license. //----------------------------------------------------------------------------- package manager import ( "sync" "zettelstore.de/z/domain/id" ) type arAction int const ( arNothing arAction = iota arReload arZettel ) type anteroom struct { num uint64 next *anteroom waiting id.Set 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) EnqueueZettel(zid id.Zid) { if !zid.IsValid() { return } ar.mx.Lock() defer ar.mx.Unlock() if ar.first == nil { ar.first = ar.makeAnteroom(zid) ar.last = ar.first return } for room := ar.first; room != nil; room = room.next { if room.reload { continue // Do not put zettel in reload room } if _, ok := room.waiting[zid]; ok { // Zettel is already waiting. Nothing to do. return } } if room := ar.last; !room.reload && (ar.maxLoad == 0 || room.curLoad < ar.maxLoad) { room.waiting.Zid(zid) room.curLoad++ return } room := ar.makeAnteroom(zid) ar.last.next = room ar.last = room } func (ar *anterooms) makeAnteroom(zid id.Zid) *anteroom { ar.nextNum++ if zid == id.Invalid { return &anteroom{num: ar.nextNum, next: nil, waiting: nil, curLoad: 0, reload: true} } c := ar.maxLoad if c == 0 { c = 100 } waiting := id.NewSetCap(ar.maxLoad, zid) 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) ar.last = ar.first } func (ar *anterooms) Reload(newZids id.Set) uint64 { ar.mx.Lock() defer ar.mx.Unlock() ar.deleteReloadedRooms() if ns := len(newZids); ns > 0 { ar.nextNum++ ar.first = &anteroom{num: ar.nextNum, next: ar.first, waiting: newZids, curLoad: ns, reload: true} if ar.first.next == nil { ar.last = ar.first } return ar.nextNum } ar.first = nil ar.last = nil return 0 } 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 } roomNo := ar.first.num if ar.first.waiting == nil { ar.removeFirst() return arReload, id.Invalid, roomNo } for zid := range ar.first.waiting { delete(ar.first.waiting, zid) if len(ar.first.waiting) == 0 { ar.removeFirst() } return arZettel, zid, roomNo } ar.removeFirst() return arNothing, id.Invalid, 0 } func (ar *anterooms) removeFirst() { ar.first = ar.first.next if ar.first == nil { ar.last = nil } } |
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 | //----------------------------------------------------------------------------- // Copyright (c) 2021-present Detlef Stern // // This file is part of Zettelstore. // // Zettelstore is licensed under the latest version of the EUPL (European Union // Public License). Please see file LICENSE.txt for your rights and obligations // under this license. //----------------------------------------------------------------------------- package manager import ( "testing" "zettelstore.de/z/domain/id" ) func TestSimple(t *testing.T) { t.Parallel() ar := newAnterooms(2) ar.EnqueueZettel(id.Zid(1)) action, zid, rno := ar.Dequeue() if zid != id.Zid(1) || action != arZettel || rno != 1 { t.Errorf("Expected arZettel/1/1, but got %v/%v/%v", action, zid, rno) } _, zid, _ = ar.Dequeue() if zid != id.Invalid { t.Errorf("Expected invalid Zid, but got %v", zid) } ar.EnqueueZettel(id.Zid(1)) ar.EnqueueZettel(id.Zid(2)) if ar.first != ar.last { t.Errorf("Expected one room, but got more") } ar.EnqueueZettel(id.Zid(3)) if ar.first == ar.last { t.Errorf("Expected more than one room, but got only one") } count := 0 for ; count < 1000; count++ { action, _, _ = ar.Dequeue() if action == arNothing { break } } if count != 3 { t.Errorf("Expected 3 dequeues, but got %v", count) } } func TestReset(t *testing.T) { t.Parallel() ar := newAnterooms(1) ar.EnqueueZettel(id.Zid(1)) ar.Reset() action, zid, _ := ar.Dequeue() if action != arReload || zid != id.Invalid { t.Errorf("Expected reload & invalid Zid, but got %v/%v", action, zid) } ar.Reload(id.NewSet(3, 4)) ar.EnqueueZettel(id.Zid(5)) ar.EnqueueZettel(id.Zid(5)) if ar.first == ar.last || ar.first.next != ar.last /*|| ar.first.next.next != ar.last*/ { t.Errorf("Expected 2 rooms") } action, zid1, _ := ar.Dequeue() if action != arZettel { t.Errorf("Expected arZettel, but got %v", action) } action, zid2, _ := ar.Dequeue() if action != arZettel { t.Errorf("Expected arZettel, but got %v", action) } if !(zid1 == id.Zid(3) && zid2 == id.Zid(4) || zid1 == id.Zid(4) && zid2 == id.Zid(3)) { t.Errorf("Zids must be 3 or 4, but got %v/%v", zid1, zid2) } action, zid, _ = ar.Dequeue() if zid != id.Zid(5) || action != arZettel { t.Errorf("Expected 5/arZettel, but got %v/%v", zid, action) } action, zid, _ = ar.Dequeue() if action != arNothing || zid != id.Invalid { t.Errorf("Expected nothing & invalid Zid, but got %v/%v", action, zid) } ar = newAnterooms(1) ar.Reload(id.NewSet(id.Zid(6))) action, zid, _ = ar.Dequeue() if zid != id.Zid(6) || action != arZettel { t.Errorf("Expected 6/arZettel, but got %v/%v", zid, action) } action, zid, _ = ar.Dequeue() if action != arNothing || zid != id.Invalid { t.Errorf("Expected nothing & invalid Zid, but got %v/%v", action, zid) } ar = newAnterooms(1) ar.EnqueueZettel(id.Zid(8)) ar.Reload(nil) action, zid, _ = ar.Dequeue() if action != arNothing || zid != id.Invalid { t.Errorf("Expected nothing & invalid Zid, but got %v/%v", action, zid) } } |
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 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 | //----------------------------------------------------------------------------- // Copyright (c) 2021-present Detlef Stern // // This file is part of Zettelstore. // // Zettelstore is licensed under the latest version of the EUPL (European Union // Public License). Please see file LICENSE.txt for your rights and obligations // under this license. //----------------------------------------------------------------------------- package manager import ( "context" "errors" "strings" "zettelstore.de/z/box" "zettelstore.de/z/domain" "zettelstore.de/z/domain/id" "zettelstore.de/z/domain/meta" "zettelstore.de/z/query" ) // 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.State() == box.StartStateStarted && mgr.boxes[0].CanCreateZettel(ctx) } // CreateZettel creates a new zettel. func (mgr *Manager) CreateZettel(ctx context.Context, zettel domain.Zettel) (id.Zid, error) { mgr.mgrLog.Debug().Msg("CreateZettel") if mgr.State() != box.StartStateStarted { return id.Invalid, box.ErrStopped } mgr.mgrMx.RLock() defer mgr.mgrMx.RUnlock() 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.mgrLog.Debug().Zid(zid).Msg("GetZettel") if mgr.State() != box.StartStateStarted { return domain.Zettel{}, box.ErrStopped } mgr.mgrMx.RLock() defer mgr.mgrMx.RUnlock() 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.mgrLog.Debug().Zid(zid).Msg("GetAllZettel") if mgr.State() != box.StartStateStarted { return nil, box.ErrStopped } mgr.mgrMx.RLock() defer mgr.mgrMx.RUnlock() 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.mgrLog.Debug().Zid(zid).Msg("GetMeta") if mgr.State() != box.StartStateStarted { return nil, box.ErrStopped } mgr.mgrMx.RLock() defer mgr.mgrMx.RUnlock() return mgr.doGetMeta(ctx, zid) } func (mgr *Manager) doGetMeta(ctx context.Context, zid id.Zid) (*meta.Meta, error) { 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.mgrLog.Debug().Zid(zid).Msg("GetAllMeta") if mgr.State() != box.StartStateStarted { return nil, box.ErrStopped } mgr.mgrMx.RLock() defer mgr.mgrMx.RUnlock() 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) (id.Set, error) { mgr.mgrLog.Debug().Msg("FetchZids") if mgr.State() != box.StartStateStarted { return nil, box.ErrStopped } result := id.Set{} mgr.mgrMx.RLock() defer mgr.mgrMx.RUnlock() for _, p := range mgr.boxes { err := p.ApplyZid(ctx, func(zid id.Zid) { result.Zid(zid) }, func(id.Zid) bool { return true }) if err != nil { return nil, err } } return result, nil } type metaMap map[id.Zid]*meta.Meta // 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, q *query.Query) ([]*meta.Meta, error) { if msg := mgr.mgrLog.Debug(); msg.Enabled() { msg.Str("query", q.String()).Msg("SelectMeta") } if mgr.State() != box.StartStateStarted { return nil, box.ErrStopped } mgr.mgrMx.RLock() defer mgr.mgrMx.RUnlock() compSearch := q.RetrieveAndCompile(mgr) selected := metaMap{} for _, term := range compSearch.Terms { rejected := id.Set{} handleMeta := func(m *meta.Meta) { zid := m.Zid if rejected.Contains(zid) { mgr.mgrLog.Trace().Zid(zid).Msg("SelectMeta/alreadyRejected") return } if _, ok := selected[zid]; ok { mgr.mgrLog.Trace().Zid(zid).Msg("SelectMeta/alreadySelected") return } if compSearch.PreMatch(m) && term.Match(m) { selected[zid] = m mgr.mgrLog.Trace().Zid(zid).Msg("SelectMeta/match") } else { rejected.Zid(zid) mgr.mgrLog.Trace().Zid(zid).Msg("SelectMeta/reject") } } for _, p := range mgr.boxes { if err := p.ApplyMeta(ctx, handleMeta, term.Retrieve); err != nil { return nil, err } } } result := make([]*meta.Meta, 0, len(selected)) for _, m := range selected { result = append(result, m) } return q.AfterSearch(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.State() == box.StartStateStarted && mgr.boxes[0].CanUpdateZettel(ctx, zettel) } // UpdateZettel updates an existing zettel. func (mgr *Manager) UpdateZettel(ctx context.Context, zettel domain.Zettel) error { mgr.mgrLog.Debug().Zid(zettel.Meta.Zid).Msg("UpdateZettel") if mgr.State() != box.StartStateStarted { return box.ErrStopped } // Remove all (computed) properties from metadata before storing the zettel. zettel.Meta = zettel.Meta.Clone() for _, p := range zettel.Meta.ComputedPairsRest() { if mgr.propertyKeys.Has(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 { if mgr.State() != box.StartStateStarted { return false } mgr.mgrMx.RLock() defer mgr.mgrMx.RUnlock() for _, p := range mgr.boxes { if !p.AllowRenameZettel(ctx, zid) { return false } } return true } // RenameZettel changes the current zid to a new zid. func (mgr *Manager) RenameZettel(ctx context.Context, curZid, newZid id.Zid) error { mgr.mgrLog.Debug().Zid(curZid).Zid(newZid).Msg("RenameZettel") if mgr.State() != box.StartStateStarted { return box.ErrStopped } mgr.mgrMx.RLock() defer mgr.mgrMx.RUnlock() for i, p := range mgr.boxes { err := p.RenameZettel(ctx, curZid, newZid) 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 { if mgr.State() != box.StartStateStarted { return false } mgr.mgrMx.RLock() defer mgr.mgrMx.RUnlock() for _, p := range mgr.boxes { if p.CanDeleteZettel(ctx, zid) { return true } } return false } // DeleteZettel removes the zettel from the box. func (mgr *Manager) DeleteZettel(ctx context.Context, zid id.Zid) error { mgr.mgrLog.Debug().Zid(zid).Msg("DeleteZettel") if mgr.State() != box.StartStateStarted { return box.ErrStopped } mgr.mgrMx.RLock() defer mgr.mgrMx.RUnlock() for _, p := range mgr.boxes { err := p.DeleteZettel(ctx, zid) if err == nil { 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 | //----------------------------------------------------------------------------- // Copyright (c) 2021-present Detlef Stern // // This file is part of Zettelstore. // // Zettelstore is licensed under the latest version of the EUPL (European Union // Public License). Please see file LICENSE.txt for your rights and obligations // under this license. //----------------------------------------------------------------------------- package 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.Walk(data, &zn.Ast) } func collectInlineIndexData(is *ast.InlineSlice, data *collectData) { ast.Walk(data, is) } func (data *collectData) Visit(node ast.Node) ast.Visitor { switch n := node.(type) { case *ast.VerbatimNode: data.addText(string(n.Content)) case *ast.TranscludeNode: data.addRef(n.Ref) case *ast.TextNode: data.addText(n.Text) case *ast.LinkNode: data.addRef(n.Ref) case *ast.EmbedRefNode: data.addRef(n.Ref) case *ast.CiteNode: data.addText(n.Key) case *ast.LiteralNode: data.addText(string(n.Content)) } return data } func (data *collectData) addText(s string) { for _, word := range strfun.NormalizeWords(s) { data.words.Add(word) } } func (data *collectData) addRef(ref *ast.Reference) { if ref == nil { return } if ref.IsExternal() { data.urls.Add(strings.ToLower(ref.Value)) } if !ref.IsZettel() { return } if zid, err := id.Parse(ref.URL.Path); err == nil { data.refs.Zid(zid) } } |
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 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 | //----------------------------------------------------------------------------- // Copyright (c) 2021-present Detlef Stern // // This file is part of Zettelstore. // // Zettelstore is licensed under the latest version of the EUPL (European Union // Public License). Please see file LICENSE.txt for your rights and obligations // under this license. //----------------------------------------------------------------------------- package manager import ( "context" "strconv" "zettelstore.de/c/api" "zettelstore.de/z/box" "zettelstore.de/z/domain/id" "zettelstore.de/z/domain/meta" ) // Enrich computes additional properties and updates the given metadata. func (mgr *Manager) Enrich(ctx context.Context, m *meta.Meta, boxNumber int) { // Calculate computed, but stored values. if _, ok := m.Get(api.KeyCreated); !ok { m.Set(api.KeyCreated, computeCreated(m.Zid)) } if box.DoNotEnrich(ctx) { // Enrich is called indirectly via indexer or enrichment is not requested // because of other reasons -> ignore this call, do not update metadata return } computePublished(m) m.Set(api.KeyBoxNumber, strconv.Itoa(boxNumber)) mgr.idxStore.Enrich(ctx, m) } func computeCreated(zid id.Zid) string { if zid <= 10101000000 { // A year 0000 is not allowed and therefore an artificaial Zid. // In the year 0001, the month must be > 0. // In the month 000101, the day must be > 0. return "00010101000000" } seconds := zid % 100 if seconds > 59 { seconds = 59 } zid /= 100 minutes := zid % 100 if minutes > 59 { minutes = 59 } zid /= 100 hours := zid % 100 if hours > 23 { hours = 23 } zid /= 100 day := zid % 100 zid /= 100 month := zid % 100 year := zid / 100 month, day = sanitizeMonthDay(year, month, day) created := ((((year*100+month)*100+day)*100+hours)*100+minutes)*100 + seconds return created.String() } func sanitizeMonthDay(year, month, day id.Zid) (id.Zid, id.Zid) { if day < 1 { day = 1 } if month < 1 { month = 1 } if month > 12 { month = 12 } switch month { case 1, 3, 5, 7, 8, 10, 12: if day > 31 { day = 31 } case 4, 6, 9, 11: if day > 30 { day = 30 } case 2: if year%4 != 0 || (year%100 == 0 && year%400 != 0) { if day > 28 { day = 28 } } else { if day > 29 { day = 29 } } } return month, day } func computePublished(m *meta.Meta) { if _, ok := m.Get(api.KeyPublished); ok { return } if modified, ok := m.Get(api.KeyModified); ok { if _, ok = meta.TimeValue(modified); ok { m.Set(api.KeyPublished, modified) return } } if created, ok := m.Get(api.KeyCreated); ok { if _, ok = meta.TimeValue(created); ok { m.Set(api.KeyPublished, created) return } } zid := m.Zid.String() if _, ok := meta.TimeValue(zid); ok { m.Set(api.KeyPublished, zid) return } // Neither the zettel was modified nor the zettel identifer contains a valid // timestamp. In this case do not set the "published" property. } |
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 228 229 230 231 232 233 234 235 236 237 238 | //----------------------------------------------------------------------------- // Copyright (c) 2021-present Detlef Stern // // This file is part of Zettelstore. // // Zettelstore is licensed under the latest version of the EUPL (European Union // Public License). Please see file LICENSE.txt for your rights and obligations // under this license. //----------------------------------------------------------------------------- package manager import ( "context" "fmt" "net/url" "time" "zettelstore.de/z/box" "zettelstore.de/z/box/manager/store" "zettelstore.de/z/domain" "zettelstore.de/z/domain/id" "zettelstore.de/z/domain/meta" "zettelstore.de/z/kernel" "zettelstore.de/z/parser" "zettelstore.de/z/strfun" ) // SearchEqual returns all zettel that contains the given exact word. // The word must be normalized through Unicode NKFD, trimmed and not empty. func (mgr *Manager) SearchEqual(word string) id.Set { found := mgr.idxStore.SearchEqual(word) mgr.idxLog.Debug().Str("word", word).Int("found", int64(len(found))).Msg("SearchEqual") if msg := mgr.idxLog.Trace(); msg.Enabled() { msg.Str("ids", fmt.Sprint(found)).Msg("IDs") } return found } // SearchPrefix returns all zettel that have a word with the given prefix. // The prefix must be normalized through Unicode NKFD, trimmed and not empty. func (mgr *Manager) SearchPrefix(prefix string) id.Set { found := mgr.idxStore.SearchPrefix(prefix) mgr.idxLog.Debug().Str("prefix", prefix).Int("found", int64(len(found))).Msg("SearchPrefix") if msg := mgr.idxLog.Trace(); msg.Enabled() { msg.Str("ids", fmt.Sprint(found)).Msg("IDs") } return found } // SearchSuffix returns all zettel that have a word with the given suffix. // The suffix must be normalized through Unicode NKFD, trimmed and not empty. func (mgr *Manager) SearchSuffix(suffix string) id.Set { found := mgr.idxStore.SearchSuffix(suffix) mgr.idxLog.Debug().Str("suffix", suffix).Int("found", int64(len(found))).Msg("SearchSuffix") if msg := mgr.idxLog.Trace(); msg.Enabled() { msg.Str("ids", fmt.Sprint(found)).Msg("IDs") } return found } // SearchContains returns all zettel that contains the given string. // The string must be normalized through Unicode NKFD, trimmed and not empty. func (mgr *Manager) SearchContains(s string) id.Set { found := mgr.idxStore.SearchContains(s) mgr.idxLog.Debug().Str("s", s).Int("found", int64(len(found))).Msg("SearchContains") if msg := mgr.idxLog.Trace(); msg.Enabled() { msg.Str("ids", fmt.Sprint(found)).Msg("IDs") } return found } // idxIndexer runs in the background and updates the index data structures. // This is the main service of the idxIndexer. func (mgr *Manager) idxIndexer() { // Something may panic. Ensure a running indexer. defer func() { if 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: mgr.idxLog.Debug().Msg("reload") 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().Local() mgr.idxSinceReload = 0 mgr.idxMx.Unlock() } case arZettel: mgr.idxLog.Debug().Zid(zid).Msg("zettel") zettel, err := mgr.GetZettel(ctx, zid) if err != nil { // Zettel was deleted or is not accessible b/c of other reasons mgr.idxLog.Trace().Zid(zid).Msg("delete") mgr.idxMx.Lock() mgr.idxSinceReload++ mgr.idxMx.Unlock() mgr.idxDeleteZettel(zid) continue } mgr.idxLog.Trace().Zid(zid).Msg("update") mgr.idxMx.Lock() if arRoomNum == roomNum { mgr.idxDurReload = time.Since(start) } mgr.idxSinceReload++ mgr.idxMx.Unlock() mgr.idxUpdateZettel(ctx, zettel) } } } 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) { var cData collectData cData.initialize() collectZettelIndexData(parser.ParseZettel(ctx, zettel, "", mgr.rtConfig), &cData) m := zettel.Meta zi := store.NewZettelIndex(m.Zid) mgr.idxCollectFromMeta(ctx, m, zi, &cData) mgr.idxProcessData(ctx, zi, &cData) toCheck := mgr.idxStore.UpdateReferences(ctx, zi) mgr.idxCheckZettel(toCheck) } func (mgr *Manager) idxCollectFromMeta(ctx context.Context, m *meta.Meta, zi *store.ZettelIndex, cData *collectData) { for _, pair := range m.ComputedPairs() { descr := meta.GetDescription(pair.Key) if descr.IsProperty() { continue } switch descr.Type { case meta.TypeID: mgr.idxUpdateValue(ctx, descr.Inverse, pair.Value, zi) case meta.TypeIDSet: for _, val := range meta.ListFromValue(pair.Value) { mgr.idxUpdateValue(ctx, descr.Inverse, val, zi) } case meta.TypeZettelmarkup: is := parser.ParseMetadata(pair.Value) collectInlineIndexData(&is, cData) case meta.TypeURL: if _, err := url.Parse(pair.Value); err == nil { cData.urls.Add(pair.Value) } default: 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.EnqueueZettel(zid) } } |
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 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 | //----------------------------------------------------------------------------- // Copyright (c) 2021-present Detlef Stern // // This file is part of Zettelstore. // // Zettelstore is licensed under the latest version of the EUPL (European Union // Public License). Please see file LICENSE.txt for your rights and obligations // under this license. //----------------------------------------------------------------------------- // Package manager coordinates the various boxes and indexes of a Zettelstore. package manager import ( "context" "io" "net/url" "sync" "time" "zettelstore.de/c/maps" "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" "zettelstore.de/z/logger" "zettelstore.de/z/strfun" ) // ConnectData contains all administration related values. type ConnectData struct { Number int // number of the box, starting with 1. Config config.Config Enricher box.Enricher Notify chan<- box.UpdateInfo } // Connect returns a handle to the specified box. func Connect(u *url.URL, authManager auth.BaseManager, cdata *ConnectData) (box.ManagedBox, error) { if authManager.IsReadonly() { rawURL := u.String() // TODO: the following is wrong under some circumstances: // 1. fragment is set if q := u.Query(); len(q) == 0 { rawURL += "?readonly" } else if _, ok := q["readonly"]; !ok { rawURL += "&readonly" } var err error if u, err = url.Parse(rawURL); err != nil { return nil, err } } if create, ok := registry[u.Scheme]; ok { return create(u, cdata) } return nil, &ErrInvalidScheme{u.Scheme} } // ErrInvalidScheme is returned if there is no box with the given scheme. type ErrInvalidScheme struct{ Scheme string } func (err *ErrInvalidScheme) Error() string { return "Invalid scheme: " + err.Scheme } type createFunc func(*url.URL, *ConnectData) (box.ManagedBox, error) var registry = map[string]createFunc{} // Register the encoder for later retrieval. func Register(scheme string, create createFunc) { if _, ok := registry[scheme]; ok { panic(scheme) } registry[scheme] = create } // GetSchemes returns all registered scheme, ordered by scheme string. func GetSchemes() []string { return maps.Keys(registry) } // Manager is a coordinating box. type Manager struct { mgrLog *logger.Logger stateMx sync.RWMutex state box.StartState mgrMx sync.RWMutex rtConfig config.Config boxes []box.ManagedBox observers []box.UpdateFunc mxObserver sync.RWMutex done chan struct{} infos chan box.UpdateInfo propertyKeys strfun.Set // Set of property key names // Indexer data idxLog *logger.Logger idxStore store.Store idxAr *anterooms idxReady chan struct{} // Signal a non-empty anteroom to background task // Indexer stats data idxMx sync.RWMutex idxLastReload time.Time idxDurReload time.Duration idxSinceReload uint64 } func (mgr *Manager) setState(newState box.StartState) { mgr.stateMx.Lock() mgr.state = newState mgr.stateMx.Unlock() } func (mgr *Manager) State() box.StartState { mgr.stateMx.RLock() state := mgr.state mgr.stateMx.RUnlock() return state } // New creates a new managing box. func New(boxURIs []*url.URL, authManager auth.BaseManager, rtConfig config.Config) (*Manager, error) { descrs := meta.GetSortedKeyDescriptions() propertyKeys := make(strfun.Set, len(descrs)) for _, kd := range descrs { if kd.IsProperty() { propertyKeys.Set(kd.Name) } } boxLog := kernel.Main.GetLogger(kernel.BoxService) mgr := &Manager{ mgrLog: boxLog.Clone().Str("box", "manager").Child(), rtConfig: rtConfig, infos: make(chan box.UpdateInfo, len(boxURIs)*10), propertyKeys: propertyKeys, idxLog: boxLog.Clone().Str("box", "index").Child(), idxStore: memstore.New(), idxAr: newAnterooms(1000), idxReady: make(chan struct{}, 1), } cdata := ConnectData{Number: 1, Config: rtConfig, Enricher: mgr, Notify: mgr.infos} boxes := make([]box.ManagedBox, 0, len(boxURIs)+2) for _, uri := range boxURIs { p, err := Connect(uri, authManager, &cdata) if err != nil { return nil, err } if p != nil { boxes = append(boxes, p) cdata.Number++ } } constbox, err := registry[" const"](nil, &cdata) if err != nil { return nil, err } cdata.Number++ compbox, err := registry[" comp"](nil, &cdata) if err != nil { return nil, err } cdata.Number++ boxes = append(boxes, constbox, compbox) mgr.boxes = boxes return mgr, nil } // RegisterObserver registers an observer that will be notified // if a zettel was found to be changed. func (mgr *Manager) RegisterObserver(f box.UpdateFunc) { if f != nil { mgr.mxObserver.Lock() mgr.observers = append(mgr.observers, f) mgr.mxObserver.Unlock() } } func (mgr *Manager) notifier() { // The call to notify may panic. Ensure a running notifier. defer func() { if r := recover(); r != nil { kernel.Main.LogRecover("Notifier", r) go mgr.notifier() } }() tsLastEvent := time.Now() cache := destutterCache{} for { select { case ci, ok := <-mgr.infos: if ok { now := time.Now() if len(cache) > 1 && tsLastEvent.Add(10*time.Second).Before(now) { // Cache contains entries and is definitely outdated mgr.mgrLog.Trace().Msg("clean destutter cache") cache = destutterCache{} } tsLastEvent = now reason, zid := ci.Reason, ci.Zid mgr.mgrLog.Debug().Uint("reason", uint64(reason)).Zid(zid).Msg("notifier") if ignoreUpdate(cache, now, reason, zid) { mgr.mgrLog.Trace().Uint("reason", uint64(reason)).Zid(zid).Msg("notifier ignored") continue } mgr.idxEnqueue(reason, zid) if ci.Box == nil { ci.Box = mgr } if mgr.State() == box.StartStateStarted { mgr.notifyObserver(&ci) } } case <-mgr.done: return } } } type destutterData struct { deadAt time.Time reason box.UpdateReason } type destutterCache = map[id.Zid]destutterData func ignoreUpdate(cache destutterCache, now time.Time, reason box.UpdateReason, zid id.Zid) bool { if dsd, found := cache[zid]; found { if dsd.reason == reason && dsd.deadAt.After(now) { return true } } cache[zid] = destutterData{ deadAt: now.Add(500 * time.Millisecond), reason: reason, } return false } func (mgr *Manager) idxEnqueue(reason box.UpdateReason, zid id.Zid) { switch reason { case box.OnReady: return case box.OnReload: mgr.idxAr.Reset() case box.OnZettel: mgr.idxAr.EnqueueZettel(zid) default: mgr.mgrLog.Warn().Uint("reason", uint64(reason)).Zid(zid).Msg("Unknown notification reason") return } select { case mgr.idxReady <- struct{}{}: default: } } func (mgr *Manager) notifyObserver(ci *box.UpdateInfo) { mgr.mxObserver.RLock() observers := mgr.observers mgr.mxObserver.RUnlock() for _, ob := range observers { ob(*ci) } } // Start the box. Now all other functions of the box are allowed. // Starting an already started box is not allowed. func (mgr *Manager) Start(ctx context.Context) error { mgr.mgrMx.Lock() defer mgr.mgrMx.Unlock() if mgr.State() != box.StartStateStopped { return box.ErrStarted } mgr.setState(box.StartStateStarting) for i := len(mgr.boxes) - 1; i >= 0; i-- { ssi, ok := mgr.boxes[i].(box.StartStopper) if !ok { continue } err := ssi.Start(ctx) if err == nil { continue } mgr.setState(box.StartStateStopping) for j := i + 1; j < len(mgr.boxes); j++ { if ssj, ok2 := mgr.boxes[j].(box.StartStopper); ok2 { ssj.Stop(ctx) } } mgr.setState(box.StartStateStopped) return err } mgr.idxAr.Reset() // Ensure an initial index run mgr.done = make(chan struct{}) go mgr.notifier() for !mgr.allBoxesStarted() { mgr.mgrLog.Trace().Msg("Wait for boxes to start") time.Sleep(time.Second) } mgr.setState(box.StartStateStarted) mgr.notifyObserver(&box.UpdateInfo{Box: mgr, Reason: box.OnReady}) go mgr.idxIndexer() return nil } func (mgr *Manager) allBoxesStarted() bool { for _, bx := range mgr.boxes { if b, ok := bx.(box.StartStopper); ok && b.State() != box.StartStateStarted { return false } } return true } // Stop the started box. Now only the Start() function is allowed. func (mgr *Manager) Stop(ctx context.Context) { mgr.mgrMx.Lock() defer mgr.mgrMx.Unlock() if mgr.State() != box.StartStateStarted { return } mgr.setState(box.StartStateStopping) close(mgr.done) for _, p := range mgr.boxes { if ss, ok := p.(box.StartStopper); ok { ss.Stop(ctx) } } mgr.setState(box.StartStateStopped) } // Refresh internal box data. func (mgr *Manager) Refresh(ctx context.Context) error { mgr.mgrLog.Debug().Msg("Refresh") if mgr.State() != box.StartStateStarted { return box.ErrStopped } mgr.mgrMx.Lock() defer mgr.mgrMx.Unlock() mgr.infos <- box.UpdateInfo{Reason: box.OnReload, Zid: id.Invalid} for _, bx := range mgr.boxes { if rb, ok := bx.(box.Refresher); ok { rb.Refresh(ctx) } } return nil } // ReadStats populates st with box statistics. func (mgr *Manager) ReadStats(st *box.Stats) { mgr.mgrLog.Debug().Msg("ReadStats") mgr.mgrMx.RLock() defer mgr.mgrMx.RUnlock() subStats := make([]box.ManagedBoxStats, len(mgr.boxes)) for i, p := range mgr.boxes { p.ReadStats(&subStats[i]) } st.ReadOnly = true sumZettel := 0 for _, sst := range subStats { if !sst.ReadOnly { st.ReadOnly = false } sumZettel += sst.Zettel } st.NumManagedBoxes = len(mgr.boxes) st.ZettelTotal = sumZettel var storeSt store.Stats mgr.idxMx.RLock() defer mgr.idxMx.RUnlock() mgr.idxStore.ReadStats(&storeSt) st.LastReload = mgr.idxLastReload st.IndexesSinceReload = mgr.idxSinceReload st.DurLastReload = mgr.idxDurReload st.ZettelIndexed = storeSt.Zettel st.IndexUpdates = storeSt.Updates st.IndexedWords = storeSt.Words st.IndexedUrls = storeSt.Urls } // Dump internal data structures to a Writer. func (mgr *Manager) Dump(w io.Writer) { mgr.idxStore.Dump(w) } |
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-present Detlef Stern // // This file is part of Zettelstore. // // Zettelstore is licensed under the latest version of the EUPL (European Union // Public License). Please see file LICENSE.txt for your rights and obligations // under this license. //----------------------------------------------------------------------------- // Package memstore stored the index in main memory. package memstore import ( "context" "fmt" "io" "sort" "strings" "sync" "zettelstore.de/c/api" "zettelstore.de/c/maps" "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 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(_ context.Context, m *meta.Meta) { if ms.doEnrich(m) { ms.mx.Lock() ms.updates++ ms.mx.Unlock() } } func (ms *memStore) doEnrich(m *meta.Meta) bool { ms.mx.RLock() defer ms.mx.RUnlock() zi, ok := ms.idx[m.Zid] if !ok { return false } var updated bool if len(zi.dead) > 0 { m.Set(api.KeyDead, zi.dead.String()) updated = true } back := removeOtherMetaRefs(m, zi.backward.Copy()) if len(zi.backward) > 0 { m.Set(api.KeyBackward, zi.backward.String()) updated = true } if len(zi.forward) > 0 { m.Set(api.KeyForward, zi.forward.String()) back = remRefs(back, zi.forward) updated = true } for k, refs := range zi.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(api.KeyBack, back.String()) updated = true } return updated } // SearchEqual returns all zettel that contains the given exact word. // The word must be normalized through Unicode NKFD, trimmed and not empty. func (ms *memStore) SearchEqual(word string) id.Set { ms.mx.RLock() defer ms.mx.RUnlock() result := id.NewSet() if refs, ok := ms.words[word]; ok { result.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 } maxZid, err := id.Parse(prefix + "99999999999999"[:14-l]) if err != nil { return result } var minZid id.Zid if l < 14 && prefix == "0000000000000"[:l] { minZid = id.Zid(1) } else { minZid, err = id.Parse(prefix + "00000000000000"[:14-l]) if err != nil { return result } } for zid, zi := range ms.idx { if minZid <= zid && zid <= maxZid { addBackwardZids(result, zid, zi) } } return result } // SearchSuffix returns all zettel that have a word with the given suffix. // The suffix must be normalized through Unicode NKFD, trimmed and not empty. func (ms *memStore) SearchSuffix(suffix string) id.Set { ms.mx.RLock() defer ms.mx.RUnlock() result := ms.selectWithPred(suffix, strings.HasSuffix) l := len(suffix) if l > 14 { return result } val, err := id.ParseUint(suffix) if err != nil { return result } modulo := uint64(1) for i := 0; i < l; i++ { modulo *= 10 } for zid, zi := range ms.idx { if uint64(zid)%modulo == val { addBackwardZids(result, zid, zi) } } return result } // SearchContains returns all zettel that contains the given string. // The string must be normalized through Unicode NKFD, trimmed and not empty. func (ms *memStore) SearchContains(s string) id.Set { ms.mx.RLock() defer ms.mx.RUnlock() result := ms.selectWithPred(s, strings.Contains) if len(s) > 14 { return result } if _, err := id.ParseUint(s); err != nil { return result } for zid, zi := range ms.idx { if strings.Contains(zid.String(), s) { addBackwardZids(result, zid, zi) } } return result } func (ms *memStore) selectWithPred(s string, pred func(string, string) bool) id.Set { // Must only be called if ms.mx is read-locked! result := id.NewSet() for word, refs := range ms.words { if !pred(word, s) { continue } result.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(zid) 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() { switch meta.Type(p.Key) { case meta.TypeID: if zid, err := id.Parse(p.Value); err == nil { back = remRef(back, zid) } case meta.TypeIDSet: for _, val := range meta.ListFromValue(p.Value) { if zid, err := id.Parse(val); err == nil { back = remRef(back, zid) } } } } return back } func (ms *memStore) UpdateReferences(_ context.Context, zidx *store.ZettelIndex) id.Set { ms.mx.Lock() defer ms.mx.Unlock() 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(_ 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.Zid(ref) } } return toCheck } func (ms *memStore) removeInverseMeta(zid id.Zid, key string, forward id.Slice) { // Must only be called if ms.mx is write-locked! for _, ref := range forward { bzi, ok := ms.idx[ref] if !ok || bzi.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) for _, s := range maps.Keys(srefs) { 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 | //----------------------------------------------------------------------------- // Copyright (c) 2021-present Detlef Stern // // This file is part of Zettelstore. // // Zettelstore is licensed under the latest version of the EUPL (European Union // Public License). Please see file LICENSE.txt for your rights and obligations // under this license. //----------------------------------------------------------------------------- package memstore import "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 | //----------------------------------------------------------------------------- // Copyright (c) 2021-present Detlef Stern // // This file is part of Zettelstore. // // Zettelstore is licensed under the latest version of the EUPL (European Union // Public License). Please see file LICENSE.txt for your rights and obligations // under this license. //----------------------------------------------------------------------------- package memstore import ( "testing" "zettelstore.de/z/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-present Detlef Stern // // This file is part of Zettelstore. // // Zettelstore is licensed under the latest version of the EUPL (European Union // Public License). Please see file LICENSE.txt for your rights and obligations // under this license. //----------------------------------------------------------------------------- // Package 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/query" ) // Stats records statistics about the store. type Stats struct { // Zettel is the number of zettel managed by the indexer. Zettel int // Updates count the number of metadata updates. Updates uint64 // Words count the different words stored in the store. Words uint64 // Urls count the different URLs stored in the store. Urls uint64 } // Store all relevant zettel data. There may be multiple implementations, i.e. // memory-based, file-based, based on SQLite, ... type Store interface { query.Searcher // 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 | //----------------------------------------------------------------------------- // Copyright (c) 2021-present Detlef Stern // // This file is part of Zettelstore. // // Zettelstore is licensed under the latest version of the EUPL (European Union // Public License). Please see file LICENSE.txt for your rights and obligations // under this license. //----------------------------------------------------------------------------- package 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 | //----------------------------------------------------------------------------- // Copyright (c) 2021-present Detlef Stern // // This file is part of Zettelstore. // // Zettelstore is licensed under the latest version of the EUPL (European Union // Public License). Please see file LICENSE.txt for your rights and obligations // under this license. //----------------------------------------------------------------------------- package 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 | //----------------------------------------------------------------------------- // Copyright (c) 2021-present Detlef Stern // // This file is part of Zettelstore. // // Zettelstore is licensed under the latest version of the EUPL (European Union // Public License). Please see file LICENSE.txt for your rights and obligations // under this license. //----------------------------------------------------------------------------- package 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(zid) } // 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(zid) 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(zid) } // SetWords sets the words to the given value. func (zi *ZettelIndex) SetWords(words WordSet) { zi.words = words } // SetUrls sets the words to the given value. func (zi *ZettelIndex) SetUrls(urls WordSet) { zi.urls = urls } // GetDeadRefs returns all dead references as a sorted list. func (zi *ZettelIndex) GetDeadRefs() id.Slice { return zi.deadrefs.Sorted() } // 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 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 | //----------------------------------------------------------------------------- // Copyright (c) 2020-present Detlef Stern // // This file is part of Zettelstore. // // Zettelstore is licensed under the latest version of the EUPL (European Union // Public License). Please see file LICENSE.txt for your rights and obligations // under this license. //----------------------------------------------------------------------------- // Package 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/kernel" "zettelstore.de/z/logger" "zettelstore.de/z/query" ) func init() { manager.Register( "mem", func(u *url.URL, cdata *manager.ConnectData) (box.ManagedBox, error) { return &memBox{ log: kernel.Main.GetLogger(kernel.BoxService).Clone(). Str("box", "mem").Int("boxnum", int64(cdata.Number)).Child(), u: u, cdata: *cdata, maxZettel: box.GetQueryInt(u, "max-zettel", 0, 127, 65535), maxBytes: box.GetQueryInt(u, "max-bytes", 0, 65535, (1024*1024*1024)-1), }, nil }) } type memBox struct { log *logger.Logger u *url.URL cdata manager.ConnectData maxZettel int maxBytes int mx sync.RWMutex // Protects the following fields zettel map[id.Zid]domain.Zettel curBytes int } func (mb *memBox) notifyChanged(zid id.Zid) { if chci := mb.cdata.Notify; chci != nil { chci <- box.UpdateInfo{Box: mb, Reason: box.OnZettel, Zid: zid} } } func (mb *memBox) Location() string { return mb.u.String() } func (mb *memBox) State() box.StartState { mb.mx.RLock() defer mb.mx.RUnlock() if mb.zettel == nil { return box.StartStateStopped } return box.StartStateStarted } func (mb *memBox) Start(context.Context) error { mb.mx.Lock() mb.zettel = make(map[id.Zid]domain.Zettel) mb.curBytes = 0 mb.mx.Unlock() mb.log.Trace().Int("max-zettel", int64(mb.maxZettel)).Int("max-bytes", int64(mb.maxBytes)).Msg("Start Box") return nil } func (mb *memBox) Stop(context.Context) { mb.mx.Lock() mb.zettel = nil mb.mx.Unlock() } func (mb *memBox) CanCreateZettel(context.Context) bool { mb.mx.RLock() defer mb.mx.RUnlock() return len(mb.zettel) < mb.maxZettel } func (mb *memBox) CreateZettel(_ context.Context, zettel domain.Zettel) (id.Zid, error) { mb.mx.Lock() newBytes := mb.curBytes + zettel.Length() if mb.maxZettel < len(mb.zettel) || mb.maxBytes < newBytes { mb.mx.Unlock() return id.Invalid, box.ErrCapacity } zid, err := box.GetNewZid(func(zid id.Zid) (bool, error) { _, ok := mb.zettel[zid] return !ok, nil }) if err != nil { mb.mx.Unlock() return id.Invalid, err } meta := zettel.Meta.Clone() meta.Zid = zid zettel.Meta = meta mb.zettel[zid] = zettel mb.curBytes = newBytes mb.mx.Unlock() mb.notifyChanged(zid) mb.log.Trace().Zid(zid).Msg("CreateZettel") return zid, nil } func (mb *memBox) GetZettel(_ context.Context, zid id.Zid) (domain.Zettel, error) { mb.mx.RLock() zettel, ok := mb.zettel[zid] mb.mx.RUnlock() if !ok { return domain.Zettel{}, box.ErrNotFound } zettel.Meta = zettel.Meta.Clone() mb.log.Trace().Msg("GetZettel") return zettel, nil } func (mb *memBox) GetMeta(_ context.Context, zid id.Zid) (*meta.Meta, error) { mb.mx.RLock() zettel, ok := mb.zettel[zid] mb.mx.RUnlock() if !ok { return nil, box.ErrNotFound } mb.log.Trace().Msg("GetMeta") return zettel.Meta.Clone(), nil } func (mb *memBox) ApplyZid(_ context.Context, handle box.ZidFunc, constraint query.RetrievePredicate) error { mb.mx.RLock() defer mb.mx.RUnlock() mb.log.Trace().Int("entries", int64(len(mb.zettel))).Msg("ApplyZid") for zid := range mb.zettel { if constraint(zid) { handle(zid) } } return nil } func (mb *memBox) ApplyMeta(ctx context.Context, handle box.MetaFunc, constraint query.RetrievePredicate) error { mb.mx.RLock() defer mb.mx.RUnlock() mb.log.Trace().Int("entries", int64(len(mb.zettel))).Msg("ApplyMeta") for zid, zettel := range mb.zettel { if constraint(zid) { m := zettel.Meta.Clone() mb.cdata.Enricher.Enrich(ctx, m, mb.cdata.Number) handle(m) } } return nil } func (mb *memBox) CanUpdateZettel(_ context.Context, zettel domain.Zettel) bool { mb.mx.RLock() defer mb.mx.RUnlock() zid := zettel.Meta.Zid if !zid.IsValid() { return false } newBytes := mb.curBytes + zettel.Length() if prevZettel, found := mb.zettel[zid]; found { newBytes -= prevZettel.Length() } return newBytes < mb.maxBytes } func (mb *memBox) UpdateZettel(_ context.Context, zettel domain.Zettel) error { m := zettel.Meta.Clone() if !m.Zid.IsValid() { return &box.ErrInvalidID{Zid: m.Zid} } mb.mx.Lock() newBytes := mb.curBytes + zettel.Length() if prevZettel, found := mb.zettel[m.Zid]; found { newBytes -= prevZettel.Length() } if mb.maxBytes < newBytes { mb.mx.Unlock() return box.ErrCapacity } zettel.Meta = m mb.zettel[m.Zid] = zettel mb.curBytes = newBytes mb.mx.Unlock() mb.notifyChanged(m.Zid) mb.log.Trace().Msg("UpdateZettel") return nil } func (*memBox) AllowRenameZettel(context.Context, id.Zid) bool { return true } func (mb *memBox) RenameZettel(_ context.Context, curZid, newZid id.Zid) error { mb.mx.Lock() zettel, ok := mb.zettel[curZid] if !ok { mb.mx.Unlock() return box.ErrNotFound } // Check that there is no zettel with newZid if _, ok = mb.zettel[newZid]; ok { mb.mx.Unlock() return &box.ErrInvalidID{Zid: newZid} } meta := zettel.Meta.Clone() meta.Zid = newZid zettel.Meta = meta mb.zettel[newZid] = zettel delete(mb.zettel, curZid) mb.mx.Unlock() mb.notifyChanged(curZid) mb.notifyChanged(newZid) mb.log.Trace().Msg("RenameZettel") return nil } func (mb *memBox) CanDeleteZettel(_ context.Context, zid id.Zid) bool { mb.mx.RLock() _, ok := mb.zettel[zid] mb.mx.RUnlock() return ok } func (mb *memBox) DeleteZettel(_ context.Context, zid id.Zid) error { mb.mx.Lock() oldZettel, found := mb.zettel[zid] if !found { mb.mx.Unlock() return box.ErrNotFound } delete(mb.zettel, zid) mb.curBytes -= oldZettel.Length() mb.mx.Unlock() mb.notifyChanged(zid) mb.log.Trace().Msg("DeleteZettel") return nil } func (mb *memBox) ReadStats(st *box.ManagedBoxStats) { st.ReadOnly = false mb.mx.RLock() st.Zettel = len(mb.zettel) mb.mx.RUnlock() mb.log.Trace().Int("zettel", int64(st.Zettel)).Msg("ReadStats") } |
Added box/notify/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 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 581 582 583 584 585 586 587 588 589 590 591 592 593 594 595 596 597 598 599 600 601 602 603 604 605 606 607 608 609 610 | //----------------------------------------------------------------------------- // Copyright (c) 2020-present Detlef Stern // // This file is part of Zettelstore. // // Zettelstore is licensed under the latest version of the EUPL (European Union // Public License). Please see file LICENSE.txt for your rights and obligations // under this license. //----------------------------------------------------------------------------- package notify import ( "errors" "fmt" "path/filepath" "regexp" "strings" "sync" "zettelstore.de/z/box" "zettelstore.de/z/domain/id" "zettelstore.de/z/kernel" "zettelstore.de/z/logger" "zettelstore.de/z/parser" "zettelstore.de/z/query" "zettelstore.de/z/strfun" ) type entrySet map[id.Zid]*DirEntry // DirServiceState signal the internal state of the service. // // The following state transitions are possible: // --newDirService--> dsCreated // dsCreated --Start--> dsStarting // dsStarting --last list notification--> dsWorking // dsWorking --directory missing--> dsMissing // dsMissing --last list notification--> dsWorking // --Stop--> dsStopping type DirServiceState uint8 const ( DsCreated DirServiceState = iota DsStarting // Reading inital scan DsWorking // Initial scan complete, fully operational DsMissing // Directory is missing DsStopping // Service is shut down ) // DirService specifies a directory service for file based zettel. type DirService struct { box box.ManagedBox log *logger.Logger dirPath string notifier Notifier infos chan<- box.UpdateInfo mx sync.RWMutex // protects status, entries state DirServiceState entries entrySet } // ErrNoDirectory signals missing directory data. var ErrNoDirectory = errors.New("unable to retrieve zettel directory information") // NewDirService creates a new directory service. func NewDirService(box box.ManagedBox, log *logger.Logger, notifier Notifier, chci chan<- box.UpdateInfo) *DirService { return &DirService{ box: box, log: log, notifier: notifier, infos: chci, state: DsCreated, } } // State the current service state. func (ds *DirService) State() DirServiceState { ds.mx.RLock() state := ds.state ds.mx.RUnlock() return state } // Start the directory service. func (ds *DirService) Start() { ds.mx.Lock() ds.state = DsStarting ds.mx.Unlock() var newEntries entrySet go ds.updateEvents(newEntries) } // Refresh the directory entries. func (ds *DirService) Refresh() { ds.notifier.Refresh() } // Stop the directory service. func (ds *DirService) Stop() { ds.mx.Lock() ds.state = DsStopping ds.mx.Unlock() ds.notifier.Close() } func (ds *DirService) logMissingEntry(action string) error { err := ErrNoDirectory ds.log.Info().Err(err).Str("action", action).Msg("Unable to get directory information") return err } // NumDirEntries returns the number of entries in the directory. func (ds *DirService) NumDirEntries() int { ds.mx.RLock() defer ds.mx.RUnlock() if ds.entries == nil { return 0 } return len(ds.entries) } // GetDirEntries returns a list of directory entries, which satisfy the given constraint. func (ds *DirService) GetDirEntries(constraint query.RetrievePredicate) []*DirEntry { ds.mx.RLock() defer ds.mx.RUnlock() if ds.entries == nil { return nil } result := make([]*DirEntry, 0, len(ds.entries)) for zid, entry := range ds.entries { if constraint(zid) { copiedEntry := *entry result = append(result, &copiedEntry) } } return result } // GetDirEntry returns a directory entry with the given zid, or nil if not found. func (ds *DirService) GetDirEntry(zid id.Zid) *DirEntry { ds.mx.RLock() defer ds.mx.RUnlock() if ds.entries == nil { return nil } foundEntry := ds.entries[zid] if foundEntry == nil { return nil } result := *foundEntry return &result } // SetNewDirEntry calculates an empty directory entry with an unused identifier and // stores it in the directory. func (ds *DirService) SetNewDirEntry() (id.Zid, error) { ds.mx.Lock() defer ds.mx.Unlock() if ds.entries == nil { return id.Invalid, ds.logMissingEntry("new") } zid, err := box.GetNewZid(func(zid id.Zid) (bool, error) { _, found := ds.entries[zid] return !found, nil }) if err != nil { return id.Invalid, err } ds.entries[zid] = &DirEntry{Zid: zid} return zid, nil } // UpdateDirEntry updates an directory entry in place. func (ds *DirService) UpdateDirEntry(updatedEntry *DirEntry) error { entry := *updatedEntry ds.mx.Lock() defer ds.mx.Unlock() if ds.entries == nil { return ds.logMissingEntry("update") } ds.entries[entry.Zid] = &entry return nil } // RenameDirEntry replaces an existing directory entry with a new one. func (ds *DirService) RenameDirEntry(oldEntry *DirEntry, newZid id.Zid) (DirEntry, error) { ds.mx.Lock() defer ds.mx.Unlock() if ds.entries == nil { return DirEntry{}, ds.logMissingEntry("rename") } if _, found := ds.entries[newZid]; found { return DirEntry{}, &box.ErrInvalidID{Zid: newZid} } oldZid := oldEntry.Zid newEntry := DirEntry{ Zid: newZid, MetaName: renameFilename(oldEntry.MetaName, oldZid, newZid), ContentName: renameFilename(oldEntry.ContentName, oldZid, newZid), ContentExt: oldEntry.ContentExt, // Duplicates must not be set, because duplicates will be deleted } delete(ds.entries, oldZid) ds.entries[newZid] = &newEntry return newEntry, nil } func renameFilename(name string, curID, newID id.Zid) string { if cur := curID.String(); strings.HasPrefix(name, cur) { name = newID.String() + name[len(cur):] } return name } // DeleteDirEntry removes a entry from the directory. func (ds *DirService) DeleteDirEntry(zid id.Zid) error { ds.mx.Lock() defer ds.mx.Unlock() if ds.entries == nil { return ds.logMissingEntry("delete") } delete(ds.entries, zid) return nil } func (ds *DirService) updateEvents(newEntries entrySet) { // Something may panic. Ensure a running service. defer func() { if r := recover(); r != nil { kernel.Main.LogRecover("DirectoryService", r) go ds.updateEvents(newEntries) } }() for ev := range ds.notifier.Events() { e, ok := ds.handleEvent(ev, newEntries) if !ok { break } newEntries = e } } func (ds *DirService) handleEvent(ev Event, newEntries entrySet) (entrySet, bool) { ds.mx.RLock() state := ds.state ds.mx.RUnlock() if msg := ds.log.Trace(); msg.Enabled() { msg.Uint("state", uint64(state)).Str("op", ev.Op.String()).Str("name", ev.Name).Msg("notifyEvent") } if state == DsStopping { return nil, false } switch ev.Op { case Error: newEntries = nil if state != DsMissing { ds.log.Warn().Err(ev.Err).Msg("Notifier confused") } case Make: newEntries = make(entrySet) case List: if ev.Name == "" { zids := getNewZids(newEntries) ds.mx.Lock() fromMissing := ds.state == DsMissing prevEntries := ds.entries ds.entries = newEntries ds.state = DsWorking ds.mx.Unlock() ds.onCreateDirectory(zids, prevEntries) if fromMissing { ds.log.Info().Str("path", ds.dirPath).Msg("Zettel directory found") } return nil, true } if newEntries != nil { ds.onUpdateFileEvent(newEntries, ev.Name) } case Destroy: ds.onDestroyDirectory() ds.log.Error().Str("path", ds.dirPath).Msg("Zettel directory missing") return nil, true case Update: ds.mx.Lock() zid := ds.onUpdateFileEvent(ds.entries, ev.Name) ds.mx.Unlock() if zid != id.Invalid { ds.notifyChange(zid) } case Delete: ds.mx.Lock() zid := ds.onDeleteFileEvent(ds.entries, ev.Name) ds.mx.Unlock() if zid != id.Invalid { ds.notifyChange(zid) } default: ds.log.Warn().Str("event", fmt.Sprintf("%v", ev)).Msg("Unknown zettel notification event") } return newEntries, true } func getNewZids(entries entrySet) id.Slice { zids := make(id.Slice, 0, len(entries)) for zid := range entries { zids = append(zids, zid) } return zids } func (ds *DirService) onCreateDirectory(zids id.Slice, prevEntries entrySet) { for _, zid := range zids { ds.notifyChange(zid) delete(prevEntries, zid) } // These were previously stored, by are not found now. // Notify system that these were deleted, e.g. for updating the index. for zid := range prevEntries { ds.notifyChange(zid) } } func (ds *DirService) onDestroyDirectory() { ds.mx.Lock() entries := ds.entries ds.entries = nil ds.state = DsMissing ds.mx.Unlock() for zid := range entries { ds.notifyChange(zid) } } var validFileName = regexp.MustCompile(`^(\d{14})`) func matchValidFileName(name string) []string { return validFileName.FindStringSubmatch(name) } func seekZid(name string) id.Zid { match := matchValidFileName(name) if len(match) == 0 { return id.Invalid } zid, err := id.Parse(match[1]) if err != nil { return id.Invalid } return zid } func fetchdirEntry(entries entrySet, zid id.Zid) *DirEntry { if entry, found := entries[zid]; found { return entry } entry := &DirEntry{Zid: zid} entries[zid] = entry return entry } func (ds *DirService) onUpdateFileEvent(entries entrySet, name string) id.Zid { if entries == nil { return id.Invalid } zid := seekZid(name) if zid == id.Invalid { return id.Invalid } entry := fetchdirEntry(entries, zid) dupName1, dupName2 := ds.updateEntry(entry, name) if dupName1 != "" { ds.log.Warn().Str("name", dupName1).Msg("Duplicate content (is ignored)") if dupName2 != "" { ds.log.Warn().Str("name", dupName2).Msg("Duplicate content (is ignored)") } return id.Invalid } return zid } func (ds *DirService) onDeleteFileEvent(entries entrySet, name string) id.Zid { if entries == nil { return id.Invalid } zid := seekZid(name) if zid == id.Invalid { return id.Invalid } entry, found := entries[zid] if !found { return zid } for i, dupName := range entry.UselessFiles { if dupName == name { removeDuplicate(entry, i) return zid } } if name == entry.ContentName { entry.ContentName = "" entry.ContentExt = "" ds.replayUpdateUselessFiles(entry) } else if name == entry.MetaName { entry.MetaName = "" ds.replayUpdateUselessFiles(entry) } if entry.ContentName == "" && entry.MetaName == "" { delete(entries, zid) } return zid } func removeDuplicate(entry *DirEntry, i int) { if len(entry.UselessFiles) == 1 { entry.UselessFiles = nil return } entry.UselessFiles = entry.UselessFiles[:i+copy(entry.UselessFiles[i:], entry.UselessFiles[i+1:])] } func (ds *DirService) replayUpdateUselessFiles(entry *DirEntry) { uselessFiles := entry.UselessFiles if len(uselessFiles) == 0 { return } entry.UselessFiles = make([]string, 0, len(uselessFiles)) for _, name := range uselessFiles { ds.updateEntry(entry, name) } if len(uselessFiles) == len(entry.UselessFiles) { return } loop: for _, prevName := range uselessFiles { for _, newName := range entry.UselessFiles { if prevName == newName { continue loop } } ds.log.Info().Str("name", prevName).Msg("Previous duplicate file becomes useful") } } func (ds *DirService) updateEntry(entry *DirEntry, name string) (string, string) { ext := onlyExt(name) if !extIsMetaAndContent(entry.ContentExt) { if ext == "" { return updateEntryMeta(entry, name), "" } if entry.MetaName == "" { if nameWithoutExt(name, ext) == entry.ContentName { // We have marked a file as content file, but it is a metadata file, // because it is the same as the new file without extension. entry.MetaName = entry.ContentName entry.ContentName = "" entry.ContentExt = "" ds.replayUpdateUselessFiles(entry) } else if entry.ContentName != "" && nameWithoutExt(entry.ContentName, entry.ContentExt) == name { // We have already a valid content file, and new file should serve as metadata file, // because it is the same as the content file without extension. entry.MetaName = name return "", "" } } } return updateEntryContent(entry, name, ext) } func nameWithoutExt(name, ext string) string { return name[0 : len(name)-len(ext)-1] } func updateEntryMeta(entry *DirEntry, name string) string { metaName := entry.MetaName if metaName == "" { entry.MetaName = name return "" } if metaName == name { return "" } if newNameIsBetter(metaName, name) { entry.MetaName = name return addUselessFile(entry, metaName) } return addUselessFile(entry, name) } func updateEntryContent(entry *DirEntry, name, ext string) (string, string) { contentName := entry.ContentName if contentName == "" { entry.ContentName = name entry.ContentExt = ext return "", "" } if contentName == name { return "", "" } contentExt := entry.ContentExt if contentExt == ext { if newNameIsBetter(contentName, name) { entry.ContentName = name return addUselessFile(entry, contentName), "" } return addUselessFile(entry, name), "" } if contentExt == extZettel { return addUselessFile(entry, name), "" } if ext == extZettel { entry.ContentName = name entry.ContentExt = ext contentName = addUselessFile(entry, contentName) if metaName := entry.MetaName; metaName != "" { metaName = addUselessFile(entry, metaName) entry.MetaName = "" return contentName, metaName } return contentName, "" } if newExtIsBetter(contentExt, ext) { entry.ContentName = name entry.ContentExt = ext return addUselessFile(entry, contentName), "" } return addUselessFile(entry, name), "" } func addUselessFile(entry *DirEntry, name string) string { for _, dupName := range entry.UselessFiles { if name == dupName { return "" } } entry.UselessFiles = append(entry.UselessFiles, name) return name } func onlyExt(name string) string { ext := filepath.Ext(name) if ext == "" || ext[0] != '.' { return ext } return ext[1:] } func newNameIsBetter(oldName, newName string) bool { if len(oldName) < len(newName) { return false } return oldName > newName } var supportedSyntax, primarySyntax strfun.Set func init() { syntaxList := parser.GetSyntaxes() supportedSyntax = strfun.NewSet(syntaxList...) primarySyntax = make(map[string]struct{}, len(syntaxList)) for _, syntax := range syntaxList { if parser.Get(syntax).Name == syntax { primarySyntax.Set(syntax) } } } func newExtIsBetter(oldExt, newExt string) bool { oldSyntax := supportedSyntax.Has(oldExt) if oldSyntax != supportedSyntax.Has(newExt) { return !oldSyntax } if oldSyntax { if oldExt == "zmk" { return false } if newExt == "zmk" { return true } oldInfo := parser.Get(oldExt) newInfo := parser.Get(newExt) if oldASTParser := oldInfo.IsASTParser; oldASTParser != newInfo.IsASTParser { return !oldASTParser } if oldTextFormat := oldInfo.IsTextFormat; oldTextFormat != newInfo.IsTextFormat { return !oldTextFormat } if oldImageFormat := oldInfo.IsImageFormat; oldImageFormat != newInfo.IsImageFormat { return oldImageFormat } if oldPrimary := primarySyntax.Has(oldExt); oldPrimary != primarySyntax.Has(newExt) { return !oldPrimary } } oldLen := len(oldExt) newLen := len(newExt) if oldLen != newLen { return newLen < oldLen } return newExt < oldExt } func (ds *DirService) notifyChange(zid id.Zid) { if chci := ds.infos; chci != nil { ds.log.Trace().Zid(zid).Msg("notifyChange") chci <- box.UpdateInfo{Box: ds.box, Reason: box.OnZettel, Zid: zid} } } |
Added box/notify/directory_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) 2022-present Detlef Stern // // This file is part of Zettelstore. // // Zettelstore is licensed under the latest version of the EUPL (European Union // Public License). Please see file LICENSE.txt for your rights and obligations // under this license. //----------------------------------------------------------------------------- package notify import ( "testing" "zettelstore.de/z/domain/id" "zettelstore.de/z/domain/meta" _ "zettelstore.de/z/parser/blob" // Allow to use BLOB parser. _ "zettelstore.de/z/parser/draw" // Allow to use draw parser. _ "zettelstore.de/z/parser/markdown" // Allow to use markdown parser. _ "zettelstore.de/z/parser/none" // Allow to use none parser. _ "zettelstore.de/z/parser/plain" // Allow to use plain parser. _ "zettelstore.de/z/parser/zettelmark" // Allow to use zettelmark parser. ) func TestSeekZid(t *testing.T) { testcases := []struct { name string zid id.Zid }{ {"", id.Invalid}, {"1", id.Invalid}, {"1234567890123", id.Invalid}, {" 12345678901234", id.Invalid}, {"12345678901234", id.Zid(12345678901234)}, {"12345678901234.ext", id.Zid(12345678901234)}, {"12345678901234 abc.ext", id.Zid(12345678901234)}, {"12345678901234.abc.ext", id.Zid(12345678901234)}, {"12345678901234 def", id.Zid(12345678901234)}, } for _, tc := range testcases { gotZid := seekZid(tc.name) if gotZid != tc.zid { t.Errorf("seekZid(%q) == %v, but got %v", tc.name, tc.zid, gotZid) } } } func TestNewExtIsBetter(t *testing.T) { extVals := []string{ // Main Formats meta.SyntaxZmk, meta.SyntaxDraw, meta.SyntaxMarkdown, meta.SyntaxMD, // Other supported text formats meta.SyntaxCSS, meta.SyntaxTxt, meta.SyntaxHTML, meta.SyntaxMustache, meta.SyntaxText, meta.SyntaxPlain, // Supported text graphics formats meta.SyntaxSVG, meta.SyntaxNone, // Supported binary graphic formats meta.SyntaxGif, meta.SyntaxPNG, meta.SyntaxJPEG, meta.SyntaxWebp, meta.SyntaxJPG, // Unsupported syntax values "gz", "cpp", "tar", "cppc", } for oldI, oldExt := range extVals { for newI, newExt := range extVals { if oldI <= newI { continue } if !newExtIsBetter(oldExt, newExt) { t.Errorf("newExtIsBetter(%q, %q) == true, but got false", oldExt, newExt) } if newExtIsBetter(newExt, oldExt) { t.Errorf("newExtIsBetter(%q, %q) == false, but got true", newExt, oldExt) } } } } |
Added box/notify/entry.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 | //----------------------------------------------------------------------------- // Copyright (c) 2020-present Detlef Stern // // This file is part of Zettelstore. // // Zettelstore is licensed under the latest version of the EUPL (European Union // Public License). Please see file LICENSE.txt for your rights and obligations // under this license. //----------------------------------------------------------------------------- package notify import ( "path/filepath" "zettelstore.de/c/api" "zettelstore.de/z/domain" "zettelstore.de/z/domain/id" "zettelstore.de/z/domain/meta" "zettelstore.de/z/parser" ) const ( extZettel = "zettel" // file contains metadata and content extBin = "bin" // file contains binary content extTxt = "txt" // file contains non-binary content ) func extIsMetaAndContent(ext string) bool { return ext == extZettel } // DirEntry stores everything for a directory entry. type DirEntry struct { Zid id.Zid MetaName string // file name of meta information ContentName string // file name of zettel content ContentExt string // (normalized) file extension of zettel content UselessFiles []string // list of other content files } // IsValid checks whether the entry is valid. func (e *DirEntry) IsValid() bool { return e != nil && e.Zid.IsValid() } // HasMetaInContent returns true, if metadata will be stored in the content file. func (e *DirEntry) HasMetaInContent() bool { return e.IsValid() && extIsMetaAndContent(e.ContentExt) } // SetupFromMetaContent fills entry data based on metadata and zettel content. func (e *DirEntry) SetupFromMetaContent(m *meta.Meta, content domain.Content, getZettelFileSyntax func() []string) { if e.Zid != m.Zid { panic("Zid differ") } if contentName := e.ContentName; contentName != "" { if !extIsMetaAndContent(e.ContentExt) && e.MetaName == "" { e.MetaName = e.calcBaseName(contentName) } return } syntax := m.GetDefault(api.KeySyntax, "") ext := calcContentExt(syntax, m.YamlSep, getZettelFileSyntax) metaName := e.MetaName eimc := extIsMetaAndContent(ext) if eimc { if metaName != "" { ext = contentExtWithMeta(syntax, content) } e.ContentName = e.calcBaseName(metaName) + "." + ext e.ContentExt = ext } else { if len(content.AsBytes()) > 0 { e.ContentName = e.calcBaseName(metaName) + "." + ext e.ContentExt = ext } if metaName == "" { e.MetaName = e.calcBaseName(e.ContentName) } } } func contentExtWithMeta(syntax string, content domain.Content) string { p := parser.Get(syntax) if content.IsBinary() { if p.IsImageFormat { return syntax } return extBin } if p.IsImageFormat { return extTxt } return syntax } func calcContentExt(syntax string, yamlSep bool, getZettelFileSyntax func() []string) string { if yamlSep { return extZettel } switch syntax { case meta.SyntaxNone, meta.SyntaxZmk: return extZettel } for _, s := range getZettelFileSyntax() { if s == syntax { return extZettel } } return syntax } func (e *DirEntry) calcBaseName(name string) string { if name == "" { return e.Zid.String() } return name[0 : len(name)-len(filepath.Ext(name))] } |
Added box/notify/fsdir.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 | //----------------------------------------------------------------------------- // Copyright (c) 2021-present Detlef Stern // // This file is part of Zettelstore. // // Zettelstore is licensed under the latest version of the EUPL (European Union // Public License). Please see file LICENSE.txt for your rights and obligations // under this license. //----------------------------------------------------------------------------- package notify import ( "os" "path/filepath" "strings" "github.com/fsnotify/fsnotify" "zettelstore.de/z/logger" ) type fsdirNotifier struct { log *logger.Logger events chan Event done chan struct{} refresh chan struct{} base *fsnotify.Watcher path string fetcher EntryFetcher parent string } // NewFSDirNotifier creates a directory based notifier that receives notifications // from the file system. func NewFSDirNotifier(log *logger.Logger, path string) (Notifier, error) { absPath, err := filepath.Abs(path) if err != nil { log.Debug().Err(err).Str("path", path).Msg("Unable to create absolute path") return nil, err } watcher, err := fsnotify.NewWatcher() if err != nil { log.Debug().Err(err).Str("absPath", absPath).Msg("Unable to create watcher") return nil, err } absParentDir := filepath.Dir(absPath) errParent := watcher.Add(absParentDir) err = watcher.Add(absPath) if errParent != nil { if err != nil { log.Error(). Str("parentDir", absParentDir).Err(errParent). Str("path", absPath).Err(err). Msg("Unable to access Zettel directory and its parent directory") watcher.Close() return nil, err } log.Warn(). Str("parentDir", absParentDir).Err(errParent). Msg("Parent of Zettel directory cannot be supervised") log.Warn().Str("path", absPath). Msg("Zettelstore might not detect a deletion or movement of the Zettel directory") } else if err != nil { // Not a problem, if container is not available. It might become available later. log.Warn().Err(err).Str("path", absPath).Msg("Zettel directory not available") } fsdn := &fsdirNotifier{ log: log, events: make(chan Event), refresh: make(chan struct{}), done: make(chan struct{}), base: watcher, path: absPath, fetcher: newDirPathFetcher(absPath), parent: absParentDir, } go fsdn.eventLoop() return fsdn, nil } func (fsdn *fsdirNotifier) Events() <-chan Event { return fsdn.events } func (fsdn *fsdirNotifier) Refresh() { fsdn.refresh <- struct{}{} } func (fsdn *fsdirNotifier) eventLoop() { defer fsdn.base.Close() defer close(fsdn.events) defer close(fsdn.refresh) if !listDirElements(fsdn.log, fsdn.fetcher, fsdn.events, fsdn.done) { return } for fsdn.readAndProcessEvent() { } } func (fsdn *fsdirNotifier) readAndProcessEvent() bool { select { case <-fsdn.done: fsdn.traceDone(1) return false default: } select { case <-fsdn.done: fsdn.traceDone(2) return false case <-fsdn.refresh: fsdn.log.Trace().Msg("refresh") listDirElements(fsdn.log, fsdn.fetcher, fsdn.events, fsdn.done) case err, ok := <-fsdn.base.Errors: fsdn.log.Trace().Err(err).Bool("ok", ok).Msg("got errors") if !ok { return false } select { case fsdn.events <- Event{Op: Error, Err: err}: case <-fsdn.done: fsdn.traceDone(3) return false } case ev, ok := <-fsdn.base.Events: fsdn.log.Trace().Str("name", ev.Name).Str("op", ev.Op.String()).Bool("ok", ok).Msg("file event") if !ok { return false } if !fsdn.processEvent(&ev) { return false } } return true } func (fsdn *fsdirNotifier) traceDone(pos int64) { fsdn.log.Trace().Int("i", pos).Msg("done with read and process events") } func (fsdn *fsdirNotifier) processEvent(ev *fsnotify.Event) bool { if strings.HasPrefix(ev.Name, fsdn.path) { if len(ev.Name) == len(fsdn.path) { return fsdn.processDirEvent(ev) } return fsdn.processFileEvent(ev) } fsdn.log.Trace().Str("path", fsdn.path).Str("name", ev.Name).Str("op", ev.Op.String()).Msg("event does not match") return true } func (fsdn *fsdirNotifier) processDirEvent(ev *fsnotify.Event) bool { if ev.Has(fsnotify.Remove) || ev.Has(fsnotify.Rename) { fsdn.log.Debug().Str("name", fsdn.path).Msg("Directory removed") fsdn.base.Remove(fsdn.path) select { case fsdn.events <- Event{Op: Destroy}: case <-fsdn.done: fsdn.log.Trace().Int("i", 1).Msg("done dir event processing") return false } return true } if ev.Has(fsnotify.Create) { err := fsdn.base.Add(fsdn.path) if err != nil { fsdn.log.IfErr(err).Str("name", fsdn.path).Msg("Unable to add directory") select { case fsdn.events <- Event{Op: Error, Err: err}: case <-fsdn.done: fsdn.log.Trace().Int("i", 2).Msg("done dir event processing") return false } } fsdn.log.Debug().Str("name", fsdn.path).Msg("Directory added") return listDirElements(fsdn.log, fsdn.fetcher, fsdn.events, fsdn.done) } fsdn.log.Trace().Str("name", ev.Name).Str("op", ev.Op.String()).Msg("Directory processed") return true } func (fsdn *fsdirNotifier) processFileEvent(ev *fsnotify.Event) bool { if ev.Has(fsnotify.Create) || ev.Has(fsnotify.Write) { if fi, err := os.Lstat(ev.Name); err != nil || !fi.Mode().IsRegular() { regular := err == nil && fi.Mode().IsRegular() fsdn.log.Trace().Str("name", ev.Name).Str("op", ev.Op.String()).Err(err).Bool("regular", regular).Msg("error with file") return true } fsdn.log.Trace().Str("name", ev.Name).Str("op", ev.Op.String()).Msg("File updated") return fsdn.sendEvent(Update, filepath.Base(ev.Name)) } if ev.Has(fsnotify.Rename) { fi, err := os.Lstat(ev.Name) if err != nil { fsdn.log.Trace().Str("name", ev.Name).Str("op", ev.Op.String()).Msg("File deleted") return fsdn.sendEvent(Delete, filepath.Base(ev.Name)) } if fi.Mode().IsRegular() { fsdn.log.Trace().Str("name", ev.Name).Str("op", ev.Op.String()).Msg("File updated") return fsdn.sendEvent(Update, filepath.Base(ev.Name)) } fsdn.log.Trace().Str("name", ev.Name).Msg("File not regular") return true } if ev.Has(fsnotify.Remove) { fsdn.log.Trace().Str("name", ev.Name).Str("op", ev.Op.String()).Msg("File deleted") return fsdn.sendEvent(Delete, filepath.Base(ev.Name)) } fsdn.log.Trace().Str("name", ev.Name).Str("op", ev.Op.String()).Msg("File processed") return true } func (fsdn *fsdirNotifier) sendEvent(op EventOp, filename string) bool { select { case fsdn.events <- Event{Op: op, Name: filename}: case <-fsdn.done: fsdn.log.Trace().Msg("done file event processing") return false } return true } func (fsdn *fsdirNotifier) Close() { close(fsdn.done) } |
Added box/notify/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 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 | //----------------------------------------------------------------------------- // Copyright (c) 2021-present Detlef Stern // // This file is part of Zettelstore. // // Zettelstore is licensed under the latest version of the EUPL (European Union // Public License). Please see file LICENSE.txt for your rights and obligations // under this license. //----------------------------------------------------------------------------- package notify import ( "archive/zip" "os" "zettelstore.de/z/logger" ) // MakeMetaFilename builds the name of the file containing metadata. func MakeMetaFilename(basename string) string { return basename //+ ".meta" } // EntryFetcher return a list of (file) names of an directory. type EntryFetcher interface { Fetch() ([]string, error) } type dirPathFetcher struct { dirPath string } func newDirPathFetcher(dirPath string) EntryFetcher { return &dirPathFetcher{dirPath} } func (dpf *dirPathFetcher) Fetch() ([]string, error) { entries, err := os.ReadDir(dpf.dirPath) if err != nil { return nil, err } result := make([]string, 0, len(entries)) for _, entry := range entries { if info, err1 := entry.Info(); err1 != nil || !info.Mode().IsRegular() { continue } result = append(result, entry.Name()) } return result, nil } type zipPathFetcher struct { zipPath string } func newZipPathFetcher(zipPath string) EntryFetcher { return &zipPathFetcher{zipPath} } func (zpf *zipPathFetcher) Fetch() ([]string, error) { reader, err := zip.OpenReader(zpf.zipPath) if err != nil { return nil, err } defer reader.Close() result := make([]string, 0, len(reader.File)) for _, f := range reader.File { result = append(result, f.Name) } return result, nil } // listDirElements write all files within the directory path as events. func listDirElements(log *logger.Logger, fetcher EntryFetcher, events chan<- Event, done <-chan struct{}) bool { select { case events <- Event{Op: Make}: case <-done: return false } entries, err := fetcher.Fetch() if err != nil { select { case events <- Event{Op: Error, Err: err}: case <-done: return false } } for _, name := range entries { log.Trace().Str("name", name).Msg("File listed") select { case events <- Event{Op: List, Name: name}: case <-done: return false } } select { case events <- Event{Op: List}: case <-done: return false } return true } |
Added box/notify/notify.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-present Detlef Stern // // This file is part of Zettelstore. // // Zettelstore is licensed under the latest version of the EUPL (European Union // Public License). Please see file LICENSE.txt for your rights and obligations // under this license. //----------------------------------------------------------------------------- // Package notify provides some notification services to be used by box services. package notify import "fmt" // Notifier send events about their container and content. type Notifier interface { // Return the channel Events() <-chan Event // Signal a refresh of the container. This will result in some events. Refresh() // Close the notifier (and eventually the channel) Close() } // EventOp describe a notification operation. type EventOp uint8 // Valid constants for event operations. // // Error signals a detected error. Details are in Event.Err. // // Make signals that the container is detected. List events will follow. // // List signals a found file, if Event.Name is not empty. Otherwise it signals // the end of files within the container. // // Destroy signals that the container is not there any more. It might me Make later again. // // Update signals that file Event.Name was created/updated. // File name is relative to the container. // // Delete signals that file Event.Name was removed. // File name is relative to the container's name. const ( _ EventOp = iota Error // Error while operating Make // Make container List // List container Destroy // Destroy container Update // Update element Delete // Delete element ) // String representation of operation code. func (c EventOp) String() string { switch c { case Error: return "ERROR" case Make: return "MAKE" case List: return "LIST" case Destroy: return "DESTROY" case Update: return "UPDATE" case Delete: return "DELETE" default: return fmt.Sprintf("UNKNOWN(%d)", c) } } // Event represents a single container / element event. type Event struct { Op EventOp Name string Err error // Valid iff Op == Error } |
Added box/notify/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 | //----------------------------------------------------------------------------- // Copyright (c) 2021-present Detlef Stern // // This file is part of Zettelstore. // // Zettelstore is licensed under the latest version of the EUPL (European Union // Public License). Please see file LICENSE.txt for your rights and obligations // under this license. //----------------------------------------------------------------------------- package notify import ( "path/filepath" "zettelstore.de/z/logger" ) type simpleDirNotifier struct { log *logger.Logger events chan Event done chan struct{} refresh chan struct{} fetcher EntryFetcher } // NewSimpleDirNotifier creates a directory based notifier that will not receive // any notifications from the operating system. func NewSimpleDirNotifier(log *logger.Logger, path string) (Notifier, error) { absPath, err := filepath.Abs(path) if err != nil { return nil, err } sdn := &simpleDirNotifier{ log: log, events: make(chan Event), done: make(chan struct{}), refresh: make(chan struct{}), fetcher: newDirPathFetcher(absPath), } go sdn.eventLoop() return sdn, nil } // NewSimpleZipNotifier creates a zip-file based notifier that will not receive // any notifications from the operating system. func NewSimpleZipNotifier(log *logger.Logger, zipPath string) Notifier { sdn := &simpleDirNotifier{ log: log, events: make(chan Event), done: make(chan struct{}), refresh: make(chan struct{}), fetcher: newZipPathFetcher(zipPath), } go sdn.eventLoop() return sdn } func (sdn *simpleDirNotifier) Events() <-chan Event { return sdn.events } func (sdn *simpleDirNotifier) Refresh() { sdn.refresh <- struct{}{} } func (sdn *simpleDirNotifier) eventLoop() { defer close(sdn.events) defer close(sdn.refresh) if !listDirElements(sdn.log, sdn.fetcher, sdn.events, sdn.done) { return } for { select { case <-sdn.done: return case <-sdn.refresh: listDirElements(sdn.log, sdn.fetcher, sdn.events, sdn.done) } } } func (sdn *simpleDirNotifier) Close() { close(sdn.done) } |
Changes to cmd/cmd_file.go.
1 2 3 4 5 6 7 8 | //----------------------------------------------------------------------------- // Copyright (c) 2020-present Detlef Stern // // This file is part of Zettelstore. // // Zettelstore is licensed under the latest version of the EUPL (European Union // Public License). Please see file LICENSE.txt for your rights and obligations // under this license. | < < < | | | < | | | | | | | | < < | | 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 | //----------------------------------------------------------------------------- // Copyright (c) 2020-present Detlef Stern // // This file is part of Zettelstore. // // Zettelstore is licensed under the latest version of the EUPL (European Union // Public License). Please see file LICENSE.txt for your rights and obligations // under this license. //----------------------------------------------------------------------------- package cmd import ( "context" "flag" "fmt" "io" "os" "zettelstore.de/c/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" ) // ---------- Subcommand: file ----------------------------------------------- func cmdFile(fs *flag.FlagSet) (int, error) { enc := fs.Lookup("t").Value.String() m, inp, err := getInput(fs.Args()) if m == nil { return 2, err } z := parser.ParseZettel( context.Background(), domain.Zettel{ Meta: m, Content: domain.NewContent(inp.Src[inp.Pos:]), }, m.GetDefault(api.KeySyntax, meta.SyntaxZmk), nil, ) encdr := encoder.Create(api.Encoder(enc)) if encdr == nil { fmt.Fprintf(os.Stderr, "Unknown format %q\n", enc) return 2, nil } _, err = encdr.WriteZettel(os.Stdout, z, parser.ParseMetadata) if err != nil { return 2, err } fmt.Println() return 0, nil } |
︙ | ︙ |
Changes to cmd/cmd_password.go.
1 2 3 4 5 6 7 8 | //----------------------------------------------------------------------------- // Copyright (c) 2020-present Detlef Stern // // This file is part of Zettelstore. // // Zettelstore is licensed under the latest version of the EUPL (European Union // Public License). Please see file LICENSE.txt for your rights and obligations // under this license. | < < < | < | | | 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 | //----------------------------------------------------------------------------- // Copyright (c) 2020-present Detlef Stern // // This file is part of Zettelstore. // // Zettelstore is licensed under the latest version of the EUPL (European Union // Public License). Please see file LICENSE.txt for your rights and obligations // under this license. //----------------------------------------------------------------------------- package cmd import ( "flag" "fmt" "os" "golang.org/x/term" "zettelstore.de/c/api" "zettelstore.de/z/auth/cred" "zettelstore.de/z/domain/id" ) // ---------- Subcommand: password ------------------------------------------- func cmdPassword(fs *flag.FlagSet) (int, error) { if fs.NArg() == 0 { fmt.Fprintln(os.Stderr, "User name and user zettel identification missing") |
︙ | ︙ | |||
60 61 62 63 64 65 66 | ident := fs.Arg(0) hashedPassword, err := cred.HashCredential(zid, ident, password) if err != nil { return 2, err } fmt.Printf("%v: %s\n%v: %s\n", | | | | 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 | ident := fs.Arg(0) hashedPassword, err := cred.HashCredential(zid, ident, password) if err != nil { return 2, err } fmt.Printf("%v: %s\n%v: %s\n", api.KeyCredential, hashedPassword, api.KeyUserID, ident, ) return 0, nil } func getPassword(prompt string) (string, error) { fmt.Fprintf(os.Stderr, "%s: ", prompt) password, err := term.ReadPassword(int(os.Stdin.Fd())) fmt.Fprintln(os.Stderr) return string(password), err } |
Changes to cmd/cmd_run.go.
1 2 3 4 5 6 7 8 | //----------------------------------------------------------------------------- // Copyright (c) 2020-present Detlef Stern // // This file is part of Zettelstore. // // Zettelstore is licensed under the latest version of the EUPL (European Union // Public License). Please see file LICENSE.txt for your rights and obligations // under this license. | < < < < | | | | | | | | | | 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 | //----------------------------------------------------------------------------- // Copyright (c) 2020-present Detlef Stern // // This file is part of Zettelstore. // // Zettelstore is licensed under the latest version of the EUPL (European Union // Public License). Please see file LICENSE.txt for your rights and obligations // under this license. //----------------------------------------------------------------------------- package cmd import ( "context" "flag" "net/http" "zettelstore.de/z/auth" "zettelstore.de/z/box" "zettelstore.de/z/config" "zettelstore.de/z/domain/meta" "zettelstore.de/z/kernel" "zettelstore.de/z/usecase" "zettelstore.de/z/web/adapter/api" "zettelstore.de/z/web/adapter/webui" "zettelstore.de/z/web/server" ) // ---------- Subcommand: run ------------------------------------------------ func flgRun(fs *flag.FlagSet) { fs.String("c", "", "configuration file") fs.Uint("a", 0, "port number kernel service (0=disable)") |
︙ | ︙ | |||
56 57 58 59 60 61 62 | protectedBoxManager, authPolicy := authManager.BoxWithPolicy(boxManager, rtConfig) kern := kernel.Main webLog := kern.GetLogger(kernel.WebService) var getUser getUserImpl logAuth := kern.GetLogger(kernel.AuthService) logUc := kern.GetLogger(kernel.CoreService).WithUser(&getUser) | < | | > | | < < < > > > < > > | | | | | > > > > > > | | > | 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 | protectedBoxManager, authPolicy := authManager.BoxWithPolicy(boxManager, rtConfig) kern := kernel.Main webLog := kern.GetLogger(kernel.WebService) var getUser getUserImpl logAuth := kern.GetLogger(kernel.AuthService) logUc := kern.GetLogger(kernel.CoreService).WithUser(&getUser) ucAuthenticate := usecase.NewAuthenticate(logAuth, authManager, authManager, boxManager) ucIsAuth := usecase.NewIsAuthenticated(logUc, &getUser, authManager) ucCreateZettel := usecase.NewCreateZettel(logUc, rtConfig, protectedBoxManager) ucGetMeta := usecase.NewGetMeta(protectedBoxManager) ucGetAllMeta := usecase.NewGetAllMeta(protectedBoxManager) ucGetZettel := usecase.NewGetZettel(protectedBoxManager) ucParseZettel := usecase.NewParseZettel(rtConfig, ucGetZettel) ucListMeta := usecase.NewListMeta(protectedBoxManager) ucEvaluate := usecase.NewEvaluate(rtConfig, ucGetZettel, ucGetMeta, ucListMeta) ucListSyntax := usecase.NewListSyntax(protectedBoxManager) ucListRoles := usecase.NewListRoles(protectedBoxManager) ucZettelContext := usecase.NewZettelContext(protectedBoxManager, rtConfig) ucDelete := usecase.NewDeleteZettel(logUc, protectedBoxManager) ucUpdate := usecase.NewUpdateZettel(logUc, protectedBoxManager) ucRename := usecase.NewRenameZettel(logUc, protectedBoxManager) ucUnlinkedRefs := usecase.NewUnlinkedReferences(protectedBoxManager, rtConfig) ucRefresh := usecase.NewRefresh(logUc, protectedBoxManager) ucVersion := usecase.NewVersion(kernel.Main.GetConfig(kernel.CoreService, kernel.CoreVersion).(string)) a := api.New( webLog.Clone().Str("adapter", "api").Child(), webSrv, authManager, authManager, rtConfig, authPolicy) wui := webui.New( webLog.Clone().Str("adapter", "wui").Child(), webSrv, authManager, rtConfig, authManager, boxManager, authPolicy, &ucEvaluate) webSrv.Handle("/", wui.MakeGetRootHandler(protectedBoxManager)) if assetDir := kern.GetConfig(kernel.WebService, kernel.WebAssetDir).(string); assetDir != "" { const assetPrefix = "/assets/" webSrv.Handle(assetPrefix, http.StripPrefix(assetPrefix, http.FileServer(http.Dir(assetDir)))) webSrv.Handle("/favicon.ico", wui.MakeFaviconHandler(assetDir)) } // Web user interface if !authManager.IsReadonly() { webSrv.AddZettelRoute('b', server.MethodGet, wui.MakeGetRenameZettelHandler(ucGetMeta)) webSrv.AddZettelRoute('b', server.MethodPost, wui.MakePostRenameZettelHandler(&ucRename)) webSrv.AddListRoute('c', server.MethodGet, wui.MakeGetZettelFromListHandler(ucListMeta, &ucEvaluate, ucListRoles, ucListSyntax)) webSrv.AddListRoute('c', server.MethodPost, wui.MakePostCreateZettelHandler(&ucCreateZettel)) webSrv.AddZettelRoute('c', server.MethodGet, wui.MakeGetCreateZettelHandler( ucGetZettel, &ucCreateZettel, ucListRoles, ucListSyntax)) webSrv.AddZettelRoute('c', server.MethodPost, wui.MakePostCreateZettelHandler(&ucCreateZettel)) webSrv.AddZettelRoute('d', server.MethodGet, wui.MakeGetDeleteZettelHandler(ucGetMeta, ucGetAllMeta)) webSrv.AddZettelRoute('d', server.MethodPost, wui.MakePostDeleteZettelHandler(&ucDelete)) webSrv.AddZettelRoute('e', server.MethodGet, wui.MakeEditGetZettelHandler(ucGetZettel, ucListRoles, ucListSyntax)) webSrv.AddZettelRoute('e', server.MethodPost, wui.MakeEditSetZettelHandler(&ucUpdate)) } webSrv.AddListRoute('g', server.MethodGet, wui.MakeGetGoActionHandler(&ucRefresh)) webSrv.AddListRoute('h', server.MethodGet, wui.MakeListHTMLMetaHandler(ucListMeta, &ucEvaluate)) webSrv.AddZettelRoute('h', server.MethodGet, wui.MakeGetHTMLZettelHandler(&ucEvaluate, ucGetMeta)) webSrv.AddListRoute('i', server.MethodGet, wui.MakeGetLoginOutHandler()) webSrv.AddListRoute('i', server.MethodPost, wui.MakePostLoginHandler(&ucAuthenticate)) webSrv.AddZettelRoute('i', server.MethodGet, wui.MakeGetInfoHandler( ucParseZettel, &ucEvaluate, ucGetMeta, ucGetAllMeta, ucUnlinkedRefs)) webSrv.AddZettelRoute('k', server.MethodGet, wui.MakeZettelContextHandler( ucZettelContext, &ucEvaluate)) // API webSrv.AddListRoute('a', server.MethodPost, a.MakePostLoginHandler(&ucAuthenticate)) webSrv.AddListRoute('a', server.MethodPut, a.MakeRenewAuthHandler()) webSrv.AddZettelRoute('o', server.MethodGet, a.MakeGetOrderHandler( usecase.NewZettelOrder(protectedBoxManager, ucEvaluate))) webSrv.AddZettelRoute('u', server.MethodGet, a.MakeListUnlinkedMetaHandler(ucGetMeta, ucUnlinkedRefs)) webSrv.AddListRoute('x', server.MethodGet, a.MakeGetDataHandler(ucVersion)) webSrv.AddListRoute('x', server.MethodPost, a.MakePostCommandHandler(&ucIsAuth, &ucRefresh)) webSrv.AddZettelRoute('x', server.MethodGet, a.MakeZettelContextHandler(ucZettelContext)) webSrv.AddListRoute('z', server.MethodGet, a.MakeQueryHandler(ucListMeta)) webSrv.AddZettelRoute('z', server.MethodGet, a.MakeGetZettelHandler(ucGetMeta, ucGetZettel, ucParseZettel, ucEvaluate)) if !authManager.IsReadonly() { webSrv.AddListRoute('z', server.MethodPost, a.MakePostCreateZettelHandler(&ucCreateZettel)) webSrv.AddZettelRoute('z', server.MethodPut, a.MakeUpdateZettelHandler(&ucUpdate)) webSrv.AddZettelRoute('z', server.MethodDelete, a.MakeDeleteZettelHandler(&ucDelete)) webSrv.AddZettelRoute('z', server.MethodMove, a.MakeRenameZettelHandler(&ucRename)) } if authManager.WithAuth() { webSrv.SetUserRetriever(usecase.NewGetUserByZid(boxManager)) } } type getUserImpl struct{} func (*getUserImpl) GetUser(ctx context.Context) *meta.Meta { return server.GetUser(ctx) } |
Changes to cmd/command.go.
1 2 3 4 5 6 7 8 | //----------------------------------------------------------------------------- // Copyright (c) 2020-present Detlef Stern // // This file is part of Zettelstore. // // Zettelstore is licensed under the latest version of the EUPL (European Union // Public License). Please see file LICENSE.txt for your rights and obligations // under this license. | < < < < < > | | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 | //----------------------------------------------------------------------------- // Copyright (c) 2020-present Detlef Stern // // This file is part of Zettelstore. // // Zettelstore is licensed under the latest version of the EUPL (European Union // Public License). Please see file LICENSE.txt for your rights and obligations // under this license. //----------------------------------------------------------------------------- package cmd import ( "flag" "zettelstore.de/c/maps" "zettelstore.de/z/logger" ) // Command stores information about commands / sub-commands. type Command struct { Name string // command name as it appears on the command line Func CommandFunc // function that executes a command Simple bool // Operate in simple-mode |
︙ | ︙ | |||
63 64 65 66 67 68 69 | // Get returns the command identified by the given name and a bool to signal success. func Get(name string) (Command, bool) { cmd, ok := commands[name] return cmd, ok } // List returns a sorted list of all registered command names. | | | 59 60 61 62 63 64 65 66 | // Get returns the command identified by the given name and a bool to signal success. func Get(name string) (Command, bool) { cmd, ok := commands[name] return cmd, ok } // List returns a sorted list of all registered command names. func List() []string { return maps.Keys(commands) } |
Changes to cmd/main.go.
1 2 3 4 5 6 7 8 | //----------------------------------------------------------------------------- // Copyright (c) 2020-present Detlef Stern // // This file is part of Zettelstore. // // Zettelstore is licensed under the latest version of the EUPL (European Union // Public License). Please see file LICENSE.txt for your rights and obligations // under this license. | < < < < | | < | | | | | | | | | | | | 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 | //----------------------------------------------------------------------------- // Copyright (c) 2020-present Detlef Stern // // This file is part of Zettelstore. // // Zettelstore is licensed under the latest version of the EUPL (European Union // Public License). Please see file LICENSE.txt for your rights and obligations // under this license. //----------------------------------------------------------------------------- package cmd import ( "crypto/sha256" "flag" "fmt" "net" "net/url" "os" "runtime/debug" "strconv" "strings" "time" "zettelstore.de/c/api" "zettelstore.de/z/auth" "zettelstore.de/z/auth/impl" "zettelstore.de/z/box" "zettelstore.de/z/box/compbox" "zettelstore.de/z/box/manager" "zettelstore.de/z/config" "zettelstore.de/z/domain/id" "zettelstore.de/z/domain/meta" "zettelstore.de/z/input" "zettelstore.de/z/kernel" "zettelstore.de/z/logger" "zettelstore.de/z/web/server" ) const strRunSimple = "run-simple" func init() { RegisterCommand(Command{ Name: "help", |
︙ | ︙ | |||
89 90 91 92 93 94 95 | }) RegisterCommand(Command{ Name: "password", Func: cmdPassword, }) } | | | | | | | | | | | | | | | | | | | | | < > < | | 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 | }) RegisterCommand(Command{ Name: "password", Func: cmdPassword, }) } func fetchStartupConfiguration(fs *flag.FlagSet) (cfg *meta.Meta) { if configFlag := fs.Lookup("c"); configFlag != nil { if filename := configFlag.Value.String(); filename != "" { content, err := readConfiguration(filename) return createConfiguration(content, err) } } content, err := searchAndReadConfiguration() return createConfiguration(content, err) } func createConfiguration(content []byte, err error) *meta.Meta { if err != nil { return meta.New(id.Invalid) } return meta.NewFromInput(id.Invalid, input.NewInput(content)) } func readConfiguration(filename string) ([]byte, error) { return os.ReadFile(filename) } func searchAndReadConfiguration() ([]byte, error) { for _, filename := range []string{"zettelstore.cfg", "zsconfig.txt", "zscfg.txt", "_zscfg"} { if content, err := readConfiguration(filename); err == nil { return content, nil } } return readConfiguration(".zscfg") } func getConfig(fs *flag.FlagSet) *meta.Meta { cfg := fetchStartupConfiguration(fs) fs.Visit(func(flg *flag.Flag) { switch flg.Name { case "p": cfg.Set(keyListenAddr, net.JoinHostPort("127.0.0.1", flg.Value.String())) case "a": cfg.Set(keyAdminPort, flg.Value.String()) case "d": val := flg.Value.String() if strings.HasPrefix(val, "/") { val = "dir://" + val } else { val = "dir:" + val } deleteConfiguredBoxes(cfg) cfg.Set(keyBoxOneURI, val) case "l": cfg.Set(keyLogLevel, flg.Value.String()) case "debug": cfg.Set(keyDebug, flg.Value.String()) case "r": cfg.Set(keyReadOnly, flg.Value.String()) case "v": cfg.Set(keyVerbose, flg.Value.String()) } }) return cfg } func deleteConfiguredBoxes(cfg *meta.Meta) { for _, p := range cfg.PairsRest() { if key := p.Key; strings.HasPrefix(key, kernel.BoxURIs) { cfg.Delete(key) } } } const ( keyAdminPort = "admin-port" keyAssetDir = "asset-dir" keyBaseURL = "base-url" keyDebug = "debug-mode" keyDefaultDirBoxType = "default-dir-box-type" keyInsecureCookie = "insecure-cookie" keyInsecureHTML = "insecure-html" keyListenAddr = "listen-addr" keyLogLevel = "log-level" keyMaxRequestSize = "max-request-size" keyOwner = "owner" keyPersistentCookie = "persistent-cookie" keyBoxOneURI = kernel.BoxURIs + "1" keyReadOnly = "read-only-mode" keyTokenLifetimeHTML = "token-lifetime-html" keyTokenLifetimeAPI = "token-lifetime-api" keyURLPrefix = "url-prefix" keyVerbose = "verbose-mode" ) func setServiceConfig(cfg *meta.Meta) bool { debugMode := cfg.GetBool(keyDebug) if debugMode && kernel.Main.GetKernelLogger().Level() > logger.DebugLevel { kernel.Main.SetLogLevel(logger.DebugLevel.String()) } if logLevel, found := cfg.Get(keyLogLevel); found { kernel.Main.SetLogLevel(logLevel) } err := setConfigValue(nil, kernel.CoreService, kernel.CoreDebug, debugMode) err = setConfigValue(err, kernel.CoreService, kernel.CoreVerbose, cfg.GetBool(keyVerbose)) if val, found := cfg.Get(keyAdminPort); found { err = setConfigValue(err, kernel.CoreService, kernel.CorePort, val) } |
︙ | ︙ | |||
208 209 210 211 212 213 214 | val, found := cfg.Get(key) if !found { break } err = setConfigValue(err, kernel.BoxService, key, val) } | < | < | < | | | 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 | val, found := cfg.Get(key) if !found { break } err = setConfigValue(err, kernel.BoxService, key, val) } err = setConfigValue(err, kernel.ConfigService, kernel.ConfigInsecureHTML, cfg.GetDefault(keyInsecureHTML, kernel.ConfigSecureHTML)) err = setConfigValue(err, kernel.WebService, kernel.WebListenAddress, cfg.GetDefault(keyListenAddr, "127.0.0.1:23123")) if val, found := cfg.Get(keyBaseURL); found { err = setConfigValue(err, kernel.WebService, kernel.WebBaseURL, val) } if val, found := cfg.Get(keyURLPrefix); found { err = setConfigValue(err, kernel.WebService, kernel.WebURLPrefix, val) } err = setConfigValue(err, kernel.WebService, kernel.WebSecureCookie, !cfg.GetBool(keyInsecureCookie)) err = setConfigValue(err, kernel.WebService, kernel.WebPersistentCookie, cfg.GetBool(keyPersistentCookie)) if val, found := cfg.Get(keyMaxRequestSize); found { err = setConfigValue(err, kernel.WebService, kernel.WebMaxRequestSize, val) } err = setConfigValue( err, kernel.WebService, kernel.WebTokenLifetimeAPI, cfg.GetDefault(keyTokenLifetimeAPI, "")) err = setConfigValue( err, kernel.WebService, kernel.WebTokenLifetimeHTML, cfg.GetDefault(keyTokenLifetimeHTML, "")) if val, found := cfg.Get(keyAssetDir); found { err = setConfigValue(err, kernel.WebService, kernel.WebAssetDir, val) } return err == nil } func setConfigValue(err error, subsys kernel.Service, key string, val any) error { if err == nil { err = kernel.Main.SetConfig(subsys, key, fmt.Sprint(val)) if err != nil { kernel.Main.GetKernelLogger().Fatal().Str("key", key).Str("value", fmt.Sprint(val)).Err(err).Msg("Unable to set configuration") } } return err } func executeCommand(name string, args ...string) int { command, ok := Get(name) if !ok { fmt.Fprintf(os.Stderr, "Unknown command %q\n", name) return 1 } fs := command.GetFlags() if err := fs.Parse(args); err != nil { fmt.Fprintf(os.Stderr, "%s: unable to parse flags: %v %v\n", name, args, err) return 1 } cfg := getConfig(fs) if !setServiceConfig(cfg) { fs.Usage() return 2 } kern := kernel.Main var createManager kernel.CreateBoxManagerFunc |
︙ | ︙ | |||
279 280 281 282 283 284 285 | secret := cfg.GetDefault("secret", "") if len(secret) < 16 && cfg.GetDefault(keyOwner, "") != "" { fmt.Fprintf(os.Stderr, "secret must have at least length 16 when authentication is enabled, but is %q\n", secret) return 2 } cfg.Delete("secret") | | | | | | 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 | secret := cfg.GetDefault("secret", "") if len(secret) < 16 && cfg.GetDefault(keyOwner, "") != "" { fmt.Fprintf(os.Stderr, "secret must have at least length 16 when authentication is enabled, but is %q\n", secret) return 2 } cfg.Delete("secret") secret = fmt.Sprintf("%x", sha256.Sum256([]byte(secret))) kern.SetCreators( func(readonly bool, owner id.Zid) (auth.Manager, error) { return impl.New(readonly, owner, secret), nil }, createManager, func(srv server.Server, plMgr box.Manager, authMgr auth.Manager, rtConfig config.Config) error { setupRouting(srv, plMgr, authMgr, rtConfig) return nil }, ) if command.Simple { kern.SetConfig(kernel.ConfigService, kernel.ConfigSimpleMode, "true") } kern.Start(command.Header, command.LineServer) exitCode, err := command.Func(fs) if err != nil { fmt.Fprintf(os.Stderr, "%s: %v\n", name, err) } kern.Shutdown(true) return exitCode } // runSimple is called, when the user just starts the software via a double click // or via a simple call “./zettelstore“ on the command line. func runSimple() int { if _, err := searchAndReadConfiguration(); err == nil { return executeCommand(strRunSimple) } dir := "./zettel" if err := os.MkdirAll(dir, 0750); err != nil { fmt.Fprintf(os.Stderr, "Unable to create zettel directory %q (%s)\n", dir, err) return 1 } |
︙ | ︙ |
Changes to cmd/register.go.
1 2 3 4 5 6 7 8 | //----------------------------------------------------------------------------- // Copyright (c) 2020-present Detlef Stern // // This file is part of Zettelstore. // // Zettelstore is licensed under the latest version of the EUPL (European Union // Public License). Please see file LICENSE.txt for your rights and obligations // under this license. | < < < > | | | | | | | | > | < | | | | | | | | | | 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 | //----------------------------------------------------------------------------- // Copyright (c) 2020-present Detlef Stern // // This file is part of Zettelstore. // // Zettelstore is licensed under the latest version of the EUPL (European Union // Public License). Please see file LICENSE.txt for your rights and obligations // under this license. //----------------------------------------------------------------------------- // Package cmd provides command generic functions. package cmd // Mention all needed encoders, parsers and stores to have them registered. import ( _ "zettelstore.de/z/box/compbox" // Allow to use computed box. _ "zettelstore.de/z/box/constbox" // Allow to use global internal box. _ "zettelstore.de/z/box/dirbox" // Allow to use directory box. _ "zettelstore.de/z/box/filebox" // Allow to use file box. _ "zettelstore.de/z/box/membox" // Allow to use in-memory box. _ "zettelstore.de/z/encoder/htmlenc" // Allow to use HTML encoder. _ "zettelstore.de/z/encoder/mdenc" // Allow to use markdown encoder. _ "zettelstore.de/z/encoder/sexprenc" // Allow to use sexpr encoder. _ "zettelstore.de/z/encoder/shtmlenc" // Allow to use SHTML encoder. _ "zettelstore.de/z/encoder/textenc" // Allow to use text encoder. _ "zettelstore.de/z/encoder/zmkenc" // Allow to use zmk encoder. _ "zettelstore.de/z/kernel/impl" // Allow kernel implementation to create itself _ "zettelstore.de/z/parser/blob" // Allow to use BLOB parser. _ "zettelstore.de/z/parser/draw" // Allow to use draw parser. _ "zettelstore.de/z/parser/markdown" // Allow to use markdown parser. _ "zettelstore.de/z/parser/none" // Allow to use none parser. _ "zettelstore.de/z/parser/plain" // Allow to use plain parser. _ "zettelstore.de/z/parser/zettelmark" // Allow to use zettelmark parser. ) |
Changes to cmd/zettelstore/main.go.
1 2 3 4 5 6 7 8 | //----------------------------------------------------------------------------- // Copyright (c) 2020-present Detlef Stern // // This file is part of Zettelstore. // // Zettelstore is licensed under the latest version of the EUPL (European Union // Public License). Please see file LICENSE.txt for your rights and obligations // under this license. | < < < | | 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 | //----------------------------------------------------------------------------- // Copyright (c) 2020-present Detlef Stern // // This file is part of Zettelstore. // // Zettelstore is licensed under the latest version of the EUPL (European Union // Public License). Please see file LICENSE.txt for your rights and obligations // under this license. //----------------------------------------------------------------------------- // Package main is the starting point for the zettelstore command. package main import ( "os" "zettelstore.de/z/cmd" ) // Version variable. Will be filled by build process. var version string = "" func main() { exitCode := cmd.Main("Zettelstore", version) os.Exit(exitCode) } |
Added collect/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 | //----------------------------------------------------------------------------- // Copyright (c) 2020-present Detlef Stern // // This file is part of Zettelstore. // // Zettelstore is licensed under the latest version of the EUPL (European Union // Public License). Please see file LICENSE.txt for your rights and obligations // under this license. //----------------------------------------------------------------------------- // Package collect provides functions to collect items from a syntax tree. package collect import "zettelstore.de/z/ast" // Summary stores the relevant parts of the syntax tree type Summary struct { Links []*ast.Reference // list of all linked material Embeds []*ast.Reference // list of all embedded material Cites []*ast.CiteNode // list of all referenced citations } // References returns all references mentioned in the given zettel. This also // includes references to images. func References(zn *ast.ZettelNode) (s Summary) { ast.Walk(&s, &zn.Ast) return s } // Visit all node to collect data for the summary. func (s *Summary) Visit(node ast.Node) ast.Visitor { switch n := node.(type) { case *ast.TranscludeNode: s.Embeds = append(s.Embeds, n.Ref) case *ast.LinkNode: s.Links = append(s.Links, n.Ref) case *ast.EmbedRefNode: s.Embeds = append(s.Embeds, n.Ref) case *ast.CiteNode: s.Cites = append(s.Cites, n) } return s } |
Added collect/collect_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 | //----------------------------------------------------------------------------- // Copyright (c) 2020-present Detlef Stern // // This file is part of Zettelstore. // // Zettelstore is licensed under the latest version of the EUPL (European Union // Public License). Please see file LICENSE.txt for your rights and obligations // under this license. //----------------------------------------------------------------------------- // Package collect_test provides some unit test for collectors. package collect_test import ( "testing" "zettelstore.de/z/ast" "zettelstore.de/z/collect" ) func parseRef(s string) *ast.Reference { r := ast.ParseReference(s) 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.Embeds != nil { t.Error("No links/images expected, but got:", summary.Links, "and", summary.Embeds) } intNode := &ast.LinkNode{Ref: parseRef("01234567890123")} para := ast.CreateParaNode(intNode, &ast.LinkNode{Ref: parseRef("https://zettelstore.de/z")}) zn.Ast = ast.BlockSlice{para} summary = collect.References(zn) if summary.Links == nil || summary.Embeds != nil { t.Error("Links expected, and no images, but got:", summary.Links, "and", summary.Embeds) } para.Inlines = append(para.Inlines, intNode) summary = collect.References(zn) if cnt := len(summary.Links); cnt != 3 { t.Error("Link count does not work. Expected: 3, got", summary.Links) } } func TestEmbed(t *testing.T) { t.Parallel() zn := &ast.ZettelNode{ Ast: ast.BlockSlice{ast.CreateParaNode(&ast.EmbedRefNode{Ref: parseRef("12345678901234")})}, } summary := collect.References(zn) if summary.Embeds == nil { t.Error("Only image expected, but got: ", summary.Embeds) } } |
Added collect/order.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 | //----------------------------------------------------------------------------- // Copyright (c) 2021-present Detlef Stern // // This file is part of Zettelstore. // // Zettelstore is licensed under the latest version of the EUPL (European Union // Public License). Please see file LICENSE.txt for your rights and obligations // under this license. //----------------------------------------------------------------------------- // Package collect provides functions to collect items from a syntax tree. package collect import "zettelstore.de/z/ast" // Order of internal reference within the given zettel. func Order(zn *ast.ZettelNode) (result []*ast.Reference) { for _, bn := range zn.Ast { ln, ok := bn.(*ast.NestedListNode) if !ok { continue } switch ln.Kind { case ast.NestedListOrdered, ast.NestedListUnordered: for _, is := range ln.Items { if ref := firstItemZettelReference(is); ref != nil { result = append(result, ref) } } } } return result } func firstItemZettelReference(is ast.ItemSlice) *ast.Reference { for _, in := range is { if pn, ok := in.(*ast.ParaNode); ok { if ref := firstInlineZettelReference(pn.Inlines); ref != nil { return ref } } } return nil } func firstInlineZettelReference(is ast.InlineSlice) (result *ast.Reference) { for _, inl := range is { switch in := inl.(type) { case *ast.LinkNode: if ref := in.Ref; ref.IsZettel() { return ref } result = firstInlineZettelReference(in.Inlines) case *ast.EmbedRefNode: result = firstInlineZettelReference(in.Inlines) case *ast.EmbedBLOBNode: result = firstInlineZettelReference(in.Inlines) case *ast.CiteNode: result = firstInlineZettelReference(in.Inlines) case *ast.FootnoteNode: // Ignore references in footnotes continue case *ast.FormatNode: result = firstInlineZettelReference(in.Inlines) default: continue } if result != nil { return result } } return nil } |
Added collect/split.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 | //----------------------------------------------------------------------------- // Copyright (c) 2020-present Detlef Stern // // This file is part of Zettelstore. // // Zettelstore is licensed under the latest version of the EUPL (European Union // Public License). Please see file LICENSE.txt for your rights and obligations // under this license. //----------------------------------------------------------------------------- // Package collect provides functions to collect items from a syntax tree. package collect import ( "zettelstore.de/z/ast" "zettelstore.de/z/strfun" ) // DivideReferences divides the given list of rederences into zettel, local, and external References. func DivideReferences(all []*ast.Reference) (zettel, local, external []*ast.Reference) { if len(all) == 0 { return nil, nil, nil } mapZettel := make(strfun.Set) mapLocal := make(strfun.Set) mapExternal := make(strfun.Set) for _, ref := range all { if ref.State == ast.RefStateSelf { continue } if ref.IsZettel() { zettel = appendRefToList(zettel, mapZettel, ref) } else if ref.IsExternal() { external = appendRefToList(external, mapExternal, ref) } else { local = appendRefToList(local, mapLocal, ref) } } return zettel, local, external } func appendRefToList(reflist []*ast.Reference, refSet strfun.Set, ref *ast.Reference) []*ast.Reference { s := ref.String() if !refSet.Has(s) { reflist = append(reflist, ref) refSet.Set(s) } return reflist } |
Added config/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 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 | //----------------------------------------------------------------------------- // Copyright (c) 2020-present Detlef Stern // // This file is part of Zettelstore. // // Zettelstore is licensed under the latest version of the EUPL (European Union // Public License). Please see file LICENSE.txt for your rights and obligations // under this license. //----------------------------------------------------------------------------- // Package config provides functions to retrieve runtime configuration data. package config import ( "context" "zettelstore.de/z/domain/meta" ) // Key values that are supported by Config.Get const ( KeyFooterZettel = "footer-zettel" KeyHomeZettel = "home-zettel" // api.KeyLang ) // Config allows to retrieve all defined configuration values that can be changed during runtime. type Config interface { AuthConfig // Get returns the value of the given key. It searches first in the given metadata, // then in the data of the current user, and at last in the system-wide data. Get(ctx context.Context, m *meta.Meta, key string) string // AddDefaultValues enriches the given meta data with its default values. AddDefaultValues(context.Context, *meta.Meta) *meta.Meta // GetSiteName returns the current value of the "site-name" key. GetSiteName() string // GetHTMLInsecurity returns the current GetHTMLInsecurity() HTMLInsecurity // GetMaxTransclusions returns the maximum number of indirect transclusions. GetMaxTransclusions() int // GetYAMLHeader returns the current value of the "yaml-header" key. GetYAMLHeader() bool // GetZettelFileSyntax returns the current value of the "zettel-file-syntax" key. GetZettelFileSyntax() []string } // AuthConfig are relevant configuration values for authentication. type AuthConfig interface { // GetSimpleMode returns true if system tuns in simple-mode. GetSimpleMode() bool // GetExpertMode returns the current value of the "expert-mode" key. GetExpertMode() bool // GetVisibility returns the visibility value of the metadata. GetVisibility(m *meta.Meta) meta.Visibility } // HTMLInsecurity states what kind of insecure HTML is allowed. // The lowest value is the most secure one (disallowing any HTML) type HTMLInsecurity uint8 // Constant values for HTMLInsecurity: const ( NoHTML HTMLInsecurity = iota SyntaxHTML MarkdownHTML ZettelmarkupHTML ) func (hi HTMLInsecurity) String() string { switch hi { case SyntaxHTML: return "html" case MarkdownHTML: return "markdown" case ZettelmarkupHTML: return "zettelmarkup" } return "secure" } // AllowHTML returns true, if the given HTML insecurity level matches the given syntax value. func (hi HTMLInsecurity) AllowHTML(syntax string) bool { switch hi { case SyntaxHTML: return syntax == meta.SyntaxHTML case MarkdownHTML: return syntax == meta.SyntaxHTML || syntax == meta.SyntaxMarkdown || syntax == meta.SyntaxMD case ZettelmarkupHTML: return syntax == meta.SyntaxZmk || syntax == meta.SyntaxHTML || syntax == meta.SyntaxMarkdown || syntax == meta.SyntaxMD } return false } |
Changes to docs/development/00010000000000.zettel.
1 2 3 4 5 | id: 00010000000000 title: Developments Notes role: zettel syntax: zmk created: 00010101000000 | | < | 1 2 3 4 5 6 7 8 9 10 | id: 00010000000000 title: Developments Notes role: zettel syntax: zmk created: 00010101000000 modified: 20221026184905 * [[Required Software|20210916193200]] * [[Fuzzing tests|20221026184300]] * [[Checklist for Release|20210916194900]] |
Changes to docs/development/20210916193200.zettel.
1 2 3 4 5 | id: 20210916193200 title: Required Software role: zettel syntax: zmk created: 20210916193200 | | > > > > > < < < < < < < < < < < | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 | id: 20210916193200 title: Required Software role: zettel syntax: zmk created: 20210916193200 modified: 20230327165135 The following software must be installed: * A current, supported [[release of Go|https://go.dev/doc/devel/release]], * [[shadow|https://pkg.go.dev/golang.org/x/tools/go/analysis/passes/shadow]] via ``go install golang.org/x/tools/go/analysis/passes/shadow/cmd/shadow@latest``, * [[staticcheck|https://staticcheck.io/]] via ``go install honnef.co/go/tools/cmd/staticcheck@latest``, * [[unparam|https://mvdan.cc/unparam]][^[[GitHub|https://github.com/mvdan/unparam]]] via ``go install mvdan.cc/unparam@latest``, * [[govulncheck|https://golang.org/x/vuln/cmd/govulncheck]] via ``go install golang.org/x/vuln/cmd/govulncheck@latest``, * [[Fossil|https://fossil-scm.org/]], * [[Git|https://git-scm.org/]] (most dependencies are accessible via Git only). Make sure that the software is in your path, e.g. via: ```sh export PATH=$PATH:/usr/local/go/bin export PATH=$PATH:$(go env GOPATH)/bin ``` |
Changes to docs/development/20210916194900.zettel.
1 2 3 4 | id: 20210916194900 title: Checklist for Release role: zettel 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 49 50 51 52 53 54 55 56 57 58 | id: 20210916194900 title: Checklist for Release role: zettel syntax: zmk modified: 20220309105459 # Sync with the official repository #* ``fossil sync -u`` # Make sure that there is no workspace defined. #* ``ls ..`` must not have a file ''go.work'', in no parent folder. # Make sure that all dependencies are up-to-date. #* ``cat go.mod`` # Clean up your Go workspace: #* ``go run tools/build.go clean`` (alternatively: ``make clean``). # All internal tests must succeed: #* ``go run tools/build.go relcheck`` (alternatively: ``make relcheck``). # The API tests must succeed on every development platform: #* ``go run tools/build.go testapi`` (alternatively: ``make api``). # Run [[linkchecker|https://linkchecker.github.io/linkchecker/]] with the manual: #* ``go run -race cmd/zettelstore/main.go run -d docs/manual`` #* ``linkchecker http://127.0.0.1:23123 2>&1 | tee lc.txt`` #* Check all ""Error: 404 Not Found"" #* Check all ""Error: 403 Forbidden"": allowed for endpoint ''/p'' with encoding ''html'' for those zettel that are accessible only in ''expert-mode''. #* Try to resolve other error messages and warnings #* Warnings about empty content can be ignored # On every development platform, the box with 10.000 zettel must run, with ''-race'' enabled: #* ``go run -race cmd/zettelstore/main.go run -d DIR``. # Create a development release: #* ``go run tools/build.go release`` (alternatively: ``make release``). # On every platform (esp. macOS), the box with 10.000 zettel must run properly: #* ``./zettelstore -d DIR`` # Update files in directory ''www'' #* index.wiki #* download.wiki #* changes.wiki #* plan.wiki # Set file ''VERSION'' to the new release version. It _must_ consist of three digits: MAJOR.MINOR.PATCH, even if PATCH is zero # Disable Fossil autosync mode: #* ``fossil setting autosync off`` # Commit the new release version: #* ``fossil commit --tag release --tag vVERSION -m "Version VERSION"`` #* **Important:** the tag must follow the given pattern, e.g. ''v0.0.15''. Otherwise client will not be able to import ''zettelkasten.de/z''. # Clean up your Go workspace: #* ``go run tools/build.go clean`` (alternatively: ``make clean``). # Create the release: #* ``go run tools/build.go release`` (alternatively: ``make release``). # Remove previous executables: #* ``fossil uv remove --glob '*-PREVVERSION*'`` # Add executables for release: #* ``cd release`` #* ``fossil uv add *.zip`` #* ``cd ..`` #* Synchronize with main repository: #* ``fossil sync -u`` # Enable autosync: #* ``fossil setting autosync on`` |
Deleted docs/development/20231218181900.zettel.
|
| < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < |
Changes to docs/manual/00000000000100.zettel.
1 2 3 4 | id: 00000000000100 title: Zettelstore Runtime Configuration role: configuration syntax: none | | | 1 2 3 4 5 6 7 8 9 10 11 12 | id: 00000000000100 title: Zettelstore Runtime Configuration role: configuration syntax: none created: 00010101000000 default-copyright: (c) 2020-present by Detlef Stern <ds@zettelstore.de> default-license: EUPL-1.2-or-later default-visibility: public footer-zettel: 00001000000100 home-zettel: 00001000000000 modified: 20221205173642 site-name: Zettelstore Manual |
︙ | ︙ |
Changes to docs/manual/00001000000000.zettel.
1 2 3 4 5 | id: 00001000000000 title: Zettelstore Manual role: manual tags: #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 | id: 00001000000000 title: Zettelstore Manual role: manual tags: #manual #zettelstore syntax: zmk modified: 20220803183647 * [[Introduction|00001001000000]] * [[Design goals|00001002000000]] * [[Installation|00001003000000]] * [[Configuration|00001004000000]] * [[Structure of Zettelstore|00001005000000]] * [[Layout of a zettel|00001006000000]] * [[Zettelmarkup|00001007000000]] * [[Other markup languages|00001008000000]] * [[Security|00001010000000]] * [[API|00001012000000]] * [[Web user interface|00001014000000]] * [[Tips and Tricks|00001017000000]] * [[Troubleshooting|00001018000000]] * Frequently asked questions Licensed under the EUPL-1.2-or-later. |
Deleted docs/manual/00001000000001.zettel.
|
| < < < < < < < < |
Deleted docs/manual/00001000000002.zettel.
|
| < < < < < < < |
Changes to docs/manual/00001001000000.zettel.
1 2 3 4 5 | id: 00001001000000 title: Introduction to the Zettelstore role: manual tags: #introduction #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 | id: 00001001000000 title: Introduction to the Zettelstore role: manual tags: #introduction #manual #zettelstore syntax: zmk [[Personal knowledge management|https://en.wikipedia.org/wiki/Personal_knowledge_management]] is about collecting, classifying, storing, searching, retrieving, assessing, evaluating, and sharing knowledge as a daily activity. Personal knowledge management is done by most people, not necessarily as part of their main business. It is essential for knowledge workers, like students, researchers, lecturers, software developers, scientists, engineers, architects, to name a few. Many hobbyists build up a significant amount of knowledge, even if the do not need to think for a living. Personal knowledge management can be seen as a prerequisite for many kinds of collaboration. Zettelstore is a software that collects and relates your notes (""zettel"") to represent and enhance your knowledge. It helps with many tasks of personal knowledge management by explicitly supporting the ""[[Zettelkasten method|https://en.wikipedia.org/wiki/Zettelkasten]]"". The method is based on creating many individual notes, each with one idea or information, that are related to each other. Since knowledge is typically build up gradually, one major focus is a long-term store of these notes, hence the name ""Zettelstore"". |
Changes to docs/manual/00001002000000.zettel.
1 2 3 4 5 6 | id: 00001002000000 title: Design goals for the Zettelstore role: manual tags: #design #goal #manual #zettelstore syntax: zmk created: 20210126175322 | | | < < < < | | | < < | 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 | id: 00001002000000 title: Design goals for the Zettelstore role: manual tags: #design #goal #manual #zettelstore syntax: zmk created: 20210126175322 modified: 20221018105415 Zettelstore supports the following design goals: ; Longevity of stored notes / zettel : Every zettel you create should be readable without the help of any tool, even without Zettelstore. : It should be not hard to write other software that works with your zettel. ; Single user : All zettel belong to you, only to you. Zettelstore provides its services only to one person: you. If the computer running Zettelstore is securely configured, there should be no risk that others are able to read or update your zettel. : If 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|00001014000000]]. Anybody can provide alternative user interfaces, e.g. for special purposes. ; Simple service : The purpose of Zettelstore is to safely store your zettel and to provide some initial relations between them. : External software can be written to deeply analyze your zettel and the structures they form. ; Security by default : Without any customization, Zettelstore provides its services in a safe and secure manner and does not expose you (or other users) to security risks. : If you know what use are doing, Zettelstore allows you to relax some security-related preferences. However, even in this case, the more secure way is chosen. |
Changes to docs/manual/00001003000000.zettel.
1 2 3 4 5 | id: 00001003000000 title: Installation of the Zettelstore software role: manual tags: #installation #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: 00001003000000 title: Installation of the Zettelstore software role: manual tags: #installation #manual #zettelstore syntax: zmk modified: 20220119145756 === The curious user You just want to check out the Zettelstore software * Grab the appropriate executable and copy it into any directory * Start the Zettelstore software, e.g. with a double click[^On Windows and macOS, the operating system tries to protect you from possible malicious software. If you encounter problem, please take a look on the [[Troubleshooting|00001018000000]] page.] * A sub-directory ""zettel"" will be created in the directory where you put the executable. It will contain your future zettel. * Open the URI [[http://localhost:23123]] with your web browser. It will present you a mostly empty Zettelstore. There will be a zettel titled ""[[Home|00010000000000]]"" that contains some helpful information. * Please read the instructions for the [[web-based user interface|00001014000000]] and learn about the various ways to write zettel. * If you restart your device, please make sure to start your Zettelstore again. === The intermediate user You already tried the Zettelstore software and now you want to use it permanently. Zettelstore should start automatically when you log into your computer. Please follow [[these instructions|00001003300000]]. === The server administrator You want to provide a shared Zettelstore that can be used from your various devices. Installing Zettelstore as a Linux service is not that hard. |
︙ | ︙ |
Changes to docs/manual/00001003300000.zettel.
1 2 3 4 5 | id: 00001003300000 title: Zettelstore installation for the intermediate user role: manual tags: #installation #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 | id: 00001003300000 title: Zettelstore installation for the intermediate user role: manual tags: #installation #manual #zettelstore syntax: zmk modified: 20220114175754 You already tried the Zettelstore software and now you want to use it permanently. Zettelstore should start automatically when you log into your computer. * Grab the appropriate executable and copy it into the appropriate directory * If you want to place your zettel into another directory, or if you want more than one [[Zettelstore box|00001004011200]], or if you want to [[enable authentication|00001010040100]], or if you want to tweak your Zettelstore in some other way, create an appropriate [[startup configuration file|00001004010000]]. * If you created a startup configuration file, you need to test it: ** Start a command line prompt for your operating system. ** Navigate to the directory, where you placed the Zettelstore executable. In most cases, this is done by the command ``cd DIR``, where ''DIR'' denotes the directory, where you placed the executable. ** Start the Zettelstore: *** On Windows execute the command ``zettelstore.exe run -c CONFIG_FILE`` *** On macOS execute the command ``./zettelstore run -c CONFIG_FILE`` *** On Linux execute the command ``./zettelstore run -c CONFIG_FILE`` ** In all cases ''CONFIG_FILE'' must be substituted by file name where you wrote the startup configuration. ** If you encounter some error messages, update the startup configuration, and try again. * Depending on your operating system, there are different ways to register Zettelstore to start automatically: ** [[Windows|00001003305000]] ** [[macOS|00001003310000]] ** [[Linux|00001003315000]] |
Changes to docs/manual/00001003305000.zettel.
1 2 3 4 5 | id: 00001003305000 title: Enable Zettelstore to start automatically on Windows role: manual tags: #installation #manual #zettelstore syntax: zmk | < | | 1 2 3 4 5 6 7 8 9 10 11 12 13 | id: 00001003305000 title: Enable Zettelstore to start automatically on Windows role: manual tags: #installation #manual #zettelstore syntax: zmk modified: 20220218125541 Windows is a complicated beast. There are several ways to automatically start Zettelstore. === Startup folder One way is to use the [[autostart folder|https://support.microsoft.com/en-us/windows/add-an-app-to-run-automatically-at-startup-in-windows-10-150da165-dcd9-7230-517b-cf3c295d89dd]]. Open the folder where you have placed in the Explorer. |
︙ | ︙ | |||
31 32 33 34 35 36 37 | === Task scheduler The Windows Task scheduler allows you to start Zettelstore as an background task. This is both an advantage and a disadvantage. | | | 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 | === Task scheduler The Windows Task scheduler allows you to start Zettelstore as an background task. This is both an advantage and a disadvantage. On the plus side, Zettelstore runs in the background, and it does not disturbs you. All you have to do is to open your web browser, enter the appropriate URL, and there you go. On the negative side, you will not be notified when you enter the wrong data in the Task scheduler and Zettelstore fails to start. This can be mitigated by first using the command line prompt to start Zettelstore with the appropriate options. Once everything works, you can register Zettelstore to be automatically started by the task scheduler. There you should make sure that you have followed the first steps as described on the [[parent page|00001003300000]]. |
︙ | ︙ | |||
68 69 70 71 72 73 74 | Create a new action. {{00001003305112}} The next steps are the trickiest. | | | 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 | Create a new action. {{00001003305112}} The next steps are the trickiest. If you did not created a startup configuration file, then create an action that starts a program. Enter the file path where you placed the Zettelstore executable. The ""Browse ..."" button helps you with that.[^I store my Zettelstore executable in the sub-directory ''bin'' of my home directory.] It is essential that you also enter a directory, which serves as the environment for your zettelstore. The (sub-) directory ''zettel'', which will contain your zettel, will be placed in this directory. If you leave the field ""Start in (optional)"" empty, the directory will be an internal Windows system directory (most likely: ''C:\\Windows\\System32''). |
︙ | ︙ |
Changes to docs/manual/00001003310000.zettel.
1 2 3 4 5 | id: 00001003310000 title: Enable Zettelstore to start automatically on macOS role: manual tags: #installation #manual #zettelstore syntax: zmk | < | 1 2 3 4 5 6 7 8 9 10 11 12 | id: 00001003310000 title: Enable Zettelstore to start automatically on macOS role: manual tags: #installation #manual #zettelstore syntax: zmk modified: 20220119124635 There are several ways to automatically start Zettelstore. * [[Login Items|#login-items]] * [[Launch Agent|#launch-agent]] |
︙ | ︙ |
Changes to docs/manual/00001003315000.zettel.
1 2 3 4 5 | id: 00001003315000 title: Enable Zettelstore to start automatically on Linux role: manual tags: #installation #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 | id: 00001003315000 title: Enable Zettelstore to start automatically on Linux role: manual tags: #installation #manual #zettelstore syntax: zmk modified: 20220307104944 Since there is no such thing as the one Linux, there are too many different ways to automatically start Zettelstore. * One way is to interpret your Linux desktop system as a server and use the [[recipe to install Zettelstore on a server|00001003600000]]. ** See below for a lighter alternative. * If you are using the [[Gnome Desktop|https://www.gnome.org/]], you could use the tool [[Tweak|https://wiki.gnome.org/action/show/Apps/Tweaks]] (formerly known as ""GNOME Tweak Tool"" or just ""Tweak Tool""). It allows to specify application that should run on startup / login. * [[KDE|https://kde.org/]] provides a system setting to [[autostart|https://docs.kde.org/stable5/en/plasma-workspace/kcontrol/autostart/]] applications. * [[Xfce|https://xfce.org/]] allows to specify [[autostart applications|https://docs.xfce.org/xfce/xfce4-session/preferences#application_autostart]]. * [[LXDE|https://www.lxde.org/]] uses [[LXSession Edit|https://wiki.lxde.org/en/LXSession_Edit]] to allow users to specify autostart applications. If you use a different desktop environment, it often helps to to provide its name and the string ""autostart"" to google for it with the search engine of your choice. Yet another way is to make use of the middleware that is provided. Many Linux distributions make use of [[systemd|https://systemd.io/]], which allows to start processes on behalf of an user. On the command line, adapt the following script to your own needs and execute it: ``` # mkdir -p "$HOME/.config/systemd/user" # cd "$HOME/.config/systemd/user" # cat <<__EOF__ > zettelstore.service [Unit] Description=Zettelstore |
︙ | ︙ |
Changes to docs/manual/00001003600000.zettel.
1 2 3 4 5 | id: 00001003600000 title: Installation of Zettelstore on a server role: manual tags: #installation #manual #zettelstore syntax: zmk | < | | 1 2 3 4 5 6 7 8 9 10 11 12 13 | id: 00001003600000 title: Installation of Zettelstore on a server role: manual tags: #installation #manual #zettelstore syntax: zmk modified: 20211125185833 You want to provide a shared Zettelstore that can be used from your various devices. Installing Zettelstore as a Linux service is not that hard. Grab the appropriate executable and copy it into the appropriate directory: ```sh # sudo mv zettelstore /usr/local/bin/zettelstore |
︙ | ︙ | |||
48 49 50 51 52 53 54 | # sudo systemctl start zettelstore ``` Use the commands ``systemctl``{=sh} and ``journalctl``{=sh} to manage the service, e.g.: ```sh # sudo systemctl status zettelstore # verify that it is running # sudo journalctl -u zettelstore # obtain the output of the running zettelstore ``` | < < < < < < | 47 48 49 50 51 52 53 | # sudo systemctl start zettelstore ``` Use the commands ``systemctl``{=sh} and ``journalctl``{=sh} to manage the service, e.g.: ```sh # sudo systemctl status zettelstore # verify that it is running # sudo journalctl -u zettelstore # obtain the output of the running zettelstore ``` |
Changes to docs/manual/00001004000000.zettel.
1 2 3 4 5 | id: 00001004000000 title: Configuration of Zettelstore role: manual tags: #configuration #manual #zettelstore syntax: zmk | < | | | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 | id: 00001004000000 title: Configuration of Zettelstore role: manual tags: #configuration #manual #zettelstore syntax: zmk modified: 20210510153233 There are some levels to change the behavior and/or the appearance of Zettelstore. # The first level is the way to start Zettelstore services and to manage it via command line (and, in part, via a graphical user interface). #* [[Command line parameters|00001004050000]] # As an intermediate user, you usually want to have more control over how Zettelstore is started. This may include the URI under which your Zettelstore is accessible, or the directories in which your Zettel are stored. You may want to permanently store the command line parameters so that you don't have to specify them every time you start Zettelstore. #* [[Zettelstore startup configuration|00001004010000]] |
︙ | ︙ |
Changes to docs/manual/00001004010000.zettel.
1 2 3 4 5 6 | id: 00001004010000 title: Zettelstore startup configuration role: manual tags: #configuration #manual #zettelstore syntax: zmk created: 20210126175322 | | | | | | | | | > | | | | | < | > | | | | < | | | | > | | | | < < < < < | | | | | 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 | id: 00001004010000 title: Zettelstore startup configuration role: manual tags: #configuration #manual #zettelstore syntax: zmk created: 20210126175322 modified: 20221128155143 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"" ; [!asset-dir|''asset-dir''] : Allows to specify a directory whose files are allowed be transferred directly with the help of the web server. The URL prefix for these files is ''/assets/''. You can use this if you want to transfer files that are too large for a note to users. Examples would be presentation files, PDF files, music files or video files. Files within the given directory will not be managed by Zettelstore.[^They will be managed by Zettelstore just in the case that the directory is one of the configured [[boxes|#box-uri-x]].] If you specify only the URL prefix, then the contents of the directory are listed to the user. To avoid this, create an empty file in the directory named ""index.html"". Default: """", no asset directory is set, the URL prefix ''/assets/'' is invalid. ; [!base-url|''base-url''] : Sets the absolute base URL for the service. Note: [[''url-prefix''|#url-prefix]] must be the suffix of ''base-url'', otherwise the web service will not start. Default: ""http://127.0.0.1:23123/"". ; [!box-uri-x|''box-uri-X''], where __X__ is a number greater or equal to one : 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. ; [!debug-mode|''debug-mode''] : Allows to debug the Zettelstore software (mostly used by the developers) if set to [[true|00001006030500]] Disables any timeout values of the internal web server and does not send some security-related data. Sets [[''log-level''|#log-level]] to ""debug"". Do not enable it for a production server. Default: ""false"" ; [!default-dir-box-type|''default-dir-box-type''] : Specifies the default value for the (sub-) type of [[directory boxes|00001004011400#type]]. Zettel are typically stored in such boxes. Default: ""notify"" ; [!insecure-cookie|''insecure-cookie''] : Must be set to [[true|00001006030500]], if authentication is enabled and Zettelstore is not accessible not via HTTPS (but via HTTP). Otherwise web browser are free to ignore the authentication cookie. Default: ""false"" ; [!insecure-html|''insecure-html''] : Allows to use HTML, e.g. within supported markup languages, even if this might introduce security-related problems. However, HTML containing the ``<script>`` or the ``<iframe>`` tag is always ignored. But due to ""clever"" ways of combining HTML, CSS, JavaScript, there might be some negative security consequences. Please be aware of this! Allowed values: ""html"" (allow zettel with [[syntax ""html""|00001008000000#html]]), ""markdown"" (""html"", plus allow inline HTML for Markdown markup only), ""zettelmarkup"" (""markdown"", plus allow inline HTML for Zettelmarkup). Any other value is interpreted as ""secure"". Default: ""secure"". ; [!listen-addr|''listen-addr''] : Configures the network address, where the Zettelstore service is listening for requests. Syntax is: ''[NETWORKIP]:PORT'', where ''NETWORKIP'' is the IP-address of the networking interface (or something like ""0.0.0.0"" if you want to listen on all network interfaces, and ''PORT'' is the TCP port. Default value: ""127.0.0.1:23123"" ; [!log-level|''log-level''] : Specify the [[logging level|00001004059700]] for the whole application or for a given (internal) service, overwriting the level ""debug"" set by configuration [[''debug-mode''|#debug-mode]]. Can be changed at runtime, even for specific internal services, with the ''log-level'' command of the [[administrator console|00001004101000#log-level]]. Several specifications are separated by the semicolon character (""'';''"", U+003B). Each specification consists of an optional service name, together with the colon character (""'':''"", U+003A), followed by the logging level. Default: ""info"". Examples: ""sense"" will produce sensing messages (e.g. a little more than ""info""); ""sense;web:debug"" will emit debugging messages for the web component of Zettelstore while still producing sensing messages for all other components. When you are familiar to operate the Zettelstore, you might set the level to ""warn"" or ""error"" to receive less noisy messages from the Zettelstore. ; [!max-request-size|''max-request-size''] : Limits the maximum byte size of a web request body to prevent clients from accidentally or maliciously sending a large request and wasting server resources. The minimum value is 1024. Default: 16777216 (16 MiB). ; [!owner|''owner''] : [[Identifier|00001006050000]] of a zettel that contains data about the owner of the Zettelstore. The owner has full authorization for the Zettelstore. Only if owner is set to some value, user [[authentication|00001010000000]] is enabled. Ensure that key [[''secret''|#secret]] is set to a value of at least 16 bytes. Otherwise the Zettelstore will not start for security reasons. ; [!persistent-cookie|''persistent-cookie''] : A [[boolean value|00001006030500]] to make the access cookie persistent. This is helpful if you access the Zettelstore via a mobile device. 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 service into a read-only mode, if set to a [[true value|00001006030500]]. No changes are possible. Default: ""false"". ; [!secret|''secret''] : A string value to make the communication with external clients strong enough so that sessions of the [[web user interface|00001014000000]] or [[API access token|00001010040700]] cannot be modified by some external unfriendly party. The string must have a length of at least 16 bytes. It is only needed to set this value, if [[authentication is enabled|00001010040100]] by setting key [[''owner''|#owner]] to some user identification. ; [!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. ''token-lifetime-api'' is for accessing Zettelstore via its [[API|00001012000000]]. Default: ""10"". ''token-lifetime-html'' specifies the lifetime for the HTML views. It is automatically extended, when a new HTML view is rendered. Default: ""60"". ; [!url-prefix|''url-prefix''] : Add the given string as a prefix to the local part of a Zettelstore local URL/URI when rendering zettel representations. Must begin and end with a slash character (""''/''"", U+002F). Note: ''url-prefix'' must be the suffix of [[''base-url''|#base-url]], otherwise the web service will not start. Default: ""/"". This allows to use a forwarding proxy [[server|00001010090100]] in front of the Zettelstore. ; [!verbose-mode|''verbose-mode''] : Be more verbose when logging data, if set to a [[true value|00001006030500]]. Default: ""false"" |
Deleted docs/manual/00001004010200.zettel.
|
| < < < < < < < < < < < < < < < < < < < < < < < < < < < < < |
Changes to docs/manual/00001004011200.zettel.
1 2 3 4 5 | id: 00001004011200 title: Zettelstore boxes 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 | id: 00001004011200 title: Zettelstore boxes role: manual tags: #configuration #manual #zettelstore syntax: zmk modified: 20220307121547 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. |
︙ | ︙ |
Changes to docs/manual/00001004011400.zettel.
1 2 3 4 5 | id: 00001004011400 title: Configure file directory boxes 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 | id: 00001004011400 title: Configure file directory boxes role: manual tags: #configuration #manual #zettelstore syntax: zmk modified: 20220724200512 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]]"") |worker|Number of worker that can access the directory in parallel|7 |readonly|Allow only operations that do not create or change 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 SMB/CIFS or NFS. |
︙ | ︙ | |||
53 54 55 56 57 58 59 | === 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 ``` | | | 52 53 54 55 56 57 58 59 | === 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/00001004011600.zettel.
1 2 3 4 5 | id: 00001004011600 title: Configure memory boxes 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 | id: 00001004011600 title: Configure memory boxes role: manual tags: #configuration #manual #zettelstore syntax: zmk modified: 20220307122554 Under most circumstances, it is preferable to further configure a memory box. This is done by appending query parameters after the base box URI ''mem:''. The following parameters are supported: |= Parameter:|Description|Default value:|Maximum value: |max-bytes|Maximum number of bytes the box will store|65535|1073741824 (1 GiB) |max-zettel|Maximum number of zettel|127|65535 The default values are somehow arbitrarily, but applicable for many use cases. While the number of zettel should be easily calculable by an user, the number of bytes might be a little more difficult. Metadata consumes 6 bytes for the zettel identifier and for each metadata value one byte for the separator, plus the length of key and data. Then size of the content is its size in bytes. For text content, its the number of bytes for its UTF-8 encoding. If one of the limits are exceeded, Zettelstore will give an error indication, based on the HTTP status code 507. |
Changes to docs/manual/00001004020000.zettel.
1 | id: 00001004020000 | | | < < < < | | > | < < < < < < < < < < < < < < < < < < < < < < < < < < | 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 | id: 00001004020000 title: Configure the running Zettelstore role: manual tags: #configuration #manual #zettelstore syntax: zmk created: 20210126175322 modified: 20230317183435 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. Some of them can be overwritten in an [[user zettel|00001010040200]], a subset of those may be overwritten in zettel that is currently used. See the full list of [[metadata that may be overwritten|00001004020200]]. ; [!default-copyright|''default-copyright''] : Copyright value to be used when rendering content. Can be overwritten in a zettel with [[meta key|00001006020000]] ''copyright''. Default: (the empty string). ; [!default-license|''default-license''] : License value to be used when rendering content. Can be overwritten in a zettel with [[meta key|00001006020000]] ''license''. Default: (the empty string). ; [!default-visibility|''default-visibility''] : Visibility to be used, if zettel does not specify a value for the [[''visibility''|00001006020000#visibility]] metadata key. Default: ""login"". ; [!expert-mode|''expert-mode''] : If set to a [[boolean true value|00001006030500]], all zettel with [[visibility ""expert""|00001010070200]] will be shown (to the owner, if [[authentication is enabled|00001010040100]]; to all, otherwise). This affects most computed zettel. Default: ""False"". ; [!footer-zettel|''footer-zettel''] : Identifier of a zettel that is rendered as HTML and will be placed as the footer of every zettel in the [[web user interface|00001014000000]]. Zettel content, delivered via the [[API|00001012000000]] as JSON, etc. is not affected. If the zettel identifier is invalid or references a zettel that could not be read (possibly because of a limited [[visibility setting|00001010070200]]), nothing is written as the footer. May be [[overwritten|00001004020200]] in a user zettel. Default: (an invalid zettel identifier) ; [!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. May be [[overwritten|00001004020200]] in a user zettel. ; [!lang|''lang''] : Language to be used when displaying content. Default: ""en"". This value is used as a default value, if it is not set in an user's zettel or in a zettel. It is also used to specify the language for all non-zettel content, e.g. lists or search results. Use values according to the language definition of [[RFC-5646|https://tools.ietf.org/html/rfc5646]]. ; [!max-transclusions|''max-transclusions''] : Maximum number of indirect transclusion. This is used to avoid an exploding ""transclusion bomb"", a form of a [[billion laughs attack|https://en.wikipedia.org/wiki/Billion_laughs_attack]]. Default: ""1024"". ; [!site-name|''site-name''] : Name of the Zettelstore instance. Will be used when displaying some lists. Default: ""Zettelstore"". ; [!yaml-header|''yaml-header''] : If [[true|00001006030500]], metadata and content will be separated by ''---\\n'' instead of an empty line (''\\n\\n''). Default: ""False"". You will probably use this key, if you are working with another software processing [[Markdown|https://daringfireball.net/projects/markdown/]] that uses a subset of [[YAML|https://yaml.org/]] to specify metadata. ; [!zettel-file-syntax|''zettel-file-syntax''] : If you create a new zettel with a syntax different to ""zmk"", Zettelstore will store the zettel as two files: one for the metadata (file without a filename extension) and another for the content (file extension based on the syntax value). If you want to specify alternative syntax values, for which you want new zettel to be stored in one file (file extension ''.zettel''), you can use this key. All values are case-insensitive, duplicate values are removed. For example, you could use this key if you're working with Markdown syntax and you want to store metadata and content in one ''.zettel'' file. If ''yaml-header'' evaluates to true, a zettel is always stored in one ''.zettel'' file. |
Changes to docs/manual/00001004020200.zettel.
1 2 3 4 5 6 | id: 00001004020200 title: Runtime configuration data that may be user specific or zettel specific role: manual tags: #configuration #manual #zettelstore syntax: zmk created: 20221205155521 | | | < < < < < < | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 | id: 00001004020200 title: Runtime configuration data that may be user specific or zettel specific role: manual tags: #configuration #manual #zettelstore syntax: zmk created: 20221205155521 modified: 20230317183403 Some metadata of the [[runtime configuration|00001004020000]] may be overwritten in an [[user zettel|00001010040200]]. A subset of those may be overwritten in zettel that is currently used. This allows to specify user specific or zettel specific behavior. The following metadata keys are supported to provide a more specific behavior: |=Key|User:|Zettel:|Remarks |[[''footer-zettel''|00001004020000#footer-zettel]]|Y|N| |[[''home-zettel''|00001004020000#home-zettel]]|Y|N| |[[''lang''|00001004020000#lang]]|Y|Y|Making it user-specific could make zettel for other user less useful |
Changes to docs/manual/00001004050000.zettel.
1 2 3 4 5 6 | id: 00001004050000 title: Command line parameters role: manual tags: #command #configuration #manual #zettelstore syntax: zmk created: 20210126175322 | | | | | | 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: 00001004050000 title: Command line parameters role: manual tags: #command #configuration #manual #zettelstore syntax: zmk created: 20210126175322 modified: 20221128161932 Zettelstore is not just a service that provides services of a zettelkasten. It allows to some tasks to be executed at the command line. Typically, the task (""sub-command"") will be given at the command line as the first parameter. If no parameter is given, the Zettelstore is called as ``` zettelstore ``` This is equivalent to call it this way: ```sh mkdir -p ./zettel zettelstore run -d ./zettel -c ./.zscfg ``` Typically this is done by starting Zettelstore via a graphical user interface by double-clicking to its file icon. === Sub-commands * [[``zettelstore help``|00001004050200]] lists all available sub-commands. * [[``zettelstore version``|00001004050400]] to display version information of Zettelstore. * [[``zettelstore run``|00001004051000]] to start the Zettelstore service. * [[``zettelstore run-simple``|00001004051100]] is typically called, when you start Zettelstore by a double.click in your GUI. * [[``zettelstore file``|00001004051200]] to render files manually without activated/running Zettelstore services. * [[``zettelstore password``|00001004051400]] to calculate data for [[user authentication|00001010040200]]. Every sub-command allows the following command line options: ; [!h|''-h''] (or ''--help'') : Does not execute the sub-command, but shows allowed command line options (except ''-h'' / ''--help''). ; [!l|''-l LOGSPEC''] |
︙ | ︙ |
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 | 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 |
︙ | ︙ |
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 | id: 00001004050400 title: The ''version'' sub-command role: manual tags: #command #configuration #manual #zettelstore syntax: zmk modified: 20211124182041 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. |
︙ | ︙ |
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 | id: 00001004051000 title: The ''run'' sub-command role: manual tags: #command #configuration #manual #zettelstore syntax: zmk modified: 20220724162050 === ``zettelstore run`` This starts the web service. ``` zettelstore run [-a PORT] [-c CONFIGFILE] [-d DIR] [-debug] [-p PORT] [-r] [-v] |
︙ | ︙ |
Changes to docs/manual/00001004051100.zettel.
1 2 3 4 5 6 | id: 00001004051100 title: The ''run-simple'' sub-command role: manual tags: #command #configuration #manual #zettelstore syntax: zmk created: 20210126175322 | | | | | | 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: 00001004051100 title: The ''run-simple'' sub-command role: manual tags: #command #configuration #manual #zettelstore syntax: zmk created: 20210126175322 modified: 20221128161922 === ``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]]. First, this sub-command checks if it can read a [[Zettelstore startup configuration|00001004010000]] file by trying the [[default values|00001004051000#c]]. If this is the case, ''run-simple'' just continues as the [[''run'' sub-command|00001004051000]], but ignores any command line options (including ''-d DIR'').[^This allows a [[curious user|00001003000000]] to become an intermediate user.] If no startup configuration was found, the sub-command 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''] |
︙ | ︙ |
Changes to docs/manual/00001004051200.zettel.
︙ | ︙ | |||
13 14 15 16 17 18 19 20 | ``` ; ''-t FORMAT'' : Specifies the output format. Supported values are: [[''html''|00001012920510]] (default), [[''md''|00001012920513]], [[''shtml''|00001012920525]], | > < | 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 | ``` ; ''-t FORMAT'' : Specifies the output format. Supported values are: [[''html''|00001012920510]] (default), [[''md''|00001012920513]], [[''sexpr''|00001012920516]], [[''shtml''|00001012920525]], [[''text''|00001012920519]], and [[''zmk''|00001012920522]]. ; ''file-1'' : Specifies the file name, where at least metadata is read. If ''file-2'' is not given, the zettel content is also read from here. ; ''file-2'' : File name where the zettel content is stored. |
︙ | ︙ |
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 28 29 30 31 32 33 34 35 36 37 38 | 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: credential: $2a$10$1q92v1Ya8Too5HD/4rKpPuCP8fZTYPochsC6DcY1T4JKwhSx8uLu6 user-id: bob ``` This will produce a hashed password (""credential"") for the new user ""bob"" to be stored in zettel ""20200911115600"". You should copy the relevant output to the zettel of the user to be secured, especially by setting the meta keys ''credential'' and ''user-id'' to the copied values. Please note that the generated hashed password is tied to the given user identification (''user-id'') and to the identifier of its zettel. Changing one of those will stop authenticating the user with the given password. In this case you have to re-run this sub-command. |
Changes to docs/manual/00001004059700.zettel.
1 2 3 4 5 | id: 00001004059700 title: List of supported logging levels 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 | id: 00001004059700 title: List of supported logging levels role: manual tags: #configuration #manual #zettelstore syntax: zmk modified: 20220113183606 Zettelstore supports various levels of logging output. This allows you to see the inner workings of Zettelstore, or to avoid it. Each level has an associated name and number. A lower number signals more logging output. |= Name | Number >| Description | Trace | 1 | Show most of the inner workings | Debug | 2 | Show many internal values that might be interesting for a [[Zettelstore developer|00000000000005]]. | Sense | 3 | Display sensing events, which are not essential information. | Info | 4 | Display information about an event. In most cases, there is no required action expected from you. | Warn | 5 | Show a warning, i.e. an event that might become an error or more. Mostly invalid data. | Error | 6 | Notify about an error, which was handled automatically. Something is broken. User intervention is not required, in most cases. Monitor the application. | Fatal | 7 | Notify about a significant error that cannot be handled automatically. At least some important functionality is disabled. | Panic | 8 | The application is in an uncertain state and notifies you about its panic. At least some part of the application is possibly restarted. | Mandatory | 9 | Important message will be shown, e.g. the Zettelstore version at startup time. | Disabled | 10 | No messages will be shown If you set the logging level to a certain value, only messages with the same or higher numerical value will be shown. E.g. if you set the logging level to ""warn"", no ""trace"", ""debug"", ""sense", and ""info"" messages are shown, but ""warn"", ""error"", ""fatal"", ""panic"", and ""mandatory"" messages. |
Changes to docs/manual/00001004059900.zettel.
1 2 3 4 5 | id: 00001004059900 title: Command line flags for profiling the application role: manual tags: #command #configuration #manual #zettelstore syntax: zmk | < | 1 2 3 4 5 6 7 8 9 10 11 12 | id: 00001004059900 title: Command line flags for profiling the application role: manual tags: #command #configuration #manual #zettelstore syntax: zmk modified: 20211122174951 If you want to measure potential bottlenecks within the software Zettelstore, there are two [[command line|00001004050000]] flags for enabling the measurement (also called __profiling__): ; ''-cpuprofile FILE'' : Enables CPU profiling. |
︙ | ︙ |
Changes to docs/manual/00001004100000.zettel.
1 2 3 4 5 | id: 00001004100000 title: Zettelstore Administrator Console 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 | id: 00001004100000 title: Zettelstore Administrator Console role: manual tags: #configuration #manual #zettelstore syntax: zmk modified: 20211103162926 The administrator console is a service accessible only on the same computer on which Zettelstore is running. It allows an experienced user to monitor and control some of the inner workings of Zettelstore. You enable the administrator console by specifying a TCP port number greater than zero (better: greater than 1024) for it, either via the [[command-line parameter ''-a''|00001004051000#a]] or via the ''admin-port'' key of the [[startup configuration file|00001004010000#admin-port]]. After you enable the administrator console, you can use tools such as [[PuTTY|https://www.chiark.greenend.org.uk/~sgtatham/putty/]] or other telnet software to connect to the administrator console. In fact, the administrator console is __not__ a full telnet service. It is merely a simple line-oriented service where each input line is interpreted separately. Therefore, you can also use tools like [[netcat|https://nc110.sourceforge.io/]], [[socat|http://www.dest-unreach.org/socat/]], etc. After connecting to the administrator console, there is no further authentication. It is not needed because you must be logged in on the same computer where Zettelstore is running. You cannot connect to the administrator console if you are on a different computer. Of course, on multi-user systems with encrusted users, you should not enable the administrator console. * Enable via [[command line|00001004051000#a]] * Enable via [[configuration file|00001004010000#admin-port]] * [[List of supported commands|00001004101000]] |
Changes to docs/manual/00001004101000.zettel.
1 2 3 4 5 | id: 00001004101000 title: List of supported commands of the administrator console role: manual tags: #configuration #manual #zettelstore syntax: zmk | < | | 1 2 3 4 5 6 7 8 9 10 11 12 13 | id: 00001004101000 title: List of supported commands of the administrator console role: manual tags: #configuration #manual #zettelstore syntax: zmk modified: 20220823194553 ; [!bye|''bye''] : Closes the connection to the administrator console. ; [!config|''config SERVICE''] : Displays all valid configuration keys for the given service. If a key ends with the hyphen-minus character (""''-''"", U+002D), the key denotes a list value. |
︙ | ︙ | |||
36 37 38 39 40 41 42 | ``get-config`` shows all current configuration data. ``get-config SERVICE`` shows only the current configuration data of the given service. ``get-config SERVICE KEY`` shows the current configuration data for the given service and key. ; [!header|''header''] | | | 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 | ``get-config`` shows all current configuration data. ``get-config SERVICE`` shows only the current configuration data of the given service. ``get-config SERVICE KEY`` shows the current configuration data for the given service and key. ; [!header|''header''] : Toggles the header mode, where each table is show with a header nor not. ; [!log-level|''log-level''] : Displays or sets the [[logging level|00001004059700]] for the kernel or a service. ``log-level`` shows all known log level. ``log-level NAME`` shows log level for the given service or for the kernel. |
︙ | ︙ | |||
75 76 77 78 79 80 81 | It may be removed without any further notice at any time. In most cases, it is a tool for software developers to optimize Zettelstore's internal workings. ; [!refresh|''refresh''] : Refresh all internal data about zettel. ; [!restart|''restart SERVICE''] : Restart the given service and all other that depend on this. ; [!services|''services''] | | | 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 | It may be removed without any further notice at any time. In most cases, it is a tool for software developers to optimize Zettelstore's internal workings. ; [!refresh|''refresh''] : Refresh all internal data about zettel. ; [!restart|''restart SERVICE''] : Restart the given service and all other that depend on this. ; [!services|''services''] : Displays s list of all available services and their current status. ; [!set-config|''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-__. |
︙ | ︙ |
Changes to docs/manual/00001005000000.zettel.
1 2 3 4 5 | id: 00001005000000 title: Structure of Zettelstore role: manual 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 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 | id: 00001005000000 title: Structure of Zettelstore role: manual tags: #design #manual #zettelstore syntax: zmk modified: 20220104213511 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 the builtin [[web user interface|00001014000000]] 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|00001012000000]]) that allows other software to communicate with the Zettelstore. Zettelstore becomes extensible by external software. For example, a more sophisticated user 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 user interface|00001014000000]] or via the [[API|00001012053200]], 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. Since the only restriction on zettel identifiers are the 14 digits, you are free to use other digit sequences. The [[configuration zettel|00001004020000]] is one prominent example, as well as these manual zettel. You can create these special zettel identifiers either with the __rename__ function of Zettelstore or by manually renaming the underlying zettel files. It is allowed that the file name contains other characters after the 14 digits. These are ignored by Zettelstore. Two filename extensions are used by Zettelstore: # ''.zettel'' is a format that stores metadata and content together in one file, # the empty file extension is used, when the content must be stored in its own file, e.g. image data; in this case, the filename just the 14 digits of the zettel identifier, and optional characters except the period ''"."''. Other filename extensions are used to determine the ""syntax"" of a zettel. This allows to use other content within the Zettelstore, e.g. images or HTML templates. 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, but without a filename extension. 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 user interface|00001014000000]], some templates are used, as well as a [[layout specification in CSS|00000000020001]]. The icon that visualizes a broken image is a [[predefined GIF image|00000000040001]]. 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|00001002000000]] was to have just one executable file to use Zettelstore. But data stored within an executable program 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: alternative 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 memory (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 6 | id: 00001005090000 title: List of predefined zettel role: manual tags: #manual #reference #zettelstore syntax: zmk created: 20210126175322 | | < < < < < < | > < < < > < < < < < | | < < < < | | 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: 00001005090000 title: List of predefined zettel role: manual tags: #manual #reference #zettelstore syntax: zmk created: 20210126175322 modified: 20220909180240 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 | [[00000000000007]] | Zettelstore Log | Lists the last 8192 log messages | [[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 | [[00000000000092]] | Zettelstore Supported Parser | Lists all supported values for metadata [[syntax|00001006020000#syntax]] that are recognized by Zettelstore | [[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 Zettel 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 Template | 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 | [[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]] | [[00000000029000]] | Zettelstore Role to CSS Map | [[Maps|00001017000000#role-css]] [[role|00001006020000#role]] to a zettel identifier that is included by the [[Base HTML Template|00000000010100]] as an CSS file | [[00000000040001]] | Generic Emoji | Image that is shown if [[original image reference|00001007040322]] 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 [[user zettel|00001010040200]] | [[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/00001006000000.zettel.
1 2 3 4 5 6 | id: 00001006000000 title: Layout of a Zettel role: manual tags: #design #manual #zettelstore syntax: zmk created: 20210126175322 | | | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 | id: 00001006000000 title: Layout of a Zettel role: manual tags: #design #manual #zettelstore syntax: zmk created: 20210126175322 modified: 20221219162703 A zettel consists of two parts: the metadata and the zettel content. Metadata gives some information mostly about the zettel content, how it should be interpreted, how it is sorted within Zettelstore. The zettel content is, well, the actual content. In many cases, the content is in plain text form. Plain text is long-lasting. However, content in binary format is also possible. |
︙ | ︙ | |||
28 29 30 31 32 33 34 | Other character encodings are not supported and will never be[^This is not a real problem, since every modern software should support UTF-8 as an encoding.]. There is support for a graphical format with a text representation: SVG. And there is support for some binary image formats, like GIF, PNG, and JPEG. === Plain, parsed, and evaluated zettel Zettelstore may present your zettel in various forms, typically retrieved with the [[endpoint|00001012920000]] ''/z/{ID}''. One way is to present the zettel as it was read by Zettelstore. | | | | | | 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 | Other character encodings are not supported and will never be[^This is not a real problem, since every modern software should support UTF-8 as an encoding.]. There is support for a graphical format with a text representation: SVG. And there is support for some binary image formats, like GIF, PNG, and JPEG. === Plain, parsed, and evaluated zettel Zettelstore may present your zettel in various forms, typically retrieved with the [[endpoint|00001012920000]] ''/z/{ID}''. One way is to present the zettel as it was read by Zettelstore. This is called ""[[plain zettel|00001003000000]]"". The second way is to present the zettel as it was recognized by Zettelstore. This is called ""[[parsed zettel|00001012053600]]"", also retrieved with the [[endpoint|00001012920000]] ''/z/{ID}'', but with additional query parameter. Such a zettel was read and analyzed. It can be presented in various [[encodings|00001012920500]].[^The [[zmk encoding|00001012920522]] allows you to compare the plain, the parsed, and the evaluated form of a zettel.] However, a zettel such as this one you are currently reading, is a ""[[evaluated zettel|00001012053500]]"", also retrieved with the [[endpoint|00001012920000]] ''/z/{ID}'' and specifying an encoding. The biggest difference to a parsed zettel is the inclusion of [[block transclusions|00001007031100]] or [[inline transclusions|00001007040324]] for an evaluated zettel. It can also be presented in various encoding, including the ""zmk"" encoding. Evaluations also applies to metadata of a zettel, if appropriate. Please note, that searching for content is based on parsed zettel. Transcluded content will only be found in transcluded zettel, but not in the zettel that transcluded the content. However, you will easily pick up that zettel by follow the [[backward|00001006020000#backward]] metadata key of the transcluded zettel. |
Changes to docs/manual/00001006010000.zettel.
1 2 3 4 5 | id: 00001006010000 title: Syntax of Metadata role: manual tags: #manual #syntax #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 | id: 00001006010000 title: Syntax of Metadata role: manual tags: #manual #syntax #zettelstore syntax: zmk modified: 20220218131923 The metadata of a zettel is a collection of key-value pairs. The syntax roughly resembles the internal header of an email ([[RFC5322|https://tools.ietf.org/html/rfc5322]]). The key is a sequence of alphanumeric characters, a hyphen-minus character (""''-''"", U+002D) is also allowed. It begins at the first position of a new line. A key is separated from its value either by * a colon character (""'':''""), * a non-empty sequence of space characters, * a sequence of space characters, followed by a colon, followed by a sequence of space characters. A Value is a sequence of printable characters. If the value should be continued in the following line, that following line (""continuation line"") must begin with a non-empty sequence of space characters. The rest of the following line will be interpreted as the next part of the value. There can be more than one continuation line for a value. A non-continuation line that contains a possibly empty sequence of characters, followed by the percent sign character (""''%''"") is treated as a comment line. It will be ignored. |
︙ | ︙ |
Changes to docs/manual/00001006020000.zettel.
1 2 3 4 5 6 | id: 00001006020000 title: Supported Metadata Keys role: manual tags: #manual #meta #reference #zettel #zettelstore syntax: zmk created: 20210126175322 | | | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 | id: 00001006020000 title: Supported Metadata Keys role: manual tags: #manual #meta #reference #zettel #zettelstore syntax: zmk created: 20210126175322 modified: 20230320150950 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]]. ; [!author|''author''] |
︙ | ︙ | |||
29 30 31 32 33 34 35 | ; [!created|''created''] : Date and time when a zettel was created through Zettelstore. If you create 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. | | < < < < < < < < < | 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 | ; [!created|''created''] : Date and time when a zettel was created through Zettelstore. If you create 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. If it is not stored within a zettel, it will be computed based on the value of the [[Zettel Identifier|00001006050000]]: if it contains a value >= 19700101000000, it will be coerced to da date/time; otherwise the version time of the running software will be used. Please note that the value von ''created'' will be different (in most cases) to the value of [[''id''|#id]] / the zettel identifier, because it is exact up to the second. When calculating a zettel identifier, Zettelstore tries to set the second value to zero, if possible. ; [!credential|''credential''] : Contains the hashed password, as it was emitted by [[``zettelstore password``|00001004051400]]. It is internally created by hashing the password, the [[zettel identifier|00001006050000]], and the value of the ''ident'' key. It is only used for zettel with a ''role'' value of ""user"". ; [!dead|''dead''] : Property that contains all references that does __not__ identify a zettel. ; [!folge|''folge''] : Is a property that contains identifier of all zettel that reference this zettel through the [[''precursor''|#precursor]] value. ; [!forward|''forward''] : Property that contains all references that identify another zettel within the content of the zettel. ; [!id|''id''] : Contains the [[zettel identifier|00001006050000]], as given by the Zettelstore. It cannot be set manually, because it is a computed value. ; [!lang|''lang''] : Language for the zettel. |
︙ | ︙ | |||
79 80 81 82 83 84 85 | ; [!precursor|''precursor''] : References zettel for which this zettel is a ""Folgezettel"" / follow-up zettel. Basically the inverse of key [[''folge''|#folge]]. ; [!predecessor|''predecessor''] : References the zettel that contains a previous version of the content. In contrast to [[''precursor''|#precurso]] / [[''folge''|#folge]], this is a reference because of technical reasons, not because of content-related reasons. Basically the inverse of key [[''successors''|#successors]]. | < < < | < < < < | | < < | | | 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 | ; [!precursor|''precursor''] : References zettel for which this zettel is a ""Folgezettel"" / follow-up zettel. Basically the inverse of key [[''folge''|#folge]]. ; [!predecessor|''predecessor''] : References the zettel that contains a previous version of the content. In contrast to [[''precursor''|#precurso]] / [[''folge''|#folge]], this is a reference because of technical reasons, not because of content-related reasons. Basically the inverse of key [[''successors''|#successors]]. ; [!published|''published''] : This property contains the timestamp of the mast modification / creation of the zettel. If [[''modified''|#modified]] is set with a valid timestamp, it contains the its value. Otherwise, if [[''created''|#created]] is set with a valid timestamp, it contains the its 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|00001007700000]] zettel based on their publication date. It is a computed value. There is no need to set it via Zettelstore. ; [!query|''query''] : Stores the [[query|00001007031140]] that was used to create the zettel. This is for future reference. ; [!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''] : Defines the role of the zettel. Can be used for selecting zettel. See [[supported zettel roles|00001006020100]]. If not given, it is ignored. ; [!successors|''successors''] : Is a property that contains identifier of all zettel that reference this zettel through the [[''predecessor''|#predecessor]] value. Therefore, it references all zettel that contain a new version of the content and/or metadata. In contrast to [[''folge''|#folge]], these are references because of technical reasons, not because of content-related reasons. In most cases, zettel referencing the current zettel should be updated to reference a successor zettel. The [[query reference|00001007040310]] [[query:backward? successors?]] lists all such zettel. ; [!summary|''summary''] : Summarizes the content of the zettel. You may use all [[inline-structued elements|00001007040000]] of Zettelmarkup. ; [!syntax|''syntax''] : Specifies the syntax that should be used for interpreting the zettel. The zettel about [[other markup languages|00001008000000]] defines supported values. If it is not given, it defaults to ''plain''. ; [!tags|''tags''] : Contains a space separated list of tags to describe the zettel further. Each Tag must begin with the number sign character (""''#''"", U+0023). ; [!title|''title''] : Specifies the title of the zettel. If not given, the value of [[''id''|#id]] will be used. ; [!url|''url''] : Defines an URL / URI for this zettel that possibly references external material. One use case is to specify the document that the current zettel comments on. The URL will be rendered special in the [[web user interface|00001014000000]] if you use the default template. ; [!useless-files|''useless-files''] : Contains the file names that are rejected to serve the content of a zettel. Is used for [[directory boxes|00001004011400]] and [[file boxes|00001004011200#file]]. If a zettel is renamed or deleted, these files will be deleted. ; [!user-id|''user-id''] : Provides some unique user identification for an [[user zettel|00001010040200]]. It is used as a user name for authentication. It is only used for zettel with a ''role'' value of ""user"". ; [!user-role|''user-role''] : Defines the basic privileges of an authenticated user, e.g. reading / changing zettel. |
︙ | ︙ |
Changes to docs/manual/00001006020100.zettel.
1 2 3 4 5 | id: 00001006020100 title: Supported Zettel Roles 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 24 25 26 27 28 29 30 31 32 33 | id: 00001006020100 title: Supported Zettel Roles role: manual tags: #manual #meta #reference #zettel #zettelstore syntax: zmk modified: 20220623183234 The [[''role'' key|00001006020000#role]] defines what kind of zettel you are writing. You are free to define your own roles. It is allowed to set an empty value or to omit the role. Some roles are defined for technical reasons: ; [!configuration|''configuration''] : A zettel that contains some configuration data for the Zettelstore. Most prominent is [[00000000000100]], as described in [[00001004020000]]. ; [!manual|''manual''] : All zettel that document the inner workings of the Zettelstore software. This role is only used in this specific Zettelstore. If you adhere to the process outlined by Niklas Luhmann, a zettel could have one of the following three roles: ; [!note|''note''] : A small note, to remember something. Notes are not real zettel, they just help to create a real zettel. Think of them as Post-it notes. ; [!literature|''literature''] : Contains some remarks about a book, a paper, a web page, etc. You should add a citation key for citing it. ; [!zettel|''zettel''] : A real zettel that contains your own thoughts. However, you are free to define additional roles, e.g. ''material'' for literature that is web-based only, ''slide'' for presentation slides, ''paper'' for the text of a scientific paper, ''project'' to define a project, ... |
Changes to docs/manual/00001006020400.zettel.
1 2 3 4 5 | id: 00001006020400 title: Supported values for metadata key ''read-only'' 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 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 | id: 00001006020400 title: Supported values for metadata key ''read-only'' role: manual tags: #manual #meta #reference #zettel #zettelstore syntax: zmk modified: 20211124132040 A zettel can be marked as read-only, if it contains a metadata value for key [[''read-only''|00001006020000#read-only]]. If user authentication is [[enabled|00001010040100]], it is possible to allow some users to change the zettel, depending on their [[user role|00001010070300]]. Otherwise, the read-only mark is just a binary value. === 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 user interface|00001014000000]]. 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 user interface|00001014000000]]. 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 | 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 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 | id: 00001006030000 title: Supported Key Types role: manual tags: #manual #meta #reference #zettel #zettelstore syntax: zmk modified: 20220304114106 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]] | ''-role'' | [[Word|00001006035500]] | ''-set'' | [[WordSet|00001006036000]] | ''-title'' | [[Zettelmarkup|00001006036500]] | ''-url'' | [[URL|00001006035000]] | ''-zettel'' | [[Identifier|00001006032000]] | ''-zid'' | [[Identifier|00001006032000]] | ''-zids'' | [[IdentifierSet|00001006032500]] | 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. * [[Credential|00001006031000]] * [[EString|00001006031500]] * [[Identifier|00001006032000]] * [[IdentifierSet|00001006032500]] * [[Number|00001006033000]] * [[String|00001006033500]] * [[TagSet|00001006034000]] * [[Timestamp|00001006034500]] * [[URL|00001006035000]] * [[Word|00001006035500]] * [[WordSet|00001006036000]] * [[Zettelmarkup|00001006036500]] |
Changes to docs/manual/00001006030500.zettel.
1 2 3 4 5 | id: 00001006030500 title: Boolean Value role: manual tags: #manual #reference #zettel #zettelstore syntax: zmk | < | 1 2 3 4 5 6 7 8 9 10 11 12 | id: 00001006030500 title: Boolean Value role: manual tags: #manual #reference #zettel #zettelstore syntax: zmk modified: 20220304114040 On some places, metadata values are interpreted as a truth value. Every character sequence that begins with a ""0"", ""F"", ""N"", ""f"", or a ""n"" is interpreted as the boolean ""false"" value. All values are interpreted as the boolean ""true"" value. |
︙ | ︙ |
Changes to docs/manual/00001006031000.zettel.
1 2 3 4 5 6 | id: 00001006031000 title: Credential Key Type role: manual tags: #manual #meta #reference #zettel #zettelstore syntax: zmk created: 20210212135017 | | | | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 | id: 00001006031000 title: Credential Key Type role: manual tags: #manual #meta #reference #zettel #zettelstore syntax: zmk created: 20210212135017 modified: 20220914130324 Values of this type denote a credential value, e.g. an encrypted password. === Allowed values All printable characters are allowed. Since a credential contains some kind of secret, the sequence of characters might have some hidden syntax to be interpreted by other parts of Zettelstore. === Query operators A credential never compares to any other value. A comparison will never match in any way. === Sorting If a list of zettel should be sorted based on a credential value, the identifier of the respective zettel is used instead. |
Changes to docs/manual/00001006031500.zettel.
1 2 3 4 5 6 | id: 00001006031500 title: EString Key Type role: manual tags: #manual #meta #reference #zettel #zettelstore syntax: zmk created: 20210212135017 | | | | | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 | id: 00001006031500 title: EString Key Type role: manual tags: #manual #meta #reference #zettel #zettelstore syntax: zmk created: 20210212135017 modified: 20220914130448 Values of this type are just a sequence of character, possibly an empty sequence. An EString is the most general metadata key type, as it places no restrictions to the character sequence.[^Well, there are some minor restrictions that follow from the [[metadata syntax|00001006010000]].] === Allowed values All printable characters are allowed. === Query operator All comparisons are done case-insensitive, i.e. ""hell"" will be the prefix of ""Hello"". === Sorting To sort two values, the underlying encoding is used to determine which value is less than the other. Uppercase letters are typically interpreted as less than their corresponding lowercase letters, i.e. ``A < a``. |
︙ | ︙ |
Changes to docs/manual/00001006032000.zettel.
1 2 3 4 5 6 | id: 00001006032000 title: Identifier Key Type role: manual tags: #manual #meta #reference #zettel #zettelstore syntax: zmk created: 20210212135017 | | | < < < < < < < | < < < | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 | id: 00001006032000 title: Identifier Key Type role: manual tags: #manual #meta #reference #zettel #zettelstore syntax: zmk created: 20210212135017 modified: 20220914134914 Values of this type denote a [[zettel identifier|00001006050000]]. === Allowed values Must be a sequence of 14 digits (""0""--""9""). === Query operator Comparison is done with the string representation of the identifiers. For example, ""000010"" matches ""[[00001006032000]]"". === Sorting Sorting is done by comparing the [[String|00001006033500]] values. If both values are identifiers, this works well because both have the same length. |
Changes to docs/manual/00001006032500.zettel.
1 2 3 4 5 6 | id: 00001006032500 title: IdentifierSet Key Type role: manual tags: #manual #meta #reference #zettel #zettelstore syntax: zmk created: 20210212135017 | | | | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 | id: 00001006032500 title: IdentifierSet Key Type role: manual tags: #manual #meta #reference #zettel #zettelstore syntax: zmk created: 20210212135017 modified: 20220914131354 Values of this type denote a (sorted) set of [[zettel identifier|00001006050000]]. A set is different to a list, as no duplicate values are allowed. === Allowed values Must be at least one sequence of 14 digits (""0""--""9""), separated by space characters. === Query operator A value matches an identifier set value, if the value matches any of the identifier set values. For example, ""000010060325"" is a prefix ""[[00001006032000]] [[00001006032500]]"". === Sorting Sorting is done by comparing the [[String|00001006033500]] values. |
Changes to docs/manual/00001006033000.zettel.
1 2 3 4 5 6 | id: 00001006033000 title: Number Key Type role: manual tags: #manual #meta #reference #zettel #zettelstore syntax: zmk created: 20210212135017 | | | < < < < < | < | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 | id: 00001006033000 title: Number Key Type role: manual tags: #manual #meta #reference #zettel #zettelstore syntax: zmk created: 20210212135017 modified: 20220914131211 Values of this type denote a numeric integer value. === Allowed values Must be a sequence of digits (""0""--""9""), optionally prefixed with a ""-"" or a ""+"" character. === Query operator All comparisons are done on the given string representation of the number, ""+12"" will be treated as a different number of ""12"". === Sorting Sorting is done by comparing the numeric values. |
Changes to docs/manual/00001006033500.zettel.
1 2 3 4 5 6 | id: 00001006033500 title: String Key Type role: manual tags: #manual #meta #reference #zettel #zettelstore syntax: zmk created: 20210212135017 | | | | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 | id: 00001006033500 title: String Key Type role: manual tags: #manual #meta #reference #zettel #zettelstore syntax: zmk created: 20210212135017 modified: 20220914130505 Values of this type are just a sequence of character, but not an empty sequence. === Allowed values All printable characters are allowed. There must be at least one such character. === Query operator All comparisons are done case-insensitive, i.e. ""hell"" will be the prefix of ""Hello"". === Sorting To sort two values, the underlying encoding is used to determine which value is less than the other. Uppercase letters are typically interpreted as less than their corresponding lowercase letters, i.e. ``A < a``. |
︙ | ︙ |
Changes to docs/manual/00001006034000.zettel.
1 2 3 4 5 6 | id: 00001006034000 title: TagSet Key Type role: manual tags: #manual #meta #reference #zettel #zettelstore syntax: zmk created: 20210212135017 | | | | | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 | id: 00001006034000 title: TagSet Key Type role: manual tags: #manual #meta #reference #zettel #zettelstore syntax: zmk created: 20210212135017 modified: 20220914131048 Values of this type denote a (sorted) set of tags. A set is different to a list, as no duplicate values are allowed. === Allowed values Every tag must must begin with the number sign character (""''#''"", U+0023), followed by at least one printable character. Tags are separated by space characters. All characters are mapped to their lower case values. === Query operator All comparisons are done case-sensitive, i.e. ""#hell"" will not be the prefix of ""#Hello"". === Sorting Sorting is done by comparing the [[String|00001006033500]] values. |
Changes to docs/manual/00001006034500.zettel.
1 2 3 4 5 6 | id: 00001006034500 title: Timestamp Key Type role: manual tags: #manual #meta #reference #zettel #zettelstore syntax: zmk created: 20210212135017 | | | < < < < < < < < | < < | < < | > > | 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: 00001006034500 title: Timestamp Key Type role: manual tags: #manual #meta #reference #zettel #zettelstore syntax: zmk created: 20210212135017 modified: 20220914130919 Values of this type denote a point in time. === Allowed values Must be a sequence of 14 digits (""0""--""9"") (same as an [[Identifier|00001006032000]]), with the restriction that is conforms to the pattern ""YYYYMMDDhhmmss"". * YYYY is the year, * MM is the month, * DD is the day, * hh is the hour, * mm is the minute, * ss is the second. === Query operator All comparisons assume that up to 14 digits are given. === Sorting Sorting is done by comparing the [[String|00001006033500]] values. If both values are timestamp values, this works well because both have the same length. |
Changes to docs/manual/00001006035000.zettel.
1 2 3 4 5 6 | id: 00001006035000 title: URL Key Type role: manual tags: #manual #meta #reference #zettel #zettelstore syntax: zmk created: 20210212135017 | | | | | | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 | id: 00001006035000 title: URL Key Type role: manual tags: #manual #meta #reference #zettel #zettelstore syntax: zmk created: 20210212135017 modified: 20220914130809 Values of this type denote an URL. === Allowed values All characters of an URL / URI are allowed. === Query operator All comparisons are done case-insensitive. For example, ""hello"" is the suffix of ""http://example.com/Hello"". === Sorting Sorting is done by comparing the [[String|00001006033500]] values. |
Changes to docs/manual/00001006035500.zettel.
1 2 3 4 5 6 | id: 00001006035500 title: Word Key Type role: manual tags: #manual #meta #reference #zettel #zettelstore syntax: zmk created: 20210212135017 | | | | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 | id: 00001006035500 title: Word Key Type role: manual tags: #manual #meta #reference #zettel #zettelstore syntax: zmk created: 20210212135017 modified: 20220914130655 Values of this type denote a single word. === Allowed values Must be a non-empty sequence of characters, but without the space character. All characters are mapped to their lower case values. === Query operator All comparisons are done case-insensitive, i.e. ""hell"" will be the prefix of ""Hello"". === Sorting Sorting is done by comparing the [[String|00001006033500]] values. |
Added docs/manual/00001006036000.zettel.
> > > > > > > > > > > > > > > > > > > > | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 | id: 00001006036000 title: WordSet Key Type role: manual tags: #manual #meta #reference #zettel #zettelstore syntax: zmk created: 20210212135017 modified: 20220914130725 Values of this type denote a (sorted) set of [[words|00001006035500]]. A set is different to a list, as no duplicate values are allowed. === Allowed values Must be a sequence of at least one word, separated by space characters. === Query operator All comparisons are done case-insensitive, i.e. ""hell"" will be the prefix of ""World, Hello"". === Sorting Sorting is done by comparing the [[String|00001006033500]] values. |
Added docs/manual/00001006036500.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 | id: 00001006036500 title: Zettelmarkup Key Type role: manual tags: #manual #meta #reference #zettel #zettelstore syntax: zmk created: 20210212135017 modified: 20220914135405 Values of this type are [[String|00001006033500]] values, interpreted as [[Zettelmarkup|00001007000000]]. === Allowed values All printable characters are allowed. There must be at least one such character. === Query operator Comparison is done similar to the full-text search: both the value to compare and the metadata value are normalized according to Unicode NKFD, ignoring everything except letters and numbers. Letters are mapped to the corresponding lower-case value. For example, ""Brücke"" will be the prefix of ""(Bruckenpfeiler,"". === Sorting To sort two values, the underlying encoding is used to determine which value is less than the other. Uppercase letters are typically interpreted as less than their corresponding lowercase letters, i.e. ``A < a``. Comparison is done character-wise by finding the first difference in the respective character sequence. For example, ``abc > aBc``. |
Changes to docs/manual/00001006050000.zettel.
1 2 3 4 5 | id: 00001006050000 title: Zettel identifier role: manual 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. |
Changes to docs/manual/00001006055000.zettel.
1 2 3 4 5 | id: 00001006055000 title: Reserved zettel identifier role: manual 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 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 | id: 00001006055000 title: Reserved zettel identifier role: manual tags: #design #manual #zettelstore syntax: zmk modified: 20220311111751 [[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 | This [[Zettelstore manual|00001000000000]] | 00000200000000 | 0000899999999 | Reserved for future use | 00009000000000 | 0000999999999 | Reserved for applications This list may change in the future. ==== External Applications |= From | To | Description | 00009000001000 | 00009000001999 | [[Zettel Presenter|https://zettelstore.de/contrib]], an application to display zettel as a HTML-based slideshow | 00009000002000 | 00009000002999 | [[Zettel Blog|https://zettelstore.de/contrib]], an application to collect and transform zettel into a blog |
Changes to docs/manual/00001007000000.zettel.
1 2 3 4 5 6 | id: 00001007000000 title: Zettelmarkup role: manual tags: #manual #zettelmarkup #zettelstore syntax: zmk created: 20210126175322 | | | | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 | id: 00001007000000 title: Zettelmarkup role: manual tags: #manual #zettelmarkup #zettelstore syntax: zmk created: 20210126175322 modified: 20221209192105 Zettelmarkup is a rich plain-text based markup language for writing zettel content. Besides the zettel content, Zettelmarkup is also used for specifying the title of a zettel, regardless of the syntax of a zettel. Zettelmarkup supports the longevity of stored notes by providing a syntax that any person can easily read, as well as a computer. Zettelmarkup can be much easier parsed / consumed by a software compared to other markup languages. Writing a parser for [[Markdown|https://daringfireball.net/projects/markdown/syntax]] is quite challenging. [[CommonMark|00001008010500]] is an attempt to make it simpler by providing a comprehensive specification, combined with an extra chapter to give hints for the implementation. Zettelmarkup follows some simple principles that anybody who knows to ho write software should be able understand to create an implementation. Zettelmarkup is a markup language on its own. This is in contrast to Markdown, which is basically a super-set of HTML: every HTML document is a valid Markdown document.[^To be precise: the content of the ``<body>`` of each HTML document is a valid Markdown document.] While HTML is a markup language that will probably last for a long time, it cannot be easily translated to other formats, such as PDF, JSON, or LaTeX. Additionally, it is allowed to embed other languages into HTML, such as CSS or even JavaScript. This could create problems with longevity as well as security problems. |
︙ | ︙ |
Changes to docs/manual/00001007010000.zettel.
1 2 3 4 5 | id: 00001007010000 title: Zettelmarkup: General Principles role: manual tags: #manual #zettelmarkup #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 49 50 51 52 53 54 55 56 57 58 59 60 | id: 00001007010000 title: Zettelmarkup: General Principles role: manual tags: #manual #zettelmarkup #zettelstore syntax: zmk modified: 20211124175047 Any document can be thought as a sequence of paragraphs and other [[block-structured elements|00001007030000]] (""blocks""), such as [[headings|00001007030300]], [[lists|00001007030200]], quotations, and code blocks. Some of these blocks can contain other blocks, for example lists may contain other lists or paragraphs. Other blocks contain [[inline-structured elements|00001007040000]] (""inlines""), such as text, [[links|00001007040310]], emphasized text, and images. With the exception of lists and tables, the markup for blocks always begins at the first position of a line with three or more identical characters. List blocks also begin at the first position of a line, but may need one or more identical character, plus a space character. [[Table blocks|00001007031000]] begin at the first position of a line with the character ""``|``"". Non-list blocks are either fully specified on that line or they span multiple lines and are delimited with the same three or more character. It depends on the block kind, whether blocks are specified on one line or on at least two lines. If a line does not begin with an explicit block element. the line is treated as a (implicit) paragraph block element that contains inline elements. This paragraph ends when a block element is detected at the beginning of a next line or when an empty line occurs. Some blocks may also contain inline elements, e.g. a heading. 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|00001007040330]], [[citation keys|00001007040340]], 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 example, 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|00001007050000]]. 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. * With some exceptions, inline-structural elements begins with two characters, quite often the same two characters. * The most important exceptions are links. * The backslash character can help to resolve possible ambiguities. * Attributes refine some block and inline elements. * Block elements have a higher priority than inline elements. These principles makes automatic recognizing zettelmarkup an (relatively) easy task. By looking at the reference implementation, a moderately skilled software developer should be able to create a appropriate software in a different programming language. |
Changes to docs/manual/00001007020000.zettel.
1 2 3 4 5 | id: 00001007020000 title: Zettelmarkup: Basic Definitions role: manual tags: #manual #zettelmarkup #zettelstore syntax: zmk | < | 1 2 3 4 5 6 7 8 9 10 11 12 | id: 00001007020000 title: Zettelmarkup: Basic Definitions role: manual tags: #manual #zettelmarkup #zettelstore syntax: zmk modified: 20220218130713 Every Zettelmarkup content consists of a sequence of Unicode code-points. Unicode code-points are called in the following as **character**s. Characters are encoded with UTF-8. |
︙ | ︙ |
Changes to docs/manual/00001007030000.zettel.
1 2 3 4 5 | id: 00001007030000 title: Zettelmarkup: Block-Structured Elements role: manual tags: #manual #zettelmarkup #zettelstore syntax: zmk | < | | 1 2 3 4 5 6 7 8 9 10 11 12 13 | id: 00001007030000 title: Zettelmarkup: Block-Structured Elements role: manual tags: #manual #zettelmarkup #zettelstore syntax: zmk modified: 20220311181036 Every markup for blocks-structured elements (""blocks"") begins at the very first position of a line. There are five kinds of blocks: lists, one-line blocks, line-range blocks, tables, and paragraphs. === Lists |
︙ | ︙ | |||
62 63 64 65 66 67 68 | :::example = Heading Some text follows. ::: This is because headings need at least three equal sign character. A paragraph is essentially a sequence of [[inline-structured elements|00001007040000]]. | | | 61 62 63 64 65 66 67 68 69 70 71 72 73 74 | :::example = Heading Some text follows. ::: This is because headings need at least three equal sign character. A paragraph is essentially a sequence of [[inline-structured elements|00001007040000]]. Inline-structured elements cam span more than one line. Paragraphs are separated by empty lines. If you want to specify a second paragraph inside a list item, or if you want to continue a paragraph on a second and more line within a list item, you must begin the paragraph with a certain number of space characters. The number of space characters depends on the kind of a list and the relevant nesting level. A line that begins with a space character and which is outside of a list or does not contain the right number of space characters is considered to be part of a paragraph. |
Changes to docs/manual/00001007030100.zettel.
1 2 3 4 5 | id: 00001007030100 title: Zettelmarkup: Description Lists role: manual tags: #manual #zettelmarkup #zettelstore syntax: zmk | < | | | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 | id: 00001007030100 title: Zettelmarkup: Description Lists role: manual tags: #manual #zettelmarkup #zettelstore syntax: zmk modified: 20220218131155 A description list is a sequence of terms to be described together with the descriptions of each term. Every term can described in multiple ways. A description term (short: __term__) is specified with one semicolon (""'';''"", U+003B) at the first position, followed by a space character and the described term, specified as a sequence of line elements. If the following lines should also be part of the term, exactly two spaces must be given at the beginning of each following line. The description of a term is given with one colon (""'':''"", U+003A) at the first position, followed by a space character and the description itself, specified as a sequence of [[inline elements|00001007040000]]. Similar to terms, following lines can also be part of the actual description, if they begin at each line with exactly two space characters. |
︙ | ︙ |
Changes to docs/manual/00001007030200.zettel.
1 2 3 4 5 | id: 00001007030200 title: Zettelmarkup: Nested Lists role: manual tags: #manual #zettelmarkup #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: 00001007030200 title: Zettelmarkup: Nested Lists role: manual tags: #manual #zettelmarkup #zettelstore syntax: zmk modified: 20220218133902 There are thee kinds of lists that can be nested: ordered lists, unordered lists, and quotation lists. Ordered lists are specified with the number sign (""''#''"", U+0023), unordered lists use the asterisk (""''*''"", U+002A), and quotation lists are specified with the greater-than sing (""''>''"", U+003E). Let's call these three characters __list characters__. Any nested list item is specified by a non-empty sequence of list characters, followed by a space character and a sequence of [[inline elements|00001007040000]]. In case of a quotation list as the last list character, the space character followed by a sequence of inline elements is optional. The number / count of list characters gives the nesting of the lists. If the following lines should also be part of the list item, exactly the same number of spaces must be given at the beginning of each of the following lines as it is the lists are nested, plus one additional space character. In other words: the inline elements must begin at the same column as it was on the previous line. The resulting sequence on inline elements is merged into a paragraph. Appropriately indented paragraphs can specified after the first one. Since each blocks-structured element has to be specified at the first position of a line, none of the nested list items may contain anything else than paragraphs. Some examples: ```zmk # One # Two # Three |
︙ | ︙ |
Changes to docs/manual/00001007030300.zettel.
1 2 3 4 5 | id: 00001007030300 title: Zettelmarkup: Headings role: manual tags: #manual #zettelmarkup #zettelstore syntax: zmk | < | | 1 2 3 4 5 6 7 8 9 10 11 12 13 | id: 00001007030300 title: Zettelmarkup: Headings role: manual tags: #manual #zettelmarkup #zettelstore syntax: zmk modified: 20220218133755 To specify a (sub-) section of a zettel, you should use the headings syntax: at the beginning of a new line type at least three equal signs (""''=''"", U+003D), plus at least one space and enter the text of the heading as [[inline elements|00001007040000]]. ```zmk === Level 1 Heading |
︙ | ︙ | |||
26 27 28 29 30 31 32 | ====== Level 4 Heading ======= Level 5 Heading ======== Level 5 Heading ::: === Notes | | | 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 | ====== Level 4 Heading ======= Level 5 Heading ======== Level 5 Heading ::: === Notes The heading level is translated to a HTML heading by adding 1 to the level, e.g. ``=== Level 1 Heading``{=zmk} translates to ==<h2>Level 1 Heading</h2>=={=html}. The ==<h1>=={=html} tag is rendered for the zettel title. This syntax is often used in a similar way in wiki implementation. However, trailing equal signs are __not__ removed, they are part of the heading text. If you use command line tools, you can easily create a draft table of contents with the command: ```sh grep -h '^====* ' ZETTEL_ID.zettel ``` |
Changes to docs/manual/00001007030400.zettel.
1 2 3 4 5 6 | id: 00001007030400 title: Zettelmarkup: Horizontal Rules / Thematic Break role: manual tags: #manual #zettelmarkup #zettelstore syntax: zmk created: 20210126175322 | | | | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 | id: 00001007030400 title: Zettelmarkup: Horizontal Rules / Thematic Break role: manual tags: #manual #zettelmarkup #zettelstore syntax: zmk created: 20210126175322 modified: 20220825185533 To signal a thematic break, you can specify a horizontal rule. This is done by entering at least three hyphen-minus characters (""''-''"", U+002D) at the first position of a line. You can add some [[attributes|00001007050000]], although the horizontal rule does not support the default attribute. Any other characters in this line will be ignored. If you do not enter the three hyphen-minus character at the very first position of a line, the are interpreted as [[inline elements|00001007040000]], typically as an ""en-dash" followed by a hyphen-minus. Example: ```zmk --- ----{.zs-deprecated} ----- |
︙ | ︙ |
Changes to docs/manual/00001007030500.zettel.
1 2 3 4 5 | id: 00001007030500 title: Zettelmarkup: Verbatim Blocks role: manual tags: #manual #zettelmarkup #zettelstore syntax: zmk | < | 1 2 3 4 5 6 7 8 9 10 11 12 | id: 00001007030500 title: Zettelmarkup: Verbatim Blocks role: manual tags: #manual #zettelmarkup #zettelstore syntax: zmk modified: 20220218131500 Verbatim blocks are used to enter text that should not be interpreted. They begin with at least three grave accent characters (""''`''"", U+0060) at the first position of a line. Alternatively, a modifier letter grave accent (""''ˋ''"", U+02CB) is also allowed[^On some devices, such as an iPhone / iPad, a grave accent character is harder to enter and is often confused with a modifier letter grave accent.]. You can add some [[attributes|00001007050000]] on the beginning line of a verbatim block, following the initiating characters. |
︙ | ︙ |
Changes to docs/manual/00001007030600.zettel.
1 2 3 4 5 | id: 00001007030600 title: Zettelmarkup: Quotation Blocks role: manual tags: #manual #zettelmarkup #zettelstore syntax: zmk | < | 1 2 3 4 5 6 7 8 9 10 11 12 | id: 00001007030600 title: Zettelmarkup: Quotation Blocks role: manual tags: #manual #zettelmarkup #zettelstore syntax: zmk modified: 20220218131806 A simple way to enter a quotation is to use the [[quotation list|00001007030200]]. A quotation list loosely follows the convention of quoting text within emails. However, if you want to attribute the quotation to someone, a quotation block is more appropriately. This kind of line-range block begins with at least three less-than characters (""''<''"", U+003C) at the first position of a line. |
︙ | ︙ |
Changes to docs/manual/00001007030700.zettel.
1 2 3 4 5 | id: 00001007030700 title: Zettelmarkup: Verse Blocks role: manual tags: #manual #zettelmarkup #zettelstore syntax: zmk | < | 1 2 3 4 5 6 7 8 9 10 11 12 | id: 00001007030700 title: Zettelmarkup: Verse Blocks role: manual tags: #manual #zettelmarkup #zettelstore syntax: zmk modified: 20220218132432 Sometimes, you want to enter text with significant space characters at the beginning of each line and with significant line endings. Poetry is one typical example. Of course, you could help yourself with hard space characters and hard line breaks, by entering a backslash character before a space character and at the end of each line. Using a verse block might be easier. |
︙ | ︙ |
Changes to docs/manual/00001007030800.zettel.
1 2 3 4 5 | id: 00001007030800 title: Zettelmarkup: Region Blocks role: manual tags: #manual #zettelmarkup #zettelstore syntax: zmk | < | | | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 | id: 00001007030800 title: Zettelmarkup: Region Blocks role: manual tags: #manual #zettelmarkup #zettelstore syntax: zmk modified: 20220323190829 Region blocks does not directly have a visual representation. They just group a range of lines. You can use region blocks to enter [[attributes|00001007050000]] that apply only to this range of lines. One example is to enter a multi-line warning that should be visible. This kind of line-range block begins with at least three colon characters (""'':''"", U+003A) at the first position of a line[^Since a [[description text|00001007030100]] only use exactly one colon character at the first position of a line, there is no possible ambiguity between these elements.]. You can add some [[attributes|00001007050000]] on the beginning line of a region block, following the initiating characters. The region block does not support the default attribute, but it supports the generic attribute. |
︙ | ︙ |
Changes to docs/manual/00001007030900.zettel.
1 2 3 4 5 | id: 00001007030900 title: Zettelmarkup: Comment Blocks role: manual tags: #manual #zettelmarkup #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 | id: 00001007030900 title: Zettelmarkup: Comment Blocks role: manual tags: #manual #zettelmarkup #zettelstore syntax: zmk modified: 20220218130330 Comment blocks are quite similar to [[verbatim blocks|00001007030500]]: both are used to enter text that should not be interpreted. While the text entered inside a verbatim block will be processed somehow, text inside a comment block will be ignored[^Well, not completely ignored: text is read, but it will typically not rendered visible.]. Comment blocks are typically used to give some internal comments, e.g. the license of a text or some internal remarks. Comment blocks begin with at least three percent sign characters (""''%''"", U+0025) at the first position of a line. You can add some [[attributes|00001007050000]] on the beginning line of a comment block, following the initiating characters. The comment block supports the default attribute: when given, the text will be rendered, e.g. as an HTML comment. When rendered to JSON, the comment block will not be ignored but it will output some JSON text. Same for other renderer. Any other character in this line will be ignored Text following the beginning line will not be interpreted, until a line begins with at least the same number of the same characters given at the beginning line. This allows to enter some percent sign characters in the text that should not be interpreted. For example: ```zmk %%% Comment Block %%% ``` will be completely ignored, while ```zmk %%%{-} Will be rendered %%% ``` will be rendered as some kind of comment[^This cannot be shown here, because a HTML comment will not be rendered visible; it will be in the HTML text.]. |
Changes to docs/manual/00001007031000.zettel.
1 2 3 4 5 | id: 00001007031000 title: Zettelmarkup: Tables role: manual tags: #manual #zettelmarkup #zettelstore syntax: zmk | < | | | | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 | id: 00001007031000 title: Zettelmarkup: Tables role: manual tags: #manual #zettelmarkup #zettelstore syntax: zmk modified: 20220218131107 Tables are used to show some data in a two-dimensional fashion. In zettelmarkup, table are not specified explicitly, but by entering __table rows__. Therefore, a table can be seen as a sequence of table rows. A table row is nothing as a sequence of __table cells__. The length of a table is the number of table rows, the width of a table is the maximum length of its rows. The first cell of a row must begin with the vertical bar character (""''|''"", U+007C) at the first position of a line. The other cells of a row begin with the same vertical bar character at later positions in that line. A cell is delimited by the vertical bar character of the next cell or by the end of the current line. A vertical bar character as the last character of a line will not result in a table cell. It will be ignored. |
︙ | ︙ | |||
29 30 31 32 33 34 35 | :::example | a1 | a2 | a3| | b1 | b2 | b3 | c1 | c2 ::: === Header row | | | 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 | :::example | a1 | a2 | a3| | b1 | b2 | b3 | c1 | c2 ::: === Header row If any cell in the first row of a table contains an equal sing character (""''=''"", U+003D) as the very first character, then this first row will be interpreted as a __table header__ row. For example: ```zmk | a1 | a2 |= a3| | b1 | b2 | b3 | c1 | c2 ``` |
︙ | ︙ |
Changes to docs/manual/00001007031110.zettel.
1 2 3 4 5 6 | id: 00001007031110 title: Zettelmarkup: Zettel Transclusion role: manual tags: #manual #zettelmarkup #zettelstore syntax: zmk created: 20220809132350 | | | | | 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 | id: 00001007031110 title: Zettelmarkup: Zettel Transclusion role: manual tags: #manual #zettelmarkup #zettelstore syntax: zmk created: 20220809132350 modified: 20220926183331 A zettel transclusion is specified by the following sequence, starting at the first position in a line: ''{{{zettel-identifier}}}''. When evaluated, the referenced zettel is read. If it contains some transclusions itself, these will be expanded, recursively. When a recursion is detected, expansion does not take place. Instead an error message replaces the transclude specification. An error message is also given, if the zettel cannot be read or if too many transclusions are made. The maximum number of transclusion can be controlled by setting the value [[''max-transclusions''|00001004020000#max-transclusions]] of the runtime configuration zettel. If everything went well, the referenced, expanded zettel will replace the transclusion element. For example, to include the text of the Zettel titled ""Zettel identifier"", just specify its identifier [[''00001006050000''|00001006050000]] in the transclude element: ```zmk {{{00001006050000}}} ``` This will result in: :::example {{{00001006050000}}} ::: Please note: if the referenced zettel is changed, all transclusions will also change. This allows, for example, to create a bigger document just by transcluding smaller zettel. In addition, if a zettel __z__ transcludes a zettel __t__, but the current user is not allowed to view zettel __t__ (but zettel __z__), then the transclusion will not take place. To the current user, it seems that there was no transclusion in zettel __z__. This allows to create a zettel with content that seems to be changed, depending on the authorization of the current user. --- Any [[attributes|00001007050000]] added to the transclusion will set/overwrite the appropriate metadata of the included zettel. Of course, this applies only to thoes attribtues, which have a valid name for a metadata key. This allows to control the evaluation of the included zettel, especially for zettel containing a diagram description. === See also [[Inline-mode transclusion|00001007040324]] does not work at the paragraph / block level, but is used for [[inline-structured elements|00001007040000]]. |
Changes to docs/manual/00001007031140.zettel.
1 2 3 4 5 6 | id: 00001007031140 title: Zettelmarkup: Query Transclusion role: manual tags: #manual #search #zettelmarkup #zettelstore syntax: zmk created: 20220809132350 | | | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 | id: 00001007031140 title: Zettelmarkup: Query Transclusion role: manual tags: #manual #search #zettelmarkup #zettelstore syntax: zmk created: 20220809132350 modified: 20230116183656 A query transclusion is specified by the following sequence, starting at the first position in a line: ''{{{query:query-expression}}}''. The line must literally start with the sequence ''{{{query:''. Everything after this prefix is interpreted as a [[query expression|00001007700000]]. When evaluated, the query expression is evaluated, often resulting in a list of [[links|00001007040310]] to zettel, matching the query expression. The result replaces the query transclusion element. |
︙ | ︙ | |||
34 35 36 37 38 39 40 41 42 | : The resulting list will be a numbered list. ; ''MINn'' (parameter) : Emit only those values with at least __n__ aggregated values. __n__ must be a positive integer, ''MIN'' must be given in upper-case letters. ; ''MAXn'' (parameter) : Emit only those values with at most __n__ aggregated values. __n__ must be a positive integer, ''MAX'' must be given in upper-case letters. ; ''KEYS'' (aggregate) : Emit a list of all metadata keys, together with the number of zettel having the key. | > > > > > > > > > < < < | < < < | 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 | : The resulting list will be a numbered list. ; ''MINn'' (parameter) : Emit only those values with at least __n__ aggregated values. __n__ must be a positive integer, ''MIN'' must be given in upper-case letters. ; ''MAXn'' (parameter) : Emit only those values with at most __n__ aggregated values. __n__ must be a positive integer, ''MAX'' must be given in upper-case letters. ; ''TITLE'' (parameter) : All words following ''TITLE'' are joined together to form a title. It is used for the ''ATOM'' and ''RSS'' action. ; ''ATOM'' (aggregate) : Transform the zettel list into an [[Atom 1.0|https://www.rfc-editor.org/rfc/rfc4287]]-conformant document / feed. The document is embedded into the referencing zettel. ; ''RSS'' (aggregate) : Transform the zettel list into a [[RSS 2.0|https://www.rssboard.org/rss-specification]]-conformant document / feed. The document is embedded into the referencing zettel. ; ''KEYS'' (aggregate) : Emit a list of all metadata keys, together with the number of zettel having the key. ; Any [[metadata key|00001006020000]] of type [[Word|00001006035500]], [[WordSet|00001006036000]], or [[TagSet|00001006034000]] (aggregates) : Emit an aggregate of the given metadata key. The key can be given in any letter case[^Except if the key name collides with one of the above names. In this case use at least one lower case letter.]. Example: ```zmk {{{query:tags:#search | tags}}} ``` This is a tag cloud of all tags that are used together with the tag #search: :::example {{{query:tags:#search | tags}}} ::: |
Changes to docs/manual/00001007031200.zettel.
1 2 3 4 5 6 | id: 00001007031200 title: Zettelmarkup: Inline-Zettel Block role: manual tags: #manual #zettelmarkup #zettelstore syntax: zmk created: 20220201142439 | | | | | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 | id: 00001007031200 title: Zettelmarkup: Inline-Zettel Block role: manual tags: #manual #zettelmarkup #zettelstore syntax: zmk created: 20220201142439 modified: 20221018121251 An inline-zettel block allows to specify some content with another syntax without creating a new zettel. This is useful, for example, if you want to embed some [[Markdown|00001008010500]] content, because you are too lazy to translate Markdown into Zettelmarkup. Another example is to specify HTML code to use it for some kind of web front-end framework. As all other [[line-range blocks|00001007030000#line-range-blocks]], an inline-zettel block begins with at least three identical characters, starting at the first position of a line. For inline-zettel blocks, the at-sign character (""''@''"", U+0040) is used. You can add some [[attributes|00001007050000]] on the beginning line of a verbatim block, following the initiating characters. The inline-zettel block uses the attribute key ""syntax"" to specify the [[syntax|00001008000000]] of the inline-zettel. Alternatively, you can use the generic attribute to specify the syntax value. If no value is provided, ""[[text|00001008000000#text]]"" is assumed. Any other character in this first line will be ignored. Text following the beginning line will not be interpreted, until a line begins with at least the same number of the same at-sign characters given at the beginning line. |
︙ | ︙ |
Changes to docs/manual/00001007031300.zettel.
︙ | ︙ | |||
10 11 12 13 14 15 16 | They begin with at least three tilde characters (""''~''"", U+007E) at the first position of a line. You can add some [[attributes|00001007050000]] on the beginning line of a verbatim block, following the initiating characters. The evaluation block supports the default attribute[^Depending on the syntax value.]: when given, all spaces in the text are rendered in HTML as open box characters (U+2423). If you want to give only one attribute and this attribute is the generic attribute, you can omit the most of the attribute syntax and just specify the value. It will be interpreted as a [[syntax|00001008000000]] value to evaluate its content. Not all syntax values are supported by Zettelstore.[^Currently just ""[[draw|00001008050000]]"".] | | | 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 | They begin with at least three tilde characters (""''~''"", U+007E) at the first position of a line. You can add some [[attributes|00001007050000]] on the beginning line of a verbatim block, following the initiating characters. The evaluation block supports the default attribute[^Depending on the syntax value.]: when given, all spaces in the text are rendered in HTML as open box characters (U+2423). If you want to give only one attribute and this attribute is the generic attribute, you can omit the most of the attribute syntax and just specify the value. It will be interpreted as a [[syntax|00001008000000]] value to evaluate its content. Not all syntax values are supported by Zettelstore.[^Currently just ""[[draw|00001008050000]]"".] The main reason for an evaluation block is to be used with external software via the [[Sexpr encoding|00001012920516]]. Any other character in this line will be ignored Text following the beginning line will not be interpreted, until a line begins with at least the same number of the same characters given at the beginning line. This allows to enter some tilde characters in the text that should not be interpreted. For example: |
︙ | ︙ |
Changes to docs/manual/00001007031400.zettel.
︙ | ︙ | |||
12 13 14 15 16 17 18 | You can add some [[attributes|00001007050000]] on the beginning line of a verbatim block, following the initiating characters. A math-mode block supports the default attribute[^Depending on the syntax value.]: when given, all spaces in the text are rendered in HTML as open box characters (U+2423). If you want to give only one attribute and this attribute is the generic attribute, you can omit the most of the attribute syntax and just specify the value. It will be interpreted as a [[syntax|00001008000000]] value to evaluate its content. Alternatively, you could provide an attribute with the key ""syntax"" and use the value to specify the syntax. Not all syntax values are supported by Zettelstore.[^Currently: none.] | | | 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 | You can add some [[attributes|00001007050000]] on the beginning line of a verbatim block, following the initiating characters. A math-mode block supports the default attribute[^Depending on the syntax value.]: when given, all spaces in the text are rendered in HTML as open box characters (U+2423). If you want to give only one attribute and this attribute is the generic attribute, you can omit the most of the attribute syntax and just specify the value. It will be interpreted as a [[syntax|00001008000000]] value to evaluate its content. Alternatively, you could provide an attribute with the key ""syntax"" and use the value to specify the syntax. Not all syntax values are supported by Zettelstore.[^Currently: none.] External software might support several values via the [[Sexpr encoding|00001012920516]]. Any other character in this line will be ignored Text following the beginning line will not be interpreted, until a line begins with at least the same number of the same characters given at the beginning line. This allows to enter some dollar-sign characters in the text that should not be interpreted. For example: |
︙ | ︙ |
Changes to docs/manual/00001007040000.zettel.
1 2 3 4 5 6 | id: 00001007040000 title: Zettelmarkup: Inline-Structured Elements role: manual tags: #manual #zettelmarkup #zettelstore syntax: zmk created: 20210126175322 | | | | | | | | | 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 | id: 00001007040000 title: Zettelmarkup: Inline-Structured Elements role: manual tags: #manual #zettelmarkup #zettelstore syntax: zmk created: 20210126175322 modified: 20220920143243 Most characters you type is concerned with inline-structured elements. The content of a zettel contains is many cases just ordinary text, lightly formatted. Inline-structured elements allow to format your text and add some helpful links or images. Sometimes, you want to enter characters that have no representation on your keyboard. ; Text formatting : Every [[text formatting|00001007040100]] element begins with two same characters at the beginning. It lasts until the same two characters occurred the second time. Some of these elements explicitly support [[attributes|00001007050000]]. ; Literal-like formatting : Sometime you want to enter the text as it is. : This is the core motivation of [[literal-like formatting|00001007040200]]. ; Reference-like text : You can reference other zettel and (external) material within one zettel. This kind of reference may be a link, or an images that is display inline when the zettel is rendered. Footnotes sometimes factor out some useful text that hinders the flow of reading text. Internal marks allow to reference something within a zettel. An important aspect of all knowledge work is to reference others work, e.g. with citation keys. All these elements can be subsumed under [[reference-like text|00001007040300]]. === Other inline elements ==== Comment A comment begins with two consecutive percent sign characters (""''%''"", U+0025). It ends at the end of the line where it begins. ==== Backslash The backslash character (""''\\''"", U+005C) gives the next character another meaning. * If a space character follows, it is converted in a non-breaking space (U+00A0). * If a line ending follows the backslash character, the line break is converted from a __soft break__ into a __hard break__. * Every other character is taken as itself, but without the interpretation of a Zettelmarkup element. For example, if you want to enter a ""'']''"" into a [[footnote text|00001007040330]], you should escape it with a backslash. ==== 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}. According to the [[HTML Standard|https://html.spec.whatwg.org/multipage/syntax.html#character-references]], some numeric code points are not allowed. These are all code point below the numeric value 32 (decimal) or 0x20 (hex) and all code points for [[noncharacter|https://infra.spec.whatwg.org/#noncharacter]] values. 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 ``–``. |
Changes to docs/manual/00001007040100.zettel.
1 2 3 4 5 | id: 00001007040100 title: Zettelmarkup: Text Formatting role: manual tags: #manual #zettelmarkup #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 | id: 00001007040100 title: Zettelmarkup: Text Formatting role: manual tags: #manual #zettelmarkup #zettelstore syntax: zmk modified: 20220218131003 Text formatting is the way to make your text visually different. Every text formatting element begins with two same characters. It ends when these two same characters occur the second time. It is possible that some [[attributes|00001007050000]] follow immediately, without any separating character. Text formatting can be nested, up to a reasonable limit. The following characters begin a text formatting: * The low line character (""''_''"", U+005F) emphasizes its text. ** Example: ``abc __def__ ghi`` is rendered in HTML as: ::abc __def__ ghi::{=example}. * The asterisk character (""''*''"", U+002A) strongly emphasized its enclosed text. ** Example: ``abc **def** ghi`` is rendered in HTML as: ::abc **def** ghi::{=example}. * The greater-than sign character (""''>''"", U+003E) marks text as inserted. ** Example: ``abc >>def>> ghi`` is rendered in HTML as: ::abc >>def>> ghi::{=example}. * Similar, the tilde character (""''~''"", U+007E) marks deleted text. ** Example: ``abc ~~def~~ ghi`` is rendered in HTML as: ::abc ~~def~~ ghi::{=example}. * The circumflex accent character (""''^''"", U+005E) allows to enter super-scripted text. ** Example: ``e=mc^^2^^`` is rendered in HTML as: ::e=mc^^2^^::{=example}. * The comma character (""'',''"", U+002C) produces sub-scripted text. ** Example: ``H,,2,,O`` is rendered in HTML as: ::H,,2,,O::{=example}. * The quotation mark character (""''"''"", U+0022) marks an inline quotation, according to the [[specified language|00001007050100]]. ** Example: ``""To be or not""`` is rendered in HTML as: ::""To be or not""::{=example}. ** Example: ``""Sein oder nicht""{lang=de}`` is rendered in HTML as: ::""Sein oder nicht""{lang=de}::{=example}. * The colon character (""'':''"", U+003A) mark some text that should belong together. It fills a similar role as [[region blocks|00001007030800]], but just for inline elements. ** Example: ``abc ::def::{=example} ghi`` is rendered in HTML as: abc ::def::{=example} ghi. |
Changes to docs/manual/00001007040200.zettel.
1 2 3 4 5 | id: 00001007040200 title: Zettelmarkup: Literal-like formatting role: manual tags: #manual #zettelmarkup #zettelstore syntax: zmk | < | | 1 2 3 4 5 6 7 8 9 10 11 12 13 | id: 00001007040200 title: Zettelmarkup: Literal-like formatting role: manual tags: #manual #zettelmarkup #zettelstore syntax: zmk modified: 20220311185110 There are some reasons to mark text that should be rendered as uninterpreted: * Mark text as literal, sometimes as part of a program. * Mark text as input you give into a computer via a keyboard. * Mark text as output from some computer, e.g. shown at the command line. === Literal text |
︙ | ︙ | |||
43 44 45 46 47 48 49 50 51 52 | Examples: * ``==The result is: 42==`` renders in HTML as ::==The result is: 42==::{=example}. * ``==The result is: 42=={-}`` renders in HTML as ::==The result is: 42=={-}::{=example}. Attributes can be specified, the default attribute has the same semantic as for literal text. === Math mode / $$\TeX$$ input This allows to enter text, that is typically interpreted by $$\TeX$$ or similar software. The main difference to all other literal-like formatting above is that the backslash character (""''\\''"", U+005C) has no special meaning. | > > > > > > > > > > > > > > | | 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 | Examples: * ``==The result is: 42==`` renders in HTML as ::==The result is: 42==::{=example}. * ``==The result is: 42=={-}`` renders in HTML as ::==The result is: 42=={-}::{=example}. Attributes can be specified, the default attribute has the same semantic as for literal text. === Inline-zettel snippet To specify an inline snippet in a different [[syntax|00001008000000]], delimit your text with two at-sign characters (""''@''"", U+0040) on each side. You can add some [[attributes|00001007050000]] immediate after the two closing at-sign characters to specify the syntax to use. Either use the attribute key ""syntax"" or use the generic attribute to specify the syntax value. If no value is provided, ""[[text|00001008000000#text]]"" is assumed. Examples: * ``A @@-->@@ B`` renders in HTML as ::A @@-->@@ B::{=example}. * ``@@<small>@@{=html}Small@@</small>@@{=html}`` renders in HTML as ::@@<small>@@{=html}Small@@</small>@@{=html}::{=example}. To some degree, an inline-zettel snippet is the @@<small>@@{=html}smaller@@</small>@@{=html} sibling of the [[inline-zettel block|00001007031200]]. For HTML syntax, the same rules apply. === Math mode / $$\TeX$$ input This allows to enter text, that is typically interpreted by $$\TeX$$ or similar software. The main difference to all other literal-like formatting above is that the backslash character (""''\\''"", U+005C) has no special meaning. Therefore it is well suited the enter text with a lot of backslash characters. Math mode text is delimited with two dollar signs (""''$''"", U+0024) on each side. You can add some [[attributes|00001007050000]] immediate after the two closing at-sign characters to specify the syntax to use. Either use the attribute key ""syntax"" or use the generic attribute to specify the syntax value. If no syntax value is provided, math mode text roughly corresponds to literal text. |
︙ | ︙ |
Changes to docs/manual/00001007040300.zettel.
1 2 3 4 5 | id: 00001007040300 title: Zettelmarkup: Reference-like text role: manual tags: #manual #zettelmarkup #zettelstore syntax: zmk | < | 1 2 3 4 5 6 7 8 9 10 11 12 | id: 00001007040300 title: Zettelmarkup: Reference-like text role: manual tags: #manual #zettelmarkup #zettelstore syntax: zmk modified: 20210810172531 An important aspect of knowledge work is to interconnect your zettel as well as provide links to (external) material. There are several kinds of references that are allowed in Zettelmarkup: * [[Links to other zettel or to (external) material|00001007040310]] * [[Embedded zettel or (external) material|00001007040320]] (""inline transclusion"") |
︙ | ︙ |
Changes to docs/manual/00001007040310.zettel.
1 2 3 4 5 6 | id: 00001007040310 title: Zettelmarkup: Links role: manual tags: #manual #zettelmarkup #zettelstore syntax: zmk created: 20210810155955 | | | | | | | 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 | id: 00001007040310 title: Zettelmarkup: Links role: manual tags: #manual #zettelmarkup #zettelstore syntax: zmk created: 20210810155955 modified: 20221024173849 There are two kinds of links, regardless of links to (internal) other zettel or to (external) material. Both kinds begin with two consecutive left square bracket characters (""''[''"", U+005B) and ends with two consecutive right square bracket characters (""'']''"", U+005D). If the content starts with more than two left square bracket characters, all but the last two will be treated as text. The first form provides some text plus the link specification, delimited by a vertical bar character (""''|''"", U+007C): ``[[text|linkspecification]]``. The text is a sequence of [[inline elements|00001007040000]]. However, it should not contain links itself. The second form just provides a link specification between the square brackets. Its text is derived from the link specification, e.g. by interpreting the link specification as text: ``[[linkspecification]]``. === Link specifications The link specification for another zettel within the same Zettelstore is just the [[zettel identifier|00001006050000]]. To reference some content within a zettel, you can append a number sign character (""''#''"", U+0023) and the name of the mark to the zettel identifier. The resulting reference is called ""zettel reference"". If the link specification begins with the string ''query:'', the text following this string will be interpreted as a [[query expression|00001007700000]]. The resulting reference is called ""query reference"". When this type of references is rendered, it will typically reference a list of all zettel that fulfills the query expression. A link specification starting with one slash character (""''/''"", U+002F), or one or two full stop characters (""''.''"", U+002E) followed by a slash character, will be interpreted as a local reference, called __hosted reference__. Such references will be interpreted relative to the web server hosting the Zettelstore. If a link specification begins with two slash characters (called __based reference__), it will be interpreted relative to the value of [[''url-prefix''|00001004010000#url-prefix]]. To specify some material outside the Zettelstore, just use an normal Uniform Resource Identifier (URI) as defined by [[RFC\ 3986|https://tools.ietf.org/html/rfc3986]]. === Other topics If the link references another zettel, and this zettel is not readable for the current user, because of a missing access rights, then only the associated text is presented. |
Changes to docs/manual/00001007040320.zettel.
1 2 3 4 5 6 | id: 00001007040320 title: Zettelmarkup: Inline Embedding / Transclusion role: manual tags: #manual #zettelmarkup #zettelstore syntax: zmk created: 20210810155955 | | | | | 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: 00001007040320 title: Zettelmarkup: Inline Embedding / Transclusion role: manual tags: #manual #zettelmarkup #zettelstore syntax: zmk created: 20210810155955 modified: 20221024173926 To some degree, an specification for embedded material is conceptually not too far away from a specification for [[linked material|00001007040310]]. Both contain a reference specification and optionally some text. In contrast to a link, the specification of embedded material must currently resolve to some kind of real content. This content replaces the embed specification. An embed specification begins with two consecutive left curly bracket characters (""''{''"", U+007B) and ends with two consecutive right curly bracket characters (""''}''"", U+007D). The curly brackets delimits either a reference specification or some text, a vertical bar character and the link specification, similar to a link. If the content starts with more than two left curly bracket characters, all but the last two will be treated as text. One difference to a link: if the text was not given, an empty string is assumed. The reference must point to some content, either zettel content or URL-referenced content. If the current user is not allowed to read the referenced zettel, the inline transclusion / embedding is ignored. If the referenced zettel does not exist, or is not readable because of other reasons, a [[spinning emoji|00000000040001]] is presented as a visual hint: Example: ``{{00000000000000}}`` will be rendered as ::{{00000000000000}}::{=example}. |
︙ | ︙ |
Changes to docs/manual/00001007040322.zettel.
1 2 3 4 5 6 | id: 00001007040322 title: Zettelmarkup: Image Embedding role: manual tags: #manual #zettelmarkup #zettelstore syntax: zmk created: 20210811154251 | | | | < | | 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 | id: 00001007040322 title: Zettelmarkup: Image Embedding role: manual tags: #manual #zettelmarkup #zettelstore syntax: zmk created: 20210811154251 modified: 20221112111054 Image content is assumed, if an URL is used or if the referenced zettel contains an image. Supported formats are: * Portable Network Graphics (""PNG""), as defined by [[RFC\ 2083|https://tools.ietf.org/html/rfc2083]]. * Graphics Interchange Format (""GIF"), as defined by [[https://www.w3.org/Graphics/GIF/spec-gif89a.txt]]. * JPEG / JPG, defined by the __Joint Photographic Experts Group__. * Scalable Vector Graphics (SVG), defined by [[https://www.w3.org/Graphics/SVG/]] * WebP, defined by [[Google|https://developers.google.com/speed/webp]] If the text is given, it will be interpreted as an alternative textual representation, to help persons with some visual disabilities. [[Attributes|00001007050000]] are supported. They must follow the last right curly bracket character immediately. One prominent example is to specify an explicit title attribute that is shown on certain web browsers when the zettel is rendered in HTML: Examples: * [!spin|``{{Spinning Emoji|00000000040001}}{title=Emoji width=30}``] is rendered as ::{{Spinning Emoji|00000000040001}}{title=Emoji width=30}::{=example}. * The above image is also the placeholder for a non-existent zettel: ** ``{{00000000009999}}`` will be rendered as ::{{00000000009999}}::{=example}. |
Changes to docs/manual/00001007040324.zettel.
1 2 3 4 5 6 | id: 00001007040324 title: Zettelmarkup: Inline-mode Transclusion role: manual tags: #manual #zettelmarkup #zettelstore syntax: zmk created: 20210811154251 | | | | | | | 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 | id: 00001007040324 title: Zettelmarkup: Inline-mode Transclusion role: manual tags: #manual #zettelmarkup #zettelstore syntax: zmk created: 20210811154251 modified: 20221116165428 Inline-mode transclusion applies to all zettel that are parsed in a non-trivial way, e.g. as structured textual content. For example, textual content is assumed if the [[syntax|00001006020000#syntax]] of a zettel is ""zmk"" ([[Zettelmarkup|00001007000000]]), or ""markdown"" / ""md"" ([[Markdown|00001008010000]]). Since this type of transclusion is at the level of [[inline-structured elements|00001007040000]], the transclude specification must be replaced with some inline-structured elements. First, the referenced zettel is read. If it contains other transclusions, these will be expanded, recursively. When an endless recursion is detected, expansion does not take place. Instead an error message replaces the transclude specification. The result of this (indirect) transclusion is searched for inline-structured elements. * If only an [[zettel identifier|00001006050000]] was specified, the first top-level [[paragraph|00001007030000#paragraphs]] is used. Since a paragraph is basically a sequence of inline-structured elements, these elements will replace the transclude specification. Example: ``{{00010000000000}}`` (see [[00010000000000]]) is rendered as ::{{00010000000000}}::{=example}. * If a fragment identifier was additionally specified, the element with the given fragment is searched: ** If it specifies a [[heading|00001007030300]], the next top-level paragraph is used. Example: ``{{00010000000000#reporting-errors}}`` is rendered as ::{{00010000000000#reporting-errors}}::{=example}. ** In case the fragment names a [[mark|00001007040350]], the inline-structured elements after the mark are used. Initial spaces and line breaks are ignored in this case. Example: ``{{00001007040322#spin}}`` is rendered as ::{{00001007040322#spin}}::{=example}. ** Just specifying the fragment identifier will reference something in the current page. This is not allowed, to prevent a possible endless recursion. * If the reference is a [[hosted or based|00001007040310#link-specifications]] link / URL to an image, that image will be rendered. Example: ``{{//z/00000000040001}}`` is rendered as ::{{//z/00000000040001}}::{=example} If no inline-structured elements are found, the transclude specification is replaced by an error message. To avoid an exploding ""transclusion bomb"", a form of a [[billion laughs attack|https://en.wikipedia.org/wiki/Billion_laughs_attack]] (also known as ""XML bomb""), the total number of transclusions / expansions is limited. The limit can be controlled by setting the value [[''max-transclusions''|00001004020000#max-transclusions]] of the runtime configuration zettel. === See also [[Full transclusion|00001007031100]] does not work inside some text, but is used for [[block-structured elements|00001007030000]]. |
Changes to docs/manual/00001007040330.zettel.
1 2 3 4 5 | id: 00001007040330 title: Zettelmarkup: Footnotes role: manual tags: #manual #zettelmarkup #zettelstore syntax: zmk | < | 1 2 3 4 5 6 7 8 9 10 11 12 | id: 00001007040330 title: Zettelmarkup: Footnotes role: manual tags: #manual #zettelmarkup #zettelstore syntax: zmk modified: 20220218130100 A footnote begins with a left square bracket, followed by a circumflex accent (""''^''"", U+005E), followed by some text, and ends with a right square bracket. Example: ``Main text[^Footnote text.].`` is rendered in HTML as: ::Main text[^Footnote text.].::{=example}. |
Changes to docs/manual/00001007040340.zettel.
1 2 3 4 5 | id: 00001007040340 title: Zettelmarkup: Citation Key role: manual tags: #manual #zettelmarkup #zettelstore syntax: zmk | < | | | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 | id: 00001007040340 title: Zettelmarkup: Citation Key role: manual tags: #manual #zettelmarkup #zettelstore syntax: zmk modified: 20220218133447 A citation key references some external material that is part of a bibliographical collection. Currently, Zettelstore implements this only partially, it is ""work in progress"". However, the syntax is: beginning with a left square bracket and followed by an at sign character (""''@''"", U+0040), a the citation key is given. The key is typically a sequence of letters and digits. If a comma character (""'',''"", U+002C) or a vertical bar character is given, the following is interpreted as [[inline elements|00001007040000]]. A right square bracket ends the text and the citation key element. |
Changes to docs/manual/00001007040350.zettel.
1 2 3 4 5 | id: 00001007040350 title: Zettelmarkup: Mark role: manual tags: #manual #zettelmarkup #zettelstore syntax: zmk | < | 1 2 3 4 5 6 7 8 9 10 11 12 | id: 00001007040350 title: Zettelmarkup: Mark role: manual tags: #manual #zettelmarkup #zettelstore syntax: zmk modified: 20220218133206 A mark allows to name a point within a zettel. This is useful if you want to reference some content in a zettel, either with a [[link|00001007040310]] or with an [[inline-mode transclusion|00001007040324]]. A mark begins with a left square bracket, followed by an exclamation mark character (""''!''"", U+0021). Now the optional mark name follows. |
︙ | ︙ |
Changes to docs/manual/00001007050000.zettel.
1 2 3 4 5 | id: 00001007050000 title: Zettelmarkup: Attributes role: manual tags: #manual #zettelmarkup #zettelstore syntax: zmk | < | 1 2 3 4 5 6 7 8 9 10 11 12 | id: 00001007050000 title: Zettelmarkup: Attributes role: manual tags: #manual #zettelmarkup #zettelstore syntax: zmk modified: 20220630194106 Attributes allows to modify the way how material is presented. Alternatively, they provide additional information to markup elements. To some degree, attributes are similar to [[HTML attributes|https://html.spec.whatwg.org/multipage/dom.html#global-attributes]]. Typical use cases for attributes are to specify the (natural) [[language|00001007050100]] for a text region, to specify the [[programming language|00001007050200]] for highlighting program code, or to make white space visible in plain text. |
︙ | ︙ |
Changes to docs/manual/00001007050200.zettel.
1 2 3 4 5 | id: 00001007050200 title: Zettelmarkup: Supported Attribute Values for Programming Languages tags: #manual #reference #zettelmarkup #zettelstore syntax: zmk role: manual | < | 1 2 3 4 5 6 7 | id: 00001007050200 title: Zettelmarkup: Supported Attribute Values for Programming Languages tags: #manual #reference #zettelmarkup #zettelstore syntax: zmk role: manual TBD |
Changes to docs/manual/00001007700000.zettel.
1 | id: 00001007700000 | | | | | | > | > > | > | | | | > > > | | | | < | > > > > > > > > > > > > > | 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 | id: 00001007700000 title: Query expression role: manual tags: #manual #search #zettelstore syntax: zmk created: 20220805150154 modified: 20220913135434 A query expression allows you to search for specific zettel and to perform some actions on them. You may select zettel based on a full-text search, based on specific metadata values, or both. A query expression consists of a __search expression__ and of an optional __action list__. Both are separated by a vertical bar character (""''|''"", U+007C). A query expression follows a [[formal syntax|00001007780000]]. === Search expression In its simplest form, a search expression just contains a string to be search for with the help of a full-text search. For example, the string ''syntax'' will search for all zettel containing the word ""syntax"". If you want to search for all zettel with a title containing the word ""syntax"", you must specify ''title:syntax''. ""title"" names the [[metadata key|00001006010000]], in this case the [[supported metadata key ""title""|00001006020000#title]]. The colon character (""'':''"") is a [[search operator|00001007705000]], in this example to specify a match. ""syntax"" is the [[search value|00001007706000]] that must match to the value of the given metadata key, here ""title"". A search expression may contain more than one search term, such as ''title:syntax''. Search terms must be separated by one or more space characters, for example ''title:syntax title:search''. All terms of a select expression must be true so that a zettel is selected. * [[Search terms|00001007702000]] * [[Search operator|00001007705000]] * [[Search value|00001007706000]] Here are [[some examples|00001007790000]] of search expressions, which can be used to manage a Zettelstore: {{{00001007790000}}} === Action List With a search expression, a list of zettel is selected. Actions allow to modify this list to a certain degree. Which actions are allowed depends on the context. However, actions are further separated into __parameter action__ and __aggregate actions__. A parameter action just sets a parameter for an aggregate action. An aggregate action transforms the list of selected zettel into a different, aggregate form. Only the first aggregate form is executed, following aggregate actions are ignored. In most contexts, valid actions include the name of metadata keys, at least of type [[Word|00001006035500]], [[WordSet|00001006036000]], or [[TagSet|00001006034000]]. |
Deleted docs/manual/00001007701000.zettel.
|
| < < < < < < < < < < < < < < < < < < < < < < < < < < |
Changes to docs/manual/00001007702000.zettel.
1 2 3 4 5 6 | id: 00001007702000 title: Search term role: manual tags: #manual #search #zettelstore syntax: zmk created: 20220805150154 | | | < < < < | | | | | 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 | id: 00001007702000 title: Search term role: manual tags: #manual #search #zettelstore syntax: zmk created: 20220805150154 modified: 20230327123724 A search term allows you to specify one search restriction. The result [[search expression|00001007700000]], which contains more than one search term, will be the applications of all restrictions. A search term can be one of the following (the first three term are collectively called __search literals__): * A metadata-based search, by specifying the name of a [[metadata key|00001006010000]], followed by a [[search operator|00001007705000]], followed by an optional [[search value|00001007706000]]. All zettel containing the given metadata key with a allowed value (depending on the search operator) are selected. If no search value is given, then all zettel containing the given metadata key are selected (or ignored, for a negated search operator). * An optional [[search operator|00001007705000]], followed by a [[search value|00001007706000]]. This specifies a full-text search for the given search value. **Note:** the search value will be normalized according to Unicode NKFD, ignoring everything except letters and numbers. Therefore, the following search expression are essentially the same: ''"search syntax"'' and ''search syntax''. The first is a search expression with one search value, which is normalized to two strings to be searched for. The second is a search expression containing two search values, giving two string to be searched for. * A metadata key followed by ""''?''"" or ""''!?''"". Is true, if zettel metadata contains / does not contain the given key. * The string ''OR'' signals that following search literals may occur alternatively in the result. Since search literals may be negated, it is possible to form any boolean search expression. Any search expression will be in a [[disjunctive normal form|https://en.wikipedia.org/wiki/Disjunctive_normal_form]]. It has no effect on the following search terms initiated with a special uppercase word. * The string ''PICK'', followed by a non-empty sequence of spaces and a number greater zero (called ""N""). This will pick randomly N elements of the result list. A zero value of N will produce the same result as if nothing was specified. If specified multiple times, the lower value takes precedence. Example: ''PICK 5 PICK 3'' will be interpreted as ''PICK 3''. * The string ''ORDER'', followed by a non-empty sequence of spaces and the name of a metadata key, will specify an ordering of the result list. If you include the string ''REVERSE'' after ''ORDER'' but before the metadata key, the ordering will be reversed. Example: ''ORDER published'' will order the resulting list based on the publishing data, while ''ORDER REVERSED published'' will return a reversed result order. An explicit order field will take precedence over the random order described below. If no random order is effective, a ``ORDER REVERSE id`` will be added. This makes the sort stable. Example: ``ORDER created`` will be interpreted as ``ORDER created ORDER REVERSE id``. Any ordering by zettel identifier will make following order terms to be ignored. Example: ``ORDER id ORDER created`` will be interpreted as ``ORDER id``. * The string ''RANDOM'' will provide a random order of the resulting list. Currently, only the first term specifying the order of the resulting list will be used. Other ordering terms will be ignored. A random order specification will be ignored, if there is an explicit ordering given or if the specification contains a valid ''PICK''. Example: ''RANDOM ORDER published'' will be interpreted as ''ORDER published'', ''PICK 3 RANDOM'' will be interpreted as ''PICK 3''. * The string ''OFFSET'', followed by a non-empty sequence of spaces and a number greater zero (called ""N""). This will ignore the first N elements of the result list, based on the specified sort order. A zero value of N will produce the same result as if nothing was specified. If specified multiple times, the higher value takes precedence. Example: ''OFFSET 4 OFFSET 8'' will be interpreted as ''OFFSET 8''. |
︙ | ︙ | |||
85 86 87 88 89 90 91 | This is resolved by the following rule: * A search term containing no [[search operator character|00001007705000]] is treated as a full-text search. * The first search operator character found in a search term divides the term into two pieces. If the first piece, from the beginning of the search term to the search operator character, is syntactically a metadata key, the search term is treated as a metadata-based search. * Otherwise, the search term is treated as a full-text search. If a term like ''PICK'', ''ORDER'', ''ORDER REVERSE'', ''OFFSET'', or ''LIMIT'' is not followed by an appropriate value, it is interpreted as a search value for a full-text search. | | > > > > | 81 82 83 84 85 86 87 88 89 90 91 92 | This is resolved by the following rule: * A search term containing no [[search operator character|00001007705000]] is treated as a full-text search. * The first search operator character found in a search term divides the term into two pieces. If the first piece, from the beginning of the search term to the search operator character, is syntactically a metadata key, the search term is treated as a metadata-based search. * Otherwise, the search term is treated as a full-text search. If a term like ''PICK'', ''ORDER'', ''ORDER REVERSE'', ''OFFSET'', or ''LIMIT'' is not followed by an appropriate value, it is interpreted as a search value for a full-text search. For example, ''ORDER 123'' will search for a zettel conatining the strings ""ORDER"" (case-insensitive) and ""123"". A specification ''RANDOM LIMIT n'' (with some valid value of ''n'') will be interpreted as ''PICK n''. Similar, a specification ''PICK n'', where ''n'' is greater or equal to the length of the result list, will be interpreted as ''RANDOM''. A specification ''LIMIT n'', where ''n'' is greater or equal to the length of the result list, will be ignored effectively. |
Changes to docs/manual/00001007705000.zettel.
1 2 3 4 5 | id: 00001007705000 title: Search operator role: manual tags: #manual #search #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: 00001007705000 title: Search operator role: manual tags: #manual #search #zettelstore syntax: zmk modified: 20220819194709 A search operator specifies how the comparison of a search value and a zettel should be executed. Every comparison is done case-insensitive, treating all uppercase letters the same as lowercase letters. The following are allowed search operator characters: * The exclamation mark character (""!"", U+0021) negates the meaning * The tilde character (""''~''"", U+007E) compares on matching (""match operator"") * The greater-than sign character (""''>''"", U+003E) matches if there is some prefix (""prefix operator"") * The less-than sign character (""''<''"", U+003C) compares a suffix relationship (""suffix operator"") * The colon character (""'':''"", U+003A) compares on equal words (""has operator"") * The question mark (""''?''"", U+003F) checks for an existing metadata key (""exist operator"") Since the exclamation mark character can be combined with the other, there are 10 possible combinations: # ""''!''"": is an abbreviation of the ""''!~''"" operator. # ""''~''"": is successful if the search value matched the value to be compared. # ""''!~''"": is successful if the search value does not match the value to be compared. # ""'':''"": is successful if the search value is equal to one word of the value to be compared. # ""''!:''"": is successful if the search value is not equal to any word of the value to be compared. # ""''>''"": is successful if the search value is a prefix of the value to be compared. # ""''!>''"": is successful if the search value is not a prefix of the value to be compared. # ""''<''"": is successful if the search value is a suffix of the value to be compared. # ""''!<''"": is successful if the search value is not a suffix of the value to be compared. # ""''?''"": is successful if the metadata contains the given key. # ""''!?''"": is successful if the metadata does not contain the given key. # ""''''"": a missing search operator can only occur for a full-text search. It is equal to the ""''~''"" operator. |
Changes to docs/manual/00001007706000.zettel.
1 2 3 4 5 | id: 00001007706000 title: Search value role: manual tags: #manual #search #zettelstore syntax: zmk | < | 1 2 3 4 5 6 7 8 9 10 | id: 00001007706000 title: Search value role: manual tags: #manual #search #zettelstore syntax: zmk modified: 20220807162031 A search value specifies a value to be searched for, depending on the [[search operator|00001007705000]]. A search value should be lower case, because all comparisons are done in a case-insensitive way and there are some upper case keywords planned. |
Deleted docs/manual/00001007710000.zettel.
|
| < < < < < < < < < < < < < < < < < |
Deleted docs/manual/00001007720000.zettel.
|
| < < < < < < < < < < < < < < < < < < < |
Deleted docs/manual/00001007720300.zettel.
|
| < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < |
Deleted docs/manual/00001007720600.zettel.
|
| < < < < < < < < < < < < |
Deleted docs/manual/00001007720900.zettel.
|
| < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < |
Deleted docs/manual/00001007721200.zettel.
|
| < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < |
Deleted docs/manual/00001007770000.zettel.
|
| < < < < < < < < < < < < < < < < < < < < < |
Changes to docs/manual/00001007780000.zettel.
1 2 3 4 5 6 | id: 00001007780000 title: Formal syntax of query expressions role: manual tags: #manual #reference #search #zettelstore syntax: zmk created: 20220810144539 | | | < < < < < < < < < < < < < < < < < | | | | | | | | | | | | | | | | | | | < < < < < < < | | 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: 00001007780000 title: Formal syntax of query expressions role: manual tags: #manual #reference #search #zettelstore syntax: zmk created: 20220810144539 modified: 20230327122634 ``` QueryExpression := SearchExpression ActionExpression? SearchExpression := SearchTerm (SPACE+ SearchTerm)*. SearchTerm := SearchOperator? SearchValue | SearchKey SearchOperator SearchValue? | SearchKey ExistOperator | "OR" | "RANDOM" | "PICK" SPACE+ PosInt | "ORDER" SPACE+ ("REVERSE" SPACE+)? SearchKey | "OFFSET" SPACE+ PosInt | "LIMIT" SPACE+ PosInt. SearchValue := Word. SearchKey := MetadataKey. SearchOperator := '!' | ('!')? ('~' | ':' | '<' | '>'). ExistOperator := '?' | '!' '?'. PosInt := '0' | ('1' .. '9') DIGIT*. ActionExpression := '|' (Word (SPACE+ Word)*)? Word := NO-SPACE NO-SPACE* ``` |
Changes to docs/manual/00001007790000.zettel.
1 2 3 4 5 6 | id: 00001007790000 title: Useful query expressions role: manual tags: #example #manual #search #zettelstore syntax: zmk created: 20220810144539 | | | < < < | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 | id: 00001007790000 title: Useful query expressions role: manual tags: #example #manual #search #zettelstore syntax: zmk created: 20220810144539 modified: 20230327122547 |= Query Expression |= Meaning | [[query:role:configuration]] | Zettel that contains some configuration data for the Zettelstore | [[query:ORDER REVERSE created LIMIT 40]] | 40 recently created zettel | [[query:ORDER REVERSE published LIMIT 40]] | 40 recently updated zettel | [[query:PICK 40]] | 40 random zettel | [[query:dead?]] | Zettel with invalid / dead links | [[query:backward!? precursor!?]] | Zettel that are not referenced by other zettel | [[query:tags!?]] | Zettel without tags |
Changes to docs/manual/00001007800000.zettel.
1 2 3 4 5 | id: 00001007800000 title: Zettelmarkup: Summary of Formatting Characters role: manual tags: #manual #reference #zettelmarkup #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 | id: 00001007800000 title: Zettelmarkup: Summary of Formatting Characters role: manual tags: #manual #reference #zettelmarkup #zettelstore syntax: zmk modified: 20220810095559 The following table gives an overview about the use of all characters that begin a markup element. |= Character :|= [[Blocks|00001007030000]] <|= [[Inlines|00001007040000]] < | ''!'' | (free) | (free) | ''"'' | [[Verse block|00001007030700]] | [[Short inline quote|00001007040100]] | ''#'' | [[Ordered list|00001007030200]] | [[Tag|00001007040000]] | ''$'' | (reserved) | (reserved) | ''%'' | [[Comment block|00001007030900]] | [[Comment|00001007040000]] | ''&'' | (free) | [[Entity|00001007040000]] | ''\''' | (free) | [[Computer input|00001007040200]] | ''('' | (free) | (free) | '')'' | (free) | (free) | ''*'' | [[Unordered list|00001007030200]] | [[strongly emphasized text|00001007040100]] | ''+'' | (free) | (free) | '','' | (free) | [[Sub-scripted text|00001007040100]] | ''-'' | [[Horizontal rule|00001007030400]] | ""[[en-dash|00001007040000]]"" | ''.'' | (free) | (free) | ''/'' | (free) | (free) | '':'' | [[Region block|00001007030800]] / [[description text|00001007030100]] | [[Inline region|00001007040100]] | '';'' | [[Description term|00001007030100]] | (free) | ''<'' | [[Quotation block|00001007030600]] | (free) | ''='' | [[Headings|00001007030300]] | [[Computer output|00001007040200]] | ''>'' | [[Quotation lists|00001007030200]] | [[Inserted text|00001007040100]] | ''?'' | (free) | (free) | ''@'' | [[Inline-Zettel block|00001007031200]] | [[Inline-zettel snippet|00001007040200#inline-zettel-snippet]] | ''['' | (reserved) | [[Linked material|00001007040300]], [[citation key|00001007040300]], [[footnote|00001007040300]], [[mark|00001007040300]] | ''\\'' | (blocked by inline meaning) | [[Escape character|00001007040000]] | '']'' | (reserved) | End of [[link|00001007040300]], [[citation key|00001007040300]], [[footnote|00001007040300]], [[mark|00001007040300]] | ''^'' | (free) | [[Super-scripted text|00001007040100]] | ''_'' | (free) | [[Emphasized text|00001007040100]] | ''`'' | [[Verbatim block|00001007030500]] | [[Literal text|00001007040200]] | ''{'' | [[Transclusion|00001007031100]] | [[Embedded material|00001007040300]], [[Attribute|00001007050000]] |
︙ | ︙ |
Changes to docs/manual/00001007903000.zettel.
1 2 3 4 5 6 | id: 00001007903000 title: Zettelmarkup: First Steps role: manual tags: #manual #tutorial #zettelmarkup #zettelstore syntax: zmk created: 20220810182917 | | | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 | id: 00001007903000 title: Zettelmarkup: First Steps role: manual tags: #manual #tutorial #zettelmarkup #zettelstore syntax: zmk created: 20220810182917 modified: 20220926183359 [[Zettelmarkup|00001007000000]] allows you to leave your text as it is, at least in many situations. Some characters have a special meaning, but you have to enter them is a defined way to see a visible change. Zettelmarkup is designed to be used for zettel, which are relatively short. It allows to produce longer texts, but you should probably use a different tool, if you want to produce an scientific paper, to name an example. === Paragraphs |
︙ | ︙ | |||
27 28 29 30 31 32 33 | | ''An __emphasized__ word'' | An __emphasized__ word | Put two underscore characters before and after the text you want to emphasize | ''Someone uses **bold** text'' | Someone uses **bold** text | Put two asterisks before and after the text you want to see bold | ''He says: ""I love you!""'' | Her says: ""I love you!"" | Put two quotation mark characters before and after the text you want to quote. You probably see a principle. One nice thing about the quotation mark characters: they are rendered according to the current language. | | | 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 | | ''An __emphasized__ word'' | An __emphasized__ word | Put two underscore characters before and after the text you want to emphasize | ''Someone uses **bold** text'' | Someone uses **bold** text | Put two asterisks before and after the text you want to see bold | ''He says: ""I love you!""'' | Her says: ""I love you!"" | Put two quotation mark characters before and after the text you want to quote. You probably see a principle. One nice thing about the quotation mark characters: they are rendered according to the current language. Examples: ""english""{lang=en}, ""french""{lang=fr}, ""german""{lang=de}, ""finnish""{lang=fi}. You will see later, how to change the current language. === Lists Quite often, text consists of lists. Zettelmarkup supports different types of lists. The most important lists are: * Unnumbered lists, |
︙ | ︙ |
Changes to docs/manual/00001007906000.zettel.
1 2 3 4 5 6 | id: 00001007906000 title: Zettelmarkup: Second Steps role: manual tags: #manual #tutorial #zettelmarkup #zettelstore syntax: zmk created: 20220811115501 | | | | 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 | id: 00001007906000 title: Zettelmarkup: Second Steps role: manual tags: #manual #tutorial #zettelmarkup #zettelstore syntax: zmk created: 20220811115501 modified: 20220926183427 After you have [[learned|00001007903000]] the basic concepts and markup of Zettelmarkup (paragraphs, emphasized text, and lists), this zettel introduces you into the concepts of links, thematic breaks, and headings. === Links A Zettelstore is much more useful, if you connect related zettel. If you read a zettel later, this allows you to know about the context of a zettel. [[Zettelmarkup|00001007000000]] allows you to specify such a connection. A connection can be specified within a paragraph via [[Links|00001007040310]]. * A link always starts with two left square bracket characters and ends with two right square bracket characters: ''[[...]]''. * Within these character sequences you specify the [[zettel identifier|00001006050000]] of the zettel you want to reference: ''[[00001007903000]]'' will connect to zettel containing the first steps into Zettelmarkup. * In addition, you should give the link a more readable description. This is done by prepending the description before the reference and use the vertical bar character to separate both: ''[[First Steps|00001007903000]]''. You are not restricted to reference your zettel. Alternatively, you might specify an URL of an external website: ''[[Zettelstore|https://zettelstore.de]]''. Of course, if you just want to specify the URL, you are allowed to omit the description: ''[[https://zettelstore.de]]'' |= Zettelmarkup | Rendered output | Remark | ''[[00001007903000]]'' | [[00001007903000]] | If no description is given, the zettel identifier acts as a description | ''[[First Steps|00001007903000]]'' | [[First Steps|00001007903000]] | The description should be chosen so that you are not confused later | ''[[https://zettelstore.de]]'' | [[https://zettelstore.de]] | A link to an external URL is rendered differently | ''[[Zettelstore|https://zettelstore.de]]'' | [[Zettelstore|https://zettelstore.de]] | You can use any URL your browser is able to support |
︙ | ︙ |
Changes to docs/manual/00001007990000.zettel.
1 2 3 4 5 6 | id: 00001007990000 title: Zettelmarkup: Cheat Sheet role: manual tags: #manual #reference #zettelmarkup syntax: zmk created: 20221209191905 | | | | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 | id: 00001007990000 title: Zettelmarkup: Cheat Sheet role: manual tags: #manual #reference #zettelmarkup syntax: zmk created: 20221209191905 modified: 20221209193310 === Overview This Zettelmarkup cheat sheet provides a quick overview of many Zettelmarkup elements. It can not cover any special case. If you need more information about any of these elements, please refer to the detailed description. === Basic Syntax |[[Text formatting|00001007040100]]|''__italic text__'' → __italic text__, ''**bold text**'' → **bold text**, ''""quoted text""'' → ""quoted text"" |[[Text editing|00001007040100]]|''>>inserted text>>'' → >>inserted text>>, ''~~deleted text~~'' → ~~deleted text~~ |[[Text literal formatting|00001007040200]]|''\'\'entered text\'\''' → ''entered text'', ''``source code``'' → ``source code``, ''==text output=='' → ==text output== |[[Superscript, subscript|00001007040100]]|''m^^2^^'' → m^^2^^, ''H,,2,,O'' → H,,2,,O |[[Links to other zettel|00001007040310]]|''[[Link text|00001007990000]]'' → [[Link text|00001007990000]] |[[Links to external resources|00001007040310]]|''[[Zettelstore|https://zettelstore.de]]'' → [[Zettelstore|https://zettelstore.de]] |[[Embed an image|00001007040322]]|''{{Image text|00000000040001}}'' → {{Image text|00000000040001}} |[[Embed content of first paragraph|00001007040324]]|''{{00001007990000}}'' → {{00001007990000}} |
︙ | ︙ |
Changes to docs/manual/00001008000000.zettel.
1 2 3 4 5 6 | id: 00001008000000 title: Other Markup Languages role: manual tags: #manual #zettelstore syntax: zmk created: 20210126175300 | | | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 | id: 00001008000000 title: Other Markup Languages role: manual tags: #manual #zettelstore syntax: zmk created: 20210126175300 modified: 20221112111148 [[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 |
︙ | ︙ | |||
37 38 39 40 41 42 43 44 45 46 47 48 49 50 | For security reasons, equivocal elements will not be encoded in the HTML format / web user interface. The ``< script ...>`` tag is an example. 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|00001008010500]] parser is used. See [[Use Markdown within Zettelstore|00001008010000]]. ; [!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''] : [[Scalable Vector Graphics|https://www.w3.org/TR/SVG2/]]. | > > < < < | 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 | For security reasons, equivocal elements will not be encoded in the HTML format / web user interface. The ``< script ...>`` tag is an example. 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|00001008010500]] 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''] : [[Scalable Vector Graphics|https://www.w3.org/TR/SVG2/]]. ; [!text|''text''], [!plain|''plain''], [!txt|''txt''] : Plain text that must not be interpreted further. ; [!zmk|''zmk''] : [[Zettelmarkup|00001007000000]]. The actual values are also listed in a zettel named [[Zettelstore Supported Parser|00000000000092]]. If you specify something else, your content will be interpreted as plain text. |
Changes to docs/manual/00001008010500.zettel.
1 2 3 4 5 6 | id: 00001008010500 title: CommonMark role: manual tags: #manual #markdown #zettelstore syntax: zmk created: 20220113183435 | | | < | < < | | | | > > | 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: 00001008010500 title: CommonMark role: manual tags: #manual #markdown #zettelstore syntax: zmk created: 20220113183435 modified: 20221018123145 url: https://commonmark.org/ [[CommonMark|https://commonmark.org/]] is a Markdown dialect, an [[attempt|https://xkcd.com/927/]] to unify all the different, divergent dialects of Markdown by providing an unambiguous syntax specification for Markdown, together with a suite of comprehensive tests to validate implementation. Time will show, if this attempt is successful. However, CommonMark is a well specified Markdown dialect, in contrast to most (if not all) other dialects. Other software adopts CommonMark somehow, notably [[GitHub Flavored Markdown|https://github.github.com/gfm/]] (GFM). But they provide proprietary extensions, which makes it harder to change to another CommonMark implementation if needed. Plus, they sometimes build on an older specification of CommonMark. Zettelstore supports the latest CommonMark [[specification version 0.30 (2021-06-19)|https://spec.commonmark.org/0.30/]]. If possible, Zettelstore will adapt to newer versions when they are available. To provide CommonMark support, Zettelstore uses currently the [[Goldmark|https://github.com/yuin/goldmark]] implementation, which passes all validation tests of CommonMark. Internally, CommonMark is translated into some kind of super-set of [[Zettelmarkup|00001007000000]], which additionally allows to use HTML code.[^Effectively, Markdown and CommonMark are itself super-sets of HTML.] This Zettelmarkup super-set is later [[encoded|00001012920500]], often into [[HTML|00001012920510]]. Because Zettelstore HTML encoding philosophy differs a little bit to that of CommonMark, Zettelstore itself will not pass the CommonMark test suite fully. However, no CommonMark language element will fail to be encoded as HTML. In most cases, the differences are not visible for an user, but only by comparing the generated HTML code. Be aware, depending on the value of the startup configuration key [[''insecure-html''|00001004010000#insecure-html]], HTML code found within a CommonMark document or within the mentioned kind of super-set of Zettelmarkup will typically be ignored for security-related reasons. |
Changes to docs/manual/00001008050000.zettel.
1 | id: 00001008050000 | | < | | 1 2 3 4 5 6 7 8 9 10 11 12 13 | id: 00001008050000 title: The ""draw"" language role: manual tags: #graphic #manual #zettelstore syntax: zmk modified: 20220311120439 Sometimes, ""a picture is worth a thousand words"". To create some graphical representations, Zettelmarkup provides a simple mechanism. Characters like ""''|''"" or ""''-''"" already provide some visual feedback. For example, to create a picture containing two boxes that are connected via an arrow, the following representation is possible: ``` ~~~draw |
︙ | ︙ |
Changes to docs/manual/00001010000000.zettel.
1 2 3 4 5 6 | id: 00001010000000 title: Security role: manual tags: #configuration #manual #security #zettelstore syntax: zmk created: 20210126175322 | | | | | | | | | | | 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 | id: 00001010000000 title: Security role: manual tags: #configuration #manual #security #zettelstore syntax: zmk created: 20210126175322 modified: 20221018123622 Your zettel could contain sensitive content. You probably want to ensure that only authorized person can read and/or modify them. Zettelstore ensures this in various ways. === Local first The Zettelstore is designed to run on your local computer. If you do not configure it in other ways, no person from another computer can connect to your Zettelstore. You must explicitly configure it to allow access from other computers. In the case that you own multiple computers, you do not have to access the Zettelstore remotely. You could install Zettelstore on each computer and set-up some software to synchronize your zettel. Since zettel are stored as ordinary files, this task could be done in various ways. === Read-only You can start the Zettelstore in an read-only mode. Nobody, not even you as the owner of the Zettelstore, can change something via its interfaces[^However, as an owner, you have access to the files that store the zettel. If you modify the files, these changes will be reflected via its interfaces.]. You enable read-only mode through the key ''readonly'' in the [[startup configuration zettel|00001004010000#readonly]] or with the ''-r'' option of the ``zettelstore run`` sub-command. === Authentication The Zettelstore can be configured that a user must authenticate itself to gain access to the content. * [[How to enable authentication|00001010040100]] * [[How to add a new user|00001010040200]] * [[How users are authenticated|00001010040400]] (some technical background) * [[Authenticated sessions|00001010040700]] === Authorization Once you have enabled authentication, it is possible to allow others to access your Zettelstore. Maybe, others should be able to read some or all of your zettel. Or you want to allow them to create new zettel, or to change them. It is up to you. If someone is authenticated as the owner of the Zettelstore (hopefully you), no restrictions apply. But as an owner, you can create ""user zettel"" to allow others to access your Zettelstore in various ways. Even if you do not want to share your Zettelstore with other persons, creating user zettel can be useful if you plan to access your Zettelstore via the [[API|00001012000000]]. Additionally, you can specify that a zettel is publicly visible. In this case no one has to authenticate itself to see the content of the zettel. Or you can specify that a zettel is visible only to the owner. In this case, no authenticated user will be able to read and change that protected zettel. * [[Visibility rules for zettel|00001010070200]] * [[User roles|00001010070300]] define basic rights of an user * [[Authorization and read-only mode|00001010070400]] * [[Access rules|00001010070600]] define the policy which user is allowed to do what operation. === Encryption When Zettelstore is accessed remotely, the messages that are sent between Zettelstore and the client must be encrypted. Otherwise, an eavesdropper could fetch sensible data, such as passwords or precious content that is not for the public. The Zettelstore itself does not encrypt messages. But you can put a server in front of it, which is able to handle encryption. Most generic web server software do allow this. To enforce encryption, [[authenticated sessions|00001010040700]] are marked as secure by default. If you still want to access the Zettelstore remotely without encryption, you must change the startup configuration. Otherwise, authentication will not work. * [[Use a server for encryption|00001010090100]] |
Changes to docs/manual/00001010040100.zettel.
1 2 3 4 5 | id: 00001010040100 title: Enable authentication role: manual tags: #authentication #configuration #manual #security #zettelstore syntax: zmk | < | 1 2 3 4 5 6 7 8 9 10 11 12 | id: 00001010040100 title: Enable authentication role: manual tags: #authentication #configuration #manual #security #zettelstore syntax: zmk modified: 20220419192817 To enable authentication, you must create a zettel that stores [[authentication data|00001010040200]] for the owner. Then you must reference this zettel within the [[startup configuration|00001004010000#owner]] under the key ''owner''. Once the startup configuration contains a valid [[zettel identifier|00001006050000]] under that key, authentication is enabled. Please note that you must also set key ''secret'' of the [[startup configuration|00001004010000#secret]] to some random string data (minimum length is 16 bytes) to secure the data exchanged with a client system. |
Changes to docs/manual/00001010040200.zettel.
1 | id: 00001010040200 | | | | | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 | id: 00001010040200 title: Creating an user zettel role: manual tags: #authentication #configuration #manual #security #zettelstore syntax: zmk created: 20210126175322 modified: 20221205160251 All data to be used for authenticating a user is store in a special zettel called ""user zettel"". A user zettel must have set the following two metadata fields: ; ''user-id'' (""user identification"") : The unique identification to be specified for authentication. ; ''credential'' : A hashed password as generated by the [[``zettelstore password``{=sh}|00001004051400]] command. |
︙ | ︙ | |||
24 25 26 27 28 29 30 | A user zettel may additionally contain metadata that [[overwrites corresponding values|00001004020200]] of the [[runtime configuration|00001004020000]]. A user zettel can only be created by the owner of the Zettelstore. The owner should execute the following steps to create a new user zettel: # Create a new zettel. | | | 24 25 26 27 28 29 30 31 32 33 34 35 36 37 | A user zettel may additionally contain metadata that [[overwrites corresponding values|00001004020200]] of the [[runtime configuration|00001004020000]]. A user zettel can only be created by the owner of the Zettelstore. The owner should execute the following steps to create a new user zettel: # Create a new zettel. # Save the zettel to get a [[identifier|00001006050000]] for this zettel. # Choose a unique identification for the user. #* If the identifier is not unique, authentication will not work for this user. # Execute the [[``zettelstore password``|00001004051400]] command. #* You have to specify the user identification and the zettel identifier #* If you should not know the password of the new user, send her/him the user identification and the user zettel identifier, so that the person can create the hashed password herself. # Edit the user zettel and add the hashed password under the meta key ''credential'' and the user identification under the key ''user-id''. |
Changes to docs/manual/00001010040400.zettel.
1 2 3 4 5 | id: 00001010040400 title: Authentication process role: manual tags: #authentication #configuration #manual #security #zettelstore syntax: zmk | < | | | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 | id: 00001010040400 title: Authentication process role: manual tags: #authentication #configuration #manual #security #zettelstore syntax: zmk modified: 20211127174943 When someone tries to authenticate itself with an user identifier / ""user name"" and a password, the following process is executed: # If meta key ''owner'' of the configuration zettel does not have a valid [[zettel identifier|00001006050000]] as value, authentication fails. # Retrieve all zettel, where the meta key ''user-id'' has the same value as the given user identification. If the list is empty, authentication fails. # From above list, the zettel with the numerically smallest identifier is selected. Or in other words: the oldest zettel is selected[^This is done to prevent an attacker from creating a new note with the same user identification]. # If the zettel does not have a value for the meta key ''credential'', authentication fails. # The value of the meta key ''credential'' is compared with the given password. |
︙ | ︙ |
Changes to docs/manual/00001010040700.zettel.
1 2 3 4 5 | id: 00001010040700 title: Access token role: manual tags: #authentication #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 | id: 00001010040700 title: Access token role: manual tags: #authentication #configuration #manual #security #zettelstore syntax: zmk modified: 20211202120950 If an user is authenticated, an ""access token"" is created that must be sent with every request to prove the identity of the caller. Otherwise the user will not be recognized by Zettelstore. If the user was authenticated via the [[web user interface|00001014000000]], the access token is stored in a [[""session cookie""|https://en.wikipedia.org/wiki/HTTP_cookie#Session_cookie]]. When the web browser is closed, theses cookies are not saved. If you want web browser to store the cookie as long as lifetime of that token, the owner must set ''persistent-cookie'' of the [[startup configuration|00001004010000]] to ''true''. If the web browser remains inactive for a period, the user will be automatically logged off, because each access token has a limited lifetime. The maximum length of this period is specified by the ''token-lifetime-html'' value of the startup configuration. Every time a web page is displayed, a fresh token is created and stored inside the cookie. If the user was authenticated via the API, the access token will be returned as the content of the response. Typically, the lifetime of this token is more short term, e.g. 10 minutes. It is specified by the ''token-lifetime-api'' value of the startup configuration. If you need more time, you can either [[re-authenticate|00001012050200]] the user or use an API call to [[renew the access token|00001012050400]]. If you remotely access your Zettelstore via HTTP (not via HTTPS, which allows encrypted communication), your must set the ''insecure-cookie'' value of the startup configuration to ''true''. In most cases, such a scenario is not recommended, because user name and password will be transferred as plain text. You could make use of such scenario if you know all parties that access the local network where you access the Zettelstore. |
Changes to docs/manual/00001010070200.zettel.
1 2 3 4 5 6 | id: 00001010070200 title: Visibility rules for zettel role: manual tags: #authorization #configuration #manual #security #zettelstore syntax: zmk created: 20210126175322 | | | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 | id: 00001010070200 title: Visibility rules for zettel role: manual tags: #authorization #configuration #manual #security #zettelstore syntax: zmk created: 20210126175322 modified: 20220923104643 For every zettel you can specify under which condition the zettel is visible to others. This is controlled with the metadata key [[''visibility''|00001006020000#visibility]]. The following values are supported: ; [!public|""public""] : The zettel is visible to everybody, even if the user is not authenticated. |
︙ | ︙ | |||
28 29 30 31 32 33 34 | This is for zettel with sensitive content that might irritate the owner. Computed zettel with internal runtime information are examples for such a zettel. When you install a Zettelstore, only [[some zettel|query:visibility:public]] have visibility ""public"". One is the zettel that contains [[CSS|00000000020001]] for displaying the [[web user interface|00001014000000]]. This is to ensure that the web interface looks nice even for not authenticated users. Another is the zettel containing the Zettelstore [[license|00000000000004]]. | | | 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 | This is for zettel with sensitive content that might irritate the owner. Computed zettel with internal runtime information are examples for such a zettel. When you install a Zettelstore, only [[some zettel|query:visibility:public]] have visibility ""public"". One is the zettel that contains [[CSS|00000000020001]] for displaying the [[web user interface|00001014000000]]. This is to ensure that the web interface looks nice even for not authenticated users. Another is the zettel containing the Zettelstore [[license|00000000000004]]. The [[default image|00000000040001]], used if an image reference is invalid, is also public visible. Please note: if [[authentication is not enabled|00001010040100]], every user has the same rights as the owner of a Zettelstore. This is also true, if the Zettelstore runs additionally in [[read-only mode|00001004010000#read-only-mode]]. In this case, the [[runtime configuration zettel|00001004020000]] is shown (its visibility is ""owner""). The [[startup configuration|00001004010000]] is not shown, because the associated computed zettel with identifier ''00000000000096'' is stored with the visibility ""expert"". If you want to show such a zettel, you must set ''expert-mode'' to true. |
︙ | ︙ |
Changes to docs/manual/00001010070300.zettel.
1 2 3 4 5 | id: 00001010070300 title: User roles role: manual tags: #authorization #configuration #manual #security #zettelstore syntax: zmk | < | 1 2 3 4 5 6 7 8 9 10 11 12 | id: 00001010070300 title: User roles role: manual tags: #authorization #configuration #manual #security #zettelstore syntax: zmk modified: 20220214175212 Every user is associated with some basic privileges. These are specified in the [[user zettel|00001010040200]] with the key ''user-role''. The following values are supported: ; [!reader|""reader""] |
︙ | ︙ |
Changes to docs/manual/00001010070400.zettel.
1 2 3 4 5 | id: 00001010070400 title: Authorization and read-only mode role: manual 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 | id: 00001010070400 title: Authorization and read-only mode role: manual tags: #authorization #configuration #manual #security #zettelstore syntax: zmk modified: 20211103164251 It is possible to enable both the read-only mode of the Zettelstore __and__ authentication/authorization. Both modes are independent from each other. This gives four use cases: ; Not read-only, no authorization : Zettelstore runs on your local computer and you only work with it. ; Not read-only, with authorization : Zettelstore is accessed remotely. You need authentication to ensure that only valid users access your Zettelstore. ; With read-only, no authorization : Zettelstore present publicly its full content to everybody. ; With read-only, with authorization : Nobody is allowed to change the content of the Zettelstore, but only specific zettel should be presented to the public. |
Changes to docs/manual/00001010070600.zettel.
1 2 3 4 5 | id: 00001010070600 title: Access rules role: manual tags: #authorization #configuration #manual #security #zettelstore syntax: zmk | < | | 1 2 3 4 5 6 7 8 9 10 11 12 13 | id: 00001010070600 title: Access rules role: manual tags: #authorization #configuration #manual #security #zettelstore syntax: zmk modified: 20211124142456 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. |
︙ | ︙ | |||
39 40 41 42 43 44 45 46 47 48 49 | ** If the zettel is the [[user zettel|00001010040200]] of the authenticated user, proceed as follows: *** If some sensitive meta values are changed (e.g. user identifier, zettel role, user role, but not hashed password), reject the access *** Since the user just updates some uncritical values, grant the access In other words: a user is allowed to change its user zettel, even if s/he has no writer privilege and if only uncritical data is changed. ** If the ''user-role'' of the user is ""reader"", reject the access. ** If the user is not allowed to create a new zettel, reject the access. ** Otherwise grant the access. * Delete a zettel ** Reject the access. Only the owner of the Zettelstore is allowed to delete a zettel. This may change in the future. | > > > | 38 39 40 41 42 43 44 45 46 47 48 49 50 51 | ** If the zettel is the [[user zettel|00001010040200]] of the authenticated user, proceed as follows: *** If some sensitive meta values are changed (e.g. user identifier, zettel role, user role, but not hashed password), reject the access *** Since the user just updates some uncritical values, grant the access In other words: a user is allowed to change its user zettel, even if s/he has no writer privilege and if only uncritical data is changed. ** If the ''user-role'' of the user is ""reader"", reject the access. ** If the user is not allowed to create a new zettel, reject the access. ** Otherwise grant the access. * Rename a zettel ** Reject the access. Only the owner of the Zettelstore is currently allowed to give a new identifier for a zettel. * Delete a zettel ** Reject the access. Only the owner of the Zettelstore is allowed to delete a zettel. This may change in the future. |
Changes to docs/manual/00001010090100.zettel.
1 2 3 4 5 | id: 00001010090100 title: External server to encrypt message transport role: manual tags: #configuration #encryption #manual #security #zettelstore syntax: zmk | < | 1 2 3 4 5 6 7 8 9 10 11 12 | id: 00001010090100 title: External server to encrypt message transport role: manual tags: #configuration #encryption #manual #security #zettelstore syntax: zmk modified: 20220217180826 Since Zettelstore does not encrypt the messages it exchanges with its clients, you may need some additional software to enable encryption. === Public-key encryption To enable encryption, you probably use some kind of encryption keys. In most cases, you need to deploy a ""public-key encryption"" process, where your side publish a public encryption key that only works with a corresponding private decryption key. |
︙ | ︙ |
Changes to docs/manual/00001012000000.zettel.
1 2 3 4 5 6 | id: 00001012000000 title: API role: manual tags: #api #manual #zettelstore syntax: zmk created: 20210126175322 | | | | < < > > > > | 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 | id: 00001012000000 title: API role: manual tags: #api #manual #zettelstore syntax: zmk created: 20210126175322 modified: 20221219150243 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 plain text and JSON as its main encoding format for exchanging messages between a Zettelstore and its client software. There is an [[overview zettel|00001012920000]] that shows the structure of the endpoints used by the API and gives an indication about its use. === Authentication If [[authentication is enabled|00001010040100]], most API calls must include an [[access token|00001010040700]] that proves the identity of the caller. * [[Authenticate an user|00001012050200]] to obtain an access token * [[Renew an access token|00001012050400]] without costly re-authentication * [[Provide an access token|00001012050600]] when doing an API call === Zettel lists * [[List all zettel|00001012051200]] * [[Query the list of all zettel|00001012051400]] === Working with zettel * [[Create a new zettel|00001012053200]] * [[Retrieve metadata and content of an existing zettel|00001012053300]] * [[Retrieve metadata of an existing zettel|00001012053400]] * [[Retrieve evaluated metadata and content of an existing zettel in various encodings|00001012053500]] * [[Retrieve parsed metadata and content of an existing zettel in various encodings|00001012053600]] * [[Retrieve context of an existing zettel|00001012053800]] * [[Retrieve unlinked references to an existing zettel|00001012053900]] * [[Retrieve zettel order within an existing zettel|00001012054000]] * [[Update metadata and content of a zettel|00001012054200]] * [[Rename a zettel|00001012054400]] * [[Delete a zettel|00001012054600]] === Various helper methods * [[Retrieve administrative data|00001012070500]] * [[Execute some commands|00001012080100]] ** [[Check for authentication|00001012080200]] ** [[Refresh internal data|00001012080500]] |
Changes to docs/manual/00001012050200.zettel.
1 2 3 4 5 | id: 00001012050200 title: API: Authenticate a client role: manual 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 49 50 51 52 53 54 55 56 | id: 00001012050200 title: API: Authenticate a client role: manual tags: #api #manual #zettelstore syntax: zmk modified: 20220107215844 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-lifetime-api'' of the [[startup configuration|00001004010000#token-lifetime-api]] (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]] ''/a'' with a POST request: ```sh # curl -X POST -u IDENT:PASSWORD http://127.0.0.1:23123/a {"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/a {"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/a {"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. Please take into account that this request will take approximately 500 milliseconds, under certain circumstances more. However, if [[authentication is not enabled|00001010040100]] and you send an authentication request, no user identification/password checking is done and you receive an artificial token immediate, without any delay: ```sh # curl -X POST -u IDENT:PASSWORD http://127.0.0.1:23123/a {"token":"freeaccess","token_type":"Bearer","expires_in":316224000} ``` In this case, it is even possible to omit the user identification/password. === HTTP Status codes In all cases of successful authentication, a JSON object is returned, which contains the token under the key ''"token"''. A successful authentication is signaled with the HTTP status code 200, as usual. Other status codes possibly send by the Zettelstore: ; ''400'' : Unable to process the request. In most cases the form data was invalid. ; ''401'' : Authentication failed. Either the user identification is invalid or you provided the wrong password. ; ''403'' |
︙ | ︙ |
Changes to docs/manual/00001012050400.zettel.
1 2 3 4 5 | id: 00001012050400 title: API: Renew an access token role: manual 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 | id: 00001012050400 title: API: Renew an access token role: manual tags: #api #manual #zettelstore syntax: zmk modified: 20220107215751 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]] ''/a'' and include the current access token in the ''Authorization'' header: ```sh # curl -X PUT -H 'Authorization: Bearer TOKEN' http://127.0.0.1:23123/a {"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. If [[authentication is not enabled|00001010040100]] and you send a renew request, no checking is done and you receive an artificial token immediate, without any delay: ```sh # curl -X PUT -H 'Authorization: Bearer freeaccess' http://127.0.0.1:23123/a {"token":"freeaccess","token_type":"Bearer","expires_in":316224000} ``` In this case, it is even possible to omit the access token. === HTTP Status codes ; ''200'' : Renew process was successful, the body contains an [[appropriate JSON object|00001012921000]]. ; ''400'' : The renew process was not successful. There are several reasons for this. Maybe access bearer token was not valid. Probably you should [[authenticate|00001012050200]] again with user identification and password. |
Changes to docs/manual/00001012050600.zettel.
1 2 3 4 5 | id: 00001012050600 title: API: Provide an access token role: manual tags: #api #manual #zettelstore syntax: zmk | < | 1 2 3 4 5 6 7 8 9 10 11 12 | id: 00001012050600 title: API: Provide an access token role: manual tags: #api #manual #zettelstore syntax: zmk modified: 20220218130020 The [[authentication process|00001012050200]] provides you with an [[access token|00001012921000]]. Most API calls need such an access token, so that they know the identity of the caller. You send the access token in the ""Authorization"" request header field, as described in [[RFC 6750, section 2.1|https://tools.ietf.org/html/rfc6750#section-2.1]]. You need to use the ""Bearer"" authentication scheme to transmit the access token. |
︙ | ︙ |
Changes to docs/manual/00001012051200.zettel.
1 2 3 4 5 6 | id: 00001012051200 title: API: List all zettel role: manual tags: #api #manual #zettelstore syntax: zmk created: 20210126175322 | | | < < < | < < < | | | > | | > > | | > > > > > > > | > | > > > > > > | | | | | | | | > > > > > | < < < > | > | < | > > > > > | > > | > > > > > > > > > > > > > > > > > > | > > > > > > > | > > > | > | | 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 | id: 00001012051200 title: API: List all zettel role: manual tags: #api #manual #zettelstore syntax: zmk created: 20210126175322 modified: 20221219154400 To list all zettel just send a HTTP GET request to the [[endpoint|00001012920000]] ''/z''[^If [[authentication is enabled|00001010040100]], you must include the a valid [[access token|00001012050200]] in the ''Authorization'' header]. Always use the endpoint ''/z'' to work with a list of zettel. Without further specifications, a plain text document is returned, with one line per zettel. Each line contains in the first 14 characters the [[zettel identifier|00001006050000]]. Separated by a space character, the title of the zettel follows: ```sh # curl http://127.0.0.1:23123/z ... 00001012051200 API: List all zettel 00001012050600 API: Provide an access token 00001012050400 API: Renew an access token 00001012050200 API: Authenticate a client ... ``` Alternatively, you may retrieve the list of all zettel as a JSON object by specifying the encoding with the query parameter ''enc=json'': ```sh # curl 'http://127.0.0.1:23123/z?enc=json' {"query":"","human":"","list":[{"id":"00001012051200","meta":{"back":"00001012000000","backward":"00001012000000 00001012920000","box-number":"1","created":"20210126175322","forward":"00001006020000 00001006050000 00001007700000 00001010040100 00001012050200 00001012051400 00001012920000 00001012921000 00001014000000","modified":"20221219150626","published":"20221219150626","role":"manual","syntax":"zmk","tags":"#api #manual #zettelstore","title":"API: List all zettel"},"rights":62},{"id":"00001012050600","meta":{"back":"00001012000000 00001012080500","backward":"00001012000000 00001012080500","box-number":"1","created":"00010101000000","forward":"00001012050200 00001012921000","modified":"20220218130020","published":"20220218130020","role":"manual","syntax":"zmk","tags":"#api #manual #zettelstore","title":"API: Provide an access token"},"rights":62},{"id":"00001012050400","meta":{"back":"00001010040700 00001012000000","backward":"00001010040700 00001012000000 00001012920000 00001012921000","box-number":"1","created":"00010101000000","forward":"00001010040100 00001012050200 00001012920000 00001012921000","modified":"20220107215751","published":"20220107215751","role":"manual","syntax":"zmk","tags":"#api #manual #zettelstore","title":"API: Renew an access token"},"rights":62},{"id":"00001012050200","meta":{"back":"00001012000000 00001012050400 00001012050600 00001012051200 00001012051400 00001012053300 00001012053400 00001012053500 00001012053600 00001012080200","backward":"00001010040700 00001012000000 00001012050400 00001012050600 00001012051200 00001012051400 00001012053300 00001012053400 00001012053500 00001012053600 00001012080200 00001012920000 00001012921000","box-number":"1","created":"00010101000000","forward":"00001004010000 00001010040100 00001010040200 00001010040700 00001012920000 00001012921000","modified":"20220107215844","published":"20220107215844","role":"manual","syntax":"zmk","tags":"#api #manual #zettelstore","title":"API: Authenticate a client"},"rights":62}, ...]} ``` If you reformat the JSON output, you will see its structure better: ``` { "query": "", "human": "", "list": [ { "id": "00001012051200", "meta": { "back": "00001012000000", "backward": "00001012000000 00001012920000", "box-number": "1", "created": "20210126175322", "forward": "00001006020000 00001006050000 00001007700000 00001010040100 00001012050200 00001012051400 00001012920000 00001012921000 00001014000000", "modified": "20221219151200", "published": "20221219151200", "role": "manual", "syntax": "zmk", "tags": "#api #manual #zettelstore", "title": "API: List all zettel" }, "rights": 62 }, { "id": "00001012050600", "meta": { "back": "00001012000000 00001012080500", "backward": "00001012000000 00001012080500", "box-number": "1", "created": "00010101000000", "forward": "00001012050200 00001012921000", "modified": "20220218130020", "published": "20220218130020", "role": "manual", "syntax": "zmk", "tags": "#api #manual #zettelstore", "title": "API: Provide an access token" }, "rights": 62 }, { "id": "00001012050400", "meta": { "back": "00001010040700 00001012000000", "backward": "00001010040700 00001012000000 00001012920000 00001012921000", "box-number": "1", "created": "00010101000000", "forward": "00001010040100 00001012050200 00001012920000 00001012921000", "modified": "20220107215751", "published": "20220107215751", "role": "manual", "syntax": "zmk", "tags": "#api #manual #zettelstore", "title": "API: Renew an access token" }, "rights": 62 }, { "id": "00001012050200", "meta": { "back": "00001012000000 00001012050400 00001012050600 00001012051200 00001012051400 00001012053300 00001012053400 00001012053500 00001012053600 00001012 080200", "backward": "00001010040700 00001012000000 00001012050400 00001012050600 00001012051200 00001012051400 00001012053300 00001012053400 00001012053500 0000 1012053600 00001012080200 00001012920000 00001012921000", "box-number": "1", "created": "00010101000000", "forward": "00001004010000 00001010040100 00001010040200 00001010040700 00001012920000 00001012921000", "modified": "20220107215844", "published": "20220107215844", "role": "manual", "syntax": "zmk", "tags": "#api #manual #zettelstore", "title": "API: Authenticate a client" }, "rights": 62 } ] } ``` The JSON object contains a key ''"list"'' where its value is a list of zettel JSON objects. These zettel JSON objects themselves contains the keys ''"id"'' (value is a string containing the [[zettel identifier|00001006050000]]), ''"meta"'' (value as a JSON object), and ''"rights"'' (encodes the [[access rights|00001012921200]] for the given zettel). The value of key ''"meta"'' effectively contains all metadata of the identified zettel, where metadata keys are encoded as JSON object keys and metadata values encoded as JSON strings. The JSON objects keys ''"query"'' and ''"human"'' will be explained later in this manual. === Note This request (and similar others) will always return a list of metadata, provided the request was syntactically correct. There will never be a HTTP status code 403 (Forbidden), even if [[authentication was enabled|00001010040100]] and you did not provide a valid access token. In this case, the resulting list might be quite short (some zettel will have [[public visibility|00001010070200]]) or the list might be empty. With this call, you cannot differentiate between an empty result list (e.g because your search did not found a zettel with the specified term) and an empty list because of missing authorization (e.g. an invalid access token). === HTTP Status codes ; ''200'' : Retrieval was successful, the body contains an [[appropriate JSON object|00001012921000]]. ; ''400'' : Request was not valid. There are several reasons for this. Maybe the access bearer token was not valid. |
Changes to docs/manual/00001012051400.zettel.
1 2 3 4 5 6 | id: 00001012051400 title: API: Query the list of all zettel role: manual tags: #api #manual #zettelstore syntax: zmk created: 20220912111111 | | < | | | | | | | | | | | | > > > | | < < < < < < < < | 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 | id: 00001012051400 title: API: Query the list of all zettel role: manual tags: #api #manual #zettelstore syntax: zmk created: 20220912111111 modified: 20230320151117 The [[endpoint|00001012920000]] ''/z'' also allows you to filter the list of all zettel[^If [[authentication is enabled|00001010040100]], you must include the a valid [[access token|00001012050200]] in the ''Authorization'' header] and optionally to provide some actions. A [[query|00001007700000]] is an optional [[search expression|00001007700000#search-expression]], together with an optional [[list of actions|00001007700000#action-list]] (described below). An empty search expression will select all zettel. An empty list of action, or no valid action, returns the list of all selected zettel metadata. Search expression and action list are separated by a vertical bar character (""''|''"", U+007C), and must be given with the query parameter ''q''. The query parameter ""''q''"" allows you to specify [[query expressions|00001007700000]] for a full-text search of all zettel content and/or restricting the search according to specific metadata. It is allowed to specify this query parameter more than once. This parameter loosely resembles the search form of the [[web user interface|00001014000000]] or those of [[Zettelmarkup's Query Transclusion|00001007031140]]. 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?q=title%3AAPI' 00001012921000 API: JSON structure of an access token 00001012920500 Formats available by the API 00001012920000 Endpoints used by the API ... ``` If you want to retrieve a JSON document: ```sh # curl 'http://127.0.0.1:23123/z?q=title%3AAPI&enc=json' {"query":"title:API","human":"title HAS API","list":[{"id":"00001012921200","meta":{"back":"00001012051200 00001012051400 00001012053300 00001012053400 00001012053800 00001012053900 00001012054000","backward":"00001012051200 00001012051400 00001012053300 00001012053400 00001012053800 00001012053900 00001012054000","box-number":"1","created":"00010101000000","forward":"00001003000000 00001006020400 00001010000000 00001010040100 00001010040200 00001010070200 00001010070300","modified":"20220201171959","published":"20220201171959","role":"manual","syntax":"zmk","tags":"#api #manual #reference #zettelstore","title":"API: Encoding of Zettel Access Rights"},"rights":62},{"id":"00001012921000","meta":{"back":"00001012050600 00001012051200","backward":"00001012050200 00001012050400 00001012050600 00001012051200","box-number":"1","created":"00010101000000","forward":"00001012050200 00001012050400","published":"00010101000000","role":"manual","syntax":"zmk","tags":"#api #manual #reference #zettelstore","title":"API: JSON structure of an access token"},"rights":62}, ...] ``` The JSON object contains a key ''"list"'' where its value is a list of zettel JSON objects. These zettel JSON objects themselves contains the keys ''"id"'' (value is a string containing the [[zettel identifier|00001006050000]]), ''"meta"'' (value as a JSON object), and ''"rights"'' (encodes the [[access rights|00001012921200]] for the given zettel). The value of key ''"meta"'' effectively contains all metadata of the identified zettel, where metadata keys are encoded as JSON object keys and metadata values encoded as JSON strings. Additionally, the JSON object contains the keys ''"query"'' and ''"human"'' with a string value. Both will contain a textual description of the underlying query if you select only some zettel with a [[query expression|00001007700000]]. Without a selection, the values are the empty string. ''"query"'' returns the normalized query expression itself, while ''"human"'' is the normalized query expression to be read by humans. An implicit precondition is that the zettel must contain the given metadata key. For a metadata key like [[''title''|00001006020000#title]], which have 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?q=url%3A'`` and ``curl 'http://localhost:23123/z?q=url%3A!'`` may result in an empty list. |
︙ | ︙ | |||
68 69 70 71 72 73 74 | ``` The result is a text file. The first word, separated by a horizontal tab (U+0009) contains the role name. The rest of the line consists of zettel identifier, where the corresponding zettel have this role. Zettel identifier are separated by a space character (U+0020). | < < < | | < > < | < < < > | | < > > > > < < < < < < < < < < < | < | < < < < | 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 | ``` The result is a text file. The first word, separated by a horizontal tab (U+0009) contains the role name. The rest of the line consists of zettel identifier, where the corresponding zettel have this role. Zettel identifier are separated by a space character (U+0020). Of course, this list can be returned as a JSON object: ```sh # curl 'http://127.0.0.1:23123/z?q=|role?enc=json' {"map":{"configuration":["00000000090002","00000000090000", ... ,"00000000000001"],"manual":["00001014000000", ... ,"00001000000000"],"zettel":["00010000000000", ... ,"00001012070500","00000000090001"]}} ``` The JSON object only contains the key ''"map"'' with the value of another object. This second object contains all role names as keys and the list of identifier of those zettel with this specific role as a value. Similar, to list all tags used in the Zettelstore, send a HTTP GET request to the endpoint ''/z?q=|tags''. If successful, the output is a JSON object: ```sh # curl 'http://127.0.0.1:23123/z?q=|tags&enc=json' {"map":{"#api":[:["00001012921000","00001012920800","00001012920522",...],"#authorization":["00001010040700","00001010040400",...],...,"#zettelstore":["00010000000000","00001014000000",...,"00001001000000"]}} ``` The JSON object only contains the key ''"map"'' with the value of another object. This second object contains all tags as keys and the list of identifier of those zettel with this tag as a value. If you want only those tags that occur at least 100 times, use the endpoint ''/z?q=|MIN100+tags''. You see from this that actions are separated by space characters. There are two types of actions: parameters and aggregates. The following actions are supported: ; ''MINn'' (parameter) : Emit only those values with at least __n__ aggregated values. __n__ must be a positive integer, ''MIN'' must be given in upper-case letters. ; ''MAXn'' (parameter) : Emit only those values with at most __n__ aggregated values. __n__ must be a positive integer, ''MAX'' must be given in upper-case letters. ; Any [[metadata key|00001006020000]] of type [[Word|00001006035500]], [[WordSet|00001006036000]], or [[TagSet|00001006034000]] (aggregates) : Emit an aggregate of the given metadata key. The key can be given in any letter case. Only the first aggregate action will be executed. === HTTP Status codes ; ''200'' : Query was successful. ; ''204'' : Query was successful, but results in no content. Most likely, you specified no appropriate aggregator. ; ''400'' : Request was not valid. There are several reasons for this. Maybe the access bearer token was not valid, or you forgot to specify a valid query. |
Deleted docs/manual/00001012051600.zettel.
|
| < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < |
Deleted docs/manual/00001012051800.zettel.
|
| < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < |
Changes to docs/manual/00001012053200.zettel.
1 2 3 4 5 6 | id: 00001012053200 title: API: Create a new zettel role: manual tags: #api #manual #zettelstore syntax: zmk created: 20210713150005 | | | > | > > > > > > > > > > > > | > > > > > | > > > > > > > | > > > > > | | | 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 | id: 00001012053200 title: API: Create a new zettel role: manual tags: #api #manual #zettelstore syntax: zmk created: 20210713150005 modified: 20221219162225 A zettel is created by adding it to the [[list of zettel|00001012000000]]. 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 zettel must be encoded in a [[plain|00001006000000]] format: first comes the [[metadata|00001006010000]] and the following content is separated by an empty line. This is the same format as used by storing zettel within a [[directory box|00001006010000]]. ``` # curl -X POST --data $'title: Note\n\nImportant content.' http://127.0.0.1:23123/z 20210903211500 ``` The zettel identifier of the created zettel is returned. In addition, the HTTP response header contains a key ''Location'' with a relative URL for the new zettel. 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. Alternatively, the body of the POST request may contain a JSON object that specifies metadata and content of the zettel to be created. To do this, you must add the query parameter ''enc=json''. 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, and only an identifier as [[metadata|00001006020000]]: ``` # curl -X POST --data '{}' 'http://127.0.0.1:23123/z?enc=json' {"id":"20210713161000"} ``` If creating the zettel was successful, the HTTP response will contain a JSON object with one key: ; ''"id"'' : Contains the [[zettel identifier|00001006050000]] of the created zettel for further usage. 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?enc=json' {"id":"20210713163100"} ``` === HTTP Status codes ; ''201'' : Zettel creation was successful, the body contains its [[zettel identifier|00001006050000]] (JSON object or plain text). ; ''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/00001012053300.zettel.
1 2 3 4 5 6 | id: 00001012053300 title: API: Retrieve metadata and content of an existing zettel role: manual tags: #api #manual #zettelstore syntax: zmk created: 20211004093206 | | | | | 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: 00001012053300 title: API: Retrieve metadata and content of an existing zettel role: manual tags: #api #manual #zettelstore syntax: zmk created: 20211004093206 modified: 20221219160613 The [[endpoint|00001012920000]] to work with metadata and content of a specific zettel is ''/z/{ID}'', where ''{ID}'' is a placeholder for the [[zettel identifier|00001006050000]]. For example, to retrieve some data about this zettel you are currently viewing, just send a HTTP GET request to the endpoint ''/z/00001012053300''[^If [[authentication is enabled|00001010040100]], you must include the a valid [[access token|00001012050200]] in the ''Authorization'' header]. ````sh # curl 'http://127.0.0.1:23123/z/00001012053300' The [[endpoint|00001012920000]] to work with metadata and content of a specific zettel is ''/z/{ID}'', where ''{ID}'' is a placeholder for the [[zettel identifier|00001006050000]]. For example, to retrieve some data about this zettel you are currently viewing, just send a HTTP GET request to the endpoint ''/z/00001012053300''[^If [[authentication is enabled|00001010040100]], you must include the a valid [[access token|00001012050200]] in the ''Authorization'' header]. ```sh ... ```` Optionally, you may provide which parts of the zettel you are requesting. In this case, add an additional query parameter ''part=PART''. |
︙ | ︙ | |||
34 35 36 37 38 39 40 | The [[endpoint|00001012920000]] to work with metadata and content of a specific zettel is ''/z/{ID}'', where ''{ID}'' is a placeholder for the [[zettel identifier|00001006050000]]. For example, to retrieve some data about this zettel you are currently viewing, just send a HTTP GET request to the endpoint ... ```` | | | < | < > | > > > | | | | | | | | | | | < > | | > > > > | > > | | < > | > | > > > | | 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 | The [[endpoint|00001012920000]] to work with metadata and content of a specific zettel is ''/z/{ID}'', where ''{ID}'' is a placeholder for the [[zettel identifier|00001006050000]]. For example, to retrieve some data about this zettel you are currently viewing, just send a HTTP GET request to the endpoint ... ```` === JSON output Alternatively, you may retrieve the zettel as a JSON object by providinv the query parameter ''enc=json'': ```sh # curl 'http://127.0.0.1:23123/z/00001012053300?enc=json&part=zettel' {"id":"00001012053300","meta":{"back":"00001012000000 00001012054400","backward":"00001012000000 00001012054400 00001012920000","box-number":"1","created":"20211004093206","forward":"00001006020000 00001006050000 00001010040100 00001012050200 00001012053400 00001012920000 00001012920800 00001012921200","modified":"20221219160211","published":"20221219160211","role":"manual","syntax":"zmk","tags":"#api #manual #zettelstore","title":"API: Retrieve metadata and content of an existing zettel"},"encoding":"","content":"The [[endpoint|00001012920000]] to work with metadata and content of a specific zettel is ''/z/{ID}'', where ''{ID}'' is a placeholder for the [[zettel identifier|00001006050000]].\n\nFor example, ... ``` Pretty-printed, this results in: ``` { "id": "00001012053300", "meta": { "back": "00001012000000 00001012054400", "backward": "00001012000000 00001012054400 00001012920000", "box-number": "1", "created": "20211004093206", "forward": "00001006020000 00001006050000 00001010040100 00001012050200 00001012053400 00001012920000 00001012920800 00001012921200", "modified": "20221219160211", "published": "20221219160211", "role": "manual", "syntax": "zmk", "tags": "#api #manual #zettelstore", "title": "API: Retrieve metadata and content of an existing zettel" }, "encoding": "", "content": "The [[endpoint|00001012920000]] to work with metadata and content of a specific zettel is ''/z/{ID}'', where ''{ID}'' (...) "rights": 62 } ``` The following keys of the JSON object are used: ; ''"id"'' : The zettel identifier of the zettel you requested. ; ''"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. ; ''"rights"'' : An integer number that describes the [[access rights|00001012921200]] for the zettel. === HTTP Status codes ; ''200'' : Retrieval was successful, the body contains an appropriate JSON object / plain zettel data. ; ''204'' : Request was valid, but there is no data to be returned. Most likely, you specified the query parameter ''part=content'', but the zettel does not contain any content. ; ''400'' : Request was not valid. There are several reasons for this. Maybe the [[zettel identifier|00001006050000]] did not consists of exactly 14 digits. ; ''403'' : You are not allowed to retrieve data of the given zettel. ; ''404'' : Zettel not found. You probably used a zettel identifier that is not used in the Zettelstore. |
Changes to docs/manual/00001012053400.zettel.
1 2 3 4 5 6 | id: 00001012053400 title: API: Retrieve metadata of an existing zettel role: manual tags: #api #manual #zettelstore syntax: zmk created: 20210726174524 | | | < | < | | < < < > | | | | | | | | > > > > > | > > > > > > > > < < < < | | 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 | id: 00001012053400 title: API: Retrieve metadata of an existing zettel role: manual tags: #api #manual #zettelstore syntax: zmk created: 20210726174524 modified: 20221219161030 The [[endpoint|00001012920000]] to work with metadata of a specific zettel is ''/z/{ID}'', where ''{ID}'' is a placeholder for the [[zettel identifier|00001006050000]][^If [[authentication is enabled|00001010040100]], you must include the a valid [[access token|00001012050200]] in the ''Authorization'' header]. To retrieve the plain metadata of a zettel, use the query parameter ''part=meta'' ````sh # curl 'http://127.0.0.1:23123/z/00001012053400?part=meta' title: API: Retrieve metadata of an existing zettel role: manual tags: #api #manual #zettelstore syntax: zmk ```` To return a JSON object, use also the query parameter ''enc=json''. ```sh # curl 'http://127.0.0.1:23123/z/00001012053400?part=meta&enc=json' {"meta":{"back":"00001012000000 00001012053300","backward":"00001012000000 00001012053300 00001012920000","box-number":"1","created":"20210726174524","forward":"00001006020000 00001006050000 00001010040100 00001012050200 00001012920000 00001012921200","modified":"20220917175233","published":"20220917175233","role":"manual","syntax":"zmk","tags":"#api #manual #zettelstore","title":"API: Retrieve metadata of an existing zettel"},"rights":62} ``` Pretty-printed, this results in: ``` { "meta": { "back": "00001012000000 00001012053300", "backward": "00001012000000 00001012053300 00001012920000", "box-number": "1", "created": "20210726174524", "forward": "00001006020000 00001006050000 00001010040100 00001012050200 00001012920000 00001012921200", "modified": "20220917175233", "published": "20220917175233", "role": "manual", "syntax": "zmk", "tags": "#api #manual #zettelstore", "title": "API: Retrieve metadata of an existing zettel" }, "rights": 62 } ``` 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). ; ''"rights"'' : An integer number that describes the [[access rights|00001012921200]] for the zettel. === HTTP Status codes ; ''200'' : Retrieval was successful, the body contains an appropriate JSON object. ; ''400'' : Request was not valid. There are several reasons for this. Maybe the zettel identifier did not consist of exactly 14 digits. ; ''403'' : You are not allowed to retrieve data of the given zettel. ; ''404'' |
︙ | ︙ |
Changes to docs/manual/00001012053500.zettel.
1 2 3 4 5 6 | id: 00001012053500 title: API: Retrieve evaluated metadata and content of an existing zettel in various encodings role: manual tags: #api #manual #zettelstore syntax: zmk created: 20210726174524 | | | | | | | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 | id: 00001012053500 title: API: Retrieve evaluated metadata and content of an existing zettel in various encodings role: manual tags: #api #manual #zettelstore syntax: zmk created: 20210726174524 modified: 20230109105303 The [[endpoint|00001012920000]] to work with evaluated metadata and content of a specific zettel is ''/z/{ID}'', where ''{ID}'' is a placeholder for the [[zettel identifier|00001006050000]]. For example, to retrieve some evaluated data about this zettel you are currently viewing in [[Sexpr encoding|00001012920516]], just send a HTTP GET request to the endpoint ''/z/00001012053500''[^If [[authentication is enabled|00001010040100]], you must include the a valid [[access token|00001012050200]] in the ''Authorization'' header] with the query parameter ''enc=sexpr''. If successful, the output is a JSON object: ```sh # curl 'http://127.0.0.1:23123/z/00001012053500?enc=sexpr' ((PARA (TEXT "The") (SPACE) (LINK-ZETTEL () "00001012920000" (TEXT "endpoint")) (SPACE) (TEXT "to") (SPACE) (TEXT "work") (SPACE) (TEXT "with") (SPACE) (TEXT "evaluated") (SPACE) (TEXT "metadata") (SPACE) (TEXT "and") (SPACE) (TEXT "content") (SPACE) (TEXT "of") (SPACE) (TEXT "a") (SPACE) (TEXT "specific") (SPACE) (TEXT "zettel") (SPACE) (TEXT "is") (SPACE) (LITERAL-INPUT () "/z/{ID}") (TEXT ",") (SPACE) (TEXT "where") (SPACE) (LITERAL-INPUT () "{ID}") ... ``` To select another encoding, you must provide the query parameter ''enc=ENCODING''. Others are ""[[html|00001012920510]]"", ""[[text|00001012920519]]"", and some [[more|00001012920500]]. In addition, you may provide a query parameter ''part=PART'' to select the relevant [[part|00001012920800]] of a zettel. ```sh # curl 'http://127.0.0.1:23123/z/00001012053500?enc=html&part=zettel' |
︙ | ︙ | |||
44 45 46 47 48 49 50 | <h1>API: Retrieve evaluated metadata and content of an existing zettel in various encodings</h1> <p>The <a href="00001012920000">endpoint</a> to work with evaluated metadata and content of a specific zettel is <kbd>/z/{ID}</kbd>, ... ``` === HTTP Status codes ; ''200'' | | | 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 | <h1>API: Retrieve evaluated metadata and content of an existing zettel in various encodings</h1> <p>The <a href="00001012920000">endpoint</a> to work with evaluated metadata and content of a specific zettel is <kbd>/z/{ID}</kbd>, ... ``` === HTTP Status codes ; ''200'' : Retrieval was successful, the body contains an appropriate JSON object. ; ''400'' : Request was not valid. There are several reasons for this. Maybe the zettel identifier did not consist of exactly 14 digits or ''enc'' / ''part'' contained illegal values. ; ''403'' : You are not allowed to retrieve data of the given zettel. ; ''404'' : Zettel not found. You probably used a zettel identifier that is not used in the Zettelstore. |
Changes to docs/manual/00001012053600.zettel.
1 2 3 4 5 6 | id: 00001012053600 title: API: Retrieve parsed metadata and content of an existing zettel in various encodings role: manual tags: #api #manual #zettelstore syntax: zmk created: 20210126175322 | | | | | | | 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: 00001012053600 title: API: Retrieve parsed metadata and content of an existing zettel in various encodings role: manual tags: #api #manual #zettelstore syntax: zmk created: 20210126175322 modified: 20230109105034 The [[endpoint|00001012920000]] to work with parsed metadata and content of a specific zettel is ''/z/{ID}'', where ''{ID}'' is a placeholder for the [[zettel identifier|00001006050000]]. A __parsed__ zettel is basically an [[unevaluated|00001012053500]] zettel: the zettel is read and analyzed, but its content is not __evaluated__. By using this endpoint, you are able to retrieve the structure of a zettel before it is evaluated. For example, to retrieve some data about this zettel you are currently viewing, just send a HTTP GET request to the endpoint ''/z/00001012053600''[^If [[authentication is enabled|00001010040100]], you must include the a valid [[access token|00001012050200]] in the ''Authorization'' header] with the query parameter ''parseonly'' (and other appropriate query parameter). For example: ```sh # curl 'http://127.0.0.1:23123/z/00001012053600?enc=sexpr&parseonly' ((PARA (TEXT "The") (SPACE) (LINK-ZETTEL () "00001012920000" (TEXT "endpoint")) (SPACE) (TEXT "to") (SPACE) (TEXT "work") (SPACE) (TEXT "with") (SPACE) ... ``` Similar to [[retrieving an encoded zettel|00001012053500]], you can specify an [[encoding|00001012920500]] and state which [[part|00001012920800]] of a zettel you are interested in. The same default values applies to this endpoint. === HTTP Status codes ; ''200'' : Retrieval was successful, the body contains an appropriate JSON object. ; ''400'' : Request was not valid. There are several reasons for this. Maybe the zettel identifier did not consist of exactly 14 digits or ''enc'' / ''part'' contained illegal values. ; ''403'' : You are not allowed to retrieve data of the given zettel. ; ''404'' |
︙ | ︙ |
Added docs/manual/00001012053800.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 | id: 00001012053800 title: API: Retrieve context of an existing zettel role: manual tags: #api #manual #zettelstore syntax: zmk created: 20210222151131 modified: 20221208165112 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]]. The context is defined by a __direction__, a __cost__, and a __limit__: * Direction: connections are directed. For example, the metadata value of ''backward'' lists all zettel that link to the current zettel, while ''forward'' list all zettel to which the current zettel links to. 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"". * Cost: a normal direct connection has cost 5, an indirect connection is the length of the shortest path between two zettel. The cost of a folgezettel or a precursor zettel is just 1, the cost of successor zettel and predecessor zettel are 2. You should limit the cost by using the parameter ''cost''. Its default value is ""17"". A value of ""0"" does disable any cost 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. The search for the context of a zettel stops at the [[home zettel|00001004020000#home-zettel]]. This zettel is connected to all other zettel. If it is included, 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&cost=5' {"id": "00001012053800","meta": {...},"rights":62,"list":[{"id": "00001012921000","meta": {...},"rights":62},{"id": "00001012920800","meta": {...},"rights":62},{"id": "00010000000000","meta": {...},"rights":62}]} ```` Formatted, this translates into:[^Metadata (key ''meta'') are hidden to make the overall structure easier to read.] ````json { "id": "00001012053800", "meta": {...}, "rights": 62, "list": [ { "id": "00001012921000", "meta": {...}, "rights":62 }, { "id": "00001012920800", "meta": {...}, "rights":62 }, { "id": "00010000000000", "meta": {...}, "rights":62 } ] } ```` === Keys The following top-level JSON keys are returned: ; ''id'' : The [[zettel identifier|00001006050000]] for which the context was requested. ; ''meta'': : The metadata of the zettel, encoded as a JSON object. ; ''rights'' : An integer number that describes the [[access rights|00001012921200]] for the given zettel. ; ''list'' : A list of JSON objects with keys ''id'', ''meta'', and ''rights'' that contains the zettel of the context. === HTTP Status codes ; ''200'' : Retrieval was successful, the body contains an appropriate JSON object. ; ''400'' : Request was not valid. ; ''403'' : You are not allowed to retrieve data of the given zettel. ; ''404'' : Zettel not found. You probably used a zettel identifier that is not used in the Zettelstore. |
Added docs/manual/00001012053900.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 | id: 00001012053900 title: API: Retrieve unlinked references to an existing zettel role: manual tags: #api #manual #zettelstore syntax: zmk created: 20211119133357 modified: 20220913152019 The value of a personal Zettelstore is determined in part by explicit connections between related zettel. If the number of zettel grow, some of these connections are missing. There are various reasons for this. Maybe, you forgot that a zettel exists. Or you add a zettel later, but forgot that previous zettel already mention its title. __Unlinked references__ are phrases in a zettel that mention the title of another, currently unlinked zettel. To retrieve unlinked references to an existing zettel, use the [[endpoint|00001012920000]] ''/u/{ID}''. ```` # curl 'http://127.0.0.1:23123/u/00001007000000' {"id": "00001007000000","meta": {...},"rights":62,"list": [{"id": "00001012070500","meta": {...},"rights":62},...{"id": "00001006020000","meta": {...},"rights":62}]} ```` Formatted, this translates into:[^Metadata (key ''meta'') are hidden to make the overall structure easier to read.] ````json { "id": "00001007000000", "meta": {...}, "rights": 62, "list": [ { "id": "00001012070500", "meta": {...}, "rights": 62 }, ... { "id": "00001006020000", "meta": {...}, "rights": 62 } ] } ```` This call searches within all zettel whether the title of the specified zettel occurs there. The other zettel must not link to the specified zettel. The title must not occur within a link (e.g. to another zettel), in a [[heading|00001007030300]], in a [[citation|00001007040340]], and must have a uniform formatting. The match must be exact, but is case-insensitive. If the title of the specified zettel contains some extra character that probably reduce the number of found unlinked references, you can specify the title phase to be searched for as a query parameter ''phrase'': ```` # curl 'http://127.0.0.1:23123/u/00001007000000?phrase=markdown' {"id": "00001007000000","meta": {...},"list": [{"id": "00001008010000","meta": {...},"rights":62},{"id": "00001004020000","meta": {...},"rights":62}]} ```` %%TODO: In addition, you are allowed to limit the search by a [[query expression|00001012051840]], which may search for zettel content. === Keys The following top-level JSON keys are returned: ; ''id'' : The [[zettel identifier|00001006050000]] for which the unlinked references were requested. ; ''meta'': : The metadata of the zettel, encoded as a JSON object. ; ''rights'' : An integer number that describes the [[access rights|00001012921200]] for the given zettel. ; ''list'' : A list of JSON objects with keys ''id'', ''meta'', and ''rights'' that describe zettel with unlinked references. === HTTP Status codes ; ''200'' : Retrieval was successful, the body contains an appropriate JSON object. ; ''400'' : Request was not valid. ; ''403'' : You are not allowed to retrieve data of the given zettel. ; ''404'' : Zettel not found. You probably used a zettel identifier that is not used in the Zettelstore. |
Added docs/manual/00001012054000.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 | id: 00001012054000 title: API: Retrieve zettel order within an existing zettel role: manual tags: #api #manual #zettelstore syntax: zmk modified: 20220202112451 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. To retrieve the ""table of contents"", the software looks at first level [[list items|00001007030200]]. If an item contains a valid reference to a zettel, this reference will be interpreted as an item in the table of contents. This applies only to first level list items (ordered or unordered list), but not to deeper levels. Only the first reference to a valid zettel is collected for the table of contents. Following references to zettel within such an list item are ignored. To retrieve the zettel order of an existing zettel, use the [[endpoint|00001012920000]] ''/o/{ID}''. ```` # curl http://127.0.0.1:23123/o/00001000000000 {"id":"00001000000000","meta":{...},"rights":62,"list":[{"id":"00001001000000","meta":{...},"rights":62},{"id":"00001002000000","meta":{...},"rights":62},{"id":"00001003000000","meta":{...},"rights":62},{"id":"00001004000000","meta":{...},"rights":62},...,{"id":"00001014000000","meta":{...},"rights":62}]} ```` Formatted, this translates into:[^Metadata (key ''meta'') are hidden to make the overall structure easier to read.] ````json { "id": "00001000000000", "meta": {...}, "rights": 62, "list": [ { "id": "00001001000000", "meta": {...}, "rights": 62 }, { "id": "00001002000000", "meta": {...}, "rights": 62 }, { "id": "00001003000000", "meta": {...}, "rights": 62 }, { "id": "00001004000000", "meta": {...}, "rights": 62 }, ... { "id": "00001014000000", "meta": {...}, "rights": 62 } ] } ```` The following top-level JSON keys are returned: ; ''id'' : The [[zettel identifier|00001006050000]] for which the references were requested. ; ''meta'': : The metadata of the zettel, encoded as a JSON object. ; ''rights'' : An integer number that describes the [[access rights|00001012921200]] for the given zettel. ; ''list'' : A list of JSON objects with keys ''id'', ''meta'', and ''rights'' that describe other zettel in the defined order. === HTTP Status codes ; ''200'' : Retrieval was successful, the body contains an appropriate JSON object. ; ''400'' : Request was not valid. ; ''403'' : You are not allowed to retrieve data of the given zettel. ; ''404'' : Zettel not found. You probably used a zettel identifier that is not used in the Zettelstore. |
Changes to docs/manual/00001012054200.zettel.
1 2 3 4 5 6 | id: 00001012054200 title: API: Update a zettel role: manual tags: #api #manual #zettelstore syntax: zmk created: 20210713150005 | | | < | | > > > > > < < > | 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 | id: 00001012054200 title: API: Update a zettel role: manual tags: #api #manual #zettelstore syntax: zmk created: 20210713150005 modified: 20221219155029 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|00001006050000]]. You must send a HTTP PUT request to that endpoint. The zettel must be encoded in a [[plain|00001006000000]] format: first comes the [[metadata|00001006010000]] and the following content is separated by an empty line. This is the same format as used by storing zettel within a [[directory box|00001006010000]]. ``` # curl -X POST --data 'title: Updated Note\n\nUpdated content.' http://127.0.0.1:23123/z/00001012054200 ``` Alternatively, you can use the JSON encoding by using the query parameter ''enc=json''. ``` # curl -X PUT --data '{}' 'http://127.0.0.1:23123/z/00001012054200?enc=json' ``` 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. |
︙ | ︙ |
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 49 | id: 00001012054400 title: API: Rename a zettel role: manual tags: #api #manual #zettelstore syntax: zmk created: 20210713150005 modified: 20221219154659 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|00001005090000]], for example. The [[endpoint|00001012920000]] to rename a zettel is ''/z/{ID}'', where ''{ID}'' is a placeholder for the [[zettel identifier|00001006050000]]. 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|00001006050000]]. 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|00001012053300]] 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]] eight 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. |
Changes to docs/manual/00001012070500.zettel.
1 | id: 00001012070500 | | < | | | > | | < | < | | 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: 00001012070500 title: Retrieve administrative data role: manual tags: #api #manual #zettelstore syntax: zmk modified: 20220805174216 The [[endpoint|00001012920000]] ''/x'' allows you to retrieve some (administrative) data. Currently, you can only request Zettelstore version data. ```` # curl 'http://127.0.0.1:23123/x' {"major":0,"minor":4,"patch":0,"info":"dev","hash":"cb121cc980-dirty"} ```` Zettelstore conforms somehow to the Standard [[Semantic Versioning|https://semver.org/]]. The names ""major"", ""minor"", and ""patch"" are described in this standard. The name ""info"" contains sometimes some additional information, e.g. ""dev"" for a development version, or ""preview"" for a preview version. The name ""hash"" contains some data to identify the version from a developers perspective. === HTTP Status codes ; ''200'' : Retrieval was successful, the body contains an appropriate JSON object. |
Changes to docs/manual/00001012080200.zettel.
1 2 3 4 5 6 | id: 00001012080200 title: API: Check for authentication role: manual tags: #api #manual #zettelstore syntax: zmk created: 20220103224858 | | | | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 | id: 00001012080200 title: API: Check for authentication role: manual tags: #api #manual #zettelstore syntax: zmk created: 20220103224858 modified: 20220908163156 API clients typically wants to know, whether [[authentication is enabled|00001010040100]] or not. If authentication is enabled, they present some form of user interface to get user name and password for the actual authentication. Then they try to [[obtain an access token|00001012050200]]. If authentication is disabled, these steps are not needed. To check for enabled authentication, you must send a HTTP POST request to the [[endpoint|00001012920000]] ''/x'' and you must specify the query parameter ''cmd=authenticated''. ```sh |
︙ | ︙ |
Changes to docs/manual/00001012080500.zettel.
1 2 3 4 5 6 | id: 00001012080500 title: API: Refresh internal data role: manual tags: #api #manual #zettelstore syntax: zmk created: 20211230230441 | | | | | 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: 00001012080500 title: API: Refresh internal data role: manual tags: #api #manual #zettelstore syntax: zmk created: 20211230230441 modified: 20220923104836 Zettelstore maintains some internal data to allow faster operations. One example is the [[content search|00001012051400]] for a term: Zettelstore does not need to scan all zettel to find all occurrences for the term. Instead, all word are stored internally, with a list of zettel where they occur. Another example is the way to determine which zettel are stored in a [[ZIP file|00001004011200]]. Scanning a ZIP file is a lengthy operation, therefore Zettelstore maintains a directory of zettel for each ZIP file. All these internal data may become stale. This should not happen, but when it comes e.g. to file handling, every operating systems behaves differently in very subtle ways. To avoid stopping and re-starting Zettelstore, you can use the API to force Zettelstore to refresh its internal data if you think it is needed. To do this, you must send a HTTP POST request to the [[endpoint|00001012920000]] ''/x'' and you must specify the query parameter ''cmd=refresh''. ```sh # curl -X POST 'http://127.0.0.1:23123/x?cmd=refresh' ``` |
︙ | ︙ |
Changes to docs/manual/00001012920000.zettel.
1 2 3 4 5 6 | id: 00001012920000 title: Endpoints used by the API role: manual tags: #api #manual #reference #zettelstore syntax: zmk created: 20210126175322 | | > > | | | > | 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 created: 20210126175322 modified: 20221219162335 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 resource 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 | ''a'' | POST: [[client authentication|00001012050200]] | | **A**uthenticate | | PUT: [[renew access token|00001012050400]] | | ''o'' | | GET: [[list zettel order|00001012054000]] | **O**rder | ''u'' | | GET [[unlinked references|00001012053900]] | **U**nlinked | ''x'' | GET: [[retrieve administrative data|00001012070500]] | GET: [[list zettel context|00001012053800]] | Conte**x**t | | POST: [[execute command|00001012080100]] | ''z'' | GET: [[list zettel|00001012051200]] | GET: [[retrieve zettel|00001012053300]] | **Z**ettel | | POST: [[create new zettel|00001012053200]] | PUT: [[update a zettel|00001012054200]] | | | DELETE: [[delete zettel|00001012054600]] | | | MOVE: [[rename 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 docs/manual/00001012920500.zettel.
1 | id: 00001012920500 | | | < > < | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 | id: 00001012920500 title: Encodings available via the [[API|00001012000000]] role: manual tags: #api #manual #reference #zettelstore syntax: zmk created: 20210126175322 modified: 20230316180859 A zettel representation can be encoded in various formats for further processing. * [[html|00001012920510]] * [[md|00001012920513]] * [[sexpr|00001012920516]] * [[shtml|00001012920525]] * [[text|00001012920519]] * [[zmk|00001012920522]] |
Changes to docs/manual/00001012920510.zettel.
1 2 3 4 5 | id: 00001012920510 title: HTML Encoding role: manual tags: #api #manual #reference #zettelstore syntax: zmk | < | 1 2 3 4 5 6 7 8 9 10 11 12 | id: 00001012920510 title: HTML Encoding role: manual tags: #api #manual #reference #zettelstore syntax: zmk modified: 20210726193034 A zettel representation in HTML. This representation is different form the [[web user interface|00001014000000]] as it contains the zettel representation only and no additional data such as the menu bar. It is intended to be used by external clients. |
︙ | ︙ |
Changes to docs/manual/00001012920516.zettel.
1 | id: 00001012920516 | | | | | | | | | > | > > > > > > | > > > > > > > > > > > > > > > > > > | 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 | id: 00001012920516 title: Sexpr Encoding role: manual tags: #api #manual #reference #zettelstore syntax: zmk created: 20220422181104 modified: 20230316182622 A zettel representation that is a [[s-expression|https://en.wikipedia.org/wiki/S-expression]] (also known as symbolic expression). It is (relatively) easy to parse and contain all relevant information of a zettel, metadata and content. For example, take a look at the Sexpr encoding of this page, which is available via the ""Info"" sub-page of this zettel: * [[//z/00001012920516?enc=sexpr&part=zettel]], * [[//z/00001012920516?enc=sexpr&part=meta]], * [[//z/00001012920516?enc=sexpr&part=content]]. If transferred via HTTP, the content type will be ''text/plain''. === Syntax of s-expressions There are only two types of elements: atoms and lists. A list always starts with the left parenthesis (""''(''"", U+0028) and ends with a right parenthesis (""'')''"", U+0029). A list may contain a possibly empty sequence of elements, i.e. lists and / or atoms. Before the last element of a list of at least to elements, a full stop character (""''.''"", U+002E) signal a pair as the last two elements. This allows a more space economic storage of data. There are three syntactic forms for an atom: numbers, symbols and strings. A number is a non-empty sequence of digits (""0"" ... ""9""). The smallest number is ``0``, there are no negative numbers. A symbol is a non-empty sequence of printable characters, except left or right parenthesis. Unicode characters of the following categories contains printable characters in the above sense: letter (L), number (N), punctuation (P), symbol (S). Symbols are case-insensitive, i.e. ""''ZETTEL''"" and ""''zettel''"" denote the same symbol. A string starts with a quotation mark (""''"''"", U+0022), contains a possibly empty sequence of Unicode characters, and ends with a quotation mark. To allow a string to contain a quotations mark, it must be prefixed by one backslash (""''\\''"", U+005C). To allow a string to contain a backslash, it also must be prefixed by one backslash. Unicode characters with a code less than U+FF are encoded by by the sequence ""''\\xNM''"", where ''NM'' is the hex encoding of the character. Unicode characters with a code less than U+FFFF are encoded by by the sequence ""''\\uNMOP''"", where ''NMOP'' is the hex encoding of the character. Unicode characters with a code less than U+FFFFFF are encoded by by the sequence ""''\\UNMOPQR''"", where ''NMOPQR'' is the hex encoding of the character. In addition, the sequence ""''\\t''"" encodes a horizontal tab (U+0009), the sequence ""''\\n''"" encodes a line feed (U+000A). Atoms are separated by Unicode characters of category separator (Z). |
Changes to docs/manual/00001012920519.zettel.
1 2 3 4 5 | id: 00001012920519 title: Text Encoding role: manual tags: #api #manual #reference #zettelstore syntax: zmk | < | 1 2 3 4 5 6 7 8 9 10 11 12 | id: 00001012920519 title: Text Encoding role: manual tags: #api #manual #reference #zettelstore syntax: zmk modified: 20210726193119 A zettel representation contains just all textual data of a zettel. Could be used for creating a search index. Every line may contain zero, one, or more words, separated by space character. |
︙ | ︙ |
Changes to docs/manual/00001012920522.zettel.
1 2 3 4 5 | id: 00001012920522 title: Zmk Encoding role: manual tags: #api #manual #reference #zettelstore syntax: zmk | < | 1 2 3 4 5 6 7 8 9 10 11 | id: 00001012920522 title: Zmk Encoding role: manual tags: #api #manual #reference #zettelstore syntax: zmk modified: 20211124140857 A zettel representation that tries to recreate a [[Zettelmarkup|00001007000000]] representation of the zettel. Useful if you want to convert [[other markup languages|00001008000000]] to Zettelmarkup (e.g. [[Markdown|00001008010000]]). If transferred via HTTP, the content type will be ''text/plain''. |
Changes to docs/manual/00001012920525.zettel.
1 2 3 4 5 6 | id: 00001012920525 title: SHTML Encoding role: manual tags: #api #manual #reference #zettelstore syntax: zmk created: 20230316181044 | | | | | | | 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: 00001012920525 title: SHTML Encoding role: manual tags: #api #manual #reference #zettelstore syntax: zmk created: 20230316181044 modified: 20230316182614 A zettel representation that is a [[s-expression|https://en.wikipedia.org/wiki/S-expression]], syntactically similar to the [[Sexpr encoding|00001012920516]], but denotes [[HTML|00001012920510]] semantics. It is derived from a XML encoding in s-expressions, called [[SXML|https://en.wikipedia.org/wiki/SXML]]. It is (relatively) easy to parse and contains everything to transform it into real HTML. In contrast to HTML, SHTML is easier to parse and to manipulate. For example, take a look at the SHTML encoding of this page, which is available via the ""Info"" sub-page of this zettel: * [[//z/00001012920525?enc=shtml&part=zettel]], * [[//z/00001012920525?enc=shtml&part=meta]], * [[//z/00001012920525?enc=shtml&part=content]]. If transferred via HTTP, the content type will be ''text/plain''. Internally, if a zettel should be transformed into HTML, the zettel is translated into the [[Sexpr encoding|00001012920516]], which is transformed into this SHTML encoding to produce the [[HTML encoding|00001012920510]]. === Syntax of SHTML There are only two types of elements: atoms and lists, similar to the Sexpr encoding. A list always starts with the left parenthesis (""''(''"", U+0028) and ends with a right parenthesis (""'')''"", U+0029). A list may contain a possibly empty sequence of elements, i.e. lists and / or atoms. Before the last element of a list of at least to elements, a full stop character (""''.''"", U+002E) signal a pair as the last two elements. This allows a more space economic storage of data. An HTML tag like ``< a href="link">Text</a>`` is encoded in SHTML with a list, where the first element is a symbol named a the tag. The second element is an optional encoding of the tag's attributes. Further elements are either other tag encodings or a string. The above tag is encoded as ``(a (@ (href . "link")) "Text")``. Also possible is to encode the attribute without pairs: ``(a (@ (href "link")) "Text")`` (note the missing full stop character). |
Changes to docs/manual/00001012920800.zettel.
1 2 3 4 5 | id: 00001012920800 title: Values to specify zettel parts role: manual tags: #api #manual #reference #zettelstore syntax: zmk | < | 1 2 3 4 5 6 7 8 9 10 11 12 | id: 00001012920800 title: Values to specify zettel parts role: manual tags: #api #manual #reference #zettelstore syntax: zmk modified: 20220214175335 When working with [[zettel|00001006000000]], you could work with the whole zettel, with its metadata, or with its content: ; [!zettel|''zettel''] : Specifies that you work with a zettel as a whole. Contains identifier, metadata, and content of a zettel. ; [!meta|''meta''] |
︙ | ︙ |
Changes to docs/manual/00001012921000.zettel.
1 | id: 00001012921000 | | < | < | > > | < < | < < < < < < < | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 | id: 00001012921000 title: API: JSON structure of an access token tags: #api #manual #reference #zettelstore syntax: zmk role: manual If the [[authentication process|00001012050200]] was successful, an access token with some additional data is returned. The same is true, if the access token was [[renewed|00001012050400]]. The response is structured as an JSON object, with the following named values: |=Name|Description |''access_token''|The access token itself, as string value, which is a [[JSON Web Token|https://tools.ietf.org/html/rfc7519]] (JWT, RFC 7915) |''token_type''|The type of the token, always set to ''"Bearer"'', as described in [[RFC 6750|https://tools.ietf.org/html/rfc6750]] |''expires_in''|An integer that gives a hint about the lifetime / endurance of the token, measured in seconds |
Changes to docs/manual/00001012921200.zettel.
1 2 3 4 5 | id: 00001012921200 title: API: Encoding of Zettel Access Rights 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 | id: 00001012921200 title: API: Encoding of Zettel Access Rights role: manual tags: #api #manual #reference #zettelstore syntax: zmk modified: 20220201171959 Various API calls return a JSON key ''"rights"'' that encodes the access rights the user currently has. It is an integer number between 0 and 62.[^Not all values in this range are used.] The value ""0"" signals that something went wrong internally while determining the access rights. A value of ""1"" says, that the current user has no access right for the given zettel. In most cases, this value will not occur, because only zettel are presented, which are at least readable by the current user. Values ""2"" to ""62"" are binary encoded values, where each bit signals a special right. |=Bit number:|Bit value:|Meaning | 1 | 2 | User is allowed to create a new zettel | 2 | 4 | User is allowed to read the zettel | 3 | 8 | User is allowed to update the zettel | 4 | 16 | User is allowed to rename the zettel | 5 | 32 | User is allowed to delete the zettel The algorithm to calculate the actual access rights from the value is relatively simple: # Search for the biggest bit value that is less than the rights value. This is an access right for the current user. # Subtract the bit value from the rights value. Remember the difference as the new rights value. |
︙ | ︙ |
Deleted docs/manual/00001012930000.zettel.
|
| < < < < < < < < < < < < < < < < < < < < < < < < < < |
Deleted docs/manual/00001012930500.zettel.
|
| < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < |
Deleted docs/manual/00001012931000.zettel.
|
| < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < |
Deleted docs/manual/00001012931200.zettel.
|
| < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < |
Deleted docs/manual/00001012931400.zettel.
|
| < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < |
Deleted docs/manual/00001012931600.zettel.
|
| < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < |
Deleted docs/manual/00001012931800.zettel.
|
| < < < < < < < < < < < < < < < < < < < < < < < < < < < |
Deleted docs/manual/00001012931900.zettel.
|
| < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < |
Changes to docs/manual/00001014000000.zettel.
1 2 3 4 5 | id: 00001014000000 title: Web user interface tags: #manual #webui #zettelstore syntax: zmk role: manual | < | 1 2 3 4 5 6 7 8 | id: 00001014000000 title: Web user interface tags: #manual #webui #zettelstore syntax: zmk role: manual The Web user interface is just a secondary way to interact with a Zettelstore. Using external software that interacts via the [[API|00001012000000]] is the recommended way. |
Changes to docs/manual/00001017000000.zettel.
1 2 3 4 5 6 | id: 00001017000000 title: Tips and Tricks role: manual tags: #manual #zettelstore syntax: zmk created: 20220803170112 | | | | | | | | | | | | | > | | | | | | 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 | id: 00001017000000 title: Tips and Tricks role: manual tags: #manual #zettelstore syntax: zmk created: 20220803170112 modified: 20221205154406 === Welcome Zettel * **Problem:** You want to put your Zettelstore into the public and need a starting zettel for your users. In addition, you still want a ""home zettel"", with all your references to internal, non-public zettel. Zettelstore only allows to specify one [[''home-zettel''|00001004020000#home-zettel]]. * **Solution 1:** *# Create a new zettel with all your references to internal, non-public zettel. Let's assume this zettel receives the zettel identifier ''20220803182600''. *# Create the zettel that should serve as the starting zettel for your users. It must have syntax [[Zettelmarkup|00001008000000#zmk]], i.e. the syntax metadata must be set to ''zmk''. If needed, set the runtime configuration [[''home-zettel|00001004020000#home-zettel]] to the value of the identifier of this zettel. *# At the beginning of the start zettel, add the following [[Zettelmarkup|00001007000000]] text in a separate paragraph: ``{{{20220803182600}}}`` (you have to adapt to the actual value of the zettel identifier for your non-public home zettel). * **Discussion:** As stated in the description for a [[transclusion|00001007031100]], a transclusion will be ignored, if the transcluded zettel is not visible to the current user. In effect, the transclusion statement (above paragraph that contained ''{{{...}}}'') is ignored when rendering the zettel. * **Solution 2:** Set a user-specific value by adding metadata ''home-zettel'' to the [[user zettel|00001010040200]]. * **Discussion:** A value for ''home-zettel'' is first searched in the user zettel of the current authenticated user. Only if it is not found, the value is looked up in the runtime configuration zettel. If multiple user should use the same home zettel, its zettel identifier must be set in all relevant user zettel. === Role-specific Layout of Zettel in Web User Interface (WebUI) [!role-css] * **Problem:** You want to add some CSS when displaying zettel of a specific [[role|00001006020000#role]]. For example, you might want to add a yellow background color for all [[configuration|00001006020100#configuration]] zettel. Or you want a multi-column layout. * **Solution:** If you enable [[''expert-mode''|00001004020000#expert-mode]], you will have access to a zettel called ""[[Zettelstore Role to CSS Map|00000000029000]]"" (its identifier is ''00000000029000''). This zettel maps a role name to a zettel that must contain the role-specific CSS code. First, create a zettel containing the needed CSS: give it any title, its role is preferably ""configuration"" (but this is not a must). Set its [[''syntax''|00001006020000#syntax]] must be set to ""[[css|00001008000000#css]]"". The content must contain the role-specific CSS code, for example ``body {background-color: #FFFFD0}``for a background in a light yellow color. Let's assume, the newly created CSS zettel got the identifier ''20220825200100''. Now, you have to connect this zettel to the zettel called ""Zettelstore Role CSS Map"". Since you have enabled ''expert-mode'', you are allowed to modify it. Add the following metadata ''css-configuration-zid: 20220825200100'' to assign the role-specific CSS code for the role ""configuration"" to the CSS zettel containing that CSS. In general, its role-assigning metadata must be like this pattern: ''css-ROLE-zid: ID'', where ''ROLE'' is the placeholder for the role, and ''ID'' for the zettel identifier containing CSS code. It is allowed to assign more than one role to a specific CSS zettel. * **Discussion:** you have to ensure that the CSS zettel is allowed to be read by the intended audience of the zettel with that given role. For example, if you made zettel with a specific role public visible, the CSS zettel must also have a [[''visibility: public''|00001010070200]] metadata. * **Extension:** if you have already established a role-specific layout for zettel, but you additionally want just one another zettel with another role to be rendered with the same CSS, you have to add metadata to the one zettel: ''css-role: ROLE'', where ''ROLE'' is the placeholder for the role that already is assigned to a specific CSS-based layout. === Zettel synchronization with iCloud (Apple) * **Problem:** You use Zettelstore on various macOS computers and you want to use the sameset of zettel across all computers. * **Solution:** Place your zettel in an iCloud folder. To configure Zettelstore to use the folder, you must specify its location within you directory structure as [[''box-uri-X''|00001004010000#box-uri-x]] (replace ''X'' with an appropriate number). Your iCloud folder is typically placed in the folder ''~/Library/Mobile Documents/com~apple~CloudDocs''. The ""''~''"" is a shortcut and specifies your home folder. Unfortunately, Zettelstore does not yet support this shortcut. Therefore you must replace it with the absolute name of your home folder. In addition, a space character is not allowed in an URI. You have to replace it with the sequence ""''%20''"". Let us assume, that you stored your zettel box inside the folder ""zettel"", which is located top-level in your iCloud folder. In this case, you must specify the following box URI within the startup configuration: ''box-uri-1: dir:///Users/USERNAME/Library/Mobile%20Documents/com~apple~CloudDocs/zettel'', replacing ''USERNAME'' with the username of that specific computer (and assuming you want to use it as the first box). * **Solution 2:** If you typically start your Zettelstore on the command line, you could use the ''-d DIR'' option for the [[''run''|00001004051000#d]] sub-command. In this case you are allowed to use the character ""''~''"". ''zettelstore run -d ~/Library/Mobile\\ Documents/com\\~apple\\~CloudDocs/zettel'' (The ""''\\''"" is needed by the command line processor to mask the following character to be processed in unintended ways.) * **Discussion:** Zettel files are synchronized between your computers via iCloud. Is does not matter, if one of your computer is offline / switched off. iCloud will synchronize the zettel files if it later comes online. However, if you use more than one computer simultaneously, you must be aware that synchronization takes some time. It might take several seconds, maybe longer, that new new version of a zettel appears on the other computer. If you update the same zettel on multiple computers at nearly the same time, iCloud will not be able to synchronize the different versions in a safe manner. Zettelstore is intentionally not aware of any synchronization within its zettel boxes. If Zettelstore behaves strangely after a synchronization took place, the page about [[Troubleshooting|00001018000000#working-with-files]] might contain some useful information. |
Changes to docs/manual/00001018000000.zettel.
1 2 3 4 5 | id: 00001018000000 title: Troubleshooting role: manual tags: #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 49 50 51 52 53 54 55 56 | id: 00001018000000 title: Troubleshooting role: manual tags: #manual #zettelstore syntax: zmk created: 00010101000000 modified: 20221020132617 This page lists some problems and their solutions that may occur when using your Zettelstore. === Installation * **Problem:** When you double-click on the Zettelstore executable icon, macOS complains that Zettelstore is an application from an unknown developer. Therefore, it will not start Zettelstore. ** **Solution:** Press the ''Ctrl'' key while opening the context menu of the Zettelstore executable with a right-click. A dialog is then opened where you can acknowledge that you understand the possible risks when you start Zettelstore. This dialog is only resented once for a given Zettelstore executable. * **Problem:** When you double-click on the Zettelstore executable icon, Windows complains that Zettelstore is an application from an unknown developer. ** **Solution:** Windows displays a dialog where you can acknowledge possible risks and allows to start Zettelstore. === Authentication * **Problem:** [[Authentication is enabled|00001010040100]] for a local running Zettelstore and there is a valid [[user zettel|00001010040200]] for the owner. But entering user name and password at the [[web user interface|00001014000000]] seems to be ignored, while entering a wrong password will result in an error message. ** **Explanation:** A local running Zettelstore typically means, that you are accessing the Zettelstore using an URL with schema ''http://'', and not ''https://'', for example ''http://localhost:23123''. The difference between these two is the missing encryption of user name / password and for the answer of the Zettelstore if you use the ''http://'' schema. To be secure by default, the Zettelstore will not work in an insecure environment. ** **Solution 1:** If you are sure that your communication medium is safe, even if you use the ''http:/\/'' schema (for example, you are running the Zettelstore on the same computer you are working on, or if the Zettelstore is running on a computer in your protected local network), then you could add the entry ''insecure-cookie: true'' in you [[startup configuration|00001004010000#insecure-cookie]] file. ** **Solution 2:** If you are not sure about the security of your communication medium (for example, if unknown persons might use your local network), then you should run an [[external server|00001010090100]] in front of your Zettelstore to enable the use of the ''https://'' schema. === Working with Zettel Files * **Problem:** When you delete a zettel file by removing it from the ""disk"", e.g. by dropping it into the trash folder, by dragging into another folder, or by removing it from the command line, Zettelstore sometimes did not detect that change. If you access the zettel via Zettelstore, a fatal error is reported. ** **Explanation:** Sometimes, the operating system does not tell Zettelstore about the removed zettel. This occurs mostly under MacOS. ** **Solution 1:** If you are running Zettelstore in [[""simple-mode""|00001004051100]] or if you have enabled [[''expert-mode''|00001004020000#expert-mode]], you are allowed to refresh the internal data by selecting ""Refresh"" in the Web User Interface (you find it in the menu ""Lists""). ** **Solution 2:** There is an [[API|00001012080500]] call to make Zettelstore aware of this change. ** **Solution 3:** If you have an enabled [[Administrator Console|00001004100000]] you can use the command [[''refresh''|00001004101000#refresh]] to make your changes visible. ** **Solution 4:** You configure the zettel box as [[""simple""|00001004011400]]. === HTML content is not shown * **Problem:** You have entered some HTML code as content for your Zettelstore, but this content is not shown on the Web User Interface. You may have entered a Zettel with syntax [[""html""|00001008000000#html]], or you have used an [[inline-zettel block|00001007031200]] with syntax ""html"", or you entered a Zettel with syntax [[""markdown""|00001008000000#markdown]] (or ""md"") and used some HTML code fragments. ** **Explanation:** Working with HTML code from unknown sources may lead so severe security problems. The HTML code may force web browsers to load more content from external server, it may contain malicious JavaScript code, it may reference to CSS artifacts that itself load from external servers and may contains malicious software. Zettelstore tries to do its best to ignore problematic HTML code, but it may fail. Either because of unknown bugs or because of yet unknown changes in the future. Zettelstore sets a restrictive [[Content Security Policy|https://www.w3.org/TR/CSP/]], but this depends on web browsers to implement them correctly and on users to not disable it. Zettelstore will not display any HTML code, which contains a ``<script>>`` or an ``<iframe>`` tag. But attackers may find other ways to deploy their malicious code. Therefore, Zettelstore disallows any HTML content as a default. If you know what you are doing, e.g. because you will never copy HTML code you do not understand, you can relax this default. ** **Solution 1:** If you want zettel with syntax ""html"" not to be ignored, you set the startup configuration key [[''insecure-html''|00001004010000#insecure-html]] to the value ""html"". ** **Solution 2:** If you want zettel with syntax ""html"" not to be ignored, **and** want to allow HTML in Markdown, you set the startup configuration key [[''insecure-html''|00001004010000#insecure-html]] to the value ""markdown"". ** **Solution 3:** If you want zettel with syntax ""html"" not to be ignored, **and** want to allow HTML in Markdown, **and** want to use HTML code within Zettelmarkup, you set the startup configuration key [[''insecure-html''|00001004010000#insecure-html]] to the value ""zettelmarkup"". |
Deleted docs/manual/00001019990010.zettel.
|
| < < < < < < < < |
Changes to docs/readmezip.txt.
︙ | ︙ | |||
13 14 15 16 17 18 19 | https://zettelstore.de/manual/. It is a live example of the zettelstore software, running in read-only mode. You can download it separately and it is possible to make it directly available for your local Zettelstore. The software, including the manual, is licensed under the European Union Public License 1.2 (or later). See the separate file LICENSE.txt. | | > | 13 14 15 16 17 18 19 20 21 | https://zettelstore.de/manual/. It is a live example of the zettelstore software, running in read-only mode. You can download it separately and it is possible to make it directly available for your local Zettelstore. The software, including the manual, is licensed under the European Union Public License 1.2 (or later). See the separate file LICENSE.txt. To get in contact with the developer, send an email to ds@zettelstore.de or follow Zettelstore on Twitter: https://twitter.com/zettelstore. |
Added domain/content.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 | //----------------------------------------------------------------------------- // Copyright (c) 2020-present Detlef Stern // // This file is part of Zettelstore. // // Zettelstore is licensed under the latest version of the EUPL (European Union // Public License). Please see file LICENSE.txt for your rights and obligations // under this license. //----------------------------------------------------------------------------- package domain import ( "bytes" "encoding/base64" "errors" "io" "unicode" "unicode/utf8" "zettelstore.de/z/input" ) // Content is just the content of a zettel. type Content struct { data []byte isBinary bool } // NewContent creates a new content from a string. func NewContent(data []byte) Content { return Content{data: data, isBinary: IsBinary(data)} } // Length returns the number of bytes stored. func (zc *Content) Length() int { return len(zc.data) } // Equal compares two content values. func (zc *Content) Equal(o *Content) bool { if zc == nil { return o == nil } if zc.isBinary != o.isBinary { return false } return bytes.Equal(zc.data, o.data) } // Set content to new string value. func (zc *Content) Set(data []byte) { zc.data = data zc.isBinary = IsBinary(data) } // Write it to a Writer func (zc *Content) Write(w io.Writer) (int, error) { return w.Write(zc.data) } // AsString returns the content itself is a string. func (zc *Content) AsString() string { return string(zc.data) } // AsBytes returns the content itself is a byte slice. func (zc *Content) AsBytes() []byte { return zc.data } // IsBinary returns true if the content contains non-unicode values or is, // interpreted a text, with a high probability binary content. func (zc *Content) IsBinary() bool { return zc.isBinary } // TrimSpace remove some space character in content, if it is not binary content. func (zc *Content) TrimSpace() { if zc.isBinary { return } inp := input.NewInput(zc.data) pos := inp.Pos for inp.Ch != input.EOS { if input.IsEOLEOS(inp.Ch) { inp.Next() pos = inp.Pos continue } if !input.IsSpace(inp.Ch) { break } inp.Next() } zc.data = bytes.TrimRightFunc(inp.Src[pos:], unicode.IsSpace) } // Encode content for future transmission. func (zc *Content) Encode() (data, encoding string) { if !zc.isBinary { return zc.AsString(), "" } return base64.StdEncoding.EncodeToString(zc.data), "base64" } // SetDecoded content to the decoded value of the given string. func (zc *Content) SetDecoded(data, encoding string) error { switch encoding { case "": zc.data = []byte(data) case "base64": decoded, err := base64.StdEncoding.DecodeString(data) if err != nil { return err } zc.data = decoded default: return errors.New("unknown encoding " + encoding) } zc.isBinary = IsBinary(zc.data) return nil } // IsBinary returns true if the given data appears to be non-text data. func IsBinary(data []byte) bool { if !utf8.Valid(data) { return true } l := len(data) for i := 0; i < l; i++ { if data[i] == 0 { return true } } return false } |
Added domain/content_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 | //----------------------------------------------------------------------------- // Copyright (c) 2020-present Detlef Stern // // This file is part of Zettelstore. // // Zettelstore is licensed under the latest version of the EUPL (European Union // Public License). Please see file LICENSE.txt for your rights and obligations // under this license. //----------------------------------------------------------------------------- package 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}, {string([]byte{0}), true}, } for i, tc := range td { content := domain.NewContent([]byte(tc.s)) got := content.IsBinary() if got != tc.exp { t.Errorf("TC=%d: expected %v, got %v", i, tc.exp, got) } } } func TestTrimSpace(t *testing.T) { t.Parallel() testcases := []struct { in, exp string }{ {"", ""}, {" ", ""}, {"abc", "abc"}, {" abc", " abc"}, {"abc ", "abc"}, {"abc \n", "abc"}, {"abc\n ", "abc"}, {"\nabc", "abc"}, {" \nabc", "abc"}, {" \n abc", " abc"}, {" \n\n abc", " abc"}, {" \n \n abc", " abc"}, {" \n \n abc \n \n ", " abc"}, } for _, tc := range testcases { c := domain.NewContent([]byte(tc.in)) c.TrimSpace() got := c.AsString() if got != tc.exp { t.Errorf("TrimSpace(%q) should be %q, but got %q", tc.in, tc.exp, got) } } } |
Added domain/id/id.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 | //----------------------------------------------------------------------------- // Copyright (c) 2020-present Detlef Stern // // This file is part of Zettelstore. // // Zettelstore is licensed under the latest version of the EUPL (European Union // Public License). Please see file LICENSE.txt for your rights and obligations // under this license. //----------------------------------------------------------------------------- // Package id provides domain specific types, constants, and functions about // zettel identifier. package id import ( "strconv" "time" "zettelstore.de/c/api" ) // Zid is the internal identifier of a zettel. Typically, it is a // time stamp of the form "YYYYMMDDHHmmSS" converted to an unsigned integer. // A zettelstore implementation should try to set the last two digits to zero, // e.g. the seconds should be zero, type Zid uint64 // Some important ZettelIDs. const ( Invalid = Zid(0) // Invalid is a Zid that will never be valid ) // ZettelIDs that are used as Zid more than once. // // Note: if you change some values, ensure that you also change them in the // Constant box. They are mentioned there literally, because these // constants are not available there. var ( ConfigurationZid = MustParse(api.ZidConfiguration) BaseTemplateZid = MustParse(api.ZidBaseTemplate) LoginTemplateZid = MustParse(api.ZidLoginTemplate) ListTemplateZid = MustParse(api.ZidListTemplate) ZettelTemplateZid = MustParse(api.ZidZettelTemplate) InfoTemplateZid = MustParse(api.ZidInfoTemplate) FormTemplateZid = MustParse(api.ZidFormTemplate) RenameTemplateZid = MustParse(api.ZidRenameTemplate) DeleteTemplateZid = MustParse(api.ZidDeleteTemplate) ContextTemplateZid = MustParse(api.ZidContextTemplate) ErrorTemplateZid = MustParse(api.ZidErrorTemplate) RoleCSSMapZid = MustParse(api.ZidRoleCSSMap) EmojiZid = MustParse(api.ZidEmoji) TOCNewTemplateZid = MustParse(api.ZidTOCNewTemplate) DefaultHomeZid = MustParse(api.ZidDefaultHome) ) const maxZid = 99999999999999 // ParseUint interprets a string as a possible zettel identifier // and returns its integer value. func ParseUint(s string) (uint64, error) { res, err := strconv.ParseUint(s, 10, 47) if err != nil { return 0, err } if res == 0 || res > maxZid { return res, strconv.ErrRange } return res, nil } // Parse interprets a string as a zettel identification and // returns its value. func Parse(s string) (Zid, error) { if len(s) != 14 { return Invalid, strconv.ErrSyntax } res, err := ParseUint(s) if err != nil { return Invalid, err } return Zid(res), nil } // MustParse tries to interpret a string as a zettel identifier and returns // its value or panics otherwise. func MustParse(s api.ZettelID) Zid { zid, err := Parse(string(s)) if err == nil { return zid } panic(err) } // String converts the zettel identification to a string of 14 digits. // Only defined for valid ids. func (zid Zid) String() string { var result [14]byte zid.toByteArray(&result) return string(result[:]) } // Bytes converts the zettel identification to a byte slice of 14 digits. // Only defined for valid ids. func (zid Zid) Bytes() []byte { var result [14]byte zid.toByteArray(&result) return result[:] } // toByteArray converts the Zid into a fixed byte array, usable for printing. // // Based on idea by Daniel Lemire: "Converting integers to fix-digit representations quickly" // https://lemire.me/blog/2021/11/18/converting-integers-to-fix-digit-representations-quickly/ func (zid Zid) toByteArray(result *[14]byte) { date := uint64(zid) / 1000000 fullyear := date / 10000 century := fullyear / 100 year := fullyear % 100 monthday := date % 10000 month := monthday / 100 day := monthday % 100 time := uint64(zid) % 1000000 hmtime := time / 100 second := time % 100 hour := hmtime / 100 minute := hmtime % 100 result[0] = byte(century/10) + '0' result[1] = byte(century%10) + '0' result[2] = byte(year/10) + '0' result[3] = byte(year%10) + '0' result[4] = byte(month/10) + '0' result[5] = byte(month%10) + '0' result[6] = byte(day/10) + '0' result[7] = byte(day%10) + '0' result[8] = byte(hour/10) + '0' result[9] = byte(hour%10) + '0' result[10] = byte(minute/10) + '0' result[11] = byte(minute%10) + '0' result[12] = byte(second/10) + '0' result[13] = byte(second%10) + '0' } // IsValid determines if zettel id is a valid one, e.g. consists of max. 14 digits. func (zid Zid) IsValid() bool { return 0 < zid && zid <= maxZid } // ZidLayout to transform a date into a Zid and into other internal dates. const ZidLayout = "20060102150405" // New returns a new zettel id based on the current time. func New(withSeconds bool) Zid { now := time.Now().Local() var s string if withSeconds { s = now.Format(ZidLayout) } else { s = now.Format("20060102150400") } res, err := Parse(s) if err != nil { panic(err) } return res } |
Added domain/id/id_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 | //----------------------------------------------------------------------------- // Copyright (c) 2020-present Detlef Stern // // This file is part of Zettelstore. // // Zettelstore is licensed under the latest version of the EUPL (European Union // Public License). Please see file LICENSE.txt for your rights and obligations // under this license. //----------------------------------------------------------------------------- // Package id_test provides unit tests for testing zettel id specific functions. package id_test import ( "testing" "zettelstore.de/z/domain/id" ) func TestIsValid(t *testing.T) { t.Parallel() validIDs := []string{ "00000000000001", "00000000000020", "00000000000300", "00000000004000", "00000000050000", "00000000600000", "00000007000000", "00000080000000", "00000900000000", "00001000000000", "00020000000000", "00300000000000", "04000000000000", "50000000000000", "99999999999999", "00001007030200", "20200310195100", } for i, sid := range validIDs { zid, err := id.Parse(sid) if err != nil { t.Errorf("i=%d: sid=%q is not valid, but should be. err=%v", i, sid, err) } s := zid.String() if s != sid { t.Errorf( "i=%d: zid=%v does not format to %q, but to %q", i, zid, sid, s) } } invalidIDs := []string{ "", "0", "a", "00000000000000", "0000000000000a", "000000000000000", "20200310T195100", } for i, sid := range invalidIDs { if zid, err := id.Parse(sid); err == nil { t.Errorf("i=%d: sid=%q is valid (zid=%s), but should not be", i, sid, zid) } } } var sResult string // to disable compiler optimization in loop below func BenchmarkString(b *testing.B) { var s string for n := 0; n < b.N; n++ { s = id.Zid(12345678901200).String() } sResult = s } var bResult []byte // to disable compiler optimization in loop below func BenchmarkBytes(b *testing.B) { var bs []byte for n := 0; n < b.N; n++ { bs = id.Zid(12345678901200).Bytes() } bResult = bs } |
Added domain/id/set.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 | //----------------------------------------------------------------------------- // Copyright (c) 2021-present Detlef Stern // // This file is part of Zettelstore. // // Zettelstore is licensed under the latest version of the EUPL (European Union // Public License). Please see file LICENSE.txt for your rights and obligations // under this license. //----------------------------------------------------------------------------- package id // Set is a set of zettel identifier type Set map[Zid]struct{} // NewSet returns a new set of identifier with the given initial values. func NewSet(zids ...Zid) Set { l := len(zids) if l < 8 { l = 8 } result := make(Set, l) result.AddSlice(zids) return result } // NewSetCap returns a new set of identifier with the given capacity and initial values. func NewSetCap(c int, zids ...Zid) Set { l := len(zids) if c < l { c = l } if c < 8 { c = 8 } result := make(Set, c) result.AddSlice(zids) return result } // Zid adds a Zid to the set. func (s Set) Zid(zid Zid) Set { if s == nil { return NewSet(zid) } s[zid] = struct{}{} return s } // Contains return true if the set is nil or if the set contains the given Zettel identifier. func (s Set) Contains(zid Zid) bool { if s != nil { _, found := s[zid] return found } return true } // Add all member from the other set. func (s Set) Add(other Set) Set { if s == nil { return other } for zid := range other { s[zid] = struct{}{} } return s } // AddSlice adds all identifier of the given slice to the set. func (s Set) AddSlice(sl Slice) { for _, zid := range sl { s[zid] = struct{}{} } } // Sorted returns the set as a sorted slice of zettel identifier. func (s Set) Sorted() Slice { if l := len(s); l > 0 { result := make(Slice, 0, l) for zid := range s { result = append(result, zid) } result.Sort() return result } return nil } // IntersectOrSet removes all zettel identifier that are not in the other set. // Both sets can be modified by this method. One of them is the set returned. // It contains the intersection of both, if s is not nil. // // If s == nil, then the other set is always returned. func (s Set) IntersectOrSet(other Set) Set { if s == nil { return other } if len(s) > len(other) { s, other = other, s } for zid := range s { _, otherOk := other[zid] if !otherOk { delete(s, zid) } } return s } // Remove all zettel identifier from 's' that are in the set 'other'. func (s Set) Remove(other Set) { if s == nil || other == nil { return } for zid := range other { delete(s, zid) } } |
Added domain/id/set_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 | //----------------------------------------------------------------------------- // Copyright (c) 2021-present Detlef Stern // // This file is part of Zettelstore. // // Zettelstore is licensed under the latest version of the EUPL (European Union // Public License). Please see file LICENSE.txt for your rights and obligations // under this license. //----------------------------------------------------------------------------- package id_test import ( "testing" "zettelstore.de/z/domain/id" ) func TestSetContains(t *testing.T) { t.Parallel() testcases := []struct { s id.Set zid id.Zid exp bool }{ {nil, id.Invalid, true}, {nil, 14, true}, {id.NewSet(), id.Invalid, false}, {id.NewSet(), 1, false}, {id.NewSet(), id.Invalid, false}, {id.NewSet(1), 1, true}, } for i, tc := range testcases { got := tc.s.Contains(tc.zid) if got != tc.exp { t.Errorf("%d: %v.Contains(%v) == %v, but got %v", i, tc.s, tc.zid, tc.exp, got) } } } func TestSetAdd(t *testing.T) { t.Parallel() testcases := []struct { s1, s2 id.Set exp id.Slice }{ {nil, nil, nil}, {id.NewSet(), nil, nil}, {id.NewSet(), id.NewSet(), nil}, {nil, id.NewSet(1), id.Slice{1}}, {id.NewSet(1), nil, id.Slice{1}}, {id.NewSet(1), id.NewSet(), id.Slice{1}}, {id.NewSet(1), id.NewSet(2), id.Slice{1, 2}}, {id.NewSet(1), id.NewSet(1), id.Slice{1}}, } for i, tc := range testcases { sl1 := tc.s1.Sorted() sl2 := tc.s2.Sorted() got := tc.s1.Add(tc.s2).Sorted() if !got.Equal(tc.exp) { t.Errorf("%d: %v.Add(%v) should be %v, but got %v", i, sl1, sl2, tc.exp, got) } } } func TestSetSorted(t *testing.T) { t.Parallel() testcases := []struct { set id.Set exp id.Slice }{ {nil, nil}, {id.NewSet(), nil}, {id.NewSet(9, 4, 6, 1, 7), id.Slice{1, 4, 6, 7, 9}}, } for i, tc := range testcases { got := tc.set.Sorted() if !got.Equal(tc.exp) { t.Errorf("%d: %v.Sorted() should be %v, but got %v", i, tc.set, tc.exp, got) } } } func TestSetIntersectOrSet(t *testing.T) { t.Parallel() testcases := []struct { s1, s2 id.Set exp id.Slice }{ {nil, nil, nil}, {id.NewSet(), nil, nil}, {nil, id.NewSet(), nil}, {id.NewSet(), id.NewSet(), nil}, {id.NewSet(1), nil, nil}, {nil, id.NewSet(1), id.Slice{1}}, {id.NewSet(1), id.NewSet(), nil}, {id.NewSet(), id.NewSet(1), nil}, {id.NewSet(1), id.NewSet(2), nil}, {id.NewSet(2), id.NewSet(1), nil}, {id.NewSet(1), id.NewSet(1), id.Slice{1}}, } for i, tc := range testcases { sl1 := tc.s1.Sorted() sl2 := tc.s2.Sorted() got := tc.s1.IntersectOrSet(tc.s2).Sorted() if !got.Equal(tc.exp) { t.Errorf("%d: %v.IntersectOrSet(%v) should be %v, but got %v", i, sl1, sl2, tc.exp, got) } } } func TestSetRemove(t *testing.T) { t.Parallel() testcases := []struct { s1, s2 id.Set exp id.Slice }{ {nil, nil, nil}, {id.NewSet(), nil, nil}, {id.NewSet(), id.NewSet(), nil}, {id.NewSet(1), nil, id.Slice{1}}, {id.NewSet(1), id.NewSet(), id.Slice{1}}, {id.NewSet(1), id.NewSet(2), id.Slice{1}}, {id.NewSet(1), id.NewSet(1), id.Slice{}}, } for i, tc := range testcases { sl1 := tc.s1.Sorted() sl2 := tc.s2.Sorted() newS1 := id.NewSet(sl1...) newS1.Remove(tc.s2) got := newS1.Sorted() if !got.Equal(tc.exp) { t.Errorf("%d: %v.Remove(%v) should be %v, but got %v", i, sl1, sl2, tc.exp, got) } } } // func BenchmarkSet(b *testing.B) { // s := id.Set{} // for i := 0; i < b.N; i++ { // s[id.Zid(i)] = true // } // } func BenchmarkSet(b *testing.B) { s := id.Set{} for i := 0; i < b.N; i++ { s[id.Zid(i)] = struct{}{} } } |
Added domain/id/slice.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 | //----------------------------------------------------------------------------- // Copyright (c) 2021-present Detlef Stern // // This file is part of Zettelstore. // // Zettelstore is licensed under the latest version of the EUPL (European Union // Public License). Please see file LICENSE.txt for your rights and obligations // under this license. //----------------------------------------------------------------------------- package id import ( "sort" "strings" ) // Slice is a sequence of zettel identifier. A special case is a sorted slice. type Slice []Zid func (zs Slice) Len() int { return len(zs) } func (zs Slice) Less(i, j int) bool { return zs[i] < zs[j] } func (zs Slice) Swap(i, j int) { zs[i], zs[j] = zs[j], zs[i] } // Sort a slice of Zids. func (zs Slice) Sort() { sort.Sort(zs) } // Copy a zettel identifier slice func (zs Slice) Copy() Slice { if zs == nil { return nil } result := make(Slice, len(zs)) copy(result, zs) return result } // Equal reports whether zs and other are the same length and contain the samle zettel // identifier. A nil argument is equivalent to an empty slice. func (zs Slice) Equal(other Slice) bool { if len(zs) != len(other) { return false } if len(zs) == 0 { return true } for i, e := range zs { if e != other[i] { return false } } return true } func (zs Slice) String() string { if len(zs) == 0 { return "" } var sb strings.Builder for i, zid := range zs { if i > 0 { sb.WriteByte(' ') } sb.WriteString(zid.String()) } return sb.String() } |
Added domain/id/slice_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 | //----------------------------------------------------------------------------- // Copyright (c) 2021-present Detlef Stern // // This file is part of Zettelstore. // // Zettelstore is licensed under the latest version of the EUPL (European Union // Public License). Please see file LICENSE.txt for your rights and obligations // under this license. //----------------------------------------------------------------------------- package id_test 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}, {id.Slice{1}, id.Slice{1}, true}, {id.Slice{1}, id.Slice{2}, false}, {id.Slice{1, 2}, id.Slice{2, 1}, false}, {id.Slice{1, 2}, id.Slice{1, 2}, true}, } for i, tc := range testcases { got := tc.s1.Equal(tc.s2) if got != tc.exp { t.Errorf("%d/%v.Equal(%v)==%v, but got %v", i, tc.s1, tc.s2, tc.exp, got) } got = tc.s2.Equal(tc.s1) if got != tc.exp { t.Errorf("%d/%v.Equal(%v)==%v, but got %v", i, tc.s2, tc.s1, tc.exp, got) } } } func TestSliceString(t *testing.T) { t.Parallel() testcases := []struct { in id.Slice exp string }{ {nil, ""}, {id.Slice{}, ""}, {id.Slice{1}, "00000000000001"}, {id.Slice{1, 2}, "00000000000001 00000000000002"}, } for i, tc := range testcases { got := tc.in.String() if got != tc.exp { t.Errorf("%d/%v: expected %q, but got %q", i, tc.in, tc.exp, got) } } } |
Added domain/meta/collection.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 | //----------------------------------------------------------------------------- // Copyright (c) 2022-present Detlef Stern // // This file is part of Zettelstore. // // Zettelstore is licensed under the latest version of the EUPL (European Union // Public License). Please see file LICENSE.txt for your rights and obligations // under this license. //----------------------------------------------------------------------------- package meta import "sort" // Arrangement stores metadata within its categories. // Typecally a category might be a tag name, a role name, a syntax value. type Arrangement map[string][]*Meta // CreateArrangement by inspecting a given key and use the found // value as a category. func CreateArrangement(metaList []*Meta, key string) Arrangement { if len(metaList) == 0 { return nil } descr := Type(key) if descr == nil { return nil } if descr.IsSet { return createSetArrangement(metaList, key) } return createSimplearrangement(metaList, key) } func createSetArrangement(metaList []*Meta, key string) Arrangement { a := make(Arrangement) for _, m := range metaList { if vals, ok := m.GetList(key); ok { for _, val := range vals { a[val] = append(a[val], m) } } } return a } func createSimplearrangement(metaList []*Meta, key string) Arrangement { a := make(Arrangement) for _, m := range metaList { if val, ok := m.Get(key); ok && val != "" { a[val] = append(a[val], m) } } return a } // Counted returns the list of categories, together with the number of // metadata for each category. func (a Arrangement) Counted() CountedCategories { if len(a) == 0 { return nil } result := make(CountedCategories, 0, len(a)) for cat, metas := range a { result = append(result, CountedCategory{Name: cat, Count: len(metas)}) } return result } // CountedCategory contains of a name and the number how much this name occured // somewhere. type CountedCategory struct { Name string Count int } // CountedCategories is the list of CountedCategories. // Every name must occur only once. type CountedCategories []CountedCategory // SortByName sorts the list by the name attribute. // Since each name must occur only once, two CountedCategories cannot have // the same name. func (ccs CountedCategories) SortByName() { sort.Slice(ccs, func(i, j int) bool { return ccs[i].Name < ccs[j].Name }) } // SortByCount sorts the list by the count attribute, descending. // If two counts are equal, elements are sorted by name. func (ccs CountedCategories) SortByCount() { sort.Slice(ccs, func(i, j int) bool { iCount, jCount := ccs[i].Count, ccs[j].Count if iCount > jCount { return true } if iCount == jCount { return ccs[i].Name < ccs[j].Name } return false }) } // Categories returns just the category names. func (ccs CountedCategories) Categories() []string { result := make([]string, len(ccs)) for i, cc := range ccs { result[i] = cc.Name } return result } |
Added domain/meta/meta.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 | //----------------------------------------------------------------------------- // Copyright (c) 2020-present Detlef Stern // // This file is part of Zettelstore. // // Zettelstore is licensed under the latest version of the EUPL (European Union // Public License). Please see file LICENSE.txt for your rights and obligations // under this license. //----------------------------------------------------------------------------- // Package meta provides the domain specific type 'meta'. package meta import ( "regexp" "sort" "strings" "unicode" "unicode/utf8" "zettelstore.de/c/api" "zettelstore.de/c/maps" "zettelstore.de/z/domain/id" "zettelstore.de/z/input" "zettelstore.de/z/strfun" ) type keyUsage int const ( _ keyUsage = iota usageUser // Key will be manipulated by the user usageComputed // Key is computed by zettelstore usageProperty // Key is computed and not stored by zettelstore ) // DescriptionKey formally describes each supported metadata key. type DescriptionKey struct { Name string Type *DescriptionType usage keyUsage Inverse string } // IsComputed returns true, if metadata is computed and not set by the user. func (kd *DescriptionKey) IsComputed() bool { return kd.usage >= usageComputed } // IsProperty returns true, if metadata is a computed property. func (kd *DescriptionKey) IsProperty() bool { return kd.usage >= usageProperty } // IsStoredComputed retruns true, if metadata is computed, but also stored. func (kd *DescriptionKey) IsStoredComputed() bool { return kd.usage == usageComputed } var registeredKeys = make(map[string]*DescriptionKey) func registerKey(name string, t *DescriptionType, usage keyUsage, inverse string) { if _, ok := registeredKeys[name]; ok { panic("Key '" + name + "' already defined") } if inverse != "" { if t != TypeID && t != TypeIDSet { panic("Inversable key '" + name + "' is not identifier type, but " + t.String()) } inv, ok := registeredKeys[inverse] if !ok { panic("Inverse Key '" + inverse + "' not found") } if !inv.IsComputed() { panic("Inverse Key '" + inverse + "' is not computed.") } if inv.Type != TypeIDSet { panic("Inverse Key '" + inverse + "' is not an identifier set, but " + inv.Type.String()) } } registeredKeys[name] = &DescriptionKey{name, t, usage, inverse} } // IsComputed returns true, if key denotes a computed metadata key. func IsComputed(name string) bool { if kd, ok := registeredKeys[name]; ok { return kd.IsComputed() } return false } // IsProperty returns true, if key denotes a property metadata value. func IsProperty(name string) bool { if kd, ok := registeredKeys[name]; ok { return kd.IsProperty() } return false } // IsStoredComputed returns true, if key denotes a computed metadata key that is stored. func IsStoredComputed(name string) bool { if kd, ok := registeredKeys[name]; ok { return kd.IsStoredComputed() } return false } // Inverse returns the name of the inverse key. func Inverse(name string) string { if kd, ok := registeredKeys[name]; ok { return kd.Inverse } return "" } // GetDescription returns the key description object of the given key name. func GetDescription(name string) DescriptionKey { if d, ok := registeredKeys[name]; ok { return *d } return DescriptionKey{Type: Type(name)} } // GetSortedKeyDescriptions delivers all metadata key descriptions as a slice, sorted by name. func GetSortedKeyDescriptions() []*DescriptionKey { keys := maps.Keys(registeredKeys) result := make([]*DescriptionKey, 0, len(keys)) for _, n := range keys { result = append(result, registeredKeys[n]) } return result } // Supported keys. func init() { registerKey(api.KeyID, TypeID, usageComputed, "") registerKey(api.KeyTitle, TypeEmpty, usageUser, "") registerKey(api.KeyRole, TypeWord, usageUser, "") registerKey(api.KeyTags, TypeTagSet, usageUser, "") registerKey(api.KeySyntax, TypeWord, usageUser, "") // Properties that are inverse keys registerKey(api.KeyFolge, TypeIDSet, usageProperty, "") registerKey(api.KeySuccessors, TypeIDSet, usageProperty, "") registerKey(api.KeyAuthor, TypeString, usageUser, "") registerKey(api.KeyBack, TypeIDSet, usageProperty, "") registerKey(api.KeyBackward, TypeIDSet, usageProperty, "") registerKey(api.KeyBoxNumber, TypeNumber, usageProperty, "") registerKey(api.KeyCopyright, TypeString, usageUser, "") registerKey(api.KeyCreated, TypeTimestamp, usageComputed, "") registerKey(api.KeyCredential, TypeCredential, usageUser, "") registerKey(api.KeyDead, TypeIDSet, usageProperty, "") registerKey(api.KeyForward, TypeIDSet, usageProperty, "") registerKey(api.KeyLang, TypeWord, usageUser, "") registerKey(api.KeyLicense, TypeEmpty, usageUser, "") registerKey(api.KeyModified, TypeTimestamp, usageComputed, "") registerKey(api.KeyPrecursor, TypeIDSet, usageUser, api.KeyFolge) registerKey(api.KeyPredecessor, TypeID, usageUser, api.KeySuccessors) registerKey(api.KeyPublished, TypeTimestamp, usageProperty, "") registerKey(api.KeyQuery, TypeEmpty, usageUser, "") registerKey(api.KeyReadOnly, TypeWord, usageUser, "") registerKey(api.KeySummary, TypeZettelmarkup, usageUser, "") registerKey(api.KeyURL, TypeURL, usageUser, "") registerKey(api.KeyUselessFiles, TypeString, usageProperty, "") registerKey(api.KeyUserID, TypeWord, usageUser, "") registerKey(api.KeyUserRole, TypeWord, usageUser, "") registerKey(api.KeyVisibility, TypeWord, usageUser, "") } // NewPrefix is the prefix for metadata key in template zettel for creating new zettel. const NewPrefix = "new-" // Meta contains all meta-data of a zettel. type Meta struct { Zid id.Zid pairs map[string]string YamlSep bool } // New creates a new chunk for storing metadata. func New(zid id.Zid) *Meta { return &Meta{Zid: zid, pairs: make(map[string]string, 5)} } // NewWithData creates metadata object with given data. func NewWithData(zid id.Zid, data map[string]string) *Meta { pairs := make(map[string]string, len(data)) for k, v := range data { pairs[k] = v } return &Meta{Zid: zid, pairs: pairs} } // Length returns the number of bytes stored for the metadata. func (m *Meta) Length() int { if m == nil { return 0 } result := 6 // storage needed for Zid for k, v := range m.pairs { result += len(k) + len(v) + 1 // 1 because separator } return result } // Clone returns a new copy of the metadata. func (m *Meta) Clone() *Meta { return &Meta{ Zid: m.Zid, pairs: m.Map(), YamlSep: m.YamlSep, } } // Map returns a copy of the meta data as a string map. func (m *Meta) Map() map[string]string { pairs := make(map[string]string, len(m.pairs)) for k, v := range m.pairs { pairs[k] = v } return pairs } var reKey = regexp.MustCompile("^[0-9a-z][-0-9a-z]{0,254}$") // KeyIsValid returns true, the the key is a valid string. func KeyIsValid(key string) bool { return reKey.MatchString(key) } // Pair is one key-value-pair of a Zettel meta. type Pair struct { Key string Value string } var firstKeys = []string{api.KeyTitle, api.KeyRole, api.KeyTags, api.KeySyntax} var firstKeySet strfun.Set func init() { firstKeySet = strfun.NewSet(firstKeys...) } // Set stores the given string value under the given key. func (m *Meta) Set(key, value string) { if key != api.KeyID { m.pairs[key] = trimValue(value) } } // SetNonEmpty stores the given value under the given key, if the value is non-empty. // An empty value will delete the previous association. func (m *Meta) SetNonEmpty(key, value string) { if value == "" { delete(m.pairs, key) } else if key != api.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 m == nil { return "", false } if key == api.KeyID { return m.Zid.String(), true } value, ok := m.pairs[key] return value, ok } // GetDefault retrieves the string value of the given key. If no value was // stored, the given default value is returned. func (m *Meta) GetDefault(key, def string) string { if value, found := m.Get(key); found { return value } return def } // GetTitle returns the title of the metadata. It is the only key that has a // defined default value: the string representation of the zettel identifier. func (m *Meta) GetTitle() string { if title, found := m.Get(api.KeyTitle); found { return title } return m.Zid.String() } // Pairs returns not computed key/values pairs stored, in a specific order. // First come the pairs with predefined keys: MetaTitleKey, MetaTagsKey, MetaSyntaxKey, // MetaContextKey. Then all other pairs are append to the list, ordered by key. func (m *Meta) Pairs() []Pair { return m.doPairs(m.getFirstKeys(), notComputedKey) } // ComputedPairs returns all key/values pairs stored, in a specific order. First come // the pairs with predefined keys: MetaTitleKey, MetaTagsKey, MetaSyntaxKey, // MetaContextKey. Then all other pairs are append to the list, ordered by key. func (m *Meta) ComputedPairs() []Pair { return m.doPairs(m.getFirstKeys(), anyKey) } // PairsRest returns not computed key/values pairs stored, except the values with // predefined keys. The pairs are ordered by key. func (m *Meta) PairsRest() []Pair { result := make([]Pair, 0, len(m.pairs)) return m.doPairs(result, notComputedKey) } // ComputedPairsRest returns all key/values pairs stored, except the values with // predefined keys. The pairs are ordered by key. func (m *Meta) ComputedPairsRest() []Pair { result := make([]Pair, 0, len(m.pairs)) return m.doPairs(result, anyKey) } func notComputedKey(key string) bool { return !IsComputed(key) } func anyKey(string) bool { return true } func (m *Meta) doPairs(firstKeys []Pair, addKeyPred func(string) bool) []Pair { keys := m.getKeysRest(addKeyPred) for _, k := range keys { firstKeys = append(firstKeys, Pair{k, m.pairs[k]}) } return firstKeys } func (m *Meta) getFirstKeys() []Pair { result := make([]Pair, 0, len(m.pairs)) for _, key := range firstKeys { if value, ok := m.pairs[key]; ok { result = append(result, Pair{key, value}) } } return result } func (m *Meta) getKeysRest(addKeyPred func(string) bool) []string { keys := make([]string, 0, len(m.pairs)) for k := range m.pairs { if !firstKeySet.Has(k) && addKeyPred(k) { keys = append(keys, k) } } sort.Strings(keys) return keys } // Delete removes a key from the data. func (m *Meta) Delete(key string) { if key != api.KeyID { delete(m.pairs, key) } } // Equal compares to metas for equality. func (m *Meta) Equal(o *Meta, allowComputed bool) bool { if m == nil && o == nil { return true } if m == nil || o == nil || m.Zid != o.Zid { return false } tested := make(strfun.Set, len(m.pairs)) for k, v := range m.pairs { tested.Set(k) if !equalValue(k, v, o, allowComputed) { return false } } for k, v := range o.pairs { if !tested.Has(k) && !equalValue(k, v, m, allowComputed) { return false } } return true } func equalValue(key, val string, other *Meta, allowComputed bool) bool { if allowComputed || !IsComputed(key) { if valO, found := other.pairs[key]; !found || val != valO { return false } } return true } // Sanitize all metadata keys and values, so that they can be written safely into a file. func (m *Meta) Sanitize() { if m == nil { return } for k, v := range m.pairs { m.pairs[RemoveNonGraphic(k)] = RemoveNonGraphic(v) } } // RemoveNonGraphic changes the given string not to include non-graphical characters. // It is needed to sanitize meta data. func RemoveNonGraphic(s string) string { if s == "" { return "" } pos := 0 var sb strings.Builder for pos < len(s) { nextPos := strings.IndexFunc(s[pos:], func(r rune) bool { return !unicode.IsGraphic(r) }) if nextPos < 0 { break } sb.WriteString(s[pos:nextPos]) sb.WriteByte(' ') _, size := utf8.DecodeRuneInString(s[nextPos:]) pos = nextPos + size } if pos == 0 { return strings.TrimSpace(s) } sb.WriteString(s[pos:]) return strings.TrimSpace(sb.String()) } |
Added domain/meta/meta_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 | //----------------------------------------------------------------------------- // Copyright (c) 2020-present Detlef Stern // // This file is part of Zettelstore. // // Zettelstore is licensed under the latest version of the EUPL (European Union // Public License). Please see file LICENSE.txt for your rights and obligations // under this license. //----------------------------------------------------------------------------- // Package meta_test provides tests for the domain specific type 'meta'. package meta import ( "strings" "testing" "zettelstore.de/c/api" "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(api.KeyTitle); ok && got != "" { t.Errorf("Title is not empty, but %q", got) } addToMeta(m, api.KeyTitle, " ") if got, ok := m.Get(api.KeyTitle); ok && got != "" { t.Errorf("Title is not empty, but %q", got) } const st = "A simple text" addToMeta(m, api.KeyTitle, " "+st+" ") if got, ok := m.Get(api.KeyTitle); !ok || got != st { t.Errorf("Title is not %q, but %q", st, got) } addToMeta(m, api.KeyTitle, " "+st+"\t") const exp = st + " " + st if got, ok := m.Get(api.KeyTitle); !ok || got != exp { t.Errorf("Title is not %q, but %q", exp, got) } m = New(testID) const at = "A Title" addToMeta(m, api.KeyTitle, at) addToMeta(m, api.KeyTitle, " ") if got, ok := m.Get(api.KeyTitle); !ok || got != at { t.Errorf("Title is not %q, but %q", at, got) } } func checkSet(t *testing.T, exp []string, m *Meta, key string) { t.Helper() got, _ := m.GetList(key) for i, tag := range exp { if i < len(got) { if tag != got[i] { t.Errorf("Pos=%d, expected %q, got %q", i, exp[i], got[i]) } } else { t.Errorf("Expected %q, but is missing", exp[i]) } } if len(exp) < len(got) { t.Errorf("Extra tags: %q", got[len(exp):]) } } func TestTagsHeader(t *testing.T) { t.Parallel() m := New(testID) checkSet(t, []string{}, m, api.KeyTags) addToMeta(m, api.KeyTags, "") checkSet(t, []string{}, m, api.KeyTags) addToMeta(m, api.KeyTags, " #t1 #t2 #t3 #t4 ") checkSet(t, []string{"#t1", "#t2", "#t3", "#t4"}, m, api.KeyTags) addToMeta(m, api.KeyTags, "#t5") checkSet(t, []string{"#t1", "#t2", "#t3", "#t4", "#t5"}, m, api.KeyTags) addToMeta(m, api.KeyTags, "t6") checkSet(t, []string{"#t1", "#t2", "#t3", "#t4", "#t5"}, m, api.KeyTags) } func TestSyntax(t *testing.T) { t.Parallel() m := New(testID) if got, ok := m.Get(api.KeySyntax); ok || got != "" { t.Errorf("Syntax is not %q, but %q", "", got) } addToMeta(m, api.KeySyntax, " ") if got, _ := m.Get(api.KeySyntax); got != "" { t.Errorf("Syntax is not %q, but %q", "", got) } addToMeta(m, api.KeySyntax, "MarkDown") const exp = "markdown" if got, ok := m.Get(api.KeySyntax); !ok || got != exp { t.Errorf("Syntax is not %q, but %q", exp, got) } addToMeta(m, api.KeySyntax, " ") if got, _ := m.Get(api.KeySyntax); got != "" { t.Errorf("Syntax is not %q, but %q", "", got) } } func checkHeader(t *testing.T, exp map[string]string, gotP []Pair) { t.Helper() got := make(map[string]string, len(gotP)) for _, p := range gotP { got[p.Key] = p.Value if _, ok := exp[p.Key]; !ok { t.Errorf("Key %q is not expected, but has value %q", p.Key, p.Value) } } for k, v := range exp { if gv, ok := got[k]; !ok || v != gv { if ok { t.Errorf("Key %q is not %q, but %q", k, v, got[k]) } else { t.Errorf("Key %q missing, should have value %q", k, v) } } } } func TestDefaultHeader(t *testing.T) { t.Parallel() m := New(testID) addToMeta(m, "h1", "d1") addToMeta(m, "H2", "D2") addToMeta(m, "H1", "D1.1") exp := map[string]string{"h1": "d1 D1.1", "h2": "D2"} checkHeader(t, exp, m.Pairs()) addToMeta(m, "", "d0") checkHeader(t, exp, m.Pairs()) addToMeta(m, "h3", "") exp["h3"] = "" checkHeader(t, exp, m.Pairs()) addToMeta(m, "h3", " ") checkHeader(t, exp, m.Pairs()) addToMeta(m, "h4", " ") exp["h4"] = "" checkHeader(t, exp, m.Pairs()) } func TestDelete(t *testing.T) { t.Parallel() m := New(testID) m.Set("key", "val") if got, ok := m.Get("key"); !ok || got != "val" { t.Errorf("Value != %q, got: %v/%q", "val", ok, got) } m.Set("key", "") if got, ok := m.Get("key"); !ok || got != "" { t.Errorf("Value != %q, got: %v/%q", "", ok, got) } m.Delete("key") if got, ok := m.Get("key"); ok || got != "" { t.Errorf("Value != %q, got: %v/%q", "", ok, got) } } func TestEqual(t *testing.T) { t.Parallel() testcases := []struct { pairs1, pairs2 []string allowComputed bool exp bool }{ {nil, nil, true, true}, {nil, nil, false, true}, {[]string{"a", "a"}, nil, false, false}, {[]string{"a", "a"}, nil, true, false}, {[]string{api.KeyFolge, "0"}, nil, true, false}, {[]string{api.KeyFolge, "0"}, nil, false, true}, {[]string{api.KeyFolge, "0"}, []string{api.KeyFolge, "0"}, true, true}, {[]string{api.KeyFolge, "0"}, []string{api.KeyFolge, "0"}, false, true}, } for i, tc := range testcases { m1 := pairs2meta(tc.pairs1) m2 := pairs2meta(tc.pairs2) got := m1.Equal(m2, tc.allowComputed) if tc.exp != got { t.Errorf("%d: %v =?= %v: expected=%v, but got=%v", i, tc.pairs1, tc.pairs2, tc.exp, got) } got = m2.Equal(m1, tc.allowComputed) if tc.exp != got { t.Errorf("%d: %v =!= %v: expected=%v, but got=%v", i, tc.pairs1, tc.pairs2, tc.exp, got) } } // Pathologic cases var m1, m2 *Meta if !m1.Equal(m2, true) { t.Error("Nil metas should be treated equal") } m1 = New(testID) if m1.Equal(m2, true) { t.Error("Empty meta should not be equal to nil") } if m2.Equal(m1, true) { t.Error("Nil meta should should not be equal to empty") } m2 = New(testID + 1) if m1.Equal(m2, true) { t.Error("Different ID should differentiate") } if m2.Equal(m1, true) { t.Error("Different ID should differentiate") } } func pairs2meta(pairs []string) *Meta { m := New(testID) for i := 0; i < len(pairs); i = i + 2 { m.Set(pairs[i], pairs[i+1]) } return m } func TestRemoveNonGraphic(t *testing.T) { testCases := []struct { inp string exp string }{ {"", ""}, {" ", ""}, {"a", "a"}, {"a ", "a"}, {"a b", "a b"}, {"\n", ""}, {"a\n", "a"}, {"a\nb", "a b"}, {"a\tb", "a b"}, } for i, tc := range testCases { got := RemoveNonGraphic(tc.inp) if tc.exp != got { t.Errorf("%q/%d: expected %q, but got %q", tc.inp, i, tc.exp, got) } } } |
Added domain/meta/parse.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 | //----------------------------------------------------------------------------- // Copyright (c) 2020-present Detlef Stern // // This file is part of Zettelstore. // // Zettelstore is licensed under the latest version of the EUPL (European Union // Public License). Please see file LICENSE.txt for your rights and obligations // under this license. //----------------------------------------------------------------------------- package meta import ( "strings" "zettelstore.de/c/api" "zettelstore.de/c/maps" "zettelstore.de/z/domain/id" "zettelstore.de/z/input" "zettelstore.de/z/strfun" ) // NewFromInput parses the meta data of a zettel. func NewFromInput(zid id.Zid, inp *input.Input) *Meta { if inp.Ch == '-' && inp.PeekN(0) == '-' && inp.PeekN(1) == '-' { skipToEOL(inp) inp.EatEOL() } meta := New(zid) for { skipSpace(inp) switch inp.Ch { case '\r': if inp.Peek() == '\n' { inp.Next() } fallthrough case '\n': inp.Next() return meta case input.EOS: return meta case '%': skipToEOL(inp) inp.EatEOL() continue } parseHeader(meta, inp) if inp.Ch == '-' && inp.PeekN(0) == '-' && inp.PeekN(1) == '-' { skipToEOL(inp) inp.EatEOL() meta.YamlSep = true return meta } } } func parseHeader(m *Meta, inp *input.Input) { pos := inp.Pos for isHeader(inp.Ch) { inp.Next() } key := inp.Src[pos:inp.Pos] skipSpace(inp) if inp.Ch == ':' { inp.Next() } var val []byte for { skipSpace(inp) pos = inp.Pos skipToEOL(inp) val = append(val, inp.Src[pos:inp.Pos]...) inp.EatEOL() if !input.IsSpace(inp.Ch) { break } val = append(val, ' ') } addToMeta(m, string(key), string(val)) } func skipSpace(inp *input.Input) { for input.IsSpace(inp.Ch) { inp.Next() } } func skipToEOL(inp *input.Input) { for { switch inp.Ch { case '\n', '\r', input.EOS: return } inp.Next() } } // Return true iff rune is valid for header key. func isHeader(ch rune) bool { return ('a' <= ch && ch <= 'z') || ('0' <= ch && ch <= '9') || ch == '-' || ('A' <= ch && ch <= 'Z') } type predValidElem func(string) bool func addToSet(set strfun.Set, elems []string, useElem predValidElem) { for _, s := range elems { if len(s) > 0 && useElem(s) { set.Set(s) } } } func addSet(m *Meta, key, val string, useElem predValidElem) { newElems := strings.Fields(val) oldElems, ok := m.GetList(key) if !ok { oldElems = nil } set := make(strfun.Set, len(newElems)+len(oldElems)) addToSet(set, newElems, useElem) if len(set) == 0 { // Nothing to add. Maybe because of rejected elements. return } addToSet(set, oldElems, useElem) m.SetList(key, maps.Keys(set)) } func addData(m *Meta, k, v string) { if o, ok := m.Get(k); !ok || o == "" { m.Set(k, v) } else if v != "" { m.Set(k, o+" "+v) } } func addToMeta(m *Meta, key, val string) { v := trimValue(val) key = strings.ToLower(key) if !KeyIsValid(key) { return } switch key { case "", api.KeyID: // Empty key and 'id' key will be ignored return } switch Type(key) { case TypeTagSet: addSet(m, key, strings.ToLower(v), func(s string) bool { return s[0] == '#' && len(s) > 1 }) case TypeWord: m.Set(key, strings.ToLower(v)) case TypeWordSet: addSet(m, key, strings.ToLower(v), func(s string) bool { return true }) case TypeID: if _, err := id.Parse(v); err == nil { m.Set(key, v) } case TypeIDSet: addSet(m, key, v, func(s string) bool { _, err := id.Parse(s) return err == nil }) case TypeTimestamp: if _, ok := TimeValue(v); ok { m.Set(key, v) } default: addData(m, key, v) } } |
Added domain/meta/parse_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 | //----------------------------------------------------------------------------- // Copyright (c) 2020-present Detlef Stern // // This file is part of Zettelstore. // // Zettelstore is licensed under the latest version of the EUPL (European Union // Public License). Please see file LICENSE.txt for your rights and obligations // under this license. //----------------------------------------------------------------------------- package meta_test import ( "strings" "testing" "zettelstore.de/c/api" "zettelstore.de/z/domain/meta" "zettelstore.de/z/input" ) func parseMetaStr(src string) *meta.Meta { return meta.NewFromInput(testID, input.NewInput([]byte(src))) } func TestEmpty(t *testing.T) { t.Parallel() m := parseMetaStr("") if got, ok := m.Get(api.KeySyntax); ok || got != "" { t.Errorf("Syntax is not %q, but %q", "", got) } if got, ok := m.GetList(api.KeyTags); ok || len(got) > 0 { t.Errorf("Tags are not nil, but %v", got) } } func TestTitle(t *testing.T) { t.Parallel() td := []struct{ s, e string }{ {api.KeyTitle + ": a title", "a title"}, {api.KeyTitle + ": a\n\t title", "a title"}, {api.KeyTitle + ": a\n\t title\r\n x", "a title x"}, {api.KeyTitle + " AbC", "AbC"}, {api.KeyTitle + " AbC\n ded", "AbC ded"}, {api.KeyTitle + ": o\ntitle: p", "o p"}, {api.KeyTitle + ": O\n\ntitle: P", "O"}, {api.KeyTitle + ": b\r\ntitle: c", "b c"}, {api.KeyTitle + ": B\r\n\r\ntitle: C", "B"}, {api.KeyTitle + ": r\rtitle: q", "r q"}, {api.KeyTitle + ": R\r\rtitle: Q", "R"}, } for i, tc := range td { m := parseMetaStr(tc.s) if got, ok := m.Get(api.KeyTitle); !ok || got != tc.e { t.Log(m) t.Errorf("TC=%d: expected %q, got %q", i, tc.e, got) } } } func TestTags(t *testing.T) { t.Parallel() testcases := []struct { src string exp string }{ {"", ""}, {api.KeyTags + ":", ""}, {api.KeyTags + ": c", ""}, {api.KeyTags + ": #", ""}, {api.KeyTags + ": #c", "c"}, {api.KeyTags + ": #c #", "c"}, {api.KeyTags + ": #c #b", "b c"}, {api.KeyTags + ": #c # #", "c"}, {api.KeyTags + ": #c # #b", "b c"}, } for i, tc := range testcases { m := parseMetaStr(tc.src) tags, found := m.GetTags(api.KeyTags) if !found { if tc.exp != "" { t.Errorf("%d / %q: no %s found", i, tc.src, api.KeyTags) } continue } if tc.exp == "" && len(tags) > 0 { t.Errorf("%d / %q: expected no %s, but got %v", i, tc.src, api.KeyTags, tags) continue } got := strings.Join(tags, " ") if tc.exp != got { t.Errorf("%d / %q: expected %q, got: %q", i, tc.src, tc.exp, got) } } } func TestNewFromInput(t *testing.T) { t.Parallel() testcases := []struct { input string exp []meta.Pair }{ {"", []meta.Pair{}}, {" a:b", []meta.Pair{{"a", "b"}}}, {"%a:b", []meta.Pair{}}, {"a:b\r\n\r\nc:d", []meta.Pair{{"a", "b"}}}, {"a:b\r\n%c:d", []meta.Pair{{"a", "b"}}}, {"% a:b\r\n c:d", []meta.Pair{{"c", "d"}}}, {"---\r\na:b\r\n", []meta.Pair{{"a", "b"}}}, {"---\r\na:b\r\n--\r\nc:d", []meta.Pair{{"a", "b"}, {"c", "d"}}}, {"---\r\na:b\r\n---\r\nc:d", []meta.Pair{{"a", "b"}}}, {"---\r\na:b\r\n----\r\nc:d", []meta.Pair{{"a", "b"}}}, {"new-title:\nnew-url:", []meta.Pair{{"new-title", ""}, {"new-url", ""}}}, } for i, tc := range testcases { meta := parseMetaStr(tc.input) if got := meta.Pairs(); !equalPairs(tc.exp, got) { t.Errorf("TC=%d: expected=%v, got=%v", i, tc.exp, got) } } // Test, whether input position is correct. inp := input.NewInput([]byte("---\na:b\n---\nX")) m := meta.NewFromInput(testID, inp) exp := []meta.Pair{{"a", "b"}} if got := m.Pairs(); !equalPairs(exp, got) { t.Errorf("Expected=%v, got=%v", exp, got) } expCh := 'X' if gotCh := inp.Ch; gotCh != expCh { t.Errorf("Expected=%v, got=%v", expCh, gotCh) } } func equalPairs(one, two []meta.Pair) bool { if len(one) != len(two) { return false } for i := 0; i < len(one); i++ { if one[i].Key != two[i].Key || one[i].Value != two[i].Value { return false } } return true } func TestPrecursorIDSet(t *testing.T) { t.Parallel() var testdata = []struct { inp string exp string }{ {"", ""}, {"123", ""}, {"12345678901234", "12345678901234"}, {"123 12345678901234", "12345678901234"}, {"12345678901234 123", "12345678901234"}, {"01234567890123 123 12345678901234", "01234567890123 12345678901234"}, {"12345678901234 01234567890123", "01234567890123 12345678901234"}, } for i, tc := range testdata { m := parseMetaStr(api.KeyPrecursor + ": " + tc.inp) if got, ok := m.Get(api.KeyPrecursor); (!ok && tc.exp != "") || tc.exp != got { t.Errorf("TC=%d: expected %q, but got %q when parsing %q", i, tc.exp, got, tc.inp) } } } |
Added domain/meta/type.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 | //----------------------------------------------------------------------------- // Copyright (c) 2020-present Detlef Stern // // This file is part of Zettelstore. // // Zettelstore is licensed under the latest version of the EUPL (European Union // Public License). Please see file LICENSE.txt for your rights and obligations // under this license. //----------------------------------------------------------------------------- package meta import ( "strconv" "strings" "sync" "time" "zettelstore.de/c/api" "zettelstore.de/z/domain/id" ) // DescriptionType is a description of a specific key type. type DescriptionType struct { Name string IsSet bool } // String returns the string representation of the given type func (t DescriptionType) String() string { return t.Name } var registeredTypes = make(map[string]*DescriptionType) func registerType(name string, isSet bool) *DescriptionType { if _, ok := registeredTypes[name]; ok { panic("Type '" + name + "' already registered") } t := &DescriptionType{name, isSet} registeredTypes[name] = t return t } // Supported key types. var ( TypeCredential = registerType(api.MetaCredential, false) TypeEmpty = registerType(api.MetaEmpty, false) TypeID = registerType(api.MetaID, false) TypeIDSet = registerType(api.MetaIDSet, true) TypeNumber = registerType(api.MetaNumber, false) TypeString = registerType(api.MetaString, false) TypeTagSet = registerType(api.MetaTagSet, true) TypeTimestamp = registerType(api.MetaTimestamp, false) TypeURL = registerType(api.MetaURL, false) TypeWord = registerType(api.MetaWord, false) TypeWordSet = registerType(api.MetaWordSet, true) TypeZettelmarkup = registerType(api.MetaZettelmarkup, false) ) // Type returns a type hint for the given key. If no type hint is specified, // TypeUnknown is returned. func (*Meta) Type(key string) *DescriptionType { return Type(key) } var ( cachedTypedKeys = make(map[string]*DescriptionType) mxTypedKey sync.RWMutex suffixTypes = map[string]*DescriptionType{ "-number": TypeNumber, "-role": TypeWord, "-set": TypeWordSet, "-title": TypeZettelmarkup, "-url": TypeURL, "-zettel": TypeID, "-zid": TypeID, "-zids": TypeIDSet, } ) // Type returns a type hint for the given key. If no type hint is specified, // TypeEmpty is returned. func Type(key string) *DescriptionType { if k, ok := registeredKeys[key]; ok { return k.Type } mxTypedKey.RLock() k, ok := cachedTypedKeys[key] mxTypedKey.RUnlock() if ok { return k } for suffix, t := range suffixTypes { if strings.HasSuffix(key, suffix) { mxTypedKey.Lock() defer mxTypedKey.Unlock() cachedTypedKeys[key] = t return t } } return TypeEmpty } // SetList stores the given string list value under the given key. func (m *Meta) SetList(key string, values []string) { if key != api.KeyID { for i, val := range values { values[i] = trimValue(val) } m.pairs[key] = strings.Join(values, " ") } } // SetNow stores the current timestamp under the given key. func (m *Meta) SetNow(key string) { m.Set(key, time.Now().Local().Format(id.ZidLayout)) } // BoolValue returns the value interpreted as a bool. func BoolValue(value string) bool { if len(value) > 0 { switch value[0] { case '0', 'f', 'F', 'n', 'N': return false } } return true } // GetBool returns the boolean value of the given key. func (m *Meta) GetBool(key string) bool { if value, ok := m.Get(key); ok { return BoolValue(value) } return false } // TimeValue returns the time value of the given value. func TimeValue(value string) (time.Time, bool) { if t, err := time.Parse(id.ZidLayout, value); err == nil { return t, true } return time.Time{}, false } // GetTime returns the time value of the given key. func (m *Meta) GetTime(key string) (time.Time, bool) { if value, ok := m.Get(key); ok { return TimeValue(value) } return time.Time{}, false } // ListFromValue transforms a string value into a list value. func ListFromValue(value string) []string { return strings.Fields(value) } // GetList retrieves the string list value of a given key. The bool value // signals, whether there was a value stored or not. func (m *Meta) GetList(key string) ([]string, bool) { value, ok := m.Get(key) if !ok { return nil, false } return ListFromValue(value), true } // GetTags returns the list of tags as a string list. Each tag does not begin // with the '#' character, in contrast to `GetList`. func (m *Meta) GetTags(key string) ([]string, bool) { tagsValue, ok := m.Get(key) if !ok { return nil, false } tags := ListFromValue(strings.ToLower(tagsValue)) for i, tag := range tags { tags[i] = CleanTag(tag) } return tags, len(tags) > 0 } // CleanTag removes the number character ('#') from a tag value and lowercases it. func CleanTag(tag string) string { if len(tag) > 1 && tag[0] == '#' { return tag[1:] } return tag } // GetListOrNil retrieves the string list value of a given key. If there was // nothing stores, a nil list is returned. func (m *Meta) GetListOrNil(key string) []string { if value, ok := m.GetList(key); ok { return value } return nil } // GetNumber retrieves the numeric value of a given key. func (m *Meta) GetNumber(key string, def int64) int64 { if value, ok := m.Get(key); ok { if num, err := strconv.ParseInt(value, 10, 64); err == nil { return num } } return def } |
Added domain/meta/type_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 | //----------------------------------------------------------------------------- // Copyright (c) 2020-present Detlef Stern // // This file is part of Zettelstore. // // Zettelstore is licensed under the latest version of the EUPL (European Union // Public License). Please see file LICENSE.txt for your rights and obligations // under this license. //----------------------------------------------------------------------------- package meta_test import ( "strconv" "testing" "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{}}, {"00000000000000", false, time.Time{}}, {"98765432109876", false, time.Time{}}, {"20201221111905", true, time.Date(2020, time.December, 21, 11, 19, 5, 0, time.UTC)}, } for i, tc := range testCases { got, ok := meta.TimeValue(tc.value) if ok != tc.valid { t.Errorf("%d: parsing of %q should be %v, but got %v", i, tc.value, tc.valid, ok) continue } if got != tc.exp { t.Errorf("%d: parsing of %q should return %v, but got %v", i, tc.value, tc.exp, got) } } } |
Added domain/meta/values.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 | //----------------------------------------------------------------------------- // Copyright (c) 2020-present Detlef Stern // // This file is part of Zettelstore. // // Zettelstore is licensed under the latest version of the EUPL (European Union // Public License). Please see file LICENSE.txt for your rights and obligations // under this license. //----------------------------------------------------------------------------- package meta import ( "fmt" "zettelstore.de/c/api" ) // Supported syntax values. const ( SyntaxCSS = api.ValueSyntaxCSS SyntaxDraw = api.ValueSyntaxDraw SyntaxGif = api.ValueSyntaxGif SyntaxHTML = api.ValueSyntaxHTML SyntaxJPEG = "jpeg" SyntaxJPG = "jpg" SyntaxMarkdown = api.ValueSyntaxMarkdown SyntaxMD = api.ValueSyntaxMD SyntaxMustache = api.ValueSyntaxMustache SyntaxNone = api.ValueSyntaxNone SyntaxPlain = "plain" SyntaxPNG = "png" SyntaxSVG = api.ValueSyntaxSVG SyntaxText = api.ValueSyntaxText SyntaxTxt = "txt" SyntaxWebp = "webp" SyntaxZmk = api.ValueSyntaxZmk ) // Visibility enumerates the variations of the 'visibility' meta key. type Visibility int // Supported values for visibility. const ( _ Visibility = iota VisibilityUnknown VisibilityPublic VisibilityCreator VisibilityLogin VisibilityOwner VisibilityExpert ) var visMap = map[string]Visibility{ api.ValueVisibilityPublic: VisibilityPublic, api.ValueVisibilityCreator: VisibilityCreator, api.ValueVisibilityLogin: VisibilityLogin, api.ValueVisibilityOwner: VisibilityOwner, api.ValueVisibilityExpert: VisibilityExpert, } var revVisMap = map[Visibility]string{} func init() { for k, v := range visMap { revVisMap[v] = k } } // GetVisibility returns the visibility value of the given string func GetVisibility(val string) Visibility { if vis, ok := visMap[val]; ok { return vis } return VisibilityUnknown } func (v Visibility) String() string { if s, ok := revVisMap[v]; ok { return s } return fmt.Sprintf("Unknown (%d)", v) } // UserRole enumerates the supported values of meta key 'user-role'. type UserRole int // Supported values for user roles. const ( _ UserRole = iota UserRoleUnknown UserRoleCreator UserRoleReader UserRoleWriter UserRoleOwner ) var urMap = map[string]UserRole{ api.ValueUserRoleCreator: UserRoleCreator, api.ValueUserRoleReader: UserRoleReader, api.ValueUserRoleWriter: UserRoleWriter, api.ValueUserRoleOwner: UserRoleOwner, } // GetUserRole role returns the user role of the given string. func GetUserRole(val string) UserRole { if ur, ok := urMap[val]; ok { return ur } return UserRoleUnknown } |
Added domain/meta/write.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 | //----------------------------------------------------------------------------- // Copyright (c) 2020-present Detlef Stern // // This file is part of Zettelstore. // // Zettelstore is licensed under the latest version of the EUPL (European Union // Public License). Please see file LICENSE.txt for your rights and obligations // under this license. //----------------------------------------------------------------------------- package meta import "io" // Write writes metadata to a writer, excluding computed and propery values. func (m *Meta) Write(w io.Writer) (int, error) { return m.doWrite(w, IsComputed) } // WriteComputed writes metadata to a writer, including computed values, // but excluding property values. func (m *Meta) WriteComputed(w io.Writer) (int, error) { return m.doWrite(w, IsProperty) } func (m *Meta) doWrite(w io.Writer, ignoreKeyPred func(string) bool) (length int, err error) { for _, p := range m.ComputedPairs() { key := p.Key if ignoreKeyPred(key) { continue } if err != nil { break } var l int l, err = io.WriteString(w, key) length += l if err == nil { l, err = w.Write(colonSpace) length += l } if err == nil { l, err = io.WriteString(w, p.Value) length += l } if err == nil { l, err = w.Write(newline) length += l } } return length, err } var ( colonSpace = []byte{':', ' '} newline = []byte{'\n'} ) |
Added domain/meta/write_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 | //----------------------------------------------------------------------------- // Copyright (c) 2020-present Detlef Stern // // This file is part of Zettelstore. // // Zettelstore is licensed under the latest version of the EUPL (European Union // Public License). Please see file LICENSE.txt for your rights and obligations // under this license. //----------------------------------------------------------------------------- package meta_test import ( "strings" "testing" "zettelstore.de/c/api" "zettelstore.de/z/domain/id" "zettelstore.de/z/domain/meta" ) const testID = id.Zid(98765432101234) func newMeta(title string, tags []string, syntax string) *meta.Meta { m := meta.New(testID) if title != "" { m.Set(api.KeyTitle, title) } if tags != nil { m.Set(api.KeyTags, strings.Join(tags, " ")) } if syntax != "" { m.Set(api.KeySyntax, syntax) } return m } func assertWriteMeta(t *testing.T, m *meta.Meta, expected string) { t.Helper() var sb strings.Builder m.Write(&sb) if got := sb.String(); got != expected { t.Errorf("\nExp: %q\ngot: %q", expected, got) } } func TestWriteMeta(t *testing.T) { t.Parallel() assertWriteMeta(t, newMeta("", nil, ""), "") m := newMeta("TITLE", []string{"#t1", "#t2"}, "syntax") assertWriteMeta(t, m, "title: TITLE\ntags: #t1 #t2\nsyntax: syntax\n") m = newMeta("TITLE", nil, "") m.Set("user", "zettel") m.Set("auth", "basic") assertWriteMeta(t, m, "title: TITLE\nauth: basic\nuser: zettel\n") } |
Added domain/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 | //----------------------------------------------------------------------------- // Copyright (c) 2020-present Detlef Stern // // This file is part of Zettelstore. // // Zettelstore is licensed under the latest version of the EUPL (European Union // Public License). Please see file LICENSE.txt for your rights and obligations // under this license. //----------------------------------------------------------------------------- // Package domain provides domain specific types, constants, and functions. package domain import "zettelstore.de/z/domain/meta" // Zettel is the main data object of a zettelstore. type Zettel struct { Meta *meta.Meta // Some additional meta-data. Content Content // The content of the zettel itself. } // Length returns the number of bytes to store the zettel (in a domain view, // not in a technical view). func (z Zettel) Length() int { return z.Meta.Length() + z.Content.Length() } // Equal compares two zettel for equality. func (z Zettel) Equal(o Zettel, allowComputed bool) bool { return z.Meta.Equal(o.Meta, allowComputed) && z.Content.Equal(&o.Content) } |
Added encoder/encoder.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 | //----------------------------------------------------------------------------- // Copyright (c) 2020-present Detlef Stern // // This file is part of Zettelstore. // // Zettelstore is licensed under the latest version of the EUPL (European Union // Public License). Please see file LICENSE.txt for your rights and obligations // under this license. //----------------------------------------------------------------------------- // Package encoder provides a generic interface to encode the abstract syntax // tree into some text form. package encoder import ( "errors" "fmt" "io" "zettelstore.de/c/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, EvalMetaFunc) (int, error) WriteMeta(io.Writer, *meta.Meta, EvalMetaFunc) (int, error) WriteContent(io.Writer, *ast.ZettelNode) (int, error) WriteBlocks(io.Writer, *ast.BlockSlice) (int, error) WriteInlines(io.Writer, *ast.InlineSlice) (int, error) } // EvalMetaFunc is a function that takes a string of metadata and returns // a list of syntax elements. type EvalMetaFunc func(string) ast.InlineSlice // Some errors to signal when encoder methods are not implemented. var ( ErrNoWriteZettel = errors.New("method WriteZettel is not implemented") 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(enc api.EncodingEnum) Encoder { if create, ok := registry[enc]; ok { return create() } return nil } // CreateFunc produces a new encoder. type CreateFunc func() Encoder var registry = map[api.EncodingEnum]CreateFunc{} // Register the encoder for later retrieval. func Register(enc api.EncodingEnum, create CreateFunc) { if _, ok := registry[enc]; ok { panic(fmt.Sprintf("Encoder %q already registered", enc)) } registry[enc] = create } // GetEncodings returns all registered encodings, ordered by encoding value. func GetEncodings() []api.EncodingEnum { result := make([]api.EncodingEnum, 0, len(registry)) for enc := range registry { result = append(result, enc) } return result } |
Added encoder/encoder_blob_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 | //----------------------------------------------------------------------------- // Copyright (c) 2021-present Detlef Stern // // This file is part of Zettelstore. // // Zettelstore is licensed under the latest version of the EUPL (European Union // Public License). Please see file LICENSE.txt for your rights and obligations // under this license. //----------------------------------------------------------------------------- package encoder_test import ( "testing" "zettelstore.de/c/api" "zettelstore.de/z/config" "zettelstore.de/z/domain/id" "zettelstore.de/z/domain/meta" "zettelstore.de/z/input" "zettelstore.de/z/parser" _ "zettelstore.de/z/parser/blob" // Allow to use BLOB parser. ) type blobTestCase struct { descr string blob []byte expect expectMap } var pngTestCases = []blobTestCase{ { descr: "Minimal PNG", blob: []byte{ 0x89, 0x50, 0x4e, 0x47, 0x0d, 0x0a, 0x1a, 0x0a, 0x00, 0x00, 0x00, 0x0d, 0x49, 0x48, 0x44, 0x52, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x01, 0x08, 0x00, 0x00, 0x00, 0x00, 0x3a, 0x7e, 0x9b, 0x55, 0x00, 0x00, 0x00, 0x0a, 0x49, 0x44, 0x41, 0x54, 0x78, 0x9c, 0x63, 0x62, 0x00, 0x00, 0x00, 0x06, 0x00, 0x03, 0x36, 0x37, 0x7c, 0xa8, 0x00, 0x00, 0x00, 0x00, 0x49, 0x45, 0x4e, 0x44, 0xae, 0x42, 0x60, 0x82, }, expect: expectMap{ encoderHTML: `<p><img alt="PNG" src="data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAAAAAA6fptVAAAACklEQVR4nGNiAAAABgADNjd8qAAAAABJRU5ErkJggg=="></p>`, encoderSexpr: `(BLOCK (BLOB (INLINE (TEXT "PNG")) "png" "iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAAAAAA6fptVAAAACklEQVR4nGNiAAAABgADNjd8qAAAAABJRU5ErkJggg=="))`, encoderSHTML: `((p (img (@ (alt . "PNG") (src . "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAAAAAA6fptVAAAACklEQVR4nGNiAAAABgADNjd8qAAAAABJRU5ErkJggg==")))))`, encoderText: "", encoderZmk: `%% Unable to display BLOB with description 'PNG' and syntax 'png'.`, }, }, } func TestBlob(t *testing.T) { m := meta.New(id.Invalid) m.Set(api.KeyTitle, "PNG") for testNum, tc := range pngTestCases { inp := input.NewInput(tc.blob) pe := &peBlocks{bs: parser.ParseBlocks(inp, m, "png", config.NoHTML)} checkEncodings(t, testNum, pe, tc.descr, tc.expect, "???") } } |
Added encoder/encoder_block_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 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 | //----------------------------------------------------------------------------- // Copyright (c) 2021-present Detlef Stern // // This file is part of Zettelstore. // // Zettelstore is licensed under the latest version of the EUPL (European Union // Public License). Please see file LICENSE.txt for your rights and obligations // under this license. //----------------------------------------------------------------------------- package encoder_test var tcsBlock = []zmkTestCase{ { descr: "Empty Zettelmarkup should produce near nothing", zmk: "", expect: expectMap{ encoderHTML: "", encoderMD: "", encoderSexpr: `(BLOCK)`, encoderSHTML: `()`, encoderText: "", encoderZmk: useZmk, }, }, { descr: "Simple text: Hello, world", zmk: "Hello, world", expect: expectMap{ encoderHTML: "<p>Hello, world</p>", encoderMD: "Hello, world", encoderSexpr: `(BLOCK (PARA (TEXT "Hello,") (SPACE) (TEXT "world")))`, encoderSHTML: `((p "Hello," " " "world"))`, encoderText: "Hello, world", encoderZmk: useZmk, }, }, { descr: "Simple block comment", zmk: "%%%\nNo\nrender\n%%%", expect: expectMap{ encoderHTML: ``, encoderMD: "", encoderSexpr: `(BLOCK (VERBATIM-COMMENT () "No\nrender"))`, encoderSHTML: `(())`, encoderText: ``, encoderZmk: useZmk, }, }, { descr: "Rendered block comment", zmk: "%%%{-}\nRender\n%%%", expect: expectMap{ encoderHTML: "<!--\nRender\n-->\n", encoderMD: "", encoderSexpr: `(BLOCK (VERBATIM-COMMENT (quote (("-" . ""))) "Render"))`, encoderSHTML: "((@@@ \"Render\"))", encoderText: ``, encoderZmk: useZmk, }, }, { descr: "Simple Heading", zmk: `=== Top`, expect: expectMap{ encoderHTML: "<h2 id=\"top\">Top</h2>", encoderMD: "# Top", encoderSexpr: `(BLOCK (HEADING 1 () "top" "top" (INLINE (TEXT "Top"))))`, encoderSHTML: `((h2 (@ (id . "top")) "Top"))`, encoderText: `Top`, encoderZmk: useZmk, }, }, { descr: "Simple List", zmk: "* A\n* B\n* C", expect: expectMap{ encoderHTML: "<ul><li>A</li><li>B</li><li>C</li></ul>", encoderMD: "* A\n* B\n* C", encoderSexpr: `(BLOCK (UNORDERED (INLINE (TEXT "A")) (INLINE (TEXT "B")) (INLINE (TEXT "C"))))`, encoderSHTML: `((ul (li "A") (li "B") (li "C")))`, encoderText: "A\nB\nC", encoderZmk: useZmk, }, }, { descr: "Nested List", zmk: "* T1\n** T2\n* T3\n** T4\n** T5\n* T6", expect: expectMap{ encoderHTML: `<ul><li><p>T1</p><ul><li>T2</li></ul></li><li><p>T3</p><ul><li>T4</li><li>T5</li></ul></li><li><p>T6</p></li></ul>`, encoderMD: "* T1\n * T2\n* T3\n * T4\n * T5\n* T6", encoderSexpr: `(BLOCK (UNORDERED (BLOCK (PARA (TEXT "T1")) (UNORDERED (INLINE (TEXT "T2")))) (BLOCK (PARA (TEXT "T3")) (UNORDERED (INLINE (TEXT "T4")) (INLINE (TEXT "T5")))) (BLOCK (PARA (TEXT "T6")))))`, encoderSHTML: `((ul (li (p "T1") (ul (li "T2"))) (li (p "T3") (ul (li "T4") (li "T5"))) (li (p "T6"))))`, encoderText: "T1\nT2\nT3\nT4\nT5\nT6", encoderZmk: useZmk, }, }, { descr: "Sequence of two lists", zmk: "* Item1.1\n* Item1.2\n* Item1.3\n\n* Item2.1\n* Item2.2", expect: expectMap{ encoderHTML: "<ul><li>Item1.1</li><li>Item1.2</li><li>Item1.3</li><li>Item2.1</li><li>Item2.2</li></ul>", encoderMD: "* Item1.1\n* Item1.2\n* Item1.3\n* Item2.1\n* Item2.2", encoderSexpr: `(BLOCK (UNORDERED (INLINE (TEXT "Item1.1")) (INLINE (TEXT "Item1.2")) (INLINE (TEXT "Item1.3")) (INLINE (TEXT "Item2.1")) (INLINE (TEXT "Item2.2"))))`, encoderSHTML: `((ul (li "Item1.1") (li "Item1.2") (li "Item1.3") (li "Item2.1") (li "Item2.2")))`, encoderText: "Item1.1\nItem1.2\nItem1.3\nItem2.1\nItem2.2", encoderZmk: "* Item1.1\n* Item1.2\n* Item1.3\n* Item2.1\n* Item2.2", }, }, { descr: "Simple horizontal rule", zmk: `---`, expect: expectMap{ encoderHTML: "<hr>", encoderMD: "---", encoderSexpr: `(BLOCK (THEMATIC ()))`, encoderSHTML: `((hr))`, encoderText: ``, encoderZmk: useZmk, }, }, { descr: "Thematic break with attribute", zmk: `---{lang="zmk"}`, expect: expectMap{ encoderHTML: `<hr lang="zmk">`, encoderMD: "---", encoderSexpr: `(BLOCK (THEMATIC (quote (("lang" . "zmk")))))`, encoderSHTML: `((hr (@ (lang . "zmk"))))`, encoderText: ``, encoderZmk: useZmk, }, }, { descr: "No list after paragraph", zmk: "Text\n*abc", expect: expectMap{ encoderHTML: "<p>Text *abc</p>", encoderMD: "Text\n*abc", encoderSexpr: `(BLOCK (PARA (TEXT "Text") (SOFT) (TEXT "*abc")))`, encoderSHTML: `((p "Text" " " "*abc"))`, encoderText: `Text *abc`, encoderZmk: useZmk, }, }, { descr: "A list after paragraph", zmk: "Text\n# abc", expect: expectMap{ encoderHTML: "<p>Text</p><ol><li>abc</li></ol>", encoderMD: "Text\n\n1. abc", encoderSexpr: `(BLOCK (PARA (TEXT "Text")) (ORDERED (INLINE (TEXT "abc"))))`, encoderSHTML: `((p "Text") (ol (li "abc")))`, encoderText: "Text\nabc", encoderZmk: useZmk, }, }, { descr: "Simple List Quote", zmk: "> ToBeOrNotToBe", expect: expectMap{ encoderHTML: "<blockquote><p>ToBeOrNotToBe</p></blockquote>", encoderMD: "> ToBeOrNotToBe", encoderSexpr: `(BLOCK (QUOTATION (INLINE (TEXT "ToBeOrNotToBe"))))`, encoderSHTML: `((blockquote (p "ToBeOrNotToBe")))`, encoderText: "ToBeOrNotToBe", encoderZmk: useZmk, }, }, { descr: "Simple Quote Block", zmk: "<<<\nToBeOrNotToBe\n<<< Romeo", expect: expectMap{ encoderHTML: "<blockquote><p>ToBeOrNotToBe</p><cite>Romeo</cite></blockquote>", encoderMD: "> ToBeOrNotToBe", encoderSexpr: `(BLOCK (REGION-QUOTE () (BLOCK (PARA (TEXT "ToBeOrNotToBe"))) (INLINE (TEXT "Romeo"))))`, encoderSHTML: `((blockquote (p "ToBeOrNotToBe") (cite "Romeo")))`, encoderText: "ToBeOrNotToBe\nRomeo", encoderZmk: useZmk, }, }, { descr: "Quote Block with multiple paragraphs", zmk: "<<<\nToBeOr\n\nNotToBe\n<<< Romeo", expect: expectMap{ encoderHTML: "<blockquote><p>ToBeOr</p><p>NotToBe</p><cite>Romeo</cite></blockquote>", encoderMD: "> ToBeOr\n\n> NotToBe", encoderSexpr: `(BLOCK (REGION-QUOTE () (BLOCK (PARA (TEXT "ToBeOr")) (PARA (TEXT "NotToBe"))) (INLINE (TEXT "Romeo"))))`, encoderSHTML: `((blockquote (p "ToBeOr") (p "NotToBe") (cite "Romeo")))`, encoderText: "ToBeOr\nNotToBe\nRomeo", encoderZmk: useZmk, }, }, { descr: "Verse block", zmk: `""" A line another line Back Paragraph Spacy Para """ Author`, expect: expectMap{ encoderHTML: "<div><p>A\u00a0line<br>\u00a0\u00a0another\u00a0line<br>Back</p><p>Paragraph</p><p>\u00a0\u00a0\u00a0\u00a0Spacy\u00a0\u00a0Para</p><cite>Author</cite></div>", encoderMD: "", encoderSexpr: "(BLOCK (REGION-VERSE () (BLOCK (PARA (TEXT \"A\") (SPACE \"\u00a0\") (TEXT \"line\") (HARD) (SPACE \"\u00a0\u00a0\") (TEXT \"another\") (SPACE \"\u00a0\") (TEXT \"line\") (HARD) (TEXT \"Back\")) (PARA (TEXT \"Paragraph\")) (PARA (SPACE \"\u00a0\u00a0\u00a0\u00a0\") (TEXT \"Spacy\") (SPACE \"\u00a0\u00a0\") (TEXT \"Para\"))) (INLINE (TEXT \"Author\"))))", encoderSHTML: "((div (p \"A\" \"\u00a0\" \"line\" (br) \"\u00a0\u00a0\" \"another\" \"\u00a0\" \"line\" (br) \"Back\") (p \"Paragraph\") (p \"\u00a0\u00a0\u00a0\u00a0\" \"Spacy\" \"\u00a0\u00a0\" \"Para\") (cite \"Author\")))", encoderText: "A line\n another line\nBack\nParagraph\n Spacy Para\nAuthor", encoderZmk: "\"\"\"\nA\u00a0line\\\n\u00a0\u00a0another\u00a0line\\\nBack\nParagraph\n\u00a0\u00a0\u00a0\u00a0Spacy\u00a0\u00a0Para\n\"\"\" Author", }, }, { descr: "Span Block", zmk: `::: A simple span and much more :::`, expect: expectMap{ encoderHTML: "<div><p>A simple span and much more</p></div>", encoderMD: "", encoderSexpr: `(BLOCK (REGION-BLOCK () (BLOCK (PARA (TEXT "A") (SPACE) (TEXT "simple") (SOFT) (SPACE) (TEXT "span") (SOFT) (TEXT "and") (SPACE) (TEXT "much") (SPACE) (TEXT "more"))) (INLINE)))`, encoderSHTML: `((div (p "A" " " "simple" " " " " "span" " " "and" " " "much" " " "more")))`, encoderText: `A simple span and much more`, encoderZmk: useZmk, }, }, { descr: "Simple Verbatim Code", zmk: "```\nHello\nWorld\n```", expect: expectMap{ encoderHTML: "<pre><code>Hello\nWorld</code></pre>", encoderMD: " Hello\n World", encoderSexpr: `(BLOCK (VERBATIM-CODE () "Hello\nWorld"))`, encoderSHTML: `((pre (code "Hello\nWorld")))`, encoderText: "Hello\nWorld", encoderZmk: useZmk, }, }, { descr: "Simple Verbatim Code with visible spaces", zmk: "```{-}\nHello World\n```", expect: expectMap{ encoderHTML: "<pre><code>Hello\u2423World</code></pre>", encoderMD: " Hello World", encoderSexpr: `(BLOCK (VERBATIM-CODE (quote (("-" . ""))) "Hello World"))`, encoderSHTML: "((pre (code \"Hello\u2423World\")))", encoderText: "Hello World", encoderZmk: useZmk, }, }, { descr: "Simple Verbatim Eval", zmk: "~~~\nHello\nWorld\n~~~", expect: expectMap{ encoderHTML: "<pre><code class=\"zs-eval\">Hello\nWorld</code></pre>", encoderMD: "", encoderSexpr: `(BLOCK (VERBATIM-EVAL () "Hello\nWorld"))`, encoderSHTML: "((pre (code (@ (class . \"zs-eval\")) \"Hello\\nWorld\")))", encoderText: "Hello\nWorld", encoderZmk: useZmk, }, }, { descr: "Simple Verbatim Math", zmk: "$$$\nHello\n\\LaTeX\n$$$", expect: expectMap{ encoderHTML: "<pre><code class=\"zs-math\">Hello\n\\LaTeX</code></pre>", encoderMD: "", encoderSexpr: `(BLOCK (VERBATIM-MATH () "Hello\n\\LaTeX"))`, encoderSHTML: "((pre (code (@ (class . \"zs-math\")) \"Hello\\n\\\\LaTeX\")))", encoderText: "Hello\n\\LaTeX", encoderZmk: useZmk, }, }, { descr: "Simple Description List", zmk: "; Zettel\n: Paper\n: Note\n; Zettelkasten\n: Slip box", expect: expectMap{ encoderHTML: "<dl><dt>Zettel</dt><dd><p>Paper</p></dd><dd><p>Note</p></dd><dt>Zettelkasten</dt><dd><p>Slip box</p></dd></dl>", encoderMD: "", encoderSexpr: `(BLOCK (DESCRIPTION (INLINE (TEXT "Zettel")) (BLOCK (BLOCK (PARA (TEXT "Paper"))) (BLOCK (PARA (TEXT "Note")))) (INLINE (TEXT "Zettelkasten")) (BLOCK (BLOCK (PARA (TEXT "Slip") (SPACE) (TEXT "box"))))))`, encoderSHTML: `((dl (dt "Zettel") (dd (p "Paper")) (dd (p "Note")) (dt "Zettelkasten") (dd (p "Slip" " " "box"))))`, encoderText: "Zettel\nPaper\nNote\nZettelkasten\nSlip box", encoderZmk: useZmk, }, }, { descr: "Description List with paragraphs as item", zmk: "; Zettel\n: Paper\n\n Note\n; Zettelkasten\n: Slip box", expect: expectMap{ encoderHTML: "<dl><dt>Zettel</dt><dd><p>Paper</p><p>Note</p></dd><dt>Zettelkasten</dt><dd><p>Slip box</p></dd></dl>", encoderMD: "", encoderSexpr: `(BLOCK (DESCRIPTION (INLINE (TEXT "Zettel")) (BLOCK (BLOCK (PARA (TEXT "Paper")) (PARA (TEXT "Note")))) (INLINE (TEXT "Zettelkasten")) (BLOCK (BLOCK (PARA (TEXT "Slip") (SPACE) (TEXT "box"))))))`, encoderSHTML: `((dl (dt "Zettel") (dd (p "Paper") (p "Note")) (dt "Zettelkasten") (dd (p "Slip" " " "box"))))`, encoderText: "Zettel\nPaper\nNote\nZettelkasten\nSlip box", encoderZmk: useZmk, }, }, { descr: "Simple Table", zmk: "|c1|c2|c3\n|d1||d3", expect: expectMap{ encoderHTML: `<table><tbody><tr><td>c1</td><td>c2</td><td>c3</td></tr><tr><td>d1</td><td></td><td>d3</td></tr></tbody></table>`, encoderMD: "", encoderSexpr: `(BLOCK (TABLE () (LIST (CELL (TEXT "c1")) (CELL (TEXT "c2")) (CELL (TEXT "c3"))) (LIST (CELL (TEXT "d1")) (CELL) (CELL (TEXT "d3")))))`, encoderSHTML: `((table (tbody (tr (td "c1") (td "c2") (td "c3")) (tr (td "d1") (td) (td "d3")))))`, encoderText: "c1 c2 c3\nd1 d3", encoderZmk: useZmk, }, }, { descr: "Table with alignment and comment", zmk: `|h1>|=h2|h3:| |%--+---+---+ |<c1|c2|:c3| |f1|f2|=f3`, expect: expectMap{ encoderHTML: `<table><thead><tr><td class="right">h1</td><td>h2</td><td class="center">h3</td></tr></thead><tbody><tr><td class="left">c1</td><td>c2</td><td class="center">c3</td></tr><tr><td class="right">f1</td><td>f2</td><td class="center">=f3</td></tr></tbody></table>`, encoderMD: "", encoderSexpr: `(BLOCK (TABLE (LIST (CELL-RIGHT (TEXT "h1")) (CELL (TEXT "h2")) (CELL-CENTER (TEXT "h3"))) (LIST (CELL-LEFT (TEXT "c1")) (CELL (TEXT "c2")) (CELL-CENTER (TEXT "c3"))) (LIST (CELL-RIGHT (TEXT "f1")) (CELL (TEXT "f2")) (CELL-CENTER (TEXT "=f3")))))`, encoderSHTML: `((table (thead (tr (td (@ (class . "right")) "h1") (td "h2") (td (@ (class . "center")) "h3"))) (tbody (tr (td (@ (class . "left")) "c1") (td "c2") (td (@ (class . "center")) "c3")) (tr (td (@ (class . "right")) "f1") (td "f2") (td (@ (class . "center")) "=f3")))))`, encoderText: "h1 h2 h3\nc1 c2 c3\nf1 f2 =f3", encoderZmk: `|=h1>|=h2|=h3: |<c1|c2|c3 |f1|f2|=f3`, }, }, { descr: "Simple Endnote", zmk: `Text[^Endnote]`, expect: expectMap{ encoderHTML: "<p>Text<sup id=\"fnref:1\"><a class=\"zs-noteref\" href=\"#fn:1\" role=\"doc-noteref\">1</a></sup></p><ol class=\"zs-endnotes\"><li class=\"zs-endnote\" id=\"fn:1\" role=\"doc-endnote\" value=\"1\">Endnote <a class=\"zs-endnote-backref\" href=\"#fnref:1\" role=\"doc-backlink\">\u21a9\ufe0e</a></li></ol>", encoderMD: "Text", encoderSexpr: `(BLOCK (PARA (TEXT "Text") (ENDNOTE () (quote (INLINE (TEXT "Endnote"))))))`, encoderSHTML: "((p \"Text\" (sup (@ (id . \"fnref:1\")) (a (@ (class . \"zs-noteref\") (href . \"#fn:1\") (role . \"doc-noteref\")) \"1\"))))", encoderText: "Text Endnote", encoderZmk: useZmk, }, }, { descr: "Nested Endnotes", zmk: `Text[^Endnote[^Nested]]`, expect: expectMap{ encoderHTML: "<p>Text<sup id=\"fnref:1\"><a class=\"zs-noteref\" href=\"#fn:1\" role=\"doc-noteref\">1</a></sup></p><ol class=\"zs-endnotes\"><li class=\"zs-endnote\" id=\"fn:1\" role=\"doc-endnote\" value=\"1\">Endnote<sup id=\"fnref:2\"><a class=\"zs-noteref\" href=\"#fn:2\" role=\"doc-noteref\">2</a></sup> <a class=\"zs-endnote-backref\" href=\"#fnref:1\" role=\"doc-backlink\">\u21a9\ufe0e</a></li><li class=\"zs-endnote\" id=\"fn:2\" role=\"doc-endnote\" value=\"2\">Nested <a class=\"zs-endnote-backref\" href=\"#fnref:2\" role=\"doc-backlink\">\u21a9\ufe0e</a></li></ol>", encoderMD: "Text", encoderSexpr: `(BLOCK (PARA (TEXT "Text") (ENDNOTE () (quote (INLINE (TEXT "Endnote") (ENDNOTE () (quote (INLINE (TEXT "Nested")))))))))`, encoderSHTML: "((p \"Text\" (sup (@ (id . \"fnref:1\")) (a (@ (class . \"zs-noteref\") (href . \"#fn:1\") (role . \"doc-noteref\")) \"1\"))))", encoderText: "Text Endnote Nested", encoderZmk: useZmk, }, }, { descr: "Transclusion", zmk: `{{{http://example.com/image}}}{width="100px"}`, expect: expectMap{ encoderHTML: `<p><img class="external" src="http://example.com/image" width="100px"></p>`, encoderMD: "", encoderSexpr: `(BLOCK (TRANSCLUDE (quote (("width" . "100px"))) (quote (EXTERNAL "http://example.com/image"))))`, encoderSHTML: `((p (img (@ (class . "external") (src . "http://example.com/image") (width . "100px")))))`, encoderText: "", encoderZmk: useZmk, }, }, { descr: "A paragraph with a inline comment only should be empty in HTML", zmk: `%% Comment`, expect: expectMap{ // encoderHTML: ``, encoderSexpr: `(BLOCK (PARA (LITERAL-COMMENT () "Comment")))`, // encoderSHTML: ``, encoderText: "", encoderZmk: useZmk, }, }, { descr: "", zmk: ``, expect: expectMap{ encoderHTML: ``, encoderSexpr: `(BLOCK)`, encoderSHTML: `()`, encoderText: "", encoderZmk: useZmk, }, }, } // func TestEncoderBlock(t *testing.T) { // executeTestCases(t, tcsBlock) // } |
Added encoder/encoder_inline_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 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 593 594 595 596 597 598 599 600 601 602 603 604 605 606 607 608 609 610 611 612 613 | //----------------------------------------------------------------------------- // Copyright (c) 2021-present Detlef Stern // // This file is part of Zettelstore. // // Zettelstore is licensed under the latest version of the EUPL (European Union // Public License). Please see file LICENSE.txt for your rights and obligations // under this license. //----------------------------------------------------------------------------- package encoder_test var tcsInline = []zmkTestCase{ { descr: "Empty Zettelmarkup should produce near nothing (inline)", zmk: "", expect: expectMap{ encoderHTML: "", encoderMD: "", encoderSexpr: `(INLINE)`, encoderSHTML: `()`, encoderText: "", encoderZmk: useZmk, }, }, { descr: "Simple text: Hello, world (inline)", zmk: `Hello, world`, expect: expectMap{ encoderHTML: "Hello, world", encoderMD: "Hello, world", encoderSexpr: `(INLINE (TEXT "Hello,") (SPACE) (TEXT "world"))`, encoderSHTML: `("Hello," " " "world")`, encoderText: "Hello, world", encoderZmk: useZmk, }, }, { descr: "Soft Break", zmk: "soft\nbreak", expect: expectMap{ encoderHTML: "soft break", encoderMD: "soft\nbreak", encoderSexpr: `(INLINE (TEXT "soft") (SOFT) (TEXT "break"))`, encoderSHTML: `("soft" " " "break")`, encoderText: "soft break", encoderZmk: useZmk, }, }, { descr: "Hard Break", zmk: "hard\\\nbreak", expect: expectMap{ encoderHTML: "hard<br>break", encoderMD: "hard\\\nbreak", encoderSexpr: `(INLINE (TEXT "hard") (HARD) (TEXT "break"))`, encoderSHTML: `("hard" (br) "break")`, encoderText: "hard\nbreak", encoderZmk: useZmk, }, }, { descr: "Emphasized formatting", zmk: "__emph__", expect: expectMap{ encoderHTML: "<em>emph</em>", encoderMD: "*emph*", encoderSexpr: `(INLINE (FORMAT-EMPH () (TEXT "emph")))`, encoderSHTML: `((em "emph"))`, encoderText: "emph", encoderZmk: useZmk, }, }, { descr: "Strong formatting", zmk: "**strong**", expect: expectMap{ encoderHTML: "<strong>strong</strong>", encoderMD: "__strong__", encoderSexpr: `(INLINE (FORMAT-STRONG () (TEXT "strong")))`, encoderSHTML: `((strong "strong"))`, encoderText: "strong", encoderZmk: useZmk, }, }, { descr: "Insert formatting", zmk: ">>insert>>", expect: expectMap{ encoderHTML: "<ins>insert</ins>", encoderMD: "insert", encoderSexpr: `(INLINE (FORMAT-INSERT () (TEXT "insert")))`, encoderSHTML: `((ins "insert"))`, encoderText: "insert", encoderZmk: useZmk, }, }, { descr: "Delete formatting", zmk: "~~delete~~", expect: expectMap{ encoderHTML: "<del>delete</del>", encoderMD: "delete", encoderSexpr: `(INLINE (FORMAT-DELETE () (TEXT "delete")))`, encoderSHTML: `((del "delete"))`, encoderText: "delete", encoderZmk: useZmk, }, }, { descr: "Update formatting", zmk: "~~old~~>>new>>", expect: expectMap{ encoderHTML: "<del>old</del><ins>new</ins>", encoderMD: "oldnew", encoderSexpr: `(INLINE (FORMAT-DELETE () (TEXT "old")) (FORMAT-INSERT () (TEXT "new")))`, encoderSHTML: `((del "old") (ins "new"))`, encoderText: "oldnew", encoderZmk: useZmk, }, }, { descr: "Superscript formatting", zmk: "^^superscript^^", expect: expectMap{ encoderHTML: `<sup>superscript</sup>`, encoderMD: "superscript", encoderSexpr: `(INLINE (FORMAT-SUPER () (TEXT "superscript")))`, encoderSHTML: `((sup "superscript"))`, encoderText: `superscript`, encoderZmk: useZmk, }, }, { descr: "Subscript formatting", zmk: ",,subscript,,", expect: expectMap{ encoderHTML: `<sub>subscript</sub>`, encoderMD: "subscript", encoderSexpr: `(INLINE (FORMAT-SUB () (TEXT "subscript")))`, encoderSHTML: `((sub "subscript"))`, encoderText: `subscript`, encoderZmk: useZmk, }, }, { descr: "Quotes formatting", zmk: `""quotes""`, expect: expectMap{ encoderHTML: "<q>quotes</q>", encoderMD: "<q>quotes</q>", encoderSexpr: `(INLINE (FORMAT-QUOTE () (TEXT "quotes")))`, encoderSHTML: `((q "quotes"))`, encoderText: `quotes`, encoderZmk: useZmk, }, }, { descr: "Quotes formatting (german)", zmk: `""quotes""{lang=de}`, expect: expectMap{ encoderHTML: `<span lang="de"><q>quotes</q></span>`, encoderMD: "<q>quotes</q>", encoderSexpr: `(INLINE (FORMAT-QUOTE (quote (("lang" . "de"))) (TEXT "quotes")))`, encoderSHTML: `((span (@ (lang . "de")) (q "quotes")))`, encoderText: `quotes`, encoderZmk: `""quotes""{lang="de"}`, }, }, { descr: "Span formatting", zmk: `::span::`, expect: expectMap{ encoderHTML: `<span>span</span>`, encoderMD: "span", encoderSexpr: `(INLINE (FORMAT-SPAN () (TEXT "span")))`, encoderSHTML: `((span "span"))`, encoderText: `span`, encoderZmk: useZmk, }, }, { descr: "Code formatting", zmk: "``code``", expect: expectMap{ encoderHTML: `<code>code</code>`, encoderMD: "`code`", encoderSexpr: `(INLINE (LITERAL-CODE () "code"))`, encoderSHTML: `((code "code"))`, encoderText: `code`, encoderZmk: useZmk, }, }, { descr: "Code formatting with visible space", zmk: "``x y``{-}", expect: expectMap{ encoderHTML: "<code>x\u2423y</code>", encoderMD: "`x y`", encoderSexpr: `(INLINE (LITERAL-CODE (quote (("-" . ""))) "x y"))`, encoderSHTML: "((code \"x\u2423y\"))", encoderText: `x y`, encoderZmk: useZmk, }, }, { descr: "HTML in Code formatting", zmk: "``<script `` abc", expect: expectMap{ encoderHTML: "<code><script </code> abc", encoderMD: "`<script ` abc", encoderSexpr: `(INLINE (LITERAL-CODE () "<script ") (SPACE) (TEXT "abc"))`, encoderSHTML: `((code "<script ") " " "abc")`, encoderText: `<script abc`, encoderZmk: useZmk, }, }, { descr: "Input formatting", zmk: `''input''`, expect: expectMap{ encoderHTML: `<kbd>input</kbd>`, encoderMD: "input", encoderSexpr: `(INLINE (LITERAL-INPUT () "input"))`, encoderSHTML: `((kbd "input"))`, encoderText: `input`, encoderZmk: useZmk, }, }, { descr: "Output formatting", zmk: `==output==`, expect: expectMap{ encoderHTML: `<samp>output</samp>`, encoderMD: "output", encoderSexpr: `(INLINE (LITERAL-OUTPUT () "output"))`, encoderSHTML: `((samp "output"))`, encoderText: `output`, encoderZmk: useZmk, }, }, { descr: "Math formatting", zmk: `$$\TeX$$`, expect: expectMap{ encoderHTML: `<code class="zs-math">\TeX</code>`, encoderMD: "\\TeX", encoderSexpr: `(INLINE (LITERAL-MATH () "\\TeX"))`, encoderSHTML: `((code (@ (class . "zs-math")) "\\TeX"))`, encoderText: `\TeX`, encoderZmk: useZmk, }, }, { descr: "Nested Span Quote formatting", zmk: `::""abc""::{lang=fr}`, expect: expectMap{ encoderHTML: `<span lang="fr"><q>abc</q></span>`, encoderMD: "<q>abc</q>", encoderSexpr: `(INLINE (FORMAT-SPAN (quote (("lang" . "fr"))) (FORMAT-QUOTE () (TEXT "abc"))))`, encoderSHTML: `((span (@ (lang . "fr")) (q "abc")))`, encoderText: `abc`, encoderZmk: `::""abc""::{lang="fr"}`, }, }, { descr: "Simple Citation", zmk: `[@Stern18]`, expect: expectMap{ encoderHTML: `<span>Stern18</span>`, // TODO encoderMD: "", encoderSexpr: `(INLINE (CITE () "Stern18"))`, encoderSHTML: `((span "Stern18"))`, // TODO encoderText: ``, encoderZmk: useZmk, }, }, { descr: "Citation", zmk: `[@Stern18 p.23]`, expect: expectMap{ encoderHTML: `<span>Stern18, p.23</span>`, // TODO encoderMD: "p.23", encoderSexpr: `(INLINE (CITE () "Stern18" (TEXT "p.23")))`, encoderSHTML: `((span "Stern18" ", " "p.23"))`, // TODO encoderText: `p.23`, encoderZmk: useZmk, }, }, { descr: "No comment", zmk: `% comment`, expect: expectMap{ encoderHTML: `% comment`, encoderMD: "% comment", encoderSexpr: `(INLINE (TEXT "%") (SPACE) (TEXT "comment"))`, encoderSHTML: `("%" " " "comment")`, encoderText: `% comment`, encoderZmk: useZmk, }, }, { descr: "Line comment (nogen HTML)", zmk: `%% line comment`, expect: expectMap{ encoderHTML: ``, encoderMD: "", encoderSexpr: `(INLINE (LITERAL-COMMENT () "line comment"))`, encoderSHTML: `(())`, encoderText: ``, encoderZmk: useZmk, }, }, { descr: "Line comment", zmk: `%%{-} line comment`, expect: expectMap{ encoderHTML: `<!-- line comment -->`, encoderMD: "", encoderSexpr: `(INLINE (LITERAL-COMMENT (quote (("-" . ""))) "line comment"))`, encoderSHTML: `((@@ "line comment"))`, encoderText: ``, encoderZmk: useZmk, }, }, { descr: "Comment after text", zmk: `Text %%{-} comment`, expect: expectMap{ encoderHTML: `Text<!-- comment -->`, encoderMD: "Text", encoderSexpr: `(INLINE (TEXT "Text") (LITERAL-COMMENT (quote (("-" . ""))) "comment"))`, encoderSHTML: `("Text" (@@ "comment"))`, encoderText: `Text`, encoderZmk: useZmk, }, }, { descr: "Comment after text and with -->", zmk: `Text %%{-} comment --> end`, expect: expectMap{ encoderHTML: `Text<!-- comment --> end -->`, encoderMD: "Text", encoderSexpr: `(INLINE (TEXT "Text") (LITERAL-COMMENT (quote (("-" . ""))) "comment --> end"))`, encoderSHTML: `("Text" (@@ "comment --> end"))`, encoderText: `Text`, encoderZmk: useZmk, }, }, { descr: "Simple inline endnote", zmk: `[^endnote]`, expect: expectMap{ encoderHTML: `<sup id="fnref:1"><a class="zs-noteref" href="#fn:1" role="doc-noteref">1</a></sup>`, encoderMD: "", encoderSexpr: `(INLINE (ENDNOTE () (quote (INLINE (TEXT "endnote")))))`, encoderSHTML: `((sup (@ (id . "fnref:1")) (a (@ (class . "zs-noteref") (href . "#fn:1") (role . "doc-noteref")) "1")))`, encoderText: `endnote`, encoderZmk: useZmk, }, }, { descr: "Simple mark", zmk: `[!mark]`, expect: expectMap{ encoderHTML: `<a id="mark"></a>`, encoderMD: "", encoderSexpr: `(INLINE (MARK "mark" "mark" "mark"))`, encoderSHTML: `((a (@ (id . "mark"))))`, encoderText: ``, encoderZmk: useZmk, }, }, { descr: "Mark with text", zmk: `[!mark|with text]`, expect: expectMap{ encoderHTML: `<a id="mark">with text</a>`, encoderMD: "with text", encoderSexpr: `(INLINE (MARK "mark" "mark" "mark" (TEXT "with") (SPACE) (TEXT "text")))`, encoderSHTML: `((a (@ (id . "mark")) "with" " " "text"))`, encoderText: `with text`, encoderZmk: useZmk, }, }, { descr: "Invalid Link", zmk: `[[link|00000000000000]]`, expect: expectMap{ encoderHTML: `<span>link</span>`, encoderMD: "[link](00000000000000)", encoderSexpr: `(INLINE (LINK-INVALID () "00000000000000" (TEXT "link")))`, encoderSHTML: `((span "link"))`, encoderText: `link`, encoderZmk: useZmk, }, }, { descr: "Invalid Simple Link", zmk: `[[00000000000000]]`, expect: expectMap{ encoderHTML: `<span>00000000000000</span>`, encoderMD: "[00000000000000](00000000000000)", encoderSexpr: `(INLINE (LINK-INVALID () "00000000000000"))`, encoderSHTML: `((span "00000000000000"))`, encoderText: ``, encoderZmk: useZmk, }, }, { descr: "Dummy Link", zmk: `[[abc]]`, expect: expectMap{ encoderHTML: `<a class="external" href="abc">abc</a>`, encoderMD: "[abc](abc)", encoderSexpr: `(INLINE (LINK-EXTERNAL () "abc"))`, encoderSHTML: `((a (@ (class . "external") (href . "abc")) "abc"))`, encoderText: ``, encoderZmk: useZmk, }, }, { descr: "Simple URL", zmk: `[[https://zettelstore.de]]`, expect: expectMap{ encoderHTML: `<a class="external" href="https://zettelstore.de">https://zettelstore.de</a>`, encoderMD: "<https://zettelstore.de>", encoderSexpr: `(INLINE (LINK-EXTERNAL () "https://zettelstore.de"))`, encoderSHTML: `((a (@ (class . "external") (href . "https://zettelstore.de")) "https://zettelstore.de"))`, encoderText: ``, encoderZmk: useZmk, }, }, { descr: "URL with Text", zmk: `[[Home|https://zettelstore.de]]`, expect: expectMap{ encoderHTML: `<a class="external" href="https://zettelstore.de">Home</a>`, encoderMD: "[Home](https://zettelstore.de)", encoderSexpr: `(INLINE (LINK-EXTERNAL () "https://zettelstore.de" (TEXT "Home")))`, encoderSHTML: `((a (@ (class . "external") (href . "https://zettelstore.de")) "Home"))`, encoderText: `Home`, encoderZmk: useZmk, }, }, { descr: "Simple Zettel ID", zmk: `[[00000000000100]]`, expect: expectMap{ encoderHTML: `<a href="00000000000100">00000000000100</a>`, encoderMD: "[00000000000100](00000000000100)", encoderSexpr: `(INLINE (LINK-ZETTEL () "00000000000100"))`, encoderSHTML: `((a (@ (href . "00000000000100")) "00000000000100"))`, encoderText: ``, encoderZmk: useZmk, }, }, { descr: "Zettel ID with Text", zmk: `[[Config|00000000000100]]`, expect: expectMap{ encoderHTML: `<a href="00000000000100">Config</a>`, encoderMD: "[Config](00000000000100)", encoderSexpr: `(INLINE (LINK-ZETTEL () "00000000000100" (TEXT "Config")))`, encoderSHTML: `((a (@ (href . "00000000000100")) "Config"))`, encoderText: `Config`, encoderZmk: useZmk, }, }, { descr: "Simple Zettel ID with fragment", zmk: `[[00000000000100#frag]]`, expect: expectMap{ encoderHTML: `<a href="00000000000100#frag">00000000000100#frag</a>`, encoderMD: "[00000000000100#frag](00000000000100#frag)", encoderSexpr: `(INLINE (LINK-ZETTEL () "00000000000100#frag"))`, encoderSHTML: `((a (@ (href . "00000000000100#frag")) "00000000000100#frag"))`, encoderText: ``, encoderZmk: useZmk, }, }, { descr: "Zettel ID with Text and fragment", zmk: `[[Config|00000000000100#frag]]`, expect: expectMap{ encoderHTML: `<a href="00000000000100#frag">Config</a>`, encoderMD: "[Config](00000000000100#frag)", encoderSexpr: `(INLINE (LINK-ZETTEL () "00000000000100#frag" (TEXT "Config")))`, encoderSHTML: `((a (@ (href . "00000000000100#frag")) "Config"))`, encoderText: `Config`, encoderZmk: useZmk, }, }, { descr: "Fragment link to self", zmk: `[[#frag]]`, expect: expectMap{ encoderHTML: `<a href="#frag">#frag</a>`, encoderMD: "[#frag](#frag)", encoderSexpr: `(INLINE (LINK-SELF () "#frag"))`, encoderSHTML: `((a (@ (href . "#frag")) "#frag"))`, encoderText: ``, encoderZmk: useZmk, }, }, { descr: "Hosted link", zmk: `[[H|/hosted]]`, expect: expectMap{ encoderHTML: `<a href="/hosted">H</a>`, encoderMD: "[H](/hosted)", encoderSexpr: `(INLINE (LINK-HOSTED () "/hosted" (TEXT "H")))`, encoderSHTML: `((a (@ (href . "/hosted")) "H"))`, encoderText: `H`, encoderZmk: useZmk, }, }, { descr: "Based link", zmk: `[[B|//based]]`, expect: expectMap{ encoderHTML: `<a href="/based">B</a>`, encoderMD: "[B](/based)", encoderSexpr: `(INLINE (LINK-BASED () "/based" (TEXT "B")))`, encoderText: `B`, encoderSHTML: `((a (@ (href . "/based")) "B"))`, encoderZmk: useZmk, }, }, { descr: "Relative link", zmk: `[[R|../relative]]`, expect: expectMap{ encoderHTML: `<a href="../relative">R</a>`, encoderMD: "[R](../relative)", encoderSexpr: `(INLINE (LINK-HOSTED () "../relative" (TEXT "R")))`, encoderSHTML: `((a (@ (href . "../relative")) "R"))`, encoderText: `R`, encoderZmk: useZmk, }, }, { descr: "Query link w/o text", zmk: `[[query:title:syntax]]`, expect: expectMap{ encoderHTML: `<a href="?q=title%3Asyntax">title:syntax</a>`, encoderMD: "", encoderSexpr: `(INLINE (LINK-QUERY () "title:syntax"))`, encoderSHTML: `((a (@ (href . "?q=title%3Asyntax")) "title:syntax"))`, encoderText: ``, encoderZmk: useZmk, }, }, { descr: "Query link with text", zmk: `[[Q|query:title:syntax]]`, expect: expectMap{ encoderHTML: `<a href="?q=title%3Asyntax">Q</a>`, encoderMD: "Q", encoderSexpr: `(INLINE (LINK-QUERY () "title:syntax" (TEXT "Q")))`, encoderSHTML: `((a (@ (href . "?q=title%3Asyntax")) "Q"))`, encoderText: `Q`, encoderZmk: useZmk, }, }, { descr: "Dummy Embed", zmk: `{{abc}}`, expect: expectMap{ encoderHTML: `<img src="abc">`, encoderMD: "", encoderSexpr: `(INLINE (EMBED () (quote (EXTERNAL "abc")) ""))`, encoderSHTML: `((img (@ (src . "abc"))))`, encoderText: ``, encoderZmk: useZmk, }, }, { descr: "Inline HTML Zettel", zmk: `@@<hr>@@{="html"}`, expect: expectMap{ encoderHTML: ``, encoderMD: "", encoderSexpr: `(INLINE)`, encoderSHTML: `()`, encoderText: ``, encoderZmk: ``, }, }, { descr: "Inline Text Zettel", zmk: `@@<hr>@@{="text"}`, expect: expectMap{ encoderHTML: ``, encoderMD: "<hr>", encoderSexpr: `(INLINE (LITERAL-ZETTEL (quote (("" . "text"))) "<hr>"))`, encoderSHTML: `(())`, encoderText: `<hr>`, encoderZmk: useZmk, }, }, { descr: "", zmk: ``, expect: expectMap{ encoderHTML: ``, encoderMD: "", encoderSexpr: `(INLINE)`, encoderSHTML: `()`, encoderText: ``, encoderZmk: useZmk, }, }, } |
Added encoder/encoder_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 | //----------------------------------------------------------------------------- // Copyright (c) 2021-present Detlef Stern // // This file is part of Zettelstore. // // Zettelstore is licensed under the latest version of the EUPL (European Union // Public License). Please see file LICENSE.txt for your rights and obligations // under this license. //----------------------------------------------------------------------------- package encoder_test import ( "fmt" "strings" "testing" "codeberg.org/t73fde/sxpf/reader" "zettelstore.de/c/api" "zettelstore.de/z/ast" "zettelstore.de/z/config" "zettelstore.de/z/domain/meta" "zettelstore.de/z/encoder" "zettelstore.de/z/input" "zettelstore.de/z/parser" _ "zettelstore.de/z/encoder/htmlenc" // Allow to use HTML encoder. _ "zettelstore.de/z/encoder/mdenc" // Allow to use markdown encoder. _ "zettelstore.de/z/encoder/sexprenc" // Allow to use sexpr encoder. _ "zettelstore.de/z/encoder/shtmlenc" // Allow to use SHTML encoder. _ "zettelstore.de/z/encoder/textenc" // Allow to use text encoder. _ "zettelstore.de/z/encoder/zmkenc" // Allow to use zmk encoder. "zettelstore.de/z/parser/cleaner" _ "zettelstore.de/z/parser/zettelmark" // Allow to use zettelmark parser. ) type zmkTestCase struct { descr string zmk string inline bool expect expectMap } type expectMap map[api.EncodingEnum]string const useZmk = "\000" const ( encoderHTML = api.EncoderHTML encoderMD = api.EncoderMD encoderSexpr = api.EncoderSexpr encoderSHTML = api.EncoderSHTML encoderText = api.EncoderText encoderZmk = api.EncoderZmk ) func TestEncoder(t *testing.T) { for i := range tcsInline { tcsInline[i].inline = true } executeTestCases(t, append(tcsBlock, tcsInline...)) } func executeTestCases(t *testing.T, testCases []zmkTestCase) { for testNum, tc := range testCases { inp := input.NewInput([]byte(tc.zmk)) var pe parserEncoder if tc.inline { is := parser.ParseInlines(inp, meta.SyntaxZmk) cleaner.CleanInlineSlice(&is) pe = &peInlines{is: is} } else { pe = &peBlocks{bs: parser.ParseBlocks(inp, nil, meta.SyntaxZmk, config.NoHTML)} } checkEncodings(t, testNum, pe, tc.descr, tc.expect, tc.zmk) checkSexpr(t, testNum, pe, tc.descr) } } func checkEncodings(t *testing.T, testNum int, pe parserEncoder, descr string, expected expectMap, zmkDefault string) { for enc, exp := range expected { encdr := encoder.Create(enc) got, err := pe.encode(encdr) if err != nil { prefix := fmt.Sprintf("Test #%d", testNum) if d := descr; d != "" { prefix += "\nReason: " + d } prefix += "\nMode: " + pe.mode() t.Errorf("%s\nEncoder: %s\nError: %v", prefix, enc, err) continue } if enc == api.EncoderZmk && exp == useZmk { exp = zmkDefault } if got != exp { prefix := fmt.Sprintf("Test #%d", testNum) if d := descr; d != "" { prefix += "\nReason: " + d } prefix += "\nMode: " + pe.mode() t.Errorf("%s\nEncoder: %s\nExpected: %q\nGot: %q", prefix, enc, exp, got) } } } func checkSexpr(t *testing.T, testNum int, pe parserEncoder, descr string) { t.Helper() encdr := encoder.Create(encoderSexpr) exp, err := pe.encode(encdr) if err != nil { t.Error(err) return } val, err := reader.MakeReader(strings.NewReader(exp)).Read() if err != nil { t.Error(err) return } got := val.Repr() if exp != got { prefix := fmt.Sprintf("Test #%d", testNum) if d := descr; d != "" { prefix += "\nReason: " + d } prefix += "\nMode: " + pe.mode() t.Errorf("%s\n\nExpected: %q\nGot: %q", prefix, exp, got) } } type parserEncoder interface { encode(encoder.Encoder) (string, error) mode() string } type peInlines struct { is ast.InlineSlice } func (in peInlines) encode(encdr encoder.Encoder) (string, error) { var sb strings.Builder if _, err := encdr.WriteInlines(&sb, &in.is); err != nil { return "", err } return sb.String(), nil } func (peInlines) mode() string { return "inline" } type peBlocks struct { bs ast.BlockSlice } func (bl peBlocks) encode(encdr encoder.Encoder) (string, error) { var sb strings.Builder if _, err := encdr.WriteBlocks(&sb, &bl.bs); err != nil { return "", err } return sb.String(), nil } func (peBlocks) mode() string { return "block" } |
Added encoder/htmlenc/htmlenc.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) 2022-present Detlef Stern // // This file is part of Zettelstore. // // Zettelstore is licensed under the latest version of the EUPL (European Union // Public License). Please see file LICENSE.txt for your rights and obligations // under this license. //----------------------------------------------------------------------------- // Package htmlenc encodes the abstract syntax tree into HTML5 via zettelstore-client. package htmlenc import ( "io" "strings" "codeberg.org/t73fde/sxhtml" "codeberg.org/t73fde/sxpf" "zettelstore.de/c/api" "zettelstore.de/c/shtml" "zettelstore.de/z/ast" "zettelstore.de/z/domain/meta" "zettelstore.de/z/encoder" "zettelstore.de/z/encoder/sexprenc" "zettelstore.de/z/encoder/shtmlenc" "zettelstore.de/z/encoder/textenc" "zettelstore.de/z/parser" ) func init() { encoder.Register(api.EncoderHTML, func() encoder.Encoder { return Create() }) } // Create an encoder. func Create() *Encoder { return &mySHE } type Encoder struct { textEnc *textenc.Encoder } var mySHE = Encoder{ textEnc: textenc.Create(), } // WriteZettel encodes a full zettel as HTML5. func (he *Encoder) WriteZettel(w io.Writer, zn *ast.ZettelNode, evalMeta encoder.EvalMetaFunc) (int, error) { tx := sexprenc.NewTransformer() xm := tx.GetMeta(zn.InhMeta, evalMeta) th := shtml.NewTransformer(1, nil) hm, err := th.Transform(xm) if err != nil { return 0, err } var isTitle ast.InlineSlice var htitle *sxpf.List plainTitle, hasTitle := zn.InhMeta.Get(api.KeyTitle) if hasTitle { isTitle = parser.ParseSpacedText(plainTitle) xtitle := tx.GetSexpr(&isTitle) htitle, err = th.Transform(xtitle) if err != nil { return 0, err } } xast := tx.GetSexpr(&zn.Ast) hast, err := th.Transform(xast) if err != nil { return 0, err } hen := th.Endnotes() sf := th.SymbolFactory() symAttr := sf.MustMake(sxhtml.NameSymAttr) head := sxpf.Nil().Cons(sf.MustMake("head")) curr := head curr = curr.AppendBang(sxpf.Nil().Cons(sxpf.Nil().Cons(sxpf.Cons(sf.MustMake("charset"), sxpf.MakeString("utf-8"))).Cons(symAttr)).Cons(sf.MustMake("meta"))) for elem := hm; elem != nil; elem = elem.Tail() { curr = curr.AppendBang(elem.Car()) } if hasTitle { var sb strings.Builder he.textEnc.WriteInlines(&sb, &isTitle) _ = curr.AppendBang(sxpf.Nil().Cons(sxpf.MakeString(sb.String())).Cons(sf.MustMake("title"))) } body := sxpf.Nil().Cons(sf.MustMake("body")) curr = body if hasTitle { curr = curr.AppendBang(htitle.Cons(sf.MustMake("h1"))) } for elem := hast; elem != nil; elem = elem.Tail() { curr = curr.AppendBang(elem.Car()) } if hen != nil { curr = curr.AppendBang(sxpf.Nil().Cons(sf.MustMake("hr"))) _ = curr.AppendBang(hen) } doc := sxpf.MakeList( sf.MustMake(sxhtml.NameSymDoctype), sxpf.MakeList(sf.MustMake("html"), head, body), ) gen := sxhtml.NewGenerator(sf, sxhtml.WithNewline) return gen.WriteHTML(w, doc) } // WriteMeta encodes meta data as HTML5. func (he *Encoder) WriteMeta(w io.Writer, m *meta.Meta, evalMeta encoder.EvalMetaFunc) (int, error) { tx := sexprenc.NewTransformer() xm := tx.GetMeta(m, evalMeta) th := shtml.NewTransformer(1, nil) hm, err := th.Transform(xm) if err != nil { return 0, err } gen := sxhtml.NewGenerator(th.SymbolFactory(), sxhtml.WithNewline) return gen.WriteListHTML(w, hm) } func (he *Encoder) WriteContent(w io.Writer, zn *ast.ZettelNode) (int, error) { return he.WriteBlocks(w, &zn.Ast) } // WriteBlocks encodes a block slice. func (*Encoder) WriteBlocks(w io.Writer, bs *ast.BlockSlice) (int, error) { tx := sexprenc.NewTransformer() xval := tx.GetSexpr(bs) th := shtml.NewTransformer(1, nil) hobj, err := th.Transform(xval) if err == nil { gen := sxhtml.NewGenerator(th.SymbolFactory()) length, err2 := gen.WriteListHTML(w, hobj) if err2 != nil { return length, err2 } l, err2 := gen.WriteHTML(w, th.Endnotes()) length += l return length, err2 } return 0, err } // WriteInlines writes an inline slice to the writer func (*Encoder) WriteInlines(w io.Writer, is *ast.InlineSlice) (int, error) { hobj, err := shtmlenc.TransformSlice(is) if err == nil { gen := sxhtml.NewGenerator(sxpf.FindSymbolFactory(hobj)) length, err2 := gen.WriteListHTML(w, hobj) if err2 != nil { return length, err2 } return length, nil } return 0, err } |
Added encoder/mdenc/mdenc.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 | //----------------------------------------------------------------------------- // Copyright (c) 2022-present Detlef Stern // // This file is part of Zettelstore. // // Zettelstore is licensed under the latest version of the EUPL (European Union // Public License). Please see file LICENSE.txt for your rights and obligations // under this license. //----------------------------------------------------------------------------- // Package mdenc encodes the abstract syntax tree back into Markdown. package mdenc import ( "io" "zettelstore.de/c/api" "zettelstore.de/z/ast" "zettelstore.de/z/domain/meta" "zettelstore.de/z/encoder" ) func init() { encoder.Register(api.EncoderMD, func() encoder.Encoder { return Create() }) } // Create an encoder. func Create() *Encoder { return &myME } type Encoder struct{} var myME Encoder // WriteZettel writes the encoded zettel to the writer. func (*Encoder) WriteZettel(w io.Writer, zn *ast.ZettelNode, evalMeta encoder.EvalMetaFunc) (int, error) { v := newVisitor(w) v.acceptMeta(zn.InhMeta, evalMeta) if zn.InhMeta.YamlSep { v.b.WriteString("---\n") } else { v.b.WriteByte('\n') } ast.Walk(v, &zn.Ast) length, err := v.b.Flush() return length, err } // WriteMeta encodes meta data as markdown. func (*Encoder) WriteMeta(w io.Writer, m *meta.Meta, evalMeta encoder.EvalMetaFunc) (int, error) { v := newVisitor(w) v.acceptMeta(m, evalMeta) length, err := v.b.Flush() return length, err } func (v *visitor) acceptMeta(m *meta.Meta, evalMeta encoder.EvalMetaFunc) { for _, p := range m.ComputedPairs() { key := p.Key v.b.WriteStrings(key, ": ") if meta.Type(key) == meta.TypeZettelmarkup { is := evalMeta(p.Value) ast.Walk(v, &is) } else { v.b.WriteString(p.Value) } v.b.WriteByte('\n') } } func (ze *Encoder) WriteContent(w io.Writer, zn *ast.ZettelNode) (int, error) { return ze.WriteBlocks(w, &zn.Ast) } // WriteBlocks writes the content of a block slice to the writer. func (*Encoder) WriteBlocks(w io.Writer, bs *ast.BlockSlice) (int, error) { v := newVisitor(w) ast.Walk(v, bs) length, err := v.b.Flush() return length, err } // WriteInlines writes an inline slice to the writer func (*Encoder) WriteInlines(w io.Writer, is *ast.InlineSlice) (int, error) { v := newVisitor(w) ast.Walk(v, is) length, err := v.b.Flush() return length, err } // visitor writes the abstract syntax tree to an EncWriter. type visitor struct { b encoder.EncWriter listInfo []int listPrefix string } func newVisitor(w io.Writer) *visitor { return &visitor{b: encoder.NewEncWriter(w)} } func (v *visitor) Visit(node ast.Node) ast.Visitor { switch n := node.(type) { case *ast.BlockSlice: v.visitBlockSlice(n) case *ast.VerbatimNode: v.visitVerbatim(n) case *ast.RegionNode: v.visitRegion(n) case *ast.HeadingNode: v.visitHeading(n) case *ast.HRuleNode: v.b.WriteString("---") case *ast.NestedListNode: v.visitNestedList(n) case *ast.DescriptionListNode: return nil // Should write no content case *ast.TableNode: return nil // Should write no content case *ast.TextNode: v.visitText(n) case *ast.SpaceNode: v.b.WriteString(n.Lexeme) case *ast.BreakNode: v.visitBreak(n) case *ast.LinkNode: v.visitLink(n) case *ast.EmbedRefNode: v.visitEmbedRef(n) case *ast.FootnoteNode: return nil // Should write no content case *ast.FormatNode: v.visitFormat(n) case *ast.LiteralNode: v.visitLiteral(n) default: return v } return nil } func (v *visitor) visitBlockSlice(bs *ast.BlockSlice) { for i, bn := range *bs { if i > 0 { v.b.WriteString("\n\n") } ast.Walk(v, bn) } } func (v *visitor) visitVerbatim(vn *ast.VerbatimNode) { lc := len(vn.Content) if vn.Kind != ast.VerbatimProg || lc == 0 { return } v.writeSpaces(4) lcm1 := lc - 1 for i := 0; i < lc; i++ { b := vn.Content[i] if b != '\n' && b != '\r' { v.b.WriteByte(b) continue } j := i + 1 for ; j < lc; j++ { c := vn.Content[j] if c != '\n' && c != '\r' { break } } if j >= lcm1 { break } v.b.WriteByte('\n') v.writeSpaces(4) i = j - 1 } } func (v *visitor) visitRegion(rn *ast.RegionNode) { if rn.Kind != ast.RegionQuote { return } first := true for _, bn := range rn.Blocks { pn, ok := bn.(*ast.ParaNode) if !ok { continue } if !first { v.b.WriteString("\n\n") } first = false v.b.WriteString("> ") ast.Walk(v, &pn.Inlines) } } func (v *visitor) visitHeading(hn *ast.HeadingNode) { const headingSigns = "###### " v.b.WriteString(headingSigns[len(headingSigns)-hn.Level-1:]) ast.Walk(v, &hn.Inlines) } func (v *visitor) visitNestedList(ln *ast.NestedListNode) { switch ln.Kind { case ast.NestedListOrdered: v.writeNestedList(ln, "1. ") case ast.NestedListUnordered: v.writeNestedList(ln, "* ") case ast.NestedListQuote: v.writeListQuote(ln) } v.listInfo = v.listInfo[:len(v.listInfo)-1] } func (v *visitor) writeNestedList(ln *ast.NestedListNode, enum string) { v.listInfo = append(v.listInfo, len(enum)) regIndent := 4*len(v.listInfo) - 4 paraIndent := regIndent + len(enum) for i, item := range ln.Items { if i > 0 { v.b.WriteByte('\n') } v.writeSpaces(regIndent) v.b.WriteString(enum) for j, in := range item { if j > 0 { v.b.WriteByte('\n') if _, ok := in.(*ast.ParaNode); ok { v.writeSpaces(paraIndent) } } ast.Walk(v, in) } } } func (v *visitor) writeListQuote(ln *ast.NestedListNode) { v.listInfo = append(v.listInfo, 0) if len(v.listInfo) > 1 { return } prefix := v.listPrefix v.listPrefix = "> " for i, item := range ln.Items { if i > 0 { v.b.WriteByte('\n') } v.b.WriteString(v.listPrefix) for j, in := range item { if j > 0 { v.b.WriteByte('\n') if _, ok := in.(*ast.ParaNode); ok { v.b.WriteString(v.listPrefix) } } ast.Walk(v, in) } } v.listPrefix = prefix } func (v *visitor) visitText(tn *ast.TextNode) { v.b.WriteString(tn.Text) } func (v *visitor) visitBreak(bn *ast.BreakNode) { if bn.Hard { v.b.WriteString("\\\n") } else { v.b.WriteByte('\n') } if l := len(v.listInfo); l > 0 { if v.listPrefix == "" { v.writeSpaces(4*l - 4 + v.listInfo[l-1]) } else { v.writeSpaces(4*l - 4) v.b.WriteString(v.listPrefix) } } } func (v *visitor) visitLink(ln *ast.LinkNode) { v.writeReference(ln.Ref, ln.Inlines) } func (v *visitor) visitEmbedRef(en *ast.EmbedRefNode) { v.b.WriteByte('!') v.writeReference(en.Ref, en.Inlines) } func (v *visitor) writeReference(ref *ast.Reference, is ast.InlineSlice) { if ref.State == ast.RefStateQuery { ast.Walk(v, &is) } else if len(is) > 0 { v.b.WriteByte('[') ast.Walk(v, &is) v.b.WriteStrings("](", ref.String()) v.b.WriteByte(')') } else if isAutoLinkable(ref) { v.b.WriteByte('<') v.b.WriteString(ref.String()) v.b.WriteByte('>') } else { s := ref.String() v.b.WriteStrings("[", s, "](", s, ")") } } func isAutoLinkable(ref *ast.Reference) bool { if ref.State != ast.RefStateExternal || ref.URL == nil { return false } return ref.URL.Scheme != "" } func (v *visitor) visitFormat(fn *ast.FormatNode) { switch fn.Kind { case ast.FormatEmph: v.b.WriteByte('*') ast.Walk(v, &fn.Inlines) v.b.WriteByte('*') case ast.FormatStrong: v.b.WriteString("__") ast.Walk(v, &fn.Inlines) v.b.WriteString("__") case ast.FormatQuote: v.b.WriteString("<q>") ast.Walk(v, &fn.Inlines) v.b.WriteString("</q>") default: ast.Walk(v, &fn.Inlines) } } func (v *visitor) visitLiteral(ln *ast.LiteralNode) { switch ln.Kind { case ast.LiteralProg: v.b.WriteByte('`') v.b.Write(ln.Content) v.b.WriteByte('`') case ast.LiteralComment, ast.LiteralHTML: // ignore everything default: v.b.Write(ln.Content) } } func (v *visitor) writeSpaces(n int) { for i := 0; i < n; i++ { v.b.WriteByte(' ') } } |
Added encoder/sexprenc/sexprenc.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 | //----------------------------------------------------------------------------- // Copyright (c) 2022-present Detlef Stern // // This file is part of Zettelstore. // // Zettelstore is licensed under the latest version of the EUPL (European Union // Public License). Please see file LICENSE.txt for your rights and obligations // under this license. //----------------------------------------------------------------------------- // Package sexprenc encodes the abstract syntax tree into a s-expr. package sexprenc import ( "io" "codeberg.org/t73fde/sxpf" "zettelstore.de/c/api" "zettelstore.de/z/ast" "zettelstore.de/z/domain/meta" "zettelstore.de/z/encoder" ) func init() { encoder.Register(api.EncoderSexpr, func() encoder.Encoder { return Create() }) } // Create a S-expr encoder func Create() *Encoder { return &mySE } type Encoder struct{} var mySE Encoder // WriteZettel writes the encoded zettel to the writer. func (*Encoder) WriteZettel(w io.Writer, zn *ast.ZettelNode, evalMeta encoder.EvalMetaFunc) (int, error) { t := NewTransformer() content := t.GetSexpr(&zn.Ast) meta := t.GetMeta(zn.InhMeta, evalMeta) return io.WriteString(w, sxpf.Nil().Cons(content).Cons(meta).Repr()) } // WriteMeta encodes meta data as s-expression. func (*Encoder) WriteMeta(w io.Writer, m *meta.Meta, evalMeta encoder.EvalMetaFunc) (int, error) { t := NewTransformer() return io.WriteString(w, t.GetMeta(m, evalMeta).Repr()) } func (se *Encoder) WriteContent(w io.Writer, zn *ast.ZettelNode) (int, error) { return se.WriteBlocks(w, &zn.Ast) } // WriteBlocks writes a block slice to the writer func (*Encoder) WriteBlocks(w io.Writer, bs *ast.BlockSlice) (int, error) { t := NewTransformer() return io.WriteString(w, t.GetSexpr(bs).Repr()) } // WriteInlines writes an inline slice to the writer func (*Encoder) WriteInlines(w io.Writer, is *ast.InlineSlice) (int, error) { t := NewTransformer() return io.WriteString(w, t.GetSexpr(is).Repr()) } |
Added encoder/sexprenc/transform.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 | //----------------------------------------------------------------------------- // Copyright (c) 2022-present Detlef Stern // // This file is part of Zettelstore. // // Zettelstore is licensed under the latest version of the EUPL (European Union // Public License). Please see file LICENSE.txt for your rights and obligations // under this license. //----------------------------------------------------------------------------- package sexprenc import ( "encoding/base64" "fmt" "log" "strings" "codeberg.org/t73fde/sxpf" "zettelstore.de/c/attrs" "zettelstore.de/c/sexpr" "zettelstore.de/z/ast" "zettelstore.de/z/domain/meta" "zettelstore.de/z/encoder" ) // NewTransformer returns a new transformer to create s-expressions from AST nodes. func NewTransformer() *Transformer { sf := sxpf.MakeMappedFactory() t := Transformer{sf: sf} t.zetSyms.InitializeZettelSymbols(sf) t.mapVerbatimKindS = map[ast.VerbatimKind]*sxpf.Symbol{ ast.VerbatimZettel: t.zetSyms.SymVerbatimZettel, ast.VerbatimProg: t.zetSyms.SymVerbatimProg, ast.VerbatimEval: t.zetSyms.SymVerbatimEval, ast.VerbatimMath: t.zetSyms.SymVerbatimMath, ast.VerbatimComment: t.zetSyms.SymVerbatimComment, ast.VerbatimHTML: t.zetSyms.SymVerbatimHTML, } t.mapRegionKindS = map[ast.RegionKind]*sxpf.Symbol{ ast.RegionSpan: t.zetSyms.SymRegionBlock, ast.RegionQuote: t.zetSyms.SymRegionQuote, ast.RegionVerse: t.zetSyms.SymRegionVerse, } t.mapNestedListKindS = map[ast.NestedListKind]*sxpf.Symbol{ ast.NestedListOrdered: t.zetSyms.SymListOrdered, ast.NestedListUnordered: t.zetSyms.SymListUnordered, ast.NestedListQuote: t.zetSyms.SymListQuote, } t.alignmentSymbolS = map[ast.Alignment]*sxpf.Symbol{ ast.AlignDefault: t.zetSyms.SymCell, ast.AlignLeft: t.zetSyms.SymCellLeft, ast.AlignCenter: t.zetSyms.SymCellCenter, ast.AlignRight: t.zetSyms.SymCellRight, } t.mapRefStateLink = map[ast.RefState]*sxpf.Symbol{ ast.RefStateInvalid: t.zetSyms.SymLinkInvalid, ast.RefStateZettel: t.zetSyms.SymLinkZettel, ast.RefStateSelf: t.zetSyms.SymLinkSelf, ast.RefStateFound: t.zetSyms.SymLinkFound, ast.RefStateBroken: t.zetSyms.SymLinkBroken, ast.RefStateHosted: t.zetSyms.SymLinkHosted, ast.RefStateBased: t.zetSyms.SymLinkBased, ast.RefStateQuery: t.zetSyms.SymLinkQuery, ast.RefStateExternal: t.zetSyms.SymLinkExternal, } t.mapFormatKindS = map[ast.FormatKind]*sxpf.Symbol{ ast.FormatEmph: t.zetSyms.SymFormatEmph, ast.FormatStrong: t.zetSyms.SymFormatStrong, ast.FormatDelete: t.zetSyms.SymFormatDelete, ast.FormatInsert: t.zetSyms.SymFormatInsert, ast.FormatSuper: t.zetSyms.SymFormatSuper, ast.FormatSub: t.zetSyms.SymFormatSub, ast.FormatQuote: t.zetSyms.SymFormatQuote, ast.FormatSpan: t.zetSyms.SymFormatSpan, } t.mapLiteralKindS = map[ast.LiteralKind]*sxpf.Symbol{ ast.LiteralZettel: t.zetSyms.SymLiteralZettel, ast.LiteralProg: t.zetSyms.SymLiteralProg, ast.LiteralInput: t.zetSyms.SymLiteralInput, ast.LiteralOutput: t.zetSyms.SymLiteralOutput, ast.LiteralComment: t.zetSyms.SymLiteralComment, ast.LiteralHTML: t.zetSyms.SymLiteralHTML, ast.LiteralMath: t.zetSyms.SymLiteralMath, } t.mapRefStateS = map[ast.RefState]*sxpf.Symbol{ ast.RefStateInvalid: t.zetSyms.SymRefStateInvalid, ast.RefStateZettel: t.zetSyms.SymRefStateZettel, ast.RefStateSelf: t.zetSyms.SymRefStateSelf, ast.RefStateFound: t.zetSyms.SymRefStateFound, ast.RefStateBroken: t.zetSyms.SymRefStateBroken, ast.RefStateHosted: t.zetSyms.SymRefStateHosted, ast.RefStateBased: t.zetSyms.SymRefStateBased, ast.RefStateQuery: t.zetSyms.SymRefStateQuery, ast.RefStateExternal: t.zetSyms.SymRefStateExternal, } t.mapMetaTypeS = map[*meta.DescriptionType]*sxpf.Symbol{ meta.TypeCredential: t.zetSyms.SymTypeCredential, meta.TypeEmpty: t.zetSyms.SymTypeEmpty, meta.TypeID: t.zetSyms.SymTypeID, meta.TypeIDSet: t.zetSyms.SymTypeIDSet, meta.TypeNumber: t.zetSyms.SymTypeNumber, meta.TypeString: t.zetSyms.SymTypeString, meta.TypeTagSet: t.zetSyms.SymTypeTagSet, meta.TypeTimestamp: t.zetSyms.SymTypeTimestamp, meta.TypeURL: t.zetSyms.SymTypeURL, meta.TypeWord: t.zetSyms.SymTypeWord, meta.TypeWordSet: t.zetSyms.SymTypeWordSet, meta.TypeZettelmarkup: t.zetSyms.SymTypeZettelmarkup, } return &t } type Transformer struct { sf sxpf.SymbolFactory zetSyms sexpr.ZettelSymbols mapVerbatimKindS map[ast.VerbatimKind]*sxpf.Symbol mapRegionKindS map[ast.RegionKind]*sxpf.Symbol mapNestedListKindS map[ast.NestedListKind]*sxpf.Symbol alignmentSymbolS map[ast.Alignment]*sxpf.Symbol mapRefStateLink map[ast.RefState]*sxpf.Symbol mapFormatKindS map[ast.FormatKind]*sxpf.Symbol mapLiteralKindS map[ast.LiteralKind]*sxpf.Symbol mapRefStateS map[ast.RefState]*sxpf.Symbol mapMetaTypeS map[*meta.DescriptionType]*sxpf.Symbol inVerse bool } func (t *Transformer) GetSexpr(node ast.Node) *sxpf.List { switch n := node.(type) { case *ast.BlockSlice: return t.getBlockSlice(n) case *ast.InlineSlice: return t.getInlineSlice(*n) case *ast.ParaNode: return t.getInlineSlice(n.Inlines).Tail().Cons(t.zetSyms.SymPara) case *ast.VerbatimNode: return sxpf.MakeList( mapGetS(t, t.mapVerbatimKindS, n.Kind), t.getAttributes(n.Attrs), sxpf.MakeString(string(n.Content)), ) case *ast.RegionNode: return t.getRegion(n) case *ast.HeadingNode: return sxpf.MakeList( t.zetSyms.SymHeading, sxpf.MakeInteger64(int64(n.Level)), t.getAttributes(n.Attrs), sxpf.MakeString(n.Slug), sxpf.MakeString(n.Fragment), t.getInlineSlice(n.Inlines), ) case *ast.HRuleNode: return sxpf.MakeList(t.zetSyms.SymThematic, t.getAttributes(n.Attrs)) case *ast.NestedListNode: return t.getNestedList(n) case *ast.DescriptionListNode: return t.getDescriptionList(n) case *ast.TableNode: return t.getTable(n) case *ast.TranscludeNode: return sxpf.MakeList(t.zetSyms.SymTransclude, t.getAttributes(n.Attrs), t.getReference(n.Ref)) case *ast.BLOBNode: return t.getBLOB(n) case *ast.TextNode: return sxpf.MakeList(t.zetSyms.SymText, sxpf.MakeString(n.Text)) case *ast.SpaceNode: if t.inVerse { return sxpf.MakeList(t.zetSyms.SymSpace, sxpf.MakeString(n.Lexeme)) } return sxpf.MakeList(t.zetSyms.SymSpace) case *ast.BreakNode: if n.Hard { return sxpf.MakeList(t.zetSyms.SymHard) } return sxpf.MakeList(t.zetSyms.SymSoft) case *ast.LinkNode: return t.getLink(n) case *ast.EmbedRefNode: return t.getInlineSlice(n.Inlines).Tail(). Cons(sxpf.MakeString(n.Syntax)). Cons(t.getReference(n.Ref)). Cons(t.getAttributes(n.Attrs)). Cons(t.zetSyms.SymEmbed) case *ast.EmbedBLOBNode: return t.getEmbedBLOB(n) case *ast.CiteNode: return t.getInlineSlice(n.Inlines).Tail(). Cons(sxpf.MakeString(n.Key)). Cons(t.getAttributes(n.Attrs)). Cons(t.zetSyms.SymCite) case *ast.FootnoteNode: text := sxpf.Nil().Cons(sxpf.Nil().Cons(t.getInlineSlice(n.Inlines)).Cons(t.zetSyms.SymQuote)) return text.Cons(t.getAttributes(n.Attrs)).Cons(t.zetSyms.SymEndnote) case *ast.MarkNode: return t.getInlineSlice(n.Inlines).Tail(). Cons(sxpf.MakeString(n.Fragment)). Cons(sxpf.MakeString(n.Slug)). Cons(sxpf.MakeString(n.Mark)). Cons(t.zetSyms.SymMark) case *ast.FormatNode: return t.getInlineSlice(n.Inlines).Tail(). Cons(t.getAttributes(n.Attrs)). Cons(mapGetS(t, t.mapFormatKindS, n.Kind)) case *ast.LiteralNode: return sxpf.MakeList( mapGetS(t, t.mapLiteralKindS, n.Kind), t.getAttributes(n.Attrs), sxpf.MakeString(string(n.Content)), ) } log.Printf("SEXPR %T %v\n", node, node) return sxpf.MakeList(t.zetSyms.SymUnknown, sxpf.MakeString(fmt.Sprintf("%T %v", node, node))) } func (t *Transformer) getRegion(rn *ast.RegionNode) *sxpf.List { saveInVerse := t.inVerse if rn.Kind == ast.RegionVerse { t.inVerse = true } symBlocks := t.GetSexpr(&rn.Blocks) t.inVerse = saveInVerse return sxpf.MakeList( mapGetS(t, t.mapRegionKindS, rn.Kind), t.getAttributes(rn.Attrs), symBlocks, t.GetSexpr(&rn.Inlines), ) } func (t *Transformer) getNestedList(ln *ast.NestedListNode) *sxpf.List { nlistObjs := make([]sxpf.Object, len(ln.Items)+1) nlistObjs[0] = mapGetS(t, t.mapNestedListKindS, ln.Kind) isCompact := isCompactList(ln.Items) for i, item := range ln.Items { if isCompact && len(item) > 0 { paragraph := t.GetSexpr(item[0]) nlistObjs[i+1] = paragraph.Tail().Cons(t.zetSyms.SymInline) continue } itemObjs := make([]sxpf.Object, len(item)) for j, in := range item { itemObjs[j] = t.GetSexpr(in) } if isCompact { nlistObjs[i+1] = sxpf.MakeList(itemObjs...).Cons(t.zetSyms.SymInline) } else { nlistObjs[i+1] = sxpf.MakeList(itemObjs...).Cons(t.zetSyms.SymBlock) } } return sxpf.MakeList(nlistObjs...) } func isCompactList(itemSlice []ast.ItemSlice) bool { for _, items := range itemSlice { if len(items) > 1 { return false } if len(items) == 1 { if _, ok := items[0].(*ast.ParaNode); !ok { return false } } } return true } func (t *Transformer) getDescriptionList(dn *ast.DescriptionListNode) *sxpf.List { dlObjs := make([]sxpf.Object, 2*len(dn.Descriptions)+1) dlObjs[0] = t.zetSyms.SymDescription for i, def := range dn.Descriptions { dlObjs[2*i+1] = t.getInlineSlice(def.Term) descObjs := make([]sxpf.Object, len(def.Descriptions)) for j, b := range def.Descriptions { dVal := make([]sxpf.Object, len(b)) for k, dn := range b { dVal[k] = t.GetSexpr(dn) } descObjs[j] = sxpf.MakeList(dVal...).Cons(t.zetSyms.SymBlock) } dlObjs[2*i+2] = sxpf.MakeList(descObjs...).Cons(t.zetSyms.SymBlock) } return sxpf.MakeList(dlObjs...) } func (t *Transformer) getTable(tn *ast.TableNode) *sxpf.List { tObjs := make([]sxpf.Object, len(tn.Rows)+2) tObjs[0] = t.zetSyms.SymTable tObjs[1] = t.getHeader(tn.Header) for i, row := range tn.Rows { tObjs[i+2] = t.getRow(row) } return sxpf.MakeList(tObjs...) } func (t *Transformer) getHeader(header ast.TableRow) *sxpf.List { if len(header) == 0 { return sxpf.Nil() } return t.getRow(header) } func (t *Transformer) getRow(row ast.TableRow) *sxpf.List { rObjs := make([]sxpf.Object, len(row)) for i, cell := range row { rObjs[i] = t.getCell(cell) } return sxpf.MakeList(rObjs...).Cons(t.zetSyms.SymList) } func (t *Transformer) getCell(cell *ast.TableCell) *sxpf.List { return t.getInlineSlice(cell.Inlines).Tail().Cons(mapGetS(t, t.alignmentSymbolS, cell.Align)) } func (t *Transformer) getBLOB(bn *ast.BLOBNode) *sxpf.List { var lastObj sxpf.Object if bn.Syntax == meta.SyntaxSVG { lastObj = sxpf.MakeString(string(bn.Blob)) } else { lastObj = getBase64String(bn.Blob) } return sxpf.MakeList( t.zetSyms.SymBLOB, t.getInlineSlice(bn.Description), sxpf.MakeString(bn.Syntax), lastObj, ) } func (t *Transformer) getLink(ln *ast.LinkNode) *sxpf.List { return t.getInlineSlice(ln.Inlines).Tail(). Cons(sxpf.MakeString(ln.Ref.Value)). Cons(t.getAttributes(ln.Attrs)). Cons(mapGetS(t, t.mapRefStateLink, ln.Ref.State)) } func (t *Transformer) getEmbedBLOB(en *ast.EmbedBLOBNode) *sxpf.List { tail := t.getInlineSlice(en.Inlines).Tail() if en.Syntax == meta.SyntaxSVG { tail = tail.Cons(sxpf.MakeString(string(en.Blob))) } else { tail = tail.Cons(getBase64String(en.Blob)) } return tail.Cons(sxpf.MakeString(en.Syntax)).Cons(t.getAttributes(en.Attrs)).Cons(t.zetSyms.SymEmbedBLOB) } func (t *Transformer) getBlockSlice(bs *ast.BlockSlice) *sxpf.List { objs := make([]sxpf.Object, len(*bs)) for i, n := range *bs { objs[i] = t.GetSexpr(n) } return sxpf.MakeList(objs...).Cons(t.zetSyms.SymBlock) } func (t *Transformer) getInlineSlice(is ast.InlineSlice) *sxpf.List { objs := make([]sxpf.Object, len(is)) for i, n := range is { objs[i] = t.GetSexpr(n) } return sxpf.MakeList(objs...).Cons(t.zetSyms.SymInline) } func (t *Transformer) getAttributes(a attrs.Attributes) sxpf.Object { if a.IsEmpty() { return sxpf.Nil() } keys := a.Keys() objs := make([]sxpf.Object, 0, len(keys)) for _, k := range keys { objs = append(objs, sxpf.Cons(sxpf.MakeString(k), sxpf.MakeString(a[k]))) } return sxpf.Nil().Cons(sxpf.MakeList(objs...)).Cons(t.zetSyms.SymQuote) } func (t *Transformer) getReference(ref *ast.Reference) *sxpf.List { return sxpf.MakeList( t.zetSyms.SymQuote, sxpf.MakeList( mapGetS(t, t.mapRefStateS, ref.State), sxpf.MakeString(ref.Value), ), ) } func (t *Transformer) GetMeta(m *meta.Meta, evalMeta encoder.EvalMetaFunc) *sxpf.List { pairs := m.ComputedPairs() objs := make([]sxpf.Object, 0, len(pairs)) for _, p := range pairs { key := p.Key ty := m.Type(key) symType := mapGetS(t, t.mapMetaTypeS, ty) strKey := sxpf.MakeString(key) var obj sxpf.Object if ty.IsSet { setList := meta.ListFromValue(p.Value) setObjs := make([]sxpf.Object, len(setList)) for i, val := range setList { setObjs[i] = sxpf.MakeString(val) } obj = sxpf.MakeList(setObjs...).Cons(t.zetSyms.SymList) } else if ty == meta.TypeZettelmarkup { is := evalMeta(p.Value) obj = t.GetSexpr(&is) } else { obj = sxpf.MakeString(p.Value) } objs = append(objs, sxpf.Nil().Cons(obj).Cons(strKey).Cons(symType)) } return sxpf.MakeList(objs...).Cons(t.zetSyms.SymMeta) } func mapGetS[T comparable](t *Transformer, m map[T]*sxpf.Symbol, k T) *sxpf.Symbol { if result, found := m[k]; found { return result } log.Println("MISS", k, m) return t.sf.MustMake(fmt.Sprintf("**%v:NOT-FOUND**", k)) } func getBase64String(data []byte) sxpf.String { var sb strings.Builder encoder := base64.NewEncoder(base64.StdEncoding, &sb) _, err := encoder.Write(data) if err == nil { err = encoder.Close() } if err == nil { return sxpf.MakeString(sb.String()) } return sxpf.MakeString("") } |
Added encoder/shtmlenc/shtmlenc.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 | //----------------------------------------------------------------------------- // Copyright (c) 2023-present Detlef Stern // // This file is part of Zettelstore. // // Zettelstore is licensed under the latest version of the EUPL (European Union // Public License). Please see file LICENSE.txt for your rights and obligations // under this license. //----------------------------------------------------------------------------- // Package shtmlenc encodes the abstract syntax tree into a s-expr which represents HTML. package shtmlenc import ( "io" "codeberg.org/t73fde/sxpf" "zettelstore.de/c/api" "zettelstore.de/c/shtml" "zettelstore.de/z/ast" "zettelstore.de/z/domain/meta" "zettelstore.de/z/encoder" "zettelstore.de/z/encoder/sexprenc" ) func init() { encoder.Register(api.EncoderSHTML, func() encoder.Encoder { return Create() }) } // Create a SHTML encoder func Create() *Encoder { return &mySE } type Encoder struct{} var mySE Encoder // WriteZettel writes the encoded zettel to the writer. func (*Encoder) WriteZettel(w io.Writer, zn *ast.ZettelNode, evalMeta encoder.EvalMetaFunc) (int, error) { tx := sexprenc.NewTransformer() th := shtml.NewTransformer(1, nil) metaSHTML, err := th.Transform(tx.GetMeta(zn.InhMeta, evalMeta)) if err != nil { return 0, err } contentSHTML, err := th.Transform(tx.GetSexpr(&zn.Ast)) if err != nil { return 0, err } result := sxpf.Cons(metaSHTML, contentSHTML) return result.Print(w) } // WriteMeta encodes meta data as s-expression. func (*Encoder) WriteMeta(w io.Writer, m *meta.Meta, evalMeta encoder.EvalMetaFunc) (int, error) { tx := sexprenc.NewTransformer() th := shtml.NewTransformer(1, nil) metaSHTML, err := th.Transform(tx.GetMeta(m, evalMeta)) if err != nil { return 0, err } return metaSHTML.Print(w) } func (se *Encoder) WriteContent(w io.Writer, zn *ast.ZettelNode) (int, error) { return se.WriteBlocks(w, &zn.Ast) } // WriteBlocks writes a block slice to the writer func (*Encoder) WriteBlocks(w io.Writer, bs *ast.BlockSlice) (int, error) { hval, err := TransformSlice(bs) if err != nil { return 0, err } return hval.Print(w) } // WriteInlines writes an inline slice to the writer func (*Encoder) WriteInlines(w io.Writer, is *ast.InlineSlice) (int, error) { hval, err := TransformSlice(is) if err != nil { return 0, err } return hval.Print(w) } // TransformSlice transforms a AST slice into SHTML. func TransformSlice(node ast.Node) (*sxpf.List, error) { tx := sexprenc.NewTransformer() xval := tx.GetSexpr(node) th := shtml.NewTransformer(1, nil) return th.Transform(xval) } |
Added encoder/textenc/textenc.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 | //----------------------------------------------------------------------------- // Copyright (c) 2020-present Detlef Stern // // This file is part of Zettelstore. // // Zettelstore is licensed under the latest version of the EUPL (European Union // Public License). Please see file LICENSE.txt for your rights and obligations // under this license. //----------------------------------------------------------------------------- // Package textenc encodes the abstract syntax tree into its text. package textenc import ( "io" "zettelstore.de/c/api" "zettelstore.de/z/ast" "zettelstore.de/z/domain/meta" "zettelstore.de/z/encoder" ) func init() { encoder.Register(api.EncoderText, func() encoder.Encoder { return Create() }) } // Create an encoder. func Create() *Encoder { return &myTE } type Encoder struct{} var myTE Encoder // Only a singleton is required. // WriteZettel writes metadata and content. func (te *Encoder) WriteZettel(w io.Writer, zn *ast.ZettelNode, evalMeta encoder.EvalMetaFunc) (int, error) { v := newVisitor(w) te.WriteMeta(&v.b, zn.InhMeta, evalMeta) v.visitBlockSlice(&zn.Ast) length, err := v.b.Flush() return length, err } // WriteMeta encodes metadata as text. func (te *Encoder) WriteMeta(w io.Writer, m *meta.Meta, evalMeta encoder.EvalMetaFunc) (int, error) { buf := encoder.NewEncWriter(w) for _, pair := range m.ComputedPairs() { switch meta.Type(pair.Key) { case meta.TypeTagSet: writeTagSet(&buf, meta.ListFromValue(pair.Value)) case meta.TypeZettelmarkup: is := evalMeta(pair.Value) te.WriteInlines(&buf, &is) default: buf.WriteString(pair.Value) } buf.WriteByte('\n') } length, err := buf.Flush() return length, err } func writeTagSet(buf *encoder.EncWriter, tags []string) { for i, tag := range tags { if i > 0 { buf.WriteByte(' ') } buf.WriteString(meta.CleanTag(tag)) } } func (te *Encoder) WriteContent(w io.Writer, zn *ast.ZettelNode) (int, error) { return te.WriteBlocks(w, &zn.Ast) } // WriteBlocks writes the content of a block slice to the writer. func (*Encoder) WriteBlocks(w io.Writer, bs *ast.BlockSlice) (int, error) { v := newVisitor(w) v.visitBlockSlice(bs) length, err := v.b.Flush() return length, err } // WriteInlines writes an inline slice to the writer func (*Encoder) WriteInlines(w io.Writer, is *ast.InlineSlice) (int, error) { v := newVisitor(w) ast.Walk(v, is) length, err := v.b.Flush() return length, err } // visitor writes the abstract syntax tree to an io.Writer. type visitor struct { b encoder.EncWriter inlinePos int } func newVisitor(w io.Writer) *visitor { return &visitor{b: encoder.NewEncWriter(w)} } func (v *visitor) Visit(node ast.Node) ast.Visitor { switch n := node.(type) { case *ast.BlockSlice: v.visitBlockSlice(n) return nil case *ast.InlineSlice: v.visitInlineSlice(n) return nil case *ast.VerbatimNode: v.visitVerbatim(n) return nil case *ast.RegionNode: v.visitBlockSlice(&n.Blocks) if len(n.Inlines) > 0 { v.b.WriteByte('\n') ast.Walk(v, &n.Inlines) } return nil case *ast.NestedListNode: v.visitNestedList(n) return nil case *ast.DescriptionListNode: v.visitDescriptionList(n) return nil case *ast.TableNode: v.visitTable(n) return nil case *ast.TranscludeNode, *ast.BLOBNode: return nil case *ast.TextNode: v.b.WriteString(n.Text) return nil case *ast.SpaceNode: v.b.WriteByte(' ') return nil case *ast.BreakNode: if n.Hard { v.b.WriteByte('\n') } else { v.b.WriteByte(' ') } return nil case *ast.LinkNode: if len(n.Inlines) > 0 { ast.Walk(v, &n.Inlines) } return nil case *ast.MarkNode: if len(n.Inlines) > 0 { ast.Walk(v, &n.Inlines) } return nil case *ast.FootnoteNode: if v.inlinePos > 0 { v.b.WriteByte(' ') } // No 'return nil' to write text case *ast.LiteralNode: if n.Kind != ast.LiteralComment { v.b.Write(n.Content) } } return v } func (v *visitor) visitVerbatim(vn *ast.VerbatimNode) { if vn.Kind == ast.VerbatimComment { return } v.b.Write(vn.Content) } func (v *visitor) visitNestedList(ln *ast.NestedListNode) { for i, item := range ln.Items { v.writePosChar(i, '\n') for j, it := range item { v.writePosChar(j, '\n') ast.Walk(v, it) } } } func (v *visitor) visitDescriptionList(dl *ast.DescriptionListNode) { for i, descr := range dl.Descriptions { v.writePosChar(i, '\n') ast.Walk(v, &descr.Term) for _, b := range descr.Descriptions { v.b.WriteByte('\n') for k, d := range b { v.writePosChar(k, '\n') ast.Walk(v, d) } } } } func (v *visitor) visitTable(tn *ast.TableNode) { if len(tn.Header) > 0 { v.writeRow(tn.Header) v.b.WriteByte('\n') } for i, row := range tn.Rows { v.writePosChar(i, '\n') v.writeRow(row) } } func (v *visitor) writeRow(row ast.TableRow) { for i, cell := range row { v.writePosChar(i, ' ') ast.Walk(v, &cell.Inlines) } } func (v *visitor) visitBlockSlice(bs *ast.BlockSlice) { for i, bn := range *bs { v.writePosChar(i, '\n') ast.Walk(v, bn) } } func (v *visitor) visitInlineSlice(is *ast.InlineSlice) { for i, in := range *is { v.inlinePos = i ast.Walk(v, in) } v.inlinePos = 0 } func (v *visitor) writePosChar(pos int, ch byte) { if pos > 0 { v.b.WriteByte(ch) } } |
Added encoder/write.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 | //----------------------------------------------------------------------------- // Copyright (c) 2020-present Detlef Stern // // This file is part of Zettelstore. // // Zettelstore is licensed under the latest version of the EUPL (European Union // Public License). Please see file LICENSE.txt for your rights and obligations // under this license. //----------------------------------------------------------------------------- package encoder import ( "encoding/base64" "io" ) // EncWriter is a specialized writer for encoding zettel. type EncWriter struct { w io.Writer // The io.Writer to write to err error // Collect error length int // Collected length } // NewEncWriter creates a new EncWriter func NewEncWriter(w io.Writer) EncWriter { return EncWriter{w: w} } // Write writes the content of p. func (w *EncWriter) Write(p []byte) (l int, err error) { if w.err != nil { return 0, w.err } l, w.err = w.w.Write(p) w.length += l return l, w.err } // WriteString writes the content of s. func (w *EncWriter) WriteString(s string) { if w.err != nil { return } var l int l, w.err = io.WriteString(w.w, s) w.length += l } // WriteStrings writes the content of sl. func (w *EncWriter) WriteStrings(sl ...string) { for _, s := range sl { w.WriteString(s) } } // WriteByte writes the content of b. func (w *EncWriter) WriteByte(b byte) error { var l int l, w.err = w.Write([]byte{b}) w.length += l return w.err } // WriteBytes writes the content of bs. func (w *EncWriter) WriteBytes(bs ...byte) { w.Write(bs) } // WriteBase64 writes the content of p, encoded with base64. func (w *EncWriter) WriteBase64(p []byte) { if w.err == nil { encoder := base64.NewEncoder(base64.StdEncoding, w.w) var l int l, w.err = encoder.Write(p) w.length += l err1 := encoder.Close() if w.err == nil { w.err = err1 } } } // Flush returns the collected length and error. func (w *EncWriter) Flush() (int, error) { return w.length, w.err } |
Added encoder/zmkenc/zmkenc.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 | //----------------------------------------------------------------------------- // Copyright (c) 2020-present Detlef Stern // // This file is part of Zettelstore. // // Zettelstore is licensed under the latest version of the EUPL (European Union // Public License). Please see file LICENSE.txt for your rights and obligations // under this license. //----------------------------------------------------------------------------- // Package zmkenc encodes the abstract syntax tree back into Zettelmarkup. package zmkenc import ( "fmt" "io" "strings" "zettelstore.de/c/api" "zettelstore.de/c/attrs" "zettelstore.de/z/ast" "zettelstore.de/z/domain/meta" "zettelstore.de/z/encoder" "zettelstore.de/z/encoder/textenc" "zettelstore.de/z/strfun" ) func init() { encoder.Register(api.EncoderZmk, func() encoder.Encoder { return Create() }) } // Create an encoder. func Create() *Encoder { return &myZE } type Encoder struct{} var myZE Encoder // WriteZettel writes the encoded zettel to the writer. func (*Encoder) WriteZettel(w io.Writer, zn *ast.ZettelNode, evalMeta encoder.EvalMetaFunc) (int, error) { v := newVisitor(w) v.acceptMeta(zn.InhMeta, evalMeta) if zn.InhMeta.YamlSep { v.b.WriteString("---\n") } else { v.b.WriteByte('\n') } ast.Walk(v, &zn.Ast) length, err := v.b.Flush() return length, err } // WriteMeta encodes meta data as zmk. func (*Encoder) WriteMeta(w io.Writer, m *meta.Meta, evalMeta encoder.EvalMetaFunc) (int, error) { v := newVisitor(w) v.acceptMeta(m, evalMeta) length, err := v.b.Flush() return length, err } func (v *visitor) acceptMeta(m *meta.Meta, evalMeta encoder.EvalMetaFunc) { for _, p := range m.ComputedPairs() { key := p.Key v.b.WriteStrings(key, ": ") if meta.Type(key) == meta.TypeZettelmarkup { is := evalMeta(p.Value) ast.Walk(v, &is) } else { v.b.WriteString(p.Value) } v.b.WriteByte('\n') } } func (ze *Encoder) WriteContent(w io.Writer, zn *ast.ZettelNode) (int, error) { return ze.WriteBlocks(w, &zn.Ast) } // WriteBlocks writes the content of a block slice to the writer. func (*Encoder) WriteBlocks(w io.Writer, bs *ast.BlockSlice) (int, error) { v := newVisitor(w) ast.Walk(v, bs) length, err := v.b.Flush() return length, err } // WriteInlines writes an inline slice to the writer func (*Encoder) WriteInlines(w io.Writer, is *ast.InlineSlice) (int, error) { v := newVisitor(w) ast.Walk(v, is) length, err := v.b.Flush() return length, err } // visitor writes the abstract syntax tree to an io.Writer. type visitor struct { b encoder.EncWriter textEnc encoder.Encoder prefix []byte inVerse bool inlinePos int } func newVisitor(w io.Writer) *visitor { return &visitor{b: encoder.NewEncWriter(w), textEnc: textenc.Create()} } func (v *visitor) Visit(node ast.Node) ast.Visitor { switch n := node.(type) { case *ast.BlockSlice: v.visitBlockSlice(n) case *ast.InlineSlice: for i, in := range *n { v.inlinePos = i ast.Walk(v, in) } v.inlinePos = 0 case *ast.VerbatimNode: v.visitVerbatim(n) case *ast.RegionNode: v.visitRegion(n) case *ast.HeadingNode: v.visitHeading(n) case *ast.HRuleNode: v.b.WriteString("---") v.visitAttributes(n.Attrs) case *ast.NestedListNode: v.visitNestedList(n) case *ast.DescriptionListNode: v.visitDescriptionList(n) case *ast.TableNode: v.visitTable(n) case *ast.TranscludeNode: v.b.WriteStrings("{{{", n.Ref.String(), "}}}") v.visitAttributes(n.Attrs) case *ast.BLOBNode: v.visitBLOB(n) case *ast.TextNode: v.visitText(n) case *ast.SpaceNode: v.b.WriteString(n.Lexeme) case *ast.BreakNode: v.visitBreak(n) case *ast.LinkNode: v.visitLink(n) case *ast.EmbedRefNode: v.visitEmbedRef(n) case *ast.EmbedBLOBNode: v.visitEmbedBLOB(n) case *ast.CiteNode: v.visitCite(n) case *ast.FootnoteNode: v.b.WriteString("[^") ast.Walk(v, &n.Inlines) v.b.WriteByte(']') v.visitAttributes(n.Attrs) case *ast.MarkNode: v.visitMark(n) case *ast.FormatNode: v.visitFormat(n) case *ast.LiteralNode: v.visitLiteral(n) default: return v } return nil } func (v *visitor) visitBlockSlice(bs *ast.BlockSlice) { var lastWasParagraph bool for i, bn := range *bs { if i > 0 { v.b.WriteByte('\n') if lastWasParagraph && !v.inVerse { if _, ok := bn.(*ast.ParaNode); ok { v.b.WriteByte('\n') } } } ast.Walk(v, bn) _, lastWasParagraph = bn.(*ast.ParaNode) } } var mapVerbatimKind = map[ast.VerbatimKind]string{ ast.VerbatimZettel: "@@@", ast.VerbatimComment: "%%%", ast.VerbatimHTML: "@@@", // Attribute is set to {="html"} ast.VerbatimProg: "```", ast.VerbatimEval: "~~~", ast.VerbatimMath: "$$$", } func (v *visitor) visitVerbatim(vn *ast.VerbatimNode) { kind, ok := mapVerbatimKind[vn.Kind] if !ok { panic(fmt.Sprintf("Unknown verbatim kind %d", vn.Kind)) } attrs := vn.Attrs if vn.Kind == ast.VerbatimHTML { attrs = syntaxToHTML(attrs) } // TODO: scan cn.Lines to find embedded kind[0]s at beginning v.b.WriteString(kind) v.visitAttributes(attrs) v.b.WriteByte('\n') v.b.Write(vn.Content) v.b.WriteByte('\n') v.b.WriteString(kind) } var mapRegionKind = map[ast.RegionKind]string{ ast.RegionSpan: ":::", ast.RegionQuote: "<<<", ast.RegionVerse: "\"\"\"", } func (v *visitor) visitRegion(rn *ast.RegionNode) { // Scan rn.Blocks for embedded regions to adjust length of regionCode kind, ok := mapRegionKind[rn.Kind] if !ok { panic(fmt.Sprintf("Unknown region kind %d", rn.Kind)) } v.b.WriteString(kind) v.visitAttributes(rn.Attrs) v.b.WriteByte('\n') saveInVerse := v.inVerse v.inVerse = rn.Kind == ast.RegionVerse ast.Walk(v, &rn.Blocks) v.inVerse = saveInVerse v.b.WriteByte('\n') v.b.WriteString(kind) if len(rn.Inlines) > 0 { v.b.WriteByte(' ') ast.Walk(v, &rn.Inlines) } } func (v *visitor) visitHeading(hn *ast.HeadingNode) { const headingSigns = "========= " v.b.WriteString(headingSigns[len(headingSigns)-hn.Level-3:]) ast.Walk(v, &hn.Inlines) v.visitAttributes(hn.Attrs) } var mapNestedListKind = map[ast.NestedListKind]byte{ ast.NestedListOrdered: '#', ast.NestedListUnordered: '*', ast.NestedListQuote: '>', } func (v *visitor) visitNestedList(ln *ast.NestedListNode) { v.prefix = append(v.prefix, mapNestedListKind[ln.Kind]) for i, item := range ln.Items { if i > 0 { v.b.WriteByte('\n') } v.b.Write(v.prefix) v.b.WriteByte(' ') for j, in := range item { if j > 0 { v.b.WriteByte('\n') if _, ok := in.(*ast.ParaNode); ok { v.writePrefixSpaces() } } ast.Walk(v, in) } } v.prefix = v.prefix[:len(v.prefix)-1] } func (v *visitor) writePrefixSpaces() { if prefixLen := len(v.prefix); prefixLen > 0 { for i := 0; i <= prefixLen; i++ { v.b.WriteByte(' ') } } } func (v *visitor) visitDescriptionList(dn *ast.DescriptionListNode) { for i, descr := range dn.Descriptions { if i > 0 { v.b.WriteByte('\n') } v.b.WriteString("; ") ast.Walk(v, &descr.Term) for _, b := range descr.Descriptions { v.b.WriteString("\n: ") for jj, dn := range b { if jj > 0 { v.b.WriteString("\n\n ") } ast.Walk(v, dn) } } } } var alignCode = map[ast.Alignment]string{ ast.AlignDefault: "", ast.AlignLeft: "<", ast.AlignCenter: ":", ast.AlignRight: ">", } func (v *visitor) visitTable(tn *ast.TableNode) { if header := tn.Header; len(header) > 0 { v.writeTableHeader(header, tn.Align) v.b.WriteByte('\n') } for i, row := range tn.Rows { if i > 0 { v.b.WriteByte('\n') } v.writeTableRow(row, tn.Align) } } func (v *visitor) writeTableHeader(header ast.TableRow, align []ast.Alignment) { for pos, cell := range header { v.b.WriteString("|=") colAlign := align[pos] if cell.Align != colAlign { v.b.WriteString(alignCode[cell.Align]) } ast.Walk(v, &cell.Inlines) if colAlign != ast.AlignDefault { v.b.WriteString(alignCode[colAlign]) } } } func (v *visitor) writeTableRow(row ast.TableRow, align []ast.Alignment) { for pos, cell := range row { v.b.WriteByte('|') if cell.Align != align[pos] { v.b.WriteString(alignCode[cell.Align]) } ast.Walk(v, &cell.Inlines) } } func (v *visitor) visitBLOB(bn *ast.BLOBNode) { if bn.Syntax == meta.SyntaxSVG { v.b.WriteStrings("@@@", bn.Syntax, "\n") v.b.Write(bn.Blob) v.b.WriteString("\n@@@\n") return } var sb strings.Builder v.textEnc.WriteInlines(&sb, &bn.Description) v.b.WriteStrings("%% Unable to display BLOB with description '", sb.String(), "' and syntax '", bn.Syntax, "'.") } var escapeSeqs = strfun.NewSet( "\\", "__", "**", "~~", "^^", ",,", ">>", `""`, "::", "''", "``", "++", "==", ) func (v *visitor) visitText(tn *ast.TextNode) { last := 0 for i := 0; i < len(tn.Text); i++ { if b := tn.Text[i]; b == '\\' { v.b.WriteString(tn.Text[last:i]) v.b.WriteBytes('\\', b) last = i + 1 continue } if i < len(tn.Text)-1 { s := tn.Text[i : i+2] if escapeSeqs.Has(s) { v.b.WriteString(tn.Text[last:i]) for j := 0; j < len(s); j++ { v.b.WriteBytes('\\', s[j]) } i++ last = i + 1 continue } } } v.b.WriteString(tn.Text[last:]) } func (v *visitor) visitBreak(bn *ast.BreakNode) { if bn.Hard { v.b.WriteString("\\\n") } else { v.b.WriteByte('\n') } v.writePrefixSpaces() } func (v *visitor) visitLink(ln *ast.LinkNode) { v.b.WriteString("[[") if len(ln.Inlines) > 0 { ast.Walk(v, &ln.Inlines) v.b.WriteByte('|') } if ln.Ref.State == ast.RefStateBased { v.b.WriteByte('/') } v.b.WriteStrings(ln.Ref.String(), "]]") } func (v *visitor) visitEmbedRef(en *ast.EmbedRefNode) { v.b.WriteString("{{") if len(en.Inlines) > 0 { ast.Walk(v, &en.Inlines) v.b.WriteByte('|') } v.b.WriteStrings(en.Ref.String(), "}}") } func (v *visitor) visitEmbedBLOB(en *ast.EmbedBLOBNode) { if en.Syntax == meta.SyntaxSVG { v.b.WriteString("@@") v.b.Write(en.Blob) v.b.WriteStrings("@@{=", en.Syntax, "}") return } v.b.WriteString("{{TODO: display inline BLOB}}") } func (v *visitor) visitCite(cn *ast.CiteNode) { v.b.WriteStrings("[@", cn.Key) if len(cn.Inlines) > 0 { v.b.WriteByte(' ') ast.Walk(v, &cn.Inlines) } v.b.WriteByte(']') v.visitAttributes(cn.Attrs) } func (v *visitor) visitMark(mn *ast.MarkNode) { v.b.WriteStrings("[!", mn.Mark) if len(mn.Inlines) > 0 { v.b.WriteByte('|') ast.Walk(v, &mn.Inlines) } v.b.WriteByte(']') } var mapFormatKind = map[ast.FormatKind][]byte{ ast.FormatEmph: []byte("__"), ast.FormatStrong: []byte("**"), ast.FormatInsert: []byte(">>"), ast.FormatDelete: []byte("~~"), ast.FormatSuper: []byte("^^"), ast.FormatSub: []byte(",,"), ast.FormatQuote: []byte(`""`), ast.FormatSpan: []byte("::"), } func (v *visitor) visitFormat(fn *ast.FormatNode) { kind, ok := mapFormatKind[fn.Kind] if !ok { panic(fmt.Sprintf("Unknown format kind %d", fn.Kind)) } v.b.Write(kind) ast.Walk(v, &fn.Inlines) v.b.Write(kind) v.visitAttributes(fn.Attrs) } func (v *visitor) visitLiteral(ln *ast.LiteralNode) { switch ln.Kind { case ast.LiteralZettel: v.writeLiteral('@', ln.Attrs, ln.Content) case ast.LiteralProg: v.writeLiteral('`', ln.Attrs, ln.Content) case ast.LiteralMath: v.b.WriteStrings("$$", string(ln.Content), "$$") v.visitAttributes(ln.Attrs) case ast.LiteralInput: v.writeLiteral('\'', ln.Attrs, ln.Content) case ast.LiteralOutput: v.writeLiteral('=', ln.Attrs, ln.Content) case ast.LiteralComment: if v.inlinePos > 0 { v.b.WriteByte(' ') } v.b.WriteString("%%") v.visitAttributes(ln.Attrs) v.b.WriteByte(' ') v.b.Write(ln.Content) case ast.LiteralHTML: v.writeLiteral('@', syntaxToHTML(ln.Attrs), ln.Content) default: panic(fmt.Sprintf("Unknown literal kind %v", ln.Kind)) } } func (v *visitor) writeLiteral(code byte, a attrs.Attributes, content []byte) { v.b.WriteBytes(code, code) v.writeEscaped(string(content), code) v.b.WriteBytes(code, code) v.visitAttributes(a) } // visitAttributes write HTML attributes func (v *visitor) visitAttributes(a attrs.Attributes) { if a.IsEmpty() { return } v.b.WriteByte('{') for i, k := range a.Keys() { if i > 0 { v.b.WriteByte(' ') } if k == "-" { v.b.WriteByte('-') continue } v.b.WriteString(k) if vl := a[k]; len(vl) > 0 { v.b.WriteStrings("=\"", vl) v.b.WriteByte('"') } } v.b.WriteByte('}') } func (v *visitor) writeEscaped(s string, toEscape byte) { last := 0 for i := 0; i < len(s); i++ { if b := s[i]; b == toEscape || b == '\\' { v.b.WriteString(s[last:i]) v.b.WriteBytes('\\', b) last = i + 1 } } v.b.WriteString(s[last:]) } func syntaxToHTML(a attrs.Attributes) attrs.Attributes { return a.Clone().Set("", meta.SyntaxHTML).Remove(api.KeySyntax) } |
Added encoding/atom/atom.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 | //----------------------------------------------------------------------------- // Copyright (c) 2022-present Detlef Stern // // This file is part of Zettelstore. // // Zettelstore is licensed under the latest version of the EUPL (European Union // Public License). Please see file LICENSE.txt for your rights and obligations // under this license. //----------------------------------------------------------------------------- // Package atom provides an Atom encoding. package atom import ( "bytes" "time" "zettelstore.de/c/api" "zettelstore.de/z/config" "zettelstore.de/z/domain/id" "zettelstore.de/z/domain/meta" "zettelstore.de/z/encoding" "zettelstore.de/z/encoding/xml" "zettelstore.de/z/kernel" "zettelstore.de/z/query" "zettelstore.de/z/strfun" ) const ContentType = "application/atom+xml" type Configuration struct { Title string Generator string NewURLBuilderAbs func() *api.URLBuilder } func (c *Configuration) Setup(cfg config.Config) { baseURL := kernel.Main.GetConfig(kernel.WebService, kernel.WebBaseURL).(string) c.Title = cfg.GetSiteName() c.Generator = (kernel.Main.GetConfig(kernel.CoreService, kernel.CoreProgname).(string) + " " + kernel.Main.GetConfig(kernel.CoreService, kernel.CoreVersion).(string)) c.NewURLBuilderAbs = func() *api.URLBuilder { return api.NewURLBuilder(baseURL, 'h') } } func (c *Configuration) Marshal(q *query.Query, ml []*meta.Meta) []byte { atomUpdated := encoding.LastUpdated(ml, time.RFC3339) feedLink := c.NewURLBuilderAbs().String() var buf bytes.Buffer buf.WriteString(`<feed xmlns="http://www.w3.org/2005/Atom">` + "\n") xml.WriteTag(&buf, " ", "title", c.Title) xml.WriteTag(&buf, " ", "id", feedLink) buf.WriteString(` <link rel="self" href="`) if s := q.String(); s != "" { strfun.XMLEscape(&buf, c.NewURLBuilderAbs().AppendQuery(s).String()) } else { strfun.XMLEscape(&buf, feedLink) } buf.WriteString(`"/>` + "\n") if atomUpdated != "" { xml.WriteTag(&buf, " ", "updated", atomUpdated) } xml.WriteTag(&buf, " ", "generator", c.Generator) buf.WriteString(" <author><name>Unknown</name></author>\n") for _, m := range ml { c.marshalMeta(&buf, m) } buf.WriteString("</feed>") return buf.Bytes() } func (c *Configuration) marshalMeta(buf *bytes.Buffer, m *meta.Meta) { entryUpdated := "" if val, found := m.Get(api.KeyPublished); found { if published, err := time.ParseInLocation(id.ZidLayout, val, time.Local); err == nil { entryUpdated = published.UTC().Format(time.RFC3339) } } link := c.NewURLBuilderAbs().SetZid(api.ZettelID(m.Zid.String())).String() buf.WriteString(" <entry>\n") xml.WriteTag(buf, " ", "title", encoding.TitleAsText(m)) xml.WriteTag(buf, " ", "id", link) buf.WriteString(` <link rel="self" href="`) strfun.XMLEscape(buf, link) buf.WriteString(`"/>` + "\n") buf.WriteString(` <link rel="alternate" type="text/html" href="`) strfun.XMLEscape(buf, link) buf.WriteString(`"/>` + "\n") if entryUpdated != "" { xml.WriteTag(buf, " ", "updated", entryUpdated) } marshalTags(buf, m) buf.WriteString(" </entry>\n") } func marshalTags(buf *bytes.Buffer, m *meta.Meta) { if tags, found := m.GetList(api.KeyTags); found && len(tags) > 0 { for _, tag := range tags { for len(tag) > 0 && tag[0] == '#' { tag = tag[1:] } if tag != "" { buf.WriteString(` <category term="`) strfun.XMLEscape(buf, tag) buf.WriteString("\"/>\n") } } } } |
Added encoding/encoding.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 | //----------------------------------------------------------------------------- // Copyright (c) 2022-present Detlef Stern // // This file is part of Zettelstore. // // Zettelstore is licensed under the latest version of the EUPL (European Union // Public License). Please see file LICENSE.txt for your rights and obligations // under this license. //----------------------------------------------------------------------------- // Package encoding provides helper functions for encodings. package encoding import ( "time" "zettelstore.de/c/api" "zettelstore.de/z/domain/id" "zettelstore.de/z/domain/meta" "zettelstore.de/z/parser" ) // LastUpdated returns the formated time of the zettel which was updated at the latest time. func LastUpdated(ml []*meta.Meta, timeFormat string) string { maxPublished := time.Date(1, time.January, 1, 0, 0, 0, 0, time.Local) for _, m := range ml { if val, found := m.Get(api.KeyPublished); found { if published, err := time.ParseInLocation(id.ZidLayout, val, time.Local); err == nil { if maxPublished.Before(published) { maxPublished = published } } } } if maxPublished.Year() > 1 { return maxPublished.UTC().Format(timeFormat) } return "" } // TitleAsText returns the title of a zettel as plain text func TitleAsText(m *meta.Meta) string { return parser.NormalizedSpacedText(m.GetTitle()) } |
Added encoding/rss/rss.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 | //----------------------------------------------------------------------------- // Copyright (c) 2022-present Detlef Stern // // This file is part of Zettelstore. // // Zettelstore is licensed under the latest version of the EUPL (European Union // Public License). Please see file LICENSE.txt for your rights and obligations // under this license. //----------------------------------------------------------------------------- // Package rss provides a RSS encoding. package rss import ( "bytes" "context" "time" "zettelstore.de/c/api" "zettelstore.de/z/config" "zettelstore.de/z/domain/id" "zettelstore.de/z/domain/meta" "zettelstore.de/z/encoding" "zettelstore.de/z/encoding/xml" "zettelstore.de/z/kernel" "zettelstore.de/z/query" "zettelstore.de/z/strfun" ) const ContentType = "application/rss+xml" type Configuration struct { Title string Language string Copyright string Generator string NewURLBuilderAbs func() *api.URLBuilder } func (c *Configuration) Setup(ctx context.Context, cfg config.Config) { baseURL := kernel.Main.GetConfig(kernel.WebService, kernel.WebBaseURL).(string) defVals := cfg.AddDefaultValues(ctx, &meta.Meta{}) c.Title = cfg.GetSiteName() c.Language = defVals.GetDefault(api.KeyLang, "") c.Copyright = defVals.GetDefault(api.KeyCopyright, "") c.Generator = (kernel.Main.GetConfig(kernel.CoreService, kernel.CoreProgname).(string) + " " + kernel.Main.GetConfig(kernel.CoreService, kernel.CoreVersion).(string)) c.NewURLBuilderAbs = func() *api.URLBuilder { return api.NewURLBuilder(baseURL, 'h') } } func (c *Configuration) Marshal(q *query.Query, ml []*meta.Meta) []byte { rssPublished := encoding.LastUpdated(ml, time.RFC1123Z) atomLink := "" if s := q.String(); s != "" { atomLink = c.NewURLBuilderAbs().AppendQuery(s).String() } var buf bytes.Buffer buf.WriteString(`<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom">` + "\n<channel>\n") xml.WriteTag(&buf, " ", "title", c.Title) xml.WriteTag(&buf, " ", "link", c.NewURLBuilderAbs().String()) xml.WriteTag(&buf, " ", "description", "") xml.WriteTag(&buf, " ", "language", c.Language) xml.WriteTag(&buf, " ", "copyright", c.Copyright) if rssPublished != "" { xml.WriteTag(&buf, " ", "pubDate", rssPublished) xml.WriteTag(&buf, " ", "lastBuildDate", rssPublished) } xml.WriteTag(&buf, " ", "generator", c.Generator) buf.WriteString(" <docs>https://www.rssboard.org/rss-specification</docs>\n") if atomLink != "" { buf.WriteString(` <atom:link href="`) strfun.XMLEscape(&buf, atomLink) buf.WriteString(`" rel="self" type="application/rss+xml"></atom:link>` + "\n") } for _, m := range ml { c.marshalMeta(&buf, m) } buf.WriteString("</channel>\n</rss>") return buf.Bytes() } func (c *Configuration) marshalMeta(buf *bytes.Buffer, m *meta.Meta) { itemPublished := "" if val, found := m.Get(api.KeyPublished); found { if published, err := time.ParseInLocation(id.ZidLayout, val, time.Local); err == nil { itemPublished = published.UTC().Format(time.RFC1123Z) } } link := c.NewURLBuilderAbs().SetZid(api.ZettelID(m.Zid.String())).String() buf.WriteString(" <item>\n") xml.WriteTag(buf, " ", "title", encoding.TitleAsText(m)) xml.WriteTag(buf, " ", "link", link) xml.WriteTag(buf, " ", "guid", link) if itemPublished != "" { xml.WriteTag(buf, " ", "pubDate", itemPublished) } marshalTags(buf, m) buf.WriteString(" </item>\n") } func marshalTags(buf *bytes.Buffer, m *meta.Meta) { if tags, found := m.GetList(api.KeyTags); found && len(tags) > 0 { for _, tag := range tags { for len(tag) > 0 && tag[0] == '#' { tag = tag[1:] } if tag != "" { xml.WriteTag(buf, " ", "category", tag) } } } } |
Added encoding/xml/xml.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 | //----------------------------------------------------------------------------- // Copyright (c) 2022-present Detlef Stern // // This file is part of Zettelstore. // // Zettelstore is licensed under the latest version of the EUPL (European Union // Public License). Please see file LICENSE.txt for your rights and obligations // under this license. //----------------------------------------------------------------------------- // Package xml provides helper for a XML-based encoding. package xml import ( "bytes" "zettelstore.de/z/strfun" ) // Header contains the string that should start all XML documents. const Header = `<?xml version="1.0" encoding="UTF-8"?>` + "\n" // WriteTag writes a simple XML tag with a given prefix and a specific value. func WriteTag(buf *bytes.Buffer, prefix, tag, value string) { buf.WriteString(prefix) buf.WriteByte('<') buf.WriteString(tag) buf.WriteByte('>') strfun.XMLEscape(buf, value) buf.WriteString("</") buf.WriteString(tag) buf.WriteString(">\n") } |
Added evaluator/evaluator.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 581 582 583 584 585 586 587 588 589 590 591 592 593 594 595 596 597 598 599 600 601 602 603 604 605 606 607 608 609 610 611 612 613 614 615 616 617 618 619 620 621 622 623 624 625 626 627 628 | //----------------------------------------------------------------------------- // Copyright (c) 2021-present Detlef Stern // // This file is part of Zettelstore. // // Zettelstore is licensed under the latest version of the EUPL (European Union // Public License). Please see file LICENSE.txt for your rights and obligations // under this license. //----------------------------------------------------------------------------- // Package evaluator interprets and evaluates the AST. package evaluator import ( "context" "errors" "fmt" "path" "strconv" "strings" "zettelstore.de/c/api" "zettelstore.de/c/attrs" "zettelstore.de/z/ast" "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/input" "zettelstore.de/z/parser" "zettelstore.de/z/parser/cleaner" "zettelstore.de/z/parser/draw" "zettelstore.de/z/query" ) // Port contains all methods to retrieve zettel (or part of it) to evaluate a zettel. type Port interface { GetMeta(context.Context, id.Zid) (*meta.Meta, error) GetZettel(context.Context, id.Zid) (domain.Zettel, error) SelectMeta(ctx context.Context, q *query.Query) ([]*meta.Meta, error) } // EvaluateZettel evaluates the given zettel in the given context, with the // given ports, and the given environment. func EvaluateZettel(ctx context.Context, port Port, rtConfig config.Config, zn *ast.ZettelNode) { if zn.Syntax == meta.SyntaxNone { // AST is empty, evaluate to a description list of metadata. zn.Ast = evaluateMetadata(zn.Meta) return } EvaluateBlock(ctx, port, rtConfig, &zn.Ast) } // EvaluateBlock evaluates the given block list in the given context, with // the given ports, and the given environment. func EvaluateBlock(ctx context.Context, port Port, rtConfig config.Config, bns *ast.BlockSlice) { evaluateNode(ctx, port, rtConfig, bns) cleaner.CleanBlockSlice(bns, true) } // EvaluateInline evaluates the given inline list in the given context, with // the given ports, and the given environment. func EvaluateInline(ctx context.Context, port Port, rtConfig config.Config, is *ast.InlineSlice) { evaluateNode(ctx, port, rtConfig, is) cleaner.CleanInlineSlice(is) } func evaluateNode(ctx context.Context, port Port, rtConfig config.Config, n ast.Node) { e := evaluator{ ctx: ctx, port: port, rtConfig: rtConfig, transcludeMax: rtConfig.GetMaxTransclusions(), transcludeCount: 0, costMap: map[id.Zid]transcludeCost{}, embedMap: map[string]ast.InlineSlice{}, marker: &ast.ZettelNode{}, } ast.Walk(&e, n) } type evaluator struct { ctx context.Context port Port rtConfig config.Config transcludeMax int transcludeCount int costMap map[id.Zid]transcludeCost marker *ast.ZettelNode embedMap map[string]ast.InlineSlice } type transcludeCost struct { zn *ast.ZettelNode ec int } func (e *evaluator) Visit(node ast.Node) ast.Visitor { switch n := node.(type) { case *ast.BlockSlice: e.visitBlockSlice(n) case *ast.InlineSlice: e.visitInlineSlice(n) default: return e } return nil } func (e *evaluator) visitBlockSlice(bs *ast.BlockSlice) { for i := 0; i < len(*bs); i++ { bn := (*bs)[i] ast.Walk(e, bn) switch n := bn.(type) { case *ast.VerbatimNode: i += transcludeNode(bs, i, e.evalVerbatimNode(n)) case *ast.TranscludeNode: i += transcludeNode(bs, i, e.evalTransclusionNode(n)) } } } func transcludeNode(bln *ast.BlockSlice, i int, bn ast.BlockNode) int { if ln, ok := bn.(*ast.BlockSlice); ok { *bln = replaceWithBlockNodes(*bln, i, *ln) return len(*ln) - 1 } if bn == nil { (*bln) = (*bln)[:i+copy((*bln)[i:], (*bln)[i+1:])] return -1 } (*bln)[i] = bn return 0 } func replaceWithBlockNodes(bns []ast.BlockNode, i int, replaceBns []ast.BlockNode) []ast.BlockNode { if len(replaceBns) == 1 { bns[i] = replaceBns[0] return bns } newIns := make([]ast.BlockNode, 0, len(bns)+len(replaceBns)-1) if i > 0 { newIns = append(newIns, bns[:i]...) } if len(replaceBns) > 0 { newIns = append(newIns, replaceBns...) } if i+1 < len(bns) { newIns = append(newIns, bns[i+1:]...) } return newIns } func (e *evaluator) evalVerbatimNode(vn *ast.VerbatimNode) ast.BlockNode { switch vn.Kind { case ast.VerbatimZettel: return e.evalVerbatimZettel(vn) case ast.VerbatimEval: if syntax, found := vn.Attrs.Get(""); found && syntax == meta.SyntaxDraw { return draw.ParseDrawBlock(vn) } } return vn } func (e *evaluator) evalVerbatimZettel(vn *ast.VerbatimNode) ast.BlockNode { m := meta.New(id.Invalid) m.Set(api.KeySyntax, getSyntax(vn.Attrs, meta.SyntaxText)) zettel := domain.Zettel{ Meta: m, Content: domain.NewContent(vn.Content), } e.transcludeCount++ zn := e.evaluateEmbeddedZettel(zettel) return &zn.Ast } func getSyntax(a attrs.Attributes, defSyntax string) string { if a != nil { if val, ok := a.Get(api.KeySyntax); ok { return val } if val, ok := a.Get(""); ok { return val } } return defSyntax } func (e *evaluator) evalTransclusionNode(tn *ast.TranscludeNode) ast.BlockNode { ref := tn.Ref // To prevent e.embedCount from counting if errText := e.checkMaxTransclusions(ref); errText != nil { return makeBlockNode(errText) } switch ref.State { case ast.RefStateZettel: // Only zettel references will be evaluated. case ast.RefStateInvalid, ast.RefStateBroken: e.transcludeCount++ return makeBlockNode(createInlineErrorText(ref, "Invalid", "or", "broken", "transclusion", "reference")) case ast.RefStateSelf: e.transcludeCount++ return makeBlockNode(createInlineErrorText(ref, "Self", "transclusion", "reference")) case ast.RefStateFound, ast.RefStateExternal: return tn case ast.RefStateHosted, ast.RefStateBased: if n := createEmbeddedNodeLocal(ref); n != nil { n.Attrs = tn.Attrs return makeBlockNode(n) } return tn case ast.RefStateQuery: e.transcludeCount++ return e.evalQueryTransclusion(tn.Ref.Value) default: return makeBlockNode(createInlineErrorText(ref, "Illegal", "block", "state", strconv.Itoa(int(ref.State)))) } zid, err := id.Parse(ref.URL.Path) if err != nil { panic(err) } cost, ok := e.costMap[zid] zn := cost.zn if zn == e.marker { e.transcludeCount++ return makeBlockNode(createInlineErrorText(ref, "Recursive", "transclusion")) } if !ok { zettel, err1 := e.port.GetZettel(box.NoEnrichContext(e.ctx), zid) if err1 != nil { if errors.Is(err1, &box.ErrNotAllowed{}) { return nil } e.transcludeCount++ return makeBlockNode(createInlineErrorText(ref, "Unable", "to", "get", "zettel")) } setMetadataFromAttributes(zettel.Meta, tn.Attrs) ec := e.transcludeCount e.costMap[zid] = transcludeCost{zn: e.marker, ec: ec} zn = e.evaluateEmbeddedZettel(zettel) e.costMap[zid] = transcludeCost{zn: zn, ec: e.transcludeCount - ec} e.transcludeCount = 0 // No stack needed, because embedding is done left-recursive, depth-first. } e.transcludeCount++ if ec := cost.ec; ec > 0 { e.transcludeCount += cost.ec } return &zn.Ast } func (e *evaluator) evalQueryTransclusion(expr string) ast.BlockNode { q := query.Parse(expr) ml, err := e.port.SelectMeta(e.ctx, q) if err != nil { if errors.Is(err, &box.ErrNotAllowed{}) { return nil } return makeBlockNode(createInlineErrorText(nil, "Unable", "to", "search", "zettel")) } result := QueryAction(e.ctx, q, ml, e.rtConfig) if result != nil { ast.Walk(e, result) } return result } func (e *evaluator) checkMaxTransclusions(ref *ast.Reference) ast.InlineNode { if maxTrans := e.transcludeMax; e.transcludeCount > maxTrans { e.transcludeCount = maxTrans + 1 return createInlineErrorText(ref, "Too", "many", "transclusions", "(must", "be", "at", "most", strconv.Itoa(maxTrans)+",", "see", "runtime", "configuration", "key", "max-transclusions)") } return nil } func makeBlockNode(in ast.InlineNode) ast.BlockNode { return ast.CreateParaNode(in) } func setMetadataFromAttributes(m *meta.Meta, a attrs.Attributes) { for aKey, aVal := range a { if meta.KeyIsValid(aKey) { m.Set(aKey, aVal) } } } func (e *evaluator) visitInlineSlice(is *ast.InlineSlice) { for i := 0; i < len(*is); i++ { in := (*is)[i] ast.Walk(e, in) switch n := in.(type) { case *ast.LinkNode: (*is)[i] = e.evalLinkNode(n) case *ast.EmbedRefNode: i += embedNode(is, i, e.evalEmbedRefNode(n)) case *ast.LiteralNode: i += embedNode(is, i, e.evalLiteralNode(n)) } } } func embedNode(is *ast.InlineSlice, i int, in ast.InlineNode) int { if ln, ok := in.(*ast.InlineSlice); ok { *is = replaceWithInlineNodes(*is, i, *ln) return len(*ln) - 1 } if in == nil { (*is) = (*is)[:i+copy((*is)[i:], (*is)[i+1:])] return -1 } (*is)[i] = in return 0 } func replaceWithInlineNodes(ins ast.InlineSlice, i int, replaceIns ast.InlineSlice) ast.InlineSlice { if len(replaceIns) == 1 { ins[i] = replaceIns[0] return ins } newIns := make(ast.InlineSlice, 0, len(ins)+len(replaceIns)-1) if i > 0 { newIns = append(newIns, ins[:i]...) } if len(replaceIns) > 0 { newIns = append(newIns, replaceIns...) } if i+1 < len(ins) { newIns = append(newIns, ins[i+1:]...) } return newIns } func (e *evaluator) evalLinkNode(ln *ast.LinkNode) ast.InlineNode { if len(ln.Inlines) == 0 { ln.Inlines = ast.InlineSlice{&ast.TextNode{Text: ln.Ref.Value}} } ref := ln.Ref if ref == nil || ref.State != ast.RefStateZettel { return ln } zid := mustParseZid(ref) _, err := e.port.GetMeta(box.NoEnrichContext(e.ctx), zid) if errors.Is(err, &box.ErrNotAllowed{}) { return &ast.FormatNode{ Kind: ast.FormatSpan, Attrs: ln.Attrs, Inlines: getLinkInline(ln), } } else if err != nil { ln.Ref.State = ast.RefStateBroken return ln } ln.Ref.State = ast.RefStateZettel return ln } func getLinkInline(ln *ast.LinkNode) ast.InlineSlice { if ln.Inlines != nil { return ln.Inlines } return ast.InlineSlice{&ast.TextNode{Text: ln.Ref.Value}} } func (e *evaluator) evalEmbedRefNode(en *ast.EmbedRefNode) ast.InlineNode { ref := en.Ref // To prevent e.embedCount from counting if errText := e.checkMaxTransclusions(ref); errText != nil { return errText } switch ref.State { case ast.RefStateZettel: // Only zettel references will be evaluated. case ast.RefStateInvalid, ast.RefStateBroken: e.transcludeCount++ return createInlineErrorImage(en) case ast.RefStateSelf: e.transcludeCount++ return createInlineErrorText(ref, "Self", "embed", "reference") case ast.RefStateFound, ast.RefStateExternal: return en case ast.RefStateHosted, ast.RefStateBased: if n := createEmbeddedNodeLocal(ref); n != nil { n.Attrs = en.Attrs n.Inlines = en.Inlines return n } return en default: return createInlineErrorText(ref, "Illegal", "inline", "state", strconv.Itoa(int(ref.State))) } zid := mustParseZid(ref) zettel, err := e.port.GetZettel(box.NoEnrichContext(e.ctx), zid) if err != nil { if errors.Is(err, &box.ErrNotAllowed{}) { return nil } e.transcludeCount++ return createInlineErrorImage(en) } if syntax := zettel.Meta.GetDefault(api.KeySyntax, ""); parser.IsImageFormat(syntax) { e.updateImageRefNode(en, zettel.Meta, syntax) return en } else if !parser.IsASTParser(syntax) { // Not embeddable. e.transcludeCount++ return createInlineErrorText(ref, "Not", "embeddable (syntax="+syntax+")") } cost, ok := e.costMap[zid] zn := cost.zn if zn == e.marker { e.transcludeCount++ return createInlineErrorText(ref, "Recursive", "transclusion") } if !ok { ec := e.transcludeCount e.costMap[zid] = transcludeCost{zn: e.marker, ec: ec} zn = e.evaluateEmbeddedZettel(zettel) e.costMap[zid] = transcludeCost{zn: zn, ec: e.transcludeCount - ec} e.transcludeCount = 0 // No stack needed, because embedding is done left-recursive, depth-first. } e.transcludeCount++ result, ok := e.embedMap[ref.Value] if !ok { // Search for text to be embedded. result = findInlineSlice(&zn.Ast, ref.URL.Fragment) e.embedMap[ref.Value] = result } if len(result) == 0 { return &ast.LiteralNode{ Kind: ast.LiteralComment, Attrs: map[string]string{"-": ""}, Content: append([]byte("Nothing to transclude: "), ref.String()...), } } if ec := cost.ec; ec > 0 { e.transcludeCount += cost.ec } return &result } func mustParseZid(ref *ast.Reference) id.Zid { zid, err := id.Parse(ref.URL.Path) if err != nil { panic(fmt.Sprintf("%v: %q (state %v) -> %v", err, ref.URL.Path, ref.State, ref)) } return zid } func (e *evaluator) updateImageRefNode(en *ast.EmbedRefNode, m *meta.Meta, syntax string) { en.Syntax = syntax if len(en.Inlines) == 0 { is := parser.ParseDescription(m) if len(is) > 0 { ast.Walk(e, &is) if len(is) > 0 { en.Inlines = is } } } } func (e *evaluator) evalLiteralNode(ln *ast.LiteralNode) ast.InlineNode { if ln.Kind != ast.LiteralZettel { return ln } e.transcludeCount++ result := e.evaluateEmbeddedInline(ln.Content, getSyntax(ln.Attrs, meta.SyntaxText)) if len(result) == 0 { return &ast.LiteralNode{ Kind: ast.LiteralComment, Attrs: map[string]string{"-": ""}, Content: []byte("Nothing to transclude"), } } return &result } func createInlineErrorImage(en *ast.EmbedRefNode) *ast.EmbedRefNode { errorZid := id.EmojiZid en.Ref = ast.ParseReference(errorZid.String()) if len(en.Inlines) == 0 { en.Inlines = parser.ParseMetadata("Error placeholder") } return en } func createInlineErrorText(ref *ast.Reference, msgWords ...string) ast.InlineNode { text := strings.Join(msgWords, " ") if ref != nil { text += ": " + ref.String() + "." } ln := &ast.LiteralNode{ Kind: ast.LiteralInput, Content: []byte(text), } fn := &ast.FormatNode{ Kind: ast.FormatStrong, Inlines: ast.InlineSlice{ln}, } fn.Attrs = fn.Attrs.AddClass("error") return fn } func createEmbeddedNodeLocal(ref *ast.Reference) *ast.EmbedRefNode { ext := path.Ext(ref.Value) if ext != "" && ext[0] == '.' { ext = ext[1:] } pinfo := parser.Get(ext) if pinfo == nil || !pinfo.IsImageFormat { return nil } return &ast.EmbedRefNode{ Ref: ref, Syntax: ext, } } func (e *evaluator) evaluateEmbeddedInline(content []byte, syntax string) ast.InlineSlice { is := parser.ParseInlines(input.NewInput(content), syntax) ast.Walk(e, &is) return is } func (e *evaluator) evaluateEmbeddedZettel(zettel domain.Zettel) *ast.ZettelNode { zn := parser.ParseZettel(e.ctx, zettel, zettel.Meta.GetDefault(api.KeySyntax, ""), e.rtConfig) ast.Walk(e, &zn.Ast) return zn } func findInlineSlice(bs *ast.BlockSlice, fragment string) ast.InlineSlice { if fragment == "" { return firstInlinesToEmbed(*bs) } fs := fragmentSearcher{fragment: fragment} ast.Walk(&fs, bs) return fs.result } func firstInlinesToEmbed(bs ast.BlockSlice) ast.InlineSlice { if ins := bs.FirstParagraphInlines(); ins != nil { return ins } if len(bs) == 0 { return nil } if bn, ok := bs[0].(*ast.BLOBNode); ok { return ast.InlineSlice{&ast.EmbedBLOBNode{ Blob: bn.Blob, Syntax: bn.Syntax, Inlines: bn.Description, }} } return nil } type fragmentSearcher struct { fragment string result ast.InlineSlice } func (fs *fragmentSearcher) Visit(node ast.Node) ast.Visitor { if len(fs.result) > 0 { return nil } switch n := node.(type) { case *ast.BlockSlice: fs.visitBlockSlice(n) case *ast.InlineSlice: fs.visitInlineSlice(n) default: return fs } return nil } func (fs *fragmentSearcher) visitBlockSlice(bs *ast.BlockSlice) { for i, bn := range *bs { if hn, ok := bn.(*ast.HeadingNode); ok && hn.Fragment == fs.fragment { fs.result = (*bs)[i+1:].FirstParagraphInlines() return } ast.Walk(fs, bn) } } func (fs *fragmentSearcher) visitInlineSlice(is *ast.InlineSlice) { for i, in := range *is { if mn, ok := in.(*ast.MarkNode); ok && mn.Fragment == fs.fragment { ris := skipSpaceNodes((*is)[i+1:]) if len(mn.Inlines) > 0 { fs.result = append(ast.InlineSlice{}, mn.Inlines...) fs.result = append(fs.result, &ast.SpaceNode{Lexeme: " "}) fs.result = append(fs.result, ris...) } else { fs.result = ris } return } ast.Walk(fs, in) } } func skipSpaceNodes(ins ast.InlineSlice) ast.InlineSlice { for i, in := range ins { switch in.(type) { case *ast.SpaceNode: case *ast.BreakNode: default: return ins[i:] } } return nil } |
Added evaluator/list.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 | //----------------------------------------------------------------------------- // Copyright (c) 2022-present Detlef Stern // // This file is part of Zettelstore. // // Zettelstore is licensed under the latest version of the EUPL (European Union // Public License). Please see file LICENSE.txt for your rights and obligations // under this license. //----------------------------------------------------------------------------- package evaluator import ( "bytes" "context" "math" "sort" "strconv" "strings" "zettelstore.de/c/api" "zettelstore.de/c/attrs" "zettelstore.de/z/ast" "zettelstore.de/z/config" "zettelstore.de/z/domain/meta" "zettelstore.de/z/encoding/atom" "zettelstore.de/z/encoding/rss" "zettelstore.de/z/parser" "zettelstore.de/z/query" ) // QueryAction transforms a list of metadata according to query actions into a AST nested list. func QueryAction(ctx context.Context, q *query.Query, ml []*meta.Meta, rtConfig config.Config) ast.BlockNode { ap := actionPara{ ctx: ctx, q: q, ml: ml, kind: ast.NestedListUnordered, min: -1, max: -1, title: rtConfig.GetSiteName(), } actions := q.Actions() if len(actions) == 0 { return ap.createBlockNodeMeta("") } acts := make([]string, 0, len(actions)) for i, act := range actions { if strings.HasPrefix(act, "N") { ap.kind = ast.NestedListOrdered continue } if strings.HasPrefix(act, "MIN") { if num, err := strconv.Atoi(act[3:]); err == nil && num > 0 { ap.min = num continue } } if strings.HasPrefix(act, "MAX") { if num, err := strconv.Atoi(act[3:]); err == nil && num > 0 { ap.max = num continue } } if act == "TITLE" && i+1 < len(actions) { ap.title = strings.Join(actions[i+1:], " ") break } acts = append(acts, act) } var firstUnknownKey string for _, act := range acts { switch act { case "ATOM": return ap.createBlockNodeAtom(rtConfig) case "RSS": return ap.createBlockNodeRSS(rtConfig) case "KEYS": return ap.createBlockNodeMetaKeys() } key := strings.ToLower(act) switch meta.Type(key) { case meta.TypeWord: return ap.createBlockNodeWord(key) case meta.TypeTagSet: return ap.createBlockNodeTagSet(key) } if firstUnknownKey == "" { firstUnknownKey = key } } return ap.createBlockNodeMeta(firstUnknownKey) } type actionPara struct { ctx context.Context q *query.Query ml []*meta.Meta kind ast.NestedListKind min int max int title string } func (ap *actionPara) createBlockNodeWord(key string) ast.BlockNode { var buf bytes.Buffer ccs, bufLen := ap.prepareCatAction(key, &buf) if len(ccs) == 0 { return nil } items := make([]ast.ItemSlice, 0, len(ccs)) ccs.SortByName() for _, cat := range ccs { buf.WriteString(cat.Name) items = append(items, ast.ItemSlice{ast.CreateParaNode(&ast.LinkNode{ Attrs: nil, Ref: ast.ParseReference(buf.String()), Inlines: ast.InlineSlice{&ast.TextNode{Text: cat.Name}}, })}) buf.Truncate(bufLen) } return &ast.NestedListNode{ Kind: ap.kind, Items: items, Attrs: nil, } } func (ap *actionPara) createBlockNodeTagSet(key string) ast.BlockNode { var buf bytes.Buffer ccs, bufLen := ap.prepareCatAction(key, &buf) if len(ccs) == 0 { return nil } ccs.SortByCount() ccs = ap.limitTags(ccs) countMap := ap.calcFontSizes(ccs) para := make(ast.InlineSlice, 0, len(ccs)) ccs.SortByName() for i, cat := range ccs { if i > 0 { para = append(para, &ast.SpaceNode{Lexeme: " "}) } buf.WriteString(cat.Name) para = append(para, &ast.LinkNode{ Attrs: countMap[cat.Count], Ref: ast.ParseReference(buf.String()), Inlines: ast.InlineSlice{ &ast.TextNode{Text: cat.Name}, }, }, &ast.FormatNode{ Kind: ast.FormatSuper, Attrs: nil, Inlines: ast.InlineSlice{&ast.TextNode{Text: strconv.Itoa(cat.Count)}}, }, ) buf.Truncate(bufLen) } return &ast.ParaNode{Inlines: para} } func (ap *actionPara) limitTags(ccs meta.CountedCategories) meta.CountedCategories { if min, max := ap.min, ap.max; min > 0 || max > 0 { if min < 0 { min = ccs[len(ccs)-1].Count } if max < 0 { max = ccs[0].Count } if ccs[len(ccs)-1].Count < min || max < ccs[0].Count { temp := make(meta.CountedCategories, 0, len(ccs)) for _, cat := range ccs { if min <= cat.Count && cat.Count <= max { temp = append(temp, cat) } } return temp } } return ccs } func (ap *actionPara) createBlockNodeMetaKeys() ast.BlockNode { arr := make(meta.Arrangement, 128) for _, m := range ap.ml { for k := range m.Map() { arr[k] = append(arr[k], m) } } if len(arr) == 0 { return nil } ccs := arr.Counted() ccs.SortByName() var buf bytes.Buffer bufLen := ap.prepareSimpleQuery(&buf) items := make([]ast.ItemSlice, 0, len(ccs)) for _, cat := range ccs { buf.WriteString(cat.Name) buf.WriteString(api.ExistOperator) q1 := buf.String() buf.Truncate(bufLen) buf.WriteString(api.ActionSeparator) buf.WriteString(cat.Name) q2 := buf.String() buf.Truncate(bufLen) items = append(items, ast.ItemSlice{ast.CreateParaNode( &ast.LinkNode{ Attrs: nil, Ref: ast.ParseReference(q1), Inlines: ast.InlineSlice{&ast.TextNode{Text: cat.Name}}, }, &ast.SpaceNode{Lexeme: " "}, &ast.TextNode{Text: "(" + strconv.Itoa(cat.Count) + ", "}, &ast.LinkNode{ Attrs: nil, Ref: ast.ParseReference(q2), Inlines: ast.InlineSlice{&ast.TextNode{Text: "values"}}, }, &ast.TextNode{Text: ")"}, )}) } return &ast.NestedListNode{ Kind: ap.kind, Items: items, Attrs: nil, } } func (ap *actionPara) createBlockNodeMeta(key string) ast.BlockNode { if len(ap.ml) == 0 { return nil } items := make([]ast.ItemSlice, 0, len(ap.ml)) for _, m := range ap.ml { if key != "" { if _, found := m.Get(key); !found { continue } } items = append(items, ast.ItemSlice{ast.CreateParaNode(&ast.LinkNode{ Attrs: nil, Ref: ast.ParseReference(m.Zid.String()), Inlines: parser.ParseSpacedText(m.GetTitle()), })}) } return &ast.NestedListNode{ Kind: ap.kind, Items: items, Attrs: nil, } } func (ap *actionPara) prepareCatAction(key string, buf *bytes.Buffer) (meta.CountedCategories, int) { if len(ap.ml) == 0 { return nil, 0 } ccs := meta.CreateArrangement(ap.ml, key).Counted() if len(ccs) == 0 { return nil, 0 } ap.prepareSimpleQuery(buf) buf.WriteString(key) buf.WriteString(api.SearchOperatorHas) bufLen := buf.Len() return ccs, bufLen } func (ap *actionPara) prepareSimpleQuery(buf *bytes.Buffer) int { sea := ap.q.Clone() sea.RemoveActions() buf.WriteString(ast.QueryPrefix) sea.Print(buf) if buf.Len() > len(ast.QueryPrefix) { buf.WriteByte(' ') } return buf.Len() } const fontSizes = 6 // Must be the number of CSS classes zs-font-size-* in base.css const fontSizes64 = float64(fontSizes) func (*actionPara) calcFontSizes(ccs meta.CountedCategories) map[int]attrs.Attributes { var fsAttrs [fontSizes]attrs.Attributes var a attrs.Attributes for i := 0; i < fontSizes; i++ { fsAttrs[i] = a.AddClass("zs-font-size-" + strconv.Itoa(i)) } countMap := make(map[int]int, len(ccs)) for _, cat := range ccs { countMap[cat.Count]++ } countList := make([]int, 0, len(countMap)) for count := range countMap { countList = append(countList, count) } sort.Ints(countList) result := make(map[int]attrs.Attributes, len(countList)) if len(countList) <= fontSizes { // If we have less different counts, center them inside the fsAttrs vector. curSize := (fontSizes - len(countList)) / 2 for _, count := range countList { result[count] = fsAttrs[curSize] curSize++ } return result } // Idea: the number of occurences for a specific count is substracted from a budget. total := float64(len(ccs)) curSize := 0 budget := calcBudget(total, 0.0) for _, count := range countList { result[count] = fsAttrs[curSize] cc := float64(countMap[count]) total -= cc budget -= cc if budget < 1 { curSize++ if curSize >= fontSizes { curSize = fontSizes budget = 0.0 } else { budget = calcBudget(total, float64(curSize)) } } } return result } func calcBudget(total, curSize float64) float64 { return math.Round(total / (fontSizes64 - curSize)) } func (ap *actionPara) createBlockNodeRSS(cfg config.Config) ast.BlockNode { var rssConfig rss.Configuration rssConfig.Setup(ap.ctx, cfg) rssConfig.Title = ap.title data := rssConfig.Marshal(ap.q, ap.ml) return &ast.VerbatimNode{ Kind: ast.VerbatimProg, Attrs: attrs.Attributes{"lang": "xml"}, Content: data, } } func (ap *actionPara) createBlockNodeAtom(cfg config.Config) ast.BlockNode { var atomConfig atom.Configuration atomConfig.Setup(cfg) atomConfig.Title = ap.title data := atomConfig.Marshal(ap.q, ap.ml) return &ast.VerbatimNode{ Kind: ast.VerbatimProg, Attrs: attrs.Attributes{"lang": "xml"}, Content: data, } } |
Added evaluator/metadata.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 | //----------------------------------------------------------------------------- // Copyright (c) 2021-present Detlef Stern // // This file is part of Zettelstore. // // Zettelstore is licensed under the latest version of the EUPL (European Union // Public License). Please see file LICENSE.txt for your rights and obligations // under this license. //----------------------------------------------------------------------------- package evaluator import ( "zettelstore.de/z/ast" "zettelstore.de/z/domain/meta" ) func evaluateMetadata(m *meta.Meta) ast.BlockSlice { descrlist := &ast.DescriptionListNode{} for _, p := range m.Pairs() { descrlist.Descriptions = append( descrlist.Descriptions, getMetadataDescription(p.Key, p.Value)) } return ast.BlockSlice{descrlist} } func getMetadataDescription(key, value string) ast.Description { is := convertMetavalueToInlineSlice(value, meta.Type(key)) return ast.Description{ Term: ast.InlineSlice{&ast.TextNode{Text: key}}, Descriptions: []ast.DescriptionSlice{{&ast.ParaNode{Inlines: is}}}, } } func convertMetavalueToInlineSlice(value string, dt *meta.DescriptionType) ast.InlineSlice { var sliceData []string if dt.IsSet { sliceData = meta.ListFromValue(value) if len(sliceData) == 0 { return nil } } else { sliceData = []string{value} } var makeLink bool switch dt { case meta.TypeID, meta.TypeIDSet: makeLink = true } result := make(ast.InlineSlice, 0, 2*len(sliceData)-1) for i, val := range sliceData { if i > 0 { result = append(result, &ast.SpaceNode{Lexeme: " "}) } tn := &ast.TextNode{Text: val} if makeLink { result = append(result, &ast.LinkNode{ Ref: ast.ParseReference(val), Inlines: ast.InlineSlice{tn}, }) } else { result = append(result, tn) } } return result } |
Changes to go.mod.
1 2 | module zettelstore.de/z | | > > | > | | | | < < < < | | | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 | module zettelstore.de/z go 1.19 require ( codeberg.org/t73fde/sxhtml v0.0.0-20230317170051-24321195e197 codeberg.org/t73fde/sxpf v0.0.0-20230319111333-7de220f3b475 github.com/fsnotify/fsnotify v1.6.0 github.com/pascaldekloe/jwt v1.12.0 github.com/yuin/goldmark v1.5.4 golang.org/x/crypto v0.7.0 golang.org/x/term v0.6.0 golang.org/x/text v0.8.0 zettelstore.de/c v0.11.0 ) require golang.org/x/sys v0.6.0 // indirect |
Changes to go.sum.
|
| > > > > | | > > | | | | > | | | | | | < < < < < < < | < | | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 | codeberg.org/t73fde/sxhtml v0.0.0-20230317170051-24321195e197 h1:6kX7TY25agLFlHNvByO1Jc3GrBA7mu7aOa8tCOniUew= codeberg.org/t73fde/sxhtml v0.0.0-20230317170051-24321195e197/go.mod h1:Dp3EwBSsE3TvdPw9QZ4Wm25ZragluVT2OayRFRiq6jk= codeberg.org/t73fde/sxpf v0.0.0-20230319111333-7de220f3b475 h1:0OTzV3FYY/Y7YsaVaSzF4Wd17pXzdH6DaSvMeqteJc4= codeberg.org/t73fde/sxpf v0.0.0-20230319111333-7de220f3b475/go.mod h1:iSbMygOmtRQYp8pryNKYzRuMibYDSR80smU2b6qm1bc= github.com/fsnotify/fsnotify v1.6.0 h1:n+5WquG0fcWoWp6xPWfHdbskMCQaFnG6PfBrh1Ky4HY= github.com/fsnotify/fsnotify v1.6.0/go.mod h1:sl3t1tCWJFWoRz9R8WJCbQihKKwmorjAbSClcnxKAGw= github.com/pascaldekloe/jwt v1.12.0 h1:imQSkPOtAIBAXoKKjL9ZVJuF/rVqJ+ntiLGpLyeqMUQ= github.com/pascaldekloe/jwt v1.12.0/go.mod h1:LiIl7EwaglmH1hWThd/AmydNCnHf/mmfluBlNqHbk8U= github.com/yuin/goldmark v1.5.4 h1:2uY/xC0roWy8IBEGLgB1ywIoEJFGmRrX21YQcvGZzjU= github.com/yuin/goldmark v1.5.4/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY= golang.org/x/crypto v0.7.0 h1:AvwMYaRytfdeVt3u6mLaxYtErKYjxA2OXjJ1HHq6t3A= golang.org/x/crypto v0.7.0/go.mod h1:pYwdfH91IfpZVANVyUOhSIPZaFoJGxTFbZhFTx+dXZU= golang.org/x/sys v0.0.0-20220908164124-27713097b956/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.6.0 h1:MVltZSvRTcU2ljQOhs94SXPftV6DCNnZViHeQps87pQ= golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/term v0.6.0 h1:clScbb1cHjoCkyRbWwBEUZ5H/tIFu5TAXIqaZD0Gcjw= golang.org/x/term v0.6.0/go.mod h1:m6U89DPEgQRMq3DNkDClhWw02AUbt2daBVO4cn4Hv9U= golang.org/x/text v0.8.0 h1:57P1ETyNKtuIjB4SRd15iJxuhj8Gc416Y78H3qgMh68= golang.org/x/text v0.8.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8= zettelstore.de/c v0.11.0 h1:eD1og+5R2DC3po8JEYUKldONxo69uIKm0jmbNH+NfZI= zettelstore.de/c v0.11.0/go.mod h1:aYfHJ/ipU9BvYVBGVwOyfFjuGpf29Pf5Xn+pjcutR6E= |
Added input/entity.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 | //----------------------------------------------------------------------------- // Copyright (c) 2022-present Detlef Stern // // This file is part of Zettelstore. // // Zettelstore is licensed under the latest version of the EUPL (European Union // Public License). Please see file LICENSE.txt for your rights and obligations // under this license. //----------------------------------------------------------------------------- package input import ( "html" "unicode" ) // ScanEntity scans either a named or a numbered entity and returns it as a string. // // For numbered entities (like { or ģ) html.UnescapeString returns // sometimes other values as expected, if the number is not well-formed. This // may happen because of some strange HTML parsing rules. But these do not // apply to Zettelmarkup. Therefore, I parse the number here in the code. func (inp *Input) ScanEntity() (res string, success bool) { if inp.Ch != '&' { return "", false } pos := inp.Pos inp.Next() if inp.Ch == '#' { inp.Next() if inp.Ch == 'x' || inp.Ch == 'X' { return inp.scanEntityBase16() } return inp.scanEntityBase10() } return inp.scanEntityNamed(pos) } func (inp *Input) scanEntityBase16() (string, bool) { inp.Next() if inp.Ch == ';' { return "", false } code := 0 for { switch ch := inp.Ch; ch { case ';': inp.Next() if r := rune(code); isValidEntity(r) { return string(r), true } return "", false case '0', '1', '2', '3', '4', '5', '6', '7', '8', '9': code = 16*code + int(ch-'0') case 'a', 'b', 'c', 'd', 'e', 'f': code = 16*code + int(ch-'a'+10) case 'A', 'B', 'C', 'D', 'E', 'F': code = 16*code + int(ch-'A'+10) default: return "", false } if code > unicode.MaxRune { return "", false } inp.Next() } } func (inp *Input) scanEntityBase10() (string, bool) { // Base 10 code if inp.Ch == ';' { return "", false } code := 0 for { switch ch := inp.Ch; ch { case ';': inp.Next() if r := rune(code); isValidEntity(r) { return string(r), true } return "", false case '0', '1', '2', '3', '4', '5', '6', '7', '8', '9': code = 10*code + int(ch-'0') default: return "", false } if code > unicode.MaxRune { return "", false } inp.Next() } } func (inp *Input) scanEntityNamed(pos int) (string, bool) { for { switch inp.Ch { case EOS, '\n', '\r', '&': return "", false case ';': inp.Next() es := string(inp.Src[pos:inp.Pos]) ues := html.UnescapeString(es) if es == ues { return "", false } return ues, true default: inp.Next() } } } // isValidEntity checks if the given code is valid for an entity. // // According to https://html.spec.whatwg.org/multipage/syntax.html#character-references // ""The numeric character reference forms described above are allowed to reference any code point // excluding U+000D CR, noncharacters, and controls other than ASCII whitespace."" func isValidEntity(r rune) bool { // No C0 control and no "code point in the range U+007F DELETE to U+009F APPLICATION PROGRAM COMMAND, inclusive." if r < ' ' || ('\u007f' <= r && r <= '\u009f') { return false } // If below any noncharacter code point, return true // // See: https://infra.spec.whatwg.org/#noncharacter if r < '\ufdd0' { return true } // First range of noncharacter code points: "(...) in the range U+FDD0 to U+FDEF, inclusive" if r <= '\ufdef' { return false } // Other noncharacter code points: switch r { case '\uFFFE', '\uFFFF', '\U0001FFFE', '\U0001FFFF', '\U0002FFFE', '\U0002FFFF', '\U0003FFFE', '\U0003FFFF', '\U0004FFFE', '\U0004FFFF', '\U0005FFFE', '\U0005FFFF', '\U0006FFFE', '\U0006FFFF', '\U0007FFFE', '\U0007FFFF', '\U0008FFFE', '\U0008FFFF', '\U0009FFFE', '\U0009FFFF', '\U000AFFFE', '\U000AFFFF', '\U000BFFFE', '\U000BFFFF', '\U000CFFFE', '\U000CFFFF', '\U000DFFFE', '\U000DFFFF', '\U000EFFFE', '\U000EFFFF', '\U000FFFFE', '\U000FFFFF', '\U0010FFFE', '\U0010FFFF': return false } return true } |
Added input/entity_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 | //----------------------------------------------------------------------------- // Copyright (c) 2020-present Detlef Stern // // This file is part of Zettelstore. // // Zettelstore is licensed under the latest version of the EUPL (European Union // Public License). Please see file LICENSE.txt for your rights and obligations // under this license. //----------------------------------------------------------------------------- package input_test import ( "testing" "zettelstore.de/z/input" ) func TestScanEntity(t *testing.T) { t.Parallel() var testcases = []struct { text string exp string }{ {"", ""}, {"a", ""}, {"&", "&"}, {"!", "!"}, {"3", "3"}, {""", "\""}, } for id, tc := range testcases { inp := input.NewInput([]byte(tc.text)) got, ok := inp.ScanEntity() if !ok { if tc.exp != "" { t.Errorf("ID=%d, text=%q: expected error, but got %q", id, tc.text, got) } if inp.Pos != 0 { t.Errorf("ID=%d, text=%q: input position advances to %d", id, tc.text, inp.Pos) } continue } if tc.exp != got { t.Errorf("ID=%d, text=%q: expected %q, but got %q", id, tc.text, tc.exp, got) } } } func TestScanIllegalEntity(t *testing.T) { t.Parallel() testcases := []string{"", "a", "& Input →", "	", ""} for i, tc := range testcases { inp := input.NewInput([]byte(tc)) got, ok := inp.ScanEntity() if ok { t.Errorf("%d: scanning %q was unexpected successful, got %q", i, tc, got) continue } } } |
Added input/input.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 | //----------------------------------------------------------------------------- // Copyright (c) 2020-present Detlef Stern // // This file is part of Zettelstore. // // Zettelstore is licensed under the latest version of the EUPL (European Union // Public License). Please see file LICENSE.txt for your rights and obligations // under this license. //----------------------------------------------------------------------------- // Package input provides an abstraction for data to be read. package input import "unicode/utf8" // Input is an abstract input source type Input struct { // Read-only, will never change Src []byte // The source string // Read-only, will change Ch rune // current character Pos int // character position in src readPos int // reading position (position after current character) } // NewInput creates a new input source. func NewInput(src []byte) *Input { inp := &Input{Src: src} inp.Next() return inp } // EOS = End of source const EOS = rune(-1) // Next reads the next rune into inp.Ch and returns it too. func (inp *Input) Next() rune { if inp.readPos >= len(inp.Src) { inp.Pos = len(inp.Src) inp.Ch = EOS return EOS } inp.Pos = inp.readPos r, w := rune(inp.Src[inp.readPos]), 1 if r >= utf8.RuneSelf { r, w = utf8.DecodeRune(inp.Src[inp.readPos:]) } inp.readPos += w inp.Ch = r return r } // Peek returns the rune following the most recently read rune without // advancing. If end-of-source was already found peek returns EOS. func (inp *Input) Peek() rune { return inp.PeekN(0) } // PeekN returns the n-th rune after the most recently read rune without // advancing. If end-of-source was already found peek returns EOS. func (inp *Input) PeekN(n int) rune { pos := inp.readPos + n if pos < len(inp.Src) { r := rune(inp.Src[pos]) if r >= utf8.RuneSelf { r, _ = utf8.DecodeRune(inp.Src[pos:]) } if r == '\t' { return ' ' } return r } return EOS } // Accept checks if the given string is a prefix of the text to be parsed. // If successful, advance position and current character. // String must only contain bytes < 128. // If not successful, everything remains as it is. func (inp *Input) Accept(s string) bool { pos := inp.Pos remaining := len(inp.Src) - pos if s == "" || len(s) > remaining { return false } // According to internal documentation of bytes.Equal, the string() will not allocate any memory. if readPos := pos + len(s); s == string(inp.Src[pos:readPos]) { inp.readPos = readPos inp.Next() return true } return false } // IsEOLEOS returns true if char is either EOS or EOL. func IsEOLEOS(ch rune) bool { switch ch { case EOS, '\n', '\r': return true } return false } // EatEOL transforms both "\r" and "\r\n" into "\n". func (inp *Input) EatEOL() { switch inp.Ch { case '\r': if inp.Peek() == '\n' { inp.Next() } inp.Ch = '\n' inp.Next() case '\n': inp.Next() } } // SetPos allows to reset the read position. func (inp *Input) SetPos(pos int) { if inp.Pos != pos { inp.readPos = pos inp.Next() } } // SkipToEOL reads until the next end-of-line. func (inp *Input) SkipToEOL() { for { switch inp.Ch { case EOS, '\n', '\r': return } inp.Next() } } // ScanLineContent reads the reaining input stream and interprets it as lines of text. func (inp *Input) ScanLineContent() []byte { result := make([]byte, 0, len(inp.Src)-inp.Pos+1) for { inp.EatEOL() posL := inp.Pos if inp.Ch == EOS { return result } inp.SkipToEOL() if len(result) > 0 { result = append(result, '\n') } result = append(result, inp.Src[posL:inp.Pos]...) } } |
Added input/input_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 | //----------------------------------------------------------------------------- // Copyright (c) 2020-present Detlef Stern // // This file is part of Zettelstore. // // Zettelstore is licensed under the latest version of the EUPL (European Union // Public License). Please see file LICENSE.txt for your rights and obligations // under this license. //----------------------------------------------------------------------------- // Package input_test provides some unit-tests for reading data. package input_test import ( "testing" "zettelstore.de/z/input" ) func TestEatEOL(t *testing.T) { t.Parallel() inp := input.NewInput(nil) inp.EatEOL() if inp.Ch != input.EOS { t.Errorf("No EOS found: %q", inp.Ch) } if inp.Pos != 0 { t.Errorf("Pos != 0: %d", inp.Pos) } inp = input.NewInput([]byte("ABC")) if inp.Ch != 'A' { t.Errorf("First ch != 'A', got %q", inp.Ch) } inp.EatEOL() if inp.Ch != 'A' { t.Errorf("First ch != 'A', got %q", inp.Ch) } } func TestAccept(t *testing.T) { t.Parallel() testcases := []struct { accept string src string acc bool exp rune }{ {"", "", false, input.EOS}, {"AB", "abc", false, 'a'}, {"AB", "ABC", true, 'C'}, {"AB", "AB", true, input.EOS}, {"AB", "A", false, 'A'}, } for i, tc := range testcases { inp := input.NewInput([]byte(tc.src)) acc := inp.Accept(tc.accept) if acc != tc.acc { t.Errorf("%d: %q.Accept(%q) == %v, but got %v", i, tc.src, tc.accept, tc.acc, acc) } if got := inp.Ch; tc.exp != got { t.Errorf("%d: %q.Accept(%q) should result in run %v, but got %v", i, tc.src, tc.accept, tc.exp, got) } } } |
Added input/runes.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 | //----------------------------------------------------------------------------- // Copyright (c) 2020-present Detlef Stern // // This file is part of Zettelstore. // // Zettelstore is licensed under the latest version of the EUPL (European Union // Public License). Please see file LICENSE.txt for your rights and obligations // under this license. //----------------------------------------------------------------------------- package input import "unicode" // IsSpace returns true if rune is a whitespace. func IsSpace(ch rune) bool { switch ch { case ' ', '\t': return true case '\n', '\r', EOS: return false } return unicode.IsSpace(ch) } |
Deleted internal/ast/ast.go.
|
| < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < |
Deleted internal/ast/block.go.
|
| < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < |
Deleted internal/ast/inline.go.
|
| < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < |
Deleted internal/ast/ref.go.
|
| < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < |
Deleted internal/ast/ref_test.go.
|
| < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < |
Deleted internal/ast/sztrans/sztrans.go.
|
| < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < |
Deleted internal/ast/walk.go.
|
| < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < |
Deleted internal/ast/walk_test.go.
|
| < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < |
Deleted internal/auth/auth.go.
|
| < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < |
Deleted internal/auth/cred/cred.go.
|
| < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < |
Deleted internal/auth/impl/digest.go.
|
| < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < |
Deleted internal/auth/impl/impl.go.
|
| < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < |
Deleted internal/auth/policy/anon.go.
|
| < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < |
Deleted internal/auth/policy/box.go.
|
| < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < |
Deleted internal/auth/policy/default.go.
|
| < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < |
Deleted internal/auth/policy/owner.go.
|
| < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < |
Deleted internal/auth/policy/policy.go.
|
| < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < |
Deleted internal/auth/policy/policy_test.go.
|
| < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < |
Deleted internal/auth/policy/readonly.go.
|
| < < < < < < < < < < < < < < < < < < < < < < < < |
Deleted internal/box/box.go.
|
| < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < |
Deleted internal/box/compbox/compbox.go.
|
| < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < |
Deleted internal/box/compbox/config.go.
|
| < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < |
Deleted internal/box/compbox/keys.go.
|
| < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < |
Deleted internal/box/compbox/log.go.
|
| < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < |
Deleted internal/box/compbox/manager.go.
|
| < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < |
Deleted internal/box/compbox/memory.go.
|
| < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < |
Deleted internal/box/compbox/parser.go.
|
| < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < |
Deleted internal/box/compbox/sx.go.
|
| < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < |
Deleted internal/box/compbox/version.go.
|
| < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < |
Deleted internal/box/constbox/base.css.
|
| < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < |
Deleted internal/box/constbox/base.sxn.
|
| < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < |
Deleted internal/box/constbox/constbox.go.
|
| < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < |
Deleted internal/box/constbox/contributors.zettel.
|
| < < < < < < < < |
Deleted internal/box/constbox/delete.sxn.
|
| < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < |
Deleted internal/box/constbox/dependencies.zettel.
|
| < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < |
Deleted internal/box/constbox/emoji_spin.gif.
cannot compute difference between binary files
Deleted internal/box/constbox/error.sxn.
|
| < < < < < < < < < < < < < < < < < |
Deleted internal/box/constbox/form.sxn.
|
| < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < |
Deleted internal/box/constbox/home.zettel.
|
| < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < |
Deleted internal/box/constbox/info.sxn.
|
| < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < |
Deleted internal/box/constbox/license.txt.
|
| < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < |
Deleted internal/box/constbox/listzettel.sxn.
|
| < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < |
Deleted internal/box/constbox/login.sxn.
|
| < < < < < < < < < < < < < < < < < < < < < < < < < < < |
Deleted internal/box/constbox/menu_lists.zettel.
|
| < < < < < < < |
Deleted internal/box/constbox/menu_new.zettel.
|
| < < < < < < |
Deleted internal/box/constbox/prelude.sxn.
|
| < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < |
Deleted internal/box/constbox/roleconfiguration.zettel.
|
| < < < < < < < < < < < < < < < < < < < < < < |
Deleted internal/box/constbox/rolerole.zettel.
|
| < < < < < < < < < < |
Deleted internal/box/constbox/roletag.zettel.
|
| < < < < < < |
Deleted internal/box/constbox/rolezettel.zettel.
|
| < < < < < < < |
Deleted internal/box/constbox/start.sxn.
|
| < < < < < < < < < < < < < < < < < |
Deleted internal/box/constbox/wuicode.sxn.
|
| < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < |
Deleted internal/box/constbox/zettel.sxn.
|
| < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < |
Deleted internal/box/dirbox/dirbox.go.
|
| < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < |
Deleted internal/box/dirbox/dirbox_test.go.
|
| < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < |
Deleted internal/box/dirbox/service.go.
|
| < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < |
Deleted internal/box/filebox/filebox.go.
|
| < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < |
Deleted internal/box/filebox/zipbox.go.
|
| < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < |
Deleted internal/box/helper.go.
|
| < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < |
Deleted internal/box/manager/anteroom.go.
|
| < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < |
Deleted internal/box/manager/anteroom_test.go.
|
| < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < |
Deleted internal/box/manager/box.go.
|
| < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < |
Deleted internal/box/manager/collect.go.
|
| < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < |
Deleted internal/box/manager/enrich.go.
|
| < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < |
Deleted internal/box/manager/indexer.go.
|
| < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < |
Deleted internal/box/manager/manager.go.
|
| < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < |
Deleted internal/box/manager/mapstore/mapstore.go.
|
| < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < |
Deleted internal/box/manager/store/store.go.
|
| < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < |
Deleted internal/box/manager/store/wordset.go.
|
| < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < |
Deleted internal/box/manager/store/wordset_test.go.
|
| < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < |
Deleted internal/box/manager/store/zettel.go.
|
| < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < |
Deleted internal/box/membox/membox.go.
|
| < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < |
Deleted internal/box/notify/directory.go.
|
| < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < |
Deleted internal/box/notify/directory_test.go.
|
| < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < |
Deleted internal/box/notify/entry.go.
|
| < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < |
Deleted internal/box/notify/fsdir.go.
|
| < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < |
Deleted internal/box/notify/helper.go.
|
| < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < |
Deleted internal/box/notify/notify.go.
|
| < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < |
Deleted internal/box/notify/simpledir.go.
|
| < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < |
Deleted internal/collect/collect.go.
|
| < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < |
Deleted internal/collect/collect_test.go.
|
| < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < |
Deleted internal/collect/order.go.
|
| < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < |
Deleted internal/config/config.go.
|
| < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < |
Deleted internal/encoder/encoder.go.
|
| < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < |
Deleted internal/encoder/encoder_blob_test.go.
|
| < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < |
Deleted internal/encoder/encoder_block_test.go.
|
| < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < |
Deleted internal/encoder/encoder_inline_test.go.
|
| < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < |
Deleted internal/encoder/encoder_test.go.
|
| < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < |
Deleted internal/encoder/htmlenc/htmlenc.go.
|
| < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < |
Deleted internal/encoder/mdenc/mdenc.go.
|
| < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < |
Deleted internal/encoder/shtmlenc/shtmlenc.go.
|
| < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < |
Deleted internal/encoder/szenc/szenc.go.
|
| < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < |
Deleted internal/encoder/szenc/transform.go.
|
| < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < |
Deleted internal/encoder/textenc/textenc.go.
|
| < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < |
Deleted internal/encoder/write.go.
|
| < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < |
Deleted internal/encoder/zmkenc/zmkenc.go.
|
| < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < |
Deleted internal/evaluator/evaluator.go.
|
| < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < |
Deleted internal/evaluator/list.go.
|
| < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < |
Deleted internal/evaluator/metadata.go.
|
| < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < |
Deleted internal/kernel/impl/auth.go.
|
| < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < |
Deleted internal/kernel/impl/box.go.
|
| < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < |
Deleted internal/kernel/impl/cfg.go.
|
| < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < |
Deleted internal/kernel/impl/cmd.go.
|
| < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < |
Deleted internal/kernel/impl/config.go.
|
| < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < |
Deleted internal/kernel/impl/core.go.
|
| < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < |
Deleted internal/kernel/impl/impl.go.
|
| < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < |
Deleted internal/kernel/impl/log.go.
|
| < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < |
Deleted internal/kernel/impl/server.go.
|
| < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < |
Deleted internal/kernel/impl/web.go.
|
| < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < |
Deleted internal/kernel/kernel.go.
|
| < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < |
Deleted internal/logger/logger.go.
|
| < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < |
Deleted internal/logger/logger_test.go.
|
| < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < |
Deleted internal/logger/message.go.
|
| < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < |
Deleted internal/parser/blob/blob.go.
|
| < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < |
Deleted internal/parser/cleaner/cleaner.go.
|
| < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < |
Deleted internal/parser/draw/ORIG_CONTRIBUTORS.
|
| < < < |
Deleted internal/parser/draw/ORIG_LICENSE.
|
| < < < < < < < < < < < < < < < < < < < < < < |
Deleted internal/parser/draw/canvas.go.
|
| < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < |
Deleted internal/parser/draw/canvas_test.go.
|
| < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < |
Deleted internal/parser/draw/char.go.
|
| < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < |
Deleted internal/parser/draw/draw.go.
|
| < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < |
Deleted internal/parser/draw/draw_test.go.
|
| < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < |
Deleted internal/parser/draw/object.go.
|
| < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < |
Deleted internal/parser/draw/point.go.
|
| < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < |
Deleted internal/parser/draw/svg.go.
|
| < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < |
Deleted internal/parser/draw/svg_test.go.
|
| < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < |
Deleted internal/parser/markdown/markdown.go.
|
| < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < |
Deleted internal/parser/none/none.go.
|
| < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < |
Deleted internal/parser/parser.go.
|
| < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < |
Deleted internal/parser/parser_test.go.
|
| < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < |
Deleted internal/parser/plain/plain.go.
|
| < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < |
Deleted internal/parser/plain/plain_test.go.
|
| < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < |
Deleted internal/parser/zettelmark/zettelmark.go.
|
| < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < |
Deleted internal/parser/zettelmark/zettelmark_fuzz_test.go.
|
| < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < |
Deleted internal/parser/zettelmark/zettelmark_test.go.
|
| < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < |
Deleted internal/query/compiled.go.
|
| < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < |
Deleted internal/query/context.go.
|
| < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < |
Deleted internal/query/parser.go.
|
| < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < |
Deleted internal/query/parser_test.go.
|
| < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < |
Deleted internal/query/print.go.
|
| < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < |
Deleted internal/query/query.go.
|
| < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < |
Deleted internal/query/retrieve.go.
|
| < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < |
Deleted internal/query/select.go.
|
| < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < |
Deleted internal/query/select_test.go.
|
| < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < |
Deleted internal/query/sorter.go.
|
| < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < |
Deleted internal/query/specs.go.
|
| < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < |
Deleted internal/query/unlinked.go.
|
| < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < |
Deleted internal/usecase/authenticate.go.
|
| < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < |
Deleted internal/usecase/create_zettel.go.
|
| < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < |
Deleted internal/usecase/delete_zettel.go.
|
| < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < |
Deleted internal/usecase/evaluate.go.
|
| < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < |
Deleted internal/usecase/get_all_zettel.go.
|
| < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < |
Deleted internal/usecase/get_special_zettel.go.
|
| < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < |
Deleted internal/usecase/get_user.go.
|
| < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < |
Deleted internal/usecase/get_zettel.go.
|
| < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < |
Deleted internal/usecase/lists.go.
|
| < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < |
Deleted internal/usecase/parse_zettel.go.
|
| < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < |
Deleted internal/usecase/query.go.
|
| < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < |
Deleted internal/usecase/refresh.go.
|
| < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < |
Deleted internal/usecase/reindex.go.
|
| < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < |
Deleted internal/usecase/update_zettel.go.
|
| < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < |
Deleted internal/usecase/usecase.go.
|
| < < < < < < < < < < < < < < < |
Deleted internal/usecase/version.go.
|
| < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < |
Deleted internal/web/adapter/adapter.go.
|
| < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < |
Deleted internal/web/adapter/api/api.go.
|
| < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < |
Deleted internal/web/adapter/api/command.go.
|
| < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < |
Deleted internal/web/adapter/api/create_zettel.go.
|
| < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < |
Deleted internal/web/adapter/api/delete_zettel.go.
|
| < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < |
Deleted internal/web/adapter/api/get_data.go.
|
| < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < |
Deleted internal/web/adapter/api/get_zettel.go.
|
| < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < |
Deleted internal/web/adapter/api/login.go.
|
| < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < |
Deleted internal/web/adapter/api/query.go.
|
| < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < |
Deleted internal/web/adapter/api/request.go.
|
| < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < |
Deleted internal/web/adapter/api/response.go.
|
| < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < |
Deleted internal/web/adapter/api/update_zettel.go.
|
| < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < |
Deleted internal/web/adapter/errors.go.
|
| < < < < < < < < < < < < < < < < < < < < < < < < < < |
Deleted internal/web/adapter/request.go.
|
| < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < |
Deleted internal/web/adapter/response.go.
|
| < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < |
Deleted internal/web/adapter/webui/const.go.
|
| < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < |
Deleted internal/web/adapter/webui/create_zettel.go.
|
| < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < |
Deleted internal/web/adapter/webui/delete_zettel.go.
|
| < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < |
Deleted internal/web/adapter/webui/edit_zettel.go.
|
| < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < |
Deleted internal/web/adapter/webui/favicon.go.
|
| < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < |
Deleted internal/web/adapter/webui/forms.go.
|
| < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < |
Deleted internal/web/adapter/webui/forms_test.go.
|
| < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < |
Deleted internal/web/adapter/webui/get_info.go.
|
| < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < |
Deleted internal/web/adapter/webui/get_zettel.go.
|
| < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < |
Deleted internal/web/adapter/webui/goaction.go.
|
| < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < |
Deleted internal/web/adapter/webui/home.go.
|
| < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < |
Deleted internal/web/adapter/webui/htmlgen.go.
|
| < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < |
Deleted internal/web/adapter/webui/htmlmeta.go.
|
| < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < |
Deleted internal/web/adapter/webui/lists.go.
|
| < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < |
Deleted internal/web/adapter/webui/login.go.
|
| < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < |
Deleted internal/web/adapter/webui/meta.go.
|
| < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < |
Deleted internal/web/adapter/webui/meta_test.go.
|
| < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < |
Deleted internal/web/adapter/webui/response.go.
|
| < < < < < < < < < < < < < < < < < < < < < < < < < < |
Deleted internal/web/adapter/webui/sxn_code.go.
|
| < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < |
Deleted internal/web/adapter/webui/template.go.
|
| < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < |
Deleted internal/web/adapter/webui/webui.go.
|
| < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < |
Deleted internal/web/content/content.go.
|
| < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < |
Deleted internal/web/content/content_test.go.
|
| < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < |
Deleted internal/web/server/impl/http.go.
|
| < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < |
Deleted internal/web/server/impl/impl.go.
|
| < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < |
Deleted internal/web/server/impl/router.go.
|
| < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < |
Deleted internal/web/server/server.go.
|
| < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < |
Deleted internal/zettel/content.go.
|
| < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < |
Deleted internal/zettel/content_test.go.
|
| < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < |
Deleted internal/zettel/zettel.go.
|
| < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < |
Added kernel/impl/auth.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 | //----------------------------------------------------------------------------- // Copyright (c) 2021-present Detlef Stern // // This file is part of Zettelstore. // // Zettelstore is licensed under the latest version of the EUPL (European Union // Public License). Please see file LICENSE.txt for your rights and obligations // under this license. //----------------------------------------------------------------------------- package impl import ( "errors" "sync" "zettelstore.de/z/auth" "zettelstore.de/z/domain/id" "zettelstore.de/z/kernel" "zettelstore.de/z/logger" ) type authService struct { srvConfig mxService sync.RWMutex manager auth.Manager createManager kernel.CreateAuthManagerFunc } var errAlreadySetOwner = errors.New("changing an existing owner not allowed") var errAlreadyROMode = errors.New("system in readonly mode cannot change this mode") func (as *authService) Initialize(logger *logger.Logger) { as.logger = logger as.descr = descriptionMap{ kernel.AuthOwner: { "Owner's zettel id", func(val string) (any, error) { if owner := as.cur[kernel.AuthOwner]; owner != nil && owner != id.Invalid { return nil, errAlreadySetOwner } if val == "" { return id.Invalid, nil } return parseZid(val) }, false, }, kernel.AuthReadonly: { "Readonly mode", func(val string) (any, error) { if ro := as.cur[kernel.AuthReadonly]; ro == true { return nil, errAlreadyROMode } return parseBool(val) }, true, }, } as.next = interfaceMap{ kernel.AuthOwner: id.Invalid, kernel.AuthReadonly: false, } } func (as *authService) GetLogger() *logger.Logger { return as.logger } func (as *authService) Start(*myKernel) error { as.mxService.Lock() defer as.mxService.Unlock() readonlyMode := as.GetNextConfig(kernel.AuthReadonly).(bool) owner := as.GetNextConfig(kernel.AuthOwner).(id.Zid) authMgr, err := as.createManager(readonlyMode, owner) if err != nil { as.logger.Fatal().Err(err).Msg("Unable to create manager") return err } as.logger.Info().Msg("Start Manager") as.manager = authMgr return nil } func (as *authService) IsStarted() bool { as.mxService.RLock() defer as.mxService.RUnlock() return as.manager != nil } func (as *authService) Stop(*myKernel) { as.logger.Info().Msg("Stop Manager") as.mxService.Lock() as.manager = nil as.mxService.Unlock() } func (*authService) GetStatistics() []kernel.KeyValue { return nil } |
Added kernel/impl/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 | //----------------------------------------------------------------------------- // Copyright (c) 2021-present Detlef Stern // // This file is part of Zettelstore. // // Zettelstore is licensed under the latest version of the EUPL (European Union // Public License). Please see file LICENSE.txt for your rights and obligations // under this license. //----------------------------------------------------------------------------- package impl import ( "context" "errors" "fmt" "io" "net/url" "strconv" "sync" "zettelstore.de/z/box" "zettelstore.de/z/kernel" "zettelstore.de/z/logger" ) type boxService struct { srvConfig mxService sync.RWMutex manager box.Manager createManager kernel.CreateBoxManagerFunc } var errInvalidDirType = errors.New("invalid directory type") func (ps *boxService) Initialize(logger *logger.Logger) { ps.logger = logger ps.descr = descriptionMap{ kernel.BoxDefaultDirType: { "Default directory box type", ps.noFrozen(func(val string) (any, error) { switch val { case kernel.BoxDirTypeNotify, kernel.BoxDirTypeSimple: return val, nil } return nil, errInvalidDirType }), true, }, kernel.BoxURIs: { "Box URI", func(val string) (any, error) { uVal, err := url.Parse(val) if err != nil { return nil, err } if uVal.Scheme == "" { uVal.Scheme = "dir" } return uVal, nil }, true, }, } ps.next = interfaceMap{ kernel.BoxDefaultDirType: kernel.BoxDirTypeNotify, } } func (ps *boxService) GetLogger() *logger.Logger { return ps.logger } func (ps *boxService) Start(kern *myKernel) error { boxURIs := make([]*url.URL, 0, 4) for i := 1; ; i++ { u := ps.GetNextConfig(kernel.BoxURIs + strconv.Itoa(i)) if u == nil { break } boxURIs = append(boxURIs, u.(*url.URL)) } ps.mxService.Lock() defer ps.mxService.Unlock() mgr, err := ps.createManager(boxURIs, kern.auth.manager, &kern.cfg) if err != nil { ps.logger.Fatal().Err(err).Msg("Unable to create manager") return err } ps.logger.Info().Str("location", mgr.Location()).Msg("Start Manager") if err = mgr.Start(context.Background()); err != nil { ps.logger.Fatal().Err(err).Msg("Unable to start manager") return err } kern.cfg.setBox(mgr) ps.manager = mgr return nil } func (ps *boxService) IsStarted() bool { ps.mxService.RLock() defer ps.mxService.RUnlock() return ps.manager != nil } func (ps *boxService) Stop(*myKernel) { ps.logger.Info().Msg("Stop Manager") ps.mxService.RLock() mgr := ps.manager ps.mxService.RUnlock() mgr.Stop(context.Background()) ps.mxService.Lock() ps.manager = nil ps.mxService.Unlock() } func (ps *boxService) GetStatistics() []kernel.KeyValue { var st box.Stats ps.mxService.RLock() ps.manager.ReadStats(&st) ps.mxService.RUnlock() return []kernel.KeyValue{ {Key: "Read-only", Value: strconv.FormatBool(st.ReadOnly)}, {Key: "Managed boxes", Value: strconv.Itoa(st.NumManagedBoxes)}, {Key: "Zettel (total)", Value: strconv.Itoa(st.ZettelTotal)}, {Key: "Zettel (indexed)", Value: strconv.Itoa(st.ZettelIndexed)}, {Key: "Last re-index", Value: st.LastReload.Format("2006-01-02 15:04:05 -0700 MST")}, {Key: "Duration last re-index", Value: fmt.Sprintf("%vms", st.DurLastReload.Milliseconds())}, {Key: "Indexes since last re-index", Value: strconv.FormatUint(st.IndexesSinceReload, 10)}, {Key: "Indexed words", Value: strconv.FormatUint(st.IndexedWords, 10)}, {Key: "Indexed URLs", Value: strconv.FormatUint(st.IndexedUrls, 10)}, {Key: "Zettel enrichments", Value: strconv.FormatUint(st.IndexUpdates, 10)}, } } func (ps *boxService) DumpIndex(w io.Writer) { ps.manager.Dump(w) } func (ps *boxService) Refresh() error { ps.mxService.RLock() defer ps.mxService.RUnlock() if ps.manager != nil { return ps.manager.Refresh(context.Background()) } return nil } |
Added kernel/impl/cfg.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 | //----------------------------------------------------------------------------- // Copyright (c) 2021-present Detlef Stern // // This file is part of Zettelstore. // // Zettelstore is licensed under the latest version of the EUPL (European Union // Public License). Please see file LICENSE.txt for your rights and obligations // under this license. //----------------------------------------------------------------------------- package impl import ( "context" "errors" "fmt" "strconv" "strings" "sync" "zettelstore.de/c/api" "zettelstore.de/z/box" "zettelstore.de/z/config" "zettelstore.de/z/domain/id" "zettelstore.de/z/domain/meta" "zettelstore.de/z/kernel" "zettelstore.de/z/logger" "zettelstore.de/z/web/server" ) type configService struct { srvConfig mxService sync.RWMutex orig *meta.Meta } // Predefined Metadata keys for runtime configuration // See: https://zettelstore.de/manual/h/00001004020000 const ( keyDefaultCopyright = "default-copyright" keyDefaultLicense = "default-license" keyDefaultVisibility = "default-visibility" keyExpertMode = "expert-mode" keyMaxTransclusions = "max-transclusions" keySiteName = "site-name" keyYAMLHeader = "yaml-header" keyZettelFileSyntax = "zettel-file-syntax" ) var errUnknownVisibility = errors.New("unknown visibility") func (cs *configService) Initialize(logger *logger.Logger) { cs.logger = logger cs.descr = descriptionMap{ keyDefaultCopyright: {"Default copyright", parseString, true}, keyDefaultLicense: {"Default license", parseString, true}, keyDefaultVisibility: { "Default zettel visibility", func(val string) (any, error) { vis := meta.GetVisibility(val) if vis == meta.VisibilityUnknown { return nil, errUnknownVisibility } return vis, nil }, true, }, keyExpertMode: {"Expert mode", parseBool, true}, config.KeyFooterZettel: {"Footer Zettel", parseInvalidZid, true}, config.KeyHomeZettel: {"Home zettel", parseZid, true}, kernel.ConfigInsecureHTML: { "Insecure HTML", cs.noFrozen(func(val string) (any, error) { switch val { case kernel.ConfigSyntaxHTML: return config.SyntaxHTML, nil case kernel.ConfigMarkdownHTML: return config.MarkdownHTML, nil case kernel.ConfigZmkHTML: return config.ZettelmarkupHTML, nil } return config.NoHTML, nil }), true, }, api.KeyLang: {"Language", parseString, true}, keyMaxTransclusions: {"Maximum transclusions", parseInt64, true}, keySiteName: {"Site name", parseString, true}, keyYAMLHeader: {"YAML header", parseBool, true}, keyZettelFileSyntax: { "Zettel file syntax", func(val string) (any, error) { return strings.Fields(val), nil }, true, }, kernel.ConfigSimpleMode: {"Simple mode", cs.noFrozen(parseBool), true}, } cs.next = interfaceMap{ keyDefaultCopyright: "", keyDefaultLicense: "", keyDefaultVisibility: meta.VisibilityLogin, keyExpertMode: false, config.KeyFooterZettel: id.Invalid, config.KeyHomeZettel: id.DefaultHomeZid, kernel.ConfigInsecureHTML: config.NoHTML, api.KeyLang: api.ValueLangEN, keyMaxTransclusions: int64(1024), keySiteName: "Zettelstore", keyYAMLHeader: false, keyZettelFileSyntax: nil, kernel.ConfigSimpleMode: false, } } func (cs *configService) GetLogger() *logger.Logger { return cs.logger } func (cs *configService) Start(*myKernel) error { cs.logger.Info().Msg("Start Service") data := meta.New(id.ConfigurationZid) for _, kv := range cs.GetNextConfigList() { data.Set(kv.Key, kv.Value) } cs.mxService.Lock() cs.orig = data cs.mxService.Unlock() return nil } func (cs *configService) IsStarted() bool { cs.mxService.RLock() defer cs.mxService.RUnlock() return cs.orig != nil } func (cs *configService) Stop(*myKernel) { cs.logger.Info().Msg("Stop Service") cs.mxService.Lock() cs.orig = nil cs.mxService.Unlock() } func (*configService) GetStatistics() []kernel.KeyValue { return nil } func (cs *configService) setBox(mgr box.Manager) { mgr.RegisterObserver(cs.observe) cs.observe(box.UpdateInfo{Box: mgr, Reason: box.OnZettel, Zid: id.ConfigurationZid}) } func (cs *configService) doUpdate(p box.BaseBox) error { m, err := p.GetMeta(context.Background(), cs.orig.Zid) cs.logger.Trace().Err(err).Msg("got config meta") if err != nil { return err } cs.mxService.Lock() for _, pair := range cs.orig.Pairs() { key := pair.Key if val, ok := m.Get(key); ok { cs.SetConfig(key, val) } else if defVal, defFound := cs.orig.Get(key); defFound { cs.SetConfig(key, defVal) } } cs.mxService.Unlock() cs.SwitchNextToCur() // Poor man's restart return nil } func (cs *configService) observe(ci box.UpdateInfo) { if ci.Reason != box.OnZettel || ci.Zid == id.ConfigurationZid { cs.logger.Debug().Uint("reason", uint64(ci.Reason)).Zid(ci.Zid).Msg("observe") go func() { cs.doUpdate(ci.Box) }() } } // --- config.Config func (cs *configService) Get(ctx context.Context, m *meta.Meta, key string) string { if m != nil { if val, found := m.Get(key); found { return val } } if user := server.GetUser(ctx); user != nil { if val, found := user.Get(key); found { return val } } result := cs.GetCurConfig(key) if result == nil { return "" } switch val := result.(type) { case string: return val case bool: if val { return api.ValueTrue } return api.ValueFalse case id.Zid: return val.String() case int: return strconv.Itoa(val) case []string: return strings.Join(val, " ") case meta.Visibility: return val.String() case fmt.Stringer: return val.String() case fmt.GoStringer: return val.GoString() } return fmt.Sprintf("%v", result) } // AddDefaultValues enriches the given meta data with its default values. func (cs *configService) AddDefaultValues(ctx context.Context, m *meta.Meta) *meta.Meta { if cs == nil { return m } result := m cs.mxService.RLock() if _, found := m.Get(api.KeyCopyright); !found { result = updateMeta(result, m, api.KeyCopyright, cs.GetCurConfig(keyDefaultCopyright).(string)) } if _, found := m.Get(api.KeyLang); !found { result = updateMeta(result, m, api.KeyLang, cs.Get(ctx, nil, api.KeyLang)) } if _, found := m.Get(api.KeyLicense); !found { result = updateMeta(result, m, api.KeyLicense, cs.GetCurConfig(keyDefaultLicense).(string)) } if _, found := m.Get(api.KeyVisibility); !found { result = updateMeta(result, m, api.KeyVisibility, cs.GetCurConfig(keyDefaultVisibility).(meta.Visibility).String()) } cs.mxService.RUnlock() return result } func updateMeta(result, m *meta.Meta, key, val string) *meta.Meta { if result == m { result = m.Clone() } result.Set(key, val) return result } func (cs *configService) GetHTMLInsecurity() config.HTMLInsecurity { return cs.GetCurConfig(kernel.ConfigInsecureHTML).(config.HTMLInsecurity) } // GetSiteName returns the current value of the "site-name" key. func (cs *configService) GetSiteName() string { return cs.GetCurConfig(keySiteName).(string) } // GetMaxTransclusions return the maximum number of indirect transclusions. func (cs *configService) GetMaxTransclusions() int { return int(cs.GetCurConfig(keyMaxTransclusions).(int64)) } // GetYAMLHeader returns the current value of the "yaml-header" key. func (cs *configService) GetYAMLHeader() bool { return cs.GetCurConfig(keyYAMLHeader).(bool) } // GetZettelFileSyntax returns the current value of the "zettel-file-syntax" key. func (cs *configService) GetZettelFileSyntax() []string { if zfs := cs.GetCurConfig(keyZettelFileSyntax); zfs != nil { return zfs.([]string) } return nil } // --- config.AuthConfig // GetSimpleMode returns true if system tuns in simple-mode. func (cs *configService) GetSimpleMode() bool { return cs.GetCurConfig(kernel.ConfigSimpleMode).(bool) } // GetExpertMode returns the current value of the "expert-mode" key. func (cs *configService) GetExpertMode() bool { return cs.GetCurConfig(keyExpertMode).(bool) } // GetVisibility returns the visibility value, or "login" if none is given. func (cs *configService) GetVisibility(m *meta.Meta) meta.Visibility { if val, ok := m.Get(api.KeyVisibility); ok { if vis := meta.GetVisibility(val); vis != meta.VisibilityUnknown { return vis } } vis := cs.GetCurConfig(keyDefaultVisibility).(meta.Visibility) if vis != meta.VisibilityUnknown { return vis } cs.mxService.RLock() val, _ := cs.orig.Get(keyDefaultVisibility) vis = meta.GetVisibility(val) cs.mxService.RUnlock() return vis } |
Added kernel/impl/cmd.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 | //----------------------------------------------------------------------------- // Copyright (c) 2021-present Detlef Stern // // This file is part of Zettelstore. // // Zettelstore is licensed under the latest version of the EUPL (European Union // Public License). Please see file LICENSE.txt for your rights and obligations // under this license. //----------------------------------------------------------------------------- package impl import ( "fmt" "io" "os" "runtime/metrics" "sort" "strconv" "strings" "zettelstore.de/c/maps" "zettelstore.de/z/kernel" "zettelstore.de/z/logger" "zettelstore.de/z/strfun" ) type cmdSession struct { w io.Writer kern *myKernel echo bool header bool colwidth int eol []byte } func (sess *cmdSession) initialize(w io.Writer, kern *myKernel) { sess.w = w sess.kern = kern sess.header = true sess.colwidth = 80 sess.eol = []byte{'\n'} } func (sess *cmdSession) executeLine(line string) bool { if sess.echo { sess.println(line) } cmd, args := splitLine(line) if c, ok := commands[cmd]; ok { return c.Func(sess, cmd, args) } if cmd == "help" { return cmdHelp(sess, cmd, args) } sess.println("Unknown command:", cmd, strings.Join(args, " ")) sess.println("-- Enter 'help' go get a list of valid commands.") return true } func (sess *cmdSession) println(args ...string) { if len(args) > 0 { io.WriteString(sess.w, args[0]) for _, arg := range args[1:] { io.WriteString(sess.w, " ") io.WriteString(sess.w, arg) } } sess.w.Write(sess.eol) } func (sess *cmdSession) usage(cmd, val string) { sess.println("Usage:", cmd, val) } func (sess *cmdSession) printTable(table [][]string) { maxLen := sess.calcMaxLen(table) if len(maxLen) == 0 { return } if sess.header { sess.printRow(table[0], maxLen, "|=", " | ", ' ') hLine := make([]string, len(table[0])) sess.printRow(hLine, maxLen, "|%", "-+-", '-') } for _, row := range table[1:] { sess.printRow(row, maxLen, "| ", " | ", ' ') } } func (sess *cmdSession) calcMaxLen(table [][]string) []int { maxLen := make([]int, 0) for _, row := range table { for colno, column := range row { if colno >= len(maxLen) { maxLen = append(maxLen, 0) } colLen := strfun.Length(column) if colLen <= maxLen[colno] { continue } if colLen < sess.colwidth { maxLen[colno] = colLen } else { maxLen[colno] = sess.colwidth } } } return maxLen } func (sess *cmdSession) printRow(row []string, maxLen []int, prefix, delim string, pad rune) { for colno, column := range row { io.WriteString(sess.w, prefix) prefix = delim io.WriteString(sess.w, strfun.JustifyLeft(column, maxLen[colno], pad)) } sess.w.Write(sess.eol) } func splitLine(line string) (string, []string) { s := strings.Fields(line) if len(s) == 0 { return "", nil } return strings.ToLower(s[0]), s[1:] } type command struct { Text string Func func(sess *cmdSession, cmd string, args []string) bool } var commands = map[string]command{ "": {"", func(*cmdSession, string, []string) bool { return true }}, "bye": { "end this session", func(*cmdSession, string, []string) bool { return false }, }, "config": {"show configuration keys", cmdConfig}, "crlf": { "toggle crlf mode", func(sess *cmdSession, cmd string, args []string) bool { if len(sess.eol) == 1 { sess.eol = []byte{'\r', '\n'} sess.println("crlf is on") } else { sess.eol = []byte{'\n'} sess.println("crlf is off") } return true }, }, "dump-index": {"writes the content of the index", cmdDumpIndex}, "dump-recover": {"show data of last recovery", cmdDumpRecover}, "echo": { "toggle echo mode", func(sess *cmdSession, cmd string, args []string) bool { sess.echo = !sess.echo if sess.echo { sess.println("echo is on") } else { sess.println("echo is off") } return true }, }, "end-profile": {"stop profiling", cmdEndProfile}, "env": {"show environment values", cmdEnvironment}, "get-config": {"show current configuration data", cmdGetConfig}, "header": { "toggle table header", func(sess *cmdSession, cmd string, args []string) bool { sess.header = !sess.header if sess.header { sess.println("header are on") } else { sess.println("header are off") } return true }, }, "log-level": {"get/set log level", cmdLogLevel}, "metrics": {"show Go runtime metrics", cmdMetrics}, "next-config": {"show next configuration data", cmdNextConfig}, "profile": {"start profiling", cmdProfile}, "refresh": {"refresh box data", cmdRefresh}, "restart": {"restart service", cmdRestart}, "services": {"show available services", cmdServices}, "set-config": {"set next configuration data", cmdSetConfig}, "shutdown": { "shutdown Zettelstore", func(sess *cmdSession, cmd string, args []string) bool { sess.kern.Shutdown(false); return false }, }, "start": {"start service", cmdStart}, "stat": {"show service statistics", cmdStat}, "stop": {"stop service", cmdStop}, } func cmdHelp(sess *cmdSession, _ string, _ []string) bool { cmds := maps.Keys(commands) table := [][]string{{"Command", "Description"}} for _, cmd := range cmds { table = append(table, []string{cmd, commands[cmd].Text}) } sess.printTable(table) return true } func cmdConfig(sess *cmdSession, cmd string, args []string) bool { srvnum, ok := lookupService(sess, cmd, args) if !ok { return true } srv := sess.kern.srvs[srvnum].srv table := [][]string{{"Key", "Description"}} for _, kd := range srv.ConfigDescriptions() { table = append(table, []string{kd.Key, kd.Descr}) } sess.printTable(table) return true } func cmdGetConfig(sess *cmdSession, _ string, args []string) bool { showConfig(sess, args, listCurConfig, func(srv service, key string) interface{} { return srv.GetCurConfig(key) }) return true } func cmdNextConfig(sess *cmdSession, _ string, args []string) bool { showConfig(sess, args, listNextConfig, func(srv service, key string) interface{} { return srv.GetNextConfig(key) }) return true } func showConfig(sess *cmdSession, args []string, listConfig func(*cmdSession, service), getConfig func(service, string) interface{}) { if len(args) == 0 { keys := make([]int, 0, len(sess.kern.srvs)) for k := range sess.kern.srvs { keys = append(keys, int(k)) } sort.Ints(keys) for i, k := range keys { if i > 0 { sess.println() } srvD := sess.kern.srvs[kernel.Service(k)] sess.println("%% Service", srvD.name) listConfig(sess, srvD.srv) } return } srvD, found := getService(sess, args[0]) if !found { return } if len(args) == 1 { listConfig(sess, srvD.srv) return } val := getConfig(srvD.srv, args[1]) if val == nil { sess.println("Unknown key", args[1], "for service", args[0]) return } sess.println(fmt.Sprintf("%v", val)) } func listCurConfig(sess *cmdSession, srv service) { listConfig(sess, func() []kernel.KeyDescrValue { return srv.GetCurConfigList(true) }) } func listNextConfig(sess *cmdSession, srv service) { listConfig(sess, srv.GetNextConfigList) } func listConfig(sess *cmdSession, getConfigList func() []kernel.KeyDescrValue) { l := getConfigList() table := [][]string{{"Key", "Value", "Description"}} for _, kdv := range l { table = append(table, []string{kdv.Key, kdv.Value, kdv.Descr}) } sess.printTable(table) } func cmdSetConfig(sess *cmdSession, cmd string, args []string) bool { if len(args) < 3 { sess.usage(cmd, "SERVICE KEY VALUE") return true } srvD, found := getService(sess, args[0]) if !found { return true } key := args[1] newValue := strings.Join(args[2:], " ") if err := srvD.srv.SetConfig(key, newValue); err == nil { sess.kern.logger.Mandatory().Str("key", key).Str("value", newValue).Msg("Update system configuration") } else { sess.println("Unable to set key", args[1], "to value", newValue, "because:", err.Error()) } return true } func cmdServices(sess *cmdSession, _ string, _ []string) bool { table := [][]string{{"Service", "Status"}} for _, name := range sortedServiceNames(sess) { if sess.kern.srvNames[name].srv.IsStarted() { table = append(table, []string{name, "started"}) } else { table = append(table, []string{name, "stopped"}) } } sess.printTable(table) return true } func cmdStart(sess *cmdSession, cmd string, args []string) bool { srvnum, ok := lookupService(sess, cmd, args) if !ok { return true } err := sess.kern.doStartService(srvnum) if err != nil { sess.println(err.Error()) } return true } func cmdRestart(sess *cmdSession, cmd string, args []string) bool { srvnum, ok := lookupService(sess, cmd, args) if !ok { return true } err := sess.kern.doRestartService(srvnum) if err != nil { sess.println(err.Error()) } return true } func cmdStop(sess *cmdSession, cmd string, args []string) bool { srvnum, ok := lookupService(sess, cmd, args) if !ok { return true } err := sess.kern.doStopService(srvnum) if err != nil { sess.println(err.Error()) } return true } func cmdStat(sess *cmdSession, cmd string, args []string) bool { if len(args) == 0 { sess.usage(cmd, "SERVICE") return true } srvD, ok := getService(sess, args[0]) if !ok { return true } kvl := srvD.srv.GetStatistics() if len(kvl) == 0 { return true } table := [][]string{{"Key", "Value"}} for _, kv := range kvl { table = append(table, []string{kv.Key, kv.Value}) } sess.printTable(table) return true } func cmdLogLevel(sess *cmdSession, _ string, args []string) bool { kern := sess.kern if len(args) == 0 { // Write log levels level := kern.logger.Level() table := [][]string{ {"Service", "Level", "Name"}, {"kernel", strconv.Itoa(int(level)), level.String()}, } for _, name := range sortedServiceNames(sess) { level = kern.srvNames[name].srv.GetLogger().Level() table = append(table, []string{name, strconv.Itoa(int(level)), level.String()}) } sess.printTable(table) return true } var l *logger.Logger name := args[0] if name == "kernel" { l = kern.logger } else { srvD, ok := getService(sess, name) if !ok { return true } l = srvD.srv.GetLogger() } if len(args) == 1 { level := l.Level() sess.println(strconv.Itoa(int(level)), level.String()) return true } level := args[1] uval, err := strconv.ParseUint(level, 10, 8) lv := logger.Level(uval) if err != nil || !lv.IsValid() { lv = logger.ParseLevel(level) } if !lv.IsValid() { sess.println("Invalid level:", level) return true } kern.logger.Mandatory().Str("name", name).Str("level", lv.String()).Msg("Update log level") l.SetLevel(lv) return true } func lookupService(sess *cmdSession, cmd string, args []string) (kernel.Service, bool) { if len(args) == 0 { sess.usage(cmd, "SERVICE") return 0, false } srvD, ok := getService(sess, args[0]) if !ok { return 0, false } return srvD.srvnum, true } func cmdProfile(sess *cmdSession, _ string, args []string) bool { var profileName string if len(args) < 1 { profileName = kernel.ProfileCPU } else { profileName = args[0] } var fileName string if len(args) < 2 { fileName = profileName + ".prof" } else { fileName = args[1] } kern := sess.kern if err := kern.doStartProfiling(profileName, fileName); err != nil { sess.println("Error:", err.Error()) } else { kern.logger.Mandatory().Str("profile", profileName).Str("file", fileName).Msg("Start profiling") } return true } func cmdEndProfile(sess *cmdSession, _ string, _ []string) bool { kern := sess.kern err := kern.doStopProfiling() if err != nil { sess.println("Error:", err.Error()) } kern.logger.Mandatory().Err(err).Msg("Stop profiling") return true } func cmdMetrics(sess *cmdSession, _ string, _ []string) bool { var samples []metrics.Sample all := metrics.All() for _, d := range all { if d.Kind == metrics.KindFloat64Histogram { continue } samples = append(samples, metrics.Sample{Name: d.Name}) } metrics.Read(samples) table := [][]string{{"Value", "Description"}} i := 0 for _, d := range all { if d.Kind == metrics.KindFloat64Histogram { continue } descr := d.Description if pos := strings.IndexByte(descr, '.'); pos > 0 { descr = descr[:pos] } value := samples[i].Value i++ var sVal string switch value.Kind() { case metrics.KindUint64: sVal = strconv.FormatUint(value.Uint64(), 10) case metrics.KindFloat64: sVal = fmt.Sprintf("%v", value.Float64()) case metrics.KindFloat64Histogram: sVal = "(Histogramm)" case metrics.KindBad: sVal = "BAD" default: sVal = fmt.Sprintf("(unexpected metric kind: %v)", value.Kind()) } table = append(table, []string{sVal, descr}) } sess.printTable(table) return true } func cmdDumpIndex(sess *cmdSession, _ string, _ []string) bool { sess.kern.DumpIndex(sess.w) return true } func cmdRefresh(sess *cmdSession, _ string, _ []string) bool { kern := sess.kern kern.logger.Mandatory().Msg("Refresh") kern.box.Refresh() return true } func cmdDumpRecover(sess *cmdSession, cmd string, args []string) bool { if len(args) == 0 { sess.usage(cmd, "RECOVER") sess.println("-- A valid value for RECOVER can be obtained via 'stat core'.") return true } lines := sess.kern.core.RecoverLines(args[0]) if len(lines) == 0 { return true } for _, line := range lines { sess.println(line) } return true } func cmdEnvironment(sess *cmdSession, _ string, _ []string) bool { workDir, err := os.Getwd() if err != nil { workDir = err.Error() } execName, err := os.Executable() if err != nil { execName = err.Error() } envs := os.Environ() sort.Strings(envs) table := [][]string{ {"Key", "Value"}, {"workdir", workDir}, {"executable", execName}, } for _, env := range envs { if pos := strings.IndexByte(env, '='); pos >= 0 && pos < len(env) { table = append(table, []string{env[:pos], env[pos+1:]}) } } sess.printTable(table) return true } func sortedServiceNames(sess *cmdSession) []string { return maps.Keys(sess.kern.srvNames) } func getService(sess *cmdSession, name string) (serviceData, bool) { srvD, found := sess.kern.srvNames[name] if !found { sess.println("Unknown service", name) } return srvD, found } |
Added kernel/impl/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 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 | //----------------------------------------------------------------------------- // Copyright (c) 2021-present Detlef Stern // // This file is part of Zettelstore. // // Zettelstore is licensed under the latest version of the EUPL (European Union // Public License). Please see file LICENSE.txt for your rights and obligations // under this license. //----------------------------------------------------------------------------- package impl import ( "errors" "fmt" "sort" "strconv" "strings" "sync" "zettelstore.de/c/maps" "zettelstore.de/z/domain/id" "zettelstore.de/z/kernel" "zettelstore.de/z/logger" ) type parseFunc func(string) (any, error) type configDescription struct { text string parse parseFunc canList bool } type descriptionMap map[string]configDescription type interfaceMap map[string]interface{} func (m interfaceMap) Clone() interfaceMap { if m == nil { return nil } result := make(interfaceMap, len(m)) for k, v := range m { result[k] = v } return result } type srvConfig struct { logger *logger.Logger mxConfig sync.RWMutex frozen bool descr descriptionMap cur interfaceMap next interfaceMap } func (cfg *srvConfig) ConfigDescriptions() []serviceConfigDescription { cfg.mxConfig.RLock() defer cfg.mxConfig.RUnlock() keys := maps.Keys(cfg.descr) result := make([]serviceConfigDescription, 0, len(keys)) for _, k := range keys { text := cfg.descr[k].text if strings.HasSuffix(k, "-") { text = text + " (list)" } result = append(result, serviceConfigDescription{Key: k, Descr: text}) } return result } var errAlreadyFrozen = errors.New("value not allowed to be set") func (cfg *srvConfig) noFrozen(parse parseFunc) parseFunc { return func(val string) (any, error) { if cfg.frozen { return nil, errAlreadyFrozen } return parse(val) } } var errListKeyNotFound = errors.New("no list key found") func (cfg *srvConfig) SetConfig(key, value string) error { cfg.mxConfig.Lock() defer cfg.mxConfig.Unlock() descr, ok := cfg.descr[key] if !ok { d, baseKey, num := cfg.getListDescription(key) if num < 0 { return errListKeyNotFound } for i := num + 1; ; i++ { k := baseKey + strconv.Itoa(i) if _, ok = cfg.next[k]; !ok { break } delete(cfg.next, k) } if num == 0 { return nil } descr = d } parse := descr.parse if parse == nil { if cfg.frozen { return errAlreadyFrozen } cfg.next[key] = value return nil } iVal, err := parse(value) if err != nil { return err } cfg.next[key] = iVal return nil } func (cfg *srvConfig) getListDescription(key string) (configDescription, string, int) { for k, d := range cfg.descr { if !strings.HasSuffix(k, "-") { continue } if !strings.HasPrefix(key, k) { continue } num, err := strconv.Atoi(key[len(k):]) if err != nil || num < 0 { continue } return d, k, num } return configDescription{}, "", -1 } func (cfg *srvConfig) GetCurConfig(key string) interface{} { cfg.mxConfig.RLock() defer cfg.mxConfig.RUnlock() if cfg.cur == nil { return cfg.next[key] } return cfg.cur[key] } func (cfg *srvConfig) GetNextConfig(key string) interface{} { cfg.mxConfig.RLock() defer cfg.mxConfig.RUnlock() return cfg.next[key] } func (cfg *srvConfig) GetCurConfigList(all bool) []kernel.KeyDescrValue { return cfg.getOneConfigList(all, cfg.GetCurConfig) } func (cfg *srvConfig) GetNextConfigList() []kernel.KeyDescrValue { return cfg.getOneConfigList(true, cfg.GetNextConfig) } func (cfg *srvConfig) getOneConfigList(all bool, getConfig func(string) interface{}) []kernel.KeyDescrValue { if len(cfg.descr) == 0 { return nil } keys := cfg.getSortedConfigKeys(all, getConfig) result := make([]kernel.KeyDescrValue, 0, len(keys)) for _, k := range keys { val := getConfig(k) if val == nil { continue } descr, ok := cfg.descr[k] if !ok { descr, _, _ = cfg.getListDescription(k) } result = append(result, kernel.KeyDescrValue{ Key: k, Descr: descr.text, Value: fmt.Sprintf("%v", val), }) } return result } func (cfg *srvConfig) getSortedConfigKeys(all bool, getConfig func(string) interface{}) []string { keys := make([]string, 0, len(cfg.descr)) for k, descr := range cfg.descr { if all || descr.canList { if !strings.HasSuffix(k, "-") { keys = append(keys, k) continue } for i := 1; ; i++ { key := k + strconv.Itoa(i) val := getConfig(key) if val == nil { break } keys = append(keys, key) } } } sort.Strings(keys) return keys } func (cfg *srvConfig) Freeze() { cfg.mxConfig.Lock() cfg.frozen = true cfg.mxConfig.Unlock() } func (cfg *srvConfig) SwitchNextToCur() { cfg.mxConfig.Lock() defer cfg.mxConfig.Unlock() cfg.cur = cfg.next.Clone() } func parseString(val string) (any, error) { return val, nil } var errNoBoolean = errors.New("no boolean value") func parseBool(val string) (any, error) { if val == "" { return false, errNoBoolean } switch val[0] { case '0', 'f', 'F', 'n', 'N': return false, nil } return true, nil } func parseInt64(val string) (any, error) { if u64, err := strconv.ParseInt(val, 10, 64); err == nil { return u64, nil } else { return nil, err } } func parseZid(val string) (any, error) { if zid, err := id.Parse(val); err == nil { return zid, nil } else { return id.Invalid, err } } func parseInvalidZid(val string) (any, error) { zid, _ := id.Parse(val) return zid, nil } |
Added kernel/impl/core.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 | //----------------------------------------------------------------------------- // Copyright (c) 2021-present Detlef Stern // // This file is part of Zettelstore. // // Zettelstore is licensed under the latest version of the EUPL (European Union // Public License). Please see file LICENSE.txt for your rights and obligations // under this license. //----------------------------------------------------------------------------- package impl import ( "fmt" "net" "os" "runtime" "sync" "time" "zettelstore.de/c/maps" "zettelstore.de/z/domain/id" "zettelstore.de/z/kernel" "zettelstore.de/z/logger" "zettelstore.de/z/strfun" ) type coreService struct { srvConfig started bool mxRecover sync.RWMutex mapRecover map[string]recoverInfo } type recoverInfo struct { count uint64 ts time.Time info interface{} stack []byte } func (cs *coreService) Initialize(logger *logger.Logger) { cs.logger = logger cs.mapRecover = make(map[string]recoverInfo) cs.descr = descriptionMap{ kernel.CoreDebug: {"Debug mode", parseBool, false}, kernel.CoreGoArch: {"Go processor architecture", nil, false}, kernel.CoreGoOS: {"Go Operating System", nil, false}, kernel.CoreGoVersion: {"Go Version", nil, false}, kernel.CoreHostname: {"Host name", nil, false}, kernel.CorePort: { "Port of command line server", cs.noFrozen(func(val string) (any, error) { port, err := net.LookupPort("tcp", val) if err != nil { return nil, err } return port, nil }), true, }, kernel.CoreProgname: {"Program name", nil, false}, kernel.CoreStarted: {"Start time", nil, false}, kernel.CoreVerbose: {"Verbose output", parseBool, true}, kernel.CoreVersion: { "Version", cs.noFrozen(func(val string) (any, error) { if val == "" { return kernel.CoreDefaultVersion, nil } return val, nil }), false, }, kernel.CoreVTime: {"Version time", nil, false}, } cs.next = interfaceMap{ kernel.CoreDebug: false, kernel.CoreGoArch: runtime.GOARCH, kernel.CoreGoOS: runtime.GOOS, kernel.CoreGoVersion: runtime.Version(), kernel.CoreHostname: "*unknown host*", kernel.CorePort: 0, kernel.CoreStarted: time.Now().Local().Format(id.ZidLayout), kernel.CoreVerbose: false, } if hn, err := os.Hostname(); err == nil { cs.next[kernel.CoreHostname] = hn } } func (cs *coreService) GetLogger() *logger.Logger { return cs.logger } func (cs *coreService) Start(*myKernel) error { cs.started = true return nil } func (cs *coreService) IsStarted() bool { return cs.started } func (cs *coreService) Stop(*myKernel) { cs.started = false } func (cs *coreService) GetStatistics() []kernel.KeyValue { cs.mxRecover.RLock() defer cs.mxRecover.RUnlock() names := maps.Keys(cs.mapRecover) result := make([]kernel.KeyValue, 0, 3*len(names)) for _, n := range names { ri := cs.mapRecover[n] result = append( result, kernel.KeyValue{ Key: fmt.Sprintf("Recover %q / Count", n), Value: fmt.Sprintf("%d", ri.count), }, kernel.KeyValue{ Key: fmt.Sprintf("Recover %q / Last ", n), Value: fmt.Sprintf("%v", ri.ts), }, kernel.KeyValue{ Key: fmt.Sprintf("Recover %q / Info ", n), Value: fmt.Sprintf("%v", ri.info), }, ) } return result } func (cs *coreService) RecoverLines(name string) []string { cs.mxRecover.RLock() ri := cs.mapRecover[name] cs.mxRecover.RUnlock() if ri.stack == nil { return nil } return append( []string{ fmt.Sprintf("Count: %d", ri.count), fmt.Sprintf("Time: %v", ri.ts), fmt.Sprintf("Reason: %v", ri.info), }, strfun.SplitLines(string(ri.stack))..., ) } func (cs *coreService) updateRecoverInfo(name string, recoverInfo interface{}, stack []byte) { cs.mxRecover.Lock() ri := cs.mapRecover[name] ri.count++ ri.ts = time.Now().Local() ri.info = recoverInfo ri.stack = stack cs.mapRecover[name] = ri cs.mxRecover.Unlock() } |
Added kernel/impl/impl.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 | //----------------------------------------------------------------------------- // Copyright (c) 2021-present Detlef Stern // // This file is part of Zettelstore. // // Zettelstore is licensed under the latest version of the EUPL (European Union // Public License). Please see file LICENSE.txt for your rights and obligations // under this license. //----------------------------------------------------------------------------- // Package impl provides the kernel implementation. package impl import ( "errors" "fmt" "io" "net" "os" "os/signal" "runtime" "runtime/debug" "runtime/pprof" "strconv" "strings" "sync" "syscall" "time" "zettelstore.de/z/domain/id" "zettelstore.de/z/kernel" "zettelstore.de/z/logger" ) // myKernel is the main internal kernel. type myKernel struct { logWriter *kernelLogWriter logger *logger.Logger wg sync.WaitGroup mx sync.RWMutex interrupt chan os.Signal profileName string fileName string profileFile *os.File profile *pprof.Profile self kernelService core coreService cfg configService auth authService box boxService web webService srvs map[kernel.Service]serviceDescr srvNames map[string]serviceData depStart serviceDependency depStop serviceDependency // reverse of depStart } type serviceDescr struct { srv service name string logLevel logger.Level } type serviceData struct { srv service srvnum kernel.Service } type serviceDependency map[kernel.Service][]kernel.Service const ( defaultNormalLogLevel = logger.InfoLevel defaultSimpleLogLevel = logger.WarnLevel ) // create a new kernel. func init() { kernel.Main = createKernel() } // create a new kernel. func createKernel() kernel.Kernel { lw := newKernelLogWriter(8192) kern := &myKernel{ logWriter: lw, logger: logger.New(lw, "").SetLevel(defaultNormalLogLevel), interrupt: make(chan os.Signal, 5), } kern.self.kernel = kern kern.srvs = map[kernel.Service]serviceDescr{ kernel.KernelService: {&kern.self, "kernel", defaultNormalLogLevel}, kernel.CoreService: {&kern.core, "core", defaultNormalLogLevel}, kernel.ConfigService: {&kern.cfg, "config", defaultNormalLogLevel}, kernel.AuthService: {&kern.auth, "auth", defaultNormalLogLevel}, kernel.BoxService: {&kern.box, "box", defaultNormalLogLevel}, kernel.WebService: {&kern.web, "web", defaultNormalLogLevel}, } kern.srvNames = make(map[string]serviceData, len(kern.srvs)) for key, srvD := range kern.srvs { if _, ok := kern.srvNames[srvD.name]; ok { kern.logger.Panic().Str("service", srvD.name).Msg("Service data already set") } kern.srvNames[srvD.name] = serviceData{srvD.srv, key} l := logger.New(lw, strings.ToUpper(srvD.name)).SetLevel(srvD.logLevel) kern.logger.Debug().Str("service", srvD.name).Msg("Initialize") srvD.srv.Initialize(l) } kern.depStart = serviceDependency{ kernel.KernelService: nil, kernel.CoreService: {kernel.KernelService}, kernel.ConfigService: {kernel.CoreService}, kernel.AuthService: {kernel.CoreService}, kernel.BoxService: {kernel.CoreService, kernel.ConfigService, kernel.AuthService}, kernel.WebService: {kernel.ConfigService, kernel.AuthService, kernel.BoxService}, } kern.depStop = make(serviceDependency, len(kern.depStart)) for srv, deps := range kern.depStart { for _, dep := range deps { kern.depStop[dep] = append(kern.depStop[dep], srv) } } return kern } func (kern *myKernel) Setup(progname, version string, versionTime time.Time) { kern.SetConfig(kernel.CoreService, kernel.CoreProgname, progname) kern.SetConfig(kernel.CoreService, kernel.CoreVersion, version) kern.SetConfig(kernel.CoreService, kernel.CoreVTime, versionTime.Local().Format(id.ZidLayout)) } func (kern *myKernel) Start(headline, lineServer bool) { for _, srvD := range kern.srvs { srvD.srv.Freeze() } if kern.cfg.GetCurConfig(kernel.ConfigSimpleMode).(bool) { kern.SetLogLevel(defaultSimpleLogLevel.String()) } kern.wg.Add(1) signal.Notify(kern.interrupt, os.Interrupt, syscall.SIGTERM) go func() { // Wait for interrupt. sig := <-kern.interrupt if strSig := sig.String(); strSig != "" { kern.logger.Info().Str("signal", strSig).Msg("Shut down Zettelstore") } kern.doShutdown() kern.wg.Done() }() kern.StartService(kernel.KernelService) if headline { logger := kern.logger logger.Mandatory().Msg(fmt.Sprintf( "%v %v (%v@%v/%v)", kern.core.GetCurConfig(kernel.CoreProgname), kern.core.GetCurConfig(kernel.CoreVersion), kern.core.GetCurConfig(kernel.CoreGoVersion), kern.core.GetCurConfig(kernel.CoreGoOS), kern.core.GetCurConfig(kernel.CoreGoArch), )) logger.Mandatory().Msg("Licensed under the latest version of the EUPL (European Union Public License)") if kern.core.GetCurConfig(kernel.CoreDebug).(bool) { logger.Warn().Msg("----------------------------------------") logger.Warn().Msg("DEBUG MODE, DO NO USE THIS IN PRODUCTION") logger.Warn().Msg("----------------------------------------") } if kern.auth.GetCurConfig(kernel.AuthReadonly).(bool) { logger.Info().Msg("Read-only mode") } } if lineServer { port := kern.core.GetNextConfig(kernel.CorePort).(int) if port > 0 { listenAddr := net.JoinHostPort("127.0.0.1", strconv.Itoa(port)) startLineServer(kern, listenAddr) } } } func (kern *myKernel) doShutdown() { kern.StopService(kernel.KernelService) // Will stop all other services. } func (kern *myKernel) WaitForShutdown() { kern.wg.Wait() kern.doStopProfiling() } // --- Shutdown operation ---------------------------------------------------- // Shutdown the service. Waits for all concurrent activity to stop. func (kern *myKernel) Shutdown(silent bool) { kern.logger.Trace().Msg("Shutdown") kern.interrupt <- &shutdownSignal{silent: silent} } type shutdownSignal struct{ silent bool } func (s *shutdownSignal) String() string { if s.silent { return "" } return "shutdown" } func (*shutdownSignal) Signal() { /* Just a signal */ } // --- Log operation --------------------------------------------------------- func (kern *myKernel) GetKernelLogger() *logger.Logger { return kern.logger } func (kern *myKernel) SetLogLevel(logLevel string) { defaultLevel, srvLevel := kern.parseLogLevel(logLevel) kern.mx.RLock() defer kern.mx.RUnlock() for srvN, srvD := range kern.srvs { if lvl, found := srvLevel[srvN]; found { srvD.srv.GetLogger().SetLevel(lvl) } else if defaultLevel != logger.NoLevel { srvD.srv.GetLogger().SetLevel(defaultLevel) } } } func (kern *myKernel) parseLogLevel(logLevel string) (logger.Level, map[kernel.Service]logger.Level) { defaultLevel := logger.NoLevel srvLevel := map[kernel.Service]logger.Level{} for _, spec := range strings.Split(logLevel, ";") { vals := cleanLogSpec(strings.Split(spec, ":")) switch len(vals) { case 0: case 1: if lvl := logger.ParseLevel(vals[0]); lvl.IsValid() { defaultLevel = lvl } default: serviceText, levelText := vals[0], vals[1] if srv, found := kern.srvNames[serviceText]; found { if lvl := logger.ParseLevel(levelText); lvl.IsValid() { srvLevel[srv.srvnum] = lvl } } } } return defaultLevel, srvLevel } func cleanLogSpec(rawVals []string) []string { vals := make([]string, 0, len(rawVals)) for _, rVal := range rawVals { val := strings.TrimSpace(rVal) if val != "" { vals = append(vals, val) } } return vals } func (kern *myKernel) RetrieveLogEntries() []kernel.LogEntry { return kern.logWriter.retrieveLogEntries() } func (kern *myKernel) GetLastLogTime() time.Time { return kern.logWriter.getLastLogTime() } // LogRecover outputs some information about the previous panic. func (kern *myKernel) LogRecover(name string, recoverInfo interface{}) bool { return kern.doLogRecover(name, recoverInfo) } func (kern *myKernel) doLogRecover(name string, recoverInfo interface{}) bool { stack := debug.Stack() kern.logger.Fatal().Str("recovered_from", fmt.Sprint(recoverInfo)).Bytes("stack", stack).Msg(name) kern.core.updateRecoverInfo(name, recoverInfo, stack) return true } // --- Profiling --------------------------------------------------------- var errProfileInWork = errors.New("already profiling") var errProfileNotFound = errors.New("profile not found") func (kern *myKernel) StartProfiling(profileName, fileName string) error { kern.mx.Lock() defer kern.mx.Unlock() return kern.doStartProfiling(profileName, fileName) } func (kern *myKernel) doStartProfiling(profileName, fileName string) error { if kern.profileName != "" { return errProfileInWork } if profileName == kernel.ProfileCPU { f, err := os.Create(fileName) if err != nil { return err } err = pprof.StartCPUProfile(f) if err != nil { f.Close() return err } kern.profileName = profileName kern.fileName = fileName kern.profileFile = f return nil } profile := pprof.Lookup(profileName) if profile == nil { return errProfileNotFound } f, err := os.Create(fileName) if err != nil { return err } kern.profileName = profileName kern.fileName = fileName kern.profile = profile kern.profileFile = f runtime.GC() // get up-to-date statistics profile.WriteTo(f, 0) return nil } func (kern *myKernel) StopProfiling() error { kern.mx.Lock() defer kern.mx.Unlock() return kern.doStopProfiling() } func (kern *myKernel) doStopProfiling() error { if kern.profileName == "" { return nil // No profile started } if kern.profileName == kernel.ProfileCPU { pprof.StopCPUProfile() } err := kern.profileFile.Close() kern.profileName = "" kern.fileName = "" kern.profile = nil kern.profileFile = nil return err } // --- Service handling -------------------------------------------------- var errUnknownService = errors.New("unknown service") func (kern *myKernel) SetConfig(srvnum kernel.Service, key, value string) error { kern.mx.Lock() defer kern.mx.Unlock() if srvD, ok := kern.srvs[srvnum]; ok { return srvD.srv.SetConfig(key, value) } return errUnknownService } func (kern *myKernel) GetConfig(srvnum kernel.Service, key string) interface{} { kern.mx.RLock() defer kern.mx.RUnlock() if srvD, ok := kern.srvs[srvnum]; ok { return srvD.srv.GetCurConfig(key) } return nil } func (kern *myKernel) GetConfigList(srvnum kernel.Service) []kernel.KeyDescrValue { kern.mx.RLock() defer kern.mx.RUnlock() if srvD, ok := kern.srvs[srvnum]; ok { return srvD.srv.GetCurConfigList(false) } return nil } func (kern *myKernel) GetServiceStatistics(srvnum kernel.Service) []kernel.KeyValue { kern.mx.RLock() defer kern.mx.RUnlock() if srvD, ok := kern.srvs[srvnum]; ok { return srvD.srv.GetStatistics() } return nil } func (kern *myKernel) GetLogger(srvnum kernel.Service) *logger.Logger { kern.mx.RLock() defer kern.mx.RUnlock() if srvD, ok := kern.srvs[srvnum]; ok { return srvD.srv.GetLogger() } return kern.GetKernelLogger() } func (kern *myKernel) SetLevel(srvnum kernel.Service, level logger.Level) { if level.IsValid() { kern.mx.RLock() if srvD, ok := kern.srvs[srvnum]; ok { srvD.srv.GetLogger().SetLevel(level) } kern.mx.RUnlock() } } func (kern *myKernel) StartService(srvnum kernel.Service) error { kern.mx.RLock() defer kern.mx.RUnlock() return kern.doStartService(srvnum) } func (kern *myKernel) doStartService(srvnum kernel.Service) error { for _, srv := range kern.sortDependency(srvnum, kern.depStart, true) { if err := srv.Start(kern); err != nil { return err } srv.SwitchNextToCur() } return nil } func (kern *myKernel) RestartService(srvnum kernel.Service) error { kern.mx.RLock() defer kern.mx.RUnlock() return kern.doRestartService(srvnum) } func (kern *myKernel) doRestartService(srvnum kernel.Service) error { deps := kern.sortDependency(srvnum, kern.depStop, false) for _, srv := range deps { srv.Stop(kern) } for i := len(deps) - 1; i >= 0; i-- { srv := deps[i] if err := srv.Start(kern); err != nil { return err } srv.SwitchNextToCur() } return nil } func (kern *myKernel) StopService(srvnum kernel.Service) error { kern.mx.Lock() defer kern.mx.Unlock() return kern.doStopService(srvnum) } func (kern *myKernel) doStopService(srvnum kernel.Service) error { for _, srv := range kern.sortDependency(srvnum, kern.depStop, false) { srv.Stop(kern) } return nil } func (kern *myKernel) sortDependency( srvnum kernel.Service, srvdeps serviceDependency, isStarted bool, ) []service { srvD, ok := kern.srvs[srvnum] if !ok { return nil } if srvD.srv.IsStarted() == isStarted { return nil } deps := srvdeps[srvnum] found := make(map[service]bool, 8) result := make([]service, 0, len(found)) for _, dep := range deps { srvDeps := kern.sortDependency(dep, srvdeps, isStarted) for _, depSrv := range srvDeps { if !found[depSrv] { result = append(result, depSrv) found[depSrv] = true } } } return append(result, srvD.srv) } func (kern *myKernel) DumpIndex(w io.Writer) { kern.box.DumpIndex(w) } type service interface { // Initialize the data for the service. Initialize(*logger.Logger) // Get service logger. GetLogger() *logger.Logger // ConfigDescriptions returns a sorted list of configuration descriptions. ConfigDescriptions() []serviceConfigDescription // SetConfig stores a configuration value. SetConfig(key, value string) error // GetCurConfig returns the current configuration value. GetCurConfig(key string) interface{} // GetNextConfig returns the next configuration value. GetNextConfig(key string) interface{} // GetCurConfigList returns a sorted list of current configuration data. GetCurConfigList(all bool) []kernel.KeyDescrValue // GetNextConfigList returns a sorted list of next configuration data. GetNextConfigList() []kernel.KeyDescrValue // GetStatistics returns a key/value list of statistical data. GetStatistics() []kernel.KeyValue // Freeze disallows to change some fixed configuration values. Freeze() // Start the service. Start(*myKernel) error // SwitchNextToCur moves next config data to current. SwitchNextToCur() // IsStarted returns true if the service was started successfully. IsStarted() bool // Stop the service. Stop(*myKernel) } type serviceConfigDescription struct{ Key, Descr string } func (kern *myKernel) SetCreators( createAuthManager kernel.CreateAuthManagerFunc, createBoxManager kernel.CreateBoxManagerFunc, setupWebServer kernel.SetupWebServerFunc, ) { kern.auth.createManager = createAuthManager kern.box.createManager = createBoxManager kern.web.setupServer = setupWebServer } // --- The kernel as a service ------------------------------------------- type kernelService struct { kernel *myKernel } func (*kernelService) Initialize(*logger.Logger) {} func (ks *kernelService) GetLogger() *logger.Logger { return ks.kernel.logger } func (*kernelService) ConfigDescriptions() []serviceConfigDescription { return nil } func (*kernelService) SetConfig(key, value string) error { return errAlreadyFrozen } func (*kernelService) GetCurConfig(key string) interface{} { return nil } func (*kernelService) GetNextConfig(key string) interface{} { return nil } func (*kernelService) GetCurConfigList(all bool) []kernel.KeyDescrValue { return nil } func (*kernelService) GetNextConfigList() []kernel.KeyDescrValue { return nil } func (*kernelService) GetStatistics() []kernel.KeyValue { return nil } func (*kernelService) Freeze() {} func (*kernelService) Start(*myKernel) error { return nil } func (*kernelService) SwitchNextToCur() {} func (*kernelService) IsStarted() bool { return true } func (*kernelService) Stop(*myKernel) {} |
Added kernel/impl/log.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 | //----------------------------------------------------------------------------- // Copyright (c) 2021-present Detlef Stern // // This file is part of Zettelstore. // // Zettelstore is licensed under the latest version of the EUPL (European Union // Public License). Please see file LICENSE.txt for your rights and obligations // under this license. //----------------------------------------------------------------------------- package impl import ( "os" "sync" "time" "zettelstore.de/z/kernel" "zettelstore.de/z/logger" ) // kernelLogWriter adapts an io.Writer to a LogWriter type kernelLogWriter struct { mx sync.RWMutex // protects buf, serializes w.Write and retrieveLogEntries lastLog time.Time buf []byte writePos int data []logEntry full bool } // newKernelLogWriter creates a new LogWriter for kernel logging. func newKernelLogWriter(capacity int) *kernelLogWriter { if capacity < 1 { capacity = 1 } return &kernelLogWriter{ lastLog: time.Now(), buf: make([]byte, 0, 500), data: make([]logEntry, capacity), } } func (klw *kernelLogWriter) WriteMessage(level logger.Level, ts time.Time, prefix, msg string, details []byte) error { klw.mx.Lock() if level > logger.DebugLevel { klw.lastLog = ts klw.data[klw.writePos] = logEntry{ level: level, ts: ts, prefix: prefix, msg: msg, details: append([]byte(nil), details...), } klw.writePos++ if klw.writePos >= cap(klw.data) { klw.writePos = 0 klw.full = true } } klw.buf = klw.buf[:0] buf := klw.buf addTimestamp(&buf, ts) buf = append(buf, ' ') buf = append(buf, level.Format()...) buf = append(buf, ' ') if prefix != "" { buf = append(buf, prefix...) buf = append(buf, ' ') } buf = append(buf, msg...) buf = append(buf, details...) buf = append(buf, '\n') _, err := os.Stdout.Write(buf) klw.mx.Unlock() if level == logger.PanicLevel { panic(err) } return err } func addTimestamp(buf *[]byte, ts time.Time) { year, month, day := ts.Date() itoa(buf, year, 4) *buf = append(*buf, '-') itoa(buf, int(month), 2) *buf = append(*buf, '-') itoa(buf, day, 2) *buf = append(*buf, ' ') hour, minute, second := ts.Clock() itoa(buf, hour, 2) *buf = append(*buf, ':') itoa(buf, minute, 2) *buf = append(*buf, ':') itoa(buf, second, 2) } func itoa(buf *[]byte, i, wid int) { var b [20]byte for bp := wid - 1; bp >= 0; bp-- { q := i / 10 b[bp] = byte('0' + i - q*10) i = q } *buf = append(*buf, b[:wid]...) } type logEntry struct { level logger.Level ts time.Time prefix string msg string details []byte } func (klw *kernelLogWriter) retrieveLogEntries() []kernel.LogEntry { klw.mx.RLock() defer klw.mx.RUnlock() if !klw.full { if klw.writePos == 0 { return nil } result := make([]kernel.LogEntry, klw.writePos) for i := 0; i < klw.writePos; i++ { copyE2E(&result[i], &klw.data[i]) } return result } result := make([]kernel.LogEntry, cap(klw.data)) pos := 0 for j := klw.writePos; j < cap(klw.data); j++ { copyE2E(&result[pos], &klw.data[j]) pos++ } for j := 0; j < klw.writePos; j++ { copyE2E(&result[pos], &klw.data[j]) pos++ } return result } func (klw *kernelLogWriter) getLastLogTime() time.Time { klw.mx.RLock() defer klw.mx.RUnlock() return klw.lastLog } func copyE2E(result *kernel.LogEntry, origin *logEntry) { result.Level = origin.level result.TS = origin.ts result.Prefix = origin.prefix result.Message = origin.msg + string(origin.details) } |
Added kernel/impl/server.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 | //----------------------------------------------------------------------------- // Copyright (c) 2021-present Detlef Stern // // This file is part of Zettelstore. // // Zettelstore is licensed under the latest version of the EUPL (European Union // Public License). Please see file LICENSE.txt for your rights and obligations // under this license. //----------------------------------------------------------------------------- package impl import ( "bufio" "net" ) func startLineServer(kern *myKernel, listenAddr string) error { ln, err := net.Listen("tcp", listenAddr) if err != nil { kern.logger.Fatal().Err(err).Msg("Unable to start administration console") return err } kern.logger.Mandatory().Str("listen", listenAddr).Msg("Start administration console") go func() { lineServer(ln, kern) }() return nil } func lineServer(ln net.Listener, kern *myKernel) { // Something may panic. Ensure a running line service. defer func() { if r := recover(); r != nil { kern.doLogRecover("Line", r) go lineServer(ln, kern) } }() for { conn, err := ln.Accept() if err != nil { // handle error kern.logger.IfErr(err).Msg("Unable to accept connection") break } go handleLineConnection(conn, kern) } ln.Close() } func handleLineConnection(conn net.Conn, kern *myKernel) { // Something may panic. Ensure a running connection. defer func() { if r := recover(); r != nil { kern.doLogRecover("LineConn", r) go handleLineConnection(conn, kern) } }() kern.logger.Mandatory().Str("from", conn.RemoteAddr().String()).Msg("Start session on administration console") cmds := cmdSession{} cmds.initialize(conn, kern) s := bufio.NewScanner(conn) for s.Scan() { line := s.Text() if !cmds.executeLine(line) { break } } conn.Close() } |
Added kernel/impl/web.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 | //----------------------------------------------------------------------------- // Copyright (c) 2021-present Detlef Stern // // This file is part of Zettelstore. // // Zettelstore is licensed under the latest version of the EUPL (European Union // Public License). Please see file LICENSE.txt for your rights and obligations // under this license. //----------------------------------------------------------------------------- package impl import ( "errors" "net" "net/netip" "net/url" "os" "path/filepath" "strconv" "strings" "sync" "time" "zettelstore.de/z/kernel" "zettelstore.de/z/logger" "zettelstore.de/z/web/server" "zettelstore.de/z/web/server/impl" ) type webService struct { srvConfig mxService sync.RWMutex srvw server.Server setupServer kernel.SetupWebServerFunc } var errURLPrefixSyntax = errors.New("must not be empty and must start with '//'") func (ws *webService) Initialize(logger *logger.Logger) { ws.logger = logger ws.descr = descriptionMap{ kernel.WebAssetDir: { "Asset file directory", func(val string) (any, error) { val = filepath.Clean(val) if finfo, err := os.Stat(val); err == nil && finfo.IsDir() { return val, nil } else { return nil, err } }, true, }, kernel.WebBaseURL: { "Base URL", func(val string) (any, error) { if _, err := url.Parse(val); err != nil { return nil, err } return val, nil }, true, }, kernel.WebListenAddress: { "Listen address", func(val string) (any, error) { // If there is no host, prepend 127.0.0.1 as host. host, _, err := net.SplitHostPort(val) if err == nil && host == "" { val = "127.0.0.1" + val } ap, err := netip.ParseAddrPort(val) if err != nil { return "", err } return ap.String(), nil }, true}, kernel.WebMaxRequestSize: {"Max Request Size", parseInt64, true}, kernel.WebPersistentCookie: {"Persistent cookie", parseBool, true}, kernel.WebSecureCookie: {"Secure cookie", parseBool, true}, kernel.WebTokenLifetimeAPI: { "Token lifetime API", makeDurationParser(10*time.Minute, 0, 1*time.Hour), true, }, kernel.WebTokenLifetimeHTML: { "Token lifetime HTML", makeDurationParser(1*time.Hour, 1*time.Minute, 30*24*time.Hour), true, }, kernel.WebURLPrefix: { "URL prefix under which the web server runs", func(val string) (any, error) { if val != "" && val[0] == '/' && val[len(val)-1] == '/' { return val, nil } return nil, errURLPrefixSyntax }, true, }, } ws.next = interfaceMap{ kernel.WebAssetDir: "", kernel.WebBaseURL: "http://127.0.0.1:23123/", kernel.WebListenAddress: "127.0.0.1:23123", kernel.WebMaxRequestSize: int64(16 * 1024 * 1024), kernel.WebPersistentCookie: false, kernel.WebSecureCookie: true, kernel.WebTokenLifetimeAPI: 1 * time.Hour, kernel.WebTokenLifetimeHTML: 10 * time.Minute, kernel.WebURLPrefix: "/", } } func makeDurationParser(defDur, minDur, maxDur time.Duration) parseFunc { return func(val string) (any, error) { if d, err := strconv.ParseUint(val, 10, 64); err == nil { secs := time.Duration(d) * time.Minute if secs < minDur { return minDur, nil } if secs > maxDur { return maxDur, nil } return secs, nil } return defDur, nil } } var errWrongBasePrefix = errors.New(kernel.WebURLPrefix + " does not match " + kernel.WebBaseURL) func (ws *webService) GetLogger() *logger.Logger { return ws.logger } func (ws *webService) Start(kern *myKernel) error { baseURL := ws.GetNextConfig(kernel.WebBaseURL).(string) listenAddr := ws.GetNextConfig(kernel.WebListenAddress).(string) urlPrefix := ws.GetNextConfig(kernel.WebURLPrefix).(string) persistentCookie := ws.GetNextConfig(kernel.WebPersistentCookie).(bool) secureCookie := ws.GetNextConfig(kernel.WebSecureCookie).(bool) maxRequestSize := ws.GetNextConfig(kernel.WebMaxRequestSize).(int64) if maxRequestSize < 1024 { maxRequestSize = 1024 } if !strings.HasSuffix(baseURL, urlPrefix) { ws.logger.Fatal().Str("base-url", baseURL).Str("url-prefix", urlPrefix).Msg( "url-prefix is not a suffix of base-url") return errWrongBasePrefix } if lap := netip.MustParseAddrPort(listenAddr); !kern.auth.manager.WithAuth() && !lap.Addr().IsLoopback() { ws.logger.Warn().Str("listen", listenAddr).Msg("service may be reached from outside, but authentication is not enabled") } srvw := impl.New(ws.logger, listenAddr, baseURL, urlPrefix, persistentCookie, secureCookie, maxRequestSize, kern.auth.manager) err := kern.web.setupServer(srvw, kern.box.manager, kern.auth.manager, &kern.cfg) if err != nil { ws.logger.Fatal().Err(err).Msg("Unable to create") return err } if kern.core.GetNextConfig(kernel.CoreDebug).(bool) { srvw.SetDebug() } if err = srvw.Run(); err != nil { ws.logger.Fatal().Err(err).Msg("Unable to start") return err } ws.logger.Info().Str("listen", listenAddr).Str("base-url", baseURL).Msg("Start Service") ws.mxService.Lock() ws.srvw = srvw ws.mxService.Unlock() if kern.cfg.GetCurConfig(kernel.ConfigSimpleMode).(bool) { listenAddr := ws.GetNextConfig(kernel.WebListenAddress).(string) if idx := strings.LastIndexByte(listenAddr, ':'); idx >= 0 { ws.logger.Mandatory().Msg(strings.Repeat("--------------------", 3)) ws.logger.Mandatory().Msg("Open your browser and enter the following URL:") ws.logger.Mandatory().Msg(" http://localhost" + listenAddr[idx:]) ws.logger.Mandatory().Msg("") ws.logger.Mandatory().Msg("If this does not work, try:") ws.logger.Mandatory().Msg(" http://127.0.0.1" + listenAddr[idx:]) } } return nil } func (ws *webService) IsStarted() bool { ws.mxService.RLock() defer ws.mxService.RUnlock() return ws.srvw != nil } func (ws *webService) Stop(*myKernel) { ws.logger.Info().Msg("Stop Service") ws.srvw.Stop() ws.mxService.Lock() ws.srvw = nil ws.mxService.Unlock() } func (*webService) GetStatistics() []kernel.KeyValue { return nil } |
Added kernel/kernel.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 | //----------------------------------------------------------------------------- // Copyright (c) 2021-present Detlef Stern // // This file is part of Zettelstore. // // Zettelstore is licensed under the latest version of the EUPL (European Union // Public License). Please see file LICENSE.txt for your rights and obligations // under this license. //----------------------------------------------------------------------------- // Package kernel provides the main kernel service. package kernel import ( "io" "net/url" "time" "zettelstore.de/z/auth" "zettelstore.de/z/box" "zettelstore.de/z/config" "zettelstore.de/z/domain/id" "zettelstore.de/z/logger" "zettelstore.de/z/web/server" ) // Kernel is the main internal service. type Kernel interface { // Setup sets the most basic data of a software: its name, its version, // and when the version was created. Setup(progname, version string, versionTime time.Time) // Start the service. Start(headline bool, lineServer bool) // WaitForShutdown blocks the call until Shutdown is called. WaitForShutdown() // Shutdown the service. Waits for all concurrent activities to stop. Shutdown(silent bool) // GetKernelLogger returns the kernel logger. GetKernelLogger() *logger.Logger // SetLogLevel sets the logging level for logger maintained by the kernel. // // Its syntax is: (SERVICE ":")? LEVEL (";" (SERICE ":")? LEVEL)*. SetLogLevel(string) // LogRecover outputs some information about the previous panic. LogRecover(name string, recoverInfo interface{}) bool // StartProfiling starts profiling the software according to a profile. // It is an error to start more than one profile. // // profileName is a valid profile (see runtime/pprof/Lookup()), or the // value "cpu" for profiling the CPI. // fileName is the name of the file where the results are written to. StartProfiling(profileName, fileName string) error // StopProfiling stops the current profiling and writes the result to // the file, which was named during StartProfiling(). // It will always be called before the software stops its operations. StopProfiling() error // SetConfig stores a configuration value. SetConfig(srv Service, key, value string) error // GetConfig returns a configuration value. GetConfig(srv Service, key string) interface{} // GetConfigList returns a sorted list of configuration data. GetConfigList(Service) []KeyDescrValue // GetLogger returns a logger for the given service. GetLogger(Service) *logger.Logger // SetLevel sets the logging level for the given service. SetLevel(Service, logger.Level) // RetrieveLogEntries returns all buffered log entries. RetrieveLogEntries() []LogEntry // GetLastLogTime returns the time when the last logging with level > DEBUG happened. GetLastLogTime() time.Time // StartService start the given service. StartService(Service) error // RestartService stops and restarts the given service, while maintaining service dependencies. RestartService(Service) error // StopService stop the given service. StopService(Service) error // GetServiceStatistics returns a key/value list with statistical data. GetServiceStatistics(Service) []KeyValue // DumpIndex writes some data about the internal index into a writer. DumpIndex(io.Writer) // SetCreators store functions to be called when a service has to be created. SetCreators(CreateAuthManagerFunc, CreateBoxManagerFunc, SetupWebServerFunc) } // Main references the main kernel. var Main Kernel // Unit is a type with just one value. type Unit struct{} // ShutdownChan is a channel used to signal a system shutdown. type ShutdownChan <-chan Unit // Constants for profile names. const ( ProfileCPU = "CPU" ProfileHead = "heap" ) // Service specifies a service, e.g. web, ... type Service uint8 // Constants for type Service. const ( _ Service = iota KernelService // The Kernel itself is also a sevice CoreService // Manages startup specific functionality ConfigService // Provides access to runtime configuration AuthService // Manages authentication BoxService // Boxes provide zettel WebService // Access to Zettelstore through Web-based API and WebUI ) // Constants for core service system keys. const ( CoreDebug = "debug" CoreGoArch = "go-arch" CoreGoOS = "go-os" CoreGoVersion = "go-version" CoreHostname = "hostname" CorePort = "port" CoreProgname = "progname" CoreStarted = "started" CoreVerbose = "verbose" CoreVersion = "version" CoreVTime = "vtime" ) // Defined values for core service. const ( CoreDefaultVersion = "unknown" ) // Constants for config service keys. const ( ConfigSimpleMode = "simple-mode" ConfigInsecureHTML = "insecure-html" ) // Constants for authentication service keys. const ( AuthOwner = "owner" AuthReadonly = "readonly" ) // Constants for box service keys. const ( BoxDefaultDirType = "defdirtype" BoxURIs = "box-uri-" ) // Allowed values for BoxDefaultDirType const ( BoxDirTypeNotify = "notify" BoxDirTypeSimple = "simple" ) // Constants for config service keys. const ( ConfigSecureHTML = "secure" ConfigSyntaxHTML = "html" ConfigMarkdownHTML = "markdown" ConfigZmkHTML = "zettelmarkup" ) // Constants for web service keys. const ( WebAssetDir = "asset-dir" WebBaseURL = "base-url" WebListenAddress = "listen" WebPersistentCookie = "persistent" WebMaxRequestSize = "max-request-size" WebSecureCookie = "secure" WebTokenLifetimeAPI = "api-lifetime" WebTokenLifetimeHTML = "html-lifetime" WebURLPrefix = "prefix" ) // KeyDescrValue is a triple of config data. type KeyDescrValue struct{ Key, Descr, Value string } // KeyValue is a pair of key and value. type KeyValue struct{ Key, Value string } // LogEntry stores values of one log line written by a logger.Logger type LogEntry struct { Level logger.Level TS time.Time Prefix string Message string } // CreateAuthManagerFunc is called to create a new auth manager. type CreateAuthManagerFunc func(readonly bool, owner id.Zid) (auth.Manager, error) // CreateBoxManagerFunc is called to create a new box manager. type CreateBoxManagerFunc func( boxURIs []*url.URL, authManager auth.Manager, rtConfig config.Config, ) (box.Manager, error) // SetupWebServerFunc is called to create a new web service handler. type SetupWebServerFunc func( webServer server.Server, boxManager box.Manager, authManager auth.Manager, rtConfig config.Config, ) error |
Added logger/logger.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 | //----------------------------------------------------------------------------- // Copyright (c) 2021-present Detlef Stern // // This file is part of Zettelstore. // // Zettelstore is licensed under the latest version of the EUPL (European Union // Public License). Please see file LICENSE.txt for your rights and obligations // under this license. //----------------------------------------------------------------------------- // Package logger implements a logging package for use in the Zettelstore. package logger import ( "context" "strconv" "strings" "sync/atomic" "time" "zettelstore.de/z/domain/meta" ) // Level defines the possible log levels type Level uint8 // Constants for Level const ( NoLevel Level = iota // the absent log level TraceLevel // Log most internal activities DebugLevel // Log most data updates SenseLevel // Log activities of minor interest InfoLevel // Log normal activities WarnLevel // Log event that can be easily recovered ErrorLevel // Log (persistent) errors FatalLevel // Log event that cannot be recovered within an internal acitivty PanicLevel // Log event that must stop the software MandatoryLevel // Log only mandatory events NeverLevel // Logging is disabled ) var logLevel = [...]string{ " ", "TRACE", "DEBUG", "SENSE", "INFO ", "WARN ", "ERROR", "FATAL", "PANIC", ">>>>>", "NEVER", } var strLevel = [...]string{ "", "trace", "debug", "sense", "info", "warn", "error", "fatal", "panic", "mandatory", "disabled", } // IsValid returns true, if the level is a valid level func (l Level) IsValid() bool { return TraceLevel <= l && l <= NeverLevel } func (l Level) String() string { if l.IsValid() { return strLevel[l] } return strconv.Itoa(int(l)) } // Format returns a string representation suitable for logging. func (l Level) Format() string { if l.IsValid() { return logLevel[l] } return strconv.Itoa(int(l)) } // ParseLevel returns the recognized level. func ParseLevel(text string) Level { for lv := TraceLevel; lv <= NeverLevel; lv++ { if len(text) > 2 && strings.HasPrefix(strLevel[lv], text) { return lv } } return NoLevel } // Logger represents an objects that emits logging messages. type Logger struct { lw LogWriter levelVal uint32 prefix string context []byte topParent *Logger uProvider UserProvider } // LogWriter writes log messages to their specified destinations. type LogWriter interface { WriteMessage(level Level, ts time.Time, prefix, msg string, details []byte) error } // New creates a new logger for the given service. // // This function must only be called from a kernel implementation, not from // code that tries to log something. func New(lw LogWriter, prefix string) *Logger { if prefix != "" && len(prefix) < 6 { prefix = (prefix + " ")[:6] } result := &Logger{ lw: lw, levelVal: uint32(InfoLevel), prefix: prefix, context: nil, uProvider: nil, } result.topParent = result return result } func newFromMessage(msg *Message) *Logger { if msg == nil { return nil } logger := msg.logger context := make([]byte, 0, len(msg.buf)) context = append(context, msg.buf...) return &Logger{ lw: nil, levelVal: 0, prefix: logger.prefix, context: context, topParent: logger.topParent, uProvider: nil, } } // SetLevel sets the level of the logger. func (l *Logger) SetLevel(newLevel Level) *Logger { if l != nil { if l.topParent != l { panic("try to set level for child logger") } atomic.StoreUint32(&l.levelVal, uint32(newLevel)) } return l } // Level returns the current level of the given logger func (l *Logger) Level() Level { if l != nil { return Level(atomic.LoadUint32(&l.levelVal)) } return NeverLevel } // Trace creates a tracing message. func (l *Logger) Trace() *Message { return newMessage(l, TraceLevel) } // Debug creates a debug message. func (l *Logger) Debug() *Message { return newMessage(l, DebugLevel) } // Sense creates a message suitable for sensing data. func (l *Logger) Sense() *Message { return newMessage(l, SenseLevel) } // Info creates a message suitable for information data. func (l *Logger) Info() *Message { return newMessage(l, InfoLevel) } // Warn creates a message suitable for warning the user. func (l *Logger) Warn() *Message { return newMessage(l, WarnLevel) } // Error creates a message suitable for errors. func (l *Logger) Error() *Message { return newMessage(l, ErrorLevel) } // IfErr creates an error message and sets the go error, if there is an error. func (l *Logger) IfErr(err error) *Message { if err != nil { return newMessage(l, ErrorLevel).Err(err) } return nil } // Fatal creates a message suitable for fatal errors. func (l *Logger) Fatal() *Message { return newMessage(l, FatalLevel) } // Panic creates a message suitable for panicing. func (l *Logger) Panic() *Message { return newMessage(l, PanicLevel) } // Mandatory creates a message that will always logged, except when logging // is disabled. func (l *Logger) Mandatory() *Message { return newMessage(l, MandatoryLevel) } // Clone creates a message to clone the logger. func (l *Logger) Clone() *Message { msg := newMessage(l, NeverLevel) if msg != nil { msg.level = NoLevel } return msg } // UserProvider allows to retrieve an user metadata from a context. type UserProvider interface { GetUser(ctx context.Context) *meta.Meta } // WithUser creates a derivied logger that allows to retrieve and log user identifer. func (l *Logger) WithUser(up UserProvider) *Logger { return &Logger{ lw: nil, levelVal: 0, prefix: l.prefix, context: l.context, topParent: l.topParent, uProvider: up, } } func (l *Logger) writeMessage(level Level, msg string, details []byte) error { return l.topParent.lw.WriteMessage(level, time.Now().Local(), l.prefix, msg, details) } |
Added logger/logger_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 | //----------------------------------------------------------------------------- // Copyright (c) 2021-present Detlef Stern // // This file is part of Zettelstore. // // Zettelstore is licensed under the latest version of the EUPL (European Union // Public License). Please see file LICENSE.txt for your rights and obligations // under this license. //----------------------------------------------------------------------------- package logger_test import ( "fmt" "os" "testing" "time" "zettelstore.de/z/logger" ) func TestParseLevel(t *testing.T) { testcases := []struct { text string exp logger.Level }{ {"tra", logger.TraceLevel}, {"deb", logger.DebugLevel}, {"info", logger.InfoLevel}, {"warn", logger.WarnLevel}, {"err", logger.ErrorLevel}, {"fata", logger.FatalLevel}, {"pan", logger.PanicLevel}, {"manda", logger.MandatoryLevel}, {"dis", logger.NeverLevel}, {"d", logger.Level(0)}, } for i, tc := range testcases { got := logger.ParseLevel(tc.text) if got != tc.exp { t.Errorf("%d: ParseLevel(%q) == %q, but got %q", i, tc.text, tc.exp, got) } } } func BenchmarkDisabled(b *testing.B) { log := logger.New(&stderrLogWriter{}, "").SetLevel(logger.NeverLevel) for n := 0; n < b.N; n++ { log.Info().Str("key", "val").Msg("Benchmark") } } type stderrLogWriter struct{} func (*stderrLogWriter) WriteMessage(level logger.Level, ts time.Time, prefix, msg string, details []byte) error { fmt.Fprintf(os.Stderr, "%v %v %v %v %v\n", level.Format(), ts, prefix, msg, string(details)) return nil } type testLogWriter struct{} func (*testLogWriter) WriteMessage(logger.Level, time.Time, string, string, []byte) error { return nil } func BenchmarkStrMessage(b *testing.B) { log := logger.New(&testLogWriter{}, "") for n := 0; n < b.N; n++ { log.Info().Str("key", "val").Msg("Benchmark") } } func BenchmarkMessage(b *testing.B) { log := logger.New(&testLogWriter{}, "") for n := 0; n < b.N; n++ { log.Info().Msg("Benchmark") } } func BenchmarkCloneStrMessage(b *testing.B) { log := logger.New(&testLogWriter{}, "").Clone().Str("sss", "ttt").Child() for n := 0; n < b.N; n++ { log.Info().Msg("123456789") } } |
Added logger/message.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 | //----------------------------------------------------------------------------- // Copyright (c) 2022-present Detlef Stern // // This file is part of Zettelstore. // // Zettelstore is licensed under the latest version of the EUPL (European Union // Public License). Please see file LICENSE.txt for your rights and obligations // under this license. //----------------------------------------------------------------------------- package logger import ( "context" "net/http" "strconv" "sync" "zettelstore.de/c/api" "zettelstore.de/z/domain/id" ) // Message presents a message to log. type Message struct { logger *Logger level Level buf []byte } func newMessage(logger *Logger, level Level) *Message { if logger != nil { if logger.topParent.Level() <= level { m := messagePool.Get().(*Message) m.logger = logger m.level = level m.buf = append(m.buf[:0], logger.context...) return m } } return nil } func recycleMessage(m *Message) { messagePool.Put(m) } var messagePool = &sync.Pool{ New: func() interface{} { return &Message{ buf: make([]byte, 0, 500), } }, } // Enabled returns whether the message will log or not. func (m *Message) Enabled() bool { return m != nil && m.level != NeverLevel } // Str adds a string value to the full message func (m *Message) Str(text, val string) *Message { if m.Enabled() { buf := append(m.buf, ',', ' ') buf = append(buf, text...) buf = append(buf, '=') m.buf = append(buf, val...) } return m } // Bool adds a boolean value to the full message func (m *Message) Bool(text string, val bool) *Message { if val { m.Str(text, "true") } else { m.Str(text, "false") } return m } // Bytes adds a byte slice value to the full message func (m *Message) Bytes(text string, val []byte) *Message { if m.Enabled() { buf := append(m.buf, ',', ' ') buf = append(buf, text...) buf = append(buf, '=') m.buf = append(buf, val...) } return m } // Err adds an error value to the full message func (m *Message) Err(err error) *Message { if err != nil { return m.Str("error", err.Error()) } return m } // Int adds an integer to the full message func (m *Message) Int(text string, i int64) *Message { return m.Str(text, strconv.FormatInt(i, 10)) } // Uint adds an unsigned integer to the full message func (m *Message) Uint(text string, u uint64) *Message { return m.Str(text, strconv.FormatUint(u, 10)) } // User adds the user-id field of the given user to the message. func (m *Message) User(ctx context.Context) *Message { if m.Enabled() { if up := m.logger.uProvider; up != nil { if user := up.GetUser(ctx); user != nil { m.buf = append(m.buf, ", user="...) if userID, found := user.Get(api.KeyUserID); found { m.buf = append(m.buf, userID...) } else { m.buf = append(m.buf, user.Zid.Bytes()...) } } } } return m } // HTTPIP adds the IP address of a HTTP request to the message. func (m *Message) HTTPIP(r *http.Request) *Message { if r == nil { return m } if from := r.Header.Get("X-Forwarded-For"); from != "" { return m.Str("ip", from) } return m.Str("IP", r.RemoteAddr) } // Zid adds a zettel identifier to the full message func (m *Message) Zid(zid id.Zid) *Message { return m.Bytes("zid", zid.Bytes()) } // Msg add the given text to the message and writes it to the log. func (m *Message) Msg(text string) { if m.Enabled() { m.logger.writeMessage(m.level, text, m.buf) recycleMessage(m) } } // Child creates a child logger with context of this message. func (m *Message) Child() *Logger { return newFromMessage(m) } |
Added parser/blob/blob.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 | //----------------------------------------------------------------------------- // Copyright (c) 2020-present Detlef Stern // // This file is part of Zettelstore. // // Zettelstore is licensed under the latest version of the EUPL (European Union // Public License). Please see file LICENSE.txt for your rights and obligations // under this license. //----------------------------------------------------------------------------- // Package blob provides a parser of binary data. package blob import ( "zettelstore.de/z/ast" "zettelstore.de/z/domain/meta" "zettelstore.de/z/input" "zettelstore.de/z/parser" ) func init() { parser.Register(&parser.Info{ Name: meta.SyntaxGif, AltNames: nil, IsASTParser: false, IsTextFormat: false, IsImageFormat: true, ParseBlocks: parseBlocks, ParseInlines: parseInlines, }) parser.Register(&parser.Info{ Name: meta.SyntaxJPEG, AltNames: []string{meta.SyntaxJPG}, IsASTParser: false, IsTextFormat: false, IsImageFormat: true, ParseBlocks: parseBlocks, ParseInlines: parseInlines, }) parser.Register(&parser.Info{ Name: meta.SyntaxPNG, AltNames: nil, IsASTParser: false, IsTextFormat: false, IsImageFormat: true, ParseBlocks: parseBlocks, ParseInlines: parseInlines, }) parser.Register(&parser.Info{ Name: meta.SyntaxWebp, AltNames: nil, IsASTParser: false, IsTextFormat: false, IsImageFormat: true, ParseBlocks: parseBlocks, ParseInlines: parseInlines, }) } func parseBlocks(inp *input.Input, m *meta.Meta, syntax string) ast.BlockSlice { if p := parser.Get(syntax); p != nil { syntax = p.Name } return ast.BlockSlice{&ast.BLOBNode{ Description: parser.ParseDescription(m), Syntax: syntax, Blob: []byte(inp.Src), }} } func parseInlines(*input.Input, string) ast.InlineSlice { return nil } |
Added parser/cleaner/cleaner.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 | //----------------------------------------------------------------------------- // Copyright (c) 2020-present Detlef Stern // // This file is part of Zettelstore. // // Zettelstore is licensed under the latest version of the EUPL (European Union // Public License). Please see file LICENSE.txt for your rights and obligations // under this license. //----------------------------------------------------------------------------- // Package cleaner provides functions to clean up the parsed AST. package cleaner import ( "strconv" "strings" "zettelstore.de/z/ast" "zettelstore.de/z/encoder/textenc" "zettelstore.de/z/strfun" ) // CleanBlockSlice cleans the given block list. func CleanBlockSlice(bs *ast.BlockSlice, allowHTML bool) { cleanNode(bs, allowHTML) } // CleanInlineSlice cleans the given inline list. func CleanInlineSlice(is *ast.InlineSlice) { cleanNode(is, false) } func cleanNode(n ast.Node, allowHTML bool) { cv := cleanVisitor{ textEnc: textenc.Create(), allowHTML: allowHTML, hasMark: false, doMark: false, } ast.Walk(&cv, n) if cv.hasMark { cv.doMark = true ast.Walk(&cv, n) } } type cleanVisitor struct { textEnc *textenc.Encoder ids map[string]ast.Node allowHTML bool hasMark bool doMark bool } func (cv *cleanVisitor) Visit(node ast.Node) ast.Visitor { switch n := node.(type) { case *ast.BlockSlice: if !cv.allowHTML { cv.visitBlockSlice(n) return nil } case *ast.InlineSlice: if !cv.allowHTML { cv.visitInlineSlice(n) return nil } case *ast.HeadingNode: cv.visitHeading(n) return nil case *ast.MarkNode: cv.visitMark(n) return nil } return cv } func (cv *cleanVisitor) visitBlockSlice(bs *ast.BlockSlice) { if bs == nil { return } if len(*bs) == 0 { *bs = nil return } for _, bn := range *bs { ast.Walk(cv, bn) } fromPos, toPos := 0, 0 for fromPos < len(*bs) { (*bs)[toPos] = (*bs)[fromPos] fromPos++ switch bn := (*bs)[toPos].(type) { case *ast.VerbatimNode: if bn.Kind != ast.VerbatimHTML { toPos++ } default: toPos++ } } for pos := toPos; pos < len(*bs); pos++ { (*bs)[pos] = nil // Allow excess nodes to be garbage collected. } *bs = (*bs)[:toPos:toPos] } func (cv *cleanVisitor) visitInlineSlice(is *ast.InlineSlice) { if is == nil { return } if len(*is) == 0 { *is = nil return } for _, bn := range *is { ast.Walk(cv, bn) } fromPos, toPos := 0, 0 for fromPos < len(*is) { (*is)[toPos] = (*is)[fromPos] fromPos++ switch in := (*is)[toPos].(type) { case *ast.LiteralNode: if in.Kind != ast.LiteralHTML { toPos++ } default: toPos++ } } for pos := toPos; pos < len(*is); pos++ { (*is)[pos] = nil // Allow excess nodes to be garbage collected. } *is = (*is)[:toPos:toPos] } func (cv *cleanVisitor) visitHeading(hn *ast.HeadingNode) { if cv.doMark || hn == nil || len(hn.Inlines) == 0 { return } if hn.Slug == "" { var sb strings.Builder _, err := cv.textEnc.WriteInlines(&sb, &hn.Inlines) if err != nil { return } hn.Slug = strfun.Slugify(sb.String()) } if hn.Slug != "" { hn.Fragment = cv.addIdentifier(hn.Slug, hn) } } func (cv *cleanVisitor) visitMark(mn *ast.MarkNode) { if !cv.doMark { cv.hasMark = true return } if mn.Mark == "" { mn.Slug = "" mn.Fragment = cv.addIdentifier("*", mn) return } if mn.Slug == "" { mn.Slug = strfun.Slugify(mn.Mark) } mn.Fragment = cv.addIdentifier(mn.Slug, mn) } func (cv *cleanVisitor) addIdentifier(id string, node ast.Node) string { if cv.ids == nil { cv.ids = map[string]ast.Node{id: node} return id } if n, ok := cv.ids[id]; ok && n != node { prefix := id + "-" for count := 1; ; count++ { newID := prefix + strconv.Itoa(count) if n2, ok2 := cv.ids[newID]; !ok2 || n2 == node { cv.ids[newID] = node return newID } } } cv.ids[id] = node return id } // CleanInlineLinks removes all links and footnote node from the given inline slice. func CleanInlineLinks(is *ast.InlineSlice) { ast.Walk(&cleanLinks{}, is) } type cleanLinks struct{} func (cl *cleanLinks) Visit(node ast.Node) ast.Visitor { ins, ok := node.(*ast.InlineSlice) if !ok { return cl } for _, in := range *ins { ast.Walk(cl, in) } if hasNoLinks(*ins) { return nil } result := make(ast.InlineSlice, 0, len(*ins)) for _, in := range *ins { switch n := in.(type) { case *ast.LinkNode: result = append(result, n.Inlines...) case *ast.FootnoteNode: // Do nothing default: result = append(result, n) } } *ins = result return nil } func hasNoLinks(ins ast.InlineSlice) bool { for _, in := range ins { switch in.(type) { case *ast.LinkNode, *ast.FootnoteNode: return false } } return true } |
Added parser/draw/ORIG_CONTRIBUTORS.
> > > | 1 2 3 | Devon H. O'Dell <devon.odell@gmail.com> Marc-Antoine Ruel <maruel@gmail.com> Mateusz Czaplinski <czapkofan@gmail.com> |
Added parser/draw/ORIG_LICENSE.
> > > > > > > > > > > > > > > > > > > > > > | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 | The MIT License (MIT) Copyright (c) 2015 The ASCIIToSVG Contributors Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. |
Added parser/draw/canvas.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 | //----------------------------------------------------------------------------- // Copyright (c) 2022-present Detlef Stern // // This file is part of Zettelstore. // // Zettelstore is licensed under the latest version of the EUPL (European Union // Public License). Please see file LICENSE.txt for your rights and obligations // under this license. // // This file was originally created by the ASCIIToSVG contributors under an MIT // license, but later changed to fulfil the needs of Zettelstore. The following // statements affects the original code as found on // https://github.com/asciitosvg/asciitosvg (Commit: // ca82a5ce41e2190a05e07af6e8b3ea4e3256a283, 2020-11-20): // // Copyright 2012 - 2018 The ASCIIToSVG Contributors // All rights reserved. //----------------------------------------------------------------------------- package draw import ( "bytes" "fmt" "image" "sort" "unicode/utf8" ) // newCanvas returns a new Canvas, initialized from the provided data. If tabWidth is set to a non-negative // value, that value will be used to convert tabs to spaces within the grid. Creation of the Canvas // can fail if the diagram contains invalid UTF-8 sequences. func newCanvas(data []byte) (*canvas, error) { c := &canvas{} lines := bytes.Split(data, []byte("\n")) c.siz.Y = len(lines) // Diagrams will often not be padded to a uniform width. To overcome this, we scan over // each line and figure out which is the longest. This becomes the width of the canvas. for i, line := range lines { if ok := utf8.Valid(line); !ok { return nil, fmt.Errorf("invalid UTF-8 encoding on line %d", i) } if i1 := utf8.RuneCount(line); i1 > c.siz.X { c.siz.X = i1 } } c.grid = make([]char, c.siz.X*c.siz.Y) c.visited = make([]bool, c.siz.X*c.siz.Y) for y, line := range lines { x := 0 for len(line) > 0 { r, l := utf8.DecodeRune(line) c.grid[y*c.siz.X+x] = char(r) x++ line = line[l:] } for ; x < c.siz.X; x++ { c.grid[y*c.siz.X+x] = ' ' } } c.findObjects() return c, nil } // canvas is the parsed source data. type canvas struct { // (0,0) is top left. grid []char visited []bool objs objects siz image.Point hasStartMarker bool hasEndMarker bool } // String provides a view into the underlying grid. func (c *canvas) String() string { return fmt.Sprintf("%+v", c.grid) } // objects returns all the objects found in the underlying grid. func (c *canvas) objects() objects { return c.objs } // size returns the visual dimensions of the Canvas. func (c *canvas) size() image.Point { return c.siz } // findObjects finds all objects (lines, polygons, and text) within the underlying grid. func (c *canvas) findObjects() { c.findPaths() c.findTexts() sort.Sort(c.objs) } // findPaths by starting with a point that wasn't yet visited, beginning at the top // left of the grid. func (c *canvas) findPaths() { for y := 0; y < c.siz.Y; y++ { p := point{y: y} for x := 0; x < c.siz.X; x++ { p.x = x if c.isVisited(p) { continue } ch := c.at(p) if !ch.isPathStart() { continue } // Found the start of a one or multiple connected paths. Traverse all // connecting points. This will generate multiple objects if multiple // paths (either open or closed) are found. c.visit(p) objs := c.scanPath([]point{p}) for _, obj := range objs { // For all points in all objects found, mark the points as visited. for _, p := range obj.Points() { c.visit(p) } } c.objs = append(c.objs, objs...) } } } // findTexts with a second pass through the grid attempts to identify any text within the grid. func (c *canvas) findTexts() { for y := 0; y < c.siz.Y; y++ { p := point{} p.y = y for x := 0; x < c.siz.X; x++ { p.x = x if c.isVisited(p) { continue } ch := c.at(p) if !ch.isTextStart() { continue } // scanText will return nil if the text at this area is simply // setting options on a container object. obj := c.scanText(p) if obj == nil { continue } for _, p := range obj.Points() { c.visit(p) } c.objs = append(c.objs, obj) } } } // scanPath tries to complete a total path (for lines or polygons) starting with some partial path. // It recurses when it finds multiple unvisited outgoing paths. func (c *canvas) scanPath(points []point) objects { cur := points[len(points)-1] next := c.next(cur) // If there are no points that can progress traversal of the path, finalize the one we're // working on, and return it. This is the terminal condition in the passive flow. if len(next) == 0 { if len(points) == 1 { // Discard 'path' of 1 point. Do not mark point as visited. c.unvisit(cur) return nil } // TODO(dhobsd): Determine if path is sharing the line with another path. If so, // we may want to join the objects such that we don't get weird rendering artifacts. o := &object{points: points} o.seal(c) return objects{o} } // If we have hit a point that can create a closed path, create an object and close // the path. Additionally, recurse to other progress directions in case e.g. an open // path spawns from this point. Paths are always closed vertically. if cur.x == points[0].x && cur.y == points[0].y+1 { o := &object{points: points} o.seal(c) r := objects{o} return append(r, c.scanPath([]point{cur})...) } // We scan depth-first instead of breadth-first, making it possible to find a // closed path. var objs objects for _, n := range next { if c.isVisited(n) { continue } c.visit(n) p2 := make([]point, len(points)+1) copy(p2, points) p2[len(p2)-1] = n objs = append(objs, c.scanPath(p2)...) } return objs } // The next returns the points that can be used to make progress, scanning (in order) horizontal // progress to the left or right, vertical progress above or below, or diagonal progress to NW, // NE, SW, and SE. It skips any points already visited, and returns all of the possible progress // points. func (c *canvas) next(pos point) []point { // Our caller must have called c.visit prior to calling this function. if !c.isVisited(pos) { panic(fmt.Errorf("internal error; revisiting %s", pos)) } var out []point nextHorizontal := func(p point) { if !c.isVisited(p) && c.at(p).canHorizontal() { out = append(out, p) } } nextVertical := func(p point) { if !c.isVisited(p) && c.at(p).canVertical() { out = append(out, p) } } nextDiagonal := func(from, to point) { if !c.isVisited(to) && c.at(to).canDiagonalFrom(c.at(from)) { out = append(out, to) } } ch := c.at(pos) if ch.canHorizontal() { if c.canLeft(pos) { n := pos n.x-- nextHorizontal(n) } if c.canRight(pos) { n := pos n.x++ nextHorizontal(n) } } if ch.canVertical() { if c.canUp(pos) { n := pos n.y-- nextVertical(n) } if c.canDown(pos) { n := pos n.y++ nextVertical(n) } } if c.canDiagonal(pos) { if c.canUp(pos) { if c.canLeft(pos) { n := pos n.x-- n.y-- nextDiagonal(pos, n) } if c.canRight(pos) { n := pos n.x++ n.y-- nextDiagonal(pos, n) } } if c.canDown(pos) { if c.canLeft(pos) { n := pos n.x-- n.y++ nextDiagonal(pos, n) } if c.canRight(pos) { n := pos n.x++ n.y++ nextDiagonal(pos, n) } } } return out } // scanText extracts a line of text. func (c *canvas) scanText(start point) *object { obj := &object{points: []point{start}, isText: true} whiteSpaceStreak := 0 cur := start for c.canRight(cur) { cur.x++ if c.isVisited(cur) { // If the point is already visited, we hit a polygon or a line. break } ch := c.at(cur) if !ch.isTextCont() { break } if ch.isSpace() { whiteSpaceStreak++ // Stop when we see 3 consecutive whitespace points. if whiteSpaceStreak > 2 { break } } else { whiteSpaceStreak = 0 } obj.points = append(obj.points, cur) } // Trim the right side of the text object. for len(obj.points) != 0 && c.at(obj.points[len(obj.points)-1]).isSpace() { obj.points = obj.points[:len(obj.points)-1] } obj.seal(c) return obj } func (c *canvas) at(p point) char { return c.grid[p.y*c.siz.X+p.x] } func (c *canvas) isVisited(p point) bool { return c.visited[p.y*c.siz.X+p.x] } func (c *canvas) visit(p point) { // TODO(dhobsd): Change code to ensure that visit() is called once and only // once per point. c.visited[p.y*c.siz.X+p.x] = true } func (c *canvas) unvisit(p point) { o := p.y*c.siz.X + p.x if !c.visited[o] { panic(fmt.Errorf("internal error: point %+v never visited", p)) } c.visited[o] = false } func (*canvas) canLeft(p point) bool { return p.x > 0 } func (c *canvas) canRight(p point) bool { return p.x < c.siz.X-1 } func (*canvas) canUp(p point) bool { return p.y > 0 } func (c *canvas) canDown(p point) bool { return p.y < c.siz.Y-1 } func (c *canvas) canDiagonal(p point) bool { return (c.canLeft(p) || c.canRight(p)) && (c.canUp(p) || c.canDown(p)) } |
Added parser/draw/canvas_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 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 593 594 595 596 597 598 599 600 601 602 603 604 605 606 607 608 609 610 611 612 613 614 615 616 617 618 619 620 621 622 623 624 625 626 627 628 629 630 631 632 633 634 635 636 637 638 639 640 641 642 643 644 645 646 647 648 649 650 651 652 653 654 655 656 657 658 659 660 661 662 663 664 665 666 667 668 669 670 671 672 673 674 675 676 677 678 679 680 681 682 683 684 685 686 687 688 689 690 691 692 693 694 695 696 697 698 699 700 701 702 703 704 705 706 707 708 709 710 711 712 713 714 715 716 | //----------------------------------------------------------------------------- // Copyright (c) 2022-present Detlef Stern // // This file is part of Zettelstore. // // Zettelstore is licensed under the latest version of the EUPL (European Union // Public License). Please see file LICENSE.txt for your rights and obligations // under this license. // // This file was originally created by the ASCIIToSVG contributors under an MIT // license, but later changed to fulfil the needs of Zettelstore. The following // statements affects the original code as found on // https://github.com/asciitosvg/asciitosvg (Commit: // ca82a5ce41e2190a05e07af6e8b3ea4e3256a283, 2020-11-20): // // Copyright 2012 - 2018 The ASCIIToSVG Contributors // All rights reserved. //----------------------------------------------------------------------------- package draw import ( "reflect" "strings" "testing" ) func TestNewCanvas(t *testing.T) { t.Parallel() data := []struct { input []string strings []string texts []string points [][]point allPoints bool }{ // 0 Small box { []string{ "+-+", "| |", "+-+", }, []string{"Path{[(0,0) (1,0) (2,0) (2,1) (2,2) (1,2) (0,2) (0,1)]}"}, []string{""}, [][]point{{{x: 0, y: 0}, {x: 2, y: 0}, {x: 2, y: 2}, {x: 0, y: 2}}}, false, }, // 1 Tight box { []string{ "++", "++", }, []string{"Path{[(0,0) (1,0) (1,1) (0,1)]}"}, []string{""}, [][]point{ { {x: 0, y: 0}, {x: 1, y: 0}, {x: 1, y: 1}, {x: 0, y: 1}, }, }, false, }, // 2 Indented box { []string{ "", " +-+", " | |", " +-+", }, []string{"Path{[(1,1) (2,1) (3,1) (3,2) (3,3) (2,3) (1,3) (1,2)]}"}, []string{""}, [][]point{{{x: 1, y: 1}, {x: 3, y: 1}, {x: 3, y: 3}, {x: 1, y: 3}}}, false, }, // 3 Free flow text { []string{ "", " foo bar ", "b baz bee", }, []string{"Text{(1,1) \"foo bar\"}", "Text{(0,2) \"b baz\"}", "Text{(9,2) \"bee\"}"}, []string{"foo bar", "b baz", "bee"}, [][]point{ {{x: 1, y: 1}, {x: 7, y: 1}}, {{x: 0, y: 2}, {x: 5, y: 2}}, {{x: 9, y: 2}, {x: 11, y: 2}}, }, false, }, // 4 Text in a box { []string{ "+--+", "|Hi|", "+--+", }, []string{"Path{[(0,0) (1,0) (2,0) (3,0) (3,1) (3,2) (2,2) (1,2) (0,2) (0,1)]}", "Text{(1,1) \"Hi\"}"}, []string{"", "Hi"}, [][]point{ {{x: 0, y: 0}, {x: 3, y: 0}, {x: 3, y: 2}, {x: 0, y: 2}}, {{x: 1, y: 1}, {x: 2, y: 1}}, }, false, }, // 5 Concave pieces { []string{ " +----+", " | |", "+---+ +----+", "| |", "+-------------+", "", // 5 "+----+", "| |", "| +---+", "| |", "| +---+", // 10 "| |", "+----+", "", " +----+", " | |", // 15 "+---+ |", "| |", "+---+ |", " | |", " +----+", }, []string{ "Path{[(4,0) (5,0) (6,0) (7,0) (8,0) (9,0) (9,1) (9,2) (10,2) (11,2) (12,2) (13,2) (14,2) (14,3) (14,4) (13,4) (12,4) (11,4) (10,4) (9,4) (8,4) (7,4) (6,4) (5,4) (4,4) (3,4) (2,4) (1,4) (0,4) (0,3) (0,2) (1,2) (2,2) (3,2) (4,2) (4,1)]}", "Path{[(0,6) (1,6) (2,6) (3,6) (4,6) (5,6) (5,7) (5,8) (6,8) (7,8) (8,8) (9,8) (9,9) (9,10) (8,10) (7,10) (6,10) (5,10) (5,11) (5,12) (4,12) (3,12) (2,12) (1,12) (0,12) (0,11) (0,10) (0,9) (0,8) (0,7)]}", "Path{[(4,14) (5,14) (6,14) (7,14) (8,14) (9,14) (9,15) (9,16) (9,17) (9,18) (9,19) (9,20) (8,20) (7,20) (6,20) (5,20) (4,20) (4,19) (4,18) (3,18) (2,18) (1,18) (0,18) (0,17) (0,16) (1,16) (2,16) (3,16) (4,16) (4,15)]}", }, []string{"", "", ""}, [][]point{ { {x: 4, y: 0}, {x: 9, y: 0}, {x: 9, y: 2}, {x: 14, y: 2}, {x: 14, y: 4}, {x: 0, y: 4}, {x: 0, y: 2}, {x: 4, y: 2}, }, { {x: 0, y: 6}, {x: 5, y: 6}, {x: 5, y: 8}, {x: 9, y: 8}, {x: 9, y: 10}, {x: 5, y: 10}, {x: 5, y: 12}, {x: 0, y: 12}, }, { {x: 4, y: 14}, {x: 9, y: 14}, {x: 9, y: 20}, {x: 4, y: 20}, {x: 4, y: 18}, {x: 0, y: 18}, {x: 0, y: 16}, {x: 4, y: 16}, }, }, false, }, // 6 Inner boxes { []string{ "+-----+", "| |", "| +-+ |", "| | | |", "| +-+ |", "| |", "+-----+", }, []string{ "Path{[(0,0) (1,0) (2,0) (3,0) (4,0) (5,0) (6,0) (6,1) (6,2) (6,3) (6,4) (6,5) (6,6) (5,6) (4,6) (3,6) (2,6) (1,6) (0,6) (0,5) (0,4) (0,3) (0,2) (0,1)]}", "Path{[(2,2) (3,2) (4,2) (4,3) (4,4) (3,4) (2,4) (2,3)]}", }, []string{"", ""}, [][]point{ {{x: 0, y: 0}, {x: 6, y: 0}, {x: 6, y: 6}, {x: 0, y: 6}}, {{x: 2, y: 2}, {x: 4, y: 2}, {x: 4, y: 4}, {x: 2, y: 4}}, }, false, }, // 7 Real world diagram example { []string{ // 1 2 3 " +------+", " |Editor|-------------+--------+", " +------+ | |", " | | v", " v | +--------+", " +------+ | |Document|", // 5 " |Window| | +--------+", " +------+ |", " | |", " +-----+-------+ |", " | | |", // 10 " v v |", "+------+ +------+ |", "|Window| |Window| |", "+------+ +------+ |", " | |", // 15 " v |", " +----+ |", " |View| |", " +----+ |", " | |", // 20 " v |", " +--------+ |", " |Document|<----+", " +--------+", }, []string{ "Path{[(6,0) (7,0) (8,0) (9,0) (10,0) (11,0) (12,0) (13,0) (13,1) (13,2) (12,2) (11,2) (10,2) (9,2) (8,2) (7,2) (6,2) (6,1)]}", "Path{[(14,1) (15,1) (16,1) (17,1) (18,1) (19,1) (20,1) (21,1) (22,1) (23,1) (24,1) (25,1) (26,1) (27,1) (28,1) (29,1) (30,1) (31,1) (32,1) (33,1) (34,1) (35,1) (36,1) (36,2) (36,3)]}", "Path{[(14,1) (15,1) (16,1) (17,1) (18,1) (19,1) (20,1) (21,1) (22,1) (23,1) (24,1) (25,1) (26,1) (27,1) (27,2) (27,3) (27,4) (27,5) (27,6) (27,7) (27,8) (27,9) (27,10) (27,11) (27,12) (27,13) (27,14) (27,15) (27,16) (27,17) (27,18) (27,19) (27,20) (27,21) (27,22) (27,23) (26,23) (25,23) (24,23) (23,23) (22,23)]}", "Path{[(10,3) (10,4)]}", "Path{[(31,4) (32,4) (33,4) (34,4) (35,4) (36,4) (37,4) (38,4) (39,4) (40,4) (40,5) (40,6) (39,6) (38,6) (37,6) (36,6) (35,6) (34,6) (33,6) (32,6) (31,6) (31,5)]}", "Path{[(6,5) (7,5) (8,5) (9,5) (10,5) (11,5) (12,5) (13,5) (13,6) (13,7) (12,7) (11,7) (10,7) (9,7) (8,7) (7,7) (6,7) (6,6)]}", "Path{[(9,8) (9,9)]}", "Path{[(9,9) (8,9) (7,9) (6,9) (5,9) (4,9) (3,9) (3,10) (3,11)]}", "Path{[(9,9) (10,9) (11,9) (12,9) (13,9) (14,9) (15,9) (16,9) (17,9) (17,10) (17,11)]}", "Path{[(0,12) (1,12) (2,12) (3,12) (4,12) (5,12) (6,12) (7,12) (7,13) (7,14) (6,14) (5,14) (4,14) (3,14) (2,14) (1,14) (0,14) (0,13)]}", "Path{[(13,12) (14,12) (15,12) (16,12) (17,12) (18,12) (19,12) (20,12) (20,13) (20,14) (19,14) (18,14) (17,14) (16,14) (15,14) (14,14) (13,14) (13,13)]}", "Path{[(16,15) (16,16)]}", "Path{[(14,17) (15,17) (16,17) (17,17) (18,17) (19,17) (19,18) (19,19) (18,19) (17,19) (16,19) (15,19) (14,19) (14,18)]}", "Path{[(16,20) (16,21)]}", "Path{[(12,22) (13,22) (14,22) (15,22) (16,22) (17,22) (18,22) (19,22) (20,22) (21,22) (21,23) (21,24) (20,24) (19,24) (18,24) (17,24) (16,24) (15,24) (14,24) (13,24) (12,24) (12,23)]}", "Text{(7,1) \"Editor\"}", "Text{(32,5) \"Document\"}", "Text{(7,6) \"Window\"}", "Text{(1,13) \"Window\"}", "Text{(14,13) \"Window\"}", "Text{(15,18) \"View\"}", "Text{(13,23) \"Document\"}", }, []string{ "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "Editor", "Document", "Window", "Window", "Window", "View", "Document", }, [][]point{ {{x: 6, y: 0}, {x: 13, y: 0}, {x: 13, y: 2}, {x: 6, y: 2}}, {{x: 14, y: 1}, {x: 36, y: 1}, {x: 36, y: 3, hint: 3}}, {{x: 14, y: 1}, {x: 27, y: 1}, {x: 27, y: 23}, {x: 22, y: 23, hint: 3}}, {{x: 10, y: 3}, {x: 10, y: 4, hint: 3}}, {{x: 31, y: 4}, {x: 40, y: 4}, {x: 40, y: 6}, {x: 31, y: 6}}, {{x: 6, y: 5}, {x: 13, y: 5}, {x: 13, y: 7}, {x: 6, y: 7}}, {{x: 9, y: 8}, {x: 9, y: 9}}, {{x: 9, y: 9}, {x: 3, y: 9}, {x: 3, y: 11, hint: 3}}, {{x: 9, y: 9}, {x: 17, y: 9}, {x: 17, y: 11, hint: 3}}, {{x: 0, y: 12}, {x: 7, y: 12}, {x: 7, y: 14}, {x: 0, y: 14}}, {{x: 13, y: 12}, {x: 20, y: 12}, {x: 20, y: 14}, {x: 13, y: 14}}, {{x: 16, y: 15}, {x: 16, y: 16, hint: 3}}, {{x: 14, y: 17}, {x: 19, y: 17}, {x: 19, y: 19}, {x: 14, y: 19}}, {{x: 16, y: 20}, {x: 16, y: 21, hint: 3}}, {{x: 12, y: 22}, {x: 21, y: 22}, {x: 21, y: 24}, {x: 12, y: 24}}, {{x: 7, y: 1}, {x: 12, y: 1}}, {{x: 32, y: 5}, {x: 39, y: 5}}, {{x: 7, y: 6}, {x: 12, y: 6}}, {{x: 1, y: 13}, {x: 6, y: 13}}, {{x: 14, y: 13}, {x: 19, y: 13}}, {{x: 15, y: 18}, {x: 18, y: 18}}, {{x: 13, y: 23}, {x: 20, y: 23}}, }, false, }, // 8 Interwined lines. { []string{ " +-----+-------+", " | | |", " | | |", " +----+-----+---- |", "--------+----+-----+-------+---+", " | | | | |", " | | | | | | |", " | | | | | | |", " | | | | | | |", "--------+----+-----+-------+---+-----+---+--+", " | | | | | | | |", " | | | | | | | |", " | -+-----+-------+---+-----+ | |", " | | | | | | | |", " | | | | +-----+---+--+", " | | | | |", " | | | | |", " --------+-----+-------+---------+---+-----", " | | | | |", " +-----+-------+---------+---+", }, // TODO(dhobsd): it's a tad overwhelming. nil, nil, nil, false, }, // 9 Indented box { []string{ "", " +-+", " | |", " +-+", }, []string{"Path{[(1,1) (2,1) (3,1) (3,2) (3,3) (2,3) (1,3) (1,2)]}"}, []string{""}, [][]point{{{x: 1, y: 1}, {x: 3, y: 1}, {x: 3, y: 3}, {x: 1, y: 3}}}, false, }, // 10 Diagonal lines with arrows { []string{ "^ ^", " \\ /", " \\ /", " \\ /", " v v", }, []string{"Path{[(0,0) (1,1) (2,2) (3,3) (4,4)]}", "Path{[(11,0) (10,1) (9,2) (8,3) (7,4)]}"}, []string{"", ""}, [][]point{ {{x: 0, y: 0, hint: 2}, {x: 4, y: 4, hint: 3}}, {{x: 11, y: 0, hint: 2}, {x: 7, y: 4, hint: 3}}, }, false, }, // 11 Diagonal lines forming an object { []string{ " .-----.", " / \\", " / \\", "+ +", "| |", "| |", "+ +", " \\ /", " \\ /", " '-----'", }, []string{"Path{[(3,0) (4,0) (5,0) (6,0) (7,0) (8,0) (9,0) (10,1) (11,2) (12,3) (12,4) (12,5) (12,6) (11,7) (10,8) (9,9) (8,9) (7,9) (6,9) (5,9) (4,9) (3,9) (2,8) (1,7) (0,6) (0,5) (0,4) (0,3) (1,2) (2,1)]}"}, []string{""}, [][]point{{ {x: 3, y: 0}, {x: 9, y: 0}, {x: 12, y: 3}, {x: 12, y: 6}, {x: 9, y: 9}, {x: 3, y: 9}, {x: 0, y: 6}, {x: 0, y: 3}, }}, false, }, // 12 A2S logo { []string{ ".-------------------------.", "| |", "| .---.-. .-----. .-----. |", "| | .-. | +--> | | <--+ |", "| | '-' | | <--+ +--> | |", "| '---'-' '-----' '-----' |", "| ascii 2 svg |", "| |", "'-------------------------'", }, []string{ "Path{[(0,0) (1,0) (2,0) (3,0) (4,0) (5,0) (6,0) (7,0) (8,0) (9,0) (10,0) (11,0) (12,0) (13,0) (14,0) (15,0) (16,0) (17,0) (18,0) (19,0) (20,0) (21,0) (22,0) (23,0) (24,0) (25,0) (26,0) (26,1) (26,2) (26,3) (26,4) (26,5) (26,6) (26,7) (26,8) (25,8) (24,8) (23,8) (22,8) (21,8) (20,8) (19,8) (18,8) (17,8) (16,8) (15,8) (14,8) (13,8) (12,8) (11,8) (10,8) (9,8) (8,8) (7,8) (6,8) (5,8) (4,8) (3,8) (2,8) (1,8) (0,8) (0,7) (0,6) (0,5) (0,4) (0,3) (0,2) (0,1)]}", "Path{[(2,2) (3,2) (4,2) (5,2) (6,2) (7,2) (8,2) (8,3) (8,4) (8,5) (7,5) (6,5) (5,5) (4,5) (3,5) (2,5) (2,4) (2,3)]}", "Path{[(2,2) (3,2) (4,2) (5,2) (6,2) (7,2) (8,2) (8,3) (8,4) (8,5) (7,5) (6,5) (6,4) (5,4) (4,4) (4,3) (5,3) (6,3)]}", "Path{[(10,2) (11,2) (12,2) (13,2) (14,2) (15,2) (16,2) (16,3) (16,4) (15,4) (14,4) (13,4)]}", "Path{[(10,2) (11,2) (12,2) (13,2) (14,2) (15,2) (16,2) (16,3) (16,4) (16,5) (15,5) (14,5) (13,5) (12,5) (11,5) (10,5) (10,4) (10,3)]}", "Path{[(18,2) (19,2) (20,2) (21,2) (22,2) (23,2) (24,2) (24,3) (23,3) (22,3) (21,3)]}", "Path{[(18,2) (19,2) (20,2) (21,2) (22,2) (23,2) (24,2) (24,3) (24,4) (24,5) (23,5) (22,5) (21,5) (20,5) (19,5) (18,5) (18,4) (19,4) (20,4) (21,4)]}", "Path{[(18,2) (19,2) (20,2) (21,2) (22,2) (23,2) (24,2) (24,3) (24,4) (24,5) (23,5) (22,5) (21,5) (20,5) (19,5) (18,5) (18,4) (18,3)]}", "Path{[(10,3) (11,3) (12,3) (13,3)]}", "Text{(3,6) \"ascii\"}", "Text{(13,6) \"2\"}", "Text{(20,6) \"svg\"}", }, []string{"", "", "", "", "", "", "", "", "", "ascii", "2", "svg"}, [][]point{ {{x: 0, y: 0}, {x: 26, y: 0}, {x: 26, y: 8}, {x: 0, y: 8}}, {{x: 2, y: 2}, {x: 8, y: 2}, {x: 8, y: 5}, {x: 2, y: 5}}, {{x: 2, y: 2}, {x: 8, y: 2}, {x: 8, y: 5}, {x: 6, y: 5}, {x: 6, y: 4}, {x: 4, y: 4}, {x: 4, y: 3}, {x: 6, y: 3}}, {{x: 10, y: 2}, {x: 16, y: 2}, {x: 16, y: 4}, {x: 13, y: 4, hint: 3}}, {{x: 10, y: 2}, {x: 16, y: 2}, {x: 16, y: 5}, {x: 10, y: 5}}, {{x: 18, y: 2}, {x: 24, y: 2}, {x: 24, y: 3}, {x: 21, y: 3, hint: 3}}, {{x: 18, y: 2}, {x: 24, y: 2}, {x: 24, y: 5}, {x: 18, y: 5}, {x: 18, y: 4}, {x: 21, y: 4, hint: 3}}, {{x: 18, y: 2}, {x: 24, y: 2}, {x: 24, y: 5}, {x: 18, y: 5}}, {{x: 10, y: 3}, {x: 13, y: 3, hint: 3}}, {{x: 3, y: 6}, {x: 7, y: 6}}, {{x: 13, y: 6}}, {{x: 20, y: 6}, {x: 22, y: 6}}, }, false, }, // 13 Ticks and dots in lines. { []string{ " ------x----->", "", " <-----*------", }, []string{"Path{[(1,0) (2,0) (3,0) (4,0) (5,0) (6,0) (7,0) (8,0) (9,0) (10,0) (11,0) (12,0) (13,0)]}", "Path{[(1,2) (2,2) (3,2) (4,2) (5,2) (6,2) (7,2) (8,2) (9,2) (10,2) (11,2) (12,2) (13,2)]}"}, []string{"", ""}, [][]point{ { {x: 1, y: 0, hint: 0}, {x: 2, y: 0, hint: 0}, {x: 3, y: 0, hint: 0}, {x: 4, y: 0, hint: 0}, {x: 5, y: 0, hint: 0}, {x: 6, y: 0, hint: 0}, {x: 7, y: 0, hint: 4}, {x: 8, y: 0, hint: 0}, {x: 9, y: 0, hint: 0}, {x: 10, y: 0, hint: 0}, {x: 11, y: 0, hint: 0}, {x: 12, y: 0, hint: 0}, {x: 13, y: 0, hint: 3}, }, { {x: 1, y: 2, hint: 2}, {x: 2, y: 2, hint: 0}, {x: 3, y: 2, hint: 0}, {x: 4, y: 2, hint: 0}, {x: 5, y: 2, hint: 0}, {x: 6, y: 2, hint: 0}, {x: 7, y: 2, hint: 5}, {x: 8, y: 2, hint: 0}, {x: 9, y: 2, hint: 0}, {x: 10, y: 2, hint: 0}, {x: 11, y: 2, hint: 0}, {x: 12, y: 2, hint: 0}, {x: 13, y: 2, hint: 0}, }, }, true, }, // 14 Multiple closed path on one object { []string{ "+-+-+", "| | |", "+-+-+", }, []string{ "Path{[(0,0) (1,0) (2,0) (3,0) (4,0) (4,1) (4,2) (3,2) (2,2) (1,2) (0,2) (0,1)]}", "Path{[(0,0) (1,0) (2,0) (3,0) (4,0) (4,1) (4,2) (3,2) (2,2) (2,1)]}", // TODO (2,0) }, []string{"", ""}, [][]point{ { {0, 0, 0}, {1, 0, 0}, {2, 0, 0}, {3, 0, 0}, {4, 0, 0}, {4, 1, 0}, {4, 2, 0}, {3, 2, 0}, {2, 2, 0}, {1, 2, 0}, {0, 2, 0}, {0, 1, 0}, }, { {0, 0, 0}, {1, 0, 0}, {2, 0, 0}, {3, 0, 0}, {4, 0, 0}, {4, 1, 0}, {4, 2, 0}, {3, 2, 0}, {2, 2, 0}, {2, 1, 0}, // TODO: {2, 0, 0} }, }, true, }, } for i, line := range data { c, err := newCanvas([]byte(strings.Join(line.input, "\n"))) if err != nil { t.Fatalf("Test %d: error creating canvas: %s", i, err) } objs := c.objects() if line.strings != nil { if got := getStrings(objs); !reflect.DeepEqual(line.strings, got) { t.Errorf("%d: expected %q, but got %q", i, line.strings, got) } } if line.texts != nil { if got := getTexts(objs); !reflect.DeepEqual(line.texts, got) { t.Errorf("%d: expected %q, but got %q", i, line.texts, got) } } if line.points != nil { if line.allPoints == false { if got := getCorners(objs); !reflect.DeepEqual(line.points, got) { t.Errorf("%d: expected %q, but got %q", i, line.points, got) } } else { if got := getPoints(objs); !reflect.DeepEqual(line.points, got) { t.Errorf("%d: expected %q, but got %q", i, line.points, got) } } } } } func TestNewCanvasBroken(t *testing.T) { // These are the ones that do not give the desired result. t.Parallel() data := []struct { input []string strings []string texts []string corners [][]point }{ // 0 URL { []string{ "github.com/foo/bar", }, []string{"Text{(0,0) \"github.com/foo/bar\"}"}, []string{"github.com/foo/bar"}, [][]point{{{x: 0, y: 0}, {x: 17, y: 0}}}, }, // 1 Merged boxes { []string{ "+-+-+", "| | |", "+-+-+", }, []string{"Path{[(0,0) (1,0) (2,0) (3,0) (4,0) (4,1) (4,2) (3,2) (2,2) (1,2) (0,2) (0,1)]}", "Path{[(0,0) (1,0) (2,0) (3,0) (4,0) (4,1) (4,2) (3,2) (2,2) (2,1)]}"}, []string{"", ""}, // TODO(dhobsd): BROKEN. [][]point{ {{x: 0, y: 0}, {x: 4, y: 0}, {x: 4, y: 2}, {x: 0, y: 2}}, {{x: 0, y: 0}, {x: 4, y: 0}, {x: 4, y: 2}, {x: 2, y: 2}, {x: 2, y: 1}}, }, }, // 2 Adjacent boxes { // TODO(dhobsd): BROKEN. This one is hard, as it can be seen as 3 boxes // but that is not what is desired. []string{ "+-++-+", "| || |", "+-++-+", }, []string{ "Path{[(0,0) (1,0) (2,0) (3,0) (4,0) (5,0) (5,1) (5,2) (4,2) (3,2) (2,2) (1,2) (0,2) (0,1)]}", "Path{[(0,0) (1,0) (2,0) (3,0) (4,0) (5,0) (5,1) (5,2) (4,2) (3,2) (2,2) (2,1)]}", "Path{[(0,0) (1,0) (2,0) (3,0) (4,0) (5,0) (5,1) (5,2) (4,2) (3,2) (3,1)]}", }, []string{"", "", ""}, [][]point{ {{x: 0, y: 0}, {x: 5, y: 0}, {x: 5, y: 2}, {x: 0, y: 2}}, {{x: 0, y: 0}, {x: 5, y: 0}, {x: 5, y: 2}, {x: 2, y: 2}, {x: 2, y: 1}}, {{x: 0, y: 0}, {x: 5, y: 0}, {x: 5, y: 2}, {x: 3, y: 2}, {x: 3, y: 1}}, }, }, } for i, line := range data { c, err := newCanvas([]byte(strings.Join(line.input, "\n"))) if err != nil { t.Fatalf("Test %d: error creating canvas: %s", i, err) } objs := c.objects() if line.strings != nil { if got := getStrings(objs); !reflect.DeepEqual(line.strings, got) { t.Errorf("%d: expected %q, but got %q", i, line.strings, got) } } if line.texts != nil { if got := getTexts(objs); !reflect.DeepEqual(line.texts, got) { t.Errorf("%d: expected %q, but got %q", i, line.texts, got) } } if line.corners != nil { if got := getCorners(objs); !reflect.DeepEqual(line.corners, got) { t.Errorf("%d: expected %q, but got %q", i, line.corners, got) } } } } func TestPointsToCorners(t *testing.T) { t.Parallel() data := []struct { in []point expected []point closed bool }{ { []point{{x: 0, y: 0}, {x: 1, y: 0}}, []point{{x: 0, y: 0}, {x: 1, y: 0}}, false, }, { []point{{x: 0, y: 0}, {x: 1, y: 0}, {x: 2, y: 0}}, []point{{x: 0, y: 0}, {x: 2, y: 0}}, false, }, { []point{{x: 0, y: 0}, {x: 1, y: 0}, {x: 1, y: 1}}, []point{{x: 0, y: 0}, {x: 1, y: 0}, {x: 1, y: 1}}, false, }, { []point{ {x: 0, y: 0}, {x: 1, y: 0}, {x: 2, y: 0}, {x: 2, y: 1}, {x: 2, y: 2}, {x: 1, y: 2}, {x: 0, y: 2}, {x: 0, y: 1}, }, []point{{x: 0, y: 0}, {x: 2, y: 0}, {x: 2, y: 2}, {x: 0, y: 2}}, true, }, { []point{{x: 0, y: 0}, {x: 1, y: 0}, {x: 1, y: 1}, {x: 0, y: 1}}, []point{{x: 0, y: 0}, {x: 1, y: 0}, {x: 1, y: 1}, {x: 0, y: 1}}, // TODO(dhobsd): Unexpected; broken. false, }, } for i, line := range data { p, c := pointsToCorners(line.in) if !reflect.DeepEqual(line.expected, p) { t.Errorf("%d: expected %v, but got %v", i, line.expected, p) } if line.closed != c { t.Errorf("%d: expected close == %v, but got %v", i, line.closed, c) } } } func BenchmarkT(b *testing.B) { data := []string{ " +-----+-------+", " | | |", " | | |", " +----+-----+---- |", "--------+----+-----+-------+---+", " | | | | |", " | | | | | | |", " | | | | | | |", " | | | | | | |", "--------+----+-----+-------+---+-----+---+--+", " | | | | | | | |", " | | | | | | | |", " | -+-----+-------+---+-----+ | |", " | | | | | | | |", " | | | | +-----+---+--+", " | | | | |", " | | | | |", " --------+-----+-------+---------+---+-----", " | | | | |", " +-----+-------+---------+---+", "", "", } chunk := []byte(strings.Join(data, "\n")) input := make([]byte, 0, len(chunk)*b.N) for i := 0; i < b.N; i++ { input = append(input, chunk...) } expected := 30 * b.N b.ResetTimer() c, err := newCanvas(input) if err != nil { b.Fatalf("Error creating canvas: %s", err) } objs := c.objects() if len(objs) != expected { b.Fatalf("%d != %d", len(objs), expected) } } // Private details. func getPoints(objs []*object) [][]point { out := [][]point{} for _, obj := range objs { out = append(out, obj.Points()) } return out } func getTexts(objs []*object) []string { out := []string{} for _, obj := range objs { t := obj.Text() if !obj.isJustText() { out = append(out, "") } else if len(t) > 0 { out = append(out, string(t)) } else { panic("failed") } } return out } func getStrings(objs []*object) []string { out := []string{} for _, obj := range objs { out = append(out, obj.String()) } return out } func getCorners(objs []*object) [][]point { out := make([][]point, len(objs)) for i, obj := range objs { out[i] = obj.Corners() } return out } |
Added parser/draw/char.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 | //----------------------------------------------------------------------------- // Copyright (c) 2022-present Detlef Stern // // This file is part of Zettelstore. // // Zettelstore is licensed under the latest version of the EUPL (European Union // Public License). Please see file LICENSE.txt for your rights and obligations // under this license. // // This file was originally created by the ASCIIToSVG contributors under an MIT // license, but later changed to fulfil the needs of Zettelstore. The following // statements affects the original code as found on // https://github.com/asciitosvg/asciitosvg (Commit: // ca82a5ce41e2190a05e07af6e8b3ea4e3256a283, 2020-11-20): // // Copyright 2012 - 2018 The ASCIIToSVG Contributors // All rights reserved. //----------------------------------------------------------------------------- package draw import "unicode" type char rune func (c char) isTextStart() bool { r := rune(c) return unicode.IsLetter(r) || unicode.IsNumber(r) || unicode.IsSymbol(r) } func (c char) isTextCont() bool { return unicode.IsPrint(rune(c)) } func (c char) isSpace() bool { return unicode.IsSpace(rune(c)) } // isPathStart returns true on any form of ascii art that can start a graph. func (c char) isPathStart() bool { return (c.isCorner() || c.isHorizontal() || c.isVertical() || c.isArrowHorizontalLeft() || c.isArrowVerticalUp() || c.isDiagonal()) && !c.isTick() && !c.isDot() } func (c char) isCorner() bool { return c == '.' || c == '\'' || c == '+' } func (c char) isRoundedCorner() bool { return c == '.' || c == '\'' } func (c char) isDashedHorizontal() bool { return c == '=' } func (c char) isHorizontal() bool { return c.isDashedHorizontal() || c.isTick() || c.isDot() || c == '-' } func (c char) isDashedVertical() bool { return c == ':' } func (c char) isVertical() bool { return c.isDashedVertical() || c.isTick() || c.isDot() || c == '|' } func (c char) isDashed() bool { return c.isDashedHorizontal() || c.isDashedVertical() } func (c char) isArrowHorizontalLeft() bool { return c == '<' } func (c char) isArrowHorizontal() bool { return c.isArrowHorizontalLeft() || c == '>' } func (c char) isArrowVerticalUp() bool { return c == '^' } func (c char) isArrowVertical() bool { return c.isArrowVerticalUp() || c == 'v' } func (c char) isArrow() bool { return c.isArrowHorizontal() || c.isArrowVertical() } func (c char) isDiagonalNorthEast() bool { return c == '/' } func (c char) isDiagonalSouthEast() bool { return c == '\\' } func (c char) isDiagonal() bool { return c.isDiagonalNorthEast() || c.isDiagonalSouthEast() } func (c char) isTick() bool { return c == 'x' } func (c char) isDot() bool { return c == '*' } // Diagonal transitions are special: you can move lines diagonally, you can move diagonally from // corners to edges or lines, but you cannot move diagonally between corners. func (c char) canDiagonalFrom(from char) bool { if from.isArrowVertical() || from.isCorner() { return c.isDiagonal() } if from.isDiagonal() { return c.isDiagonal() || c.isCorner() || c.isArrowVertical() || c.isHorizontal() || c.isVertical() } if from.isHorizontal() || from.isVertical() { return c.isDiagonal() } return false } func (c char) canHorizontal() bool { return c.isHorizontal() || c.isCorner() || c.isArrowHorizontal() } func (c char) canVertical() bool { return c.isVertical() || c.isCorner() || c.isArrowVertical() } |
Added parser/draw/draw.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 | //----------------------------------------------------------------------------- // Copyright (c) 2022-present Detlef Stern // // This file is part of Zettelstore. // // Zettelstore is licensed under the latest version of the EUPL (European Union // Public License). Please see file LICENSE.txt for your rights and obligations // under this license. //----------------------------------------------------------------------------- // Package draw provides a parser to create SVG from ASCII drawing. // // It is not a parser registered by the general parser framework (directed by // metadata "syntax" of a zettel). It will be used when a zettel is evaluated. package draw import ( "strconv" "zettelstore.de/c/attrs" "zettelstore.de/z/ast" "zettelstore.de/z/domain/meta" "zettelstore.de/z/input" "zettelstore.de/z/parser" ) func init() { parser.Register(&parser.Info{ Name: meta.SyntaxDraw, AltNames: []string{}, IsASTParser: true, IsTextFormat: true, IsImageFormat: false, ParseBlocks: parseBlocks, ParseInlines: parseInlines, }) } const ( defaultFont = "" defaultScaleX = 10 defaultScaleY = 20 ) func parseBlocks(inp *input.Input, m *meta.Meta, _ string) ast.BlockSlice { font := m.GetDefault("font", defaultFont) scaleX := m.GetNumber("x-scale", defaultScaleX) scaleY := m.GetNumber("y-scale", defaultScaleY) if scaleX < 1 || 1000000 < scaleX { scaleX = defaultScaleX } if scaleY < 1 || 1000000 < scaleY { scaleY = defaultScaleY } canvas, err := newCanvas(inp.Src[inp.Pos:]) if err != nil { return ast.BlockSlice{ast.CreateParaNode(canvasErrMsg(err)...)} } svg := canvasToSVG(canvas, font, int(scaleX), int(scaleY)) if len(svg) == 0 { return ast.BlockSlice{ast.CreateParaNode(noSVGErrMsg()...)} } return ast.BlockSlice{&ast.BLOBNode{ Description: parser.ParseDescription(m), Syntax: meta.SyntaxSVG, Blob: svg, }} } func parseInlines(inp *input.Input, _ string) ast.InlineSlice { canvas, err := newCanvas(inp.Src[inp.Pos:]) if err != nil { return canvasErrMsg(err) } svg := canvasToSVG(canvas, defaultFont, defaultScaleX, defaultScaleY) if len(svg) == 0 { return noSVGErrMsg() } return ast.InlineSlice{&ast.EmbedBLOBNode{ Attrs: nil, Syntax: meta.SyntaxSVG, Blob: svg, Inlines: nil, }} } // ParseDrawBlock parses the content of an eval verbatim node into an SVG image BLOB. func ParseDrawBlock(vn *ast.VerbatimNode) ast.BlockNode { font := defaultFont if val, found := vn.Attrs.Get("font"); found { font = val } scaleX := getScale(vn.Attrs, "x-scale", defaultScaleX) scaleY := getScale(vn.Attrs, "y-scale", defaultScaleY) canvas, err := newCanvas(vn.Content) if err != nil { return ast.CreateParaNode(canvasErrMsg(err)...) } if scaleX < 1 || 1000000 < scaleX { scaleX = defaultScaleX } if scaleY < 1 || 1000000 < scaleY { scaleY = defaultScaleY } svg := canvasToSVG(canvas, font, scaleX, scaleY) if len(svg) == 0 { return ast.CreateParaNode(noSVGErrMsg()...) } return &ast.BLOBNode{ Description: nil, // TODO: look for attribute "summary" / "title" Syntax: meta.SyntaxSVG, Blob: svg, } } func getScale(a attrs.Attributes, key string, defVal int) int { if val, found := a.Get(key); found { if n, err := strconv.Atoi(val); err == nil && 0 < n && n < 100000 { return n } } return defVal } func canvasErrMsg(err error) ast.InlineSlice { return ast.CreateInlineSliceFromWords("Error:", err.Error()) } func noSVGErrMsg() ast.InlineSlice { return ast.CreateInlineSliceFromWords("NO", "IMAGE") } |
Added parser/draw/draw_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 | //----------------------------------------------------------------------------- // Copyright (c) 2022-present Detlef Stern // // This file is part of Zettelstore. // // Zettelstore is licensed under the latest version of the EUPL (European Union // Public License). Please see file LICENSE.txt for your rights and obligations // under this license. //----------------------------------------------------------------------------- package draw_test import ( "testing" "zettelstore.de/z/config" "zettelstore.de/z/domain/meta" "zettelstore.de/z/input" "zettelstore.de/z/parser" ) func FuzzParseBlocks(f *testing.F) { f.Fuzz(func(t *testing.T, src []byte) { t.Parallel() inp := input.NewInput(src) parser.ParseBlocks(inp, nil, meta.SyntaxDraw, config.NoHTML) }) } |
Added parser/draw/object.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 | //----------------------------------------------------------------------------- // Copyright (c) 2022-present Detlef Stern // // This file is part of Zettelstore. // // Zettelstore is licensed under the latest version of the EUPL (European Union // Public License). Please see file LICENSE.txt for your rights and obligations // under this license. // // This file was originally created by the ASCIIToSVG contributors under an MIT // license, but later changed to fulfil the needs of Zettelstore. The following // statements affects the original code as found on // https://github.com/asciitosvg/asciitosvg (Commit: // ca82a5ce41e2190a05e07af6e8b3ea4e3256a283, 2020-11-20): // // Copyright 2012 - 2018 The ASCIIToSVG Contributors // All rights reserved. //----------------------------------------------------------------------------- package draw import "fmt" // object represents one of an open path, a closed path, or text. type object struct { // points always starts with the top most, then left most point, proceeding to the right. points []point text []rune corners []point isText bool isClosed bool isDashed bool } // Points returns all the points occupied by this Object. Every object has at least one point, // and all points are both in-order and contiguous. func (o *object) Points() []point { return o.points } // Corners returns all the corners (change of direction) along the path. func (o *object) Corners() []point { return o.corners } // IsClosed is true if the object is composed of a closed path. func (o *object) IsClosed() bool { return o.isClosed } // IsDashed is true if this object is a path object, and lines should be drawn dashed. func (o *object) IsDashed() bool { return o.isDashed } // Text returns the text associated with this object if textual, and nil otherwise. func (o *object) Text() []rune { return o.text } func (o *object) isOpenPath() bool { return !o.isClosed && !o.isText } func (o *object) isClosedPath() bool { return o.isClosed && !o.isText } func (o *object) isJustText() bool { return o.isText } func (o *object) String() string { if o.isJustText() { return fmt.Sprintf("Text{%s %q}", o.points[0], string(o.text)) } return fmt.Sprintf("Path{%v}", o.points) } // seal finalizes the object, setting its text, its corners, and its various rendering hints. func (o *object) seal(c *canvas) { if c.at(o.points[0]).isArrow() { o.points[0].hint = startMarker c.hasStartMarker = true } if c.at(o.points[len(o.points)-1]).isArrow() { o.points[len(o.points)-1].hint = endMarker c.hasEndMarker = true } o.corners, o.isClosed = pointsToCorners(o.points) o.text = make([]rune, len(o.points)) for i, p := range o.points { ch := c.at(p) if !o.isJustText() { if ch.isTick() { o.points[i].hint = tick } else if ch.isDot() { o.points[i].hint = dot } if ch.isDashed() { o.isDashed = true } for _, corner := range o.corners { if corner.x == p.x && corner.y == p.y && c.at(p).isRoundedCorner() { o.points[i].hint = roundedCorner } } } o.text[i] = rune(ch) } } // objects implements a sortable collection of Object interfaces. type objects []*object func (o objects) Len() int { return len(o) } func (o objects) Swap(i, j int) { o[i], o[j] = o[j], o[i] } // Less returns in order top most, then left most. func (o objects) Less(i, j int) bool { // TODO(dhobsd): This doesn't catch every z-index case we could possibly want. We should // support z-indexing of objects through an a2s tag. l := o[i] r := o[j] lt := l.isJustText() rt := r.isJustText() if lt != rt { return rt } lp := l.Points()[0] rp := r.Points()[0] if lp.y != rp.y { return lp.y < rp.y } return lp.x < rp.x } const ( dirNone = iota // No directionality dirH // Horizontal dirV // Vertical dirSE // South-East dirSW // South-West dirNW // North-West dirNE // North-East ) // pointsToCorners returns all the corners (points at which there is a change of directionality) for // a path. It additionally returns a truth value indicating whether the points supplied indicate a // closed path. func pointsToCorners(points []point) ([]point, bool) { l := len(points) // A path containing fewer than 3 points can neither be closed, nor change direction. if l < 3 { return points, false } out := []point{points[0]} dir := dirNone if isHorizontal(points[0], points[1]) { dir = dirH } else if isVertical(points[0], points[1]) { dir = dirV } else if isDiagonalSE(points[0], points[1]) { dir = dirSE } else if isDiagonalSW(points[0], points[1]) { dir = dirSW } else if isDiagonalNW(points[0], points[1]) { dir = dirNW } else if isDiagonalNE(points[0], points[1]) { dir = dirNE } else { panic(fmt.Errorf("discontiguous points: %+v", points)) } cornerFunc := func(idx, newDir int) { if dir != newDir { out = append(out, points[idx-1]) dir = newDir } } // Starting from the third point, check to see if the directionality between points P and // P-1 has changed. for i := 2; i < l; i++ { if isHorizontal(points[i-1], points[i]) { cornerFunc(i, dirH) } else if isVertical(points[i-1], points[i]) { cornerFunc(i, dirV) } else if isDiagonalSE(points[i-1], points[i]) { cornerFunc(i, dirSE) } else if isDiagonalSW(points[i-1], points[i]) { cornerFunc(i, dirSW) } else if isDiagonalNW(points[i-1], points[i]) { cornerFunc(i, dirNW) } else if isDiagonalNE(points[i-1], points[i]) { cornerFunc(i, dirNE) } else { panic(fmt.Errorf("discontiguous points: %+v", points)) } } // Check if the points indicate a closed path. If not, append the last point. last := points[l-1] closed := true closedFunc := func(newDir int) { if dir != newDir { closed = false out = append(out, last) } } if isHorizontal(points[0], last) { closedFunc(dirH) } else if isVertical(points[0], last) { closedFunc(dirV) } else if isDiagonalNE(last, points[0]) { closedFunc(dirNE) } else { // Note: we'll always find any closed polygon from its top-left-most point. If it // is closed, it must be closed in the north-easterly direction, thus we don't test // for any other types of polygone closure. closed = false out = append(out, last) } return out, closed } |
Added parser/draw/point.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 | //----------------------------------------------------------------------------- // Copyright (c) 2022-present Detlef Stern // // This file is part of Zettelstore. // // Zettelstore is licensed under the latest version of the EUPL (European Union // Public License). Please see file LICENSE.txt for your rights and obligations // under this license. // // This file was originally created by the ASCIIToSVG contributors under an MIT // license, but later changed to fulfil the needs of Zettelstore. The following // statements affects the original code as found on // https://github.com/asciitosvg/asciitosvg (Commit: // ca82a5ce41e2190a05e07af6e8b3ea4e3256a283, 2020-11-20): // // Copyright 2012 - 2018 The ASCIIToSVG Contributors // All rights reserved. //----------------------------------------------------------------------------- package draw import "fmt" // A renderHint suggests ways the SVG renderer may appropriately represent this point. type renderHint uint8 const ( _ renderHint = iota roundedCorner // the renderer should smooth corners on this path. startMarker // this point should have an SVG marker-start attribute. endMarker // this point should have an SVG marker-end attribute. tick // the renderer should mark a tick in the path at this point. dot // the renderer should insert a filled dot in the path at this point. ) // A point is an X,Y coordinate in the diagram's grid. The grid represents (0, 0) as the top-left // of the diagram. The point also provides hints to the renderer as to how it should be interpreted. type point struct { x, y int hint renderHint } // String implements fmt.Stringer on Point. func (p point) String() string { return fmt.Sprintf("(%d,%d)", p.x, p.y) } // isHorizontal returns true if p1 and p2 are horizontally aligned. func isHorizontal(p1, p2 point) bool { d := p1.x - p2.x return d <= 1 && d >= -1 && p1.y == p2.y } // isVertical returns true if p1 and p2 are vertically aligned. func isVertical(p1, p2 point) bool { d := p1.y - p2.y return d <= 1 && d >= -1 && p1.x == p2.x } // The following functions return true when the diagonals are connected in various compass directions. func isDiagonalSE(p1, p2 point) bool { return p1.x-p2.x == -1 && p1.y-p2.y == -1 } func isDiagonalSW(p1, p2 point) bool { return p1.x-p2.x == 1 && p1.y-p2.y == -1 } func isDiagonalNW(p1, p2 point) bool { return p1.x-p2.x == 1 && p1.y-p2.y == 1 } func isDiagonalNE(p1, p2 point) bool { return p1.x-p2.x == -1 && p1.y-p2.y == 1 } |
Added parser/draw/svg.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 | //----------------------------------------------------------------------------- // Copyright (c) 2022-present Detlef Stern // // This file is part of Zettelstore. // // Zettelstore is licensed under the latest version of the EUPL (European Union // Public License). Please see file LICENSE.txt for your rights and obligations // under this license. // // This file was originally created by the ASCIIToSVG contributors under an MIT // license, but later changed to fulfil the needs of Zettelstore. The following // statements affects the original code as found on // https://github.com/asciitosvg/asciitosvg (Commit: // ca82a5ce41e2190a05e07af6e8b3ea4e3256a283, 2020-11-20): // // Copyright 2012 - 2018 The ASCIIToSVG Contributors // All rights reserved. //----------------------------------------------------------------------------- package draw import ( "bytes" "fmt" "io" "strings" "zettelstore.de/z/strfun" ) // canvasToSVG renders the supplied asciitosvg.Canvas to SVG, based on the supplied options. func canvasToSVG(c *canvas, font string, scaleX, scaleY int) []byte { if len(c.objects()) == 0 { return nil } if font == "" { font = "monospace" } var b bytes.Buffer fmt.Fprintf(&b, `<svg class="zs-draw" width="%d" height="%d" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">`, (c.size().X+1)*scaleX, (c.size().Y+1)*scaleY) writeMarkerDefs(&b, c, scaleX, scaleY) // 3 passes, first closed paths, then open paths, then text. writeClosedPaths(&b, c, scaleX, scaleY) writeOpenPaths(&b, c, scaleX, scaleY) writeTexts(&b, c, escape(font), scaleX, scaleY) io.WriteString(&b, "</svg>") return b.Bytes() } const ( nameStartMarker = "iPointer" nameEndMarker = "Pointer" ) func writeMarkerDefs(w io.Writer, c *canvas, scaleX, scaleY int) { const markerTag = `<marker id="%s" viewBox="0 0 10 10" refX="5" refY="5" markerUnits="strokeWidth" markerWidth="%g" markerHeight="%g" orient="auto"><path d="%s" /></marker>` x := float64(scaleX) / 2 y := float64(scaleY) / 2 if c.hasStartMarker { fmt.Fprintf(w, markerTag, nameStartMarker, x, y, "M 10 0 L 10 10 L 0 5 z") } if c.hasEndMarker { fmt.Fprintf(w, markerTag, nameEndMarker, x, y, "M 0 0 L 10 5 L 0 10 z") } } const pathTag = `<path id="%s%d" %sd="%s" />` func writeClosedPaths(w io.Writer, c *canvas, scaleX, scaleY int) { first := true for i, obj := range c.objects() { if !obj.isClosedPath() { continue } if first { io.WriteString(w, `<g id="closed" stroke="#000" stroke-width="2" fill="none">`) first = false } opts := "" if obj.IsDashed() { opts = `stroke-dasharray="5 5" ` } fmt.Fprintf(w, pathTag, "closed", i, opts, flatten(obj.Points(), scaleX, scaleY)+"Z") } if !first { io.WriteString(w, "</g>") } } func writeOpenPaths(w io.Writer, c *canvas, scaleX, scaleY int) { const optStartMarker = `marker-start="url(#` + nameStartMarker + `)" ` const optEndMarker = `marker-end="url(#` + nameEndMarker + `)" ` first := true for i, obj := range c.objects() { if !obj.isOpenPath() { continue } if first { io.WriteString(w, `<g id="lines" stroke="#000" stroke-width="2" fill="none">`) first = false } points := obj.Points() for _, p := range points { switch p.hint { case dot: sp := scale(p, scaleX, scaleY) fmt.Fprintf(w, `<circle cx="%g" cy="%g" r="3" fill="#000" />`, sp.X, sp.Y) case tick: sp := scale(p, scaleX, scaleY) const tickLine = `<line x1="%g" y1="%g" x2="%g" y2="%g" stroke-width="2" />` fmt.Fprintf(w, tickLine, sp.X-4, sp.Y-4, sp.X+4, sp.Y+4) fmt.Fprintf(w, tickLine, sp.X+4, sp.Y-4, sp.X-4, sp.Y+4) } } opts := "" if obj.IsDashed() { opts += `stroke-dasharray="5 5" ` } if points[0].hint == startMarker { opts += optStartMarker } if points[len(points)-1].hint == endMarker { opts += optEndMarker } fmt.Fprintf(w, pathTag, "open", i, opts, flatten(points, scaleX, scaleY)) } if !first { io.WriteString(w, "</g>") } } func writeTexts(w io.Writer, c *canvas, font string, scaleX, scaleY int) { fontSize := float64(scaleY) * 0.75 deltaX := float64(scaleX) / 4 deltaY := float64(scaleY) / 4 first := true for i, obj := range c.objects() { if !obj.isJustText() { continue } if first { fmt.Fprintf(w, `<g id="text" stroke="none" style="font-family:%s;font-size:%gpx">`, font, fontSize) first = false } text := string(obj.Text()) sp := scale(obj.Points()[0], scaleX, scaleY) fmt.Fprintf(w, `<text id="obj%d" x="%g" y="%g">%s</text>`, i, sp.X-deltaX, sp.Y+deltaY, escape(text)) } if !first { io.WriteString(w, "</g>") } } func escape(s string) string { var sb strings.Builder strfun.XMLEscape(&sb, s) return sb.String() } type scaledPoint struct { X float64 Y float64 Hint renderHint } func scale(p point, scaleX, scaleY int) scaledPoint { return scaledPoint{ X: (float64(p.x) + .5) * float64(scaleX), Y: (float64(p.y) + .5) * float64(scaleY), Hint: p.hint, } } func flatten(points []point, scaleX, scaleY int) string { var result strings.Builder // Scaled start point, and previous point (which is always initially the start point). sp := scale(points[0], scaleX, scaleY) pp := sp for i, cp := range points { p := scale(cp, scaleX, scaleY) // Our start point is represented by a single moveto command (unless the start point // is a rounded corner) as the shape will be closed with the Z command automatically // if we have a closed polygon. If our start point is a rounded corner, we have to go // ahead and draw that curve. if i == 0 { if cp.hint == roundedCorner { fmt.Fprintf(&result, "M %g %g Q %g %g %g %g ", p.X, p.Y+10, p.X, p.Y, p.X+10, p.Y) continue } fmt.Fprintf(&result, "M %g %g ", p.X, p.Y) continue } // If this point has a rounded corner, we need to calculate the curve. This algorithm // only works when the shapes are drawn in a clockwise manner. if cp.hint == roundedCorner { // The control point is always the original corner. cx := p.X cy := p.Y sx, sy, ex, ey := 0., 0., 0., 0. // We need to know the next point to determine which way to turn. var np scaledPoint if i == len(points)-1 { np = sp } else { np = scale(points[i+1], scaleX, scaleY) } if pp.X == p.X { // If we're on the same vertical axis, our starting X coordinate is // the same as the control point coordinate sx = p.X // Offset start point from control point in the proper direction. if pp.Y < p.Y { sy = p.Y - 10 } else { sy = p.Y + 10 } ey = p.Y // Offset endpoint from control point in the proper direction. if np.X < p.X { ex = p.X - 10 } else { ex = p.X + 10 } } else if pp.Y == p.Y { // Horizontal decisions mirror vertical's above. sy = p.Y if pp.X < p.X { sx = p.X - 10 } else { sx = p.X + 10 } ex = p.X if np.Y <= p.Y { ey = p.Y - 10 } else { ey = p.Y + 10 } } fmt.Fprintf(&result, "L %g %g Q %g %g %g %g ", sx, sy, cx, cy, ex, ey) } else { // Just draw a straight line. fmt.Fprintf(&result, "L %g %g ", p.X, p.Y) } pp = p } return result.String() } |
Added parser/draw/svg_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 | //----------------------------------------------------------------------------- // Copyright (c) 2022-present Detlef Stern // // This file is part of Zettelstore. // // Zettelstore is licensed under the latest version of the EUPL (European Union // Public License). Please see file LICENSE.txt for your rights and obligations // under this license. // // This file was originally created by the ASCIIToSVG contributors under an MIT // license, but later changed to fulfil the needs of Zettelstore. The following // statements affects the original code as found on // https://github.com/asciitosvg/asciitosvg (Commit: // ca82a5ce41e2190a05e07af6e8b3ea4e3256a283, 2020-11-20): // // Copyright 2012 - 2018 The ASCIIToSVG Contributors // All rights reserved. //----------------------------------------------------------------------------- package draw import ( "strings" "testing" ) func TestCanvasToSVG(t *testing.T) { t.Parallel() data := []struct { input []string length int }{ // 0 Box with dashed corners and text { []string{ "+--.", "|Hi:", "+--+", }, 482, }, // 2 Ticks and dots in lines. { []string{ " ------x----->", "", " <-----*------", }, 1084, }, // 3 Just text { []string{ " foo", }, 261, }, } for i, line := range data { canvas, err := newCanvas([]byte(strings.Join(line.input, "\n"))) if err != nil { t.Fatalf("Error creating canvas: %s", err) } actual := string(canvasToSVG(canvas, "", 9, 16)) // TODO(dhobsd): Use golden file? Worth postponing once output is actually // nice. if line.length != len(actual) { t.Errorf("%d: expected length %d, but got %d\n%q", i, line.length, len(actual), actual) } } } |
Added parser/markdown/markdown.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 | //----------------------------------------------------------------------------- // Copyright (c) 2020-present Detlef Stern // // This file is part of Zettelstore. // // Zettelstore is licensed under the latest version of the EUPL (European Union // Public License). Please see file LICENSE.txt for your rights and obligations // under this license. //----------------------------------------------------------------------------- // Package markdown provides a parser for markdown. package markdown import ( "bytes" "fmt" "strconv" "strings" gm "github.com/yuin/goldmark" gmAst "github.com/yuin/goldmark/ast" gmText "github.com/yuin/goldmark/text" "zettelstore.de/c/attrs" "zettelstore.de/z/ast" "zettelstore.de/z/domain/meta" "zettelstore.de/z/encoder/textenc" "zettelstore.de/z/input" "zettelstore.de/z/parser" ) func init() { parser.Register(&parser.Info{ Name: meta.SyntaxMarkdown, AltNames: []string{meta.SyntaxMD}, IsASTParser: true, IsTextFormat: true, IsImageFormat: false, ParseBlocks: parseBlocks, ParseInlines: parseInlines, }) } func parseBlocks(inp *input.Input, _ *meta.Meta, _ string) ast.BlockSlice { p := parseMarkdown(inp) return p.acceptBlockChildren(p.docNode) } func parseInlines(inp *input.Input, syntax string) ast.InlineSlice { bs := parseBlocks(inp, nil, syntax) return bs.FirstParagraphInlines() } func parseMarkdown(inp *input.Input) *mdP { source := []byte(inp.Src[inp.Pos:]) parser := gm.DefaultParser() node := parser.Parse(gmText.NewReader(source)) textEnc := textenc.Create() return &mdP{source: source, docNode: node, textEnc: textEnc} } type mdP struct { source []byte docNode gmAst.Node textEnc *textenc.Encoder } func (p *mdP) acceptBlockChildren(docNode gmAst.Node) ast.BlockSlice { if docNode.Type() != gmAst.TypeDocument { panic(fmt.Sprintf("Expected document, but got node type %v", docNode.Type())) } result := make(ast.BlockSlice, 0, docNode.ChildCount()) for child := docNode.FirstChild(); child != nil; child = child.NextSibling() { if block := p.acceptBlock(child); block != nil { result = append(result, block) } } return result } func (p *mdP) acceptBlock(node gmAst.Node) ast.ItemNode { if node.Type() != gmAst.TypeBlock { panic(fmt.Sprintf("Expected block node, but got node type %v", node.Type())) } switch n := node.(type) { case *gmAst.Paragraph: return p.acceptParagraph(n) case *gmAst.TextBlock: return p.acceptTextBlock(n) case *gmAst.Heading: return p.acceptHeading(n) case *gmAst.ThematicBreak: return p.acceptThematicBreak() case *gmAst.CodeBlock: return p.acceptCodeBlock(n) case *gmAst.FencedCodeBlock: return p.acceptFencedCodeBlock(n) case *gmAst.Blockquote: return p.acceptBlockquote(n) case *gmAst.List: return p.acceptList(n) case *gmAst.HTMLBlock: return p.acceptHTMLBlock(n) } panic(fmt.Sprintf("Unhandled block node of kind %v", node.Kind())) } func (p *mdP) acceptParagraph(node *gmAst.Paragraph) ast.ItemNode { if is := p.acceptInlineChildren(node); len(is) > 0 { return &ast.ParaNode{Inlines: is} } return nil } func (p *mdP) acceptHeading(node *gmAst.Heading) *ast.HeadingNode { return &ast.HeadingNode{ Level: node.Level, Inlines: p.acceptInlineChildren(node), Attrs: nil, } } func (*mdP) acceptThematicBreak() *ast.HRuleNode { return &ast.HRuleNode{ Attrs: nil, //TODO } } func (p *mdP) acceptCodeBlock(node *gmAst.CodeBlock) *ast.VerbatimNode { return &ast.VerbatimNode{ Kind: ast.VerbatimProg, Attrs: nil, //TODO Content: p.acceptRawText(node), } } func (p *mdP) acceptFencedCodeBlock(node *gmAst.FencedCodeBlock) *ast.VerbatimNode { var a attrs.Attributes if language := node.Language(p.source); len(language) > 0 { a = a.Set("class", "language-"+cleanText(language, true)) } return &ast.VerbatimNode{ Kind: ast.VerbatimProg, Attrs: a, Content: p.acceptRawText(node), } } func (p *mdP) acceptRawText(node gmAst.Node) []byte { lines := node.Lines() result := make([]byte, 0, 512) for i := 0; i < lines.Len(); i++ { s := lines.At(i) line := s.Value(p.source) if l := len(line); l > 0 { if l > 1 && line[l-2] == '\r' && line[l-1] == '\n' { line = line[0 : l-2] } else if line[l-1] == '\n' || line[l-1] == '\r' { line = line[0 : l-1] } } if i > 0 { result = append(result, '\n') } result = append(result, line...) } return result } func (p *mdP) acceptBlockquote(node *gmAst.Blockquote) *ast.NestedListNode { return &ast.NestedListNode{ Kind: ast.NestedListQuote, Items: []ast.ItemSlice{ p.acceptItemSlice(node), }, } } func (p *mdP) acceptList(node *gmAst.List) ast.ItemNode { kind := ast.NestedListUnordered var a attrs.Attributes if node.IsOrdered() { kind = ast.NestedListOrdered if node.Start != 1 { a = a.Set("start", strconv.Itoa(node.Start)) } } items := make([]ast.ItemSlice, 0, node.ChildCount()) for child := node.FirstChild(); child != nil; child = child.NextSibling() { item, ok := child.(*gmAst.ListItem) if !ok { panic(fmt.Sprintf("Expected list item node, but got %v", child.Kind())) } items = append(items, p.acceptItemSlice(item)) } return &ast.NestedListNode{ Kind: kind, Items: items, Attrs: a, } } func (p *mdP) acceptItemSlice(node gmAst.Node) ast.ItemSlice { result := make(ast.ItemSlice, 0, node.ChildCount()) for elem := node.FirstChild(); elem != nil; elem = elem.NextSibling() { if item := p.acceptBlock(elem); item != nil { result = append(result, item) } } return result } func (p *mdP) acceptTextBlock(node *gmAst.TextBlock) ast.ItemNode { if is := p.acceptInlineChildren(node); len(is) > 0 { return &ast.ParaNode{Inlines: is} } return nil } func (p *mdP) acceptHTMLBlock(node *gmAst.HTMLBlock) *ast.VerbatimNode { content := p.acceptRawText(node) if node.HasClosure() { closure := node.ClosureLine.Value(p.source) if l := len(closure); l > 1 && closure[l-1] == '\n' { closure = closure[:l-1] } if len(content) > 1 { content = append(content, '\n') } content = append(content, closure...) } return &ast.VerbatimNode{ Kind: ast.VerbatimHTML, Content: content, } } func (p *mdP) acceptInlineChildren(node gmAst.Node) ast.InlineSlice { result := make(ast.InlineSlice, 0, node.ChildCount()) for child := node.FirstChild(); child != nil; child = child.NextSibling() { if inlines := p.acceptInline(child); inlines != nil { result = append(result, inlines...) } } return result } func (p *mdP) acceptInline(node gmAst.Node) ast.InlineSlice { if node.Type() != gmAst.TypeInline { panic(fmt.Sprintf("Expected inline node, but got %v", node.Type())) } switch n := node.(type) { case *gmAst.Text: return p.acceptText(n) case *gmAst.CodeSpan: return p.acceptCodeSpan(n) case *gmAst.Emphasis: return p.acceptEmphasis(n) case *gmAst.Link: return p.acceptLink(n) case *gmAst.Image: return p.acceptImage(n) case *gmAst.AutoLink: return p.acceptAutoLink(n) case *gmAst.RawHTML: return p.acceptRawHTML(n) } panic(fmt.Sprintf("Unhandled inline node %v", node.Kind())) } func (p *mdP) acceptText(node *gmAst.Text) ast.InlineSlice { segment := node.Segment if node.IsRaw() { return splitText(string(segment.Value(p.source))) } ins := splitText(string(segment.Value(p.source))) result := make(ast.InlineSlice, 0, len(ins)+1) for _, in := range ins { if tn, ok := in.(*ast.TextNode); ok { tn.Text = cleanText([]byte(tn.Text), true) } result = append(result, in) } if node.HardLineBreak() { result = append(result, &ast.BreakNode{Hard: true}) } else if node.SoftLineBreak() { result = append(result, &ast.BreakNode{Hard: false}) } return result } // splitText transform the text into a sequence of TextNode and SpaceNode func splitText(text string) ast.InlineSlice { if text == "" { return nil } result := make(ast.InlineSlice, 0, 1) state := 0 // 0=unknown,1=non-spaces,2=spaces lastPos := 0 for pos, ch := range text { if input.IsSpace(ch) { if state == 1 { result = append(result, &ast.TextNode{Text: text[lastPos:pos]}) lastPos = pos } state = 2 } else { if state == 2 { result = append(result, &ast.SpaceNode{Lexeme: text[lastPos:pos]}) lastPos = pos } state = 1 } } switch state { case 1: result = append(result, &ast.TextNode{Text: text[lastPos:]}) case 2: result = append(result, &ast.SpaceNode{Lexeme: text[lastPos:]}) default: panic(fmt.Sprintf("Unexpected state %v", state)) } return result } var ignoreAfterBS = map[byte]struct{}{ '!': {}, '"': {}, '#': {}, '$': {}, '%': {}, '&': {}, '\'': {}, '(': {}, ')': {}, '*': {}, '+': {}, ',': {}, '-': {}, '.': {}, '/': {}, ':': {}, ';': {}, '<': {}, '=': {}, '>': {}, '?': {}, '@': {}, '[': {}, '\\': {}, ']': {}, '^': {}, '_': {}, '`': {}, '{': {}, '|': {}, '}': {}, '~': {}, } // cleanText removes backslashes from TextNodes and expands entities func cleanText(text []byte, cleanBS bool) string { lastPos := 0 var sb strings.Builder for pos, ch := range text { if pos < lastPos { continue } if ch == '&' { inp := input.NewInput([]byte(text[pos:])) if s, ok := inp.ScanEntity(); ok { sb.Write(text[lastPos:pos]) sb.WriteString(s) lastPos = pos + inp.Pos } continue } if cleanBS && ch == '\\' && pos < len(text)-1 { if _, found := ignoreAfterBS[text[pos+1]]; found { sb.Write(text[lastPos:pos]) sb.WriteByte(text[pos+1]) lastPos = pos + 2 } } } if lastPos < len(text) { sb.Write(text[lastPos:]) } return sb.String() } func (p *mdP) acceptCodeSpan(node *gmAst.CodeSpan) ast.InlineSlice { return ast.InlineSlice{ &ast.LiteralNode{ Kind: ast.LiteralProg, Attrs: nil, //TODO Content: cleanCodeSpan(node.Text(p.source)), }, } } func cleanCodeSpan(text []byte) []byte { if len(text) == 0 { return nil } lastPos := 0 var buf bytes.Buffer for pos, ch := range text { if ch == '\n' { buf.Write(text[lastPos:pos]) if pos < len(text)-1 { buf.WriteByte(' ') } lastPos = pos + 1 } } buf.Write(text[lastPos:]) return buf.Bytes() } func (p *mdP) acceptEmphasis(node *gmAst.Emphasis) ast.InlineSlice { kind := ast.FormatEmph if node.Level == 2 { kind = ast.FormatStrong } return ast.InlineSlice{ &ast.FormatNode{ Kind: kind, Attrs: nil, //TODO Inlines: p.acceptInlineChildren(node), }, } } func (p *mdP) acceptLink(node *gmAst.Link) ast.InlineSlice { ref := ast.ParseReference(cleanText(node.Destination, true)) var a attrs.Attributes if title := node.Title; len(title) > 0 { a = a.Set("title", cleanText(title, true)) } return ast.InlineSlice{ &ast.LinkNode{ Ref: ref, Inlines: p.acceptInlineChildren(node), Attrs: a, }, } } func (p *mdP) acceptImage(node *gmAst.Image) ast.InlineSlice { ref := ast.ParseReference(cleanText(node.Destination, true)) var a attrs.Attributes if title := node.Title; len(title) > 0 { a = a.Set("title", cleanText(title, true)) } return ast.InlineSlice{ &ast.EmbedRefNode{ Ref: ref, Inlines: p.flattenInlineSlice(node), Attrs: a, }, } } func (p *mdP) flattenInlineSlice(node gmAst.Node) ast.InlineSlice { is := p.acceptInlineChildren(node) var sb strings.Builder _, err := p.textEnc.WriteInlines(&sb, &is) if err != nil { panic(err) } if sb.Len() == 0 { return nil } return ast.InlineSlice{&ast.TextNode{Text: sb.String()}} } func (p *mdP) acceptAutoLink(node *gmAst.AutoLink) ast.InlineSlice { u := node.URL(p.source) if node.AutoLinkType == gmAst.AutoLinkEmail && !bytes.HasPrefix(bytes.ToLower(u), []byte("mailto:")) { u = append([]byte("mailto:"), u...) } return ast.InlineSlice{ &ast.LinkNode{ Ref: ast.ParseReference(cleanText(u, false)), Inlines: nil, Attrs: nil, // TODO }, } } func (p *mdP) acceptRawHTML(node *gmAst.RawHTML) ast.InlineSlice { segs := make([][]byte, 0, node.Segments.Len()) for i := 0; i < node.Segments.Len(); i++ { segment := node.Segments.At(i) segs = append(segs, segment.Value(p.source)) } return ast.InlineSlice{ &ast.LiteralNode{ Kind: ast.LiteralHTML, Attrs: nil, // TODO: add HTML as language Content: bytes.Join(segs, nil), }, } } |
Added parser/markdown/markdown_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 | //----------------------------------------------------------------------------- // Copyright (c) 2020-present Detlef Stern // // This file is part of Zettelstore. // // Zettelstore is licensed under the latest version of the EUPL (European Union // Public License). Please see file LICENSE.txt for your rights and obligations // under this license. //----------------------------------------------------------------------------- package markdown import ( "strings" "testing" "zettelstore.de/z/ast" ) func TestSplitText(t *testing.T) { t.Parallel() var testcases = []struct { text string exp string }{ {"", ""}, {"abc", "Tabc"}, {" ", "S "}, {"abc def", "TabcS Tdef"}, {"abc def ", "TabcS TdefS "}, {" abc def ", "S TabcS TdefS "}, } for i, tc := range testcases { var sb strings.Builder for _, in := range splitText(tc.text) { switch n := in.(type) { case *ast.TextNode: sb.WriteByte('T') sb.WriteString(n.Text) case *ast.SpaceNode: sb.WriteByte('S') sb.WriteString(n.Lexeme) default: sb.WriteByte('Q') } } got := sb.String() if tc.exp != got { t.Errorf("TC=%d, text=%q, exp=%q, got=%q", i, tc.text, tc.exp, got) } } } |
Added parser/none/none.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-present Detlef Stern // // This file is part of Zettelstore. // // Zettelstore is licensed under the latest version of the EUPL (European Union // Public License). Please see file LICENSE.txt for your rights and obligations // under this license. //----------------------------------------------------------------------------- // Package none provides a none-parser, e.g. for zettel with just metadata. package none import ( "zettelstore.de/z/ast" "zettelstore.de/z/domain/meta" "zettelstore.de/z/input" "zettelstore.de/z/parser" ) func init() { parser.Register(&parser.Info{ Name: meta.SyntaxNone, AltNames: []string{}, IsASTParser: false, IsTextFormat: false, IsImageFormat: false, ParseBlocks: parseBlocks, ParseInlines: parseInlines, }) } func parseBlocks(*input.Input, *meta.Meta, string) ast.BlockSlice { return nil } func parseInlines(inp *input.Input, _ string) ast.InlineSlice { inp.SkipToEOL() return nil } |
Added parser/parser.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 | //----------------------------------------------------------------------------- // Copyright (c) 2020-present Detlef Stern // // This file is part of Zettelstore. // // Zettelstore is licensed under the latest version of the EUPL (European Union // Public License). Please see file LICENSE.txt for your rights and obligations // under this license. //----------------------------------------------------------------------------- // Package parser provides a generic interface to a range of different parsers. package parser import ( "context" "fmt" "strings" "zettelstore.de/c/api" "zettelstore.de/z/ast" "zettelstore.de/z/config" "zettelstore.de/z/domain" "zettelstore.de/z/domain/meta" "zettelstore.de/z/input" "zettelstore.de/z/parser/cleaner" ) // Info describes a single parser. // // Before ParseBlocks() or ParseInlines() is called, ensure the input stream to // be valid. This can ce achieved on calling inp.Next() after the input stream // was created. type Info struct { Name string AltNames []string IsASTParser bool IsTextFormat bool IsImageFormat bool ParseBlocks func(*input.Input, *meta.Meta, string) ast.BlockSlice ParseInlines func(*input.Input, string) ast.InlineSlice } var registry = map[string]*Info{} // Register the parser (info) for later retrieval. func Register(pi *Info) { if _, ok := registry[pi.Name]; ok { panic(fmt.Sprintf("Parser %q already registered", pi.Name)) } registry[pi.Name] = pi for _, alt := range pi.AltNames { if _, ok := registry[alt]; ok { panic(fmt.Sprintf("Parser %q already registered", alt)) } registry[alt] = pi } } // GetSyntaxes returns a list of syntaxes implemented by all registered parsers. func GetSyntaxes() []string { result := make([]string, 0, len(registry)) for syntax := range registry { result = append(result, syntax) } return result } // Get the parser (info) by name. If name not found, use a default parser. func Get(name string) *Info { if pi := registry[name]; pi != nil { return pi } if pi := registry["plain"]; pi != nil { return pi } panic(fmt.Sprintf("No parser for %q found", name)) } // IsASTParser returns whether the given syntax parses text into an AST or not. func IsASTParser(syntax string) bool { pi, ok := registry[syntax] if !ok { return false } return pi.IsASTParser } // IsTextFormat returns whether the given syntax is known to be a text format. func IsTextFormat(syntax string) bool { pi, ok := registry[syntax] if !ok { return false } return pi.IsTextFormat } // IsImageFormat returns whether the given syntax is known to be an image format. func IsImageFormat(syntax string) bool { pi, ok := registry[syntax] if !ok { return false } return pi.IsImageFormat } // ParseBlocks parses some input and returns a slice of block nodes. func ParseBlocks(inp *input.Input, m *meta.Meta, syntax string, hi config.HTMLInsecurity) ast.BlockSlice { return parseBlocksAndClean(inp, m, syntax, hi) } func parseBlocksAndClean(inp *input.Input, m *meta.Meta, syntax string, hi config.HTMLInsecurity) ast.BlockSlice { bs := Get(syntax).ParseBlocks(inp, m, syntax) cleaner.CleanBlockSlice(&bs, hi.AllowHTML(syntax)) return bs } // ParseInlines parses some input and returns a slice of inline nodes. func ParseInlines(inp *input.Input, syntax string) ast.InlineSlice { // Do not clean, because we don't know the context where this function will be called. return Get(syntax).ParseInlines(inp, syntax) } // ParseMetadata parses a string as Zettelmarkup, resulting in an inline slice. // Typically used to parse the title or other metadata of type Zettelmarkup. func ParseMetadata(value string) ast.InlineSlice { return ParseInlines(input.NewInput([]byte(value)), meta.SyntaxZmk) } // ParseSpacedText returns an inline slice that consists just of test and space node. // No Zettelmarkup parsing is done. It is typically used to transform the zettel title into an inline slice. func ParseSpacedText(s string) ast.InlineSlice { return ast.CreateInlineSliceFromWords(meta.ListFromValue(s)...) } // NormalizedSpacedText returns the given string, but normalize multiple spaces to one space. func NormalizedSpacedText(s string) string { return strings.Join(meta.ListFromValue(s), " ") } // ParseDescription returns a suitable description stored in the metadata as an inline slice. func ParseDescription(m *meta.Meta) ast.InlineSlice { if m == nil { return nil } if descr, found := m.Get(api.KeySummary); found { in := ParseMetadata(descr) cleaner.CleanInlineLinks(&in) return in } if title, found := m.Get(api.KeyTitle); found { return ParseSpacedText(title) } return nil } // ParseZettel parses the zettel based on the syntax. func ParseZettel(ctx context.Context, zettel domain.Zettel, syntax string, rtConfig config.Config) *ast.ZettelNode { m := zettel.Meta inhMeta := m if rtConfig != nil { inhMeta = rtConfig.AddDefaultValues(ctx, inhMeta) } if syntax == "" { syntax, _ = inhMeta.Get(api.KeySyntax) } parseMeta := inhMeta if syntax == meta.SyntaxNone { parseMeta = m } hi := config.NoHTML if rtConfig != nil { hi = rtConfig.GetHTMLInsecurity() } return &ast.ZettelNode{ Meta: m, Content: zettel.Content, Zid: m.Zid, InhMeta: inhMeta, Ast: parseBlocksAndClean(input.NewInput(zettel.Content.AsBytes()), parseMeta, syntax, hi), Syntax: syntax, } } |
Added parser/parser_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 | //----------------------------------------------------------------------------- // Copyright (c) 2021-present Detlef Stern // // This file is part of Zettelstore. // // Zettelstore is licensed under the latest version of the EUPL (European Union // Public License). Please see file LICENSE.txt for your rights and obligations // under this license. //----------------------------------------------------------------------------- package parser_test import ( "testing" "zettelstore.de/z/domain/meta" "zettelstore.de/z/parser" "zettelstore.de/z/strfun" _ "zettelstore.de/z/parser/blob" // Allow to use BLOB parser. _ "zettelstore.de/z/parser/draw" // Allow to use draw parser. _ "zettelstore.de/z/parser/markdown" // Allow to use markdown parser. _ "zettelstore.de/z/parser/none" // Allow to use none parser. _ "zettelstore.de/z/parser/plain" // Allow to use plain parser. _ "zettelstore.de/z/parser/zettelmark" // Allow to use zettelmark parser. ) func TestParserType(t *testing.T) { syntaxSet := strfun.NewSet(parser.GetSyntaxes()...) testCases := []struct { syntax string ast bool text bool image bool }{ {meta.SyntaxHTML, false, true, false}, {meta.SyntaxCSS, false, true, false}, {meta.SyntaxDraw, true, true, false}, {meta.SyntaxGif, false, false, true}, {meta.SyntaxJPEG, false, false, true}, {meta.SyntaxJPG, false, false, true}, {meta.SyntaxMarkdown, true, true, false}, {meta.SyntaxMD, true, true, false}, {meta.SyntaxMustache, false, true, false}, {meta.SyntaxNone, false, false, false}, {meta.SyntaxPlain, false, true, false}, {meta.SyntaxPNG, false, false, true}, {meta.SyntaxSVG, false, true, true}, {meta.SyntaxText, false, true, false}, {meta.SyntaxTxt, false, true, false}, {meta.SyntaxWebp, false, false, true}, {meta.SyntaxZmk, true, true, false}, } for _, tc := range testCases { delete(syntaxSet, tc.syntax) if got := parser.IsASTParser(tc.syntax); got != tc.ast { t.Errorf("Syntax %q is AST: %v, but got %v", tc.syntax, tc.ast, got) } if got := parser.IsTextFormat(tc.syntax); got != tc.text { t.Errorf("Syntax %q is text: %v, but got %v", tc.syntax, tc.text, got) } if got := parser.IsImageFormat(tc.syntax); got != tc.image { t.Errorf("Syntax %q is image: %v, but got %v", tc.syntax, tc.image, got) } } for syntax := range syntaxSet { t.Errorf("Forgot to test syntax %q", syntax) } } |
Added parser/plain/plain.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 | //----------------------------------------------------------------------------- // Copyright (c) 2020-present Detlef Stern // // This file is part of Zettelstore. // // Zettelstore is licensed under the latest version of the EUPL (European Union // Public License). Please see file LICENSE.txt for your rights and obligations // under this license. //----------------------------------------------------------------------------- // Package plain provides a parser for plain text data. package plain import ( "strings" "zettelstore.de/c/attrs" "zettelstore.de/z/ast" "zettelstore.de/z/domain/meta" "zettelstore.de/z/input" "zettelstore.de/z/parser" ) func init() { parser.Register(&parser.Info{ Name: meta.SyntaxTxt, AltNames: []string{meta.SyntaxPlain, meta.SyntaxText}, IsASTParser: false, IsTextFormat: true, IsImageFormat: false, ParseBlocks: parseBlocks, ParseInlines: parseInlines, }) parser.Register(&parser.Info{ Name: meta.SyntaxHTML, AltNames: []string{}, IsASTParser: false, IsTextFormat: true, IsImageFormat: false, ParseBlocks: parseBlocksHTML, ParseInlines: parseInlinesHTML, }) parser.Register(&parser.Info{ Name: meta.SyntaxCSS, AltNames: []string{}, IsASTParser: false, IsTextFormat: true, IsImageFormat: false, ParseBlocks: parseBlocks, ParseInlines: parseInlines, }) parser.Register(&parser.Info{ Name: meta.SyntaxSVG, AltNames: []string{}, IsASTParser: false, IsTextFormat: true, IsImageFormat: true, ParseBlocks: parseSVGBlocks, ParseInlines: parseSVGInlines, }) parser.Register(&parser.Info{ Name: meta.SyntaxMustache, AltNames: []string{}, IsASTParser: false, IsTextFormat: true, IsImageFormat: false, ParseBlocks: parseBlocks, ParseInlines: parseInlines, }) } func parseBlocks(inp *input.Input, _ *meta.Meta, syntax string) ast.BlockSlice { return doParseBlocks(inp, syntax, ast.VerbatimProg) } func parseBlocksHTML(inp *input.Input, _ *meta.Meta, syntax string) ast.BlockSlice { return doParseBlocks(inp, syntax, ast.VerbatimHTML) } func doParseBlocks(inp *input.Input, syntax string, kind ast.VerbatimKind) ast.BlockSlice { return ast.BlockSlice{ &ast.VerbatimNode{ Kind: kind, Attrs: attrs.Attributes{"": syntax}, Content: inp.ScanLineContent(), }, } } func parseInlines(inp *input.Input, syntax string) ast.InlineSlice { return doParseInlines(inp, syntax, ast.LiteralProg) } func parseInlinesHTML(inp *input.Input, syntax string) ast.InlineSlice { return doParseInlines(inp, syntax, ast.LiteralHTML) } func doParseInlines(inp *input.Input, syntax string, kind ast.LiteralKind) ast.InlineSlice { inp.SkipToEOL() return ast.InlineSlice{&ast.LiteralNode{ Kind: kind, Attrs: attrs.Attributes{"": syntax}, Content: append([]byte(nil), inp.Src[0:inp.Pos]...), }} } func parseSVGBlocks(inp *input.Input, _ *meta.Meta, syntax string) ast.BlockSlice { is := parseSVGInlines(inp, syntax) if len(is) == 0 { return nil } return ast.BlockSlice{ast.CreateParaNode(is...)} } func parseSVGInlines(inp *input.Input, syntax string) ast.InlineSlice { svgSrc := scanSVG(inp) if svgSrc == "" { return nil } return ast.InlineSlice{&ast.EmbedBLOBNode{ Blob: []byte(svgSrc), Syntax: syntax, }} } func scanSVG(inp *input.Input) string { for input.IsSpace(inp.Ch) { inp.Next() } svgSrc := string(inp.Src[inp.Pos:]) if !strings.HasPrefix(svgSrc, "<svg ") { return "" } // TODO: check proper end </svg> return svgSrc } |
Added parser/zettelmark/block.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 581 582 583 584 585 586 587 588 589 590 591 592 593 594 595 596 597 598 599 600 601 602 603 604 605 606 607 608 609 610 611 612 613 614 615 616 617 618 619 620 621 622 623 624 625 626 627 628 629 630 631 632 633 634 635 636 637 638 639 640 641 642 643 644 645 646 647 648 649 650 651 652 653 654 655 656 657 658 659 660 661 662 663 664 665 666 667 668 669 670 671 672 673 674 675 676 677 678 | //----------------------------------------------------------------------------- // Copyright (c) 2020-present Detlef Stern // // This file is part of Zettelstore. // // Zettelstore is licensed under the latest version of the EUPL (European Union // Public License). Please see file LICENSE.txt for your rights and obligations // under this license. //----------------------------------------------------------------------------- package zettelmark import ( "fmt" "zettelstore.de/z/ast" "zettelstore.de/z/input" ) // parseBlockSlice parses a sequence of blocks. func (cp *zmkP) parseBlockSlice() ast.BlockSlice { inp := cp.inp var lastPara *ast.ParaNode bs := ast.BlockSlice{} for inp.Ch != input.EOS { bn, cont := cp.parseBlock(lastPara) if bn != nil { bs = append(bs, bn) } if !cont { lastPara, _ = bn.(*ast.ParaNode) } } if cp.nestingLevel != 0 { panic("Nesting level was not decremented") } return bs } // parseBlock parses one block. func (cp *zmkP) parseBlock(lastPara *ast.ParaNode) (res ast.BlockNode, cont bool) { inp := cp.inp pos := inp.Pos if cp.nestingLevel <= maxNestingLevel { cp.nestingLevel++ defer func() { cp.nestingLevel-- }() var bn ast.BlockNode success := false switch inp.Ch { case input.EOS: return nil, false case '\n', '\r': inp.EatEOL() cp.cleanupListsAfterEOL() return nil, false case ':': bn, success = cp.parseColon() case '@', '`', runeModGrave, '%', '~', '$': cp.clearStacked() bn, success = cp.parseVerbatim() case '"', '<': cp.clearStacked() bn, success = cp.parseRegion() case '=': cp.clearStacked() bn, success = cp.parseHeading() case '-': cp.clearStacked() bn, success = cp.parseHRule() case '*', '#', '>': cp.table = nil cp.descrl = nil bn, success = cp.parseNestedList() case ';': cp.lists = nil cp.table = nil bn, success = cp.parseDefTerm() case ' ': cp.table = nil bn, success = nil, cp.parseIndent() case '|': cp.lists = nil cp.descrl = nil bn, success = cp.parseRow(), true case '{': cp.clearStacked() bn, success = cp.parseTransclusion() } if success { return bn, false } } inp.SetPos(pos) cp.clearStacked() pn := cp.parsePara() if startsWithSpaceSoftBreak(pn) { pn.Inlines = pn.Inlines[2:] } else if lastPara != nil { lastPara.Inlines = append(lastPara.Inlines, pn.Inlines...) return nil, true } return pn, false } func startsWithSpaceSoftBreak(pn *ast.ParaNode) bool { ins := pn.Inlines if len(ins) < 2 { return false } _, isSpace := ins[0].(*ast.SpaceNode) _, isBreak := ins[1].(*ast.BreakNode) return isSpace && isBreak } func (cp *zmkP) cleanupListsAfterEOL() { for _, l := range cp.lists { if lits := len(l.Items); lits > 0 { l.Items[lits-1] = append(l.Items[lits-1], &nullItemNode{}) } } if cp.descrl != nil { defPos := len(cp.descrl.Descriptions) - 1 if ldds := len(cp.descrl.Descriptions[defPos].Descriptions); ldds > 0 { cp.descrl.Descriptions[defPos].Descriptions[ldds-1] = append( cp.descrl.Descriptions[defPos].Descriptions[ldds-1], &nullDescriptionNode{}) } } } // parseColon determines which element should be parsed. func (cp *zmkP) parseColon() (ast.BlockNode, bool) { inp := cp.inp if inp.PeekN(1) == ':' { cp.clearStacked() return cp.parseRegion() } return cp.parseDefDescr() } // parsePara parses paragraphed inline material. func (cp *zmkP) parsePara() *ast.ParaNode { ins := ast.InlineSlice{} for { in := cp.parseInline() if in == nil { return &ast.ParaNode{Inlines: ins} } ins = append(ins, in) if _, ok := in.(*ast.BreakNode); ok { ch := cp.inp.Ch switch ch { // Must contain all cases from above switch in parseBlock. case input.EOS, '\n', '\r', '@', '`', runeModGrave, '%', '~', '$', '"', '<', '=', '-', '*', '#', '>', ';', ':', ' ', '|', '{': return &ast.ParaNode{Inlines: ins} } } } } // countDelim read from input until a non-delimiter is found and returns number of delimiter chars. func (cp *zmkP) countDelim(delim rune) int { cnt := 0 for cp.inp.Ch == delim { cnt++ cp.inp.Next() } return cnt } // parseVerbatim parses a verbatim block. func (cp *zmkP) parseVerbatim() (rn *ast.VerbatimNode, success bool) { inp := cp.inp fch := inp.Ch cnt := cp.countDelim(fch) if cnt < 3 { return nil, false } attrs := cp.parseBlockAttributes() inp.SkipToEOL() if inp.Ch == input.EOS { return nil, false } var kind ast.VerbatimKind switch fch { case '@': kind = ast.VerbatimZettel case '`', runeModGrave: kind = ast.VerbatimProg case '%': kind = ast.VerbatimComment case '~': kind = ast.VerbatimEval case '$': kind = ast.VerbatimMath default: panic(fmt.Sprintf("%q is not a verbatim char", fch)) } rn = &ast.VerbatimNode{Kind: kind, Attrs: attrs, Content: make([]byte, 0, 512)} for { inp.EatEOL() posL := inp.Pos switch inp.Ch { case fch: if cp.countDelim(fch) >= cnt { inp.SkipToEOL() return rn, true } inp.SetPos(posL) case input.EOS: return nil, false } inp.SkipToEOL() if len(rn.Content) > 0 { rn.Content = append(rn.Content, '\n') } rn.Content = append(rn.Content, inp.Src[posL:inp.Pos]...) } } var runeRegion = map[rune]ast.RegionKind{ ':': ast.RegionSpan, '<': ast.RegionQuote, '"': ast.RegionVerse, } // parseRegion parses a block region. func (cp *zmkP) parseRegion() (rn *ast.RegionNode, success bool) { inp := cp.inp fch := inp.Ch kind, ok := runeRegion[fch] if !ok { panic(fmt.Sprintf("%q is not a region char", fch)) } cnt := cp.countDelim(fch) if cnt < 3 { return nil, false } attrs := cp.parseBlockAttributes() inp.SkipToEOL() if inp.Ch == input.EOS { return nil, false } rn = &ast.RegionNode{ Kind: kind, Attrs: attrs, Blocks: nil, Inlines: nil, } var lastPara *ast.ParaNode inp.EatEOL() for { posL := inp.Pos switch inp.Ch { case fch: if cp.countDelim(fch) >= cnt { cp.parseRegionLastLine(rn) return rn, true } inp.SetPos(posL) case input.EOS: return nil, false } bn, cont := cp.parseBlock(lastPara) if bn != nil { rn.Blocks = append(rn.Blocks, bn) } if !cont { lastPara, _ = bn.(*ast.ParaNode) } } } func (cp *zmkP) parseRegionLastLine(rn *ast.RegionNode) { cp.clearStacked() // remove any lists defined in the region cp.skipSpace() for { switch cp.inp.Ch { case input.EOS, '\n', '\r': return } in := cp.parseInline() if in == nil { return } rn.Inlines = append(rn.Inlines, in) } } // parseHeading parses a head line. func (cp *zmkP) parseHeading() (hn *ast.HeadingNode, success bool) { inp := cp.inp delims := cp.countDelim(inp.Ch) if delims < 3 { return nil, false } if inp.Ch != ' ' { return nil, false } inp.Next() cp.skipSpace() if delims > 7 { delims = 7 } hn = &ast.HeadingNode{Level: delims - 2, Inlines: nil} for { if input.IsEOLEOS(inp.Ch) { return hn, true } in := cp.parseInline() if in == nil { return hn, true } hn.Inlines = append(hn.Inlines, in) if inp.Ch == '{' && inp.Peek() != '{' { attrs := cp.parseBlockAttributes() hn.Attrs = attrs inp.SkipToEOL() return hn, true } } } // parseHRule parses a horizontal rule. func (cp *zmkP) parseHRule() (hn *ast.HRuleNode, success bool) { inp := cp.inp if cp.countDelim(inp.Ch) < 3 { return nil, false } attrs := cp.parseBlockAttributes() inp.SkipToEOL() return &ast.HRuleNode{Attrs: attrs}, true } var mapRuneNestedList = map[rune]ast.NestedListKind{ '*': ast.NestedListUnordered, '#': ast.NestedListOrdered, '>': ast.NestedListQuote, } // parseNestedList parses a list. func (cp *zmkP) parseNestedList() (res ast.BlockNode, success bool) { inp := cp.inp kinds := cp.parseNestedListKinds() if kinds == nil { return nil, false } cp.skipSpace() if kinds[len(kinds)-1] != ast.NestedListQuote && input.IsEOLEOS(inp.Ch) { return nil, false } if len(kinds) < len(cp.lists) { cp.lists = cp.lists[:len(kinds)] } ln, newLnCount := cp.buildNestedList(kinds) pn := cp.parseLinePara() if pn == nil { pn = &ast.ParaNode{} } ln.Items = append(ln.Items, ast.ItemSlice{pn}) return cp.cleanupParsedNestedList(newLnCount) } func (cp *zmkP) parseNestedListKinds() []ast.NestedListKind { inp := cp.inp codes := make([]ast.NestedListKind, 0, 4) for { code, ok := mapRuneNestedList[inp.Ch] if !ok { panic(fmt.Sprintf("%q is not a region char", inp.Ch)) } codes = append(codes, code) inp.Next() switch inp.Ch { case '*', '#', '>': case ' ', input.EOS, '\n', '\r': return codes default: return nil } } } func (cp *zmkP) buildNestedList(kinds []ast.NestedListKind) (ln *ast.NestedListNode, newLnCount int) { for i, kind := range kinds { if i < len(cp.lists) { if cp.lists[i].Kind != kind { ln = &ast.NestedListNode{Kind: kind} newLnCount++ cp.lists[i] = ln cp.lists = cp.lists[:i+1] } else { ln = cp.lists[i] } } else { ln = &ast.NestedListNode{Kind: kind} newLnCount++ cp.lists = append(cp.lists, ln) } } return ln, newLnCount } func (cp *zmkP) cleanupParsedNestedList(newLnCount int) (res ast.BlockNode, success bool) { listDepth := len(cp.lists) for i := 0; i < newLnCount; i++ { childPos := listDepth - i - 1 parentPos := childPos - 1 if parentPos < 0 { return cp.lists[0], true } if prevItems := cp.lists[parentPos].Items; len(prevItems) > 0 { lastItem := len(prevItems) - 1 prevItems[lastItem] = append(prevItems[lastItem], cp.lists[childPos]) } else { cp.lists[parentPos].Items = []ast.ItemSlice{{cp.lists[childPos]}} } } return nil, true } // parseDefTerm parses a term of a definition list. func (cp *zmkP) parseDefTerm() (res ast.BlockNode, success bool) { inp := cp.inp inp.Next() if inp.Ch != ' ' { return nil, false } inp.Next() cp.skipSpace() descrl := cp.descrl if descrl == nil { descrl = &ast.DescriptionListNode{} cp.descrl = descrl } descrl.Descriptions = append(descrl.Descriptions, ast.Description{}) defPos := len(descrl.Descriptions) - 1 if defPos == 0 { res = descrl } for { in := cp.parseInline() if in == nil { if len(descrl.Descriptions[defPos].Term) == 0 { return nil, false } return res, true } descrl.Descriptions[defPos].Term = append(descrl.Descriptions[defPos].Term, in) if _, ok := in.(*ast.BreakNode); ok { return res, true } } } // parseDefDescr parses a description of a definition list. func (cp *zmkP) parseDefDescr() (res ast.BlockNode, success bool) { inp := cp.inp inp.Next() if inp.Ch != ' ' { return nil, false } inp.Next() cp.skipSpace() descrl := cp.descrl if descrl == nil || len(descrl.Descriptions) == 0 { return nil, false } defPos := len(descrl.Descriptions) - 1 if len(descrl.Descriptions[defPos].Term) == 0 { return nil, false } pn := cp.parseLinePara() if pn == nil { return nil, false } cp.lists = nil cp.table = nil descrl.Descriptions[defPos].Descriptions = append(descrl.Descriptions[defPos].Descriptions, ast.DescriptionSlice{pn}) return nil, true } // parseIndent parses initial spaces to continue a list. func (cp *zmkP) parseIndent() bool { inp := cp.inp cnt := 0 for { inp.Next() if inp.Ch != ' ' { break } cnt++ } if cp.lists != nil { return cp.parseIndentForList(cnt) } if cp.descrl != nil { return cp.parseIndentForDescription(cnt) } return false } func (cp *zmkP) parseIndentForList(cnt int) bool { if len(cp.lists) < cnt { cnt = len(cp.lists) } cp.lists = cp.lists[:cnt] if cnt == 0 { return false } ln := cp.lists[cnt-1] pn := cp.parseLinePara() if pn == nil { pn = &ast.ParaNode{} } lbn := ln.Items[len(ln.Items)-1] if lpn, ok := lbn[len(lbn)-1].(*ast.ParaNode); ok { lpn.Inlines = append(lpn.Inlines, pn.Inlines...) } else { ln.Items[len(ln.Items)-1] = append(ln.Items[len(ln.Items)-1], pn) } return true } func (cp *zmkP) parseIndentForDescription(cnt int) bool { defPos := len(cp.descrl.Descriptions) - 1 if cnt < 1 || defPos < 0 { return false } if len(cp.descrl.Descriptions[defPos].Descriptions) == 0 { // Continuation of a definition term for { in := cp.parseInline() if in == nil { return true } cp.descrl.Descriptions[defPos].Term = append(cp.descrl.Descriptions[defPos].Term, in) if _, ok := in.(*ast.BreakNode); ok { return true } } } // Continuation of a definition description pn := cp.parseLinePara() if pn == nil { return false } descrPos := len(cp.descrl.Descriptions[defPos].Descriptions) - 1 lbn := cp.descrl.Descriptions[defPos].Descriptions[descrPos] if lpn, ok := lbn[len(lbn)-1].(*ast.ParaNode); ok { lpn.Inlines = append(lpn.Inlines, pn.Inlines...) } else { descrPos = len(cp.descrl.Descriptions[defPos].Descriptions) - 1 cp.descrl.Descriptions[defPos].Descriptions[descrPos] = append(cp.descrl.Descriptions[defPos].Descriptions[descrPos], pn) } return true } // parseLinePara parses one line of inline material. func (cp *zmkP) parseLinePara() *ast.ParaNode { ins := ast.InlineSlice{} for { in := cp.parseInline() if in == nil { if len(ins) == 0 { return nil } return &ast.ParaNode{Inlines: ins} } ins = append(ins, in) if _, ok := in.(*ast.BreakNode); ok { return &ast.ParaNode{Inlines: ins} } } } // parseRow parse one table row. func (cp *zmkP) parseRow() ast.BlockNode { inp := cp.inp if inp.Peek() == '%' { inp.SkipToEOL() return nil } row := ast.TableRow{} for { inp.Next() cell := cp.parseCell() if cell != nil { row = append(row, cell) } switch inp.Ch { case '\n', '\r': inp.EatEOL() fallthrough case input.EOS: // add to table if cp.table == nil { cp.table = &ast.TableNode{Rows: []ast.TableRow{row}} return cp.table } cp.table.Rows = append(cp.table.Rows, row) return nil } // inp.Ch must be '|' } } // parseCell parses one single cell of a table row. func (cp *zmkP) parseCell() *ast.TableCell { inp := cp.inp var l ast.InlineSlice for { if input.IsEOLEOS(inp.Ch) { if len(l) == 0 { return nil } return &ast.TableCell{Inlines: l} } if inp.Ch == '|' { return &ast.TableCell{Inlines: l} } l = append(l, cp.parseInline()) } } // parseTransclusion parses '{' '{' '{' ZID '}' '}' '}' func (cp *zmkP) parseTransclusion() (ast.BlockNode, bool) { if cp.countDelim('{') != 3 { return nil, false } inp := cp.inp posA, posE := inp.Pos, 0 loop: for { switch inp.Ch { case input.EOS: return nil, false case '\n', '\r', ' ', '\t': if !hasQueryPrefix(inp.Src[posA:]) { return nil, false } case '\\': inp.Next() switch inp.Ch { case input.EOS, '\n', '\r': return nil, false } case '}': posE = inp.Pos if posA >= posE { return nil, false } inp.Next() if inp.Ch != '}' { continue } inp.Next() if inp.Ch != '}' { continue } break loop } inp.Next() } inp.Next() // consume last '}' a := cp.parseBlockAttributes() inp.SkipToEOL() refText := string(inp.Src[posA:posE]) ref := ast.ParseReference(refText) return &ast.TranscludeNode{Attrs: a, Ref: ref}, true } |
Added parser/zettelmark/inline.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 | //----------------------------------------------------------------------------- // Copyright (c) 2020-present Detlef Stern // // This file is part of Zettelstore. // // Zettelstore is licensed under the latest version of the EUPL (European Union // Public License). Please see file LICENSE.txt for your rights and obligations // under this license. //----------------------------------------------------------------------------- package zettelmark import ( "bytes" "fmt" "strings" "zettelstore.de/c/attrs" "zettelstore.de/z/ast" "zettelstore.de/z/domain/meta" "zettelstore.de/z/input" ) // parseInlineSlice parses a sequence of Inlines until EOS. func (cp *zmkP) parseInlineSlice() (ins ast.InlineSlice) { inp := cp.inp for inp.Ch != input.EOS { in := cp.parseInline() if in == nil { break } ins = append(ins, in) } return ins } func (cp *zmkP) parseInline() ast.InlineNode { inp := cp.inp pos := inp.Pos if cp.nestingLevel <= maxNestingLevel { cp.nestingLevel++ defer func() { cp.nestingLevel-- }() var in ast.InlineNode success := false switch inp.Ch { case input.EOS: return nil case '\n', '\r': return cp.parseSoftBreak() case ' ', '\t': return cp.parseSpace() case '[': inp.Next() switch inp.Ch { case '[': in, success = cp.parseLink() case '@': in, success = cp.parseCite() case '^': in, success = cp.parseFootnote() case '!': in, success = cp.parseMark() } case '{': inp.Next() if inp.Ch == '{' { in, success = cp.parseEmbed() } case '%': in, success = cp.parseComment() case '_', '*', '>', '~', '^', ',', '"', ':': in, success = cp.parseFormat() case '@', '\'', '`', '=', runeModGrave: in, success = cp.parseLiteral() case '$': in, success = cp.parseLiteralMath() case '\\': return cp.parseBackslash() case '-': in, success = cp.parseNdash() case '&': in, success = cp.parseEntity() } if success { return in } } inp.SetPos(pos) return cp.parseText() } func (cp *zmkP) parseText() *ast.TextNode { inp := cp.inp pos := inp.Pos if inp.Ch == '\\' { cp.inp.Next() return cp.parseBackslashRest() } for { inp.Next() switch inp.Ch { // The following case must contain all runes that occur in parseInline! // Plus the closing brackets ] and } and ) and the middle | case input.EOS, '\n', '\r', ' ', '\t', '[', ']', '{', '}', '(', ')', '|', '%', '_', '*', '>', '~', '^', ',', '"', ':', '\'', '@', '`', runeModGrave, '$', '=', '\\', '-', '&': return &ast.TextNode{Text: string(inp.Src[pos:inp.Pos])} } } } func (cp *zmkP) parseBackslash() ast.InlineNode { inp := cp.inp inp.Next() switch inp.Ch { case '\n', '\r': inp.EatEOL() return &ast.BreakNode{Hard: true} default: return cp.parseBackslashRest() } } func (cp *zmkP) parseBackslashRest() *ast.TextNode { inp := cp.inp if input.IsEOLEOS(inp.Ch) { return &ast.TextNode{Text: "\\"} } if inp.Ch == ' ' { inp.Next() return &ast.TextNode{Text: "\u00a0"} } pos := inp.Pos inp.Next() return &ast.TextNode{Text: string(inp.Src[pos:inp.Pos])} } func (cp *zmkP) parseSpace() *ast.SpaceNode { inp := cp.inp pos := inp.Pos for { inp.Next() switch inp.Ch { case ' ', '\t': default: return &ast.SpaceNode{Lexeme: string(inp.Src[pos:inp.Pos])} } } } func (cp *zmkP) parseSoftBreak() *ast.BreakNode { cp.inp.EatEOL() return &ast.BreakNode{} } func (cp *zmkP) parseLink() (*ast.LinkNode, bool) { if ref, is, ok := cp.parseReference('[', ']'); ok { attrs := cp.parseInlineAttributes() if len(ref) > 0 { return &ast.LinkNode{ Ref: ast.ParseReference(ref), Inlines: is, Attrs: attrs, }, true } } return nil, false } func hasQueryPrefix(src []byte) bool { return len(src) > len(ast.QueryPrefix) && string(src[:len(ast.QueryPrefix)]) == ast.QueryPrefix } func (cp *zmkP) parseReference(openCh, closeCh rune) (ref string, is ast.InlineSlice, _ bool) { inp := cp.inp inp.Next() cp.skipSpace() if inp.Ch == openCh { // Additional opening chars result in a fail return "", nil, false } pos := inp.Pos if !hasQueryPrefix(inp.Src[pos:]) { hasSpace, ok := cp.readReferenceToSep(closeCh) if !ok { return "", nil, false } if inp.Ch == '|' { // First part must be inline text if pos == inp.Pos { // [[| or {{| return "", nil, false } cp.inp = input.NewInput(inp.Src[pos:inp.Pos]) for { in := cp.parseInline() if in == nil { break } is = append(is, in) } cp.inp = inp inp.Next() } else { if hasSpace { return "", nil, false } inp.SetPos(pos) } } cp.skipSpace() pos = inp.Pos if !cp.readReferenceToClose(closeCh) { return "", nil, false } ref = strings.TrimSpace(string(inp.Src[pos:inp.Pos])) inp.Next() if inp.Ch != closeCh { return "", nil, false } inp.Next() if len(is) == 0 { return ref, nil, true } return ref, is, true } func (cp *zmkP) readReferenceToSep(closeCh rune) (bool, bool) { hasSpace := false inp := cp.inp for { switch inp.Ch { case input.EOS: return false, false case '\n', '\r', ' ': hasSpace = true case '|': return hasSpace, true case '\\': inp.Next() switch inp.Ch { case input.EOS: return false, false case '\n', '\r': hasSpace = true } case '%': inp.Next() if inp.Ch == '%' { inp.SkipToEOL() } continue case closeCh: inp.Next() if inp.Ch == closeCh { return hasSpace, true } continue } inp.Next() } } func (cp *zmkP) readReferenceToClose(closeCh rune) bool { inp := cp.inp pos := inp.Pos for { switch inp.Ch { case input.EOS: return false case '\t', '\r', '\n', ' ': if !hasQueryPrefix(inp.Src[pos:]) { return false } case '\\': inp.Next() switch inp.Ch { case input.EOS, '\n', '\r': return false } case closeCh: return true } inp.Next() } } func (cp *zmkP) parseCite() (*ast.CiteNode, bool) { inp := cp.inp inp.Next() switch inp.Ch { case ' ', ',', '|', ']', '\n', '\r': return nil, false } pos := inp.Pos loop: for { switch inp.Ch { case input.EOS: return nil, false case ' ', ',', '|', ']', '\n', '\r': break loop } inp.Next() } posL := inp.Pos switch inp.Ch { case ' ', ',', '|': inp.Next() } ins, ok := cp.parseLinkLikeRest() if !ok { return nil, false } attrs := cp.parseInlineAttributes() return &ast.CiteNode{Key: string(inp.Src[pos:posL]), Inlines: ins, Attrs: attrs}, true } func (cp *zmkP) parseFootnote() (*ast.FootnoteNode, bool) { cp.inp.Next() ins, ok := cp.parseLinkLikeRest() if !ok { return nil, false } attrs := cp.parseInlineAttributes() return &ast.FootnoteNode{Inlines: ins, Attrs: attrs}, true } func (cp *zmkP) parseLinkLikeRest() (ast.InlineSlice, bool) { cp.skipSpace() ins := ast.InlineSlice{} inp := cp.inp for inp.Ch != ']' { in := cp.parseInline() if in == nil { return nil, false } ins = append(ins, in) if _, ok := in.(*ast.BreakNode); ok && input.IsEOLEOS(inp.Ch) { return nil, false } } inp.Next() if len(ins) == 0 { return nil, true } return ins, true } func (cp *zmkP) parseEmbed() (ast.InlineNode, bool) { if ref, ins, ok := cp.parseReference('{', '}'); ok { attrs := cp.parseInlineAttributes() if len(ref) > 0 { r := ast.ParseReference(ref) return &ast.EmbedRefNode{ Ref: r, Inlines: ins, Attrs: attrs, }, true } } return nil, false } func (cp *zmkP) parseMark() (*ast.MarkNode, bool) { inp := cp.inp inp.Next() pos := inp.Pos for inp.Ch != '|' && inp.Ch != ']' { if !isNameRune(inp.Ch) { return nil, false } inp.Next() } mark := inp.Src[pos:inp.Pos] ins := ast.InlineSlice{} if inp.Ch == '|' { inp.Next() var ok bool ins, ok = cp.parseLinkLikeRest() if !ok { return nil, false } } else { inp.Next() } mn := &ast.MarkNode{Mark: string(mark), Inlines: ins} return mn, true } func (cp *zmkP) parseComment() (res *ast.LiteralNode, success bool) { inp := cp.inp inp.Next() if inp.Ch != '%' { return nil, false } for inp.Ch == '%' { inp.Next() } attrs := cp.parseInlineAttributes() cp.skipSpace() pos := inp.Pos for { if input.IsEOLEOS(inp.Ch) { return &ast.LiteralNode{ Kind: ast.LiteralComment, Attrs: attrs, Content: append([]byte(nil), inp.Src[pos:inp.Pos]...), }, true } inp.Next() } } var mapRuneFormat = map[rune]ast.FormatKind{ '_': ast.FormatEmph, '*': ast.FormatStrong, '>': ast.FormatInsert, '~': ast.FormatDelete, '^': ast.FormatSuper, ',': ast.FormatSub, '"': ast.FormatQuote, ':': ast.FormatSpan, } func (cp *zmkP) parseFormat() (res ast.InlineNode, success bool) { inp := cp.inp fch := inp.Ch kind, ok := mapRuneFormat[fch] if !ok { panic(fmt.Sprintf("%q is not a formatting char", fch)) } inp.Next() // read 2nd formatting character if inp.Ch != fch { return nil, false } inp.Next() fn := &ast.FormatNode{Kind: kind, Inlines: nil} for { if inp.Ch == input.EOS { return nil, false } if inp.Ch == fch { inp.Next() if inp.Ch == fch { inp.Next() fn.Attrs = cp.parseInlineAttributes() return fn, true } fn.Inlines = append(fn.Inlines, &ast.TextNode{Text: string(fch)}) } else if in := cp.parseInline(); in != nil { if _, ok = in.(*ast.BreakNode); ok && input.IsEOLEOS(inp.Ch) { return nil, false } fn.Inlines = append(fn.Inlines, in) } } } var mapRuneLiteral = map[rune]ast.LiteralKind{ '@': ast.LiteralZettel, '`': ast.LiteralProg, runeModGrave: ast.LiteralProg, '\'': ast.LiteralInput, '=': ast.LiteralOutput, // No '$': ast.LiteralMath, because paring literal math is a little different } func (cp *zmkP) parseLiteral() (res ast.InlineNode, success bool) { inp := cp.inp fch := inp.Ch kind, ok := mapRuneLiteral[fch] if !ok { panic(fmt.Sprintf("%q is not a formatting char", fch)) } inp.Next() // read 2nd formatting character if inp.Ch != fch { return nil, false } inp.Next() var buf bytes.Buffer for { if inp.Ch == input.EOS { return nil, false } if inp.Ch == fch { if inp.Peek() == fch { inp.Next() inp.Next() return createLiteralNode(kind, cp.parseInlineAttributes(), buf.Bytes()), true } buf.WriteRune(fch) inp.Next() } else { tn := cp.parseText() buf.WriteString(tn.Text) } } } func createLiteralNode(kind ast.LiteralKind, a attrs.Attributes, content []byte) *ast.LiteralNode { if kind == ast.LiteralZettel { if val, found := a.Get(""); found && val == meta.SyntaxHTML { kind = ast.LiteralHTML a = a.Remove("") } } return &ast.LiteralNode{ Kind: kind, Attrs: a, Content: content, } } func (cp *zmkP) parseLiteralMath() (res ast.InlineNode, success bool) { inp := cp.inp inp.Next() // read 2nd formatting character if inp.Ch != '$' { return nil, false } inp.Next() pos := inp.Pos for { if inp.Ch == input.EOS { return nil, false } if inp.Ch == '$' && inp.Peek() == '$' { content := append([]byte{}, inp.Src[pos:inp.Pos]...) inp.Next() inp.Next() fn := &ast.LiteralNode{ Kind: ast.LiteralMath, Attrs: cp.parseInlineAttributes(), Content: content, } return fn, true } inp.Next() } } func (cp *zmkP) parseNdash() (res *ast.TextNode, success bool) { inp := cp.inp if inp.Peek() != inp.Ch { return nil, false } inp.Next() inp.Next() return &ast.TextNode{Text: "\u2013"}, true } func (cp *zmkP) parseEntity() (res *ast.TextNode, success bool) { if text, ok := cp.inp.ScanEntity(); ok { return &ast.TextNode{Text: text}, true } return nil, false } |
Added parser/zettelmark/node.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 | //----------------------------------------------------------------------------- // Copyright (c) 2020-present Detlef Stern // // This file is part of Zettelstore. // // Zettelstore is licensed under the latest version of the EUPL (European Union // Public License). Please see file LICENSE.txt for your rights and obligations // under this license. //----------------------------------------------------------------------------- package zettelmark import "zettelstore.de/z/ast" // Internal nodes for parsing zettelmark. These will be removed in // post-processing. // nullItemNode specifies a removable placeholder for an item node. type nullItemNode struct { ast.ItemNode } // nullDescriptionNode specifies a removable placeholder. type nullDescriptionNode struct { ast.DescriptionNode } |
Added parser/zettelmark/post-processor.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 | //----------------------------------------------------------------------------- // Copyright (c) 2020-present Detlef Stern // // This file is part of Zettelstore. // // Zettelstore is licensed under the latest version of the EUPL (European Union // Public License). Please see file LICENSE.txt for your rights and obligations // under this license. //----------------------------------------------------------------------------- package zettelmark import ( "strings" "zettelstore.de/z/ast" ) // postProcessBlocks is the entry point for post-processing a list of block nodes. func postProcessBlocks(bs *ast.BlockSlice) { pp := postProcessor{} ast.Walk(&pp, bs) } // postProcessInlines is the entry point for post-processing a list of inline nodes. func postProcessInlines(is *ast.InlineSlice) { pp := postProcessor{} ast.Walk(&pp, is) } // postProcessor is a visitor that cleans the abstract syntax tree. type postProcessor struct { inVerse bool } func (pp *postProcessor) Visit(node ast.Node) ast.Visitor { switch n := node.(type) { case *ast.BlockSlice: pp.visitBlockSlice(n) case *ast.InlineSlice: pp.visitInlineSlice(n) case *ast.ParaNode: return pp case *ast.RegionNode: pp.visitRegion(n) case *ast.HeadingNode: return pp case *ast.NestedListNode: pp.visitNestedList(n) case *ast.DescriptionListNode: pp.visitDescriptionList(n) case *ast.TableNode: pp.visitTable(n) case *ast.LinkNode: return pp case *ast.EmbedRefNode: return pp case *ast.EmbedBLOBNode: return pp case *ast.CiteNode: return pp case *ast.FootnoteNode: return pp case *ast.FormatNode: return pp } return nil } func (pp *postProcessor) visitRegion(rn *ast.RegionNode) { oldVerse := pp.inVerse if rn.Kind == ast.RegionVerse { pp.inVerse = true } pp.visitBlockSlice(&rn.Blocks) if len(rn.Inlines) > 0 { pp.visitInlineSlice(&rn.Inlines) } pp.inVerse = oldVerse } func (pp *postProcessor) visitNestedList(ln *ast.NestedListNode) { for i, item := range ln.Items { ln.Items[i] = pp.processItemSlice(item) } if ln.Kind != ast.NestedListQuote { return } items := []ast.ItemSlice{} collectedInlines := ast.InlineSlice{} addCollectedParagraph := func() { if len(collectedInlines) > 1 { items = append(items, []ast.ItemNode{&ast.ParaNode{Inlines: collectedInlines[1:]}}) collectedInlines = ast.InlineSlice{} } } for _, item := range ln.Items { if len(item) != 1 { // i.e. 0 or > 1 addCollectedParagraph() items = append(items, item) continue } // len(item) == 1 if pn, ok := item[0].(*ast.ParaNode); ok { collectedInlines = append(collectedInlines, &ast.BreakNode{}) collectedInlines = append(collectedInlines, pn.Inlines...) continue } addCollectedParagraph() items = append(items, item) } addCollectedParagraph() ln.Items = items } func (pp *postProcessor) visitDescriptionList(dn *ast.DescriptionListNode) { for i, def := range dn.Descriptions { if len(def.Term) > 0 { ast.Walk(pp, &dn.Descriptions[i].Term) } for j, b := range def.Descriptions { dn.Descriptions[i].Descriptions[j] = pp.processDescriptionSlice(b) } } } func (pp *postProcessor) visitTable(tn *ast.TableNode) { width := tableWidth(tn) tn.Align = make([]ast.Alignment, width) for i := 0; i < width; i++ { tn.Align[i] = ast.AlignDefault } if len(tn.Rows) > 0 && isHeaderRow(tn.Rows[0]) { tn.Header = tn.Rows[0] tn.Rows = tn.Rows[1:] pp.visitTableHeader(tn) } if len(tn.Header) > 0 { tn.Header = appendCells(tn.Header, width, tn.Align) for i, cell := range tn.Header { pp.processCell(cell, tn.Align[i]) } } pp.visitTableRows(tn, width) } func (*postProcessor) visitTableHeader(tn *ast.TableNode) { for pos, cell := range tn.Header { ins := cell.Inlines if len(ins) == 0 { continue } if textNode, ok := ins[0].(*ast.TextNode); ok { textNode.Text = strings.TrimPrefix(textNode.Text, "=") } if textNode, ok := ins[len(ins)-1].(*ast.TextNode); ok { if tnl := len(textNode.Text); tnl > 0 { if align := getAlignment(textNode.Text[tnl-1]); align != ast.AlignDefault { tn.Align[pos] = align textNode.Text = textNode.Text[0 : tnl-1] } } } } } func (pp *postProcessor) visitTableRows(tn *ast.TableNode, width int) { for i, row := range tn.Rows { tn.Rows[i] = appendCells(row, width, tn.Align) row = tn.Rows[i] for i, cell := range row { pp.processCell(cell, tn.Align[i]) } } } func tableWidth(tn *ast.TableNode) int { width := 0 for _, row := range tn.Rows { if width < len(row) { width = len(row) } } return width } func appendCells(row ast.TableRow, width int, colAlign []ast.Alignment) ast.TableRow { for len(row) < width { row = append(row, &ast.TableCell{ Align: colAlign[len(row)], Inlines: nil, }) } return row } func isHeaderRow(row ast.TableRow) bool { for _, cell := range row { if is := cell.Inlines; len(is) > 0 { if textNode, ok := is[0].(*ast.TextNode); ok { if strings.HasPrefix(textNode.Text, "=") { return true } } } } return false } func getAlignment(ch byte) ast.Alignment { switch ch { case ':': return ast.AlignCenter case '<': return ast.AlignLeft case '>': return ast.AlignRight default: return ast.AlignDefault } } // processCell tries to recognize cell formatting. func (pp *postProcessor) processCell(cell *ast.TableCell, colAlign ast.Alignment) { if tn := initialText(cell.Inlines); tn != nil { align := getAlignment(tn.Text[0]) if align == ast.AlignDefault { cell.Align = colAlign } else { tn.Text = tn.Text[1:] cell.Align = align } } else { cell.Align = colAlign } ast.Walk(pp, &cell.Inlines) } func initialText(ins ast.InlineSlice) *ast.TextNode { if len(ins) == 0 { return nil } if tn, ok := ins[0].(*ast.TextNode); ok && len(tn.Text) > 0 { return tn } return nil } func (pp *postProcessor) visitBlockSlice(bs *ast.BlockSlice) { if bs == nil { return } if len(*bs) == 0 { *bs = nil return } for _, bn := range *bs { ast.Walk(pp, bn) } fromPos, toPos := 0, 0 for fromPos < len(*bs) { (*bs)[toPos] = (*bs)[fromPos] fromPos++ switch bn := (*bs)[toPos].(type) { case *ast.ParaNode: if len(bn.Inlines) > 0 { toPos++ } case *nullItemNode: case *nullDescriptionNode: default: toPos++ } } for pos := toPos; pos < len(*bs); pos++ { (*bs)[pos] = nil // Allow excess nodes to be garbage collected. } *bs = (*bs)[:toPos:toPos] } // processItemSlice post-processes a slice of items. // It is one of the working horses for post-processing. func (pp *postProcessor) processItemSlice(ins ast.ItemSlice) ast.ItemSlice { if len(ins) == 0 { return nil } for _, in := range ins { ast.Walk(pp, in) } fromPos, toPos := 0, 0 for fromPos < len(ins) { ins[toPos] = ins[fromPos] fromPos++ switch in := ins[toPos].(type) { case *ast.ParaNode: if in != nil && len(in.Inlines) > 0 { toPos++ } case *nullItemNode: case *nullDescriptionNode: default: toPos++ } } for pos := toPos; pos < len(ins); pos++ { ins[pos] = nil // Allow excess nodes to be garbage collected. } return ins[:toPos:toPos] } // processDescriptionSlice post-processes a slice of descriptions. // It is one of the working horses for post-processing. func (pp *postProcessor) processDescriptionSlice(dns ast.DescriptionSlice) ast.DescriptionSlice { if len(dns) == 0 { return nil } for _, dn := range dns { ast.Walk(pp, dn) } fromPos, toPos := 0, 0 for fromPos < len(dns) { dns[toPos] = dns[fromPos] fromPos++ switch dn := dns[toPos].(type) { case *ast.ParaNode: if len(dn.Inlines) > 0 { toPos++ } case *nullDescriptionNode: default: toPos++ } } for pos := toPos; pos < len(dns); pos++ { dns[pos] = nil // Allow excess nodes to be garbage collected. } return dns[:toPos:toPos] } func (pp *postProcessor) visitInlineSlice(is *ast.InlineSlice) { if is == nil { return } if len(*is) == 0 { *is = nil return } for _, in := range *is { ast.Walk(pp, in) } pp.processInlineSliceHead(is) toPos := pp.processInlineSliceCopy(is) toPos = pp.processInlineSliceTail(is, toPos) *is = (*is)[:toPos:toPos] } // processInlineSliceHead removes leading spaces and empty text. func (pp *postProcessor) processInlineSliceHead(is *ast.InlineSlice) { ins := *is for i, in := range ins { switch in := in.(type) { case *ast.SpaceNode: if pp.inVerse { *is = ins[i:] return } case *ast.TextNode: if len(in.Text) > 0 { *is = ins[i:] return } default: *is = ins[i:] return } } *is = ins[0:0] } // processInlineSliceCopy goes forward through the slice and tries to eliminate // elements that follow the current element. // // Two text nodes are merged into one. // // Two spaces following a break are merged into a hard break. func (pp *postProcessor) processInlineSliceCopy(is *ast.InlineSlice) int { ins := *is maxPos := len(ins) toPos := pp.processInlineSliceCopyLoop(is, maxPos) for pos := toPos; pos < maxPos; pos++ { ins[pos] = nil // Allow excess nodes to be garbage collected. } return toPos } func (pp *postProcessor) processInlineSliceCopyLoop(is *ast.InlineSlice, maxPos int) int { ins := *is fromPos, toPos := 0, 0 for fromPos < maxPos { ins[toPos] = ins[fromPos] fromPos++ switch in := ins[toPos].(type) { case *ast.TextNode: fromPos = processTextNode(ins, maxPos, in, fromPos) case *ast.SpaceNode: if pp.inVerse { in.Lexeme = strings.Repeat("\u00a0", in.Count()) } fromPos = processSpaceNode(ins, maxPos, in, toPos, fromPos) case *ast.BreakNode: if pp.inVerse { in.Hard = true } } toPos++ } return toPos } func processTextNode(ins ast.InlineSlice, maxPos int, in *ast.TextNode, fromPos int) int { for fromPos < maxPos { if tn, ok := ins[fromPos].(*ast.TextNode); ok { in.Text = in.Text + tn.Text fromPos++ } else { break } } return fromPos } func processSpaceNode(ins ast.InlineSlice, maxPos int, in *ast.SpaceNode, toPos, fromPos int) int { if fromPos < maxPos { switch nn := ins[fromPos].(type) { case *ast.BreakNode: if in.Count() > 1 { nn.Hard = true ins[toPos] = nn fromPos++ } case *ast.LiteralNode: if nn.Kind == ast.LiteralComment { ins[toPos] = ins[fromPos] fromPos++ } } } return fromPos } // processInlineSliceTail removes empty text nodes, breaks and spaces at the end. func (*postProcessor) processInlineSliceTail(is *ast.InlineSlice, toPos int) int { ins := *is for toPos > 0 { switch n := ins[toPos-1].(type) { case *ast.TextNode: if len(n.Text) > 0 { return toPos } case *ast.BreakNode: case *ast.SpaceNode: default: return toPos } toPos-- ins[toPos] = nil // Kill node to enable garbage collection } return toPos } |
Added parser/zettelmark/zettelmark.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 | //----------------------------------------------------------------------------- // Copyright (c) 2020-present Detlef Stern // // This file is part of Zettelstore. // // Zettelstore is licensed under the latest version of the EUPL (European Union // Public License). Please see file LICENSE.txt for your rights and obligations // under this license. //----------------------------------------------------------------------------- // Package zettelmark provides a parser for zettelmarkup. package zettelmark import ( "strings" "unicode" "zettelstore.de/c/attrs" "zettelstore.de/z/ast" "zettelstore.de/z/domain/meta" "zettelstore.de/z/input" "zettelstore.de/z/parser" ) func init() { parser.Register(&parser.Info{ Name: meta.SyntaxZmk, AltNames: nil, IsASTParser: true, IsTextFormat: true, IsImageFormat: false, ParseBlocks: parseBlocks, ParseInlines: parseInlines, }) } func parseBlocks(inp *input.Input, _ *meta.Meta, _ string) ast.BlockSlice { parser := &zmkP{inp: inp} bs := parser.parseBlockSlice() postProcessBlocks(&bs) return bs } func parseInlines(inp *input.Input, _ string) ast.InlineSlice { parser := &zmkP{inp: inp} is := parser.parseInlineSlice() postProcessInlines(&is) return is } type zmkP struct { inp *input.Input // Input stream lists []*ast.NestedListNode // Stack of lists table *ast.TableNode // Current table descrl *ast.DescriptionListNode // Current description list nestingLevel int // Count nesting of block and inline elements } // runeModGrave is Unicode code point U+02CB (715) called "MODIFIER LETTER // GRAVE ACCENT". On the iPad it is much more easier to type in this code point // than U+0060 (96) "Grave accent" (aka backtick). Therefore, U+02CB will be // considered equivalent to U+0060. const runeModGrave = 'ˋ' // This is NOT '`'! const maxNestingLevel = 50 // clearStacked removes all multi-line nodes from parser. func (cp *zmkP) clearStacked() { cp.lists = nil cp.table = nil cp.descrl = nil } func (cp *zmkP) parseNormalAttribute(attrs map[string]string) bool { inp := cp.inp posK := inp.Pos for isNameRune(inp.Ch) { inp.Next() } if posK == inp.Pos { return false } key := string(inp.Src[posK:inp.Pos]) if inp.Ch != '=' { attrs[key] = "" return true } return cp.parseAttributeValue(key, attrs) } func (cp *zmkP) parseAttributeValue(key string, attrs map[string]string) bool { inp := cp.inp inp.Next() if inp.Ch == '"' { return cp.parseQuotedAttributeValue(key, attrs) } posV := inp.Pos for { switch inp.Ch { case input.EOS: return false case '\n', '\r', ' ', '}': updateAttrs(attrs, key, string(inp.Src[posV:inp.Pos])) return true } inp.Next() } } func (cp *zmkP) parseQuotedAttributeValue(key string, attrs map[string]string) bool { inp := cp.inp inp.Next() var sb strings.Builder for { switch inp.Ch { case input.EOS: return false case '"': updateAttrs(attrs, key, sb.String()) inp.Next() return true case '\\': inp.Next() switch inp.Ch { case input.EOS, '\n', '\r': return false } fallthrough default: sb.WriteRune(inp.Ch) inp.Next() } } } func updateAttrs(attrs map[string]string, key, val string) { if prevVal := attrs[key]; len(prevVal) > 0 { attrs[key] = prevVal + " " + val } else { attrs[key] = val } } func (cp *zmkP) parseBlockAttributes() attrs.Attributes { inp := cp.inp pos := inp.Pos for isNameRune(inp.Ch) { inp.Next() } if pos < inp.Pos { return attrs.Attributes{"": string(inp.Src[pos:inp.Pos])} } // No immediate name: skip spaces cp.skipSpace() return cp.parseInlineAttributes() } func (cp *zmkP) parseInlineAttributes() attrs.Attributes { inp := cp.inp pos := inp.Pos if attrs, success := cp.doParseAttributes(); success { return attrs } inp.SetPos(pos) return nil } // doParseAttributes reads attributes. func (cp *zmkP) doParseAttributes() (res attrs.Attributes, success bool) { inp := cp.inp if inp.Ch != '{' { return nil, false } inp.Next() a := attrs.Attributes{} if !cp.parseAttributeValues(a) { return nil, false } inp.Next() return a, true } func (cp *zmkP) parseAttributeValues(a attrs.Attributes) bool { inp := cp.inp for { cp.skipSpaceLine() switch inp.Ch { case input.EOS: return false case '}': return true case '.': inp.Next() posC := inp.Pos for isNameRune(inp.Ch) { inp.Next() } if posC == inp.Pos { return false } updateAttrs(a, "class", string(inp.Src[posC:inp.Pos])) case '=': delete(a, "") if !cp.parseAttributeValue("", a) { return false } default: if !cp.parseNormalAttribute(a) { return false } } switch inp.Ch { case '}': return true case '\n', '\r': case ' ', ',': inp.Next() default: return false } } } func (cp *zmkP) skipSpaceLine() { for inp := cp.inp; ; { switch inp.Ch { case ' ': inp.Next() case '\n', '\r': inp.EatEOL() default: return } } } func (cp *zmkP) skipSpace() { for inp := cp.inp; inp.Ch == ' '; { inp.Next() } } func isNameRune(ch rune) bool { return unicode.IsLetter(ch) || unicode.IsDigit(ch) || ch == '-' || ch == '_' } |
Added parser/zettelmark/zettelmark_fuzz_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 | //----------------------------------------------------------------------------- // Copyright (c) 2022-present Detlef Stern // // This file is part of Zettelstore. // // Zettelstore is licensed under the latest version of the EUPL (European Union // Public License). Please see file LICENSE.txt for your rights and obligations // under this license. //----------------------------------------------------------------------------- package zettelmark_test import ( "testing" "zettelstore.de/z/config" "zettelstore.de/z/domain/meta" "zettelstore.de/z/input" "zettelstore.de/z/parser" ) func FuzzParseBlocks(f *testing.F) { f.Fuzz(func(t *testing.T, src []byte) { t.Parallel() inp := input.NewInput(src) parser.ParseBlocks(inp, nil, meta.SyntaxZmk, config.NoHTML) }) } |
Added parser/zettelmark/zettelmark_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 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 593 594 595 596 597 598 599 600 601 602 603 604 605 606 607 608 609 610 611 612 613 614 615 616 617 618 619 620 621 622 623 624 625 626 627 628 629 630 631 632 633 634 635 636 637 638 639 640 641 642 643 644 645 646 647 648 649 650 651 652 653 654 655 656 657 658 659 660 661 662 663 664 665 666 667 668 669 670 671 672 673 674 675 676 677 678 679 680 681 682 683 684 685 686 687 688 689 690 691 692 693 694 695 696 697 698 699 700 701 702 703 704 705 706 707 708 709 710 711 712 713 714 715 716 717 718 719 720 721 722 723 724 725 726 727 728 729 730 731 732 733 734 735 736 737 738 739 740 741 742 743 744 745 746 747 748 749 750 751 752 753 754 755 756 757 758 759 760 761 762 763 764 765 766 767 768 769 770 771 772 773 774 775 776 777 778 779 780 781 782 783 784 785 786 787 788 789 790 791 792 793 794 795 796 797 798 799 800 801 802 803 804 805 806 807 808 809 810 811 812 813 814 815 816 817 818 819 820 821 822 823 824 825 826 827 828 829 830 831 832 833 834 835 836 837 838 839 840 841 842 843 844 845 846 847 848 849 850 851 852 853 854 855 856 857 858 859 860 861 862 863 864 865 866 867 868 869 870 871 872 873 874 875 876 877 878 879 880 881 882 883 884 885 886 887 888 889 890 891 892 893 894 895 896 897 898 899 900 901 902 903 904 905 906 907 908 909 910 911 912 913 914 915 916 917 918 919 920 921 922 923 924 925 926 927 928 929 930 931 932 933 934 935 936 937 938 939 940 941 942 943 944 945 946 947 948 949 950 951 952 953 954 955 956 957 958 959 960 961 962 963 964 965 966 967 968 969 970 971 972 973 974 975 976 977 978 979 980 981 982 983 984 985 986 987 988 989 990 991 992 993 994 995 996 997 998 999 1000 1001 1002 1003 1004 1005 1006 1007 1008 1009 1010 1011 1012 1013 1014 1015 1016 1017 1018 1019 1020 1021 1022 1023 1024 1025 1026 1027 1028 1029 1030 1031 1032 1033 1034 1035 1036 1037 1038 1039 1040 1041 1042 1043 1044 | //----------------------------------------------------------------------------- // Copyright (c) 2020-present Detlef Stern // // This file is part of Zettelstore. // // Zettelstore is licensed under the latest version of the EUPL (European Union // Public License). Please see file LICENSE.txt for your rights and obligations // under this license. //----------------------------------------------------------------------------- // Package zettelmark_test provides some tests for the zettelmarkup parser. package zettelmark_test import ( "fmt" "strings" "testing" "zettelstore.de/c/attrs" "zettelstore.de/z/ast" "zettelstore.de/z/config" "zettelstore.de/z/domain/meta" "zettelstore.de/z/input" "zettelstore.de/z/parser" ) type TestCase struct{ source, want string } type TestCases []TestCase func replace(s string, tcs TestCases) TestCases { var testCases TestCases for _, tc := range tcs { source := strings.ReplaceAll(tc.source, "$", s) want := strings.ReplaceAll(tc.want, "$", s) testCases = append(testCases, TestCase{source, want}) } return testCases } func checkTcs(t *testing.T, tcs TestCases) { t.Helper() for tcn, tc := range tcs { t.Run(fmt.Sprintf("TC=%02d,src=%q", tcn, tc.source), func(st *testing.T) { st.Helper() inp := input.NewInput([]byte(tc.source)) bns := parser.ParseBlocks(inp, nil, meta.SyntaxZmk, config.NoHTML) var tv TestVisitor ast.Walk(&tv, &bns) got := tv.String() if tc.want != got { st.Errorf("\nwant=%q\n got=%q", tc.want, got) } }) } } func TestEOL(t *testing.T) { t.Parallel() checkTcs(t, TestCases{ {"", ""}, {"\n", ""}, {"\r", ""}, {"\r\n", ""}, {"\n\n", ""}, }) } func TestText(t *testing.T) { t.Parallel() checkTcs(t, TestCases{ {"abcd", "(PARA abcd)"}, {"ab cd", "(PARA ab SP cd)"}, {"abcd ", "(PARA abcd)"}, {" abcd", "(PARA abcd)"}, {"\\", "(PARA \\)"}, {"\\\n", ""}, {"\\\ndef", "(PARA HB def)"}, {"\\\r", ""}, {"\\\rdef", "(PARA HB def)"}, {"\\\r\n", ""}, {"\\\r\ndef", "(PARA HB def)"}, {"\\a", "(PARA a)"}, {"\\aa", "(PARA aa)"}, {"a\\a", "(PARA aa)"}, {"\\+", "(PARA +)"}, {"\\ ", "(PARA \u00a0)"}, {"http://a, http://b", "(PARA http://a, SP http://b)"}, }) } func TestSpace(t *testing.T) { t.Parallel() checkTcs(t, TestCases{ {" ", ""}, {"\t", ""}, {" ", ""}, }) } func TestSoftBreak(t *testing.T) { t.Parallel() checkTcs(t, TestCases{ {"x\ny", "(PARA x SB y)"}, {"z\n", "(PARA z)"}, {" \n ", ""}, {" \n", ""}, }) } func TestHardBreak(t *testing.T) { t.Parallel() checkTcs(t, TestCases{ {"x \ny", "(PARA x HB y)"}, {"z \n", "(PARA z)"}, {" \n ", ""}, {" \n", ""}, }) } func TestLink(t *testing.T) { t.Parallel() checkTcs(t, TestCases{ {"[", "(PARA [)"}, {"[[", "(PARA [[)"}, {"[[|", "(PARA [[|)"}, {"[[]", "(PARA [[])"}, {"[[|]", "(PARA [[|])"}, {"[[]]", "(PARA [[]])"}, {"[[|]]", "(PARA [[|]])"}, {"[[ ]]", "(PARA [[ SP ]])"}, {"[[\n]]", "(PARA [[ SB ]])"}, {"[[ a]]", "(PARA (LINK a))"}, {"[[a ]]", "(PARA [[a SP ]])"}, {"[[a\n]]", "(PARA [[a SB ]])"}, {"[[a]]", "(PARA (LINK a))"}, {"[[12345678901234]]", "(PARA (LINK 12345678901234))"}, {"[[a]", "(PARA [[a])"}, {"[[|a]]", "(PARA [[|a]])"}, {"[[b|]]", "(PARA [[b|]])"}, {"[[b|a]]", "(PARA (LINK a b))"}, {"[[b| a]]", "(PARA (LINK a b))"}, {"[[b%c|a]]", "(PARA (LINK a b%c))"}, {"[[b%%c|a]]", "(PARA [[b {% c|a]]})"}, {"[[b|a]", "(PARA [[b|a])"}, {"[[b\nc|a]]", "(PARA (LINK a b SB c))"}, {"[[b c|a#n]]", "(PARA (LINK a#n b SP c))"}, {"[[a]]go", "(PARA (LINK a) go)"}, {"[[b|a]]{go}", "(PARA (LINK a b)[ATTR go])"}, {"[[[[a]]|b]]", "(PARA [[ (LINK a) |b]])"}, {"[[a[b]c|d]]", "(PARA (LINK d a[b]c))"}, {"[[[b]c|d]]", "(PARA [ (LINK d b]c))"}, {"[[a[]c|d]]", "(PARA (LINK d a[]c))"}, {"[[a[b]|d]]", "(PARA (LINK d a[b]))"}, {"[[\\|]]", "(PARA (LINK %5C%7C))"}, {"[[\\||a]]", "(PARA (LINK a |))"}, {"[[b\\||a]]", "(PARA (LINK a b|))"}, {"[[b\\|c|a]]", "(PARA (LINK a b|c))"}, {"[[\\]]]", "(PARA (LINK %5C%5D))"}, {"[[\\]|a]]", "(PARA (LINK a ]))"}, {"[[b\\]|a]]", "(PARA (LINK a b]))"}, {"[[\\]\\||a]]", "(PARA (LINK a ]|))"}, {"[[http://a]]", "(PARA (LINK http://a))"}, {"[[http://a|http://a]]", "(PARA (LINK http://a http://a))"}, {"[[[[a]]]]", "(PARA [[ (LINK a) ]])"}, {"[[query:title]]", "(PARA (LINK query:title))"}, {"[[query:title syntax]]", "(PARA (LINK query:title syntax))"}, {"[[query:title | action]]", "(PARA (LINK query:title | action))"}, {"[[Text|query:title]]", "(PARA (LINK query:title Text))"}, {"[[Text|query:title syntax]]", "(PARA (LINK query:title syntax Text))"}, {"[[Text|query:title | action]]", "(PARA (LINK query:title | action Text))"}, }) } func TestCite(t *testing.T) { t.Parallel() checkTcs(t, TestCases{ {"[@", "(PARA [@)"}, {"[@]", "(PARA [@])"}, {"[@a]", "(PARA (CITE a))"}, {"[@ a]", "(PARA [@ SP a])"}, {"[@a ]", "(PARA (CITE a))"}, {"[@a\n]", "(PARA (CITE a))"}, {"[@a\nx]", "(PARA (CITE a SB x))"}, {"[@a\n\n]", "(PARA [@a)(PARA ])"}, {"[@a,\n]", "(PARA (CITE a))"}, {"[@a,n]", "(PARA (CITE a n))"}, {"[@a| n]", "(PARA (CITE a n))"}, {"[@a|n ]", "(PARA (CITE a n))"}, {"[@a,[@b]]", "(PARA (CITE a (CITE b)))"}, {"[@a]{color=green}", "(PARA (CITE a)[ATTR color=green])"}, }) } func TestFootnote(t *testing.T) { t.Parallel() checkTcs(t, TestCases{ {"[^", "(PARA [^)"}, {"[^]", "(PARA (FN))"}, {"[^abc]", "(PARA (FN abc))"}, {"[^abc ]", "(PARA (FN abc))"}, {"[^abc\ndef]", "(PARA (FN abc SB def))"}, {"[^abc\n\ndef]", "(PARA [^abc)(PARA def])"}, {"[^abc[^def]]", "(PARA (FN abc (FN def)))"}, {"[^abc]{-}", "(PARA (FN abc)[ATTR -])"}, }) } func TestEmbed(t *testing.T) { t.Parallel() checkTcs(t, TestCases{ {"{", "(PARA {)"}, {"{{", "(PARA {{)"}, {"{{|", "(PARA {{|)"}, {"{{}", "(PARA {{})"}, {"{{|}", "(PARA {{|})"}, {"{{}}", "(PARA {{}})"}, {"{{|}}", "(PARA {{|}})"}, {"{{ }}", "(PARA {{ SP }})"}, {"{{\n}}", "(PARA {{ SB }})"}, {"{{a }}", "(PARA {{a SP }})"}, {"{{a\n}}", "(PARA {{a SB }})"}, {"{{a}}", "(PARA (EMBED a))"}, {"{{12345678901234}}", "(PARA (EMBED 12345678901234))"}, {"{{ a}}", "(PARA (EMBED a))"}, {"{{a}", "(PARA {{a})"}, {"{{|a}}", "(PARA {{|a}})"}, {"{{b|}}", "(PARA {{b|}})"}, {"{{b|a}}", "(PARA (EMBED a b))"}, {"{{b| a}}", "(PARA (EMBED a b))"}, {"{{b|a}", "(PARA {{b|a})"}, {"{{b\nc|a}}", "(PARA (EMBED a b SB c))"}, {"{{b c|a#n}}", "(PARA (EMBED a#n b SP c))"}, {"{{a}}{go}", "(PARA (EMBED a)[ATTR go])"}, {"{{{{a}}|b}}", "(PARA {{ (EMBED a) |b}})"}, {"{{\\|}}", "(PARA (EMBED %5C%7C))"}, {"{{\\||a}}", "(PARA (EMBED a |))"}, {"{{b\\||a}}", "(PARA (EMBED a b|))"}, {"{{b\\|c|a}}", "(PARA (EMBED a b|c))"}, {"{{\\}}}", "(PARA (EMBED %5C%7D))"}, {"{{\\}|a}}", "(PARA (EMBED a }))"}, {"{{b\\}|a}}", "(PARA (EMBED a b}))"}, {"{{\\}\\||a}}", "(PARA (EMBED a }|))"}, {"{{http://a}}", "(PARA (EMBED http://a))"}, {"{{http://a|http://a}}", "(PARA (EMBED http://a http://a))"}, {"{{{{a}}}}", "(PARA {{ (EMBED a) }})"}, }) } func TestMark(t *testing.T) { t.Parallel() checkTcs(t, TestCases{ {"[!", "(PARA [!)"}, {"[!\n", "(PARA [!)"}, {"[!]", "(PARA (MARK #*))"}, {"[!][!]", "(PARA (MARK #*) (MARK #*-1))"}, {"[! ]", "(PARA [! SP ])"}, {"[!a]", "(PARA (MARK \"a\" #a))"}, {"[!a][!a]", "(PARA (MARK \"a\" #a) (MARK \"a\" #a-1))"}, {"[!a ]", "(PARA [!a SP ])"}, {"[!a_]", "(PARA (MARK \"a_\" #a))"}, {"[!a_][!a]", "(PARA (MARK \"a_\" #a) (MARK \"a\" #a-1))"}, {"[!a-b]", "(PARA (MARK \"a-b\" #a-b))"}, {"[!a|b]", "(PARA (MARK \"a\" #a b))"}, {"[!a|]", "(PARA (MARK \"a\" #a))"}, {"[!|b]", "(PARA (MARK #* b))"}, {"[!|b c]", "(PARA (MARK #* b SP c))"}, }) } func TestComment(t *testing.T) { t.Parallel() checkTcs(t, TestCases{ {"%", "(PARA %)"}, {"%%", "(PARA {%})"}, {"%\n", "(PARA %)"}, {"%%\n", "(PARA {%})"}, {"%%a", "(PARA {% a})"}, {"%%%a", "(PARA {% a})"}, {"%% a", "(PARA {% a})"}, {"%%% a", "(PARA {% a})"}, {"%% % a", "(PARA {% % a})"}, {"%%a", "(PARA {% a})"}, {"a%%b", "(PARA a {% b})"}, {"a %%b", "(PARA a {% b})"}, {" %%b", "(PARA {% b})"}, {"%%b ", "(PARA {% b })"}, {"100%", "(PARA 100%)"}, }) } func TestFormat(t *testing.T) { t.Parallel() // Not for Insert / '>', because collision with quoted list for _, ch := range []string{"_", "*", "~", "^", ",", "\"", ":"} { checkTcs(t, replace(ch, TestCases{ {"$", "(PARA $)"}, {"$$", "(PARA $$)"}, {"$$$", "(PARA $$$)"}, {"$$$$", "(PARA {$})"}, })) } for _, ch := range []string{"_", "*", ">", "~", "^", ",", "\"", ":"} { checkTcs(t, replace(ch, TestCases{ {"$$a$$", "(PARA {$ a})"}, {"$$a$$$", "(PARA {$ a} $)"}, {"$$$a$$", "(PARA {$ $a})"}, {"$$$a$$$", "(PARA {$ $a} $)"}, {"$\\$", "(PARA $$)"}, {"$\\$$", "(PARA $$$)"}, {"$$\\$", "(PARA $$$)"}, {"$$a\\$$", "(PARA $$a$$)"}, {"$$a$\\$", "(PARA $$a$$)"}, {"$$a\\$$$", "(PARA {$ a$})"}, {"$$a\na$$", "(PARA {$ a SB a})"}, {"$$a\n\na$$", "(PARA $$a)(PARA a$$)"}, {"$$a$${go}", "(PARA {$ a}[ATTR go])"}, })) } checkTcs(t, TestCases{ {"__****__", "(PARA {_ {*}})"}, {"__**a**__", "(PARA {_ {* a}})"}, {"__**__**", "(PARA __ {* __})"}, }) } func TestLiteral(t *testing.T) { t.Parallel() for _, ch := range []string{"@", "`", "'", "="} { checkTcs(t, replace(ch, TestCases{ {"$", "(PARA $)"}, {"$$", "(PARA $$)"}, {"$$$", "(PARA $$$)"}, {"$$$$", "(PARA {$})"}, {"$$a$$", "(PARA {$ a})"}, {"$$a$$$", "(PARA {$ a} $)"}, {"$$$a$$", "(PARA {$ $a})"}, {"$$$a$$$", "(PARA {$ $a} $)"}, {"$\\$", "(PARA $$)"}, {"$\\$$", "(PARA $$$)"}, {"$$\\$", "(PARA $$$)"}, {"$$a\\$$", "(PARA $$a$$)"}, {"$$a$\\$", "(PARA $$a$$)"}, {"$$a\\$$$", "(PARA {$ a$})"}, {"$$a$${go}", "(PARA {$ a}[ATTR go])"}, })) } checkTcs(t, TestCases{ {"''````''", "(PARA {' ````})"}, {"''``a``''", "(PARA {' ``a``})"}, {"''``''``", "(PARA {' ``} ``)"}, {"''\\'''", "(PARA {' '})"}, }) } func TestLiteralMath(t *testing.T) { t.Parallel() checkTcs(t, TestCases{ {"$", "(PARA $)"}, {"$$", "(PARA $$)"}, {"$$$", "(PARA $$$)"}, {"$$$$", "(PARA {$})"}, {"$$a$$", "(PARA {$ a})"}, {"$$a$$$", "(PARA {$ a} $)"}, {"$$$a$$", "(PARA {$ $a})"}, {"$$$a$$$", "(PARA {$ $a} $)"}, {`$\$`, "(PARA $$)"}, {`$\$$`, "(PARA $$$)"}, {`$$\$`, "(PARA $$$)"}, {`$$a\$$`, `(PARA {$ a\})`}, {`$$a$\$`, "(PARA $$a$$)"}, {`$$a\$$$`, `(PARA {$ a\} $)`}, {"$$a$${go}", "(PARA {$ a}[ATTR go])"}, }) } func TestMixFormatCode(t *testing.T) { t.Parallel() checkTcs(t, TestCases{ {"__abc__\n**def**", "(PARA {_ abc} SB {* def})"}, {"''abc''\n==def==", "(PARA {' abc} SB {= def})"}, {"__abc__\n==def==", "(PARA {_ abc} SB {= def})"}, {"__abc__\n``def``", "(PARA {_ abc} SB {` def})"}, {"\"\"ghi\"\"\n::abc::\n``def``\n", "(PARA {\" ghi} SB {: abc} SB {` def})"}, }) } func TestNDash(t *testing.T) { t.Parallel() checkTcs(t, TestCases{ {"--", "(PARA \u2013)"}, {"a--b", "(PARA a\u2013b)"}, }) } func TestEntity(t *testing.T) { t.Parallel() checkTcs(t, TestCases{ {"&", "(PARA &)"}, {"&;", "(PARA &;)"}, {"&#;", "(PARA &#;)"}, {"a;", "(PARA a;)"}, {"&#x;", "(PARA &#x;)"}, {"�z;", "(PARA �z;)"}, {"&1;", "(PARA &1;)"}, {"	", "(PARA 	)"}, // No numeric entities below space are not allowed. {"", "(PARA )"}, // Good cases {"<", "(PARA <)"}, {"0", "(PARA 0)"}, {"J", "(PARA J)"}, {"J", "(PARA J)"}, {"…", "(PARA \u2026)"}, {" ", "(PARA \u00a0)"}, {"E: &,?;c.", "(PARA E: SP &,?;c.)"}, }) } func TestVerbatimZettel(t *testing.T) { t.Parallel() checkTcs(t, TestCases{ {"@@@\n@@@", "(ZETTEL)"}, {"@@@\nabc\n@@@", "(ZETTEL\nabc)"}, {"@@@@def\nabc\n@@@@", "(ZETTEL\nabc)[ATTR =def]"}, }) } func TestVerbatimCode(t *testing.T) { t.Parallel() checkTcs(t, TestCases{ {"```\n```", "(PROG)"}, {"```\nabc\n```", "(PROG\nabc)"}, {"```\nabc\n````", "(PROG\nabc)"}, {"````\nabc\n````", "(PROG\nabc)"}, {"````\nabc\n```\n````", "(PROG\nabc\n```)"}, {"````go\nabc\n````", "(PROG\nabc)[ATTR =go]"}, }) } func TestVerbatimEval(t *testing.T) { t.Parallel() checkTcs(t, TestCases{ {"~~~\n~~~", "(EVAL)"}, {"~~~\nabc\n~~~", "(EVAL\nabc)"}, {"~~~\nabc\n~~~~", "(EVAL\nabc)"}, {"~~~~\nabc\n~~~~", "(EVAL\nabc)"}, {"~~~~\nabc\n~~~\n~~~~", "(EVAL\nabc\n~~~)"}, {"~~~~go\nabc\n~~~~", "(EVAL\nabc)[ATTR =go]"}, }) } func TestVerbatimMath(t *testing.T) { t.Parallel() checkTcs(t, TestCases{ {"$$$\n$$$", "(MATH)"}, {"$$$\nabc\n$$$", "(MATH\nabc)"}, {"$$$\nabc\n$$$$", "(MATH\nabc)"}, {"$$$$\nabc\n$$$$", "(MATH\nabc)"}, {"$$$$\nabc\n$$$\n$$$$", "(MATH\nabc\n$$$)"}, {"$$$$go\nabc\n$$$$", "(MATH\nabc)[ATTR =go]"}, }) } func TestVerbatimComment(t *testing.T) { t.Parallel() checkTcs(t, TestCases{ {"%%%\n%%%", "(COMMENT)"}, {"%%%\nabc\n%%%", "(COMMENT\nabc)"}, {"%%%%go\nabc\n%%%%", "(COMMENT\nabc)[ATTR =go]"}, }) } func TestPara(t *testing.T) { t.Parallel() checkTcs(t, TestCases{ {"a\n\nb", "(PARA a)(PARA b)"}, {"a\n \nb", "(PARA a)(PARA b)"}, }) } func TestSpanRegion(t *testing.T) { t.Parallel() checkTcs(t, TestCases{ {":::\n:::", "(SPAN)"}, {":::\nabc\n:::", "(SPAN (PARA abc))"}, {":::\nabc\n::::", "(SPAN (PARA abc))"}, {"::::\nabc\n::::", "(SPAN (PARA abc))"}, {"::::\nabc\n:::\ndef\n:::\n::::", "(SPAN (PARA abc)(SPAN (PARA def)))"}, {":::{go}\n:::", "(SPAN)[ATTR go]"}, {":::\nabc\n::: def ", "(SPAN (PARA abc) (LINE def))"}, }) } func TestQuoteRegion(t *testing.T) { t.Parallel() checkTcs(t, TestCases{ {"<<<\n<<<", "(QUOTE)"}, {"<<<\nabc\n<<<", "(QUOTE (PARA abc))"}, {"<<<\nabc\n<<<<", "(QUOTE (PARA abc))"}, {"<<<<\nabc\n<<<<", "(QUOTE (PARA abc))"}, {"<<<<\nabc\n<<<\ndef\n<<<\n<<<<", "(QUOTE (PARA abc)(QUOTE (PARA def)))"}, {"<<<go\n<<<", "(QUOTE)[ATTR =go]"}, {"<<<\nabc\n<<< def ", "(QUOTE (PARA abc) (LINE def))"}, }) } func TestVerseRegion(t *testing.T) { t.Parallel() checkTcs(t, replace("\"", TestCases{ {"$$$\n$$$", "(VERSE)"}, {"$$$\nabc\n$$$", "(VERSE (PARA abc))"}, {"$$$\nabc\n$$$$", "(VERSE (PARA abc))"}, {"$$$$\nabc\n$$$$", "(VERSE (PARA abc))"}, {"$$$\nabc\ndef\n$$$", "(VERSE (PARA abc HB def))"}, {"$$$$\nabc\n$$$\ndef\n$$$\n$$$$", "(VERSE (PARA abc)(VERSE (PARA def)))"}, {"$$$go\n$$$", "(VERSE)[ATTR =go]"}, {"$$$\nabc\n$$$ def ", "(VERSE (PARA abc) (LINE def))"}, })) } func TestHeading(t *testing.T) { t.Parallel() checkTcs(t, TestCases{ {"=h", "(PARA =h)"}, {"= h", "(PARA = SP h)"}, {"==h", "(PARA ==h)"}, {"== h", "(PARA == SP h)"}, {"===h", "(PARA ===h)"}, {"=== h", "(H1 h #h)"}, {"=== h", "(H1 h #h)"}, {"==== h", "(H2 h #h)"}, {"===== h", "(H3 h #h)"}, {"====== h", "(H4 h #h)"}, {"======= h", "(H5 h #h)"}, {"======== h", "(H5 h #h)"}, {"=", "(PARA =)"}, {"=== h=__=a__", "(H1 h= {_ =a} #h-a)"}, {"=\n", "(PARA =)"}, {"a=", "(PARA a=)"}, {" =", "(PARA =)"}, {"=== h\na", "(H1 h #h)(PARA a)"}, {"=== h i {-}", "(H1 h SP i #h-i)[ATTR -]"}, {"=== h {{a}}", "(H1 h SP (EMBED a) #h)"}, {"=== h{{a}}", "(H1 h (EMBED a) #h)"}, {"=== {{a}}", "(H1 (EMBED a))"}, {"=== h {{a}}{-}", "(H1 h SP (EMBED a)[ATTR -] #h)"}, {"=== h {{a}} {-}", "(H1 h SP (EMBED a) #h)[ATTR -]"}, {"=== h {-}{{a}}", "(H1 h #h)[ATTR -]"}, {"=== h{id=abc}", "(H1 h #h)[ATTR id=abc]"}, {"=== h\n=== h", "(H1 h #h)(H1 h #h-1)"}, }) } func TestHRule(t *testing.T) { t.Parallel() checkTcs(t, TestCases{ {"-", "(PARA -)"}, {"---", "(HR)"}, {"----", "(HR)"}, {"---A", "(HR)[ATTR =A]"}, {"---A-", "(HR)[ATTR =A-]"}, {"-1", "(PARA -1)"}, {"2-1", "(PARA 2-1)"}, {"--- { go } ", "(HR)[ATTR go]"}, {"--- { .go } ", "(HR)[ATTR class=go]"}, }) } func TestList(t *testing.T) { t.Parallel() // No ">" in the following, because quotation lists may have empty items. for _, ch := range []string{"*", "#"} { checkTcs(t, replace(ch, TestCases{ {"$", "(PARA $)"}, {"$$", "(PARA $$)"}, {"$$$", "(PARA $$$)"}, {"$ ", "(PARA $)"}, {"$$ ", "(PARA $$)"}, {"$$$ ", "(PARA $$$)"}, })) } checkTcs(t, TestCases{ {"* abc", "(UL {(PARA abc)})"}, {"** abc", "(UL {(UL {(PARA abc)})})"}, {"*** abc", "(UL {(UL {(UL {(PARA abc)})})})"}, {"**** abc", "(UL {(UL {(UL {(UL {(PARA abc)})})})})"}, {"** abc\n**** def", "(UL {(UL {(PARA abc)(UL {(UL {(PARA def)})})})})"}, {"* abc\ndef", "(UL {(PARA abc)})(PARA def)"}, {"* abc\n def", "(UL {(PARA abc)})(PARA def)"}, {"* abc\n* def", "(UL {(PARA abc)} {(PARA def)})"}, {"* abc\n def", "(UL {(PARA abc SB def)})"}, {"* abc\n def", "(UL {(PARA abc SB def)})"}, {"* abc\n\ndef", "(UL {(PARA abc)})(PARA def)"}, {"* abc\n\n def", "(UL {(PARA abc)})(PARA def)"}, {"* abc\n\n def", "(UL {(PARA abc)(PARA def)})"}, {"* abc\n\n def", "(UL {(PARA abc)(PARA def)})"}, {"* abc\n** def", "(UL {(PARA abc)(UL {(PARA def)})})"}, {"* abc\n** def\n* ghi", "(UL {(PARA abc)(UL {(PARA def)})} {(PARA ghi)})"}, {"* abc\n\n def\n* ghi", "(UL {(PARA abc)(PARA def)} {(PARA ghi)})"}, {"* abc\n** def\n ghi\n jkl", "(UL {(PARA abc)(UL {(PARA def SB ghi)})(PARA jkl)})"}, // A list does not last beyond a region {":::\n# abc\n:::\n# def", "(SPAN (OL {(PARA abc)}))(OL {(PARA def)})"}, // A HRule creates a new list {"* abc\n---\n* def", "(UL {(PARA abc)})(HR)(UL {(PARA def)})"}, // Changing list type adds a new list {"* abc\n# def", "(UL {(PARA abc)})(OL {(PARA def)})"}, // Quotation lists may have empty items {">", "(QL {})"}, }) } func TestQuoteList(t *testing.T) { t.Parallel() checkTcs(t, TestCases{ {"> w1 w2", "(QL {(PARA w1 SP w2)})"}, {"> w1\n> w2", "(QL {(PARA w1 SB w2)})"}, {"> w1\n>\n>w2", "(QL {(PARA w1)} {})(PARA >w2)"}, }) } func TestEnumAfterPara(t *testing.T) { t.Parallel() checkTcs(t, TestCases{ {"abc\n* def", "(PARA abc)(UL {(PARA def)})"}, {"abc\n*def", "(PARA abc SB *def)"}, }) } func TestDefinition(t *testing.T) { t.Parallel() checkTcs(t, TestCases{ {";", "(PARA ;)"}, {"; ", "(PARA ;)"}, {"; abc", "(DL (DT abc))"}, {"; abc\ndef", "(DL (DT abc))(PARA def)"}, {"; abc\n def", "(DL (DT abc))(PARA def)"}, {"; abc\n def", "(DL (DT abc SB def))"}, {":", "(PARA :)"}, {": ", "(PARA :)"}, {": abc", "(PARA : SP abc)"}, {"; abc\n: def", "(DL (DT abc) (DD (PARA def)))"}, {"; abc\n: def\nghi", "(DL (DT abc) (DD (PARA def)))(PARA ghi)"}, {"; abc\n: def\n ghi", "(DL (DT abc) (DD (PARA def)))(PARA ghi)"}, {"; abc\n: def\n ghi", "(DL (DT abc) (DD (PARA def SB ghi)))"}, {"; abc\n: def\n\n ghi", "(DL (DT abc) (DD (PARA def)(PARA ghi)))"}, {"; abc\n:", "(DL (DT abc))(PARA :)"}, {"; abc\n: def\n: ghi", "(DL (DT abc) (DD (PARA def)) (DD (PARA ghi)))"}, {"; abc\n: def\n; ghi\n: jkl", "(DL (DT abc) (DD (PARA def)) (DT ghi) (DD (PARA jkl)))"}, }) } func TestTable(t *testing.T) { t.Parallel() checkTcs(t, TestCases{ {"|", "(TAB (TR))"}, {"||", "(TAB (TR (TD)))"}, {"| |", "(TAB (TR (TD)))"}, {"|a", "(TAB (TR (TD a)))"}, {"|a|", "(TAB (TR (TD a)))"}, {"|a| ", "(TAB (TR (TD a)(TD)))"}, {"|a|b", "(TAB (TR (TD a)(TD b)))"}, {"|a|b\n|c|d", "(TAB (TR (TD a)(TD b))(TR (TD c)(TD d)))"}, {"|%", ""}, {"|a|b\n|%---\n|c|d", "(TAB (TR (TD a)(TD b))(TR (TD c)(TD d)))"}, {"|a|b\n|c", "(TAB (TR (TD a)(TD b))(TR (TD c)(TD)))"}, }) } func TestTransclude(t *testing.T) { t.Parallel() checkTcs(t, TestCases{ {"{{{a}}}", "(TRANSCLUDE a)"}, {"{{{a}}}b", "(TRANSCLUDE a)[ATTR =b]"}, {"{{{a}}}}", "(TRANSCLUDE a)"}, {"{{{a\\}}}}", "(TRANSCLUDE a%5C%7D)"}, {"{{{a\\}}}}b", "(TRANSCLUDE a%5C%7D)[ATTR =b]"}, {"{{{a}}", "(PARA { (EMBED a))"}, {"{{{a}}}{go=b}", "(TRANSCLUDE a)[ATTR go=b]"}, }) } func TestBlockAttr(t *testing.T) { t.Parallel() checkTcs(t, TestCases{ {":::go\n:::", "(SPAN)[ATTR =go]"}, {":::go=\n:::", "(SPAN)[ATTR =go]"}, {":::{}\n:::", "(SPAN)"}, {":::{ }\n:::", "(SPAN)"}, {":::{.go}\n:::", "(SPAN)[ATTR class=go]"}, {":::{=go}\n:::", "(SPAN)[ATTR =go]"}, {":::{go}\n:::", "(SPAN)[ATTR go]"}, {":::{go=py}\n:::", "(SPAN)[ATTR go=py]"}, {":::{.go=py}\n:::", "(SPAN)"}, {":::{go=}\n:::", "(SPAN)[ATTR go]"}, {":::{.go=}\n:::", "(SPAN)"}, {":::{go py}\n:::", "(SPAN)[ATTR go py]"}, {":::{go\npy}\n:::", "(SPAN)[ATTR go py]"}, {":::{.go py}\n:::", "(SPAN)[ATTR class=go py]"}, {":::{go .py}\n:::", "(SPAN)[ATTR class=py go]"}, {":::{.go py=3}\n:::", "(SPAN)[ATTR class=go py=3]"}, {"::: { go } \n:::", "(SPAN)[ATTR go]"}, {"::: { .go } \n:::", "(SPAN)[ATTR class=go]"}, }) checkTcs(t, replace("\"", TestCases{ {":::{py=3}\n:::", "(SPAN)[ATTR py=3]"}, {":::{py=$2 3$}\n:::", "(SPAN)[ATTR py=$2 3$]"}, {":::{py=$2\\$3$}\n:::", "(SPAN)[ATTR py=2$3]"}, {":::{py=2$3}\n:::", "(SPAN)[ATTR py=2$3]"}, {":::{py=$2\n3$}\n:::", "(SPAN)[ATTR py=$2\n3$]"}, {":::{py=$2 3}\n:::", "(SPAN)"}, {":::{py=2 py=3}\n:::", "(SPAN)[ATTR py=$2 3$]"}, {":::{.go .py}\n:::", "(SPAN)[ATTR class=$go py$]"}, {":::{go go}\n:::", "(SPAN)[ATTR go]"}, {":::{=py =go}\n:::", "(SPAN)[ATTR =go]"}, })) } func TestInlineAttr(t *testing.T) { t.Parallel() checkTcs(t, TestCases{ {"::a::{}", "(PARA {: a})"}, {"::a::{ }", "(PARA {: a})"}, {"::a::{.go}", "(PARA {: a}[ATTR class=go])"}, {"::a::{=go}", "(PARA {: a}[ATTR =go])"}, {"::a::{go}", "(PARA {: a}[ATTR go])"}, {"::a::{go=py}", "(PARA {: a}[ATTR go=py])"}, {"::a::{.go=py}", "(PARA {: a} {.go=py})"}, {"::a::{go=}", "(PARA {: a}[ATTR go])"}, {"::a::{.go=}", "(PARA {: a} {.go=})"}, {"::a::{go py}", "(PARA {: a}[ATTR go py])"}, {"::a::{go\npy}", "(PARA {: a}[ATTR go py])"}, {"::a::{.go py}", "(PARA {: a}[ATTR class=go py])"}, {"::a::{go .py}", "(PARA {: a}[ATTR class=py go])"}, {"::a::{ \n go \n .py\n \n}", "(PARA {: a}[ATTR class=py go])"}, {"::a::{ \n go \n .py\n\n}", "(PARA {: a}[ATTR class=py go])"}, {"::a::{\ngo\n}", "(PARA {: a}[ATTR go])"}, }) checkTcs(t, replace("\"", TestCases{ {"::a::{py=3}", "(PARA {: a}[ATTR py=3])"}, {"::a::{py=$2 3$}", "(PARA {: a}[ATTR py=$2 3$])"}, {"::a::{py=$2\\$3$}", "(PARA {: a}[ATTR py=2$3])"}, {"::a::{py=2$3}", "(PARA {: a}[ATTR py=2$3])"}, {"::a::{py=$2\n3$}", "(PARA {: a}[ATTR py=$2\n3$])"}, {"::a::{py=$2 3}", "(PARA {: a} {py=$2 SP 3})"}, {"::a::{py=2 py=3}", "(PARA {: a}[ATTR py=$2 3$])"}, {"::a::{.go .py}", "(PARA {: a}[ATTR class=$go py$])"}, })) } func TestTemp(t *testing.T) { t.Parallel() checkTcs(t, TestCases{ {"", ""}, }) } // -------------------------------------------------------------------------- // TestVisitor serializes the abstract syntax tree to a string. type TestVisitor struct { sb strings.Builder } func (tv *TestVisitor) String() string { return tv.sb.String() } func (tv *TestVisitor) Visit(node ast.Node) ast.Visitor { switch n := node.(type) { case *ast.InlineSlice: tv.visitInlineSlice(n) case *ast.ParaNode: tv.sb.WriteString("(PARA") ast.Walk(tv, &n.Inlines) tv.sb.WriteByte(')') case *ast.VerbatimNode: code, ok := mapVerbatimKind[n.Kind] if !ok { panic(fmt.Sprintf("Unknown verbatim code %v", n.Kind)) } tv.sb.WriteString(code) if len(n.Content) > 0 { tv.sb.WriteByte('\n') tv.sb.Write(n.Content) } tv.sb.WriteByte(')') tv.visitAttributes(n.Attrs) case *ast.RegionNode: code, ok := mapRegionKind[n.Kind] if !ok { panic(fmt.Sprintf("Unknown region code %v", n.Kind)) } tv.sb.WriteString(code) if len(n.Blocks) > 0 { tv.sb.WriteByte(' ') ast.Walk(tv, &n.Blocks) } if len(n.Inlines) > 0 { tv.sb.WriteString(" (LINE") ast.Walk(tv, &n.Inlines) tv.sb.WriteByte(')') } tv.sb.WriteByte(')') tv.visitAttributes(n.Attrs) case *ast.HeadingNode: fmt.Fprintf(&tv.sb, "(H%d", n.Level) ast.Walk(tv, &n.Inlines) if n.Fragment != "" { tv.sb.WriteString(" #") tv.sb.WriteString(n.Fragment) } tv.sb.WriteByte(')') tv.visitAttributes(n.Attrs) case *ast.HRuleNode: tv.sb.WriteString("(HR)") tv.visitAttributes(n.Attrs) case *ast.NestedListNode: tv.sb.WriteString(mapNestedListKind[n.Kind]) for _, item := range n.Items { tv.sb.WriteString(" {") ast.WalkItemSlice(tv, item) tv.sb.WriteByte('}') } tv.sb.WriteByte(')') case *ast.DescriptionListNode: tv.sb.WriteString("(DL") for _, def := range n.Descriptions { tv.sb.WriteString(" (DT") ast.Walk(tv, &def.Term) tv.sb.WriteByte(')') for _, b := range def.Descriptions { tv.sb.WriteString(" (DD ") ast.WalkDescriptionSlice(tv, b) tv.sb.WriteByte(')') } } tv.sb.WriteByte(')') case *ast.TableNode: tv.sb.WriteString("(TAB") if len(n.Header) > 0 { tv.sb.WriteString(" (TR") for _, cell := range n.Header { tv.sb.WriteString(" (TH") tv.sb.WriteString(alignString[cell.Align]) ast.Walk(tv, &cell.Inlines) tv.sb.WriteString(")") } tv.sb.WriteString(")") } if len(n.Rows) > 0 { tv.sb.WriteString(" ") for _, row := range n.Rows { tv.sb.WriteString("(TR") for i, cell := range row { if i == 0 { tv.sb.WriteString(" ") } tv.sb.WriteString("(TD") tv.sb.WriteString(alignString[cell.Align]) ast.Walk(tv, &cell.Inlines) tv.sb.WriteString(")") } tv.sb.WriteString(")") } } tv.sb.WriteString(")") case *ast.TranscludeNode: fmt.Fprintf(&tv.sb, "(TRANSCLUDE %v)", n.Ref) tv.visitAttributes(n.Attrs) case *ast.BLOBNode: tv.sb.WriteString("(BLOB ") tv.sb.WriteString(n.Syntax) tv.sb.WriteString(")") case *ast.TextNode: tv.sb.WriteString(n.Text) case *ast.SpaceNode: if l := n.Count(); l == 1 { tv.sb.WriteString("SP") } else { fmt.Fprintf(&tv.sb, "SP%d", l) } case *ast.BreakNode: if n.Hard { tv.sb.WriteString("HB") } else { tv.sb.WriteString("SB") } case *ast.LinkNode: fmt.Fprintf(&tv.sb, "(LINK %v", n.Ref) ast.Walk(tv, &n.Inlines) tv.sb.WriteByte(')') tv.visitAttributes(n.Attrs) case *ast.EmbedRefNode: fmt.Fprintf(&tv.sb, "(EMBED %v", n.Ref) if len(n.Inlines) > 0 { ast.Walk(tv, &n.Inlines) } tv.sb.WriteByte(')') tv.visitAttributes(n.Attrs) case *ast.EmbedBLOBNode: panic("TODO: zmktest blob") case *ast.CiteNode: fmt.Fprintf(&tv.sb, "(CITE %s", n.Key) if len(n.Inlines) > 0 { ast.Walk(tv, &n.Inlines) } tv.sb.WriteByte(')') tv.visitAttributes(n.Attrs) case *ast.FootnoteNode: tv.sb.WriteString("(FN") ast.Walk(tv, &n.Inlines) tv.sb.WriteByte(')') tv.visitAttributes(n.Attrs) case *ast.MarkNode: tv.sb.WriteString("(MARK") if n.Mark != "" { tv.sb.WriteString(" \"") tv.sb.WriteString(n.Mark) tv.sb.WriteByte('"') } if n.Fragment != "" { tv.sb.WriteString(" #") tv.sb.WriteString(n.Fragment) } if len(n.Inlines) > 0 { ast.Walk(tv, &n.Inlines) } tv.sb.WriteByte(')') case *ast.FormatNode: fmt.Fprintf(&tv.sb, "{%c", mapFormatKind[n.Kind]) ast.Walk(tv, &n.Inlines) tv.sb.WriteByte('}') tv.visitAttributes(n.Attrs) case *ast.LiteralNode: code, ok := mapLiteralKind[n.Kind] if !ok { panic(fmt.Sprintf("No element for code %v", n.Kind)) } tv.sb.WriteByte('{') tv.sb.WriteRune(code) if len(n.Content) > 0 { tv.sb.WriteByte(' ') tv.sb.Write(n.Content) } tv.sb.WriteByte('}') tv.visitAttributes(n.Attrs) default: return tv } return nil } var mapVerbatimKind = map[ast.VerbatimKind]string{ ast.VerbatimZettel: "(ZETTEL", ast.VerbatimProg: "(PROG", ast.VerbatimEval: "(EVAL", ast.VerbatimMath: "(MATH", ast.VerbatimComment: "(COMMENT", } var mapRegionKind = map[ast.RegionKind]string{ ast.RegionSpan: "(SPAN", ast.RegionQuote: "(QUOTE", ast.RegionVerse: "(VERSE", } var mapNestedListKind = map[ast.NestedListKind]string{ ast.NestedListOrdered: "(OL", ast.NestedListUnordered: "(UL", ast.NestedListQuote: "(QL", } var alignString = map[ast.Alignment]string{ ast.AlignDefault: "", ast.AlignLeft: "l", ast.AlignCenter: "c", ast.AlignRight: "r", } var mapFormatKind = map[ast.FormatKind]rune{ ast.FormatEmph: '_', ast.FormatStrong: '*', ast.FormatInsert: '>', ast.FormatDelete: '~', ast.FormatSuper: '^', ast.FormatSub: ',', ast.FormatQuote: '"', ast.FormatSpan: ':', } var mapLiteralKind = map[ast.LiteralKind]rune{ ast.LiteralZettel: '@', ast.LiteralProg: '`', ast.LiteralInput: '\'', ast.LiteralOutput: '=', ast.LiteralComment: '%', ast.LiteralMath: '$', } func (tv *TestVisitor) visitInlineSlice(is *ast.InlineSlice) { for _, in := range *is { tv.sb.WriteByte(' ') ast.Walk(tv, in) } } func (tv *TestVisitor) visitAttributes(a attrs.Attributes) { if a.IsEmpty() { return } tv.sb.WriteString("[ATTR") for _, k := range a.Keys() { tv.sb.WriteByte(' ') tv.sb.WriteString(k) v := a[k] if len(v) > 0 { tv.sb.WriteByte('=') if quoteString(v) { tv.sb.WriteByte('"') tv.sb.WriteString(v) tv.sb.WriteByte('"') } else { tv.sb.WriteString(v) } } } tv.sb.WriteByte(']') } func quoteString(s string) bool { for _, ch := range s { if ch <= ' ' { return true } } return false } |
Added query/parser.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 | //----------------------------------------------------------------------------- // Copyright (c) 2022-present Detlef Stern // // This file is part of Zettelstore. // // Zettelstore is licensed under the latest version of the EUPL (European Union // Public License). Please see file LICENSE.txt for your rights and obligations // under this license. //----------------------------------------------------------------------------- package query import ( "strconv" "zettelstore.de/z/domain/meta" "zettelstore.de/z/input" ) // Parse the query specification and return a Query object. func Parse(spec string) (q *Query) { return q.Parse(spec) } // Parse the query string and update the Query object. func (q *Query) Parse(spec string) *Query { state := parserState{ inp: input.NewInput([]byte(spec)), } q = state.parse(q) if q != nil { for len(q.terms) > 1 && q.terms[len(q.terms)-1].isEmpty() { q.terms = q.terms[:len(q.terms)-1] } } return q } type parserState struct { inp *input.Input } func (ps *parserState) mustStop() bool { return ps.inp.Ch == input.EOS } func (ps *parserState) acceptSingleKw(s string) bool { if ps.inp.Accept(s) && (ps.isSpace() || ps.mustStop()) { return true } return false } func (ps *parserState) acceptKwArgs(s string) bool { if ps.inp.Accept(s) && ps.isSpace() { ps.skipSpace() return true } return false } const ( actionSeparatorChar = '|' existOperatorChar = '?' searchOperatorNotChar = '!' searchOperatorHasChar = ':' searchOperatorPrefixChar = '>' searchOperatorSuffixChar = '<' searchOperatorMatchChar = '~' kwLimit = "LIMIT" kwOffset = "OFFSET" kwOr = "OR" kwOrder = "ORDER" kwPick = "PICK" kwRandom = "RANDOM" kwReverse = "REVERSE" ) func (ps *parserState) parse(q *Query) *Query { inp := ps.inp for { ps.skipSpace() if ps.mustStop() { break } pos := inp.Pos if ps.acceptSingleKw(kwOr) { q = createIfNeeded(q) if !q.terms[len(q.terms)-1].isEmpty() { q.terms = append(q.terms, conjTerms{}) } continue } inp.SetPos(pos) if ps.acceptSingleKw(kwRandom) { q = createIfNeeded(q) if len(q.order) == 0 && q.pick <= 0 { q.order = []sortOrder{{"", false}} } continue } inp.SetPos(pos) if ps.acceptKwArgs(kwPick) { if s, ok := ps.parsePick(q); ok { q = s continue } } inp.SetPos(pos) if ps.acceptKwArgs(kwOrder) { if s, ok := ps.parseOrder(q); ok { q = s continue } } inp.SetPos(pos) if ps.acceptKwArgs(kwOffset) { if s, ok := ps.parseOffset(q); ok { q = s continue } } inp.SetPos(pos) if ps.acceptKwArgs(kwLimit) { if s, ok := ps.parseLimit(q); ok { q = s continue } } inp.SetPos(pos) if isActionSep(inp.Ch) { q = ps.parseActions(q) break } q = ps.parseText(q) } return q } func (ps *parserState) parsePick(q *Query) (*Query, bool) { num, ok := ps.scanPosInt() if !ok { return q, false } q = createIfNeeded(q) if q.pick == 0 || q.pick >= num { q.pick = num } if len(q.order) == 1 && q.order[0].isRandom() { q.order = nil } return q, true } func (ps *parserState) parseOrder(q *Query) (*Query, bool) { reverse := false if ps.acceptKwArgs(kwReverse) { reverse = true } word := ps.scanWord() if len(word) == 0 { return q, false } if sWord := string(word); meta.KeyIsValid(sWord) { q = createIfNeeded(q) if len(q.order) == 1 && q.order[0].isRandom() { q.order = nil } q.order = append(q.order, sortOrder{sWord, reverse}) return q, true } return q, false } func (ps *parserState) parseOffset(q *Query) (*Query, bool) { num, ok := ps.scanPosInt() if !ok { return q, false } q = createIfNeeded(q) if q.offset <= num { q.offset = num } return q, true } func (ps *parserState) parseLimit(q *Query) (*Query, bool) { num, ok := ps.scanPosInt() if !ok { return q, false } q = createIfNeeded(q) if q.limit == 0 || q.limit >= num { q.limit = num } return q, true } func (ps *parserState) parseActions(q *Query) *Query { ps.inp.Next() var words []string for { ps.skipSpace() word := ps.scanWord() if len(word) == 0 { break } words = append(words, string(word)) } if len(words) > 0 { q = createIfNeeded(q) q.actions = words } return q } func (ps *parserState) parseText(q *Query) *Query { inp := ps.inp pos := inp.Pos op, hasOp := ps.scanSearchOp() if hasOp && (op == cmpExist || op == cmpNotExist) { inp.SetPos(pos) hasOp = false } text, key := ps.scanSearchTextOrKey(hasOp) if len(key) > 0 { // Assert: hasOp == false op, hasOp = ps.scanSearchOp() // Assert hasOp == true if op == cmpExist || op == cmpNotExist { if ps.isSpace() || isActionSep(inp.Ch) || ps.mustStop() { return q.addKey(string(key), op) } ps.inp.SetPos(pos) hasOp = false text = ps.scanWord() key = nil } else { text = ps.scanWord() } } else if len(text) == 0 { // Only an empty search operation is found -> ignore it return q } q = createIfNeeded(q) if hasOp { if key == nil { q.addSearch(expValue{string(text), op}) } else { last := len(q.terms) - 1 if q.terms[last].mvals == nil { q.terms[last].mvals = expMetaValues{string(key): {expValue{string(text), op}}} } else { sKey := string(key) q.terms[last].mvals[sKey] = append(q.terms[last].mvals[sKey], expValue{string(text), op}) } } } else { // Assert key == nil q.addSearch(expValue{string(text), cmpMatch}) } return q } func (ps *parserState) scanSearchTextOrKey(hasOp bool) ([]byte, []byte) { inp := ps.inp pos := inp.Pos allowKey := !hasOp for !ps.isSpace() && !isActionSep(inp.Ch) && !ps.mustStop() { if allowKey { switch inp.Ch { case searchOperatorNotChar, existOperatorChar, searchOperatorHasChar, searchOperatorPrefixChar, searchOperatorSuffixChar, searchOperatorMatchChar: allowKey = false if key := inp.Src[pos:inp.Pos]; meta.KeyIsValid(string(key)) { return nil, key } } } inp.Next() } return inp.Src[pos:inp.Pos], nil } func (ps *parserState) scanWord() []byte { inp := ps.inp pos := inp.Pos for !ps.isSpace() && !isActionSep(inp.Ch) && !ps.mustStop() { inp.Next() } return inp.Src[pos:inp.Pos] } func (ps *parserState) scanPosInt() (int, bool) { inp := ps.inp ch := inp.Ch if ch == '0' { ch = inp.Next() if isSpace(ch) || isActionSep(inp.Ch) || ps.mustStop() { return 0, true } return 0, false } word := ps.scanWord() if len(word) == 0 { return 0, false } uval, err := strconv.ParseUint(string(word), 10, 63) if err != nil { return 0, false } return int(uval), true } func (ps *parserState) scanSearchOp() (compareOp, bool) { inp := ps.inp ch := inp.Ch negate := false if ch == searchOperatorNotChar { ch = inp.Next() negate = true } op := cmpUnknown switch ch { case existOperatorChar: inp.Next() op = cmpExist case searchOperatorHasChar: inp.Next() op = cmpHas case searchOperatorSuffixChar: inp.Next() op = cmpSuffix case searchOperatorPrefixChar: inp.Next() op = cmpPrefix case searchOperatorMatchChar: inp.Next() op = cmpMatch default: if negate { return cmpNoMatch, true } return cmpUnknown, false } if negate { return op.negate(), true } return op, true } func (ps *parserState) isSpace() bool { return isSpace(ps.inp.Ch) } func isSpace(ch rune) bool { switch ch { case input.EOS: return false case ' ', '\t', '\n', '\r': return true } return input.IsSpace(ch) } func (ps *parserState) skipSpace() { for ps.isSpace() { ps.inp.Next() } } func isActionSep(ch rune) bool { return ch == actionSeparatorChar } |
Added query/parser_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 | //----------------------------------------------------------------------------- // Copyright (c) 2022-present Detlef Stern // // This file is part of Zettelstore. // // Zettelstore is licensed under the latest version of the EUPL (European Union // Public License). Please see file LICENSE.txt for your rights and obligations // under this license. //----------------------------------------------------------------------------- package query_test import ( "testing" "zettelstore.de/z/query" ) func TestParser(t *testing.T) { t.Parallel() testcases := []struct { spec string exp string }{ {"?", "?"}, {"!?", "!?"}, {"?a", "?a"}, {"!?a", "!?a"}, {"key?", "key?"}, {"key!?", "key!?"}, {"b key?", "key? b"}, {"b key!?", "key!? b"}, {"key?a", "key?a"}, {"key!?a", "key!?a"}, {"", ""}, {"!", ""}, {":", ""}, {"!:", ""}, {">", ""}, {"!>", ""}, {"<", ""}, {"!<", ""}, {"~", ""}, {"!~", ""}, {`a`, `a`}, {`!a`, `!a`}, {`:a`, `:a`}, {`!:a`, `!:a`}, {`>a`, `>a`}, {`!>a`, `!>a`}, {`<a`, `<a`}, {`!<a`, `!<a`}, {`~a`, `a`}, {`!~a`, `!a`}, {`key:`, `key:`}, {`key!:`, `key!:`}, {`key>`, `key>`}, {`key!>`, `key!>`}, {`key<`, `key<`}, {`key!<`, `key!<`}, {`key~`, `key~`}, {`key!~`, `key!~`}, {`key:a`, `key:a`}, {`key!:a`, `key!:a`}, {`key>a`, `key>a`}, {`key!>a`, `key!>a`}, {`key<a`, `key<a`}, {`key!<a`, `key!<a`}, {`key~a`, `key~a`}, {`key!~a`, `key!~a`}, {`key1:a key2:b`, `key1:a key2:b`}, {`key1: key2:b`, `key1: key2:b`}, {"word key:a", "key:a word"}, {`PICK 3`, `PICK 3`}, {`PICK 9 PICK 11`, `PICK 9`}, {`PICK 5 RANDOM`, `PICK 5`}, {`RANDOM PICK 7`, `PICK 7`}, {`RANDOM`, `RANDOM`}, {`RANDOM a`, `a RANDOM`}, {`a RANDOM`, `a RANDOM`}, {`RANDOM RANDOM a`, `a RANDOM`}, {`RANDOMRANDOM a`, `RANDOMRANDOM a`}, {`a RANDOMRANDOM`, `a RANDOMRANDOM`}, {`ORDER`, `ORDER`}, {"ORDER a b", "b ORDER a"}, {"a ORDER", "a ORDER"}, {"ORDER %", "ORDER %"}, {"ORDER a %", "% ORDER a"}, {"ORDER REVERSE", "ORDER REVERSE"}, {"ORDER REVERSE a b", "b ORDER REVERSE a"}, {"a RANDOM ORDER b", "a ORDER b"}, {"a ORDER b RANDOM", "a ORDER b"}, {"OFFSET", "OFFSET"}, {"OFFSET a", "OFFSET a"}, {"OFFSET 10 a", "a OFFSET 10"}, {"OFFSET 01 a", "OFFSET 01 a"}, {"OFFSET 0 a", "a"}, {"a OFFSET 0", "a"}, {"OFFSET 4 OFFSET 8", "OFFSET 8"}, {"OFFSET 8 OFFSET 4", "OFFSET 8"}, {"LIMIT", "LIMIT"}, {"LIMIT a", "LIMIT a"}, {"LIMIT 10 a", "a LIMIT 10"}, {"LIMIT 01 a", "LIMIT 01 a"}, {"LIMIT 0 a", "a"}, {"a LIMIT 0", "a"}, {"LIMIT 4 LIMIT 8", "LIMIT 4"}, {"LIMIT 8 LIMIT 4", "LIMIT 4"}, {"OR", ""}, {"OR OR", ""}, {"a OR", "a"}, {"OR b", "b"}, {"OR a OR", "a"}, {"a OR b", "a OR b"}, {"|", ""}, {" | RANDOM", "| RANDOM"}, {"| RANDOM", "| RANDOM"}, {"a|a b ", "a | a b"}, } for i, tc := range testcases { got := query.Parse(tc.spec).String() if tc.exp != got { t.Errorf("%d: Parse(%q) does not yield %q, but got %q", i, tc.spec, tc.exp, got) continue } gotReparse := query.Parse(got).String() if gotReparse != got { t.Errorf("%d: Parse(%q) does not yield itself, but %q", i, got, gotReparse) } gotPipe := query.Parse(got + "|").String() if gotPipe != got { t.Errorf("%d: Parse(%q) does not yield itself, but %q", i, got+"|", gotReparse) } } } |
Added query/print.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 | //----------------------------------------------------------------------------- // Copyright (c) 2020-present Detlef Stern // // This file is part of Zettelstore. // // Zettelstore is licensed under the latest version of the EUPL (European Union // Public License). Please see file LICENSE.txt for your rights and obligations // under this license. //----------------------------------------------------------------------------- package query import ( "io" "strconv" "strings" "zettelstore.de/c/api" "zettelstore.de/c/maps" ) var op2string = map[compareOp]string{ cmpExist: api.ExistOperator, cmpNotExist: api.ExistNotOperator, cmpHas: api.SearchOperatorHas, cmpHasNot: api.SearchOperatorHasNot, cmpPrefix: api.SearchOperatorPrefix, cmpNoPrefix: api.SearchOperatorNoPrefix, cmpSuffix: api.SearchOperatorSuffix, cmpNoSuffix: api.SearchOperatorNoSuffix, cmpMatch: api.SearchOperatorMatch, cmpNoMatch: api.SearchOperatorNoMatch, } func (q *Query) String() string { var sb strings.Builder q.Print(&sb) return sb.String() } // Print the query in a parseable form. func (q *Query) Print(w io.Writer) { if q == nil { return } env := printEnv{w: w} for i, term := range q.terms { if i > 0 { env.writeString(" OR") } for _, name := range maps.Keys(term.keys) { env.printSpace() env.writeString(name) if op := term.keys[name]; op == cmpExist || op == cmpNotExist { env.writeString(op2string[op]) } else { env.writeString(api.ExistOperator) env.printSpace() env.writeString(name) env.writeString(api.ExistNotOperator) } } for _, name := range maps.Keys(term.mvals) { env.printExprValues(name, term.mvals[name]) } if len(term.search) > 0 { env.printExprValues("", term.search) } } env.printPosInt(kwPick, q.pick) env.printOrder(q.order) env.printPosInt(kwOffset, q.offset) env.printPosInt(kwLimit, q.limit) env.printActions(q.actions) } type printEnv struct { w io.Writer space bool } var bsSpace = []byte{' '} func (pe *printEnv) printSpace() { if pe.space { pe.w.Write(bsSpace) return } pe.space = true } func (pe *printEnv) write(ch byte) { pe.w.Write([]byte{ch}) } func (pe *printEnv) writeString(s string) { io.WriteString(pe.w, s) } func (pe *printEnv) printExprValues(key string, values []expValue) { for _, val := range values { pe.printSpace() pe.writeString(key) switch op := val.op; op { case cmpMatch: // An empty key signals a full-text search. Since "~" is the default op in this case, // it can be ignored. Therefore, print only "~" if there is a key. if key != "" { pe.writeString(api.SearchOperatorMatch) } case cmpNoMatch: // An empty key signals a full-text search. Since "!" is the shortcut for "!~", // it can be ignored. Therefore, print only "!~" if there is a key. if key == "" { pe.writeString(api.SearchOperatorNot) } else { pe.writeString(api.SearchOperatorNoMatch) } default: if s, found := op2string[op]; found { pe.writeString(s) } else { pe.writeString("%" + strconv.Itoa(int(op))) } } if s := val.value; s != "" { pe.writeString(s) } } } func (q *Query) Human() string { var sb strings.Builder q.PrintHuman(&sb) return sb.String() } // PrintHuman the query to a writer in a human readable form. func (q *Query) PrintHuman(w io.Writer) { if q == nil { return } env := printEnv{w: w} for i, term := range q.terms { if i > 0 { env.writeString(" OR ") env.space = false } for _, name := range maps.Keys(term.keys) { if env.space { env.writeString(" AND ") } env.writeString(name) switch term.keys[name] { case cmpExist: env.writeString(" EXIST") case cmpNotExist: env.writeString(" NOT EXIST") default: env.writeString(" IS SCHRÖDINGER'S CAT") } env.space = true } for _, name := range maps.Keys(term.mvals) { if env.space { env.writeString(" AND ") } env.writeString(name) env.printHumanSelectExprValues(term.mvals[name]) env.space = true } if len(term.search) > 0 { if env.space { env.writeString(" ") } env.writeString("ANY") env.printHumanSelectExprValues(term.search) env.space = true } } env.printPosInt(kwPick, q.pick) env.printOrder(q.order) env.printPosInt(kwOffset, q.offset) env.printPosInt(kwLimit, q.limit) env.printActions(q.actions) } func (pe *printEnv) printHumanSelectExprValues(values []expValue) { if len(values) == 0 { pe.writeString(" MATCH ANY") return } for j, val := range values { if j > 0 { pe.writeString(" AND") } switch val.op { case cmpHas: pe.writeString(" HAS ") case cmpHasNot: pe.writeString(" HAS NOT ") case cmpPrefix: pe.writeString(" PREFIX ") case cmpNoPrefix: pe.writeString(" NOT PREFIX ") case cmpSuffix: pe.writeString(" SUFFIX ") case cmpNoSuffix: pe.writeString(" NOT SUFFIX ") case cmpMatch: pe.writeString(" MATCH ") case cmpNoMatch: pe.writeString(" NOT MATCH ") default: pe.writeString(" MaTcH ") } if val.value == "" { pe.writeString("NOTHING") } else { pe.writeString(val.value) } } } func (pe *printEnv) printOrder(order []sortOrder) { for _, o := range order { if o.isRandom() { pe.printSpace() pe.writeString(kwRandom) continue } else if o.key == api.KeyID && o.descending { continue } pe.printSpace() pe.writeString(kwOrder) if o.descending { pe.printSpace() pe.writeString(kwReverse) } pe.printSpace() pe.writeString(o.key) } } func (pe *printEnv) printPosInt(key string, val int) { if val > 0 { pe.printSpace() pe.writeString(key) pe.writeString(" ") pe.writeString(strconv.Itoa(val)) } } func (pe *printEnv) printActions(words []string) { if len(words) > 0 { pe.printSpace() pe.write(actionSeparatorChar) for _, word := range words { pe.printSpace() pe.writeString(word) } } } |
Added query/query.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 | //----------------------------------------------------------------------------- // Copyright (c) 2020-present Detlef Stern // // This file is part of Zettelstore. // // Zettelstore is licensed under the latest version of the EUPL (European Union // Public License). Please see file LICENSE.txt for your rights and obligations // under this license. //----------------------------------------------------------------------------- // Package query provides a query for zettel. package query import ( "math/rand" "sort" "zettelstore.de/z/domain/id" "zettelstore.de/z/domain/meta" ) // Searcher is used to select zettel identifier based on search criteria. type Searcher interface { // Select all zettel that contains the given exact word. // The word must be normalized through Unicode NKFD, trimmed and not empty. SearchEqual(word string) id.Set // Select all zettel that have a word with the given prefix. // The prefix must be normalized through Unicode NKFD, trimmed and not empty. SearchPrefix(prefix string) id.Set // Select all zettel that have a word with the given suffix. // The suffix must be normalized through Unicode NKFD, trimmed and not empty. SearchSuffix(suffix string) id.Set // Select all zettel that contains the given string. // The string must be normalized through Unicode NKFD, trimmed and not empty. SearchContains(s string) id.Set } // Query specifies a mechanism for querying zettel. type Query struct { // Fields to be used for selecting preMatch MetaMatchFunc // Match that must be true terms []conjTerms // Allow to create predictable randomness seed int pick int // Randomly pick elements, <= 0: no pick // Fields to be used for sorting order []sortOrder offset int // <= 0: no offset limit int // <= 0: no limit // Execute specification actions []string } // Compiled is a compiled query, to be used in a Box type Compiled struct { PreMatch MetaMatchFunc // Precondition for Match and Retrieve Terms []CompiledTerm } // MetaMatchFunc is a function determine whethe some metadata should be selected or not. type MetaMatchFunc func(*meta.Meta) bool func matchAlways(*meta.Meta) bool { return true } func matchNever(*meta.Meta) bool { return false } // CompiledTerm is the preprocessed sequence of conjugated search terms. type CompiledTerm struct { Match MetaMatchFunc // Match on metadata Retrieve RetrievePredicate // Retrieve from full-text search } // RetrievePredicate returns true, if the given Zid is contained in the (full-text) search. type RetrievePredicate func(id.Zid) bool type keyExistMap map[string]compareOp type expMetaValues map[string][]expValue type conjTerms struct { keys keyExistMap mvals expMetaValues // Expected values for a meta datum search []expValue // Search string } func (ct *conjTerms) isEmpty() bool { return len(ct.keys) == 0 && len(ct.mvals) == 0 && len(ct.search) == 0 } func (ct *conjTerms) addKey(key string, op compareOp) { if ct.keys == nil { ct.keys = map[string]compareOp{key: op} return } if prevOp, found := ct.keys[key]; found { if prevOp != op { ct.keys[key] = cmpUnknown } return } ct.keys[key] = op } func (ct *conjTerms) addSearch(val expValue) { ct.search = append(ct.search, val) } type sortOrder struct { key string descending bool } func (so *sortOrder) isRandom() bool { return so.key == "" } func createIfNeeded(q *Query) *Query { if q == nil { return &Query{ terms: []conjTerms{{}}, } } return q } // Clone the query value. func (q *Query) Clone() *Query { if q == nil { return nil } c := new(Query) c.preMatch = q.preMatch c.terms = make([]conjTerms, len(q.terms)) for i, term := range q.terms { if len(term.keys) > 0 { c.terms[i].keys = make(keyExistMap, len(term.keys)) for k, v := range term.keys { c.terms[i].keys[k] = v } } // if len(c.mvals) > 0 { c.terms[i].mvals = make(expMetaValues, len(term.mvals)) for k, v := range term.mvals { c.terms[i].mvals[k] = v } // } if len(term.search) > 0 { c.terms[i].search = append([]expValue{}, term.search...) } } c.seed = q.seed c.pick = q.pick if len(q.order) > 0 { c.order = append([]sortOrder{}, q.order...) } c.offset = q.offset c.limit = q.limit c.actions = q.actions return c } type compareOp uint8 const ( cmpUnknown compareOp = iota cmpExist cmpNotExist cmpHas cmpHasNot cmpPrefix cmpNoPrefix cmpSuffix cmpNoSuffix cmpMatch cmpNoMatch ) var negateMap = map[compareOp]compareOp{ cmpUnknown: cmpUnknown, cmpExist: cmpNotExist, cmpHas: cmpHasNot, cmpHasNot: cmpHas, cmpPrefix: cmpNoPrefix, cmpNoPrefix: cmpPrefix, cmpSuffix: cmpNoSuffix, cmpNoSuffix: cmpSuffix, cmpMatch: cmpNoMatch, cmpNoMatch: cmpMatch, } func (op compareOp) negate() compareOp { return negateMap[op] } var negativeMap = map[compareOp]bool{ cmpNotExist: true, cmpHasNot: true, cmpNoPrefix: true, cmpNoSuffix: true, cmpNoMatch: true, } func (op compareOp) isNegated() bool { return negativeMap[op] } type expValue struct { value string op compareOp } func (q *Query) addSearch(val expValue) { q.terms[len(q.terms)-1].addSearch(val) } func (q *Query) addKey(key string, op compareOp) *Query { q = createIfNeeded(q) q.terms[len(q.terms)-1].addKey(key, op) return q } // SetPreMatch sets the pre-selection predicate. func (q *Query) SetPreMatch(preMatch MetaMatchFunc) *Query { q = createIfNeeded(q) if q.preMatch != nil { panic("search PreMatch already set") } q.preMatch = preMatch return q } // SetSeed sets a seed value. func (q *Query) SetSeed(seed int) *Query { q = createIfNeeded(q) q.seed = seed return q } // GetSeed returns the seed value if one was set. func (q *Query) GetSeed() (int, bool) { if q == nil { return 0, false } return q.seed, q.seed > 0 } // SetLimit sets the given limit of the query object. func (q *Query) SetLimit(limit int) *Query { q = createIfNeeded(q) if limit < 0 { limit = 0 } q.limit = limit return q } // GetLimit returns the current offset value. func (q *Query) GetLimit() int { if q == nil { return 0 } return q.limit } // Actions returns the slice of action specifications func (q *Query) Actions() []string { if q == nil { return nil } return q.actions } // RemoveActions will remove the action part of a query. func (q *Query) RemoveActions() { if q != nil { q.actions = nil } } // EnrichNeeded returns true, if the query references a metadata key that // is calculated via metadata enrichments. func (q *Query) EnrichNeeded() bool { if q == nil { return false } if len(q.actions) > 0 { // Unknown, what an action will use. Example: RSS needs api.KeyPublished. return true } for _, term := range q.terms { for key := range term.keys { if meta.IsProperty(key) { return true } } for key := range term.mvals { if meta.IsProperty(key) { return true } } } for _, o := range q.order { if meta.IsProperty(o.key) { return true } } return false } // RetrieveAndCompile queries the search index and returns a predicate // for its results and returns a matching predicate. func (q *Query) RetrieveAndCompile(searcher Searcher) Compiled { if q == nil { return Compiled{ PreMatch: matchAlways, Terms: []CompiledTerm{{ Match: matchAlways, Retrieve: alwaysIncluded, }}} } q = q.Clone() preMatch := q.preMatch if preMatch == nil { preMatch = matchAlways } result := Compiled{PreMatch: preMatch} for _, term := range q.terms { cTerm := term.retrievAndCompileTerm(searcher) if cTerm.Retrieve == nil { if cTerm.Match == nil { // no restriction on match/retrieve -> all will match return Compiled{ PreMatch: preMatch, Terms: []CompiledTerm{{ Match: matchAlways, Retrieve: alwaysIncluded, }}} } cTerm.Retrieve = alwaysIncluded } if cTerm.Match == nil { cTerm.Match = matchAlways } result.Terms = append(result.Terms, cTerm) } return result } func (ct *conjTerms) retrievAndCompileTerm(searcher Searcher) CompiledTerm { match := ct.compileMeta() // Match might add some searches var pred RetrievePredicate if searcher != nil { pred = ct.retrieveIndex(searcher) } return CompiledTerm{Match: match, Retrieve: pred} } // retrieveIndex and return a predicate to ask for results. func (ct *conjTerms) retrieveIndex(searcher Searcher) RetrievePredicate { if len(ct.search) == 0 { return nil } normCalls, plainCalls, negCalls := prepareRetrieveCalls(searcher, ct.search) if hasConflictingCalls(normCalls, plainCalls, negCalls) { return neverIncluded } positives := retrievePositives(normCalls, plainCalls) if positives == nil { // No positive search for words, must contain only words for a negative search. // Otherwise len(search) == 0 (see above) negatives := retrieveNegatives(negCalls) return func(zid id.Zid) bool { return !negatives.Contains(zid) } } if len(positives) == 0 { // Positive search didn't found anything. We can omit the negative search. return neverIncluded } if len(negCalls) == 0 { // Positive search found something, but there is no negative search. return positives.Contains } negatives := retrieveNegatives(negCalls) return func(zid id.Zid) bool { return positives.Contains(zid) && !negatives.Contains(zid) } } // AfterSearch applies all terms to the metadata list that was searched. // // This includes sorting, offset, limit, and picking. func (q *Query) AfterSearch(metaList []*meta.Meta) []*meta.Meta { if len(metaList) == 0 { return metaList } if q == nil { sort.Slice(metaList, func(i, j int) bool { return metaList[i].Zid > metaList[j].Zid }) return metaList } metaList = q.doPick(metaList) if len(q.order) == 0 { if q.pick <= 0 { sort.Slice(metaList, func(i, j int) bool { return metaList[i].Zid > metaList[j].Zid }) } } else if q.order[0].isRandom() { metaList = q.sortRandomly(metaList) } else { sort.Slice(metaList, createSortFunc(q.order, metaList)) } if q.offset > 0 { if q.offset > len(metaList) { return nil } metaList = metaList[q.offset:] } return q.Limit(metaList) } func (q *Query) setSeed() { if q != nil && q.seed <= 0 { q.seed = int(rand.Intn(10000) + 1) } } func (q *Query) doPick(metaList []*meta.Meta) []*meta.Meta { pick := q.pick if pick <= 0 { return metaList } if limit := q.limit; limit > 0 && limit < pick { pick = limit } if pick >= len(metaList) && len(q.order) == 0 { return q.sortRandomly(metaList) } return q.doPickN(metaList, pick) } func (q *Query) doPickN(metaList []*meta.Meta, pick int) []*meta.Meta { q.setSeed() rnd := rand.New(rand.NewSource(int64(q.seed))) result := make([]*meta.Meta, pick) for i := 0; i < pick; i++ { n := rnd.Intn(pick - i) result[i] = metaList[n] metaList[n] = metaList[len(metaList)-i-1] metaList[len(metaList)-i-1] = nil } return result } func (q *Query) sortRandomly(metaList []*meta.Meta) []*meta.Meta { // Optimization: RANDOM LIMIT n, where n < len(metaList) is essentially a PICK n. if limit := q.limit; limit > 0 && limit < len(metaList) { return q.doPickN(metaList, limit) } sort.Slice(metaList, func(i, j int) bool { return metaList[i].Zid > metaList[j].Zid }) q.setSeed() rnd := rand.New(rand.NewSource(int64(q.seed))) rnd.Shuffle( len(metaList), func(i, j int) { metaList[i], metaList[j] = metaList[j], metaList[i] }, ) return metaList } // Limit returns only s.GetLimit() elements of the given list. func (q *Query) Limit(metaList []*meta.Meta) []*meta.Meta { if q == nil { return metaList } if limit := q.limit; limit > 0 && limit < len(metaList) { return metaList[:limit] } return metaList } |
Added query/retrieve.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 | //----------------------------------------------------------------------------- // Copyright (c) 2021-present Detlef Stern // // This file is part of Zettelstore. // // Zettelstore is licensed under the latest version of the EUPL (European Union // Public License). Please see file LICENSE.txt for your rights and obligations // under this license. //----------------------------------------------------------------------------- package query // This file contains helper functions to search within the index. import ( "fmt" "strings" "zettelstore.de/z/domain/id" "zettelstore.de/z/strfun" ) type searchOp struct { s string op compareOp } type searchFunc func(string) id.Set type searchCallMap map[searchOp]searchFunc var cmpPred = map[compareOp]func(string, string) bool{ cmpHas: func(s, t string) bool { return s == t }, cmpPrefix: strings.HasPrefix, cmpSuffix: strings.HasSuffix, cmpMatch: strings.Contains, } func (scm searchCallMap) addSearch(s string, op compareOp, sf searchFunc) { pred := cmpPred[op] for k := range scm { if op == cmpMatch { if strings.Contains(k.s, s) { return } if strings.Contains(s, k.s) { delete(scm, k) break } } if k.op != op { continue } if pred(k.s, s) { return } if pred(s, k.s) { delete(scm, k) } } scm[searchOp{s: s, op: op}] = sf } func alwaysIncluded(id.Zid) bool { return true } func neverIncluded(id.Zid) bool { return false } func prepareRetrieveCalls(searcher Searcher, search []expValue) (normCalls, plainCalls, negCalls searchCallMap) { normCalls = make(searchCallMap, len(search)) negCalls = make(searchCallMap, len(search)) for _, val := range search { for _, word := range strfun.NormalizeWords(val.value) { if cmpOp := val.op; cmpOp.isNegated() { cmpOp = cmpOp.negate() negCalls.addSearch(word, cmpOp, getSearchFunc(searcher, cmpOp)) } else { normCalls.addSearch(word, cmpOp, getSearchFunc(searcher, cmpOp)) } } } plainCalls = make(searchCallMap, len(search)) for _, val := range search { word := strings.ToLower(strings.TrimSpace(val.value)) if cmpOp := val.op; cmpOp.isNegated() { cmpOp = cmpOp.negate() negCalls.addSearch(word, cmpOp, getSearchFunc(searcher, cmpOp)) } else { plainCalls.addSearch(word, cmpOp, getSearchFunc(searcher, cmpOp)) } } return normCalls, plainCalls, negCalls } func hasConflictingCalls(normCalls, plainCalls, negCalls searchCallMap) bool { for val := range negCalls { if _, found := normCalls[val]; found { return true } if _, found := plainCalls[val]; found { return true } } return false } func retrievePositives(normCalls, plainCalls searchCallMap) id.Set { if isSuperset(normCalls, plainCalls) { var normResult id.Set for c, sf := range normCalls { normResult = normResult.IntersectOrSet(sf(c.s)) } return normResult } type searchResults map[searchOp]id.Set var cache searchResults var plainResult id.Set for c, sf := range plainCalls { result := sf(c.s) if _, found := normCalls[c]; found { if cache == nil { cache = make(searchResults) } cache[c] = result } plainResult = plainResult.IntersectOrSet(result) } var normResult id.Set for c, sf := range normCalls { if cache != nil { if result, found := cache[c]; found { normResult = normResult.IntersectOrSet(result) continue } } normResult = normResult.IntersectOrSet(sf(c.s)) } return normResult.Add(plainResult) } func isSuperset(normCalls, plainCalls searchCallMap) bool { for c := range plainCalls { if _, found := normCalls[c]; !found { return false } } return true } func retrieveNegatives(negCalls searchCallMap) id.Set { var negatives id.Set for val, sf := range negCalls { negatives = negatives.Add(sf(val.s)) } return negatives } func getSearchFunc(searcher Searcher, op compareOp) searchFunc { switch op { case cmpHas: return searcher.SearchEqual case cmpPrefix: return searcher.SearchPrefix case cmpSuffix: return searcher.SearchSuffix case cmpMatch: return searcher.SearchContains default: panic(fmt.Sprintf("Unexpected value of comparison operation: %v", op)) } } |
Added query/select.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 | //----------------------------------------------------------------------------- // Copyright (c) 2020-present Detlef Stern // // This file is part of Zettelstore. // // Zettelstore is licensed under the latest version of the EUPL (European Union // Public License). Please see file LICENSE.txt for your rights and obligations // under this license. //----------------------------------------------------------------------------- package query import ( "fmt" "strings" "unicode/utf8" "zettelstore.de/z/domain/meta" "zettelstore.de/z/encoder/textenc" "zettelstore.de/z/parser" "zettelstore.de/z/strfun" ) type matchValueFunc func(value string) bool func matchValueNever(string) bool { return false } type matchSpec struct { key string match matchValueFunc } // compileMeta calculates a selection func based on the given select criteria. func (ct *conjTerms) compileMeta() MetaMatchFunc { for key := range ct.mvals { // All queried keys must exist ct.addKey(key, cmpExist) } for _, op := range ct.keys { if op != cmpExist && op != cmpNotExist { return matchNever } } posSpecs, negSpecs := ct.createSelectSpecs() if len(posSpecs) > 0 || len(negSpecs) > 0 || len(ct.keys) > 0 { return makeSearchMetaMatchFunc(posSpecs, negSpecs, ct.keys) } return nil } func (ct *conjTerms) createSelectSpecs() (posSpecs, negSpecs []matchSpec) { posSpecs = make([]matchSpec, 0, len(ct.mvals)) negSpecs = make([]matchSpec, 0, len(ct.mvals)) for key, values := range ct.mvals { if !meta.KeyIsValid(key) { continue } posMatch, negMatch := createPosNegMatchFunc(key, values, ct.addSearch) if posMatch != nil { posSpecs = append(posSpecs, matchSpec{key, posMatch}) } if negMatch != nil { negSpecs = append(negSpecs, matchSpec{key, negMatch}) } } return posSpecs, negSpecs } type addSearchFunc func(val expValue) func noAddSearch(expValue) { /* Just does nothing, for negated queries */ } func createPosNegMatchFunc(key string, values []expValue, addSearch addSearchFunc) (posMatch, negMatch matchValueFunc) { posValues := make([]expValue, 0, len(values)) negValues := make([]expValue, 0, len(values)) for _, val := range values { if val.op.isNegated() { negValues = append(negValues, val) } else { posValues = append(posValues, val) } } if meta.IsProperty(key) { // Properties are not stored in the Zettelstore and in the search index. addSearch = noAddSearch } return createMatchFunc(key, posValues, addSearch), createMatchFunc(key, negValues, addSearch) } func createMatchFunc(key string, values []expValue, addSearch addSearchFunc) matchValueFunc { if len(values) == 0 { return nil } switch meta.Type(key) { case meta.TypeCredential: return matchValueNever case meta.TypeID, meta.TypeTimestamp: // ID and timestamp use the same layout return createMatchIDFunc(values, addSearch) case meta.TypeIDSet: return createMatchIDSetFunc(values, addSearch) case meta.TypeTagSet: return createMatchTagSetFunc(values, addSearch) case meta.TypeWord: return createMatchWordOrStringFunc(values, addSearch) case meta.TypeWordSet: return createMatchWordSetFunc(values, addSearch) case meta.TypeZettelmarkup: return createMatchZmkFunc(values, addSearch) } return createMatchWordOrStringFunc(values, addSearch) } func createMatchIDFunc(values []expValue, addSearch addSearchFunc) matchValueFunc { preds := valuesToStringPredicates(values, addSearch) return func(value string) bool { for _, pred := range preds { if !pred(value) { return false } } return true } } func createMatchIDSetFunc(values []expValue, addSearch addSearchFunc) matchValueFunc { predList := valuesToStringSetPredicates(preprocessSet(values), addSearch) return func(value string) bool { ids := meta.ListFromValue(value) for _, preds := range predList { for _, pred := range preds { if !pred(ids) { return false } } } return true } } func createMatchTagSetFunc(values []expValue, addSearch addSearchFunc) matchValueFunc { predList := valuesToStringSetPredicates(processTagSet(preprocessSet(sliceToLower(values))), addSearch) return func(value string) bool { tags := meta.ListFromValue(value) // Remove leading '#' from each tag for i, tag := range tags { tags[i] = meta.CleanTag(tag) } for _, preds := range predList { for _, pred := range preds { if !pred(tags) { return false } } } return true } } func processTagSet(valueSet [][]expValue) [][]expValue { result := make([][]expValue, len(valueSet)) for i, values := range valueSet { tags := make([]expValue, len(values)) for j, val := range values { if tval := val.value; tval != "" && tval[0] == '#' { tval = meta.CleanTag(tval) tags[j] = expValue{value: tval, op: val.op} } else { tags[j] = expValue{value: tval, op: val.op} } } result[i] = tags } return result } func createMatchWordOrStringFunc(values []expValue, addSearch addSearchFunc) matchValueFunc { preds := valuesToStringPredicates(sliceToLower(values), addSearch) return func(value string) bool { value = strings.ToLower(value) for _, pred := range preds { if !pred(value) { return false } } return true } } func createMatchWordSetFunc(values []expValue, addSearch addSearchFunc) matchValueFunc { predsList := valuesToStringSetPredicates(preprocessSet(sliceToLower(values)), addSearch) return func(value string) bool { words := meta.ListFromValue(value) for _, preds := range predsList { for _, pred := range preds { if !pred(words) { return false } } } return true } } func sliceToLower(sl []expValue) []expValue { result := make([]expValue, 0, len(sl)) for _, s := range sl { result = append(result, expValue{ value: strings.ToLower(s.value), op: s.op, }) } return result } func createMatchZmkFunc(values []expValue, addSearch addSearchFunc) matchValueFunc { normPreds := make([]stringPredicate, 0, len(values)) negPreds := make([]stringPredicate, 0, len(values)) for _, v := range values { for _, word := range strfun.NormalizeWords(v.value) { if cmpOp := v.op; cmpOp.isNegated() { cmpOp = cmpOp.negate() negPreds = append(negPreds, createStringCompareFunc(word, cmpOp)) } else { normPreds = append(normPreds, createStringCompareFunc(word, cmpOp)) addSearch(expValue{word, cmpOp}) // addSearch only for positive selections } } } return func(metaValue string) bool { temp := strings.Fields(zmk2text(metaValue)) values := make([]string, 0, len(temp)) for _, s := range temp { values = append(values, strfun.NormalizeWords(s)...) } for _, pred := range normPreds { if noneOf(pred, values) { return false } } for _, pred := range negPreds { for _, val := range values { if pred(val) { return false } } } return true } } func noneOf(pred stringPredicate, values []string) bool { for _, value := range values { if pred(value) { return false } } return true } func zmk2text(zmk string) string { isASCII, hasUpper, needParse := true, false, false for i := 0; i < len(zmk); i++ { ch := zmk[i] if ch >= utf8.RuneSelf { isASCII = false break } hasUpper = hasUpper || ('A' <= ch && ch <= 'Z') needParse = needParse || !(('A' <= ch && ch <= 'Z') || ('a' <= ch && ch <= 'z') || ('0' <= ch && ch <= '9') || ch == ' ') } if isASCII { if !needParse { if !hasUpper { return zmk } return strings.ToLower(zmk) } } is := parser.ParseMetadata(zmk) var sb strings.Builder if _, err := textenc.Create().WriteInlines(&sb, &is); err != nil { return strings.ToLower(zmk) } return strings.ToLower(sb.String()) } func preprocessSet(set []expValue) [][]expValue { result := make([][]expValue, 0, len(set)) for _, elem := range set { splitElems := strings.Split(elem.value, ",") valueElems := make([]expValue, 0, len(splitElems)) for _, se := range splitElems { e := strings.TrimSpace(se) if len(e) > 0 { valueElems = append(valueElems, expValue{value: e, op: elem.op}) } } if len(valueElems) > 0 { result = append(result, valueElems) } } return result } type stringPredicate func(string) bool func valuesToStringPredicates(values []expValue, addSearch addSearchFunc) []stringPredicate { result := make([]stringPredicate, len(values)) for i, v := range values { if !v.op.isNegated() { addSearch(v) // addSearch only for positive selections } result[i] = createStringCompareFunc(v.value, v.op) } return result } func createStringCompareFunc(cmpVal string, cmpOp compareOp) stringPredicate { switch cmpOp { case cmpHas: return func(metaVal string) bool { return metaVal == cmpVal } case cmpHasNot: return func(metaVal string) bool { return metaVal != cmpVal } case cmpPrefix: return func(metaVal string) bool { return strings.HasPrefix(metaVal, cmpVal) } case cmpNoPrefix: return func(metaVal string) bool { return !strings.HasPrefix(metaVal, cmpVal) } case cmpSuffix: return func(metaVal string) bool { return strings.HasSuffix(metaVal, cmpVal) } case cmpNoSuffix: return func(metaVal string) bool { return !strings.HasSuffix(metaVal, cmpVal) } case cmpMatch: return func(metaVal string) bool { return strings.Contains(metaVal, cmpVal) } case cmpNoMatch: return func(metaVal string) bool { return !strings.Contains(metaVal, cmpVal) } default: panic(fmt.Sprintf("Unknown compare operation %d with value %q", cmpOp, cmpVal)) } } type stringSetPredicate func(value []string) bool func valuesToStringSetPredicates(values [][]expValue, addSearch addSearchFunc) [][]stringSetPredicate { result := make([][]stringSetPredicate, len(values)) for i, val := range values { elemPreds := make([]stringSetPredicate, len(val)) for j, v := range val { opVal := v.value // loop variable is used in closure --> save needed value switch v.op { case cmpHas: addSearch(v) // addSearch only for positive selections elemPreds[j] = makeStringSetPredicate(opVal, stringEqual, true) case cmpHasNot: elemPreds[j] = makeStringSetPredicate(opVal, stringEqual, false) case cmpPrefix: addSearch(v) elemPreds[j] = makeStringSetPredicate(opVal, strings.HasPrefix, true) case cmpNoPrefix: elemPreds[j] = makeStringSetPredicate(opVal, strings.HasPrefix, false) case cmpSuffix: addSearch(v) elemPreds[j] = makeStringSetPredicate(opVal, strings.HasSuffix, true) case cmpNoSuffix: elemPreds[j] = makeStringSetPredicate(opVal, strings.HasSuffix, false) case cmpMatch: addSearch(v) elemPreds[j] = makeStringSetPredicate(opVal, strings.Contains, true) case cmpNoMatch: elemPreds[j] = makeStringSetPredicate(opVal, strings.Contains, false) default: panic(fmt.Sprintf("Unknown compare operation %d with value %q", v.op, opVal)) } } result[i] = elemPreds } return result } func stringEqual(val1, val2 string) bool { return val1 == val2 } type compareStringFunc func(val1, val2 string) bool func makeStringSetPredicate(neededValue string, compare compareStringFunc, foundResult bool) stringSetPredicate { return func(metaVals []string) bool { for _, metaVal := range metaVals { if compare(metaVal, neededValue) { return foundResult } } return !foundResult } } func makeSearchMetaMatchFunc(posSpecs, negSpecs []matchSpec, kem keyExistMap) MetaMatchFunc { // Optimize: no specs --> just check kwhether key exists if len(posSpecs) == 0 && len(negSpecs) == 0 { if len(kem) == 0 { return nil } return func(m *meta.Meta) bool { return matchMetaKeyExists(m, kem) } } // Optimize: only negative or only positive matching if len(posSpecs) == 0 { return func(m *meta.Meta) bool { return matchMetaKeyExists(m, kem) && matchMetaSpecs(m, negSpecs) } } if len(negSpecs) == 0 { return func(m *meta.Meta) bool { return matchMetaKeyExists(m, kem) && matchMetaSpecs(m, posSpecs) } } return func(m *meta.Meta) bool { return matchMetaKeyExists(m, kem) && matchMetaSpecs(m, posSpecs) && matchMetaSpecs(m, negSpecs) } } func matchMetaKeyExists(m *meta.Meta, kem keyExistMap) bool { for key, op := range kem { _, found := m.Get(key) if found != (op == cmpExist) { return false } } return true } func matchMetaSpecs(m *meta.Meta, specs []matchSpec) bool { for _, s := range specs { if value := m.GetDefault(s.key, ""); !s.match(value) { return false } } return true } |
Added query/select_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 | //----------------------------------------------------------------------------- // Copyright (c) 2021-present Detlef Stern // // This file is part of Zettelstore. // // Zettelstore is licensed under the latest version of the EUPL (European Union // Public License). Please see file LICENSE.txt for your rights and obligations // under this license. //----------------------------------------------------------------------------- package query_test import ( "testing" "zettelstore.de/c/api" "zettelstore.de/z/domain/id" "zettelstore.de/z/domain/meta" "zettelstore.de/z/query" ) func TestMatchZidNegate(t *testing.T) { q := query.Parse(api.KeyID + api.SearchOperatorHasNot + string(api.ZidVersion) + " " + api.KeyID + api.SearchOperatorHasNot + string(api.ZidLicense)) compiled := q.RetrieveAndCompile(nil) testCases := []struct { zid api.ZettelID exp bool }{ {api.ZidVersion, false}, {api.ZidLicense, false}, {api.ZidAuthors, true}, } for i, tc := range testCases { m := meta.New(id.MustParse(tc.zid)) if compiled.Terms[0].Match(m) != tc.exp { if tc.exp { t.Errorf("%d: meta %v must match %q", i, m.Zid, q) } else { t.Errorf("%d: meta %v must not match %q", i, m.Zid, q) } } } } |
Added query/sorter.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 | //----------------------------------------------------------------------------- // Copyright (c) 2020-present Detlef Stern // // This file is part of Zettelstore. // // Zettelstore is licensed under the latest version of the EUPL (European Union // Public License). Please see file LICENSE.txt for your rights and obligations // under this license. //----------------------------------------------------------------------------- package query import ( "strconv" "zettelstore.de/c/api" "zettelstore.de/z/domain/meta" ) type sortFunc func(i, j int) bool func createSortFunc(order []sortOrder, ml []*meta.Meta) sortFunc { hasID := false sortFuncs := make([]sortFunc, 0, len(order)+1) for _, o := range order { sortFuncs = append(sortFuncs, createOneSortFunc(o.key, o.descending, ml)) if o.key == api.KeyID { hasID = true break } } if !hasID { sortFuncs = append(sortFuncs, func(i, j int) bool { return ml[i].Zid > ml[j].Zid }) } // return sortFuncs[0] if len(sortFuncs) == 1 { return sortFuncs[0] } return func(i, j int) bool { for _, sf := range sortFuncs { if sf(i, j) { return true } if sf(j, i) { return false } } return false } } func createOneSortFunc(key string, descending bool, ml []*meta.Meta) sortFunc { keyType := meta.Type(key) if key == api.KeyID || keyType == meta.TypeCredential { if descending { return func(i, j int) bool { return ml[i].Zid > ml[j].Zid } } return func(i, j int) bool { return ml[i].Zid < ml[j].Zid } } if keyType == meta.TypeNumber { return createSortNumberFunc(ml, key, descending) } return createSortStringFunc(ml, key, descending) } func createSortNumberFunc(ml []*meta.Meta, key string, descending bool) sortFunc { if descending { return func(i, j int) bool { iVal, iOk := getNum(ml[i], key) jVal, jOk := getNum(ml[j], key) return (iOk && (!jOk || iVal > jVal)) || !jOk } } return func(i, j int) bool { iVal, iOk := getNum(ml[i], key) jVal, jOk := getNum(ml[j], key) return (iOk && (!jOk || iVal < jVal)) || !jOk } } func createSortStringFunc(ml []*meta.Meta, key string, descending bool) sortFunc { if descending { return func(i, j int) bool { iVal, iOk := ml[i].Get(key) jVal, jOk := ml[j].Get(key) return (iOk && (!jOk || iVal > jVal)) || !jOk } } return func(i, j int) bool { iVal, iOk := ml[i].Get(key) jVal, jOk := ml[j].Get(key) return (iOk && (!jOk || iVal < jVal)) || !jOk } } func getNum(m *meta.Meta, key string) (int64, bool) { if s, ok := m.Get(key); ok { if i, err := strconv.ParseInt(s, 10, 64); err == nil { return i, true } } return 0, false } |
Added strfun/escape.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-present Detlef Stern // // This file is part of Zettelstore. // // Zettelstore is licensed under the latest version of the EUPL (European Union // Public License). Please see file LICENSE.txt for your rights and obligations // under this license. //----------------------------------------------------------------------------- package strfun import "io" var ( escQuot = []byte(""") // longer than """, but often requested in standards escAmp = []byte("&") escApos = []byte("apos;") // longer than "'", but sometimes requested in tests escLt = []byte("<") escGt = []byte(">") escTab = []byte("	") escNull = []byte("\uFFFD") ) // XMLEscape writes the string to the given writer, where every rune that has a special // meaning in XML is escaped. func XMLEscape(w io.Writer, s string) { var esc []byte last := 0 for i, ch := range s { switch ch { case '\000': esc = escNull case '"': esc = escQuot case '\'': esc = escApos case '&': esc = escAmp case '<': esc = escLt case '>': esc = escGt case '\t': esc = escTab default: continue } io.WriteString(w, s[last:i]) w.Write(esc) last = i + 1 } io.WriteString(w, s[last:]) } |
Added strfun/set.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 | //----------------------------------------------------------------------------- // Copyright (c) 2022-present Detlef Stern // // This file is part of Zettelstore. // // Zettelstore is licensed under the latest version of the EUPL (European Union // Public License). Please see file LICENSE.txt for your rights and obligations // under this license. //----------------------------------------------------------------------------- package strfun // Set ist a set of strings. type Set map[string]struct{} // NewSet creates a new set from the given values. func NewSet(values ...string) Set { s := make(Set, len(values)) for _, v := range values { s.Set(v) } return s } // Set adds the given string to the set. func (s Set) Set(v string) { s[v] = struct{}{} } // Has returns true, if given value is in set. func (s Set) Has(v string) bool { _, found := s[v]; return found } |
Changes to strfun/slugify.go.
1 2 3 4 5 6 7 8 | //----------------------------------------------------------------------------- // Copyright (c) 2020-present Detlef Stern // // This file is part of Zettelstore. // // Zettelstore is licensed under the latest version of the EUPL (European Union // Public License). Please see file LICENSE.txt for your rights and obligations // under this license. | < < < | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 | //----------------------------------------------------------------------------- // Copyright (c) 2020-present Detlef Stern // // This file is part of Zettelstore. // // Zettelstore is licensed under the latest version of the EUPL (European Union // Public License). Please see file LICENSE.txt for your rights and obligations // under this license. //----------------------------------------------------------------------------- package strfun import ( "strings" "unicode" |
︙ | ︙ |
Changes to strfun/slugify_test.go.
1 2 3 4 5 6 7 8 | //----------------------------------------------------------------------------- // Copyright (c) 2020-present Detlef Stern // // This file is part of Zettelstore. // // Zettelstore is licensed under the latest version of the EUPL (European Union // Public License). Please see file LICENSE.txt for your rights and obligations // under this license. | < < < | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 | //----------------------------------------------------------------------------- // Copyright (c) 2020-present Detlef Stern // // This file is part of Zettelstore. // // Zettelstore is licensed under the latest version of the EUPL (European Union // Public License). Please see file LICENSE.txt for your rights and obligations // under this license. //----------------------------------------------------------------------------- package strfun_test import ( "testing" |
︙ | ︙ | |||
34 35 36 37 38 39 40 | if got := strfun.Slugify(test.in); got != test.exp { t.Errorf("%q: %q != %q", test.in, got, test.exp) } } } func eqStringSlide(got, exp []string) bool { | < < < | | | | | 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 | if got := strfun.Slugify(test.in); got != test.exp { t.Errorf("%q: %q != %q", test.in, got, test.exp) } } } func eqStringSlide(got, exp []string) bool { if len(got) != len(exp) { return false } for i, g := range got { if g != exp[i] { return false } } return true } func TestNormalizeWord(t *testing.T) { t.Parallel() tests := []struct { in string exp []string }{ {"", []string{}}, {" ", []string{}}, {"ˋ", []string{}}, // No single diacritic char, such as U+02CB {"simple test", []string{"simple", "test"}}, {"I'm a go developer", []string{"i", "m", "a", "go", "developer"}}, {"-!->simple test<-!-", []string{"simple", "test"}}, {"äöüÄÖÜß", []string{"aouaouß"}}, {"\"aèf", []string{"aef"}}, {"a#b", []string{"a", "b"}}, {"*", []string{}}, {"123", []string{"123"}}, {"1²3", []string{"123"}}, {"Period.", []string{"period"}}, {" WORD NUMBER ", []string{"word", "number"}}, } for _, test := range tests { if got := strfun.NormalizeWords(test.in); !eqStringSlide(got, test.exp) { t.Errorf("%q: %q != %q", test.in, got, test.exp) } } } |
Changes to strfun/strfun.go.
1 2 3 4 5 6 7 8 | //----------------------------------------------------------------------------- // Copyright (c) 2021-present Detlef Stern // // This file is part of Zettelstore. // // Zettelstore is licensed under the latest version of the EUPL (European Union // Public License). Please see file LICENSE.txt for your rights and obligations // under this license. | < < < < | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 | //----------------------------------------------------------------------------- // Copyright (c) 2021-present Detlef Stern // // This file is part of Zettelstore. // // Zettelstore is licensed under the latest version of the EUPL (European Union // Public License). Please see file LICENSE.txt for your rights and obligations // under this license. //----------------------------------------------------------------------------- // Package strfun provides some string functions. package strfun import ( "strings" "unicode/utf8" ) // Length returns the number of runes in the given string. func Length(s string) int { return utf8.RuneCountInString(s) } |
︙ | ︙ | |||
39 40 41 42 43 44 45 | runes[maxLen-1] = '\u2025' } var sb strings.Builder for _, r := range runes { sb.WriteRune(r) } | | < < < < < < < < | 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 | runes[maxLen-1] = '\u2025' } var sb strings.Builder for _, r := range runes { sb.WriteRune(r) } for i := 0; i < maxLen-len(runes); i++ { sb.WriteRune(pad) } return sb.String() } // SplitLines splits the given string into a list of lines. func SplitLines(s string) []string { return strings.FieldsFunc(s, func(r rune) bool { return r == '\n' || r == '\r' }) } |
Changes to strfun/strfun_test.go.
1 2 3 4 5 6 7 8 | //----------------------------------------------------------------------------- // Copyright (c) 2021-present Detlef Stern // // This file is part of Zettelstore. // // Zettelstore is licensed under the latest version of the EUPL (European Union // Public License). Please see file LICENSE.txt for your rights and obligations // under this license. | < < < | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 | //----------------------------------------------------------------------------- // Copyright (c) 2021-present Detlef Stern // // This file is part of Zettelstore. // // Zettelstore is licensed under the latest version of the EUPL (European Union // Public License). Please see file LICENSE.txt for your rights and obligations // under this license. //----------------------------------------------------------------------------- package strfun_test import ( "testing" |
︙ | ︙ |
Added template/LICENSE.
> > > > > > > > > > > > > > > > > > > | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 | 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. |
Added template/mustache.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 581 582 583 584 585 586 587 588 589 590 591 592 593 594 595 596 597 598 599 600 601 602 603 604 605 606 607 608 609 610 611 612 613 614 615 616 617 618 619 620 621 622 623 624 625 626 627 628 629 630 631 632 633 634 635 636 637 638 639 640 641 642 643 644 645 646 647 648 649 650 651 652 653 654 655 656 657 658 659 660 661 662 663 664 665 666 667 668 669 670 671 672 673 674 675 676 677 678 679 680 681 682 683 684 685 686 687 688 689 690 691 692 693 694 695 696 697 698 699 700 701 702 703 704 705 706 707 708 709 710 711 712 | //----------------------------------------------------------------------------- // Copyright (c) 2020-present Detlef Stern // // This file is part of Zettelstore. // // Zettelstore is licensed under the latest version of the EUPL (European Union // Public License). Please see file LICENSE.txt for your rights and obligations // under this license. // // This file was derived from previous work: // - https://github.com/hoisie/mustache (License: MIT) // Copyright (c) 2009 Michael Hoisie // - https://github.com/cbroglie/mustache (a fork from above code) // Starting with commit [f9b4cbf] // Does not have an explicit copyright and obviously continues with // above MIT license. // The license text is included in the same directory where this file is // located. See file LICENSE. //----------------------------------------------------------------------------- // Package template implements the Mustache templating language. package template import ( "fmt" "io" "reflect" "regexp" "strings" "codeberg.org/t73fde/sxhtml" "codeberg.org/t73fde/sxpf" ) // Node represents a node in the parse tree. // It is either a Tag or a textNode. type node interface { node() } // Tag represents the different mustache tag types. // // Not all methods apply to all kinds of tags. Restrictions, if any, are noted // in the documentation for each method. Use the Type method to find out the // type of tag before calling type-specific methods. Calling a method // inappropriate to the type of tag causes a run time panic. type Tag interface { node // Type returns the type of the tag. Type() TagType // Name returns the name of the tag. Name() string // Tags returns any child tags. It panics for tag types which cannot contain // child tags (i.e. variable tags). Tags() []Tag } // A TagType represents the specific type of mustache tag that a Tag // represents. The zero TagType is not a valid type. type TagType uint // Defines representing the possible Tag types const ( Invalid TagType = iota Variable Section InvertedSection Partial ) type varNode struct { name string raw bool } func (*varNode) node() {} func (*varNode) Type() TagType { return Variable } func (e *varNode) Name() string { return e.name } func (*varNode) Tags() []Tag { panic("mustache: Tags on Variable type") } type sectionNode struct { name string inverted bool startline int nodes []node } func (*sectionNode) node() {} func (e *sectionNode) Type() TagType { if e.inverted { return InvertedSection } return Section } func (e *sectionNode) Name() string { return e.name } func (e *sectionNode) Tags() []Tag { return extractTags(e.nodes) } type partialNode struct { name string indent string prov PartialProvider } func (*partialNode) node() {} func (*partialNode) Type() TagType { return Partial } func (e *partialNode) Name() string { return e.name } func (*partialNode) Tags() []Tag { return nil } type textNode struct { text []byte } func (*textNode) node() {} // Template represents a compiled mustache template type Template struct { data string otag string ctag string p int curline int nodes []node partial PartialProvider errmiss bool // Error when variable is not found? } type parseError struct { line int message string } func (p parseError) Error() string { return fmt.Sprintf("line %d: %s", p.line, p.message) } // Tags returns the mustache tags for the given template func (tmpl *Template) Tags() []Tag { return extractTags(tmpl.nodes) } func extractTags(nodes []node) []Tag { tags := make([]Tag, 0, len(nodes)) for _, elem := range nodes { switch elem := elem.(type) { case *varNode: tags = append(tags, elem) case *sectionNode: tags = append(tags, elem) case *partialNode: tags = append(tags, elem) } } return tags } func (tmpl *Template) readString(s string) (string, error) { newlines := 0 for i := tmpl.p; ; i++ { //are we at the end of the string? if i+len(s) > len(tmpl.data) { return tmpl.data[tmpl.p:], io.EOF } if tmpl.data[i] == '\n' { newlines++ } if tmpl.data[i] != s[0] { continue } match := true for j := 1; j < len(s); j++ { if s[j] != tmpl.data[i+j] { match = false break } } if match { e := i + len(s) text := tmpl.data[tmpl.p:e] tmpl.p = e tmpl.curline += newlines return text, nil } } } type textReadingResult struct { text string padding string mayStandalone bool } func (tmpl *Template) readText() (*textReadingResult, error) { pPrev := tmpl.p text, err := tmpl.readString(tmpl.otag) if err == io.EOF { return &textReadingResult{ text: text, padding: "", mayStandalone: false, }, err } i := tmpl.p - len(tmpl.otag) for ; i > pPrev; i-- { if tmpl.data[i-1] != ' ' && tmpl.data[i-1] != '\t' { break } } if i == 0 || tmpl.data[i-1] == '\n' { return &textReadingResult{ text: tmpl.data[pPrev:i], padding: tmpl.data[i : tmpl.p-len(tmpl.otag)], mayStandalone: true, }, nil } return &textReadingResult{ text: tmpl.data[pPrev : tmpl.p-len(tmpl.otag)], padding: "", mayStandalone: false, }, nil } type tagReadingResult struct { tag string standalone bool } var skipWhitespaceTagTypes = map[byte]struct{}{ '#': {}, '^': {}, '/': {}, '<': {}, '>': {}, '=': {}, '!': {}, } func (tmpl *Template) readTag(mayStandalone bool) (*tagReadingResult, error) { var text string var err error if tmpl.p < len(tmpl.data) && tmpl.data[tmpl.p] == '{' { text, err = tmpl.readString("}" + tmpl.ctag) } else { text, err = tmpl.readString(tmpl.ctag) } if err == io.EOF { //put the remaining text in a block return nil, parseError{tmpl.curline, "unmatched open tag"} } text = text[:len(text)-len(tmpl.ctag)] //trim the close tag off the text tag := strings.TrimSpace(text) if tag == "" { return nil, parseError{tmpl.curline, "empty tag"} } eow := tmpl.p for i := tmpl.p; i < len(tmpl.data); i++ { if !(tmpl.data[i] == ' ' || tmpl.data[i] == '\t') { eow = i break } } standalone := tmpl.skipWhitespaceTag(tag, eow, mayStandalone) return &tagReadingResult{ tag: tag, standalone: standalone, }, nil } func (tmpl *Template) skipWhitespaceTag(tag string, eow int, mayStandalone bool) bool { if !mayStandalone { return true } // Skip all whitespaces apeared after these types of tags until end of line if // the line only contains a tag and whitespaces. if _, ok := skipWhitespaceTagTypes[tag[0]]; !ok { return false } if eow == len(tmpl.data) { tmpl.p = eow return true } if eow < len(tmpl.data) && tmpl.data[eow] == '\n' { tmpl.p = eow + 1 tmpl.curline++ return true } if eow+1 < len(tmpl.data) && tmpl.data[eow] == '\r' && tmpl.data[eow+1] == '\n' { tmpl.p = eow + 2 tmpl.curline++ return true } return false } func (tmpl *Template) parsePartial(name, indent string) *partialNode { return &partialNode{ name: name, indent: indent, prov: tmpl.partial, } } func (tmpl *Template) parseSection(section *sectionNode) error { for { textResult, err := tmpl.readText() text := textResult.text padding := textResult.padding mayStandalone := textResult.mayStandalone if err == io.EOF { //put the remaining text in a block return parseError{section.startline, "Section " + section.name + " has no closing tag"} } // put text into an item section.nodes = append(section.nodes, &textNode{[]byte(text)}) tagResult, err := tmpl.readTag(mayStandalone) if err != nil { return err } if !tagResult.standalone { section.nodes = append(section.nodes, &textNode{[]byte(padding)}) } tag := tagResult.tag switch tag[0] { case '!': //ignore comment case '#', '^': name := strings.TrimSpace(tag[1:]) sn := §ionNode{name, tag[0] == '^', tmpl.curline, []node{}} err = tmpl.parseSection(sn) if err != nil { return err } section.nodes = append(section.nodes, sn) case '/': name := strings.TrimSpace(tag[1:]) if name != section.name { return parseError{tmpl.curline, "interleaved closing tag: " + name} } return nil case '>': name := strings.TrimSpace(tag[1:]) partial := tmpl.parsePartial(name, textResult.padding) section.nodes = append(section.nodes, partial) case '=': if tag[len(tag)-1] != '=' { return parseError{tmpl.curline, "Invalid meta tag"} } tag = strings.TrimSpace(tag[1 : len(tag)-1]) newtags := strings.SplitN(tag, " ", 2) if len(newtags) == 2 { tmpl.otag = newtags[0] tmpl.ctag = newtags[1] } case '{': if tag[len(tag)-1] == '}' { //use a raw tag name := strings.TrimSpace(tag[1 : len(tag)-1]) section.nodes = append(section.nodes, &varNode{name, true}) } case '&': name := strings.TrimSpace(tag[1:]) section.nodes = append(section.nodes, &varNode{name, true}) default: section.nodes = append(section.nodes, &varNode{tag, false}) } } } func (tmpl *Template) parse() error { for { textResult, err := tmpl.readText() text := textResult.text padding := textResult.padding mayStandalone := textResult.mayStandalone if err == io.EOF { //put the remaining text in a block tmpl.nodes = append(tmpl.nodes, &textNode{[]byte(text)}) return nil } // put text into an item tmpl.nodes = append(tmpl.nodes, &textNode{[]byte(text)}) tagResult, err := tmpl.readTag(mayStandalone) if err != nil { return err } if !tagResult.standalone { tmpl.nodes = append(tmpl.nodes, &textNode{[]byte(padding)}) } tag := tagResult.tag switch tag[0] { case '!': //ignore comment case '#', '^': name := strings.TrimSpace(tag[1:]) sn := §ionNode{name, tag[0] == '^', tmpl.curline, []node{}} err = tmpl.parseSection(sn) if err != nil { return err } tmpl.nodes = append(tmpl.nodes, sn) case '/': return parseError{tmpl.curline, "unmatched close tag"} case '>': name := strings.TrimSpace(tag[1:]) partial := tmpl.parsePartial(name, textResult.padding) tmpl.nodes = append(tmpl.nodes, partial) case '=': if tag[len(tag)-1] != '=' { return parseError{tmpl.curline, "Invalid meta tag"} } tag = strings.TrimSpace(tag[1 : len(tag)-1]) newtags := strings.SplitN(tag, " ", 2) if len(newtags) == 2 { tmpl.otag = newtags[0] tmpl.ctag = newtags[1] } case '{': //use a raw tag if tag[len(tag)-1] == '}' { name := strings.TrimSpace(tag[1 : len(tag)-1]) tmpl.nodes = append(tmpl.nodes, &varNode{name, true}) } case '&': name := strings.TrimSpace(tag[1:]) tmpl.nodes = append(tmpl.nodes, &varNode{name, true}) default: tmpl.nodes = append(tmpl.nodes, &varNode{tag, false}) } } } // Evaluate interfaces and pointers looking for a value that can look up the // name, via a struct field, method, or map key, and return the result of the // lookup. func lookup(stack []reflect.Value, name string, errMissing bool) (reflect.Value, error) { // dot notation if pos := strings.IndexByte(name, '.'); pos > 0 && pos < len(name)-1 { v, err := lookup(stack, name[:pos], errMissing) if err != nil { return v, err } return lookup([]reflect.Value{v}, name[pos+1:], errMissing) } for i := len(stack) - 1; i >= 0; i-- { if val, ok := lookupValue(stack[i], name); ok { return val, nil } } if errMissing { return reflect.Value{}, fmt.Errorf("missing variable %q", name) } return reflect.Value{}, nil } func lookupValue(v reflect.Value, name string) (reflect.Value, bool) { for v.IsValid() { typ := v.Type() if n := v.Type().NumMethod(); n > 0 { for i := 0; i < n; i++ { m := typ.Method(i) mtyp := m.Type if m.Name == name && mtyp.NumIn() == 1 { return v.Method(i).Call(nil)[0], true } } } if name == "." { return v, true } switch av := v; av.Kind() { case reflect.Ptr: v = av.Elem() case reflect.Interface: v = av.Elem() case reflect.Struct: return sanitizeValue(av.FieldByName(name)) case reflect.Map: return sanitizeValue(av.MapIndex(reflect.ValueOf(name))) default: return reflect.Value{}, false } } return reflect.Value{}, false } func sanitizeValue(v reflect.Value) (reflect.Value, bool) { if v.IsValid() { return v, true } return reflect.Value{}, false } func isEmpty(v reflect.Value) bool { if !v.IsValid() || v.Interface() == nil { return true } valueInd := indirect(v) if !valueInd.IsValid() { return true } switch val := valueInd; val.Kind() { case reflect.Array, reflect.Slice: return val.Len() == 0 case reflect.String: return strings.TrimSpace(val.String()) == "" default: return valueInd.IsZero() } } func indirect(v reflect.Value) reflect.Value { loop: for v.IsValid() { switch av := v; av.Kind() { case reflect.Ptr: v = av.Elem() case reflect.Interface: v = av.Elem() default: break loop } } return v } func (tmpl *Template) renderSection(w io.Writer, section *sectionNode, stack []reflect.Value) error { value, err := lookup(stack, section.name, false) if err != nil { return err } // if the value is empty, check if it's an inverted section if isEmpty(value) != section.inverted { return nil } if !section.inverted { switch val := indirect(value); val.Kind() { case reflect.Slice, reflect.Array: valLen := val.Len() enumeration := make([]reflect.Value, valLen) for i := 0; i < valLen; i++ { enumeration[i] = val.Index(i) } topStack := len(stack) stack = append(stack, enumeration[0]) for _, elem := range enumeration { stack[topStack] = elem if err = tmpl.renderNodes(w, section.nodes, stack); err != nil { return err } } return nil case reflect.Map, reflect.Struct: return tmpl.renderNodes(w, section.nodes, append(stack, value)) } return tmpl.renderNodes(w, section.nodes, stack) } return tmpl.renderNodes(w, section.nodes, stack) } func (tmpl *Template) renderNodes(w io.Writer, nodes []node, stack []reflect.Value) error { for _, n := range nodes { if err := tmpl.renderNode(w, n, stack); err != nil { return err } } return nil } func (tmpl *Template) renderNode(w io.Writer, node node, stack []reflect.Value) error { switch n := node.(type) { case *textNode: _, err := w.Write(n.text) return err case *varNode: val, err := lookup(stack, n.name, tmpl.errmiss) if err != nil { return err } if val.IsValid() { if n.raw { fmt.Fprint(w, val.Interface()) } else { sxhtml.NewGenerator(nil).WriteHTML(w, sxpf.MakeString(fmt.Sprint(val.Interface()))) } } case *sectionNode: if err := tmpl.renderSection(w, n, stack); err != nil { return err } case *partialNode: partial, err := getPartials(n.prov, n.name, n.indent) if err != nil { return err } if err = partial.renderTemplate(w, stack); err != nil { return err } } return nil } func (tmpl *Template) renderTemplate(w io.Writer, stack []reflect.Value) error { for _, n := range tmpl.nodes { if err := tmpl.renderNode(w, n, stack); err != nil { return err } } return nil } // Render uses the given data source - generally a map or struct - to render // the compiled template to an io.Writer. func (tmpl *Template) Render(w io.Writer, data interface{}) error { return tmpl.renderTemplate(w, []reflect.Value{reflect.ValueOf(data)}) } // ParseString compiles a mustache template string, retrieving any // required partials from the given provider. The resulting output can be used // to efficiently render the template multiple times with different data // sources. func ParseString(data string, partials PartialProvider) (*Template, error) { if partials == nil { partials = &EmptyProvider } tmpl := Template{data, "{{", "}}", 0, 1, []node{}, partials, false} err := tmpl.parse() if err != nil { return nil, err } return &tmpl, err } // SetErrorOnMissing will produce an error is a variable is not found. func (tmpl *Template) SetErrorOnMissing() { tmpl.errmiss = true } // PartialProvider comprises the behaviors required of a struct to be able to // provide partials to the mustache rendering engine. type PartialProvider interface { // Get accepts the name of a partial and returns the parsed partial, if it // could be found; a valid but empty template, if it could not be found; or // nil and error if an error occurred (other than an inability to find the // partial). Get(name string) (string, error) } // ErrPartialNotFound is returned if a partial was not found. type ErrPartialNotFound struct { Name string } func (err *ErrPartialNotFound) Error() string { return "Partial '" + err.Name + "' not found" } // StaticProvider implements the PartialProvider interface by providing // partials drawn from a map, which maps partial name to template contents. type StaticProvider struct { Partials map[string]string } // Get accepts the name of a partial and returns the parsed partial. func (sp *StaticProvider) Get(name string) (string, error) { if sp.Partials != nil { if data, ok := sp.Partials[name]; ok { return data, nil } } return "", &ErrPartialNotFound{name} } // emptyProvider will always returns an empty string. type emptyProvider struct{} // Get accepts the name of a partial and returns the parsed partial. func (*emptyProvider) Get(string) (string, error) { return "", nil } // EmptyProvider is a partial provider that will always return an empty string. var EmptyProvider emptyProvider var nonEmptyLine = regexp.MustCompile(`(?m:^(.+)$)`) func getPartials(partials PartialProvider, name, indent string) (*Template, error) { data, err := partials.Get(name) if err != nil { return nil, err } // indent non empty lines data = nonEmptyLine.ReplaceAllString(data, indent+"$1") return ParseString(data, partials) } |
Added template/mustache_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 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 | //----------------------------------------------------------------------------- // Copyright (c) 2020-present Detlef Stern // // This file is part of Zettelstore. // // Zettelstore is licensed under the latest version of the EUPL (European Union // Public License). Please see file LICENSE.txt for your rights and obligations // under this license. // // This file was derived from previous work: // - https://github.com/hoisie/mustache (License: MIT) // Copyright (c) 2009 Michael Hoisie // - https://github.com/cbroglie/mustache (a fork from above code) // Starting with commit [f9b4cbf] // Does not have an explicit copyright and obviously continues with // above MIT license. // The license text is included in the same directory where this file is // located. See file LICENSE. //----------------------------------------------------------------------------- package template_test import ( "fmt" "strconv" "strings" "testing" "zettelstore.de/z/template" ) type Test struct { tmpl string context interface{} expected string err error } type Data struct { A bool B string } type User struct { Name string ID int64 } type Settings struct { Allow bool } func (u User) Func1() string { return u.Name } func (u *User) Func2() string { return u.Name } func (u *User) Func3() (map[string]string, error) { return map[string]string{"name": u.Name}, nil } func (u *User) Func4() (map[string]string, error) { return nil, nil } func (u *User) Func5() (*Settings, error) { return &Settings{true}, nil } func (u *User) Func6() ([]interface{}, error) { var v []interface{} v = append(v, &Settings{true}) return v, nil } func (u User) Truefunc1() bool { return true } func (u *User) Truefunc2() bool { return true } func makeVector(n int) []interface{} { var v []interface{} for i := 0; i < n; i++ { v = append(v, &User{"Mike", 1}) } return v } type Category struct { Tag string Description string } func (c Category) DisplayName() string { return c.Tag + " - " + c.Description } var tests = []Test{ {`hello world`, nil, "hello world", nil}, {`hello {{name}}`, map[string]string{"name": "world"}, "hello world", nil}, {`{{var}}`, map[string]string{"var": "5 > 2"}, "5 > 2", nil}, {`{{{var}}}`, map[string]string{"var": "5 > 2"}, "5 > 2", nil}, // {`{{var}}`, map[string]string{"var": "& \" < >"}, "& " < >", nil}, {`{{{var}}}`, map[string]string{"var": "& \" < >"}, "& \" < >", nil}, {`{{a}}{{b}}{{c}}{{d}}`, map[string]string{"a": "a", "b": "b", "c": "c", "d": "d"}, "abcd", nil}, {`0{{a}}1{{b}}23{{c}}456{{d}}89`, map[string]string{"a": "a", "b": "b", "c": "c", "d": "d"}, "0a1b23c456d89", nil}, {`hello {{! comment }}world`, map[string]string{}, "hello world", nil}, {`{{ a }}{{=<% %>=}}<%b %><%={{ }}=%>{{ c }}`, map[string]string{"a": "a", "b": "b", "c": "c"}, "abc", nil}, {`{{ a }}{{= <% %> =}}<%b %><%= {{ }}=%>{{c}}`, map[string]string{"a": "a", "b": "b", "c": "c"}, "abc", nil}, //section tests {`{{#A}}{{B}}{{/A}}`, Data{true, "hello"}, "hello", nil}, {`{{#A}}{{{B}}}{{/A}}`, Data{true, "5 > 2"}, "5 > 2", nil}, {`{{#A}}{{B}}{{/A}}`, Data{true, "5 > 2"}, "5 > 2", nil}, {`{{#A}}{{B}}{{/A}}`, Data{false, "hello"}, "", nil}, {`{{a}}{{#b}}{{b}}{{/b}}{{c}}`, map[string]string{"a": "a", "b": "b", "c": "c"}, "abc", nil}, {`{{#A}}{{B}}{{/A}}`, struct { A []struct { B string } }{[]struct { B string }{{"a"}, {"b"}, {"c"}}}, "abc", nil, }, {`{{#A}}{{b}}{{/A}}`, struct{ A []map[string]string }{[]map[string]string{{"b": "a"}, {"b": "b"}, {"b": "c"}}}, "abc", nil}, {`{{#users}}{{Name}}{{/users}}`, map[string]interface{}{"users": []User{{"Mike", 1}}}, "Mike", nil}, {`{{#users}}gone{{Name}}{{/users}}`, map[string]interface{}{"users": nil}, "", nil}, {`{{#users}}gone{{Name}}{{/users}}`, map[string]interface{}{"users": (*User)(nil)}, "", nil}, {`{{#users}}gone{{Name}}{{/users}}`, map[string]interface{}{"users": []User{}}, "", nil}, {`{{#users}}{{Name}}{{/users}}`, map[string]interface{}{"users": []*User{{"Mike", 1}}}, "Mike", nil}, {`{{#users}}{{Name}}{{/users}}`, map[string]interface{}{"users": []interface{}{&User{"Mike", 12}}}, "Mike", nil}, {`{{#users}}{{Name}}{{/users}}`, map[string]interface{}{"users": makeVector(1)}, "Mike", nil}, {`{{Name}}`, User{"Mike", 1}, "Mike", nil}, {`{{Name}}`, &User{"Mike", 1}, "Mike", nil}, {"{{#users}}\n{{Name}}\n{{/users}}", map[string]interface{}{"users": makeVector(2)}, "Mike\nMike\n", nil}, {"{{#users}}\r\n{{Name}}\r\n{{/users}}", map[string]interface{}{"users": makeVector(2)}, "Mike\r\nMike\r\n", nil}, //falsy: golang zero values {"{{#a}}Hi {{.}}{{/a}}", map[string]interface{}{"a": nil}, "", nil}, {"{{#a}}Hi {{.}}{{/a}}", map[string]interface{}{"a": false}, "", nil}, {"{{#a}}Hi {{.}}{{/a}}", map[string]interface{}{"a": 0}, "", nil}, {"{{#a}}Hi {{.}}{{/a}}", map[string]interface{}{"a": 0.0}, "", nil}, {"{{#a}}Hi {{.}}{{/a}}", map[string]interface{}{"a": ""}, "", nil}, {"{{#a}}Hi {{.}}{{/a}}", map[string]interface{}{"a": Data{}}, "", nil}, {"{{#a}}Hi {{.}}{{/a}}", map[string]interface{}{"a": []interface{}{}}, "", nil}, {"{{#a}}Hi {{.}}{{/a}}", map[string]interface{}{"a": [0]interface{}{}}, "", nil}, //falsy: special cases we disagree with golang {"{{#a}}Hi {{.}}{{/a}}", map[string]interface{}{"a": "\t"}, "", nil}, {"{{#a}}Hi {{.}}{{/a}}", map[string]interface{}{"a": []interface{}{0}}, "Hi 0", nil}, {"{{#a}}Hi {{.}}{{/a}}", map[string]interface{}{"a": [1]interface{}{0}}, "Hi 0", nil}, //section does not exist {`{{#has}}{{/has}}`, &User{"Mike", 1}, "", nil}, // implicit iterator tests {`"{{#list}}({{.}}){{/list}}"`, map[string]interface{}{"list": []string{"a", "b", "c", "d", "e"}}, "\"(a)(b)(c)(d)(e)\"", nil}, {`"{{#list}}({{.}}){{/list}}"`, map[string]interface{}{"list": []int{1, 2, 3, 4, 5}}, "\"(1)(2)(3)(4)(5)\"", nil}, {`"{{#list}}({{.}}){{/list}}"`, map[string]interface{}{"list": []float64{1.10, 2.20, 3.30, 4.40, 5.50}}, "\"(1.1)(2.2)(3.3)(4.4)(5.5)\"", nil}, //inverted section tests {`{{a}}{{^b}}b{{/b}}{{c}}`, map[string]interface{}{"a": "a", "b": false, "c": "c"}, "abc", nil}, {`{{^a}}b{{/a}}`, map[string]interface{}{"a": false}, "b", nil}, {`{{^a}}b{{/a}}`, map[string]interface{}{"a": true}, "", nil}, {`{{^a}}b{{/a}}`, map[string]interface{}{"a": "nonempty string"}, "", nil}, {`{{^a}}b{{/a}}`, map[string]interface{}{"a": []string{}}, "b", nil}, {`{{a}}{{^b}}b{{/b}}{{c}}`, map[string]string{"a": "a", "c": "c"}, "abc", nil}, //function tests {`{{#users}}{{Func1}}{{/users}}`, map[string]interface{}{"users": []User{{"Mike", 1}}}, "Mike", nil}, {`{{#users}}{{Func1}}{{/users}}`, map[string]interface{}{"users": []*User{{"Mike", 1}}}, "Mike", nil}, {`{{#users}}{{Func2}}{{/users}}`, map[string]interface{}{"users": []*User{{"Mike", 1}}}, "Mike", nil}, {`{{#users}}{{#Func3}}{{name}}{{/Func3}}{{/users}}`, map[string]interface{}{"users": []*User{{"Mike", 1}}}, "Mike", nil}, {`{{#users}}{{#Func4}}{{name}}{{/Func4}}{{/users}}`, map[string]interface{}{"users": []*User{{"Mike", 1}}}, "", nil}, {`{{#Truefunc1}}abcd{{/Truefunc1}}`, User{"Mike", 1}, "abcd", nil}, {`{{#Truefunc1}}abcd{{/Truefunc1}}`, &User{"Mike", 1}, "abcd", nil}, {`{{#Truefunc2}}abcd{{/Truefunc2}}`, &User{"Mike", 1}, "abcd", nil}, {`{{#Func5}}{{#Allow}}abcd{{/Allow}}{{/Func5}}`, &User{"Mike", 1}, "abcd", nil}, {`{{#user}}{{#Func5}}{{#Allow}}abcd{{/Allow}}{{/Func5}}{{/user}}`, map[string]interface{}{"user": &User{"Mike", 1}}, "abcd", nil}, {`{{#user}}{{#Func6}}{{#Allow}}abcd{{/Allow}}{{/Func6}}{{/user}}`, map[string]interface{}{"user": &User{"Mike", 1}}, "abcd", nil}, //context chaining {`hello {{#section}}{{name}}{{/section}}`, map[string]interface{}{"section": map[string]string{"name": "world"}}, "hello world", nil}, {`hello {{#section}}{{name}}{{/section}}`, map[string]interface{}{"name": "bob", "section": map[string]string{"name": "world"}}, "hello world", nil}, {`hello {{#bool}}{{#section}}{{name}}{{/section}}{{/bool}}`, map[string]interface{}{"bool": true, "section": map[string]string{"name": "world"}}, "hello world", nil}, {`{{#users}}{{canvas}}{{/users}}`, map[string]interface{}{"canvas": "hello", "users": []User{{"Mike", 1}}}, "hello", nil}, {`{{#categories}}{{DisplayName}}{{/categories}}`, map[string][]*Category{ "categories": {&Category{"a", "b"}}, }, "a - b", nil}, //dotted names(dot notation) {`"{{person.name}}" == "{{#person}}{{name}}{{/person}}"`, map[string]interface{}{"person": map[string]string{"name": "Joe"}}, `"Joe" == "Joe"`, nil}, {`"{{{person.name}}}" == "{{#person}}{{{name}}}{{/person}}"`, map[string]interface{}{"person": map[string]string{"name": "Joe"}}, `"Joe" == "Joe"`, nil}, {`"{{a.b.c.d.e.name}}" == "Phil"`, map[string]interface{}{"a": map[string]interface{}{"b": map[string]interface{}{"c": map[string]interface{}{"d": map[string]interface{}{"e": map[string]string{"name": "Phil"}}}}}}, `"Phil" == "Phil"`, nil}, {`"{{#a}}{{b.c.d.e.name}}{{/a}}" == "Phil"`, map[string]interface{}{"a": map[string]interface{}{"b": map[string]interface{}{"c": map[string]interface{}{"d": map[string]interface{}{"e": map[string]string{"name": "Phil"}}}}}, "b": map[string]interface{}{"c": map[string]interface{}{"d": map[string]interface{}{"e": map[string]string{"name": "Wrong"}}}}}, `"Phil" == "Phil"`, nil}, } func parseString(data string) (*template.Template, error) { return template.ParseString(data, nil) } func render(tmpl *template.Template, data interface{}) (string, error) { var sf strings.Builder err := tmpl.Render(&sf, data) return sf.String(), err } func renderString(data string, errMissing bool, value interface{}) (string, error) { tmpl, err := parseString(data) if err != nil { return "", err } if errMissing { tmpl.SetErrorOnMissing() } return render(tmpl, value) } func TestBasic(t *testing.T) { t.Parallel() for _, test := range tests { output, err := renderString(test.tmpl, false, test.context) if err != nil { t.Errorf("%q expected %q but got error: %v", test.tmpl, test.expected, err) } else if output != test.expected { t.Errorf("%q expected %q got %q", test.tmpl, test.expected, output) } } // Now set "error on missing variable" and test again for _, test := range tests { output, err := renderString(test.tmpl, true, test.context) if err != nil { t.Errorf("%q expected %q but got error: %v", test.tmpl, test.expected, err) } else if output != test.expected { t.Errorf("%q expected %q got %q", test.tmpl, test.expected, output) } } } var missing = []Test{ //does not exist {`{{dne}}`, map[string]string{"name": "world"}, "", nil}, {`{{dne}}`, User{"Mike", 1}, "", nil}, {`{{dne}}`, &User{"Mike", 1}, "", nil}, //dotted names(dot notation) {`"{{a.b.c}}" == ""`, map[string]interface{}{}, `"" == ""`, nil}, {`"{{a.b.c.name}}" == ""`, map[string]interface{}{"a": map[string]interface{}{"b": map[string]string{}}, "c": map[string]string{"name": "Jim"}}, `"" == ""`, nil}, {`{{#a}}{{b.c}}{{/a}}`, map[string]interface{}{"a": map[string]interface{}{"b": map[string]string{}}, "b": map[string]string{"c": "ERROR"}}, "", nil}, } func TestMissing(t *testing.T) { t.Parallel() for _, test := range missing { output, err := renderString(test.tmpl, false, test.context) if err != nil { t.Error(err) } else if output != test.expected { t.Errorf("%q expected %q got %q", test.tmpl, test.expected, output) } } // Now set "error on missing varaible" and confirm we get errors. for _, test := range missing { output, err := renderString(test.tmpl, true, test.context) if err == nil { t.Errorf("%q expected missing variable error but got %q", test.tmpl, output) } else if !strings.Contains(err.Error(), "missing variable") { t.Errorf("%q expected missing variable error but got %q", test.tmpl, err.Error()) } } } var malformed = []Test{ {`{{#a}}{{}}{{/a}}`, Data{true, "hello"}, "", fmt.Errorf("line 1: empty tag")}, {`{{}}`, nil, "", fmt.Errorf("line 1: empty tag")}, {`{{}`, nil, "", fmt.Errorf("line 1: unmatched open tag")}, {`{{`, nil, "", fmt.Errorf("line 1: unmatched open tag")}, //invalid syntax - https://github.com/hoisie/mustache/issues/10 {`{{#a}}{{#b}}{{/a}}{{/b}}}`, map[string]interface{}{}, "", fmt.Errorf("line 1: interleaved closing tag: a")}, } func TestMalformed(t *testing.T) { t.Parallel() for _, test := range malformed { output, err := renderString(test.tmpl, false, test.context) if err != nil { if test.err == nil { t.Error(err) } else if test.err.Error() != err.Error() { t.Errorf("%q expected error %q but got error %q", test.tmpl, test.err.Error(), err.Error()) } } else { if test.err == nil { t.Errorf("%q expected %q got %q", test.tmpl, test.expected, output) } else { t.Errorf("%q expected error %q but got %q", test.tmpl, test.err.Error(), output) } } } } type Person struct { FirstName string LastName string } func (p *Person) Name1() string { return p.FirstName + " " + p.LastName } func (p Person) Name2() string { return p.FirstName + " " + p.LastName } func TestPointerReceiver(t *testing.T) { t.Parallel() p := Person{"John", "Smith"} tests := []struct { tmpl string context interface{} expected string }{ { tmpl: "{{Name1}}", context: &p, expected: "John Smith", }, { tmpl: "{{Name2}}", context: &p, expected: "John Smith", }, { tmpl: "{{Name1}}", context: p, expected: "", }, { tmpl: "{{Name2}}", context: p, expected: "John Smith", }, } for _, test := range tests { output, err := renderString(test.tmpl, false, test.context) if err != nil { t.Error(err) } else if output != test.expected { t.Errorf("expected %q got %q", test.expected, output) } } } type tag struct { Type template.TagType Name string Tags []tag } type tagsTest struct { tmpl string tags []tag } var tagTests = []tagsTest{ { tmpl: `hello world`, tags: nil, }, { tmpl: `hello {{name}}`, tags: []tag{ { Type: template.Variable, Name: "name", }, }, }, { tmpl: `{{#name}}hello {{name}}{{/name}}{{^name}}hello {{name2}}{{/name}}`, tags: []tag{ { Type: template.Section, Name: "name", Tags: []tag{ { Type: template.Variable, Name: "name", }, }, }, { Type: template.InvertedSection, Name: "name", Tags: []tag{ { Type: template.Variable, Name: "name2", }, }, }, }, }, } func TestTags(t *testing.T) { t.Parallel() for _, test := range tagTests { testTags(t, &test) } } func testTags(t *testing.T, test *tagsTest) { tmpl, err := parseString(test.tmpl) if err != nil { t.Error(err) return } compareTags(t, tmpl.Tags(), test.tags) } func compareTags(t *testing.T, actual []template.Tag, expected []tag) { if len(actual) != len(expected) { t.Errorf("expected %d tags, got %d", len(expected), len(actual)) return } for i, tag := range actual { if tag.Type() != expected[i].Type { t.Errorf("expected %s, got %s", tagString(expected[i].Type), tagString(tag.Type())) return } if tag.Name() != expected[i].Name { t.Errorf("expected %s, got %s", expected[i].Name, tag.Name()) return } switch tag.Type() { case template.Variable: if len(expected[i].Tags) != 0 { t.Errorf("expected %d tags, got 0", len(expected[i].Tags)) return } case template.Section, template.InvertedSection: compareTags(t, tag.Tags(), expected[i].Tags) case template.Partial: compareTags(t, tag.Tags(), expected[i].Tags) default: t.Errorf("invalid tag type: %s", tagString(tag.Type())) return } } } func tagString(t template.TagType) string { if int(t) < len(tagNames) { return tagNames[t] } return "type" + strconv.Itoa(int(t)) } var tagNames = []string{ template.Invalid: "Invalid", template.Variable: "Variable", template.Section: "Section", template.InvertedSection: "InvertedSection", template.Partial: "Partial", } |
Added template/spec_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 | //----------------------------------------------------------------------------- // Copyright (c) 2020-present Detlef Stern // // This file is part of Zettelstore. // // Zettelstore is licensed under the latest version of the EUPL (European Union // Public License). Please see file LICENSE.txt for your rights and obligations // under this license. // // This file was derived from previous work: // - https://github.com/hoisie/mustache (License: MIT) // Copyright (c) 2009 Michael Hoisie // - https://github.com/cbroglie/mustache (a fork from above code) // Starting with commit [f9b4cbf] // Does not have an explicit copyright and obviously continues with // above MIT license. // The license text is included in the same directory where this file is // located. See file LICENSE. //----------------------------------------------------------------------------- package template_test import ( "encoding/json" "errors" "os" "path/filepath" "sort" "testing" "zettelstore.de/z/template" ) var enabledTests = map[string]map[string]bool{ "comments.json": { "Inline": true, "Multiline": true, "Standalone": true, "Indented Standalone": true, "Standalone Line Endings": true, "Standalone Without Previous Line": true, "Standalone Without Newline": true, "Multiline Standalone": true, "Indented Multiline Standalone": true, "Indented Inline": true, "Surrounding Whitespace": true, }, "delimiters.json": { "Pair Behavior": true, "Special Characters": true, "Sections": true, "Inverted Sections": true, "Partial Inheritence": true, "Post-Partial Behavior": true, "Outlying Whitespace (Inline)": true, "Standalone Tag": true, "Indented Standalone Tag": true, "Pair with Padding": true, "Surrounding Whitespace": true, "Standalone Line Endings": true, "Standalone Without Previous Line": true, "Standalone Without Newline": true, }, "interpolation.json": { "No Interpolation": true, "Basic Interpolation": true, "HTML Escaping": true, "Triple Mustache": true, "Ampersand": true, "Basic Integer Interpolation": true, "Triple Mustache Integer Interpolation": true, "Ampersand Integer Interpolation": true, "Basic Decimal Interpolation": true, "Triple Mustache Decimal Interpolation": true, "Ampersand Decimal Interpolation": true, "Basic Context Miss Interpolation": true, "Triple Mustache Context Miss Interpolation": true, "Ampersand Context Miss Interpolation": true, "Dotted Names - Basic Interpolation": true, "Dotted Names - Triple Mustache Interpolation": true, "Dotted Names - Ampersand Interpolation": true, "Dotted Names - Arbitrary Depth": true, "Dotted Names - Broken Chains": true, "Dotted Names - Broken Chain Resolution": true, "Dotted Names - Initial Resolution": true, "Interpolation - Surrounding Whitespace": true, "Triple Mustache - Surrounding Whitespace": true, "Ampersand - Surrounding Whitespace": true, "Interpolation - Standalone": true, "Triple Mustache - Standalone": true, "Ampersand - Standalone": true, "Interpolation With Padding": true, "Triple Mustache With Padding": true, "Ampersand With Padding": true, }, "inverted.json": { "Falsey": true, "Truthy": true, "Context": true, "List": true, "Empty List": true, "Doubled": true, "Nested (Falsey)": true, "Nested (Truthy)": true, "Context Misses": true, "Dotted Names - Truthy": true, "Dotted Names - Falsey": true, "Internal Whitespace": true, "Indented Inline Sections": true, "Standalone Lines": true, "Standalone Indented Lines": true, "Padding": true, "Dotted Names - Broken Chains": true, "Surrounding Whitespace": true, "Standalone Line Endings": true, "Standalone Without Previous Line": true, "Standalone Without Newline": true, }, "partials.json": { "Basic Behavior": true, "Failed Lookup": true, "Context": true, "Recursion": true, "Surrounding Whitespace": true, "Inline Indentation": true, "Standalone Line Endings": true, "Standalone Without Previous Line": true, "Standalone Without Newline": true, "Standalone Indentation": true, "Padding Whitespace": true, }, "sections.json": { "Truthy": true, "Falsey": true, "Context": true, "Deeply Nested Contexts": true, "List": true, "Empty List": true, "Doubled": true, "Nested (Truthy)": true, "Nested (Falsey)": true, "Context Misses": true, "Implicit Iterator - String": true, "Implicit Iterator - Integer": true, "Implicit Iterator - Decimal": true, "Implicit Iterator - Array": true, "Dotted Names - Truthy": true, "Dotted Names - Falsey": true, "Dotted Names - Broken Chains": true, "Surrounding Whitespace": true, "Internal Whitespace": true, "Indented Inline Sections": true, "Standalone Lines": true, "Indented Standalone Lines": true, "Standalone Line Endings": true, "Standalone Without Previous Line": true, "Standalone Without Newline": true, "Padding": true, }, "~lambdas.json": nil, // not implemented } type specTest struct { Name string `json:"name"` Data interface{} `json:"data"` Expected string `json:"expected"` Template string `json:"template"` Description string `json:"desc"` Partials map[string]string `json:"partials"` } type specTestSuite struct { Tests []specTest `json:"tests"` } func getRoot() string { curDir, err := os.Getwd() if err != nil { curDir = os.Getenv("PWD") } return filepath.Join(curDir, "..", "testdata", "mustache") } func TestSpec(t *testing.T) { t.Parallel() root := getRoot() if _, err := os.Stat(root); err != nil { if errors.Is(err, os.ErrNotExist) { t.Fatalf("Could not find the mustache testdata folder at %s'", root) } t.Fatal(err) } paths, err := filepath.Glob(filepath.Join(root, "*.json")) if err != nil { t.Fatal(err) } sort.Strings(paths) for _, path := range paths { _, file := filepath.Split(path) enabled, ok := enabledTests[file] if !ok { t.Errorf("Unexpected file %s, consider adding to enabledFiles", file) continue } if enabled == nil { continue } b, err2 := os.ReadFile(path) if err2 != nil { t.Fatal(err2) } var suite specTestSuite err2 = json.Unmarshal(b, &suite) if err2 != nil { t.Fatal(err2) } for _, test := range suite.Tests { runTest(t, file, &test) } } } func selectProvider(partials map[string]string) template.PartialProvider { if len(partials) == 0 { return &template.EmptyProvider } return &template.StaticProvider{partials} } func runTest(t *testing.T, file string, test *specTest) { enabled, ok := enabledTests[file][test.Name] if !ok { t.Errorf("[%s %s]: Unexpected test, add to enabledTests", file, test.Name) } if !enabled { t.Logf("[%s %s]: Skipped", file, test.Name) return } tmpl, err := template.ParseString(test.Template, selectProvider(test.Partials)) if err != nil { t.Errorf("[%s %s]: %s", file, test.Name, err.Error()) return } out, err := render(tmpl, test.Data) if err != nil { t.Errorf("[%s %s]: %s", file, test.Name, err.Error()) return } if out != test.Expected { t.Errorf("[%s %s]: Expected %q, got %q", file, test.Name, test.Expected, out) return } t.Logf("[%s %s]: Passed", file, test.Name) } |
Added testdata/mustache/comments.json.
> | 1 | {"__ATTN__":"Do not edit this file; changes belong in the appropriate YAML file.","overview":"Comment tags represent content that should never appear in the resulting\noutput.\n\nThe tag's content may contain any substring (including newlines) EXCEPT the\nclosing delimiter.\n\nComment tags SHOULD be treated as standalone when appropriate.\n","tests":[{"name":"Inline","data":{},"expected":"1234567890","template":"12345{{! Comment Block! }}67890","desc":"Comment blocks should be removed from the template."},{"name":"Multiline","data":{},"expected":"1234567890\n","template":"12345{{!\n This is a\n multi-line comment...\n}}67890\n","desc":"Multiline comments should be permitted."},{"name":"Standalone","data":{},"expected":"Begin.\nEnd.\n","template":"Begin.\n{{! Comment Block! }}\nEnd.\n","desc":"All standalone comment lines should be removed."},{"name":"Indented Standalone","data":{},"expected":"Begin.\nEnd.\n","template":"Begin.\n {{! Indented Comment Block! }}\nEnd.\n","desc":"All standalone comment lines should be removed."},{"name":"Standalone Line Endings","data":{},"expected":"|\r\n|","template":"|\r\n{{! Standalone Comment }}\r\n|","desc":"\"\\r\\n\" should be considered a newline for standalone tags."},{"name":"Standalone Without Previous Line","data":{},"expected":"!","template":" {{! I'm Still Standalone }}\n!","desc":"Standalone tags should not require a newline to precede them."},{"name":"Standalone Without Newline","data":{},"expected":"!\n","template":"!\n {{! I'm Still Standalone }}","desc":"Standalone tags should not require a newline to follow them."},{"name":"Multiline Standalone","data":{},"expected":"Begin.\nEnd.\n","template":"Begin.\n{{!\nSomething's going on here...\n}}\nEnd.\n","desc":"All standalone comment lines should be removed."},{"name":"Indented Multiline Standalone","data":{},"expected":"Begin.\nEnd.\n","template":"Begin.\n {{!\n Something's going on here...\n }}\nEnd.\n","desc":"All standalone comment lines should be removed."},{"name":"Indented Inline","data":{},"expected":" 12 \n","template":" 12 {{! 34 }}\n","desc":"Inline comments should not strip whitespace"},{"name":"Surrounding Whitespace","data":{},"expected":"12345 67890","template":"12345 {{! Comment Block! }} 67890","desc":"Comment removal should preserve surrounding whitespace."}]} |
Added testdata/mustache/delimiters.json.
> | 1 | {"__ATTN__":"Do not edit this file; changes belong in the appropriate YAML file.","overview":"Set Delimiter tags are used to change the tag delimiters for all content\nfollowing the tag in the current compilation unit.\n\nThe tag's content MUST be any two non-whitespace sequences (separated by\nwhitespace) EXCEPT an equals sign ('=') followed by the current closing\ndelimiter.\n\nSet Delimiter tags SHOULD be treated as standalone when appropriate.\n","tests":[{"name":"Pair Behavior","data":{"text":"Hey!"},"expected":"(Hey!)","template":"{{=<% %>=}}(<%text%>)","desc":"The equals sign (used on both sides) should permit delimiter changes."},{"name":"Special Characters","data":{"text":"It worked!"},"expected":"(It worked!)","template":"({{=[ ]=}}[text])","desc":"Characters with special meaning regexen should be valid delimiters."},{"name":"Sections","data":{"section":true,"data":"I got interpolated."},"expected":"[\n I got interpolated.\n |data|\n\n {{data}}\n I got interpolated.\n]\n","template":"[\n{{#section}}\n {{data}}\n |data|\n{{/section}}\n\n{{= | | =}}\n|#section|\n {{data}}\n |data|\n|/section|\n]\n","desc":"Delimiters set outside sections should persist."},{"name":"Inverted Sections","data":{"section":false,"data":"I got interpolated."},"expected":"[\n I got interpolated.\n |data|\n\n {{data}}\n I got interpolated.\n]\n","template":"[\n{{^section}}\n {{data}}\n |data|\n{{/section}}\n\n{{= | | =}}\n|^section|\n {{data}}\n |data|\n|/section|\n]\n","desc":"Delimiters set outside inverted sections should persist."},{"name":"Partial Inheritence","data":{"value":"yes"},"expected":"[ .yes. ]\n[ .yes. ]\n","template":"[ {{>include}} ]\n{{= | | =}}\n[ |>include| ]\n","desc":"Delimiters set in a parent template should not affect a partial.","partials":{"include":".{{value}}."}},{"name":"Post-Partial Behavior","data":{"value":"yes"},"expected":"[ .yes. .yes. ]\n[ .yes. .|value|. ]\n","template":"[ {{>include}} ]\n[ .{{value}}. .|value|. ]\n","desc":"Delimiters set in a partial should not affect the parent template.","partials":{"include":".{{value}}. {{= | | =}} .|value|."}},{"name":"Surrounding Whitespace","data":{},"expected":"| |","template":"| {{=@ @=}} |","desc":"Surrounding whitespace should be left untouched."},{"name":"Outlying Whitespace (Inline)","data":{},"expected":" | \n","template":" | {{=@ @=}}\n","desc":"Whitespace should be left untouched."},{"name":"Standalone Tag","data":{},"expected":"Begin.\nEnd.\n","template":"Begin.\n{{=@ @=}}\nEnd.\n","desc":"Standalone lines should be removed from the template."},{"name":"Indented Standalone Tag","data":{},"expected":"Begin.\nEnd.\n","template":"Begin.\n {{=@ @=}}\nEnd.\n","desc":"Indented standalone lines should be removed from the template."},{"name":"Standalone Line Endings","data":{},"expected":"|\r\n|","template":"|\r\n{{= @ @ =}}\r\n|","desc":"\"\\r\\n\" should be considered a newline for standalone tags."},{"name":"Standalone Without Previous Line","data":{},"expected":"=","template":" {{=@ @=}}\n=","desc":"Standalone tags should not require a newline to precede them."},{"name":"Standalone Without Newline","data":{},"expected":"=\n","template":"=\n {{=@ @=}}","desc":"Standalone tags should not require a newline to follow them."},{"name":"Pair with Padding","data":{},"expected":"||","template":"|{{= @ @ =}}|","desc":"Superfluous in-tag whitespace should be ignored."}]} |
Added testdata/mustache/interpolation.json.
> | 1 | {"__ATTN__":"Do not edit this file; changes belong in the appropriate YAML file.","overview":"Interpolation tags are used to integrate dynamic content into the template.\n\nThe tag's content MUST be a non-whitespace character sequence NOT containing\nthe current closing delimiter.\n\nThis tag's content names the data to replace the tag. A single period (`.`)\nindicates that the item currently sitting atop the context stack should be\nused; otherwise, name resolution is as follows:\n 1) Split the name on periods; the first part is the name to resolve, any\n remaining parts should be retained.\n 2) Walk the context stack from top to bottom, finding the first context\n that is a) a hash containing the name as a key OR b) an object responding\n to a method with the given name.\n 3) If the context is a hash, the data is the value associated with the\n name.\n 4) If the context is an object, the data is the value returned by the\n method with the given name.\n 5) If any name parts were retained in step 1, each should be resolved\n against a context stack containing only the result from the former\n resolution. If any part fails resolution, the result should be considered\n falsey, and should interpolate as the empty string.\nData should be coerced into a string (and escaped, if appropriate) before\ninterpolation.\n\nThe Interpolation tags MUST NOT be treated as standalone.\n","tests":[{"name":"No Interpolation","data":{},"expected":"Hello from {Mustache}!\n","template":"Hello from {Mustache}!\n","desc":"Mustache-free templates should render as-is."},{"name":"Basic Interpolation","data":{"subject":"world"},"expected":"Hello, world!\n","template":"Hello, {{subject}}!\n","desc":"Unadorned tags should interpolate content into the template."},{"name":"HTML Escaping","data":{"forbidden":"& \" < >"},"expected":"These characters should be HTML escaped: & " < >\n","template":"These characters should be HTML escaped: {{forbidden}}\n","desc":"Basic interpolation should be HTML escaped."},{"name":"Triple Mustache","data":{"forbidden":"& \" < >"},"expected":"These characters should not be HTML escaped: & \" < >\n","template":"These characters should not be HTML escaped: {{{forbidden}}}\n","desc":"Triple mustaches should interpolate without HTML escaping."},{"name":"Ampersand","data":{"forbidden":"& \" < >"},"expected":"These characters should not be HTML escaped: & \" < >\n","template":"These characters should not be HTML escaped: {{&forbidden}}\n","desc":"Ampersand should interpolate without HTML escaping."},{"name":"Basic Integer Interpolation","data":{"mph":85},"expected":"\"85 miles an hour!\"","template":"\"{{mph}} miles an hour!\"","desc":"Integers should interpolate seamlessly."},{"name":"Triple Mustache Integer Interpolation","data":{"mph":85},"expected":"\"85 miles an hour!\"","template":"\"{{{mph}}} miles an hour!\"","desc":"Integers should interpolate seamlessly."},{"name":"Ampersand Integer Interpolation","data":{"mph":85},"expected":"\"85 miles an hour!\"","template":"\"{{&mph}} miles an hour!\"","desc":"Integers should interpolate seamlessly."},{"name":"Basic Decimal Interpolation","data":{"power":1.21},"expected":"\"1.21 jiggawatts!\"","template":"\"{{power}} jiggawatts!\"","desc":"Decimals should interpolate seamlessly with proper significance."},{"name":"Triple Mustache Decimal Interpolation","data":{"power":1.21},"expected":"\"1.21 jiggawatts!\"","template":"\"{{{power}}} jiggawatts!\"","desc":"Decimals should interpolate seamlessly with proper significance."},{"name":"Ampersand Decimal Interpolation","data":{"power":1.21},"expected":"\"1.21 jiggawatts!\"","template":"\"{{&power}} jiggawatts!\"","desc":"Decimals should interpolate seamlessly with proper significance."},{"name":"Basic Context Miss Interpolation","data":{},"expected":"I () be seen!","template":"I ({{cannot}}) be seen!","desc":"Failed context lookups should default to empty strings."},{"name":"Triple Mustache Context Miss Interpolation","data":{},"expected":"I () be seen!","template":"I ({{{cannot}}}) be seen!","desc":"Failed context lookups should default to empty strings."},{"name":"Ampersand Context Miss Interpolation","data":{},"expected":"I () be seen!","template":"I ({{&cannot}}) be seen!","desc":"Failed context lookups should default to empty strings."},{"name":"Dotted Names - Basic Interpolation","data":{"person":{"name":"Joe"}},"expected":"\"Joe\" == \"Joe\"","template":"\"{{person.name}}\" == \"{{#person}}{{name}}{{/person}}\"","desc":"Dotted names should be considered a form of shorthand for sections."},{"name":"Dotted Names - Triple Mustache Interpolation","data":{"person":{"name":"Joe"}},"expected":"\"Joe\" == \"Joe\"","template":"\"{{{person.name}}}\" == \"{{#person}}{{{name}}}{{/person}}\"","desc":"Dotted names should be considered a form of shorthand for sections."},{"name":"Dotted Names - Ampersand Interpolation","data":{"person":{"name":"Joe"}},"expected":"\"Joe\" == \"Joe\"","template":"\"{{&person.name}}\" == \"{{#person}}{{&name}}{{/person}}\"","desc":"Dotted names should be considered a form of shorthand for sections."},{"name":"Dotted Names - Arbitrary Depth","data":{"a":{"b":{"c":{"d":{"e":{"name":"Phil"}}}}}},"expected":"\"Phil\" == \"Phil\"","template":"\"{{a.b.c.d.e.name}}\" == \"Phil\"","desc":"Dotted names should be functional to any level of nesting."},{"name":"Dotted Names - Broken Chains","data":{"a":{}},"expected":"\"\" == \"\"","template":"\"{{a.b.c}}\" == \"\"","desc":"Any falsey value prior to the last part of the name should yield ''."},{"name":"Dotted Names - Broken Chain Resolution","data":{"a":{"b":{}},"c":{"name":"Jim"}},"expected":"\"\" == \"\"","template":"\"{{a.b.c.name}}\" == \"\"","desc":"Each part of a dotted name should resolve only against its parent."},{"name":"Dotted Names - Initial Resolution","data":{"a":{"b":{"c":{"d":{"e":{"name":"Phil"}}}}},"b":{"c":{"d":{"e":{"name":"Wrong"}}}}},"expected":"\"Phil\" == \"Phil\"","template":"\"{{#a}}{{b.c.d.e.name}}{{/a}}\" == \"Phil\"","desc":"The first part of a dotted name should resolve as any other name."},{"name":"Interpolation - Surrounding Whitespace","data":{"string":"---"},"expected":"| --- |","template":"| {{string}} |","desc":"Interpolation should not alter surrounding whitespace."},{"name":"Triple Mustache - Surrounding Whitespace","data":{"string":"---"},"expected":"| --- |","template":"| {{{string}}} |","desc":"Interpolation should not alter surrounding whitespace."},{"name":"Ampersand - Surrounding Whitespace","data":{"string":"---"},"expected":"| --- |","template":"| {{&string}} |","desc":"Interpolation should not alter surrounding whitespace."},{"name":"Interpolation - Standalone","data":{"string":"---"},"expected":" ---\n","template":" {{string}}\n","desc":"Standalone interpolation should not alter surrounding whitespace."},{"name":"Triple Mustache - Standalone","data":{"string":"---"},"expected":" ---\n","template":" {{{string}}}\n","desc":"Standalone interpolation should not alter surrounding whitespace."},{"name":"Ampersand - Standalone","data":{"string":"---"},"expected":" ---\n","template":" {{&string}}\n","desc":"Standalone interpolation should not alter surrounding whitespace."},{"name":"Interpolation With Padding","data":{"string":"---"},"expected":"|---|","template":"|{{ string }}|","desc":"Superfluous in-tag whitespace should be ignored."},{"name":"Triple Mustache With Padding","data":{"string":"---"},"expected":"|---|","template":"|{{{ string }}}|","desc":"Superfluous in-tag whitespace should be ignored."},{"name":"Ampersand With Padding","data":{"string":"---"},"expected":"|---|","template":"|{{& string }}|","desc":"Superfluous in-tag whitespace should be ignored."}]} |
Added testdata/mustache/inverted.json.
> | 1 | {"__ATTN__":"Do not edit this file; changes belong in the appropriate YAML file.","overview":"Inverted Section tags and End Section tags are used in combination to wrap a\nsection of the template.\n\nThese tags' content MUST be a non-whitespace character sequence NOT\ncontaining the current closing delimiter; each Inverted Section tag MUST be\nfollowed by an End Section tag with the same content within the same\nsection.\n\nThis tag's content names the data to replace the tag. Name resolution is as\nfollows:\n 1) Split the name on periods; the first part is the name to resolve, any\n remaining parts should be retained.\n 2) Walk the context stack from top to bottom, finding the first context\n that is a) a hash containing the name as a key OR b) an object responding\n to a method with the given name.\n 3) If the context is a hash, the data is the value associated with the\n name.\n 4) If the context is an object and the method with the given name has an\n arity of 1, the method SHOULD be called with a String containing the\n unprocessed contents of the sections; the data is the value returned.\n 5) Otherwise, the data is the value returned by calling the method with\n the given name.\n 6) If any name parts were retained in step 1, each should be resolved\n against a context stack containing only the result from the former\n resolution. If any part fails resolution, the result should be considered\n falsey, and should interpolate as the empty string.\nIf the data is not of a list type, it is coerced into a list as follows: if\nthe data is truthy (e.g. `!!data == true`), use a single-element list\ncontaining the data, otherwise use an empty list.\n\nThis section MUST NOT be rendered unless the data list is empty.\n\nInverted Section and End Section tags SHOULD be treated as standalone when\nappropriate.\n","tests":[{"name":"Falsey","data":{"boolean":false},"expected":"\"This should be rendered.\"","template":"\"{{^boolean}}This should be rendered.{{/boolean}}\"","desc":"Falsey sections should have their contents rendered."},{"name":"Truthy","data":{"boolean":true},"expected":"\"\"","template":"\"{{^boolean}}This should not be rendered.{{/boolean}}\"","desc":"Truthy sections should have their contents omitted."},{"name":"Context","data":{"context":{"name":"Joe"}},"expected":"\"\"","template":"\"{{^context}}Hi {{name}}.{{/context}}\"","desc":"Objects and hashes should behave like truthy values."},{"name":"List","data":{"list":[{"n":1},{"n":2},{"n":3}]},"expected":"\"\"","template":"\"{{^list}}{{n}}{{/list}}\"","desc":"Lists should behave like truthy values."},{"name":"Empty List","data":{"list":[]},"expected":"\"Yay lists!\"","template":"\"{{^list}}Yay lists!{{/list}}\"","desc":"Empty lists should behave like falsey values."},{"name":"Doubled","data":{"two":"second","bool":false},"expected":"* first\n* second\n* third\n","template":"{{^bool}}\n* first\n{{/bool}}\n* {{two}}\n{{^bool}}\n* third\n{{/bool}}\n","desc":"Multiple inverted sections per template should be permitted."},{"name":"Nested (Falsey)","data":{"bool":false},"expected":"| A B C D E |","template":"| A {{^bool}}B {{^bool}}C{{/bool}} D{{/bool}} E |","desc":"Nested falsey sections should have their contents rendered."},{"name":"Nested (Truthy)","data":{"bool":true},"expected":"| A E |","template":"| A {{^bool}}B {{^bool}}C{{/bool}} D{{/bool}} E |","desc":"Nested truthy sections should be omitted."},{"name":"Context Misses","data":{},"expected":"[Cannot find key 'missing'!]","template":"[{{^missing}}Cannot find key 'missing'!{{/missing}}]","desc":"Failed context lookups should be considered falsey."},{"name":"Dotted Names - Truthy","data":{"a":{"b":{"c":true}}},"expected":"\"\" == \"\"","template":"\"{{^a.b.c}}Not Here{{/a.b.c}}\" == \"\"","desc":"Dotted names should be valid for Inverted Section tags."},{"name":"Dotted Names - Falsey","data":{"a":{"b":{"c":false}}},"expected":"\"Not Here\" == \"Not Here\"","template":"\"{{^a.b.c}}Not Here{{/a.b.c}}\" == \"Not Here\"","desc":"Dotted names should be valid for Inverted Section tags."},{"name":"Dotted Names - Broken Chains","data":{"a":{}},"expected":"\"Not Here\" == \"Not Here\"","template":"\"{{^a.b.c}}Not Here{{/a.b.c}}\" == \"Not Here\"","desc":"Dotted names that cannot be resolved should be considered falsey."},{"name":"Surrounding Whitespace","data":{"boolean":false},"expected":" | \t|\t | \n","template":" | {{^boolean}}\t|\t{{/boolean}} | \n","desc":"Inverted sections should not alter surrounding whitespace."},{"name":"Internal Whitespace","data":{"boolean":false},"expected":" | \n | \n","template":" | {{^boolean}} {{! Important Whitespace }}\n {{/boolean}} | \n","desc":"Inverted should not alter internal whitespace."},{"name":"Indented Inline Sections","data":{"boolean":false},"expected":" NO\n WAY\n","template":" {{^boolean}}NO{{/boolean}}\n {{^boolean}}WAY{{/boolean}}\n","desc":"Single-line sections should not alter surrounding whitespace."},{"name":"Standalone Lines","data":{"boolean":false},"expected":"| This Is\n|\n| A Line\n","template":"| This Is\n{{^boolean}}\n|\n{{/boolean}}\n| A Line\n","desc":"Standalone lines should be removed from the template."},{"name":"Standalone Indented Lines","data":{"boolean":false},"expected":"| This Is\n|\n| A Line\n","template":"| This Is\n {{^boolean}}\n|\n {{/boolean}}\n| A Line\n","desc":"Standalone indented lines should be removed from the template."},{"name":"Standalone Line Endings","data":{"boolean":false},"expected":"|\r\n|","template":"|\r\n{{^boolean}}\r\n{{/boolean}}\r\n|","desc":"\"\\r\\n\" should be considered a newline for standalone tags."},{"name":"Standalone Without Previous Line","data":{"boolean":false},"expected":"^\n/","template":" {{^boolean}}\n^{{/boolean}}\n/","desc":"Standalone tags should not require a newline to precede them."},{"name":"Standalone Without Newline","data":{"boolean":false},"expected":"^\n/\n","template":"^{{^boolean}}\n/\n {{/boolean}}","desc":"Standalone tags should not require a newline to follow them."},{"name":"Padding","data":{"boolean":false},"expected":"|=|","template":"|{{^ boolean }}={{/ boolean }}|","desc":"Superfluous in-tag whitespace should be ignored."}]} |
Added testdata/mustache/partials.json.
> | 1 | {"__ATTN__":"Do not edit this file; changes belong in the appropriate YAML file.","overview":"Partial tags are used to expand an external template into the current\ntemplate.\n\nThe tag's content MUST be a non-whitespace character sequence NOT containing\nthe current closing delimiter.\n\nThis tag's content names the partial to inject. Set Delimiter tags MUST NOT\naffect the parsing of a partial. The partial MUST be rendered against the\ncontext stack local to the tag. If the named partial cannot be found, the\nempty string SHOULD be used instead, as in interpolations.\n\nPartial tags SHOULD be treated as standalone when appropriate. If this tag\nis used standalone, any whitespace preceding the tag should treated as\nindentation, and prepended to each line of the partial before rendering.\n","tests":[{"name":"Basic Behavior","data":{},"expected":"\"from partial\"","template":"\"{{>text}}\"","desc":"The greater-than operator should expand to the named partial.","partials":{"text":"from partial"}},{"name":"Failed Lookup","data":{},"expected":"\"\"","template":"\"{{>text}}\"","desc":"The empty string should be used when the named partial is not found.","partials":{}},{"name":"Context","data":{"text":"content"},"expected":"\"*content*\"","template":"\"{{>partial}}\"","desc":"The greater-than operator should operate within the current context.","partials":{"partial":"*{{text}}*"}},{"name":"Recursion","data":{"content":"X","nodes":[{"content":"Y","nodes":[]}]},"expected":"X<Y<>>","template":"{{>node}}","desc":"The greater-than operator should properly recurse.","partials":{"node":"{{content}}<{{#nodes}}{{>node}}{{/nodes}}>"}},{"name":"Surrounding Whitespace","data":{},"expected":"| \t|\t |","template":"| {{>partial}} |","desc":"The greater-than operator should not alter surrounding whitespace.","partials":{"partial":"\t|\t"}},{"name":"Inline Indentation","data":{"data":"|"},"expected":" | >\n>\n","template":" {{data}} {{> partial}}\n","desc":"Whitespace should be left untouched.","partials":{"partial":">\n>"}},{"name":"Standalone Line Endings","data":{},"expected":"|\r\n>|","template":"|\r\n{{>partial}}\r\n|","desc":"\"\\r\\n\" should be considered a newline for standalone tags.","partials":{"partial":">"}},{"name":"Standalone Without Previous Line","data":{},"expected":" >\n >>","template":" {{>partial}}\n>","desc":"Standalone tags should not require a newline to precede them.","partials":{"partial":">\n>"}},{"name":"Standalone Without Newline","data":{},"expected":">\n >\n >","template":">\n {{>partial}}","desc":"Standalone tags should not require a newline to follow them.","partials":{"partial":">\n>"}},{"name":"Standalone Indentation","data":{"content":"<\n->"},"expected":"\\\n |\n <\n->\n |\n/\n","template":"\\\n {{>partial}}\n/\n","desc":"Each line of the partial should be indented before rendering.","partials":{"partial":"|\n{{{content}}}\n|\n"}},{"name":"Padding Whitespace","data":{"boolean":true},"expected":"|[]|","template":"|{{> partial }}|","desc":"Superfluous in-tag whitespace should be ignored.","partials":{"partial":"[]"}}]} |
Added testdata/mustache/sections.json.
> | 1 | {"__ATTN__":"Do not edit this file; changes belong in the appropriate YAML file.","overview":"Section tags and End Section tags are used in combination to wrap a section\nof the template for iteration\n\nThese tags' content MUST be a non-whitespace character sequence NOT\ncontaining the current closing delimiter; each Section tag MUST be followed\nby an End Section tag with the same content within the same section.\n\nThis tag's content names the data to replace the tag. Name resolution is as\nfollows:\n 1) Split the name on periods; the first part is the name to resolve, any\n remaining parts should be retained.\n 2) Walk the context stack from top to bottom, finding the first context\n that is a) a hash containing the name as a key OR b) an object responding\n to a method with the given name.\n 3) If the context is a hash, the data is the value associated with the\n name.\n 4) If the context is an object and the method with the given name has an\n arity of 1, the method SHOULD be called with a String containing the\n unprocessed contents of the sections; the data is the value returned.\n 5) Otherwise, the data is the value returned by calling the method with\n the given name.\n 6) If any name parts were retained in step 1, each should be resolved\n against a context stack containing only the result from the former\n resolution. If any part fails resolution, the result should be considered\n falsey, and should interpolate as the empty string.\nIf the data is not of a list type, it is coerced into a list as follows: if\nthe data is truthy (e.g. `!!data == true`), use a single-element list\ncontaining the data, otherwise use an empty list.\n\nFor each element in the data list, the element MUST be pushed onto the\ncontext stack, the section MUST be rendered, and the element MUST be popped\noff the context stack.\n\nSection and End Section tags SHOULD be treated as standalone when\nappropriate.\n","tests":[{"name":"Truthy","data":{"boolean":true},"expected":"\"This should be rendered.\"","template":"\"{{#boolean}}This should be rendered.{{/boolean}}\"","desc":"Truthy sections should have their contents rendered."},{"name":"Falsey","data":{"boolean":false},"expected":"\"\"","template":"\"{{#boolean}}This should not be rendered.{{/boolean}}\"","desc":"Falsey sections should have their contents omitted."},{"name":"Context","data":{"context":{"name":"Joe"}},"expected":"\"Hi Joe.\"","template":"\"{{#context}}Hi {{name}}.{{/context}}\"","desc":"Objects and hashes should be pushed onto the context stack."},{"name":"Deeply Nested Contexts","data":{"a":{"one":1},"b":{"two":2},"c":{"three":3},"d":{"four":4},"e":{"five":5}},"expected":"1\n121\n12321\n1234321\n123454321\n1234321\n12321\n121\n1\n","template":"{{#a}}\n{{one}}\n{{#b}}\n{{one}}{{two}}{{one}}\n{{#c}}\n{{one}}{{two}}{{three}}{{two}}{{one}}\n{{#d}}\n{{one}}{{two}}{{three}}{{four}}{{three}}{{two}}{{one}}\n{{#e}}\n{{one}}{{two}}{{three}}{{four}}{{five}}{{four}}{{three}}{{two}}{{one}}\n{{/e}}\n{{one}}{{two}}{{three}}{{four}}{{three}}{{two}}{{one}}\n{{/d}}\n{{one}}{{two}}{{three}}{{two}}{{one}}\n{{/c}}\n{{one}}{{two}}{{one}}\n{{/b}}\n{{one}}\n{{/a}}\n","desc":"All elements on the context stack should be accessible."},{"name":"List","data":{"list":[{"item":1},{"item":2},{"item":3}]},"expected":"\"123\"","template":"\"{{#list}}{{item}}{{/list}}\"","desc":"Lists should be iterated; list items should visit the context stack."},{"name":"Empty List","data":{"list":[]},"expected":"\"\"","template":"\"{{#list}}Yay lists!{{/list}}\"","desc":"Empty lists should behave like falsey values."},{"name":"Doubled","data":{"two":"second","bool":true},"expected":"* first\n* second\n* third\n","template":"{{#bool}}\n* first\n{{/bool}}\n* {{two}}\n{{#bool}}\n* third\n{{/bool}}\n","desc":"Multiple sections per template should be permitted."},{"name":"Nested (Truthy)","data":{"bool":true},"expected":"| A B C D E |","template":"| A {{#bool}}B {{#bool}}C{{/bool}} D{{/bool}} E |","desc":"Nested truthy sections should have their contents rendered."},{"name":"Nested (Falsey)","data":{"bool":false},"expected":"| A E |","template":"| A {{#bool}}B {{#bool}}C{{/bool}} D{{/bool}} E |","desc":"Nested falsey sections should be omitted."},{"name":"Context Misses","data":{},"expected":"[]","template":"[{{#missing}}Found key 'missing'!{{/missing}}]","desc":"Failed context lookups should be considered falsey."},{"name":"Implicit Iterator - String","data":{"list":["a","b","c","d","e"]},"expected":"\"(a)(b)(c)(d)(e)\"","template":"\"{{#list}}({{.}}){{/list}}\"","desc":"Implicit iterators should directly interpolate strings."},{"name":"Implicit Iterator - Integer","data":{"list":[1,2,3,4,5]},"expected":"\"(1)(2)(3)(4)(5)\"","template":"\"{{#list}}({{.}}){{/list}}\"","desc":"Implicit iterators should cast integers to strings and interpolate."},{"name":"Implicit Iterator - Decimal","data":{"list":[1.1,2.2,3.3,4.4,5.5]},"expected":"\"(1.1)(2.2)(3.3)(4.4)(5.5)\"","template":"\"{{#list}}({{.}}){{/list}}\"","desc":"Implicit iterators should cast decimals to strings and interpolate."},{"name":"Implicit Iterator - Array","desc":"Implicit iterators should allow iterating over nested arrays.","data":{"list":[[1,2,3],["a","b","c"]]},"template":"\"{{#list}}({{#.}}{{.}}{{/.}}){{/list}}\"","expected":"\"(123)(abc)\""},{"name":"Dotted Names - Truthy","data":{"a":{"b":{"c":true}}},"expected":"\"Here\" == \"Here\"","template":"\"{{#a.b.c}}Here{{/a.b.c}}\" == \"Here\"","desc":"Dotted names should be valid for Section tags."},{"name":"Dotted Names - Falsey","data":{"a":{"b":{"c":false}}},"expected":"\"\" == \"\"","template":"\"{{#a.b.c}}Here{{/a.b.c}}\" == \"\"","desc":"Dotted names should be valid for Section tags."},{"name":"Dotted Names - Broken Chains","data":{"a":{}},"expected":"\"\" == \"\"","template":"\"{{#a.b.c}}Here{{/a.b.c}}\" == \"\"","desc":"Dotted names that cannot be resolved should be considered falsey."},{"name":"Surrounding Whitespace","data":{"boolean":true},"expected":" | \t|\t | \n","template":" | {{#boolean}}\t|\t{{/boolean}} | \n","desc":"Sections should not alter surrounding whitespace."},{"name":"Internal Whitespace","data":{"boolean":true},"expected":" | \n | \n","template":" | {{#boolean}} {{! Important Whitespace }}\n {{/boolean}} | \n","desc":"Sections should not alter internal whitespace."},{"name":"Indented Inline Sections","data":{"boolean":true},"expected":" YES\n GOOD\n","template":" {{#boolean}}YES{{/boolean}}\n {{#boolean}}GOOD{{/boolean}}\n","desc":"Single-line sections should not alter surrounding whitespace."},{"name":"Standalone Lines","data":{"boolean":true},"expected":"| This Is\n|\n| A Line\n","template":"| This Is\n{{#boolean}}\n|\n{{/boolean}}\n| A Line\n","desc":"Standalone lines should be removed from the template."},{"name":"Indented Standalone Lines","data":{"boolean":true},"expected":"| This Is\n|\n| A Line\n","template":"| This Is\n {{#boolean}}\n|\n {{/boolean}}\n| A Line\n","desc":"Indented standalone lines should be removed from the template."},{"name":"Standalone Line Endings","data":{"boolean":true},"expected":"|\r\n|","template":"|\r\n{{#boolean}}\r\n{{/boolean}}\r\n|","desc":"\"\\r\\n\" should be considered a newline for standalone tags."},{"name":"Standalone Without Previous Line","data":{"boolean":true},"expected":"#\n/","template":" {{#boolean}}\n#{{/boolean}}\n/","desc":"Standalone tags should not require a newline to precede them."},{"name":"Standalone Without Newline","data":{"boolean":true},"expected":"#\n/\n","template":"#{{#boolean}}\n/\n {{/boolean}}","desc":"Standalone tags should not require a newline to follow them."},{"name":"Padding","data":{"boolean":true},"expected":"|=|","template":"|{{# boolean }}={{/ boolean }}|","desc":"Superfluous in-tag whitespace should be ignored."}]} |
Added testdata/mustache/~lambdas.json.
> | 1 | {"__ATTN__":"Do not edit this file; changes belong in the appropriate YAML file.","overview":"Lambdas are a special-cased data type for use in interpolations and\nsections.\n\nWhen used as the data value for an Interpolation tag, the lambda MUST be\ntreatable as an arity 0 function, and invoked as such. The returned value\nMUST be rendered against the default delimiters, then interpolated in place\nof the lambda.\n\nWhen used as the data value for a Section tag, the lambda MUST be treatable\nas an arity 1 function, and invoked as such (passing a String containing the\nunprocessed section contents). The returned value MUST be rendered against\nthe current delimiters, then interpolated in place of the section.\n","tests":[{"name":"Interpolation","data":{"lambda":{"php":"return \"world\";","clojure":"(fn [] \"world\")","__tag__":"code","perl":"sub { \"world\" }","python":"lambda: \"world\"","ruby":"proc { \"world\" }","js":"function() { return \"world\" }"}},"expected":"Hello, world!","template":"Hello, {{lambda}}!","desc":"A lambda's return value should be interpolated."},{"name":"Interpolation - Expansion","data":{"planet":"world","lambda":{"php":"return \"{{planet}}\";","clojure":"(fn [] \"{{planet}}\")","__tag__":"code","perl":"sub { \"{{planet}}\" }","python":"lambda: \"{{planet}}\"","ruby":"proc { \"{{planet}}\" }","js":"function() { return \"{{planet}}\" }"}},"expected":"Hello, world!","template":"Hello, {{lambda}}!","desc":"A lambda's return value should be parsed."},{"name":"Interpolation - Alternate Delimiters","data":{"planet":"world","lambda":{"php":"return \"|planet| => {{planet}}\";","clojure":"(fn [] \"|planet| => {{planet}}\")","__tag__":"code","perl":"sub { \"|planet| => {{planet}}\" }","python":"lambda: \"|planet| => {{planet}}\"","ruby":"proc { \"|planet| => {{planet}}\" }","js":"function() { return \"|planet| => {{planet}}\" }"}},"expected":"Hello, (|planet| => world)!","template":"{{= | | =}}\nHello, (|&lambda|)!","desc":"A lambda's return value should parse with the default delimiters."},{"name":"Interpolation - Multiple Calls","data":{"lambda":{"php":"global $calls; return ++$calls;","clojure":"(def g (atom 0)) (fn [] (swap! g inc))","__tag__":"code","perl":"sub { no strict; $calls += 1 }","python":"lambda: globals().update(calls=globals().get(\"calls\",0)+1) or calls","ruby":"proc { $calls ||= 0; $calls += 1 }","js":"function() { return (g=(function(){return this})()).calls=(g.calls||0)+1 }"}},"expected":"1 == 2 == 3","template":"{{lambda}} == {{{lambda}}} == {{lambda}}","desc":"Interpolated lambdas should not be cached."},{"name":"Escaping","data":{"lambda":{"php":"return \">\";","clojure":"(fn [] \">\")","__tag__":"code","perl":"sub { \">\" }","python":"lambda: \">\"","ruby":"proc { \">\" }","js":"function() { return \">\" }"}},"expected":"<>>","template":"<{{lambda}}{{{lambda}}}","desc":"Lambda results should be appropriately escaped."},{"name":"Section","data":{"x":"Error!","lambda":{"php":"return ($text == \"{{x}}\") ? \"yes\" : \"no\";","clojure":"(fn [text] (if (= text \"{{x}}\") \"yes\" \"no\"))","__tag__":"code","perl":"sub { $_[0] eq \"{{x}}\" ? \"yes\" : \"no\" }","python":"lambda text: text == \"{{x}}\" and \"yes\" or \"no\"","ruby":"proc { |text| text == \"{{x}}\" ? \"yes\" : \"no\" }","js":"function(txt) { return (txt == \"{{x}}\" ? \"yes\" : \"no\") }"}},"expected":"<yes>","template":"<{{#lambda}}{{x}}{{/lambda}}>","desc":"Lambdas used for sections should receive the raw section string."},{"name":"Section - Expansion","data":{"planet":"Earth","lambda":{"php":"return $text . \"{{planet}}\" . $text;","clojure":"(fn [text] (str text \"{{planet}}\" text))","__tag__":"code","perl":"sub { $_[0] . \"{{planet}}\" . $_[0] }","python":"lambda text: \"%s{{planet}}%s\" % (text, text)","ruby":"proc { |text| \"#{text}{{planet}}#{text}\" }","js":"function(txt) { return txt + \"{{planet}}\" + txt }"}},"expected":"<-Earth->","template":"<{{#lambda}}-{{/lambda}}>","desc":"Lambdas used for sections should have their results parsed."},{"name":"Section - Alternate Delimiters","data":{"planet":"Earth","lambda":{"php":"return $text . \"{{planet}} => |planet|\" . $text;","clojure":"(fn [text] (str text \"{{planet}} => |planet|\" text))","__tag__":"code","perl":"sub { $_[0] . \"{{planet}} => |planet|\" . $_[0] }","python":"lambda text: \"%s{{planet}} => |planet|%s\" % (text, text)","ruby":"proc { |text| \"#{text}{{planet}} => |planet|#{text}\" }","js":"function(txt) { return txt + \"{{planet}} => |planet|\" + txt }"}},"expected":"<-{{planet}} => Earth->","template":"{{= | | =}}<|#lambda|-|/lambda|>","desc":"Lambdas used for sections should parse with the current delimiters."},{"name":"Section - Multiple Calls","data":{"lambda":{"php":"return \"__\" . $text . \"__\";","clojure":"(fn [text] (str \"__\" text \"__\"))","__tag__":"code","perl":"sub { \"__\" . $_[0] . \"__\" }","python":"lambda text: \"__%s__\" % (text)","ruby":"proc { |text| \"__#{text}__\" }","js":"function(txt) { return \"__\" + txt + \"__\" }"}},"expected":"__FILE__ != __LINE__","template":"{{#lambda}}FILE{{/lambda}} != {{#lambda}}LINE{{/lambda}}","desc":"Lambdas used for sections should not be cached."},{"name":"Inverted Section","data":{"static":"static","lambda":{"php":"return false;","clojure":"(fn [text] false)","__tag__":"code","perl":"sub { 0 }","python":"lambda text: 0","ruby":"proc { |text| false }","js":"function(txt) { return false }"}},"expected":"<>","template":"<{{^lambda}}{{static}}{{/lambda}}>","desc":"Lambdas used for inverted sections should be considered truthy."}]} |
Deleted testdata/testbox/00000999999999.zettel.
|
| < < < < < < < < < < < |
Deleted testdata/testbox/20230929102100.zettel.
|
| < < < < < < < |
Changes to tests/client/client_test.go.
1 2 3 4 5 6 7 8 | //----------------------------------------------------------------------------- // Copyright (c) 2021-present Detlef Stern // // This file is part of Zettelstore. // // Zettelstore client is licensed under the latest version of the EUPL // (European Union Public License). Please see file LICENSE.txt for your rights // and obligations under this license. | < < < < < | | < < | | > > > > > > | | | | | | | | | < | | | | | | < | | | | | < < < | | | | | | > > > > > > > > > | | | > > > > | | | < < | | | | | | | > > > > > > > > > | > > > | | > | | < | | < < | < < < | > > > > | | | 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 | //----------------------------------------------------------------------------- // Copyright (c) 2021-present Detlef Stern // // This file is part of Zettelstore. // // Zettelstore client is licensed under the latest version of the EUPL // (European Union Public License). Please see file LICENSE.txt for your rights // and obligations under this license. //----------------------------------------------------------------------------- // Package client provides a client for accessing the Zettelstore via its API. package client_test import ( "context" "flag" "fmt" "net/http" "net/url" "strconv" "testing" "zettelstore.de/c/api" "zettelstore.de/c/client" "zettelstore.de/z/kernel" ) func nextZid(zid api.ZettelID) api.ZettelID { numVal, err := strconv.ParseUint(string(zid), 10, 64) if err != nil { panic(err) } return api.ZettelID(fmt.Sprintf("%014d", numVal+1)) } func TestNextZid(t *testing.T) { testCases := []struct { zid, exp api.ZettelID }{ {api.ZettelID("00000000000000"), api.ZettelID("00000000000001")}, } for i, tc := range testCases { if got := nextZid(tc.zid); got != tc.exp { t.Errorf("%d: zid=%q, exp=%q, got=%q", i, tc.zid, tc.exp, got) } } } func TestListZettel(t *testing.T) { const ( ownerZettel = 47 configRoleZettel = 29 writerZettel = ownerZettel - 23 readerZettel = ownerZettel - 23 creatorZettel = 7 publicZettel = 4 ) testdata := []struct { user string exp int }{ {"", publicZettel}, {"creator", creatorZettel}, {"reader", readerZettel}, {"writer", writerZettel}, {"owner", ownerZettel}, } t.Parallel() c := getClient() for i, tc := range testdata { t.Run(fmt.Sprintf("User %d/%q", i, tc.user), func(tt *testing.T) { c.SetAuth(tc.user, tc.user) q, h, l, err := c.ListZettelJSON(context.Background(), "") if err != nil { tt.Error(err) return } if q != "" { tt.Errorf("Query should be empty, but is %q", q) } if h != "" { tt.Errorf("Human should be empty, but is %q", q) } got := len(l) if got != tc.exp { tt.Errorf("List of length %d expected, but got %d\n%v", tc.exp, got, l) } }) } q, h, l, err := c.ListZettelJSON(context.Background(), api.KeyRole+api.SearchOperatorHas+api.ValueRoleConfiguration) if err != nil { t.Error(err) return } expQ := "role:configuration" if q != expQ { t.Errorf("Query should be %q, but is %q", expQ, q) } expH := "role HAS configuration" if h != expH { t.Errorf("Human should be %q, but is %q", expH, h) } got := len(l) if got != configRoleZettel { t.Errorf("List of length %d expected, but got %d\n%v", configRoleZettel, got, l) } pl, err := c.ListZettel(context.Background(), api.KeyRole+api.SearchOperatorHas+api.ValueRoleConfiguration) if err != nil { t.Error(err) return } compareZettelList(t, pl, l) } func compareZettelList(t *testing.T, pl [][]byte, l []api.ZidMetaJSON) { t.Helper() if len(pl) != len(l) { t.Errorf("Different list lenght: Plain=%d, JSON=%d", len(pl), len(l)) } else { for i, line := range pl { if got := api.ZettelID(line[:14]); got != l[i].ID { t.Errorf("%d: JSON=%q, got=%q", i, l[i].ID, got) } } } } func TestGetZettelJSON(t *testing.T) { t.Parallel() c := getClient() c.SetAuth("owner", "owner") z, err := c.GetZettelJSON(context.Background(), api.ZidDefaultHome) if err != nil { t.Error(err) return } if m := z.Meta; len(m) == 0 { t.Errorf("Exptected non-empty meta, but got %v", z.Meta) } if z.Content == "" || z.Encoding != "" { t.Errorf("Expect non-empty content, but empty encoding (got %q)", z.Encoding) } m, err := c.GetMeta(context.Background(), api.ZidDefaultHome) if err != nil { t.Error(err) return } if len(m) != len(z.Meta) { t.Errorf("Pure meta differs from zettel meta: %s vs %s", m, z.Meta) return } for k, v := range z.Meta { got, ok := m[k] if !ok { t.Errorf("Pure meta has no key %q", k) continue } if got != v { t.Errorf("Pure meta has different value for key %q: %q vs %q", k, got, v) } } } func TestGetParsedEvaluatedZettel(t *testing.T) { t.Parallel() c := getClient() c.SetAuth("owner", "owner") encodings := []api.EncodingEnum{ api.EncoderHTML, api.EncoderSexpr, api.EncoderText, } for _, enc := range encodings { content, err := c.GetParsedZettel(context.Background(), api.ZidDefaultHome, enc) if err != nil { t.Error(err) continue } if len(content) == 0 { t.Errorf("Empty content for parsed encoding %v", enc) } content, err = c.GetEvaluatedZettel(context.Background(), api.ZidDefaultHome, enc) if err != nil { t.Error(err) continue } if len(content) == 0 { t.Errorf("Empty content for evaluated encoding %v", enc) } } } func checkZid(t *testing.T, expected, got api.ZettelID) bool { t.Helper() if expected != got { t.Errorf("Expected a Zid %q, but got %q", expected, got) return false } return true } func checkListZid(t *testing.T, l []api.ZidMetaJSON, pos int, expected api.ZettelID) { t.Helper() if got := api.ZettelID(l[pos].ID); got != expected { t.Errorf("Expected result[%d]=%v, but got %v", pos, expected, got) } } func TestGetZettelOrder(t *testing.T) { t.Parallel() c := getClient() c.SetAuth("owner", "owner") rl, err := c.GetZettelOrder(context.Background(), api.ZidTOCNewTemplate) if err != nil { t.Error(err) return } if !checkZid(t, api.ZidTOCNewTemplate, rl.ID) { return } l := rl.List if got := len(l); got != 2 { t.Errorf("Expected list of length 2, got %d", got) return } checkListZid(t, l, 0, api.ZidTemplateNewZettel) checkListZid(t, l, 1, api.ZidTemplateNewUser) } func TestGetZettelContext(t *testing.T) { const ( allUserZid = api.ZettelID("20211019200500") ownerZid = api.ZettelID("20210629163300") writerZid = api.ZettelID("20210629165000") readerZid = api.ZettelID("20210629165024") creatorZid = api.ZettelID("20210629165050") limitAll = 3 ) t.Parallel() c := getClient() c.SetAuth("owner", "owner") rl, err := c.GetZettelContext(context.Background(), ownerZid, client.DirBoth, 0, limitAll) if err != nil { t.Error(err) return } if !checkZid(t, ownerZid, rl.ID) { return } l := rl.List if got := len(l); got != limitAll { t.Errorf("Expected list of length %d, got %d", limitAll, got) t.Error(rl) return } checkListZid(t, l, 0, allUserZid) // checkListZid(t, l, 1, writerZid) // checkListZid(t, l, 2, readerZid) checkListZid(t, l, 1, creatorZid) rl, err = c.GetZettelContext(context.Background(), ownerZid, client.DirBackward, 0, 0) if err != nil { t.Error(err) return } if !checkZid(t, ownerZid, rl.ID) { return } l = rl.List if got, exp := len(l), 4; got != exp { t.Errorf("Expected list of length %d, got %d", exp, got) return } checkListZid(t, l, 0, allUserZid) } func TestGetUnlinkedReferences(t *testing.T) { t.Parallel() c := getClient() c.SetAuth("owner", "owner") zl, err := c.GetUnlinkedReferences(context.Background(), api.ZidDefaultHome, nil) if err != nil { t.Error(err) return } if !checkZid(t, api.ZidDefaultHome, zl.ID) { return } l := zl.List if got := len(l); got != 1 { t.Errorf("Expected list of length 1, got %d", got) return } } func failNoErrorOrNoCode(t *testing.T, err error, goodCode int) bool { if err != nil { if cErr, ok := err.(*client.Error); ok { |
︙ | ︙ | |||
318 319 320 321 322 323 324 | } } func TestListTags(t *testing.T) { t.Parallel() c := getClient() c.SetAuth("owner", "owner") | | | | | | | | | < < < < < < < < < < < < < < < < < < < < < | | | | | < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < | < < < < < < < < < < < < < < < < < < < | 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 | } } func TestListTags(t *testing.T) { t.Parallel() c := getClient() c.SetAuth("owner", "owner") tm, err := c.QueryMapMeta(context.Background(), api.ActionSeparator+api.KeyTags) if err != nil { t.Error(err) return } tags := []struct { key string size int }{ {"#invisible", 1}, {"#user", 4}, {"#test", 4}, } if len(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.QueryMapMeta(context.Background(), api.ActionSeparator+api.KeyRole) 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 _, id := range exp { if _, found := rl[id]; !found { t.Errorf("Role map expected key %q", id) } } } func TestVersion(t *testing.T) { t.Parallel() c := getClient() ver, err := c.GetVersionJSON(context.Background()) if err != nil { t.Error(err) return } if ver.Major != -1 || ver.Minor != -1 || ver.Patch != -1 || ver.Info != kernel.CoreDefaultVersion || ver.Hash != "" { t.Error(ver) } } var baseURL string func init() { flag.StringVar(&baseURL, "base-url", "", "Base URL") } func getClient() *client.Client { |
︙ | ︙ |
Changes to tests/client/crud_test.go.
1 2 3 4 5 6 7 8 | //----------------------------------------------------------------------------- // Copyright (c) 2021-present Detlef Stern // // This file is part of Zettelstore. // // Zettelstore client is licensed under the latest version of the EUPL // (European Union Public License). Please see file LICENSE.txt for your rights // and obligations under this license. | < < < | | < < | | 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 | //----------------------------------------------------------------------------- // Copyright (c) 2021-present Detlef Stern // // This file is part of Zettelstore. // // Zettelstore client is licensed under the latest version of the EUPL // (European Union Public License). Please see file LICENSE.txt for your rights // and obligations under this license. //----------------------------------------------------------------------------- package client_test import ( "context" "strings" "testing" "zettelstore.de/c/api" "zettelstore.de/c/client" ) // --------------------------------------------------------------------------- // Tests that change the Zettelstore must nor run parallel to other tests. func TestCreateGetRenameDeleteZettel(t *testing.T) { // Is not to be allowed to run in parallel with other tests. zettel := `title: A Test Example content.` c := getClient() c.SetAuth("owner", "owner") zid, err := c.CreateZettel(context.Background(), []byte(zettel)) |
︙ | ︙ | |||
50 51 52 53 54 55 56 | } exp := `title: A Test Example content.` if string(data) != exp { t.Errorf("Expected zettel data: %q, but got %q", exp, data) } | > > > > > | > | | | > > > > > > | > | | | | | | | | | | | | | | | | | | | | | | | | 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 | } exp := `title: A Test Example content.` if string(data) != exp { t.Errorf("Expected zettel data: %q, but got %q", exp, data) } newZid := nextZid(zid) err = c.RenameZettel(context.Background(), zid, newZid) if err != nil { t.Error("Cannot rename", zid, ":", err) newZid = zid } doDelete(t, c, newZid) } func TestCreateGetRenameDeleteZettelJSON(t *testing.T) { // Is not to be allowed to run in parallel with other tests. c := getClient() c.SetAuth("creator", "creator") zid, err := c.CreateZettelJSON(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 := nextZid(zid) c.SetAuth("owner", "owner") err = c.RenameZettel(context.Background(), zid, newZid) if err != nil { t.Error("Cannot rename", zid, ":", err) newZid = zid } c.SetAuth("owner", "owner") doDelete(t, c, newZid) } func TestCreateGetDeleteZettelJSON(t *testing.T) { // Is not to be allowed to run in parallel with other tests. c := getClient() c.SetAuth("owner", "owner") wrongModified := "19691231115959" zid, err := c.CreateZettelJSON(context.Background(), &api.ZettelDataJSON{ Meta: api.ZettelMeta{ api.KeyTitle: "A\nTitle", // \n must be converted into a space api.KeyModified: wrongModified, }, }) if err != nil { t.Error("Cannot create zettel:", err) return } z, err := c.GetZettelJSON(context.Background(), zid) if err != nil { t.Error("Cannot get zettel:", zid, err) } else { exp := "A Title" if got := z.Meta[api.KeyTitle]; got != exp { t.Errorf("Expected title %q, but got %q", exp, got) } if got := z.Meta[api.KeyModified]; got != "" { t.Errorf("Create allowed to set the modified key: %q", got) } } doDelete(t, c, zid) } func TestUpdateZettel(t *testing.T) { c := getClient() c.SetAuth("owner", "owner") z, err := c.GetZettel(context.Background(), api.ZidDefaultHome, api.PartZettel) if err != nil { t.Error(err) return } if !strings.HasPrefix(string(z), "title: Home\n") { t.Error("Got unexpected zettel", z) return } newZettel := `title: Empty Home role: zettel syntax: zmk Empty` err = c.UpdateZettel(context.Background(), api.ZidDefaultHome, []byte(newZettel)) if err != nil { t.Error(err) return } zt, err := c.GetZettel(context.Background(), api.ZidDefaultHome, api.PartZettel) if err != nil { t.Error(err) return } if string(zt) != newZettel { t.Errorf("Expected zettel %q, got %q", newZettel, zt) } // Must delete to clean up for next tests doDelete(t, c, api.ZidDefaultHome) } func TestUpdateZettelJSON(t *testing.T) { c := getClient() c.SetAuth("writer", "writer") z, err := c.GetZettelJSON(context.Background(), api.ZidDefaultHome) if err != nil { t.Error(err) return } if got := z.Meta[api.KeyTitle]; got != "Home" { t.Errorf("Title of zettel is not \"Home\", but %q", got) return } newTitle := "New Home" z.Meta[api.KeyTitle] = newTitle wrongModified := "19691231235959" z.Meta[api.KeyModified] = wrongModified err = c.UpdateZettelJSON(context.Background(), api.ZidDefaultHome, z) if err != nil { t.Error(err) return } zt, err := c.GetZettelJSON(context.Background(), api.ZidDefaultHome) if err != nil { t.Error(err) return } if got := zt.Meta[api.KeyTitle]; got != newTitle { t.Errorf("Title of zettel is not %q, but %q", newTitle, got) } if got := zt.Meta[api.KeyModified]; got == wrongModified { t.Errorf("Update did not change the modified key: %q", got) } // Must delete to clean up for next tests c.SetAuth("owner", "owner") doDelete(t, c, api.ZidDefaultHome) } func doDelete(t *testing.T, c *client.Client, zid api.ZettelID) { err := c.DeleteZettel(context.Background(), zid) if err != nil { t.Helper() t.Error("Cannot delete", zid, ":", err) } } |
Changes to tests/client/embed_test.go.
1 2 3 4 5 6 7 8 | //----------------------------------------------------------------------------- // Copyright (c) 2021-present Detlef Stern // // This file is part of Zettelstore. // // Zettelstore is licensed under the latest version of the EUPL (European Union // Public License). Please see file LICENSE.txt for your rights and obligations // under this license. | < < < | < | | | | | | | | | 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-present Detlef Stern // // This file is part of Zettelstore. // // Zettelstore is licensed under the latest version of the EUPL (European Union // Public License). Please see file LICENSE.txt for your rights and obligations // under this license. //----------------------------------------------------------------------------- package client_test import ( "context" "strings" "testing" "zettelstore.de/c/api" ) const ( abcZid = api.ZettelID("20211020121000") abc10Zid = api.ZettelID("20211020121100") ) func TestZettelTransclusion(t *testing.T) { t.Parallel() c := getClient() c.SetAuth("owner", "owner") const abc10000Zid = api.ZettelID("20211020121400") contentMap := map[api.ZettelID]int{ abcZid: 1, abc10Zid: 10, api.ZettelID("20211020121145"): 100, api.ZettelID("20211020121300"): 1000, } content, err := c.GetZettel(context.Background(), abcZid, api.PartContent) if err != nil { t.Error(err) return } baseContent := string(content) |
︙ | ︙ | |||
77 78 79 80 81 82 83 | } func TestZettelTransclusionNoPrivilegeEscalation(t *testing.T) { t.Parallel() c := getClient() c.SetAuth("reader", "reader") | | | 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 | } func TestZettelTransclusionNoPrivilegeEscalation(t *testing.T) { t.Parallel() c := getClient() c.SetAuth("reader", "reader") zettelData, err := c.GetZettelJSON(context.Background(), api.ZidEmoji) if err != nil { t.Error(err) return } expectedEnc := "base64" if got := zettelData.Encoding; expectedEnc != got { t.Errorf("Zettel %q: encoding %q expected, but got %q", abcZid, expectedEnc, got) |
︙ | ︙ | |||
119 120 121 122 123 124 125 | func TestRecursiveTransclusion(t *testing.T) { t.Parallel() c := getClient() c.SetAuth("owner", "owner") const ( | | | | | | | | | | | | 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 | func TestRecursiveTransclusion(t *testing.T) { t.Parallel() c := getClient() c.SetAuth("owner", "owner") const ( selfRecursiveZid = api.ZettelID("20211020182600") indirectRecursive1Zid = api.ZettelID("20211020183700") indirectRecursive2Zid = api.ZettelID("20211020183800") ) recursiveZettel := map[api.ZettelID]api.ZettelID{ selfRecursiveZid: selfRecursiveZid, indirectRecursive1Zid: indirectRecursive2Zid, indirectRecursive2Zid: indirectRecursive1Zid, } for zid, errZid := range recursiveZettel { content, err := c.GetEvaluatedZettel(context.Background(), zid, api.EncoderHTML) if err != nil { t.Error(err) continue } sContent := string(content) checkContentContains(t, zid, sContent, "Recursive transclusion") checkContentContains(t, zid, sContent, string(errZid)) } } func TestNothingToTransclude(t *testing.T) { t.Parallel() c := getClient() c.SetAuth("owner", "owner") const ( transZid = api.ZettelID("20211020184342") emptyZid = api.ZettelID("20211020184300") ) content, err := c.GetEvaluatedZettel(context.Background(), transZid, api.EncoderHTML) if err != nil { t.Error(err) return } sContent := string(content) checkContentContains(t, transZid, sContent, "<!-- Nothing to transclude") checkContentContains(t, transZid, sContent, string(emptyZid)) } func TestSelfEmbedRef(t *testing.T) { t.Parallel() c := getClient() c.SetAuth("owner", "owner") const selfEmbedZid = api.ZettelID("20211020185400") content, err := c.GetEvaluatedZettel(context.Background(), selfEmbedZid, api.EncoderHTML) if err != nil { t.Error(err) return } checkContentContains(t, selfEmbedZid, string(content), "Self embed reference") } func checkContentContains(t *testing.T, zid api.ZettelID, content, expected string) { if !strings.Contains(content, expected) { t.Helper() t.Errorf("Zettel %q should contain %q, but does not: %q", zid, expected, content) } } |
Changes to tests/markdown_test.go.
1 2 3 4 5 6 7 8 | //----------------------------------------------------------------------------- // Copyright (c) 2020-present Detlef Stern // // This file is part of Zettelstore. // // Zettelstore is licensed under the latest version of the EUPL (European Union // Public License). Please see file LICENSE.txt for your rights and obligations // under this license. | < < < < < < | | | | | | | | | | | | > | | | | 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 | //----------------------------------------------------------------------------- // Copyright (c) 2020-present Detlef Stern // // This file is part of Zettelstore. // // Zettelstore is licensed under the latest version of the EUPL (European Union // Public License). Please see file LICENSE.txt for your rights and obligations // under this license. //----------------------------------------------------------------------------- package tests import ( "bytes" "encoding/json" "fmt" "os" "strings" "testing" "zettelstore.de/c/api" "zettelstore.de/z/ast" "zettelstore.de/z/config" "zettelstore.de/z/domain/meta" "zettelstore.de/z/encoder" _ "zettelstore.de/z/encoder/htmlenc" _ "zettelstore.de/z/encoder/mdenc" _ "zettelstore.de/z/encoder/sexprenc" _ "zettelstore.de/z/encoder/shtmlenc" _ "zettelstore.de/z/encoder/textenc" _ "zettelstore.de/z/encoder/zmkenc" "zettelstore.de/z/input" "zettelstore.de/z/parser" _ "zettelstore.de/z/parser/markdown" _ "zettelstore.de/z/parser/zettelmark" ) type markdownTestCase struct { Markdown string `json:"markdown"` HTML string `json:"html"` Example int `json:"example"` StartLine int `json:"start_line"` EndLine int `json:"end_line"` Section string `json:"section"` } func TestEncoderAvailability(t *testing.T) { t.Parallel() encoderMissing := false for _, enc := range encodings { enc := encoder.Create(enc) if enc == nil { t.Errorf("No encoder for %q found", enc) encoderMissing = true } } if encoderMissing { panic("At least one encoder is missing. See test log") |
︙ | ︙ | |||
79 80 81 82 83 84 85 | ast := createMDBlockSlice(tc.Markdown, config.NoHTML) testAllEncodings(t, tc, &ast) testZmkEncoding(t, tc, &ast) } } func createMDBlockSlice(markdown string, hi config.HTMLInsecurity) ast.BlockSlice { | | | | | | | | | | 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 | ast := createMDBlockSlice(tc.Markdown, config.NoHTML) testAllEncodings(t, tc, &ast) testZmkEncoding(t, tc, &ast) } } func createMDBlockSlice(markdown string, hi config.HTMLInsecurity) ast.BlockSlice { return parser.ParseBlocks(input.NewInput([]byte(markdown)), nil, meta.SyntaxMarkdown, hi) } func testAllEncodings(t *testing.T, tc markdownTestCase, ast *ast.BlockSlice) { var sb strings.Builder testID := tc.Example*100 + 1 for _, enc := range encodings { t.Run(fmt.Sprintf("Encode %v %v", enc, testID), func(st *testing.T) { encoder.Create(enc).WriteBlocks(&sb, ast) sb.Reset() }) } } func testZmkEncoding(t *testing.T, tc markdownTestCase, ast *ast.BlockSlice) { zmkEncoder := encoder.Create(api.EncoderZmk) var buf bytes.Buffer testID := tc.Example*100 + 1 t.Run(fmt.Sprintf("Encode zmk %14d", testID), func(st *testing.T) { buf.Reset() zmkEncoder.WriteBlocks(&buf, ast) // gotFirst := buf.String() testID = tc.Example*100 + 2 secondAst := parser.ParseBlocks(input.NewInput(buf.Bytes()), nil, meta.SyntaxZmk, config.NoHTML) buf.Reset() zmkEncoder.WriteBlocks(&buf, &secondAst) gotSecond := buf.String() // if gotFirst != gotSecond { // st.Errorf("\nCMD: %q\n1st: %q\n2nd: %q", tc.Markdown, gotFirst, gotSecond) // } testID = tc.Example*100 + 3 thirdAst := parser.ParseBlocks(input.NewInput(buf.Bytes()), nil, meta.SyntaxZmk, config.NoHTML) buf.Reset() zmkEncoder.WriteBlocks(&buf, &thirdAst) gotThird := buf.String() if gotSecond != gotThird { st.Errorf("\n1st: %q\n2nd: %q", gotSecond, gotThird) } }) } func TestAdditionalMarkdown(t *testing.T) { testcases := []struct { md string exp string }{ {`abc<br>def`, `abc@@<br>@@{="html"}def`}, } zmkEncoder := encoder.Create(api.EncoderZmk) var sb strings.Builder for i, tc := range testcases { ast := createMDBlockSlice(tc.md, config.MarkdownHTML) sb.Reset() zmkEncoder.WriteBlocks(&sb, &ast) got := sb.String() if got != tc.exp { t.Errorf("%d: %q -> %q, but got %q", i, tc.md, tc.exp, got) } } } |
Changes to tests/naughtystrings_test.go.
1 2 3 4 5 6 7 8 | //----------------------------------------------------------------------------- // Copyright (c) 2020-present Detlef Stern // // This file is part of Zettelstore. // // Zettelstore is licensed under the latest version of the EUPL (European Union // Public License). Please see file LICENSE.txt for your rights and obligations // under this license. | < < < < < | | | | | | 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 | //----------------------------------------------------------------------------- // Copyright (c) 2020-present Detlef Stern // // This file is part of Zettelstore. // // Zettelstore is licensed under the latest version of the EUPL (European Union // Public License). Please see file LICENSE.txt for your rights and obligations // under this license. //----------------------------------------------------------------------------- package tests import ( "bufio" "io" "os" "path/filepath" "testing" _ "zettelstore.de/z/cmd" "zettelstore.de/z/domain/meta" "zettelstore.de/z/encoder" "zettelstore.de/z/input" "zettelstore.de/z/parser" ) // Test all parser / encoder with a list of "naughty strings", i.e. unusual strings // that often crash software. func getNaughtyStrings() (result []string, err error) { fpath := filepath.Join("..", "testdata", "naughty", "blns.txt") |
︙ | ︙ | |||
56 57 58 59 60 61 62 | } } return result } func getAllEncoder() (result []encoder.Encoder) { for _, enc := range encoder.GetEncodings() { | | | 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 | } } return result } func getAllEncoder() (result []encoder.Encoder) { for _, enc := range encoder.GetEncodings() { e := encoder.Create(enc) result = append(result, e) } return result } func TestNaughtyStringParser(t *testing.T) { blns, err := getNaughtyStrings() |
︙ | ︙ | |||
80 81 82 83 84 85 86 | } encs := getAllEncoder() if len(encs) == 0 { t.Fatal("no encoder found") } for _, s := range blns { for _, pinfo := range pinfos { | | > > > > > | 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 | } encs := getAllEncoder() if len(encs) == 0 { t.Fatal("no encoder found") } for _, s := range blns { for _, pinfo := range pinfos { bs := pinfo.ParseBlocks(input.NewInput([]byte(s)), &meta.Meta{}, pinfo.Name) is := pinfo.ParseInlines(input.NewInput([]byte(s)), pinfo.Name) for _, enc := range encs { _, err = enc.WriteBlocks(io.Discard, &bs) if err != nil { t.Error(err) } _, err = enc.WriteInlines(io.Discard, &is) if err != nil { t.Error(err) } } } } } |
Changes to tests/regression_test.go.
1 2 3 4 5 6 7 8 | //----------------------------------------------------------------------------- // Copyright (c) 2020-present Detlef Stern // // This file is part of Zettelstore. // // Zettelstore is licensed under the latest version of the EUPL (European Union // Public License). Please see file LICENSE.txt for your rights and obligations // under this license. | < < < < < | | | | | | | | | | | | 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-present Detlef Stern // // This file is part of Zettelstore. // // Zettelstore is licensed under the latest version of the EUPL (European Union // Public License). Please see file LICENSE.txt for your rights and obligations // under this license. //----------------------------------------------------------------------------- // Package tests provides some higher-level tests. package tests import ( "context" "fmt" "io" "net/url" "os" "path/filepath" "strings" "testing" "zettelstore.de/c/api" "zettelstore.de/z/ast" "zettelstore.de/z/box" "zettelstore.de/z/box/manager" "zettelstore.de/z/config" "zettelstore.de/z/domain/meta" "zettelstore.de/z/encoder" "zettelstore.de/z/kernel" "zettelstore.de/z/parser" _ "zettelstore.de/z/box/dirbox" ) var encodings = []api.EncodingEnum{ api.EncoderHTML, api.EncoderSexpr, api.EncoderText, } func getFileBoxes(wd, kind string) (root string, boxes []box.ManagedBox) { root = filepath.Clean(filepath.Join(wd, "..", "testdata", kind)) entries, err := os.ReadDir(root) if err != nil { |
︙ | ︙ | |||
121 122 123 124 125 126 127 | } return u.Path[len(root):] } func checkMetaFile(t *testing.T, resultName string, zn *ast.ZettelNode, enc api.EncodingEnum) { t.Helper() | | | < | | | | | | 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 | } return u.Path[len(root):] } func checkMetaFile(t *testing.T, resultName string, zn *ast.ZettelNode, enc api.EncodingEnum) { t.Helper() if enc := encoder.Create(enc); enc != nil { var sf strings.Builder enc.WriteMeta(&sf, zn.Meta, parser.ParseMetadata) checkFileContent(t, resultName, sf.String()) return } panic(fmt.Sprintf("Unknown writer encoding %q", enc)) } func checkMetaBox(t *testing.T, p box.ManagedBox, wd, boxName string) { ss := p.(box.StartStopper) if err := ss.Start(context.Background()); err != nil { panic(err) } metaList := []*meta.Meta{} err := p.ApplyMeta(context.Background(), func(m *meta.Meta) { metaList = append(metaList, m) }, nil) if err != nil { panic(err) } for _, meta := range metaList { zettel, err2 := p.GetZettel(context.Background(), meta.Zid) if err2 != nil { panic(err2) } z := parser.ParseZettel(context.Background(), zettel, "", testConfig) for _, enc := range encodings { t.Run(fmt.Sprintf("%s::%d(%s)", p.Location(), meta.Zid, enc), func(st *testing.T) { resultName := filepath.Join(wd, "result", "meta", boxName, z.Zid.String()+"."+enc.String()) checkMetaFile(st, resultName, z, enc) }) |
︙ | ︙ | |||
167 168 169 170 171 172 173 | func (*myConfig) AddDefaultValues(_ context.Context, m *meta.Meta) *meta.Meta { return m } func (*myConfig) GetHTMLInsecurity() config.HTMLInsecurity { return config.NoHTML } func (*myConfig) GetListPageSize() int { return 0 } func (*myConfig) GetSiteName() string { return "" } func (*myConfig) GetYAMLHeader() bool { return false } | | | 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 | func (*myConfig) AddDefaultValues(_ context.Context, m *meta.Meta) *meta.Meta { return m } func (*myConfig) GetHTMLInsecurity() config.HTMLInsecurity { return config.NoHTML } func (*myConfig) GetListPageSize() int { return 0 } func (*myConfig) GetSiteName() string { return "" } func (*myConfig) GetYAMLHeader() bool { return false } func (*myConfig) GetZettelFileSyntax() []string { return nil } func (*myConfig) GetSimpleMode() bool { return false } func (*myConfig) GetExpertMode() bool { return false } func (*myConfig) GetVisibility(*meta.Meta) meta.Visibility { return meta.VisibilityPublic } func (*myConfig) GetMaxTransclusions() int { return 1024 } var testConfig = &myConfig{} |
︙ | ︙ |
Added tools/build.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 | //----------------------------------------------------------------------------- // Copyright (c) 2021-present Detlef Stern // // This file is part of Zettelstore. // // Zettelstore is licensed under the latest version of the EUPL (European Union // Public License). Please see file LICENSE.txt for your rights and obligations // under this license. //----------------------------------------------------------------------------- // Package main provides a command to build and run the software. package main import ( "archive/zip" "errors" "flag" "fmt" "io" "io/fs" "net" "os" "os/exec" "path/filepath" "strings" "time" "zettelstore.de/z/strfun" ) var directProxy = []string{"GOPROXY=direct"} func executeCommand(env []string, name string, arg ...string) (string, error) { logCommand("EXEC", env, name, arg) var out strings.Builder cmd := prepareCommand(env, name, arg, &out) err := cmd.Run() return out.String(), err } func prepareCommand(env []string, name string, arg []string, out io.Writer) *exec.Cmd { if len(env) > 0 { env = append(env, os.Environ()...) } cmd := exec.Command(name, arg...) cmd.Env = env cmd.Stdin = nil cmd.Stdout = out cmd.Stderr = os.Stderr return cmd } func logCommand(exec string, env []string, name string, arg []string) { if verbose { if len(env) > 0 { for i, e := range env { fmt.Fprintf(os.Stderr, "ENV%d %v\n", i+1, e) } } fmt.Fprintln(os.Stderr, exec, name, arg) } } func readVersionFile() (string, error) { content, err := os.ReadFile("VERSION") if err != nil { return "", err } return strings.TrimFunc(string(content), func(r rune) bool { return r <= ' ' }), nil } func getVersion() string { base, err := readVersionFile() if err != nil { base = "dev" } return base } var dirtyPrefixes = []string{ "DELETED ", "ADDED ", "UPDATED ", "CONFLICT ", "EDITED ", "RENAMED ", "EXTRA "} const dirtySuffix = "-dirty" func readFossilDirty() (string, error) { s, err := executeCommand(nil, "fossil", "status", "--differ") if err != nil { return "", err } for _, line := range strfun.SplitLines(s) { for _, prefix := range dirtyPrefixes { if strings.HasPrefix(line, prefix) { return dirtySuffix, nil } } } return "", nil } func getFossilDirty() string { fossil, err := readFossilDirty() if err != nil { return "" } return fossil } func findExec(cmd string) string { if path, err := executeCommand(nil, "which", cmd); err == nil && path != "" { return strings.TrimSpace(path) } return "" } func cmdCheck(forRelease bool) error { if err := checkGoTest("./..."); err != nil { return err } if err := checkGoVet(); err != nil { return err } if err := checkShadow(forRelease); err != nil { return err } if err := checkStaticcheck(); err != nil { return err } if err := checkUnparam(forRelease); err != nil { return err } if forRelease { if err := checkGoVulncheck(); err != nil { return err } } return checkFossilExtra() } func checkGoTest(pkg string, testParams ...string) error { args := []string{"test", pkg} args = append(args, testParams...) out, err := executeCommand(directProxy, "go", args...) if err != nil { for _, line := range strfun.SplitLines(out) { if strings.HasPrefix(line, "ok") || strings.HasPrefix(line, "?") { continue } fmt.Fprintln(os.Stderr, line) } } return err } func checkGoVet() error { out, err := executeCommand(nil, "go", "vet", "./...") if err != nil { fmt.Fprintln(os.Stderr, "Some checks failed") if len(out) > 0 { fmt.Fprintln(os.Stderr, out) } } return err } func checkShadow(forRelease bool) error { path, err := findExecStrict("shadow", forRelease) if path == "" { return err } out, err := executeCommand(nil, path, "-strict", "./...") if err != nil { fmt.Fprintln(os.Stderr, "Some shadowed variables found") if len(out) > 0 { fmt.Fprintln(os.Stderr, out) } } return err } func checkStaticcheck() error { out, err := executeCommand(nil, "staticcheck", "./...") if err != nil { fmt.Fprintln(os.Stderr, "Some staticcheck problems found") if len(out) > 0 { fmt.Fprintln(os.Stderr, out) } } return err } func checkUnparam(forRelease bool) error { path, err := findExecStrict("unparam", forRelease) if path == "" { return err } out, err := executeCommand(nil, path, "./...") if err != nil { fmt.Fprintln(os.Stderr, "Some unparam problems found") if len(out) > 0 { fmt.Fprintln(os.Stderr, out) } } if forRelease { if out2, err2 := executeCommand(nil, path, "-exported", "-tests", "./..."); err2 != nil { fmt.Fprintln(os.Stderr, "Some optional unparam problems found") if len(out2) > 0 { fmt.Fprintln(os.Stderr, out2) } } } return err } func checkGoVulncheck() error { out, err := executeCommand(nil, "govulncheck", "./...") if err != nil { fmt.Fprintln(os.Stderr, "Some checks failed") if len(out) > 0 { fmt.Fprintln(os.Stderr, out) } } return err } func findExecStrict(cmd string, forRelease bool) (string, error) { path := findExec(cmd) if path != "" || !forRelease { return path, nil } return "", errors.New("Command '" + cmd + "' not installed, but required for release") } func checkFossilExtra() error { out, err := executeCommand(nil, "fossil", "extra") if err != nil { fmt.Fprintln(os.Stderr, "Unable to execute 'fossil extra'") return err } if len(out) > 0 { fmt.Fprint(os.Stderr, "Warning: unversioned file(s):") for i, extra := range strfun.SplitLines(out) { if i > 0 { fmt.Fprint(os.Stderr, ",") } fmt.Fprintf(os.Stderr, " %q", extra) } fmt.Fprintln(os.Stderr) } return nil } type zsInfo struct { cmd *exec.Cmd out strings.Builder adminAddress string } func cmdTestAPI() error { var err error var info zsInfo needServer := !addressInUse(":23123") if needServer { err = startZettelstore(&info) } if err != nil { return err } err = checkGoTest("zettelstore.de/z/tests/client", "-base-url", "http://127.0.0.1:23123") if needServer { err1 := stopZettelstore(&info) if err == nil { err = err1 } } return err } func startZettelstore(info *zsInfo) error { info.adminAddress = ":2323" name, arg := "go", []string{ "run", "cmd/zettelstore/main.go", "run", "-c", "./testdata/testbox/19700101000000.zettel", "-a", info.adminAddress[1:]} logCommand("FORK", nil, name, arg) cmd := prepareCommand(nil, name, arg, &info.out) if !verbose { cmd.Stderr = nil } err := cmd.Start() time.Sleep(2 * time.Second) for i := 0; i < 100; i++ { time.Sleep(time.Millisecond * 100) if addressInUse(info.adminAddress) { info.cmd = cmd return err } } return errors.New("zettelstore did not start") } func stopZettelstore(i *zsInfo) error { conn, err := net.Dial("tcp", i.adminAddress) if err != nil { fmt.Println("Unable to stop Zettelstore") return err } io.WriteString(conn, "shutdown\n") conn.Close() err = i.cmd.Wait() return err } func addressInUse(address string) bool { conn, err := net.Dial("tcp", address) if err != nil { return false } conn.Close() return true } func cmdBuild() error { return doBuild(directProxy, getVersion(), "bin/zettelstore") } func doBuild(env []string, version, target string) error { env = append(env, "CGO_ENABLED=0") out, err := executeCommand( env, "go", "build", "-tags", "osusergo,netgo", "-trimpath", "-ldflags", fmt.Sprintf("-X main.version=%v -w", version), "-o", target, "zettelstore.de/z/cmd/zettelstore", ) if err != nil { return err } if len(out) > 0 { fmt.Println(out) } return nil } func cmdManual() error { base := getReleaseVersionData() return createManualZip(".", base) } func createManualZip(path, base string) error { manualPath := filepath.Join("docs", "manual") entries, err := os.ReadDir(manualPath) if err != nil { return err } zipName := filepath.Join(path, "manual-"+base+".zip") zipFile, err := os.OpenFile(zipName, os.O_RDWR|os.O_CREATE, 0600) if err != nil { return err } defer zipFile.Close() zipWriter := zip.NewWriter(zipFile) defer zipWriter.Close() for _, entry := range entries { if err = createManualZipEntry(manualPath, entry, zipWriter); err != nil { return err } } return nil } func createManualZipEntry(path string, entry fs.DirEntry, zipWriter *zip.Writer) error { info, err := entry.Info() if err != nil { return err } fh, err := zip.FileInfoHeader(info) if err != nil { return err } fh.Name = entry.Name() fh.Method = zip.Deflate w, err := zipWriter.CreateHeader(fh) if err != nil { return err } manualFile, err := os.Open(filepath.Join(path, entry.Name())) if err != nil { return err } defer manualFile.Close() _, err = io.Copy(w, manualFile) return err } func getReleaseVersionData() string { if fossil := getFossilDirty(); fossil != "" { fmt.Fprintln(os.Stderr, "Warning: releasing a dirty version") } base := getVersion() if strings.HasSuffix(base, "dev") { return base[:len(base)-3] + "preview-" + time.Now().Local().Format("20060102") } return base } func cmdRelease() error { if err := cmdCheck(true); err != nil { return err } base := getReleaseVersionData() releases := []struct { arch string os string env []string name string }{ {"amd64", "linux", nil, "zettelstore"}, {"arm", "linux", []string{"GOARM=6"}, "zettelstore"}, {"amd64", "darwin", nil, "zettelstore"}, {"arm64", "darwin", nil, "zettelstore"}, {"amd64", "windows", nil, "zettelstore.exe"}, } for _, rel := range releases { env := append([]string{}, rel.env...) env = append(env, "GOARCH="+rel.arch, "GOOS="+rel.os) env = append(env, directProxy...) zsName := filepath.Join("releases", rel.name) if err := doBuild(env, base, zsName); err != nil { return err } zipName := fmt.Sprintf("zettelstore-%v-%v-%v.zip", base, rel.os, rel.arch) if err := createReleaseZip(zsName, zipName, rel.name); err != nil { return err } if err := os.Remove(zsName); err != nil { return err } } return createManualZip("releases", base) } func createReleaseZip(zsName, zipName, fileName string) error { zipFile, err := os.OpenFile(filepath.Join("releases", zipName), os.O_RDWR|os.O_CREATE, 0600) if err != nil { return err } defer zipFile.Close() zw := zip.NewWriter(zipFile) defer zw.Close() err = addFileToZip(zw, zsName, fileName) if err != nil { return err } err = addFileToZip(zw, "LICENSE.txt", "LICENSE.txt") if err != nil { return err } err = addFileToZip(zw, "docs/readmezip.txt", "README.txt") return err } func addFileToZip(zipFile *zip.Writer, filepath, filename string) error { zsFile, err := os.Open(filepath) if err != nil { return err } defer zsFile.Close() stat, err := zsFile.Stat() if err != nil { return err } fh, err := zip.FileInfoHeader(stat) if err != nil { return err } fh.Name = filename fh.Method = zip.Deflate w, err := zipFile.CreateHeader(fh) if err != nil { return err } _, err = io.Copy(w, zsFile) return err } func cmdClean() error { for _, dir := range []string{"bin", "releases"} { err := os.RemoveAll(dir) if err != nil { return err } } out, err := executeCommand(nil, "go", "clean", "./...") if err != nil { return err } if len(out) > 0 { fmt.Println(out) } out, err = executeCommand(nil, "go", "clean", "-cache", "-modcache", "-testcache") if err != nil { return err } if len(out) > 0 { fmt.Println(out) } return nil } func cmdHelp() { fmt.Println(`Usage: go run tools/build.go [-v] COMMAND Options: -v Verbose output. Commands: build Build the software for local computer. check Check current working state: execute tests, static analysis tools, extra files, ... Is automatically done when releasing the software. clean Remove all build and release directories. help Output this text. manual Create a ZIP file with all manual zettel relcheck Check current working state for release. release Create the software for various platforms and put them in appropriate named ZIP files. testapi Start a Zettelstore and execute API tests. version Print the current version of the software. All commands can be abbreviated as long as they remain unique.`) } var ( verbose bool ) func main() { flag.BoolVar(&verbose, "v", false, "Verbose output") flag.Parse() var err error args := flag.Args() if len(args) < 1 { cmdHelp() } else { switch args[0] { case "b", "bu", "bui", "buil", "build": err = cmdBuild() case "m", "ma", "man", "manu", "manua", "manual": err = cmdManual() case "r", "re", "rel", "rele", "relea", "releas", "release": err = cmdRelease() case "cl", "cle", "clea", "clean": err = cmdClean() case "v", "ve", "ver", "vers", "versi", "versio", "version": fmt.Print(getVersion()) case "ch", "che", "chec", "check": err = cmdCheck(false) case "relc", "relch", "relche", "relchec", "relcheck": err = cmdCheck(true) case "t", "te", "tes", "test", "testa", "testap", "testapi": cmdTestAPI() case "h", "he", "hel", "help": cmdHelp() default: fmt.Fprintf(os.Stderr, "Unknown command %q\n", args[0]) cmdHelp() os.Exit(1) } } if err != nil { fmt.Fprintln(os.Stderr, err) } } |
Deleted tools/build/build.go.
|
| < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < |
Deleted tools/check/check.go.
|
| < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < |
Deleted tools/clean/clean.go.
|
| < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < |
Deleted tools/devtools/devtools.go.
|
| < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < |
Deleted tools/htmllint/htmllint.go.
|
| < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < |
Deleted tools/testapi/testapi.go.
|
| < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < |
Deleted tools/tools.go.
|
| < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < |
Added usecase/authenticate.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 | //----------------------------------------------------------------------------- // Copyright (c) 2020-present Detlef Stern // // This file is part of Zettelstore. // // Zettelstore is licensed under the latest version of the EUPL (European Union // Public License). Please see file LICENSE.txt for your rights and obligations // under this license. //----------------------------------------------------------------------------- package usecase import ( "context" "math/rand" "net/http" "time" "zettelstore.de/c/api" "zettelstore.de/z/auth" "zettelstore.de/z/auth/cred" "zettelstore.de/z/domain/id" "zettelstore.de/z/domain/meta" "zettelstore.de/z/logger" "zettelstore.de/z/query" ) // AuthenticatePort is the interface used by this use case. type AuthenticatePort interface { GetMeta(context.Context, id.Zid) (*meta.Meta, error) SelectMeta(context.Context, *query.Query) ([]*meta.Meta, error) } // Authenticate is the data for this use case. type Authenticate struct { log *logger.Logger token auth.TokenManager port AuthenticatePort ucGetUser GetUser } // NewAuthenticate creates a new use case. func NewAuthenticate(log *logger.Logger, token auth.TokenManager, authz auth.AuthzManager, port AuthenticatePort) Authenticate { return Authenticate{ log: log, token: token, port: port, ucGetUser: NewGetUser(authz, port), } } // Run executes the use case. // // Parameter "r" is just included to produce better logging messages. It may be nil. Do not use it // for other purposes. func (uc *Authenticate) Run(ctx context.Context, r *http.Request, ident, credential string, d time.Duration, k auth.TokenKind) ([]byte, error) { identMeta, err := uc.ucGetUser.Run(ctx, ident) defer addDelay(time.Now(), 500*time.Millisecond, 100*time.Millisecond) if identMeta == nil || err != nil { uc.log.Info().Str("ident", ident).Err(err).HTTPIP(r).Msg("No user with given ident found") compensateCompare() return nil, err } if hashCred, ok := identMeta.Get(api.KeyCredential); ok { ok, err = cred.CompareHashAndCredential(hashCred, identMeta.Zid, ident, credential) if err != nil { uc.log.Info().Str("ident", ident).Err(err).HTTPIP(r).Msg("Error while comparing credentials") return nil, err } if ok { token, err2 := uc.token.GetToken(identMeta, d, k) if err2 != nil { uc.log.Info().Str("ident", ident).Err(err).Msg("Unable to produce authentication token") return nil, err2 } uc.log.Info().Str("user", ident).Msg("Successful") return token, nil } uc.log.Info().Str("ident", ident).HTTPIP(r).Msg("Credentials don't match") return nil, nil } uc.log.Info().Str("ident", ident).Msg("No credential stored") compensateCompare() return nil, nil } // compensateCompare if normal comapare is not possible, to avoid timing hints. func compensateCompare() { cred.CompareHashAndCredential( "$2a$10$WHcSO3G9afJ3zlOYQR1suuf83bCXED2jmzjti/MH4YH4l2mivDuze", id.Invalid, "", "") } // addDelay after credential checking to allow some CPU time for other tasks. // durDelay is the normal delay, if time spend for checking is smaller than // the minimum delay minDelay. In addition some jitter (+/- 50 ms) is added. func addDelay(start time.Time, durDelay, minDelay time.Duration) { jitter := time.Duration(rand.Intn(100)-50) * time.Millisecond if elapsed := time.Since(start); elapsed+minDelay < durDelay { time.Sleep(durDelay - elapsed + jitter) } else { time.Sleep(minDelay + jitter) } } // IsAuthenticatedPort contains method for this usecase. type IsAuthenticatedPort interface { GetUser(context.Context) *meta.Meta } // IsAuthenticated cheks if the caller is alrwady authenticated. type IsAuthenticated struct { log *logger.Logger port IsAuthenticatedPort authz auth.AuthzManager } // NewIsAuthenticated creates a new use case object. func NewIsAuthenticated(log *logger.Logger, port IsAuthenticatedPort, authz auth.AuthzManager) IsAuthenticated { return IsAuthenticated{ log: log, port: port, authz: authz, } } // IsAuthenticatedResult is an enumeration. type IsAuthenticatedResult uint8 // Values for IsAuthenticatedResult. const ( _ IsAuthenticatedResult = iota IsAuthenticatedDisabled IsAuthenticatedAndValid IsAuthenticatedAndInvalid ) // Run executes the use case. func (uc *IsAuthenticated) Run(ctx context.Context) IsAuthenticatedResult { if !uc.authz.WithAuth() { uc.log.Sense().Str("auth", "disabled").Msg("IsAuthenticated") return IsAuthenticatedDisabled } if uc.port.GetUser(ctx) == nil { uc.log.Sense().Msg("IsAuthenticated is false") return IsAuthenticatedAndInvalid } uc.log.Sense().Msg("IsAuthenticated is true") return IsAuthenticatedAndValid } |
Added usecase/context.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 | //----------------------------------------------------------------------------- // Copyright (c) 2021-present Detlef Stern // // This file is part of Zettelstore. // // Zettelstore is licensed under the latest version of the EUPL (European Union // Public License). Please see file LICENSE.txt for your rights and obligations // under this license. //----------------------------------------------------------------------------- package usecase import ( "context" "zettelstore.de/c/api" "zettelstore.de/z/domain/id" "zettelstore.de/z/domain/meta" "zettelstore.de/z/query" ) // ZettelContextPort is the interface used by this use case. type ZettelContextPort interface { // GetMeta retrieves just the meta data of a specific zettel. GetMeta(ctx context.Context, zid id.Zid) (*meta.Meta, error) // SelectMeta returns all zettel metadata that match the selection criteria. SelectMeta(ctx context.Context, q *query.Query) ([]*meta.Meta, error) } // ZettelContextConfig is the interface to allow the usecase to read some config data. type ZettelContextConfig interface { // Get returns a config value that might be user-specific. Get(ctx context.Context, m *meta.Meta, key string) string } // ZettelContext is the data for this use case. type ZettelContext struct { port ZettelContextPort config ZettelContextConfig } // NewZettelContext creates a new use case. func NewZettelContext(port ZettelContextPort, config ZettelContextConfig) ZettelContext { return ZettelContext{port: port, config: config} } // ZettelContextDirection determines the way, the context is calculated. type ZettelContextDirection int // Constant values for ZettelContextDirection const ( _ ZettelContextDirection = iota ZettelContextForward // Traverse all forwarding links ZettelContextBackward // Traverse all backwaring links ZettelContextBoth // Traverse both directions ) // Run executes the use case. func (uc ZettelContext) Run(ctx context.Context, zid id.Zid, dir ZettelContextDirection, maxCost, limit int) (result []*meta.Meta, err error) { start, err := uc.port.GetMeta(ctx, zid) if err != nil { return nil, err } tasks := newQueue(start, maxCost, limit) isBackward := dir == ZettelContextBoth || dir == ZettelContextBackward isForward := dir == ZettelContextBoth || dir == ZettelContextForward for { m, cost := tasks.next() if m == nil { break } result = append(result, m) for _, p := range m.ComputedPairsRest() { tasks.addPair(ctx, uc.port, p.Key, p.Value, cost, isBackward, isForward) } if tags, found := m.GetList(api.KeyTags); found { for _, tag := range tags { tasks.addSameTag(ctx, uc.port, tag, cost) } } } return result, nil } type ztlCtxTask struct { next *ztlCtxTask prev *ztlCtxTask meta *meta.Meta cost int } type contextQueue struct { seen id.Set first *ztlCtxTask last *ztlCtxTask maxCost int limit int tagCost map[string][]*meta.Meta } func newQueue(m *meta.Meta, maxCost, limit int) *contextQueue { task := &ztlCtxTask{ next: nil, prev: nil, meta: m, cost: 0, } result := &contextQueue{ seen: id.NewSet(), first: task, last: task, maxCost: maxCost, limit: limit, tagCost: make(map[string][]*meta.Meta, 1024), } return result } func (zc *contextQueue) addPair( ctx context.Context, port ZettelContextPort, key, value string, curCost int, isBackward, isForward bool, ) { if key == api.KeyBack { return } newCost := curCost + contextCost(key) if key == api.KeyBackward { if isBackward { zc.addIDSet(ctx, port, newCost, value) } return } if key == api.KeyForward { if isForward { zc.addIDSet(ctx, port, newCost, value) } return } hasInverse := meta.Inverse(key) != "" if (!hasInverse || !isBackward) && (hasInverse || !isForward) { return } if t := meta.Type(key); t == meta.TypeID { zc.addID(ctx, port, newCost, value) } else if t == meta.TypeIDSet { zc.addIDSet(ctx, port, newCost, value) } } func contextCost(key string) int { switch key { case api.KeyFolge, api.KeyPrecursor: return 1 case api.KeySuccessors, api.KeyPredecessor: return 2 } return 3 } func (zc *contextQueue) addID(ctx context.Context, port ZettelContextPort, newCost int, value string) { if zc.costMaxed(newCost) { return } zid, err := id.Parse(value) if err != nil { return } m, err := port.GetMeta(ctx, zid) if err != nil { return } zc.addMeta(m, newCost) } func (zc *contextQueue) addMeta(m *meta.Meta, newCost int) { task := &ztlCtxTask{next: nil, prev: nil, meta: m, cost: newCost} if zc.first == nil { zc.first = task zc.last = task return } // Search backward for a task t with at most the same cost for t := zc.last; t != nil; t = t.prev { if t.cost <= task.cost { // Found! if t.next != nil { t.next.prev = task } task.next = t.next t.next = task task.prev = t if task.next == nil { zc.last = task } return } } // We have not found a task, therefore the new task is the first one task.next = zc.first zc.first.prev = task zc.first = task } func (zc *contextQueue) costMaxed(newCost int) bool { return (zc.maxCost > 0 && newCost > zc.maxCost) || zc.hasLimit() } func (zc *contextQueue) addIDSet(ctx context.Context, port ZettelContextPort, newCost int, value string) { elems := meta.ListFromValue(value) refCost := referenceCost(newCost, len(elems)) for _, val := range elems { zc.addID(ctx, port, refCost, val) } } func referenceCost(baseCost int, numReferences int) int { baseCost++ if numReferences < 5 { return baseCost } if numReferences < 9 { return baseCost * 2 } if numReferences < 17 { return baseCost * 3 } if numReferences < 33 { return baseCost * 4 } if numReferences < 65 { return baseCost * 5 } return baseCost * numReferences / 8 } func (zc *contextQueue) addSameTag(ctx context.Context, port ZettelContextPort, tag string, baseCost int) { tagMetas, found := zc.tagCost[tag] if !found { q := query.Parse(api.KeyTags + api.SearchOperatorHas + tag) ml, err := port.SelectMeta(ctx, q) if err != nil { return } tagMetas = ml zc.tagCost[tag] = ml } cost := tagCost(baseCost, len(tagMetas)) if zc.costMaxed(cost) { return } for _, m := range tagMetas { zc.addMeta(m, cost) } } func tagCost(baseCost, numTags int) int { if numTags < 5 { return baseCost + numTags } return (baseCost + 2) * (numTags + 1) / 4 } func (zc *contextQueue) next() (*meta.Meta, int) { if zc.hasLimit() { return nil, -1 } for zc.first != nil { task := zc.first zc.first = task.next if zc.first == nil { zc.last = nil } else { zc.first.prev = nil } m := task.meta zid := m.Zid _, found := zc.seen[zid] if found { continue } zc.seen.Zid(zid) return m, task.cost } return nil, -1 } func (zc *contextQueue) hasLimit() bool { limit := zc.limit return limit > 0 && len(zc.seen) > limit } |
Added usecase/create_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 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 | //----------------------------------------------------------------------------- // Copyright (c) 2020-present Detlef Stern // // This file is part of Zettelstore. // // Zettelstore is licensed under the latest version of the EUPL (European Union // Public License). Please see file LICENSE.txt for your rights and obligations // under this license. //----------------------------------------------------------------------------- package usecase import ( "context" "time" "zettelstore.de/c/api" "zettelstore.de/z/config" "zettelstore.de/z/domain" "zettelstore.de/z/domain/id" "zettelstore.de/z/domain/meta" "zettelstore.de/z/logger" ) // CreateZettelPort is the interface used by this use case. type CreateZettelPort interface { // CreateZettel creates a new zettel. CreateZettel(ctx context.Context, zettel domain.Zettel) (id.Zid, error) } // CreateZettel is the data for this use case. type CreateZettel struct { log *logger.Logger rtConfig config.Config port CreateZettelPort } // NewCreateZettel creates a new use case. func NewCreateZettel(log *logger.Logger, rtConfig config.Config, port CreateZettelPort) CreateZettel { return CreateZettel{ log: log, rtConfig: rtConfig, port: port, } } // PrepareCopy the zettel for further modification. func (*CreateZettel) PrepareCopy(origZettel domain.Zettel) domain.Zettel { m := origZettel.Meta.Clone() if title, found := m.Get(api.KeyTitle); found { m.Set(api.KeyTitle, prependTitle(title, "Copy", "Copy of ")) } setReadonly(m) content := origZettel.Content content.TrimSpace() return domain.Zettel{Meta: m, Content: content} } // PrepareVersion the zettel for further modification. func (*CreateZettel) PrepareVersion(origZettel domain.Zettel) domain.Zettel { origMeta := origZettel.Meta m := origMeta.Clone() m.Set(api.KeyPredecessor, origMeta.Zid.String()) setReadonly(m) content := origZettel.Content content.TrimSpace() return domain.Zettel{Meta: m, Content: content} } // PrepareFolge the zettel for further modification. func (*CreateZettel) PrepareFolge(origZettel domain.Zettel) domain.Zettel { origMeta := origZettel.Meta m := meta.New(id.Invalid) if title, found := origMeta.Get(api.KeyTitle); found { m.Set(api.KeyTitle, prependTitle(title, "Folge", "Folge of ")) } m.SetNonEmpty(api.KeyRole, origMeta.GetDefault(api.KeyRole, "")) m.SetNonEmpty(api.KeyTags, origMeta.GetDefault(api.KeyTags, "")) m.SetNonEmpty(api.KeySyntax, origMeta.GetDefault(api.KeySyntax, "")) m.Set(api.KeyPrecursor, origMeta.Zid.String()) return domain.Zettel{Meta: m, Content: domain.NewContent(nil)} } // PrepareNew the zettel for further modification. func (*CreateZettel) PrepareNew(origZettel domain.Zettel) domain.Zettel { m := meta.New(id.Invalid) om := origZettel.Meta m.SetNonEmpty(api.KeyTitle, om.GetDefault(api.KeyTitle, "")) m.SetNonEmpty(api.KeyRole, om.GetDefault(api.KeyRole, "")) m.SetNonEmpty(api.KeyTags, om.GetDefault(api.KeyTags, "")) m.SetNonEmpty(api.KeySyntax, om.GetDefault(api.KeySyntax, "")) const prefixLen = len(meta.NewPrefix) for _, pair := range om.PairsRest() { if key := pair.Key; len(key) > prefixLen && key[0:prefixLen] == meta.NewPrefix { m.Set(key[prefixLen:], pair.Value) } } content := origZettel.Content content.TrimSpace() return domain.Zettel{Meta: m, Content: content} } func prependTitle(title, s0, s1 string) string { if len(title) > 0 { return s1 + title } return s0 } func setReadonly(m *meta.Meta) { if _, found := m.Get(api.KeyReadOnly); found { // Currently, "false" is a safe value. // // If the current user and its role is known, a more elaborative calculation // could be done: set it to a value, so that the current user will be able // to modify it later. m.Set(api.KeyReadOnly, api.ValueFalse) } } // Run executes the use case. func (uc *CreateZettel) Run(ctx context.Context, zettel domain.Zettel) (id.Zid, error) { m := zettel.Meta if m.Zid.IsValid() { return m.Zid, nil // TODO: new error: already exists } m.Set(api.KeyCreated, time.Now().Local().Format(id.ZidLayout)) m.Delete(api.KeyModified) m.YamlSep = uc.rtConfig.GetYAMLHeader() zettel.Content.TrimSpace() zid, err := uc.port.CreateZettel(ctx, zettel) uc.log.Info().User(ctx).Zid(zid).Err(err).Msg("Create zettel") return zid, err } |
Added usecase/delete_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 | //----------------------------------------------------------------------------- // Copyright (c) 2020-present Detlef Stern // // This file is part of Zettelstore. // // Zettelstore is licensed under the latest version of the EUPL (European Union // Public License). Please see file LICENSE.txt for your rights and obligations // under this license. //----------------------------------------------------------------------------- package usecase import ( "context" "zettelstore.de/z/domain/id" "zettelstore.de/z/logger" ) // DeleteZettelPort is the interface used by this use case. type DeleteZettelPort interface { // DeleteZettel removes the zettel from the box. DeleteZettel(ctx context.Context, zid id.Zid) error } // DeleteZettel is the data for this use case. type DeleteZettel struct { log *logger.Logger port DeleteZettelPort } // NewDeleteZettel creates a new use case. func NewDeleteZettel(log *logger.Logger, port DeleteZettelPort) DeleteZettel { return DeleteZettel{log: log, port: port} } // Run executes the use case. func (uc *DeleteZettel) Run(ctx context.Context, zid id.Zid) error { err := uc.port.DeleteZettel(ctx, zid) uc.log.Info().User(ctx).Zid(zid).Err(err).Msg("Delete zettel") return err } |
Added usecase/evaluate.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-present Detlef Stern // // This file is part of Zettelstore. // // Zettelstore is licensed under the latest version of the EUPL (European Union // Public License). Please see file LICENSE.txt for your rights and obligations // under this license. //----------------------------------------------------------------------------- package usecase import ( "context" "zettelstore.de/z/ast" "zettelstore.de/z/config" "zettelstore.de/z/domain" "zettelstore.de/z/domain/id" "zettelstore.de/z/domain/meta" "zettelstore.de/z/evaluator" "zettelstore.de/z/parser" "zettelstore.de/z/query" ) // Evaluate is the data for this use case. type Evaluate struct { rtConfig config.Config getZettel GetZettel getMeta GetMeta listMeta ListMeta } // NewEvaluate creates a new use case. func NewEvaluate(rtConfig config.Config, getZettel GetZettel, getMeta GetMeta, listMeta ListMeta) Evaluate { return Evaluate{ rtConfig: rtConfig, getZettel: getZettel, getMeta: getMeta, listMeta: listMeta, } } // Run executes the use case. func (uc *Evaluate) Run(ctx context.Context, zid id.Zid, syntax string) (*ast.ZettelNode, error) { zettel, err := uc.getZettel.Run(ctx, zid) if err != nil { return nil, err } zn, err := parser.ParseZettel(ctx, zettel, syntax, uc.rtConfig), nil if err != nil { return nil, err } evaluator.EvaluateZettel(ctx, uc, uc.rtConfig, zn) return zn, nil } // RunBlockNode executes the use case for a metadata list. func (uc *Evaluate) RunBlockNode(ctx context.Context, bn ast.BlockNode) ast.BlockSlice { if bn == nil { return nil } bns := ast.BlockSlice{bn} evaluator.EvaluateBlock(ctx, uc, uc.rtConfig, &bns) return bns } // RunMetadata executes the use case for a metadata value. func (uc *Evaluate) RunMetadata(ctx context.Context, value string) ast.InlineSlice { is := parser.ParseMetadata(value) evaluator.EvaluateInline(ctx, uc, uc.rtConfig, &is) return is } // GetMeta retrieves the metadata of a given zettel identifier. func (uc *Evaluate) GetMeta(ctx context.Context, zid id.Zid) (*meta.Meta, error) { return uc.getMeta.Run(ctx, zid) } // GetZettel retrieves the full zettel of a given zettel identifier. func (uc *Evaluate) GetZettel(ctx context.Context, zid id.Zid) (domain.Zettel, error) { return uc.getZettel.Run(ctx, zid) } // SelectMeta returns a list of metadata that comply to the given selection criteria. func (uc *Evaluate) SelectMeta(ctx context.Context, q *query.Query) ([]*meta.Meta, error) { return uc.listMeta.Run(ctx, q) } |
Added usecase/get_all_meta.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 | //----------------------------------------------------------------------------- // Copyright (c) 2021-present Detlef Stern // // This file is part of Zettelstore. // // Zettelstore is licensed under the latest version of the EUPL (European Union // Public License). Please see file LICENSE.txt for your rights and obligations // under this license. //----------------------------------------------------------------------------- package usecase import ( "context" "zettelstore.de/z/domain/id" "zettelstore.de/z/domain/meta" ) // GetAllMetaPort is the interface used by this use case. type GetAllMetaPort interface { // GetAllMeta retrieves just the meta data of a specific zettel. GetAllMeta(ctx context.Context, zid id.Zid) ([]*meta.Meta, error) } // GetAllMeta is the data for this use case. type GetAllMeta struct { port GetAllMetaPort } // NewGetAllMeta creates a new use case. func NewGetAllMeta(port GetAllMetaPort) GetAllMeta { return GetAllMeta{port: port} } // Run executes the use case. func (uc GetAllMeta) Run(ctx context.Context, zid id.Zid) ([]*meta.Meta, error) { return uc.port.GetAllMeta(ctx, zid) } |
Added usecase/get_meta.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 | //----------------------------------------------------------------------------- // Copyright (c) 2020-present Detlef Stern // // This file is part of Zettelstore. // // Zettelstore is licensed under the latest version of the EUPL (European Union // Public License). Please see file LICENSE.txt for your rights and obligations // under this license. //----------------------------------------------------------------------------- package usecase import ( "context" "zettelstore.de/z/domain/id" "zettelstore.de/z/domain/meta" ) // GetMetaPort is the interface used by this use case. type GetMetaPort interface { // GetMeta retrieves just the meta data of a specific zettel. GetMeta(ctx context.Context, zid id.Zid) (*meta.Meta, error) } // GetMeta is the data for this use case. type GetMeta struct { port GetMetaPort } // NewGetMeta creates a new use case. func NewGetMeta(port GetMetaPort) GetMeta { return GetMeta{port: port} } // Run executes the use case. func (uc GetMeta) Run(ctx context.Context, zid id.Zid) (*meta.Meta, error) { return uc.port.GetMeta(ctx, zid) } |
Added usecase/get_user.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 | //----------------------------------------------------------------------------- // Copyright (c) 2020-present Detlef Stern // // This file is part of Zettelstore. // // Zettelstore is licensed under the latest version of the EUPL (European Union // Public License). Please see file LICENSE.txt for your rights and obligations // under this license. //----------------------------------------------------------------------------- package usecase import ( "context" "zettelstore.de/c/api" "zettelstore.de/z/auth" "zettelstore.de/z/box" "zettelstore.de/z/domain/id" "zettelstore.de/z/domain/meta" "zettelstore.de/z/query" ) // Use case: return user identified by meta key ident. // --------------------------------------------------- // GetUserPort is the interface used by this use case. type GetUserPort interface { GetMeta(ctx context.Context, zid id.Zid) (*meta.Meta, error) SelectMeta(ctx context.Context, q *query.Query) ([]*meta.Meta, error) } // GetUser is the data for this use case. type GetUser struct { authz auth.AuthzManager port GetUserPort } // NewGetUser creates a new use case. func NewGetUser(authz auth.AuthzManager, port GetUserPort) GetUser { return GetUser{authz: authz, port: port} } // Run executes the use case. func (uc GetUser) Run(ctx context.Context, ident string) (*meta.Meta, error) { ctx = box.NoEnrichContext(ctx) // It is important to try first with the owner. First, because another user // could give herself the same ''ident''. Second, in most cases the owner // will authenticate. identMeta, err := uc.port.GetMeta(ctx, uc.authz.Owner()) if err == nil && identMeta.GetDefault(api.KeyUserID, "") == ident { return identMeta, nil } // Owner was not found or has another ident. Try via list search. q := query.Parse(api.KeyUserID + api.SearchOperatorHas + ident + " " + api.SearchOperatorHas + ident) metaList, err := uc.port.SelectMeta(ctx, q) if err != nil { return nil, err } if len(metaList) < 1 { return nil, nil } return metaList[len(metaList)-1], nil } // Use case: return an user identified by zettel id and assert given ident value. // ------------------------------------------------------------------------------ // GetUserByZidPort is the interface used by this use case. type GetUserByZidPort interface { GetMeta(ctx context.Context, zid id.Zid) (*meta.Meta, error) } // GetUserByZid is the data for this use case. type GetUserByZid struct { port GetUserByZidPort } // NewGetUserByZid creates a new use case. func NewGetUserByZid(port GetUserByZidPort) GetUserByZid { return GetUserByZid{port: port} } // GetUser executes the use case. func (uc GetUserByZid) GetUser(ctx context.Context, zid id.Zid, ident string) (*meta.Meta, error) { userMeta, err := uc.port.GetMeta(box.NoEnrichContext(ctx), zid) if err != nil { return nil, err } if val, ok := userMeta.Get(api.KeyUserID); !ok || val != ident { return nil, nil } return userMeta, nil } |
Added usecase/get_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 | //----------------------------------------------------------------------------- // Copyright (c) 2020-present Detlef Stern // // This file is part of Zettelstore. // // Zettelstore is licensed under the latest version of the EUPL (European Union // Public License). Please see file LICENSE.txt for your rights and obligations // under this license. //----------------------------------------------------------------------------- package usecase import ( "context" "zettelstore.de/z/domain" "zettelstore.de/z/domain/id" ) // GetZettelPort is the interface used by this use case. type GetZettelPort interface { // GetZettel retrieves a specific zettel. GetZettel(ctx context.Context, zid id.Zid) (domain.Zettel, error) } // GetZettel is the data for this use case. type GetZettel struct { port GetZettelPort } // NewGetZettel creates a new use case. func NewGetZettel(port GetZettelPort) GetZettel { return GetZettel{port: port} } // Run executes the use case. func (uc GetZettel) Run(ctx context.Context, zid id.Zid) (domain.Zettel, error) { return uc.port.GetZettel(ctx, zid) } |
Added usecase/lists.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 | //----------------------------------------------------------------------------- // Copyright (c) 2020-present Detlef Stern // // This file is part of Zettelstore. // // Zettelstore is licensed under the latest version of the EUPL (European Union // Public License). Please see file LICENSE.txt for your rights and obligations // under this license. //----------------------------------------------------------------------------- package usecase import ( "context" "zettelstore.de/c/api" "zettelstore.de/z/box" "zettelstore.de/z/domain/meta" "zettelstore.de/z/parser" "zettelstore.de/z/query" ) // ListMetaPort is the interface used by this use case. type ListMetaPort interface { // SelectMeta returns all zettel metadata that match the selection criteria. SelectMeta(ctx context.Context, q *query.Query) ([]*meta.Meta, error) } // ListMeta is the data for this use case. type ListMeta struct { port ListMetaPort } // NewListMeta creates a new use case. func NewListMeta(port ListMetaPort) ListMeta { return ListMeta{port: port} } // Run executes the use case. func (uc ListMeta) Run(ctx context.Context, q *query.Query) ([]*meta.Meta, error) { return uc.port.SelectMeta(ctx, q) } // -------- List roles ------------------------------------------------------- // ListSyntaxPort is the interface used by this use case. type ListSyntaxPort interface { // SelectMeta returns all zettel metadata that match the selection criteria. SelectMeta(ctx context.Context, q *query.Query) ([]*meta.Meta, error) } // ListSyntax is the data for this use case. type ListSyntax struct { port ListSyntaxPort } // NewListSyntax creates a new use case. func NewListSyntax(port ListSyntaxPort) ListSyntax { return ListSyntax{port: port} } // Run executes the use case. func (uc ListSyntax) Run(ctx context.Context) (meta.Arrangement, error) { q := query.Parse(api.KeySyntax + api.ExistOperator) // We look for all metadata with a syntax key metas, err := uc.port.SelectMeta(box.NoEnrichContext(ctx), q) if err != nil { return nil, err } result := meta.CreateArrangement(metas, api.KeySyntax) for _, syn := range parser.GetSyntaxes() { if _, found := result[syn]; !found { delete(result, syn) } } return result, nil } // -------- List roles ------------------------------------------------------- // ListRolesPort is the interface used by this use case. type ListRolesPort interface { // SelectMeta returns all zettel metadata that match the selection criteria. SelectMeta(ctx context.Context, q *query.Query) ([]*meta.Meta, error) } // ListRoles is the data for this use case. type ListRoles struct { port ListRolesPort } // NewListRoles creates a new use case. func NewListRoles(port ListRolesPort) ListRoles { return ListRoles{port: port} } // Run executes the use case. func (uc ListRoles) Run(ctx context.Context) (meta.Arrangement, error) { q := query.Parse(api.KeyRole + api.ExistOperator) // We look for all metadata with an existing role key metas, err := uc.port.SelectMeta(box.NoEnrichContext(ctx), q) if err != nil { return nil, err } return meta.CreateArrangement(metas, api.KeyRole), nil } |
Added usecase/order.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-present Detlef Stern // // This file is part of Zettelstore. // // Zettelstore is licensed under the latest version of the EUPL (European Union // Public License). Please see file LICENSE.txt for your rights and obligations // under this license. //----------------------------------------------------------------------------- package usecase import ( "context" "zettelstore.de/z/collect" "zettelstore.de/z/domain/id" "zettelstore.de/z/domain/meta" ) // ZettelOrderPort is the interface used by this use case. type ZettelOrderPort interface { // GetMeta retrieves just the meta data of a specific zettel. GetMeta(ctx context.Context, zid id.Zid) (*meta.Meta, error) } // ZettelOrder is the data for this use case. type ZettelOrder struct { port ZettelOrderPort evaluate Evaluate } // NewZettelOrder creates a new use case. func NewZettelOrder(port ZettelOrderPort, evaluate Evaluate) ZettelOrder { return ZettelOrder{port: port, evaluate: evaluate} } // Run executes the use case. func (uc ZettelOrder) Run(ctx context.Context, zid id.Zid, syntax string) ( start *meta.Meta, result []*meta.Meta, err error, ) { zn, err := uc.evaluate.Run(ctx, zid, syntax) if err != nil { return nil, nil, err } for _, ref := range collect.Order(zn) { if collectedZid, err2 := id.Parse(ref.URL.Path); err2 == nil { if m, err3 := uc.port.GetMeta(ctx, collectedZid); err3 == nil { result = append(result, m) } } } return zn.Meta, result, nil } |
Added usecase/parse_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 | //----------------------------------------------------------------------------- // Copyright (c) 2020-present Detlef Stern // // This file is part of Zettelstore. // // Zettelstore is licensed under the latest version of the EUPL (European Union // Public License). Please see file LICENSE.txt for your rights and obligations // under this license. //----------------------------------------------------------------------------- package usecase import ( "context" "zettelstore.de/z/ast" "zettelstore.de/z/config" "zettelstore.de/z/domain/id" "zettelstore.de/z/parser" ) // ParseZettel is the data for this use case. type ParseZettel struct { rtConfig config.Config getZettel GetZettel } // NewParseZettel creates a new use case. func NewParseZettel(rtConfig config.Config, getZettel GetZettel) ParseZettel { return ParseZettel{rtConfig: rtConfig, getZettel: getZettel} } // Run executes the use case. func (uc ParseZettel) Run(ctx context.Context, zid id.Zid, syntax string) (*ast.ZettelNode, error) { zettel, err := uc.getZettel.Run(ctx, zid) if err != nil { return nil, err } return parser.ParseZettel(ctx, zettel, syntax, uc.rtConfig), nil } |
Added usecase/refresh.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-present Detlef Stern // // This file is part of Zettelstore. // // Zettelstore is licensed under the latest version of the EUPL (European Union // Public License). Please see file LICENSE.txt for your rights and obligations // under this license. //----------------------------------------------------------------------------- package usecase import ( "context" "zettelstore.de/z/logger" ) // RefreshPort is the interface used by this use case. type RefreshPort interface { Refresh(context.Context) error } // Refresh is the data for this use case. type Refresh struct { log *logger.Logger port RefreshPort } // NewRefresh creates a new use case. func NewRefresh(log *logger.Logger, port RefreshPort) Refresh { return Refresh{log: log, port: port} } // Run executes the use case. func (uc *Refresh) Run(ctx context.Context) error { err := uc.port.Refresh(ctx) uc.log.Info().User(ctx).Err(err).Msg("Refresh internal data") return err } |
Added usecase/rename_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 | //----------------------------------------------------------------------------- // Copyright (c) 2020-present Detlef Stern // // This file is part of Zettelstore. // // Zettelstore is licensed under the latest version of the EUPL (European Union // Public License). Please see file LICENSE.txt for your rights and obligations // under this license. //----------------------------------------------------------------------------- package usecase import ( "context" "zettelstore.de/z/box" "zettelstore.de/z/domain/id" "zettelstore.de/z/domain/meta" "zettelstore.de/z/logger" ) // RenameZettelPort is the interface used by this use case. type RenameZettelPort interface { // GetMeta retrieves just the meta data of a specific zettel. GetMeta(ctx context.Context, zid id.Zid) (*meta.Meta, error) // Rename changes the current id to a new id. RenameZettel(ctx context.Context, curZid, newZid id.Zid) error } // RenameZettel is the data for this use case. type RenameZettel struct { log *logger.Logger port RenameZettelPort } // ErrZidInUse is returned if the zettel id is not appropriate for the box operation. type ErrZidInUse struct{ Zid id.Zid } func (err *ErrZidInUse) Error() string { return "Zettel id already in use: " + err.Zid.String() } // NewRenameZettel creates a new use case. func NewRenameZettel(log *logger.Logger, port RenameZettelPort) RenameZettel { return RenameZettel{log: log, port: port} } // Run executes the use case. func (uc *RenameZettel) Run(ctx context.Context, curZid, newZid id.Zid) error { noEnrichCtx := box.NoEnrichContext(ctx) if _, err := uc.port.GetMeta(noEnrichCtx, curZid); err != nil { return err } if newZid == curZid { // Nothing to do return nil } if _, err := uc.port.GetMeta(noEnrichCtx, newZid); err == nil { return &ErrZidInUse{Zid: newZid} } err := uc.port.RenameZettel(ctx, curZid, newZid) uc.log.Info().User(ctx).Zid(curZid).Err(err).Zid(newZid).Msg("Rename zettel") return err } |
Added usecase/unlinked_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 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 | //----------------------------------------------------------------------------- // Copyright (c) 2021-present Detlef Stern // // This file is part of Zettelstore. // // Zettelstore is licensed under the latest version of the EUPL (European Union // Public License). Please see file LICENSE.txt for your rights and obligations // under this license. //----------------------------------------------------------------------------- package usecase import ( "context" "strings" "unicode" "zettelstore.de/c/api" "zettelstore.de/z/ast" "zettelstore.de/z/config" "zettelstore.de/z/domain" "zettelstore.de/z/domain/id" "zettelstore.de/z/domain/meta" "zettelstore.de/z/encoder/textenc" "zettelstore.de/z/evaluator" "zettelstore.de/z/parser" "zettelstore.de/z/query" ) // UnlinkedReferencesPort is the interface used by this use case. type UnlinkedReferencesPort interface { GetMeta(ctx context.Context, zid id.Zid) (*meta.Meta, error) GetZettel(ctx context.Context, zid id.Zid) (domain.Zettel, error) SelectMeta(ctx context.Context, q *query.Query) ([]*meta.Meta, error) } // UnlinkedReferences is the data for this use case. type UnlinkedReferences struct { port UnlinkedReferencesPort rtConfig config.Config encText *textenc.Encoder } // NewUnlinkedReferences creates a new use case. func NewUnlinkedReferences(port UnlinkedReferencesPort, rtConfig config.Config) UnlinkedReferences { return UnlinkedReferences{ port: port, rtConfig: rtConfig, encText: textenc.Create(), } } // Run executes the usecase with already evaluated title value. func (uc *UnlinkedReferences) Run(ctx context.Context, phrase string, q *query.Query) ([]*meta.Meta, error) { words := makeWords(phrase) if len(words) == 0 { return nil, nil } var sb strings.Builder for _, word := range words { sb.WriteString(" :") sb.WriteString(word) } q = q.Parse(sb.String()) // Limit applies to the filtering process, not to SelectMeta limit := q.GetLimit() q = q.SetLimit(0) candidates, err := uc.port.SelectMeta(ctx, q) if err != nil { return nil, err } q = q.SetLimit(limit) // Restore limit return q.Limit(uc.filterCandidates(ctx, candidates, words)), nil } func makeWords(text string) []string { return strings.FieldsFunc(text, func(r rune) bool { return unicode.In(r, unicode.C, unicode.P, unicode.Z) }) } func (uc *UnlinkedReferences) filterCandidates(ctx context.Context, candidates []*meta.Meta, words []string) []*meta.Meta { result := make([]*meta.Meta, 0, len(candidates)) candLoop: for _, cand := range candidates { zettel, err := uc.port.GetZettel(ctx, cand.Zid) if err != nil { continue } v := unlinkedVisitor{ words: words, found: false, } v.text = v.joinWords(words) for _, pair := range zettel.Meta.Pairs() { if meta.Type(pair.Key) != meta.TypeZettelmarkup { continue } is := parser.ParseMetadata(pair.Value) evaluator.EvaluateInline(ctx, uc.port, uc.rtConfig, &is) ast.Walk(&v, &is) if v.found { result = append(result, cand) continue candLoop } } syntax := zettel.Meta.GetDefault(api.KeySyntax, "") if !parser.IsASTParser(syntax) { continue } zn, err := parser.ParseZettel(ctx, zettel, syntax, uc.rtConfig), nil if err != nil { continue } evaluator.EvaluateZettel(ctx, uc.port, uc.rtConfig, zn) ast.Walk(&v, &zn.Ast) if v.found { result = append(result, cand) } } return result } func (*unlinkedVisitor) joinWords(words []string) string { return " " + strings.ToLower(strings.Join(words, " ")) + " " } type unlinkedVisitor struct { words []string text string found bool } func (v *unlinkedVisitor) Visit(node ast.Node) ast.Visitor { switch n := node.(type) { case *ast.InlineSlice: v.checkWords(n) return nil case *ast.HeadingNode: return nil case *ast.LinkNode, *ast.EmbedRefNode, *ast.EmbedBLOBNode, *ast.CiteNode: return nil } return v } func (v *unlinkedVisitor) checkWords(is *ast.InlineSlice) { if len(*is) < 2*len(v.words)-1 { return } for _, text := range v.splitInlineTextList(is) { if strings.Contains(text, v.text) { v.found = true } } } func (v *unlinkedVisitor) splitInlineTextList(is *ast.InlineSlice) []string { var result []string var curList []string for _, in := range *is { switch n := in.(type) { case *ast.TextNode: curList = append(curList, makeWords(n.Text)...) case *ast.SpaceNode: default: if curList != nil { result = append(result, v.joinWords(curList)) curList = nil } } } if curList != nil { result = append(result, v.joinWords(curList)) } return result } |
Added usecase/update_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 | //----------------------------------------------------------------------------- // Copyright (c) 2020-present Detlef Stern // // This file is part of Zettelstore. // // Zettelstore is licensed under the latest version of the EUPL (European Union // Public License). Please see file LICENSE.txt for your rights and obligations // under this license. //----------------------------------------------------------------------------- package usecase import ( "context" "zettelstore.de/c/api" "zettelstore.de/z/box" "zettelstore.de/z/domain" "zettelstore.de/z/domain/id" "zettelstore.de/z/domain/meta" "zettelstore.de/z/logger" ) // UpdateZettelPort is the interface used by this use case. type UpdateZettelPort interface { // GetZettel retrieves a specific zettel. GetZettel(ctx context.Context, zid id.Zid) (domain.Zettel, error) // UpdateZettel updates an existing zettel. UpdateZettel(ctx context.Context, zettel domain.Zettel) error } // UpdateZettel is the data for this use case. type UpdateZettel struct { log *logger.Logger port UpdateZettelPort } // NewUpdateZettel creates a new use case. func NewUpdateZettel(log *logger.Logger, port UpdateZettelPort) UpdateZettel { return UpdateZettel{log: log, port: port} } // Run executes the use case. func (uc *UpdateZettel) Run(ctx context.Context, zettel domain.Zettel, hasContent bool) error { m := zettel.Meta oldZettel, err := uc.port.GetZettel(box.NoEnrichContext(ctx), m.Zid) if err != nil { return err } if zettel.Equal(oldZettel, false) { return nil } // Update relevant computed, but stored values. if _, found := m.Get(api.KeyCreated); !found { if val, crFound := oldZettel.Meta.Get(api.KeyCreated); crFound { m.Set(api.KeyCreated, val) } } m.SetNow(api.KeyModified) m.YamlSep = oldZettel.Meta.YamlSep if m.Zid == id.ConfigurationZid { m.Set(api.KeySyntax, meta.SyntaxNone) } if !hasContent { zettel.Content = oldZettel.Content } zettel.Content.TrimSpace() err = uc.port.UpdateZettel(ctx, zettel) uc.log.Sense().User(ctx).Zid(m.Zid).Err(err).Msg("Update zettel") return err } |
Added usecase/usecase.go.
> > > > > > > > > > > > | 1 2 3 4 5 6 7 8 9 10 11 12 | //----------------------------------------------------------------------------- // Copyright (c) 2021-present Detlef Stern // // This file is part of Zettelstore. // // Zettelstore is licensed under the latest version of the EUPL (European Union // Public License). Please see file LICENSE.txt for your rights and obligations // under this license. //----------------------------------------------------------------------------- // Package usecase provides (business) use cases for the zettelstore. package usecase |
Added usecase/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 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 | //----------------------------------------------------------------------------- // Copyright (c) 2022-present Detlef Stern // // This file is part of Zettelstore. // // Zettelstore is licensed under the latest version of the EUPL (European Union // Public License). Please see file LICENSE.txt for your rights and obligations // under this license. //----------------------------------------------------------------------------- package usecase import ( "regexp" "strconv" "zettelstore.de/z/kernel" ) // Version is the data for this use case. type Version struct { vr VersionResult } // NewVersion creates a new use case. func NewVersion(version string) Version { return Version{calculateVersionResult(version)} } // VersionResult is the data structure returned by this usecase. type VersionResult struct { Major int Minor int Patch int Info string Hash string } var invalidVersion = VersionResult{ Major: -1, Minor: -1, Patch: -1, Info: kernel.CoreDefaultVersion, Hash: "", } var reVersion = regexp.MustCompile(`^(\d+)\.(\d+)(\.(\d+))?(-(([[:alnum:]]|-)+))?(\+(([[:alnum:]])+(-[[:alnum:]]+)?))?`) func calculateVersionResult(version string) VersionResult { match := reVersion.FindStringSubmatch(version) if len(match) < 12 { return invalidVersion } major, err := strconv.Atoi(match[1]) if err != nil { return invalidVersion } minor, err := strconv.Atoi(match[2]) if err != nil { return invalidVersion } patch, err := strconv.Atoi(match[4]) if err != nil { patch = 0 } return VersionResult{ Major: major, Minor: minor, Patch: patch, Info: match[6], Hash: match[9], } } // Run executes the use case. func (uc Version) Run() VersionResult { return uc.vr } |
Added web/adapter/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 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 | //----------------------------------------------------------------------------- // Copyright (c) 2021-present Detlef Stern // // This file is part of Zettelstore. // // Zettelstore is licensed under the latest version of the EUPL (European Union // Public License). Please see file LICENSE.txt for your rights and obligations // under this license. //----------------------------------------------------------------------------- // Package api provides api handlers for web requests. package api import ( "bytes" "context" "net/http" "time" "zettelstore.de/c/api" "zettelstore.de/z/auth" "zettelstore.de/z/config" "zettelstore.de/z/domain/meta" "zettelstore.de/z/kernel" "zettelstore.de/z/logger" "zettelstore.de/z/web/adapter" "zettelstore.de/z/web/server" ) // API holds all data and methods for delivering API call results. type API struct { log *logger.Logger b server.Builder authz auth.AuthzManager token auth.TokenManager rtConfig config.Config policy auth.Policy tokenLifetime time.Duration } // New creates a new API object. func New(log *logger.Logger, b server.Builder, authz auth.AuthzManager, token auth.TokenManager, rtConfig config.Config, pol auth.Policy) *API { a := &API{ log: log, b: b, authz: authz, token: token, rtConfig: rtConfig, policy: pol, tokenLifetime: kernel.Main.GetConfig(kernel.WebService, kernel.WebTokenLifetimeAPI).(time.Duration), } return a } // GetURLPrefix returns the configured URL prefix of the web server. func (a *API) GetURLPrefix() string { return a.b.GetURLPrefix() } // NewURLBuilder creates a new URL builder object with the given key. func (a *API) NewURLBuilder(key byte) *api.URLBuilder { return a.b.NewURLBuilder(key) } func (a *API) getAuthData(ctx context.Context) *server.AuthData { return server.GetAuthData(ctx) } func (a *API) withAuth() bool { return a.authz.WithAuth() } func (a *API) getToken(ident *meta.Meta) ([]byte, error) { return a.token.GetToken(ident, a.tokenLifetime, auth.KindJSON) } func (a *API) reportUsecaseError(w http.ResponseWriter, err error) { code, text := adapter.CodeMessageFromError(err) if code == http.StatusInternalServerError { a.log.IfErr(err).Msg(text) http.Error(w, http.StatusText(code), code) return } // TODO: must call PrepareHeader somehow http.Error(w, text, code) } func writeBuffer(w http.ResponseWriter, buf *bytes.Buffer, contentType string) error { return adapter.WriteData(w, buf.Bytes(), contentType) } func (a *API) getRights(ctx context.Context, m *meta.Meta) (result api.ZettelRights) { pol := a.policy user := server.GetUser(ctx) if pol.CanCreate(user, m) { result |= api.ZettelCanCreate } if pol.CanRead(user, m) { result |= api.ZettelCanRead } if pol.CanWrite(user, m, m) { result |= api.ZettelCanWrite } if pol.CanRename(user, m) { result |= api.ZettelCanRename } if pol.CanDelete(user, m) { result |= api.ZettelCanDelete } if result == 0 { return api.ZettelCanNone } return result } |
Added web/adapter/api/command.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) 2021-present Detlef Stern // // This file is part of Zettelstore. // // Zettelstore is licensed under the latest version of the EUPL (European Union // Public License). Please see file LICENSE.txt for your rights and obligations // under this license. //----------------------------------------------------------------------------- package api import ( "context" "net/http" "zettelstore.de/c/api" "zettelstore.de/z/usecase" ) // MakePostCommandHandler creates a new HTTP handler to execute certain commands. func (a *API) MakePostCommandHandler( ucIsAuth *usecase.IsAuthenticated, ucRefresh *usecase.Refresh, ) http.HandlerFunc { return func(w http.ResponseWriter, r *http.Request) { ctx := r.Context() switch api.Command(r.URL.Query().Get(api.QueryKeyCommand)) { case api.CommandAuthenticated: handleIsAuthenticated(ctx, w, ucIsAuth) return case api.CommandRefresh: err := ucRefresh.Run(ctx) if err != nil { a.reportUsecaseError(w, err) return } w.WriteHeader(http.StatusNoContent) return } http.Error(w, "Unknown command", http.StatusBadRequest) } } func handleIsAuthenticated(ctx context.Context, w http.ResponseWriter, ucIsAuth *usecase.IsAuthenticated) { switch ucIsAuth.Run(ctx) { case usecase.IsAuthenticatedDisabled: w.WriteHeader(http.StatusOK) case usecase.IsAuthenticatedAndValid: w.WriteHeader(http.StatusNoContent) case usecase.IsAuthenticatedAndInvalid: w.WriteHeader(http.StatusUnauthorized) default: http.Error(w, "Unexpected result value", http.StatusInternalServerError) } } |
Added web/adapter/api/create_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 | //----------------------------------------------------------------------------- // Copyright (c) 2021-present Detlef Stern // // This file is part of Zettelstore. // // Zettelstore is licensed under the latest version of the EUPL (European Union // Public License). Please see file LICENSE.txt for your rights and obligations // under this license. //----------------------------------------------------------------------------- package api import ( "bytes" "net/http" "zettelstore.de/c/api" "zettelstore.de/z/domain" "zettelstore.de/z/domain/id" "zettelstore.de/z/usecase" "zettelstore.de/z/web/adapter" "zettelstore.de/z/web/content" ) // MakePostCreateZettelHandler creates a new HTTP handler to store content of // an existing zettel. func (a *API) MakePostCreateZettelHandler(createZettel *usecase.CreateZettel) http.HandlerFunc { return func(w http.ResponseWriter, r *http.Request) { q := r.URL.Query() enc, encStr := getEncoding(r, q) var zettel domain.Zettel var err error switch enc { case api.EncoderPlain: zettel, err = buildZettelFromPlainData(r, id.Invalid) case api.EncoderJson: zettel, err = buildZettelFromJSONData(r, id.Invalid) default: http.Error(w, http.StatusText(http.StatusBadRequest), http.StatusBadRequest) return } if err != nil { a.reportUsecaseError(w, adapter.NewErrBadRequest(err.Error())) return } ctx := r.Context() newZid, err := createZettel.Run(ctx, zettel) if err != nil { a.reportUsecaseError(w, err) return } var result []byte var contentType string location := a.NewURLBuilder('z').SetZid(api.ZettelID(newZid.String())) switch enc { case api.EncoderPlain: result = newZid.Bytes() contentType = content.PlainText case api.EncoderJson: var buf bytes.Buffer err = encodeJSONData(&buf, api.ZidJSON{ID: api.ZettelID(newZid.String())}) if err != nil { a.log.Fatal().Err(err).Zid(newZid).Msg("Unable to store new Zid in buffer") http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError) return } result = buf.Bytes() contentType = content.JSON location.AppendKVQuery(api.QueryKeyEncoding, api.EncodingJson) default: panic(encStr) } h := adapter.PrepareHeader(w, contentType) h.Set(api.HeaderLocation, location.String()) w.WriteHeader(http.StatusCreated) _, err = w.Write(result) a.log.IfErr(err).Zid(newZid).Msg("Create Zettel") } } |
Added web/adapter/api/delete_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 | //----------------------------------------------------------------------------- // Copyright (c) 2021-present Detlef Stern // // This file is part of Zettelstore. // // Zettelstore is licensed under the latest version of the EUPL (European Union // Public License). Please see file LICENSE.txt for your rights and obligations // under this license. //----------------------------------------------------------------------------- package api import ( "net/http" "zettelstore.de/z/domain/id" "zettelstore.de/z/usecase" ) // MakeDeleteZettelHandler creates a new HTTP handler to delete a zettel. func (a *API) MakeDeleteZettelHandler(deleteZettel *usecase.DeleteZettel) http.HandlerFunc { return func(w http.ResponseWriter, r *http.Request) { zid, err := id.Parse(r.URL.Path[1:]) if err != nil { http.NotFound(w, r) return } if err = deleteZettel.Run(r.Context(), zid); err != nil { a.reportUsecaseError(w, err) return } w.WriteHeader(http.StatusNoContent) } } |
Added web/adapter/api/get_data.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 | //----------------------------------------------------------------------------- // Copyright (c) 2022-present Detlef Stern // // This file is part of Zettelstore. // // Zettelstore is licensed under the latest version of the EUPL (European Union // Public License). Please see file LICENSE.txt for your rights and obligations // under this license. //----------------------------------------------------------------------------- package api import ( "bytes" "net/http" "zettelstore.de/c/api" "zettelstore.de/z/usecase" "zettelstore.de/z/web/content" ) // MakeGetDataHandler creates a new HTTP handler to return zettelstore data. func (a *API) MakeGetDataHandler(ucVersion usecase.Version) http.HandlerFunc { return func(w http.ResponseWriter, r *http.Request) { version := ucVersion.Run() result := api.VersionJSON{ Major: version.Major, Minor: version.Minor, Patch: version.Patch, Info: version.Info, Hash: version.Hash, } var buf bytes.Buffer err := encodeJSONData(&buf, result) if err != nil { a.log.Fatal().Err(err).Msg("Unable to version info in buffer") http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError) return } err = writeBuffer(w, &buf, content.JSON) a.log.IfErr(err).Msg("Write Version Info") } } |
Added web/adapter/api/get_order.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-present Detlef Stern // // This file is part of Zettelstore. // // Zettelstore is licensed under the latest version of the EUPL (European Union // Public License). Please see file LICENSE.txt for your rights and obligations // under this license. //----------------------------------------------------------------------------- package api import ( "net/http" "zettelstore.de/c/api" "zettelstore.de/z/domain/id" "zettelstore.de/z/usecase" ) // MakeGetOrderHandler creates a new API handler to return zettel references // of a given zettel. func (a *API) MakeGetOrderHandler(zettelOrder usecase.ZettelOrder) http.HandlerFunc { return func(w http.ResponseWriter, r *http.Request) { zid, err := id.Parse(r.URL.Path[1:]) if err != nil { http.NotFound(w, r) return } ctx := r.Context() q := r.URL.Query() start, metas, err := zettelOrder.Run(ctx, zid, q.Get(api.KeySyntax)) if err != nil { a.reportUsecaseError(w, err) return } err = a.writeMetaList(ctx, w, start, metas) a.log.IfErr(err).Zid(zid).Msg("Write Zettel Order") } } |
Added web/adapter/api/get_unlinked_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 | //----------------------------------------------------------------------------- // Copyright (c) 2021-present Detlef Stern // // This file is part of Zettelstore. // // Zettelstore is licensed under the latest version of the EUPL (European Union // Public License). Please see file LICENSE.txt for your rights and obligations // under this license. //----------------------------------------------------------------------------- package api import ( "bytes" "net/http" "zettelstore.de/c/api" "zettelstore.de/z/domain/id" "zettelstore.de/z/parser" "zettelstore.de/z/usecase" "zettelstore.de/z/web/adapter" "zettelstore.de/z/web/content" ) // MakeListUnlinkedMetaHandler creates a new HTTP handler for the use case "list unlinked references". func (a *API) MakeListUnlinkedMetaHandler(getMeta usecase.GetMeta, unlinkedRefs usecase.UnlinkedReferences) http.HandlerFunc { return func(w http.ResponseWriter, r *http.Request) { zid, err := id.Parse(r.URL.Path[1:]) if err != nil { http.NotFound(w, r) return } ctx := r.Context() zm, err := getMeta.Run(ctx, zid) if err != nil { a.reportUsecaseError(w, err) return } que := r.URL.Query() phrase := que.Get(api.QueryKeyPhrase) if phrase == "" { if title, found := zm.Get(api.KeyTitle); found { phrase = parser.NormalizedSpacedText(title) } } metaList, err := unlinkedRefs.Run(ctx, phrase, adapter.AddUnlinkedRefsToQuery(adapter.GetQuery(que), zm)) if err != nil { a.reportUsecaseError(w, err) return } result := api.ZidMetaRelatedList{ ID: api.ZettelID(zid.String()), Meta: zm.Map(), Rights: a.getRights(ctx, zm), List: make([]api.ZidMetaJSON, 0, len(metaList)), } for _, m := range metaList { result.List = append(result.List, api.ZidMetaJSON{ ID: api.ZettelID(m.Zid.String()), Meta: m.Map(), Rights: a.getRights(ctx, m), }) } var buf bytes.Buffer err = encodeJSONData(&buf, result) if err != nil { a.log.Fatal().Err(err).Zid(zid).Msg("Unable to store unlinked references in buffer") http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError) return } err = writeBuffer(w, &buf, content.JSON) a.log.IfErr(err).Zid(zid).Msg("Write Unlinked References") } } |
Added web/adapter/api/get_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 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 | //----------------------------------------------------------------------------- // Copyright (c) 2020-present Detlef Stern // // This file is part of Zettelstore. // // Zettelstore is licensed under the latest version of the EUPL (European Union // Public License). Please see file LICENSE.txt for your rights and obligations // under this license. //----------------------------------------------------------------------------- package api import ( "bytes" "context" "fmt" "net/http" "zettelstore.de/c/api" "zettelstore.de/z/ast" "zettelstore.de/z/box" "zettelstore.de/z/domain/id" "zettelstore.de/z/encoder" "zettelstore.de/z/parser" "zettelstore.de/z/usecase" "zettelstore.de/z/web/adapter" "zettelstore.de/z/web/content" ) // MakeGetZettelHandler creates a new HTTP handler to return a zettel in various encodings. func (a *API) MakeGetZettelHandler(getMeta usecase.GetMeta, getZettel usecase.GetZettel, parseZettel usecase.ParseZettel, evaluate usecase.Evaluate) http.HandlerFunc { return func(w http.ResponseWriter, r *http.Request) { zid, err := id.Parse(r.URL.Path[1:]) if err != nil { http.NotFound(w, r) return } q := r.URL.Query() part := getPart(q, partContent) ctx := r.Context() switch enc, encStr := getEncoding(r, q); enc { case api.EncoderPlain: a.writePlainData(w, ctx, zid, part, getMeta, getZettel) case api.EncoderJson: a.writeJSONData(w, ctx, zid, part, getMeta, getZettel) default: var zn *ast.ZettelNode var em func(value string) ast.InlineSlice if q.Has(api.QueryKeyParseOnly) { zn, err = parseZettel.Run(ctx, zid, q.Get(api.KeySyntax)) em = parser.ParseMetadata } else { zn, err = evaluate.Run(ctx, zid, q.Get(api.KeySyntax)) em = func(value string) ast.InlineSlice { return evaluate.RunMetadata(ctx, value) } } if err != nil { a.reportUsecaseError(w, err) return } a.writeEncodedZettelPart(w, zn, em, enc, encStr, part) } } } func (a *API) writePlainData(w http.ResponseWriter, ctx context.Context, zid id.Zid, part partType, getMeta usecase.GetMeta, getZettel usecase.GetZettel) { var buf bytes.Buffer var contentType string var err error switch part { case partZettel: z, err2 := getZettel.Run(box.NoEnrichContext(ctx), zid) if err2 != nil { a.reportUsecaseError(w, err2) return } _, err2 = z.Meta.Write(&buf) if err2 == nil { err2 = buf.WriteByte('\n') } if err2 == nil { _, err = z.Content.Write(&buf) } case partMeta: m, err2 := getMeta.Run(box.NoEnrichContext(ctx), zid) if err2 != nil { a.reportUsecaseError(w, err2) return } contentType = content.PlainText _, err = m.Write(&buf) case partContent: z, err2 := getZettel.Run(box.NoEnrichContext(ctx), zid) if err2 != nil { a.reportUsecaseError(w, err2) return } contentType = content.MIMEFromSyntax(z.Meta.GetDefault(api.KeySyntax, "")) _, err = z.Content.Write(&buf) } if err != nil { a.log.Fatal().Err(err).Zid(zid).Msg("Unable to store plain zettel/part in buffer") http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError) return } err = writeBuffer(w, &buf, contentType) a.log.IfErr(err).Zid(zid).Msg("Write Plain data") } func (a *API) writeJSONData(w http.ResponseWriter, ctx context.Context, zid id.Zid, part partType, getMeta usecase.GetMeta, getZettel usecase.GetZettel) { var buf bytes.Buffer var err error switch part { case partZettel: z, err2 := getZettel.Run(ctx, zid) if err2 != nil { a.reportUsecaseError(w, err2) return } zContent, encoding := z.Content.Encode() err = encodeJSONData(&buf, api.ZettelJSON{ ID: api.ZettelID(zid.String()), Meta: z.Meta.Map(), Encoding: encoding, Content: zContent, Rights: a.getRights(ctx, z.Meta), }) case partMeta: m, err2 := getMeta.Run(ctx, zid) if err2 != nil { a.reportUsecaseError(w, err2) return } err = encodeJSONData(&buf, api.MetaJSON{ Meta: m.Map(), Rights: a.getRights(ctx, m), }) case partContent: z, err2 := getZettel.Run(ctx, zid) if err2 != nil { a.reportUsecaseError(w, err2) return } zContent, encoding := z.Content.Encode() err = encodeJSONData(&buf, api.ZettelContentJSON{ Encoding: encoding, Content: zContent, }) } if err != nil { a.log.Fatal().Err(err).Zid(zid).Msg("Unable to store zettel in buffer") http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError) return } err = writeBuffer(w, &buf, content.JSON) a.log.IfErr(err).Zid(zid).Msg("Write JSON data") } func (a *API) writeEncodedZettelPart( w http.ResponseWriter, zn *ast.ZettelNode, evalMeta encoder.EvalMetaFunc, enc api.EncodingEnum, encStr string, part partType, ) { encdr := encoder.Create(enc) if encdr == nil { adapter.BadRequest(w, fmt.Sprintf("Zettel %q not available in encoding %q", zn.Meta.Zid, encStr)) return } var err error var buf bytes.Buffer switch part { case partZettel: _, err = encdr.WriteZettel(&buf, zn, evalMeta) case partMeta: _, err = encdr.WriteMeta(&buf, zn.InhMeta, evalMeta) case partContent: _, err = encdr.WriteContent(&buf, zn) } if err != nil { a.log.Fatal().Err(err).Zid(zn.Zid).Msg("Unable to store data in buffer") http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError) return } if buf.Len() == 0 { w.WriteHeader(http.StatusNoContent) return } err = writeBuffer(w, &buf, content.MIMEFromEncoding(enc)) a.log.IfErr(err).Zid(zn.Zid).Msg("Write Encoded Zettel") } |
Added web/adapter/api/get_zettel_context.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 | //----------------------------------------------------------------------------- // Copyright (c) 2020-present Detlef Stern // // This file is part of Zettelstore. // // Zettelstore is licensed under the latest version of the EUPL (European Union // Public License). Please see file LICENSE.txt for your rights and obligations // under this license. //----------------------------------------------------------------------------- package api import ( "net/http" "zettelstore.de/c/api" "zettelstore.de/z/domain/id" "zettelstore.de/z/usecase" "zettelstore.de/z/web/adapter" ) // MakeZettelContextHandler creates a new HTTP handler for the use case "zettel context". func (a *API) MakeZettelContextHandler(getContext usecase.ZettelContext) http.HandlerFunc { return func(w http.ResponseWriter, r *http.Request) { zid, err := id.Parse(r.URL.Path[1:]) if err != nil { http.NotFound(w, r) return } q := r.URL.Query() dir := adapter.GetZCDirection(q.Get(api.QueryKeyDir)) cost, ok := adapter.GetInteger(q, api.QueryKeyCost) if !ok || cost < 0 { cost = 17 } limit, ok := adapter.GetInteger(q, api.QueryKeyLimit) if !ok || limit < 0 { limit = 200 } ctx := r.Context() metaList, err := getContext.Run(ctx, zid, dir, cost, limit) if err != nil { a.reportUsecaseError(w, err) return } err = a.writeMetaList(ctx, w, metaList[0], metaList[1:]) a.log.IfErr(err).Zid(zid).Msg("Write Context") } } |
Added web/adapter/api/json.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 | //----------------------------------------------------------------------------- // Copyright (c) 2020-present Detlef Stern // // This file is part of Zettelstore. // // Zettelstore is licensed under the latest version of the EUPL (European Union // Public License). Please see file LICENSE.txt for your rights and obligations // under this license. //----------------------------------------------------------------------------- package api import ( "bytes" "context" "encoding/json" "io" "net/http" "zettelstore.de/c/api" "zettelstore.de/z/domain" "zettelstore.de/z/domain/id" "zettelstore.de/z/domain/meta" "zettelstore.de/z/web/content" ) func encodeJSONData(w io.Writer, data interface{}) error { enc := json.NewEncoder(w) enc.SetEscapeHTML(false) return enc.Encode(data) } func (a *API) writeMetaList(ctx context.Context, w http.ResponseWriter, m *meta.Meta, metaList []*meta.Meta) error { outList := make([]api.ZidMetaJSON, len(metaList)) for i, m := range metaList { outList[i].ID = api.ZettelID(m.Zid.String()) outList[i].Meta = m.Map() outList[i].Rights = a.getRights(ctx, m) } var buf bytes.Buffer err := encodeJSONData(&buf, api.ZidMetaRelatedList{ ID: api.ZettelID(m.Zid.String()), Meta: m.Map(), Rights: a.getRights(ctx, m), List: outList, }) if err != nil { a.log.Fatal().Err(err).Zid(m.Zid).Msg("Unable to store meta list in buffer") http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError) return nil } return writeBuffer(w, &buf, content.JSON) } func buildZettelFromJSONData(r *http.Request, zid id.Zid) (domain.Zettel, error) { var zettel domain.Zettel dec := json.NewDecoder(r.Body) var zettelData api.ZettelDataJSON if err := dec.Decode(&zettelData); err != nil { return zettel, err } m := meta.New(zid) for k, v := range zettelData.Meta { m.Set(meta.RemoveNonGraphic(k), meta.RemoveNonGraphic(v)) } zettel.Meta = m if err := zettel.Content.SetDecoded(zettelData.Content, zettelData.Encoding); err != nil { return zettel, err } return zettel, nil } |
Added web/adapter/api/login.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 | //----------------------------------------------------------------------------- // Copyright (c) 2020-present Detlef Stern // // This file is part of Zettelstore. // // Zettelstore is licensed under the latest version of the EUPL (European Union // Public License). Please see file LICENSE.txt for your rights and obligations // under this license. //----------------------------------------------------------------------------- package api import ( "bytes" "encoding/json" "net/http" "time" "zettelstore.de/c/api" "zettelstore.de/z/auth" "zettelstore.de/z/usecase" "zettelstore.de/z/web/adapter" "zettelstore.de/z/web/content" ) // MakePostLoginHandler creates a new HTTP handler to authenticate the given user via API. func (a *API) MakePostLoginHandler(ucAuth *usecase.Authenticate) http.HandlerFunc { return func(w http.ResponseWriter, r *http.Request) { if !a.withAuth() { err := a.writeJSONToken(w, "freeaccess", 24*366*10*time.Hour) a.log.IfErr(err).Msg("Login/free") return } var token []byte if ident, cred := retrieveIdentCred(r); ident != "" { var err error token, err = ucAuth.Run(r.Context(), r, ident, cred, a.tokenLifetime, auth.KindJSON) if err != nil { a.reportUsecaseError(w, err) return } } if len(token) == 0 { w.Header().Set("WWW-Authenticate", `Bearer realm="Default"`) http.Error(w, "Authentication failed", http.StatusUnauthorized) return } err := a.writeJSONToken(w, string(token), a.tokenLifetime) a.log.IfErr(err).Msg("Login") } } func retrieveIdentCred(r *http.Request) (string, string) { if ident, cred, ok := adapter.GetCredentialsViaForm(r); ok { return ident, cred } if ident, cred, ok := r.BasicAuth(); ok { return ident, cred } return "", "" } func (a *API) writeJSONToken(w http.ResponseWriter, token string, lifetime time.Duration) error { var buf bytes.Buffer je := json.NewEncoder(&buf) err := je.Encode(api.AuthJSON{ Token: token, Type: "Bearer", Expires: int(lifetime / time.Second), }) if err != nil { a.log.Fatal().Err(err).Msg("Unable to store token in buffer") http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError) return nil } return writeBuffer(w, &buf, content.JSON) } // MakeRenewAuthHandler creates a new HTTP handler to renew the authenticate of a user. func (a *API) MakeRenewAuthHandler() http.HandlerFunc { return func(w http.ResponseWriter, r *http.Request) { ctx := r.Context() if !a.withAuth() { err := a.writeJSONToken(w, "freeaccess", 24*366*10*time.Hour) a.log.IfErr(err).Msg("Refresh/free") return } authData := a.getAuthData(ctx) if authData == nil || len(authData.Token) == 0 || authData.User == nil { adapter.BadRequest(w, "Not authenticated") return } totalLifetime := authData.Expires.Sub(authData.Issued) currentLifetime := authData.Now.Sub(authData.Issued) // If we are in the first quarter of the tokens lifetime, return the token if currentLifetime*4 < totalLifetime { err := a.writeJSONToken(w, string(authData.Token), totalLifetime-currentLifetime) a.log.IfErr(err).Msg("Write old token") return } // Token is a little bit aged. Create a new one token, err := a.getToken(authData.User) if err != nil { a.reportUsecaseError(w, err) return } err = a.writeJSONToken(w, string(token), a.tokenLifetime) a.log.IfErr(err).Msg("Write renewed token") } } |
Added web/adapter/api/query.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 | //----------------------------------------------------------------------------- // Copyright (c) 2022-present Detlef Stern // // This file is part of Zettelstore. // // Zettelstore is licensed under the latest version of the EUPL (European Union // Public License). Please see file LICENSE.txt for your rights and obligations // under this license. //----------------------------------------------------------------------------- package api import ( "bytes" "fmt" "io" "net/http" "strconv" "strings" "zettelstore.de/c/api" "zettelstore.de/z/domain/meta" "zettelstore.de/z/query" "zettelstore.de/z/usecase" "zettelstore.de/z/web/adapter" "zettelstore.de/z/web/content" ) // MakeQueryHandler creates a new HTTP handler to perform a query. func (a *API) MakeQueryHandler(listMeta usecase.ListMeta) http.HandlerFunc { return func(w http.ResponseWriter, r *http.Request) { ctx := r.Context() q := r.URL.Query() sq := adapter.GetQuery(q) metaList, err := listMeta.Run(ctx, sq) if err != nil { a.reportUsecaseError(w, err) return } var encoder zettelEncoder var contentType string switch enc, _ := getEncoding(r, q); enc { case api.EncoderPlain: encoder = &plainZettelEncoder{} contentType = content.PlainText case api.EncoderJson: encoder = &jsonZettelEncoder{ sq: sq, getRights: func(m *meta.Meta) api.ZettelRights { return a.getRights(ctx, m) }, } contentType = content.JSON default: http.Error(w, http.StatusText(http.StatusBadRequest), http.StatusBadRequest) return } var buf bytes.Buffer err = queryAction(&buf, encoder, metaList, sq) if err != nil { a.log.Error().Err(err).Str("query", sq.String()).Msg("execute query action") http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError) } err = writeBuffer(w, &buf, contentType) a.log.IfErr(err).Msg("write result buffer") } } func queryAction(w io.Writer, enc zettelEncoder, ml []*meta.Meta, sq *query.Query) error { min, max := -1, -1 if actions := sq.Actions(); len(actions) > 0 { acts := make([]string, 0, len(actions)) for _, act := range actions { if strings.HasPrefix(act, "MIN") { if num, err := strconv.Atoi(act[3:]); err == nil && num > 0 { min = num continue } } if strings.HasPrefix(act, "MAX") { if num, err := strconv.Atoi(act[3:]); err == nil && num > 0 { max = num continue } } acts = append(acts, act) } for _, act := range acts { key := strings.ToLower(act) switch meta.Type(key) { case meta.TypeWord, meta.TypeTagSet: return encodeKeyArrangement(w, enc, ml, key, min, max) } } } return enc.writeMetaList(w, ml) } func encodeKeyArrangement(w io.Writer, enc zettelEncoder, ml []*meta.Meta, key string, min, max int) error { arr0 := meta.CreateArrangement(ml, key) arr := make(meta.Arrangement, len(arr0)) for k0, ml0 := range arr0 { if len(ml0) < min || (max > 0 && len(ml0) > max) { continue } arr[k0] = ml0 } return enc.writeArrangement(w, arr) } type zettelEncoder interface { writeMetaList(w io.Writer, ml []*meta.Meta) error writeArrangement(w io.Writer, arr meta.Arrangement) error } type plainZettelEncoder struct{} func (*plainZettelEncoder) writeMetaList(w io.Writer, ml []*meta.Meta) error { for _, m := range ml { _, err := fmt.Fprintln(w, m.Zid.String(), m.GetTitle()) if err != nil { return err } } return nil } func (*plainZettelEncoder) writeArrangement(w io.Writer, arr meta.Arrangement) error { for key, ml := range arr { _, err := io.WriteString(w, key) if err != nil { return err } for i, m := range ml { if i == 0 { _, err = io.WriteString(w, "\t") } else { _, err = io.WriteString(w, " ") } if err != nil { return err } _, err = io.WriteString(w, m.Zid.String()) if err != nil { return err } } _, err = io.WriteString(w, "\n") if err != nil { return err } } return nil } type jsonZettelEncoder struct { sq *query.Query getRights func(*meta.Meta) api.ZettelRights } func (jze *jsonZettelEncoder) writeMetaList(w io.Writer, ml []*meta.Meta) error { result := make([]api.ZidMetaJSON, 0, len(ml)) for _, m := range ml { result = append(result, api.ZidMetaJSON{ ID: api.ZettelID(m.Zid.String()), Meta: m.Map(), Rights: jze.getRights(m), }) } err := encodeJSONData(w, api.ZettelListJSON{ Query: jze.sq.String(), Human: jze.sq.Human(), List: result, }) return err } func (*jsonZettelEncoder) writeArrangement(w io.Writer, arr meta.Arrangement) error { mm := make(api.MapMeta, len(arr)) for key, metaList := range arr { zidList := make([]api.ZettelID, 0, len(metaList)) for _, m := range metaList { zidList = append(zidList, api.ZettelID(m.Zid.String())) } mm[key] = zidList } return encodeJSONData(w, api.MapListJSON{Map: mm}) } |
Added web/adapter/api/rename_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 | //----------------------------------------------------------------------------- // Copyright (c) 2021-present Detlef Stern // // This file is part of Zettelstore. // // Zettelstore is licensed under the latest version of the EUPL (European Union // Public License). Please see file LICENSE.txt for your rights and obligations // under this license. //----------------------------------------------------------------------------- package api import ( "net/http" "net/url" "zettelstore.de/c/api" "zettelstore.de/z/domain/id" "zettelstore.de/z/usecase" ) // MakeRenameZettelHandler creates a new HTTP handler to update a zettel. func (a *API) MakeRenameZettelHandler(renameZettel *usecase.RenameZettel) http.HandlerFunc { return func(w http.ResponseWriter, r *http.Request) { zid, err := id.Parse(r.URL.Path[1:]) if err != nil { http.NotFound(w, r) return } newZid, found := getDestinationZid(r) if !found { http.Error(w, http.StatusText(http.StatusBadRequest), http.StatusBadRequest) return } if err = renameZettel.Run(r.Context(), zid, newZid); err != nil { a.reportUsecaseError(w, err) return } w.WriteHeader(http.StatusNoContent) } } func getDestinationZid(r *http.Request) (id.Zid, bool) { if values, ok := r.Header[api.HeaderDestination]; ok { for _, value := range values { if zid, ok2 := getZidFromURL(value); ok2 { return zid, true } } } return id.Invalid, false } func getZidFromURL(val string) (id.Zid, bool) { u, err := url.Parse(val) if err != nil { return id.Invalid, false } if len(u.Path) < len(api.ZidVersion) { return id.Invalid, false } zid, err := id.Parse(u.Path[len(u.Path)-len(api.ZidVersion):]) if err != nil { return id.Invalid, false } return zid, true } |
Added web/adapter/api/request.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 | //----------------------------------------------------------------------------- // Copyright (c) 2020-present Detlef Stern // // This file is part of Zettelstore. // // Zettelstore is licensed under the latest version of the EUPL (European Union // Public License). Please see file LICENSE.txt for your rights and obligations // under this license. //----------------------------------------------------------------------------- package api import ( "io" "net/http" "net/url" "zettelstore.de/c/api" "zettelstore.de/z/domain" "zettelstore.de/z/domain/id" "zettelstore.de/z/domain/meta" "zettelstore.de/z/input" ) // getEncoding returns the data encoding selected by the caller. func getEncoding(r *http.Request, q url.Values) (api.EncodingEnum, string) { encoding := q.Get(api.QueryKeyEncoding) if encoding != "" { return api.Encoder(encoding), encoding } if enc, ok := getOneEncoding(r, api.HeaderAccept); ok { return api.Encoder(enc), enc } if enc, ok := getOneEncoding(r, api.HeaderContentType); ok { return api.Encoder(enc), enc } return api.EncoderPlain, api.EncoderPlain.String() } func getOneEncoding(r *http.Request, key string) (string, bool) { if values, ok := r.Header[key]; ok { for _, value := range values { if enc, ok2 := contentType2encoding(value); ok2 { return enc, true } } } return "", false } var mapCT2encoding = map[string]string{ "application/json": "json", "text/html": api.EncodingHTML, } func contentType2encoding(contentType string) (string, bool) { // TODO: only check before first ';' enc, ok := mapCT2encoding[contentType] return enc, ok } type partType int const ( _ partType = iota partMeta partContent partZettel ) var partMap = map[string]partType{ api.PartMeta: partMeta, api.PartContent: partContent, api.PartZettel: partZettel, } func getPart(q url.Values, defPart partType) partType { if part, ok := partMap[q.Get(api.QueryKeyPart)]; ok { return part } return defPart } func (p partType) String() string { switch p { case partMeta: return "meta" case partContent: return "content" case partZettel: return "zettel" } return "" } func (p partType) DefString(defPart partType) string { if p == defPart { return "" } return p.String() } func buildZettelFromPlainData(r *http.Request, zid id.Zid) (domain.Zettel, error) { b, err := io.ReadAll(r.Body) if err != nil { return domain.Zettel{}, err } inp := input.NewInput(b) m := meta.NewFromInput(zid, inp) return domain.Zettel{ Meta: m, Content: domain.NewContent(inp.Src[inp.Pos:]), }, nil } |
Added web/adapter/api/update_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 | //----------------------------------------------------------------------------- // Copyright (c) 2021-present Detlef Stern // // This file is part of Zettelstore. // // Zettelstore is licensed under the latest version of the EUPL (European Union // Public License). Please see file LICENSE.txt for your rights and obligations // under this license. //----------------------------------------------------------------------------- package api import ( "net/http" "zettelstore.de/c/api" "zettelstore.de/z/domain" "zettelstore.de/z/domain/id" "zettelstore.de/z/usecase" "zettelstore.de/z/web/adapter" ) // MakeUpdateZettelHandler creates a new HTTP handler to update a zettel. func (a *API) MakeUpdateZettelHandler(updateZettel *usecase.UpdateZettel) http.HandlerFunc { return func(w http.ResponseWriter, r *http.Request) { zid, err := id.Parse(r.URL.Path[1:]) if err != nil { http.NotFound(w, r) return } q := r.URL.Query() var zettel domain.Zettel switch enc, _ := getEncoding(r, q); enc { case api.EncoderPlain: zettel, err = buildZettelFromPlainData(r, zid) case api.EncoderJson: zettel, err = buildZettelFromJSONData(r, zid) default: http.Error(w, http.StatusText(http.StatusBadRequest), http.StatusBadRequest) return } if err != nil { a.reportUsecaseError(w, adapter.NewErrBadRequest(err.Error())) return } if err = updateZettel.Run(r.Context(), zettel, true); err != nil { a.reportUsecaseError(w, err) return } w.WriteHeader(http.StatusNoContent) } } |
Added web/adapter/errors.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 | //----------------------------------------------------------------------------- // Copyright (c) 2020-present Detlef Stern // // This file is part of Zettelstore. // // Zettelstore is licensed under the latest version of the EUPL (European Union // Public License). Please see file LICENSE.txt for your rights and obligations // under this license. //----------------------------------------------------------------------------- // Package adapter provides handlers for web requests. package adapter import "net/http" // BadRequest signals HTTP status code 400. func BadRequest(w http.ResponseWriter, text string) { http.Error(w, text, http.StatusBadRequest) } // Forbidden signals HTTP status code 403. func Forbidden(w http.ResponseWriter, text string) { http.Error(w, text, http.StatusForbidden) } // NotFound signals HTTP status code 404. func NotFound(w http.ResponseWriter, text string) { http.Error(w, text, http.StatusNotFound) } |
Added web/adapter/request.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 | //----------------------------------------------------------------------------- // Copyright (c) 2020-present Detlef Stern // // This file is part of Zettelstore. // // Zettelstore is licensed under the latest version of the EUPL (European Union // Public License). Please see file LICENSE.txt for your rights and obligations // under this license. //----------------------------------------------------------------------------- package adapter import ( "net/http" "net/url" "strconv" "strings" "zettelstore.de/c/api" "zettelstore.de/z/domain/meta" "zettelstore.de/z/kernel" "zettelstore.de/z/query" "zettelstore.de/z/usecase" ) // GetCredentialsViaForm retrieves the authentication credentions from a form. func GetCredentialsViaForm(r *http.Request) (ident, cred string, ok bool) { err := r.ParseForm() if err != nil { kernel.Main.GetLogger(kernel.WebService).Info().Err(err).Msg("Unable to parse form") return "", "", false } ident = strings.TrimSpace(r.PostFormValue("username")) cred = r.PostFormValue("password") if ident == "" { return "", "", false } return ident, cred, true } // GetInteger returns the integer value of the named query key. func GetInteger(q url.Values, key string) (int, bool) { s := q.Get(key) if s != "" { if val, err := strconv.Atoi(s); err == nil { return val, true } } return 0, false } // GetQuery retrieves the specified options from a query. func GetQuery(vals url.Values) (result *query.Query) { if exprs, found := vals[api.QueryKeyQuery]; found { result = query.Parse(strings.Join(exprs, " ")) } if seeds, found := vals[api.QueryKeySeed]; found { for _, seed := range seeds { if si, err := strconv.ParseInt(seed, 10, 31); err == nil { result = result.SetSeed(int(si)) break } } } return result } // GetZCDirection returns a direction value for a given string. func GetZCDirection(s string) usecase.ZettelContextDirection { switch s { case api.DirBackward: return usecase.ZettelContextBackward case api.DirForward: return usecase.ZettelContextForward } return usecase.ZettelContextBoth } // AddUnlinkedRefsToQuery inspects metadata and enhances the given query to ignore // some zettel identifier. func AddUnlinkedRefsToQuery(q *query.Query, m *meta.Meta) *query.Query { var sb strings.Builder sb.WriteString(api.KeyID) sb.WriteString("!:") sb.WriteString(m.Zid.String()) for _, pair := range m.ComputedPairsRest() { switch meta.Type(pair.Key) { case meta.TypeID: sb.WriteByte(' ') sb.WriteString(api.KeyID) sb.WriteString("!:") sb.WriteString(pair.Value) case meta.TypeIDSet: for _, value := range meta.ListFromValue(pair.Value) { sb.WriteByte(' ') sb.WriteString(api.KeyID) sb.WriteString("!:") sb.WriteString(value) } } } return q.Parse(sb.String()) } |
Added web/adapter/response.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 | //----------------------------------------------------------------------------- // Copyright (c) 2020-present Detlef Stern // // This file is part of Zettelstore. // // Zettelstore is licensed under the latest version of the EUPL (European Union // Public License). Please see file LICENSE.txt for your rights and obligations // under this license. //----------------------------------------------------------------------------- package adapter import ( "errors" "fmt" "net/http" "zettelstore.de/c/api" "zettelstore.de/z/box" "zettelstore.de/z/usecase" ) // WriteData emits the given data to the response writer. func WriteData(w http.ResponseWriter, data []byte, contentType string) error { if len(data) == 0 { w.WriteHeader(http.StatusNoContent) return nil } PrepareHeader(w, contentType) w.WriteHeader(http.StatusOK) _, err := w.Write(data) return err } // PrepareHeader sets the HTTP header to defined values. func PrepareHeader(w http.ResponseWriter, contentType string) http.Header { h := w.Header() if contentType != "" { h.Set(api.HeaderContentType, contentType) } return h } // ErrBadRequest is returned if the caller made an invalid HTTP request. type ErrBadRequest struct { Text string } // NewErrBadRequest creates an new bad request error. func NewErrBadRequest(text string) error { return &ErrBadRequest{Text: text} } func (err *ErrBadRequest) Error() string { return err.Text } // CodeMessageFromError returns an appropriate HTTP status code and text from a given error. func CodeMessageFromError(err error) (int, string) { if err == box.ErrNotFound { return http.StatusNotFound, http.StatusText(http.StatusNotFound) } if err1, ok := err.(*box.ErrNotAllowed); ok { return http.StatusForbidden, err1.Error() } if err1, ok := err.(*box.ErrInvalidID); ok { return http.StatusBadRequest, fmt.Sprintf("Zettel-ID %q not appropriate in this context", err1.Zid) } if err1, ok := err.(*usecase.ErrZidInUse); ok { return http.StatusBadRequest, fmt.Sprintf("Zettel-ID %q already in use", err1.Zid) } if err1, ok := err.(*ErrBadRequest); ok { return http.StatusBadRequest, err1.Text } if errors.Is(err, box.ErrStopped) { return http.StatusInternalServerError, fmt.Sprintf("Zettelstore not operational: %v", err) } if errors.Is(err, box.ErrConflict) { return http.StatusConflict, "Zettelstore operations conflicted" } if errors.Is(err, box.ErrCapacity) { return http.StatusInsufficientStorage, "Zettelstore reached one of its storage limits" } return http.StatusInternalServerError, err.Error() } |
Added web/adapter/webui/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 | //----------------------------------------------------------------------------- // Copyright (c) 2022-present Detlef Stern // // This file is part of Zettelstore. // // Zettelstore is licensed under the latest version of the EUPL (European Union // Public License). Please see file LICENSE.txt for your rights and obligations // under this license. //----------------------------------------------------------------------------- package webui // WebUI related constants. const queryKeyAction = "action" // Values for queryKeyAction const ( valueActionCopy = "copy" valueActionFolge = "folge" valueActionNew = "new" valueActionVersion = "version" ) // Enumeration for queryKeyAction type createAction uint8 const ( actionCopy createAction = iota actionFolge actionNew actionVersion ) func getCreateAction(s string) createAction { switch s { case valueActionCopy: return actionCopy case valueActionFolge: return actionFolge case valueActionNew: return actionNew case valueActionVersion: return actionVersion default: return actionCopy } } |
Added web/adapter/webui/create_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 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 | //----------------------------------------------------------------------------- // Copyright (c) 2020-present Detlef Stern // // This file is part of Zettelstore. // // Zettelstore is licensed under the latest version of the EUPL (European Union // Public License). Please see file LICENSE.txt for your rights and obligations // under this license. //----------------------------------------------------------------------------- package webui import ( "bytes" "context" "net/http" "zettelstore.de/c/api" "zettelstore.de/z/box" "zettelstore.de/z/domain" "zettelstore.de/z/domain/id" "zettelstore.de/z/domain/meta" "zettelstore.de/z/encoder/zmkenc" "zettelstore.de/z/evaluator" "zettelstore.de/z/parser" "zettelstore.de/z/usecase" "zettelstore.de/z/web/adapter" "zettelstore.de/z/web/server" ) // MakeGetCreateZettelHandler creates a new HTTP handler to display the // HTML edit view for the various zettel creation methods. func (wui *WebUI) MakeGetCreateZettelHandler( getZettel usecase.GetZettel, createZettel *usecase.CreateZettel, ucListRoles usecase.ListRoles, ucListSyntax usecase.ListSyntax) http.HandlerFunc { return func(w http.ResponseWriter, r *http.Request) { ctx := r.Context() q := r.URL.Query() op := getCreateAction(q.Get(queryKeyAction)) zid, err := id.Parse(r.URL.Path[1:]) if err != nil { wui.reportError(ctx, w, box.ErrNotFound) return } origZettel, err := getZettel.Run(box.NoEnrichContext(ctx), zid) if err != nil { wui.reportError(ctx, w, box.ErrNotFound) return } roleData, syntaxData := retrieveDataLists(ctx, ucListRoles, ucListSyntax) switch op { case actionCopy: wui.renderZettelForm(ctx, w, createZettel.PrepareCopy(origZettel), "Copy Zettel", "", roleData, syntaxData) case actionVersion: wui.renderZettelForm(ctx, w, createZettel.PrepareVersion(origZettel), "Version Zettel", "", roleData, syntaxData) case actionFolge: wui.renderZettelForm(ctx, w, createZettel.PrepareFolge(origZettel), "Folge Zettel", "", roleData, syntaxData) case actionNew: title := parser.NormalizedSpacedText(origZettel.Meta.GetTitle()) wui.renderZettelForm(ctx, w, createZettel.PrepareNew(origZettel), title, "", roleData, syntaxData) } } } func retrieveDataLists(ctx context.Context, ucListRoles usecase.ListRoles, ucListSyntax usecase.ListSyntax) ([]string, []string) { roleData := dataListFromArrangement(ucListRoles.Run(ctx)) syntaxData := dataListFromArrangement(ucListSyntax.Run(ctx)) return roleData, syntaxData } func dataListFromArrangement(ar meta.Arrangement, err error) []string { if err == nil { l := ar.Counted() l.SortByCount() return l.Categories() } return nil } func (wui *WebUI) renderZettelForm( ctx context.Context, w http.ResponseWriter, zettel domain.Zettel, title string, formActionURL string, roleData []string, syntaxData []string, ) { user := server.GetUser(ctx) m := zettel.Meta var base baseData wui.makeBaseData(ctx, wui.rtConfig.Get(ctx, m, api.KeyLang), title, "", user, &base) wui.renderTemplate(ctx, w, id.FormTemplateZid, &base, formZettelData{ Heading: title, FormActionURL: formActionURL, MetaTitle: m.GetDefault(api.KeyTitle, ""), MetaTags: m.GetDefault(api.KeyTags, ""), MetaRole: m.GetDefault(api.KeyRole, ""), HasRoleData: len(roleData) > 0, RoleData: roleData, HasSyntaxData: len(syntaxData) > 0, SyntaxData: syntaxData, MetaSyntax: m.GetDefault(api.KeySyntax, ""), MetaPairsRest: m.PairsRest(), IsTextContent: !zettel.Content.IsBinary(), Content: zettel.Content.AsString(), }) } // MakePostCreateZettelHandler creates a new HTTP handler to store content of // an existing zettel. func (wui *WebUI) MakePostCreateZettelHandler(createZettel *usecase.CreateZettel) http.HandlerFunc { return func(w http.ResponseWriter, r *http.Request) { ctx := r.Context() reEdit, zettel, err := parseZettelForm(r, id.Invalid) if err == errMissingContent { wui.reportError(ctx, w, adapter.NewErrBadRequest("Content is missing")) return } if err != nil { const msg = "Unable to read form data" wui.log.Info().Err(err).Msg(msg) wui.reportError(ctx, w, adapter.NewErrBadRequest(msg)) return } newZid, err := createZettel.Run(ctx, zettel) if err != nil { wui.reportError(ctx, w, err) return } if reEdit { wui.redirectFound(w, r, wui.NewURLBuilder('e').SetZid(api.ZettelID(newZid.String()))) } else { wui.redirectFound(w, r, wui.NewURLBuilder('h').SetZid(api.ZettelID(newZid.String()))) } } } // MakeGetZettelFromListHandler creates a new HTTP handler to store content of // an existing zettel. func (wui *WebUI) MakeGetZettelFromListHandler( listMeta usecase.ListMeta, evaluate *usecase.Evaluate, ucListRoles usecase.ListRoles, ucListSyntax usecase.ListSyntax) http.HandlerFunc { return func(w http.ResponseWriter, r *http.Request) { q := adapter.GetQuery(r.URL.Query()) ctx := r.Context() metaList, err := listMeta.Run(box.NoEnrichQuery(ctx, q), q) if err != nil { wui.reportError(ctx, w, err) return } bns := evaluate.RunBlockNode(ctx, evaluator.QueryAction(ctx, q, metaList, wui.rtConfig)) enc := zmkenc.Create() var zmkContent bytes.Buffer _, err = enc.WriteBlocks(&zmkContent, &bns) if err != nil { wui.reportError(ctx, w, err) return } m := meta.New(id.Invalid) m.Set(api.KeyTitle, q.Human()) m.Set(api.KeySyntax, api.ValueSyntaxZmk) if qval := q.String(); qval != "" { m.Set(api.KeyQuery, qval) } zettel := domain.Zettel{Meta: m, Content: domain.NewContent(zmkContent.Bytes())} roleData, syntaxData := retrieveDataLists(ctx, ucListRoles, ucListSyntax) wui.renderZettelForm(ctx, w, zettel, "Zettel from list", wui.createNewURL, roleData, syntaxData) } } |
Added web/adapter/webui/delete_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 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 | //----------------------------------------------------------------------------- // Copyright (c) 2020-present Detlef Stern // // This file is part of Zettelstore. // // Zettelstore is licensed under the latest version of the EUPL (European Union // Public License). Please see file LICENSE.txt for your rights and obligations // under this license. //----------------------------------------------------------------------------- package webui import ( "net/http" "zettelstore.de/c/api" "zettelstore.de/c/maps" "zettelstore.de/z/box" "zettelstore.de/z/domain/id" "zettelstore.de/z/domain/meta" "zettelstore.de/z/strfun" "zettelstore.de/z/usecase" "zettelstore.de/z/web/server" ) // MakeGetDeleteZettelHandler creates a new HTTP handler to display the // HTML delete view of a zettel. func (wui *WebUI) MakeGetDeleteZettelHandler(getMeta usecase.GetMeta, getAllMeta usecase.GetAllMeta) http.HandlerFunc { return func(w http.ResponseWriter, r *http.Request) { ctx := r.Context() zid, err := id.Parse(r.URL.Path[1:]) if err != nil { wui.reportError(ctx, w, box.ErrNotFound) return } ms, err := getAllMeta.Run(ctx, zid) if err != nil { wui.reportError(ctx, w, err) return } m := ms[0] var shadowedBox string var incomingLinks simpleLinks if len(ms) > 1 { shadowedBox = ms[1].GetDefault(api.KeyBoxNumber, "???") } else { getTextTitle := wui.makeGetTextTitle(ctx, getMeta) incomingLinks = wui.encodeIncoming(m, getTextTitle) } uselessFiles := retrieveUselessFiles(m) user := server.GetUser(ctx) var base baseData wui.makeBaseData(ctx, wui.rtConfig.Get(ctx, m, api.KeyLang), "Delete Zettel "+m.Zid.String(), "", user, &base) wui.renderTemplate(ctx, w, id.DeleteTemplateZid, &base, struct { Zid string MetaPairs []meta.Pair HasShadows bool ShadowedBox string Incoming simpleLinks HasUselessFiles bool UselessFiles []string }{ Zid: zid.String(), MetaPairs: m.ComputedPairs(), HasShadows: shadowedBox != "", ShadowedBox: shadowedBox, Incoming: incomingLinks, HasUselessFiles: len(uselessFiles) > 0, UselessFiles: uselessFiles, }) } } func retrieveUselessFiles(m *meta.Meta) []string { if val, found := m.Get(api.KeyUselessFiles); found { return []string{val} } return nil } // MakePostDeleteZettelHandler creates a new HTTP handler to delete a zettel. func (wui *WebUI) MakePostDeleteZettelHandler(deleteZettel *usecase.DeleteZettel) http.HandlerFunc { return func(w http.ResponseWriter, r *http.Request) { ctx := r.Context() zid, err := id.Parse(r.URL.Path[1:]) if err != nil { wui.reportError(ctx, w, box.ErrNotFound) return } if err = deleteZettel.Run(r.Context(), zid); err != nil { wui.reportError(ctx, w, err) return } wui.redirectFound(w, r, wui.NewURLBuilder('/')) } } func (wui *WebUI) encodeIncoming(m *meta.Meta, getTextTitle getTextTitleFunc) simpleLinks { zidMap := make(strfun.Set) addListValues(zidMap, m, api.KeyBackward) for _, kd := range meta.GetSortedKeyDescriptions() { inverseKey := kd.Inverse if inverseKey == "" { continue } ikd := meta.GetDescription(inverseKey) switch ikd.Type { case meta.TypeID: if val, ok := m.Get(inverseKey); ok { zidMap.Set(val) } case meta.TypeIDSet: addListValues(zidMap, m, inverseKey) } } return createSimpleLinks(wui.encodeZidLinks(maps.Keys(zidMap), getTextTitle)) } func addListValues(zidMap strfun.Set, m *meta.Meta, key string) { if values, ok := m.GetList(key); ok { for _, val := range values { zidMap.Set(val) } } } |
Added web/adapter/webui/edit_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 | //----------------------------------------------------------------------------- // Copyright (c) 2020-present Detlef Stern // // This file is part of Zettelstore. // // Zettelstore is licensed under the latest version of the EUPL (European Union // Public License). Please see file LICENSE.txt for your rights and obligations // under this license. //----------------------------------------------------------------------------- package webui import ( "net/http" "zettelstore.de/c/api" "zettelstore.de/z/box" "zettelstore.de/z/domain/id" "zettelstore.de/z/usecase" "zettelstore.de/z/web/adapter" ) // MakeEditGetZettelHandler creates a new HTTP handler to display the // HTML edit view of a zettel. func (wui *WebUI) MakeEditGetZettelHandler(getZettel usecase.GetZettel, ucListRoles usecase.ListRoles, ucListSyntax usecase.ListSyntax) http.HandlerFunc { return func(w http.ResponseWriter, r *http.Request) { ctx := r.Context() zid, err := id.Parse(r.URL.Path[1:]) if err != nil { wui.reportError(ctx, w, box.ErrNotFound) return } zettel, err := getZettel.Run(box.NoEnrichContext(ctx), zid) if err != nil { wui.reportError(ctx, w, err) return } roleData, syntaxData := retrieveDataLists(ctx, ucListRoles, ucListSyntax) wui.renderZettelForm(ctx, w, zettel, "Edit Zettel", "", roleData, syntaxData) } } // MakeEditSetZettelHandler creates a new HTTP handler to store content of // an existing zettel. func (wui *WebUI) MakeEditSetZettelHandler(updateZettel *usecase.UpdateZettel) http.HandlerFunc { return func(w http.ResponseWriter, r *http.Request) { ctx := r.Context() zid, err := id.Parse(r.URL.Path[1:]) if err != nil { wui.reportError(ctx, w, box.ErrNotFound) return } reEdit, zettel, err := parseZettelForm(r, zid) hasContent := true if err != nil { if err != errMissingContent { const msg = "Unable to read zettel form" wui.log.Info().Err(err).Msg(msg) wui.reportError(ctx, w, adapter.NewErrBadRequest(msg)) return } hasContent = false } if err = updateZettel.Run(r.Context(), zettel, hasContent); err != nil { wui.reportError(ctx, w, err) return } if reEdit { wui.redirectFound(w, r, wui.NewURLBuilder('e').SetZid(api.ZettelID(zid.String()))) } else { wui.redirectFound(w, r, wui.NewURLBuilder('h').SetZid(api.ZettelID(zid.String()))) } } } |
Added web/adapter/webui/favicon.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) 2022-present Detlef Stern // // This file is part of Zettelstore. // // Zettelstore is licensed under the latest version of the EUPL (European Union // Public License). Please see file LICENSE.txt for your rights and obligations // under this license. //----------------------------------------------------------------------------- package webui import ( "io" "net/http" "os" "path/filepath" "zettelstore.de/z/web/adapter" ) func (wui *WebUI) MakeFaviconHandler(baseDir string) http.HandlerFunc { return func(w http.ResponseWriter, r *http.Request) { filename := filepath.Join(baseDir, "favicon.ico") f, err := os.Open(filename) if err != nil { wui.log.Sense().Err(err).Msg("Favicon not found") http.Error(w, http.StatusText(http.StatusNotFound), http.StatusNotFound) return } defer f.Close() data, err := io.ReadAll(f) if err != nil { wui.log.Info().Err(err).Msg("Unable to read favicon data") http.Error(w, http.StatusText(http.StatusNotFound), http.StatusNotFound) return } err = adapter.WriteData(w, data, "") wui.log.IfErr(err).Msg("Write favicon") } } |
Added web/adapter/webui/forms.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 | //----------------------------------------------------------------------------- // Copyright (c) 2020-present Detlef Stern // // This file is part of Zettelstore. // // Zettelstore is licensed under the latest version of the EUPL (European Union // Public License). Please see file LICENSE.txt for your rights and obligations // under this license. //----------------------------------------------------------------------------- package webui import ( "bytes" "errors" "io" "net/http" "regexp" "strings" "unicode" "zettelstore.de/c/api" "zettelstore.de/z/domain" "zettelstore.de/z/domain/id" "zettelstore.de/z/domain/meta" "zettelstore.de/z/input" "zettelstore.de/z/kernel" "zettelstore.de/z/parser" "zettelstore.de/z/web/content" ) type formZettelData struct { Heading string FormActionURL string MetaTitle string MetaRole string HasRoleData bool RoleData []string HasSyntaxData bool SyntaxData []string MetaTags string MetaSyntax string MetaPairsRest []meta.Pair IsTextContent bool Content string } var ( bsCRLF = []byte{'\r', '\n'} bsLF = []byte{'\n'} ) var errMissingContent = errors.New("missing zettel content") func parseZettelForm(r *http.Request, zid id.Zid) (bool, domain.Zettel, error) { maxRequestSize := kernel.Main.GetConfig(kernel.WebService, kernel.WebMaxRequestSize).(int64) err := r.ParseMultipartForm(maxRequestSize) if err != nil { return false, domain.Zettel{}, err } _, doSave := r.Form["save"] var m *meta.Meta if postMeta, ok := trimmedFormValue(r, "meta"); ok { m = meta.NewFromInput(zid, input.NewInput(removeEmptyLines([]byte(postMeta)))) m.Sanitize() } else { m = meta.New(zid) } if postTitle, ok := trimmedFormValue(r, "title"); ok { m.Set(api.KeyTitle, meta.RemoveNonGraphic(postTitle)) } if postTags, ok := trimmedFormValue(r, "tags"); ok { if tags := strings.Fields(meta.RemoveNonGraphic(postTags)); len(tags) > 0 { m.SetList(api.KeyTags, tags) } } if postRole, ok := trimmedFormValue(r, "role"); ok { m.Set(api.KeyRole, meta.RemoveNonGraphic(postRole)) } if postSyntax, ok := trimmedFormValue(r, "syntax"); ok { m.Set(api.KeySyntax, meta.RemoveNonGraphic(postSyntax)) } if data := textContent(r); data != nil { return doSave, domain.Zettel{Meta: m, Content: domain.NewContent(data)}, nil } if data, m2 := uploadedContent(r, m); data != nil { return doSave, domain.Zettel{Meta: m2, Content: domain.NewContent(data)}, nil } if allowEmptyContent(m) { return doSave, domain.Zettel{Meta: m, Content: domain.NewContent(nil)}, nil } return doSave, domain.Zettel{Meta: m, Content: domain.NewContent(nil)}, errMissingContent } func trimmedFormValue(r *http.Request, key string) (string, bool) { if values, ok := r.PostForm[key]; ok && len(values) > 0 { value := strings.TrimSpace(values[0]) if len(value) > 0 { return value, true } } return "", false } func textContent(r *http.Request) []byte { if values, found := r.PostForm["content"]; found && len(values) > 0 { result := bytes.ReplaceAll([]byte(values[0]), bsCRLF, bsLF) if bytes.IndexFunc(result, func(ch rune) bool { return !unicode.IsSpace(ch) }) >= 0 { return result } } return nil } func uploadedContent(r *http.Request, m *meta.Meta) ([]byte, *meta.Meta) { file, fh, err := r.FormFile("file") if file != nil { defer file.Close() if err == nil { data, err2 := io.ReadAll(file) if err2 != nil { return nil, m } if cts, found := fh.Header["Content-Type"]; found && len(cts) > 0 { ct := cts[0] if fileSyntax := content.SyntaxFromMIME(ct, data); fileSyntax != "" { m = m.Clone() m.Set(api.KeySyntax, fileSyntax) } } return data, m } } return nil, m } func allowEmptyContent(m *meta.Meta) bool { if syntax, found := m.Get(api.KeySyntax); found { if syntax == api.ValueSyntaxNone { return true } if pinfo := parser.Get(syntax); pinfo != nil { return pinfo.IsTextFormat } } return true } var reEmptyLines = regexp.MustCompile(`(\n|\r)+\s*(\n|\r)+`) func removeEmptyLines(s []byte) []byte { b := bytes.TrimSpace(s) return reEmptyLines.ReplaceAllLiteral(b, []byte{'\n'}) } |
Added web/adapter/webui/forms_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 | //----------------------------------------------------------------------------- // Copyright (c) 2020-present Detlef Stern // // This file is part of Zettelstore. // // Zettelstore is licensed under the latest version of the EUPL (European Union // Public License). Please see file LICENSE.txt for your rights and obligations // under this license. //----------------------------------------------------------------------------- package webui import "testing" func TestRemoveEmptyLines(t *testing.T) { t.Parallel() testcases := []struct { in string exp string }{ {"", ""}, {"a", "a"}, {"\na", "a"}, {"a\n", "a"}, {"a\nb", "a\nb"}, {"a\n\nb", "a\nb"}, {"a\n \nb", "a\nb"}, } for i, tc := range testcases { got := string(removeEmptyLines([]byte(tc.in))) if got != tc.exp { t.Errorf("%d/%q: expected=%q, got=%q", i, tc.in, tc.exp, got) } } } |
Added web/adapter/webui/get_info.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 | //----------------------------------------------------------------------------- // Copyright (c) 2020-present Detlef Stern // // This file is part of Zettelstore. // // Zettelstore is licensed under the latest version of the EUPL (European Union // Public License). Please see file LICENSE.txt for your rights and obligations // under this license. //----------------------------------------------------------------------------- package webui import ( "context" "net/http" "sort" "strings" "codeberg.org/t73fde/sxpf" "zettelstore.de/c/api" "zettelstore.de/z/ast" "zettelstore.de/z/box" "zettelstore.de/z/collect" "zettelstore.de/z/domain/id" "zettelstore.de/z/encoder" "zettelstore.de/z/evaluator" "zettelstore.de/z/parser" "zettelstore.de/z/usecase" "zettelstore.de/z/web/adapter" "zettelstore.de/z/web/server" ) type metaDataInfo struct { Key string Value string } type matrixLine struct { Header string Elements []simpleLink } // MakeGetInfoHandler creates a new HTTP handler for the use case "get zettel". func (wui *WebUI) MakeGetInfoHandler( parseZettel usecase.ParseZettel, evaluate *usecase.Evaluate, getMeta usecase.GetMeta, getAllMeta usecase.GetAllMeta, unlinkedRefs usecase.UnlinkedReferences, ) http.HandlerFunc { return func(w http.ResponseWriter, r *http.Request) { ctx := r.Context() q := r.URL.Query() zid, err := id.Parse(r.URL.Path[1:]) if err != nil { wui.reportError(ctx, w, box.ErrNotFound) return } zn, err := parseZettel.Run(ctx, zid, q.Get(api.KeySyntax)) if err != nil { wui.reportError(ctx, w, err) return } enc := wui.getSimpleHTMLEncoder() pairs := zn.Meta.ComputedPairs() metaData := make([]metaDataInfo, len(pairs)) getTextTitle := wui.makeGetTextTitle(ctx, getMeta) for i, p := range pairs { var sb strings.Builder if sval := wui.writeHTMLMetaValue( p.Key, p.Value, getTextTitle, func(val string) ast.InlineSlice { return evaluate.RunMetadata(ctx, val) }, enc); !sxpf.IsNil(sval) { wui.htmlGen.WriteHTML(&sb, sval) } metaData[i] = metaDataInfo{p.Key, sb.String()} } summary := collect.References(zn) locLinks, qLinks, extLinks := splitLocSeaExtLinks(append(summary.Links, summary.Embeds...)) queryLinks := make([]simpleLink, len(qLinks)) for i, sq := range qLinks { queryLinks[i].Text = sq queryLinks[i].URL = wui.NewURLBuilder('h').AppendQuery(sq).String() } title := parser.NormalizedSpacedText(zn.InhMeta.GetTitle()) phrase := q.Get(api.QueryKeyPhrase) if phrase == "" { phrase = title } phrase = strings.TrimSpace(phrase) unlinkedMeta, err := unlinkedRefs.Run( ctx, phrase, adapter.AddUnlinkedRefsToQuery(nil, zn.InhMeta)) if err != nil { wui.reportError(ctx, w, err) return } bns := evaluate.RunBlockNode(ctx, evaluator.QueryAction(ctx, nil, unlinkedMeta, wui.rtConfig)) unlinkedContent, err := enc.BlocksString(&bns) if err != nil { wui.reportError(ctx, w, err) return } shadowLinks := getShadowLinks(ctx, zid, getAllMeta) endnotes, err := enc.BlocksString(&ast.BlockSlice{}) if err != nil { endnotes = "" } user := server.GetUser(ctx) canCreate := wui.canCreate(ctx, user) apiZid := api.ZettelID(zid.String()) var base baseData wui.makeBaseData(ctx, wui.rtConfig.Get(ctx, zn.InhMeta, api.KeyLang), title, "", user, &base) wui.renderTemplate(ctx, w, id.InfoTemplateZid, &base, struct { Zid string WebURL string ContextURL string CanWrite bool EditURL string CanCopy bool CopyURL string CanVersion bool VersionURL string CanFolge bool FolgeURL string CanRename bool RenameURL string CanDelete bool DeleteURL string MetaData []metaDataInfo HasLocLinks bool LocLinks []localLink QueryLinks simpleLinks HasExtLinks bool ExtLinks []string ExtNewWindow string UnLinksContent string UnLinksPhrase string QueryKeyPhrase string EvalMatrix []matrixLine ParseMatrix []matrixLine HasShadowLinks bool ShadowLinks []string Endnotes string }{ Zid: zid.String(), WebURL: wui.NewURLBuilder('h').SetZid(apiZid).String(), ContextURL: wui.NewURLBuilder('k').SetZid(apiZid).String(), CanWrite: wui.canWrite(ctx, user, zn.Meta, zn.Content), EditURL: wui.NewURLBuilder('e').SetZid(apiZid).String(), CanCopy: canCreate && !zn.Content.IsBinary(), CopyURL: wui.NewURLBuilder('c').SetZid(apiZid).AppendKVQuery(queryKeyAction, valueActionCopy).String(), CanVersion: canCreate, VersionURL: wui.NewURLBuilder('c').SetZid(apiZid).AppendKVQuery(queryKeyAction, valueActionVersion).String(), CanFolge: canCreate, FolgeURL: wui.NewURLBuilder('c').SetZid(apiZid).AppendKVQuery(queryKeyAction, valueActionFolge).String(), CanRename: wui.canRename(ctx, user, zn.Meta), RenameURL: wui.NewURLBuilder('b').SetZid(apiZid).String(), CanDelete: wui.canDelete(ctx, user, zn.Meta), DeleteURL: wui.NewURLBuilder('d').SetZid(apiZid).String(), MetaData: metaData, HasLocLinks: len(locLinks) > 0, LocLinks: locLinks, QueryLinks: createSimpleLinks(queryLinks), HasExtLinks: len(extLinks) > 0, ExtLinks: extLinks, ExtNewWindow: htmlAttrNewWindow(len(extLinks) > 0), UnLinksContent: unlinkedContent, UnLinksPhrase: phrase, QueryKeyPhrase: api.QueryKeyPhrase, EvalMatrix: wui.infoAPIMatrix(zid, false), ParseMatrix: wui.infoAPIMatrixPlain(zid), HasShadowLinks: len(shadowLinks) > 0, ShadowLinks: shadowLinks, Endnotes: endnotes, }) } } type localLink struct { Valid bool Zid string } func splitLocSeaExtLinks(links []*ast.Reference) (locLinks []localLink, queries, extLinks []string) { if len(links) == 0 { return nil, nil, nil } for _, ref := range links { if ref.State == ast.RefStateSelf || ref.IsZettel() { continue } if ref.State == ast.RefStateQuery { queries = append(queries, ref.Value) continue } if ref.IsExternal() { extLinks = append(extLinks, ref.String()) continue } locLinks = append(locLinks, localLink{ref.IsValid(), ref.String()}) } return locLinks, queries, extLinks } func (wui *WebUI) infoAPIMatrix(zid id.Zid, parseOnly bool) []matrixLine { encodings := encoder.GetEncodings() encTexts := make([]string, 0, len(encodings)) for _, f := range encodings { encTexts = append(encTexts, f.String()) } sort.Strings(encTexts) parts := getParts() matrix := make([]matrixLine, 0, len(parts)) u := wui.NewURLBuilder('z').SetZid(api.ZettelID(zid.String())) for _, part := range parts { row := make([]simpleLink, len(encTexts)) for j, enc := range encTexts { if parseOnly { u.AppendKVQuery(api.QueryKeyParseOnly, "") } u.AppendKVQuery(api.QueryKeyPart, part) u.AppendKVQuery(api.QueryKeyEncoding, enc) row[j] = simpleLink{enc, u.AttrString()} u.ClearQuery() } matrix = append(matrix, matrixLine{part, row}) } return matrix } func (wui *WebUI) infoAPIMatrixPlain(zid id.Zid) []matrixLine { matrix := wui.infoAPIMatrix(zid, true) apiZid := api.ZettelID(zid.String()) // Append plain and JSON format u := wui.NewURLBuilder('z').SetZid(apiZid) for i, part := range getParts() { u.AppendKVQuery(api.QueryKeyPart, part) matrix[i].Elements = append(matrix[i].Elements, simpleLink{"plain", u.AttrString()}) u.ClearQuery() } u.AppendKVQuery(api.QueryKeyEncoding, api.EncodingJson) u.AppendKVQuery(api.QueryKeyPart, api.PartZettel) matrix[0].Elements = append(matrix[0].Elements, simpleLink{"json", u.AttrString()}) u.ClearQuery() u.AppendKVQuery(api.QueryKeyEncoding, api.EncodingJson) u.AppendKVQuery(api.QueryKeyPart, api.PartMeta) matrix[1].Elements = append(matrix[1].Elements, simpleLink{"json", u.AttrString()}) return matrix } func getParts() []string { return []string{api.PartZettel, api.PartMeta, api.PartContent} } func getShadowLinks(ctx context.Context, zid id.Zid, getAllMeta usecase.GetAllMeta) []string { ml, err := getAllMeta.Run(ctx, zid) if err != nil || len(ml) < 2 { return nil } result := make([]string, 0, len(ml)-1) for _, m := range ml[1:] { if boxNo, ok := m.Get(api.KeyBoxNumber); ok { result = append(result, boxNo) } } return result } |
Added web/adapter/webui/get_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 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 | //----------------------------------------------------------------------------- // Copyright (c) 2020-present Detlef Stern // // This file is part of Zettelstore. // // Zettelstore is licensed under the latest version of the EUPL (European Union // Public License). Please see file LICENSE.txt for your rights and obligations // under this license. //----------------------------------------------------------------------------- package webui import ( "net/http" "strings" "zettelstore.de/c/api" "zettelstore.de/z/box" "zettelstore.de/z/domain/id" "zettelstore.de/z/domain/meta" "zettelstore.de/z/parser" "zettelstore.de/z/usecase" "zettelstore.de/z/web/server" ) // MakeGetHTMLZettelHandler creates a new HTTP handler for the use case "get zettel". func (wui *WebUI) MakeGetHTMLZettelHandler(evaluate *usecase.Evaluate, getMeta usecase.GetMeta) http.HandlerFunc { return func(w http.ResponseWriter, r *http.Request) { ctx := r.Context() zid, err := id.Parse(r.URL.Path[1:]) if err != nil { wui.reportError(ctx, w, box.ErrNotFound) return } q := r.URL.Query() zn, err := evaluate.Run(ctx, zid, q.Get(api.KeySyntax)) if err != nil { wui.reportError(ctx, w, err) return } enc := wui.getSimpleHTMLEncoder() htmlContent, err := enc.BlocksString(&zn.Ast) if err != nil { wui.reportError(ctx, w, err) return } var roleCSSURL string cssZid, err := wui.retrieveCSSZidFromRole(ctx, *zn.InhMeta) if err != nil { wui.reportError(ctx, w, err) return } if cssZid != id.Invalid { roleCSSURL = wui.NewURLBuilder('z').SetZid(api.ZettelID(cssZid.String())).String() } user := server.GetUser(ctx) roleText := zn.Meta.GetDefault(api.KeyRole, "") canCreate := wui.canCreate(ctx, user) getTextTitle := wui.makeGetTextTitle(ctx, getMeta) extURL, hasExtURL := wui.formatURLFromMeta(zn.Meta, api.KeyURL) folgeLinks := createSimpleLinks(wui.encodeZettelLinks(zn.InhMeta, api.KeyFolge, getTextTitle)) backLinks := createSimpleLinks(wui.encodeZettelLinks(zn.InhMeta, api.KeyBack, getTextTitle)) successorLinks := createSimpleLinks(wui.encodeZettelLinks(zn.InhMeta, api.KeySuccessors, getTextTitle)) apiZid := api.ZettelID(zid.String()) var base baseData title := parser.NormalizedSpacedText(zn.InhMeta.GetTitle()) wui.makeBaseData(ctx, wui.rtConfig.Get(ctx, zn.InhMeta, api.KeyLang), title, roleCSSURL, user, &base) base.MetaHeader = enc.MetaString(zn.InhMeta, createEvalMetadataFunc(ctx, evaluate)) wui.renderTemplate(ctx, w, id.ZettelTemplateZid, &base, struct { Heading string RoleCSS string CanWrite bool EditURL string Zid string InfoURL string RoleText string RoleURL string Tags simpleLinks CanCopy bool CopyURL string CanVersion bool VersionURL string CanFolge bool FolgeURL string PredecessorRefs string PrecursorRefs string HasExtURL bool ExtURL string Author string Content string NeedBottomNav bool FolgeLinks simpleLinks BackLinks simpleLinks SuccessorLinks simpleLinks }{ Heading: title, RoleCSS: roleCSSURL, CanWrite: wui.canWrite(ctx, user, zn.Meta, zn.Content), EditURL: wui.NewURLBuilder('e').SetZid(apiZid).String(), Zid: zid.String(), InfoURL: wui.NewURLBuilder('i').SetZid(apiZid).String(), RoleText: roleText, RoleURL: wui.NewURLBuilder('h').AppendQuery(api.KeyRole + api.SearchOperatorHas + roleText).String(), Tags: createSimpleLinks(wui.buildTagInfos(zn.Meta)), CanCopy: canCreate && !zn.Content.IsBinary(), CopyURL: wui.NewURLBuilder('c').SetZid(apiZid).AppendKVQuery(queryKeyAction, valueActionCopy).String(), CanVersion: canCreate, VersionURL: wui.NewURLBuilder('c').SetZid(apiZid).AppendKVQuery(queryKeyAction, valueActionVersion).String(), CanFolge: canCreate, FolgeURL: wui.NewURLBuilder('c').SetZid(apiZid).AppendKVQuery(queryKeyAction, valueActionFolge).String(), PredecessorRefs: wui.encodeIdentifierSet(zn.InhMeta, api.KeyPredecessor, getTextTitle), PrecursorRefs: wui.encodeIdentifierSet(zn.InhMeta, api.KeyPrecursor, getTextTitle), ExtURL: extURL, HasExtURL: hasExtURL, Author: zn.Meta.GetDefault(api.KeyAuthor, ""), Content: htmlContent, NeedBottomNav: folgeLinks.Has || backLinks.Has || successorLinks.Has, FolgeLinks: folgeLinks, BackLinks: backLinks, SuccessorLinks: successorLinks, }) } } func (wui *WebUI) formatURLFromMeta(m *meta.Meta, key string) (string, bool) { val, found := m.Get(key) if !found { return "", false } if val == "" { return "", false } var sb strings.Builder _, err := wui.htmlGen.WriteHTML(&sb, wui.transformURL(val)) if err != nil { return "", false } return sb.String(), true } func (wui *WebUI) buildTagInfos(m *meta.Meta) []simpleLink { var tagInfos []simpleLink if tags, ok := m.GetList(api.KeyTags); ok { ub := wui.NewURLBuilder('h') tagInfos = make([]simpleLink, len(tags)) for i, tag := range tags { tagInfos[i] = simpleLink{Text: tag, URL: ub.AppendQuery(api.KeyTags + api.SearchOperatorHas + tag).String()} ub.ClearQuery() } } return tagInfos } func (wui *WebUI) encodeIdentifierSet(m *meta.Meta, key string, getTextTitle getTextTitleFunc) string { if value, ok := m.Get(key); ok { sval := wui.transformIdentifierSet(meta.ListFromValue(value), getTextTitle) var sb strings.Builder wui.htmlGen.WriteHTML(&sb, sval) return sb.String() } return "" } func (wui *WebUI) encodeZettelLinks(m *meta.Meta, key string, getTextTitle getTextTitleFunc) []simpleLink { values, ok := m.GetList(key) if !ok || len(values) == 0 { return nil } return wui.encodeZidLinks(values, getTextTitle) } func (wui *WebUI) encodeZidLinks(values []string, getTextTitle getTextTitleFunc) []simpleLink { result := make([]simpleLink, 0, len(values)) for _, val := range values { zid, err := id.Parse(val) if err != nil { continue } if title, found := getTextTitle(zid); found > 0 { url := wui.NewURLBuilder('h').SetZid(api.ZettelID(zid.String())).String() if title == "" { result = append(result, simpleLink{Text: val, URL: url}) } else { result = append(result, simpleLink{Text: title, URL: url}) } } } return result } |
Added web/adapter/webui/goaction.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 | //----------------------------------------------------------------------------- // Copyright (c) 2020-present Detlef Stern // // This file is part of Zettelstore. // // Zettelstore is licensed under the latest version of the EUPL (European Union // Public License). Please see file LICENSE.txt for your rights and obligations // under this license. //----------------------------------------------------------------------------- package webui import ( "net/http" "zettelstore.de/z/usecase" ) // MakeGetGoActionHandler creates a new HTTP handler to execute certain commands. func (wui *WebUI) MakeGetGoActionHandler(ucRefresh *usecase.Refresh) http.HandlerFunc { return func(w http.ResponseWriter, r *http.Request) { ctx := r.Context() // Currently, command "refresh" is the only command to be executed. err := ucRefresh.Run(ctx) if err != nil { wui.reportError(ctx, w, err) return } wui.redirectFound(w, r, wui.NewURLBuilder('/')) } } |
Added web/adapter/webui/home.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) 2020-present Detlef Stern // // This file is part of Zettelstore. // // Zettelstore is licensed under the latest version of the EUPL (European Union // Public License). Please see file LICENSE.txt for your rights and obligations // under this license. //----------------------------------------------------------------------------- package webui import ( "context" "errors" "net/http" "zettelstore.de/c/api" "zettelstore.de/z/box" "zettelstore.de/z/config" "zettelstore.de/z/domain/id" "zettelstore.de/z/domain/meta" "zettelstore.de/z/web/server" ) type getRootStore interface { // GetMeta retrieves just the meta data of a specific zettel. GetMeta(ctx context.Context, zid id.Zid) (*meta.Meta, error) } // MakeGetRootHandler creates a new HTTP handler to show the root URL. func (wui *WebUI) MakeGetRootHandler(s getRootStore) http.HandlerFunc { return func(w http.ResponseWriter, r *http.Request) { ctx := r.Context() if r.URL.Path != "/" { wui.reportError(ctx, w, box.ErrNotFound) return } homeZid, _ := id.Parse(wui.rtConfig.Get(ctx, nil, config.KeyHomeZettel)) apiHomeZid := api.ZettelID(homeZid.String()) if homeZid != id.DefaultHomeZid { if _, err := s.GetMeta(ctx, homeZid); err == nil { wui.redirectFound(w, r, wui.NewURLBuilder('h').SetZid(apiHomeZid)) return } homeZid = id.DefaultHomeZid } _, err := s.GetMeta(ctx, homeZid) if err == nil { wui.redirectFound(w, r, wui.NewURLBuilder('h').SetZid(apiHomeZid)) return } if errors.Is(err, &box.ErrNotAllowed{}) && wui.authz.WithAuth() && server.GetUser(ctx) == nil { wui.redirectFound(w, r, wui.NewURLBuilder('i')) return } wui.redirectFound(w, r, wui.NewURLBuilder('h')) } } |
Added web/adapter/webui/htmlgen.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 | //----------------------------------------------------------------------------- // Copyright (c) 2022-present Detlef Stern // // This file is part of Zettelstore. // // Zettelstore is licensed under the latest version of the EUPL (European Union // Public License). Please see file LICENSE.txt for your rights and obligations // under this license. //----------------------------------------------------------------------------- package webui import ( "net/url" "strings" "codeberg.org/t73fde/sxhtml" "codeberg.org/t73fde/sxpf" "zettelstore.de/c/api" "zettelstore.de/c/attrs" "zettelstore.de/c/maps" "zettelstore.de/c/sexpr" "zettelstore.de/c/shtml" "zettelstore.de/z/ast" "zettelstore.de/z/domain/meta" "zettelstore.de/z/encoder" "zettelstore.de/z/encoder/sexprenc" "zettelstore.de/z/strfun" ) // Builder allows to build new URLs for the web service. type urlBuilder interface { GetURLPrefix() string NewURLBuilder(key byte) *api.URLBuilder } type htmlGenerator struct { tx *sexprenc.Transformer th *shtml.Transformer symAt *sxpf.Symbol } func (wui *WebUI) createGenerator(builder urlBuilder) *htmlGenerator { th := shtml.NewTransformer(1, wui.sf) symA := th.Make("a") symImg := th.Make("img") symAt := th.Make(sxhtml.NameSymAttr) symHref := th.Make("href") symClass := th.Make("class") symTarget := th.Make("target") symRel := th.Make("rel") findA := func(obj sxpf.Object) (attr, assoc, rest *sxpf.List) { lst, ok := obj.(*sxpf.List) if !ok || !symA.IsEqual(lst.Car()) { return nil, nil, nil } rest = lst.Tail() if rest == nil { return nil, nil, nil } objA := rest.Car() attr, ok = objA.(*sxpf.List) if !ok || !symAt.IsEqual(attr.Car()) { return nil, nil, nil } return attr, attr.Tail(), rest.Tail() } linkZettel := func(_ sxpf.Environment, args *sxpf.List, prevFn sxpf.Callable) sxpf.Object { obj, err := prevFn.Call(nil, args) if err != nil { return sxpf.Nil() } attr, assoc, rest := findA(obj) if attr == nil { return obj } hrefP := assoc.Assoc(symHref) if hrefP == nil { return obj } href, ok := hrefP.Cdr().(sxpf.String) if !ok { return obj } zid, fragment, hasFragment := strings.Cut(href.String(), "#") u := builder.NewURLBuilder('h').SetZid(api.ZettelID(zid)) if hasFragment { u = u.SetFragment(fragment) } assoc = assoc.Cons(sxpf.Cons(symHref, sxpf.MakeString(u.String()))) return rest.Cons(assoc.Cons(symAt)).Cons(symA) } th.SetRebinder(func(te *shtml.TransformEnv) { te.Rebind(sexpr.NameSymLinkZettel, linkZettel) te.Rebind(sexpr.NameSymLinkFound, linkZettel) te.Rebind(sexpr.NameSymLinkBased, func(_ sxpf.Environment, args *sxpf.List, prevFn sxpf.Callable) sxpf.Object { obj, err := prevFn.Call(nil, args) if err != nil { return sxpf.Nil() } attr, assoc, rest := findA(obj) if attr == nil { return obj } hrefP := assoc.Assoc(symHref) if hrefP == nil { return obj } href, ok := hrefP.Cdr().(sxpf.String) if !ok { return obj } u := builder.NewURLBuilder('/').SetRawLocal(href.String()) assoc = assoc.Cons(sxpf.Cons(symHref, sxpf.MakeString(u.String()))) return rest.Cons(assoc.Cons(symAt)).Cons(symA) }) te.Rebind(sexpr.NameSymLinkQuery, func(_ sxpf.Environment, args *sxpf.List, prevFn sxpf.Callable) sxpf.Object { obj, err := prevFn.Call(nil, args) if err != nil { return sxpf.Nil() } attr, assoc, rest := findA(obj) if attr == nil { return obj } hrefP := assoc.Assoc(symHref) if hrefP == nil { return obj } href, ok := hrefP.Cdr().(sxpf.String) if !ok { return obj } ur, err := url.Parse(href.String()) if err != nil { return obj } q := ur.Query().Get(api.QueryKeyQuery) if q == "" { return obj } u := builder.NewURLBuilder('h').AppendQuery(q) assoc = assoc.Cons(sxpf.Cons(symHref, sxpf.MakeString(u.String()))) return rest.Cons(assoc.Cons(symAt)).Cons(symA) }) te.Rebind(sexpr.NameSymLinkExternal, func(_ sxpf.Environment, args *sxpf.List, prevFn sxpf.Callable) sxpf.Object { obj, err := prevFn.Call(nil, args) if err != nil { return sxpf.Nil() } attr, assoc, rest := findA(obj) if attr == nil { return obj } assoc = assoc.Cons(sxpf.Cons(symClass, sxpf.MakeString("external"))). Cons(sxpf.Cons(symTarget, sxpf.MakeString("_blank"))). Cons(sxpf.Cons(symRel, sxpf.MakeString("noopener noreferrer"))) return rest.Cons(assoc.Cons(symAt)).Cons(symA) }) te.Rebind(sexpr.NameSymEmbed, func(_ sxpf.Environment, args *sxpf.List, prevFn sxpf.Callable) sxpf.Object { obj, err := prevFn.Call(nil, args) if err != nil { return sxpf.Nil() } lst, ok := obj.(*sxpf.List) if !ok || !symImg.IsEqual(lst.Car()) { return obj } attr, ok := lst.Tail().Car().(*sxpf.List) if !ok || !symAt.IsEqual(attr.Car()) { return obj } symSrc := th.Make("src") srcP := attr.Tail().Assoc(symSrc) if srcP == nil { return obj } src, ok := srcP.Cdr().(sxpf.String) if !ok { return obj } zid := api.ZettelID(src) if !zid.IsValid() { return obj } u := builder.NewURLBuilder('z').SetZid(zid) imgAttr := attr.Tail().Cons(sxpf.Cons(symSrc, sxpf.MakeString(u.String()))).Cons(symAt) return lst.Tail().Tail().Cons(imgAttr).Cons(symImg) }) }) return &htmlGenerator{ tx: sexprenc.NewTransformer(), th: th, symAt: symAt, } } var mapMetaKey = map[string]string{ api.KeyCopyright: "copyright", api.KeyLicense: "license", } func (g *htmlGenerator) MetaString(m *meta.Meta, evalMeta encoder.EvalMetaFunc) string { tm := g.tx.GetMeta(m, evalMeta) hm, err := g.th.Transform(tm) if err != nil { return "" } ignore := strfun.NewSet(api.KeyTitle, api.KeyLang) metaMap := make(map[string]*sxpf.List, m.Length()) if tags, ok := m.Get(api.KeyTags); ok { metaMap[api.KeyTags] = g.transformMetaTags(tags) ignore.Set(api.KeyTags) } for elem := hm; elem != nil; elem = elem.Tail() { mlst, ok := elem.Car().(*sxpf.List) if !ok { continue } att, ok := mlst.Tail().Car().(*sxpf.List) if !ok { continue } if !att.Car().IsEqual(g.symAt) { continue } a := make(attrs.Attributes, 32) for aelem := att.Tail(); aelem != nil; aelem = aelem.Tail() { if p, ok2 := aelem.Car().(*sxpf.List); ok2 { key := p.Car() val := p.Cdr() if tail, ok3 := val.(*sxpf.List); ok3 { val = tail.Car() } a = a.Set(key.String(), val.String()) } } name, found := a.Get("name") if !found || ignore.Has(name) { continue } var newName string if altName, found2 := mapMetaKey[name]; found2 { newName = altName } else { newName = "zs-" + name } a = a.Set("name", newName) metaMap[newName] = g.th.TransformMeta(a) } result := sxpf.Nil() keys := maps.Keys(metaMap) for i := len(keys) - 1; i >= 0; i-- { result = result.Cons(metaMap[keys[i]]) } var sb strings.Builder gen := sxhtml.NewGenerator(sxpf.FindSymbolFactory(result)) _, _ = gen.WriteListHTML(&sb, result) return sb.String() } func (g *htmlGenerator) transformMetaTags(tags string) *sxpf.List { var sb strings.Builder for i, val := range meta.ListFromValue(tags) { if i > 0 { sb.WriteString(", ") } sb.WriteString(strings.TrimPrefix(val, "#")) } metaTags := sb.String() if len(metaTags) == 0 { return sxpf.Nil() } return g.th.TransformMeta(attrs.Attributes{"name": "keywords", "content": metaTags}) } // BlocksString encodes a block slice. func (g *htmlGenerator) BlocksString(bs *ast.BlockSlice) (string, error) { if bs == nil || len(*bs) == 0 { return "", nil } sx := g.tx.GetSexpr(bs) sh, err := g.th.Transform(sx) if err != nil { return "", err } var sb strings.Builder gen := sxhtml.NewGenerator(sxpf.FindSymbolFactory(sh), sxhtml.WithNewline) _, err = gen.WriteListHTML(&sb, sh) if err != nil { return sb.String(), err } _, err = gen.WriteHTML(&sb, g.th.Endnotes()) return sb.String(), err } // InlinesSxHTML returns an inline slice, encoded as a SxHTML object. func (g *htmlGenerator) InlinesSxHTML(is *ast.InlineSlice) *sxpf.List { if is == nil || len(*is) == 0 { return sxpf.Nil() } sx := g.tx.GetSexpr(is) sh, err := g.th.Transform(sx) if err != nil { return sxpf.Nil() } return sh } // InlinesString returns an inline slice, encoded as a HTML string. func (g *htmlGenerator) InlinesString(is *ast.InlineSlice) string { if sh := g.InlinesSxHTML(is); !sxpf.IsNil(sh) { var sb strings.Builder gen := sxhtml.NewGenerator(sxpf.FindSymbolFactory(sh)) _, _ = gen.WriteListHTML(&sb, sh) return sb.String() } return "" } |
Added web/adapter/webui/htmlmeta.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-present Detlef Stern // // This file is part of Zettelstore. // // Zettelstore is licensed under the latest version of the EUPL (European Union // Public License). Please see file LICENSE.txt for your rights and obligations // under this license. //----------------------------------------------------------------------------- package webui import ( "context" "errors" "fmt" "net/url" "time" "codeberg.org/t73fde/sxhtml" "codeberg.org/t73fde/sxpf" "zettelstore.de/c/api" "zettelstore.de/z/ast" "zettelstore.de/z/box" "zettelstore.de/z/domain/id" "zettelstore.de/z/domain/meta" "zettelstore.de/z/parser" "zettelstore.de/z/usecase" ) func (wui *WebUI) writeHTMLMetaValue( key, value string, getTextTitle getTextTitleFunc, evalMetadata evalMetadataFunc, gen *htmlGenerator, ) sxpf.Object { var sval sxpf.Object = sxpf.Nil() switch kt := meta.Type(key); kt { case meta.TypeCredential: sval = sxpf.MakeString(value) case meta.TypeEmpty: sval = sxpf.MakeString(value) case meta.TypeID: sval = wui.transformIdentifier(value, getTextTitle) case meta.TypeIDSet: sval = wui.transformIdentifierSet(meta.ListFromValue(value), getTextTitle) case meta.TypeNumber: sval = wui.transformLink(key, value, value) case meta.TypeString: sval = sxpf.MakeString(value) case meta.TypeTagSet: sval = wui.transformTagSet(key, meta.ListFromValue(value)) case meta.TypeTimestamp: if ts, ok := meta.TimeValue(value); ok { sval = wui.transformTimestamp(ts) } case meta.TypeURL: sval = wui.transformURL(value) case meta.TypeWord: sval = wui.transformWord(key, value) case meta.TypeWordSet: sval = wui.transformWordSet(key, meta.ListFromValue(value)) case meta.TypeZettelmarkup: sval = wui.transformZmkMetadata(value, evalMetadata, gen) default: sval = sxpf.Nil().Cons(sxpf.MakeString(fmt.Sprintf(" <b>(Unhandled type: %v, key: %v)</b>", kt, key))).Cons(wui.sf.MustMake("b")) } return sval } func (wui *WebUI) transformIdentifier(val string, getTextTitle getTextTitleFunc) sxpf.Object { text := sxpf.MakeString(val) zid, err := id.Parse(val) if err != nil { return text } title, found := getTextTitle(zid) switch { case found > 0: ub := wui.NewURLBuilder('h').SetZid(api.ZettelID(zid.String())) attrs := sxpf.Nil() if title != "" { attrs = attrs.Cons(sxpf.Cons(wui.sf.MustMake("title"), sxpf.MakeString(title))) } attrs = attrs.Cons(sxpf.Cons(wui.sf.MustMake("href"), sxpf.MakeString(ub.String()))).Cons(wui.sf.MustMake(sxhtml.NameSymAttr)) return sxpf.Nil().Cons(sxpf.MakeString(zid.String())).Cons(attrs).Cons(wui.sf.MustMake("a")) case found == 0: return sxpf.Nil().Cons(text).Cons(wui.sf.MustMake("s")) default: // case found < 0: return text } } func (wui *WebUI) transformIdentifierSet(vals []string, getTextTitle getTextTitleFunc) sxpf.Object { if len(vals) == 0 { return sxpf.Nil() } space := sxpf.MakeString(" ") text := make([]sxpf.Object, 0, 2*len(vals)) for _, val := range vals { text = append(text, space, wui.transformIdentifier(val, getTextTitle)) } return sxpf.MakeList(text[1:]...).Cons(wui.sf.MustMake("span")) } func (wui *WebUI) transformTagSet(key string, tags []string) sxpf.Object { if len(tags) == 0 { return sxpf.Nil() } space := sxpf.MakeString(" ") text := make([]sxpf.Object, 0, 2*len(tags)) for _, tag := range tags { text = append(text, space, wui.transformLink(key, tag, tag)) } return sxpf.MakeList(text[1:]...).Cons(wui.sf.MustMake("span")) } func (wui *WebUI) transformTimestamp(ts time.Time) sxpf.Object { return sxpf.MakeList( wui.sf.MustMake("time"), sxpf.MakeList( wui.sf.MustMake(sxhtml.NameSymAttr), sxpf.Cons(wui.sf.MustMake("datetime"), sxpf.MakeString(ts.Format("2006-01-02T15:04:05"))), ), sxpf.MakeList(wui.sf.MustMake(sxhtml.NameSymNoEscape), sxpf.MakeString(ts.Format("2006-01-02 15:04:05"))), ) } func (wui *WebUI) transformURL(val string) sxpf.Object { text := sxpf.MakeString(val) u, err := url.Parse(val) if err == nil { if us := u.String(); us != "" { return sxpf.MakeList( wui.sf.MustMake("a"), sxpf.MakeList( wui.sf.MustMake(sxhtml.NameSymAttr), sxpf.Cons(wui.sf.MustMake("href"), sxpf.MakeString(val)), sxpf.Cons(wui.sf.MustMake("target"), sxpf.MakeString("_blank")), sxpf.Cons(wui.sf.MustMake("rel"), sxpf.MakeString("noopener noreferrer")), ), text, ) } } return text } func (wui *WebUI) transformWord(key, word string) sxpf.Object { return wui.transformLink(key, word, word) } func (wui *WebUI) transformWordSet(key string, words []string) sxpf.Object { if len(words) == 0 { return sxpf.Nil() } space := sxpf.MakeString(" ") text := make([]sxpf.Object, 0, 2*len(words)) for _, tag := range words { text = append(text, space, wui.transformWord(key, tag)) } return sxpf.MakeList(text[1:]...).Cons(wui.sf.MustMake("span")) } func (wui *WebUI) transformLink(key, value, text string) sxpf.Object { return sxpf.MakeList( wui.sf.MustMake("a"), sxpf.MakeList( wui.sf.MustMake(sxhtml.NameSymAttr), sxpf.Cons(wui.sf.MustMake("href"), sxpf.MakeString(wui.NewURLBuilder('h').AppendQuery(key+api.SearchOperatorHas+value).String())), ), sxpf.MakeString(text), ) } type evalMetadataFunc = func(string) ast.InlineSlice func createEvalMetadataFunc(ctx context.Context, evaluate *usecase.Evaluate) evalMetadataFunc { return func(value string) ast.InlineSlice { return evaluate.RunMetadata(ctx, value) } } type getTextTitleFunc func(id.Zid) (string, int) func (wui *WebUI) makeGetTextTitle(ctx context.Context, getMeta usecase.GetMeta) getTextTitleFunc { return func(zid id.Zid) (string, int) { m, err := getMeta.Run(box.NoEnrichContext(ctx), zid) if err != nil { if errors.Is(err, &box.ErrNotAllowed{}) { return "", -1 } return "", 0 } return parser.NormalizedSpacedText(m.GetTitle()), 1 } } func (wui *WebUI) transformZmkMetadata(value string, evalMetadata evalMetadataFunc, gen *htmlGenerator) sxpf.Object { is := evalMetadata(value) return gen.InlinesSxHTML(&is).Cons(wui.sf.MustMake("span")) } |
Added web/adapter/webui/lists.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 | //----------------------------------------------------------------------------- // Copyright (c) 2020-present Detlef Stern // // This file is part of Zettelstore. // // Zettelstore is licensed under the latest version of the EUPL (European Union // Public License). Please see file LICENSE.txt for your rights and obligations // under this license. //----------------------------------------------------------------------------- package webui import ( "context" "io" "net/http" "net/url" "strings" "zettelstore.de/c/api" "zettelstore.de/z/box" "zettelstore.de/z/domain/id" "zettelstore.de/z/domain/meta" "zettelstore.de/z/encoding/atom" "zettelstore.de/z/encoding/rss" "zettelstore.de/z/encoding/xml" "zettelstore.de/z/evaluator" "zettelstore.de/z/query" "zettelstore.de/z/usecase" "zettelstore.de/z/web/adapter" "zettelstore.de/z/web/server" ) // MakeListHTMLMetaHandler creates a HTTP handler for rendering the list of zettel as HTML. func (wui *WebUI) MakeListHTMLMetaHandler(listMeta usecase.ListMeta, evaluate *usecase.Evaluate) http.HandlerFunc { return func(w http.ResponseWriter, r *http.Request) { q := adapter.GetQuery(r.URL.Query()) ctx := r.Context() metaList, err := listMeta.Run(box.NoEnrichQuery(ctx, q), q) if err != nil { wui.reportError(ctx, w, err) return } if actions := q.Actions(); len(actions) > 0 { switch actions[0] { case "ATOM": wui.renderAtom(w, q, metaList) return case "RSS": wui.renderRSS(ctx, w, q, metaList) return } } bns := evaluate.RunBlockNode(ctx, evaluator.QueryAction(ctx, q, metaList, wui.rtConfig)) enc := wui.getSimpleHTMLEncoder() htmlContent, err := enc.BlocksString(&bns) if err != nil { wui.reportError(ctx, w, err) return } seed, found := q.GetSeed() if !found { seed = 0 } user := server.GetUser(ctx) var base baseData wui.makeBaseData(ctx, wui.rtConfig.Get(ctx, nil, api.KeyLang), wui.rtConfig.GetSiteName(), "", user, &base) wui.renderTemplate(ctx, w, id.ListTemplateZid, &base, struct { Title string SearchURL string QueryValue string QueryKeyQuery string Content string CanCreate bool CreateURL string QueryKeySeed string Seed int }{ Title: wui.listTitleQuery(q), SearchURL: base.SearchURL, QueryValue: q.String(), QueryKeyQuery: base.QueryKeyQuery, Content: htmlContent, CanCreate: wui.canCreate(ctx, user), CreateURL: base.CreateNewURL, QueryKeySeed: base.QueryKeySeed, Seed: seed, }) } } func (wui *WebUI) renderRSS(ctx context.Context, w http.ResponseWriter, q *query.Query, ml []*meta.Meta) { var rssConfig rss.Configuration rssConfig.Setup(ctx, wui.rtConfig) if actions := q.Actions(); len(actions) > 2 && actions[1] == "TITLE" { rssConfig.Title = strings.Join(actions[2:], " ") } data := rssConfig.Marshal(q, ml) adapter.PrepareHeader(w, rss.ContentType) w.WriteHeader(http.StatusOK) var err error if _, err = io.WriteString(w, xml.Header); err == nil { _, err = w.Write(data) } if err != nil { wui.log.IfErr(err).Msg("unable to write RSS data") } } func (wui *WebUI) renderAtom(w http.ResponseWriter, q *query.Query, ml []*meta.Meta) { var atomConfig atom.Configuration atomConfig.Setup(wui.rtConfig) if actions := q.Actions(); len(actions) > 2 && actions[1] == "TITLE" { atomConfig.Title = strings.Join(actions[2:], " ") } data := atomConfig.Marshal(q, ml) adapter.PrepareHeader(w, atom.ContentType) w.WriteHeader(http.StatusOK) var err error if _, err = io.WriteString(w, xml.Header); err == nil { _, err = w.Write(data) } if err != nil { wui.log.IfErr(err).Msg("unable to write Atom data") } } // MakeZettelContextHandler creates a new HTTP handler for the use case "zettel context". func (wui *WebUI) MakeZettelContextHandler(getContext usecase.ZettelContext, evaluate *usecase.Evaluate) http.HandlerFunc { return func(w http.ResponseWriter, r *http.Request) { ctx := r.Context() zid, err := id.Parse(r.URL.Path[1:]) if err != nil { wui.reportError(ctx, w, box.ErrNotFound) return } q := r.URL.Query() dir := adapter.GetZCDirection(q.Get(api.QueryKeyDir)) cost := getIntParameter(q, api.QueryKeyCost, 17) limit := getIntParameter(q, api.QueryKeyLimit, 200) metaList, err := getContext.Run(ctx, zid, dir, cost, limit) if err != nil { wui.reportError(ctx, w, err) return } bns := evaluate.RunBlockNode(ctx, evaluator.QueryAction(ctx, nil, metaList, wui.rtConfig)) enc := wui.getSimpleHTMLEncoder() htmlContent, err := enc.BlocksString(&bns) if err != nil { wui.reportError(ctx, w, err) return } apiZid := api.ZettelID(zid.String()) costs := []string{"2", "3", "5", "8", "13", "21", "34", "55", "89"} costLinks := make([]simpleLink, len(costs)) costURL := wui.NewURLBuilder('k').SetZid(apiZid) for i, cost := range costs { costURL.ClearQuery() switch dir { case usecase.ZettelContextBackward: costURL.AppendKVQuery(api.QueryKeyDir, api.DirBackward) case usecase.ZettelContextForward: costURL.AppendKVQuery(api.QueryKeyDir, api.DirForward) } costURL.AppendKVQuery(api.QueryKeyCost, cost) costLinks[i].Text = cost costLinks[i].URL = costURL.String() } var base baseData user := server.GetUser(ctx) wui.makeBaseData(ctx, wui.rtConfig.Get(ctx, nil, api.KeyLang), wui.rtConfig.GetSiteName(), "", user, &base) wui.renderTemplate(ctx, w, id.ContextTemplateZid, &base, struct { Title string InfoURL string Costs []simpleLink Content string }{ Title: "Zettel Context", InfoURL: wui.NewURLBuilder('i').SetZid(apiZid).String(), Costs: costLinks, Content: htmlContent, }) } } func getIntParameter(q url.Values, key string, minValue int) int { val, ok := adapter.GetInteger(q, key) if !ok || val < 0 { return minValue } return val } func (wui *WebUI) listTitleQuery(q *query.Query) string { if q == nil { return wui.rtConfig.GetSiteName() } var sb strings.Builder q.PrintHuman(&sb) return sb.String() } |
Added web/adapter/webui/login.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 | //----------------------------------------------------------------------------- // Copyright (c) 2020-present Detlef Stern // // This file is part of Zettelstore. // // Zettelstore is licensed under the latest version of the EUPL (European Union // Public License). Please see file LICENSE.txt for your rights and obligations // under this license. //----------------------------------------------------------------------------- package webui import ( "context" "net/http" "zettelstore.de/c/api" "zettelstore.de/z/auth" "zettelstore.de/z/domain/id" "zettelstore.de/z/usecase" "zettelstore.de/z/web/adapter" ) // MakeGetLoginOutHandler creates a new HTTP handler to display the HTML login view, // or to execute a logout. func (wui *WebUI) MakeGetLoginOutHandler() http.HandlerFunc { return func(w http.ResponseWriter, r *http.Request) { query := r.URL.Query() if query.Has("logout") { wui.clearToken(r.Context(), w) wui.redirectFound(w, r, wui.NewURLBuilder('/')) return } wui.renderLoginForm(wui.clearToken(r.Context(), w), w, false) } } func (wui *WebUI) renderLoginForm(ctx context.Context, w http.ResponseWriter, retry bool) { var base baseData wui.makeBaseData(ctx, wui.rtConfig.Get(ctx, nil, api.KeyLang), "Login", "", nil, &base) wui.renderTemplate(ctx, w, id.LoginTemplateZid, &base, struct { Title string Retry bool }{ Title: base.Title, Retry: retry, }) } // MakePostLoginHandler creates a new HTTP handler to authenticate the given user. func (wui *WebUI) MakePostLoginHandler(ucAuth *usecase.Authenticate) http.HandlerFunc { return func(w http.ResponseWriter, r *http.Request) { if !wui.authz.WithAuth() { wui.redirectFound(w, r, wui.NewURLBuilder('/')) return } ctx := r.Context() ident, cred, ok := adapter.GetCredentialsViaForm(r) if !ok { wui.reportError(ctx, w, adapter.NewErrBadRequest("Unable to read login form")) return } token, err := ucAuth.Run(ctx, r, ident, cred, wui.tokenLifetime, auth.KindHTML) if err != nil { wui.reportError(ctx, w, err) return } if token == nil { wui.renderLoginForm(wui.clearToken(ctx, w), w, true) return } wui.setToken(w, token) wui.redirectFound(w, r, wui.NewURLBuilder('/')) } } |
Added web/adapter/webui/rename_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 90 91 92 93 94 95 96 97 98 99 | //----------------------------------------------------------------------------- // Copyright (c) 2020-present Detlef Stern // // This file is part of Zettelstore. // // Zettelstore is licensed under the latest version of the EUPL (European Union // Public License). Please see file LICENSE.txt for your rights and obligations // under this license. //----------------------------------------------------------------------------- package webui import ( "fmt" "net/http" "strings" "zettelstore.de/c/api" "zettelstore.de/z/box" "zettelstore.de/z/domain/id" "zettelstore.de/z/domain/meta" "zettelstore.de/z/usecase" "zettelstore.de/z/web/adapter" "zettelstore.de/z/web/server" ) // MakeGetRenameZettelHandler creates a new HTTP handler to display the // HTML rename view of a zettel. func (wui *WebUI) MakeGetRenameZettelHandler(getMeta usecase.GetMeta) http.HandlerFunc { return func(w http.ResponseWriter, r *http.Request) { ctx := r.Context() zid, err := id.Parse(r.URL.Path[1:]) if err != nil { wui.reportError(ctx, w, box.ErrNotFound) return } m, err := getMeta.Run(ctx, zid) if err != nil { wui.reportError(ctx, w, err) return } getTextTitle := wui.makeGetTextTitle(ctx, getMeta) uselessFiles := retrieveUselessFiles(m) user := server.GetUser(ctx) var base baseData wui.makeBaseData(ctx, wui.rtConfig.Get(ctx, m, api.KeyLang), "Rename Zettel "+zid.String(), "", user, &base) wui.renderTemplate(ctx, w, id.RenameTemplateZid, &base, struct { Zid string MetaPairs []meta.Pair Incoming simpleLinks HasUselessFiles bool UselessFiles []string }{ Zid: zid.String(), MetaPairs: m.ComputedPairs(), Incoming: wui.encodeIncoming(m, getTextTitle), HasUselessFiles: len(uselessFiles) > 0, UselessFiles: uselessFiles, }) } } // MakePostRenameZettelHandler creates a new HTTP handler to rename an existing zettel. func (wui *WebUI) MakePostRenameZettelHandler(renameZettel *usecase.RenameZettel) http.HandlerFunc { return func(w http.ResponseWriter, r *http.Request) { ctx := r.Context() curZid, err := id.Parse(r.URL.Path[1:]) if err != nil { wui.reportError(ctx, w, box.ErrNotFound) return } if err = r.ParseForm(); err != nil { wui.reportError(ctx, w, adapter.NewErrBadRequest("Unable to read rename zettel form")) return } if formCurZid, err1 := id.Parse( r.PostFormValue("curzid")); err1 != nil || formCurZid != curZid { wui.reportError(ctx, w, adapter.NewErrBadRequest("Invalid value for current zettel id in form")) return } formNewZid := strings.TrimSpace(r.PostFormValue("newzid")) newZid, err := id.Parse(formNewZid) if err != nil { wui.reportError( ctx, w, adapter.NewErrBadRequest(fmt.Sprintf("Invalid new zettel id %q", formNewZid))) return } if err = renameZettel.Run(r.Context(), curZid, newZid); err != nil { wui.reportError(ctx, w, err) return } wui.redirectFound(w, r, wui.NewURLBuilder('h').SetZid(api.ZettelID(newZid.String()))) } } |
Added web/adapter/webui/response.go.
> > > > > > > > > > > > > > > > > > > > > > > | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 | //----------------------------------------------------------------------------- // Copyright (c) 2021-present Detlef Stern // // This file is part of Zettelstore. // // Zettelstore is licensed under the latest version of the EUPL (European Union // Public License). Please see file LICENSE.txt for your rights and obligations // under this license. //----------------------------------------------------------------------------- package webui import ( "net/http" "zettelstore.de/c/api" ) func (wui *WebUI) redirectFound(w http.ResponseWriter, r *http.Request, ub *api.URLBuilder) { us := ub.String() wui.log.Debug().Str("uri", us).Msg("redirect") http.Redirect(w, r, us, http.StatusFound) } |
Added web/adapter/webui/webui.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 | //----------------------------------------------------------------------------- // Copyright (c) 2021-present Detlef Stern // // This file is part of Zettelstore. // // Zettelstore is licensed under the latest version of the EUPL (European Union // Public License). Please see file LICENSE.txt for your rights and obligations // under this license. //----------------------------------------------------------------------------- // Package webui provides web-UI handlers for web requests. package webui import ( "context" "net/http" "strings" "sync" "time" "codeberg.org/t73fde/sxhtml" "codeberg.org/t73fde/sxpf" "zettelstore.de/c/api" "zettelstore.de/z/auth" "zettelstore.de/z/box" "zettelstore.de/z/collect" "zettelstore.de/z/config" "zettelstore.de/z/domain" "zettelstore.de/z/domain/id" "zettelstore.de/z/domain/meta" "zettelstore.de/z/encoder" "zettelstore.de/z/kernel" "zettelstore.de/z/logger" "zettelstore.de/z/parser" "zettelstore.de/z/template" "zettelstore.de/z/usecase" "zettelstore.de/z/web/adapter" "zettelstore.de/z/web/server" ) // WebUI holds all data for delivering the web ui. type WebUI struct { log *logger.Logger debug bool ab server.AuthBuilder authz auth.AuthzManager rtConfig config.Config token auth.TokenManager box webuiBox policy auth.Policy evalZettel *usecase.Evaluate mxCache sync.RWMutex templateCache map[id.Zid]*template.Template mxRoleCSSMap sync.RWMutex roleCSSMap map[string]id.Zid tokenLifetime time.Duration cssBaseURL string cssUserURL string homeURL string listZettelURL string listRolesURL string listTagsURL string refreshURL string withAuth bool loginURL string logoutURL string searchURL string createNewURL string sf sxpf.SymbolFactory htmlGen *sxhtml.Generator } type webuiBox interface { CanCreateZettel(ctx context.Context) bool GetZettel(ctx context.Context, zid id.Zid) (domain.Zettel, error) GetMeta(ctx context.Context, zid id.Zid) (*meta.Meta, error) CanUpdateZettel(ctx context.Context, zettel domain.Zettel) bool AllowRenameZettel(ctx context.Context, zid id.Zid) bool CanDeleteZettel(ctx context.Context, zid id.Zid) bool } // New creates a new WebUI struct. func New(log *logger.Logger, ab server.AuthBuilder, authz auth.AuthzManager, rtConfig config.Config, token auth.TokenManager, mgr box.Manager, pol auth.Policy, evalZettel *usecase.Evaluate) *WebUI { loginoutBase := ab.NewURLBuilder('i') sf := sxpf.MakeMappedFactory() wui := &WebUI{ log: log, debug: kernel.Main.GetConfig(kernel.CoreService, kernel.CoreDebug).(bool), ab: ab, rtConfig: rtConfig, authz: authz, token: token, box: mgr, policy: pol, evalZettel: evalZettel, tokenLifetime: kernel.Main.GetConfig(kernel.WebService, kernel.WebTokenLifetimeHTML).(time.Duration), cssBaseURL: ab.NewURLBuilder('z').SetZid(api.ZidBaseCSS).String(), cssUserURL: ab.NewURLBuilder('z').SetZid(api.ZidUserCSS).String(), homeURL: ab.NewURLBuilder('/').String(), listZettelURL: ab.NewURLBuilder('h').String(), listRolesURL: ab.NewURLBuilder('h').AppendQuery(api.ActionSeparator + api.KeyRole).String(), listTagsURL: ab.NewURLBuilder('h').AppendQuery(api.ActionSeparator + api.KeyTags).String(), refreshURL: ab.NewURLBuilder('g').AppendKVQuery("_c", "r").String(), withAuth: authz.WithAuth(), loginURL: loginoutBase.String(), logoutURL: loginoutBase.AppendKVQuery("logout", "").String(), searchURL: ab.NewURLBuilder('h').String(), createNewURL: ab.NewURLBuilder('c').String(), sf: sf, htmlGen: sxhtml.NewGenerator(sf), } wui.observe(box.UpdateInfo{Box: mgr, Reason: box.OnReload, Zid: id.Invalid}) mgr.RegisterObserver(wui.observe) return wui } func (wui *WebUI) observe(ci box.UpdateInfo) { wui.mxCache.Lock() if ci.Reason == box.OnReload || ci.Zid == id.BaseTemplateZid { wui.templateCache = make(map[id.Zid]*template.Template, len(wui.templateCache)) } else { delete(wui.templateCache, ci.Zid) } wui.mxCache.Unlock() wui.mxRoleCSSMap.Lock() if ci.Reason == box.OnReload || ci.Zid == id.RoleCSSMapZid { wui.roleCSSMap = nil } wui.mxRoleCSSMap.Unlock() } func (wui *WebUI) cacheSetTemplate(zid id.Zid, t *template.Template) { wui.mxCache.Lock() wui.templateCache[zid] = t wui.mxCache.Unlock() } func (wui *WebUI) cacheGetTemplate(zid id.Zid) (*template.Template, bool) { wui.mxCache.RLock() t, ok := wui.templateCache[zid] wui.mxCache.RUnlock() return t, ok } func (wui *WebUI) retrieveCSSZidFromRole(ctx context.Context, m meta.Meta) (id.Zid, error) { wui.mxRoleCSSMap.RLock() if wui.roleCSSMap == nil { wui.mxRoleCSSMap.RUnlock() wui.mxRoleCSSMap.Lock() mMap, err := wui.box.GetMeta(ctx, id.RoleCSSMapZid) if err == nil { wui.roleCSSMap = createRoleCSSMap(mMap) } wui.mxRoleCSSMap.Unlock() if err != nil { return id.Invalid, err } wui.mxRoleCSSMap.RLock() } defer wui.mxRoleCSSMap.RUnlock() if role, found := m.Get("css-role"); found { if result, found2 := wui.roleCSSMap[role]; found2 { return result, nil } } if role, found := m.Get(api.KeyRole); found { if result, found2 := wui.roleCSSMap[role]; found2 { return result, nil } } return id.Invalid, nil } func createRoleCSSMap(mMap *meta.Meta) map[string]id.Zid { result := make(map[string]id.Zid) for _, p := range mMap.PairsRest() { key := p.Key if len(key) < 9 || !strings.HasPrefix(key, "css-") || !strings.HasSuffix(key, "-zid") { continue } zid, err2 := id.Parse(p.Value) if err2 != nil { continue } result[key[4:len(key)-4]] = zid } return result } func (wui *WebUI) canCreate(ctx context.Context, user *meta.Meta) bool { m := meta.New(id.Invalid) return wui.policy.CanCreate(user, m) && wui.box.CanCreateZettel(ctx) } func (wui *WebUI) canWrite( ctx context.Context, user, meta *meta.Meta, content domain.Content) bool { return wui.policy.CanWrite(user, meta, meta) && wui.box.CanUpdateZettel(ctx, domain.Zettel{Meta: meta, Content: content}) } func (wui *WebUI) canRename(ctx context.Context, user, m *meta.Meta) bool { return wui.policy.CanRename(user, m) && wui.box.AllowRenameZettel(ctx, m.Zid) } func (wui *WebUI) canDelete(ctx context.Context, user, m *meta.Meta) bool { return wui.policy.CanDelete(user, m) && wui.box.CanDeleteZettel(ctx, m.Zid) } func (wui *WebUI) canRefresh(user *meta.Meta) bool { return wui.policy.CanRefresh(user) } func (wui *WebUI) getTemplate( ctx context.Context, templateID id.Zid) (*template.Template, error) { if t, ok := wui.cacheGetTemplate(templateID); ok { return t, nil } realTemplateZettel, err := wui.box.GetZettel(ctx, templateID) if err != nil { return nil, err } t, err := template.ParseString(realTemplateZettel.Content.AsString(), nil) if err == nil { // t.SetErrorOnMissing() wui.cacheSetTemplate(templateID, t) } return t, err } type simpleLink struct { Text string URL string } type simpleLinks struct { Has bool Links []simpleLink } func createSimpleLinks(ls []simpleLink) simpleLinks { return simpleLinks{ Has: len(ls) > 0, Links: ls, } } type baseData struct { Lang string MetaHeader string CSSBaseURL string CSSUserURL string CSSRoleURL string Title string HomeURL string WithUser bool WithAuth bool UserIsValid bool UserZettelURL string UserIdent string LoginURL string LogoutURL string ListZettelURL string ListRolesURL string ListTagsURL string CanRefresh bool RefreshURL string NewZettelLinks simpleLinks SearchURL string QueryKeyQuery string QueryKeySeed string CreateNewURL string Content string FooterHTML string DebugMode bool } func (wui *WebUI) makeBaseData(ctx context.Context, lang, title, roleCSSURL string, user *meta.Meta, data *baseData) { var userZettelURL string var userIdent string userIsValid := user != nil if userIsValid { userZettelURL = wui.NewURLBuilder('h').SetZid(api.ZettelID(user.Zid.String())).String() userIdent = user.GetDefault(api.KeyUserID, "") } data.Lang = lang data.CSSBaseURL = wui.cssBaseURL data.CSSUserURL = wui.cssUserURL data.CSSRoleURL = roleCSSURL data.Title = title data.HomeURL = wui.homeURL data.WithAuth = wui.withAuth data.WithUser = data.WithAuth data.UserIsValid = userIsValid data.UserZettelURL = userZettelURL data.UserIdent = userIdent data.LoginURL = wui.loginURL data.LogoutURL = wui.logoutURL data.ListZettelURL = wui.listZettelURL data.ListRolesURL = wui.listRolesURL data.ListTagsURL = wui.listTagsURL data.CanRefresh = wui.canRefresh(user) data.RefreshURL = wui.refreshURL data.NewZettelLinks = createSimpleLinks(wui.fetchNewTemplates(ctx, user)) data.SearchURL = wui.searchURL data.QueryKeyQuery = api.QueryKeyQuery data.QueryKeySeed = api.QueryKeySeed data.CreateNewURL = wui.createNewURL data.FooterHTML = wui.calculateFooterHTML(ctx) data.DebugMode = wui.debug } func (wui *WebUI) getSimpleHTMLEncoder() *htmlGenerator { return wui.createGenerator(wui) } // htmlAttrNewWindow returns HTML attribute string for opening a link in a new window. // If hasURL is false an empty string is returned. func htmlAttrNewWindow(hasURL bool) string { if hasURL { return ` target="_blank" rel="noopener noreferrer"` } return "" } func (wui *WebUI) fetchNewTemplates(ctx context.Context, user *meta.Meta) (result []simpleLink) { ctx = box.NoEnrichContext(ctx) if !wui.canCreate(ctx, user) { return nil } menu, err := wui.box.GetZettel(ctx, id.TOCNewTemplateZid) if err != nil { return nil } refs := collect.Order(parser.ParseZettel(ctx, menu, "", wui.rtConfig)) for _, ref := range refs { zid, err2 := id.Parse(ref.URL.Path) if err2 != nil { continue } m, err2 := wui.box.GetMeta(ctx, zid) if err2 != nil { continue } if !wui.policy.CanRead(user, m) { continue } result = append(result, simpleLink{ Text: parser.NormalizedSpacedText(m.GetTitle()), URL: wui.NewURLBuilder('c').SetZid(api.ZettelID(m.Zid.String())). AppendKVQuery(queryKeyAction, valueActionNew).String(), }) } return result } func (wui *WebUI) calculateFooterHTML(ctx context.Context) string { if footerZid, err := id.Parse(wui.rtConfig.Get(ctx, nil, config.KeyFooterZettel)); err == nil { if zn, err2 := wui.evalZettel.Run(ctx, footerZid, ""); err2 == nil { htmlEnc := encoder.Create(api.EncoderHTML) var sb strings.Builder if _, err2 = htmlEnc.WriteBlocks(&sb, &zn.Ast); err2 == nil { return sb.String() } } } return "" } func (wui *WebUI) renderTemplate( ctx context.Context, w http.ResponseWriter, templateID id.Zid, base *baseData, data interface{}) { wui.renderTemplateStatus(ctx, w, http.StatusOK, templateID, base, data) } func (wui *WebUI) reportError(ctx context.Context, w http.ResponseWriter, err error) { code, text := adapter.CodeMessageFromError(err) if code == http.StatusInternalServerError { wui.log.Error().Msg(err.Error()) } user := server.GetUser(ctx) var base baseData wui.makeBaseData(ctx, api.ValueLangEN, "Error", "", user, &base) wui.renderTemplateStatus(ctx, w, code, id.ErrorTemplateZid, &base, struct { ErrorTitle string ErrorText string }{ ErrorTitle: http.StatusText(code), ErrorText: text, }) } func (wui *WebUI) renderTemplateStatus( ctx context.Context, w http.ResponseWriter, code int, templateID id.Zid, base *baseData, data interface{}) { bt, err := wui.getTemplate(ctx, id.BaseTemplateZid) if err != nil { wui.log.IfErr(err).Zid(id.BaseTemplateZid).Msg("Unable to get template") http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError) return } t, err := wui.getTemplate(ctx, templateID) if err != nil { wui.log.IfErr(err).Zid(templateID).Msg("Unable to get template") http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError) return } if user := server.GetUser(ctx); user != nil { if tok, err1 := wui.token.GetToken(user, wui.tokenLifetime, auth.KindHTML); err1 == nil { wui.setToken(w, tok) } } var content strings.Builder err = t.Render(&content, data) if err == nil { wui.prepareAndWriteHeader(w, code) base.Content = content.String() err = bt.Render(w, base) } if err != nil { wui.log.IfErr(err).Msg("Unable to write HTML via template") } } // GetURLPrefix returns the configured URL prefix of the web server. func (wui *WebUI) GetURLPrefix() string { return wui.ab.GetURLPrefix() } // NewURLBuilder creates a new URL builder object with the given key. func (wui *WebUI) NewURLBuilder(key byte) *api.URLBuilder { return wui.ab.NewURLBuilder(key) } func (wui *WebUI) clearToken(ctx context.Context, w http.ResponseWriter) context.Context { return wui.ab.ClearToken(ctx, w) } func (wui *WebUI) setToken(w http.ResponseWriter, token []byte) { wui.ab.SetToken(w, token, wui.tokenLifetime) } func (wui *WebUI) prepareAndWriteHeader(w http.ResponseWriter, statusCode int) { h := adapter.PrepareHeader(w, "text/html; charset=utf-8") h.Set("Content-Security-Policy", "default-src 'self'; img-src * data:; style-src 'self' 'unsafe-inline'") h.Set("Permissions-Policy", "payment=(), interest-cohort=()") h.Set("Referrer-Policy", "no-referrer") h.Set("X-Content-Type-Options", "nosniff") if !wui.debug { h.Set("X-Frame-Options", "sameorigin") } w.WriteHeader(statusCode) } |
Added web/content/content.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 | //----------------------------------------------------------------------------- // Copyright (c) 2022-present Detlef Stern // // This file is part of Zettelstore. // // Zettelstore is licensed under the latest version of the EUPL (European Union // Public License). Please see file LICENSE.txt for your rights and obligations // under this license. //----------------------------------------------------------------------------- // Package content manages content handling within the web package. // It translates syntax values into content types, and vice versa. package content import ( "mime" "net/http" "zettelstore.de/c/api" "zettelstore.de/z/domain" "zettelstore.de/z/domain/meta" ) const ( UnknownMIME = "application/octet-stream" mimeGIF = "image/gif" mimeHTML = "text/html; charset=utf-8" mimeJPEG = "image/jpeg" mimeMarkdown = "text/markdown; charset=utf-8" JSON = "application/json" PlainText = "text/plain; charset=utf-8" mimePNG = "image/png" mimeWEBP = "image/webp" ) var encoding2mime = map[api.EncodingEnum]string{ api.EncoderHTML: mimeHTML, api.EncoderMD: mimeMarkdown, api.EncoderSexpr: PlainText, api.EncoderSHTML: PlainText, api.EncoderText: PlainText, api.EncoderZmk: PlainText, } // MIMEFromEncoding returns the MIME encoding for a given zettel encoding func MIMEFromEncoding(enc api.EncodingEnum) string { if m, found := encoding2mime[enc]; found { return m } return UnknownMIME } var syntax2mime = map[string]string{ meta.SyntaxCSS: "text/css; charset=utf-8", meta.SyntaxDraw: PlainText, meta.SyntaxGif: mimeGIF, meta.SyntaxHTML: mimeHTML, meta.SyntaxJPEG: mimeJPEG, meta.SyntaxJPG: mimeJPEG, meta.SyntaxMarkdown: mimeMarkdown, meta.SyntaxMD: mimeMarkdown, meta.SyntaxMustache: PlainText, meta.SyntaxNone: "", meta.SyntaxPlain: PlainText, meta.SyntaxPNG: mimePNG, meta.SyntaxSVG: "image/svg+xml", meta.SyntaxTxt: PlainText, meta.SyntaxText: PlainText, meta.SyntaxWebp: mimeWEBP, meta.SyntaxZmk: "text/x-zmk; charset=utf-8", // Additional syntaxes that are parsed as plain text. "js": "text/javascript; charset=utf-8", "pdf": "application/pdf", "xml": "text/xml; charset=utf-8", } // MIMEFromSyntax returns a MIME encoding for a given syntax value. func MIMEFromSyntax(syntax string) string { if mt, found := syntax2mime[syntax]; found { return mt } return UnknownMIME } var mime2syntax = map[string]string{ mimeGIF: meta.SyntaxGif, mimeJPEG: meta.SyntaxJPEG, mimePNG: meta.SyntaxPNG, mimeWEBP: meta.SyntaxWebp, "text/html": meta.SyntaxHTML, "text/markdown": meta.SyntaxMarkdown, "text/plain": meta.SyntaxText, // Additional syntaxes "application/pdf": "pdf", "text/javascript": "js", } func SyntaxFromMIME(m string, data []byte) string { mt, _, _ := mime.ParseMediaType(m) if syntax, found := mime2syntax[mt]; found { return syntax } if len(data) > 0 { ct := http.DetectContentType(data) mt, _, _ = mime.ParseMediaType(ct) if syntax, found := mime2syntax[mt]; found { return syntax } if ext, err := mime.ExtensionsByType(mt); err != nil && len(ext) > 0 { return ext[0][1:] } if domain.IsBinary(data) { return "binary" } } return "plain" } |
Added web/content/content_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 | //----------------------------------------------------------------------------- // Copyright (c) 2022-present Detlef Stern // // This file is part of Zettelstore. // // Zettelstore is licensed under the latest version of the EUPL (European Union // Public License). Please see file LICENSE.txt for your rights and obligations // under this license. //----------------------------------------------------------------------------- package content_test import ( "testing" "zettelstore.de/z/parser" "zettelstore.de/z/web/content" _ "zettelstore.de/z/parser/blob" // Allow to use BLOB parser. _ "zettelstore.de/z/parser/draw" // Allow to use draw parser. _ "zettelstore.de/z/parser/markdown" // Allow to use markdown parser. _ "zettelstore.de/z/parser/none" // Allow to use none parser. _ "zettelstore.de/z/parser/plain" // Allow to use plain parser. _ "zettelstore.de/z/parser/zettelmark" // Allow to use zettelmark parser. ) func TestSupportedSyntax(t *testing.T) { for _, syntax := range parser.GetSyntaxes() { mt := content.MIMEFromSyntax(syntax) if mt == content.UnknownMIME { t.Errorf("No MIME type registered for syntax %q", syntax) continue } newSyntax := content.SyntaxFromMIME(mt, nil) pinfo := parser.Get(newSyntax) if pinfo == nil { t.Errorf("MIME type for syntax %q is %q, but this has no corresponding syntax", syntax, mt) continue } } } |
Added web/server/impl/http.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 | //----------------------------------------------------------------------------- // Copyright (c) 2020-present Detlef Stern // // This file is part of Zettelstore. // // Zettelstore is licensed under the latest version of the EUPL (European Union // Public License). Please see file LICENSE.txt for your rights and obligations // under this license. //----------------------------------------------------------------------------- package impl import ( "context" "net" "net/http" "time" ) // Server timeout values const ( shutdownTimeout = 5 * time.Second readTimeout = 5 * time.Second writeTimeout = 10 * time.Second idleTimeout = 120 * time.Second ) // httpServer is a HTTP server. type httpServer struct { http.Server waitStop chan struct{} } // initializeHTTPServer creates a new HTTP server object. func (srv *httpServer) initializeHTTPServer(addr string, handler http.Handler) { if addr == "" { addr = ":http" } srv.Server = http.Server{ Addr: addr, Handler: handler, // See: https://blog.cloudflare.com/exposing-go-on-the-internet/ ReadTimeout: readTimeout, WriteTimeout: writeTimeout, IdleTimeout: idleTimeout, } srv.waitStop = make(chan struct{}) } // SetDebug enables debugging goroutines that are started by the server. // Basically, just the timeout values are reset. This method should be called // before running the server. func (srv *httpServer) SetDebug() { srv.ReadTimeout = 0 srv.WriteTimeout = 0 srv.IdleTimeout = 0 } // Run starts the web server, but does not wait for its completion. func (srv *httpServer) Run() error { ln, err := net.Listen("tcp", srv.Addr) if err != nil { return err } go func() { srv.Serve(ln) }() return nil } // Stop the web server. func (srv *httpServer) Stop() { ctx, cancel := context.WithTimeout(context.Background(), shutdownTimeout) defer cancel() srv.Shutdown(ctx) } |
Added web/server/impl/impl.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 | //----------------------------------------------------------------------------- // Copyright (c) 2021-present Detlef Stern // // This file is part of Zettelstore. // // Zettelstore is licensed under the latest version of the EUPL (European Union // Public License). Please see file LICENSE.txt for your rights and obligations // under this license. //----------------------------------------------------------------------------- // Package impl provides the Zettelstore web service. package impl import ( "context" "net/http" "time" "zettelstore.de/c/api" "zettelstore.de/z/auth" "zettelstore.de/z/domain/meta" "zettelstore.de/z/logger" "zettelstore.de/z/web/server" ) type myServer struct { log *logger.Logger baseURL string server httpServer router httpRouter persistentCookie bool secureCookie bool } // New creates a new web server. func New(log *logger.Logger, listenAddr, baseURL, urlPrefix string, persistentCookie, secureCookie bool, maxRequestSize int64, auth auth.TokenManager) server.Server { srv := myServer{ log: log, baseURL: baseURL, persistentCookie: persistentCookie, secureCookie: secureCookie, } srv.router.initializeRouter(log, urlPrefix, maxRequestSize, auth) srv.server.initializeHTTPServer(listenAddr, &srv.router) return &srv } func (srv *myServer) Handle(pattern string, handler http.Handler) { srv.router.Handle(pattern, handler) } func (srv *myServer) AddListRoute(key byte, method server.Method, handler http.Handler) { srv.router.addListRoute(key, method, handler) } func (srv *myServer) AddZettelRoute(key byte, method server.Method, handler http.Handler) { srv.router.addZettelRoute(key, method, handler) } func (srv *myServer) SetUserRetriever(ur server.UserRetriever) { srv.router.ur = ur } func (srv *myServer) GetURLPrefix() string { return srv.router.urlPrefix } func (srv *myServer) NewURLBuilder(key byte) *api.URLBuilder { return api.NewURLBuilder(srv.GetURLPrefix(), key) } func (srv *myServer) NewURLBuilderAbs(key byte) *api.URLBuilder { return api.NewURLBuilder(srv.baseURL, key) } const sessionName = "zsession" func (srv *myServer) SetToken(w http.ResponseWriter, token []byte, d time.Duration) { cookie := http.Cookie{ Name: sessionName, Value: string(token), Path: srv.GetURLPrefix(), Secure: srv.secureCookie, HttpOnly: true, SameSite: http.SameSiteLaxMode, } if srv.persistentCookie && d > 0 { cookie.Expires = time.Now().Add(d).Add(30 * time.Second).UTC() } srv.log.Debug().Bytes("token", token).Msg("SetToken") if v := cookie.String(); v != "" { w.Header().Add("Set-Cookie", v) w.Header().Add("Cache-Control", `no-cache="Set-Cookie"`) w.Header().Add("Vary", "Cookie") } } // ClearToken invalidates the session cookie by sending an empty one. func (srv *myServer) ClearToken(ctx context.Context, w http.ResponseWriter) context.Context { if authData := server.GetAuthData(ctx); authData == nil { // No authentication data stored in session, nothing to do. return ctx } if w != nil { srv.SetToken(w, nil, 0) } return updateContext(ctx, nil, nil) } func updateContext(ctx context.Context, user *meta.Meta, data *auth.TokenData) context.Context { if data == nil { return context.WithValue(ctx, server.CtxKeySession, &server.AuthData{User: user}) } return context.WithValue( ctx, server.CtxKeySession, &server.AuthData{ User: user, Token: data.Token, Now: data.Now, Issued: data.Issued, Expires: data.Expires, }) } func (srv *myServer) SetDebug() { srv.server.SetDebug() } func (srv *myServer) Run() error { return srv.server.Run() } func (srv *myServer) Stop() { srv.server.Stop() } |
Added web/server/impl/router.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 | //----------------------------------------------------------------------------- // Copyright (c) 2020-present Detlef Stern // // This file is part of Zettelstore. // // Zettelstore is licensed under the latest version of the EUPL (European Union // Public License). Please see file LICENSE.txt for your rights and obligations // under this license. //----------------------------------------------------------------------------- package impl import ( "io" "net/http" "regexp" "strings" "zettelstore.de/c/api" "zettelstore.de/z/auth" "zettelstore.de/z/kernel" "zettelstore.de/z/logger" "zettelstore.de/z/web/server" ) type ( methodHandler [server.MethodLAST]http.Handler routingTable [256]*methodHandler ) var mapMethod = map[string]server.Method{ http.MethodHead: server.MethodHead, http.MethodGet: server.MethodGet, http.MethodPost: server.MethodPost, http.MethodPut: server.MethodPut, http.MethodDelete: server.MethodDelete, api.MethodMove: server.MethodMove, } // httpRouter handles all routing for zettelstore. type httpRouter struct { log *logger.Logger urlPrefix string auth auth.TokenManager minKey byte maxKey byte reURL *regexp.Regexp listTable routingTable zettelTable routingTable ur server.UserRetriever mux *http.ServeMux maxReqSize int64 } // initializeRouter creates a new, empty router with the given root handler. func (rt *httpRouter) initializeRouter(log *logger.Logger, urlPrefix string, maxRequestSize int64, auth auth.TokenManager) { rt.log = log rt.urlPrefix = urlPrefix rt.auth = auth rt.minKey = 255 rt.maxKey = 0 rt.reURL = regexp.MustCompile("^$") rt.mux = http.NewServeMux() rt.maxReqSize = maxRequestSize } func (rt *httpRouter) addRoute(key byte, method server.Method, handler http.Handler, table *routingTable) { // Set minKey and maxKey; re-calculate regexp. if key < rt.minKey || rt.maxKey < key { if key < rt.minKey { rt.minKey = key } if rt.maxKey < key { rt.maxKey = key } rt.reURL = regexp.MustCompile( "^/(?:([" + string(rt.minKey) + "-" + string(rt.maxKey) + "])(?:/(?:([0-9]{14})/?)?)?)$") } mh := table[key] if mh == nil { mh = new(methodHandler) table[key] = mh } mh[method] = handler if method == server.MethodGet { if prevHandler := mh[server.MethodHead]; prevHandler == nil { mh[server.MethodHead] = handler } } } // addListRoute adds a route for the given key and HTTP method to work with a list. func (rt *httpRouter) addListRoute(key byte, method server.Method, handler http.Handler) { rt.addRoute(key, method, handler, &rt.listTable) } // addZettelRoute adds a route for the given key and HTTP method to work with a zettel. func (rt *httpRouter) addZettelRoute(key byte, method server.Method, handler http.Handler) { rt.addRoute(key, method, handler, &rt.zettelTable) } // Handle registers the handler for the given pattern. If a handler already exists for pattern, Handle panics. func (rt *httpRouter) Handle(pattern string, handler http.Handler) { rt.mux.Handle(pattern, handler) } func (rt *httpRouter) ServeHTTP(w http.ResponseWriter, r *http.Request) { // Something may panic. Ensure a kernel log. defer func() { if reco := recover(); reco != nil { rt.log.Error().Str("Method", r.Method).Str("URL", r.URL.String()).HTTPIP(r).Msg("Recover context") kernel.Main.LogRecover("Web", reco) } }() var withDebug bool if msg := rt.log.Debug(); msg.Enabled() { withDebug = true w = &traceResponseWriter{original: w} msg.Str("method", r.Method).Str("uri", r.RequestURI).HTTPIP(r).Msg("ServeHTTP") } if prefixLen := len(rt.urlPrefix); prefixLen > 1 { if len(r.URL.Path) < prefixLen || r.URL.Path[:prefixLen] != rt.urlPrefix { http.Error(w, http.StatusText(http.StatusNotFound), http.StatusNotFound) if withDebug { rt.log.Debug().Int("sc", int64(w.(*traceResponseWriter).statusCode)).Msg("/ServeHTTP/prefix") } return } r.URL.Path = r.URL.Path[prefixLen-1:] } r.Body = http.MaxBytesReader(w, r.Body, rt.maxReqSize) match := rt.reURL.FindStringSubmatch(r.URL.Path) if len(match) != 3 { rt.mux.ServeHTTP(w, rt.addUserContext(r)) if withDebug { rt.log.Debug().Int("sc", int64(w.(*traceResponseWriter).statusCode)).Msg("match other") } return } if withDebug { rt.log.Debug().Str("key", match[1]).Str("zid", match[2]).Msg("path match") } key := match[1][0] var mh *methodHandler if match[2] == "" { mh = rt.listTable[key] } else { mh = rt.zettelTable[key] } method, ok := mapMethod[r.Method] if ok && mh != nil { if handler := mh[method]; handler != nil { r.URL.Path = "/" + match[2] handler.ServeHTTP(w, rt.addUserContext(r)) if withDebug { rt.log.Debug().Int("sc", int64(w.(*traceResponseWriter).statusCode)).Msg("/ServeHTTP") } return } } http.Error(w, http.StatusText(http.StatusMethodNotAllowed), http.StatusMethodNotAllowed) if withDebug { rt.log.Debug().Int("sc", int64(w.(*traceResponseWriter).statusCode)).Msg("no match") } } func (rt *httpRouter) addUserContext(r *http.Request) *http.Request { if rt.ur == nil { // No auth needed return r } k := auth.KindJSON t := getHeaderToken(r) if len(t) == 0 { rt.log.Debug().Msg("no jwt token found") // IP already logged: ServeHTTP k = auth.KindHTML t = getSessionToken(r) } if len(t) == 0 { rt.log.Debug().Msg("no auth token found in request") // IP already logged: ServeHTTP return r } tokenData, err := rt.auth.CheckToken(t, k) if err != nil { rt.log.Sense().Err(err).HTTPIP(r).Msg("invalid auth token") return r } ctx := r.Context() user, err := rt.ur.GetUser(ctx, tokenData.Zid, tokenData.Ident) if err != nil { rt.log.Sense().Zid(tokenData.Zid).Str("ident", tokenData.Ident).Err(err).HTTPIP(r).Msg("auth user not found") return r } return r.WithContext(updateContext(ctx, user, &tokenData)) } func getSessionToken(r *http.Request) []byte { cookie, err := r.Cookie(sessionName) if err != nil { return nil } return []byte(cookie.Value) } func getHeaderToken(r *http.Request) []byte { h := r.Header["Authorization"] if h == nil { return nil } // “Multiple message-header fields with the same field-name MAY be // present in a message if and only if the entire field-value for that // header field is defined as a comma-separated list.” // — “Hypertext Transfer Protocol” RFC 2616, subsection 4.2 auth := strings.Join(h, ", ") const prefix = "Bearer " // RFC 2617, subsection 1.2 defines the scheme token as case-insensitive. if len(auth) < len(prefix) || !strings.EqualFold(auth[:len(prefix)], prefix) { return nil } return []byte(auth[len(prefix):]) } type traceResponseWriter struct { original http.ResponseWriter statusCode int } func (w *traceResponseWriter) Header() http.Header { return w.original.Header() } func (w *traceResponseWriter) Write(p []byte) (int, error) { return w.original.Write(p) } func (w *traceResponseWriter) WriteHeader(statusCode int) { w.statusCode = statusCode w.original.WriteHeader(statusCode) } func (w *traceResponseWriter) WriteString(s string) (int, error) { return io.WriteString(w.original, s) } |
Added web/server/server.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 | //----------------------------------------------------------------------------- // Copyright (c) 2020-present Detlef Stern // // This file is part of Zettelstore. // // Zettelstore is licensed under the latest version of the EUPL (European Union // Public License). Please see file LICENSE.txt for your rights and obligations // under this license. //----------------------------------------------------------------------------- // Package server provides the Zettelstore web service. package server import ( "context" "net/http" "time" "zettelstore.de/c/api" "zettelstore.de/z/domain/id" "zettelstore.de/z/domain/meta" ) // UserRetriever allows to retrieve user data based on a given zettel identifier. type UserRetriever interface { GetUser(ctx context.Context, zid id.Zid, ident string) (*meta.Meta, error) } // Method enumerates the allowed HTTP methods. type Method uint8 // Values for method type const ( MethodGet Method = iota MethodHead MethodPost MethodPut MethodMove MethodDelete MethodLAST // must always be the last one ) // Router allows to state routes for various URL paths. type Router interface { Handle(pattern string, handler http.Handler) AddListRoute(key byte, method Method, handler http.Handler) AddZettelRoute(key byte, method Method, handler http.Handler) SetUserRetriever(ur UserRetriever) } // Builder allows to build new URLs for the web service. type Builder interface { GetURLPrefix() string NewURLBuilder(key byte) *api.URLBuilder NewURLBuilderAbs(key byte) *api.URLBuilder } // Auth is the authencation interface. type Auth interface { // SetToken sends the token to the client. SetToken(w http.ResponseWriter, token []byte, d time.Duration) // ClearToken invalidates the session cookie by sending an empty one. ClearToken(ctx context.Context, w http.ResponseWriter) context.Context } // AuthData stores all relevant authentication data for a context. type AuthData struct { User *meta.Meta Token []byte Now time.Time Issued time.Time Expires time.Time } // GetAuthData returns the full authentication data from the context. func GetAuthData(ctx context.Context) *AuthData { if ctx != nil { data, ok := ctx.Value(CtxKeySession).(*AuthData) if ok { return data } } return nil } // GetUser returns the metadata of the current user, or nil if there is no one. func GetUser(ctx context.Context) *meta.Meta { if data := GetAuthData(ctx); data != nil { return data.User } return nil } // CtxKeyTypeSession is just an additional type to make context value retrieval unambiguous. type CtxKeyTypeSession struct{} // CtxKeySession is the key value to retrieve Authdata var CtxKeySession CtxKeyTypeSession // AuthBuilder is a Builder that also allows to execute authentication functions. type AuthBuilder interface { Auth Builder } // Server is the main web server for accessing Zettelstore via HTTP. type Server interface { Router Auth Builder SetDebug() Run() error Stop() } |
Changes to www/build.md.
1 | # How to build Zettelstore | < < < | | < | | > > | > | | | > | > | > > | > > | | < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < | 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 | # How to build Zettelstore ## Prerequisites You must install the following software: * A current, supported [release of Go](https://go.dev/doc/devel/release), * [staticcheck](https://staticcheck.io/), * [shadow](https://pkg.go.dev/golang.org/x/tools/go/analysis/passes/shadow), * [unparam](https://mvdan.cc/unparam), * [govulncheck](https://golang.org/x/vuln/cmd/govulncheck), * [Fossil](https://fossil-scm.org/), * [Git](https://git-scm.org) (so that Go can download some dependencies). See folder <tt>docs/development</tt> (a zettel box) for details. ## Clone the repository Most of this is covered by the excellent Fossil [documentation](https://fossil-scm.org/home/doc/trunk/www/quickstart.wiki). 1. Create a directory to store your Fossil repositories. Let's assume, you have created <tt>$HOME/fossils</tt>. 1. Clone the repository: `fossil clone https://zettelstore.de/ $HOME/fossils/zettelstore.fossil`. 1. Create a working directory. Let's assume, you have created <tt>$HOME/zettelstore</tt>. 1. Change into this directory: `cd $HOME/zettelstore`. 1. Open development: `fossil open $HOME/fossils/zettelstore.fossil`. (If you are not able to use Fossil, you could try the GitHub mirror <https://github.com/zettelstore/zettelstore>.) ## The build tool In directory <tt>tools</tt> there is a Go file called <tt>build.go</tt>. It automates most aspects, (hopefully) platform-independent. The script is called as: ``` go run tools/build.go [-v] COMMAND ``` The flag `-v` enables the verbose mode. It outputs all commands called by the tool. Some important `COMMAND`s are: * `build`: builds the software with correct version information and puts it into a freshly created directory <tt>bin</tt>. * `check`: checks the current state of the working directory to be ready for release (or commit). * `clean`: removes the build directories and cleans the Go cache. * `version`: prints the current version information. Therefore, the easiest way to build your own version of the Zettelstore software is to execute the command ``` go run tools/build.go build ``` In case of errors, please send the output of the verbose execution: ``` go run tools/build.go -v build ``` |
Changes to www/changes.wiki.
1 2 | <title>Change Log</title> | < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < | < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < | | | | | | | | | | < | | | | | | | < | | | | | | < | | | | | | | | | | | | | | 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 | <title>Change Log</title> <a id="0_12"></a> <h2>Changes for Version 0.12.0 (pending)</h2> <a id="0_11"></a> <h2>Changes for Version 0.11.0 (2023-03-27)</h2> * Remove ZJSON encoding. It was announced in version 0.10.0. Use Sexpr encoding instead. (breaking) * Title of a zettel is no longer interpreted as Zettelmarkup text. Now it is just a plain string, possibly empty. Therefore, no inline formatting (like bold text), no links, no footnotes, no citations (the latter made rendering the title often questionable, in some contexts). If you used special entities, please use the unicode characters directly. However, as a good practice, it is often the best to printable ASCII characters. (breaking) * Remove runtime configuration <tt>marker-external</tt>. It was added in version [#0_0_6|0.0.6] and updated in [#0_0_10|0.0.10]. If you want to change the marker for an external URL, you could modify zettel 00000000020001 (Zettelstore Base CSS) or zettel 00000000025001 (Zettelstore User CSS, preferred) by changing / adding a rule to add some content after an external <tt><a ...></tt> tag. (breaking: webui) * Add SHTML encoding. This allows to ensure the quality of generated HTML code. In addition, clients might use it, because it is easier to parse and manipulate than ordinary HTML. In the future, HTML template zettel will probably also use SHTML, deprecating the current Mustache syntax (which was added in [#0_0_9|0.0.9]). (major) * Search term <tt>PICK n</tt>, where <tt>n</tt> is an integer value greater zero, will pick randomly <tt>n</tt> elements from the search result list. Somehow similar (and faster) as <tt>RANDOM LIMIT n</tt>, but allows also later ordering of the resulting list. (minor) * Changed cost model for zettel context: a zettel with more outgoing/incoming references has higher cost than a zettel with less references. Also added support for traversing tags, with a similar cost model. As an effect, zettel hubs (in many cases your home zettel) will less likely add its references. Same for often used tags. The cost model might change in some details in the future, but the idea of a penalty applied to zettel / tags with many references will hold. (minor) * Some smaller bug fixes and improvements, to the software and to the documentation. <a id="0_10"></a> <h2>Changes for Version 0.10.1 (2023-01-30)</h2> * Show button to save a query into a zettel only when the current user has authorization to do it. (fix: webui) <h2>Changes for Version 0.10.0 (2023-01-24)</h2> * Remove support for endpoints <tt>/j, /m, /q, /p, /v</tt>. Their functions are merged into endpoint <tt>/z</tt>. This was announced in version 0.9.0. Please use only client library with at least version 0.10.0 too. (breaking: api) * Remove support for runtime configuration key <tt>footer-html</tt>. Use <tt>footer-zettel</tt> instead. Deprecated in version 0.9.0. (breaking: webui) * Save a query into a zettel to freeze it. (major: webui) * Allow to show all used metadata keys, linked with their occurrences and their values. (minor: webui) * Mark ZJSON encoding as deprecated for v0.11.0. Please use Sexpr encoding instead. (deprecated) * Some smaller bug fixes and improvements, to the software and to the documentation. <a id="0_9"></a> <h2>Changes for Version 0.9.0 (2022-12-12)</h2> * Remove support syntax <tt>pikchr</tt>. Although it was a nice idea to include it into Zettelstore, the implementation is too brittle (w.r.t. the expected long lifetime of Zettelstore). There should be other ways to support SVG front-ends. (breaking) * Allow to upload content when creating / updating a zettel. (major: webui) * Add syntax “draw” (again) (minor: zettelmarkup) * Allow to encode zettel in Markdown. Please note: not every aspect of a zettel can be encoded in Markdown. Those aspects will be ignored. (minor: api) * Enhance zettel context by raising the importance of folge zettel (and similar). (minor: api, webui) * Interpret zettel files with extension <tt>.webp</tt> as an binary image file format. (minor) * Allow to specify service specific log level via statup configuration and via command line. (minor) * Allow to specify a zettel to serve footer content via runtime comfiguration <tt>footer-zettel</tt>. Can be overwritten by user zettel. (minor: webui) * Footer data is automatically separated by a thematic break / horizontal rule. If you do not like it, you have to update the base template. (minor: webui) * Allow to set runtime configuration <tt>home-zettel</tt> in the user zettel to make it user-specific. (minor: webui) * Serve favicon.ico from the asset directory. (minor: webui) * Zettelmarkup cheat sheet (minor: manual) * Runtime configuration key <tt>footer-html</tt> will be removed in Version 0.10.0. Please use <tt>footer-zettel</tt> instead. (deprecated: webui) * In the next version 0.10.0, the API endpoints for a zettel (<tt>/j</tt>, <tt>/p</tt>, <tt>/v</tt>) will be merged with endpoint <tt>/z</tt>. Basically, the previous endpoint will be refactored as query parameter of endpoint <tt>/z</tt>. To reduce errors, there will be no version, where the previous endpoint are still available and the new funnctionality is still there. This is a warning to prepare for some breaking changes in v0.10.0. This also affects the API client implementation. (warning: api) * Some smaller bug fixes and improvements, to the software and to the documentation. <a id="0_8"></a> <h2>Changes for Version 0.8.0 (2022-10-20)</h2> * Remove support for tags within zettel content. Removes also property metadata keys <tt>all-tags</tt> and <tt>computed-tags</tt>. Deprecated in version 0.7.0. (breaking: zettelmarkup, api, webui) * Remove API endpoint <tt>/m</tt>, which retrieve aggregated (tags, roles) zettel identifier. Deprecated in version 0.7.0. (breaking: api) * Remove support for URL query parameter starting with an underscore. Deprecated in version 0.7.0. (breaking: api, webui) * Ignore HTML content by default, and allow HTML gradually by setting startup value <tt>insecure-html</tt>. (breaking: markup) * Endpoint <tt>/q</tt> returns list of full metadata, if no query action is specified. A HTTP call <tt>GET /z</tt> (retrieving metadata of all or some zettel) is now an alias for <tt>GET /q</tt>. (major: api) * Allow to create a zettel that acts as the new version of an existing zettel. Useful if you want to have access to older, outdated content. (minor: webui) * Allow transclusion to reference local image via URL. (minor: zettelmarkup, webui) * Add categories in RSS feed, based on zettel tags. |
︙ | ︙ | |||
585 586 587 588 589 590 591 | <a id="0_7"></a> <h2>Changes for Version 0.7.1 (2022-09-18)</h2> * Produce a RSS feed compatible to Miniflux. (minor) * Make sure to always produce a pubdata in RSS feed. (bug) * Prefix search for data that looks like a zettel identifier may end with a | | | | | | | | | < | | | | | | | | | | | | < | | | | | | | | | | | | | 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 | <a id="0_7"></a> <h2>Changes for Version 0.7.1 (2022-09-18)</h2> * Produce a RSS feed compatible to Miniflux. (minor) * Make sure to always produce a pubdata in RSS feed. (bug) * Prefix search for data that looks like a zettel identifier may end with a <tt>0</tt>. (bug) * Fix glitch on manual zettel. (bug) <h2>Changes for Version 0.7.0 (2022-09-17)</h2> * Removes support for URL query parameter to search for metadata values, sorting, offset, and limit a zettel list. Deprecated in version 0.6.0 (breaking: api, webui) * Allow to search for the existence / non-existence of a metadata key with the "?" operator: <tt>key?</tt> and <tt>key!?</tt>. Previously, the ":" operator was used for this by specifying an empty search value. Now you can use the ":" operator to find empty / non-empty metadata values. If you specify a search operator for metadata, the specified key is assumed to exist. (breaking: api, webui) * Rename “search expression” into “query expressions”. Similar, the reference prefix <tt>search:</tt> to specify a query link or a query transclusion is renamed to <tt>query:</tt> (breaking: zettelmarkup) * Rename query parameter for query expression from <tt>_s</tt> to <tt>q</tt>. (breaking: api, webui) * Cleanup names for HTTP query parameters in WebUI. Update your bookmarks if you used them. (For API: see below) (breaking: webui) * Allow search terms to be OR-ed. This allows to specify any search expression in disjunctive normal form. Therefore, the NEGATE term is not needed any more. (breaking: api, webui) * Replace runtime configuration <tt>default-lang</tt> with <tt>lang</tt>. Additionally, <tt>lang</tt> set at the zettel of the current user, will provide a default value for the current user, overwriting the global default value. (breaking) * Add new syntax <tt>pikchr</tt>, a markup language for diagrams in technical documentation. (major) * Add endpoint <tt>/q</tt> to query the zettelstore and aggregate resulting values. This is done by extending the query syntax. (major: api) * Add support for query actions. Actions may aggregate w.r.t. some metadata keys, or produce an RSS feed. (major: api, webui) * Query results can be ordered for more than one metadata key. Ordering by zettel identifier is an implicit last order expression to produce stable results. (minor: api, webui) * Add support for an asset directory, accessible via URL prefix <tt>/assests/</tt>. (minor: server) * Add support for metadata key <tt>created</tt>, a timestamp when the zettel was created. Since key <tt>published</tt> is now either <tt>created</tt> or <tt>modified</tt>, it will now always contains a valid time stamp. (minor) * Add support for metadata key <tt>author</tt>. It will be displayed on a zettel, if set. (minor: webui) * Remove CSS for lists. The browsers default value for <tt>padding-left</tt> will be used. (minor: webui) * Removed templates for rendering roles and tags lists. This is now done by query actions. (minor: webui) * Tags within zettel content are deprecated in version 0.8. This affects the computed metadata keys <tt>content-tags</tt> and <tt>all-tags</tt>. They will be removed. The number sign of a content tag introduces unintended tags, esp. in the english language; content tags may occur within links → links within links, when rendered as HTML; content tags may occur in the title of a zettel; naming of content tags, zettel tags, and their union is confusing for many. Migration: use zettel tags or replace content tag with a search. (deprecated: zettelmarkup) * Cleanup names for HTTP query parameter for API calls. Essentially, underscore characters in front are removed. Please use new names, old names will be deprecated in version 0.8. (deprecated: api) * Some smaller bug fixes and improvements, to the software and to the documentation. |
︙ | ︙ | |||
681 682 683 684 685 686 687 | (bug) <h2>Changes for Version 0.6.0 (2022-08-11)</h2> * Translating of "..." into horizontal ellipsis is no longer supported. Use &hellip; instead. (breaking: zettelmarkup) * Allow to specify search expressions, which allow to specify search | | | | 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 | (bug) <h2>Changes for Version 0.6.0 (2022-08-11)</h2> * Translating of "..." into horizontal ellipsis is no longer supported. Use &hellip; instead. (breaking: zettelmarkup) * Allow to specify search expressions, which allow to specify search criterias by using a simple syntax. Can be specified in WebUI's search box and via the API by using query parameter "_s". (major: api, webui) * A link reference is allowed to be a search expression. The WebUI will render this as a link to a list of zettel that satisfy the search expression. (major: zettelmarkup, webui) * A block transclusion is allowed to specify a search expression. When evaluated, the transclusion is replaced by a list of zettel that satisfy the search expression. (major: zettelmarkup) * When presenting a zettel list, allow to change the search expression. (minor: webui) * When evaluating a zettel, ignore transclusions if current user is not allowed to read transcluded zettel. (minor) * Added a small tutorial for Zettelmarkup. (minor: manual) * Using URL query parameter to search for metdata values, specify an ordering, an offset, and a limit for the resulting list, will be removed in version 0.7. Replace these with the more useable search expressions. Please be aware that the = search operator is also deprecated. It was only introduced to help the migration. (deprecated: api, webui) * Some smaller bug fixes and improvements, to the software and to the documentation. |
︙ | ︙ | |||
730 731 732 733 734 735 736 | * If authentication is enabled, a secret of at least 16 bytes must be set in the startup configuration. (breaking) * “Sexpr” encoding replaces “Native” encoding. Sexpr encoding is much easier to parse, compared with native and ZJSON encoding. In most cases it is smaller than ZJSON. (breaking: api) | | | | | | 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 | * If authentication is enabled, a secret of at least 16 bytes must be set in the startup configuration. (breaking) * “Sexpr” encoding replaces “Native” encoding. Sexpr encoding is much easier to parse, compared with native and ZJSON encoding. In most cases it is smaller than ZJSON. (breaking: api) * Endpoint <tt>/r</tt> is changed to <tt>/m?_key=role</tt> and returns now a map of role names to the list of zettel having this role. Endpoint <tt>/t</tt> is changed to <tt>/m?_key=tags</tt>. It already returned mapping described before. (breaking: api) * Remove support for a default value for metadata key title, role, and syntax. Title and role are now allowed to be empty, an empty syntax value defaults to “plain”. (breaking) * Add support for an “evaluation block” syntax in Zettelmarkup to allow interpretation of content by external software. |
︙ | ︙ | |||
761 762 763 764 765 766 767 | Zettelstore WebUI. (minor: webui) * A zettel can be saved while creating / editing it. There is no need to manually re-edit it by using the 'e' endpoint. (minor: webui) * Zettel role and zettel syntax are backed by a HTML5 data list element which lists supported and used values to help to enter a valid value. | | | | | | | 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 | Zettelstore WebUI. (minor: webui) * A zettel can be saved while creating / editing it. There is no need to manually re-edit it by using the 'e' endpoint. (minor: webui) * Zettel role and zettel syntax are backed by a HTML5 data list element which lists supported and used values to help to enter a valid value. (mirnor: webui) * Allow to use startup configuration, even if started in simple mode. (minor) * Log authentication issues in level "sense"; add caller IP address to some web server log messages. (minor: web server) * New startup configuration key <kbd>max-request-size</kbd> to limit a web request body to prevent client sending too large requests. (minor: web server) * Many smaller bug fixes and improvements, to the software and to the documentation. <a id="0_4"></a> <h2>Changes for Version 0.4 (2022-03-08)</h2> * Encoding “djson” renamed to “zjson” (<em>zettel json</em>). (breaking: api; minor: webui) * Remove inline quotation syntax <tt><<...<<</tt>. Now, <tt>""...""</tt> generates the equivalent code. Typographical quotes are generated by the browser, not by Zettelstore. (breaking: Zettelmarkup) * Remove inline formatting for monospace. Its syntax is now used by the similar syntax element of literal computer input. Monospace was just a visual element with no semantic association. Now, the syntax <kbd>++...++</kbd> is obsolete. (breaking: Zettelmarkup). * Remove API call to parse Zettelmarkup texts and encode it as text and HTML. Was call “POST /v”. It was needed to separately encode the titles of zettel. The same effect can be achieved by fetching the ZJSON representation and encode it using the function in the Zettelstore |
︙ | ︙ | |||
810 811 812 813 814 815 816 | interpreted as Zettelmarkup. Similar, the suffix <kbd>-set</kbd> denotes a set/list of words and the suffix <kbd>-zids</kbd> a set/list of zettel identifier. (minor: api, webui) * Change generated URLs for zettel-creation forms. If you have bookmarked them, e.g. to create a new zettel, you should update. (minor: webui) | | | | | | | | 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 | interpreted as Zettelmarkup. Similar, the suffix <kbd>-set</kbd> denotes a set/list of words and the suffix <kbd>-zids</kbd> a set/list of zettel identifier. (minor: api, webui) * Change generated URLs for zettel-creation forms. If you have bookmarked them, e.g. to create a new zettel, you should update. (minor: webui) * Remove support for metadata key <tt>no-index</tt> to suppress indexing selected zettel. It was introduced in <a href="#0_0_11">v0.0.11</a>, but disallows some future optimizations for searching zettel. (minor: api, webui) * Make some metadata-based searches a little bit faster by executing a (in-memory-based) full-text search first. Now only those zettel are loaded from file that contain the metdata value. (minor: api, webui) * Add an API call to retrieve the version of the Zettelstore. (minor: api) * Limit the amount of zettel and bytes to be stored in a memory box. Allows to use it with public access. (minor: box) * Disallow to cache the authentication cookie. Will remove most unexpected log-outs when using a mobile device. (minor: webui) * Many smaller bug fixes and improvements, to the software and to the documentation. <a id="0_3"></a> <h2>Changes for Version 0.3 (2022-02-09)</h2> * Zettel files with extension <tt>.meta</tt> are now treated as content files. Previoulsy, they were interpreted as metadata files. The interpretation as metadata files was deprecated in version 0.2. (breaking: directory and file/zip box) * Add syntax “draw” to produce some graphical representations. (major) * Add Zettelmarkup syntax to specify full transclusion of other zettel. (major: Zettelmarkup) * Add Zettelmarkup syntax to specify inline-zettel, both for |
︙ | ︙ | |||
854 855 856 857 858 859 860 | (minor: directory and file/zip box) * Many smaller bug fixes and improvements, to the software and to the documentation. <a id="0_2"></a> <h2>Changes for Version 0.2 (2022-01-19)</h2> * v0.2.1 (2021-02-01) updates the license year in some documents | | | | | | | | | | | | < | | | | | | | | | | | < | | | | | | | | | > | | | < | | | | 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 | (minor: directory and file/zip box) * Many smaller bug fixes and improvements, to the software and to the documentation. <a id="0_2"></a> <h2>Changes for Version 0.2 (2022-01-19)</h2> * v0.2.1 (2021-02-01) updates the license year in some documents * Remove support for <tt>;;small text;;</tt> Zettelmarkup. (breaking: Zettelmarkup) * On macOS, the downloadable executable program is now called “zettelstore”, as on all other Unix-like platforms. (possibly breaking: macOS) * External metadata (e.g. for zettel with file extension other than <tt>.zettel</tt>) are stored in files without an extension. Metadata files with extension <tt>.meta</tt> are still recognized, but result in a warning message. In a future version (probably v0.3), <tt>.meta</tt> files will be treated as ordinary content files, possibly resulting in duplicate content. In other words: usage of <tt>.meta</tt> files for storing metadata is deprecated. (possibly breaking: directory and file box) * Show unlinked references in info page of each zettel. Unlinked references are phrases within zettel content that might reference another zettel with the same title as the phase. (major: webui) * Add endpoint <tt>/u/{ID}</tt> to retrieve unlinked references. (major: api) * Provide a logging facility. Log messages are written to standard output. Messages with level “information” are also written to a circular buffer (of length 8192) which can be retrieved via a computed zettel. There is a command line flag <tt>-l LEVEL</tt> to specify an application global logging level on startup (default: “information”). Logging level can also be changed via the administrator console, even for specific (sub-) services. (major) * The internal handling of zettel files is rewritten. This allows less reloads ands detects when the directory containing the zettel files is removed. The API, WebUI, and the admin console allow to manually refresh the internal state on demand. (major: box, webui) * <tt>.zettel</tt> files with YAML header are now correctly written. (bug) * Selecting zettel based on their metadata allows the same syntax as searching for zettel content. For example, you can list all zettel that have an identifier not ending with <tt>00</tt> by using the query <tt>id=!<00</tt>. (minor: api, webui) * Remove support for <tt>//deprecated emphasized//</tt> Zettelmarkup. (minor: Zettelmarkup) * Add options to profile the software. Profiling can be enabled at the command line or via the administrator console. (minor) * Add computed zettel that lists all supported parser / recognized zettel syntaxes. (minor) * Add API call to check for enabled authentication. (minor: api) * Renewing an API access token works even if authentication is not enabled. This corresponds to the behaviour of optaining an access token. (minor: api) * If there is nothing to return, use HTTP status code 204, instead of 200 + <tt>Content-Length: 0</tt>. (minor: api) * Metadata key <tt>duplicates</tt> stores the duplicate file names, instead of just a boolean value that there were duplicate file names. (minor) * Document autostarting Zettelstore on Windows, macOS, and Linux. (minor) * Many smaller bug fixes and improvements, to the software and to the documentation. <a id="0_1"></a><a id="0_1_0"></a> <h2>Changes for Version 0.1 (2021-11-11)</h2> * v0.1.3 (2021-12-15) fixes a bug where the modification date could be set when a new zettel is created. * v0.1.2 (2021-11-18) fixes a bug when selecting zettel from a list when more than one comparison is negated. * v0.1.1 (2021-11-12) updates the documentation, mostly related to the deprecation of the <tt>//</tt> markup. * Remove visual Zettelmarkup (italic, underline). Semantic Zettelmarkup (emphasize, insert) is still allowed, but got a different syntax. The new syntax for <ins>inserted text</ins> is <tt>>>inserted>></tt>, while its previous syntax now denotes <em>emphasized text</em>: <tt>__emphasized__</tt>. The previous syntax for emphasized text is now deprecated: <tt>//deprecated emphasized//</tt>. Starting with Version 0.2.0, the deprecated syntax will not be supported. The reason is the collision with URLs that also contain the characters <tt>//</tt>. The ZMK encoding of a zettel may help with the transition (<tt>/v/{ZettelID}?_part=zettel&_enc=zmk</tt>, on the Info page of each zettel in the WebUI). Additionally, all deprecated uses of <tt>//</tt> will be rendered with a dashed box within the WebUI. (breaking: Zettelmarkup). * API client software is now a [https://zettelstore.de/client/|separate] project. (breaking) * Initial support for HTTP security headers (Content-Security-Policy, Permissions-Policy, Referrer-Policy, X-Content-Type-Options, X-Frame-Options). Header values are currently some constant values. (possibly breaking: api, webui) * Remove visual Zettelmarkup (bold, striketrough). Semantic Zettelmarkup (strong, delete) is still allowed and replaces the visual elements syntactically. The visual appearance should not change (depends on your changes / additions to CSS zettel). (possibly breaking: Zettelmarkup). * Add API endpoint <tt>POST /v</tt> to retrieve HTMl and text encoded strings from given ZettelMarkup encoded values. This will be used to render a HTML page from a given zettel: in many cases the title of a zettel must be treated separately. (minor: api) * Add API endpoint <tt>/m</tt> to retrieve only the metadata of a zettel. (minor: api) * New metadata value <tt>content-tags</tt> contains the tags that were given in the zettel content. To put it simply, <tt>all-tags</tt> = <tt>tags</tt> + <tt>content-tags</tt>. (minor) * Calculating the context of a zettel stops at the home zettel. (minor: api, webui) * When renaming or deleting a zettel, a warning will be given, if other zettel references the given zettel, or when “deleting” will uncover zettel in overlay box. (minor: webui) |
︙ | ︙ | |||
987 988 989 990 991 992 993 | (info) * Many smaller bug fixes and improvements, to the software and to the documentation. <a id="0_0_15"></a> <h2>Changes for Version 0.0.15 (2021-09-17)</h2> * Move again endpoint characters for authentication to make room for future | | < | | < > | > | | | | | | | | | | | | | | | | | | | | | | | | | | < | | | | | | | | | | | | | | < | | | | | | | < | < | | | < | | | < | | | | | | | | | | < | | < | | | < | < | < | | < | < | < | | | | | | | | | | | | | | | | | < | | | | | | | | | | | | | | | | | | | | | | < | | | | | < | | | | | | 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 593 594 595 596 597 598 599 600 601 602 603 604 605 606 607 608 609 610 611 612 613 614 615 616 617 618 619 620 621 622 623 624 625 626 627 628 629 630 631 632 633 634 635 636 637 638 639 640 641 642 643 644 645 646 647 648 649 650 651 652 653 654 655 656 657 658 659 660 661 662 663 664 665 666 667 668 669 670 671 672 673 674 675 676 677 678 679 680 681 682 683 684 685 686 687 688 689 690 691 692 693 694 695 696 697 698 699 700 701 702 703 704 705 706 707 708 709 710 711 712 713 714 715 716 717 718 719 720 721 722 723 724 725 726 727 728 729 730 731 732 733 734 735 736 737 738 739 740 741 742 743 744 745 746 747 748 749 750 751 752 753 754 755 756 757 758 759 760 761 762 763 764 765 766 767 768 769 770 771 772 773 774 775 776 777 778 779 780 781 782 783 784 785 786 787 788 789 790 791 792 793 794 795 796 797 798 799 800 801 802 803 804 805 806 807 808 809 810 811 812 813 814 815 816 817 818 819 820 821 822 823 824 825 826 827 828 829 830 831 832 833 834 835 836 837 838 839 840 841 842 843 844 845 846 847 848 849 850 851 852 853 854 855 856 857 858 859 860 861 862 863 864 865 866 867 868 869 870 871 872 873 874 875 876 877 878 879 880 881 882 883 884 885 886 887 888 889 890 891 892 893 894 895 896 897 898 899 900 901 902 903 904 905 906 907 908 909 910 911 912 913 914 915 916 917 918 919 920 921 922 923 924 925 926 927 928 929 930 931 932 933 934 935 936 937 938 939 940 941 942 943 944 945 946 947 948 949 950 951 952 953 954 955 956 957 958 959 960 961 962 963 964 965 966 967 968 969 970 971 972 973 974 975 976 977 978 979 | (info) * Many smaller bug fixes and improvements, to the software and to the documentation. <a id="0_0_15"></a> <h2>Changes for Version 0.0.15 (2021-09-17)</h2> * Move again endpoint characters for authentication to make room for future features. WebUI authentication moves from <tt>/a</tt> to <tt>/i</tt> (login) and <tt>/i?logout</tt> (logout). API authentication moves from <tt>/v</tt> to </tt>/a</tt>. JSON-based basic zettel handling moves from <tt>/z</tt> to <tt>/j</tt> and <tt>/z/{ID}</tt> to <tt>/j/{ID}</tt>. Since the API client is updated too, this should not be a breaking change for most users. (minor: api, webui; possibly breaking) * Add API endpoint <tt>/v/{ID}</tt> to retrieve an evaluated zettel in various encodings. Mostly replaces endpoint <tt>/z/{ID}</tt> for other encodings except “json” and “raw”. Endpoint <tt>/j/{ID}</tt> now only returns JSON data, endpoint <tt>/z/{ID}</tt> is used to retrieve plain zettel data (previously called “raw”). See documentation for details. (major: api; breaking) * Metadata values of type <em>tag set</em> (the metadata with key <tt>tags</tt> is its most prominent example), are now compared in a case-insensitive manner. Tags that only differ in upper / lower case character are now treated identical. This might break your workflow, if you depend on case-sensitive comparison of tag values. Tag values are translated to their lower case equivalent before comparing them and when you edit a zettel through Zettelstore. If you just modify the zettel files, your tag values remain unchanged. (major; breaking) * Endpoint <tt>/z/{ID}</tt> allows the same methods as endpoint <tt>/j/{ID}</tt>: <tt>GET</tt> retrieves zettel (see above), <tt>PUT</tt> updates a zettel, <tt>DELETE</tt> deletes a zettel, <tt>MOVE</tt> renames a zettel. In addtion, <tt>POST /z</tt> will create a new zettel. When zettel data must be given, the format is plain text, with metadata separated from content by an empty line. See documentation for more details. (major: api (plus WebUI for some details)) * Allows to transclude / expand the content of another zettel into a target zettel when the zettel is rendered. By using the syntax of embedding an image (which is some kind of expansion too), the first top-level paragraph of a zettel may be transcluded into the target zettel. Endless recursion is checked, as well as a possible “transclusion bomb ” (similar to a XML bomb). See manual for details. (major: zettelmarkup) * The endpoint <tt>/z</tt> allows to list zettel in a simpler format than endpoint <tt>/j</tt>: one line per zettel, and only zettel identifier plus zettel title. (minor: api) * Folgezettel are now displayed with full title at the bottom of a page. (minor: webui) * Add API endpoint <tt>/p/{ID}</tt> to retrieve a parsed, but not evaluated zettel in various encodings. (minor: api) * Fix: do not list a shadowed zettel that matches the select criteria. (minor) * Many smaller bug fixes and improvements, to the software and to the documentation. <a id="0_0_14"></a> <h2>Changes for Version 0.0.14 (2021-07-23)</h2> * Rename “place” into “box”. This also affects the configuration keys to specify boxes <tt>box-uri<em>X</em></tt> (previously <tt>place-uri-<em>X</em></tt>. Older changes documented here are renamed too. (breaking) * Add API for creating, updating, renaming, and deleting zettel. (major: api) * Initial API client for Go. (major: api) * Remove support for paging of WebUI list. Runtime configuration key <tt>list-page-size</tt> is removed. If you still specify it, it will be ignored. (major: webui) * Use endpoint <tt>/v</tt> for user authentication via API. Endpoint <tt>/a</tt> is now used for the web user interface only. Similar, endpoint <tt>/y</tt> (“zettel context”) is renamed to <tt>/x</tt>. (minor, possibly breaking) * Type of used-defined metadata is determined by suffix of key: <tt>-number</tt>, <tt>-url</tt>, <tt>-zid</tt> will result the values to be interpreted as a number, an URL, or a zettel identifier. (minor, but possibly breaking if you already used a metadata key with above suffixes, but as a string type) * New <tt>user-role</tt> “creator”, which is only allowed to create new zettel (except user zettel). This role may only read and update public zettel or its own user zettel. Added to support future client software (e.g. on a mobile device) that automatically creates new zettel but, in case of a password loss, should not allow to read existing zettel. (minor, possibly breaking, because new zettel template zettel must always prepend the string <tt>new-</tt> before metdata keys that should be transferred to the new zettel) * New suported metadata key <tt>box-number</tt>, which gives an indication from which box the zettel was loaded. (minor) * New supported syntax <tt>html</tt>. (minor) * New predefined zettel “User CSS” that can be used to redefine some predefined CSS (without modifying the base CSS zettel). (minor: webui) * When a user moves a zettel file with additional characters into the box directory, these characters are preserved when zettel is updated. (bug) * The phase “filtering a zettel list” is more precise “selecting zettel” (documentation) * Many smaller bug fixes and improvements, to the software and to the documentation. <a id="0_0_13"></a> <h2>Changes for Version 0.0.13 (2021-06-01)</h2> * Startup configuration <tt>box-<em>X</em>-uri</tt> (where <em>X</em> is a number greater than zero) has been renamed to <tt>box-uri-<em>X</em></tt>. (breaking) * Web server processes startup configuration <tt>url-prefix</tt>. There is no need for stripping the prefix by a front-end web server any more. (breaking: webui, api) * Administrator console (only optional accessible locally). Enable it only on systems with a single user or with trusted users. It is disabled by default. (major: core) * Remove visibility value “simple-expert” introduced in [#0_0_8|version 0.0.8]. It was too complicated, esp. authorization. There was a name collision with the “simple” directory box sub-type. (major) * For security reasons, HTML blocks are not encoded as HTML if they contain certain snippets, such as <tt><script</tt> or <tt><iframe</tt>. These may be caused by using CommonMark as a zettel syntax. (major) * Full-text search can be a prefix search or a search for equal words, in addition to the search whether a word just contains word of the search term. (minor: api, webui) * Full-text search for URLs, with above additional operators. (minor: api, webui) * Add system zettel about license, contributors, and dependencies (and their license). For a nicer layout of zettel identifier, the zettel about environment values and about runtime metrics got new zettel identifier. This affects only user that referenced those zettel. (minor) * Local images that cannot be read (not found or no access rights) are substituted with the new default image, a spinning emoji. See [/file?name=box/constbox/emoji_spin.gif]. (minor: webui) * Add zettelmarkup syntax for a table row that should be ignored: <tt>|%</tt>. This allows to paste output of the administrator console into a zettel. (minor: zmk) * Many smaller bug fixes and improvements, to the software and to the documentation. <a id="0_0_12"></a> <h2>Changes for Version 0.0.12 (2021-04-16)</h2> * Raise the per-process limit of open files on macOS to 1.048.576. This allows most macOS users to use at least 500.000 zettel. That should be enough for the near future. (major) * Mitigate the shortcomings of the macOS version by introducing types of directory boxes. The original directory box type is now called "notify" (the default value). There is a new type called "simple". This new type does not notify Zettelstore when some of the underlying Zettel files change. (major) * Add new startup configuration <tt>default-dir-box-type</tt>, which gives the default value for specifying a directory box type. The default value is “notify”. On macOS, the default value may be changed “simple” if some errors occur while raising the per-process limit of open files. (minor) <a id="0_0_11"></a> <h2>Changes for Version 0.0.11 (2021-04-05)</h2> * New box schema "file" allows to read zettel from a ZIP file. A zettel collection can now be packaged and distributed easier. (major: server) * Non-restricted search is a full-text search. The search string will be normalized according to Unicode NFKD. Every character that is not a letter or a number will be ignored for the search. It is sufficient if the words to be searched are part of words inside a zettel, both content and metadata. (major: api, webui) * A zettel can be excluded from being indexed (and excluded from being found in a search) if it contains the metadata <tt>no-index: true</tt>. (minor: api, webui) * Menu bar is shown when displaying error messages. (minor: webui) * When selecting zettel, it can be specified that a given value should <em>not</em> match. Previously, only the whole select criteria could be negated (which is still possible). (minor: api, webui) * You can select a zettel by specifying that specific metadata keys must (or must not) be present. (minor: api, webui) * Context of a zettel (introduced in version 0.0.10) does not take tags into account any more. Using some tags for determining the context resulted into erratic, non-deterministic context lists. (minor: api, webui) * Selecting zettel depending on tag values can be both by comparing only the prefix or the whole string. If a search value begins with '#', only zettel with the exact tag will be returned. Otherwise a zettel will be returned if the search string just matches the prefix of only one of its tags. (minor: api, webui) * Many smaller bug fixes and improvements, to the software and to the documentation. A note for users of macOS: in the current release and with macOS's default values, a zettel directory must not contain more than approx. 250 files. There are three options to mitigate this limitation temporarily: # You update the per-process limit of open files on macOS. # You setup a virtualization environment to run Zettelstore on Linux or Windows. # You wait for version 0.0.12 which addresses this issue. <a id="0_0_10"></a> <h2>Changes for Version 0.0.10 (2021-02-26)</h2> * Menu item “Home” now redirects to a home zettel. Its default identifier is <tt>000100000000</tt>. The identifier can be changed with configuration key <tt>home-zettel</tt>, which supersedes key <tt>start</tt>. The default home zettel contains some welcoming information for the new user. (major: webui) * Show context of a zettel by following all backward and/or forward reference up to a defined depth and list the resulting zettel. Additionally, some zettel with similar tags as the initial zettel are also taken into account. (major: api, webui) * A zettel that references other zettel within first-level list items, can act as a “table of contents” zettel. The API endpoint <tt>/o/{ID}</tt> allows to retrieve the referenced zettel in the same order as they occur in the zettel. (major: api) * The zettel “New Menu” with identifier <tt>00000000090000</tt> contains a list of all zettel that should act as a template for new zettel. They are listed in the WebUIs ”New“ menu. This is an application of the previous item. It supersedes the usage of a role <tt>new-template</tt> introduced in [#0_0_6|version 0.0.6]. <b>Please update your zettel if you make use of the now deprecated feature.</b> (major: webui) * A reference that starts with two slash characters (“<code>//</code>”) it will be interpreted relative to the value of <code>url-prefix</code>. For example, if <code>url-prefix</code> has the value <code>/manual/</code>, the reference <code>[[Zettel list|//h]]</code> will render as <code><a href="/manual/h">Zettel list</a></code>. (minor: syntax) * Searching/selecting ignores the leading '#' character of tags. (minor: api, webui) * When result of selecting or searching is presented, the query is written as the page heading. (minor: webui) * A reference to a zettel that contains a URL fragment, will now be processed by the indexer. (bug: server) * Runtime configuration key <tt>marker-external</tt> now defaults to “&#10138;” (“➚”). It is more beautiful than the previous “&#8599;&#xfe0e;” (“↗︎”), which also needed the additional “&#xfe0e;” to disable the conversion to an emoji on iPadOS. (minor: webui) * A pre-build binary for macOS ARM64 (also known as Apple silicon) is available. (minor: infrastructure) * Many smaller bug fixes and improvements, to the software and to the documentation. <a id="0_0_9"></a> <h2>Changes for Version 0.0.9 (2021-01-29)</h2> This is the first version that is managed by [https://fossil-scm.org|Fossil] instead of GitHub. To access older versions, use the Git repository under [https://github.com/zettelstore/zettelstore-github|zettelstore-github]. <h3>Server / API</h3> * (major) Support for property metadata. Metadata key <tt>published</tt> is the first example of such a property. * (major) A background activity (called <i>indexer</i>) continuously monitors zettel changes to establish the reverse direction of found internal links. This affects the new metadata keys <tt>precursor</tt> and <tt>folge</tt>. A user specifies the precursor of a zettel and the indexer computes the property metadata for [https://forum.zettelkasten.de/discussion/996/definition-folgezettel|Folgezettel]. Metadata keys with type “Identifier” or “IdentifierSet” that have no inverse key (like <tt>precursor</tt> and <tt>folge</tt> with add to the key <tt>forward</tt> that also collects all internal links within the content. The computed inverse is <tt>backward</tt>, which provides all backlinks. The key <tt>back</tt> is computed as the value of <tt>backward</tt>, but without forward links. Therefore, <tt>back</tt> is something like the list of “smart backlinks”. * (minor) If Zettelstore is being stopped, an appropriate message is written in the console log. * (minor) New computed zettel with environmental data, the list of supported meta data keys, and statistics about all configured zettel boxes. Some other computed zettel got a new identifier (to make room for other variant). * (minor) Remove zettel <tt>00000000000004</tt>, which contained the Go version that produced the Zettelstore executable. It was too specific to the current implementation. This information is now included in zettel <tt>00000000000006</tt> (<i>Zettelstore Environment Values</i>). * (minor) Predefined templates for new zettel do not contain any value for attribute <tt>visibility</tt> any more. * (minor) Add a new metadata key type called “Zettelmarkup”. It is a non-empty string, that will be formatted with Zettelmarkup. <tt>title</tt> and <tt>default-title</tt> have this type. * (major) Rename zettel syntax “meta” to “none”. Please update the <i>Zettelstore Runtime Configuration</i> and all other zettel that previously used the value “meta”. Other zettel are typically user zettel, used for authentication. However, there is no real harm, if you do not update these zettel. In this case, the metadata is just not presented when rendered. Zettelstore will still work. * (minor) Login will take at least 500 milliseconds to mitigate login attacks. This affects both the API and the WebUI. * (minor) Add a sort option “_random” to produce a zettel list in random order. <tt>_order</tt> / <tt>order</tt> are now an aliases for the query parameters <tt>_sort</tt> / <tt>sort</tt>. <h3>WebUI</h3> * (major) HTML template zettel for WebUI now use [https://mustache.github.io/|Mustache] syntax instead of previously used [https://golang.org/pkg/html/template/|Go template] syntax. This allows these zettel to be used, even when there is another Zettelstore implementation, in another programming language. Mustache is available for approx. 48 programming languages, instead of only one for Go templates. <b>If you modified your templates, you <i>must</i> adapt them to the new syntax. Otherwise the WebUI will not work.</b> * (major) Show zettel identifier of folgezettel and precursor zettel in the header of a rendered zettel. If a zettel has real backlinks, they are shown at the botton of the page (“Additional links to this zettel”). * (minor) All property metadata, even computed metadata is shown in the info page of a zettel. * (minor) Rendering of metadata keys <tt>title</tt> and <tt>default-title</tt> in info page changed to a full HTML output for these Zettelmarkup encoded values. * (minor) Always show the zettel identifier on the zettel detail view. Previously, the identifier was not shown if the zettel was not editable. * (minor) Do not show computed metadata in edit forms anymore. <a id="0_0_8"></a> <h2>Changes for Version 0.0.8 (2020-12-23)</h2> <h3>Server / API</h3> * (bug) Zettel files with extension <tt>.jpg</tt> and without metadata will get a <tt>syntax</tt> value “jpg”. The internal data structure got the same value internally, instead of “jpeg”. This has been fixed for all possible alternative syntax values. * (bug) If a file, e.g. an image file like <tt>20201130190200.jpg</tt>, is added to the directory box, its metadata are just calculated from the information available. Updated metadata did not find its way into the zettel box, because the <tt>.meta</tt> file was not written. * (bug) If just the <tt>.meta</tt> file was deleted manually, the zettel was assumed to be missing. A workaround is to restart the software. If the <tt>.meta</tt> file is deleted, metadata is now calculated in the same way when the <tt>.meta</tt> file is non-existing at the start of the software. * (bug) A link to the current zettel, only using a fragment (e.g. <code>[[Title|#title]]</code>) is now handled correctly as a zettel link (and not as a link to external material). * (minor) Allow zettel to be marked as “read only”. This is done through the metadata key <tt>read-only</tt>. * (bug) When renaming a zettel, check all boxes for the new zettel identifier, not just the first one. Otherwise it will be possible to shadow a read-only zettel from a next box, effectively modifying it. * (minor) Add support for a configurable default value for metadata key <tt>visibility</tt>. * (bug) If <tt>list-page-size</tt> is set to a relatively small value and the authenticated user is <i>not</i> the owner, some zettel were not shown in the list of zettel or were not returned by the API. * (minor) Add support for new visibility “expert”. An owner becomes an expert, if the runtime configuration key <tt>expert-mode</tt> is set to true. * (major) Add support for computed zettel. These zettel have an identifier less than <tt>0000000000100</tt>. Most of them are only visible, if <tt>expert-mode</tt> is enabled. * (bug) Fixes a memory leak that results in too many open files after approx. 125 reload operations. * (major) Predefined templates for new zettel got an explicit value for visibility: “login”. Please update these zettel if you modified them. * (major) Rename key <tt>readonly</tt> of <i>Zettelstore Startup Configuration</i> to <tt>read-only-mode</tt>. This was done to avoid some confusion with the the zettel metadata key <tt>read-only</tt>. <b>Please adapt your startup configuration. Otherwise your Zettelstore will be accidentally writable.</b> * (minor) References starting with “./” and “../” are treated as a local reference. Previously, only the prefix “/” was treated as a local reference. * (major) Metadata key <tt>modified</tt> will be set automatically to the current local time if a zettel is updated through Zettelstore. <b>If you used that key previously for your own, you should rename it before you upgrade.</b> * (minor) The new visibility value “simple-expert” ensures that many computed zettel are shown for new users. This is to enable them to send useful bug reports. * (minor) When a zettel is stored as a file, its identifier is additionally stored within the metadata. This helps for better robustness in case the file names were corrupted. In addition, there could be a tool that compares the identifier with the file name. <h3>WebUI</h3> * (minor) Remove list of tags in “List Zettel” and search results. There was some feedback that the additional tags were not helpful. * (minor) Move zettel field "role" above "tags" and move "syntax" more to "content". * (minor) Rename zettel operation “clone” to “copy”. * (major) All predefined HTML templates have now a visibility value “expert”. If you want to see them as an non-expert owner, you must temporary enable <tt>expert-mode</tt> and change the <tt>visibility</tt> metadata value. * (minor) Initial support for [https://zettelkasten.de/posts/tags/folgezettel/|Folgezettel]. If you click on “Folge” (detail view or info view), a new zettel is created with a reference (<tt>precursor</tt>) to the original zettel. Title, role, tags, and syntax are copied from the original zettel. * (major) Most predefined zettel have a title prefix of “Zettelstore”. * (minor) If started in simple mode, e.g. via double click or without any command, some information for the new user is presented. In the terminal, there is a hint about opening the web browser and use |
︙ | ︙ | |||
1433 1434 1435 1436 1437 1438 1439 | to stay with the old licenses (AGPLv3+, CC BY-SA 4.0), you are free to fork from the previous version. <a id="0_0_6"></a> <h2>Changes for Version 0.0.6 (2020-11-23)</h2> <h3>Server</h3> * (major) Rename identifier of <i>Zettelstore Runtime Configuration</i> to | | < | | | | | | | | | | 989 990 991 992 993 994 995 996 997 998 999 1000 1001 1002 1003 1004 1005 1006 1007 1008 1009 1010 1011 1012 1013 1014 1015 1016 1017 1018 1019 1020 1021 1022 1023 1024 1025 1026 1027 1028 1029 | to stay with the old licenses (AGPLv3+, CC BY-SA 4.0), you are free to fork from the previous version. <a id="0_0_6"></a> <h2>Changes for Version 0.0.6 (2020-11-23)</h2> <h3>Server</h3> * (major) Rename identifier of <i>Zettelstore Runtime Configuration</i> to <tt>00000000000100</tt> (previously <tt>00000000000001</tt>). This is done to gain some free identifier with smaller number to be used internally. <b>If you customized this zettel, please make sure to rename it to the new identifier.</b> * (major) Rename the two essential metadata keys of a user zettel to <tt>credential</tt> and <tt>user-id</tt>. The previous values were <tt>cred</tt> and <tt>ident</tt>. <b>If you enabled user authentication and added some user zettel, make sure to change them accordingly. Otherwise these users will not authenticated any more.</b> * (minor) Rename the scheme of the box URL where predefined zettel are stored to “const”. The previous value was “globals”. <h3>Zettelmarkup</h3> * (bug) Allow to specify a <i>fragment</i> in a reference to a zettel. Used to link to an internal position within a zettel. This applies to CommonMark too. <h3>API</h3> * (bug) Encoding binary content in format “json” now results in valid JSON content. * (bug) All query parameters of selecting zettel must be true, regardless if a specific key occurs more than one or not. * (minor) Encode all inherited meta values in all formats except “raw”. A meta value is called <i>inherited</i> if there is a key starting with <tt>default-</tt> in the <i>Zettelstore Runtime Configuration</i>. Applies to WebUI also. * (minor) Automatic calculated identifier for headings (only for “html”, “djson”, “native” format and for the Web user interface). You can use this to provide a zettel reference that links to the heading, without specifying an explicit mark (<code>[!mark]</code>). * (major) Allow to retrieve all references of a given zettel. |
︙ | ︙ | |||
1485 1486 1487 1488 1489 1490 1491 | contrast to “zettel references” and “external references”). When a local reference is displayed as an URL on the WebUI, it will not opened in a new window/tab. They will receive a <i>local</i> marker, when encoded as “djson” or “native”. Local references are listed on the <i>Info page</i> of each zettel. * (minor) Change the default value for some visual sugar put after an | | | | < | | | | | | | | | < | | | | | < | 1040 1041 1042 1043 1044 1045 1046 1047 1048 1049 1050 1051 1052 1053 1054 1055 1056 1057 1058 1059 1060 1061 1062 1063 1064 1065 1066 1067 1068 1069 1070 1071 1072 1073 1074 1075 1076 1077 1078 1079 1080 1081 1082 1083 1084 1085 1086 1087 1088 1089 1090 1091 1092 1093 1094 1095 1096 1097 1098 1099 1100 1101 1102 1103 1104 1105 1106 1107 1108 1109 1110 1111 1112 | contrast to “zettel references” and “external references”). When a local reference is displayed as an URL on the WebUI, it will not opened in a new window/tab. They will receive a <i>local</i> marker, when encoded as “djson” or “native”. Local references are listed on the <i>Info page</i> of each zettel. * (minor) Change the default value for some visual sugar put after an external URL to <tt>&\#8599;&\#xfe0e;</tt> (“↗︎”). This affects the former key <tt>icon-material</tt> of the <i>Zettelstore Runtime Configuration</i>, which is renamed to <tt>marker-external</tt>. * (major) Allow multiple zettel to act as templates for creating new zettel. All zettel with a role value “new-template” act as a template to create a new zettel. The WebUI menu item “New” changed to a drop-down list with all those zettel, ordered by their identifier. All metadata keys with the prefix <tt>new-</tt> will be translated to a new or updated keys/value without that prefix. You can use this mechanism to specify a role for the new zettel, or a different title. The title of the template zettel is used in the drop-down list. The initial template zettel “New Zettel” has now a different zettel identifier (now: <tt>00000000091001</tt>, was: <tt>00000000040001</tt>). <b>Please update it, if you changed that zettel.</b> <br>Note: this feature was superseded in [#0_0_10|version 0.0.10] by the “New Menu” zettel. * (minor) When a page should be opened in a new windows (e.g. for external references), the web browser is instructed to decouple the new page from the previous one for privacy and security reasons. In detail, the web browser is instructed to omit referrer information and to omit a JS object linking to the page that contained the external link. * (minor) If the value of the <i>Zettelstore Runtime Configuration</i> key <tt>list-page-size</tt> is greater than zero, the number of WebUI list elements will be restricted and it is possible to change to the next/previous page to list more elements. * (minor) Change CSS to enhance reading: make <code>line-height</code> a little smaller (previous: 1.6, now 1.4) and move list items to the left. <a id="0_0_5"></a> <h2>Changes for Version 0.0.5 (2020-10-22)</h2> * Application Programming Interface (API) to allow external software to retrieve zettel data from the Zettelstore. * Specify boxes, where zettel are stored, via an URL. * Add support for a custom footer. <a id="0_0_4"></a> <h2>Changes for Version 0.0.4 (2020-09-11)</h2> * Optional user authentication/authorization. * New sub-commands <tt>file</tt> (use Zettelstore as a command line filter), <tt>password</tt> (for authentication), and <tt>config</tt>. <a id="0_0_3"></a> <h2>Changes for Version 0.0.3 (2020-08-31)</h2> * Starting Zettelstore has been changed by introducing sub-commands. This change is also reflected on the server installation procedures. * Limitations on renaming zettel has been relaxed. <a id="0_0_2"></a> <h2>Changes for Version 0.0.2 (2020-08-28)</h2> * Configuration zettel now has ID <tt>00000000000001</tt> (previously: <tt>00000000000000</tt>). * The zettel with ID <tt>00000000000000</tt> is no longer shown in any zettel list. If you changed the configuration zettel, you should rename it manually in its file directory. * Creating a new zettel is now done by cloning an existing zettel. To mimic the previous behaviour, a zettel with ID <tt>00000000040001</tt> is introduced. You can change it if you need a different template zettel. <a id="0_0_1"></a> <h2>Changes for Version 0.0.1 (2020-08-21)</h2> * Initial public release. |
Changes to www/download.wiki.
1 2 3 4 5 6 7 8 9 10 11 | <title>Download</title> <h1>Download of Zettelstore Software</h1> <h2>Foreword</h2> * Zettelstore is free/libre open source software, licensed under EUPL-1.2-or-later. * The software is provided as-is. * There is no guarantee that it will not damage your system. * However, it is in use by the main developer since March 2020 without any damage. * It may be useful for you. It is useful for me. * Take a look at the [https://zettelstore.de/manual/|manual] to know how to start and use it. <h2>ZIP-ped Executables</h2> | | < | | | | | | | 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 | <title>Download</title> <h1>Download of Zettelstore Software</h1> <h2>Foreword</h2> * Zettelstore is free/libre open source software, licensed under EUPL-1.2-or-later. * The software is provided as-is. * There is no guarantee that it will not damage your system. * However, it is in use by the main developer since March 2020 without any damage. * It may be useful for you. It is useful for me. * Take a look at the [https://zettelstore.de/manual/|manual] to know how to start and use it. <h2>ZIP-ped Executables</h2> Build: <code>v0.11.0</code> (2023-03-27). * [/uv/zettelstore-0.11.0-linux-amd64.zip|Linux] (amd64) * [/uv/zettelstore-0.11.0-linux-arm.zip|Linux] (arm6, e.g. Raspberry Pi) * [/uv/zettelstore-0.11.0-darwin-amd64.zip|macOS] (amd64) * [/uv/zettelstore-0.11.0-darwin-arm64.zip|macOS] (arm64, aka Apple silicon) * [/uv/zettelstore-0.11.0-windows-amd64.zip|Windows] (amd64) Unzip the appropriate file, install and execute Zettelstore according to the manual. <h2>Zettel for the manual</h2> As a starter, you can download the zettel for the manual [/uv/manual-0.11.0.zip|here]. Just unzip the contained files and put them into your zettel folder or configure a file box to read the zettel directly from the ZIP file. |
Changes to www/impri.wiki.
︙ | ︙ | |||
10 11 12 13 14 15 16 | If you do not log into this site, or login as the user "anonymous", the only personal data this web service will process is your IP adress. It will be used to send the data of the website you requested to you and to mitigate possible attacks against this website. This website is hosted by [https://ionos.de|1&1 IONOS SE]. According to | | | 10 11 12 13 14 15 16 17 18 | If you do not log into this site, or login as the user "anonymous", the only personal data this web service will process is your IP adress. It will be used to send the data of the website you requested to you and to mitigate possible attacks against this website. This website is hosted by [https://ionos.de|1&1 IONOS SE]. According to [https://www.ionos.de/hilfe/datenschutz/datenverarbeitung-von-webseitenbesuchern-ihres-11-ionos-produktes/andere-11-ionos-produkte/|their information], no processing of personal data is done by them. |
Changes to www/index.wiki.
︙ | ︙ | |||
12 13 14 15 16 17 18 | To get an initial impression, take a look at the [https://zettelstore.de/manual/|manual]. It is a live example of the zettelstore software, running in read-only mode. The software, including the manual, is licensed under the [/file?name=LICENSE.txt&ci=trunk|European Union Public License 1.2 (or later)]. | | | | | | < < | > | | | | | | < | | 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 | To get an initial impression, take a look at the [https://zettelstore.de/manual/|manual]. It is a live example of the zettelstore software, running in read-only mode. The software, including the manual, is licensed under the [/file?name=LICENSE.txt&ci=trunk|European Union Public License 1.2 (or later)]. [https://zettelstore.de/client|Zettelstore Client] provides client software to access Zettelstore via its API more easily, [https://zettelstore.de/contrib|Zettelstore Contrib] contains contributed software, which often connects to Zettelstore via its API. Some of the software packages may be experimental. [https://twitter.com/zettelstore|Stay] <a rel="me" href="https://nerdculture.de/@zettelstore">tuned</a> … <hr> <h3>Latest Release: 0.11.0 (2023-03-27)</h3> * [./download.wiki|Download] * [./changes.wiki#0_11|Change summary] * [/timeline?p=v0.11.0&bt=v0.10.0&y=ci|Check-ins for version 0.11.0], [/vdiff?to=v0.11.0&from=v0.10.0|content diff] * [/timeline?df=v0.11.0&y=ci|Check-ins derived from the 0.11.0 release], [/vdiff?from=v0.11.0&to=trunk|content diff] * [./plan.wiki|Limitations and planned improvements] * [/timeline?t=release|Timeline of all past releases] <hr> <h2>Build instructions</h2> Just install [https://golang.org/dl/|Go] and some Go-based tools. Please read the [./build.md|instructions] for details. * [/dir?ci=trunk|Source code] * [/download|Download the source code] as a tarball or a ZIP file (you must [/login|login] as user "anonymous"). |
Changes to www/plan.wiki.
1 2 3 4 5 | <title>Limitations and planned improvements</title> Here is a list of some shortcomings of Zettelstore. They are planned to be solved. | | > | > | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 | <title>Limitations and planned improvements</title> Here is a list of some shortcomings of Zettelstore. They are planned to be solved. <h3>Serious limitations</h3> * … <h3>Smaller limitations</h3> * Quoted attribute values are not yet supported in Zettelmarkup: <code>{key="value with space"}</code>. * The horizontal tab character (<tt>U+0009</tt>) is not supported. * Missing support for citation keys. * Changing the content syntax is not reflected in file extension. * File names with additional text besides the zettel identifier are not always preserved. |
︙ | ︙ |