Many hyperlinks are disabled.
Use anonymous login
to enable hyperlinks.
Difference From trunk To version-0.0.13
2024-04-22
| ||
15:01 | Rename package sxhtml to sxwebs/sxhtml; update some dependencies ... (Leaf check-in: 3e4f0da507 user: stern tags: trunk) | |
2024-04-18
| ||
13:30 | Adapt to client change: api.URLBuilder ... (check-in: 85cb9d749b user: stern tags: trunk) | |
2021-06-07
| ||
09:11 | Increase version to 0.0.14-dev to begin next development cycle ... (check-in: 7dd6f4dd5c user: stern tags: trunk) | |
2021-06-01
| ||
12:35 | Version 0.0.13 ... (check-in: 11d9b6da63 user: stern tags: trunk, release, version-0.0.13) | |
10:14 | Log output while starting Command Line Server ... (check-in: 968a91bbaa user: stern tags: trunk) | |
Added .deepsource.toml.
> > > > > > > > | 1 2 3 4 5 6 7 8 | version = 1 [[analyzers]] name = "go" enabled = true [analyzers.meta] import_paths = ["github.com/zettelstore/zettelstore"] |
Changes to LICENSE.txt.
|
| | | 1 2 3 4 5 6 7 8 | Copyright (c) 2020-2021 Detlef Stern Licensed under the EUPL Zettelstore is licensed under the European Union Public License, version 1.2 or later (EUPL v. 1.2). The license is available in the official languages of the EU. The English version is included here. Please see https://joinup.ec.europa.eu/community/eupl/og_page/eupl for official |
︙ | ︙ |
Changes to Makefile.
1 |
| | | | | < < < < < < | | | | | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 | ## Copyright (c) 2020-2021 Detlef Stern ## ## This file is part of zettelstore. ## ## Zettelstore is licensed under the latest version of the EUPL (European Union ## Public License). Please see file LICENSE.txt for your rights and obligations ## under this license. .PHONY: check build release clean check: go run tools/build.go check 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 | 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. The software, including the manual, is licensed under the [European Union Public License 1.2 (or later)](https://zettelstore.de/home/file?name=LICENSE.txt&ci=trunk). [Stay tuned](https://twitter.com/zettelstore)… |
Changes to VERSION.
|
| | | 1 | 0.0.13 |
Changes to ast/ast.go.
1 | //----------------------------------------------------------------------------- | | | < < < | | | | > | < | > > > | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 | //----------------------------------------------------------------------------- // Copyright (c) 2020-2021 Detlef Stern // // This file is part of zettelstore. // // Zettelstore is licensed under the latest version of the EUPL (European Union // Public License). Please see file LICENSE.txt for your rights and obligations // under this license. //----------------------------------------------------------------------------- // Package ast provides the abstract syntax tree. 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 { // Zettel domain.Zettel 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. } // Node is the interface, all nodes must implement. type Node interface { Accept(v Visitor) } // BlockNode is the interface that all block nodes must implement. type BlockNode interface { Node blockNode() } // BlockSlice is a slice of BlockNodes. type BlockSlice []BlockNode // ItemNode is a node that can occur as a list item. type ItemNode interface { BlockNode itemNode() } |
︙ | ︙ | |||
63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 | 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 | > > > | < | 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 | type DescriptionSlice []DescriptionNode // InlineNode is the interface that all inline nodes must implement. type InlineNode interface { Node inlineNode() } // InlineSlice is a slice of InlineNodes. type InlineSlice []InlineNode // Reference is a reference to external or internal material. type Reference struct { URL *url.URL Value string 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 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 RefStateExternal // Reference to external material ) |
Added ast/attr.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-2021 Detlef Stern // // This file is part of zettelstore. // // Zettelstore is licensed under the latest version of the EUPL (European Union // Public License). Please see file LICENSE.txt for your rights and obligations // under this license. //----------------------------------------------------------------------------- // Package ast provides the abstract syntax tree. package ast import ( "strings" ) // Attributes store additional information about some node types. type Attributes struct { Attrs map[string]string } // HasDefault returns true, if the default attribute "-" has been set. func (a *Attributes) HasDefault() bool { if a != nil { _, ok := a.Attrs["-"] return ok } return false } // RemoveDefault removes the default attribute func (a *Attributes) RemoveDefault() { a.Remove("-") } // Get returns the attribute value of the given key and a succes value. func (a *Attributes) Get(key string) (string, bool) { if a != nil { value, ok := a.Attrs[key] return value, ok } return "", false } // Clone returns a duplicate of the attribute. func (a *Attributes) Clone() *Attributes { if a == nil { return nil } attrs := make(map[string]string, len(a.Attrs)) for k, v := range a.Attrs { attrs[k] = v } return &Attributes{attrs} } // Set changes the attribute that a given key has now a given value. func (a *Attributes) Set(key, value string) *Attributes { if a == nil { return &Attributes{map[string]string{key: value}} } if a.Attrs == nil { a.Attrs = make(map[string]string) } a.Attrs[key] = value return a } // Remove the key from the attributes. func (a *Attributes) Remove(key string) { if a != nil { delete(a.Attrs, key) } } // AddClass adds a value to the class attribute. func (a *Attributes) AddClass(class string) *Attributes { if a == nil { return &Attributes{map[string]string{"class": class}} } classes := a.GetClasses() for _, cls := range classes { if cls == class { return a } } classes = append(classes, class) a.Attrs["class"] = strings.Join(classes, " ") return a } // GetClasses returns the class values as a string slice func (a *Attributes) GetClasses() []string { if a == nil { return nil } classes, ok := a.Attrs["class"] if !ok { return nil } return strings.Fields(classes) } |
Added ast/attr_test.go.
> > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 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) 2020 Detlef Stern // // This file is part of zettelstore. // // Zettelstore is licensed under the latest version of the EUPL (European Union // Public License). Please see file LICENSE.txt for your rights and obligations // under this license. //----------------------------------------------------------------------------- // Package ast provides the abstract syntax tree. package ast_test import ( "testing" "zettelstore.de/z/ast" ) func TestHasDefault(t *testing.T) { attr := &ast.Attributes{} if attr.HasDefault() { t.Error("Should not have default attr") } attr = &ast.Attributes{Attrs: map[string]string{"-": "value"}} if !attr.HasDefault() { t.Error("Should have default attr") } } func TestAttrClone(t *testing.T) { orig := &ast.Attributes{} clone := orig.Clone() if len(clone.Attrs) > 0 { t.Error("Attrs must be empty") } orig = &ast.Attributes{Attrs: map[string]string{"": "0", "-": "1", "a": "b"}} clone = orig.Clone() m := clone.Attrs if m[""] != "0" || m["-"] != "1" || m["a"] != "b" || len(m) != len(orig.Attrs) { t.Error("Wrong cloned map") } m["a"] = "c" if orig.Attrs["a"] != "b" { t.Error("Aliased map") } } |
Changes to ast/block.go.
1 | //----------------------------------------------------------------------------- | | | < < < > < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < | | | < < | < | | | | | | | | < | < < | | < > | | | | | | | | | | | < < < | < | | < > | | < > | | | | | | | | | | | | | | | < < < < < < | | | < < < < < < < < < < < < < | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 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 | //----------------------------------------------------------------------------- // Copyright (c) 2020-2021 Detlef Stern // // This file is part of zettelstore. // // Zettelstore is licensed under the latest version of the EUPL (European Union // Public License). Please see file LICENSE.txt for your rights and obligations // under this license. //----------------------------------------------------------------------------- // Package ast provides the abstract syntax tree. package ast // Definition of Block nodes. // ParaNode contains just a sequence of inline elements. // Another name is "paragraph". type ParaNode struct { Inlines InlineSlice } func (pn *ParaNode) blockNode() {} func (pn *ParaNode) itemNode() {} func (pn *ParaNode) descriptionNode() {} // Accept a visitor and visit the node. func (pn *ParaNode) Accept(v Visitor) { v.VisitPara(pn) } //-------------------------------------------------------------------------- // VerbatimNode contains lines of uninterpreted text type VerbatimNode struct { Code VerbatimCode Attrs *Attributes Lines []string } // VerbatimCode specifies the format that is applied to code inline nodes. type VerbatimCode int // Constants for VerbatimCode const ( _ VerbatimCode = iota VerbatimProg // Program code. VerbatimComment // Block comment VerbatimHTML // Block HTML, e.g. for Markdown ) func (vn *VerbatimNode) blockNode() {} func (vn *VerbatimNode) itemNode() {} // Accept a visitor an visit the node. func (vn *VerbatimNode) Accept(v Visitor) { v.VisitVerbatim(vn) } //-------------------------------------------------------------------------- // RegionNode encapsulates a region of block nodes. type RegionNode struct { Code RegionCode Attrs *Attributes Blocks BlockSlice Inlines InlineSlice // Additional text at the end of the region } // RegionCode specifies the actual region type. type RegionCode int // Values for RegionCode const ( _ RegionCode = iota RegionSpan // Just a span of blocks RegionQuote // A longer quotation RegionVerse // Line breaks matter ) func (rn *RegionNode) blockNode() {} func (rn *RegionNode) itemNode() {} // Accept a visitor and visit the node. func (rn *RegionNode) Accept(v Visitor) { v.VisitRegion(rn) } //-------------------------------------------------------------------------- // HeadingNode stores the heading text and level. type HeadingNode struct { Level int Inlines InlineSlice // Heading text, possibly formatted Slug string // Heading text, suitable to be used as an URL fragment Attrs *Attributes } func (hn *HeadingNode) blockNode() {} func (hn *HeadingNode) itemNode() {} // Accept a visitor and visit the node. func (hn *HeadingNode) Accept(v Visitor) { v.VisitHeading(hn) } //-------------------------------------------------------------------------- // HRuleNode specifies a horizontal rule. type HRuleNode struct { Attrs *Attributes } func (hn *HRuleNode) blockNode() {} func (hn *HRuleNode) itemNode() {} // Accept a visitor and visit the node. func (hn *HRuleNode) Accept(v Visitor) { v.VisitHRule(hn) } //-------------------------------------------------------------------------- // NestedListNode specifies a nestable list, either ordered or unordered. type NestedListNode struct { Code NestedListCode Items []ItemSlice Attrs *Attributes } // NestedListCode specifies the actual list type. type NestedListCode int // Values for ListCode const ( _ NestedListCode = iota NestedListOrdered // Ordered list. NestedListUnordered // Unordered list. NestedListQuote // Quote list. ) func (ln *NestedListNode) blockNode() {} func (ln *NestedListNode) itemNode() {} // Accept a visitor and visit the node. func (ln *NestedListNode) Accept(v Visitor) { v.VisitNestedList(ln) } //-------------------------------------------------------------------------- // DescriptionListNode specifies a description list. type DescriptionListNode struct { Descriptions []Description } // Description is one element of a description list. type Description struct { Term InlineSlice Descriptions []DescriptionSlice } func (dn *DescriptionListNode) blockNode() {} // Accept a visitor and visit the node. func (dn *DescriptionListNode) Accept(v Visitor) { v.VisitDescriptionList(dn) } //-------------------------------------------------------------------------- // TableNode specifies a full table type TableNode struct { Header TableRow // The header row Align []Alignment // Default column alignment |
︙ | ︙ | |||
243 244 245 246 247 248 249 | _ Alignment = iota AlignDefault // Default alignment, inherited AlignLeft // Left alignment AlignCenter // Center the content AlignRight // Right alignment ) | < < < | < < < | < < < < < < < < < | < | < < < < < < < < < < < | | | | | | | 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 | _ Alignment = iota AlignDefault // Default alignment, inherited AlignLeft // Left alignment AlignCenter // Center the content AlignRight // Right alignment ) func (tn *TableNode) blockNode() {} // Accept a visitor and visit the node. func (tn *TableNode) Accept(v Visitor) { v.VisitTable(tn) } //-------------------------------------------------------------------------- // BLOBNode contains just binary data that must be interpreted according to // a syntax. type BLOBNode struct { Title string Syntax string Blob []byte } func (bn *BLOBNode) blockNode() {} // Accept a visitor and visit the node. func (bn *BLOBNode) Accept(v Visitor) { v.VisitBLOB(bn) } |
Changes to ast/inline.go.
1 | //----------------------------------------------------------------------------- | | | < < < > < < < < < < < < | < | < < | < < < < | < | | | | < | < < | < < | | | | | | | < < | < | < < | | | < > > | < > | < < | < < | | < | > | | < | < | < < < | < < < < < < | | < | < < < | | > | | | < < | < | < > | < < < < > | < | | | | | | | | > | > | > | > | | | | > | | > | < > | | | | | | | < | | | < | < > | | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 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 | //----------------------------------------------------------------------------- // Copyright (c) 2020-2021 Detlef Stern // // This file is part of zettelstore. // // Zettelstore is licensed under the latest version of the EUPL (European Union // Public License). Please see file LICENSE.txt for your rights and obligations // under this license. //----------------------------------------------------------------------------- // Package ast provides the abstract syntax tree. package ast // Definitions of inline nodes. // TextNode just contains some text. type TextNode struct { Text string // The text itself. } func (tn *TextNode) inlineNode() {} // Accept a visitor and visit the node. func (tn *TextNode) Accept(v Visitor) { v.VisitText(tn) } // -------------------------------------------------------------------------- // TagNode contains a tag. type TagNode struct { Tag string // The text itself. } func (tn *TagNode) inlineNode() {} // Accept a visitor and visit the node. func (tn *TagNode) Accept(v Visitor) { v.VisitTag(tn) } // -------------------------------------------------------------------------- // SpaceNode tracks inter-word space characters. type SpaceNode struct { Lexeme string } func (sn *SpaceNode) inlineNode() {} // Accept a visitor and visit the node. func (sn *SpaceNode) Accept(v Visitor) { v.VisitSpace(sn) } // -------------------------------------------------------------------------- // BreakNode signals a new line that must / should be interpreted as a new line break. type BreakNode struct { Hard bool // Hard line break? } func (bn *BreakNode) inlineNode() {} // Accept a visitor and visit the node. func (bn *BreakNode) Accept(v Visitor) { v.VisitBreak(bn) } // -------------------------------------------------------------------------- // LinkNode contains the specified link. type LinkNode struct { Ref *Reference Inlines InlineSlice // The text associated with the link. OnlyRef bool // True if no text was specified. Attrs *Attributes // Optional attributes } func (ln *LinkNode) inlineNode() {} // Accept a visitor and visit the node. func (ln *LinkNode) Accept(v Visitor) { v.VisitLink(ln) } // -------------------------------------------------------------------------- // ImageNode contains the specified image reference. type ImageNode struct { Ref *Reference // Reference to image Blob []byte // BLOB data of the image, as an alternative to Ref. Syntax string // Syntax of Blob Inlines InlineSlice // The text associated with the image. Attrs *Attributes // Optional attributes } func (in *ImageNode) inlineNode() {} // Accept a visitor and visit the node. func (in *ImageNode) Accept(v Visitor) { v.VisitImage(in) } // -------------------------------------------------------------------------- // CiteNode contains the specified citation. type CiteNode struct { Key string // The citation key Inlines InlineSlice // The text associated with the citation. Attrs *Attributes // Optional attributes } func (cn *CiteNode) inlineNode() {} // Accept a visitor and visit the node. func (cn *CiteNode) Accept(v Visitor) { v.VisitCite(cn) } // -------------------------------------------------------------------------- // MarkNode contains the specified merked position. // It is a BlockNode too, because although it is typically parsed during inline // mode, it is moved into block mode afterwards. type MarkNode struct { Text string } func (mn *MarkNode) inlineNode() {} // Accept a visitor and visit the node. func (mn *MarkNode) Accept(v Visitor) { v.VisitMark(mn) } // -------------------------------------------------------------------------- // FootnoteNode contains the specified footnote. type FootnoteNode struct { Inlines InlineSlice // The footnote text. Attrs *Attributes // Optional attributes } func (fn *FootnoteNode) inlineNode() {} // Accept a visitor and visit the node. func (fn *FootnoteNode) Accept(v Visitor) { v.VisitFootnote(fn) } // -------------------------------------------------------------------------- // FormatNode specifies some inline formatting. type FormatNode struct { Code FormatCode Attrs *Attributes // Optional attributes. Inlines InlineSlice } // FormatCode specifies the format that is applied to the inline nodes. type FormatCode int // Constants for FormatCode const ( _ FormatCode = iota FormatItalic // Italic text. FormatEmph // Semantically emphasized text. FormatBold // Bold text. FormatStrong // Semantically strongly emphasized text. FormatUnder // Underlined text. FormatInsert // Inserted text. FormatStrike // Text that is no longer relevant or no longer accurate. FormatDelete // Deleted text. FormatSuper // Superscripted text. FormatSub // SubscriptedText. FormatQuote // Quoted text. FormatQuotation // Quotation text. FormatSmall // Smaller text. FormatSpan // Generic inline container. FormatMonospace // Monospaced text. ) func (fn *FormatNode) inlineNode() {} // Accept a visitor and visit the node. func (fn *FormatNode) Accept(v Visitor) { v.VisitFormat(fn) } // -------------------------------------------------------------------------- // LiteralNode specifies some uninterpreted text. type LiteralNode struct { Code LiteralCode Attrs *Attributes // Optional attributes. Text string } // LiteralCode specifies the format that is applied to code inline nodes. type LiteralCode int // Constants for LiteralCode const ( _ LiteralCode = iota LiteralProg // Inline program code. LiteralKeyb // Keyboard strokes. LiteralOutput // Sample output. LiteralComment // Inline comment LiteralHTML // Inline HTML, e.g. for Markdown ) func (rn *LiteralNode) inlineNode() {} // Accept a visitor and visit the node. func (rn *LiteralNode) Accept(v Visitor) { v.VisitLiteral(rn) } |
Changes to ast/ref.go.
1 | //----------------------------------------------------------------------------- | | | < < < > < < | < < < | > < < < | | < < < < < | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 | //----------------------------------------------------------------------------- // Copyright (c) 2020-2021 Detlef Stern // // This file is part of zettelstore. // // Zettelstore is licensed under the latest version of the EUPL (European Union // Public License). Please see file LICENSE.txt for your rights and obligations // under this license. //----------------------------------------------------------------------------- // Package ast provides the abstract syntax tree. package ast import ( "net/url" "zettelstore.de/z/domain/id" ) // ParseReference parses a string and returns a reference. func ParseReference(s string) *Reference { switch s { case "", "00000000000000": return &Reference{URL: nil, Value: s, State: RefStateInvalid} } 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 len(u.Scheme)+len(u.Opaque)+len(u.Host) == 0 && u.User == nil { if _, err := id.Parse(u.Path); err == nil { return &Reference{URL: u, Value: s, State: RefStateZettel} } if u.Path == "" && u.Fragment != "" { return &Reference{URL: u, Value: s, State: RefStateSelf} } } return &Reference{URL: u, Value: s, State: RefStateExternal} } func localState(path string) (RefState, bool) { if len(path) > 0 && path[0] == '/' { if len(path) > 1 && path[1] == '/' { return RefStateBased, true } return RefStateHosted, true } |
︙ | ︙ | |||
78 79 80 81 82 83 84 | } // String returns the string representation of a reference. func (r Reference) String() string { if r.URL != nil { return r.URL.String() } | < < < | 64 65 66 67 68 69 70 71 72 73 74 75 76 77 | } // String returns the string representation of a reference. func (r Reference) String() string { if r.URL != nil { return r.URL.String() } 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. |
︙ | ︙ |
Changes to ast/ref_test.go.
1 | //----------------------------------------------------------------------------- | | | < < < > < | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 | //----------------------------------------------------------------------------- // Copyright (c) 2020-2021 Detlef Stern // // This file is part of zettelstore. // // Zettelstore is licensed under the latest version of the EUPL (European Union // Public License). Please see file LICENSE.txt for your rights and obligations // under this license. //----------------------------------------------------------------------------- // Package ast_test provides the tests for the abstract syntax tree. package ast_test import ( "testing" "zettelstore.de/z/ast" ) func TestParseReference(t *testing.T) { testcases := []struct { link string err bool exp string }{ {"", true, ""}, {"123", false, "123"}, |
︙ | ︙ | |||
40 41 42 43 44 45 46 | 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) { | < | 37 38 39 40 41 42 43 44 45 46 47 48 49 50 | if got.IsValid() && got.String() != tc.exp { t.Errorf("TC=%d, Reference of %q is %q, but got %q", i, tc.link, tc.exp, got) } } } func TestReferenceIsZettelMaterial(t *testing.T) { testcases := []struct { link string isZettel bool isExternal bool isLocal bool }{ {"", false, false, false}, |
︙ | ︙ |
Added ast/traverser.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) 2020-2021 Detlef Stern // // This file is part of zettelstore. // // Zettelstore is licensed under the latest version of the EUPL (European Union // Public License). Please see file LICENSE.txt for your rights and obligations // under this license. //----------------------------------------------------------------------------- // Package ast provides the abstract syntax tree. package ast // A traverser is a Visitor that just traverses the AST and delegates node // spacific actions to a Visitor. This Visitor should not traverse the AST. // TopDownTraverser visits first the node and then the children nodes. type TopDownTraverser struct { v Visitor } // NewTopDownTraverser creates a new traverser. func NewTopDownTraverser(visitor Visitor) TopDownTraverser { return TopDownTraverser{visitor} } // VisitVerbatim has nothing to traverse. func (t TopDownTraverser) VisitVerbatim(vn *VerbatimNode) { t.v.VisitVerbatim(vn) } // VisitRegion traverses the content and the additional text. func (t TopDownTraverser) VisitRegion(rn *RegionNode) { t.v.VisitRegion(rn) t.VisitBlockSlice(rn.Blocks) t.VisitInlineSlice(rn.Inlines) } // VisitHeading traverses the heading. func (t TopDownTraverser) VisitHeading(hn *HeadingNode) { t.v.VisitHeading(hn) t.VisitInlineSlice(hn.Inlines) } // VisitHRule traverses nothing. func (t TopDownTraverser) VisitHRule(hn *HRuleNode) { t.v.VisitHRule(hn) } // VisitNestedList traverses all nested list elements. func (t TopDownTraverser) VisitNestedList(ln *NestedListNode) { t.v.VisitNestedList(ln) for _, item := range ln.Items { t.visitItemSlice(item) } } // VisitDescriptionList traverses all description terms and their associated // descriptions. func (t TopDownTraverser) VisitDescriptionList(dn *DescriptionListNode) { t.v.VisitDescriptionList(dn) for _, defs := range dn.Descriptions { t.VisitInlineSlice(defs.Term) for _, descr := range defs.Descriptions { t.visitDescriptionSlice(descr) } } } // VisitPara traverses the inlines of a paragraph. func (t TopDownTraverser) VisitPara(pn *ParaNode) { t.v.VisitPara(pn) t.VisitInlineSlice(pn.Inlines) } // VisitTable traverses all cells of the header and then row-wise all cells of // the table body. func (t TopDownTraverser) VisitTable(tn *TableNode) { t.v.VisitTable(tn) for _, col := range tn.Header { t.VisitInlineSlice(col.Inlines) } for _, row := range tn.Rows { for _, col := range row { t.VisitInlineSlice(col.Inlines) } } } // VisitBLOB traverses nothing. func (t TopDownTraverser) VisitBLOB(bn *BLOBNode) { t.v.VisitBLOB(bn) } // VisitText traverses nothing. func (t TopDownTraverser) VisitText(tn *TextNode) { t.v.VisitText(tn) } // VisitTag traverses nothing. func (t TopDownTraverser) VisitTag(tn *TagNode) { t.v.VisitTag(tn) } // VisitSpace traverses nothing. func (t TopDownTraverser) VisitSpace(sn *SpaceNode) { t.v.VisitSpace(sn) } // VisitBreak traverses nothing. func (t TopDownTraverser) VisitBreak(bn *BreakNode) { t.v.VisitBreak(bn) } // VisitLink traverses the link text. func (t TopDownTraverser) VisitLink(ln *LinkNode) { t.v.VisitLink(ln) t.VisitInlineSlice(ln.Inlines) } // VisitImage traverses the image text. func (t TopDownTraverser) VisitImage(in *ImageNode) { t.v.VisitImage(in) t.VisitInlineSlice(in.Inlines) } // VisitCite traverses the cite text. func (t TopDownTraverser) VisitCite(cn *CiteNode) { t.v.VisitCite(cn) t.VisitInlineSlice(cn.Inlines) } // VisitFootnote traverses the footnote text. func (t TopDownTraverser) VisitFootnote(fn *FootnoteNode) { t.v.VisitFootnote(fn) t.VisitInlineSlice(fn.Inlines) } // VisitMark traverses nothing. func (t TopDownTraverser) VisitMark(mn *MarkNode) { t.v.VisitMark(mn) } // VisitFormat traverses the formatted text. func (t TopDownTraverser) VisitFormat(fn *FormatNode) { t.v.VisitFormat(fn) t.VisitInlineSlice(fn.Inlines) } // VisitLiteral traverses nothing. func (t TopDownTraverser) VisitLiteral(ln *LiteralNode) { t.v.VisitLiteral(ln) } // VisitBlockSlice traverses a block slice. func (t TopDownTraverser) VisitBlockSlice(bns BlockSlice) { for _, bn := range bns { bn.Accept(t) } } func (t TopDownTraverser) visitItemSlice(ins ItemSlice) { for _, in := range ins { in.Accept(t) } } func (t TopDownTraverser) visitDescriptionSlice(dns DescriptionSlice) { for _, dn := range dns { dn.Accept(t) } } // VisitInlineSlice traverses a block slice. func (t TopDownTraverser) VisitInlineSlice(ins InlineSlice) { for _, in := range ins { in.Accept(t) } } |
Added ast/visitor.go.
> > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 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 Detlef Stern // // This file is part of zettelstore. // // Zettelstore is licensed under the latest version of the EUPL (European Union // Public License). Please see file LICENSE.txt for your rights and obligations // under this license. //----------------------------------------------------------------------------- // Package ast provides the abstract syntax tree. package ast // Visitor is the interface all visitors must implement. type Visitor interface { // Block nodes VisitVerbatim(vn *VerbatimNode) VisitRegion(rn *RegionNode) VisitHeading(hn *HeadingNode) VisitHRule(hn *HRuleNode) VisitNestedList(ln *NestedListNode) VisitDescriptionList(dn *DescriptionListNode) VisitPara(pn *ParaNode) VisitTable(tn *TableNode) VisitBLOB(bn *BLOBNode) // Inline nodes VisitText(tn *TextNode) VisitTag(tn *TagNode) VisitSpace(sn *SpaceNode) VisitBreak(bn *BreakNode) VisitLink(ln *LinkNode) VisitImage(in *ImageNode) VisitCite(cn *CiteNode) VisitFootnote(fn *FootnoteNode) VisitMark(mn *MarkNode) VisitFormat(fn *FormatNode) VisitLiteral(ln *LiteralNode) } |
Deleted ast/walk.go.
|
| < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < |
Deleted ast/walk_test.go.
|
| < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < |
Changes to auth/auth.go.
1 | //----------------------------------------------------------------------------- | | | < < < | | | > | | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 | //----------------------------------------------------------------------------- // Copyright (c) 2021 Detlef Stern // // This file is part of zettelstore. // // Zettelstore is licensed under the latest version of the EUPL (European Union // Public License). Please see file LICENSE.txt for your rights and obligations // under this license. //----------------------------------------------------------------------------- // Package auth provides services for authentification / authorization. package auth import ( "time" "zettelstore.de/z/config" "zettelstore.de/z/domain/id" "zettelstore.de/z/domain/meta" "zettelstore.de/z/place" "zettelstore.de/z/web/server" ) // BaseManager allows to check some base auth modes. type BaseManager interface { // IsReadonly returns true, if the systems is configured to run in read-only-mode. IsReadonly() bool } |
︙ | ︙ | |||
41 42 43 44 45 46 47 | // TokenKind specifies for which application / usage a token is/was requested. type TokenKind int // Allowed values of token kind const ( _ TokenKind = iota | | | | 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 | // 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 |
︙ | ︙ | |||
77 78 79 80 81 82 83 | } // Manager is the main interface for providing the service. type Manager interface { TokenManager AuthzManager | | | | < < < | 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 | } // Manager is the main interface for providing the service. type Manager interface { TokenManager AuthzManager PlaceWithPolicy(auth server.Auth, unprotectedPlace place.Place, rtConfig config.Config) (place.Place, Policy) } // Policy is an interface for checking access authorization. type Policy interface { // User is allowed to create a new zettel. 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 } |
Changes to auth/cred/cred.go.
1 | //----------------------------------------------------------------------------- | | | < < < | | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 | //----------------------------------------------------------------------------- // Copyright (c) 2020-2021 Detlef Stern // // This file is part of zettelstore. // // Zettelstore is licensed under the latest version of the EUPL (European Union // Public License). Please see file LICENSE.txt for your rights and obligations // under this license. //----------------------------------------------------------------------------- // Package 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 { |
︙ | ︙ | |||
43 44 45 46 47 48 49 | return false, nil } return false, err } func createFullCredential(zid id.Zid, ident, credential string) []byte { var buf bytes.Buffer | | | 40 41 42 43 44 45 46 47 48 49 50 51 52 53 | 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() } |
Deleted auth/impl/digest.go.
|
| < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < |
Changes to auth/impl/impl.go.
1 | //----------------------------------------------------------------------------- | | | < < < | | < > | | | | | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 | //----------------------------------------------------------------------------- // Copyright (c) 2021 Detlef Stern // // This file is part of zettelstore. // // Zettelstore is licensed under the latest version of the EUPL (European Union // Public License). Please see file LICENSE.txt for your rights and obligations // under this license. //----------------------------------------------------------------------------- // Package impl provides services for authentification / authorization. package impl import ( "errors" "hash/fnv" "io" "time" "github.com/pascaldekloe/jwt" "zettelstore.de/z/auth" "zettelstore.de/z/auth/policy" "zettelstore.de/z/config" "zettelstore.de/z/domain/id" "zettelstore.de/z/domain/meta" "zettelstore.de/z/kernel" "zettelstore.de/z/place" "zettelstore.de/z/web/server" ) type myAuth struct { readonly bool owner id.Zid secret []byte } |
︙ | ︙ | |||
66 67 68 69 70 71 72 | } 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 } | > | > | > > > | | | | < | > > > | | > > > > | > > | < | < | | < < < < < < < | > > > | < | | | | > > > > | > | | > > | < < > | | | < < < < < | | 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 | } 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 // ErrNoUser signals that the meta data has no role value 'user'. var ErrNoUser = errors.New("auth: meta is no user") // ErrNoIdent signals that the 'ident' key is missing. var ErrNoIdent = errors.New("auth: missing ident") // ErrOtherKind signals that the token was defined for another token kind. 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) { if role, ok := ident.Get(meta.KeyRole); !ok || role != meta.ValueRoleUser { return nil, ErrNoUser } subject, ok := ident.Get(meta.KeyUserID) if !ok || subject == "" { return nil, ErrNoIdent } now := time.Now().Round(time.Second) 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, err := id.Parse(zidS); err == nil { if kind, ok := claims.Set["_tk"].(float64); ok { if auth.TokenKind(kind) == k { return auth.TokenData{ Token: token, Now: now, Issued: claims.Issued.Time(), Expires: expires, Ident: ident, Zid: zid, }, nil } } return auth.TokenData{}, ErrOtherKind } } return auth.TokenData{}, ErrNoZid } func (a *myAuth) Owner() id.Zid { return a.owner } func (a *myAuth) IsOwner(zid id.Zid) bool { return zid.IsValid() && zid == a.owner } |
︙ | ︙ | |||
163 164 165 166 167 168 169 | return meta.UserRoleUnknown } return meta.UserRoleOwner } if a.IsOwner(user.Zid) { return meta.UserRoleOwner } | | | | | 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 | return meta.UserRoleUnknown } return meta.UserRoleOwner } if a.IsOwner(user.Zid) { return meta.UserRoleOwner } if val, ok := user.Get(meta.KeyUserRole); ok { if ur := meta.GetUserRole(val); ur != meta.UserRoleUnknown { return ur } } return meta.UserRoleReader } func (a *myAuth) PlaceWithPolicy(auth server.Auth, unprotectedPlace place.Place, rtConfig config.Config) (place.Place, auth.Policy) { return policy.PlaceWithPolicy(auth, a, unprotectedPlace, rtConfig) } |
Changes to auth/policy/anon.go.
1 | //----------------------------------------------------------------------------- | | | < < < > | | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 | //----------------------------------------------------------------------------- // Copyright (c) 2020-2021 Detlef Stern // // This file is part of zettelstore. // // Zettelstore is licensed under the latest version of the EUPL (European Union // Public License). Please see file LICENSE.txt for your rights and obligations // under this license. //----------------------------------------------------------------------------- // Package policy provides some interfaces and implementation for authorization policies. 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 } |
︙ | ︙ | |||
40 41 42 43 44 45 46 | 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) } | < < < < < < < | 38 39 40 41 42 43 44 45 46 47 48 49 50 | 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) checkVisibility(m *meta.Meta) bool { if ap.authConfig.GetVisibility(m) == meta.VisibilityExpert { return ap.authConfig.GetExpertMode() } return true } |
Deleted auth/policy/box.go.
|
| < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < |
Changes to auth/policy/default.go.
1 | //----------------------------------------------------------------------------- | | | < < < > < | | | | < < | | | | | | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 | //----------------------------------------------------------------------------- // Copyright (c) 2020-2021 Detlef Stern // // This file is part of zettelstore. // // Zettelstore is licensed under the latest version of the EUPL (European Union // Public License). Please see file LICENSE.txt for your rights and obligations // under this license. //----------------------------------------------------------------------------- // Package policy provides some interfaces and implementation for authorizsation policies. package policy import ( "zettelstore.de/z/auth" "zettelstore.de/z/domain/meta" ) type defaultPolicy struct { manager auth.AuthzManager } func (d *defaultPolicy) CanCreate(user, newMeta *meta.Meta) bool { return true } func (d *defaultPolicy) CanRead(user, m *meta.Meta) bool { return true } func (d *defaultPolicy) CanWrite(user, oldMeta, newMeta *meta.Meta) bool { return d.canChange(user, oldMeta) } func (d *defaultPolicy) CanRename(user, m *meta.Meta) bool { return d.canChange(user, m) } func (d *defaultPolicy) CanDelete(user, m *meta.Meta) bool { return d.canChange(user, m) } func (d *defaultPolicy) canChange(user, m *meta.Meta) bool { metaRo, ok := m.Get(meta.KeyReadOnly) if !ok { return true } if user == nil { // If we are here, there is no authentication. // See owner.go:CanWrite. // No authentication: check for owner-like restriction, because the user // acts as an owner return metaRo != meta.ValueUserRoleOwner && !meta.BoolValue(metaRo) } userRole := d.manager.GetUserRole(user) switch metaRo { case meta.ValueUserRoleReader: return userRole > meta.UserRoleReader case meta.ValueUserRoleWriter: return userRole > meta.UserRoleWriter case meta.ValueUserRoleOwner: return userRole > meta.UserRoleOwner } return !meta.BoolValue(metaRo) } |
Changes to auth/policy/owner.go.
1 | //----------------------------------------------------------------------------- | | | < < < > < | | | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 | //----------------------------------------------------------------------------- // Copyright (c) 2020-2021 Detlef Stern // // This file is part of zettelstore. // // Zettelstore is licensed under the latest version of the EUPL (European Union // Public License). Please see file LICENSE.txt for your rights and obligations // under this license. //----------------------------------------------------------------------------- // Package 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" ) 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 role, ok := newMeta.Get(meta.KeyRole); ok && role == meta.ValueRoleUser { 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. |
︙ | ︙ | |||
59 60 61 62 63 64 65 | return false case meta.VisibilityPublic: return true } if user == nil { return false } | | < < | < < < < < | | | | | | < | 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 | return false case meta.VisibilityPublic: return true } if user == nil { return false } if role, ok := m.Get(meta.KeyRole); ok && role == meta.ValueRoleUser { // Only the user can read its own zettel return user.Zid == m.Zid } return true } var noChangeUser = []string{ meta.KeyID, meta.KeyRole, meta.KeyUserID, meta.KeyUserRole, } func (o *ownerPolicy) CanWrite(user, oldMeta, newMeta *meta.Meta) bool { if user == nil || !o.pre.CanWrite(user, oldMeta, newMeta) { return false } 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 role, ok := oldMeta.Get(meta.KeyRole); ok && role == meta.ValueRoleUser { // Here we know, that user.Zid == newMeta.Zid (because of userCanRead) and // user.Zid == newMeta.Zid (because oldMeta.Zid == newMeta.Zid) for _, key := range noChangeUser { if oldMeta.GetDefault(key, "") != newMeta.GetDefault(key, "") { return false } } return true } if o.manager.GetUserRole(user) == meta.UserRoleReader { return false } return o.userCanCreate(user, newMeta) } func (o *ownerPolicy) CanRename(user, m *meta.Meta) bool { if user == nil || !o.pre.CanRename(user, m) { |
︙ | ︙ | |||
131 132 133 134 135 136 137 | } if res, ok := o.checkVisibility(user, o.authConfig.GetVisibility(m)); ok { return res } return o.userIsOwner(user) } | < < < < < < < < < < | | 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 | } if res, ok := o.checkVisibility(user, o.authConfig.GetVisibility(m)); ok { return res } return o.userIsOwner(user) } 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(meta.KeyUserRole); ok && val == meta.ValueUserRoleOwner { return true } return false } |
Added auth/policy/place.go.
> > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 | //----------------------------------------------------------------------------- // Copyright (c) 2020-2021 Detlef Stern // // This file is part of zettelstore. // // Zettelstore is licensed under the latest version of the EUPL (European Union // Public License). Please see file LICENSE.txt for your rights and obligations // under this license. //----------------------------------------------------------------------------- // Package policy provides some interfaces and implementation for authorizsation policies. package policy import ( "context" "io" "zettelstore.de/z/auth" "zettelstore.de/z/config" "zettelstore.de/z/domain" "zettelstore.de/z/domain/id" "zettelstore.de/z/domain/meta" "zettelstore.de/z/place" "zettelstore.de/z/search" "zettelstore.de/z/web/server" ) // PlaceWithPolicy wraps the given place inside a policy place. func PlaceWithPolicy( auth server.Auth, manager auth.AuthzManager, place place.Place, authConfig config.AuthConfig, ) (place.Place, auth.Policy) { pol := newPolicy(manager, authConfig) return newPlace(auth, place, pol), pol } // polPlace implements a policy place. type polPlace struct { auth server.Auth place place.Place policy auth.Policy } // newPlace creates a new policy place. func newPlace(auth server.Auth, place place.Place, policy auth.Policy) place.Place { return &polPlace{ auth: auth, place: place, policy: policy, } } func (pp *polPlace) Location() string { return pp.place.Location() } func (pp *polPlace) CanCreateZettel(ctx context.Context) bool { return pp.place.CanCreateZettel(ctx) } func (pp *polPlace) CreateZettel(ctx context.Context, zettel domain.Zettel) (id.Zid, error) { user := pp.auth.GetUser(ctx) if pp.policy.CanCreate(user, zettel.Meta) { return pp.place.CreateZettel(ctx, zettel) } return id.Invalid, place.NewErrNotAllowed("Create", user, id.Invalid) } func (pp *polPlace) GetZettel(ctx context.Context, zid id.Zid) (domain.Zettel, error) { zettel, err := pp.place.GetZettel(ctx, zid) if err != nil { return domain.Zettel{}, err } user := pp.auth.GetUser(ctx) if pp.policy.CanRead(user, zettel.Meta) { return zettel, nil } return domain.Zettel{}, place.NewErrNotAllowed("GetZettel", user, zid) } func (pp *polPlace) GetMeta(ctx context.Context, zid id.Zid) (*meta.Meta, error) { m, err := pp.place.GetMeta(ctx, zid) if err != nil { return nil, err } user := pp.auth.GetUser(ctx) if pp.policy.CanRead(user, m) { return m, nil } return nil, place.NewErrNotAllowed("GetMeta", user, zid) } func (pp *polPlace) FetchZids(ctx context.Context) (id.Set, error) { return nil, place.NewErrNotAllowed("fetch-zids", pp.auth.GetUser(ctx), id.Invalid) } func (pp *polPlace) SelectMeta(ctx context.Context, s *search.Search) ([]*meta.Meta, error) { user := pp.auth.GetUser(ctx) canRead := pp.policy.CanRead s = s.AddPreMatch(func(m *meta.Meta) bool { return canRead(user, m) }) return pp.place.SelectMeta(ctx, s) } func (pp *polPlace) CanUpdateZettel(ctx context.Context, zettel domain.Zettel) bool { return pp.place.CanUpdateZettel(ctx, zettel) } func (pp *polPlace) UpdateZettel(ctx context.Context, zettel domain.Zettel) error { zid := zettel.Meta.Zid user := pp.auth.GetUser(ctx) if !zid.IsValid() { return &place.ErrInvalidID{Zid: zid} } // Write existing zettel oldMeta, err := pp.place.GetMeta(ctx, zid) if err != nil { return err } if pp.policy.CanWrite(user, oldMeta, zettel.Meta) { return pp.place.UpdateZettel(ctx, zettel) } return place.NewErrNotAllowed("Write", user, zid) } func (pp *polPlace) AllowRenameZettel(ctx context.Context, zid id.Zid) bool { return pp.place.AllowRenameZettel(ctx, zid) } func (pp *polPlace) RenameZettel(ctx context.Context, curZid, newZid id.Zid) error { meta, err := pp.place.GetMeta(ctx, curZid) if err != nil { return err } user := pp.auth.GetUser(ctx) if pp.policy.CanRename(user, meta) { return pp.place.RenameZettel(ctx, curZid, newZid) } return place.NewErrNotAllowed("Rename", user, curZid) } func (pp *polPlace) CanDeleteZettel(ctx context.Context, zid id.Zid) bool { return pp.place.CanDeleteZettel(ctx, zid) } func (pp *polPlace) DeleteZettel(ctx context.Context, zid id.Zid) error { meta, err := pp.place.GetMeta(ctx, zid) if err != nil { return err } user := pp.auth.GetUser(ctx) if pp.policy.CanDelete(user, meta) { return pp.place.DeleteZettel(ctx, zid) } return place.NewErrNotAllowed("Delete", user, zid) } func (pp *polPlace) ReadStats(st *place.Stats) { pp.place.ReadStats(st) } func (pp *polPlace) Dump(w io.Writer) { pp.place.Dump(w) } |
Changes to auth/policy/policy.go.
1 | //----------------------------------------------------------------------------- | | | < < < | | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 | //----------------------------------------------------------------------------- // Copyright (c) 2020-2021 Detlef Stern // // This file is part of zettelstore. // // Zettelstore is licensed under the latest version of the EUPL (European Union // Public License). Please see file LICENSE.txt for your rights and obligations // under this license. //----------------------------------------------------------------------------- // Package policy provides some interfaces and implementation for authorizsation policies. package policy import ( "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{} |
︙ | ︙ | |||
63 64 65 66 67 68 69 | 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) } | < < < < | 60 61 62 63 64 65 66 | 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) } |
Changes to auth/policy/policy_test.go.
1 | //----------------------------------------------------------------------------- | | | < < < > < | | < < | < | < | < | < | < | < | < | < | | > > | < | | | | < | | | | | < | | > > > > | > > > | < < < < | < < < < < < < < < < < < < < < < < < | < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < | | | < | | | | < < < < < < < | | | | | | | | | | | | | | | < < < < < < | | | | | | | | | | | | | | | | | | | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 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 | //----------------------------------------------------------------------------- // Copyright (c) 2020-2021 Detlef Stern // // This file is part of zettelstore. // // Zettelstore is licensed under the latest version of the EUPL (European Union // Public License). Please see file LICENSE.txt for your rights and obligations // under this license. //----------------------------------------------------------------------------- // Package policy provides some interfaces and implementation for authorizsation policies. package policy import ( "fmt" "testing" "zettelstore.de/z/auth" "zettelstore.de/z/domain/id" "zettelstore.de/z/domain/meta" ) func TestPolicies(t *testing.T) { testScene := []struct { readonly bool withAuth bool expert bool }{ {true, true, true}, {true, true, false}, {true, false, true}, {true, false, false}, {false, true, true}, {false, true, false}, {false, false, true}, {false, false, false}, } for _, ts := range testScene { authzManager := &testAuthzManager{ readOnly: ts.readonly, withAuth: ts.withAuth, } pol := newPolicy(authzManager, &authConfig{ts.expert}) name := fmt.Sprintf("readonly=%v/withauth=%v/expert=%v", ts.readonly, ts.withAuth, ts.expert) t.Run(name, func(tt *testing.T) { testCreate(tt, pol, ts.withAuth, ts.readonly, ts.expert) testRead(tt, pol, ts.withAuth, ts.readonly, ts.expert) testWrite(tt, pol, ts.withAuth, ts.readonly, ts.expert) testRename(tt, pol, ts.withAuth, ts.readonly, ts.expert) testDelete(tt, pol, ts.withAuth, ts.readonly, ts.expert) }) } } type testAuthzManager struct { readOnly bool withAuth bool } func (a *testAuthzManager) IsReadonly() bool { return a.readOnly } func (a *testAuthzManager) Owner() id.Zid { return ownerZid } func (a *testAuthzManager) IsOwner(zid id.Zid) bool { return zid == ownerZid } func (a *testAuthzManager) WithAuth() bool { return a.withAuth } func (a *testAuthzManager) GetUserRole(user *meta.Meta) meta.UserRole { if user == nil { if a.WithAuth() { return meta.UserRoleUnknown } return meta.UserRoleOwner } if a.IsOwner(user.Zid) { return meta.UserRoleOwner } if val, ok := user.Get(meta.KeyUserRole); ok { if ur := meta.GetUserRole(val); ur != meta.UserRoleUnknown { return ur } } return meta.UserRoleReader } type authConfig struct{ expert bool } func (ac *authConfig) GetExpertMode() bool { return ac.expert } func (ac *authConfig) GetVisibility(m *meta.Meta) meta.Visibility { if vis, ok := m.Get(meta.KeyVisibility); ok { switch vis { case meta.ValueVisibilityPublic: return meta.VisibilityPublic case meta.ValueVisibilityOwner: return meta.VisibilityOwner case meta.ValueVisibilityExpert: return meta.VisibilityExpert } } return meta.VisibilityLogin } func testCreate(t *testing.T, pol auth.Policy, withAuth, readonly, isExpert bool) { t.Helper() anonUser := newAnon() 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}, {reader, nil, false}, {writer, nil, false}, {owner, nil, false}, {owner2, nil, false}, // Ordinary zettel {anonUser, zettel, !withAuth && !readonly}, {reader, zettel, !withAuth && !readonly}, {writer, zettel, !readonly}, {owner, zettel, !readonly}, {owner2, zettel, !readonly}, // User zettel {anonUser, userZettel, !withAuth && !readonly}, {reader, userZettel, !withAuth && !readonly}, {writer, userZettel, !withAuth && !readonly}, {owner, userZettel, !readonly}, {owner2, userZettel, !readonly}, } for _, tc := range testCases { t.Run("Create", func(tt *testing.T) { got := pol.CanCreate(tc.user, tc.meta) if tc.exp != got { tt.Errorf("exp=%v, but got=%v", tc.exp, got) } }) } } func testRead(t *testing.T, pol auth.Policy, withAuth, readonly, expert bool) { t.Helper() anonUser := newAnon() reader := newReader() writer := newWriter() owner := newOwner() owner2 := newOwner2() zettel := newZettel() publicZettel := newPublicZettel() loginZettel := newLoginZettel() ownerZettel := newOwnerZettel() expertZettel := newExpertZettel() userZettel := newUserZettel() testCases := []struct { user *meta.Meta meta *meta.Meta exp bool }{ // No meta {anonUser, nil, false}, {reader, nil, false}, {writer, nil, false}, {owner, nil, false}, {owner2, nil, false}, // Ordinary zettel {anonUser, zettel, !withAuth}, {reader, zettel, true}, {writer, zettel, true}, {owner, zettel, true}, {owner2, zettel, true}, // Public zettel {anonUser, publicZettel, true}, {reader, publicZettel, true}, {writer, publicZettel, true}, {owner, publicZettel, true}, {owner2, publicZettel, true}, // Login zettel {anonUser, loginZettel, !withAuth}, {reader, loginZettel, true}, {writer, loginZettel, true}, {owner, loginZettel, true}, {owner2, loginZettel, true}, // Owner zettel {anonUser, ownerZettel, !withAuth}, {reader, ownerZettel, !withAuth}, {writer, ownerZettel, !withAuth}, {owner, ownerZettel, true}, {owner2, ownerZettel, true}, // Expert zettel {anonUser, expertZettel, !withAuth && expert}, {reader, expertZettel, !withAuth && expert}, {writer, expertZettel, !withAuth && expert}, {owner, expertZettel, expert}, {owner2, expertZettel, expert}, // Other user zettel {anonUser, userZettel, !withAuth}, {reader, userZettel, !withAuth}, {writer, userZettel, !withAuth}, {owner, userZettel, true}, {owner2, userZettel, true}, // Own user zettel {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() 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(meta.KeyUserRole, owner.GetDefault(meta.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}, {reader, nil, nil, false}, {writer, nil, nil, false}, {owner, nil, nil, false}, {owner2, nil, nil, false}, // No old meta {anonUser, nil, zettel, false}, {reader, nil, zettel, false}, {writer, nil, zettel, false}, {owner, nil, zettel, false}, {owner2, nil, zettel, false}, // No new meta {anonUser, zettel, nil, false}, {reader, zettel, nil, false}, {writer, zettel, nil, false}, {owner, zettel, nil, false}, {owner2, zettel, nil, false}, // Old an new zettel have different zettel identifier {anonUser, zettel, publicZettel, false}, {reader, zettel, publicZettel, false}, {writer, zettel, publicZettel, false}, {owner, zettel, publicZettel, false}, {owner2, zettel, publicZettel, false}, // Overwrite a normal zettel {anonUser, zettel, zettel, notAuthNotReadonly}, {reader, zettel, zettel, notAuthNotReadonly}, {writer, zettel, zettel, !readonly}, {owner, zettel, zettel, !readonly}, {owner2, zettel, zettel, !readonly}, // Public zettel {anonUser, publicZettel, publicZettel, notAuthNotReadonly}, {reader, publicZettel, publicZettel, notAuthNotReadonly}, {writer, publicZettel, publicZettel, !readonly}, {owner, publicZettel, publicZettel, !readonly}, {owner2, publicZettel, publicZettel, !readonly}, // Login zettel {anonUser, loginZettel, loginZettel, notAuthNotReadonly}, {reader, loginZettel, loginZettel, notAuthNotReadonly}, {writer, loginZettel, loginZettel, !readonly}, {owner, loginZettel, loginZettel, !readonly}, {owner2, loginZettel, loginZettel, !readonly}, // Owner zettel {anonUser, ownerZettel, ownerZettel, notAuthNotReadonly}, {reader, ownerZettel, ownerZettel, notAuthNotReadonly}, {writer, ownerZettel, ownerZettel, notAuthNotReadonly}, {owner, ownerZettel, ownerZettel, !readonly}, {owner2, ownerZettel, ownerZettel, !readonly}, // Expert zettel {anonUser, expertZettel, expertZettel, notAuthNotReadonly && expert}, {reader, expertZettel, expertZettel, notAuthNotReadonly && expert}, {writer, expertZettel, expertZettel, notAuthNotReadonly && expert}, {owner, expertZettel, expertZettel, !readonly && expert}, {owner2, expertZettel, expertZettel, !readonly && expert}, // Other user zettel {anonUser, userZettel, userZettel, notAuthNotReadonly}, {reader, userZettel, userZettel, notAuthNotReadonly}, {writer, userZettel, userZettel, notAuthNotReadonly}, {owner, userZettel, userZettel, !readonly}, {owner2, userZettel, userZettel, !readonly}, // Own user zettel {reader, reader, reader, !readonly}, {writer, writer, writer, !readonly}, {owner, owner, owner, !readonly}, {owner2, owner2, owner2, !readonly}, // Writer cannot change importand metadata of its own user zettel {writer, writer, writerNew, notAuthNotReadonly}, // No r/o zettel {anonUser, roFalse, roFalse, notAuthNotReadonly}, {reader, roFalse, roFalse, notAuthNotReadonly}, {writer, roFalse, roFalse, !readonly}, {owner, roFalse, roFalse, !readonly}, {owner2, roFalse, roFalse, !readonly}, // Reader r/o zettel {anonUser, roReader, roReader, false}, {reader, roReader, roReader, false}, {writer, roReader, roReader, !readonly}, {owner, roReader, roReader, !readonly}, {owner2, roReader, roReader, !readonly}, // Writer r/o zettel {anonUser, roWriter, roWriter, false}, {reader, roWriter, roWriter, false}, {writer, roWriter, roWriter, false}, {owner, roWriter, roWriter, !readonly}, {owner2, roWriter, roWriter, !readonly}, // Owner r/o zettel {anonUser, roOwner, roOwner, false}, {reader, roOwner, roOwner, false}, {writer, roOwner, roOwner, false}, {owner, roOwner, roOwner, false}, {owner2, roOwner, roOwner, false}, // r/o = true zettel {anonUser, roTrue, roTrue, false}, {reader, roTrue, roTrue, false}, {writer, roTrue, roTrue, false}, {owner, roTrue, roTrue, false}, {owner2, roTrue, roTrue, false}, } for _, tc := range testCases { t.Run("Write", func(tt *testing.T) { got := pol.CanWrite(tc.user, tc.old, tc.new) if tc.exp != got { tt.Errorf("exp=%v, but got=%v", tc.exp, got) } }) } } func testRename(t *testing.T, pol auth.Policy, withAuth, readonly, expert bool) { t.Helper() anonUser := newAnon() reader := newReader() writer := newWriter() owner := newOwner() owner2 := newOwner2() zettel := newZettel() expertZettel := newExpertZettel() roFalse := newRoFalseZettel() roTrue := newRoTrueZettel() roReader := newRoReaderZettel() roWriter := newRoWriterZettel() roOwner := newRoOwnerZettel() notAuthNotReadonly := !withAuth && !readonly testCases := []struct { user *meta.Meta meta *meta.Meta exp bool }{ // No meta {anonUser, nil, false}, {reader, nil, false}, {writer, nil, false}, {owner, nil, false}, {owner2, nil, false}, // Any zettel {anonUser, zettel, notAuthNotReadonly}, {reader, zettel, notAuthNotReadonly}, {writer, zettel, notAuthNotReadonly}, {owner, zettel, !readonly}, {owner2, zettel, !readonly}, // Expert zettel {anonUser, expertZettel, notAuthNotReadonly && expert}, {reader, expertZettel, notAuthNotReadonly && expert}, {writer, expertZettel, notAuthNotReadonly && expert}, {owner, expertZettel, !readonly && expert}, {owner2, expertZettel, !readonly && expert}, // No r/o zettel {anonUser, roFalse, notAuthNotReadonly}, {reader, roFalse, notAuthNotReadonly}, {writer, roFalse, notAuthNotReadonly}, {owner, roFalse, !readonly}, {owner2, roFalse, !readonly}, // Reader r/o zettel {anonUser, roReader, false}, {reader, roReader, false}, {writer, roReader, notAuthNotReadonly}, {owner, roReader, !readonly}, {owner2, roReader, !readonly}, // Writer r/o zettel {anonUser, roWriter, false}, {reader, roWriter, false}, {writer, roWriter, false}, {owner, roWriter, !readonly}, {owner2, roWriter, !readonly}, // Owner r/o zettel {anonUser, roOwner, false}, {reader, roOwner, false}, {writer, roOwner, false}, {owner, roOwner, false}, {owner2, roOwner, false}, // r/o = true zettel {anonUser, roTrue, false}, {reader, roTrue, false}, {writer, roTrue, false}, {owner, roTrue, false}, {owner2, roTrue, false}, } for _, tc := range testCases { t.Run("Rename", func(tt *testing.T) { got := pol.CanRename(tc.user, tc.meta) if tc.exp != got { tt.Errorf("exp=%v, but got=%v", tc.exp, got) } }) } } func testDelete(t *testing.T, pol auth.Policy, withAuth, readonly, expert bool) { t.Helper() anonUser := newAnon() reader := newReader() writer := newWriter() owner := newOwner() owner2 := newOwner2() zettel := newZettel() expertZettel := newExpertZettel() roFalse := newRoFalseZettel() roTrue := newRoTrueZettel() roReader := newRoReaderZettel() roWriter := newRoWriterZettel() roOwner := newRoOwnerZettel() notAuthNotReadonly := !withAuth && !readonly testCases := []struct { user *meta.Meta meta *meta.Meta exp bool }{ // No meta {anonUser, nil, false}, {reader, nil, false}, {writer, nil, false}, {owner, nil, false}, {owner2, nil, false}, // Any zettel {anonUser, zettel, notAuthNotReadonly}, {reader, zettel, notAuthNotReadonly}, {writer, zettel, notAuthNotReadonly}, {owner, zettel, !readonly}, {owner2, zettel, !readonly}, // Expert zettel {anonUser, expertZettel, notAuthNotReadonly && expert}, {reader, expertZettel, notAuthNotReadonly && expert}, {writer, expertZettel, notAuthNotReadonly && expert}, {owner, expertZettel, !readonly && expert}, {owner2, expertZettel, !readonly && expert}, // No r/o zettel {anonUser, roFalse, notAuthNotReadonly}, {reader, roFalse, notAuthNotReadonly}, {writer, roFalse, notAuthNotReadonly}, {owner, roFalse, !readonly}, {owner2, roFalse, !readonly}, // Reader r/o zettel {anonUser, roReader, false}, {reader, roReader, false}, {writer, roReader, notAuthNotReadonly}, {owner, roReader, !readonly}, {owner2, roReader, !readonly}, // Writer r/o zettel {anonUser, roWriter, false}, {reader, roWriter, false}, {writer, roWriter, false}, {owner, roWriter, !readonly}, {owner2, roWriter, !readonly}, // Owner r/o zettel {anonUser, roOwner, false}, {reader, roOwner, false}, {writer, roOwner, false}, {owner, roOwner, false}, {owner2, roOwner, false}, // r/o = true zettel {anonUser, roTrue, false}, {reader, roTrue, false}, {writer, roTrue, false}, {owner, roTrue, false}, {owner2, roTrue, false}, } for _, tc := range testCases { t.Run("Delete", func(tt *testing.T) { got := pol.CanDelete(tc.user, tc.meta) if tc.exp != got { tt.Errorf("exp=%v, but got=%v", tc.exp, got) } }) } } const ( 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 newReader() *meta.Meta { user := meta.New(readerZid) user.Set(meta.KeyTitle, "Reader") user.Set(meta.KeyRole, meta.ValueRoleUser) user.Set(meta.KeyUserRole, meta.ValueUserRoleReader) return user } func newWriter() *meta.Meta { user := meta.New(writerZid) user.Set(meta.KeyTitle, "Writer") user.Set(meta.KeyRole, meta.ValueRoleUser) user.Set(meta.KeyUserRole, meta.ValueUserRoleWriter) return user } func newOwner() *meta.Meta { user := meta.New(ownerZid) user.Set(meta.KeyTitle, "Owner") user.Set(meta.KeyRole, meta.ValueRoleUser) user.Set(meta.KeyUserRole, meta.ValueUserRoleOwner) return user } func newOwner2() *meta.Meta { user := meta.New(owner2Zid) user.Set(meta.KeyTitle, "Owner 2") user.Set(meta.KeyRole, meta.ValueRoleUser) user.Set(meta.KeyUserRole, meta.ValueUserRoleOwner) return user } func newZettel() *meta.Meta { m := meta.New(zettelZid) m.Set(meta.KeyTitle, "Any Zettel") return m } func newPublicZettel() *meta.Meta { m := meta.New(visZid) m.Set(meta.KeyTitle, "Public Zettel") m.Set(meta.KeyVisibility, meta.ValueVisibilityPublic) return m } func newLoginZettel() *meta.Meta { m := meta.New(visZid) m.Set(meta.KeyTitle, "Login Zettel") m.Set(meta.KeyVisibility, meta.ValueVisibilityLogin) return m } func newOwnerZettel() *meta.Meta { m := meta.New(visZid) m.Set(meta.KeyTitle, "Owner Zettel") m.Set(meta.KeyVisibility, meta.ValueVisibilityOwner) return m } func newExpertZettel() *meta.Meta { m := meta.New(visZid) m.Set(meta.KeyTitle, "Expert Zettel") m.Set(meta.KeyVisibility, meta.ValueVisibilityExpert) return m } func newRoFalseZettel() *meta.Meta { m := meta.New(zettelZid) m.Set(meta.KeyTitle, "No r/o Zettel") m.Set(meta.KeyReadOnly, "false") return m } func newRoTrueZettel() *meta.Meta { m := meta.New(zettelZid) m.Set(meta.KeyTitle, "A r/o Zettel") m.Set(meta.KeyReadOnly, "true") return m } func newRoReaderZettel() *meta.Meta { m := meta.New(zettelZid) m.Set(meta.KeyTitle, "Reader r/o Zettel") m.Set(meta.KeyReadOnly, meta.ValueUserRoleReader) return m } func newRoWriterZettel() *meta.Meta { m := meta.New(zettelZid) m.Set(meta.KeyTitle, "Writer r/o Zettel") m.Set(meta.KeyReadOnly, meta.ValueUserRoleWriter) return m } func newRoOwnerZettel() *meta.Meta { m := meta.New(zettelZid) m.Set(meta.KeyTitle, "Owner r/o Zettel") m.Set(meta.KeyReadOnly, meta.ValueUserRoleOwner) return m } func newUserZettel() *meta.Meta { m := meta.New(userZid) m.Set(meta.KeyTitle, "Any User") m.Set(meta.KeyRole, meta.ValueRoleUser) return m } |
Changes to auth/policy/readonly.go.
1 | //----------------------------------------------------------------------------- | | | < < < > | | | | | | < | 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-2021 Detlef Stern // // This file is part of zettelstore. // // Zettelstore is licensed under the latest version of the EUPL (European Union // Public License). Please see file LICENSE.txt for your rights and obligations // under this license. //----------------------------------------------------------------------------- // Package policy provides some interfaces and implementation for authorization policies. package policy import "zettelstore.de/z/domain/meta" type roPolicy struct{} func (p *roPolicy) CanCreate(user, newMeta *meta.Meta) bool { return false } func (p *roPolicy) CanRead(user, m *meta.Meta) bool { return true } func (p *roPolicy) CanWrite(user, oldMeta, newMeta *meta.Meta) bool { return false } func (p *roPolicy) CanRename(user, m *meta.Meta) bool { return false } func (p *roPolicy) CanDelete(user, m *meta.Meta) bool { return false } |
Deleted box/box.go.
|
| < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < |
Deleted box/compbox/compbox.go.
|
| < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < |
Deleted box/compbox/config.go.
|
| < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < |
Deleted box/compbox/keys.go.
|
| < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < |
Deleted box/compbox/log.go.
|
| < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < |
Deleted box/compbox/manager.go.
|
| < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < |
Deleted box/compbox/memory.go.
|
| < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < |
Deleted box/compbox/parser.go.
|
| < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < |
Deleted box/compbox/version.go.
|
| < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < |
Deleted box/constbox/base.css.
|
| < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < |
Deleted box/constbox/base.sxn.
|
| < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < |
Deleted box/constbox/constbox.go.
|
| < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < |
Deleted box/constbox/contributors.zettel.
|
| < < < < < < < < |
Deleted box/constbox/delete.sxn.
|
| < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < |
Deleted box/constbox/dependencies.zettel.
|
| < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < |
Deleted box/constbox/emoji_spin.gif.
cannot compute difference between binary files
Deleted box/constbox/error.sxn.
|
| < < < < < < < < < < < < < < < < < |
Deleted box/constbox/form.sxn.
|
| < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < |
Deleted box/constbox/home.zettel.
|
| < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < |
Deleted box/constbox/info.sxn.
|
| < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < |
Deleted box/constbox/license.txt.
|
| < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < |
Deleted box/constbox/listzettel.sxn.
|
| < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < |
Deleted box/constbox/login.sxn.
|
| < < < < < < < < < < < < < < < < < < < < < < < < < < < |
Deleted box/constbox/newtoc.zettel.
|
| < < < < < < |
Deleted box/constbox/prelude.sxn.
|
| < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < |
Deleted box/constbox/rename.sxn.
|
| < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < |
Deleted box/constbox/roleconfiguration.zettel.
|
| < < < < < < < < < < < < < < < < < < < < |
Deleted box/constbox/rolerole.zettel.
|
| < < < < < < < < < < |
Deleted box/constbox/roletag.zettel.
|
| < < < < < < |
Deleted box/constbox/rolezettel.zettel.
|
| < < < < < < < |
Deleted box/constbox/start.sxn.
|
| < < < < < < < < < < < < < < < < < |
Deleted box/constbox/wuicode.sxn.
|
| < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < |
Deleted box/constbox/zettel.sxn.
|
| < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < |
Deleted box/dirbox/dirbox.go.
|
| < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < |
Deleted box/dirbox/dirbox_test.go.
|
| < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < |
Deleted box/dirbox/service.go.
|
| < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < |
Deleted box/filebox/filebox.go.
|
| < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < |
Deleted box/filebox/zipbox.go.
|
| < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < |
Deleted box/helper.go.
|
| < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < |
Deleted box/manager/anteroom.go.
|
| < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < |
Deleted box/manager/anteroom_test.go.
|
| < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < |
Deleted box/manager/box.go.
|
| < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < |
Deleted box/manager/collect.go.
|
| < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < |
Deleted box/manager/enrich.go.
|
| < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < |
Deleted box/manager/indexer.go.
|
| < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < |
Deleted box/manager/manager.go.
|
| < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < |
Deleted box/manager/mapstore/mapstore.go.
|
| < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < |
Deleted box/manager/mapstore/refs.go.
|
| < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < |
Deleted box/manager/mapstore/refs_test.go.
|
| < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < |
Deleted box/manager/store/store.go.
|
| < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < |
Deleted box/manager/store/wordset.go.
|
| < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < |
Deleted box/manager/store/wordset_test.go.
|
| < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < |
Deleted box/manager/store/zettel.go.
|
| < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < |
Deleted box/membox/membox.go.
|
| < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < |
Deleted box/notify/directory.go.
|
| < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < |
Deleted box/notify/directory_test.go.
|
| < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < |
Deleted box/notify/entry.go.
|
| < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < |
Deleted box/notify/fsdir.go.
|
| < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < |
Deleted box/notify/helper.go.
|
| < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < |
Deleted box/notify/notify.go.
|
| < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < |
Deleted box/notify/simpledir.go.
|
| < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < |
Changes to cmd/cmd_file.go.
1 | //----------------------------------------------------------------------------- | | | < < < < | < | | | | | | | < | | | | | | | | | | | | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 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 | //----------------------------------------------------------------------------- // Copyright (c) 2020-2021 Detlef Stern // // This file is part of zettelstore. // // Zettelstore is licensed under the latest version of the EUPL (European Union // Public License). Please see file LICENSE.txt for your rights and obligations // under this license. //----------------------------------------------------------------------------- package cmd import ( "flag" "fmt" "io" "os" "zettelstore.de/z/domain" "zettelstore.de/z/domain/id" "zettelstore.de/z/domain/meta" "zettelstore.de/z/encoder" "zettelstore.de/z/input" "zettelstore.de/z/parser" ) // ---------- Subcommand: file ----------------------------------------------- func cmdFile(fs *flag.FlagSet, cfg *meta.Meta) (int, error) { format := fs.Lookup("t").Value.String() m, inp, err := getInput(fs.Args()) if m == nil { return 2, err } z := parser.ParseZettel( domain.Zettel{ Meta: m, Content: domain.NewContent(inp.Src[inp.Pos:]), }, m.GetDefault(meta.KeySyntax, meta.ValueSyntaxZmk), nil, ) enc := encoder.Create(format, &encoder.Environment{Lang: m.GetDefault(meta.KeyLang, meta.ValueLangEN)}) if enc == nil { fmt.Fprintf(os.Stderr, "Unknown format %q\n", format) return 2, nil } _, err = enc.WriteZettel(os.Stdout, z, format != "raw") if err != nil { return 2, err } fmt.Println() return 0, nil } func getInput(args []string) (*meta.Meta, *input.Input, error) { if len(args) < 1 { src, err := io.ReadAll(os.Stdin) if err != nil { return nil, nil, err } inp := input.NewInput(string(src)) m := meta.NewFromInput(id.New(true), inp) return m, inp, nil } src, err := os.ReadFile(args[0]) if err != nil { return nil, nil, err } inp := input.NewInput(string(src)) m := meta.NewFromInput(id.New(true), inp) if len(args) > 1 { src, err := os.ReadFile(args[1]) if err != nil { return nil, nil, err } inp = input.NewInput(string(src)) } return m, inp, nil } |
Changes to cmd/cmd_password.go.
1 | //----------------------------------------------------------------------------- | | | < < < | | | | | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 | //----------------------------------------------------------------------------- // Copyright (c) 2020 Detlef Stern // // This file is part of zettelstore. // // Zettelstore is licensed under the latest version of the EUPL (European Union // Public License). Please see file LICENSE.txt for your rights and obligations // under this license. //----------------------------------------------------------------------------- package cmd import ( "flag" "fmt" "os" "golang.org/x/term" "zettelstore.de/z/auth/cred" "zettelstore.de/z/domain/id" "zettelstore.de/z/domain/meta" ) // ---------- Subcommand: password ------------------------------------------- func cmdPassword(fs *flag.FlagSet, cfg *meta.Meta) (int, error) { if fs.NArg() == 0 { fmt.Fprintln(os.Stderr, "User name and user zettel identification missing") return 2, nil } if fs.NArg() == 1 { fmt.Fprintln(os.Stderr, "User zettel identification missing") return 2, nil |
︙ | ︙ | |||
59 60 61 62 63 64 65 | 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", meta.KeyCredential, hashedPassword, meta.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 | //----------------------------------------------------------------------------- | | | < < < < | | > > < | | < | | < | > > > | < > | > | | > | > > > > | < | < | < | < < < < < > | | | | | < < < < > > > | | | | < | | > | < < < < < < < < < < < < | | | | | > | | > > > > > > > | > | > | > | < | < < | | > | | < | < | > | | < < | < < < < | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 | //----------------------------------------------------------------------------- // Copyright (c) 2020-2021 Detlef Stern // // This file is part of zettelstore. // // Zettelstore is licensed under the latest version of the EUPL (European Union // Public License). Please see file LICENSE.txt for your rights and obligations // under this license. //----------------------------------------------------------------------------- package cmd import ( "flag" "net/http" "zettelstore.de/z/auth" "zettelstore.de/z/config" "zettelstore.de/z/domain/meta" "zettelstore.de/z/kernel" "zettelstore.de/z/place" "zettelstore.de/z/usecase" "zettelstore.de/z/web/adapter" "zettelstore.de/z/web/adapter/api" "zettelstore.de/z/web/adapter/webui" "zettelstore.de/z/web/server" ) // ---------- Subcommand: run ------------------------------------------------ func flgRun(fs *flag.FlagSet) { fs.String("c", defConfigfile, "configuration file") fs.Uint("a", 0, "port number kernel service (0=disable)") fs.Uint("p", 23123, "port number web service") fs.String("d", "", "zettel directory") fs.Bool("r", false, "system-wide read-only mode") fs.Bool("v", false, "verbose mode") fs.Bool("debug", false, "debug mode") } func withDebug(fs *flag.FlagSet) bool { dbg := fs.Lookup("debug") return dbg != nil && dbg.Value.String() == "true" } func runFunc(fs *flag.FlagSet, cfg *meta.Meta) (int, error) { exitCode, err := doRun(withDebug(fs)) kernel.Main.WaitForShutdown() return exitCode, err } func doRun(debug bool) (int, error) { kern := kernel.Main kern.SetDebug(debug) if err := kern.StartService(kernel.WebService); err != nil { return 1, err } return 0, nil } func setupRouting(webSrv server.Server, placeManager place.Manager, authManager auth.Manager, rtConfig config.Config) { protectedPlaceManager, authPolicy := authManager.PlaceWithPolicy(webSrv, placeManager, rtConfig) api := api.New(webSrv, authManager, authManager, webSrv, rtConfig) wui := webui.New(webSrv, authManager, rtConfig, authManager, placeManager, authPolicy) ucAuthenticate := usecase.NewAuthenticate(authManager, authManager, placeManager) ucCreateZettel := usecase.NewCreateZettel(rtConfig, protectedPlaceManager) ucGetMeta := usecase.NewGetMeta(protectedPlaceManager) ucGetZettel := usecase.NewGetZettel(protectedPlaceManager) ucParseZettel := usecase.NewParseZettel(rtConfig, ucGetZettel) ucListMeta := usecase.NewListMeta(protectedPlaceManager) ucListRoles := usecase.NewListRole(protectedPlaceManager) ucListTags := usecase.NewListTags(protectedPlaceManager) ucZettelContext := usecase.NewZettelContext(protectedPlaceManager) webSrv.Handle("/", wui.MakeGetRootHandler(protectedPlaceManager)) webSrv.AddListRoute('a', http.MethodGet, wui.MakeGetLoginHandler()) webSrv.AddListRoute('a', http.MethodPost, adapter.MakePostLoginHandler( api.MakePostLoginHandlerAPI(ucAuthenticate), wui.MakePostLoginHandlerHTML(ucAuthenticate))) webSrv.AddListRoute('a', http.MethodPut, api.MakeRenewAuthHandler()) webSrv.AddZettelRoute('a', http.MethodGet, wui.MakeGetLogoutHandler()) if !authManager.IsReadonly() { webSrv.AddZettelRoute('b', http.MethodGet, wui.MakeGetRenameZettelHandler(ucGetMeta)) webSrv.AddZettelRoute('b', http.MethodPost, wui.MakePostRenameZettelHandler( usecase.NewRenameZettel(protectedPlaceManager))) webSrv.AddZettelRoute('c', http.MethodGet, wui.MakeGetCopyZettelHandler( ucGetZettel, usecase.NewCopyZettel())) webSrv.AddZettelRoute('c', http.MethodPost, wui.MakePostCreateZettelHandler(ucCreateZettel)) webSrv.AddZettelRoute('d', http.MethodGet, wui.MakeGetDeleteZettelHandler(ucGetZettel)) webSrv.AddZettelRoute('d', http.MethodPost, wui.MakePostDeleteZettelHandler( usecase.NewDeleteZettel(protectedPlaceManager))) webSrv.AddZettelRoute('e', http.MethodGet, wui.MakeEditGetZettelHandler(ucGetZettel)) webSrv.AddZettelRoute('e', http.MethodPost, wui.MakeEditSetZettelHandler( usecase.NewUpdateZettel(protectedPlaceManager))) webSrv.AddZettelRoute('f', http.MethodGet, wui.MakeGetFolgeZettelHandler( ucGetZettel, usecase.NewFolgeZettel(rtConfig))) webSrv.AddZettelRoute('f', http.MethodPost, wui.MakePostCreateZettelHandler(ucCreateZettel)) webSrv.AddZettelRoute('g', http.MethodGet, wui.MakeGetNewZettelHandler( ucGetZettel, usecase.NewNewZettel())) webSrv.AddZettelRoute('g', http.MethodPost, wui.MakePostCreateZettelHandler(ucCreateZettel)) } webSrv.AddListRoute('f', http.MethodGet, wui.MakeSearchHandler( usecase.NewSearch(protectedPlaceManager), ucGetMeta, ucGetZettel)) webSrv.AddListRoute('h', http.MethodGet, wui.MakeListHTMLMetaHandler( ucListMeta, ucListRoles, ucListTags)) webSrv.AddZettelRoute('h', http.MethodGet, wui.MakeGetHTMLZettelHandler( ucParseZettel, ucGetMeta)) webSrv.AddZettelRoute('i', http.MethodGet, wui.MakeGetInfoHandler(ucParseZettel, ucGetMeta)) webSrv.AddZettelRoute('j', http.MethodGet, wui.MakeZettelContextHandler(ucZettelContext)) webSrv.AddZettelRoute('l', http.MethodGet, api.MakeGetLinksHandler(ucParseZettel)) webSrv.AddZettelRoute('o', http.MethodGet, api.MakeGetOrderHandler( usecase.NewZettelOrder(protectedPlaceManager, ucParseZettel))) webSrv.AddListRoute('r', http.MethodGet, api.MakeListRoleHandler(ucListRoles)) webSrv.AddListRoute('t', http.MethodGet, api.MakeListTagsHandler(ucListTags)) webSrv.AddZettelRoute('y', http.MethodGet, api.MakeZettelContextHandler(ucZettelContext)) webSrv.AddListRoute('z', http.MethodGet, api.MakeListMetaHandler( usecase.NewListMeta(protectedPlaceManager), ucGetMeta, ucParseZettel)) webSrv.AddZettelRoute('z', http.MethodGet, api.MakeGetZettelHandler( ucParseZettel, ucGetMeta)) if authManager.WithAuth() { webSrv.SetUserRetriever(usecase.NewGetUserByZid(placeManager)) } } |
Added cmd/cmd_run_simple.go.
> > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 | //----------------------------------------------------------------------------- // Copyright (c) 2020-2021 Detlef Stern // // This file is part of zettelstore. // // Zettelstore is licensed under the latest version of the EUPL (European Union // Public License). Please see file LICENSE.txt for your rights and obligations // under this license. //----------------------------------------------------------------------------- package cmd import ( "flag" "fmt" "os" "strings" "zettelstore.de/z/domain/meta" "zettelstore.de/z/kernel" ) func flgSimpleRun(fs *flag.FlagSet) { fs.String("d", "", "zettel directory") } func runSimpleFunc(fs *flag.FlagSet, cfg *meta.Meta) (int, error) { kern := kernel.Main listenAddr := kern.GetConfig(kernel.WebService, kernel.WebListenAddress).(string) exitCode, err := doRun(false) if idx := strings.LastIndexByte(listenAddr, ':'); idx >= 0 { kern.Log() kern.Log("--------------------------") kern.Log("Open your browser and enter the following URL:") kern.Log() kern.Log(fmt.Sprintf(" http://localhost%v", listenAddr[idx:])) kern.Log() } kern.WaitForShutdown() return exitCode, err } // runSimple is called, when the user just starts the software via a double click // or via a simple call ``./zettelstore`` on the command line. func runSimple() int { dir := "./zettel" if err := os.MkdirAll(dir, 0750); err != nil { fmt.Fprintf(os.Stderr, "Unable to create zettel directory %q (%s)\n", dir, err) os.Exit(1) } return executeCommand("run-simple", "-d", dir) } |
Changes to cmd/command.go.
1 | //----------------------------------------------------------------------------- | | | < < < > < | | | < | | < | | > | < < | | | > > > > > > > | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 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-2021 Detlef Stern // // This file is part of zettelstore. // // Zettelstore is licensed under the latest version of the EUPL (European Union // Public License). Please see file LICENSE.txt for your rights and obligations // under this license. //----------------------------------------------------------------------------- package cmd import ( "flag" "sort" "zettelstore.de/z/domain/meta" ) // Command stores information about commands / sub-commands. type Command struct { Name string // command name as it appears on the command line Func CommandFunc // function that executes a command Places bool // if true then places will be set up Header bool // Print a heading on startup Flags func(*flag.FlagSet) // function to set up flag.FlagSet flags *flag.FlagSet // flags that belong to the command } // CommandFunc is the function that executes the command. // It accepts the parsed command line parameters. // It returns the exit code and an error. type CommandFunc func(*flag.FlagSet, *meta.Meta) (int, error) // GetFlags return the flag.FlagSet defined for the command. func (c *Command) GetFlags() *flag.FlagSet { return c.flags } var commands = make(map[string]Command) // RegisterCommand registers the given command. func RegisterCommand(cmd Command) { if cmd.Name == "" || cmd.Func == nil { panic("Required command values missing") } if _, ok := commands[cmd.Name]; ok { panic("Command already registered: " + cmd.Name) } cmd.flags = flag.NewFlagSet(cmd.Name, flag.ExitOnError) if cmd.Flags != nil { cmd.Flags(cmd.flags) } commands[cmd.Name] = cmd } // Get returns the command identified by the given name and a bool to signal success. 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 { result := make([]string, 0, len(commands)) for name := range commands { result = append(result, name) } sort.Strings(result) return result } |
Added cmd/fd_limit.go.
> > > > > > > > > > > > > > > | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 | //----------------------------------------------------------------------------- // Copyright (c) 2021 Detlef Stern // // This file is part of zettelstore. // // Zettelstore is licensed under the latest version of the EUPL (European Union // Public License). Please see file LICENSE.txt for your rights and obligations // under this license. //----------------------------------------------------------------------------- // +build !darwin package cmd func raiseFdLimit() error { return nil } |
Added cmd/fd_limit_raise.go.
> > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 | //----------------------------------------------------------------------------- // Copyright (c) 2021 Detlef Stern // // This file is part of zettelstore. // // Zettelstore is licensed under the latest version of the EUPL (European Union // Public License). Please see file LICENSE.txt for your rights and obligations // under this license. //----------------------------------------------------------------------------- // +build darwin package cmd import ( "log" "syscall" ) const minFiles = 1048576 func raiseFdLimit() error { var rLimit syscall.Rlimit err := syscall.Getrlimit(syscall.RLIMIT_NOFILE, &rLimit) if err != nil { return err } if rLimit.Cur >= minFiles { return nil } rLimit.Cur = minFiles if rLimit.Cur > rLimit.Max { rLimit.Cur = rLimit.Max } err = syscall.Setrlimit(syscall.RLIMIT_NOFILE, &rLimit) if err != nil { return err } err = syscall.Getrlimit(syscall.RLIMIT_NOFILE, &rLimit) if err != nil { return err } if rLimit.Cur < minFiles { log.Printf("Make sure you have no more than %d files in all your places if you enabled notification\n", rLimit.Cur) } return nil } |
Changes to cmd/main.go.
1 | //----------------------------------------------------------------------------- | | | < < < | < < < < | | | | | | | | | > > | | | | | | < | | | | < < | < < < | | | > | < < | < | < | | < | < < < < < < < < < < < | | > | > > | > < | < < < < | | | | > | | < > | < < < | | < | < < | | | | | | | | < < < < < | < < < | | | | | | | | | > | | | | | < < < < | < | | < < < | | | | | < > > | > > > > > > > > > | | | | | > > > > > > | > > > > > > > > > > > | > > | | < < < < < < < < < < < < < < < < < | < < | < < < < < < < < < < < < < < | | | < < < < < < < < < < < < < < < < < | < < < < < | < < < | | < < < < < | | | | | | < < < < < | < < < < < < < < < < < < | < < < < < < < < < < < < < | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 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 | //----------------------------------------------------------------------------- // Copyright (c) 2020-2021 Detlef Stern // // This file is part of zettelstore. // // Zettelstore is licensed under the latest version of the EUPL (European Union // Public License). Please see file LICENSE.txt for your rights and obligations // under this license. //----------------------------------------------------------------------------- package cmd import ( "errors" "flag" "fmt" "net" "net/url" "os" "strconv" "strings" "zettelstore.de/z/auth" "zettelstore.de/z/auth/impl" "zettelstore.de/z/config" "zettelstore.de/z/domain/id" "zettelstore.de/z/domain/meta" "zettelstore.de/z/input" "zettelstore.de/z/kernel" "zettelstore.de/z/place" "zettelstore.de/z/place/manager" "zettelstore.de/z/place/progplace" "zettelstore.de/z/web/server" ) const ( defConfigfile = ".zscfg" ) func init() { RegisterCommand(Command{ Name: "help", Func: func(*flag.FlagSet, *meta.Meta) (int, error) { fmt.Println("Available commands:") for _, name := range List() { fmt.Printf("- %q\n", name) } return 0, nil }, }) RegisterCommand(Command{ Name: "version", Func: func(*flag.FlagSet, *meta.Meta) (int, error) { return 0, nil }, Header: true, }) RegisterCommand(Command{ Name: "run", Func: runFunc, Places: true, Header: true, Flags: flgRun, }) RegisterCommand(Command{ Name: "run-simple", Func: runSimpleFunc, Places: true, Header: true, Flags: flgSimpleRun, }) RegisterCommand(Command{ Name: "file", Func: cmdFile, Flags: func(fs *flag.FlagSet) { fs.String("t", "html", "target output format") }, }) RegisterCommand(Command{ Name: "password", Func: cmdPassword, }) } func readConfig(fs *flag.FlagSet) (cfg *meta.Meta) { var configFile string if configFlag := fs.Lookup("c"); configFlag != nil { configFile = configFlag.Value.String() } else { configFile = defConfigfile } content, err := os.ReadFile(configFile) if err != nil { return meta.New(id.Invalid) } return meta.NewFromInput(id.Invalid, input.NewInput(string(content))) } func getConfig(fs *flag.FlagSet) *meta.Meta { cfg := readConfig(fs) fs.Visit(func(flg *flag.Flag) { switch flg.Name { case "p": if portStr, err := parsePort(flg.Value.String()); err == nil { cfg.Set(keyListenAddr, net.JoinHostPort("127.0.0.1", portStr)) } case "a": if portStr, err := parsePort(flg.Value.String()); err == nil { cfg.Set(keyAdminPort, portStr) } case "d": val := flg.Value.String() if strings.HasPrefix(val, "/") { val = "dir://" + val } else { val = "dir:" + val } cfg.Set(keyPlaceOneURI, val) case "r": cfg.Set(keyReadOnly, flg.Value.String()) case "v": cfg.Set(keyVerbose, flg.Value.String()) } }) return cfg } func parsePort(s string) (string, error) { port, err := net.LookupPort("tcp", s) if err != nil { fmt.Fprintf(os.Stderr, "Wrong port specification: %q", s) return "", err } return strconv.Itoa(port), nil } const ( keyAdminPort = "admin-port" keyDefaultDirPlaceType = "default-dir-place-type" keyInsecureCookie = "insecure-cookie" keyListenAddr = "listen-addr" keyOwner = "owner" keyPersistentCookie = "persistent-cookie" keyPlaceOneURI = kernel.PlaceURIs + "1" keyReadOnly = "read-only-mode" keyTokenLifetimeHTML = "token-lifetime-html" keyTokenLifetimeAPI = "token-lifetime-api" keyURLPrefix = "url-prefix" keyVerbose = "verbose" ) func setServiceConfig(cfg *meta.Meta) error { ok := setConfigValue(true, kernel.CoreService, kernel.CoreVerbose, cfg.GetBool(keyVerbose)) if val, found := cfg.Get(keyAdminPort); found { ok = setConfigValue(ok, kernel.CoreService, kernel.CorePort, val) } ok = setConfigValue(ok, kernel.AuthService, kernel.AuthOwner, cfg.GetDefault(keyOwner, "")) ok = setConfigValue(ok, kernel.AuthService, kernel.AuthReadonly, cfg.GetBool(keyReadOnly)) ok = setConfigValue( ok, kernel.PlaceService, kernel.PlaceDefaultDirType, cfg.GetDefault(keyDefaultDirPlaceType, kernel.PlaceDirTypeNotify)) ok = setConfigValue(ok, kernel.PlaceService, kernel.PlaceURIs+"1", "dir:./zettel") format := kernel.PlaceURIs + "%v" for i := 1; ; i++ { key := fmt.Sprintf(format, i) val, found := cfg.Get(key) if !found { break } ok = setConfigValue(ok, kernel.PlaceService, key, val) } ok = setConfigValue( ok, kernel.WebService, kernel.WebListenAddress, cfg.GetDefault(keyListenAddr, "127.0.0.1:23123")) ok = setConfigValue(ok, kernel.WebService, kernel.WebURLPrefix, cfg.GetDefault(keyURLPrefix, "/")) ok = setConfigValue(ok, kernel.WebService, kernel.WebSecureCookie, !cfg.GetBool(keyInsecureCookie)) ok = setConfigValue(ok, kernel.WebService, kernel.WebPersistentCookie, cfg.GetBool(keyPersistentCookie)) ok = setConfigValue( ok, kernel.WebService, kernel.WebTokenLifetimeAPI, cfg.GetDefault(keyTokenLifetimeAPI, "")) ok = setConfigValue( ok, kernel.WebService, kernel.WebTokenLifetimeHTML, cfg.GetDefault(keyTokenLifetimeHTML, "")) if !ok { return errors.New("unable to set configuration") } return nil } func setConfigValue(ok bool, subsys kernel.Service, key string, val interface{}) bool { done := kernel.Main.SetConfig(subsys, key, fmt.Sprintf("%v", val)) if !done { kernel.Main.Log("unable to set configuration:", key, val) } return ok && done } func setupOperations(cfg *meta.Meta, withPlaces bool) { var createManager kernel.CreatePlaceManagerFunc if withPlaces { err := raiseFdLimit() if err != nil { srvm := kernel.Main srvm.Log("Raising some limitions did not work:", err) srvm.Log("Prepare to encounter errors. Most of them can be mitigated. See the manual for details") srvm.SetConfig(kernel.PlaceService, kernel.PlaceDefaultDirType, kernel.PlaceDirTypeSimple) } createManager = func(placeURIs []*url.URL, authManager auth.Manager, rtConfig config.Config) (place.Manager, error) { progplace.Setup(cfg) return manager.New(placeURIs, authManager, rtConfig) } } else { createManager = func([]*url.URL, auth.Manager, config.Config) (place.Manager, error) { return nil, nil } } kernel.Main.SetCreators( func(readonly bool, owner id.Zid) (auth.Manager, error) { return impl.New(readonly, owner, cfg.GetDefault("secret", "")), nil }, createManager, func(srv server.Server, plMgr place.Manager, authMgr auth.Manager, rtConfig config.Config) error { setupRouting(srv, plMgr, authMgr, rtConfig) return nil }, ) } func executeCommand(name string, args ...string) int { command, ok := Get(name) if !ok { 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 err := setServiceConfig(cfg); err != nil { fmt.Fprintf(os.Stderr, "%s: %v\n", name, err) return 2 } setupOperations(cfg, command.Places) kernel.Main.Start(command.Header) exitCode, err := command.Func(fs, cfg) if err != nil { fmt.Fprintf(os.Stderr, "%s: %v\n", name, err) } kernel.Main.Shutdown(true) return exitCode } // Main is the real entrypoint of the zettelstore. func Main(progName, buildVersion string) { kernel.Main.SetConfig(kernel.CoreService, kernel.CoreProgname, progName) kernel.Main.SetConfig(kernel.CoreService, kernel.CoreVersion, buildVersion) var exitCode int if len(os.Args) <= 1 { exitCode = runSimple() } else { exitCode = executeCommand(os.Args[1], os.Args[2:]...) } if exitCode != 0 { os.Exit(exitCode) } } |
Changes to cmd/register.go.
1 | //----------------------------------------------------------------------------- | | | < < < < < < < < | | | < > > > > > | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 | //----------------------------------------------------------------------------- // Copyright (c) 2020-2021 Detlef Stern // // This file is part of zettelstore. // // Zettelstore is licensed under the latest version of the EUPL (European Union // Public License). Please see file LICENSE.txt for your rights and obligations // under this license. //----------------------------------------------------------------------------- // Package cmd provides command generic functions. package cmd // Mention all needed encoders, parsers and stores to have them registered. import ( _ "zettelstore.de/z/encoder/htmlenc" // Allow to use HTML encoder. _ "zettelstore.de/z/encoder/jsonenc" // Allow to use JSON encoder. _ "zettelstore.de/z/encoder/nativeenc" // Allow to use native encoder. _ "zettelstore.de/z/encoder/rawenc" // Allow to use raw encoder. _ "zettelstore.de/z/encoder/textenc" // Allow to use text encoder. _ "zettelstore.de/z/encoder/zmkenc" // Allow to use zmk encoder. _ "zettelstore.de/z/kernel/impl" // Allow kernel implementation to create itself _ "zettelstore.de/z/parser/blob" // Allow to use BLOB parser. _ "zettelstore.de/z/parser/markdown" // Allow to use markdown parser. _ "zettelstore.de/z/parser/none" // Allow to use none parser. _ "zettelstore.de/z/parser/plain" // Allow to use plain parser. _ "zettelstore.de/z/parser/zettelmark" // Allow to use zettelmark parser. _ "zettelstore.de/z/place/constplace" // Allow to use global internal place. _ "zettelstore.de/z/place/dirplace" // Allow to use directory place. _ "zettelstore.de/z/place/fileplace" // Allow to use file place. _ "zettelstore.de/z/place/memplace" // Allow to use memory place. _ "zettelstore.de/z/place/progplace" // Allow to use computed place. ) |
Changes to cmd/zettelstore/main.go.
1 | //----------------------------------------------------------------------------- | | | < < < < < < | < | < | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 | //----------------------------------------------------------------------------- // Copyright (c) 2020-2021 Detlef Stern // // This file is part of zettelstore. // // Zettelstore is licensed under the latest version of the EUPL (European Union // Public License). Please see file LICENSE.txt for your rights and obligations // under this license. //----------------------------------------------------------------------------- // Package main is the starting point for the zettelstore command. package main import "zettelstore.de/z/cmd" // Version variable. Will be filled by build process. var version string = "" func main() { cmd.Main("Zettelstore", version) } |
Changes to collect/collect.go.
1 | //----------------------------------------------------------------------------- | | | < < < > | > | | | > | | > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > | | | > > | > > | > | | > | > > > > > > | > | | | | > > | > > > > > > > > | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 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-2021 Detlef Stern // // This file is part of zettelstore. // // Zettelstore is licensed under the latest version of the EUPL (European Union // Public License). Please see file LICENSE.txt for your rights and obligations // under this license. //----------------------------------------------------------------------------- // Package collect provides functions to collect items from a syntax tree. package collect import ( "zettelstore.de/z/ast" ) // Summary stores the relevant parts of the syntax tree type Summary struct { Links []*ast.Reference // list of all referenced links Images []*ast.Reference // list of all referenced images Cites []*ast.CiteNode // list of all referenced citations } // References returns all references mentioned in the given zettel. This also // includes references to images. func References(zn *ast.ZettelNode) Summary { lv := linkVisitor{} ast.NewTopDownTraverser(&lv).VisitBlockSlice(zn.Ast) return lv.summary } type linkVisitor struct { summary Summary } // VisitVerbatim does nothing. func (lv *linkVisitor) VisitVerbatim(vn *ast.VerbatimNode) {} // VisitRegion does nothing. func (lv *linkVisitor) VisitRegion(rn *ast.RegionNode) {} // VisitHeading does nothing. func (lv *linkVisitor) VisitHeading(hn *ast.HeadingNode) {} // VisitHRule does nothing. func (lv *linkVisitor) VisitHRule(hn *ast.HRuleNode) {} // VisitList does nothing. func (lv *linkVisitor) VisitNestedList(ln *ast.NestedListNode) {} // VisitDescriptionList does nothing. func (lv *linkVisitor) VisitDescriptionList(dn *ast.DescriptionListNode) {} // VisitPara does nothing. func (lv *linkVisitor) VisitPara(pn *ast.ParaNode) {} // VisitTable does nothing. func (lv *linkVisitor) VisitTable(tn *ast.TableNode) {} // VisitBLOB does nothing. func (lv *linkVisitor) VisitBLOB(bn *ast.BLOBNode) {} // VisitText does nothing. func (lv *linkVisitor) VisitText(tn *ast.TextNode) {} // VisitTag does nothing. func (lv *linkVisitor) VisitTag(tn *ast.TagNode) {} // VisitSpace does nothing. func (lv *linkVisitor) VisitSpace(sn *ast.SpaceNode) {} // VisitBreak does nothing. func (lv *linkVisitor) VisitBreak(bn *ast.BreakNode) {} // VisitLink collects the given link as a reference. func (lv *linkVisitor) VisitLink(ln *ast.LinkNode) { lv.summary.Links = append(lv.summary.Links, ln.Ref) } // VisitImage collects the image links as a reference. func (lv *linkVisitor) VisitImage(in *ast.ImageNode) { if in.Ref != nil { lv.summary.Images = append(lv.summary.Images, in.Ref) } } // VisitCite collects the citation. func (lv *linkVisitor) VisitCite(cn *ast.CiteNode) { lv.summary.Cites = append(lv.summary.Cites, cn) } // VisitFootnote does nothing. func (lv *linkVisitor) VisitFootnote(fn *ast.FootnoteNode) {} // VisitMark does nothing. func (lv *linkVisitor) VisitMark(mn *ast.MarkNode) {} // VisitFormat does nothing. func (lv *linkVisitor) VisitFormat(fn *ast.FormatNode) {} // VisitLiteral does nothing. func (lv *linkVisitor) VisitLiteral(ln *ast.LiteralNode) {} |
Changes to collect/collect_test.go.
1 | //----------------------------------------------------------------------------- | | | < < < | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 | //----------------------------------------------------------------------------- // Copyright (c) 2020-2021 Detlef Stern // // This file is part of zettelstore. // // Zettelstore is licensed under the latest version of the EUPL (European Union // Public License). Please see file LICENSE.txt for your rights and obligations // under this license. //----------------------------------------------------------------------------- // Package collect_test provides some unit test for collectors. package collect_test import ( "testing" |
︙ | ︙ | |||
26 27 28 29 30 31 32 | if !r.IsValid() { panic(s) } return r } func TestLinks(t *testing.T) { | < | | > > > | > > | | | < | > > > > > > | | | 23 24 25 26 27 28 29 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 | if !r.IsValid() { panic(s) } return r } func TestLinks(t *testing.T) { zn := &ast.ZettelNode{} summary := collect.References(zn) if summary.Links != nil || summary.Images != nil { t.Error("No links/images expected, but got:", summary.Links, "and", summary.Images) } intNode := &ast.LinkNode{Ref: parseRef("01234567890123")} para := &ast.ParaNode{ Inlines: ast.InlineSlice{ intNode, &ast.LinkNode{Ref: parseRef("https://zettelstore.de/z")}, }, } zn.Ast = ast.BlockSlice{para} summary = collect.References(zn) if summary.Links == nil || summary.Images != nil { t.Error("Links expected, and no images, but got:", summary.Links, "and", summary.Images) } para.Inlines = append(para.Inlines, intNode) summary = collect.References(zn) if cnt := len(summary.Links); cnt != 3 { t.Error("Link count does not work. Expected: 3, got", summary.Links) } } func TestImage(t *testing.T) { zn := &ast.ZettelNode{ Ast: ast.BlockSlice{ &ast.ParaNode{ Inlines: ast.InlineSlice{ &ast.ImageNode{Ref: parseRef("12345678901234")}, }, }, }, } summary := collect.References(zn) if summary.Images == nil { t.Error("Only image expected, but got: ", summary.Images) } } |
Changes to collect/order.go.
1 | //----------------------------------------------------------------------------- | | | < < < | < < < | | | | | > | | | < < | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 | //----------------------------------------------------------------------------- // Copyright (c) 2021 Detlef Stern // // This file is part of zettelstore. // // Zettelstore is licensed under the latest version of the EUPL (European Union // Public License). Please see file LICENSE.txt for your rights and obligations // under this license. //----------------------------------------------------------------------------- // Package 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 { if ln, ok := bn.(*ast.NestedListNode); ok { switch ln.Code { case ast.NestedListOrdered, ast.NestedListUnordered: for _, is := range ln.Items { if ref := firstItemZettelReference(is); ref != nil { result = append(result, ref) } } } } } return result } 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(ins ast.InlineSlice) (result *ast.Reference) { for _, inl := range ins { switch in := inl.(type) { case *ast.LinkNode: if ref := in.Ref; ref.IsZettel() { return ref } result = firstInlineZettelReference(in.Inlines) case *ast.ImageNode: result = firstInlineZettelReference(in.Inlines) case *ast.CiteNode: result = firstInlineZettelReference(in.Inlines) case *ast.FootnoteNode: // Ignore references in footnotes continue case *ast.FormatNode: |
︙ | ︙ |
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 | //----------------------------------------------------------------------------- // Copyright (c) 2020-2021 Detlef Stern // // This file is part of zettelstore. // // Zettelstore is licensed under the latest version of the EUPL (European Union // Public License). Please see file LICENSE.txt for your rights and obligations // under this license. //----------------------------------------------------------------------------- // Package collect provides functions to collect items from a syntax tree. package collect import "zettelstore.de/z/ast" // DivideReferences divides the given list of rederences into zettel, local, and external References. func DivideReferences(all []*ast.Reference) (zettel, local, external []*ast.Reference) { if len(all) == 0 { return nil, nil, nil } mapZettel := make(map[string]bool) mapLocal := make(map[string]bool) mapExternal := make(map[string]bool) for _, ref := range all { if ref.State == ast.RefStateSelf { continue } if ref.IsZettel() { zettel = appendRefToList(zettel, mapZettel, ref) } else if ref.IsExternal() { external = appendRefToList(external, mapExternal, ref) } else { local = appendRefToList(local, mapLocal, ref) } } return zettel, local, external } func appendRefToList(reflist []*ast.Reference, refSet map[string]bool, ref *ast.Reference) []*ast.Reference { s := ref.String() if _, ok := refSet[s]; !ok { reflist = append(reflist, ref) refSet[s] = true } return reflist } |
Changes to config/config.go.
1 | //----------------------------------------------------------------------------- | | | < < < < < | < | < < < < < < < < < > > | > > | > | > > | > | | | | | > > > > > > > > > > > < < < | | | > > | | < < | < < < > | > > > > > | < < < | > | > | < > > | | | > | < < > | < < < < < | | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 | //----------------------------------------------------------------------------- // Copyright (c) 2020-2021 Detlef Stern // // This file is part of zettelstore. // // Zettelstore is licensed under the latest version of the EUPL (European Union // Public License). Please see file LICENSE.txt for your rights and obligations // under this license. //----------------------------------------------------------------------------- // Package config provides functions to retrieve runtime configuration data. package config import ( "zettelstore.de/z/domain/id" "zettelstore.de/z/domain/meta" ) // Config allows to retrieve all defined configuration values that can be changed during runtime. type Config interface { AuthConfig // AddDefaultValues enriches the given meta data with its default values. AddDefaultValues(m *meta.Meta) *meta.Meta // GetDefaultTitle returns the current value of the "default-title" key. GetDefaultTitle() string // GetDefaultRole returns the current value of the "default-role" key. GetDefaultRole() string // GetDefaultSyntax returns the current value of the "default-syntax" key. GetDefaultSyntax() string // GetDefaultLang returns the current value of the "default-lang" key. GetDefaultLang() string // GetSiteName returns the current value of the "site-name" key. GetSiteName() string // GetHomeZettel returns the value of the "home-zettel" key. GetHomeZettel() id.Zid // GetDefaultVisibility returns the default value for zettel visibility. GetDefaultVisibility() meta.Visibility // GetYAMLHeader returns the current value of the "yaml-header" key. GetYAMLHeader() bool // GetZettelFileSyntax returns the current value of the "zettel-file-syntax" key. GetZettelFileSyntax() []string // GetMarkerExternal returns the current value of the "marker-external" key. GetMarkerExternal() string // GetFooterHTML returns HTML code that should be embedded into the footer // of each WebUI page. GetFooterHTML() string // GetListPageSize returns the maximum length of a list to be returned in WebUI. // A value less or equal to zero signals no limit. GetListPageSize() int } // AuthConfig are relevant configuration values for authentication. type AuthConfig interface { // 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 } // GetTitle returns the value of the "title" key of the given meta. If there // is no such value, GetDefaultTitle is returned. func GetTitle(m *meta.Meta, cfg Config) string { if val, ok := m.Get(meta.KeyTitle); ok { return val } return cfg.GetDefaultTitle() } // GetRole returns the value of the "role" key of the given meta. If there // is no such value, GetDefaultRole is returned. func GetRole(m *meta.Meta, cfg Config) string { if val, ok := m.Get(meta.KeyRole); ok { return val } return cfg.GetDefaultRole() } // GetSyntax returns the value of the "syntax" key of the given meta. If there // is no such value, GetDefaultSyntax is returned. func GetSyntax(m *meta.Meta, cfg Config) string { if val, ok := m.Get(meta.KeySyntax); ok { return val } return cfg.GetDefaultSyntax() } // GetLang returns the value of the "lang" key of the given meta. If there is // no such value, GetDefaultLang is returned. func GetLang(m *meta.Meta, cfg Config) string { if val, ok := m.Get(meta.KeyLang); ok { return val } return cfg.GetDefaultLang() } |
Deleted docs/development/00010000000000.zettel.
|
| < < < < < < < < < < < |
Deleted docs/development/20210916193200.zettel.
|
| < < < < < < < < < < < < < < < < < < < < < < < < < < < < |
Deleted docs/development/20210916194900.zettel.
|
| < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < |
Deleted docs/development/20221026184300.zettel.
|
| < < < < < < < < < < < < < < |
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 13 | id: 00000000000100 title: Zettelstore Runtime Configuration role: configuration syntax: none default-copyright: (c) 2020-2021 by Detlef Stern <ds@zettelstore.de> default-license: EUPL-1.2-or-later default-visibility: public footer-html: <hr><p><a href="/home/doc/trunk/www/impri.wiki">Imprint / Privacy</a></p> home-zettel: 00001000000000 no-index: true site-name: Zettelstore Manual visibility: owner |
Deleted docs/manual/00000000025001.
|
| < < < < < < < |
Deleted docs/manual/00000000025001.css.
|
| < < |
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 | id: 00001000000000 title: Zettelstore Manual role: manual tags: #manual #zettelstore syntax: zmk * [[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]] * Troubleshooting * Frequently asked questions Licensed under the EUPL-1.2-or-later. |
Deleted docs/manual/00001000000001.zettel.
|
| < < < < < < < < |
Deleted docs/manual/00001000000100.zettel.
|
| < < < < < < < < |
Changes to docs/manual/00001002000000.zettel.
1 2 | id: 00001002000000 title: Design goals for the 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 | id: 00001002000000 title: Design goals for the Zettelstore tags: #design #goal #manual #zettelstore syntax: zmk role: manual Zettelstore supports the following design goals: ; Longevity of stored notes / zettel : Every zettel you create should be readable without the help of any tool, even without Zettelstore. : It should be not hard to write other software that works with your zettel. ; Single user : All zettel belong to you, only to you. Zettelstore provides its services only to one person: you. If your device is securely configured, there should be no risk that others are able to read or update your zettel. : If you want, you can customize Zettelstore in a way that some specific or all persons are able to read some of your zettel. ; Ease of installation : If you want to use the Zettelstore software, all you need is to copy the executable to an appropriate place and start working. : Upgrading the software is done just by replacing the executable with a newer one. ; Ease of operation : There is only one executable for Zettelstore and one directory, where your zettel are placed. : If you decide to use multiple directories, you are free to configure Zettelstore appropriately. ; Multiple modes of operation : You can use Zettelstore as a standalone software on your device, but you are not restricted to it. : You can install the software on a central server, or you can install it on all your devices with no restrictions how to synchronize your zettel. ; Multiple user interfaces : Zettelstore provides a default web-based user interface. Anybody can provide alternative user interfaces, e.g. for special purposes. ; Simple service : The purpose of Zettelstore is to safely store your zettel and to provide some initial relations between them. : External software can be written to deeply analyze your zettel and the structures they form. |
Changes to docs/manual/00001003000000.zettel.
1 2 3 4 5 | 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 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 | id: 00001003000000 title: Installation of the Zettelstore software role: manual tags: #installation #manual #zettelstore syntax: zmk === The curious user You just want to check out the Zettelstore software * Grab the appropriate executable and copy it into any directory * Start the Zettelstore software, e.g. with a double click * A sub-directory ""zettel"" will be created in the directory where you placed the executable. It will contain your future zettel. * Open the URI [[http://localhost:23123]] with your web browser. It will present you a mostly empty Zettelstore. There will be a zettel titled ""[[Home|00010000000000]]"" that contains some helpful information. * Please read the instructions for the web-based user interface and learn about the various ways to write zettel. * 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. * Grab the appropriate executable and copy it into the appropriate directory * ... === The server administrator You want to provide a shared Zettelstore that can be used from your various devices. Installing Zettelstore as a Linux service is not that hard. Grab the appropriate executable and copy it into the appropriate directory: ```sh # sudo mv zettelstore /usr/local/bin/zettelstore ``` Create a group named ''zettelstore'': ```sh # sudo groupadd --system zettelstore ``` Create a system user of that group, named ''zettelstore'', with a home folder: ```sh # sudo useradd --system --gid zettelstore \ --create-home --home-dir /var/lib/zettelstore \ --shell /usr/sbin/nologin \ --comment "Zettelstore server" \ zettelstore ``` Create a systemd service file and place it into ''/etc/systemd/system/zettelstore.service'': ```ini [Unit] Description=Zettelstore After=network.target [Service] Type=simple User=zettelstore Group=zettelstore ExecStart=/usr/local/bin/zettelstore run -d /var/lib/zettelstore WorkingDirectory=/var/lib/zettelstore [Install] WantedBy=multi-user.target ``` Double-check everything. Now you can enable and start the zettelstore as a service: ```sh # sudo systemctl daemon-reload # sudo systemctl enable zettelstore # sudo systemctl start zettelstore ``` Use the commands ``systemctl``{=sh} and ``journalctl``{=sh} to manage the service, e.g.: ```sh # sudo systemctl status zettelstore # verify that it is running # sudo journalctl -u zettelstore # obtain the output of the running zettelstore ``` |
Deleted docs/manual/00001003300000.zettel.
|
| < < < < < < < < < < < < < < < < < < < < < < < < < < |
Deleted docs/manual/00001003305000.zettel.
|
| < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < |
Deleted docs/manual/00001003305102.png.
cannot compute difference between binary files
Deleted docs/manual/00001003305104.png.
cannot compute difference between binary files
Deleted docs/manual/00001003305106.png.
cannot compute difference between binary files
Deleted docs/manual/00001003305108.png.
cannot compute difference between binary files
Deleted docs/manual/00001003305110.png.
cannot compute difference between binary files
Deleted docs/manual/00001003305112.png.
cannot compute difference between binary files
Deleted docs/manual/00001003305114.png.
cannot compute difference between binary files
Deleted docs/manual/00001003305116.png.
cannot compute difference between binary files
Deleted docs/manual/00001003305118.png.
cannot compute difference between binary files
Deleted docs/manual/00001003305120.png.
cannot compute difference between binary files
Deleted docs/manual/00001003305122.png.
cannot compute difference between binary files
Deleted docs/manual/00001003305124.png.
cannot compute difference between binary files
Deleted docs/manual/00001003310000.zettel.
|
| < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < |
Deleted docs/manual/00001003310104.png.
cannot compute difference between binary files
Deleted docs/manual/00001003310106.png.
cannot compute difference between binary files
Deleted docs/manual/00001003310108.png.
cannot compute difference between binary files
Deleted docs/manual/00001003310110.png.
cannot compute difference between binary files
Deleted docs/manual/00001003315000.zettel.
|
| < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < |
Deleted docs/manual/00001003600000.zettel.
|
| < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < |
Changes to docs/manual/00001004010000.zettel.
1 2 3 4 5 | id: 00001004010000 title: Zettelstore startup configuration role: manual tags: #configuration #manual #zettelstore syntax: zmk | < | | | | < | | < < < < < | < < < < < < < < < < < < < < < < < < < < < < < < < < < < | | | | | | < < < < < < < < < < | | | | < < < < < < < < < < < < < < < < < | < < < | | | | > > > > > > > | | < | < < < | < < | | > < | | < < < | | | > | | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 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 | id: 00001004010000 title: Zettelstore startup configuration role: manual tags: #configuration #manual #zettelstore syntax: zmk modified: 20210525121644 The configuration file, as specified by the ''-c CONFIGFILE'' [[command line option|00001004051000]], allows you to specify some startup options. These options cannot be stored in a [[configuration zettel|00001004020000]] because either they are needed before Zettelstore can start or because of security reasons. For example, Zettelstore need to know in advance, on which network address is must listen or where zettel are placed. An attacker that is able to change the owner can do anything. Therefore only the owner of the computer on which Zettelstore runs can change this information. The file for startup configuration must be created via a text editor in advance. 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 administrators console. On most operating systems, the value must be greater than ''1024'' unless you start Zettelstore with the full privileges of a system administrator (which is not recommended). Default: ''0'' ; [!default-dir-place-type]''default-dir-place-type'' : Specifies the default value for the (sub-) type of [[directory places|00001004011400#type]]. Zettel are typically stored in such places. Default: ''notify'' ; [!insecure-cookie]''insecure-cookie'' : Must be set to ''true'', if authentication is enabled and Zettelstore is not accessible not via HTTPS (but via HTTP). Otherwise web browser are free to ignore the authentication cookie. Default: ''false'' ; [!listen-addr]''listen-addr'' : Configures the network address, where is zettel web 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"'' ; [!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. ; [!persistent-cookie]''persistent-cookie'' : A boolean value 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'' ; [!place-uri-X]''place-uri-//X//'', where //X// is a number greater or equal to one : Specifies a [[place|00001004011200]] where zettel are stored. During startup //X// is counted up, starting with one, until no key is found. This allows to configure more than one place. If no ''place-uri-1'' key is given, the overall effect will be the same as if only ''place-uri-1'' was specified with the value ''dir://.zettel''. In this case, even a key ''place-uri-2'' will be ignored. ; [!read-only-mode]''read-only-mode'' : Puts the Zettelstore web service into a read-only mode. No changes are possible. Default: false. ; [!token-lifetime-api]''token-lifetime-api'', [!token-lifetime-html]''token-lifetime-html'' : 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. Default: 10. ''token-lifetime-html'' specifies the lifetime for the HTML views. Default: 60. It is automatically extended, when a new HTML view is rendered. ; [!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''). Default: ''"/"''. This allows to use a forwarding proxy [[server|00001010090100]] in front of the Zettelstore. ; ''verbose'' : Be more verbose inf logging data. Default: false Other keys will be ignored. |
Changes to docs/manual/00001004011200.zettel.
1 | id: 00001004011200 | | | | | | | | | | | | | | < | | | | | | | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 | id: 00001004011200 title: Zettelstore places role: manual tags: #configuration #manual #zettelstore syntax: zmk modified: 20210525121452 A Zettelstore must store its zettel somehow and somewhere. In most cases you want to store your zettel as files in a directory. Under certain circumstances you may want to store your zettel in other places. An example are the [[predefined zettel|00001005090000]] that come with a Zettelstore. They are stored within the software itself. In another situation you may want to store your zettel volatile, e.g. if you want to provide a sandbox for experimenting. To cope with these (and more) situations, you configure Zettelstore to use one or more places. This is done via the ''place-uri-X'' keys of the [[startup configuration|00001004010000#place-uri-X]] (X is a number). Places are specified using special [[URIs|https://en.wikipedia.org/wiki/Uniform_Resource_Identifier]], somehow similar to web addresses. The following place URIs are supported: ; ''dir:\//DIR'' : Specifies a directory where zettel files are stored. ''DIR'' is the file path. Although it is possible to use relative file paths, such as ''./zettel'' (→ URI is ''dir:\//.zettel''), it is preferable to use absolute file paths, e.g. ''/home/user/zettel''. The directory must exist before starting the Zettelstore[^There is one exception: when Zettelstore is [[started without any parameter|00001004050000]], e.g. via double-clicking its icon, an directory called ''./zettel'' will be created.]. It is possible to [[configure|00001004011400]] a directory place. ; ''file:FILE.zip'' oder ''file:/\//path/to/file.zip'' : Specifies a ZIP file which contains files that store zettel. You can create such a ZIP file, if you zip a directory full of zettel files. This place is always read-only. ; ''mem:'' : Stores all its zettel in volatile memory. If you stop the Zettelstore, all changes are lost. All places that you configure via the ''store-uri-X'' keys form a chain of places. If a zettel should be retrieved, a search starts in the place specified with the ''place-uri-2'' key, then ''place-uri-3'' and so on. If a zettel is created or changed, it is always stored in the place specified with the ''place-uri-1'' key. This allows to overwrite zettel from other places, e.g. the predefined zettel. If you use the ''mem:'' place, where zettel are stored in volatile memory, it makes only sense if you configure it as ''place-uri-1''. Such a place will be empty when Zettelstore starts and only the first place will receive updates. You must make sure that your computer has enough RAM to store all zettel. |
Changes to docs/manual/00001004011400.zettel.
1 | id: 00001004011400 | | | | | | > | | | | | > > > > > > > > > > > > > > > > > > > > > | > > > | < < | | | | | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 | id: 00001004011400 title: Configure file directory places role: manual tags: #configuration #manual #zettelstore syntax: zmk modified: 20210525121232 Under certain circumstances, it is preferable to further configure a file directory place. This is done by appending query parameters after the base place URI ''dir:\//DIR''. The following parameters are supported: |= Parameter:|Description|Default value:| |type|(Sub-) Type of the directory service|(value of ''[[default-dir-place-type|00001004010000#default-dir-place-type]]'') |rescan|Time (in seconds) after which the directory should be scanned fully|600 |worker|Number of worker that can access the directory in parallel|(depends on type) |readonly|Allow only operations that do not change a zettel or create a new zettel|n/a === Type On some operating systems, Zettelstore tries to detect changes to zettel files outside of Zettelstore's control[^This includes Linux, Windows, and macOS.]. On other operating systems, this may be not possible, due to technical limitations. Automatic detection of external changes is also not possible, if zettel files are placed on an external service, such as a file server accessed via SMD/CIFS or NFS. To cope with this uncertainty, Zettelstore provides various internal implementations of a directory place. The default values should match the needs of different users, as explained in the [[installation part|00001003000000]] of this manual. The following values are supported: ; simple : Is not able to detect external changes. Works on all platforms. Is a little slower than other implementations (up to three times slower). ; notify : Automatically detect external changes. Tries to optimize performance, at a little cost of main memory (RAM). === Rescan When the parameter ''type'' is set to ""notify"", Zettelstore automatically detects changes to zettel files that originates from other software. It is done on a ""best-effort"" basis. Under certain circumstances it is possible that Zettelstore does not detect a change done by another software. To cope with this unlikely, but still possible event, Zettelstore re-scans periodically the file directory. The time interval is configured by the ''rescan'' parameter, e.g. ``` place-uri-1: dir:///home/zettel?rescan=300 ``` This makes Zettelstore to re-scan the directory ''/home/zettel/'' every 300 seconds, i.e. 5 minutes. For a Zettelstore with many zettel, re-scanning the directory may take a while, especially if it is stored on a remote computer (e.g. via CIFS/SMB or NFS). In this case, you should adjust the parameter value. Please note that a directory re-scan invalidates all internal data of a Zettelstore. It might trigger a re-build of the backlink database (and other internal databases). Therefore a large value if preferred. This value is ignored for other directory place type, such as ""simple"". === Worker Internally, Zettelstore parallels concurrent requests for a zettel or its metadata. The number of parallel activities is configured by the ''worker'' parameter. A computer contains a limited number of internal processing units (CPU). Its number ranges from 1 to (currently) 128, e.g. in bigger server environments. Zettelstore typically runs on a system with 1 to 8 CPUs. Access to zettel file is ultimately managed by the underlying operating system. Depending on the hardware and on the type of the directory place, only a limited number of parallel accesses are desirable. On smaller hardware[^In comparison to a normal desktop or laptop computer], such as the [[Raspberry Zero|https://www.raspberrypi.org/products/raspberry-pi-zero/]], a smaller value might be appropriate. Every worker needs some amount of main memory (RAM) and some amount of processing power. On bigger hardware, with some fast file services, a bigger value could result in higher performance, if needed. For a directory place of type ""notify"", the default value is: 7. The directory place type ""simple"" limits the value to a maximum of 1, i.e. no concurrency is possible with this type of directory place. For various reasons, the value should be a prime number, with a maximum value of 1499. === Readonly Sometimes you may want to provide zettel from a file directory place, but you want to disallow any changes. If you provide the query parameter ''readonly'' (with or without a corresponding value), the place will disallow any changes. ``` place-uri-1: dir:///home/zettel?readonly ``` If you put the whole Zettelstore in [[read-only|00001004010000]] [[mode|00001004051000]], all configured file directory places will be in read-only mode too, even if not explicitly configured. |
Deleted docs/manual/00001004011600.zettel.
|
| < < < < < < < < < < < < < < < < < < < < < < < < < |
Changes to docs/manual/00001004020000.zettel.
1 2 3 4 5 | id: 00001004020000 title: Configure the running Zettelstore role: manual tags: #configuration #manual #zettelstore syntax: zmk | < < < | | < < | | | | > > > > > > > > > > > > > > > > > > | < | | | < | | > | | < | < < < | | < | < < < | | < < | < < < < | < < < < < < < < < | < < < < < | < | | | < | | > > | | > | > | | > | > | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 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: 00001004020000 title: Configure the running Zettelstore role: manual tags: #configuration #manual #zettelstore syntax: zmk You can configure a running Zettelstore by modifying the special zettel with the ID [[00000000000100]]. This zettel is called ""configuration zettel"". The following metadata keys change the appearance / behavior of Zettelstore: ; [!default-copyright]''default-copyright'' : Copyright value to be used when rendering content. Can be overwritten in a zettel with [[meta key|00001006020000]] ''copyright''. Default: (the empty string). ; [!default-lang]''default-lang'' : Default language to be used when displaying content. Can be overwritten in a zettel with [[meta key|00001006020000]] ''lang''. Default: ''en''. This value 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]]. ; [!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-role]''default-role'' : Role to be used, if a zettel specifies no ''role'' [[meta key|00001006020000]]. Default: ''zettel''. ; [!default-syntax]''default-syntax'' : Syntax to be used, if a zettel specifies no ''syntax'' [[meta key|00001006020000]]. Default: ''zmk'' (""Zettelmarkup""). ; [!default-title]''default-title'' : Title to be used, if a zettel specifies no ''title'' [[meta key|00001006020000]]. Default: ''Untitled''. You can use all [[inline-structured elements|00001007040000]] of Zettelmarkup. ; [!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, all zettel with [[visibility ""expert""|00001010070200]] will be shown (to the owner, if authentication is enabled; to all, otherwise). This affects most computed zettel. Default: False. ; [!footer-html]''footer-html'' : Contains some HTML code that will be included into the footer of each Zettelstore web page. It only affects the [[web user interface|00001014000000]]. Zettel content, delivered via the [[API|00001012000000]] as JSON, etc. is not affected. Default: (the empty string). ; [!home-zettel]''home-zettel'' : Specifies the identifier of the zettel, that should be presented for the default view / home view. If not given or if the identifier does not identify a zettel, the zettel with the identifier ''00010000000000'' is shown. ; [!marker-external]''marker-external'' : Some HTML code that is displayed after a reference to external material. Default: ''&\#10138;'', to display a ""➚"" sign. ; [!list-page-size]''list-page-size'' : If set to a value greater than zero, specifies the number of items shown in WebUI lists. Basically, this is the list of all zettel (possibly restricted) and the list of search results. Default: ''0''. ; [!site-name]''site-name'' : Name of the Zettelstore instance. Will be used when displaying some lists. Default: ''Zettelstore''. ; [!yaml-header]''yaml-header'' : If true, 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 ""meta"" and ""zmk"", Zettelstore will store the zettel as two files: one for the metadata (file extension ''.meta'') and one 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, duplicates 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. |
Deleted docs/manual/00001004020200.zettel.
|
| < < < < < < < < < < < < < < < < < < < < < < |
Changes to docs/manual/00001004050000.zettel.
1 2 3 4 5 | id: 00001004050000 title: Command line parameters 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 | id: 00001004050000 title: Command line parameters role: manual tags: #command #configuration #manual #zettelstore syntax: zmk modified: 20210511140731 Zettelstore is not just a web 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 web-based 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. |
Changes to docs/manual/00001004050200.zettel.
1 2 3 4 5 | id: 00001004050200 title: The ''help'' sub-command role: manual tags: #command #configuration #manual #zettelstore syntax: zmk | | > | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 | id: 00001004050200 title: The ''help'' sub-command role: manual tags: #command #configuration #manual #zettelstore syntax: zmk precursor: 00001004050000 Lists all implemented sub-commands. Example: ``` # zettelstore help Available commands: - "config" - "file" - "help" - "password" - "run" - "run-simple" - "version" ``` |
Changes to docs/manual/00001004050400.zettel.
1 2 3 4 5 | id: 00001004050400 title: The ''version'' sub-command role: manual tags: #command #configuration #manual #zettelstore syntax: zmk | | | | | > | | | < | > < < < | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 | id: 00001004050400 title: The ''version'' sub-command role: manual tags: #command #configuration #manual #zettelstore syntax: zmk precursor: 00001004050000 Emits some information about the Zettelstore's version. This allows you to check, whether your installed Zettelstore is The name of the software (""Zettelstore"") and the build version information is given, as well as the compiler version, the name of the computer running the Zettelstore, and an indication about the operating system and the processor architecture of that computer. The build version information is a string like ''v1.0.2-34-gf567a3''. The part ""v1.0.2"" is the release version. The string ""34"" specifies the number of internal patches, after the release was published. ""gf567a3"" is a code uniquely identify the version to the developer. Everything after the release version is optional, eg. ""v1.4.3"" is a valid build version information too. Example: ``` # zettelstore version Zettelstore (v0.0.4/go1.15) running on mycomputer (linux/amd64) ``` In this example, Zettelstore is running in the released version ""v.0.0.4"" and was compiled using [[Go, version 1.15|https://golang.org/doc/go1.15]]. It runs on a computer named ""mycomputer"". The software was build for running under a Linux operating system with an ""amd64"" processor. |
Changes to docs/manual/00001004051000.zettel.
1 2 3 4 5 | id: 00001004051000 title: The ''run'' sub-command role: manual tags: #command #configuration #manual #zettelstore syntax: zmk | | > | | | | | | | | | | | | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 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: 00001004051000 title: The ''run'' sub-command role: manual tags: #command #configuration #manual #zettelstore syntax: zmk modified: 20210510153318 precursor: 00001004050000 === ``zettelstore run`` This starts the web service. ``` zettelstore run [-c CONFIGFILE] [-d DIR] [-p PORT] [-r] [-v] ``` ; [!a]''-a PORT'' : Specifies the TCP port through which you can reach the [[administrator console|00001004100000]]. See the explanation of [[''admin-port''|00001004010000#admin-port]] for more details. ; [!c]''-c CONFIGFILE'' : Specifies ''CONFIGFILE'' as a file, where [[startup configuration data|00001004010000]] is read. It is ignored, when the given file is not available, nor readable. Default: ''./.zscfg''. (''.\\.zscfg'' on Windows)), where ''.'' denotes the ""current directory"". ; [!d]''-d DIR'' : Specifies ''DIR'' as the directory that contains all zettel. Default is ''./zettel'' (''.\\zettel'' on Windows), where ''.'' denotes the ""current directory"". ; [!debug]''-debug'' : Allows better debugging of the internal web server by disabling any timeout values. You should specify this only as a developer. Especially do not enable it for a production server. [[https://blog.cloudflare.com/exposing-go-on-the-internet/#timeouts]] contains a good explanation for the usefulness of sensitive timeout values. ; [!p]''-p PORT'' : Specifies the integer value ''PORT'' as the TCP port, where the Zettelstore web server listens for requests. Default: 23123. Zettelstore listens only on ''127.0.0.1'', e.g. only requests from the current computer will be processed. If you want to listen on network card to process requests from other computer, please use [[''listen-addr''|00001004010000#listen-addr]] of the configuration file as described below. ; [!r]''-r'' : Puts the Zettelstore in read-only mode. No changes are possible via the web interface / via the API. This allows to publish your content without any risks of unauthorized changes. ; [!v]''-v'' : Be more verbose in writing logs. Command line options take precedence over [[configuration file|00001004010000]] options. |
Changes to docs/manual/00001004051100.zettel.
1 2 3 4 5 | id: 00001004051100 title: The ''run-simple'' sub-command role: manual tags: #command #configuration #manual #zettelstore syntax: zmk | | < < < < < | | < | | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 | id: 00001004051100 title: The ''run-simple'' sub-command role: manual tags: #command #configuration #manual #zettelstore syntax: zmk precursor: 00001004050000 === ``zettelstore run-simple`` This sub-command is implicitly called, when an user starts Zettelstore by double-clicking on its GUI icon. It is s simplified variant of the [[''run'' sub-command|00001004051000]]. It allows only to specify a zettel directory. The directory will be created automatically, if it does not exist. This is the only difference to the ''run'' sub-command, where the directory must exists. ``` zettelstore run-simple [-d DIR] ``` ; [!d]''-d DIR'' : Specifies ''DIR'' as the directory that contains all zettel. Default is ''./zettel'' (''.\\zettel'' on Windows), where ''.'' denotes the ""current directory"". |
Changes to docs/manual/00001004051200.zettel.
1 2 3 4 5 | id: 00001004051200 title: The ''file'' sub-command role: manual tags: #command #configuration #manual #zettelstore syntax: zmk | | < | | > | | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 | id: 00001004051200 title: The ''file'' sub-command role: manual tags: #command #configuration #manual #zettelstore syntax: zmk precursor: 00001004050000 Reads zettel data from a file (or from standard input / stdin) and renders it to standard output / stdout. This allows Zettelstore to render files manually. ``` zettelstore file [-t FORMAT] [file-1 [file-2]] ``` ; ''-t FORMAT'' : Specifies the output format. Supported values are: [[''html''|00001012920510]] (default), [[''djson''|00001012920503]], [[''json''|00001012920501]], [[''native''|00001012920513]], [[''raw''|00001012920516]], [[''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. If neither ''file-1'' nor ''file-2'' are given, metadata and zettel content are read from standard input / stdin. |
Changes to docs/manual/00001004051400.zettel.
1 2 3 4 5 | id: 00001004051400 title: The ''password'' sub-command role: manual tags: #command #configuration #manual #zettelstore syntax: zmk | | | | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 | id: 00001004051400 title: The ''password'' sub-command role: manual tags: #command #configuration #manual #zettelstore syntax: zmk precursor: 00001004050000 This sub-command is used to create a hashed password for to be authenticated users. It reads a password from standard input (two times, both must be equal) and writes the hashed password to standard output. 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 background. An example: ``` # zettelstore password bob 20200911115600 Password: Again: |
︙ | ︙ |
Deleted docs/manual/00001004059700.zettel.
|
| < < < < < < < < < < < < < < < < < < < < < < < < |
Deleted docs/manual/00001004059900.zettel.
|
| < < < < < < < < < < < < < < < < < < < < < < < < |
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: 20210510155859 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 untrusted 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 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 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: 00001004101000 title: List of supported commands of the administrator console role: manual tags: #configuration #manual #zettelstore syntax: zmk modified: 20210525161623 ; ''bye'' : Closes the connection to the administrator console. ; ''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. Keys of list elements are specified by appending a number greater than zero to the key. ; ''crlf'' : Toggles CRLF mode for console output. Changes end of line sequences between Windows mode (==\\r\\n==) and non-Windows mode (==\\n==, initial value). Often used on Windows telnet clients that otherwise scramble the output of commands. ; ''dump-index'' : Displays the content of the internal search index. ; ''dump-recover RECOVER'' : Displays data about the last given recovered internal activity. The value for ''RECOVER'' can be obtained via the command ``stat core``, which lists all overview data about all recoveries. ; ''echo'' : Toggles the echo mode, where each command is printed before execution ; ''env'' : Display environment values. ; ''help'' : Displays a list of all available commands. ; ''get-config'' : Displays current configuration data. ``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'' : Toggles the header mode, where each table is show with a header nor not. ; ''metrics'' : Displays some values that reflect the inner workings of Zettelstore. See [[here|https://golang.org/pkg/runtime/metrics/]] for a technical description of these values. ; ''next-config'' : Displays next configuration data. It will be the current configuration, if the corresponding services is restarted. ``next-config`` shows all next configuration data. ``next-config SERVICE`` shows only the next configuration data of the given service. ``next-config SERVICE KEY`` shows the next configuration data for the given service and key. ; ''restart SERVICE'' : Restart the given service and all other that depend on this. ; ''services'' : Displays s list of all available services and their current status. ; ''set-config SERVICE KEY VALUE'' : Sets a single configuration value for the next configuration of a given service. It will become effective if the service is restarted. If the key specifies a list value, all other list values with a number greater than the given key are deleted. You can use the special number ""0"" to delete all values. E.g. ``set-config place place-uri-0 any_text`` will remove all values of the list //place-uri-//. ; ''shutdown'' : Terminate the Zettelstore itself (and closes the connection to the administrator console). ; ''start SERVICE'' : Start the given bservice and all dependent services. ; ''stat SERVICE'' : Display some statistical values for the given service. ; ''stop SERVICE'' : Stop the given service and all other that depend on this. |
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 | id: 00001005000000 title: Structure of Zettelstore role: manual tags: #design #manual #zettelstore syntax: zmk Zettelstore is a software that manages your zettel. Since every zettel must be readable without any special tool, most zettel has to be stored as ordinary files within specific directories. Typically, file names and file content must comply to specific rules so that Zettelstore can manage them. If you add, delete, or change zettel files with other tools, e.g. a text editor, Zettelstore will monitor these actions. Zettelstore provides additional services to the user. Via a builtin web interface you can work with zettel in various ways. For example, you are able to list zettel, to create new zettel, to edit them, or to delete them. You can view zettel details and relations between zettel. In addition, Zettelstore provides an ""application programming interface"" (API) that allows other software to communicate with the Zettelstore. Zettelstore becomes extensible by external software. For example, a more sophisticated web interface could be build, or an application for your mobile device that allows you to send content to your Zettelstore as new zettel. === Where zettel are stored Your zettel are stored as files in a specific directory. If you have not explicitly specified the directory, a default directory will be used. The directory has to be specified at [[startup time|00001004010000]]. Nested directories are not supported (yet). Every file in this directory that should be monitored by Zettelstore must have a file name that begins with 14 digits (0-9), the [[zettel identifier|00001006050000]]. If you create a new zettel via the web interface or the API, the zettel identifier will be the timestamp of the current date and time (format is ''YYYYMMDDhhmmss''). This allows zettel to be sorted naturally by creation time. 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. The file name must have an file extension. Two file extensions are used by Zettelstore: ''.meta'' and ''.zettel''. Other file 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. 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 placed in the same file, separated by an empty line or a line with three dashes (""''-\-\-''"", also known as ""YAML separator""). === Predefined zettel Zettelstore contains some [[predefined zettel|00001005090000]] to work properly. The [[configuration zettel|00001004020000]] is one example. To render the builtin web interface, some templates are used, as well as a layout specification in CSS. The icon that visualizes an external link is a predefined SVG image. All of these are visible to the Zettelstore as zettel. One reason for this is to allow you to modify these zettel to adapt Zettelstore to your needs and visual preferences. Where are these zettel stored? They are stored within the Zettelstore software itself, because one design goal was to have just one file to use Zettelstore. But data stored within an executable programm cannot be changed later[^Well, it can, but it is a very bad idea to allow this. Mostly for security reasons.]. To allow changing predefined zettel, both the file store and the internal zettel store are internally chained together. If you change a zettel, it will be always stored as a file. If a zettel is requested, Zettelstore will first try to read that zettel from a file. If such a file was not found, the internal zettel store is searched secondly. Therefore, the file store ""shadows"" the internal zettel store. If you want to read the original zettel, you either have to delete the zettel (which removes it from the file directory), or you have to rename it to another zettel identifier. Now we have two places where zettel are stored: in the specific directory and within the Zettelstore software. * [[List of predefined zettel|00001005090000]] |
Changes to docs/manual/00001005090000.zettel.
1 2 3 4 5 | id: 00001005090000 title: List of predefined zettel role: manual tags: #manual #reference #zettelstore syntax: zmk | < | < < | < | | | < | < | | < < < < | | | < < < | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 | id: 00001005090000 title: List of predefined zettel role: manual tags: #manual #reference #zettelstore syntax: zmk modified: 20210511180816 The following table lists all predefined zettel with their purpose. |= Identifier :|= Title | Purpose | [[00000000000001]] | Zettelstore Version | Contains the version string of the running Zettelstore | [[00000000000002]] | Zettelstore Host | Contains the name of the computer running the Zettelstore | [[00000000000003]] | Zettelstore Operating System | Contains the operating system and CPU architecture of the computer running the Zettelstore | [[00000000000004]] | Zettelstore License | Lists the license of Zettelstore | [[00000000000005]] | Zettelstore Contributors | Lists all contributors of Zettelstore | [[00000000000006]] | Zettelstore Dependencies | Lists all licensed content | [[00000000000020]] | Zettelstore Place Manager | Contains some statistics about zettel places and the the index process | [[00000000000090]] | Zettelstore Supported Metadata Keys | Contains all supported metadata keys, their [[types|00001006030000]], and more | [[00000000000096]] | Zettelstore Startup Configuration | Contains the effective values of the [[startup configuration|00001004010000]] | [[00000000000100]] | Zettelstore Runtime Configuration | Allows to [[configure Zettelstore at runtime|00001004020000]] | [[00000000010100]] | Zettelstore Base HTML Template | Contains the general layout of the HTML view | [[00000000010200]] | Zettelstore Login Form HTML Template | Layout of the login form, when authentication is [[enabled|00001010040100]] | [[00000000010300]] | Zettelstore List Meta HTML Template | Used when displaying a list of zettel | [[00000000010401]] | Zettelstore Detail HTML Template | Layout for the HTML detail view of one zettel | [[00000000010402]] | Zettelstore Info HTML Templöate | Layout for the information view of a specific zettel | [[00000000010403]] | Zettelstore Form HTML Template | Form that is used to create a new or to change an existing zettel that contains text | [[00000000010404]] | Zettelstore Rename Form HTML Template | View that is displayed to change the [[zettel identifier|00001006050000]] | [[00000000010405]] | Zettelstore Delete HTML Template | View to confirm the deletion of a zettel | [[00000000010500]] | Zettelstore List Roles HTML Template | Layout for listing all roles | [[00000000010600]] | Zettelstore List Tags HTML Template | Layout of tags lists | [[00000000020001]] | Zettelstore Base CSS | CSS file that is included by the [[Base HTML Template|00000000010100]] | [[00000000040001]] | Generic Emoji | Image that is shown if original image reference is invalid | [[00000000090000]] | New Menu | Contains items that should contain in the zettel template menu | [[00000000090001]] | New Zettel | Template for a new zettel with role ""[[zettel|00001006020100]]"" | [[00000000090002]] | New User | Template for a new zettel with role ""[[user|00001006020100#user]]"" | [[00010000000000]] | Home | Default home zettel, contains some welcome information If a zettel is not linked, it is not accessible for the current user. **Important:** All identifier may change until a stable version of the software is released. |
Changes to docs/manual/00001006000000.zettel.
1 2 | id: 00001006000000 title: Layout of a 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 | id: 00001006000000 title: Layout of a Zettel tags: #design #manual #zettelstore syntax: zmk role: manual A zettel consists of two part: 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. Metadata has to conform to a [[special syntax|00001006010000]]. It is effectively a collection of key/value pairs. Some keys have a [[special meaning|00001006020000]] and most of the predefined keys need values of a specific [[type|00001006030000]]. Each zettel is given a unique [[identifier|00001006050000]]. To some degree, the zettel identifier is part of the metadata.. The zettel content is your valuable content. Zettelstore contains some predefined parsers that interpret the zettel content to the syntax of the zettel. This includes markup languages, like [[Zettelmarkup|00001007000000]] and [[CommonMark|https://commonmark.org/]]. Other text formats are also supported, like CSS and HTML templates. Plain text content is always Unicode, encoded as UTF-8. 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 represenation: SVG. And the is support for some binary image formats, like GIF, PNG, and JPEG. |
Changes to docs/manual/00001006010000.zettel.
1 2 | id: 00001006010000 title: Syntax of Metadata | < | < | < | | | 1 2 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 tags: #manual #syntax #zettelstore syntax: zmk role: manual 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 (""''-''"") 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. Parsing metadata ends, if an empty line is found or if a line with at least three hyphen-minus characters is found. |
︙ | ︙ |
Changes to docs/manual/00001006020000.zettel.
1 2 3 4 5 | id: 00001006020000 title: Supported Metadata Keys 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 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 | id: 00001006020000 title: Supported Metadata Keys role: manual tags: #manual #meta #reference #zettel #zettelstore syntax: zmk Although you are free to define your own metadata, by using any key (according to the [[syntax|00001006010000]]), some keys have a special meaning that is enforced by Zettelstore. See the [[computed list of supported metadata keys|00000000000090]] for details. Most keys conform to a [[type|00001006030000]]. ; [!back]''back'' : Is a property that contains the identifier of all zettel that reference the zettel of this metadata, that are not referenced by this zettel. Basically, it is the value of [[''backward''|#bachward]], but without any zettel identifier that is contained in [[''forward''|#forward]]. ; [!backward]''backward'' : Is a property that contains the identifier of all zettel that reference the zettel of this metadata. References within inversable values are not included here, e.g. [[''precursor''|#precursor]]. ; [!copyright]''copyright'' : Defines a copyright string that will be encoded. If not given, the value ''default-copyright'' from the [[configuration zettel|00001004020000#default-copyright]] will be used. ; [!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. Mostly used for HTML rendering of the zettel. If not given, the value ''default-lang'' from the [[configuration zettel|00001004020000#default-lang]] will be used. Use values according to the language definition of [[RFC-5646|https://tools.ietf.org/html/rfc5646]]. ; [!license]''license'' : Defines a license string that will be rendered. If not given, the value ''default-license'' from the [[configuration zettel|00001004020000#default-license]] will be used. ; [!modified]''modified'' : Date and time when a zettel was modified through Zettelstore. If you edit a zettel with an editor software outside Zettelstore, you should set it manually to an appropriate value. This is a computed value. There is no need to set it via Zettelstore. ; [!no-index]''no-index'' : If set to true, the zettel will not be indexed and therefore not be found in full-text searches. ; [!precursor]''precursor'' : References zettel for which this zettel is a ""Folgezettel"" / follow-up zettel. Basically the inverse of key [[''folge''|#folge]]. ; [!published]''published'' : This property contains the timestamp of the mast modification / creation of the zettel. If [[''modified''|#modified]]is set, it contains the same value. Otherwise, if the zettel identifier contains a valid timestamp, the identifier is used. In all other cases, this property is not set. It can be used for [[sorting|00001012051800#sort]] zettel based on their publication date. It is a computed value. There is no need to set it via Zettelstore. ; [!read-only]''read-only'' : Marks a zettel as read-only. 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, the value ''default-role'' from the [[configuration zettel|00001004020000#default-role]] will be used. ; [!syntax]''syntax'' : Specifies the syntax that should be used for interpreting the zettel. The zettel about [[other markup languages|00001008000000]] defines supported values. If not given, the value ''default-syntax'' from the [[configuration zettel|00001004020000#default-syntax]] will be used. ; [!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 ''default-title'' from the [[configuration zettel|00001004020000#default-title]] will be used. You can use all [[inline-structured elements|00001007040000]] of Zettelmarkup. ; [!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 on the web user interface if you use the default template. ; [!user-id]''user-id'' : Provides some unique user identification for a user zettel. 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. Is only valid in a user zettel. See [[User roles|00001010070300]] for more details. ; [!visibility]''visibility'' : When you work with authentication, you can give every zettel a value to decide, who can see the zettel. Its default value can be set with [[''default-visibility''|00001004020000#default-visibility]] of the configuration zettel. See [[visibility rules for zettel|00001010070200]] for more details. |
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 34 35 36 37 38 | id: 00001006020100 title: Supported Zettel Roles role: manual tags: #manual #meta #reference #zettel #zettelstore syntax: zmk The [[''role'' key|00001006020000#role]] defines what kind of zettel you are writing. The following values are used internally by Zettelstore and must exist: ; [!user]''user'' : If you want to use [[authentication|00001010000000]], all zettel that identify users of the zettel store must have this role. Beside of this, you are free to define your own roles. The role ''zettel'' is predefined as the default role, but you can [[change this|00001004020000#default-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 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 raw 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 | id: 00001006020400 title: Supported values for metadata key ''read-only'' role: manual tags: #manual #meta #reference #zettel #zettelstore syntax: zmk 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 interface. However, if the zettel is stored as a file in a [[directory place|00001004011400]], the zettel could be modified using an external editor. === Authentication enabled If there is no metadata value for key ''read-only'' or if its [[boolean value|00001006030500]] is interpreted as ""false"", anybody can modify the zettel. If the metadata value is the same as an explicit [[user role|00001010070300]], users with that role (or a role with lower rights) are not allowed to modify the zettel. ; ""reader"" : Neither an unauthenticated user nor a user with role ""reader"" is allowed to modify the zettel. Users with role ""writer"" or the owner itself still can modify the zettel. ; ""writer"" : Neither an unauthenticated user, nor users with roles ""reader"" or ""writer"" are allowed to modify the zettel. Only the owner of the Zettelstore can modify the zettel. If the metadata value is something else (one of the values ""true"" or ""owner"" is recommended), no user is allowed modify the zettel through the web interface. However, if the zettel is accessible as a file in a [[directory place|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 | id: 00001006030000 title: Supported Key Types role: manual tags: #manual #meta #reference #zettel #zettelstore syntax: zmk Most [[supported metadata keys|00001006020000]] conform to a type. Every metadata key should conform to a type. User-defined metadata keys are of type EString. The name of the metadata key is bound to the key type Every key type has an associated validation rule to check values of the given type. There is also a rule how values are matched, e.g. against a search term when selecting some zettel. And there is a rule, how values compare for sorting. * [[Boolean|00001006030500]] * [[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 | id: 00001006030500 | | | < | > | | > > > > > > | | > | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 | id: 00001006030500 title: Boolean Key Type role: manual tags: #manual #meta #reference #zettel #zettelstore syntax: zmk Values of this type denote a truth value. === Allowed values Every character sequence that begins with a ""''0''"", ""''F''"", ""''N''"", ""''f''"", or a ""''n''"" is interpreted as the ""false"" boolean value. All other metadata value is interpreted as the ""true"" boolean value. === Match operator The match operator is the equals operator, i.e. * ``(true == true) == true`` * ``(false == false) == true`` * ``(true == false) == false`` * ``(false == true) == false`` === Sorting The ""false"" value is less than the ""true"" value: ``false < true`` |
Changes to docs/manual/00001006031000.zettel.
1 2 3 4 5 | id: 00001006031000 title: Credential Key Type 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 | id: 00001006031000 title: Credential Key Type role: manual tags: #manual #meta #reference #zettel #zettelstore syntax: zmk 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. === Match operator A credential never matches to any other value. === 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 | id: 00001006031500 title: EString Key Type 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 | id: 00001006031500 title: EString Key Type role: manual tags: #manual #meta #reference #zettel #zettelstore syntax: zmk 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. === Match operator A value matches an EString value, if the first value is part of the EString value. This check is done case-insensitive. For example, ""hell"" matches ""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``. Comparison is done character-wise by finding the first difference in the respective character sequence. For example, ``abc > aBc``. |
Changes to docs/manual/00001006032000.zettel.
1 2 3 4 5 | id: 00001006032000 title: Identifier Key Type 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 | id: 00001006032000 title: Identifier Key Type role: manual tags: #manual #meta #reference #zettel #zettelstore syntax: zmk Values of this type denote a [[zettel identifier|00001006050000]]. === Allowed values Must be a sequence of 14 digits (""0""--""9""). === Match operator A value matches an identifier value, if the first value is the prefix of the identifier value. 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 | id: 00001006032500 title: IdentifierSet Key Type 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 | id: 00001006032500 title: IdentifierSet Key Type role: manual tags: #manual #meta #reference #zettel #zettelstore syntax: zmk Values of this type denote a (sorted) set of [[zettel identifier|00001006050000]]. A set is different to a list, as no duplicates are allowed. === Allowed values Must be at least one sequence of 14 digits (""0""--""9""), separated by space characters. === Match operator A value matches an identifier set value, if the first value is a prefix of one of the identifier value. For example, ""000010"" matches ""[[00001006032000]] [[00001006032500]]"". === Sorting Sorting is done by comparing the [[String|00001006033500]] values. |
Changes to docs/manual/00001006033000.zettel.
1 2 3 4 5 | id: 00001006033000 title: Number Key Type 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 | id: 00001006033000 title: Number Key Type role: manual tags: #manual #meta #reference #zettel #zettelstore syntax: zmk 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. === Match operator The match operator is the equals operator, i.e. two values must be numeric equal to match. This includes that ""+12"" is equal to ""12"", therefore both values match. === Sorting Sorting is done by comparing the numeric values. |
Changes to docs/manual/00001006033500.zettel.
1 2 3 4 5 | id: 00001006033500 title: String Key Type 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 | id: 00001006033500 title: String Key Type role: manual tags: #manual #meta #reference #zettel #zettelstore syntax: zmk 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. === Match operator A value matches a String value, if the first value is part of the String value. This check is done case-insensitive. For example, ""hell"" matches ""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``. Comparison is done character-wise by finding the first difference in the respective character sequence. For example, ``abc > aBc``. |
Changes to docs/manual/00001006034000.zettel.
1 2 3 4 5 | id: 00001006034000 title: TagSet Key Type 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 | id: 00001006034000 title: TagSet Key Type role: manual tags: #manual #meta #reference #zettel #zettelstore syntax: zmk Values of this type denote a (sorted) set of tags. A set is different to a list, as no duplicates 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. === Match operator It depends of the first character of a search string how it is matched against a tag set value: * If the first character of the search string is a number sign character, it must exactly match one of the values of a tag. * In other cases, the search string must be the prefix of at least one tag. Conpectually, all number sign characters are removed at the beginning of the search string and of all tags. === Sorting Sorting is done by comparing the [[String|00001006033500]] values. |
Changes to docs/manual/00001006034500.zettel.
1 2 3 4 5 | id: 00001006034500 title: Timestamp Key Type 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 | id: 00001006034500 title: Timestamp Key Type role: manual tags: #manual #meta #reference #zettel #zettelstore syntax: zmk modified: 20210511131903 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. === Match operator A value matches a timestamp value, if the first value is the prefix of the timestamp value. For example, ""202102"" matches ""20210212143200"". === 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 | id: 00001006035000 title: URL Key Type 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 | id: 00001006035000 title: URL Key Type role: manual tags: #manual #meta #reference #zettel #zettelstore syntax: zmk Values of this type denote an URL. === Allowed values All characters of an URL / URI are allowed. === Match operator A value matches a URL value, if the first value is part of the URL value. This check is done case-insensitive. For example, ""hell"" matches ""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 | id: 00001006035500 title: Word Key Type 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 | id: 00001006035500 title: Word Key Type role: manual tags: #manual #meta #reference #zettel #zettelstore syntax: zmk Values of this type denote a single word. === Allowed values Must be a non-empty sequence of characters, but without the space character. === Match operator A value matches a word value, if both value are character-wise equal. === 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 | id: 00001006036000 title: WordSet Key Type role: manual tags: #manual #meta #reference #zettel #zettelstore syntax: zmk Values of this type denote a (sorted) set of [[words|00001006035500]]. A set is different to a list, as no duplicates are allowed. === Allowed values Must be a sequence of at least one word, separated by space characters. === Match operator A value matches an wordset value, if the first value is equal to one of the word values in the word set. === Sorting Sorting is done by comparing the [[String|00001006033500]] values. |
Changes to docs/manual/00001006036500.zettel.
1 2 3 4 5 | id: 00001006036500 title: Zettelmarkup Key Type 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 | id: 00001006036500 title: Zettelmarkup Key Type role: manual tags: #manual #meta #reference #zettel #zettelstore syntax: zmk 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. === Match operator A value matches a String value, if the first value is part of the String value. This check is done case-insensitive. For example, ""hell"" matches ""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``. 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 | id: 00001006050000 title: Zettel identifier | < | | < | | 1 2 3 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: 00001006050000 title: Zettel identifier tags: #design #manual #zettelstore syntax: zmk role: manual Each zettel is given a unique identifier. To some degree, the zettel identifier is part of the metadata. Basically, the identifier is given by the [[Zettelstore|00001005000000]] software. 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. In fact, all identifiers of zettel initially provided by an empty Zettelstore begin with ""000000"", except the home zettel ''00010000000000''. Zettel identifier of this manual have be chosen to begin with ""000010"". A zettel can have any identifier that contains 14 digits and that is not in use by another zettel managed by the same Zettelstore. |
Deleted docs/manual/00001006055000.zettel.
|
| < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < |
Changes to docs/manual/00001007000000.zettel.
1 2 | id: 00001007000000 title: Zettelmarkup | < | < | | | | | | | < | < | < < | 1 2 3 4 5 6 7 8 9 10 11 12 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: 00001007000000 title: Zettelmarkup tags: #manual #zettelmarkup #zettelstore syntax: zmk role: manual 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. Zettelmark supports the longevity of stored notes by providing a syntax that any person can easily read, as well as a computer. Zettelmark 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|https://commonmark.org/]] is an attempt to make it simpler by providing a comprehensive specification, combined with an extra chapter to give hints for the implementation. Zettelmark 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 superset of HTML. 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. Zettelmarkup is a rich markup language, but it focusses on relatively short zettel content. It allows embedding other content, simple tables, quotations, description lists, and images. It provides a broad range of inline formatting, including //emphasized//{-}, **strong**{-}, __underlined__, ;;small;;, ~~deleted~~{-} and __inserted__{-} text. Footnotes[^like this] are supported, links to other zettel and to external material, as well as citation keys. Zettelmarkup might be seen as a proprietary markup language. But if you want to use Markdown/CommonMark and you need support for footnotes, you'll end up with a proprietary extension. However, the Zettelstore supports CommonMark as a zettel syntax, so you can mix both Zettelmarkup zettel and CommonMark zettel in one store to get the best of both worlds. * [[General principles|00001007010000]] * [[Basic definitions|00001007020000]] * [[Block-structured elements|00001007030000]] * [[Inline-structured element|00001007040000]] * [[Attributes|00001007050000]] * [[Summary of formatting characters|00001007060000]] |
Changes to docs/manual/00001007010000.zettel.
1 2 | id: 00001007010000 title: Zettelmarkup: General Principles | < | | | | | | | | | | | | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 | id: 00001007010000 title: Zettelmarkup: General Principles tags: #manual #zettelmarkup #zettelstore syntax: zmk role: manual Any document can be thought as a sequence of paragraphs and other blocks-structural elements (""blocks""), such as headings, lists, 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-structural elements (""inlines""), such as text, links, 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 begins at the first position of a line, but may need one or more character, plus a space character. Table blocks begins 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 placed at the end of non-space text.]. Some inline elements do not follow the rule of two identical character, especially to specify footnotes, citation keys, and local marks. These elements begin with one opening square bracket (""``[``""), use a character for specifying the kind of the inline, typically allow to specify some content, and end with one closing square bracket (""``]``""). One inline element that does not begin with two characters is the ""entity"". It allows to specify any Unicode character. The specification of that character is placed between an ampersand character and a semicolon: ``&...;``{=zmk}. For exmple, an ""n-dash"" could also be specified as ``–``{==zmk}. The backslash character (""``\\``"") possibly gives the next character a special meaning. This allows to resolve some left ambiguities. For example, a list of depth 2 will begin a line with ``** Item 2.2``{=zmk}. An inline element to strongly emphasize some text begin with a space will be specified as ``** Text**``{=zmk}. To force the inline element formatting at the beginning of a line, ``**\\ Text**``{=zmk} should better be specified. Many block and inline elements can be refined by additional attributes. Attributes resemble roughly HTML attributes and are placed near the corresponding elements by using the syntax ``{...}``{=zmk}. One example is to make space characters visible inside a inline literal element: ``1 + 2 = 3``{-} was specified by using the default attribute: ``\`\`1 + 2 = 3\`\`{-}``. To summarize: * With some exceptions, blocks-structural elements begins at the for position of a line with three identical characters. * The most important exception to this rule is the specification of lists. * If no block element is found, a paragraph with inline elements is assumed. |
︙ | ︙ |
Changes to docs/manual/00001007020000.zettel.
1 2 | id: 00001007020000 title: Zettelmarkup: Basic Definitions | < | | | < | | | | | | | | | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 | id: 00001007020000 title: Zettelmarkup: Basic Definitions tags: #manual #zettelmarkup #zettelstore syntax: zmk role: manual Every zettelmark content consists of a sequence of Unicode codepoints. Unicode codepoints are called in the following as **character**s. Characters are encoded with UTF-8. A **line** is a sequence of characters, except newline (''U+000A'') and carraige return (''U+000D''), followed by a line ending sequence or the end of content. A **line ending** is either a newline not followed by a carriage return, a newline followed by a carriage return, or a carriage return. Different line can be finalized by different line endings. An **empty line** is an empty sequence of characters, followed by a line ending or the end of content. The **space** character is the Unicode codepoint ''U+0020''. |
Changes to docs/manual/00001007030000.zettel.
1 | id: 00001007030000 | | < | < | | | | < < < | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 | id: 00001007030000 title: Zettelmarkup: Blocks-Structured Elements tags: #manual #zettelmarkup #zettelstore syntax: zmk role: manual 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 In Zettelmarkup, lists themselves are not specified, but list items. A sequence of list items is considered as a list. [[Description lists|00001007030100]] contain two different item types: the term to be described and the description itself. These cannot be combined with other lists. Ordered lists, unordered lists, and quotation lists can be combined into [[nested lists|00001007030200]]. === One-line blocks * [[Headings|00001007030300]] allow to structure the content of a zettel. * The [[horizontal rule|00001007030400]] signals a thematic break === Line-range blocks This kind of blocks encompass at least two lines. To be useful, they encompass more lines. They begin with at least three identical characters at the first position of the beginning line. They end at the line, that contains at least the same number of these identical characters, beginning at the first position of that line. This allows line-range blocks to be nested. Additionally, all other blocks elements are allowed in line-range blocks. * [[Verbatim blocks|00001007030500]] do not interpret their content, * [[Quotation blocks|00001007030600]] specify a block-length quotation, * [[Verse blocks|00001007030700]] allow to enter poetry, lyrics and similar text, where line endings are important * [[Region blocks|00001007030800]] just mark a region, e.g. for common formatting * [[Comment blocks|00001007030900]] allow to enter text that will be ignored when rendered === Tables Similar to lists are tables not specified explicitly. A sequence of table rows is considered a [[table|00001007031000]]. A table row itself is a sequence of table cells. |
︙ | ︙ |
Changes to docs/manual/00001007030100.zettel.
1 2 | id: 00001007030100 title: Zettelmarkup: Description Lists | < | | | | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 | id: 00001007030100 title: Zettelmarkup: Description Lists tags: #manual #zettelmarkup #zettelstore syntax: zmk role: manual 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. Similar to terms, following lines can also be part of the actual description, if they begin at each line with exactly two space characters. In contrast to terms, the actual descriptions are merged into a paragraph. This is because, an actual description can contain more than one paragraph. As usual, paragraphs are separated by an empty line. Every following paragraph of an actual description must be indented by two space characters. |
︙ | ︙ |
Changes to docs/manual/00001007030200.zettel.
1 2 | id: 00001007030200 title: Zettelmarkup: Nested Lists | < | | | | | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 | id: 00001007030200 title: Zettelmarkup: Nested Lists tags: #manual #zettelmarkup #zettelstore syntax: zmk role: manual 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. 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. |
︙ | ︙ | |||
87 88 89 90 91 92 93 | *# A.3 * B * C ::: Please note that two lists cannot be separated by an empty line. | | | | 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 | *# A.3 * B * C ::: Please note that two lists cannot be separated by an empty line. Instead you should place a horizonal rule (""thematic break"") between them. You could also use a mark element or a hard line break to separate the two lists: ```zmk # One # Two [!sep] # Uno # Due --- |
︙ | ︙ |
Changes to docs/manual/00001007030300.zettel.
1 2 | id: 00001007030300 title: Zettelmarkup: Headings | < | | | | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 | id: 00001007030300 title: Zettelmarkup: Headings tags: #manual #zettelmarkup #zettelstore syntax: zmk role: manual 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. ```zmk === Level 1 Heading ==== Level 2 Heading ===== Level 3 Heading ====== Level 4 Heading ======= Level 5 Heading |
︙ | ︙ | |||
29 30 31 32 33 34 35 | === 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. | | | 28 29 30 31 32 33 34 35 36 37 38 39 40 41 | === 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 | id: 00001007030400 | | < | < | | | | | | | 1 2 3 4 5 6 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: 00001007030400 title: Zettelmarkup: Horizontal Rule tags: #manual #zettelmarkup #zettelstore syntax: zmk role: manual To signal a thematic break, you can specify a horizonal 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 character in this line will be ignored If you do not enter the three hyphen-minus charachter 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 --- ----{color=green} ----- --- inline --- ignored ``` is rendered in HTML as :::example --- ----{color=green} ----- --- inline --- ignored ::: |
Changes to docs/manual/00001007030500.zettel.
1 2 | id: 00001007030500 title: Zettelmarkup: Verbatim Blocks | < | | | | | | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 | id: 00001007030500 title: Zettelmarkup: Verbatim Blocks tags: #manual #zettelmarkup #zettelstore syntax: zmk role: manual 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. The verbatim block supports the default attribute: 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 (programming) language to support colourizing the text when rendered in HTML. 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 grave accent characters in the text that should not be interpreted. For example: |
︙ | ︙ |
Changes to docs/manual/00001007030600.zettel.
1 2 | id: 00001007030600 title: Zettelmarkup: Quotation Blocks | < | | | | | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 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: 00001007030600 title: Zettelmarkup: Quotation Blocks tags: #manual #zettelmarkup #zettelstore syntax: zmk role: manual 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 seomeone, 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. You can add some [[attributes|00001007050000]] on the beginning line of a quotation block, following the initiating characters. The quotation does not support the default attribute, nor the generic attribute. Attributes are interpreted on HTML rendering. Any other character in this line will be ignored Text following the beginning line will 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 a quotation block within a quotation block. At the ending line, you can enter some [[inline elements|00001007040000]] after the less-than characters. These will interpreted as some attribution text. For example: ```zmk <<<< A quotation with an embedded quotation <<<{style=color:green} Embedded <<< Embedded Author <<<< ``` will be rendered in HTML as: :::example <<<< A quotation with an embedded quotation <<<{style=color:green} Embedded <<< Embedded Author <<<< Quotation Author ::: |
Changes to docs/manual/00001007030700.zettel.
1 2 | id: 00001007030700 title: Zettelmarkup: Verse Blocks | < | | | | | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 | id: 00001007030700 title: Zettelmarkup: Verse Blocks tags: #manual #zettelmarkup #zettelstore syntax: zmk role: manual 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. This kind of line-range block begins with at least three quotation mark characters (""''"''"", ''U+0022'') at the first position of a line. You can add some [[attributes|00001007050000]] on the beginning line of a verse block, following the initiating characters. The verse block does not support the default attribute, nor the generic attribute. Attributes are interpreted on HTML rendering. Any other character in this line will be ignored. Text following the beginning line will 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 a verse block within a verse block. At the ending line, you can enter some [[inline elements|00001007040000]] after the quotation mark characters. These will interpreted as some attribution text. For example: ```zmk """" A verse block with an embedded verse block """{style=color:green} Embedded verse block """ Embedded Author """" Verse Author ``` will be rendered as: :::example """" A verse block with an embedded verse block """{style=color:green} Embedded verse block """ Embedded Author """" Verse Author ::: |
Changes to docs/manual/00001007030800.zettel.
1 2 | id: 00001007030800 title: Zettelmarkup: Region Blocks | < | | | < | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 | id: 00001007030800 title: Zettelmarkup: Region Blocks tags: #manual #zettelmarkup #zettelstore syntax: zmk role: manual 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 verse block, following the initiating characters. The region block does not support the default attribute, but it supports the generic attribute. Some generic attributes, like ``=note``, ``=warning`` will be rendered special. Attributes are interpreted on HTML rendering. Any other character in this line will be ignored. Text following the beginning line will 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 a region block within a region block. At the ending line, you can enter some [[inline elements|00001007040000]] after the colon characters. These will interpreted as some attribution text. |
︙ | ︙ | |||
65 66 67 68 69 70 71 | Generic attributes that are result in a special HTML rendering are: * example * note * tip * important * caution * warning | < < < < < < < < < < < < < | 63 64 65 66 67 68 69 | Generic attributes that are result in a special HTML rendering are: * example * note * tip * important * caution * warning |
Changes to docs/manual/00001007030900.zettel.
1 2 | id: 00001007030900 title: Zettelmarkup: Comment Blocks | < | < | | | | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 | id: 00001007030900 title: Zettelmarkup: Comment Blocks tags: #manual #zettelmarkup #zettelstore syntax: zmk role: manual 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 renderers. 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: |
︙ | ︙ |
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 19 20 21 22 23 24 25 26 27 28 29 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 | id: 00001007031000 title: Zettelmarkup: Tables role: manual tags: #manual #zettelmarkup #zettelstore syntax: zmk modified: 20210523185812 Tables are used to show some data in a two-dimenensional 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. Inside a cell, you can specify any [[inline elements|00001007040000]]. For example: ```zmk | a1 | a2 | a3| | b1 | b2 | b3 | c1 | c2 ``` will be rendered in HTML as: :::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 ``` will be rendered in HTML as: :::example | a1 | a2 |= a3| | b1 | b2 | b3 | c1 | c2 ::: === Column alignment Inside a header row, you can specify the alignment of each header cell by a given character as the last character of a cell. The alignment of a header cell determines the alignment of every cell in the same column. The following characters specify the alignment: * the colon character (""'':''"", ''U+003A'') forces a centered aligment, * the less-than sign character (""''<''"", ''U+0060'') specifies an alignment to the left, * the greater-than sign character (""''>''"", ''U+0062'') will produce right aligned cells. If no alignment character is given, a default alignment is used. For example: ```zmk |=Left<|Right>|Center:|Default |123456|123456|123456|123456| |
︙ | ︙ | |||
86 87 88 89 90 91 92 | |=Left<|Right>|Center:|Default |>R|:C|<L |123456|123456|123456|123456| |123|123|123|123 ::: === Rows to be ignored | | | | | | | | | 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 | |=Left<|Right>|Center:|Default |>R|:C|<L |123456|123456|123456|123456| |123|123|123|123 ::: === Rows to be ignored A line that begins with the sequence ''|%'' (vertical bar character (""''|''"", ''U+007C''), followed by a percent sign character (“%”, U+0025)) will be ignored. This allows to specify a horizontal rule that is not rendered. Such tables are emitted by some commands of the [[administrator console|00001004100000]]. For example, the command ``get-config place`` will emit ``` |=Key | Value | Description |%-----------+--------+----------------------------- | defdirtype | notify | Default directory place type ``` This is rendered in HTML as: :::example |=Key | Value | Description |%-----------+--------+----------------------------- | defdirtype | notify | Default directory place type ::: |
Deleted docs/manual/00001007031100.zettel.
|
| < < < < < < < < < < < < < < < < < < < < < < < < < |
Deleted docs/manual/00001007031110.zettel.
|
| < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < |
Deleted docs/manual/00001007031140.zettel.
|
| < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < |
Deleted docs/manual/00001007031200.zettel.
|
| < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < |
Deleted docs/manual/00001007031300.zettel.
|
| < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < |
Deleted docs/manual/00001007031400.zettel.
|
| < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < |
Changes to docs/manual/00001007040000.zettel.
1 2 | id: 00001007040000 title: Zettelmarkup: Inline-Structured Elements | < | < | | | | | | | | | | | | | | | | | | | | > > > > | | | < < < | > > > | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 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 | id: 00001007040000 title: Zettelmarkup: Inline-Structured Elements tags: #manual #zettelmarkup #zettelstore syntax: zmk role: manual 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 render 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 ==== Comments 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, you should escape it with a backslash. ==== Tag Any text that begins with a number sign character (""''#''"", ''U+0023''), followed by a non-empty sequence of Unicode letters, Unicode digits, the hyphen-minus character (""''-''"", ''U+002D''), or the low line character (""''_''"", ''U+005F'') is interpreted as an //inline tag//. They will be considered equivalent to tags in metadata. ==== Entities & more Sometimes it is not easy to enter special characters. If you know the Unicode code point of that character, or its name according to the [[HTML standard|https://html.spec.whatwg.org/multipage/named-characters.html]], you can enter it by number or by name. Regardless which method you use, an entity always begins with an ampersand character (""''&''"", ''U+0026'') and ends with a semicolon character (""'';''"", ''U+003B''). If you know the HTML name of the character you want to enter, place it between these two character. Example: ``&`` is rendered as ::&::{=example}. If you want to enter its numeric code point, a number sign character must follow the ampersand character, followed by digits to base 10. Example: ``&`` is rendered in HTML as ::&::{=example}. You also can enter its numeric code point as a hex number, if you place the letter ""x"" after the numeric sign character. Example: ``&`` is rendered in HTML as ::&::{=example}. Since some Unicode character are used quite often, a special notation is introduced for them: * Two consecutive hyphen-minus characters result in an //en-dash// character. It is typically used in numeric ranges. ``pages 4--7`` will be rendered in HTML as: ::pages 4--7::{=example}. Alternative specifications are: ``–``, ``&x8211``, and ``–``. * Three consecutive full stop characters (""''.''"", ''U+002E'') after a space result in an horizontal ellipsis character. ``to be continued ... later`` will be rendered in HTML as: ::to be continued, ... later::{=example}. Alternative specifications are: ``…``, ``&x8230``, and ``…``. |
Changes to docs/manual/00001007040100.zettel.
1 2 | id: 00001007040100 title: Zettelmarkup: Text Formatting | < | < | | > | > | | > | > > > | | > > | < < > | | | | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 | id: 00001007040100 title: Zettelmarkup: Text Formatting tags: #manual #zettelmarkup #zettelstore syntax: zmk role: manual 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 slash character (""''/''"", ''U+002F'') emphasizes its text. Often, such text is rendered in italics. If the default attribute is specified, the emphasized text is not just rendered as such, but also internally marked as emphasized. ** Example: ``abc //def// ghi`` is rendered in HTML as: ::abc //def// ghi::{=example}. ** Example: ``abc //def//{-} ghi`` is rendered in HTML as: ::abc //def//{-} ghi::{=example}. * The asterisk character (""''*''"", ''U+002A'') strongly emphasized its enclosed text. The text is often rendered in bold. Again, the default attribute will force a explicit semantic meaning of strong emphasizing. ** Example: ``abc **def** ghi`` is rendered in HTML as: ::abc **def** ghi::{=example}. ** Example: ``abc **def**{-} ghi`` is rendered in HTML as: ::abc **def**{-} ghi::{=example}. * The low line character (""''_''"", ''U+005F'') produces underlined text. If the default attribute was specified, it is semantically rendered as inserted text. ** Example: ``abc __def__ ghi`` is rendered in HTML as: ::abc __def__ ghi::{=example}. ** Example: ``abc __def__{-} ghi`` is rendered in HTML as: ::abc __def__{-} ghi::{=example}. * Similar, the tilde character (""''~''"", ''U+007E'') produces text that is strike-through. If the default attribute was specified, it is semantically rendered as deleted text. ** Example: ``abc ~~def~~ ghi`` is rendered in HTML as: ::abc ~~def~~ ghi::{=example}. ** Example: ``abc ~~def~~{-} ghi`` is rendered in HTML as: ::abc ~~def~~{-} ghi::{=example}. * The apostrophe character (""''\'''"", ''U+0027'') renders text in mono-space / fixed font width. ** Example: ``abc ''def'' ghi`` is rendered in HTML as: ::abc ''def'' ghi::{=example}. * The circumflex accent character (""''^''"", ''U+005E'') allows to enter superscripted text. ** Example: ``e=mc^^2^^`` is rendered in HTML as: ::e=mc^^2^^::{=example}. * The comma character (""'',''"", ''U+002C'') produces subscripted text. ** Example: ``H,,2,,O`` is rendered in HTML as: ::H,,2,,O::{=example}. * The less-than sign character (""''<''"", ''U+003C'') marks an inline quotation. ** Example: ``<<To be or not<<`` is rendered in HTML as: ::<<To be or not<<::{=example}. * The quotation mark character (""''"''"", ''U+0022'') produces the right typographic quotation marks 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 semicolon character (""'';''"", ''U+003B'') specifies text that should be rendered with small characters. ** Example: ``abc ;;def;; ghi`` is rendered in HTML as: ::abc ;;def;; ghi::{=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::{style=color:green} ghi`` is rendered in HTML as: abc ::def::{style=color:green} 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 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 | id: 00001007040200 title: Zettelmarkup: Literal-like formatting role: manual tags: #manual #zettelmarkup #zettelstore syntax: zmk modified: 20210525121114 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 Literal text somehow relates to [[verbatim blocks|00001007030500]]: their content should not be interpreted further, but may be rendered special. It is specified by two grave accent characters (""''`''"", ''U+0060''), followed by the text, followed by again two grave accent characters, optionally followed by an [[attribute|00001007050000]] specification. Similar to the verbatim block, the literal element allows also a modifier letter grave accent (""''ˋ''"", ''U+02CB'') as an alternative to the grave accent character[^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.]. However, all four characters must be the same. The literal element supports the default attribute: when given, all spaces in the text are rendered in HTML as open box characters (""''␣''"", ''U+2423''). The use of a generic attribute allwos to specify a (programming) language that controls syntax colouring when rendered in HTML. If you want to specify a grave accent character in the text, either use modifier grave accent characters as delimiters for the element, or put a backslash character before the grave accent character you want to use inside the element. If you want to enter a backslash character, you need to enter two of these. Examples: * ``\`\`abc def\`\``` is rendered in HTML as ::``abc def``::{=example}. * ``\`\`abc def\`\`{-}`` is rendered in HTML as ::``abc def``{-}::{=example}. * ``\`\`abc\\\`def\`\``` is rendered in HTML as ::``abc\`def``::{=example}. * ``\`\`abc\\\\def\`\``` is rendered in HTML as ::``abc\\def``::{=example}. === Keyboard input To mark text as input into a computer program, delimit your text with two plus sign characters (""''+''"", ''U+002B'') on each side. Example: * ``++STRG-C++`` renders in HTML as ::++STRG-C++::{=example}. * ``++STRG C++{-}`` renders in HTML as ::++STRG C++{-}::{=example}. Attributes can be specified, the default attribute has the same semantic as for literal text. === Computer output To mark text as output from a computer program, delimit your text with two equal sign characters (""''=''"", ''U+003D'') on each side. 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. |
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 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 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 | id: 00001007040300 title: Zettelmarkup: Reference-like text role: manual tags: #manual #zettelmarkup #zettelstore syntax: zmk 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. * Links to (external material). * Embed images that are stored within your Zettelstore. * Embed external images. * Reference via a footnote. * Reference via a citation key. * Put a mark within your zettel that you can reference later with a link. === Links 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''). The first form provides some text plus the link specification, delimited by a vertical bar character (""''|''"", ''U+007C''): ``[[text|linkspecification]]``. 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]]``. 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"". 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]]. If the URL begins with the slash character (""/"", ''U+002F''), or if it begins with ""./"" or with ""../"", i.e. without scheme, user info, and host name, the reference will be treated as a ""local reference"", otherwise as an ""external reference"". If the URL begins with two slash characters, it will be interpreted relative to the value of [[''url-prefix''|00001004010000#url-prefix]]. The text in the second form is just a sequence of inline elements. === Images To some degree, an image specification is conceptually not too far away from a link specification. Both contain a link specification and optionally some text. In contrast to a link, the link specification of an image must resolve to actual graphical image data. That data is read when rendered as HTML, and is embedded inside the zettel as an inline image. An image 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 link specification or some text, a vertical bar character and the link specification, similar to a link. One difference to a link: if the text was not given, an empty string is assumed. The link specification must reference a graphical image representation if the image is about to be rendered. 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/]] 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: * ``{{Spinning Emoji|00000000040001}}{title=Emoji width=30}`` is rendered as ::{{Spinning Emoji|00000000040001}}{title=Emoji width=30}::{=example}. * The above image is also a placeholder for a non-existent image: ** ``{{00000000000000}}`` will be rendered as ::{{00000000000000}}::{=example}. ** ``{{00000000009999}}`` will be rendered as ::{{00000000009999}}::{=example}. === Footnotes 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}. === Citation key A citation key references some external material that is part of a bibliografical 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. A right square bracket ends the text and the citation key element. === Mark A mark allows to name a point within a zettel. This is useful if you want to reference some content in a bigger-sized zettel[^Other uses of marks will be given, if Zettelmarkup is extended by a concept called //transclusion//.]. A mark begins with a left square bracket, followed by an exclamation mark character (""''!''"", ''U+0021''). Now the optional mark name follows. It is a (possibly empty) sequence of Unicode letters, Unicode digits, the hyphen-minus character (""''-''"", ''U+002D''), or the low-line character (""''_''"", ''U+005F''). The mark element ends with a right square bracket. Examples: * ``[!]`` is a mark without a name, the empty mark. * ``[!mark]`` is a mark with the name ""mark"". |
Deleted docs/manual/00001007040310.zettel.
|
| < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < |
Deleted docs/manual/00001007040320.zettel.
|
| < < < < < < < < < < < < < < < < < < < < < < < < < < < < |
Deleted docs/manual/00001007040322.zettel.
|
| < < < < < < < < < < < < < < < < < < < < < < < < < < < < |
Deleted docs/manual/00001007040324.zettel.
|
| < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < |
Deleted docs/manual/00001007040330.zettel.
|
| < < < < < < < < < < < < |
Deleted docs/manual/00001007040340.zettel.
|
| < < < < < < < < < < < < < < < |
Deleted docs/manual/00001007040350.zettel.
|
| < < < < < < < < < < < < < < < < < < < < < |
Changes to docs/manual/00001007050000.zettel.
1 2 | id: 00001007050000 title: Zettelmarkup: Attributes | < | | | | | | | < | | | | | | | | | < < < < < < < < < < < < < < < < < < < < < < < < < < < | | > | | | > > > > > > > > > > | > | > > > > > > > > > > > > > > > > | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 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 | id: 00001007050000 title: Zettelmarkup: Attributes tags: #manual #zettelmarkup #zettelstore syntax: zmk role: manual 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 raw text. Attributes are specified within curly brackets ``{...}``. Of course, more than one attribute can be specified. Attributes are separated by a sequence of space characters or by a comma character. An attribute normally consists of an optional key and an optional value. The key is a sequence of letters, digits, a hyphen-minus (""''-''"", ''U+002D'', and a low line / underscore (""''_''"", ''U+005D''). It can be empty. The value is a sequence of any character, except space and the right curly bracket (""''}''"", ''U+007D''). If the value must contain a space or the right curly bracket, the value can be specified within two quotation marks (""''"''"", ''U+0022''). Within the quotation marks, the backslash character functions as an escape character to specify the quotation mark (and the backslash character too). Some examples: * ``{key=value}`` sets the attribute //key// to value //value//. * ``{key="value with space"}`` sets the attribute to the given value. * ``{key="value with quote \\" (and backslash \\\\)"}`` * ``{name}`` sets the attribute //name//. It has no corresponding value. It is equivalent to ``{name=}``. * ``{=key}`` sets the //generic attribute//{-} to the given value. It is mostly used for modifying behaviour according to a programming language. * ``{.key}`` sets the //class attribute//{-} to the given value. It is equivalent to ``{class=key}``. In these examples, ``key`` must conform the the syntax of attribute keys, even if it is used as a value. If a key is given more than once in an attribute, the values are concatenated (and separated by a space). * ``{key=value1 key=value2}`` is the same as ``{key"value1 value2"}``. * ``{key key}`` is the same as ``{key}``. * ``{.class1 .class2}`` is equivalent to ``{class="class1 class2"}``. This is not true for the generic attribute. In ``{=key1 =key2}``, the first key is ignored. Therefore it is equivalent to ``{=key2}``. The key ""''-''"" (just hyphen-minus) is special. It is called //default attribute//{-} and has a markup specific meaning. For example, when used for raw text, it replaces the non-visible space with a visible representation: * ++``Hello, world``{-}++ produces ==Hello, world=={-}. * ++``Hello, world``++ produces ==Hello, world==. For some block elements, there is a syntax variant if you only want to specify a generic attribute. For all line-range blocks you can specify the generic attributes directly in the first line, after the three (or more) block characters. ``` :::attr ... ::: ``` is equivalent to ``` :::{=attr} ... ::: ```. For other blocks, the closing curly bracket must be on the same line where the block element begins. However, spaces are allowed between the blocks characters and the attributes. ``` === Heading {style=color:green} ``` is allowed and equivalent to ``` === Heading{style=color:green} ```. But ``` === Heading {style=color:green background=grey} ``` is not allowed. Same for ``` === Heading {style=color:" green"} ```. For inlines, the attributes must immediately follow the inline markup. However, the attributes may be continued on the next line when a space or line ending character is possible. ``::GREEN::{style=color:green}`` is allowed, but not ``::GREEN:: {style=color:green}``. ``` ::GREEN::{style=color:green background=grey} ``` is allowed, but not ``` ::GREEN::{style=color: green} ```. However, ``` ::GREEN::{style=color:" green"} ``` is allowed, because line endings are allowed within quotes. === Reference material * [[Supported attribute values for natural languages|00001007050100]] * [[Supported attribute values for programming languages|00001007050200]] |
Changes to docs/manual/00001007050100.zettel.
1 2 | id: 00001007050100 title: Zettelmarkup: Supported Attribute Values for Natural Languages | < | < | > > > > > > > | 1 2 3 4 5 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: 00001007050100 title: Zettelmarkup: Supported Attribute Values for Natural Languages tags: #manual #reference #zettelmarkup #zettelstore syntax: zmk role: manual With an [[attribute|00001007050000]] it is possible to specify the natural language of a text region. This is important, if you want to render your markup into an environment, where this is significant. HTML is such an environment. To specify the language within an attribute, you must use the key ''lang''. The language itself is specified according to the language definition of [[RFC-5646|https://tools.ietf.org/html/rfc5646]]. Examples: * ``{lang=en}`` for the english language * ``{lang=en-us}`` for the english dialect spoken in the United States of America * ``{lang=de}`` for the german language * ``{lang=de-at}`` for the german language dialect spoken in Austria * ``{lang=de-de}`` for the german language dialect spoken in Germany The actual [[typographic quotations marks|00001007040100]] (``""...""``) are derived from the current language. The language of a zettel (meta key ''lang'') or of the whole Zettelstore (''default-lang'' of the [[configuration zettel|00001004020000#default-lang]]) can be overwritten by an attribute: ``""...""{lang=fr}``{=zmk}. Currently, Zettelstore supports the following primary languages: * ''de'' * ''en'' * ''fr'' These are used, even if a dialect was specified. |
Added docs/manual/00001007060000.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 | id: 00001007060000 title: Zettelmarkup: Summary of Formatting Characters tags: #manual #reference #zettelmarkup #zettelstore syntax: zmk role: manual The following table gives an overview about the use of all characters that begin a markup element. |= Character :|= Blocks <|= Inlines < | ''!'' | (free) | (free) | ''"'' | [[Verse block|00001007030700]] | [[Typographic quotation mark|00001007040100]] | ''#'' | [[Ordered list|00001007030200]] | [[Tag|00001007040000]] | ''$'' | (reserved) | (reserved) | ''%'' | [[Comment block|00001007030900]] | [[Comment|00001007040000]] | ''&'' | (free) | [[Entity|00001007040000]] | ''\''' | (free) | [[Monospace text|00001007040100]] | ''('' | (free) | (free) | '')'' | (free) | (free) | ''*'' | [[Unordered list|00001007030200]] | [[strongly emphasized / bold text|00001007040100]] | ''+'' | (free) | [[Keyboard input|00001007040200]] | '','' | (free) | [[Subscripted text|00001007040100]] | ''-'' | [[Horizonal rule|00001007030400]] | ""[[en-dash|00001007040000]]"" | ''.'' | (free) | [[Horizontal ellipsis|00001007040000]] | ''/'' | (free) | [[Emphasized / italics text|00001007040100]] | '':'' | [[Region block|00001007030800]] / [[description text|00001007030100]] | [[Inline region|00001007040100]] | '';'' | [[Description term|00001007030100]] | [[Small text|00001007040100]] | ''<'' | [[Quotation block|00001007030600]] | [[Short inline quote|00001007040100]] | ''='' | [[Headings|00001007030300]] | [[Computer output|00001007040200]] | ''>'' | [[Quotation lists|00001007030200]] | (blocked: to remove anyambiguity with quotation lists) | ''?'' | (free) | (free) | ''@'' | (free) | (reserved) | ''['' | (reserved) | [[Link|00001007040300]], [[citation key|00001007040300]], [[footnote|00001007040300]], [[mark|00001007040300]] | ''\\'' | (blocked by inline meaning) | [[Escape character|00001007040000]] | '']'' | (reserved) | End of link, citation key, footnote, mark | ''^'' | (free) | [[Superscripted text|00001007040100]] | ''_'' | (free) | [[Underlined text|00001007040100]] | ''`'' | [[Verbatim block|00001007030500]] | [[Literal text|00001007040200]] | ''{'' | (reserved) | [[Image|00001007040300]], [[Attribute|00001007050000]] | ''|'' | [[Table row / table cell|00001007031000]] | Separator within link and image formatting | ''}'' | (reserved) | End of Image, End of Attribute | ''~'' | (free) | [[Strike-through text|00001007040100]] |
Deleted docs/manual/00001007700000.zettel.
|
| < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < |
Deleted docs/manual/00001007701000.zettel.
|
| < < < < < < < < < < < < < < < < < < < < < < < < < < |
Deleted docs/manual/00001007702000.zettel.
|
| < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < |
Deleted docs/manual/00001007705000.zettel.
|
| < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < |
Deleted docs/manual/00001007706000.zettel.
|
| < < < < < < < < < < |
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.
|
| < < < < < < < < < < < < < < < < < < < < < |
Deleted docs/manual/00001007780000.zettel.
|
| < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < |
Deleted docs/manual/00001007790000.zettel.
|
| < < < < < < < < < < < < < < < < < < < |
Deleted docs/manual/00001007800000.zettel.
|
| < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < |
Deleted docs/manual/00001007900000.zettel.
|
| < < < < < < < < < < |
Deleted docs/manual/00001007903000.zettel.
|
| < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < |
Deleted docs/manual/00001007906000.zettel.
|
| < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < |
Deleted docs/manual/00001007990000.zettel.
|
| < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < |
Changes to docs/manual/00001008000000.zettel.
1 2 3 4 5 | id: 00001008000000 title: Other Markup Languages role: manual tags: #manual #zettelstore syntax: zmk | < | | | | | < < | < < < | < < < < < < | > > | | | | < < | | | < < | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 | id: 00001008000000 title: Other Markup Languages role: manual tags: #manual #zettelstore syntax: zmk modified: 20210523194915 [[Zettelmarkup|00001007000000]] is not the only markup language you can use to define your content. Zettelstore is quite agnostic with respect to markup languages. Of course, Zettelmarkup plays an important role. However, with the exception of zettel titles, you can use any (markup) language that is supported: * CSS * HTML template data * Image formats: GIF, PNG, JPEG, SVG * Markdown * Plain text, not further interpreted The [[metadata key|00001006020000#syntax]] ""''syntax''"" specifies which language should be used. If it is not given, the key ""''default-syntax''"" will be used (specified in the [[configuration zettel|00001004020000#default-syntax]]). The following syntax values are supported: ; [!css]''css'' : A [[Cascading Style Sheet|https://www.w3.org/Style/CSS/]], to be used when rendering a zettel as HTML. ; [!gif]''gif''; [!jpeg]''jpeg''; [!jpg]''jpg''; [!png]''png'' : The formats for pixel graphics. Typically the data is stored in a separate file and the syntax is given in the ''.meta'' file. ; [!markdown]''markdown'', [!md]''md'' : For those who desperately need [[Markdown|https://daringfireball.net/projects/markdown/]]. Since the world of Markdown is so diverse, a [[CommonMark|https://commonmark.org/]] parser is used. See [[Use Markdown within Zettelstore|00001008010000]]. ; [!mustache]''mustache'' : A [[Mustache template|https://mustache.github.io/]], used when rendering a zettel as HTML. ; [!none]''none'' : Only the metadata of a zettel is ""parsed"". Useful for displaying the full metadata. The [[runtime configuration zettel|00000000000100]] uses this syntax. The zettel content is ignored. ; [!svg]''svg'' : A [[Scalable Vector Graphics|https://www.w3.org/TR/SVG2/]]. The icon for external material is an example. ; [!text]''text'', [!plain]''plain'', [!txt]''txt'' : Just plain text that must not be interpreted further. ; [!zmk]''zmk'' : [[Zettelmarkup|00001007000000]]. If you specify something else, your content will be interpreted as plain text. |
Changes to docs/manual/00001008010000.zettel.
1 2 3 4 5 | id: 00001008010000 title: Use Markdown within Zettelstore role: manual tags: #manual #markdown #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 | id: 00001008010000 title: Use Markdown within Zettelstore role: manual tags: #manual #markdown #zettelstore syntax: zmk modified: 20210518102155 If you are customized to use Markdown as your markup language, you can configure Zettelstore to support your decision. Zettelstore supports [[CommonMark|https://commonmark.org/]], an [[attempt|https://xkcd.com/927/]] to unify all the different, divergent dialects of Markdown. === Use Markdown as the default markup language of Zettelstore Add the key ''default-syntax'' with a value of ''md'' or ''markdown'' to the [[configuration zettel|00000000000100]]. Whether to use ''md'' or ''markdown'' is not just a matter to taste. It also depends on the value of [[''zettel-file-syntax''|00001004020000#zettel-file-syntax]] and, to some degree, on the value of [[''yaml-header''|00001004020000#yaml-header]]. If you set ''yaml-header'' to true, then new content is always stored in a file with the extension ''.zettel''. Otherwise ''zettel-file-syntax'' lists all syntax values, where its content should be stored in a file with the extension ''.zettel''. If neither ''yaml-header'' nor ''zettel-file-syntax'' is set, new content is stored in a file where its file name extension is the same as the syntax value of that zettel. In this case it makes a difference, whether you specify ''md'' or ''markdown''. If you specify the syntax ''md'', your content will be stored in a file with the ''.md'' extension. Similar for the syntax ''markdown''. If you want to process the files that store the zettel content, e.g. with some other Markdown tools, this may be important. Not every Markdown tool allows both file extensions. BTW, metadata is stored in a file with the extension ''.meta'', if neither ''yaml-header'' nor ''zettel-file-syntax'' is set. === Security aspects You should be aware that Markdown is a superset of HTML. Any HTML code is valid Markdown code. If you write your own zettel, this is probably not a problem. However, if you receive zettel from others, you should be careful. An attacker might include malicious HTML code in your zettel. For example, HTML allows to embed JavaScript, a full-sized programming language that drives many web sites. When a zettel is displayed, JavaScript code might be executed, sometimes with harmful results. Zettelstore mitigates this problem by ignoring suspicious text when it encodes a zettel as HTML. Any HTML text that might contain the ``<script>`` tag or the ``<iframe>`` tag is ignored. This may lead to unexpected results if you depend on these. Other encoding [[formats|00001012920500]] may still contain the full HTML text. Any external client of Zettelstore, which does not use Zettelstore's HTML encoding, must be programmed to take care of malicious code. |
Deleted docs/manual/00001008010500.zettel.
|
| < < < < < < < < < < < < < < < < < < < < < < < < < < < < < |
Deleted docs/manual/00001008050000.zettel.
|
| < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < |
Changes to docs/manual/00001010000000.zettel.
1 2 3 4 5 | id: 00001010000000 title: Security role: manual tags: #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 | id: 00001010000000 title: Security role: manual tags: #configuration #manual #security #zettelstore syntax: zmk 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 your 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.]. |
︙ | ︙ | |||
37 38 39 40 41 42 43 | 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. | | | | | 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 | 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. Additionally, you can specify that a zettel is publicily 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, [[authentication 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 | id: 00001010040100 title: Enable authentication role: manual tags: #authentication #configuration #manual #security #zettelstore syntax: zmk 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. |
Changes to docs/manual/00001010040200.zettel.
1 2 | id: 00001010040200 title: Creating an user 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 | id: 00001010040200 title: Creating an user zettel tags: #authentication #configuration #manual #security #zettelstore syntax: zmk role: manual 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 three 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. ; ''role'' : Must contain the value ""user"". The title of the zettel typically specifies the real name of the user. 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 with the role ""user"". # 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 | id: 00001010040400 title: Authentication process | < | > | | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 | id: 00001010040400 title: Authentication process tags: #authentication #configuration #manual #security #zettelstore syntax: zmk role: manual 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 role ''user'', authentication fails. # 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. If they do not match, authentication fails. The authentication is successful, because the Zettelstore has an owner, the identifier matches a user zettel, and the password conforms to the stored credential. |
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 | id: 00001010040700 title: Access token role: manual tags: #authentication #configuration #manual #security #zettelstore syntax: zmk 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 interface, 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-timeout-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 | id: 00001010070200 title: Visibility rules for zettel 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 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 | id: 00001010070200 title: Visibility rules for zettel role: manual tags: #authorization #configuration #manual #security #zettelstore syntax: zmk modified: 20210510194652 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. ; [!login]""login"" : Only an authenticated user can access the zettel. This is the default value for [[''default-visibility''|00001004020000#default-visibility]]. ; [!owner]""owner"" : Only the owner of the Zettelstore can access the zettel. This is for zettel with sensitive content, e.g. the [[configuration zettel|00001004020000]] or the various zettel that contains the templates for rendering zettel in HTML. ; [!expert]""expert"" : Only the owner of the Zettelstore can access the zettel, if runtime configuration [[''expert-mode''|00001004020000#expert-mode]] is set to a boolean true value. 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 have visibility ""public"". One is the zettel that contains CSS for displaying the web interface. This is to ensure that the web interface looks nice even for not authenticated users. Another is the zettel containing the [[version|00000000000001]] of the Zettelstore. Yet other zettel lists the Zettelstore [[license|00000000000004]], its [[contributors|00000000000005]], and external, licensed [[dependencies|00000000000006]], such as program code written by others or graphics designed by others. The [[default image|00000000040001]], used if an image reference is invalid,is also public visible. Please note: if authentication is not enabled, 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 13 14 15 16 17 18 19 20 21 22 23 24 25 | id: 00001010070300 title: User roles role: manual tags: #authorization #configuration #manual #security #zettelstore syntax: zmk Every user is associated with some basic privileges. These are specified in the user zettel with the key ''user-role''. The following values are supported: ; ""reader"" : The user is allowed to read zettel. This is the default value for any user except the owner of the Zettelstore. ; ""writer"" : The user is allowed to create new zettel and to change existing zettel. There are two other user roles, implicitly defined: ; The anonymous user : This role is assigned to any user that is not authenticated. Can only read zettel with visibility [[public|00001010070200]], but cannot change them. ; The owner : The user that is configured to be the owner of the Zettelstore. Does not need to specify a user role in its user zettel. Is not restricted in the use of Zettelstore, except when a zettel is marked as [[read-only|00001006020400]]. |
Changes to docs/manual/00001010070400.zettel.
1 2 | id: 00001010070400 title: Authorization and read-only mode | < | | | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 | id: 00001010070400 title: Authorization and read-only mode tags: #authorization #configuration #manual #security #zettelstore syntax: zmk role: manual 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 | id: 00001010070600 title: Access rules | < | | | < | | | | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 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: 00001010070600 title: Access rules tags: #authorization #configuration #manual #security #zettelstore syntax: zmk role: manual Whether an operation of the Zettelstore is allowed or rejected, depends on various factors. The following rules are checked first, in this order: # In read-only mode, every operation except the ""Read"" operation is rejected. # If there is no owner, authentication is disabled and every operation is allowed for everybody. # If the user is authenticated and it is the owner, then the operation is allowed. In the second step, when authentication is enabled and the requesting user is not the owner, everything depends on the requested operation. * Read a zettel: ** If the visibility is ""public"", the access is granted. ** If the visibility is ""owner"", the access is rejected. ** If the user is not authenticated, access is rejected. ** If the zettel requested is an user zettel, reject the access if the users identification is not the same as of the ''ident'' meta key in the zettel. In other words: only the requesting user is allowed to access its own user zettel. ** Otherwise the user is authenticated, no sensitive zettel is requested. Allow to read the zettel. * Create a new zettel ** If the user is not authenticated, reject the access. ** If the ''user-role'' of the user is ""reader"", reject the access. ** If the user tries to create an user zettel, the access is rejected. Only the owner is allowed to create user zettel. ** In all other cases allow to create the zettel. * Change an existing zettel ** If the user is not allowed to read the zettel (see above), reject the access. ** If the user is not authenticated, reject the access. ** If the zettel is the user zettel 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 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 | id: 00001010090100 title: External server to encrypt message transport role: manual tags: #configuration #encryption #manual #security #zettelstore syntax: zmk modified: 20210511131719 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. Technically, this is not trivial. Any client who wants to communicate with your Zettelstore must trust the public encryption key. Otherwise the client cannot be sure that it is communication with your Zettelstore. This problem is solved in part with [[Let's Encrypt|https://letsencrypt.org/]], <<a free, automated, and open certificate authority (CA), run for the public’s benefit. It is a service provided by the [[Internet Security Research Group|https://www.abetterinternet.org/]]<<. Alternatively, you can buy these keys for public-key encryption at ""certificate authorities"" or its dealers. === Server software for encryption The solution of placing a server for encryption in front of an encryption-unaware server is a relatively old one. There are many different alternatives to choose. First, there are web servers. Business-grade web servers must enable encryption. Most of them allow to forward a request unencrypted to another web server. Some examples: * [[Apache Web Server|https://httpd.apache.org/]]: enable [[mod_proxy|http://httpd.apache.org/docs/current/mod/mod_proxy.html]] and configure a reverse proxy. * [[nginx|https://nginx.org/]]: set-up a reverse proxy with the [[''proxy_pass''|https://nginx.org/en/docs/http/ngx_http_proxy_module.html#proxy_pass]] directive. * [[Caddy|https://caddyserver.com/]]: see below for details. Other software is also possible. There exists software dedicated for this task of handling the encryption part. Some examples: * [[stunnel|https://www.stunnel.org/]] (<<a proxy designed to add TLS encryption functionality to existing clients and servers without any changes in the programs' code.<<) * [[Traefik|https://traefik.io/]]: set-up a [[router|https://docs.traefik.io/routing/routers/]]. === Example configuration for Caddy For the inexperienced owner of a Zettelstore, [[Caddy|https://caddyserver.com/]] is a good option[^In fact, the [[server-based installation procedure|00001003000000]] of Zettelstore was inspired by Caddy.]. Caddy has the capability to automatically fetch appropriately encryption key from Let's Encrypt, without any further configuration. The only requirement of doing this is that the server must be publicly accessible. |
︙ | ︙ | |||
63 64 65 66 67 68 69 | } } ``` This will forwards requests with the prefix ""/manual/"" to the running Zettelstore. All other requests will be handled by Caddy itself. In this case you must specify the [[startup configuration key ''url-prefix''|00001004010000#url-prefix]] with the value ""/manual/"". | | | 63 64 65 66 67 68 69 70 | } } ``` This will forwards requests with the prefix ""/manual/"" to the running Zettelstore. All other requests will be handled by Caddy itself. In this case you must specify the [[startup configuration key ''url-prefix''|00001004010000#url-prefix]] with the value ""/manual/"". This is to allow Zettelstore ignore the prefix while reading web requests and to give the correct URLs with the given prefix when sending a web response. |
Changes to docs/manual/00001012000000.zettel.
1 2 3 4 5 | id: 00001012000000 title: API 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 | id: 00001012000000 title: API role: manual tags: #api #manual #zettelstore syntax: zmk The API (short for ""**A**pplication **P**rogramming **I**nterface"") is the primary way to communicate with a running Zettelstore. Most integration with other systems and services is done through the API. The [[web user interface|00001014000000]] is just an alternative, secondary way of interacting with a Zettelstore. === Background The API is HTTP-based and uses JSON as its main encoding format for exchanging messages between a Zettelstore and its client software. There is an [[overview zettel|00001012920000]] that shows the structure of the endpoints used by the API and gives an indication about its use. While JSON is the main encoding format, it is possible to retrieve zettel representations in other formats. If you want to create a new zettel or to change an existing one, you have to use JSON. There is an [[overview zettel for encoding formats|00001012920500]] that describes the valid formats. Various parts of a zettel can be retrieved. There are the [[possible values to specify zettel parts|00001012920800]]. === 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 metadata of all zettel|00001012051200]] * [[List all zettel, but in different encoding formats|00001012051400]] * [[List all zettel, but include different parts of a zettel|00001012051600]] * [[Shape the list of zettel metadata with filter options|00001012051800]] * [[Sort the list of zettel metadata|00001012052000]] * [[List all tags|00001012052200]] * [[List all roles|00001012052400]] === Working with zettel * Create a new zettel * [[Retrieve metadata and content of an existing zettel|00001012053400]] * [[Retrieve references of an existing zettel|00001012053600]] * [[Retrieve context of an existing zettel|00001012053800]] * [[Retrieve zettel order within an existing zettel|00001012054000]] * Update metadata and content of a zettel * Rename a zettel * Delete a zettel |
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 | id: 00001012050200 title: API: Authenticate a client role: manual tags: #api #manual #zettelstore syntax: zmk Authentication for future API calls is done by sending a [[user identification|00001010040200]] and a password to the Zettelstore to obtain an [[access token|00001010040700]]. This token has to be used for other API calls. It is valid for a relatively short amount of time, as configured with the key ''token-timeout-api'' of the [[startup configuration|00001004010000]] (typically 10 minutes). 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. === 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'' : Authentication is not active. |
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 | id: 00001012050400 title: API: Renew an access token role: manual tags: #api #manual #zettelstore syntax: zmk An access token is only valid for a certain duration. Since the [[authentication process|00001012050200]] will need some processing time, there is a way to renew the token without providing full authentication data. 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. === 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 authorization was not [[enabled|00001010040100]], or the 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 | id: 00001012050600 title: API: Provide an access token | < | | | | | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 | id: 00001012050600 title: API: Provide an access token tags: #api #manual #zettelstore syntax: zmk role: manual 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. For example (raw HTTP): ``` GET /z HTTP/1.0 Authorization: Bearer eyJhbGciOiJIUzUxMiJ9.eyJfdGsiOjEsImV4cCI6MTYwMTczMTI3NSwiaWF0IjoxNjAxNzMwNjc1LCJzdWIiOiJhYmMiLCJ6aWQiOiIyMDIwMTAwMzE1MDEwMCJ9.ekhXkvn146P2bMKFQcU-bNlvgbeO6sS39hs6U5EKfjIqnSInkuHYjYAIfUqf_clYRfr6YBlX5izii8XfxV8jhg ``` Note, that there is exactly one space character (''U+0020'') between the string ""Bearer"" and the access token: ``Authorization: Bearer eyJhbGciOiJIUzUxMiJ9.ey...``{-}. If you use the [[curl|https://curl.haxx.se/]] tool, you can use the ++-H++ command line parameter to set this header field. |
Changes to docs/manual/00001012051200.zettel.
1 | id: 00001012051200 | | < < | < | < < < < < < | < < > > > | > | > > > > | > > > > > > | > > > > | > > > > | < > | < < < < > > > > > > > | < < < > > | | < | > | | < < | < < < < < > > > | < > > > > > > | < < < < < < < < | < > > > | | | > < > > | | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 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: 00001012051200 title: API: List metadata of all zettel role: manual tags: #api #manual #zettelstore syntax: zmk To list the metadata of 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]. If successful, the output is a JSON object: ```sh # curl http://127.0.0.1:23123/z {"list":[{"id":"00001012051200","url":"/z/00001012051200","meta":{"title":"API: Renew an access token","tags":"#api #manual #zettelstore","syntax":"zmk","role":"manual"}},{"id":"00001012050600","url":"/z/00001012050600","meta":{"title":"API: Provide an access token","tags":"#api #manual #zettelstore","syntax":"zmk","role":"manual"}},{"id":"00001012050400","url":"/z/00001012050400","meta":{"title":"API: Renew an access token","tags":"#api #manual #zettelstore","syntax":"zmk","role":"manual"}},{"id":"00001012050200","url":"/z/00001012050200","meta":{"title":"API: Authenticate a client","tags":"#api #manual #zettelstore","syntax":"zmk","role":"manual"}},{"id":"00001012000000","url":"/z/00001012000000","meta":{"title":"API","tags":"#api #manual #zettelstore","syntax":"zmk","role":"manual"}}]} ``` The JSON object contains a key ''"list"'' where its value is a list of zettel JSON objects. These zettel JSON objects themself contains the keys ''"id"'' (value is a string containing the zettel identifier), ''"url"'' (value is a string containing the URL of the zettel), and ''"meta"'' (value ss a JSON object). The vlaue 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. If you reformat the JSON output from the ''GET /z'' call, you'll see its structure better: ```json { "list": [ { "id": "00001012051200", "url": "/z/00001012051200", "meta": { "title": "API: List for all zettel some data", "tags": "#api #manual #zettelstore", "syntax": "zmk", "role": "manual" } }, { "id": "00001012050600", "url": "/z/00001012050600", "meta": { "title": "API: Provide an access token", "tags": "#api #manual #zettelstore", "syntax": "zmk", "role": "manual" } }, { "id": "00001012050400", "url": "/z/00001012050400", "meta": { "title": "API: Renew an access token", "tags": "#api #manual #zettelstore", "syntax": "zmk", "role": "manual" } }, { "id": "00001012050200", "url": "/z/00001012050200", "meta": { "title": "API: Authenticate a client", "tags": "#api #manual #zettelstore", "syntax": "zmk", "role": "manual" } }, { "id": "00001012000000", "url": "/z/00001012000000", "meta": { "title": "API", "tags": "#api #manual #zettelstore", "syntax": "zmk", "role": "manual" } } ] } ``` In this special case, the metadata of each zettel just contains the four default keys ''title'', ''tags'', ''syntax'', and ''role''. If you specify the request in above simple way, you will always get a JSON object that contains all zettel maintained by your Zettelstore and which contains just the metadata of each zettel. There are several ways to change this behavior. * [[Specify a different encoding format|00001012051400]] * [[Specify which detail information of a zettel should be included|00001012051600]] === 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 | id: 00001012051400 | | < | < < < | < < < | < | < < | < | | < < < < < < < | < | < > | < < < < < > | | < < < < < | < | < < | < > > | > > > | < > > | < < | | < < > | < < < | | < < | < | | < < < | < < < < < | < < > > | > > > > | < | | | < < | | | > > | | | | | < < < < < | < < < < < < < | < < < < > | < < < < < | < > > > | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 | id: 00001012051400 title: API: List all zettel, but in different encoding formats tags: #api #manual #zettelstore syntax: zmk role: manual You can add a query parameter ''_format=[[FORMAT|00001012920500]]'' to select the encoding format when [[retrieving all zettel|00001012051200]]. Probably some formats are not very useful and may not make sense. Currently implemented are the formats [[''json''|00001012920501]] (default), [[''djson''|00001012920503]], and [[''html''|00001012920510]]. The format ''json'' will be selected, if no ''_format'' is specified. ''djson'' will return a JSON object with some more detailed information. For example, the [[Zettelmarkup|00001007000000]] structure will be returned and tags are returned as a list of strings. ''html'' will return an HTML encoding of the zettel list, using a HTML unnumbered list. ```sh # curl 'http://127.0.0.1:23123/z?_format=djson' {"list":[{"id":"00001012051400","url":"/z/00001012051400?_format=djson","meta":{"title":[{"t":"Text","s":"API:"},{"t":"Space"},{"t":"Text","s":"List"},{"t":"Space"},{"t":"Text","s":"all"},{"t":"Space"},{"t":"Text","s":"zettel,"},{"t":"Space"},{"t":"Text","s":"but"},{"t":"Space"},{"t":"Text","s":"in"},{"t":"Space"},{"t":"Text","s":"different"},{"t":"Space"},{"t":"Text","s":"encoding"},{"t":"Space"},{"t":"Text","s":"formats"}],"tags":["#api","#manual","#zettelstore"],"syntax":"zmk","role":"manual"}}, ... ``` Or reformatted: ````json { "list": [ { "id": "00001012051400", "url": "/z/00001012051400?_format=djson", "meta": { "title": [ { "t": "Text", "s": "API:" }, { "t": "Space" }, { "t": "Text", "s": "List" }, { "t": "Space" }, { "t": "Text", "s": "all" }, { "t": "Space" }, { "t": "Text", "s": "zettel," }, ... ```` ```sh # curl 'http://127.0.0.1:23123/z?_format=html' <html lang="en"> <body> <ul> <li><a href="/z/00001012051400?_format=html">API: List all zettel, but in different encoding formats</a></li> <li><a href="/z/00001012051200?_format=html">API: List metadata of all zettel</a></li> <li><a href="/z/00001012050600?_format=html">API: Provide an access token</a></li> <li><a href="/z/00001012050400?_format=html">API: Renew an access token</a></li> <li><a href="/z/00001012050200?_format=html">API: Authenticate a client</a></li> <li><a href="/z/00001012000000?_format=html">API</a></li> </ul> </body> </html>``` If you request a JSON-based encoding format, you can request additional [[parts of a zettel|00001012051600]]. === 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 you provided a wrong value for ''_format''. ; ''501'' : Encoding format is not yet implemented, but will be in the future. |
Changes to docs/manual/00001012051600.zettel.
1 | id: 00001012051600 | | < | < < > > > | < < < | < < < < < < < > > | > > | < | < < > | | < > | | < > > | < | > > | > | > | < | < < | > > | > > | | < > | < | < < < < < | > < < < | < | > | < < < < > > > > | < | | > | > | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 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 | id: 00001012051600 title: API: List all zettel, but include different parts of a zettel tags: #api #manual #zettelstore syntax: zmk role: manual For JSON-based formats[^[[''json''|00001012920501]] and [[''djson''|00001012920503]]] you can add a query parameter ''_part=[[PART|00001012920800]]'' to select which parts of a zettel must be encoded. All allowed parts are implemented. The JSON keys ''"id"'' and ''"url"'' are always included. The default value is ''_part=meta''. If you just want to know the zettel identifier and the zettel url, specify ''_part=id'': ```sh # curl 'http://127.0.0.1:23123/z?_format=djson&_part=id' {"list":[{"id":"00001012051600","url":"/z/00001012051600?_format=djson"},{"id":"00001012051400","url":"/z/00001012051400?_format=djson"},{"id":"00001012051200","url":"/z/00001012051200?_format=djson"},{"id":"00001012050600","url":"/z/00001012050600?_format=djson"},{"id":"00001012050400","url":"/z/00001012050400?_format=djson"},{"id":"00001012050200","url":"/z/00001012050200?_format=djson"},{"id":"00001012000000","url":"/z/00001012000000?_format=djson"}]} ``` Or reformatted: ````json { "list": [ { "id": "00001012051600", "url": "/z/00001012051600?_format=djson" }, { "id": "00001012051400", "url": "/z/00001012051400?_format=djson" }, { "id": "00001012051200", "url": "/z/00001012051200?_format=djson" }, { "id": "00001012050600", "url": "/z/00001012050600?_format=djson" }, { "id": "00001012050400", "url": "/z/00001012050400?_format=djson" }, { "id": "00001012050200", "url": "/z/00001012050200?_format=djson" }, { "id": "00001012000000", "url": "/z/00001012000000?_format=djson" } ] } ```` A value ''_part=content'' will include the zettel content. If ''_format=json'', the content is a string value. For ''_format=djson'', a detailed JSON value will be returned. ```sh # curl 'http://127.0.0.1:23123/z?_part=content' {"list":[{"id":"00001012051600","url":"/z/00001012051600","content":"For JSON-based formats[^[[''json''|00001012920501]] and [[''djson''|00001012920503]]] you can add a query parameter ''_part=[[PART|00001012920800]]'' to select the encoding format when [[retrieving all zettel|00001012051200]].\n\nAll given parts are implemented.\nThe JSON keys ''\"id\"'' ... ``` The value ''_part=zettel'' will include both the metadata of a zettel //and// its zettel content. Metadata will additionally include all autogenerated default values, such as the key ''"copyright"'' and ''"license"''. === 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 you provided a wrong value for ''_part''. |
Changes to docs/manual/00001012051800.zettel.
1 | id: 00001012051800 | | < | > | > > | < > > > < | > > > > > | | < < < < | < > | | | | | | | < > | < | > | > > > > > | | | | < > | | | > > > > | > > > > > < > < | | | | | < > > > > < < > | > > | > > | < > | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 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 | id: 00001012051800 title: API: Shape the list of zettel metadata with filter options role: manual tags: #api #manual #zettelstore syntax: zmk modified: 20210510150129 In most cases, it is not essential to list //all// zettel. Typically, you are interested only in a subset of the zettel maintained by your Zettelstore. This is done by adding some query parameters to the general ''GET /z'' request. === Filter Every query parameter that does //not// begin with the low line character (""_"", ''U+005F'') is treated as the name of a [[metadata|00001006010000]] key. According to the [[type|00001006030000]] of a metadata key, zettel are matched and therefore filtered. All [[supported|00001006020000]] metadata keys have a well-defined type. User-defined keys have the type ''e'' (string, possibly empty). For example, if you want to retrieve all zettel that contain the string ""API"" in its title, your request will be: ```sh # curl 'http://127.0.0.1:23123/z?title=API' {"list":[{"id":"00001012921000","url":"/z/00001012921000","meta":{"title":"API: JSON structure of an access token","tags":"#api #manual #reference #zettelstore","syntax":"zmk","role":"manual"}},{"id":"00001012920500","url":"/z/00001012920500","meta":{"title":"Formats available by the API","tags":"#api #manual #reference #zettelstore","syntax":"zmk","role":"manual"}},{"id":"00001012920000","url":"/z/00001012920000","meta":{"title":"Endpoints used by the API","tags":"#api #manual #reference #zettelstore","syntax":"zmk","role":"manual"}}, ... ``` However, if you want all zettel that does //not// match a given value, you must prefix the value with the exclamation mark character (""!"", ''U+0021''). For example, if you want to retrieve all zettel that do not contain the string ""API"" in their title, your request will be: ```sh # curl 'http://127.0.0.1:23123/z?title=!API' {"list":[{"id":"00010000000000","url":"/z/00010000000000","meta":{"back":"00001003000000 00001005090000","backward":"00001003000000 00001005090000","copyright":"(c) 2020-2021 by Detlef Stern <ds@zettelstore.de>","forward":"00000000000001 00000000000003 00000000000096 00000000000100","lang":"en","license":"EUPL-1.2-or-later","role":"zettel","syntax":"zmk","title":"Home"}},{"id":"00001014000000","url":"/z/00001014000000","meta":{"back":"00001000000000 00001004020000 00001012920510","backward":"00001000000000 00001004020000 00001012000000 00001012920510","copyright":"(c) 2020-2021 by Detlef Stern <ds@zettelstore.de>","forward":"00001012000000","lang":"en","license":"EUPL-1.2-or-later","published":"00001014000000","role":"manual","syntax":"zmk","tags":"#manual #webui #zettelstore","title":"Web user interface"}}, ... ``` The empty query parameter values matches all zettel that contain the given metadata key. Similar, if you specify just the exclamation mark character as a query parameter value, only those zettel match that does //not// contain the given metadata key. For example ``curl 'http://localhost:23123/z?back=!&backward='`` returns all zettel that are reachable via other zettel, but also references these zettel. === Output only specific parts of a zettel If you are just interested in the zettel identifier, you should add the ""''_part''"" query parameter: ```sh # curl 'http://127.0.0.1:23123/z?title=API&_part=id' {"list":[{"id":"00001012921000","url":"/z/00001012921000"},{"id":"00001012920500","url":"/z/00001012920500"},{"id":"00001012920000","url":"/z/00001012920000"},{"id":"00001012051800","url":"/z/00001012051800"},{"id":"00001012051600","url":"/z/00001012051600"},{"id":"00001012051400","url":"/z/00001012051400"},{"id":"00001012051200","url":"/z/00001012051200"},{"id":"00001012050600","url":"/z/00001012050600"},{"id":"00001012050400","url":"/z/00001012050400"},{"id":"00001012050200","url":"/z/00001012050200"},{"id":"00001012000000","url":"/z/00001012000000"}]} ``` If you want only those zettel that additionally must contain the string ""JSON"", you have to add an additional query parameter: ```sh # curl 'http://127.0.0.1:23123/z?title=API&_part=id&title=JSON' {"list":[{"id":"00001012921000","url":"/z/00001012921000"}]} ``` Similarly, if you add another query parameter, the intersection of both results is returned: ```sh # curl 'http://127.0.0.1:23123/z?title=API&_part=id&id=00001012050' {"list":[{"id":"00001012050600","url":"/z/00001012050600"},{"id":"00001012050400","url":"/z/00001012050400"},{"id":"00001012050200","url":"/z/00001012050200"}]} ``` === Limit and offset By using the query parameter ""''_limit''"", which must have an integer value, you specifying an upper limit of list elements: ```sh # curl 'http://192.168.17.7:23121/z?title=API&_part=id&_sort=id&_limit=2' {"list":[{"id":"00001012000000","url":"/z/00001012000000"},{"id":"00001012050200","url":"/z/00001012050200"}]} ``` The query parameter ""''_offset''"" allows to list not only the first elements, but to begin at a specific element: ```sh # curl 'http://192.168.17.7:23121/z?title=API&_part=id&_sort=id&_limit=2&_offset=1' {"list":[{"id":"00001012050200","url":"/z/00001012050200"},{"id":"00001012050400","url":"/z/00001012050400"}]} ``` === General filter The query parameter ""''_s''"" allows to provide a string for a full-text search of all zettel. The search string will be normalized according to Unicode NKFD, ignoring everything except letters and numbers. If the search string starts with the character ""''!''"", it will be removed and the query matches all zettel that **does not match** the search string. In the next step, the first character of the search string will be inspected. If it contains one of the characters ""'':''"", ""''=''"", ""''>''"", or ""''~''"", this will modify how the search will be performed. The character will be removed from the start of the search string. For example, assume the search string is ""def"": ; ""'':''"", ""''~''"" (or none of these characters)[^""'':''"" is always the character for specifying the default comparison. In this case, it is equal to ""''~''"". If you omit a comparison character, the default comparison is used.] : The zettel must contain a word that contains the search string. ""def"", ""defghi"", and ""abcdefghi"" are matching the search string. ; ""''=''"" : The zettel must contain a word that is equal to the search string. Only the word ""def"" matches the search string. ; ""''>''"" : The zettel must contain a word with the search string as a prefix. A word like ""def"" or ""defghi"" matches the search string. If you want to include an initial ""''!''"" into the search string, you must prefix that with the escape character ""''\\''"". For example ""\\!abc"" will search for zettel that contains the string ""!abc"". A similar rule applies to the characters that specify the way how the search will be done. For example, ""!\\=abc"" will search for zettel that do not contains the string ""=abc"". You are allowed to specify this query parameter more than once. All results will be intersected, i.e. a zettel will be included into the list if all of the provided values match. This parameter loosely resembles the search box of the web user interface. |
Added docs/manual/00001012052000.zettel.
> > > > > > > > > > > > > > > > > > > > > > | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 | id: 00001012052000 title: API: Sort the list of zettel metadata role: manual tags: #api #manual #zettelstore syntax: zmk If not specified, the list of zettel is sorted descending by the value of the zettel identifier. The highest zettel identifier, which is a number, comes first. You change that with the ""''_sort''"" query parameter. Alternatively, you can also use the ""''_order''"" query parameter. It is an alias. Its value is the name of a metadata key, optionally prefixed with a hyphen-minus character (""-"", ''U+002D''). According to the [[type|00001006030000]] of a metadata key, the list of zettel is sorted. If hyphen-minus is given, the order is descending, else ascending. If you want a random list of zettel, specify the value ""_random"" in place of the metadata key. ""''_sort=_random''"" (or ""''_order=_random''"") is the query parameter in this case. If can be combined with ""[[''_limit=1''|00001012051800]]"" to obtain a random zettel. Currently, only the first occurrence of ''_sort'' is recognized. In the future it will be possible to specify a combined sort key. |
Added docs/manual/00001012052200.zettel.
> > > > > > > > > > > > > > > > > > | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 | id: 00001012052200 title: API: List all tags role: manual tags: #api #manual #zettelstore syntax: zmk To list all [[tags|00001006020000#tags]] used in the Zettelstore just send a HTTP GET request to the [[endpoint|00001012920000]] ''/t''. If successful, the output is a JSON object: ```sh # curl http://127.0.0.1:23123/t {"tags":{"#api":[:["00001012921000","00001012920800","00001012920522",...],"#authorization":["00001010040700","00001010040400",...],...,"#zettelstore":["00010000000000","00001014000000",...,"00001001000000"]}} ``` The JSON object only contains the key ''"tags"'' 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. Please note that this structure will likely change in the future to be more compliant with other API calls. |
Added docs/manual/00001012052400.zettel.
> > > > > > > > > > > > > > > > > > | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 | id: 00001012052400 title: API: List all roles role: manual tags: #api #manual #zettelstore syntax: zmk To list all [[roles|00001006020100]] used in the Zettelstore just send a HTTP GET request to the [[endpoint|00001012920000]] ''/r''. If successful, the output is a JSON object: ```sh # curl http://127.0.0.1:23123/r {"role-list":["configuration","manual","user","zettel"]} ``` The JSON object only contains the key ''"role-list"'' with the value of a sorted string list. Each string names one role. Please note that this structure will likely change in the future to be more compliant with other API calls. |
Deleted docs/manual/00001012053200.zettel.
|
| < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < |
Deleted docs/manual/00001012053300.zettel.
|
| < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < |
Changes to docs/manual/00001012053400.zettel.
1 | id: 00001012053400 | | < < | < | > | | < < | < | | < | < | | | > > > > > > > > > > > > > > | | > | < | | < < < < < < < < > | < < < < | | | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 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: 00001012053400 title: API: Retrieve metadata and content of an existing zettel role: manual tags: #api #manual #zettelstore syntax: zmk 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 (14 digits). For example, to retrieve some data about this zettel you are currently viewing, just send a HTTP GET request to the endpoint ''/z/00001012053400''[^If [[authentication is enabled|00001010040100]], you must include the a valid [[access token|00001012050200]] in the ''Authorization'' header]. If successful, the output is a JSON object: ```sh # curl http://127.0.0.1:23123/z/00001012053400 {"id":"00001012053400","url":"/z/00001012053400","meta":{"title":"API: Retrieve data for an exisiting zettel","tags":"#api #manual #zettelstore","syntax":"zmk","role":"manual","copyright":"(c) 2020 by Detlef Stern <ds@zettelstore.de>","lang":"en","license":"CC BY-SA 4.0"},"content":"The endpoint to work with a specific zettel is ''/z/{ID}'', where ''{ID}'' is a placeholder for the zettel identifier (14 digits).\n\nFor example, ... ``` Similar to listing all or some zettel, you can provide a query parameter ''_format=[[FORMAT|00001012920500]]'' to select a different encoding format. The default encoding format is ""[[json|00001012920501]]"". Others are ""[[djson|00001012920503]]"", ""[[html|00001012920510]]"", and some more. ```sh # curl 'http://127.0.0.1:23123/z/00001012053400?_format=html' <!DOCTYPE html> <html lang="en"> <head> <meta charset="utf-8"> <title>API: Retrieve data for an exisiting zettel</title> <meta name="keywords" content="api, manual, zettelstore"> <meta name="zs-syntax" content="zmk"> <meta name="zs-role" content="manual"> <meta name="copyright" content="(c) 2020 by Detlef Stern <ds@zettelstore.de>"> <meta name="license" content="CC BY-SA 4.0"> </head> <body> <p>The endpoint to work with a specific zettel is <span style="font-family:monospace">/z/{ID}</span>, where <span style="font-family:monospace">{ID}</span> is a placeholder for the zettel identifier (14 digits).</p> ... ``` You also can use the query parameter ''_part=[[PART|00001012920800]]'' to specify which parts of a zettel must be encoded. In this case, its default value is ''zettel''. ```sh # curl 'http://192.168.17.7:23121/z/00001012053400?_format=html&_part=meta' <meta name="zs-title" content="API: Retrieve data for an exisiting zettel"> <meta name="keywords" content="api, manual, zettelstore"> <meta name="zs-syntax" content="zmk"> <meta name="zs-role" content="manual"> ``` (Note that metadata of the whole zettel contains some computed values, such as ''"copyright"'' and ''"license"'', while the metadata for ''_part=meta'' just contain the explicitly specified metadata.) === 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 ''_format'' / ''_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. |
Deleted docs/manual/00001012053500.zettel.
|
| < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < |
Changes to docs/manual/00001012053600.zettel.
1 | id: 00001012053600 | | < > | > > > | > > > > > > | > > > > > > | > > | < < > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > | > > > > > > > | > > > > > > > > > > > > > > > > | < < < > > > < < > > > > | | | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 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 | id: 00001012053600 title: API: Retrieve references of an existing zettel tags: #api #manual #zettelstore syntax: zmk role: manual The web of zettel is one important value of a Zettelstore. Many zettel references other zettel, images, external/local material or, via citations, external literature. By using the [[endpoint|00001012920000]] ''/l/{ID}'' you are able to retrieve these references. ```` # curl http://127.0.0.1:23123/l/00001012053600 {"id":"00001012053600","url":"/z/00001012053600","links":{"incoming":[],"outgoing":[{"id":"00001012920000","url":"/z/00001012920000"},{"id":"00001007040300","url":"/z/00001007040300#links"},{"id":"00001007040300","url":"/z/00001007040300#images"},{"id":"00001007040300","url":"/z/00001007040300#citation-key"}],"local":[],"external":[]},"images":{"outgoing":[],"local":[],"external":[]},"cites":[]} ```` Formatted, this translates into: ````json { "id": "00001012053600", "url": "/z/00001012053600", "links": { "incoming": [], "outgoing": [ { "id": "00001012920000", "url": "/z/00001012920000" }, { "id": "00001007040300", "url": "/z/00001007040300#links" }, { "id": "00001007040300", "url": "/z/00001007040300#images" }, { "id": "00001007040300", "url": "/z/00001007040300#citation-key" } ], "local": [], "external": [] }, "images": { "outgoing": [], "local": [], "external": [] }, "cites": [] } ```` === Kind The following to-level JSON keys are returned: ; ''id'' : The zettel identifier for which the references were requested. ; ''url'' : The API endpoint to fetch more information about the zettel. ; ''link'' : A JSON object that contains information about incoming and outgoing [[links|00001007040300#links]]. ; ''image'' : A JSON object that contains information about referenced [[images|00001007040300#images]]. ; ''cite'' : A JSON list of [[citation keys|00001007040300#citation-key]] (as JSON strings). The query parameter ''kind'' controls which of these values is retrieved: |= ''kind'' <| Number >| Returned values | (nothing) | (14) |links, images, and citation keys | ''link'' | 2|links | ''image'' | 4|images | ''cite'' | 8|citation keys | ''both'' | (6)|links and images | ''all'' | (14)|links, images, and citation keys The ""Number"" column gives an indication about an alternative way of specifying the kind. Every kind is given a number, which you also can use to specify the requested kind of reference. To get more than one kind, just add the numbers. If you want to retrieve only images and citations, which was not given a ''kind'' value, just specify ''kind=12''. === Matter If you request to retrieve referenced links and/or referenced images, you can further control, which matter of references should be retrieved. This is controlled by the value of the query parameter ''matter'': |= ''matter''| Number >| Returned reference list | (nothing) | (30) | incoming, outgoing, local, and external references | ''incoming'' | 2|incoming reference, not allowed for images (aka ""backlinks"", not yet implemented) | ''outgoing'' | 4|outgoing references | ''local'' | 8|local references, i.e. local, non-zettel material | ''external'' |16| external references, i.e. on the web | ''zettel'' | (6)|incoming and outgoing references | ''material'' |(24)| local and external references | ''all'' | (30)| incoming, outgoing, local, and external references Incoming and outgoing references are basically zettel. Therefore the list elements are JSON objects with keys ''id'' and ''url''. Local and external references are strings. Similar to the ''kind'' query parameter, each matter is associated with a number. To retrieve a combination of matter values that does not have a name, just add the numbers. For example, if you want to retrieve only the outgoing and the external references, specify ''matter=20''. If a list is not going to retrieved, the associated value is ``null``{=json} instead of an empty list. === 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 ''_format'' / ''_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. |
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 | id: 00001012053800 title: API: Retrieve context of an existing zettel role: manual tags: #api #manual #zettelstore syntax: zmk The context of an origin zettel consists of those zettel that are somehow connected to the origin zettel. Direct connections of an origin zettel to other zettel are visible via [[metadata values|00001006020000]], such as ''backward'', ''forward'' or other values with type [[identifier|00001006032000]] or [[set of identifier|00001006032500]]. Zettel are also connected by using same [[tags|00001006020000#tags]]. The context is defined by a //direction//, a //depth//, and a /limit//: * Direction: connections are directed. For example, the metadata value of ''backward'' lists all zettel that link to the current zettel, while ''formward'' list all zettel to which the current zettel links. When you are only interested in one direction, set the parameter ''dir'' either to the value ""backward"" or ""forward"". All other values, including a missing value, is interpreted as ""both"". * Depth: a direct connection has depth 1, an indirect connection is the length of the shortest path between two zettel. You should limit the depth by using the parameter ''depth''. Its default value is ""5"". A value of ""0"" does disable any depth check. * Limit: to set an upper bound for the returned context, you should use the parameter ''limit''. Its default value is ""200"". A value of ""0"" disables does not limit the number of elements returned. Zettel with same tags as the origin zettel are considered depth 1. Only for the origin zettel, tags are used to calculate a connection. Currently, only some of the newest zettel with a given tag are considered a connection.[^The number of zettel is given by the value of parameter ''depth''.] Otherwise the context would become too big and therefore unusable. To retrieve the context of an existing zettel, use the [[endpoint|00001012920000]] ''/y/{ID}''. ```` # curl 'http://127.0.0.1:23123/y/00001012053800?limit=3&dir=forward&depth=2' {"id": "00001012053800","url": "/z/00001012053800","meta": {...},"list": [{"id": "00001012921000","url": "/z/00001012921000","meta": {...}},{"id": "00001012920800","url": "/z/00001012920800","meta": {...}},{"id": "00010000000000","url": "/z/00010000000000","meta": {...}}]} ```` Formatted, this translates into:[^Metadata (key ''meta'') are hidden to make the overall structure easier to read.] ````json { "id": "00001012053800", "url": "/z/00001012053800", "meta": {...}, "list": [ { "id": "00001012921000", "url": "/z/00001012921000", "meta": {...} }, { "id": "00001012920800", "url": "/z/00001012920800", "meta": {...} }, { "id": "00010000000000", "url": "/z/00010000000000", "meta": {...} } ] } ```` === Keys The following top-level JSON keys are returned: ; ''id'' : The zettel identifier for which the context was requested. ; ''url'' : The API endpoint to fetch more information about the zettel. ; ''meta'': : The metadata of the zettel, encoded as a JSON object. ; ''list'' : A list of JSON objects with keys ''id'', ''url'' and ''meta'' 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/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 | id: 00001012054000 title: API: Retrieve zettel order within an existing zettel role: manual tags: #api #manual #zettelstore syntax: zmk Some zettel act as a ""table of contents"" for other zettel. The [[initial zettel|00001000000000]] of this manual is one example, the [[general API description|00001012000000]] is another. Every zettel with a certain internal structure can act as the ""table of contents"" for others. What is a ""table of contents""? Basically, it is just a list of references to other zettel. 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","url":"/z/00001000000000","meta":{...},"list":[{"id":"00001001000000","url":"/z/00001001000000","meta":{...}},{"id":"00001002000000","url":"/z/00001002000000","meta":{...}},{"id":"00001003000000","url":"/z/00001003000000","meta":{...}},{"id":"00001004000000","url":"/z/00001004000000","meta":{...}},...,{"id":"00001014000000","url":"/z/00001014000000","meta":{...}}]} ```` Formatted, this translates into:[^Metadata (key ''meta'') are hidden to make the overall structure easier to read.] ````json { "id": "00001000000000", "url": "/z/00001000000000", "order": [ { "id": "00001001000000", "url": "/z/00001001000000", "meta": {...} }, { "id": "00001002000000", "url": "/z/00001002000000", "meta": {...} }, { "id": "00001003000000", "url": "/z/00001003000000", "meta": {...} }, { "id": "00001004000000", "url": "/z/00001004000000", "meta": {...} }, ... { "id": "00001014000000", "url": "/z/00001014000000", "meta": {...} } ] } ```` === Kind The following top-level JSON keys are returned: ; ''id'' : The zettel identifier for which the references were requested. ; ''url'' : The API endpoint to fetch more information about the zettel. ; ''meta'': : The metadata of the zettel, encoded as a JSON object. ; ''list'' : A list of JSON objects with keys ''id'', ''url'', and ''meta'' 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. |
Deleted docs/manual/00001012054200.zettel.
|
| < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < |
Deleted docs/manual/00001012054400.zettel.
|
| < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < |
Deleted docs/manual/00001012054600.zettel.
|
| < < < < < < < < < < < < < < < < < < < < < < < < < < < |
Deleted docs/manual/00001012070500.zettel.
|
| < < < < < < < < < < < < < < < < < < < < < < < < < < < < < |
Deleted docs/manual/00001012080100.zettel.
|
| < < < < < < < < < < < < < < < < < |
Deleted docs/manual/00001012080200.zettel.
|
| < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < |
Deleted docs/manual/00001012080500.zettel.
|
| < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < |
Changes to docs/manual/00001012920000.zettel.
1 2 3 4 5 | id: 00001012920000 title: Endpoints used by the API role: manual tags: #api #manual #reference #zettelstore syntax: zmk | < | | | | | > > | | > | | | < | | | | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 | id: 00001012920000 title: Endpoints used by the API role: manual tags: #api #manual #reference #zettelstore syntax: zmk modified: 20210511131339 All API endpoints conform to the pattern ''[PREFIX]LETTER[/ZETTEL-ID]'', where: ; ''PREFIX'' : is the URL prefix (default: ''/''), configured via the ''url-prefix'' [[startup configuration|00001004010000]], ; ''LETTER'' : is a single letter that specifies the ressource type, ; ''ZETTEL-ID'' : is an optional 14 digits string that uniquely [[identify a zettel|00001006050000]]. The following letters are currently in use: |= Letter:| Without zettel identifier | With [[zettel identifier|00001006050000]] | ''a'' | POST: [[Client authentication|00001012050200]] | | | PUT: [[renew access token|00001012050400]] | | ''l'' | | GET: [[list references|00001012053600]] | ''o'' | | GET: [[list zettel order|00001012054000]] | ''r'' | GET: [[list roles|00001012052400]] | ''t'' | GET: [[list tags|00001012052200]] | ''y'' | | GET: [[list zettel context|00001012053800]] | ''z'' | GET: [[list zettel|00001012051200]] | GET: [[retrieve zettel|00001012053400]] | | POST: add new zettel | PUT: change a zettel | | | DELETE: delete the zettel 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 17 18 19 | id: 00001012920500 title: Formats available by the API tags: #api #manual #reference #zettelstore syntax: zmk role: manual A zettel representation can be encoded in various formats for further processing. * [[json|00001012920501]] (default) * [[djson|00001012920503]] * [[html|00001012920510]] * [[native|00001012920513]] * [[raw|00001012920516]] * [[text|00001012920519]] * [[zmk|00001012920522]] Planned formats: ; ''pjson'' : Encoder that emits JSON data that can be fed into [[pandoc|https://pandoc.org]] for further processing. |
Added docs/manual/00001012920501.zettel.
> > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 | id: 00001012920501 title: JSON Format role: manual tags: #api #manual #reference #zettelstore syntax: zmk This is the default representation of a zettel or a list of zettel. Basically, user provided data is encoded as a string (zettel content and metadata values), The metadata and some other structuring data is encoded a JSON object. The JSON objects contains various name/value pairs, depending which content should be encoded: * ''"id"'': the [[zettel identifier|00001006050000]], as a string. * ''"url"'': the base URL to the zettel, as a string. * ''"meta"'': the metadata of the zettel, as an JSON object. See below for details. * ''"encoding"'' and ''"content"'': the actual content of the zettel. See below for details. ''"id"'' and ''"url"'' are always sent to the client. It depends on the value of the required [[zettel part|00001012920800]], whether ''"meta"'' or ''"content"'' or both are sent. For an example, take a look at the JSON encoding of this page, which is available via the ""Info"" sub-page of this zettel: * [[//z/00001012920501?_part=id]], * [[//z/00001012920501?_part=zettel]], * [[//z/00001012920501?_part=meta]], * [[//z/00001012920501?_part=content]]. If transferred via HTTP, the content type will be ''application/json''. === Metadata This ia a JSON object, that maps [[metadata keys|00001006010000]] to their values. Their values are encoded as strings, even if they contain a number (or something else). You can always expect the keys ''"title"'', ''"tags"'', ''"syntax"'', and ''"role"'', together with their values. The Zettelstore provides default values for these values, if they are not set for a zettel. There is a list of [[supported metadata keys|00001006020000]]. === Content When the content is text-only, it is encoded as a plain string with an ''"encoding"'' value of ''""'' (empty string). If the content contains binary content: * ''"encoding"'' specifies the string encoding of the content. Currently, only the value ''"base64"'' is supported (as described in [[RFC4648, section 4|https://tools.ietf.org/html/rfc4648#section-4]]. * ''"value"'' is a string that contains the encoded binary content. For example, if the content just consists of three zero bytes, it will be encoded as ``(...),"encoding":"base64","value":"AAAA"``{=json}. |
Added docs/manual/00001012920503.zettel.
> > > > > > > > > > > > > > > > > > > | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 | id: 00001012920503 title: DJSON Format role: manual tags: #api #manual #reference #zettelstore syntax: zmk A zettel representation that allows to process the syntactic structure of a zettel. It is a JSON-based encoding format, but different to [[json|00001012920501]]. For an example, take a look at the JSON encoding of this page, which is available via the ""Info"" sub-page of this zettel: * [[../z/00001012920503?_format=djson&_part=id]], * [[../z/00001012920503?_format=djson&_part=zettel]], * [[../z/00001012920503?_format=djson&_part=meta]], * [[../z/00001012920503?_format=djson&_part=content]]. If transferred via HTTP, the content type will be ''application/json''. TODO: detailed description. |
Changes to docs/manual/00001012920510.zettel.
1 | id: 00001012920510 | | < | | 1 2 3 4 5 6 7 8 9 10 11 12 | id: 00001012920510 title: HTML Format tags: #api #manual #reference #zettelstore syntax: zmk role: manual 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. If transferred via HTTP, the content type will be ''text/html''. |
Changes to docs/manual/00001012920513.zettel.
1 | id: 00001012920513 | | < | < | < > | < < < < < < < < < < < < | < < < < | 1 2 3 4 5 6 7 8 9 10 11 12 | id: 00001012920513 title: Native Format tags: #api #manual #reference #zettelstore syntax: zmk role: manual A zettel representation shows the structure of a zettel in a more user-friendly way. Mostly used for debugging. If transferred via HTTP, the content type will be ''text/plain''. TODO: formal description |
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 | id: 00001012920516 title: Raw Format tags: #api #manual #reference #zettelstore syntax: zmk role: manual A zettel representation as it was loaded from the zettel content. Often used to have access to the bytes of an image. If transferred via HTTP, the content type will depend on various factors: * If the whole zettel should be transferred and it contains textual data only, ''text/plain'' will be used. Otherwise the content type of the whole zettel will be ''application/octet-stream''. * If just metadata is transferred, ''text/plain'' will be used. * If just the content has to be transferred, the content type depends on the actual dats. A PNG image will be transferred as ''image/png'', HTML content as ''text/html'', and so on. |
Changes to docs/manual/00001012920519.zettel.
1 | id: 00001012920519 | | < | | | 1 2 3 4 5 6 7 8 9 10 11 12 | id: 00001012920519 title: Text Format tags: #api #manual #reference #zettelstore syntax: zmk role: manual 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, spearated by space character. If transferred via HTTP, the content type will be ''text/plain''. |
Changes to docs/manual/00001012920522.zettel.
1 | id: 00001012920522 | | < | | | 1 2 3 4 5 6 7 8 9 10 | id: 00001012920522 title: Zmk Format tags: #api #manual #reference #zettelstore syntax: zmk role: manual 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). If transferred via HTTP, the content type will be ''text/plain''. |
Deleted docs/manual/00001012920525.zettel.
|
| < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < |
Changes to docs/manual/00001012920800.zettel.
1 2 | id: 00001012920800 title: Values to specify zettel parts | < | | | | > > > | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 | id: 00001012920800 title: Values to specify zettel parts tags: #api #manual #reference #zettelstore syntax: zmk role: manual 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'' : Specifies that you only want to cope with the metadata of a zettel. Contains identifier and metadata of a zettel. ; [!content]''content'' : Specifies that you are only interested in the zettel content. Contains identifier and content of a zettel. ; [!id]''id'' : States that you just want the zettel identifier and the URL of the zettel. Only valid for JSON-based encoding formats[^[[json|00001012920501]] and [[djson|00001012920503]].]. |
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 [[authentiction 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 |
Deleted docs/manual/00001012921200.zettel.
|
| < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < |
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.
|
| < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < |
Deleted docs/manual/00001017000000.zettel.
|
| < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < |
Deleted docs/manual/00001018000000.zettel.
|
| < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < |
Deleted docs/manual/00001019990010.zettel.
|
| < < < < < < < < |
Deleted docs/manual/20231128184200.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 | //----------------------------------------------------------------------------- // Copyright (c) 2020-2021 Detlef Stern // // This file is part of zettelstore. // // Zettelstore is licensed under the latest version of the EUPL (European Union // Public License). Please see file LICENSE.txt for your rights and obligations // under this license. //----------------------------------------------------------------------------- // Package domain provides domain specific types, constants, and functions. package domain import "unicode/utf8" // Content is just the uninterpreted content of a zettel. type Content string // NewContent creates a new content from a string. func NewContent(s string) Content { return Content(s) } // AsString returns the content itself is a string. func (zc Content) AsString() string { return string(zc) } // AsBytes returns the content itself is a byte slice. func (zc Content) AsBytes() []byte { return []byte(zc) } // 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 { s := string(zc) if !utf8.ValidString(s) { return true } l := len(s) for i := 0; i < l; i++ { if s[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 | //----------------------------------------------------------------------------- // Copyright (c) 2020 Detlef Stern // // This file is part of zettelstore. // // Zettelstore is licensed under the latest version of the EUPL (European Union // Public License). Please see file LICENSE.txt for your rights and obligations // under this license. //----------------------------------------------------------------------------- package domain_test import ( "testing" "zettelstore.de/z/domain" ) func TestContentIsBinary(t *testing.T) { td := []struct { s string exp bool }{ {"abc", false}, {"äöü", false}, {"", false}, {string([]byte{0}), true}, } for i, tc := range td { content := domain.NewContent(tc.s) got := content.IsBinary() if got != tc.exp { t.Errorf("TC=%d: expected %v, got %v", i, 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 | //----------------------------------------------------------------------------- // Copyright (c) 2020-2021 Detlef Stern // // This file is part of zettelstore. // // Zettelstore is licensed under the latest version of the EUPL (European Union // Public License). Please see file LICENSE.txt for your rights and obligations // under this license. //----------------------------------------------------------------------------- // Package id provides domain specific types, constants, and functions about // zettel identifier. package id import ( "strconv" "time" ) // 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. // Note: if you change some values, ensure that you also change them in the // constant place. They are mentioned there literally, because these // constants are not available there. const ( Invalid = Zid(0) // Invalid is a Zid that will never be valid // System zettel VersionZid = Zid(1) HostZid = Zid(2) OperatingSystemZid = Zid(3) LicenseZid = Zid(4) AuthorsZid = Zid(5) DependenciesZid = Zid(6) PlaceManagerZid = Zid(20) MetadataKeyZid = Zid(90) StartupConfigurationZid = Zid(96) ConfigurationZid = Zid(100) // WebUI HTML templates are in the range 10000..19999 BaseTemplateZid = Zid(10100) LoginTemplateZid = Zid(10200) ListTemplateZid = Zid(10300) ZettelTemplateZid = Zid(10401) InfoTemplateZid = Zid(10402) FormTemplateZid = Zid(10403) RenameTemplateZid = Zid(10404) DeleteTemplateZid = Zid(10405) ContextTemplateZid = Zid(10406) RolesTemplateZid = Zid(10500) TagsTemplateZid = Zid(10600) ErrorTemplateZid = Zid(10700) // WebUI CSS zettel are in the range 20000..29999 BaseCSSZid = Zid(20001) // WebUI JS zettel are in the range 30000..39999 // WebUI image zettel are in the range 40000..49999 EmojiZid = Zid(40001) // Range 90000...99999 is reserved for zettel templates TOCNewTemplateZid = Zid(90000) TemplateNewZettelZid = Zid(90001) TemplateNewUserZid = Zid(90002) DefaultHomeZid = Zid(10000000000) ) 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 } const digits = "0123456789" // String converts the zettel identification to a string of 14 digits. // Only defined for valid ids. func (zid Zid) String() string { return string(zid.Bytes()) } // Bytes converts the zettel identification to a byte slice of 14 digits. // Only defined for valid ids. func (zid Zid) Bytes() []byte { result := make([]byte, 14) for i := 13; i >= 0; i-- { result[i] = digits[zid%10] zid /= 10 } return result } // 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 } // New returns a new zettel id based on the current time. func New(withSeconds bool) Zid { now := time.Now() var s string if withSeconds { s = now.Format("20060102150405") } 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 | //----------------------------------------------------------------------------- // Copyright (c) 2020-2021 Detlef Stern // // This file is part of zettelstore. // // Zettelstore is licensed under the latest version of the EUPL (European Union // Public License). Please see file LICENSE.txt for your rights and obligations // under this license. //----------------------------------------------------------------------------- // Package 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) { 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, sid, zid, 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) } } } |
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 | //----------------------------------------------------------------------------- // Copyright (c) 2021 Detlef Stern // // This file is part of zettelstore. // // Zettelstore is licensed under the latest version of the EUPL (European Union // Public License). Please see file LICENSE.txt for your rights and obligations // under this license. //----------------------------------------------------------------------------- // Package id provides domain specific types, constants, and functions about // zettel identifier. package id // Set is a set of zettel identifier type Set map[Zid]bool // 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 } // AddSlice adds all identifier of the given slice to the set. func (s Set) AddSlice(sl Slice) { for _, zid := range sl { s[zid] = true } } // 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 } // Intersect 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. func (s Set) Intersect(other Set) Set { if len(s) > len(other) { s, other = other, s } for zid, inSet := range s { if !inSet { delete(s, zid) continue } otherInSet, otherOk := other[zid] if !otherInSet || !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, inSet := range other { if inSet { 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 | //----------------------------------------------------------------------------- // Copyright (c) 2021 Detlef Stern // // This file is part of zettelstore. // // Zettelstore is licensed under the latest version of the EUPL (European Union // Public License). Please see file LICENSE.txt for your rights and obligations // under this license. //----------------------------------------------------------------------------- // Package id provides domain specific types, constants, and functions about // zettel identifier. package id_test import ( "testing" "zettelstore.de/z/domain/id" ) func TestSetSorted(t *testing.T) { testcases := []struct { set id.Set exp id.Slice }{ {nil, nil}, {id.NewSet(), nil}, {id.NewSet(9, 4, 6, 1, 7), id.Slice{1, 4, 6, 7, 9}}, } for i, tc := range testcases { got := tc.set.Sorted() if !got.Equal(tc.exp) { t.Errorf("%d: %v.Sorted() should be %v, but got %v", i, tc.set, tc.exp, got) } } } func TestSetIntersection(t *testing.T) { testcases := []struct { s1, s2 id.Set exp id.Slice }{ {nil, nil, nil}, {id.NewSet(), nil, nil}, {id.NewSet(), id.NewSet(), nil}, {id.NewSet(1), nil, nil}, {id.NewSet(1), id.NewSet(), nil}, {id.NewSet(1), id.NewSet(2), 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.Intersect(tc.s2).Sorted() if !got.Equal(tc.exp) { t.Errorf("%d: %v.Intersect(%v) should be %v, but got %v", i, sl1, sl2, tc.exp, got) } got = id.NewSet(sl2...).Intersect(id.NewSet(sl1...)).Sorted() if !got.Equal(tc.exp) { t.Errorf("%d: %v.Intersect(%v) should be %v, but got %v", i, sl2, sl1, tc.exp, got) } } } func TestSetRemove(t *testing.T) { testcases := []struct { s1, s2 id.Set exp id.Slice }{ {nil, nil, nil}, {id.NewSet(), nil, nil}, {id.NewSet(), id.NewSet(), nil}, {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) } } } |
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 68 69 | //----------------------------------------------------------------------------- // Copyright (c) 2021 Detlef Stern // // This file is part of zettelstore. // // Zettelstore is licensed under the latest version of the EUPL (European Union // Public License). Please see file LICENSE.txt for your rights and obligations // under this license. //----------------------------------------------------------------------------- // Package id provides domain specific types, constants, and functions about // zettel identifier. 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 | //----------------------------------------------------------------------------- // Copyright (c) 2021 Detlef Stern // // This file is part of zettelstore. // // Zettelstore is licensed under the latest version of the EUPL (European Union // Public License). Please see file LICENSE.txt for your rights and obligations // under this license. //----------------------------------------------------------------------------- // Package id provides domain specific types, constants, and functions about // zettel identifier. package id_test import ( "testing" "zettelstore.de/z/domain/id" ) func TestSliceSort(t *testing.T) { zs := id.Slice{9, 4, 6, 1, 7} zs.Sort() exp := id.Slice{1, 4, 6, 7, 9} if !zs.Equal(exp) { t.Errorf("Slice.Sort did not work. Expected %v, got %v", exp, zs) } } func TestCopy(t *testing.T) { var orig id.Slice got := orig.Copy() if got != nil { t.Errorf("Nil copy resulted in %v", got) } orig = id.Slice{9, 4, 6, 1, 7} got = orig.Copy() if !orig.Equal(got) { t.Errorf("Slice.Copy did not work. Expected %v, got %v", orig, got) } } func TestSliceEqual(t *testing.T) { testcases := []struct { s1, s2 id.Slice exp bool }{ {nil, nil, true}, {nil, id.Slice{}, true}, {nil, id.Slice{1}, false}, {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) { 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/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 | //----------------------------------------------------------------------------- // Copyright (c) 2020-2021 Detlef Stern // // This file is part of zettelstore. // // Zettelstore is licensed under the latest version of the EUPL (European Union // Public License). Please see file LICENSE.txt for your rights and obligations // under this license. //----------------------------------------------------------------------------- // Package meta provides the domain specific type 'meta'. package meta import ( "regexp" "sort" "strings" "zettelstore.de/z/domain/id" "zettelstore.de/z/runes" ) type keyUsage int const ( _ keyUsage = iota usageUser // Key will be manipulated by the user usageComputed // Key is computed by zettelstore usageProperty // Key is computed and not stored by zettelstore ) // DescriptionKey formally describes each supported metadata key. type DescriptionKey struct { Name string Type *DescriptionType usage keyUsage Inverse string } // IsComputed returns true, if metadata is computed and not set by the user. func (kd *DescriptionKey) IsComputed() bool { return kd.usage >= usageComputed } // IsProperty returns true, if metadata is a computed property. func (kd *DescriptionKey) IsProperty() bool { return kd.usage >= usageProperty } var registeredKeys = make(map[string]*DescriptionKey) func registerKey(name string, t *DescriptionType, usage keyUsage, inverse string) 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} return name } // 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 } // 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: TypeUnknown} } // GetSortedKeyDescriptions delivers all metadata key descriptions as a slice, sorted by name. func GetSortedKeyDescriptions() []*DescriptionKey { names := make([]string, 0, len(registeredKeys)) for n := range registeredKeys { names = append(names, n) } sort.Strings(names) result := make([]*DescriptionKey, 0, len(names)) for _, n := range names { result = append(result, registeredKeys[n]) } return result } // Supported keys. var ( KeyID = registerKey("id", TypeID, usageComputed, "") KeyTitle = registerKey("title", TypeZettelmarkup, usageUser, "") KeyRole = registerKey("role", TypeWord, usageUser, "") KeyTags = registerKey("tags", TypeTagSet, usageUser, "") KeySyntax = registerKey("syntax", TypeWord, usageUser, "") KeyBack = registerKey("back", TypeIDSet, usageProperty, "") KeyBackward = registerKey("backward", TypeIDSet, usageProperty, "") KeyCopyright = registerKey("copyright", TypeString, usageUser, "") KeyCredential = registerKey("credential", TypeCredential, usageUser, "") KeyDead = registerKey("dead", TypeIDSet, usageProperty, "") KeyDefaultCopyright = registerKey("default-copyright", TypeString, usageUser, "") KeyDefaultLang = registerKey("default-lang", TypeWord, usageUser, "") KeyDefaultLicense = registerKey("default-license", TypeEmpty, usageUser, "") KeyDefaultRole = registerKey("default-role", TypeWord, usageUser, "") KeyDefaultSyntax = registerKey("default-syntax", TypeWord, usageUser, "") KeyDefaultTitle = registerKey("default-title", TypeZettelmarkup, usageUser, "") KeyDefaultVisibility = registerKey("default-visibility", TypeWord, usageUser, "") KeyDuplicates = registerKey("duplicates", TypeBool, usageUser, "") KeyExpertMode = registerKey("expert-mode", TypeBool, usageUser, "") KeyFolge = registerKey("folge", TypeIDSet, usageProperty, "") KeyFooterHTML = registerKey("footer-html", TypeString, usageUser, "") KeyForward = registerKey("forward", TypeIDSet, usageProperty, "") KeyHomeZettel = registerKey("home-zettel", TypeID, usageUser, "") KeyLang = registerKey("lang", TypeWord, usageUser, "") KeyLicense = registerKey("license", TypeEmpty, usageUser, "") KeyListPageSize = registerKey("list-page-size", TypeNumber, usageUser, "") KeyMarkerExternal = registerKey("marker-external", TypeEmpty, usageUser, "") KeyModified = registerKey("modified", TypeTimestamp, usageComputed, "") KeyNoIndex = registerKey("no-index", TypeBool, usageUser, "") KeyPrecursor = registerKey("precursor", TypeIDSet, usageUser, KeyFolge) KeyPublished = registerKey("published", TypeTimestamp, usageProperty, "") KeyReadOnly = registerKey("read-only", TypeWord, usageUser, "") KeySiteName = registerKey("site-name", TypeString, usageUser, "") KeyURL = registerKey("url", TypeURL, usageUser, "") KeyUserID = registerKey("user-id", TypeWord, usageUser, "") KeyUserRole = registerKey("user-role", TypeWord, usageUser, "") KeyVisibility = registerKey("visibility", TypeWord, usageUser, "") KeyYAMLHeader = registerKey("yaml-header", TypeBool, usageUser, "") KeyZettelFileSyntax = registerKey("zettel-file-syntax", TypeWordSet, usageUser, "") ) // Important values for some keys. const ( ValueRoleConfiguration = "configuration" ValueRoleUser = "user" ValueRoleZettel = "zettel" ValueSyntaxNone = "none" ValueSyntaxGif = "gif" ValueSyntaxText = "text" ValueSyntaxZmk = "zmk" ValueTrue = "true" ValueFalse = "false" ValueLangEN = "en" ValueUserRoleReader = "reader" ValueUserRoleWriter = "writer" ValueUserRoleOwner = "owner" ValueVisibilityExpert = "expert" ValueVisibilityOwner = "owner" ValueVisibilityLogin = "login" ValueVisibilityPublic = "public" ) // 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 meta-data func New(zid id.Zid) *Meta { return &Meta{Zid: zid, pairs: make(map[string]string, 5)} } // 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{KeyTitle, KeyRole, KeyTags, KeySyntax} var firstKeySet map[string]bool func init() { firstKeySet = make(map[string]bool, len(firstKeys)) for _, k := range firstKeys { firstKeySet[k] = true } } // Set stores the given string value under the given key. func (m *Meta) Set(key, value string) { if key != KeyID { m.pairs[key] = trimValue(value) } } func trimValue(value string) string { return strings.TrimFunc(value, runes.IsSpace) } // Get retrieves the string value of a given key. The bool value signals, // whether there was a value stored or not. func (m *Meta) Get(key string) (string, bool) { if key == KeyID { return m.Zid.String(), true } 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, ok := m.Get(key); ok { return value } return def } // Pairs 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) Pairs(allowComputed bool) []Pair { return m.doPairs(true, allowComputed) } // PairsRest returns all key/values pairs stored, except the values with // predefined keys. The pairs are ordered by key. func (m *Meta) PairsRest(allowComputed bool) []Pair { return m.doPairs(false, allowComputed) } func (m *Meta) doPairs(first, allowComputed bool) []Pair { result := make([]Pair, 0, len(m.pairs)) if first { for _, key := range firstKeys { if value, ok := m.pairs[key]; ok { result = append(result, Pair{key, value}) } } } keys := make([]string, 0, len(m.pairs)-len(result)) for k := range m.pairs { if !firstKeySet[k] && (allowComputed || !IsComputed(k)) { keys = append(keys, k) } } sort.Strings(keys) for _, k := range keys { result = append(result, Pair{k, m.pairs[k]}) } return result } // Delete removes a key from the data. func (m *Meta) Delete(key string) { if key != 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(map[string]bool, len(m.pairs)) for k, v := range m.pairs { tested[k] = true if !equalValue(k, v, o, allowComputed) { return false } } for k, v := range o.pairs { if !tested[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, ok := other.pairs[key]; !ok || val != valO { return false } } return true } |
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 | //----------------------------------------------------------------------------- // Copyright (c) 2020 Detlef Stern // // This file is part of zettelstore. // // Zettelstore is licensed under the latest version of the EUPL (European Union // Public License). Please see file LICENSE.txt for your rights and obligations // under this license. //----------------------------------------------------------------------------- // Package meta provides the domain specific type 'meta'. package meta import ( "strings" "testing" "zettelstore.de/z/domain/id" ) const testID = id.Zid(98765432101234) func TestKeyIsValid(t *testing.T) { validKeys := []string{"0", "a", "0-", "title", "title-----", strings.Repeat("r", 255)} for _, key := range validKeys { if !KeyIsValid(key) { t.Errorf("Key %q wrongly identified as invalid key", key) } } invalidKeys := []string{"", "-", "-a", "Title", "a_b", strings.Repeat("e", 256)} for _, key := range invalidKeys { if KeyIsValid(key) { t.Errorf("Key %q wrongly identified as valid key", key) } } } func TestTitleHeader(t *testing.T) { m := New(testID) if got, ok := m.Get(KeyTitle); ok || got != "" { t.Errorf("Title is not empty, but %q", got) } addToMeta(m, KeyTitle, " ") if got, ok := m.Get(KeyTitle); ok || got != "" { t.Errorf("Title is not empty, but %q", got) } const st = "A simple text" addToMeta(m, KeyTitle, " "+st+" ") if got, ok := m.Get(KeyTitle); !ok || got != st { t.Errorf("Title is not %q, but %q", st, got) } addToMeta(m, KeyTitle, " "+st+"\t") const exp = st + " " + st if got, ok := m.Get(KeyTitle); !ok || got != exp { t.Errorf("Title is not %q, but %q", exp, got) } m = New(testID) const at = "A Title" addToMeta(m, KeyTitle, at) addToMeta(m, KeyTitle, " ") if got, ok := m.Get(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) { m := New(testID) checkSet(t, []string{}, m, KeyTags) addToMeta(m, KeyTags, "") checkSet(t, []string{}, m, KeyTags) addToMeta(m, KeyTags, " #t1 #t2 #t3 #t4 ") checkSet(t, []string{"#t1", "#t2", "#t3", "#t4"}, m, KeyTags) addToMeta(m, KeyTags, "#t5") checkSet(t, []string{"#t1", "#t2", "#t3", "#t4", "#t5"}, m, KeyTags) addToMeta(m, KeyTags, "t6") checkSet(t, []string{"#t1", "#t2", "#t3", "#t4", "#t5"}, m, KeyTags) } func TestSyntax(t *testing.T) { m := New(testID) if got, ok := m.Get(KeySyntax); ok || got != "" { t.Errorf("Syntax is not %q, but %q", "", got) } addToMeta(m, KeySyntax, " ") if got, _ := m.Get(KeySyntax); got != "" { t.Errorf("Syntax is not %q, but %q", "", got) } addToMeta(m, KeySyntax, "MarkDown") const exp = "markdown" if got, ok := m.Get(KeySyntax); !ok || got != exp { t.Errorf("Syntax is not %q, but %q", exp, got) } addToMeta(m, KeySyntax, " ") if got, _ := m.Get(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) { m := New(testID) addToMeta(m, "h1", "d1") addToMeta(m, "H2", "D2") addToMeta(m, "H1", "D1.1") exp := map[string]string{"h1": "d1 D1.1", "h2": "D2"} checkHeader(t, exp, m.Pairs(true)) addToMeta(m, "", "d0") checkHeader(t, exp, m.Pairs(true)) addToMeta(m, "h3", "") exp["h3"] = "" checkHeader(t, exp, m.Pairs(true)) addToMeta(m, "h3", " ") checkHeader(t, exp, m.Pairs(true)) addToMeta(m, "h4", " ") exp["h4"] = "" checkHeader(t, exp, m.Pairs(true)) } func TestDelete(t *testing.T) { m := New(testID) m.Set("key", "val") if got, ok := m.Get("key"); !ok || got != "val" { t.Errorf("Value != %q, got: %v/%q", "val", ok, got) } m.Set("key", "") if got, ok := m.Get("key"); !ok || got != "" { t.Errorf("Value != %q, got: %v/%q", "", ok, got) } m.Delete("key") if got, ok := m.Get("key"); ok || got != "" { t.Errorf("Value != %q, got: %v/%q", "", ok, got) } } func TestEqual(t *testing.T) { testcases := []struct { pairs1, pairs2 []string allowComputed bool exp bool }{ {nil, nil, true, true}, {nil, nil, false, true}, {[]string{"a", "a"}, nil, false, false}, {[]string{"a", "a"}, nil, true, false}, {[]string{KeyFolge, "0"}, nil, true, false}, {[]string{KeyFolge, "0"}, nil, false, true}, {[]string{KeyFolge, "0"}, []string{KeyFolge, "0"}, true, true}, {[]string{KeyFolge, "0"}, []string{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 } |
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 178 179 180 181 182 183 184 185 186 187 | //----------------------------------------------------------------------------- // Copyright (c) 2020-2021 Detlef Stern // // This file is part of zettelstore. // // Zettelstore is licensed under the latest version of the EUPL (European Union // Public License). Please see file LICENSE.txt for your rights and obligations // under this license. //----------------------------------------------------------------------------- // Package meta provides the domain specific type 'meta'. package meta import ( "sort" "strings" "zettelstore.de/z/domain/id" "zettelstore.de/z/input" "zettelstore.de/z/runes" ) // 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 string for { skipSpace(inp) pos = inp.Pos skipToEOL(inp) val += inp.Src[pos:inp.Pos] inp.EatEOL() if !runes.IsSpace(inp.Ch) { break } val += " " } addToMeta(m, key, val) } func skipSpace(inp *input.Input) { for runes.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 map[string]bool, elems []string, useElem predValidElem) { for _, s := range elems { if len(s) > 0 && useElem(s) { set[s] = true } } } func addSet(m *Meta, key, val string, useElem predValidElem) { newElems := strings.Fields(val) oldElems, ok := m.GetList(key) if !ok { oldElems = nil } set := make(map[string]bool, len(newElems)+len(oldElems)) addToSet(set, newElems, useElem) if len(set) == 0 { // Nothing to add. Maybe because of filtered elements. return } addToSet(set, oldElems, useElem) resultList := make([]string, 0, len(set)) for tag := range set { resultList = append(resultList, tag) } sort.Strings(resultList) m.SetList(key, resultList) } 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 "", KeyID: // Empty key and 'id' key will be ignored return } switch Type(key) { case TypeString, TypeZettelmarkup: if v != "" { addData(m, key, v) } case TypeTagSet: addSet(m, key, v, func(s string) bool { return s[0] == '#' }) 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 | //----------------------------------------------------------------------------- // Copyright (c) 2020-2021 Detlef Stern // // This file is part of zettelstore. // // Zettelstore is licensed under the latest version of the EUPL (European Union // Public License). Please see file LICENSE.txt for your rights and obligations // under this license. //----------------------------------------------------------------------------- // Package meta_test provides tests for the domain specific type 'meta'. package meta_test import ( "testing" "zettelstore.de/z/domain/meta" "zettelstore.de/z/input" ) func parseMetaStr(src string) *meta.Meta { return meta.NewFromInput(testID, input.NewInput(src)) } func TestEmpty(t *testing.T) { m := parseMetaStr("") if got, ok := m.Get(meta.KeySyntax); ok || got != "" { t.Errorf("Syntax is not %q, but %q", "", got) } if got, ok := m.GetList(meta.KeyTags); ok || len(got) > 0 { t.Errorf("Tags are not nil, but %v", got) } } func TestTitle(t *testing.T) { td := []struct{ s, e string }{ {meta.KeyTitle + ": a title", "a title"}, {meta.KeyTitle + ": a\n\t title", "a title"}, {meta.KeyTitle + ": a\n\t title\r\n x", "a title x"}, {meta.KeyTitle + " AbC", "AbC"}, {meta.KeyTitle + " AbC\n ded", "AbC ded"}, {meta.KeyTitle + ": o\ntitle: p", "o p"}, {meta.KeyTitle + ": O\n\ntitle: P", "O"}, {meta.KeyTitle + ": b\r\ntitle: c", "b c"}, {meta.KeyTitle + ": B\r\n\r\ntitle: C", "B"}, {meta.KeyTitle + ": r\rtitle: q", "r q"}, {meta.KeyTitle + ": R\r\rtitle: Q", "R"}, } for i, tc := range td { m := parseMetaStr(tc.s) if got, ok := m.Get(meta.KeyTitle); !ok || got != tc.e { t.Log(m) t.Errorf("TC=%d: expected %q, got %q", i, tc.e, got) } } m := parseMetaStr(meta.KeyTitle + ": ") if title, ok := m.Get(meta.KeyTitle); ok { t.Errorf("Expected a missing title key, but got %q (meta=%v)", title, m) } } func TestNewFromInput(t *testing.T) { testcases := []struct { input string exp []meta.Pair }{ {"", []meta.Pair{}}, {" a:b", []meta.Pair{{"a", "b"}}}, {"%a:b", []meta.Pair{}}, {"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"}}}, } for i, tc := range testcases { meta := parseMetaStr(tc.input) if got := meta.Pairs(true); !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("---\na:b\n---\nX") m := meta.NewFromInput(testID, inp) exp := []meta.Pair{{"a", "b"}} if got := m.Pairs(true); !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) { 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(meta.KeyPrecursor + ": " + tc.inp) if got, ok := m.Get(meta.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 | //----------------------------------------------------------------------------- // Copyright (c) 2020-2021 Detlef Stern // // This file is part of zettelstore. // // Zettelstore is licensed under the latest version of the EUPL (European Union // Public License). Please see file LICENSE.txt for your rights and obligations // under this license. //----------------------------------------------------------------------------- // Package meta provides the domain specific type 'meta'. package meta import ( "strconv" "strings" "time" ) // DescriptionType is a description of a specific key type. type DescriptionType struct { Name string IsSet bool } // 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 ( TypeBool = registerType("Boolean", false) TypeCredential = registerType("Credential", false) TypeEmpty = registerType("EString", false) TypeID = registerType("Identifier", false) TypeIDSet = registerType("IdentifierSet", true) TypeNumber = registerType("Number", false) TypeString = registerType("String", false) TypeTagSet = registerType("TagSet", true) TypeTimestamp = registerType("Timestamp", false) TypeURL = registerType("URL", false) TypeUnknown = registerType("Unknown", false) TypeWord = registerType("Word", false) TypeWordSet = registerType("WordSet", true) TypeZettelmarkup = registerType("Zettelmarkup", false) ) // Type returns a type hint for the given key. If no type hint is specified, // TypeUnknown is returned. func (m *Meta) Type(key string) *DescriptionType { return Type(key) } // Type returns a type hint for the given key. If no type hint is specified, // TypeUnknown is returned. func Type(key string) *DescriptionType { if k, ok := registeredKeys[key]; ok { return k.Type } return TypeUnknown } // SetList stores the given string list value under the given key. func (m *Meta) SetList(key string, values []string) { if key != KeyID { for i, val := range values { values[i] = trimValue(val) } m.pairs[key] = strings.Join(values, " ") } } // CleanTag removes the number charachter ('#') from a tag value. func CleanTag(tag string) string { if len(tag) > 1 && tag[0] == '#' { return tag[1:] } return tag } // SetNow stores the current timestamp under the given key. func (m *Meta) SetNow(key string) { m.Set(key, time.Now().Format("20060102150405")) } // 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("20060102150405", 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) { tags, ok := m.GetList(key) if !ok { return nil, false } for i, tag := range tags { tags[i] = CleanTag(tag) } return tags, len(tags) > 0 } // 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) (int, bool) { if value, ok := m.Get(key); ok { if num, err := strconv.Atoi(value); err == nil { return num, true } } return 0, false } |
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 | //----------------------------------------------------------------------------- // Copyright (c) 2020 Detlef Stern // // This file is part of zettelstore. // // Zettelstore is licensed under the latest version of the EUPL (European Union // Public License). Please see file LICENSE.txt for your rights and obligations // under this license. //----------------------------------------------------------------------------- // Package meta_test provides tests for the domain specific type 'meta'. package meta_test import ( "strconv" "testing" "time" "zettelstore.de/z/domain/id" "zettelstore.de/z/domain/meta" ) func TestNow(t *testing.T) { m := meta.New(id.Invalid) m.SetNow("key") val, ok := m.Get("key") if !ok { t.Error("Unable to get value of key") } if len(val) != 14 { t.Errorf("Value is not 14 digits long: %q", val) } if _, err := strconv.ParseInt(val, 10, 64); err != nil { t.Errorf("Unable to parse %q as an int64: %v", val, err) } if _, ok := m.GetTime("key"); !ok { t.Errorf("Unable to get time from value %q", val) } } func TestGetTime(t *testing.T) { testCases := []struct { value string valid bool exp time.Time }{ {"", false, time.Time{}}, {"1", false, time.Time{}}, {"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 | //----------------------------------------------------------------------------- // Copyright (c) 2020-2021 Detlef Stern // // This file is part of zettelstore. // // Zettelstore is licensed under the latest version of the EUPL (European Union // Public License). Please see file LICENSE.txt for your rights and obligations // under this license. //----------------------------------------------------------------------------- // Package meta provides the domain specific type 'meta'. package meta // Visibility enumerates the variations of the 'visibility' meta key. type Visibility int // Supported values for visibility. const ( _ Visibility = iota VisibilityUnknown VisibilityPublic VisibilityLogin VisibilityOwner VisibilityExpert ) var visMap = map[string]Visibility{ ValueVisibilityPublic: VisibilityPublic, ValueVisibilityLogin: VisibilityLogin, ValueVisibilityOwner: VisibilityOwner, ValueVisibilityExpert: VisibilityExpert, } // GetVisibility returns the visibility value of the given string func GetVisibility(val string) Visibility { if vis, ok := visMap[val]; ok { return vis } return VisibilityUnknown } // UserRole enumerates the supported values of meta key 'user-role'. type UserRole int // Supported values for user roles. const ( _ UserRole = iota UserRoleUnknown UserRoleReader UserRoleWriter UserRoleOwner ) var urMap = map[string]UserRole{ ValueUserRoleReader: UserRoleReader, ValueUserRoleWriter: UserRoleWriter, ValueUserRoleOwner: UserRoleOwner, } // GetUserRole role returns the user role of the given string. func GetUserRole(val string) UserRole { if ur, ok := urMap[val]; ok { return ur } return UserRoleUnknown } |
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 Detlef Stern // // This file is part of zettelstore. // // Zettelstore is licensed under 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 ( "bytes" "io" ) // Write writes a zettel meta to a writer. func (m *Meta) Write(w io.Writer, allowComputed bool) (int, error) { var buf bytes.Buffer for _, p := range m.Pairs(allowComputed) { buf.WriteString(p.Key) buf.WriteString(": ") buf.WriteString(p.Value) buf.WriteByte('\n') } return w.Write(buf.Bytes()) } var ( newline = []byte{'\n'} yamlSep = []byte{'-', '-', '-', '\n'} ) // WriteAsHeader writes the zettel meta to the writer, plus the separators func (m *Meta) WriteAsHeader(w io.Writer, allowComputed bool) (int, error) { var lb, lc, la int var err error if m.YamlSep { lb, err = w.Write(yamlSep) if err != nil { return lb, err } } lc, err = m.Write(w, allowComputed) if err != nil { return lb + lc, err } if m.YamlSep { la, err = w.Write(yamlSep) } else { la, err = w.Write(newline) } return lb + lc + la, err } |
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 | //----------------------------------------------------------------------------- // Copyright (c) 2020 Detlef Stern // // This file is part of zettelstore. // // Zettelstore is licensed under the latest version of the EUPL (European Union // Public License). Please see file LICENSE.txt for your rights and obligations // under this license. //----------------------------------------------------------------------------- // Package meta_test provides tests for the domain specific type 'meta'. package meta_test import ( "strings" "testing" "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(meta.KeyTitle, title) } if tags != nil { m.Set(meta.KeyTags, strings.Join(tags, " ")) } if syntax != "" { m.Set(meta.KeySyntax, syntax) } return m } func assertWriteMeta(t *testing.T, m *meta.Meta, expected string) { t.Helper() sb := strings.Builder{} m.Write(&sb, true) if got := sb.String(); got != expected { t.Errorf("\nExp: %q\ngot: %q", expected, got) } } func TestWriteMeta(t *testing.T) { assertWriteMeta(t, newMeta("", nil, ""), "") m := newMeta("TITLE", []string{"#t1", "#t2"}, "syntax") assertWriteMeta(t, m, "title: TITLE\ntags: #t1 #t2\nsyntax: syntax\n") m = newMeta("TITLE", nil, "") m.Set("user", "zettel") m.Set("auth", "basic") assertWriteMeta(t, m, "title: TITLE\nauth: basic\nuser: zettel\n") } |
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 | //----------------------------------------------------------------------------- // Copyright (c) 2020-2021 Detlef Stern // // This file is part of zettelstore. // // Zettelstore is licensed under the latest version of the EUPL (European Union // Public License). Please see file LICENSE.txt for your rights and obligations // under this license. //----------------------------------------------------------------------------- // Package domain provides domain specific types, constants, and functions. package domain import ( "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. } // Equal compares two zettel for equality. func (z Zettel) Equal(o Zettel, allowComputed bool) bool { return z.Meta.Equal(o.Meta, allowComputed) && z.Content == o.Content } |
Added encoder/buffer.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 Detlef Stern // // This file is part of zettelstore. // // Zettelstore is licensed under 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 ( "encoding/base64" "io" ) // BufWriter is a specialized buffered writer for encoding zettel. type BufWriter struct { w io.Writer // The io.Writer to write to err error // Collect error length int // Sum length buf []byte // Buffer to collect bytes } // NewBufWriter creates a new BufWriter func NewBufWriter(w io.Writer) BufWriter { return BufWriter{w: w, buf: make([]byte, 0, 4096)} } // Write writes the contents of p into the buffer. func (w *BufWriter) Write(p []byte) (int, error) { if w.err == nil { w.buf = append(w.buf, p...) if len(w.buf) > 2048 { w.flush() if w.err != nil { return 0, w.err } } return len(p), nil } return 0, w.err } // WriteString writes the contents of s into the buffer. func (w *BufWriter) WriteString(s string) (int, error) { return w.Write([]byte(s)) } // WriteStrings writes the contents of sl into the buffer. func (w *BufWriter) WriteStrings(sl ...string) { for _, s := range sl { w.WriteString(s) } } // WriteByte writes the content of b into the buffer. func (w *BufWriter) WriteByte(b byte) error { w.buf = append(w.buf, b) return nil } // WriteBytes writes the content of bs into the buffer. func (w *BufWriter) WriteBytes(bs ...byte) { w.buf = append(w.buf, bs...) } // WriteBase64 writes the content of p into the buffer, encoded with base64. func (w *BufWriter) WriteBase64(p []byte) { if w.err == nil { w.flush() } if w.err == nil { encoder := base64.NewEncoder(base64.StdEncoding, w.w) length, err := encoder.Write(p) w.length += length err1 := encoder.Close() if err == nil { w.err = err1 } else { w.err = err } } } // Flush writes any buffered data to the underlying io.Writer. It returns the // number of bytes written and an error if something went wrong. func (w *BufWriter) Flush() (int, error) { if w.err == nil { w.flush() } return w.length, w.err } func (w *BufWriter) flush() { length, err := w.w.Write(w.buf) w.buf = w.buf[:0] w.length += length w.err = err } |
Added encoder/encfun/encfun.go.
> > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 | //----------------------------------------------------------------------------- // Copyright (c) 2021 Detlef Stern // // This file is part of zettelstore. // // Zettelstore is licensed under the latest version of the EUPL (European Union // Public License). Please see file LICENSE.txt for your rights and obligations // under this license. //----------------------------------------------------------------------------- // Package encfun provides some helper function to work with encodings. package encfun import ( "strings" "zettelstore.de/z/ast" "zettelstore.de/z/domain/meta" "zettelstore.de/z/encoder" "zettelstore.de/z/parser" ) // MetaAsInlineSlice returns the value of the given metadata key as an inlince slice. func MetaAsInlineSlice(m *meta.Meta, key string) ast.InlineSlice { return parser.ParseMetadata(m.GetDefault(key, "")) } // MetaAsText returns the value of given metadata as text. func MetaAsText(m *meta.Meta, key string) string { textEncoder := encoder.Create("text", nil) var sb strings.Builder _, err := textEncoder.WriteInlines(&sb, MetaAsInlineSlice(m, key)) if err == nil { return sb.String() } return "" } |
Changes to encoder/encoder.go.
1 | //----------------------------------------------------------------------------- | | | < < < < > > < | | | | | < < < < | | | < < | < | > | | > | | | > > > > > > | | | | | | > > > > > > > > > > > > > | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 | //----------------------------------------------------------------------------- // Copyright (c) 2020-2021 Detlef Stern // // This file is part of zettelstore. // // Zettelstore is licensed under the latest version of the EUPL (European Union // Public License). Please see file LICENSE.txt for your rights and obligations // under this license. //----------------------------------------------------------------------------- // Package encoder provides a generic interface to encode the abstract syntax // tree into some text form. package encoder import ( "errors" "io" "log" "sort" "zettelstore.de/z/ast" "zettelstore.de/z/domain/meta" ) // Encoder is an interface that allows to encode different parts of a zettel. type Encoder interface { WriteZettel(io.Writer, *ast.ZettelNode, bool) (int, error) WriteMeta(io.Writer, *meta.Meta) (int, error) WriteContent(io.Writer, *ast.ZettelNode) (int, error) WriteBlocks(io.Writer, ast.BlockSlice) (int, error) WriteInlines(io.Writer, ast.InlineSlice) (int, error) } // 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(format string, env *Environment) Encoder { if info, ok := registry[format]; ok { return info.Create(env) } return nil } // Info stores some data about an encoder. type Info struct { Create func(*Environment) Encoder Default bool } var registry = map[string]Info{} var defFormat string // Register the encoder for later retrieval. func Register(format string, info Info) { if _, ok := registry[format]; ok { log.Fatalf("Writer with format %q already registered", format) } if info.Default { if defFormat != "" && defFormat != format { log.Fatalf("Default format already set: %q, new format: %q", defFormat, format) } defFormat = format } registry[format] = info } // GetFormats returns all registered formats, ordered by format name. func GetFormats() []string { result := make([]string, 0, len(registry)) for format := range registry { result = append(result, format) } sort.Strings(result) return result } // GetDefaultFormat returns the format that should be used as default. func GetDefaultFormat() string { if defFormat != "" { return defFormat } if _, ok := registry["json"]; ok { return "json" } log.Fatalf("No default format given") return "" } |
Deleted encoder/encoder_blob_test.go.
|
| < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < |
Deleted encoder/encoder_block_test.go.
|
| < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < |
Deleted encoder/encoder_inline_test.go.
|
| < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < |
Deleted encoder/encoder_test.go.
|
| < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < |
Added encoder/env.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 | //----------------------------------------------------------------------------- // Copyright (c) 2021 Detlef Stern // // This file is part of zettelstore. // // Zettelstore is licensed under the latest version of the EUPL (European Union // Public License). Please see file LICENSE.txt for your rights and obligations // under this license. //----------------------------------------------------------------------------- // Package encoder provides a generic interface to encode the abstract syntax // tree into some text form. package encoder import "zettelstore.de/z/ast" // Environment specifies all data and functions that affects encoding. type Environment struct { // Important for many encoder. LinkAdapter func(*ast.LinkNode) ast.InlineNode ImageAdapter func(*ast.ImageNode) ast.InlineNode CiteAdapter func(*ast.CiteNode) ast.InlineNode // Important for HTML encoder Lang string // default language Interactive bool // Encoded data will be placed in interactive content Xhtml bool // use XHTML syntax instead of HTML syntax MarkerExternal string // Marker after link to (external) material. NewWindow bool // open link in new window IgnoreMeta map[string]bool footnotes []*ast.FootnoteNode // Stores footnotes detected while encoding } // AdaptLink helps to call the link adapter. func (env *Environment) AdaptLink(ln *ast.LinkNode) (*ast.LinkNode, ast.InlineNode) { if env == nil || env.LinkAdapter == nil { return ln, nil } n := env.LinkAdapter(ln) if n == nil { return ln, nil } if ln2, ok := n.(*ast.LinkNode); ok { return ln2, nil } return nil, n } // AdaptImage helps to call the link adapter. func (env *Environment) AdaptImage(in *ast.ImageNode) (*ast.ImageNode, ast.InlineNode) { if env == nil || env.ImageAdapter == nil { return in, nil } n := env.ImageAdapter(in) if n == nil { return in, nil } if in2, ok := n.(*ast.ImageNode); ok { return in2, nil } return nil, n } // AdaptCite helps to call the link adapter. func (env *Environment) AdaptCite(cn *ast.CiteNode) (*ast.CiteNode, ast.InlineNode) { if env == nil || env.CiteAdapter == nil { return cn, nil } n := env.CiteAdapter(cn) if n == nil { return cn, nil } if cn2, ok := n.(*ast.CiteNode); ok { return cn2, nil } return nil, n } // IsInteractive returns true, if Interactive is enabled and currently embedded // interactive encoding will take place. func (env *Environment) IsInteractive(inInteractive bool) bool { return inInteractive && env != nil && env.Interactive } // IsXHTML return true, if XHTML is enabled. func (env *Environment) IsXHTML() bool { return env != nil && env.Xhtml } // HasNewWindow retruns true, if a new browser windows should be opened. func (env *Environment) HasNewWindow() bool { return env != nil && env.NewWindow } // AddFootnote adds a footnote node to the environment and returns the number of that footnote. func (env *Environment) AddFootnote(fn *ast.FootnoteNode) int { if env == nil { return 0 } env.footnotes = append(env.footnotes, fn) return len(env.footnotes) } // GetCleanFootnotes returns the list of remembered footnote and forgets about them. func (env *Environment) GetCleanFootnotes() []*ast.FootnoteNode { if env == nil { return nil } result := env.footnotes env.footnotes = nil return result } |
Added encoder/htmlenc/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 | //----------------------------------------------------------------------------- // Copyright (c) 2020-2021 Detlef Stern // // This file is part of zettelstore. // // Zettelstore is licensed under the latest version of the EUPL (European Union // Public License). Please see file LICENSE.txt for your rights and obligations // under this license. //----------------------------------------------------------------------------- // Package htmlenc encodes the abstract syntax tree into HTML5. package htmlenc import ( "fmt" "strconv" "strings" "zettelstore.de/z/ast" ) // VisitPara emits HTML code for a paragraph: <p>...</p> func (v *visitor) VisitPara(pn *ast.ParaNode) { v.b.WriteString("<p>") v.acceptInlineSlice(pn.Inlines) v.writeEndPara() } // VisitVerbatim emits HTML code for verbatim lines. func (v *visitor) VisitVerbatim(vn *ast.VerbatimNode) { switch vn.Code { case ast.VerbatimProg: oldVisible := v.visibleSpace if vn.Attrs != nil { v.visibleSpace = vn.Attrs.HasDefault() } v.b.WriteString("<pre><code") v.visitAttributes(vn.Attrs) v.b.WriteByte('>') for _, line := range vn.Lines { v.writeHTMLEscaped(line) v.b.WriteByte('\n') } v.b.WriteString("</code></pre>\n") v.visibleSpace = oldVisible case ast.VerbatimComment: if vn.Attrs.HasDefault() { v.b.WriteString("<!--\n") for _, line := range vn.Lines { v.writeHTMLEscaped(line) v.b.WriteByte('\n') } v.b.WriteString("-->\n") } case ast.VerbatimHTML: for _, line := range vn.Lines { if !ignoreHTMLText(line) { v.b.WriteStrings(line, "\n") } } default: panic(fmt.Sprintf("Unknown verbatim code %v", vn.Code)) } } var htmlSnippetsIgnore = []string{ "<script", "</script", "<iframe", "</iframe", } func ignoreHTMLText(s string) bool { lower := strings.ToLower(s) for _, snippet := range htmlSnippetsIgnore { if strings.Contains(lower, snippet) { return true } } return false } var specialSpanAttr = map[string]bool{ "example": true, "note": true, "tip": true, "important": true, "caution": true, "warning": true, } func processSpanAttributes(attrs *ast.Attributes) *ast.Attributes { if attrVal, ok := attrs.Get(""); ok { attrVal = strings.ToLower(attrVal) if specialSpanAttr[attrVal] { attrs = attrs.Clone() attrs.Remove("") attrs = attrs.AddClass("zs-indication").AddClass("zs-" + attrVal) } } return attrs } // VisitRegion writes HTML code for block regions. func (v *visitor) VisitRegion(rn *ast.RegionNode) { var code string attrs := rn.Attrs oldVerse := v.inVerse switch rn.Code { case ast.RegionSpan: code = "div" attrs = processSpanAttributes(attrs) case ast.RegionVerse: v.inVerse = true code = "div" case ast.RegionQuote: code = "blockquote" default: panic(fmt.Sprintf("Unknown region code %v", rn.Code)) } v.lang.push(attrs) defer v.lang.pop() v.b.WriteStrings("<", code) v.visitAttributes(attrs) v.b.WriteString(">\n") v.acceptBlockSlice(rn.Blocks) if len(rn.Inlines) > 0 { v.b.WriteString("<cite>") v.acceptInlineSlice(rn.Inlines) v.b.WriteString("</cite>\n") } v.b.WriteStrings("</", code, ">\n") v.inVerse = oldVerse } // VisitHeading writes the HTML code for a heading. func (v *visitor) VisitHeading(hn *ast.HeadingNode) { v.lang.push(hn.Attrs) defer v.lang.pop() lvl := hn.Level if lvl > 6 { lvl = 6 // HTML has H1..H6 } strLvl := strconv.Itoa(lvl) v.b.WriteStrings("<h", strLvl) v.visitAttributes(hn.Attrs) if slug := hn.Slug; len(slug) > 0 { v.b.WriteStrings(" id=\"", slug, "\"") } v.b.WriteByte('>') v.acceptInlineSlice(hn.Inlines) v.b.WriteStrings("</h", strLvl, ">\n") } // VisitHRule writes HTML code for a horizontal rule: <hr>. func (v *visitor) VisitHRule(hn *ast.HRuleNode) { v.b.WriteString("<hr") v.visitAttributes(hn.Attrs) if v.env.IsXHTML() { v.b.WriteString(" />\n") } else { v.b.WriteString(">\n") } } var listCode = map[ast.NestedListCode]string{ ast.NestedListOrdered: "ol", ast.NestedListUnordered: "ul", } // VisitNestedList writes HTML code for lists and blockquotes. func (v *visitor) VisitNestedList(ln *ast.NestedListNode) { v.lang.push(ln.Attrs) defer v.lang.pop() if ln.Code == ast.NestedListQuote { // NestedListQuote -> HTML <blockquote> doesn't use <li>...</li> v.writeQuotationList(ln) return } code, ok := listCode[ln.Code] if !ok { panic(fmt.Sprintf("Invalid list code %v", ln.Code)) } compact := isCompactList(ln.Items) v.b.WriteStrings("<", code) v.visitAttributes(ln.Attrs) v.b.WriteString(">\n") for _, item := range ln.Items { v.b.WriteString("<li>") v.writeItemSliceOrPara(item, compact) v.b.WriteString("</li>\n") } v.b.WriteStrings("</", code, ">\n") } func (v *visitor) writeQuotationList(ln *ast.NestedListNode) { v.b.WriteString("<blockquote>\n") inPara := false for _, item := range ln.Items { if pn := getParaItem(item); pn != nil { if inPara { v.b.WriteByte('\n') } else { v.b.WriteString("<p>") inPara = true } v.acceptInlineSlice(pn.Inlines) } else { if inPara { v.writeEndPara() inPara = false } v.acceptItemSlice(item) } } if inPara { v.writeEndPara() } v.b.WriteString("</blockquote>\n") } func getParaItem(its ast.ItemSlice) *ast.ParaNode { if len(its) != 1 { return nil } if pn, ok := its[0].(*ast.ParaNode); ok { return pn } return nil } func isCompactList(insl []ast.ItemSlice) bool { for _, ins := range insl { if !isCompactSlice(ins) { return false } } return true } func isCompactSlice(ins ast.ItemSlice) bool { if len(ins) < 1 { return true } if len(ins) == 1 { switch ins[0].(type) { case *ast.ParaNode, *ast.VerbatimNode, *ast.HRuleNode: return true case *ast.NestedListNode: return false } } return false } // writeItemSliceOrPara emits the content of a paragraph if the paragraph is // the only element of the block slice and if compact mode is true. Otherwise, // the item slice is emitted normally. func (v *visitor) writeItemSliceOrPara(ins ast.ItemSlice, compact bool) { if compact && len(ins) == 1 { if para, ok := ins[0].(*ast.ParaNode); ok { v.acceptInlineSlice(para.Inlines) return } } v.acceptItemSlice(ins) } func (v *visitor) writeDescriptionsSlice(ds ast.DescriptionSlice) { if len(ds) == 1 { if para, ok := ds[0].(*ast.ParaNode); ok { v.acceptInlineSlice(para.Inlines) return } } for _, dn := range ds { dn.Accept(v) } } // VisitDescriptionList emits a HTML description list. func (v *visitor) VisitDescriptionList(dn *ast.DescriptionListNode) { v.b.WriteString("<dl>\n") for _, descr := range dn.Descriptions { v.b.WriteString("<dt>") v.acceptInlineSlice(descr.Term) v.b.WriteString("</dt>\n") for _, b := range descr.Descriptions { v.b.WriteString("<dd>") v.writeDescriptionsSlice(b) v.b.WriteString("</dd>\n") } } v.b.WriteString("</dl>\n") } // VisitTable emits a HTML table. func (v *visitor) VisitTable(tn *ast.TableNode) { v.b.WriteString("<table>\n") if len(tn.Header) > 0 { v.b.WriteString("<thead>\n") v.writeRow(tn.Header, "<th", "</th>") v.b.WriteString("</thead>\n") } if len(tn.Rows) > 0 { v.b.WriteString("<tbody>\n") for _, row := range tn.Rows { v.writeRow(row, "<td", "</td>") } v.b.WriteString("</tbody>\n") } v.b.WriteString("</table>\n") } var alignStyle = map[ast.Alignment]string{ ast.AlignDefault: ">", ast.AlignLeft: " style=\"text-align:left\">", ast.AlignCenter: " style=\"text-align:center\">", ast.AlignRight: " style=\"text-align:right\">", } func (v *visitor) writeRow(row ast.TableRow, cellStart, cellEnd string) { v.b.WriteString("<tr>") for _, cell := range row { v.b.WriteString(cellStart) if len(cell.Inlines) == 0 { v.b.WriteByte('>') } else { v.b.WriteString(alignStyle[cell.Align]) v.acceptInlineSlice(cell.Inlines) } v.b.WriteString(cellEnd) } v.b.WriteString("</tr>\n") } // VisitBLOB writes the binary object as a value. func (v *visitor) VisitBLOB(bn *ast.BLOBNode) { switch bn.Syntax { case "gif", "jpeg", "png": v.b.WriteStrings("<img src=\"data:image/", bn.Syntax, ";base64,") v.b.WriteBase64(bn.Blob) v.b.WriteString("\" title=\"") v.writeQuotedEscaped(bn.Title) v.b.WriteString("\">\n") default: v.b.WriteStrings("<p class=\"error\">Unable to display BLOB with syntax '", bn.Syntax, "'.</p>\n") } } func (v *visitor) writeEndPara() { v.b.WriteString("</p>\n") } |
Changes to encoder/htmlenc/htmlenc.go.
1 | //----------------------------------------------------------------------------- | | | < < < | < < < < | | | < | < | < < < < < < < < < | | < < < | | | < | < > | < < < > | < < < < < < < | < < < < < < < < < | | | < > | | | < < < < < < < < < < | < | < < | < | | | | | | > > > > > > | > > > | | | | | < < < < < < < | | | | < < | | < < < < | < > | > | < | | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 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-2021 Detlef Stern // // This file is part of zettelstore. // // Zettelstore is licensed under the latest version of the EUPL (European Union // Public License). Please see file LICENSE.txt for your rights and obligations // under this license. //----------------------------------------------------------------------------- // Package htmlenc encodes the abstract syntax tree into HTML5. package htmlenc import ( "io" "strings" "zettelstore.de/z/ast" "zettelstore.de/z/domain/meta" "zettelstore.de/z/encoder" "zettelstore.de/z/encoder/encfun" "zettelstore.de/z/parser" ) func init() { encoder.Register("html", encoder.Info{ Create: func(env *encoder.Environment) encoder.Encoder { return &htmlEncoder{env: env} }, }) } type htmlEncoder struct { env *encoder.Environment } // WriteZettel encodes a full zettel as HTML5. func (he *htmlEncoder) WriteZettel(w io.Writer, zn *ast.ZettelNode, inhMeta bool) (int, error) { v := newVisitor(he, w) if !he.env.IsXHTML() { v.b.WriteString("<!DOCTYPE html>\n") } if env := he.env; env != nil && env.Lang == "" { v.b.WriteStrings("<html>\n<head>") } else { v.b.WriteStrings("<html lang=\"", env.Lang, "\">") } v.b.WriteString("\n<head>\n<meta charset=\"utf-8\">\n") v.b.WriteStrings("<title>", encfun.MetaAsText(zn.InhMeta, meta.KeyTitle), "</title>") if inhMeta { v.acceptMeta(zn.InhMeta) } else { v.acceptMeta(zn.Meta) } v.b.WriteString("\n</head>\n<body>\n") v.acceptBlockSlice(zn.Ast) v.writeEndnotes() v.b.WriteString("</body>\n</html>") length, err := v.b.Flush() return length, err } // WriteMeta encodes meta data as HTML5. func (he *htmlEncoder) WriteMeta(w io.Writer, m *meta.Meta) (int, error) { v := newVisitor(he, w) // Write title if title, ok := m.Get(meta.KeyTitle); ok { textEnc := encoder.Create("text", nil) var sb strings.Builder textEnc.WriteInlines(&sb, parser.ParseMetadata(title)) v.b.WriteStrings("<meta name=\"zs-", meta.KeyTitle, "\" content=\"") v.writeQuotedEscaped(sb.String()) v.b.WriteString("\">") } // Write other metadata v.acceptMeta(m) length, err := v.b.Flush() return length, err } func (he *htmlEncoder) WriteContent(w io.Writer, zn *ast.ZettelNode) (int, error) { return he.WriteBlocks(w, zn.Ast) } // WriteBlocks encodes a block slice. func (he *htmlEncoder) WriteBlocks(w io.Writer, bs ast.BlockSlice) (int, error) { v := newVisitor(he, w) v.acceptBlockSlice(bs) v.writeEndnotes() length, err := v.b.Flush() return length, err } // WriteInlines writes an inline slice to the writer func (he *htmlEncoder) WriteInlines(w io.Writer, is ast.InlineSlice) (int, error) { v := newVisitor(he, w) if env := he.env; env != nil { v.inInteractive = env.Interactive } v.acceptInlineSlice(is) length, err := v.b.Flush() return length, err } |
Added encoder/htmlenc/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 | //----------------------------------------------------------------------------- // Copyright (c) 2020-2021 Detlef Stern // // This file is part of zettelstore. // // Zettelstore is licensed under the latest version of the EUPL (European Union // Public License). Please see file LICENSE.txt for your rights and obligations // under this license. //----------------------------------------------------------------------------- // Package htmlenc encodes the abstract syntax tree into HTML5. package htmlenc import ( "fmt" "strconv" "strings" "zettelstore.de/z/ast" "zettelstore.de/z/domain/meta" ) // VisitText writes text content. func (v *visitor) VisitText(tn *ast.TextNode) { v.writeHTMLEscaped(tn.Text) } // VisitTag writes tag content. func (v *visitor) VisitTag(tn *ast.TagNode) { // TODO: erst mal als span. Link wäre gut, muss man vermutlich via Callback lösen. v.b.WriteString("<span class=\"zettel-tag\">#") v.writeHTMLEscaped(tn.Tag) v.b.WriteString("</span>") } // VisitSpace emits a white space. func (v *visitor) VisitSpace(sn *ast.SpaceNode) { if v.inVerse || v.env.IsXHTML() { v.b.WriteString(sn.Lexeme) } else { v.b.WriteByte(' ') } } // VisitBreak writes HTML code for line breaks. func (v *visitor) VisitBreak(bn *ast.BreakNode) { if bn.Hard { if v.env.IsXHTML() { v.b.WriteString("<br />\n") } else { v.b.WriteString("<br>\n") } } else { v.b.WriteByte('\n') } } // VisitLink writes HTML code for links. func (v *visitor) VisitLink(ln *ast.LinkNode) { ln, n := v.env.AdaptLink(ln) if n != nil { n.Accept(v) return } v.lang.push(ln.Attrs) defer v.lang.pop() switch ln.Ref.State { case ast.RefStateSelf, ast.RefStateFound, ast.RefStateHosted, ast.RefStateBased: v.writeAHref(ln.Ref, ln.Attrs, ln.Inlines) case ast.RefStateBroken: attrs := ln.Attrs.Clone() attrs = attrs.Set("class", "zs-broken") attrs = attrs.Set("title", "Zettel not found") // l10n v.writeAHref(ln.Ref, attrs, ln.Inlines) case ast.RefStateExternal: attrs := ln.Attrs.Clone() attrs = attrs.Set("class", "zs-external") if v.env.HasNewWindow() { attrs = attrs.Set("target", "_blank").Set("rel", "noopener noreferrer") } v.writeAHref(ln.Ref, attrs, ln.Inlines) if v.env != nil { v.b.WriteString(v.env.MarkerExternal) } default: if v.env.IsInteractive(v.inInteractive) { v.writeSpan(ln.Inlines, ln.Attrs) return } v.b.WriteString("<a href=\"") v.writeQuotedEscaped(ln.Ref.Value) v.b.WriteByte('"') v.visitAttributes(ln.Attrs) v.b.WriteByte('>') v.inInteractive = true v.acceptInlineSlice(ln.Inlines) v.inInteractive = false v.b.WriteString("</a>") } } func (v *visitor) writeAHref(ref *ast.Reference, attrs *ast.Attributes, ins ast.InlineSlice) { if v.env.IsInteractive(v.inInteractive) { v.writeSpan(ins, attrs) return } v.b.WriteString("<a href=\"") v.writeReference(ref) v.b.WriteByte('"') v.visitAttributes(attrs) v.b.WriteByte('>') v.inInteractive = true v.acceptInlineSlice(ins) v.inInteractive = false v.b.WriteString("</a>") } // VisitImage writes HTML code for images. func (v *visitor) VisitImage(in *ast.ImageNode) { in, n := v.env.AdaptImage(in) if n != nil { n.Accept(v) return } v.lang.push(in.Attrs) defer v.lang.pop() if in.Ref == nil { v.b.WriteString("<img src=\"data:image/") switch in.Syntax { case "svg": v.b.WriteString("svg+xml;utf8,") v.writeQuotedEscaped(string(in.Blob)) default: v.b.WriteStrings(in.Syntax, ";base64,") v.b.WriteBase64(in.Blob) } } else { v.b.WriteString("<img src=\"") v.writeReference(in.Ref) } v.b.WriteString("\" alt=\"") v.acceptInlineSlice(in.Inlines) v.b.WriteByte('"') v.visitAttributes(in.Attrs) if v.env.IsXHTML() { v.b.WriteString(" />") } else { v.b.WriteByte('>') } } // VisitCite writes code for citations. func (v *visitor) VisitCite(cn *ast.CiteNode) { cn, n := v.env.AdaptCite(cn) if n != nil { n.Accept(v) return } if cn == nil { return } v.lang.push(cn.Attrs) defer v.lang.pop() v.b.WriteString(cn.Key) if len(cn.Inlines) > 0 { v.b.WriteString(", ") v.acceptInlineSlice(cn.Inlines) } } // VisitFootnote write HTML code for a footnote. func (v *visitor) VisitFootnote(fn *ast.FootnoteNode) { v.lang.push(fn.Attrs) defer v.lang.pop() if v.env.IsInteractive(v.inInteractive) { return } n := strconv.Itoa(v.env.AddFootnote(fn)) v.b.WriteStrings("<sup id=\"fnref:", n, "\"><a href=\"#fn:", n, "\" class=\"zs-footnote-ref\" role=\"doc-noteref\">", n, "</a></sup>") // TODO: what to do with Attrs? } // VisitMark writes HTML code to mark a position. func (v *visitor) VisitMark(mn *ast.MarkNode) { if v.env.IsInteractive(v.inInteractive) { return } if len(mn.Text) > 0 { v.b.WriteStrings("<a id=\"", mn.Text, "\"></a>") } } // VisitFormat write HTML code for formatting text. func (v *visitor) VisitFormat(fn *ast.FormatNode) { v.lang.push(fn.Attrs) defer v.lang.pop() var code string attrs := fn.Attrs switch fn.Code { case ast.FormatItalic: code = "i" case ast.FormatEmph: code = "em" case ast.FormatBold: code = "b" case ast.FormatStrong: code = "strong" case ast.FormatUnder: code = "u" // TODO: ändern in <span class="XXX"> case ast.FormatInsert: code = "ins" case ast.FormatStrike: code = "s" case ast.FormatDelete: code = "del" case ast.FormatSuper: code = "sup" case ast.FormatSub: code = "sub" case ast.FormatQuotation: code = "q" case ast.FormatSmall: code = "small" case ast.FormatSpan: v.writeSpan(fn.Inlines, processSpanAttributes(attrs)) return case ast.FormatMonospace: code = "span" attrs = attrs.Set("style", "font-family:monospace") case ast.FormatQuote: v.visitQuotes(fn) return default: panic(fmt.Sprintf("Unknown format code %v", fn.Code)) } v.b.WriteStrings("<", code) v.visitAttributes(attrs) v.b.WriteByte('>') v.acceptInlineSlice(fn.Inlines) v.b.WriteStrings("</", code, ">") } func (v *visitor) writeSpan(ins ast.InlineSlice, attrs *ast.Attributes) { v.b.WriteString("<span") v.visitAttributes(attrs) v.b.WriteByte('>') v.acceptInlineSlice(ins) v.b.WriteString("</span>") } var langQuotes = map[string][2]string{ meta.ValueLangEN: {"“", "”"}, "de": {"„", "“"}, "fr": {"« ", " »"}, } func getQuotes(lang string) (string, string) { langFields := strings.FieldsFunc(lang, func(r rune) bool { return r == '-' || r == '_' }) for len(langFields) > 0 { langSup := strings.Join(langFields, "-") quotes, ok := langQuotes[langSup] if ok { return quotes[0], quotes[1] } langFields = langFields[0 : len(langFields)-1] } return "\"", "\"" } func (v *visitor) visitQuotes(fn *ast.FormatNode) { _, withSpan := fn.Attrs.Get("lang") if withSpan { v.b.WriteString("<span") v.visitAttributes(fn.Attrs) v.b.WriteByte('>') } openingQ, closingQ := getQuotes(v.lang.top()) v.b.WriteString(openingQ) v.acceptInlineSlice(fn.Inlines) v.b.WriteString(closingQ) if withSpan { v.b.WriteString("</span>") } } // VisitLiteral write HTML code for literal inline text. func (v *visitor) VisitLiteral(ln *ast.LiteralNode) { switch ln.Code { case ast.LiteralProg: v.writeLiteral("<code", "</code>", ln.Attrs, ln.Text) case ast.LiteralKeyb: v.writeLiteral("<kbd", "</kbd>", ln.Attrs, ln.Text) case ast.LiteralOutput: v.writeLiteral("<samp", "</samp>", ln.Attrs, ln.Text) case ast.LiteralComment: v.b.WriteString("<!-- ") v.writeHTMLEscaped(ln.Text) // writeCommentEscaped v.b.WriteString(" -->") case ast.LiteralHTML: if !ignoreHTMLText(ln.Text) { v.b.WriteString(ln.Text) } default: panic(fmt.Sprintf("Unknown literal code %v", ln.Code)) } } func (v *visitor) writeLiteral(codeS, codeE string, attrs *ast.Attributes, text string) { oldVisible := v.visibleSpace if attrs != nil { v.visibleSpace = attrs.HasDefault() } v.b.WriteString(codeS) v.visitAttributes(attrs) v.b.WriteByte('>') v.writeHTMLEscaped(text) v.b.WriteString(codeE) v.visibleSpace = oldVisible } |
Added encoder/htmlenc/langstack.go.
> > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 | //----------------------------------------------------------------------------- // Copyright (c) 2020 Detlef Stern // // This file is part of zettelstore. // // Zettelstore is licensed under the latest version of the EUPL (European Union // Public License). Please see file LICENSE.txt for your rights and obligations // under this license. //----------------------------------------------------------------------------- // Package htmlenc encodes the abstract syntax tree into HTML5. package htmlenc import "zettelstore.de/z/ast" type langStack struct { items []string } func newLangStack(lang string) langStack { items := make([]string, 1, 16) items[0] = lang return langStack{items} } func (s langStack) top() string { return s.items[len(s.items)-1] } func (s *langStack) pop() { s.items = s.items[0 : len(s.items)-1] } func (s *langStack) push(attrs *ast.Attributes) { if value, ok := attrs.Get("lang"); ok { s.items = append(s.items, value) } else { s.items = append(s.items, s.top()) } } |
Added encoder/htmlenc/langstack_test.go.
> > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 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) 2020 Detlef Stern // // This file is part of zettelstore. // // Zettelstore is licensed under the latest version of the EUPL (European Union // Public License). Please see file LICENSE.txt for your rights and obligations // under this license. //----------------------------------------------------------------------------- // Package htmlenc encodes the abstract syntax tree into HTML5. package htmlenc import ( "testing" "zettelstore.de/z/ast" ) func TestStackSimple(t *testing.T) { exp := "de" s := newLangStack(exp) if got := s.top(); got != exp { t.Errorf("Init: expected %q, but got %q", exp, got) return } a := &ast.Attributes{} s.push(a) if got := s.top(); exp != got { t.Errorf("Empty push: expected %q, but got %q", exp, got) } exp2 := "en" a = a.Set("lang", exp2) s.push(a) if got := s.top(); exp2 != got { t.Errorf("Full push: expected %q, but got %q", exp2, got) } s.pop() if got := s.top(); exp != got { t.Errorf("pop: expected %q, but got %q", exp, got) } } |
Added encoder/htmlenc/visitor.go.
> > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 | //----------------------------------------------------------------------------- // Copyright (c) 2020-2021 Detlef Stern // // This file is part of zettelstore. // // Zettelstore is licensed under the latest version of the EUPL (European Union // Public License). Please see file LICENSE.txt for your rights and obligations // under this license. //----------------------------------------------------------------------------- // Package htmlenc encodes the abstract syntax tree into HTML5. package htmlenc import ( "io" "sort" "strconv" "strings" "zettelstore.de/z/ast" "zettelstore.de/z/domain/meta" "zettelstore.de/z/encoder" "zettelstore.de/z/strfun" ) // visitor writes the abstract syntax tree to an io.Writer. type visitor struct { env *encoder.Environment b encoder.BufWriter visibleSpace bool // Show space character in raw text inVerse bool // In verse block inInteractive bool // Rendered interactive HTML code lang langStack } func newVisitor(he *htmlEncoder, w io.Writer) *visitor { var lang string if he.env != nil { lang = he.env.Lang } return &visitor{ env: he.env, b: encoder.NewBufWriter(w), lang: newLangStack(lang), } } var mapMetaKey = map[string]string{ meta.KeyCopyright: "copyright", meta.KeyLicense: "license", } func (v *visitor) acceptMeta(m *meta.Meta) { for _, pair := range m.Pairs(true) { if env := v.env; env != nil && env.IgnoreMeta[pair.Key] { continue } if pair.Key == meta.KeyTitle { continue } if pair.Key == meta.KeyTags { v.writeTags(pair.Value) } else if key, ok := mapMetaKey[pair.Key]; ok { v.writeMeta("", key, pair.Value) } else { v.writeMeta("zs-", pair.Key, pair.Value) } } } func (v *visitor) writeTags(tags string) { v.b.WriteString("\n<meta name=\"keywords\" content=\"") for i, val := range meta.ListFromValue(tags) { if i > 0 { v.b.WriteString(", ") } v.writeQuotedEscaped(strings.TrimPrefix(val, "#")) } v.b.WriteString("\">") } func (v *visitor) writeMeta(prefix, key, value string) { v.b.WriteStrings("\n<meta name=\"", prefix, key, "\" content=\"") v.writeQuotedEscaped(value) v.b.WriteString("\">") } func (v *visitor) acceptBlockSlice(bns ast.BlockSlice) { for _, bn := range bns { bn.Accept(v) } } func (v *visitor) acceptItemSlice(ins ast.ItemSlice) { for _, in := range ins { in.Accept(v) } } func (v *visitor) acceptInlineSlice(ins ast.InlineSlice) { for _, in := range ins { in.Accept(v) } } func (v *visitor) writeEndnotes() { footnotes := v.env.GetCleanFootnotes() if len(footnotes) > 0 { v.b.WriteString("<ol class=\"zs-endnotes\">\n") for i := 0; i < len(footnotes); i++ { // Do not use a range loop above, because a footnote may contain // a footnote. Therefore v.enc.footnote may grow during the loop. fn := footnotes[i] n := strconv.Itoa(i + 1) v.b.WriteStrings("<li id=\"fn:", n, "\" role=\"doc-endnote\">") v.acceptInlineSlice(fn.Inlines) v.b.WriteStrings( " <a href=\"#fnref:", n, "\" class=\"zs-footnote-backref\" role=\"doc-backlink\">↩︎</a></li>\n") } v.b.WriteString("</ol>\n") } } // visitAttributes write HTML attributes func (v *visitor) visitAttributes(a *ast.Attributes) { if a == nil || len(a.Attrs) == 0 { return } keys := make([]string, 0, len(a.Attrs)) for k := range a.Attrs { if k != "-" { keys = append(keys, k) } } sort.Strings(keys) for _, k := range keys { if k == "" || k == "-" { continue } v.b.WriteStrings(" ", k) vl := a.Attrs[k] if len(vl) > 0 { v.b.WriteString("=\"") v.writeQuotedEscaped(vl) v.b.WriteByte('"') } } } func (v *visitor) writeHTMLEscaped(s string) { strfun.HTMLEscape(&v.b, s, v.visibleSpace) } func (v *visitor) writeQuotedEscaped(s string) { strfun.HTMLAttrEscape(&v.b, s) } func (v *visitor) writeReference(ref *ast.Reference) { if ref.URL == nil { v.writeHTMLEscaped(ref.Value) return } v.b.WriteString(ref.URL.String()) } |
Added encoder/jsonenc/djsonenc.go.
> > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 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 | //----------------------------------------------------------------------------- // Copyright (c) 2020-2021 Detlef Stern // // This file is part of zettelstore. // // Zettelstore is licensed under the latest version of the EUPL (European Union // Public License). Please see file LICENSE.txt for your rights and obligations // under this license. //----------------------------------------------------------------------------- // Package jsonenc encodes the abstract syntax tree into JSON. package jsonenc import ( "fmt" "io" "sort" "strconv" "zettelstore.de/z/ast" "zettelstore.de/z/domain/meta" "zettelstore.de/z/encoder" "zettelstore.de/z/encoder/encfun" ) func init() { encoder.Register("djson", encoder.Info{ Create: func(env *encoder.Environment) encoder.Encoder { return &jsonDetailEncoder{env: env} }, }) } type jsonDetailEncoder struct { env *encoder.Environment } // WriteZettel writes the encoded zettel to the writer. func (je *jsonDetailEncoder) WriteZettel(w io.Writer, zn *ast.ZettelNode, inhMeta bool) (int, error) { v := newDetailVisitor(w, je) v.b.WriteString("{\"meta\":{\"title\":") v.acceptInlineSlice(encfun.MetaAsInlineSlice(zn.InhMeta, meta.KeyTitle)) if inhMeta { v.writeMeta(zn.InhMeta) } else { v.writeMeta(zn.Meta) } v.b.WriteByte('}') v.b.WriteString(",\"content\":") v.acceptBlockSlice(zn.Ast) v.b.WriteByte('}') length, err := v.b.Flush() return length, err } // WriteMeta encodes meta data as JSON. func (je *jsonDetailEncoder) WriteMeta(w io.Writer, m *meta.Meta) (int, error) { v := newDetailVisitor(w, je) v.b.WriteString("{\"title\":") v.acceptInlineSlice(encfun.MetaAsInlineSlice(m, meta.KeyTitle)) v.writeMeta(m) v.b.WriteByte('}') length, err := v.b.Flush() return length, err } func (je *jsonDetailEncoder) WriteContent(w io.Writer, zn *ast.ZettelNode) (int, error) { return je.WriteBlocks(w, zn.Ast) } // WriteBlocks writes a block slice to the writer func (je *jsonDetailEncoder) WriteBlocks(w io.Writer, bs ast.BlockSlice) (int, error) { v := newDetailVisitor(w, je) v.acceptBlockSlice(bs) length, err := v.b.Flush() return length, err } // WriteInlines writes an inline slice to the writer func (je *jsonDetailEncoder) WriteInlines(w io.Writer, is ast.InlineSlice) (int, error) { v := newDetailVisitor(w, je) v.acceptInlineSlice(is) length, err := v.b.Flush() return length, err } // detailVisitor writes the abstract syntax tree to an io.Writer. type detailVisitor struct { b encoder.BufWriter env *encoder.Environment } func newDetailVisitor(w io.Writer, je *jsonDetailEncoder) *detailVisitor { return &detailVisitor{b: encoder.NewBufWriter(w), env: je.env} } // VisitPara emits JSON code for a paragraph. func (v *detailVisitor) VisitPara(pn *ast.ParaNode) { v.writeNodeStart("Para") v.writeContentStart('i') v.acceptInlineSlice(pn.Inlines) v.b.WriteByte('}') } var verbatimCode = map[ast.VerbatimCode]string{ ast.VerbatimProg: "CodeBlock", ast.VerbatimComment: "CommentBlock", ast.VerbatimHTML: "HTMLBlock", } // VisitVerbatim emits JSON code for verbatim lines. func (v *detailVisitor) VisitVerbatim(vn *ast.VerbatimNode) { code, ok := verbatimCode[vn.Code] if !ok { panic(fmt.Sprintf("Unknown verbatim code %v", vn.Code)) } v.writeNodeStart(code) v.visitAttributes(vn.Attrs) v.writeContentStart('l') for i, line := range vn.Lines { if i > 0 { v.b.WriteByte(',') } writeEscaped(&v.b, line) } v.b.WriteString("]}") } var regionCode = map[ast.RegionCode]string{ ast.RegionSpan: "SpanBlock", ast.RegionQuote: "QuoteBlock", ast.RegionVerse: "VerseBlock", } // VisitRegion writes JSON code for block regions. func (v *detailVisitor) VisitRegion(rn *ast.RegionNode) { code, ok := regionCode[rn.Code] if !ok { panic(fmt.Sprintf("Unknown region code %v", rn.Code)) } v.writeNodeStart(code) v.visitAttributes(rn.Attrs) v.writeContentStart('b') v.acceptBlockSlice(rn.Blocks) if len(rn.Inlines) > 0 { v.writeContentStart('i') v.acceptInlineSlice(rn.Inlines) } v.b.WriteByte('}') } // VisitHeading writes the JSON code for a heading. func (v *detailVisitor) VisitHeading(hn *ast.HeadingNode) { v.writeNodeStart("Heading") v.visitAttributes(hn.Attrs) v.writeContentStart('n') v.b.WriteString(strconv.Itoa(hn.Level)) if slug := hn.Slug; len(slug) > 0 { v.writeContentStart('s') v.b.WriteStrings("\"", slug, "\"") } v.writeContentStart('i') v.acceptInlineSlice(hn.Inlines) v.b.WriteByte('}') } // VisitHRule writes JSON code for a horizontal rule: <hr>. func (v *detailVisitor) VisitHRule(hn *ast.HRuleNode) { v.writeNodeStart("Hrule") v.visitAttributes(hn.Attrs) v.b.WriteByte('}') } var listCode = map[ast.NestedListCode]string{ ast.NestedListOrdered: "OrderedList", ast.NestedListUnordered: "BulletList", ast.NestedListQuote: "QuoteList", } // VisitNestedList writes JSON code for lists and blockquotes. func (v *detailVisitor) VisitNestedList(ln *ast.NestedListNode) { v.writeNodeStart(listCode[ln.Code]) v.writeContentStart('c') for i, item := range ln.Items { if i > 0 { v.b.WriteByte(',') } v.acceptItemSlice(item) } v.b.WriteString("]}") } // VisitDescriptionList emits a JSON description list. func (v *detailVisitor) VisitDescriptionList(dn *ast.DescriptionListNode) { v.writeNodeStart("DescriptionList") v.writeContentStart('g') for i, def := range dn.Descriptions { if i > 0 { v.b.WriteByte(',') } v.b.WriteByte('[') v.acceptInlineSlice(def.Term) if len(def.Descriptions) > 0 { for _, b := range def.Descriptions { v.b.WriteByte(',') v.acceptDescriptionSlice(b) } } v.b.WriteByte(']') } v.b.WriteString("]}") } // VisitTable emits a JSON table. func (v *detailVisitor) VisitTable(tn *ast.TableNode) { v.writeNodeStart("Table") v.writeContentStart('p') // Table header v.b.WriteByte('[') for i, cell := range tn.Header { if i > 0 { v.b.WriteByte(',') } v.writeCell(cell) } v.b.WriteString("],") // Table rows v.b.WriteByte('[') for i, row := range tn.Rows { if i > 0 { v.b.WriteByte(',') } v.b.WriteByte('[') for j, cell := range row { if j > 0 { v.b.WriteByte(',') } v.writeCell(cell) } v.b.WriteByte(']') } v.b.WriteString("]]}") } var alignmentCode = map[ast.Alignment]string{ ast.AlignDefault: "[\"\",", ast.AlignLeft: "[\"<\",", ast.AlignCenter: "[\":\",", ast.AlignRight: "[\">\",", } func (v *detailVisitor) writeCell(cell *ast.TableCell) { v.b.WriteString(alignmentCode[cell.Align]) v.acceptInlineSlice(cell.Inlines) v.b.WriteByte(']') } // VisitBLOB writes the binary object as a value. func (v *detailVisitor) VisitBLOB(bn *ast.BLOBNode) { v.writeNodeStart("Blob") v.writeContentStart('q') writeEscaped(&v.b, bn.Title) v.writeContentStart('s') writeEscaped(&v.b, bn.Syntax) v.writeContentStart('o') v.b.WriteBase64(bn.Blob) v.b.WriteString("\"}") } // VisitText writes text content. func (v *detailVisitor) VisitText(tn *ast.TextNode) { v.writeNodeStart("Text") v.writeContentStart('s') writeEscaped(&v.b, tn.Text) v.b.WriteByte('}') } // VisitTag writes tag content. func (v *detailVisitor) VisitTag(tn *ast.TagNode) { v.writeNodeStart("Tag") v.writeContentStart('s') writeEscaped(&v.b, tn.Tag) v.b.WriteByte('}') } // VisitSpace emits a white space. func (v *detailVisitor) VisitSpace(sn *ast.SpaceNode) { v.writeNodeStart("Space") if l := len(sn.Lexeme); l > 1 { v.writeContentStart('n') v.b.WriteString(strconv.Itoa(l)) } v.b.WriteByte('}') } // VisitBreak writes JSON code for line breaks. func (v *detailVisitor) VisitBreak(bn *ast.BreakNode) { if bn.Hard { v.writeNodeStart("Hard") } else { v.writeNodeStart("Soft") } v.b.WriteByte('}') } var mapRefState = map[ast.RefState]string{ ast.RefStateInvalid: "invalid", ast.RefStateZettel: "zettel", ast.RefStateSelf: "self", ast.RefStateFound: "zettel", ast.RefStateBroken: "broken", ast.RefStateHosted: "local", ast.RefStateBased: "based", ast.RefStateExternal: "external", } // VisitLink writes JSON code for links. func (v *detailVisitor) VisitLink(ln *ast.LinkNode) { ln, n := v.env.AdaptLink(ln) if n != nil { n.Accept(v) return } v.writeNodeStart("Link") v.visitAttributes(ln.Attrs) v.writeContentStart('q') writeEscaped(&v.b, mapRefState[ln.Ref.State]) v.writeContentStart('s') writeEscaped(&v.b, ln.Ref.String()) v.writeContentStart('i') v.acceptInlineSlice(ln.Inlines) v.b.WriteByte('}') } // VisitImage writes JSON code for images. func (v *detailVisitor) VisitImage(in *ast.ImageNode) { in, n := v.env.AdaptImage(in) if n != nil { n.Accept(v) return } v.writeNodeStart("Image") v.visitAttributes(in.Attrs) if in.Ref == nil { v.writeContentStart('j') v.b.WriteString("\"s\":") writeEscaped(&v.b, in.Syntax) switch in.Syntax { case "svg": v.writeContentStart('q') writeEscaped(&v.b, string(in.Blob)) default: v.writeContentStart('o') v.b.WriteBase64(in.Blob) v.b.WriteByte('"') } v.b.WriteByte('}') } else { v.writeContentStart('s') writeEscaped(&v.b, in.Ref.String()) } if len(in.Inlines) > 0 { v.writeContentStart('i') v.acceptInlineSlice(in.Inlines) } v.b.WriteByte('}') } // VisitCite writes code for citations. func (v *detailVisitor) VisitCite(cn *ast.CiteNode) { v.writeNodeStart("Cite") v.visitAttributes(cn.Attrs) v.writeContentStart('s') writeEscaped(&v.b, cn.Key) if len(cn.Inlines) > 0 { v.writeContentStart('i') v.acceptInlineSlice(cn.Inlines) } v.b.WriteByte('}') } // VisitFootnote write JSON code for a footnote. func (v *detailVisitor) VisitFootnote(fn *ast.FootnoteNode) { v.writeNodeStart("Footnote") v.visitAttributes(fn.Attrs) v.writeContentStart('i') v.acceptInlineSlice(fn.Inlines) v.b.WriteByte('}') } // VisitMark writes JSON code to mark a position. func (v *detailVisitor) VisitMark(mn *ast.MarkNode) { v.writeNodeStart("Mark") if len(mn.Text) > 0 { v.writeContentStart('s') writeEscaped(&v.b, mn.Text) } v.b.WriteByte('}') } var formatCode = map[ast.FormatCode]string{ ast.FormatItalic: "Italic", ast.FormatEmph: "Emph", ast.FormatBold: "Bold", ast.FormatStrong: "Strong", ast.FormatMonospace: "Mono", ast.FormatStrike: "Strikethrough", ast.FormatDelete: "Delete", ast.FormatUnder: "Underline", ast.FormatInsert: "Insert", ast.FormatSuper: "Super", ast.FormatSub: "Sub", ast.FormatQuote: "Quote", ast.FormatQuotation: "Quotation", ast.FormatSmall: "Small", ast.FormatSpan: "Span", } // VisitFormat write JSON code for formatting text. func (v *detailVisitor) VisitFormat(fn *ast.FormatNode) { v.writeNodeStart(formatCode[fn.Code]) v.visitAttributes(fn.Attrs) v.writeContentStart('i') v.acceptInlineSlice(fn.Inlines) v.b.WriteByte('}') } var literalCode = map[ast.LiteralCode]string{ ast.LiteralProg: "Code", ast.LiteralKeyb: "Input", ast.LiteralOutput: "Output", ast.LiteralComment: "Comment", ast.LiteralHTML: "HTML", } // VisitLiteral write JSON code for literal inline text. func (v *detailVisitor) VisitLiteral(ln *ast.LiteralNode) { code, ok := literalCode[ln.Code] if !ok { panic(fmt.Sprintf("Unknown literal code %v", ln.Code)) } v.writeNodeStart(code) v.visitAttributes(ln.Attrs) v.writeContentStart('s') writeEscaped(&v.b, ln.Text) v.b.WriteByte('}') } func (v *detailVisitor) acceptBlockSlice(bns ast.BlockSlice) { v.b.WriteByte('[') for i, bn := range bns { if i > 0 { v.b.WriteByte(',') } bn.Accept(v) } v.b.WriteByte(']') } func (v *detailVisitor) acceptItemSlice(ins ast.ItemSlice) { v.b.WriteByte('[') for i, in := range ins { if i > 0 { v.b.WriteByte(',') } in.Accept(v) } v.b.WriteByte(']') } func (v *detailVisitor) acceptDescriptionSlice(dns ast.DescriptionSlice) { v.b.WriteByte('[') for i, dn := range dns { if i > 0 { v.b.WriteByte(',') } dn.Accept(v) } v.b.WriteByte(']') } func (v *detailVisitor) acceptInlineSlice(ins ast.InlineSlice) { v.b.WriteByte('[') for i, in := range ins { if i > 0 { v.b.WriteByte(',') } in.Accept(v) } v.b.WriteByte(']') } // visitAttributes write JSON attributes func (v *detailVisitor) visitAttributes(a *ast.Attributes) { if a == nil || len(a.Attrs) == 0 { return } keys := make([]string, 0, len(a.Attrs)) for k := range a.Attrs { keys = append(keys, k) } sort.Strings(keys) v.b.WriteString(",\"a\":{\"") for i, k := range keys { if i > 0 { v.b.WriteString("\",\"") } v.b.Write(Escape(k)) v.b.WriteString("\":\"") v.b.Write(Escape(a.Attrs[k])) } v.b.WriteString("\"}") } func (v *detailVisitor) writeNodeStart(t string) { v.b.WriteStrings("{\"t\":\"", t, "\"") } var contentCode = map[rune][]byte{ 'b': []byte(",\"b\":"), // List of blocks 'c': []byte(",\"c\":["), // List of list of blocks 'g': []byte(",\"g\":["), // General list 'i': []byte(",\"i\":"), // List of inlines 'j': []byte(",\"j\":{"), // Embedded JSON object 'l': []byte(",\"l\":["), // List of lines 'n': []byte(",\"n\":"), // Number 'o': []byte(",\"o\":\""), // Byte object 'p': []byte(",\"p\":["), // Generic tuple 'q': []byte(",\"q\":"), // String, if 's' is also needed 's': []byte(",\"s\":"), // String 't': []byte("Content code 't' is not allowed"), 'y': []byte("Content code 'y' is not allowed"), // field after 'j' } func (v *detailVisitor) writeContentStart(code rune) { if b, ok := contentCode[code]; ok { v.b.Write(b) return } panic("Unknown content code " + strconv.Itoa(int(code))) } func (v *detailVisitor) writeMeta(m *meta.Meta) { for _, p := range m.Pairs(true) { if p.Key == meta.KeyTitle { continue } v.b.WriteString(",\"") v.b.Write(Escape(p.Key)) v.b.WriteString("\":") if m.Type(p.Key).IsSet { v.writeSetValue(p.Value) } else { v.b.WriteByte('"') v.b.Write(Escape(p.Value)) v.b.WriteByte('"') } } } func (v *detailVisitor) writeSetValue(value string) { v.b.WriteByte('[') for i, val := range meta.ListFromValue(value) { if i > 0 { v.b.WriteByte(',') } v.b.WriteByte('"') v.b.Write(Escape(val)) v.b.WriteByte('"') } v.b.WriteByte(']') } |
Added encoder/jsonenc/jsonenc.go.
> > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 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 | //----------------------------------------------------------------------------- // Copyright (c) 2020-2021 Detlef Stern // // This file is part of zettelstore. // // Zettelstore is licensed under the latest version of the EUPL (European Union // Public License). Please see file LICENSE.txt for your rights and obligations // under this license. //----------------------------------------------------------------------------- // Package jsonenc encodes the abstract syntax tree into some JSON formats. package jsonenc import ( "bytes" "io" "zettelstore.de/z/ast" "zettelstore.de/z/domain/meta" "zettelstore.de/z/encoder" ) func init() { encoder.Register("json", encoder.Info{ Create: func(*encoder.Environment) encoder.Encoder { return &jsonEncoder{} }, Default: true, }) } // jsonEncoder is just a stub. It is not implemented. The real implementation // is in file web/adapter/json.go type jsonEncoder struct{} // WriteZettel writes the encoded zettel to the writer. func (je *jsonEncoder) WriteZettel( w io.Writer, zn *ast.ZettelNode, inhMeta bool) (int, error) { return 0, encoder.ErrNoWriteZettel } // WriteMeta encodes meta data as HTML5. func (je *jsonEncoder) WriteMeta(w io.Writer, meta *meta.Meta) (int, error) { return 0, encoder.ErrNoWriteMeta } func (je *jsonEncoder) WriteContent(w io.Writer, zn *ast.ZettelNode) (int, error) { return 0, encoder.ErrNoWriteContent } // WriteBlocks writes a block slice to the writer func (je *jsonEncoder) WriteBlocks(w io.Writer, bs ast.BlockSlice) (int, error) { return 0, encoder.ErrNoWriteBlocks } // WriteInlines writes an inline slice to the writer func (je *jsonEncoder) WriteInlines(w io.Writer, is ast.InlineSlice) (int, error) { return 0, encoder.ErrNoWriteInlines } var ( jsBackslash = []byte{'\\', '\\'} jsDoubleQuote = []byte{'\\', '"'} jsNewline = []byte{'\\', 'n'} jsTab = []byte{'\\', 't'} jsCr = []byte{'\\', 'r'} jsUnicode = []byte{'\\', 'u', '0', '0', '0', '0'} jsHex = []byte("0123456789ABCDEF") ) // Escape returns the given string as a byte slice, where every non-printable // rune is made printable. func Escape(s string) []byte { var buf bytes.Buffer last := 0 for i, ch := range s { var b []byte switch ch { case '\t': b = jsTab case '\r': b = jsCr case '\n': b = jsNewline case '"': b = jsDoubleQuote case '\\': b = jsBackslash default: if ch < ' ' { b = jsUnicode b[2] = '0' b[3] = '0' b[4] = jsHex[ch>>4] b[5] = jsHex[ch&0xF] } else { continue } } buf.WriteString(s[last:i]) buf.Write(b) last = i + 1 } buf.WriteString(s[last:]) return buf.Bytes() } func writeEscaped(b *encoder.BufWriter, s string) { b.WriteByte('"') b.Write(Escape(s)) b.WriteByte('"') } |
Deleted encoder/mdenc/mdenc.go.
|
| < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < |
Added encoder/nativeenc/nativeenc.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 | //----------------------------------------------------------------------------- // Copyright (c) 2020-2021 Detlef Stern // // This file is part of zettelstore. // // Zettelstore is licensed under the latest version of the EUPL (European Union // Public License). Please see file LICENSE.txt for your rights and obligations // under this license. //----------------------------------------------------------------------------- // Package nativeenc encodes the abstract syntax tree into native format. package nativeenc import ( "fmt" "io" "sort" "strconv" "zettelstore.de/z/ast" "zettelstore.de/z/domain/meta" "zettelstore.de/z/encoder" "zettelstore.de/z/encoder/encfun" "zettelstore.de/z/parser" ) func init() { encoder.Register("native", encoder.Info{ Create: func(env *encoder.Environment) encoder.Encoder { return &nativeEncoder{env: env} }, }) } type nativeEncoder struct { env *encoder.Environment } // WriteZettel encodes the zettel to the writer. func (ne *nativeEncoder) WriteZettel( w io.Writer, zn *ast.ZettelNode, inhMeta bool) (int, error) { v := newVisitor(w, ne) v.b.WriteString("[Title ") v.acceptInlineSlice(encfun.MetaAsInlineSlice(zn.InhMeta, meta.KeyTitle)) v.b.WriteByte(']') if inhMeta { v.acceptMeta(zn.InhMeta, false) } else { v.acceptMeta(zn.Meta, false) } v.b.WriteByte('\n') v.acceptBlockSlice(zn.Ast) length, err := v.b.Flush() return length, err } // WriteMeta encodes meta data in native format. func (ne *nativeEncoder) WriteMeta(w io.Writer, m *meta.Meta) (int, error) { v := newVisitor(w, ne) v.acceptMeta(m, true) length, err := v.b.Flush() return length, err } func (ne *nativeEncoder) WriteContent(w io.Writer, zn *ast.ZettelNode) (int, error) { return ne.WriteBlocks(w, zn.Ast) } // WriteBlocks writes a block slice to the writer func (ne *nativeEncoder) WriteBlocks(w io.Writer, bs ast.BlockSlice) (int, error) { v := newVisitor(w, ne) v.acceptBlockSlice(bs) length, err := v.b.Flush() return length, err } // WriteInlines writes an inline slice to the writer func (ne *nativeEncoder) WriteInlines(w io.Writer, is ast.InlineSlice) (int, error) { v := newVisitor(w, ne) v.acceptInlineSlice(is) length, err := v.b.Flush() return length, err } // visitor writes the abstract syntax tree to an io.Writer. type visitor struct { b encoder.BufWriter level int env *encoder.Environment } func newVisitor(w io.Writer, enc *nativeEncoder) *visitor { return &visitor{b: encoder.NewBufWriter(w), env: enc.env} } var ( rawBackslash = []byte{'\\', '\\'} rawDoubleQuote = []byte{'\\', '"'} rawNewline = []byte{'\\', 'n'} ) func (v *visitor) acceptMeta(m *meta.Meta, withTitle bool) { if withTitle { v.b.WriteString("[Title ") v.acceptInlineSlice(parser.ParseMetadata(m.GetDefault(meta.KeyTitle, ""))) v.b.WriteByte(']') } v.writeMetaString(m, meta.KeyRole, "Role") v.writeMetaList(m, meta.KeyTags, "Tags") v.writeMetaString(m, meta.KeySyntax, "Syntax") pairs := m.PairsRest(true) if len(pairs) == 0 { return } v.b.WriteString("\n[Header") v.level++ for i, p := range pairs { if i > 0 { v.b.WriteByte(',') } v.writeNewLine() v.b.WriteByte('[') v.b.WriteStrings(p.Key, " \"") v.writeEscaped(p.Value) v.b.WriteString("\"]") } v.level-- v.b.WriteByte(']') } func (v *visitor) writeMetaString(m *meta.Meta, key, native string) { if val, ok := m.Get(key); ok && len(val) > 0 { v.b.WriteStrings("\n[", native, " \"", val, "\"]") } } func (v *visitor) writeMetaList(m *meta.Meta, key, native string) { if vals, ok := m.GetList(key); ok && len(vals) > 0 { v.b.WriteStrings("\n[", native) for _, val := range vals { v.b.WriteByte(' ') v.b.WriteString(val) } v.b.WriteByte(']') } } // VisitPara emits native code for a paragraph. func (v *visitor) VisitPara(pn *ast.ParaNode) { v.b.WriteString("[Para ") v.acceptInlineSlice(pn.Inlines) v.b.WriteByte(']') } var verbatimCode = map[ast.VerbatimCode][]byte{ ast.VerbatimProg: []byte("[CodeBlock"), ast.VerbatimComment: []byte("[CommentBlock"), ast.VerbatimHTML: []byte("[HTMLBlock"), } // VisitVerbatim emits native code for verbatim lines. func (v *visitor) VisitVerbatim(vn *ast.VerbatimNode) { code, ok := verbatimCode[vn.Code] if !ok { panic(fmt.Sprintf("Unknown verbatim code %v", vn.Code)) } v.b.Write(code) v.visitAttributes(vn.Attrs) v.b.WriteString(" \"") for i, line := range vn.Lines { if i > 0 { v.b.Write(rawNewline) } v.writeEscaped(line) } v.b.WriteString("\"]") } var regionCode = map[ast.RegionCode][]byte{ ast.RegionSpan: []byte("[SpanBlock"), ast.RegionQuote: []byte("[QuoteBlock"), ast.RegionVerse: []byte("[VerseBlock"), } // VisitRegion writes native code for block regions. func (v *visitor) VisitRegion(rn *ast.RegionNode) { code, ok := regionCode[rn.Code] if !ok { panic(fmt.Sprintf("Unknown region code %v", rn.Code)) } v.b.Write(code) v.visitAttributes(rn.Attrs) v.level++ v.writeNewLine() v.b.WriteByte('[') v.level++ v.acceptBlockSlice(rn.Blocks) v.level-- v.b.WriteByte(']') if len(rn.Inlines) > 0 { v.b.WriteByte(',') v.writeNewLine() v.b.WriteString("[Cite ") v.acceptInlineSlice(rn.Inlines) v.b.WriteByte(']') } v.level-- v.b.WriteByte(']') } // VisitHeading writes the native code for a heading. func (v *visitor) VisitHeading(hn *ast.HeadingNode) { v.b.WriteStrings("[Heading ", strconv.Itoa(hn.Level), " \"", hn.Slug, "\"") v.visitAttributes(hn.Attrs) v.b.WriteByte(' ') v.acceptInlineSlice(hn.Inlines) v.b.WriteByte(']') } // VisitHRule writes native code for a horizontal rule: <hr>. func (v *visitor) VisitHRule(hn *ast.HRuleNode) { v.b.WriteString("[Hrule") v.visitAttributes(hn.Attrs) v.b.WriteByte(']') } var listCode = map[ast.NestedListCode][]byte{ ast.NestedListOrdered: []byte("[OrderedList"), ast.NestedListUnordered: []byte("[BulletList"), ast.NestedListQuote: []byte("[QuoteList"), } // VisitNestedList writes native code for lists and blockquotes. func (v *visitor) VisitNestedList(ln *ast.NestedListNode) { v.b.Write(listCode[ln.Code]) v.level++ for i, item := range ln.Items { if i > 0 { v.b.WriteByte(',') } v.writeNewLine() v.level++ v.b.WriteByte('[') v.acceptItemSlice(item) v.b.WriteByte(']') v.level-- } v.level-- v.b.WriteByte(']') } // VisitDescriptionList emits a native description list. func (v *visitor) VisitDescriptionList(dn *ast.DescriptionListNode) { v.b.WriteString("[DescriptionList") v.level++ for i, descr := range dn.Descriptions { if i > 0 { v.b.WriteByte(',') } v.writeNewLine() v.b.WriteString("[Term [") v.acceptInlineSlice(descr.Term) v.b.WriteByte(']') if len(descr.Descriptions) > 0 { v.level++ for _, b := range descr.Descriptions { v.b.WriteByte(',') v.writeNewLine() v.b.WriteString("[Description") v.level++ v.writeNewLine() v.acceptDescriptionSlice(b) v.b.WriteByte(']') v.level-- } v.level-- } v.b.WriteByte(']') } v.level-- v.b.WriteByte(']') } // VisitTable emits a native table. func (v *visitor) VisitTable(tn *ast.TableNode) { v.b.WriteString("[Table") v.level++ if len(tn.Header) > 0 { v.writeNewLine() v.b.WriteString("[Header ") for i, cell := range tn.Header { if i > 0 { v.b.WriteByte(',') } v.writeCell(cell) } v.b.WriteString("],") } for i, row := range tn.Rows { if i > 0 { v.b.WriteByte(',') } v.writeNewLine() v.b.WriteString("[Row ") for j, cell := range row { if j > 0 { v.b.WriteByte(',') } v.writeCell(cell) } v.b.WriteByte(']') } v.level-- v.b.WriteByte(']') } var alignString = map[ast.Alignment]string{ ast.AlignDefault: " Default", ast.AlignLeft: " Left", ast.AlignCenter: " Center", ast.AlignRight: " Right", } func (v *visitor) writeCell(cell *ast.TableCell) { v.b.WriteStrings("[Cell", alignString[cell.Align]) if len(cell.Inlines) > 0 { v.b.WriteByte(' ') v.acceptInlineSlice(cell.Inlines) } v.b.WriteByte(']') } // VisitBLOB writes the binary object as a value. func (v *visitor) VisitBLOB(bn *ast.BLOBNode) { v.b.WriteString("[BLOB \"") v.writeEscaped(bn.Title) v.b.WriteString("\" \"") v.writeEscaped(bn.Syntax) v.b.WriteString("\" \"") v.b.WriteBase64(bn.Blob) v.b.WriteString("\"]") } // VisitText writes text content. func (v *visitor) VisitText(tn *ast.TextNode) { v.b.WriteString("Text \"") v.writeEscaped(tn.Text) v.b.WriteByte('"') } // VisitTag writes tag content. func (v *visitor) VisitTag(tn *ast.TagNode) { v.b.WriteString("Tag \"") v.writeEscaped(tn.Tag) v.b.WriteByte('"') } // VisitSpace emits a white space. func (v *visitor) VisitSpace(sn *ast.SpaceNode) { v.b.WriteString("Space") if l := len(sn.Lexeme); l > 1 { v.b.WriteByte(' ') v.b.WriteString(strconv.Itoa(l)) } } // VisitBreak writes native code for line breaks. func (v *visitor) VisitBreak(bn *ast.BreakNode) { if bn.Hard { v.b.WriteString("Break") } else { v.b.WriteString("Space") } } var mapRefState = map[ast.RefState]string{ ast.RefStateInvalid: "INVALID", ast.RefStateZettel: "ZETTEL", ast.RefStateSelf: "SELF", ast.RefStateFound: "ZETTEL", ast.RefStateBroken: "BROKEN", ast.RefStateHosted: "LOCAL", ast.RefStateBased: "BASED", ast.RefStateExternal: "EXTERNAL", } // VisitLink writes native code for links. func (v *visitor) VisitLink(ln *ast.LinkNode) { ln, n := v.env.AdaptLink(ln) if n != nil { n.Accept(v) return } v.b.WriteString("Link") v.visitAttributes(ln.Attrs) v.b.WriteByte(' ') v.b.WriteString(mapRefState[ln.Ref.State]) v.b.WriteString(" \"") v.writeEscaped(ln.Ref.String()) v.b.WriteString("\" [") if !ln.OnlyRef { v.acceptInlineSlice(ln.Inlines) } v.b.WriteByte(']') } // VisitImage writes native code for images. func (v *visitor) VisitImage(in *ast.ImageNode) { in, n := v.env.AdaptImage(in) if n != nil { n.Accept(v) return } v.b.WriteString("Image") v.visitAttributes(in.Attrs) if in.Ref == nil { v.b.WriteStrings(" {\"", in.Syntax, "\" \"") switch in.Syntax { case "svg": v.writeEscaped(string(in.Blob)) default: v.b.WriteString("\" \"") v.b.WriteBase64(in.Blob) } v.b.WriteString("\"}") } else { v.b.WriteStrings(" \"", in.Ref.String(), "\"") } if len(in.Inlines) > 0 { v.b.WriteString(" [") v.acceptInlineSlice(in.Inlines) v.b.WriteByte(']') } } // VisitCite writes code for citations. func (v *visitor) VisitCite(cn *ast.CiteNode) { v.b.WriteString("Cite") v.visitAttributes(cn.Attrs) v.b.WriteString(" \"") v.writeEscaped(cn.Key) v.b.WriteByte('"') if len(cn.Inlines) > 0 { v.b.WriteString(" [") v.acceptInlineSlice(cn.Inlines) v.b.WriteByte(']') } } // VisitFootnote write native code for a footnote. func (v *visitor) VisitFootnote(fn *ast.FootnoteNode) { v.b.WriteString("Footnote") v.visitAttributes(fn.Attrs) v.b.WriteString(" [") v.acceptInlineSlice(fn.Inlines) v.b.WriteByte(']') } // VisitMark writes native code to mark a position. func (v *visitor) VisitMark(mn *ast.MarkNode) { v.b.WriteString("Mark") if len(mn.Text) > 0 { v.b.WriteString(" \"") v.writeEscaped(mn.Text) v.b.WriteByte('"') } } var formatCode = map[ast.FormatCode][]byte{ ast.FormatItalic: []byte("Italic"), ast.FormatEmph: []byte("Emph"), ast.FormatBold: []byte("Bold"), ast.FormatStrong: []byte("Strong"), ast.FormatUnder: []byte("Underline"), ast.FormatInsert: []byte("Insert"), ast.FormatMonospace: []byte("Mono"), ast.FormatStrike: []byte("Strikethrough"), ast.FormatDelete: []byte("Delete"), ast.FormatSuper: []byte("Super"), ast.FormatSub: []byte("Sub"), ast.FormatQuote: []byte("Quote"), ast.FormatQuotation: []byte("Quotation"), ast.FormatSmall: []byte("Small"), ast.FormatSpan: []byte("Span"), } // VisitFormat write native code for formatting text. func (v *visitor) VisitFormat(fn *ast.FormatNode) { v.b.Write(formatCode[fn.Code]) v.visitAttributes(fn.Attrs) v.b.WriteString(" [") v.acceptInlineSlice(fn.Inlines) v.b.WriteByte(']') } var literalCode = map[ast.LiteralCode][]byte{ ast.LiteralProg: []byte("Code"), ast.LiteralKeyb: []byte("Input"), ast.LiteralOutput: []byte("Output"), ast.LiteralComment: []byte("Comment"), ast.LiteralHTML: []byte("HTML"), } // VisitLiteral write native code for code inline text. func (v *visitor) VisitLiteral(ln *ast.LiteralNode) { code, ok := literalCode[ln.Code] if !ok { panic(fmt.Sprintf("Unknown literal code %v", ln.Code)) } v.b.Write(code) v.visitAttributes(ln.Attrs) v.b.WriteString(" \"") v.writeEscaped(ln.Text) v.b.WriteByte('"') } func (v *visitor) acceptBlockSlice(bns ast.BlockSlice) { for i, bn := range bns { if i > 0 { v.b.WriteByte(',') v.writeNewLine() } bn.Accept(v) } } func (v *visitor) acceptItemSlice(ins ast.ItemSlice) { for i, in := range ins { if i > 0 { v.b.WriteByte(',') v.writeNewLine() } in.Accept(v) } } func (v *visitor) acceptDescriptionSlice(dns ast.DescriptionSlice) { for i, dn := range dns { if i > 0 { v.b.WriteByte(',') v.writeNewLine() } dn.Accept(v) } } func (v *visitor) acceptInlineSlice(ins ast.InlineSlice) { for i, in := range ins { if i > 0 { v.b.WriteByte(',') } in.Accept(v) } } // visitAttributes write native attributes func (v *visitor) visitAttributes(a *ast.Attributes) { if a == nil || len(a.Attrs) == 0 { return } keys := make([]string, 0, len(a.Attrs)) for k := range a.Attrs { keys = append(keys, k) } sort.Strings(keys) v.b.WriteString(" (\"") if val, ok := a.Attrs[""]; ok { v.writeEscaped(val) } v.b.WriteString("\",[") first := true for _, k := range keys { if k == "" { continue } if !first { v.b.WriteByte(',') } v.b.WriteString(k) val := a.Attrs[k] if len(val) > 0 { v.b.WriteString("=\"") v.writeEscaped(val) v.b.WriteByte('"') } first = false } v.b.WriteString("])") } func (v *visitor) writeNewLine() { v.b.WriteByte('\n') for i := 0; i < v.level; i++ { v.b.WriteByte(' ') } } func (v *visitor) writeEscaped(s string) { last := 0 for i, ch := range s { var b []byte switch ch { case '\n': b = rawNewline case '"': b = rawDoubleQuote case '\\': b = rawBackslash default: continue } v.b.WriteString(s[last:i]) v.b.Write(b) last = i + 1 } v.b.WriteString(s[last:]) } |
Added encoder/rawenc/rawenc.go.
> > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 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 | //----------------------------------------------------------------------------- // Copyright (c) 2020-2021 Detlef Stern // // This file is part of zettelstore. // // Zettelstore is licensed under the latest version of the EUPL (European Union // Public License). Please see file LICENSE.txt for your rights and obligations // under this license. //----------------------------------------------------------------------------- // Package rawenc encodes the abstract syntax tree as raw content. package rawenc import ( "io" "zettelstore.de/z/ast" "zettelstore.de/z/domain/meta" "zettelstore.de/z/encoder" ) func init() { encoder.Register("raw", encoder.Info{ Create: func(*encoder.Environment) encoder.Encoder { return &rawEncoder{} }, }) } type rawEncoder struct{} // WriteZettel writes the encoded zettel to the writer. func (re *rawEncoder) WriteZettel( w io.Writer, zn *ast.ZettelNode, inhMeta bool) (int, error) { b := encoder.NewBufWriter(w) if inhMeta { zn.InhMeta.Write(&b, true) } else { zn.Meta.Write(&b, true) } b.WriteByte('\n') b.WriteString(zn.Content.AsString()) length, err := b.Flush() return length, err } // WriteMeta encodes meta data as HTML5. func (re *rawEncoder) WriteMeta(w io.Writer, m *meta.Meta) (int, error) { b := encoder.NewBufWriter(w) m.Write(&b, true) length, err := b.Flush() return length, err } func (re *rawEncoder) WriteContent(w io.Writer, zn *ast.ZettelNode) (int, error) { b := encoder.NewBufWriter(w) b.WriteString(zn.Content.AsString()) length, err := b.Flush() return length, err } // WriteBlocks writes a block slice to the writer func (re *rawEncoder) WriteBlocks(w io.Writer, bs ast.BlockSlice) (int, error) { return 0, encoder.ErrNoWriteBlocks } // WriteInlines writes an inline slice to the writer func (re *rawEncoder) WriteInlines(w io.Writer, is ast.InlineSlice) (int, error) { return 0, encoder.ErrNoWriteInlines } |
Deleted encoder/shtmlenc/shtmlenc.go.
|
| < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < |
Deleted encoder/szenc/szenc.go.
|
| < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < |
Deleted encoder/szenc/transform.go.
|
| < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < |
Changes to encoder/textenc/textenc.go.
1 | //----------------------------------------------------------------------------- | | | < < < | | | > | | | < < | < < | > | > > > | | | | > > > > > > | > > > > > < | | | | < < < < < < < < < < | | | | | | | < | > | < < < < < | < < < < < < < < < | < < < < < < < < < < < < < < < < < < < < < < < | < < < < < < < < < < < < < < < < < < < < < < < | | | > > | | > | > > > > > > > > > > > > > > > > > > > > | < | | < > > | | > | > | > < < < < > > | | > > > > > > | > > > | | > | | > > > | | | > > > | > > > | | > > > | > > | | > > > > > | > > > > > > | > > > | | > > > | | > > > > > | > > | > > > > | > > > > > | > > | > > > > | > > > | > | | > > > > > > > > > > > > > > > | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 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 | //----------------------------------------------------------------------------- // Copyright (c) 2020-2021 Detlef Stern // // This file is part of zettelstore. // // Zettelstore is licensed under the latest version of the EUPL (European Union // Public License). Please see file LICENSE.txt for your rights and obligations // under this license. //----------------------------------------------------------------------------- // Package textenc encodes the abstract syntax tree into its text. package textenc import ( "io" "zettelstore.de/z/ast" "zettelstore.de/z/domain/meta" "zettelstore.de/z/encoder" "zettelstore.de/z/parser" ) func init() { encoder.Register("text", encoder.Info{ Create: func(*encoder.Environment) encoder.Encoder { return &textEncoder{} }, }) } type textEncoder struct{} // WriteZettel writes metadata and content. func (te *textEncoder) WriteZettel(w io.Writer, zn *ast.ZettelNode, inhMeta bool) (int, error) { v := newVisitor(w) if inhMeta { te.WriteMeta(&v.b, zn.InhMeta) } else { te.WriteMeta(&v.b, zn.Meta) } v.acceptBlockSlice(zn.Ast) length, err := v.b.Flush() return length, err } // WriteMeta encodes metadata as text. func (te *textEncoder) WriteMeta(w io.Writer, m *meta.Meta) (int, error) { b := encoder.NewBufWriter(w) for _, pair := range m.Pairs(true) { switch meta.Type(pair.Key) { case meta.TypeBool: if meta.BoolValue(pair.Value) { b.WriteString("true") } else { b.WriteString("false") } case meta.TypeTagSet: for i, tag := range meta.ListFromValue(pair.Value) { if i > 0 { b.WriteByte(' ') } b.WriteString(meta.CleanTag(tag)) } case meta.TypeZettelmarkup: te.WriteInlines(w, parser.ParseMetadata(pair.Value)) default: b.WriteString(pair.Value) } b.WriteByte('\n') } length, err := b.Flush() return length, err } func (te *textEncoder) WriteContent(w io.Writer, zn *ast.ZettelNode) (int, error) { return te.WriteBlocks(w, zn.Ast) } // WriteBlocks writes the content of a block slice to the writer. func (te *textEncoder) WriteBlocks(w io.Writer, bs ast.BlockSlice) (int, error) { v := newVisitor(w) v.acceptBlockSlice(bs) length, err := v.b.Flush() return length, err } // WriteInlines writes an inline slice to the writer func (te *textEncoder) WriteInlines(w io.Writer, is ast.InlineSlice) (int, error) { v := newVisitor(w) v.acceptInlineSlice(is) length, err := v.b.Flush() return length, err } // visitor writes the abstract syntax tree to an io.Writer. type visitor struct { b encoder.BufWriter } func newVisitor(w io.Writer) *visitor { return &visitor{b: encoder.NewBufWriter(w)} } // VisitPara emits text code for a paragraph func (v *visitor) VisitPara(pn *ast.ParaNode) { v.acceptInlineSlice(pn.Inlines) } // VisitVerbatim emits text for verbatim lines. func (v *visitor) VisitVerbatim(vn *ast.VerbatimNode) { if vn.Code == ast.VerbatimComment { return } for i, line := range vn.Lines { if i > 0 { v.b.WriteByte('\n') } v.b.WriteString(line) } } // VisitRegion writes text code for block regions. func (v *visitor) VisitRegion(rn *ast.RegionNode) { v.acceptBlockSlice(rn.Blocks) if len(rn.Inlines) > 0 { v.b.WriteByte('\n') v.acceptInlineSlice(rn.Inlines) } } // VisitHeading writes the text code for a heading. func (v *visitor) VisitHeading(hn *ast.HeadingNode) { v.acceptInlineSlice(hn.Inlines) } // VisitHRule writes nothing for a horizontal rule. func (v *visitor) VisitHRule(hn *ast.HRuleNode) {} // VisitNestedList writes text code for lists and blockquotes. func (v *visitor) VisitNestedList(ln *ast.NestedListNode) { for i, item := range ln.Items { if i > 0 { v.b.WriteByte('\n') } v.acceptItemSlice(item) } } // VisitDescriptionList emits a text for a description list. func (v *visitor) VisitDescriptionList(dn *ast.DescriptionListNode) { for i, descr := range dn.Descriptions { if i > 0 { v.b.WriteByte('\n') } v.acceptInlineSlice(descr.Term) for _, b := range descr.Descriptions { v.b.WriteByte('\n') v.acceptDescriptionSlice(b) } } } // VisitTable emits a text table. func (v *visitor) VisitTable(tn *ast.TableNode) { if len(tn.Header) > 0 { for i, cell := range tn.Header { if i > 0 { v.b.WriteByte(' ') } v.acceptInlineSlice(cell.Inlines) } v.b.WriteByte('\n') } for i, row := range tn.Rows { if i > 0 { v.b.WriteByte('\n') } for j, cell := range row { if j > 0 { v.b.WriteByte(' ') } v.acceptInlineSlice(cell.Inlines) } } } // VisitBLOB writes nothing, because it contains no text. func (v *visitor) VisitBLOB(bn *ast.BLOBNode) {} // VisitText writes text content. func (v *visitor) VisitText(tn *ast.TextNode) { v.b.WriteString(tn.Text) } // VisitTag writes tag content. func (v *visitor) VisitTag(tn *ast.TagNode) { v.b.WriteStrings("#", tn.Tag) } // VisitSpace emits a white space. func (v *visitor) VisitSpace(sn *ast.SpaceNode) { v.b.WriteByte(' ') } // VisitBreak writes text code for line breaks. func (v *visitor) VisitBreak(bn *ast.BreakNode) { if bn.Hard { v.b.WriteByte('\n') } else { v.b.WriteByte(' ') } } // VisitLink writes text code for links. func (v *visitor) VisitLink(ln *ast.LinkNode) { if !ln.OnlyRef { v.acceptInlineSlice(ln.Inlines) } } // VisitImage writes text code for images. func (v *visitor) VisitImage(in *ast.ImageNode) { v.acceptInlineSlice(in.Inlines) } // VisitCite writes code for citations. func (v *visitor) VisitCite(cn *ast.CiteNode) { v.acceptInlineSlice(cn.Inlines) } // VisitFootnote write text code for a footnote. func (v *visitor) VisitFootnote(fn *ast.FootnoteNode) { v.b.WriteByte(' ') v.acceptInlineSlice(fn.Inlines) } // VisitMark writes nothing for a mark. func (v *visitor) VisitMark(mn *ast.MarkNode) {} // VisitFormat write text code for formatting text. func (v *visitor) VisitFormat(fn *ast.FormatNode) { v.acceptInlineSlice(fn.Inlines) } // VisitLiteral write text code for literal inline text. func (v *visitor) VisitLiteral(ln *ast.LiteralNode) { if ln.Code != ast.LiteralComment { v.b.WriteString(ln.Text) } } // VisitAttributes never writes any attribute data. func (v *visitor) VisitAttributes(a *ast.Attributes) {} func (v *visitor) acceptBlockSlice(bns ast.BlockSlice) { for i, bn := range bns { if i > 0 { v.b.WriteByte('\n') } bn.Accept(v) } } func (v *visitor) acceptItemSlice(ins ast.ItemSlice) { for i, in := range ins { if i > 0 { v.b.WriteByte('\n') } in.Accept(v) } } func (v *visitor) acceptDescriptionSlice(dns ast.DescriptionSlice) { for i, dn := range dns { if i > 0 { v.b.WriteByte('\n') } dn.Accept(v) } } func (v *visitor) acceptInlineSlice(ins ast.InlineSlice) { for _, in := range ins { in.Accept(v) } } |
Deleted encoder/write.go.
|
| < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < |
Changes to encoder/zmkenc/zmkenc.go.
1 | //----------------------------------------------------------------------------- | | | < < < | < < | | < < > | | | < < | < < > | | | | < | | | < < < | < < < < < < < < < < < < < < | | | | | | | | | < | | < | | > > | | < < < < < < < < | < < | < | < < < > | < < < < < < < < < < < | < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < | < < < | | | < < < < | < < < < < < < < < | < < < < < < < < < | | | | | > | | > | | | | < < | < < | | > > | > > > > > > > > | > > | < > | > | | | < < < | | > | | | | | > | < < < < < | | | < | | | < < < | > | | < < < | > > | | | | < | | | | > | > | | > | < > | | < | | | | < > | | > | | | | < < < | < < < < | < > | | < | < | < < < | > > > > > > > > > > > > > > > > | < < | | | | | > > > > > > > > > > > | > > | | | > > > | | | < < < > | > | | | | | | | | < < < < < < | < < | | | | > | | | < < < < < < < < < < < < < < < < < < < < < < < < < < > | > > > > > > > > > > > > > > > > > > > > > > > > > > > > | > > > > | > > > > > | > > > | | < < < | | < < < | < < < > | > | > > > > > > > > > > > | > | < < < < > > | | > | | > > > > > > | | | < < < < | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 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 | //----------------------------------------------------------------------------- // Copyright (c) 2020-2021 Detlef Stern // // This file is part of zettelstore. // // Zettelstore is licensed under the latest version of the EUPL (European Union // Public License). Please see file LICENSE.txt for your rights and obligations // under this license. //----------------------------------------------------------------------------- // Package zmkenc encodes the abstract syntax tree back into Zettelmarkup. package zmkenc import ( "fmt" "io" "sort" "zettelstore.de/z/ast" "zettelstore.de/z/domain/meta" "zettelstore.de/z/encoder" ) func init() { encoder.Register("zmk", encoder.Info{ Create: func(*encoder.Environment) encoder.Encoder { return &zmkEncoder{} }, }) } type zmkEncoder struct{} // WriteZettel writes the encoded zettel to the writer. func (ze *zmkEncoder) WriteZettel( w io.Writer, zn *ast.ZettelNode, inhMeta bool) (int, error) { v := newVisitor(w, ze) if inhMeta { zn.InhMeta.WriteAsHeader(&v.b, true) } else { zn.Meta.WriteAsHeader(&v.b, true) } v.acceptBlockSlice(zn.Ast) length, err := v.b.Flush() return length, err } // WriteMeta encodes meta data as zmk. func (ze *zmkEncoder) WriteMeta(w io.Writer, m *meta.Meta) (int, error) { return m.Write(w, true) } func (ze *zmkEncoder) WriteContent(w io.Writer, zn *ast.ZettelNode) (int, error) { return ze.WriteBlocks(w, zn.Ast) } // WriteBlocks writes the content of a block slice to the writer. func (ze *zmkEncoder) WriteBlocks(w io.Writer, bs ast.BlockSlice) (int, error) { v := newVisitor(w, ze) v.acceptBlockSlice(bs) length, err := v.b.Flush() return length, err } // WriteInlines writes an inline slice to the writer func (ze *zmkEncoder) WriteInlines(w io.Writer, is ast.InlineSlice) (int, error) { v := newVisitor(w, ze) v.acceptInlineSlice(is) length, err := v.b.Flush() return length, err } // visitor writes the abstract syntax tree to an io.Writer. type visitor struct { b encoder.BufWriter prefix []byte enc *zmkEncoder } func newVisitor(w io.Writer, enc *zmkEncoder) *visitor { return &visitor{ b: encoder.NewBufWriter(w), enc: enc, } } // VisitPara emits HTML code for a paragraph: <p>...</p> func (v *visitor) VisitPara(pn *ast.ParaNode) { v.acceptInlineSlice(pn.Inlines) v.b.WriteByte('\n') if len(v.prefix) == 0 { v.b.WriteByte('\n') } } // VisitVerbatim emits HTML code for verbatim lines. func (v *visitor) VisitVerbatim(vn *ast.VerbatimNode) { // TODO: scan cn.Lines to find embedded "`"s at beginning v.b.WriteString("```") v.visitAttributes(vn.Attrs) v.b.WriteByte('\n') for _, line := range vn.Lines { v.b.WriteStrings(line, "\n") } v.b.WriteString("```\n") } var regionCode = map[ast.RegionCode]string{ ast.RegionSpan: ":::", ast.RegionQuote: "<<<", ast.RegionVerse: "\"\"\"", } // VisitRegion writes HTML code for block regions. func (v *visitor) VisitRegion(rn *ast.RegionNode) { // Scan rn.Blocks for embedded regions to adjust length of regionCode code, ok := regionCode[rn.Code] if !ok { panic(fmt.Sprintf("Unknown region code %d", rn.Code)) } v.b.WriteString(code) v.visitAttributes(rn.Attrs) v.b.WriteByte('\n') v.acceptBlockSlice(rn.Blocks) v.b.WriteString(code) if len(rn.Inlines) > 0 { v.b.WriteByte(' ') v.acceptInlineSlice(rn.Inlines) } v.b.WriteByte('\n') } // VisitHeading writes the HTML code for a heading. func (v *visitor) VisitHeading(hn *ast.HeadingNode) { for i := 0; i <= hn.Level; i++ { v.b.WriteByte('=') } v.b.WriteByte(' ') v.acceptInlineSlice(hn.Inlines) v.visitAttributes(hn.Attrs) v.b.WriteByte('\n') } // VisitHRule writes HTML code for a horizontal rule: <hr>. func (v *visitor) VisitHRule(hn *ast.HRuleNode) { v.b.WriteString("---") v.visitAttributes(hn.Attrs) v.b.WriteByte('\n') } var listCode = map[ast.NestedListCode]byte{ ast.NestedListOrdered: '#', ast.NestedListUnordered: '*', ast.NestedListQuote: '>', } // VisitNestedList writes HTML code for lists and blockquotes. func (v *visitor) VisitNestedList(ln *ast.NestedListNode) { v.prefix = append(v.prefix, listCode[ln.Code]) for _, item := range ln.Items { v.b.Write(v.prefix) v.b.WriteByte(' ') for i, in := range item { if i > 0 { if _, ok := in.(*ast.ParaNode); ok { v.b.WriteByte('\n') for j := 0; j <= len(v.prefix); j++ { v.b.WriteByte(' ') } } } in.Accept(v) } } v.prefix = v.prefix[:len(v.prefix)-1] v.b.WriteByte('\n') } // VisitDescriptionList emits a HTML description list. func (v *visitor) VisitDescriptionList(dn *ast.DescriptionListNode) { for _, descr := range dn.Descriptions { v.b.WriteString("; ") v.acceptInlineSlice(descr.Term) v.b.WriteByte('\n') for _, b := range descr.Descriptions { v.b.WriteString(": ") for _, dn := range b { dn.Accept(v) } v.b.WriteByte('\n') } } } var alignCode = map[ast.Alignment]string{ ast.AlignDefault: "", ast.AlignLeft: "<", ast.AlignCenter: ":", ast.AlignRight: ">", } // VisitTable emits a HTML table. func (v *visitor) VisitTable(tn *ast.TableNode) { if len(tn.Header) > 0 { for pos, cell := range tn.Header { v.b.WriteString("|=") colAlign := tn.Align[pos] if cell.Align != colAlign { v.b.WriteString(alignCode[cell.Align]) } v.acceptInlineSlice(cell.Inlines) if colAlign != ast.AlignDefault { v.b.WriteString(alignCode[colAlign]) } } v.b.WriteByte('\n') } for _, row := range tn.Rows { for pos, cell := range row { v.b.WriteByte('|') if cell.Align != tn.Align[pos] { v.b.WriteString(alignCode[cell.Align]) } v.acceptInlineSlice(cell.Inlines) } v.b.WriteByte('\n') } v.b.WriteByte('\n') } // VisitBLOB writes the binary object as a value. func (v *visitor) VisitBLOB(bn *ast.BLOBNode) { v.b.WriteStrings( "%% Unable to display BLOB with title '", bn.Title, "' and syntax '", bn.Syntax, "'\n") } var escapeSeqs = map[string]bool{ "\\": true, "//": true, "**": true, "__": true, "~~": true, "^^": true, ",,": true, "<<": true, "\"\"": true, ";;": true, "::": true, "''": true, "``": true, "++": true, "==": true, } // VisitText writes text content. func (v *visitor) VisitText(tn *ast.TextNode) { last := 0 for i := 0; i < len(tn.Text); i++ { if b := tn.Text[i]; b == '\\' { v.b.WriteString(tn.Text[last:i]) v.b.WriteBytes('\\', b) last = i + 1 continue } if i < len(tn.Text)-1 { s := tn.Text[i : i+2] if _, ok := escapeSeqs[s]; ok { 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:]) } // VisitTag writes tag content. func (v *visitor) VisitTag(tn *ast.TagNode) { v.b.WriteStrings("#", tn.Tag) } // VisitSpace emits a white space. func (v *visitor) VisitSpace(sn *ast.SpaceNode) { v.b.WriteString(sn.Lexeme) } // VisitBreak writes HTML code for line breaks. func (v *visitor) VisitBreak(bn *ast.BreakNode) { if bn.Hard { v.b.WriteString("\\\n") } else { v.b.WriteByte('\n') } if prefixLen := len(v.prefix); prefixLen > 0 { for i := 0; i <= prefixLen; i++ { v.b.WriteByte(' ') } } } // VisitLink writes HTML code for links. func (v *visitor) VisitLink(ln *ast.LinkNode) { v.b.WriteString("[[") if !ln.OnlyRef { v.acceptInlineSlice(ln.Inlines) v.b.WriteByte('|') } v.b.WriteStrings(ln.Ref.String(), "]]") } // VisitImage writes HTML code for images. func (v *visitor) VisitImage(in *ast.ImageNode) { if in.Ref != nil { v.b.WriteString("{{") if len(in.Inlines) > 0 { v.acceptInlineSlice(in.Inlines) v.b.WriteByte('|') } v.b.WriteStrings(in.Ref.String(), "}}") } } // VisitCite writes code for citations. func (v *visitor) VisitCite(cn *ast.CiteNode) { v.b.WriteStrings("[@", cn.Key) if len(cn.Inlines) > 0 { v.b.WriteString(", ") v.acceptInlineSlice(cn.Inlines) } v.b.WriteByte(']') v.visitAttributes(cn.Attrs) } // VisitFootnote write HTML code for a footnote. func (v *visitor) VisitFootnote(fn *ast.FootnoteNode) { v.b.WriteString("[^") v.acceptInlineSlice(fn.Inlines) v.b.WriteByte(']') v.visitAttributes(fn.Attrs) } // VisitMark writes HTML code to mark a position. func (v *visitor) VisitMark(mn *ast.MarkNode) { v.b.WriteStrings("[!", mn.Text, "]") } var formatCode = map[ast.FormatCode][]byte{ ast.FormatItalic: []byte("//"), ast.FormatEmph: []byte("//"), ast.FormatBold: []byte("**"), ast.FormatStrong: []byte("**"), ast.FormatUnder: []byte("__"), ast.FormatInsert: []byte("__"), ast.FormatStrike: []byte("~~"), ast.FormatDelete: []byte("~~"), ast.FormatSuper: []byte("^^"), ast.FormatSub: []byte(",,"), ast.FormatQuotation: []byte("<<"), ast.FormatQuote: []byte("\"\""), ast.FormatSmall: []byte(";;"), ast.FormatSpan: []byte("::"), ast.FormatMonospace: []byte("''"), } // VisitFormat write HTML code for formatting text. func (v *visitor) VisitFormat(fn *ast.FormatNode) { code, ok := formatCode[fn.Code] if !ok { panic(fmt.Sprintf("Unknown format code %d", fn.Code)) } attrs := fn.Attrs switch fn.Code { case ast.FormatEmph, ast.FormatStrong, ast.FormatInsert, ast.FormatDelete: attrs = attrs.Clone() attrs.Set("-", "") } v.b.Write(code) v.acceptInlineSlice(fn.Inlines) v.b.Write(code) v.visitAttributes(attrs) } // VisitLiteral write Zettelmarkup for inline literal text. func (v *visitor) VisitLiteral(ln *ast.LiteralNode) { switch ln.Code { case ast.LiteralProg: v.writeLiteral('`', ln.Attrs, ln.Text) case ast.LiteralKeyb: v.writeLiteral('+', ln.Attrs, ln.Text) case ast.LiteralOutput: v.writeLiteral('=', ln.Attrs, ln.Text) case ast.LiteralComment: v.b.WriteStrings("%% ", ln.Text) case ast.LiteralHTML: v.b.WriteString("``") v.writeEscaped(ln.Text, '`') v.b.WriteString("``{=html,.warning}") default: panic(fmt.Sprintf("Unknown literal code %v", ln.Code)) } } func (v *visitor) writeLiteral(code byte, attrs *ast.Attributes, text string) { v.b.WriteBytes(code, code) v.writeEscaped(text, code) v.b.WriteBytes(code, code) v.visitAttributes(attrs) } func (v *visitor) acceptBlockSlice(bns ast.BlockSlice) { for _, bn := range bns { bn.Accept(v) } } func (v *visitor) acceptInlineSlice(ins ast.InlineSlice) { for _, in := range ins { in.Accept(v) } } // visitAttributes write HTML attributes func (v *visitor) visitAttributes(a *ast.Attributes) { if a == nil || len(a.Attrs) == 0 { return } keys := make([]string, 0, len(a.Attrs)) for k := range a.Attrs { keys = append(keys, k) } sort.Strings(keys) v.b.WriteByte('{') for i, k := range keys { if i > 0 { v.b.WriteByte(' ') } if k == "-" { v.b.WriteByte('-') continue } v.b.WriteString(k) if vl := a.Attrs[k]; len(vl) > 0 { v.b.WriteStrings("=\"", vl) v.b.WriteByte('"') } } v.b.WriteByte('}') } func (v *visitor) writeEscaped(s string, toEscape byte) { last := 0 for i := 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:]) } |
Deleted encoding/atom/atom.go.
|
| < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < |
Deleted encoding/encoding.go.
|
| < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < |
Deleted encoding/rss/rss.go.
|
| < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < |
Deleted encoding/xml/xml.go.
|
| < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < |
Deleted evaluator/evaluator.go.
|
| < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < |
Deleted evaluator/list.go.
|
| < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < |
Deleted evaluator/metadata.go.
|
| < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < |
Changes to go.mod.
1 2 | module zettelstore.de/z | | | > | | | | < < < < < < < < | 1 2 3 4 5 6 7 8 9 10 11 12 | module zettelstore.de/z go 1.16 require ( github.com/fsnotify/fsnotify v1.4.9 github.com/pascaldekloe/jwt v1.10.0 github.com/yuin/goldmark v1.3.7 golang.org/x/crypto v0.0.0-20210220033148-5ea612d1eb83 golang.org/x/term v0.0.0-20201117132131-f5c789dd3221 golang.org/x/text v0.3.6 ) |
Changes to go.sum.
|
| | | > > | | | | > > | > | > | | | | < < < < < < | | | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 | github.com/fsnotify/fsnotify v1.4.9 h1:hsms1Qyu0jgnwNXIxa+/V/PDsU6CfLf6CNO8H7IWoS4= github.com/fsnotify/fsnotify v1.4.9/go.mod h1:znqG4EE+3YCdAaPaxE2ZRY/06pZUdp0tY4IgpuI1SZQ= github.com/pascaldekloe/jwt v1.10.0 h1:ktcIUV4TPvh404R5dIBEnPCsSwj0sqi3/0+XafE5gJs= github.com/pascaldekloe/jwt v1.10.0/go.mod h1:TKhllgThT7TOP5rGr2zMLKEDZRAgJfBbtKyVeRsNB9A= github.com/yuin/goldmark v1.3.7 h1:NSaHgaeJFCtWXCBkBKXw0rhgMuJ0VoE9FB5mWldcrQ4= github.com/yuin/goldmark v1.3.7/go.mod h1:mwnBkeHKe2W/ZEtQ+71ViKU8L12m81fl3OWwC1Zlc8k= golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= golang.org/x/crypto v0.0.0-20210220033148-5ea612d1eb83 h1:/ZScEX8SfEmUGRHs0gxpqteO5nfNW6axyZbBdw9A12g= golang.org/x/crypto v0.0.0-20210220033148-5ea612d1eb83/go.mod h1:jdWPYTVW3xRLrWPugEBEK3UY2ZEsg3UU495nc5E+M+I= golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20191005200804-aed5e4c7ecf9/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20191026070338-33540a1f6037 h1:YyJpGZS1sBuBCzLAR1VEpK193GlqGZbnPFnPV/5Rsb4= golang.org/x/sys v0.0.0-20191026070338-33540a1f6037/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/term v0.0.0-20201117132131-f5c789dd3221 h1:/ZHdbVpdR/jk3g30/d4yUL0JU9kksj8+F/bnQUVLGDM= golang.org/x/term v0.0.0-20201117132131-f5c789dd3221/go.mod h1:Nr5EML6q2oocZ2LXRh80K7BxOlk5/8JxuGnuhpl+muw= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.6 h1:aRYxNxv6iGQlyVaZmk6ZgYEDa+Jg18DxebPSrd6bg1M= golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= |
Added input/input.go.
> > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 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-2021 Detlef Stern // // This file is part of zettelstore. // // Zettelstore is licensed under the latest version of the EUPL (European Union // Public License). Please see file LICENSE.txt for your rights and obligations // under this license. //----------------------------------------------------------------------------- // Package input provides an abstraction for data to be read. package input import ( "html" "unicode" "unicode/utf8" ) // Input is an abstract input source type Input struct { // Read-only, will never change Src string // The source string // Read-only, will change Ch rune // current character Pos int // character position in src readPos int // reading position (position after current character) } // NewInput creates a new input source. func NewInput(src string) *Input { inp := &Input{Src: src} inp.Next() return inp } // EOS = End of source const EOS = rune(-1) // Next reads the next rune into inp.Ch. func (inp *Input) Next() { if inp.readPos < len(inp.Src) { inp.Pos = inp.readPos r, w := rune(inp.Src[inp.readPos]), 1 if r >= utf8.RuneSelf { r, w = utf8.DecodeRuneInString(inp.Src[inp.readPos:]) } inp.readPos += w inp.Ch = r } else { inp.Pos = len(inp.Src) inp.Ch = EOS } } // Peek returns the rune following the most recently read rune without // advancing. If end-of-source was already found peek returns EOS. func (inp *Input) Peek() rune { return inp.PeekN(0) } // PeekN returns the n-th rune after the most recently read rune without // advancing. If end-of-source was already found peek returns EOS. func (inp *Input) PeekN(n int) rune { pos := inp.readPos + n if pos < len(inp.Src) { r := rune(inp.Src[pos]) if r >= utf8.RuneSelf { r, _ = utf8.DecodeRuneInString(inp.Src[pos:]) } if r == '\t' { return ' ' } return r } return EOS } // IsEOLEOS returns true if char is either EOS or EOL. func IsEOLEOS(ch rune) bool { switch ch { case EOS, '\n', '\r': return true } return false } // EatEOL transforms both "\r" and "\r\n" into "\n". func (inp *Input) EatEOL() { switch inp.Ch { case '\r': if inp.Peek() == '\n' { inp.Next() } inp.Ch = '\n' inp.Next() case '\n': inp.Next() } } // SetPos allows to reset the read position. func (inp *Input) SetPos(pos int) { inp.readPos = pos inp.Next() } // SkipToEOL reads until the next end-of-line. func (inp *Input) SkipToEOL() { for { switch inp.Ch { case EOS, '\n', '\r': return } inp.Next() } } // ScanEntity scans either a named or a numbered entity and returns it as a string. // // For numbered entities (like { or ģ) html.UnescapeString returns // sometimes other values as expected, if the number is not well-formed. This // may happen because of some strange HTML parsing rules. But these do not // apply to Zettelmarkup. Therefore, I parse the number here in the code. func (inp *Input) ScanEntity() (res string, success bool) { if inp.Ch != '&' { return "", false } pos := inp.Pos inp.Next() if inp.Ch == '#' { inp.Next() if inp.Ch == 'x' || inp.Ch == 'X' { return inp.scanEntityBase16() } return inp.scanEntityBase10() } return inp.scanEntityNamed(pos) } func (inp *Input) scanEntityBase16() (string, bool) { inp.Next() if inp.Ch == ';' { return "", false } code := 0 for { switch ch := inp.Ch; ch { case ';': inp.Next() return string(rune(code)), true case '0', '1', '2', '3', '4', '5', '6', '7', '8', '9': code = 16*code + int(ch-'0') case 'a', 'b', 'c', 'd', 'e', 'f': code = 16*code + int(ch-'a'+10) case 'A', 'B', 'C', 'D', 'E', 'F': code = 16*code + int(ch-'A'+10) default: return "", false } if code > unicode.MaxRune { return "", false } inp.Next() } } func (inp *Input) scanEntityBase10() (string, bool) { // Base 10 code if inp.Ch == ';' { return "", false } code := 0 for { switch ch := inp.Ch; ch { case ';': inp.Next() return string(rune(code)), true case '0', '1', '2', '3', '4', '5', '6', '7', '8', '9': code = 10*code + int(ch-'0') default: return "", false } if code > unicode.MaxRune { return "", false } inp.Next() } } func (inp *Input) scanEntityNamed(pos int) (string, bool) { for { switch inp.Ch { case EOS, '\n', '\r': return "", false case ';': inp.Next() es := inp.Src[pos:inp.Pos] ues := html.UnescapeString(es) if es == ues { return "", false } return ues, true } inp.Next() } } |
Added input/input_test.go.
> > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 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) 2020-2021 Detlef Stern // // This file is part of zettelstore. // // Zettelstore is licensed under the latest version of the EUPL (European Union // Public License). Please see file LICENSE.txt for your rights and obligations // under this license. //----------------------------------------------------------------------------- // Package input_test provides some unit-tests for reading data. package input_test import ( "testing" "zettelstore.de/z/input" ) func TestEatEOL(t *testing.T) { inp := input.NewInput("") inp.EatEOL() if inp.Ch != input.EOS { t.Errorf("No EOS found: %q", inp.Ch) } if inp.Pos != 0 { t.Errorf("Pos != 0: %d", inp.Pos) } inp = input.NewInput("ABC") if inp.Ch != 'A' { t.Errorf("First ch != 'A', got %q", inp.Ch) } inp.EatEOL() if inp.Ch != 'A' { t.Errorf("First ch != 'A', got %q", inp.Ch) } } func TestScanEntity(t *testing.T) { var testcases = []struct { text string exp string }{ {"", ""}, {"a", ""}, {"&", "&"}, {"	", "\t"}, {""", "\""}, } for id, tc := range testcases { inp := input.NewInput(tc.text) got, ok := inp.ScanEntity() if !ok { if tc.exp != "" { t.Errorf("ID=%d, text=%q: expected error, but got %q", id, tc.text, got) } if inp.Pos != 0 { t.Errorf("ID=%d, text=%q: input position advances to %d", id, tc.text, inp.Pos) } continue } if tc.exp != got { t.Errorf("ID=%d, text=%q: expected %q, but got %q", id, tc.text, tc.exp, got) } } } |
Changes to kernel/impl/auth.go.
1 | //----------------------------------------------------------------------------- | | | < < < > < | | < < < < | < | | < < < | | | < < | | | | | > < > | > > | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 | //----------------------------------------------------------------------------- // Copyright (c) 2021 Detlef Stern // // This file is part of zettelstore. // // Zettelstore is licensed under the latest version of the EUPL (European Union // Public License). Please see file LICENSE.txt for your rights and obligations // under this license. //----------------------------------------------------------------------------- // Package impl provides the kernel implementation. package impl import ( "sync" "zettelstore.de/z/auth" "zettelstore.de/z/domain/id" "zettelstore.de/z/kernel" ) type authService struct { srvConfig mxService sync.RWMutex manager auth.Manager createManager kernel.CreateAuthManagerFunc } func (as *authService) Initialize() { as.descr = descriptionMap{ kernel.AuthOwner: { "Owner's zettel id", func(val string) interface{} { if owner := as.cur[kernel.AuthOwner]; owner != nil && owner != id.Invalid { return nil } return parseZid(val) }, false, }, kernel.AuthReadonly: { "Read-only mode", func(val string) interface{} { if ro := as.cur[kernel.AuthReadonly]; ro == true { return nil } return parseBool(val) }, true, }, } as.next = interfaceMap{ kernel.AuthOwner: id.Invalid, kernel.AuthReadonly: false, } } func (as *authService) Start(kern *myKernel) error { as.mxService.Lock() defer as.mxService.Unlock() readonlyMode := as.GetNextConfig(kernel.AuthReadonly).(bool) owner := as.GetNextConfig(kernel.AuthOwner).(id.Zid) authMgr, err := as.createManager(readonlyMode, owner) if err != nil { kern.doLog("Unable to create auth manager:", err) return err } kern.doLog("Start Auth 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(kern *myKernel) error { kern.doLog("Stop Auth Manager") as.mxService.Lock() defer as.mxService.Unlock() as.manager = nil return nil } func (as *authService) GetStatistics() []kernel.KeyValue { return nil } |
Deleted kernel/impl/box.go.
|
| < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < |
Changes to kernel/impl/cfg.go.
1 | //----------------------------------------------------------------------------- | | | < < < > < < | | | < < < < < < | < < < < < < < < < < < < | < | < | > | > > | | | | | | | | | | | < | < | < < | | | < | | | | < < < < < | | > > > | | > < | | | < | | | < < < < < < | | | | | | | | < > | > > > > > > > > > > | | < > > | > | > > > > | | | | < < | | < | | < < | < | | < | > > | > > | | | | | < | < | < | < | < | < | | > > > > > > | < | | | | < < > > | | | | < | | > | | < < < < < | < > | > | > | | < | | | > | > > | < < | | | | < < | > > | | > < | < > | < < | | < < < | < > > > > | | > | | > | | | | | | > > > | > > | > > | < < | > | > | > > > | | < > | | | | < < < | | < < < < < < | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 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 Detlef Stern // // This file is part of zettelstore. // // Zettelstore is licensed under the latest version of the EUPL (European Union // Public License). Please see file LICENSE.txt for your rights and obligations // under this license. //----------------------------------------------------------------------------- // Package impl provides the kernel implementation. package impl import ( "context" "fmt" "strconv" "strings" "sync" "zettelstore.de/z/domain/id" "zettelstore.de/z/domain/meta" "zettelstore.de/z/kernel" "zettelstore.de/z/place" ) type configService struct { srvConfig mxService sync.RWMutex rtConfig *myConfig } func (cs *configService) Initialize() { cs.descr = descriptionMap{ meta.KeyDefaultCopyright: {"Default copyright", parseString, true}, meta.KeyDefaultLang: {"Default language", parseString, true}, meta.KeyDefaultRole: {"Default role", parseString, true}, meta.KeyDefaultSyntax: {"Default syntax", parseString, true}, meta.KeyDefaultTitle: {"Default title", parseString, true}, meta.KeyDefaultVisibility: { "Default zettel visibility", func(val string) interface{} { vis := meta.GetVisibility(val) if vis == meta.VisibilityUnknown { return nil } return vis }, true, }, meta.KeyExpertMode: {"Expert mode", parseBool, true}, meta.KeyFooterHTML: {"Footer HTML", parseString, true}, meta.KeyHomeZettel: {"Home zettel", parseZid, true}, meta.KeyListPageSize: { "List page size", func(val string) interface{} { iVal, err := strconv.Atoi(val) if err != nil { return nil } return iVal }, true, }, meta.KeyMarkerExternal: {"Marker external URL", parseString, true}, meta.KeySiteName: {"Site name", parseString, true}, meta.KeyYAMLHeader: {"YAML header", parseBool, true}, meta.KeyZettelFileSyntax: { "Zettel file syntax", func(val string) interface{} { return strings.Fields(val) }, true, }, } cs.next = interfaceMap{ meta.KeyDefaultCopyright: "", meta.KeyDefaultLang: meta.ValueLangEN, meta.KeyDefaultRole: meta.ValueRoleZettel, meta.KeyDefaultSyntax: meta.ValueSyntaxZmk, meta.KeyDefaultTitle: "Untitled", meta.KeyDefaultVisibility: meta.VisibilityLogin, meta.KeyExpertMode: false, meta.KeyFooterHTML: "", meta.KeyHomeZettel: id.DefaultHomeZid, meta.KeyListPageSize: 0, meta.KeyMarkerExternal: "➚", meta.KeySiteName: "Zettelstore", meta.KeyYAMLHeader: false, meta.KeyZettelFileSyntax: nil, } } func (cs *configService) Start(kern *myKernel) error { kern.doLog("Start Config Service") data := meta.New(id.ConfigurationZid) for _, kv := range cs.GetNextConfigList() { data.Set(kv.Key, fmt.Sprintf("%v", kv.Value)) } cs.mxService.Lock() cs.rtConfig = newConfig(data) cs.mxService.Unlock() return nil } func (cs *configService) IsStarted() bool { cs.mxService.RLock() defer cs.mxService.RUnlock() return cs.rtConfig != nil } func (cs *configService) Stop(kern *myKernel) error { kern.doLog("Stop Config Service") cs.mxService.Lock() cs.rtConfig = nil cs.mxService.Unlock() return nil } func (cs *configService) GetStatistics() []kernel.KeyValue { return nil } func (cs *configService) setPlace(mgr place.Manager) { cs.rtConfig.setPlace(mgr) } // myConfig contains all runtime configuration data relevant for the software. type myConfig struct { mx sync.RWMutex orig *meta.Meta data *meta.Meta } // New creates a new Config value. func newConfig(orig *meta.Meta) *myConfig { cfg := myConfig{ orig: orig, data: orig.Clone(), } return &cfg } func (cfg *myConfig) setPlace(mgr place.Manager) { mgr.RegisterObserver(cfg.observe) cfg.doUpdate(mgr) } func (cfg *myConfig) doUpdate(p place.Place) error { m, err := p.GetMeta(context.Background(), cfg.data.Zid) if err != nil { return err } cfg.mx.Lock() for _, pair := range cfg.data.Pairs(false) { if val, ok := m.Get(pair.Key); ok { cfg.data.Set(pair.Key, val) } } cfg.mx.Unlock() return nil } func (cfg *myConfig) observe(ci place.UpdateInfo) { if ci.Reason == place.OnReload || ci.Zid == id.ConfigurationZid { go func() { cfg.doUpdate(ci.Place) }() } } var defaultKeys = map[string]string{ meta.KeyCopyright: meta.KeyDefaultCopyright, meta.KeyLang: meta.KeyDefaultLang, meta.KeyLicense: meta.KeyDefaultLicense, meta.KeyRole: meta.KeyDefaultRole, meta.KeySyntax: meta.KeyDefaultSyntax, meta.KeyTitle: meta.KeyDefaultTitle, } // AddDefaultValues enriches the given meta data with its default values. func (cfg *myConfig) AddDefaultValues(m *meta.Meta) *meta.Meta { if cfg == nil { return m } result := m cfg.mx.RLock() for k, d := range defaultKeys { if _, ok := result.Get(k); !ok { if result == m { result = m.Clone() } if val, ok := cfg.data.Get(d); ok { result.Set(k, val) } } } cfg.mx.RUnlock() return result } func (cfg *myConfig) getString(key string) string { cfg.mx.RLock() val, _ := cfg.data.Get(key) cfg.mx.RUnlock() return val } func (cfg *myConfig) getBool(key string) bool { cfg.mx.RLock() val := cfg.data.GetBool(key) cfg.mx.RUnlock() return val } // GetDefaultTitle returns the current value of the "default-title" key. func (cfg *myConfig) GetDefaultTitle() string { return cfg.getString(meta.KeyDefaultTitle) } // GetDefaultRole returns the current value of the "default-role" key. func (cfg *myConfig) GetDefaultRole() string { return cfg.getString(meta.KeyDefaultRole) } // GetDefaultSyntax returns the current value of the "default-syntax" key. func (cfg *myConfig) GetDefaultSyntax() string { return cfg.getString(meta.KeyDefaultSyntax) } // GetDefaultLang returns the current value of the "default-lang" key. func (cfg *myConfig) GetDefaultLang() string { return cfg.getString(meta.KeyDefaultLang) } // GetSiteName returns the current value of the "site-name" key. func (cfg *myConfig) GetSiteName() string { return cfg.getString(meta.KeySiteName) } // GetHomeZettel returns the value of the "home-zettel" key. func (cfg *myConfig) GetHomeZettel() id.Zid { val := cfg.getString(meta.KeyHomeZettel) if homeZid, err := id.Parse(val); err == nil { return homeZid } cfg.mx.RLock() val, _ = cfg.orig.Get(meta.KeyHomeZettel) homeZid, _ := id.Parse(val) cfg.mx.RUnlock() return homeZid } // GetDefaultVisibility returns the default value for zettel visibility. func (cfg *myConfig) GetDefaultVisibility() meta.Visibility { val := cfg.getString(meta.KeyDefaultVisibility) if vis := meta.GetVisibility(val); vis != meta.VisibilityUnknown { return vis } cfg.mx.RLock() val, _ = cfg.orig.Get(meta.KeyDefaultVisibility) vis := meta.GetVisibility(val) cfg.mx.RUnlock() return vis } // GetYAMLHeader returns the current value of the "yaml-header" key. func (cfg *myConfig) GetYAMLHeader() bool { return cfg.getBool(meta.KeyYAMLHeader) } // GetMarkerExternal returns the current value of the "marker-external" key. func (cfg *myConfig) GetMarkerExternal() string { return cfg.getString(meta.KeyMarkerExternal) } // GetFooterHTML returns HTML code that should be embedded into the footer // of each WebUI page. func (cfg *myConfig) GetFooterHTML() string { return cfg.getString(meta.KeyFooterHTML) } // GetListPageSize returns the maximum length of a list to be returned in WebUI. // A value less or equal to zero signals no limit. func (cfg *myConfig) GetListPageSize() int { cfg.mx.RLock() defer cfg.mx.RUnlock() if value, ok := cfg.data.GetNumber(meta.KeyListPageSize); ok { return value } value, _ := cfg.orig.GetNumber(meta.KeyListPageSize) return value } // GetZettelFileSyntax returns the current value of the "zettel-file-syntax" key. func (cfg *myConfig) GetZettelFileSyntax() []string { cfg.mx.RLock() defer cfg.mx.RUnlock() return cfg.data.GetListOrNil(meta.KeyZettelFileSyntax) } // --- AuthConfig // GetExpertMode returns the current value of the "expert-mode" key func (cfg *myConfig) GetExpertMode() bool { return cfg.getBool(meta.KeyExpertMode) } // GetVisibility returns the visibility value, or "login" if none is given. func (cfg *myConfig) GetVisibility(m *meta.Meta) meta.Visibility { if val, ok := m.Get(meta.KeyVisibility); ok { if vis := meta.GetVisibility(val); vis != meta.VisibilityUnknown { return vis } } return cfg.GetDefaultVisibility() } |
Changes to kernel/impl/cmd.go.
1 | //----------------------------------------------------------------------------- | | | < < < > < < < | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 | //----------------------------------------------------------------------------- // Copyright (c) 2021 Detlef Stern // // This file is part of zettelstore. // // Zettelstore is licensed under the latest version of the EUPL (European Union // Public License). Please see file LICENSE.txt for your rights and obligations // under this license. //----------------------------------------------------------------------------- // Package impl provides the kernel implementation. package impl import ( "fmt" "io" "os" "runtime/metrics" "sort" "strings" "zettelstore.de/z/kernel" "zettelstore.de/z/strfun" ) type cmdSession struct { w io.Writer kern *myKernel echo bool |
︙ | ︙ | |||
68 69 70 71 72 73 74 | io.WriteString(sess.w, " ") io.WriteString(sess.w, arg) } } sess.w.Write(sess.eol) } | < < < < | 63 64 65 66 67 68 69 70 71 72 73 74 75 76 | io.WriteString(sess.w, " ") io.WriteString(sess.w, arg) } } sess.w.Write(sess.eol) } func (sess *cmdSession) printTable(table [][]string) { maxLen := sess.calcMaxLen(table) if len(maxLen) == 0 { return } if sess.header { sess.printRow(table[0], maxLen, "|=", " | ", ' ') |
︙ | ︙ | |||
165 166 167 168 169 170 171 | sess.println("echo is on") } else { sess.println("echo is off") } return true }, }, | < | | < < < | | > > > > > > > | 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 | sess.println("echo is on") } else { sess.println("echo is off") } return true }, }, "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 }, }, "metrics": {"show Go runtime metrics", cmdMetrics}, "next-config": {"show next configuration data", cmdNextConfig}, "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, cmd string, args []string) bool { cmds := make([]string, 0, len(commands)) for key := range commands { if key == "" { continue } cmds = append(cmds, key) } sort.Strings(cmds) table := [][]string{{"Command", "Description"}} for _, cmd := range cmds { table = append(table, []string{cmd, commands[cmd].Text}) } sess.printTable(table) return true } |
︙ | ︙ | |||
220 221 222 223 224 225 226 | table := [][]string{{"Key", "Description"}} for _, kd := range srv.ConfigDescriptions() { table = append(table, []string{kd.Key, kd.Descr}) } sess.printTable(table) return true } | | | | | 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 | 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, cmd string, args []string) bool { showConfig(sess, args, listCurConfig, func(srv service, key string) interface{} { return srv.GetConfig(key) }) return true } func cmdNextConfig(sess *cmdSession, cmd string, args []string) bool { showConfig(sess, args, listNextConfig, func(srv service, key string) interface{} { return srv.GetNextConfig(key) }) return true } func showConfig(sess *cmdSession, args []string, listConfig func(*cmdSession, service), getConfig func(service, string) interface{}) { |
︙ | ︙ | |||
250 251 252 253 254 255 256 | srvD := sess.kern.srvs[kernel.Service(k)] sess.println("%% Service", srvD.name) listConfig(sess, srvD.srv) } return } | | | > | | | | > < | < < | | > > > > > > | | 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 | srvD := sess.kern.srvs[kernel.Service(k)] sess.println("%% Service", srvD.name) listConfig(sess, srvD.srv) } return } srvD, ok := sess.kern.srvNames[args[0]] if !ok { sess.println("Unknown service:", args[0]) return } if len(args) == 1 { listConfig(sess, srvD.srv) return } 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.GetConfigList(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.println("Usage:", cmd, "SERIVCE KEY VALUE") return true } srvD, ok := sess.kern.srvNames[args[0]] if !ok { sess.println("Unknown service:", args[0]) return true } newValue := strings.Join(args[2:], " ") if !srvD.srv.SetConfig(args[1], newValue) { sess.println("Unable to set key", args[1], "to value", newValue) } return true } func cmdServices(sess *cmdSession, cmd string, args []string) bool { names := make([]string, 0, len(sess.kern.srvNames)) for name := range sess.kern.srvNames { names = append(names, name) } sort.Strings(names) table := [][]string{{"Service", "Status"}} for _, name := range names { if sess.kern.srvNames[name].srv.IsStarted() { table = append(table, []string{name, "started"}) } else { table = append(table, []string{name, "stopped"}) } } sess.printTable(table) |
︙ | ︙ | |||
350 351 352 353 354 355 356 | sess.println(err.Error()) } return true } func cmdStat(sess *cmdSession, cmd string, args []string) bool { if len(args) == 0 { | | | > < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < | | > < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < | | 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 | sess.println(err.Error()) } return true } func cmdStat(sess *cmdSession, cmd string, args []string) bool { if len(args) == 0 { sess.println("Usage:", cmd, "SERVICE") return true } srvD, ok := sess.kern.srvNames[args[0]] if !ok { sess.println("Unknown service", args[0]) return true } kvl := srvD.srv.GetStatistics() if len(kvl) == 0 { return true } table := [][]string{{"Key", "Value"}} for _, kv := range kvl { table = append(table, []string{kv.Key, kv.Value}) } sess.printTable(table) return true } func lookupService(sess *cmdSession, cmd string, args []string) (kernel.Service, bool) { if len(args) == 0 { sess.println("Usage:", cmd, "SERVICE") return 0, false } srvD, ok := sess.kern.srvNames[args[0]] if !ok { sess.println("Unknown service", args[0]) return 0, false } return srvD.srvnum, true } func cmdMetrics(sess *cmdSession, cmd string, args []string) bool { var samples []metrics.Sample all := metrics.All() for _, d := range all { if d.Kind == metrics.KindFloat64Histogram { continue } samples = append(samples, metrics.Sample{Name: d.Name}) |
︙ | ︙ | |||
487 488 489 490 491 492 493 | descr = descr[:pos] } value := samples[i].Value i++ var sVal string switch value.Kind() { case metrics.KindUint64: | | | < < < < < < < < | | | 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 | descr = descr[:pos] } value := samples[i].Value i++ var sVal string switch value.Kind() { case metrics.KindUint64: sVal = fmt.Sprintf("%v", value.Uint64()) 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, cmd string, args []string) bool { sess.kern.DumpIndex(sess.w) return true } func cmdDumpRecover(sess *cmdSession, cmd string, args []string) bool { if len(args) == 0 { sess.println("Usage:", cmd, "RECOVER") sess.println("-- A valid value for RECOVER can be obtained via 'stat core'.") return true } lines := sess.kern.core.RecoverLines(args[0]) if len(lines) == 0 { return true } for _, line := range lines { sess.println(line) } return true } func cmdEnvironment(sess *cmdSession, cmd string, args []string) bool { workDir, err := os.Getwd() if err != nil { workDir = err.Error() } execName, err := os.Executable() if err != nil { execName = err.Error() |
︙ | ︙ | |||
556 557 558 559 560 561 562 | if pos := strings.IndexByte(env, '='); pos >= 0 && pos < len(env) { table = append(table, []string{env[:pos], env[pos+1:]}) } } sess.printTable(table) return true } | < < < < < < < < < < | 469 470 471 472 473 474 475 | if pos := strings.IndexByte(env, '='); pos >= 0 && pos < len(env) { table = append(table, []string{env[:pos], env[pos+1:]}) } } sess.printTable(table) return true } |
Changes to kernel/impl/config.go.
1 | //----------------------------------------------------------------------------- | | | < < < > < < | | < | < > | > > > < < | | < < | | > | | | | | | | | | | | | | > > > > > > > > > > | > > > > > > > > < < < < < < < < < < < < < < < < < < < < < < | < < | | | | < < < < < < < < | | < < < < < < | | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 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 | //----------------------------------------------------------------------------- // Copyright (c) 2021 Detlef Stern // // This file is part of zettelstore. // // Zettelstore is licensed under the latest version of the EUPL (European Union // Public License). Please see file LICENSE.txt for your rights and obligations // under this license. //----------------------------------------------------------------------------- // Package impl provides the kernel implementation. package impl import ( "fmt" "sort" "strconv" "strings" "sync" "zettelstore.de/z/domain/id" "zettelstore.de/z/kernel" ) type parseFunc func(string) interface{} 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 { mxConfig sync.RWMutex frozen bool descr descriptionMap cur interfaceMap next interfaceMap } func (cfg *srvConfig) ConfigDescriptions() []serviceConfigDescription { cfg.mxConfig.RLock() defer cfg.mxConfig.RUnlock() keys := make([]string, 0, len(cfg.descr)) for k := range cfg.descr { keys = append(keys, k) } sort.Strings(keys) result := make([]serviceConfigDescription, 0, len(keys)) for _, k := range keys { text := cfg.descr[k].text if strings.HasSuffix(k, "-") { text = text + " (list)" } result = append(result, serviceConfigDescription{Key: k, Descr: text}) } return result } func (cfg *srvConfig) noFrozen(parse parseFunc) parseFunc { return func(val string) interface{} { if cfg.frozen { return nil } return parse(val) } } func (cfg *srvConfig) SetConfig(key, value string) bool { cfg.mxConfig.Lock() defer cfg.mxConfig.Unlock() descr, ok := cfg.descr[key] if !ok { d, baseKey, num := cfg.getListDescription(key) if num < 0 { return false } format := baseKey + "%d" for i := num + 1; ; i++ { k := fmt.Sprintf(format, i) if _, ok = cfg.next[k]; !ok { break } delete(cfg.next, k) } if num == 0 { return true } descr = d } parse := descr.parse if parse == nil { if cfg.frozen { return false } cfg.next[key] = value return true } iVal := parse(value) if iVal == nil { return false } cfg.next[key] = iVal return true } 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) GetConfig(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) GetConfigList(all bool) []kernel.KeyDescrValue { return cfg.getConfigList(all, cfg.GetConfig) } func (cfg *srvConfig) GetNextConfigList() []kernel.KeyDescrValue { return cfg.getConfigList(true, cfg.GetNextConfig) } func (cfg *srvConfig) getConfigList(all bool, getConfig func(string) interface{}) []kernel.KeyDescrValue { if len(cfg.descr) == 0 { return nil } keys := make([]string, 0, len(cfg.descr)) for k, descr := range cfg.descr { if all || descr.canList { if !strings.HasSuffix(k, "-") { keys = append(keys, k) continue } format := k + "%d" for i := 1; ; i++ { key := fmt.Sprintf(format, i) val := getConfig(key) if val == nil { break } keys = append(keys, key) } } } sort.Strings(keys) result := make([]kernel.KeyDescrValue, 0, len(keys)) for _, k := range keys { val := getConfig(k) if val == nil { continue } 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) 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) interface{} { return val } func parseBool(val string) interface{} { if val == "" { return false } switch val[0] { case '0', 'f', 'F', 'n', 'N': return false } return true } func parseZid(val string) interface{} { if zid, err := id.Parse(val); err == nil { return zid } return id.Invalid } |
Changes to kernel/impl/core.go.
1 | //----------------------------------------------------------------------------- | | | < < < > > < < < | < < | | | < | | | < < < < < | | > > | > > > | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 | //----------------------------------------------------------------------------- // Copyright (c) 2021 Detlef Stern // // This file is part of zettelstore. // // Zettelstore is licensed under the latest version of the EUPL (European Union // Public License). Please see file LICENSE.txt for your rights and obligations // under this license. //----------------------------------------------------------------------------- // Package impl provides the kernel implementation. package impl import ( "fmt" "net" "os" "runtime" "sort" "sync" "time" "zettelstore.de/z/kernel" "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() { cs.mapRecover = make(map[string]recoverInfo) cs.descr = descriptionMap{ 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) interface{} { port, err := net.LookupPort("tcp", val) if err != nil { return nil } return port }), true, }, kernel.CoreProgname: {"Program name", nil, false}, kernel.CoreVerbose: {"Verbose output", parseBool, true}, kernel.CoreVersion: { "Version", cs.noFrozen(func(val string) interface{} { if val == "" { return "unknown" } return val }), false, }, } cs.next = interfaceMap{ kernel.CoreGoArch: runtime.GOARCH, kernel.CoreGoOS: runtime.GOOS, kernel.CoreGoVersion: runtime.Version(), kernel.CoreHostname: "*unknown host*", kernel.CorePort: 0, kernel.CoreVerbose: false, } if hn, err := os.Hostname(); err == nil { cs.next[kernel.CoreHostname] = hn } } func (cs *coreService) Start(kern *myKernel) error { cs.started = true return nil } func (cs *coreService) IsStarted() bool { return cs.started } func (cs *coreService) Stop(*myKernel) error { cs.started = false return nil } func (cs *coreService) GetStatistics() []kernel.KeyValue { cs.mxRecover.RLock() defer cs.mxRecover.RUnlock() names := make([]string, 0, len(cs.mapRecover)) for n := range cs.mapRecover { names = append(names, n) } sort.Strings(names) result := make([]kernel.KeyValue, 0, 3*len(names)) for _, n := range names { ri := cs.mapRecover[n] result = append( result, kernel.KeyValue{ Key: fmt.Sprintf("Recover %q / Count", n), |
︙ | ︙ | |||
146 147 148 149 150 151 152 | ) } func (cs *coreService) updateRecoverInfo(name string, recoverInfo interface{}, stack []byte) { cs.mxRecover.Lock() ri := cs.mapRecover[name] ri.count++ | | | 139 140 141 142 143 144 145 146 147 148 149 150 151 | ) } func (cs *coreService) updateRecoverInfo(name string, recoverInfo interface{}, stack []byte) { cs.mxRecover.Lock() ri := cs.mapRecover[name] ri.count++ ri.ts = time.Now() ri.info = recoverInfo ri.stack = stack cs.mapRecover[name] = ri cs.mxRecover.Unlock() } |
Changes to kernel/impl/impl.go.
1 | //----------------------------------------------------------------------------- | | | < < < < > < < < < < < < | | < < < < < | | | | | | | < < < < < < | | | | < < < < < | | | | | | < < | < | | | < < < < < < | < < < | | | < | | | | | | | < < < < < < < < < < | | < | | | | | | | < | | > | > > > > > > < | < < < | | < < | | < < < < < < < < | < < < < < < < < < < | < < < < < < < < < < < < < < < < < < < < < < < | < < | < < > < > < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < | | | | < < < < < < < < < < < < < < < < < < < < | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 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 Detlef Stern // // This file is part of zettelstore. // // Zettelstore is licensed under 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 ( "fmt" "io" "log" "net" "os" "os/signal" "runtime/debug" "strconv" "sync" "syscall" "zettelstore.de/z/kernel" ) // myKernel is the main internal kernel. type myKernel struct { // started bool wg sync.WaitGroup mx sync.RWMutex interrupt chan os.Signal debug bool core coreService cfg configService auth authService place placeService web webService srvs map[kernel.Service]serviceDescr srvNames map[string]serviceData depStart serviceDependency depStop serviceDependency // reverse of depStart } type serviceDescr struct { srv service name string } type serviceData struct { srv service srvnum kernel.Service } type serviceDependency map[kernel.Service][]kernel.Service // create and start a new kernel. func init() { kernel.Main = createAndStart() } // create and start a new kernel. func createAndStart() kernel.Kernel { kern := &myKernel{ interrupt: make(chan os.Signal, 5), } kern.srvs = map[kernel.Service]serviceDescr{ kernel.CoreService: {&kern.core, "core"}, kernel.ConfigService: {&kern.cfg, "config"}, kernel.AuthService: {&kern.auth, "auth"}, kernel.PlaceService: {&kern.place, "place"}, kernel.WebService: {&kern.web, "web"}, } kern.srvNames = make(map[string]serviceData, len(kern.srvs)) for key, srvD := range kern.srvs { if _, ok := kern.srvNames[srvD.name]; ok { panic(fmt.Sprintf("Key %q already given for service %v", key, srvD.name)) } kern.srvNames[srvD.name] = serviceData{srvD.srv, key} srvD.srv.Initialize() } kern.depStart = serviceDependency{ kernel.CoreService: nil, kernel.ConfigService: {kernel.CoreService}, kernel.AuthService: {kernel.CoreService}, kernel.PlaceService: {kernel.CoreService, kernel.ConfigService, kernel.AuthService}, kernel.WebService: {kernel.ConfigService, kernel.AuthService, kernel.PlaceService}, } kern.depStop = make(serviceDependency, len(kern.depStart)) for srv, deps := range kern.depStart { for _, dep := range deps { kern.depStop[dep] = append(kern.depStop[dep], srv) } } return kern } func (kern *myKernel) Start(headline bool) { for _, srvD := range kern.srvs { srvD.srv.Freeze() } 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.doLog("Shut down Zettelstore:", strSig) } kern.shutdown() kern.wg.Done() }() kern.StartService(kernel.CoreService) if headline { kern.doLog(fmt.Sprintf( "%v %v (%v@%v/%v)", kern.core.GetConfig(kernel.CoreProgname), kern.core.GetConfig(kernel.CoreVersion), kern.core.GetConfig(kernel.CoreGoVersion), kern.core.GetConfig(kernel.CoreGoOS), kern.core.GetConfig(kernel.CoreGoArch), )) kern.doLog("Licensed under the latest version of the EUPL (European Union Public License)") if kern.auth.GetConfig(kernel.AuthReadonly).(bool) { kern.doLog("Read-only mode") } } port := kern.core.GetNextConfig(kernel.CorePort).(int) if port > 0 { listenAddr := net.JoinHostPort("127.0.0.1", strconv.Itoa(port)) startLineServer(kern, listenAddr) } } func (kern *myKernel) shutdown() { kern.StopService(kernel.CoreService) // Will stop all other services. } func (kern *myKernel) WaitForShutdown() { kern.wg.Wait() } func (kern *myKernel) SetDebug(enable bool) bool { kern.mx.Lock() prevDebug := kern.debug kern.debug = enable kern.mx.Unlock() return prevDebug } // --- Shutdown operation ---------------------------------------------------- // Shutdown the service. Waits for all concurrent activity to stop. func (kern *myKernel) Shutdown(silent bool) { kern.interrupt <- &shutdownSignal{silent: silent} } type shutdownSignal struct{ silent bool } func (s *shutdownSignal) String() string { if s.silent { return "" } return "shutdown" } func (s *shutdownSignal) Signal() { /* Just a signal */ } // --- Log operation --------------------------------------------------------- // Log some activity. func (kern *myKernel) Log(args ...interface{}) { kern.mx.Lock() defer kern.mx.Unlock() kern.doLog(args...) } func (kern *myKernel) doLog(args ...interface{}) { log.Println(args...) } // LogRecover outputs some information about the previous panic. func (kern *myKernel) LogRecover(name string, recoverInfo interface{}) bool { return kern.doLogRecover(name, recoverInfo) } func (kern *myKernel) doLogRecover(name string, recoverInfo interface{}) bool { kern.Log(name, "recovered from:", recoverInfo) stack := debug.Stack() os.Stderr.Write(stack) kern.core.updateRecoverInfo(name, recoverInfo, stack) return true } // --- Service handling -------------------------------------------------- func (kern *myKernel) SetConfig(srvnum kernel.Service, key, value string) bool { kern.mx.Lock() defer kern.mx.Unlock() if srvD, ok := kern.srvs[srvnum]; ok { return srvD.srv.SetConfig(key, value) } return false } func (kern *myKernel) GetConfig(srvnum kernel.Service, key string) interface{} { kern.mx.RLock() defer kern.mx.RUnlock() if srvD, ok := kern.srvs[srvnum]; ok { return srvD.srv.GetConfig(key) } return nil } func (kern *myKernel) GetConfigList(srvnum kernel.Service) []kernel.KeyDescrValue { kern.mx.RLock() defer kern.mx.RUnlock() if srvD, ok := kern.srvs[srvnum]; ok { return srvD.srv.GetConfigList(false) } return nil } func (kern *myKernel) GetServiceStatistics(srvnum kernel.Service) []kernel.KeyValue { kern.mx.RLock() defer kern.mx.RUnlock() if srvD, ok := kern.srvs[srvnum]; ok { return srvD.srv.GetStatistics() } return nil } func (kern *myKernel) 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) { |
︙ | ︙ | |||
430 431 432 433 434 435 436 | 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 { | | > > | | < | > > | | < | | < < < | | | | | | | | < < < < < < < < < < < < < < < < < < < < < | 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 | 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 { if err := srv.Stop(kern); err != nil { return err } } for i := len(deps) - 1; i >= 0; i-- { srv := deps[i] if err := srv.Start(kern); err != nil { return err } srv.SwitchNextToCur() } return nil } func (kern *myKernel) StopService(srvnum kernel.Service) error { kern.mx.RLock() defer kern.mx.RUnlock() return kern.doStopService(srvnum) } func (kern *myKernel) doStopService(srvnum kernel.Service) error { for _, srv := range kern.sortDependency(srvnum, kern.depStop, false) { if err := srv.Stop(kern); err != nil { return err } } 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, 4) result := make([]service, 0, 4) for _, dep := range deps { srvDeps := kern.sortDependency(dep, srvdeps, isStarted) for _, depSrv := range srvDeps { if !found[depSrv] { result = append(result, depSrv) found[depSrv] = true } } } return append(result, srvD.srv) } func (kern *myKernel) DumpIndex(w io.Writer) { kern.place.DumpIndex(w) } type service interface { // Initialize the data for the service. Initialize() // ConfigDescriptions returns a sorted list of configuration descriptions. ConfigDescriptions() []serviceConfigDescription // SetConfig stores a configuration value. SetConfig(key, value string) bool // GetConfig returns the current configuration value. GetConfig(key string) interface{} // GetNextConfig returns the next configuration value. GetNextConfig(key string) interface{} // GetConfigList returns a sorted list of current configuration data. GetConfigList(all bool) []kernel.KeyDescrValue // GetNextConfigList returns a sorted list of next configuration data. GetNextConfigList() []kernel.KeyDescrValue // GetStatistics returns a key/value list of statistical data. 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) error } type serviceConfigDescription struct{ Key, Descr string } func (kern *myKernel) SetCreators( createAuthManager kernel.CreateAuthManagerFunc, createPlaceManager kernel.CreatePlaceManagerFunc, setupWebServer kernel.SetupWebServerFunc, ) { kern.auth.createManager = createAuthManager kern.place.createManager = createPlaceManager kern.web.setupServer = setupWebServer } |
Deleted kernel/impl/log.go.
|
| < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < |
Added kernel/impl/place.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 Detlef Stern // // This file is part of zettelstore. // // Zettelstore is licensed under the latest version of the EUPL (European Union // Public License). Please see file LICENSE.txt for your rights and obligations // under this license. //----------------------------------------------------------------------------- // Package impl provides the kernel implementation. package impl import ( "context" "fmt" "io" "net/url" "sync" "zettelstore.de/z/kernel" "zettelstore.de/z/place" ) type placeService struct { srvConfig mxService sync.RWMutex manager place.Manager createManager kernel.CreatePlaceManagerFunc } func (ps *placeService) Initialize() { ps.descr = descriptionMap{ kernel.PlaceDefaultDirType: { "Default directory place type", ps.noFrozen(func(val string) interface{} { switch val { case kernel.PlaceDirTypeNotify, kernel.PlaceDirTypeSimple: return val } return nil }), true, }, kernel.PlaceURIs: { "Place URI", func(val string) interface{} { uVal, err := url.Parse(val) if err != nil { return nil } if uVal.Scheme == "" { uVal.Scheme = "dir" } return uVal }, true, }, } ps.next = interfaceMap{ kernel.PlaceDefaultDirType: kernel.PlaceDirTypeNotify, } } func (ps *placeService) Start(kern *myKernel) error { placeURIs := make([]*url.URL, 0, 4) format := kernel.PlaceURIs + "%d" for i := 1; ; i++ { u := ps.GetNextConfig(fmt.Sprintf(format, i)) if u == nil { break } placeURIs = append(placeURIs, u.(*url.URL)) } ps.mxService.Lock() defer ps.mxService.Unlock() mgr, err := ps.createManager(placeURIs, kern.auth.manager, kern.cfg.rtConfig) if err != nil { kern.doLog("Unable to create place manager:", err) return err } kern.doLog("Start Place Manager:", mgr.Location()) if err := mgr.Start(context.Background()); err != nil { kern.doLog("Unable to start place manager:", err) } kern.cfg.setPlace(mgr) ps.manager = mgr return nil } func (ps *placeService) IsStarted() bool { ps.mxService.RLock() defer ps.mxService.RUnlock() return ps.manager != nil } func (ps *placeService) Stop(kern *myKernel) error { kern.doLog("Stop Place Manager") ps.mxService.RLock() mgr := ps.manager ps.mxService.RUnlock() err := mgr.Stop(context.Background()) ps.mxService.Lock() ps.manager = nil ps.mxService.Unlock() return err } func (ps *placeService) GetStatistics() []kernel.KeyValue { var st place.Stats ps.mxService.RLock() ps.manager.ReadStats(&st) ps.mxService.RUnlock() return []kernel.KeyValue{ {Key: "Read-only", Value: fmt.Sprintf("%v", st.ReadOnly)}, {Key: "Sub-places", Value: fmt.Sprintf("%v", st.NumManagedPlaces)}, {Key: "Zettel (total)", Value: fmt.Sprintf("%v", st.ZettelTotal)}, {Key: "Zettel (indexed)", Value: fmt.Sprintf("%v", st.ZettelIndexed)}, {Key: "Last re-index", Value: st.LastReload.Format("2006-01-02 15:04:05 -0700 MST")}, {Key: "Indexes since last re-index", Value: fmt.Sprintf("%v", st.IndexesSinceReload)}, {Key: "Duration last index", Value: fmt.Sprintf("%vms", st.DurLastIndex.Milliseconds())}, {Key: "Indexed words", Value: fmt.Sprintf("%v", st.IndexedWords)}, {Key: "Indexed URLs", Value: fmt.Sprintf("%v", st.IndexedUrls)}, {Key: "Zettel enrichments", Value: fmt.Sprintf("%v", st.IndexUpdates)}, } } func (ps *placeService) DumpIndex(w io.Writer) { ps.manager.Dump(w) } |
Changes to kernel/impl/server.go.
1 | //----------------------------------------------------------------------------- | | | < < < > | | | | | | | < | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 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 Detlef Stern // // This file is part of zettelstore. // // Zettelstore is licensed under 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 ( "bufio" "net" ) func startLineServer(kern *myKernel, listenAddr string) error { ln, err := net.Listen("tcp", listenAddr) if err != nil { kern.doLog("Unable to start Line Command Server:", err) return err } kern.doLog("Start Line Command Server:", listenAddr) go func() { lineServer(ln, kern) }() return nil } func lineServer(ln net.Listener, kern *myKernel) { // Something may panic. Ensure a running line service. defer func() { if r := recover(); r != nil { kern.doLogRecover("Line", r) go lineServer(ln, kern) } }() for { conn, err := ln.Accept() if err != nil { // handle error kern.doLog("Unable to accept connection:", err) 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) } }() cmds := cmdSession{} cmds.initialize(conn, kern) s := bufio.NewScanner(conn) for s.Scan() { line := s.Text() if !cmds.executeLine(line) { break } } conn.Close() } |
Changes to kernel/impl/web.go.
1 | //----------------------------------------------------------------------------- | | | < < < > < < < < < < < > > > > > > > | | > | < < < < < < < < < < < < < < < < < < < < < < < | < | | < > | < | | < | | | < < < | | | | | < < < < < < < < | < < < < < < < < < < < | | | | | | | < < < < < < < < < < < < < | | | > | | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 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 Detlef Stern // // This file is part of zettelstore. // // Zettelstore is licensed under 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 ( "net" "strconv" "sync" "time" "zettelstore.de/z/kernel" "zettelstore.de/z/web/server" "zettelstore.de/z/web/server/impl" ) type webService struct { srvConfig mxService sync.RWMutex srvw server.Server setupServer kernel.SetupWebServerFunc } // Constants for web service keys. const ( WebSecureCookie = "secure" WebListenAddress = "listen" WebPersistentCookie = "persistent" WebTokenLifetimeAPI = "api-lifetime" WebTokenLifetimeHTML = "html-lifetime" WebURLPrefix = "prefix" ) func (ws *webService) Initialize() { ws.descr = descriptionMap{ kernel.WebListenAddress: { "Listen address", func(val string) interface{} { host, port, err := net.SplitHostPort(val) if err != nil { return nil } if _, err := net.LookupPort("tcp", port); err != nil { return nil } return net.JoinHostPort(host, port) }, true}, kernel.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) interface{} { if val != "" && val[0] == '/' && val[len(val)-1] == '/' { return val } return nil }, true, }, } ws.next = interfaceMap{ kernel.WebListenAddress: "127.0.0.1:23123", 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) interface{} { if d, err := strconv.ParseUint(val, 10, 64); err == nil { secs := time.Duration(d) * time.Minute if secs < minDur { return minDur } if secs > maxDur { return maxDur } return secs } return defDur } } func (ws *webService) Start(kern *myKernel) error { listenAddr := ws.GetNextConfig(kernel.WebListenAddress).(string) urlPrefix := ws.GetNextConfig(kernel.WebURLPrefix).(string) persistentCookie := ws.GetNextConfig(kernel.WebPersistentCookie).(bool) secureCookie := ws.GetNextConfig(kernel.WebSecureCookie).(bool) srvw := impl.New(listenAddr, urlPrefix, persistentCookie, secureCookie, kern.auth.manager) err := kern.web.setupServer(srvw, kern.place.manager, kern.auth.manager, kern.cfg.rtConfig) if err != nil { kern.doLog("Unable to create Web Server:", err) return err } if kern.debug { srvw.SetDebug() } if err := srvw.Run(); err != nil { kern.doLog("Unable to start Web Service:", err) return err } kern.doLog("Start Web Service:", listenAddr) ws.mxService.Lock() ws.srvw = srvw ws.mxService.Unlock() return nil } func (ws *webService) IsStarted() bool { ws.mxService.RLock() defer ws.mxService.RUnlock() return ws.srvw != nil } func (ws *webService) Stop(kern *myKernel) error { kern.doLog("Stop Web Service") err := ws.srvw.Stop() ws.mxService.Lock() ws.srvw = nil ws.mxService.Unlock() return err } func (ws *webService) GetStatistics() []kernel.KeyValue { return nil } |
Changes to kernel/kernel.go.
1 | //----------------------------------------------------------------------------- | | | < < < < < | | | < < < < | > > > < < | < < < | < < < < < < < < < < < < < | < < < < < < < < < < < < | < < < < < < | < | | | | | < < < < < < < < < < < < < < | | | | | | < < < < < < < < < < < < < < < < < < < | | | | | | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 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) 2021 Detlef Stern // // This file is part of zettelstore. // // Zettelstore is licensed under the latest version of the EUPL (European Union // Public License). Please see file LICENSE.txt for your rights and obligations // under this license. //----------------------------------------------------------------------------- // Package kernel provides the main kernel service. package kernel import ( "io" "net/url" "zettelstore.de/z/auth" "zettelstore.de/z/config" "zettelstore.de/z/domain/id" "zettelstore.de/z/place" "zettelstore.de/z/web/server" ) // Kernel is the main internal service. type Kernel interface { // Start the service. Start(headline bool) // WaitForShutdown blocks the call until Shutdown is called. WaitForShutdown() // SetDebug to enable/disable debug mode SetDebug(enable bool) bool // Shutdown the service. Waits for all concurrent activities to stop. Shutdown(silent bool) // Log some activity. Log(args ...interface{}) // LogRecover outputs some information about the previous panic. LogRecover(name string, recoverInfo interface{}) bool // SetConfig stores a configuration value. SetConfig(srv Service, key, value string) bool // GetConfig returns a configuration value. GetConfig(srv Service, key string) interface{} // GetConfigList returns a sorted list of configuration data. GetConfigList(Service) []KeyDescrValue // 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, CreatePlaceManagerFunc, 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 // Service specifies a service, e.g. web, ... type Service uint8 // Constants for type Service. const ( _ Service = iota CoreService ConfigService AuthService PlaceService WebService ) // Constants for core service system keys. const ( CoreGoArch = "go-arch" CoreGoOS = "go-os" CoreGoVersion = "go-version" CoreHostname = "hostname" CorePort = "port" CoreProgname = "progname" CoreVerbose = "verbose" CoreVersion = "version" ) // Constants for authentication service keys. const ( AuthOwner = "owner" AuthReadonly = "readonly" ) // Constants for place service keys. const ( PlaceDefaultDirType = "defdirtype" PlaceURIs = "place-uri-" ) // Allowed values for PlaceDefaultDirType const ( PlaceDirTypeNotify = "notify" PlaceDirTypeSimple = "simple" ) // Constants for web service keys. const ( WebListenAddress = "listen" WebPersistentCookie = "persistent" 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 } // CreateAuthManagerFunc is called to create a new auth manager. type CreateAuthManagerFunc func(readonly bool, owner id.Zid) (auth.Manager, error) // CreatePlaceManagerFunc is called to create a new place manager. type CreatePlaceManagerFunc func( placeURIs []*url.URL, authManager auth.Manager, rtConfig config.Config, ) (place.Manager, error) // SetupWebServerFunc is called to create a new web service handler. type SetupWebServerFunc func( webServer server.Server, placeManager place.Manager, authManager auth.Manager, rtConfig config.Config, ) error |
Deleted logger/logger.go.
|
| < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < |
Deleted logger/logger_test.go.
|
| < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < |
Deleted logger/message.go.
|
| < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < |
Changes to parser/blob/blob.go.
1 | //----------------------------------------------------------------------------- | | | < < < | | | | | | < < < | | < < < < < < < < < | | < < < | | | | < < < | | > | < > > | | | | | > > | > | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 | //----------------------------------------------------------------------------- // Copyright (c) 2020 Detlef Stern // // This file is part of zettelstore. // // Zettelstore is licensed under the latest version of the EUPL (European Union // Public License). Please see file LICENSE.txt for your rights and obligations // under this license. //----------------------------------------------------------------------------- // Package 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: "gif", AltNames: nil, ParseBlocks: parseBlocks, ParseInlines: parseInlines, }) parser.Register(&parser.Info{ Name: "jpeg", AltNames: []string{"jpg"}, ParseBlocks: parseBlocks, ParseInlines: parseInlines, }) parser.Register(&parser.Info{ Name: "png", AltNames: nil, ParseBlocks: parseBlocks, ParseInlines: parseInlines, }) } func parseBlocks(inp *input.Input, m *meta.Meta, syntax string) ast.BlockSlice { if p := parser.Get(syntax); p != nil { syntax = p.Name } title, _ := m.Get(meta.KeyTitle) return ast.BlockSlice{ &ast.BLOBNode{ Title: title, Syntax: syntax, Blob: []byte(inp.Src), }, } } func parseInlines(inp *input.Input, syntax string) ast.InlineSlice { return ast.InlineSlice{} } |
Changes to parser/cleaner/cleaner.go.
1 | //----------------------------------------------------------------------------- | | | < < < | | | < | < < < < | | < < | | > < < < < < < < < < < < < < < < < | < | < < < < | | | < > > | | < | | > > | > > > | | | > | | < > > | | < < > > < < < < < | < < > > | > > | < < > > | < > > | | | < < | < | < < < | | | < < < < > < < < < | < < > > | > > | < | > | < > > | > > > | < < | | | < | | | < < < < < < < < | < | | < | | > > | > > | | < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 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 | //----------------------------------------------------------------------------- // Copyright (c) 2020-2021 Detlef Stern // // This file is part of zettelstore. // // Zettelstore is licensed under the latest version of the EUPL (European Union // Public License). Please see file LICENSE.txt for your rights and obligations // under this license. //----------------------------------------------------------------------------- // Package cleaner provides funxtions to clean up the parsed AST. package cleaner import ( "strconv" "strings" "zettelstore.de/z/ast" "zettelstore.de/z/encoder" "zettelstore.de/z/strfun" ) // CleanupBlockSlice cleans the given block slice. func CleanupBlockSlice(bs ast.BlockSlice) { cv := &cleanupVisitor{ textEnc: encoder.Create("text", nil), doMark: false, } t := ast.NewTopDownTraverser(cv) t.VisitBlockSlice(bs) if cv.hasMark { cv.doMark = true t.VisitBlockSlice(bs) } } type cleanupVisitor struct { textEnc encoder.Encoder ids map[string]ast.Node hasMark bool doMark bool } // VisitVerbatim does nothing. func (cv *cleanupVisitor) VisitVerbatim(vn *ast.VerbatimNode) {} // VisitRegion does nothing. func (cv *cleanupVisitor) VisitRegion(rn *ast.RegionNode) {} // VisitHeading calculates the heading slug. func (cv *cleanupVisitor) VisitHeading(hn *ast.HeadingNode) { if cv.doMark || hn == nil || hn.Inlines == nil { return } var sb strings.Builder _, err := cv.textEnc.WriteInlines(&sb, hn.Inlines) if err != nil { return } s := strfun.Slugify(sb.String()) if len(s) > 0 { hn.Slug = cv.addIdentifier(s, hn) } } // VisitHRule does nothing. func (cv *cleanupVisitor) VisitHRule(hn *ast.HRuleNode) {} // VisitList does nothing. func (cv *cleanupVisitor) VisitNestedList(ln *ast.NestedListNode) {} // VisitDescriptionList does nothing. func (cv *cleanupVisitor) VisitDescriptionList(dn *ast.DescriptionListNode) {} // VisitPara does nothing. func (cv *cleanupVisitor) VisitPara(pn *ast.ParaNode) {} // VisitTable does nothing. func (cv *cleanupVisitor) VisitTable(tn *ast.TableNode) {} // VisitBLOB does nothing. func (cv *cleanupVisitor) VisitBLOB(bn *ast.BLOBNode) {} // VisitText does nothing. func (cv *cleanupVisitor) VisitText(tn *ast.TextNode) {} // VisitTag does nothing. func (cv *cleanupVisitor) VisitTag(tn *ast.TagNode) {} // VisitSpace does nothing. func (cv *cleanupVisitor) VisitSpace(sn *ast.SpaceNode) {} // VisitBreak does nothing. func (cv *cleanupVisitor) VisitBreak(bn *ast.BreakNode) {} // VisitLink collects the given link as a reference. func (cv *cleanupVisitor) VisitLink(ln *ast.LinkNode) {} // VisitImage collects the image links as a reference. func (cv *cleanupVisitor) VisitImage(in *ast.ImageNode) {} // VisitCite does nothing. func (cv *cleanupVisitor) VisitCite(cn *ast.CiteNode) {} // VisitFootnote does nothing. func (cv *cleanupVisitor) VisitFootnote(fn *ast.FootnoteNode) {} // VisitMark checks for duplicate marks and changes them. func (cv *cleanupVisitor) VisitMark(mn *ast.MarkNode) { if mn == nil { return } if !cv.doMark { cv.hasMark = true return } if mn.Text == "" { mn.Text = cv.addIdentifier("*", mn) return } mn.Text = cv.addIdentifier(mn.Text, mn) } // VisitFormat does nothing. func (cv *cleanupVisitor) VisitFormat(fn *ast.FormatNode) {} // VisitLiteral does nothing. func (cv *cleanupVisitor) VisitLiteral(ln *ast.LiteralNode) {} func (cv *cleanupVisitor) addIdentifier(id string, node ast.Node) string { if cv.ids == nil { cv.ids = map[string]ast.Node{id: node} return id } if n, ok := cv.ids[id]; ok && n != node { prefix := id + "-" for count := 1; ; count++ { newID := prefix + strconv.Itoa(count) if n, ok := cv.ids[newID]; !ok || n == node { cv.ids[newID] = node return newID } } } cv.ids[id] = node return id } |
Deleted parser/draw/ORIG_CONTRIBUTORS.
|
| < < < |
Deleted parser/draw/ORIG_LICENSE.
|
| < < < < < < < < < < < < < < < < < < < < < < |
Deleted parser/draw/canvas.go.
|
| < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < |
Deleted parser/draw/canvas_test.go.
|
| < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < |
Deleted parser/draw/char.go.
|
| < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < |
Deleted parser/draw/draw.go.
|
| < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < |
Deleted parser/draw/draw_test.go.
|
| < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < |
Deleted parser/draw/object.go.
|
| < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < |
Deleted parser/draw/point.go.
|
| < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < |
Deleted parser/draw/svg.go.
|
| < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < |
Deleted parser/draw/svg_test.go.
|
| < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < |
Changes to parser/markdown/markdown.go.
1 | //----------------------------------------------------------------------------- | | | < < < < | | | | | | | < < < | | | | < | | | | | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 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-2021 Detlef Stern // // This file is part of zettelstore. // // Zettelstore is licensed under the latest version of the EUPL (European Union // Public License). Please see file LICENSE.txt for your rights and obligations // under this license. //----------------------------------------------------------------------------- // Package markdown provides a parser for markdown. package markdown import ( "bytes" "fmt" "strings" gm "github.com/yuin/goldmark" gmAst "github.com/yuin/goldmark/ast" gmText "github.com/yuin/goldmark/text" "zettelstore.de/z/ast" "zettelstore.de/z/domain/meta" "zettelstore.de/z/encoder" "zettelstore.de/z/input" "zettelstore.de/z/parser" "zettelstore.de/z/runes" ) func init() { parser.Register(&parser.Info{ Name: "markdown", AltNames: []string{"md"}, ParseBlocks: parseBlocks, ParseInlines: parseInlines, }) } func parseBlocks(inp *input.Input, m *meta.Meta, syntax string) ast.BlockSlice { p := parseMarkdown(inp) return p.acceptBlockSlice(p.docNode) } func parseInlines(inp *input.Input, syntax string) ast.InlineSlice { panic("markdown.parseInline not yet implemented") } func parseMarkdown(inp *input.Input) *mdP { source := []byte(inp.Src[inp.Pos:]) parser := gm.DefaultParser() node := parser.Parse(gmText.NewReader(source)) textEnc := encoder.Create("text", nil) return &mdP{source: source, docNode: node, textEnc: textEnc} } type mdP struct { source []byte docNode gmAst.Node textEnc encoder.Encoder } func (p *mdP) acceptBlockSlice(docNode gmAst.Node) ast.BlockSlice { if docNode.Type() != gmAst.TypeDocument { panic(fmt.Sprintf("Expected document, but got node type %v", docNode.Type())) } result := make(ast.BlockSlice, 0, docNode.ChildCount()) for child := docNode.FirstChild(); child != nil; child = child.NextSibling() { if block := p.acceptBlock(child); block != nil { result = append(result, block) |
︙ | ︙ | |||
89 90 91 92 93 94 95 | case *gmAst.Paragraph: return p.acceptParagraph(n) case *gmAst.TextBlock: return p.acceptTextBlock(n) case *gmAst.Heading: return p.acceptHeading(n) case *gmAst.ThematicBreak: | | | | > > | | | | | | | | | | | | | < | < < | | | | | | | | | > > | | < | | < < | | | | 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 | 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(n) 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 ins := p.acceptInlineSlice(node); len(ins) > 0 { return &ast.ParaNode{ Inlines: ins, } } return nil } func (p *mdP) acceptHeading(node *gmAst.Heading) *ast.HeadingNode { return &ast.HeadingNode{ Level: node.Level, Inlines: p.acceptInlineSlice(node), Attrs: nil, } } func (p *mdP) acceptThematicBreak(node *gmAst.ThematicBreak) *ast.HRuleNode { return &ast.HRuleNode{ Attrs: nil, //TODO } } func (p *mdP) acceptCodeBlock(node *gmAst.CodeBlock) *ast.VerbatimNode { return &ast.VerbatimNode{ Code: ast.VerbatimProg, Attrs: nil, //TODO Lines: p.acceptRawText(node), } } func (p *mdP) acceptFencedCodeBlock(node *gmAst.FencedCodeBlock) *ast.VerbatimNode { var attrs *ast.Attributes if language := node.Language(p.source); len(language) > 0 { attrs = attrs.Set("class", "language-"+cleanText(string(language), true)) } return &ast.VerbatimNode{ Code: ast.VerbatimProg, Attrs: attrs, Lines: p.acceptRawText(node), } } func (p *mdP) acceptRawText(node gmAst.Node) []string { lines := node.Lines() result := make([]string, 0, lines.Len()) for i := 0; i < lines.Len(); i++ { s := lines.At(i) line := s.Value(p.source) if l := len(line); l > 0 { if l > 1 && line[l-2] == '\r' && line[l-1] == '\n' { line = line[0 : l-2] } else if line[l-1] == '\n' || line[l-1] == '\r' { line = line[0 : l-1] } } result = append(result, string(line)) } return result } func (p *mdP) acceptBlockquote(node *gmAst.Blockquote) *ast.NestedListNode { return &ast.NestedListNode{ Code: ast.NestedListQuote, Items: []ast.ItemSlice{ p.acceptItemSlice(node), }, } } func (p *mdP) acceptList(node *gmAst.List) ast.ItemNode { code := ast.NestedListUnordered var attrs *ast.Attributes if node.IsOrdered() { code = ast.NestedListOrdered if node.Start != 1 { attrs = attrs.Set("start", fmt.Sprintf("%d", node.Start)) } } items := make([]ast.ItemSlice, 0, node.ChildCount()) for child := node.FirstChild(); child != nil; child = child.NextSibling() { item, ok := child.(*gmAst.ListItem) if !ok { panic(fmt.Sprintf("Expected list item node, but got %v", child.Kind())) } items = append(items, p.acceptItemSlice(item)) } return &ast.NestedListNode{ Code: code, Items: items, Attrs: attrs, } } 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 ins := p.acceptInlineSlice(node); len(ins) > 0 { return &ast.ParaNode{ Inlines: ins, } } return nil } func (p *mdP) acceptHTMLBlock(node *gmAst.HTMLBlock) *ast.VerbatimNode { lines := p.acceptRawText(node) if node.HasClosure() { closure := string(node.ClosureLine.Value(p.source)) if l := len(closure); l > 1 && closure[l-1] == '\n' { closure = closure[:l-1] } lines = append(lines, closure) } return &ast.VerbatimNode{ Code: ast.VerbatimHTML, Lines: lines, } } func (p *mdP) acceptInlineSlice(node gmAst.Node) ast.InlineSlice { result := make(ast.InlineSlice, 0, node.ChildCount()) for child := node.FirstChild(); child != nil; child = child.NextSibling() { if inlines := p.acceptInline(child); inlines != nil { result = append(result, inlines...) } } return result |
︙ | ︙ | |||
276 277 278 279 280 281 282 | 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 { | | | | | 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 | 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(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 ast.InlineSlice{} } result := make(ast.InlineSlice, 0, 1) state := 0 // 0=unknown,1=non-spaces,2=spaces lastPos := 0 for pos, ch := range text { if runes.IsSpace(ch) { if state == 1 { result = append(result, &ast.TextNode{Text: text[lastPos:pos]}) lastPos = pos } state = 2 } else { if state == 2 { |
︙ | ︙ | |||
323 324 325 326 327 328 329 | result = append(result, &ast.SpaceNode{Lexeme: text[lastPos:]}) default: panic(fmt.Sprintf("Unexpected state %v", state)) } return result } | | | > | | > | | | | | < | | | | | > > | | | | | | | | | | | | > > > | | | | | | | | | > | | | | | | | | | | > | > > > > | | | > > > > > | | > | | | | | | | | 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 | result = append(result, &ast.SpaceNode{Lexeme: text[lastPos:]}) default: panic(fmt.Sprintf("Unexpected state %v", state)) } return result } var ignoreAfterBS = map[byte]bool{ '!': true, '"': true, '#': true, '$': true, '%': true, '&': true, '\'': true, '(': true, ')': true, '*': true, '+': true, ',': true, '-': true, '.': true, '/': true, ':': true, ';': true, '<': true, '=': true, '>': true, '?': true, '@': true, '[': true, '\\': true, ']': true, '^': true, '_': true, '`': true, '{': true, '|': true, '}': true, '~': true, } // cleanText removes backslashes from TextNodes and expands entities func cleanText(text string, cleanBS bool) string { lastPos := 0 var sb strings.Builder for pos, ch := range text { if pos < lastPos { continue } if ch == '&' { inp := input.NewInput(text[pos:]) if s, ok := inp.ScanEntity(); ok { sb.WriteString(text[lastPos:pos]) sb.WriteString(s) lastPos = pos + inp.Pos } continue } if cleanBS && ch == '\\' && pos < len(text)-1 && ignoreAfterBS[text[pos+1]] { sb.WriteString(text[lastPos:pos]) sb.WriteByte(text[pos+1]) lastPos = pos + 2 } } if lastPos == 0 { return text } if lastPos < len(text) { sb.WriteString(text[lastPos:]) } return sb.String() } func (p *mdP) acceptCodeSpan(node *gmAst.CodeSpan) ast.InlineSlice { return ast.InlineSlice{ &ast.LiteralNode{ Code: ast.LiteralProg, Attrs: nil, //TODO Text: cleanCodeSpan(string(node.Text(p.source))), }, } } func cleanCodeSpan(text string) string { if text == "" { return "" } lastPos := 0 var sb strings.Builder for pos, ch := range text { if ch == '\n' { sb.WriteString(text[lastPos:pos]) if pos < len(text)-1 { sb.WriteByte(' ') } lastPos = pos + 1 } } if lastPos == 0 { return text } sb.WriteString(text[lastPos:]) return sb.String() } func (p *mdP) acceptEmphasis(node *gmAst.Emphasis) ast.InlineSlice { code := ast.FormatEmph if node.Level == 2 { code = ast.FormatStrong } return ast.InlineSlice{ &ast.FormatNode{ Code: code, Attrs: nil, //TODO Inlines: p.acceptInlineSlice(node), }, } } func (p *mdP) acceptLink(node *gmAst.Link) ast.InlineSlice { ref := ast.ParseReference(cleanText(string(node.Destination), true)) var attrs *ast.Attributes if title := string(node.Title); len(title) > 0 { attrs = attrs.Set("title", cleanText(title, true)) } return ast.InlineSlice{ &ast.LinkNode{ Ref: ref, Inlines: p.acceptInlineSlice(node), OnlyRef: false, Attrs: attrs, }, } } func (p *mdP) acceptImage(node *gmAst.Image) ast.InlineSlice { ref := ast.ParseReference(cleanText(string(node.Destination), true)) var attrs *ast.Attributes if title := string(node.Title); len(title) > 0 { attrs = attrs.Set("title", cleanText(title, true)) } return ast.InlineSlice{ &ast.ImageNode{ Ref: ref, Inlines: p.flattenInlineSlice(node), Attrs: attrs, }, } } func (p *mdP) flattenInlineSlice(node gmAst.Node) ast.InlineSlice { ins := p.acceptInlineSlice(node) var sb strings.Builder _, err := p.textEnc.WriteInlines(&sb, ins) if err != nil { panic(err) } text := sb.String() if text == "" { return nil } return ast.InlineSlice{ &ast.TextNode{ Text: text, }, } } func (p *mdP) acceptAutoLink(node *gmAst.AutoLink) ast.InlineSlice { url := node.URL(p.source) if node.AutoLinkType == gmAst.AutoLinkEmail && !bytes.HasPrefix(bytes.ToLower(url), []byte("mailto:")) { url = append([]byte("mailto:"), url...) } ref := ast.ParseReference(cleanText(string(url), false)) label := node.Label(p.source) if len(label) == 0 { label = url } return ast.InlineSlice{ &ast.LinkNode{ Ref: ref, Inlines: ast.InlineSlice{&ast.TextNode{Text: string(label)}}, OnlyRef: true, Attrs: nil, //TODO }, } } func (p *mdP) acceptRawHTML(node *gmAst.RawHTML) ast.InlineSlice { segs := make([]string, 0, node.Segments.Len()) for i := 0; i < node.Segments.Len(); i++ { segment := node.Segments.At(i) segs = append(segs, string(segment.Value(p.source))) } return ast.InlineSlice{ &ast.LiteralNode{ Code: ast.LiteralHTML, Attrs: nil, // TODO: add HTML as language Text: strings.Join(segs, ""), }, } } |
Changes to parser/markdown/markdown_test.go.
1 | //----------------------------------------------------------------------------- | | | < < < > < | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 | //----------------------------------------------------------------------------- // Copyright (c) 2020 Detlef Stern // // This file is part of zettelstore. // // Zettelstore is licensed under 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 ( "strings" "testing" "zettelstore.de/z/ast" ) func TestSplitText(t *testing.T) { var testcases = []struct { text string exp string }{ {"", ""}, {"abc", "Tabc"}, {" ", "S "}, |
︙ | ︙ |
Changes to parser/none/none.go.
1 | //----------------------------------------------------------------------------- | | | < < < | | | | | | | < < < | | > > > > > > > > | > > > > > > > > > > | > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > | | > > > > > > > > > > > > > > > | > | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 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-2021 Detlef Stern // // This file is part of zettelstore. // // Zettelstore is licensed under the latest version of the EUPL (European Union // Public License). Please see file LICENSE.txt for your rights and obligations // under this license. //----------------------------------------------------------------------------- // Package none provides a none-parser for meta data. 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.ValueSyntaxNone, AltNames: []string{}, ParseBlocks: parseBlocks, ParseInlines: parseInlines, }) } func parseBlocks(inp *input.Input, m *meta.Meta, syntax string) ast.BlockSlice { descrlist := &ast.DescriptionListNode{} for _, p := range m.Pairs(true) { descrlist.Descriptions = append( descrlist.Descriptions, getDescription(p.Key, p.Value)) } return ast.BlockSlice{descrlist} } func getDescription(key, value string) ast.Description { return ast.Description{ Term: ast.InlineSlice{&ast.TextNode{Text: key}}, Descriptions: []ast.DescriptionSlice{ ast.DescriptionSlice{ &ast.ParaNode{ Inlines: convertToInlineSlice(value, meta.Type(key)), }, }, }, } } func convertToInlineSlice(value string, dt *meta.DescriptionType) ast.InlineSlice { var sliceData []string if dt.IsSet { sliceData = meta.ListFromValue(value) if len(sliceData) == 0 { return ast.InlineSlice{} } } else { sliceData = []string{value} } var makeLink bool switch dt { case meta.TypeID, meta.TypeIDSet: makeLink = true } result := make(ast.InlineSlice, 0, 2*len(sliceData)-1) for i, val := range sliceData { if i > 0 { result = append(result, &ast.SpaceNode{Lexeme: " "}) } tn := &ast.TextNode{Text: val} if makeLink { result = append(result, &ast.LinkNode{ Ref: ast.ParseReference(val), Inlines: ast.InlineSlice{tn}, }) } else { result = append(result, tn) } } return result } func parseInlines(inp *input.Input, syntax string) ast.InlineSlice { inp.SkipToEOL() return ast.InlineSlice{ &ast.FormatNode{ Code: ast.FormatSpan, Attrs: &ast.Attributes{Attrs: map[string]string{"class": "warning"}}, Inlines: ast.InlineSlice{ &ast.TextNode{Text: "parser.meta.ParseInlines:"}, &ast.SpaceNode{Lexeme: " "}, &ast.TextNode{Text: "not"}, &ast.SpaceNode{Lexeme: " "}, &ast.TextNode{Text: "possible"}, &ast.SpaceNode{Lexeme: " "}, &ast.TextNode{Text: "("}, &ast.TextNode{Text: inp.Src[0:inp.Pos]}, &ast.TextNode{Text: ")"}, }, }, } } |
Changes to parser/parser.go.
1 | //----------------------------------------------------------------------------- | | | < < < < | < < < | | | > | | < < < | | | | | < < < < < < < < | | < | < < < < < < < < < < < < < < < < | | < | | < < < < < < < < < < < < < < < < < < < < < < < < < < | | < < < | | < < < < < | < | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 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-2021 Detlef Stern // // This file is part of zettelstore. // // Zettelstore is licensed under the latest version of the EUPL (European Union // Public License). Please see file LICENSE.txt for your rights and obligations // under this license. //----------------------------------------------------------------------------- // Package parser provides a generic interface to a range of different parsers. package parser import ( "log" "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 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) *Info { if _, ok := registry[pi.Name]; ok { log.Fatalf("Parser %q already registered", pi.Name) } registry[pi.Name] = pi for _, alt := range pi.AltNames { if _, ok := registry[alt]; ok { log.Fatalf("Parser %q already registered", alt) } registry[alt] = pi } return pi } // Get the parser (info) by name. If name not found, use a default parser. func Get(name string) *Info { if pi := registry[name]; pi != nil { return pi } if pi := registry["plain"]; pi != nil { return pi } log.Printf("No parser for %q found", name) panic("No default parser registered") } // ParseBlocks parses some input and returns a slice of block nodes. func ParseBlocks(inp *input.Input, m *meta.Meta, syntax string) ast.BlockSlice { bs := Get(syntax).ParseBlocks(inp, m, syntax) cleaner.CleanupBlockSlice(bs) return bs } // ParseInlines parses some input and returns a slice of inline nodes. func ParseInlines(inp *input.Input, syntax string) ast.InlineSlice { 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(title string) ast.InlineSlice { return ParseInlines(input.NewInput(title), meta.ValueSyntaxZmk) } // ParseZettel parses the zettel based on the syntax. func ParseZettel(zettel domain.Zettel, syntax string, rtConfig config.Config) *ast.ZettelNode { m := zettel.Meta inhMeta := rtConfig.AddDefaultValues(m) if syntax == "" { syntax, _ = inhMeta.Get(meta.KeySyntax) } parseMeta := inhMeta if syntax == meta.ValueSyntaxNone { parseMeta = m } return &ast.ZettelNode{ Meta: m, Content: zettel.Content, Zid: m.Zid, InhMeta: inhMeta, Ast: ParseBlocks(input.NewInput(zettel.Content.AsString()), parseMeta, syntax), } } |
Deleted parser/parser_test.go.
|
| < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < |
Changes to parser/plain/plain.go.
1 | //----------------------------------------------------------------------------- | | | < < < < < | | | | | | < < < | | | < < < < | | | < < < < | | | | > > > | | > | | > | | > > | < | < | | < > > | < | > > | < < < < | | > | | | | | < < < < < < < < < < < < < < < | | | | > > > > | > | | | > | | < < < < < < < < < < < < < < < < < < < < < < < < < < < | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 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-2021 Detlef Stern // // This file is part of zettelstore. // // Zettelstore is licensed under the latest version of the EUPL (European Union // Public License). Please see file LICENSE.txt for your rights and obligations // under this license. //----------------------------------------------------------------------------- // Package plain provides a parser for plain text data. package plain import ( "strings" "zettelstore.de/z/ast" "zettelstore.de/z/domain/meta" "zettelstore.de/z/input" "zettelstore.de/z/parser" "zettelstore.de/z/runes" ) func init() { parser.Register(&parser.Info{ Name: "txt", AltNames: []string{"plain", "text"}, ParseBlocks: parseBlocks, ParseInlines: parseInlines, }) parser.Register(&parser.Info{ Name: "css", ParseBlocks: parseBlocks, ParseInlines: parseInlines, }) parser.Register(&parser.Info{ Name: "svg", ParseBlocks: parseSVGBlocks, ParseInlines: parseSVGInlines, }) parser.Register(&parser.Info{ Name: "mustache", ParseBlocks: parseBlocks, ParseInlines: parseInlines, }) } func parseBlocks(inp *input.Input, m *meta.Meta, syntax string) ast.BlockSlice { return ast.BlockSlice{ &ast.VerbatimNode{ Code: ast.VerbatimProg, Attrs: &ast.Attributes{Attrs: map[string]string{"": syntax}}, Lines: readLines(inp), }, } } func readLines(inp *input.Input) (lines []string) { for { inp.EatEOL() posL := inp.Pos if inp.Ch == input.EOS { return lines } inp.SkipToEOL() lines = append(lines, inp.Src[posL:inp.Pos]) } } func parseInlines(inp *input.Input, syntax string) ast.InlineSlice { inp.SkipToEOL() return ast.InlineSlice{ &ast.LiteralNode{ Code: ast.LiteralProg, Attrs: &ast.Attributes{Attrs: map[string]string{"": syntax}}, Text: inp.Src[0:inp.Pos], }, } } func parseSVGBlocks(inp *input.Input, m *meta.Meta, syntax string) ast.BlockSlice { ins := parseSVGInlines(inp, syntax) if ins == nil { return nil } return ast.BlockSlice{ &ast.ParaNode{ Inlines: ins, }, } } func parseSVGInlines(inp *input.Input, syntax string) ast.InlineSlice { svgSrc := scanSVG(inp) if svgSrc == "" { return nil } return ast.InlineSlice{ &ast.ImageNode{ Blob: []byte(svgSrc), Syntax: syntax, }, } } func scanSVG(inp *input.Input) string { for runes.IsSpace(inp.Ch) { inp.Next() } svgSrc := inp.Src[inp.Pos:] if !strings.HasPrefix(svgSrc, "<svg ") { return "" } // TODO: check proper end </svg> return svgSrc } |
Changes to parser/zettelmark/block.go.
1 | //----------------------------------------------------------------------------- | | | < < < > | | | < | | | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 | //----------------------------------------------------------------------------- // Copyright (c) 2020-2021 Detlef Stern // // This file is part of zettelstore. // // Zettelstore is licensed under the latest version of the EUPL (European Union // Public License). Please see file LICENSE.txt for your rights and obligations // under this license. //----------------------------------------------------------------------------- // Package zettelmark provides a parser for zettelmarkup. 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 result := make(ast.BlockSlice, 0, 2) for inp.Ch != input.EOS { bn, cont := cp.parseBlock(lastPara) if bn != nil { result = append(result, bn) } if !cont { lastPara, _ = bn.(*ast.ParaNode) } } if cp.nestingLevel != 0 { panic("Nesting level was not decremented") } return result } // 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 { |
︙ | ︙ | |||
57 58 59 60 61 62 63 | return nil, false case '\n', '\r': inp.EatEOL() cp.cleanupListsAfterEOL() return nil, false case ':': bn, success = cp.parseColon() | | | 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 | 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() |
︙ | ︙ | |||
79 80 81 82 83 84 85 | bn, success = cp.parseNestedList() case ';': cp.lists = nil cp.table = nil bn, success = cp.parseDefTerm() case ' ': cp.table = nil | | | < < < < < | < < < < < < < < < < | 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 | bn, success = cp.parseNestedList() case ';': cp.lists = nil cp.table = nil bn, success = cp.parseDefTerm() case ' ': cp.table = nil bn, success = cp.parseIndent() case '|': cp.lists = nil cp.descrl = nil bn, success = cp.parseRow() } if success { return bn, false } } inp.SetPos(pos) cp.clearStacked() pn := cp.parsePara() if lastPara != nil { lastPara.Inlines = append(lastPara.Inlines, pn.Inlines...) return nil, true } return pn, false } 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 { |
︙ | ︙ | |||
142 143 144 145 146 147 148 | return cp.parseRegion() } return cp.parseDefDescr() } // parsePara parses paragraphed inline material. func (cp *zmkP) parsePara() *ast.ParaNode { | | | | | | | 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 | return cp.parseRegion() } return cp.parseDefDescr() } // parsePara parses paragraphed inline material. func (cp *zmkP) parsePara() *ast.ParaNode { pn := &ast.ParaNode{} for { in := cp.parseInline() if in == nil { return pn } pn.Inlines = append(pn.Inlines, 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 pn } } } } // countDelim read from input until a non-delimiter is found and returns number of delimiter chars. func (cp *zmkP) countDelim(delim rune) int { |
︙ | ︙ | |||
178 179 180 181 182 183 184 | 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 } | | | < < | | < < < < | < < < | | | | | < < < < < | 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 | 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.parseAttributes(true) inp.SkipToEOL() if inp.Ch == input.EOS { return nil, false } var code ast.VerbatimCode switch fch { case '`', runeModGrave: code = ast.VerbatimProg case '%': code = ast.VerbatimComment default: panic(fmt.Sprintf("%q is not a verbatim char", fch)) } rn = &ast.VerbatimNode{Code: code, Attrs: attrs} 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() rn.Lines = append(rn.Lines, inp.Src[posL:inp.Pos]) } } var runeRegion = map[rune]ast.RegionCode{ ':': 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 code, 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.parseAttributes(true) inp.SkipToEOL() if inp.Ch == input.EOS { return nil, false } rn = &ast.RegionNode{Code: code, Attrs: attrs} var lastPara *ast.ParaNode inp.EatEOL() for { posL := inp.Pos switch inp.Ch { case fch: if cp.countDelim(fch) >= cnt { |
︙ | ︙ | |||
287 288 289 290 291 292 293 294 295 296 297 298 | } 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 | > | | > > > < < < | < | | > | | | | | | | | < < < < | | | | | | | | | | 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 | } 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 lvl := cp.countDelim(inp.Ch) if lvl < 3 { return nil, false } if lvl > 7 { lvl = 7 } if inp.Ch != ' ' { return nil, false } inp.Next() cp.skipSpace() hn = &ast.HeadingNode{Level: lvl - 1} for { if input.IsEOLEOS(inp.Ch) { return hn, true } in := cp.parseInline() if in == nil { return hn, true } if inp.Ch == '{' { attrs := cp.parseAttributes(true) hn.Attrs = attrs inp.SkipToEOL() return hn, true } hn.Inlines = append(hn.Inlines, in) } } // 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.parseAttributes(true) inp.SkipToEOL() return &ast.HRuleNode{Attrs: attrs}, true } var mapRuneNestedList = map[rune]ast.NestedListCode{ '*': ast.NestedListUnordered, '#': ast.NestedListOrdered, '>': ast.NestedListQuote, } // parseNestedList parses a list. func (cp *zmkP) parseNestedList() (res ast.BlockNode, success bool) { inp := cp.inp codes := cp.parseNestedListCodes() if codes == nil { return nil, false } cp.skipSpace() if codes[len(codes)-1] != ast.NestedListQuote && input.IsEOLEOS(inp.Ch) { return nil, false } if len(codes) < len(cp.lists) { cp.lists = cp.lists[:len(codes)] } ln, newLnCount := cp.buildNestedList(codes) ln.Items = append(ln.Items, ast.ItemSlice{cp.parseLinePara()}) return cp.cleanupParsedNestedList(newLnCount) } func (cp *zmkP) parseNestedListCodes() []ast.NestedListCode { inp := cp.inp codes := make([]ast.NestedListCode, 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(codes []ast.NestedListCode) (ln *ast.NestedListNode, newLnCount int) { for i, code := range codes { if i < len(cp.lists) { if cp.lists[i].Code != code { ln = &ast.NestedListNode{Code: code} newLnCount++ cp.lists[i] = ln cp.lists = cp.lists[:i+1] } else { ln = cp.lists[i] } } else { ln = &ast.NestedListNode{Code: code} 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 |
︙ | ︙ | |||
446 447 448 449 450 451 452 | defPos := len(descrl.Descriptions) - 1 if defPos == 0 { res = descrl } for { in := cp.parseInline() if in == nil { | | | 411 412 413 414 415 416 417 418 419 420 421 422 423 424 425 | defPos := len(descrl.Descriptions) - 1 if defPos == 0 { res = descrl } for { in := cp.parseInline() if in == nil { if descrl.Descriptions[defPos].Term == nil { 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 |
︙ | ︙ | |||
472 473 474 475 476 477 478 | inp.Next() cp.skipSpace() descrl := cp.descrl if descrl == nil || len(descrl.Descriptions) == 0 { return nil, false } defPos := len(descrl.Descriptions) - 1 | | | | | | < < < | 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 | inp.Next() cp.skipSpace() descrl := cp.descrl if descrl == nil || len(descrl.Descriptions) == 0 { return nil, false } defPos := len(descrl.Descriptions) - 1 if descrl.Descriptions[defPos].Term == nil { 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() (res ast.BlockNode, success bool) { inp := cp.inp cnt := 0 for { inp.Next() if inp.Ch != ' ' { break } cnt++ } if cp.lists != nil { return nil, cp.parseIndentForList(cnt) } if cp.descrl != nil { return nil, cp.parseIndentForDescription(cnt) } return nil, 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() 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 |
︙ | ︙ | |||
556 557 558 559 560 561 562 | 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 { | | | | | | | | | | | | | | | | < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < | 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 | 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 { pn := &ast.ParaNode{} for { in := cp.parseInline() if in == nil { if pn.Inlines == nil { return nil } return pn } pn.Inlines = append(pn.Inlines, in) if _, ok := in.(*ast.BreakNode); ok { return pn } } } // parseRow parse one table row. func (cp *zmkP) parseRow() (res ast.BlockNode, success bool) { inp := cp.inp if inp.Peek() == '%' { inp.SkipToEOL() return nil, true } 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, true } cp.table.Rows = append(cp.table.Rows, row) return nil, true } // inp.Ch must be '|' } } // parseCell parses one single cell of a table row. func (cp *zmkP) parseCell() *ast.TableCell { inp := cp.inp var slice ast.InlineSlice for { if input.IsEOLEOS(inp.Ch) { if len(slice) == 0 { return nil } return &ast.TableCell{Inlines: slice} } if inp.Ch == '|' { return &ast.TableCell{Inlines: slice} } slice = append(slice, cp.parseInline()) } } |
Changes to parser/zettelmark/inline.go.
1 | //----------------------------------------------------------------------------- | | | < < < > < < < | | > | | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 | //----------------------------------------------------------------------------- // Copyright (c) 2020-2021 Detlef Stern // // This file is part of zettelstore. // // Zettelstore is licensed under the latest version of the EUPL (European Union // Public License). Please see file LICENSE.txt for your rights and obligations // under this license. //----------------------------------------------------------------------------- // Package zettelmark provides a parser for zettelmarkup. package zettelmark import ( "fmt" "strings" "zettelstore.de/z/ast" "zettelstore.de/z/input" ) // parseInlineSlice parses a sequence of Inlines until EOS. func (cp *zmkP) parseInlineSlice() ast.InlineSlice { inp := cp.inp var ins ast.InlineSlice for inp.Ch != input.EOS { in := cp.parseInline() if in == nil { return ins } ins = append(ins, in) } return ins } func (cp *zmkP) parseInline() ast.InlineNode { |
︙ | ︙ | |||
64 65 66 67 68 69 70 | in, success = cp.parseFootnote() case '!': in, success = cp.parseMark() } case '{': inp.Next() if inp.Ch == '{' { | | > > | | < < < | | | > > > > > | 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 | in, success = cp.parseFootnote() case '!': in, success = cp.parseMark() } case '{': inp.Next() if inp.Ch == '{' { in, success = cp.parseImage() } case '#': return cp.parseTag() case '%': in, success = cp.parseComment() case '/', '*', '_', '~', '\'', '^', ',', '<', '"', ';', ':': in, success = cp.parseFormat() case '+', '`', '=', runeModGrave: in, success = cp.parseLiteral() 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 == '\\' { return cp.parseTextBackslash() } 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: inp.Src[pos:inp.Pos]} } } } func (cp *zmkP) parseTextBackslash() *ast.TextNode { cp.inp.Next() return cp.parseBackslashRest() } func (cp *zmkP) parseBackslash() ast.InlineNode { inp := cp.inp inp.Next() switch inp.Ch { case '\n', '\r': inp.EatEOL() |
︙ | ︙ | |||
130 131 132 133 134 135 136 | } if inp.Ch == ' ' { inp.Next() return &ast.TextNode{Text: "\u00a0"} } pos := inp.Pos inp.Next() | | | | | > > > > > > | | > < < < < | < < < < < | | | | | | | | | > | | < < | < < < | | | | | < | < | | < < < | < < < < < < < < < < < < < < < < < < < < < < < < | | < | 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 | } if inp.Ch == ' ' { inp.Next() return &ast.TextNode{Text: "\u00a0"} } pos := inp.Pos inp.Next() return &ast.TextNode{Text: 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: 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, ins, ok := cp.parseReference(']'); ok { attrs := cp.parseAttributes(false) if len(ref) > 0 { onlyRef := false r := ast.ParseReference(ref) if ins == nil { ins = ast.InlineSlice{&ast.TextNode{Text: ref}} onlyRef = true } return &ast.LinkNode{ Ref: r, Inlines: ins, OnlyRef: onlyRef, Attrs: attrs, }, true } } return nil, false } func (cp *zmkP) parseReference(closeCh rune) (ref string, ins ast.InlineSlice, ok bool) { inp := cp.inp inp.Next() cp.skipSpace() pos := inp.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 } sepPos := inp.Pos inp.SetPos(pos) for inp.Pos < sepPos { ins = append(ins, cp.parseInline()) } inp.Next() pos = inp.Pos } else if hasSpace { return "", nil, false } inp.SetPos(pos) cp.skipSpace() pos = inp.Pos if !cp.readReferenceToClose(closeCh) { return "", nil, false } ref = inp.Src[pos:inp.Pos] inp.Next() if inp.Ch != closeCh { return "", nil, false } inp.Next() return ref, ins, 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 closeCh: inp.Next() if inp.Ch == closeCh { return hasSpace, true } continue } inp.Next() } } func (cp *zmkP) readReferenceToClose(closeCh rune) bool { inp := cp.inp for { switch inp.Ch { case input.EOS, '\n', '\r', ' ': return false case closeCh: return true } inp.Next() } } |
︙ | ︙ | |||
309 310 311 312 313 314 315 | case ' ', ',', '|': inp.Next() } ins, ok := cp.parseLinkLikeRest() if !ok { return nil, false } | | | | | < < < | | | < < | < < | | < < | < < < | | | > > > | > > > > | > | < | < < < < | | | | > | | | > | | | | | | | | < | < | > | | > > | | < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < | 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 | case ' ', ',', '|': inp.Next() } ins, ok := cp.parseLinkLikeRest() if !ok { return nil, false } attrs := cp.parseAttributes(false) return &ast.CiteNode{Key: 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.parseAttributes(false) return &ast.FootnoteNode{Inlines: ins, Attrs: attrs}, true } func (cp *zmkP) parseLinkLikeRest() (ast.InlineSlice, bool) { cp.skipSpace() var 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() return ins, true } func (cp *zmkP) parseImage() (ast.InlineNode, bool) { if ref, ins, ok := cp.parseReference('}'); ok { attrs := cp.parseAttributes(false) if len(ref) > 0 { r := ast.ParseReference(ref) return &ast.ImageNode{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 != ']' { if !isNameRune(inp.Ch) { return nil, false } inp.Next() } mn := &ast.MarkNode{Text: inp.Src[pos:inp.Pos]} inp.Next() return mn, true } func (cp *zmkP) parseTag() ast.InlineNode { inp := cp.inp posH := inp.Pos inp.Next() pos := inp.Pos for isNameRune(inp.Ch) { inp.Next() } if pos == inp.Pos || inp.Ch == '#' { return &ast.TextNode{Text: inp.Src[posH:inp.Pos]} } return &ast.TagNode{Tag: inp.Src[pos:inp.Pos]} } 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() } cp.skipSpace() pos := inp.Pos for { if input.IsEOLEOS(inp.Ch) { return &ast.LiteralNode{Code: ast.LiteralComment, Text: inp.Src[pos:inp.Pos]}, true } inp.Next() } } var mapRuneFormat = map[rune]ast.FormatCode{ '/': ast.FormatItalic, '*': ast.FormatBold, '_': ast.FormatUnder, '~': ast.FormatStrike, '\'': ast.FormatMonospace, '^': ast.FormatSuper, ',': ast.FormatSub, '<': ast.FormatQuotation, '"': ast.FormatQuote, ';': ast.FormatSmall, ':': ast.FormatSpan, } func (cp *zmkP) parseFormat() (res ast.InlineNode, success bool) { inp := cp.inp fch := inp.Ch code, 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{Code: code} for { if inp.Ch == input.EOS { return nil, false } if inp.Ch == fch { inp.Next() if inp.Ch == fch { inp.Next() fn.Attrs = cp.parseAttributes(false) 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.LiteralCode{ '`': ast.LiteralProg, runeModGrave: ast.LiteralProg, '+': ast.LiteralKeyb, '=': ast.LiteralOutput, } func (cp *zmkP) parseLiteral() (res ast.InlineNode, success bool) { inp := cp.inp fch := inp.Ch code, 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 } fn := &ast.LiteralNode{Code: code} inp.Next() var sb strings.Builder for { if inp.Ch == input.EOS { return nil, false } if inp.Ch == fch { if inp.Peek() == fch { inp.Next() inp.Next() fn.Attrs = cp.parseAttributes(false) fn.Text = sb.String() return fn, true } sb.WriteRune(fch) inp.Next() } else { tn := cp.parseText() sb.WriteString(tn.Text) } } } func (cp *zmkP) parseNdash() (res *ast.TextNode, success bool) { inp := cp.inp if inp.Peek() != inp.Ch { return nil, false |
︙ | ︙ |
Changes to parser/zettelmark/node.go.
1 | //----------------------------------------------------------------------------- | | | < < < > > | > > > > > > > | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 | //----------------------------------------------------------------------------- // Copyright (c) 2020-2021 Detlef Stern // // This file is part of zettelstore. // // Zettelstore is licensed under the latest version of the EUPL (European Union // Public License). Please see file LICENSE.txt for your rights and obligations // under this license. //----------------------------------------------------------------------------- // Package zettelmark provides a parser for zettelmarkup. 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 } // Accept a visitor and visit the node. func (nn *nullItemNode) Accept(v ast.Visitor) {} // nullDescriptionNode specifies a removable placeholder. type nullDescriptionNode struct { ast.DescriptionNode } // Accept a visitor and visit the node. func (nn *nullDescriptionNode) Accept(v ast.Visitor) {} |
Changes to parser/zettelmark/post-processor.go.
1 | //----------------------------------------------------------------------------- | | | < < < > | | | | > | | < < < | < < < < < < < < < < < < < < < < < < < < < < < < < > > > > | | | > | < | | > > > > | < < | < < < < < | < < < < < < | | < < < < < | < < < < < | | < < | < < < | | < | < > | | | | | | | | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 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-2021 Detlef Stern // // This file is part of zettelstore. // // Zettelstore is licensed under the latest version of the EUPL (European Union // Public License). Please see file LICENSE.txt for your rights and obligations // under this license. //----------------------------------------------------------------------------- // Package zettelmark provides a parser for zettelmarkup. 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) ast.BlockSlice { pp := postProcessor{} return pp.processBlockSlice(bs) } // postProcessInlines is the entry point for post-processing a list of inline nodes. func postProcessInlines(is ast.InlineSlice) ast.InlineSlice { pp := postProcessor{} return pp.processInlineSlice(is) } // postProcessor is a visitor that cleans the abstract syntax tree. type postProcessor struct { inVerse bool } // VisitPara post-processes a paragraph. func (pp *postProcessor) VisitPara(pn *ast.ParaNode) { if pn != nil { pn.Inlines = pp.processInlineSlice(pn.Inlines) } } // VisitVerbatim does nothing, no post-processing needed. func (pp *postProcessor) VisitVerbatim(vn *ast.VerbatimNode) {} // VisitRegion post-processes a region. func (pp *postProcessor) VisitRegion(rn *ast.RegionNode) { oldVerse := pp.inVerse if rn.Code == ast.RegionVerse { pp.inVerse = true } rn.Blocks = pp.processBlockSlice(rn.Blocks) pp.inVerse = oldVerse rn.Inlines = pp.processInlineSlice(rn.Inlines) } // VisitHeading post-processes a heading. func (pp *postProcessor) VisitHeading(hn *ast.HeadingNode) { hn.Inlines = pp.processInlineSlice(hn.Inlines) } // VisitHRule does nothing, no post-processing needed. func (pp *postProcessor) VisitHRule(hn *ast.HRuleNode) {} // VisitList post-processes a list. func (pp *postProcessor) VisitNestedList(ln *ast.NestedListNode) { for i, item := range ln.Items { ln.Items[i] = pp.processItemSlice(item) } } // VisitDescriptionList post-processes a description list. func (pp *postProcessor) VisitDescriptionList(dn *ast.DescriptionListNode) { for i, def := range dn.Descriptions { dn.Descriptions[i].Term = pp.processInlineSlice(def.Term) for j, b := range def.Descriptions { dn.Descriptions[i].Descriptions[j] = pp.processDescriptionSlice(b) } } } // VisitTable post-processes a table. 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 (pp *postProcessor) visitTableHeader(tn *ast.TableNode) { for pos, cell := range tn.Header { inlines := cell.Inlines if len(inlines) == 0 { continue } if textNode, ok := inlines[0].(*ast.TextNode); ok { textNode.Text = strings.TrimPrefix(textNode.Text, "=") } if textNode, ok := inlines[len(inlines)-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] } } } |
︙ | ︙ | |||
190 191 192 193 194 195 196 | } } return width } func appendCells(row ast.TableRow, width int, colAlign []ast.Alignment) ast.TableRow { for len(row) < width { | | < < < | | | | 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 | } } 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)]}) } return row } func isHeaderRow(row ast.TableRow) bool { for i := 0; i < len(row); i++ { if inlines := row[i].Inlines; len(inlines) > 0 { if textNode, ok := inlines[0].(*ast.TextNode); ok { if strings.HasPrefix(textNode.Text, "=") { return true } } } } return false |
︙ | ︙ | |||
226 227 228 229 230 231 232 | default: return ast.AlignDefault } } // processCell tries to recognize cell formatting. func (pp *postProcessor) processCell(cell *ast.TableCell, colAlign ast.Alignment) { | | > > > | | | > > | > > | > > | > > | > > | > > > > | > > > > > | > > > > | > > > | > > | | > > > > | | | > > > > > | > > > > | > > | > > > | | | | | | | | < < < | | 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 | default: return ast.AlignDefault } } // processCell tries to recognize cell formatting. func (pp *postProcessor) processCell(cell *ast.TableCell, colAlign ast.Alignment) { if len(cell.Inlines) == 0 { return } if textNode, ok := cell.Inlines[0].(*ast.TextNode); ok && len(textNode.Text) > 0 { align := getAlignment(textNode.Text[0]) if align == ast.AlignDefault { cell.Align = colAlign } else { textNode.Text = textNode.Text[1:] cell.Align = align } } else { cell.Align = colAlign } cell.Inlines = pp.processInlineSlice(cell.Inlines) } // VisitBLOB does nothing. func (pp *postProcessor) VisitBLOB(bn *ast.BLOBNode) {} // VisitText does nothing. func (pp *postProcessor) VisitText(tn *ast.TextNode) {} // VisitTag does nothing. func (pp *postProcessor) VisitTag(tn *ast.TagNode) {} // VisitSpace does nothing. func (pp *postProcessor) VisitSpace(sn *ast.SpaceNode) {} // VisitBreak does nothing. func (pp *postProcessor) VisitBreak(bn *ast.BreakNode) {} // VisitLink post-processes a link. func (pp *postProcessor) VisitLink(ln *ast.LinkNode) { ln.Inlines = pp.processInlineSlice(ln.Inlines) } // VisitImage post-processes an image. func (pp *postProcessor) VisitImage(in *ast.ImageNode) { if len(in.Inlines) > 0 { in.Inlines = pp.processInlineSlice(in.Inlines) } } // VisitCite post-processes a citation. func (pp *postProcessor) VisitCite(cn *ast.CiteNode) { cn.Inlines = pp.processInlineSlice(cn.Inlines) } // VisitFootnote post-processes a footnote. func (pp *postProcessor) VisitFootnote(fn *ast.FootnoteNode) { fn.Inlines = pp.processInlineSlice(fn.Inlines) } // VisitMark post-processes a mark. func (pp *postProcessor) VisitMark(mn *ast.MarkNode) {} var mapSemantic = map[ast.FormatCode]ast.FormatCode{ ast.FormatItalic: ast.FormatEmph, ast.FormatBold: ast.FormatStrong, ast.FormatUnder: ast.FormatInsert, ast.FormatStrike: ast.FormatDelete, } // VisitFormat post-processes formatted inline nodes. func (pp *postProcessor) VisitFormat(fn *ast.FormatNode) { if fn.Attrs != nil && fn.Attrs.HasDefault() { if newCode, ok := mapSemantic[fn.Code]; ok { fn.Attrs.RemoveDefault() fn.Code = newCode } } fn.Inlines = pp.processInlineSlice(fn.Inlines) } // VisitLiteral post-processes an inline literal. func (pp *postProcessor) VisitLiteral(cn *ast.LiteralNode) {} // processBlockSlice post-processes a slice of blocks. // It is one of the working horses for post-processing. func (pp *postProcessor) processBlockSlice(bns ast.BlockSlice) ast.BlockSlice { for _, bn := range bns { bn.Accept(pp) } fromPos, toPos := 0, 0 for fromPos < len(bns) { bns[toPos] = bns[fromPos] fromPos++ switch bn := bns[toPos].(type) { case *ast.ParaNode: if len(bn.Inlines) > 0 { toPos++ } case *nullItemNode: case *nullDescriptionNode: default: toPos++ } } for pos := toPos; pos < len(bns); pos++ { bns[pos] = nil // Allow excess nodes to be garbage collected. } return bns[: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 { for _, in := range ins { in.Accept(pp) } fromPos, toPos := 0, 0 for fromPos < len(ins) { ins[toPos] = ins[fromPos] fromPos++ switch in := ins[toPos].(type) { case *ast.ParaNode: |
︙ | ︙ | |||
315 316 317 318 319 320 321 | } 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 { | < < < | | 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 | } 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 { for _, dn := range dns { dn.Accept(pp) } fromPos, toPos := 0, 0 for fromPos < len(dns) { dns[toPos] = dns[fromPos] fromPos++ switch dn := dns[toPos].(type) { case *ast.ParaNode: |
︙ | ︙ | |||
341 342 343 344 345 346 347 | } for pos := toPos; pos < len(dns); pos++ { dns[pos] = nil // Allow excess nodes to be garbage collected. } return dns[:toPos:toPos] } | > > | < < < | < | | | > | > | | | > > | < | | < < < < < | < | | | < > | | | | > | | > | > > | > | > > > > | > > > > > > > > > > > > > | | > > | < > > | < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < | < > > > > > > > > > > > > | 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 | } for pos := toPos; pos < len(dns); pos++ { dns[pos] = nil // Allow excess nodes to be garbage collected. } return dns[:toPos:toPos] } // processInlineSlice post-processes a slice of inline nodes. // It is one of the working horses for post-processing. func (pp *postProcessor) processInlineSlice(ins ast.InlineSlice) ast.InlineSlice { if len(ins) == 0 { return nil } for _, in := range ins { in.Accept(pp) } if !pp.inVerse { ins = processInlineSliceHead(ins) } toPos := pp.processInlineSliceCopy(ins) toPos = pp.processInlineSliceTail(ins, toPos) ins = ins[:toPos:toPos] pp.processInlineSliceInplace(ins) return ins } // processInlineSliceHead removes leading spaces and empty text. func processInlineSliceHead(ins ast.InlineSlice) ast.InlineSlice { for i := 0; i < len(ins); i++ { switch in := ins[i].(type) { case *ast.SpaceNode: case *ast.TextNode: if len(in.Text) > 0 { return ins[i:] } default: return ins[i:] } } return 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(ins ast.InlineSlice) int { maxPos := len(ins) for { again, toPos := pp.processInlineSliceCopyLoop(ins, maxPos) for pos := toPos; pos < maxPos; pos++ { ins[pos] = nil // Allow excess nodes to be garbage collected. } if !again { return toPos } maxPos = toPos } } func (pp *postProcessor) processInlineSliceCopyLoop( ins ast.InlineSlice, maxPos int) (bool, int) { again := false fromPos, toPos := 0, 0 for fromPos < maxPos { ins[toPos] = ins[fromPos] fromPos++ switch in := ins[toPos].(type) { case *ast.TextNode: for fromPos < maxPos { if tn, ok := ins[fromPos].(*ast.TextNode); ok { in.Text = in.Text + tn.Text fromPos++ } else { break } } case *ast.SpaceNode: if fromPos < maxPos { switch nn := ins[fromPos].(type) { case *ast.BreakNode: if len(in.Lexeme) > 1 { nn.Hard = true ins[toPos] = nn fromPos++ } case *ast.TextNode: if pp.inVerse { ins[toPos] = &ast.TextNode{Text: strings.Repeat("\u00a0", len(in.Lexeme)) + nn.Text} fromPos++ again = true } } } case *ast.BreakNode: if pp.inVerse { in.Hard = true } } toPos++ } return again, toPos } // processInlineSliceTail removes empty text nodes, breaks and spaces at the end. func (pp *postProcessor) processInlineSliceTail(ins ast.InlineSlice, toPos int) int { 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 } func (pp *postProcessor) processInlineSliceInplace(ins ast.InlineSlice) { for _, in := range ins { if n, ok := in.(*ast.TextNode); ok { if n.Text == "..." { n.Text = "\u2026" } else if len(n.Text) == 4 && strings.IndexByte(",;:!?", n.Text[3]) >= 0 && n.Text[:3] == "..." { n.Text = "\u2026" + n.Text[3:] } } } } |
Changes to parser/zettelmark/zettelmark.go.
1 | //----------------------------------------------------------------------------- | | | < < < < | < | | | | | < < < | | | | < | | < | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 | //----------------------------------------------------------------------------- // Copyright (c) 2020-2021 Detlef Stern // // This file is part of zettelstore. // // Zettelstore is licensed under the latest version of the EUPL (European Union // Public License). Please see file LICENSE.txt for your rights and obligations // under this license. //----------------------------------------------------------------------------- // Package zettelmark provides a parser for zettelmarkup. package zettelmark import ( "unicode" "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.ValueSyntaxZmk, AltNames: nil, ParseBlocks: parseBlocks, ParseInlines: parseInlines, }) } func parseBlocks(inp *input.Input, m *meta.Meta, syntax string) ast.BlockSlice { parser := &zmkP{inp: inp} bs := parser.parseBlockSlice() return postProcessBlocks(bs) } func parseInlines(inp *input.Input, syntax string) ast.InlineSlice { parser := &zmkP{inp: inp} is := parser.parseInlineSlice() return postProcessInlines(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 |
︙ | ︙ | |||
70 71 72 73 74 75 76 | // clearStacked removes all multi-line nodes from parser. func (cp *zmkP) clearStacked() { cp.lists = nil cp.table = nil cp.descrl = nil } | | > > > | | > | | > > > > > | | | | > > > > > > | > > > > > > | > | | | | | | | | | < | < < | > < | | | | | | | | | | > > > | > > > > | 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 | // 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, sameLine bool) 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 } if sameLine && input.IsEOLEOS(inp.Ch) { return false } return cp.parseAttributeValue(key, attrs, sameLine) } func (cp *zmkP) parseAttributeValue( key string, attrs map[string]string, sameLine bool) bool { inp := cp.inp inp.Next() if inp.Ch == '"' { return cp.parseQuotedAttributeValue(key, attrs, sameLine) } posV := inp.Pos for { switch inp.Ch { case input.EOS: return false case '\n', '\r': if sameLine { return false } fallthrough case ' ', '}': updateAttrs(attrs, key, inp.Src[posV:inp.Pos]) return true } inp.Next() } } func (cp *zmkP) parseQuotedAttributeValue(key string, attrs map[string]string, sameLine bool) bool { inp := cp.inp inp.Next() var val string for { switch inp.Ch { case input.EOS: return false case '"': updateAttrs(attrs, key, val) inp.Next() return true case '\n', '\r': if sameLine { return false } inp.EatEOL() val += " " case '\\': inp.Next() switch inp.Ch { case input.EOS, '\n', '\r': return false } fallthrough default: val += string(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 } } // parseAttributes reads optional attributes. // If sameLine is True, it is called from block nodes. In this case, a single // name is allowed. It will parse as {name}. Attributes are not allowed to be // continued on next line. // If sameLine is False, it is called from inline nodes. In this case, the next // rune must be '{'. A continuation on next lines is allowed. func (cp *zmkP) parseAttributes(sameLine bool) *ast.Attributes { inp := cp.inp if sameLine { pos := inp.Pos for isNameRune(inp.Ch) { inp.Next() } if pos < inp.Pos { return &ast.Attributes{Attrs: map[string]string{"": inp.Src[pos:inp.Pos]}} } // No immediate name: skip spaces cp.skipSpace() } pos := inp.Pos attrs, success := cp.doParseAttributes(sameLine) if sameLine || success { return attrs } inp.SetPos(pos) return nil } func (cp *zmkP) doParseAttributes(sameLine bool) (res *ast.Attributes, success bool) { inp := cp.inp if inp.Ch != '{' { return nil, false } inp.Next() attrs := map[string]string{} if !cp.parseAttributeValues(sameLine, attrs) { return nil, false } inp.Next() return &ast.Attributes{Attrs: attrs}, true } func (cp *zmkP) parseAttributeValues(sameLine bool, attrs map[string]string) bool { inp := cp.inp for { cp.skipSpaceLine(sameLine) 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(attrs, "class", inp.Src[posC:inp.Pos]) case '=': delete(attrs, "") if !cp.parseAttributeValue("", attrs, sameLine) { return false } default: if !cp.parseNormalAttribute(attrs, sameLine) { return false } } switch inp.Ch { case '}': return true case '\n', '\r': if sameLine { return false } case ' ', ',': inp.Next() default: return false } } } func (cp *zmkP) skipSpaceLine(sameLine bool) { if sameLine { cp.skipSpace() return } for inp := cp.inp; ; { switch inp.Ch { case ' ': inp.Next() case '\n', '\r': inp.EatEOL() default: |
︙ | ︙ |
Deleted parser/zettelmark/zettelmark_fuzz_test.go.
|
| < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < |
Changes to parser/zettelmark/zettelmark_test.go.
1 | //----------------------------------------------------------------------------- | | | < < < > < < | | | > > > > | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 | //----------------------------------------------------------------------------- // Copyright (c) 2020-2021 Detlef Stern // // This file is part of zettelstore. // // Zettelstore is licensed under the latest version of the EUPL (European Union // Public License). Please see file LICENSE.txt for your rights and obligations // under this license. //----------------------------------------------------------------------------- // Package zettelmark_test provides some tests for the zettelmarkup parser. package zettelmark_test import ( "fmt" "sort" "strings" "testing" "zettelstore.de/z/ast" "zettelstore.de/z/domain/meta" "zettelstore.de/z/input" "zettelstore.de/z/parser" // Ensure that the text encoder is available. // Needed by parser/cleanup.go _ "zettelstore.de/z/encoder/textenc" ) type TestCase struct{ source, want string } type TestCases []TestCase func replace(s string, tcs TestCases) TestCases { var testCases TestCases |
︙ | ︙ | |||
43 44 45 46 47 48 49 | 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() | | | | < < > > > > > > > | < < < < | | | | | | | < < < < < < < < < < < < < < < < < < < | < | | | | | | | | | | > | > > | | | | | | < < | < | < | < | < | < | | < < | < < | < < < < | | | < | < < < < < < < < < < | < < | | < < < | < < < < < < < | | | | < < | | | < < < < | < < < < < < < < < | < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < | | | | | | | | | | < < < < < < < < < < | 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 | 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(tc.source) bns := parser.ParseBlocks(inp, nil, meta.ValueSyntaxZmk) var tv TestVisitor tv.visitBlockSlice(bns) got := tv.String() if tc.want != got { st.Errorf("\nwant=%q\n got=%q", tc.want, got) } }) } } func TestEOL(t *testing.T) { checkTcs(t, TestCases{ {"", ""}, {"\n", ""}, {"\r", ""}, {"\r\n", ""}, {"\n\n", ""}, }) } func TestText(t *testing.T) { 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)"}, {"...", "(PARA \u2026)"}, {"...,", "(PARA \u2026,)"}, {"...;", "(PARA \u2026;)"}, {"...:", "(PARA \u2026:)"}, {"...!", "(PARA \u2026!)"}, {"...?", "(PARA \u2026?)"}, {"...-", "(PARA ...-)"}, {"a...b", "(PARA a...b)"}, }) } func TestSpace(t *testing.T) { checkTcs(t, TestCases{ {" ", ""}, {"\t", ""}, {" ", ""}, }) } func TestSoftBreak(t *testing.T) { checkTcs(t, TestCases{ {"x\ny", "(PARA x SB y)"}, {"z\n", "(PARA z)"}, {" \n ", ""}, {" \n", ""}, }) } func TestHardBreak(t *testing.T) { checkTcs(t, TestCases{ {"x \ny", "(PARA x HB y)"}, {"z \n", "(PARA z)"}, {" \n ", ""}, {" \n", ""}, }) } func TestLink(t *testing.T) { checkTcs(t, TestCases{ {"[", "(PARA [)"}, {"[[", "(PARA [[)"}, {"[[|", "(PARA [[|)"}, {"[[]", "(PARA [[])"}, {"[[|]", "(PARA [[|])"}, {"[[]]", "(PARA [[]])"}, {"[[|]]", "(PARA [[|]])"}, {"[[ ]]", "(PARA [[ SP ]])"}, {"[[\n]]", "(PARA [[ SB ]])"}, {"[[ a]]", "(PARA (LINK a a))"}, {"[[a ]]", "(PARA [[a SP ]])"}, {"[[a\n]]", "(PARA [[a SB ]])"}, {"[[a]]", "(PARA (LINK a a))"}, {"[[12345678901234]]", "(PARA (LINK 12345678901234 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 a) go)"}, {"[[a]]{go}", "(PARA (LINK a a)[ATTR go])"}, {"[[[[a]]|b]]", "(PARA (LINK [[a [[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]))"}, }) } func TestCite(t *testing.T) { 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) { 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 TestImage(t *testing.T) { 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 (IMAGE a))"}, {"{{12345678901234}}", "(PARA (IMAGE 12345678901234))"}, {"{{ a}}", "(PARA (IMAGE a))"}, {"{{a}", "(PARA {{a})"}, {"{{|a}}", "(PARA {{|a}})"}, {"{{b|}}", "(PARA {{b|}})"}, {"{{b|a}}", "(PARA (IMAGE a b))"}, {"{{b| a}}", "(PARA (IMAGE a b))"}, {"{{b|a}", "(PARA {{b|a})"}, {"{{b\nc|a}}", "(PARA (IMAGE a b SB c))"}, {"{{b c|a#n}}", "(PARA (IMAGE a#n b SP c))"}, {"{{a}}{go}", "(PARA (IMAGE a)[ATTR go])"}, {"{{{{a}}|b}}", "(PARA (IMAGE %7B%7Ba) |b}})"}, }) } func TestTag(t *testing.T) { checkTcs(t, TestCases{ {"#", "(PARA #)"}, {"##", "(PARA ##)"}, {"###", "(PARA ###)"}, {"#tag", "(PARA #tag#)"}, {"#tag,", "(PARA #tag# ,)"}, {"#t-g ", "(PARA #t-g#)"}, {"#t_g", "(PARA #t_g#)"}, }) } func TestMark(t *testing.T) { checkTcs(t, TestCases{ {"[!", "(PARA [!)"}, {"[!\n", "(PARA [!)"}, {"[!]", "(PARA (MARK *))"}, {"[! ]", "(PARA [! SP ])"}, {"[!a]", "(PARA (MARK a))"}, {"[!a ]", "(PARA [!a SP ])"}, {"[!a_]", "(PARA (MARK a_))"}, {"[!a-b]", "(PARA (MARK a-b))"}, {"[!a][!a]", "(PARA (MARK a) (MARK a-1))"}, {"[!][!]", "(PARA (MARK *) (MARK *-1))"}, }) } func TestComment(t *testing.T) { 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 SP {% b})"}, {" %%b", "(PARA {% b})"}, {"%%b ", "(PARA {% b })"}, {"100%", "(PARA 100%)"}, }) } func TestFormat(t *testing.T) { 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\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) { 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 TestMixFormatCode(t *testing.T) { 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) { checkTcs(t, TestCases{ {"--", "(PARA \u2013)"}, {"a--b", "(PARA a\u2013b)"}, }) } func TestEntity(t *testing.T) { checkTcs(t, TestCases{ {"&", "(PARA &)"}, {"&;", "(PARA &;)"}, {"&#;", "(PARA &#;)"}, {"a;", "(PARA & #1a# ;)"}, {"&#x;", "(PARA & #x# ;)"}, {"�z;", "(PARA & #x0z# ;)"}, {"&1;", "(PARA &1;)"}, // Good cases {"<", "(PARA <)"}, {"0", "(PARA 0)"}, {"J", "(PARA J)"}, {"J", "(PARA J)"}, {"…", "(PARA \u2026)"}, {"E: &, ;
.", "(PARA E: SP &,\r;\n.)"}, }) } func TestVerbatim(t *testing.T) { 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 TestSpanRegion(t *testing.T) { 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) { 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) { 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) { checkTcs(t, TestCases{ {"=h", "(PARA =h)"}, {"= h", "(PARA = SP h)"}, {"==h", "(PARA ==h)"}, {"== h", "(PARA == SP h)"}, {"===h", "(PARA ===h)"}, {"=== h", "(H2 h)"}, {"=== h", "(H2 h)"}, {"==== h", "(H3 h)"}, {"===== h", "(H4 h)"}, {"====== h", "(H5 h)"}, {"======= h", "(H6 h)"}, {"======== h", "(H6 h)"}, {"=", "(PARA =)"}, {"=== h=//=a//", "(H2 h= {/ =a})"}, {"=\n", "(PARA =)"}, {"a=", "(PARA a=)"}, {" =", "(PARA =)"}, {"=== h\na", "(H2 h)(PARA a)"}, {"=== h i {-}", "(H2 h SP i)[ATTR -]"}, }) } func TestHRule(t *testing.T) { 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) { // No ">" in the following, because quotation lists may have empty items. for _, ch := range []string{"*", "#"} { checkTcs(t, replace(ch, TestCases{ {"$", "(PARA $)"}, {"$$", "(PARA $$)"}, {"$$$", "(PARA $$$)"}, {"$ ", "(PARA $)"}, |
︙ | ︙ | |||
609 610 611 612 613 614 615 | // 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)})"}, | | < < < < < < < < < < < | 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 | // 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 mayx have empty items {">", "(QL {})"}, }) } func TestEnumAfterPara(t *testing.T) { checkTcs(t, TestCases{ {"abc\n* def", "(PARA abc)(UL {(PARA def)})"}, {"abc\n*def", "(PARA abc SB *def)"}, }) } func TestDefinition(t *testing.T) { 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))"}, |
︙ | ︙ | |||
655 656 657 658 659 660 661 | {"; 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) { | < < < < < < < < < < < < < < < < < < | | < | 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 | {"; 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) { checkTcs(t, TestCases{ {"|", "(TAB (TR))"}, {"|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)))"}, }) } func TestBlockAttr(t *testing.T) { 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 (PARA 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 (PARA 3$}))"}, {":::{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) { 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])"}, |
︙ | ︙ | |||
745 746 747 748 749 750 751 | {"::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])"}, | | < | | < | < < < < | < < < < < < < < < < < < < < < < < < < < < < < < | < < < < < < < < < < < < < | < < < < < < < < < < | < < < < < < < < < < < | < < > | < < < < < < < < | < < < < < < < < < < < < < < | < < < < | < < | < < < < < < | < < | | < < | < < < | < < | < < < < < < | < < < < < < < | < < < < < | | < < < < < < | < < < | < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < | > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > | > > > > > > > > > > > > > > > > > > > > > > > > | | | | | | | | | | | > | > > > > > > > > > > > > > > > > > > | > | | | > | | | > > > > | | > > | > > > > > > > > > > > > > | > > > | > > > | > > > > > > > > > > | | > | < > > > > > | > > > > | > > > > > > > | | > > > | > > | > > > > > > | > > | > > | > | | | > | > > > | | | | > > > | > > > > > > > | > | | > > > > > | > | > > > | > > > | > > > > > > > > | | > | > | 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 | {"::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 3$])"}, {"::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) { checkTcs(t, TestCases{ {"", ""}, }) } // -------------------------------------------------------------------------- // TestVisitor serializes the abstract syntax tree to a string. type TestVisitor struct { b strings.Builder } func (tv *TestVisitor) String() string { return tv.b.String() } func (tv *TestVisitor) VisitPara(pn *ast.ParaNode) { tv.b.WriteString("(PARA") tv.visitInlineSlice(pn.Inlines) tv.b.WriteByte(')') } var mapVerbatimCode = map[ast.VerbatimCode]string{ ast.VerbatimProg: "(PROG", } func (tv *TestVisitor) VisitVerbatim(vn *ast.VerbatimNode) { code, ok := mapVerbatimCode[vn.Code] if !ok { panic(fmt.Sprintf("Unknown verbatim code %v", vn.Code)) } tv.b.WriteString(code) for _, line := range vn.Lines { tv.b.WriteByte('\n') tv.b.WriteString(line) } tv.b.WriteByte(')') tv.visitAttributes(vn.Attrs) } var mapRegionCode = map[ast.RegionCode]string{ ast.RegionSpan: "(SPAN", ast.RegionQuote: "(QUOTE", ast.RegionVerse: "(VERSE", } // VisitRegion stores information about a region. func (tv *TestVisitor) VisitRegion(rn *ast.RegionNode) { code, ok := mapRegionCode[rn.Code] if !ok { panic(fmt.Sprintf("Unknown region code %v", rn.Code)) } tv.b.WriteString(code) if rn.Blocks != nil { tv.b.WriteByte(' ') tv.visitBlockSlice(rn.Blocks) } if len(rn.Inlines) > 0 { tv.b.WriteString(" (LINE") tv.visitInlineSlice(rn.Inlines) tv.b.WriteByte(')') } tv.b.WriteByte(')') tv.visitAttributes(rn.Attrs) } func (tv *TestVisitor) VisitHeading(hn *ast.HeadingNode) { fmt.Fprintf(&tv.b, "(H%d", hn.Level) tv.visitInlineSlice(hn.Inlines) tv.b.WriteByte(')') tv.visitAttributes(hn.Attrs) } func (tv *TestVisitor) VisitHRule(hn *ast.HRuleNode) { tv.b.WriteString("(HR)") tv.visitAttributes(hn.Attrs) } var mapNestedListCode = map[ast.NestedListCode]string{ ast.NestedListOrdered: "(OL", ast.NestedListUnordered: "(UL", ast.NestedListQuote: "(QL", } func (tv *TestVisitor) VisitNestedList(ln *ast.NestedListNode) { tv.b.WriteString(mapNestedListCode[ln.Code]) for _, item := range ln.Items { tv.b.WriteString(" {") tv.visitItemSlice(item) tv.b.WriteByte('}') } tv.b.WriteByte(')') } func (tv *TestVisitor) VisitDescriptionList(dn *ast.DescriptionListNode) { tv.b.WriteString("(DL") for _, def := range dn.Descriptions { tv.b.WriteString(" (DT") tv.visitInlineSlice(def.Term) tv.b.WriteByte(')') for _, b := range def.Descriptions { tv.b.WriteString(" (DD ") tv.visitDescriptionSlice(b) tv.b.WriteByte(')') } } tv.b.WriteByte(')') } var alignString = map[ast.Alignment]string{ ast.AlignDefault: "", ast.AlignLeft: "l", ast.AlignCenter: "c", ast.AlignRight: "r", } // VisitTable emits a HTML table. func (tv *TestVisitor) VisitTable(tn *ast.TableNode) { tv.b.WriteString("(TAB") if len(tn.Header) > 0 { tv.b.WriteString(" (TR") for _, cell := range tn.Header { tv.b.WriteString(" (TH") tv.b.WriteString(alignString[cell.Align]) tv.visitInlineSlice(cell.Inlines) tv.b.WriteString(")") } tv.b.WriteString(")") } if len(tn.Rows) > 0 { tv.b.WriteString(" ") for _, row := range tn.Rows { tv.b.WriteString("(TR") for i, cell := range row { if i == 0 { tv.b.WriteString(" ") } tv.b.WriteString("(TD") tv.b.WriteString(alignString[cell.Align]) tv.visitInlineSlice(cell.Inlines) tv.b.WriteString(")") } tv.b.WriteString(")") } } tv.b.WriteString(")") } func (tv *TestVisitor) VisitBLOB(bn *ast.BLOBNode) { tv.b.WriteString("(BLOB ") tv.b.WriteString(bn.Syntax) tv.b.WriteString(")") } func (tv *TestVisitor) VisitText(tn *ast.TextNode) { tv.b.WriteString(tn.Text) } func (tv *TestVisitor) VisitTag(tn *ast.TagNode) { tv.b.WriteByte('#') tv.b.WriteString(tn.Tag) tv.b.WriteByte('#') } func (tv *TestVisitor) VisitSpace(sn *ast.SpaceNode) { if len(sn.Lexeme) == 1 { tv.b.WriteString("SP") } else { fmt.Fprintf(&tv.b, "SP%d", len(sn.Lexeme)) } } func (tv *TestVisitor) VisitBreak(bn *ast.BreakNode) { if bn.Hard { tv.b.WriteString("HB") } else { tv.b.WriteString("SB") } } func (tv *TestVisitor) VisitLink(tn *ast.LinkNode) { fmt.Fprintf(&tv.b, "(LINK %s", tn.Ref) tv.visitInlineSlice(tn.Inlines) tv.b.WriteByte(')') tv.visitAttributes(tn.Attrs) } func (tv *TestVisitor) VisitImage(in *ast.ImageNode) { fmt.Fprintf(&tv.b, "(IMAGE %s", in.Ref) tv.visitInlineSlice(in.Inlines) tv.b.WriteByte(')') tv.visitAttributes(in.Attrs) } func (tv *TestVisitor) VisitCite(cn *ast.CiteNode) { fmt.Fprintf(&tv.b, "(CITE %s", cn.Key) tv.visitInlineSlice(cn.Inlines) tv.b.WriteByte(')') tv.visitAttributes(cn.Attrs) } func (tv *TestVisitor) VisitFootnote(fn *ast.FootnoteNode) { tv.b.WriteString("(FN") tv.visitInlineSlice(fn.Inlines) tv.b.WriteByte(')') tv.visitAttributes(fn.Attrs) } func (tv *TestVisitor) VisitMark(mn *ast.MarkNode) { tv.b.WriteString("(MARK") if len(mn.Text) > 0 { tv.b.WriteByte(' ') tv.b.WriteString(mn.Text) } tv.b.WriteByte(')') } var mapCode = map[ast.FormatCode]rune{ ast.FormatItalic: '/', ast.FormatBold: '*', ast.FormatUnder: '_', ast.FormatStrike: '~', ast.FormatMonospace: '\'', ast.FormatSuper: '^', ast.FormatSub: ',', ast.FormatQuote: '"', ast.FormatQuotation: '<', ast.FormatSmall: ';', ast.FormatSpan: ':', } func (tv *TestVisitor) VisitFormat(fn *ast.FormatNode) { fmt.Fprintf(&tv.b, "{%c", mapCode[fn.Code]) tv.visitInlineSlice(fn.Inlines) tv.b.WriteByte('}') tv.visitAttributes(fn.Attrs) } var mapLiteralCode = map[ast.LiteralCode]rune{ ast.LiteralProg: '`', ast.LiteralKeyb: '+', ast.LiteralOutput: '=', ast.LiteralComment: '%', } func (tv *TestVisitor) VisitLiteral(ln *ast.LiteralNode) { code, ok := mapLiteralCode[ln.Code] if !ok { panic(fmt.Sprintf("No element for code %v", ln.Code)) } tv.b.WriteByte('{') tv.b.WriteRune(code) if len(ln.Text) > 0 { tv.b.WriteByte(' ') tv.b.WriteString(ln.Text) } tv.b.WriteByte('}') tv.visitAttributes(ln.Attrs) } func (tv *TestVisitor) visitBlockSlice(bns ast.BlockSlice) { for _, bn := range bns { bn.Accept(tv) } } func (tv *TestVisitor) visitItemSlice(ins ast.ItemSlice) { for _, in := range ins { in.Accept(tv) } } func (tv *TestVisitor) visitDescriptionSlice(dns ast.DescriptionSlice) { for _, dn := range dns { dn.Accept(tv) } } func (tv *TestVisitor) visitInlineSlice(ins ast.InlineSlice) { for _, in := range ins { tv.b.WriteByte(' ') in.Accept(tv) } } func (tv *TestVisitor) visitAttributes(a *ast.Attributes) { if a == nil || len(a.Attrs) == 0 { return } tv.b.WriteString("[ATTR") keys := make([]string, 0, len(a.Attrs)) for k := range a.Attrs { keys = append(keys, k) } sort.Strings(keys) for _, k := range keys { tv.b.WriteByte(' ') tv.b.WriteString(k) v := a.Attrs[k] if len(v) > 0 { tv.b.WriteByte('=') if strings.ContainsRune(v, ' ') { tv.b.WriteByte('"') tv.b.WriteString(v) tv.b.WriteByte('"') } else { tv.b.WriteString(v) } } } tv.b.WriteByte(']') } |
Added place/constplace/base.css.
> > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 | *,*::before,*::after { box-sizing: border-box; } html { font-size: 1rem; font-family: serif; scroll-behavior: smooth; height: 100%; } body { margin: 0; min-height: 100vh; text-rendering: optimizeSpeed; line-height: 1.4; overflow-x: hidden; background-color: #f8f8f8 ; height: 100%; } nav.zs-menu { background-color: hsl(210, 28%, 90%); overflow: auto; white-space: nowrap; font-family: sans-serif; padding-left: .5rem; } nav.zs-menu > a { float:left; display: block; text-align: center; padding:.41rem .5rem; text-decoration: none; color:black; } nav.zs-menu > a:hover, .zs-dropdown:hover button { background-color: hsl(210, 28%, 80%); } nav.zs-menu form { float: right; } nav.zs-menu form input[type=text] { padding: .12rem; border: none; margin-top: .25rem; margin-right: .5rem; } .zs-dropdown { float: left; overflow: hidden; } .zs-dropdown > button { font-size: 16px; border: none; outline: none; color: black; padding:.41rem .5rem; background-color: inherit; font-family: inherit; margin: 0; } .zs-dropdown-content { display: none; position: absolute; background-color: #f9f9f9; min-width: 160px; box-shadow: 0px 8px 16px 0px rgba(0,0,0,0.2); z-index: 1; } .zs-dropdown-content > a { float: none; color: black; padding:.41rem .5rem; text-decoration: none; display: block; text-align: left; } .zs-dropdown-content > a:hover { background-color: hsl(210, 28%, 75%); } .zs-dropdown:hover > .zs-dropdown-content { display: block; } main { padding: 0 1rem; } article > * + * { margin-top: .5rem; } article header { padding: 0; margin: 0; } h1,h2,h3,h4,h5,h6 { font-family:sans-serif; font-weight:normal } h1 { font-size:1.5rem; margin:.65rem 0 } h2 { font-size:1.25rem; margin:.70rem 0 } h3 { font-size:1.15rem; margin:.75rem 0 } h4 { font-size:1.05rem; margin:.8rem 0; font-weight: bold } h5 { font-size:1.05rem; margin:.8rem 0 } h6 { font-size:1.05rem; margin:.8rem 0; font-weight: lighter } p { margin: .5rem 0 0 0; } ol,ul { padding-left: 1.1rem; } li,figure,figcaption,dl { margin: 0; } dt { margin: .5rem 0 0 0; } dt+dd { margin-top: 0; } dd { margin: .5rem 0 0 2rem; } dd > p:first-child { margin: 0 0 0 0; } blockquote { border-left: 0.5rem solid lightgray; padding-left: 1rem; margin-left: 1rem; margin-right: 2rem; font-style: italic; } blockquote p { margin-bottom: .5rem; } blockquote cite { font-style: normal; } table { border-collapse: collapse; border-spacing: 0; max-width: 100%; } th,td { text-align: left; padding: .25rem .5rem; } td { border-bottom: 1px solid hsl(0, 0%, 85%); } thead th { border-bottom: 2px solid hsl(0, 0%, 70%); } tfoot th { border-top: 2px solid hsl(0, 0%, 70%); } main form { padding: 0 .5em; margin: .5em 0 0 0; } main form:after { content: "."; display: block; height: 0; clear: both; visibility: hidden; } main form div { margin: .5em 0 0 0 } input { font-family: monospace; } input[type="submit"],button,select { font: inherit; } label { font-family: sans-serif; font-size:.9rem } label::after { content:":" } textarea { font-family: monospace; resize: vertical; width: 100%; } .zs-input { padding: .5em; display:block; border:none; border-bottom:1px solid #ccc; width:100%; } .zs-button { float:right; margin: .5em 0 .5em 1em; } a:not([class]) { text-decoration-skip-ink: auto; } .zs-broken { text-decoration: line-through; } img { max-width: 100%; } .zs-endnotes { padding-top: .5rem; border-top: 1px solid; } code,pre,kbd { font-family: monospace; font-size: 85%; } code { padding: .1rem .2rem; background: #f0f0f0; border: 1px solid #ccc; border-radius: .25rem; } pre { padding: .5rem .7rem; max-width: 100%; overflow: auto; border: 1px solid #ccc; border-radius: .5rem; background: #f0f0f0; } pre code { font-size: 95%; position: relative; padding: 0; border: none; } div.zs-indication { padding: .5rem .7rem; max-width: 100%; border-radius: .5rem; border: 1px solid black; } div.zs-indication p:first-child { margin-top: 0; } span.zs-indication { border: 1px solid black; border-radius: .25rem; padding: .1rem .2rem; font-size: 95%; } .zs-example { border-style: dotted !important } .zs-error { background-color: lightpink; border-style: none !important; font-weight: bold; } kbd { background: hsl(210, 5%, 100%); border: 1px solid hsl(210, 5%, 70%); border-radius: .25rem; padding: .1rem .2rem; font-size: 75%; } .zs-meta { font-size:.75rem; color:#444; margin-bottom:1rem; } .zs-meta a { color:#444; } h1+.zs-meta { margin-top:-1rem; } details > summary { width: 100%; background-color: #eee; font-family:sans-serif; } details > ul { margin-top:0; padding-left:2rem; background-color: #eee; } footer { padding: 0 1rem; } @media (prefers-reduced-motion: reduce) { * { animation-duration: 0.01ms !important; animation-iteration-count: 1 !important; transition-duration: 0.01ms !important; scroll-behavior: auto !important; } } |
Added place/constplace/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 | <!DOCTYPE html> <html{{#Lang}} lang="{{Lang}}"{{/Lang}}> <head> <meta charset="utf-8"> <meta name="referrer" content="no-referrer"> <meta name="viewport" content="width=device-width, initial-scale=1.0"> <meta name="generator" content="Zettelstore"> <meta name="format-detection" content="telephone=no"> {{{MetaHeader}}} <link rel="stylesheet" href="{{{StylesheetURL}}}"> <title>{{Title}}</title> </head> <body> <nav class="zs-menu"> <a href="{{{HomeURL}}}">Home</a> {{#WithUser}} <div class="zs-dropdown"> <button>User</button> <nav class="zs-dropdown-content"> {{#WithAuth}} {{#UserIsValid}} <a href="{{{UserZettelURL}}}">{{UserIdent}}</a> {{/UserIsValid}} {{^UserIsValid}} <a href="{{{LoginURL}}}">Login</a> {{/UserIsValid}} {{#UserIsValid}} <a href="{{{UserLogoutURL}}}">Logout</a> {{/UserIsValid}} {{/WithAuth}} </nav> </div> {{/WithUser}} <div class="zs-dropdown"> <button>Lists</button> <nav class="zs-dropdown-content"> <a href="{{{ListZettelURL}}}">List Zettel</a> <a href="{{{ListRolesURL}}}">List Roles</a> <a href="{{{ListTagsURL}}}">List Tags</a> </nav> </div> {{#CanCreate}} <div class="zs-dropdown"> <button>New</button> <nav class="zs-dropdown-content"> {{#NewZettelLinks}} <a href="{{{URL}}}">{{Text}}</a> {{/NewZettelLinks}} </nav> </div> {{/CanCreate}} <form action="{{{SearchURL}}}"> <input type="text" placeholder="Search.." name="s"> </form> </nav> <main class="content"> {{{Content}}} </main> {{#FooterHTML}} <footer> {{{FooterHTML}}} </footer> {{/FooterHTML}} </body> </html> |
Added place/constplace/constplace.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 | //----------------------------------------------------------------------------- // Copyright (c) 2020-2021 Detlef Stern // // This file is part of zettelstore. // // Zettelstore is licensed under the latest version of the EUPL (European Union // Public License). Please see file LICENSE.txt for your rights and obligations // under this license. //----------------------------------------------------------------------------- // Package constplace places zettel inside the executable. package constplace import ( "context" _ "embed" // Allow to embed file content "net/url" "zettelstore.de/z/domain" "zettelstore.de/z/domain/id" "zettelstore.de/z/domain/meta" "zettelstore.de/z/place" "zettelstore.de/z/place/manager" "zettelstore.de/z/search" ) func init() { manager.Register( " const", func(u *url.URL, cdata *manager.ConnectData) (place.ManagedPlace, error) { return &constPlace{zettel: constZettelMap, enricher: cdata.Enricher}, nil }) } type constHeader map[string]string func makeMeta(zid id.Zid, h constHeader) *meta.Meta { m := meta.New(zid) for k, v := range h { m.Set(k, v) } return m } type constZettel struct { header constHeader content domain.Content } type constPlace struct { zettel map[id.Zid]constZettel enricher place.Enricher } func (cp *constPlace) Location() string { return "const:" } func (cp *constPlace) CanCreateZettel(ctx context.Context) bool { return false } func (cp *constPlace) CreateZettel(ctx context.Context, zettel domain.Zettel) (id.Zid, error) { return id.Invalid, place.ErrReadOnly } func (cp *constPlace) GetZettel(ctx context.Context, zid id.Zid) (domain.Zettel, error) { if z, ok := cp.zettel[zid]; ok { return domain.Zettel{Meta: makeMeta(zid, z.header), Content: z.content}, nil } return domain.Zettel{}, place.ErrNotFound } func (cp *constPlace) GetMeta(ctx context.Context, zid id.Zid) (*meta.Meta, error) { if z, ok := cp.zettel[zid]; ok { return makeMeta(zid, z.header), nil } return nil, place.ErrNotFound } func (cp *constPlace) FetchZids(ctx context.Context) (id.Set, error) { result := id.NewSetCap(len(cp.zettel)) for zid := range cp.zettel { result[zid] = true } return result, nil } func (cp *constPlace) SelectMeta(ctx context.Context, match search.MetaMatchFunc) (res []*meta.Meta, err error) { for zid, zettel := range cp.zettel { m := makeMeta(zid, zettel.header) cp.enricher.Enrich(ctx, m) if match(m) { res = append(res, m) } } return res, nil } func (cp *constPlace) CanUpdateZettel(ctx context.Context, zettel domain.Zettel) bool { return false } func (cp *constPlace) UpdateZettel(ctx context.Context, zettel domain.Zettel) error { return place.ErrReadOnly } func (cp *constPlace) AllowRenameZettel(ctx context.Context, zid id.Zid) bool { _, ok := cp.zettel[zid] return !ok } func (cp *constPlace) RenameZettel(ctx context.Context, curZid, newZid id.Zid) error { if _, ok := cp.zettel[curZid]; ok { return place.ErrReadOnly } return place.ErrNotFound } func (cp *constPlace) CanDeleteZettel(ctx context.Context, zid id.Zid) bool { return false } func (cp *constPlace) DeleteZettel(ctx context.Context, zid id.Zid) error { if _, ok := cp.zettel[zid]; ok { return place.ErrReadOnly } return place.ErrNotFound } func (cp *constPlace) ReadStats(st *place.ManagedPlaceStats) { st.ReadOnly = true st.Zettel = len(cp.zettel) } const syntaxTemplate = "mustache" var constZettelMap = map[id.Zid]constZettel{ id.ConfigurationZid: { constHeader{ meta.KeyTitle: "Zettelstore Runtime Configuration", meta.KeyRole: meta.ValueRoleConfiguration, meta.KeyVisibility: meta.ValueVisibilityOwner, meta.KeySyntax: meta.ValueSyntaxNone, meta.KeyNoIndex: meta.ValueTrue, }, ""}, id.LicenseZid: { constHeader{ meta.KeyTitle: "Zettelstore License", meta.KeyRole: meta.ValueRoleConfiguration, meta.KeyVisibility: meta.ValueVisibilityPublic, meta.KeySyntax: meta.ValueSyntaxText, meta.KeyLang: meta.ValueLangEN, meta.KeyReadOnly: meta.ValueTrue, }, domain.NewContent(contentLicense)}, id.AuthorsZid: { constHeader{ meta.KeyTitle: "Zettelstore Contributors", meta.KeyRole: meta.ValueRoleConfiguration, meta.KeyVisibility: meta.ValueVisibilityPublic, meta.KeySyntax: meta.ValueSyntaxZmk, meta.KeyLang: meta.ValueLangEN, meta.KeyReadOnly: meta.ValueTrue, }, domain.NewContent(contentContributors)}, id.DependenciesZid: { constHeader{ meta.KeyTitle: "Zettelstore Dependencies", meta.KeyRole: meta.ValueRoleConfiguration, meta.KeyVisibility: meta.ValueVisibilityPublic, meta.KeySyntax: meta.ValueSyntaxZmk, meta.KeyLang: meta.ValueLangEN, meta.KeyReadOnly: meta.ValueTrue, }, domain.NewContent(contentDependencies)}, id.BaseTemplateZid: { constHeader{ meta.KeyTitle: "Zettelstore Base HTML Template", meta.KeyRole: meta.ValueRoleConfiguration, meta.KeyVisibility: meta.ValueVisibilityExpert, meta.KeySyntax: syntaxTemplate, meta.KeyNoIndex: meta.ValueTrue, }, domain.NewContent(contentBaseMustache)}, id.LoginTemplateZid: { constHeader{ meta.KeyTitle: "Zettelstore Login Form HTML Template", meta.KeyRole: meta.ValueRoleConfiguration, meta.KeyVisibility: meta.ValueVisibilityExpert, meta.KeySyntax: syntaxTemplate, meta.KeyNoIndex: meta.ValueTrue, }, domain.NewContent(contentLoginMustache)}, id.ZettelTemplateZid: { constHeader{ meta.KeyTitle: "Zettelstore Zettel HTML Template", meta.KeyRole: meta.ValueRoleConfiguration, meta.KeyVisibility: meta.ValueVisibilityExpert, meta.KeySyntax: syntaxTemplate, meta.KeyNoIndex: meta.ValueTrue, }, domain.NewContent(contentZettelMustache)}, id.InfoTemplateZid: { constHeader{ meta.KeyTitle: "Zettelstore Info HTML Template", meta.KeyRole: meta.ValueRoleConfiguration, meta.KeyVisibility: meta.ValueVisibilityExpert, meta.KeySyntax: syntaxTemplate, meta.KeyNoIndex: meta.ValueTrue, }, domain.NewContent(contentInfoMustache)}, id.ContextTemplateZid: { constHeader{ meta.KeyTitle: "Zettelstore Context HTML Template", meta.KeyRole: meta.ValueRoleConfiguration, meta.KeyVisibility: meta.ValueVisibilityExpert, meta.KeySyntax: syntaxTemplate, meta.KeyNoIndex: meta.ValueTrue, }, domain.NewContent(contentContextMustache)}, id.FormTemplateZid: { constHeader{ meta.KeyTitle: "Zettelstore Form HTML Template", meta.KeyRole: meta.ValueRoleConfiguration, meta.KeyVisibility: meta.ValueVisibilityExpert, meta.KeySyntax: syntaxTemplate, meta.KeyNoIndex: meta.ValueTrue, }, domain.NewContent(contentFormMustache)}, id.RenameTemplateZid: { constHeader{ meta.KeyTitle: "Zettelstore Rename Form HTML Template", meta.KeyRole: meta.ValueRoleConfiguration, meta.KeyVisibility: meta.ValueVisibilityExpert, meta.KeySyntax: syntaxTemplate, meta.KeyNoIndex: meta.ValueTrue, }, domain.NewContent(contentRenameMustache)}, id.DeleteTemplateZid: { constHeader{ meta.KeyTitle: "Zettelstore Delete HTML Template", meta.KeyRole: meta.ValueRoleConfiguration, meta.KeyVisibility: meta.ValueVisibilityExpert, meta.KeySyntax: syntaxTemplate, meta.KeyNoIndex: meta.ValueTrue, }, domain.NewContent(contentDeleteMustache)}, id.ListTemplateZid: { constHeader{ meta.KeyTitle: "Zettelstore List Zettel HTML Template", meta.KeyRole: meta.ValueRoleConfiguration, meta.KeyVisibility: meta.ValueVisibilityExpert, meta.KeySyntax: syntaxTemplate, meta.KeyNoIndex: meta.ValueTrue, }, domain.NewContent(contentListZettelMustache)}, id.RolesTemplateZid: { constHeader{ meta.KeyTitle: "Zettelstore List Roles HTML Template", meta.KeyRole: meta.ValueRoleConfiguration, meta.KeyVisibility: meta.ValueVisibilityExpert, meta.KeySyntax: syntaxTemplate, meta.KeyNoIndex: meta.ValueTrue, }, domain.NewContent(contentListRolesMustache)}, id.TagsTemplateZid: { constHeader{ meta.KeyTitle: "Zettelstore List Tags HTML Template", meta.KeyRole: meta.ValueRoleConfiguration, meta.KeyVisibility: meta.ValueVisibilityExpert, meta.KeySyntax: syntaxTemplate, meta.KeyNoIndex: meta.ValueTrue, }, domain.NewContent(contentListTagsMustache)}, id.ErrorTemplateZid: { constHeader{ meta.KeyTitle: "Zettelstore Error HTML Template", meta.KeyRole: meta.ValueRoleConfiguration, meta.KeyVisibility: meta.ValueVisibilityExpert, meta.KeySyntax: syntaxTemplate, meta.KeyNoIndex: meta.ValueTrue, }, domain.NewContent(contentErrorMustache)}, id.BaseCSSZid: { constHeader{ meta.KeyTitle: "Zettelstore Base CSS", meta.KeyRole: meta.ValueRoleConfiguration, meta.KeyVisibility: meta.ValueVisibilityPublic, meta.KeySyntax: "css", meta.KeyNoIndex: meta.ValueTrue, }, domain.NewContent(contentBaseCSS)}, id.EmojiZid: { constHeader{ meta.KeyTitle: "Generic Emoji", meta.KeyRole: meta.ValueRoleConfiguration, meta.KeyVisibility: meta.ValueVisibilityPublic, meta.KeySyntax: meta.ValueSyntaxGif, meta.KeyReadOnly: meta.ValueTrue, }, domain.NewContent(contentEmoji)}, id.TOCNewTemplateZid: { constHeader{ meta.KeyTitle: "New Menu", meta.KeyRole: meta.ValueRoleConfiguration, meta.KeySyntax: meta.ValueSyntaxZmk, meta.KeyLang: meta.ValueLangEN, }, domain.NewContent(contentNewTOCZettel)}, id.TemplateNewZettelZid: { constHeader{ meta.KeyTitle: "New Zettel", meta.KeyRole: meta.ValueRoleZettel, meta.KeySyntax: meta.ValueSyntaxZmk, }, ""}, id.TemplateNewUserZid: { constHeader{ meta.KeyTitle: "New User", meta.KeyRole: meta.ValueRoleUser, meta.KeyCredential: "", meta.KeyUserID: "", meta.KeyUserRole: meta.ValueUserRoleReader, meta.KeySyntax: meta.ValueSyntaxNone, }, ""}, id.DefaultHomeZid: { constHeader{ meta.KeyTitle: "Home", meta.KeyRole: meta.ValueRoleZettel, meta.KeySyntax: meta.ValueSyntaxZmk, meta.KeyLang: meta.ValueLangEN, }, domain.NewContent(contentHomeZettel)}, } //go:embed license.txt var contentLicense string //go:embed contributors.zettel var contentContributors string //go:embed dependencies.zettel var contentDependencies string //go:embed base.mustache var contentBaseMustache string //go:embed login.mustache var contentLoginMustache string //go:embed zettel.mustache var contentZettelMustache string //go:embed info.mustache var contentInfoMustache string //go:embed context.mustache var contentContextMustache string //go:embed form.mustache var contentFormMustache string //go:embed rename.mustache var contentRenameMustache string //go:embed delete.mustache var contentDeleteMustache string //go:embed listzettel.mustache var contentListZettelMustache string //go:embed listroles.mustache var contentListRolesMustache string //go:embed listtags.mustache var contentListTagsMustache string //go:embed error.mustache var contentErrorMustache string //go:embed base.css var contentBaseCSS string //go:embed emoji_spin.gif var contentEmoji string //go:embed newtoc.zettel var contentNewTOCZettel string //go:embed home.zettel var contentHomeZettel string |
Added place/constplace/context.mustache.
> > > > > > > > > > > > > > > > | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 | <nav> <header> <h1>{{Title}}</h1> <div class="zs-meta"> <a href="{{{InfoURL}}}">Info</a> · <a href="?dir=backward">Backward</a> · <a href="?dir=both">Both</a> · <a href="?dir=forward">Forward</a> · Depth:{{#Depths}} <a href="{{{URL}}}">{{{Text}}}</a>{{/Depths}} </div> </header> <p><a href="{{{Start.URL}}}">{{{Start.Text}}}</a></p> <ul> {{#Metas}}<li><a href="{{{URL}}}">{{{Text}}}</a></li> {{/Metas}}</ul> </nav> |
Added place/constplace/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 place/constplace/delete.mustache.
> > > > > > > > > > > > > > > | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 | <article> <header> <h1>Delete Zettel {{Zid}}</h1> </header> <p>Do you really want to delete this zettel?</p> <dl> {{#MetaPairs}} <dt>{{Key}}:</dt><dd>{{Value}}</dd> {{/MetaPairs}} </dl> <form method="POST"> <input class="zs-button" type="submit" value="Delete"> </form> </article> {{end}} |
Added place/constplace/dependencies.zettel.
> > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 | Zettelstore is made with the help of other software and other artifacts. Thank you very much! This zettel lists all of them, together with their license. === Go runtime and associated libraries ; License : BSD 3-Clause "New" or "Revised" License ``` Copyright (c) 2009 The Go Authors. All rights reserved. Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met: * Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer. * Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution. * Neither the name of Google Inc. nor the names of its contributors may be used to endorse or promote products derived from this software without specific prior written permission. THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. ``` === Fsnotify ; URL : [[https://fsnotify.org/]] ; License : BSD 3-Clause "New" or "Revised" License ; Source : [[https://github.com/fsnotify/fsnotify]] ``` Copyright (c) 2012 The Go Authors. All rights reserved. Copyright (c) 2012-2019 fsnotify Authors. All rights reserved. Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met: * Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer. * Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution. * Neither the name of Google Inc. nor the names of its contributors may be used to endorse or promote products derived from this software without specific prior written permission. THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. ``` === hoisie/mustache / cbroglie/mustache ; URL & Source : [[https://github.com/hoisie/mustache]] / [[https://github.com/cbroglie/mustache]] ; License : MIT License ; Remarks : cbroglie/mustache is a fork from hoisie/mustache (starting with commit [f9b4cbf]). cbroglie/mustache does not claim a copyright and includes just the license file from hoisie/mustache. cbroglie/mustache obviously continues with the original license. ``` Copyright (c) 2009 Michael Hoisie Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. ``` === pascaldekloe/jwt ; URL & Source : [[https://github.com/pascaldekloe/jwt]] ; License : [[CC0 1.0 Universal|https://creativecommons.org/publicdomain/zero/1.0/legalcode]] ``` To the extent possible under law, Pascal S. de Kloe has waived all copyright and related or neighboring rights to JWT. This work is published from The Netherlands. https://creativecommons.org/publicdomain/zero/1.0/legalcode ``` === yuin/goldmark ; URL & Source : [[https://github.com/yuin/goldmark]] ; License : MIT License ``` MIT License Copyright (c) 2019 Yusuke Inuzuka Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. ``` |
Added place/constplace/emoji_spin.gif.
cannot compute difference between binary files
Added place/constplace/error.mustache.
> > > > > > | 1 2 3 4 5 6 | <article> <header> <h1>{{ErrorTitle}}</h1> </header> {{ErrorText}} </article> |
Added place/constplace/form.mustache.
> > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 | <article> <header> <h1>{{Heading}}</h1> </header> <form method="POST"> <div> <label for="title">Title</label> <input class="zs-input" type="text" id="title" name="title" placeholder="Title.." value="{{MetaTitle}}" autofocus> </div> <div> <div> <label for="role">Role</label> <input class="zs-input" type="text" id="role" name="role" placeholder="role.." value="{{MetaRole}}"> </div> <label for="tags">Tags</label> <input class="zs-input" type="text" id="tags" name="tags" placeholder="#tag" value="{{MetaTags}}"> </div> <div> <label for="meta">Metadata</label> <textarea class="zs-input" id="meta" name="meta" rows="4" placeholder="metakey: metavalue"> {{#MetaPairsRest}} {{Key}}: {{Value}} {{/MetaPairsRest}} </textarea> </div> <div> <label for="syntax">Syntax</label> <input class="zs-input" type="text" id="syntax" name="syntax" placeholder="syntax.." value="{{MetaSyntax}}"> </div> <div> {{#IsTextContent}} <label for="content">Content</label> <textarea class="zs-input zs-content" id="meta" name="content" rows="20" placeholder="Your content..">{{Content}}</textarea> {{/IsTextContent}} </div> <input class="zs-button" type="submit" value="Submit"> </form> </article> |
Added place/constplace/home.zettel.
> > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 | === Thank you for using Zettelstore! You will find the lastest information about Zettelstore at [[https://zettelstore.de]]. Check that website regulary for [[upgrades|https://zettelstore.de/home/doc/trunk/www/download.wiki]] to the latest version. You should consult the [[change log|https://zettelstore.de/home/doc/trunk/www/changes.wiki]] before upgrading. Sometimes, you have to edit some of your Zettelstore-related zettel before upgrading. Since Zettelstore is currently in a development state, every upgrade might fix some of your problems. To check for versions, there is a zettel with the [[current version|00000000000001]] of your Zettelstore. If you have problems concerning Zettelstore, do not hesitate to get in [[contact with the main developer|mailto:ds@zettelstore.de]]. === Reporting errors If you have encountered an error, please include the content of the following zettel in your mail (if possible): * [[Zettelstore Version|00000000000001]] * [[Zettelstore Operating System|00000000000003]] * [[Zettelstore Startup Configuration|00000000000096]] * [[Zettelstore Runtime Configuration|00000000000100]] Additionally, you have to describe, what you have done before that error occurs and what you have expected instead. Please do not forget to include the error message, if there is one. Some of above Zettelstore zettel can only be retrieved if you enabled ""expert mode"". Otherwise, only some zettel are linked. To enable expert mode, edit the zettel [[Zettelstore Runtime Configuration|00000000000100]]: please set the metadata value of the key ''expert-mode'' to true. To do you, enter the string ''expert-mode:true'' inside the edit view of the metadata. === Information about this zettel This zettel is your home zettel. It is part of the Zettelstore software itself. Every time you click on the [[Home|//]] link in the menu bar, you will be redirected to this zettel. You can change the content of this zettel by clicking on ""Edit"" above. This allows you to customize your home zettel. Alternatively, you can designate another zettel as your home zettel. Edit the [[Zettelstore Runtime Configuration|00000000000100]] by adding the metadata key ''home-zettel''. Its value is the identifier of the zettel that should act as the new home zettel. You will find the identifier of each zettel between their ""Edit"" and the ""Info"" link above. The identifier of this zettel is ''00010000000000''. If you provide a wrong identifier, this zettel will be shown as the home zettel. Take a look inside the manual for further details. |
Added place/constplace/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 | <article> <header> <h1>Information for Zettel {{Zid}}</h1> <a href="{{{WebURL}}}">Web</a> · <a href="{{{ContextURL}}}">Context</a> {{#CanWrite}} · <a href="{{{EditURL}}}">Edit</a>{{/CanWrite}} {{#CanFolge}} · <a href="{{{FolgeURL}}}">Folge</a>{{/CanFolge}} {{#CanCopy}} · <a href="{{{CopyURL}}}">Copy</a>{{/CanCopy}} {{#CanRename}}· <a href="{{{RenameURL}}}">Rename</a>{{/CanRename}} {{#CanDelete}}· <a href="{{{DeleteURL}}}">Delete</a>{{/CanDelete}} </header> <h2>Interpreted Metadata</h2> <table>{{#MetaData}}<tr><td>{{Key}}</td><td>{{{Value}}}</td></tr>{{/MetaData}}</table> {{#HasLinks}} <h2>References</h2> {{#HasLocLinks}} <h3>Local</h3> <ul> {{#LocLinks}} {{#Valid}}<li><a href="{{{Zid}}}">{{Zid}}</a></li>{{/Valid}} {{^Valid}}<li>{{Zid}}</li>{{/Valid}} {{/LocLinks}} </ul> {{/HasLocLinks}} {{#HasExtLinks}} <h3>External</h3> <ul> {{#ExtLinks}} <li><a href="{{{.}}}"{{{ExtNewWindow}}}>{{.}}</a></li> {{/ExtLinks}} </ul> {{/HasExtLinks}} {{/HasLinks}} <h2>Parts and format</h3> <table> {{#Matrix}} <tr> {{#Elements}}{{#HasURL}}<td><a href="{{{URL}}}">{{Text}}</td>{{/HasURL}}{{^HasURL}}<th>{{Text}}</th>{{/HasURL}} {{/Elements}} </tr> {{/Matrix}} </table> {{#Endnotes}}{{{Endnotes}}}{{/Endnotes}} </article> |
Added place/constplace/license.txt.
> > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 | Copyright (c) 2020-2021 Detlef Stern Licensed under the EUPL Zettelstore is licensed under the European Union Public License, version 1.2 or later (EUPL v. 1.2). The license is available in the official languages of the EU. The English version is included here. Please see https://joinup.ec.europa.eu/community/eupl/og_page/eupl for official translations of the other languages. ------------------------------------------------------------------------------- EUROPEAN UNION PUBLIC LICENCE v. 1.2 EUPL © the European Union 2007, 2016 This European Union Public Licence (the ‘EUPL’) applies to the Work (as defined below) which is provided under the terms of this Licence. Any use of the Work, other than as authorised under this Licence is prohibited (to the extent such use is covered by a right of the copyright holder of the Work). The Work is provided under the terms of this Licence when the Licensor (as defined below) has placed the following notice immediately following the copyright notice for the Work: Licensed under the EUPL or has expressed by any other means his willingness to license under the EUPL. 1. Definitions In this Licence, the following terms have the following meaning: — ‘The Licence’: this Licence. — ‘The Original Work’: the work or software distributed or communicated by the Licensor under this Licence, available as Source Code and also as Executable Code as the case may be. — ‘Derivative Works’: the works or software that could be created by the Licensee, based upon the Original Work or modifications thereof. This Licence does not define the extent of modification or dependence on the Original Work required in order to classify a work as a Derivative Work; this extent is determined by copyright law applicable in the country mentioned in Article 15. — ‘The Work’: the Original Work or its Derivative Works. — ‘The Source Code’: the human-readable form of the Work which is the most convenient for people to study and modify. — ‘The Executable Code’: any code which has generally been compiled and which is meant to be interpreted by a computer as a program. — ‘The Licensor’: the natural or legal person that distributes or communicates the Work under the Licence. — ‘Contributor(s)’: any natural or legal person who modifies the Work under the Licence, or otherwise contributes to the creation of a Derivative Work. — ‘The Licensee’ or ‘You’: any natural or legal person who makes any usage of the Work under the terms of the Licence. — ‘Distribution’ or ‘Communication’: any act of selling, giving, lending, renting, distributing, communicating, transmitting, or otherwise making available, online or offline, copies of the Work or providing access to its essential functionalities at the disposal of any other natural or legal person. 2. Scope of the rights granted by the Licence The Licensor hereby grants You a worldwide, royalty-free, non-exclusive, sublicensable licence to do the following, for the duration of copyright vested in the Original Work: — use the Work in any circumstance and for all usage, — reproduce the Work, — modify the Work, and make Derivative Works based upon the Work, — communicate to the public, including the right to make available or display the Work or copies thereof to the public and perform publicly, as the case may be, the Work, — distribute the Work or copies thereof, — lend and rent the Work or copies thereof, — sublicense rights in the Work or copies thereof. Those rights can be exercised on any media, supports and formats, whether now known or later invented, as far as the applicable law permits so. In the countries where moral rights apply, the Licensor waives his right to exercise his moral right to the extent allowed by law in order to make effective the licence of the economic rights here above listed. The Licensor grants to the Licensee royalty-free, non-exclusive usage rights to any patents held by the Licensor, to the extent necessary to make use of the rights granted on the Work under this Licence. 3. Communication of the Source Code The Licensor may provide the Work either in its Source Code form, or as Executable Code. If the Work is provided as Executable Code, the Licensor provides in addition a machine-readable copy of the Source Code of the Work along with each copy of the Work that the Licensor distributes or indicates, in a notice following the copyright notice attached to the Work, a repository where the Source Code is easily and freely accessible for as long as the Licensor continues to distribute or communicate the Work. 4. Limitations on copyright Nothing in this Licence is intended to deprive the Licensee of the benefits from any exception or limitation to the exclusive rights of the rights owners in the Work, of the exhaustion of those rights or of other applicable limitations thereto. 5. Obligations of the Licensee The grant of the rights mentioned above is subject to some restrictions and obligations imposed on the Licensee. Those obligations are the following: Attribution right: The Licensee shall keep intact all copyright, patent or trademarks notices and all notices that refer to the Licence and to the disclaimer of warranties. The Licensee must include a copy of such notices and a copy of the Licence with every copy of the Work he/she distributes or communicates. The Licensee must cause any Derivative Work to carry prominent notices stating that the Work has been modified and the date of modification. Copyleft clause: If the Licensee distributes or communicates copies of the Original Works or Derivative Works, this Distribution or Communication will be done under the terms of this Licence or of a later version of this Licence unless the Original Work is expressly distributed only under this version of the Licence — for example by communicating ‘EUPL v. 1.2 only’. The Licensee (becoming Licensor) cannot offer or impose any additional terms or conditions on the Work or Derivative Work that alter or restrict the terms of the Licence. Compatibility clause: If the Licensee Distributes or Communicates Derivative Works or copies thereof based upon both the Work and another work licensed under a Compatible Licence, this Distribution or Communication can be done under the terms of this Compatible Licence. For the sake of this clause, ‘Compatible Licence’ refers to the licences listed in the appendix attached to this Licence. Should the Licensee's obligations under the Compatible Licence conflict with his/her obligations under this Licence, the obligations of the Compatible Licence shall prevail. Provision of Source Code: When distributing or communicating copies of the Work, the Licensee will provide a machine-readable copy of the Source Code or indicate a repository where this Source will be easily and freely available for as long as the Licensee continues to distribute or communicate the Work. Legal Protection: This Licence does not grant permission to use the trade names, trademarks, service marks, or names of the Licensor, except as required for reasonable and customary use in describing the origin of the Work and reproducing the content of the copyright notice. 6. Chain of Authorship The original Licensor warrants that the copyright in the Original Work granted hereunder is owned by him/her or licensed to him/her and that he/she has the power and authority to grant the Licence. Each Contributor warrants that the copyright in the modifications he/she brings to the Work are owned by him/her or licensed to him/her and that he/she has the power and authority to grant the Licence. Each time You accept the Licence, the original Licensor and subsequent Contributors grant You a licence to their contributions to the Work, under the terms of this Licence. 7. Disclaimer of Warranty The Work is a work in progress, which is continuously improved by numerous Contributors. It is not a finished work and may therefore contain defects or ‘bugs’ inherent to this type of development. For the above reason, the Work is provided under the Licence on an ‘as is’ basis and without warranties of any kind concerning the Work, including without limitation merchantability, fitness for a particular purpose, absence of defects or errors, accuracy, non-infringement of intellectual property rights other than copyright as stated in Article 6 of this Licence. This disclaimer of warranty is an essential part of the Licence and a condition for the grant of any rights to the Work. 8. Disclaimer of Liability Except in the cases of wilful misconduct or damages directly caused to natural persons, the Licensor will in no event be liable for any direct or indirect, material or moral, damages of any kind, arising out of the Licence or of the use of the Work, including without limitation, damages for loss of goodwill, work stoppage, computer failure or malfunction, loss of data or any commercial damage, even if the Licensor has been advised of the possibility of such damage. However, the Licensor will be liable under statutory product liability laws as far such laws apply to the Work. 9. Additional agreements While distributing the Work, You may choose to conclude an additional agreement, defining obligations or services consistent with this Licence. However, if accepting obligations, You may act only on your own behalf and on your sole responsibility, not on behalf of the original Licensor or any other Contributor, and only if You agree to indemnify, defend, and hold each Contributor harmless for any liability incurred by, or claims asserted against such Contributor by the fact You have accepted any warranty or additional liability. 10. Acceptance of the Licence The provisions of this Licence can be accepted by clicking on an icon ‘I agree’ placed under the bottom of a window displaying the text of this Licence or by affirming consent in any other similar way, in accordance with the rules of applicable law. Clicking on that icon indicates your clear and irrevocable acceptance of this Licence and all of its terms and conditions. Similarly, you irrevocably accept this Licence and all of its terms and conditions by exercising any rights granted to You by Article 2 of this Licence, such as the use of the Work, the creation by You of a Derivative Work or the Distribution or Communication by You of the Work or copies thereof. 11. Information to the public In case of any Distribution or Communication of the Work by means of electronic communication by You (for example, by offering to download the Work from a remote location) the distribution channel or media (for example, a website) must at least provide to the public the information requested by the applicable law regarding the Licensor, the Licence and the way it may be accessible, concluded, stored and reproduced by the Licensee. 12. Termination of the Licence The Licence and the rights granted hereunder will terminate automatically upon any breach by the Licensee of the terms of the Licence. Such a termination will not terminate the licences of any person who has received the Work from the Licensee under the Licence, provided such persons remain in full compliance with the Licence. 13. Miscellaneous Without prejudice of Article 9 above, the Licence represents the complete agreement between the Parties as to the Work. If any provision of the Licence is invalid or unenforceable under applicable law, this will not affect the validity or enforceability of the Licence as a whole. Such provision will be construed or reformed so as necessary to make it valid and enforceable. The European Commission may publish other linguistic versions or new versions of this Licence or updated versions of the Appendix, so far this is required and reasonable, without reducing the scope of the rights granted by the Licence. New versions of the Licence will be published with a unique version number. All linguistic versions of this Licence, approved by the European Commission, have identical value. Parties can take advantage of the linguistic version of their choice. 14. Jurisdiction Without prejudice to specific agreement between parties, — any litigation resulting from the interpretation of this License, arising between the European Union institutions, bodies, offices or agencies, as a Licensor, and any Licensee, will be subject to the jurisdiction of the Court of Justice of the European Union, as laid down in article 272 of the Treaty on the Functioning of the European Union, — any litigation arising between other parties and resulting from the interpretation of this License, will be subject to the exclusive jurisdiction of the competent court where the Licensor resides or conducts its primary business. 15. Applicable Law Without prejudice to specific agreement between parties, — this Licence shall be governed by the law of the European Union Member State where the Licensor has his seat, resides or has his registered office, — this licence shall be governed by Belgian law if the Licensor has no seat, residence or registered office inside a European Union Member State. Appendix ‘Compatible Licences’ according to Article 5 EUPL are: — GNU General Public License (GPL) v. 2, v. 3 — GNU Affero General Public License (AGPL) v. 3 — Open Software License (OSL) v. 2.1, v. 3.0 — Eclipse Public License (EPL) v. 1.0 — CeCILL v. 2.0, v. 2.1 — Mozilla Public Licence (MPL) v. 2 — GNU Lesser General Public Licence (LGPL) v. 2.1, v. 3 — Creative Commons Attribution-ShareAlike v. 3.0 Unported (CC BY-SA 3.0) for works other than software — European Union Public Licence (EUPL) v. 1.1, v. 1.2 — Québec Free and Open-Source Licence — Reciprocity (LiLiQ-R) or Strong Reciprocity (LiLiQ-R+) The European Commission may update this Appendix to later versions of the above licences without producing a new version of the EUPL, as long as they provide the rights granted in Article 2 of this Licence and protect the covered Source Code from exclusive appropriation. All other changes or additions to this Appendix require the production of a new EUPL version. |
Added place/constplace/listroles.mustache.
> > > > > > > > | 1 2 3 4 5 6 7 8 | <nav> <header> <h1>Currently used roles</h1> </header> <ul> {{#Roles}}<li><a href="{{{URL}}}">{{Text}}</a></li> {{/Roles}}</ul> </nav> |
Added place/constplace/listtags.mustache.
> > > > > > > > > > | 1 2 3 4 5 6 7 8 9 10 | <nav> <header> <h1>Currently used tags</h1> <div class="zs-meta"> <a href="{{{ListTagsURL}}}">All</a>{{#MinCounts}}, <a href="{{{URL}}}">{{Count}}</a>{{/MinCounts}} </div> </header> {{#Tags}} <a href="{{{URL}}}" style="font-size:{{Size}}%">{{Name}}</a><sup>{{Count}}</sup> {{/Tags}} </nav> |
Added place/constplace/listzettel.mustache.
> > > > > > > > > > > > > > > > > > > | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 | <nav> <header> <h1>{{Title}}</h1> </header> <ul> {{#Metas}}<li><a href="{{{URL}}}">{{{Text}}}</a></li> {{/Metas}}</ul> {{#HasPrevNext}} <p> {{#HasPrev}} <a href="{{{PrevURL}}}" rel="prev">Prev</a> {{#HasNext}},{{/HasNext}} {{/HasPrev}} {{#HasNext}} <a href="{{{NextURL}}}" rel="next">Next</a> {{/HasNext}} </p> {{/HasPrevNext}} </nav> |
Added place/constplace/login.mustache.
> > > > > > > > > > > > > > > > > > > | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 | <article> <header> <h1>{{Title}}</h1> </header> {{#Retry}} <div class="zs-indication zs-error">Wrong user name / password. Try again.</div> {{/Retry}} <form method="POST" action="?_format=html"> <div> <label for="username">User name</label> <input class="zs-input" type="text" id="username" name="username" placeholder="Your user name.." autofocus> </div> <div> <label for="password">Password</label> <input class="zs-input" type="password" id="password" name="password" placeholder="Your password.."> </div> <input class="zs-button" type="submit" value="Login"> </form> </article> |
Added place/constplace/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 place/constplace/rename.mustache.
> > > > > > > > > > > > > > > > > > > | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 | <article> <header> <h1>Rename Zettel {{.Zid}}</h1> </header> <p>Do you really want to rename this zettel?</p> <form method="POST"> <div> <label for="newid">New zettel id</label> <input class="zs-input" type="text" id="newzid" name="newzid" placeholder="ZID.." value="{{Zid}}" autofocus> </div> <input type="hidden" id="curzid" name="curzid" value="{{Zid}}"> <input class="zs-button" type="submit" value="Rename"> </form> <dl> {{#MetaPairs}} <dt>{{Key}}:</dt><dd>{{Value}}</dd> {{/MetaPairs}} </dl> </article> |
Added place/constplace/zettel.mustache.
> > > > > > > > > > > > > > > > > > > > > > > > > > > > | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 | <article> <header> <h1>{{{HTMLTitle}}}</h1> <div class="zs-meta"> {{#CanWrite}}<a href="{{{EditURL}}}">Edit</a> ·{{/CanWrite}} {{Zid}} · <a href="{{{InfoURL}}}">Info</a> · (<a href="{{{RoleURL}}}">{{RoleText}}</a>) {{#HasTags}}· {{#Tags}} <a href="{{{URL}}}">{{Text}}</a>{{/Tags}}{{/HasTags}} {{#CanCopy}}· <a href="{{{CopyURL}}}">Copy</a>{{/CanCopy}} {{#CanFolge}}· <a href="{{{FolgeURL}}}">Folge</a>{{/CanFolge}} {{#FolgeRefs}}<br>Folge: {{{FolgeRefs}}}{{/FolgeRefs}} {{#PrecursorRefs}}<br>Precursor: {{{PrecursorRefs}}}{{/PrecursorRefs}} {{#HasExtURL}}<br>URL: <a href="{{{ExtURL}}}"{{{ExtNewWindow}}}>{{ExtURL}}</a>{{/HasExtURL}} </div> </header> {{{Content}}} {{#HasBackLinks}} <details> <summary>Additional links to this zettel</summary> <ul> {{#BackLinks}} <li><a href="{{{URL}}}">{{Text}}</a></li> {{/BackLinks}} </ul> </details> {{/HasBackLinks}} </article> |
Added place/dirplace/directory/directory.go.
> > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 | //----------------------------------------------------------------------------- // Copyright (c) 2020-2021 Detlef Stern // // This file is part of zettelstore. // // Zettelstore is licensed under the latest version of the EUPL (European Union // Public License). Please see file LICENSE.txt for your rights and obligations // under this license. //----------------------------------------------------------------------------- // Package directory manages the directory interface of a dirstore. package directory import "zettelstore.de/z/domain/id" // Service is the interface of a directory service. type Service interface { Start() error Stop() error NumEntries() (int, error) GetEntries() ([]*Entry, error) GetEntry(zid id.Zid) (*Entry, error) GetNew() (*Entry, error) UpdateEntry(entry *Entry) error RenameEntry(curEntry, newEntry *Entry) error DeleteEntry(zid id.Zid) error } // MetaSpec defines all possibilities where meta data can be stored. type MetaSpec int // Constants for MetaSpec const ( _ MetaSpec = iota MetaSpecNone // no meta information MetaSpecFile // meta information is in meta file MetaSpecHeader // meta information is in header ) // Entry stores everything for a directory entry. type Entry struct { Zid id.Zid MetaSpec MetaSpec // location of meta information MetaPath string // file path of meta information ContentPath string // file path of zettel content ContentExt string // (normalized) file extension of zettel content Duplicates bool // multiple content files } // IsValid checks whether the entry is valid. func (e *Entry) IsValid() bool { return e != nil && e.Zid.IsValid() } |
Added place/dirplace/dirplace.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 | //----------------------------------------------------------------------------- // Copyright (c) 2020-2021 Detlef Stern // // This file is part of zettelstore. // // Zettelstore is licensed under the latest version of the EUPL (European Union // Public License). Please see file LICENSE.txt for your rights and obligations // under this license. //----------------------------------------------------------------------------- // Package dirplace provides a directory-based zettel place. package dirplace import ( "context" "errors" "net/url" "os" "path/filepath" "strconv" "strings" "sync" "time" "zettelstore.de/z/domain" "zettelstore.de/z/domain/id" "zettelstore.de/z/domain/meta" "zettelstore.de/z/place" "zettelstore.de/z/place/dirplace/directory" "zettelstore.de/z/place/fileplace" "zettelstore.de/z/place/manager" "zettelstore.de/z/search" ) func init() { manager.Register("dir", func(u *url.URL, cdata *manager.ConnectData) (place.ManagedPlace, error) { path := getDirPath(u) if _, err := os.Stat(path); errors.Is(err, os.ErrNotExist) { return nil, err } dirSrvSpec, defWorker, maxWorker := getDirSrvInfo(u.Query().Get("type")) dp := dirPlace{ location: u.String(), readonly: getQueryBool(u, "readonly"), cdata: *cdata, dir: path, dirRescan: time.Duration(getQueryInt(u, "rescan", 60, 3600, 30*24*60*60)) * time.Second, dirSrvSpec: dirSrvSpec, fSrvs: uint32(getQueryInt(u, "worker", 1, defWorker, maxWorker)), } return &dp, nil }) } type directoryServiceSpec int const ( _ directoryServiceSpec = iota dirSrvAny dirSrvSimple dirSrvNotify ) func getDirPath(u *url.URL) string { if u.Opaque != "" { return filepath.Clean(u.Opaque) } return filepath.Clean(u.Path) } func getQueryBool(u *url.URL, key string) bool { _, ok := u.Query()[key] return ok } func getQueryInt(u *url.URL, key string, min, def, max int) int { sVal := u.Query().Get(key) if sVal == "" { return def } iVal, err := strconv.Atoi(sVal) if err != nil { return def } if iVal < min { return min } if iVal > max { return max } return iVal } // dirPlace uses a directory to store zettel as files. type dirPlace struct { location string readonly bool cdata manager.ConnectData dir string dirRescan time.Duration dirSrvSpec directoryServiceSpec dirSrv directory.Service mustNotify bool fSrvs uint32 fCmds []chan fileCmd mxCmds sync.RWMutex } func (dp *dirPlace) Location() string { return dp.location } func (dp *dirPlace) Start(ctx context.Context) error { dp.mxCmds.Lock() dp.fCmds = make([]chan fileCmd, 0, dp.fSrvs) for i := uint32(0); i < dp.fSrvs; i++ { cc := make(chan fileCmd) go fileService(i, cc) dp.fCmds = append(dp.fCmds, cc) } dp.setupDirService() dp.mxCmds.Unlock() if dp.dirSrv == nil { panic("No directory service") } return dp.dirSrv.Start() } func (dp *dirPlace) Stop(ctx context.Context) error { dirSrv := dp.dirSrv dp.dirSrv = nil err := dirSrv.Stop() for _, c := range dp.fCmds { close(c) } return err } func (dp *dirPlace) notifyChanged(reason place.UpdateReason, zid id.Zid) { if dp.mustNotify { if chci := dp.cdata.Notify; chci != nil { chci <- place.UpdateInfo{Reason: reason, Zid: zid} } } } func (dp *dirPlace) getFileChan(zid id.Zid) chan fileCmd { // Based on https://en.wikipedia.org/wiki/Fowler%E2%80%93Noll%E2%80%93Vo_hash_function var sum uint32 = 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 *dirPlace) CanCreateZettel(ctx context.Context) bool { return !dp.readonly } func (dp *dirPlace) CreateZettel(ctx context.Context, zettel domain.Zettel) (id.Zid, error) { if dp.readonly { return id.Invalid, place.ErrReadOnly } entry, err := dp.dirSrv.GetNew() if err != nil { return id.Invalid, err } meta := zettel.Meta meta.Zid = entry.Zid dp.updateEntryFromMeta(entry, meta) err = setZettel(dp, entry, zettel) if err == nil { dp.dirSrv.UpdateEntry(entry) } dp.notifyChanged(place.OnUpdate, meta.Zid) return meta.Zid, err } func (dp *dirPlace) GetZettel(ctx context.Context, zid id.Zid) (domain.Zettel, error) { entry, err := dp.dirSrv.GetEntry(zid) if err != nil || !entry.IsValid() { return domain.Zettel{}, place.ErrNotFound } m, c, err := getMetaContent(dp, entry, zid) if err != nil { return domain.Zettel{}, err } dp.cleanupMeta(ctx, m) zettel := domain.Zettel{Meta: m, Content: domain.NewContent(c)} return zettel, nil } func (dp *dirPlace) GetMeta(ctx context.Context, zid id.Zid) (*meta.Meta, error) { entry, err := dp.dirSrv.GetEntry(zid) if err != nil || !entry.IsValid() { return nil, place.ErrNotFound } m, err := getMeta(dp, entry, zid) if err != nil { return nil, err } dp.cleanupMeta(ctx, m) return m, nil } func (dp *dirPlace) FetchZids(ctx context.Context) (id.Set, error) { entries, err := dp.dirSrv.GetEntries() if err != nil { return nil, err } result := id.NewSetCap(len(entries)) for _, entry := range entries { result[entry.Zid] = true } return result, nil } func (dp *dirPlace) SelectMeta(ctx context.Context, match search.MetaMatchFunc) (res []*meta.Meta, err error) { entries, err := dp.dirSrv.GetEntries() if err != nil { return nil, err } res = make([]*meta.Meta, 0, len(entries)) // The following loop could be parallelized if needed for performance. for _, entry := range entries { m, err1 := getMeta(dp, entry, entry.Zid) err = err1 if err != nil { continue } dp.cleanupMeta(ctx, m) dp.cdata.Enricher.Enrich(ctx, m) if match(m) { res = append(res, m) } } if err != nil { return nil, err } return res, nil } func (dp *dirPlace) CanUpdateZettel(ctx context.Context, zettel domain.Zettel) bool { return !dp.readonly } func (dp *dirPlace) UpdateZettel(ctx context.Context, zettel domain.Zettel) error { if dp.readonly { return place.ErrReadOnly } meta := zettel.Meta if !meta.Zid.IsValid() { return &place.ErrInvalidID{Zid: meta.Zid} } entry, err := dp.dirSrv.GetEntry(meta.Zid) if err != nil { return err } if !entry.IsValid() { // Existing zettel, but new in this place. entry = &directory.Entry{Zid: meta.Zid} dp.updateEntryFromMeta(entry, meta) } else if entry.MetaSpec == directory.MetaSpecNone { defaultMeta := fileplace.CalcDefaultMeta(entry.Zid, entry.ContentExt) if !meta.Equal(defaultMeta, true) { dp.updateEntryFromMeta(entry, meta) dp.dirSrv.UpdateEntry(entry) } } err = setZettel(dp, entry, zettel) if err == nil { dp.notifyChanged(place.OnUpdate, meta.Zid) } return err } func (dp *dirPlace) updateEntryFromMeta(entry *directory.Entry, meta *meta.Meta) { entry.MetaSpec, entry.ContentExt = dp.calcSpecExt(meta) basePath := filepath.Join(dp.dir, entry.Zid.String()) if entry.MetaSpec == directory.MetaSpecFile { entry.MetaPath = basePath + ".meta" } entry.ContentPath = basePath + "." + entry.ContentExt entry.Duplicates = false } func (dp *dirPlace) calcSpecExt(m *meta.Meta) (directory.MetaSpec, string) { if m.YamlSep { return directory.MetaSpecHeader, "zettel" } syntax := m.GetDefault(meta.KeySyntax, "bin") switch syntax { case meta.ValueSyntaxNone, meta.ValueSyntaxZmk: return directory.MetaSpecHeader, "zettel" } for _, s := range dp.cdata.Config.GetZettelFileSyntax() { if s == syntax { return directory.MetaSpecHeader, "zettel" } } return directory.MetaSpecFile, syntax } func (dp *dirPlace) AllowRenameZettel(ctx context.Context, zid id.Zid) bool { return !dp.readonly } func (dp *dirPlace) RenameZettel(ctx context.Context, curZid, newZid id.Zid) error { if dp.readonly { return place.ErrReadOnly } if curZid == newZid { return nil } curEntry, err := dp.dirSrv.GetEntry(curZid) if err != nil || !curEntry.IsValid() { return place.ErrNotFound } // Check whether zettel with new ID already exists in this place if _, err = dp.GetMeta(ctx, newZid); err == nil { return &place.ErrInvalidID{Zid: newZid} } oldMeta, oldContent, err := getMetaContent(dp, curEntry, curZid) if err != nil { return err } newEntry := directory.Entry{ Zid: newZid, MetaSpec: curEntry.MetaSpec, MetaPath: renamePath(curEntry.MetaPath, curZid, newZid), ContentPath: renamePath(curEntry.ContentPath, curZid, newZid), ContentExt: curEntry.ContentExt, } if err = dp.dirSrv.RenameEntry(curEntry, &newEntry); err != nil { return err } oldMeta.Zid = newZid newZettel := domain.Zettel{Meta: oldMeta, Content: domain.NewContent(oldContent)} if err = setZettel(dp, &newEntry, newZettel); err != nil { // "Rollback" rename. No error checking... dp.dirSrv.RenameEntry(&newEntry, curEntry) return err } err = deleteZettel(dp, curEntry, curZid) if err == nil { dp.notifyChanged(place.OnDelete, curZid) dp.notifyChanged(place.OnUpdate, newZid) } return err } func (dp *dirPlace) CanDeleteZettel(ctx context.Context, zid id.Zid) bool { if dp.readonly { return false } entry, err := dp.dirSrv.GetEntry(zid) return err == nil && entry.IsValid() } func (dp *dirPlace) DeleteZettel(ctx context.Context, zid id.Zid) error { if dp.readonly { return place.ErrReadOnly } entry, err := dp.dirSrv.GetEntry(zid) if err != nil || !entry.IsValid() { return nil } dp.dirSrv.DeleteEntry(zid) err = deleteZettel(dp, entry, zid) if err == nil { dp.notifyChanged(place.OnDelete, zid) } return err } func (dp *dirPlace) ReadStats(st *place.ManagedPlaceStats) { st.ReadOnly = dp.readonly st.Zettel, _ = dp.dirSrv.NumEntries() } func (dp *dirPlace) cleanupMeta(ctx context.Context, m *meta.Meta) { if role, ok := m.Get(meta.KeyRole); !ok || role == "" { m.Set(meta.KeyRole, dp.cdata.Config.GetDefaultRole()) } if syntax, ok := m.Get(meta.KeySyntax); !ok || syntax == "" { m.Set(meta.KeySyntax, dp.cdata.Config.GetDefaultSyntax()) } } func renamePath(path string, curID, newID id.Zid) string { dir, file := filepath.Split(path) if cur := curID.String(); strings.HasPrefix(file, cur) { file = newID.String() + file[len(cur):] return filepath.Join(dir, file) } return path } |
Added place/dirplace/makedir.go.
> > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 | //----------------------------------------------------------------------------- // Copyright (c) 2021 Detlef Stern // // This file is part of zettelstore. // // Zettelstore is licensed under the latest version of the EUPL (European Union // Public License). Please see file LICENSE.txt for your rights and obligations // under this license. //----------------------------------------------------------------------------- // Package dirplace provides a directory-based zettel place. package dirplace import ( "zettelstore.de/z/kernel" "zettelstore.de/z/place/dirplace/notifydir" "zettelstore.de/z/place/dirplace/simpledir" ) func getDirSrvInfo(dirType string) (directoryServiceSpec, int, int) { for count := 0; count < 2; count++ { switch dirType { case kernel.PlaceDirTypeNotify: return dirSrvNotify, 7, 1499 case kernel.PlaceDirTypeSimple: return dirSrvSimple, 1, 1 default: dirType = kernel.Main.GetConfig(kernel.PlaceService, kernel.PlaceDefaultDirType).(string) } } panic("unable to set default dir place type: " + dirType) } func (dp *dirPlace) setupDirService() { switch dp.dirSrvSpec { case dirSrvSimple: dp.dirSrv = simpledir.NewService(dp.dir) dp.mustNotify = true default: dp.dirSrv = notifydir.NewService(dp.dir, dp.dirRescan, dp.cdata.Notify) dp.mustNotify = false } } |
Added place/dirplace/notifydir/notifydir.go.
> > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 | //----------------------------------------------------------------------------- // Copyright (c) 2020-2021 Detlef Stern // // This file is part of zettelstore. // // Zettelstore is licensed under the latest version of the EUPL (European Union // Public License). Please see file LICENSE.txt for your rights and obligations // under this license. //----------------------------------------------------------------------------- // Package notifydir manages the notified directory part of a dirstore. package notifydir import ( "time" "zettelstore.de/z/domain/id" "zettelstore.de/z/place" "zettelstore.de/z/place/dirplace/directory" ) // notifyService specifies a directory scan service. type notifyService struct { dirPath string rescanTime time.Duration done chan struct{} cmds chan dirCmd infos chan<- place.UpdateInfo } // NewService creates a new directory service. func NewService(directoryPath string, rescanTime time.Duration, chci chan<- place.UpdateInfo) directory.Service { srv := ¬ifyService{ dirPath: directoryPath, rescanTime: rescanTime, cmds: make(chan dirCmd), infos: chci, } return srv } // Start makes the directory service operational. func (srv *notifyService) Start() error { tick := make(chan struct{}) rawEvents := make(chan *fileEvent) events := make(chan *fileEvent) ready := make(chan int) go srv.directoryService(events, ready) go collectEvents(events, rawEvents) go watchDirectory(srv.dirPath, rawEvents, tick) if srv.done != nil { panic("src.done already set") } srv.done = make(chan struct{}) go ping(tick, srv.rescanTime, srv.done) <-ready return nil } // Stop stops the directory service. func (srv *notifyService) Stop() error { close(srv.done) srv.done = nil return nil } func (srv *notifyService) notifyChange(reason place.UpdateReason, zid id.Zid) { if chci := srv.infos; chci != nil { chci <- place.UpdateInfo{Reason: reason, Zid: zid} } } // NumEntries returns the number of managed zettel. func (srv *notifyService) NumEntries() (int, error) { resChan := make(chan resNumEntries) srv.cmds <- &cmdNumEntries{resChan} return <-resChan, nil } // GetEntries returns an unsorted list of all current directory entries. func (srv *notifyService) GetEntries() ([]*directory.Entry, error) { resChan := make(chan resGetEntries) srv.cmds <- &cmdGetEntries{resChan} return <-resChan, nil } // GetEntry returns the entry with the specified zettel id. If there is no such // zettel id, an empty entry is returned. func (srv *notifyService) GetEntry(zid id.Zid) (*directory.Entry, error) { resChan := make(chan resGetEntry) srv.cmds <- &cmdGetEntry{zid, resChan} return <-resChan, nil } // GetNew returns an entry with a new zettel id. func (srv *notifyService) GetNew() (*directory.Entry, error) { resChan := make(chan resNewEntry) srv.cmds <- &cmdNewEntry{resChan} result := <-resChan return result.entry, result.err } // UpdateEntry notifies the directory of an updated entry. func (srv *notifyService) UpdateEntry(entry *directory.Entry) error { resChan := make(chan struct{}) srv.cmds <- &cmdUpdateEntry{entry, resChan} <-resChan return nil } // RenameEntry notifies the directory of an renamed entry. func (srv *notifyService) RenameEntry(curEntry, newEntry *directory.Entry) error { resChan := make(chan resRenameEntry) srv.cmds <- &cmdRenameEntry{curEntry, newEntry, resChan} return <-resChan } // DeleteEntry removes a zettel id from the directory of entries. func (srv *notifyService) DeleteEntry(zid id.Zid) error { resChan := make(chan struct{}) srv.cmds <- &cmdDeleteEntry{zid, resChan} <-resChan return nil } |
Added place/dirplace/notifydir/service.go.
> > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 | //----------------------------------------------------------------------------- // Copyright (c) 2020-2021 Detlef Stern // // This file is part of zettelstore. // // Zettelstore is licensed under the latest version of the EUPL (European Union // Public License). Please see file LICENSE.txt for your rights and obligations // under this license. //----------------------------------------------------------------------------- // Package notifydir manages the notified directory part of a dirstore. package notifydir import ( "log" "time" "zettelstore.de/z/domain/id" "zettelstore.de/z/place" "zettelstore.de/z/place/dirplace/directory" ) // ping sends every tick a signal to reload the directory list func ping(tick chan<- struct{}, rescanTime time.Duration, done <-chan struct{}) { ticker := time.NewTicker(rescanTime) defer close(tick) for { select { case _, ok := <-ticker.C: if !ok { return } tick <- struct{}{} case _, ok := <-done: if !ok { ticker.Stop() return } } } } func newEntry(ev *fileEvent) *directory.Entry { de := new(directory.Entry) de.Zid = ev.zid updateEntry(de, ev) return de } func updateEntry(de *directory.Entry, ev *fileEvent) { if ev.ext == "meta" { de.MetaSpec = directory.MetaSpecFile de.MetaPath = ev.path return } if de.ContentExt != "" && de.ContentExt != ev.ext { de.Duplicates = true return } if de.MetaSpec != directory.MetaSpecFile { if ev.ext == "zettel" { de.MetaSpec = directory.MetaSpecHeader } else { de.MetaSpec = directory.MetaSpecNone } } de.ContentPath = ev.path de.ContentExt = ev.ext } type dirMap map[id.Zid]*directory.Entry func dirMapUpdate(dm dirMap, ev *fileEvent) { de := dm[ev.zid] if de == nil { dm[ev.zid] = newEntry(ev) return } updateEntry(de, ev) } func deleteFromMap(dm dirMap, ev *fileEvent) { if ev.ext == "meta" { if entry, ok := dm[ev.zid]; ok { if entry.MetaSpec == directory.MetaSpecFile { entry.MetaSpec = directory.MetaSpecNone return } } } delete(dm, ev.zid) } // directoryService is the main service. func (srv *notifyService) directoryService(events <-chan *fileEvent, ready chan<- int) { curMap := make(dirMap) var newMap dirMap for { select { case ev, ok := <-events: if !ok { return } switch ev.status { case fileStatusReloadStart: newMap = make(dirMap) case fileStatusReloadEnd: curMap = newMap newMap = nil if ready != nil { ready <- len(curMap) close(ready) ready = nil } srv.notifyChange(place.OnReload, id.Invalid) case fileStatusError: log.Println("DIRPLACE", "ERROR", ev.err) case fileStatusUpdate: srv.processFileUpdateEvent(ev, curMap, newMap) case fileStatusDelete: srv.processFileDeleteEvent(ev, curMap, newMap) } case cmd, ok := <-srv.cmds: if ok { cmd.run(curMap) } } } } func (srv *notifyService) processFileUpdateEvent(ev *fileEvent, curMap, newMap dirMap) { if newMap != nil { dirMapUpdate(newMap, ev) } else { dirMapUpdate(curMap, ev) srv.notifyChange(place.OnUpdate, ev.zid) } } func (srv *notifyService) processFileDeleteEvent(ev *fileEvent, curMap, newMap dirMap) { if newMap != nil { deleteFromMap(newMap, ev) } else { deleteFromMap(curMap, ev) srv.notifyChange(place.OnDelete, ev.zid) } } type dirCmd interface { run(m dirMap) } type cmdNumEntries struct { result chan<- resNumEntries } type resNumEntries = int func (cmd *cmdNumEntries) run(m dirMap) { cmd.result <- len(m) } type cmdGetEntries struct { result chan<- resGetEntries } type resGetEntries []*directory.Entry func (cmd *cmdGetEntries) run(m dirMap) { res := make([]*directory.Entry, len(m)) i := 0 for _, de := range m { entry := *de res[i] = &entry i++ } cmd.result <- res } type cmdGetEntry struct { zid id.Zid result chan<- resGetEntry } type resGetEntry = *directory.Entry func (cmd *cmdGetEntry) run(m dirMap) { entry := m[cmd.zid] if entry == nil { cmd.result <- nil } else { result := *entry cmd.result <- &result } } type cmdNewEntry struct { result chan<- resNewEntry } type resNewEntry struct { entry *directory.Entry err error } func (cmd *cmdNewEntry) run(m dirMap) { zid, err := place.GetNewZid(func(zid id.Zid) (bool, error) { _, ok := m[zid] return !ok, nil }) if err != nil { cmd.result <- resNewEntry{nil, err} return } entry := &directory.Entry{Zid: zid} m[zid] = entry cmd.result <- resNewEntry{&directory.Entry{Zid: zid}, nil} } type cmdUpdateEntry struct { entry *directory.Entry result chan<- struct{} } func (cmd *cmdUpdateEntry) run(m dirMap) { entry := *cmd.entry m[entry.Zid] = &entry cmd.result <- struct{}{} } type cmdRenameEntry struct { curEntry *directory.Entry newEntry *directory.Entry result chan<- resRenameEntry } type resRenameEntry = error func (cmd *cmdRenameEntry) run(m dirMap) { newEntry := *cmd.newEntry newZid := newEntry.Zid if _, found := m[newZid]; found { cmd.result <- &place.ErrInvalidID{Zid: newZid} return } delete(m, cmd.curEntry.Zid) m[newZid] = &newEntry cmd.result <- nil } type cmdDeleteEntry struct { zid id.Zid result chan<- struct{} } func (cmd *cmdDeleteEntry) run(m dirMap) { delete(m, cmd.zid) cmd.result <- struct{}{} } |
Added place/dirplace/notifydir/watch.go.
> > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 | //----------------------------------------------------------------------------- // Copyright (c) 2020-2021 Detlef Stern // // This file is part of zettelstore. // // Zettelstore is licensed under the latest version of the EUPL (European Union // Public License). Please see file LICENSE.txt for your rights and obligations // under this license. //----------------------------------------------------------------------------- // Package notifydir manages the notified directory part of a dirstore. package notifydir import ( "os" "path/filepath" "regexp" "time" "github.com/fsnotify/fsnotify" "zettelstore.de/z/domain/id" ) var validFileName = regexp.MustCompile(`^(\d{14}).*(\.(.+))$`) func matchValidFileName(name string) []string { return validFileName.FindStringSubmatch(name) } type fileStatus int const ( fileStatusNone fileStatus = iota fileStatusReloadStart fileStatusReloadEnd fileStatusError fileStatusUpdate fileStatusDelete ) type fileEvent struct { status fileStatus path string // Full file path zid id.Zid ext string // File extension err error // Error if Status == fileStatusError } type sendResult int const ( sendDone sendResult = iota sendReload sendExit ) func watchDirectory(directory string, events chan<- *fileEvent, tick <-chan struct{}) { defer close(events) var watcher *fsnotify.Watcher defer func() { if watcher != nil { watcher.Close() } }() sendEvent := func(ev *fileEvent) sendResult { select { case events <- ev: case _, ok := <-tick: if ok { return sendReload } return sendExit } return sendDone } sendError := func(err error) sendResult { return sendEvent(&fileEvent{status: fileStatusError, err: err}) } sendFileEvent := func(status fileStatus, path string, match []string) sendResult { zid, err := id.Parse(match[1]) if err != nil { return sendDone } event := &fileEvent{ status: status, path: path, zid: zid, ext: match[3], } return sendEvent(event) } reloadStartEvent := &fileEvent{status: fileStatusReloadStart} reloadEndEvent := &fileEvent{status: fileStatusReloadEnd} reloadFiles := func() bool { entries, err := os.ReadDir(directory) if err != nil { if res := sendError(err); res != sendDone { return res == sendReload } return true } if res := sendEvent(reloadStartEvent); res != sendDone { return res == sendReload } if watcher != nil { watcher.Close() } watcher, err = fsnotify.NewWatcher() if err != nil { if res := sendError(err); res != sendDone { return res == sendReload } } for _, entry := range entries { if entry.IsDir() { continue } if info, err1 := entry.Info(); err1 != nil || !info.Mode().IsRegular() { continue } name := entry.Name() match := matchValidFileName(name) if len(match) > 0 { path := filepath.Join(directory, name) if res := sendFileEvent(fileStatusUpdate, path, match); res != sendDone { return res == sendReload } } } if watcher != nil { err = watcher.Add(directory) if err != nil { if res := sendError(err); res != sendDone { return res == sendReload } } } if res := sendEvent(reloadEndEvent); res != sendDone { return res == sendReload } return true } handleEvents := func() bool { const createOps = fsnotify.Create | fsnotify.Write const deleteOps = fsnotify.Remove | fsnotify.Rename for { select { case wevent, ok := <-watcher.Events: if !ok { return false } path := filepath.Clean(wevent.Name) match := matchValidFileName(filepath.Base(path)) if len(match) == 0 { continue } if wevent.Op&createOps != 0 { if fi, err := os.Lstat(path); err != nil || !fi.Mode().IsRegular() { continue } if res := sendFileEvent( fileStatusUpdate, path, match); res != sendDone { return res == sendReload } } if wevent.Op&deleteOps != 0 { if res := sendFileEvent( fileStatusDelete, path, match); res != sendDone { return res == sendReload } } case err, ok := <-watcher.Errors: if !ok { return false } if res := sendError(err); res != sendDone { return res == sendReload } case _, ok := <-tick: return ok } } } for { if !reloadFiles() { return } if watcher == nil { if _, ok := <-tick; !ok { return } } else { if !handleEvents() { return } } } } func sendCollectedEvents(out chan<- *fileEvent, events []*fileEvent) { for _, ev := range events { if ev.status != fileStatusNone { out <- ev } } } func addEvent(events []*fileEvent, ev *fileEvent) []*fileEvent { switch ev.status { case fileStatusNone: return events case fileStatusReloadStart: events = events[0:0] case fileStatusUpdate, fileStatusDelete: if len(events) > 0 && mergeEvents(events, ev) { return events } } return append(events, ev) } func mergeEvents(events []*fileEvent, ev *fileEvent) bool { for i := len(events) - 1; i >= 0; i-- { oev := events[i] switch oev.status { case fileStatusReloadStart, fileStatusReloadEnd: return false case fileStatusUpdate, fileStatusDelete: if ev.path == oev.path { if ev.status == oev.status { return true } oev.status = fileStatusNone return false } } } return false } func collectEvents(out chan<- *fileEvent, in <-chan *fileEvent) { defer close(out) var sendTime time.Time sendTimeSet := false ticker := time.NewTicker(500 * time.Millisecond) defer ticker.Stop() events := make([]*fileEvent, 0, 32) buffer := false for { select { case ev, ok := <-in: if !ok { sendCollectedEvents(out, events) return } if ev.status == fileStatusReloadStart { buffer = false events = events[0:0] } if buffer { if !sendTimeSet { sendTime = time.Now().Add(1500 * time.Millisecond) sendTimeSet = true } events = addEvent(events, ev) if len(events) > 1024 { sendCollectedEvents(out, events) events = events[0:0] sendTimeSet = false } continue } out <- ev if ev.status == fileStatusReloadEnd { buffer = true } case now := <-ticker.C: if sendTimeSet && now.After(sendTime) { sendCollectedEvents(out, events) events = events[0:0] sendTimeSet = false } } } } |
Added place/dirplace/notifydir/watch_test.go.
> > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 | //----------------------------------------------------------------------------- // Copyright (c) 2020 Detlef Stern // // This file is part of zettelstore. // // Zettelstore is licensed under the latest version of the EUPL (European Union // Public License). Please see file LICENSE.txt for your rights and obligations // under this license. //----------------------------------------------------------------------------- // Package notifydir manages the notified directory part of a dirstore. package notifydir import "testing" func sameStringSlices(sl1, sl2 []string) bool { if len(sl1) != len(sl2) { return false } for i := 0; i < len(sl1); i++ { if sl1[i] != sl2[i] { return false } } return true } func TestMatchValidFileName(t *testing.T) { testcases := []struct { name string exp []string }{ {"", []string{}}, {".txt", []string{}}, {"12345678901234.txt", []string{"12345678901234", ".txt", "txt"}}, {"12345678901234abc.txt", []string{"12345678901234", ".txt", "txt"}}, {"12345678901234.abc.txt", []string{"12345678901234", ".txt", "txt"}}, } for i, tc := range testcases { got := matchValidFileName(tc.name) if len(got) == 0 { if len(tc.exp) > 0 { t.Errorf("TC=%d, name=%q, exp=%v, got=%v", i, tc.name, tc.exp, got) } } else { if got[0] != tc.name { t.Errorf("TC=%d, name=%q, got=%v", i, tc.name, got) } if !sameStringSlices(got[1:], tc.exp) { t.Errorf("TC=%d, name=%q, exp=%v, got=%v", i, tc.name, tc.exp, got) } } } } |
Added place/dirplace/service.go.
> > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 | //----------------------------------------------------------------------------- // Copyright (c) 2020-2021 Detlef Stern // // This file is part of zettelstore. // // Zettelstore is licensed under the latest version of the EUPL (European Union // Public License). Please see file LICENSE.txt for your rights and obligations // under this license. //----------------------------------------------------------------------------- // Package dirplace provides a directory-based zettel place. package dirplace import ( "os" "zettelstore.de/z/domain" "zettelstore.de/z/domain/id" "zettelstore.de/z/domain/meta" "zettelstore.de/z/input" "zettelstore.de/z/place/dirplace/directory" "zettelstore.de/z/place/fileplace" ) func fileService(num uint32, cmds <-chan fileCmd) { for cmd := range cmds { cmd.run() } } type fileCmd interface { run() } // COMMAND: getMeta ---------------------------------------- // // Retrieves the meta data from a zettel. func getMeta(dp *dirPlace, entry *directory.Entry, zid id.Zid) (*meta.Meta, error) { rc := make(chan resGetMeta) dp.getFileChan(zid) <- &fileGetMeta{entry, rc} res := <-rc close(rc) return res.meta, res.err } type fileGetMeta struct { entry *directory.Entry rc chan<- resGetMeta } type resGetMeta struct { meta *meta.Meta err error } func (cmd *fileGetMeta) run() { entry := cmd.entry var m *meta.Meta var err error switch entry.MetaSpec { case directory.MetaSpecFile: m, err = parseMetaFile(entry.Zid, entry.MetaPath) case directory.MetaSpecHeader: m, _, err = parseMetaContentFile(entry.Zid, entry.ContentPath) default: m = fileplace.CalcDefaultMeta(entry.Zid, entry.ContentExt) } if err == nil { cmdCleanupMeta(m, entry) } cmd.rc <- resGetMeta{m, err} } // COMMAND: getMetaContent ---------------------------------------- // // Retrieves the meta data and the content of a zettel. func getMetaContent(dp *dirPlace, entry *directory.Entry, zid id.Zid) (*meta.Meta, string, error) { rc := make(chan resGetMetaContent) dp.getFileChan(zid) <- &fileGetMetaContent{entry, rc} res := <-rc close(rc) return res.meta, res.content, res.err } type fileGetMetaContent struct { entry *directory.Entry rc chan<- resGetMetaContent } type resGetMetaContent struct { meta *meta.Meta content string err error } func (cmd *fileGetMetaContent) run() { var m *meta.Meta var content string var err error entry := cmd.entry switch entry.MetaSpec { case directory.MetaSpecFile: m, err = parseMetaFile(entry.Zid, entry.MetaPath) if err == nil { content, err = readFileContent(entry.ContentPath) } case directory.MetaSpecHeader: m, content, err = parseMetaContentFile(entry.Zid, entry.ContentPath) default: m = fileplace.CalcDefaultMeta(entry.Zid, entry.ContentExt) content, err = readFileContent(entry.ContentPath) } if err == nil { cmdCleanupMeta(m, entry) } cmd.rc <- resGetMetaContent{m, content, err} } // COMMAND: setZettel ---------------------------------------- // // Writes a new or exsting zettel. func setZettel(dp *dirPlace, entry *directory.Entry, zettel domain.Zettel) error { rc := make(chan resSetZettel) dp.getFileChan(zettel.Meta.Zid) <- &fileSetZettel{entry, zettel, rc} err := <-rc close(rc) return err } type fileSetZettel struct { entry *directory.Entry zettel domain.Zettel rc chan<- resSetZettel } type resSetZettel = error func (cmd *fileSetZettel) run() { var err error switch cmd.entry.MetaSpec { case directory.MetaSpecFile: err = cmd.runMetaSpecFile() case directory.MetaSpecHeader: err = cmd.runMetaSpecHeader() case directory.MetaSpecNone: // TODO: if meta has some additional infos: write meta to new .meta; // update entry in dir err = writeFileContent(cmd.entry.ContentPath, cmd.zettel.Content.AsString()) default: panic("TODO: ???") } cmd.rc <- err } func (cmd *fileSetZettel) runMetaSpecFile() error { f, err := openFileWrite(cmd.entry.MetaPath) if err == nil { err = writeFileZid(f, cmd.zettel.Meta.Zid) if err == nil { _, err = cmd.zettel.Meta.Write(f, true) if err1 := f.Close(); err == nil { err = err1 } if err == nil { err = writeFileContent(cmd.entry.ContentPath, cmd.zettel.Content.AsString()) } } } return err } func (cmd *fileSetZettel) runMetaSpecHeader() error { f, err := openFileWrite(cmd.entry.ContentPath) if err == nil { err = writeFileZid(f, cmd.zettel.Meta.Zid) if err == nil { _, err = cmd.zettel.Meta.WriteAsHeader(f, true) if err == nil { _, err = f.WriteString(cmd.zettel.Content.AsString()) if err1 := f.Close(); err == nil { err = err1 } } } } return err } // COMMAND: deleteZettel ---------------------------------------- // // Deletes an existing zettel. func deleteZettel(dp *dirPlace, entry *directory.Entry, zid id.Zid) error { rc := make(chan resDeleteZettel) dp.getFileChan(zid) <- &fileDeleteZettel{entry, rc} err := <-rc close(rc) return err } type fileDeleteZettel struct { entry *directory.Entry rc chan<- resDeleteZettel } type resDeleteZettel = error func (cmd *fileDeleteZettel) run() { var err error switch cmd.entry.MetaSpec { case directory.MetaSpecFile: err1 := os.Remove(cmd.entry.MetaPath) err = os.Remove(cmd.entry.ContentPath) if err == nil { err = err1 } case directory.MetaSpecHeader: err = os.Remove(cmd.entry.ContentPath) case directory.MetaSpecNone: err = os.Remove(cmd.entry.ContentPath) default: panic("TODO: ???") } cmd.rc <- err } // Utility functions ---------------------------------------- func readFileContent(path string) (string, error) { data, err := os.ReadFile(path) if err != nil { return "", err } return string(data), nil } func parseMetaFile(zid id.Zid, path string) (*meta.Meta, error) { src, err := readFileContent(path) if err != nil { return nil, err } inp := input.NewInput(src) return meta.NewFromInput(zid, inp), nil } func parseMetaContentFile(zid id.Zid, path string) (*meta.Meta, string, error) { src, err := readFileContent(path) if err != nil { return nil, "", err } inp := input.NewInput(src) meta := meta.NewFromInput(zid, inp) return meta, src[inp.Pos:], nil } func cmdCleanupMeta(m *meta.Meta, entry *directory.Entry) { fileplace.CleanupMeta( m, entry.Zid, entry.ContentExt, entry.MetaSpec == directory.MetaSpecFile, entry.Duplicates, ) } func openFileWrite(path string) (*os.File, error) { return os.OpenFile(path, os.O_WRONLY|os.O_CREATE|os.O_TRUNC, 0600) } func writeFileZid(f *os.File, zid id.Zid) error { _, err := f.WriteString("id: ") if err == nil { _, err = f.Write(zid.Bytes()) if err == nil { _, err = f.WriteString("\n") } } return err } func writeFileContent(path string, content string) error { f, err := openFileWrite(path) if err == nil { _, err = f.WriteString(content) if err1 := f.Close(); err == nil { err = err1 } } return err } |
Added place/dirplace/simpledir/simpledir.go.
> > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 | //----------------------------------------------------------------------------- // Copyright (c) 2021 Detlef Stern // // This file is part of zettelstore. // // Zettelstore is licensed under the latest version of the EUPL (European Union // Public License). Please see file LICENSE.txt for your rights and obligations // under this license. //----------------------------------------------------------------------------- // Package simpledir manages the directory part of a dirstore. package simpledir import ( "os" "path/filepath" "regexp" "sync" "zettelstore.de/z/domain/id" "zettelstore.de/z/place" "zettelstore.de/z/place/dirplace/directory" ) // simpleService specifies a directory service without scanning. type simpleService struct { dirPath string mx sync.Mutex } // NewService creates a new directory service. func NewService(directoryPath string) directory.Service { return &simpleService{ dirPath: directoryPath, } } func (ss *simpleService) Start() error { ss.mx.Lock() defer ss.mx.Unlock() _, err := os.ReadDir(ss.dirPath) return err } func (ss *simpleService) Stop() error { return nil } func (ss *simpleService) NumEntries() (int, error) { ss.mx.Lock() defer ss.mx.Unlock() entries, err := ss.getEntries() if err == nil { return len(entries), nil } return 0, err } func (ss *simpleService) GetEntries() ([]*directory.Entry, error) { ss.mx.Lock() defer ss.mx.Unlock() entrySet, err := ss.getEntries() if err != nil { return nil, err } result := make([]*directory.Entry, 0, len(entrySet)) for _, entry := range entrySet { result = append(result, entry) } return result, nil } func (ss *simpleService) getEntries() (map[id.Zid]*directory.Entry, error) { dirEntries, err := os.ReadDir(ss.dirPath) if err != nil { return nil, err } entrySet := make(map[id.Zid]*directory.Entry) for _, dirEntry := range dirEntries { if dirEntry.IsDir() { continue } if info, err1 := dirEntry.Info(); err1 != nil || !info.Mode().IsRegular() { continue } name := dirEntry.Name() match := matchValidFileName(name) if len(match) == 0 { continue } zid, err := id.Parse(match[1]) if err != nil { continue } var entry *directory.Entry if e, ok := entrySet[zid]; ok { entry = e } else { entry = &directory.Entry{Zid: zid} entrySet[zid] = entry } updateEntry(entry, filepath.Join(ss.dirPath, name), match[3]) } return entrySet, nil } var validFileName = regexp.MustCompile(`^(\d{14}).*(\.(.+))$`) func matchValidFileName(name string) []string { return validFileName.FindStringSubmatch(name) } func updateEntry(entry *directory.Entry, path, ext string) { if ext == "meta" { entry.MetaSpec = directory.MetaSpecFile entry.MetaPath = path } else if entry.ContentExt != "" && entry.ContentExt != ext { entry.Duplicates = true } else { if entry.MetaSpec != directory.MetaSpecFile { if ext == "zettel" { entry.MetaSpec = directory.MetaSpecHeader } else { entry.MetaSpec = directory.MetaSpecNone } } entry.ContentPath = path entry.ContentExt = ext } } func (ss *simpleService) GetEntry(zid id.Zid) (*directory.Entry, error) { ss.mx.Lock() defer ss.mx.Unlock() return ss.getEntry(zid) } func (ss *simpleService) getEntry(zid id.Zid) (*directory.Entry, error) { pattern := filepath.Join(ss.dirPath, zid.String()) + "*.*" paths, err := filepath.Glob(pattern) if err != nil { return nil, err } if len(paths) == 0 { return nil, nil } entry := &directory.Entry{Zid: zid} for _, path := range paths { ext := filepath.Ext(path) if len(ext) > 0 && ext[0] == '.' { ext = ext[1:] } updateEntry(entry, path, ext) } return entry, nil } func (ss *simpleService) GetNew() (*directory.Entry, error) { ss.mx.Lock() defer ss.mx.Unlock() zid, err := place.GetNewZid(func(zid id.Zid) (bool, error) { entry, err := ss.getEntry(zid) if err != nil { return false, nil } return !entry.IsValid(), nil }) if err != nil { return nil, err } return &directory.Entry{Zid: zid}, nil } func (ss *simpleService) UpdateEntry(entry *directory.Entry) error { // Noting to to, since the actual file update is done by dirplace return nil } func (ss *simpleService) RenameEntry(curEntry, newEntry *directory.Entry) error { // Noting to to, since the actual file rename is done by dirplace return nil } func (ss *simpleService) DeleteEntry(zid id.Zid) error { // Noting to to, since the actual file delete is done by dirplace return nil } |
Added place/fileplace/fileplace.go.
> > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 | //----------------------------------------------------------------------------- // Copyright (c) 2021 Detlef Stern // // This file is part of zettelstore. // // Zettelstore is licensed under the latest version of the EUPL (European Union // Public License). Please see file LICENSE.txt for your rights and obligations // under this license. //----------------------------------------------------------------------------- // Package fileplace provides places that are stored in a file. package fileplace import ( "errors" "net/url" "path/filepath" "strings" "zettelstore.de/z/domain/id" "zettelstore.de/z/domain/meta" "zettelstore.de/z/place" "zettelstore.de/z/place/manager" ) func init() { manager.Register("file", func(u *url.URL, cdata *manager.ConnectData) (place.ManagedPlace, error) { path := getFilepathFromURL(u) ext := strings.ToLower(filepath.Ext(path)) if ext != ".zip" { return nil, errors.New("unknown extension '" + ext + "' in place URL: " + u.String()) } return &zipPlace{name: path, enricher: cdata.Enricher}, nil }) } func getFilepathFromURL(u *url.URL) string { name := u.Opaque if name == "" { name = u.Path } components := strings.Split(name, "/") fileName := filepath.Join(components...) if len(components) > 0 && components[0] == "" { return "/" + fileName } return fileName } var alternativeSyntax = map[string]string{ "htm": "html", } func calculateSyntax(ext string) string { ext = strings.ToLower(ext) if syntax, ok := alternativeSyntax[ext]; ok { return syntax } return ext } // CalcDefaultMeta returns metadata with default values for the given entry. func CalcDefaultMeta(zid id.Zid, ext string) *meta.Meta { m := meta.New(zid) m.Set(meta.KeyTitle, zid.String()) m.Set(meta.KeySyntax, calculateSyntax(ext)) return m } // CleanupMeta enhances the given metadata. func CleanupMeta(m *meta.Meta, zid id.Zid, ext string, inMeta, duplicates bool) { if title, ok := m.Get(meta.KeyTitle); !ok || title == "" { m.Set(meta.KeyTitle, zid.String()) } if inMeta { if syntax, ok := m.Get(meta.KeySyntax); !ok || syntax == "" { dm := CalcDefaultMeta(zid, ext) syntax, ok = dm.Get(meta.KeySyntax) if !ok { panic("Default meta must contain syntax") } m.Set(meta.KeySyntax, syntax) } } if duplicates { m.Set(meta.KeyDuplicates, meta.ValueTrue) } } |
Added place/fileplace/zipplace.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 | //----------------------------------------------------------------------------- // Copyright (c) 2021 Detlef Stern // // This file is part of zettelstore. // // Zettelstore is licensed under the latest version of the EUPL (European Union // Public License). Please see file LICENSE.txt for your rights and obligations // under this license. //----------------------------------------------------------------------------- // Package fileplace provides places that are stored in a file. package fileplace import ( "archive/zip" "context" "io" "regexp" "strings" "zettelstore.de/z/domain" "zettelstore.de/z/domain/id" "zettelstore.de/z/domain/meta" "zettelstore.de/z/input" "zettelstore.de/z/place" "zettelstore.de/z/search" ) var validFileName = regexp.MustCompile(`^(\d{14}).*(\.(.+))$`) func matchValidFileName(name string) []string { return validFileName.FindStringSubmatch(name) } type zipEntry struct { metaName string contentName string contentExt string // (normalized) file extension of zettel content metaInHeader bool } type zipPlace struct { name string enricher place.Enricher zettel map[id.Zid]*zipEntry // no lock needed, because read-only after creation } func (zp *zipPlace) Location() string { if strings.HasPrefix(zp.name, "/") { return "file://" + zp.name } return "file:" + zp.name } func (zp *zipPlace) Start(ctx context.Context) error { reader, err := zip.OpenReader(zp.name) if err != nil { return err } defer reader.Close() zp.zettel = make(map[id.Zid]*zipEntry) for _, f := range reader.File { match := matchValidFileName(f.Name) if len(match) < 1 { continue } zid, err := id.Parse(match[1]) if err != nil { continue } zp.addFile(zid, f.Name, match[3]) } return nil } func (zp *zipPlace) addFile(zid id.Zid, name, ext string) { entry := zp.zettel[zid] if entry == nil { entry = &zipEntry{} zp.zettel[zid] = entry } switch ext { case "zettel": if entry.contentExt == "" { entry.contentName = name entry.contentExt = ext entry.metaInHeader = true } case "meta": entry.metaName = name entry.metaInHeader = false default: if entry.contentExt == "" { entry.contentExt = ext entry.contentName = name } } } func (zp *zipPlace) Stop(ctx context.Context) error { zp.zettel = nil return nil } func (zp *zipPlace) CanCreateZettel(ctx context.Context) bool { return false } func (zp *zipPlace) CreateZettel(ctx context.Context, zettel domain.Zettel) (id.Zid, error) { return id.Invalid, place.ErrReadOnly } func (zp *zipPlace) GetZettel(ctx context.Context, zid id.Zid) (domain.Zettel, error) { entry, ok := zp.zettel[zid] if !ok { return domain.Zettel{}, place.ErrNotFound } reader, err := zip.OpenReader(zp.name) if err != nil { return domain.Zettel{}, err } defer reader.Close() var m *meta.Meta var src string var inMeta bool if entry.metaInHeader { src, err = readZipFileContent(reader, entry.contentName) if err != nil { return domain.Zettel{}, err } inp := input.NewInput(src) m = meta.NewFromInput(zid, inp) src = src[inp.Pos:] } else if metaName := entry.metaName; metaName != "" { m, err = readZipMetaFile(reader, zid, metaName) if err != nil { return domain.Zettel{}, err } src, err = readZipFileContent(reader, entry.contentName) if err != nil { return domain.Zettel{}, err } inMeta = true } else { m = CalcDefaultMeta(zid, entry.contentExt) } CleanupMeta(m, zid, entry.contentExt, inMeta, false) return domain.Zettel{Meta: m, Content: domain.NewContent(src)}, nil } func (zp *zipPlace) GetMeta(ctx context.Context, zid id.Zid) (*meta.Meta, error) { entry, ok := zp.zettel[zid] if !ok { return nil, place.ErrNotFound } reader, err := zip.OpenReader(zp.name) if err != nil { return nil, err } defer reader.Close() return readZipMeta(reader, zid, entry) } func (zp *zipPlace) FetchZids(ctx context.Context) (id.Set, error) { result := id.NewSetCap(len(zp.zettel)) for zid := range zp.zettel { result[zid] = true } return result, nil } func (zp *zipPlace) SelectMeta(ctx context.Context, match search.MetaMatchFunc) (res []*meta.Meta, err error) { reader, err := zip.OpenReader(zp.name) if err != nil { return nil, err } defer reader.Close() for zid, entry := range zp.zettel { m, err := readZipMeta(reader, zid, entry) if err != nil { continue } zp.enricher.Enrich(ctx, m) if match(m) { res = append(res, m) } } return res, nil } func (zp *zipPlace) CanUpdateZettel(ctx context.Context, zettel domain.Zettel) bool { return false } func (zp *zipPlace) UpdateZettel(ctx context.Context, zettel domain.Zettel) error { return place.ErrReadOnly } func (zp *zipPlace) AllowRenameZettel(ctx context.Context, zid id.Zid) bool { _, ok := zp.zettel[zid] return !ok } func (zp *zipPlace) RenameZettel(ctx context.Context, curZid, newZid id.Zid) error { if _, ok := zp.zettel[curZid]; ok { return place.ErrReadOnly } return place.ErrNotFound } func (zp *zipPlace) CanDeleteZettel(ctx context.Context, zid id.Zid) bool { return false } func (zp *zipPlace) DeleteZettel(ctx context.Context, zid id.Zid) error { if _, ok := zp.zettel[zid]; ok { return place.ErrReadOnly } return place.ErrNotFound } func (zp *zipPlace) ReadStats(st *place.ManagedPlaceStats) { st.ReadOnly = true st.Zettel = len(zp.zettel) } func readZipMeta(reader *zip.ReadCloser, zid id.Zid, entry *zipEntry) (m *meta.Meta, err error) { var inMeta bool if entry.metaInHeader { m, err = readZipMetaFile(reader, zid, entry.contentName) } else if metaName := entry.metaName; metaName != "" { m, err = readZipMetaFile(reader, zid, entry.metaName) inMeta = true } else { m = CalcDefaultMeta(zid, entry.contentExt) } if err == nil { CleanupMeta(m, zid, entry.contentExt, inMeta, false) } return m, err } func readZipMetaFile(reader *zip.ReadCloser, zid id.Zid, name string) (*meta.Meta, error) { src, err := readZipFileContent(reader, name) if err != nil { return nil, err } inp := input.NewInput(src) return meta.NewFromInput(zid, inp), nil } func readZipFileContent(reader *zip.ReadCloser, name string) (string, error) { f, err := reader.Open(name) if err != nil { return "", err } defer f.Close() buf, err := io.ReadAll(f) if err != nil { return "", err } return string(buf), nil } |
Added place/helper.go.
> > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 | //----------------------------------------------------------------------------- // Copyright (c) 2021 Detlef Stern // // This file is part of zettelstore. // // Zettelstore is licensed under the latest version of the EUPL (European Union // Public License). Please see file LICENSE.txt for your rights and obligations // under this license. //----------------------------------------------------------------------------- // Package place provides a generic interface to zettel places. package place import ( "time" "zettelstore.de/z/domain/id" ) // GetNewZid calculates a new and unused zettel identifier, based on the current date and time. func GetNewZid(testZid func(id.Zid) (bool, error)) (id.Zid, error) { withSeconds := false for i := 0; i < 90; i++ { // Must be completed within 9 seconds (less than web/server.writeTimeout) zid := id.New(withSeconds) found, err := testZid(zid) if err != nil { return id.Invalid, err } if found { return zid, nil } // TODO: do not wait here unconditionally. time.Sleep(100 * time.Millisecond) withSeconds = true } return id.Invalid, ErrConflict } |
Added place/manager/anteroom.go.
> > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 | //----------------------------------------------------------------------------- // Copyright (c) 2021 Detlef Stern // // This file is part of zettelstore. // // Zettelstore is licensed under the latest version of the EUPL (European Union // Public License). Please see file LICENSE.txt for your rights and obligations // under this license. //----------------------------------------------------------------------------- // Package manager coordinates the various places and indexes of a Zettelstore. package manager import ( "sync" "zettelstore.de/z/domain/id" ) type arAction int const ( arNothing arAction = iota arReload arUpdate arDelete ) type anteroom struct { next *anteroom waiting map[id.Zid]arAction curLoad int reload bool } type anterooms struct { mx sync.Mutex first *anteroom last *anteroom maxLoad int } func newAnterooms(maxLoad int) *anterooms { return &anterooms{maxLoad: maxLoad} } func (ar *anterooms) Enqueue(zid id.Zid, action arAction) { if !zid.IsValid() || action == arNothing || action == arReload { return } ar.mx.Lock() defer ar.mx.Unlock() if ar.first == nil { ar.first = ar.makeAnteroom(zid, action) ar.last = ar.first return } for room := ar.first; room != nil; room = room.next { if room.reload { continue // Do not place zettel in reload room } a, ok := room.waiting[zid] if !ok { continue } switch action { case a: return case arUpdate: room.waiting[zid] = action case arDelete: room.waiting[zid] = action } return } if room := ar.last; !room.reload && (ar.maxLoad == 0 || room.curLoad < ar.maxLoad) { room.waiting[zid] = action room.curLoad++ return } room := ar.makeAnteroom(zid, action) ar.last.next = room ar.last = room } func (ar *anterooms) makeAnteroom(zid id.Zid, action arAction) *anteroom { c := ar.maxLoad if c == 0 { c = 100 } waiting := make(map[id.Zid]arAction, c) waiting[zid] = action return &anteroom{next: nil, waiting: waiting, curLoad: 1, reload: false} } func (ar *anterooms) Reset() { ar.mx.Lock() defer ar.mx.Unlock() ar.first = ar.makeAnteroom(id.Invalid, arReload) ar.last = ar.first } func (ar *anterooms) Reload(delZids id.Slice, newZids id.Set) { ar.mx.Lock() defer ar.mx.Unlock() delWaiting := createWaitingSlice(delZids, arDelete) newWaiting := createWaitingSet(newZids, arUpdate) ar.deleteReloadedRooms() if ds := len(delWaiting); ds > 0 { if ns := len(newWaiting); ns > 0 { roomNew := &anteroom{next: ar.first, waiting: newWaiting, curLoad: ns, reload: true} ar.first = &anteroom{next: roomNew, waiting: delWaiting, curLoad: ds, reload: true} if roomNew.next == nil { ar.last = roomNew } return } ar.first = &anteroom{next: ar.first, waiting: delWaiting, curLoad: ds} if ar.first.next == nil { ar.last = ar.first } return } if ns := len(newWaiting); ns > 0 { ar.first = &anteroom{next: ar.first, waiting: newWaiting, curLoad: ns} if ar.first.next == nil { ar.last = ar.first } return } ar.first = nil ar.last = nil } func createWaitingSlice(zids id.Slice, action arAction) map[id.Zid]arAction { waitingSet := make(map[id.Zid]arAction, len(zids)) for _, zid := range zids { if zid.IsValid() { waitingSet[zid] = action } } return waitingSet } func createWaitingSet(zids id.Set, action arAction) map[id.Zid]arAction { waitingSet := make(map[id.Zid]arAction, len(zids)) for zid := range zids { if zid.IsValid() { waitingSet[zid] = action } } return waitingSet } func (ar *anterooms) deleteReloadedRooms() { room := ar.first for room != nil && room.reload { room = room.next } ar.first = room if room == nil { ar.last = nil } } func (ar *anterooms) Dequeue() (arAction, id.Zid) { ar.mx.Lock() defer ar.mx.Unlock() if ar.first == nil { return arNothing, id.Invalid } for zid, action := range ar.first.waiting { delete(ar.first.waiting, zid) if len(ar.first.waiting) == 0 { ar.first = ar.first.next if ar.first == nil { ar.last = nil } } return action, zid } return arNothing, id.Invalid } |
Added place/manager/anteroom_test.go.
> > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 | //----------------------------------------------------------------------------- // Copyright (c) 2021 Detlef Stern // // This file is part of zettelstore. // // Zettelstore is licensed under the latest version of the EUPL (European Union // Public License). Please see file LICENSE.txt for your rights and obligations // under this license. //----------------------------------------------------------------------------- // Package manager coordinates the various places and indexes of a Zettelstore. package manager import ( "testing" "zettelstore.de/z/domain/id" ) func TestSimple(t *testing.T) { ar := newAnterooms(2) ar.Enqueue(id.Zid(1), arUpdate) action, zid := ar.Dequeue() if zid != id.Zid(1) || action != arUpdate { t.Errorf("Expected 1/arUpdate, but got %v/%v", zid, action) } action, zid = ar.Dequeue() if zid != id.Invalid && action != arDelete { t.Errorf("Expected invalid Zid, but got %v", zid) } ar.Enqueue(id.Zid(1), arUpdate) ar.Enqueue(id.Zid(2), arUpdate) if ar.first != ar.last { t.Errorf("Expected one room, but got more") } ar.Enqueue(id.Zid(3), arUpdate) if ar.first == ar.last { t.Errorf("Expected more than one room, but got only one") } count := 0 for ; count < 1000; count++ { action, _ := ar.Dequeue() if action == arNothing { break } } if count != 3 { t.Errorf("Expected 3 dequeues, but got %v", count) } } func TestReset(t *testing.T) { ar := newAnterooms(1) ar.Enqueue(id.Zid(1), arUpdate) ar.Reset() action, zid := ar.Dequeue() if action != arReload || zid != id.Invalid { t.Errorf("Expected reload & invalid Zid, but got %v/%v", action, zid) } ar.Reload(id.Slice{2}, id.NewSet(3, 4)) ar.Enqueue(id.Zid(5), arUpdate) ar.Enqueue(id.Zid(5), arDelete) ar.Enqueue(id.Zid(5), arDelete) ar.Enqueue(id.Zid(5), arUpdate) if ar.first == ar.last || ar.first.next == ar.last || ar.first.next.next != ar.last { t.Errorf("Expected 3 rooms") } action, zid = ar.Dequeue() if zid != id.Zid(2) || action != arDelete { t.Errorf("Expected 2/arDelete, but got %v/%v", zid, action) } action, zid1 := ar.Dequeue() if action != arUpdate { t.Errorf("Expected arUpdate, but got %v", action) } action, zid2 := ar.Dequeue() if action != arUpdate { t.Errorf("Expected arUpdate, but got %v", action) } if !(zid1 == id.Zid(3) && zid2 == id.Zid(4) || zid1 == id.Zid(4) && zid2 == id.Zid(3)) { t.Errorf("Zids must be 3 or 4, but got %v/%v", zid1, zid2) } action, zid = ar.Dequeue() if zid != id.Zid(5) || action != arUpdate { t.Errorf("Expected 5/arUpdate, but got %v/%v", zid, action) } action, zid = ar.Dequeue() if action != arNothing || zid != id.Invalid { t.Errorf("Expected nothing & invalid Zid, but got %v/%v", action, zid) } ar = newAnterooms(1) ar.Reload(nil, id.NewSet(id.Zid(6))) action, zid = ar.Dequeue() if zid != id.Zid(6) || action != arUpdate { t.Errorf("Expected 6/arUpdate, but got %v/%v", zid, action) } action, zid = ar.Dequeue() if action != arNothing || zid != id.Invalid { t.Errorf("Expected nothing & invalid Zid, but got %v/%v", action, zid) } ar = newAnterooms(1) ar.Reload(id.Slice{7}, nil) action, zid = ar.Dequeue() if zid != id.Zid(7) || action != arDelete { t.Errorf("Expected 7/arDelete, but got %v/%v", zid, action) } action, zid = ar.Dequeue() if action != arNothing || zid != id.Invalid { t.Errorf("Expected nothing & invalid Zid, but got %v/%v", action, zid) } ar = newAnterooms(1) ar.Enqueue(id.Zid(8), arUpdate) ar.Reload(nil, nil) action, zid = ar.Dequeue() if action != arNothing || zid != id.Invalid { t.Errorf("Expected nothing & invalid Zid, but got %v/%v", action, zid) } } |
Added place/manager/collect.go.
> > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 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 Detlef Stern // // This file is part of zettelstore. // // Zettelstore is licensed under 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 places and indexes of a Zettelstore. package manager import ( "strings" "zettelstore.de/z/ast" "zettelstore.de/z/domain/id" "zettelstore.de/z/place/manager/store" "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.NewTopDownTraverser(data).VisitBlockSlice(zn.Ast) } func collectInlineIndexData(ins ast.InlineSlice, data *collectData) { ast.NewTopDownTraverser(data).VisitInlineSlice(ins) } // VisitVerbatim collects the verbatim text in the word set. func (data *collectData) VisitVerbatim(vn *ast.VerbatimNode) { for _, line := range vn.Lines { data.addText(line) } } // VisitRegion does nothing. func (data *collectData) VisitRegion(rn *ast.RegionNode) {} // VisitHeading does nothing. func (data *collectData) VisitHeading(hn *ast.HeadingNode) {} // VisitHRule does nothing. func (data *collectData) VisitHRule(hn *ast.HRuleNode) {} // VisitList does nothing. func (data *collectData) VisitNestedList(ln *ast.NestedListNode) {} // VisitDescriptionList does nothing. func (data *collectData) VisitDescriptionList(dn *ast.DescriptionListNode) {} // VisitPara does nothing. func (data *collectData) VisitPara(pn *ast.ParaNode) {} // VisitTable does nothing. func (data *collectData) VisitTable(tn *ast.TableNode) {} // VisitBLOB does nothing. func (data *collectData) VisitBLOB(bn *ast.BLOBNode) {} // VisitText collects the text in the word set. func (data *collectData) VisitText(tn *ast.TextNode) { data.addText(tn.Text) } // VisitTag collects the tag name in the word set. func (data *collectData) VisitTag(tn *ast.TagNode) { data.addText(tn.Tag) } // VisitSpace does nothing. func (data *collectData) VisitSpace(sn *ast.SpaceNode) {} // VisitBreak does nothing. func (data *collectData) VisitBreak(bn *ast.BreakNode) {} // VisitLink collects the given link as a reference. func (data *collectData) VisitLink(ln *ast.LinkNode) { ref := ln.Ref if ref == nil { return } if ref.IsExternal() { data.urls.Add(strings.ToLower(ref.Value)) } if !ref.IsZettel() { return } if zid, err := id.Parse(ref.URL.Path); err == nil { data.refs[zid] = true } } // VisitImage collects the image links as a reference. func (data *collectData) VisitImage(in *ast.ImageNode) { ref := in.Ref if ref == nil { return } if ref.IsExternal() { data.urls.Add(strings.ToLower(ref.Value)) } if !ref.IsZettel() { return } if zid, err := id.Parse(ref.URL.Path); err == nil { data.refs[zid] = true } } // VisitCite does nothing. func (data *collectData) VisitCite(cn *ast.CiteNode) {} // VisitFootnote does nothing. func (data *collectData) VisitFootnote(fn *ast.FootnoteNode) {} // VisitMark does nothing. func (data *collectData) VisitMark(mn *ast.MarkNode) {} // VisitFormat does nothing. func (data *collectData) VisitFormat(fn *ast.FormatNode) {} // VisitLiteral collects the literal words in the word set. func (data *collectData) VisitLiteral(ln *ast.LiteralNode) { data.addText(ln.Text) } func (data *collectData) addText(s string) { for _, word := range strfun.NormalizeWords(s) { data.words.Add(word) } } |
Added place/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 | //----------------------------------------------------------------------------- // Copyright (c) 2021 Detlef Stern // // This file is part of zettelstore. // // Zettelstore is licensed under the latest version of the EUPL (European Union // Public License). Please see file LICENSE.txt for your rights and obligations // under this license. //----------------------------------------------------------------------------- // Package manager coordinates the various places and indexes of a Zettelstore. package manager import ( "context" "zettelstore.de/z/domain/meta" "zettelstore.de/z/place" ) // Enrich computes additional properties and updates the given metadata. func (mgr *Manager) Enrich(ctx context.Context, m *meta.Meta) { if place.DoNotEnrich(ctx) { // Enrich is called indirectly via indexer or enrichment is not requested // because of other reasons -> ignore this call, do not update meta data return } computePublished(m) mgr.idxStore.Enrich(ctx, m) } func computePublished(m *meta.Meta) { if _, ok := m.Get(meta.KeyPublished); ok { return } if modified, ok := m.Get(meta.KeyModified); ok { if _, ok = meta.TimeValue(modified); ok { m.Set(meta.KeyPublished, modified) return } } zid := m.Zid.String() if _, ok := meta.TimeValue(zid); ok { m.Set(meta.KeyPublished, zid) return } // Neither the zettel was modified nor the zettel identifer contains a valid // timestamp. In this case do not set the "published" property. } |
Added place/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 | //----------------------------------------------------------------------------- // Copyright (c) 2021 Detlef Stern // // This file is part of zettelstore. // // Zettelstore is licensed under the latest version of the EUPL (European Union // Public License). Please see file LICENSE.txt for your rights and obligations // under this license. //----------------------------------------------------------------------------- // Package manager coordinates the various places and indexes of a Zettelstore. package manager import ( "context" "net/url" "time" "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/place" "zettelstore.de/z/place/manager/store" "zettelstore.de/z/strfun" ) // SelectEqual all zettel that contains the given exact word. // The word must be normalized through Unicode NKFD, trimmed and not empty. func (mgr *Manager) SelectEqual(word string) id.Set { return mgr.idxStore.SelectEqual(word) } // SelectPrefix 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) SelectPrefix(prefix string) id.Set { return mgr.idxStore.SelectPrefix(prefix) } // SelectSuffix 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) SelectSuffix(suffix string) id.Set { return mgr.idxStore.SelectSuffix(suffix) } // SelectContains all zettel that contains the given string. // The string must be normalized through Unicode NKFD, trimmed and not empty. func (mgr *Manager) SelectContains(s string) id.Set { return mgr.idxStore.SelectContains(s) } // idxIndexer runs in the background and updates the index data structures. // This is the main service of the idxIndexer. func (mgr *Manager) idxIndexer() { // Something may panic. Ensure a running indexer. defer func() { if r := recover(); r != nil { kernel.Main.LogRecover("Indexer", r) go mgr.idxIndexer() } }() timerDuration := 15 * time.Second timer := time.NewTimer(timerDuration) ctx := place.NoEnrichContext(context.Background()) for { start := time.Now() if mgr.idxWorkService(ctx) { mgr.idxMx.Lock() mgr.idxDurLastIndex = time.Since(start) mgr.idxMx.Unlock() } if !mgr.idxSleepService(timer, timerDuration) { return } } } func (mgr *Manager) idxWorkService(ctx context.Context) bool { changed := false for { switch action, zid := mgr.idxAr.Dequeue(); action { case arNothing: return changed case arReload: zids, err := mgr.FetchZids(ctx) if err == nil { mgr.idxAr.Reload(nil, zids) mgr.idxMx.Lock() mgr.idxLastReload = time.Now() mgr.idxSinceReload = 0 mgr.idxMx.Unlock() } case arUpdate: changed = true mgr.idxMx.Lock() mgr.idxSinceReload++ mgr.idxMx.Unlock() zettel, err := mgr.GetZettel(ctx, zid) if err != nil { // TODO: on some errors put the zid into a "try later" set continue } mgr.idxUpdateZettel(ctx, zettel) case arDelete: changed = true mgr.idxMx.Lock() mgr.idxSinceReload++ mgr.idxMx.Unlock() mgr.idxDeleteZettel(zid) } } } func (mgr *Manager) idxSleepService(timer *time.Timer, timerDuration time.Duration) bool { select { case _, ok := <-mgr.idxReady: if !ok { return false } case _, ok := <-timer.C: if !ok { return false } timer.Reset(timerDuration) case <-mgr.done: if !timer.Stop() { <-timer.C } return false } return true } func (mgr *Manager) idxUpdateZettel(ctx context.Context, zettel domain.Zettel) { m := zettel.Meta if m.GetBool(meta.KeyNoIndex) { // Zettel maybe in index toCheck := mgr.idxStore.DeleteZettel(ctx, m.Zid) mgr.idxCheckZettel(toCheck) return } var cData collectData cData.initialize() collectZettelIndexData(parser.ParseZettel(zettel, "", mgr.rtConfig), &cData) zi := store.NewZettelIndex(m.Zid) mgr.idxCollectFromMeta(ctx, m, zi, &cData) mgr.idxProcessData(ctx, zi, &cData) toCheck := mgr.idxStore.UpdateReferences(ctx, zi) mgr.idxCheckZettel(toCheck) } func (mgr *Manager) idxCollectFromMeta(ctx context.Context, m *meta.Meta, zi *store.ZettelIndex, cData *collectData) { for _, pair := range m.Pairs(false) { descr := meta.GetDescription(pair.Key) if descr.IsComputed() { continue } switch descr.Type { case meta.TypeID: mgr.idxUpdateValue(ctx, descr.Inverse, pair.Value, zi) case meta.TypeIDSet: for _, val := range meta.ListFromValue(pair.Value) { mgr.idxUpdateValue(ctx, descr.Inverse, val, zi) } case meta.TypeZettelmarkup: collectInlineIndexData(parser.ParseMetadata(pair.Value), cData) case meta.TypeURL: if _, err := url.Parse(pair.Value); err == nil { cData.urls.Add(pair.Value) } default: for _, word := range strfun.NormalizeWords(pair.Value) { cData.words.Add(word) } } } } func (mgr *Manager) idxProcessData(ctx context.Context, zi *store.ZettelIndex, cData *collectData) { for ref := range cData.refs { if _, err := mgr.GetMeta(ctx, ref); err == nil { zi.AddBackRef(ref) } else { zi.AddDeadRef(ref) } } zi.SetWords(cData.words) zi.SetUrls(cData.urls) } func (mgr *Manager) idxUpdateValue(ctx context.Context, inverse string, 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 inverse == "" { zi.AddBackRef(zid) return } zi.AddMetaRef(inverse, zid) } func (mgr *Manager) idxDeleteZettel(zid id.Zid) { toCheck := mgr.idxStore.DeleteZettel(context.Background(), zid) mgr.idxCheckZettel(toCheck) } func (mgr *Manager) idxCheckZettel(s id.Set) { for zid := range s { mgr.idxAr.Enqueue(zid, arUpdate) } } |
Added place/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 | //----------------------------------------------------------------------------- // Copyright (c) 2021 Detlef Stern // // This file is part of zettelstore. // // Zettelstore is licensed under the latest version of the EUPL (European Union // Public License). Please see file LICENSE.txt for your rights and obligations // under this license. //----------------------------------------------------------------------------- // Package manager coordinates the various places and indexes of a Zettelstore. package manager import ( "context" "io" "log" "net/url" "sort" "sync" "time" "zettelstore.de/z/auth" "zettelstore.de/z/config" "zettelstore.de/z/domain/id" "zettelstore.de/z/domain/meta" "zettelstore.de/z/kernel" "zettelstore.de/z/place" "zettelstore.de/z/place/manager/memstore" "zettelstore.de/z/place/manager/store" ) // ConnectData contains all administration related values. type ConnectData struct { Config config.Config Enricher place.Enricher Notify chan<- place.UpdateInfo } // Connect returns a handle to the specified place func Connect(u *url.URL, authManager auth.BaseManager, cdata *ConnectData) (place.ManagedPlace, 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 place 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) (place.ManagedPlace, error) var registry = map[string]createFunc{} // Register the encoder for later retrieval. func Register(scheme string, create createFunc) { if _, ok := registry[scheme]; ok { log.Fatalf("Place with scheme %q already registered", scheme) } registry[scheme] = create } // GetSchemes returns all registered scheme, ordered by scheme string. func GetSchemes() []string { result := make([]string, 0, len(registry)) for scheme := range registry { result = append(result, scheme) } sort.Strings(result) return result } // Manager is a coordinating place. type Manager struct { mgrMx sync.RWMutex started bool rtConfig config.Config subplaces []place.ManagedPlace observers []place.UpdateFunc mxObserver sync.RWMutex done chan struct{} infos chan place.UpdateInfo propertyKeys map[string]bool // Set of property key names // Indexer data idxStore store.Store idxAr *anterooms idxReady chan struct{} // Signal a non-empty anteroom to background task // Indexer stats data idxMx sync.RWMutex idxLastReload time.Time idxSinceReload uint64 idxDurLastIndex time.Duration } // New creates a new managing place. func New(placeURIs []*url.URL, authManager auth.BaseManager, rtConfig config.Config) (*Manager, error) { propertyKeys := make(map[string]bool) for _, kd := range meta.GetSortedKeyDescriptions() { if kd.IsProperty() { propertyKeys[kd.Name] = true } } mgr := &Manager{ rtConfig: rtConfig, infos: make(chan place.UpdateInfo, len(placeURIs)*10), propertyKeys: propertyKeys, idxStore: memstore.New(), idxAr: newAnterooms(10), idxReady: make(chan struct{}, 1), } cdata := ConnectData{Config: rtConfig, Enricher: mgr, Notify: mgr.infos} subplaces := make([]place.ManagedPlace, 0, len(placeURIs)+2) for _, uri := range placeURIs { p, err := Connect(uri, authManager, &cdata) if err != nil { return nil, err } if p != nil { subplaces = append(subplaces, p) } } constplace, err := registry[" const"](nil, &cdata) if err != nil { return nil, err } progplace, err := registry[" prog"](nil, &cdata) if err != nil { return nil, err } subplaces = append(subplaces, constplace, progplace) mgr.subplaces = subplaces return mgr, nil } // RegisterObserver registers an observer that will be notified // if a zettel was found to be changed. func (mgr *Manager) RegisterObserver(f place.UpdateFunc) { if f != nil { mgr.mxObserver.Lock() mgr.observers = append(mgr.observers, f) mgr.mxObserver.Unlock() } } func (mgr *Manager) notifyObserver(ci *place.UpdateInfo) { mgr.mxObserver.RLock() observers := mgr.observers mgr.mxObserver.RUnlock() for _, ob := range observers { ob(*ci) } } func (mgr *Manager) notifier() { // The call to notify may panic. Ensure a running notifier. defer func() { if r := recover(); r != nil { kernel.Main.LogRecover("Notifier", r) go mgr.notifier() } }() for { select { case ci, ok := <-mgr.infos: if ok { mgr.idxEnqueue(ci.Reason, ci.Zid) if ci.Place == nil { ci.Place = mgr } mgr.notifyObserver(&ci) } case <-mgr.done: return } } } func (mgr *Manager) idxEnqueue(reason place.UpdateReason, zid id.Zid) { switch reason { case place.OnReload: mgr.idxAr.Reset() case place.OnUpdate: mgr.idxAr.Enqueue(zid, arUpdate) case place.OnDelete: mgr.idxAr.Enqueue(zid, arDelete) default: return } select { case mgr.idxReady <- struct{}{}: default: } } // Start the place. Now all other functions of the place are allowed. // Starting an already started place is not allowed. func (mgr *Manager) Start(ctx context.Context) error { mgr.mgrMx.Lock() if mgr.started { mgr.mgrMx.Unlock() return place.ErrStarted } for i := len(mgr.subplaces) - 1; i >= 0; i-- { ssi, ok := mgr.subplaces[i].(place.StartStopper) if !ok { continue } err := ssi.Start(ctx) if err == nil { continue } for j := i + 1; j < len(mgr.subplaces); j++ { if ssj, ok := mgr.subplaces[j].(place.StartStopper); ok { ssj.Stop(ctx) } } mgr.mgrMx.Unlock() return err } mgr.idxAr.Reset() // Ensure an initial index run mgr.done = make(chan struct{}) go mgr.notifier() go mgr.idxIndexer() // mgr.startIndexer(mgr) mgr.started = true mgr.mgrMx.Unlock() mgr.infos <- place.UpdateInfo{Reason: place.OnReload, Zid: id.Invalid} return nil } // Stop the started place. Now only the Start() function is allowed. func (mgr *Manager) Stop(ctx context.Context) error { mgr.mgrMx.Lock() defer mgr.mgrMx.Unlock() if !mgr.started { return place.ErrStopped } close(mgr.done) var err error for _, p := range mgr.subplaces { if ss, ok := p.(place.StartStopper); ok { if err1 := ss.Stop(ctx); err1 != nil && err == nil { err = err1 } } } mgr.started = false return err } // ReadStats populates st with place statistics func (mgr *Manager) ReadStats(st *place.Stats) { mgr.mgrMx.RLock() defer mgr.mgrMx.RUnlock() subStats := make([]place.ManagedPlaceStats, len(mgr.subplaces)) for i, p := range mgr.subplaces { p.ReadStats(&subStats[i]) } st.ReadOnly = true sumZettel := 0 for _, sst := range subStats { if !sst.ReadOnly { st.ReadOnly = false } sumZettel += sst.Zettel } st.NumManagedPlaces = len(mgr.subplaces) 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.DurLastIndex = mgr.idxDurLastIndex 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 place/manager/memstore/memstore.go.
> > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 326 327 328 329 330 331 332 333 334 335 336 337 338 339 340 341 342 343 344 345 346 347 348 349 350 351 352 353 354 355 356 357 358 359 360 361 362 363 364 365 366 367 368 369 370 371 372 373 374 375 376 377 378 379 380 381 382 383 384 385 386 387 388 389 390 391 392 393 394 395 396 397 398 399 400 401 402 403 404 405 406 407 408 409 410 411 412 413 414 415 416 417 418 419 420 421 422 423 424 425 426 427 428 429 430 431 432 433 434 435 436 437 438 439 440 441 442 443 444 445 446 447 448 449 450 451 452 453 454 455 456 457 458 459 460 461 462 463 464 465 466 467 468 469 470 471 472 473 474 475 476 477 478 479 480 481 482 483 484 485 486 487 488 489 490 491 492 493 494 495 496 497 498 499 500 501 502 503 504 505 506 507 508 509 510 511 512 513 514 515 516 517 518 519 520 521 522 523 524 525 526 527 528 529 530 531 532 533 534 535 536 537 538 539 540 541 542 543 544 545 546 547 548 549 550 551 552 553 554 555 556 557 558 559 560 561 562 563 564 565 566 567 568 569 570 571 572 573 574 575 576 577 578 579 580 | //----------------------------------------------------------------------------- // Copyright (c) 2021 Detlef Stern // // This file is part of zettelstore. // // Zettelstore is licensed under the latest version of the EUPL (European Union // Public License). Please see file LICENSE.txt for your rights and obligations // under this license. //----------------------------------------------------------------------------- // Package memstore stored the index in main memory. package memstore import ( "context" "fmt" "io" "sort" "strings" "sync" "zettelstore.de/z/domain/id" "zettelstore.de/z/domain/meta" "zettelstore.de/z/place/manager/store" ) type metaRefs struct { forward id.Slice backward id.Slice } type zettelIndex struct { dead id.Slice forward id.Slice backward id.Slice meta map[string]metaRefs words []string urls []string } func (zi *zettelIndex) isEmpty() bool { if len(zi.forward) > 0 || len(zi.backward) > 0 || len(zi.dead) > 0 || len(zi.words) > 0 { return false } return zi.meta == nil || len(zi.meta) == 0 } type stringRefs map[string]id.Slice type memStore struct { mx sync.RWMutex idx map[id.Zid]*zettelIndex dead map[id.Zid]id.Slice // map dead refs where they occur words stringRefs urls stringRefs // Stats updates uint64 } // New returns a new memory-based index store. func New() store.Store { return &memStore{ idx: make(map[id.Zid]*zettelIndex), dead: make(map[id.Zid]id.Slice), words: make(stringRefs), urls: make(stringRefs), } } func (ms *memStore) Enrich(ctx context.Context, m *meta.Meta) { if ms.doEnrich(ctx, m) { ms.mx.Lock() ms.updates++ ms.mx.Unlock() } } func (ms *memStore) doEnrich(ctx context.Context, m *meta.Meta) bool { ms.mx.RLock() defer ms.mx.RUnlock() zi, ok := ms.idx[m.Zid] if !ok { return false } var updated bool if len(zi.dead) > 0 { m.Set(meta.KeyDead, zi.dead.String()) updated = true } back := removeOtherMetaRefs(m, zi.backward.Copy()) if len(zi.backward) > 0 { m.Set(meta.KeyBackward, zi.backward.String()) updated = true } if len(zi.forward) > 0 { m.Set(meta.KeyForward, zi.forward.String()) back = remRefs(back, zi.forward) updated = true } if len(zi.meta) > 0 { for k, refs := range zi.meta { if len(refs.backward) > 0 { m.Set(k, refs.backward.String()) back = remRefs(back, refs.backward) updated = true } } } if len(back) > 0 { m.Set(meta.KeyBack, back.String()) updated = true } return updated } // SelectEqual all zettel that contains the given exact word. // The word must be normalized through Unicode NKFD, trimmed and not empty. func (ms *memStore) SelectEqual(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 } // Select 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) SelectPrefix(prefix string) id.Set { ms.mx.RLock() defer ms.mx.RUnlock() result := ms.selectWithPred(prefix, strings.HasPrefix) l := len(prefix) if l > 14 { return result } minZid, err := id.Parse(prefix + "00000000000000"[:14-l]) if err != nil { return result } maxZid, err := id.Parse(prefix + "99999999999999"[:14-l]) if err != nil { return result } for zid, zi := range ms.idx { if minZid <= zid && zid <= maxZid { addBackwardZids(result, zid, zi) } } return result } // Select 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) SelectSuffix(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 } // Select all zettel that contains the given string. // The string must be normalized through Unicode NKFD, trimmed and not empty. func (ms *memStore) SelectContains(s string) id.Set { ms.mx.RLock() defer ms.mx.RUnlock() result := ms.selectWithPred(s, strings.Contains) if len(s) > 14 { return result } if _, err := id.ParseUint(s); err != nil { return result } for zid, zi := range ms.idx { if strings.Contains(zid.String(), s) { addBackwardZids(result, zid, zi) } } return result } func (ms *memStore) selectWithPred(s string, pred func(string, string) bool) id.Set { // Must only be called if ms.mx is read-locked! result := id.NewSet() for word, refs := range ms.words { if !pred(word, s) { continue } result.AddSlice(refs) } for u, refs := range ms.urls { if !pred(u, s) { continue } result.AddSlice(refs) } return result } func addBackwardZids(result id.Set, zid id.Zid, zi *zettelIndex) { // Must only be called if ms.mx is read-locked! result[zid] = true result.AddSlice(zi.backward) for _, mref := range zi.meta { result.AddSlice(mref.backward) } } func removeOtherMetaRefs(m *meta.Meta, back id.Slice) id.Slice { for _, p := range m.PairsRest(false) { switch meta.Type(p.Key) { case meta.TypeID: if zid, err := id.Parse(p.Value); err == nil { back = remRef(back, zid) } case meta.TypeIDSet: for _, val := range meta.ListFromValue(p.Value) { if zid, err := id.Parse(val); err == nil { back = remRef(back, zid) } } } } return back } func (ms *memStore) UpdateReferences(ctx context.Context, zidx *store.ZettelIndex) id.Set { ms.mx.Lock() defer ms.mx.Unlock() zi, ziExist := ms.idx[zidx.Zid] if !ziExist || zi == nil { zi = &zettelIndex{} ziExist = false } // Is this zettel an old dead reference mentioned in other zettel? var toCheck id.Set if refs, ok := ms.dead[zidx.Zid]; ok { // These must be checked later again toCheck = id.NewSet(refs...) delete(ms.dead, zidx.Zid) } ms.updateDeadReferences(zidx, zi) ms.updateForwardBackwardReferences(zidx, zi) ms.updateMetadataReferences(zidx, zi) zi.words = updateWordSet(zidx.Zid, ms.words, zi.words, zidx.GetWords()) zi.urls = updateWordSet(zidx.Zid, ms.urls, zi.urls, zidx.GetUrls()) // Check if zi must be inserted into ms.idx if !ziExist && !zi.isEmpty() { ms.idx[zidx.Zid] = zi } return toCheck } func (ms *memStore) updateDeadReferences(zidx *store.ZettelIndex, zi *zettelIndex) { // Must only be called if ms.mx is write-locked! drefs := zidx.GetDeadRefs() newRefs, remRefs := refsDiff(drefs, zi.dead) zi.dead = drefs for _, ref := range remRefs { ms.dead[ref] = remRef(ms.dead[ref], zidx.Zid) } for _, ref := range newRefs { ms.dead[ref] = addRef(ms.dead[ref], zidx.Zid) } } func (ms *memStore) updateForwardBackwardReferences(zidx *store.ZettelIndex, zi *zettelIndex) { // Must only be called if ms.mx is write-locked! brefs := zidx.GetBackRefs() newRefs, remRefs := refsDiff(brefs, zi.forward) zi.forward = brefs for _, ref := range remRefs { bzi := ms.getEntry(ref) bzi.backward = remRef(bzi.backward, zidx.Zid) } for _, ref := range newRefs { bzi := ms.getEntry(ref) bzi.backward = addRef(bzi.backward, zidx.Zid) } } func (ms *memStore) updateMetadataReferences(zidx *store.ZettelIndex, zi *zettelIndex) { // Must only be called if ms.mx is write-locked! metarefs := zidx.GetMetaRefs() for key, mr := range zi.meta { if _, ok := metarefs[key]; ok { continue } ms.removeInverseMeta(zidx.Zid, key, mr.forward) } if zi.meta == nil { zi.meta = make(map[string]metaRefs) } for key, mrefs := range metarefs { mr := zi.meta[key] newRefs, remRefs := refsDiff(mrefs, mr.forward) mr.forward = mrefs zi.meta[key] = mr for _, ref := range newRefs { bzi := ms.getEntry(ref) if bzi.meta == nil { bzi.meta = make(map[string]metaRefs) } bmr := bzi.meta[key] bmr.backward = addRef(bmr.backward, zidx.Zid) bzi.meta[key] = bmr } ms.removeInverseMeta(zidx.Zid, key, remRefs) } } func updateWordSet(zid id.Zid, srefs stringRefs, prev []string, next store.WordSet) []string { // Must only be called if ms.mx is write-locked! newWords, removeWords := next.Diff(prev) for _, word := range newWords { if refs, ok := srefs[word]; ok { srefs[word] = addRef(refs, zid) continue } srefs[word] = id.Slice{zid} } for _, word := range removeWords { refs, ok := srefs[word] if !ok { continue } refs2 := remRef(refs, zid) if len(refs2) == 0 { delete(srefs, word) continue } srefs[word] = refs2 } return next.Words() } func (ms *memStore) getEntry(zid id.Zid) *zettelIndex { // Must only be called if ms.mx is write-locked! if zi, ok := ms.idx[zid]; ok { return zi } zi := &zettelIndex{} ms.idx[zid] = zi return zi } func (ms *memStore) DeleteZettel(ctx context.Context, zid id.Zid) id.Set { ms.mx.Lock() defer ms.mx.Unlock() zi, ok := ms.idx[zid] if !ok { return nil } ms.deleteDeadSources(zid, zi) toCheck := ms.deleteForwardBackward(zid, zi) if len(zi.meta) > 0 { for key, mrefs := range zi.meta { ms.removeInverseMeta(zid, key, mrefs.forward) } } ms.deleteWords(zid, zi.words) delete(ms.idx, zid) return toCheck } func (ms *memStore) deleteDeadSources(zid id.Zid, zi *zettelIndex) { // Must only be called if ms.mx is write-locked! for _, ref := range zi.dead { if drefs, ok := ms.dead[ref]; ok { drefs = remRef(drefs, zid) if len(drefs) > 0 { ms.dead[ref] = drefs } else { delete(ms.dead, ref) } } } } func (ms *memStore) deleteForwardBackward(zid id.Zid, zi *zettelIndex) id.Set { // Must only be called if ms.mx is write-locked! var toCheck id.Set for _, ref := range zi.forward { if fzi, ok := ms.idx[ref]; ok { fzi.backward = remRef(fzi.backward, zid) } } for _, ref := range zi.backward { if bzi, ok := ms.idx[ref]; ok { bzi.forward = remRef(bzi.forward, zid) if toCheck == nil { toCheck = id.NewSet() } toCheck[ref] = true } } return toCheck } func (ms *memStore) removeInverseMeta(zid id.Zid, key string, forward id.Slice) { // Must only be called if ms.mx is write-locked! for _, ref := range forward { bzi, ok := ms.idx[ref] if !ok || bzi.meta == nil { continue } bmr, ok := bzi.meta[key] if !ok { continue } bmr.backward = remRef(bmr.backward, zid) if len(bmr.backward) > 0 || len(bmr.forward) > 0 { bzi.meta[key] = bmr } else { delete(bzi.meta, key) if len(bzi.meta) == 0 { bzi.meta = nil } } } } func (ms *memStore) deleteWords(zid id.Zid, words []string) { // Must only be called if ms.mx is write-locked! for _, word := range words { refs, ok := ms.words[word] if !ok { continue } refs2 := remRef(refs, zid) if len(refs2) == 0 { delete(ms.words, word) continue } ms.words[word] = refs2 } } func (ms *memStore) ReadStats(st *store.Stats) { ms.mx.RLock() st.Zettel = len(ms.idx) st.Updates = ms.updates st.Words = uint64(len(ms.words)) st.Urls = uint64(len(ms.urls)) ms.mx.RUnlock() } func (ms *memStore) Dump(w io.Writer) { ms.mx.RLock() defer ms.mx.RUnlock() io.WriteString(w, "=== Dump\n") ms.dumpIndex(w) ms.dumpDead(w) dumpStringRefs(w, "Words", "", "", ms.words) dumpStringRefs(w, "URLs", "[[", "]]", ms.urls) } func (ms *memStore) dumpIndex(w io.Writer) { if len(ms.idx) == 0 { return } io.WriteString(w, "==== Zettel Index\n") zids := make(id.Slice, 0, len(ms.idx)) for id := range ms.idx { zids = append(zids, id) } zids.Sort() for _, id := range zids { fmt.Fprintln(w, "=====", id) zi := ms.idx[id] if len(zi.dead) > 0 { fmt.Fprintln(w, "* Dead:", zi.dead) } dumpZids(w, "* Forward:", zi.forward) dumpZids(w, "* Backward:", zi.backward) for k, fb := range zi.meta { fmt.Fprintln(w, "* Meta", k) dumpZids(w, "** Forward:", fb.forward) dumpZids(w, "** Backward:", fb.backward) } dumpStrings(w, "* Words", "", "", zi.words) dumpStrings(w, "* URLs", "[[", "]]", zi.urls) } } func (ms *memStore) dumpDead(w io.Writer) { if len(ms.dead) == 0 { return } fmt.Fprintf(w, "==== Dead References\n") zids := make(id.Slice, 0, len(ms.dead)) for id := range ms.dead { zids = append(zids, id) } zids.Sort() for _, id := range zids { fmt.Fprintln(w, ";", id) fmt.Fprintln(w, ":", ms.dead[id]) } } func dumpZids(w io.Writer, prefix string, zids id.Slice) { if len(zids) > 0 { io.WriteString(w, prefix) for _, zid := range zids { io.WriteString(w, " ") w.Write(zid.Bytes()) } fmt.Fprintln(w) } } func dumpStrings(w io.Writer, title, preString, postString string, slice []string) { if len(slice) > 0 { sl := make([]string, len(slice)) copy(sl, slice) sort.Strings(sl) fmt.Fprintln(w, title) for _, s := range sl { fmt.Fprintf(w, "** %s%s%s\n", preString, s, postString) } } } func dumpStringRefs(w io.Writer, title, preString, postString string, srefs stringRefs) { if len(srefs) == 0 { return } fmt.Fprintln(w, "====", title) slice := make([]string, 0, len(srefs)) for s := range srefs { slice = append(slice, s) } sort.Strings(slice) for _, s := range slice { fmt.Fprintf(w, "; %s%s%s\n", preString, s, postString) fmt.Fprintln(w, ":", srefs[s]) } } |
Added place/manager/memstore/refs.go.
> > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 | //----------------------------------------------------------------------------- // Copyright (c) 2021 Detlef Stern // // This file is part of zettelstore. // // Zettelstore is licensed under the latest version of the EUPL (European Union // Public License). Please see file LICENSE.txt for your rights and obligations // under this license. //----------------------------------------------------------------------------- // Package memstore stored the index in main memory. package memstore import "zettelstore.de/z/domain/id" func refsDiff(refsN, refsO id.Slice) (newRefs, remRefs id.Slice) { npos, opos := 0, 0 for npos < len(refsN) && opos < len(refsO) { rn, ro := refsN[npos], refsO[opos] if rn == ro { npos++ opos++ continue } if rn < ro { newRefs = append(newRefs, rn) npos++ continue } remRefs = append(remRefs, ro) opos++ } if npos < len(refsN) { newRefs = append(newRefs, refsN[npos:]...) } if opos < len(refsO) { remRefs = append(remRefs, refsO[opos:]...) } return newRefs, remRefs } func addRef(refs id.Slice, ref id.Zid) id.Slice { hi := len(refs) for lo := 0; lo < hi; { m := lo + (hi-lo)/2 if r := refs[m]; r == ref { return refs } else if r < ref { lo = m + 1 } else { hi = m } } refs = append(refs, id.Invalid) copy(refs[hi+1:], refs[hi:]) refs[hi] = ref return refs } func remRefs(refs, rem id.Slice) id.Slice { if len(refs) == 0 || len(rem) == 0 { return refs } result := make(id.Slice, 0, len(refs)) rpos, dpos := 0, 0 for rpos < len(refs) && dpos < len(rem) { rr, dr := refs[rpos], rem[dpos] if rr < dr { result = append(result, rr) rpos++ continue } if dr < rr { dpos++ continue } rpos++ dpos++ } if rpos < len(refs) { result = append(result, refs[rpos:]...) } return result } func remRef(refs id.Slice, ref id.Zid) id.Slice { hi := len(refs) for lo := 0; lo < hi; { m := lo + (hi-lo)/2 if r := refs[m]; r == ref { copy(refs[m:], refs[m+1:]) refs = refs[:len(refs)-1] return refs } else if r < ref { lo = m + 1 } else { hi = m } } return refs } |
Added place/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 | //----------------------------------------------------------------------------- // Copyright (c) 2021 Detlef Stern // // This file is part of zettelstore. // // Zettelstore is licensed under the latest version of the EUPL (European Union // Public License). Please see file LICENSE.txt for your rights and obligations // under this license. //----------------------------------------------------------------------------- // Package memstore stored the index in main memory. package memstore import ( "testing" "zettelstore.de/z/domain/id" ) func assertRefs(t *testing.T, i int, got, exp id.Slice) { t.Helper() if got == nil && exp != nil { t.Errorf("%d: got nil, but expected %v", i, exp) return } if got != nil && exp == nil { t.Errorf("%d: expected nil, but got %v", i, got) return } if len(got) != len(exp) { t.Errorf("%d: expected len(%v)==%d, but got len(%v)==%d", i, exp, len(exp), got, len(got)) return } for p, n := range exp { if got := got[p]; got != id.Zid(n) { t.Errorf("%d: pos %d: expected %d, but got %d", i, p, n, got) } } } func TestRefsDiff(t *testing.T) { 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) { 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) { 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) { 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 place/manager/place.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 | //----------------------------------------------------------------------------- // Copyright (c) 2021 Detlef Stern // // This file is part of zettelstore. // // Zettelstore is licensed under the latest version of the EUPL (European Union // Public License). Please see file LICENSE.txt for your rights and obligations // under this license. //----------------------------------------------------------------------------- // Package manager coordinates the various places and indexes of a Zettelstore. package manager import ( "context" "errors" "sort" "strings" "zettelstore.de/z/domain" "zettelstore.de/z/domain/id" "zettelstore.de/z/domain/meta" "zettelstore.de/z/place" "zettelstore.de/z/search" ) // Conatains all place.Place related functions // Location returns some information where the place is located. func (mgr *Manager) Location() string { if len(mgr.subplaces) <= 2 { return "NONE" } var sb strings.Builder for i := 0; i < len(mgr.subplaces)-2; i++ { if i > 0 { sb.WriteString(", ") } sb.WriteString(mgr.subplaces[i].Location()) } return sb.String() } // CanCreateZettel returns true, if place could possibly create a new zettel. func (mgr *Manager) CanCreateZettel(ctx context.Context) bool { mgr.mgrMx.RLock() defer mgr.mgrMx.RUnlock() return mgr.started && mgr.subplaces[0].CanCreateZettel(ctx) } // CreateZettel creates a new zettel. func (mgr *Manager) CreateZettel(ctx context.Context, zettel domain.Zettel) (id.Zid, error) { mgr.mgrMx.RLock() defer mgr.mgrMx.RUnlock() if !mgr.started { return id.Invalid, place.ErrStopped } return mgr.subplaces[0].CreateZettel(ctx, zettel) } // GetZettel retrieves a specific zettel. func (mgr *Manager) GetZettel(ctx context.Context, zid id.Zid) (domain.Zettel, error) { mgr.mgrMx.RLock() defer mgr.mgrMx.RUnlock() if !mgr.started { return domain.Zettel{}, place.ErrStopped } for _, p := range mgr.subplaces { if z, err := p.GetZettel(ctx, zid); err != place.ErrNotFound { if err == nil { mgr.Enrich(ctx, z.Meta) } return z, err } } return domain.Zettel{}, place.ErrNotFound } // GetMeta retrieves just the meta data of a specific zettel. func (mgr *Manager) GetMeta(ctx context.Context, zid id.Zid) (*meta.Meta, error) { mgr.mgrMx.RLock() defer mgr.mgrMx.RUnlock() if !mgr.started { return nil, place.ErrStopped } for _, p := range mgr.subplaces { if m, err := p.GetMeta(ctx, zid); err != place.ErrNotFound { if err == nil { mgr.Enrich(ctx, m) } return m, err } } return nil, place.ErrNotFound } // FetchZids returns the set of all zettel identifer managed by the place. func (mgr *Manager) FetchZids(ctx context.Context) (result id.Set, err error) { mgr.mgrMx.RLock() defer mgr.mgrMx.RUnlock() if !mgr.started { return nil, place.ErrStopped } for _, p := range mgr.subplaces { zids, err := p.FetchZids(ctx) if err != nil { return nil, err } if result == nil { result = zids } else if len(result) <= len(zids) { for zid := range result { zids[zid] = true } result = zids } else { for zid := range zids { result[zid] = true } } } return result, nil } // SelectMeta returns all zettel meta data that match the selection // criteria. The result is ordered by descending zettel id. func (mgr *Manager) SelectMeta(ctx context.Context, s *search.Search) ([]*meta.Meta, error) { mgr.mgrMx.RLock() defer mgr.mgrMx.RUnlock() if !mgr.started { return nil, place.ErrStopped } var result []*meta.Meta match := s.CompileMatch(mgr) for _, p := range mgr.subplaces { selected, err := p.SelectMeta(ctx, match) if err != nil { return nil, err } sort.Slice(selected, func(i, j int) bool { return selected[i].Zid > selected[j].Zid }) if len(result) == 0 { result = selected } else { result = place.MergeSorted(result, selected) } } if s == nil { return result, nil } return s.Sort(result), nil } // CanUpdateZettel returns true, if place could possibly update the given zettel. func (mgr *Manager) CanUpdateZettel(ctx context.Context, zettel domain.Zettel) bool { mgr.mgrMx.RLock() defer mgr.mgrMx.RUnlock() return mgr.started && mgr.subplaces[0].CanUpdateZettel(ctx, zettel) } // UpdateZettel updates an existing zettel. func (mgr *Manager) UpdateZettel(ctx context.Context, zettel domain.Zettel) error { mgr.mgrMx.RLock() defer mgr.mgrMx.RUnlock() if !mgr.started { return place.ErrStopped } // Remove all (computed) properties from metadata before storing the zettel. zettel.Meta = zettel.Meta.Clone() for _, p := range zettel.Meta.PairsRest(true) { if mgr.propertyKeys[p.Key] { zettel.Meta.Delete(p.Key) } } return mgr.subplaces[0].UpdateZettel(ctx, zettel) } // AllowRenameZettel returns true, if place will not disallow renaming the zettel. func (mgr *Manager) AllowRenameZettel(ctx context.Context, zid id.Zid) bool { mgr.mgrMx.RLock() defer mgr.mgrMx.RUnlock() if !mgr.started { return false } for _, p := range mgr.subplaces { if !p.AllowRenameZettel(ctx, zid) { return false } } return true } // RenameZettel changes the current zid to a new zid. func (mgr *Manager) RenameZettel(ctx context.Context, curZid, newZid id.Zid) error { mgr.mgrMx.RLock() defer mgr.mgrMx.RUnlock() if !mgr.started { return place.ErrStopped } for i, p := range mgr.subplaces { err := p.RenameZettel(ctx, curZid, newZid) if err != nil && !errors.Is(err, place.ErrNotFound) { for j := 0; j < i; j++ { mgr.subplaces[j].RenameZettel(ctx, newZid, curZid) } return err } } return nil } // CanDeleteZettel returns true, if place could possibly delete the given zettel. func (mgr *Manager) CanDeleteZettel(ctx context.Context, zid id.Zid) bool { mgr.mgrMx.RLock() defer mgr.mgrMx.RUnlock() if !mgr.started { return false } for _, p := range mgr.subplaces { if p.CanDeleteZettel(ctx, zid) { return true } } return false } // DeleteZettel removes the zettel from the place. func (mgr *Manager) DeleteZettel(ctx context.Context, zid id.Zid) error { mgr.mgrMx.RLock() defer mgr.mgrMx.RUnlock() if !mgr.started { return place.ErrStopped } for _, p := range mgr.subplaces { err := p.DeleteZettel(ctx, zid) if err == nil { return nil } if !errors.Is(err, place.ErrNotFound) && !errors.Is(err, place.ErrReadOnly) { return err } } return place.ErrNotFound } |
Added place/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 | //----------------------------------------------------------------------------- // Copyright (c) 2021 Detlef Stern // // This file is part of zettelstore. // // Zettelstore is licensed under the latest version of the EUPL (European Union // Public License). Please see file LICENSE.txt for your rights and obligations // under this license. //----------------------------------------------------------------------------- // Package store contains general index data for storing a zettel index. package store import ( "context" "io" "zettelstore.de/z/domain/id" "zettelstore.de/z/place" "zettelstore.de/z/search" ) // Stats records statistics about the store. type Stats struct { // Zettel is the number of zettel managed by the indexer. Zettel int // Updates count the number of metadata updates. Updates uint64 // Words count the different words stored in the store. Words uint64 // Urls count the different URLs stored in the store. Urls uint64 } // Store all relevant zettel data. There may be multiple implementations, i.e. // memory-based, file-based, based on SQLite, ... type Store interface { place.Enricher search.Selector // 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 place/manager/store/wordset.go.
> > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 | //----------------------------------------------------------------------------- // Copyright (c) 2021 Detlef Stern // // This file is part of zettelstore. // // Zettelstore is licensed under the latest version of the EUPL (European Union // Public License). Please see file LICENSE.txt for your rights and obligations // under this license. //----------------------------------------------------------------------------- // Package store contains general index data for storing a zettel index. package store // WordSet contains the set of all words, with the count of their occurrences. type WordSet map[string]int // NewWordSet returns a new WordSet. func NewWordSet() WordSet { return make(WordSet) } // Add one word to the set func (ws WordSet) Add(s string) { ws[s] = ws[s] + 1 } // Words gives the slice of all words in the set. func (ws WordSet) Words() []string { if len(ws) == 0 { return nil } words := make([]string, 0, len(ws)) for w := range ws { words = append(words, w) } return words } // Diff calculates the word slice to be added and to be removed from oldWords // to get the given word set. func (ws WordSet) Diff(oldWords []string) (newWords, removeWords []string) { if len(ws) == 0 { return nil, oldWords } if len(oldWords) == 0 { return ws.Words(), nil } oldSet := make(WordSet, len(oldWords)) for _, ow := range oldWords { if _, ok := ws[ow]; ok { oldSet[ow] = 1 continue } removeWords = append(removeWords, ow) } for w := range ws { if _, ok := oldSet[w]; ok { continue } newWords = append(newWords, w) } return newWords, removeWords } |
Added place/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 | //----------------------------------------------------------------------------- // Copyright (c) 2021 Detlef Stern // // This file is part of zettelstore. // // Zettelstore is licensed under the latest version of the EUPL (European Union // Public License). Please see file LICENSE.txt for your rights and obligations // under this license. //----------------------------------------------------------------------------- // Package store contains general index data for storing a zettel index. package store_test import ( "sort" "testing" "zettelstore.de/z/place/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) { 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) { 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 place/manager/store/zettel.go.
> > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 | //----------------------------------------------------------------------------- // Copyright (c) 2021 Detlef Stern // // This file is part of zettelstore. // // Zettelstore is licensed under the latest version of the EUPL (European Union // Public License). Please see file LICENSE.txt for your rights and obligations // under this license. //----------------------------------------------------------------------------- // Package store contains general index data for storing a zettel index. package store import "zettelstore.de/z/domain/id" // ZettelIndex contains all index data of a zettel. type ZettelIndex struct { Zid id.Zid // zid of the indexed zettel backrefs id.Set // set of back references metarefs map[string]id.Set // references to inverse keys deadrefs id.Set // set of dead references words WordSet urls WordSet } // NewZettelIndex creates a new zettel index. func NewZettelIndex(zid id.Zid) *ZettelIndex { return &ZettelIndex{ Zid: zid, backrefs: id.NewSet(), metarefs: make(map[string]id.Set), deadrefs: id.NewSet(), } } // AddBackRef adds a reference to a zettel where the current zettel links to // without any more information. func (zi *ZettelIndex) AddBackRef(zid id.Zid) { zi.backrefs[zid] = true } // AddMetaRef adds a named reference to a zettel. On that zettel, the given // metadata key should point back to the current zettel. func (zi *ZettelIndex) AddMetaRef(key string, zid id.Zid) { if zids, ok := zi.metarefs[key]; ok { zids[zid] = true return } zi.metarefs[key] = id.NewSet(zid) } // AddDeadRef adds a dead reference to a zettel. func (zi *ZettelIndex) AddDeadRef(zid id.Zid) { zi.deadrefs[zid] = true } // SetWords sets the words to the given value. func (zi *ZettelIndex) SetWords(words WordSet) { zi.words = words } // SetUrls sets the words to the given value. func (zi *ZettelIndex) SetUrls(urls WordSet) { zi.urls = urls } // GetDeadRefs returns all dead references as a sorted list. func (zi *ZettelIndex) GetDeadRefs() id.Slice { return zi.deadrefs.Sorted() } // GetBackRefs returns all back references as a sorted list. func (zi *ZettelIndex) GetBackRefs() id.Slice { return zi.backrefs.Sorted() } // GetMetaRefs returns all meta references as a map of strings to a sorted list of references func (zi *ZettelIndex) GetMetaRefs() map[string]id.Slice { if len(zi.metarefs) == 0 { return nil } result := make(map[string]id.Slice, len(zi.metarefs)) for key, refs := range zi.metarefs { result[key] = refs.Sorted() } return result } // GetWords returns a reference to the set of words. It must not be modified. func (zi *ZettelIndex) GetWords() WordSet { return zi.words } // GetUrls returns a reference to the set of URLs. It must not be modified. func (zi *ZettelIndex) GetUrls() WordSet { return zi.urls } |
Added place/memplace/memplace.go.
> > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 | //----------------------------------------------------------------------------- // Copyright (c) 2020-2021 Detlef Stern // // This file is part of zettelstore. // // Zettelstore is licensed under the latest version of the EUPL (European Union // Public License). Please see file LICENSE.txt for your rights and obligations // under this license. //----------------------------------------------------------------------------- // Package memplace stores zettel volatile in main memory. package memplace import ( "context" "net/url" "sync" "zettelstore.de/z/domain" "zettelstore.de/z/domain/id" "zettelstore.de/z/domain/meta" "zettelstore.de/z/place" "zettelstore.de/z/place/manager" "zettelstore.de/z/search" ) func init() { manager.Register( "mem", func(u *url.URL, cdata *manager.ConnectData) (place.ManagedPlace, error) { return &memPlace{u: u, cdata: *cdata}, nil }) } type memPlace struct { u *url.URL cdata manager.ConnectData zettel map[id.Zid]domain.Zettel mx sync.RWMutex } func (mp *memPlace) notifyChanged(reason place.UpdateReason, zid id.Zid) { if chci := mp.cdata.Notify; chci != nil { chci <- place.UpdateInfo{Reason: reason, Zid: zid} } } func (mp *memPlace) Location() string { return mp.u.String() } func (mp *memPlace) Start(ctx context.Context) error { mp.mx.Lock() mp.zettel = make(map[id.Zid]domain.Zettel) mp.mx.Unlock() return nil } func (mp *memPlace) Stop(ctx context.Context) error { mp.mx.Lock() mp.zettel = nil mp.mx.Unlock() return nil } func (mp *memPlace) CanCreateZettel(ctx context.Context) bool { return true } func (mp *memPlace) CreateZettel(ctx context.Context, zettel domain.Zettel) (id.Zid, error) { mp.mx.Lock() zid, err := place.GetNewZid(func(zid id.Zid) (bool, error) { _, ok := mp.zettel[zid] return !ok, nil }) if err != nil { mp.mx.Unlock() return id.Invalid, err } meta := zettel.Meta.Clone() meta.Zid = zid zettel.Meta = meta mp.zettel[zid] = zettel mp.mx.Unlock() mp.notifyChanged(place.OnUpdate, zid) return zid, nil } func (mp *memPlace) GetZettel(ctx context.Context, zid id.Zid) (domain.Zettel, error) { mp.mx.RLock() zettel, ok := mp.zettel[zid] mp.mx.RUnlock() if !ok { return domain.Zettel{}, place.ErrNotFound } zettel.Meta = zettel.Meta.Clone() return zettel, nil } func (mp *memPlace) GetMeta(ctx context.Context, zid id.Zid) (*meta.Meta, error) { mp.mx.RLock() zettel, ok := mp.zettel[zid] mp.mx.RUnlock() if !ok { return nil, place.ErrNotFound } return zettel.Meta.Clone(), nil } func (mp *memPlace) FetchZids(ctx context.Context) (id.Set, error) { mp.mx.RLock() result := id.NewSetCap(len(mp.zettel)) for zid := range mp.zettel { result[zid] = true } mp.mx.RUnlock() return result, nil } func (mp *memPlace) SelectMeta(ctx context.Context, match search.MetaMatchFunc) ([]*meta.Meta, error) { result := make([]*meta.Meta, 0, len(mp.zettel)) mp.mx.RLock() for _, zettel := range mp.zettel { m := zettel.Meta.Clone() mp.cdata.Enricher.Enrich(ctx, m) if match(m) { result = append(result, m) } } mp.mx.RUnlock() return result, nil } func (mp *memPlace) CanUpdateZettel(ctx context.Context, zettel domain.Zettel) bool { return true } func (mp *memPlace) UpdateZettel(ctx context.Context, zettel domain.Zettel) error { mp.mx.Lock() meta := zettel.Meta.Clone() if !meta.Zid.IsValid() { return &place.ErrInvalidID{Zid: meta.Zid} } zettel.Meta = meta mp.zettel[meta.Zid] = zettel mp.mx.Unlock() mp.notifyChanged(place.OnUpdate, meta.Zid) return nil } func (mp *memPlace) AllowRenameZettel(ctx context.Context, zid id.Zid) bool { return true } func (mp *memPlace) RenameZettel(ctx context.Context, curZid, newZid id.Zid) error { mp.mx.Lock() zettel, ok := mp.zettel[curZid] if !ok { mp.mx.Unlock() return place.ErrNotFound } // Check that there is no zettel with newZid if _, ok = mp.zettel[newZid]; ok { mp.mx.Unlock() return &place.ErrInvalidID{Zid: newZid} } meta := zettel.Meta.Clone() meta.Zid = newZid zettel.Meta = meta mp.zettel[newZid] = zettel delete(mp.zettel, curZid) mp.mx.Unlock() mp.notifyChanged(place.OnDelete, curZid) mp.notifyChanged(place.OnUpdate, newZid) return nil } func (mp *memPlace) CanDeleteZettel(ctx context.Context, zid id.Zid) bool { mp.mx.RLock() _, ok := mp.zettel[zid] mp.mx.RUnlock() return ok } func (mp *memPlace) DeleteZettel(ctx context.Context, zid id.Zid) error { mp.mx.Lock() if _, ok := mp.zettel[zid]; !ok { mp.mx.Unlock() return place.ErrNotFound } delete(mp.zettel, zid) mp.mx.Unlock() mp.notifyChanged(place.OnDelete, zid) return nil } func (mp *memPlace) ReadStats(st *place.ManagedPlaceStats) { st.ReadOnly = false mp.mx.RLock() st.Zettel = len(mp.zettel) mp.mx.RUnlock() } |
Added place/merge.go.
> > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 | //----------------------------------------------------------------------------- // Copyright (c) 2020-2021 Detlef Stern // // This file is part of zettelstore. // // Zettelstore is licensed under the latest version of the EUPL (European Union // Public License). Please see file LICENSE.txt for your rights and obligations // under this license. //----------------------------------------------------------------------------- // Package place provides a generic interface to zettel places. package place import "zettelstore.de/z/domain/meta" // MergeSorted returns a merged sequence of metadata, sorted by Zid. // The lists first and second must be sorted descending by Zid. func MergeSorted(first, second []*meta.Meta) []*meta.Meta { lenFirst := len(first) lenSecond := len(second) result := make([]*meta.Meta, 0, lenFirst+lenSecond) iFirst := 0 iSecond := 0 for iFirst < lenFirst && iSecond < lenSecond { zidFirst := first[iFirst].Zid zidSecond := second[iSecond].Zid if zidFirst > zidSecond { result = append(result, first[iFirst]) iFirst++ } else if zidFirst < zidSecond { result = append(result, second[iSecond]) iSecond++ } else { // zidFirst == zidSecond result = append(result, first[iFirst]) iFirst++ iSecond++ } } if iFirst < lenFirst { result = append(result, first[iFirst:]...) } else { result = append(result, second[iSecond:]...) } return result } |
Added place/place.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 | //----------------------------------------------------------------------------- // Copyright (c) 2020-2021 Detlef Stern // // This file is part of zettelstore. // // Zettelstore is licensed under the latest version of the EUPL (European Union // Public License). Please see file LICENSE.txt for your rights and obligations // under this license. //----------------------------------------------------------------------------- // Package place provides a generic interface to zettel places. package place import ( "context" "errors" "fmt" "io" "time" "zettelstore.de/z/domain" "zettelstore.de/z/domain/id" "zettelstore.de/z/domain/meta" "zettelstore.de/z/search" ) // BasePlace is implemented by all Zettel places. type BasePlace interface { // Location returns some information where the place is located. // Format is dependent of the place. Location() string // CanCreateZettel returns true, if place could possibly create a new zettel. CanCreateZettel(ctx context.Context) bool // CreateZettel creates a new zettel. // Returns the new zettel id (and an error indication). CreateZettel(ctx context.Context, zettel domain.Zettel) (id.Zid, error) // GetZettel retrieves a specific zettel. GetZettel(ctx context.Context, zid id.Zid) (domain.Zettel, error) // GetMeta retrieves just the meta data of a specific zettel. GetMeta(ctx context.Context, zid id.Zid) (*meta.Meta, error) // FetchZids returns the set of all zettel identifer managed by the place. FetchZids(ctx context.Context) (id.Set, error) // CanUpdateZettel returns true, if place 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 place 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 place could possibly delete the given zettel. CanDeleteZettel(ctx context.Context, zid id.Zid) bool // DeleteZettel removes the zettel from the place. DeleteZettel(ctx context.Context, zid id.Zid) error } // ManagedPlace is the interface of managed places. type ManagedPlace interface { BasePlace // SelectMeta returns all zettel meta data that match the selection criteria. SelectMeta(ctx context.Context, match search.MetaMatchFunc) ([]*meta.Meta, error) // ReadStats populates st with place statistics ReadStats(st *ManagedPlaceStats) } // ManagedPlaceStats records statistics about the place. type ManagedPlaceStats struct { // ReadOnly indicates that the places cannot be changed ReadOnly bool // Zettel is the number of zettel managed by the place. Zettel int } // StartStopper performs simple lifecycle management. type StartStopper interface { // Start the place. Now all other functions of the place are allowed. // Starting an already started place is not allowed. Start(ctx context.Context) error // Stop the started place. Now only the Start() function is allowed. Stop(ctx context.Context) error } // Place is a place to be used outside the place package and its descendants. type Place interface { BasePlace // SelectMeta returns a list of metadata that comply to the given selection criteria. SelectMeta(ctx context.Context, s *search.Search) ([]*meta.Meta, error) // ReadStats populates st with place statistics ReadStats(st *Stats) // Dump internal data to a Writer. Dump(w io.Writer) } // Stats record stattistics about a full place. type Stats struct { // ReadOnly indicates that the places cannot be changed ReadOnly bool // NumManagedPlaces is the number of places managed. NumManagedPlaces int // Zettel is the number of zettel managed by the place, including // duplicates across managed places. ZettelTotal int // LastReload stores the timestamp when a full re-index was done. LastReload time.Time // IndexesSinceReload counts indexing a zettel since the full re-index. IndexesSinceReload uint64 // DurLastIndex is the duration of the last index run. This could be a // full re-index or a re-index of a single zettel. DurLastIndex time.Duration // 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 place-managing place. type Manager interface { Place StartStopper Subject } // UpdateReason gives an indication, why the ObserverFunc was called. type UpdateReason uint8 // Values for Reason const ( _ UpdateReason = iota OnReload // Place was reloaded OnUpdate // A zettel was created or changed OnDelete // A zettel was removed ) // UpdateInfo contains all the data about a changed zettel. type UpdateInfo struct { Place Place Reason UpdateReason Zid id.Zid } // UpdateFunc is a function to be called when a change is detected. type UpdateFunc func(UpdateInfo) // Subject is a place 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) } // NoEnrichContext will signal an enricher that nothing has to be done. // This is useful for an Indexer, but also for some place.Place calls, when // just the plain metadata is needed. func NoEnrichContext(ctx context.Context) context.Context { return context.WithValue(ctx, ctxNoEnrichKey, &ctxNoEnrichKey) } type ctxNoEnrichType struct{} var ctxNoEnrichKey ctxNoEnrichType // DoNotEnrich determines if the context is marked to not enrich metadata. func DoNotEnrich(ctx context.Context) bool { _, ok := ctx.Value(ctxNoEnrichKey).(*ctxNoEnrichType) return ok } // ErrNotAllowed is returned if the caller is not allowed to perform the operation. type ErrNotAllowed struct { Op string User *meta.Meta Zid id.Zid } // NewErrNotAllowed creates an new authorization error. func NewErrNotAllowed(op string, user *meta.Meta, zid id.Zid) error { return &ErrNotAllowed{ Op: op, User: user, Zid: zid, } } func (err *ErrNotAllowed) Error() string { if err.User == nil { if err.Zid.IsValid() { return fmt.Sprintf( "operation %q on zettel %v not allowed for not authorized user", err.Op, err.Zid.String()) } return fmt.Sprintf("operation %q not allowed for not authorized user", err.Op) } if err.Zid.IsValid() { return fmt.Sprintf( "operation %q on zettel %v not allowed for user %v/%v", err.Op, err.Zid.String(), err.User.GetDefault(meta.KeyUserID, "?"), err.User.Zid.String()) } return fmt.Sprintf( "operation %q not allowed for user %v/%v", err.Op, err.User.GetDefault(meta.KeyUserID, "?"), err.User.Zid.String()) } // Is return true, if the error is of type ErrNotAllowed. func (err *ErrNotAllowed) Is(target error) bool { return true } // ErrStarted is returned when trying to start an already started place. var ErrStarted = errors.New("place is already started") // ErrStopped is returned if calling methods on a place that was not started. var ErrStopped = errors.New("place is stopped") // ErrReadOnly is returned if there is an attepmt to write to a read-only place. var ErrReadOnly = errors.New("read-only place") // ErrNotFound is returned if a zettel was not found in the place. var ErrNotFound = errors.New("zettel not found") // ErrConflict is returned if a place operation detected a conflict.. // One example: if calculating a new zettel identifier takes too long. var ErrConflict = errors.New("conflict") // ErrInvalidID is returned if the zettel id is not appropriate for the place operation. type ErrInvalidID struct{ Zid id.Zid } func (err *ErrInvalidID) Error() string { return "invalid Zettel id: " + err.Zid.String() } |
Added place/progplace/config.go.
> > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 | //----------------------------------------------------------------------------- // Copyright (c) 2020-2021 Detlef Stern // // This file is part of zettelstore. // // Zettelstore is licensed under the latest version of the EUPL (European Union // Public License). Please see file LICENSE.txt for your rights and obligations // under this license. //----------------------------------------------------------------------------- // Package progplace provides zettel that inform the user about the internal Zettelstore state. package progplace import ( "strings" "zettelstore.de/z/domain/id" "zettelstore.de/z/domain/meta" ) func genConfigZettelM(zid id.Zid) *meta.Meta { if myConfig == nil { return nil } m := meta.New(zid) m.Set(meta.KeyTitle, "Zettelstore Startup Configuration") m.Set(meta.KeyVisibility, meta.ValueVisibilityExpert) return m } func genConfigZettelC(m *meta.Meta) string { var sb strings.Builder for i, p := range myConfig.Pairs(false) { if i > 0 { sb.WriteByte('\n') } sb.WriteString("; ''") sb.WriteString(p.Key) sb.WriteString("''") if p.Value != "" { sb.WriteString("\n: ``") for _, r := range p.Value { if r == '`' { sb.WriteByte('\\') } sb.WriteRune(r) } sb.WriteString("``") } } return sb.String() } |
Added place/progplace/keys.go.
> > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 | //----------------------------------------------------------------------------- // Copyright (c) 2020-2021 Detlef Stern // // This file is part of zettelstore. // // Zettelstore is licensed under the latest version of the EUPL (European Union // Public License). Please see file LICENSE.txt for your rights and obligations // under this license. //----------------------------------------------------------------------------- // Package progplace provides zettel that inform the user about the internal Zettelstore state. package progplace import ( "fmt" "strings" "zettelstore.de/z/domain/id" "zettelstore.de/z/domain/meta" ) func genKeysM(zid id.Zid) *meta.Meta { m := meta.New(zid) m.Set(meta.KeyTitle, "Zettelstore Supported Metadata Keys") m.Set(meta.KeyVisibility, meta.ValueVisibilityLogin) return m } func genKeysC(*meta.Meta) string { keys := meta.GetSortedKeyDescriptions() var sb strings.Builder sb.WriteString("|=Name<|=Type<|=Computed?:|=Property?:\n") for _, kd := range keys { fmt.Fprintf(&sb, "|%v|%v|%v|%v\n", kd.Name, kd.Type.Name, kd.IsComputed(), kd.IsProperty()) } return sb.String() } |
Added place/progplace/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 Detlef Stern // // This file is part of zettelstore. // // Zettelstore is licensed under the latest version of the EUPL (European Union // Public License). Please see file LICENSE.txt for your rights and obligations // under this license. //----------------------------------------------------------------------------- // Package progplace provides zettel that inform the user about the internal // Zettelstore state. package progplace import ( "fmt" "strings" "zettelstore.de/z/domain/id" "zettelstore.de/z/domain/meta" "zettelstore.de/z/kernel" ) func genManagerM(zid id.Zid) *meta.Meta { m := meta.New(zid) m.Set(meta.KeyTitle, "Zettelstore Place Manager") return m } func genManagerC(*meta.Meta) string { kvl := kernel.Main.GetServiceStatistics(kernel.PlaceService) if len(kvl) == 0 { return "No statistics available" } var sb strings.Builder sb.WriteString("|=Name|=Value>\n") for _, kv := range kvl { fmt.Fprintf(&sb, "| %v | %v\n", kv.Key, kv.Value) } return sb.String() } |
Added place/progplace/progplace.go.
> > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 | //----------------------------------------------------------------------------- // Copyright (c) 2020-2021 Detlef Stern // // This file is part of zettelstore. // // Zettelstore is licensed under the latest version of the EUPL (European Union // Public License). Please see file LICENSE.txt for your rights and obligations // under this license. //----------------------------------------------------------------------------- // Package progplace provides zettel that inform the user about the internal // Zettelstore state. package progplace import ( "context" "net/url" "zettelstore.de/z/domain" "zettelstore.de/z/domain/id" "zettelstore.de/z/domain/meta" "zettelstore.de/z/place" "zettelstore.de/z/place/manager" "zettelstore.de/z/search" ) func init() { manager.Register( " prog", func(u *url.URL, cdata *manager.ConnectData) (place.ManagedPlace, error) { return getPlace(cdata.Enricher), nil }) } type progPlace struct { filter place.Enricher } var myConfig *meta.Meta var myZettel = map[id.Zid]struct { meta func(id.Zid) *meta.Meta content func(*meta.Meta) string }{ id.VersionZid: {genVersionBuildM, genVersionBuildC}, id.HostZid: {genVersionHostM, genVersionHostC}, id.OperatingSystemZid: {genVersionOSM, genVersionOSC}, id.PlaceManagerZid: {genManagerM, genManagerC}, id.MetadataKeyZid: {genKeysM, genKeysC}, id.StartupConfigurationZid: {genConfigZettelM, genConfigZettelC}, } // Get returns the one program place. func getPlace(mf place.Enricher) place.ManagedPlace { return &progPlace{filter: mf} } // Setup remembers important values. func Setup(cfg *meta.Meta) { myConfig = cfg.Clone() } func (pp *progPlace) Location() string { return "" } func (pp *progPlace) CanCreateZettel(ctx context.Context) bool { return false } func (pp *progPlace) CreateZettel( ctx context.Context, zettel domain.Zettel) (id.Zid, error) { return id.Invalid, place.ErrReadOnly } func (pp *progPlace) GetZettel(ctx context.Context, zid id.Zid) (domain.Zettel, error) { if gen, ok := myZettel[zid]; ok && gen.meta != nil { if m := gen.meta(zid); m != nil { updateMeta(m) if genContent := gen.content; genContent != nil { return domain.Zettel{ Meta: m, Content: domain.NewContent(genContent(m)), }, nil } return domain.Zettel{Meta: m}, nil } } return domain.Zettel{}, place.ErrNotFound } func (pp *progPlace) GetMeta(ctx context.Context, zid id.Zid) (*meta.Meta, error) { if gen, ok := myZettel[zid]; ok { if genMeta := gen.meta; genMeta != nil { if m := genMeta(zid); m != nil { updateMeta(m) return m, nil } } } return nil, place.ErrNotFound } func (pp *progPlace) FetchZids(ctx context.Context) (id.Set, error) { result := id.NewSetCap(len(myZettel)) for zid, gen := range myZettel { if genMeta := gen.meta; genMeta != nil { if genMeta(zid) != nil { result[zid] = true } } } return result, nil } func (pp *progPlace) SelectMeta(ctx context.Context, match search.MetaMatchFunc) (res []*meta.Meta, err error) { for zid, gen := range myZettel { if genMeta := gen.meta; genMeta != nil { if m := genMeta(zid); m != nil { updateMeta(m) pp.filter.Enrich(ctx, m) if match(m) { res = append(res, m) } } } } return res, nil } func (pp *progPlace) CanUpdateZettel(ctx context.Context, zettel domain.Zettel) bool { return false } func (pp *progPlace) UpdateZettel(ctx context.Context, zettel domain.Zettel) error { return place.ErrReadOnly } func (pp *progPlace) AllowRenameZettel(ctx context.Context, zid id.Zid) bool { _, ok := myZettel[zid] return !ok } func (pp *progPlace) RenameZettel(ctx context.Context, curZid, newZid id.Zid) error { if _, ok := myZettel[curZid]; ok { return place.ErrReadOnly } return place.ErrNotFound } func (pp *progPlace) CanDeleteZettel(ctx context.Context, zid id.Zid) bool { return false } func (pp *progPlace) DeleteZettel(ctx context.Context, zid id.Zid) error { if _, ok := myZettel[zid]; ok { return place.ErrReadOnly } return place.ErrNotFound } func (pp *progPlace) ReadStats(st *place.ManagedPlaceStats) { st.ReadOnly = true st.Zettel = len(myZettel) } func updateMeta(m *meta.Meta) { m.Set(meta.KeyNoIndex, meta.ValueTrue) m.Set(meta.KeySyntax, meta.ValueSyntaxZmk) m.Set(meta.KeyRole, meta.ValueRoleConfiguration) m.Set(meta.KeyLang, meta.ValueLangEN) m.Set(meta.KeyReadOnly, meta.ValueTrue) if _, ok := m.Get(meta.KeyVisibility); !ok { m.Set(meta.KeyVisibility, meta.ValueVisibilityExpert) } } |
Added place/progplace/version.go.
> > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 | //----------------------------------------------------------------------------- // Copyright (c) 2020-2021 Detlef Stern // // This file is part of zettelstore. // // Zettelstore is licensed under the latest version of the EUPL (European Union // Public License). Please see file LICENSE.txt for your rights and obligations // under this license. //----------------------------------------------------------------------------- // Package progplace provides zettel that inform the user about the internal Zettelstore state. package progplace import ( "fmt" "zettelstore.de/z/domain/id" "zettelstore.de/z/domain/meta" "zettelstore.de/z/kernel" ) func getVersionMeta(zid id.Zid, title string) *meta.Meta { m := meta.New(zid) m.Set(meta.KeyTitle, title) m.Set(meta.KeyVisibility, meta.ValueVisibilityExpert) return m } func genVersionBuildM(zid id.Zid) *meta.Meta { m := getVersionMeta(zid, "Zettelstore Version") m.Set(meta.KeyVisibility, meta.ValueVisibilityPublic) return m } func genVersionBuildC(*meta.Meta) string { return kernel.Main.GetConfig(kernel.CoreService, kernel.CoreVersion).(string) } func genVersionHostM(zid id.Zid) *meta.Meta { return getVersionMeta(zid, "Zettelstore Host") } func genVersionHostC(*meta.Meta) string { return kernel.Main.GetConfig(kernel.CoreService, kernel.CoreHostname).(string) } func genVersionOSM(zid id.Zid) *meta.Meta { return getVersionMeta(zid, "Zettelstore Operating System") } func genVersionOSC(*meta.Meta) string { return fmt.Sprintf( "%v/%v", kernel.Main.GetConfig(kernel.CoreService, kernel.CoreGoOS).(string), kernel.Main.GetConfig(kernel.CoreService, kernel.CoreGoArch).(string), ) } |
Deleted query/compiled.go.
|
| < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < |
Deleted query/context.go.
|
| < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < |
Deleted query/parser.go.
|
| < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < |
Deleted query/parser_test.go.
|
| < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < |
Deleted query/print.go.
|
| < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < |
Deleted query/query.go.
|
| < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < |
Deleted query/retrieve.go.
|
| < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < |
Deleted query/select.go.
|
| < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < |
Deleted query/select_test.go.
|
| < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < |
Deleted query/sorter.go.
|
| < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < |
Deleted query/specs.go.
|
| < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < |
Deleted query/unlinked.go.
|
| < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < |
Added runes/runes.go.
> > > > > > > > > > > > > > > > > > > > > | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 | //----------------------------------------------------------------------------- // Copyright (c) 2020 Detlef Stern // // This file is part of zettelstore. // // Zettelstore is licensed under the latest version of the EUPL (European Union // Public License). Please see file LICENSE.txt for your rights and obligations // under this license. //----------------------------------------------------------------------------- // Package runes provides some functions on runes. package runes // IsSpace returns true if rune is a whitespace. func IsSpace(ch rune) bool { switch ch { case ' ', '\t': return true } return false } |
Added search/filter.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 | //----------------------------------------------------------------------------- // Copyright (c) 2020-2021 Detlef Stern // // This file is part of zettelstore. // // Zettelstore is licensed under the latest version of the EUPL (European Union // Public License). Please see file LICENSE.txt for your rights and obligations // under this license. //----------------------------------------------------------------------------- // Package search provides a zettel search. package search import ( "strings" "zettelstore.de/z/domain/meta" ) type matchFunc func(value string) bool func matchNever(value string) bool { return false } func matchAlways(value string) bool { return true } type matchSpec struct { key string match matchFunc } // compileFilter calculates a filter func based on the given filter. func compileFilter(tags expTagValues) MetaMatchFunc { specs, nomatch := createFilterSpecs(tags) if len(specs) == 0 && len(nomatch) == 0 { return nil } return makeSearchMetaFilterFunc(specs, nomatch) } func createFilterSpecs(tags map[string][]expValue) ([]matchSpec, []string) { specs := make([]matchSpec, 0, len(tags)) var nomatch []string for key, values := range tags { if !meta.KeyIsValid(key) { continue } if empty, negates := hasEmptyValues(values); empty { if negates == 0 { specs = append(specs, matchSpec{key, matchAlways}) continue } if len(values) < negates { specs = append(specs, matchSpec{key, matchNever}) continue } nomatch = append(nomatch, key) continue } match := createMatchFunc(key, values) if match != nil { specs = append(specs, matchSpec{key, match}) } } return specs, nomatch } func hasEmptyValues(values []expValue) (bool, int) { var negates int for _, v := range values { if v.value != "" { continue } if !v.negate { return true, 0 } negates++ } return negates > 0, negates } func createMatchFunc(key string, values []expValue) matchFunc { switch meta.Type(key) { case meta.TypeBool: return createMatchBoolFunc(values) case meta.TypeCredential: return matchNever case meta.TypeID, meta.TypeTimestamp: // ID and timestamp use the same layout return createMatchIDFunc(values) case meta.TypeIDSet: return createMatchIDSetFunc(values) case meta.TypeTagSet: return createMatchTagSetFunc(values) case meta.TypeWord: return createMatchWordFunc(values) case meta.TypeWordSet: return createMatchWordSetFunc(values) } return createMatchStringFunc(values) } func createMatchBoolFunc(values []expValue) matchFunc { preValues := make([]bool, 0, len(values)) for _, v := range values { boolValue := meta.BoolValue(v.value) if v.negate { boolValue = !boolValue } preValues = append(preValues, boolValue) } return func(value string) bool { bValue := meta.BoolValue(value) for _, v := range preValues { if bValue != v { return false } } return true } } func createMatchIDFunc(values []expValue) matchFunc { return func(value string) bool { for _, v := range values { if strings.HasPrefix(value, v.value) == v.negate { return false } } return true } } func createMatchIDSetFunc(values []expValue) matchFunc { idValues := preprocessSet(sliceToLower(values)) return func(value string) bool { ids := meta.ListFromValue(value) for _, neededIDs := range idValues { for _, neededID := range neededIDs { if matchAllID(ids, neededID.value) == neededID.negate { return false } } } return true } } func matchAllID(zettelIDs []string, neededID string) bool { for _, zt := range zettelIDs { if strings.HasPrefix(zt, neededID) { return true } } return false } func createMatchTagSetFunc(values []expValue) matchFunc { tagValues := processTagSet(preprocessSet(values)) 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 _, neededTags := range tagValues { for _, neededTag := range neededTags { if matchAllTag(tags, neededTag.value, neededTag.equal) == neededTag.negate { return false } } } return true } } type tagQueryValue struct { value string negate bool equal bool // not equal == prefix } func processTagSet(valueSet [][]expValue) [][]tagQueryValue { result := make([][]tagQueryValue, len(valueSet)) for i, values := range valueSet { tags := make([]tagQueryValue, len(values)) for j, val := range values { if tval := val.value; tval != "" && tval[0] == '#' { tval = meta.CleanTag(tval) tags[j] = tagQueryValue{value: tval, negate: val.negate, equal: true} } else { tags[j] = tagQueryValue{value: tval, negate: val.negate, equal: false} } } result[i] = tags } return result } func matchAllTag(zettelTags []string, neededTag string, equal bool) bool { if equal { for _, zt := range zettelTags { if zt == neededTag { return true } } } else { for _, zt := range zettelTags { if strings.HasPrefix(zt, neededTag) { return true } } } return false } func createMatchWordFunc(values []expValue) matchFunc { values = sliceToLower(values) return func(value string) bool { value = strings.ToLower(value) for _, v := range values { if (value == v.value) == v.negate { return false } } return true } } func createMatchWordSetFunc(values []expValue) matchFunc { wordValues := preprocessSet(sliceToLower(values)) return func(value string) bool { words := meta.ListFromValue(value) for _, neededWords := range wordValues { for _, neededWord := range neededWords { if matchAllWord(words, neededWord.value) == neededWord.negate { return false } } } return true } } func createMatchStringFunc(values []expValue) matchFunc { values = sliceToLower(values) return func(value string) bool { value = strings.ToLower(value) for _, v := range values { if strings.Contains(value, v.value) == v.negate { return false } } return true } } func makeSearchMetaFilterFunc(specs []matchSpec, nomatch []string) MetaMatchFunc { return func(m *meta.Meta) bool { for _, s := range specs { if value, ok := m.Get(s.key); !ok || !s.match(value) { return false } } for _, key := range nomatch { if _, ok := m.Get(key); ok { 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), negate: s.negate, }) } return result } 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, negate: elem.negate}) } } if len(valueElems) > 0 { result = append(result, valueElems) } } return result } func matchAllWord(zettelWords []string, neededWord string) bool { for _, zw := range zettelWords { if zw == neededWord { return true } } return false } |
Added search/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 | //----------------------------------------------------------------------------- // Copyright (c) 2020-2021 Detlef Stern // // This file is part of zettelstore. // // Zettelstore is licensed under the latest version of the EUPL (European Union // Public License). Please see file LICENSE.txt for your rights and obligations // under this license. //----------------------------------------------------------------------------- // Package search provides a zettel search. package search import ( "io" "sort" "strconv" "zettelstore.de/z/domain/meta" ) // Print the filter to a writer. func (s *Search) Print(w io.Writer) { if s.negate { io.WriteString(w, "NOT (") } space := false if len(s.search) > 0 { io.WriteString(w, "ANY") printFilterExprValues(w, s.search) space = true } names := make([]string, 0, len(s.tags)) for name := range s.tags { names = append(names, name) } sort.Strings(names) for _, name := range names { if space { io.WriteString(w, " AND ") } io.WriteString(w, name) printFilterExprValues(w, s.tags[name]) space = true } if s.negate { io.WriteString(w, ")") space = true } if ord := s.order; len(ord) > 0 { switch ord { case meta.KeyID: // Ignore case RandomOrder: space = printSpace(w, space) io.WriteString(w, "RANDOM") default: space = printSpace(w, space) io.WriteString(w, "SORT ") io.WriteString(w, ord) if s.descending { io.WriteString(w, " DESC") } } } if off := s.offset; off > 0 { space = printSpace(w, space) io.WriteString(w, "OFFSET ") io.WriteString(w, strconv.Itoa(off)) } if lim := s.limit; lim > 0 { _ = printSpace(w, space) io.WriteString(w, "LIMIT ") io.WriteString(w, strconv.Itoa(lim)) } } func printFilterExprValues(w io.Writer, values []expValue) { if len(values) == 0 { io.WriteString(w, " MATCH ANY") return } for j, val := range values { if j > 0 { io.WriteString(w, " AND") } if val.negate { io.WriteString(w, " NOT") } switch val.op { case cmpDefault: io.WriteString(w, " MATCH ") case cmpEqual: io.WriteString(w, " HAS ") case cmpPrefix: io.WriteString(w, " PREFIX ") case cmpSuffix: io.WriteString(w, " SUFFIX ") case cmpContains: io.WriteString(w, " CONTAINS ") default: io.WriteString(w, " MaTcH ") } if val.value == "" { io.WriteString(w, "ANY") } else { io.WriteString(w, val.value) } } } func printSpace(w io.Writer, space bool) bool { if space { io.WriteString(w, " ") } return true } |
Added search/search.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 | //----------------------------------------------------------------------------- // Copyright (c) 2020-2021 Detlef Stern // // This file is part of zettelstore. // // Zettelstore is licensed under the latest version of the EUPL (European Union // Public License). Please see file LICENSE.txt for your rights and obligations // under this license. //----------------------------------------------------------------------------- // Package search provides a zettel search. package search import ( "math/rand" "sort" "strings" "sync" "zettelstore.de/z/domain/id" "zettelstore.de/z/domain/meta" ) // Selector is used to select zettel identifier based on selection criteria. type Selector interface { // Select all zettel that contains the given exact word. // The word must be normalized through Unicode NKFD, trimmed and not empty. SelectEqual(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. SelectPrefix(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. SelectSuffix(suffix string) id.Set // Select all zettel that contains the given string. // The string must be normalized through Unicode NKFD, trimmed and not empty. SelectContains(s string) id.Set } // MetaMatchFunc is a function determine whethe some metadata should be filtered or not. type MetaMatchFunc func(*meta.Meta) bool // Search specifies a mechanism for selecting zettel. type Search struct { mx sync.RWMutex // Protects other attributes // Fields to be used for filtering preMatch MetaMatchFunc // Match that must be true tags expTagValues // Expected values for a tag search []expValue // Search string negate bool // Negate the result of the whole filtering process // Fields to be used for sorting order string // Name of meta key. None given: use "id" descending bool // Sort by order, but descending offset int // <= 0: no offset limit int // <= 0: no limit } type expTagValues map[string][]expValue // RandomOrder is a pseudo metadata key that selects a random order. const RandomOrder = "_random" type compareOp uint8 const ( cmpDefault compareOp = iota cmpEqual cmpPrefix cmpSuffix cmpContains ) type expValue struct { value string op compareOp negate bool } // AddExpr adds a match expression to the filter. func (s *Search) AddExpr(key, val string) *Search { val, negate, op := parseOp(strings.TrimSpace(val)) if s == nil { s = new(Search) } s.mx.Lock() defer s.mx.Unlock() if key == "" { s.search = append(s.search, expValue{value: val, op: op, negate: negate}) } else if s.tags == nil { s.tags = expTagValues{key: {{value: val, op: op, negate: negate}}} } else { s.tags[key] = append(s.tags[key], expValue{value: val, op: op, negate: negate}) } return s } func parseOp(s string) (r string, negate bool, op compareOp) { if s == "" { return s, false, cmpDefault } if s[0] == '\\' { return s[1:], false, cmpDefault } if s[0] == '!' { negate = true s = s[1:] } if s == "" { return s, negate, cmpDefault } if s[0] == '\\' { return s[1:], negate, cmpDefault } switch s[0] { case ':': return s[1:], negate, cmpDefault case '=': return s[1:], negate, cmpEqual case '>': return s[1:], negate, cmpPrefix case '<': return s[1:], negate, cmpSuffix case '~': return s[1:], negate, cmpContains } return s, negate, cmpDefault } // SetNegate changes the filter to reverse its selection. func (s *Search) SetNegate() *Search { if s == nil { s = new(Search) } s.mx.Lock() defer s.mx.Unlock() s.negate = true return s } // AddPreMatch adds the pre-filter selection predicate. func (s *Search) AddPreMatch(preMatch MetaMatchFunc) *Search { if s == nil { s = new(Search) } s.mx.Lock() defer s.mx.Unlock() if pre := s.preMatch; pre == nil { s.preMatch = preMatch } else { s.preMatch = func(m *meta.Meta) bool { return preMatch(m) && pre(m) } } return s } // AddOrder adds the given order to the search object. func (s *Search) AddOrder(key string, descending bool) *Search { if s == nil { s = new(Search) } s.mx.Lock() defer s.mx.Unlock() if s.order != "" { panic("order field already set: " + s.order) } s.order = key s.descending = descending return s } // SetOffset sets the given offset of the search object. func (s *Search) SetOffset(offset int) *Search { if s == nil { s = new(Search) } s.mx.Lock() defer s.mx.Unlock() if offset < 0 { offset = 0 } s.offset = offset return s } // GetOffset returns the current offset value. func (s *Search) GetOffset() int { if s == nil { return 0 } s.mx.RLock() defer s.mx.RUnlock() return s.offset } // SetLimit sets the given limit of the search object. func (s *Search) SetLimit(limit int) *Search { if s == nil { s = new(Search) } s.mx.Lock() defer s.mx.Unlock() if limit < 0 { limit = 0 } s.limit = limit return s } // GetLimit returns the current offset value. func (s *Search) GetLimit() int { if s == nil { return 0 } s.mx.RLock() defer s.mx.RUnlock() return s.limit } // HasComputedMetaKey returns true, if the filter references a metadata key which // a computed value. func (s *Search) HasComputedMetaKey() bool { if s == nil { return false } s.mx.RLock() defer s.mx.RUnlock() for key := range s.tags { if meta.IsComputed(key) { return true } } if order := s.order; order != "" && meta.IsComputed(order) { return true } return false } // CompileMatch returns a function to match meta data based on filter specification. func (s *Search) CompileMatch(selector Selector) MetaMatchFunc { if s == nil { return filterNone } s.mx.Lock() defer s.mx.Unlock() compMeta := compileFilter(s.tags) compSearch := compileFullSearch(selector, s.search) if preMatch := s.preMatch; preMatch != nil { return compilePreMatch(preMatch, compMeta, compSearch, s.negate) } return compileNoPreMatch(compMeta, compSearch, s.negate) } func filterNone(m *meta.Meta) bool { return true } func compilePreMatch(preMatch, compMeta, compSearch MetaMatchFunc, negate bool) MetaMatchFunc { if compMeta == nil { if compSearch == nil { return preMatch } if negate { return func(m *meta.Meta) bool { return preMatch(m) && !compSearch(m) } } return func(m *meta.Meta) bool { return preMatch(m) && compSearch(m) } } if compSearch == nil { if negate { return func(m *meta.Meta) bool { return preMatch(m) && !compMeta(m) } } return func(m *meta.Meta) bool { return preMatch(m) && compMeta(m) } } if negate { return func(m *meta.Meta) bool { return preMatch(m) && (!compMeta(m) || !compSearch(m)) } } return func(m *meta.Meta) bool { return preMatch(m) && compMeta(m) && compSearch(m) } } func compileNoPreMatch(compMeta, compSearch MetaMatchFunc, negate bool) MetaMatchFunc { if compMeta == nil { if compSearch == nil { if negate { return func(m *meta.Meta) bool { return false } } return filterNone } if negate { return func(m *meta.Meta) bool { return !compSearch(m) } } return compSearch } if compSearch == nil { if negate { return func(m *meta.Meta) bool { return !compMeta(m) } } return compMeta } if negate { return func(m *meta.Meta) bool { return !compMeta(m) || !compSearch(m) } } return func(m *meta.Meta) bool { return compMeta(m) && compSearch(m) } } // Sort applies the sorter to the slice of meta data. func (s *Search) Sort(metaList []*meta.Meta) []*meta.Meta { if len(metaList) == 0 { return metaList } if s == nil { sort.Slice(metaList, func(i, j int) bool { return metaList[i].Zid > metaList[j].Zid }) return metaList } if s.order == "" { sort.Slice(metaList, createSortFunc(meta.KeyID, true, metaList)) } else if s.order == RandomOrder { rand.Shuffle(len(metaList), func(i, j int) { metaList[i], metaList[j] = metaList[j], metaList[i] }) } else { sort.Slice(metaList, createSortFunc(s.order, s.descending, metaList)) } if s.offset > 0 { if s.offset > len(metaList) { return nil } metaList = metaList[s.offset:] } if s.limit > 0 && s.limit < len(metaList) { metaList = metaList[:s.limit] } return metaList } |
Added search/selector.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 | //----------------------------------------------------------------------------- // Copyright (c) 2021 Detlef Stern // // This file is part of zettelstore. // // Zettelstore is licensed under the latest version of the EUPL (European Union // Public License). Please see file LICENSE.txt for your rights and obligations // under this license. //----------------------------------------------------------------------------- // Package search provides a zettel search. package search // This file is about "compiling" a search expression into a function. import ( "fmt" "strings" "zettelstore.de/z/domain/id" "zettelstore.de/z/domain/meta" "zettelstore.de/z/strfun" ) func compileFullSearch(selector Selector, search []expValue) MetaMatchFunc { normSearch := compileNormalizedSearch(selector, search) plainSearch := compilePlainSearch(selector, search) if normSearch == nil { if plainSearch == nil { return nil } return plainSearch } if plainSearch == nil { return normSearch } return func(m *meta.Meta) bool { return normSearch(m) || plainSearch(m) } } func compileNormalizedSearch(selector Selector, search []expValue) MetaMatchFunc { var positives, negatives []expValue posSet := make(map[string]bool) negSet := make(map[string]bool) for _, val := range search { for _, word := range strfun.NormalizeWords(val.value) { if val.negate { if _, ok := negSet[word]; !ok { negSet[word] = true negatives = append(negatives, expValue{ value: word, op: val.op, negate: true, }) } } else { if _, ok := posSet[word]; !ok { posSet[word] = true positives = append(positives, expValue{ value: word, op: val.op, negate: false, }) } } } } return compileSearch(selector, positives, negatives) } func compilePlainSearch(selector Selector, search []expValue) MetaMatchFunc { var positives, negatives []expValue for _, val := range search { if val.negate { negatives = append(negatives, expValue{ value: strings.ToLower(strings.TrimSpace(val.value)), op: val.op, negate: true, }) } else { positives = append(positives, expValue{ value: strings.ToLower(strings.TrimSpace(val.value)), op: val.op, negate: false, }) } } return compileSearch(selector, positives, negatives) } func compileSearch(selector Selector, poss, negs []expValue) MetaMatchFunc { if len(poss) == 0 { if len(negs) == 0 { return nil } return makeNegOnlySearch(selector, negs) } if len(negs) == 0 { return makePosOnlySearch(selector, poss) } return makePosNegSearch(selector, poss, negs) } func makePosOnlySearch(selector Selector, poss []expValue) MetaMatchFunc { retrievePos := compileRetrieveZids(selector, poss) var ids id.Set return func(m *meta.Meta) bool { if ids == nil { ids = retrievePos() } _, ok := ids[m.Zid] return ok } } func makeNegOnlySearch(selector Selector, negs []expValue) MetaMatchFunc { retrieveNeg := compileRetrieveZids(selector, negs) var ids id.Set return func(m *meta.Meta) bool { if ids == nil { ids = retrieveNeg() } _, ok := ids[m.Zid] return !ok } } func makePosNegSearch(selector Selector, poss, negs []expValue) MetaMatchFunc { retrievePos := compileRetrieveZids(selector, poss) retrieveNeg := compileRetrieveZids(selector, negs) var ids id.Set return func(m *meta.Meta) bool { if ids == nil { ids = retrievePos() ids.Remove(retrieveNeg()) } _, okPos := ids[m.Zid] return okPos } } func compileRetrieveZids(selector Selector, values []expValue) func() id.Set { selFuncs := make([]selectorFunc, 0, len(values)) stringVals := make([]string, 0, len(values)) for _, val := range values { selFuncs = append(selFuncs, compileSelectOp(selector, val.op)) stringVals = append(stringVals, val.value) } if len(selFuncs) == 0 { return func() id.Set { return id.NewSet() } } if len(selFuncs) == 1 { return func() id.Set { return selFuncs[0](stringVals[0]) } } return func() id.Set { result := selFuncs[0](stringVals[0]) for i, f := range selFuncs[1:] { result = result.Intersect(f(stringVals[i+1])) } return result } } type selectorFunc func(string) id.Set func compileSelectOp(selector Selector, op compareOp) selectorFunc { switch op { case cmpDefault, cmpContains: return selector.SelectContains case cmpEqual: return selector.SelectEqual case cmpPrefix: return selector.SelectPrefix case cmpSuffix: return selector.SelectSuffix default: panic(fmt.Sprintf("Unexpected value of comparison operation: %v", op)) } } |
Added search/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 | //----------------------------------------------------------------------------- // Copyright (c) 2020-2021 Detlef Stern // // This file is part of zettelstore. // // Zettelstore is licensed under the latest version of the EUPL (European Union // Public License). Please see file LICENSE.txt for your rights and obligations // under this license. //----------------------------------------------------------------------------- // Package search provides a zettel search. package search import ( "strconv" "zettelstore.de/z/domain/meta" ) type sortFunc func(i, j int) bool func createSortFunc(key string, descending bool, ml []*meta.Meta) sortFunc { keyType := meta.Type(key) if key == meta.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.TypeBool { return createSortBoolFunc(ml, key, descending) } if keyType == meta.TypeNumber { return createSortNumberFunc(ml, key, descending) } return createSortStringFunc(ml, key, descending) } func createSortBoolFunc(ml []*meta.Meta, key string, descending bool) sortFunc { if descending { return func(i, j int) bool { left := ml[i].GetBool(key) if left == ml[j].GetBool(key) { return i > j } return left } } return func(i, j int) bool { right := ml[j].GetBool(key) if ml[i].GetBool(key) == right { return i < j } return right } } 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) (int, bool) { if s, ok := m.Get(key); ok { if i, err := strconv.Atoi(s); err == nil { return i, true } } return 0, false } |
Changes to strfun/escape.go.
1 | //----------------------------------------------------------------------------- | | | < < < > | | < | | | | | < > | < > | > | | | > > | > > | | | | > > > > > > > > > > | > > > > > > > > | | > > > > | | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 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 Detlef Stern // // This file is part of zettelstore. // // Zettelstore is licensed under 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 "io" var ( htmlQuot = []byte(""") // shorter than "&39;", but often requested in standards htmlAmp = []byte("&") htmlLt = []byte("<") htmlGt = []byte(">") htmlNull = []byte("\uFFFD") htmlVisSpace = []byte("\u2423") ) // HTMLEscape writes to w the escaped HTML equivalent of the given string. // If visibleSpace is true, each space is written as U-2423. func HTMLEscape(w io.Writer, s string, visibleSpace bool) { last := 0 var html []byte lenS := len(s) for i := 0; i < lenS; i++ { switch s[i] { case '\000': html = htmlNull case ' ': if visibleSpace { html = htmlVisSpace } else { continue } case '"': html = htmlQuot case '&': html = htmlAmp case '<': html = htmlLt case '>': html = htmlGt default: continue } io.WriteString(w, s[last:i]) w.Write(html) last = i + 1 } io.WriteString(w, s[last:]) } // HTMLAttrEscape writes to w the escaped HTML equivalent of the given string to be used // in attributes. func HTMLAttrEscape(w io.Writer, s string) { last := 0 var html []byte lenS := len(s) for i := 0; i < lenS; i++ { switch s[i] { case '\000': html = htmlNull case '"': html = htmlQuot case '&': html = htmlAmp default: continue } io.WriteString(w, s[last:i]) w.Write(html) last = i + 1 } io.WriteString(w, s[last:]) } |
Deleted strfun/set.go.
|
| < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < |
Changes to strfun/slugify.go.
1 | //----------------------------------------------------------------------------- | | | < < < > | > | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 | //----------------------------------------------------------------------------- // Copyright (c) 2020-2021 Detlef Stern // // This file is part of zettelstore. // // Zettelstore is licensed under the latest version of the EUPL (European Union // Public License). Please see file LICENSE.txt for your rights and obligations // under this license. //----------------------------------------------------------------------------- // Package strfun provides some string functions. package strfun import ( "strings" "unicode" "golang.org/x/text/unicode/norm" ) // NormalizeWords produces a word list that is normalized for better searching. func NormalizeWords(s string) []string { result := make([]string, 0, 1) word := make([]rune, 0, len(s)) for _, r := range norm.NFKD.String(s) { if unicode.Is(unicode.Diacritic, r) { continue } if unicode.In(r, unicode.Letter, unicode.Number) { word = append(word, unicode.ToLower(r)) |
︙ | ︙ |
Changes to strfun/slugify_test.go.
1 | //----------------------------------------------------------------------------- | | | < < < > < | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 | //----------------------------------------------------------------------------- // Copyright (c) 2020-2021 Detlef Stern // // This file is part of zettelstore. // // Zettelstore is licensed under the latest version of the EUPL (European Union // Public License). Please see file LICENSE.txt for your rights and obligations // under this license. //----------------------------------------------------------------------------- // Package strfun provides some string functions. package strfun_test import ( "testing" "zettelstore.de/z/strfun" ) func TestSlugify(t *testing.T) { tests := []struct{ in, exp string }{ {"simple test", "simple-test"}, {"I'm a go developer", "i-m-a-go-developer"}, {"-!->simple test<-!-", "simple-test"}, {"äöüÄÖÜß", "aouaouß"}, {"\"aèf", "aef"}, {"a#b", "a-b"}, |
︙ | ︙ | |||
46 47 48 49 50 51 52 | return false } } return true } func TestNormalizeWord(t *testing.T) { | < | 43 44 45 46 47 48 49 50 51 52 53 54 55 56 | return false } } return true } func TestNormalizeWord(t *testing.T) { tests := []struct { in string exp []string }{ {"", []string{}}, {" ", []string{}}, {"ˋ", []string{}}, // No single diacritic char, such as U+02CB |
︙ | ︙ |
Changes to strfun/strfun.go.
1 | //----------------------------------------------------------------------------- | | | < < < > > > > > > | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 | //----------------------------------------------------------------------------- // Copyright (c) 2021 Detlef Stern // // This file is part of zettelstore. // // Zettelstore is licensed under the latest version of the EUPL (European Union // Public License). Please see file LICENSE.txt for your rights and obligations // under this license. //----------------------------------------------------------------------------- // Package strfun provides some string functions. package strfun import ( "strings" "unicode" "unicode/utf8" ) // TrimSpaceRight returns a slice of the string s, with all trailing white space removed, // as defined by Unicode. func TrimSpaceRight(s string) string { return strings.TrimRightFunc(s, unicode.IsSpace) } // Length returns the number of runes in the given string. func Length(s string) int { return utf8.RuneCountInString(s) } // JustifyLeft ensures that the string has a defined length. |
︙ | ︙ | |||
39 40 41 42 43 44 45 | runes[maxLen-1] = '\u2025' } var sb strings.Builder for _, r := range runes { sb.WriteRune(r) } | | < < < < < < < < | 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 | 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 | //----------------------------------------------------------------------------- | | | < < < > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > < < | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 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) 2021 Detlef Stern // // This file is part of zettelstore. // // Zettelstore is licensed under the latest version of the EUPL (European Union // Public License). Please see file LICENSE.txt for your rights and obligations // under this license. //----------------------------------------------------------------------------- // Package strfun provides some string functions. package strfun_test import ( "testing" "zettelstore.de/z/strfun" ) func TestTrimSpaceRight(t *testing.T) { const space = "\t\v\r\f\n\u0085\u00a0\u2000\u3000" testcases := []struct { in string exp string }{ {"", ""}, {"abc", "abc"}, {" ", ""}, {space, ""}, {space + "abc" + space, space + "abc"}, {" \t\r\n \t\t\r\r\n\n ", ""}, {" \t\r\n x\t\t\r\r\n\n ", " \t\r\n x"}, {" \u2000\t\r\n x\t\t\r\r\ny\n \u3000", " \u2000\t\r\n x\t\t\r\r\ny"}, {"1 \t\r\n2", "1 \t\r\n2"}, {" x\x80", " x\x80"}, {" x\xc0", " x\xc0"}, {"x \xc0\xc0 ", "x \xc0\xc0"}, {"x \xc0", "x \xc0"}, {"x \xc0 ", "x \xc0"}, {"x \xc0\xc0 ", "x \xc0\xc0"}, {"x ☺\xc0\xc0 ", "x ☺\xc0\xc0"}, {"x ☺ ", "x ☺"}, } for i, tc := range testcases { got := strfun.TrimSpaceRight(tc.in) if got != tc.exp { t.Errorf("%d/%q: expected %q, got %q", i, tc.in, tc.exp, got) } } } func TestLength(t *testing.T) { testcases := []struct { in string exp int }{ {"", 0}, {"äbc", 3}, } for i, tc := range testcases { got := strfun.Length(tc.in) if got != tc.exp { t.Errorf("%d/%q: expected %v, got %v", i, tc.in, tc.exp, got) } } } func TestJustifyLeft(t *testing.T) { testcases := []struct { in string ml int exp string }{ {"", 0, ""}, {"äbc", 0, ""}, |
︙ | ︙ | |||
55 56 57 58 59 60 61 | if got != tc.exp { t.Errorf("%d/%q/%d: expected %q, got %q", i, tc.in, tc.ml, tc.exp, got) } } } func TestSplitLines(t *testing.T) { | < | 83 84 85 86 87 88 89 90 91 92 93 94 95 96 | if got != tc.exp { t.Errorf("%d/%q/%d: expected %q, got %q", i, tc.in, tc.ml, tc.exp, got) } } } func TestSplitLines(t *testing.T) { testcases := []struct { in string exp []string }{ {"", nil}, {"\n", nil}, {"a", []string{"a"}}, |
︙ | ︙ |
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 713 714 715 716 717 718 719 720 | //----------------------------------------------------------------------------- // Copyright (c) 2020-2021 Detlef Stern // // This file is part of zettelstore. // // Zettelstore is licensed under the latest version of the EUPL (European Union // Public License). Please see file LICENSE.txt for your rights and obligations // under this license. // // 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" "zettelstore.de/z/strfun" ) // 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 (e *varNode) node() {} func (e *varNode) Type() TagType { return Variable } func (e *varNode) Name() string { return e.name } func (e *varNode) Tags() []Tag { panic("mustache: Tags on Variable type") } type sectionNode struct { name string inverted bool startline int nodes []node } func (e *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 (e *partialNode) node() {} func (e *partialNode) Type() TagType { return Partial } func (e *partialNode) Name() string { return e.name } func (e *partialNode) Tags() []Tag { return nil } type textNode struct { text []byte } func (e *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]bool{ '#': true, '^': true, '/': true, '<': true, '>': true, '=': true, '!': true, } 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, error) { return &partialNode{ name: name, indent: indent, prov: tmpl.partial, }, nil } 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, err := tmpl.parsePartial(name, textResult.padding) if err != nil { return err } 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, err := tmpl.parsePartial(name, textResult.padding) if err != nil { return err } 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 { s := fmt.Sprint(val.Interface()) strfun.HTMLEscape(w, s, false) } } 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 (ep *emptyProvider) Get(name 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 | //----------------------------------------------------------------------------- // Copyright (c) 2020-2021 Detlef Stern // // This file is part of zettelstore. // // Zettelstore is licensed under the latest version of the EUPL (European Union // Public License). Please see file LICENSE.txt for your rights and obligations // under this license. // // 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 ( "bytes" "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 buf bytes.Buffer err := tmpl.Render(&buf, data) return buf.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) { 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) { 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) { 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) { 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) { 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 | //----------------------------------------------------------------------------- // Copyright (c) 2020-2021 Detlef Stern // // This file is part of zettelstore. // // Zettelstore is licensed under the latest version of the EUPL (European Union // Public License). Please see file LICENSE.txt for your rights and obligations // under this license. // // 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) { 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, err := os.ReadFile(path) if err != nil { t.Fatal(err) } var suite specTestSuite err = json.Unmarshal(b, &suite) if err != nil { t.Fatal(err) } 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/content/blockcomment/20200215204700.zettel.
> > > > > > > > | 1 2 3 4 5 6 7 8 | title: Simple Test %%% No render %%% %%%{-} Render %%% |
Added testdata/content/cite/20200215204700.zettel.
> > > | 1 2 3 | title: Simple Test [@Stern18]{-} |
Added testdata/content/comment/20200215204700.zettel.
> > > > | 1 2 3 4 | title: Simple Test % No comment %% Comment |
Added testdata/content/descrlist/20200226122100.zettel.
> > > > > > > | 1 2 3 4 5 6 7 | title: Simple Test ; Zettel : Paper : Note ; Zettelkasten : Slip box |
Added testdata/content/edit/20200215204700.zettel.
> > > > > | 1 2 3 4 5 | title: Simple Test ~~delete~~{-} __insert__{-} ~~kill~~{-}__create__{-} |
Added testdata/content/footnote/20200215204700.zettel.
> > > | 1 2 3 | title: Simple Test Text[^foot]{=sidebar} |
Added testdata/content/format/20200215204700.zettel.
> > > > > > > > > > > > > > > > > > > | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 | title: Simple Test role: zettel //italic// //emph//{-} **bold** **strong**{-} __unterline__ ~~strike~~ ''monospace'' ^^superscript^^ ,,subscript,, ""Quotes"" <<Quotation<< ;;small;; ::span:: ``code`` ++input++ ==output== |
Added testdata/content/format/20201107164400.zettel.
> > > > | 1 2 3 4 | title: Nested Lang role: zettel ::""abc""::{lang=fr} |
Added testdata/content/heading/20200215204700.zettel.
> > > | 1 2 3 | title: Simple Test === First |
Added testdata/content/hrule/20200215204700.zettel.
> > > | 1 2 3 | title: Simple Test --- |
Added testdata/content/image/20200215204700.zettel.
> > > | 1 2 3 | title: Simple Test {{abc}} |
Added testdata/content/link/20200215204700.zettel.
> > > > > > > > > > > | 1 2 3 4 5 6 7 8 9 10 11 | title: Simple Test [[Home|https://zettelstore.de/z]] [[https://zettelstore.de]] [[Config|00000000000100]] [[00000000000100]] [[Frag|#frag]] [[#frag]] [[H|/hosted]] [[B|//based]] [[R|../rel]] |
Added testdata/content/list/20200215204700.zettel.
> > > > > | 1 2 3 4 5 | title: Simple Test * Item 1 * Item 2 * Item 3 |
Added testdata/content/list/20200217194800.zettel.
> > > > > > > > | 1 2 3 4 5 6 7 8 | title: Second list * Item1.1 * Item1.2 * Item1.3 * Item2.1 * Item2.2 |
Added testdata/content/list/20200516105700.zettel.
> > > > > > > | 1 2 3 4 5 6 7 | title: Schachtelliste * T1 ** T2 * T3 ** T4 * T5 |
Added testdata/content/literal/20200215204700.zettel.
> > > > > | 1 2 3 4 5 | title: Simple Test ++input++ ``program`` ==output== |
Added testdata/content/mark/20200215204700.zettel.
> > > | 1 2 3 | title: Simple Test [!mark] |
Added testdata/content/paragraph/20200215185900.zettel.
> > > > | 1 2 3 4 | title: Simple Zettel tags: #test #simple This is a zettel for testing. |
Added testdata/content/paragraph/20200217151800.zettel.
> > > > > > > > | 1 2 3 4 5 6 7 8 | title: Continuation after Paragraph tags: #test #paragraph Text Text *abc Text Text * abc |
Added testdata/content/png/20200512180900.png.
cannot compute difference between binary files
Added testdata/content/quoteblock/20200215204700.zettel.
> > > > > | 1 2 3 4 5 | title: Simple Test <<< To be or not to be. <<< Romeo |
Added testdata/content/spanblock/20200215204700.zettel.
> > > > > > > | 1 2 3 4 5 6 7 | title: Simple Test ::: A simple span and much more ::: |
Added testdata/content/table/20200215204700.zettel.
> > > | 1 2 3 | title: Simple Test |c1|c2|c3| |
Added testdata/content/table/20200618140700.zettel.
> > > > > > | 1 2 3 4 5 6 | title: Testtable |h1>|=h2|h3:| |%--+---+---+ |<c1|c2|:c3| |f1|f2|=f3 |
Added testdata/content/verbatim/20200215204700.zettel.
> > > > > > > | 1 2 3 4 5 6 7 | title: Simple Test ``` if __name__ == "main": print("Hello, World") exit(0) ``` |
Added testdata/content/verseblock/20200215204700.zettel.
> > > > > > > > > > > | 1 2 3 4 5 6 7 8 9 10 11 | title: Simple Test """ A line another line Back Paragraph Spacy Para """ Author |
Changes to testdata/markdown/README.md.
|
| | | 1 2 | File `spec.son` is the CommonMark specification test file. You can find all versions here: <https://spec.commonmark.org/>. |
Changes to testdata/markdown/spec.json.
1 2 3 4 5 | [ { "markdown": "\tfoo\tbaz\t\tbim\n", "html": "<pre><code>foo\tbaz\t\tbim\n</code></pre>\n", "example": 1, | | | | | | | | | | | | | | | | | | | | | | | < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | < < < < < < < < | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | > > > > > > > > | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | < < < < < < < < | | | | | | < < < < < < < < | | | | | | | | | | | | | | | | | | | | | | | | | | | < < < < < < < < | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 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 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 1113 1114 1115 1116 1117 1118 1119 1120 1121 1122 1123 1124 1125 1126 1127 1128 1129 1130 1131 1132 1133 1134 1135 1136 1137 1138 1139 1140 1141 1142 1143 1144 1145 1146 1147 1148 1149 1150 1151 1152 1153 1154 1155 1156 1157 1158 1159 1160 1161 1162 1163 1164 1165 1166 1167 1168 1169 1170 1171 1172 1173 1174 1175 1176 1177 1178 1179 1180 1181 1182 1183 1184 1185 1186 1187 1188 1189 1190 1191 1192 1193 1194 1195 1196 1197 1198 1199 1200 1201 1202 1203 1204 1205 1206 1207 1208 1209 1210 1211 1212 1213 1214 1215 1216 1217 1218 1219 1220 1221 1222 1223 1224 1225 1226 1227 1228 1229 1230 1231 1232 1233 1234 1235 1236 1237 1238 1239 1240 1241 1242 1243 1244 1245 1246 1247 1248 1249 1250 1251 1252 1253 1254 1255 1256 1257 1258 1259 1260 1261 1262 1263 1264 1265 1266 1267 1268 1269 1270 1271 1272 1273 1274 1275 1276 1277 1278 1279 1280 1281 1282 1283 1284 1285 1286 1287 1288 1289 1290 1291 1292 1293 1294 1295 1296 1297 1298 1299 1300 1301 1302 1303 1304 1305 1306 1307 1308 1309 1310 1311 1312 1313 1314 1315 1316 1317 1318 1319 1320 1321 1322 1323 1324 1325 1326 1327 1328 1329 1330 1331 1332 1333 1334 1335 1336 1337 1338 1339 1340 1341 1342 1343 1344 1345 1346 1347 1348 1349 1350 1351 1352 1353 1354 1355 1356 1357 1358 1359 1360 1361 1362 1363 1364 1365 1366 1367 1368 1369 1370 1371 1372 1373 1374 1375 1376 1377 1378 1379 1380 1381 1382 1383 1384 1385 1386 1387 1388 1389 1390 1391 1392 1393 1394 1395 1396 1397 1398 1399 1400 1401 1402 1403 1404 1405 1406 1407 1408 1409 1410 1411 1412 1413 1414 1415 1416 1417 1418 1419 1420 1421 1422 1423 1424 1425 1426 1427 1428 1429 1430 1431 1432 1433 1434 1435 1436 1437 1438 1439 1440 1441 1442 1443 1444 1445 1446 1447 1448 1449 1450 1451 1452 1453 1454 1455 1456 1457 1458 1459 1460 1461 1462 1463 1464 1465 1466 1467 1468 1469 1470 1471 1472 1473 1474 1475 1476 1477 1478 1479 1480 1481 1482 1483 1484 1485 1486 1487 1488 1489 1490 1491 1492 1493 1494 1495 1496 1497 1498 1499 1500 1501 1502 1503 1504 1505 1506 1507 1508 1509 1510 1511 1512 1513 1514 1515 1516 1517 1518 1519 1520 1521 1522 1523 1524 1525 1526 1527 1528 1529 1530 1531 1532 1533 1534 1535 1536 1537 1538 1539 1540 1541 1542 1543 1544 1545 1546 1547 1548 1549 1550 1551 1552 1553 1554 1555 1556 1557 1558 1559 1560 1561 1562 1563 1564 1565 1566 1567 1568 1569 1570 1571 1572 1573 1574 1575 1576 1577 1578 1579 1580 1581 1582 1583 1584 1585 1586 1587 1588 1589 1590 1591 1592 1593 1594 1595 1596 1597 1598 1599 1600 1601 1602 1603 1604 1605 1606 1607 1608 1609 1610 1611 1612 1613 1614 1615 1616 1617 1618 1619 1620 1621 1622 1623 1624 1625 1626 1627 1628 1629 1630 1631 1632 1633 1634 1635 1636 1637 1638 1639 1640 1641 1642 1643 1644 1645 1646 1647 1648 1649 1650 1651 1652 1653 1654 1655 1656 1657 1658 1659 1660 1661 1662 1663 1664 1665 1666 1667 1668 1669 1670 1671 1672 1673 1674 1675 1676 1677 1678 1679 1680 1681 1682 1683 1684 1685 1686 1687 1688 1689 1690 1691 1692 1693 1694 1695 1696 1697 1698 1699 1700 1701 1702 1703 1704 1705 1706 1707 1708 1709 1710 1711 1712 1713 1714 1715 1716 1717 1718 1719 1720 1721 1722 1723 1724 1725 1726 1727 1728 1729 1730 1731 1732 1733 1734 1735 1736 1737 1738 1739 1740 1741 1742 1743 1744 1745 1746 1747 1748 1749 1750 1751 1752 1753 1754 1755 1756 1757 1758 1759 1760 1761 1762 1763 1764 1765 1766 1767 1768 1769 1770 1771 1772 1773 1774 1775 1776 1777 1778 1779 1780 1781 1782 1783 1784 1785 1786 1787 1788 1789 1790 1791 1792 1793 1794 1795 1796 1797 1798 1799 1800 1801 1802 1803 1804 1805 1806 1807 1808 1809 1810 1811 1812 1813 1814 1815 1816 1817 1818 1819 1820 1821 1822 1823 1824 1825 1826 1827 1828 1829 1830 1831 1832 1833 1834 1835 1836 1837 1838 1839 1840 1841 1842 1843 1844 1845 1846 1847 1848 1849 1850 1851 1852 1853 1854 1855 1856 1857 1858 1859 1860 1861 1862 1863 1864 1865 1866 1867 1868 1869 1870 1871 1872 1873 1874 1875 1876 1877 1878 1879 1880 1881 1882 1883 1884 1885 1886 1887 1888 1889 1890 1891 1892 1893 1894 1895 1896 1897 1898 1899 1900 1901 1902 1903 1904 1905 1906 1907 1908 1909 1910 1911 1912 1913 1914 1915 1916 1917 1918 1919 1920 1921 1922 1923 1924 1925 1926 1927 1928 1929 1930 1931 1932 1933 1934 1935 1936 1937 1938 1939 1940 1941 1942 1943 1944 1945 1946 1947 1948 1949 1950 1951 1952 1953 1954 1955 1956 1957 1958 1959 1960 1961 1962 1963 1964 1965 1966 1967 1968 1969 1970 1971 1972 1973 1974 1975 1976 1977 1978 1979 1980 1981 1982 1983 1984 1985 1986 1987 1988 1989 1990 1991 1992 1993 1994 1995 1996 1997 1998 1999 2000 2001 2002 2003 2004 2005 2006 2007 2008 2009 2010 2011 2012 2013 2014 2015 2016 2017 2018 2019 2020 2021 2022 2023 2024 2025 2026 2027 2028 2029 2030 2031 2032 2033 2034 2035 2036 2037 2038 2039 2040 2041 2042 2043 2044 2045 2046 2047 2048 2049 2050 2051 2052 2053 2054 2055 2056 2057 2058 2059 2060 2061 2062 2063 2064 2065 2066 2067 2068 2069 2070 2071 2072 2073 2074 2075 2076 2077 2078 2079 2080 2081 2082 2083 2084 2085 2086 2087 2088 2089 2090 2091 2092 2093 2094 2095 2096 2097 2098 2099 2100 2101 2102 2103 2104 2105 2106 2107 2108 2109 2110 2111 2112 2113 2114 2115 2116 2117 2118 2119 2120 2121 2122 2123 2124 2125 2126 2127 2128 2129 2130 2131 2132 2133 2134 2135 2136 2137 2138 2139 2140 2141 2142 2143 2144 2145 2146 2147 2148 2149 2150 2151 2152 2153 2154 2155 2156 2157 2158 2159 2160 2161 2162 2163 2164 2165 2166 2167 2168 2169 2170 2171 2172 2173 2174 2175 2176 2177 2178 2179 2180 2181 2182 2183 2184 2185 2186 2187 2188 2189 2190 2191 2192 2193 2194 2195 2196 2197 2198 2199 2200 2201 2202 2203 2204 2205 2206 2207 2208 2209 2210 2211 2212 2213 2214 2215 2216 2217 2218 2219 2220 2221 2222 2223 2224 2225 2226 2227 2228 2229 2230 2231 2232 2233 2234 2235 2236 2237 2238 2239 2240 2241 2242 2243 2244 2245 2246 2247 2248 2249 2250 2251 2252 2253 2254 2255 2256 2257 2258 2259 2260 2261 2262 2263 2264 2265 2266 2267 2268 2269 2270 2271 2272 2273 2274 2275 2276 2277 2278 2279 2280 2281 2282 2283 2284 2285 2286 2287 2288 2289 2290 2291 2292 2293 2294 2295 2296 2297 2298 2299 2300 2301 2302 2303 2304 2305 2306 2307 2308 2309 2310 2311 2312 2313 2314 2315 2316 2317 2318 2319 2320 2321 2322 2323 2324 2325 2326 2327 2328 2329 2330 2331 2332 2333 2334 2335 2336 2337 2338 2339 2340 2341 2342 2343 2344 2345 2346 2347 2348 2349 2350 2351 2352 2353 2354 2355 2356 2357 2358 2359 2360 2361 2362 2363 2364 2365 2366 2367 2368 2369 2370 2371 2372 2373 2374 2375 2376 2377 2378 2379 2380 2381 2382 2383 2384 2385 2386 2387 2388 2389 2390 2391 2392 2393 2394 2395 2396 2397 2398 2399 2400 2401 2402 2403 2404 2405 2406 2407 2408 2409 2410 2411 2412 2413 2414 2415 2416 2417 2418 2419 2420 2421 2422 2423 2424 2425 2426 2427 2428 2429 2430 2431 2432 2433 2434 2435 2436 2437 2438 2439 2440 2441 2442 2443 2444 2445 2446 2447 2448 2449 2450 2451 2452 2453 2454 2455 2456 2457 2458 2459 2460 2461 2462 2463 2464 2465 2466 2467 2468 2469 2470 2471 2472 2473 2474 2475 2476 2477 2478 2479 2480 2481 2482 2483 2484 2485 2486 2487 2488 2489 2490 2491 2492 2493 2494 2495 2496 2497 2498 2499 2500 2501 2502 2503 2504 2505 2506 2507 2508 2509 2510 2511 2512 2513 2514 2515 2516 2517 2518 2519 2520 2521 2522 2523 2524 2525 2526 2527 2528 2529 2530 2531 2532 2533 2534 2535 2536 2537 2538 2539 2540 2541 2542 2543 2544 2545 2546 2547 2548 2549 2550 2551 2552 2553 2554 2555 2556 2557 2558 2559 2560 2561 2562 2563 2564 2565 2566 2567 2568 2569 2570 2571 2572 2573 2574 2575 2576 2577 2578 2579 2580 2581 2582 2583 2584 2585 2586 2587 2588 2589 2590 2591 2592 2593 2594 2595 2596 2597 2598 2599 2600 2601 2602 2603 2604 2605 2606 2607 2608 2609 2610 2611 2612 2613 2614 2615 2616 2617 2618 2619 2620 2621 2622 2623 2624 2625 2626 2627 2628 2629 2630 2631 2632 2633 2634 2635 2636 2637 2638 2639 2640 2641 2642 2643 2644 2645 2646 2647 2648 2649 2650 2651 2652 2653 2654 2655 2656 2657 2658 2659 2660 2661 2662 2663 2664 2665 2666 2667 2668 2669 2670 2671 2672 2673 2674 2675 2676 2677 2678 2679 2680 2681 2682 2683 2684 2685 2686 2687 2688 2689 2690 2691 2692 2693 2694 2695 2696 2697 2698 2699 2700 2701 2702 2703 2704 2705 2706 2707 2708 2709 2710 2711 2712 2713 2714 2715 2716 2717 2718 2719 2720 2721 2722 2723 2724 2725 2726 2727 2728 2729 2730 2731 2732 2733 2734 2735 2736 2737 2738 2739 2740 2741 2742 2743 2744 2745 2746 2747 2748 2749 2750 2751 2752 2753 2754 2755 2756 2757 2758 2759 2760 2761 2762 2763 2764 2765 2766 2767 2768 2769 2770 2771 2772 2773 2774 2775 2776 2777 2778 2779 2780 2781 2782 2783 2784 2785 2786 2787 2788 2789 2790 2791 2792 2793 2794 2795 2796 2797 2798 2799 2800 2801 2802 2803 2804 2805 2806 2807 2808 2809 2810 2811 2812 2813 2814 2815 2816 2817 2818 2819 2820 2821 2822 2823 2824 2825 2826 2827 2828 2829 2830 2831 2832 2833 2834 2835 2836 2837 2838 2839 2840 2841 2842 2843 2844 2845 2846 2847 2848 2849 2850 2851 2852 2853 2854 2855 2856 2857 2858 2859 2860 2861 2862 2863 2864 2865 2866 2867 2868 2869 2870 2871 2872 2873 2874 2875 2876 2877 2878 2879 2880 2881 2882 2883 2884 2885 2886 2887 2888 2889 2890 2891 2892 2893 2894 2895 2896 2897 2898 2899 2900 2901 2902 2903 2904 2905 2906 2907 2908 2909 2910 2911 2912 2913 2914 2915 2916 2917 2918 2919 2920 2921 2922 2923 2924 2925 2926 2927 2928 2929 2930 2931 2932 2933 2934 2935 2936 2937 2938 2939 2940 2941 2942 2943 2944 2945 2946 2947 2948 2949 2950 2951 2952 2953 2954 2955 2956 2957 2958 2959 2960 2961 2962 2963 2964 2965 2966 2967 2968 2969 2970 2971 2972 2973 2974 2975 2976 2977 2978 2979 2980 2981 2982 2983 2984 2985 2986 2987 2988 2989 2990 2991 2992 2993 2994 2995 2996 2997 2998 2999 3000 3001 3002 3003 3004 3005 3006 3007 3008 3009 3010 3011 3012 3013 3014 3015 3016 3017 3018 3019 3020 3021 3022 3023 3024 3025 3026 3027 3028 3029 3030 3031 3032 3033 3034 3035 3036 3037 3038 3039 3040 3041 3042 3043 3044 3045 3046 3047 3048 3049 3050 3051 3052 3053 3054 3055 3056 3057 3058 3059 3060 3061 3062 3063 3064 3065 3066 3067 3068 3069 3070 3071 3072 3073 3074 3075 3076 3077 3078 3079 3080 3081 3082 3083 3084 3085 3086 3087 3088 3089 3090 3091 3092 3093 3094 3095 3096 3097 3098 3099 3100 3101 3102 3103 3104 3105 3106 3107 3108 3109 3110 3111 3112 3113 3114 3115 3116 3117 3118 3119 3120 3121 3122 3123 3124 3125 3126 3127 3128 3129 3130 3131 3132 3133 3134 3135 3136 3137 3138 3139 3140 3141 3142 3143 3144 3145 3146 3147 3148 3149 3150 3151 3152 3153 3154 3155 3156 3157 3158 3159 3160 3161 3162 3163 3164 3165 3166 3167 3168 3169 3170 3171 3172 3173 3174 3175 3176 3177 3178 3179 3180 3181 3182 3183 3184 3185 3186 3187 3188 3189 3190 3191 3192 3193 3194 3195 3196 3197 3198 3199 3200 3201 3202 3203 3204 3205 3206 3207 3208 3209 3210 3211 3212 3213 3214 3215 3216 3217 3218 3219 3220 3221 3222 3223 3224 3225 3226 3227 3228 3229 3230 3231 3232 3233 3234 3235 3236 3237 3238 3239 3240 3241 3242 3243 3244 3245 3246 3247 3248 3249 3250 3251 3252 3253 3254 3255 3256 3257 3258 3259 3260 3261 3262 3263 3264 3265 3266 3267 3268 3269 3270 3271 3272 3273 3274 3275 3276 3277 3278 3279 3280 3281 3282 3283 3284 3285 3286 3287 3288 3289 3290 3291 3292 3293 3294 3295 3296 3297 3298 3299 3300 3301 3302 3303 3304 3305 3306 3307 3308 3309 3310 3311 3312 3313 3314 3315 3316 3317 3318 3319 3320 3321 3322 3323 3324 3325 3326 3327 3328 3329 3330 3331 3332 3333 3334 3335 3336 3337 3338 3339 3340 3341 3342 3343 3344 3345 3346 3347 3348 3349 3350 3351 3352 3353 3354 3355 3356 3357 3358 3359 3360 3361 3362 3363 3364 3365 3366 3367 3368 3369 3370 3371 3372 3373 3374 3375 3376 3377 3378 3379 3380 3381 3382 3383 3384 3385 3386 3387 3388 3389 3390 3391 3392 3393 3394 3395 3396 3397 3398 3399 3400 3401 3402 3403 3404 3405 3406 3407 3408 3409 3410 3411 3412 3413 3414 3415 3416 3417 3418 3419 3420 3421 3422 3423 3424 3425 3426 3427 3428 3429 3430 3431 3432 3433 3434 3435 3436 3437 3438 3439 3440 3441 3442 3443 3444 3445 3446 3447 3448 3449 3450 3451 3452 3453 3454 3455 3456 3457 3458 3459 3460 3461 3462 3463 3464 3465 3466 3467 3468 3469 3470 3471 3472 3473 3474 3475 3476 3477 3478 3479 3480 3481 3482 3483 3484 3485 3486 3487 3488 3489 3490 3491 3492 3493 3494 3495 3496 3497 3498 3499 3500 3501 3502 3503 3504 3505 3506 3507 3508 3509 3510 3511 3512 3513 3514 3515 3516 3517 3518 3519 3520 3521 3522 3523 3524 3525 3526 3527 3528 3529 3530 3531 3532 3533 3534 3535 3536 3537 3538 3539 3540 3541 3542 3543 3544 3545 3546 3547 3548 3549 3550 3551 3552 3553 3554 3555 3556 3557 3558 3559 3560 3561 3562 3563 3564 3565 3566 3567 3568 3569 3570 3571 3572 3573 3574 3575 3576 3577 3578 3579 3580 3581 3582 3583 3584 3585 3586 3587 3588 3589 3590 3591 3592 3593 3594 3595 3596 3597 3598 3599 3600 3601 3602 3603 3604 3605 3606 3607 3608 3609 3610 3611 3612 3613 3614 3615 3616 3617 3618 3619 3620 3621 3622 3623 3624 3625 3626 3627 3628 3629 3630 3631 3632 3633 3634 3635 3636 3637 3638 3639 3640 3641 3642 3643 3644 3645 3646 3647 3648 3649 3650 3651 3652 3653 3654 3655 3656 3657 3658 3659 3660 3661 3662 3663 3664 3665 3666 3667 3668 3669 3670 3671 3672 3673 3674 3675 3676 3677 3678 3679 3680 3681 3682 3683 3684 3685 3686 3687 3688 3689 3690 3691 3692 3693 3694 3695 3696 3697 3698 3699 3700 3701 3702 3703 3704 3705 3706 3707 3708 3709 3710 3711 3712 3713 3714 3715 3716 3717 3718 3719 3720 3721 3722 3723 3724 3725 3726 3727 3728 3729 3730 3731 3732 3733 3734 3735 3736 3737 3738 3739 3740 3741 3742 3743 3744 3745 3746 3747 3748 3749 3750 3751 3752 3753 3754 3755 3756 3757 3758 3759 3760 3761 3762 3763 3764 3765 3766 3767 3768 3769 3770 3771 3772 3773 3774 3775 3776 3777 3778 3779 3780 3781 3782 3783 3784 3785 3786 3787 3788 3789 3790 3791 3792 3793 3794 3795 3796 3797 3798 3799 3800 3801 3802 3803 3804 3805 3806 3807 3808 3809 3810 3811 3812 3813 3814 3815 3816 3817 3818 3819 3820 3821 3822 3823 3824 3825 3826 3827 3828 3829 3830 3831 3832 3833 3834 3835 3836 3837 3838 3839 3840 3841 3842 3843 3844 3845 3846 3847 3848 3849 3850 3851 3852 3853 3854 3855 3856 3857 3858 3859 3860 3861 3862 3863 3864 3865 3866 3867 3868 3869 3870 3871 3872 3873 3874 3875 3876 3877 3878 3879 3880 3881 3882 3883 3884 3885 3886 3887 3888 3889 3890 3891 3892 3893 3894 3895 3896 3897 3898 3899 3900 3901 3902 3903 3904 3905 3906 3907 3908 3909 3910 3911 3912 3913 3914 3915 3916 3917 3918 3919 3920 3921 3922 3923 3924 3925 3926 3927 3928 3929 3930 3931 3932 3933 3934 3935 3936 3937 3938 3939 3940 3941 3942 3943 3944 3945 3946 3947 3948 3949 3950 3951 3952 3953 3954 3955 3956 3957 3958 3959 3960 3961 3962 3963 3964 3965 3966 3967 3968 3969 3970 3971 3972 3973 3974 3975 3976 3977 3978 3979 3980 3981 3982 3983 3984 3985 3986 3987 3988 3989 3990 3991 3992 3993 3994 3995 3996 3997 3998 3999 4000 4001 4002 4003 4004 4005 4006 4007 4008 4009 4010 4011 4012 4013 4014 4015 4016 4017 4018 4019 4020 4021 4022 4023 4024 4025 4026 4027 4028 4029 4030 4031 4032 4033 4034 4035 4036 4037 4038 4039 4040 4041 4042 4043 4044 4045 4046 4047 4048 4049 4050 4051 4052 4053 4054 4055 4056 4057 4058 4059 4060 4061 4062 4063 4064 4065 4066 4067 4068 4069 4070 4071 4072 4073 4074 4075 4076 4077 4078 4079 4080 4081 4082 4083 4084 4085 4086 4087 4088 4089 4090 4091 4092 4093 4094 4095 4096 4097 4098 4099 4100 4101 4102 4103 4104 4105 4106 4107 4108 4109 4110 4111 4112 4113 4114 4115 4116 4117 4118 4119 4120 4121 4122 4123 4124 4125 4126 4127 4128 4129 4130 4131 4132 4133 4134 4135 4136 4137 4138 4139 4140 4141 4142 4143 4144 4145 4146 4147 4148 4149 4150 4151 4152 4153 4154 4155 4156 4157 4158 4159 4160 4161 4162 4163 4164 4165 4166 4167 4168 4169 4170 4171 4172 4173 4174 4175 4176 4177 4178 4179 4180 4181 4182 4183 4184 4185 4186 4187 4188 4189 4190 4191 4192 4193 4194 4195 4196 4197 4198 4199 4200 4201 4202 4203 4204 4205 4206 4207 4208 4209 4210 4211 4212 4213 4214 4215 4216 4217 4218 4219 4220 4221 4222 4223 4224 4225 4226 4227 4228 4229 4230 4231 4232 4233 4234 4235 4236 4237 4238 4239 4240 4241 4242 4243 4244 4245 4246 4247 4248 4249 4250 4251 4252 4253 4254 4255 4256 4257 4258 4259 4260 4261 4262 4263 4264 4265 4266 4267 4268 4269 4270 4271 4272 4273 4274 4275 4276 4277 4278 4279 4280 4281 4282 4283 4284 4285 4286 4287 4288 4289 4290 4291 4292 4293 4294 4295 4296 4297 4298 4299 4300 4301 4302 4303 4304 4305 4306 4307 4308 4309 4310 4311 4312 4313 4314 4315 4316 4317 4318 4319 4320 4321 4322 4323 4324 4325 4326 4327 4328 4329 4330 4331 4332 4333 4334 4335 4336 4337 4338 4339 4340 4341 4342 4343 4344 4345 4346 4347 4348 4349 4350 4351 4352 4353 4354 4355 4356 4357 4358 4359 4360 4361 4362 4363 4364 4365 4366 4367 4368 4369 4370 4371 4372 4373 4374 4375 4376 4377 4378 4379 4380 4381 4382 4383 4384 4385 4386 4387 4388 4389 4390 4391 4392 4393 4394 4395 4396 4397 4398 4399 4400 4401 4402 4403 4404 4405 4406 4407 4408 4409 4410 4411 4412 4413 4414 4415 4416 4417 4418 4419 4420 4421 4422 4423 4424 4425 4426 4427 4428 4429 4430 4431 4432 4433 4434 4435 4436 4437 4438 4439 4440 4441 4442 4443 4444 4445 4446 4447 4448 4449 4450 4451 4452 4453 4454 4455 4456 4457 4458 4459 4460 4461 4462 4463 4464 4465 4466 4467 4468 4469 4470 4471 4472 4473 4474 4475 4476 4477 4478 4479 4480 4481 4482 4483 4484 4485 4486 4487 4488 4489 4490 4491 4492 4493 4494 4495 4496 4497 4498 4499 4500 4501 4502 4503 4504 4505 4506 4507 4508 4509 4510 4511 4512 4513 4514 4515 4516 4517 4518 4519 4520 4521 4522 4523 4524 4525 4526 4527 4528 4529 4530 4531 4532 4533 4534 4535 4536 4537 4538 4539 4540 4541 4542 4543 4544 4545 4546 4547 4548 4549 4550 4551 4552 4553 4554 4555 4556 4557 4558 4559 4560 4561 4562 4563 4564 4565 4566 4567 4568 4569 4570 4571 4572 4573 4574 4575 4576 4577 4578 4579 4580 4581 4582 4583 4584 4585 4586 4587 4588 4589 4590 4591 4592 4593 4594 4595 4596 4597 4598 4599 4600 4601 4602 4603 4604 4605 4606 4607 4608 4609 4610 4611 4612 4613 4614 4615 4616 4617 4618 4619 4620 4621 4622 4623 4624 4625 4626 4627 4628 4629 4630 4631 4632 4633 4634 4635 4636 4637 4638 4639 4640 4641 4642 4643 4644 4645 4646 4647 4648 4649 4650 4651 4652 4653 4654 4655 4656 4657 4658 4659 4660 4661 4662 4663 4664 4665 4666 4667 4668 4669 4670 4671 4672 4673 4674 4675 4676 4677 4678 4679 4680 4681 4682 4683 4684 4685 4686 4687 4688 4689 4690 4691 4692 4693 4694 4695 4696 4697 4698 4699 4700 4701 4702 4703 4704 4705 4706 4707 4708 4709 4710 4711 4712 4713 4714 4715 4716 4717 4718 4719 4720 4721 4722 4723 4724 4725 4726 4727 4728 4729 4730 4731 4732 4733 4734 4735 4736 4737 4738 4739 4740 4741 4742 4743 4744 4745 4746 4747 4748 4749 4750 4751 4752 4753 4754 4755 4756 4757 4758 4759 4760 4761 4762 4763 4764 4765 4766 4767 4768 4769 4770 4771 4772 4773 4774 4775 4776 4777 4778 4779 4780 4781 4782 4783 4784 4785 4786 4787 4788 4789 4790 4791 4792 4793 4794 4795 4796 4797 4798 4799 4800 4801 4802 4803 4804 4805 4806 4807 4808 4809 4810 4811 4812 4813 4814 4815 4816 4817 4818 4819 4820 4821 4822 4823 4824 4825 4826 4827 4828 4829 4830 4831 4832 4833 4834 4835 4836 4837 4838 4839 4840 4841 4842 4843 4844 4845 4846 4847 4848 4849 4850 4851 4852 4853 4854 4855 4856 4857 4858 4859 4860 4861 4862 4863 4864 4865 4866 4867 4868 4869 4870 4871 4872 4873 4874 4875 4876 4877 4878 4879 4880 4881 4882 4883 4884 4885 4886 4887 4888 4889 4890 4891 4892 4893 4894 4895 4896 4897 4898 4899 4900 4901 4902 4903 4904 4905 4906 4907 4908 4909 4910 4911 4912 4913 4914 4915 4916 4917 4918 4919 4920 4921 4922 4923 4924 4925 4926 4927 4928 4929 4930 4931 4932 4933 4934 4935 4936 4937 4938 4939 4940 4941 4942 4943 4944 4945 4946 4947 4948 4949 4950 4951 4952 4953 4954 4955 4956 4957 4958 4959 4960 4961 4962 4963 4964 4965 4966 4967 4968 4969 4970 4971 4972 4973 4974 4975 4976 4977 4978 4979 4980 4981 4982 4983 4984 4985 4986 4987 4988 4989 4990 4991 4992 4993 4994 4995 4996 4997 4998 4999 5000 5001 5002 5003 5004 5005 5006 5007 5008 5009 5010 5011 5012 5013 5014 5015 5016 5017 5018 5019 5020 5021 5022 5023 5024 5025 5026 5027 5028 5029 5030 5031 5032 5033 5034 5035 5036 5037 5038 5039 5040 5041 5042 5043 5044 5045 5046 5047 5048 5049 5050 5051 5052 5053 5054 5055 5056 5057 5058 5059 5060 5061 5062 5063 5064 5065 5066 5067 5068 5069 5070 5071 5072 5073 5074 5075 5076 5077 5078 5079 5080 5081 5082 5083 5084 5085 5086 5087 5088 5089 5090 5091 5092 5093 5094 5095 5096 5097 5098 5099 5100 5101 5102 5103 5104 5105 5106 5107 5108 5109 5110 5111 5112 5113 5114 5115 5116 5117 5118 5119 5120 5121 5122 5123 5124 5125 5126 5127 5128 5129 5130 5131 5132 5133 5134 5135 5136 5137 5138 5139 5140 5141 5142 5143 5144 5145 5146 5147 5148 5149 5150 5151 5152 5153 5154 5155 5156 5157 5158 5159 5160 5161 5162 5163 5164 5165 5166 5167 5168 5169 5170 5171 5172 5173 5174 5175 5176 5177 5178 5179 5180 5181 5182 5183 5184 5185 5186 5187 5188 5189 5190 5191 5192 5193 5194 | [ { "markdown": "\tfoo\tbaz\t\tbim\n", "html": "<pre><code>foo\tbaz\t\tbim\n</code></pre>\n", "example": 1, "start_line": 352, "end_line": 357, "section": "Tabs" }, { "markdown": " \tfoo\tbaz\t\tbim\n", "html": "<pre><code>foo\tbaz\t\tbim\n</code></pre>\n", "example": 2, "start_line": 359, "end_line": 364, "section": "Tabs" }, { "markdown": " a\ta\n ὐ\ta\n", "html": "<pre><code>a\ta\nὐ\ta\n</code></pre>\n", "example": 3, "start_line": 366, "end_line": 373, "section": "Tabs" }, { "markdown": " - foo\n\n\tbar\n", "html": "<ul>\n<li>\n<p>foo</p>\n<p>bar</p>\n</li>\n</ul>\n", "example": 4, "start_line": 379, "end_line": 390, "section": "Tabs" }, { "markdown": "- foo\n\n\t\tbar\n", "html": "<ul>\n<li>\n<p>foo</p>\n<pre><code> bar\n</code></pre>\n</li>\n</ul>\n", "example": 5, "start_line": 392, "end_line": 404, "section": "Tabs" }, { "markdown": ">\t\tfoo\n", "html": "<blockquote>\n<pre><code> foo\n</code></pre>\n</blockquote>\n", "example": 6, "start_line": 415, "end_line": 422, "section": "Tabs" }, { "markdown": "-\t\tfoo\n", "html": "<ul>\n<li>\n<pre><code> foo\n</code></pre>\n</li>\n</ul>\n", "example": 7, "start_line": 424, "end_line": 433, "section": "Tabs" }, { "markdown": " foo\n\tbar\n", "html": "<pre><code>foo\nbar\n</code></pre>\n", "example": 8, "start_line": 436, "end_line": 443, "section": "Tabs" }, { "markdown": " - foo\n - bar\n\t - baz\n", "html": "<ul>\n<li>foo\n<ul>\n<li>bar\n<ul>\n<li>baz</li>\n</ul>\n</li>\n</ul>\n</li>\n</ul>\n", "example": 9, "start_line": 445, "end_line": 461, "section": "Tabs" }, { "markdown": "#\tFoo\n", "html": "<h1>Foo</h1>\n", "example": 10, "start_line": 463, "end_line": 467, "section": "Tabs" }, { "markdown": "*\t*\t*\t\n", "html": "<hr />\n", "example": 11, "start_line": 469, "end_line": 473, "section": "Tabs" }, { "markdown": "- `one\n- two`\n", "html": "<ul>\n<li>`one</li>\n<li>two`</li>\n</ul>\n", "example": 12, "start_line": 496, "end_line": 504, "section": "Precedence" }, { "markdown": "***\n---\n___\n", "html": "<hr />\n<hr />\n<hr />\n", "example": 13, "start_line": 535, "end_line": 543, "section": "Thematic breaks" }, { "markdown": "+++\n", "html": "<p>+++</p>\n", "example": 14, "start_line": 548, "end_line": 552, "section": "Thematic breaks" }, { "markdown": "===\n", "html": "<p>===</p>\n", "example": 15, "start_line": 555, "end_line": 559, "section": "Thematic breaks" }, { "markdown": "--\n**\n__\n", "html": "<p>--\n**\n__</p>\n", "example": 16, "start_line": 564, "end_line": 572, "section": "Thematic breaks" }, { "markdown": " ***\n ***\n ***\n", "html": "<hr />\n<hr />\n<hr />\n", "example": 17, "start_line": 577, "end_line": 585, "section": "Thematic breaks" }, { "markdown": " ***\n", "html": "<pre><code>***\n</code></pre>\n", "example": 18, "start_line": 590, "end_line": 595, "section": "Thematic breaks" }, { "markdown": "Foo\n ***\n", "html": "<p>Foo\n***</p>\n", "example": 19, "start_line": 598, "end_line": 604, "section": "Thematic breaks" }, { "markdown": "_____________________________________\n", "html": "<hr />\n", "example": 20, "start_line": 609, "end_line": 613, "section": "Thematic breaks" }, { "markdown": " - - -\n", "html": "<hr />\n", "example": 21, "start_line": 618, "end_line": 622, "section": "Thematic breaks" }, { "markdown": " ** * ** * ** * **\n", "html": "<hr />\n", "example": 22, "start_line": 625, "end_line": 629, "section": "Thematic breaks" }, { "markdown": "- - - -\n", "html": "<hr />\n", "example": 23, "start_line": 632, "end_line": 636, "section": "Thematic breaks" }, { "markdown": "- - - - \n", "html": "<hr />\n", "example": 24, "start_line": 641, "end_line": 645, "section": "Thematic breaks" }, { "markdown": "_ _ _ _ a\n\na------\n\n---a---\n", "html": "<p>_ _ _ _ a</p>\n<p>a------</p>\n<p>---a---</p>\n", "example": 25, "start_line": 650, "end_line": 660, "section": "Thematic breaks" }, { "markdown": " *-*\n", "html": "<p><em>-</em></p>\n", "example": 26, "start_line": 666, "end_line": 670, "section": "Thematic breaks" }, { "markdown": "- foo\n***\n- bar\n", "html": "<ul>\n<li>foo</li>\n</ul>\n<hr />\n<ul>\n<li>bar</li>\n</ul>\n", "example": 27, "start_line": 675, "end_line": 687, "section": "Thematic breaks" }, { "markdown": "Foo\n***\nbar\n", "html": "<p>Foo</p>\n<hr />\n<p>bar</p>\n", "example": 28, "start_line": 692, "end_line": 700, "section": "Thematic breaks" }, { "markdown": "Foo\n---\nbar\n", "html": "<h2>Foo</h2>\n<p>bar</p>\n", "example": 29, "start_line": 709, "end_line": 716, "section": "Thematic breaks" }, { "markdown": "* Foo\n* * *\n* Bar\n", "html": "<ul>\n<li>Foo</li>\n</ul>\n<hr />\n<ul>\n<li>Bar</li>\n</ul>\n", "example": 30, "start_line": 722, "end_line": 734, "section": "Thematic breaks" }, { "markdown": "- Foo\n- * * *\n", "html": "<ul>\n<li>Foo</li>\n<li>\n<hr />\n</li>\n</ul>\n", "example": 31, "start_line": 739, "end_line": 749, "section": "Thematic breaks" }, { "markdown": "# foo\n## foo\n### foo\n#### foo\n##### foo\n###### foo\n", "html": "<h1>foo</h1>\n<h2>foo</h2>\n<h3>foo</h3>\n<h4>foo</h4>\n<h5>foo</h5>\n<h6>foo</h6>\n", "example": 32, "start_line": 768, "end_line": 782, "section": "ATX headings" }, { "markdown": "####### foo\n", "html": "<p>####### foo</p>\n", "example": 33, "start_line": 787, "end_line": 791, "section": "ATX headings" }, { "markdown": "#5 bolt\n\n#hashtag\n", "html": "<p>#5 bolt</p>\n<p>#hashtag</p>\n", "example": 34, "start_line": 802, "end_line": 809, "section": "ATX headings" }, { "markdown": "\\## foo\n", "html": "<p>## foo</p>\n", "example": 35, "start_line": 814, "end_line": 818, "section": "ATX headings" }, { "markdown": "# foo *bar* \\*baz\\*\n", "html": "<h1>foo <em>bar</em> *baz*</h1>\n", "example": 36, "start_line": 823, "end_line": 827, "section": "ATX headings" }, { "markdown": "# foo \n", "html": "<h1>foo</h1>\n", "example": 37, "start_line": 832, "end_line": 836, "section": "ATX headings" }, { "markdown": " ### foo\n ## foo\n # foo\n", "html": "<h3>foo</h3>\n<h2>foo</h2>\n<h1>foo</h1>\n", "example": 38, "start_line": 841, "end_line": 849, "section": "ATX headings" }, { "markdown": " # foo\n", "html": "<pre><code># foo\n</code></pre>\n", "example": 39, "start_line": 854, "end_line": 859, "section": "ATX headings" }, { "markdown": "foo\n # bar\n", "html": "<p>foo\n# bar</p>\n", "example": 40, "start_line": 862, "end_line": 868, "section": "ATX headings" }, { "markdown": "## foo ##\n ### bar ###\n", "html": "<h2>foo</h2>\n<h3>bar</h3>\n", "example": 41, "start_line": 873, "end_line": 879, "section": "ATX headings" }, { "markdown": "# foo ##################################\n##### foo ##\n", "html": "<h1>foo</h1>\n<h5>foo</h5>\n", "example": 42, "start_line": 884, "end_line": 890, "section": "ATX headings" }, { "markdown": "### foo ### \n", "html": "<h3>foo</h3>\n", "example": 43, "start_line": 895, "end_line": 899, "section": "ATX headings" }, { "markdown": "### foo ### b\n", "html": "<h3>foo ### b</h3>\n", "example": 44, "start_line": 906, "end_line": 910, "section": "ATX headings" }, { "markdown": "# foo#\n", "html": "<h1>foo#</h1>\n", "example": 45, "start_line": 915, "end_line": 919, "section": "ATX headings" }, { "markdown": "### foo \\###\n## foo #\\##\n# foo \\#\n", "html": "<h3>foo ###</h3>\n<h2>foo ###</h2>\n<h1>foo #</h1>\n", "example": 46, "start_line": 925, "end_line": 933, "section": "ATX headings" }, { "markdown": "****\n## foo\n****\n", "html": "<hr />\n<h2>foo</h2>\n<hr />\n", "example": 47, "start_line": 939, "end_line": 947, "section": "ATX headings" }, { "markdown": "Foo bar\n# baz\nBar foo\n", "html": "<p>Foo bar</p>\n<h1>baz</h1>\n<p>Bar foo</p>\n", "example": 48, "start_line": 950, "end_line": 958, "section": "ATX headings" }, { "markdown": "## \n#\n### ###\n", "html": "<h2></h2>\n<h1></h1>\n<h3></h3>\n", "example": 49, "start_line": 963, "end_line": 971, "section": "ATX headings" }, { "markdown": "Foo *bar*\n=========\n\nFoo *bar*\n---------\n", "html": "<h1>Foo <em>bar</em></h1>\n<h2>Foo <em>bar</em></h2>\n", "example": 50, "start_line": 1006, "end_line": 1015, "section": "Setext headings" }, { "markdown": "Foo *bar\nbaz*\n====\n", "html": "<h1>Foo <em>bar\nbaz</em></h1>\n", "example": 51, "start_line": 1020, "end_line": 1027, "section": "Setext headings" }, { "markdown": " Foo *bar\nbaz*\t\n====\n", "html": "<h1>Foo <em>bar\nbaz</em></h1>\n", "example": 52, "start_line": 1034, "end_line": 1041, "section": "Setext headings" }, { "markdown": "Foo\n-------------------------\n\nFoo\n=\n", "html": "<h2>Foo</h2>\n<h1>Foo</h1>\n", "example": 53, "start_line": 1046, "end_line": 1055, "section": "Setext headings" }, { "markdown": " Foo\n---\n\n Foo\n-----\n\n Foo\n ===\n", "html": "<h2>Foo</h2>\n<h2>Foo</h2>\n<h1>Foo</h1>\n", "example": 54, "start_line": 1061, "end_line": 1074, "section": "Setext headings" }, { "markdown": " Foo\n ---\n\n Foo\n---\n", "html": "<pre><code>Foo\n---\n\nFoo\n</code></pre>\n<hr />\n", "example": 55, "start_line": 1079, "end_line": 1092, "section": "Setext headings" }, { "markdown": "Foo\n ---- \n", "html": "<h2>Foo</h2>\n", "example": 56, "start_line": 1098, "end_line": 1103, "section": "Setext headings" }, { "markdown": "Foo\n ---\n", "html": "<p>Foo\n---</p>\n", "example": 57, "start_line": 1108, "end_line": 1114, "section": "Setext headings" }, { "markdown": "Foo\n= =\n\nFoo\n--- -\n", "html": "<p>Foo\n= =</p>\n<p>Foo</p>\n<hr />\n", "example": 58, "start_line": 1119, "end_line": 1130, "section": "Setext headings" }, { "markdown": "Foo \n-----\n", "html": "<h2>Foo</h2>\n", "example": 59, "start_line": 1135, "end_line": 1140, "section": "Setext headings" }, { "markdown": "Foo\\\n----\n", "html": "<h2>Foo\\</h2>\n", "example": 60, "start_line": 1145, "end_line": 1150, "section": "Setext headings" }, { "markdown": "`Foo\n----\n`\n\n<a title=\"a lot\n---\nof dashes\"/>\n", "html": "<h2>`Foo</h2>\n<p>`</p>\n<h2><a title="a lot</h2>\n<p>of dashes"/></p>\n", "example": 61, "start_line": 1156, "end_line": 1169, "section": "Setext headings" }, { "markdown": "> Foo\n---\n", "html": "<blockquote>\n<p>Foo</p>\n</blockquote>\n<hr />\n", "example": 62, "start_line": 1175, "end_line": 1183, "section": "Setext headings" }, { "markdown": "> foo\nbar\n===\n", "html": "<blockquote>\n<p>foo\nbar\n===</p>\n</blockquote>\n", "example": 63, "start_line": 1186, "end_line": 1196, "section": "Setext headings" }, { "markdown": "- Foo\n---\n", "html": "<ul>\n<li>Foo</li>\n</ul>\n<hr />\n", "example": 64, "start_line": 1199, "end_line": 1207, "section": "Setext headings" }, { "markdown": "Foo\nBar\n---\n", "html": "<h2>Foo\nBar</h2>\n", "example": 65, "start_line": 1214, "end_line": 1221, "section": "Setext headings" }, { "markdown": "---\nFoo\n---\nBar\n---\nBaz\n", "html": "<hr />\n<h2>Foo</h2>\n<h2>Bar</h2>\n<p>Baz</p>\n", "example": 66, "start_line": 1227, "end_line": 1239, "section": "Setext headings" }, { "markdown": "\n====\n", "html": "<p>====</p>\n", "example": 67, "start_line": 1244, "end_line": 1249, "section": "Setext headings" }, { "markdown": "---\n---\n", "html": "<hr />\n<hr />\n", "example": 68, "start_line": 1256, "end_line": 1262, "section": "Setext headings" }, { "markdown": "- foo\n-----\n", "html": "<ul>\n<li>foo</li>\n</ul>\n<hr />\n", "example": 69, "start_line": 1265, "end_line": 1273, "section": "Setext headings" }, { "markdown": " foo\n---\n", "html": "<pre><code>foo\n</code></pre>\n<hr />\n", "example": 70, "start_line": 1276, "end_line": 1283, "section": "Setext headings" }, { "markdown": "> foo\n-----\n", "html": "<blockquote>\n<p>foo</p>\n</blockquote>\n<hr />\n", "example": 71, "start_line": 1286, "end_line": 1294, "section": "Setext headings" }, { "markdown": "\\> foo\n------\n", "html": "<h2>> foo</h2>\n", "example": 72, "start_line": 1300, "end_line": 1305, "section": "Setext headings" }, { "markdown": "Foo\n\nbar\n---\nbaz\n", "html": "<p>Foo</p>\n<h2>bar</h2>\n<p>baz</p>\n", "example": 73, "start_line": 1331, "end_line": 1341, "section": "Setext headings" }, { "markdown": "Foo\nbar\n\n---\n\nbaz\n", "html": "<p>Foo\nbar</p>\n<hr />\n<p>baz</p>\n", "example": 74, "start_line": 1347, "end_line": 1359, "section": "Setext headings" }, { "markdown": "Foo\nbar\n* * *\nbaz\n", "html": "<p>Foo\nbar</p>\n<hr />\n<p>baz</p>\n", "example": 75, "start_line": 1365, "end_line": 1375, "section": "Setext headings" }, { "markdown": "Foo\nbar\n\\---\nbaz\n", "html": "<p>Foo\nbar\n---\nbaz</p>\n", "example": 76, "start_line": 1380, "end_line": 1390, "section": "Setext headings" }, { "markdown": " a simple\n indented code block\n", "html": "<pre><code>a simple\n indented code block\n</code></pre>\n", "example": 77, "start_line": 1408, "end_line": 1415, "section": "Indented code blocks" }, { "markdown": " - foo\n\n bar\n", "html": "<ul>\n<li>\n<p>foo</p>\n<p>bar</p>\n</li>\n</ul>\n", "example": 78, "start_line": 1422, "end_line": 1433, "section": "Indented code blocks" }, { "markdown": "1. foo\n\n - bar\n", "html": "<ol>\n<li>\n<p>foo</p>\n<ul>\n<li>bar</li>\n</ul>\n</li>\n</ol>\n", "example": 79, "start_line": 1436, "end_line": 1449, "section": "Indented code blocks" }, { "markdown": " <a/>\n *hi*\n\n - one\n", "html": "<pre><code><a/>\n*hi*\n\n- one\n</code></pre>\n", "example": 80, "start_line": 1456, "end_line": 1467, "section": "Indented code blocks" }, { "markdown": " chunk1\n\n chunk2\n \n \n \n chunk3\n", "html": "<pre><code>chunk1\n\nchunk2\n\n\n\nchunk3\n</code></pre>\n", "example": 81, "start_line": 1472, "end_line": 1489, "section": "Indented code blocks" }, { "markdown": " chunk1\n \n chunk2\n", "html": "<pre><code>chunk1\n \n chunk2\n</code></pre>\n", "example": 82, "start_line": 1495, "end_line": 1504, "section": "Indented code blocks" }, { "markdown": "Foo\n bar\n\n", "html": "<p>Foo\nbar</p>\n", "example": 83, "start_line": 1510, "end_line": 1517, "section": "Indented code blocks" }, { "markdown": " foo\nbar\n", "html": "<pre><code>foo\n</code></pre>\n<p>bar</p>\n", "example": 84, "start_line": 1524, "end_line": 1531, "section": "Indented code blocks" }, { "markdown": "# Heading\n foo\nHeading\n------\n foo\n----\n", "html": "<h1>Heading</h1>\n<pre><code>foo\n</code></pre>\n<h2>Heading</h2>\n<pre><code>foo\n</code></pre>\n<hr />\n", "example": 85, "start_line": 1537, "end_line": 1552, "section": "Indented code blocks" }, { "markdown": " foo\n bar\n", "html": "<pre><code> foo\nbar\n</code></pre>\n", "example": 86, "start_line": 1557, "end_line": 1564, "section": "Indented code blocks" }, { "markdown": "\n \n foo\n \n\n", "html": "<pre><code>foo\n</code></pre>\n", "example": 87, "start_line": 1570, "end_line": 1579, "section": "Indented code blocks" }, { "markdown": " foo \n", "html": "<pre><code>foo \n</code></pre>\n", "example": 88, "start_line": 1584, "end_line": 1589, "section": "Indented code blocks" }, { "markdown": "```\n<\n >\n```\n", "html": "<pre><code><\n >\n</code></pre>\n", "example": 89, "start_line": 1639, "end_line": 1648, "section": "Fenced code blocks" }, { "markdown": "~~~\n<\n >\n~~~\n", "html": "<pre><code><\n >\n</code></pre>\n", "example": 90, "start_line": 1653, "end_line": 1662, "section": "Fenced code blocks" }, { "markdown": "``\nfoo\n``\n", "html": "<p><code>foo</code></p>\n", "example": 91, "start_line": 1666, "end_line": 1672, "section": "Fenced code blocks" }, { "markdown": "```\naaa\n~~~\n```\n", "html": "<pre><code>aaa\n~~~\n</code></pre>\n", "example": 92, "start_line": 1677, "end_line": 1686, "section": "Fenced code blocks" }, { "markdown": "~~~\naaa\n```\n~~~\n", "html": "<pre><code>aaa\n```\n</code></pre>\n", "example": 93, "start_line": 1689, "end_line": 1698, "section": "Fenced code blocks" }, { "markdown": "````\naaa\n```\n``````\n", "html": "<pre><code>aaa\n```\n</code></pre>\n", "example": 94, "start_line": 1703, "end_line": 1712, "section": "Fenced code blocks" }, { "markdown": "~~~~\naaa\n~~~\n~~~~\n", "html": "<pre><code>aaa\n~~~\n</code></pre>\n", "example": 95, "start_line": 1715, "end_line": 1724, "section": "Fenced code blocks" }, { "markdown": "```\n", "html": "<pre><code></code></pre>\n", "example": 96, "start_line": 1730, "end_line": 1734, "section": "Fenced code blocks" }, { "markdown": "`````\n\n```\naaa\n", "html": "<pre><code>\n```\naaa\n</code></pre>\n", "example": 97, "start_line": 1737, "end_line": 1747, "section": "Fenced code blocks" }, { "markdown": "> ```\n> aaa\n\nbbb\n", "html": "<blockquote>\n<pre><code>aaa\n</code></pre>\n</blockquote>\n<p>bbb</p>\n", "example": 98, "start_line": 1750, "end_line": 1761, "section": "Fenced code blocks" }, { "markdown": "```\n\n \n```\n", "html": "<pre><code>\n \n</code></pre>\n", "example": 99, "start_line": 1766, "end_line": 1775, "section": "Fenced code blocks" }, { "markdown": "```\n```\n", "html": "<pre><code></code></pre>\n", "example": 100, "start_line": 1780, "end_line": 1785, "section": "Fenced code blocks" }, { "markdown": " ```\n aaa\naaa\n```\n", "html": "<pre><code>aaa\naaa\n</code></pre>\n", "example": 101, "start_line": 1792, "end_line": 1801, "section": "Fenced code blocks" }, { "markdown": " ```\naaa\n aaa\naaa\n ```\n", "html": "<pre><code>aaa\naaa\naaa\n</code></pre>\n", "example": 102, "start_line": 1804, "end_line": 1815, "section": "Fenced code blocks" }, { "markdown": " ```\n aaa\n aaa\n aaa\n ```\n", "html": "<pre><code>aaa\n aaa\naaa\n</code></pre>\n", "example": 103, "start_line": 1818, "end_line": 1829, "section": "Fenced code blocks" }, { "markdown": " ```\n aaa\n ```\n", "html": "<pre><code>```\naaa\n```\n</code></pre>\n", "example": 104, "start_line": 1834, "end_line": 1843, "section": "Fenced code blocks" }, { "markdown": "```\naaa\n ```\n", "html": "<pre><code>aaa\n</code></pre>\n", "example": 105, "start_line": 1849, "end_line": 1856, "section": "Fenced code blocks" }, { "markdown": " ```\naaa\n ```\n", "html": "<pre><code>aaa\n</code></pre>\n", "example": 106, "start_line": 1859, "end_line": 1866, "section": "Fenced code blocks" }, { "markdown": "```\naaa\n ```\n", "html": "<pre><code>aaa\n ```\n</code></pre>\n", "example": 107, "start_line": 1871, "end_line": 1879, "section": "Fenced code blocks" }, { "markdown": "``` ```\naaa\n", "html": "<p><code> </code>\naaa</p>\n", "example": 108, "start_line": 1885, "end_line": 1891, "section": "Fenced code blocks" }, { "markdown": "~~~~~~\naaa\n~~~ ~~\n", "html": "<pre><code>aaa\n~~~ ~~\n</code></pre>\n", "example": 109, "start_line": 1894, "end_line": 1902, "section": "Fenced code blocks" }, { "markdown": "foo\n```\nbar\n```\nbaz\n", "html": "<p>foo</p>\n<pre><code>bar\n</code></pre>\n<p>baz</p>\n", "example": 110, "start_line": 1908, "end_line": 1919, "section": "Fenced code blocks" }, { "markdown": "foo\n---\n~~~\nbar\n~~~\n# baz\n", "html": "<h2>foo</h2>\n<pre><code>bar\n</code></pre>\n<h1>baz</h1>\n", "example": 111, "start_line": 1925, "end_line": 1937, "section": "Fenced code blocks" }, { "markdown": "```ruby\ndef foo(x)\n return 3\nend\n```\n", "html": "<pre><code class=\"language-ruby\">def foo(x)\n return 3\nend\n</code></pre>\n", "example": 112, "start_line": 1947, "end_line": 1958, "section": "Fenced code blocks" }, { "markdown": "~~~~ ruby startline=3 $%@#$\ndef foo(x)\n return 3\nend\n~~~~~~~\n", "html": "<pre><code class=\"language-ruby\">def foo(x)\n return 3\nend\n</code></pre>\n", "example": 113, "start_line": 1961, "end_line": 1972, "section": "Fenced code blocks" }, { "markdown": "````;\n````\n", "html": "<pre><code class=\"language-;\"></code></pre>\n", "example": 114, "start_line": 1975, "end_line": 1980, "section": "Fenced code blocks" }, { "markdown": "``` aa ```\nfoo\n", "html": "<p><code>aa</code>\nfoo</p>\n", "example": 115, "start_line": 1985, "end_line": 1991, "section": "Fenced code blocks" }, { "markdown": "~~~ aa ``` ~~~\nfoo\n~~~\n", "html": "<pre><code class=\"language-aa\">foo\n</code></pre>\n", "example": 116, "start_line": 1996, "end_line": 2003, "section": "Fenced code blocks" }, { "markdown": "```\n``` aaa\n```\n", "html": "<pre><code>``` aaa\n</code></pre>\n", "example": 117, "start_line": 2008, "end_line": 2015, "section": "Fenced code blocks" }, { "markdown": "<table><tr><td>\n<pre>\n**Hello**,\n\n_world_.\n</pre>\n</td></tr></table>\n", "html": "<table><tr><td>\n<pre>\n**Hello**,\n<p><em>world</em>.\n</pre></p>\n</td></tr></table>\n", "example": 118, "start_line": 2087, "end_line": 2102, "section": "HTML blocks" }, { "markdown": "<table>\n <tr>\n <td>\n hi\n </td>\n </tr>\n</table>\n\nokay.\n", "html": "<table>\n <tr>\n <td>\n hi\n </td>\n </tr>\n</table>\n<p>okay.</p>\n", "example": 119, "start_line": 2116, "end_line": 2135, "section": "HTML blocks" }, { "markdown": " <div>\n *hello*\n <foo><a>\n", "html": " <div>\n *hello*\n <foo><a>\n", "example": 120, "start_line": 2138, "end_line": 2146, "section": "HTML blocks" }, { "markdown": "</div>\n*foo*\n", "html": "</div>\n*foo*\n", "example": 121, "start_line": 2151, "end_line": 2157, "section": "HTML blocks" }, { "markdown": "<DIV CLASS=\"foo\">\n\n*Markdown*\n\n</DIV>\n", "html": "<DIV CLASS=\"foo\">\n<p><em>Markdown</em></p>\n</DIV>\n", "example": 122, "start_line": 2162, "end_line": 2172, "section": "HTML blocks" }, { "markdown": "<div id=\"foo\"\n class=\"bar\">\n</div>\n", "html": "<div id=\"foo\"\n class=\"bar\">\n</div>\n", "example": 123, "start_line": 2178, "end_line": 2186, "section": "HTML blocks" }, { "markdown": "<div id=\"foo\" class=\"bar\n baz\">\n</div>\n", "html": "<div id=\"foo\" class=\"bar\n baz\">\n</div>\n", "example": 124, "start_line": 2189, "end_line": 2197, "section": "HTML blocks" }, { "markdown": "<div>\n*foo*\n\n*bar*\n", "html": "<div>\n*foo*\n<p><em>bar</em></p>\n", "example": 125, "start_line": 2201, "end_line": 2210, "section": "HTML blocks" }, { "markdown": "<div id=\"foo\"\n*hi*\n", "html": "<div id=\"foo\"\n*hi*\n", "example": 126, "start_line": 2217, "end_line": 2223, "section": "HTML blocks" }, { "markdown": "<div class\nfoo\n", "html": "<div class\nfoo\n", "example": 127, "start_line": 2226, "end_line": 2232, "section": "HTML blocks" }, { "markdown": "<div *???-&&&-<---\n*foo*\n", "html": "<div *???-&&&-<---\n*foo*\n", "example": 128, "start_line": 2238, "end_line": 2244, "section": "HTML blocks" }, { "markdown": "<div><a href=\"bar\">*foo*</a></div>\n", "html": "<div><a href=\"bar\">*foo*</a></div>\n", "example": 129, "start_line": 2250, "end_line": 2254, "section": "HTML blocks" }, { "markdown": "<table><tr><td>\nfoo\n</td></tr></table>\n", "html": "<table><tr><td>\nfoo\n</td></tr></table>\n", "example": 130, "start_line": 2257, "end_line": 2265, "section": "HTML blocks" }, { "markdown": "<div></div>\n``` c\nint x = 33;\n```\n", "html": "<div></div>\n``` c\nint x = 33;\n```\n", "example": 131, "start_line": 2274, "end_line": 2284, "section": "HTML blocks" }, { "markdown": "<a href=\"foo\">\n*bar*\n</a>\n", "html": "<a href=\"foo\">\n*bar*\n</a>\n", "example": 132, "start_line": 2291, "end_line": 2299, "section": "HTML blocks" }, { "markdown": "<Warning>\n*bar*\n</Warning>\n", "html": "<Warning>\n*bar*\n</Warning>\n", "example": 133, "start_line": 2304, "end_line": 2312, "section": "HTML blocks" }, { "markdown": "<i class=\"foo\">\n*bar*\n</i>\n", "html": "<i class=\"foo\">\n*bar*\n</i>\n", "example": 134, "start_line": 2315, "end_line": 2323, "section": "HTML blocks" }, { "markdown": "</ins>\n*bar*\n", "html": "</ins>\n*bar*\n", "example": 135, "start_line": 2326, "end_line": 2332, "section": "HTML blocks" }, { "markdown": "<del>\n*foo*\n</del>\n", "html": "<del>\n*foo*\n</del>\n", "example": 136, "start_line": 2341, "end_line": 2349, "section": "HTML blocks" }, { "markdown": "<del>\n\n*foo*\n\n</del>\n", "html": "<del>\n<p><em>foo</em></p>\n</del>\n", "example": 137, "start_line": 2356, "end_line": 2366, "section": "HTML blocks" }, { "markdown": "<del>*foo*</del>\n", "html": "<p><del><em>foo</em></del></p>\n", "example": 138, "start_line": 2374, "end_line": 2378, "section": "HTML blocks" }, { "markdown": "<pre language=\"haskell\"><code>\nimport Text.HTML.TagSoup\n\nmain :: IO ()\nmain = print $ parseTags tags\n</code></pre>\nokay\n", "html": "<pre language=\"haskell\"><code>\nimport Text.HTML.TagSoup\n\nmain :: IO ()\nmain = print $ parseTags tags\n</code></pre>\n<p>okay</p>\n", "example": 139, "start_line": 2390, "end_line": 2406, "section": "HTML blocks" }, { "markdown": "<script type=\"text/javascript\">\n// JavaScript example\n\ndocument.getElementById(\"demo\").innerHTML = \"Hello JavaScript!\";\n</script>\nokay\n", "html": "<script type=\"text/javascript\">\n// JavaScript example\n\ndocument.getElementById(\"demo\").innerHTML = \"Hello JavaScript!\";\n</script>\n<p>okay</p>\n", "example": 140, "start_line": 2411, "end_line": 2425, "section": "HTML blocks" }, { "markdown": "<style\n type=\"text/css\">\nh1 {color:red;}\n\np {color:blue;}\n</style>\nokay\n", "html": "<style\n type=\"text/css\">\nh1 {color:red;}\n\np {color:blue;}\n</style>\n<p>okay</p>\n", "example": 141, "start_line": 2430, "end_line": 2446, "section": "HTML blocks" }, { "markdown": "<style\n type=\"text/css\">\n\nfoo\n", "html": "<style\n type=\"text/css\">\n\nfoo\n", "example": 142, "start_line": 2453, "end_line": 2463, "section": "HTML blocks" }, { "markdown": "> <div>\n> foo\n\nbar\n", "html": "<blockquote>\n<div>\nfoo\n</blockquote>\n<p>bar</p>\n", "example": 143, "start_line": 2466, "end_line": 2477, "section": "HTML blocks" }, { "markdown": "- <div>\n- foo\n", "html": "<ul>\n<li>\n<div>\n</li>\n<li>foo</li>\n</ul>\n", "example": 144, "start_line": 2480, "end_line": 2490, "section": "HTML blocks" }, { "markdown": "<style>p{color:red;}</style>\n*foo*\n", "html": "<style>p{color:red;}</style>\n<p><em>foo</em></p>\n", "example": 145, "start_line": 2495, "end_line": 2501, "section": "HTML blocks" }, { "markdown": "<!-- foo -->*bar*\n*baz*\n", "html": "<!-- foo -->*bar*\n<p><em>baz</em></p>\n", "example": 146, "start_line": 2504, "end_line": 2510, "section": "HTML blocks" }, { "markdown": "<script>\nfoo\n</script>1. *bar*\n", "html": "<script>\nfoo\n</script>1. *bar*\n", "example": 147, "start_line": 2516, "end_line": 2524, "section": "HTML blocks" }, { "markdown": "<!-- Foo\n\nbar\n baz -->\nokay\n", "html": "<!-- Foo\n\nbar\n baz -->\n<p>okay</p>\n", "example": 148, "start_line": 2529, "end_line": 2541, "section": "HTML blocks" }, { "markdown": "<?php\n\n echo '>';\n\n?>\nokay\n", "html": "<?php\n\n echo '>';\n\n?>\n<p>okay</p>\n", "example": 149, "start_line": 2547, "end_line": 2561, "section": "HTML blocks" }, { "markdown": "<!DOCTYPE html>\n", "html": "<!DOCTYPE html>\n", "example": 150, "start_line": 2566, "end_line": 2570, "section": "HTML blocks" }, { "markdown": "<![CDATA[\nfunction matchwo(a,b)\n{\n if (a < b && a < 0) then {\n return 1;\n\n } else {\n\n return 0;\n }\n}\n]]>\nokay\n", "html": "<![CDATA[\nfunction matchwo(a,b)\n{\n if (a < b && a < 0) then {\n return 1;\n\n } else {\n\n return 0;\n }\n}\n]]>\n<p>okay</p>\n", "example": 151, "start_line": 2575, "end_line": 2603, "section": "HTML blocks" }, { "markdown": " <!-- foo -->\n\n <!-- foo -->\n", "html": " <!-- foo -->\n<pre><code><!-- foo -->\n</code></pre>\n", "example": 152, "start_line": 2608, "end_line": 2616, "section": "HTML blocks" }, { "markdown": " <div>\n\n <div>\n", "html": " <div>\n<pre><code><div>\n</code></pre>\n", "example": 153, "start_line": 2619, "end_line": 2627, "section": "HTML blocks" }, { "markdown": "Foo\n<div>\nbar\n</div>\n", "html": "<p>Foo</p>\n<div>\nbar\n</div>\n", "example": 154, "start_line": 2633, "end_line": 2643, "section": "HTML blocks" }, { "markdown": "<div>\nbar\n</div>\n*foo*\n", "html": "<div>\nbar\n</div>\n*foo*\n", "example": 155, "start_line": 2650, "end_line": 2660, "section": "HTML blocks" }, { "markdown": "Foo\n<a href=\"bar\">\nbaz\n", "html": "<p>Foo\n<a href=\"bar\">\nbaz</p>\n", "example": 156, "start_line": 2665, "end_line": 2673, "section": "HTML blocks" }, { "markdown": "<div>\n\n*Emphasized* text.\n\n</div>\n", "html": "<div>\n<p><em>Emphasized</em> text.</p>\n</div>\n", "example": 157, "start_line": 2706, "end_line": 2716, "section": "HTML blocks" }, { "markdown": "<div>\n*Emphasized* text.\n</div>\n", "html": "<div>\n*Emphasized* text.\n</div>\n", "example": 158, "start_line": 2719, "end_line": 2727, "section": "HTML blocks" }, { "markdown": "<table>\n\n<tr>\n\n<td>\nHi\n</td>\n\n</tr>\n\n</table>\n", "html": "<table>\n<tr>\n<td>\nHi\n</td>\n</tr>\n</table>\n", "example": 159, "start_line": 2741, "end_line": 2761, "section": "HTML blocks" }, { "markdown": "<table>\n\n <tr>\n\n <td>\n Hi\n </td>\n\n </tr>\n\n</table>\n", "html": "<table>\n <tr>\n<pre><code><td>\n Hi\n</td>\n</code></pre>\n </tr>\n</table>\n", "example": 160, "start_line": 2768, "end_line": 2789, "section": "HTML blocks" }, { "markdown": "[foo]: /url \"title\"\n\n[foo]\n", "html": "<p><a href=\"/url\" title=\"title\">foo</a></p>\n", "example": 161, "start_line": 2816, "end_line": 2822, "section": "Link reference definitions" }, { "markdown": " [foo]: \n /url \n 'the title' \n\n[foo]\n", "html": "<p><a href=\"/url\" title=\"the title\">foo</a></p>\n", "example": 162, "start_line": 2825, "end_line": 2833, "section": "Link reference definitions" }, { "markdown": "[Foo*bar\\]]:my_(url) 'title (with parens)'\n\n[Foo*bar\\]]\n", "html": "<p><a href=\"my_(url)\" title=\"title (with parens)\">Foo*bar]</a></p>\n", "example": 163, "start_line": 2836, "end_line": 2842, "section": "Link reference definitions" }, { "markdown": "[Foo bar]:\n<my url>\n'title'\n\n[Foo bar]\n", "html": "<p><a href=\"my%20url\" title=\"title\">Foo bar</a></p>\n", "example": 164, "start_line": 2845, "end_line": 2853, "section": "Link reference definitions" }, { "markdown": "[foo]: /url '\ntitle\nline1\nline2\n'\n\n[foo]\n", "html": "<p><a href=\"/url\" title=\"\ntitle\nline1\nline2\n\">foo</a></p>\n", "example": 165, "start_line": 2858, "end_line": 2872, "section": "Link reference definitions" }, { "markdown": "[foo]: /url 'title\n\nwith blank line'\n\n[foo]\n", "html": "<p>[foo]: /url 'title</p>\n<p>with blank line'</p>\n<p>[foo]</p>\n", "example": 166, "start_line": 2877, "end_line": 2887, "section": "Link reference definitions" }, { "markdown": "[foo]:\n/url\n\n[foo]\n", "html": "<p><a href=\"/url\">foo</a></p>\n", "example": 167, "start_line": 2892, "end_line": 2899, "section": "Link reference definitions" }, { "markdown": "[foo]:\n\n[foo]\n", "html": "<p>[foo]:</p>\n<p>[foo]</p>\n", "example": 168, "start_line": 2904, "end_line": 2911, "section": "Link reference definitions" }, { "markdown": "[foo]: <>\n\n[foo]\n", "html": "<p><a href=\"\">foo</a></p>\n", "example": 169, "start_line": 2916, "end_line": 2922, "section": "Link reference definitions" }, { "markdown": "[foo]: <bar>(baz)\n\n[foo]\n", "html": "<p>[foo]: <bar>(baz)</p>\n<p>[foo]</p>\n", "example": 170, "start_line": 2927, "end_line": 2934, "section": "Link reference definitions" }, { "markdown": "[foo]: /url\\bar\\*baz \"foo\\\"bar\\baz\"\n\n[foo]\n", "html": "<p><a href=\"/url%5Cbar*baz\" title=\"foo"bar\\baz\">foo</a></p>\n", "example": 171, "start_line": 2940, "end_line": 2946, "section": "Link reference definitions" }, { "markdown": "[foo]\n\n[foo]: url\n", "html": "<p><a href=\"url\">foo</a></p>\n", "example": 172, "start_line": 2951, "end_line": 2957, "section": "Link reference definitions" }, { "markdown": "[foo]\n\n[foo]: first\n[foo]: second\n", "html": "<p><a href=\"first\">foo</a></p>\n", "example": 173, "start_line": 2963, "end_line": 2970, "section": "Link reference definitions" }, { "markdown": "[FOO]: /url\n\n[Foo]\n", "html": "<p><a href=\"/url\">Foo</a></p>\n", "example": 174, "start_line": 2976, "end_line": 2982, "section": "Link reference definitions" }, { "markdown": "[ΑΓΩ]: /φου\n\n[αγω]\n", "html": "<p><a href=\"/%CF%86%CE%BF%CF%85\">αγω</a></p>\n", "example": 175, "start_line": 2985, "end_line": 2991, "section": "Link reference definitions" }, { "markdown": "[foo]: /url\n", "html": "", "example": 176, "start_line": 2997, "end_line": 3000, "section": "Link reference definitions" }, { "markdown": "[\nfoo\n]: /url\nbar\n", "html": "<p>bar</p>\n", "example": 177, "start_line": 3005, "end_line": 3012, "section": "Link reference definitions" }, { "markdown": "[foo]: /url \"title\" ok\n", "html": "<p>[foo]: /url "title" ok</p>\n", "example": 178, "start_line": 3018, "end_line": 3022, "section": "Link reference definitions" }, { "markdown": "[foo]: /url\n\"title\" ok\n", "html": "<p>"title" ok</p>\n", "example": 179, "start_line": 3027, "end_line": 3032, "section": "Link reference definitions" }, { "markdown": " [foo]: /url \"title\"\n\n[foo]\n", "html": "<pre><code>[foo]: /url "title"\n</code></pre>\n<p>[foo]</p>\n", "example": 180, "start_line": 3038, "end_line": 3046, "section": "Link reference definitions" }, { "markdown": "```\n[foo]: /url\n```\n\n[foo]\n", "html": "<pre><code>[foo]: /url\n</code></pre>\n<p>[foo]</p>\n", "example": 181, "start_line": 3052, "end_line": 3062, "section": "Link reference definitions" }, { "markdown": "Foo\n[bar]: /baz\n\n[bar]\n", "html": "<p>Foo\n[bar]: /baz</p>\n<p>[bar]</p>\n", "example": 182, "start_line": 3067, "end_line": 3076, "section": "Link reference definitions" }, { "markdown": "# [Foo]\n[foo]: /url\n> bar\n", "html": "<h1><a href=\"/url\">Foo</a></h1>\n<blockquote>\n<p>bar</p>\n</blockquote>\n", "example": 183, "start_line": 3082, "end_line": 3091, "section": "Link reference definitions" }, { "markdown": "[foo]: /url\nbar\n===\n[foo]\n", "html": "<h1>bar</h1>\n<p><a href=\"/url\">foo</a></p>\n", "example": 184, "start_line": 3093, "end_line": 3101, "section": "Link reference definitions" }, { "markdown": "[foo]: /url\n===\n[foo]\n", "html": "<p>===\n<a href=\"/url\">foo</a></p>\n", "example": 185, "start_line": 3103, "end_line": 3110, "section": "Link reference definitions" }, { "markdown": "[foo]: /foo-url \"foo\"\n[bar]: /bar-url\n \"bar\"\n[baz]: /baz-url\n\n[foo],\n[bar],\n[baz]\n", "html": "<p><a href=\"/foo-url\" title=\"foo\">foo</a>,\n<a href=\"/bar-url\" title=\"bar\">bar</a>,\n<a href=\"/baz-url\">baz</a></p>\n", "example": 186, "start_line": 3116, "end_line": 3129, "section": "Link reference definitions" }, { "markdown": "[foo]\n\n> [foo]: /url\n", "html": "<p><a href=\"/url\">foo</a></p>\n<blockquote>\n</blockquote>\n", "example": 187, "start_line": 3137, "end_line": 3145, "section": "Link reference definitions" }, { "markdown": "[foo]: /url\n", "html": "", "example": 188, "start_line": 3154, "end_line": 3157, "section": "Link reference definitions" }, { "markdown": "aaa\n\nbbb\n", "html": "<p>aaa</p>\n<p>bbb</p>\n", "example": 189, "start_line": 3171, "end_line": 3178, "section": "Paragraphs" }, { "markdown": "aaa\nbbb\n\nccc\nddd\n", "html": "<p>aaa\nbbb</p>\n<p>ccc\nddd</p>\n", "example": 190, "start_line": 3183, "end_line": 3194, "section": "Paragraphs" }, { "markdown": "aaa\n\n\nbbb\n", "html": "<p>aaa</p>\n<p>bbb</p>\n", "example": 191, "start_line": 3199, "end_line": 3207, "section": "Paragraphs" }, { "markdown": " aaa\n bbb\n", "html": "<p>aaa\nbbb</p>\n", "example": 192, "start_line": 3212, "end_line": 3218, "section": "Paragraphs" }, { "markdown": "aaa\n bbb\n ccc\n", "html": "<p>aaa\nbbb\nccc</p>\n", "example": 193, "start_line": 3224, "end_line": 3232, "section": "Paragraphs" }, { "markdown": " aaa\nbbb\n", "html": "<p>aaa\nbbb</p>\n", "example": 194, "start_line": 3238, "end_line": 3244, "section": "Paragraphs" }, { "markdown": " aaa\nbbb\n", "html": "<pre><code>aaa\n</code></pre>\n<p>bbb</p>\n", "example": 195, "start_line": 3247, "end_line": 3254, "section": "Paragraphs" }, { "markdown": "aaa \nbbb \n", "html": "<p>aaa<br />\nbbb</p>\n", "example": 196, "start_line": 3261, "end_line": 3267, "section": "Paragraphs" }, { "markdown": " \n\naaa\n \n\n# aaa\n\n \n", "html": "<p>aaa</p>\n<h1>aaa</h1>\n", "example": 197, "start_line": 3278, "end_line": 3290, "section": "Blank lines" }, { "markdown": "> # Foo\n> bar\n> baz\n", "html": "<blockquote>\n<h1>Foo</h1>\n<p>bar\nbaz</p>\n</blockquote>\n", "example": 198, "start_line": 3344, "end_line": 3354, "section": "Block quotes" }, { "markdown": "># Foo\n>bar\n> baz\n", "html": "<blockquote>\n<h1>Foo</h1>\n<p>bar\nbaz</p>\n</blockquote>\n", "example": 199, "start_line": 3359, "end_line": 3369, "section": "Block quotes" }, { "markdown": " > # Foo\n > bar\n > baz\n", "html": "<blockquote>\n<h1>Foo</h1>\n<p>bar\nbaz</p>\n</blockquote>\n", "example": 200, "start_line": 3374, "end_line": 3384, "section": "Block quotes" }, { "markdown": " > # Foo\n > bar\n > baz\n", "html": "<pre><code>> # Foo\n> bar\n> baz\n</code></pre>\n", "example": 201, "start_line": 3389, "end_line": 3398, "section": "Block quotes" }, { "markdown": "> # Foo\n> bar\nbaz\n", "html": "<blockquote>\n<h1>Foo</h1>\n<p>bar\nbaz</p>\n</blockquote>\n", "example": 202, "start_line": 3404, "end_line": 3414, "section": "Block quotes" }, { "markdown": "> bar\nbaz\n> foo\n", "html": "<blockquote>\n<p>bar\nbaz\nfoo</p>\n</blockquote>\n", "example": 203, "start_line": 3420, "end_line": 3430, "section": "Block quotes" }, { "markdown": "> foo\n---\n", "html": "<blockquote>\n<p>foo</p>\n</blockquote>\n<hr />\n", "example": 204, "start_line": 3444, "end_line": 3452, "section": "Block quotes" }, { "markdown": "> - foo\n- bar\n", "html": "<blockquote>\n<ul>\n<li>foo</li>\n</ul>\n</blockquote>\n<ul>\n<li>bar</li>\n</ul>\n", "example": 205, "start_line": 3464, "end_line": 3476, "section": "Block quotes" }, { "markdown": "> foo\n bar\n", "html": "<blockquote>\n<pre><code>foo\n</code></pre>\n</blockquote>\n<pre><code>bar\n</code></pre>\n", "example": 206, "start_line": 3482, "end_line": 3492, "section": "Block quotes" }, { "markdown": "> ```\nfoo\n```\n", "html": "<blockquote>\n<pre><code></code></pre>\n</blockquote>\n<p>foo</p>\n<pre><code></code></pre>\n", "example": 207, "start_line": 3495, "end_line": 3505, "section": "Block quotes" }, { "markdown": "> foo\n - bar\n", "html": "<blockquote>\n<p>foo\n- bar</p>\n</blockquote>\n", "example": 208, "start_line": 3511, "end_line": 3519, "section": "Block quotes" }, { "markdown": ">\n", "html": "<blockquote>\n</blockquote>\n", "example": 209, "start_line": 3535, "end_line": 3540, "section": "Block quotes" }, { "markdown": ">\n> \n> \n", "html": "<blockquote>\n</blockquote>\n", "example": 210, "start_line": 3543, "end_line": 3550, "section": "Block quotes" }, { "markdown": ">\n> foo\n> \n", "html": "<blockquote>\n<p>foo</p>\n</blockquote>\n", "example": 211, "start_line": 3555, "end_line": 3563, "section": "Block quotes" }, { "markdown": "> foo\n\n> bar\n", "html": "<blockquote>\n<p>foo</p>\n</blockquote>\n<blockquote>\n<p>bar</p>\n</blockquote>\n", "example": 212, "start_line": 3568, "end_line": 3579, "section": "Block quotes" }, { "markdown": "> foo\n> bar\n", "html": "<blockquote>\n<p>foo\nbar</p>\n</blockquote>\n", "example": 213, "start_line": 3590, "end_line": 3598, "section": "Block quotes" }, { "markdown": "> foo\n>\n> bar\n", "html": "<blockquote>\n<p>foo</p>\n<p>bar</p>\n</blockquote>\n", "example": 214, "start_line": 3603, "end_line": 3612, "section": "Block quotes" }, { "markdown": "foo\n> bar\n", "html": "<p>foo</p>\n<blockquote>\n<p>bar</p>\n</blockquote>\n", "example": 215, "start_line": 3617, "end_line": 3625, "section": "Block quotes" }, { "markdown": "> aaa\n***\n> bbb\n", "html": "<blockquote>\n<p>aaa</p>\n</blockquote>\n<hr />\n<blockquote>\n<p>bbb</p>\n</blockquote>\n", "example": 216, "start_line": 3631, "end_line": 3643, "section": "Block quotes" }, { "markdown": "> bar\nbaz\n", "html": "<blockquote>\n<p>bar\nbaz</p>\n</blockquote>\n", "example": 217, "start_line": 3649, "end_line": 3657, "section": "Block quotes" }, { "markdown": "> bar\n\nbaz\n", "html": "<blockquote>\n<p>bar</p>\n</blockquote>\n<p>baz</p>\n", "example": 218, "start_line": 3660, "end_line": 3669, "section": "Block quotes" }, { "markdown": "> bar\n>\nbaz\n", "html": "<blockquote>\n<p>bar</p>\n</blockquote>\n<p>baz</p>\n", "example": 219, "start_line": 3672, "end_line": 3681, "section": "Block quotes" }, { "markdown": "> > > foo\nbar\n", "html": "<blockquote>\n<blockquote>\n<blockquote>\n<p>foo\nbar</p>\n</blockquote>\n</blockquote>\n</blockquote>\n", "example": 220, "start_line": 3688, "end_line": 3700, "section": "Block quotes" }, { "markdown": ">>> foo\n> bar\n>>baz\n", "html": "<blockquote>\n<blockquote>\n<blockquote>\n<p>foo\nbar\nbaz</p>\n</blockquote>\n</blockquote>\n</blockquote>\n", "example": 221, "start_line": 3703, "end_line": 3717, "section": "Block quotes" }, { "markdown": "> code\n\n> not code\n", "html": "<blockquote>\n<pre><code>code\n</code></pre>\n</blockquote>\n<blockquote>\n<p>not code</p>\n</blockquote>\n", "example": 222, "start_line": 3725, "end_line": 3737, "section": "Block quotes" }, { "markdown": "A paragraph\nwith two lines.\n\n indented code\n\n> A block quote.\n", "html": "<p>A paragraph\nwith two lines.</p>\n<pre><code>indented code\n</code></pre>\n<blockquote>\n<p>A block quote.</p>\n</blockquote>\n", "example": 223, "start_line": 3779, "end_line": 3794, "section": "List items" }, { "markdown": "1. A paragraph\n with two lines.\n\n indented code\n\n > A block quote.\n", "html": "<ol>\n<li>\n<p>A paragraph\nwith two lines.</p>\n<pre><code>indented code\n</code></pre>\n<blockquote>\n<p>A block quote.</p>\n</blockquote>\n</li>\n</ol>\n", "example": 224, "start_line": 3801, "end_line": 3820, "section": "List items" }, { "markdown": "- one\n\n two\n", "html": "<ul>\n<li>one</li>\n</ul>\n<p>two</p>\n", "example": 225, "start_line": 3834, "end_line": 3843, "section": "List items" }, { "markdown": "- one\n\n two\n", "html": "<ul>\n<li>\n<p>one</p>\n<p>two</p>\n</li>\n</ul>\n", "example": 226, "start_line": 3846, "end_line": 3857, "section": "List items" }, { "markdown": " - one\n\n two\n", "html": "<ul>\n<li>one</li>\n</ul>\n<pre><code> two\n</code></pre>\n", "example": 227, "start_line": 3860, "end_line": 3870, "section": "List items" }, { "markdown": " - one\n\n two\n", "html": "<ul>\n<li>\n<p>one</p>\n<p>two</p>\n</li>\n</ul>\n", "example": 228, "start_line": 3873, "end_line": 3884, "section": "List items" }, { "markdown": " > > 1. one\n>>\n>> two\n", "html": "<blockquote>\n<blockquote>\n<ol>\n<li>\n<p>one</p>\n<p>two</p>\n</li>\n</ol>\n</blockquote>\n</blockquote>\n", "example": 229, "start_line": 3895, "end_line": 3910, "section": "List items" }, { "markdown": ">>- one\n>>\n > > two\n", "html": "<blockquote>\n<blockquote>\n<ul>\n<li>one</li>\n</ul>\n<p>two</p>\n</blockquote>\n</blockquote>\n", "example": 230, "start_line": 3922, "end_line": 3935, "section": "List items" }, { "markdown": "-one\n\n2.two\n", "html": "<p>-one</p>\n<p>2.two</p>\n", "example": 231, "start_line": 3941, "end_line": 3948, "section": "List items" }, { "markdown": "- foo\n\n\n bar\n", "html": "<ul>\n<li>\n<p>foo</p>\n<p>bar</p>\n</li>\n</ul>\n", "example": 232, "start_line": 3954, "end_line": 3966, "section": "List items" }, { "markdown": "1. foo\n\n ```\n bar\n ```\n\n baz\n\n > bam\n", "html": "<ol>\n<li>\n<p>foo</p>\n<pre><code>bar\n</code></pre>\n<p>baz</p>\n<blockquote>\n<p>bam</p>\n</blockquote>\n</li>\n</ol>\n", "example": 233, "start_line": 3971, "end_line": 3993, "section": "List items" }, { "markdown": "- Foo\n\n bar\n\n\n baz\n", "html": "<ul>\n<li>\n<p>Foo</p>\n<pre><code>bar\n\n\nbaz\n</code></pre>\n</li>\n</ul>\n", "example": 234, "start_line": 3999, "end_line": 4017, "section": "List items" }, { "markdown": "123456789. ok\n", "html": "<ol start=\"123456789\">\n<li>ok</li>\n</ol>\n", "example": 235, "start_line": 4021, "end_line": 4027, "section": "List items" }, { "markdown": "1234567890. not ok\n", "html": "<p>1234567890. not ok</p>\n", "example": 236, "start_line": 4030, "end_line": 4034, "section": "List items" }, { "markdown": "0. ok\n", "html": "<ol start=\"0\">\n<li>ok</li>\n</ol>\n", "example": 237, "start_line": 4039, "end_line": 4045, "section": "List items" }, { "markdown": "003. ok\n", "html": "<ol start=\"3\">\n<li>ok</li>\n</ol>\n", "example": 238, "start_line": 4048, "end_line": 4054, "section": "List items" }, { "markdown": "-1. not ok\n", "html": "<p>-1. not ok</p>\n", "example": 239, "start_line": 4059, "end_line": 4063, "section": "List items" }, { "markdown": "- foo\n\n bar\n", "html": "<ul>\n<li>\n<p>foo</p>\n<pre><code>bar\n</code></pre>\n</li>\n</ul>\n", "example": 240, "start_line": 4082, "end_line": 4094, "section": "List items" }, { "markdown": " 10. foo\n\n bar\n", "html": "<ol start=\"10\">\n<li>\n<p>foo</p>\n<pre><code>bar\n</code></pre>\n</li>\n</ol>\n", "example": 241, "start_line": 4099, "end_line": 4111, "section": "List items" }, { "markdown": " indented code\n\nparagraph\n\n more code\n", "html": "<pre><code>indented code\n</code></pre>\n<p>paragraph</p>\n<pre><code>more code\n</code></pre>\n", "example": 242, "start_line": 4118, "end_line": 4130, "section": "List items" }, { "markdown": "1. indented code\n\n paragraph\n\n more code\n", "html": "<ol>\n<li>\n<pre><code>indented code\n</code></pre>\n<p>paragraph</p>\n<pre><code>more code\n</code></pre>\n</li>\n</ol>\n", "example": 243, "start_line": 4133, "end_line": 4149, "section": "List items" }, { "markdown": "1. indented code\n\n paragraph\n\n more code\n", "html": "<ol>\n<li>\n<pre><code> indented code\n</code></pre>\n<p>paragraph</p>\n<pre><code>more code\n</code></pre>\n</li>\n</ol>\n", "example": 244, "start_line": 4155, "end_line": 4171, "section": "List items" }, { "markdown": " foo\n\nbar\n", "html": "<p>foo</p>\n<p>bar</p>\n", "example": 245, "start_line": 4182, "end_line": 4189, "section": "List items" }, { "markdown": "- foo\n\n bar\n", "html": "<ul>\n<li>foo</li>\n</ul>\n<p>bar</p>\n", "example": 246, "start_line": 4192, "end_line": 4201, "section": "List items" }, { "markdown": "- foo\n\n bar\n", "html": "<ul>\n<li>\n<p>foo</p>\n<p>bar</p>\n</li>\n</ul>\n", "example": 247, "start_line": 4209, "end_line": 4220, "section": "List items" }, { "markdown": "-\n foo\n-\n ```\n bar\n ```\n-\n baz\n", "html": "<ul>\n<li>foo</li>\n<li>\n<pre><code>bar\n</code></pre>\n</li>\n<li>\n<pre><code>baz\n</code></pre>\n</li>\n</ul>\n", "example": 248, "start_line": 4237, "end_line": 4258, "section": "List items" }, { "markdown": "- \n foo\n", "html": "<ul>\n<li>foo</li>\n</ul>\n", "example": 249, "start_line": 4263, "end_line": 4270, "section": "List items" }, { "markdown": "-\n\n foo\n", "html": "<ul>\n<li></li>\n</ul>\n<p>foo</p>\n", "example": 250, "start_line": 4277, "end_line": 4286, "section": "List items" }, { "markdown": "- foo\n-\n- bar\n", "html": "<ul>\n<li>foo</li>\n<li></li>\n<li>bar</li>\n</ul>\n", "example": 251, "start_line": 4291, "end_line": 4301, "section": "List items" }, { "markdown": "- foo\n- \n- bar\n", "html": "<ul>\n<li>foo</li>\n<li></li>\n<li>bar</li>\n</ul>\n", "example": 252, "start_line": 4306, "end_line": 4316, "section": "List items" }, { "markdown": "1. foo\n2.\n3. bar\n", "html": "<ol>\n<li>foo</li>\n<li></li>\n<li>bar</li>\n</ol>\n", "example": 253, "start_line": 4321, "end_line": 4331, "section": "List items" }, { "markdown": "*\n", "html": "<ul>\n<li></li>\n</ul>\n", "example": 254, "start_line": 4336, "end_line": 4342, "section": "List items" }, { "markdown": "foo\n*\n\nfoo\n1.\n", "html": "<p>foo\n*</p>\n<p>foo\n1.</p>\n", "example": 255, "start_line": 4346, "end_line": 4357, "section": "List items" }, { "markdown": " 1. A paragraph\n with two lines.\n\n indented code\n\n > A block quote.\n", "html": "<ol>\n<li>\n<p>A paragraph\nwith two lines.</p>\n<pre><code>indented code\n</code></pre>\n<blockquote>\n<p>A block quote.</p>\n</blockquote>\n</li>\n</ol>\n", "example": 256, "start_line": 4368, "end_line": 4387, "section": "List items" }, { "markdown": " 1. A paragraph\n with two lines.\n\n indented code\n\n > A block quote.\n", "html": "<ol>\n<li>\n<p>A paragraph\nwith two lines.</p>\n<pre><code>indented code\n</code></pre>\n<blockquote>\n<p>A block quote.</p>\n</blockquote>\n</li>\n</ol>\n", "example": 257, "start_line": 4392, "end_line": 4411, "section": "List items" }, { "markdown": " 1. A paragraph\n with two lines.\n\n indented code\n\n > A block quote.\n", "html": "<ol>\n<li>\n<p>A paragraph\nwith two lines.</p>\n<pre><code>indented code\n</code></pre>\n<blockquote>\n<p>A block quote.</p>\n</blockquote>\n</li>\n</ol>\n", "example": 258, "start_line": 4416, "end_line": 4435, "section": "List items" }, { "markdown": " 1. A paragraph\n with two lines.\n\n indented code\n\n > A block quote.\n", "html": "<pre><code>1. A paragraph\n with two lines.\n\n indented code\n\n > A block quote.\n</code></pre>\n", "example": 259, "start_line": 4440, "end_line": 4455, "section": "List items" }, { "markdown": " 1. A paragraph\nwith two lines.\n\n indented code\n\n > A block quote.\n", "html": "<ol>\n<li>\n<p>A paragraph\nwith two lines.</p>\n<pre><code>indented code\n</code></pre>\n<blockquote>\n<p>A block quote.</p>\n</blockquote>\n</li>\n</ol>\n", "example": 260, "start_line": 4470, "end_line": 4489, "section": "List items" }, { "markdown": " 1. A paragraph\n with two lines.\n", "html": "<ol>\n<li>A paragraph\nwith two lines.</li>\n</ol>\n", "example": 261, "start_line": 4494, "end_line": 4502, "section": "List items" }, { "markdown": "> 1. > Blockquote\ncontinued here.\n", "html": "<blockquote>\n<ol>\n<li>\n<blockquote>\n<p>Blockquote\ncontinued here.</p>\n</blockquote>\n</li>\n</ol>\n</blockquote>\n", "example": 262, "start_line": 4507, "end_line": 4521, "section": "List items" }, { "markdown": "> 1. > Blockquote\n> continued here.\n", "html": "<blockquote>\n<ol>\n<li>\n<blockquote>\n<p>Blockquote\ncontinued here.</p>\n</blockquote>\n</li>\n</ol>\n</blockquote>\n", "example": 263, "start_line": 4524, "end_line": 4538, "section": "List items" }, { "markdown": "- foo\n - bar\n - baz\n - boo\n", "html": "<ul>\n<li>foo\n<ul>\n<li>bar\n<ul>\n<li>baz\n<ul>\n<li>boo</li>\n</ul>\n</li>\n</ul>\n</li>\n</ul>\n</li>\n</ul>\n", "example": 264, "start_line": 4552, "end_line": 4573, "section": "List items" }, { "markdown": "- foo\n - bar\n - baz\n - boo\n", "html": "<ul>\n<li>foo</li>\n<li>bar</li>\n<li>baz</li>\n<li>boo</li>\n</ul>\n", "example": 265, "start_line": 4578, "end_line": 4590, "section": "List items" }, { "markdown": "10) foo\n - bar\n", "html": "<ol start=\"10\">\n<li>foo\n<ul>\n<li>bar</li>\n</ul>\n</li>\n</ol>\n", "example": 266, "start_line": 4595, "end_line": 4606, "section": "List items" }, { "markdown": "10) foo\n - bar\n", "html": "<ol start=\"10\">\n<li>foo</li>\n</ol>\n<ul>\n<li>bar</li>\n</ul>\n", "example": 267, "start_line": 4611, "end_line": 4621, "section": "List items" }, { "markdown": "- - foo\n", "html": "<ul>\n<li>\n<ul>\n<li>foo</li>\n</ul>\n</li>\n</ul>\n", "example": 268, "start_line": 4626, "end_line": 4636, "section": "List items" }, { "markdown": "1. - 2. foo\n", "html": "<ol>\n<li>\n<ul>\n<li>\n<ol start=\"2\">\n<li>foo</li>\n</ol>\n</li>\n</ul>\n</li>\n</ol>\n", "example": 269, "start_line": 4639, "end_line": 4653, "section": "List items" }, { "markdown": "- # Foo\n- Bar\n ---\n baz\n", "html": "<ul>\n<li>\n<h1>Foo</h1>\n</li>\n<li>\n<h2>Bar</h2>\nbaz</li>\n</ul>\n", "example": 270, "start_line": 4658, "end_line": 4672, "section": "List items" }, { "markdown": "- foo\n- bar\n+ baz\n", "html": "<ul>\n<li>foo</li>\n<li>bar</li>\n</ul>\n<ul>\n<li>baz</li>\n</ul>\n", "example": 271, "start_line": 4894, "end_line": 4906, "section": "Lists" }, { "markdown": "1. foo\n2. bar\n3) baz\n", "html": "<ol>\n<li>foo</li>\n<li>bar</li>\n</ol>\n<ol start=\"3\">\n<li>baz</li>\n</ol>\n", "example": 272, "start_line": 4909, "end_line": 4921, "section": "Lists" }, { "markdown": "Foo\n- bar\n- baz\n", "html": "<p>Foo</p>\n<ul>\n<li>bar</li>\n<li>baz</li>\n</ul>\n", "example": 273, "start_line": 4928, "end_line": 4938, "section": "Lists" }, { "markdown": "The number of windows in my house is\n14. The number of doors is 6.\n", "html": "<p>The number of windows in my house is\n14. The number of doors is 6.</p>\n", "example": 274, "start_line": 5005, "end_line": 5011, "section": "Lists" }, { "markdown": "The number of windows in my house is\n1. The number of doors is 6.\n", "html": "<p>The number of windows in my house is</p>\n<ol>\n<li>The number of doors is 6.</li>\n</ol>\n", "example": 275, "start_line": 5015, "end_line": 5023, "section": "Lists" }, { "markdown": "- foo\n\n- bar\n\n\n- baz\n", "html": "<ul>\n<li>\n<p>foo</p>\n</li>\n<li>\n<p>bar</p>\n</li>\n<li>\n<p>baz</p>\n</li>\n</ul>\n", "example": 276, "start_line": 5029, "end_line": 5048, "section": "Lists" }, { "markdown": "- foo\n - bar\n - baz\n\n\n bim\n", "html": "<ul>\n<li>foo\n<ul>\n<li>bar\n<ul>\n<li>\n<p>baz</p>\n<p>bim</p>\n</li>\n</ul>\n</li>\n</ul>\n</li>\n</ul>\n", "example": 277, "start_line": 5050, "end_line": 5072, "section": "Lists" }, { "markdown": "- foo\n- bar\n\n<!-- -->\n\n- baz\n- bim\n", "html": "<ul>\n<li>foo</li>\n<li>bar</li>\n</ul>\n<!-- -->\n<ul>\n<li>baz</li>\n<li>bim</li>\n</ul>\n", "example": 278, "start_line": 5080, "end_line": 5098, "section": "Lists" }, { "markdown": "- foo\n\n notcode\n\n- foo\n\n<!-- -->\n\n code\n", "html": "<ul>\n<li>\n<p>foo</p>\n<p>notcode</p>\n</li>\n<li>\n<p>foo</p>\n</li>\n</ul>\n<!-- -->\n<pre><code>code\n</code></pre>\n", "example": 279, "start_line": 5101, "end_line": 5124, "section": "Lists" }, { "markdown": "- a\n - b\n - c\n - d\n - e\n - f\n- g\n", "html": "<ul>\n<li>a</li>\n<li>b</li>\n<li>c</li>\n<li>d</li>\n<li>e</li>\n<li>f</li>\n<li>g</li>\n</ul>\n", "example": 280, "start_line": 5132, "end_line": 5150, "section": "Lists" }, { "markdown": "1. a\n\n 2. b\n\n 3. c\n", "html": "<ol>\n<li>\n<p>a</p>\n</li>\n<li>\n<p>b</p>\n</li>\n<li>\n<p>c</p>\n</li>\n</ol>\n", "example": 281, "start_line": 5153, "end_line": 5171, "section": "Lists" }, { "markdown": "- a\n - b\n - c\n - d\n - e\n", "html": "<ul>\n<li>a</li>\n<li>b</li>\n<li>c</li>\n<li>d\n- e</li>\n</ul>\n", "example": 282, "start_line": 5177, "end_line": 5191, "section": "Lists" }, { "markdown": "1. a\n\n 2. b\n\n 3. c\n", "html": "<ol>\n<li>\n<p>a</p>\n</li>\n<li>\n<p>b</p>\n</li>\n</ol>\n<pre><code>3. c\n</code></pre>\n", "example": 283, "start_line": 5197, "end_line": 5214, "section": "Lists" }, { "markdown": "- a\n- b\n\n- c\n", "html": "<ul>\n<li>\n<p>a</p>\n</li>\n<li>\n<p>b</p>\n</li>\n<li>\n<p>c</p>\n</li>\n</ul>\n", "example": 284, "start_line": 5220, "end_line": 5237, "section": "Lists" }, { "markdown": "* a\n*\n\n* c\n", "html": "<ul>\n<li>\n<p>a</p>\n</li>\n<li></li>\n<li>\n<p>c</p>\n</li>\n</ul>\n", "example": 285, "start_line": 5242, "end_line": 5257, "section": "Lists" }, { "markdown": "- a\n- b\n\n c\n- d\n", "html": "<ul>\n<li>\n<p>a</p>\n</li>\n<li>\n<p>b</p>\n<p>c</p>\n</li>\n<li>\n<p>d</p>\n</li>\n</ul>\n", "example": 286, "start_line": 5264, "end_line": 5283, "section": "Lists" }, { "markdown": "- a\n- b\n\n [ref]: /url\n- d\n", "html": "<ul>\n<li>\n<p>a</p>\n</li>\n<li>\n<p>b</p>\n</li>\n<li>\n<p>d</p>\n</li>\n</ul>\n", "example": 287, "start_line": 5286, "end_line": 5304, "section": "Lists" }, { "markdown": "- a\n- ```\n b\n\n\n ```\n- c\n", "html": "<ul>\n<li>a</li>\n<li>\n<pre><code>b\n\n\n</code></pre>\n</li>\n<li>c</li>\n</ul>\n", "example": 288, "start_line": 5309, "end_line": 5328, "section": "Lists" }, { "markdown": "- a\n - b\n\n c\n- d\n", "html": "<ul>\n<li>a\n<ul>\n<li>\n<p>b</p>\n<p>c</p>\n</li>\n</ul>\n</li>\n<li>d</li>\n</ul>\n", "example": 289, "start_line": 5335, "end_line": 5353, "section": "Lists" }, { "markdown": "* a\n > b\n >\n* c\n", "html": "<ul>\n<li>a\n<blockquote>\n<p>b</p>\n</blockquote>\n</li>\n<li>c</li>\n</ul>\n", "example": 290, "start_line": 5359, "end_line": 5373, "section": "Lists" }, { "markdown": "- a\n > b\n ```\n c\n ```\n- d\n", "html": "<ul>\n<li>a\n<blockquote>\n<p>b</p>\n</blockquote>\n<pre><code>c\n</code></pre>\n</li>\n<li>d</li>\n</ul>\n", "example": 291, "start_line": 5379, "end_line": 5397, "section": "Lists" }, { "markdown": "- a\n", "html": "<ul>\n<li>a</li>\n</ul>\n", "example": 292, "start_line": 5402, "end_line": 5408, "section": "Lists" }, { "markdown": "- a\n - b\n", "html": "<ul>\n<li>a\n<ul>\n<li>b</li>\n</ul>\n</li>\n</ul>\n", "example": 293, "start_line": 5411, "end_line": 5422, "section": "Lists" }, { "markdown": "1. ```\n foo\n ```\n\n bar\n", "html": "<ol>\n<li>\n<pre><code>foo\n</code></pre>\n<p>bar</p>\n</li>\n</ol>\n", "example": 294, "start_line": 5428, "end_line": 5442, "section": "Lists" }, { "markdown": "* foo\n * bar\n\n baz\n", "html": "<ul>\n<li>\n<p>foo</p>\n<ul>\n<li>bar</li>\n</ul>\n<p>baz</p>\n</li>\n</ul>\n", "example": 295, "start_line": 5447, "end_line": 5462, "section": "Lists" }, { "markdown": "- a\n - b\n - c\n\n- d\n - e\n - f\n", "html": "<ul>\n<li>\n<p>a</p>\n<ul>\n<li>b</li>\n<li>c</li>\n</ul>\n</li>\n<li>\n<p>d</p>\n<ul>\n<li>e</li>\n<li>f</li>\n</ul>\n</li>\n</ul>\n", "example": 296, "start_line": 5465, "end_line": 5490, "section": "Lists" }, { "markdown": "`hi`lo`\n", "html": "<p><code>hi</code>lo`</p>\n", "example": 297, "start_line": 5499, "end_line": 5503, "section": "Inlines" }, { "markdown": "\\!\\\"\\#\\$\\%\\&\\'\\(\\)\\*\\+\\,\\-\\.\\/\\:\\;\\<\\=\\>\\?\\@\\[\\\\\\]\\^\\_\\`\\{\\|\\}\\~\n", "html": "<p>!"#$%&'()*+,-./:;<=>?@[\\]^_`{|}~</p>\n", "example": 298, "start_line": 5513, "end_line": 5517, "section": "Backslash escapes" }, { "markdown": "\\\t\\A\\a\\ \\3\\φ\\«\n", "html": "<p>\\\t\\A\\a\\ \\3\\φ\\«</p>\n", "example": 299, "start_line": 5523, "end_line": 5527, "section": "Backslash escapes" }, { "markdown": "\\*not emphasized*\n\\<br/> not a tag\n\\[not a link](/foo)\n\\`not code`\n1\\. not a list\n\\* not a list\n\\# not a heading\n\\[foo]: /url \"not a reference\"\n\\ö not a character entity\n", "html": "<p>*not emphasized*\n<br/> not a tag\n[not a link](/foo)\n`not code`\n1. not a list\n* not a list\n# not a heading\n[foo]: /url "not a reference"\n&ouml; not a character entity</p>\n", "example": 300, "start_line": 5533, "end_line": 5553, "section": "Backslash escapes" }, { "markdown": "\\\\*emphasis*\n", "html": "<p>\\<em>emphasis</em></p>\n", "example": 301, "start_line": 5558, "end_line": 5562, "section": "Backslash escapes" }, { "markdown": "foo\\\nbar\n", "html": "<p>foo<br />\nbar</p>\n", "example": 302, "start_line": 5567, "end_line": 5573, "section": "Backslash escapes" }, { "markdown": "`` \\[\\` ``\n", "html": "<p><code>\\[\\`</code></p>\n", "example": 303, "start_line": 5579, "end_line": 5583, "section": "Backslash escapes" }, { "markdown": " \\[\\]\n", "html": "<pre><code>\\[\\]\n</code></pre>\n", "example": 304, "start_line": 5586, "end_line": 5591, "section": "Backslash escapes" }, { "markdown": "~~~\n\\[\\]\n~~~\n", "html": "<pre><code>\\[\\]\n</code></pre>\n", "example": 305, "start_line": 5594, "end_line": 5601, "section": "Backslash escapes" }, { "markdown": "<http://example.com?find=\\*>\n", "html": "<p><a href=\"http://example.com?find=%5C*\">http://example.com?find=\\*</a></p>\n", "example": 306, "start_line": 5604, "end_line": 5608, "section": "Backslash escapes" }, { "markdown": "<a href=\"/bar\\/)\">\n", "html": "<a href=\"/bar\\/)\">\n", "example": 307, "start_line": 5611, "end_line": 5615, "section": "Backslash escapes" }, { "markdown": "[foo](/bar\\* \"ti\\*tle\")\n", "html": "<p><a href=\"/bar*\" title=\"ti*tle\">foo</a></p>\n", "example": 308, "start_line": 5621, "end_line": 5625, "section": "Backslash escapes" }, { "markdown": "[foo]\n\n[foo]: /bar\\* \"ti\\*tle\"\n", "html": "<p><a href=\"/bar*\" title=\"ti*tle\">foo</a></p>\n", "example": 309, "start_line": 5628, "end_line": 5634, "section": "Backslash escapes" }, { "markdown": "``` foo\\+bar\nfoo\n```\n", "html": "<pre><code class=\"language-foo+bar\">foo\n</code></pre>\n", "example": 310, "start_line": 5637, "end_line": 5644, "section": "Backslash escapes" }, { "markdown": " & © Æ Ď\n¾ ℋ ⅆ\n∲ ≧̸\n", "html": "<p> & © Æ Ď\n¾ ℋ ⅆ\n∲ ≧̸</p>\n", "example": 311, "start_line": 5674, "end_line": 5682, "section": "Entity and numeric character references" }, { "markdown": "# Ӓ Ϡ �\n", "html": "<p># Ӓ Ϡ �</p>\n", "example": 312, "start_line": 5693, "end_line": 5697, "section": "Entity and numeric character references" }, { "markdown": "" ആ ಫ\n", "html": "<p>" ആ ಫ</p>\n", "example": 313, "start_line": 5706, "end_line": 5710, "section": "Entity and numeric character references" }, { "markdown": "  &x; &#; &#x;\n�\n&#abcdef0;\n&ThisIsNotDefined; &hi?;\n", "html": "<p>&nbsp &x; &#; &#x;\n&#987654321;\n&#abcdef0;\n&ThisIsNotDefined; &hi?;</p>\n", "example": 314, "start_line": 5715, "end_line": 5725, "section": "Entity and numeric character references" }, { "markdown": "©\n", "html": "<p>&copy</p>\n", "example": 315, "start_line": 5732, "end_line": 5736, "section": "Entity and numeric character references" }, { "markdown": "&MadeUpEntity;\n", "html": "<p>&MadeUpEntity;</p>\n", "example": 316, "start_line": 5742, "end_line": 5746, "section": "Entity and numeric character references" }, { "markdown": "<a href=\"öö.html\">\n", "html": "<a href=\"öö.html\">\n", "example": 317, "start_line": 5753, "end_line": 5757, "section": "Entity and numeric character references" }, { "markdown": "[foo](/föö \"föö\")\n", "html": "<p><a href=\"/f%C3%B6%C3%B6\" title=\"föö\">foo</a></p>\n", "example": 318, "start_line": 5760, "end_line": 5764, "section": "Entity and numeric character references" }, { "markdown": "[foo]\n\n[foo]: /föö \"föö\"\n", "html": "<p><a href=\"/f%C3%B6%C3%B6\" title=\"föö\">foo</a></p>\n", "example": 319, "start_line": 5767, "end_line": 5773, "section": "Entity and numeric character references" }, { "markdown": "``` föö\nfoo\n```\n", "html": "<pre><code class=\"language-föö\">foo\n</code></pre>\n", "example": 320, "start_line": 5776, "end_line": 5783, "section": "Entity and numeric character references" }, { "markdown": "`föö`\n", "html": "<p><code>f&ouml;&ouml;</code></p>\n", "example": 321, "start_line": 5789, "end_line": 5793, "section": "Entity and numeric character references" }, { "markdown": " föfö\n", "html": "<pre><code>f&ouml;f&ouml;\n</code></pre>\n", "example": 322, "start_line": 5796, "end_line": 5801, "section": "Entity and numeric character references" }, { "markdown": "*foo*\n*foo*\n", "html": "<p>*foo*\n<em>foo</em></p>\n", "example": 323, "start_line": 5808, "end_line": 5814, "section": "Entity and numeric character references" }, { "markdown": "* foo\n\n* foo\n", "html": "<p>* foo</p>\n<ul>\n<li>foo</li>\n</ul>\n", "example": 324, "start_line": 5816, "end_line": 5825, "section": "Entity and numeric character references" }, { "markdown": "foo bar\n", "html": "<p>foo\n\nbar</p>\n", "example": 325, "start_line": 5827, "end_line": 5833, "section": "Entity and numeric character references" }, { "markdown": "	foo\n", "html": "<p>\tfoo</p>\n", "example": 326, "start_line": 5835, "end_line": 5839, "section": "Entity and numeric character references" }, { "markdown": "[a](url "tit")\n", "html": "<p>[a](url "tit")</p>\n", "example": 327, "start_line": 5842, "end_line": 5846, "section": "Entity and numeric character references" }, { "markdown": "`foo`\n", "html": "<p><code>foo</code></p>\n", "example": 328, "start_line": 5870, "end_line": 5874, "section": "Code spans" }, { "markdown": "`` foo ` bar ``\n", "html": "<p><code>foo ` bar</code></p>\n", "example": 329, "start_line": 5881, "end_line": 5885, "section": "Code spans" }, { "markdown": "` `` `\n", "html": "<p><code>``</code></p>\n", "example": 330, "start_line": 5891, "end_line": 5895, "section": "Code spans" }, { "markdown": "` `` `\n", "html": "<p><code> `` </code></p>\n", "example": 331, "start_line": 5899, "end_line": 5903, "section": "Code spans" }, { "markdown": "` a`\n", "html": "<p><code> a</code></p>\n", "example": 332, "start_line": 5908, "end_line": 5912, "section": "Code spans" }, { "markdown": "` b `\n", "html": "<p><code> b </code></p>\n", "example": 333, "start_line": 5917, "end_line": 5921, "section": "Code spans" }, { "markdown": "` `\n` `\n", "html": "<p><code> </code>\n<code> </code></p>\n", "example": 334, "start_line": 5925, "end_line": 5931, "section": "Code spans" }, { "markdown": "``\nfoo\nbar \nbaz\n``\n", "html": "<p><code>foo bar baz</code></p>\n", "example": 335, "start_line": 5936, "end_line": 5944, "section": "Code spans" }, { "markdown": "``\nfoo \n``\n", "html": "<p><code>foo </code></p>\n", "example": 336, "start_line": 5946, "end_line": 5952, "section": "Code spans" }, { "markdown": "`foo bar \nbaz`\n", "html": "<p><code>foo bar baz</code></p>\n", "example": 337, "start_line": 5957, "end_line": 5962, "section": "Code spans" }, { "markdown": "`foo\\`bar`\n", "html": "<p><code>foo\\</code>bar`</p>\n", "example": 338, "start_line": 5974, "end_line": 5978, "section": "Code spans" }, { "markdown": "``foo`bar``\n", "html": "<p><code>foo`bar</code></p>\n", "example": 339, "start_line": 5985, "end_line": 5989, "section": "Code spans" }, { "markdown": "` foo `` bar `\n", "html": "<p><code>foo `` bar</code></p>\n", "example": 340, "start_line": 5991, "end_line": 5995, "section": "Code spans" }, { "markdown": "*foo`*`\n", "html": "<p>*foo<code>*</code></p>\n", "example": 341, "start_line": 6003, "end_line": 6007, "section": "Code spans" }, { "markdown": "[not a `link](/foo`)\n", "html": "<p>[not a <code>link](/foo</code>)</p>\n", "example": 342, "start_line": 6012, "end_line": 6016, "section": "Code spans" }, { "markdown": "`<a href=\"`\">`\n", "html": "<p><code><a href="</code>">`</p>\n", "example": 343, "start_line": 6022, "end_line": 6026, "section": "Code spans" }, { "markdown": "<a href=\"`\">`\n", "html": "<p><a href=\"`\">`</p>\n", "example": 344, "start_line": 6031, "end_line": 6035, "section": "Code spans" }, { "markdown": "`<http://foo.bar.`baz>`\n", "html": "<p><code><http://foo.bar.</code>baz>`</p>\n", "example": 345, "start_line": 6040, "end_line": 6044, "section": "Code spans" }, { "markdown": "<http://foo.bar.`baz>`\n", "html": "<p><a href=\"http://foo.bar.%60baz\">http://foo.bar.`baz</a>`</p>\n", "example": 346, "start_line": 6049, "end_line": 6053, "section": "Code spans" }, { "markdown": "```foo``\n", "html": "<p>```foo``</p>\n", "example": 347, "start_line": 6059, "end_line": 6063, "section": "Code spans" }, { "markdown": "`foo\n", "html": "<p>`foo</p>\n", "example": 348, "start_line": 6066, "end_line": 6070, "section": "Code spans" }, { "markdown": "`foo``bar``\n", "html": "<p>`foo<code>bar</code></p>\n", "example": 349, "start_line": 6075, "end_line": 6079, "section": "Code spans" }, { "markdown": "*foo bar*\n", "html": "<p><em>foo bar</em></p>\n", "example": 350, "start_line": 6292, "end_line": 6296, "section": "Emphasis and strong emphasis" }, { "markdown": "a * foo bar*\n", "html": "<p>a * foo bar*</p>\n", "example": 351, "start_line": 6302, "end_line": 6306, "section": "Emphasis and strong emphasis" }, { "markdown": "a*\"foo\"*\n", "html": "<p>a*"foo"*</p>\n", "example": 352, "start_line": 6313, "end_line": 6317, "section": "Emphasis and strong emphasis" }, { "markdown": "* a *\n", "html": "<p>* a *</p>\n", "example": 353, "start_line": 6322, "end_line": 6326, "section": "Emphasis and strong emphasis" }, { "markdown": "foo*bar*\n", "html": "<p>foo<em>bar</em></p>\n", "example": 354, "start_line": 6331, "end_line": 6335, "section": "Emphasis and strong emphasis" }, { "markdown": "5*6*78\n", "html": "<p>5<em>6</em>78</p>\n", "example": 355, "start_line": 6338, "end_line": 6342, "section": "Emphasis and strong emphasis" }, { "markdown": "_foo bar_\n", "html": "<p><em>foo bar</em></p>\n", "example": 356, "start_line": 6347, "end_line": 6351, "section": "Emphasis and strong emphasis" }, { "markdown": "_ foo bar_\n", "html": "<p>_ foo bar_</p>\n", "example": 357, "start_line": 6357, "end_line": 6361, "section": "Emphasis and strong emphasis" }, { "markdown": "a_\"foo\"_\n", "html": "<p>a_"foo"_</p>\n", "example": 358, "start_line": 6367, "end_line": 6371, "section": "Emphasis and strong emphasis" }, { "markdown": "foo_bar_\n", "html": "<p>foo_bar_</p>\n", "example": 359, "start_line": 6376, "end_line": 6380, "section": "Emphasis and strong emphasis" }, { "markdown": "5_6_78\n", "html": "<p>5_6_78</p>\n", "example": 360, "start_line": 6383, "end_line": 6387, "section": "Emphasis and strong emphasis" }, { "markdown": "пристаням_стремятся_\n", "html": "<p>пристаням_стремятся_</p>\n", "example": 361, "start_line": 6390, "end_line": 6394, "section": "Emphasis and strong emphasis" }, { "markdown": "aa_\"bb\"_cc\n", "html": "<p>aa_"bb"_cc</p>\n", "example": 362, "start_line": 6400, "end_line": 6404, "section": "Emphasis and strong emphasis" }, { "markdown": "foo-_(bar)_\n", "html": "<p>foo-<em>(bar)</em></p>\n", "example": 363, "start_line": 6411, "end_line": 6415, "section": "Emphasis and strong emphasis" }, { "markdown": "_foo*\n", "html": "<p>_foo*</p>\n", "example": 364, "start_line": 6423, "end_line": 6427, "section": "Emphasis and strong emphasis" }, { "markdown": "*foo bar *\n", "html": "<p>*foo bar *</p>\n", "example": 365, "start_line": 6433, "end_line": 6437, "section": "Emphasis and strong emphasis" }, { "markdown": "*foo bar\n*\n", "html": "<p>*foo bar\n*</p>\n", "example": 366, "start_line": 6442, "end_line": 6448, "section": "Emphasis and strong emphasis" }, { "markdown": "*(*foo)\n", "html": "<p>*(*foo)</p>\n", "example": 367, "start_line": 6455, "end_line": 6459, "section": "Emphasis and strong emphasis" }, { "markdown": "*(*foo*)*\n", "html": "<p><em>(<em>foo</em>)</em></p>\n", "example": 368, "start_line": 6465, "end_line": 6469, "section": "Emphasis and strong emphasis" }, { "markdown": "*foo*bar\n", "html": "<p><em>foo</em>bar</p>\n", "example": 369, "start_line": 6474, "end_line": 6478, "section": "Emphasis and strong emphasis" }, { "markdown": "_foo bar _\n", "html": "<p>_foo bar _</p>\n", "example": 370, "start_line": 6487, "end_line": 6491, "section": "Emphasis and strong emphasis" }, { "markdown": "_(_foo)\n", "html": "<p>_(_foo)</p>\n", "example": 371, "start_line": 6497, "end_line": 6501, "section": "Emphasis and strong emphasis" }, { "markdown": "_(_foo_)_\n", "html": "<p><em>(<em>foo</em>)</em></p>\n", "example": 372, "start_line": 6506, "end_line": 6510, "section": "Emphasis and strong emphasis" }, { "markdown": "_foo_bar\n", "html": "<p>_foo_bar</p>\n", "example": 373, "start_line": 6515, "end_line": 6519, "section": "Emphasis and strong emphasis" }, { "markdown": "_пристаням_стремятся\n", "html": "<p>_пристаням_стремятся</p>\n", "example": 374, "start_line": 6522, "end_line": 6526, "section": "Emphasis and strong emphasis" }, { "markdown": "_foo_bar_baz_\n", "html": "<p><em>foo_bar_baz</em></p>\n", "example": 375, "start_line": 6529, "end_line": 6533, "section": "Emphasis and strong emphasis" }, { "markdown": "_(bar)_.\n", "html": "<p><em>(bar)</em>.</p>\n", "example": 376, "start_line": 6540, "end_line": 6544, "section": "Emphasis and strong emphasis" }, { "markdown": "**foo bar**\n", "html": "<p><strong>foo bar</strong></p>\n", "example": 377, "start_line": 6549, "end_line": 6553, "section": "Emphasis and strong emphasis" }, { "markdown": "** foo bar**\n", "html": "<p>** foo bar**</p>\n", "example": 378, "start_line": 6559, "end_line": 6563, "section": "Emphasis and strong emphasis" }, { "markdown": "a**\"foo\"**\n", "html": "<p>a**"foo"**</p>\n", "example": 379, "start_line": 6570, "end_line": 6574, "section": "Emphasis and strong emphasis" }, { "markdown": "foo**bar**\n", "html": "<p>foo<strong>bar</strong></p>\n", "example": 380, "start_line": 6579, "end_line": 6583, "section": "Emphasis and strong emphasis" }, { "markdown": "__foo bar__\n", "html": "<p><strong>foo bar</strong></p>\n", "example": 381, "start_line": 6588, "end_line": 6592, "section": "Emphasis and strong emphasis" }, { "markdown": "__ foo bar__\n", "html": "<p>__ foo bar__</p>\n", "example": 382, "start_line": 6598, "end_line": 6602, "section": "Emphasis and strong emphasis" }, { "markdown": "__\nfoo bar__\n", "html": "<p>__\nfoo bar__</p>\n", "example": 383, "start_line": 6606, "end_line": 6612, "section": "Emphasis and strong emphasis" }, { "markdown": "a__\"foo\"__\n", "html": "<p>a__"foo"__</p>\n", "example": 384, "start_line": 6618, "end_line": 6622, "section": "Emphasis and strong emphasis" }, { "markdown": "foo__bar__\n", "html": "<p>foo__bar__</p>\n", "example": 385, "start_line": 6627, "end_line": 6631, "section": "Emphasis and strong emphasis" }, { "markdown": "5__6__78\n", "html": "<p>5__6__78</p>\n", "example": 386, "start_line": 6634, "end_line": 6638, "section": "Emphasis and strong emphasis" }, { "markdown": "пристаням__стремятся__\n", "html": "<p>пристаням__стремятся__</p>\n", "example": 387, "start_line": 6641, "end_line": 6645, "section": "Emphasis and strong emphasis" }, { "markdown": "__foo, __bar__, baz__\n", "html": "<p><strong>foo, <strong>bar</strong>, baz</strong></p>\n", "example": 388, "start_line": 6648, "end_line": 6652, "section": "Emphasis and strong emphasis" }, { "markdown": "foo-__(bar)__\n", "html": "<p>foo-<strong>(bar)</strong></p>\n", "example": 389, "start_line": 6659, "end_line": 6663, "section": "Emphasis and strong emphasis" }, { "markdown": "**foo bar **\n", "html": "<p>**foo bar **</p>\n", "example": 390, "start_line": 6672, "end_line": 6676, "section": "Emphasis and strong emphasis" }, { "markdown": "**(**foo)\n", "html": "<p>**(**foo)</p>\n", "example": 391, "start_line": 6685, "end_line": 6689, "section": "Emphasis and strong emphasis" }, { "markdown": "*(**foo**)*\n", "html": "<p><em>(<strong>foo</strong>)</em></p>\n", "example": 392, "start_line": 6695, "end_line": 6699, "section": "Emphasis and strong emphasis" }, { "markdown": "**Gomphocarpus (*Gomphocarpus physocarpus*, syn.\n*Asclepias physocarpa*)**\n", "html": "<p><strong>Gomphocarpus (<em>Gomphocarpus physocarpus</em>, syn.\n<em>Asclepias physocarpa</em>)</strong></p>\n", "example": 393, "start_line": 6702, "end_line": 6708, "section": "Emphasis and strong emphasis" }, { "markdown": "**foo \"*bar*\" foo**\n", "html": "<p><strong>foo "<em>bar</em>" foo</strong></p>\n", "example": 394, "start_line": 6711, "end_line": 6715, "section": "Emphasis and strong emphasis" }, { "markdown": "**foo**bar\n", "html": "<p><strong>foo</strong>bar</p>\n", "example": 395, "start_line": 6720, "end_line": 6724, "section": "Emphasis and strong emphasis" }, { "markdown": "__foo bar __\n", "html": "<p>__foo bar __</p>\n", "example": 396, "start_line": 6732, "end_line": 6736, "section": "Emphasis and strong emphasis" }, { "markdown": "__(__foo)\n", "html": "<p>__(__foo)</p>\n", "example": 397, "start_line": 6742, "end_line": 6746, "section": "Emphasis and strong emphasis" }, { "markdown": "_(__foo__)_\n", "html": "<p><em>(<strong>foo</strong>)</em></p>\n", "example": 398, "start_line": 6752, "end_line": 6756, "section": "Emphasis and strong emphasis" }, { "markdown": "__foo__bar\n", "html": "<p>__foo__bar</p>\n", "example": 399, "start_line": 6761, "end_line": 6765, "section": "Emphasis and strong emphasis" }, { "markdown": "__пристаням__стремятся\n", "html": "<p>__пристаням__стремятся</p>\n", "example": 400, "start_line": 6768, "end_line": 6772, "section": "Emphasis and strong emphasis" }, { "markdown": "__foo__bar__baz__\n", "html": "<p><strong>foo__bar__baz</strong></p>\n", "example": 401, "start_line": 6775, "end_line": 6779, "section": "Emphasis and strong emphasis" }, { "markdown": "__(bar)__.\n", "html": "<p><strong>(bar)</strong>.</p>\n", "example": 402, "start_line": 6786, "end_line": 6790, "section": "Emphasis and strong emphasis" }, { "markdown": "*foo [bar](/url)*\n", "html": "<p><em>foo <a href=\"/url\">bar</a></em></p>\n", "example": 403, "start_line": 6798, "end_line": 6802, "section": "Emphasis and strong emphasis" }, { "markdown": "*foo\nbar*\n", "html": "<p><em>foo\nbar</em></p>\n", "example": 404, "start_line": 6805, "end_line": 6811, "section": "Emphasis and strong emphasis" }, { "markdown": "_foo __bar__ baz_\n", "html": "<p><em>foo <strong>bar</strong> baz</em></p>\n", "example": 405, "start_line": 6817, "end_line": 6821, "section": "Emphasis and strong emphasis" }, { "markdown": "_foo _bar_ baz_\n", "html": "<p><em>foo <em>bar</em> baz</em></p>\n", "example": 406, "start_line": 6824, "end_line": 6828, "section": "Emphasis and strong emphasis" }, { "markdown": "__foo_ bar_\n", "html": "<p><em><em>foo</em> bar</em></p>\n", "example": 407, "start_line": 6831, "end_line": 6835, "section": "Emphasis and strong emphasis" }, { "markdown": "*foo *bar**\n", "html": "<p><em>foo <em>bar</em></em></p>\n", "example": 408, "start_line": 6838, "end_line": 6842, "section": "Emphasis and strong emphasis" }, { "markdown": "*foo **bar** baz*\n", "html": "<p><em>foo <strong>bar</strong> baz</em></p>\n", "example": 409, "start_line": 6845, "end_line": 6849, "section": "Emphasis and strong emphasis" }, { "markdown": "*foo**bar**baz*\n", "html": "<p><em>foo<strong>bar</strong>baz</em></p>\n", "example": 410, "start_line": 6851, "end_line": 6855, "section": "Emphasis and strong emphasis" }, { "markdown": "*foo**bar*\n", "html": "<p><em>foo**bar</em></p>\n", "example": 411, "start_line": 6875, "end_line": 6879, "section": "Emphasis and strong emphasis" }, { "markdown": "***foo** bar*\n", "html": "<p><em><strong>foo</strong> bar</em></p>\n", "example": 412, "start_line": 6888, "end_line": 6892, "section": "Emphasis and strong emphasis" }, { "markdown": "*foo **bar***\n", "html": "<p><em>foo <strong>bar</strong></em></p>\n", "example": 413, "start_line": 6895, "end_line": 6899, "section": "Emphasis and strong emphasis" }, { "markdown": "*foo**bar***\n", "html": "<p><em>foo<strong>bar</strong></em></p>\n", "example": 414, "start_line": 6902, "end_line": 6906, "section": "Emphasis and strong emphasis" }, { "markdown": "foo***bar***baz\n", "html": "<p>foo<em><strong>bar</strong></em>baz</p>\n", "example": 415, "start_line": 6913, "end_line": 6917, "section": "Emphasis and strong emphasis" }, { "markdown": "foo******bar*********baz\n", "html": "<p>foo<strong><strong><strong>bar</strong></strong></strong>***baz</p>\n", "example": 416, "start_line": 6919, "end_line": 6923, "section": "Emphasis and strong emphasis" }, { "markdown": "*foo **bar *baz* bim** bop*\n", "html": "<p><em>foo <strong>bar <em>baz</em> bim</strong> bop</em></p>\n", "example": 417, "start_line": 6928, "end_line": 6932, "section": "Emphasis and strong emphasis" }, { "markdown": "*foo [*bar*](/url)*\n", "html": "<p><em>foo <a href=\"/url\"><em>bar</em></a></em></p>\n", "example": 418, "start_line": 6935, "end_line": 6939, "section": "Emphasis and strong emphasis" }, { "markdown": "** is not an empty emphasis\n", "html": "<p>** is not an empty emphasis</p>\n", "example": 419, "start_line": 6944, "end_line": 6948, "section": "Emphasis and strong emphasis" }, { "markdown": "**** is not an empty strong emphasis\n", "html": "<p>**** is not an empty strong emphasis</p>\n", "example": 420, "start_line": 6951, "end_line": 6955, "section": "Emphasis and strong emphasis" }, { "markdown": "**foo [bar](/url)**\n", "html": "<p><strong>foo <a href=\"/url\">bar</a></strong></p>\n", "example": 421, "start_line": 6964, "end_line": 6968, "section": "Emphasis and strong emphasis" }, { "markdown": "**foo\nbar**\n", "html": "<p><strong>foo\nbar</strong></p>\n", "example": 422, "start_line": 6971, "end_line": 6977, "section": "Emphasis and strong emphasis" }, { "markdown": "__foo _bar_ baz__\n", "html": "<p><strong>foo <em>bar</em> baz</strong></p>\n", "example": 423, "start_line": 6983, "end_line": 6987, "section": "Emphasis and strong emphasis" }, { "markdown": "__foo __bar__ baz__\n", "html": "<p><strong>foo <strong>bar</strong> baz</strong></p>\n", "example": 424, "start_line": 6990, "end_line": 6994, "section": "Emphasis and strong emphasis" }, { "markdown": "____foo__ bar__\n", "html": "<p><strong><strong>foo</strong> bar</strong></p>\n", "example": 425, "start_line": 6997, "end_line": 7001, "section": "Emphasis and strong emphasis" }, { "markdown": "**foo **bar****\n", "html": "<p><strong>foo <strong>bar</strong></strong></p>\n", "example": 426, "start_line": 7004, "end_line": 7008, "section": "Emphasis and strong emphasis" }, { "markdown": "**foo *bar* baz**\n", "html": "<p><strong>foo <em>bar</em> baz</strong></p>\n", "example": 427, "start_line": 7011, "end_line": 7015, "section": "Emphasis and strong emphasis" }, { "markdown": "**foo*bar*baz**\n", "html": "<p><strong>foo<em>bar</em>baz</strong></p>\n", "example": 428, "start_line": 7018, "end_line": 7022, "section": "Emphasis and strong emphasis" }, { "markdown": "***foo* bar**\n", "html": "<p><strong><em>foo</em> bar</strong></p>\n", "example": 429, "start_line": 7025, "end_line": 7029, "section": "Emphasis and strong emphasis" }, { "markdown": "**foo *bar***\n", "html": "<p><strong>foo <em>bar</em></strong></p>\n", "example": 430, "start_line": 7032, "end_line": 7036, "section": "Emphasis and strong emphasis" }, { "markdown": "**foo *bar **baz**\nbim* bop**\n", "html": "<p><strong>foo <em>bar <strong>baz</strong>\nbim</em> bop</strong></p>\n", "example": 431, "start_line": 7041, "end_line": 7047, "section": "Emphasis and strong emphasis" }, { "markdown": "**foo [*bar*](/url)**\n", "html": "<p><strong>foo <a href=\"/url\"><em>bar</em></a></strong></p>\n", "example": 432, "start_line": 7050, "end_line": 7054, "section": "Emphasis and strong emphasis" }, { "markdown": "__ is not an empty emphasis\n", "html": "<p>__ is not an empty emphasis</p>\n", "example": 433, "start_line": 7059, "end_line": 7063, "section": "Emphasis and strong emphasis" }, { "markdown": "____ is not an empty strong emphasis\n", "html": "<p>____ is not an empty strong emphasis</p>\n", "example": 434, "start_line": 7066, "end_line": 7070, "section": "Emphasis and strong emphasis" }, { "markdown": "foo ***\n", "html": "<p>foo ***</p>\n", "example": 435, "start_line": 7076, "end_line": 7080, "section": "Emphasis and strong emphasis" }, { "markdown": "foo *\\**\n", "html": "<p>foo <em>*</em></p>\n", "example": 436, "start_line": 7083, "end_line": 7087, "section": "Emphasis and strong emphasis" }, { "markdown": "foo *_*\n", "html": "<p>foo <em>_</em></p>\n", "example": 437, "start_line": 7090, "end_line": 7094, "section": "Emphasis and strong emphasis" }, { "markdown": "foo *****\n", "html": "<p>foo *****</p>\n", "example": 438, "start_line": 7097, "end_line": 7101, "section": "Emphasis and strong emphasis" }, { "markdown": "foo **\\***\n", "html": "<p>foo <strong>*</strong></p>\n", "example": 439, "start_line": 7104, "end_line": 7108, "section": "Emphasis and strong emphasis" }, { "markdown": "foo **_**\n", "html": "<p>foo <strong>_</strong></p>\n", "example": 440, "start_line": 7111, "end_line": 7115, "section": "Emphasis and strong emphasis" }, { "markdown": "**foo*\n", "html": "<p>*<em>foo</em></p>\n", "example": 441, "start_line": 7122, "end_line": 7126, "section": "Emphasis and strong emphasis" }, { "markdown": "*foo**\n", "html": "<p><em>foo</em>*</p>\n", "example": 442, "start_line": 7129, "end_line": 7133, "section": "Emphasis and strong emphasis" }, { "markdown": "***foo**\n", "html": "<p>*<strong>foo</strong></p>\n", "example": 443, "start_line": 7136, "end_line": 7140, "section": "Emphasis and strong emphasis" }, { "markdown": "****foo*\n", "html": "<p>***<em>foo</em></p>\n", "example": 444, "start_line": 7143, "end_line": 7147, "section": "Emphasis and strong emphasis" }, { "markdown": "**foo***\n", "html": "<p><strong>foo</strong>*</p>\n", "example": 445, "start_line": 7150, "end_line": 7154, "section": "Emphasis and strong emphasis" }, { "markdown": "*foo****\n", "html": "<p><em>foo</em>***</p>\n", "example": 446, "start_line": 7157, "end_line": 7161, "section": "Emphasis and strong emphasis" }, { "markdown": "foo ___\n", "html": "<p>foo ___</p>\n", "example": 447, "start_line": 7167, "end_line": 7171, "section": "Emphasis and strong emphasis" }, { "markdown": "foo _\\__\n", "html": "<p>foo <em>_</em></p>\n", "example": 448, "start_line": 7174, "end_line": 7178, "section": "Emphasis and strong emphasis" }, { "markdown": "foo _*_\n", "html": "<p>foo <em>*</em></p>\n", "example": 449, "start_line": 7181, "end_line": 7185, "section": "Emphasis and strong emphasis" }, { "markdown": "foo _____\n", "html": "<p>foo _____</p>\n", "example": 450, "start_line": 7188, "end_line": 7192, "section": "Emphasis and strong emphasis" }, { "markdown": "foo __\\___\n", "html": "<p>foo <strong>_</strong></p>\n", "example": 451, "start_line": 7195, "end_line": 7199, "section": "Emphasis and strong emphasis" }, { "markdown": "foo __*__\n", "html": "<p>foo <strong>*</strong></p>\n", "example": 452, "start_line": 7202, "end_line": 7206, "section": "Emphasis and strong emphasis" }, { "markdown": "__foo_\n", "html": "<p>_<em>foo</em></p>\n", "example": 453, "start_line": 7209, "end_line": 7213, "section": "Emphasis and strong emphasis" }, { "markdown": "_foo__\n", "html": "<p><em>foo</em>_</p>\n", "example": 454, "start_line": 7220, "end_line": 7224, "section": "Emphasis and strong emphasis" }, { "markdown": "___foo__\n", "html": "<p>_<strong>foo</strong></p>\n", "example": 455, "start_line": 7227, "end_line": 7231, "section": "Emphasis and strong emphasis" }, { "markdown": "____foo_\n", "html": "<p>___<em>foo</em></p>\n", "example": 456, "start_line": 7234, "end_line": 7238, "section": "Emphasis and strong emphasis" }, { "markdown": "__foo___\n", "html": "<p><strong>foo</strong>_</p>\n", "example": 457, "start_line": 7241, "end_line": 7245, "section": "Emphasis and strong emphasis" }, { "markdown": "_foo____\n", "html": "<p><em>foo</em>___</p>\n", "example": 458, "start_line": 7248, "end_line": 7252, "section": "Emphasis and strong emphasis" }, { "markdown": "**foo**\n", "html": "<p><strong>foo</strong></p>\n", "example": 459, "start_line": 7258, "end_line": 7262, "section": "Emphasis and strong emphasis" }, { "markdown": "*_foo_*\n", "html": "<p><em><em>foo</em></em></p>\n", "example": 460, "start_line": 7265, "end_line": 7269, "section": "Emphasis and strong emphasis" }, { "markdown": "__foo__\n", "html": "<p><strong>foo</strong></p>\n", "example": 461, "start_line": 7272, "end_line": 7276, "section": "Emphasis and strong emphasis" }, { "markdown": "_*foo*_\n", "html": "<p><em><em>foo</em></em></p>\n", "example": 462, "start_line": 7279, "end_line": 7283, "section": "Emphasis and strong emphasis" }, { "markdown": "****foo****\n", "html": "<p><strong><strong>foo</strong></strong></p>\n", "example": 463, "start_line": 7289, "end_line": 7293, "section": "Emphasis and strong emphasis" }, { "markdown": "____foo____\n", "html": "<p><strong><strong>foo</strong></strong></p>\n", "example": 464, "start_line": 7296, "end_line": 7300, "section": "Emphasis and strong emphasis" }, { "markdown": "******foo******\n", "html": "<p><strong><strong><strong>foo</strong></strong></strong></p>\n", "example": 465, "start_line": 7307, "end_line": 7311, "section": "Emphasis and strong emphasis" }, { "markdown": "***foo***\n", "html": "<p><em><strong>foo</strong></em></p>\n", "example": 466, "start_line": 7316, "end_line": 7320, "section": "Emphasis and strong emphasis" }, { "markdown": "_____foo_____\n", "html": "<p><em><strong><strong>foo</strong></strong></em></p>\n", "example": 467, "start_line": 7323, "end_line": 7327, "section": "Emphasis and strong emphasis" }, { "markdown": "*foo _bar* baz_\n", "html": "<p><em>foo _bar</em> baz_</p>\n", "example": 468, "start_line": 7332, "end_line": 7336, "section": "Emphasis and strong emphasis" }, { "markdown": "*foo __bar *baz bim__ bam*\n", "html": "<p><em>foo <strong>bar *baz bim</strong> bam</em></p>\n", "example": 469, "start_line": 7339, "end_line": 7343, "section": "Emphasis and strong emphasis" }, { "markdown": "**foo **bar baz**\n", "html": "<p>**foo <strong>bar baz</strong></p>\n", "example": 470, "start_line": 7348, "end_line": 7352, "section": "Emphasis and strong emphasis" }, { "markdown": "*foo *bar baz*\n", "html": "<p>*foo <em>bar baz</em></p>\n", "example": 471, "start_line": 7355, "end_line": 7359, "section": "Emphasis and strong emphasis" }, { "markdown": "*[bar*](/url)\n", "html": "<p>*<a href=\"/url\">bar*</a></p>\n", "example": 472, "start_line": 7364, "end_line": 7368, "section": "Emphasis and strong emphasis" }, { "markdown": "_foo [bar_](/url)\n", "html": "<p>_foo <a href=\"/url\">bar_</a></p>\n", "example": 473, "start_line": 7371, "end_line": 7375, "section": "Emphasis and strong emphasis" }, { "markdown": "*<img src=\"foo\" title=\"*\"/>\n", "html": "<p>*<img src=\"foo\" title=\"*\"/></p>\n", "example": 474, "start_line": 7378, "end_line": 7382, "section": "Emphasis and strong emphasis" }, { "markdown": "**<a href=\"**\">\n", "html": "<p>**<a href=\"**\"></p>\n", "example": 475, "start_line": 7385, "end_line": 7389, "section": "Emphasis and strong emphasis" }, { "markdown": "__<a href=\"__\">\n", "html": "<p>__<a href=\"__\"></p>\n", "example": 476, "start_line": 7392, "end_line": 7396, "section": "Emphasis and strong emphasis" }, { "markdown": "*a `*`*\n", "html": "<p><em>a <code>*</code></em></p>\n", "example": 477, "start_line": 7399, "end_line": 7403, "section": "Emphasis and strong emphasis" }, { "markdown": "_a `_`_\n", "html": "<p><em>a <code>_</code></em></p>\n", "example": 478, "start_line": 7406, "end_line": 7410, "section": "Emphasis and strong emphasis" }, { "markdown": "**a<http://foo.bar/?q=**>\n", "html": "<p>**a<a href=\"http://foo.bar/?q=**\">http://foo.bar/?q=**</a></p>\n", "example": 479, "start_line": 7413, "end_line": 7417, "section": "Emphasis and strong emphasis" }, { "markdown": "__a<http://foo.bar/?q=__>\n", "html": "<p>__a<a href=\"http://foo.bar/?q=__\">http://foo.bar/?q=__</a></p>\n", "example": 480, "start_line": 7420, "end_line": 7424, "section": "Emphasis and strong emphasis" }, { "markdown": "[link](/uri \"title\")\n", "html": "<p><a href=\"/uri\" title=\"title\">link</a></p>\n", "example": 481, "start_line": 7503, "end_line": 7507, "section": "Links" }, { "markdown": "[link](/uri)\n", "html": "<p><a href=\"/uri\">link</a></p>\n", "example": 482, "start_line": 7512, "end_line": 7516, "section": "Links" }, { "markdown": "[link]()\n", "html": "<p><a href=\"\">link</a></p>\n", "example": 483, "start_line": 7521, "end_line": 7525, "section": "Links" }, { "markdown": "[link](<>)\n", "html": "<p><a href=\"\">link</a></p>\n", "example": 484, "start_line": 7528, "end_line": 7532, "section": "Links" }, { "markdown": "[link](/my uri)\n", "html": "<p>[link](/my uri)</p>\n", "example": 485, "start_line": 7537, "end_line": 7541, "section": "Links" }, { "markdown": "[link](</my uri>)\n", "html": "<p><a href=\"/my%20uri\">link</a></p>\n", "example": 486, "start_line": 7543, "end_line": 7547, "section": "Links" }, { "markdown": "[link](foo\nbar)\n", "html": "<p>[link](foo\nbar)</p>\n", "example": 487, "start_line": 7552, "end_line": 7558, "section": "Links" }, { "markdown": "[link](<foo\nbar>)\n", "html": "<p>[link](<foo\nbar>)</p>\n", "example": 488, "start_line": 7560, "end_line": 7566, "section": "Links" }, { "markdown": "[a](<b)c>)\n", "html": "<p><a href=\"b)c\">a</a></p>\n", "example": 489, "start_line": 7571, "end_line": 7575, "section": "Links" }, { "markdown": "[link](<foo\\>)\n", "html": "<p>[link](<foo>)</p>\n", "example": 490, "start_line": 7579, "end_line": 7583, "section": "Links" }, { "markdown": "[a](<b)c\n[a](<b)c>\n[a](<b>c)\n", "html": "<p>[a](<b)c\n[a](<b)c>\n[a](<b>c)</p>\n", "example": 491, "start_line": 7588, "end_line": 7596, "section": "Links" }, { "markdown": "[link](\\(foo\\))\n", "html": "<p><a href=\"(foo)\">link</a></p>\n", "example": 492, "start_line": 7600, "end_line": 7604, "section": "Links" }, { "markdown": "[link](foo(and(bar)))\n", "html": "<p><a href=\"foo(and(bar))\">link</a></p>\n", "example": 493, "start_line": 7609, "end_line": 7613, "section": "Links" }, { "markdown": "[link](foo\\(and\\(bar\\))\n", "html": "<p><a href=\"foo(and(bar)\">link</a></p>\n", "example": 494, "start_line": 7618, "end_line": 7622, "section": "Links" }, { "markdown": "[link](<foo(and(bar)>)\n", "html": "<p><a href=\"foo(and(bar)\">link</a></p>\n", "example": 495, "start_line": 7625, "end_line": 7629, "section": "Links" }, { "markdown": "[link](foo\\)\\:)\n", "html": "<p><a href=\"foo):\">link</a></p>\n", "example": 496, "start_line": 7635, "end_line": 7639, "section": "Links" }, { "markdown": "[link](#fragment)\n\n[link](http://example.com#fragment)\n\n[link](http://example.com?foo=3#frag)\n", "html": "<p><a href=\"#fragment\">link</a></p>\n<p><a href=\"http://example.com#fragment\">link</a></p>\n<p><a href=\"http://example.com?foo=3#frag\">link</a></p>\n", "example": 497, "start_line": 7644, "end_line": 7654, "section": "Links" }, { "markdown": "[link](foo\\bar)\n", "html": "<p><a href=\"foo%5Cbar\">link</a></p>\n", "example": 498, "start_line": 7660, "end_line": 7664, "section": "Links" }, { "markdown": "[link](foo%20bä)\n", "html": "<p><a href=\"foo%20b%C3%A4\">link</a></p>\n", "example": 499, "start_line": 7676, "end_line": 7680, "section": "Links" }, { "markdown": "[link](\"title\")\n", "html": "<p><a href=\"%22title%22\">link</a></p>\n", "example": 500, "start_line": 7687, "end_line": 7691, "section": "Links" }, { "markdown": "[link](/url \"title\")\n[link](/url 'title')\n[link](/url (title))\n", "html": "<p><a href=\"/url\" title=\"title\">link</a>\n<a href=\"/url\" title=\"title\">link</a>\n<a href=\"/url\" title=\"title\">link</a></p>\n", "example": 501, "start_line": 7696, "end_line": 7704, "section": "Links" }, { "markdown": "[link](/url \"title \\\""\")\n", "html": "<p><a href=\"/url\" title=\"title ""\">link</a></p>\n", "example": 502, "start_line": 7710, "end_line": 7714, "section": "Links" }, { "markdown": "[link](/url \"title\")\n", "html": "<p><a href=\"/url%C2%A0%22title%22\">link</a></p>\n", "example": 503, "start_line": 7720, "end_line": 7724, "section": "Links" }, { "markdown": "[link](/url \"title \"and\" title\")\n", "html": "<p>[link](/url "title "and" title")</p>\n", "example": 504, "start_line": 7729, "end_line": 7733, "section": "Links" }, { "markdown": "[link](/url 'title \"and\" title')\n", "html": "<p><a href=\"/url\" title=\"title "and" title\">link</a></p>\n", "example": 505, "start_line": 7738, "end_line": 7742, "section": "Links" }, { "markdown": "[link]( /uri\n \"title\" )\n", "html": "<p><a href=\"/uri\" title=\"title\">link</a></p>\n", "example": 506, "start_line": 7762, "end_line": 7767, "section": "Links" }, { "markdown": "[link] (/uri)\n", "html": "<p>[link] (/uri)</p>\n", "example": 507, "start_line": 7773, "end_line": 7777, "section": "Links" }, { "markdown": "[link [foo [bar]]](/uri)\n", "html": "<p><a href=\"/uri\">link [foo [bar]]</a></p>\n", "example": 508, "start_line": 7783, "end_line": 7787, "section": "Links" }, { "markdown": "[link] bar](/uri)\n", "html": "<p>[link] bar](/uri)</p>\n", "example": 509, "start_line": 7790, "end_line": 7794, "section": "Links" }, { "markdown": "[link [bar](/uri)\n", "html": "<p>[link <a href=\"/uri\">bar</a></p>\n", "example": 510, "start_line": 7797, "end_line": 7801, "section": "Links" }, { "markdown": "[link \\[bar](/uri)\n", "html": "<p><a href=\"/uri\">link [bar</a></p>\n", "example": 511, "start_line": 7804, "end_line": 7808, "section": "Links" }, { "markdown": "[link *foo **bar** `#`*](/uri)\n", "html": "<p><a href=\"/uri\">link <em>foo <strong>bar</strong> <code>#</code></em></a></p>\n", "example": 512, "start_line": 7813, "end_line": 7817, "section": "Links" }, { "markdown": "[![moon](moon.jpg)](/uri)\n", "html": "<p><a href=\"/uri\"><img src=\"moon.jpg\" alt=\"moon\" /></a></p>\n", "example": 513, "start_line": 7820, "end_line": 7824, "section": "Links" }, { "markdown": "[foo [bar](/uri)](/uri)\n", "html": "<p>[foo <a href=\"/uri\">bar</a>](/uri)</p>\n", "example": 514, "start_line": 7829, "end_line": 7833, "section": "Links" }, { "markdown": "[foo *[bar [baz](/uri)](/uri)*](/uri)\n", "html": "<p>[foo <em>[bar <a href=\"/uri\">baz</a>](/uri)</em>](/uri)</p>\n", "example": 515, "start_line": 7836, "end_line": 7840, "section": "Links" }, { "markdown": "![[[foo](uri1)](uri2)](uri3)\n", "html": "<p><img src=\"uri3\" alt=\"[foo](uri2)\" /></p>\n", "example": 516, "start_line": 7843, "end_line": 7847, "section": "Links" }, { "markdown": "*[foo*](/uri)\n", "html": "<p>*<a href=\"/uri\">foo*</a></p>\n", "example": 517, "start_line": 7853, "end_line": 7857, "section": "Links" }, { "markdown": "[foo *bar](baz*)\n", "html": "<p><a href=\"baz*\">foo *bar</a></p>\n", "example": 518, "start_line": 7860, "end_line": 7864, "section": "Links" }, { "markdown": "*foo [bar* baz]\n", "html": "<p><em>foo [bar</em> baz]</p>\n", "example": 519, "start_line": 7870, "end_line": 7874, "section": "Links" }, { "markdown": "[foo <bar attr=\"](baz)\">\n", "html": "<p>[foo <bar attr=\"](baz)\"></p>\n", "example": 520, "start_line": 7880, "end_line": 7884, "section": "Links" }, { "markdown": "[foo`](/uri)`\n", "html": "<p>[foo<code>](/uri)</code></p>\n", "example": 521, "start_line": 7887, "end_line": 7891, "section": "Links" }, { "markdown": "[foo<http://example.com/?search=](uri)>\n", "html": "<p>[foo<a href=\"http://example.com/?search=%5D(uri)\">http://example.com/?search=](uri)</a></p>\n", "example": 522, "start_line": 7894, "end_line": 7898, "section": "Links" }, { "markdown": "[foo][bar]\n\n[bar]: /url \"title\"\n", "html": "<p><a href=\"/url\" title=\"title\">foo</a></p>\n", "example": 523, "start_line": 7932, "end_line": 7938, "section": "Links" }, { "markdown": "[link [foo [bar]]][ref]\n\n[ref]: /uri\n", "html": "<p><a href=\"/uri\">link [foo [bar]]</a></p>\n", "example": 524, "start_line": 7947, "end_line": 7953, "section": "Links" }, { "markdown": "[link \\[bar][ref]\n\n[ref]: /uri\n", "html": "<p><a href=\"/uri\">link [bar</a></p>\n", "example": 525, "start_line": 7956, "end_line": 7962, "section": "Links" }, { "markdown": "[link *foo **bar** `#`*][ref]\n\n[ref]: /uri\n", "html": "<p><a href=\"/uri\">link <em>foo <strong>bar</strong> <code>#</code></em></a></p>\n", "example": 526, "start_line": 7967, "end_line": 7973, "section": "Links" }, { "markdown": "[![moon](moon.jpg)][ref]\n\n[ref]: /uri\n", "html": "<p><a href=\"/uri\"><img src=\"moon.jpg\" alt=\"moon\" /></a></p>\n", "example": 527, "start_line": 7976, "end_line": 7982, "section": "Links" }, { "markdown": "[foo [bar](/uri)][ref]\n\n[ref]: /uri\n", "html": "<p>[foo <a href=\"/uri\">bar</a>]<a href=\"/uri\">ref</a></p>\n", "example": 528, "start_line": 7987, "end_line": 7993, "section": "Links" }, { "markdown": "[foo *bar [baz][ref]*][ref]\n\n[ref]: /uri\n", "html": "<p>[foo <em>bar <a href=\"/uri\">baz</a></em>]<a href=\"/uri\">ref</a></p>\n", "example": 529, "start_line": 7996, "end_line": 8002, "section": "Links" }, { "markdown": "*[foo*][ref]\n\n[ref]: /uri\n", "html": "<p>*<a href=\"/uri\">foo*</a></p>\n", "example": 530, "start_line": 8011, "end_line": 8017, "section": "Links" }, { "markdown": "[foo *bar][ref]\n\n[ref]: /uri\n", "html": "<p><a href=\"/uri\">foo *bar</a></p>\n", "example": 531, "start_line": 8020, "end_line": 8026, "section": "Links" }, { "markdown": "[foo <bar attr=\"][ref]\">\n\n[ref]: /uri\n", "html": "<p>[foo <bar attr=\"][ref]\"></p>\n", "example": 532, "start_line": 8032, "end_line": 8038, "section": "Links" }, { "markdown": "[foo`][ref]`\n\n[ref]: /uri\n", "html": "<p>[foo<code>][ref]</code></p>\n", "example": 533, "start_line": 8041, "end_line": 8047, "section": "Links" }, { "markdown": "[foo<http://example.com/?search=][ref]>\n\n[ref]: /uri\n", "html": "<p>[foo<a href=\"http://example.com/?search=%5D%5Bref%5D\">http://example.com/?search=][ref]</a></p>\n", "example": 534, "start_line": 8050, "end_line": 8056, "section": "Links" }, { "markdown": "[foo][BaR]\n\n[bar]: /url \"title\"\n", "html": "<p><a href=\"/url\" title=\"title\">foo</a></p>\n", "example": 535, "start_line": 8061, "end_line": 8067, "section": "Links" }, { "markdown": "[Толпой][Толпой] is a Russian word.\n\n[ТОЛПОЙ]: /url\n", "html": "<p><a href=\"/url\">Толпой</a> is a Russian word.</p>\n", "example": 536, "start_line": 8072, "end_line": 8078, "section": "Links" }, { "markdown": "[Foo\n bar]: /url\n\n[Baz][Foo bar]\n", "html": "<p><a href=\"/url\">Baz</a></p>\n", "example": 537, "start_line": 8084, "end_line": 8091, "section": "Links" }, { "markdown": "[foo] [bar]\n\n[bar]: /url \"title\"\n", "html": "<p>[foo] <a href=\"/url\" title=\"title\">bar</a></p>\n", "example": 538, "start_line": 8097, "end_line": 8103, "section": "Links" }, { "markdown": "[foo]\n[bar]\n\n[bar]: /url \"title\"\n", "html": "<p>[foo]\n<a href=\"/url\" title=\"title\">bar</a></p>\n", "example": 539, "start_line": 8106, "end_line": 8114, "section": "Links" }, { "markdown": "[foo]: /url1\n\n[foo]: /url2\n\n[bar][foo]\n", "html": "<p><a href=\"/url1\">bar</a></p>\n", "example": 540, "start_line": 8147, "end_line": 8155, "section": "Links" }, { "markdown": "[bar][foo\\!]\n\n[foo!]: /url\n", "html": "<p>[bar][foo!]</p>\n", "example": 541, "start_line": 8162, "end_line": 8168, "section": "Links" }, { "markdown": "[foo][ref[]\n\n[ref[]: /uri\n", "html": "<p>[foo][ref[]</p>\n<p>[ref[]: /uri</p>\n", "example": 542, "start_line": 8174, "end_line": 8181, "section": "Links" }, { "markdown": "[foo][ref[bar]]\n\n[ref[bar]]: /uri\n", "html": "<p>[foo][ref[bar]]</p>\n<p>[ref[bar]]: /uri</p>\n", "example": 543, "start_line": 8184, "end_line": 8191, "section": "Links" }, { "markdown": "[[[foo]]]\n\n[[[foo]]]: /url\n", "html": "<p>[[[foo]]]</p>\n<p>[[[foo]]]: /url</p>\n", "example": 544, "start_line": 8194, "end_line": 8201, "section": "Links" }, { "markdown": "[foo][ref\\[]\n\n[ref\\[]: /uri\n", "html": "<p><a href=\"/uri\">foo</a></p>\n", "example": 545, "start_line": 8204, "end_line": 8210, "section": "Links" }, { "markdown": "[bar\\\\]: /uri\n\n[bar\\\\]\n", "html": "<p><a href=\"/uri\">bar\\</a></p>\n", "example": 546, "start_line": 8215, "end_line": 8221, "section": "Links" }, { "markdown": "[]\n\n[]: /uri\n", "html": "<p>[]</p>\n<p>[]: /uri</p>\n", "example": 547, "start_line": 8226, "end_line": 8233, "section": "Links" }, { "markdown": "[\n ]\n\n[\n ]: /uri\n", "html": "<p>[\n]</p>\n<p>[\n]: /uri</p>\n", "example": 548, "start_line": 8236, "end_line": 8247, "section": "Links" }, { "markdown": "[foo][]\n\n[foo]: /url \"title\"\n", "html": "<p><a href=\"/url\" title=\"title\">foo</a></p>\n", "example": 549, "start_line": 8259, "end_line": 8265, "section": "Links" }, { "markdown": "[*foo* bar][]\n\n[*foo* bar]: /url \"title\"\n", "html": "<p><a href=\"/url\" title=\"title\"><em>foo</em> bar</a></p>\n", "example": 550, "start_line": 8268, "end_line": 8274, "section": "Links" }, { "markdown": "[Foo][]\n\n[foo]: /url \"title\"\n", "html": "<p><a href=\"/url\" title=\"title\">Foo</a></p>\n", "example": 551, "start_line": 8279, "end_line": 8285, "section": "Links" }, { "markdown": "[foo] \n[]\n\n[foo]: /url \"title\"\n", "html": "<p><a href=\"/url\" title=\"title\">foo</a>\n[]</p>\n", "example": 552, "start_line": 8292, "end_line": 8300, "section": "Links" }, { "markdown": "[foo]\n\n[foo]: /url \"title\"\n", "html": "<p><a href=\"/url\" title=\"title\">foo</a></p>\n", "example": 553, "start_line": 8312, "end_line": 8318, "section": "Links" }, { "markdown": "[*foo* bar]\n\n[*foo* bar]: /url \"title\"\n", "html": "<p><a href=\"/url\" title=\"title\"><em>foo</em> bar</a></p>\n", "example": 554, "start_line": 8321, "end_line": 8327, "section": "Links" }, { "markdown": "[[*foo* bar]]\n\n[*foo* bar]: /url \"title\"\n", "html": "<p>[<a href=\"/url\" title=\"title\"><em>foo</em> bar</a>]</p>\n", "example": 555, "start_line": 8330, "end_line": 8336, "section": "Links" }, { "markdown": "[[bar [foo]\n\n[foo]: /url\n", "html": "<p>[[bar <a href=\"/url\">foo</a></p>\n", "example": 556, "start_line": 8339, "end_line": 8345, "section": "Links" }, { "markdown": "[Foo]\n\n[foo]: /url \"title\"\n", "html": "<p><a href=\"/url\" title=\"title\">Foo</a></p>\n", "example": 557, "start_line": 8350, "end_line": 8356, "section": "Links" }, { "markdown": "[foo] bar\n\n[foo]: /url\n", "html": "<p><a href=\"/url\">foo</a> bar</p>\n", "example": 558, "start_line": 8361, "end_line": 8367, "section": "Links" }, { "markdown": "\\[foo]\n\n[foo]: /url \"title\"\n", "html": "<p>[foo]</p>\n", "example": 559, "start_line": 8373, "end_line": 8379, "section": "Links" }, { "markdown": "[foo*]: /url\n\n*[foo*]\n", "html": "<p>*<a href=\"/url\">foo*</a></p>\n", "example": 560, "start_line": 8385, "end_line": 8391, "section": "Links" }, { "markdown": "[foo][bar]\n\n[foo]: /url1\n[bar]: /url2\n", "html": "<p><a href=\"/url2\">foo</a></p>\n", "example": 561, "start_line": 8397, "end_line": 8404, "section": "Links" }, { "markdown": "[foo][]\n\n[foo]: /url1\n", "html": "<p><a href=\"/url1\">foo</a></p>\n", "example": 562, "start_line": 8406, "end_line": 8412, "section": "Links" }, { "markdown": "[foo]()\n\n[foo]: /url1\n", "html": "<p><a href=\"\">foo</a></p>\n", "example": 563, "start_line": 8416, "end_line": 8422, "section": "Links" }, { "markdown": "[foo](not a link)\n\n[foo]: /url1\n", "html": "<p><a href=\"/url1\">foo</a>(not a link)</p>\n", "example": 564, "start_line": 8424, "end_line": 8430, "section": "Links" }, { "markdown": "[foo][bar][baz]\n\n[baz]: /url\n", "html": "<p>[foo]<a href=\"/url\">bar</a></p>\n", "example": 565, "start_line": 8435, "end_line": 8441, "section": "Links" }, { "markdown": "[foo][bar][baz]\n\n[baz]: /url1\n[bar]: /url2\n", "html": "<p><a href=\"/url2\">foo</a><a href=\"/url1\">baz</a></p>\n", "example": 566, "start_line": 8447, "end_line": 8454, "section": "Links" }, { "markdown": "[foo][bar][baz]\n\n[baz]: /url1\n[foo]: /url2\n", "html": "<p>[foo]<a href=\"/url1\">bar</a></p>\n", "example": 567, "start_line": 8460, "end_line": 8467, "section": "Links" }, { "markdown": "![foo](/url \"title\")\n", "html": "<p><img src=\"/url\" alt=\"foo\" title=\"title\" /></p>\n", "example": 568, "start_line": 8483, "end_line": 8487, "section": "Images" }, { "markdown": "![foo *bar*]\n\n[foo *bar*]: train.jpg \"train & tracks\"\n", "html": "<p><img src=\"train.jpg\" alt=\"foo bar\" title=\"train & tracks\" /></p>\n", "example": 569, "start_line": 8490, "end_line": 8496, "section": "Images" }, { "markdown": "![foo ![bar](/url)](/url2)\n", "html": "<p><img src=\"/url2\" alt=\"foo bar\" /></p>\n", "example": 570, "start_line": 8499, "end_line": 8503, "section": "Images" }, { "markdown": "![foo [bar](/url)](/url2)\n", "html": "<p><img src=\"/url2\" alt=\"foo bar\" /></p>\n", "example": 571, "start_line": 8506, "end_line": 8510, "section": "Images" }, { "markdown": "![foo *bar*][]\n\n[foo *bar*]: train.jpg \"train & tracks\"\n", "html": "<p><img src=\"train.jpg\" alt=\"foo bar\" title=\"train & tracks\" /></p>\n", "example": 572, "start_line": 8520, "end_line": 8526, "section": "Images" }, { "markdown": "![foo *bar*][foobar]\n\n[FOOBAR]: train.jpg \"train & tracks\"\n", "html": "<p><img src=\"train.jpg\" alt=\"foo bar\" title=\"train & tracks\" /></p>\n", "example": 573, "start_line": 8529, "end_line": 8535, "section": "Images" }, { "markdown": "![foo](train.jpg)\n", "html": "<p><img src=\"train.jpg\" alt=\"foo\" /></p>\n", "example": 574, "start_line": 8538, "end_line": 8542, "section": "Images" }, { "markdown": "My ![foo bar](/path/to/train.jpg \"title\" )\n", "html": "<p>My <img src=\"/path/to/train.jpg\" alt=\"foo bar\" title=\"title\" /></p>\n", "example": 575, "start_line": 8545, "end_line": 8549, "section": "Images" }, { "markdown": "![foo](<url>)\n", "html": "<p><img src=\"url\" alt=\"foo\" /></p>\n", "example": 576, "start_line": 8552, "end_line": 8556, "section": "Images" }, { "markdown": "![](/url)\n", "html": "<p><img src=\"/url\" alt=\"\" /></p>\n", "example": 577, "start_line": 8559, "end_line": 8563, "section": "Images" }, { "markdown": "![foo][bar]\n\n[bar]: /url\n", "html": "<p><img src=\"/url\" alt=\"foo\" /></p>\n", "example": 578, "start_line": 8568, "end_line": 8574, "section": "Images" }, { "markdown": "![foo][bar]\n\n[BAR]: /url\n", "html": "<p><img src=\"/url\" alt=\"foo\" /></p>\n", "example": 579, "start_line": 8577, "end_line": 8583, "section": "Images" }, { "markdown": "![foo][]\n\n[foo]: /url \"title\"\n", "html": "<p><img src=\"/url\" alt=\"foo\" title=\"title\" /></p>\n", "example": 580, "start_line": 8588, "end_line": 8594, "section": "Images" }, { "markdown": "![*foo* bar][]\n\n[*foo* bar]: /url \"title\"\n", "html": "<p><img src=\"/url\" alt=\"foo bar\" title=\"title\" /></p>\n", "example": 581, "start_line": 8597, "end_line": 8603, "section": "Images" }, { "markdown": "![Foo][]\n\n[foo]: /url \"title\"\n", "html": "<p><img src=\"/url\" alt=\"Foo\" title=\"title\" /></p>\n", "example": 582, "start_line": 8608, "end_line": 8614, "section": "Images" }, { "markdown": "![foo] \n[]\n\n[foo]: /url \"title\"\n", "html": "<p><img src=\"/url\" alt=\"foo\" title=\"title\" />\n[]</p>\n", "example": 583, "start_line": 8620, "end_line": 8628, "section": "Images" }, { "markdown": "![foo]\n\n[foo]: /url \"title\"\n", "html": "<p><img src=\"/url\" alt=\"foo\" title=\"title\" /></p>\n", "example": 584, "start_line": 8633, "end_line": 8639, "section": "Images" }, { "markdown": "![*foo* bar]\n\n[*foo* bar]: /url \"title\"\n", "html": "<p><img src=\"/url\" alt=\"foo bar\" title=\"title\" /></p>\n", "example": 585, "start_line": 8642, "end_line": 8648, "section": "Images" }, { "markdown": "![[foo]]\n\n[[foo]]: /url \"title\"\n", "html": "<p>![[foo]]</p>\n<p>[[foo]]: /url "title"</p>\n", "example": 586, "start_line": 8653, "end_line": 8660, "section": "Images" }, { "markdown": "![Foo]\n\n[foo]: /url \"title\"\n", "html": "<p><img src=\"/url\" alt=\"Foo\" title=\"title\" /></p>\n", "example": 587, "start_line": 8665, "end_line": 8671, "section": "Images" }, { "markdown": "!\\[foo]\n\n[foo]: /url \"title\"\n", "html": "<p>![foo]</p>\n", "example": 588, "start_line": 8677, "end_line": 8683, "section": "Images" }, { "markdown": "\\![foo]\n\n[foo]: /url \"title\"\n", "html": "<p>!<a href=\"/url\" title=\"title\">foo</a></p>\n", "example": 589, "start_line": 8689, "end_line": 8695, "section": "Images" }, { "markdown": "<http://foo.bar.baz>\n", "html": "<p><a href=\"http://foo.bar.baz\">http://foo.bar.baz</a></p>\n", "example": 590, "start_line": 8722, "end_line": 8726, "section": "Autolinks" }, { "markdown": "<http://foo.bar.baz/test?q=hello&id=22&boolean>\n", "html": "<p><a href=\"http://foo.bar.baz/test?q=hello&id=22&boolean\">http://foo.bar.baz/test?q=hello&id=22&boolean</a></p>\n", "example": 591, "start_line": 8729, "end_line": 8733, "section": "Autolinks" }, { "markdown": "<irc://foo.bar:2233/baz>\n", "html": "<p><a href=\"irc://foo.bar:2233/baz\">irc://foo.bar:2233/baz</a></p>\n", "example": 592, "start_line": 8736, "end_line": 8740, "section": "Autolinks" }, { "markdown": "<MAILTO:FOO@BAR.BAZ>\n", "html": "<p><a href=\"MAILTO:FOO@BAR.BAZ\">MAILTO:FOO@BAR.BAZ</a></p>\n", "example": 593, "start_line": 8745, "end_line": 8749, "section": "Autolinks" }, { "markdown": "<a+b+c:d>\n", "html": "<p><a href=\"a+b+c:d\">a+b+c:d</a></p>\n", "example": 594, "start_line": 8757, "end_line": 8761, "section": "Autolinks" }, { "markdown": "<made-up-scheme://foo,bar>\n", "html": "<p><a href=\"made-up-scheme://foo,bar\">made-up-scheme://foo,bar</a></p>\n", "example": 595, "start_line": 8764, "end_line": 8768, "section": "Autolinks" }, { "markdown": "<http://../>\n", "html": "<p><a href=\"http://../\">http://../</a></p>\n", "example": 596, "start_line": 8771, "end_line": 8775, "section": "Autolinks" }, { "markdown": "<localhost:5001/foo>\n", "html": "<p><a href=\"localhost:5001/foo\">localhost:5001/foo</a></p>\n", "example": 597, "start_line": 8778, "end_line": 8782, "section": "Autolinks" }, { "markdown": "<http://foo.bar/baz bim>\n", "html": "<p><http://foo.bar/baz bim></p>\n", "example": 598, "start_line": 8787, "end_line": 8791, "section": "Autolinks" }, { "markdown": "<http://example.com/\\[\\>\n", "html": "<p><a href=\"http://example.com/%5C%5B%5C\">http://example.com/\\[\\</a></p>\n", "example": 599, "start_line": 8796, "end_line": 8800, "section": "Autolinks" }, { "markdown": "<foo@bar.example.com>\n", "html": "<p><a href=\"mailto:foo@bar.example.com\">foo@bar.example.com</a></p>\n", "example": 600, "start_line": 8818, "end_line": 8822, "section": "Autolinks" }, { "markdown": "<foo+special@Bar.baz-bar0.com>\n", "html": "<p><a href=\"mailto:foo+special@Bar.baz-bar0.com\">foo+special@Bar.baz-bar0.com</a></p>\n", "example": 601, "start_line": 8825, "end_line": 8829, "section": "Autolinks" }, { "markdown": "<foo\\+@bar.example.com>\n", "html": "<p><foo+@bar.example.com></p>\n", "example": 602, "start_line": 8834, "end_line": 8838, "section": "Autolinks" }, { "markdown": "<>\n", "html": "<p><></p>\n", "example": 603, "start_line": 8843, "end_line": 8847, "section": "Autolinks" }, { "markdown": "< http://foo.bar >\n", "html": "<p>< http://foo.bar ></p>\n", "example": 604, "start_line": 8850, "end_line": 8854, "section": "Autolinks" }, { "markdown": "<m:abc>\n", "html": "<p><m:abc></p>\n", "example": 605, "start_line": 8857, "end_line": 8861, "section": "Autolinks" }, { "markdown": "<foo.bar.baz>\n", "html": "<p><foo.bar.baz></p>\n", "example": 606, "start_line": 8864, "end_line": 8868, "section": "Autolinks" }, { "markdown": "http://example.com\n", "html": "<p>http://example.com</p>\n", "example": 607, "start_line": 8871, "end_line": 8875, "section": "Autolinks" }, { "markdown": "foo@bar.example.com\n", "html": "<p>foo@bar.example.com</p>\n", "example": 608, "start_line": 8878, "end_line": 8882, "section": "Autolinks" }, { "markdown": "<a><bab><c2c>\n", "html": "<p><a><bab><c2c></p>\n", "example": 609, "start_line": 8960, "end_line": 8964, "section": "Raw HTML" }, { "markdown": "<a/><b2/>\n", "html": "<p><a/><b2/></p>\n", "example": 610, "start_line": 8969, "end_line": 8973, "section": "Raw HTML" }, { "markdown": "<a /><b2\ndata=\"foo\" >\n", "html": "<p><a /><b2\ndata=\"foo\" ></p>\n", "example": 611, "start_line": 8978, "end_line": 8984, "section": "Raw HTML" }, { "markdown": "<a foo=\"bar\" bam = 'baz <em>\"</em>'\n_boolean zoop:33=zoop:33 />\n", "html": "<p><a foo=\"bar\" bam = 'baz <em>\"</em>'\n_boolean zoop:33=zoop:33 /></p>\n", "example": 612, "start_line": 8989, "end_line": 8995, "section": "Raw HTML" }, { "markdown": "Foo <responsive-image src=\"foo.jpg\" />\n", "html": "<p>Foo <responsive-image src=\"foo.jpg\" /></p>\n", "example": 613, "start_line": 9000, "end_line": 9004, "section": "Raw HTML" }, { "markdown": "<33> <__>\n", "html": "<p><33> <__></p>\n", "example": 614, "start_line": 9009, "end_line": 9013, "section": "Raw HTML" }, { "markdown": "<a h*#ref=\"hi\">\n", "html": "<p><a h*#ref="hi"></p>\n", "example": 615, "start_line": 9018, "end_line": 9022, "section": "Raw HTML" }, { "markdown": "<a href=\"hi'> <a href=hi'>\n", "html": "<p><a href="hi'> <a href=hi'></p>\n", "example": 616, "start_line": 9027, "end_line": 9031, "section": "Raw HTML" }, { "markdown": "< a><\nfoo><bar/ >\n<foo bar=baz\nbim!bop />\n", "html": "<p>< a><\nfoo><bar/ >\n<foo bar=baz\nbim!bop /></p>\n", "example": 617, "start_line": 9036, "end_line": 9046, "section": "Raw HTML" }, { "markdown": "<a href='bar'title=title>\n", "html": "<p><a href='bar'title=title></p>\n", "example": 618, "start_line": 9051, "end_line": 9055, "section": "Raw HTML" }, { "markdown": "</a></foo >\n", "html": "<p></a></foo ></p>\n", "example": 619, "start_line": 9060, "end_line": 9064, "section": "Raw HTML" }, { "markdown": "</a href=\"foo\">\n", "html": "<p></a href="foo"></p>\n", "example": 620, "start_line": 9069, "end_line": 9073, "section": "Raw HTML" }, { "markdown": "foo <!-- this is a\ncomment - with hyphen -->\n", "html": "<p>foo <!-- this is a\ncomment - with hyphen --></p>\n", "example": 621, "start_line": 9078, "end_line": 9084, "section": "Raw HTML" }, { "markdown": "foo <!-- not a comment -- two hyphens -->\n", "html": "<p>foo <!-- not a comment -- two hyphens --></p>\n", "example": 622, "start_line": 9087, "end_line": 9091, "section": "Raw HTML" }, { "markdown": "foo <!--> foo -->\n\nfoo <!-- foo--->\n", "html": "<p>foo <!--> foo --></p>\n<p>foo <!-- foo---></p>\n", "example": 623, "start_line": 9096, "end_line": 9103, "section": "Raw HTML" }, { "markdown": "foo <?php echo $a; ?>\n", "html": "<p>foo <?php echo $a; ?></p>\n", "example": 624, "start_line": 9108, "end_line": 9112, "section": "Raw HTML" }, { "markdown": "foo <!ELEMENT br EMPTY>\n", "html": "<p>foo <!ELEMENT br EMPTY></p>\n", "example": 625, "start_line": 9117, "end_line": 9121, "section": "Raw HTML" }, { "markdown": "foo <![CDATA[>&<]]>\n", "html": "<p>foo <![CDATA[>&<]]></p>\n", "example": 626, "start_line": 9126, "end_line": 9130, "section": "Raw HTML" }, { "markdown": "foo <a href=\"ö\">\n", "html": "<p>foo <a href=\"ö\"></p>\n", "example": 627, "start_line": 9136, "end_line": 9140, "section": "Raw HTML" }, { "markdown": "foo <a href=\"\\*\">\n", "html": "<p>foo <a href=\"\\*\"></p>\n", "example": 628, "start_line": 9145, "end_line": 9149, "section": "Raw HTML" }, { "markdown": "<a href=\"\\\"\">\n", "html": "<p><a href="""></p>\n", "example": 629, "start_line": 9152, "end_line": 9156, "section": "Raw HTML" }, { "markdown": "foo \nbaz\n", "html": "<p>foo<br />\nbaz</p>\n", "example": 630, "start_line": 9166, "end_line": 9172, "section": "Hard line breaks" }, { "markdown": "foo\\\nbaz\n", "html": "<p>foo<br />\nbaz</p>\n", "example": 631, "start_line": 9178, "end_line": 9184, "section": "Hard line breaks" }, { "markdown": "foo \nbaz\n", "html": "<p>foo<br />\nbaz</p>\n", "example": 632, "start_line": 9189, "end_line": 9195, "section": "Hard line breaks" }, { "markdown": "foo \n bar\n", "html": "<p>foo<br />\nbar</p>\n", "example": 633, "start_line": 9200, "end_line": 9206, "section": "Hard line breaks" }, { "markdown": "foo\\\n bar\n", "html": "<p>foo<br />\nbar</p>\n", "example": 634, "start_line": 9209, "end_line": 9215, "section": "Hard line breaks" }, { "markdown": "*foo \nbar*\n", "html": "<p><em>foo<br />\nbar</em></p>\n", "example": 635, "start_line": 9221, "end_line": 9227, "section": "Hard line breaks" }, { "markdown": "*foo\\\nbar*\n", "html": "<p><em>foo<br />\nbar</em></p>\n", "example": 636, "start_line": 9230, "end_line": 9236, "section": "Hard line breaks" }, { "markdown": "`code \nspan`\n", "html": "<p><code>code span</code></p>\n", "example": 637, "start_line": 9241, "end_line": 9246, "section": "Hard line breaks" }, { "markdown": "`code\\\nspan`\n", "html": "<p><code>code\\ span</code></p>\n", "example": 638, "start_line": 9249, "end_line": 9254, "section": "Hard line breaks" }, { "markdown": "<a href=\"foo \nbar\">\n", "html": "<p><a href=\"foo \nbar\"></p>\n", "example": 639, "start_line": 9259, "end_line": 9265, "section": "Hard line breaks" }, { "markdown": "<a href=\"foo\\\nbar\">\n", "html": "<p><a href=\"foo\\\nbar\"></p>\n", "example": 640, "start_line": 9268, "end_line": 9274, "section": "Hard line breaks" }, { "markdown": "foo\\\n", "html": "<p>foo\\</p>\n", "example": 641, "start_line": 9281, "end_line": 9285, "section": "Hard line breaks" }, { "markdown": "foo \n", "html": "<p>foo</p>\n", "example": 642, "start_line": 9288, "end_line": 9292, "section": "Hard line breaks" }, { "markdown": "### foo\\\n", "html": "<h3>foo\\</h3>\n", "example": 643, "start_line": 9295, "end_line": 9299, "section": "Hard line breaks" }, { "markdown": "### foo \n", "html": "<h3>foo</h3>\n", "example": 644, "start_line": 9302, "end_line": 9306, "section": "Hard line breaks" }, { "markdown": "foo\nbaz\n", "html": "<p>foo\nbaz</p>\n", "example": 645, "start_line": 9317, "end_line": 9323, "section": "Soft line breaks" }, { "markdown": "foo \n baz\n", "html": "<p>foo\nbaz</p>\n", "example": 646, "start_line": 9329, "end_line": 9335, "section": "Soft line breaks" }, { "markdown": "hello $.;'there\n", "html": "<p>hello $.;'there</p>\n", "example": 647, "start_line": 9349, "end_line": 9353, "section": "Textual content" }, { "markdown": "Foo χρῆν\n", "html": "<p>Foo χρῆν</p>\n", "example": 648, "start_line": 9356, "end_line": 9360, "section": "Textual content" }, { "markdown": "Multiple spaces\n", "html": "<p>Multiple spaces</p>\n", "example": 649, "start_line": 9365, "end_line": 9369, "section": "Textual content" } ] |
Changes to testdata/meta/title/20200310110300.zettel.
|
| | | 1 | title: A ""Title"" with //Markup//, ``Zettelmarkup``{=zmk} |
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/naughty/LICENSE.
|
| < < < < < < < < < < < < < < < < < < < < < < |
Deleted testdata/naughty/README.md.
|
| < < < < < < |
Deleted testdata/naughty/blns.txt.
|
| < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < |
Deleted testdata/testbox/00000000000100.zettel.
|
| < < < < < < < < |
Deleted testdata/testbox/19700101000000.zettel.
|
| < < < < < < < < < < < < |
Deleted testdata/testbox/20210629163300.zettel.
|
| < < < < < < < < < |
Deleted testdata/testbox/20210629165000.zettel.
|
| < < < < < < < < < < |
Deleted testdata/testbox/20210629165024.zettel.
|
| < < < < < < < < < < |
Deleted testdata/testbox/20210629165050.zettel.
|
| < < < < < < < < < < |
Deleted testdata/testbox/20211019200500.zettel.
|
| < < < < < < < < < < |
Deleted testdata/testbox/20211020121000.zettel.
|
| < < < < < < < < |
Deleted testdata/testbox/20211020121100.zettel.
|
| < < < < < < |
Deleted testdata/testbox/20211020121145.zettel.
|
| < < < < < < |
Deleted testdata/testbox/20211020121300.zettel.
|
| < < < < < < |
Deleted testdata/testbox/20211020121400.zettel.
|
| < < < < < < |
Deleted testdata/testbox/20211020182600.zettel.
|
| < < < < < < < |
Deleted testdata/testbox/20211020183700.zettel.
|
| < < < < < < < |
Deleted testdata/testbox/20211020183800.zettel.
|
| < < < < < < |
Deleted testdata/testbox/20211020184300.zettel.
|
| < < < < < |
Deleted testdata/testbox/20211020184342.zettel.
|
| < < < < < < |
Deleted testdata/testbox/20211020185400.zettel.
|
| < < < < < < < < |
Deleted testdata/testbox/20230929102100.zettel.
|
| < < < < < < < |
Deleted tests/client/client_test.go.
|
| < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < |
Deleted tests/client/crud_test.go.
|
| < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < |
Deleted tests/client/embed_test.go.
|
| < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < |
Changes to tests/markdown_test.go.
1 | //----------------------------------------------------------------------------- | | | < < < > < > < < < | | | | | < > > > > > > > > > > > > > > > > > > > > > > > > > > > > < | | | < > > > | | | > > > | < < < < | | | | > > > > > > > > > > > > > > > > > > > > > > > > > | | | < | | > | < | | > | < | | > | | < < < < < < < < < < < < < < < < < < < | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 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 | //----------------------------------------------------------------------------- // Copyright (c) 2020-2021 Detlef Stern // // This file is part of zettelstore. // // Zettelstore is licensed under the latest version of the EUPL (European Union // Public License). Please see file LICENSE.txt for your rights and obligations // under this license. //----------------------------------------------------------------------------- // Package tests provides some higher-level tests. package tests import ( "encoding/json" "fmt" "os" "regexp" "strings" "testing" "zettelstore.de/z/ast" "zettelstore.de/z/encoder" _ "zettelstore.de/z/encoder/htmlenc" _ "zettelstore.de/z/encoder/jsonenc" _ "zettelstore.de/z/encoder/nativeenc" _ "zettelstore.de/z/encoder/textenc" _ "zettelstore.de/z/encoder/zmkenc" "zettelstore.de/z/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"` } // exceptions lists all CommonMark tests that should not be tested for identical HTML output var exceptions = []string{ " - foo\n - bar\n\t - baz\n", // 9 "<script type=\"text/javascript\">\n// JavaScript example\n\ndocument.getElementById(\"demo\").innerHTML = \"Hello JavaScript!\";\n</script>\nokay\n", // 140 "<script>\nfoo\n</script>1. *bar*\n", // 147 "- foo\n - bar\n - baz\n - boo\n", // 264 "10) foo\n - bar\n", // 266 "- # Foo\n- Bar\n ---\n baz\n", // 270 "- foo\n\n- bar\n\n\n- baz\n", // 276 "- foo\n - bar\n - baz\n\n\n bim\n", // 277 "1. a\n\n 2. b\n\n 3. c\n", // 281 "1. a\n\n 2. b\n\n 3. c\n", // 283 "- a\n- b\n\n- c\n", // 284 "* a\n*\n\n* c\n", // 285 "- a\n- b\n\n [ref]: /url\n- d\n", // 287 "- a\n - b\n\n c\n- d\n", // 289 "* a\n > b\n >\n* c\n", // 290 "- a\n > b\n ```\n c\n ```\n- d\n", // 291 "- a\n - b\n", // 293 "<http://example.com?find=\\*>\n", // 306 "<http://foo.bar.`baz>`\n", // 346 "[foo<http://example.com/?search=](uri)>\n", // 522 "[foo<http://example.com/?search=][ref]>\n\n[ref]: /uri\n", // 534 "<http://foo.bar.baz/test?q=hello&id=22&boolean>\n", // 591 } var reHeadingID = regexp.MustCompile(` id="[^"]*"`) func TestEncoderAvailability(t *testing.T) { encoderMissing := false for _, format := range formats { enc := encoder.Create(format, nil) if enc == nil { t.Errorf("No encoder for %q found", format) encoderMissing = true } } if encoderMissing { panic("At least one encoder is missing. See test log") } } func TestMarkdownSpec(t *testing.T) { content, err := os.ReadFile("../testdata/markdown/spec.json") if err != nil { panic(err) } var testcases []markdownTestCase if err = json.Unmarshal(content, &testcases); err != nil { panic(err) } excMap := make(map[string]bool, len(exceptions)) for _, exc := range exceptions { excMap[exc] = true } for _, tc := range testcases { ast := parser.ParseBlocks(input.NewInput(tc.Markdown), nil, "markdown") testAllEncodings(t, tc, ast) if _, found := excMap[tc.Markdown]; !found { testHTMLEncoding(t, tc, ast) } testZmkEncoding(t, tc, ast) } } func testAllEncodings(t *testing.T, tc markdownTestCase, ast ast.BlockSlice) { var sb strings.Builder testID := tc.Example*100 + 1 for _, format := range formats { t.Run(fmt.Sprintf("Encode %v %v", format, testID), func(st *testing.T) { encoder.Create(format, nil).WriteBlocks(&sb, ast) sb.Reset() }) } } func testHTMLEncoding(t *testing.T, tc markdownTestCase, ast ast.BlockSlice) { htmlEncoder := encoder.Create("html", &encoder.Environment{Xhtml: true}) var sb strings.Builder testID := tc.Example*100 + 1 t.Run(fmt.Sprintf("Encode md html %v", testID), func(st *testing.T) { htmlEncoder.WriteBlocks(&sb, ast) gotHTML := sb.String() sb.Reset() mdHTML := tc.HTML mdHTML = strings.ReplaceAll(mdHTML, "\"MAILTO:", "\"mailto:") gotHTML = strings.ReplaceAll(gotHTML, " class=\"zs-external\"", "") gotHTML = strings.ReplaceAll(gotHTML, "%2A", "*") // url.QueryEscape if strings.Count(gotHTML, "<h") > 0 { gotHTML = reHeadingID.ReplaceAllString(gotHTML, "") } if gotHTML != mdHTML { mdHTML = strings.ReplaceAll(mdHTML, "<li>\n", "<li>") if gotHTML != mdHTML { st.Errorf("\nCMD: %q\nExp: %q\nGot: %q", tc.Markdown, mdHTML, gotHTML) } } }) } func testZmkEncoding(t *testing.T, tc markdownTestCase, ast ast.BlockSlice) { zmkEncoder := encoder.Create("zmk", nil) var sb strings.Builder testID := tc.Example*100 + 1 t.Run(fmt.Sprintf("Encode zmk %14d", testID), func(st *testing.T) { zmkEncoder.WriteBlocks(&sb, ast) gotFirst := sb.String() sb.Reset() testID = tc.Example*100 + 2 secondAst := parser.ParseBlocks(input.NewInput(gotFirst), nil, "zmk") zmkEncoder.WriteBlocks(&sb, secondAst) gotSecond := sb.String() sb.Reset() // if gotFirst != gotSecond { // st.Errorf("\nCMD: %q\n1st: %q\n2nd: %q", tc.Markdown, gotFirst, gotSecond) // } testID = tc.Example*100 + 3 thirdAst := parser.ParseBlocks(input.NewInput(gotFirst), nil, "zmk") zmkEncoder.WriteBlocks(&sb, thirdAst) gotThird := sb.String() sb.Reset() if gotSecond != gotThird { st.Errorf("\n1st: %q\n2nd: %q", gotSecond, gotThird) } }) } |
Deleted tests/naughtystrings_test.go.
|
| < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < |
Changes to tests/regression_test.go.
1 | //----------------------------------------------------------------------------- | | | < < < < | | | | | > > > > > | > > | < < < | < | | < < < < < | | | | | | | | | | | | > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > | > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > | | | | | | | | < | < | | | | | | | > | | > < | | < > > > > > | | > | | | < | | < < | | | | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 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 | //----------------------------------------------------------------------------- // Copyright (c) 2020-2021 Detlef Stern // // This file is part of zettelstore. // // Zettelstore is licensed under the latest version of the EUPL (European Union // Public License). Please see file LICENSE.txt for your rights and obligations // under this license. //----------------------------------------------------------------------------- // Package tests provides some higher-level tests. package tests import ( "context" "fmt" "io" "net/url" "os" "path/filepath" "strings" "testing" "zettelstore.de/z/ast" "zettelstore.de/z/domain" "zettelstore.de/z/domain/id" "zettelstore.de/z/domain/meta" "zettelstore.de/z/encoder" "zettelstore.de/z/kernel" "zettelstore.de/z/parser" "zettelstore.de/z/place" "zettelstore.de/z/place/manager" _ "zettelstore.de/z/encoder/htmlenc" _ "zettelstore.de/z/encoder/jsonenc" _ "zettelstore.de/z/encoder/nativeenc" _ "zettelstore.de/z/encoder/textenc" _ "zettelstore.de/z/encoder/zmkenc" _ "zettelstore.de/z/parser/blob" _ "zettelstore.de/z/parser/zettelmark" _ "zettelstore.de/z/place/dirplace" ) var formats = []string{"html", "djson", "native", "text"} func getFilePlaces(wd string, kind string) (root string, places []place.ManagedPlace) { root = filepath.Clean(filepath.Join(wd, "..", "testdata", kind)) entries, err := os.ReadDir(root) if err != nil { panic(err) } cdata := manager.ConnectData{Config: testConfig, Enricher: &noEnrich{}, Notify: nil} for _, entry := range entries { if entry.IsDir() { u, err := url.Parse("dir://" + filepath.Join(root, entry.Name()) + "?type=" + kernel.PlaceDirTypeSimple) if err != nil { panic(err) } place, err := manager.Connect(u, &noAuth{}, &cdata) if err != nil { panic(err) } places = append(places, place) } } return root, places } type noEnrich struct{} func (nf *noEnrich) Enrich(ctx context.Context, m *meta.Meta) {} func (nf *noEnrich) Remove(ctx context.Context, m *meta.Meta) {} type noAuth struct{} func (na *noAuth) IsReadonly() bool { return false } func trimLastEOL(s string) string { if lastPos := len(s) - 1; lastPos >= 0 && s[lastPos] == '\n' { return s[:lastPos] } return s } func resultFile(file string) (data string, err error) { f, err := os.Open(file) if err != nil { return "", err } defer f.Close() src, err := io.ReadAll(f) return string(src), err } func checkFileContent(t *testing.T, filename string, gotContent string) { t.Helper() wantContent, err := resultFile(filename) if err != nil { t.Error(err) return } gotContent = trimLastEOL(gotContent) wantContent = trimLastEOL(wantContent) if gotContent != wantContent { t.Errorf("\nWant: %q\nGot: %q", wantContent, gotContent) } } func checkBlocksFile(t *testing.T, resultName string, zn *ast.ZettelNode, format string) { t.Helper() var env encoder.Environment if enc := encoder.Create(format, &env); enc != nil { var sb strings.Builder enc.WriteBlocks(&sb, zn.Ast) checkFileContent(t, resultName, sb.String()) return } panic(fmt.Sprintf("Unknown writer format %q", format)) } func checkZmkEncoder(t *testing.T, zn *ast.ZettelNode) { zmkEncoder := encoder.Create("zmk", nil) var sb strings.Builder zmkEncoder.WriteBlocks(&sb, zn.Ast) gotFirst := sb.String() sb.Reset() newZettel := parser.ParseZettel(domain.Zettel{ Meta: zn.Meta, Content: domain.NewContent("\n" + gotFirst)}, "", testConfig) zmkEncoder.WriteBlocks(&sb, newZettel.Ast) gotSecond := sb.String() sb.Reset() if gotFirst != gotSecond { t.Errorf("\n1st: %q\n2nd: %q", gotFirst, gotSecond) } } func getPlaceName(p place.ManagedPlace, root string) string { u, err := url.Parse(p.Location()) if err != nil { panic("Unable to parse URL '" + p.Location() + "': " + err.Error()) } return u.Path[len(root):] } func match(*meta.Meta) bool { return true } func checkContentPlace(t *testing.T, p place.ManagedPlace, wd, placeName string) { ss := p.(place.StartStopper) if err := ss.Start(context.Background()); err != nil { panic(err) } metaList, err := p.SelectMeta(context.Background(), match) if err != nil { panic(err) } for _, meta := range metaList { zettel, err := p.GetZettel(context.Background(), meta.Zid) if err != nil { panic(err) } z := parser.ParseZettel(zettel, "", testConfig) for _, format := range formats { t.Run(fmt.Sprintf("%s::%d(%s)", p.Location(), meta.Zid, format), func(st *testing.T) { resultName := filepath.Join(wd, "result", "content", placeName, z.Zid.String()+"."+format) checkBlocksFile(st, resultName, z, format) }) } t.Run(fmt.Sprintf("%s::%d", p.Location(), meta.Zid), func(st *testing.T) { checkZmkEncoder(st, z) }) } if err := ss.Stop(context.Background()); err != nil { panic(err) } } func TestContentRegression(t *testing.T) { wd, err := os.Getwd() if err != nil { panic(err) } root, places := getFilePlaces(wd, "content") for _, p := range places { checkContentPlace(t, p, wd, getPlaceName(p, root)) } } func checkMetaFile(t *testing.T, resultName string, zn *ast.ZettelNode, format string) { t.Helper() if enc := encoder.Create(format, nil); enc != nil { var sb strings.Builder enc.WriteMeta(&sb, zn.Meta) checkFileContent(t, resultName, sb.String()) return } panic(fmt.Sprintf("Unknown writer format %q", format)) } func checkMetaPlace(t *testing.T, p place.ManagedPlace, wd, placeName string) { ss := p.(place.StartStopper) if err := ss.Start(context.Background()); err != nil { panic(err) } metaList, err := p.SelectMeta(context.Background(), match) if err != nil { panic(err) } for _, meta := range metaList { zettel, err := p.GetZettel(context.Background(), meta.Zid) if err != nil { panic(err) } z := parser.ParseZettel(zettel, "", testConfig) for _, format := range formats { t.Run(fmt.Sprintf("%s::%d(%s)", p.Location(), meta.Zid, format), func(st *testing.T) { resultName := filepath.Join(wd, "result", "meta", placeName, z.Zid.String()+"."+format) checkMetaFile(st, resultName, z, format) }) } } if err := ss.Stop(context.Background()); err != nil { panic(err) } } type myConfig struct{} func (cfg *myConfig) AddDefaultValues(m *meta.Meta) *meta.Meta { return m } func (cfg *myConfig) GetDefaultTitle() string { return "" } func (cfg *myConfig) GetDefaultRole() string { return meta.ValueRoleZettel } func (cfg *myConfig) GetDefaultSyntax() string { return meta.ValueSyntaxZmk } func (cfg *myConfig) GetDefaultLang() string { return "" } func (cfg *myConfig) GetDefaultVisibility() meta.Visibility { return meta.VisibilityPublic } func (cfg *myConfig) GetFooterHTML() string { return "" } func (cfg *myConfig) GetHomeZettel() id.Zid { return id.Invalid } func (cfg *myConfig) GetListPageSize() int { return 0 } func (cfg *myConfig) GetMarkerExternal() string { return "" } func (cfg *myConfig) GetSiteName() string { return "" } func (cfg *myConfig) GetYAMLHeader() bool { return false } func (cfg *myConfig) GetZettelFileSyntax() []string { return nil } func (cfg *myConfig) GetExpertMode() bool { return false } func (cfg *myConfig) GetVisibility(*meta.Meta) meta.Visibility { return cfg.GetDefaultVisibility() } var testConfig = &myConfig{} func TestMetaRegression(t *testing.T) { wd, err := os.Getwd() if err != nil { panic(err) } root, places := getFilePlaces(wd, "meta") for _, p := range places { checkMetaPlace(t, p, wd, getPlaceName(p, root)) } } |
Added tests/result/content/blockcomment/20200215204700.djson.
> | 1 | [{"t":"CommentBlock","l":["No render"]},{"t":"CommentBlock","a":{"-":""},"l":["Render"]}] |
Added tests/result/content/blockcomment/20200215204700.html.
> > > | 1 2 3 | <!-- Render --> |
Added tests/result/content/blockcomment/20200215204700.native.
> > | 1 2 | [CommentBlock "No render"], [CommentBlock ("",[-]) "Render"] |
Added tests/result/content/blockcomment/20200215204700.text.
Added tests/result/content/cite/20200215204700.djson.
> | 1 | [{"t":"Para","i":[{"t":"Cite","a":{"-":""},"s":"Stern18"}]}] |
Added tests/result/content/cite/20200215204700.html.
> | 1 | <p>Stern18</p> |
Added tests/result/content/cite/20200215204700.native.
> | 1 | [Para Cite ("",[-]) "Stern18"] |
Added tests/result/content/cite/20200215204700.text.
Added tests/result/content/comment/20200215204700.djson.
> | 1 | [{"t":"Para","i":[{"t":"Text","s":"%"},{"t":"Space"},{"t":"Text","s":"No"},{"t":"Space"},{"t":"Text","s":"comment"},{"t":"Soft"},{"t":"Comment","s":"Comment"}]}] |
Added tests/result/content/comment/20200215204700.html.
> > | 1 2 | <p>% No comment <!-- Comment --></p> |
Added tests/result/content/comment/20200215204700.native.
> | 1 | [Para Text "%",Space,Text "No",Space,Text "comment",Space,Comment "Comment"] |
Added tests/result/content/comment/20200215204700.text.
> | 1 | % No comment |
Added tests/result/content/descrlist/20200226122100.djson.
> | 1 | [{"t":"DescriptionList","g":[[[{"t":"Text","s":"Zettel"}],[{"t":"Para","i":[{"t":"Text","s":"Paper"}]}],[{"t":"Para","i":[{"t":"Text","s":"Note"}]}]],[[{"t":"Text","s":"Zettelkasten"}],[{"t":"Para","i":[{"t":"Text","s":"Slip"},{"t":"Space"},{"t":"Text","s":"box"}]}]]]}] |
Added tests/result/content/descrlist/20200226122100.html.
> > > > > > > | 1 2 3 4 5 6 7 | <dl> <dt>Zettel</dt> <dd>Paper</dd> <dd>Note</dd> <dt>Zettelkasten</dt> <dd>Slip box</dd> </dl> |
Added tests/result/content/descrlist/20200226122100.native.
> > > > > > > > > | 1 2 3 4 5 6 7 8 9 | [DescriptionList [Term [Text "Zettel"], [Description [Para Text "Paper"]], [Description [Para Text "Note"]]], [Term [Text "Zettelkasten"], [Description [Para Text "Slip",Space,Text "box"]]]] |
Added tests/result/content/descrlist/20200226122100.text.
> > > > > | 1 2 3 4 5 | Zettel Paper Note Zettelkasten Slip box |
Added tests/result/content/edit/20200215204700.djson.
> | 1 | [{"t":"Para","i":[{"t":"Delete","i":[{"t":"Text","s":"delete"}]},{"t":"Soft"},{"t":"Insert","i":[{"t":"Text","s":"insert"}]},{"t":"Soft"},{"t":"Delete","i":[{"t":"Text","s":"kill"}]},{"t":"Insert","i":[{"t":"Text","s":"create"}]}]}] |
Added tests/result/content/edit/20200215204700.html.
> > > | 1 2 3 | <p><del>delete</del> <ins>insert</ins> <del>kill</del><ins>create</ins></p> |
Added tests/result/content/edit/20200215204700.native.
> | 1 | [Para Delete [Text "delete"],Space,Insert [Text "insert"],Space,Delete [Text "kill"],Insert [Text "create"]] |
Added tests/result/content/edit/20200215204700.text.
> | 1 | delete insert killcreate |
Added tests/result/content/footnote/20200215204700.djson.
> | 1 | [{"t":"Para","i":[{"t":"Text","s":"Text"},{"t":"Footnote","a":{"":"sidebar"},"i":[{"t":"Text","s":"foot"}]}]}] |
Added tests/result/content/footnote/20200215204700.html.
> > > > | 1 2 3 4 | <p>Text<sup id="fnref:1"><a href="#fn:1" class="zs-footnote-ref" role="doc-noteref">1</a></sup></p> <ol class="zs-endnotes"> <li id="fn:1" role="doc-endnote">foot <a href="#fnref:1" class="zs-footnote-backref" role="doc-backlink">↩︎</a></li> </ol> |
Added tests/result/content/footnote/20200215204700.native.
> | 1 | [Para Text "Text",Footnote ("sidebar",[]) [Text "foot"]] |
Added tests/result/content/footnote/20200215204700.text.
> | 1 | Text foot |
Added tests/result/content/format/20200215204700.djson.
> | 1 | [{"t":"Para","i":[{"t":"Italic","i":[{"t":"Text","s":"italic"}]},{"t":"Soft"},{"t":"Emph","i":[{"t":"Text","s":"emph"}]},{"t":"Soft"},{"t":"Bold","i":[{"t":"Text","s":"bold"}]},{"t":"Soft"},{"t":"Strong","i":[{"t":"Text","s":"strong"}]},{"t":"Soft"},{"t":"Underline","i":[{"t":"Text","s":"unterline"}]},{"t":"Soft"},{"t":"Strikethrough","i":[{"t":"Text","s":"strike"}]},{"t":"Soft"},{"t":"Mono","i":[{"t":"Text","s":"monospace"}]},{"t":"Soft"},{"t":"Super","i":[{"t":"Text","s":"superscript"}]},{"t":"Soft"},{"t":"Sub","i":[{"t":"Text","s":"subscript"}]},{"t":"Soft"},{"t":"Quote","i":[{"t":"Text","s":"Quotes"}]},{"t":"Soft"},{"t":"Quotation","i":[{"t":"Text","s":"Quotation"}]},{"t":"Soft"},{"t":"Small","i":[{"t":"Text","s":"small"}]},{"t":"Soft"},{"t":"Span","i":[{"t":"Text","s":"span"}]},{"t":"Soft"},{"t":"Code","s":"code"},{"t":"Soft"},{"t":"Input","s":"input"},{"t":"Soft"},{"t":"Output","s":"output"}]}] |
Added tests/result/content/format/20200215204700.html.
> > > > > > > > > > > > > > > > | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 | <p><i>italic</i> <em>emph</em> <b>bold</b> <strong>strong</strong> <u>unterline</u> <s>strike</s> <span style="font-family:monospace">monospace</span> <sup>superscript</sup> <sub>subscript</sub> "Quotes" <q>Quotation</q> <small>small</small> <span>span</span> <code>code</code> <kbd>input</kbd> <samp>output</samp></p> |
Added tests/result/content/format/20200215204700.native.
> | 1 | [Para Italic [Text "italic"],Space,Emph [Text "emph"],Space,Bold [Text "bold"],Space,Strong [Text "strong"],Space,Underline [Text "unterline"],Space,Strikethrough [Text "strike"],Space,Mono [Text "monospace"],Space,Super [Text "superscript"],Space,Sub [Text "subscript"],Space,Quote [Text "Quotes"],Space,Quotation [Text "Quotation"],Space,Small [Text "small"],Space,Span [Text "span"],Space,Code "code",Space,Input "input",Space,Output "output"] |
Added tests/result/content/format/20200215204700.text.
> | 1 | italic emph bold strong unterline strike monospace superscript subscript Quotes Quotation small span code input output |
Added tests/result/content/format/20201107164400.djson.
> | 1 | [{"t":"Para","i":[{"t":"Span","a":{"lang":"fr"},"i":[{"t":"Quote","i":[{"t":"Text","s":"abc"}]}]}]}] |
Added tests/result/content/format/20201107164400.html.
> | 1 | <p><span lang="fr">« abc »</span></p> |
Added tests/result/content/format/20201107164400.native.
> | 1 | [Para Span ("",[lang="fr"]) [Quote [Text "abc"]]] |
Added tests/result/content/format/20201107164400.text.
> | 1 | abc |
Added tests/result/content/heading/20200215204700.djson.
> | 1 | [{"t":"Heading","n":2,"s":"first","i":[{"t":"Text","s":"First"}]}] |
Added tests/result/content/heading/20200215204700.html.
> | 1 | <h2 id="first">First</h2> |
Added tests/result/content/heading/20200215204700.native.
> | 1 | [Heading 2 "first" Text "First"] |
Added tests/result/content/heading/20200215204700.text.
> | 1 | First |
Added tests/result/content/hrule/20200215204700.djson.
> | 1 | [{"t":"Hrule"}] |
Added tests/result/content/hrule/20200215204700.html.
> | 1 | <hr> |
Added tests/result/content/hrule/20200215204700.native.
> | 1 | [Hrule] |
Added tests/result/content/hrule/20200215204700.text.
Added tests/result/content/image/20200215204700.djson.
> | 1 | [{"t":"Para","i":[{"t":"Image","s":"abc"}]}] |
Added tests/result/content/image/20200215204700.html.
> | 1 | <p><img src="abc" alt=""></p> |
Added tests/result/content/image/20200215204700.native.
> | 1 | [Para Image "abc"] |
Added tests/result/content/image/20200215204700.text.
Added tests/result/content/link/20200215204700.djson.
> | 1 | [{"t":"Para","i":[{"t":"Link","q":"external","s":"https://zettelstore.de/z","i":[{"t":"Text","s":"Home"}]},{"t":"Soft"},{"t":"Link","q":"external","s":"https://zettelstore.de","i":[{"t":"Text","s":"https://zettelstore.de"}]},{"t":"Soft"},{"t":"Link","q":"zettel","s":"00000000000100","i":[{"t":"Text","s":"Config"}]},{"t":"Soft"},{"t":"Link","q":"zettel","s":"00000000000100","i":[{"t":"Text","s":"00000000000100"}]},{"t":"Soft"},{"t":"Link","q":"self","s":"#frag","i":[{"t":"Text","s":"Frag"}]},{"t":"Soft"},{"t":"Link","q":"self","s":"#frag","i":[{"t":"Text","s":"#frag"}]},{"t":"Soft"},{"t":"Link","q":"local","s":"/hosted","i":[{"t":"Text","s":"H"}]},{"t":"Soft"},{"t":"Link","q":"based","s":"/based","i":[{"t":"Text","s":"B"}]},{"t":"Soft"},{"t":"Link","q":"local","s":"../rel","i":[{"t":"Text","s":"R"}]}]}] |
Added tests/result/content/link/20200215204700.html.
> > > > > > > > > | 1 2 3 4 5 6 7 8 9 | <p><a href="https://zettelstore.de/z" class="zs-external">Home</a> <a href="https://zettelstore.de" class="zs-external">https://zettelstore.de</a> <a href="00000000000100">Config</a> <a href="00000000000100">00000000000100</a> <a href="#frag">Frag</a> <a href="#frag">#frag</a> <a href="/hosted">H</a> <a href="/based">B</a> <a href="../rel">R</a></p> |
Added tests/result/content/link/20200215204700.native.
> | 1 | [Para Link EXTERNAL "https://zettelstore.de/z" [Text "Home"],Space,Link EXTERNAL "https://zettelstore.de" [],Space,Link ZETTEL "00000000000100" [Text "Config"],Space,Link ZETTEL "00000000000100" [],Space,Link SELF "#frag" [Text "Frag"],Space,Link SELF "#frag" [],Space,Link LOCAL "/hosted" [Text "H"],Space,Link BASED "/based" [Text "B"],Space,Link LOCAL "../rel" [Text "R"]] |
Added tests/result/content/link/20200215204700.text.
> | 1 | Home Config Frag H B R |
Added tests/result/content/list/20200215204700.djson.
> | 1 | [{"t":"BulletList","c":[[{"t":"Para","i":[{"t":"Text","s":"Item"},{"t":"Space"},{"t":"Text","s":"1"}]}],[{"t":"Para","i":[{"t":"Text","s":"Item"},{"t":"Space"},{"t":"Text","s":"2"}]}],[{"t":"Para","i":[{"t":"Text","s":"Item"},{"t":"Space"},{"t":"Text","s":"3"}]}]]}] |
Added tests/result/content/list/20200215204700.html.
> > > > > | 1 2 3 4 5 | <ul> <li>Item 1</li> <li>Item 2</li> <li>Item 3</li> </ul> |
Added tests/result/content/list/20200215204700.native.
> > > > | 1 2 3 4 | [BulletList [[Para Text "Item",Space,Text "1"]], [[Para Text "Item",Space,Text "2"]], [[Para Text "Item",Space,Text "3"]]] |
Added tests/result/content/list/20200215204700.text.
> > > | 1 2 3 | Item 1 Item 2 Item 3 |
Added tests/result/content/list/20200217194800.djson.
> | 1 | [{"t":"BulletList","c":[[{"t":"Para","i":[{"t":"Text","s":"Item1.1"}]}],[{"t":"Para","i":[{"t":"Text","s":"Item1.2"}]}],[{"t":"Para","i":[{"t":"Text","s":"Item1.3"}]}],[{"t":"Para","i":[{"t":"Text","s":"Item2.1"}]}],[{"t":"Para","i":[{"t":"Text","s":"Item2.2"}]}]]}] |
Added tests/result/content/list/20200217194800.html.
> > > > > > > | 1 2 3 4 5 6 7 | <ul> <li>Item1.1</li> <li>Item1.2</li> <li>Item1.3</li> <li>Item2.1</li> <li>Item2.2</li> </ul> |
Added tests/result/content/list/20200217194800.native.
> > > > > > | 1 2 3 4 5 6 | [BulletList [[Para Text "Item1.1"]], [[Para Text "Item1.2"]], [[Para Text "Item1.3"]], [[Para Text "Item2.1"]], [[Para Text "Item2.2"]]] |
Added tests/result/content/list/20200217194800.text.
> > > > > | 1 2 3 4 5 | Item1.1 Item1.2 Item1.3 Item2.1 Item2.2 |
Added tests/result/content/list/20200516105700.djson.
> | 1 | [{"t":"BulletList","c":[[{"t":"Para","i":[{"t":"Text","s":"T1"}]},{"t":"BulletList","c":[[{"t":"Para","i":[{"t":"Text","s":"T2"}]}]]}],[{"t":"Para","i":[{"t":"Text","s":"T3"}]},{"t":"BulletList","c":[[{"t":"Para","i":[{"t":"Text","s":"T4"}]}]]}],[{"t":"Para","i":[{"t":"Text","s":"T5"}]}]]}] |
Added tests/result/content/list/20200516105700.html.
> > > > > > > > > > > > > > | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 | <ul> <li><p>T1</p> <ul> <li>T2</li> </ul> </li> <li><p>T3</p> <ul> <li>T4</li> </ul> </li> <li><p>T5</p> </li> </ul> |
Added tests/result/content/list/20200516105700.native.
> > > > > > > > | 1 2 3 4 5 6 7 8 | [BulletList [[Para Text "T1"], [BulletList [[Para Text "T2"]]]], [[Para Text "T3"], [BulletList [[Para Text "T4"]]]], [[Para Text "T5"]]] |
Added tests/result/content/list/20200516105700.text.
> > > > > | 1 2 3 4 5 | T1 T2 T3 T4 T5 |
Added tests/result/content/literal/20200215204700.djson.
> | 1 | [{"t":"Para","i":[{"t":"Input","s":"input"},{"t":"Soft"},{"t":"Code","s":"program"},{"t":"Soft"},{"t":"Output","s":"output"}]}] |
Added tests/result/content/literal/20200215204700.html.
> > > | 1 2 3 | <p><kbd>input</kbd> <code>program</code> <samp>output</samp></p> |
Added tests/result/content/literal/20200215204700.native.
> | 1 | [Para Input "input",Space,Code "program",Space,Output "output"] |
Added tests/result/content/literal/20200215204700.text.
> | 1 | input program output |
Added tests/result/content/mark/20200215204700.djson.
> | 1 | [{"t":"Para","i":[{"t":"Mark","s":"mark"}]}] |
Added tests/result/content/mark/20200215204700.html.
> | 1 | <p><a id="mark"></a></p> |
Added tests/result/content/mark/20200215204700.native.
> | 1 | [Para Mark "mark"] |
Added tests/result/content/mark/20200215204700.text.
Added tests/result/content/paragraph/20200215185900.djson.
> | 1 | [{"t":"Para","i":[{"t":"Text","s":"This"},{"t":"Space"},{"t":"Text","s":"is"},{"t":"Space"},{"t":"Text","s":"a"},{"t":"Space"},{"t":"Text","s":"zettel"},{"t":"Space"},{"t":"Text","s":"for"},{"t":"Space"},{"t":"Text","s":"testing."}]}] |
Added tests/result/content/paragraph/20200215185900.html.
> | 1 | <p>This is a zettel for testing.</p> |
Added tests/result/content/paragraph/20200215185900.native.
> | 1 | [Para Text "This",Space,Text "is",Space,Text "a",Space,Text "zettel",Space,Text "for",Space,Text "testing."] |
Added tests/result/content/paragraph/20200215185900.text.
> | 1 | This is a zettel for testing. |
Added tests/result/content/paragraph/20200217151800.djson.
> | 1 | [{"t":"Para","i":[{"t":"Text","s":"Text"},{"t":"Space"},{"t":"Text","s":"Text"},{"t":"Soft"},{"t":"Text","s":"*abc"}]},{"t":"Para","i":[{"t":"Text","s":"Text"},{"t":"Space"},{"t":"Text","s":"Text"}]},{"t":"BulletList","c":[[{"t":"Para","i":[{"t":"Text","s":"abc"}]}]]}] |
Added tests/result/content/paragraph/20200217151800.html.
> > > > > > | 1 2 3 4 5 6 | <p>Text Text *abc</p> <p>Text Text</p> <ul> <li>abc</li> </ul> |
Added tests/result/content/paragraph/20200217151800.native.
> > > > | 1 2 3 4 | [Para Text "Text",Space,Text "Text",Space,Text "*abc"], [Para Text "Text",Space,Text "Text"], [BulletList [[Para Text "abc"]]] |
Added tests/result/content/paragraph/20200217151800.text.
> > > | 1 2 3 | Text Text *abc Text Text abc |
Added tests/result/content/png/20200512180900.djson.
> | 1 | [{"t":"Blob","q":"20200512180900","s":"png","o":"iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAAAAAA6fptVAAAACklEQVR4nGNiAAAABgADNjd8qAAAAABJRU5ErkJggg=="}] |
Added tests/result/content/png/20200512180900.html.
> | 1 | <img src="data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAAAAAA6fptVAAAACklEQVR4nGNiAAAABgADNjd8qAAAAABJRU5ErkJggg==" title="20200512180900"> |
Added tests/result/content/png/20200512180900.native.
> | 1 | [BLOB "20200512180900" "png" "iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAAAAAA6fptVAAAACklEQVR4nGNiAAAABgADNjd8qAAAAABJRU5ErkJggg=="] |
Added tests/result/content/png/20200512180900.text.
Added tests/result/content/quoteblock/20200215204700.djson.
> | 1 | [{"t":"QuoteBlock","b":[{"t":"Para","i":[{"t":"Text","s":"To"},{"t":"Space"},{"t":"Text","s":"be"},{"t":"Space"},{"t":"Text","s":"or"},{"t":"Space"},{"t":"Text","s":"not"},{"t":"Space"},{"t":"Text","s":"to"},{"t":"Space"},{"t":"Text","s":"be."}]}],"i":[{"t":"Text","s":"Romeo"}]}] |
Added tests/result/content/quoteblock/20200215204700.html.
> > > > | 1 2 3 4 | <blockquote> <p>To be or not to be.</p> <cite>Romeo</cite> </blockquote> |
Added tests/result/content/quoteblock/20200215204700.native.
> > > | 1 2 3 | [QuoteBlock [[Para Text "To",Space,Text "be",Space,Text "or",Space,Text "not",Space,Text "to",Space,Text "be."]], [Cite Text "Romeo"]] |
Added tests/result/content/quoteblock/20200215204700.text.
> > | 1 2 | To be or not to be. Romeo |
Added tests/result/content/spanblock/20200215204700.djson.
> | 1 | [{"t":"SpanBlock","b":[{"t":"Para","i":[{"t":"Text","s":"A"},{"t":"Space"},{"t":"Text","s":"simple"},{"t":"Soft"},{"t":"Space","n":3},{"t":"Text","s":"span"},{"t":"Soft"},{"t":"Text","s":"and"},{"t":"Space"},{"t":"Text","s":"much"},{"t":"Space"},{"t":"Text","s":"more"}]}]}] |
Added tests/result/content/spanblock/20200215204700.html.
> > > > > | 1 2 3 4 5 | <div> <p>A simple span and much more</p> </div> |
Added tests/result/content/spanblock/20200215204700.native.
> > | 1 2 | [SpanBlock [[Para Text "A",Space,Text "simple",Space,Space 3,Text "span",Space,Text "and",Space,Text "much",Space,Text "more"]]] |
Added tests/result/content/spanblock/20200215204700.text.
> | 1 | A simple span and much more |
Added tests/result/content/table/20200215204700.djson.
> | 1 | [{"t":"Table","p":[[],[[["",[{"t":"Text","s":"c1"}]],["",[{"t":"Text","s":"c2"}]],["",[{"t":"Text","s":"c3"}]]]]]}] |
Added tests/result/content/table/20200215204700.html.
> > > > > | 1 2 3 4 5 | <table> <tbody> <tr><td>c1</td><td>c2</td><td>c3</td></tr> </tbody> </table> |
Added tests/result/content/table/20200215204700.native.
> > | 1 2 | [Table [Row [Cell Default Text "c1"],[Cell Default Text "c2"],[Cell Default Text "c3"]]] |
Added tests/result/content/table/20200215204700.text.
> | 1 | c1 c2 c3 |
Added tests/result/content/table/20200618140700.djson.
> | 1 | [{"t":"Table","p":[[[">",[{"t":"Text","s":"h1"}]],["",[{"t":"Text","s":"h2"}]],[":",[{"t":"Text","s":"h3"}]]],[[["<",[{"t":"Text","s":"c1"}]],["",[{"t":"Text","s":"c2"}]],[":",[{"t":"Text","s":"c3"}]]],[[">",[{"t":"Text","s":"f1"}]],["",[{"t":"Text","s":"f2"}]],[":",[{"t":"Text","s":"=f3"}]]]]]}] |
Added tests/result/content/table/20200618140700.html.
> > > > > > > > > | 1 2 3 4 5 6 7 8 9 | <table> <thead> <tr><th style="text-align:right">h1</th><th>h2</th><th style="text-align:center">h3</th></tr> </thead> <tbody> <tr><td style="text-align:left">c1</td><td>c2</td><td style="text-align:center">c3</td></tr> <tr><td style="text-align:right">f1</td><td>f2</td><td style="text-align:center">=f3</td></tr> </tbody> </table> |
Added tests/result/content/table/20200618140700.native.
> > > > | 1 2 3 4 | [Table [Header [Cell Right Text "h1"],[Cell Default Text "h2"],[Cell Center Text "h3"]], [Row [Cell Left Text "c1"],[Cell Default Text "c2"],[Cell Center Text "c3"]], [Row [Cell Right Text "f1"],[Cell Default Text "f2"],[Cell Center Text "=f3"]]] |
Added tests/result/content/table/20200618140700.text.
> > > | 1 2 3 | h1 h2 h3 c1 c2 c3 f1 f2 =f3 |
Added tests/result/content/verbatim/20200215204700.djson.
> | 1 | [{"t":"CodeBlock","l":["if __name__ == \"main\":"," print(\"Hello, World\")","exit(0)"]}] |
Added tests/result/content/verbatim/20200215204700.html.
> > > > | 1 2 3 4 | <pre><code>if __name__ == "main": print("Hello, World") exit(0) </code></pre> |
Added tests/result/content/verbatim/20200215204700.native.
> | 1 | [CodeBlock "if __name__ == \"main\":\n print(\"Hello, World\")\nexit(0)"] |
Added tests/result/content/verbatim/20200215204700.text.
> > > | 1 2 3 | if __name__ == "main": print("Hello, World") exit(0) |
Added tests/result/content/verseblock/20200215204700.djson.
> | 1 | [{"t":"VerseBlock","b":[{"t":"Para","i":[{"t":"Text","s":"A line"},{"t":"Hard"},{"t":"Text","s":" another line"},{"t":"Hard"},{"t":"Text","s":"Back"}]},{"t":"Para","i":[{"t":"Text","s":"Paragraph"}]},{"t":"Para","i":[{"t":"Text","s":" Spacy Para"}]}],"i":[{"t":"Text","s":"Author"}]}] |
Added tests/result/content/verseblock/20200215204700.html.
> > > > > > > > | 1 2 3 4 5 6 7 8 | <div> <p>A line<br> another line<br> Back</p> <p>Paragraph</p> <p> Spacy Para</p> <cite>Author</cite> </div> |
Added tests/result/content/verseblock/20200215204700.native.
> > > > > | 1 2 3 4 5 | [VerseBlock [[Para Text "A line",Break,Text " another line",Break,Text "Back"], [Para Text "Paragraph"], [Para Text " Spacy Para"]], [Cite Text "Author"]] |
Added tests/result/content/verseblock/20200215204700.text.
> > > > > > | 1 2 3 4 5 6 | A line another line Back Paragraph Spacy Para Author |
Added tests/result/meta/copyright/20200310125800.djson.
> | 1 | {"title":[{"t":"Text","s":"Header"},{"t":"Space"},{"t":"Text","s":"Test"}],"role":"zettel","syntax":"zmk","copyright":"(c) 2020 Detlef Stern","license":"CC BY-SA 4.0"} |
Deleted tests/result/meta/copyright/20200310125800.zjson.
|
| < |
Added tests/result/meta/header/20200310125800.djson.
> | 1 | {"title":[{"t":"Text","s":"Header"},{"t":"Space"},{"t":"Text","s":"Test"}],"role":"zettel","syntax":"zmk","x-no":"00000000000000"} |
Deleted tests/result/meta/header/20200310125800.zjson.
|
| < |
Added tests/result/meta/title/20200310110300.djson.
> | 1 | {"title":[{"t":"Text","s":"A"},{"t":"Space"},{"t":"Quote","i":[{"t":"Text","s":"Title"}]},{"t":"Space"},{"t":"Text","s":"with"},{"t":"Space"},{"t":"Italic","i":[{"t":"Text","s":"Markup"}]},{"t":"Text","s":","},{"t":"Space"},{"t":"Code","a":{"":"zmk"},"s":"Zettelmarkup"}],"role":"zettel","syntax":"zmk"} |
Changes to tests/result/meta/title/20200310110300.native.
|
| | | 1 2 3 | [Title Text "A",Space,Quote [Text "Title"],Space,Text "with",Space,Italic [Text "Markup"],Text ",",Space,Code ("zmk",[]) "Zettelmarkup"] [Role "zettel"] [Syntax "zmk"] |
Deleted tests/result/meta/title/20200310110300.zjson.
|
| < |
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 | //----------------------------------------------------------------------------- // Copyright (c) 2021 Detlef Stern // // This file is part of zettelstore. // // Zettelstore is licensed under the latest version of the EUPL (European Union // Public License). Please see file LICENSE.txt for your rights and obligations // under this license. //----------------------------------------------------------------------------- // Package main provides a command to build and run the software. package main import ( "archive/zip" "bytes" "flag" "fmt" "io" "io/fs" "os" "os/exec" "path/filepath" "regexp" "strings" "time" "zettelstore.de/z/strfun" ) func executeCommand(env []string, name string, arg ...string) (string, error) { 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) } if len(env) > 0 { env = append(env, os.Environ()...) } var out bytes.Buffer cmd := exec.Command(name, arg...) cmd.Env = env cmd.Stdin = nil cmd.Stdout = &out cmd.Stderr = os.Stderr err := cmd.Run() return out.String(), err } func readVersionFile() (string, error) { content, err := os.ReadFile("VERSION") if err != nil { return "", err } return strings.TrimFunc(string(content), func(r rune) bool { return r <= ' ' }), nil } var fossilCheckout = regexp.MustCompile(`^checkout:\s+([0-9a-f]+)\s`) var dirtyPrefixes = []string{ "DELETED ", "ADDED ", "UPDATED ", "CONFLICT ", "EDITED ", "RENAMED ", "EXTRA "} const dirtySuffix = "-dirty" func readFossilVersion() (string, error) { s, err := executeCommand(nil, "fossil", "status", "--differ") if err != nil { return "", err } var hash, suffix string for _, line := range strfun.SplitLines(s) { if hash == "" { if m := fossilCheckout.FindStringSubmatch(line); len(m) > 0 { hash = m[1][:10] if suffix != "" { return hash + suffix, nil } continue } } if suffix == "" { for _, prefix := range dirtyPrefixes { if strings.HasPrefix(line, prefix) { suffix = dirtySuffix if hash != "" { return hash + suffix, nil } break } } } } return hash, nil } func getVersionData() (string, string) { base, err := readVersionFile() if err != nil { base = "dev" } fossil, err := readFossilVersion() if err != nil { return base, "" } return base, fossil } func calcVersion(base, vcs string) string { return base + "+" + vcs } func getVersion() string { base, vcs := getVersionData() return calcVersion(base, vcs) } func findExec(cmd string) string { if path, err := executeCommand(nil, "which", "shadow"); err == nil && path != "" { return path } return "" } func cmdCheck() error { if err := checkGoTest(); err != nil { return err } if err := checkGoVet(); err != nil { return err } if err := checkGoLint(); err != nil { return err } if err := checkGoVetShadow(); err != nil { return err } if err := checkStaticcheck(); err != nil { return err } return checkFossilExtra() } func checkGoTest() error { out, err := executeCommand(nil, "go", "test", "./...") if err != nil { for _, line := range strfun.SplitLines(out) { if strings.HasPrefix(line, "ok") || strings.HasPrefix(line, "?") { continue } fmt.Fprintln(os.Stderr, line) } } return err } func checkGoVet() error { out, err := executeCommand(nil, "go", "vet", "./...") if err != nil { fmt.Fprintln(os.Stderr, "Some checks failed") if len(out) > 0 { fmt.Fprintln(os.Stderr, out) } } return err } func checkGoLint() error { out, err := executeCommand(nil, "golint", "./...") if out != "" { fmt.Fprintln(os.Stderr, "Some lints failed") fmt.Fprint(os.Stderr, out) } return err } func checkGoVetShadow() error { path := findExec("shadow") if path == "" { return nil } out, err := executeCommand(nil, "go", "vet", "-vettool", strings.TrimSpace(path), "./...") if err != nil { fmt.Fprintln(os.Stderr, "Some shadowed variables found") if len(out) > 0 { fmt.Fprintln(os.Stderr, out) } } return err } func checkStaticcheck() error { out, err := executeCommand(nil, "staticcheck", "./...") if err != nil { fmt.Fprintln(os.Stderr, "Some staticcheck problems found") if len(out) > 0 { fmt.Fprintln(os.Stderr, out) } } return err } func checkFossilExtra() error { out, err := executeCommand(nil, "fossil", "extra") if err != nil { fmt.Fprintln(os.Stderr, "Unable to execute 'fossil extra'") return err } if len(out) > 0 { fmt.Fprint(os.Stderr, "Warning: unversioned file(s):") for i, extra := range strfun.SplitLines(out) { if i > 0 { fmt.Fprint(os.Stderr, ",") } fmt.Fprintf(os.Stderr, " %q", extra) } fmt.Fprintln(os.Stderr) } return nil } func cmdBuild() error { return doBuild(nil, getVersion(), "bin/zettelstore") } func doBuild(env []string, version, target string) error { out, err := executeCommand( env, "go", "build", "-tags", "osusergo,netgo", "-trimpath", "-ldflags", fmt.Sprintf("-X main.version=%v -w", version), "-o", target, "zettelstore.de/z/cmd/zettelstore", ) if err != nil { return err } if len(out) > 0 { fmt.Println(out) } return nil } func cmdManual() error { base, _ := getReleaseVersionData() return createManualZip(".", base) } func createManualZip(path, base string) error { manualPath := filepath.Join("docs", "manual") entries, err := os.ReadDir(manualPath) if err != nil { return err } zipName := filepath.Join(path, "manual-"+base+".zip") zipFile, err := os.OpenFile(zipName, os.O_RDWR|os.O_CREATE, 0600) if err != nil { return err } defer zipFile.Close() zipWriter := zip.NewWriter(zipFile) defer zipWriter.Close() for _, entry := range entries { if err = createManualZipEntry(manualPath, entry, zipWriter); err != nil { return err } } return nil } func createManualZipEntry(path string, entry fs.DirEntry, zipWriter *zip.Writer) error { info, err := entry.Info() if err != nil { return err } fh, err := zip.FileInfoHeader(info) if err != nil { return err } fh.Name = entry.Name() fh.Method = zip.Deflate w, err := zipWriter.CreateHeader(fh) if err != nil { return err } manualFile, err := os.Open(filepath.Join(path, entry.Name())) if err != nil { return err } defer manualFile.Close() _, err = io.Copy(w, manualFile) return err } func getReleaseVersionData() (string, string) { base, fossil := getVersionData() if strings.HasSuffix(base, "dev") { base = base[:len(base)-3] + "preview-" + time.Now().Format("20060102") } if strings.HasSuffix(fossil, dirtySuffix) { fmt.Fprintf(os.Stderr, "Warning: releasing a dirty version %v\n", fossil) base = base + dirtySuffix } return base, fossil } func cmdRelease() error { if err := cmdCheck(); err != nil { return err } base, fossil := getReleaseVersionData() releases := []struct { arch string os string env []string name string }{ {"amd64", "linux", nil, "zettelstore"}, {"arm", "linux", []string{"GOARM=6"}, "zettelstore"}, {"amd64", "darwin", nil, "iZettelstore"}, {"arm64", "darwin", nil, "iZettelstore"}, {"amd64", "windows", nil, "zettelstore.exe"}, } for _, rel := range releases { env := append(rel.env, "GOARCH="+rel.arch, "GOOS="+rel.os) zsName := filepath.Join("releases", rel.name) if err := doBuild(env, calcVersion(base, fossil), zsName); err != nil { return err } zipName := fmt.Sprintf("zettelstore-%v-%v-%v.zip", base, rel.os, rel.arch) if err := createReleaseZip(zsName, zipName, rel.name); err != nil { return err } if err := os.Remove(zsName); err != nil { return err } } return createManualZip("releases", base) } func createReleaseZip(zsName, zipName, fileName string) error { zipFile, err := os.OpenFile(filepath.Join("releases", zipName), os.O_RDWR|os.O_CREATE, 0600) if err != nil { return err } defer zipFile.Close() zw := zip.NewWriter(zipFile) defer zw.Close() err = addFileToZip(zw, zsName, fileName) if err != nil { return err } err = addFileToZip(zw, "LICENSE.txt", "LICENSE.txt") if err != nil { return err } err = addFileToZip(zw, "docs/readmezip.txt", "README.txt") return err } func addFileToZip(zipFile *zip.Writer, filepath, filename string) error { zsFile, err := os.Open(filepath) if err != nil { return err } defer zsFile.Close() stat, err := zsFile.Stat() if err != nil { return err } fh, err := zip.FileInfoHeader(stat) if err != nil { return err } fh.Name = filename fh.Method = zip.Deflate w, err := zipFile.CreateHeader(fh) if err != nil { return err } _, err = io.Copy(w, zsFile) return err } func cmdClean() error { for _, dir := range []string{"bin", "releases"} { err := os.RemoveAll(dir) if err != nil { return err } } return nil } func cmdHelp() { fmt.Println(`Usage: go run tools/build.go [-v] COMMAND Options: -v Verbose output. Commands: build Build the software for local computer. check Check current working state: execute tests, static analysis tools, extra files, ... Is automatically done when releasing the software. clean Remove all build and release directories. help Outputs this text. manual Create a ZIP file with all manual zettel release Create the software for various platforms and put them in appropriate named ZIP files. version Print the current version of the software. All commands can be abbreviated as long as they remain unique.`) } var ( verbose bool ) func main() { flag.BoolVar(&verbose, "v", false, "Verbose output") flag.Parse() var err error args := flag.Args() if len(args) < 1 { cmdHelp() } else { switch args[0] { case "b", "bu", "bui", "buil", "build": err = cmdBuild() case "m", "ma", "man", "manu", "manua", "manual": err = cmdManual() case "r", "re", "rel", "rele", "relea", "releas", "release": err = cmdRelease() case "cl", "cle", "clea", "clean": err = cmdClean() case "v", "ve", "ver", "vers", "versi", "versio", "version": fmt.Print(getVersion()) case "ch", "che", "chec", "check": err = cmdCheck() case "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.
|
| < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < |
Changes to usecase/authenticate.go.
1 | //----------------------------------------------------------------------------- | | | < < < > | < < | | | > > > > > > < > | | < > | < < < | < | | < | | < | < < < | < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 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) 2020-2021 Detlef Stern // // This file is part of zettelstore. // // Zettelstore is licensed under the latest version of the EUPL (European Union // Public License). Please see file LICENSE.txt for your rights and obligations // under this license. //----------------------------------------------------------------------------- // Package usecase provides (business) use cases for the zettelstore. package usecase import ( "context" "math/rand" "time" "zettelstore.de/z/auth" "zettelstore.de/z/auth/cred" "zettelstore.de/z/domain/id" "zettelstore.de/z/domain/meta" "zettelstore.de/z/search" ) // AuthenticatePort is the interface used by this use case. type AuthenticatePort interface { GetMeta(ctx context.Context, zid id.Zid) (*meta.Meta, error) SelectMeta(ctx context.Context, s *search.Search) ([]*meta.Meta, error) } // Authenticate is the data for this use case. type Authenticate struct { token auth.TokenManager port AuthenticatePort ucGetUser GetUser } // NewAuthenticate creates a new use case. func NewAuthenticate(token auth.TokenManager, authz auth.AuthzManager, port AuthenticatePort) Authenticate { return Authenticate{ token: token, port: port, ucGetUser: NewGetUser(authz, port), } } // Run executes the use case. func (uc Authenticate) Run(ctx context.Context, ident, credential string, d time.Duration, k auth.TokenKind) ([]byte, error) { identMeta, err := uc.ucGetUser.Run(ctx, ident) defer addDelay(time.Now(), 500*time.Millisecond, 100*time.Millisecond) if identMeta == nil || err != nil { compensateCompare() return nil, err } if hashCred, ok := identMeta.Get(meta.KeyCredential); ok { ok, err := cred.CompareHashAndCredential(hashCred, identMeta.Zid, ident, credential) if err != nil { return nil, err } if ok { token, err := uc.token.GetToken(identMeta, d, k) if err != nil { return nil, err } return token, nil } return nil, nil } 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) } } |
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 | //----------------------------------------------------------------------------- // Copyright (c) 2021 Detlef Stern // // This file is part of zettelstore. // // Zettelstore is licensed under the latest version of the EUPL (European Union // Public License). Please see file LICENSE.txt for your rights and obligations // under this license. //----------------------------------------------------------------------------- // Package usecase provides (business) use cases for the Zettelstore. package usecase import ( "context" "zettelstore.de/z/domain/id" "zettelstore.de/z/domain/meta" ) // ZettelContextPort is the interface used by this use case. type ZettelContextPort interface { // GetMeta retrieves just the meta data of a specific zettel. GetMeta(ctx context.Context, zid id.Zid) (*meta.Meta, error) } // ZettelContext is the data for this use case. type ZettelContext struct { port ZettelContextPort } // NewZettelContext creates a new use case. func NewZettelContext(port ZettelContextPort) ZettelContext { return ZettelContext{port: port} } // ZettelContextDirection determines the way, the context is calculated. type ZettelContextDirection int // Constant values for ZettelContextDirection const ( _ ZettelContextDirection = iota ZettelContextForward // Traverse all forwarding links ZettelContextBackward // Traverse all backwaring links ZettelContextBoth // Traverse both directions ) // ParseZCDirection returns a direction value for a given string. func ParseZCDirection(s string) ZettelContextDirection { switch s { case "backward": return ZettelContextBackward case "forward": return ZettelContextForward } return ZettelContextBoth } // Run executes the use case. func (uc ZettelContext) Run(ctx context.Context, zid id.Zid, dir ZettelContextDirection, depth, limit int) (result []*meta.Meta, err error) { start, err := uc.port.GetMeta(ctx, zid) if err != nil { return nil, err } tasks := ztlCtx{depth: depth} uc.addInitialTasks(ctx, &tasks, start) visited := id.NewSet() isBackward := dir == ZettelContextBoth || dir == ZettelContextBackward isForward := dir == ZettelContextBoth || dir == ZettelContextForward for !tasks.empty() { m, curDepth := tasks.pop() if _, ok := visited[m.Zid]; ok { continue } visited[m.Zid] = true result = append(result, m) if limit > 0 && len(result) > limit { // start is the first element of result break } curDepth++ for _, p := range m.PairsRest(true) { if p.Key == meta.KeyBackward { if isBackward { uc.addIDSet(ctx, &tasks, curDepth, p.Value) } continue } if p.Key == meta.KeyForward { if isForward { uc.addIDSet(ctx, &tasks, curDepth, p.Value) } continue } if p.Key != meta.KeyBack { hasInverse := meta.Inverse(p.Key) != "" if (!hasInverse || !isBackward) && (hasInverse || !isForward) { continue } if t := meta.Type(p.Key); t == meta.TypeID { uc.addID(ctx, &tasks, curDepth, p.Value) } else if t == meta.TypeIDSet { uc.addIDSet(ctx, &tasks, curDepth, p.Value) } } } } return result, nil } func (uc ZettelContext) addInitialTasks(ctx context.Context, tasks *ztlCtx, start *meta.Meta) { tasks.add(start, 0) } func (uc ZettelContext) addID(ctx context.Context, tasks *ztlCtx, depth int, value string) { if zid, err := id.Parse(value); err == nil { if m, err := uc.port.GetMeta(ctx, zid); err == nil { tasks.add(m, depth) } } } func (uc ZettelContext) addIDSet(ctx context.Context, tasks *ztlCtx, depth int, value string) { for _, val := range meta.ListFromValue(value) { uc.addID(ctx, tasks, depth, val) } } type ztlCtxTask struct { next *ztlCtxTask meta *meta.Meta depth int } type ztlCtx struct { first *ztlCtxTask last *ztlCtxTask depth int } func (zc *ztlCtx) add(m *meta.Meta, depth int) { if zc.depth > 0 && depth > zc.depth { return } task := &ztlCtxTask{next: nil, meta: m, depth: depth} if zc.first == nil { zc.first = task zc.last = task } else { zc.last.next = task zc.last = task } } func (zc *ztlCtx) empty() bool { return zc.first == nil } func (zc *ztlCtx) pop() (*meta.Meta, int) { task := zc.first if task == nil { return nil, -1 } zc.first = task.next if zc.first == nil { zc.last = nil } return task.meta, task.depth } |
Added usecase/copy_zettel.go.
> > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 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-2021 Detlef Stern // // This file is part of zettelstore. // // Zettelstore is licensed under the latest version of the EUPL (European Union // Public License). Please see file LICENSE.txt for your rights and obligations // under this license. //----------------------------------------------------------------------------- // Package usecase provides (business) use cases for the zettelstore. package usecase import ( "zettelstore.de/z/domain" "zettelstore.de/z/domain/meta" "zettelstore.de/z/strfun" ) // CopyZettel is the data for this use case. type CopyZettel struct{} // NewCopyZettel creates a new use case. func NewCopyZettel() CopyZettel { return CopyZettel{} } // Run executes the use case. func (uc CopyZettel) Run(origZettel domain.Zettel) domain.Zettel { m := origZettel.Meta.Clone() if title, ok := m.Get(meta.KeyTitle); ok { if len(title) > 0 { title = "Copy of " + title } else { title = "Copy" } m.Set(meta.KeyTitle, title) } content := strfun.TrimSpaceRight(origZettel.Content.AsString()) return domain.Zettel{Meta: m, Content: domain.Content(content)} } |
Changes to usecase/create_zettel.go.
1 | //----------------------------------------------------------------------------- | | | < < < > < < | | | | | < | < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < | | | > > > > > > > | | < < | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 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-2021 Detlef Stern // // This file is part of zettelstore. // // Zettelstore is licensed under the latest version of the EUPL (European Union // Public License). Please see file LICENSE.txt for your rights and obligations // under this license. //----------------------------------------------------------------------------- // Package usecase provides (business) use cases for the zettelstore. package usecase import ( "context" "zettelstore.de/z/config" "zettelstore.de/z/domain" "zettelstore.de/z/domain/id" "zettelstore.de/z/domain/meta" "zettelstore.de/z/strfun" ) // CreateZettelPort is the interface used by this use case. type CreateZettelPort interface { // CreateZettel creates a new zettel. CreateZettel(ctx context.Context, zettel domain.Zettel) (id.Zid, error) } // CreateZettel is the data for this use case. type CreateZettel struct { rtConfig config.Config port CreateZettelPort } // NewCreateZettel creates a new use case. func NewCreateZettel(rtConfig config.Config, port CreateZettelPort) CreateZettel { return CreateZettel{ rtConfig: rtConfig, port: port, } } // 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 } if title, ok := m.Get(meta.KeyTitle); !ok || title == "" { m.Set(meta.KeyTitle, uc.rtConfig.GetDefaultTitle()) } if role, ok := m.Get(meta.KeyRole); !ok || role == "" { m.Set(meta.KeyRole, uc.rtConfig.GetDefaultRole()) } if syntax, ok := m.Get(meta.KeySyntax); !ok || syntax == "" { m.Set(meta.KeySyntax, uc.rtConfig.GetDefaultSyntax()) } m.YamlSep = uc.rtConfig.GetYAMLHeader() zettel.Content = domain.Content(strfun.TrimSpaceRight(zettel.Content.AsString())) return uc.port.CreateZettel(ctx, zettel) } |
Changes to usecase/delete_zettel.go.
1 | //----------------------------------------------------------------------------- | | | < < < > < | | < | | | | < < | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 | //----------------------------------------------------------------------------- // Copyright (c) 2020 Detlef Stern // // This file is part of zettelstore. // // Zettelstore is licensed under the latest version of the EUPL (European Union // Public License). Please see file LICENSE.txt for your rights and obligations // under this license. //----------------------------------------------------------------------------- // Package usecase provides (business) use cases for the zettelstore. package usecase import ( "context" "zettelstore.de/z/domain/id" ) // DeleteZettelPort is the interface used by this use case. type DeleteZettelPort interface { // DeleteZettel removes the zettel from the place. DeleteZettel(ctx context.Context, zid id.Zid) error } // DeleteZettel is the data for this use case. type DeleteZettel struct { port DeleteZettelPort } // NewDeleteZettel creates a new use case. func NewDeleteZettel(port DeleteZettelPort) DeleteZettel { return DeleteZettel{port: port} } // Run executes the use case. func (uc DeleteZettel) Run(ctx context.Context, zid id.Zid) error { return uc.port.DeleteZettel(ctx, zid) } |
Deleted usecase/evaluate.go.
|
| < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < |
Added usecase/folge_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 | //----------------------------------------------------------------------------- // Copyright (c) 2020 Detlef Stern // // This file is part of zettelstore. // // Zettelstore is licensed under the latest version of the EUPL (European Union // Public License). Please see file LICENSE.txt for your rights and obligations // under this license. //----------------------------------------------------------------------------- // Package usecase provides (business) use cases for the zettelstore. package usecase import ( "zettelstore.de/z/config" "zettelstore.de/z/domain" "zettelstore.de/z/domain/id" "zettelstore.de/z/domain/meta" ) // FolgeZettel is the data for this use case. type FolgeZettel struct { rtConfig config.Config } // NewFolgeZettel creates a new use case. func NewFolgeZettel(rtConfig config.Config) FolgeZettel { return FolgeZettel{rtConfig} } // Run executes the use case. func (uc FolgeZettel) Run(origZettel domain.Zettel) domain.Zettel { origMeta := origZettel.Meta m := meta.New(id.Invalid) if title, ok := origMeta.Get(meta.KeyTitle); ok { if len(title) > 0 { title = "Folge of " + title } else { title = "Folge" } m.Set(meta.KeyTitle, title) } m.Set(meta.KeyRole, config.GetRole(origMeta, uc.rtConfig)) m.Set(meta.KeyTags, origMeta.GetDefault(meta.KeyTags, "")) m.Set(meta.KeySyntax, uc.rtConfig.GetDefaultSyntax()) m.Set(meta.KeyPrecursor, origMeta.Zid.String()) return domain.Zettel{Meta: m, Content: ""} } |
Deleted usecase/get_all_zettel.go.
|
| < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < |
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 40 | //----------------------------------------------------------------------------- // Copyright (c) 2020 Detlef Stern // // This file is part of zettelstore. // // Zettelstore is licensed under the latest version of the EUPL (European Union // Public License). Please see file LICENSE.txt for your rights and obligations // under this license. //----------------------------------------------------------------------------- // Package usecase provides (business) use cases for the zettelstore. package usecase import ( "context" "zettelstore.de/z/domain/id" "zettelstore.de/z/domain/meta" ) // GetMetaPort is the interface used by this use case. type GetMetaPort interface { // GetMeta retrieves just the meta data of a specific zettel. GetMeta(ctx context.Context, zid id.Zid) (*meta.Meta, error) } // GetMeta is the data for this use case. type GetMeta struct { port GetMetaPort } // NewGetMeta creates a new use case. func NewGetMeta(port GetMetaPort) GetMeta { return GetMeta{port: port} } // Run executes the use case. func (uc GetMeta) Run(ctx context.Context, zid id.Zid) (*meta.Meta, error) { return uc.port.GetMeta(ctx, zid) } |
Deleted usecase/get_special_zettel.go.
|
| < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < |
Changes to usecase/get_user.go.
1 | //----------------------------------------------------------------------------- | | | < < < > < | | | | < | | | | | > > > > | > > | | | | < | | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 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-2021 Detlef Stern // // This file is part of zettelstore. // // Zettelstore is licensed under the latest version of the EUPL (European Union // Public License). Please see file LICENSE.txt for your rights and obligations // under this license. //----------------------------------------------------------------------------- // Package usecase provides (business) use cases for the zettelstore. package usecase import ( "context" "zettelstore.de/z/auth" "zettelstore.de/z/domain/id" "zettelstore.de/z/domain/meta" "zettelstore.de/z/place" "zettelstore.de/z/search" ) // Use case: return user identified by meta key ident. // --------------------------------------------------- // 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, s *search.Search) ([]*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 = place.NoEnrichContext(ctx) // It is important to try first with the owner. First, because another user // could give herself the same ''ident''. Second, in most cases the owner // will authenticate. identMeta, err := uc.port.GetMeta(ctx, uc.authz.Owner()) if err == nil && identMeta.GetDefault(meta.KeyUserID, "") == ident { if role, ok := identMeta.Get(meta.KeyRole); !ok || role != meta.ValueRoleUser { return nil, nil } return identMeta, nil } // Owner was not found or has another ident. Try via list search. var s *search.Search s = s.AddExpr(meta.KeyRole, meta.ValueRoleUser) s = s.AddExpr(meta.KeyUserID, ident) metaList, err := uc.port.SelectMeta(ctx, s) if err != nil { return nil, err } if len(metaList) < 1 { return nil, nil } 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(place.NoEnrichContext(ctx), zid) if err != nil { return nil, err } if val, ok := userMeta.Get(meta.KeyUserID); !ok || val != ident { return nil, nil } return userMeta, nil } |
Changes to usecase/get_zettel.go.
1 | //----------------------------------------------------------------------------- | | | < < < > | | | | | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 | //----------------------------------------------------------------------------- // Copyright (c) 2020 Detlef Stern // // This file is part of zettelstore. // // Zettelstore is licensed under the latest version of the EUPL (European Union // Public License). Please see file LICENSE.txt for your rights and obligations // under this license. //----------------------------------------------------------------------------- // Package usecase provides (business) use cases for the zettelstore. package usecase import ( "context" "zettelstore.de/z/domain" "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/list_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 | //----------------------------------------------------------------------------- // Copyright (c) 2020-2021 Detlef Stern // // This file is part of zettelstore. // // Zettelstore is licensed under the latest version of the EUPL (European Union // Public License). Please see file LICENSE.txt for your rights and obligations // under this license. //----------------------------------------------------------------------------- // Package usecase provides (business) use cases for the zettelstore. package usecase import ( "context" "zettelstore.de/z/domain/meta" "zettelstore.de/z/search" ) // ListMetaPort is the interface used by this use case. type ListMetaPort interface { // SelectMeta returns all zettel meta data that match the selection criteria. SelectMeta(ctx context.Context, s *search.Search) ([]*meta.Meta, error) } // ListMeta is the data for this use case. type ListMeta struct { port ListMetaPort } // NewListMeta creates a new use case. func NewListMeta(port ListMetaPort) ListMeta { return ListMeta{port: port} } // Run executes the use case. func (uc ListMeta) Run(ctx context.Context, s *search.Search) ([]*meta.Meta, error) { return uc.port.SelectMeta(ctx, s) } |
Added usecase/list_role.go.
> > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 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-2021 Detlef Stern // // This file is part of zettelstore. // // Zettelstore is licensed under the latest version of the EUPL (European Union // Public License). Please see file LICENSE.txt for your rights and obligations // under this license. //----------------------------------------------------------------------------- // Package usecase provides (business) use cases for the zettelstore. package usecase import ( "context" "sort" "zettelstore.de/z/domain/meta" "zettelstore.de/z/place" "zettelstore.de/z/search" ) // ListRolePort is the interface used by this use case. type ListRolePort interface { // SelectMeta returns all zettel meta data that match the selection criteria. SelectMeta(ctx context.Context, s *search.Search) ([]*meta.Meta, error) } // ListRole is the data for this use case. type ListRole struct { port ListRolePort } // NewListRole creates a new use case. func NewListRole(port ListRolePort) ListRole { return ListRole{port: port} } // Run executes the use case. func (uc ListRole) Run(ctx context.Context) ([]string, error) { metas, err := uc.port.SelectMeta(place.NoEnrichContext(ctx), nil) if err != nil { return nil, err } roles := make(map[string]bool, 8) for _, m := range metas { if role, ok := m.Get(meta.KeyRole); ok && role != "" { roles[role] = true } } result := make([]string, 0, len(roles)) for role := range roles { result = append(result, role) } sort.Strings(result) return result, nil } |
Added usecase/list_tags.go.
> > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 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) 2020-2021 Detlef Stern // // This file is part of zettelstore. // // Zettelstore is licensed under the latest version of the EUPL (European Union // Public License). Please see file LICENSE.txt for your rights and obligations // under this license. //----------------------------------------------------------------------------- // Package usecase provides (business) use cases for the zettelstore. package usecase import ( "context" "zettelstore.de/z/domain/meta" "zettelstore.de/z/place" "zettelstore.de/z/search" ) // ListTagsPort is the interface used by this use case. type ListTagsPort interface { // SelectMeta returns all zettel meta data that match the selection criteria. SelectMeta(ctx context.Context, s *search.Search) ([]*meta.Meta, error) } // ListTags is the data for this use case. type ListTags struct { port ListTagsPort } // NewListTags creates a new use case. func NewListTags(port ListTagsPort) ListTags { return ListTags{port: port} } // TagData associates tags with a list of all zettel meta that use this tag type TagData map[string][]*meta.Meta // Run executes the use case. func (uc ListTags) Run(ctx context.Context, minCount int) (TagData, error) { metas, err := uc.port.SelectMeta(place.NoEnrichContext(ctx), nil) if err != nil { return nil, err } result := make(TagData) for _, m := range metas { if tl, ok := m.GetList(meta.KeyTags); ok && len(tl) > 0 { for _, t := range tl { result[t] = append(result[t], m) } } } if minCount > 1 { for t, ms := range result { if len(ms) < minCount { delete(result, t) } } } return result, nil } |
Deleted usecase/lists.go.
|
| < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < |
Added usecase/new_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-2021 Detlef Stern // // This file is part of zettelstore. // // Zettelstore is licensed under the latest version of the EUPL (European Union // Public License). Please see file LICENSE.txt for your rights and obligations // under this license. //----------------------------------------------------------------------------- // Package usecase provides (business) use cases for the zettelstore. package usecase import ( "zettelstore.de/z/domain" "zettelstore.de/z/strfun" ) // NewZettel is the data for this use case. type NewZettel struct{} // NewNewZettel creates a new use case. func NewNewZettel() NewZettel { return NewZettel{} } // Run executes the use case. func (uc NewZettel) Run(origZettel domain.Zettel) domain.Zettel { m := origZettel.Meta.Clone() const prefix = "new-" for _, pair := range m.PairsRest(false) { if key := pair.Key; len(key) > len(prefix) && key[0:len(prefix)] == prefix { m.Set(key[len(prefix):], pair.Value) m.Delete(key) } } content := strfun.TrimSpaceRight(origZettel.Content.AsString()) return domain.Zettel{Meta: m, Content: domain.Content(content)} } |
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 55 | //----------------------------------------------------------------------------- // Copyright (c) 2021 Detlef Stern // // This file is part of zettelstore. // // Zettelstore is licensed under the latest version of the EUPL (European Union // Public License). Please see file LICENSE.txt for your rights and obligations // under this license. //----------------------------------------------------------------------------- // Package usecase provides (business) use cases for the Zettelstore. package usecase import ( "context" "zettelstore.de/z/collect" "zettelstore.de/z/domain/id" "zettelstore.de/z/domain/meta" ) // ZettelOrderPort is the interface used by this use case. type ZettelOrderPort interface { // GetMeta retrieves just the meta data of a specific zettel. GetMeta(ctx context.Context, zid id.Zid) (*meta.Meta, error) } // ZettelOrder is the data for this use case. type ZettelOrder struct { port ZettelOrderPort parseZettel ParseZettel } // NewZettelOrder creates a new use case. func NewZettelOrder(port ZettelOrderPort, parseZettel ParseZettel) ZettelOrder { return ZettelOrder{port: port, parseZettel: parseZettel} } // Run executes the use case. func (uc ZettelOrder) Run( ctx context.Context, zid id.Zid, syntax string, ) (start *meta.Meta, result []*meta.Meta, err error) { zn, err := uc.parseZettel.Run(ctx, zid, syntax) if err != nil { return nil, nil, err } for _, ref := range collect.Order(zn) { if zid, err := id.Parse(ref.URL.Path); err == nil { if m, err := uc.port.GetMeta(ctx, zid); err == nil { result = append(result, m) } } } return zn.Meta, result, nil } |
Changes to usecase/parse_zettel.go.
1 | //----------------------------------------------------------------------------- | | | < < < > | | > | | | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 | //----------------------------------------------------------------------------- // Copyright (c) 2020 Detlef Stern // // This file is part of zettelstore. // // Zettelstore is licensed under the latest version of the EUPL (European Union // Public License). Please see file LICENSE.txt for your rights and obligations // under this license. //----------------------------------------------------------------------------- // Package usecase provides (business) use cases for the zettelstore. package usecase import ( "context" "zettelstore.de/z/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(zettel, syntax, uc.rtConfig), nil } |
Deleted usecase/query.go.
|
| < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < |
Deleted usecase/refresh.go.
|
| < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < |
Deleted usecase/reindex.go.
|
| < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < |
Changes to usecase/rename_zettel.go.
1 | //----------------------------------------------------------------------------- | | | < < < > | | | < > | > > < | | | | | | | | | | < < | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 | //----------------------------------------------------------------------------- // Copyright (c) 2020-2021 Detlef Stern // // This file is part of zettelstore. // // Zettelstore is licensed under the latest version of the EUPL (European Union // Public License). Please see file LICENSE.txt for your rights and obligations // under this license. //----------------------------------------------------------------------------- // Package usecase provides (business) use cases for the zettelstore. package usecase import ( "context" "zettelstore.de/z/domain/id" "zettelstore.de/z/domain/meta" "zettelstore.de/z/place" ) // RenameZettelPort is the interface used by this use case. type RenameZettelPort interface { // GetMeta retrieves just the meta data of a specific zettel. 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 { port RenameZettelPort } // ErrZidInUse is returned if the zettel id is not appropriate for the place operation. type ErrZidInUse struct{ Zid id.Zid } func (err *ErrZidInUse) Error() string { return "Zettel id already in use: " + err.Zid.String() } // NewRenameZettel creates a new use case. func NewRenameZettel(port RenameZettelPort) RenameZettel { return RenameZettel{port: port} } // Run executes the use case. func (uc RenameZettel) Run(ctx context.Context, curZid, newZid id.Zid) error { noEnrichCtx := place.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} } return uc.port.RenameZettel(ctx, curZid, newZid) } |
Added usecase/search.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) 2020-2021 Detlef Stern // // This file is part of zettelstore. // // Zettelstore is licensed under the latest version of the EUPL (European Union // Public License). Please see file LICENSE.txt for your rights and obligations // under this license. //----------------------------------------------------------------------------- // Package usecase provides (business) use cases for the zettelstore. package usecase import ( "context" "zettelstore.de/z/domain/meta" "zettelstore.de/z/place" "zettelstore.de/z/search" ) // SearchPort is the interface used by this use case. type SearchPort interface { // SelectMeta returns all zettel meta data that match the selection criteria. SelectMeta(ctx context.Context, s *search.Search) ([]*meta.Meta, error) } // Search is the data for this use case. type Search struct { port SearchPort } // NewSearch creates a new use case. func NewSearch(port SearchPort) Search { return Search{port: port} } // Run executes the use case. func (uc Search) Run(ctx context.Context, s *search.Search) ([]*meta.Meta, error) { if !s.HasComputedMetaKey() { ctx = place.NoEnrichContext(ctx) } return uc.port.SelectMeta(ctx, s) } |
Changes to usecase/update_zettel.go.
1 | //----------------------------------------------------------------------------- | | | < < < > < | | | | | | | < | | | | < < < < < < < | < | < | < | < < | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 | //----------------------------------------------------------------------------- // Copyright (c) 2020-2021 Detlef Stern // // This file is part of zettelstore. // // Zettelstore is licensed under the latest version of the EUPL (European Union // Public License). Please see file LICENSE.txt for your rights and obligations // under this license. //----------------------------------------------------------------------------- // Package usecase provides (business) use cases for the zettelstore. package usecase import ( "context" "zettelstore.de/z/domain" "zettelstore.de/z/domain/id" "zettelstore.de/z/domain/meta" "zettelstore.de/z/place" "zettelstore.de/z/strfun" ) // UpdateZettelPort is the interface used by this use case. type UpdateZettelPort interface { // GetZettel retrieves a specific zettel. 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 { port UpdateZettelPort } // NewUpdateZettel creates a new use case. func NewUpdateZettel(port UpdateZettelPort) UpdateZettel { return UpdateZettel{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(place.NoEnrichContext(ctx), m.Zid) if err != nil { return err } if zettel.Equal(oldZettel, false) { return nil } m.SetNow(meta.KeyModified) m.YamlSep = oldZettel.Meta.YamlSep if m.Zid == id.ConfigurationZid { m.Set(meta.KeySyntax, meta.ValueSyntaxNone) } if !hasContent { zettel.Content = domain.Content(strfun.TrimSpaceRight(oldZettel.Content.AsString())) } return uc.port.UpdateZettel(ctx, zettel) } |
Deleted usecase/usecase.go.
|
| < < < < < < < < < < < < < < < |
Deleted usecase/version.go.
|
| < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < |
Deleted web/adapter/adapter.go.
|
| < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < |
Changes to web/adapter/api/api.go.
1 | //----------------------------------------------------------------------------- | | | < < < < < < | | < < < > < | | < | < > < | > > > | | | | | | < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 | //----------------------------------------------------------------------------- // Copyright (c) 2021 Detlef Stern // // This file is part of zettelstore. // // Zettelstore is licensed under the latest version of the EUPL (European Union // Public License). Please see file LICENSE.txt for your rights and obligations // under this license. //----------------------------------------------------------------------------- // Package api provides api handlers for web requests. package api import ( "context" "time" "zettelstore.de/z/auth" "zettelstore.de/z/config" "zettelstore.de/z/domain/meta" "zettelstore.de/z/kernel" "zettelstore.de/z/web/server" ) // API holds all data and methods for delivering API call results. type API struct { b server.Builder rtConfig config.Config authz auth.AuthzManager token auth.TokenManager auth server.Auth tokenLifetime time.Duration } // New creates a new API object. func New(b server.Builder, authz auth.AuthzManager, token auth.TokenManager, auth server.Auth, rtConfig config.Config) *API { api := &API{ b: b, authz: authz, token: token, auth: auth, rtConfig: rtConfig, tokenLifetime: kernel.Main.GetConfig(kernel.WebService, kernel.WebTokenLifetimeAPI).(time.Duration), } return api } // GetURLPrefix returns the configured URL prefix of the web server. func (api *API) GetURLPrefix() string { return api.b.GetURLPrefix() } // NewURLBuilder creates a new URL builder object with the given key. func (api *API) NewURLBuilder(key byte) server.URLBuilder { return api.b.NewURLBuilder(key) } func (api *API) getAuthData(ctx context.Context) *server.AuthData { return api.auth.GetAuthData(ctx) } func (api *API) withAuth() bool { return api.authz.WithAuth() } func (api *API) getToken(ident *meta.Meta) ([]byte, error) { return api.token.GetToken(ident, api.tokenLifetime, auth.KindJSON) } |
Deleted web/adapter/api/command.go.
|
| < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < |
Added web/adapter/api/content_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 | //----------------------------------------------------------------------------- // Copyright (c) 2020 Detlef Stern // // This file is part of zettelstore. // // Zettelstore is licensed under the latest version of the EUPL (European Union // Public License). Please see file LICENSE.txt for your rights and obligations // under this license. //----------------------------------------------------------------------------- // Package api provides api handlers for web requests. package api const plainText = "text/plain; charset=utf-8" var mapFormat2CT = map[string]string{ "html": "text/html; charset=utf-8", "native": plainText, "json": "application/json", "djson": "application/json", "text": plainText, "zmk": plainText, "raw": plainText, // In some cases... } func format2ContentType(format string) string { ct, ok := mapFormat2CT[format] if !ok { return "application/octet-stream" } return ct } var mapSyntax2CT = map[string]string{ "css": "text/css; charset=utf-8", "gif": "image/gif", "html": "text/html; charset=utf-8", "jpeg": "image/jpeg", "jpg": "image/jpeg", "js": "text/javascript; charset=utf-8", "pdf": "application/pdf", "png": "image/png", "svg": "image/svg+xml", "xml": "text/xml; charset=utf-8", "zmk": "text/x-zmk; charset=utf-8", "plain": plainText, "text": plainText, "markdown": "text/markdown; charset=utf-8", "md": "text/markdown; charset=utf-8", "mustache": plainText, //"graphviz": "text/vnd.graphviz; charset=utf-8", } func syntax2contentType(syntax string) (string, bool) { contentType, ok := mapSyntax2CT[syntax] return contentType, ok } |
Deleted web/adapter/api/create_zettel.go.
|
| < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < |
Deleted web/adapter/api/delete_zettel.go.
|
| < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < |
Deleted web/adapter/api/get_data.go.
|
| < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < |
Added web/adapter/api/get_links.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-2021 Detlef Stern // // This file is part of zettelstore. // // Zettelstore is licensed under the latest version of the EUPL (European Union // Public License). Please see file LICENSE.txt for your rights and obligations // under this license. //----------------------------------------------------------------------------- // Package api provides api handlers for web requests. package api import ( "encoding/json" "net/http" "strconv" "zettelstore.de/z/ast" "zettelstore.de/z/collect" "zettelstore.de/z/domain/id" "zettelstore.de/z/usecase" "zettelstore.de/z/web/adapter" ) type jsonGetLinks struct { ID string `json:"id"` URL string `json:"url"` Links struct { Incoming []jsonIDURL `json:"incoming"` Outgoing []jsonIDURL `json:"outgoing"` Local []string `json:"local"` External []string `json:"external"` } `json:"links"` Images struct { Outgoing []jsonIDURL `json:"outgoing"` Local []string `json:"local"` External []string `json:"external"` } `json:"images"` Cites []string `json:"cites"` } // MakeGetLinksHandler creates a new API handler to return links to other material. func (api *API) MakeGetLinksHandler(parseZettel usecase.ParseZettel) http.HandlerFunc { return func(w http.ResponseWriter, r *http.Request) { zid, err := id.Parse(r.URL.Path[1:]) if err != nil { http.NotFound(w, r) return } ctx := r.Context() q := r.URL.Query() zn, err := parseZettel.Run(ctx, zid, q.Get("syntax")) if err != nil { adapter.ReportUsecaseError(w, err) return } summary := collect.References(zn) kind := getKindFromValue(q.Get("kind")) matter := getMatterFromValue(q.Get("matter")) if !validKindMatter(kind, matter) { adapter.BadRequest(w, "Invalid kind/matter") return } outData := jsonGetLinks{ ID: zid.String(), URL: api.NewURLBuilder('z').SetZid(zid).String(), } if kind&kindLink != 0 { api.setupLinkJSONRefs(summary, matter, &outData) } if kind&kindImage != 0 { api.setupImageJSONRefs(summary, matter, &outData) } if kind&kindCite != 0 { outData.Cites = stringCites(summary.Cites) } w.Header().Set(adapter.ContentType, format2ContentType("json")) enc := json.NewEncoder(w) enc.SetEscapeHTML(false) enc.Encode(&outData) } } func (api *API) setupLinkJSONRefs(summary collect.Summary, matter matterType, outData *jsonGetLinks) { if matter&matterIncoming != 0 { outData.Links.Incoming = []jsonIDURL{} } zetRefs, locRefs, extRefs := collect.DivideReferences(summary.Links) if matter&matterOutgoing != 0 { outData.Links.Outgoing = api.idURLRefs(zetRefs) } if matter&matterLocal != 0 { outData.Links.Local = stringRefs(locRefs) } if matter&matterExternal != 0 { outData.Links.External = stringRefs(extRefs) } } func (api *API) setupImageJSONRefs(summary collect.Summary, matter matterType, outData *jsonGetLinks) { zetRefs, locRefs, extRefs := collect.DivideReferences(summary.Images) if matter&matterOutgoing != 0 { outData.Images.Outgoing = api.idURLRefs(zetRefs) } if matter&matterLocal != 0 { outData.Images.Local = stringRefs(locRefs) } if matter&matterExternal != 0 { outData.Images.External = stringRefs(extRefs) } } func (api *API) idURLRefs(refs []*ast.Reference) []jsonIDURL { result := make([]jsonIDURL, 0, len(refs)) for _, ref := range refs { path := ref.URL.Path ub := api.NewURLBuilder('z').AppendPath(path) if fragment := ref.URL.Fragment; len(fragment) > 0 { ub.SetFragment(fragment) } result = append(result, jsonIDURL{ID: path, URL: ub.String()}) } return result } func stringRefs(refs []*ast.Reference) []string { result := make([]string, 0, len(refs)) for _, ref := range refs { result = append(result, ref.String()) } return result } func stringCites(cites []*ast.CiteNode) []string { mapKey := make(map[string]bool) result := make([]string, 0, len(cites)) for _, cn := range cites { if _, ok := mapKey[cn.Key]; !ok { mapKey[cn.Key] = true result = append(result, cn.Key) } } return result } type kindType int const ( _ kindType = 1 << iota kindLink kindImage kindCite ) var mapKind = map[string]kindType{ "": kindLink | kindImage | kindCite, "link": kindLink, "image": kindImage, "cite": kindCite, "both": kindLink | kindImage, "all": kindLink | kindImage | kindCite, } func getKindFromValue(value string) kindType { if k, ok := mapKind[value]; ok { return k } if n, err := strconv.Atoi(value); err == nil && n > 0 { return kindType(n) } return 0 } type matterType int const ( _ matterType = 1 << iota matterIncoming matterOutgoing matterLocal matterExternal ) var mapMatter = map[string]matterType{ "": matterIncoming | matterOutgoing | matterLocal | matterExternal, "incoming": matterIncoming, "outgoing": matterOutgoing, "local": matterLocal, "external": matterExternal, "zettel": matterIncoming | matterOutgoing, "material": matterLocal | matterExternal, "all": matterIncoming | matterOutgoing | matterLocal | matterExternal, } func getMatterFromValue(value string) matterType { if m, ok := mapMatter[value]; ok { return m } if n, err := strconv.Atoi(value); err == nil && n > 0 { return matterType(n) } return 0 } func validKindMatter(kind kindType, matter matterType) bool { if kind == 0 { return false } if kind&kindLink != 0 { return matter != 0 } if kind&kindImage != 0 { if matter == 0 || matter == matterIncoming { return false } return true } if kind&kindCite != 0 { return matter == matterOutgoing } return false } |
Added web/adapter/api/get_order.go.
> > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 | //----------------------------------------------------------------------------- // Copyright (c) 2021 Detlef Stern // // This file is part of zettelstore. // // Zettelstore is licensed under the latest version of the EUPL (European Union // Public License). Please see file LICENSE.txt for your rights and obligations // under this license. //----------------------------------------------------------------------------- // Package api provides api handlers for web requests. package api import ( "net/http" "zettelstore.de/z/domain/id" "zettelstore.de/z/usecase" "zettelstore.de/z/web/adapter" ) // MakeGetOrderHandler creates a new API handler to return zettel references // of a given zettel. func (api *API) MakeGetOrderHandler(zettelOrder usecase.ZettelOrder) http.HandlerFunc { return func(w http.ResponseWriter, r *http.Request) { zid, err := id.Parse(r.URL.Path[1:]) if err != nil { http.NotFound(w, r) return } ctx := r.Context() q := r.URL.Query() start, metas, err := zettelOrder.Run(ctx, zid, q.Get("syntax")) if err != nil { adapter.ReportUsecaseError(w, err) return } api.writeMetaList(w, start, metas) } } |
Added web/adapter/api/get_role_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 | //----------------------------------------------------------------------------- // Copyright (c) 2020-2021 Detlef Stern // // This file is part of zettelstore. // // Zettelstore is licensed under the latest version of the EUPL (European Union // Public License). Please see file LICENSE.txt for your rights and obligations // under this license. //----------------------------------------------------------------------------- // Package api provides api handlers for web requests. package api import ( "fmt" "net/http" "zettelstore.de/z/encoder" "zettelstore.de/z/encoder/jsonenc" "zettelstore.de/z/usecase" "zettelstore.de/z/web/adapter" ) // MakeListRoleHandler creates a new HTTP handler for the use case "list some zettel". func (api *API) MakeListRoleHandler(listRole usecase.ListRole) http.HandlerFunc { return func(w http.ResponseWriter, r *http.Request) { roleList, err := listRole.Run(r.Context()) if err != nil { adapter.ReportUsecaseError(w, err) return } format := adapter.GetFormat(r, r.URL.Query(), encoder.GetDefaultFormat()) switch format { case "json": w.Header().Set(adapter.ContentType, format2ContentType(format)) renderListRoleJSON(w, roleList) default: adapter.BadRequest(w, fmt.Sprintf("Role list not available in format %q", format)) } } } func renderListRoleJSON(w http.ResponseWriter, roleList []string) { buf := encoder.NewBufWriter(w) buf.WriteString("{\"role-list\":[") first := true for _, role := range roleList { if first { buf.WriteByte('"') first = false } else { buf.WriteString("\",\"") } buf.Write(jsonenc.Escape(role)) } if !first { buf.WriteByte('"') } buf.WriteString("]}") buf.Flush() } |
Added web/adapter/api/get_tags_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 | //----------------------------------------------------------------------------- // Copyright (c) 2020-2021 Detlef Stern // // This file is part of zettelstore. // // Zettelstore is licensed under the latest version of the EUPL (European Union // Public License). Please see file LICENSE.txt for your rights and obligations // under this license. //----------------------------------------------------------------------------- // Package api provides api handlers for web requests. package api import ( "fmt" "net/http" "sort" "strconv" "zettelstore.de/z/encoder" "zettelstore.de/z/encoder/jsonenc" "zettelstore.de/z/usecase" "zettelstore.de/z/web/adapter" ) // MakeListTagsHandler creates a new HTTP handler for the use case "list some zettel". func (api *API) MakeListTagsHandler(listTags usecase.ListTags) http.HandlerFunc { return func(w http.ResponseWriter, r *http.Request) { iMinCount, _ := strconv.Atoi(r.URL.Query().Get("min")) tagData, err := listTags.Run(r.Context(), iMinCount) if err != nil { adapter.ReportUsecaseError(w, err) return } format := adapter.GetFormat(r, r.URL.Query(), encoder.GetDefaultFormat()) switch format { case "json": w.Header().Set(adapter.ContentType, format2ContentType(format)) renderListTagsJSON(w, tagData) default: adapter.BadRequest(w, fmt.Sprintf("Tags list not available in format %q", format)) } } } func renderListTagsJSON(w http.ResponseWriter, tagData usecase.TagData) { buf := encoder.NewBufWriter(w) tagList := make([]string, 0, len(tagData)) for tag := range tagData { tagList = append(tagList, tag) } sort.Strings(tagList) buf.WriteString("{\"tags\":{") first := true for _, tag := range tagList { if first { buf.WriteByte('"') first = false } else { buf.WriteString(",\"") } buf.Write(jsonenc.Escape(tag)) buf.WriteString("\":[") for i, meta := range tagData[tag] { if i > 0 { buf.WriteByte(',') } buf.WriteByte('"') buf.WriteString(meta.Zid.String()) buf.WriteByte('"') } buf.WriteString("]") } buf.WriteString("}}") buf.Flush() } |
Changes to web/adapter/api/get_zettel.go.
1 | //----------------------------------------------------------------------------- | | | < < < > | < < < < | | | | | | | < | | < < | | < | < < > | < < < < | < < < < < < < | | | | < | > > > > | < | < < > | > | > | < < < | | > > > > > > > > > > | | < < < < < < < | | < < | | < < < | < < < < < < < | < < < < | | > | | < < | < < < < < < < | < < < < < > | > > > | > > > > | > > | < < < | | | | < < < < | | | | < < < < < < | < | < < < | | < < < | > > > > | < < > > > | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 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-2021 Detlef Stern // // This file is part of zettelstore. // // Zettelstore is licensed under the latest version of the EUPL (European Union // Public License). Please see file LICENSE.txt for your rights and obligations // under this license. //----------------------------------------------------------------------------- // Package api provides api handlers for web requests. package api import ( "errors" "fmt" "net/http" "zettelstore.de/z/ast" "zettelstore.de/z/config" "zettelstore.de/z/domain/id" "zettelstore.de/z/domain/meta" "zettelstore.de/z/encoder" "zettelstore.de/z/place" "zettelstore.de/z/usecase" "zettelstore.de/z/web/adapter" ) // MakeGetZettelHandler creates a new HTTP handler to return a rendered zettel. func (api *API) MakeGetZettelHandler(parseZettel usecase.ParseZettel, getMeta usecase.GetMeta) http.HandlerFunc { return func(w http.ResponseWriter, r *http.Request) { zid, err := id.Parse(r.URL.Path[1:]) if err != nil { http.NotFound(w, r) return } ctx := r.Context() q := r.URL.Query() format := adapter.GetFormat(r, q, encoder.GetDefaultFormat()) if format == "raw" { ctx = place.NoEnrichContext(ctx) } zn, err := parseZettel.Run(ctx, zid, q.Get("syntax")) if err != nil { adapter.ReportUsecaseError(w, err) return } part := getPart(q, partZettel) if part == partUnknown { adapter.BadRequest(w, "Unknown _part parameter") return } switch format { case "json", "djson": w.Header().Set(adapter.ContentType, format2ContentType(format)) err = api.getWriteMetaZettelFunc(ctx, format, part, partZettel, getMeta)(w, zn) if err != nil { adapter.InternalServerError(w, "Write D/JSON", err) } return } env := encoder.Environment{ LinkAdapter: adapter.MakeLinkAdapter(ctx, api, 'z', getMeta, part.DefString(partZettel), format), ImageAdapter: adapter.MakeImageAdapter(ctx, api, getMeta), CiteAdapter: nil, Lang: config.GetLang(zn.InhMeta, api.rtConfig), Xhtml: false, MarkerExternal: "", NewWindow: false, IgnoreMeta: map[string]bool{meta.KeyLang: true}, } switch part { case partZettel: err = writeZettelPartZettel(w, zn, format, env) case partMeta: err = writeZettelPartMeta(w, zn, format) case partContent: err = api.writeZettelPartContent(w, zn, format, env) } if err != nil { if errors.Is(err, adapter.ErrNoSuchFormat) { adapter.BadRequest(w, fmt.Sprintf("Zettel %q not available in format %q", zid.String(), format)) return } adapter.InternalServerError(w, "Get zettel", err) } } } func writeZettelPartZettel(w http.ResponseWriter, zn *ast.ZettelNode, format string, env encoder.Environment) error { enc := encoder.Create(format, &env) if enc == nil { return adapter.ErrNoSuchFormat } inhMeta := false if format != "raw" { w.Header().Set(adapter.ContentType, format2ContentType(format)) inhMeta = true } _, err := enc.WriteZettel(w, zn, inhMeta) return err } func writeZettelPartMeta(w http.ResponseWriter, zn *ast.ZettelNode, format string) error { w.Header().Set(adapter.ContentType, format2ContentType(format)) if enc := encoder.Create(format, nil); enc != nil { if format == "raw" { _, err := enc.WriteMeta(w, zn.Meta) return err } _, err := enc.WriteMeta(w, zn.InhMeta) return err } return adapter.ErrNoSuchFormat } func (api *API) writeZettelPartContent(w http.ResponseWriter, zn *ast.ZettelNode, format string, env encoder.Environment) error { if format == "raw" { if ct, ok := syntax2contentType(config.GetSyntax(zn.Meta, api.rtConfig)); ok { w.Header().Add(adapter.ContentType, ct) } } else { w.Header().Set(adapter.ContentType, format2ContentType(format)) } return writeContent(w, zn, format, &env) } |
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 | //----------------------------------------------------------------------------- // Copyright (c) 2020-2021 Detlef Stern // // This file is part of zettelstore. // // Zettelstore is licensed under the latest version of the EUPL (European Union // Public License). Please see file LICENSE.txt for your rights and obligations // under this license. //----------------------------------------------------------------------------- // Package api provides api handlers for web requests. package api import ( "net/http" "zettelstore.de/z/domain/id" "zettelstore.de/z/usecase" "zettelstore.de/z/web/adapter" ) // MakeZettelContextHandler creates a new HTTP handler for the use case "zettel context". func (api *API) MakeZettelContextHandler(getContext usecase.ZettelContext) http.HandlerFunc { return func(w http.ResponseWriter, r *http.Request) { zid, err := id.Parse(r.URL.Path[1:]) if err != nil { http.NotFound(w, r) return } q := r.URL.Query() dir := usecase.ParseZCDirection(q.Get("dir")) depth, ok := adapter.GetInteger(q, "depth") if !ok || depth < 0 { depth = 5 } limit, ok := adapter.GetInteger(q, "limit") if !ok || limit < 0 { limit = 200 } ctx := r.Context() metaList, err := getContext.Run(ctx, zid, dir, depth, limit) if err != nil { adapter.ReportUsecaseError(w, err) return } api.writeMetaList(w, metaList[0], metaList[1:]) } } |
Added web/adapter/api/get_zettel_list.go.
> > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 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) 2020-2021 Detlef Stern // // This file is part of zettelstore. // // Zettelstore is licensed under the latest version of the EUPL (European Union // Public License). Please see file LICENSE.txt for your rights and obligations // under this license. //----------------------------------------------------------------------------- // Package api provides api handlers for web requests. package api import ( "fmt" "net/http" "zettelstore.de/z/domain/meta" "zettelstore.de/z/encoder" "zettelstore.de/z/parser" "zettelstore.de/z/place" "zettelstore.de/z/usecase" "zettelstore.de/z/web/adapter" ) // MakeListMetaHandler creates a new HTTP handler for the use case "list some zettel". func (api *API) MakeListMetaHandler( listMeta usecase.ListMeta, getMeta usecase.GetMeta, parseZettel usecase.ParseZettel, ) http.HandlerFunc { return func(w http.ResponseWriter, r *http.Request) { ctx := r.Context() q := r.URL.Query() s := adapter.GetSearch(q, false) format := adapter.GetFormat(r, q, encoder.GetDefaultFormat()) part := getPart(q, partMeta) if part == partUnknown { adapter.BadRequest(w, "Unknown _part parameter") return } ctx1 := ctx if format == "html" || (!s.HasComputedMetaKey() && (part == partID || part == partContent)) { ctx1 = place.NoEnrichContext(ctx1) } metaList, err := listMeta.Run(ctx1, s) if err != nil { adapter.ReportUsecaseError(w, err) return } w.Header().Set(adapter.ContentType, format2ContentType(format)) switch format { case "html": api.renderListMetaHTML(w, metaList) case "json", "djson": api.renderListMetaXJSON(ctx, w, metaList, format, part, partMeta, getMeta, parseZettel) case "native", "raw", "text", "zmk": adapter.NotImplemented(w, fmt.Sprintf("Zettel list in format %q not yet implemented", format)) default: adapter.BadRequest(w, fmt.Sprintf("Zettel list not available in format %q", format)) } } } func (api *API) renderListMetaHTML(w http.ResponseWriter, metaList []*meta.Meta) { env := encoder.Environment{Interactive: true} buf := encoder.NewBufWriter(w) buf.WriteStrings("<html lang=\"", api.rtConfig.GetDefaultLang(), "\">\n<body>\n<ul>\n") for _, m := range metaList { title := m.GetDefault(meta.KeyTitle, "") htmlTitle, err := adapter.FormatInlines(parser.ParseMetadata(title), "html", &env) if err != nil { adapter.InternalServerError(w, "Format HTML inlines", err) return } buf.WriteStrings( "<li><a href=\"", api.NewURLBuilder('z').SetZid(m.Zid).AppendQuery("_format", "html").String(), "\">", htmlTitle, "</a></li>\n") } buf.WriteString("</ul>\n</body>\n</html>") buf.Flush() } |
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 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 | //----------------------------------------------------------------------------- // Copyright (c) 2020-2021 Detlef Stern // // This file is part of zettelstore. // // Zettelstore is licensed under the latest version of the EUPL (European Union // Public License). Please see file LICENSE.txt for your rights and obligations // under this license. //----------------------------------------------------------------------------- // Package api provides api handlers for web requests. package api import ( "context" "encoding/json" "io" "net/http" "zettelstore.de/z/ast" "zettelstore.de/z/domain" "zettelstore.de/z/domain/id" "zettelstore.de/z/domain/meta" "zettelstore.de/z/encoder" "zettelstore.de/z/usecase" "zettelstore.de/z/web/adapter" ) type jsonIDURL struct { ID string `json:"id"` URL string `json:"url"` } type jsonZettel struct { ID string `json:"id"` URL string `json:"url"` Meta map[string]string `json:"meta"` Encoding string `json:"encoding"` Content interface{} `json:"content"` } type jsonMeta struct { ID string `json:"id"` URL string `json:"url"` Meta map[string]string `json:"meta"` } type jsonMetaList struct { ID string `json:"id"` URL string `json:"url"` Meta map[string]string `json:"meta"` List []jsonMeta `json:"list"` } type jsonContent struct { ID string `json:"id"` URL string `json:"url"` Encoding string `json:"encoding"` Content interface{} `json:"content"` } func encodedContent(content domain.Content) (string, interface{}) { if content.IsBinary() { return "base64", content.AsBytes() } return "", content.AsString() } var ( djsonMetaHeader = []byte(",\"meta\":") djsonContentHeader = []byte(",\"content\":") djsonHeader1 = []byte("{\"id\":\"") djsonHeader2 = []byte("\",\"url\":\"") djsonHeader3 = []byte("?_format=") djsonHeader4 = []byte("\"") djsonFooter = []byte("}") ) func (api *API) writeDJSONHeader(w io.Writer, zid id.Zid) error { _, err := w.Write(djsonHeader1) if err == nil { _, err = w.Write(zid.Bytes()) } if err == nil { _, err = w.Write(djsonHeader2) } if err == nil { _, err = io.WriteString(w, api.NewURLBuilder('z').SetZid(zid).String()) } if err == nil { _, err = w.Write(djsonHeader3) if err == nil { _, err = io.WriteString(w, "djson") } } if err == nil { _, err = w.Write(djsonHeader4) } return err } func (api *API) renderListMetaXJSON( ctx context.Context, w http.ResponseWriter, metaList []*meta.Meta, format string, part, defPart partType, getMeta usecase.GetMeta, parseZettel usecase.ParseZettel, ) { prepareZettel := api.getPrepareZettelFunc(ctx, parseZettel, part) writeZettel := api.getWriteMetaZettelFunc(ctx, format, part, defPart, getMeta) err := writeListXJSON(w, metaList, prepareZettel, writeZettel) if err != nil { adapter.InternalServerError(w, "Get list", err) } } type prepareZettelFunc func(m *meta.Meta) (*ast.ZettelNode, error) func (api *API) getPrepareZettelFunc(ctx context.Context, parseZettel usecase.ParseZettel, part partType) prepareZettelFunc { switch part { case partZettel, partContent: return func(m *meta.Meta) (*ast.ZettelNode, error) { return parseZettel.Run(ctx, m.Zid, "") } case partMeta, partID: return func(m *meta.Meta) (*ast.ZettelNode, error) { return &ast.ZettelNode{ Meta: m, Content: "", Zid: m.Zid, InhMeta: api.rtConfig.AddDefaultValues(m), Ast: nil, }, nil } } return nil } type writeZettelFunc func(io.Writer, *ast.ZettelNode) error func (api *API) getWriteMetaZettelFunc(ctx context.Context, format string, part, defPart partType, getMeta usecase.GetMeta) writeZettelFunc { switch part { case partZettel: return api.getWriteZettelFunc(ctx, format, defPart, getMeta) case partMeta: return api.getWriteMetaFunc(ctx, format) case partContent: return api.getWriteContentFunc(ctx, format, defPart, getMeta) case partID: return api.getWriteIDFunc(ctx, format) default: panic(part) } } func (api *API) getWriteZettelFunc(ctx context.Context, format string, defPart partType, getMeta usecase.GetMeta) writeZettelFunc { if format == "json" { return func(w io.Writer, zn *ast.ZettelNode) error { encoding, content := encodedContent(zn.Content) return encodeJSONData(w, jsonZettel{ ID: zn.Zid.String(), URL: api.NewURLBuilder('z').SetZid(zn.Zid).String(), Meta: zn.InhMeta.Map(), Encoding: encoding, Content: content, }) } } enc := encoder.Create("djson", nil) if enc == nil { panic("no DJSON encoder found") } return func(w io.Writer, zn *ast.ZettelNode) error { err := api.writeDJSONHeader(w, zn.Zid) if err != nil { return err } _, err = w.Write(djsonMetaHeader) if err != nil { return err } _, err = enc.WriteMeta(w, zn.InhMeta) if err != nil { return err } _, err = w.Write(djsonContentHeader) if err != nil { return err } err = writeContent(w, zn, "djson", &encoder.Environment{ LinkAdapter: adapter.MakeLinkAdapter(ctx, api, 'z', getMeta, partZettel.DefString(defPart), "djson"), ImageAdapter: adapter.MakeImageAdapter(ctx, api, getMeta)}) if err != nil { return err } _, err = w.Write(djsonFooter) return err } } func (api *API) getWriteMetaFunc(ctx context.Context, format string) writeZettelFunc { if format == "json" { return func(w io.Writer, zn *ast.ZettelNode) error { return encodeJSONData(w, jsonMeta{ ID: zn.Zid.String(), URL: api.NewURLBuilder('z').SetZid(zn.Zid).String(), Meta: zn.InhMeta.Map(), }) } } enc := encoder.Create("djson", nil) if enc == nil { panic("no DJSON encoder found") } return func(w io.Writer, zn *ast.ZettelNode) error { err := api.writeDJSONHeader(w, zn.Zid) if err != nil { return err } _, err = w.Write(djsonMetaHeader) if err != nil { return err } _, err = enc.WriteMeta(w, zn.InhMeta) if err != nil { return err } _, err = w.Write(djsonFooter) return err } } func (api *API) getWriteContentFunc(ctx context.Context, format string, defPart partType, getMeta usecase.GetMeta) writeZettelFunc { if format == "json" { return func(w io.Writer, zn *ast.ZettelNode) error { encoding, content := encodedContent(zn.Content) return encodeJSONData(w, jsonContent{ ID: zn.Zid.String(), URL: api.NewURLBuilder('z').SetZid(zn.Zid).String(), Encoding: encoding, Content: content, }) } } return func(w io.Writer, zn *ast.ZettelNode) error { err := api.writeDJSONHeader(w, zn.Zid) if err != nil { return err } _, err = w.Write(djsonContentHeader) if err != nil { return err } err = writeContent(w, zn, "djson", &encoder.Environment{ LinkAdapter: adapter.MakeLinkAdapter(ctx, api, 'z', getMeta, partContent.DefString(defPart), "djson"), ImageAdapter: adapter.MakeImageAdapter(ctx, api, getMeta)}) if err != nil { return err } _, err = w.Write(djsonFooter) return err } } func (api *API) getWriteIDFunc(ctx context.Context, format string) writeZettelFunc { if format == "json" { return func(w io.Writer, zn *ast.ZettelNode) error { return encodeJSONData(w, jsonIDURL{ ID: zn.Zid.String(), URL: api.NewURLBuilder('z').SetZid(zn.Zid).String(), }) } } return func(w io.Writer, zn *ast.ZettelNode) error { err := api.writeDJSONHeader(w, zn.Zid) if err != nil { return err } _, err = w.Write(djsonFooter) return err } } var ( jsonListHeader = []byte("{\"list\":[") jsonListSep = []byte{','} jsonListFooter = []byte("]}") ) func writeListXJSON(w http.ResponseWriter, metaList []*meta.Meta, prepareZettel prepareZettelFunc, writeZettel writeZettelFunc) error { _, err := w.Write(jsonListHeader) for i, m := range metaList { if err != nil { return err } if i > 0 { _, err = w.Write(jsonListSep) if err != nil { return err } } zn, err1 := prepareZettel(m) if err1 != nil { return err1 } err = writeZettel(w, zn) } if err == nil { _, err = w.Write(jsonListFooter) } return err } func writeContent(w io.Writer, zn *ast.ZettelNode, format string, env *encoder.Environment) error { enc := encoder.Create(format, env) if enc == nil { return adapter.ErrNoSuchFormat } _, err := enc.WriteContent(w, zn) return err } func encodeJSONData(w io.Writer, data interface{}) error { enc := json.NewEncoder(w) enc.SetEscapeHTML(false) return enc.Encode(data) } func (api *API) writeMetaList(w http.ResponseWriter, m *meta.Meta, metaList []*meta.Meta) error { outData := jsonMetaList{ ID: m.Zid.String(), URL: api.NewURLBuilder('z').SetZid(m.Zid).String(), Meta: m.Map(), List: make([]jsonMeta, len(metaList)), } for i, m := range metaList { outData.List[i].ID = m.Zid.String() outData.List[i].URL = api.NewURLBuilder('z').SetZid(m.Zid).String() outData.List[i].Meta = m.Map() } w.Header().Set(adapter.ContentType, format2ContentType("json")) return encodeJSONData(w, outData) } |
Changes to web/adapter/api/login.go.
1 | //----------------------------------------------------------------------------- | | | < < < > > < < | | | > | < < | | > | < < > > > > > > > > > > > > > | < < < < < < | > | < < | | > | < | | < < < < < < < < < | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 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-2021 Detlef Stern // // This file is part of zettelstore. // // Zettelstore is licensed under the latest version of the EUPL (European Union // Public License). Please see file LICENSE.txt for your rights and obligations // under this license. //----------------------------------------------------------------------------- // Package api provides api handlers for web requests. package api import ( "encoding/json" "net/http" "time" "zettelstore.de/z/auth" "zettelstore.de/z/usecase" "zettelstore.de/z/web/adapter" ) // MakePostLoginHandlerAPI creates a new HTTP handler to authenticate the given user via API. func (api *API) MakePostLoginHandlerAPI(ucAuth usecase.Authenticate) http.HandlerFunc { return func(w http.ResponseWriter, r *http.Request) { if !api.withAuth() { w.Header().Set(adapter.ContentType, format2ContentType("json")) writeJSONToken(w, "freeaccess", 24*366*10*time.Hour) return } var token []byte if ident, cred := retrieveIdentCred(r); ident != "" { var err error token, err = ucAuth.Run(r.Context(), ident, cred, api.tokenLifetime, auth.KindJSON) if err != nil { adapter.ReportUsecaseError(w, err) return } } if len(token) == 0 { w.Header().Set("WWW-Authenticate", `Bearer realm="Default"`) http.Error(w, "Authentication failed", http.StatusUnauthorized) return } w.Header().Set(adapter.ContentType, format2ContentType("json")) writeJSONToken(w, string(token), api.tokenLifetime) } } 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 writeJSONToken(w http.ResponseWriter, token string, lifetime time.Duration) { je := json.NewEncoder(w) je.Encode(struct { Token string `json:"access_token"` Type string `json:"token_type"` Expires int `json:"expires_in"` }{ Token: token, Type: "Bearer", Expires: int(lifetime / time.Second), }) } // MakeRenewAuthHandler creates a new HTTP handler to renew the authenticate of a user. func (api *API) MakeRenewAuthHandler() http.HandlerFunc { return func(w http.ResponseWriter, r *http.Request) { ctx := r.Context() authData := api.getAuthData(ctx) if authData == nil || len(authData.Token) == 0 || authData.User == nil { adapter.BadRequest(w, "Not authenticated") return } totalLifetime := authData.Expires.Sub(authData.Issued) currentLifetime := authData.Now.Sub(authData.Issued) // If we are in the first quarter of the tokens lifetime, return the token if currentLifetime*4 < totalLifetime { w.Header().Set(adapter.ContentType, format2ContentType("json")) writeJSONToken(w, string(authData.Token), totalLifetime-currentLifetime) return } // Token is a little bit aged. Create a new one token, err := api.getToken(authData.User) if err != nil { adapter.ReportUsecaseError(w, err) return } w.Header().Set(adapter.ContentType, format2ContentType("json")) writeJSONToken(w, string(token), api.tokenLifetime) } } |
Deleted web/adapter/api/query.go.
|
| < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < |
Deleted web/adapter/api/rename_zettel.go.
|
| < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < |
Changes to web/adapter/api/request.go.
1 | //----------------------------------------------------------------------------- | | | < < < > | < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < | > > | | | > > > > | | > > > > < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 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-2021 Detlef Stern // // This file is part of zettelstore. // // Zettelstore is licensed under the latest version of the EUPL (European Union // Public License). Please see file LICENSE.txt for your rights and obligations // under this license. //----------------------------------------------------------------------------- // Package api provides api handlers for web requests. package api import "net/url" type partType int const ( partUnknown partType = iota partID partMeta partContent partZettel ) var partMap = map[string]partType{ "id": partID, "meta": partMeta, "content": partContent, "zettel": partZettel, } func getPart(q url.Values, defPart partType) partType { p := q.Get("_part") if p == "" { return defPart } if part, ok := partMap[p]; ok { return part } return partUnknown } func (p partType) String() string { switch p { case partID: return "id" case partMeta: return "meta" case partContent: return "content" case partZettel: return "zettel" case partUnknown: return "unknown" } return "" } func (p partType) DefString(defPart partType) string { if p == defPart { return "" } return p.String() } |
Deleted web/adapter/api/response.go.
|
| < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < |
Deleted web/adapter/api/update_zettel.go.
|
| < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < |
Added web/adapter/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 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 | //----------------------------------------------------------------------------- // Copyright (c) 2020-2021 Detlef Stern // // This file is part of zettelstore. // // Zettelstore is licensed under the latest version of the EUPL (European Union // Public License). Please see file LICENSE.txt for your rights and obligations // under this license. //----------------------------------------------------------------------------- // Package adapter provides handlers for web requests. package adapter import ( "context" "errors" "strings" "zettelstore.de/z/ast" "zettelstore.de/z/domain/id" "zettelstore.de/z/encoder" "zettelstore.de/z/place" "zettelstore.de/z/usecase" "zettelstore.de/z/web/server" ) // ErrNoSuchFormat signals an unsupported encoding format var ErrNoSuchFormat = errors.New("no such format") // FormatInlines returns a string representation of the inline slice. func FormatInlines(is ast.InlineSlice, format string, env *encoder.Environment) (string, error) { enc := encoder.Create(format, env) if enc == nil { return "", ErrNoSuchFormat } var content strings.Builder _, err := enc.WriteInlines(&content, is) if err != nil { return "", err } return content.String(), nil } // MakeLinkAdapter creates an adapter to change a link node during encoding. func MakeLinkAdapter( ctx context.Context, b server.Builder, key byte, getMeta usecase.GetMeta, part, format string, ) func(*ast.LinkNode) ast.InlineNode { return func(origLink *ast.LinkNode) ast.InlineNode { origRef := origLink.Ref if origRef == nil { return origLink } if origRef.State == ast.RefStateBased { newLink := *origLink urlPrefix := b.GetURLPrefix() newRef := ast.ParseReference(urlPrefix + origRef.Value[1:]) newRef.State = ast.RefStateHosted newLink.Ref = newRef return &newLink } if origRef.State != ast.RefStateZettel { return origLink } zid, err := id.Parse(origRef.URL.Path) if err != nil { panic(err) } _, err = getMeta.Run(place.NoEnrichContext(ctx), zid) if errors.Is(err, &place.ErrNotAllowed{}) { return &ast.FormatNode{ Code: ast.FormatSpan, Attrs: origLink.Attrs, Inlines: origLink.Inlines, } } var newRef *ast.Reference if err == nil { ub := b.NewURLBuilder(key).SetZid(zid) if part != "" { ub.AppendQuery("_part", part) } if format != "" { ub.AppendQuery("_format", format) } if fragment := origRef.URL.EscapedFragment(); fragment != "" { ub.SetFragment(fragment) } newRef = ast.ParseReference(ub.String()) newRef.State = ast.RefStateFound } else { newRef = ast.ParseReference(origRef.Value) newRef.State = ast.RefStateBroken } newLink := *origLink newLink.Ref = newRef return &newLink } } // MakeImageAdapter creates an adapter to change an image node during encoding. func MakeImageAdapter(ctx context.Context, b server.Builder, getMeta usecase.GetMeta) func(*ast.ImageNode) ast.InlineNode { return func(origImage *ast.ImageNode) ast.InlineNode { if origImage.Ref == nil { return origImage } switch origImage.Ref.State { case ast.RefStateInvalid: return createZettelImage(b, origImage, id.EmojiZid, ast.RefStateInvalid) case ast.RefStateZettel: zid, err := id.Parse(origImage.Ref.Value) if err != nil { panic(err) } _, err = getMeta.Run(place.NoEnrichContext(ctx), zid) if err != nil { return createZettelImage(b, origImage, id.EmojiZid, ast.RefStateBroken) } return createZettelImage(b, origImage, zid, ast.RefStateFound) } return origImage } } func createZettelImage(b server.Builder, origImage *ast.ImageNode, zid id.Zid, state ast.RefState) *ast.ImageNode { newImage := *origImage newImage.Ref = ast.ParseReference( b.NewURLBuilder('z').SetZid(zid).AppendQuery("_part", "content").AppendQuery("_format", "raw").String()) newImage.Ref.State = state return &newImage } |
Changes to web/adapter/errors.go.
1 | //----------------------------------------------------------------------------- | | | < < < > | > > > > > > > | > > > > | > > > > > > > | > | > > > > > | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 | //----------------------------------------------------------------------------- // Copyright (c) 2020 Detlef Stern // // This file is part of zettelstore. // // Zettelstore is licensed under the latest version of the EUPL (European Union // Public License). Please see file LICENSE.txt for your rights and obligations // under this license. //----------------------------------------------------------------------------- // Package adapter provides handlers for web requests. package adapter import ( "log" "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) } // InternalServerError signals HTTP status code 500. func InternalServerError(w http.ResponseWriter, text string, err error) { http.Error(w, "Internal Server Error", http.StatusInternalServerError) if text == "" { log.Println(err) } else { log.Printf("%v: %v", text, err) } } // NotImplemented signals HTTP status code 501 func NotImplemented(w http.ResponseWriter, text string) { http.Error(w, text, http.StatusNotImplemented) log.Println(text) } |
Added web/adapter/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 | //----------------------------------------------------------------------------- // Copyright (c) 2020 Detlef Stern // // This file is part of zettelstore. // // Zettelstore is licensed under the latest version of the EUPL (European Union // Public License). Please see file LICENSE.txt for your rights and obligations // under this license. //----------------------------------------------------------------------------- // Package adapter provides handlers for web requests. package adapter import ( "fmt" "log" "net/http" "strings" "zettelstore.de/z/encoder" ) // MakePostLoginHandler creates a new HTTP handler to authenticate the given user. func MakePostLoginHandler(apiHandler, htmlHandler http.HandlerFunc) http.HandlerFunc { return func(w http.ResponseWriter, r *http.Request) { switch format := GetFormat(r, r.URL.Query(), encoder.GetDefaultFormat()); format { case "json": apiHandler(w, r) case "html": htmlHandler(w, r) default: BadRequest(w, fmt.Sprintf("Authentication not available in format %q", format)) } } } // GetCredentialsViaForm retrieves the authentication credentions from a form. func GetCredentialsViaForm(r *http.Request) (ident, cred string, ok bool) { err := r.ParseForm() if err != nil { log.Println(err) return "", "", false } ident = strings.TrimSpace(r.PostFormValue("username")) cred = r.PostFormValue("password") if ident == "" { return "", "", false } return ident, cred, true } |
Changes to web/adapter/request.go.
1 | //----------------------------------------------------------------------------- | | | < < < > < | | > > > > > > > > > > | > > | > > | | | > > | > > > > > | | | | > > > > > | | > > > > > > > > > | > > > > > > > > > > > > > > > > > > > | > > > > | > | > | | > > | > > > > > > | > > > > > > > > | > > | > > > > > | > | > | > | | > > > > | | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 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 | //----------------------------------------------------------------------------- // Copyright (c) 2020-2021 Detlef Stern // // This file is part of zettelstore. // // Zettelstore is licensed under the latest version of the EUPL (European Union // Public License). Please see file LICENSE.txt for your rights and obligations // under this license. //----------------------------------------------------------------------------- // Package adapter provides handlers for web requests. package adapter import ( "net/http" "net/url" "strconv" "strings" "zettelstore.de/z/domain/meta" "zettelstore.de/z/search" ) // 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 } // ContentType defines the HTTP header value "Content-Type". const ContentType = "Content-Type" // GetFormat returns the data format selected by the caller. func GetFormat(r *http.Request, q url.Values, defFormat string) string { format := q.Get("_format") if len(format) > 0 { return format } if format, ok := getOneFormat(r, "Accept"); ok { return format } if format, ok := getOneFormat(r, ContentType); ok { return format } return defFormat } func getOneFormat(r *http.Request, key string) (string, bool) { if values, ok := r.Header[key]; ok { for _, value := range values { if format, ok := contentType2format(value); ok { return format, true } } } return "", false } var mapCT2format = map[string]string{ "application/json": "json", "text/html": "html", } func contentType2format(contentType string) (string, bool) { // TODO: only check before first ';' format, ok := mapCT2format[contentType] return format, ok } // GetSearch retrieves the specified filter and sorting options from a query. func GetSearch(q url.Values, forSearch bool) (s *search.Search) { sortQKey, orderQKey, offsetQKey, limitQKey, negateQKey, sQKey := getQueryKeys(forSearch) for key, values := range q { switch key { case sortQKey, orderQKey: s = extractOrderFromQuery(values, s) case offsetQKey: s = extractOffsetFromQuery(values, s) case limitQKey: s = extractLimitFromQuery(values, s) case negateQKey: s = s.SetNegate() case sQKey: s = setCleanedQueryValues(s, "", values) default: if !forSearch && meta.KeyIsValid(key) { s = setCleanedQueryValues(s, key, values) } } } return s } func extractOrderFromQuery(values []string, s *search.Search) *search.Search { if len(values) > 0 { descending := false sortkey := values[0] if strings.HasPrefix(sortkey, "-") { descending = true sortkey = sortkey[1:] } if meta.KeyIsValid(sortkey) || sortkey == search.RandomOrder { s = s.AddOrder(sortkey, descending) } } return s } func extractOffsetFromQuery(values []string, s *search.Search) *search.Search { if len(values) > 0 { if offset, err := strconv.Atoi(values[0]); err == nil { s = s.SetOffset(offset) } } return s } func extractLimitFromQuery(values []string, s *search.Search) *search.Search { if len(values) > 0 { if limit, err := strconv.Atoi(values[0]); err == nil { s = s.SetLimit(limit) } } return s } func getQueryKeys(forSearch bool) (string, string, string, string, string, string) { if forSearch { return "sort", "order", "offset", "limit", "negate", "s" } return "_sort", "_order", "_offset", "_limit", "_negate", "_s" } func setCleanedQueryValues(s *search.Search, key string, values []string) *search.Search { for _, val := range values { s = s.AddExpr(key, val) } return s } |
Changes to web/adapter/response.go.
1 | //----------------------------------------------------------------------------- | | | < < < > > < < | < > | < < < < < > | < | | | < < < < < < < | | | < | | < < | | < | | < | < < < < < < < < | < | | | < < < < < < < | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 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) 2020 Detlef Stern // // This file is part of zettelstore. // // Zettelstore is licensed under the latest version of the EUPL (European Union // Public License). Please see file LICENSE.txt for your rights and obligations // under this license. //----------------------------------------------------------------------------- // Package adapter provides handlers for web requests. package adapter import ( "errors" "fmt" "log" "net/http" "zettelstore.de/z/place" "zettelstore.de/z/usecase" ) // ReportUsecaseError returns an appropriate HTTP status code for errors in use cases. func ReportUsecaseError(w http.ResponseWriter, err error) { code, text := CodeMessageFromError(err) if code == http.StatusInternalServerError { log.Printf("%v: %v", text, err) } http.Error(w, text, code) } // ErrBadRequest is returned if the caller made an invalid HTTP request. type ErrBadRequest struct { Text string } // NewErrBadRequest creates an new bad request error. func NewErrBadRequest(text string) error { return &ErrBadRequest{Text: text} } func (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 == place.ErrNotFound { return http.StatusNotFound, http.StatusText(http.StatusNotFound) } if err1, ok := err.(*place.ErrNotAllowed); ok { return http.StatusForbidden, err1.Error() } if err1, ok := err.(*place.ErrInvalidID); ok { return http.StatusBadRequest, fmt.Sprintf("Zettel-ID %q not appropriate in this context", err1.Zid) } if err1, ok := err.(*usecase.ErrZidInUse); ok { return http.StatusBadRequest, fmt.Sprintf("Zettel-ID %q already in use", err1.Zid) } if err1, ok := err.(*ErrBadRequest); ok { return http.StatusBadRequest, err1.Text } if errors.Is(err, place.ErrStopped) { return http.StatusInternalServerError, fmt.Sprintf("Zettelstore not operational: %v", err) } if errors.Is(err, place.ErrConflict) { return http.StatusConflict, "Zettelstore operations conflicted" } return http.StatusInternalServerError, err.Error() } |
Deleted web/adapter/webui/const.go.
|
| < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < |
Changes to web/adapter/webui/create_zettel.go.
1 | //----------------------------------------------------------------------------- | | | < < < > < > < < < | | | | | | | | | | > > > > > > > > > > > > > > > > > > > > > > > > > > > > | | | < < > | > | > | > | | > | | > | < < < < < > < < < < < < < < < | > > > > > > > > > > < | | < < | | | < | < < | | < > | | < < < > | | | > > | | | < < < < | < < < | < > | | | < < < | < < | < < | | | | | < < | < < < | | | < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 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-2021 Detlef Stern // // This file is part of zettelstore. // // Zettelstore is licensed under the latest version of the EUPL (European Union // Public License). Please see file LICENSE.txt for your rights and obligations // under this license. //----------------------------------------------------------------------------- // Package webui provides web-UI handlers for web requests. package webui import ( "context" "fmt" "net/http" "zettelstore.de/z/config" "zettelstore.de/z/domain" "zettelstore.de/z/domain/id" "zettelstore.de/z/domain/meta" "zettelstore.de/z/encoder" "zettelstore.de/z/input" "zettelstore.de/z/parser" "zettelstore.de/z/place" "zettelstore.de/z/usecase" "zettelstore.de/z/web/adapter" ) // MakeGetCopyZettelHandler creates a new HTTP handler to display the // HTML edit view of a copied zettel. func (wui *WebUI) MakeGetCopyZettelHandler(getZettel usecase.GetZettel, copyZettel usecase.CopyZettel) http.HandlerFunc { return func(w http.ResponseWriter, r *http.Request) { ctx := r.Context() origZettel, err := getOrigZettel(ctx, w, r, getZettel, "Copy") if err != nil { wui.reportError(ctx, w, err) return } wui.renderZettelForm(w, r, copyZettel.Run(origZettel), "Copy Zettel", "Copy Zettel") } } // MakeGetFolgeZettelHandler creates a new HTTP handler to display the // HTML edit view of a follow-up zettel. func (wui *WebUI) MakeGetFolgeZettelHandler(getZettel usecase.GetZettel, folgeZettel usecase.FolgeZettel) http.HandlerFunc { return func(w http.ResponseWriter, r *http.Request) { ctx := r.Context() origZettel, err := getOrigZettel(ctx, w, r, getZettel, "Folge") if err != nil { wui.reportError(ctx, w, err) return } wui.renderZettelForm(w, r, folgeZettel.Run(origZettel), "Folge Zettel", "Folgezettel") } } // MakeGetNewZettelHandler creates a new HTTP handler to display the // HTML edit view of a zettel. func (wui *WebUI) MakeGetNewZettelHandler(getZettel usecase.GetZettel, newZettel usecase.NewZettel) http.HandlerFunc { return func(w http.ResponseWriter, r *http.Request) { ctx := r.Context() origZettel, err := getOrigZettel(ctx, w, r, getZettel, "New") if err != nil { wui.reportError(ctx, w, err) return } m := origZettel.Meta title := parser.ParseInlines(input.NewInput(config.GetTitle(m, wui.rtConfig)), meta.ValueSyntaxZmk) textTitle, err := adapter.FormatInlines(title, "text", nil) if err != nil { wui.reportError(ctx, w, err) return } env := encoder.Environment{Lang: config.GetLang(m, wui.rtConfig)} htmlTitle, err := adapter.FormatInlines(title, "html", &env) if err != nil { wui.reportError(ctx, w, err) return } wui.renderZettelForm(w, r, newZettel.Run(origZettel), textTitle, htmlTitle) } } func getOrigZettel( ctx context.Context, w http.ResponseWriter, r *http.Request, getZettel usecase.GetZettel, op string, ) (domain.Zettel, error) { if format := adapter.GetFormat(r, r.URL.Query(), "html"); format != "html" { return domain.Zettel{}, adapter.NewErrBadRequest( fmt.Sprintf("%v zettel not possible in format %q", op, format)) } zid, err := id.Parse(r.URL.Path[1:]) if err != nil { return domain.Zettel{}, place.ErrNotFound } origZettel, err := getZettel.Run(place.NoEnrichContext(ctx), zid) if err != nil { return domain.Zettel{}, place.ErrNotFound } return origZettel, nil } func (wui *WebUI) renderZettelForm( w http.ResponseWriter, r *http.Request, zettel domain.Zettel, title, heading string, ) { ctx := r.Context() user := wui.getUser(ctx) m := zettel.Meta var base baseData wui.makeBaseData(ctx, config.GetLang(m, wui.rtConfig), title, user, &base) wui.renderTemplate(ctx, w, id.FormTemplateZid, &base, formZettelData{ Heading: heading, MetaTitle: m.GetDefault(meta.KeyTitle, ""), MetaTags: m.GetDefault(meta.KeyTags, ""), MetaRole: config.GetRole(m, wui.rtConfig), MetaSyntax: config.GetSyntax(m, wui.rtConfig), MetaPairsRest: m.PairsRest(false), IsTextContent: !zettel.Content.IsBinary(), Content: zettel.Content.AsString(), }) } // MakePostCreateZettelHandler creates a new HTTP handler to store content of // an existing zettel. func (wui *WebUI) MakePostCreateZettelHandler(createZettel usecase.CreateZettel) http.HandlerFunc { return func(w http.ResponseWriter, r *http.Request) { ctx := r.Context() zettel, hasContent, err := parseZettelForm(r, id.Invalid) if err != nil { wui.reportError(ctx, w, adapter.NewErrBadRequest("Unable to read form data")) return } if !hasContent { wui.reportError(ctx, w, adapter.NewErrBadRequest("Content is missing")) return } newZid, err := createZettel.Run(ctx, zettel) if err != nil { wui.reportError(ctx, w, err) return } redirectFound(w, r, wui.NewURLBuilder('h').SetZid(newZid)) } } |
Changes to web/adapter/webui/delete_zettel.go.
1 | //----------------------------------------------------------------------------- | | | < < < > > < < < | | | | | | | | < < | > | | < < < < < < < < < < < < < | < < < < < > | < | < | < < < < < < < < | < > > | < | < | < < < | < | < < | | < | | | | | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 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) 2020-2021 Detlef Stern // // This file is part of zettelstore. // // Zettelstore is licensed under the latest version of the EUPL (European Union // Public License). Please see file LICENSE.txt for your rights and obligations // under this license. //----------------------------------------------------------------------------- // Package webui provides web-UI handlers for web requests. package webui import ( "fmt" "net/http" "zettelstore.de/z/config" "zettelstore.de/z/domain/id" "zettelstore.de/z/domain/meta" "zettelstore.de/z/place" "zettelstore.de/z/usecase" "zettelstore.de/z/web/adapter" ) // MakeGetDeleteZettelHandler creates a new HTTP handler to display the // HTML delete view of a zettel. func (wui *WebUI) MakeGetDeleteZettelHandler(getZettel usecase.GetZettel) http.HandlerFunc { return func(w http.ResponseWriter, r *http.Request) { ctx := r.Context() if format := adapter.GetFormat(r, r.URL.Query(), "html"); format != "html" { wui.reportError(ctx, w, adapter.NewErrBadRequest( fmt.Sprintf("Delete zettel not possible in format %q", format))) return } zid, err := id.Parse(r.URL.Path[1:]) if err != nil { wui.reportError(ctx, w, place.ErrNotFound) return } zettel, err := getZettel.Run(ctx, zid) if err != nil { wui.reportError(ctx, w, err) return } user := wui.getUser(ctx) m := zettel.Meta var base baseData wui.makeBaseData(ctx, config.GetLang(m, wui.rtConfig), "Delete Zettel "+m.Zid.String(), user, &base) wui.renderTemplate(ctx, w, id.DeleteTemplateZid, &base, struct { Zid string MetaPairs []meta.Pair }{ Zid: zid.String(), MetaPairs: m.Pairs(true), }) } } // MakePostDeleteZettelHandler creates a new HTTP handler to delete a zettel. func (wui *WebUI) MakePostDeleteZettelHandler(deleteZettel usecase.DeleteZettel) http.HandlerFunc { 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, place.ErrNotFound) return } if err := deleteZettel.Run(r.Context(), zid); err != nil { wui.reportError(ctx, w, err) return } redirectFound(w, r, wui.NewURLBuilder('/')) } } |
Changes to web/adapter/webui/edit_zettel.go.
1 | //----------------------------------------------------------------------------- | | | < < < > > > | | > | | | < | | | > > > > > | > > > | > > > > > > > > > > | < | | | < < < < | | | < | | < < < < | | | < | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 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-2021 Detlef Stern // // This file is part of zettelstore. // // Zettelstore is licensed under the latest version of the EUPL (European Union // Public License). Please see file LICENSE.txt for your rights and obligations // under this license. //----------------------------------------------------------------------------- // Package webui provides web-UI handlers for web requests. package webui import ( "fmt" "net/http" "zettelstore.de/z/config" "zettelstore.de/z/domain/id" "zettelstore.de/z/domain/meta" "zettelstore.de/z/place" "zettelstore.de/z/usecase" "zettelstore.de/z/web/adapter" ) // MakeEditGetZettelHandler creates a new HTTP handler to display the // HTML edit view of a zettel. func (wui *WebUI) MakeEditGetZettelHandler(getZettel usecase.GetZettel) http.HandlerFunc { return func(w http.ResponseWriter, r *http.Request) { ctx := r.Context() zid, err := id.Parse(r.URL.Path[1:]) if err != nil { wui.reportError(ctx, w, place.ErrNotFound) return } zettel, err := getZettel.Run(place.NoEnrichContext(ctx), zid) if err != nil { wui.reportError(ctx, w, err) return } if format := adapter.GetFormat(r, r.URL.Query(), "html"); format != "html" { wui.reportError(ctx, w, adapter.NewErrBadRequest( fmt.Sprintf("Edit zettel %q not possible in format %q", zid, format))) return } user := wui.getUser(ctx) m := zettel.Meta var base baseData wui.makeBaseData(ctx, config.GetLang(m, wui.rtConfig), "Edit Zettel", user, &base) wui.renderTemplate(ctx, w, id.FormTemplateZid, &base, formZettelData{ Heading: base.Title, MetaTitle: m.GetDefault(meta.KeyTitle, ""), MetaRole: m.GetDefault(meta.KeyRole, ""), MetaTags: m.GetDefault(meta.KeyTags, ""), MetaSyntax: m.GetDefault(meta.KeySyntax, ""), MetaPairsRest: m.PairsRest(false), IsTextContent: !zettel.Content.IsBinary(), Content: zettel.Content.AsString(), }) } } // MakeEditSetZettelHandler creates a new HTTP handler to store content of // an existing zettel. func (wui *WebUI) MakeEditSetZettelHandler(updateZettel usecase.UpdateZettel) http.HandlerFunc { return func(w http.ResponseWriter, r *http.Request) { ctx := r.Context() zid, err := id.Parse(r.URL.Path[1:]) if err != nil { wui.reportError(ctx, w, place.ErrNotFound) return } zettel, hasContent, err := parseZettelForm(r, zid) if err != nil { wui.reportError(ctx, w, adapter.NewErrBadRequest("Unable to read zettel form")) return } if err := updateZettel.Run(r.Context(), zettel, hasContent); err != nil { wui.reportError(ctx, w, err) return } redirectFound(w, r, wui.NewURLBuilder('h').SetZid(zid)) } } |
Deleted web/adapter/webui/favicon.go.
|
| < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < |
Changes to web/adapter/webui/forms.go.
1 | //----------------------------------------------------------------------------- | | | < < < > < < < < < < < | < | < | | < > > > > > > | > | < | < | < | | < | < | | < < < | | | | | | < | > | | > | < | < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 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) 2020-2021 Detlef Stern // // This file is part of zettelstore. // // Zettelstore is licensed under the latest version of the EUPL (European Union // Public License). Please see file LICENSE.txt for your rights and obligations // under this license. //----------------------------------------------------------------------------- // Package webui provides web-UI handlers for web requests. package webui import ( "net/http" "strings" "zettelstore.de/z/domain" "zettelstore.de/z/domain/id" "zettelstore.de/z/domain/meta" "zettelstore.de/z/input" ) type formZettelData struct { Heading string MetaTitle string MetaRole string MetaTags string MetaSyntax string MetaPairsRest []meta.Pair IsTextContent bool Content string } func parseZettelForm(r *http.Request, zid id.Zid) (domain.Zettel, bool, error) { err := r.ParseForm() if err != nil { return domain.Zettel{}, false, err } var m *meta.Meta if postMeta, ok := trimmedFormValue(r, "meta"); ok { m = meta.NewFromInput(zid, input.NewInput(postMeta)) } else { m = meta.New(zid) } if postTitle, ok := trimmedFormValue(r, "title"); ok { m.Set(meta.KeyTitle, postTitle) } if postTags, ok := trimmedFormValue(r, "tags"); ok { if tags := strings.Fields(postTags); len(tags) > 0 { m.SetList(meta.KeyTags, tags) } } if postRole, ok := trimmedFormValue(r, "role"); ok { m.Set(meta.KeyRole, postRole) } if postSyntax, ok := trimmedFormValue(r, "syntax"); ok { m.Set(meta.KeySyntax, postSyntax) } if values, ok := r.PostForm["content"]; ok && len(values) > 0 { return domain.Zettel{ Meta: m, Content: domain.NewContent( strings.ReplaceAll(strings.TrimSpace(values[0]), "\r\n", "\n")), }, true, nil } return domain.Zettel{ Meta: m, Content: domain.NewContent(""), }, false, nil } func trimmedFormValue(r *http.Request, key string) (string, bool) { if values, ok := r.PostForm[key]; ok && len(values) > 0 { value := strings.TrimSpace(values[0]) if len(value) > 0 { return value, true } } return "", false } |
Deleted web/adapter/webui/forms_test.go.
|
| < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < |
Changes to web/adapter/webui/get_info.go.
1 | //----------------------------------------------------------------------------- | | | < < < > | < < < | | | | | | | | < < < < < < < < < < < < < < < < < | > > | < < < > | > > | > > | < < < < | > > > | | < | > | | | | > > | < < < < | | | | < < < < | > | | | | < > | > | < < < < < < < < < < < < < > < > < < < < < < < < | > > > > | | > | > | > | > | < | | > | < < < < < > | | | < < | < | | | | | < | > > | > > | > > > > > | < | > | | | | | > > | < < | | < > | | < < < < < < < < | < < < < | < > > > | > > | > > | | < < | < < < < < < | < < < < < < | < < | | > | | > > | < < > > > > > > > > > | | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 | //----------------------------------------------------------------------------- // Copyright (c) 2020-2021 Detlef Stern // // This file is part of zettelstore. // // Zettelstore is licensed under the latest version of the EUPL (European Union // Public License). Please see file LICENSE.txt for your rights and obligations // under this license. //----------------------------------------------------------------------------- // Package webui provides web-UI handlers for web requests. package webui import ( "fmt" "net/http" "strings" "zettelstore.de/z/ast" "zettelstore.de/z/collect" "zettelstore.de/z/config" "zettelstore.de/z/domain/id" "zettelstore.de/z/domain/meta" "zettelstore.de/z/encoder" "zettelstore.de/z/encoder/encfun" "zettelstore.de/z/place" "zettelstore.de/z/usecase" "zettelstore.de/z/web/adapter" ) type metaDataInfo struct { Key string Value string } type matrixElement struct { Text string HasURL bool URL string } type matrixLine struct { Elements []matrixElement } // MakeGetInfoHandler creates a new HTTP handler for the use case "get zettel". func (wui *WebUI) MakeGetInfoHandler(parseZettel usecase.ParseZettel, getMeta usecase.GetMeta) http.HandlerFunc { return func(w http.ResponseWriter, r *http.Request) { ctx := r.Context() q := r.URL.Query() if format := adapter.GetFormat(r, q, "html"); format != "html" { wui.reportError(ctx, w, adapter.NewErrBadRequest( fmt.Sprintf("Zettel info not available in format %q", format))) return } zid, err := id.Parse(r.URL.Path[1:]) if err != nil { wui.reportError(ctx, w, place.ErrNotFound) return } zn, err := parseZettel.Run(ctx, zid, q.Get("syntax")) if err != nil { wui.reportError(ctx, w, err) return } summary := collect.References(zn) locLinks, extLinks := splitLocExtLinks(append(summary.Links, summary.Images...)) lang := config.GetLang(zn.InhMeta, wui.rtConfig) env := encoder.Environment{Lang: lang} pairs := zn.Meta.Pairs(true) metaData := make([]metaDataInfo, len(pairs)) getTitle := makeGetTitle(ctx, getMeta, &env) for i, p := range pairs { var html strings.Builder wui.writeHTMLMetaValue(&html, zn.Meta, p.Key, getTitle, &env) metaData[i] = metaDataInfo{p.Key, html.String()} } endnotes, err := formatBlocks(nil, "html", &env) if err != nil { endnotes = "" } textTitle := encfun.MetaAsText(zn.InhMeta, meta.KeyTitle) user := wui.getUser(ctx) var base baseData wui.makeBaseData(ctx, lang, textTitle, user, &base) wui.renderTemplate(ctx, w, id.InfoTemplateZid, &base, struct { Zid string WebURL string ContextURL string CanWrite bool EditURL string CanFolge bool FolgeURL string CanCopy bool CopyURL string CanRename bool RenameURL string CanDelete bool DeleteURL string MetaData []metaDataInfo HasLinks bool HasLocLinks bool LocLinks []localLink HasExtLinks bool ExtLinks []string ExtNewWindow string Matrix []matrixLine Endnotes string }{ Zid: zid.String(), WebURL: wui.NewURLBuilder('h').SetZid(zid).String(), ContextURL: wui.NewURLBuilder('j').SetZid(zid).String(), CanWrite: wui.canWrite(ctx, user, zn.Meta, zn.Content), EditURL: wui.NewURLBuilder('e').SetZid(zid).String(), CanFolge: base.CanCreate, FolgeURL: wui.NewURLBuilder('f').SetZid(zid).String(), CanCopy: base.CanCreate && !zn.Content.IsBinary(), CopyURL: wui.NewURLBuilder('c').SetZid(zid).String(), CanRename: wui.canRename(ctx, user, zn.Meta), RenameURL: wui.NewURLBuilder('b').SetZid(zid).String(), CanDelete: wui.canDelete(ctx, user, zn.Meta), DeleteURL: wui.NewURLBuilder('d').SetZid(zid).String(), MetaData: metaData, HasLinks: len(extLinks)+len(locLinks) > 0, HasLocLinks: len(locLinks) > 0, LocLinks: locLinks, HasExtLinks: len(extLinks) > 0, ExtLinks: extLinks, ExtNewWindow: htmlAttrNewWindow(len(extLinks) > 0), Matrix: wui.infoAPIMatrix(zid), Endnotes: endnotes, }) } } type localLink struct { Valid bool Zid string } func splitLocExtLinks(links []*ast.Reference) (locLinks []localLink, extLinks []string) { if len(links) == 0 { return nil, nil } for _, ref := range links { if ref.State == ast.RefStateSelf { continue } if ref.IsZettel() { continue } if ref.IsExternal() { extLinks = append(extLinks, ref.String()) continue } locLinks = append(locLinks, localLink{ref.IsValid(), ref.String()}) } return locLinks, extLinks } func (wui *WebUI) infoAPIMatrix(zid id.Zid) []matrixLine { formats := encoder.GetFormats() defFormat := encoder.GetDefaultFormat() parts := []string{"zettel", "meta", "content"} matrix := make([]matrixLine, 0, len(parts)) u := wui.NewURLBuilder('z').SetZid(zid) for _, part := range parts { row := make([]matrixElement, 0, len(formats)+1) row = append(row, matrixElement{part, false, ""}) for _, format := range formats { u.AppendQuery("_part", part) if format != defFormat { u.AppendQuery("_format", format) } row = append(row, matrixElement{format, true, u.String()}) u.ClearQuery() } matrix = append(matrix, matrixLine{row}) } return matrix } |
Changes to web/adapter/webui/get_zettel.go.
1 | //----------------------------------------------------------------------------- | | | < < < > | | | | | | | | | | < | < | | | | | | > > > > > > > > > | > > > > > | > | | > > | > | | < | | < < | > > | < > | | | | | | | | | | | | > > > > > | | > > | < > | > > > | < > > > > > | < < > > > | < < < < > > > > > > | < < < | > > > > | | < | < < | | > | > | | > > > | | > > > > > | < < < < < < | < < < | | > | > > | > > | | < | > | | > | < > > | | | > | > > > | | | < > < > | | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 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-2021 Detlef Stern // // This file is part of zettelstore. // // Zettelstore is licensed under the latest version of the EUPL (European Union // Public License). Please see file LICENSE.txt for your rights and obligations // under this license. //----------------------------------------------------------------------------- // Package webui provides web-UI handlers for web requests. package webui import ( "bytes" "net/http" "strings" "zettelstore.de/z/ast" "zettelstore.de/z/config" "zettelstore.de/z/domain/id" "zettelstore.de/z/domain/meta" "zettelstore.de/z/encoder" "zettelstore.de/z/encoder/encfun" "zettelstore.de/z/place" "zettelstore.de/z/usecase" "zettelstore.de/z/web/adapter" ) // MakeGetHTMLZettelHandler creates a new HTTP handler for the use case "get zettel". func (wui *WebUI) MakeGetHTMLZettelHandler(parseZettel usecase.ParseZettel, getMeta usecase.GetMeta) http.HandlerFunc { return func(w http.ResponseWriter, r *http.Request) { ctx := r.Context() zid, err := id.Parse(r.URL.Path[1:]) if err != nil { wui.reportError(ctx, w, place.ErrNotFound) return } syntax := r.URL.Query().Get("syntax") zn, err := parseZettel.Run(ctx, zid, syntax) if err != nil { wui.reportError(ctx, w, err) return } lang := config.GetLang(zn.InhMeta, wui.rtConfig) envHTML := encoder.Environment{ LinkAdapter: adapter.MakeLinkAdapter(ctx, wui, 'h', getMeta, "", ""), ImageAdapter: adapter.MakeImageAdapter(ctx, wui, getMeta), CiteAdapter: nil, Lang: lang, Xhtml: false, MarkerExternal: wui.rtConfig.GetMarkerExternal(), NewWindow: true, IgnoreMeta: map[string]bool{meta.KeyTitle: true, meta.KeyLang: true}, } metaHeader, err := formatMeta(zn.InhMeta, "html", &envHTML) if err != nil { wui.reportError(ctx, w, err) return } htmlTitle, err := adapter.FormatInlines( encfun.MetaAsInlineSlice(zn.InhMeta, meta.KeyTitle), "html", &envHTML) if err != nil { wui.reportError(ctx, w, err) return } htmlContent, err := formatBlocks(zn.Ast, "html", &envHTML) if err != nil { wui.reportError(ctx, w, err) return } textTitle := encfun.MetaAsText(zn.InhMeta, meta.KeyTitle) user := wui.getUser(ctx) roleText := zn.Meta.GetDefault(meta.KeyRole, "*") tags := wui.buildTagInfos(zn.Meta) getTitle := makeGetTitle(ctx, getMeta, &encoder.Environment{Lang: lang}) extURL, hasExtURL := zn.Meta.Get(meta.KeyURL) backLinks := wui.formatBackLinks(zn.InhMeta, getTitle) var base baseData wui.makeBaseData(ctx, lang, textTitle, user, &base) base.MetaHeader = metaHeader wui.renderTemplate(ctx, w, id.ZettelTemplateZid, &base, struct { HTMLTitle string CanWrite bool EditURL string Zid string InfoURL string RoleText string RoleURL string HasTags bool Tags []simpleLink CanCopy bool CopyURL string CanFolge bool FolgeURL string FolgeRefs string PrecursorRefs string HasExtURL bool ExtURL string ExtNewWindow string Content string HasBackLinks bool BackLinks []simpleLink }{ HTMLTitle: htmlTitle, CanWrite: wui.canWrite(ctx, user, zn.Meta, zn.Content), EditURL: wui.NewURLBuilder('e').SetZid(zid).String(), Zid: zid.String(), InfoURL: wui.NewURLBuilder('i').SetZid(zid).String(), RoleText: roleText, RoleURL: wui.NewURLBuilder('h').AppendQuery("role", roleText).String(), HasTags: len(tags) > 0, Tags: tags, CanCopy: base.CanCreate && !zn.Content.IsBinary(), CopyURL: wui.NewURLBuilder('c').SetZid(zid).String(), CanFolge: base.CanCreate, FolgeURL: wui.NewURLBuilder('f').SetZid(zid).String(), FolgeRefs: wui.formatMetaKey(zn.InhMeta, meta.KeyFolge, getTitle), PrecursorRefs: wui.formatMetaKey(zn.InhMeta, meta.KeyPrecursor, getTitle), ExtURL: extURL, HasExtURL: hasExtURL, ExtNewWindow: htmlAttrNewWindow(envHTML.NewWindow && hasExtURL), Content: htmlContent, HasBackLinks: len(backLinks) > 0, BackLinks: backLinks, }) } } func formatBlocks(bs ast.BlockSlice, format string, env *encoder.Environment) (string, error) { enc := encoder.Create(format, env) if enc == nil { return "", adapter.ErrNoSuchFormat } var content strings.Builder _, err := enc.WriteBlocks(&content, bs) if err != nil { return "", err } return content.String(), nil } func formatMeta(m *meta.Meta, format string, env *encoder.Environment) (string, error) { enc := encoder.Create(format, env) if enc == nil { return "", adapter.ErrNoSuchFormat } var content strings.Builder _, err := enc.WriteMeta(&content, m) if err != nil { return "", err } return content.String(), nil } func (wui *WebUI) buildTagInfos(m *meta.Meta) []simpleLink { var tagInfos []simpleLink if tags, ok := m.GetList(meta.KeyTags); ok { ub := wui.NewURLBuilder('h') tagInfos = make([]simpleLink, len(tags)) for i, tag := range tags { tagInfos[i] = simpleLink{Text: tag, URL: ub.AppendQuery("tags", tag).String()} ub.ClearQuery() } } return tagInfos } func (wui *WebUI) formatMetaKey(m *meta.Meta, key string, getTitle getTitleFunc) string { if _, ok := m.Get(key); ok { var buf bytes.Buffer wui.writeHTMLMetaValue(&buf, m, key, getTitle, nil) return buf.String() } return "" } func (wui *WebUI) formatBackLinks(m *meta.Meta, getTitle getTitleFunc) []simpleLink { values, ok := m.GetList(meta.KeyBack) if !ok || len(values) == 0 { return nil } result := make([]simpleLink, 0, len(values)) for _, val := range values { zid, err := id.Parse(val) if err != nil { continue } if title, found := getTitle(zid, "text"); found > 0 { url := wui.NewURLBuilder('h').SetZid(zid).String() if title == "" { result = append(result, simpleLink{Text: val, URL: url}) } else { result = append(result, simpleLink{Text: title, URL: url}) } } } return result } |
Deleted web/adapter/webui/goaction.go.
|
| < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < |
Changes to web/adapter/webui/home.go.
1 | //----------------------------------------------------------------------------- | | | < < < > < < | | | < > | | | | < | | | | | | | | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 | //----------------------------------------------------------------------------- // Copyright (c) 2020-2021 Detlef Stern // // This file is part of zettelstore. // // Zettelstore is licensed under the latest version of the EUPL (European Union // Public License). Please see file LICENSE.txt for your rights and obligations // under this license. //----------------------------------------------------------------------------- // Package webui provides web-UI handlers for web requests. package webui import ( "context" "errors" "net/http" "zettelstore.de/z/domain/id" "zettelstore.de/z/domain/meta" "zettelstore.de/z/place" ) type getRootStore interface { // GetMeta retrieves just the meta data of a specific zettel. GetMeta(ctx context.Context, zid id.Zid) (*meta.Meta, error) } // 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, place.ErrNotFound) return } homeZid := wui.rtConfig.GetHomeZettel() if homeZid != id.DefaultHomeZid { if _, err := s.GetMeta(ctx, homeZid); err == nil { redirectFound(w, r, wui.NewURLBuilder('h').SetZid(homeZid)) return } homeZid = id.DefaultHomeZid } _, err := s.GetMeta(ctx, homeZid) if err == nil { redirectFound(w, r, wui.NewURLBuilder('h').SetZid(homeZid)) return } if errors.Is(err, &place.ErrNotAllowed{}) && wui.authz.WithAuth() && wui.getUser(ctx) == nil { redirectFound(w, r, wui.NewURLBuilder('a')) return } redirectFound(w, r, wui.NewURLBuilder('h')) } } |
Deleted web/adapter/webui/htmlgen.go.
|
| < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < |
Changes to web/adapter/webui/htmlmeta.go.
1 | //----------------------------------------------------------------------------- | | | < < < > > > > > | < < < | | | | | > > > | < < < < < | > > < > | | > | > | | > | > | < | < < < < < < < < > > > > | > < > > > > > > > | > > > > > > > > | > > > | > > | | < < | > > | < < < > | > > > | > > > > | > | > > > > | < < < | | < | < | | > > | | | > | < < < | < < | | > > > | | | | > | | > > > | | > > > | > | < > > | < > | | | > > > | < < < < < < < | > | < < | > > > | | | | | > > > | | > | | < < < < | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 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 | //----------------------------------------------------------------------------- // Copyright (c) 2020-2021 Detlef Stern // // This file is part of zettelstore. // // Zettelstore is licensed under the latest version of the EUPL (European Union // Public License). Please see file LICENSE.txt for your rights and obligations // under this license. //----------------------------------------------------------------------------- // Package webui provides web-UI handlers for web requests. package webui import ( "context" "errors" "fmt" "io" "net/url" "time" "zettelstore.de/z/domain/id" "zettelstore.de/z/domain/meta" "zettelstore.de/z/encoder" "zettelstore.de/z/parser" "zettelstore.de/z/place" "zettelstore.de/z/strfun" "zettelstore.de/z/usecase" "zettelstore.de/z/web/adapter" ) var space = []byte{' '} func (wui *WebUI) writeHTMLMetaValue(w io.Writer, m *meta.Meta, key string, getTitle getTitleFunc, env *encoder.Environment) { switch kt := m.Type(key); kt { case meta.TypeBool: wui.writeHTMLBool(w, key, m.GetBool(key)) case meta.TypeCredential: writeCredential(w, m.GetDefault(key, "???c")) case meta.TypeEmpty: writeEmpty(w, m.GetDefault(key, "???e")) case meta.TypeID: wui.writeIdentifier(w, m.GetDefault(key, "???i"), getTitle) case meta.TypeIDSet: if l, ok := m.GetList(key); ok { wui.writeIdentifierSet(w, l, getTitle) } case meta.TypeNumber: writeNumber(w, m.GetDefault(key, "???n")) case meta.TypeString: writeString(w, m.GetDefault(key, "???s")) case meta.TypeTagSet: if l, ok := m.GetList(key); ok { wui.writeTagSet(w, key, l) } case meta.TypeTimestamp: if ts, ok := m.GetTime(key); ok { writeTimestamp(w, ts) } case meta.TypeURL: writeURL(w, m.GetDefault(key, "???u")) case meta.TypeWord: wui.writeWord(w, key, m.GetDefault(key, "???w")) case meta.TypeWordSet: if l, ok := m.GetList(key); ok { wui.writeWordSet(w, key, l) } case meta.TypeZettelmarkup: writeZettelmarkup(w, m.GetDefault(key, "???z"), env) case meta.TypeUnknown: writeUnknown(w, m.GetDefault(key, "???u")) default: strfun.HTMLEscape(w, m.GetDefault(key, "???w"), false) fmt.Fprintf(w, " <b>(Unhandled type: %v, key: %v)</b>", kt, key) } } func (wui *WebUI) writeHTMLBool(w io.Writer, key string, val bool) { if val { wui.writeLink(w, key, "true", "True") } else { wui.writeLink(w, key, "false", "False") } } func writeCredential(w io.Writer, val string) { strfun.HTMLEscape(w, val, false) } func writeEmpty(w io.Writer, val string) { strfun.HTMLEscape(w, val, false) } func (wui *WebUI) writeIdentifier(w io.Writer, val string, getTitle func(id.Zid, string) (string, int)) { zid, err := id.Parse(val) if err != nil { strfun.HTMLEscape(w, val, false) return } title, found := getTitle(zid, "text") switch { case found > 0: if title == "" { fmt.Fprintf(w, "<a href=\"%v\">%v</a>", wui.NewURLBuilder('h').SetZid(zid), zid) } else { fmt.Fprintf(w, "<a href=\"%v\" title=\"%v\">%v</a>", wui.NewURLBuilder('h').SetZid(zid), title, zid) } case found == 0: fmt.Fprintf(w, "<s>%v</s>", val) case found < 0: io.WriteString(w, val) } } func (wui *WebUI) writeIdentifierSet(w io.Writer, vals []string, getTitle func(id.Zid, string) (string, int)) { for i, val := range vals { if i > 0 { w.Write(space) } wui.writeIdentifier(w, val, getTitle) } } func writeNumber(w io.Writer, val string) { strfun.HTMLEscape(w, val, false) } func writeString(w io.Writer, val string) { strfun.HTMLEscape(w, val, false) } func writeUnknown(w io.Writer, val string) { strfun.HTMLEscape(w, val, false) } func (wui *WebUI) writeTagSet(w io.Writer, key string, tags []string) { for i, tag := range tags { if i > 0 { w.Write(space) } wui.writeLink(w, key, tag, tag) } } func writeTimestamp(w io.Writer, ts time.Time) { io.WriteString(w, ts.Format("2006-01-02 15:04:05")) } func writeURL(w io.Writer, val string) { u, err := url.Parse(val) if err != nil { strfun.HTMLEscape(w, val, false) return } fmt.Fprintf(w, "<a href=\"%v\">", u) strfun.HTMLEscape(w, val, false) io.WriteString(w, "</a>") } func (wui *WebUI) writeWord(w io.Writer, key, word string) { wui.writeLink(w, key, word, word) } func (wui *WebUI) writeWordSet(w io.Writer, key string, words []string) { for i, word := range words { if i > 0 { w.Write(space) } wui.writeWord(w, key, word) } } func writeZettelmarkup(w io.Writer, val string, env *encoder.Environment) { title, err := adapter.FormatInlines(parser.ParseMetadata(val), "html", env) if err != nil { strfun.HTMLEscape(w, val, false) return } io.WriteString(w, title) } func (wui *WebUI) writeLink(w io.Writer, key, value, text string) { fmt.Fprintf(w, "<a href=\"%v?%v=%v\">", wui.NewURLBuilder('h'), url.QueryEscape(key), url.QueryEscape(value)) strfun.HTMLEscape(w, text, false) io.WriteString(w, "</a>") } type getTitleFunc func(id.Zid, string) (string, int) func makeGetTitle(ctx context.Context, getMeta usecase.GetMeta, env *encoder.Environment) getTitleFunc { return func(zid id.Zid, format string) (string, int) { m, err := getMeta.Run(place.NoEnrichContext(ctx), zid) if err != nil { if errors.Is(err, &place.ErrNotAllowed{}) { return "", -1 } return "", 0 } astTitle := parser.ParseMetadata(m.GetDefault(meta.KeyTitle, "")) title, err := adapter.FormatInlines(astTitle, format, env) if err == nil { return title, 1 } return "", 1 } } |
Changes to web/adapter/webui/lists.go.
1 | //----------------------------------------------------------------------------- | | | < < < > < | < < < < | | | | | | < < < > > > > > > > | < < | > > | > | | > > > > > > | | > > > > > | > | > > > > > | < > | > | | > > > | | | | | | | | > | > | | > > > > | > | | | | < | < < | | | > > > > > > | | > | < < > > > | | | | | | > > > > > > > > > > > | > | | > > | > > > > > > > > | > | | < | < | < > | | < | < | > > | > > | < | | | > > > > | > > > > > | > | | | < > > > > > | > | | < < < < < < < > | < | < < < < < < > | < > > > > > | | | < > > > > > > < < > > > > | | | | < < | > > | | | | > | < | > > | > > > > > > | | | < > | | | > > | < < | | > > > > > > | > > > | | < | | < < < | | > > > > | | | < | > > > > | < < < | > > | | | | | | < < | | > > | | > | | > > > > > > > > > | | | | | > | | < | | | | | | > > | < < | > > > | > > > > > | < < < | | | > | > | < > > > > > | > < | > | | > > > > | | | > | | > | | < | | > > | > > | | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 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 | //----------------------------------------------------------------------------- // Copyright (c) 2020-2021 Detlef Stern // // This file is part of zettelstore. // // Zettelstore is licensed under the latest version of the EUPL (European Union // Public License). Please see file LICENSE.txt for your rights and obligations // under this license. //----------------------------------------------------------------------------- // Package webui provides web-UI handlers for web requests. package webui import ( "context" "net/http" "net/url" "sort" "strconv" "strings" "zettelstore.de/z/domain/id" "zettelstore.de/z/domain/meta" "zettelstore.de/z/encoder" "zettelstore.de/z/parser" "zettelstore.de/z/place" "zettelstore.de/z/search" "zettelstore.de/z/usecase" "zettelstore.de/z/web/adapter" ) // MakeListHTMLMetaHandler creates a HTTP handler for rendering the list of // zettel as HTML. func (wui *WebUI) MakeListHTMLMetaHandler( listMeta usecase.ListMeta, listRole usecase.ListRole, listTags usecase.ListTags, ) http.HandlerFunc { return func(w http.ResponseWriter, r *http.Request) { query := r.URL.Query() switch query.Get("_l") { case "r": wui.renderRolesList(w, r, listRole) case "t": wui.renderTagsList(w, r, listTags) default: wui.renderZettelList(w, r, listMeta) } } } func (wui *WebUI) renderZettelList(w http.ResponseWriter, r *http.Request, listMeta usecase.ListMeta) { query := r.URL.Query() s := adapter.GetSearch(query, false) ctx := r.Context() title := wui.listTitleSearch("Filter", s) wui.renderMetaList( ctx, w, title, s, func(s *search.Search) ([]*meta.Meta, error) { if !s.HasComputedMetaKey() { ctx = place.NoEnrichContext(ctx) } return listMeta.Run(ctx, s) }, func(offset int) string { return wui.newPageURL('h', query, offset, "_offset", "_limit") }) } type roleInfo struct { Text string URL string } func (wui *WebUI) renderRolesList(w http.ResponseWriter, r *http.Request, listRole usecase.ListRole) { ctx := r.Context() roleList, err := listRole.Run(ctx) if err != nil { adapter.ReportUsecaseError(w, err) return } roleInfos := make([]roleInfo, 0, len(roleList)) for _, role := range roleList { roleInfos = append( roleInfos, roleInfo{role, wui.NewURLBuilder('h').AppendQuery("role", role).String()}) } user := wui.getUser(ctx) var base baseData wui.makeBaseData(ctx, wui.rtConfig.GetDefaultLang(), wui.rtConfig.GetSiteName(), user, &base) wui.renderTemplate(ctx, w, id.RolesTemplateZid, &base, struct { Roles []roleInfo }{ Roles: roleInfos, }) } type countInfo struct { Count string URL string } type tagInfo struct { Name string URL string count int Count string Size string } var fontSizes = [...]int{75, 83, 100, 117, 150, 200} func (wui *WebUI) renderTagsList(w http.ResponseWriter, r *http.Request, listTags usecase.ListTags) { ctx := r.Context() iMinCount, _ := strconv.Atoi(r.URL.Query().Get("min")) tagData, err := listTags.Run(ctx, iMinCount) if err != nil { wui.reportError(ctx, w, err) return } user := wui.getUser(ctx) tagsList := make([]tagInfo, 0, len(tagData)) countMap := make(map[int]int) baseTagListURL := wui.NewURLBuilder('h') for tag, ml := range tagData { count := len(ml) countMap[count]++ tagsList = append( tagsList, tagInfo{tag, baseTagListURL.AppendQuery("tags", tag).String(), count, "", ""}) baseTagListURL.ClearQuery() } sort.Slice(tagsList, func(i, j int) bool { return tagsList[i].Name < tagsList[j].Name }) countList := make([]int, 0, len(countMap)) for count := range countMap { countList = append(countList, count) } sort.Ints(countList) for pos, count := range countList { countMap[count] = fontSizes[(pos*len(fontSizes))/len(countList)] } for i := 0; i < len(tagsList); i++ { count := tagsList[i].count tagsList[i].Count = strconv.Itoa(count) tagsList[i].Size = strconv.Itoa(countMap[count]) } var base baseData wui.makeBaseData(ctx, wui.rtConfig.GetDefaultLang(), wui.rtConfig.GetSiteName(), user, &base) minCounts := make([]countInfo, 0, len(countList)) for _, c := range countList { sCount := strconv.Itoa(c) minCounts = append(minCounts, countInfo{sCount, base.ListTagsURL + "&min=" + sCount}) } wui.renderTemplate(ctx, w, id.TagsTemplateZid, &base, struct { ListTagsURL string MinCounts []countInfo Tags []tagInfo }{ ListTagsURL: base.ListTagsURL, MinCounts: minCounts, Tags: tagsList, }) } // MakeSearchHandler creates a new HTTP handler for the use case "search". func (wui *WebUI) MakeSearchHandler( ucSearch usecase.Search, getMeta usecase.GetMeta, getZettel usecase.GetZettel, ) http.HandlerFunc { return func(w http.ResponseWriter, r *http.Request) { query := r.URL.Query() ctx := r.Context() s := adapter.GetSearch(query, true) if s == nil { redirectFound(w, r, wui.NewURLBuilder('h')) return } title := wui.listTitleSearch("Search", s) wui.renderMetaList( ctx, w, title, s, func(s *search.Search) ([]*meta.Meta, error) { if !s.HasComputedMetaKey() { ctx = place.NoEnrichContext(ctx) } return ucSearch.Run(ctx, s) }, func(offset int) string { return wui.newPageURL('f', query, offset, "offset", "limit") }) } } // MakeZettelContextHandler creates a new HTTP handler for the use case "zettel context". func (wui *WebUI) MakeZettelContextHandler(getContext usecase.ZettelContext) http.HandlerFunc { return func(w http.ResponseWriter, r *http.Request) { ctx := r.Context() zid, err := id.Parse(r.URL.Path[1:]) if err != nil { wui.reportError(ctx, w, place.ErrNotFound) return } q := r.URL.Query() dir := usecase.ParseZCDirection(q.Get("dir")) depth := getIntParameter(q, "depth", 5) limit := getIntParameter(q, "limit", 200) metaList, err := getContext.Run(ctx, zid, dir, depth, limit) if err != nil { wui.reportError(ctx, w, err) return } metaLinks, err := wui.buildHTMLMetaList(metaList) if err != nil { adapter.InternalServerError(w, "Build HTML meta list", err) return } depths := []string{"2", "3", "4", "5", "6", "7", "8", "9", "10"} depthLinks := make([]simpleLink, len(depths)) depthURL := wui.NewURLBuilder('j').SetZid(zid) for i, depth := range depths { depthURL.ClearQuery() switch dir { case usecase.ZettelContextBackward: depthURL.AppendQuery("dir", "backward") case usecase.ZettelContextForward: depthURL.AppendQuery("dir", "forward") } depthURL.AppendQuery("depth", depth) depthLinks[i].Text = depth depthLinks[i].URL = depthURL.String() } var base baseData user := wui.getUser(ctx) wui.makeBaseData(ctx, wui.rtConfig.GetDefaultLang(), wui.rtConfig.GetSiteName(), user, &base) wui.renderTemplate(ctx, w, id.ContextTemplateZid, &base, struct { Title string InfoURL string Depths []simpleLink Start simpleLink Metas []simpleLink }{ Title: "Zettel Context", InfoURL: wui.NewURLBuilder('i').SetZid(zid).String(), Depths: depthLinks, Start: metaLinks[0], Metas: metaLinks[1:], }) } } func getIntParameter(q url.Values, key string, minValue int) int { val, ok := adapter.GetInteger(q, key) if !ok || val < 0 { return minValue } return val } func (wui *WebUI) renderMetaList( ctx context.Context, w http.ResponseWriter, title string, s *search.Search, ucMetaList func(sorter *search.Search) ([]*meta.Meta, error), pageURL func(int) string) { var metaList []*meta.Meta var err error var prevURL, nextURL string if lps := wui.rtConfig.GetListPageSize(); lps > 0 { if s.GetLimit() < lps { s.SetLimit(lps + 1) } metaList, err = ucMetaList(s) if err != nil { wui.reportError(ctx, w, err) return } if offset := s.GetOffset(); offset > 0 { offset -= lps if offset < 0 { offset = 0 } prevURL = pageURL(offset) } if len(metaList) >= s.GetLimit() { nextURL = pageURL(s.GetOffset() + lps) metaList = metaList[:len(metaList)-1] } } else { metaList, err = ucMetaList(s) if err != nil { wui.reportError(ctx, w, err) return } } user := wui.getUser(ctx) metas, err := wui.buildHTMLMetaList(metaList) if err != nil { wui.reportError(ctx, w, err) return } var base baseData wui.makeBaseData(ctx, wui.rtConfig.GetDefaultLang(), wui.rtConfig.GetSiteName(), user, &base) wui.renderTemplate(ctx, w, id.ListTemplateZid, &base, struct { Title string Metas []simpleLink HasPrevNext bool HasPrev bool PrevURL string HasNext bool NextURL string }{ Title: title, Metas: metas, HasPrevNext: len(prevURL) > 0 || len(nextURL) > 0, HasPrev: len(prevURL) > 0, PrevURL: prevURL, HasNext: len(nextURL) > 0, NextURL: nextURL, }) } func (wui *WebUI) listTitleSearch(prefix string, s *search.Search) string { if s == nil { return wui.rtConfig.GetSiteName() } var sb strings.Builder sb.WriteString(prefix) if s != nil { sb.WriteString(": ") s.Print(&sb) } return sb.String() } func (wui *WebUI) newPageURL(key byte, query url.Values, offset int, offsetKey, limitKey string) string { ub := wui.NewURLBuilder(key) for key, values := range query { if key != offsetKey && key != limitKey { for _, val := range values { ub.AppendQuery(key, val) } } } if offset > 0 { ub.AppendQuery(offsetKey, strconv.Itoa(offset)) } return ub.String() } // buildHTMLMetaList builds a zettel list based on a meta list for HTML rendering. func (wui *WebUI) buildHTMLMetaList(metaList []*meta.Meta) ([]simpleLink, error) { defaultLang := wui.rtConfig.GetDefaultLang() metas := make([]simpleLink, 0, len(metaList)) for _, m := range metaList { var lang string if val, ok := m.Get(meta.KeyLang); ok { lang = val } else { lang = defaultLang } title, _ := m.Get(meta.KeyTitle) env := encoder.Environment{Lang: lang, Interactive: true} htmlTitle, err := adapter.FormatInlines(parser.ParseMetadata(title), "html", &env) if err != nil { return nil, err } metas = append(metas, simpleLink{ Text: htmlTitle, URL: wui.NewURLBuilder('h').SetZid(m.Zid).String(), }) } return metas, nil } |
Changes to web/adapter/webui/login.go.
1 | //----------------------------------------------------------------------------- | | | < < < > < < | | | | < | < < < < < < > | < < | > > | > | < | | | | | | > > > > > > > > | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 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-2021 Detlef Stern // // This file is part of zettelstore. // // Zettelstore is licensed under the latest version of the EUPL (European Union // Public License). Please see file LICENSE.txt for your rights and obligations // under this license. //----------------------------------------------------------------------------- // Package webui provides web-UI handlers for web requests. package webui import ( "context" "net/http" "zettelstore.de/z/auth" "zettelstore.de/z/domain/id" "zettelstore.de/z/usecase" "zettelstore.de/z/web/adapter" ) // MakeGetLoginHandler creates a new HTTP handler to display the HTML login view. func (wui *WebUI) MakeGetLoginHandler() http.HandlerFunc { return func(w http.ResponseWriter, r *http.Request) { wui.renderLoginForm(wui.clearToken(r.Context(), w), w, false) } } func (wui *WebUI) renderLoginForm(ctx context.Context, w http.ResponseWriter, retry bool) { var base baseData wui.makeBaseData(ctx, wui.rtConfig.GetDefaultLang(), "Login", nil, &base) wui.renderTemplate(ctx, w, id.LoginTemplateZid, &base, struct { Title string Retry bool }{ Title: base.Title, Retry: retry, }) } // MakePostLoginHandlerHTML creates a new HTTP handler to authenticate the given user. func (wui *WebUI) MakePostLoginHandlerHTML(ucAuth usecase.Authenticate) http.HandlerFunc { return func(w http.ResponseWriter, r *http.Request) { if !wui.authz.WithAuth() { redirectFound(w, r, wui.NewURLBuilder('/')) return } 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, 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) redirectFound(w, r, wui.NewURLBuilder('/')) } } // MakeGetLogoutHandler creates a new HTTP handler to log out the current user func (wui *WebUI) MakeGetLogoutHandler() http.HandlerFunc { return func(w http.ResponseWriter, r *http.Request) { wui.clearToken(r.Context(), w) redirectFound(w, r, wui.NewURLBuilder('/')) } } |
Deleted web/adapter/webui/meta.go.
|
| < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < |
Deleted web/adapter/webui/meta_test.go.
|
| < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < |
Changes to web/adapter/webui/rename_zettel.go.
1 | //----------------------------------------------------------------------------- | | | < < < > | | | | | | | < | | | | > > > > | | | < > | < < < | < | > | < | > | | < | | < < | < < | < < < | < | | | | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 | //----------------------------------------------------------------------------- // Copyright (c) 2020-2021 Detlef Stern // // This file is part of zettelstore. // // Zettelstore is licensed under the latest version of the EUPL (European Union // Public License). Please see file LICENSE.txt for your rights and obligations // under this license. //----------------------------------------------------------------------------- // Package webui provides web-UI handlers for web requests. package webui import ( "fmt" "net/http" "strings" "zettelstore.de/z/config" "zettelstore.de/z/domain/id" "zettelstore.de/z/domain/meta" "zettelstore.de/z/place" "zettelstore.de/z/usecase" "zettelstore.de/z/web/adapter" ) // MakeGetRenameZettelHandler creates a new HTTP handler to display the // 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, place.ErrNotFound) return } m, err := getMeta.Run(ctx, zid) if err != nil { wui.reportError(ctx, w, err) return } if format := adapter.GetFormat(r, r.URL.Query(), "html"); format != "html" { wui.reportError(ctx, w, adapter.NewErrBadRequest( fmt.Sprintf("Rename zettel %q not possible in format %q", zid.String(), format))) return } user := wui.getUser(ctx) var base baseData wui.makeBaseData(ctx, config.GetLang(m, wui.rtConfig), "Rename Zettel "+zid.String(), user, &base) wui.renderTemplate(ctx, w, id.RenameTemplateZid, &base, struct { Zid string MetaPairs []meta.Pair }{ Zid: zid.String(), MetaPairs: m.Pairs(true), }) } } // MakePostRenameZettelHandler creates a new HTTP handler to rename an existing zettel. func (wui *WebUI) MakePostRenameZettelHandler(renameZettel usecase.RenameZettel) http.HandlerFunc { 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, place.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 } newZid, err := id.Parse(strings.TrimSpace(r.PostFormValue("newzid"))) if err != nil { wui.reportError(ctx, w, adapter.NewErrBadRequest(fmt.Sprintf("Invalid new zettel id %q", newZid))) return } if err := renameZettel.Run(r.Context(), curZid, newZid); err != nil { wui.reportError(ctx, w, err) return } redirectFound(w, r, wui.NewURLBuilder('h').SetZid(newZid)) } } |
Changes to web/adapter/webui/response.go.
1 | //----------------------------------------------------------------------------- | | | < < < > | | < < | | 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 Detlef Stern // // This file is part of zettelstore. // // Zettelstore is licensed under 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 ( "net/http" "zettelstore.de/z/web/server" ) func redirectFound(w http.ResponseWriter, r *http.Request, ub server.URLBuilder) { http.Redirect(w, r, ub.String(), http.StatusFound) } |
Deleted web/adapter/webui/sxn_code.go.
|
| < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < |
Deleted web/adapter/webui/template.go.
|
| < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < |
Changes to web/adapter/webui/webui.go.
1 | //----------------------------------------------------------------------------- | | | < < < > > | | < | | | | | | | | | | | | < < | < | < | < < < < | < < < < < | < < < < | | | | | | | | | < < < < | < < < < | > | | | < | < | < | < < < < | < < < < < | | | | < < < < | < < | < | | > | < | < < | | | | | > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > | > > > > > > > > > > | | > > > > | < > > | | > > > > > > > > > > > > > > > | < < < < < < < < < < < < < | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 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 | //----------------------------------------------------------------------------- // Copyright (c) 2021 Detlef Stern // // This file is part of zettelstore. // // Zettelstore is licensed under the latest version of the EUPL (European Union // Public License). Please see file LICENSE.txt for your rights and obligations // under this license. //----------------------------------------------------------------------------- // Package webui provides web-UI handlers for web requests. package webui import ( "bytes" "context" "log" "net/http" "sync" "time" "zettelstore.de/z/auth" "zettelstore.de/z/collect" "zettelstore.de/z/config" "zettelstore.de/z/domain" "zettelstore.de/z/domain/id" "zettelstore.de/z/domain/meta" "zettelstore.de/z/encoder" "zettelstore.de/z/input" "zettelstore.de/z/kernel" "zettelstore.de/z/parser" "zettelstore.de/z/place" "zettelstore.de/z/template" "zettelstore.de/z/web/adapter" "zettelstore.de/z/web/server" ) // WebUI holds all data for delivering the web ui. type WebUI struct { ab server.AuthBuilder authz auth.AuthzManager rtConfig config.Config token auth.TokenManager place webuiPlace policy auth.Policy templateCache map[id.Zid]*template.Template mxCache sync.RWMutex tokenLifetime time.Duration stylesheetURL string homeURL string listZettelURL string listRolesURL string listTagsURL string withAuth bool loginURL string searchURL string } type webuiPlace interface { CanCreateZettel(ctx context.Context) bool GetZettel(ctx context.Context, zid id.Zid) (domain.Zettel, error) GetMeta(ctx context.Context, zid id.Zid) (*meta.Meta, error) CanUpdateZettel(ctx context.Context, zettel domain.Zettel) bool AllowRenameZettel(ctx context.Context, zid id.Zid) bool CanDeleteZettel(ctx context.Context, zid id.Zid) bool } // New creates a new WebUI struct. func New(ab server.AuthBuilder, authz auth.AuthzManager, rtConfig config.Config, token auth.TokenManager, mgr place.Manager, pol auth.Policy) *WebUI { wui := &WebUI{ ab: ab, rtConfig: rtConfig, authz: authz, token: token, place: mgr, policy: pol, tokenLifetime: kernel.Main.GetConfig(kernel.WebService, kernel.WebTokenLifetimeHTML).(time.Duration), stylesheetURL: ab.NewURLBuilder('z').SetZid( id.BaseCSSZid).AppendQuery("_format", "raw").AppendQuery( "_part", "content").String(), homeURL: ab.NewURLBuilder('/').String(), listZettelURL: ab.NewURLBuilder('h').String(), listRolesURL: ab.NewURLBuilder('h').AppendQuery("_l", "r").String(), listTagsURL: ab.NewURLBuilder('h').AppendQuery("_l", "t").String(), withAuth: authz.WithAuth(), loginURL: ab.NewURLBuilder('a').String(), searchURL: ab.NewURLBuilder('f').String(), } wui.observe(place.UpdateInfo{Place: mgr, Reason: place.OnReload, Zid: id.Invalid}) mgr.RegisterObserver(wui.observe) return wui } func (wui *WebUI) observe(ci place.UpdateInfo) { wui.mxCache.Lock() if ci.Reason == place.OnReload || ci.Zid == id.BaseTemplateZid { wui.templateCache = make(map[id.Zid]*template.Template, len(wui.templateCache)) } else { delete(wui.templateCache, ci.Zid) } wui.mxCache.Unlock() } 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) canCreate(ctx context.Context, user *meta.Meta) bool { m := meta.New(id.Invalid) return wui.policy.CanCreate(user, m) && wui.place.CanCreateZettel(ctx) } func (wui *WebUI) canWrite( ctx context.Context, user, meta *meta.Meta, content domain.Content) bool { return wui.policy.CanWrite(user, meta, meta) && wui.place.CanUpdateZettel(ctx, domain.Zettel{Meta: meta, Content: content}) } func (wui *WebUI) canRename(ctx context.Context, user, m *meta.Meta) bool { return wui.policy.CanRename(user, m) && wui.place.AllowRenameZettel(ctx, m.Zid) } func (wui *WebUI) canDelete(ctx context.Context, user, m *meta.Meta) bool { return wui.policy.CanDelete(user, m) && wui.place.CanDeleteZettel(ctx, m.Zid) } func (wui *WebUI) getTemplate( ctx context.Context, templateID id.Zid) (*template.Template, error) { if t, ok := wui.cacheGetTemplate(templateID); ok { return t, nil } realTemplateZettel, err := wui.place.GetZettel(ctx, templateID) if err != nil { return nil, err } t, err := template.ParseString(realTemplateZettel.Content.AsString(), nil) if err == nil { // t.SetErrorOnMissing() wui.cacheSetTemplate(templateID, t) } return t, err } type simpleLink struct { Text string URL string } type baseData struct { Lang string MetaHeader string StylesheetURL string Title string HomeURL string WithUser bool WithAuth bool UserIsValid bool UserZettelURL string UserIdent string UserLogoutURL string LoginURL string ListZettelURL string ListRolesURL string ListTagsURL string CanCreate bool NewZettelURL string NewZettelLinks []simpleLink SearchURL string Content string FooterHTML string } func (wui *WebUI) makeBaseData( ctx context.Context, lang, title string, user *meta.Meta, data *baseData) { var ( newZettelLinks []simpleLink userZettelURL string userIdent string userLogoutURL string ) canCreate := wui.canCreate(ctx, user) if canCreate { newZettelLinks = wui.fetchNewTemplates(ctx, user) } userIsValid := user != nil if userIsValid { userZettelURL = wui.NewURLBuilder('h').SetZid(user.Zid).String() userIdent = user.GetDefault(meta.KeyUserID, "") userLogoutURL = wui.NewURLBuilder('a').SetZid(user.Zid).String() } data.Lang = lang data.StylesheetURL = wui.stylesheetURL data.Title = title data.HomeURL = wui.homeURL data.WithAuth = wui.withAuth data.WithUser = data.WithAuth data.UserIsValid = userIsValid data.UserZettelURL = userZettelURL data.UserIdent = userIdent data.UserLogoutURL = userLogoutURL data.LoginURL = wui.loginURL data.ListZettelURL = wui.listZettelURL data.ListRolesURL = wui.listRolesURL data.ListTagsURL = wui.listTagsURL data.CanCreate = canCreate data.NewZettelLinks = newZettelLinks data.SearchURL = wui.searchURL data.FooterHTML = wui.rtConfig.GetFooterHTML() } // htmlAttrNewWindow eturns HTML attribute string for opening a link in a new window. // If hasURL is false an empty string is returned. func htmlAttrNewWindow(hasURL bool) string { if hasURL { return " target=\"_blank\" ref=\"noopener noreferrer\"" } return "" } func (wui *WebUI) fetchNewTemplates(ctx context.Context, user *meta.Meta) []simpleLink { ctx = place.NoEnrichContext(ctx) menu, err := wui.place.GetZettel(ctx, id.TOCNewTemplateZid) if err != nil { return nil } zn := parser.ParseZettel(menu, "", wui.rtConfig) refs := collect.Order(zn) result := make([]simpleLink, 0, len(refs)) for _, ref := range refs { zid, err := id.Parse(ref.URL.Path) if err != nil { continue } m, err := wui.place.GetMeta(ctx, zid) if err != nil { continue } if !wui.policy.CanRead(user, m) { continue } title := config.GetTitle(m, wui.rtConfig) astTitle := parser.ParseInlines(input.NewInput(title), meta.ValueSyntaxZmk) env := encoder.Environment{Lang: config.GetLang(m, wui.rtConfig)} menuTitle, err := adapter.FormatInlines(astTitle, "html", &env) if err != nil { menuTitle, err = adapter.FormatInlines(astTitle, "text", nil) if err != nil { menuTitle = title } } result = append(result, simpleLink{ Text: menuTitle, URL: wui.NewURLBuilder('g').SetZid(m.Zid).String(), }) } return result } func (wui *WebUI) renderTemplate( ctx context.Context, w http.ResponseWriter, templateID id.Zid, base *baseData, data interface{}) { wui.renderTemplateStatus(ctx, w, http.StatusOK, templateID, base, data) } func (wui *WebUI) reportError(ctx context.Context, w http.ResponseWriter, err error) { code, text := adapter.CodeMessageFromError(err) if code == http.StatusInternalServerError { log.Printf("%v: %v", text, err) } user := wui.getUser(ctx) var base baseData wui.makeBaseData(ctx, meta.ValueLangEN, "Error", user, &base) wui.renderTemplateStatus(ctx, w, code, id.ErrorTemplateZid, &base, struct { ErrorTitle string ErrorText string }{ ErrorTitle: http.StatusText(code), ErrorText: text, }) } func (wui *WebUI) renderTemplateStatus( ctx context.Context, w http.ResponseWriter, code int, templateID id.Zid, base *baseData, data interface{}) { bt, err := wui.getTemplate(ctx, id.BaseTemplateZid) if err != nil { adapter.InternalServerError(w, "Unable to get base template", err) return } t, err := wui.getTemplate(ctx, templateID) if err != nil { adapter.InternalServerError(w, "Unable to get template", err) return } if user := wui.getUser(ctx); user != nil { if tok, err1 := wui.token.GetToken(user, wui.tokenLifetime, auth.KindHTML); err1 == nil { wui.setToken(w, tok) } } var content bytes.Buffer err = t.Render(&content, data) if err == nil { base.Content = content.String() w.Header().Set(adapter.ContentType, "text/html; charset=utf-8") w.WriteHeader(code) err = bt.Render(w, base) } if err != nil { log.Println("Unable to render template", err) } } func (wui *WebUI) getUser(ctx context.Context) *meta.Meta { return wui.ab.GetUser(ctx) } // GetURLPrefix returns the configured URL prefix of the web server. func (wui *WebUI) GetURLPrefix() string { return wui.ab.GetURLPrefix() } // NewURLBuilder creates a new URL builder object with the given key. func (wui *WebUI) NewURLBuilder(key byte) server.URLBuilder { return wui.ab.NewURLBuilder(key) } func (wui *WebUI) clearToken(ctx context.Context, w http.ResponseWriter) context.Context { return wui.ab.ClearToken(ctx, w) } func (wui *WebUI) setToken(w http.ResponseWriter, token []byte) { wui.ab.SetToken(w, token, wui.tokenLifetime) } |
Deleted web/content/content.go.
|
| < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < |
Deleted web/content/content_test.go.
|
| < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < |
Changes to web/server/impl/http.go.
1 | //----------------------------------------------------------------------------- | | | < < < > > > | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 | //----------------------------------------------------------------------------- // Copyright (c) 2020-2021 Detlef Stern // // This file is part of zettelstore. // // Zettelstore is licensed under the latest version of the EUPL (European Union // Public License). Please see file LICENSE.txt for your rights and obligations // under this license. //----------------------------------------------------------------------------- // Package impl provides the Zettelstore web service. 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 |
︙ | ︙ | |||
66 67 68 69 70 71 72 | } go func() { srv.Serve(ln) }() return nil } // Stop the web server. | | | | 66 67 68 69 70 71 72 73 74 75 76 77 78 | } go func() { srv.Serve(ln) }() return nil } // Stop the web server. func (srv *httpServer) Stop() error { ctx, cancel := context.WithTimeout(context.Background(), shutdownTimeout) defer cancel() return srv.Shutdown(ctx) } |
Changes to web/server/impl/impl.go.
1 | //----------------------------------------------------------------------------- | | | < < < < | < < < | < < | | | | | > > > | > > > > > < < < < < < | < < | < < < < < < < > > > > > > > > > > > > > | | | | | | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 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 Detlef Stern // // This file is part of zettelstore. // // Zettelstore is licensed under 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/z/auth" "zettelstore.de/z/domain/meta" "zettelstore.de/z/web/server" ) type myServer struct { server httpServer router httpRouter persistentCookie bool secureCookie bool } // New creates a new web server. func New(listenAddr, urlPrefix string, persistentCookie, secureCookie bool, auth auth.TokenManager) server.Server { srv := myServer{ persistentCookie: persistentCookie, secureCookie: secureCookie, } srv.router.initializeRouter(urlPrefix, auth) srv.server.initializeHTTPServer(listenAddr, &srv.router) return &srv } func (srv *myServer) Handle(pattern string, handler http.Handler) { srv.router.Handle(pattern, handler) } func (srv *myServer) AddListRoute(key byte, httpMethod string, handler http.Handler) { srv.router.addListRoute(key, httpMethod, handler) } func (srv *myServer) AddZettelRoute(key byte, httpMethod string, handler http.Handler) { srv.router.addZettelRoute(key, httpMethod, handler) } func (srv *myServer) SetUserRetriever(ur server.UserRetriever) { srv.router.ur = ur } func (srv *myServer) GetUser(ctx context.Context) *meta.Meta { if data := srv.GetAuthData(ctx); data != nil { return data.User } return nil } func (srv *myServer) NewURLBuilder(key byte) server.URLBuilder { return &URLBuilder{router: &srv.router, key: key} } func (srv *myServer) GetURLPrefix() string { return srv.router.urlPrefix } const sessionName = "zsession" func (srv *myServer) SetToken(w http.ResponseWriter, token []byte, d time.Duration) { cookie := http.Cookie{ Name: sessionName, Value: string(token), Path: srv.GetURLPrefix(), Secure: srv.secureCookie, HttpOnly: true, SameSite: http.SameSiteStrictMode, } if srv.persistentCookie && d > 0 { cookie.Expires = time.Now().Add(d).Add(30 * time.Second).UTC() } http.SetCookie(w, &cookie) } // ClearToken invalidates the session cookie by sending an empty one. func (srv *myServer) ClearToken(ctx context.Context, w http.ResponseWriter) context.Context { if w != nil { srv.SetToken(w, nil, 0) } return updateContext(ctx, nil, nil) } // GetAuthData returns the full authentication data from the context. func (srv *myServer) GetAuthData(ctx context.Context) *server.AuthData { data, ok := ctx.Value(ctxKeySession).(*server.AuthData) if ok { return data } return nil } type ctxKeyTypeSession struct{} var ctxKeySession ctxKeyTypeSession func updateContext(ctx context.Context, user *meta.Meta, data *auth.TokenData) context.Context { if data == nil { return context.WithValue(ctx, ctxKeySession, &server.AuthData{User: user}) } return context.WithValue( ctx, 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() error { return srv.server.Stop() } |
Changes to web/server/impl/router.go.
1 | //----------------------------------------------------------------------------- | | | < < < > < < < < | | < < < < < < < < < < < | < | > | | | | | | | | | | | | < < < < < < < < < < < < < < < < < < < | < < < < < < < < < < | < > | | < < | | < | | | < < < | | < | | < | | > | > < | < | < < < | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 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 | //----------------------------------------------------------------------------- // Copyright (c) 2020-2021 Detlef Stern // // This file is part of zettelstore. // // Zettelstore is licensed under the latest version of the EUPL (European Union // Public License). Please see file LICENSE.txt for your rights and obligations // under this license. //----------------------------------------------------------------------------- // Package impl provides the Zettelstore web service. package impl import ( "net/http" "regexp" "strings" "zettelstore.de/z/auth" "zettelstore.de/z/web/server" ) type ( methodHandler map[string]http.Handler routingTable map[byte]methodHandler ) // httpRouter handles all routing for zettelstore. type httpRouter struct { urlPrefix string auth auth.TokenManager minKey byte maxKey byte reURL *regexp.Regexp listTable routingTable zettelTable routingTable ur server.UserRetriever mux *http.ServeMux } // initializeRouter creates a new, empty router with the given root handler. func (rt *httpRouter) initializeRouter(urlPrefix string, auth auth.TokenManager) { rt.urlPrefix = urlPrefix rt.auth = auth rt.minKey = 255 rt.maxKey = 0 rt.reURL = regexp.MustCompile("^$") rt.mux = http.NewServeMux() rt.listTable = make(routingTable) rt.zettelTable = make(routingTable) } func (rt *httpRouter) addRoute(key byte, httpMethod string, handler http.Handler, table routingTable) { // Set minKey and maxKey; re-calculate regexp. if key < rt.minKey || rt.maxKey < key { if key < rt.minKey { rt.minKey = key } if rt.maxKey < key { rt.maxKey = key } rt.reURL = regexp.MustCompile( "^/(?:([" + string(rt.minKey) + "-" + string(rt.maxKey) + "])(?:/(?:([0-9]{14})/?)?)?)$") } mh, hasKey := table[key] if !hasKey { mh = make(methodHandler) table[key] = mh } mh[httpMethod] = handler if httpMethod == http.MethodGet { if _, hasHead := table[key][http.MethodHead]; !hasHead { table[key][http.MethodHead] = handler } } } // addListRoute adds a route for the given key and HTTP method to work with a list. func (rt *httpRouter) addListRoute(key byte, httpMethod string, handler http.Handler) { rt.addRoute(key, httpMethod, handler, rt.listTable) } // addZettelRoute adds a route for the given key and HTTP method to work with a zettel. func (rt *httpRouter) addZettelRoute(key byte, httpMethod string, handler http.Handler) { rt.addRoute(key, httpMethod, handler, rt.zettelTable) } // Handle registers the handler for the given pattern. If a handler already exists for pattern, Handle panics. func (rt *httpRouter) Handle(pattern string, handler http.Handler) { rt.mux.Handle(pattern, handler) } func (rt *httpRouter) ServeHTTP(w http.ResponseWriter, r *http.Request) { 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) return } r.URL.Path = r.URL.Path[prefixLen-1:] } match := rt.reURL.FindStringSubmatch(r.URL.Path) if len(match) == 3 { key := match[1][0] table := rt.zettelTable if match[2] == "" { table = rt.listTable } if mh, ok := table[key]; ok { if handler, ok := mh[r.Method]; ok { r.URL.Path = "/" + match[2] handler.ServeHTTP(w, rt.addUserContext(r)) return } http.Error(w, http.StatusText(http.StatusMethodNotAllowed), http.StatusMethodNotAllowed) return } } rt.mux.ServeHTTP(w, rt.addUserContext(r)) } func (rt *httpRouter) addUserContext(r *http.Request) *http.Request { if rt.ur == nil { return r } k := auth.KindJSON t := getHeaderToken(r) if len(t) == 0 { k = auth.KindHTML t = getSessionToken(r) } if len(t) == 0 { return r } tokenData, err := rt.auth.CheckToken(t, k) if err != nil { return r } ctx := r.Context() user, err := rt.ur.GetUser(ctx, tokenData.Zid, tokenData.Ident) if err != nil { return r } return r.WithContext(updateContext(ctx, user, &tokenData)) } func getSessionToken(r *http.Request) []byte { cookie, err := r.Cookie(sessionName) |
︙ | ︙ | |||
224 225 226 227 228 229 230 | 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):]) } | < < < < < < < < < < < < < < < | 167 168 169 170 171 172 173 | 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):]) } |
Added web/server/impl/urlbuilder.go.
> > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 | //----------------------------------------------------------------------------- // Copyright (c) 2020-2021 Detlef Stern // // This file is part of zettelstore. // // Zettelstore is licensed under the latest version of the EUPL (European Union // Public License). Please see file LICENSE.txt for your rights and obligations // under this license. //----------------------------------------------------------------------------- // Package impl provides the Zettelstore web service. package impl import ( "net/url" "strings" "zettelstore.de/z/domain/id" "zettelstore.de/z/web/server" ) type urlQuery struct{ key, val string } // URLBuilder should be used to create zettelstore URLs. type URLBuilder struct { router *httpRouter key byte path []string query []urlQuery fragment string } // Clone an URLBuilder func (ub *URLBuilder) Clone() server.URLBuilder { cpy := new(URLBuilder) cpy.key = ub.key if len(ub.path) > 0 { cpy.path = make([]string, 0, len(ub.path)) cpy.path = append(cpy.path, ub.path...) } if len(ub.query) > 0 { cpy.query = make([]urlQuery, 0, len(ub.query)) cpy.query = append(cpy.query, ub.query...) } cpy.fragment = ub.fragment return cpy } // SetZid sets the zettel identifier. func (ub *URLBuilder) SetZid(zid id.Zid) server.URLBuilder { if len(ub.path) > 0 { panic("Cannot add Zid") } ub.path = append(ub.path, zid.String()) return ub } // AppendPath adds a new path element func (ub *URLBuilder) AppendPath(p string) server.URLBuilder { ub.path = append(ub.path, p) return ub } // AppendQuery adds a new query parameter func (ub *URLBuilder) AppendQuery(key, value string) server.URLBuilder { ub.query = append(ub.query, urlQuery{key, value}) return ub } // ClearQuery removes all query parameters. func (ub *URLBuilder) ClearQuery() server.URLBuilder { ub.query = nil ub.fragment = "" return ub } // SetFragment stores the fragment func (ub *URLBuilder) SetFragment(s string) server.URLBuilder { ub.fragment = s return ub } // String produces a string value. func (ub *URLBuilder) String() string { var sb strings.Builder sb.WriteString(ub.router.urlPrefix) if ub.key != '/' { sb.WriteByte(ub.key) } for _, p := range ub.path { sb.WriteByte('/') sb.WriteString(url.PathEscape(p)) } if len(ub.fragment) > 0 { sb.WriteByte('#') sb.WriteString(ub.fragment) } for i, q := range ub.query { if i == 0 { sb.WriteByte('?') } else { sb.WriteByte('&') } sb.WriteString(q.key) sb.WriteByte('=') sb.WriteString(url.QueryEscape(q.val)) } return sb.String() } |
Changes to web/server/server.go.
1 | //----------------------------------------------------------------------------- | | | < < < < | | > > > > > > > > > > > > > > > > > > > > > > > > < < < < < < < < < < < < < < | | | < | < > > > > < < < < < < < < < < < < < < < < < < < < < < < < < | | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 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-2021 Detlef Stern // // This file is part of zettelstore. // // Zettelstore is licensed under the latest version of the EUPL (European Union // Public License). Please see file LICENSE.txt for your rights and obligations // under this license. //----------------------------------------------------------------------------- // Package server provides the Zettelstore web service. package server import ( "context" "net/http" "time" "zettelstore.de/z/domain/id" "zettelstore.de/z/domain/meta" ) // URLBuilder builds URLs. type URLBuilder interface { // Clone an URLBuilder Clone() URLBuilder // SetZid sets the zettel identifier. SetZid(zid id.Zid) URLBuilder // AppendPath adds a new path element AppendPath(p string) URLBuilder // AppendQuery adds a new query parameter AppendQuery(key, value string) URLBuilder // ClearQuery removes all query parameters. ClearQuery() URLBuilder // SetFragment stores the fragment SetFragment(s string) URLBuilder // String produces a string value. String() string } // UserRetriever allows to retrieve user data based on a given zettel identifier. type UserRetriever interface { GetUser(ctx context.Context, zid id.Zid, ident string) (*meta.Meta, error) } // Router allows to state routes for various URL paths. type Router interface { Handle(pattern string, handler http.Handler) AddListRoute(key byte, httpMethod string, handler http.Handler) AddZettelRoute(key byte, httpMethod string, handler http.Handler) SetUserRetriever(ur UserRetriever) } // Builder allows to build new URLs for the web service. type Builder interface { GetURLPrefix() string NewURLBuilder(key byte) URLBuilder } // Auth is. type Auth interface { GetUser(context.Context) *meta.Meta SetToken(w http.ResponseWriter, token []byte, d time.Duration) // ClearToken invalidates the session cookie by sending an empty one. ClearToken(ctx context.Context, w http.ResponseWriter) context.Context // GetAuthData returns the full authentication data from the context. GetAuthData(ctx context.Context) *AuthData } // AuthData stores all relevant authentication data for a context. type AuthData struct { User *meta.Meta Token []byte Now time.Time Issued time.Time Expires time.Time } // 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() error } |
Changes to www/build.md.
|
| | | | < < < | < < < | < | | | | > > | > | | | | | | | > > > > | | < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 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 | # How to build the Zettelstore ## Prerequisites You must install the following software: * A current, supported [release of Go](https://golang.org/doc/devel/release.html), * [golint](https://github.com/golang/lint|golint), * [Fossil](https://fossil-scm.org/). ## Clone the repository Most of this is covered by the excellent Fossil documentation. 1. Create a directory to store your Fossil repositories. Let's assume, you have created <tt>$HOME/fossil</tt>. 1. Clone the repository: `fossil clone https://zettelstore.de/ $HOME/fossil/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/fossil/zettelstore.fossil`. (If you are not able to use Fossil, you could try the Git 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. `COMMAND` is one of: * `build`: builds the software with correct version information and places 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). * `release`: executes `check` command and if this was successful, builds the software for various platforms, and creates ZIP files for each executable. Everything is placed in the directory <tt>releases</tt>. * `clean`: removes the directories <tt>bin</tt> and <tt>releases</tt>. * `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 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 | <title>Change Log</title> <a name="0_0_14"></a> <h2>Changes for Version 0.0.14 (pending)</h2> <a name="0_0_13"></a> <h2>Changes for Version 0.0.13 (2021-06-01)</h2> * Startup configuration <tt>place-<em>X</em>-uri</tt> (where <em>X</em> is a number greater than zero) has been renamed to <tt>place-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 place 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=place/constplace/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 inprovements, to the software and to the documentation. <a name="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 places. The original directory place type is now called "notify" (the default value). There is a new type called "simple". This new type does not notify Zettelstore when some of the underlying Zettel files change. (major) * Add new startup configuration <tt>default-dir-place-type</tt>, which gives the default value for specifying a directory place type. The default value is “notify”. On macOS, the default value may be changed “simple” if some errors occur while raising the per-process limit of open files. (minor) <a name="0_0_11"></a> <h2>Changes for Version 0.0.11 (2021-04-05)</h2> * New place schema "file" allows to read zettel from a ZIP file. A zettel collection can now be packaged and distributed easier. (major: server) * Non-restricted search is a full-text search. The search string will be normalized according to Unicode NFKD. Every character that is not a letter 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 filtering a list of zettel, it can be specified that a given value should <em>not</em> match. Previously, only the whole filter expression could be negated (which is still possible). (minor: api, webui) * You can filter a zettel list by specifying that specific metadata keys must (or must not) be present. (minor: api, webui) * Context of a zettel (introduced in version 0.0.10) does not take tags into account any more. Using some tags for determining the context resulted into erratic, non-deterministic context lists. (minor: api, webui) * Filtering zettel depending on tag values can be both by comparing only the prefix or the whole string. If a search value begins with '#', only zettel with the exact tag will be returned. Otherwise a zettel will be returned if the search string just matches the prefix of only one of its tags. (minor: api, webui) * Many smaller bug fixes and inprovements, to the software and to the documentation. A note for users of macOS: in the current release and with macOS's default values, a zettel directory place must not contain more than approx. 250 files. There are three options to mitigate this limitation temporarily: # You update the per-process limit of open files on macOS. # You setup a virtualization environment to run Zettelstore on Linux or Windows. # You wait for version 0.0.12 which addresses this issue. <a name="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/filtering ignores the leading '#' character of tags. (minor: api, webui) * When result of filtering or searching is presented, the query is written as the page heading. (minor: webui) * A reference to a zettel that contains a URL fragment, will now be processed by the indexer. (bug: server) * Runtime configuration key <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 inprovements, to the software and to the documentation. <a name="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 places. 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 name="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 place, * its metadata are just calculated from the information available. Updated metadata did not find its way into the place, because the <tt>.meta</tt> file was not written. This has been fixed. * (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. This has been fixed. 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 places for the new zettel identifier, not just the first place. Otherwise it will be possible to shadow a read-only zettel from a next place, effectively modifying it. * (minor) Add support for a configurable default value for metadata key <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 a specific URL. A <i>Welcome zettel</i> is created, to give some more information. (This change also applies to the server itself, but it is more suited to the WebUI user.) <a name="0_0_7"></a> <h2>Changes for Version 0.0.7 (2020-11-24)</h2> * With this version, Zettelstore and this manual got a new license, the [https://joinup.ec.europa.eu/collection/eupl|European Union Public Licence] (EUPL), version 1.2 or later. Nothing else changed. If you want to stay with the old licenses (AGPLv3+, CC BY-SA 4.0), you are free to fork from the previous version. <a name="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 place 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 parameter of filtering a list must be true, regardless if a specific key occurs more than one or not. * (minor) Encode all inherited meta values in all formats except “raw”. A meta value is called <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. <h3>Web user interface (WebUI)</h3> * (minor) Focus on the first text field on some forms (new zettel, edit zettel, rename zettel, login) * (major) Adapt all HTML templates to a simpler structure. * (bug) Rendered wrong URLs for internal links on info page. * (bug) If a zettel contains binary content it cannot be cloned. For such a zettel only the metadata can be changed. * (minor) Non-zettel references that neither have an URL scheme, user info, nor host name, are considered “local references” (in contrast to “zettel references” and “external references”). When a local reference is displayed as an URL on the WebUI, it will not opened in a new window/tab. They will receive a <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 placed 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 name="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 places, where zettel are stored, via an URL. * Add support for a custom footer. <a name="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 name="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 name="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 name="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 | <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.0.13</code> (2021-06-01). * [/uv/zettelstore-0.0.13-linux-amd64.zip|Linux] (amd64) * [/uv/zettelstore-0.0.13-linux-arm.zip|Linux] (arm6, e.g. Raspberry Pi) * [/uv/zettelstore-0.0.13-windows-amd64.zip|Windows] (amd64) * [/uv/zettelstore-0.0.13-darwin-amd64.zip|macOS] (amd64) * [/uv/zettelstore-0.0.13-darwin-arm64.zip|macOS] (arm64, aka Apple silicon) Unzip the appropriate file, install and execute Zettelstore according to the manual. <h2>Zettel for the manual</h2> As a starter, you can download the zettel for the manual [/uv/manual-0.0.13.zip|here]. Just unzip the contained files and put them into your zettel folder or configure a file place to read the zettel directly from the ZIP file. |
Changes to www/index.wiki.
1 2 3 4 5 | <title>Home</title> <b>Zettelstore</b> 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 | | | | | | | < | < < < < < < < | < | | | | | | | | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 | <title>Home</title> <b>Zettelstore</b> is a software that collects and relates your notes (“zettel”) to represent and enhance your knowledge. It helps with many tasks of personal knowledge management by explicitly supporting the [https://en.wikipedia.org/wiki/Zettelkasten|Zettelkasten method]. The method is based on creating many individual notes, each with one idea or information, that are related to each other. Since knowledge is typically build up gradually, one major focus is a long-term store of these notes, hence the name “Zettelstore”. To get an initial impression, take a look at the [https://zettelstore.de/manual/|manual]. It is a live example of the zettelstore software, running in read-only mode. The software, including the manual, is licensed under the [/file?name=LICENSE.txt&ci=trunk|European Union Public License 1.2 (or later)]. [https://twitter.com/zettelstore|Stay tuned]… <hr> <h3>Latest Release: 0.0.13 (2021-06-01)</h3> * [./download.wiki|Download] * [./changes.wiki#0_0_13|Change summary] * [/timeline?p=version-0.0.13&bt=version-0.0.12&y=ci|Check-ins for version 0.0.13], [/vdiff?to=version-0.0.13&from=version-0.0.12|content diff] * [/timeline?df=version-0.0.13&y=ci|Check-ins derived from the 0.0.13 release], [/vdiff?from=version-0.0.13&to=trunk|content diff] * [./plan.wiki|Limitations and planned improvements] * [/timeline?t=release|Timeline of all past releases] <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 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 | <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> * Content with binary data (e.g. a GIF, PNG, or JPG file) cannot be created nor modified via the standard web interface. As a workaround, you should place your file into the directory where your zettel are stored. Make sure that the file name starts with unique 14 digits that make up the zettel identifier. * Automatic lists and transclusions are not supported in Zettelmarkup. * … <h3>Smaller limitations</h3> * Quoted attribute values are not yet supported in Zettelmarkup: <code>{key="value with space"}</code>. * The <tt>file</tt> sub-command currently does not support output format “json”. * 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. * Backspace character in links does not always work, esp. for <tt>\|</tt> or <tt>\]</tt>. * … <h3>Planned improvements</h3> * Support for mathematical content is missing, e.g. <code>$$F(x) &= \\int^a_b \\frac{1}{3}x^3$$</code>. * Render zettel in [https://pandoc.org|pandoc's] JSON version of their native AST to make pandoc an external renderer for Zettelstore. * … |
Deleted zettel/content.go.
|
| < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < |
Deleted zettel/content_test.go.
|
| < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < |
Deleted zettel/id/digraph.go.
|
| < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < |
Deleted zettel/id/digraph_test.go.
|
| < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < |
Deleted zettel/id/edge.go.
|
| < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < |
Deleted zettel/id/id.go.
|
| < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < |
Deleted zettel/id/id_test.go.
|
| < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < |
Deleted zettel/id/set.go.
|
| < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < |
Deleted zettel/id/set_test.go.
|
| < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < |
Deleted zettel/id/slice.go.
|
| < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < |
Deleted zettel/id/slice_test.go.
|
| < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < |
Deleted zettel/meta/collection.go.
|
| < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < |
Deleted zettel/meta/meta.go.
|
| < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < |
Deleted zettel/meta/meta_test.go.
|
| < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < |
Deleted zettel/meta/parse.go.
|
| < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < |
Deleted zettel/meta/parse_test.go.
|
| < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < |
Deleted zettel/meta/type.go.
|
| < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < |
Deleted zettel/meta/type_test.go.
|
| < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < |
Deleted zettel/meta/values.go.
|
| < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < |
Deleted zettel/meta/write.go.
|
| < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < |
Deleted zettel/meta/write_test.go.
|
| < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < |
Deleted zettel/zettel.go.
|
| < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < |