Many hyperlinks are disabled.
Use anonymous login
to enable hyperlinks.
Difference From trunk To version-0.0.9
2024-04-18
| ||
13:30 | Adapt to client change: api.URLBuilder ... (Leaf check-in: 85cb9d749b user: stern tags: trunk) | |
07:57 | Update dependency zettel ... (check-in: af30a48347 user: stern tags: trunk) | |
2021-01-29
| ||
18:44 | Typo ... (check-in: 84effdca96 user: stern tags: trunk) | |
18:16 | Version 0.0.9 ... (check-in: 5d25b46c82 user: stern tags: trunk, release, version-0.0.9) | |
17:34 | Prepare for release. Fix indexer bug. Add index.Store.Write. ... (check-in: 2b8648602f user: stern tags: trunk) | |
Deleted .github/dependabot.yml.
|
| < < < < < < < < < < < < |
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 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 | ## 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. .PHONY: test check validate race run build build-dev release clean PACKAGE := zettelstore.de/z/cmd/zettelstore GO_LDFLAG_VERSION := -X main.buildVersion=$(shell go run tools/version.go || echo unknown) GOFLAGS_DEVELOP := -ldflags "$(GO_LDFLAG_VERSION)" -tags osusergo,netgo GOFLAGS_RELEASE := -ldflags "$(GO_LDFLAG_VERSION) -w" -tags osusergo,netgo test: go test ./... check: go vet ./... ~/go/bin/golint ./... validate: test check race: go test -race ./... build-dev: mkdir -p bin go build $(GOFLAGS_DEVELOP) -o bin/zettelstore $(PACKAGE) build: mkdir -p bin go build $(GOFLAGS_RELEASE) -o bin/zettelstore $(PACKAGE) release: mkdir -p releases GOARCH=amd64 GOOS=linux go build $(GOFLAGS_RELEASE) -o releases/zettelstore $(PACKAGE) GOARCH=arm GOARM=6 GOOS=linux go build $(GOFLAGS_RELEASE) -o releases/zettelstore-arm6 $(PACKAGE) GOARCH=amd64 GOOS=darwin go build $(GOFLAGS_RELEASE) -o releases/iZettelstore $(PACKAGE) GOARCH=amd64 GOOS=windows go build $(GOFLAGS_RELEASE) -o releases/zettelstore.exe $(PACKAGE) clean: rm -rf bin releases |
Changes to README.md.
|
| | < < < < < < < < | < < < < < < < < < < < < < < < | 1 2 3 | # zettelstore A storage and service for zettel notes. |
Changes to VERSION.
|
| | | 1 | 0.0.9 |
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 | //----------------------------------------------------------------------------- // 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 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 Zid id.Zid // Zettel identification. InhMeta *meta.Meta // Meta data of the zettel, with inherited values. Title InlineSlice // Zettel title is a sequence of inline nodes. 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 | 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 ( | > > > | | | | | < < | | | 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 | 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 URL RefStateZettel // Valid reference to an internal zettel RefStateZettelSelf // Valid reference to same zettel with a fragment RefStateZettelFound // Valid reference to an existing internal zettel RefStateZettelBroken // Valid reference to a non-existing internal zettel RefStateLocal // Valid reference to a non-zettel, but local hosted RefStateExternal // Valid 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 Detlef Stern // // This file is part of zettelstore. // // Zettelstore is licensed under 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 string, 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 Detlef Stern // // This file is part of zettelstore. // // Zettelstore is licensed under 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 accordung 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 Detlef Stern // // This file is part of zettelstore. // // Zettelstore is licensed under 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 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 Detlef Stern // // This file is part of zettelstore. // // Zettelstore is licensed under 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 { if len(s) == 0 { return &Reference{URL: nil, Value: s, State: RefStateInvalid} } 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: RefStateZettelSelf} } if isLocalPath(u.Path) { return &Reference{URL: u, Value: s, State: RefStateLocal} } } return &Reference{URL: u, Value: s, State: RefStateExternal} } func isLocalPath(path string) bool { if len(path) > 0 && path[0] == '/' { return true } if len(path) > 1 && path[0] == '.' { if len(path) > 2 && path[1] == '.' && path[2] == '/' { return true } return path[1] == '/' } return false } // 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. func (r *Reference) IsZettel() bool { switch r.State { case RefStateZettel, RefStateZettelSelf, RefStateZettelFound, RefStateZettelBroken: return true } return false } // IsLocal returns true if reference is local func (r *Reference) IsLocal() bool { return r.State == RefStateLocal } // IsExternal returns true if it is a referencen to external material. func (r *Reference) IsExternal() bool { return r.State == RefStateExternal } |
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 Detlef Stern // // This file is part of zettelstore. // // Zettelstore is licensed under 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 51 52 53 54 55 56 57 58 59 60 61 62 63 64 | 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}, {"http://zettelstore.de/z/ast", false, true, false}, {"12345678901234", true, false, false}, {"12345678901234#local", true, false, false}, {"http://12345678901234", false, true, false}, {"http://zettelstore.de/z/12345678901234", false, true, false}, {"http://zettelstore.de/12345678901234", false, true, false}, {"/12345678901234", false, false, true}, {"./12345678901234", false, false, true}, {"../12345678901234", false, false, true}, {".../12345678901234", false, true, false}, } for i, tc := range testcases { ref := ast.ParseReference(tc.link) |
︙ | ︙ |
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 | //----------------------------------------------------------------------------- // 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 // 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) } } 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.
|
| < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < |
Deleted auth/auth.go.
|
| < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < |
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 26 27 28 29 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 Detlef Stern // // This file is part of zettelstore. // // Zettelstore is licensed under 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 string, credential string) (string, error) { fullCredential := createFullCredential(zid, ident, credential) res, err := bcrypt.GenerateFromPassword(fullCredential, bcrypt.DefaultCost) if err != nil { return "", err } return string(res), nil } // CompareHashAndCredential checks, whether the hashed credential is a possible // value when hashing the credential. func CompareHashAndCredential(hashed string, zid id.Zid, ident string, credential string) (bool, error) { fullCredential := createFullCredential(zid, ident, credential) err := bcrypt.CompareHashAndPassword([]byte(hashed), fullCredential) if err == nil { return true, nil } if err == bcrypt.ErrMismatchedHashAndPassword { return false, nil } return false, err } func createFullCredential(zid id.Zid, ident string, 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.
|
| < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < |
Deleted auth/impl/impl.go.
|
| < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < |
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 25 26 27 28 29 30 31 32 33 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 policy provides some interfaces and implementation for authorization policies. package policy import ( "zettelstore.de/z/domain/meta" ) type anonPolicy struct { simpleMode bool expertMode func() bool getVisibility func(*meta.Meta) meta.Visibility pre Policy } func (ap *anonPolicy) CanReload(user *meta.Meta) bool { return ap.pre.CanReload(user) } func (ap *anonPolicy) CanCreate(user *meta.Meta, newMeta *meta.Meta) bool { return ap.pre.CanCreate(user, newMeta) } func (ap *anonPolicy) CanRead(user *meta.Meta, m *meta.Meta) bool { return ap.pre.CanRead(user, m) && ap.checkVisibility(m) } func (ap *anonPolicy) CanWrite(user *meta.Meta, oldMeta, newMeta *meta.Meta) bool { return ap.pre.CanWrite(user, oldMeta, newMeta) && ap.checkVisibility(oldMeta) } func (ap *anonPolicy) CanRename(user *meta.Meta, m *meta.Meta) bool { return ap.pre.CanRename(user, m) && ap.checkVisibility(m) } func (ap *anonPolicy) CanDelete(user *meta.Meta, m *meta.Meta) bool { return ap.pre.CanDelete(user, m) && ap.checkVisibility(m) } func (ap *anonPolicy) checkVisibility(m *meta.Meta) bool { switch ap.getVisibility(m) { case meta.VisibilitySimple: return ap.simpleMode || ap.expertMode() case meta.VisibilityExpert: return ap.expertMode() } 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 56 57 58 59 60 61 62 63 64 65 66 67 68 69 | //----------------------------------------------------------------------------- // 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 policy provides some interfaces and implementation for authorizsation policies. package policy import ( "zettelstore.de/z/config/runtime" "zettelstore.de/z/domain/meta" ) type defaultPolicy struct{} func (d *defaultPolicy) CanReload(user *meta.Meta) bool { return true } func (d *defaultPolicy) CanCreate(user *meta.Meta, newMeta *meta.Meta) bool { return true } func (d *defaultPolicy) CanRead(user *meta.Meta, m *meta.Meta) bool { return true } func (d *defaultPolicy) CanWrite(user *meta.Meta, oldMeta, newMeta *meta.Meta) bool { return d.canChange(user, oldMeta) } func (d *defaultPolicy) CanRename(user *meta.Meta, m *meta.Meta) bool { return d.canChange(user, m) } func (d *defaultPolicy) CanDelete(user *meta.Meta, m *meta.Meta) bool { return d.canChange(user, m) } func (d *defaultPolicy) canChange(user *meta.Meta, 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 := runtime.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 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 | //----------------------------------------------------------------------------- // Copyright (c) 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 policy provides some interfaces and implementation for authorizsation policies. package policy import ( "zettelstore.de/z/config/runtime" "zettelstore.de/z/domain/id" "zettelstore.de/z/domain/meta" ) type ownerPolicy struct { expertMode func() bool isOwner func(id.Zid) bool getVisibility func(*meta.Meta) meta.Visibility pre Policy } func (o *ownerPolicy) CanReload(user *meta.Meta) bool { // No need to call o.pre.CanReload(user), because it will always return true. // Both the default and the readonly policy allow to reload a place. // Only the owner is allowed to reload a place return o.userIsOwner(user) } func (o *ownerPolicy) CanCreate(user *meta.Meta, 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 *meta.Meta, newMeta *meta.Meta) bool { if runtime.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 *meta.Meta, m *meta.Meta) bool { // No need to call o.pre.CanRead(user, meta), because it will always return true. // Both the default and the readonly policy allow to read a zettel. vis := o.getVisibility(m) if res, ok := o.checkVisibility(user, vis); ok { return res } return o.userIsOwner(user) || o.userCanRead(user, m, vis) } func (o *ownerPolicy) userCanRead(user *meta.Meta, m *meta.Meta, vis meta.Visibility) bool { switch vis { case meta.VisibilityOwner, meta.VisibilitySimple, meta.VisibilityExpert: 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 *meta.Meta, oldMeta, newMeta *meta.Meta) bool { if user == nil || !o.pre.CanWrite(user, oldMeta, newMeta) { return false } vis := o.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 runtime.GetUserRole(user) == meta.UserRoleReader { return false } return o.userCanCreate(user, newMeta) } func (o *ownerPolicy) CanRename(user *meta.Meta, m *meta.Meta) bool { if user == nil || !o.pre.CanRename(user, m) { return false } if res, ok := o.checkVisibility(user, o.getVisibility(m)); ok { return res } return o.userIsOwner(user) } func (o *ownerPolicy) CanDelete(user *meta.Meta, m *meta.Meta) bool { if user == nil || !o.pre.CanDelete(user, m) { return false } if res, ok := o.checkVisibility(user, o.getVisibility(m)); ok { return res } return o.userIsOwner(user) } func (o *ownerPolicy) checkVisibility(user *meta.Meta, vis meta.Visibility) (bool, bool) { switch vis { case meta.VisibilitySimple, meta.VisibilityExpert: return o.userIsOwner(user) && o.expertMode(), true } return false, false } func (o *ownerPolicy) userIsOwner(user *meta.Meta) bool { if user == nil { return false } if o.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 166 167 168 169 170 171 172 173 174 175 176 177 | //----------------------------------------------------------------------------- // Copyright (c) 2020-2021 Detlef Stern // // This file is part of zettelstore. // // Zettelstore is licensed under the latest version of the EUPL (European Union // Public License). Please see file LICENSE.txt for your rights and obligations // under this license. //----------------------------------------------------------------------------- // Package policy provides some interfaces and implementation for authorizsation policies. package policy import ( "context" "zettelstore.de/z/domain" "zettelstore.de/z/domain/id" "zettelstore.de/z/domain/meta" "zettelstore.de/z/place" "zettelstore.de/z/web/session" ) // PlaceWithPolicy wraps the given place inside a policy place. func PlaceWithPolicy( place place.Place, simpleMode bool, withAuth func() bool, isReadOnlyMode bool, expertMode func() bool, isOwner func(id.Zid) bool, getVisibility func(*meta.Meta) meta.Visibility, ) (place.Place, Policy) { pol := newPolicy(simpleMode, withAuth, isReadOnlyMode, expertMode, isOwner, getVisibility) return newPlace(place, pol), pol } // polPlace implements a policy place. type polPlace struct { place place.Place policy Policy } // newPlace creates a new policy place. func newPlace(place place.Place, policy Policy) place.Place { return &polPlace{ 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 := session.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 := session.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 := session.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) (map[id.Zid]bool, error) { return nil, place.NewErrNotAllowed("fetch-zids", session.GetUser(ctx), id.Invalid) } func (pp *polPlace) SelectMeta( ctx context.Context, f *place.Filter, s *place.Sorter) ([]*meta.Meta, error) { user := session.GetUser(ctx) f = place.EnsureFilter(f) canRead := pp.policy.CanRead if sel := f.Select; sel != nil { f.Select = func(m *meta.Meta) bool { return canRead(user, m) && sel(m) } } else { f.Select = func(m *meta.Meta) bool { return canRead(user, m) } } result, err := pp.place.SelectMeta(ctx, f, s) return result, err } 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 := session.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 := session.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 := session.GetUser(ctx) if pp.policy.CanDelete(user, meta) { return pp.place.DeleteZettel(ctx, zid) } return place.NewErrNotAllowed("Delete", user, zid) } func (pp *polPlace) Reload(ctx context.Context) error { user := session.GetUser(ctx) if pp.policy.CanReload(user) { return pp.place.Reload(ctx) } return place.NewErrNotAllowed("Reload", user, id.Invalid) } func (pp *polPlace) ReadStats(st *place.Stats) { pp.place.ReadStats(st) } |
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 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 | //----------------------------------------------------------------------------- // Copyright (c) 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 policy provides some interfaces and implementation for authorizsation policies. package policy import ( "zettelstore.de/z/domain/id" "zettelstore.de/z/domain/meta" ) // Policy is an interface for checking access authorization. type Policy interface { // User is allowed to reload a place. CanReload(user *meta.Meta) bool // User is allowed to create a new zettel. CanCreate(user *meta.Meta, newMeta *meta.Meta) bool // User is allowed to read zettel CanRead(user *meta.Meta, m *meta.Meta) bool // User is allowed to write zettel. CanWrite(user *meta.Meta, oldMeta, newMeta *meta.Meta) bool // User is allowed to rename zettel CanRename(user *meta.Meta, m *meta.Meta) bool // User is allowed to delete zettel CanDelete(user *meta.Meta, m *meta.Meta) bool } // newPolicy creates a policy based on given constraints. func newPolicy( simpleMode bool, withAuth func() bool, isReadOnlyMode bool, expertMode func() bool, isOwner func(id.Zid) bool, getVisibility func(*meta.Meta) meta.Visibility, ) Policy { var pol Policy if isReadOnlyMode { pol = &roPolicy{} } else { pol = &defaultPolicy{} } if withAuth() { pol = &ownerPolicy{ expertMode: expertMode, isOwner: isOwner, getVisibility: getVisibility, pre: pol, } } else { pol = &anonPolicy{ simpleMode: simpleMode, expertMode: expertMode, getVisibility: getVisibility, pre: pol, } } return &prePolicy{pol} } type prePolicy struct { post Policy } func (p *prePolicy) CanReload(user *meta.Meta) bool { return p.post.CanReload(user) } func (p *prePolicy) CanCreate(user *meta.Meta, newMeta *meta.Meta) bool { return newMeta != nil && p.post.CanCreate(user, newMeta) } func (p *prePolicy) CanRead(user *meta.Meta, m *meta.Meta) bool { return m != nil && p.post.CanRead(user, m) } func (p *prePolicy) CanWrite(user *meta.Meta, oldMeta, newMeta *meta.Meta) bool { return oldMeta != nil && newMeta != nil && oldMeta.Zid == newMeta.Zid && p.post.CanWrite(user, oldMeta, newMeta) } func (p *prePolicy) CanRename(user *meta.Meta, m *meta.Meta) bool { return m != nil && p.post.CanRename(user, m) } func (p *prePolicy) CanDelete(user *meta.Meta, 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 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 | //----------------------------------------------------------------------------- // 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 policy provides some interfaces and implementation for authorizsation policies. package policy import ( "fmt" "testing" "zettelstore.de/z/domain/id" "zettelstore.de/z/domain/meta" ) func TestPolicies(t *testing.T) { testScene := []struct { simple bool readonly bool withAuth bool expert bool }{ {true, true, true, true}, {true, true, true, false}, {true, true, false, true}, {true, true, false, false}, {true, false, true, true}, {true, false, true, false}, {true, false, false, true}, {true, false, false, false}, {false, true, true, true}, {false, true, true, false}, {false, true, false, true}, {false, true, false, false}, {false, false, true, true}, {false, false, true, false}, {false, false, false, true}, {false, false, false, false}, } for _, ts := range testScene { var authFunc func() bool if ts.withAuth { authFunc = withAuth } else { authFunc = withoutAuth } var expertFunc func() bool if ts.expert { expertFunc = expertMode } else { expertFunc = noExpertMode } pol := newPolicy(ts.simple, authFunc, ts.readonly, expertFunc, isOwner, getVisibility) name := fmt.Sprintf("simple=%v/readonly=%v/withauth=%v/expert=%v", ts.simple, ts.readonly, ts.withAuth, ts.expert) t.Run(name, func(tt *testing.T) { testReload(tt, pol, ts.simple, ts.withAuth, ts.readonly, ts.expert) testCreate(tt, pol, ts.simple, ts.withAuth, ts.readonly, ts.expert) testRead(tt, pol, ts.simple, ts.withAuth, ts.readonly, ts.expert) testWrite(tt, pol, ts.simple, ts.withAuth, ts.readonly, ts.expert) testRename(tt, pol, ts.simple, ts.withAuth, ts.readonly, ts.expert) testDelete(tt, pol, ts.simple, ts.withAuth, ts.readonly, ts.expert) }) } } func withAuth() bool { return true } func withoutAuth() bool { return false } func expertMode() bool { return true } func noExpertMode() bool { return false } func isOwner(zid id.Zid) bool { return zid == ownerZid } func 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 case meta.ValueVisibilitySimple: return meta.VisibilitySimple } } return meta.VisibilityLogin } func testReload(t *testing.T, pol Policy, simple bool, withAuth bool, readonly bool, isExpert bool) { t.Helper() testCases := []struct { user *meta.Meta exp bool }{ {newAnon(), !withAuth}, {newReader(), !withAuth}, {newWriter(), !withAuth}, {newOwner(), true}, } for _, tc := range testCases { t.Run("Reload", func(tt *testing.T) { got := pol.CanReload(tc.user) if tc.exp != got { tt.Errorf("exp=%v, but got=%v", tc.exp, got) } }) } } func testCreate(t *testing.T, pol Policy, simple bool, withAuth bool, readonly bool, 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 Policy, simple bool, withAuth bool, readonly bool, expert bool) { t.Helper() anonUser := newAnon() reader := newReader() writer := newWriter() owner := newOwner() owner2 := newOwner2() zettel := newZettel() publicZettel := newPublicZettel() loginZettel := newLoginZettel() ownerZettel := newOwnerZettel() expertZettel := newExpertZettel() simpleZettel := newSimpleZettel() 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}, // Simple expert zettel {anonUser, simpleZettel, !withAuth && (simple || expert)}, {reader, simpleZettel, !withAuth && (simple || expert)}, {writer, simpleZettel, !withAuth && (simple || expert)}, {owner, simpleZettel, (!withAuth && simple) || expert}, {owner2, simpleZettel, (!withAuth && simple) || 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 Policy, simple bool, withAuth bool, readonly bool, expert bool) { t.Helper() anonUser := newAnon() reader := newReader() writer := newWriter() owner := newOwner() owner2 := newOwner2() zettel := newZettel() publicZettel := newPublicZettel() loginZettel := newLoginZettel() ownerZettel := newOwnerZettel() expertZettel := newExpertZettel() simpleZettel := newSimpleZettel() userZettel := newUserZettel() writerNew := writer.Clone() writerNew.Set(meta.KeyUserRole, owner.GetDefault(meta.KeyUserRole, "")) roFalse := newRoFalseZettel() roTrue := newRoTrueZettel() roReader := newRoReaderZettel() roWriter := newRoWriterZettel() roOwner := newRoOwnerZettel() 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, !withAuth && !readonly}, {reader, zettel, zettel, !withAuth && !readonly}, {writer, zettel, zettel, !readonly}, {owner, zettel, zettel, !readonly}, {owner2, zettel, zettel, !readonly}, // Public zettel {anonUser, publicZettel, publicZettel, !withAuth && !readonly}, {reader, publicZettel, publicZettel, !withAuth && !readonly}, {writer, publicZettel, publicZettel, !readonly}, {owner, publicZettel, publicZettel, !readonly}, {owner2, publicZettel, publicZettel, !readonly}, // Login zettel {anonUser, loginZettel, loginZettel, !withAuth && !readonly}, {reader, loginZettel, loginZettel, !withAuth && !readonly}, {writer, loginZettel, loginZettel, !readonly}, {owner, loginZettel, loginZettel, !readonly}, {owner2, loginZettel, loginZettel, !readonly}, // Owner zettel {anonUser, ownerZettel, ownerZettel, !withAuth && !readonly}, {reader, ownerZettel, ownerZettel, !withAuth && !readonly}, {writer, ownerZettel, ownerZettel, !withAuth && !readonly}, {owner, ownerZettel, ownerZettel, !readonly}, {owner2, ownerZettel, ownerZettel, !readonly}, // Expert zettel {anonUser, expertZettel, expertZettel, !withAuth && !readonly && expert}, {reader, expertZettel, expertZettel, !withAuth && !readonly && expert}, {writer, expertZettel, expertZettel, !withAuth && !readonly && expert}, {owner, expertZettel, expertZettel, !readonly && expert}, {owner2, expertZettel, expertZettel, !readonly && expert}, // Simple expert zettel {anonUser, simpleZettel, expertZettel, !withAuth && !readonly && (simple || expert)}, {reader, simpleZettel, expertZettel, !withAuth && !readonly && (simple || expert)}, {writer, simpleZettel, expertZettel, !withAuth && !readonly && (simple || expert)}, {owner, simpleZettel, expertZettel, !readonly && ((!withAuth && simple) || expert)}, {owner2, simpleZettel, expertZettel, !readonly && ((!withAuth && simple) || expert)}, // Other user zettel {anonUser, userZettel, userZettel, !withAuth && !readonly}, {reader, userZettel, userZettel, !withAuth && !readonly}, {writer, userZettel, userZettel, !withAuth && !readonly}, {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, !withAuth && !readonly}, // No r/o zettel {anonUser, roFalse, roFalse, !withAuth && !readonly}, {reader, roFalse, roFalse, !withAuth && !readonly}, {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 Policy, simple bool, withAuth bool, readonly bool, expert bool) { t.Helper() anonUser := newAnon() reader := newReader() writer := newWriter() owner := newOwner() owner2 := newOwner2() zettel := newZettel() expertZettel := newExpertZettel() simpleZettel := newSimpleZettel() roFalse := newRoFalseZettel() roTrue := newRoTrueZettel() roReader := newRoReaderZettel() roWriter := newRoWriterZettel() roOwner := newRoOwnerZettel() 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, !withAuth && !readonly}, {reader, zettel, !withAuth && !readonly}, {writer, zettel, !withAuth && !readonly}, {owner, zettel, !readonly}, {owner2, zettel, !readonly}, // Expert zettel {anonUser, expertZettel, !withAuth && !readonly && expert}, {reader, expertZettel, !withAuth && !readonly && expert}, {writer, expertZettel, !withAuth && !readonly && expert}, {owner, expertZettel, !readonly && expert}, {owner2, expertZettel, !readonly && expert}, // Simple expert zettel {anonUser, simpleZettel, !withAuth && !readonly && (simple || expert)}, {reader, simpleZettel, !withAuth && !readonly && (simple || expert)}, {writer, simpleZettel, !withAuth && !readonly && (simple || expert)}, {owner, simpleZettel, !readonly && ((!withAuth && simple) || expert)}, {owner2, simpleZettel, !readonly && ((!withAuth && simple) || expert)}, // No r/o zettel {anonUser, roFalse, !withAuth && !readonly}, {reader, roFalse, !withAuth && !readonly}, {writer, roFalse, !withAuth && !readonly}, {owner, roFalse, !readonly}, {owner2, roFalse, !readonly}, // Reader r/o zettel {anonUser, roReader, false}, {reader, roReader, false}, {writer, roReader, !withAuth && !readonly}, {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 Policy, simple bool, withAuth bool, readonly bool, expert bool) { t.Helper() anonUser := newAnon() reader := newReader() writer := newWriter() owner := newOwner() owner2 := newOwner2() zettel := newZettel() expertZettel := newExpertZettel() simpleZettel := newSimpleZettel() roFalse := newRoFalseZettel() roTrue := newRoTrueZettel() roReader := newRoReaderZettel() roWriter := newRoWriterZettel() roOwner := newRoOwnerZettel() 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, !withAuth && !readonly}, {reader, zettel, !withAuth && !readonly}, {writer, zettel, !withAuth && !readonly}, {owner, zettel, !readonly}, {owner2, zettel, !readonly}, // Expert zettel {anonUser, expertZettel, !withAuth && !readonly && expert}, {reader, expertZettel, !withAuth && !readonly && expert}, {writer, expertZettel, !withAuth && !readonly && expert}, {owner, expertZettel, !readonly && expert}, {owner2, expertZettel, !readonly && expert}, // Simple expert zettel {anonUser, simpleZettel, !withAuth && !readonly && (simple || expert)}, {reader, simpleZettel, !withAuth && !readonly && (simple || expert)}, {writer, simpleZettel, !withAuth && !readonly && (simple || expert)}, {owner, simpleZettel, !readonly && ((!withAuth && simple) || expert)}, {owner2, simpleZettel, !readonly && ((!withAuth && simple) || expert)}, // No r/o zettel {anonUser, roFalse, !withAuth && !readonly}, {reader, roFalse, !withAuth && !readonly}, {writer, roFalse, !withAuth && !readonly}, {owner, roFalse, !readonly}, {owner2, roFalse, !readonly}, // Reader r/o zettel {anonUser, roReader, false}, {reader, roReader, false}, {writer, roReader, !withAuth && !readonly}, {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 newSimpleZettel() *meta.Meta { m := meta.New(visZid) m.Set(meta.KeyTitle, "Simple Zettel") m.Set(meta.KeyVisibility, meta.ValueVisibilitySimple) 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 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 policy provides some interfaces and implementation for authorization policies. package policy import "zettelstore.de/z/domain/meta" type roPolicy struct{} func (p *roPolicy) CanReload(user *meta.Meta) bool { return true } func (p *roPolicy) CanCreate(user *meta.Meta, newMeta *meta.Meta) bool { return false } func (p *roPolicy) CanRead(user *meta.Meta, m *meta.Meta) bool { return true } func (p *roPolicy) CanWrite(user *meta.Meta, oldMeta, newMeta *meta.Meta) bool { return false } func (p *roPolicy) CanRename(user *meta.Meta, m *meta.Meta) bool { return false } func (p *roPolicy) CanDelete(user *meta.Meta, m *meta.Meta) bool { return false } |
Added auth/token/token.go.
> > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 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 | //----------------------------------------------------------------------------- // 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 token provides some function for handling auth token. package token import ( "errors" "time" "github.com/pascaldekloe/jwt" "zettelstore.de/z/config/startup" "zettelstore.de/z/domain/id" "zettelstore.de/z/domain/meta" ) 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") // Kind specifies for which application / usage a token is/was requested. type Kind int // Allowed values of token kind const ( _ Kind = iota KindJSON KindHTML ) // GetToken returns a token to be used for authentification. func GetToken(ident *meta.Meta, d time.Duration, kind Kind) ([]byte, error) { if role, ok := ident.Get(meta.KeyRole); !ok || role != meta.ValueRoleUser { return nil, ErrNoUser } subject, ok := ident.Get(meta.KeyUserID) if !ok || len(subject) == 0 { 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, startup.Secret()) if err != nil { return nil, err } return token, nil } // ErrTokenExpired signals an exired token var ErrTokenExpired = errors.New("auth: token expired") // Data contains some important elements from a token. type Data struct { Token []byte Now time.Time Issued time.Time Expires time.Time Ident string Zid id.Zid } // CheckToken checks the validity of the token and returns relevant data. func CheckToken(token []byte, k Kind) (Data, error) { h, err := jwt.NewHMAC(reqHash, startup.Secret()) if err != nil { return Data{}, err } claims, err := h.Check(token) if err != nil { return Data{}, err } now := time.Now().Round(time.Second) expires := claims.Expires.Time() if expires.Before(now) { return Data{}, ErrTokenExpired } ident := claims.Subject if len(ident) == 0 { return Data{}, 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 Kind(kind) == k { return Data{ Token: token, Now: now, Issued: claims.Issued.Time(), Expires: expires, Ident: ident, Zid: zid, }, nil } } return Data{}, ErrOtherKind } } return Data{}, ErrNoZid } |
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.
|
| < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < |
Added cmd/cmd_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 | //----------------------------------------------------------------------------- // 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" "zettelstore.de/z/config/startup" ) // ---------- Subcommand: config --------------------------------------------- func cmdConfig(*flag.FlagSet) (int, error) { fmtVersion() fmt.Println("Stores") fmt.Printf(" Read-only mode = %v\n", startup.IsReadOnlyMode()) fmt.Println("Web") fmt.Printf(" Listen address = %q\n", startup.ListenAddress()) fmt.Printf(" URL prefix = %q\n", startup.URLPrefix()) if startup.WithAuth() { fmt.Println("Auth") fmt.Printf(" Owner = %v\n", startup.Owner()) fmt.Printf(" Secure cookie = %v\n", startup.SecureCookie()) fmt.Printf(" Persistent cookie = %v\n", startup.PersistentCookie()) htmlLifetime, apiLifetime := startup.TokenLifetime() fmt.Printf(" HTML lifetime = %v\n", htmlLifetime) fmt.Printf(" API lifetime = %v\n", apiLifetime) } return 0, nil } |
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 84 85 86 | //----------------------------------------------------------------------------- // 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" "io/ioutil" "os" "zettelstore.de/z/config/runtime" "zettelstore.de/z/domain" "zettelstore.de/z/domain/id" "zettelstore.de/z/domain/meta" "zettelstore.de/z/encoder" "zettelstore.de/z/input" "zettelstore.de/z/parser" ) // ---------- Subcommand: file ----------------------------------------------- func cmdFile(fs *flag.FlagSet) (int, error) { format := fs.Lookup("t").Value.String() meta, inp, err := getInput(fs.Args()) if meta == nil { return 2, err } z := parser.ParseZettel( domain.Zettel{ Meta: meta, Content: domain.NewContent(inp.Src[inp.Pos:]), }, runtime.GetSyntax(meta), ) enc := encoder.Create( format, &encoder.StringOption{Key: "lang", Value: runtime.GetLang(meta)}, ) 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 := ioutil.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 := ioutil.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 := ioutil.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 | //----------------------------------------------------------------------------- // 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/crypto/ssh/terminal" "zettelstore.de/z/auth/cred" "zettelstore.de/z/domain/id" "zettelstore.de/z/domain/meta" ) // ---------- Subcommand: password ------------------------------------------- func cmdPassword(fs *flag.FlagSet) (int, error) { if fs.NArg() == 0 { fmt.Fprintln(os.Stderr, "User name and user zettel identification missing") |
︙ | ︙ | |||
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 := terminal.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 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 | //----------------------------------------------------------------------------- // 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" "log" "net/http" "zettelstore.de/z/auth/policy" "zettelstore.de/z/config/runtime" "zettelstore.de/z/config/startup" "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/router" "zettelstore.de/z/web/server" "zettelstore.de/z/web/session" ) // ---------- Subcommand: run ------------------------------------------------ func flgRun(fs *flag.FlagSet) { fs.String("c", defConfigfile, "configuration file") fs.Uint("p", 23123, "port number") 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 enableDebug(fs *flag.FlagSet, srv *server.Server) { if dbg := fs.Lookup("debug"); dbg != nil && dbg.Value.String() == "true" { srv.SetDebug() } } func runFunc(fs *flag.FlagSet) (int, error) { listenAddr := startup.ListenAddress() readonlyMode := startup.IsReadOnlyMode() logBeforeRun(listenAddr, readonlyMode) handler := setupRouting(startup.PlaceManager(), readonlyMode) srv := server.New(listenAddr, handler) enableDebug(fs, srv) if err := srv.Run(); err != nil { return 1, err } return 0, nil } func logBeforeRun(listenAddr string, readonlyMode bool) { v := startup.GetVersion() log.Printf("%v %v (%v@%v/%v)", v.Prog, v.Build, v.GoVersion, v.Os, v.Arch) log.Println("Licensed under the latest version of the EUPL (European Union Public License)") log.Printf("Listening on %v", listenAddr) log.Printf("Zettel location [%v]", startup.PlaceManager().Location()) if readonlyMode { log.Println("Read-only mode") } } func setupRouting(mgr place.Manager, readonlyMode bool) http.Handler { var up place.Place = mgr pp, pol := policy.PlaceWithPolicy( up, startup.IsSimple(), startup.WithAuth, readonlyMode, runtime.GetExpertMode, startup.IsOwner, runtime.GetVisibility) te := webui.NewTemplateEngine(mgr, pol) ucAuthenticate := usecase.NewAuthenticate(up) ucGetMeta := usecase.NewGetMeta(pp) ucGetZettel := usecase.NewGetZettel(pp) ucParseZettel := usecase.NewParseZettel(ucGetZettel) ucListMeta := usecase.NewListMeta(pp) ucListRoles := usecase.NewListRole(pp) ucListTags := usecase.NewListTags(pp) listHTMLMetaHandler := webui.MakeListHTMLMetaHandler(te, ucListMeta) getHTMLZettelHandler := webui.MakeGetHTMLZettelHandler(te, ucParseZettel, ucGetMeta) router := router.NewRouter() router.Handle("/", webui.MakeGetRootHandler( pp, listHTMLMetaHandler, getHTMLZettelHandler)) router.AddListRoute('a', http.MethodGet, webui.MakeGetLoginHandler(te)) router.AddListRoute('a', http.MethodPost, adapter.MakePostLoginHandler( api.MakePostLoginHandlerAPI(ucAuthenticate), webui.MakePostLoginHandlerHTML(te, ucAuthenticate))) router.AddListRoute('a', http.MethodPut, api.MakeRenewAuthHandler()) router.AddZettelRoute('a', http.MethodGet, webui.MakeGetLogoutHandler()) router.AddListRoute('c', http.MethodGet, adapter.MakeReloadHandler( usecase.NewReload(pp), api.ReloadHandlerAPI, webui.ReloadHandlerHTML)) if !readonlyMode { router.AddZettelRoute('c', http.MethodGet, webui.MakeGetCopyZettelHandler( te, ucGetZettel, usecase.NewCopyZettel())) router.AddZettelRoute('c', http.MethodPost, webui.MakePostCreateZettelHandler( usecase.NewCreateZettel(pp))) router.AddZettelRoute('d', http.MethodGet, webui.MakeGetDeleteZettelHandler( te, ucGetZettel)) router.AddZettelRoute('d', http.MethodPost, webui.MakePostDeleteZettelHandler( usecase.NewDeleteZettel(pp))) router.AddZettelRoute('e', http.MethodGet, webui.MakeEditGetZettelHandler( te, ucGetZettel)) router.AddZettelRoute('e', http.MethodPost, webui.MakeEditSetZettelHandler( usecase.NewUpdateZettel(pp))) router.AddZettelRoute('f', http.MethodGet, webui.MakeGetFolgeZettelHandler( te, ucGetZettel, usecase.NewFolgeZettel())) router.AddZettelRoute('f', http.MethodPost, webui.MakePostCreateZettelHandler( usecase.NewCreateZettel(pp))) } router.AddListRoute('h', http.MethodGet, listHTMLMetaHandler) router.AddZettelRoute('h', http.MethodGet, getHTMLZettelHandler) router.AddZettelRoute('i', http.MethodGet, webui.MakeGetInfoHandler( te, ucParseZettel, ucGetMeta)) router.AddZettelRoute('k', http.MethodGet, webui.MakeWebUIListsHandler( te, ucListMeta, ucListRoles, ucListTags)) router.AddZettelRoute('l', http.MethodGet, api.MakeGetLinksHandler(ucParseZettel)) if !readonlyMode { router.AddZettelRoute('n', http.MethodGet, webui.MakeGetNewZettelHandler( te, ucGetZettel, usecase.NewNewZettel())) router.AddZettelRoute('n', http.MethodPost, webui.MakePostCreateZettelHandler( usecase.NewCreateZettel(pp))) } router.AddListRoute('r', http.MethodGet, api.MakeListRoleHandler(ucListRoles)) if !readonlyMode { router.AddZettelRoute('r', http.MethodGet, webui.MakeGetRenameZettelHandler( te, ucGetMeta)) router.AddZettelRoute('r', http.MethodPost, webui.MakePostRenameZettelHandler( usecase.NewRenameZettel(pp))) } router.AddListRoute('t', http.MethodGet, api.MakeListTagsHandler(ucListTags)) router.AddListRoute('s', http.MethodGet, webui.MakeSearchHandler( te, usecase.NewSearch(pp), ucGetMeta, ucGetZettel)) router.AddListRoute('z', http.MethodGet, api.MakeListMetaHandler( usecase.NewListMeta(pp), ucGetMeta, ucParseZettel)) router.AddZettelRoute('z', http.MethodGet, api.MakeGetZettelHandler( ucParseZettel, ucGetMeta)) return session.NewHandler(router, usecase.NewGetUserByZid(up)) } |
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 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 | //----------------------------------------------------------------------------- // Copyright (c) 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 ( "context" "flag" "fmt" "log" "os" "strings" "zettelstore.de/z/domain" "zettelstore.de/z/place" "zettelstore.de/z/web/server" "zettelstore.de/z/config/startup" "zettelstore.de/z/domain/id" "zettelstore.de/z/domain/meta" ) func flgSimpleRun(fs *flag.FlagSet) { fs.String("d", "", "zettel directory") } func runSimpleFunc(*flag.FlagSet) (int, error) { p := startup.PlaceManager() if _, err := p.GetMeta(context.Background(), id.WelcomeZid); err != nil { if err == place.ErrNotFound { updateWelcomeZettel(p) } } listenAddr := startup.ListenAddress() readonlyMode := startup.IsReadOnlyMode() logBeforeRun(listenAddr, readonlyMode) if idx := strings.LastIndexByte(listenAddr, ':'); idx >= 0 { log.Println() log.Println("--------------------------") log.Printf("Open your browser and enter the following URL:") log.Println() log.Printf(" http://localhost%v", listenAddr[idx:]) } handler := setupRouting(startup.PlaceManager(), readonlyMode) srv := server.New(listenAddr, handler) if err := srv.Run(); err != nil { return 1, err } return 0, nil } // 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() { dir := "./zettel" if err := os.MkdirAll(dir, 0755); err != nil { fmt.Fprintf(os.Stderr, "Unable to create zettel directory %q (%s)\n", dir, err) os.Exit(1) } executeCommand("run-simple", "-d", dir) } func updateWelcomeZettel(p place.Place) { m := meta.New(id.WelcomeZid) m.Set(meta.KeyTitle, "Welcome to Zettelstore") m.Set(meta.KeyRole, meta.ValueRoleZettel) m.Set(meta.KeySyntax, meta.ValueSyntaxZmk) zid, err := p.CreateZettel( context.Background(), domain.Zettel{Meta: m, Content: domain.NewContent(welcomeZettelContent)}, ) if err == nil { p.RenameZettel(context.Background(), zid, id.WelcomeZid) } } var welcomeZettelContent = `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: * [[Zettelstore Version|00000000000001]] * [[Zettelstore Operating System|00000000000003]] * [[Zettelstore Startup Configuration|00000000000096]] * [[Zettelstore Startup Values|00000000000098]] * [[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 was generated automatically. Every time you start Zettelstore by double clicking in your graphical user interface, or by just starting it in a command line via something like ''zettelstore'', and this zettel does not exist, it will be generated. This allows you to edit this zettel for your own needs. If you don't need it anymore, you can delete this zettel by clicking on ""Info"" and then on ""Delete"". However, by starting Zettelstore as described above, the original version of this zettel will be restored. If you start Zettelstore with the ''run'' command, e.g. as a service or via command line, this zettel will not be generated. But if it exists before, it will not be deleted. In this case, Zettelstore assumes that you have enough knowledge and that you do not need zettel. ` |
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 | //----------------------------------------------------------------------------- // 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" "sort" ) // 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 Simple bool // Should start in simple mode Flags func(*flag.FlagSet) // function to set up flag.FlagSet flags *flag.FlagSet // flags that belong to the command } // CommandFunc is the function that executes the command. // It accepts the parsed command line parameters. // It returns the exit code and an error. type CommandFunc func(*flag.FlagSet) (int, error) |
︙ | ︙ | |||
47 48 49 50 51 52 53 | 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) | < < | | | > > > > > > > | 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 | 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 } |
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 | //----------------------------------------------------------------------------- // 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 ( "context" "flag" "fmt" "io/ioutil" "os" "strings" "zettelstore.de/z/config/runtime" "zettelstore.de/z/config/startup" "zettelstore.de/z/domain/id" "zettelstore.de/z/domain/meta" "zettelstore.de/z/index" "zettelstore.de/z/index/indexer" "zettelstore.de/z/input" "zettelstore.de/z/place" "zettelstore.de/z/place/manager" "zettelstore.de/z/place/progplace" ) const ( defConfigfile = ".zscfg" ) func init() { RegisterCommand(Command{ Name: "help", Func: func(*flag.FlagSet) (int, error) { fmt.Println("Available commands:") for _, name := range List() { fmt.Printf("- %q\n", name) } return 0, nil }, }) RegisterCommand(Command{ Name: "version", Func: func(*flag.FlagSet) (int, error) { fmtVersion() return 0, nil }, }) RegisterCommand(Command{ Name: "run", Func: runFunc, Places: true, Flags: flgRun, }) RegisterCommand(Command{ Name: "run-simple", Func: runSimpleFunc, Places: true, Simple: true, Flags: flgSimpleRun, }) RegisterCommand(Command{ Name: "config", Func: cmdConfig, Flags: flgRun, }) 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 fmtVersion() { version := startup.GetVersion() fmt.Printf("%v (%v/%v) running on %v (%v/%v)\n", version.Prog, version.Build, version.GoVersion, version.Hostname, version.Os, version.Arch) } func getConfig(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 := ioutil.ReadFile(configFile) if err != nil { cfg = meta.New(id.Invalid) } else { cfg = meta.NewFromInput(id.Invalid, input.NewInput(string(content))) } fs.Visit(func(flg *flag.Flag) { switch flg.Name { case "p": cfg.Set(startup.KeyListenAddress, "127.0.0.1:"+flg.Value.String()) case "d": val := flg.Value.String() if strings.HasPrefix(val, "/") { val = "dir://" + val } else { val = "dir:" + val } cfg.Set(startup.KeyPlaceOneURI, val) case "r": cfg.Set(startup.KeyReadOnlyMode, flg.Value.String()) case "v": cfg.Set(startup.KeyVerbose, flg.Value.String()) } }) return cfg } func setupOperations(cfg *meta.Meta, withPlaces bool, simple bool) error { var mgr place.Manager var idx index.Indexer if withPlaces { idx = indexer.New() filter := index.NewMetaFilter(idx) p, err := manager.New(getPlaces(cfg), cfg.GetBool(startup.KeyReadOnlyMode), filter) if err != nil { return err } mgr = p } err := startup.SetupStartup(cfg, mgr, idx, simple) if err != nil { fmt.Fprintln(os.Stderr, "Unable to connect to specified places") return err } if withPlaces { if err := mgr.Start(context.Background()); err != nil { fmt.Fprintln(os.Stderr, "Unable to start zettel place") return err } runtime.SetupConfiguration(mgr) progplace.Setup(cfg, mgr, idx) idx.Start(mgr) } return nil } func getPlaces(cfg *meta.Meta) []string { var result []string = nil for cnt := 1; ; cnt++ { key := fmt.Sprintf("place-%v-uri", cnt) uri, ok := cfg.Get(key) if !ok || uri == "" { if cnt > 1 { break } uri = "dir:./zettel" } result = append(result, uri) } return result } func cleanupOperations(withPlaces bool) error { if withPlaces { startup.Indexer().Stop() if err := startup.PlaceManager().Stop(context.Background()); err != nil { fmt.Fprintln(os.Stderr, "Unable to stop zettel place") return err } } return nil } func executeCommand(name string, args ...string) { command, ok := Get(name) if !ok { fmt.Fprintf(os.Stderr, "Unknown command %q\n", name) os.Exit(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) os.Exit(1) } cfg := getConfig(fs) if err := setupOperations(cfg, command.Places, command.Simple); err != nil { fmt.Fprintf(os.Stderr, "%s: %v\n", name, err) os.Exit(2) } exitCode, err := command.Func(fs) if err != nil { fmt.Fprintf(os.Stderr, "%s: %v\n", name, err) } if err := cleanupOperations(command.Places); err != nil { fmt.Fprintf(os.Stderr, "%s: %v\n", name, err) } if exitCode != 0 { os.Exit(exitCode) } } // Main is the real entrypoint of the zettelstore. func Main(progName, buildVersion string) { startup.SetupVersion(progName, buildVersion) if len(os.Args) <= 1 { runSimple() } else { executeCommand(os.Args[1], os.Args[2:]...) } } |
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 | //----------------------------------------------------------------------------- // 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/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/memplace" // Allow to use memory 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 22 23 | //----------------------------------------------------------------------------- // 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 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 buildVersion string = "" func main() { cmd.Main("Zettelstore", buildVersion) } |
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 Detlef Stern // // This file is part of zettelstore. // // Zettelstore is licensed under 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 Detlef Stern // // This file is part of zettelstore. // // Zettelstore is licensed under 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) } } |
Deleted collect/order.go.
|
| < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < |
Added collect/split.go.
> > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 | //----------------------------------------------------------------------------- // 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 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, duplicates bool) (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.RefStateZettelSelf { continue } s := ref.String() if ref.IsZettel() { if duplicates { zettel = append(zettel, ref) } else { if _, ok := mapZettel[s]; !ok { zettel = append(zettel, ref) mapZettel[s] = true } } } else if ref.IsExternal() { if duplicates { external = append(external, ref) } else { if _, ok := mapExternal[s]; !ok { external = append(external, ref) mapExternal[s] = true } } } else { if duplicates { local = append(local, ref) } else { if _, ok := mapLocal[s]; !ok { local = append(local, ref) mapLocal[s] = true } } } } return zettel, local, external } |
Deleted config/config.go.
|
| < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < |
Added config/runtime/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 | //----------------------------------------------------------------------------- // 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 runtime provides functions to retrieve runtime configuration data. package runtime import ( "zettelstore.de/z/config/startup" "zettelstore.de/z/domain/meta" ) var mapDefaultKeys = map[string]func() string{ meta.KeyCopyright: GetDefaultCopyright, meta.KeyLang: GetDefaultLang, meta.KeyLicense: GetDefaultLicense, meta.KeyRole: GetDefaultRole, meta.KeySyntax: GetDefaultSyntax, meta.KeyTitle: GetDefaultTitle, } // AddDefaultValues enriches the given meta data with its default values. func AddDefaultValues(m *meta.Meta) *meta.Meta { result := m for k, f := range mapDefaultKeys { if _, ok := result.Get(k); !ok { if result == m { result = m.Clone() } if val := f(); len(val) > 0 || m.Type(k) == meta.TypeEmpty { result.Set(k, val) } } } return result } // 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) string { if syntax, ok := m.Get(meta.KeyTitle); ok && len(syntax) > 0 { return syntax } return 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) string { if syntax, ok := m.Get(meta.KeyRole); ok && len(syntax) > 0 { return syntax } return 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) string { if syntax, ok := m.Get(meta.KeySyntax); ok && len(syntax) > 0 { return syntax } return 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) string { if lang, ok := m.Get(meta.KeyLang); ok && len(lang) > 0 { return lang } return GetDefaultLang() } // GetVisibility returns the visibility value, or "login" if none is given. func 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 GetDefaultVisibility() } // GetUserRole role returns the user role of the given user zettel. func GetUserRole(user *meta.Meta) meta.UserRole { if user == nil { if startup.WithAuth() { return meta.UserRoleUnknown } return meta.UserRoleOwner } if startup.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 } |
Added config/runtime/runtime.go.
> > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 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 | //----------------------------------------------------------------------------- // 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 runtime provides functions to retrieve runtime configuration data. package runtime import ( "strconv" "zettelstore.de/z/domain/id" "zettelstore.de/z/domain/meta" "zettelstore.de/z/place" "zettelstore.de/z/place/stock" ) // --- Configuration zettel -------------------------------------------------- var configStock stock.Stock // SetupConfiguration enables the configuration data. func SetupConfiguration(mgr place.Manager) { if configStock != nil { panic("configStock already set") } configStock = stock.NewStock(mgr) if err := configStock.Subscribe(id.ConfigurationZid); err != nil { panic(err) } } // getConfigurationMeta returns the meta data of the configuration zettel. func getConfigurationMeta() *meta.Meta { if configStock == nil { panic("configStock not set") } return configStock.GetMeta(id.ConfigurationZid) } // GetDefaultTitle returns the current value of the "default-title" key. func GetDefaultTitle() string { if config := getConfigurationMeta(); config != nil { if title, ok := config.Get(meta.KeyDefaultTitle); ok { return title } } return "Untitled" } // GetDefaultSyntax returns the current value of the "default-syntax" key. func GetDefaultSyntax() string { if configStock != nil { if config := getConfigurationMeta(); config != nil { if syntax, ok := config.Get(meta.KeyDefaultSyntax); ok { return syntax } } } return meta.ValueSyntaxZmk } // GetDefaultRole returns the current value of the "default-role" key. func GetDefaultRole() string { if configStock != nil { if config := getConfigurationMeta(); config != nil { if role, ok := config.Get(meta.KeyDefaultRole); ok { return role } } } return meta.ValueRoleZettel } // GetDefaultLang returns the current value of the "default-lang" key. func GetDefaultLang() string { if configStock != nil { if config := getConfigurationMeta(); config != nil { if lang, ok := config.Get(meta.KeyDefaultLang); ok { return lang } } } return "en" } // GetDefaultCopyright returns the current value of the "default-copyright" key. func GetDefaultCopyright() string { if configStock != nil { if config := getConfigurationMeta(); config != nil { if copyright, ok := config.Get(meta.KeyDefaultCopyright); ok { return copyright } } // TODO: get owner } return "" } // GetDefaultLicense returns the current value of the "default-license" key. func GetDefaultLicense() string { if configStock != nil { if config := getConfigurationMeta(); config != nil { if license, ok := config.Get(meta.KeyDefaultLicense); ok { return license } } } return "" } // GetExpertMode returns the current value of the "expert-mode" key func GetExpertMode() bool { if config := getConfigurationMeta(); config != nil { if mode, ok := config.Get(meta.KeyExpertMode); ok { return meta.BoolValue(mode) } } return false } // GetSiteName returns the current value of the "site-name" key. func GetSiteName() string { if config := getConfigurationMeta(); config != nil { if name, ok := config.Get(meta.KeySiteName); ok { return name } } return "Zettelstore" } // GetStart returns the value of the "start" key. func GetStart() id.Zid { if config := getConfigurationMeta(); config != nil { if start, ok := config.Get(meta.KeyStart); ok { if startID, err := id.Parse(start); err == nil { return startID } } } return id.Invalid } // GetDefaultVisibility returns the default value for zettel visibility. func GetDefaultVisibility() meta.Visibility { if config := getConfigurationMeta(); config != nil { if value, ok := config.Get(meta.KeyDefaultVisibility); ok { if vis := meta.GetVisibility(value); vis != meta.VisibilityUnknown { return vis } } } return meta.VisibilityLogin } // GetYAMLHeader returns the current value of the "yaml-header" key. func GetYAMLHeader() bool { if config := getConfigurationMeta(); config != nil { return config.GetBool(meta.KeyYAMLHeader) } return false } // GetZettelFileSyntax returns the current value of the "zettel-file-syntax" key. func GetZettelFileSyntax() []string { if config := getConfigurationMeta(); config != nil { return config.GetListOrNil(meta.KeyZettelFileSyntax) } return nil } // GetMarkerExternal returns the current value of the "marker-external" key. func GetMarkerExternal() string { if config := getConfigurationMeta(); config != nil { if html, ok := config.Get(meta.KeyMarkerExternal); ok { return html } } return "↗︎" } // GetFooterHTML returns HTML code that should be embedded into the footer // of each WebUI page. func GetFooterHTML() string { if config := getConfigurationMeta(); config != nil { if data, ok := config.Get(meta.KeyFooterHTML); ok { return data } } return "" } // GetListPageSize returns the maximum length of a list to be returned in WebUI. // A value less or equal to zero signals no limit. func GetListPageSize() int { if config := getConfigurationMeta(); config != nil { if data, ok := config.Get(meta.KeyListPageSize); ok { if value, err := strconv.Atoi(data); err == nil { return value } } } return 0 } |
Added config/startup/startup.go.
> > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 | //----------------------------------------------------------------------------- // Copyright (c) 2020-2021 Detlef Stern // // This file is part of zettelstore. // // Zettelstore is licensed under the latest version of the EUPL (European Union // Public License). Please see file LICENSE.txt for your rights and obligations // under this license. //----------------------------------------------------------------------------- // Package startup provides functions to retrieve startup configuration data. package startup import ( "hash/fnv" "io" "strconv" "time" "zettelstore.de/z/domain/id" "zettelstore.de/z/domain/meta" "zettelstore.de/z/index" "zettelstore.de/z/place" ) var config struct { simple bool // was started without run command verbose bool readonlyMode bool urlPrefix string listenAddress string owner id.Zid withAuth bool secret []byte insecCookie bool persistCookie bool htmlLifetime time.Duration apiLifetime time.Duration manager place.Manager indexer index.Indexer } // Predefined keys for startup zettel const ( KeyInsecureCookie = "insecure-cookie" KeyListenAddress = "listen-addr" KeyOwner = "owner" KeyPersistentCookie = "persistent-cookie" KeyPlaceOneURI = "place-1-uri" KeyReadOnlyMode = "read-only-mode" KeyTokenLifetimeHTML = "token-lifetime-html" KeyTokenLifetimeAPI = "token-lifetime-api" KeyURLPrefix = "url-prefix" KeyVerbose = "verbose" ) // SetupStartup initializes the startup data. func SetupStartup(cfg *meta.Meta, manager place.Manager, idx index.Indexer, simple bool) error { if config.urlPrefix != "" { panic("startup.config already set") } config.simple = simple config.verbose = cfg.GetBool(KeyVerbose) config.readonlyMode = cfg.GetBool(KeyReadOnlyMode) config.urlPrefix = cfg.GetDefault(KeyURLPrefix, "/") if prefix, ok := cfg.Get(KeyURLPrefix); ok && len(prefix) > 0 && prefix[0] == '/' && prefix[len(prefix)-1] == '/' { config.urlPrefix = prefix } else { config.urlPrefix = "/" } if val, ok := cfg.Get(KeyListenAddress); ok { config.listenAddress = val // TODO: check for valid string } else { config.listenAddress = "127.0.0.1:23123" } config.owner = id.Invalid if owner, ok := cfg.Get(KeyOwner); ok { if zid, err := id.Parse(owner); err == nil { config.owner = zid config.withAuth = true } } if config.withAuth { config.insecCookie = cfg.GetBool(KeyInsecureCookie) config.persistCookie = cfg.GetBool(KeyPersistentCookie) config.secret = calcSecret(cfg) config.htmlLifetime = getDuration( cfg, KeyTokenLifetimeHTML, 1*time.Hour, 1*time.Minute, 30*24*time.Hour) config.apiLifetime = getDuration( cfg, KeyTokenLifetimeAPI, 10*time.Minute, 0, 1*time.Hour) } config.simple = simple && !config.withAuth config.manager = manager config.indexer = idx return nil } func calcSecret(cfg *meta.Meta) []byte { h := fnv.New128() if secret, ok := cfg.Get("secret"); ok { io.WriteString(h, secret) } io.WriteString(h, version.Prog) io.WriteString(h, version.Build) io.WriteString(h, version.Hostname) io.WriteString(h, version.GoVersion) io.WriteString(h, version.Os) io.WriteString(h, version.Arch) return h.Sum(nil) } func getDuration( cfg *meta.Meta, key string, defDur, minDur, maxDur time.Duration) time.Duration { if s, ok := cfg.Get(key); ok && len(s) > 0 { if d, err := strconv.ParseUint(s, 10, 64); err == nil { secs := time.Duration(d) * time.Minute if secs < minDur { return minDur } if secs > maxDur { return maxDur } return secs } } return defDur } // IsSimple returns true if Zettelstore was not started with command "run" // and authentication is disabled. func IsSimple() bool { return config.simple } // IsVerbose returns whether the system should be more chatty about its operations. func IsVerbose() bool { return config.verbose } // IsReadOnlyMode returns whether the system is in read-only mode or not. func IsReadOnlyMode() bool { return config.readonlyMode } // URLPrefix returns the configured prefix to be used when providing URL to // the service. func URLPrefix() string { return config.urlPrefix } // ListenAddress returns the string that specifies the the network card and the ip port // where the server listens for requests func ListenAddress() string { return config.listenAddress } // WithAuth returns true if user authentication is enabled. func WithAuth() bool { return config.withAuth } // SecureCookie returns whether the web app should set cookies to secure mode. func SecureCookie() bool { return config.withAuth && !config.insecCookie } // PersistentCookie returns whether the web app should set persistent cookies // (instead of temporary). func PersistentCookie() bool { return config.persistCookie } // Owner returns the zid of the zettelkasten's owner. // If there is no owner defined, the value ZettelID(0) is returned. func Owner() id.Zid { return config.owner } // IsOwner returns true, if the given user is the owner of the Zettelstore. func IsOwner(zid id.Zid) bool { return zid.IsValid() && zid == config.owner } // Secret returns the interal application secret. It is typically used to // encrypt session values. func Secret() []byte { return config.secret } // TokenLifetime return the token lifetime for the web/HTML access and for the // API access. If lifetime for API access is equal to zero, no API access is // possible. func TokenLifetime() (htmlLifetime, apiLifetime time.Duration) { return config.htmlLifetime, config.apiLifetime } // PlaceManager returns the managing place. func PlaceManager() place.Manager { return config.manager } // Indexer returns the current indexer. func Indexer() index.Indexer { return config.indexer } |
Added config/startup/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 | //----------------------------------------------------------------------------- // 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 startup provides functions to retrieve startup configuration data. package startup import ( "os" "runtime" ) // Version describes all elements of a software version. type Version struct { Prog string // Name of the software Build string // Representation of build process Hostname string // Host name a reported by the kernel GoVersion string // Version of go Os string // GOOS Arch string // GOARCH // More to come } var version Version // SetupVersion initializes the version data. func SetupVersion(progName, buildVersion string) { version.Prog = progName if buildVersion == "" { version.Build = "unknown" } else { version.Build = buildVersion } if hn, err := os.Hostname(); err == nil { version.Hostname = hn } else { version.Hostname = "*unknown host*" } version.GoVersion = runtime.Version() version.Os = runtime.GOOS version.Arch = runtime.GOARCH } // GetVersion returns the current software version data. func GetVersion() Version { return version } |
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> modified: 20210111182407 site-name: Zettelstore Manual start: 00001000000000 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 22 | id: 00001000000000 title: Zettelstore Manual role: manual tags: #manual #zettelstore syntax: zmk modified: 20210126174156 * [[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/00001001000000.zettel.
1 2 3 4 5 6 7 8 9 10 11 12 | id: 00001001000000 title: Introduction to the Zettelstore role: manual tags: #introduction #manual #zettelstore syntax: zmk [[Personal knowledge management|https://en.wikipedia.org/wiki/Personal_knowledge_management]] is about collecting, classifying, storing, searching, retrieving, assessing, evaluating, and sharing knowledge as a daily activity. Personal knowledge management is done by most people, not necessarily as part of their main business. It is essential for knowledge workers, like students, researchers, | > | 1 2 3 4 5 6 7 8 9 10 11 12 13 | id: 00001001000000 title: Introduction to the Zettelstore role: manual tags: #introduction #manual #zettelstore syntax: zmk modified: 20210126170856 [[Personal knowledge management|https://en.wikipedia.org/wiki/Personal_knowledge_management]] is about collecting, classifying, storing, searching, retrieving, assessing, evaluating, and sharing knowledge as a daily activity. Personal knowledge management is done by most people, not necessarily as part of their main business. It is essential for knowledge workers, like students, researchers, |
︙ | ︙ |
Changes to docs/manual/00001002000000.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 | 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 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 | title: Installation of the Zettelstore software role: manual tags: #installation #manual #zettelstore syntax: zmk modified: 20201221142822 === 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 ""Welcome to Zettelstore"" 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. === 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/00001004000000.zettel.
1 2 3 4 5 | id: 00001004000000 title: Configuration of Zettelstore role: manual tags: #configuration #manual #zettelstore syntax: zmk | | | | < < < | < | > | | | > | < | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 | id: 00001004000000 title: Configuration of Zettelstore role: manual tags: #configuration #manual #zettelstore syntax: zmk modified: 20210125195740 There are two levels to change the behavior and/or the appearance of Zettelstore. The first level is the configuration that is needed to start the services provided by Zettelstore. For example, this includes the URI under which your Zettelstore is accessible. * [[Zettelstore start-up configuration|00001004010000]] The second level is configuring the running Zettelstore. For example, you can configure the default language of your Zettelstore. * [[Configure a running Zettelstore|00001004020000]] The third level is the way to start Zettelstore services and to manage it. * [[Command line parameters|00001004050000]] |
Changes to docs/manual/00001004010000.zettel.
1 | id: 00001004010000 | | < | | | | < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < | | | < < < < < < < < < < | | | | < < < < < < < < < < < < < < < < < | < < < | | | | > > > > > > > | | < | < < < | < < | | > < | | < < < | | | > | | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 | id: 00001004010000 title: Zettelstore start-up configuration role: manual tags: #configuration #manual #zettelstore syntax: zmk modified: 20201226183537 The configuration file, as specified by the ''-c CONFIGFILE'' [[command line option|00001004051000]], allows you to specify some start-up 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 start-up 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: ; [!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-X-uri]''place-//X//-uri'', where //X// is a number greater or equal to one : Specifies a [[place|00001004011200]] where zettel are stored. During startup //X// is counted, starting with one, until no key is found. This allows to configure more than one place. If no ''place-1-uri'' key is given, the overall effect will be the same as if only ''place-1-uri'' was specified with the value ''dir://.zettel''. In this case, even a key ''place-2-uri'' 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 start 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 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 | title: Zettelstore places tags: #configuration #manual #zettelstore syntax: zmk role: manual 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 volatilely, 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-X-uri'' keys of the [[start-up configuration|00001004010000#place-X-uri]] (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. ; ''mem:'' : Stores all its zettel in volatile memory. If you stop the Zettelstore, all changes are lost. ; ''const:'' : Is a place of predefined, essential zettel. All places that you configure via the ''store-X-uri'' keys form a chain of places. If a zettel should be retrieved, a search starts in the place specified with the ''place-1-uri'' key, then ''place-2-uri'' and so on. If a zettel is created or changed, it is always stored in the place specified with the ''place-1-uri'' key. This allows to overwrite zettel from other places, e.g. the predefined zettel. If you did not configure the place of the predefined zettel (''const:'') they will automatically be appended as a last place. Otherwise Zettelstore will not work in certain situations. If you use the ''mem:'' place, where zettel are stored in volatile memory, it makes only sense if you configure it as ''place-1-uri''. Such a place will be empty when Zettelstore starts and only the place 1 will receive updates. You must make sure that your computer has enough RAM to store all zettel. |
Changes to docs/manual/00001004011400.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 | title: Configure file directory places tags: #configuration #manual #zettelstore syntax: zmk role: manual 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:| |rescan|Time (in seconds) after which the directory should be scanned fully|600 |worker|Number of worker that can access the directory in parallel|17 |readonly|Allow only operations that do not change a zettel or create a new zettel|n/a === Rescan On most platforms, Zettelstore automatically detects changes to zettel files that originates from other software[^This includes most Linux distributions, macOS, and Windows]. 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 diectory. The time interval is configured by the ''rescan'' parameter, e.g. ``` place-1-uri: 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. === Worker Internally, Zettelstore parallels concurrent requests for a zettel or its metadata. The number of parallel activities is configured by the ''worker'' parameter. Its default value 17 is a good compromise when you think about the high variability of possible Zettelstore environments. 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, only a limited number of parallel accesses are desireable. Since Zettelstore is a single-user software, the value 17 is quite reasonable, even for higher use. 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. === 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-1-uri: 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 85 | id: 00001004020000 title: Configure the running Zettelstore role: manual tags: #configuration #manual #zettelstore syntax: zmk modified: 20201231131204 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). ; [!marker-external]''marker-external'' : Some HTML code that is displayed after a reference to external material. Default: ''&\#8599;&\#xfe0e;'', to display a ""↗︎"" sign[^The string ''&\#xfe0e;'' is needed to enforce the sign on all platforms.]. ; [!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''. ; [!start]''start'' : Specifies the ID of the zettel, that should be presented for the default view. If not given or if the ID does not identify a zettel, the list of all zettel is shown. ; [!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 29 | id: 00001004050000 title: Command line parameters role: manual tags: #command #configuration #manual #zettelstore syntax: zmk modified: 20210104115555 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 config``|00001004050600]] to show the currently active [[configuration|00001004000000]]. * [[``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 22 | id: 00001004050200 title: The ''help'' sub-command role: manual tags: #command #configuration #manual #zettelstore syntax: zmk modified: 20210104115646 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 29 | id: 00001004050400 title: The ''version'' sub-command role: manual tags: #command #configuration #manual #zettelstore syntax: zmk modified: 20210104115659 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. |
Added docs/manual/00001004050600.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 | id: 00001004050600 title: The ''config'' sub-command role: manual tags: #command #configuration #manual #zettelstore syntax: zmk modified: 20210104115712 precursor: 00001004050000 Shows the Zettelstore configuration, for debugging purposes. Currently, only the [[start-up configuration|00001004010000]] is shown. This sub-command uses the same command line parameters as [[``zettelstore run``|00001004051000]]. An example for an unconfigured Zettelstore: ``` # zettelstore config Zettelstore (v0.0.4/go1.15) running on mycomputer (linux/amd64) Stores Read only = false Web Listen Addr = "127.0.0.1:23123" URL prefix = "/" ``` The first line is identical to the output of the [[``zettelkasten version``|00001004050400]] sub-command. |
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 | id: 00001004051000 title: The ''run'' sub-command role: manual tags: #command #configuration #manual #zettelstore syntax: zmk modified: 20210104115719 precursor: 00001004050000 === ``zettelstore run`` This starts the web service. ``` zettelstore run [-c CONFIGFILE] [-d DIR] [-p PORT] [-r] [-v] ``` ; ''-c CONFIGFILE'' : Specifies ''CONFIGFILE'' as a file, where [[start-up 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 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 PORT'' : Specifies the integer value ''PORT'' as the TCP port, where the Zettelstore service 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'' of the configuration file as described below. ; ''-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'' : Be more verbose in writing logs. Writes the startup configuration to stderr. Command line options take precedence over configuration file 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 24 | id: 00001004051100 title: The ''run-simple'' sub-command role: manual tags: #command #configuration #manual #zettelstore syntax: zmk modified: 20210104115448 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 31 | id: 00001004051200 title: The ''file'' sub-command role: manual tags: #command #configuration #manual #zettelstore syntax: zmk modified: 20210104115726 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 28 | id: 00001004051400 title: The ''password'' sub-command role: manual tags: #command #configuration #manual #zettelstore syntax: zmk modified: 20210104115737 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.
|
| < < < < < < < < < < < < < < < < < < < < < < < < |
Deleted docs/manual/00001004100000.zettel.
|
| < < < < < < < < < < < < < < < < < < < < < < < < < |
Deleted docs/manual/00001004101000.zettel.
|
| < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < |
Changes to docs/manual/00001005000000.zettel.
1 2 3 4 5 | id: 00001005000000 title: Structure of Zettelstore role: manual tags: #design #manual #zettelstore syntax: zmk | | | | | | | | | > | < < < | | | | | | | | < < < < < < < < < < < < | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 | id: 00001005000000 title: Structure of Zettelstore role: manual tags: #design #manual #zettelstore syntax: zmk modified: 20210125195908 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 starts 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 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 | id: 00001005090000 title: List of predefined zettel role: manual tags: #manual #reference #zettelstore syntax: zmk modified: 20210126114739 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 | [[00000000000006]] | Zettelstore Environment Values | Contains environmental data of Zettelstore executable | [[00000000000008]] | Zettelstore Runtime Values | Contains values that reflect the inner working; see [[here|https://golang.org/pkg/runtime/]] for a technical description of these values | [[00000000000018]] | Zettelstore Indexer | Provides some statistics about the index process | [[00000000000020]] | Zettelstore Place Manager | Contains some statistics about zettel places | [[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]] | [[00000000000098]] | Zettelstore Startup Values | Contains all values computed from 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]] | [[00000000091001]] | New Zettel | Template for a new zettel with role ""[[zettel|00001006020100]]"" | [[00000000096001]] | New User | Template for a new zettel with role ""[[user|00001006020100#user]]"" If a zettel is not linked, it is not accessible for the current user. **Important:** The identifier may change until a stable version of the software is released. |
Changes to docs/manual/00001006000000.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 | 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 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 | 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 starts at 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 start 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 103 104 105 106 107 108 109 | id: 00001006020000 title: Supported Metadata Keys role: manual tags: #manual #meta #reference #zettel #zettelstore syntax: zmk modified: 20210123223645 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. ; [!new-role]''new-role'' : Used in a template zettel to specify the ''role'' of the new zettel. ; [!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 start 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. --- Not yet supported, but planned: ; [!folge]''folge'' : The IDs of zettel that acts as a [[Folgezettel|https://zettelkasten.de/posts/tags/folgezettel/]]. |
Changes to docs/manual/00001006020100.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 | title: Supported Zettel Roles tags: #manual #meta #reference #zettel #zettelstore syntax: zmk role: manual The [[''role'' key|00001006020000#role]] defines what kind of zettel you are writing. The following values are used by Zettelstore: ; [!new-template]''new-template'' : Zettel with this role are used as templates for creating new zettel. Within such a zettel, the metadata key [[''new-role''|00001006020000#new-role]] is used to specify the role of the new zettel. ; [!user]''user'' : If you want to use [[authentication|00001010000000]], all zettel that identify users of the zettel store must have this role. Beside 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 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 | 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|00001006030000]] 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|00001006030000]] is interpreted as ""false"", anybody can modify the zettel. If the metadata value is the same as an explicit [[user role|00001010070300]], user with that role (or below) 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 (the value ""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 should have such an access. |
Changes to docs/manual/00001006030000.zettel.
1 2 3 4 5 | id: 00001006030000 title: Supported Key Types role: manual tags: #manual #meta #reference #zettel #zettelstore syntax: zmk | < | | | | < < < < < < < < < < < | < | | | > > > | > | | < | | < < < < > > > > | | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 | id: 00001006030000 title: Supported Key Types role: manual tags: #manual #meta #reference #zettel #zettelstore syntax: zmk modified: 20210108184053 Most [[supported metadata keys|00001006020000]] conform to a type. Every metadata key should conform to a type. Every key type is specified by a letter. User-defined types are normally strings (type ''e''). 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. |= Name | Meaning | Match | Sorting | Boolean | Boolean value, False if value starts with ""''0''"", ""''F''"", ""''N''"", ""''f''"", or ""''n''"" | Boolean match | False < True | Credential | Value is a credential, e.g. an encrypted password (planned) | Never matches | Uses zettel identifier for sorting | Timestamp | Timestamp value YYYYMMDDHHmmSS | prefix match | by number | EString | Any string, possibly empty | case-insensitive contains | case-sensitive | Identifier | Value is a [[zettel identifier|00001006050000]] | prefix match | by number | Number | Integer value | exact match | by number | String | Any string, must not be empty | case-insensitive contains | case-sensitive | TagSet | Value is a space-separated list of tags | exact match for one tag | case sensitive by sorted tags | Word | Alfanumeric word, case-insensitive | case-insensitive equality | case-sensitive | WordSet | Space-separated list of alfanumeric words, case-insensitive | case-insensitive match for one word | case-sensitive by sorted words | URL | URL / URI | case-insensitive contains | case-sensitive | Zettelmarkup | Any string, must not be empty, formatted in [[Zettelmarkup|00001007000000]] | case-insensitive contains | case-sensitive |
Deleted docs/manual/00001006030500.zettel.
|
| < < < < < < < < < < < < < < |
Deleted docs/manual/00001006031000.zettel.
|
| < < < < < < < < < < < < < < < < < < < < |
Deleted docs/manual/00001006031500.zettel.
|
| < < < < < < < < < < < < < < < < < < < < < < < < < |
Deleted docs/manual/00001006032000.zettel.
|
| < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < |
Deleted docs/manual/00001006032500.zettel.
|
| < < < < < < < < < < < < < < < < < < < < < < |
Deleted docs/manual/00001006033000.zettel.
|
| < < < < < < < < < < < < < < < < < < < < < < < < |
Deleted docs/manual/00001006033500.zettel.
|
| < < < < < < < < < < < < < < < < < < < < < < < < |
Deleted docs/manual/00001006034000.zettel.
|
| < < < < < < < < < < < < < < < < < < < < < < < |
Deleted docs/manual/00001006034500.zettel.
|
| < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < |
Deleted docs/manual/00001006035000.zettel.
|
| < < < < < < < < < < < < < < < < < < < |
Deleted docs/manual/00001006035500.zettel.
|
| < < < < < < < < < < < < < < < < < < < < |
Deleted docs/manual/00001006036500.zettel.
|
| < < < < < < < < < < < < < < < < < < < < < < < < < < < |
Changes to docs/manual/00001006050000.zettel.
|
| < < | | < | < < < | | | < | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 | 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"". The identifiers of zettel if this manual have be chosen to start 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 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 | 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 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 | 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 starts at the first position of a line and starts with three or more identical characters. List blocks also starts at the first position of a line, but may need one or more character, plus a space character. Table blocks starts 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 start 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 starts with two non-space, often identical characters. With some exceptions, two identical non-space characters starts 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, starting 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 start 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 start 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, list of depth 2 will start a line with ``** Item 2.2``{=zmk}. An inline element to strongly emphasize some text start with a space will be specified as ``** Text**``{=zmk}. To force the inline element formatting at the start 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 starts at the for position of a line with three identical characters. * The most important exception to this rule is the specification of lists. * If no block element is found, a paragraph with inline elements is assumed. * With some exceptions, inline-structural elements starts with two characters, quite often the same two characters. * The most important exceptions are links. * The backslash character can help to resolve possible ambiguities. * Attributes refine some block and inline elements. * Block elements have a higher priority than inline elements. These principles makes automatic recognizing zettelmarkup an (relatively) easy task. By looking at the reference implementation, a moderately skilled software developer should be able to create a appropriate software in a different programming language. |
Changes to docs/manual/00001007020000.zettel.
|
| < < | | | < | | | | | | | | | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 | 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 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 | title: Zettelmarkup: Blocks-Structured Elements tags: #manual #zettelmarkup #zettelstore syntax: zmk role: manual Every markup for blocks-structured elements (""blocks"") starts 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 start 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, starting 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. === Paragraphs Any line that does not conform to another blocks-structured element starts a paragraph. This has the implication that a mistyped syntax element for a block element will be part of the paragraph. For example: ```zmk = Heading Some text follows. ``` will be rendered in HTML as :::example = Heading Some text follows. ::: This is because headings need at least three equal sign character. A paragraph is essentially a sequence of [[inline-structured elements|00001007040000]]. Inline-structured elements cam span more than one line. Paragraphs are separated by empty lines. If you want to specify a second paragraph inside a list item, or if you want to continue a paragraph on a second and more line within a list item, you must start the paragraph with a certain number of space characters. The number of space characters depends on the kind of a list and the relevant nesting level. A line that starts with a space character and which is outside of a list or does not contain the right number of space characters is considered to be part of a paragraph. |
Changes to docs/manual/00001007030100.zettel.
|
| < < | | | | | | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 | 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 start the 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 start 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. Example: |
︙ | ︙ |
Changes to docs/manual/00001007030200.zettel.
|
| < < | | | | | < | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 | 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 start the each following line as it is the lists are nested, plus one additional space character. In other words: the inline elements must start at the same column as it was on the previous line. The resulting sequence on inline elements is merged into a paragraph. Appropriately indented paragraphs can specified after the first one. Since each blocks-structured element has to be specified at the first position of a line, none of the nested list items may contain anything else than paragraphs. Some examples: ```zmk |
︙ | ︙ | |||
87 88 89 90 91 92 93 | *# A.3 * B * C ::: Please note that two lists cannot be separated by an empty line. | | | | 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 | *# 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 3 4 5 6 7 8 9 10 11 12 13 14 15 | 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 start 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. | | | 27 28 29 30 31 32 33 34 35 36 37 38 39 40 | === Notes The heading level is translated to a HTML heading by adding 1 to the level, e.g. ``=== Level 1 Heading``{=zmk} translates to ==<h2>Level 1 Heading</h2>=={=html}. The ==<h1>=={=html} tag is rendered for the zettel title. This syntax is often used in a similar way in wiki implementation. However, trailing equal signs are //not//{-} removed, they are part of the heading text. If you use command line tools, you can easily create a draft table of contents with the command: ```sh grep -h '^====* ' ZETTEL_ID.zettel ``` |
Changes to docs/manual/00001007030400.zettel.
|
| < | < | < | | | | | | | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 | 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 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 | 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 start 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 start 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 starting line will not be interpreted, until a line starts with at least the same number of the same characters given at the starting line. This allows to enter some grave accent characters in the text that should not be interpreted. For example: `````zmk ````zmk ``` ```` |
︙ | ︙ |
Changes to docs/manual/00001007030600.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 | 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 starts with at least three less-than characters (""''<''"", ''U+003C'') at the first position of a line. You can add some [[attributes|00001007050000]] on the start 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 starting line will be interpreted, until a line starts with at least the same number of the same characters given at the starting 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 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 | 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 starts with at least three quotation mark characters (""''"''"", ''U+0022'') at the first position of a line. You can add some [[attributes|00001007050000]] on the start 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 starting line will be interpreted, until a line starts with at least the same number of the same characters given at the starting 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 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 | 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 starts 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 start 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 starting line will be interpreted, until a line starts with at least the same number of the same characters given at the starting 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. For example: ```zmk |
︙ | ︙ | |||
65 66 67 68 69 70 71 | Generic attributes that are result in a special HTML rendering are: * example * note * tip * important * caution * warning | < < < < < < < < < < < < < | 62 63 64 65 66 67 68 | 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 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 | 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 start with at least three percent sign characters (""''%''"", ''U+0025'') at the first position of a line. You can add some [[attributes|00001007050000]] on the start 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 starting line will not be interpreted, until a line starts with at least the same number of the same characters given at the starting line. This allows to enter some percent sign characters in the text that should not be interpreted. For example: ```zmk %%% Comment Block |
︙ | ︙ |
Changes to docs/manual/00001007031000.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 | title: Zettelmarkup: Tables tags: #manual #zettelmarkup #zettelstore syntax: zmk role: manual 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 start with the vertical bar character (""''|''"", ''U+007C'') at the first position of a line. The other cells of a row start 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 ::: 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 ::: 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| |123|123|123|123 ``` will be rendered in HTML as: :::example |=Left<|Right>|Center:|Default |123456|123456|123456|123456| |123|123|123|123 ::: To specify the alignment of an individual cell, you can enter these characters for alignment as the first character of that cell. For example: ```zmk |=Left<|Right>|Center:|Default |>R|:C|<L |123456|123456|123456|123456| |123|123|123|123 ``` will be rendered in HTML as: :::example |=Left<|Right>|Center:|Default |>R|:C|<L |123456|123456|123456|123456| |123|123|123|123 ::: |
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 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 | 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 starts 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 is started with two consecutive percent sign characters (""''%''"", ''U+0025''). It ends at the end of the line where it started. ==== 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 starts 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 starts 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 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 | 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 starts 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 start 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 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 | title: Zettelmarkup: Literal-like formatting tags: #manual #zettelmarkup #zettelstore syntax: zmk role: manual 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 place 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 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 | 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 starts 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 starts with the slash character (""/"", ''U+002F''), i.e. without scheme, user info, and host name, or if it starts with ""./"" or with ""../"", the reference will be treated as a ""local reference"", otherwise as an ""external reference"". 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 starts 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: %%Example: %%``{{External link|00000000030001}}{title=External width=30}`` is rendered as ::{{External link|00000000030001}}{title=External width=30}::{=example}. === Footnotes A footnote starts 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: starting 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 starts 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 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 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 | 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 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 | 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]]) 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. |
Changes to docs/manual/00001007050200.zettel.
|
| < | 1 2 3 4 5 6 | title: Zettelmarkup: Supported Attribute Values for Programming Languages tags: #manual #reference #zettelmarkup #zettelstore syntax: zmk role: manual TBD |
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 | 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 start 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: 20210111182215 [[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: * Markdown * Images: GIF, PNG, JPEG, SVG * CSS * HTML template data * 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]]). 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 as the main markup language of 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 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 | title: Use Markdown as the main markup language of Zettelstore tags: #manual #markdown #zettelstore syntax: zmk role: manual If you are customized to use Markdown as your markup language, you can configure Zettelstore to support your decision. Just 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, but also depends on the value of ''zettel-file-syntax'' and, to some degree, on the value of ''yaml-header''. All key are described [[here|00001004020000]]. 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. |
Deleted docs/manual/00001008010500.zettel.
|
| < < < < < < < < < < < < < < < < < < < < < < < < < < < < < |
Deleted docs/manual/00001008050000.zettel.
|
| < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < |
Changes to docs/manual/00001010000000.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 | title: Security tags: #configuration #manual #security #zettelstore syntax: zmk role: manual 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.]. You enable read-only mode through the key ''readonly'' in the [[start-up configuration zettel|00001004010000]] or with the ''-r'' option of the ``zettelstore run`` sub-command. === Authentication The Zettelstore can be configured that a user must authenticate itself to gain access to the content. * [[How to enable authentication|00001010040100]] * [[How to add a new user|00001010040200]] * [[How users are authenticated|00001010040400]] (some technical background) * [[Authenticated sessions|00001010040700]] === Authorization Once you have enabled authentication, it is possible to allow others to access your Zettelstore. Maybe, others should be able to read some or all of your zettel. Or you want to allow them to create new zettel, or to change them. It is up to you. If someone is authenticated as the owner of the Zettelstore (hopefully you), no restrictions apply. But as an owner, you can create ""user zettel"" to allow others to access your Zettelstore in various ways. Even if you do not want to share your Zettelstore with other persons, creating user zettel can be useful if you plan to access your Zettelstore via the API. 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 start-up configuration. Otherwise, authentication will not work. * [[Use a server for encryption|00001010090100]] |
Changes to docs/manual/00001010040100.zettel.
|
| < < | | | < < | 1 2 3 4 5 6 7 8 | title: Enable authentication tags: #authentication #configuration #manual #security #zettelstore syntax: zmk role: manual To enable authentication, you must create a zettel that stores [[authentication data|00001010040200]] for the owner. Then you must reference this zettel within the [[start-up configuration|00001004010000]] under the key ''owner''. Once the start-up configuration contains a valid [[zettel identifier|00001006050000]] under that key, authentication is enabled. |
Changes to docs/manual/00001010040200.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 | 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 3 4 5 6 7 8 9 10 11 12 13 14 15 16 | 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 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 | title: Access token tags: #authentication #configuration #manual #security #zettelstore syntax: zmk role: manual 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 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 | title: Visibility rules for zettel role: manual tags: #authorization #configuration #manual #security #zettelstore syntax: zmk modified: 20201221174224 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. ; [!simple-expert]""simple-expert"" : The owner of the Zettelstore cab access the zettel, if expert mode is enabled, or if authentication is disabled and the Zettelstore is started without any command. The reason for this is to show all computed zettel to an user that started the Zettestore in simple mode. Many computed zettel should be given in error reporting and a new user might not be able to enable expert mode. When you install a Zettelstore, only two zettel have visibility ""public"". The first zettel 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. The other zettel is the zettel containing the [[version|00000000000001]] of the Zettelstore. 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 ''00000000000099'' 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 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 | 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 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 | 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 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 | 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. * Reload internal values ** Reject the access. Only the owner of the Zettelstore is allowed to perform a reload operation. 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: 20210125195546 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. |
︙ | ︙ | |||
55 56 57 58 59 60 61 62 63 64 65 | If you want to add some additional content on the server, you could change the configuration as follows: ``` zettelstore.de { file_server * { root /var/www/html } route /manual/* { reverse_proxy localhost:23123 } } ``` | > | | | | 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 | If you want to add some additional content on the server, you could change the configuration as follows: ``` zettelstore.de { file_server * { root /var/www/html } route /manual/* { uri strip_prefix /manual reverse_proxy localhost:23123 } } ``` 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 start-tp configuration key ''url-prefix'' with the value ""/manual"". This is to allow the Zettelstore to give you the correct URLs with the given prefix. |
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 | id: 00001012000000 title: API role: manual tags: #api #manual #zettelstore syntax: zmk modified: 20210112113014 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|00001006020000]] used in a Zettelstore * List all [[roles|00001006020100]] used in a Zettelstore. === Working with zettel * Create a new zettel * [[Retrieve metadata and content of an existing zettel|00001012053400]] * [[Retrieve references of an existing zettel|00001012053600]] * 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 48 | id: 00001012050200 title: API: Authenticate a client role: manual tags: #api #manual #zettelstore syntax: zmk modified: 20210111190943 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 [[start-up 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 ''/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 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 | title: API: Renew an access token tags: #api #manual #zettelstore syntax: zmk role: manual 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 ''/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 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 | 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 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 | title: API: List metadata of all zettel tags: #api #manual #zettelstore syntax: zmk role: manual To list the metadata of all zettel just send a HTTP GET request to the endpoint ''/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 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 | 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 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 | 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 | id: 00001012051800 title: API: Shape the list of zettel metadata with filter options role: manual tags: #api #manual #zettelstore syntax: zmk modified: 20210112114019 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// start 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"}}, ... ``` 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 start 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, which will be searched for in all metadata. While searching, the [[type|00001006030000]] of each metadata key will be respected. 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 both 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 23 | id: 00001012052000 title: API: Sort the list of zettel metadata role: manual tags: #api #manual #zettelstore syntax: zmk modified: 20210112113839 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. |
Deleted docs/manual/00001012053200.zettel.
|
| < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < |
Deleted docs/manual/00001012053300.zettel.
|
| < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < |
Changes to docs/manual/00001012053400.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 | title: API: Retrieve metadata and content of an existing zettel tags: #api #manual #zettelstore syntax: zmk role: manual The endpoint 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 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 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 | 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. |
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 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 | title: Endpoints used by the API tags: #api #manual #reference #zettelstore syntax: zmk role: manual All API endpoints conform to the pattern ''[PREFIX]/LETTER[/ZETTEL-ID]'', where: ; ''PREFIX'' : is an optional URL prefix, 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]] | ''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 will start with ''http://127.0.0.1:23123''. |
Changes to docs/manual/00001012920500.zettel.
|
| < | < | < < > > | < | > > > > | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 | 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 | 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 | 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 2 3 4 5 6 7 8 9 10 11 | 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 2 3 4 5 6 7 8 9 10 11 | 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 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 | 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 2 3 4 5 6 7 8 9 10 11 | 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 2 3 4 5 6 7 8 9 | 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 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 | 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 2 3 4 5 6 7 8 9 10 11 12 13 | 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.
|
| < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < |
Changes to docs/manual/00001014000000.zettel.
|
| < | 1 2 3 4 5 6 7 | title: Web user interface tags: #manual #webui #zettelstore syntax: zmk role: manual The Web user interface is just a secondary way to interact with a Zettelstore. Using external software that interacts via the [[API|00001012000000]] is the recommended way. |
Deleted docs/manual/00001017000000.zettel.
|
| < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < |
Deleted docs/manual/00001018000000.zettel.
|
| < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < |
Deleted docs/manual/00001019990010.zettel.
|
| < < < < < < < < |
Deleted docs/manual/20231128184200.zettel.
|
| < < < < < < < |
Deleted docs/readmezip.txt.
|
| < < < < < < < < < < < < < < < < < < < < |
Added domain/content.go.
> > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 | //----------------------------------------------------------------------------- // 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 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 | //----------------------------------------------------------------------------- // 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 ( "sort" "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 const ( Invalid = Zid(0) // Invalid is a Zid that will never be valid ConfigurationZid = Zid(100) BaseTemplateZid = Zid(10100) LoginTemplateZid = Zid(10200) ListTemplateZid = Zid(10300) DetailTemplateZid = Zid(10401) InfoTemplateZid = Zid(10402) FormTemplateZid = Zid(10403) RenameTemplateZid = Zid(10404) DeleteTemplateZid = Zid(10405) RolesTemplateZid = Zid(10500) TagsTemplateZid = Zid(10600) BaseCSSZid = Zid(20001) // Range 90000...99999 is reserved for zettel templates TemplateNewZettelZid = Zid(91001) TemplateNewUserZid = Zid(96001) WelcomeZid = Zid(19700101000000) ) const maxZid = 99999999999999 // Parse interprets a string as a zettel identification and // returns its integer value. func Parse(s string) (Zid, error) { if len(s) != 14 { return Invalid, strconv.ErrSyntax } res, err := strconv.ParseUint(s, 10, 47) if err != nil { return Invalid, err } if res == 0 { return Invalid, strconv.ErrRange } 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 } // Sort a slice of Zids. func Sort(zids []Zid) { sort.Sort(zidSlice(zids)) } type zidSlice []Zid func (zs zidSlice) Len() int { return len(zs) } func (zs zidSlice) Less(i, j int) bool { return zs[i] < zs[j] } func (zs zidSlice) Swap(i, j int) { zs[i], zs[j] = zs[j], zs[i] } |
Added domain/id/id_test.go.
> > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 | //----------------------------------------------------------------------------- // 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 id_test provides unit tests for testing zettel id specific functions. package id_test import ( "testing" "zettelstore.de/z/domain/id" ) func TestParseZettelID(t *testing.T) { } 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", "000000000000000", "99999999999999a", "20200310T195100", } for i, zid := range invalidIDs { if _, err := id.Parse(zid); err == nil { t.Errorf("i=%d: zid=%q is valid, but should not be", i, zid) } } } |
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 | //----------------------------------------------------------------------------- // 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 } // 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, "") KeyLang = registerKey("lang", TypeWord, usageUser, "") KeyLicense = registerKey("license", TypeEmpty, usageUser, "") KeyListPageSize = registerKey("list-page-size", TypeNumber, usageUser, "") KeyNewRole = registerKey("new-role", TypeWord, usageUser, "") KeyMarkerExternal = registerKey("marker-external", TypeEmpty, usageUser, "") KeyModified = registerKey("modified", TypeTimestamp, usageComputed, "") KeyPrecursor = registerKey("precursor", TypeIDSet, usageUser, KeyFolge) KeyPublished = registerKey("published", TypeTimestamp, usageProperty, "") KeyReadOnly = registerKey("read-only", TypeWord, usageUser, "") KeySiteName = registerKey("site-name", TypeString, usageUser, "") KeyStart = registerKey("start", TypeID, 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" ValueRoleNewTemplate = "new-template" ValueRoleZettel = "zettel" ValueSyntaxNone = "none" ValueSyntaxZmk = "zmk" ValueTrue = "true" ValueFalse = "false" ValueUserRoleReader = "reader" ValueUserRoleWriter = "writer" ValueUserRoleOwner = "owner" ValueVisibilityExpert = "expert" ValueVisibilityOwner = "owner" ValueVisibilityLogin = "login" ValueVisibilityPublic = "public" ValueVisibilitySimple = "simple-expert" ) // 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 string, 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 bool, 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 232 233 234 235 236 237 238 239 240 241 242 243 244 245 | //----------------------------------------------------------------------------- // 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 newMeta(title string, tags []string, syntax string) *Meta { m := New(testID) if title != "" { m.Set(KeyTitle, title) } if tags != nil { m.Set(KeyTags, strings.Join(tags, " ")) } if syntax != "" { m.Set(KeySyntax, syntax) } return m } 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 188 189 | //----------------------------------------------------------------------------- // 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) } case TypeEmpty: fallthrough 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 | //----------------------------------------------------------------------------- // 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 ( "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, " ") } } // 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 } // 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 } |
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 67 68 | //----------------------------------------------------------------------------- // 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 // Visibility enumerates the variations of the 'visibility' meta key. type Visibility int // Supported values for visibility. const ( _ Visibility = iota VisibilityUnknown VisibilityPublic VisibilityLogin VisibilityOwner VisibilitySimple VisibilityExpert ) var visMap = map[string]Visibility{ ValueVisibilityPublic: VisibilityPublic, ValueVisibilityLogin: VisibilityLogin, ValueVisibilityOwner: VisibilityOwner, ValueVisibilitySimple: VisibilitySimple, 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 } |
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 95 96 97 98 99 100 101 102 103 104 105 | //----------------------------------------------------------------------------- // 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 ( "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 { SetOption(Option) 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") ) // Option allows to configure an encoder type Option interface { Name() string } // Create builds a new encoder with the given options. func Create(format string, options ...Option) Encoder { if info, ok := registry[format]; ok { enc := info.Create() for _, opt := range options { enc.SetOption(opt) } return enc } return nil } // Info stores some data about an encoder. type Info struct { Create func() 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/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 | //----------------------------------------------------------------------------- // 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 ( "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.b.WriteString("</p>\n") } // 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 { v.b.WriteStrings(line, "\n") } default: panic(fmt.Sprintf("Unknown verbatim code %v", vn.Code)) } } 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.xhtml { 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.b.WriteString("</p>\n") inPara = false } v.acceptItemSlice(item) } } if inPara { v.b.WriteString("</p>\n") } 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") } } |
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 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 | //----------------------------------------------------------------------------- // Copyright (c) 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 ( "fmt" "io" "strings" "zettelstore.de/z/ast" "zettelstore.de/z/domain/meta" "zettelstore.de/z/encoder" ) func init() { encoder.Register("html", encoder.Info{ Create: func() encoder.Encoder { return &htmlEncoder{} }, }) } type htmlEncoder struct { lang string // default language xhtml bool // use XHTML syntax instead of HTML syntax markerExternal string // Marker after link to (external) material. newWindow bool // open link in new window adaptLink func(*ast.LinkNode) ast.InlineNode adaptImage func(*ast.ImageNode) ast.InlineNode adaptCite func(*ast.CiteNode) ast.InlineNode ignoreMeta map[string]bool footnotes []*ast.FootnoteNode } func (he *htmlEncoder) SetOption(option encoder.Option) { switch opt := option.(type) { case *encoder.StringOption: switch opt.Key { case "lang": he.lang = opt.Value case meta.KeyMarkerExternal: he.markerExternal = opt.Value } case *encoder.BoolOption: switch opt.Key { case "newwindow": he.newWindow = opt.Value case "xhtml": he.xhtml = opt.Value } case *encoder.StringsOption: switch opt.Key { case "no-meta": he.ignoreMeta = make(map[string]bool, len(opt.Value)) for _, v := range opt.Value { he.ignoreMeta[v] = true } } case *encoder.AdaptLinkOption: he.adaptLink = opt.Adapter case *encoder.AdaptImageOption: he.adaptImage = opt.Adapter case *encoder.AdaptCiteOption: he.adaptCite = opt.Adapter default: var name string if option != nil { name = option.Name() } fmt.Println("HESO", option, name) } } // 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.xhtml { v.b.WriteString("<!DOCTYPE html>\n") } v.b.WriteStrings("<html lang=\"", he.lang, "\">\n<head>\n<meta charset=\"utf-8\">\n") textEnc := encoder.Create("text") var sb strings.Builder textEnc.WriteInlines(&sb, zn.Title) v.b.WriteStrings("<title>", sb.String(), "</title>") if inhMeta { v.acceptMeta(zn.InhMeta, false) } else { v.acceptMeta(zn.Zettel.Meta, false) } 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) v.acceptMeta(m, true) 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) 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 | //----------------------------------------------------------------------------- // 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 ( "fmt" "strconv" "strings" "zettelstore.de/z/ast" ) // 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.xhtml { 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.xhtml { 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) { if adapt := v.enc.adaptLink; adapt != nil { n := adapt(ln) var ok bool if ln, ok = n.(*ast.LinkNode); !ok { n.Accept(v) return } } v.lang.push(ln.Attrs) defer v.lang.pop() switch ln.Ref.State { case ast.RefStateZettelSelf, ast.RefStateZettelFound, ast.RefStateLocal: v.writeAHref(ln.Ref, ln.Attrs, ln.Inlines) case ast.RefStateZettelBroken: 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.enc.newWindow { attrs = attrs.Set("target", "_blank").Set("rel", "noopener noreferrer") } v.writeAHref(ln.Ref, attrs, ln.Inlines) v.b.WriteString(v.enc.markerExternal) default: v.b.WriteString("<a href=\"") v.writeQuotedEscaped(ln.Ref.Value) v.b.WriteByte('"') v.visitAttributes(ln.Attrs) v.b.WriteByte('>') v.acceptInlineSlice(ln.Inlines) v.b.WriteString("</a>") } } func (v *visitor) writeAHref(ref *ast.Reference, attrs *ast.Attributes, ins ast.InlineSlice) { v.b.WriteString("<a href=\"") v.writeReference(ref) v.b.WriteByte('"') v.visitAttributes(attrs) v.b.WriteByte('>') v.acceptInlineSlice(ins) v.b.WriteString("</a>") } // VisitImage writes HTML code for images. func (v *visitor) VisitImage(in *ast.ImageNode) { if adapt := v.enc.adaptImage; adapt != nil { n := adapt(in) var ok bool if in, ok = n.(*ast.ImageNode); !ok { 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.xhtml { v.b.WriteString(" />") } else { v.b.WriteByte('>') } } // VisitCite writes code for citations. func (v *visitor) VisitCite(cn *ast.CiteNode) { if adapt := v.enc.adaptCite; adapt != nil { n := adapt(cn) if n != cn { n.Accept(v) return } } v.lang.push(cn.Attrs) defer v.lang.pop() if cn != nil { 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() v.enc.footnotes = append(v.enc.footnotes, fn) n := strconv.Itoa(len(v.enc.footnotes)) 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 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: code = "span" attrs = processSpanAttributes(attrs) 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, ">") } var langQuotes = map[string][2]string{ "en": {"“", "”"}, "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: 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 | //----------------------------------------------------------------------------- // 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 ( "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 { enc *htmlEncoder b encoder.BufWriter visibleSpace bool // Show space character in raw text inVerse bool // In verse block xhtml bool // copied from enc.xhtml lang langStack } func newVisitor(he *htmlEncoder, w io.Writer) *visitor { return &visitor{ enc: he, b: encoder.NewBufWriter(w), xhtml: he.xhtml, lang: newLangStack(he.lang), } } var mapMetaKey = map[string]string{ meta.KeyCopyright: "copyright", meta.KeyLicense: "license", } func (v *visitor) acceptMeta(m *meta.Meta, withTitle bool) { for i, pair := range m.Pairs(true) { if i == 0 { // "title" is number 0... if withTitle && !v.enc.ignoreMeta[pair.Key] { v.b.WriteStrings("<meta name=\"zs-", pair.Key, "\" content=\"") v.writeQuotedEscaped(pair.Value) v.b.WriteString("\">") } continue } if !v.enc.ignoreMeta[pair.Key] { if pair.Key == meta.KeyTags { v.b.WriteString("\n<meta name=\"keywords\" content=\"") for i, val := range meta.ListFromValue(pair.Value) { if i > 0 { v.b.WriteString(", ") } v.writeQuotedEscaped(strings.TrimPrefix(val, "#")) } v.b.WriteString("\">") } else if key, ok := mapMetaKey[pair.Key]; ok { v.writeMeta("", key, pair.Value) } else { v.writeMeta("zs-", pair.Key, pair.Value) } } } } 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() { if len(v.enc.footnotes) > 0 { v.b.WriteString("<ol class=\"zs-endnotes\">\n") for i := 0; i < len(v.enc.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 := v.enc.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 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 | //----------------------------------------------------------------------------- // 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 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" ) func init() { encoder.Register("djson", encoder.Info{ Create: func() encoder.Encoder { return &jsonDetailEncoder{} }, }) } type jsonDetailEncoder struct { adaptLink func(*ast.LinkNode) ast.InlineNode adaptImage func(*ast.ImageNode) ast.InlineNode title ast.InlineSlice } // SetOption sets an option for the encoder func (je *jsonDetailEncoder) SetOption(option encoder.Option) { switch opt := option.(type) { case *encoder.TitleOption: je.title = opt.Inline case *encoder.AdaptLinkOption: je.adaptLink = opt.Adapter case *encoder.AdaptImageOption: je.adaptImage = opt.Adapter } } // 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(zn.Title) if inhMeta { v.writeMeta(zn.InhMeta, false) } else { v.writeMeta(zn.Zettel.Meta, false) } 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.WriteByte('{') if je.title == nil { v.writeMeta(m, true) } else { v.b.WriteString("\"title\":") v.acceptInlineSlice(je.title) v.writeMeta(m, false) } 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 enc *jsonDetailEncoder } func newDetailVisitor(w io.Writer, je *jsonDetailEncoder) *detailVisitor { return &detailVisitor{b: encoder.NewBufWriter(w), enc: je} } // 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.RefStateZettelSelf: "self", ast.RefStateZettelFound: "zettel", ast.RefStateZettelBroken: "broken", ast.RefStateLocal: "local", ast.RefStateExternal: "external", } // VisitLink writes JSON code for links. func (v *detailVisitor) VisitLink(ln *ast.LinkNode) { if adapt := v.enc.adaptLink; adapt != nil { n := adapt(ln) var ok bool if ln, ok = n.(*ast.LinkNode); !ok { 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) { if adapt := v.enc.adaptImage; adapt != nil { n := adapt(in) var ok bool if in, ok = n.(*ast.ImageNode); !ok { 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, withTitle bool) { first := withTitle for _, p := range m.Pairs(true) { if p.Key == "title" && !withTitle { continue } if first { v.b.WriteByte('"') first = false } else { v.b.WriteString(",\"") } v.b.Write(Escape(p.Key)) v.b.WriteString("\":") if m.Type(p.Key).IsSet { v.b.WriteByte('[') for i, val := range meta.ListFromValue(p.Value) { if i > 0 { v.b.WriteByte(',') } v.b.WriteByte('"') v.b.Write(Escape(val)) v.b.WriteByte('"') } v.b.WriteByte(']') } else { v.b.WriteByte('"') v.b.Write(Escape(p.Value)) 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 112 113 114 | //----------------------------------------------------------------------------- // 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 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.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{} // SetOption sets an option for the encoder func (je *jsonEncoder) SetOption(option encoder.Option) {} // 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 615 616 617 618 619 620 621 622 623 624 625 626 627 628 | //----------------------------------------------------------------------------- // 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 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" ) func init() { encoder.Register("native", encoder.Info{ Create: func() encoder.Encoder { return &nativeEncoder{} }, }) } type nativeEncoder struct { adaptLink func(*ast.LinkNode) ast.InlineNode adaptImage func(*ast.ImageNode) ast.InlineNode } // SetOption sets one option for this encoder. func (ne *nativeEncoder) SetOption(option encoder.Option) { switch opt := option.(type) { case *encoder.AdaptLinkOption: ne.adaptLink = opt.Adapter case *encoder.AdaptImageOption: ne.adaptImage = opt.Adapter } } // 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(zn.Title) v.b.WriteByte(']') if inhMeta { v.acceptMeta(zn.InhMeta, false) } else { v.acceptMeta(zn.Zettel.Meta, false) } v.b.WriteByte('\n') v.acceptBlockSlice(zn.Ast) length, err := v.b.Flush() return length, err } // WriteMeta encodes meta data as HTML5. 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 enc *nativeEncoder } func newVisitor(w io.Writer, enc *nativeEncoder) *visitor { return &visitor{b: encoder.NewBufWriter(w), enc: enc} } var ( rawBackslash = []byte{'\\', '\\'} rawDoubleQuote = []byte{'\\', '"'} rawNewline = []byte{'\\', 'n'} ) func (v *visitor) acceptMeta(m *meta.Meta, withTitle bool) { if withTitle { v.b.WriteString("[Title \"") v.writeEscaped(m.GetDefault(meta.KeyTitle, "")) v.b.WriteString("\"]") } v.writeMetaString(m, meta.KeyRole, "Role") v.writeMetaList(m, meta.KeyTags, "Tags") v.writeMetaString(m, meta.KeySyntax, "Syntax") if pairs := m.PairsRest(true); len(pairs) > 0 { v.b.WriteString("\n[Header") first := true v.level++ for _, p := range pairs { if !first { v.b.WriteByte(',') } v.writeNewLine() v.b.WriteByte('[') v.b.WriteStrings(p.Key, " \"") v.writeEscaped(p.Value) v.b.WriteString("\"]") first = false } v.level-- v.b.WriteByte(']') } } func (v *visitor) writeMetaString(m *meta.Meta, key string, 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 string, 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.RefStateZettelSelf: "SELF", ast.RefStateZettelFound: "ZETTEL", ast.RefStateZettelBroken: "BROKEN", ast.RefStateLocal: "LOCAL", ast.RefStateExternal: "EXTERNAL", } // VisitLink writes native code for links. func (v *visitor) VisitLink(ln *ast.LinkNode) { if adapt := v.enc.adaptLink; adapt != nil { n := adapt(ln) var ok bool if ln, ok = n.(*ast.LinkNode); !ok { 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) { if adapt := v.enc.adaptImage; adapt != nil { n := adapt(in) var ok bool if in, ok = n.(*ast.ImageNode); !ok { 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/options.go.
> > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 | //----------------------------------------------------------------------------- // Copyright (c) 2020 Detlef Stern // // This file is part of zettelstore. // // Zettelstore is licensed under 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" ) // StringOption is an option with a string value type StringOption struct { Key string Value string } // Name returns the visible name of this option. func (so *StringOption) Name() string { return so.Key } // BoolOption is an option with a boolean value. type BoolOption struct { Key string Value bool } // Name returns the visible name of this option. func (bo *BoolOption) Name() string { return bo.Key } // TitleOption is an option to give the title as a AST inline slice type TitleOption struct { Inline ast.InlineSlice } // Name returns the visible name of this option. func (mo *TitleOption) Name() string { return "title" } // StringsOption is an option that have a sequence of strings as the value. type StringsOption struct { Key string Value []string } // Name returns the visible name of this option. func (so *StringsOption) Name() string { return so.Key } // AdaptLinkOption specifies a link adapter. type AdaptLinkOption struct { Adapter func(*ast.LinkNode) ast.InlineNode } // Name returns the visible name of this option. func (al *AdaptLinkOption) Name() string { return "AdaptLinkOption" } // AdaptImageOption specifies an image adapter. type AdaptImageOption struct { Adapter func(*ast.ImageNode) ast.InlineNode } // Name returns the visible name of this option. func (al *AdaptImageOption) Name() string { return "AdaptImageOption" } // AdaptCiteOption specifies a citation adapter. type AdaptCiteOption struct { Adapter func(*ast.CiteNode) ast.InlineNode } // Name returns the visible name of this option. func (al *AdaptCiteOption) Name() string { return "AdaptCiteOption" } |
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 69 70 71 | //----------------------------------------------------------------------------- // 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 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.Encoder { return &rawEncoder{} }, }) } type rawEncoder struct{} // SetOption sets an option for the encoder func (re *rawEncoder) SetOption(option encoder.Option) {} // 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.Zettel.Meta.Write(&b, true) } b.WriteByte('\n') b.WriteString(zn.Zettel.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.Zettel.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 | //----------------------------------------------------------------------------- // 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 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" ) func init() { encoder.Register("text", encoder.Info{ Create: func() encoder.Encoder { return &textEncoder{} }, }) } type textEncoder struct{} // SetOption sets an option for this encoder func (te *textEncoder) SetOption(option encoder.Option) {} // WriteZettel does nothing. 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.Zettel.Meta) } v.acceptBlockSlice(zn.Ast) length, err := v.b.Flush() return length, err } // WriteMeta encodes meta data as text. func (te *textEncoder) WriteMeta(w io.Writer, m *meta.Meta) (int, error) { b := encoder.NewBufWriter(w) for _, pair := range m.Pairs(true) { 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 469 470 | //----------------------------------------------------------------------------- // 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 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.Encoder { return &zmkEncoder{} }, }) } type zmkEncoder struct{} // SetOption sets an option for this encoder. func (ze *zmkEncoder) SetOption(option encoder.Option) {} // 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.Zettel.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]) } 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 | module zettelstore.de/z go 1.15 require ( github.com/fsnotify/fsnotify v1.4.9 github.com/pascaldekloe/jwt v1.10.0 github.com/yuin/goldmark v1.3.0 golang.org/x/crypto v0.0.0-20200820211705-5c72a883971a golang.org/x/text v0.3.0 ) |
Changes to go.sum.
|
| | | > > | | | | > > | | | | | | < < < < < < < < | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 | 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.0 h1:DRvEHivhJ1fQhZbpmttnonfC674RycyZGE/5IJzDKgg= github.com/yuin/goldmark v1.3.0/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-20200820211705-5c72a883971a h1:vclmkQCjlDX5OydZ9wv8rBCcS0QyQY66Mpf/7BZbInM= golang.org/x/crypto v0.0.0-20200820211705-5c72a883971a/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= 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-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20191005200804-aed5e4c7ecf9 h1:L2auWcuQIvxz9xSEqzESnV/QN/gNRXNApHi3fYwl2w0= golang.org/x/sys v0.0.0-20191005200804-aed5e4c7ecf9/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/text v0.3.0 h1:g61tztE5qeGQ89tm6NTjjM9VPIm088od1l6aSorWRWg= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= |
Added index/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 | //----------------------------------------------------------------------------- // 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 index allows to search for metadata and content. package index import ( "context" "zettelstore.de/z/domain/meta" ) // Remover is used to remove some metadata before they are stored in a place. type Remover interface { // Remove removes computed properties from the given metadata. // It is called by the manager place before meta data is updated. Remove(ctx context.Context, m *meta.Meta) } // MetaFilter is used by places to filter and set computed metadata value. type MetaFilter interface { Enricher Remover } type metaFilter struct { index Indexer properties map[string]bool // Set of property key names } // NewMetaFilter creates a new meta filter. func NewMetaFilter(idx Indexer) MetaFilter { properties := make(map[string]bool) for _, kd := range meta.GetSortedKeyDescriptions() { if kd.IsProperty() { properties[kd.Name] = true } } return &metaFilter{ index: idx, properties: properties, } } func (mf *metaFilter) Enrich(ctx context.Context, m *meta.Meta) { computePublished(m) mf.index.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. } func (mf *metaFilter) Remove(ctx context.Context, m *meta.Meta) { for _, p := range m.PairsRest(true) { if mf.properties[p.Key] { m.Delete(p.Key) } } } |
Added index/index.go.
> > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 | //----------------------------------------------------------------------------- // Copyright (c) 2021 Detlef Stern // // This file is part of zettelstore. // // Zettelstore is licensed under the latest version of the EUPL (European Union // Public License). Please see file LICENSE.txt for your rights and obligations // under this license. //----------------------------------------------------------------------------- // Package index allows to search for metadata and content. package index import ( "context" "io" "time" "zettelstore.de/z/domain" "zettelstore.de/z/domain/id" "zettelstore.de/z/domain/meta" "zettelstore.de/z/place" ) // 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 } // Port contains all the used functions to access zettel to be indexed. type Port interface { RegisterObserver(func(place.ChangeInfo)) FetchZids(context.Context) (map[id.Zid]bool, error) GetMeta(context.Context, id.Zid) (*meta.Meta, error) GetZettel(context.Context, id.Zid) (domain.Zettel, error) } // Indexer contains all the functions of an index. type Indexer interface { Enricher // Start the index. It will read all zettel and store index data for later retrieval. Start(Port) // Stop the index. No zettel are read any more, but the current index data // can stil be retrieved. Stop() // ReadStats populates st with indexer statistics. ReadStats(st *IndexerStats) } // IndexerStats records statistics about the indexer. type IndexerStats struct { // 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 // Store records statistics about the underlying index store. Store StoreStats } // Store all relevant zettel data. There may be multiple implementations, i.e. // memory-based, file-based, based on SQLite, ... type Store interface { Enricher // UpdateReferences for a specific zettel. UpdateReferences(context.Context, *ZettelIndex) // DeleteZettel removes index data for given zettel. DeleteZettel(context.Context, id.Zid) // ReadStats populates st with store statistics. ReadStats(st *StoreStats) // Write the content to a Writer Write(io.Writer) } // StoreStats records statistics about the store. type StoreStats struct { // Zettel is the number of zettel managed by the indexer. Zettel int // Updates count the number of metadata updates. Updates uint64 } |
Added index/indexer/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 | //----------------------------------------------------------------------------- // 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 indexer allows to search for metadata and content. package indexer import ( "sync" "zettelstore.de/z/domain/id" ) type anteroom struct { next *anteroom waiting map[id.Zid]bool 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, val bool) { if !zid.IsValid() { return } ar.mx.Lock() defer ar.mx.Unlock() if ar.first == nil { ar.first = ar.makeAnteroom(zid, val) 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 } if v, ok := room.waiting[zid]; ok { if val == v { return } room.waiting[zid] = val return } } if room := ar.last; !room.reload && (ar.maxLoad == 0 || room.curLoad < ar.maxLoad) { room.waiting[zid] = val room.curLoad++ return } room := ar.makeAnteroom(zid, val) ar.last.next = room ar.last = room } func (ar *anterooms) makeAnteroom(zid id.Zid, val bool) *anteroom { cap := ar.maxLoad if cap == 0 { cap = 100 } waiting := make(map[id.Zid]bool, cap) waiting[zid] = val 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, true) ar.last = ar.first } func (ar *anterooms) Reload(delZids []id.Zid, newZids map[id.Zid]bool) { ar.mx.Lock() defer ar.mx.Unlock() delWaiting := make(map[id.Zid]bool, len(delZids)) for _, zid := range delZids { if zid.IsValid() { delWaiting[zid] = false } } newWaiting := make(map[id.Zid]bool, len(newZids)) for zid := range newZids { if zid.IsValid() { newWaiting[zid] = true } } // Delete previous reload rooms room := ar.first for ; room != nil && room.reload; room = room.next { } ar.first = room if room == nil { ar.last = nil } 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 } } else { ar.first = &anteroom{next: ar.first, waiting: delWaiting, curLoad: ds} if ar.first.next == nil { ar.last = ar.first } } } else { 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 } } else { ar.first = nil ar.last = nil } } } func (ar *anterooms) Dequeue() (id.Zid, bool) { ar.mx.Lock() defer ar.mx.Unlock() if ar.first == nil { return id.Invalid, false } for zid, val := 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 zid, val } return id.Invalid, false } |
Added index/indexer/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 indexer allows to search for metadata and content. package indexer import ( "testing" "zettelstore.de/z/domain/id" ) func TestSimple(t *testing.T) { ar := newAnterooms(2) ar.Enqueue(id.Zid(1), true) zid, val := ar.Dequeue() if zid != id.Zid(1) || val != true { t.Errorf("Expected 1/true, but got %v/%v", zid, val) } zid, val = ar.Dequeue() if zid != id.Invalid && val != false { t.Errorf("Expected invalid Zid, but got %v", zid) } ar.Enqueue(id.Zid(1), true) ar.Enqueue(id.Zid(2), true) if ar.first != ar.last { t.Errorf("Expected one room, but got more") } ar.Enqueue(id.Zid(3), true) if ar.first == ar.last { t.Errorf("Expected more than one room, but got only one") } count := 0 for ; ; count++ { zid, val := ar.Dequeue() if zid == id.Invalid && val == false { 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), true) ar.Reset() zid, val := ar.Dequeue() if zid != id.Invalid && val != true { t.Errorf("Expected invalid Zid, but got %v/%v", zid, val) } ar.Reload([]id.Zid{id.Zid(2)}, map[id.Zid]bool{id.Zid(3): true, id.Zid(4): false}) ar.Enqueue(id.Zid(5), true) ar.Enqueue(id.Zid(5), false) ar.Enqueue(id.Zid(5), false) ar.Enqueue(id.Zid(5), true) if ar.first == ar.last || ar.first.next == ar.last || ar.first.next.next != ar.last { t.Errorf("Expected 3 rooms") } zid, val = ar.Dequeue() if zid != id.Zid(2) || val != false { t.Errorf("Expected 2/false, but got %v/%v", zid, val) } zid1, val := ar.Dequeue() if val != true { t.Errorf("Expected true, but got %v", val) } zid2, val := ar.Dequeue() if val != true { t.Errorf("Expected true, but got %v", val) } 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) } zid, val = ar.Dequeue() if zid != id.Zid(5) || val != true { t.Errorf("Expected 5/true, but got %v/%v", zid, val) } zid, val = ar.Dequeue() if zid != id.Invalid && val != false { t.Errorf("Expected invalid Zid, but got %v", zid) } ar = newAnterooms(1) ar.Reload(nil, map[id.Zid]bool{id.Zid(6): true}) zid, val = ar.Dequeue() if zid != id.Zid(6) || val != true { t.Errorf("Expected 6/true, but got %v/%v", zid, val) } zid, val = ar.Dequeue() if zid != id.Invalid && val != false { t.Errorf("Expected invalid Zid, but got %v", zid) } ar = newAnterooms(1) ar.Reload([]id.Zid{id.Zid(7)}, nil) zid, val = ar.Dequeue() if zid != id.Zid(7) || val != false { t.Errorf("Expected 7/false, but got %v/%v", zid, val) } zid, val = ar.Dequeue() if zid != id.Invalid && val != false { t.Errorf("Expected invalid Zid, but got %v", zid) } ar = newAnterooms(1) ar.Enqueue(id.Zid(8), true) ar.Reload(nil, nil) zid, val = ar.Dequeue() if zid != id.Invalid && val != false { t.Errorf("Expected invalid Zid, but got %v", zid) } } |
Added index/indexer/indexer.go.
> > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 | //----------------------------------------------------------------------------- // Copyright (c) 2021 Detlef Stern // // This file is part of zettelstore. // // Zettelstore is licensed under the latest version of the EUPL (European Union // Public License). Please see file LICENSE.txt for your rights and obligations // under this license. //----------------------------------------------------------------------------- // Package indexer allows to search for metadata and content. package indexer import ( "context" "sync" "time" "zettelstore.de/z/ast" "zettelstore.de/z/collect" "zettelstore.de/z/domain" "zettelstore.de/z/domain/id" "zettelstore.de/z/domain/meta" "zettelstore.de/z/index" "zettelstore.de/z/index/memstore" "zettelstore.de/z/parser" "zettelstore.de/z/place" ) type indexer struct { store index.Store ar *anterooms ready chan struct{} // Signal a non-empty anteroom to background task done chan struct{} // Stop background task observe bool started bool // Stats data mx sync.RWMutex lastReload time.Time sinceReload uint64 durLastIndex time.Duration } // New creates a new indexer. func New() index.Indexer { return &indexer{ store: memstore.New(), ar: newAnterooms(10), ready: make(chan struct{}, 1), } } func (idx *indexer) observer(ci place.ChangeInfo) { switch ci.Reason { case place.OnReload: idx.ar.Reset() case place.OnUpdate: idx.ar.Enqueue(ci.Zid, true) case place.OnDelete: idx.ar.Enqueue(ci.Zid, false) default: return } select { case idx.ready <- struct{}{}: default: } } func (idx *indexer) Start(p index.Port) { if idx.started { panic("Index already started") } idx.done = make(chan struct{}) if !idx.observe { p.RegisterObserver(idx.observer) idx.observe = true } idx.ar.Reset() // Ensure an initial index run go idx.indexer(p) idx.started = true } func (idx *indexer) Stop() { if !idx.started { panic("Index already stopped") } close(idx.done) idx.started = false } // Enrich reads all properties in the index and updates the metadata. func (idx *indexer) Enrich(ctx context.Context, m *meta.Meta) { if index.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 } idx.store.Enrich(ctx, m) } func (idx *indexer) ReadStats(st *index.IndexerStats) { idx.mx.RLock() st.LastReload = idx.lastReload st.IndexesSinceReload = idx.sinceReload st.DurLastIndex = idx.durLastIndex idx.mx.RUnlock() idx.store.ReadStats(&st.Store) } type indexerPort interface { getMetaPort FetchZids(ctx context.Context) (map[id.Zid]bool, error) GetZettel(ctx context.Context, zid id.Zid) (domain.Zettel, error) } // indexer runs in the background and updates the index data structures. func (idx *indexer) indexer(p indexerPort) { // Something may panic. Ensure a running indexer. defer func() { if err := recover(); err != nil { go idx.indexer(p) } }() timerDuration := 15 * time.Second timer := time.NewTimer(timerDuration) ctx := index.NoEnrichContext(context.Background()) for { start := time.Now() changed := false for { zid, val := idx.ar.Dequeue() if zid.IsValid() { changed = true idx.mx.Lock() idx.sinceReload++ idx.mx.Unlock() if !val { idx.deleteZettel(zid) continue } zettel, err := p.GetZettel(ctx, zid) if err != nil { // TODO: on some errors put the zid into a "try later" set continue } idx.updateZettel(ctx, zettel, p) continue } if val == false { break } zids, err := p.FetchZids(ctx) if err == nil { idx.ar.Reload(nil, zids) idx.mx.Lock() idx.lastReload = time.Now() idx.sinceReload = 0 idx.mx.Unlock() } } if changed { idx.mx.Lock() idx.durLastIndex = time.Now().Sub(start) idx.mx.Unlock() } select { case _, ok := <-idx.ready: if !ok { return } case _, ok := <-timer.C: if !ok { return } timer.Reset(timerDuration) case _, ok := <-idx.done: if !ok { if !timer.Stop() { <-timer.C } return } } } } type getMetaPort interface { GetMeta(ctx context.Context, zid id.Zid) (*meta.Meta, error) } func (idx *indexer) updateZettel(ctx context.Context, zettel domain.Zettel, p getMetaPort) { m := zettel.Meta zi := index.NewZettelIndex(m.Zid) for _, pair := range m.PairsRest(false) { descr := meta.GetDescription(pair.Key) if descr.IsComputed() { continue } switch descr.Type { case meta.TypeID: updateValue(ctx, descr.Inverse, pair.Value, p, zi) case meta.TypeIDSet: for _, val := range meta.ListFromValue(pair.Value) { updateValue(ctx, descr.Inverse, val, p, zi) } } } zn := parser.ParseZettel(zettel, "") refs := collect.References(zn) updateReferences(ctx, refs.Links, p, zi) updateReferences(ctx, refs.Images, p, zi) idx.store.UpdateReferences(ctx, zi) } func updateValue( ctx context.Context, inverse string, value string, p getMetaPort, zi *index.ZettelIndex) { zid, err := id.Parse(value) if err != nil { return } if _, err := p.GetMeta(ctx, zid); err != nil { zi.AddDeadRef(zid) return } if inverse == "" { zi.AddBackRef(zid) return } zi.AddMetaRef(inverse, zid) } func updateReferences( ctx context.Context, refs []*ast.Reference, p getMetaPort, zi *index.ZettelIndex) { zrefs, _, _ := collect.DivideReferences(refs, false) for _, ref := range zrefs { updateReference(ctx, ref.Value, p, zi) } } func updateReference( ctx context.Context, value string, p getMetaPort, zi *index.ZettelIndex) { zid, err := id.Parse(value) if err != nil { return } if _, err := p.GetMeta(ctx, zid); err != nil { zi.AddDeadRef(zid) return } zi.AddBackRef(zid) } func (idx *indexer) deleteZettel(zid id.Zid) { idx.store.DeleteZettel(context.Background(), zid) } |
Added index/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 | //----------------------------------------------------------------------------- // 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" "sync" "zettelstore.de/z/domain/id" "zettelstore.de/z/domain/meta" "zettelstore.de/z/index" ) type metaRefs struct { forward []id.Zid backward []id.Zid } type zettelIndex struct { dead string forward []id.Zid backward []id.Zid meta map[string]metaRefs } func (zi *zettelIndex) isEmpty() bool { if len(zi.forward) > 0 || len(zi.backward) > 0 || zi.dead != "" { return false } return zi.meta == nil || len(zi.meta) == 0 } type memStore struct { mx sync.RWMutex idx map[id.Zid]*zettelIndex // Stats updates uint64 } // New returns a new memory-based index store. func New() index.Store { return &memStore{ idx: make(map[id.Zid]*zettelIndex), } } func (ms *memStore) Enrich(ctx context.Context, m *meta.Meta) { ms.mx.RLock() defer ms.mx.RUnlock() zi, ok := ms.idx[m.Zid] if !ok { return } var updated bool if zi.dead != "" { m.Set(meta.KeyDead, zi.dead) updated = true } back := zi.backward if len(zi.backward) > 0 { m.Set(meta.KeyBackward, refsToString(zi.backward)) updated = true } if len(zi.forward) > 0 { m.Set(meta.KeyForward, refsToString(zi.forward)) 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, refsToString(refs.backward)) back = remRefs(back, refs.backward) updated = true } } } if len(back) > 0 { m.Set(meta.KeyBack, refsToString(back)) updated = true } if updated { ms.updates++ } } func (ms *memStore) UpdateReferences(ctx context.Context, zidx *index.ZettelIndex) { ms.mx.Lock() defer ms.mx.Unlock() zi, ziExist := ms.idx[zidx.Zid] if !ziExist || zi == nil { zi = &zettelIndex{} ziExist = false } // Update dead references if drefs := zidx.GetDeadRefs(); len(drefs) > 0 { zi.dead = refsToString(drefs) } else { zi.dead = "" } // Update forward and backward references brefs := zidx.GetBackRefs() newRefs, remRefs := refsDiff(brefs, zi.forward) zi.forward = brefs for _, ref := range newRefs { bzi := ms.getEntry(ref) bzi.backward = addRef(bzi.backward, zidx.Zid) } for _, ref := range remRefs { bzi := ms.getEntry(ref) bzi.backward = remRef(bzi.backward, zidx.Zid) } // Update metadata references 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) } // Check if zi must be inserted into ms.idx if !ziExist && !zi.isEmpty() { ms.idx[zidx.Zid] = zi } } func (ms *memStore) getEntry(zid id.Zid) *zettelIndex { 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) { ms.mx.Lock() defer ms.mx.Unlock() zi, ok := ms.idx[zid] if !ok { return } 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 len(zi.meta) > 0 { for key, mrefs := range zi.meta { ms.removeInverseMeta(zid, key, mrefs.forward) } } delete(ms.idx, zid) } func (ms *memStore) removeInverseMeta(zid id.Zid, key string, forward []id.Zid) { // Must only be called if ms.mx is write-locked! for _, ref := range forward { if bzi, ok := ms.idx[ref]; ok { if bzi.meta != nil { if bmr, ok := bzi.meta[key]; ok { 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) ReadStats(st *index.StoreStats) { ms.mx.RLock() st.Zettel = len(ms.idx) st.Updates = ms.updates ms.mx.RUnlock() } func (ms *memStore) Write(w io.Writer) { ms.mx.RLock() zids := make([]id.Zid, 0, len(ms.idx)) for id := range ms.idx { zids = append(zids, id) } id.Sort(zids) for _, id := range zids { fmt.Fprintln(w, id) zi := ms.idx[id] fmt.Fprintln(w, "-", zi.dead) writeZidsLn(w, ">", zi.forward) writeZidsLn(w, "<", zi.backward) if zi.meta == nil { fmt.Fprintln(w, "*NIL") } else if len(zi.meta) == 0 { fmt.Fprintln(w, "*(0)") } else { for k, fb := range zi.meta { fmt.Fprintln(w, "*", k) writeZidsLn(w, "]", fb.forward) writeZidsLn(w, "[", fb.backward) } } } ms.mx.RUnlock() } func writeZidsLn(w io.Writer, prefix string, zids []id.Zid) { io.WriteString(w, prefix) for _, zid := range zids { io.WriteString(w, " ") w.Write(zid.Bytes()) } fmt.Fprintln(w) } |
Added index/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 102 103 104 105 106 107 108 109 110 | //----------------------------------------------------------------------------- // 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 ( "bytes" "zettelstore.de/z/domain/id" ) func refsToString(refs []id.Zid) string { var buf bytes.Buffer for i, dref := range refs { if i > 0 { buf.WriteByte(' ') } buf.Write(dref.Bytes()) } return buf.String() } func refsDiff(refsN, refsO []id.Zid) (newRefs, remRefs []id.Zid) { 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.Zid, ref id.Zid) []id.Zid { if len(refs) == 0 { return append(refs, ref) } for i, r := range refs { if r == ref { return refs } if r > ref { return append(refs[:i], append([]id.Zid{ref}, refs[i:]...)...) } } return append(refs, ref) } func remRefs(refs []id.Zid, rem []id.Zid) []id.Zid { if len(refs) == 0 || len(rem) == 0 { return refs } result := make([]id.Zid, 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.Zid, ref id.Zid) []id.Zid { if refs != nil { for i, r := range refs { if r == ref { return append(refs[:i], refs[i+1:]...) } if r > ref { return refs } } } return refs } |
Added index/memstore/refs_test.go.
> > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 | //----------------------------------------------------------------------------- // 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 numsToRefs(nums []uint) []id.Zid { if nums == nil { return nil } refs := make([]id.Zid, 0, len(nums)) for _, n := range nums { refs = append(refs, id.Zid(n)) } return refs } func assertRefs(t *testing.T, i int, got []id.Zid, exp []uint) { 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 TestRefsToString(t *testing.T) { testcases := []struct { in []uint exp string }{ {nil, ""}, {[]uint{}, ""}, {[]uint{1}, "00000000000001"}, {[]uint{1, 2}, "00000000000001 00000000000002"}, } for i, tc := range testcases { got := refsToString(numsToRefs(tc.in)) if got != tc.exp { t.Errorf("%d/%v: expected %q, but got %q", i, tc.in, tc.exp, got) } } } func TestRefsDiff(t *testing.T) { testcases := []struct { in1, in2 []uint exp1, exp2 []uint }{ {nil, nil, nil, nil}, {[]uint{1}, nil, []uint{1}, nil}, {nil, []uint{1}, nil, []uint{1}}, {[]uint{1}, []uint{1}, nil, nil}, {[]uint{1, 2}, []uint{1}, []uint{2}, nil}, {[]uint{1, 2}, []uint{1, 3}, []uint{2}, []uint{3}}, {[]uint{1, 4}, []uint{1, 3}, []uint{4}, []uint{3}}, } for i, tc := range testcases { got1, got2 := refsDiff(numsToRefs(tc.in1), numsToRefs(tc.in2)) assertRefs(t, i, got1, tc.exp1) assertRefs(t, i, got2, tc.exp2) } } func TestAddRef(t *testing.T) { testcases := []struct { ref []uint zid uint exp []uint }{ {nil, 5, []uint{5}}, {[]uint{1}, 5, []uint{1, 5}}, {[]uint{10}, 5, []uint{5, 10}}, {[]uint{5}, 5, []uint{5}}, {[]uint{1, 10}, 5, []uint{1, 5, 10}}, {[]uint{1, 5, 10}, 5, []uint{1, 5, 10}}, } for i, tc := range testcases { got := addRef(numsToRefs(tc.ref), id.Zid(tc.zid)) assertRefs(t, i, got, tc.exp) } } func TestRemRefs(t *testing.T) { testcases := []struct { in1, in2 []uint exp []uint }{ {nil, nil, nil}, {nil, []uint{}, nil}, {[]uint{}, nil, []uint{}}, {[]uint{}, []uint{}, []uint{}}, {[]uint{1}, []uint{5}, []uint{1}}, {[]uint{10}, []uint{5}, []uint{10}}, {[]uint{1, 5}, []uint{5}, []uint{1}}, {[]uint{5, 10}, []uint{5}, []uint{10}}, {[]uint{1, 10}, []uint{5}, []uint{1, 10}}, {[]uint{1}, []uint{2, 5}, []uint{1}}, {[]uint{10}, []uint{2, 5}, []uint{10}}, {[]uint{1, 5}, []uint{2, 5}, []uint{1}}, {[]uint{5, 10}, []uint{2, 5}, []uint{10}}, {[]uint{1, 2, 5}, []uint{2, 5}, []uint{1}}, {[]uint{2, 5, 10}, []uint{2, 5}, []uint{10}}, {[]uint{1, 10}, []uint{2, 5}, []uint{1, 10}}, {[]uint{1}, []uint{5, 9}, []uint{1}}, {[]uint{10}, []uint{5, 9}, []uint{10}}, {[]uint{1, 5}, []uint{5, 9}, []uint{1}}, {[]uint{5, 10}, []uint{5, 9}, []uint{10}}, {[]uint{1, 5, 9}, []uint{5, 9}, []uint{1}}, {[]uint{5, 9, 10}, []uint{5, 9}, []uint{10}}, {[]uint{1, 10}, []uint{5, 9}, []uint{1, 10}}, } for i, tc := range testcases { got := remRefs(numsToRefs(tc.in1), numsToRefs(tc.in2)) assertRefs(t, i, got, tc.exp) } } func TestRemRef(t *testing.T) { testcases := []struct { ref []uint zid uint exp []uint }{ {nil, 5, nil}, {[]uint{}, 5, []uint{}}, {[]uint{1}, 5, []uint{1}}, {[]uint{10}, 5, []uint{10}}, {[]uint{1, 5}, 5, []uint{1}}, {[]uint{5, 10}, 5, []uint{10}}, {[]uint{1, 5, 10}, 5, []uint{1, 10}}, } for i, tc := range testcases { got := remRef(numsToRefs(tc.ref), id.Zid(tc.zid)) assertRefs(t, i, got, tc.exp) } } |
Added index/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 index allows to search for metadata and content. package index 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 map[id.Zid]bool // set of back references metarefs map[string]map[id.Zid]bool // references to inverse keys deadrefs map[id.Zid]bool // set of dead references } // NewZettelIndex creates a new zettel index. func NewZettelIndex(zid id.Zid) *ZettelIndex { return &ZettelIndex{ Zid: zid, backrefs: make(map[id.Zid]bool), metarefs: make(map[string]map[id.Zid]bool), deadrefs: make(map[id.Zid]bool), } } // 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] = map[id.Zid]bool{zid: true} } // AddDeadRef adds a dead reference to a zettel. func (zi *ZettelIndex) AddDeadRef(zid id.Zid) { zi.deadrefs[zid] = true } // GetDeadRefs returns all dead references as a sorted list. func (zi *ZettelIndex) GetDeadRefs() []id.Zid { return sortedZids(zi.deadrefs) } // GetBackRefs returns all back references as a sorted list. func (zi *ZettelIndex) GetBackRefs() []id.Zid { return sortedZids(zi.backrefs) } // GetMetaRefs returns all meta references as a map of strings to a sorted list of references func (zi *ZettelIndex) GetMetaRefs() map[string][]id.Zid { if len(zi.metarefs) == 0 { return nil } result := make(map[string][]id.Zid, len(zi.metarefs)) for key, refs := range zi.metarefs { result[key] = sortedZids(refs) } return result } func sortedZids(refmap map[id.Zid]bool) []id.Zid { if l := len(refmap); l > 0 { result := make([]id.Zid, 0, l) for zid := range refmap { result = append(result, zid) } id.Sort(result) return result } return nil } |
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 | //----------------------------------------------------------------------------- // 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 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 } // 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() } return } // 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 == '#' { code := 0 inp.Next() if inp.Ch == 'x' || inp.Ch == 'X' { // Base 16 code inp.Next() if inp.Ch == ';' { return "", false } 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() } } // Base 10 code if inp.Ch == ';' { return "", false } 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() } } 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 Detlef Stern // // This file is part of zettelstore. // // Zettelstore is licensed under 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) } } } |
Deleted kernel/impl/auth.go.
|
| < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < |
Deleted kernel/impl/box.go.
|
| < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < |
Deleted kernel/impl/cfg.go.
|
| < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < |
Deleted kernel/impl/cmd.go.
|
| < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < |
Deleted kernel/impl/config.go.
|
| < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < |
Deleted kernel/impl/core.go.
|
| < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < |
Deleted kernel/impl/impl.go.
|
| < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < |
Deleted kernel/impl/log.go.
|
| < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < |
Deleted kernel/impl/server.go.
|
| < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < |
Deleted kernel/impl/web.go.
|
| < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < |
Deleted kernel/kernel.go.
|
| < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < |
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{} } |
Deleted parser/cleaner/cleaner.go.
|
| < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < |
Added parser/cleanup.go.
> > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 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) 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 parser provides a generic interface to a range of different parsers. package parser import ( "strconv" "strings" "zettelstore.de/z/ast" "zettelstore.de/z/encoder" "zettelstore.de/z/strfun" // Ensure that the text encoder is available _ "zettelstore.de/z/encoder/textenc" ) func cleanupBlockSlice(bs ast.BlockSlice) { cv := &cleanupVisitor{ textEnc: encoder.Create("text"), 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 len(mn.Text) == 0 { 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 Detlef Stern // // This file is part of zettelstore. // // Zettelstore is licensed under 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") 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 len(text) == 0 { 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 | result = append(result, &ast.SpaceNode{Lexeme: text[lastPos:]}) default: panic(fmt.Sprintf("Unexpected state %v", state)) } return result } // cleanText removes backslashes from TextNodes and expands entities func cleanText(text string, cleanBS bool) string { if len(text) == 0 { return "" } lastPos := 0 var sb strings.Builder for pos, ch := range text { if pos < lastPos { continue } switch ch { case '\\': if cleanBS && pos < len(text)-1 { switch b := text[pos+1]; b { case '!', '"', '#', '$', '%', '&', '\'', '(', ')', '*', '+', ',', '-', '.', '/', ':', ';', '<', '=', '>', '?', '@', '[', '\\', ']', '^', '_', '`', '{', '|', '}', '~': sb.WriteString(text[lastPos:pos]) sb.WriteByte(b) lastPos = pos + 2 default: } } case '&': inp := input.NewInput(text[pos:]) s, ok := inp.ScanEntity() if ok { sb.WriteString(text[lastPos:pos]) sb.WriteString(s) lastPos = pos + inp.Pos } default: } } 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 len(text) == 0 { return "" } lastPos := 0 var sb strings.Builder for pos, ch := range text { switch ch { case '\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), 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) //return ins } return ast.InlineSlice{ &ast.TextNode{ Text: sb.String(), }, } } 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)}}, 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 | //----------------------------------------------------------------------------- // 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/runtime" "zettelstore.de/z/domain" "zettelstore.de/z/domain/meta" "zettelstore.de/z/input" ) // 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) 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) } // ParseTitle parses the title of a zettel, always as Zettelmarkup func ParseTitle(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) *ast.ZettelNode { m := zettel.Meta inhMeta := runtime.AddDefaultValues(zettel.Meta) if len(syntax) == 0 { syntax, _ = inhMeta.Get(meta.KeySyntax) } title, _ := inhMeta.Get(meta.KeyTitle) parseMeta := inhMeta if syntax == meta.ValueSyntaxNone { parseMeta = m } return &ast.ZettelNode{ Zettel: zettel, Zid: m.Zid, InhMeta: inhMeta, Title: ParseTitle(title), 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 117 | //----------------------------------------------------------------------------- // 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 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 switch inp.Ch { case 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 46 47 48 49 50 51 52 53 54 55 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 Detlef Stern // // This file is part of zettelstore. // // Zettelstore is licensed under 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 { cp.nestingLevel++ defer func() { cp.nestingLevel-- }() var bn ast.BlockNode success := false switch inp.Ch { case input.EOS: return nil, false case '\n', '\r': inp.EatEOL() for _, l := range cp.lists { if lits := len(l.Items); lits > 0 { l.Items[lits-1] = append(l.Items[lits-1], &nullItemNode{}) } } if cp.descrl != nil { defPos := len(cp.descrl.Descriptions) - 1 if ldds := len(cp.descrl.Descriptions[defPos].Descriptions); ldds > 0 { cp.descrl.Descriptions[defPos].Descriptions[ldds-1] = append( cp.descrl.Descriptions[defPos].Descriptions[ldds-1], &nullDescriptionNode{}) } } 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 | | | < < < < < | < < < < < < < < < < < < < < < < < < < < < < < < < | | | | | | 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 | 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 } // parseColon determines which element should be parsed. func (cp *zmkP) parseColon() (ast.BlockNode, bool) { inp := cp.inp if inp.PeekN(1) == ':' { cp.clearStacked() return cp.parseRegion() } return cp.parseDefDescr() } // parsePara parses paragraphed inline material. func (cp *zmkP) parsePara() *ast.ParaNode { 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 } | | | < < | | < < < < | < < < | | | | | < < < < < > > > > > > > > > | > | > > > < < < < < < < < < < < < < < < < | | > > > | < | | > | < | | > | | | < < < < < < < < | < < < < < < < < < < < | < < < | | > > > > > > > > > > > | | | | | | | < < | < | | > > < > | > | > | > | | < < < < < < < | < | | | | | | | | | < < < | | | | | | | | | | | | | | | | | | | | | | | | | | < | | | | | | | | | | | | | | | > > > | | | | | | < < < < | | | < < < < < < < < < < < < < < < < < < < < < < | < < | | < < < < | < | < < | | < < < > | < < < | < < < < < < < < < < < | 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 | 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 { cp.clearStacked() // remove any lists defined in the region for inp.Ch == ' ' { inp.Next() } for { switch inp.Ch { case input.EOS, '\n', '\r': return rn, true } in := cp.parseInline() if in == nil { return rn, true } rn.Inlines = append(rn.Inlines, in) } } inp.SetPos(posL) case input.EOS: return nil, false } bn, cont := cp.parseBlock(lastPara) if bn != nil { rn.Blocks = append(rn.Blocks, bn) } if !cont { lastPara, _ = bn.(*ast.ParaNode) } } } // 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() for inp.Ch == ' ' { inp.Next() } hn = &ast.HeadingNode{Level: lvl - 1} for { switch inp.Ch { case input.EOS, '\n', '\r': 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, } // parseList parses a list. func (cp *zmkP) parseNestedList() (res ast.BlockNode, success bool) { inp := cp.inp codes := []ast.NestedListCode{} loopInit: 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': break loopInit default: return nil, false } } for inp.Ch == ' ' { inp.Next() } if codes[len(codes)-1] != ast.NestedListQuote { switch inp.Ch { case input.EOS, '\n', '\r': return nil, false } } if len(codes) < len(cp.lists) { cp.lists = cp.lists[:len(codes)] } var ln *ast.NestedListNode newLnCount := 0 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) } } ln.Items = append(ln.Items, ast.ItemSlice{cp.parseLinePara()}) listDepth := len(cp.lists) for i := 0; i < newLnCount; i++ { childPos := listDepth - i - 1 parentPos := childPos - 1 if parentPos < 0 { return cp.lists[0], true } if prevItems := cp.lists[parentPos].Items; len(prevItems) > 0 { lastItem := len(prevItems) - 1 prevItems[lastItem] = append(prevItems[lastItem], cp.lists[childPos]) } else { cp.lists[parentPos].Items = []ast.ItemSlice{ ast.ItemSlice{cp.lists[childPos]}, } } } return nil, true } // parseDefTerm parses a term of a definition list. func (cp *zmkP) parseDefTerm() (res ast.BlockNode, success bool) { inp := cp.inp inp.Next() if inp.Ch != ' ' { return nil, false } inp.Next() for inp.Ch == ' ' { inp.Next() } descrl := cp.descrl if descrl == nil { descrl = &ast.DescriptionListNode{} cp.descrl = descrl } descrl.Descriptions = append(descrl.Descriptions, ast.Description{}) defPos := len(descrl.Descriptions) - 1 if defPos == 0 { res = descrl } for { in := cp.parseInline() if in == nil { if 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 } } } // parseDefDescr parses a description of a definition list. func (cp *zmkP) parseDefDescr() (res ast.BlockNode, success bool) { inp := cp.inp inp.Next() if inp.Ch != ' ' { return nil, false } inp.Next() for inp.Ch == ' ' { inp.Next() } 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 { // Identation for a list? if len(cp.lists) < cnt { cnt = len(cp.lists) } cp.lists = cp.lists[:cnt] if cnt == 0 { return nil, 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 nil, true } if cp.descrl != nil { // Indentation for definition list defPos := len(cp.descrl.Descriptions) - 1 if cnt < 1 || defPos < 0 { return nil, false } if len(cp.descrl.Descriptions[defPos].Descriptions) == 0 { // Continuation of a definition term for { in := cp.parseInline() if in == nil { return nil, true } cp.descrl.Descriptions[defPos].Term = append(cp.descrl.Descriptions[defPos].Term, in) if _, ok := in.(*ast.BreakNode); ok { return nil, true } } } else { // Continuation of a definition description pn := cp.parseLinePara() if pn == nil { return nil, 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 nil, true } } return nil, false } // 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 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 { switch inp.Ch { case input.EOS, '\n', '\r': if len(slice) == 0 { return nil } fallthrough case '|': 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 Detlef Stern // // This file is part of zettelstore. // // Zettelstore is licensed under 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 { |
︙ | ︙ | |||
63 64 65 66 67 68 69 | case '^': in, success = cp.parseFootnote() case '!': in, success = cp.parseMark() } case '{': inp.Next() | | > | > > | | < < < | | | > > > > > > | < | | | | | > > > > > > | | > < < < < | < | < < > > < < | > > > > > > > > > | | | | | | > | | | | | > | | < | | | | | > | > > > | < | < > | < > | > > | > > | < < < | < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < | 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 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 | case '^': in, success = cp.parseFootnote() case '!': in, success = cp.parseMark() } case '{': inp.Next() switch inp.Ch { case '{': 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() return &ast.BreakNode{Hard: true} default: return cp.parseBackslashRest() } } func (cp *zmkP) parseBackslashRest() *ast.TextNode { inp := cp.inp switch inp.Ch { case input.EOS, '\n', '\r': return &ast.TextNode{Text: "\\"} case ' ': 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() for inp.Ch == ' ' { inp.Next() } hasSpace := false pos := inp.Pos loop: for { switch inp.Ch { case input.EOS: return "", nil, false case '\n', '\r', ' ': hasSpace = true case '|', closeCh: break loop } inp.Next() } if inp.Ch == '|' { // First part must be inline text if pos == inp.Pos { // [[| or {{| return "", nil, false } inp.SetPos(pos) loop1: for { switch inp.Ch { case input.EOS, '|': break loop1 } in := cp.parseInline() ins = append(ins, in) } inp.Next() pos = inp.Pos } else if hasSpace { return "", nil, false } inp.SetPos(pos) for inp.Ch == ' ' { inp.Next() pos = inp.Pos } loop2: for { switch inp.Ch { case input.EOS, '\n', '\r', ' ': return "", nil, false case closeCh: break loop2 } inp.Next() } ref = inp.Src[pos:inp.Pos] inp.Next() if inp.Ch != closeCh { return "", nil, false } inp.Next() return ref, ins, true } func (cp *zmkP) parseCite() (*ast.CiteNode, bool) { inp := cp.inp inp.Next() switch inp.Ch { case ' ', ',', '|', ']', '\n', '\r': |
︙ | ︙ | |||
309 310 311 312 313 314 315 | case ' ', ',', '|': inp.Next() } ins, ok := cp.parseLinkLikeRest() if !ok { return nil, false } | | | | < < > > > > | > > > | > < < < | | | < < | < < | | < < | < < < | | | > > > | > > > > | > | | | > > | | < < < < | | | | > | | | > | | | | > < | | > > | > | < | < | > | | > > | | < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < | 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 | 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) { inp := cp.inp for inp.Ch == ' ' { inp.Next() } var ins ast.InlineSlice for inp.Ch != ']' { in := cp.parseInline() if in == nil { return nil, false } ins = append(ins, in) if _, ok := in.(*ast.BreakNode); ok { ch := cp.inp.Ch switch ch { case input.EOS, '\n', '\r': 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() } for inp.Ch == ' ' { inp.Next() } pos := inp.Pos for { switch inp.Ch { case input.EOS, '\n', '\r': 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 } fn := &ast.FormatNode{Code: code} inp.Next() 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 { switch inp.Ch { case input.EOS, '\n', '\r': 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 36 37 38 39 40 41 | //----------------------------------------------------------------------------- // 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 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 block. type nullItemNode struct { ast.ItemNode } func (nn *nullItemNode) blockNode() {} func (nn *nullItemNode) 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 } func (nn *nullDescriptionNode) blockNode() {} func (nn *nullDescriptionNode) 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 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 Detlef Stern // // This file is part of zettelstore. // // Zettelstore is licensed under 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 post-processes a verbatim block. 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 post-processes a horizontal rule. 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, 0, width) for i := 0; i < width; i++ { tn.Align = append(tn.Align, ast.AlignDefault) } if len(tn.Rows) > 0 && isHeaderRow(tn.Rows[0]) { tn.Header = tn.Rows[0] tn.Rows = tn.Rows[1:] for pos, cell := range tn.Header { if inlines := cell.Inlines; len(inlines) > 0 { if textNode, ok := inlines[0].(*ast.TextNode); ok { if strings.HasPrefix(textNode.Text, "=") { textNode.Text = textNode.Text[1:] } } 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] } } } } } } 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]) } } for i, row := range tn.Rows { tn.Rows[i] = appendCells(row, width, tn.Align) row = tn.Rows[i] for i, cell := range row { pp.processCell(cell, tn.Align[i]) } } } func tableWidth(tn *ast.TableNode) int { width := 0 for _, row := range tn.Rows { if width < len(row) { width = len(row) } } return width } func appendCells(row ast.TableRow, width int, colAlign []ast.Alignment) ast.TableRow { for len(row) < width { row = append(row, &ast.TableCell{Align: colAlign[len(row)]}) } 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) { | | > > > | | | > > | > > | > > | > > | > > | > > > > | > > > > > | > > > > | > > > | > > | | > > > > | | | > > > > > | > > > > | > > | > > > | | | | | | | | < < < | | 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 | 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 { | < < < | | 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 | } 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] } | > > | < < < | < | | | > | > | | | > > | < | | < < < < < | < | | | < < < < < < < | < | | | | | | | < < < < < < < < < < < < < < < < < | | | | | | | | < < | < | | | | | | | | | | | | > | | | > > > > > > > > > > > | > > > | < > > > > > > > > > > > > > | 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 | } 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 := 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++ } for pos := toPos; pos < maxPos; pos++ { ins[pos] = nil // Allow excess nodes to be garbage collected. } if !again { return toPos } maxPos = 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 { switch n := in.(type) { case *ast.TextNode: 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 Detlef Stern // // This file is part of zettelstore. // // Zettelstore is licensed under 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 { switch inp.Ch { case input.EOS, '\n', '\r': 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 == '"' { 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() } } } 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 updateAttrs(attrs map[string]string, key string, 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(!sameLine) } 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{} loop: for { cp.skipSpace(!sameLine) switch inp.Ch { case input.EOS: return nil, false case '}': break loop case '.': inp.Next() posC := inp.Pos for isNameRune(inp.Ch) { inp.Next() } if posC == inp.Pos { return nil, false } updateAttrs(attrs, "class", inp.Src[posC:inp.Pos]) case '=': delete(attrs, "") if !cp.parseAttributeValue("", attrs, sameLine) { return nil, false } default: if !cp.parseNormalAttribute(attrs, sameLine) { return nil, false } } switch inp.Ch { case '}': break loop case '\n', '\r': if sameLine { return nil, false } case ' ', ',': inp.Next() default: return nil, false } } inp.Next() return &ast.Attributes{Attrs: attrs}, true } func (cp *zmkP) skipSpace(eolIsSpace bool) { inp := cp.inp if eolIsSpace { for { switch inp.Ch { case ' ': inp.Next() case '\n', '\r': inp.EatEOL() default: return } } } for inp.Ch == ' ' { inp.Next() } } func isNameRune(ch rune) bool { return unicode.IsLetter(ch) || unicode.IsDigit(ch) || ch == '-' || ch == '_' } |
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 | //----------------------------------------------------------------------------- // 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 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" ) 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() | | | | < < > > > > > > > | < < < < | | | | | | < < < < < < < < < < < < < < < < < < < < < < < | < | | | | | | | | | | > | > > | | | | | | < < | < | < | < | < | < | | < < | < < | < < < < | | | < | < < < < < < < < < < | < < | | < < < | < < < < < < < | | | | < < | | | < < < < | < < < < < < < < < | < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < | | | | | | | | | | < < < < < < < < < < | 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 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 | 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]])"}, }) } 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)})"}, | | < < < < < < < < < < < | 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 | // 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) { | < < < < < < < < < < < < < < < < < < < < | | < | 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 | {"; 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)))"}, }) } 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])"}, | | < | | < | < < < < | < < < < < < < < < < < < < < < < < < < < < < < < | < < < < < < < < < < < < < | < < < < < < < < < < | < < < < < < < < < < < | < < > | < < < < < < < < | < < < < < < < < < < < < < < | < < < < | < < | < < < < < < | < < | | < < | < < < | < < | < < < < < < | < < < < < < < | < < < < < | | < < < < < < | < < < | < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < | > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > | > > > > > > > > > > > > > > > > > > > > > > > > | | | | | | | | | | | > | > > > > > > > > > > > > > > > > > > | > | | | > | | | > > > > | | > > | > > > > > > > > > > > > > | > > > | > > > | > > > > > > > > > > | | > | < > > > > > | > > > > | > > > > > > > | | > > > | > > | > > > > > > | > > | > > | > | | | > | > > > | | | | > > > | > > > > > > > | > | | > > > > > | > | > > > | > > > | > > > > > > > > | | > | > | 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 | {"::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.IndexRune(v, ' ') >= 0 { tv.b.WriteByte('"') tv.b.WriteString(v) tv.b.WriteByte('"') } else { tv.b.WriteString(v) } } } tv.b.WriteByte(']') } |
Added place/constplace/constdata.go.
> > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 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 | //----------------------------------------------------------------------------- // 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 stores zettel inside the executable. package constplace import ( "zettelstore.de/z/domain" "zettelstore.de/z/domain/id" "zettelstore.de/z/domain/meta" ) const ( syntaxTemplate = "mustache" ) var constZettelMap = map[id.Zid]constZettel{ id.ConfigurationZid: constZettel{ constHeader{ meta.KeyTitle: "Zettelstore Runtime Configuration", meta.KeyRole: meta.ValueRoleConfiguration, meta.KeyVisibility: meta.ValueVisibilityOwner, meta.KeySyntax: meta.ValueSyntaxNone, }, "", }, id.BaseTemplateZid: constZettel{ constHeader{ meta.KeyTitle: "Zettelstore Base HTML Template", meta.KeyRole: meta.ValueRoleConfiguration, meta.KeyVisibility: meta.ValueVisibilityExpert, meta.KeySyntax: syntaxTemplate, }, domain.NewContent( `<!DOCTYPE html> <html{{#Lang}} lang="{{Lang}}"{{/Lang}}> <head> <meta charset="utf-8"> <meta name="referrer" content="same-origin"> <meta name="viewport" content="width=device-width, initial-scale=1.0"> <meta name="generator" content="Zettelstore"> {{{MetaHeader}}} <link rel="stylesheet" href="{{{StylesheetURL}}}"> {{{Header}}} <title>{{Title}}</title> </head> <body> <nav class="zs-menu"> <a href="{{{HomeURL}}}">Home</a> <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}} {{#WithAuth}} <div class="zs-dropdown"> <button>User</button> <nav class="zs-dropdown-content"> {{#UserIsValid}} <a href="{{{UserZettelURL}}}">{{UserIdent}}</a> <a href="{{{UserLogoutURL}}}">Logout</a> {{/UserIsValid}} {{^UserIsValid}} <a href="{{{LoginURL}}}">Login</a> {{/UserIsValid}} {{#CanReload}} <a href="{{{ReloadURL}}}">Reload</a> {{/CanReload}} </nav> </div> {{/WithAuth}} {{{Menu}}} <form action="{{{SearchURL}}}"> <input type="text" placeholder="Search.." name="s"> </form> </nav> <main class="content"> {{{Content}}} </main> {{#FooterHTML}} <footer> {{{FooterHTML}}} </footer> {{/FooterHTML}} </body> </html>`, ), }, id.LoginTemplateZid: constZettel{ constHeader{ meta.KeyTitle: "Zettelstore Login Form HTML Template", meta.KeyRole: meta.ValueRoleConfiguration, meta.KeyVisibility: meta.ValueVisibilityExpert, meta.KeySyntax: syntaxTemplate, }, domain.NewContent( `<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>`, )}, id.ListTemplateZid: constZettel{ constHeader{ meta.KeyTitle: "Zettelstore List Meta HTML Template", meta.KeyRole: meta.ValueRoleConfiguration, meta.KeyVisibility: meta.ValueVisibilityExpert, meta.KeySyntax: syntaxTemplate, }, domain.NewContent( `<h1>{{Title}}</h1> <ul> {{#Metas}}<li><a href="{{{URL}}}">{{{Title}}}</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}}`)}, id.DetailTemplateZid: constZettel{ constHeader{ meta.KeyTitle: "Zettelstore Detail HTML Template", meta.KeyRole: meta.ValueRoleConfiguration, meta.KeyVisibility: meta.ValueVisibilityExpert, meta.KeySyntax: syntaxTemplate, }, domain.NewContent( `<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}} {{#CanNew}}· <a href="{{{NewURL}}}">New</a>{{/CanNew}} {{#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>Links to this zettel</summary> <ul> {{#BackLinks}} <li><a href="{{{URL}}}">{{Text}}</a></li> {{/BackLinks}} </ul> </details> {{/HasBackLinks}} </article>`)}, id.InfoTemplateZid: constZettel{ constHeader{ meta.KeyTitle: "Zettelstore Info HTML Template", meta.KeyRole: meta.ValueRoleConfiguration, meta.KeyVisibility: meta.ValueVisibilityExpert, meta.KeySyntax: syntaxTemplate, }, domain.NewContent( `<article> <header> <h1>Information for Zettel {{Zid}}</h1> <a href="{{{WebURL}}}">Web</a> {{#CanWrite}} · <a href="{{{EditURL}}}">Edit</a>{{/CanWrite}} {{#CanFolge}} · <a href="{{{FolgeURL}}}">Folge</a>{{/CanFolge}} {{#CanCopy}} · <a href="{{{CopyURL}}}">Copy</a>{{/CanCopy}} {{#CanNew}} · <a href="{{{NewURL}}}">New</a>{{/CanNew}} {{#CanRename}}· <a href="{{{RenameURL}}}">Rename</a>{{/CanRename}} {{#CanDelete}}· <a href="{{{DeleteURL}}}">Delete</a>{{/CanDelete}} </header> <h2>Interpreted Meta Data</h2> <table>{{#MetaData}}<tr><td>{{Key}}</td><td>{{{Value}}}</td></tr>{{/MetaData}}</table> {{#HasLinks}} <h2>References</h2> {{#HasLocLinks}} <h3>Local</h3> <ul> {{#LocLinks}} <li><a href="{{{.}}}">{{.}}</a></li> {{/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> </article>`), }, id.FormTemplateZid: constZettel{ constHeader{ meta.KeyTitle: "Zettelstore Form HTML Template", meta.KeyRole: meta.ValueRoleConfiguration, meta.KeyVisibility: meta.ValueVisibilityExpert, meta.KeySyntax: syntaxTemplate, }, `<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>`, }, id.RenameTemplateZid: constZettel{ constHeader{ meta.KeyTitle: "Zettelstore Rename Form HTML Template", meta.KeyRole: meta.ValueRoleConfiguration, meta.KeyVisibility: meta.ValueVisibilityExpert, meta.KeySyntax: syntaxTemplate, }, `<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>`, }, id.DeleteTemplateZid: constZettel{ constHeader{ meta.KeyTitle: "Zettelstore Delete HTML Template", meta.KeyRole: meta.ValueRoleConfiguration, meta.KeyVisibility: meta.ValueVisibilityExpert, meta.KeySyntax: syntaxTemplate, }, `<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}}`, }, id.RolesTemplateZid: constZettel{ constHeader{ meta.KeyTitle: "Zettelstore List Roles HTML Template", meta.KeyRole: meta.ValueRoleConfiguration, meta.KeyVisibility: meta.ValueVisibilityExpert, meta.KeySyntax: syntaxTemplate, }, `<h1>Currently used roles</h1> <ul> {{#Roles}}<li><a href="{{{URL}}}">{{Text}}</a></li> {{/Roles}}</ul>`, }, id.TagsTemplateZid: constZettel{ constHeader{ meta.KeyTitle: "Zettelstore List Tags HTML Template", meta.KeyRole: meta.ValueRoleConfiguration, meta.KeyVisibility: meta.ValueVisibilityExpert, meta.KeySyntax: syntaxTemplate, }, `<h1>Currently used tags</h1> <div class="zs-meta"> <a href="{{{#ListTagsURL}}}">All</a>{{#MinCounts}}, <a href="{{{URL}}}">{{Count}}</a>{{/MinCounts}} </div> {{#Tags}} <a href="{{{URL}}}" style="font-size:{{Size}}%">{{Name}}</a><sup>{{Count}}</sup> {{/Tags}}`, }, id.BaseCSSZid: constZettel{ constHeader{ meta.KeyTitle: "Zettelstore Base CSS", meta.KeyRole: meta.ValueRoleConfiguration, meta.KeyVisibility: meta.ValueVisibilityPublic, meta.KeySyntax: "css", }, `/* Default CSS */ *,*::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: inline-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:#888; margin-bottom:1rem; } .zs-meta a { color:#888; } 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; } } `, }, id.TemplateNewZettelZid: constZettel{ constHeader{ meta.KeyTitle: "New Zettel", meta.KeyRole: meta.ValueRoleNewTemplate, meta.KeyNewRole: meta.ValueRoleZettel, meta.KeySyntax: meta.ValueSyntaxZmk, }, "", }, id.TemplateNewUserZid: constZettel{ constHeader{ meta.KeyTitle: "New User", meta.KeyRole: meta.ValueRoleNewTemplate, meta.KeyNewRole: meta.ValueRoleUser, meta.KeyCredential: "", meta.KeyUserID: "", meta.KeyUserRole: meta.ValueUserRoleReader, meta.KeySyntax: meta.ValueSyntaxNone, }, "", }, } |
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 | //----------------------------------------------------------------------------- // 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" "net/url" "zettelstore.de/z/domain" "zettelstore.de/z/domain/id" "zettelstore.de/z/domain/meta" "zettelstore.de/z/index" "zettelstore.de/z/place" "zettelstore.de/z/place/manager" ) func init() { manager.Register( " const", func(u *url.URL, cdata *manager.ConnectData) (place.Place, error) { return &constPlace{zettel: constZettelMap, filter: cdata.Filter}, 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 filter index.MetaFilter } 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) (map[id.Zid]bool, error) { result := make(map[id.Zid]bool, len(cp.zettel)) for zid := range cp.zettel { result[zid] = true } return result, nil } func (cp *constPlace) SelectMeta( ctx context.Context, f *place.Filter, s *place.Sorter) (res []*meta.Meta, err error) { hasMatch := place.CreateFilterFunc(f) for zid, zettel := range cp.zettel { m := makeMeta(zid, zettel.header) cp.filter.Enrich(ctx, m) if hasMatch(m) { res = append(res, m) } } return place.ApplySorter(res, s), 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) Reload(ctx context.Context) error { return nil } func (cp *constPlace) ReadStats(st *place.Stats) { st.ReadOnly = true st.Zettel = len(cp.zettel) } |
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 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 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 directory manages the directory part of a dirstore. package directory import ( "time" "zettelstore.de/z/domain/id" "zettelstore.de/z/place" ) // Service specifies a directory scan service. type Service struct { dirPath string rescanTime time.Duration done chan struct{} cmds chan dirCmd infos chan<- place.ChangeInfo } // NewService creates a new directory service. func NewService(directoryPath string, rescanTime time.Duration, chci chan<- place.ChangeInfo) *Service { srv := &Service{ dirPath: directoryPath, rescanTime: rescanTime, cmds: make(chan dirCmd), infos: chci, } return srv } // Start makes the directory service operational. func (srv *Service) Start() { 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 } // Stop stops the directory service. func (srv *Service) Stop() { close(srv.done) srv.done = nil } func (srv *Service) notifyChange(reason place.ChangeReason, zid id.Zid) { if chci := srv.infos; chci != nil { chci <- place.ChangeInfo{Reason: reason, Zid: zid} } } // NumEntries returns the number of managed zettel. func (srv *Service) NumEntries() int { resChan := make(chan resNumEntries) srv.cmds <- &cmdNumEntries{resChan} return <-resChan } // GetEntries returns an unsorted list of all current directory entries. func (srv *Service) GetEntries() []Entry { resChan := make(chan resGetEntries) srv.cmds <- &cmdGetEntries{resChan} return <-resChan } // GetEntry returns the entry with the specified zettel id. If there is no such // zettel id, an empty entry is returned. func (srv *Service) GetEntry(zid id.Zid) Entry { resChan := make(chan resGetEntry) srv.cmds <- &cmdGetEntry{zid, resChan} return <-resChan } // GetNew returns an entry with a new zettel id. func (srv *Service) GetNew() Entry { resChan := make(chan resNewEntry) srv.cmds <- &cmdNewEntry{resChan} return <-resChan } // UpdateEntry notifies the directory of an updated entry. func (srv *Service) UpdateEntry(entry *Entry) { resChan := make(chan struct{}) srv.cmds <- &cmdUpdateEntry{entry, resChan} <-resChan } // RenameEntry notifies the directory of an renamed entry. func (srv *Service) RenameEntry(curEntry, newEntry *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 *Service) DeleteEntry(zid id.Zid) { resChan := make(chan struct{}) srv.cmds <- &cmdDeleteEntry{zid, resChan} <-resChan } |
Added place/dirplace/directory/entry.go.
> > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 | //----------------------------------------------------------------------------- // 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 directory manages the directory part of a dirstore. package directory import ( "strings" "zettelstore.de/z/domain/id" "zettelstore.de/z/domain/meta" ) // MetaSpec defines all possibilities where meta data can be stored. type MetaSpec int // Constants for MetaSpec const ( MetaSpecUnknown 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.Zid.IsValid() } var alternativeSyntax = map[string]string{ "htm": "html", } func (e *Entry) calculateSyntax() string { ext := strings.ToLower(e.ContentExt) if syntax, ok := alternativeSyntax[ext]; ok { return syntax } return ext } // CalcDefaultMeta returns metadata with default values for the given entry. func (e *Entry) CalcDefaultMeta() *meta.Meta { m := meta.New(e.Zid) m.Set(meta.KeyTitle, e.Zid.String()) m.Set(meta.KeySyntax, e.calculateSyntax()) return m } |
Added place/dirplace/directory/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 | //----------------------------------------------------------------------------- // 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 part of a directory place. package directory import ( "log" "time" "zettelstore.de/z/domain/id" "zettelstore.de/z/place" ) // 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) *Entry { de := new(Entry) de.Zid = ev.zid updateEntry(de, ev) return de } func updateEntry(de *Entry, ev *fileEvent) { if ev.ext == "meta" { de.MetaSpec = MetaSpecFile de.MetaPath = ev.path return } if len(de.ContentExt) != 0 && de.ContentExt != ev.ext { de.Duplicates = true return } if de.MetaSpec != MetaSpecFile { if ev.ext == "zettel" { de.MetaSpec = MetaSpecHeader } else { de.MetaSpec = MetaSpecNone } } de.ContentPath = ev.path de.ContentExt = ev.ext } type dirMap map[id.Zid]*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 == MetaSpecFile { entry.MetaSpec = MetaSpecNone return } } } delete(dm, ev.zid) } // directoryService is the main service. func (srv *Service) 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: if newMap != nil { dirMapUpdate(newMap, ev) } else { dirMapUpdate(curMap, ev) srv.notifyChange(place.OnUpdate, ev.zid) } case fileStatusDelete: if newMap != nil { deleteFromMap(newMap, ev) } else { deleteFromMap(curMap, ev) srv.notifyChange(place.OnDelete, ev.zid) } } case cmd, ok := <-srv.cmds: if ok { cmd.run(curMap) } } } } 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 []Entry func (cmd *cmdGetEntries) run(m dirMap) { res := make([]Entry, 0, len(m)) for _, de := range m { res = append(res, *de) } cmd.result <- res } type cmdGetEntry struct { zid id.Zid result chan<- resGetEntry } type resGetEntry = Entry func (cmd *cmdGetEntry) run(m dirMap) { entry := m[cmd.zid] if entry == nil { cmd.result <- Entry{Zid: id.Invalid} } else { cmd.result <- *entry } } type cmdNewEntry struct { result chan<- resNewEntry } type resNewEntry = Entry func (cmd *cmdNewEntry) run(m dirMap) { zid := id.New(false) if _, ok := m[zid]; !ok { entry := &Entry{Zid: zid, MetaSpec: MetaSpecUnknown} m[zid] = entry cmd.result <- *entry return } for { zid = id.New(true) if _, ok := m[zid]; !ok { entry := &Entry{Zid: zid, MetaSpec: MetaSpecUnknown} m[zid] = entry cmd.result <- *entry return } // TODO: do not wait here, but in a non-blocking goroutine. time.Sleep(100 * time.Millisecond) } } type cmdUpdateEntry struct { entry *Entry result chan<- struct{} } func (cmd *cmdUpdateEntry) run(m dirMap) { entry := *cmd.entry m[entry.Zid] = &entry cmd.result <- struct{}{} } type cmdRenameEntry struct { curEntry *Entry newEntry *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/directory/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 301 302 303 | //----------------------------------------------------------------------------- // 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 directory manages the directory part of a directory place. package directory import ( "io/ioutil" "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 { files, err := ioutil.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 _, file := range files { if !file.Mode().IsRegular() { continue } name := file.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 } } } pause := func() bool { for { select { case _, ok := <-tick: return ok } } } for { if !reloadFiles() { return } if watcher == nil { if !pause() { 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 { return append(events, ev) } for i := len(events) - 1; i >= 0; i-- { oev := events[i] switch oev.status { case fileStatusReloadStart, fileStatusReloadEnd: return append(events, ev) case fileStatusUpdate, fileStatusDelete: if ev.path == oev.path { if ev.status == oev.status { return events } oev.status = fileStatusNone return append(events, ev) } } } } return append(events, ev) } 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/directory/watch_test.go.
> > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 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 directory manages the directory part of a directory place. package directory 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/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 | //----------------------------------------------------------------------------- // 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" "net/url" "os" "path/filepath" "strconv" "strings" "sync" "time" "zettelstore.de/z/config/runtime" "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/manager" ) func init() { manager.Register("dir", func(u *url.URL, cdata *manager.ConnectData) (place.Place, error) { path := getDirPath(u) if _, err := os.Stat(path); os.IsNotExist(err) { return nil, err } dp := dirPlace{ u: u, readonly: getQueryBool(u, "readonly"), cdata: *cdata, dir: path, dirRescan: time.Duration( getQueryInt(u, "rescan", 60, 3600, 30*24*60*60)) * time.Second, fSrvs: uint32(getQueryInt(u, "worker", 1, 17, 1499)), } return &dp, nil }) } 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 { u *url.URL readonly bool cdata manager.ConnectData dir string dirRescan time.Duration dirSrv *directory.Service fSrvs uint32 fCmds []chan fileCmd mxCmds sync.RWMutex } func (dp *dirPlace) Location() string { return dp.u.String() } 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.dirSrv = directory.NewService(dp.dir, dp.dirRescan, dp.cdata.Notify) dp.mxCmds.Unlock() dp.dirSrv.Start() return nil } 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) Stop(ctx context.Context) error { dirSrv := dp.dirSrv dp.dirSrv = nil dirSrv.Stop() for _, c := range dp.fCmds { close(c) } return nil } 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 } meta := zettel.Meta entry := dp.dirSrv.GetNew() meta.Zid = entry.Zid dp.updateEntryFromMeta(&entry, meta) err := setZettel(dp, &entry, zettel) if err == nil { dp.dirSrv.UpdateEntry(&entry) } return meta.Zid, err } func (dp *dirPlace) GetZettel(ctx context.Context, zid id.Zid) (domain.Zettel, error) { entry := dp.dirSrv.GetEntry(zid) if !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 := dp.dirSrv.GetEntry(zid) if !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) (map[id.Zid]bool, error) { entries := dp.dirSrv.GetEntries() result := make(map[id.Zid]bool, len(entries)) for _, entry := range entries { result[entry.Zid] = true } return result, nil } func (dp *dirPlace) SelectMeta( ctx context.Context, f *place.Filter, s *place.Sorter) (res []*meta.Meta, err error) { hasMatch := place.CreateFilterFunc(f) entries := dp.dirSrv.GetEntries() res = make([]*meta.Meta, 0, len(entries)) for _, entry := range entries { // TODO: execute requests in parallel m, err := getMeta(dp, &entry, entry.Zid) if err != nil { continue } dp.cleanupMeta(ctx, m) dp.cdata.Filter.Enrich(ctx, m) if hasMatch(m) { res = append(res, m) } } if err != nil { return nil, err } return place.ApplySorter(res, s), 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 := dp.dirSrv.GetEntry(meta.Zid) if !entry.IsValid() { // Existing zettel, but new in this place. entry.Zid = meta.Zid dp.updateEntryFromMeta(&entry, meta) } else if entry.MetaSpec == directory.MetaSpecNone { if defaultMeta := entry.CalcDefaultMeta(); !meta.Equal(defaultMeta, true) { dp.updateEntryFromMeta(&entry, meta) dp.dirSrv.UpdateEntry(&entry) } } return setZettel(dp, &entry, zettel) } func (dp *dirPlace) updateEntryFromMeta(entry *directory.Entry, meta *meta.Meta) { entry.MetaSpec, entry.ContentExt = 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 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 runtime.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 := dp.dirSrv.GetEntry(curZid) if !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 } if err := deleteZettel(dp, &curEntry, curZid); err != nil { return err } return nil } func (dp *dirPlace) CanDeleteZettel(ctx context.Context, zid id.Zid) bool { if dp.readonly { return false } entry := dp.dirSrv.GetEntry(zid) return entry.IsValid() } func (dp *dirPlace) DeleteZettel(ctx context.Context, zid id.Zid) error { if dp.readonly { return place.ErrReadOnly } entry := dp.dirSrv.GetEntry(zid) if !entry.IsValid() { return nil } dp.dirSrv.DeleteEntry(zid) err := deleteZettel(dp, &entry, zid) return err } func (dp *dirPlace) Reload(ctx context.Context) error { // Brute force: stop everything, then start everything. // Could be done better in the future... err := dp.Stop(ctx) if err == nil { err = dp.Start(ctx) } return err } func (dp *dirPlace) ReadStats(st *place.Stats) { 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, runtime.GetDefaultRole()) } if syntax, ok := m.Get(meta.KeySyntax); !ok || syntax == "" { m.Set(meta.KeySyntax, runtime.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/service.go.
> > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 | //----------------------------------------------------------------------------- // 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 dirplace provides a directory-based zettel place. package dirplace import ( "io/ioutil" "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" ) 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 resGetMetaContent) dp.getFileChan(zid) <- &fileGetMetaContent{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() { var m *meta.Meta var err error switch cmd.entry.MetaSpec { case directory.MetaSpecFile: m, err = parseMetaFile(cmd.entry.Zid, cmd.entry.MetaPath) case directory.MetaSpecHeader: m, _, err = parseMetaContentFile(cmd.entry.Zid, cmd.entry.ContentPath) default: m = cmd.entry.CalcDefaultMeta() } if err == nil { cleanupMeta(m, cmd.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 switch cmd.entry.MetaSpec { case directory.MetaSpecFile: m, err = parseMetaFile(cmd.entry.Zid, cmd.entry.MetaPath) content, err = readFileContent(cmd.entry.ContentPath) case directory.MetaSpecHeader: m, content, err = parseMetaContentFile(cmd.entry.Zid, cmd.entry.ContentPath) default: m = cmd.entry.CalcDefaultMeta() content, err = readFileContent(cmd.entry.ContentPath) } if err == nil { cleanupMeta(m, cmd.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 f *os.File var err error switch cmd.entry.MetaSpec { case directory.MetaSpecFile: 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()) } } } case directory.MetaSpecHeader: 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 } } } } 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()) case directory.MetaSpecUnknown: panic("TODO: ???") } cmd.rc <- 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) case directory.MetaSpecUnknown: panic("TODO: ???") } cmd.rc <- err } // Utility functions ---------------------------------------- func readFileContent(path string) (string, error) { data, err := ioutil.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 cleanupMeta(m *meta.Meta, entry *directory.Entry) { if title, ok := m.Get(meta.KeyTitle); !ok || title == "" { m.Set(meta.KeyTitle, entry.Zid.String()) } switch entry.MetaSpec { case directory.MetaSpecFile: if syntax, ok := m.Get(meta.KeySyntax); !ok || syntax == "" { dm := entry.CalcDefaultMeta() syntax, ok = dm.Get(meta.KeySyntax) if !ok { panic("Default meta must contain syntax") } m.Set(meta.KeySyntax, syntax) } } if entry.Duplicates { m.Set(meta.KeyDuplicates, meta.ValueTrue) } } func openFileWrite(path string) (*os.File, error) { return os.OpenFile(path, os.O_WRONLY|os.O_CREATE|os.O_TRUNC, 0644) } 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/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 | //----------------------------------------------------------------------------- // 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 ( "strings" "zettelstore.de/z/domain/meta" ) // EnsureFilter make sure that there is a current filter. func EnsureFilter(filter *Filter) *Filter { if filter == nil { filter = new(Filter) filter.Expr = make(FilterExpr) } return filter } // FilterFunc is a predicate to check if given meta must be selected. type FilterFunc func(*meta.Meta) bool func selectAll(m *meta.Meta) bool { return true } type matchFunc func(value string) bool func matchAlways(value string) bool { return true } func matchNever(value string) bool { return false } type matchSpec struct { key string match matchFunc } // CreateFilterFunc calculates a filter func based on the given filter. func CreateFilterFunc(filter *Filter) FilterFunc { if filter == nil { return selectAll } specs := make([]matchSpec, 0, len(filter.Expr)) var searchAll FilterFunc for key, values := range filter.Expr { if len(key) == 0 { // Special handling if searching all keys... searchAll = createSearchAllFunc(values, filter.Negate) continue } if meta.KeyIsValid(key) { match := createMatchFunc(key, values) if match != nil { specs = append(specs, matchSpec{key, match}) } } } if len(specs) == 0 { if searchAll == nil { if sel := filter.Select; sel != nil { return sel } return selectAll } return addSelectFunc(filter, searchAll) } negate := filter.Negate searchMeta := func(m *meta.Meta) bool { for _, s := range specs { value, ok := m.Get(s.key) if !ok || !s.match(value) { return negate } } return !negate } if searchAll == nil { return addSelectFunc(filter, searchMeta) } return addSelectFunc(filter, func(meta *meta.Meta) bool { return searchAll(meta) || searchMeta(meta) }) } func addSelectFunc(filter *Filter, f FilterFunc) FilterFunc { if filter == nil { return f } if sel := filter.Select; sel != nil { return func(meta *meta.Meta) bool { return sel(meta) && f(meta) } } return f } func createMatchFunc(key string, values []string) matchFunc { switch meta.Type(key) { case meta.TypeBool: preValues := make([]bool, 0, len(values)) for _, v := range values { preValues = append(preValues, meta.BoolValue(v)) } return func(value string) bool { bValue := meta.BoolValue(value) for _, v := range preValues { if bValue != v { return false } } return true } case meta.TypeCredential: return matchNever case meta.TypeID, meta.TypeTimestamp: // ID and timestamp use the same layout return func(value string) bool { for _, v := range values { if !strings.HasPrefix(value, v) { return false } } return true } case meta.TypeIDSet: 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) { return false } } } return true } case meta.TypeTagSet: tagValues := preprocessSet(values) return func(value string) bool { tags := meta.ListFromValue(value) for _, neededTags := range tagValues { for _, neededTag := range neededTags { if !matchAllTag(tags, neededTag) { return false } } } return true } case meta.TypeWord: values = sliceToLower(values) return func(value string) bool { value = strings.ToLower(value) for _, v := range values { if value != v { return false } } return true } case meta.TypeWordSet: 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) { return false } } } return true } } values = sliceToLower(values) return func(value string) bool { value = strings.ToLower(value) for _, v := range values { if !strings.Contains(value, v) { return false } } return true } } func createSearchAllFunc(values []string, negate bool) FilterFunc { matchFuncs := map[*meta.DescriptionType]matchFunc{} return func(m *meta.Meta) bool { for _, p := range m.Pairs(true) { keyType := meta.Type(p.Key) match, ok := matchFuncs[keyType] if !ok { if keyType == meta.TypeBool { match = createBoolSearchFunc(p.Key, values) } else { match = createMatchFunc(p.Key, values) } matchFuncs[keyType] = match } if match(p.Value) { return !negate } } match, ok := matchFuncs[meta.Type(meta.KeyID)] if !ok { match = createMatchFunc(meta.KeyID, values) } return match(m.Zid.String()) != negate } } // createBoolSearchFunc only creates a matchFunc if the values to compare are // possible bool values. Otherwise every meta with a bool key could match the // search query. func createBoolSearchFunc(key string, values []string) matchFunc { for _, v := range values { if len(v) > 0 && !strings.ContainsRune("01tfTFynYN", rune(v[0])) { return func(value string) bool { return false } } } return createMatchFunc(key, values) } func sliceToLower(sl []string) []string { result := make([]string, 0, len(sl)) for _, s := range sl { result = append(result, strings.ToLower(s)) } return result } func isEmptySlice(sl []string) bool { for _, s := range sl { if len(s) > 0 { return false } } return true } func preprocessSet(set []string) [][]string { result := make([][]string, 0, len(set)) for _, elem := range set { splitElems := strings.Split(elem, ",") valueElems := make([]string, 0, len(splitElems)) for _, se := range splitElems { e := strings.TrimSpace(se) if len(e) > 0 { valueElems = append(valueElems, e) } } if len(valueElems) > 0 { result = append(result, valueElems) } } return result } func matchAllID(zettelIDs []string, neededID string) bool { for _, zt := range zettelIDs { if strings.HasPrefix(zt, neededID) { return true } } return false } func matchAllTag(zettelTags []string, neededTag string) bool { for _, zt := range zettelTags { if zt == neededTag { return true } } return false } func matchAllWord(zettelWords []string, neededWord string) bool { for _, zw := range zettelWords { if zw == neededWord { return true } } return false } |
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 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 | //----------------------------------------------------------------------------- // 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 of a Zettelstore. package manager import ( "context" "log" "net/url" "sort" "strings" "sync" "zettelstore.de/z/domain" "zettelstore.de/z/domain/id" "zettelstore.de/z/domain/meta" "zettelstore.de/z/index" "zettelstore.de/z/place" ) // ConnectData contains all administration related values. type ConnectData struct { Filter index.MetaFilter Notify chan<- place.ChangeInfo } // Connect returns a handle to the specified place func Connect(rawURL string, readonlyMode bool, cdata *ConnectData) (place.Place, error) { u, err := url.Parse(rawURL) if err != nil { return nil, err } if u.Scheme == "" { u.Scheme = "dir" } if readonlyMode { // 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" } 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.Place, 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 { mx sync.RWMutex started bool placeURIs []url.URL subplaces []place.Place filter index.MetaFilter observers []func(place.ChangeInfo) mxObserver sync.RWMutex done chan struct{} infos chan place.ChangeInfo } // New creates a new managing place. func New(placeURIs []string, readonlyMode bool, filter index.MetaFilter) (*Manager, error) { mgr := &Manager{ filter: filter, infos: make(chan place.ChangeInfo, len(placeURIs)*10), } cdata := ConnectData{Filter: filter, Notify: mgr.infos} subplaces := make([]place.Place, 0, len(placeURIs)+2) for _, uri := range placeURIs { p, err := Connect(uri, readonlyMode, &cdata) if err != nil { return nil, err } 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 func(place.ChangeInfo)) { if f != nil { mgr.mxObserver.Lock() mgr.observers = append(mgr.observers, f) mgr.mxObserver.Unlock() } } func (mgr *Manager) notifyObserver(ci place.ChangeInfo) { mgr.mxObserver.RLock() observers := mgr.observers mgr.mxObserver.RUnlock() for _, ob := range observers { ob(ci) } } func notifier(notify func(place.ChangeInfo), infos <-chan place.ChangeInfo, done <-chan struct{}) { // The call to notify may panic. Ensure a running notifier. defer func() { if err := recover(); err != nil { go notifier(notify, infos, done) } }() for { select { case ci, ok := <-infos: if ok { notify(ci) } case _, ok := <-done: if !ok { return } } } } // Location returns some information where the place is located. func (mgr *Manager) Location() string { if len(mgr.subplaces) < 2 { return mgr.subplaces[0].Location() } 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() } // 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.mx.Lock() if mgr.started { mgr.mx.Unlock() return place.ErrStarted } for i := len(mgr.subplaces) - 1; i >= 0; i-- { if ssi, ok := mgr.subplaces[i].(place.StartStopper); ok { if err := ssi.Start(ctx); err != nil { for j := i + 1; j < len(mgr.subplaces); j++ { if ssj, ok := mgr.subplaces[j].(place.StartStopper); ok { ssj.Stop(ctx) } } mgr.mx.Unlock() return err } } } mgr.done = make(chan struct{}) go notifier(mgr.notifyObserver, mgr.infos, mgr.done) mgr.started = true mgr.mx.Unlock() mgr.infos <- place.ChangeInfo{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.mx.Lock() defer mgr.mx.Unlock() if !mgr.started { return place.ErrStopped } close(mgr.done) mgr.done = nil 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 } // CanCreateZettel returns true, if place could possibly create a new zettel. func (mgr *Manager) CanCreateZettel(ctx context.Context) bool { mgr.mx.RLock() defer mgr.mx.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.mx.RLock() defer mgr.mx.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.mx.RLock() defer mgr.mx.RUnlock() if !mgr.started { return domain.Zettel{}, place.ErrStopped } for _, p := range mgr.subplaces { if z, err := p.GetZettel(ctx, zid); err != place.ErrNotFound { mgr.filter.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.mx.RLock() defer mgr.mx.RUnlock() if !mgr.started { return nil, place.ErrStopped } for _, p := range mgr.subplaces { if m, err := p.GetMeta(ctx, zid); err != place.ErrNotFound { mgr.filter.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 map[id.Zid]bool, err error) { mgr.mx.RLock() defer mgr.mx.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, f *place.Filter, s *place.Sorter) ([]*meta.Meta, error) { mgr.mx.RLock() defer mgr.mx.RUnlock() if !mgr.started { return nil, place.ErrStopped } var result []*meta.Meta for _, p := range mgr.subplaces { selected, err := p.SelectMeta(ctx, f, nil) if err != nil { return nil, err } if len(result) == 0 { result = selected } else { result = place.MergeSorted(result, selected) } } if s == nil { return result, nil } return place.ApplySorter(result, s), nil } // CanUpdateZettel returns true, if place could possibly update the given zettel. func (mgr *Manager) CanUpdateZettel(ctx context.Context, zettel domain.Zettel) bool { mgr.mx.RLock() defer mgr.mx.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.mx.RLock() defer mgr.mx.RUnlock() if !mgr.started { return place.ErrStopped } zettel.Meta = zettel.Meta.Clone() mgr.filter.Remove(ctx, zettel.Meta) 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.mx.RLock() defer mgr.mx.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.mx.RLock() defer mgr.mx.RUnlock() if !mgr.started { return place.ErrStopped } for i, p := range mgr.subplaces { if err := p.RenameZettel(ctx, curZid, newZid); err != nil && 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.mx.RLock() defer mgr.mx.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.mx.RLock() defer mgr.mx.RUnlock() if !mgr.started { return place.ErrStopped } for _, p := range mgr.subplaces { if err := p.DeleteZettel(ctx, zid); err != place.ErrNotFound && err != place.ErrReadOnly { return err } } return place.ErrNotFound } // Reload clears all caches, reloads all internal data to reflect changes // that were possibly undetected. func (mgr *Manager) Reload(ctx context.Context) error { mgr.mx.RLock() defer mgr.mx.RUnlock() if !mgr.started { return place.ErrStopped } var err error for _, p := range mgr.subplaces { if err1 := p.Reload(ctx); err1 != nil && err == nil { err = err1 } } mgr.infos <- place.ChangeInfo{Reason: place.OnReload, Zid: id.Invalid} return err } // ReadStats populates st with place statistics func (mgr *Manager) ReadStats(st *place.Stats) { subStats := make([]place.Stats, 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.Zettel = sumZettel } // NumPlaces returns the number of managed places. func (mgr *Manager) NumPlaces() int { return len(mgr.subplaces) } |
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 201 202 203 204 205 206 207 208 209 | //----------------------------------------------------------------------------- // 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" "time" "zettelstore.de/z/domain" "zettelstore.de/z/domain/id" "zettelstore.de/z/domain/meta" "zettelstore.de/z/place" "zettelstore.de/z/place/manager" ) func init() { manager.Register( "mem", func(u *url.URL, cdata *manager.ConnectData) (place.Place, 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.ChangeReason, zid id.Zid) { if chci := mp.cdata.Notify; chci != nil { chci <- place.ChangeInfo{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() meta := zettel.Meta.Clone() meta.Zid = mp.calcNewZid() zettel.Meta = meta mp.zettel[meta.Zid] = zettel mp.mx.Unlock() mp.notifyChanged(place.OnUpdate, meta.Zid) return meta.Zid, nil } func (mp *memPlace) calcNewZid() id.Zid { zid := id.New(false) if _, ok := mp.zettel[zid]; !ok { return zid } for { zid = id.New(true) if _, ok := mp.zettel[zid]; !ok { return zid } time.Sleep(100 * time.Millisecond) } } 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) (map[id.Zid]bool, error) { mp.mx.RLock() result := make(map[id.Zid]bool, len(mp.zettel)) for zid := range mp.zettel { result[zid] = true } mp.mx.RUnlock() return result, nil } func (mp *memPlace) SelectMeta(ctx context.Context, f *place.Filter, s *place.Sorter) ([]*meta.Meta, error) { filterFunc := place.CreateFilterFunc(f) result := make([]*meta.Meta, 0, len(mp.zettel)) mp.mx.RLock() for _, zettel := range mp.zettel { m := zettel.Meta.Clone() mp.cdata.Filter.Enrich(ctx, m) if filterFunc(m) { result = append(result, m) } } mp.mx.RUnlock() return place.ApplySorter(result, s), 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) Reload(ctx context.Context) error { return nil } func (mp *memPlace) ReadStats(st *place.Stats) { 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 Detlef Stern // // This file is part of zettelstore. // // Zettelstore is licensed under 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 meta data, sorted by a given Sorter. // 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 | //----------------------------------------------------------------------------- // 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" "zettelstore.de/z/domain" "zettelstore.de/z/domain/id" "zettelstore.de/z/domain/meta" ) // Place is implemented by all Zettel places. type Place 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) (map[id.Zid]bool, error) // SelectMeta returns all zettel meta data that match the selection criteria. // TODO: more docs SelectMeta(ctx context.Context, f *Filter, s *Sorter) ([]*meta.Meta, 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 // Reload clears all caches, reloads all internal data to reflect changes // that were possibly undetected. Reload(ctx context.Context) error // ReadStats populates st with place statistics ReadStats(st *Stats) } // Stats records statistics about the place. type Stats 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 } // ChangeReason gives an indication, why the ObserverFunc was called. type ChangeReason int // Values for ChangeReason const ( _ ChangeReason = iota OnReload // Place was reloaded OnUpdate // A zettel was created or changed OnDelete // A zettel was removed ) // ChangeInfo contains all the data about a changed zettel. type ChangeInfo struct { Reason ChangeReason Zid id.Zid } // Manager is a place-managing place. type Manager interface { Place StartStopper // RegisterObserver registers an observer that will be notified // if one or all zettel are found to be changed. RegisterObserver(func(ChangeInfo)) // NumPlaces returns the number of managed places. NumPlaces() int } // 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()) } // IsErrNotAllowed return true, if the error is of type ErrNotAllowed. func IsErrNotAllowed(err error) bool { _, ok := err.(*ErrNotAllowed) return ok } // 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") // 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() } // Filter specifies a mechanism for selecting zettel. type Filter struct { Expr FilterExpr Negate bool Select func(*meta.Meta) bool } // FilterExpr is the encoding of a search filter. type FilterExpr map[string][]string // map of keys to or-ed values // Sorter specifies ordering and limiting a sequnce of meta data. type Sorter struct { 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 } |
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 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 | //----------------------------------------------------------------------------- // Copyright (c) 2020-2021 Detlef Stern // // This file is part of zettelstore. // // Zettelstore is licensed under 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/config/startup" "zettelstore.de/z/domain/id" "zettelstore.de/z/domain/meta" ) func genConfigZettelM(zid id.Zid) *meta.Meta { if myPlace.startConfig == nil { return nil } m := meta.New(zid) m.Set(meta.KeyTitle, "Zettelstore Startup Configuration") m.Set(meta.KeyVisibility, meta.ValueVisibilitySimple) return m } func genConfigZettelC(m *meta.Meta) string { var sb strings.Builder for i, p := range myPlace.startConfig.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() } func genConfigM(zid id.Zid) *meta.Meta { if myPlace.startConfig == nil { return nil } m := meta.New(zid) m.Set(meta.KeyTitle, "Zettelstore Startup Values") m.Set(meta.KeyRole, meta.ValueRoleConfiguration) m.Set(meta.KeySyntax, meta.ValueSyntaxZmk) m.Set(meta.KeyVisibility, meta.ValueVisibilitySimple) m.Set(meta.KeyReadOnly, meta.ValueTrue) return m } func genConfigC(m *meta.Meta) string { var sb strings.Builder sb.WriteString("|=Name|=Value>\n") fmt.Fprintf(&sb, "|Simple|%v\n", startup.IsSimple()) fmt.Fprintf(&sb, "|Verbose|%v\n", startup.IsVerbose()) fmt.Fprintf(&sb, "|Read-only|%v\n", startup.IsReadOnlyMode()) fmt.Fprintf(&sb, "|URL prefix|%v\n", startup.URLPrefix()) // There must be a space before the next "%v". Listen address may start with a ":" fmt.Fprintf(&sb, "|Listen address| %v\n", startup.ListenAddress()) fmt.Fprintf(&sb, "|Authentication enabled|%v\n", startup.WithAuth()) fmt.Fprintf(&sb, "|Secure cookie|%v\n", startup.SecureCookie()) fmt.Fprintf(&sb, "|Persistent Cookie|%v\n", startup.PersistentCookie()) html, api := startup.TokenLifetime() fmt.Fprintf(&sb, "|API Token lifetime|%v\n", api) fmt.Fprintf(&sb, "|HTML Token lifetime|%v\n", html) return sb.String() } |
Added place/progplace/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 | //----------------------------------------------------------------------------- // 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" "os" "sort" "strings" "zettelstore.de/z/config/startup" "zettelstore.de/z/domain/id" "zettelstore.de/z/domain/meta" ) func genEnvironmentM(zid id.Zid) *meta.Meta { if myPlace.startConfig == nil { return nil } m := meta.New(zid) m.Set(meta.KeyTitle, "Zettelstore Environment Values") return m } func genEnvironmentC(*meta.Meta) string { workDir, err := os.Getwd() if err != nil { workDir = err.Error() } execName, err := os.Executable() if err != nil { execName = err.Error() } envs := os.Environ() sort.Strings(envs) var sb strings.Builder sb.WriteString("|=Name|=Value>\n") fmt.Fprintf(&sb, "|Working directory| %v\n", workDir) fmt.Fprintf(&sb, "|Executable| %v\n", execName) fmt.Fprintf(&sb, "|Build with| %v\n", startup.GetVersion().GoVersion) sb.WriteString("=== Environment\n") sb.WriteString("|=Key>|=Value<\n") for _, env := range envs { if pos := strings.IndexByte(env, '='); pos >= 0 && pos < len(env) { fmt.Fprintf(&sb, "| %v| %v\n", env[:pos], env[pos+1:]) } else { fmt.Fprintf(&sb, "| %v\n", env) } } return sb.String() } |
Added place/progplace/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 | //----------------------------------------------------------------------------- // 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/index" ) func genIndexerM(zid id.Zid) *meta.Meta { if myPlace.indexer == nil { return nil } m := meta.New(zid) m.Set(meta.KeyTitle, "Zettelstore Indexer") return m } func genIndexerC(*meta.Meta) string { ixer := myPlace.indexer var stats index.IndexerStats ixer.ReadStats(&stats) var sb strings.Builder sb.WriteString("|=Name|=Value>\n") fmt.Fprintf(&sb, "|Zettel| %v\n", stats.Store.Zettel) fmt.Fprintf(&sb, "|Last re-index| %v\n", stats.LastReload.Format("2006-01-02 15:04:05 -0700 MST")) fmt.Fprintf(&sb, "|Indexes since last re-index| %v\n", stats.IndexesSinceReload) fmt.Fprintf(&sb, "|Duration last index| %vms\n", stats.DurLastIndex.Milliseconds()) fmt.Fprintf(&sb, "|Zettel enrichments| %v\n", stats.Store.Updates) 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 42 43 44 45 | //----------------------------------------------------------------------------- // 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/place" ) func genManagerM(zid id.Zid) *meta.Meta { if myPlace.manager == nil { return nil } m := meta.New(zid) m.Set(meta.KeyTitle, "Zettelstore Place Manager") return m } func genManagerC(*meta.Meta) string { mgr := myPlace.manager var stats place.Stats mgr.ReadStats(&stats) var sb strings.Builder sb.WriteString("|=Name|=Value>\n") fmt.Fprintf(&sb, "|Read-only| %v\n", stats.ReadOnly) fmt.Fprintf(&sb, "|Zettel| %v\n", stats.Zettel) fmt.Fprintf(&sb, "|Sub-places| %v\n", mgr.NumPlaces()) 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 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 | //----------------------------------------------------------------------------- // 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/index" "zettelstore.de/z/place" "zettelstore.de/z/place/manager" ) func init() { manager.Register( " prog", func(u *url.URL, cdata *manager.ConnectData) (place.Place, error) { return getPlace(cdata.Filter), nil }) } type ( zettelGen struct { meta func(id.Zid) *meta.Meta content func(*meta.Meta) string } progPlace struct { zettel map[id.Zid]zettelGen filter index.MetaFilter startConfig *meta.Meta manager place.Manager indexer index.Indexer } ) var myPlace *progPlace // Get returns the one program place. func getPlace(mf index.MetaFilter) place.Place { if myPlace == nil { myPlace = &progPlace{ zettel: map[id.Zid]zettelGen{ id.Zid(1): {genVersionBuildM, genVersionBuildC}, id.Zid(2): {genVersionHostM, genVersionHostC}, id.Zid(3): {genVersionOSM, genVersionOSC}, id.Zid(6): {genEnvironmentM, genEnvironmentC}, id.Zid(8): {genRuntimeM, genRuntimeC}, id.Zid(18): {genIndexerM, genIndexerC}, id.Zid(20): {genManagerM, genManagerC}, id.Zid(90): {genKeysM, genKeysC}, id.Zid(96): {genConfigZettelM, genConfigZettelC}, id.Zid(98): {genConfigM, genConfigC}, }, filter: mf, } } return myPlace } // Setup remembers important values. func Setup(startConfig *meta.Meta, manager place.Manager, idx index.Indexer) { if myPlace == nil { panic("progplace.getPlace not called") } if myPlace.startConfig != nil || myPlace.manager != nil { panic("progplace.Setup already called") } myPlace.startConfig = startConfig.Clone() myPlace.manager = manager myPlace.indexer = idx } 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 := pp.zettel[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 := pp.zettel[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) (map[id.Zid]bool, error) { result := make(map[id.Zid]bool, len(pp.zettel)) for zid, gen := range pp.zettel { if genMeta := gen.meta; genMeta != nil { if genMeta(zid) != nil { result[zid] = true } } } return result, nil } func (pp *progPlace) SelectMeta( ctx context.Context, f *place.Filter, s *place.Sorter) (res []*meta.Meta, err error) { hasMatch := place.CreateFilterFunc(f) for zid, gen := range pp.zettel { if genMeta := gen.meta; genMeta != nil { if m := genMeta(zid); m != nil { updateMeta(m) pp.filter.Enrich(ctx, m) if hasMatch(m) { res = append(res, m) } } } } return place.ApplySorter(res, s), 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 := pp.zettel[zid] return !ok } func (pp *progPlace) RenameZettel(ctx context.Context, curZid, newZid id.Zid) error { if _, ok := pp.zettel[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 := pp.zettel[zid]; ok { return place.ErrReadOnly } return place.ErrNotFound } func (pp *progPlace) Reload(ctx context.Context) error { return nil } func (pp *progPlace) ReadStats(st *place.Stats) { st.ReadOnly = true st.Zettel = len(pp.zettel) } func updateMeta(m *meta.Meta) { m.Set(meta.KeySyntax, meta.ValueSyntaxZmk) m.Set(meta.KeyRole, meta.ValueRoleConfiguration) m.Set(meta.KeyReadOnly, meta.ValueTrue) if _, ok := m.Get(meta.KeyVisibility); !ok { m.Set(meta.KeyVisibility, meta.ValueVisibilityExpert) } } |
Added place/progplace/runtime.go.
> > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 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-2021 Detlef Stern // // This file is part of zettelstore. // // Zettelstore is licensed under 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" "runtime" "strings" "time" "zettelstore.de/z/domain/id" "zettelstore.de/z/domain/meta" ) func genRuntimeM(zid id.Zid) *meta.Meta { if myPlace.startConfig == nil { return nil } m := meta.New(zid) m.Set(meta.KeyTitle, "Zettelstore Runtime Values") return m } func genRuntimeC(*meta.Meta) string { var sb strings.Builder sb.WriteString("|=Name|=Value>\n") fmt.Fprintf(&sb, "|Number of CPUs|%v\n", runtime.NumCPU()) fmt.Fprintf(&sb, "|Number of goroutines|%v\n", runtime.NumGoroutine()) fmt.Fprintf(&sb, "|Number of Cgo calls|%v\n", runtime.NumCgoCall()) var m runtime.MemStats runtime.ReadMemStats(&m) fmt.Fprintf(&sb, "|Memory from OS|%v\n", m.Sys) fmt.Fprintf(&sb, "|Objects active|%v\n", m.Mallocs-m.Frees) fmt.Fprintf(&sb, "|Heap alloc|%v\n", m.HeapAlloc) fmt.Fprintf(&sb, "|Heap sys|%v\n", m.HeapSys) fmt.Fprintf(&sb, "|Heap idle|%v\n", m.HeapIdle) fmt.Fprintf(&sb, "|Heap in use|%v\n", m.HeapInuse) fmt.Fprintf(&sb, "|Heap released|%v\n", m.HeapReleased) fmt.Fprintf(&sb, "|Heap objects|%v\n", m.HeapObjects) fmt.Fprintf(&sb, "|Stack in use|%v\n", m.StackInuse) fmt.Fprintf(&sb, "|Stack sys|%v\n", m.StackSys) fmt.Fprintf(&sb, "|Garbage collection metadata|%v\n", m.GCSys) fmt.Fprintf(&sb, "|Last garbage collection|%v\n", time.Unix((int64)(m.LastGC/1000000000), 0)) fmt.Fprintf(&sb, "|Garbage collection goal|%v\n", m.NextGC) fmt.Fprintf(&sb, "|Garbage collections|%v\n", m.NumGC) fmt.Fprintf(&sb, "|Forced garbage collections|%v\n", m.NumForcedGC) fmt.Fprintf(&sb, "|Garbage collection fraction|%.3f%%\n", m.GCCPUFraction*100.0) return sb.String() } |
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 | //----------------------------------------------------------------------------- // 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/config/startup" "zettelstore.de/z/domain/id" "zettelstore.de/z/domain/meta" ) func getVersionMeta(zid id.Zid, title string) *meta.Meta { m := meta.New(zid) m.Set(meta.KeyTitle, title) m.Set(meta.KeyVisibility, meta.ValueVisibilitySimple) 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 startup.GetVersion().Build } func genVersionHostM(zid id.Zid) *meta.Meta { return getVersionMeta(zid, "Zettelstore Host") } func genVersionHostC(*meta.Meta) string { return startup.GetVersion().Hostname } func genVersionOSM(zid id.Zid) *meta.Meta { return getVersionMeta(zid, "Zettelstore Operating System") } func genVersionOSC(*meta.Meta) string { v := startup.GetVersion() return fmt.Sprintf("%v/%v", v.Os, v.Arch) } |
Added place/sorter.go.
> > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 | //----------------------------------------------------------------------------- // Copyright (c) 2020-2021 Detlef Stern // // This file is part of zettelstore. // // Zettelstore is licensed under 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 ( "math/rand" "sort" "strconv" "zettelstore.de/z/domain/meta" ) // RandomOrder is a pseudo metadata key that selects a random order. const RandomOrder = "_random" // EnsureSorter makes sure that there is a sorter object. func EnsureSorter(sorter *Sorter) *Sorter { if sorter == nil { sorter = new(Sorter) } return sorter } // ApplySorter applies the given sorter to the slide of meta data. func ApplySorter(metaList []*meta.Meta, s *Sorter) []*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, getSortFunc(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, getSortFunc(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 } type sortFunc func(i, j int) bool func getSortFunc(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 } } else if keyType == meta.TypeBool { 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 } } else if keyType == meta.TypeNumber { 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 } } 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 } |
Added place/stock/stock.go.
> > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 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 stock allows to get zettel without reading it from a place. package stock import ( "context" "sync" "zettelstore.de/z/domain" "zettelstore.de/z/domain/id" "zettelstore.de/z/domain/meta" "zettelstore.de/z/place" ) // Place is a place that is used by a stock. type Place interface { // RegisterObserver registers an observer that will be notified // if all or one zettel are found to be changed. RegisterObserver(ob func(place.ChangeInfo)) // GetZettel retrieves a specific zettel. GetZettel(ctx context.Context, zid id.Zid) (domain.Zettel, error) } // Stock allow to get subscribed zettel without reading it from a place. type Stock interface { Subscribe(zid id.Zid) error GetZettel(zid id.Zid) domain.Zettel GetMeta(zid id.Zid) *meta.Meta } // NewStock creates a new stock that operates on the given place. func NewStock(place Place) Stock { //RegisterChangeObserver(func(domain.Zid)) stock := &defaultStock{ place: place, subs: make(map[id.Zid]domain.Zettel), } place.RegisterObserver(stock.observe) return stock } type defaultStock struct { place Place subs map[id.Zid]domain.Zettel mxSubs sync.RWMutex } // observe tracks all changes the place signals. func (s *defaultStock) observe(ci place.ChangeInfo) { if ci.Reason == place.OnReload { go func() { s.mxSubs.Lock() defer s.mxSubs.Unlock() for zid := range s.subs { s.update(zid) } }() return } s.mxSubs.RLock() defer s.mxSubs.RUnlock() if _, found := s.subs[ci.Zid]; found { go func() { s.mxSubs.Lock() defer s.mxSubs.Unlock() s.update(ci.Zid) }() } } func (s *defaultStock) update(zid id.Zid) { if zettel, err := s.place.GetZettel(context.Background(), zid); err == nil { s.subs[zid] = zettel return } } // Subscribe adds a zettel to the stock. func (s *defaultStock) Subscribe(zid id.Zid) error { s.mxSubs.Lock() defer s.mxSubs.Unlock() if _, found := s.subs[zid]; found { return nil } zettel, err := s.place.GetZettel(context.Background(), zid) if err != nil { return err } s.subs[zid] = zettel return nil } // GetZettel returns the zettel with the given zid, if in stock, else an empty zettel func (s *defaultStock) GetZettel(zid id.Zid) domain.Zettel { s.mxSubs.RLock() defer s.mxSubs.RUnlock() return s.subs[zid] } // GetZettel returns the zettel Meta with the given zid, if in stock, else nil. func (s *defaultStock) GetMeta(zid id.Zid) *meta.Meta { s.mxSubs.RLock() zettel, ok := s.subs[zid] s.mxSubs.RUnlock() if ok { return zettel.Meta } return nil } |
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 } |
Deleted staticcheck.conf.
|
| < < |
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 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 strfun provides some string functions. package strfun import ( "strings" "unicode" "golang.org/x/text/unicode/norm" ) var ( useUnicode = []*unicode.RangeTable{ unicode.Letter, unicode.Number, } ignoreUnicode = []*unicode.RangeTable{ unicode.Mark, unicode.Sk, unicode.Lm, } ) // Slugify returns a string that can be used as part of an URL func Slugify(s string) string { s = strings.TrimSpace(s) result := make([]rune, 0, len(s)) addDash := false for _, r := range norm.NFKD.String(s) { if unicode.IsOneOf(useUnicode, r) { result = append(result, unicode.ToLower(r)) addDash = true } else if !unicode.IsOneOf(ignoreUnicode, r) && addDash { result = append(result, '-') addDash = false } } if i := len(result) - 1; i >= 0 && result[i] == '-' { result = result[:i] } return string(result) } |
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 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 strfun provides some string functions. package strfun_test import ( "testing" "zettelstore.de/z/strfun" ) var 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"}, {"*", ""}, } func TestSlugify(t *testing.T) { for _, test := range tests { if got := strfun.Slugify(test.in); got != test.exp { t.Errorf("%q: %q != %q", test.in, got, test.exp) } } } |
Deleted strfun/strfun.go.
|
| < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < |
Deleted strfun/strfun_test.go.
|
| < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < |
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 | //----------------------------------------------------------------------------- // 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. // // 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 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 } 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 len(tag) == 0 { 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 } } // Skip all whitespaces apeared after these types of tags until end of line if // the line only contains a tag and whitespaces. const skipWhitespaceTagTypes = "#^/<>=!" standalone := true if mayStandalone { if !strings.Contains(skipWhitespaceTagTypes, tag[0:1]) { standalone = false } else { if eow == len(tmpl.data) { standalone = true tmpl.p = eow } else if eow < len(tmpl.data) && tmpl.data[eow] == '\n' { standalone = true tmpl.p = eow + 1 tmpl.curline++ } else if eow+1 < len(tmpl.data) && tmpl.data[eow] == '\r' && tmpl.data[eow+1] == '\n' { standalone = true tmpl.p = eow + 2 tmpl.curline++ } else { standalone = false } } } return &tagReadingResult{ tag: tag, standalone: standalone, }, nil } 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 break 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 break 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) } Outer: for i := len(stack) - 1; i >= 0; i-- { v := stack[i] 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], nil } } } if name == "." { return v, nil } switch av := v; av.Kind() { case reflect.Ptr: v = av.Elem() case reflect.Interface: v = av.Elem() case reflect.Struct: ret := av.FieldByName(name) if ret.IsValid() { return ret, nil } continue Outer case reflect.Map: ret := av.MapIndex(reflect.ValueOf(name)) if ret.IsValid() { return ret, nil } continue Outer default: continue Outer } } } if errMissing { return reflect.Value{}, fmt.Errorf("Missing variable %q", name) } return reflect.Value{}, nil } 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 len(strings.TrimSpace(val.String())) == 0 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 nil, check if it's an inverted section isEmpty := isEmpty(value) if isEmpty && !section.inverted || !isEmpty && 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, 0, valLen) for i := 0; i < valLen; i++ { enumeration = append(enumeration, val.Index(i)) } topStack := len(stack) stack = append(stack, enumeration[0]) defer func() { stack = stack[:topStack-1] }() for _, elem := range enumeration { stack[topStack] = elem for _, n := range section.nodes { if err := tmpl.renderNode(w, n, stack); err != nil { return err } } } return nil case reflect.Map, reflect.Struct: stack = append(stack, value) defer func() { stack = stack[:len(stack)-2] }() } } for _, n := range section.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 476 477 478 479 480 481 482 483 484 485 486 487 488 489 490 491 492 493 | //----------------------------------------------------------------------------- // 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. // // 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 LayoutTest struct { layout string tmpl string context interface{} expected string } var layoutTests = []LayoutTest{ {`Header {{content}} Footer`, `Hello World`, nil, `Header Hello World Footer`}, {`Header {{content}} Footer`, `Hello {{s}}`, map[string]string{"s": "World"}, `Header Hello World Footer`}, {`Header {{content}} Footer`, `Hello {{content}}`, map[string]string{"content": "World"}, `Header Hello World Footer`}, {`Header {{extra}} {{content}} Footer`, `Hello {{content}}`, map[string]string{"content": "World", "extra": "extra"}, `Header extra Hello World Footer`}, {`Header {{content}} {{content}} Footer`, `Hello {{content}}`, map[string]string{"content": "World"}, `Header Hello World Hello World Footer`}, } 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) case template.Invalid: t.Errorf("invalid tag type: %s", tagString(tag.Type())) return 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 Detlef Stern // // This file is part of zettelstore. // // Zettelstore is licensed 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" "io/ioutil" "os" "path/filepath" "sort" "testing" "zettelstore.de/z/template" ) var enabledTests = map[string]map[string]bool{ "comments.json": map[string]bool{ "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": map[string]bool{ "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": map[string]bool{ "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": map[string]bool{ "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": map[string]bool{ "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": map[string]bool{ "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 os.IsNotExist(err) { 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 := ioutil.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 | title: Simple Test [[Home|https://zettelstore.de/z]] [[https://zettelstore.de]] [[Config|00000000000100]] [[00000000000100]] [[Frag|#frag]] [[#frag]] |
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 | 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 | //----------------------------------------------------------------------------- // 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 tests provides some higher-level tests. package tests import ( "encoding/json" "fmt" "io/ioutil" "regexp" "strings" "testing" "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 "- 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 TestMarkdownSpec(t *testing.T) { content, err := ioutil.ReadFile("../testdata/markdown/spec.json") if err != nil { panic(err) } var testcases []markdownTestCase if err = json.Unmarshal(content, &testcases); err != nil { panic(err) } for _, format := range formats { enc := encoder.Create(format) if enc == nil { panic(fmt.Sprintf("No encoder for %q found", format)) } } excMap := make(map[string]bool, len(exceptions)) for _, exc := range exceptions { excMap[exc] = true } htmlEncoder := encoder.Create("html", &encoder.BoolOption{Key: "xhtml", Value: true}) zmkEncoder := encoder.Create("zmk") var sb strings.Builder for _, tc := range testcases { testID := tc.Example*100 + 1 ast := parser.ParseBlocks(input.NewInput(tc.Markdown), nil, "markdown") for _, format := range formats { t.Run(fmt.Sprintf("Encode %v %v", format, testID), func(st *testing.T) { encoder.Create(format).WriteBlocks(&sb, ast) sb.Reset() }) } if _, found := excMap[tc.Markdown]; !found { 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) } } }) } 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 | //----------------------------------------------------------------------------- // 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/ioutil" "os" "path/filepath" "strings" "testing" "zettelstore.de/z/ast" "zettelstore.de/z/domain" "zettelstore.de/z/domain/meta" "zettelstore.de/z/encoder" "zettelstore.de/z/parser" "zettelstore.de/z/place" _ "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" "zettelstore.de/z/place/manager" ) var formats = []string{"html", "djson", "native", "text"} func getFilePlaces(wd string, kind string) (root string, places []place.Place) { root = filepath.Clean(filepath.Join(wd, "..", "testdata", kind)) infos, err := ioutil.ReadDir(root) if err != nil { panic(err) } cdata := manager.ConnectData{Filter: &noFilter{}, Notify: nil} for _, info := range infos { if info.Mode().IsDir() { place, err := manager.Connect( "dir://"+filepath.Join(root, info.Name()), false, &cdata, ) if err != nil { panic(err) } places = append(places, place) } } return root, places } type noFilter struct{} func (nf *noFilter) Enrich(ctx context.Context, m *meta.Meta) {} func (nf *noFilter) Remove(ctx context.Context, m *meta.Meta) {} 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 := ioutil.ReadAll(f) return string(src), err } func checkFileContent(t *testing.T, filename string, gotContent string) { 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() if enc := encoder.Create(format); 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") var sb strings.Builder zmkEncoder.WriteBlocks(&sb, zn.Ast) gotFirst := sb.String() sb.Reset() newZettel := parser.ParseZettel(domain.Zettel{ Meta: zn.Zettel.Meta, Content: domain.NewContent("\n" + gotFirst)}, "") zmkEncoder.WriteBlocks(&sb, newZettel.Ast) gotSecond := sb.String() sb.Reset() if gotFirst != gotSecond { t.Errorf("\n1st: %q\n2nd: %q", gotFirst, gotSecond) } } func TestContentRegression(t *testing.T) { wd, err := os.Getwd() if err != nil { panic(err) } root, places := getFilePlaces(wd, "content") for _, p := range places { ss := p.(place.StartStopper) if err := ss.Start(context.Background()); err != nil { panic(err) } placeName := p.Location()[len("dir://")+len(root):] metaList, err := p.SelectMeta(context.Background(), nil, nil) 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, "") 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 checkMetaFile(t *testing.T, resultName string, zn *ast.ZettelNode, format string) { t.Helper() if enc := encoder.Create(format); enc != nil { var sb strings.Builder enc.WriteMeta(&sb, zn.Zettel.Meta) checkFileContent(t, resultName, sb.String()) return } panic(fmt.Sprintf("Unknown writer format %q", format)) } func TestMetaRegression(t *testing.T) { wd, err := os.Getwd() if err != nil { panic(err) } root, places := getFilePlaces(wd, "meta") for _, p := range places { ss := p.(place.StartStopper) if err := ss.Start(context.Background()); err != nil { panic(err) } placeName := p.Location()[len("dir://")+len(root):] metaList, err := p.SelectMeta(context.Background(), nil, nil) 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, "") 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) } } } |
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"}]}]}] |
Added tests/result/content/link/20200215204700.html.
> > > > > > | 1 2 3 4 5 6 | <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></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" []] |
Added tests/result/content/link/20200215204700.text.
> | 1 | Home Config Frag |
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":"Header Test","role":"zettel","syntax":"zmk","copyright":"(c) 2020 Detlef Stern","license":"CC BY-SA 4.0"} |
Changes to tests/result/meta/copyright/20200310125800.native.
|
| | | 1 2 3 4 5 6 | [Title "Header Test"] [Role "zettel"] [Syntax "zmk"] [Header [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":"Header Test","role":"zettel","syntax":"zmk","x-no":"00000000000000"} |
Changes to tests/result/meta/header/20200310125800.native.
|
| | | 1 2 3 4 5 | [Title "Header Test"] [Role "zettel"] [Syntax "zmk"] [Header [x-no "00000000000000"]] |
Deleted tests/result/meta/header/20200310125800.zjson.
|
| < |
Added tests/result/meta/title/20200310110300.djson.
> | 1 | {"title":"A \"\"Title\"\" with //Markup//, ``Zettelmarkup``{=zmk}","role":"zettel","syntax":"zmk"} |
Changes to tests/result/meta/title/20200310110300.html.
|
| | | 1 2 3 | <meta name="zs-title" content="A ""Title"" with //Markup//, ``Zettelmarkup``{=zmk}"> <meta name="zs-role" content="zettel"> <meta name="zs-syntax" content="zmk"> |
Changes to tests/result/meta/title/20200310110300.native.
|
| | | 1 2 3 | [Title "A \"\"Title\"\" with //Markup//, ``Zettelmarkup``{=zmk}"] [Role "zettel"] [Syntax "zmk"] |
Changes to tests/result/meta/title/20200310110300.text.
|
| | | 1 2 3 | A ""Title"" with //Markup//, ``Zettelmarkup``{=zmk} zettel zmk |
Deleted tests/result/meta/title/20200310110300.zjson.
|
| < |
Deleted tools/build/build.go.
|
| < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < |
Deleted tools/check/check.go.
|
| < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < |
Deleted tools/clean/clean.go.
|
| < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < |
Deleted tools/devtools/devtools.go.
|
| < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < |
Deleted tools/htmllint/htmllint.go.
|
| < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < |
Deleted tools/testapi/testapi.go.
|
| < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < |
Deleted tools/tools.go.
|
| < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < |
Added tools/version.go.
> > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 | package main import ( "bytes" "errors" "fmt" "io/ioutil" "os" "os/exec" "regexp" "strings" ) func readVersionFile() (string, error) { content, err := ioutil.ReadFile("VERSION") if err != nil { return "", err } return strings.TrimFunc(string(content), func(r rune) bool { return r <= ' ' }), nil } var fossilHash = regexp.MustCompile("\\[[0-9a-fA-F]+\\]") var dirtyPrefixes = []string{"DELETED", "ADDED", "UPDATED", "CONFLICT", "EDITED", "RENAMED"} func readFossilVersion() (string, error) { var out bytes.Buffer cmd := exec.Command("fossil", "timeline", "--limit", "1") cmd.Stdin = nil cmd.Stdout = &out cmd.Stderr = os.Stderr if err := cmd.Run(); err != nil { return "", err } hash := fossilHash.FindString(out.String()) if len(hash) < 3 { return "", errors.New("No fossil hash found") } hash = hash[1 : len(hash)-1] out.Reset() cmd = exec.Command("fossil", "status") cmd.Stdin = nil cmd.Stdout = &out cmd.Stderr = os.Stderr if err := cmd.Run(); err != nil { return "", err } lines := strings.FieldsFunc(out.String(), func(r rune) bool { return r == '\n' || r == '\r' }) for _, line := range lines { for _, prefix := range dirtyPrefixes { if strings.HasPrefix(line, prefix) { return hash + "-dirty", nil } } } return hash, nil } func main() { base, err := readVersionFile() if err != nil { fmt.Fprintf(os.Stderr, "No VERSION found: %v\n", err) base = "dev" } fossil, err := readFossilVersion() if err != nil { fmt.Print(base) } fmt.Printf("%v+%v", base, fossil) } |
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 | //----------------------------------------------------------------------------- // 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" "math/rand" "time" "zettelstore.de/z/auth/cred" "zettelstore.de/z/auth/token" "zettelstore.de/z/domain/id" "zettelstore.de/z/domain/meta" "zettelstore.de/z/place" ) // 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, f *place.Filter, s *place.Sorter) ([]*meta.Meta, error) } // Authenticate is the data for this use case. type Authenticate struct { port AuthenticatePort ucGetUser GetUser } // NewAuthenticate creates a new use case. func NewAuthenticate(port AuthenticatePort) Authenticate { return Authenticate{ port: port, ucGetUser: NewGetUser(port), } } // Run executes the use case. func (uc Authenticate) Run(ctx context.Context, ident string, credential string, d time.Duration, k token.Kind) ([]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 := 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/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 | //----------------------------------------------------------------------------- // 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/domain" "zettelstore.de/z/domain/meta" ) // 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) } return domain.Zettel{Meta: m, Content: origZettel.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 | //----------------------------------------------------------------------------- // 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/config/runtime" "zettelstore.de/z/domain" "zettelstore.de/z/domain/id" "zettelstore.de/z/domain/meta" ) // 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 { port CreateZettelPort } // NewCreateZettel creates a new use case. func NewCreateZettel(port CreateZettelPort) CreateZettel { return CreateZettel{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, runtime.GetDefaultTitle()) } if role, ok := m.Get(meta.KeyRole); !ok || role == "" { m.Set(meta.KeyRole, runtime.GetDefaultRole()) } if syntax, ok := m.Get(meta.KeySyntax); !ok || syntax == "" { m.Set(meta.KeySyntax, runtime.GetDefaultSyntax()) } m.YamlSep = runtime.GetYAMLHeader() 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 | //----------------------------------------------------------------------------- // 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/runtime" "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{} // NewFolgeZettel creates a new use case. func NewFolgeZettel() FolgeZettel { return FolgeZettel{} } // 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, runtime.GetRole(origMeta)) m.Set(meta.KeyTags, origMeta.GetDefault(meta.KeyTags, "")) m.Set(meta.KeySyntax, runtime.GetSyntax(origMeta)) 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 103 104 105 106 107 | //----------------------------------------------------------------------------- // 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/startup" "zettelstore.de/z/domain/id" "zettelstore.de/z/domain/meta" "zettelstore.de/z/index" "zettelstore.de/z/place" ) // 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, f *place.Filter, s *place.Sorter) ([]*meta.Meta, error) } // GetUser is the data for this use case. type GetUser struct { port GetUserPort } // NewGetUser creates a new use case. func NewGetUser(port GetUserPort) GetUser { return GetUser{port: port} } // Run executes the use case. func (uc GetUser) Run(ctx context.Context, ident string) (*meta.Meta, error) { if !startup.WithAuth() { return nil, nil } ctx = index.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, startup.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. filter := place.Filter{ Expr: map[string][]string{ meta.KeyRole: []string{meta.ValueRoleUser}, meta.KeyUserID: []string{ident}, }, } metaList, err := uc.port.SelectMeta(ctx, &filter, nil) 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} } // Run executes the use case. func (uc GetUserByZid) Run(ctx context.Context, zid id.Zid, ident string) (*meta.Meta, error) { userMeta, err := uc.port.GetMeta(index.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 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 ( "context" "zettelstore.de/z/domain/meta" "zettelstore.de/z/place" ) // ListMetaPort is the interface used by this use case. type ListMetaPort interface { // SelectMeta returns all zettel meta data that match the selection // criteria. The result is ordered by descending zettel id. SelectMeta(ctx context.Context, f *place.Filter, s *place.Sorter) ([]*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, f *place.Filter, s *place.Sorter) ([]*meta.Meta, error) { return uc.port.SelectMeta(ctx, f, 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 58 | //----------------------------------------------------------------------------- // 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/index" "zettelstore.de/z/place" ) // ListRolePort is the interface used by this use case. type ListRolePort interface { // SelectMeta returns all zettel meta data that match the selection // criteria. The result is ordered by descending zettel id. SelectMeta(ctx context.Context, f *place.Filter, s *place.Sorter) ([]*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(index.NoEnrichContext(ctx), nil, 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 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/domain/meta" "zettelstore.de/z/index" "zettelstore.de/z/place" ) // ListTagsPort is the interface used by this use case. type ListTagsPort interface { // SelectMeta returns all zettel meta data that match the selection // criteria. The result is ordered by descending zettel id. SelectMeta(ctx context.Context, f *place.Filter, s *place.Sorter) ([]*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(index.NoEnrichContext(ctx), nil, 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 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 ( "zettelstore.de/z/domain" "zettelstore.de/z/domain/meta" ) // 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() if role, ok := m.Get(meta.KeyRole); ok && role == meta.ValueRoleNewTemplate { 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) } } } return domain.Zettel{Meta: m, Content: origZettel.Content} } |
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 | //----------------------------------------------------------------------------- // 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/domain/id" "zettelstore.de/z/parser" ) // ParseZettel is the data for this use case. type ParseZettel struct { getZettel GetZettel } // NewParseZettel creates a new use case. func NewParseZettel(getZettel GetZettel) ParseZettel { return ParseZettel{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), nil } |
Deleted usecase/query.go.
|
| < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < |
Deleted usecase/refresh.go.
|
| < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < |
Deleted usecase/reindex.go.
|
| < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < |
Added usecase/reload.go.
> > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 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 Detlef Stern // // This file is part of zettelstore. // // Zettelstore is licensed under 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" ) // ReloadPort is the interface used by this use case. type ReloadPort interface { // Reload clears all caches, reloads all internal data to reflect changes // that were possibly undetected. Reload(ctx context.Context) error } // Reload is the data for this use case. type Reload struct { port ReloadPort } // NewReload creates a new use case. func NewReload(port ReloadPort) Reload { return Reload{port: port} } // Run executes the use case. func (uc Reload) Run(ctx context.Context) error { return uc.port.Reload(ctx) } |
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 | //----------------------------------------------------------------------------- // 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/index" ) // 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 := index.NoEnrichContext(ctx) if _, err := uc.port.GetMeta(noEnrichCtx, curZid); err != nil { return err } 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 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/meta" "zettelstore.de/z/index" "zettelstore.de/z/place" ) // SearchPort is the interface used by this use case. type SearchPort interface { // SelectMeta returns all zettel meta data that match the selection // criteria. The result is ordered by descending zettel id. SelectMeta(ctx context.Context, f *place.Filter, s *place.Sorter) ([]*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, f *place.Filter, s *place.Sorter) ([]*meta.Meta, error) { // TODO: interpret f.Expr[""]. Can contain expressions for specific meta tags. if !usesComputedMeta(f, s) { ctx = index.NoEnrichContext(ctx) } return uc.port.SelectMeta(ctx, f, s) } func usesComputedMeta(f *place.Filter, s *place.Sorter) bool { if f != nil { for key := range f.Expr { if key == "" || meta.IsComputed(key) { return true } } } if s != nil { if order := s.Order; order != "" && meta.IsComputed(order) { return true } } return false } |
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 | //----------------------------------------------------------------------------- // 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/index" ) // 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(index.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 = oldZettel.Content } return uc.port.UpdateZettel(ctx, zettel) } |
Deleted usecase/usecase.go.
|
| < < < < < < < < < < < < < < < |
Deleted usecase/version.go.
|
| < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < |
Deleted web/adapter/adapter.go.
|
| < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < |
Deleted web/adapter/api/api.go.
|
| < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < |
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 | //----------------------------------------------------------------------------- // 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 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 } q := r.URL.Query() zn, err := parseZettel.Run(r.Context(), 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: adapter.NewURLBuilder('z').SetZid(zid).String(), } if kind&kindLink != 0 { if matter&matterIncoming != 0 { // Backlinks not yet implemented outData.Links.Incoming = []jsonIDURL{} } zetRefs, locRefs, extRefs := collect.DivideReferences(summary.Links, false) if matter&matterOutgoing != 0 { outData.Links.Outgoing = idURLRefs(zetRefs) } if matter&matterLocal != 0 { outData.Links.Local = stringRefs(locRefs) } if matter&matterExternal != 0 { outData.Links.External = stringRefs(extRefs) } } if kind&kindImage != 0 { zetRefs, locRefs, extRefs := collect.DivideReferences(summary.Images, false) if matter&matterOutgoing != 0 { outData.Images.Outgoing = idURLRefs(zetRefs) } if matter&matterLocal != 0 { outData.Images.Local = stringRefs(locRefs) } if matter&matterExternal != 0 { outData.Images.External = stringRefs(extRefs) } } if kind&kindCite != 0 { outData.Cites = stringCites(summary.Cites) } w.Header().Set("Content-Type", format2ContentType("json")) enc := json.NewEncoder(w) enc.SetEscapeHTML(false) err = enc.Encode(&outData) } } func idURLRefs(refs []*ast.Reference) []jsonIDURL { result := make([]jsonIDURL, 0, len(refs)) for _, ref := range refs { path := ref.URL.Path ub := adapter.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 { if matter == 0 { return false } return true } 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_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 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("Content-Type", 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 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("Content-Type", 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 | //----------------------------------------------------------------------------- // 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/config/runtime" "zettelstore.de/z/domain/id" "zettelstore.de/z/domain/meta" "zettelstore.de/z/encoder" "zettelstore.de/z/usecase" "zettelstore.de/z/web/adapter" ) // MakeGetZettelHandler creates a new HTTP handler to return a rendered zettel. func 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() zn, err := parseZettel.Run(ctx, zid, q.Get("syntax")) if err != nil { adapter.ReportUsecaseError(w, err) return } format := adapter.GetFormat(r, q, encoder.GetDefaultFormat()) part := getPart(q, partZettel) switch format { case "json", "djson": if part == partUnknown { adapter.BadRequest(w, "Unknown _part parameter") return } w.Header().Set("Content-Type", format2ContentType(format)) if format != "djson" { err = writeJSONZettel(w, zn, part) } else { err = writeDJSONZettel(ctx, w, zn, part, partZettel, getMeta) } if err != nil { adapter.InternalServerError(w, "Write D/JSON", err) } return } langOption := encoder.StringOption{Key: "lang", Value: runtime.GetLang(zn.InhMeta)} linkAdapter := encoder.AdaptLinkOption{ Adapter: adapter.MakeLinkAdapter(ctx, 'z', getMeta, part.DefString(partZettel), format), } imageAdapter := encoder.AdaptImageOption{Adapter: adapter.MakeImageAdapter()} switch part { case partZettel: inhMeta := false if format != "raw" { w.Header().Set("Content-Type", format2ContentType(format)) inhMeta = true } enc := encoder.Create(format, &langOption, &linkAdapter, &imageAdapter, &encoder.StringsOption{ Key: "no-meta", Value: []string{ meta.KeyLang, }, }, ) if enc == nil { err = adapter.ErrNoSuchFormat } else { _, err = enc.WriteZettel(w, zn, inhMeta) } case partMeta: w.Header().Set("Content-Type", format2ContentType(format)) if format == "raw" { // Don't write inherited meta data, just the raw err = writeMeta(w, zn.Zettel.Meta, format) } else { err = writeMeta(w, zn.InhMeta, format) } case partContent: if format == "raw" { if ct, ok := syntax2contentType(runtime.GetSyntax(zn.Zettel.Meta)); ok { w.Header().Add("Content-Type", ct) } } else { w.Header().Set("Content-Type", format2ContentType(format)) } err = writeContent(w, zn, format, &langOption, &encoder.StringOption{ Key: meta.KeyMarkerExternal, Value: runtime.GetMarkerExternal()}, &linkAdapter, &imageAdapter, ) default: adapter.BadRequest(w, "Unknown _part parameter") return } if err != nil { if 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) } } } |
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 | //----------------------------------------------------------------------------- // 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/config/runtime" "zettelstore.de/z/domain/meta" "zettelstore.de/z/encoder" "zettelstore.de/z/index" "zettelstore.de/z/parser" "zettelstore.de/z/usecase" "zettelstore.de/z/web/adapter" ) // MakeListMetaHandler creates a new HTTP handler for the use case "list some zettel". func 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() filter, sorter := adapter.GetFilterSorter(q, false) format := adapter.GetFormat(r, q, encoder.GetDefaultFormat()) part := getPart(q, partMeta) ctx1 := ctx if format == "html" || (filter == nil && sorter == nil && (part == partID || part == partContent)) { ctx1 = index.NoEnrichContext(ctx1) } metaList, err := listMeta.Run(ctx1, filter, sorter) if err != nil { adapter.ReportUsecaseError(w, err) return } w.Header().Set("Content-Type", format2ContentType(format)) switch format { case "html": renderListMetaHTML(w, metaList) case "json", "djson": 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 renderListMetaHTML(w http.ResponseWriter, metaList []*meta.Meta) { buf := encoder.NewBufWriter(w) buf.WriteStrings("<html lang=\"", runtime.GetDefaultLang(), "\">\n<body>\n<ul>\n") for _, m := range metaList { title := m.GetDefault(meta.KeyTitle, "") htmlTitle, err := adapter.FormatInlines(parser.ParseTitle(title), "html") if err != nil { adapter.InternalServerError(w, "Format HTML inlines", err) return } buf.WriteStrings( "<li><a href=\"", adapter.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 | //----------------------------------------------------------------------------- // 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/config/runtime" "zettelstore.de/z/domain" "zettelstore.de/z/domain/id" "zettelstore.de/z/domain/meta" "zettelstore.de/z/encoder" "zettelstore.de/z/parser" "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 jsonContent struct { ID string `json:"id"` URL string `json:"url"` Encoding string `json:"encoding"` Content interface{} `json:"content"` } func writeJSONZettel(w http.ResponseWriter, z *ast.ZettelNode, part partType) error { var outData interface{} idData := jsonIDURL{ ID: z.Zid.String(), URL: adapter.NewURLBuilder('z').SetZid(z.Zid).String(), } switch part { case partZettel: encoding, content := encodedContent(z.Zettel.Content) outData = jsonZettel{ ID: idData.ID, URL: idData.URL, Meta: z.InhMeta.Map(), Encoding: encoding, Content: content, } case partMeta: outData = jsonMeta{ ID: idData.ID, URL: idData.URL, Meta: z.InhMeta.Map(), } case partContent: encoding, content := encodedContent(z.Zettel.Content) outData = jsonContent{ ID: idData.ID, URL: idData.URL, Encoding: encoding, Content: content, } case partID: outData = idData default: panic(part) } enc := json.NewEncoder(w) enc.SetEscapeHTML(false) return enc.Encode(outData) } func encodedContent(content domain.Content) (string, interface{}) { if content.IsBinary() { return "base64", content.AsBytes() } return "", content.AsString() } func writeDJSONZettel( ctx context.Context, w http.ResponseWriter, z *ast.ZettelNode, part, defPart partType, getMeta usecase.GetMeta, ) (err error) { switch part { case partZettel: err = writeDJSONHeader(w, z.Zid) if err == nil { err = writeDJSONMeta(w, z) } if err == nil { err = writeDJSONContent(ctx, w, z, part, defPart, getMeta) } case partMeta: err = writeDJSONHeader(w, z.Zid) if err == nil { err = writeDJSONMeta(w, z) } case partContent: err = writeDJSONHeader(w, z.Zid) if err == nil { err = writeDJSONContent(ctx, w, z, part, defPart, getMeta) } case partID: writeDJSONHeader(w, z.Zid) default: panic(part) } if err == nil { _, err = w.Write(djsonFooter) } return err } var ( djsonMetaHeader = []byte(",\"meta\":") djsonContentHeader = []byte(",\"content\":") djsonHeader1 = []byte("{\"id\":\"") djsonHeader2 = []byte("\",\"url\":\"") djsonHeader3 = []byte("?_format=") djsonHeader4 = []byte("\"") djsonFooter = []byte("}") ) func writeDJSONHeader(w http.ResponseWriter, 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, adapter.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 writeDJSONMeta(w io.Writer, z *ast.ZettelNode) error { _, err := w.Write(djsonMetaHeader) if err == nil { err = writeMeta(w, z.InhMeta, "djson", &encoder.TitleOption{Inline: z.Title}) } return err } func writeDJSONContent( ctx context.Context, w io.Writer, z *ast.ZettelNode, part, defPart partType, getMeta usecase.GetMeta, ) (err error) { _, err = w.Write(djsonContentHeader) if err == nil { err = writeContent(w, z, "djson", &encoder.AdaptLinkOption{ Adapter: adapter.MakeLinkAdapter(ctx, 'z', getMeta, part.DefString(defPart), "djson"), }, &encoder.AdaptImageOption{Adapter: adapter.MakeImageAdapter()}, ) } return err } var ( jsonListHeader = []byte("{\"list\":[") jsonListSep = []byte{','} jsonListFooter = []byte("]}") ) var setJSON = map[string]bool{"json": true} func renderListMetaXJSON( ctx context.Context, w http.ResponseWriter, metaList []*meta.Meta, format string, part, defPart partType, getMeta usecase.GetMeta, parseZettel usecase.ParseZettel, ) { var readZettel bool switch part { case partZettel, partContent: readZettel = true case partMeta, partID: readZettel = false default: adapter.BadRequest(w, "Unknown _part parameter") return } isJSON := setJSON[format] _, err := w.Write(jsonListHeader) for i, m := range metaList { if err != nil { break } if i > 0 { _, err = w.Write(jsonListSep) } if err != nil { break } var zn *ast.ZettelNode if readZettel { z, err1 := parseZettel.Run(ctx, m.Zid, "") if err1 != nil { err = err1 break } zn = z } else { zn = &ast.ZettelNode{ Zettel: domain.Zettel{Meta: m, Content: ""}, Zid: m.Zid, InhMeta: runtime.AddDefaultValues(m), Title: parser.ParseTitle( m.GetDefault(meta.KeyTitle, runtime.GetDefaultTitle())), Ast: nil, } } if isJSON { err = writeJSONZettel(w, zn, part) } else { err = writeDJSONZettel(ctx, w, zn, part, defPart, getMeta) } } if err == nil { _, err = w.Write(jsonListFooter) } if err != nil { adapter.InternalServerError(w, "Get list", err) } } func writeContent( w io.Writer, zn *ast.ZettelNode, format string, options ...encoder.Option) error { enc := encoder.Create(format, options...) if enc == nil { return adapter.ErrNoSuchFormat } _, err := enc.WriteContent(w, zn) return err } func writeMeta( w io.Writer, m *meta.Meta, format string, options ...encoder.Option) error { enc := encoder.Create(format, options...) if enc == nil { return adapter.ErrNoSuchFormat } _, err := enc.WriteMeta(w, m) return err } |
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 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 | //----------------------------------------------------------------------------- // 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 import ( "encoding/json" "net/http" "time" "zettelstore.de/z/auth/token" "zettelstore.de/z/config/startup" "zettelstore.de/z/usecase" "zettelstore.de/z/web/adapter" "zettelstore.de/z/web/session" ) // MakePostLoginHandlerAPI creates a new HTTP handler to authenticate the given user via API. func MakePostLoginHandlerAPI(auth usecase.Authenticate) http.HandlerFunc { return func(w http.ResponseWriter, r *http.Request) { if !startup.WithAuth() { w.Header().Set("Content-Type", format2ContentType("json")) writeJSONToken(w, "freeaccess", 24*366*10*time.Hour) return } _, apiDur := startup.TokenLifetime() authenticateViaJSON(auth, w, r, apiDur) } } func authenticateViaJSON( auth usecase.Authenticate, w http.ResponseWriter, r *http.Request, authDuration time.Duration, ) { token, err := authenticateForJSON(auth, w, r, authDuration) if err != nil { adapter.ReportUsecaseError(w, err) return } if token == nil { w.Header().Set("WWW-Authenticate", `Bearer realm="Default"`) http.Error(w, "Authentication failed", http.StatusUnauthorized) return } w.Header().Set("Content-Type", format2ContentType("json")) writeJSONToken(w, string(token), authDuration) } func authenticateForJSON( auth usecase.Authenticate, w http.ResponseWriter, r *http.Request, authDuration time.Duration, ) ([]byte, error) { ident, cred, ok := adapter.GetCredentialsViaForm(r) if !ok { if ident, cred, ok = r.BasicAuth(); !ok { return nil, nil } } token, err := auth.Run(r.Context(), ident, cred, authDuration, token.KindJSON) return token, err } 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 MakeRenewAuthHandler() http.HandlerFunc { return func(w http.ResponseWriter, r *http.Request) { ctx := r.Context() auth := session.GetAuthData(ctx) if auth == nil || auth.Token == nil || auth.User == nil { adapter.BadRequest(w, "Not authenticated") return } totalLifetime := auth.Expires.Sub(auth.Issued) currentLifetime := auth.Now.Sub(auth.Issued) // If we are in the first quarter of the tokens lifetime, return the token if currentLifetime*4 < totalLifetime { w.Header().Set("Content-Type", format2ContentType("json")) writeJSONToken(w, string(auth.Token), totalLifetime-currentLifetime) return } // Toke is a little bit aged. Create a new one _, apiDur := startup.TokenLifetime() token, err := token.GetToken(auth.User, apiDur, token.KindJSON) if err != nil { adapter.ReportUsecaseError(w, err) return } w.Header().Set("Content-Type", format2ContentType("json")) writeJSONToken(w, string(token), apiDur) } } |
Deleted web/adapter/api/query.go.
|
| < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < |
Added web/adapter/api/reload.go.
> > > > > > > > > > > > > > > > > > > > > > | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 | //----------------------------------------------------------------------------- // Copyright (c) 2020 Detlef Stern // // This file is part of zettelstore. // // Zettelstore is licensed under 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" ) // ReloadHandlerAPI creates a new HTTP handler for the use case "reload". func ReloadHandlerAPI(w http.ResponseWriter, r *http.Request, format string) { w.Header().Set("Content-Type", format2ContentType(format)) w.WriteHeader(http.StatusNoContent) } |
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 len(p) == 0 { 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 | //----------------------------------------------------------------------------- // 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/index" "zettelstore.de/z/place" "zettelstore.de/z/usecase" ) // 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, options ...encoder.Option) (string, error) { enc := encoder.Create(format, options...) 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, 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 || origRef.State != ast.RefStateZettel { return origLink } zid, err := id.Parse(origRef.URL.Path) if err != nil { panic(err) } _, err = getMeta.Run(index.NoEnrichContext(ctx), zid) newLink := *origLink if err == nil { u := NewURLBuilder(key).SetZid(zid) if part != "" { u.AppendQuery("_part", part) } if format != "" { u.AppendQuery("_format", format) } if fragment := origRef.URL.EscapedFragment(); len(fragment) > 0 { u.SetFragment(fragment) } newRef := ast.ParseReference(u.String()) newRef.State = ast.RefStateZettelFound newLink.Ref = newRef return &newLink } if place.IsErrNotAllowed(err) { return &ast.FormatNode{ Code: ast.FormatSpan, Attrs: origLink.Attrs, Inlines: origLink.Inlines, } } newRef := ast.ParseReference(origRef.Value) newRef.State = ast.RefStateZettelBroken newLink.Ref = newRef return &newLink } } // MakeImageAdapter creates an adapter to change an image node during encoding. func MakeImageAdapter() func(*ast.ImageNode) ast.InlineNode { return func(origImage *ast.ImageNode) ast.InlineNode { if origImage.Ref == nil || origImage.Ref.State != ast.RefStateZettel { return origImage } newImage := *origImage zid, err := id.Parse(newImage.Ref.Value) if err != nil { panic(err) } newImage.Ref = ast.ParseReference( NewURLBuilder('z').SetZid(zid).AppendQuery("_part", "content").AppendQuery( "_format", "raw").String()) newImage.Ref.State = ast.RefStateZettelFound 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 } |
Added web/adapter/reload.go.
> > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 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 Detlef Stern // // This file is part of zettelstore. // // Zettelstore is licensed under 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" "zettelstore.de/z/encoder" "zettelstore.de/z/usecase" ) // MakeReloadHandler creates a new HTTP handler for the use case "reload". func MakeReloadHandler( reload usecase.Reload, apiHandler func(http.ResponseWriter, *http.Request, string), htmlHandler http.HandlerFunc) http.HandlerFunc { return func(w http.ResponseWriter, r *http.Request) { err := reload.Run(r.Context()) if err != nil { ReportUsecaseError(w, err) return } if format := GetFormat(r, r.URL.Query(), encoder.GetDefaultFormat()); format != "html" { apiHandler(w, r, format) } htmlHandler(w, r) } } |
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 | //----------------------------------------------------------------------------- // 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/place" ) // 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, "Content-Type"); 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 } // GetFilterSorter retrieves the specified filter and sorting options from a query. func GetFilterSorter(q url.Values, forSearch bool) (filter *place.Filter, sorter *place.Sorter) { sortQKey, orderQKey, offsetQKey, limitQKey, negateQKey, sQKey := getQueryKeys(forSearch) for key, values := range q { switch key { case sortQKey, orderQKey: if len(values) > 0 { descending := false sortkey := values[0] if strings.HasPrefix(sortkey, "-") { descending = true sortkey = sortkey[1:] } if meta.KeyIsValid(sortkey) || sortkey == place.RandomOrder { sorter = place.EnsureSorter(sorter) sorter.Order = sortkey sorter.Descending = descending } } case offsetQKey: if len(values) > 0 { if offset, err := strconv.Atoi(values[0]); err == nil { sorter = place.EnsureSorter(sorter) sorter.Offset = offset } } case limitQKey: if len(values) > 0 { if limit, err := strconv.Atoi(values[0]); err == nil { sorter = place.EnsureSorter(sorter) sorter.Limit = limit } } case negateQKey: filter = place.EnsureFilter(filter) filter.Negate = true case sQKey: if values := cleanQueryValues(values); len(values) > 0 { filter = place.EnsureFilter(filter) filter.Expr[""] = values } default: if !forSearch && meta.KeyIsValid(key) { filter = place.EnsureFilter(filter) filter.Expr[key] = cleanQueryValues(values) } } } return filter, sorter } 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 cleanQueryValues(values []string) []string { result := make([]string, 0, len(values)) for _, val := range values { val = strings.TrimSpace(val) if len(val) > 0 { result = append(result, val) } } return result } |
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 | //----------------------------------------------------------------------------- // 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" "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) { if err == place.ErrNotFound { NotFound(w, http.StatusText(404)) return } if err, ok := err.(*place.ErrNotAllowed); ok { Forbidden(w, err.Error()) return } if err, ok := err.(*place.ErrInvalidID); ok { BadRequest(w, fmt.Sprintf("Zettel-ID %q not appropriate in this context.", err.Zid.String())) return } if err, ok := err.(*usecase.ErrZidInUse); ok { BadRequest(w, fmt.Sprintf("Zettel-ID %q already in use.", err.Zid.String())) return } if err == place.ErrStopped { InternalServerError(w, "Zettelstore not operational.", err) return } InternalServerError(w, "", err) } |
Added web/adapter/urlbuilder.go.
> > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 | //----------------------------------------------------------------------------- // 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 ( "net/url" "strings" "zettelstore.de/z/config/startup" "zettelstore.de/z/domain/id" ) type urlQuery struct{ key, val string } // URLBuilder should be used to create zettelstore URLs. type URLBuilder struct { key byte path []string query []urlQuery fragment string } // NewURLBuilder creates a new URLBuilder. func NewURLBuilder(key byte) *URLBuilder { return &URLBuilder{key: key} } // Clone an URLBuilder func (ub *URLBuilder) Clone() *URLBuilder { copy := new(URLBuilder) copy.key = ub.key if len(ub.path) > 0 { copy.path = make([]string, 0, len(ub.path)) } for _, p := range ub.path { copy.path = append(copy.path, p) } if len(ub.query) > 0 { copy.query = make([]urlQuery, 0, len(ub.query)) } for _, q := range ub.query { copy.query = append(copy.query, q) } copy.fragment = ub.fragment return copy } // SetZid sets the zettel identifier. func (ub *URLBuilder) SetZid(zid id.Zid) *URLBuilder { if len(ub.path) > 0 { panic("Cannot add Zid") } ub.path = append(ub.path, zid.String()) return ub } // AppendPath adds a new path element func (ub *URLBuilder) AppendPath(p string) *URLBuilder { ub.path = append(ub.path, p) return ub } // AppendQuery adds a new query parameter func (ub *URLBuilder) AppendQuery(key string, value string) *URLBuilder { ub.query = append(ub.query, urlQuery{key, value}) return ub } // ClearQuery removes all query parameters. func (ub *URLBuilder) ClearQuery() *URLBuilder { ub.query = nil ub.fragment = "" return ub } // SetFragment stores the fragment func (ub *URLBuilder) SetFragment(s string) *URLBuilder { ub.fragment = s return ub } // String produces a string value. func (ub *URLBuilder) String() string { var sb strings.Builder sb.WriteString(startup.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() } |
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 154 155 156 157 | //----------------------------------------------------------------------------- // 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 wet-UI handlers for web requests. package webui import ( "fmt" "net/http" "zettelstore.de/z/config/runtime" "zettelstore.de/z/domain" "zettelstore.de/z/domain/id" "zettelstore.de/z/domain/meta" "zettelstore.de/z/encoder" "zettelstore.de/z/index" "zettelstore.de/z/input" "zettelstore.de/z/parser" "zettelstore.de/z/usecase" "zettelstore.de/z/web/adapter" "zettelstore.de/z/web/session" ) // MakeGetCopyZettelHandler creates a new HTTP handler to display the // HTML edit view of a copied zettel. func MakeGetCopyZettelHandler( te *TemplateEngine, getZettel usecase.GetZettel, copyZettel usecase.CopyZettel, ) http.HandlerFunc { return func(w http.ResponseWriter, r *http.Request) { if origZettel, ok := getOrigZettel(w, r, getZettel, "Copy"); ok { renderZettelForm(w, r, te, 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 MakeGetFolgeZettelHandler( te *TemplateEngine, getZettel usecase.GetZettel, folgeZettel usecase.FolgeZettel, ) http.HandlerFunc { return func(w http.ResponseWriter, r *http.Request) { if origZettel, ok := getOrigZettel(w, r, getZettel, "Folge"); ok { renderZettelForm(w, r, te, folgeZettel.Run(origZettel), "Folge Zettel", "Folgezettel") } } } // MakeGetNewZettelHandler creates a new HTTP handler to display the // HTML edit view of a zettel. func MakeGetNewZettelHandler( te *TemplateEngine, getZettel usecase.GetZettel, newZettel usecase.NewZettel, ) http.HandlerFunc { return func(w http.ResponseWriter, r *http.Request) { if origZettel, ok := getOrigZettel(w, r, getZettel, "New"); ok { m := origZettel.Meta title := parser.ParseInlines( input.NewInput(runtime.GetTitle(m)), meta.ValueSyntaxZmk) langOption := encoder.StringOption{Key: "lang", Value: runtime.GetLang(m)} textTitle, err := adapter.FormatInlines(title, "text", &langOption) if err != nil { adapter.InternalServerError(w, "Format Text inlines for WebUI", err) return } htmlTitle, err := adapter.FormatInlines(title, "html", &langOption) if err != nil { adapter.InternalServerError(w, "Format HTML inlines for WebUI", err) return } renderZettelForm(w, r, te, newZettel.Run(origZettel), textTitle, htmlTitle) } } } func getOrigZettel( w http.ResponseWriter, r *http.Request, getZettel usecase.GetZettel, op string, ) (domain.Zettel, bool) { if format := adapter.GetFormat(r, r.URL.Query(), "html"); format != "html" { adapter.BadRequest(w, fmt.Sprintf("%v zettel not possible in format %q", op, format)) return domain.Zettel{}, false } zid, err := id.Parse(r.URL.Path[1:]) if err != nil { http.NotFound(w, r) return domain.Zettel{}, false } origZettel, err := getZettel.Run(index.NoEnrichContext(r.Context()), zid) if err != nil { http.NotFound(w, r) return domain.Zettel{}, false } return origZettel, true } func renderZettelForm( w http.ResponseWriter, r *http.Request, te *TemplateEngine, zettel domain.Zettel, title string, heading string, ) { ctx := r.Context() user := session.GetUser(ctx) m := zettel.Meta var base baseData te.makeBaseData(ctx, runtime.GetLang(m), title, user, &base) te.renderTemplate(ctx, w, id.FormTemplateZid, &base, formZettelData{ Heading: heading, MetaTitle: runtime.GetTitle(m), MetaTags: m.GetDefault(meta.KeyTags, ""), MetaRole: runtime.GetRole(m), MetaSyntax: runtime.GetSyntax(m), 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 MakePostCreateZettelHandler(createZettel usecase.CreateZettel) http.HandlerFunc { return func(w http.ResponseWriter, r *http.Request) { zettel, hasContent, err := parseZettelForm(r, id.Invalid) if err != nil { adapter.BadRequest(w, "Unable to read form data") return } if !hasContent { adapter.BadRequest(w, "Content is missing") return } if newZid, err := createZettel.Run(r.Context(), zettel); err != nil { adapter.ReportUsecaseError(w, err) } else { http.Redirect(w, r, adapter.NewURLBuilder('h').SetZid(newZid).String(), http.StatusFound) } } } |
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 80 | //----------------------------------------------------------------------------- // 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 webui provides wet-UI handlers for web requests. package webui import ( "fmt" "net/http" "zettelstore.de/z/config/runtime" "zettelstore.de/z/domain/id" "zettelstore.de/z/domain/meta" "zettelstore.de/z/usecase" "zettelstore.de/z/web/adapter" "zettelstore.de/z/web/session" ) // MakeGetDeleteZettelHandler creates a new HTTP handler to display the // HTML delete view of a zettel. func MakeGetDeleteZettelHandler( te *TemplateEngine, getZettel usecase.GetZettel, ) http.HandlerFunc { return func(w http.ResponseWriter, r *http.Request) { if format := adapter.GetFormat(r, r.URL.Query(), "html"); format != "html" { adapter.BadRequest(w, fmt.Sprintf("Delete zettel not possible in format %q", format)) return } zid, err := id.Parse(r.URL.Path[1:]) if err != nil { http.NotFound(w, r) return } ctx := r.Context() zettel, err := getZettel.Run(ctx, zid) if err != nil { adapter.ReportUsecaseError(w, err) return } user := session.GetUser(ctx) m := zettel.Meta var base baseData te.makeBaseData(ctx, runtime.GetLang(m), "Delete Zettel "+m.Zid.String(), user, &base) te.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 MakePostDeleteZettelHandler(deleteZettel usecase.DeleteZettel) http.HandlerFunc { return func(w http.ResponseWriter, r *http.Request) { zid, err := id.Parse(r.URL.Path[1:]) if err != nil { http.NotFound(w, r) return } if err := deleteZettel.Run(r.Context(), zid); err != nil { adapter.ReportUsecaseError(w, err) return } http.Redirect(w, r, adapter.NewURLBuilder('/').String(), http.StatusFound) } } |
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 wet-UI handlers for web requests. package webui import ( "fmt" "net/http" "zettelstore.de/z/config/runtime" "zettelstore.de/z/domain/id" "zettelstore.de/z/domain/meta" "zettelstore.de/z/index" "zettelstore.de/z/usecase" "zettelstore.de/z/web/adapter" "zettelstore.de/z/web/session" ) // MakeEditGetZettelHandler creates a new HTTP handler to display the // HTML edit view of a zettel. func MakeEditGetZettelHandler( te *TemplateEngine, getZettel usecase.GetZettel) 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() zettel, err := getZettel.Run(index.NoEnrichContext(ctx), zid) if err != nil { adapter.ReportUsecaseError(w, err) return } if format := adapter.GetFormat(r, r.URL.Query(), "html"); format != "html" { adapter.BadRequest(w, fmt.Sprintf("Edit zettel %q not possible in format %q", zid.String(), format)) return } user := session.GetUser(ctx) m := zettel.Meta var base baseData te.makeBaseData(ctx, runtime.GetLang(m), "Edit Zettel", user, &base) te.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 MakeEditSetZettelHandler(updateZettel usecase.UpdateZettel) http.HandlerFunc { return func(w http.ResponseWriter, r *http.Request) { zid, err := id.Parse(r.URL.Path[1:]) if err != nil { http.NotFound(w, r) return } zettel, hasContent, err := parseZettelForm(r, zid) if err != nil { adapter.BadRequest(w, "Unable to read zettel form") return } if err := updateZettel.Run(r.Context(), zettel, hasContent); err != nil { adapter.ReportUsecaseError(w, err) return } http.Redirect( w, r, adapter.NewURLBuilder('h').SetZid(zid).String(), http.StatusFound) } } |
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 Detlef Stern // // This file is part of zettelstore. // // Zettelstore is licensed under 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 wet-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 186 187 188 | //----------------------------------------------------------------------------- // 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 wet-UI handlers for web requests. package webui import ( "fmt" "net/http" "strings" "zettelstore.de/z/ast" "zettelstore.de/z/collect" "zettelstore.de/z/config/runtime" "zettelstore.de/z/domain/id" "zettelstore.de/z/domain/meta" "zettelstore.de/z/encoder" "zettelstore.de/z/usecase" "zettelstore.de/z/web/adapter" "zettelstore.de/z/web/session" ) type metaDataInfo struct { Key string Value string } type zettelReference struct { Zid id.Zid Title string HasURL bool URL 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 MakeGetInfoHandler( te *TemplateEngine, parseZettel usecase.ParseZettel, getMeta usecase.GetMeta, ) http.HandlerFunc { return func(w http.ResponseWriter, r *http.Request) { q := r.URL.Query() if format := adapter.GetFormat(r, q, "html"); format != "html" { adapter.BadRequest(w, fmt.Sprintf("Zettel info not available in format %q", format)) return } zid, err := id.Parse(r.URL.Path[1:]) if err != nil { http.NotFound(w, r) return } ctx := r.Context() zn, err := parseZettel.Run(ctx, zid, q.Get("syntax")) if err != nil { adapter.ReportUsecaseError(w, err) return } langOption := &encoder.StringOption{ Key: "lang", Value: runtime.GetLang(zn.InhMeta)} summary := collect.References(zn) locLinks, extLinks := splitLocExtLinks(append(summary.Links, summary.Images...)) textTitle, err := adapter.FormatInlines(zn.Title, "text", nil, langOption) if err != nil { adapter.InternalServerError(w, "Format Text inlines for info", err) return } pairs := zn.Zettel.Meta.Pairs(true) metaData := make([]metaDataInfo, 0, len(pairs)) getTitle := makeGetTitle(ctx, getMeta, langOption) for _, p := range pairs { var html strings.Builder writeHTMLMetaValue(&html, zn.Zettel.Meta, p.Key, getTitle, langOption) metaData = append(metaData, metaDataInfo{p.Key, html.String()}) } formats := encoder.GetFormats() defFormat := encoder.GetDefaultFormat() parts := []string{"zettel", "meta", "content"} matrix := make([]matrixLine, 0, len(parts)) u := adapter.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}) } user := session.GetUser(ctx) var base baseData te.makeBaseData(ctx, langOption.Value, textTitle, user, &base) canCopy := base.CanCreate && !zn.Zettel.Content.IsBinary() te.renderTemplate(ctx, w, id.InfoTemplateZid, &base, struct { Zid string WebURL string CanWrite bool EditURL string CanFolge bool FolgeURL string CanCopy bool CopyURL string CanNew bool NewURL string CanRename bool RenameURL string CanDelete bool DeleteURL string MetaData []metaDataInfo HasLinks bool HasLocLinks bool LocLinks []string HasExtLinks bool ExtLinks []string ExtNewWindow string Matrix []matrixLine }{ Zid: zid.String(), WebURL: adapter.NewURLBuilder('h').SetZid(zid).String(), CanWrite: te.canWrite(ctx, user, zn.Zettel), EditURL: adapter.NewURLBuilder('e').SetZid(zid).String(), CanFolge: base.CanCreate && !zn.Zettel.Content.IsBinary(), FolgeURL: adapter.NewURLBuilder('f').SetZid(zid).String(), CanCopy: canCopy, CopyURL: adapter.NewURLBuilder('c').SetZid(zid).String(), CanNew: canCopy && zn.Zettel.Meta.GetDefault(meta.KeyRole, "") == meta.ValueRoleNewTemplate, NewURL: adapter.NewURLBuilder('n').SetZid(zid).String(), CanRename: te.canRename(ctx, user, zn.Zettel.Meta), RenameURL: adapter.NewURLBuilder('r').SetZid(zid).String(), CanDelete: te.canDelete(ctx, user, zn.Zettel.Meta), DeleteURL: adapter.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: matrix, }) } } func splitLocExtLinks(links []*ast.Reference) (locLinks []string, extLinks []string) { if len(links) == 0 { return nil, nil } for _, ref := range links { if ref.State == ast.RefStateZettelSelf { continue } if ref.IsZettel() { continue } else if ref.IsExternal() { extLinks = append(extLinks, ref.String()) } else { locLinks = append(locLinks, ref.String()) } } return locLinks, extLinks } |
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 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 | //----------------------------------------------------------------------------- // 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 wet-UI handlers for web requests. package webui import ( "bytes" "net/http" "strings" "zettelstore.de/z/ast" "zettelstore.de/z/config/runtime" "zettelstore.de/z/domain/id" "zettelstore.de/z/domain/meta" "zettelstore.de/z/encoder" "zettelstore.de/z/usecase" "zettelstore.de/z/web/adapter" "zettelstore.de/z/web/session" ) // MakeGetHTMLZettelHandler creates a new HTTP handler for the use case "get zettel". func MakeGetHTMLZettelHandler( te *TemplateEngine, 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() syntax := r.URL.Query().Get("syntax") zn, err := parseZettel.Run(ctx, zid, syntax) if err != nil { adapter.ReportUsecaseError(w, err) return } metaHeader, err := formatMeta( zn.InhMeta, "html", &encoder.StringsOption{ Key: "no-meta", Value: []string{meta.KeyTitle, meta.KeyLang}, }, ) if err != nil { adapter.InternalServerError(w, "Format meta", err) return } langOption := encoder.StringOption{Key: "lang", Value: runtime.GetLang(zn.InhMeta)} htmlTitle, err := adapter.FormatInlines(zn.Title, "html", &langOption) if err != nil { adapter.InternalServerError(w, "Format HTML inlines", err) return } textTitle, err := adapter.FormatInlines(zn.Title, "text", &langOption) if err != nil { adapter.InternalServerError(w, "Format text inlines", err) return } newWindow := true htmlContent, err := formatBlocks( zn.Ast, "html", &langOption, &encoder.StringOption{ Key: meta.KeyMarkerExternal, Value: runtime.GetMarkerExternal()}, &encoder.BoolOption{Key: "newwindow", Value: newWindow}, &encoder.AdaptLinkOption{ Adapter: adapter.MakeLinkAdapter(ctx, 'h', getMeta, "", ""), }, &encoder.AdaptImageOption{Adapter: adapter.MakeImageAdapter()}, ) if err != nil { adapter.InternalServerError(w, "Format blocks", err) return } user := session.GetUser(ctx) roleText := zn.Zettel.Meta.GetDefault(meta.KeyRole, "*") tags := buildTagInfos(zn.Zettel.Meta) getTitle := makeGetTitle(ctx, getMeta, &langOption) extURL, hasExtURL := zn.Zettel.Meta.Get(meta.KeyURL) backLinks := formatBackLinks(zn.InhMeta, getTitle) var base baseData te.makeBaseData(ctx, langOption.Value, textTitle, user, &base) base.MetaHeader = metaHeader canCopy := base.CanCreate && !zn.Zettel.Content.IsBinary() te.renderTemplate(ctx, w, id.DetailTemplateZid, &base, struct { HTMLTitle string CanWrite bool EditURL string Zid string InfoURL string RoleText string RoleURL string HasTags bool Tags []simpleLink CanCopy bool CopyURL string CanNew bool NewURL string CanFolge bool FolgeURL string FolgeRefs string PrecursorRefs string HasExtURL bool ExtURL string ExtNewWindow string Content string HasBackLinks bool BackLinks []simpleLink }{ HTMLTitle: htmlTitle, CanWrite: te.canWrite(ctx, user, zn.Zettel), EditURL: adapter.NewURLBuilder('e').SetZid(zid).String(), Zid: zid.String(), InfoURL: adapter.NewURLBuilder('i').SetZid(zid).String(), RoleText: roleText, RoleURL: adapter.NewURLBuilder('h').AppendQuery("role", roleText).String(), HasTags: len(tags) > 0, Tags: tags, CanCopy: canCopy, CopyURL: adapter.NewURLBuilder('c').SetZid(zid).String(), CanNew: canCopy && roleText == meta.ValueRoleNewTemplate, NewURL: adapter.NewURLBuilder('n').SetZid(zid).String(), CanFolge: base.CanCreate && !zn.Zettel.Content.IsBinary(), FolgeURL: adapter.NewURLBuilder('f').SetZid(zid).String(), FolgeRefs: formatMetaKey(zn.InhMeta, meta.KeyFolge, getTitle), PrecursorRefs: formatMetaKey(zn.InhMeta, meta.KeyPrecursor, getTitle), ExtURL: extURL, HasExtURL: hasExtURL, ExtNewWindow: htmlAttrNewWindow(newWindow && hasExtURL), Content: htmlContent, HasBackLinks: len(backLinks) > 0, BackLinks: backLinks, }) } } func formatBlocks( bs ast.BlockSlice, format string, options ...encoder.Option) (string, error) { enc := encoder.Create(format, options...) 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, options ...encoder.Option) (string, error) { enc := encoder.Create(format, options...) 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 buildTagInfos(m *meta.Meta) []simpleLink { var tagInfos []simpleLink if tags, ok := m.GetList(meta.KeyTags); ok { tagInfos = make([]simpleLink, 0, len(tags)) ub := adapter.NewURLBuilder('h') for _, t := range tags { // Cast to template.HTML is ok, because "t" is a tag name // and contains only legal characters by construction. tagInfos = append( tagInfos, simpleLink{Text: t, URL: ub.AppendQuery("tags", t).String()}) ub.ClearQuery() } } return tagInfos } func formatMetaKey(m *meta.Meta, key string, getTitle getTitleFunc) string { if _, ok := m.Get(key); ok { var buf bytes.Buffer writeHTMLMetaValue(&buf, m, key, getTitle, nil) return buf.String() } return "" } func 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 := adapter.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 | //----------------------------------------------------------------------------- // 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 webui provides wet-UI handlers for web requests. package webui import ( "context" "net/http" "zettelstore.de/z/config/runtime" "zettelstore.de/z/domain/id" "zettelstore.de/z/domain/meta" ) 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 MakeGetRootHandler( s getRootStore, startNotFound, startFound http.HandlerFunc) http.HandlerFunc { return func(w http.ResponseWriter, r *http.Request) { if r.URL.Path != "/" { http.NotFound(w, r) return } startID := runtime.GetStart() if startID.IsValid() { if _, err := s.GetMeta(r.Context(), startID); err == nil { r.URL.Path = "/" + startID.String() startFound(w, r) return } } startNotFound(w, r) } } |
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 206 207 208 | //----------------------------------------------------------------------------- // 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 wet-UI handlers for web requests. package webui import ( "context" "fmt" "io" "net/url" "time" "zettelstore.de/z/domain/id" "zettelstore.de/z/domain/meta" "zettelstore.de/z/encoder" "zettelstore.de/z/index" "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 writeHTMLMetaValue(w io.Writer, m *meta.Meta, key string, getTitle getTitleFunc, option encoder.Option) { switch kt := m.Type(key); kt { case meta.TypeBool: 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: writeIdentifier(w, m.GetDefault(key, "???i"), getTitle) case meta.TypeIDSet: if l, ok := m.GetList(key); ok { 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 { 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: writeWord(w, key, m.GetDefault(key, "???w")) case meta.TypeWordSet: if l, ok := m.GetList(key); ok { writeWordSet(w, key, l) } case meta.TypeZettelmarkup: writeZettelmarkup(w, m.GetDefault(key, "???z"), option) default: strfun.HTMLEscape(w, m.GetDefault(key, "???w"), false) fmt.Fprintf(w, " <b>(Unhandled type: %v, key: %v)</b>", kt, key) } } func writeHTMLBool(w io.Writer, key string, val bool) { if val { writeLink(w, key, "True") } else { writeLink(w, key, "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 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>", adapter.NewURLBuilder('h').SetZid(zid), zid, ) } else { fmt.Fprintf( w, "<a href=\"%v\" title=\"%v\">%v</a>", adapter.NewURLBuilder('h').SetZid(zid), title, zid, ) } case found == 0: fmt.Fprintf(w, "<s>%v</s>", val) case found < 0: io.WriteString(w, val) } } func writeIdentifierSet(w io.Writer, vals []string, getTitle func(id.Zid, string) (string, int)) { for i, val := range vals { if i > 0 { w.Write(space) } 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 writeTagSet(w io.Writer, key string, tags []string) { for i, tag := range tags { if i > 0 { w.Write(space) } writeLink(w, key, 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 writeWord(w io.Writer, key, word string) { writeLink(w, key, word) } func writeWordSet(w io.Writer, key string, words []string) { for i, word := range words { if i > 0 { w.Write(space) } writeWord(w, key, word) } } func writeZettelmarkup(w io.Writer, val string, option encoder.Option) { astTitle := parser.ParseTitle(val) title, err := adapter.FormatInlines(astTitle, "html", option) if err != nil { strfun.HTMLEscape(w, val, false) return } io.WriteString(w, title) } func writeLink(w io.Writer, key, value string) { fmt.Fprintf( w, "<a href=\"%v?%v=%v\">", adapter.NewURLBuilder('h'), url.QueryEscape(key), url.QueryEscape(value)) strfun.HTMLEscape(w, value, false) io.WriteString(w, "</a>") } type getTitleFunc func(id.Zid, string) (string, int) func makeGetTitle(ctx context.Context, getMeta usecase.GetMeta, langOption encoder.Option) getTitleFunc { return func(zid id.Zid, format string) (string, int) { m, err := getMeta.Run(index.NoEnrichContext(ctx), zid) if err != nil { if place.IsErrNotAllowed(err) { return "", -1 } return "", 0 } astTitle := parser.ParseTitle(m.GetDefault(meta.KeyTitle, "")) title, err := adapter.FormatInlines(astTitle, format, langOption) 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 | //----------------------------------------------------------------------------- // 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 wet-UI handlers for web requests. package webui import ( "context" "net/http" "net/url" "sort" "strconv" "zettelstore.de/z/config/runtime" "zettelstore.de/z/domain/id" "zettelstore.de/z/domain/meta" "zettelstore.de/z/encoder" "zettelstore.de/z/index" "zettelstore.de/z/parser" "zettelstore.de/z/place" "zettelstore.de/z/usecase" "zettelstore.de/z/web/adapter" "zettelstore.de/z/web/session" ) // MakeListHTMLMetaHandler creates a HTTP handler for rendering the list of zettel as HTML. func MakeListHTMLMetaHandler( te *TemplateEngine, listMeta usecase.ListMeta) http.HandlerFunc { return func(w http.ResponseWriter, r *http.Request) { renderWebUIZettelList(w, r, te, listMeta) } } // MakeWebUIListsHandler creates a new HTTP handler for the use case "list some zettel". func MakeWebUIListsHandler( te *TemplateEngine, listMeta usecase.ListMeta, listRole usecase.ListRole, listTags usecase.ListTags, ) 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 } switch zid { case 1: renderWebUIZettelList(w, r, te, listMeta) case 2: renderWebUIRolesList(w, r, te, listRole) case 3: renderWebUITagsList(w, r, te, listTags) } } } func renderWebUIZettelList( w http.ResponseWriter, r *http.Request, te *TemplateEngine, listMeta usecase.ListMeta) { query := r.URL.Query() filter, sorter := adapter.GetFilterSorter(query, false) ctx := r.Context() renderWebUIMetaList( ctx, w, te, sorter, func(sorter *place.Sorter) ([]*meta.Meta, error) { if filter == nil && (sorter == nil || sorter.Order == "") { ctx = index.NoEnrichContext(ctx) } return listMeta.Run(ctx, filter, sorter) }, func(offset int) string { return newPageURL('h', query, offset, "_offset", "_limit") }) } type roleInfo struct { Text string URL string } func renderWebUIRolesList( w http.ResponseWriter, r *http.Request, te *TemplateEngine, 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 _, r := range roleList { roleInfos = append( roleInfos, roleInfo{r, adapter.NewURLBuilder('h').AppendQuery("role", r).String()}) } user := session.GetUser(ctx) var base baseData te.makeBaseData(ctx, runtime.GetDefaultLang(), runtime.GetSiteName(), user, &base) te.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 renderWebUITagsList( w http.ResponseWriter, r *http.Request, te *TemplateEngine, listTags usecase.ListTags, ) { ctx := r.Context() iMinCount, _ := strconv.Atoi(r.URL.Query().Get("min")) tagData, err := listTags.Run(ctx, iMinCount) if err != nil { adapter.ReportUsecaseError(w, err) return } user := session.GetUser(ctx) tagsList := make([]tagInfo, 0, len(tagData)) countMap := make(map[int]int) baseTagListURL := adapter.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 te.makeBaseData(ctx, runtime.GetDefaultLang(), runtime.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}) } te.renderTemplate(ctx, w, id.TagsTemplateZid, &base, struct { MinCounts []countInfo Tags []tagInfo }{ MinCounts: minCounts, Tags: tagsList, }) } // MakeSearchHandler creates a new HTTP handler for the use case "search". func MakeSearchHandler( te *TemplateEngine, search usecase.Search, getMeta usecase.GetMeta, getZettel usecase.GetZettel, ) http.HandlerFunc { return func(w http.ResponseWriter, r *http.Request) { query := r.URL.Query() filter, sorter := adapter.GetFilterSorter(query, true) if filter == nil || len(filter.Expr) == 0 { http.Redirect(w, r, adapter.NewURLBuilder('h').String(), http.StatusFound) return } ctx := r.Context() renderWebUIMetaList( ctx, w, te, sorter, func(sorter *place.Sorter) ([]*meta.Meta, error) { if filter == nil && (sorter == nil || sorter.Order == "") { ctx = index.NoEnrichContext(ctx) } return search.Run(ctx, filter, sorter) }, func(offset int) string { return newPageURL('s', query, offset, "offset", "limit") }) } } func renderWebUIMetaList( ctx context.Context, w http.ResponseWriter, te *TemplateEngine, sorter *place.Sorter, ucMetaList func(sorter *place.Sorter) ([]*meta.Meta, error), pageURL func(int) string) { var metaList []*meta.Meta var err error var prevURL, nextURL string if lps := runtime.GetListPageSize(); lps > 0 { sorter = place.EnsureSorter(sorter) if sorter.Limit < lps { sorter.Limit = lps + 1 } metaList, err = ucMetaList(sorter) if err != nil { adapter.ReportUsecaseError(w, err) return } if offset := sorter.Offset; offset > 0 { offset -= lps if offset < 0 { offset = 0 } prevURL = pageURL(offset) } if len(metaList) >= sorter.Limit { nextURL = pageURL(sorter.Offset + lps) metaList = metaList[:len(metaList)-1] } } else { metaList, err = ucMetaList(sorter) if err != nil { adapter.ReportUsecaseError(w, err) return } } user := session.GetUser(ctx) metas, err := buildHTMLMetaList(metaList) if err != nil { adapter.InternalServerError(w, "Build HTML meta list", err) return } var base baseData te.makeBaseData(ctx, runtime.GetDefaultLang(), runtime.GetSiteName(), user, &base) te.renderTemplate(ctx, w, id.ListTemplateZid, &base, struct { Title string Metas []metaInfo HasPrevNext bool HasPrev bool PrevURL string HasNext bool NextURL string }{ Title: base.Title, Metas: metas, HasPrevNext: len(prevURL) > 0 || len(nextURL) > 0, HasPrev: len(prevURL) > 0, PrevURL: prevURL, HasNext: len(nextURL) > 0, NextURL: nextURL, }) } func newPageURL(key byte, query url.Values, offset int, offsetKey, limitKey string) string { urlBuilder := adapter.NewURLBuilder(key) for key, values := range query { if key != offsetKey && key != limitKey { for _, val := range values { urlBuilder.AppendQuery(key, val) } } } if offset > 0 { urlBuilder.AppendQuery(offsetKey, strconv.Itoa(offset)) } return urlBuilder.String() } type metaInfo struct { Title string URL string } // buildHTMLMetaList builds a zettel list based on a meta list for HTML rendering. func buildHTMLMetaList(metaList []*meta.Meta) ([]metaInfo, error) { defaultLang := runtime.GetDefaultLang() langOption := encoder.StringOption{Key: "lang", Value: ""} metas := make([]metaInfo, 0, len(metaList)) for _, m := range metaList { if lang, ok := m.Get(meta.KeyLang); ok { langOption.Value = lang } else { langOption.Value = defaultLang } title, _ := m.Get(meta.KeyTitle) htmlTitle, err := adapter.FormatInlines( parser.ParseTitle(title), "html", &langOption) if err != nil { return nil, err } metas = append(metas, metaInfo{ Title: htmlTitle, URL: adapter.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 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 | //----------------------------------------------------------------------------- // 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 webui provides wet-UI handlers for web requests. package webui import ( "context" "fmt" "net/http" "time" "zettelstore.de/z/auth/token" "zettelstore.de/z/config/runtime" "zettelstore.de/z/config/startup" "zettelstore.de/z/domain/id" "zettelstore.de/z/usecase" "zettelstore.de/z/web/adapter" "zettelstore.de/z/web/session" ) // MakeGetLoginHandler creates a new HTTP handler to display the HTML login view. func MakeGetLoginHandler(te *TemplateEngine) http.HandlerFunc { return func(w http.ResponseWriter, r *http.Request) { renderLoginForm(session.ClearToken(r.Context(), w), w, te, false) } } func renderLoginForm(ctx context.Context, w http.ResponseWriter, te *TemplateEngine, retry bool) { var base baseData te.makeBaseData(ctx, runtime.GetDefaultLang(), "Login", nil, &base) te.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 MakePostLoginHandlerHTML(te *TemplateEngine, auth usecase.Authenticate) http.HandlerFunc { return func(w http.ResponseWriter, r *http.Request) { if !startup.WithAuth() { http.Redirect(w, r, adapter.NewURLBuilder('/').String(), http.StatusFound) return } htmlDur, _ := startup.TokenLifetime() authenticateViaHTML(te, auth, w, r, htmlDur) } } func authenticateViaHTML( te *TemplateEngine, auth usecase.Authenticate, w http.ResponseWriter, r *http.Request, authDuration time.Duration, ) { ident, cred, ok := adapter.GetCredentialsViaForm(r) if !ok { adapter.BadRequest(w, "Unable to read login form") return } ctx := r.Context() token, err := auth.Run(ctx, ident, cred, authDuration, token.KindHTML) if err != nil { adapter.ReportUsecaseError(w, err) return } if token == nil { renderLoginForm(session.ClearToken(ctx, w), w, te, true) return } session.SetToken(w, token, authDuration) http.Redirect(w, r, adapter.NewURLBuilder('/').String(), http.StatusFound) } // MakeGetLogoutHandler creates a new HTTP handler to log out the current user func MakeGetLogoutHandler() http.HandlerFunc { return func(w http.ResponseWriter, r *http.Request) { if format := adapter.GetFormat(r, r.URL.Query(), "html"); format != "html" { adapter.BadRequest(w, fmt.Sprintf("Logout not possible in format %q", format)) return } session.ClearToken(r.Context(), w) http.Redirect(w, r, adapter.NewURLBuilder('/').String(), http.StatusFound) } } |
Deleted web/adapter/webui/meta.go.
|
| < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < |
Deleted web/adapter/webui/meta_test.go.
|
| < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < |
Added web/adapter/webui/reload.go.
> > > > > > > > > > > > > > > > > > > > > > > | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 | //----------------------------------------------------------------------------- // Copyright (c) 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 webui provides wet-UI handlers for web requests. package webui import ( "net/http" "zettelstore.de/z/web/adapter" ) // ReloadHandlerHTML creates a new HTTP handler for the use case "reload". func ReloadHandlerHTML(w http.ResponseWriter, r *http.Request) { http.Redirect(w, r, adapter.NewURLBuilder('/').String(), http.StatusFound) } |
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 | //----------------------------------------------------------------------------- // 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 webui provides wet-UI handlers for web requests. package webui import ( "fmt" "net/http" "strings" "zettelstore.de/z/config/runtime" "zettelstore.de/z/domain/id" "zettelstore.de/z/domain/meta" "zettelstore.de/z/usecase" "zettelstore.de/z/web/adapter" "zettelstore.de/z/web/session" ) // MakeGetRenameZettelHandler creates a new HTTP handler to display the // HTML rename view of a zettel. func MakeGetRenameZettelHandler( te *TemplateEngine, 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() m, err := getMeta.Run(ctx, zid) if err != nil { adapter.ReportUsecaseError(w, err) return } if format := adapter.GetFormat(r, r.URL.Query(), "html"); format != "html" { adapter.BadRequest(w, fmt.Sprintf("Rename zettel %q not possible in format %q", zid.String(), format)) return } user := session.GetUser(ctx) var base baseData te.makeBaseData(ctx, runtime.GetLang(m), "Rename Zettel "+zid.String(), user, &base) te.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 MakePostRenameZettelHandler(renameZettel usecase.RenameZettel) http.HandlerFunc { return func(w http.ResponseWriter, r *http.Request) { curZid, err := id.Parse(r.URL.Path[1:]) if err != nil { http.NotFound(w, r) return } if err := r.ParseForm(); err != nil { adapter.BadRequest(w, "Unable to read rename zettel form") return } if formCurZid, err := id.Parse( r.PostFormValue("curzid")); err != nil || formCurZid != curZid { adapter.BadRequest(w, "Invalid value for current zettel id in form") return } newZid, err := id.Parse(strings.TrimSpace(r.PostFormValue("newzid"))) if err != nil { adapter.BadRequest(w, fmt.Sprintf("Invalid new zettel id %q", newZid.String())) return } if err := renameZettel.Run(r.Context(), curZid, newZid); err != nil { adapter.ReportUsecaseError(w, err) return } http.Redirect( w, r, adapter.NewURLBuilder('h').SetZid(newZid).String(), http.StatusFound) } } |
Deleted web/adapter/webui/response.go.
|
| < < < < < < < < < < < < < < < < < < < < < < < < < < |
Deleted web/adapter/webui/sxn_code.go.
|
| < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < |
Changes to web/adapter/webui/template.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 | //----------------------------------------------------------------------------- // 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 wet-UI handlers for web requests. package webui import ( "bytes" "context" "net/http" "sync" "zettelstore.de/z/auth/policy" "zettelstore.de/z/auth/token" "zettelstore.de/z/config/runtime" "zettelstore.de/z/config/startup" "zettelstore.de/z/domain" "zettelstore.de/z/domain/id" "zettelstore.de/z/domain/meta" "zettelstore.de/z/encoder" "zettelstore.de/z/index" "zettelstore.de/z/input" "zettelstore.de/z/parser" "zettelstore.de/z/place" "zettelstore.de/z/template" "zettelstore.de/z/web/adapter" "zettelstore.de/z/web/session" ) type templatePlace 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) SelectMeta(ctx context.Context, f *place.Filter, s *place.Sorter) ([]*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 } // TemplateEngine is the way to render HTML templates. type TemplateEngine struct { place templatePlace templateCache map[id.Zid]*template.Template mxCache sync.RWMutex policy policy.Policy stylesheetURL string homeURL string listZettelURL string listRolesURL string listTagsURL string withAuth bool loginURL string reloadURL string searchURL string } // NewTemplateEngine creates a new TemplateEngine. func NewTemplateEngine(mgr place.Manager, pol policy.Policy) *TemplateEngine { te := &TemplateEngine{ place: mgr, policy: pol, stylesheetURL: adapter.NewURLBuilder('z').SetZid( id.BaseCSSZid).AppendQuery("_format", "raw").AppendQuery( "_part", "content").String(), homeURL: adapter.NewURLBuilder('/').String(), listZettelURL: adapter.NewURLBuilder('h').String(), listRolesURL: adapter.NewURLBuilder('k').SetZid(2).String(), listTagsURL: adapter.NewURLBuilder('k').SetZid(3).String(), withAuth: startup.WithAuth(), loginURL: adapter.NewURLBuilder('a').String(), reloadURL: adapter.NewURLBuilder('c').AppendQuery("_format", "html").String(), searchURL: adapter.NewURLBuilder('s').String(), } te.observe(place.ChangeInfo{Reason: place.OnReload, Zid: id.Invalid}) mgr.RegisterObserver(te.observe) return te } func (te *TemplateEngine) observe(ci place.ChangeInfo) { te.mxCache.Lock() if ci.Reason == place.OnReload || ci.Zid == id.BaseTemplateZid { te.templateCache = make(map[id.Zid]*template.Template, len(te.templateCache)) } else { delete(te.templateCache, ci.Zid) } te.mxCache.Unlock() } func (te *TemplateEngine) cacheSetTemplate(zid id.Zid, t *template.Template) { te.mxCache.Lock() te.templateCache[zid] = t te.mxCache.Unlock() } func (te *TemplateEngine) cacheGetTemplate(zid id.Zid) (*template.Template, bool) { te.mxCache.RLock() t, ok := te.templateCache[zid] te.mxCache.RUnlock() return t, ok } func (te *TemplateEngine) canCreate(ctx context.Context, user *meta.Meta) bool { m := meta.New(id.Invalid) return te.policy.CanCreate(user, m) && te.place.CanCreateZettel(ctx) } func (te *TemplateEngine) canWrite( ctx context.Context, user *meta.Meta, zettel domain.Zettel) bool { return te.policy.CanWrite(user, zettel.Meta, zettel.Meta) && te.place.CanUpdateZettel(ctx, zettel) } func (te *TemplateEngine) canRename( ctx context.Context, user *meta.Meta, m *meta.Meta) bool { return te.policy.CanRename(user, m) && te.place.AllowRenameZettel(ctx, m.Zid) } func (te *TemplateEngine) canDelete( ctx context.Context, user *meta.Meta, m *meta.Meta) bool { return te.policy.CanDelete(user, m) && te.place.CanDeleteZettel(ctx, m.Zid) } func (te *TemplateEngine) getTemplate( ctx context.Context, templateID id.Zid) (*template.Template, error) { if t, ok := te.cacheGetTemplate(templateID); ok { return t, nil } realTemplateZettel, err := te.place.GetZettel(ctx, templateID) if err != nil { return nil, err } t, err := template.ParseString(realTemplateZettel.Content.AsString(), nil) if err == nil { te.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 ListZettelURL string ListRolesURL string ListTagsURL string CanCreate bool NewZettelURL string NewZettelLinks []simpleLink WithAuth bool UserIsValid bool UserZettelURL string UserIdent string UserLogoutURL string LoginURL string CanReload bool ReloadURL string SearchURL string Content string FooterHTML string } func (te *TemplateEngine) makeBaseData( ctx context.Context, lang string, title string, user *meta.Meta, data *baseData) { var ( newZettelLinks []simpleLink userZettelURL string userIdent string userLogoutURL string ) canCreate := te.canCreate(ctx, user) if canCreate { newZettelLinks = te.fetchNewTemplates(ctx, user) } userIsValid := user != nil if userIsValid { userZettelURL = adapter.NewURLBuilder('h').SetZid(user.Zid).String() userIdent = user.GetDefault(meta.KeyUserID, "") userLogoutURL = adapter.NewURLBuilder('a').SetZid(user.Zid).String() } data.Lang = lang data.StylesheetURL = te.stylesheetURL data.Title = title data.HomeURL = te.homeURL data.ListZettelURL = te.listZettelURL data.ListRolesURL = te.listRolesURL data.ListTagsURL = te.listTagsURL data.CanCreate = canCreate data.NewZettelLinks = newZettelLinks data.WithAuth = te.withAuth data.UserIsValid = userIsValid data.UserZettelURL = userZettelURL data.UserIdent = userIdent data.UserLogoutURL = userLogoutURL data.LoginURL = te.loginURL data.CanReload = te.policy.CanReload(user) data.ReloadURL = te.reloadURL data.SearchURL = te.searchURL data.FooterHTML = runtime.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 "" } var templatePlaceFilter = &place.Filter{ Expr: place.FilterExpr{ meta.KeyRole: []string{meta.ValueRoleNewTemplate}, }, } var templatePlaceSorter = &place.Sorter{ Order: "id", Descending: false, Offset: -1, Limit: 31, // Just to be one the safe side... } func (te *TemplateEngine) fetchNewTemplates(ctx context.Context, user *meta.Meta) []simpleLink { ctx = index.NoEnrichContext(ctx) templateList, err := te.place.SelectMeta(ctx, templatePlaceFilter, templatePlaceSorter) if err != nil { return nil } result := make([]simpleLink, 0, len(templateList)) for _, m := range templateList { if te.policy.CanRead(user, m) { title := runtime.GetTitle(m) langOption := encoder.StringOption{Key: "lang", Value: runtime.GetLang(m)} astTitle := parser.ParseInlines( input.NewInput(runtime.GetTitle(m)), meta.ValueSyntaxZmk) menuTitle, err := adapter.FormatInlines(astTitle, "html", &langOption) if err != nil { menuTitle, err = adapter.FormatInlines(astTitle, "text", &langOption) if err != nil { menuTitle = title } } result = append(result, simpleLink{ Text: menuTitle, URL: adapter.NewURLBuilder('n').SetZid(m.Zid).String(), }) } } return result } func (te *TemplateEngine) renderTemplate( ctx context.Context, w http.ResponseWriter, templateID id.Zid, base *baseData, data interface{}) { bt, err := te.getTemplate(ctx, id.BaseTemplateZid) if err != nil { adapter.InternalServerError(w, "Unable to get base template", err) return } t, err := te.getTemplate(ctx, templateID) if err != nil { adapter.InternalServerError(w, "Unable to get template", err) return } if user := session.GetUser(ctx); user != nil { htmlLifetime, _ := startup.TokenLifetime() t, err := token.GetToken(user, htmlLifetime, token.KindHTML) if err == nil { session.SetToken(w, t, htmlLifetime) } } var content bytes.Buffer err = t.Render(&content, data) base.Content = content.String() w.Header().Set("Content-Type", "text/html; charset=utf-8") err = bt.Render(w, base) if err != nil { adapter.InternalServerError(w, "Unable to render template", err) } } |
Deleted web/adapter/webui/webui.go.
|
| < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < |
Deleted web/content/content.go.
|
| < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < |
Deleted web/content/content_test.go.
|
| < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < |
Added web/router/router.go.
> > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 | //----------------------------------------------------------------------------- // 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 router provides a router for web requests. package router import ( "net/http" "regexp" ) type ( methodHandler map[string]http.Handler routingTable map[byte]methodHandler ) // Router handles all routing for zettelstore. type Router struct { minKey byte maxKey byte reURL *regexp.Regexp tables [2]routingTable mux *http.ServeMux } const ( indexList = 0 indexZettel = 1 ) // NewRouter creates a new, empty router with the given root handler. func NewRouter() *Router { router := &Router{ minKey: 255, maxKey: 0, reURL: regexp.MustCompile("^$"), mux: http.NewServeMux(), } router.tables[indexList] = make(routingTable) router.tables[indexZettel] = make(routingTable) return router } func (rt *Router) addRoute(key byte, httpMethod string, handler http.Handler, index int) { // 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 := rt.tables[index][key] if !hasKey { mh = make(methodHandler) rt.tables[index][key] = mh } mh[httpMethod] = handler if httpMethod == http.MethodGet { if _, hasHead := rt.tables[index][key][http.MethodHead]; !hasHead { rt.tables[index][key][http.MethodHead] = handler } } } // AddListRoute adds a route for the given key and HTTP method to work with a list. func (rt *Router) AddListRoute(key byte, httpMethod string, handler http.Handler) { rt.addRoute(key, httpMethod, handler, indexList) } // AddZettelRoute adds a route for the given key and HTTP method to work with a zettel. func (rt *Router) AddZettelRoute(key byte, httpMethod string, handler http.Handler) { rt.addRoute(key, httpMethod, handler, indexZettel) } // Handle registers the handler for the given pattern. If a handler already exists for pattern, Handle panics. func (rt *Router) Handle(pattern string, handler http.Handler) { rt.mux.Handle(pattern, handler) } func (rt *Router) ServeHTTP(w http.ResponseWriter, r *http.Request) { match := rt.reURL.FindStringSubmatch(r.URL.Path) if len(match) == 3 { key := match[1][0] index := indexZettel if len(match[2]) == 0 { index = indexList } if mh, ok := rt.tables[index][key]; ok { if handler, ok := mh[r.Method]; ok { r.URL.Path = "/" + match[2] handler.ServeHTTP(w, r) return } http.Error(w, http.StatusText(http.StatusMethodNotAllowed), http.StatusMethodNotAllowed) return } } rt.mux.ServeHTTP(w, r) } |
Deleted web/server/impl/http.go.
|
| < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < |
Deleted web/server/impl/impl.go.
|
| < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < |
Deleted web/server/impl/router.go.
|
| < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < |
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 | //----------------------------------------------------------------------------- // 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 server provides a web server. package server import ( "context" "log" "net/http" "os" "os/signal" "syscall" "time" ) // Server timeout values const ( shutdownTimeout = 5 * time.Second readTimeout = 5 * time.Second writeTimeout = 10 * time.Second idleTimeout = 120 * time.Second ) // Server is a HTTP server. type Server struct { *http.Server waitShutdown chan struct{} } // New creates a new HTTP server object. func New(addr string, handler http.Handler) *Server { if addr == "" { addr = ":http" } srv := &Server{ Server: &http.Server{ Addr: addr, Handler: handler, // See: https://blog.cloudflare.com/exposing-go-on-the-internet/ ReadTimeout: readTimeout, WriteTimeout: writeTimeout, IdleTimeout: idleTimeout, }, waitShutdown: make(chan struct{}), } return srv } // 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 *Server) SetDebug() { srv.ReadTimeout = 0 srv.WriteTimeout = 0 srv.IdleTimeout = 0 } // Run starts the web server and wait for its completion. func (srv *Server) Run() error { waitInterrupt := make(chan os.Signal) waitError := make(chan error) signal.Notify(waitInterrupt, os.Interrupt, syscall.SIGTERM) go func() { select { case <-waitInterrupt: case <-srv.waitShutdown: } ctx, cancel := context.WithTimeout(context.Background(), shutdownTimeout) defer cancel() log.Println("Stopping Zettelstore...") if err := srv.Shutdown(ctx); err != nil { waitError <- err return } waitError <- nil }() if err := srv.ListenAndServe(); err != nil && err != http.ErrServerClosed { return err } return <-waitError } // Stop the web server. func (srv *Server) Stop() { close(srv.waitShutdown) } |
Added web/session/session.go.
> > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 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 | //----------------------------------------------------------------------------- // 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 session provides utilities for using sessions. package session import ( "context" "net/http" "strings" "time" "zettelstore.de/z/auth/token" "zettelstore.de/z/config/startup" "zettelstore.de/z/domain/meta" "zettelstore.de/z/usecase" ) const sessionName = "zsession" // SetToken sets the session cookie for later user identification. func SetToken(w http.ResponseWriter, token []byte, d time.Duration) { cookie := http.Cookie{ Name: sessionName, Value: string(token), Path: startup.URLPrefix(), Secure: startup.SecureCookie(), HttpOnly: true, SameSite: http.SameSiteStrictMode, } if startup.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 ClearToken(ctx context.Context, w http.ResponseWriter) context.Context { if w != nil { SetToken(w, nil, 0) } return updateContext(ctx, nil, nil) } // Handler enriches the request context with optional user information. type Handler struct { next http.Handler getUserByZid usecase.GetUserByZid } // NewHandler creates a new handler. func NewHandler(next http.Handler, getUserByZid usecase.GetUserByZid) *Handler { return &Handler{ next: next, getUserByZid: getUserByZid, } } type ctxKeyType struct{} var ctxKey ctxKeyType // AuthData stores all relevant authentication data for a context. type AuthData struct { User *meta.Meta Token []byte Now time.Time Issued time.Time Expires time.Time } // GetAuthData returns the full authentication data from the context. func GetAuthData(ctx context.Context) *AuthData { data, ok := ctx.Value(ctxKey).(*AuthData) if ok { return data } return nil } // GetUser returns the user meta data from the context, if there is one. Else return nil. func GetUser(ctx context.Context) *meta.Meta { if data := GetAuthData(ctx); data != nil { return data.User } return nil } func updateContext( ctx context.Context, user *meta.Meta, data *token.Data) context.Context { if data == nil { return context.WithValue(ctx, ctxKey, &AuthData{User: user}) } return context.WithValue( ctx, ctxKey, &AuthData{ User: user, Token: data.Token, Now: data.Now, Issued: data.Issued, Expires: data.Expires, }) } // ServeHTTP processes one HTTP request. func (h *Handler) ServeHTTP(w http.ResponseWriter, r *http.Request) { k := token.KindJSON t := getHeaderToken(r) if t == nil { k = token.KindHTML t = getSessionToken(r) } if t == nil { h.next.ServeHTTP(w, r) return } tokenData, err := token.CheckToken(t, k) if err != nil { h.next.ServeHTTP(w, r) return } ctx := r.Context() user, err := h.getUserByZid.Run(ctx, tokenData.Zid, tokenData.Ident) if err != nil { h.next.ServeHTTP(w, r) return } h.next.ServeHTTP(w, r.WithContext(updateContext(ctx, user, &tokenData))) } func getSessionToken(r *http.Request) []byte { cookie, err := r.Cookie(sessionName) if err != nil { return nil } return []byte(cookie.Value) } func getHeaderToken(r *http.Request) []byte { h := r.Header["Authorization"] if h == nil { return nil } // “Multiple message-header fields with the same field-name MAY be // present in a message if and only if the entire field-value for that // header field is defined as a comma-separated list.” // — “Hypertext Transfer Protocol” RFC 2616, subsection 4.2 auth := strings.Join(h, ", ") const prefix = "Bearer " // RFC 2617, subsection 1.2 defines the scheme token as case-insensitive. if len(auth) < len(prefix) || !strings.EqualFold(auth[:len(prefix)], prefix) { return nil } return []byte(auth[len(prefix):]) } |
Deleted www/build.md.
|
| < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < |
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 | <title>Change Log</title> <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 <a href="https://forum.zettelkasten.de/discussion/996/definition-folgezettel">Folgezettel</a>. 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 enviromental 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 <a href="https://mustache.github.io/">Mustache</a> syntax instead of previously used <a href="https://golang.org/pkg/html/template/">Go template</a> 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 backlinks, they are shown at the botton of the page (ldquo;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 <a href="https://zettelkasten.de/posts/tags/folgezettel/">Folgezettel</a>. 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 <a href="https://joinup.ec.europa.eu/collection/eupl">European Union Public Licence</a> (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> * (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 | <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. | | | | | | | < | < | < | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 | <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 <a href="/manual/">manual</a> to know how to start and use it. <h2>ZIP-ped Executables</h2> Build: <code>v0.0.9</code> (2021-01-29). * [/uv/zettelstore.zip|Linux] (amd64) * [/uv/zettelstore-arm6.zip|Linux] (arm6, e.g. Raspberry Pi) * [/uv/zettelstore.exe.zip|Windows] (amd64) * [/uv/iZettelstore.zip|macOs] (amd64) Unzip the appropriate file, install and execute Zettelstore according to the manual. <h2>Zettel for the manual</h2> As a starter, you can download the zettel for the manual [/uv/manual.zip|here]. Just unzip the file and put it into your zettel folder. |
Changes to www/impri.wiki.
1 2 3 4 5 | <title>Imprint & Privacy</title> <h1>Imprint</h1> Detlef Stern<br> Max-Planck-Str. 39<br> 74081 Heilbronn<br> | > | > > | > | | > | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 | <title>Imprint & Privacy</title> <h1>Imprint</h1> <p> Detlef Stern<br> Max-Planck-Str. 39<br> 74081 Heilbronn<br> Phone: +49 (173) 4905619<br> Mail: ds (at) zettelstore.de </p> <h1>Privacy</h1> <p> If you do not log into this site, or login as the user "anonymous", the only personal data this web service will process is your IP adress. It will be used to send the data of the website you requested to you and to mitigate possible attacks against this website. </p> <p> This website is hosted by <a href="https://ionos.de">1&1 IONOS SE</a>. According to <a href="https://www.ionos.de/hilfe/datenschutz/datenverarbeitung-von-webseitenbesuchern-ihres-11-ionos-produktes/andere-11-ionos-produkte/">their information</a>, no processing of personal data is done by them. </p> |
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 | <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 [href="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 <a href="/manual/">manual</a>. 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.9 (2021-01-29)</h3> * [./download.wiki|Download] * [./changes.wiki#0_0_9|Change Summary] * [./plan.wiki|Limitations and Planned Improvements] * [/dir?ci=trunk|Source Code] (mirrored on <a href="https://github.com/zettelstore/zettelstore">GitHub</a>) * [/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 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 | <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. * The search function uses only the metadata of a zettel, not its content. * … <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. * … <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 <a href="https://pandoc.org">pandoc's</a> 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.
|
| < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < |